Add realtime ASR preview updates
This commit is contained in:
parent
5671a6482a
commit
59c34ec1ff
@ -96,6 +96,8 @@ TEXT_MODEL_API_KEY=none
|
|||||||
|
|
||||||
VOICEPRINT_STORE=/data/asr_voiceprints.json
|
VOICEPRINT_STORE=/data/asr_voiceprints.json
|
||||||
VOICEPRINT_MATCH_THRESHOLD=0.45
|
VOICEPRINT_MATCH_THRESHOLD=0.45
|
||||||
|
REALTIME_PARTIAL_MIN_SECONDS=1.5
|
||||||
|
REALTIME_PARTIAL_INTERVAL_SECONDS=2.0
|
||||||
```
|
```
|
||||||
|
|
||||||
说明:
|
说明:
|
||||||
|
|||||||
70
asr/main.py
70
asr/main.py
@ -10,6 +10,7 @@ PUNC 模型: punc_ct-transformer_zh-cn-common-vocab272727-pytorch
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
|
import asyncio
|
||||||
import tempfile
|
import tempfile
|
||||||
import logging
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
@ -100,6 +101,8 @@ model = None
|
|||||||
VOICE_DETECTED_URL = os.environ.get("VOICE_DETECTED_URL", "http://127.0.0.1:18000")
|
VOICE_DETECTED_URL = os.environ.get("VOICE_DETECTED_URL", "http://127.0.0.1:18000")
|
||||||
VOICEPRINT_STORE = os.environ.get("VOICEPRINT_STORE", "/tmp/asr_voiceprints.json")
|
VOICEPRINT_STORE = os.environ.get("VOICEPRINT_STORE", "/tmp/asr_voiceprints.json")
|
||||||
VOICEPRINT_MATCH_THRESHOLD = float(os.environ.get("VOICEPRINT_MATCH_THRESHOLD", "0.45"))
|
VOICEPRINT_MATCH_THRESHOLD = float(os.environ.get("VOICEPRINT_MATCH_THRESHOLD", "0.45"))
|
||||||
|
REALTIME_PARTIAL_INTERVAL_SECONDS = float(os.environ.get("REALTIME_PARTIAL_INTERVAL_SECONDS", "2.0"))
|
||||||
|
REALTIME_PARTIAL_MIN_SECONDS = float(os.environ.get("REALTIME_PARTIAL_MIN_SECONDS", "1.5"))
|
||||||
|
|
||||||
|
|
||||||
def _voiceprint_key(database_name: str, collection_name: str) -> str:
|
def _voiceprint_key(database_name: str, collection_name: str) -> str:
|
||||||
@ -245,9 +248,39 @@ async def health():
|
|||||||
return {"status": "healthy"}
|
return {"status": "healthy"}
|
||||||
|
|
||||||
|
|
||||||
|
def _write_pcm_wav(audio_bytes: bytes) -> str:
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
|
||||||
|
tmp_path = tmp.name
|
||||||
|
|
||||||
|
with wave.open(tmp_path, "wb") as wav_file:
|
||||||
|
wav_file.setnchannels(1)
|
||||||
|
wav_file.setsampwidth(2)
|
||||||
|
wav_file.setframerate(16000)
|
||||||
|
wav_file.writeframes(audio_bytes)
|
||||||
|
|
||||||
|
return tmp_path
|
||||||
|
|
||||||
|
|
||||||
|
def _text_delta(previous: str, current: str) -> str:
|
||||||
|
previous = previous or ""
|
||||||
|
current = current or ""
|
||||||
|
if not current or current == previous:
|
||||||
|
return ""
|
||||||
|
if current.startswith(previous):
|
||||||
|
return current[len(previous):]
|
||||||
|
|
||||||
|
max_overlap = min(len(previous), len(current), 80)
|
||||||
|
for size in range(max_overlap, 0, -1):
|
||||||
|
if previous[-size:] == current[:size]:
|
||||||
|
return current[size:]
|
||||||
|
return current
|
||||||
|
|
||||||
|
|
||||||
async def run_asr_websocket(websocket: WebSocket):
|
async def run_asr_websocket(websocket: WebSocket):
|
||||||
await websocket.accept(subprotocol="binary")
|
await websocket.accept(subprotocol="binary")
|
||||||
audio_chunks = []
|
audio_chunks = []
|
||||||
|
last_partial_at = 0.0
|
||||||
|
last_partial_text = ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
@ -255,6 +288,33 @@ async def run_asr_websocket(websocket: WebSocket):
|
|||||||
|
|
||||||
if "bytes" in message and message["bytes"]:
|
if "bytes" in message and message["bytes"]:
|
||||||
audio_chunks.append(message["bytes"])
|
audio_chunks.append(message["bytes"])
|
||||||
|
audio_seconds = sum(len(chunk) for chunk in audio_chunks) / 32000.0
|
||||||
|
now = asyncio.get_running_loop().time()
|
||||||
|
|
||||||
|
if (
|
||||||
|
audio_seconds >= REALTIME_PARTIAL_MIN_SECONDS
|
||||||
|
and now - last_partial_at >= REALTIME_PARTIAL_INTERVAL_SECONDS
|
||||||
|
):
|
||||||
|
last_partial_at = now
|
||||||
|
partial_path = None
|
||||||
|
try:
|
||||||
|
partial_path = _write_pcm_wav(b"".join(audio_chunks))
|
||||||
|
result = await run_asr(partial_path, language="zh", response_format="json")
|
||||||
|
partial_text = result.get("text", "") if isinstance(result, dict) else ""
|
||||||
|
if partial_text and partial_text != last_partial_text:
|
||||||
|
await websocket.send_json({
|
||||||
|
"mode": "2pass-online",
|
||||||
|
"text": partial_text,
|
||||||
|
"timestamp": str(int(audio_seconds * 1000)),
|
||||||
|
"is_full_text": True,
|
||||||
|
"is_final": False,
|
||||||
|
})
|
||||||
|
last_partial_text = partial_text
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"实时部分识别失败: {e}")
|
||||||
|
finally:
|
||||||
|
if partial_path and os.path.exists(partial_path):
|
||||||
|
os.unlink(partial_path)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if "text" in message and message["text"]:
|
if "text" in message and message["text"]:
|
||||||
@ -276,15 +336,7 @@ async def run_asr_websocket(websocket: WebSocket):
|
|||||||
tmp_path = None
|
tmp_path = None
|
||||||
try:
|
try:
|
||||||
if audio_chunks:
|
if audio_chunks:
|
||||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
|
tmp_path = _write_pcm_wav(b"".join(audio_chunks))
|
||||||
tmp_path = tmp.name
|
|
||||||
|
|
||||||
with wave.open(tmp_path, "wb") as wav_file:
|
|
||||||
wav_file.setnchannels(1)
|
|
||||||
wav_file.setsampwidth(2)
|
|
||||||
wav_file.setframerate(16000)
|
|
||||||
wav_file.writeframes(b"".join(audio_chunks))
|
|
||||||
|
|
||||||
result = await run_asr(tmp_path, language="zh", response_format="json")
|
result = await run_asr(tmp_path, language="zh", response_format="json")
|
||||||
if isinstance(result, dict):
|
if isinstance(result, dict):
|
||||||
text = result.get("text", "")
|
text = result.get("text", "")
|
||||||
|
|||||||
@ -29,6 +29,8 @@ LLM_ROLE_HISTORY_MAX_CHARS=1200
|
|||||||
VOICE_DETECTED_URL=http://voice-dected:8000
|
VOICE_DETECTED_URL=http://voice-dected:8000
|
||||||
VOICEPRINT_STORE=/data/asr_voiceprints.json
|
VOICEPRINT_STORE=/data/asr_voiceprints.json
|
||||||
VOICEPRINT_MATCH_THRESHOLD=0.45
|
VOICEPRINT_MATCH_THRESHOLD=0.45
|
||||||
|
REALTIME_PARTIAL_MIN_SECONDS=1.5
|
||||||
|
REALTIME_PARTIAL_INTERVAL_SECONDS=2.0
|
||||||
|
|
||||||
# fastapi_wss legacy/local ASR-with-speaker helper
|
# fastapi_wss legacy/local ASR-with-speaker helper
|
||||||
ASR_LOCAL_URL=http://asr-test:59805/asr
|
ASR_LOCAL_URL=http://asr-test:59805/asr
|
||||||
|
|||||||
@ -158,11 +158,14 @@ def live_asr_proxy(client_ws):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if payload.get('type') == 'stop':
|
if payload.get('type') == 'stop':
|
||||||
|
logger.info('Realtime ASR stop requested, waiting for upstream final result')
|
||||||
upstream.send(json.dumps({'is_speaking': False}, ensure_ascii=False))
|
upstream.send(json.dumps({'is_speaking': False}, ensure_ascii=False))
|
||||||
stop_requested.set()
|
stop_requested.set()
|
||||||
deadline = time.time() + 3.0
|
deadline = time.time() + 25.0
|
||||||
while not upstream_closed.is_set() and time.time() < deadline:
|
while not upstream_closed.is_set() and time.time() < deadline:
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
|
if not upstream_closed.is_set():
|
||||||
|
logger.warning('Realtime ASR upstream did not close before final wait timeout')
|
||||||
break
|
break
|
||||||
if 'is_speaking' in payload or payload.get('mode'):
|
if 'is_speaking' in payload or payload.get('mode'):
|
||||||
upstream.send(json.dumps(payload, ensure_ascii=False))
|
upstream.send(json.dumps(payload, ensure_ascii=False))
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1218,7 +1218,11 @@ export class RealtimeAsrPipeline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.recTextOnline += shouldBreak ? text + "\n" : text;
|
if (parsedMessage.is_full_text) {
|
||||||
|
this.recTextOnline = shouldBreak ? text + "\n" : text;
|
||||||
|
} else {
|
||||||
|
this.recTextOnline += shouldBreak ? text + "\n" : text;
|
||||||
|
}
|
||||||
this.lastOnlineTimestamp = currentTimestamp;
|
this.lastOnlineTimestamp = currentTimestamp;
|
||||||
} else if (mode === "2pass-offline") {
|
} else if (mode === "2pass-offline") {
|
||||||
// online 通道也可能回显 offline 句子,这里只作为交接预览保存。
|
// online 通道也可能回显 offline 句子,这里只作为交接预览保存。
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user