fix(dictation): keep transcription context opt-in

An English instruction used as transcript context caused the transcription
model to discard Portuguese list items. Omit built-in context and retain
explicit vocabulary overrides, including an empty override to disable it.

Co-authored-by: d0zingcat <d0zingcat@users.noreply.github.com>
Co-authored-by: Joey Hotz <joeyhotz1@gmail.com>
This commit is contained in:
d0zingcat
2026-11-06 03:13:21 +00:00
committed by paseo-bot[bot]
co-authored by Joey Hotz
parent 3ed57e9dd7
commit f898537233
4 changed files with 82 additions and 8 deletions
@@ -33,9 +33,9 @@ async function main(): Promise<void> {
const apiKey = requireEnv("OPENAI_API_KEY");
const transcriptionModel = process.env.STT_MODEL ?? "gpt-4o-transcribe";
const prompt =
process.env.PASEO_DICTATION_TRANSCRIPTION_PROMPT ??
"Transcribe only what the speaker says. Do not add words. Preserve punctuation and casing. If the audio is silence or non-speech noise, return an empty transcript.";
// Mirrors the daemon: no built-in prompt, only an explicit override. Whisper
// treats `prompt` as preceding transcript, so a default would bias the baseline.
const prompt = process.env.PASEO_DICTATION_TRANSCRIPTION_PROMPT?.trim() || undefined;
const openai = new OpenAI({ apiKey });
@@ -43,7 +43,7 @@ async function main(): Promise<void> {
file: await import("node:fs").then((fs) => fs.createReadStream(audioPath)),
language: "en",
model: transcriptionModel,
prompt,
...(prompt ? { prompt } : {}),
response_format: "json",
// Aim for determinism.
temperature: 0,
@@ -57,11 +57,13 @@ class FakeRealtimeSession extends EventEmitter implements StreamingTranscription
class FakeSttProvider implements SpeechToTextProvider {
public readonly id = "fake";
public lastLanguage?: string;
public lastPrompt?: string;
constructor(private readonly session: FakeRealtimeSession) {}
createSession(
params: Parameters<SpeechToTextProvider["createSession"]>[0],
): StreamingTranscriptionSession {
this.lastLanguage = params.language;
this.lastPrompt = params.prompt;
return this.session;
}
}
@@ -594,3 +596,58 @@ it("closes every dictation stream when one provider cleanup fails", async () =>
expect(second.closed).toBe(true);
expect(manager.hasDemand).toBe(false);
});
describe("DictationStreamManager (transcription prompt)", () => {
const originalPromptEnv = process.env.PASEO_DICTATION_TRANSCRIPTION_PROMPT;
afterEach(() => {
if (originalPromptEnv === undefined) {
delete process.env.PASEO_DICTATION_TRANSCRIPTION_PROMPT;
} else {
process.env.PASEO_DICTATION_TRANSCRIPTION_PROMPT = originalPromptEnv;
}
});
const startDictation = async (
options: { language?: string } = {},
): Promise<{ provider: FakeSttProvider; manager: DictationStreamManager }> => {
const session = new FakeRealtimeSession();
const provider = new FakeSttProvider(session);
const manager = new DictationStreamManager({
logger: pino({ level: "silent" }),
emit: () => {},
sessionId: "prompt",
stt: provider,
...(options.language ? { language: options.language } : {}),
});
await manager.handleStart("d1", "audio/pcm;rate=24000;bits=16");
manager.cleanupAll();
return { provider, manager };
};
it("sends no prompt by default", async () => {
delete process.env.PASEO_DICTATION_TRANSCRIPTION_PROMPT;
const { provider } = await startDictation();
expect(provider.lastPrompt).toBeUndefined();
});
it("sends no prompt for non-English dictation by default", async () => {
delete process.env.PASEO_DICTATION_TRANSCRIPTION_PROMPT;
const { provider } = await startDictation({ language: "zh" });
expect(provider.lastLanguage).toBe("zh");
expect(provider.lastPrompt).toBeUndefined();
});
it("passes an explicit prompt override through", async () => {
process.env.PASEO_DICTATION_TRANSCRIPTION_PROMPT =
" Vocabulary: Paseo, Dokploy, Kubernetes. ";
const { provider } = await startDictation();
expect(provider.lastPrompt).toBe("Vocabulary: Paseo, Dokploy, Kubernetes.");
});
it.each(["", " "])("treats a blank override (%j) as no prompt", async (value) => {
process.env.PASEO_DICTATION_TRANSCRIPTION_PROMPT = value;
const { provider } = await startDictation();
expect(provider.lastPrompt).toBeUndefined();
});
});
@@ -38,6 +38,12 @@ function parseNonNegativeNumber(value: string | undefined): number | null {
return parsed;
}
/** Unset, empty, and whitespace-only overrides all mean "send no prompt". */
function readTranscriptionPromptOverride(env: NodeJS.ProcessEnv): string | undefined {
const value = env.PASEO_DICTATION_TRANSCRIPTION_PROMPT?.trim();
return value ? value : undefined;
}
function convertPCMToWavBuffer(
pcmBuffer: Buffer,
sampleRate: number,
@@ -185,16 +191,16 @@ export class DictationStreamManager {
return;
}
const transcriptionPrompt =
process.env.PASEO_DICTATION_TRANSCRIPTION_PROMPT ??
"Transcribe only what the speaker says. Do not add words. Preserve punctuation and casing. If the audio is silence or non-speech noise, return an empty transcript.";
// Whisper uses the prompt as transcript context, not silence instructions.
// Keep context opt-in so it does not bias speech or introduce unspoken text.
const transcriptionPrompt = readTranscriptionPromptOverride(process.env);
let stt: ReturnType<SpeechToTextProvider["createSession"]>;
try {
stt = sttProvider.createSession({
logger: this.logger.child({ dictationId }),
language: this.language,
prompt: transcriptionPrompt,
...(transcriptionPrompt ? { prompt: transcriptionPrompt } : {}),
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
+11
View File
@@ -142,8 +142,19 @@ Keys and base URLs follow the fallback rule above.
- `PASEO_VOICE_LOCAL_STT_MODEL`, `PASEO_VOICE_LOCAL_TTS_MODEL`, local voice STT/TTS model IDs
- `PASEO_DICTATION_LANGUAGE`, dictation STT language
- `PASEO_VOICE_LANGUAGE`, voice mode STT language; falls back to `PASEO_DICTATION_LANGUAGE` when unset
- `PASEO_DICTATION_TRANSCRIPTION_PROMPT`, optional dictation transcription prompt. Unset by default; empty or whitespace-only is treated as unset
- `PASEO_VOICE_LOCAL_TTS_SPEAKER_ID`, `PASEO_VOICE_LOCAL_TTS_SPEED`, optional local voice TTS tuning
## Transcription Prompt
Paseo sends no transcription prompt by default. Whisper-family models use `prompt` as transcript context, so instruction text can distort the transcript or bias its language. Omitting it does not prevent every hallucination on silence or noise.
Set `PASEO_DICTATION_TRANSCRIPTION_PROMPT` to send a prompt explicitly, for example to bias toward project vocabulary. An empty or whitespace-only value means no prompt. This affects dictation only; voice mode never sends a prompt.
```bash
PASEO_DICTATION_TRANSCRIPTION_PROMPT="Vocabulary: Paseo, TypeScript."
```
## Operational Notes
Voice mode can launch and control agents. Treat voice prompts with the same care as direct agent instructions, especially when specifying working directories or destructive operations.