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:
du5t
2026-06-19 00:30:15 +09:00
parent acdf098c41
commit 22e9b805a6
2 changed files with 49 additions and 2 deletions

View File

@@ -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

View File

@@ -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}`);