Сервис при запуске проверяет последний релиз и обновляет только папку app - 19 КБ вместо 113 МБ всего пакета. Контрольная сумма, откат при неудачной проверке, пользовательские файлы не трогаются. Попутно: ffmpeg ищется лениво, а не в конструкторе Pipeline - его отсутствие роняло импорт вместо внятного сообщения. Схема API закрыта по умолчанию. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
10 KiB
talkscore-asr
Локальный сервис распознавания русской речи с разделением по говорящим. Работает офлайн: аудио не покидает машину.
- Распознавание - GigaAM v3 e2e-rnnt (SaluteDevices), с пунктуацией и заглавными
- Разделение говорящих - sherpa-onnx с моделями pyannote и NeMo TitaNet
- Скорость - около ×26 realtime на четырёх потоках, то есть час записи за 2-3 минуты
Установка на Windows
Python ставить не нужно, всё уже внутри архива.
- Распакуйте
talkscore-asr-windows.zip, например вC:\talkscore-asr - Запустите
download_models.bat- скачает модели, около 900 МБ, один раз - Запустите
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.
Как это работает:
start.batперед запуском сервиса спрашивает у Gitea последний релиз- если версия там новее, скачивает
app-<версия>.zipи сверяет контрольную сумму - откладывает текущий код, ставит новый и проверяет, что он импортируется
- если проверка не прошла, возвращает предыдущую версию
Нет сети или 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. Ничего компилировать не требуется.