Files
talkscore-asr/README.md
T
Vladimir BryzgalovandClaude Opus 5 44becfbe5e Автообновление кода через релизы Gitea
Сервис при запуске проверяет последний релиз и обновляет только папку app -
19 КБ вместо 113 МБ всего пакета. Контрольная сумма, откат при неудачной
проверке, пользовательские файлы не трогаются.

Попутно: ffmpeg ищется лениво, а не в конструкторе Pipeline - его отсутствие
роняло импорт вместо внятного сообщения. Схема API закрыта по умолчанию.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 21:44:52 +05:00

10 KiB
Raw Blame History

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:

[server]
host = "0.0.0.0"     # 127.0.0.1 - только с этой машины
port = 8756

[security]
token = "..."                            # Authorization: Bearer <token>
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 <token>.

Отправить запись

curl -X POST "http://ХОСТ:8756/v1/jobs?speakers=2" \
     -H "Authorization: Bearer ТОКЕН" \
     -F "file=@call.mp3"
{"job_id": "fd47ba135eae429cbf37fb6ec1d8c34c", "status": "queued", "queue_position": 0}

Принимается любой формат, который читает ffmpeg: mp3, wav, m4a, ogg, opus, wma.

Забрать результат

curl "http://ХОСТ:8756/v1/jobs/JOB_ID" -H "Authorization: Bearer ТОКЕН"

Пока задача не готова, приходит {"status": "queued", "queue_position": 1} или {"status": "running"}. Готовый результат:

{
  "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}
}

Удалить задачу

curl -X DELETE "http://ХОСТ:8756/v1/jobs/JOB_ID" -H "Authorization: Bearer ТОКЕН"

Пример на 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:

[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.

Выпуск новой версии

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.

Разработка

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. Ничего компилировать не требуется.