@backbay/glia-agent 0.1.0

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.
@@ -0,0 +1,743 @@
1
+ // src/audio/types.ts
2
+ var DEFAULT_TARGET_AVO = {
3
+ // Match the emotion system's "idle" anchor for a stable neutral baseline.
4
+ arousal: 0.25,
5
+ valence: 0.6,
6
+ openness: 0.35
7
+ };
8
+ function clamp01(value) {
9
+ return Math.max(0, Math.min(1, value));
10
+ }
11
+ function createTraceId() {
12
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
13
+ return crypto.randomUUID();
14
+ }
15
+ return `trace_${Date.now()}_${Math.random().toString(16).slice(2)}`;
16
+ }
17
+
18
+ // src/audio/schema.ts
19
+ import { z } from "zod";
20
+ var AudioProofVersionSchema = z.enum(["1.0"]);
21
+ var AudioFormatSchema = z.enum(["wav", "pcm_s16le", "opus", "mp3", "flac"]);
22
+ var VoiceLicenseCategorySchema = z.enum(["cc0", "cc-by", "cc-by-nc", "custom", "unknown"]);
23
+ var AvoSchema = z.object({
24
+ valence: z.number().min(0).max(1),
25
+ arousal: z.number().min(0).max(1),
26
+ openness: z.number().min(0).max(1)
27
+ });
28
+ var AudioArtifactSchema = z.object({
29
+ id: z.string().min(1),
30
+ uri: z.string().optional(),
31
+ format: AudioFormatSchema,
32
+ sha256: z.string().min(32),
33
+ sampleRateHz: z.number().int().min(8e3).optional(),
34
+ channels: z.number().int().min(1).max(8).optional(),
35
+ durationMs: z.number().int().min(1)
36
+ });
37
+ var AudioGateResultSchema = z.object({
38
+ passed: z.boolean(),
39
+ metrics: z.record(z.string(), z.unknown()).optional(),
40
+ reason: z.string().optional()
41
+ });
42
+ var AudioGatesSchema = z.object({
43
+ quality: AudioGateResultSchema,
44
+ semantic: AudioGateResultSchema,
45
+ affect: AudioGateResultSchema,
46
+ multimodalConsistency: AudioGateResultSchema.optional(),
47
+ watermark: AudioGateResultSchema.optional(),
48
+ speakerConsistency: AudioGateResultSchema.optional(),
49
+ antiSpoof: AudioGateResultSchema.optional(),
50
+ mos: AudioGateResultSchema.optional(),
51
+ safetyText: AudioGateResultSchema.optional(),
52
+ safetyAudio: AudioGateResultSchema.optional()
53
+ });
54
+ var EvidenceRefSchema = z.object({
55
+ type: z.enum(["run", "run_receipt", "artifact", "ui"]),
56
+ runId: z.string().optional(),
57
+ receiptHash: z.string().optional(),
58
+ path: z.string().optional(),
59
+ digest: z.string().optional(),
60
+ componentId: z.string().optional(),
61
+ note: z.string().optional()
62
+ });
63
+ var AudioProofSchema = z.object({
64
+ version: AudioProofVersionSchema,
65
+ createdAt: z.string().datetime(),
66
+ manifest: z.object({
67
+ traceId: z.string().optional(),
68
+ runId: z.string().optional(),
69
+ text: z.string().min(1),
70
+ language: z.string().optional(),
71
+ targetAffect: AvoSchema,
72
+ policy: z.object({
73
+ safetyMode: z.boolean(),
74
+ trustTier: z.string().optional(),
75
+ voiceCloningAllowed: z.boolean()
76
+ }),
77
+ cognitionSnapshot: z.record(z.string(), z.unknown()).optional()
78
+ }),
79
+ proof: z.object({
80
+ synthesis: z.object({
81
+ providerId: z.string().min(1),
82
+ model: z.object({
83
+ id: z.string().min(1),
84
+ revision: z.string().optional(),
85
+ sha256: z.string().optional()
86
+ }),
87
+ voice: z.object({
88
+ voiceId: z.string().min(1),
89
+ licenseCategory: VoiceLicenseCategorySchema,
90
+ licenseText: z.string().optional(),
91
+ source: z.string().optional()
92
+ }),
93
+ controls: z.record(z.string(), z.unknown()).optional(),
94
+ seed: z.number().int().min(0).optional()
95
+ }),
96
+ attempts: z.array(
97
+ z.object({
98
+ attempt: z.number().int().min(1),
99
+ artifactRef: z.string().min(1),
100
+ notes: z.string().optional(),
101
+ gates: AudioGatesSchema
102
+ })
103
+ ).optional(),
104
+ artifacts: z.array(AudioArtifactSchema).min(1),
105
+ gates: AudioGatesSchema,
106
+ evidence: z.array(EvidenceRefSchema).optional()
107
+ }),
108
+ verdict: z.object({
109
+ passed: z.boolean(),
110
+ reason: z.string().optional(),
111
+ score: z.number().optional()
112
+ })
113
+ });
114
+ function validateAudioProof(audioProof) {
115
+ const result = AudioProofSchema.safeParse(audioProof);
116
+ if (result.success) {
117
+ return { success: true, data: result.data };
118
+ }
119
+ return { success: false, errors: result.error };
120
+ }
121
+
122
+ // src/audio/planner.ts
123
+ function pickFirstAllowedVoiceId(voices, allowedVoiceIds) {
124
+ const list = voices.list();
125
+ for (const entry of list) {
126
+ if (!allowedVoiceIds || allowedVoiceIds.includes(entry.voiceId)) {
127
+ return entry.voiceId;
128
+ }
129
+ }
130
+ return null;
131
+ }
132
+ function pickVoiceId(args) {
133
+ const { voices, allowedVoiceIds, preferredVoiceId, requiredTag } = args;
134
+ if (preferredVoiceId) {
135
+ const entry = voices.get(preferredVoiceId);
136
+ if (entry && (!allowedVoiceIds || allowedVoiceIds.includes(entry.voiceId))) {
137
+ if (!requiredTag || (entry.tags ?? []).includes(requiredTag)) {
138
+ return entry.voiceId;
139
+ }
140
+ }
141
+ }
142
+ for (const entry of voices.list()) {
143
+ if (allowedVoiceIds && !allowedVoiceIds.includes(entry.voiceId)) continue;
144
+ if (requiredTag && !(entry.tags ?? []).includes(requiredTag)) continue;
145
+ return entry.voiceId;
146
+ }
147
+ return null;
148
+ }
149
+ function stripExcessPunctuation(text) {
150
+ return text.replace(/[!?]{2,}/g, (m) => m[0] ?? "!").replace(/\.{3,}/g, "\u2026");
151
+ }
152
+ function clampTextForSafetyMode(text) {
153
+ return stripExcessPunctuation(text).replace(/!/g, ".");
154
+ }
155
+ function planSpeech(input) {
156
+ const {
157
+ text,
158
+ language,
159
+ runId,
160
+ targetAffect,
161
+ policy,
162
+ voices,
163
+ signals,
164
+ defaults
165
+ } = input;
166
+ const personaDriftRisk = clamp01(signals?.personaDriftRisk ?? 0);
167
+ const confidence = clamp01(signals?.confidence ?? 0.8);
168
+ const risk = clamp01(signals?.risk ?? 1 - confidence);
169
+ const groundedVoiceTag = defaults?.groundedVoiceTag ?? "grounded";
170
+ const defaultVoiceTag = defaults?.defaultVoiceTag ?? "default";
171
+ const requireGrounded = policy.safetyMode || personaDriftRisk >= 0.6 || risk >= 0.7 && confidence <= 0.5;
172
+ const voiceId = (requireGrounded ? pickVoiceId({
173
+ voices,
174
+ allowedVoiceIds: policy.allowedVoiceIds,
175
+ preferredVoiceId: defaults?.voiceId,
176
+ requiredTag: groundedVoiceTag
177
+ }) : pickVoiceId({
178
+ voices,
179
+ allowedVoiceIds: policy.allowedVoiceIds,
180
+ preferredVoiceId: defaults?.voiceId,
181
+ requiredTag: defaultVoiceTag
182
+ })) ?? pickVoiceId({
183
+ voices,
184
+ allowedVoiceIds: policy.allowedVoiceIds,
185
+ preferredVoiceId: defaults?.voiceId
186
+ }) ?? pickFirstAllowedVoiceId(voices, policy.allowedVoiceIds);
187
+ if (!voiceId) {
188
+ throw new Error("No available voices (voice catalog is empty or policy blocks all voices)");
189
+ }
190
+ const baseTemperature = clamp01(defaults?.temperature ?? 0.65);
191
+ const temperature = requireGrounded ? Math.min(baseTemperature, 0.25) : baseTemperature;
192
+ const controls = {
193
+ temperature
194
+ };
195
+ const plannedText = policy.safetyMode || personaDriftRisk >= 0.7 ? clampTextForSafetyMode(text) : stripExcessPunctuation(text);
196
+ return {
197
+ traceId: createTraceId(),
198
+ runId,
199
+ text: plannedText,
200
+ language,
201
+ voiceId,
202
+ targetAffect: targetAffect ?? DEFAULT_TARGET_AVO,
203
+ controls,
204
+ policy: {
205
+ safetyMode: policy.safetyMode,
206
+ trustTier: policy.trustTier,
207
+ voiceCloningAllowed: policy.voiceCloningAllowed
208
+ }
209
+ };
210
+ }
211
+ function planSpeechFromCognition(input) {
212
+ const { cognition, ...rest } = input;
213
+ const signals = {
214
+ mode: cognition.mode,
215
+ personaDriftRisk: cognition.personaDriftRisk,
216
+ confidence: cognition.confidence,
217
+ risk: cognition.risk
218
+ };
219
+ const targetAffect = rest.targetAffect ?? cognition.emotionAVO;
220
+ return planSpeech({
221
+ ...rest,
222
+ targetAffect,
223
+ signals
224
+ });
225
+ }
226
+
227
+ // src/audio/overlay.ts
228
+ var DEFAULT_OVERLAY_PHRASES = {
229
+ ack: ["Got it.", "Okay.", "Understood."],
230
+ hold: ["One moment.", "Give me a second.", "Hang on."],
231
+ done: ["Done.", "All set.", "Finished."],
232
+ error: ["I hit an error.", "Something went wrong.", "I ran into a problem."],
233
+ warning: ["Careful.", "Heads up.", "Just a note."]
234
+ };
235
+ function pickOverlayPhrase(args) {
236
+ const { token, library, index = 0 } = args;
237
+ const phrases = library?.[token] ?? DEFAULT_OVERLAY_PHRASES[token];
238
+ if (!phrases || phrases.length === 0) {
239
+ return DEFAULT_OVERLAY_PHRASES[token][0];
240
+ }
241
+ return phrases[index % phrases.length] ?? phrases[0];
242
+ }
243
+
244
+ // src/audio/providers/httpSpeechSynthesisProvider.ts
245
+ var HttpSpeechSynthesisProvider = class {
246
+ providerId = "http";
247
+ baseUrl;
248
+ headers;
249
+ synthPath;
250
+ constructor(options) {
251
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
252
+ this.headers = options.headers ?? {};
253
+ this.synthPath = options.synthPath ?? "/v1/tts";
254
+ }
255
+ async synthesizeSpeech(request, options) {
256
+ const response = await fetch(`${this.baseUrl}${this.synthPath}`, {
257
+ method: "POST",
258
+ headers: {
259
+ "Content-Type": "application/json",
260
+ ...this.headers
261
+ },
262
+ body: JSON.stringify(request),
263
+ signal: options?.signal
264
+ });
265
+ if (!response.ok) {
266
+ const text = await response.text().catch(() => "");
267
+ throw new Error(`TTS request failed: HTTP ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`);
268
+ }
269
+ const contentType = response.headers.get("content-type") ?? "";
270
+ if (contentType.startsWith("audio/") || contentType === "application/octet-stream") {
271
+ throw new Error(
272
+ "TTS endpoint returned raw audio bytes; bb-ui requires a JSON response with artifact metadata for verification-first operation"
273
+ );
274
+ }
275
+ const payload = await response.json();
276
+ if (!payload?.artifact?.id || !payload?.artifact?.sha256 || !payload?.artifact?.durationMs) {
277
+ throw new Error("TTS endpoint response missing required `artifact` fields");
278
+ }
279
+ const audio = await this.resolveAudio(payload);
280
+ let proof = payload.proof;
281
+ if (proof) {
282
+ const parsed = validateAudioProof(proof);
283
+ if (!parsed.success) {
284
+ throw new Error("Invalid AudioProof from TTS endpoint");
285
+ }
286
+ proof = parsed.data;
287
+ }
288
+ return {
289
+ audio,
290
+ artifact: payload.artifact,
291
+ proof
292
+ };
293
+ }
294
+ async resolveAudio(payload) {
295
+ if (payload.audioBase64) {
296
+ const bytes = typeof atob !== "undefined" ? (() => {
297
+ const byteString = atob(payload.audioBase64 ?? "");
298
+ const arr = new Uint8Array(byteString.length);
299
+ for (let i = 0; i < byteString.length; i++) {
300
+ arr[i] = byteString.charCodeAt(i);
301
+ }
302
+ return arr;
303
+ })() : (() => {
304
+ const BufferCtor = globalThis.Buffer;
305
+ if (!BufferCtor) {
306
+ throw new Error("Base64 decoding is not supported in this environment");
307
+ }
308
+ return Uint8Array.from(BufferCtor.from(payload.audioBase64, "base64"));
309
+ })();
310
+ return new Blob([bytes], { type: "audio/wav" });
311
+ }
312
+ if (payload.audioUrl) {
313
+ const res = await fetch(payload.audioUrl);
314
+ if (!res.ok) {
315
+ throw new Error(`Failed to fetch audioUrl: HTTP ${res.status}`);
316
+ }
317
+ return await res.blob();
318
+ }
319
+ throw new Error("TTS endpoint response missing `audioBase64` or `audioUrl`");
320
+ }
321
+ };
322
+
323
+ // src/audio/hooks/useAudioPlayer.ts
324
+ import { useCallback, useEffect, useRef, useState } from "react";
325
+ function useAudioPlayer(options = {}) {
326
+ const { volume = 1, onEnded, onError } = options;
327
+ const audioRef = useRef(null);
328
+ const objectUrlRef = useRef(null);
329
+ const [isPlaying, setIsPlaying] = useState(false);
330
+ const [error, setError] = useState(null);
331
+ if (!audioRef.current && typeof Audio !== "undefined") {
332
+ audioRef.current = new Audio();
333
+ }
334
+ const stop = useCallback(() => {
335
+ const audio = audioRef.current;
336
+ if (!audio) return;
337
+ try {
338
+ audio.pause();
339
+ audio.currentTime = 0;
340
+ } catch {
341
+ } finally {
342
+ setIsPlaying(false);
343
+ }
344
+ }, []);
345
+ const play = useCallback(
346
+ async (source) => {
347
+ const audio = audioRef.current;
348
+ if (!audio) {
349
+ throw new Error("Audio playback not supported in this environment");
350
+ }
351
+ setError(null);
352
+ if (objectUrlRef.current) {
353
+ URL.revokeObjectURL(objectUrlRef.current);
354
+ objectUrlRef.current = null;
355
+ }
356
+ const src = typeof source === "string" ? source : URL.createObjectURL(source);
357
+ if (typeof source !== "string") {
358
+ objectUrlRef.current = src;
359
+ }
360
+ audio.volume = volume;
361
+ audio.src = src;
362
+ try {
363
+ await audio.play();
364
+ setIsPlaying(true);
365
+ } catch (err) {
366
+ const message = err instanceof Error ? err.message : "Failed to play audio";
367
+ setIsPlaying(false);
368
+ setError(message);
369
+ onError?.(err instanceof Error ? err : new Error(message));
370
+ throw err instanceof Error ? err : new Error(message);
371
+ }
372
+ },
373
+ [volume, onError]
374
+ );
375
+ useEffect(() => {
376
+ const audio = audioRef.current;
377
+ if (!audio) return;
378
+ const handleEnded = () => {
379
+ setIsPlaying(false);
380
+ onEnded?.();
381
+ };
382
+ const handleError = () => {
383
+ setIsPlaying(false);
384
+ const message = "Audio element error";
385
+ setError(message);
386
+ onError?.(new Error(message));
387
+ };
388
+ audio.addEventListener("ended", handleEnded);
389
+ audio.addEventListener("error", handleError);
390
+ return () => {
391
+ audio.removeEventListener("ended", handleEnded);
392
+ audio.removeEventListener("error", handleError);
393
+ };
394
+ }, [onEnded, onError]);
395
+ useEffect(() => {
396
+ return () => {
397
+ stop();
398
+ if (objectUrlRef.current) {
399
+ URL.revokeObjectURL(objectUrlRef.current);
400
+ objectUrlRef.current = null;
401
+ }
402
+ };
403
+ }, [stop]);
404
+ return {
405
+ isPlaying,
406
+ error,
407
+ play,
408
+ stop,
409
+ audioElement: audioRef.current
410
+ };
411
+ }
412
+
413
+ // src/audio/hooks/useBargeIn.ts
414
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
415
+ function useBargeIn(options) {
416
+ const { stream, enabled = true, threshold = 0.02, hangoverMs = 250, onBargeIn } = options;
417
+ const [isUserSpeaking, setIsUserSpeaking] = useState2(false);
418
+ const [levelRms, setLevelRms] = useState2(0);
419
+ const rafRef = useRef2(null);
420
+ const lastAboveRef = useRef2(0);
421
+ const prevSpeakingRef = useRef2(false);
422
+ useEffect2(() => {
423
+ if (!enabled || !stream) {
424
+ setIsUserSpeaking(false);
425
+ setLevelRms(0);
426
+ return;
427
+ }
428
+ if (typeof AudioContext === "undefined") {
429
+ return;
430
+ }
431
+ const audioContext = new AudioContext();
432
+ const source = audioContext.createMediaStreamSource(stream);
433
+ const analyser = audioContext.createAnalyser();
434
+ analyser.fftSize = 2048;
435
+ source.connect(analyser);
436
+ const buffer = new Float32Array(analyser.fftSize);
437
+ const tick = () => {
438
+ analyser.getFloatTimeDomainData(buffer);
439
+ let sum = 0;
440
+ for (let i = 0; i < buffer.length; i++) {
441
+ const x = buffer[i];
442
+ sum += x * x;
443
+ }
444
+ const rms = Math.sqrt(sum / buffer.length);
445
+ setLevelRms(rms);
446
+ const now = performance.now();
447
+ if (rms >= threshold) {
448
+ lastAboveRef.current = now;
449
+ }
450
+ const speaking = rms >= threshold || now - lastAboveRef.current <= hangoverMs;
451
+ setIsUserSpeaking(speaking);
452
+ if (!prevSpeakingRef.current && speaking) {
453
+ onBargeIn?.();
454
+ }
455
+ prevSpeakingRef.current = speaking;
456
+ rafRef.current = requestAnimationFrame(tick);
457
+ };
458
+ rafRef.current = requestAnimationFrame(tick);
459
+ return () => {
460
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
461
+ rafRef.current = null;
462
+ try {
463
+ source.disconnect();
464
+ analyser.disconnect();
465
+ } catch {
466
+ }
467
+ audioContext.close().catch(() => {
468
+ });
469
+ };
470
+ }, [stream, enabled, threshold, hangoverMs, onBargeIn]);
471
+ return { isUserSpeaking, levelRms };
472
+ }
473
+
474
+ // src/audio/hooks/useSpeechSynthesis.ts
475
+ import { useCallback as useCallback2, useEffect as useEffect3, useMemo, useRef as useRef3, useState as useState3 } from "react";
476
+ function useSpeechSynthesis(options) {
477
+ const {
478
+ provider,
479
+ verifier,
480
+ voices,
481
+ policy,
482
+ signals,
483
+ defaults,
484
+ bargeIn,
485
+ volume = 1,
486
+ verificationMode,
487
+ onProof,
488
+ onBargeIn,
489
+ onError
490
+ } = options;
491
+ const effectiveVerificationMode = useMemo(() => {
492
+ if (verificationMode) return verificationMode;
493
+ if (policy.requireProofBeforePlayback) return "before_playback";
494
+ if (verifier) return "after_playback";
495
+ return "never";
496
+ }, [verificationMode, policy.requireProofBeforePlayback, verifier]);
497
+ const abortRef = useRef3(null);
498
+ const [isSynthesizing, setIsSynthesizing] = useState3(false);
499
+ const [error, setError] = useState3(null);
500
+ const [lastRequest, setLastRequest] = useState3(null);
501
+ const [lastResult, setLastResult] = useState3(null);
502
+ const [lastProof, setLastProof] = useState3(null);
503
+ const player = useAudioPlayer({
504
+ volume,
505
+ onError: (err) => {
506
+ setError(err.message);
507
+ onError?.(err);
508
+ }
509
+ });
510
+ const bargeInState = useBargeIn({
511
+ stream: bargeIn?.stream ?? null,
512
+ enabled: !!bargeIn?.stream,
513
+ threshold: bargeIn?.threshold,
514
+ hangoverMs: bargeIn?.hangoverMs,
515
+ onBargeIn: () => {
516
+ onBargeIn?.();
517
+ }
518
+ });
519
+ const cancel = useCallback2(() => {
520
+ abortRef.current?.abort();
521
+ abortRef.current = null;
522
+ setIsSynthesizing(false);
523
+ player.stop();
524
+ }, [player]);
525
+ useEffect3(() => {
526
+ if (!bargeIn?.stream) return;
527
+ if (!bargeInState.isUserSpeaking) return;
528
+ cancel();
529
+ }, [bargeIn?.stream, bargeInState.isUserSpeaking, cancel]);
530
+ const speak = useCallback2(
531
+ async (text, speakOptions) => {
532
+ cancel();
533
+ setError(null);
534
+ setIsSynthesizing(true);
535
+ const controller = new AbortController();
536
+ abortRef.current = controller;
537
+ try {
538
+ const request = planSpeech({
539
+ text,
540
+ language: speakOptions?.language,
541
+ runId: speakOptions?.runId,
542
+ targetAffect: speakOptions?.targetAffect,
543
+ policy,
544
+ voices,
545
+ signals,
546
+ defaults
547
+ });
548
+ setLastRequest(request);
549
+ const result = await provider.synthesizeSpeech(request, { signal: controller.signal });
550
+ setLastResult(result);
551
+ let proof = result.proof;
552
+ if (proof) {
553
+ const parsed = validateAudioProof(proof);
554
+ if (!parsed.success) {
555
+ throw new Error("Provider returned invalid AudioProof");
556
+ }
557
+ proof = parsed.data;
558
+ }
559
+ const verifyNow = effectiveVerificationMode === "before_playback" && (!proof || !proof.verdict?.passed) && !!verifier;
560
+ if (verifyNow && verifier) {
561
+ proof = await verifier.verifySpeech({ request, result, policy });
562
+ const parsed = validateAudioProof(proof);
563
+ if (!parsed.success) {
564
+ throw new Error("Verifier returned invalid AudioProof");
565
+ }
566
+ proof = parsed.data;
567
+ }
568
+ if (policy.requireProofBeforePlayback && (!proof || proof.verdict.passed !== true)) {
569
+ throw new Error("Playback blocked by policy: missing or failing AudioProof");
570
+ }
571
+ await player.play(result.audio);
572
+ if (effectiveVerificationMode === "after_playback" && verifier && !proof) {
573
+ void (async () => {
574
+ try {
575
+ const p = await verifier.verifySpeech({ request, result, policy });
576
+ const parsed = validateAudioProof(p);
577
+ if (!parsed.success) return;
578
+ setLastProof(parsed.data);
579
+ onProof?.(parsed.data);
580
+ } catch {
581
+ }
582
+ })();
583
+ }
584
+ if (proof) {
585
+ setLastProof(proof);
586
+ onProof?.(proof);
587
+ }
588
+ } catch (err) {
589
+ const message = err instanceof Error ? err.message : "Speech synthesis failed";
590
+ setError(message);
591
+ onError?.(err instanceof Error ? err : new Error(message));
592
+ throw err instanceof Error ? err : new Error(message);
593
+ } finally {
594
+ setIsSynthesizing(false);
595
+ abortRef.current = null;
596
+ }
597
+ },
598
+ [
599
+ cancel,
600
+ policy,
601
+ voices,
602
+ signals,
603
+ defaults,
604
+ provider,
605
+ verifier,
606
+ effectiveVerificationMode,
607
+ player,
608
+ onProof,
609
+ onError
610
+ ]
611
+ );
612
+ return {
613
+ isSynthesizing,
614
+ isSpeaking: player.isPlaying,
615
+ error,
616
+ lastRequest,
617
+ lastResult,
618
+ lastProof,
619
+ speak,
620
+ cancel
621
+ };
622
+ }
623
+
624
+ // src/audio/hooks/useAudioOverlay.ts
625
+ import { useCallback as useCallback3, useMemo as useMemo2, useRef as useRef4 } from "react";
626
+ function useAudioOverlay(options) {
627
+ const {
628
+ provider,
629
+ verifier,
630
+ voices,
631
+ policy,
632
+ signals,
633
+ defaults,
634
+ phrases,
635
+ bargeIn,
636
+ volume,
637
+ verificationMode,
638
+ onProof,
639
+ onBargeIn,
640
+ onError
641
+ } = options;
642
+ const phraseIndexRef = useRef4(0);
643
+ const overlayDefaults = useMemo2(() => {
644
+ return {
645
+ ...defaults,
646
+ // Default to low temperature so overlay cues are stable and “assistant-like”.
647
+ temperature: defaults?.temperature ?? 0.2
648
+ };
649
+ }, [defaults]);
650
+ const speech = useSpeechSynthesis({
651
+ provider,
652
+ verifier,
653
+ voices,
654
+ policy,
655
+ signals,
656
+ defaults: overlayDefaults,
657
+ bargeIn,
658
+ volume,
659
+ verificationMode,
660
+ onProof,
661
+ onBargeIn,
662
+ onError
663
+ });
664
+ const speakToken = useCallback3(
665
+ async (token, overlaySpeakOptions) => {
666
+ const idx = phraseIndexRef.current++;
667
+ const text = pickOverlayPhrase({ token, library: phrases, index: idx });
668
+ await speech.speak(text, overlaySpeakOptions);
669
+ },
670
+ [speech, phrases]
671
+ );
672
+ const speakText = useCallback3(
673
+ async (text, overlaySpeakOptions) => {
674
+ await speech.speak(text, overlaySpeakOptions);
675
+ },
676
+ [speech]
677
+ );
678
+ return {
679
+ isSynthesizing: speech.isSynthesizing,
680
+ isSpeaking: speech.isSpeaking,
681
+ error: speech.error,
682
+ lastProof: speech.lastProof,
683
+ speakToken,
684
+ speakText,
685
+ cancel: speech.cancel
686
+ };
687
+ }
688
+
689
+ // src/audio/hooks/useHybridSpeech.ts
690
+ import { useCallback as useCallback4, useEffect as useEffect4 } from "react";
691
+ function useHybridSpeech(options) {
692
+ const { main: mainOptions, overlay: overlayOptions, stopOverlayOnMainSpeak = true } = options;
693
+ const main = useSpeechSynthesis(mainOptions);
694
+ const overlayEnabled = overlayOptions.enabled !== false;
695
+ const overlay = useAudioOverlay(overlayOptions);
696
+ useEffect4(() => {
697
+ if (!stopOverlayOnMainSpeak) return;
698
+ if (!overlayEnabled) return;
699
+ if (!main.isSpeaking) return;
700
+ overlay.cancel();
701
+ }, [stopOverlayOnMainSpeak, overlayEnabled, main.isSpeaking, overlay]);
702
+ const speakWithAck = useCallback4(
703
+ async (text, speakOptions) => {
704
+ if (overlayEnabled) {
705
+ void overlay.speakToken("ack", speakOptions).catch(() => {
706
+ });
707
+ }
708
+ try {
709
+ await main.speak(text, speakOptions);
710
+ } catch (err) {
711
+ if (overlayEnabled) overlay.cancel();
712
+ throw err;
713
+ }
714
+ },
715
+ [overlayEnabled, overlay, main]
716
+ );
717
+ const cancelAll = useCallback4(() => {
718
+ main.cancel();
719
+ if (overlayEnabled) overlay.cancel();
720
+ }, [main, overlayEnabled, overlay]);
721
+ return { main, overlay, speakWithAck, cancelAll };
722
+ }
723
+
724
+ export {
725
+ DEFAULT_TARGET_AVO,
726
+ clamp01,
727
+ createTraceId,
728
+ AudioFormatSchema,
729
+ VoiceLicenseCategorySchema,
730
+ AudioProofSchema,
731
+ validateAudioProof,
732
+ planSpeech,
733
+ planSpeechFromCognition,
734
+ DEFAULT_OVERLAY_PHRASES,
735
+ pickOverlayPhrase,
736
+ HttpSpeechSynthesisProvider,
737
+ useAudioPlayer,
738
+ useBargeIn,
739
+ useSpeechSynthesis,
740
+ useAudioOverlay,
741
+ useHybridSpeech
742
+ };
743
+ //# sourceMappingURL=chunk-XE2IVCKJ.js.map