# talkscore-asr Локальный сервис распознавания русской речи с разделением по говорящим. Работает офлайн: аудио не покидает машину. - **Распознавание** - GigaAM v3 e2e-rnnt (SaluteDevices), с пунктуацией и заглавными - **Разделение говорящих** - sherpa-onnx с моделями pyannote и NeMo TitaNet - **Скорость** - около ×26 realtime на четырёх потоках, то есть час записи за 2-3 минуты ## Установка на Windows Python ставить не нужно, всё уже внутри архива. 1. Распакуйте `talkscore-asr-windows.zip`, например в `C:\talkscore-asr` 2. Запустите `download_models.bat` - скачает модели, около 900 МБ, один раз 3. Запустите `start.bat` При первом запуске рядом появится `config.toml` со сгенерированным токеном. Откройте его, скопируйте токен и при необходимости поменяйте настройки. Проверка, что сервис жив: ``` curl http://localhost:8756/health ``` ### Если onnxruntime не загружается Нужен Microsoft Visual C++ Redistributable 2015-2022 (x64). На большинстве систем он уже стоит; если нет - скачайте с сайта Microsoft и установите. ## Настройка `config.toml`: ```toml [server] host = "0.0.0.0" # 127.0.0.1 - только с этой машины port = 8756 [security] token = "..." # Authorization: Bearer allow_ips = "192.168.1.0/24, 10.8.0.5" # пусто = разрешены все адреса [processing] threads = 0 # 0 = половина ядер, на Ryzen 9 9950X это 16 speakers = 2 # 0 = определять автоматически max_upload_mb = 500 keep_results_hours = 72 ``` **Про доступ.** Проверяются оба условия: адрес и токен. Пустой токен закрывает сервис полностью, а не открывает - чтобы забытая настройка не выставила его наружу. Пустой `allow_ips`, наоборот, снимает ограничение по адресам, поэтому при выходе наружу заполняйте его обязательно. **Про число говорящих.** На реальных звонках автоопределение работает плохо: вместо двух участников находит десятки. Если знаете, что в записи двое, оставляйте `speakers = 2`. ## API Во всех запросах, кроме `/health`, нужен заголовок `Authorization: Bearer `. ### Отправить запись ```bash curl -X POST "http://ХОСТ:8756/v1/jobs?speakers=2" \ -H "Authorization: Bearer ТОКЕН" \ -F "file=@call.mp3" ``` ```json {"job_id": "fd47ba135eae429cbf37fb6ec1d8c34c", "status": "queued", "queue_position": 0} ``` Принимается любой формат, который читает ffmpeg: mp3, wav, m4a, ogg, opus, wma. ### Забрать результат ```bash curl "http://ХОСТ:8756/v1/jobs/JOB_ID" -H "Authorization: Bearer ТОКЕН" ``` Пока задача не готова, приходит `{"status": "queued", "queue_position": 1}` или `{"status": "running"}`. Готовый результат: ```json { "job_id": "fd47ba...", "status": "done", "filename": "call.mp3", "duration_sec": 1003.0, "turns": [ {"speaker": 1, "start": 7.2, "end": 9.4, "text": "Ну, давайте послушаю вас ещё."}, {"speaker": 2, "start": 9.6, "end": 33.1, "text": "Поняла. То есть тут обучение зависит только от вас."} ], "stats": { "speakers": 2, "speech_sec": 557.0, "silence_sec": 446.1, "turns_count": 137, "by_speaker": [ {"speaker": 1, "speech_sec": 236.8, "share_pct": 42.5}, {"speaker": 2, "speech_sec": 320.2, "share_pct": 57.5} ] }, "timing": {"diarization_sec": 29.1, "asr_sec": 9.7, "realtime_factor": 25.9} } ``` ### Удалить задачу ```bash curl -X DELETE "http://ХОСТ:8756/v1/jobs/JOB_ID" -H "Authorization: Bearer ТОКЕН" ``` ### Пример на Python ```python import time import requests API = "http://192.168.1.50:8756" HEAD = {"Authorization": "Bearer ТОКЕН"} with open("call.mp3", "rb") as f: job = requests.post(f"{API}/v1/jobs", headers=HEAD, files={"file": f}).json() while True: r = requests.get(f"{API}/v1/jobs/{job['job_id']}", headers=HEAD).json() if r["status"] in ("done", "failed"): break time.sleep(5) for turn in r["turns"]: print(f"[{turn['start']:.0f}с] Спикер {turn['speaker']}: {turn['text']}") ``` ## Словарь замен `replacements.txt` чинит систематические ошибки на английских терминах: GigaAM обучена только на русском и коверкает их предсказуемо. ``` гугл так менеджер = Google Tag Manager ледами = лидами ``` Регистр не важен, замена идёт по целым словам, поэтому правило `лед = лид` не тронет слово «лидер». Файл перечитывается перед каждой задачей - правки применяются без перезапуска сервиса. Заодно постобработка приводит типографику к принятому виду: все виды тире заменяются на дефис, кавычки отбиваются пробелом. ## Обновления Сервис проверяет новую версию при каждом запуске и обновляет только папку `app` - это десятки килобайт. Python, библиотеки, ffmpeg и модели остаются на месте, перекидывать весь пакет заново не нужно. Настройка в `config.toml`: ```toml [update] enabled = true server = "https://git.netranking.ru" repo = "bryzgalov/talkscore-asr" token = "" # токен Gitea с правом чтения; для публичного репозитория не нужен ``` Репозиторий приватный, поэтому токен обязателен. Создать его: Gitea → Settings → Applications → Generate Token, достаточно права `read:repository`. Как это работает: 1. `start.bat` перед запуском сервиса спрашивает у Gitea последний релиз 2. если версия там новее, скачивает `app-<версия>.zip` и сверяет контрольную сумму 3. откладывает текущий код, ставит новый и проверяет, что он импортируется 4. если проверка не прошла, возвращает предыдущую версию Нет сети или Gitea недоступен - сервис просто запускается на текущей версии. `config.toml` и `replacements.txt` обновление не трогает: они ваши. Чтобы выключить проверку совсем, поставьте `enabled = false`. ### Выпуск новой версии ```bash uv run --with requests python build/release.py 0.2.0 -m "что изменилось" ``` Скрипт проставит версию в `app/version.py`, соберёт архив только из кода и опубликует релиз с контрольной суммой. Целевая машина подхватит его при следующем запуске. ## Автозапуск Чтобы сервис поднимался при старте Windows, создайте задачу в планировщике: ``` schtasks /create /tn "talkscore-asr" /tr "C:\talkscore-asr\start.bat" ^ /sc onstart /ru SYSTEM /rl HIGHEST ``` ## Что важно знать про качество - **Разделение говорящих зависит от записи.** Там, где один участник говорит через линию, а другой в комнате, тембры различаются и разделение точное. Если оба записаны в похожих условиях, модель может слить их в одного. На проверочных звонках так вышло на одной записи из трёх. - **Перекрывающаяся речь размечается одним говорящим.** Когда участники перебивают друг друга, реплики могут склеиваться. - **Роли надёжнее определять по смыслу.** Если разделение по голосу подвело, расставить роли по содержанию реплик - задача для LLM, а не для звука. - Пороги диаризации (`min_duration_on = 1.0`, `min_duration_off = 0.7`) подобраны так, чтобы отсекать обрывки на перебивках. Ценой стали короткие «да» и «угу»: если они нужны для аналитики, снизьте значения в `app/pipeline.py`. ## Разработка ```bash uv run --with pytest --with fastapi --with httpx --with python-multipart python -m pytest -q uv run --with pip python build/make_windows_zip.py # пересобрать пакет ``` Сборка идёт на любой ОС: скачиваются встраиваемый Python для Windows, колёса под win_amd64 и ffmpeg. Ничего компилировать не требуется.