neoagent 2.3.1-beta.42 → 2.3.1-beta.44

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.3.1-beta.42",
3
+ "version": "2.3.1-beta.44",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "MIT",
6
6
  "main": "server/index.js",
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"42d3d75a56efe1a2e9902f52dc8006099c45d9
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "380195857" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "705842576" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });
@@ -40,9 +40,17 @@ const EMOJI_SPEECH_REGEX =
40
40
  const WEARABLE_SAFE_AUDIO_FORMAT = Object.freeze({
41
41
  responseFormat: 'wav',
42
42
  mimeType: 'audio/wav',
43
+ streamResponseFormat: 'pcm',
44
+ streamMimeType: 'audio/wav',
45
+ pcmSampleRate: 24000,
46
+ pcmChannels: 1,
47
+ pcmBitsPerSample: 16,
43
48
  deepgramEncoding: 'linear16',
44
49
  deepgramContainer: 'wav',
50
+ deepgramStreamContainer: 'none',
45
51
  });
52
+ const MIN_STREAM_PCM_CHUNK_BYTES = 24000;
53
+ const MAX_STREAM_PCM_CHUNK_BYTES = 48000;
46
54
 
47
55
  function withTimeout(promise, timeoutMs, label) {
48
56
  const normalizedTimeout = Number(timeoutMs);
@@ -198,10 +206,11 @@ async function fetchAudioStreamOrThrow(url, init, errorPrefix, defaultMimeType =
198
206
  while (true) {
199
207
  const { done, value } = await reader.read();
200
208
  if (done) break;
201
- if (value?.length) chunks.push(Buffer.from(value));
209
+ if (value?.length) {
210
+ chunks.push(Buffer.from(value));
211
+ }
202
212
  }
203
- const audioBytes = Buffer.concat(chunks);
204
- await onChunk({ audioBytes, mimeType });
213
+ await onChunk({ audioBytes: Buffer.concat(chunks), mimeType });
205
214
  }
206
215
 
207
216
  function guessExtFromMimeType(mimeType) {
@@ -262,6 +271,49 @@ function wrapPcmAsWav(audioBytes, format) {
262
271
  return Buffer.concat([header, data]);
263
272
  }
264
273
 
274
+ async function streamPcmAsWavChunks(readable, format, onChunk) {
275
+ const source = readable && typeof readable.getReader === 'function'
276
+ ? readable
277
+ : null;
278
+ let pending = Buffer.alloc(0);
279
+
280
+ async function flushPending(force = false) {
281
+ while (pending.length >= MIN_STREAM_PCM_CHUNK_BYTES || (force && pending.length > 0)) {
282
+ const targetLength = force
283
+ ? pending.length
284
+ : Math.min(pending.length, MAX_STREAM_PCM_CHUNK_BYTES);
285
+ const evenLength = targetLength - (targetLength % 2);
286
+ if (evenLength <= 0) return;
287
+ const pcmChunk = pending.subarray(0, evenLength);
288
+ pending = pending.subarray(evenLength);
289
+ await onChunk({
290
+ audioBytes: wrapPcmAsWav(pcmChunk, format),
291
+ mimeType: WEARABLE_SAFE_AUDIO_FORMAT.streamMimeType,
292
+ });
293
+ }
294
+ }
295
+
296
+ if (source) {
297
+ const reader = source.getReader();
298
+ while (true) {
299
+ const { done, value } = await reader.read();
300
+ if (done) break;
301
+ if (value?.length) {
302
+ pending = Buffer.concat([pending, Buffer.from(value)]);
303
+ await flushPending(false);
304
+ }
305
+ }
306
+ } else {
307
+ for await (const chunk of readable) {
308
+ if (chunk?.length) {
309
+ pending = Buffer.concat([pending, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
310
+ await flushPending(false);
311
+ }
312
+ }
313
+ }
314
+ await flushPending(true);
315
+ }
316
+
265
317
  async function transcribeWithOpenAi(filePath, model, options = {}) {
266
318
  const client = getOpenAiClient({
267
319
  apiKey: typeof options.apiKey === 'string' ? options.apiKey.trim() : '',
@@ -380,8 +432,20 @@ async function streamWithOpenAi(text, model, voice, options = {}, onChunk) {
380
432
  model: String(model || 'gpt-4o-mini-tts').trim() || 'gpt-4o-mini-tts',
381
433
  voice: String(voice || 'alloy').trim() || 'alloy',
382
434
  input: text,
383
- response_format: useWearableSafeAudio ? WEARABLE_SAFE_AUDIO_FORMAT.responseFormat : 'mp3',
435
+ response_format: useWearableSafeAudio ? WEARABLE_SAFE_AUDIO_FORMAT.streamResponseFormat : 'mp3',
384
436
  });
437
+ if (useWearableSafeAudio) {
438
+ await streamPcmAsWavChunks(
439
+ response.body,
440
+ {
441
+ bitsPerSample: WEARABLE_SAFE_AUDIO_FORMAT.pcmBitsPerSample,
442
+ sampleRate: WEARABLE_SAFE_AUDIO_FORMAT.pcmSampleRate,
443
+ channels: WEARABLE_SAFE_AUDIO_FORMAT.pcmChannels,
444
+ },
445
+ onChunk,
446
+ );
447
+ return;
448
+ }
385
449
  const chunks = [];
386
450
  for await (const chunk of response.body) {
387
451
  chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
@@ -427,7 +491,32 @@ async function streamWithDeepgram(text, model, options = {}, onChunk) {
427
491
  });
428
492
  if (useWearableSafeAudio) {
429
493
  searchParams.set('encoding', WEARABLE_SAFE_AUDIO_FORMAT.deepgramEncoding);
430
- searchParams.set('container', WEARABLE_SAFE_AUDIO_FORMAT.deepgramContainer);
494
+ searchParams.set('container', WEARABLE_SAFE_AUDIO_FORMAT.deepgramStreamContainer);
495
+ searchParams.set('sample_rate', String(WEARABLE_SAFE_AUDIO_FORMAT.pcmSampleRate));
496
+ const response = await fetch(
497
+ `https://api.deepgram.com/v1/speak?${searchParams.toString()}`,
498
+ {
499
+ method: 'POST',
500
+ headers: {
501
+ Authorization: `Token ${apiKey}`,
502
+ 'Content-Type': 'application/json',
503
+ },
504
+ body: JSON.stringify({ text }),
505
+ },
506
+ );
507
+ if (!response.ok) {
508
+ await throwResponseError(response, 'Deepgram TTS stream failed');
509
+ }
510
+ await streamPcmAsWavChunks(
511
+ response.body,
512
+ {
513
+ bitsPerSample: WEARABLE_SAFE_AUDIO_FORMAT.pcmBitsPerSample,
514
+ sampleRate: WEARABLE_SAFE_AUDIO_FORMAT.pcmSampleRate,
515
+ channels: WEARABLE_SAFE_AUDIO_FORMAT.pcmChannels,
516
+ },
517
+ onChunk,
518
+ );
519
+ return;
431
520
  }
432
521
  await fetchAudioStreamOrThrow(
433
522
  `https://api.deepgram.com/v1/speak?${searchParams.toString()}`,
@@ -468,8 +468,6 @@ class VoiceRuntimeManager {
468
468
  provider: session.voiceSettings?.liveProvider,
469
469
  model: session.voiceSettings?.liveTtsModel,
470
470
  voice: session.voiceSettings?.liveVoice,
471
- transport: 'wearable',
472
- responseFormat: 'wav',
473
471
  });
474
472
  const spokenContent = sanitizeSpeechText(content);
475
473
 
@@ -481,6 +479,7 @@ class VoiceRuntimeManager {
481
479
  for (const attempt of ttsAttempts) {
482
480
  index = 0;
483
481
  streamError = null;
482
+ let attemptChunks = 0;
484
483
  try {
485
484
  await synthesizeVoiceReplyStream(
486
485
  spokenContent,
@@ -494,13 +493,21 @@ class VoiceRuntimeManager {
494
493
  audioBase64: audioBytes.toString('base64'),
495
494
  mimeType,
496
495
  });
496
+ attemptChunks += 1;
497
497
  index += 1;
498
498
  },
499
499
  );
500
+ if (attemptChunks === 0) {
501
+ throw new Error(`${attempt.provider} TTS produced no audio chunks.`);
502
+ }
500
503
  streamError = null;
501
504
  break;
502
505
  } catch (error) {
503
506
  streamError = String(error?.message || error || 'Voice playback failed.');
507
+ console.warn(`[VoiceRuntime] ${attempt.provider} TTS failed for flutter session ${sessionId}: ${streamError}`);
508
+ if (attemptChunks > 0) {
509
+ break;
510
+ }
504
511
  }
505
512
  }
506
513
  } catch (error) {
@@ -546,6 +553,8 @@ class VoiceRuntimeManager {
546
553
  provider: session.voiceSettings?.liveProvider,
547
554
  model: session.voiceSettings?.liveTtsModel,
548
555
  voice: session.voiceSettings?.liveVoice,
556
+ transport: 'wearable',
557
+ responseFormat: 'wav',
549
558
  });
550
559
  const spokenContent = sanitizeSpeechText(content);
551
560
  let index = 0;
@@ -557,6 +566,7 @@ class VoiceRuntimeManager {
557
566
  for (const attempt of ttsAttempts) {
558
567
  index = 0;
559
568
  streamError = null;
569
+ let attemptChunks = 0;
560
570
  try {
561
571
  await synthesizeVoiceReplyStream(
562
572
  spokenContent,
@@ -571,12 +581,20 @@ class VoiceRuntimeManager {
571
581
  audioBase64: audioBytes.toString('base64'),
572
582
  mimeType,
573
583
  }));
584
+ attemptChunks += 1;
574
585
  index += 1;
575
586
  },
576
587
  );
588
+ if (attemptChunks === 0) {
589
+ throw new Error(`${attempt.provider} TTS produced no audio chunks.`);
590
+ }
577
591
  break;
578
592
  } catch (error) {
579
593
  streamError = String(error?.message || error || 'Voice playback failed.');
594
+ console.warn(`[VoiceRuntime] ${attempt.provider} TTS failed for wearable session ${sessionId}: ${streamError}`);
595
+ if (attemptChunks > 0) {
596
+ break;
597
+ }
580
598
  }
581
599
  }
582
600
  } catch (error) {
@@ -618,6 +636,8 @@ class VoiceRuntimeManager {
618
636
  provider,
619
637
  model: provider === voiceOptions.provider ? voiceOptions.model : null,
620
638
  voice: provider === voiceOptions.provider ? voiceOptions.voice : null,
639
+ transport: voiceOptions.transport,
640
+ responseFormat: voiceOptions.responseFormat,
621
641
  });
622
642
  const runtime = provider === voiceOptions.provider
623
643
  ? {
@@ -1,10 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const fs = require('fs');
4
- const path = require('path');
5
-
6
- const { APP_DIR } = require('../../../runtime/paths');
7
-
3
+ const DEFAULT_GITHUB_REPOSITORY = 'NeoLabs-Systems/NeoAgent';
8
4
  const DEFAULT_ASSET_NAME = 'neoagent-wearable-firmware.bin';
9
5
  const MANIFEST_CACHE_TTL_MS = 5 * 60 * 1000;
10
6
  const manifestCache = new Map();
@@ -33,28 +29,10 @@ function parseRepositorySlug(value) {
33
29
  return null;
34
30
  }
35
31
 
36
- function readPackageRepositorySlug() {
37
- try {
38
- const packageJsonPath = path.join(APP_DIR, 'package.json');
39
- const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
40
- const repository = pkg?.repository;
41
- if (typeof repository === 'string') {
42
- return parseRepositorySlug(repository);
43
- }
44
- if (repository && typeof repository.url === 'string') {
45
- return parseRepositorySlug(repository.url);
46
- }
47
- } catch {
48
- return null;
49
- }
50
- return null;
51
- }
52
-
53
32
  function getGithubRepository() {
54
33
  return (
55
34
  parseRepositorySlug(process.env.NEOAGENT_WEARABLE_FIRMWARE_GITHUB_REPOSITORY)
56
- || parseRepositorySlug(process.env.GITHUB_REPOSITORY)
57
- || readPackageRepositorySlug()
35
+ || DEFAULT_GITHUB_REPOSITORY
58
36
  );
59
37
  }
60
38
 
@@ -86,10 +64,17 @@ function toBoolean(value, fallback = false) {
86
64
  return fallback;
87
65
  }
88
66
 
89
- function selectGithubRelease(releases, channel) {
67
+ function releaseHasAsset(release, assetName) {
68
+ return Boolean(selectReleaseAsset(release, assetName));
69
+ }
70
+
71
+ function selectGithubRelease(releases, channel, assetName = null) {
90
72
  const normalizedChannel = normalizeChannel(channel);
91
73
  if (normalizedChannel === 'stable') {
92
- return Array.isArray(releases) ? releases.find((release) => release && release.prerelease === false && release.draft === false) || null : null;
74
+ const candidates = Array.isArray(releases)
75
+ ? releases.filter((release) => release && release.prerelease === false && release.draft === false)
76
+ : [];
77
+ return candidates.find((release) => !assetName || releaseHasAsset(release, assetName)) || candidates[0] || null;
93
78
  }
94
79
 
95
80
  const betaPattern = /-beta(?:\.\d+)?$/i;
@@ -101,7 +86,7 @@ function selectGithubRelease(releases, channel) {
101
86
  const leftPublished = Date.parse(left?.published_at ?? left?.created_at ?? '') || 0;
102
87
  return rightPublished - leftPublished;
103
88
  });
104
- return candidates[0] || null;
89
+ return candidates.find((release) => !assetName || releaseHasAsset(release, assetName)) || candidates[0] || null;
105
90
  }
106
91
 
107
92
  function selectReleaseAsset(release, assetName) {
@@ -183,13 +168,10 @@ function parseChecksumBody(body, assetName) {
183
168
  return null;
184
169
  }
185
170
 
186
- async function fetchGithubRelease(fetchImpl, repository, channel, token) {
171
+ async function fetchGithubRelease(fetchImpl, repository, channel, token, assetName = null) {
187
172
  const normalizedChannel = normalizeChannel(channel);
188
- if (normalizedChannel === 'stable') {
189
- return fetchGithubJson(fetchImpl, `https://api.github.com/repos/${repository}/releases/latest`, token);
190
- }
191
173
  const releases = await fetchGithubJson(fetchImpl, `https://api.github.com/repos/${repository}/releases?per_page=100`, token);
192
- const release = selectGithubRelease(releases, normalizedChannel);
174
+ const release = selectGithubRelease(releases, normalizedChannel, assetName);
193
175
  if (!release) {
194
176
  const error = new Error(`No ${normalizedChannel} firmware release found for ${repository}`);
195
177
  error.status = 404;
@@ -282,7 +264,7 @@ async function resolveFirmwareManifest({
282
264
 
283
265
  try {
284
266
  const token = getGithubToken();
285
- const release = await fetchGithubRelease(fetchImpl, repository, normalizedChannel, token);
267
+ const release = await fetchGithubRelease(fetchImpl, repository, normalizedChannel, token, assetName);
286
268
  const asset = selectReleaseAsset(release, assetName);
287
269
  if (!asset || !asset.browser_download_url) {
288
270
  return {
@@ -359,6 +341,7 @@ async function resolveFirmwareManifest({
359
341
 
360
342
  module.exports = {
361
343
  DEFAULT_ASSET_NAME,
344
+ DEFAULT_GITHUB_REPOSITORY,
362
345
  getFirmwareAssetName,
363
346
  getGithubRepository,
364
347
  normalizeChannel,
@@ -139,11 +139,7 @@ class WearableService {
139
139
  const version = getVersionInfo();
140
140
  const manifest = await resolveFirmwareManifest({
141
141
  channel,
142
- downloadUrlOverride: toTrimmedString(process.env.NEOAGENT_WEARABLE_FIRMWARE_DOWNLOAD_URL, 2000),
143
- currentVersionOverride: toTrimmedString(process.env.NEOAGENT_WEARABLE_FIRMWARE_VERSION, 120),
144
- releaseNotesUrlOverride: toTrimmedString(process.env.NEOAGENT_WEARABLE_FIRMWARE_RELEASE_NOTES_URL, 2000),
145
- sha256Override: toTrimmedString(process.env.NEOAGENT_WEARABLE_FIRMWARE_SHA256, 256),
146
- repositoryOverride: toTrimmedString(process.env.NEOAGENT_WEARABLE_FIRMWARE_GITHUB_REPOSITORY || process.env.GITHUB_REPOSITORY, 256),
142
+ repositoryOverride: toTrimmedString(process.env.NEOAGENT_WEARABLE_FIRMWARE_GITHUB_REPOSITORY, 256),
147
143
  assetNameOverride: toTrimmedString(process.env.NEOAGENT_WEARABLE_FIRMWARE_ASSET_NAME, 128),
148
144
  });
149
145
  return {