feat(cli): --beam-size + --correct; add COLAB.md GPU full-transcribe guide
- transcribe: --beam-size(CPU 속도), --correct(사내 LLM 청크 보정, SCRIBE_LLM_*), config.beam_size(CPU 1~2 권장). 보정 시 전체 수집 후 한 번에 출력. - COLAB.md: Colab(전사 전용·게이트 미도달) + 온프렘 GPU(전사+보정 풀 파이프라인) 가이드. 23 tests pass, ruff clean. --correct 미설정 시 우아한 에러 검증. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
|||||||
|
# Colab / GPU 풀 전사 가이드
|
||||||
|
|
||||||
|
GPU 환경(Colab T4/A100 또는 온프렘 GPU)에서 **풀 강연을 빠르게** 전사(+선택 보정)합니다.
|
||||||
|
CPU(개발 박스)는 풀 강연이 느려(turbo ~RTF 5×) 비권장 — 여기서 돌리세요.
|
||||||
|
GPU(T4)에서 turbo는 대략 실시간의 ~0.1~0.3× → **37분 강연이 수 분**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## A) Google Colab — 전사 전용
|
||||||
|
|
||||||
|
> Colab은 외부 클라우드라 **사내 LLM 게이트(192.168.0.123)에 못 닿습니다** → `--correct`(보정) 불가, **전사만**.
|
||||||
|
> 런타임 → 런타임 유형 변경 → **GPU(T4)** 선택.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 1) 시스템 의존성 + uv
|
||||||
|
!apt-get -qq update && apt-get -qq install -y ffmpeg
|
||||||
|
!curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
|
import os; os.environ["PATH"] = "/root/.local/bin:" + os.environ["PATH"]
|
||||||
|
|
||||||
|
# 2) 코드 (저장소 익명 read 허용)
|
||||||
|
!git clone -b feat/p1-core https://git.lukehemmin.com/lukehemmin/luke_scribe.git
|
||||||
|
%cd luke_scribe
|
||||||
|
|
||||||
|
# 3) 의존성 (엔진 + GPU CUDA 런타임)
|
||||||
|
!uv sync --extra engine --extra gpu
|
||||||
|
|
||||||
|
# 4) GPU 인식 확인 (T3면 turbo+large-v3 동시상주)
|
||||||
|
!uv run luke-scribe detect
|
||||||
|
|
||||||
|
# 5) 오디오 업로드 (또는 Drive 마운트)
|
||||||
|
from google.colab import files
|
||||||
|
AUDIO = list(files.upload().keys())[0]
|
||||||
|
|
||||||
|
# 6) 풀 전사 (large-v3-turbo) — 더 높은 정확도는 --model large-v3
|
||||||
|
!uv run luke-scribe transcribe "$AUDIO" --model large-v3-turbo --language ko --timestamps | tee transcript.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Colab을 API로 외부 노출하려면
|
||||||
|
```python
|
||||||
|
# cloudflared 공개 URL 발급 → 외부에서 curl
|
||||||
|
!uv sync --extra engine --extra gpu --extra api
|
||||||
|
import subprocess, os
|
||||||
|
os.environ["SCRIBE_API_KEYS"] = '["colab-test"]'
|
||||||
|
!nohup uv run luke-scribe serve --host 0.0.0.0 --port 8000 --tunnel cloudflare > serve.log 2>&1 &
|
||||||
|
import time; time.sleep(8); print(open("serve.log").read()) # public *.trycloudflare.com URL 확인
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## B) 온프렘 GPU — 전사 + 사내 LLM 보정 (풀 파이프라인)
|
||||||
|
|
||||||
|
사내망(게이트 192.168.0.123 도달) + GPU 머신이면 **음차→영문 복원까지** 한 번에:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone -b feat/p1-core https://git.lukehemmin.com/lukehemmin/luke_scribe.git && cd luke_scribe
|
||||||
|
uv sync --extra engine --extra gpu
|
||||||
|
|
||||||
|
export SCRIBE_LLM_BASE_URL=http://192.168.0.123:8080/v1
|
||||||
|
export SCRIBE_LLM_API_KEY=<사내 키> # 셸 히스토리 주의
|
||||||
|
export SCRIBE_LLM_MODEL=copilot-gpt-4o
|
||||||
|
export SCRIBE_LLM_MAX_CHARS=3000 # 사내 LLM 컨텍스트 창에 맞춰(~8k→1500/~16k→3000/~30k→6000)
|
||||||
|
|
||||||
|
# 전사 + 청크 보정을 한 명령으로
|
||||||
|
uv run luke-scribe transcribe talk.m4a --model large-v3-turbo --language ko --correct | tee transcript.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
API로:
|
||||||
|
```bash
|
||||||
|
uv run luke-scribe serve # 출력된 X-API-Key 사용
|
||||||
|
curl -H "X-API-Key: <키>" -F file=@talk.m4a -F model=large-v3-turbo -F correct=true \
|
||||||
|
http://localhost:8000/v1/transcribe
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 참고
|
||||||
|
- 보정은 긴 전사를 `SCRIBE_LLM_MAX_CHARS` 청크로 분할 + **러닝 글로서리**로 처리(작은 컨텍스트 창 대응).
|
||||||
|
- 약 GPU(1050/2GB)는 turbo도 안 들어가 자동으로 **CPU(T0)** 로 강등 — `detect`로 등급 확인.
|
||||||
|
- 오디오 파일은 저장소에 없음(`.gitignore`) — Colab 업로드/Drive 또는 온프렘 로컬 경로 사용.
|
||||||
+34
-4
@@ -55,6 +55,8 @@ def transcribe(
|
|||||||
device: str = typer.Option("auto", help="auto|cpu|cuda"),
|
device: str = typer.Option("auto", help="auto|cpu|cuda"),
|
||||||
word_timestamps: bool = typer.Option(False, "--word-timestamps"),
|
word_timestamps: bool = typer.Option(False, "--word-timestamps"),
|
||||||
vad: bool = typer.Option(True, "--vad/--no-vad", help="무음 제거"),
|
vad: bool = typer.Option(True, "--vad/--no-vad", help="무음 제거"),
|
||||||
|
beam_size: int = typer.Option(None, "--beam-size", help="디코딩 빔(CPU 1~2 권장=속도↑)"),
|
||||||
|
correct: bool = typer.Option(False, "--correct", help="사내 LLM 보정(SCRIBE_LLM_* 설정 필요)"),
|
||||||
timestamps: bool = typer.Option(False, "--timestamps", help="세그먼트 [start–end] 표시"),
|
timestamps: bool = typer.Option(False, "--timestamps", help="세그먼트 [start–end] 표시"),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""단발 파일 전사 (faster-whisper, CPU/GPU 자동, AC-4 일부)."""
|
"""단발 파일 전사 (faster-whisper, CPU/GPU 자동, AC-4 일부)."""
|
||||||
@@ -90,17 +92,45 @@ def transcribe(
|
|||||||
)
|
)
|
||||||
|
|
||||||
engine = FasterWhisperEngine(model_name, dev, profile.compute_type, cache_dir=settings.model_cache_dir)
|
engine = FasterWhisperEngine(model_name, dev, profile.compute_type, cache_dir=settings.model_cache_dir)
|
||||||
segments, tinfo = engine.transcribe(file, language=lang, word_timestamps=word_timestamps, vad=vad)
|
segments, tinfo = engine.transcribe(
|
||||||
|
file, language=lang, word_timestamps=word_timestamps, vad=vad,
|
||||||
|
beam_size=(beam_size or settings.beam_size),
|
||||||
|
)
|
||||||
|
|
||||||
count = 0
|
seg_list = []
|
||||||
for seg in segments:
|
for seg in segments:
|
||||||
count += 1
|
seg_list.append({"start": seg.start, "end": seg.end, "text": seg.text.strip()})
|
||||||
|
if not correct: # 스트리밍 출력(보정 시엔 전체를 모은 뒤 한 번에)
|
||||||
if timestamps:
|
if timestamps:
|
||||||
console.print(f"[cyan][{seg.start:6.2f}–{seg.end:6.2f}][/] {seg.text.strip()}")
|
console.print(f"[cyan][{seg.start:6.2f}–{seg.end:6.2f}][/] {seg.text.strip()}")
|
||||||
else:
|
else:
|
||||||
console.print(seg.text.strip())
|
console.print(seg.text.strip())
|
||||||
|
|
||||||
|
if correct:
|
||||||
|
from .postprocess import llm as llm_correct
|
||||||
|
from .postprocess import rules
|
||||||
|
|
||||||
|
text = " ".join(s["text"] for s in seg_list).strip()
|
||||||
|
try:
|
||||||
|
text = rules.normalize(
|
||||||
|
llm_correct.correct(
|
||||||
|
text,
|
||||||
|
base_url=settings.llm_base_url,
|
||||||
|
api_key=settings.llm_api_key,
|
||||||
|
model=settings.llm_model,
|
||||||
|
max_chars=settings.llm_max_chars,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except llm_correct.LLMNotConfigured as exc:
|
||||||
|
console.print(f"[red]--correct:[/] {exc}")
|
||||||
|
raise typer.Exit(code=1) from exc
|
||||||
|
console.print(text)
|
||||||
|
|
||||||
detected = getattr(tinfo, "language", None)
|
detected = getattr(tinfo, "language", None)
|
||||||
console.print(f"[green]✓ {count} segments · detected_lang={detected} · model_used={model_name}[/]")
|
console.print(
|
||||||
|
f"[green]✓ {len(seg_list)} segments · detected_lang={detected} · "
|
||||||
|
f"model_used={model_name} · corrected={correct}[/]"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class Settings(BaseSettings):
|
|||||||
device: str = "auto"
|
device: str = "auto"
|
||||||
compute_type: str | None = None # None=자동(cc/VRAM 기반)
|
compute_type: str | None = None # None=자동(cc/VRAM 기반)
|
||||||
workers: int | None = None # None=자동 산정
|
workers: int | None = None # None=자동 산정
|
||||||
|
beam_size: int = 5 # 디코딩 빔(CPU는 1~2 권장=속도↑, GPU는 5)
|
||||||
|
|
||||||
# 언어 (기본 ko, 요청별 override)
|
# 언어 (기본 ko, 요청별 override)
|
||||||
language: str = "ko"
|
language: str = "ko"
|
||||||
|
|||||||
Reference in New Issue
Block a user