По одному JSON не понять, чья ошибка: распознавание склеило реплики или
LLM неверно расставила роли. Страница показывает обе стороны и принимает
ответ модели, чтобы диалог собрался на глазах.
GET /v1/jobs/{id}/view - токен заголовком либо ссылкой ?token=ЗНАЧЕНИЕ.
Страница самодостаточна: ни одного внешнего запроса.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Диаризация занимала 79 % времени, а её метку мы не используем: на записи
с одним микрофоном голоса не расходятся, роли расставляет LLM по смыслу.
Silero VAD решает единственную нужную задачу и делает это вчетверо быстрее.
Замер на 205 минутах: x16 -> x60, слов на 1,8 % меньше. Семь записей
из восьми в пределах +-4 %, на одной теряется 19 %.
Отдельно найдено: max_speech_duration у Silero по умолчанию 20 с, и на
таких кусках распознавание теряет текст. Снижение до 6 с даёт 6-8 п.п.
полноты бесплатно. Диаризация сохранена настройкой diarize.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Машина переехала в сеть с серым адресом, извне до неё не достучаться.
Туннель поднимается с её стороны наружу, машина получает постоянный
10.90.0.2, по которому её видит сервер. Через туннель идёт только трафик
до сервера - интернет машины не затрагивается.
Ключевая пара создаётся на самой машине, наружу уходит только открытый ключ.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Диаризация задавала не только «кто говорит», но и границы того, что вообще
попадает в распознавание: фразы вне её сегментов пропадали молча. Второй
проход их возвращает с пометкой recovered=true и speaker=0.
Замер на восьми записях: 131 слово из 17 004 (0,8 %), 3-19 с на запись.
Возвращается и настоящий диалог (вопрос о цене, даты, документы), и радио
на фоне, поэтому проход отключается настройкой recover_gaps.
Скрипты render_transcript.py и check_gaps.py - для сверки расшифровки
с записью и поиска потерянных фраз.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Отпечаток секрета (sha256, 8 символов) в /health по токену: две стороны
сверяют настройки, не пересылая значение. В журнал доставки добавлены размер
тела, его sha256, начало подписи и текст ответа - по ним видно, расходится
секрет или принимающая сторона считает подпись не от сырых байтов.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Caddy ругался на два места. header_up X-Forwarded-For был лишним: Caddy сам
игнорирует этот заголовок из входящего запроса и подставляет реальный адрес,
защита от подделки обеспечивается его поведением по умолчанию. Пустая строка
перед глобальным блоком делала файл неформатированным по мнению caddy fmt.
Проверено caddy fmt и caddy validate на сгенерированном файле.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Разбивка на предложения нужна LLM-разметке и была бы дублирована на стороне
Talkscore. Логика живёт рядом с ASR, который и ставит пунктуацию, а сам
вызов LLM остаётся на Talkscore: промпт меняется чаще, чем расшифровка,
и переразметить сохранённый текст дешевле, чем гонять аудио заново.
Документ интеграции дополнен итогами сравнения моделей, batch-ценами
и проверками формата ответа.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Маршруты /docs и /openapi.json проверяли только адрес. С выключенным
списком адресов это означало открытый доступ к описанию API из интернета.
Теперь они отвечают 404, пока docs не включён явно, а включённые требуют
адрес из списка и токен - заголовком либо ссылкой /docs?token=ЗНАЧЕНИЕ.
Попутно исправлено сравнение токена: compare_digest на строках с не-ASCII
бросает TypeError, и токен с кириллицей давал 500 вместо 401.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Файлы вне папки app обновление не доставляло: чтобы изменить порядок
запуска или конфигурацию Caddy, требовалась переустановка. Теперь .bat -
заглушка из трёх строк, вызывающая app/launcher.py, а Caddyfile создаётся
из настроек [https] в config.toml. Отредактированный вручную Caddyfile
не затирается: новая версия откладывается рядом как Caddyfile.new.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Uvicorn заводил свои обработчики с propagate=False, поэтому его строки шли
мимо нашего формата и рвались посреди слова. log_config=None отдаёт всё
общему обработчику.
Занятый порт сервис теперь замечает до запуска и объясняет, что делать,
вместо строки uvicorn про Errno 10048.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Windows-консоль не разбирала ANSI и печатала их как мусор, поэтому цвет
был выключен целиком. Теперь разбор включается через WinAPI, а при неудаче
цвет молча отключается. Длинные сообщения переносятся по словам с отступом
под текст. Вывод Caddy уведён в файл - он забивал консоль сертификатами.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Дублированный раздел ронял обновление трассировкой tomllib. Теперь
ConfigError объясняет по-русски, что раздел объявлен дважды и какие
разделы должны встречаться ровно один раз.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Прежняя оценка \$0,035 за час была расчётной и ошибочной в двадцать раз:
я считал выход по формату ответа, а обе модели оказались рассуждающими
и тратят на размышления тысячи токенов.
Замер: полные размышления \$0,75 за час, минимальные \$0,076, выключенные
\$0,055. Но при выключенных модель начинает менять роли местами в середине
разговора, поэтому рекомендованы минимальные.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Токен и расшифровки разговоров ходили по сети открытым текстом. Теперь в
пакете лежит Caddy: он держит сертификаты Let's Encrypt и продлевает их сам,
а сервис уходит на localhost.
Отдельно решён вопрос подмены адреса: за прокси все запросы приходят с
localhost, и список разрешённых адресов пускал бы кого угодно. Заголовку
с настоящим адресом сервис верит только от доверенного прокси - на это
восемь тестов.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
205 минут разговоров: время по этапам, стоимость разметки ролей и качество
разделения говорящих. Главная цифра - разделение по голосам сработало на
одной записи из восьми, поэтому роли восстанавливает LLM.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Словарь замен расширен под нишу: категории прав, документы, госорганы,
термины обучения и оплаты, частые ошибки на плохом звуке.
В docs - инструкция для агента Talkscore: что выключить на своей стороне,
как ставить задачи и принимать вебхук, и главное - почему полю speaker
доверять нельзя и как восстанавливать роли через LLM.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ключевая находка по записям с микрофоном на столе: без выравнивания тихий
дальний участник сливается с громким и разделение разваливается. Замер на
трёх разговорах: доля второго участника выросла с 1.8 до 24.3 процента,
переключений между репликами - с 21 до 71 процента.
Работает только выравнивание: шумоподавление, полосовой фильтр и компандер
не дали ничего. Параметры подобраны замером, f=400:g=3 давал 8.6 процента
вместо 43.2. На распознавание не влияет - расшифровки фрагментов совпали
дословно.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Опрос статуса заставлял принимающую сторону дёргать сервис каждые
несколько секунд. Теперь при завершении задачи результат уходит POST-ом
на заданный адрес, с подписью HMAC-SHA256 в заголовке и тремя попытками
при неудаче. Адрес задаётся в настройках или параметром запроса.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Полный трейс показал настоящую причину сбоя: падало не распознавание,
а метрика separation_quality из 0.5.0 - она скармливала модели отпечатков
реплики целиком, и на 190-секундной та не выдержала. Теперь берётся кусок
из середины реплики, а сбой оценки лишь обнуляет её, не трогая расшифровку.
Добавлен /v1/logs: последние записи журнала с фильтром по уровню, чтобы
разбирать сбои без копирования консоли вручную.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
На Windows реплика в 190 секунд роняет энкодер GigaAM в свёртке, хотя на
macOS та же самая проходит: пределы у сборок onnxruntime разные и заранее
неизвестны. Вместо подбора константы кусок при ошибке делится пополам и
пробуется снова, до пяти уровней. Базовый размер куска снижен со 150 до 60
секунд - с запасом под любую сборку.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
На Windows процессы поднимаются через spawn и заново импортируют app.main.
Хранилище задач создавалось на уровне модуля, поэтому каждый новый процесс
при старте помечал чужие выполняющиеся задачи как сорванные - все три
записи падали с "сервис был перезапущен во время обработки".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Замер подтвердил догадку: sherpa-onnx и onnxruntime держат GIL. Две задачи
в двух потоках идут ровно столько же, сколько подряд (1.04x у диаризации,
1.17x у распознавания) - отсюда и незагруженный процессор при работе.
В двух процессах те же задачи дают 1.59x даже с загрузкой моделей в каждом.
Пул процессов включается при workers > 1. Функции воркера вынесены в модуль,
не тянущий app.main: на Windows дочерний процесс поднимается через spawn и
иначе стартовал бы ещё один веб-сервер. Если процессы не запустятся, сервис
откатывается на однопроцессный режим.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
На записях с микрофоном на столе голоса участников для модели почти
неразличимы: перебор четырёх моделей отпечатков и смена алгоритма
кластеризации баланс улучшают, но роли всё равно скачут.
Поэтому сервис теперь отдаёт то, на что можно опереться: акустику каждой
реплики (громкость, доля высоких, центроид - они связаны с расстоянием
до микрофона) и метрику separation_quality с флагом speakers_reliable.
Ниже 0.35 разметка по говорящим случайна, и роли должна определять LLM
по смыслу реплик.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Бенчмарк на Ryzen 9 9950X показал обратное тому, что было на Apple M4:
там всё растёт до 16 потоков (распознавание x14.7 -> x61.3), здесь после
четырёх начинается спад. Причина в неоднородных ядрах M4.
Дефолт стал умеренным (половина ядер, но не больше 8), а точное значение
подбирается через /v1/benchmark. Воркер по умолчанию один: параллельная
обработка выигрыша не дала, процессор и так загружен целиком.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Масштабирование ONNX зависит от процессора: замеры на Apple M4 не
переносятся на Ryzen, а подбирать конфигурацию перезапусками мучительно.
Теперь /v1/benchmark гоняет минуту записи на 1, 2, 4, 8 и 16 потоках
и говорит, что поставить в config.toml.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Прогрет был только нулевой воркер, остальные получали пустой конвейер и
роняли задачу с "модели не загружены". В комментарии было написано, что
они греются сами, но кода для этого не было.
Заодно отключены ANSI-цвета uvicorn на Windows: консоль их не разбирает
и печатала управляющие последовательности как мусор.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Замеры: распознавание даёт x52 на 4 потоках против x13 на 16, разделение
говорящих x33 против x8. Дальше четырёх потоков синхронизация съедает весь
выигрыш, поэтому ядра занимаются несколькими задачами сразу.
По умолчанию 4 потока на задачу и до 4 задач параллельно. Захват задачи
из очереди стал атомарным - без этого два воркера брали одну и ту же.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Штатные /docs и /openapi.json не требуют токена, поэтому раньше были
выключены совсем. Теперь это свои маршруты, закрытые тем же списком
адресов, что и остальной сервис: со своей машины открываются, с чужой
отдают 403. У методов появились описания, в схеме объявлен Bearer -
работает кнопка Authorize.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Правки в app/config.py не действуют и теряются при обновлении: это код,
а настройки читаются из config.toml. Теперь при старте печатается путь
к нему и что фактически применилось - задан ли токен и сколько адресов
разрешено. В самом модуле - предупреждение вверху файла.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Отказ по списку доступа отлаживался вслепую: сервис не сообщал, каким видит
адрес обратившегося. Теперь /health отдаёт your_ip, your_ip_allowed и версию,
не раскрывая при этом сам список.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Обновление перемещало папку app целиком, а код выполняется именно из неё.
Windows не даёт двигать такую папку: перемещение не прошло, откат тоже,
и app исчезла - сервис переставал запускаться.
Теперь папка остаётся на месте: новая версия проверяется во временном
каталоге ДО установки, затем файлы перезаписываются по одному с копией
для отката. Устаревшие модули удаляются, __pycache__ не трогается.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Gitea на приватном репозитории отвечает 404 вместо 403, и сообщение
"404 Not Found" не подсказывало, что дело в токене. Теперь при 401/403/404
объясняется причина и куда вписать токен.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Сервис при запуске проверяет последний релиз и обновляет только папку app -
19 КБ вместо 113 МБ всего пакета. Контрольная сумма, откат при неудачной
проверке, пользовательские файлы не трогаются.
Попутно: ffmpeg ищется лениво, а не в конструкторе Pipeline - его отсутствие
роняло импорт вместо внятного сообщения. Схема API закрыта по умолчанию.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Локальный FastAPI-сервис поверх GigaAM v3 и sherpa-onnx: приём аудио,
очередь задач, разделение по говорящим, постобработка терминов.
Доставка на Windows - ZIP со встроенным Python, без установки чего-либо.
Обновление кода при запуске тянется из релизов Gitea: меняется только
папка app, десятки килобайт вместо всего пакета.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>