fix(ai): STT — имя файла по content-type, webm/mp4 уходили как voice.dat
CI / test (push) Successful in 7m46s
CI / deploy-ift (push) Successful in 3m56s
CI / e2e-ift (push) Successful in 1m16s
CI / deploy-stage (push) Successful in 2m54s
CI / e2e-stage (push) Failing after 1m8s

xAI определяет формат аудио по расширению имени файла в
multipart-части, а не по её content-type. Back слал нейтральное
filename="voice.dat": wav проходил случайно (узнаваемый заголовок),
а webm/opus и mp4 из браузерного MediaRecorder получали 400
«Could not detect audio format» → 502 upstream_error → тост
«Распознавание речи временно недоступно».

Расширение выводится из mime (параметры codecs отбрасываются);
неизвестный тип — .wav. Тесты на маппинг и подстановку в multipart.
This commit is contained in:
2026-08-23 21:51:19 +03:00
parent 61f7a5f719
commit e0e3455281
2 changed files with 59 additions and 5 deletions
+28 -2
View File
@@ -36,7 +36,9 @@ logic_ai_transcribe_test_() ->
{"per-user rate limit", fun test_user_rate_limit/0},
{"global upstream throttle", fun test_global_throttle/0},
{"invalid audio rejected", fun test_invalid_audio/0},
{"multipart layout", fun test_multipart/0}
{"multipart layout", fun test_multipart/0},
{"file name from content-type", fun test_file_name/0},
{"multipart carries typed file name", fun test_multipart_file_name/0}
]}.
%% Стаб апстрима: фиксирует запрос и отвечает успешным JSON.
@@ -59,7 +61,10 @@ test_xai_success() ->
?assertMatch({_, _}, binary:match(Body, <<"name=\"file\"">>)),
?assertMatch({_, _}, binary:match(Body, <<"audio-bytes">>)),
%% Без поля format — язык определяется авто (format=true требует language).
?assertEqual(nomatch, binary:match(Body, <<"name=\"format\"">>)).
?assertEqual(nomatch, binary:match(Body, <<"name=\"format\"">>)),
%% webm обязан уйти с расширением .webm (апстрим определяет формат
%% по имени файла, voice.dat даёт 400).
?assertMatch({_, _}, binary:match(Body, <<"filename=\"voice.webm\"">>)).
test_openai_adapter() ->
os:putenv("STT_PROVIDER", "openai"),
@@ -124,3 +129,24 @@ test_multipart() ->
?assert(binary:match(Body, <<"--\r\n">>) =/= nomatch),
%% Завершающий boundary в конце тела: --<B>--\r\n.
?assertEqual(<<"--\r\n">>, binary:part(Body, byte_size(Body), -4)).
%% Расширение имени файла выводится из mime; параметры codecs
%% отбрасываются (браузер шлёт audio/webm;codecs=opus).
test_file_name() ->
?assertEqual(<<"voice.webm">>, logic_ai_transcribe:file_name(<<"audio/webm">>)),
?assertEqual(<<"voice.webm">>,
logic_ai_transcribe:file_name(<<"audio/webm;codecs=opus">>)),
?assertEqual(<<"voice.mp4">>, logic_ai_transcribe:file_name(<<"audio/mp4">>)),
?assertEqual(<<"voice.wav">>, logic_ai_transcribe:file_name(<<"audio/wav">>)),
?assertEqual(<<"voice.wav">>, logic_ai_transcribe:file_name(<<"audio/x-wav">>)),
?assertEqual(<<"voice.mp3">>, logic_ai_transcribe:file_name(<<"audio/mpeg">>)),
?assertEqual(<<"voice.m4a">>, logic_ai_transcribe:file_name(<<"audio/x-m4a">>)),
?assertEqual(<<"voice.wav">>,
logic_ai_transcribe:file_name(<<"application/octet-stream">>)).
%% transcribe() подставляет типизированное имя файла в multipart.
test_multipart_file_name() ->
?assertEqual({ok, <<"привет">>, <<"ru">>},
logic_ai_transcribe:transcribe(<<"u1">>, <<"audio-bytes">>, <<"audio/mp4">>)),
{_Url, _Headers, _CType, Body} = get(stt_call),
?assertMatch({_, _}, binary:match(Body, <<"filename=\"voice.mp4\"">>)).