feat(stt): pass conversation context as Whisper initial_prompt to reduce mishearings
This commit is contained in:
@@ -649,9 +649,10 @@ export const getVoiceStatus = () => apiGet<VoiceStatusResult>('/api/voice/status
|
|||||||
export const getVoiceList = () =>
|
export const getVoiceList = () =>
|
||||||
apiGet<{ voices: VoiceEntry[] }>('/api/voice/voices').then(r => r.voices)
|
apiGet<{ voices: VoiceEntry[] }>('/api/voice/voices').then(r => r.voices)
|
||||||
|
|
||||||
export async function transcribeAudio(blob: Blob): Promise<{ transcript: string; duration_ms: number }> {
|
export async function transcribeAudio(blob: Blob, context?: string): Promise<{ transcript: string; duration_ms: number }> {
|
||||||
const form = new FormData()
|
const form = new FormData()
|
||||||
form.append('audio', blob, 'audio.webm')
|
form.append('audio', blob, 'audio.webm')
|
||||||
|
if (context) form.append('context', context)
|
||||||
const res = await fetch('/api/voice/transcribe', { method: 'POST', body: form })
|
const res = await fetch('/api/voice/transcribe', { method: 'POST', body: form })
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }))
|
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }))
|
||||||
|
|||||||
@@ -102,7 +102,8 @@ async function stopPtt() {
|
|||||||
|
|
||||||
let transcript: string
|
let transcript: string
|
||||||
try {
|
try {
|
||||||
const result = await transcribeAudio(blob)
|
const lastAssistant = [...messages.value].reverse().find(m => m.role === 'assistant')?.content
|
||||||
|
const result = await transcribeAudio(blob, lastAssistant)
|
||||||
transcript = result.transcript.trim()
|
transcript = result.transcript.trim()
|
||||||
} catch {
|
} catch {
|
||||||
phase.value = 'error'
|
phase.value = 'error'
|
||||||
|
|||||||
@@ -80,10 +80,12 @@ async def transcribe_audio():
|
|||||||
return jsonify({"error": "Audio file too large (max 25 MB)"}), 413
|
return jsonify({"error": "Audio file too large (max 25 MB)"}), 413
|
||||||
|
|
||||||
mime_type = audio_file.content_type or "audio/webm"
|
mime_type = audio_file.content_type or "audio/webm"
|
||||||
|
form = await request.form
|
||||||
|
context = (form.get("context") or "").strip() or None
|
||||||
|
|
||||||
t0 = time.monotonic()
|
t0 = time.monotonic()
|
||||||
try:
|
try:
|
||||||
transcript = await transcribe(audio_bytes, mime_type)
|
transcript = await transcribe(audio_bytes, mime_type, initial_prompt=context)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("STT transcription failed")
|
logger.exception("STT transcription failed")
|
||||||
return jsonify({"error": "Transcription failed"}), 500
|
return jsonify({"error": "Transcription failed"}), 500
|
||||||
|
|||||||
@@ -55,8 +55,12 @@ def stt_available() -> bool:
|
|||||||
return _model is not None
|
return _model is not None
|
||||||
|
|
||||||
|
|
||||||
async def transcribe(audio_bytes: bytes, mime_type: str = "audio/webm") -> str:
|
async def transcribe(audio_bytes: bytes, mime_type: str = "audio/webm", initial_prompt: str | None = None) -> str:
|
||||||
"""Transcribe audio bytes to text. Runs the model in a thread executor."""
|
"""Transcribe audio bytes to text. Runs the model in a thread executor.
|
||||||
|
|
||||||
|
initial_prompt: optional text to bias the model toward domain-specific vocabulary
|
||||||
|
(e.g. recent conversation context). Reduces mishearings like "gold" for "cold".
|
||||||
|
"""
|
||||||
if _model is None:
|
if _model is None:
|
||||||
raise RuntimeError("STT model not loaded")
|
raise RuntimeError("STT model not loaded")
|
||||||
|
|
||||||
@@ -73,7 +77,11 @@ async def transcribe(audio_bytes: bytes, mime_type: str = "audio/webm") -> str:
|
|||||||
f.write(audio_bytes)
|
f.write(audio_bytes)
|
||||||
f.flush()
|
f.flush()
|
||||||
t0 = time.monotonic()
|
t0 = time.monotonic()
|
||||||
segments, _ = _model.transcribe(f.name, beam_size=5) # type: ignore[union-attr]
|
segments, _ = _model.transcribe( # type: ignore[union-attr]
|
||||||
|
f.name,
|
||||||
|
beam_size=5,
|
||||||
|
initial_prompt=initial_prompt or None,
|
||||||
|
)
|
||||||
text = " ".join(seg.text.strip() for seg in segments).strip()
|
text = " ".join(seg.text.strip() for seg in segments).strip()
|
||||||
logger.debug("STT transcription took %.2fs", time.monotonic() - t0)
|
logger.debug("STT transcription took %.2fs", time.monotonic() - t0)
|
||||||
return text
|
return text
|
||||||
|
|||||||
Reference in New Issue
Block a user