diff --git a/apps/stt/main.py b/apps/stt/main.py index 5692f89..451ea8a 100644 --- a/apps/stt/main.py +++ b/apps/stt/main.py @@ -177,27 +177,38 @@ def _wav_to_float32(wav_bytes: bytes) -> tuple[np.ndarray, int]: return audio, rate +def _resample_audio(audio: np.ndarray, src_rate: int, dst_rate: int = 16000) -> np.ndarray: + if src_rate == dst_rate or audio.size == 0: + return audio.astype(np.float32, copy=False) + duration = audio.shape[0] / float(src_rate) + new_len = max(1, int(round(duration * dst_rate))) + x_old = np.linspace(0.0, 1.0, num=audio.shape[0], endpoint=False) + x_new = np.linspace(0.0, 1.0, num=new_len, endpoint=False) + return np.interp(x_new, x_old, audio).astype(np.float32) + + def _ensure_16k(wav_bytes: bytes) -> np.ndarray: audio, rate = _wav_to_float32(wav_bytes) if rate == 16000: return audio + # Prefer ffmpeg when available; otherwise pure-numpy resample (browser often sends 44.1/48 kHz) ffmpeg = shutil.which("ffmpeg") - if not ffmpeg: - raise HTTPException(status_code=400, detail="نرخ نمونه‌برداری صوت پشتیبانی نشد") - with tempfile.TemporaryDirectory() as tmp: - src = Path(tmp) / "in.wav" - dst = Path(tmp) / "out.wav" - src.write_bytes(wav_bytes) - result = subprocess.run( - [ffmpeg, "-y", "-i", str(src), "-ac", "1", "-ar", "16000", "-f", "wav", str(dst)], - capture_output=True, - check=False, - ) - if result.returncode != 0 or not dst.exists(): - raise HTTPException(status_code=400, detail="نرمال‌سازی صوت ناموفق بود") - audio, _ = _wav_to_float32(dst.read_bytes()) - return audio + if ffmpeg: + with tempfile.TemporaryDirectory() as tmp: + src = Path(tmp) / "in.wav" + dst = Path(tmp) / "out.wav" + src.write_bytes(wav_bytes) + result = subprocess.run( + [ffmpeg, "-y", "-i", str(src), "-ac", "1", "-ar", "16000", "-f", "wav", str(dst)], + capture_output=True, + check=False, + ) + if result.returncode == 0 and dst.exists(): + audio, _ = _wav_to_float32(dst.read_bytes()) + return audio + + return _resample_audio(audio, rate, 16000) def _transcribe_local(wav_bytes: bytes) -> str: diff --git a/apps/web/src/components/VoiceToTextButton.vue b/apps/web/src/components/VoiceToTextButton.vue index edcfb2d..3342e06 100644 --- a/apps/web/src/components/VoiceToTextButton.vue +++ b/apps/web/src/components/VoiceToTextButton.vue @@ -137,6 +137,25 @@ function mergeChunks(parts: Float32Array[]) { return out; } +/** Downsample to 16 kHz so STT does not depend on server ffmpeg. */ +function resampleTo16k(samples: Float32Array, srcRate: number) { + const dstRate = 16000; + if (!srcRate || srcRate === dstRate) return { samples, rate: srcRate || dstRate }; + const ratio = srcRate / dstRate; + const newLen = Math.max(1, Math.floor(samples.length / ratio)); + const out = new Float32Array(newLen); + for (let i = 0; i < newLen; i += 1) { + const srcIndex = i * ratio; + const i0 = Math.floor(srcIndex); + const i1 = Math.min(samples.length - 1, i0 + 1); + const t = srcIndex - i0; + const a = samples[i0] ?? 0; + const b = samples[i1] ?? a; + out[i] = a + (b - a) * t; + } + return { samples: out, rate: dstRate }; +} + function encodeWav(samples: Float32Array, rate: number) { const bytes = samples.length * 2; const buffer = new ArrayBuffer(44 + bytes); @@ -189,7 +208,8 @@ async function finish(keep: boolean) { setTranscribing(true); try { - const text = await upload(encodeWav(samples, rate)); + const prepared = resampleTo16k(samples, rate); + const text = await upload(encodeWav(prepared.samples, prepared.rate)); if (!text) { setError('متنی از صدا تشخیص داده نشد.'); return;