Server cell sets LUKESCRIBE_TUNNEL=cloudflare so the app built-in CloudflareTunnel (trycloudflare quick tunnel) starts alongside the server. After health+auth checks the cell polls server.log for the trycloudflare URL, prints it with /docs dashboard and /health links, and verifies external reachability. cloudflared binary is downloaded in the install cell (graceful skip on failure). Troubleshooting section documents the temporary/public nature of the link.
21 KiB
21 KiB
In [ ]:
# 0) 런타임 확인 — T4 GPU가 활성 상태여야 합니다
!nvidia-smi
import sys
print('Python', sys.version.split()[0])
In [ ]:
# 1) 클론 또는 업데이트 (private 저장소면 GITEA_TOKEN 입력)
GITEA_TOKEN = '' # ← 필요 시 입력: https://git.lukehemmin.com/user/settings/applications
if GITEA_TOKEN:
REPO = f'https://{GITEA_TOKEN}@git.lukehemmin.com/lukehemmin/luke_scribe.git'
else:
REPO = 'https://git.lukehemmin.com/lukehemmin/luke_scribe.git'
import os, subprocess
if os.path.isdir('/content/luke_scribe/.git'):
# 이미 클론된 레포 → 최신 브랜치로 갱신 (pull)
print('기존 레포 감지 → pull로 갱신')
subprocess.run(['git', 'fetch', 'origin'], cwd='/content/luke_scribe', check=True)
subprocess.run(['git', 'checkout', 'feat/full-platform'], cwd='/content/luke_scribe', check=True)
subprocess.run(['git', 'pull', '--ff-only', 'origin', 'feat/full-platform'], cwd='/content/luke_scribe', check=True)
else:
# 최초 실행 → 클론
subprocess.run(['git', 'clone', '-b', 'feat/full-platform', REPO], cwd='/content', check=True)
os.chdir('/content/luke_scribe')
print('작업 디렉터리 →', os.getcwd())
!git log --oneline -1
In [ ]:
# 2) 시스템 패키지 (ffmpeg — ffprobe/정규화에 필수)
!apt-get update -qq && apt-get install -y -qq ffmpeg >/dev/null
!ffmpeg -version 2>&1 | head -1
!ffprobe -version 2>&1 | head -1
In [ ]:
# 3) 설치 — Colab은 시스템 pip가 표준 (venv는 Colab에서 ensurepip 오류로 실패할 수 있음)
!pip install -q --upgrade pip
!pip install -q -e '.[engine,api]'
!pip install -q edge-tts # 샘플 음성 생성용
# CUDA 13(Colab)에서는 CTranslate2 wheel(CUDA 12용)이 런타임 라이브러리를 못 찾음.
# CUDA 12 런타임(cuBLAS/cuDNN)을 pip로 설치하고, 실제 .so 위치를 find로 찾아
# LD_LIBRARY_PATH에 추가한다 (nvidia-*-cu12는 namespace package라 __file__이 없음).
!pip install -q nvidia-cublas-cu12 nvidia-cudnn-cu12
import os, site, subprocess
lib_dirs = set()
for lib in ('libcublas.so', 'libcudnn.so'):
out = subprocess.run(
['bash', '-c', f'find {site.getsitepackages()[0]} -name "{lib}*" 2>/dev/null | head -3'],
capture_output=True, text=True,
).stdout
for line in out.splitlines():
d = os.path.dirname(line)
if d:
lib_dirs.add(d)
os.environ['LD_LIBRARY_PATH'] = ':'.join(lib_dirs) + ':' + os.environ.get('LD_LIBRARY_PATH', '')
print('LD_LIBRARY_PATH:', os.environ['LD_LIBRARY_PATH'])
print('설치 완료')
# Cloudflare 터널용 cloudflared 바이너리 (실패해도 진행 — 터널 없이 로컬 사용 가능)
!wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -O /usr/local/bin/cloudflared || echo 'cloudflared 다운로드 실패 — 터널 없이 계속합니다'
!chmod +x /usr/local/bin/cloudflared 2>/dev/null; cloudflared --version 2>&1 | head -1 || echo 'cloudflared 미설치 — 터널 생략'
!which luke-scribe && luke-scribe --help 2>&1 | head -8
In [ ]:
# 4) CTranslate2 GPU 검증 — unsupported device cuda:0 해결 여부 확인
import ctranslate2
print('ctranslate2', ctranslate2.__version__)
print('CUDA device count:', ctranslate2.get_cuda_device_count())
if ctranslate2.get_cuda_device_count() > 0:
print('GPU 사용 가능 ✅ — 이후 전사는 GPU로 실행됩니다')
else:
print('GPU 사용 불가 — CPU 폴백 필요 (--device cpu --compute-type int8)')
In [ ]:
# 4) detect — GPU 감지 / 능력 등급 / 정밀도 / 워커수
!luke-scribe detect
In [ ]:
# 5) 테스트 스위트
!python -m pytest tests/ -q 2>&1 | tail -5
In [ ]:
# 6) 샘플 오디오 생성 (한국어 + 기술용어, 무료 TTS)
!mkdir -p samples
!edge-tts --voice ko-KR-SunHiNeural --text '오늘은 vLLM 서버를 Kubernetes 클러스터에 배포하는 방법을 설명합니다. GPU 가속 추론으로 대기 시간을 줄일 수 있습니다.' --write-media samples/colab-ko-en.mp3
!ffprobe -v error -show_entries format=duration -of json samples/colab-ko-en.mp3
In [ ]:
# 7) 실전 전사 — GPU 자동 감지 + 모델 다운로드 (large-v3-turbo)
# 첫 실행 시 Hugging Face에서 모델을 다운로드합니다 (turbo ≈ 1.6GB, 1~2분)
# v0.1: 'cuda:N' device를 분리해 CTranslate2 계약에 맞게 전달하도록 수정됨
# (기존: unsupported device cuda:0). 그래도 실패하면 CPU 폴백 안내가 출력됩니다.
import subprocess, json
r = subprocess.run(
['luke-scribe', 'transcribe', 'samples/colab-ko-en.mp3', '--language', 'ko', '--device', 'auto'],
capture_output=True, text=True,
)
print(r.stdout[-2500:] if r.stdout else '')
print(r.stderr[-800:] if r.stderr else '')
In [ ]:
# 8) hotword 포함 전사 (용어 보존 강화)
!luke-scribe transcribe samples/colab-ko-en.mp3 --language ko --device auto --hotword vLLM --hotword Kubernetes
In [ ]:
# 9) API 키 생성 — raw 키는 1회만 출력되므로 여기서 캡처한다
import subprocess, json, os
r = subprocess.run(
['luke-scribe', 'key', '--create', '--scopes', 'transcribe,admin', '--file', '/content/api_keys.json'],
capture_output=True, text=True,
)
created = json.loads(r.stdout)
RAW_KEY = created['key'] # 이후 셀에서 사용
print('key_id:', created['key_id'], '| scopes:', created['scopes'])
print('raw 키 캡처 완료 (표시 안 함)')
In [ ]:
# 10) 서버 기동 (in-proc 큐, 백그라운드 — nohup)
# 이전 실행에서 남은 서버가 포트 8000을 점유하면 새 키를 모른 채 401이 난다.
# → 먼저 기존 서버를 모두 종료하고, 키 파일 경로를 명시해 재시작한다.
import subprocess, os, time, urllib.request, urllib.error
assert 'RAW_KEY' in globals(), '셀 9(키 생성)를 먼저 실행하세요'
subprocess.run(['pkill', '-f', 'luke-scribe serve'], capture_output=True)
time.sleep(2)
# 서버가 읽을 키 파일/큐를 명시 (cwd 의존 제거)
os.environ['LUKESCRIBE_API_KEY_FILE'] = '/content/api_keys.json'
os.environ['LUKESCRIBE_QUEUE_BACKEND'] = 'inproc'
# in-proc 서버가 자체 워커 스레드로 큐를 소비 (업로드 → 완료까지 API 단독 처리)
os.environ['LUKESCRIBE_AUTO_WORKER'] = 'true'
# Cloudflare Quick Tunnel — 외부 접속 링크 발급 (cloudflared는 셀 3에서 설치)
os.environ['LUKESCRIBE_TUNNEL'] = 'cloudflare'
!mkdir -p /content/logs
!nohup luke-scribe serve --port 8000 > /content/logs/server.log 2>&1 &
ok = False
for _ in range(40):
try:
urllib.request.urlopen('http://localhost:8000/health', timeout=1)
ok = True
break
except Exception:
time.sleep(1)
if not ok:
print('서버 기동 실패 — 로그:')
print(open('/content/logs/server.log').read()[-1500:])
raise SystemExit('서버 기동 실패')
# 키 인증 검증 — RAW_KEY로 /v1/jobs 호출 → 200이어야 함 (401이면 종료)
req = urllib.request.Request('http://localhost:8000/v1/jobs', headers={'X-API-Key': RAW_KEY})
try:
resp = urllib.request.urlopen(req, timeout=5)
print('서버 기동 OK + 키 인증 OK (HTTP', resp.status, ')')
except urllib.error.HTTPError as e:
print('키 인증 실패 (HTTP', e.code, ') — 서버 로그:')
print(open('/content/logs/server.log').read()[-1500:])
raise SystemExit('키 인증 실패 — RAW_KEY 캡처/키 파일 확인')
# ── Cloudflare 터널 URL 캡처 + 외부 접속 검증 (서버 로그의 trycloudflare URL) ──
import re
TUNNEL_URL = None
for _ in range(60):
log = open('/content/logs/server.log', encoding='utf-8', errors='replace').read()
m = re.search(r'https://[a-z0-9-]+\.trycloudflare\.com', log)
if m:
TUNNEL_URL = m.group(0)
break
time.sleep(2)
if TUNNEL_URL:
print('🌐 Cloudflare 터널 (외부 접속):', TUNNEL_URL)
print(' 대시보드 (API Docs):', TUNNEL_URL + '/docs')
print(' 상태 (health):', TUNNEL_URL + '/health')
try:
ext = urllib.request.urlopen(TUNNEL_URL + '/health', timeout=15)
print(' 외부 접속 검증 OK (HTTP', ext.status, ')')
except Exception as exc:
print(' 외부 접속 검증 실패:', exc)
print(' ※ 임시 링크 — Colab 세션 종료 시 닫힘. API 호출에는 RAW_KEY(X-API-Key) 필요.')
else:
print('터널 URL 미감지 (cloudflared 미설치/연결 실패) — 서버 로그:')
print(open('/content/logs/server.log').read()[-1200:])
print('로컬(8000)에서 계속 사용할 수 있습니다.')
In [ ]:
# 11) 업로드 → poll → 결과 (RAW_KEY는 셀 9에서 캡처된 값)
import json, time, urllib.request, urllib.error, subprocess
assert 'RAW_KEY' in globals(), '셀 9(키 생성)를 먼저 실행하세요'
# multipart 업로드 (curl 사용 — 간단)
r = subprocess.run(
['curl', '-s', '-X', 'POST', 'http://localhost:8000/v1/jobs',
'-H', f'X-API-Key: {RAW_KEY}',
'-F', 'file=@samples/colab-ko-en.mp3',
'-F', 'options={"language":"ko","formats":["json","srt"]}'],
capture_output=True, text=True,
)
job = json.loads(r.stdout)
print('생성:', job)
job_id = job.get('job_id')
# 완료까지 poll (auto-worker가 서버 프로세스에서 큐를 소비 — 첫 실행은
# 모델 다운로드 ~1.6GB 포함이라 최대 4분까지 대기)
if job_id:
print('job_id =', job_id)
for _ in range(120):
st = json.loads(urllib.request.urlopen(
urllib.request.Request(f'http://localhost:8000/v1/jobs/{job_id}',
headers={'X-API-Key': RAW_KEY})
).read())
if st['status'] in ('completed', 'failed', 'cancelled'):
print('최종 상태:', st['status'])
break
time.sleep(2)
else:
print('폴링 60초 초과 — 서버 로그 확인: /content/logs/server.log')
else:
print('업로드 실패 — 서버 로그 확인: /content/logs/server.log')
In [ ]:
# 12) 브로커 + 워커 포함 전체 흐름 (in-proc)
# 서버의 in-proc 브로커에 enqueue된 job을 같은 프로세스의 워커가 소비하는 구조는
# 프로세스 분리 필요 → 여기서는 클라이언트에서 직접 Worker.drain() 호출로 검증
from luke_scribe.config import Settings
from luke_scribe.jobqueue.broker import InProcBroker
from luke_scribe.jobqueue.worker import Worker
from luke_scribe.jobqueue.jobs import Job
from luke_scribe.results.store import ResultStore
settings = Settings(_env_file=None, queue_backend='inproc',
results_dir='/content/results', model_cache_dir=None)
broker = InProcBroker(settings)
store = ResultStore(settings.results_dir)
job = Job(type='file', lane='batch', source_path='samples/colab-ko-en.mp3',
options={'model': 'large-v3-turbo', 'language': 'ko', 'device': 'auto'})
broker.enqueue(job)
worker = Worker(settings=settings, broker=broker, store=store)
worker.drain()
result = store.read_result(job.id)
if result is None:
# 전사 실패(예: GPU 초기화 실패) 시 결과가 저장되지 않음 — 상태/오류 출력
state = broker.get(job.id)
print('결과 없음 — job 상태:', getattr(state, 'status', 'unknown'))
if state is not None:
err = getattr(state, 'error_message', None) or getattr(state, 'error_code', None)
print('job 오류:', err or '없음')
else:
print('status:', result.status)
print('text:', result.text[:120])
print('device:', result.execution.device, '| ct:', result.execution.compute_type, '| rtf:', result.timings.rtf)
In [ ]:
# 13) 벤치마크 — 샘플 manifest 사용 (선택, 시간 소요)
# entities는 {canonical, surface, start_char, end_char} dict여야 한다
# (문자열이면 entity_retention이 .get() 호출에 실패해 clip이 실패 처리됨)
import yaml
REF_TEXT = '오늘은 vLLM 서버를 Kubernetes 클러스터에 배포하는 방법을 설명합니다. GPU 가속 추론으로 대기 시간을 줄일 수 있습니다.'
with open('/content/reference.txt', 'w', encoding='utf-8') as f:
f.write(REF_TEXT)
entities = []
for name in ('vLLM', 'Kubernetes', 'GPU'):
idx = REF_TEXT.index(name)
entities.append({
'canonical': name, 'surface': name,
'start_char': idx, 'end_char': idx + len(name),
})
manifest = {
'name': 'colab-quick',
'dataset_version': '1.0',
'language': 'ko',
'targets': {'entity_preservation': 0.95, 'cer': 0.15},
'clips': [
{'id': 'ko-en-tech', 'audio_path': 'samples/colab-ko-en.mp3',
'reference_path': '/content/reference.txt',
'duration_sec': 10.5, 'entities': entities},
],
}
yaml.safe_dump(manifest, open('/content/manifest.yaml', 'w'))
# 실행 (기본: turbo만 → 빠름)
!luke-scribe bench /content/manifest.yaml --models large-v3-turbo --device auto --repeats 2 --output /content/bench-report.json 2>&1 | tail -20