# py_faster_whisper: как звук проходит через систему

Статус: **as-built по checkout**, 16 сентября 2026 года.  
Git revision: `21c85b0c7bc7a96497ce78ec2ead44b113f18db6` (`main`).  
Основание: исходники, тесты, effective local configuration, Docker/DRS и CI этого репозитория.

Запущенные контейнеры, image digests, Swarm/Insight, production NATS-трафик и end-to-end работа внешних моделей в этом проходе не проверялись.

## 1. Что это за система

`py_faster_whisper` принимает речь и организует её обработку внешними моделями. Сам репозиторий не содержит модель Whisper: он принимает аудио, подготавливает его, вызывает LinTO ASR и сервисы диаризации, хранит состояние задания и возвращает текст с говорящими.

Работа разделена на три контура:

1. **Live bridge** собирает короткие фразы из NATS-потока и возвращает текстовые события.
2. **Automatic file** принимает целый файл и сам доводит его до готового результата.
3. **Staged file** позволяет отдельно запускать подготовку, распознавание, диаризацию и объединение.

Все три контура запускаются из `python -m src.stt_whisper_to_nats`. Дальше они расходятся по разным моделям состояния и способам выдачи результата.

`VERIFIED / SOURCE`: `src/stt_whisper_to_nats.py:1615-1688`, `src/webui/server.py`, `src/webui/staged_jobs.py`.

## 2. Как читать доказательства

| Метка | Что она означает |
|---|---|
| `VERIFIED / SOURCE` | Связь подтверждена кодом, тестом или effective configuration этого checkout. |
| `VERIFIED / RUNTIME (local)` | Поведение наблюдалось при локальном запуске. Это не доказательство production runtime. |
| `INFERRED` | Вывод следует из нескольких источников, но не подтверждён прямым test/trace. |
| `UNKNOWN` | Доказательств недостаточно или нужный слой не проверялся. |

Исходники, pipeline, образ, desired stack, запущенный runtime и конкретный пользовательский сценарий — разные уровни доказательств. Успех на одном уровне не подтверждает следующий.

## 3. Где проходит граница репозитория

```mermaid
flowchart LR
    Live[Live producer] -->|PCM16 через NATS| Repo[py_faster_whisper]
    User[Человек / HTTP-клиент] -->|медиафайл| Repo
    Repo -->|multipart HTTP| ASR[LinTO ASR]
    Repo -->|аудио + metadata| Diar[Diarization backend]
    Repo -->|state + artifacts| FS[(Local filesystem)]
    Repo -->|progress + result| NATS[(NATS)]
    Repo -->|HTTP snapshot| User
```

Как читать схему:

- **Live producer** принадлежит соседней системе и передаёт короткие PCM16-пакеты.
- **Репозиторий** управляет порядком действий: готовит аудио, вызывает модели и собирает результат.
- **LinTO и diarization backend** выполняют тяжёлую модельную работу за HTTP-границей.
- **Filesystem** хранит файловые задания; **NATS** переносит live-аудио и события.
- **Человек или HTTP-клиент** видит состояние задания и готовый текст через WebUI/API.

WebRTC ingress, NATS infrastructure, LinTO, внешние H100 routes, registry и Swarm находятся за этой границей. Поэтому `/health = ok` у WebUI подтверждает процесс, но не готовность моделей.

`VERIFIED / SOURCE`: `src/stt_whisper_to_nats.py:83-145`, `src/webui/server.py:1075-1194`, `stack/stack.drs:12-70`.

## 4. Один entrypoint, три контура работы

```mermaid
flowchart TB
    Main[_main_async] --> Live[SttWhisperToNatsService]
    Main --> WebUI[run_webui / create_app]
    WebUI --> Mode{WEBUI_PAGE_MODE}
    Mode -->|diarize / transcribe| Auto[FileJobWorkerService]
    Mode -->|staged / summary / bench / solution / case| Staged[StagedFileJobService]
    Live --> Text[короткие фразы в NATS]
    Auto --> Result[готовый file job]
    Staged --> Steps[повторяемые шаги и artifacts]
```

Блоки схемы:

- **`_main_async`** читает конфигурацию и решает, запускать live bridge, WebUI или оба процесса.
- **`SttWhisperToNatsService`** владеет буферами живого аудио и закрывает фразы по VAD/тайм-ауту.
- **`FileJobWorkerService`** выполняет полный файловый процесс без ручного управления шагами.
- **`StagedFileJobService`** работает с уже сохранёнными промежуточными результатами.
- **`WEBUI_PAGE_MODE`** выбирает нужный контур на HTTP-границе.

