Persist realtime transcription sessions to history
The websocket handler now accumulates the full session's PCM audio
(not just the per-chunk buffer that gets discarded after each
worker call) and, on stop, writes it as a wav to UPLOAD_DIR plus a
result JSON to RESULT_DIR via the new _save_realtime_history()
helper — same shape /transcribe already writes, tagged
gateway_backend="realtime" so the history table visually
distinguishes it from file uploads. This reuses /asr/history and
/asr/uploads/{filename} as-is; no new endpoints needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -268,6 +268,41 @@ async def _send_chunk(wav_bytes: bytes, *, model: str, language: str, beam_size:
|
|||||||
return resp.json()
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
|
def _save_realtime_history(
|
||||||
|
*,
|
||||||
|
all_audio: bytearray,
|
||||||
|
text: str,
|
||||||
|
segments: List[Dict[str, Any]],
|
||||||
|
model: str,
|
||||||
|
language: str,
|
||||||
|
duration: float,
|
||||||
|
) -> Optional[str]:
|
||||||
|
if not all_audio:
|
||||||
|
return None
|
||||||
|
|
||||||
|
ensure_runtime_dirs()
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
||||||
|
saved_upload = UPLOAD_DIR / f"{timestamp}.wav"
|
||||||
|
saved_upload.write_bytes(_pcm_float32_to_wav(bytes(all_audio)))
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"backend": "faster-whisper",
|
||||||
|
"gateway_backend": "realtime",
|
||||||
|
"model": model,
|
||||||
|
"language": language,
|
||||||
|
"duration": round(duration, 3),
|
||||||
|
"text": text,
|
||||||
|
"segments": segments,
|
||||||
|
"diarized": False,
|
||||||
|
"_filename": "실시간 녹음",
|
||||||
|
"_timestamp": timestamp,
|
||||||
|
"upload_file": str(saved_upload),
|
||||||
|
}
|
||||||
|
result_path = RESULT_DIR / f"{timestamp}.json"
|
||||||
|
result_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
return timestamp
|
||||||
|
|
||||||
|
|
||||||
@ws_router.websocket("/ws/realtime")
|
@ws_router.websocket("/ws/realtime")
|
||||||
async def realtime_ws(websocket: WebSocket) -> None:
|
async def realtime_ws(websocket: WebSocket) -> None:
|
||||||
if not websocket.session.get("user"):
|
if not websocket.session.get("user"):
|
||||||
@@ -276,6 +311,7 @@ async def realtime_ws(websocket: WebSocket) -> None:
|
|||||||
await websocket.accept()
|
await websocket.accept()
|
||||||
|
|
||||||
audio_buf = bytearray()
|
audio_buf = bytearray()
|
||||||
|
all_audio = bytearray()
|
||||||
partial_texts: List[str] = []
|
partial_texts: List[str] = []
|
||||||
all_segments: List[Dict[str, Any]] = []
|
all_segments: List[Dict[str, Any]] = []
|
||||||
time_offset = 0.0
|
time_offset = 0.0
|
||||||
@@ -333,6 +369,7 @@ async def realtime_ws(websocket: WebSocket) -> None:
|
|||||||
break
|
break
|
||||||
if "bytes" in data:
|
if "bytes" in data:
|
||||||
audio_buf.extend(data["bytes"])
|
audio_buf.extend(data["bytes"])
|
||||||
|
all_audio.extend(data["bytes"])
|
||||||
while len(audio_buf) >= chunk_bytes:
|
while len(audio_buf) >= chunk_bytes:
|
||||||
await process_buffer(bytearray(audio_buf[:chunk_bytes]))
|
await process_buffer(bytearray(audio_buf[:chunk_bytes]))
|
||||||
audio_buf = audio_buf[chunk_bytes:]
|
audio_buf = audio_buf[chunk_bytes:]
|
||||||
@@ -341,11 +378,21 @@ async def realtime_ws(websocket: WebSocket) -> None:
|
|||||||
if msg.get("cmd") == "stop":
|
if msg.get("cmd") == "stop":
|
||||||
if audio_buf:
|
if audio_buf:
|
||||||
await process_buffer(audio_buf)
|
await process_buffer(audio_buf)
|
||||||
|
full_text = " ".join(x for x in partial_texts if x)
|
||||||
|
result_id = _save_realtime_history(
|
||||||
|
all_audio=all_audio,
|
||||||
|
text=full_text,
|
||||||
|
segments=all_segments,
|
||||||
|
model=model,
|
||||||
|
language=language,
|
||||||
|
duration=time_offset,
|
||||||
|
)
|
||||||
await websocket.send_json({
|
await websocket.send_json({
|
||||||
"type": "final",
|
"type": "final",
|
||||||
"text": " ".join(x for x in partial_texts if x),
|
"text": full_text,
|
||||||
"segments": all_segments,
|
"segments": all_segments,
|
||||||
"diarized": False,
|
"diarized": False,
|
||||||
|
"id": result_id,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ async function startRecording() {
|
|||||||
appendLive('');
|
appendLive('');
|
||||||
} else if (msg.type === 'final') {
|
} else if (msg.type === 'final') {
|
||||||
rtFinalText.value = msg.text || '';
|
rtFinalText.value = msg.text || '';
|
||||||
setIndicator('idle', '완료');
|
setIndicator('idle', msg.id ? '완료 (처리 내역에 저장됨)' : '완료');
|
||||||
cleanupAudio();
|
cleanupAudio();
|
||||||
} else if (msg.type === 'error') {
|
} else if (msg.type === 'error') {
|
||||||
setIndicator('idle', `오류: ${msg.detail}`);
|
setIndicator('idle', `오류: ${msg.detail}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user