Skip to content

Commit 30caaf8

Browse files
authored
fix(websocket): finalize file input on end control (#3440)
Signed-off-by: LauraGPT <lauragpt@users.noreply.github.com> Co-authored-by: LauraGPT <lauragpt@users.noreply.github.com>
1 parent 6570924 commit 30caaf8

4 files changed

Lines changed: 938 additions & 32 deletions

File tree

runtime/python/websocket/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,12 @@ python funasr_wss_client.py \
6868
--audio_in [if set, loadding from wav.scp, else recording from mircrophone] \
6969
--output_dir [if set, write the results to output_dir] \
7070
--mode [`online` for streaming asr, `offline` for non-streaming, `2pass` for unifying streaming and non-streaming asr] \
71-
--thread_num [thread_num for send data]
71+
--thread_num [thread_num for send data] \
72+
--result_timeout [seconds to wait for the final server acknowledgement]
7273
```
7374

75+
When `--audio_in` is set, the client sends `{"is_speaking": false, "is_end": true}` after the last audio frame. The server flushes pending online and offline inference before replying with `{"is_end": true, "is_final": true}`. If inference fails, the acknowledgement contains `{"is_end": true, "is_final": false, "error": "..."}`. The client waits up to `--result_timeout` seconds for this acknowledgement; the default is 300 seconds.
76+
7477
#### Usage examples
7578
##### ASR offline client
7679
Recording from mircrophone

runtime/python/websocket/funasr_wss_client.py

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@
5050
parser.add_argument("--ssl", type=int, default=1, help="1 for ssl connect, 0 for no ssl")
5151
parser.add_argument("--use_itn", type=int, default=1, help="1 for using itn, 0 for not itn")
5252
parser.add_argument("--mode", type=str, default="2pass", help="offline, online, 2pass")
53+
parser.add_argument(
54+
"--result_timeout",
55+
type=float,
56+
default=300.0,
57+
help="seconds to wait for the server's end-of-input acknowledgement",
58+
)
5359

5460
# ✅ 验收日志输出目录(每个 meeting 单独写,避免多进程抢文件)
5561
parser.add_argument("--log_dir", type=str, default="./asr_logs", help="验收日志输出目录")
@@ -185,7 +191,7 @@ async def record_microphone():
185191
await asyncio.sleep(0.01)
186192

187193

188-
async def record_from_scp(chunk_begin, chunk_size):
194+
async def record_from_scp(chunk_begin, chunk_size, completion_queue):
189195
"""从 wav/scp 文件读取音频分片发送,用于压测和延迟测试"""
190196
global voices, latency_first_audio_time, latency_last_audio_time
191197
if args.audio_in.endswith(".scp"):
@@ -268,7 +274,6 @@ async def record_from_scp(chunk_begin, chunk_size):
268274
)
269275

270276
await websocket.send(message)
271-
is_speaking = True
272277

273278
# 初始化该 wav 的统计状态
274279
latency_first_audio_time[wav_name] = None
@@ -286,10 +291,6 @@ async def record_from_scp(chunk_begin, chunk_size):
286291

287292
await websocket.send(data)
288293

289-
if i == chunk_num - 1:
290-
is_speaking = False
291-
await websocket.send(json.dumps({"is_speaking": is_speaking}, ensure_ascii=False))
292-
293294
# ✅ sleep策略:默认按实时节奏;若开启 send_without_sleep 则几乎不 sleep(压测)
294295
if args.send_without_sleep:
295296
sleep_duration = 0.001
@@ -301,20 +302,30 @@ async def record_from_scp(chunk_begin, chunk_size):
301302
)
302303
await asyncio.sleep(sleep_duration)
303304

304-
if not args.mode == "offline":
305-
await asyncio.sleep(2)
306-
307-
if args.mode == "offline":
308-
global offline_msg_done
309-
while not offline_msg_done:
310-
await asyncio.sleep(1)
311-
312-
await asyncio.sleep(10)
305+
await websocket.send(
306+
json.dumps({"is_speaking": False, "is_end": True}, ensure_ascii=False)
307+
)
308+
try:
309+
acknowledgement = await asyncio.wait_for(
310+
completion_queue.get(), timeout=args.result_timeout
311+
)
312+
except asyncio.TimeoutError as e:
313+
await websocket.close()
314+
raise TimeoutError(
315+
f"server did not acknowledge end of input for {wav_name!r} "
316+
f"within {args.result_timeout:g} seconds"
317+
) from e
318+
if not acknowledgement.get("is_final", False):
319+
await websocket.close()
320+
error = acknowledgement.get("error") or "server did not finalize input"
321+
raise RuntimeError(
322+
f"server failed to finalize {wav_name!r}: {error}"
323+
)
313324

314325
await websocket.close()
315326

316327

317-
async def message(id, writer: MeetingWriter):
328+
async def message(id, writer: MeetingWriter, completion_queue):
318329
"""接收服务端识别结果 + 打印实时文本 + 打印延迟 + 写验收日志(events.jsonl)"""
319330
import websockets
320331
global websocket, voices, offline_msg_done
@@ -324,6 +335,13 @@ async def message(id, writer: MeetingWriter):
324335
text_print = ""
325336
text_print_2pass_online = ""
326337
text_print_2pass_offline = ""
338+
end_ack_received = False
339+
340+
def release_sender_with_error(error):
341+
if completion_queue is not None and not end_ack_received:
342+
completion_queue.put_nowait(
343+
{"is_end": True, "is_final": False, "error": error}
344+
)
327345

328346
if args.output_dir is not None:
329347
ibest_writer = open(
@@ -372,6 +390,10 @@ async def message(id, writer: MeetingWriter):
372390

373391
timestamp = meg.get("timestamp", "")
374392
offline_msg_done = meg.get("is_final", False)
393+
if meg.get("is_end"):
394+
end_ack_received = True
395+
if completion_queue is not None:
396+
completion_queue.put_nowait(meg)
375397

376398
# ✅ 验收友好:每条消息落 events.jsonl(便于后处理)
377399
event = {
@@ -460,8 +482,10 @@ async def message(id, writer: MeetingWriter):
460482

461483
except websockets.exceptions.ConnectionClosedOK:
462484
print(f"[MEETING {id}] connection closed normally")
485+
release_sender_with_error("connection closed before end-of-input acknowledgement")
463486
except Exception as e:
464487
print(f"[MEETING {id}] Exception:", e)
488+
release_sender_with_error(f"receiver failed before end-of-input acknowledgement: {e}")
465489
finally:
466490
try:
467491
if ibest_writer is not None:
@@ -497,12 +521,15 @@ async def ws_client(id, chunk_begin, chunk_size):
497521
) as websocket:
498522
meeting_tag = f"{id}_{i}"
499523
writer = MeetingWriter(args.log_dir, meeting_id=meeting_tag, flush_every=args.log_flush_every)
524+
completion_queue = asyncio.Queue() if args.audio_in is not None else None
500525
try:
501526
if args.audio_in is not None:
502-
task = asyncio.create_task(record_from_scp(i, 1))
527+
task = asyncio.create_task(record_from_scp(i, 1, completion_queue))
503528
else:
504529
task = asyncio.create_task(record_microphone())
505-
task3 = asyncio.create_task(message(str(id) + "_" + str(i), writer)) # processid+fileid
530+
task3 = asyncio.create_task(
531+
message(str(id) + "_" + str(i), writer, completion_queue)
532+
) # processid+fileid
506533
await asyncio.gather(task, task3)
507534
finally:
508535
writer.close()

0 commit comments

Comments
 (0)