osborn 0.9.190 → 0.9.192
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/claude-llm.js +8 -3
- package/dist/config.d.ts +3 -3
- package/dist/config.js +32 -8
- package/dist/index.js +12 -2
- package/dist/turn-detector-shim.d.ts +10 -4
- package/dist/turn-detector-shim.js +92 -17
- package/dist/voice-io.d.ts +4 -3
- package/dist/voice-io.js +35 -0
- package/package.json +2 -1
package/dist/claude-llm.js
CHANGED
|
@@ -62,6 +62,11 @@ function stripMarkdownForTTS(text) {
|
|
|
62
62
|
.replace(/ +/g, ' ')
|
|
63
63
|
.trim();
|
|
64
64
|
}
|
|
65
|
+
function getSubagentsDir(workingDir) {
|
|
66
|
+
const dir = join(workingDir, 'subagents');
|
|
67
|
+
mkdirSync(dir, { recursive: true });
|
|
68
|
+
return dir;
|
|
69
|
+
}
|
|
65
70
|
/**
|
|
66
71
|
* Load skill files from agent/.claude/skills/{name}/SKILL.md
|
|
67
72
|
* Injects into system prompt so Claude sees them as available capabilities.
|
|
@@ -1332,7 +1337,7 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
1332
1337
|
// agents roster prevents any SubagentStop(agent_type==='writer') from
|
|
1333
1338
|
// firing inside this one-shot query and re-arming the backstop loop.
|
|
1334
1339
|
const reviewerOptions = {
|
|
1335
|
-
cwd: this.#opts.workingDirectory,
|
|
1340
|
+
cwd: getSubagentsDir(this.#opts.workingDirectory),
|
|
1336
1341
|
permissionMode: 'default',
|
|
1337
1342
|
systemPrompt: NAMED_AGENTS.reviewer.prompt,
|
|
1338
1343
|
allowedTools: ['Read', 'Glob', 'Grep', 'Bash', 'Write', 'Edit'],
|
|
@@ -1441,7 +1446,7 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
1441
1446
|
gitDiff,
|
|
1442
1447
|
].join('\n');
|
|
1443
1448
|
const testerOptions = {
|
|
1444
|
-
cwd: this.#opts.workingDirectory,
|
|
1449
|
+
cwd: getSubagentsDir(this.#opts.workingDirectory),
|
|
1445
1450
|
permissionMode: 'default',
|
|
1446
1451
|
systemPrompt: NAMED_AGENTS.tester.prompt,
|
|
1447
1452
|
allowedTools: ['Read', 'Glob', 'Grep', 'Bash', 'Write', 'Edit'],
|
|
@@ -1528,7 +1533,7 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
1528
1533
|
// an agents roster would allow delegation back to the writer, which would
|
|
1529
1534
|
// fire SubagentStop(agent_type==='writer') and re-arm the backstop.
|
|
1530
1535
|
const gateOptions = {
|
|
1531
|
-
cwd: this.#opts.workingDirectory,
|
|
1536
|
+
cwd: getSubagentsDir(this.#opts.workingDirectory),
|
|
1532
1537
|
permissionMode: 'default',
|
|
1533
1538
|
systemPrompt: NAMED_AGENTS.reasoner.prompt,
|
|
1534
1539
|
hooks: {
|
package/dist/config.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ import type { McpServerConfig } from './claude-handler.js';
|
|
|
2
2
|
export type VoiceMode = 'pipeline';
|
|
3
3
|
export type EditMode = 'read-only' | 'edit';
|
|
4
4
|
export type AgentMode = 'plan' | 'execute' | 'research';
|
|
5
|
-
export type STTProvider = 'deepgram' | 'groq-whisper' | 'openai-whisper';
|
|
6
|
-
export type TTSProvider = 'openai' | 'deepgram';
|
|
5
|
+
export type STTProvider = 'soniox' | 'deepgram' | 'groq-whisper' | 'openai-whisper';
|
|
6
|
+
export type TTSProvider = 'soniox' | 'openai' | 'deepgram';
|
|
7
7
|
export interface DirectConfig {
|
|
8
8
|
stt?: {
|
|
9
9
|
provider?: STTProvider;
|
|
@@ -146,7 +146,7 @@ export declare function invalidateSessionListCache(): void;
|
|
|
146
146
|
*
|
|
147
147
|
* @param limit - Max sessions to return (default 100, sorted by recency)
|
|
148
148
|
*/
|
|
149
|
-
export declare function listAllClaudeSessions(limit?: number): Promise<ClaudeSessionEntry[]>;
|
|
149
|
+
export declare function listAllClaudeSessions(limit?: number, includeSubagents?: boolean): Promise<ClaudeSessionEntry[]>;
|
|
150
150
|
/**
|
|
151
151
|
* Session summary for context briefing when switching sessions
|
|
152
152
|
*/
|
package/dist/config.js
CHANGED
|
@@ -47,13 +47,29 @@ const DEFAULT_CONFIG = {
|
|
|
47
47
|
voiceMode: 'pipeline',
|
|
48
48
|
direct: {
|
|
49
49
|
stt: {
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
// Soniox stt-rt-v4 — semantic endpointing (ML, not silence), word timestamps,
|
|
51
|
+
// custom vocabulary via context.terms. ~60% cheaper than nova-3 ($0.0017 vs $0.0043/min).
|
|
52
|
+
// Needs SONIOX_API_KEY.
|
|
53
|
+
provider: 'soniox',
|
|
54
|
+
model: 'stt-rt-v4',
|
|
55
|
+
// Previous: Deepgram nova-3, silence-based endpointing (550ms configured in voice-io.ts).
|
|
56
|
+
// Switch back: provider: 'deepgram', model: 'nova-3'
|
|
52
57
|
},
|
|
53
58
|
tts: {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
// Soniox tts-rt-v1 — real-time WebSocket streaming, speed control (0.7–1.3x),
|
|
60
|
+
// clean abort on interruption. Estimated ~$4–16/M chars vs OpenAI tts-1-hd $30/M.
|
|
61
|
+
// Pricing: $0.70/hr of generated speech (preview). Needs SONIOX_API_KEY.
|
|
62
|
+
provider: 'soniox',
|
|
63
|
+
model: 'tts-rt-v1',
|
|
64
|
+
voice: 'Maya',
|
|
65
|
+
// Previous: OpenAI tts-1-hd, voice fable — high quality, $30/M chars, ~500ms TTFB.
|
|
66
|
+
// Switch back: provider: 'openai', model: 'tts-1-hd', voice: 'fable'
|
|
67
|
+
// Other options already wired in voice-io.ts:
|
|
68
|
+
// Rime Mist v3: provider: 'rime', voice: 'cove' — 37ms TTFB, $30/M, WebSocket
|
|
69
|
+
// Fish Audio s2-pro: provider: 'fishaudio', voice: '<id>' — $15/M, voice cloning
|
|
70
|
+
// Groq Orpheus: provider: 'groq-orpheus', voice: 'autumn' — fast Groq chips, $22/M
|
|
71
|
+
// Deepgram Aura-2: provider: 'deepgram', model: 'aura-2-asteria-en' — $15/M, ~100ms TTFB
|
|
72
|
+
// OpenAI tts-1: provider: 'openai', model: 'tts-1', voice: 'fable' — $15/M
|
|
57
73
|
},
|
|
58
74
|
},
|
|
59
75
|
mcpServers: {
|
|
@@ -555,15 +571,23 @@ export function invalidateSessionListCache() {
|
|
|
555
571
|
*
|
|
556
572
|
* @param limit - Max sessions to return (default 100, sorted by recency)
|
|
557
573
|
*/
|
|
558
|
-
export async function listAllClaudeSessions(limit = 1000) {
|
|
574
|
+
export async function listAllClaudeSessions(limit = 1000, includeSubagents = false) {
|
|
559
575
|
if (_sessionListCache && Date.now() < _sessionListCache.expiresAt) {
|
|
560
|
-
|
|
576
|
+
const cached = includeSubagents
|
|
577
|
+
? _sessionListCache.data
|
|
578
|
+
: _sessionListCache.data.filter(s => !s.projectSlug.endsWith('-subagents'));
|
|
579
|
+
return cached.slice(0, limit);
|
|
561
580
|
}
|
|
562
581
|
const projectsDir = getClaudeProjectsDir();
|
|
563
582
|
if (!existsSync(projectsDir))
|
|
564
583
|
return [];
|
|
565
|
-
// 1. Discover all project folders
|
|
584
|
+
// 1. Discover all project folders — skip subagent slugs unless explicitly requested.
|
|
585
|
+
// Subagent sessions live in {workingDir}/subagents, which slugifies to a folder
|
|
586
|
+
// ending in "-subagents". Filtering here keeps the session limit budget for
|
|
587
|
+
// real user sessions and prevents subagent noise from crowding out other projects.
|
|
566
588
|
const projectFolders = readdirSync(projectsDir).filter(name => {
|
|
589
|
+
if (!includeSubagents && name.endsWith('-subagents'))
|
|
590
|
+
return false;
|
|
567
591
|
const fullPath = join(projectsDir, name);
|
|
568
592
|
try {
|
|
569
593
|
return statSync(fullPath).isDirectory();
|
package/dist/index.js
CHANGED
|
@@ -3603,9 +3603,19 @@ async function main() {
|
|
|
3603
3603
|
// discardAudioIfUninterruptible: true, ttsReadIdleTimeout: 10000,
|
|
3604
3604
|
// maxUnrecoverableErrors: 3) are what was silently running via caret-resolved
|
|
3605
3605
|
// 1.4.5 throughout the user's working month. Restoring them.
|
|
3606
|
-
|
|
3606
|
+
// STT endpointing is the default (nova-3, 25ms silence-based VAD — reliable, fast).
|
|
3607
|
+
// CloudTurnDetector (ML semantic EOT) is used ONLY if LIVEKIT_REMOTE_EOT_URL is set
|
|
3608
|
+
// AND the endpoint passes a startup probe (returns valid JSON probability).
|
|
3609
|
+
// If the probe fails (wrong URL, no auth, non-JSON response), STT is kept.
|
|
3610
|
+
let turnDetection = 'stt';
|
|
3611
|
+
if (process.env.LIVEKIT_REMOTE_EOT_URL) {
|
|
3612
|
+
const detector = new CloudTurnDetector();
|
|
3613
|
+
const eotLive = await detector.probe();
|
|
3614
|
+
if (eotLive)
|
|
3615
|
+
turnDetection = detector;
|
|
3616
|
+
}
|
|
3607
3617
|
const session = new voice.AgentSession({
|
|
3608
|
-
turnDetection
|
|
3618
|
+
turnDetection,
|
|
3609
3619
|
preemptiveGeneration: false, // Only fire LLM on final committed transcript, not partial preemptives
|
|
3610
3620
|
// Commented out — kept for reference. These were added across 0.9.60/0.9.61
|
|
3611
3621
|
// to try to harden interrupt + TTS handling, but evidence (osbornojure
|
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
* because it calls getJobContext(). This shim implements the same _TurnDetector
|
|
6
6
|
* interface but makes the remote HTTP call directly — no worker framework needed.
|
|
7
7
|
*
|
|
8
|
+
* Auth: LiveKit Cloud inference requires a signed JWT (same format as room access
|
|
9
|
+
* tokens). We cache the token and refresh it before expiry so we're not signing
|
|
10
|
+
* on every turn.
|
|
11
|
+
*
|
|
8
12
|
* On LiveKit Cloud (LIVEKIT_REMOTE_EOT_URL set): HTTP call to inference gateway.
|
|
9
13
|
* Without the URL: Returns 1.0 (always end of turn — STT endpointing handles it).
|
|
10
14
|
*/
|
|
11
15
|
import type { llm } from '@livekit/agents';
|
|
12
|
-
/**
|
|
13
|
-
* Implements _TurnDetector interface for LiveKit Cloud remote inference
|
|
14
|
-
* without requiring JobContext / worker framework.
|
|
15
|
-
*/
|
|
16
16
|
export declare class CloudTurnDetector {
|
|
17
17
|
#private;
|
|
18
18
|
readonly model = "lk_end_of_utterance_multilingual";
|
|
@@ -20,5 +20,11 @@ export declare class CloudTurnDetector {
|
|
|
20
20
|
constructor();
|
|
21
21
|
unlikelyThreshold(_language?: string): Promise<number | undefined>;
|
|
22
22
|
supportsLanguage(_language?: string): Promise<boolean>;
|
|
23
|
+
/**
|
|
24
|
+
* Startup probe — send a minimal request to confirm the endpoint returns
|
|
25
|
+
* valid JSON probability. Returns true if the EOT service is live and real.
|
|
26
|
+
* Called once at session init; if false, index.ts falls back to 'stt'.
|
|
27
|
+
*/
|
|
28
|
+
probe(): Promise<boolean>;
|
|
23
29
|
predictEndOfTurn(chatCtx: llm.ChatContext, _timeout?: number): Promise<number>;
|
|
24
30
|
}
|
|
@@ -5,19 +5,24 @@
|
|
|
5
5
|
* because it calls getJobContext(). This shim implements the same _TurnDetector
|
|
6
6
|
* interface but makes the remote HTTP call directly — no worker framework needed.
|
|
7
7
|
*
|
|
8
|
+
* Auth: LiveKit Cloud inference requires a signed JWT (same format as room access
|
|
9
|
+
* tokens). We cache the token and refresh it before expiry so we're not signing
|
|
10
|
+
* on every turn.
|
|
11
|
+
*
|
|
8
12
|
* On LiveKit Cloud (LIVEKIT_REMOTE_EOT_URL set): HTTP call to inference gateway.
|
|
9
13
|
* Without the URL: Returns 1.0 (always end of turn — STT endpointing handles it).
|
|
10
14
|
*/
|
|
11
15
|
import { log } from '@livekit/agents';
|
|
16
|
+
import { AccessToken } from 'livekit-server-sdk';
|
|
12
17
|
const REMOTE_INFERENCE_TIMEOUT = 2000;
|
|
13
18
|
const MAX_HISTORY_TURNS = 15;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* without requiring JobContext / worker framework.
|
|
17
|
-
*/
|
|
19
|
+
const TOKEN_TTL_SECONDS = 600; // 10-minute JWT
|
|
20
|
+
const TOKEN_REFRESH_BUFFER = 60; // refresh 60s before expiry
|
|
18
21
|
export class CloudTurnDetector {
|
|
19
22
|
#remoteUrl;
|
|
20
23
|
#logger = log();
|
|
24
|
+
#cachedToken;
|
|
25
|
+
#tokenExpiresAt = 0;
|
|
21
26
|
model = 'lk_end_of_utterance_multilingual';
|
|
22
27
|
provider = 'livekit';
|
|
23
28
|
constructor() {
|
|
@@ -30,18 +35,78 @@ export class CloudTurnDetector {
|
|
|
30
35
|
console.log(`🧠 Turn detector: LiveKit Cloud remote inference (${this.#remoteUrl})`);
|
|
31
36
|
}
|
|
32
37
|
else {
|
|
33
|
-
console.log('🧠 Turn detector: No LIVEKIT_REMOTE_EOT_URL — STT endpointing
|
|
38
|
+
console.log('🧠 Turn detector: No LIVEKIT_REMOTE_EOT_URL — STT endpointing only');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async #getAuthToken() {
|
|
42
|
+
const apiKey = process.env.LIVEKIT_API_KEY;
|
|
43
|
+
const apiSecret = process.env.LIVEKIT_API_SECRET;
|
|
44
|
+
if (!apiKey || !apiSecret)
|
|
45
|
+
return undefined;
|
|
46
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
47
|
+
if (this.#cachedToken && nowSec < this.#tokenExpiresAt - TOKEN_REFRESH_BUFFER) {
|
|
48
|
+
return this.#cachedToken;
|
|
34
49
|
}
|
|
50
|
+
// Sign a fresh token — identity is the agent, no room grants needed for inference
|
|
51
|
+
const at = new AccessToken(apiKey, apiSecret, {
|
|
52
|
+
identity: 'osborn-eot-agent',
|
|
53
|
+
ttl: `${TOKEN_TTL_SECONDS}s`,
|
|
54
|
+
});
|
|
55
|
+
this.#cachedToken = await at.toJwt();
|
|
56
|
+
this.#tokenExpiresAt = nowSec + TOKEN_TTL_SECONDS;
|
|
57
|
+
return this.#cachedToken;
|
|
35
58
|
}
|
|
36
59
|
async unlikelyThreshold(_language) {
|
|
37
|
-
return undefined;
|
|
60
|
+
return undefined;
|
|
38
61
|
}
|
|
39
62
|
async supportsLanguage(_language) {
|
|
40
|
-
return true;
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Startup probe — send a minimal request to confirm the endpoint returns
|
|
67
|
+
* valid JSON probability. Returns true if the EOT service is live and real.
|
|
68
|
+
* Called once at session init; if false, index.ts falls back to 'stt'.
|
|
69
|
+
*/
|
|
70
|
+
async probe() {
|
|
71
|
+
if (!this.#remoteUrl)
|
|
72
|
+
return false;
|
|
73
|
+
try {
|
|
74
|
+
const token = await this.#getAuthToken();
|
|
75
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
76
|
+
if (token)
|
|
77
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
78
|
+
const resp = await fetch(`${this.#remoteUrl}/eot/multi`, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
body: JSON.stringify({
|
|
81
|
+
messages: [{ role: 'user', content: 'test' }],
|
|
82
|
+
jobId: 'osborn-probe',
|
|
83
|
+
workerId: 'osborn-direct',
|
|
84
|
+
}),
|
|
85
|
+
headers,
|
|
86
|
+
signal: AbortSignal.timeout(3000),
|
|
87
|
+
});
|
|
88
|
+
if (!resp.ok)
|
|
89
|
+
return false;
|
|
90
|
+
const text = await resp.text();
|
|
91
|
+
try {
|
|
92
|
+
const data = JSON.parse(text);
|
|
93
|
+
const ok = typeof data.probability === 'number';
|
|
94
|
+
console.log(`🧠 EOT probe: ${ok ? '✅ live' : '❌ non-JSON ("' + text.slice(0, 30) + '")'} — ${ok ? 'using CloudTurnDetector' : 'falling back to STT'}`);
|
|
95
|
+
return ok;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
console.log(`🧠 EOT probe: ❌ non-JSON response ("${text.slice(0, 40)}") — falling back to STT endpointing`);
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
console.log(`🧠 EOT probe: ❌ unreachable — ${err instanceof Error ? err.message : err} — falling back to STT`);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
41
106
|
}
|
|
42
107
|
async predictEndOfTurn(chatCtx, _timeout) {
|
|
43
108
|
if (!this.#remoteUrl) {
|
|
44
|
-
return 1.0;
|
|
109
|
+
return 1.0;
|
|
45
110
|
}
|
|
46
111
|
try {
|
|
47
112
|
const messages = chatCtx
|
|
@@ -57,31 +122,41 @@ export class CloudTurnDetector {
|
|
|
57
122
|
excludeAudio: true,
|
|
58
123
|
excludeTimestamp: true,
|
|
59
124
|
}),
|
|
60
|
-
// Dummy IDs — LiveKit Cloud uses these for routing/logging, not auth
|
|
61
125
|
jobId: `osborn-${Date.now()}`,
|
|
62
126
|
workerId: 'osborn-direct',
|
|
63
127
|
};
|
|
64
128
|
const agentId = process.env.LIVEKIT_AGENT_ID;
|
|
65
|
-
if (agentId)
|
|
129
|
+
if (agentId)
|
|
66
130
|
request.agentId = agentId;
|
|
67
|
-
|
|
131
|
+
const token = await this.#getAuthToken();
|
|
132
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
133
|
+
if (token)
|
|
134
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
68
135
|
const resp = await fetch(`${this.#remoteUrl}/eot/multi`, {
|
|
69
136
|
method: 'POST',
|
|
70
137
|
body: JSON.stringify(request),
|
|
71
|
-
headers
|
|
138
|
+
headers,
|
|
72
139
|
signal: AbortSignal.timeout(REMOTE_INFERENCE_TIMEOUT),
|
|
73
140
|
});
|
|
74
141
|
if (!resp.ok) {
|
|
75
|
-
|
|
142
|
+
this.#logger.warn(`EOT inference returned ${resp.status} — falling back to STT`);
|
|
143
|
+
return 1.0;
|
|
76
144
|
}
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
145
|
+
const text = await resp.text();
|
|
146
|
+
try {
|
|
147
|
+
const data = JSON.parse(text);
|
|
148
|
+
if (typeof data.probability === 'number' && data.probability >= 0) {
|
|
149
|
+
return data.probability;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Non-JSON response (e.g. "OK") — log once then fall through
|
|
154
|
+
this.#logger.warn(`EOT inference returned non-JSON: "${text.slice(0, 40)}" — auth may be wrong`);
|
|
80
155
|
}
|
|
81
156
|
return 1.0;
|
|
82
157
|
}
|
|
83
158
|
catch {
|
|
84
|
-
return 1.0;
|
|
159
|
+
return 1.0;
|
|
85
160
|
}
|
|
86
161
|
}
|
|
87
162
|
}
|
package/dist/voice-io.d.ts
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
import * as deepgram from '@livekit/agents-plugin-deepgram';
|
|
6
6
|
import * as openai from '@livekit/agents-plugin-openai';
|
|
7
7
|
import * as silero from '@livekit/agents-plugin-silero';
|
|
8
|
+
import * as soniox from '@livekit/agents-plugin-soniox';
|
|
8
9
|
export interface STTConfig {
|
|
9
|
-
provider: 'deepgram' | 'deepgram-flux' | 'groq-whisper' | 'openai-whisper';
|
|
10
|
+
provider: 'soniox' | 'deepgram' | 'deepgram-flux' | 'groq-whisper' | 'openai-whisper';
|
|
10
11
|
model?: string;
|
|
11
12
|
language?: string;
|
|
12
13
|
/** Deepgram Flux: end-of-turn confidence threshold (0.0-1.0, default 0.7) */
|
|
@@ -15,7 +16,7 @@ export interface STTConfig {
|
|
|
15
16
|
eotTimeoutMs?: number;
|
|
16
17
|
}
|
|
17
18
|
export interface TTSConfig {
|
|
18
|
-
provider: 'openai' | 'deepgram' | 'groq-orpheus' | 'fishaudio' | 'rime';
|
|
19
|
+
provider: 'soniox' | 'openai' | 'deepgram' | 'groq-orpheus' | 'fishaudio' | 'rime';
|
|
19
20
|
voice?: string;
|
|
20
21
|
model?: string;
|
|
21
22
|
}
|
|
@@ -27,7 +28,7 @@ export interface VoiceIOConfig {
|
|
|
27
28
|
* Create STT (Speech-to-Text) instance based on config
|
|
28
29
|
* Note: Gemini STT is not available in Node.js, using Deepgram as default
|
|
29
30
|
*/
|
|
30
|
-
export declare function createSTT(config: STTConfig): deepgram.STT | deepgram.STTv2 | openai.STT;
|
|
31
|
+
export declare function createSTT(config: STTConfig): soniox.STT | deepgram.STT | deepgram.STTv2 | openai.STT;
|
|
31
32
|
/**
|
|
32
33
|
* Create TTS (Text-to-Speech) instance based on config
|
|
33
34
|
*/
|
package/dist/voice-io.js
CHANGED
|
@@ -7,13 +7,31 @@ import * as fishaudio from '@livekit/agents-plugin-fishaudio';
|
|
|
7
7
|
import * as openai from '@livekit/agents-plugin-openai';
|
|
8
8
|
import * as rime from '@livekit/agents-plugin-rime';
|
|
9
9
|
import * as silero from '@livekit/agents-plugin-silero';
|
|
10
|
+
import * as soniox from '@livekit/agents-plugin-soniox';
|
|
10
11
|
/**
|
|
11
12
|
* Create STT (Speech-to-Text) instance based on config
|
|
12
13
|
* Note: Gemini STT is not available in Node.js, using Deepgram as default
|
|
13
14
|
*/
|
|
14
15
|
export function createSTT(config) {
|
|
15
16
|
switch (config.provider) {
|
|
17
|
+
case 'soniox':
|
|
18
|
+
// Soniox stt-rt-v4 — semantic endpointing: ML model holds on incomplete thoughts,
|
|
19
|
+
// commits on natural sentence ends. maxEndpointDelayMs 500–3000ms (minimum = fastest).
|
|
20
|
+
// endpointLatencyAdjustmentLevel 0–3: higher = more aggressive latency reduction
|
|
21
|
+
// while keeping semantic smarts. context.terms biases recognition toward code vocab.
|
|
22
|
+
return new soniox.STT({
|
|
23
|
+
model: (config.model || 'stt-rt-v4'),
|
|
24
|
+
languageHints: config.language ? [config.language] : ['en'],
|
|
25
|
+
maxEndpointDelayMs: 1200, // give model room to decide on mid-thought pauses
|
|
26
|
+
endpointLatencyAdjustmentLevel: 2, // aggressive but not max — good for voice assistant
|
|
27
|
+
context: {
|
|
28
|
+
terms: ['Claude', 'TypeScript', 'LiveKit', 'Deepgram', 'npm', 'Railway', 'Fly.io'],
|
|
29
|
+
},
|
|
30
|
+
});
|
|
16
31
|
case 'deepgram':
|
|
32
|
+
// Previous default. Silence-based endpointing (550ms configured = wait for 550ms quiet).
|
|
33
|
+
// Fast and reliable, but commits on any pause — doesn't understand mid-thought hesitations.
|
|
34
|
+
// Switch back: provider: 'deepgram', model: 'nova-3'
|
|
17
35
|
return new deepgram.STT({
|
|
18
36
|
model: (config.model || 'nova-3'),
|
|
19
37
|
language: config.language || 'en',
|
|
@@ -49,13 +67,30 @@ export function createSTT(config) {
|
|
|
49
67
|
export function createTTS(config) {
|
|
50
68
|
let tts;
|
|
51
69
|
switch (config.provider) {
|
|
70
|
+
case 'soniox':
|
|
71
|
+
// Soniox tts-rt-v1 — real-time WebSocket streaming, clean abort on interruption.
|
|
72
|
+
// Estimated ~$4–16/M chars ($0.70/hr of generated speech, preview pricing).
|
|
73
|
+
// speed: 0.7–1.3x. voices: Maya (female), others at soniox.com/docs/tts/voices.
|
|
74
|
+
// Previous TTS: OpenAI tts-1-hd (fable) — $30/M chars, ~500ms TTFB, HTTP chunked.
|
|
75
|
+
// Switch back: provider: 'openai', model: 'tts-1-hd', voice: 'fable'
|
|
76
|
+
tts = new soniox.TTS({
|
|
77
|
+
model: (config.model || 'tts-rt-v1'),
|
|
78
|
+
voice: config.voice || 'Maya',
|
|
79
|
+
speed: 1.0,
|
|
80
|
+
});
|
|
81
|
+
break;
|
|
52
82
|
case 'openai':
|
|
83
|
+
// tts-1-hd: $30/M chars, ~500ms TTFB, 6 voices: alloy echo fable onyx nova shimmer.
|
|
84
|
+
// tts-1 (cheaper): $15/M chars, slightly lower quality, same voices.
|
|
53
85
|
tts = new openai.TTS({
|
|
54
86
|
voice: config.voice || 'alloy',
|
|
55
87
|
model: config.model || 'tts-1',
|
|
56
88
|
});
|
|
57
89
|
break;
|
|
58
90
|
case 'deepgram':
|
|
91
|
+
// Aura-2 voices: aura-2-asteria-en, aura-2-luna-en, aura-2-stella-en, aura-2-hera-en
|
|
92
|
+
// aura-2-orion-en, aura-2-arcas-en, aura-2-perseus-en, aura-2-angus-en, aura-2-orpheus-en
|
|
93
|
+
// ~$15/M chars, ~100ms TTFB.
|
|
59
94
|
tts = new deepgram.TTS({
|
|
60
95
|
model: (config.model || 'aura-2-asteria-en'),
|
|
61
96
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "osborn",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.192",
|
|
4
4
|
"description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"@livekit/agents-plugin-openai": "1.4.6",
|
|
40
40
|
"@livekit/agents-plugin-rime": "1.4.6",
|
|
41
41
|
"@livekit/agents-plugin-silero": "1.4.6",
|
|
42
|
+
"@livekit/agents-plugin-soniox": "^1.8.1",
|
|
42
43
|
"@livekit/rtc-node": "0.13.29",
|
|
43
44
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
44
45
|
"@smithery/api": "^0.48.0",
|