@fiodos/web-core 0.1.5 → 0.1.7
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/cjs/adapters/webVoiceAdapter.js +65 -19
- package/dist/cjs/controller/AgentController.d.ts +1 -0
- package/dist/cjs/controller/AgentController.js +21 -3
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/adapters/webVoiceAdapter.js +65 -19
- package/dist/esm/controller/AgentController.d.ts +1 -0
- package/dist/esm/controller/AgentController.js +22 -4
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +1 -1
|
@@ -401,20 +401,40 @@ function createWebVoiceAdapter(options = {}) {
|
|
|
401
401
|
// later programmatic playback (after the network round-trip) is allowed
|
|
402
402
|
// and audible even with the silent switch on.
|
|
403
403
|
const ctx = ensureAudioCtx();
|
|
404
|
-
if (
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
404
|
+
if (ctx) {
|
|
405
|
+
try {
|
|
406
|
+
if (ctx.state === 'suspended')
|
|
407
|
+
void ctx.resume();
|
|
408
|
+
// A 1-frame silent buffer fully unlocks playback on iOS Safari.
|
|
409
|
+
const buffer = ctx.createBuffer(1, 1, ctx.sampleRate);
|
|
410
|
+
const source = ctx.createBufferSource();
|
|
411
|
+
source.buffer = buffer;
|
|
412
|
+
source.connect(ctx.destination);
|
|
413
|
+
source.start(0);
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
/* best-effort unlock */
|
|
417
|
+
}
|
|
415
418
|
}
|
|
416
|
-
|
|
417
|
-
|
|
419
|
+
// Unlock device TTS (SpeechSynthesis) too. The spoken reply happens AFTER
|
|
420
|
+
// the network round-trip — outside this gesture — and Safari/Chrome stay
|
|
421
|
+
// SILENT for a first programmatic speak that wasn't preceded by a
|
|
422
|
+
// gesture-initiated one. Warm the engine here: kick off voice loading and
|
|
423
|
+
// speak a zero-volume blank utterance inside the tap so the real reply is
|
|
424
|
+
// allowed to play later.
|
|
425
|
+
if (!options.disableDeviceTtsFallback && typeof window !== 'undefined' && window.speechSynthesis) {
|
|
426
|
+
try {
|
|
427
|
+
const synth = window.speechSynthesis;
|
|
428
|
+
synth.getVoices(); // triggers async voice loading on Chrome
|
|
429
|
+
if (synth.paused)
|
|
430
|
+
synth.resume();
|
|
431
|
+
const warm = new SpeechSynthesisUtterance(' ');
|
|
432
|
+
warm.volume = 0;
|
|
433
|
+
synth.speak(warm);
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
/* best-effort unlock */
|
|
437
|
+
}
|
|
418
438
|
}
|
|
419
439
|
},
|
|
420
440
|
async playAudioBase64Mp3(base64) {
|
|
@@ -456,20 +476,32 @@ function createWebVoiceAdapter(options = {}) {
|
|
|
456
476
|
// Voices load lazily on Chrome — speaking before they exist is silent.
|
|
457
477
|
const voices = await loadVoicesOnce(synth);
|
|
458
478
|
return new Promise((resolve) => {
|
|
479
|
+
let keepAlive = null;
|
|
480
|
+
const cleanup = () => {
|
|
481
|
+
if (keepAlive) {
|
|
482
|
+
clearInterval(keepAlive);
|
|
483
|
+
keepAlive = null;
|
|
484
|
+
}
|
|
485
|
+
};
|
|
459
486
|
try {
|
|
460
|
-
// Clear any
|
|
461
|
-
//
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
synth.resume();
|
|
487
|
+
// Clear any previous/wedged queue. After a prior cancel() (e.g. the
|
|
488
|
+
// stopPlayback() fired when listening started) Chrome on macOS can sit
|
|
489
|
+
// in a stuck "paused" state that does NOT report paused===true, so a
|
|
490
|
+
// conditional resume() never fires and every later speak() is silent.
|
|
491
|
+
synth.cancel();
|
|
466
492
|
const utter = new SpeechSynthesisUtterance(text);
|
|
467
493
|
utter.lang = ttsOptions.locale;
|
|
468
494
|
const voice = pickVoiceForLocale(voices, ttsOptions.locale);
|
|
469
495
|
if (voice)
|
|
470
496
|
utter.voice = voice;
|
|
497
|
+
utter.volume = 1;
|
|
498
|
+
utter.rate = 1;
|
|
499
|
+
utter.pitch = 1;
|
|
500
|
+
// Keep a strong ref so Chrome can't GC the utterance mid-speech.
|
|
471
501
|
ttsUtterance = utter;
|
|
472
502
|
const finish = () => {
|
|
503
|
+
cleanup();
|
|
504
|
+
utter.onstart = null;
|
|
473
505
|
utter.onend = null;
|
|
474
506
|
utter.onerror = null;
|
|
475
507
|
if (ttsUtterance === utter)
|
|
@@ -479,8 +511,22 @@ function createWebVoiceAdapter(options = {}) {
|
|
|
479
511
|
utter.onend = finish;
|
|
480
512
|
utter.onerror = finish;
|
|
481
513
|
synth.speak(utter);
|
|
514
|
+
// THE FIX: resume() unconditionally right after queuing. This un-wedges
|
|
515
|
+
// the engine from the silent stuck-paused state described above.
|
|
516
|
+
synth.resume();
|
|
517
|
+
// Chrome desktop stops speaking after ~15s; pulse pause/resume to keep
|
|
518
|
+
// long replies going, and stop the timer once it has finished.
|
|
519
|
+
keepAlive = setInterval(() => {
|
|
520
|
+
if (!synth.speaking && !synth.pending) {
|
|
521
|
+
cleanup();
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
synth.pause();
|
|
525
|
+
synth.resume();
|
|
526
|
+
}, 9000);
|
|
482
527
|
}
|
|
483
528
|
catch {
|
|
529
|
+
cleanup();
|
|
484
530
|
resolve();
|
|
485
531
|
}
|
|
486
532
|
});
|
|
@@ -54,6 +54,9 @@ class AgentController {
|
|
|
54
54
|
// Safety cap so the orb can never get stuck in 'speaking' if playback/TTS
|
|
55
55
|
// never reports completion (e.g. a stalled SpeechSynthesis engine).
|
|
56
56
|
this.speakTimer = null;
|
|
57
|
+
// Once the backend shows it has no server-side TTS (a reply arrives with null
|
|
58
|
+
// audio), skip the extra /preview-tts round-trip and speak via device voice.
|
|
59
|
+
this.serverTtsUnavailable = false;
|
|
57
60
|
this.pendingConsentIntent = null;
|
|
58
61
|
this.listeners = new Set();
|
|
59
62
|
this.unsubscribeSpeech = null;
|
|
@@ -90,7 +93,11 @@ class AgentController {
|
|
|
90
93
|
});
|
|
91
94
|
this.screenStore = (0, screenContextStore_1.createScreenContextStore)({
|
|
92
95
|
getCurrentRoute: () => config.navigation.getCurrentRoute(),
|
|
93
|
-
|
|
96
|
+
// The integrator's fallback wins; otherwise the orb gets screen awareness
|
|
97
|
+
// out of the box from the matched manifest route (label + description +
|
|
98
|
+
// available actions). Null when no route matches → raw route, as today.
|
|
99
|
+
routeFallback: config.routeContextFallback ??
|
|
100
|
+
((route) => (0, core_1.defaultRouteContextText)(route, config.manifest, this.messages)),
|
|
94
101
|
});
|
|
95
102
|
this.speech = (0, speechSession_1.createSpeechSession)({
|
|
96
103
|
adapter: config.voice,
|
|
@@ -197,13 +204,20 @@ class AgentController {
|
|
|
197
204
|
this.setPhase('speaking');
|
|
198
205
|
try {
|
|
199
206
|
let audio = audioBase64;
|
|
200
|
-
if (
|
|
201
|
-
|
|
207
|
+
if (audio) {
|
|
208
|
+
this.serverTtsUnavailable = false; // server voice is working
|
|
209
|
+
}
|
|
210
|
+
// Once we know the backend ships no server TTS, skip the dead
|
|
211
|
+
// /preview-tts round-trip (pure latency) and use the device voice.
|
|
212
|
+
if (!audio && text && !this.serverTtsUnavailable) {
|
|
202
213
|
try {
|
|
203
214
|
audio = await this.config.backend.previewTts(this.getTtsVoice() ?? '', text);
|
|
215
|
+
if (!audio)
|
|
216
|
+
this.serverTtsUnavailable = true;
|
|
204
217
|
}
|
|
205
218
|
catch {
|
|
206
219
|
audio = null;
|
|
220
|
+
this.serverTtsUnavailable = true;
|
|
207
221
|
}
|
|
208
222
|
}
|
|
209
223
|
const playback = audio
|
|
@@ -418,6 +432,10 @@ class AgentController {
|
|
|
418
432
|
}, { signal: controller.signal });
|
|
419
433
|
reply = turn.reply;
|
|
420
434
|
audioBase64 = turn.audioBase64;
|
|
435
|
+
// Learn whether this backend ships server-side TTS: a non-empty reply
|
|
436
|
+
// with no audio means it doesn't — later lines then speak instantly.
|
|
437
|
+
if (reply)
|
|
438
|
+
this.serverTtsUnavailable = !audioBase64;
|
|
421
439
|
const decision = await (0, turnEngine_1.decideAction)({
|
|
422
440
|
action: turn.action,
|
|
423
441
|
manifest: this.config.manifest,
|
package/dist/cjs/version.d.ts
CHANGED
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
* Auto-generated by scripts/sync-sdk-version.mjs from package.json. Do not edit
|
|
6
6
|
* by hand — change package.json `version` and re-run the sync/release script.
|
|
7
7
|
*/
|
|
8
|
-
export declare const SDK_VERSION = "0.1.
|
|
8
|
+
export declare const SDK_VERSION = "0.1.7";
|
|
9
9
|
export declare const SDK_PACKAGE = "@fiodos/web-core";
|
package/dist/cjs/version.js
CHANGED
|
@@ -8,5 +8,5 @@ exports.SDK_PACKAGE = exports.SDK_VERSION = void 0;
|
|
|
8
8
|
* Auto-generated by scripts/sync-sdk-version.mjs from package.json. Do not edit
|
|
9
9
|
* by hand — change package.json `version` and re-run the sync/release script.
|
|
10
10
|
*/
|
|
11
|
-
exports.SDK_VERSION = '0.1.
|
|
11
|
+
exports.SDK_VERSION = '0.1.7';
|
|
12
12
|
exports.SDK_PACKAGE = '@fiodos/web-core';
|
|
@@ -398,20 +398,40 @@ export function createWebVoiceAdapter(options = {}) {
|
|
|
398
398
|
// later programmatic playback (after the network round-trip) is allowed
|
|
399
399
|
// and audible even with the silent switch on.
|
|
400
400
|
const ctx = ensureAudioCtx();
|
|
401
|
-
if (
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
401
|
+
if (ctx) {
|
|
402
|
+
try {
|
|
403
|
+
if (ctx.state === 'suspended')
|
|
404
|
+
void ctx.resume();
|
|
405
|
+
// A 1-frame silent buffer fully unlocks playback on iOS Safari.
|
|
406
|
+
const buffer = ctx.createBuffer(1, 1, ctx.sampleRate);
|
|
407
|
+
const source = ctx.createBufferSource();
|
|
408
|
+
source.buffer = buffer;
|
|
409
|
+
source.connect(ctx.destination);
|
|
410
|
+
source.start(0);
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
/* best-effort unlock */
|
|
414
|
+
}
|
|
412
415
|
}
|
|
413
|
-
|
|
414
|
-
|
|
416
|
+
// Unlock device TTS (SpeechSynthesis) too. The spoken reply happens AFTER
|
|
417
|
+
// the network round-trip — outside this gesture — and Safari/Chrome stay
|
|
418
|
+
// SILENT for a first programmatic speak that wasn't preceded by a
|
|
419
|
+
// gesture-initiated one. Warm the engine here: kick off voice loading and
|
|
420
|
+
// speak a zero-volume blank utterance inside the tap so the real reply is
|
|
421
|
+
// allowed to play later.
|
|
422
|
+
if (!options.disableDeviceTtsFallback && typeof window !== 'undefined' && window.speechSynthesis) {
|
|
423
|
+
try {
|
|
424
|
+
const synth = window.speechSynthesis;
|
|
425
|
+
synth.getVoices(); // triggers async voice loading on Chrome
|
|
426
|
+
if (synth.paused)
|
|
427
|
+
synth.resume();
|
|
428
|
+
const warm = new SpeechSynthesisUtterance(' ');
|
|
429
|
+
warm.volume = 0;
|
|
430
|
+
synth.speak(warm);
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
/* best-effort unlock */
|
|
434
|
+
}
|
|
415
435
|
}
|
|
416
436
|
},
|
|
417
437
|
async playAudioBase64Mp3(base64) {
|
|
@@ -453,20 +473,32 @@ export function createWebVoiceAdapter(options = {}) {
|
|
|
453
473
|
// Voices load lazily on Chrome — speaking before they exist is silent.
|
|
454
474
|
const voices = await loadVoicesOnce(synth);
|
|
455
475
|
return new Promise((resolve) => {
|
|
476
|
+
let keepAlive = null;
|
|
477
|
+
const cleanup = () => {
|
|
478
|
+
if (keepAlive) {
|
|
479
|
+
clearInterval(keepAlive);
|
|
480
|
+
keepAlive = null;
|
|
481
|
+
}
|
|
482
|
+
};
|
|
456
483
|
try {
|
|
457
|
-
// Clear any
|
|
458
|
-
//
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
synth.resume();
|
|
484
|
+
// Clear any previous/wedged queue. After a prior cancel() (e.g. the
|
|
485
|
+
// stopPlayback() fired when listening started) Chrome on macOS can sit
|
|
486
|
+
// in a stuck "paused" state that does NOT report paused===true, so a
|
|
487
|
+
// conditional resume() never fires and every later speak() is silent.
|
|
488
|
+
synth.cancel();
|
|
463
489
|
const utter = new SpeechSynthesisUtterance(text);
|
|
464
490
|
utter.lang = ttsOptions.locale;
|
|
465
491
|
const voice = pickVoiceForLocale(voices, ttsOptions.locale);
|
|
466
492
|
if (voice)
|
|
467
493
|
utter.voice = voice;
|
|
494
|
+
utter.volume = 1;
|
|
495
|
+
utter.rate = 1;
|
|
496
|
+
utter.pitch = 1;
|
|
497
|
+
// Keep a strong ref so Chrome can't GC the utterance mid-speech.
|
|
468
498
|
ttsUtterance = utter;
|
|
469
499
|
const finish = () => {
|
|
500
|
+
cleanup();
|
|
501
|
+
utter.onstart = null;
|
|
470
502
|
utter.onend = null;
|
|
471
503
|
utter.onerror = null;
|
|
472
504
|
if (ttsUtterance === utter)
|
|
@@ -476,8 +508,22 @@ export function createWebVoiceAdapter(options = {}) {
|
|
|
476
508
|
utter.onend = finish;
|
|
477
509
|
utter.onerror = finish;
|
|
478
510
|
synth.speak(utter);
|
|
511
|
+
// THE FIX: resume() unconditionally right after queuing. This un-wedges
|
|
512
|
+
// the engine from the silent stuck-paused state described above.
|
|
513
|
+
synth.resume();
|
|
514
|
+
// Chrome desktop stops speaking after ~15s; pulse pause/resume to keep
|
|
515
|
+
// long replies going, and stop the timer once it has finished.
|
|
516
|
+
keepAlive = setInterval(() => {
|
|
517
|
+
if (!synth.speaking && !synth.pending) {
|
|
518
|
+
cleanup();
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
synth.pause();
|
|
522
|
+
synth.resume();
|
|
523
|
+
}, 9000);
|
|
479
524
|
}
|
|
480
525
|
catch {
|
|
526
|
+
cleanup();
|
|
481
527
|
resolve();
|
|
482
528
|
}
|
|
483
529
|
});
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* deliberate modal tap; a loose "yes" never confirms them.
|
|
16
16
|
* - The agent only ever executes actions declared in the manifest.
|
|
17
17
|
*/
|
|
18
|
-
import { combineContexts, createActionExecutor, createConsentManager, createSafeTelemetry, createSanitizer, createConversationSession, formatMessage, prepareConversationForTurn, recordConversationExchange, resolveConfirmationLexicon, resolveMessages, validateManifest, } from '@fiodos/core';
|
|
18
|
+
import { combineContexts, createActionExecutor, createConsentManager, createSafeTelemetry, createSanitizer, createConversationSession, defaultRouteContextText, formatMessage, prepareConversationForTurn, recordConversationExchange, resolveConfirmationLexicon, resolveMessages, validateManifest, } from '@fiodos/core';
|
|
19
19
|
import { AgentApiError } from '../api/errors.js';
|
|
20
20
|
import { formatAgentTurnError } from '../api/formatTurnError.js';
|
|
21
21
|
import { buildManifestPayload } from '../api/backendClient.js';
|
|
@@ -51,6 +51,9 @@ export class AgentController {
|
|
|
51
51
|
// Safety cap so the orb can never get stuck in 'speaking' if playback/TTS
|
|
52
52
|
// never reports completion (e.g. a stalled SpeechSynthesis engine).
|
|
53
53
|
this.speakTimer = null;
|
|
54
|
+
// Once the backend shows it has no server-side TTS (a reply arrives with null
|
|
55
|
+
// audio), skip the extra /preview-tts round-trip and speak via device voice.
|
|
56
|
+
this.serverTtsUnavailable = false;
|
|
54
57
|
this.pendingConsentIntent = null;
|
|
55
58
|
this.listeners = new Set();
|
|
56
59
|
this.unsubscribeSpeech = null;
|
|
@@ -87,7 +90,11 @@ export class AgentController {
|
|
|
87
90
|
});
|
|
88
91
|
this.screenStore = createScreenContextStore({
|
|
89
92
|
getCurrentRoute: () => config.navigation.getCurrentRoute(),
|
|
90
|
-
|
|
93
|
+
// The integrator's fallback wins; otherwise the orb gets screen awareness
|
|
94
|
+
// out of the box from the matched manifest route (label + description +
|
|
95
|
+
// available actions). Null when no route matches → raw route, as today.
|
|
96
|
+
routeFallback: config.routeContextFallback ??
|
|
97
|
+
((route) => defaultRouteContextText(route, config.manifest, this.messages)),
|
|
91
98
|
});
|
|
92
99
|
this.speech = createSpeechSession({
|
|
93
100
|
adapter: config.voice,
|
|
@@ -194,13 +201,20 @@ export class AgentController {
|
|
|
194
201
|
this.setPhase('speaking');
|
|
195
202
|
try {
|
|
196
203
|
let audio = audioBase64;
|
|
197
|
-
if (
|
|
198
|
-
|
|
204
|
+
if (audio) {
|
|
205
|
+
this.serverTtsUnavailable = false; // server voice is working
|
|
206
|
+
}
|
|
207
|
+
// Once we know the backend ships no server TTS, skip the dead
|
|
208
|
+
// /preview-tts round-trip (pure latency) and use the device voice.
|
|
209
|
+
if (!audio && text && !this.serverTtsUnavailable) {
|
|
199
210
|
try {
|
|
200
211
|
audio = await this.config.backend.previewTts(this.getTtsVoice() ?? '', text);
|
|
212
|
+
if (!audio)
|
|
213
|
+
this.serverTtsUnavailable = true;
|
|
201
214
|
}
|
|
202
215
|
catch {
|
|
203
216
|
audio = null;
|
|
217
|
+
this.serverTtsUnavailable = true;
|
|
204
218
|
}
|
|
205
219
|
}
|
|
206
220
|
const playback = audio
|
|
@@ -415,6 +429,10 @@ export class AgentController {
|
|
|
415
429
|
}, { signal: controller.signal });
|
|
416
430
|
reply = turn.reply;
|
|
417
431
|
audioBase64 = turn.audioBase64;
|
|
432
|
+
// Learn whether this backend ships server-side TTS: a non-empty reply
|
|
433
|
+
// with no audio means it doesn't — later lines then speak instantly.
|
|
434
|
+
if (reply)
|
|
435
|
+
this.serverTtsUnavailable = !audioBase64;
|
|
418
436
|
const decision = await decideAction({
|
|
419
437
|
action: turn.action,
|
|
420
438
|
manifest: this.config.manifest,
|
package/dist/esm/version.d.ts
CHANGED
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
* Auto-generated by scripts/sync-sdk-version.mjs from package.json. Do not edit
|
|
6
6
|
* by hand — change package.json `version` and re-run the sync/release script.
|
|
7
7
|
*/
|
|
8
|
-
export declare const SDK_VERSION = "0.1.
|
|
8
|
+
export declare const SDK_VERSION = "0.1.7";
|
|
9
9
|
export declare const SDK_PACKAGE = "@fiodos/web-core";
|
package/dist/esm/version.js
CHANGED
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
* Auto-generated by scripts/sync-sdk-version.mjs from package.json. Do not edit
|
|
6
6
|
* by hand — change package.json `version` and re-run the sync/release script.
|
|
7
7
|
*/
|
|
8
|
-
export const SDK_VERSION = '0.1.
|
|
8
|
+
export const SDK_VERSION = '0.1.7';
|
|
9
9
|
export const SDK_PACKAGE = '@fiodos/web-core';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fiodos/web-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Framework-agnostic browser layer for the Fiodos agent: a vanilla AgentController (orchestrator), DOM adapters (navigation/voice/storage), HTTP client and decision engine over @fiodos/core. Shared base for @fiodos/vue, @fiodos/svelte and @fiodos/angular (no framework imports).",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"publishConfig": {
|