osborn 0.9.197 → 0.9.199

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.
@@ -31,6 +31,12 @@ const TOOL_CALL_BUDGET = 3;
31
31
  * Strip markdown formatting for TTS (text-to-speech)
32
32
  * Removes **bold**, ##headers, ```code```, etc. so TTS doesn't read them literally
33
33
  */
34
+ function extractSpeedMarker(text) {
35
+ const m = text.match(/^\[SPEED:(0\.[5-9]\d*|1\.[0-3]\d*)\]/);
36
+ if (!m)
37
+ return { text };
38
+ return { text: text.slice(m[0].length), speed: parseFloat(m[1]) };
39
+ }
34
40
  function stripMarkdownForTTS(text) {
35
41
  return text
36
42
  // Remove code blocks (``` ... ```)
@@ -677,6 +683,7 @@ export class ClaudeLLM extends llm.LLM {
677
683
  #currentTurnMessageId = null;
678
684
  #currentTurnChunkIndex = 0;
679
685
  #currentTurnChunks = [];
686
+ #currentTurnSpeed;
680
687
  #backgroundConsumerRunning = false;
681
688
  // Active queries — multiple can be running (SDK queues them internally).
682
689
  // We keep ALL references so interrupt() can stop whatever is currently executing.
@@ -1249,18 +1256,22 @@ export class ClaudeLLM extends llm.LLM {
1249
1256
  this.#currentTurnMessageId = crypto.randomUUID();
1250
1257
  this.#currentTurnChunkIndex = 0;
1251
1258
  this.#currentTurnChunks = [];
1259
+ this.#currentTurnSpeed = undefined;
1252
1260
  }
1253
1261
  const turnMessageId = this.#currentTurnMessageId;
1254
1262
  for (const block of msg.message.content) {
1255
1263
  if (block.type === 'text' && block.text) {
1256
1264
  const chunkIndex = this.#currentTurnChunkIndex;
1257
- callbacks.eventEmitter.emit('assistant_text', { text: block.text, messageId: turnMessageId, chunkIndex });
1258
- const ttsChunk = stripMarkdownForTTS(block.text);
1265
+ const { text: cleanText, speed } = extractSpeedMarker(block.text);
1266
+ if (speed !== undefined)
1267
+ this.#currentTurnSpeed = speed;
1268
+ callbacks.eventEmitter.emit('assistant_text', { text: cleanText, messageId: turnMessageId, chunkIndex });
1269
+ const ttsChunk = stripMarkdownForTTS(cleanText);
1259
1270
  if (ttsChunk.trim()) {
1260
1271
  this.#currentTurnChunks.push(ttsChunk);
1261
1272
  this.#currentTurnChunkIndex++;
1262
1273
  console.log(`🔊 TTS say (${ttsChunk.length} chars): "${ttsChunk}"`);
1263
- callbacks.eventEmitter.emit('tts_say', { text: ttsChunk, messageId: turnMessageId, chunkIndex });
1274
+ callbacks.eventEmitter.emit('tts_say', { text: ttsChunk, messageId: turnMessageId, chunkIndex, speed: this.#currentTurnSpeed });
1264
1275
  }
1265
1276
  }
1266
1277
  }
@@ -1279,6 +1290,7 @@ export class ClaudeLLM extends llm.LLM {
1279
1290
  this.#currentTurnMessageId = null;
1280
1291
  this.#currentTurnChunkIndex = 0;
1281
1292
  this.#currentTurnChunks = [];
1293
+ this.#currentTurnSpeed = undefined;
1282
1294
  console.log('✅ Claude turn complete (persistent session stays alive)');
1283
1295
  }
1284
1296
  }
@@ -2136,6 +2148,7 @@ class ClaudeLLMStream extends llm.LLMStream {
2136
2148
  let streamTurnMessageId = null;
2137
2149
  let streamTurnChunkIndex = 0;
2138
2150
  let streamTurnChunks = [];
2151
+ let streamTurnSpeed;
2139
2152
  // DIRECT MODE OPTIMIZATION: When skipTTSQueue is true, we run the Claude query
2140
2153
  // in the background and return from run() immediately. This is critical because:
2141
2154
  //
@@ -2227,11 +2240,16 @@ class ClaudeLLMStream extends llm.LLMStream {
2227
2240
  streamTurnMessageId = crypto.randomUUID();
2228
2241
  streamTurnChunkIndex = 0;
2229
2242
  streamTurnChunks = [];
2243
+ streamTurnSpeed = undefined;
2230
2244
  }
2231
2245
  for (const block of message.message.content) {
2232
2246
  if (block.type === 'text' && block.text) {
2233
2247
  hasOutput = true;
2234
- const rawText = block.text;
2248
+ // Strip [SPEED:X.X] marker before emitting to frontend or TTS
2249
+ const { text: cleanText, speed } = extractSpeedMarker(block.text);
2250
+ if (speed !== undefined)
2251
+ streamTurnSpeed = speed;
2252
+ const rawText = cleanText;
2235
2253
  const chunkIndex = streamTurnChunkIndex;
2236
2254
  // Emit RAW text to frontend (for chat bubbles with full formatting)
2237
2255
  this.#eventEmitter.emit('assistant_text', { text: rawText, messageId: streamTurnMessageId, chunkIndex });
@@ -2244,7 +2262,7 @@ class ClaudeLLMStream extends llm.LLMStream {
2244
2262
  // Direct mode: emit event for session.say() — bypasses LiveKit's
2245
2263
  // BufferedTokenStream which causes stuck/delayed/out-of-order audio
2246
2264
  console.log(`🔊 TTS say (${ttsChunk.length} chars): "${ttsChunk}"`);
2247
- this.#eventEmitter.emit('tts_say', { text: ttsChunk, messageId: streamTurnMessageId, chunkIndex });
2265
+ this.#eventEmitter.emit('tts_say', { text: ttsChunk, messageId: streamTurnMessageId, chunkIndex, speed: streamTurnSpeed });
2248
2266
  }
2249
2267
  else {
2250
2268
  // Realtime mode: use LLM stream queue (framework handles TTS)
package/dist/config.js CHANGED
@@ -75,18 +75,17 @@ const DEFAULT_CONFIG = {
75
75
  },
76
76
  tts: {
77
77
  // ── Active ─────────────────────────────────────────────────────────────
78
- provider: 'openai',
79
- model: 'tts-1-hd',
80
- voice: 'fable',
81
- // Soniox TTS requires @livekit/agents-plugin-soniox@1.8.1 which needs agents@1.8.1.
82
- // Pending full LiveKit 1.4→1.8 upgrade. Swap back once agents are upgraded.
83
- // provider: 'soniox', model: 'tts-rt-v1', voice: 'Victoria'
84
- // WebSocket streaming, clean abort, speed control, ~$4–16/M. Needs agents@1.8.1.
78
+ provider: 'soniox',
79
+ model: 'tts-rt-v1',
80
+ voice: 'Victoria',
81
+ // WebSocket streaming, clean abort, speed control 0.7–1.3x, ~$4–16/M chars.
82
+ // Victoria = en-GB female, refined. Also: Isla (en-GB, lively), Maya (en-US female).
85
83
  //
86
84
  // ── Alternatives ───────────────────────────────────────────────────────
85
+ // provider: 'openai', model: 'tts-1-hd', voice: 'fable'
86
+ // $30/M chars, ~500ms TTFB, HTTP streaming. 6 voices.
87
87
  // provider: 'openai', model: 'tts-1', voice: 'fable'
88
88
  // $15/M chars, slightly lower quality, same voices.
89
- // $15/M chars, slightly lower quality, same voices.
90
89
  // provider: 'deepgram', model: 'aura-2-asteria-en'
91
90
  // $15/M chars, ~100ms TTFB, WebSocket. Voices: asteria luna stella hera orion arcas perseus angus orpheus.
92
91
  // provider: 'rime', model: 'mistv3', voice: 'cove'
package/dist/index.js CHANGED
@@ -3441,6 +3441,13 @@ async function main() {
3441
3441
  }
3442
3442
  const sayId = Date.now(); // simple ID to correlate start/end logs
3443
3443
  console.log(`🗣️ [${sayId}] session.say START (${data.text.length} chars): "${data.text}"`);
3444
+ // Apply agent-requested speed (e.g. [SPEED:0.85] stripped from response text)
3445
+ if (data.speed !== undefined) {
3446
+ try {
3447
+ tts.updateOptions?.({ speed: data.speed });
3448
+ }
3449
+ catch { }
3450
+ }
3444
3451
  try {
3445
3452
  const handle = currentSession.say(data.text);
3446
3453
  if (handle && typeof handle.addDoneCallback === 'function') {
@@ -3647,9 +3654,9 @@ async function main() {
3647
3654
  // a full 3s window to keep talking before deciding it was false and
3648
3655
  // resuming. Other two knobs left at SDK defaults.
3649
3656
  interruption: {
3650
- minDuration: 1500, // default 500 — require 1.5s sustained speech (faster barge-in than 2500)
3651
- minWords: 2, // default 0 — require ≥2 transcript words
3652
- falseInterruptionTimeout: 3500, // default 2000 — 3.5s false-interrupt window (belt-and-suspenders since minDuration was loosened)
3657
+ minDuration: 800, // default 500 — require 800ms sustained speech (tightened from 1500; Soniox semantic STT reduces false positives)
3658
+ minWords: 1, // default 0 — require ≥1 word ("stop", "wait", "no" now count)
3659
+ falseInterruptionTimeout: 3500, // default 2000 — 3.5s false-interrupt window
3653
3660
  // resumeFalseInterruption: true, // default true (unchanged)
3654
3661
  // discardAudioIfUninterruptible: true,// default true (unchanged)
3655
3662
  },
@@ -4125,11 +4132,6 @@ async function main() {
4125
4132
  sendAgentTranscript(text, 'conv_item');
4126
4133
  }
4127
4134
  });
4128
- // FALLBACK: user_speech_committed
4129
- sess.on('user_speech_committed', (ev) => {
4130
- const transcript = ev.transcript || ev.text || '';
4131
- sendUserTranscript(transcript, 'committed');
4132
- });
4133
4135
  // Agent state tracking
4134
4136
  sess.on('agent_state_changed', (ev) => {
4135
4137
  agentState = ev.newState;
@@ -4,7 +4,6 @@
4
4
  */
5
5
  import * as deepgram from '@livekit/agents-plugin-deepgram';
6
6
  import * as openai from '@livekit/agents-plugin-openai';
7
- import * as silero from '@livekit/agents-plugin-silero';
8
7
  import * as soniox from '@livekit/agents-plugin-soniox';
9
8
  export interface STTConfig {
10
9
  provider: 'soniox' | 'deepgram' | 'deepgram-flux' | 'groq-whisper' | 'openai-whisper';
@@ -41,4 +40,4 @@ export declare function createTTS(config: TTSConfig): any;
41
40
  * - Split sentences when user pauses briefly mid-speech
42
41
  * - False triggers from ambient noise
43
42
  */
44
- export declare function createVAD(): Promise<silero.VAD>;
43
+ export declare function createVAD(): Promise<import("@livekit/agents").VAD>;
package/dist/voice-io.js CHANGED
@@ -23,7 +23,7 @@ export function createSTT(config) {
23
23
  model: (config.model || 'stt-rt-v4'),
24
24
  languageHints: config.language ? [config.language] : ['en'],
25
25
  maxEndpointDelayMs: 1200, // give model room to decide on mid-thought pauses
26
- endpointLatencyAdjustmentLevel: 2, // aggressive but not max — good for voice assistant
26
+ endpointLatencyAdjustmentLevel: 3, // max aggression — semantic model handles accuracy
27
27
  context: {
28
28
  terms: ['Claude', 'TypeScript', 'LiveKit', 'Deepgram', 'npm', 'Railway', 'Fly.io'],
29
29
  },
@@ -76,7 +76,7 @@ export function createTTS(config) {
76
76
  tts = new soniox.TTS({
77
77
  model: (config.model || 'tts-rt-v1'),
78
78
  voice: config.voice || 'Maya',
79
- speed: 1.0,
79
+ speed: 0.9,
80
80
  });
81
81
  break;
82
82
  case 'openai':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.197",
3
+ "version": "0.9.199",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,15 +32,15 @@
32
32
  "dependencies": {
33
33
  "@anthropic-ai/claude-agent-sdk": "^0.2.91",
34
34
  "@anthropic-ai/sdk": "^0.80.0",
35
- "@livekit/agents": "1.4.6",
36
- "@livekit/agents-plugin-deepgram": "1.4.6",
37
- "@livekit/agents-plugin-fishaudio": "1.4.6",
38
- "@livekit/agents-plugin-livekit": "1.4.6",
39
- "@livekit/agents-plugin-openai": "1.4.6",
40
- "@livekit/agents-plugin-rime": "1.4.6",
41
- "@livekit/agents-plugin-silero": "1.4.6",
42
- "@livekit/agents-plugin-soniox": "1.4.6",
43
- "@livekit/rtc-node": "0.13.29",
35
+ "@livekit/agents": "1.8.1",
36
+ "@livekit/agents-plugin-deepgram": "1.8.1",
37
+ "@livekit/agents-plugin-fishaudio": "1.8.1",
38
+ "@livekit/agents-plugin-livekit": "1.8.1",
39
+ "@livekit/agents-plugin-openai": "1.8.1",
40
+ "@livekit/agents-plugin-rime": "1.8.1",
41
+ "@livekit/agents-plugin-silero": "1.8.1",
42
+ "@livekit/agents-plugin-soniox": "1.8.1",
43
+ "@livekit/rtc-node": "0.13.35",
44
44
  "@modelcontextprotocol/sdk": "^1.29.0",
45
45
  "@smithery/api": "^0.48.0",
46
46
  "@types/diff": "^8.0.0",