# py_faster_whisper

Этот репозиторий обслуживает ASR bridge для `voice-chat` и file transcription WebUI.

## Codex workspace

Codex автоматически читает корневой [`AGENTS.md`](</experiments/voice-ai/archive/y/AGENTS.md>). Карта компонентов и штатные workflows находятся в [`.knowledge/project-inventory.md`](</experiments/voice-ai/archive/y/.knowledge/project-inventory.md>) и [`.knowledge/workflows.md`](</experiments/voice-ai/archive/y/.knowledge/workflows.md>).

Для checkout используется project overlay `monitorsoft:py-faster-whisper` и централизованно установленные Monitorsoft skills. Локальные копии `.agents/skills`, credentials и project-specific `.codex/config.toml` не требуются. При работе вне checkout project можно выбрать из корня `codex-workspace`:

```bash
python scripts/workspace.py scope set --company monitorsoft --project py-faster-whisper
```

Каноническая cross-project схема сервисов, subject-ов, payload-ов и внешнего `rag-stack`: [`webrtc-komaroff/DOCS/ARCHITECTURE.md`](</experiments/voice-ai/archive/c/DOCS/ARCHITECTURE.md>)

Текущий рабочий stack именно для `/whisper-diarization`, включая progress по NATS, CPU/GPU ограничения, sequence/UML diagrams и production проблемы:
- [DOCS/FILE_DIARIZATION_STACK.md](</experiments/voice-ai/archive/y/DOCS/FILE_DIARIZATION_STACK.md>)
- [CODEMAP.md](</experiments/voice-ai/archive/y/CODEMAP.md>)
- [readme_for_dumer.md](</experiments/voice-ai/archive/y/readme_for_dumer.md>)
- [../webrtc-komaroff/DOCS/DIAGNOSTICS.md](</experiments/voice-ai/archive/c/DOCS/DIAGNOSTICS.md>)

## Какие режимы реально есть

- live ASR для `/chat`:
  `src_core -> inference.whisper.stream.<session_id> -> stt_whisper_to_nats -> LinTO HTTP -> inference.whisper.text.<session_id> -> src_core`
- plain file ASR для `/whisper-trasncription`:
  `browser upload -> py_faster_whisper WebUI -> in-process file runner -> inference.whisper.file.event.<job_id> / inference.whisper.file.session.<session_id> -> browser`
- combined file ASR + diarization для `/whisper-diarization`:
  `browser upload -> submit-time /healthz probe -> in-process file runner -> LinTO HTTP + selected diarization sidecar -> inference.whisper.file.diar.event.<job_id> + inference.whisper.file.diar_text.event.<job_id> / inference.whisper.file.diar_text.session.<session_id>`
- diagnostics page для `/status/` (`/health/` остаётся compatibility alias):
  `browser -> py_faster_whisper diagnostics WebUI -> HTTP/NATS probes -> webrtc-komaroff + rag-stack/H100`

Отдельного локального runtime `src.main` больше нет. Основной entrypoint один:

```bash
python -m src.stt_whisper_to_nats
```

## Локальный запуск и открытие

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -U pip
pip install -r src/requirements.txt
cp src/env.example src/.env
# настрой NATS/LinTO и нужные runtime-параметры в локальном src/.env
set -a
source src/.env
set +a
python -m src.stt_whisper_to_nats
```

При стандартных `WEBUI_ENABLED=1` и `WEBUI_PORT=8090` WebUI доступен на `http://127.0.0.1:8090/`.

