diff --git a/app/asr/router.py b/app/asr/router.py index bf8b689..0e68848 100644 --- a/app/asr/router.py +++ b/app/asr/router.py @@ -268,6 +268,41 @@ async def _send_chunk(wav_bytes: bytes, *, model: str, language: str, beam_size: 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") async def realtime_ws(websocket: WebSocket) -> None: if not websocket.session.get("user"): @@ -276,6 +311,7 @@ async def realtime_ws(websocket: WebSocket) -> None: await websocket.accept() audio_buf = bytearray() + all_audio = bytearray() partial_texts: List[str] = [] all_segments: List[Dict[str, Any]] = [] time_offset = 0.0 @@ -333,6 +369,7 @@ async def realtime_ws(websocket: WebSocket) -> None: break if "bytes" in data: audio_buf.extend(data["bytes"]) + all_audio.extend(data["bytes"]) while len(audio_buf) >= chunk_bytes: await process_buffer(bytearray(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 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({ "type": "final", - "text": " ".join(x for x in partial_texts if x), + "text": full_text, "segments": all_segments, "diarized": False, + "id": result_id, }) break diff --git a/app/ui/assets/js/asr.js b/app/ui/assets/js/asr.js index d50c296..725e499 100644 --- a/app/ui/assets/js/asr.js +++ b/app/ui/assets/js/asr.js @@ -200,7 +200,7 @@ async function startRecording() { appendLive(''); } else if (msg.type === 'final') { rtFinalText.value = msg.text || ''; - setIndicator('idle', '완료'); + setIndicator('idle', msg.id ? '완료 (처리 내역에 저장됨)' : '완료'); cleanupAudio(); } else if (msg.type === 'error') { setIndicator('idle', `오류: ${msg.detail}`);