diff --git a/notebooks/luke-scribe-colab.ipynb b/notebooks/luke-scribe-colab.ipynb index b418e67..674ab04 100644 --- a/notebooks/luke-scribe-colab.ipynb +++ b/notebooks/luke-scribe-colab.ipynb @@ -59,7 +59,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "# 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\nimport os, site, subprocess\nlib_dirs = set()\nfor 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)\nos.environ['LD_LIBRARY_PATH'] = ':'.join(lib_dirs) + ':' + os.environ.get('LD_LIBRARY_PATH', '')\nprint('LD_LIBRARY_PATH:', os.environ['LD_LIBRARY_PATH'])\nprint('설치 완료')\n\n# 실행 중인 Colab 커널은 새 .pth 파일을 읽지 못한다 (pip editable 설치는\n# 인터프리터 시작 시에만 반영). → 커널 sys.path에 src/를 직접 등록해\n# luke_scribe import를 보장한다 (서브프로세스 CLI는 .pth를 읽으므로 무관).\nimport sys, os as _os\n_root = _os.getcwd()\nwhile not _os.path.isdir(_os.path.join(_root, 'src', 'luke_scribe')) and _root != _os.path.dirname(_root):\n _root = _os.path.dirname(_root)\nsys.path.insert(0, _os.path.join(_root, 'src'))\nimport luke_scribe.config as _cfg\nprint('패키지 임포트 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" + "source": "# 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\nimport os, site, subprocess\nlib_dirs = set()\nfor 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)\nos.environ['LD_LIBRARY_PATH'] = ':'.join(lib_dirs) + ':' + os.environ.get('LD_LIBRARY_PATH', '')\nprint('LD_LIBRARY_PATH:', os.environ['LD_LIBRARY_PATH'])\nprint('설치 완료')\n\n# 실행 중인 Colab 커널은 새 .pth 파일을 읽지 못한다 (pip editable 설치는\n# 인터프리터 시작 시에만 반영). → 커널 sys.path에 src/를 직접 등록해\n# luke_scribe import를 보장한다 (서브프로세스 CLI는 .pth를 읽으므로 무관).\nimport sys, os as _os\n_root = _os.getcwd()\nwhile not _os.path.isdir(_os.path.join(_root, 'src', 'luke_scribe')) and _root != _os.path.dirname(_root):\n _root = _os.path.dirname(_root)\nsys.path.insert(0, _os.path.join(_root, 'src'))\ntry:\n import luke_scribe.config as _cfg\n print('패키지 임포트 OK →', _cfg.__file__)\nexcept ImportError as _e:\n print('경고: luke_scribe import 실패 —', _e)\n print(' 셀 1(클론)이 실행됐는지, 저장소가 src/luke_scribe 구조인지 확인하세요')\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" }, { "cell_type": "code", @@ -140,7 +140,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "# 10) 서버 기동 (in-proc 큐, 백그라운드 — nohup)\n# 이전 실행에서 남은 서버가 포트 8000을 점유하면 새 키를 모른 채 401이 난다.\n# → 먼저 기존 서버를 모두 종료하고, 키 파일 경로를 명시해 재시작한다.\nimport subprocess, os, re, shutil, 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# in-proc 서버가 자체 워커 스레드로 큐를 소비 (업로드 → 완료까지 API 단독 처리)\nos.environ['LUKESCRIBE_AUTO_WORKER'] = 'true'\n# Cloudflare Quick Tunnel — 외부 접속 링크 발급 (cloudflared는 셀 3에서 설치)\nos.environ['LUKESCRIBE_TUNNEL'] = 'cloudflare'\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\n# ── Cloudflare 터널 URL 캡처 + 외부 접속 검증 (서버 로그의 trycloudflare URL) ──\nTUNNEL_URL = None\nif 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)\nelse:\n print('cloudflared 미설치 (셀 3 다운로드 실패) — 터널 생략, 로컬(8000) 계속 사용')\n\nif 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) 필요.')\nelse:\n print('터널 URL 미감지 — 서버 로그:')\n print(open('/content/logs/server.log').read()[-1200:])\n print('로컬(8000)에서 계속 사용할 수 있습니다.')\n" + "source": "# 10) 서버 기동 (in-proc 큐, 백그라운드 — nohup)\n# 이전 실행에서 남은 서버가 포트 8000을 점유하면 새 키를 모른 채 401이 난다.\n# → 먼저 기존 서버를 모두 종료하고, 키 파일 경로를 명시해 재시작한다.\nimport subprocess, os, re, shutil, 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# in-proc 서버가 자체 워커 스레드로 큐를 소비 (업로드 → 완료까지 API 단독 처리)\nos.environ['LUKESCRIBE_AUTO_WORKER'] = 'true'\n# Cloudflare Quick Tunnel — 외부 접속 링크 발급 (cloudflared는 셀 3에서 설치)\nos.environ['LUKESCRIBE_TUNNEL'] = 'cloudflare'\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\n# ── Cloudflare 터널 URL 캡처 + 외부 접속 검증 (서버 로그의 trycloudflare URL) ──\nTUNNEL_URL = None\nif 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)\nelse:\n print('cloudflared 미설치 (셀 3 다운로드 실패) — 터널 생략, 로컬(8000) 계속 사용')\n\nif 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 _last_err = None\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 as _e:\n _last_err = _e\n time.sleep(2)\n if not ext_ok:\n print(' 외부 접속 검증 실패 (DNS 전파 지연이거나 Cloudflare 브라우저 체크 — 브라우저에서 직접 확인하세요):', _last_err)\n print(' ※ 임시 링크 — Colab 세션 종료 시 닫힘. API 호출에는 RAW_KEY(X-API-Key) 필요.')\nelse:\n print('터널 URL 미감지 — 서버 로그:')\n print(open('/content/logs/server.log').read()[-1200:])\n print('로컬(8000)에서 계속 사용할 수 있습니다.')\n" }, { "cell_type": "code", diff --git a/scripts/build_colab_notebook.py b/scripts/build_colab_notebook.py index 9167c71..f1fd9df 100644 --- a/scripts/build_colab_notebook.py +++ b/scripts/build_colab_notebook.py @@ -142,8 +142,12 @@ def cells() -> list[dict]: "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" + "try:\n" + " import luke_scribe.config as _cfg\n" + " print('패키지 임포트 OK →', _cfg.__file__)\n" + "except ImportError as _e:\n" + " print('경고: luke_scribe import 실패 —', _e)\n" + " print(' 셀 1(클론)이 실행됐는지, 저장소가 src/luke_scribe 구조인지 확인하세요')\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" @@ -285,16 +289,18 @@ def cells() -> list[dict]: " print(' 상태 (health):', TUNNEL_URL + '/health')\n" " # DNS 전파 지연 대비 재시도 (새 trycloudflare 호스트는 초반에 못 풀릴 수 있음)\n" " ext_ok = False\n" + " _last_err = None\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" + " except Exception as _e:\n" + " _last_err = _e\n" " time.sleep(2)\n" " if not ext_ok:\n" - " print(' 외부 접속 검증 실패 (DNS 전파 지연이거나 Cloudflare 브라우저 체크 — 브라우저에서 직접 확인하세요)')\n" + " print(' 외부 접속 검증 실패 (DNS 전파 지연이거나 Cloudflare 브라우저 체크 — 브라우저에서 직접 확인하세요):', _last_err)\n" " print(' ※ 임시 링크 — Colab 세션 종료 시 닫힘. API 호출에는 RAW_KEY(X-API-Key) 필요.')\n" "else:\n" " print('터널 URL 미감지 — 서버 로그:')\n"