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
|
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"42d3d75a56efe1a2e9902f52dc8006099c45d9
|
|
|
37
37
|
|
|
38
38
|
_flutter.loader.load({
|
|
39
39
|
serviceWorkerSettings: {
|
|
40
|
-
serviceWorkerVersion: "
|
|
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)
|
|
209
|
+
if (value?.length) {
|
|
210
|
+
chunks.push(Buffer.from(value));
|
|
211
|
+
}
|
|
202
212
|
}
|
|
203
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
||
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|