Эти контуры используют общие адаптеры LinTO, диаризации и transport, но по-разному хранят состояние. Поэтому их полезно разбирать отдельно.

`VERIFIED / SOURCE`: `src/stt_whisper_to_nats.py:83-145,586-745,1615-1688`, `src/webui/server.py:1075-1194`.

## 5. Контур 1: Live bridge

### Роль

Live bridge превращает непрерывный поток PCM16 в отдельные текстовые фразы. Он нужен, когда результат должен появляться во время разговора, а не после загрузки целого файла.

```mermaid
sequenceDiagram
    participant P as Live producer
    participant N as NATS
    participant B as Live bridge
    participant V as Silero VAD
    participant L as LinTO
    participant C as Consumer

    P->>N: stream.{token}: PCM16 frame
    N->>B: binary wire packet
    B->>V: накопленный buffer 16 kHz
    V-->>B: speech timestamps
    B->>N: pending + phrase_id
    B->>L: temporary WAV
    L-->>B: text
    B->>N: result / error
    N-->>C: phrase event
```

Что происходит по блокам:

- **Producer** отправляет metadata и PCM16 samples в `inference.whisper.stream.*`.
- **Bridge** ведёт отдельный `PhraseStreamState` для каждого output subject.
- **Silero VAD** находит участки речи и помогает определить момент закрытия фразы.
- **LinTO** получает временный WAV и возвращает текст.
- **NATS consumer** получает `pending`, затем `result` или `error` с тем же `phrase_id`.

### Ограничение

Буфер живёт в памяти процесса. После restart незавершённая фраза не восстанавливается. Временный WAV удаляется в `finally` после обращения к LinTO.

`VERIFIED / SOURCE`: `src/stt_whisper_to_nats.py:83-438`, `src/utils/wire_packet.py`, `src/utils/silero_onnx_vad.py`.

## 6. Контур 2: Automatic file

### Роль

Automatic file нужен для простого сценария: человек загружает файл и ждёт готовый текст. Runner сам проходит подготовку, ASR, диаризацию, объединение и cleanup.

```mermaid
sequenceDiagram
    participant U as Browser / API
    participant W as WebUI
    participant R as File runner
    participant S as FileJobStore
    participant L as LinTO
    participant D as Diarization

    U->>W: POST /api/file-transcribe
    W->>R: start_job(job_id, session_id)
    R->>S: source + starting state
    par Распознавание
        R->>L: chunks /transcribe
        L-->>R: words + timestamps
    and Разделение говорящих
        R->>D: audio artifact /diarize
        D-->>R: speaker segments
    end
    R->>R: words + segments → turns
    R->>S: transcript + combined result
    U->>W: GET /api/file-job
    W-->>U: state + result
```

Что происходит по блокам:

- **WebUI** проверяет вход, создаёт `job_id` и передаёт работу runner.
- **Runner** нормализует звук в mono PCM WAV 16 kHz и режет длинную запись на перекрывающиеся chunks.
- **LinTO** возвращает слова с метками времени; без них нельзя надёжно назначить говорящих.
- **Diarization backend** возвращает интервалы говорящих на той же временной шкале.
- **Runner** сопоставляет слова и интервалы, строит turns и сохраняет результат в `FileJobStore`.

### Ветка ошибки

Если пользователь запросил диаризацию, а backend недоступен, WebUI может продолжить транскрипцию без говорящих. Причина сохраняется в `diarization_fallback_reason`; отсутствие speakers не выдаётся за полный результат.

`VERIFIED / SOURCE`: `src/webui/server.py:603-793`, `src/stt_whisper_to_nats.py:668-1542`, `src/webui/file_mode.py:839-1253`.

## 7. Контур 3: Staged file

### Роль

Staged file разделяет дорогую обработку на повторяемые шаги. Это полезно для сравнения моделей и настроек на одном аудио: файл не нужно загружать и нормализовать заново.

```mermaid
flowchart LR
    Upload[Upload] --> Prepare[Prepare]
    Prepare --> T[Transcribe]
    Prepare --> D[Diarize]
    T --> TT[(transcription.json)]
    D --> DD[(diarization.json)]
    TT --> C[Combine]
    DD --> C
    C --> R[(combined.json)]
```

Что происходит по блокам:

