mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 16:34:36 +00:00
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:
committed by
paseo-bot[bot]
co-authored by
Joey Hotz
parent
3ed57e9dd7
commit
f898537233
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user