feat: full-platform STT API (v2.3 consensus plan) #1
@@ -109,7 +109,7 @@
|
|||||||
"execution_count": null,
|
"execution_count": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": "# 7) 실전 전사 — GPU 자동 감지 + 모델 다운로드 (large-v3-turbo)\n# 첫 실행 시 Hugging Face에서 모델을 다운로드합니다 (turbo ≈ 1.6GB, 1~2분)\n# CUDA 13 환경에서 CTranslate2가 GPU를 못 열면 자동으로 CPU로 폴백합니다.\nimport subprocess, json\nr = subprocess.run(\n ['luke-scribe', 'transcribe', 'samples/colab-ko-en.mp3', '--language', 'ko', '--device', 'auto'],\n capture_output=True, text=True,\n)\nprint(r.stdout[-2500:] if r.stdout else '')\nprint(r.stderr[-800:] if r.stderr else '')\n"
|
"source": "# 7) 실전 전사 — GPU 자동 감지 + 모델 다운로드 (large-v3-turbo)\n# 첫 실행 시 Hugging Face에서 모델을 다운로드합니다 (turbo ≈ 1.6GB, 1~2분)\n# v0.1: 'cuda:N' device를 분리해 CTranslate2 계약에 맞게 전달하도록 수정됨\n# (기존: unsupported device cuda:0). 그래도 실패하면 CPU 폴백 안내가 출력됩니다.\nimport subprocess, json\nr = subprocess.run(\n ['luke-scribe', 'transcribe', 'samples/colab-ko-en.mp3', '--language', 'ko', '--device', 'auto'],\n capture_output=True, text=True,\n)\nprint(r.stdout[-2500:] if r.stdout else '')\nprint(r.stderr[-800:] if r.stderr else '')\n"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
@@ -140,7 +140,7 @@
|
|||||||
"execution_count": null,
|
"execution_count": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": "# 10) 서버 기동 (in-proc 큐, 백그라운드 — Colab에서는 %%bash --bg 또는 nohup 사용)\n!mkdir -p /content/logs\n!nohup luke-scribe serve --port 8000 > /content/logs/server.log 2>&1 &\nimport time, urllib.request\nok = False\nfor _ in range(40):\n try:\n urllib.request.urlopen('http://localhost:8000/health', timeout=1)\n ok = True\n break\n except Exception:\n time.sleep(1)\nprint('서버 기동 OK' if ok else '서버 기동 실패 — 로그:')\nif not ok:\n print(open('/content/logs/server.log').read()[-1500:])\n"
|
"source": "# 10) 서버 기동 (in-proc 큐, 백그라운드 — nohup)\n# 이전 실행에서 남은 서버가 포트 8000을 점유하면 새 키를 모른 채 401이 난다.\n# → 먼저 기존 서버를 모두 종료하고, 키 파일 경로를 명시해 재시작한다.\nimport subprocess, os, time, urllib.request, urllib.error\n\nassert 'RAW_KEY' in globals(), '셀 9(키 생성)를 먼저 실행하세요'\n\nsubprocess.run(['pkill', '-f', 'luke-scribe serve'], capture_output=True)\ntime.sleep(2)\n\n# 서버가 읽을 키 파일/큐를 명시 (cwd 의존 제거)\nos.environ['LUKESCRIBE_API_KEY_FILE'] = '/content/api_keys.json'\nos.environ['LUKESCRIBE_QUEUE_BACKEND'] = 'inproc'\n\n!mkdir -p /content/logs\n!nohup luke-scribe serve --port 8000 > /content/logs/server.log 2>&1 &\n\nok = False\nfor _ in range(40):\n try:\n urllib.request.urlopen('http://localhost:8000/health', timeout=1)\n ok = True\n break\n except Exception:\n time.sleep(1)\nif not ok:\n print('서버 기동 실패 — 로그:')\n print(open('/content/logs/server.log').read()[-1500:])\n raise SystemExit('서버 기동 실패')\n\n# 키 인증 검증 — RAW_KEY로 /v1/jobs 호출 → 200이어야 함 (401이면 종료)\nreq = urllib.request.Request('http://localhost:8000/v1/jobs', headers={'X-API-Key': RAW_KEY})\ntry:\n resp = urllib.request.urlopen(req, timeout=5)\n print('서버 기동 OK + 키 인증 OK (HTTP', resp.status, ')')\nexcept urllib.error.HTTPError as e:\n print('키 인증 실패 (HTTP', e.code, ') — 서버 로그:')\n print(open('/content/logs/server.log').read()[-1500:])\n raise SystemExit('키 인증 실패 — RAW_KEY 캡처/키 파일 확인')\n"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@@ -159,7 +159,7 @@
|
|||||||
"execution_count": null,
|
"execution_count": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": "# 12) 브로커 + 워커 포함 전체 흐름 (in-proc)\n# 서버의 in-proc 브로커에 enqueue된 job을 같은 프로세스의 워커가 소비하는 구조는\n# 프로세스 분리 필요 → 여기서는 클라이언트에서 직접 Worker.drain() 호출로 검증\nfrom luke_scribe.config import Settings\nfrom luke_scribe.jobqueue.broker import InProcBroker\nfrom luke_scribe.jobqueue.worker import Worker\nfrom luke_scribe.jobqueue.jobs import Job\nfrom luke_scribe.results.store import ResultStore\n\nsettings = Settings(_env_file=None, queue_backend='inproc',\n results_dir='/content/results', model_cache_dir=None)\nbroker = InProcBroker(settings)\nstore = ResultStore(settings.results_dir)\n\njob = Job(type='file', lane='batch', source_path='samples/colab-ko-en.mp3',\n options={'model': 'large-v3-turbo', 'language': 'ko', 'device': 'auto'})\nbroker.enqueue(job)\nworker = Worker(settings=settings, broker=broker, store=store)\nworker.drain()\n\nresult = store.read_result(job.id)\nprint('status:', result.status)\nprint('text:', result.text[:120])\nprint('device:', result.execution.device, '| ct:', result.execution.compute_type, '| rtf:', result.timings.rtf)\n"
|
"source": "# 12) 브로커 + 워커 포함 전체 흐름 (in-proc)\n# 서버의 in-proc 브로커에 enqueue된 job을 같은 프로세스의 워커가 소비하는 구조는\n# 프로세스 분리 필요 → 여기서는 클라이언트에서 직접 Worker.drain() 호출로 검증\nfrom luke_scribe.config import Settings\nfrom luke_scribe.jobqueue.broker import InProcBroker\nfrom luke_scribe.jobqueue.worker import Worker\nfrom luke_scribe.jobqueue.jobs import Job\nfrom luke_scribe.results.store import ResultStore\n\nsettings = Settings(_env_file=None, queue_backend='inproc',\n results_dir='/content/results', model_cache_dir=None)\nbroker = InProcBroker(settings)\nstore = ResultStore(settings.results_dir)\n\njob = Job(type='file', lane='batch', source_path='samples/colab-ko-en.mp3',\n options={'model': 'large-v3-turbo', 'language': 'ko', 'device': 'auto'})\nbroker.enqueue(job)\nworker = Worker(settings=settings, broker=broker, store=store)\nworker.drain()\n\nresult = store.read_result(job.id)\nif result is None:\n # 전사 실패(예: GPU 초기화 실패) 시 결과가 저장되지 않음 — 상태/오류 출력\n state = broker.get(job.id)\n print('결과 없음 — job 상태:', getattr(state, 'status', 'unknown'))\n if state is not None:\n print('job 오류:', getattr(state, 'error', None) or '없음')\nelse:\n print('status:', result.status)\n print('text:', result.text[:120])\n print('device:', result.execution.device, '| ct:', result.execution.compute_type, '| rtf:', result.timings.rtf)\n"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
@@ -176,7 +176,7 @@
|
|||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"source": "## 8) 문제 해결\n\n### CUDA 13 환경에서 `unsupported device cuda:0` (faster-whisper/CTranslate2)\n\nColab이 CUDA 13.0(드라이버 580)으로 올라가면서, CUDA 12용으로 빌드된\nCTranslate2 wheel이 런타임 라이브러리(cuBLAS/cuDNN)를 찾지 못하는 이슈가 발생합니다.\n설치 셀 3에서 CUDA 12 런타임을 pip로 설치하고 `LD_LIBRARY_PATH`를 잡아 해결합니다.\n그래도 안 되면 CPU로 폴백: `--device cpu --compute-type int8` (느리지만 확실).\n\n### 모델 다운로드 실패\n\n- Hugging Face 연결 필요. 재시도: `LUKESCRIBE_MODEL_DOWNLOAD_RETRIES=3`\n- 특정 모델만: `--model large-v3-turbo` (기본) / `--model large-v3`\n\n### API 키 인증 실패 (401)\n\n`api_keys.json`에는 **다이제스트만** 저장되고 raw 키는 생성 시 1회만 출력됩니다\n(보안 설계). 셀 9에서 `RAW_KEY`를 캡처했는지 확인하세요 — 파일에서 키를 읽으면 401이 납니다.\n\n### OOM (16GB VRAM)\n\n- EngineOwner가 자동으로 정밀도를 강등합니다 (float16 → int8_float16 → int8 → CPU)\n- 수동 지정: `--compute-type int8_float16`\n\n### 저장소가 private인데 클론 안 됨\n\n1번 셀의 `GITEA_TOKEN`에 토큰을 입력하고 런타임 → **Restart session** 후 다시 실행하세요.\n"
|
"source": "## 8) 문제 해결\n\n### 전사 실패: `unsupported device cuda:0`\n\n**근본 원인**: v0.1 엔진이 `device=\"cuda:0\"`(인덱스 포함)를 그대로 CTranslate2에 전달했는데,\nCTranslate2는 `device=\"cuda\"`만 허용하고 인덱스는 별도 `device_index` 인자로 받습니다.\n→ 엔진에서 `device`/`device_index`를 분리하도록 **수정 완료** (이 브랜치 최신 커밋 포함).\n\n참고: Colab의 CUDA 13(드라이버 580)에서는 CUDA 12용 CTranslate2 wheel이\n런타임 라이브러리(cuBLAS/cuDNN)를 못 찾을 수 있어, 설치 셀 3에서\n`nvidia-cublas-cu12`/`nvidia-cudnn-cu12` + `LD_LIBRARY_PATH`를 설정합니다.\n그래도 실패하면 CPU 폴백: `--device cpu --compute-type int8` (느리지만 확실).\n\n### API 키 인증 실패 (401)\n\n1. `api_keys.json`에는 **다이제스트만** 저장되고 raw 키는 생성 시 1회만 출력됩니다\n (보안 설계). 셀 9에서 `RAW_KEY`를 캡처했는지 확인하세요 — 파일에서 키를 읽으면 401.\n2. **이전 실행에서 남은 서버**가 포트 8000을 점유하면 새 키를 모른 채 401이 납니다.\n 셀 10이 시작 시 `pkill`로 기존 서버를 종료하고 키 인증(HTTP 200)까지 검증합니다.\n\n### 모델 다운로드 실패\n\n- Hugging Face 연결 필요. 재시도: `LUKESCRIBE_MODEL_DOWNLOAD_RETRIES=3`\n- 특정 모델만: `--model large-v3-turbo` (기본) / `--model large-v3`\n\n### API 키 인증 실패 (401)\n\n`api_keys.json`에는 **다이제스트만** 저장되고 raw 키는 생성 시 1회만 출력됩니다\n(보안 설계). 셀 9에서 `RAW_KEY`를 캡처했는지 확인하세요 — 파일에서 키를 읽으면 401이 납니다.\n\n### OOM (16GB VRAM)\n\n- EngineOwner가 자동으로 정밀도를 강등합니다 (float16 → int8_float16 → int8 → CPU)\n- 수동 지정: `--compute-type int8_float16`\n\n### 저장소가 private인데 클론 안 됨\n\n1번 셀의 `GITEA_TOKEN`에 토큰을 입력하고 런타임 → **Restart session** 후 다시 실행하세요.\n"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -28,7 +28,13 @@ def md(source: str) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def code(source: str) -> dict:
|
def code(source: str) -> dict:
|
||||||
return {"cell_type": "code", "execution_count": None, "metadata": {}, "outputs": [], "source": source}
|
return {
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": None,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": source,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def cells() -> list[dict]:
|
def cells() -> list[dict]:
|
||||||
@@ -94,7 +100,9 @@ def cells() -> list[dict]:
|
|||||||
"print('작업 디렉터리 →', os.getcwd())\n"
|
"print('작업 디렉터리 →', os.getcwd())\n"
|
||||||
"!git log --oneline -1\n"
|
"!git log --oneline -1\n"
|
||||||
),
|
),
|
||||||
md("## 2) 시스템 의존성 + 설치\n\nffmpeg(오디오 정규화) 설치 후 venv에 luke-scribe를 설치합니다."),
|
md(
|
||||||
|
"## 2) 시스템 의존성 + 설치\n\nffmpeg(오디오 정규화) 설치 후 venv에 luke-scribe를 설치합니다."
|
||||||
|
),
|
||||||
code(
|
code(
|
||||||
"# 2) 시스템 패키지 (ffmpeg — ffprobe/정규화에 필수)\n"
|
"# 2) 시스템 패키지 (ffmpeg — ffprobe/정규화에 필수)\n"
|
||||||
"!apt-get update -qq && apt-get install -y -qq ffmpeg >/dev/null\n"
|
"!apt-get update -qq && apt-get install -y -qq ffmpeg >/dev/null\n"
|
||||||
@@ -143,15 +151,11 @@ def cells() -> list[dict]:
|
|||||||
"CPU-only 환경에서는 `T0 / cpu / int8`이 나왔지만,\n"
|
"CPU-only 환경에서는 `T0 / cpu / int8`이 나왔지만,\n"
|
||||||
"Colab GPU(A100 80GB / T4 16GB)에서는 GPU가 감지되어야 합니다."
|
"Colab GPU(A100 80GB / T4 16GB)에서는 GPU가 감지되어야 합니다."
|
||||||
),
|
),
|
||||||
code(
|
code("# 4) detect — GPU 감지 / 능력 등급 / 정밀도 / 워커수\n!luke-scribe detect\n"),
|
||||||
"# 4) detect — GPU 감지 / 능력 등급 / 정밀도 / 워커수\n"
|
md(
|
||||||
"!luke-scribe detect\n"
|
"## 4) 단위/통합 테스트 (127개)\n\nmock 기반 테스트가 GPU 환경에서도 전부 통과하는지 확인합니다."
|
||||||
),
|
|
||||||
md("## 4) 단위/통합 테스트 (127개)\n\nmock 기반 테스트가 GPU 환경에서도 전부 통과하는지 확인합니다."),
|
|
||||||
code(
|
|
||||||
"# 5) 테스트 스위트\n"
|
|
||||||
"!python -m pytest tests/ -q 2>&1 | tail -5\n"
|
|
||||||
),
|
),
|
||||||
|
code("# 5) 테스트 스위트\n!python -m pytest tests/ -q 2>&1 | tail -5\n"),
|
||||||
md(
|
md(
|
||||||
"## 5) 실전 전사 (GPU)\n\n"
|
"## 5) 실전 전사 (GPU)\n\n"
|
||||||
"### 5-1) 샘플 오디오 생성\n\n"
|
"### 5-1) 샘플 오디오 생성\n\n"
|
||||||
@@ -167,7 +171,8 @@ def cells() -> list[dict]:
|
|||||||
code(
|
code(
|
||||||
"# 7) 실전 전사 — GPU 자동 감지 + 모델 다운로드 (large-v3-turbo)\n"
|
"# 7) 실전 전사 — GPU 자동 감지 + 모델 다운로드 (large-v3-turbo)\n"
|
||||||
"# 첫 실행 시 Hugging Face에서 모델을 다운로드합니다 (turbo ≈ 1.6GB, 1~2분)\n"
|
"# 첫 실행 시 Hugging Face에서 모델을 다운로드합니다 (turbo ≈ 1.6GB, 1~2분)\n"
|
||||||
"# CUDA 13 환경에서 CTranslate2가 GPU를 못 열면 자동으로 CPU로 폴백합니다.\n"
|
"# v0.1: 'cuda:N' device를 분리해 CTranslate2 계약에 맞게 전달하도록 수정됨\n"
|
||||||
|
"# (기존: unsupported device cuda:0). 그래도 실패하면 CPU 폴백 안내가 출력됩니다.\n"
|
||||||
"import subprocess, json\n"
|
"import subprocess, json\n"
|
||||||
"r = subprocess.run(\n"
|
"r = subprocess.run(\n"
|
||||||
" ['luke-scribe', 'transcribe', 'samples/colab-ko-en.mp3', '--language', 'ko', '--device', 'auto'],\n"
|
" ['luke-scribe', 'transcribe', 'samples/colab-ko-en.mp3', '--language', 'ko', '--device', 'auto'],\n"
|
||||||
@@ -202,10 +207,23 @@ def cells() -> list[dict]:
|
|||||||
"print('raw 키 캡처 완료 (표시 안 함)')\n"
|
"print('raw 키 캡처 완료 (표시 안 함)')\n"
|
||||||
),
|
),
|
||||||
code(
|
code(
|
||||||
"# 10) 서버 기동 (in-proc 큐, 백그라운드 — Colab에서는 %%bash --bg 또는 nohup 사용)\n"
|
"# 10) 서버 기동 (in-proc 큐, 백그라운드 — nohup)\n"
|
||||||
|
"# 이전 실행에서 남은 서버가 포트 8000을 점유하면 새 키를 모른 채 401이 난다.\n"
|
||||||
|
"# → 먼저 기존 서버를 모두 종료하고, 키 파일 경로를 명시해 재시작한다.\n"
|
||||||
|
"import subprocess, os, time, urllib.request, urllib.error\n"
|
||||||
|
"\n"
|
||||||
|
"assert 'RAW_KEY' in globals(), '셀 9(키 생성)를 먼저 실행하세요'\n"
|
||||||
|
"\n"
|
||||||
|
"subprocess.run(['pkill', '-f', 'luke-scribe serve'], capture_output=True)\n"
|
||||||
|
"time.sleep(2)\n"
|
||||||
|
"\n"
|
||||||
|
"# 서버가 읽을 키 파일/큐를 명시 (cwd 의존 제거)\n"
|
||||||
|
"os.environ['LUKESCRIBE_API_KEY_FILE'] = '/content/api_keys.json'\n"
|
||||||
|
"os.environ['LUKESCRIBE_QUEUE_BACKEND'] = 'inproc'\n"
|
||||||
|
"\n"
|
||||||
"!mkdir -p /content/logs\n"
|
"!mkdir -p /content/logs\n"
|
||||||
"!nohup luke-scribe serve --port 8000 > /content/logs/server.log 2>&1 &\n"
|
"!nohup luke-scribe serve --port 8000 > /content/logs/server.log 2>&1 &\n"
|
||||||
"import time, urllib.request\n"
|
"\n"
|
||||||
"ok = False\n"
|
"ok = False\n"
|
||||||
"for _ in range(40):\n"
|
"for _ in range(40):\n"
|
||||||
" try:\n"
|
" try:\n"
|
||||||
@@ -214,9 +232,20 @@ def cells() -> list[dict]:
|
|||||||
" break\n"
|
" break\n"
|
||||||
" except Exception:\n"
|
" except Exception:\n"
|
||||||
" time.sleep(1)\n"
|
" time.sleep(1)\n"
|
||||||
"print('서버 기동 OK' if ok else '서버 기동 실패 — 로그:')\n"
|
|
||||||
"if not ok:\n"
|
"if not ok:\n"
|
||||||
|
" print('서버 기동 실패 — 로그:')\n"
|
||||||
" print(open('/content/logs/server.log').read()[-1500:])\n"
|
" print(open('/content/logs/server.log').read()[-1500:])\n"
|
||||||
|
" raise SystemExit('서버 기동 실패')\n"
|
||||||
|
"\n"
|
||||||
|
"# 키 인증 검증 — RAW_KEY로 /v1/jobs 호출 → 200이어야 함 (401이면 종료)\n"
|
||||||
|
"req = urllib.request.Request('http://localhost:8000/v1/jobs', headers={'X-API-Key': RAW_KEY})\n"
|
||||||
|
"try:\n"
|
||||||
|
" resp = urllib.request.urlopen(req, timeout=5)\n"
|
||||||
|
" print('서버 기동 OK + 키 인증 OK (HTTP', resp.status, ')')\n"
|
||||||
|
"except urllib.error.HTTPError as e:\n"
|
||||||
|
" print('키 인증 실패 (HTTP', e.code, ') — 서버 로그:')\n"
|
||||||
|
" print(open('/content/logs/server.log').read()[-1500:])\n"
|
||||||
|
" raise SystemExit('키 인증 실패 — RAW_KEY 캡처/키 파일 확인')\n"
|
||||||
),
|
),
|
||||||
code(
|
code(
|
||||||
"# 11) 업로드 → poll → 결과 (RAW_KEY는 셀 9에서 캡처된 값)\n"
|
"# 11) 업로드 → poll → 결과 (RAW_KEY는 셀 9에서 캡처된 값)\n"
|
||||||
@@ -229,7 +258,7 @@ def cells() -> list[dict]:
|
|||||||
" ['curl', '-s', '-X', 'POST', 'http://localhost:8000/v1/jobs',\n"
|
" ['curl', '-s', '-X', 'POST', 'http://localhost:8000/v1/jobs',\n"
|
||||||
" '-H', f'X-API-Key: {RAW_KEY}',\n"
|
" '-H', f'X-API-Key: {RAW_KEY}',\n"
|
||||||
" '-F', 'file=@samples/colab-ko-en.mp3',\n"
|
" '-F', 'file=@samples/colab-ko-en.mp3',\n"
|
||||||
" '-F', 'options={\"language\":\"ko\",\"formats\":[\"json\",\"srt\"]}'],\n"
|
' \'-F\', \'options={"language":"ko","formats":["json","srt"]}\'],\n'
|
||||||
" capture_output=True, text=True,\n"
|
" capture_output=True, text=True,\n"
|
||||||
")\n"
|
")\n"
|
||||||
"job = json.loads(r.stdout)\n"
|
"job = json.loads(r.stdout)\n"
|
||||||
@@ -281,6 +310,13 @@ def cells() -> list[dict]:
|
|||||||
"worker.drain()\n"
|
"worker.drain()\n"
|
||||||
"\n"
|
"\n"
|
||||||
"result = store.read_result(job.id)\n"
|
"result = store.read_result(job.id)\n"
|
||||||
|
"if result is None:\n"
|
||||||
|
" # 전사 실패(예: GPU 초기화 실패) 시 결과가 저장되지 않음 — 상태/오류 출력\n"
|
||||||
|
" state = broker.get(job.id)\n"
|
||||||
|
" print('결과 없음 — job 상태:', getattr(state, 'status', 'unknown'))\n"
|
||||||
|
" if state is not None:\n"
|
||||||
|
" print('job 오류:', getattr(state, 'error', None) or '없음')\n"
|
||||||
|
"else:\n"
|
||||||
" print('status:', result.status)\n"
|
" print('status:', result.status)\n"
|
||||||
" print('text:', result.text[:120])\n"
|
" print('text:', result.text[:120])\n"
|
||||||
" print('device:', result.execution.device, '| ct:', result.execution.compute_type, '| rtf:', result.timings.rtf)\n"
|
" print('device:', result.execution.device, '| ct:', result.execution.compute_type, '| rtf:', result.timings.rtf)\n"
|
||||||
@@ -310,11 +346,19 @@ def cells() -> list[dict]:
|
|||||||
),
|
),
|
||||||
md(
|
md(
|
||||||
"## 8) 문제 해결\n\n"
|
"## 8) 문제 해결\n\n"
|
||||||
"### CUDA 13 환경에서 `unsupported device cuda:0` (faster-whisper/CTranslate2)\n\n"
|
"### 전사 실패: `unsupported device cuda:0`\n\n"
|
||||||
"Colab이 CUDA 13.0(드라이버 580)으로 올라가면서, CUDA 12용으로 빌드된\n"
|
'**근본 원인**: v0.1 엔진이 `device="cuda:0"`(인덱스 포함)를 그대로 CTranslate2에 전달했는데,\n'
|
||||||
"CTranslate2 wheel이 런타임 라이브러리(cuBLAS/cuDNN)를 찾지 못하는 이슈가 발생합니다.\n"
|
'CTranslate2는 `device="cuda"`만 허용하고 인덱스는 별도 `device_index` 인자로 받습니다.\n'
|
||||||
"설치 셀 3에서 CUDA 12 런타임을 pip로 설치하고 `LD_LIBRARY_PATH`를 잡아 해결합니다.\n"
|
"→ 엔진에서 `device`/`device_index`를 분리하도록 **수정 완료** (이 브랜치 최신 커밋 포함).\n\n"
|
||||||
"그래도 안 되면 CPU로 폴백: `--device cpu --compute-type int8` (느리지만 확실).\n\n"
|
"참고: Colab의 CUDA 13(드라이버 580)에서는 CUDA 12용 CTranslate2 wheel이\n"
|
||||||
|
"런타임 라이브러리(cuBLAS/cuDNN)를 못 찾을 수 있어, 설치 셀 3에서\n"
|
||||||
|
"`nvidia-cublas-cu12`/`nvidia-cudnn-cu12` + `LD_LIBRARY_PATH`를 설정합니다.\n"
|
||||||
|
"그래도 실패하면 CPU 폴백: `--device cpu --compute-type int8` (느리지만 확실).\n\n"
|
||||||
|
"### API 키 인증 실패 (401)\n\n"
|
||||||
|
"1. `api_keys.json`에는 **다이제스트만** 저장되고 raw 키는 생성 시 1회만 출력됩니다\n"
|
||||||
|
" (보안 설계). 셀 9에서 `RAW_KEY`를 캡처했는지 확인하세요 — 파일에서 키를 읽으면 401.\n"
|
||||||
|
"2. **이전 실행에서 남은 서버**가 포트 8000을 점유하면 새 키를 모른 채 401이 납니다.\n"
|
||||||
|
" 셀 10이 시작 시 `pkill`로 기존 서버를 종료하고 키 인증(HTTP 200)까지 검증합니다.\n\n"
|
||||||
"### 모델 다운로드 실패\n\n"
|
"### 모델 다운로드 실패\n\n"
|
||||||
"- Hugging Face 연결 필요. 재시도: `LUKESCRIBE_MODEL_DOWNLOAD_RETRIES=3`\n"
|
"- Hugging Face 연결 필요. 재시도: `LUKESCRIBE_MODEL_DOWNLOAD_RETRIES=3`\n"
|
||||||
"- 특정 모델만: `--model large-v3-turbo` (기본) / `--model large-v3`\n"
|
"- 특정 모델만: `--model large-v3-turbo` (기본) / `--model large-v3`\n"
|
||||||
|
|||||||
@@ -86,10 +86,15 @@ class FasterWhisperEngine(TranscriptionEngine):
|
|||||||
with self._lock:
|
with self._lock:
|
||||||
model = self._models.get(model_key)
|
model = self._models.get(model_key)
|
||||||
if model is None:
|
if model is None:
|
||||||
|
# CTranslate2는 device 문자열로 "cuda"만 허용한다 ("cuda:0" 형태는
|
||||||
|
# "unsupported device cuda:0" 오류). DeviceManager가 내려주는
|
||||||
|
# "cuda:N"을 device + device_index로 분리해 전달한다.
|
||||||
|
device_name, device_index = self._split_device(options.device)
|
||||||
try:
|
try:
|
||||||
model = WhisperModel(
|
model = WhisperModel(
|
||||||
options.model,
|
options.model,
|
||||||
device=options.device,
|
device=device_name,
|
||||||
|
device_index=device_index,
|
||||||
compute_type=options.compute_type,
|
compute_type=options.compute_type,
|
||||||
download_root=self._download_root(),
|
download_root=self._download_root(),
|
||||||
)
|
)
|
||||||
@@ -128,6 +133,20 @@ class FasterWhisperEngine(TranscriptionEngine):
|
|||||||
wrapped = _CancellableSegmentIterator(segments_iter, should_cancel or (lambda: False))
|
wrapped = _CancellableSegmentIterator(segments_iter, should_cancel or (lambda: False))
|
||||||
return TranscriptionOutcome(wrapped, info=info)
|
return TranscriptionOutcome(wrapped, info=info)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _split_device(device: str) -> tuple[str, int]:
|
||||||
|
"""'cuda:N' → ('cuda', N). CTranslate2는 device='cuda'만 허용하므로 분리한다.
|
||||||
|
|
||||||
|
'cpu'/'cuda' 같은 인덱스 없는 값은 그대로 (device, 0)을 반환한다.
|
||||||
|
"""
|
||||||
|
if device.startswith("cuda") and ":" in device:
|
||||||
|
name, _, idx = device.partition(":")
|
||||||
|
try:
|
||||||
|
return name, int(idx)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return device, 0
|
||||||
|
|
||||||
def _download_root(self) -> str | None:
|
def _download_root(self) -> str | None:
|
||||||
from ..config import get_settings
|
from ..config import get_settings
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
"""FasterWhisperEngine 단위 테스트 — CTranslate2 호출 계약 (device 분리).
|
||||||
|
|
||||||
|
faster-whisper/CTranslate2는 ``device`` 문자열로 "cuda"만 허용하며 인덱스는
|
||||||
|
별도 ``device_index`` 인자로 받는다. DeviceManager가 내려주는 "cuda:N"을
|
||||||
|
제대로 분리해 전달하는지 mock으로 검증한다 (GPU 환경에서만 재현되는 버그
|
||||||
|
— Colab 실전 테스트에서 ``unsupported device cuda:0``으로 확인됨).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
|
||||||
|
from luke_scribe.engine.base import TranscriptionOptions
|
||||||
|
from luke_scribe.engine.faster_whisper_engine import FasterWhisperEngine
|
||||||
|
|
||||||
|
|
||||||
|
class FakeWhisperModel:
|
||||||
|
"""faster_whisper.WhisperModel 대체 — 생성 인자만 기록한다."""
|
||||||
|
|
||||||
|
calls: list[dict] = []
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
self.kwargs = kwargs
|
||||||
|
self.__class__.calls.append(kwargs)
|
||||||
|
|
||||||
|
def transcribe(self, audio_path, **kwargs):
|
||||||
|
return iter([]), {}
|
||||||
|
|
||||||
|
|
||||||
|
def _install_fake(monkeypatch) -> None:
|
||||||
|
mod = types.ModuleType("faster_whisper")
|
||||||
|
mod.WhisperModel = FakeWhisperModel
|
||||||
|
monkeypatch.setitem(sys.modules, "faster_whisper", mod)
|
||||||
|
FakeWhisperModel.calls.clear()
|
||||||
|
|
||||||
|
|
||||||
|
def _opts(**kw) -> TranscriptionOptions:
|
||||||
|
kw.setdefault("model", "large-v3-turbo")
|
||||||
|
kw.setdefault("device", "cuda:0")
|
||||||
|
kw.setdefault("compute_type", "float16")
|
||||||
|
return TranscriptionOptions(**kw)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cuda_device_index_split(monkeypatch):
|
||||||
|
"""'cuda:0' → device='cuda', device_index=0 (Colab A100 첫 GPU)."""
|
||||||
|
_install_fake(monkeypatch)
|
||||||
|
FasterWhisperEngine().transcribe("/tmp/x.wav", _opts())
|
||||||
|
kwargs = FakeWhisperModel.calls[-1]
|
||||||
|
assert kwargs["device"] == "cuda"
|
||||||
|
assert kwargs["device_index"] == 0
|
||||||
|
assert kwargs["compute_type"] == "float16"
|
||||||
|
|
||||||
|
|
||||||
|
def test_cuda_second_device_index(monkeypatch):
|
||||||
|
"""'cuda:1' → device='cuda', device_index=1."""
|
||||||
|
_install_fake(monkeypatch)
|
||||||
|
FasterWhisperEngine().transcribe("/tmp/x.wav", _opts(device="cuda:1"))
|
||||||
|
kwargs = FakeWhisperModel.calls[-1]
|
||||||
|
assert kwargs["device"] == "cuda"
|
||||||
|
assert kwargs["device_index"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_cpu_passthrough(monkeypatch):
|
||||||
|
"""'cpu' → 그대로 device='cpu', device_index=0."""
|
||||||
|
_install_fake(monkeypatch)
|
||||||
|
FasterWhisperEngine().transcribe("/tmp/x.wav", _opts(device="cpu", compute_type="int8"))
|
||||||
|
kwargs = FakeWhisperModel.calls[-1]
|
||||||
|
assert kwargs["device"] == "cpu"
|
||||||
|
assert kwargs["device_index"] == 0
|
||||||
|
assert kwargs["compute_type"] == "int8"
|
||||||
|
|
||||||
|
|
||||||
|
def test_split_device_helper():
|
||||||
|
assert FasterWhisperEngine._split_device("cuda:0") == ("cuda", 0)
|
||||||
|
assert FasterWhisperEngine._split_device("cuda:3") == ("cuda", 3)
|
||||||
|
assert FasterWhisperEngine._split_device("cpu") == ("cpu", 0)
|
||||||
|
assert FasterWhisperEngine._split_device("cuda") == ("cuda", 0)
|
||||||
|
# 파싱 불가 인덱스는 그대로 전달 (모델 로드 시 명시적 오류로 fail-explicit)
|
||||||
|
assert FasterWhisperEngine._split_device("cuda:xx") == ("cuda:xx", 0)
|
||||||
Reference in New Issue
Block a user