- **Prepare** сохраняет исходник и создаёт рабочее аудио.
- **Transcribe** получает слова с timestamps и записывает `transcription.json`.
- **Diarize** получает speaker segments и записывает `diarization.json`.
- **Combine** читает оба сохранённых результата и не вызывает модели повторно.
- **`combined.json`** становится итогом, который WebUI возвращает через job snapshot.

`Combine` остаётся заблокирован, пока не готовы обе ветки. Один активный step защищён локальной блокировкой; optional HTTP lock добавляет lease между процессами и страницами.

`VERIFIED / SOURCE`: `src/webui/staged_jobs.py:146-540`, `src/webui/job_lock.py`, `tests/test_diarization.py`.

## 8. Как живут данные файлового задания

```mermaid
flowchart LR
    Input[input.ext] --> Normalize[normalized.wav]
    Normalize --> Chunks[chunks/*.wav]
    Chunks --> Words[transcription.json]
    Input --> Audio[diarization_input.m4a]
    Audio --> Segments[diarization.json]
    Words --> Merge[Attribution + turns]
    Segments --> Merge
    Merge --> Combined[combined.json]
    State[state.json] -. tracks .-> Normalize
    State -. tracks .-> Words
    State -. tracks .-> Segments
    State -. tracks .-> Combined
```

Разбор данных:

- **`input.ext`** — исходный upload; он определяет идентичность задания.
- **`normalized.wav` и `chunks/`** — рабочие данные для ASR.
- **`diarization_input.m4a`** или WAV — аудио, подготовленное для backend диаризации.
- **`transcription.json` и `diarization.json`** — две независимые ветки результата.
- **`combined.json`** соединяет текст и говорящих; **`state.json`** хранит progress и признаки готовности.

Корневой каталог по умолчанию: `локальный источник<job_id>/`. Automatic job планирует удаление примерно через 30 секунд после terminal completion. Сохранность staged artifacts зависит от runtime mount и cleanup policy; production retention остаётся `UNKNOWN`.

`VERIFIED / SOURCE`: `src/webui/file_mode.py:631-837`, `src/stt_whisper_to_nats.py:1491-1510`.

## 9. Где искать реализацию

```text
src/
├── stt_whisper_to_nats.py      # entrypoint, live bridge, automatic file runner
├── webui/
│   ├── server.py               # HTTP routes и выбор runner
│   ├── file_mode.py            # store, audio, chunks, LinTO client
│   ├── staged_jobs.py          # Prepare/Transcribe/Diarize/Combine
│   ├── job_lock.py             # optional shared lock
│   ├── diagnostics.py          # dependency checks
│   └── static/                 # browser UI
├── diarization.py              # backend client, attribution, turns
├── model_transport.py          # direct/CONNECT transport
├── *_diarization_server.py     # pyannote, sherpa, Sortformer
└── utils/                      # wire packet, VAD, audio helpers

tests/                          # contracts и orchestration branches
docker/                         # runtime images и local composition
stack/                          # desired legacy CPU deployment
```

Эта карта отвечает на вопрос «куда идти». `server.py` владеет HTTP-границей, runners — порядком действий, `file_mode.py` — рабочими файлами и LinTO client, `diarization.py` — назначением говорящих, `model_transport.py` — способом HTTP-доставки.

## 10. Два выборочных call tree

Call tree показывает реализацию ветки. Временной порядок участников уже показан sequence diagrams выше.

### Live path

```text
main
└── _main_async
    └── SttWhisperToNatsService.run
        └── _handle_message                 # NATS callback
            └── _process_message
                └── _check_stream_buffer
                    ├── _get_timestamps     # Silero VAD
                    └── _transcribe_and_publish
                        ├── transcribe_http_text
                        └── _publish_json   # NATS result
```

### File path

```text
run_webui
└── create_app
    └── POST /api/file-transcribe
        └── _file_http_handler
            └── FileJobWorkerService.start_job
                └── _process_job
                    ├── write_normalized_audio_wav
                    ├── transcribe_http_json
                    ├── diarize_http_json
                    ├── attribute_words / build_speaker_turns
                    └── FileJobStore writes + NATS events
```

Обе ветки сходятся на HTTP-адаптерах моделей, но расходятся по состоянию: live хранит буфер в памяти, file пишет каталог job.

## 11. Контракты, которые нельзя менять локально

