Whisper Diarization Stack
Этот документ описывает рабочий stack для /whisper-diarization, фактический обмен данными между сервисами и проблемы, которые проявились в production при запуске pyannote diarization.
Что сейчас считается рабочей схемой
- browser открывает
/whisper-diarization src/webui/server.pyпринимает upload и прямо в том же процессе запускаетFileJobWorkerService- перед стартом job на
/whisper-diarizationсервер делает submit-timeGET DIARIZATION_HEALTH_URL src/stt_whisper_to_nats.pyнормализует медиа вnormalized.wav, режет его на chunk-ы и отправляет chunk-ы вLINTO_HTTP_URL- при healthy diarization backend тот же runner параллельно отправляет компактный audio-only artifact в
DIARIZATION_HTTP_URL; вautoрежиме AAC-дорожка из MP4/M4A копируется без повторного encode - при unhealthy diarization backend job сохраняет
requested_diarization=true, но стартует сrun_diarization=falseи дружелюбной fallback reason src/pyannote_diarization_server.pyвыполняет speaker diarization и публикует real progress по NATS- browser получает:
- file job progress/result из
inference.whisper.file.event.<job_id> - diarization progress/result из
inference.whisper.file.diar_text.session.<session_id>, только еслиrun_diarization=true - HTTP snapshot и обычные file state event'ы с durable полями
requested_diarization,run_diarization,diarization_backend_status,diarization_fallback_reason
Текущий production deploy:
- ASR (
linto_stt_whisper_http) работает на GPU - stack config для diarization вынесен в
diarization-models.drs pyannote_diarization_sharedоставлен для совместимого shared MIG режима, аpyannote_diarization_isolatedиспользует отдельный MIG slice- image rollout для
pyannote_diarizationотделён от остальныхvoice-chat/*runtime images черезPYANNOTE_DIARIZATION_IMAGE_TAG - progress speaker separation приходит двумя слоями:
- coarse stage events из
stt_whisper_to_nats - hook-based real progress из
pyannote_diarization - если backend отвечает
404/5xxили недоступен по transport, UI больше не показывает сырой ingress error, а деградирует в transcript-only на той же странице
Архитектурная схема
Исходный Mermaid
flowchart LR
Browser["Browser /whisper-diarization"] -->|POST file-transcribe| WebUI["py_faster_whisper WebUI"]
WebUI -->|GET /healthz| Health["pyannote_diarization /healthz"]
WebUI -->|start_job(payload)| Worker["in-process FileJobWorkerService"]
Worker -->|normalized.wav chunks| LinTO["linto_stt_whisper_http /transcribe"]
Worker -->|audio artifact + job_id/session_id| Pyannote["pyannote_diarization /diarize"]
Worker -->|job events| NATS["NATS inference.*"]
LinTO -->|chunk JSON| Worker
Pyannote -->|final diarization JSON| Worker
Pyannote -->|diar_progress / diar_text_progress| NATS
Worker -->|job events + diar stage events| NATS
NATS -->|file.event + diar_text.session| BrowserSequence Diagram
Исходный Mermaid
sequenceDiagram
participant B as Browser
participant W as WebUI
participant H as Healthz
participant F as File Runner
participant N as NATS
participant L as LinTO HTTP
participant P as Pyannote Sidecar
B->>W: POST /api/file-transcribe (file, session_id)
W->>H: GET /healthz
H-->>W: 200 ok or 503/404
W->>F: start_job(requested_diarization, run_diarization, fallback_reason)
F->>N: prepare_media / chunk_* progress
F->>P: POST /diarize (only when run_diarization=true)
F->>L: POST /transcribe chunk #1..N
P->>N: diar_text_progress(step, completed, total, percent)
L-->>F: chunk transcript JSON
F->>N: job_done(text)
P-->>F: diarization JSON
F->>N: diar_done + diar_text_done
N-->>B: live updates in status + speaker transcriptUML State Diagram
Исходный Mermaid
stateDiagram-v2
[*] --> Uploaded
Uploaded --> BackendProbe
BackendProbe --> PreparingAudio: diarization ready
BackendProbe --> TranscriptOnlyFallback: diarization unavailable
TranscriptOnlyFallback --> PreparingAudio
PreparingAudio --> ChunkTranscription
PreparingAudio --> DiarizationStarted: run_diarization=true
ChunkTranscription --> ChunkTranscription: chunk_started / chunk_progress / chunk_done
DiarizationStarted --> DiarizationRunning: diar_text_progress
ChunkTranscription --> PlainTextReady: job_done
PlainTextReady --> WaitingSpeakerText: diarization still running
DiarizationRunning --> WaitingSpeakerText
WaitingSpeakerText --> SpeakerTranscriptReady: diar_text_done
DiarizationRunning --> DiarizationFailed: diar_text_error
ChunkTranscription --> Failed: job_error
SpeakerTranscriptReady --> [*]
DiarizationFailed --> [*]
Failed --> [*]Data Exchange
1. Browser -> WebUI
- endpoint:
POST /api/file-transcribe - fields:
job_idsession_idlanguagesyntax_postprocessfile- intent:
/whisper-trasncriptionвсегда даётrequested_diarization=false/whisper-diarizationвсегда даётrequested_diarization=true
2. WebUI -> Pyannote Health Probe
- transport: HTTP
GET /healthz - host override:
DIARIZATION_HEALTH_HOST - outcome:
- health ok ->
run_diarization=true - backend unavailable / pipeline not ready ->
run_diarization=false, сохраняетсяdiarization_fallback_reason
3. WebUI -> Local File Runner
- transport: direct in-process call
file_runner.start_job(...) - durable flags:
requested_diarizationrun_diarizationdiarization_backend_statusdiarization_fallback_reason- worker копирует эти поля в каждый
file.event.<job_id>/file.session.<session_id>payload, поэтому browser и snapshot читают уже готовый state без дополнительных inference rules
4. Worker -> LinTO
- transport: HTTP
POST /transcribe - unit of work: one normalized audio chunk
- result: JSON with
textandwords
5. Worker -> Pyannote Sidecar
- transport: HTTP
POST /diarize - file: compact audio-only artifact;
automay copy source AAC only while it stays under the upload guard, andnormalized.wavis used only for guarded short-file WAV mode - additive metadata fields:
job_idsession_idjob_file_name- speaker constraints:
num_speakersmin_speakersmax_speakers- этот шаг пропускается, если submit-time probe уже решил
run_diarization=false
6. Sidecar -> NATS
- structured subject:
inference.whisper.file.diar.event.<job_id>- text/session subjects:
inference.whisper.file.diar_text.event.<job_id>inference.whisper.file.diar_text.session.<session_id>
Progress events
diar_progressdiar_text_progress
Текущие поля progress payload:
typejob_idsession_idfile_namephase = diarize_runningstepcompletedtotalpercentdetails
Final events
diar_donediar_text_donediar_errordiar_text_error
7. WebUI Snapshot -> Browser
- endpoint:
GET /api/file-job?job_id=... - snapshot fields:
requested_diarizationrun_diarizationdiarization_backend_statusdiarization_fallback_reason- те же поля уже присутствуют в file state event'ах; snapshot используется для дочитывания transcript и raw diarization artifacts
/whisper-diarizationфильтрует jobs поrequested_diarization, а не поrun_diarization, поэтому downgraded transcript-only job остаётся видимым на той же странице
Runtime Codemap
Краткая карта файлов вынесена отдельно в ../CODEMAP.md.
Главные entrypoints этого stack:
src/stt_whisper_to_nats.pysrc/pyannote_diarization_server.pysrc/webui/server.pysrc/webui/static/app.jsdocker/diarization-base/Dockerfiledocker/diarization/Dockerfile../rag-stack/stack/diarization-models.drs
Проблемы, которые уже всплывали
1. TorchCodec / FFmpeg mismatch
Симптом:
Could not load libtorchcodecundefined symbol: aoti_torch_create_device_guard
Что оказалось причиной:
- несовместимая пара
torch 2.8.0+cu126иtorchcodec 0.11.0+cu126 - build graph держал старый override версии
torchcodec
Что сделали:
- зафиксировали
torchcodec 0.7.0+cu126 - убрали stale override
- сделали fail-fast smoke import на build/startup
2. Healthcheck restart loop
Симптом:
pyannote_diarizationстартовал, готовил pipeline, а потом контейнер пересоздавался примерно раз в минуту
Что оказалось причиной:
- сломанный inline
HEALTHCHECKс невалиднымpython -c
Что сделали:
- вынесли probe в
src.pyannote_healthcheck
3. GPU rollout и shared MIG
Исторический симптом production runtime:
RuntimeError: NVML_SUCCESS == r INTERNAL ASSERT FAILED at "/pytorch/c10/cuda/CUDACachingAllocator.cpp":1131
Что важно:
- ошибка возникала не на import/load, а на первом реальном inference внутри pyannote
- перенос sidecar на отдельный MIG slice проблему не снял
- это указывает не на бизнес-логику diarization, а на связку:
- PyTorch CUDA allocator
- NVML
- MIG runtime
- host driver / CUDA compatibility
Что изменилось сейчас:
- stack снова таргетит GPU runtime для
pyannote_diarization - batch sizes жёстко зажаты до
1 - сервис делит MIG slice с LinTO, поэтому operational failures теперь могут быть двух типов:
- GPU/NVML/runtime проблема во время inference
- service availability проблема до старта inference
То есть приложение больше не предполагает, что GPU всегда стабилен. Оно проверяет доступность backend-а заранее и умеет не блокировать transcription, если diarization backend сейчас недоступен.
4. Ingress 404 и backend unavailable
Симптом:
Speaker transcript error (worker_error): Diarization HTTP error 404: 404 page not found- либо пользователь видел plain text, но не понимал, почему speaker branch не стартовал
Что оказалось причиной:
- ingress route был жив, но за ним не было работающего
pyannote_diarization - возможные operational причины:
- сервис оставлен в
scale 0 - image tag отсутствует в registry
- backend ещё грузится и отдаёт
503 pipeline_not_ready
Что сделали:
- ввели submit-time health probe через
DIARIZATION_HEALTH_URL 404/5xxи transport errors теперь схлопываются в дружелюбноеSpeaker diarization unavailable: diarization backend is not reachable./whisper-diarizationсохраняетrequested_diarization=true, но может запускать job сrun_diarization=false- UI остаётся на diarization page, показывает transcript-only fallback и не ждёт speaker transcript бесконечно
5. Почему в UI стало понятнее, что происходит
Сейчас одновременно работают две защиты:
- coarse stage events из worker
- real hook-based progress из pyannote sidecar по NATS
Если diarization реально не стартовал, UI больше не маскирует это под "ещё подождите", а показывает явную fallback reason.
Текущие operational риски
pyannote_diarizationтеперь деплоится с отдельнымPYANNOTE_DIARIZATION_IMAGE_TAG; это убирает coupling с pinnedVOICE_CHAT_IMAGE_TAG, но требует держать отдельный image tag в registry.- shared MIG с LinTO остаётся узким местом для latency и memory pressure.
- health probe решает только доступность backend-а в момент submit. Если backend умрёт посередине job, transcription завершится, а diarization уйдёт в friendly error.
Для отдельной GPU-диагностики остаются те же практические направления:
- Уйти с shared MIG на отдельный slice или полный GPU и повторить pyannote inference.
- Проверить host driver / NVML against
torch==2.8.0+cu126runtime и пересобрать image при смене CUDA wheel set. - Протестировать allocator flags:
PYTORCH_CUDA_ALLOC_CONF=backend:cudaMallocAsyncPYTORCH_NO_CUDA_MEMORY_CACHING=1- Проверить ту же модель на другой версии PyTorch/CUDA wheels, а не только на
2.8.0+cu126.