fix: normalize faster-whisper segments to dicts + in-proc auto-worker

Colab run 3 (A100) surfaced three GPU/API-path bugs mocks couldn't catch:

1. faster-whisper yields namedtuple Segments, but batch/bench consume
   them as dicts (.get) -> AttributeError 'Segment' has no attribute
   'get' on every real transcription. Engine now normalizes segments
   to dicts (_to_dict_segments) at the boundary.

2. API TranscribeOptions carries engine-irrelevant keys (formats,
   timestamps, glossary_id, post_correction, diarize); worker's
   TranscriptionOptions(**job.options) crashed with TypeError. Worker
   now filters job.options to TranscriptionOptions.__slots__.

3. in-proc server never consumed its own queue (jobs stayed queued
   forever). Added opt-in Settings.auto_worker (default off): lifespan
   starts a daemon Worker thread for inproc backend, stopped on
   shutdown. Notebook enables it via LUKESCRIBE_AUTO_WORKER=true so the
   API upload -> completed flow works end to end.

Notebook: bench manifest now uses clips schema (audio_path/duration_sec/
entities); cell 22 reads error_message/error_code; upload poll window
raised to 4min (first-run model download).

+ 5 tests (namedtuple/dict segments, API-style options, auto_worker
on/off); 136 tests pass, ruff clean.
This commit is contained in:
2026-08-12 17:32:18 +09:00
parent 741bce9fc6
commit 20777386fe
9 changed files with 218 additions and 16 deletions
+22
View File
@@ -9,6 +9,7 @@ Lifespan (plan §3.10a/§3.5):
from __future__ import annotations
import logging
import threading
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
@@ -63,6 +64,23 @@ def create_app(settings: Settings | None = None) -> FastAPI:
app.state.session_guard = _SessionGuard(settings)
# in-proc 백엔드 + auto_worker: 같은 프로세스 워커 스레드가 큐를 소비
# (dev/Colab에서 별도 워커 프로세스 없이 업로드 → 완료 흐름이 가능하게 함)
inproc_worker = None
if settings.queue_backend == "inproc" and settings.auto_worker:
from ..jobqueue.worker import Worker
inproc_worker = Worker(
settings=settings,
broker=broker,
store=store,
owner=owner,
worker_id="api-inproc",
)
threading.Thread(target=inproc_worker.run_forever, daemon=True).start()
logger.info("in-proc auto-worker 스레드 시작 (queue_backend=inproc, auto_worker=true)")
app.state.inproc_worker = inproc_worker
# 모델 프로비저닝 (선택) — 설정된 경우에만
model_cache = settings.model_cache_dir
if model_cache:
@@ -99,6 +117,10 @@ def create_app(settings: Settings | None = None) -> FastAPI:
tunnel.stop()
except Exception:
pass
if inproc_worker is not None:
# stop()은 다음 루프 반복에서 반영 — 진행 중 job은 끝까지 완료 후
# 스레드가 종료된다 (daemon + 프로세스 종료 흐름에서는 안전)
inproc_worker.stop()
owner.unload_all()
app = FastAPI(