diff --git a/notebooks/luke-scribe-colab.ipynb b/notebooks/luke-scribe-colab.ipynb index 137b884..311af09 100644 --- a/notebooks/luke-scribe-colab.ipynb +++ b/notebooks/luke-scribe-colab.ipynb @@ -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 # ── 외부 접속 검증 ──\n # Colab VM의 DNS가 새 trycloudflare 호스트를 해석 못 하는 경우가 있다\n # (실측: Name or service not known — 브라우저/폰에선 정상 접속).\n # 순서: ① urllib(시스템 DNS) ② 실패 시 DoH(Cloudflare DNS)로 우회 검증\n import socket\n _host = TUNNEL_URL.split('//')[1].split('/')[0]\n _dns_ip = None\n try:\n _dns_ip = socket.gethostbyname(_host)\n except socket.gaierror as _de:\n print(' (VM DNS 해석 실패:', _de, '— DoH로 우회 검증 시도)')\n\n ext_ok = False\n _last_err = None\n if _dns_ip:\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 and shutil.which('curl'):\n # VM DNS 우회: Cloudflare DoH로 호스트 해석 → 터널 직접 접속 (외부와 동일 경로)\n for _ in range(6):\n _c = subprocess.run(\n ['curl', '-sS', '-o', '/dev/null', '-w', '%{http_code}', '--max-time', '15',\n '--doh-url', 'https://cloudflare-dns.com/dns-query', TUNNEL_URL + '/health'],\n capture_output=True, text=True,\n )\n if _c.stdout.strip() == '200':\n print(' 외부 접속 검증 OK (DoH 우회, HTTP 200)')\n ext_ok = True\n break\n _last_err = _c.stdout.strip() or _c.stderr.strip() or _last_err\n time.sleep(2)\n if not ext_ok:\n print(' 자동 검증 불가 — 브라우저에서 직접 열어보세요:', TUNNEL_URL + '/docs')\n print(' (URL이 발급됐다는 건 Cloudflare edge↔서버 터널이 이미 연결된 상태입니다. 상세:', _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" + "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 # ── 외부 접속 검증 ──\n # Colab VM의 DNS가 새 trycloudflare 호스트를 해석 못 하는 경우가 있다\n # (실측: Name or service not known — 브라우저/폰에선 정상 접속).\n # 순서: ① urllib(시스템 DNS) ② 실패 시 DoH(Cloudflare DNS)로 우회 검증\n import socket\n _host = TUNNEL_URL.split('//')[1].split('/')[0]\n _dns_ip = None\n try:\n _dns_ip = socket.gethostbyname(_host)\n except socket.gaierror as _de:\n print(' (VM DNS 해석 실패:', _de, '— DoH로 우회 검증 시도)')\n\n ext_ok = False\n _last_err = None\n if _dns_ip:\n # DNS가 풀리면 터널도 이미 연결된 상태 — 2회면 충분\n for _ in range(2):\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 and shutil.which('curl'):\n if _dns_ip:\n print(' (시스템 DNS 경로 실패 — DoH로 우회 검증 시도)')\n # VM DNS 우회: Cloudflare DoH로 호스트 해석 → 터널 직접 접속 (외부와 동일 경로)\n for _ in range(4):\n _c = subprocess.run(\n ['curl', '-sS', '-o', '/dev/null', '-w', '%{http_code}', '--max-time', '10',\n '--doh-url', 'https://cloudflare-dns.com/dns-query', TUNNEL_URL + '/health'],\n capture_output=True, text=True,\n )\n _code = _c.stdout.strip()\n if _code == '200':\n print(' 외부 접속 검증 OK (DoH 우회, HTTP 200)')\n ext_ok = True\n break\n # '000'은 접속 실패 — stderr가 더 설명적\n if _code == '000':\n _last_err = _c.stderr.strip() or _last_err\n else:\n _last_err = _code or _c.stderr.strip() or _last_err\n time.sleep(2)\n if not ext_ok:\n print(' 자동 검증 불가 — 브라우저에서 직접 열어보세요:', TUNNEL_URL + '/docs')\n print(' (URL이 발급됐다는 건 Cloudflare edge↔서버 터널이 이미 연결된 상태입니다. 상세:', _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 d40f5de..0311cb1 100644 --- a/scripts/build_colab_notebook.py +++ b/scripts/build_colab_notebook.py @@ -302,7 +302,8 @@ def cells() -> list[dict]: " ext_ok = False\n" " _last_err = None\n" " if _dns_ip:\n" - " for _ in range(6):\n" + " # DNS가 풀리면 터널도 이미 연결된 상태 — 2회면 충분\n" + " for _ in range(2):\n" " try:\n" " ext = urllib.request.urlopen(TUNNEL_URL + '/health', timeout=15)\n" " print(' 외부 접속 검증 OK (HTTP', ext.status, ')')\n" @@ -312,18 +313,25 @@ def cells() -> list[dict]: " _last_err = _e\n" " time.sleep(2)\n" " if not ext_ok and shutil.which('curl'):\n" + " if _dns_ip:\n" + " print(' (시스템 DNS 경로 실패 — DoH로 우회 검증 시도)')\n" " # VM DNS 우회: Cloudflare DoH로 호스트 해석 → 터널 직접 접속 (외부와 동일 경로)\n" - " for _ in range(6):\n" + " for _ in range(4):\n" " _c = subprocess.run(\n" - " ['curl', '-sS', '-o', '/dev/null', '-w', '%{http_code}', '--max-time', '15',\n" + " ['curl', '-sS', '-o', '/dev/null', '-w', '%{http_code}', '--max-time', '10',\n" " '--doh-url', 'https://cloudflare-dns.com/dns-query', TUNNEL_URL + '/health'],\n" " capture_output=True, text=True,\n" " )\n" - " if _c.stdout.strip() == '200':\n" + " _code = _c.stdout.strip()\n" + " if _code == '200':\n" " print(' 외부 접속 검증 OK (DoH 우회, HTTP 200)')\n" " ext_ok = True\n" " break\n" - " _last_err = _c.stdout.strip() or _c.stderr.strip() or _last_err\n" + " # '000'은 접속 실패 — stderr가 더 설명적\n" + " if _code == '000':\n" + " _last_err = _c.stderr.strip() or _last_err\n" + " else:\n" + " _last_err = _code or _c.stderr.strip() or _last_err\n" " time.sleep(2)\n" " if not ext_ok:\n" " print(' 자동 검증 불가 — 브라우저에서 직접 열어보세요:', TUNNEL_URL + '/docs')\n"