| Граница | Контракт | Что проверить при изменении |
|---|---|---|
| Browser → WebUI | `POST /api/file-transcribe`, `/file-prepare`, `/file-{step}-step` | form fields, page mode, reset, snapshot |
| WebUI polling | `GET /api/file-job` | terminal/error states и rendering |
| Producer → live bridge | `inference.whisper.stream.*`, binary wire packet | producer и parser |
| Bridge → consumer | `inference.whisper.text.*`, pending/error/result JSON | correlation и terminal event |
| Runner → observers | `*.file.event.*`, `*.file.diar*` | job/session fanout |
| Runner → LinTO | multipart `file`; file mode ждёт word timestamps | parser, chunk merge, attribution |
| Runner → diarizer | audio + job metadata → segments | progress и domain mapping |

HTTP связывает caller и response во времени. NATS разделяет producer и consumer границей сообщения. Файлы связывают повторные шаги через сохранённое состояние. Похожая ошибка в UI требует разных проверок для каждой границы.

`VERIFIED / SOURCE`: `src/webui/server.py:1153-1178`, `src/webui/file_mode.py:285-366,1150-1253`, `src/model_transport.py`.

## 12. Три backend диаризации

| Backend | Что получает | Что отдаёт | Особенность runtime |
|---|---|---|---|
| pyannote | multipart audio + hints | segments, progress | HF access, lazy load, CPU route в desired stack |
| sherpa-onnx | multipart audio | speaker segments | CPU pipeline, ffmpeg normalization, model cache |
| Sortformer | multipart audio | stitched segments | NeMo/Triton, окна для длинного аудио |

Внешний HTTP shape похож, но backend нельзя считать взаимозаменяемым только по наличию `/diarize`. Desired `stack/stack.drs` направляет bridge на локальный pyannote; sherpa и Sortformer объявлены как отдельные сервисы без автоматического переключения.

`VERIFIED / SOURCE`: `src/*_diarization_server.py`, `src/diarization.py`, `stack/stack.drs:53-70`.

## 13. Что происходит при сбое

| Сбой | Наблюдаемый результат | Где искать |
|---|---|---|
| NATS недоступен | live bridge не стартует; file HTTP может работать без live events | entrypoint, NATS connection |
| LinTO не отвечает | transcription получает error; готового текста нет | `file_mode.py`, live publish path |
| Diarization недоступна | возможен transcript-only результат с явной причиной | `server.py`, runner state |
| Proxy route настроен, но не работает | запрос не падает скрыто на direct transport | `model_transport.py` |
| Job lock недоступен | остаётся только local one-active-task guard | `job_lock.py`, runners |
| Процесс перезапущен | live buffer теряется; file recovery зависит от сохранённых artifacts | live state, `FileJobStore` |

Здесь нет общего fallback-механизма. Каждый контур сохраняет собственную семантику ошибки, поэтому диагностика начинается с границы, на которой пропал результат.

## 14. Как исходники доходят до работающей системы

```mermaid
flowchart LR
    S[Source] --> P[Pipeline]
    P --> I[Image]
    I --> D[Desired DRS]
    D --> R[Running tasks]
    R --> B[Named behavior]
```

Состояние ступеней:

- **Source** — entrypoints, contracts и tests проверены в `21c85b0`.
- **Pipeline** — build/push описан; job с тестами закомментирован.
- **Image** — digest конкретной сборки не проверялся.
- **Desired DRS** — proxy, bridge и три sidecar описаны в `stack/stack.drs`.
- **Running tasks** — Swarm health/update state не проверялись.
- **Named behavior** — production live/file smoke не выполнялся.

Desired stack подключается к внешней сети `web-rtc-komaroff_default`. Deploy wrapper объединяет `otlp.drs + stack.drs` и вызывает Dry Stack с `--prune`; это описание намерения deployment, а не доказательство текущего runtime.

`VERIFIED / SOURCE`: `.gitlab-ci.yml`, `docker/build-images.sh`, `stack/stack.drs`, `stack/deploy.sh`. Остальные слои помечены выше.

## 15. Что защищают тесты

16 сентября 2026 года выполнено:

```bash
.venv/bin/python -m unittest discover -s tests -v
```

Результат: `Ran 115 tests in 0.943s — OK`. Предупреждения `aiohttp.NotAppKeyWarning` не привели к падениям.

Проверены transport, WebUI routes/modes, file state, staged steps, locks, diarization parsing/attribution, diagnostics, pyannote helpers и Sortformer stitching. Этот прогон не подтверждает реальные модели, production NATS routing, image/deployment state, cold/warm readiness и путь browser → model → result.

