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.
This commit is contained in:
truetype committed 2026-09-16 21:22:41 +02:00
1 parent a0bbd9a035
commit b15bd7794e
3 files changed
+125 -42

No files matched your search

+23 -4
View File
@@ -2560,6 +2560,15 @@ function transcribeWavBuffer(buffer, options = {}) {
if (options.translate) {
args.push("-tr");
}
// Suppress non-speech tokens (e.g. [Musica], [Applauso]) that appear
// when the model misidentifies compressed/noisy audio as non-speech
if (options.suppressNonSpeech !== false) {
args.push("-sns");
}
// Lower no-speech threshold (default 0.60) so the model is less likely
// to label quiet or compressed speech as non-speech
const noSpeechThreshold = options.noSpeechThreshold || "0.30";
args.push("-nth", String(noSpeechThreshold));
const startedAt = Date.now();
console.log(" [speech] Starting:", backend.cli, args.join(" "));
@@ -6030,14 +6039,19 @@ const server = http.createServer(async (req, res) => {
const parsed = new URL(req.url, `http://${req.headers.host || "localhost"}`);
try {
const audio = await readBinaryBody(req);
const result = await runExclusiveSpeechOperation(() => transcribeAudioBuffer(audio, {
const suppressNonSpeech = parsed.searchParams.get("suppressNonSpeech");
const noSpeechThreshold = parsed.searchParams.get("noSpeechThreshold");
const transcribeOptions = {
model: parsed.searchParams.get("model") || speechSettings.model,
language: parsed.searchParams.get("language") || "auto",
filename: parsed.searchParams.get("filename") || req.headers["x-filename"] || "recording",
threads: parsed.searchParams.get("threads") || speechSettings.threads,
backendPreference: parsed.searchParams.get("backendPreference") || speechSettings.backendPreference,
translate: parsed.searchParams.get("translate") === "true",
}));
};
if (suppressNonSpeech !== null) transcribeOptions.suppressNonSpeech = suppressNonSpeech !== "false";
if (noSpeechThreshold !== null) transcribeOptions.noSpeechThreshold = noSpeechThreshold;
const result = await runExclusiveSpeechOperation(() => transcribeAudioBuffer(audio, transcribeOptions));
return json(res, 200, { ok: true, transcription: result });
} catch (err) {
speechError = err.message || String(err);
@@ -6052,14 +6066,19 @@ const server = http.createServer(async (req, res) => {
try {
const audio = await readBinaryBody(req);
const outputFormat = parsed.searchParams.get("format") || "txt";
const result = await runExclusiveSpeechOperation(() => transcribeAudioBuffer(audio, {
const suppressNonSpeech = parsed.searchParams.get("suppressNonSpeech");
const noSpeechThreshold = parsed.searchParams.get("noSpeechThreshold");
const transcribeOptions = {
model: parsed.searchParams.get("model") || speechSettings.model,
language: parsed.searchParams.get("language") || "auto",
filename: parsed.searchParams.get("filename") || req.headers["x-filename"] || "recording",
threads: parsed.searchParams.get("threads") || speechSettings.threads,
backendPreference: parsed.searchParams.get("backendPreference") || speechSettings.backendPreference,
translate: parsed.searchParams.get("translate") === "true",
}));
};
if (suppressNonSpeech !== null) transcribeOptions.suppressNonSpeech = suppressNonSpeech !== "false";
if (noSpeechThreshold !== null) transcribeOptions.noSpeechThreshold = noSpeechThreshold;
const result = await runExclusiveSpeechOperation(() => transcribeAudioBuffer(audio, transcribeOptions));
const filename = parsed.searchParams.get("output_filename") || `transcription-${Date.now()}`;
let fileContent = "";