@omnivoice/page-agent 0.4.0 → 0.6.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,533 @@
1
+ export declare interface AudioPlayback {
2
+ push(pcm16: Int16Array): void;
3
+ mark(mark: PlaybackMark): void;
4
+ clear(): void;
5
+ close(): Promise<void>;
6
+ }
7
+
8
+ export declare const browserSocket: SocketFactory;
9
+
10
+ export declare type Capability = typeof TEXT_INPUT | typeof MUTE | typeof REPLAY | string;
11
+
12
+ export declare const CAPTURE_BLOCK = 512;
13
+
14
+ /**
15
+ * Áudio do navegador para o modo realtime: captura do microfone e reprodução da resposta.
16
+ *
17
+ * Nada aqui roda ao importar o módulo. O microfone só é pedido quando `startBrowserMicrophone`
18
+ * é chamado, e isso só acontece dentro de `RealtimeEngine.enter()`.
19
+ *
20
+ * Formato do protocolo do servidor de áudio:
21
+ * - captura: blocos de 512 amostras int16 a 16 kHz (32 ms), contados a partir de 0;
22
+ * - reprodução: PCM int16 na taxa anunciada pelo servidor (`ready.rate`).
23
+ */
24
+ export declare const CAPTURE_RATE = 16000;
25
+
26
+ export declare function createBrowserVoice(language: string, listenTimeoutMs?: number): LocalVoice;
27
+
28
+ export declare function createEngine(settings: VoiceSettings, deps?: EngineDeps): VoiceEngine;
29
+
30
+ /**
31
+ * Cria a sessão e abre a engine escolhida por `settings.speechMode`.
32
+ * É aqui — e só aqui — que o microfone/conexão são pedidos; importar o módulo ou criar `VoiceSession` não os liga.
33
+ */
34
+ export declare function createVoiceSession(settings: VoiceSettings, deps?: EngineDeps): Promise<VoiceSession>;
35
+
36
+ export declare function createVoiceSettings(overrides?: Partial<VoiceSettings>): VoiceSettings;
37
+
38
+ /** Tipo de efeito observável após ação. */
39
+ export declare type EffectKind = "notification" | "navigation" | "loading";
40
+
41
+ /** Resultado da observação de efeito. */
42
+ export declare type EffectOutcome = {
43
+ status: "observed";
44
+ kind: EffectKind;
45
+ detail: string;
46
+ } | {
47
+ status: "unknown";
48
+ detail: string;
49
+ };
50
+
51
+ export declare interface EngineDeps {
52
+ realtime?: RealtimeDeps;
53
+ local?: LocalDeps;
54
+ }
55
+
56
+ /**
57
+ * Distribuição de eventos para vários consumidores. Cada `subscribe()` recebe a sua própria fila,
58
+ * então um consumidor lento não perde eventos nem rouba os dos outros. Sem dependência de Node.
59
+ */
60
+ export declare class EventHub<T> {
61
+ private readonly subscribers;
62
+ private closed;
63
+ get isClosed(): boolean;
64
+ emit(event: T): void;
65
+ /** Encerra todos os iteradores: quem espera recebe `done`, quem tem fila a consome antes. */
66
+ close(): void;
67
+ subscribe(): AsyncIterable<T>;
68
+ }
69
+
70
+ /** Canal ativo de entrada: voz (microfone) ou chat (texto). */
71
+ export declare type InputChannel = "voice" | "chat";
72
+
73
+ /** Junta a origem do servidor (que pode ter um prefixo, ex.: `/t/abc`) com um caminho relativo a ela. */
74
+ export declare function joinServerUrl(base: string, endpoint: string): URL;
75
+
76
+ export declare interface LocalDeps {
77
+ voice?: LocalVoice;
78
+ }
79
+
80
+ export declare class LocalEngine implements VoiceEngine {
81
+ readonly capabilities: ReadonlySet<Capability>;
82
+ private readonly settings;
83
+ private readonly deps;
84
+ private readonly hub;
85
+ private voice;
86
+ private muted;
87
+ private running;
88
+ private turn;
89
+ private lastError;
90
+ private unmute;
91
+ constructor(settings: VoiceSettings, deps?: LocalDeps);
92
+ enter(): Promise<void>;
93
+ exit(): Promise<void>;
94
+ snapshot(): VoiceSnapshot;
95
+ setMuted(muted: boolean): void;
96
+ submit(text: string): void;
97
+ /** Fala um texto com a síntese do navegador (a resposta do agente, por exemplo). */
98
+ speak(text: string): void;
99
+ events(): AsyncIterable<VoiceEvent>;
100
+ private waitUnmuted;
101
+ private listenLoop;
102
+ }
103
+
104
+ /**
105
+ * Voz do próprio navegador (Web Speech API): reconhecimento e síntese locais, sem servidor de áudio.
106
+ * Só toca no microfone quando `listen()` é chamado.
107
+ */
108
+ export declare interface LocalVoice {
109
+ /** O navegador oferece reconhecimento de fala. */
110
+ readonly supportsInput: boolean;
111
+ /** Escuta uma fala e resolve com a transcrição final; resolve `""` quando ninguém falou. */
112
+ listen(): Promise<string>;
113
+ stopListening(): void;
114
+ speak(text: string): void;
115
+ stopSpeaking(): void;
116
+ }
117
+
118
+ /** Uma mensagem no histórico de conversa. */
119
+ export declare type Message = {
120
+ role: "user" | "assistant";
121
+ content: string;
122
+ timestamp: number;
123
+ channel: InputChannel;
124
+ confidence?: number;
125
+ };
126
+
127
+ export declare interface MicrophoneCapture {
128
+ stop(): Promise<void>;
129
+ }
130
+
131
+ /** Abre o microfone e entrega blocos até `stop()`. */
132
+ export declare type MicrophoneFactory = (onFrame: (frame: MicrophoneFrame) => void) => Promise<MicrophoneCapture>;
133
+
134
+ export declare interface MicrophoneFrame {
135
+ readonly block: number;
136
+ readonly pcm: Int16Array;
137
+ /** Instante (ms) no relógio do AudioContext em que o bloco ficou completo. `performance` não existe no escopo do AudioWorklet. */
138
+ readonly at: number;
139
+ }
140
+
141
+ export declare const MUTE = "mute";
142
+
143
+ /** Aceita "1"/"local" e "2"/"realtime" (sem diferenciar maiúsculas); qualquer outra coisa vira `local`. */
144
+ export declare function parseSpeechMode(choice: string): SpeechMode;
145
+
146
+ export declare type PlaybackFactory = (rate: number, onReport: (report: PlaybackReport) => void) => Promise<AudioPlayback>;
147
+
148
+ export declare interface PlaybackMark {
149
+ readonly id: number;
150
+ readonly label: string;
151
+ readonly offset: number;
152
+ }
153
+
154
+ /** Relato do reprodutor ao servidor: `reached`, `rendered` ou `underrun` (a forma que o servidor lê). */
155
+ export declare type PlaybackReport = Readonly<Record<string, unknown>> & {
156
+ readonly type: string;
157
+ };
158
+
159
+ /**
160
+ * Lê a configuração de um objeto de ambiente. O argumento é obrigatório de propósito: o módulo roda
161
+ * no navegador, onde não existe `process`. Em Node passe `process.env`; no Vite, `import.meta.env`.
162
+ */
163
+ export declare function readVoiceSettings(env: VoiceEnv): VoiceSettings;
164
+
165
+ /** Pontos de substituição (testes, ambientes sem DOM). O padrão usa as APIs do navegador. */
166
+ export declare interface RealtimeDeps {
167
+ fetch?: typeof fetch;
168
+ socket?: SocketFactory;
169
+ microphone?: MicrophoneFactory;
170
+ playback?: PlaybackFactory;
171
+ }
172
+
173
+ export declare class RealtimeEngine implements VoiceEngine {
174
+ readonly capabilities: ReadonlySet<Capability>;
175
+ private readonly settings;
176
+ private readonly deps;
177
+ private readonly hub;
178
+ private socket;
179
+ private playback;
180
+ private playbackRate;
181
+ private microphone;
182
+ private microphoneStarting;
183
+ private muted;
184
+ private opened;
185
+ private ready;
186
+ private closing;
187
+ private turn;
188
+ private proxySessionId;
189
+ private lastError;
190
+ private settle;
191
+ constructor(settings: VoiceSettings, deps?: RealtimeDeps);
192
+ enter(): Promise<void>;
193
+ exit(): Promise<void>;
194
+ snapshot(): VoiceSnapshot;
195
+ setMuted(muted: boolean): void;
196
+ submit(_text: string): void;
197
+ events(): AsyncIterable<VoiceEvent>;
198
+ private lease;
199
+ private onMessage;
200
+ private onState;
201
+ private startAudio;
202
+ private startPlayback;
203
+ private ensureMicrophone;
204
+ private stopMicrophone;
205
+ private sendFrame;
206
+ private sendPlaybackReport;
207
+ private sendJson;
208
+ private onClose;
209
+ /** Falha durante a abertura rejeita `enter()`; depois dela vira evento de erro. */
210
+ private fail;
211
+ private report;
212
+ private emit;
213
+ private trace;
214
+ private teardown;
215
+ }
216
+
217
+ export declare const REPLAY = "replay";
218
+
219
+ export declare type SocketFactory = (url: string, handlers: SocketHandlers) => SocketLike;
220
+
221
+ export declare interface SocketHandlers {
222
+ onOpen(): void;
223
+ onMessage(data: string | ArrayBuffer): void;
224
+ onError(): void;
225
+ onClose(code: number, reason: string): void;
226
+ }
227
+
228
+ export declare interface SocketLike {
229
+ readonly isOpen: boolean;
230
+ send(data: string | Uint8Array): void;
231
+ close(code?: number, reason?: string): void;
232
+ }
233
+
234
+ /**
235
+ * Types compartilhados para a integração de voz.
236
+ * Baseado no pattern Conversation + Engine de NoStopPageTest.
237
+ */
238
+ /** Modo de fala: local (modelos no device) ou realtime (OpenAI full-duplex). */
239
+ export declare type SpeechMode = "local" | "realtime";
240
+
241
+ /** Microfone real: `getUserMedia` + AudioWorklet a 16 kHz. Só pede permissão quando chamado. */
242
+ export declare const startBrowserMicrophone: MicrophoneFactory;
243
+
244
+ /** Reprodutor real: AudioWorklet na taxa pedida; devolve ao servidor onde a reprodução está. */
245
+ export declare const startBrowserPlayback: PlaybackFactory;
246
+
247
+ /** Capacidades oferecidas pela engine de voz. */
248
+ export declare const TEXT_INPUT = "text_input";
249
+
250
+ /** Estado de um turno: agente escutando, processando, falando. */
251
+ export declare type TurnState = "idle" | "listening" | "processing" | "speaking";
252
+
253
+ /**
254
+ * Entrada de um usuário: voz transcrita ou texto digitado.
255
+ * Ambos são normalizados para "utterance" (enunciado).
256
+ */
257
+ export declare type UserInput = {
258
+ utterance: string;
259
+ channel: InputChannel;
260
+ confidence?: number;
261
+ timestamp: number;
262
+ };
263
+
264
+ /** Mensagem de erro se a configuração não serve para o modo escolhido; `undefined` se está ok. */
265
+ export declare function validateSettings(settings: VoiceSettings): string | undefined;
266
+
267
+ export declare const VOICE_DEFAULTS: Omit<VoiceSettings, "sessionId">;
268
+
269
+ /**
270
+ * Ponte entre a sessão e quem a consome.
271
+ *
272
+ * - Texto digitado entra por `onUserText`, que só o enfileira na sessão.
273
+ * - Os eventos públicos da sessão entram por `onVoiceEvent`, que REGISTRA o que a sessão aceitou
274
+ * (`turn.heard`) e o que o agente respondeu (`agent.finished`). Ele nunca reenfileira: a sessão
275
+ * já absorveu a fala do motor, e reenfileirar o `turn.heard` público criaria um ciclo infinito.
276
+ *
277
+ * Exemplo de uso:
278
+ * const bridge = new VoiceBridge(voiceSession);
279
+ * chatInput.onSubmit((text) => bridge.onUserText(text));
280
+ * for await (const ev of voiceSession.events()) bridge.onVoiceEvent(ev);
281
+ */
282
+ export declare class VoiceBridge {
283
+ private session;
284
+ private messageBuffer;
285
+ constructor(session: VoiceSessionReader);
286
+ /**
287
+ * Entrada de texto do usuário (chat): vira UserInput e é enfileirada na sessão.
288
+ * O registro no histórico da ponte acontece quando a sessão publicar o `turn.heard` correspondente.
289
+ */
290
+ onUserText(text: string): void;
291
+ /**
292
+ * Evento público da sessão. `turn.heard` e `agent.finished` entram no histórico da ponte;
293
+ * nenhum evento aqui cria entrada nova na sessão.
294
+ */
295
+ onVoiceEvent(event: VoiceEvent): void;
296
+ /**
297
+ * Contexto atual: tudo o que o page-agent precisa saber sobre voz.
298
+ * Leitura apenas; nunca muta.
299
+ */
300
+ get context(): VoiceContext;
301
+ /**
302
+ * Snapshot para o painel.
303
+ */
304
+ get snapshot(): VoiceSnapshot;
305
+ /**
306
+ * Histórico de mensagens: construído incrementalmente
307
+ * conforme entradas chegam e respostas são produzidas.
308
+ */
309
+ get messages(): readonly Message[];
310
+ /**
311
+ * Muta/desmuta o microfone.
312
+ */
313
+ setMuted(muted: boolean): void;
314
+ /**
315
+ * Registra uma mensagem no histórico local do bridge.
316
+ * Não persiste (isso é job do store se necessário).
317
+ */
318
+ private recordMessage;
319
+ /**
320
+ * Reset para testes ou novo começo de conversa.
321
+ */
322
+ clearHistory(): void;
323
+ }
324
+
325
+ /** Estado compartilhado entre voz e navegação. */
326
+ export declare type VoiceContext = {
327
+ mode: SpeechMode;
328
+ channel: InputChannel;
329
+ turn: TurnState;
330
+ sessionId: string;
331
+ conversationHistory: Message[];
332
+ muted: boolean;
333
+ lastTurnTime?: number;
334
+ };
335
+
336
+ /**
337
+ * Protocol para uma engine de voz (Local ou Realtime).
338
+ * Baseado no EngineLike de NoStopPageTest.
339
+ */
340
+ export declare interface VoiceEngine {
341
+ readonly capabilities: ReadonlySet<Capability>;
342
+ /** Abre a engine (inicializa modelos, conexões, etc). */
343
+ enter(): Promise<void>;
344
+ /** Fecha a engine. */
345
+ exit(exc?: Error): Promise<void>;
346
+ /** Snapshot atual para o painel. */
347
+ snapshot(): VoiceSnapshot;
348
+ /** Muta ou desmuta o microfone. */
349
+ setMuted(muted: boolean): void;
350
+ /** Submete texto como se tivesse sido ouvido (se TEXT_INPUT). */
351
+ submit(text: string): void;
352
+ /** Itera eventos públicos até fecha. */
353
+ events(): AsyncIterable<VoiceEvent>;
354
+ }
355
+
356
+ /** Variáveis de ambiente/`import.meta.env`: valores podem faltar. */
357
+ export declare type VoiceEnv = Readonly<Record<string, string | undefined>>;
358
+
359
+ export declare class VoiceError extends Error {
360
+ readonly code: VoiceErrorCode;
361
+ constructor(code: VoiceErrorCode, message: string, options?: {
362
+ cause?: unknown;
363
+ });
364
+ }
365
+
366
+ /** Erros da voz com um `code` estável para quem precisa decidir o que fazer (não depender do texto). */
367
+ export declare type VoiceErrorCode = "INVALID_SETTINGS" | "UNSUPPORTED" | "LEASE_FAILED" | "CONNECTION_FAILED" | "REFUSED" | "TIMEOUT" | "CLOSED" | "MICROPHONE";
368
+
369
+ /** Evento emitido pela sessão de voz. */
370
+ export declare type VoiceEvent = {
371
+ kind: "session.ready";
372
+ data: VoiceSnapshot;
373
+ } | {
374
+ kind: "user.started";
375
+ data: {
376
+ channel: InputChannel;
377
+ };
378
+ } | {
379
+ kind: "turn.heard";
380
+ data: {
381
+ text: string;
382
+ confidence?: number;
383
+ channel?: InputChannel;
384
+ };
385
+ } | {
386
+ kind: "agent.started";
387
+ data: {
388
+ turn: TurnState;
389
+ };
390
+ } | {
391
+ kind: "agent.finished";
392
+ data: {
393
+ text: string;
394
+ };
395
+ } | {
396
+ kind: "sound.started";
397
+ data: object;
398
+ } | {
399
+ kind: "speaker.idle";
400
+ data: object;
401
+ } | {
402
+ kind: "channel.switched";
403
+ data: {
404
+ from: InputChannel;
405
+ to: InputChannel;
406
+ };
407
+ } | {
408
+ kind: "error";
409
+ data: {
410
+ message: string;
411
+ code: string;
412
+ };
413
+ };
414
+
415
+ export declare class VoiceSession implements VoiceSessionReader {
416
+ readonly sessionId: string;
417
+ readonly context: VoiceContext;
418
+ private engine;
419
+ private readonly settings;
420
+ private readonly inputQueue;
421
+ private readonly hub;
422
+ private isClosed;
423
+ /** Flag para ativar/desativar mutação de estado. Padrão: true (ativado). */
424
+ private allowMutations;
425
+ constructor(settings: VoiceSettings, allowMutations?: boolean);
426
+ /** Abre a sessão: abre a engine e passa a repassar os eventos dela. */
427
+ abrir(engine: VoiceEngine): Promise<void>;
428
+ /** Fecha a sessão. */
429
+ fechar(): Promise<void>;
430
+ /** Snapshot atual para o painel. */
431
+ snapshot(): VoiceSnapshot;
432
+ /** Habilita/desabilita mutação de estado. Padrão: true (ativado). false = modo auditoria (read-only). */
433
+ setMutationsAllowed(allowed: boolean): void;
434
+ /** Enfileira entrada do usuário (voz transcrita ou texto). Sem efeito se mutações desabilitadas. */
435
+ enqueueInput(input: UserInput): void;
436
+ /** Muta/desmuta o microfone. Sem efeito se mutações desabilitadas. */
437
+ setMuted(muted: boolean): void;
438
+ /** Eventos públicos a partir de agora, até a sessão fechar. Cada chamada tem a sua fila. */
439
+ events(): AsyncIterable<VoiceEvent>;
440
+ /**
441
+ * Alterna entre voz e chat.
442
+ * O histórico é preservado; apenas a origem do próximo input muda.
443
+ * Sem efeito se mutações desabilitadas.
444
+ */
445
+ switchToChannel(channel: InputChannel): void;
446
+ /** Processa a fila: atualiza histórico e avisa o bridge. */
447
+ private processNextInput;
448
+ /** Repassa os eventos da engine enquanto a sessão estiver aberta. */
449
+ private processEngineEvents;
450
+ private handleEngineEvent;
451
+ private emitPublic;
452
+ }
453
+
454
+ /**
455
+ * Controlador de sessão: gerencia transições de canal (voz ↔ chat)
456
+ * e manutenção de estado.
457
+ *
458
+ * Um dono por estado mutável: só aqui muda de "voice" para "chat".
459
+ * Ninguém mais muda VoiceContext.channel.
460
+ */
461
+ export declare class VoiceSessionController {
462
+ private session;
463
+ private bridge;
464
+ constructor(session: VoiceSessionReader, bridge: VoiceBridge);
465
+ /**
466
+ * Muda de canal (voz → chat ou chat → voz).
467
+ * O contexto é preservado: histórico segue intacto.
468
+ * Só muda a ORIGEM do próximo input.
469
+ *
470
+ * Implementação: sinaliza à sessão e deixa ela gerir o event loop.
471
+ */
472
+ switchChannel(to: InputChannel): void;
473
+ /**
474
+ * Contexto atual (apenas leitura).
475
+ */
476
+ get context(): VoiceContext;
477
+ /**
478
+ * Muta/desmuta.
479
+ */
480
+ setMuted(muted: boolean): void;
481
+ }
482
+
483
+ /**
484
+ * Contrato que a sessão de voz expõe para o bridge.
485
+ * Apenas leitura: o bridge nunca escreve no estado da sessão.
486
+ */
487
+ export declare interface VoiceSessionReader {
488
+ readonly sessionId: string;
489
+ readonly context: VoiceContext;
490
+ readonly snapshot: () => VoiceSnapshot;
491
+ /** Enfileira entrada do usuário (voz ou texto). */
492
+ enqueueInput(input: UserInput): void;
493
+ /** Itera eventos públicos. */
494
+ events(): AsyncIterable<VoiceEvent>;
495
+ /** Muta/desmuta. */
496
+ setMuted(muted: boolean): void;
497
+ }
498
+
499
+ export declare interface VoiceSettings {
500
+ /** `realtime`: áudio full-duplex pelo servidor (proxy). `local`: reconhecimento/síntese do navegador. */
501
+ readonly speechMode: SpeechMode;
502
+ /** Idioma BCP-47 usado pelo modo local. */
503
+ readonly language: string;
504
+ /** Origem do servidor de áudio, ex.: `http://localhost:8765` ou `https://voz.exemplo.com/t/abc`. */
505
+ readonly proxyServerUrl: string;
506
+ /** Caminho (relativo à origem) que entrega o `session_id`. */
507
+ readonly proxyLeaseEndpoint: string;
508
+ /** Caminho (relativo à origem) do WebSocket de áudio. */
509
+ readonly proxyWebSocketEndpoint: string;
510
+ /** `session_id` já obtido por outro meio; vazio = pedir ao endpoint de lease. */
511
+ readonly proxySessionId: string;
512
+ /** Tempo máximo para o servidor aceitar a conexão e avisar que está pronto. */
513
+ readonly connectTimeoutMs: number;
514
+ /** Microfone ligado quando a sessão abre (`false` = abre mutado). */
515
+ readonly mic: boolean;
516
+ /** Identificador local da sessão (histórico, eventos). */
517
+ readonly sessionId: string;
518
+ readonly trace: boolean;
519
+ }
520
+
521
+ /** Snapshot do estado de voz para o painel. */
522
+ export declare type VoiceSnapshot = {
523
+ mode: SpeechMode;
524
+ channel: InputChannel;
525
+ state: TurnState;
526
+ muted: boolean;
527
+ sessionId: string;
528
+ messageCount: number;
529
+ lastMessageTime?: number;
530
+ error?: string;
531
+ };
532
+
533
+ export { }