Colab run 5 failed at cell 12 (import luke_scribe.config) because hatchling editable installs write a .pth pointing at src/ that the interpreter only reads at startup; a long-running Colab kernel started before pip install -e never sees it. Subprocesses (CLI) work, but kernel-side imports fail. Fix: install cell now walks up to the repo root and inserts src/ into the kernel sys.path, verifying with import luke_scribe.config. Cell 12 has a defensive guard of the same kind. Tunnel external verification now retries up to 6x2s for DNS propagation. Troubleshooting section documents the issue.
498 lines
29 KiB
Python
498 lines
29 KiB
Python
"""Colab 실전 테스트 노트북 생성 스크립트.
|
|
|
|
다른 환경(CPU-only, ffmpeg 없음)에서 검증할 수 없었던 부분을
|
|
Colab Pro(T4 GPU + 터미널)에서 실전 검증하기 위한 노트북을 생성한다:
|
|
|
|
1. 저장소 클론 (feat/full-platform)
|
|
2. 시스템 의존성 (ffmpeg) + venv + luke-scribe 설치
|
|
3. `detect` — T4 GPU 실제 감지 (능력 등급/정밀도/워커수)
|
|
4. 단위/통합 테스트 (127개 mock)
|
|
5. 샘플 오디오 생성 (한국어+영문 기술용어, edge-tts)
|
|
6. 실전 전사 (GPU, faster-whisper 모델 다운로드) + glossary/hotword 검증
|
|
7. API 서버 기동 + curl 스모크 (업로드 → poll → 결과)
|
|
8. 벤치마크 (turbo vs large-v3)
|
|
|
|
사용법: python scripts/build_colab_notebook.py → notebooks/luke-scribe-colab.ipynb
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
OUT = Path("notebooks/luke-scribe-colab.ipynb")
|
|
|
|
|
|
def md(source: str) -> dict:
|
|
return {"cell_type": "markdown", "metadata": {}, "source": source}
|
|
|
|
|
|
def code(source: str) -> dict:
|
|
return {
|
|
"cell_type": "code",
|
|
"execution_count": None,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": source,
|
|
}
|
|
|
|
|
|
def cells() -> list[dict]:
|
|
return [
|
|
md(
|
|
"# luke_scribe — Colab 실전 테스트 노트북\n"
|
|
"\n"
|
|
"> 내부용 로컬 STT 전사 API (faster-whisper, hardware-adaptive, privacy-first)\n"
|
|
"\n"
|
|
"이 노트북은 CPU-only 개발 환경에서 **mock으로만 검증**했던 것을,\n"
|
|
"Colab Pro(GPU + 터미널)에서 **실전 검증**하기 위한 것입니다.\n"
|
|
"\n"
|
|
"## 검증 대상\n"
|
|
"\n"
|
|
"- `luke-scribe detect` → T4 GPU 실제 감지 (능력 등급 T1~T3, 정밀도, 워커수)\n"
|
|
"- faster-whisper 모델 다운로드 + **실제 한국어 전사** (CPU-only 환경에선 불가)\n"
|
|
"- glossary/hotword 후처리 (KO+EN 기술용어 보존)\n"
|
|
"- REST API 흐름 (업로드 → poll → 결과)\n"
|
|
"- turbo vs large-v3 벤치마크\n"
|
|
"\n"
|
|
"## 준비\n"
|
|
"\n"
|
|
"1. 런타임 → 런타임 유형 변경 → **T4 GPU** 선택\n"
|
|
"2. (Colab Pro) 터미널을 사용해도 동일한 명령을 실행할 수 있습니다\n"
|
|
"3. 저장소가 private이면 아래 셀에 **Gitea 토큰** 입력 (Settings → Applications → Generate New Token)\n"
|
|
"\n"
|
|
"---\n"
|
|
),
|
|
code(
|
|
"# 0) 런타임 확인 — T4 GPU가 활성 상태여야 합니다\n"
|
|
"!nvidia-smi\n"
|
|
"import sys\n"
|
|
"print('Python', sys.version.split()[0])\n"
|
|
),
|
|
md(
|
|
"## 1) 저장소 클론\n"
|
|
"\n"
|
|
"`feat/full-platform` 브랜치를 클론합니다.\n"
|
|
"저장소가 private이면 아래 셀의 `GITEA_TOKEN`에 토큰을 입력하세요."
|
|
),
|
|
code(
|
|
"# 1) 클론 또는 업데이트 (private 저장소면 GITEA_TOKEN 입력)\n"
|
|
"GITEA_TOKEN = '' # ← 필요 시 입력: https://git.lukehemmin.com/user/settings/applications\n"
|
|
"\n"
|
|
"if GITEA_TOKEN:\n"
|
|
" REPO = f'https://{GITEA_TOKEN}@git.lukehemmin.com/lukehemmin/luke_scribe.git'\n"
|
|
"else:\n"
|
|
" REPO = 'https://git.lukehemmin.com/lukehemmin/luke_scribe.git'\n"
|
|
"\n"
|
|
"import os, subprocess\n"
|
|
"\n"
|
|
"if os.path.isdir('/content/luke_scribe/.git'):\n"
|
|
" # 이미 클론된 레포 → 최신 브랜치로 갱신 (pull)\n"
|
|
" print('기존 레포 감지 → pull로 갱신')\n"
|
|
" subprocess.run(['git', 'fetch', 'origin'], cwd='/content/luke_scribe', check=True)\n"
|
|
" subprocess.run(['git', 'checkout', 'feat/full-platform'], cwd='/content/luke_scribe', check=True)\n"
|
|
" subprocess.run(['git', 'pull', '--ff-only', 'origin', 'feat/full-platform'], cwd='/content/luke_scribe', check=True)\n"
|
|
"else:\n"
|
|
" # 최초 실행 → 클론\n"
|
|
" subprocess.run(['git', 'clone', '-b', 'feat/full-platform', REPO], cwd='/content', check=True)\n"
|
|
"\n"
|
|
"os.chdir('/content/luke_scribe')\n"
|
|
"print('작업 디렉터리 →', os.getcwd())\n"
|
|
"!git log --oneline -1\n"
|
|
),
|
|
md(
|
|
"## 2) 시스템 의존성 + 설치\n\nffmpeg(오디오 정규화) 설치 후 venv에 luke-scribe를 설치합니다."
|
|
),
|
|
code(
|
|
"# 2) 시스템 패키지 (ffmpeg — ffprobe/정규화에 필수)\n"
|
|
"!apt-get update -qq && apt-get install -y -qq ffmpeg >/dev/null\n"
|
|
"!ffmpeg -version 2>&1 | head -1\n"
|
|
"!ffprobe -version 2>&1 | head -1\n"
|
|
),
|
|
code(
|
|
"# 3) 설치 — Colab은 시스템 pip가 표준 (venv는 Colab에서 ensurepip 오류로 실패할 수 있음)\n"
|
|
"!pip install -q --upgrade pip\n"
|
|
"!pip install -q -e '.[engine,api]'\n"
|
|
"!pip install -q edge-tts # 샘플 음성 생성용\n"
|
|
"\n"
|
|
"# CUDA 13(Colab)에서는 CTranslate2 wheel(CUDA 12용)이 런타임 라이브러리를 못 찾음.\n"
|
|
"# CUDA 12 런타임(cuBLAS/cuDNN)을 pip로 설치하고, 실제 .so 위치를 find로 찾아\n"
|
|
"# LD_LIBRARY_PATH에 추가한다 (nvidia-*-cu12는 namespace package라 __file__이 없음).\n"
|
|
"!pip install -q nvidia-cublas-cu12 nvidia-cudnn-cu12\n"
|
|
"import os, site, subprocess\n"
|
|
"lib_dirs = set()\n"
|
|
"for lib in ('libcublas.so', 'libcudnn.so'):\n"
|
|
" out = subprocess.run(\n"
|
|
" ['bash', '-c', f'find {site.getsitepackages()[0]} -name \"{lib}*\" 2>/dev/null | head -3'],\n"
|
|
" capture_output=True, text=True,\n"
|
|
" ).stdout\n"
|
|
" for line in out.splitlines():\n"
|
|
" d = os.path.dirname(line)\n"
|
|
" if d:\n"
|
|
" lib_dirs.add(d)\n"
|
|
"os.environ['LD_LIBRARY_PATH'] = ':'.join(lib_dirs) + ':' + os.environ.get('LD_LIBRARY_PATH', '')\n"
|
|
"print('LD_LIBRARY_PATH:', os.environ['LD_LIBRARY_PATH'])\n"
|
|
"print('설치 완료')\n"
|
|
"\n"
|
|
"# 실행 중인 Colab 커널은 새 .pth 파일을 읽지 못한다 (pip editable 설치는\n"
|
|
"# 인터프리터 시작 시에만 반영). → 커널 sys.path에 src/를 직접 등록해\n"
|
|
"# luke_scribe import를 보장한다 (서브프로세스 CLI는 .pth를 읽으므로 무관).\n"
|
|
"import sys, os as _os\n"
|
|
"_root = _os.getcwd()\n"
|
|
"while not _os.path.isdir(_os.path.join(_root, 'src', 'luke_scribe')) and _root != _os.path.dirname(_root):\n"
|
|
" _root = _os.path.dirname(_root)\n"
|
|
"sys.path.insert(0, _os.path.join(_root, 'src'))\n"
|
|
"import luke_scribe.config as _cfg\n"
|
|
"print('패키지 임포트 OK →', _cfg.__file__)\n"
|
|
"\n"
|
|
"# Cloudflare 터널용 cloudflared 바이너리 (실패해도 진행 — 터널 없이 로컬 사용 가능)\n"
|
|
"!wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -O /usr/local/bin/cloudflared || echo 'cloudflared 다운로드 실패 — 터널 없이 계속합니다'\n"
|
|
"!chmod +x /usr/local/bin/cloudflared 2>/dev/null; cloudflared --version 2>&1 | head -1 || echo 'cloudflared 미설치 — 터널 생략'\n"
|
|
"!which luke-scribe && luke-scribe --help 2>&1 | head -8\n"
|
|
),
|
|
code(
|
|
"# 4) CTranslate2 GPU 검증 — unsupported device cuda:0 해결 여부 확인\n"
|
|
"import ctranslate2\n"
|
|
"print('ctranslate2', ctranslate2.__version__)\n"
|
|
"print('CUDA device count:', ctranslate2.get_cuda_device_count())\n"
|
|
"if ctranslate2.get_cuda_device_count() > 0:\n"
|
|
" print('GPU 사용 가능 ✅ — 이후 전사는 GPU로 실행됩니다')\n"
|
|
"else:\n"
|
|
" print('GPU 사용 불가 — CPU 폴백 필요 (--device cpu --compute-type int8)')\n"
|
|
),
|
|
md(
|
|
"## 3) 하드웨어 감지 — GPU 실제 확인\n"
|
|
"\n"
|
|
"CPU-only 환경에서는 `T0 / cpu / int8`이 나왔지만,\n"
|
|
"Colab GPU(A100 80GB / T4 16GB)에서는 GPU가 감지되어야 합니다."
|
|
),
|
|
code("# 4) detect — GPU 감지 / 능력 등급 / 정밀도 / 워커수\n!luke-scribe detect\n"),
|
|
md(
|
|
"## 4) 단위/통합 테스트 (127개)\n\nmock 기반 테스트가 GPU 환경에서도 전부 통과하는지 확인합니다."
|
|
),
|
|
code("# 5) 테스트 스위트\n!python -m pytest tests/ -q 2>&1 | tail -5\n"),
|
|
md(
|
|
"## 5) 실전 전사 (GPU)\n\n"
|
|
"### 5-1) 샘플 오디오 생성\n\n"
|
|
"edge-tts(MS TTS)로 **한국어 + 영문 기술용어가 섞인** 샘플을 생성합니다.\n"
|
|
"이 텍스트에는 `vLLM`, `Kubernetes`, `GPU` 같은 용어가 포함되어 glossary/hotword 검증에 적합합니다."
|
|
),
|
|
code(
|
|
"# 6) 샘플 오디오 생성 (한국어 + 기술용어, 무료 TTS)\n"
|
|
"!mkdir -p samples\n"
|
|
"!edge-tts --voice ko-KR-SunHiNeural --text '오늘은 vLLM 서버를 Kubernetes 클러스터에 배포하는 방법을 설명합니다. GPU 가속 추론으로 대기 시간을 줄일 수 있습니다.' --write-media samples/colab-ko-en.mp3\n"
|
|
"!ffprobe -v error -show_entries format=duration -of json samples/colab-ko-en.mp3\n"
|
|
),
|
|
code(
|
|
"# 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 폴백 안내가 출력됩니다.\n"
|
|
"import subprocess, json\n"
|
|
"r = subprocess.run(\n"
|
|
" ['luke-scribe', 'transcribe', 'samples/colab-ko-en.mp3', '--language', 'ko', '--device', 'auto'],\n"
|
|
" capture_output=True, text=True,\n"
|
|
")\n"
|
|
"print(r.stdout[-2500:] if r.stdout else '')\n"
|
|
"print(r.stderr[-800:] if r.stderr else '')\n"
|
|
),
|
|
md(
|
|
"### 5-2) 후처리 검증\n\n"
|
|
"glossary(오인식 용어 복원) + hotword(용어 사전 주입) 동작을 확인합니다.\n"
|
|
"`--hotword vLLM Kubernetes`를 주면 initial_prompt에 용어가 주입되어 보존률이 올라갑니다."
|
|
),
|
|
code(
|
|
"# 8) hotword 포함 전사 (용어 보존 강화)\n"
|
|
"!luke-scribe transcribe samples/colab-ko-en.mp3 --language ko --device auto --hotword vLLM --hotword Kubernetes\n"
|
|
),
|
|
md(
|
|
"## 6) REST API 스모크\n\n"
|
|
"서버를 백그라운드로 띄우고 **업로드 → poll → 결과(SRT)** 흐름을 검증합니다."
|
|
),
|
|
code(
|
|
"# 9) API 키 생성 — raw 키는 1회만 출력되므로 여기서 캡처한다\n"
|
|
"import subprocess, json, os\n"
|
|
"r = subprocess.run(\n"
|
|
" ['luke-scribe', 'key', '--create', '--scopes', 'transcribe,admin', '--file', '/content/api_keys.json'],\n"
|
|
" capture_output=True, text=True,\n"
|
|
")\n"
|
|
"created = json.loads(r.stdout)\n"
|
|
"RAW_KEY = created['key'] # 이후 셀에서 사용\n"
|
|
"print('key_id:', created['key_id'], '| scopes:', created['scopes'])\n"
|
|
"print('raw 키 캡처 완료 (표시 안 함)')\n"
|
|
),
|
|
code(
|
|
"# 10) 서버 기동 (in-proc 큐, 백그라운드 — nohup)\n"
|
|
"# 이전 실행에서 남은 서버가 포트 8000을 점유하면 새 키를 모른 채 401이 난다.\n"
|
|
"# → 먼저 기존 서버를 모두 종료하고, 키 파일 경로를 명시해 재시작한다.\n"
|
|
"import subprocess, os, re, shutil, 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"
|
|
"# in-proc 서버가 자체 워커 스레드로 큐를 소비 (업로드 → 완료까지 API 단독 처리)\n"
|
|
"os.environ['LUKESCRIBE_AUTO_WORKER'] = 'true'\n"
|
|
"# Cloudflare Quick Tunnel — 외부 접속 링크 발급 (cloudflared는 셀 3에서 설치)\n"
|
|
"os.environ['LUKESCRIBE_TUNNEL'] = 'cloudflare'\n"
|
|
"\n"
|
|
"!mkdir -p /content/logs\n"
|
|
"!nohup luke-scribe serve --port 8000 > /content/logs/server.log 2>&1 &\n"
|
|
"\n"
|
|
"ok = False\n"
|
|
"for _ 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)\n"
|
|
"if not ok:\n"
|
|
" print('서버 기동 실패 — 로그:')\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"
|
|
"\n"
|
|
"# ── Cloudflare 터널 URL 캡처 + 외부 접속 검증 (서버 로그의 trycloudflare URL) ──\n"
|
|
"TUNNEL_URL = None\n"
|
|
"if shutil.which('cloudflared'):\n"
|
|
" for _ in range(60):\n"
|
|
" log = open('/content/logs/server.log', encoding='utf-8', errors='replace').read()\n"
|
|
" m = re.search(r'https://[a-z0-9-]+\\.trycloudflare\\.com', log)\n"
|
|
" if m:\n"
|
|
" TUNNEL_URL = m.group(0)\n"
|
|
" break\n"
|
|
" time.sleep(2)\n"
|
|
"else:\n"
|
|
" print('cloudflared 미설치 (셀 3 다운로드 실패) — 터널 생략, 로컬(8000) 계속 사용')\n"
|
|
"\n"
|
|
"if TUNNEL_URL:\n"
|
|
" print('🌐 Cloudflare 터널 (외부 접속):', TUNNEL_URL)\n"
|
|
" print(' 대시보드 (API Docs):', TUNNEL_URL + '/docs')\n"
|
|
" print(' 상태 (health):', TUNNEL_URL + '/health')\n"
|
|
" # DNS 전파 지연 대비 재시도 (새 trycloudflare 호스트는 초반에 못 풀릴 수 있음)\n"
|
|
" ext_ok = False\n"
|
|
" for _ in range(6):\n"
|
|
" try:\n"
|
|
" ext = urllib.request.urlopen(TUNNEL_URL + '/health', timeout=15)\n"
|
|
" print(' 외부 접속 검증 OK (HTTP', ext.status, ')')\n"
|
|
" ext_ok = True\n"
|
|
" break\n"
|
|
" except Exception:\n"
|
|
" time.sleep(2)\n"
|
|
" if not ext_ok:\n"
|
|
" print(' 외부 접속 검증 실패 (DNS 전파 지연이거나 Cloudflare 브라우저 체크 — 브라우저에서 직접 확인하세요)')\n"
|
|
" print(' ※ 임시 링크 — Colab 세션 종료 시 닫힘. API 호출에는 RAW_KEY(X-API-Key) 필요.')\n"
|
|
"else:\n"
|
|
" print('터널 URL 미감지 — 서버 로그:')\n"
|
|
" print(open('/content/logs/server.log').read()[-1200:])\n"
|
|
" print('로컬(8000)에서 계속 사용할 수 있습니다.')\n"
|
|
),
|
|
code(
|
|
"# 11) 업로드 → poll → 결과 (RAW_KEY는 셀 9에서 캡처된 값)\n"
|
|
"import json, time, urllib.request, urllib.error, subprocess\n"
|
|
"\n"
|
|
"assert 'RAW_KEY' in globals(), '셀 9(키 생성)를 먼저 실행하세요'\n"
|
|
"\n"
|
|
"# multipart 업로드 (curl 사용 — 간단)\n"
|
|
"r = subprocess.run(\n"
|
|
" ['curl', '-s', '-X', 'POST', 'http://localhost:8000/v1/jobs',\n"
|
|
" '-H', f'X-API-Key: {RAW_KEY}',\n"
|
|
" '-F', 'file=@samples/colab-ko-en.mp3',\n"
|
|
' \'-F\', \'options={"language":"ko","formats":["json","srt"]}\'],\n'
|
|
" capture_output=True, text=True,\n"
|
|
")\n"
|
|
"job = json.loads(r.stdout)\n"
|
|
"print('생성:', job)\n"
|
|
"job_id = job.get('job_id')\n"
|
|
"\n"
|
|
"# 완료까지 poll (auto-worker가 서버 프로세스에서 큐를 소비 — 첫 실행은\n"
|
|
"# 모델 다운로드 ~1.6GB 포함이라 최대 4분까지 대기)\n"
|
|
"if job_id:\n"
|
|
" print('job_id =', job_id)\n"
|
|
" for _ in range(120):\n"
|
|
" st = json.loads(urllib.request.urlopen(\n"
|
|
" urllib.request.Request(f'http://localhost:8000/v1/jobs/{job_id}',\n"
|
|
" headers={'X-API-Key': RAW_KEY})\n"
|
|
" ).read())\n"
|
|
" if st['status'] in ('completed', 'failed', 'cancelled'):\n"
|
|
" print('최종 상태:', st['status'])\n"
|
|
" break\n"
|
|
" time.sleep(2)\n"
|
|
" else:\n"
|
|
" print('폴링 60초 초과 — 서버 로그 확인: /content/logs/server.log')\n"
|
|
"else:\n"
|
|
" print('업로드 실패 — 서버 로그 확인: /content/logs/server.log')\n"
|
|
),
|
|
md(
|
|
"### 참고 — 서버에서 실제 전사까지 실행하려면\n\n"
|
|
"in-proc 브로커는 큐만 받고 워커가 별도로 소비해야 합니다. 워커 스레드를 띄우거나\n"
|
|
"간단하게는 배치 파이프라인을 직접 호출하는 CLI가 더 간단합니다. API 전체 흐름(워커 포함)은\n"
|
|
"다음 셀에서 `python`으로 브로커 + 워커를 함께 돌려 확인합니다."
|
|
),
|
|
code(
|
|
"# 12) 브로커 + 워커 포함 전체 흐름 (in-proc)\n"
|
|
"# 서버의 in-proc 브로커에 enqueue된 job을 같은 프로세스의 워커가 소비하는 구조는\n"
|
|
"# 프로세스 분리 필요 → 여기서는 클라이언트에서 직접 Worker.drain() 호출로 검증\n"
|
|
"# 커널 sys.path 보강 — editable 설치는 인터프리터 시작 시에만 반영되므로\n"
|
|
"# 실행 중인 커널은 src/를 직접 등록해야 한다 (셀 3에서도 처리하지만 방어적 가드)\n"
|
|
"import sys, os as _os\n"
|
|
"if not any(_os.path.isfile(_os.path.join(p, 'luke_scribe', 'config.py')) for p in sys.path):\n"
|
|
" _root = _os.getcwd()\n"
|
|
" while not _os.path.isdir(_os.path.join(_root, 'src', 'luke_scribe')) and _root != _os.path.dirname(_root):\n"
|
|
" _root = _os.path.dirname(_root)\n"
|
|
" sys.path.insert(0, _os.path.join(_root, 'src'))\n"
|
|
"from luke_scribe.config import Settings\n"
|
|
"from luke_scribe.jobqueue.broker import InProcBroker\n"
|
|
"from luke_scribe.jobqueue.worker import Worker\n"
|
|
"from luke_scribe.jobqueue.jobs import Job\n"
|
|
"from luke_scribe.results.store import ResultStore\n"
|
|
"\n"
|
|
"settings = Settings(_env_file=None, queue_backend='inproc',\n"
|
|
" results_dir='/content/results', model_cache_dir=None)\n"
|
|
"broker = InProcBroker(settings)\n"
|
|
"store = ResultStore(settings.results_dir)\n"
|
|
"\n"
|
|
"job = Job(type='file', lane='batch', source_path='samples/colab-ko-en.mp3',\n"
|
|
" options={'model': 'large-v3-turbo', 'language': 'ko', 'device': 'auto'})\n"
|
|
"broker.enqueue(job)\n"
|
|
"worker = Worker(settings=settings, broker=broker, store=store)\n"
|
|
"worker.drain()\n"
|
|
"\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"
|
|
" err = getattr(state, 'error_message', None) or getattr(state, 'error_code', None)\n"
|
|
" print('job 오류:', err or '없음')\n"
|
|
"else:\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"
|
|
),
|
|
md(
|
|
"## 7) 벤치마크 (turbo vs large-v3)\n\n"
|
|
"manifest에 모델 비교 항목이 있으면 실행합니다. 두 모델을 모두 다운로드하므로\n"
|
|
"시간이 걸립니다 (turbo ~1.6GB + large-v3 ~3GB)."
|
|
),
|
|
code(
|
|
"# 13) 벤치마크 — 샘플 manifest 사용 (선택, 시간 소요)\n"
|
|
"# entities는 {canonical, surface, start_char, end_char} dict여야 한다\n"
|
|
"# (문자열이면 entity_retention이 .get() 호출에 실패해 clip이 실패 처리됨)\n"
|
|
"import yaml\n"
|
|
"\n"
|
|
"REF_TEXT = '오늘은 vLLM 서버를 Kubernetes 클러스터에 배포하는 방법을 설명합니다. GPU 가속 추론으로 대기 시간을 줄일 수 있습니다.'\n"
|
|
"with open('/content/reference.txt', 'w', encoding='utf-8') as f:\n"
|
|
" f.write(REF_TEXT)\n"
|
|
"\n"
|
|
"entities = []\n"
|
|
"for name in ('vLLM', 'Kubernetes', 'GPU'):\n"
|
|
" idx = REF_TEXT.index(name)\n"
|
|
" entities.append({\n"
|
|
" 'canonical': name, 'surface': name,\n"
|
|
" 'start_char': idx, 'end_char': idx + len(name),\n"
|
|
" })\n"
|
|
"\n"
|
|
"manifest = {\n"
|
|
" 'name': 'colab-quick',\n"
|
|
" 'dataset_version': '1.0',\n"
|
|
" 'language': 'ko',\n"
|
|
" 'targets': {'entity_preservation': 0.95, 'cer': 0.15},\n"
|
|
" 'clips': [\n"
|
|
" {'id': 'ko-en-tech', 'audio_path': 'samples/colab-ko-en.mp3',\n"
|
|
" 'reference_path': '/content/reference.txt',\n"
|
|
" 'duration_sec': 10.5, 'entities': entities},\n"
|
|
" ],\n"
|
|
"}\n"
|
|
"yaml.safe_dump(manifest, open('/content/manifest.yaml', 'w'))\n"
|
|
"\n"
|
|
"# 실행 (기본: turbo만 → 빠름)\n"
|
|
"!luke-scribe bench /content/manifest.yaml --models large-v3-turbo --device auto --repeats 2 --output /content/bench-report.json 2>&1 | tail -20\n"
|
|
),
|
|
md(
|
|
"## 8) 문제 해결\n\n"
|
|
"### 전사 실패: `unsupported device cuda:0`\n\n"
|
|
'**근본 원인**: v0.1 엔진이 `device="cuda:0"`(인덱스 포함)를 그대로 CTranslate2에 전달했는데,\n'
|
|
'CTranslate2는 `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\n"
|
|
"1. `api_keys.json`에는 **다이제스트만** 저장되고 raw 키는 생성 시 1회만 출력됩니다\n"
|
|
" (보안 설계). 셀 9에서 `RAW_KEY`를 캡처했는지 확인하세요 — 파일에서 키를 읽으면 401.\n"
|
|
"2. **이전 실행에서 남은 서버**가 포트 8000을 점유하면 새 키를 모른 채 401이 납니다.\n"
|
|
" 셀 10이 시작 시 `pkill`로 기존 서버를 종료하고 키 인증(HTTP 200)까지 검증합니다.\n\n"
|
|
"### 업로드 후 job이 queued에 머무는 경우\n\n"
|
|
"in-proc 백엔드는 별도 워커 프로세스가 없으면 큐를 소비하지 못합니다.\n"
|
|
"셀 10이 `LUKESCRIBE_AUTO_WORKER=true`로 서버를 띄우면 서버 내부 워커 스레드가\n"
|
|
"업로드 → 완료까지 처리합니다 (v0.1에서 추가된 옵트인 기능).\n\n"
|
|
"### Cloudflare 터널로 외부에서 접속 (선택)\n\n"
|
|
"서버 셀 10이 완료되면 `https://xxx.trycloudflare.com` 외부 접속 링크가 출력됩니다.\n"
|
|
"- **임시 링크** — Colab 세션(서버 프로세스)이 살아 있는 동안만 유효하며, 세션 종료/재시작 시 닫힙니다.\n"
|
|
"- `/docs`(Swagger 대시보드)와 `/health`는 별도 키 없이 열리지만, **실제 API 호출은 `X-API-Key`(RAW_KEY)가 필요**합니다.\n"
|
|
"- cloudflared는 셀 3에서 설치됩니다. 다운로드 실패 시 터널 없이 로컬(8000)에서 계속 사용할 수 있습니다.\n"
|
|
"- 터널은 `http://localhost:8000`을 고정으로 바라봅니다. 다른 포트로 서버를 띄우면 터널이 연결되지 않습니다.\n"
|
|
"- trycloudflare는 **인증 없는 Quick Tunnel**이라 URL만 알면 누구나 접근 가능합니다. 민감 데이터는 올리지 마세요.\n\n"
|
|
"### `No module named 'luke_scribe'` (셀 12에서 import 실패)\n\n"
|
|
"Colab 커널은 실행 중에는 pip editable 설치가 만든 .pth를 읽지 못합니다\n"
|
|
"(인터프리터 시작 시에만 반영). 설치 셀 3이 커널 sys.path에 `src/`를 직접\n"
|
|
"등록해 해결했습니다 — 이 셀만 다시 실행하려면 설치 셀 3(또는 전체)을 재실행하세요.\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\n"
|
|
"1번 셀의 `GITEA_TOKEN`에 토큰을 입력하고 런타임 → **Restart session** 후 다시 실행하세요.\n"
|
|
),
|
|
]
|
|
|
|
|
|
def main() -> None:
|
|
nb = {
|
|
"nbformat": 4,
|
|
"nbformat_minor": 0,
|
|
"metadata": {
|
|
"colab": {"provenance": [], "gpuType": "T4"},
|
|
"kernelspec": {
|
|
"display_name": "Python 3",
|
|
"language": "python",
|
|
"name": "python3",
|
|
},
|
|
"language_info": {"name": "python", "version": "3.11"},
|
|
"accelerator": "GPU",
|
|
},
|
|
"cells": cells(),
|
|
}
|
|
OUT.parent.mkdir(parents=True, exist_ok=True)
|
|
OUT.write_text(json.dumps(nb, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
n_code = sum(1 for c in nb["cells"] if c["cell_type"] == "code")
|
|
print(f"✅ {OUT} 생성 (셀 {len(nb['cells'])}개, 코드 {n_code}개)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|