`VERIFIED / RUNTIME (local)`: локальный unittest run. CI сам по себе эти тесты сейчас не запускает — job в `.gitlab-ci.yml` закомментирован.

## 16. Риски и область влияния

| Причина | Последствие | Где проявится |
|---|---|---|
| `README.md` называет `stack/stack.drs` пустым, хотя в нём 5 сервисов | разработчик неверно определит deployment ownership | onboarding, delivery |
| `.version=0.0.20`, `src/.version=0.0.19` | разные части могут сообщать разные версии | image labels, metadata |
| CI tests job выключен | build/deploy не обязан выполнить 115 тестов | regression confidence |
| Job state по умолчанию в `/tmp` | сохранность зависит от mount | restart, staged jobs |
| Один active job/step на процесс | масштабирование требует общей coordination model | concurrent files |
| Оркестрационные модули занимают 1000–1688 строк | изменение контракта затрагивает несколько задач | review, regression surface |
| `/health` проверяет только процесс | зелёный route не означает готовую модель | readiness, diagnostics |

Таблица не требует немедленного рефакторинга. Она показывает, где локальная правка нуждается в более широкой проверке.

## 17. Если нужно изменить систему

| Задача | Начать здесь | Проверить до завершения |
|---|---|---|
| WebUI layout или текст | `src/webui/static/`, `server.py` | page modes и browser rendering |
| Upload/job API | `src/webui/server.py` | snapshot, reset, automatic/staged tests |
| Chunks или timestamps | `src/webui/file_mode.py` | overlap merge и LinTO JSON |
| Speaker attribution | `src/diarization.py` | midpoint/overlap и turns tests |
| Staged workflow | `src/webui/staged_jobs.py` | artifacts, invalidation, lock, cancel |
| Live phrase behavior | `SttWhisperToNatsService` | wire packet, VAD, events, consumer |
| Proxy/model route | `model_transport.py`, `bench.py` | fail-closed tests и diagnostics |
| Deployment | `stack/*.drs`, `.gitlab-ci.yml` | image → desired → running → smoke |

Сначала назовите наблюдаемый эффект и изменяемый контракт. Затем ищите файл. Такой порядок не даёт ограничиться одной функцией там, где меняются producer, consumer или runtime route.

## 18. Что остаётся неизвестным

1. Какие image digests реально запущены?
2. Совпадают ли runtime env, subjects и routes с DRS?
3. Какие backend диаризации доступны и прогреты сейчас?
4. Какой storage mount используется для file jobs и какова retention policy?
5. Есть ли production job lock service?
6. Каковы latency/error profile для live phrase и больших файлов?
7. Какая version file считается canonical?

Эти вопросы нельзя закрыть чтением исходников. Для ответа нужны CI/image/Insight/NATS/browser evidence.

## 19. Короткий маршрут нового разработчика

1. Начните с разделов 1–4 и выберите один из трёх контуров.
2. Пройдите его sequence diagram от входа до наблюдаемого результата.
3. Найдите соответствующий call tree и implementation anchors.
4. Выпишите контракт, который изменится: HTTP, NATS, artifact или env.
5. Проверьте tests обеих сторон границы.
6. Для delivery отдельно докажите image, desired DRS, running state и named behavior.

## 20. Claim ledger

| Утверждение | Статус | Evidence |
|---|---|---|
| Один entrypoint запускает live bridge и/или WebUI | `VERIFIED / SOURCE` | `src/stt_whisper_to_nats.py:1615-1688` |
| Live path использует NATS → VAD → LinTO → NATS | `VERIFIED / SOURCE` | `src/stt_whisper_to_nats.py:83-438` |
| File path создаёт normalized WAV, chunks и timed transcript | `VERIFIED / SOURCE` | `src/webui/file_mode.py:839-1253` |
| ASR и diarization могут выполняться параллельно | `VERIFIED / SOURCE` | `src/stt_whisper_to_nats.py:914-947` |
| Staged Combine не вызывает модели повторно | `VERIFIED / SOURCE` + tests | `src/webui/staged_jobs.py`, `tests/test_diarization.py` |
| Proxy path не делает silent direct fallback | `VERIFIED / SOURCE` + tests | `src/model_transport.py`, `tests/test_model_transport.py` |
| Desired stack использует local pyannote CPU | `VERIFIED / SOURCE` | `stack/stack.drs:53-64` |
| Production соответствует desired stack | `UNKNOWN` | runtime evidence не собиралось |
| End-to-end обработка реального файла сейчас работает | `UNKNOWN` | named smoke не выполнялся |