[`docker/docker-compose.yml`](#archive-availability) является build/composition descriptor, но не автономным one-command environment: ему нужны внешний NATS, LinTO, registry/build variables и заранее созданные external volumes `vad_models` и `whisper_out`.

## Production ownership

- Public WebUI и file routes объявлены в соседнем [`webrtc-komaroff/stack/webrtc.drs`](#archive-availability) и открываются на `https://webrtc-komaroff.gis-master.ru/`.
- GPU inference/diarization wiring находится также во внешних `rag-stack/stack/llm-models.drs` и `rag-stack/stack/diarization-models.drs`.
- Текущий [`stack/stack.drs`](#archive-availability) пуст. Deploy wrapper передаёт его Dry Stack, но он не воспроизводит наблюдаемый в Insight stack `py_faster_whisper`.
- Runtime stack можно наблюдать на `https://gis-master.ru/insight/stack?stack=py_faster_whisper`; его Git-owned deployment source пока не подтверждён.

## Что делает сервис

- читает live audio packets из `ASR_IN_PREFIX + session_id`
- режет поток на фразы через Silero VAD
- отправляет каждую фразу в `LINTO_HTTP_URL`
- публикует final `text` в `ASR_OUT_PREFIX + session_id`
- поднимает WebUI в двух page-mode режимах: plain transcription и diarization
- для file mode исполняет job прямо внутри WebUI runtime без отдельной очереди и без отдельного file worker
- для combined file mode может отдельно запускать выбранный diarization sidecar (`pyannote`, `sherpa-onnx`, `sortformer`) на компактном audio artifact; в `auto` режиме AAC-дорожка из MP4/M4A копируется без повторного encode, а fallback собирается из `normalized.wav`
- перед каждым upload на `/whisper-diarization` делает короткий health probe в `DIARIZATION_HEALTH_URL`; если backend недоступен, upload остаётся на той же странице и автоматически деградирует в transcript-only
- в режиме `WEBUI_PAGE_MODE=diagnostics` отдает страницу `/status/` и API `/status/api/diagnostics` для проверки связанного voice-chat stack

Важно: текущий live path использует `LINTO_HTTP_URL`. В конфигурации inference stack может быть развернут и LinTO websocket backend, но этот код в текущем состоянии его не использует.

## Subjects

- live input: `inference.whisper.stream.<session_id>`
- live output: `inference.whisper.text.<session_id>`
- file progress/result: `inference.whisper.file.event.<job_id>`
- file session stream: `inference.whisper.file.session.<session_id>`
- file diarization structured result: `inference.whisper.file.diar.event.<job_id>`
- file diarization speaker transcript: `inference.whisper.file.diar_text.event.<job_id>`
- file diarization speaker transcript session stream: `inference.whisper.file.diar_text.session.<session_id>`
- file diarization progress event types on those subjects:
  `diar_progress`, `diar_text_progress`

## `/whisper-trasncription` и `/whisper-diarization`

- `/whisper-trasncription` это plain file transcription page без speaker separation
- `/whisper-diarization` это отдельная combined page для text transcription + speaker separation
- на `/whisper-diarization`:
  - `Transcribe + Diarize` сначала проверяет `DIARIZATION_HEALTH_URL`
  - если backend здоров, запускается combined flow: text transcription + speaker separation
  - если backend недоступен, upload остаётся на `/whisper-diarization`, но job стартует как transcript-only
  - отдельного `Diarize only` режима на этой странице нет
  - `Speaker transcript` строится после ASR, потому что использует word timestamps
  - page affinity хранится отдельно от фактического выполнения diarization:
    - `requested_diarization=true` означает, что job был создан со страницы `/whisper-diarization`
  - `run_diarization=true` означает, что выбранный diarization sidecar реально запускался
    - каждый file state event и HTTP snapshot несёт эти флаги вместе с `diarization_backend_status` и `diarization_fallback_reason`, браузер их больше не восстанавливает из косвенных diarization payload'ов
  - при downgrade UI показывает friendly fallback reason, не рендерит `Waiting for final speaker transcript.` и оставляет download-кнопки speaker artifacts выключенными

- на `/whisper-trasncription`:
  - показывается только plain text transcription
  - `requested_diarization=false`, `run_diarization=false`
  - speaker transcript и diarization progress на этой странице не рендерятся

- для `/whisper-diarization` браузер загружает `wav/mp3/m4a/mp4`
- upload handler сохраняет исходный файл и сразу запускает local file job в том же сервисе
- file job нормализует audio, режет его на chunk-ы и шлёт их по одному в LinTO HTTP
- если submit-time health probe прошёл, параллельно запускается выбранный diarization sidecar на компактном audio-only artifact
- если submit-time health probe не прошёл, transcription всё равно выполняется, а fallback reason сохраняется в каждом file state event и в snapshot
- браузер получает compact progress/result сигналы по NATS WebSocket, а тяжёлые artifacts дочитывает по HTTP из того же сервиса

Legacy `/whisper-trasncription` intentionally stays separate from the diarization page.

## Pyannote Access

Для `pyannote_diarization` нужен Hugging Face `Read` token того аккаунта, который уже принял условия модели `pyannote/speaker-diarization-community-1`.

Как получить token:
- открыть страницу выпуска нового `Read` token:
  `https://huggingface.co/settings/tokens/new?tokenType=read`
- отдельно убедиться, что под тем же аккаунтом приняты условия здесь:
  `https://huggingface.co/pyannote/speaker-diarization-community-1`

Где используется token:
- локально: env `PYANNOTE_HF_TOKEN` для sidecar `pyannote_diarization`
- в legacy CPU stack `py_faster_whisper`: masked/protected GitLab CI/CD variable `PYANNOTE_HF_TOKEN`; `stack/stack.drs` передаёт token sidecar и фиксирует `DEVICE=cpu`
- в `rag-stack` deploy: GitLab CI/CD variable `PYANNOTE_HF_TOKEN`

Важно:
- token не хранится в git
- если `PYANNOTE_HF_TOKEN` не задан, `/healthz` отвечает `503`, и новый upload на `/whisper-diarization` автоматически пойдёт как transcript-only
- если token задан, но условия модели не приняты, sidecar вернёт короткий `HF access denied ...`

## Главные env

- `NATS_URL`
- `ASR_IN_PREFIX`
- `ASR_OUT_PREFIX`
- `LINTO_HTTP_URL`
- `LINTO_HTTP_HOST`
- `LINTO_HTTP_LANGUAGE`
- `WEBUI_ENABLED`
- `WEBUI_FILE_TRANSCRIBE_ENABLED`
- `FILE_CHUNK_SECONDS`
- `FILE_CHUNK_OVERLAP_SECONDS`
- `DIARIZATION_ENABLED`
- `DIARIZATION_HTTP_URL`
- `DIARIZATION_HTTP_PROXY_URL` — optional streaming CONNECT proxy; the URL above remains the model backend URL
- `DIARIZATION_HEALTH_URL`
- `DIARIZATION_HEALTH_HOST`
- `DIARIZATION_HEALTH_TIMEOUT_S`
- `PYANNOTE_HF_TOKEN`
- `PYANNOTE_PIPELINE_ID`
- `PYANNOTE_CACHE_DIR`
- `PYANNOTE_STACK_REVISION`
- `PYANNOTE_SEGMENTATION_BATCH_SIZE`
- `PYANNOTE_EMBEDDING_BATCH_SIZE`
- `ASR_FILE_DIAR_EVENT_PREFIX`
- `ASR_FILE_DIAR_TEXT_EVENT_PREFIX`
- `ASR_FILE_DIAR_TEXT_SESSION_EVENT_PREFIX`
- `MIN_SPEECH_DURATION_MS`
- `MIN_SILENCE_DURATION_MS`
- `MAX_SPEECH_DURATION_S`
- `SPEECH_PAD_MS`
- `VAD_THRESHOLD`
- `BUFFER_CHECK_INTERVAL_S`

Пример env лежит в [src/env.example](#archive-availability).

`pyannote/speaker-diarization-community-1` приходит с pipeline config, где
`segmentation_batch_size=32` и `embedding_batch_size=32`. Для нашего
`1g.24gb` MIG slice это слишком агрессивно и может провоцировать
`NVML_SUCCESS == r INTERNAL ASSERT FAILED` внутри PyTorch allocator.

Поэтому sidecar поддерживает low-memory override через
`PYANNOTE_SEGMENTATION_BATCH_SIZE` и `PYANNOTE_EMBEDDING_BATCH_SIZE`.
В production stack они включаются для GPU runtime и ставятся в `1`.
