Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **Deepgram keyterms configuration** — `voice.deepgramKeyterms` lets users
provide words or phrases (for example app names, tools, and product names)
that are sent to Deepgram as `keyterm` query parameters to improve Nova-3
recognition accuracy.

## [7.2.2] - 2026-05-01

### Added
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,24 @@ Settings stored in Pi's settings files under the `voice` key:
into `~/.pi/agent/settings.json`. If you paste a key during onboarding, that is
an explicit save and it still goes to `~/.env.secrets` or `~/.zshrc`.

### Deepgram keyterms

When using the Deepgram backend, add `deepgramKeyterms` to bias recognition
toward words or phrases that are frequently misheard, such as app names,
libraries, or product names:

```json
{
"voice": {
"backend": "deepgram",
"deepgramKeyterms": ["ffmpeg", "GStreamer", "pi agent"]
}
}
```

Each entry is sent to Deepgram as a `keyterm` query parameter. This setting is
ignored by the local/offline backend.

---

## Troubleshooting
Expand Down
6 changes: 6 additions & 0 deletions extensions/voice/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export interface VoiceConfig {
localEndpoint?: string;
/** Global-only shortcut used to toggle recording without hold-to-talk */
toggleShortcut?: string;
/** Deepgram Nova-3 keyterms: words/phrases to bias recognition toward. */
deepgramKeyterms?: string[];

// ─── TTS (text-to-speech) ─────────────────────────────────────────
// All TTS fields are opt-in (default: TTS disabled). New in v6.0.0.
Expand Down Expand Up @@ -125,6 +127,7 @@ export const DEFAULT_CONFIG: VoiceConfig = {
localModel: undefined,
localEndpoint: undefined,
toggleShortcut: "ctrl+shift+v",
deepgramKeyterms: [],
// TTS defaults — all opt-in
ttsEnabled: false,
ttsBackend: "local",
Expand Down Expand Up @@ -197,6 +200,9 @@ function migrateConfig(rawVoice: any, source: VoiceConfigSource): VoiceConfig {
toggleShortcut: source !== "project" && typeof rawVoice.toggleShortcut === "string"
? rawVoice.toggleShortcut
: DEFAULT_CONFIG.toggleShortcut,
deepgramKeyterms: Array.isArray(rawVoice.deepgramKeyterms)
? rawVoice.deepgramKeyterms.filter((term: unknown): term is string => typeof term === "string" && term.trim().length > 0)
: DEFAULT_CONFIG.deepgramKeyterms,
// TTS fields — type-validated; mismatched persisted values fall
// back to safe defaults so a hand-edited config can't poison the
// engine. Notably: ttsLocalVoiceId rejects strings (would crash
Expand Down
4 changes: 4 additions & 0 deletions extensions/voice/deepgram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export function buildDeepgramWsUrl(config: VoiceConfig): string {
smart_format: "true",
interim_results: "true",
});
for (const keyterm of config.deepgramKeyterms ?? []) {
const normalized = keyterm.trim();
if (normalized) params.append("keyterm", normalized);
}
return `${DEEPGRAM_WS_URL}?${params.toString()}`;
}

Expand Down
13 changes: 13 additions & 0 deletions tests/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,19 @@ describe("loadConfigWithSource", () => {
expect(result.config.onboarding.completed).toBe(false);
});

test("loads Deepgram keyterms from settings", () => {
const cwd = makeTempDir();
const agentDir = path.join(cwd, "agent-home");
writeSettings(agentDir, "settings.json", {
enabled: true,
deepgramKeyterms: ["Raycast", "", " ", "VS Code"],
});

const result = loadConfigWithSource(cwd, { agentDir });

expect(result.config.deepgramKeyterms).toEqual(["Raycast", "VS Code"]);
});

test("prefers project config over global config and preserves project scope", () => {
const cwd = makeTempDir();
const agentDir = path.join(cwd, "agent-home");
Expand Down
24 changes: 24 additions & 0 deletions tests/deepgram.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { describe, expect, test } from "bun:test";
import { DEFAULT_CONFIG } from "../extensions/voice/config";
import { buildDeepgramWsUrl } from "../extensions/voice/deepgram";

describe("buildDeepgramWsUrl", () => {
test("adds Deepgram keyterms from config", () => {
const url = new URL(buildDeepgramWsUrl({
...DEFAULT_CONFIG,
language: "pt-BR",
deepgramKeyterms: ["Raycast", "Linear", "VS Code"],
}));

expect(url.searchParams.getAll("keyterm")).toEqual(["Raycast", "Linear", "VS Code"]);
});

test("skips blank Deepgram keyterms", () => {
const url = new URL(buildDeepgramWsUrl({
...DEFAULT_CONFIG,
deepgramKeyterms: ["", " ", "Cursor"],
}));

expect(url.searchParams.getAll("keyterm")).toEqual(["Cursor"]);
});
});