From b15bd7794e9364034023358fb780c20f96f74510 Mon Sep 17 00:00:00 2001 From: truetype Date: Wed, 16 Sep 2026 21:22:41 +0200 Subject: [PATCH] fix(speech): generate TXT/JSON/MD locally from transcription result Download buttons now use the already-completed transcription result instead of re-sending the audio to the server for re-transcription. This eliminates redundant server work and prevents download failures when the second transcription attempt errors out. Also wires up suppressNonSpeech and noSpeechThreshold options through the API and server pipeline. --- .../src/components/SpeechTranscriber.jsx | 138 +++++++++++++----- app/frontend/src/services/api.js | 2 + scripts/server/serve.cjs | 27 +++- 3 files changed, 125 insertions(+), 42 deletions(-) diff --git a/app/frontend/src/components/SpeechTranscriber.jsx b/app/frontend/src/components/SpeechTranscriber.jsx index 41b79e9..cc2bd93 100644 --- a/app/frontend/src/components/SpeechTranscriber.jsx +++ b/app/frontend/src/components/SpeechTranscriber.jsx @@ -147,10 +147,16 @@ export default function SpeechTranscriber({ const threads = speechSettings?.threads || 4; const backendPreference = speechSettings?.backendPreference || "auto"; const translate = speechSettings?.translate === true; + const suppressNonSpeech = speechSettings?.suppressNonSpeech !== false; + const noSpeechThreshold = speechSettings?.noSpeechThreshold ?? 0.30; + const showAdvanced = speechSettings?.showAdvanced === true; const setLanguage = (val) => setSpeechSettings((prev) => ({ ...prev, language: val })); const setThreads = (val) => setSpeechSettings((prev) => ({ ...prev, threads: val })); const setBackendPreference = (val) => setSpeechSettings((prev) => ({ ...prev, backendPreference: val })); + const setSuppressNonSpeech = (val) => setSpeechSettings((prev) => ({ ...prev, suppressNonSpeech: val })); + const setNoSpeechThreshold = (val) => setSpeechSettings((prev) => ({ ...prev, noSpeechThreshold: val })); + const setShowAdvanced = (val) => setSpeechSettings((prev) => ({ ...prev, showAdvanced: val })); const [audioBlob, setAudioBlob] = useState(null); const [audioName, setAudioName] = useState(""); @@ -324,6 +330,8 @@ export default function SpeechTranscriber({ backendPreference, translate, filename: audioName || "audio.wav", + suppressNonSpeech, + noSpeechThreshold, signal: controller.signal, }); setResult(transcription); @@ -352,54 +360,62 @@ export default function SpeechTranscriber({ setIsTranscribing(false); }; - const saveText = async () => { - if (!audioBlob) return; - await downloadTranscription("txt"); + const saveText = () => { + if (!result) return; + downloadResultAsFile(result, "txt"); }; - const saveJson = async () => { - if (!audioBlob) return; - await downloadTranscription("json"); + const saveJson = () => { + if (!result) return; + downloadResultAsFile(result, "json"); }; - const saveMarkdown = async () => { - if (!audioBlob) return; - await downloadTranscription("md"); + const saveMarkdown = () => { + if (!result) return; + downloadResultAsFile(result, "md"); }; - const downloadTranscription = async (format) => { - if (!audioBlob) return; + const downloadResultAsFile = (transcription, format) => { try { - const params = new URLSearchParams(); - if (selectedModel) params.set("model", selectedModel); - params.set("language", speechSettings?.language || "auto"); - params.set("format", format); - params.set("threads", String(speechSettings?.threads || 4)); - params.set("backendPreference", speechSettings?.backendPreference || "auto"); - if (speechSettings?.translate) params.set("translate", "true"); - params.set("filename", audioName || "audio.wav"); - params.set("output_filename", `transcription-${Date.now()}`); + const baseName = (audioName || "transcription").replace(/\.[^.]+$/, ""); + const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19); + let fileContent = ""; + let contentType = "text/plain;charset=utf-8"; + let extension = "txt"; - const res = await fetch(`/api/speech/transcribe-file?${params.toString()}`, { - method: "POST", - headers: { "Content-Type": "audio/wav" }, - body: audioBlob, - }); - - if (!res.ok) { - const errData = await res.json().catch(() => ({})); - throw new Error(errData.error || `Download failed (HTTP ${res.status})`); + if (format === "json") { + fileContent = JSON.stringify({ + text: transcription?.text || "", + model: transcription?.model || selectedModel || "", + modelName: transcription?.modelName || "", + language: transcription?.language || speechSettings?.language || "auto", + backend: transcription?.backend || "", + backendMode: transcription?.backendMode || "", + sourceFilename: transcription?.sourceFilename || audioName || "", + durationMs: transcription?.durationMs || 0, + raw: transcription?.raw || {}, + createdAt: transcription?.createdAt || new Date().toISOString(), + }, null, 2); + contentType = "application/json;charset=utf-8"; + extension = "json"; + } else if (format === "md") { + const modelName = transcription?.modelName || transcription?.model || selectedModel || "unknown"; + const lang = transcription?.language || speechSettings?.language || "auto"; + const backend = transcription?.backend || "unknown"; + const backendMode = transcription?.backendMode || ""; + const source = transcription?.sourceFilename || audioName || "unknown"; + const durMs = transcription?.durationMs || 0; + const durSec = durMs ? (durMs / 1000).toFixed(1) : "?"; + fileContent = `# Trascrizione\n\n${transcription?.text || ""}\n\n---\n*Modello: ${modelName}\n*Lingua: ${lang}\n*Backend: ${backend} (${backendMode})\n*File sorgente: ${source}\n*Durata elaborazione: ${durSec}s\n*Generato il: ${new Date().toISOString()}`; + contentType = "text/markdown;charset=utf-8"; + extension = "md"; + } else { + fileContent = transcription?.text || ""; + contentType = "text/plain;charset=utf-8"; + extension = "txt"; } - const blob = await res.blob(); - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = `transcription-${Date.now()}.${format}`; - document.body.appendChild(link); - link.click(); - link.remove(); - URL.revokeObjectURL(url); + downloadBlob(`${baseName}-${stamp}.${extension}`, fileContent, contentType); } catch (err) { showAlert?.({ title: "Download Failed", message: err.message || String(err), danger: true }); } @@ -437,6 +453,13 @@ export default function SpeechTranscriber({ + {selectedModel && selectedModel.toLowerCase().includes("base") && ( +
+ ⚠️ Il modello Base può non trascrivere correttamente audio compresso (es. Google Meet, Zoom) — potrebbe rilevare solo "[Musica]". + Si consiglia il modello Small Multilingual per audio di bassa qualità. +
+ )} +
+ {/* Advanced settings toggle */} +
+ +
+ + {showAdvanced && ( +
+ + +
+ )} +