@ai-ecoverse/pi-audio 1.3.0 → 1.4.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,135 @@
1
+ import { blankUsage } from "./blank-usage.js";
2
+ import { VoiceRequestError } from "./errors.js";
3
+ import { sentencesOf } from "./sentences.js";
4
+ import {
5
+ SpeechEventStream
6
+ } from "./speech.js";
7
+ import { addAudioCounters } from "./usage.js";
8
+ function shiftSpan(span, offset) {
9
+ if ("entryId" in span) {
10
+ return {
11
+ entryId: span.entryId,
12
+ contentIndex: span.contentIndex,
13
+ start: span.start + offset,
14
+ end: span.end + offset
15
+ };
16
+ }
17
+ return { itemId: span.itemId, start: span.start + offset, end: span.end + offset };
18
+ }
19
+ function mergeUsage(usages) {
20
+ const total = blankUsage();
21
+ for (const usage of usages) {
22
+ total.input += usage.input;
23
+ total.output += usage.output;
24
+ total.cacheRead += usage.cacheRead;
25
+ total.cacheWrite += usage.cacheWrite;
26
+ total.totalTokens += usage.totalTokens;
27
+ total.cost.input += usage.cost.input;
28
+ total.cost.output += usage.cost.output;
29
+ total.cost.cacheRead += usage.cost.cacheRead;
30
+ total.cost.cacheWrite += usage.cost.cacheWrite;
31
+ total.cost.total += usage.cost.total;
32
+ if (usage.reasoning !== void 0) total.reasoning = (total.reasoning ?? 0) + usage.reasoning;
33
+ if (usage.cacheWrite1h !== void 0) {
34
+ total.cacheWrite1h = (total.cacheWrite1h ?? 0) + usage.cacheWrite1h;
35
+ }
36
+ addAudioCounters(total, usage);
37
+ }
38
+ return total;
39
+ }
40
+ function failure(error) {
41
+ if (error instanceof VoiceRequestError) {
42
+ return { type: "error", reason: error.reason, error: error.message };
43
+ }
44
+ const message = error instanceof Error ? error.message : String(error);
45
+ return { type: "error", reason: "error", error: message };
46
+ }
47
+ async function playSentence(speak, model, input, sentence, offset, options, stream, usages, state) {
48
+ let inner;
49
+ try {
50
+ inner = speak(model, { ...input, text: sentence }, options);
51
+ } catch (error) {
52
+ stream.push(failure(error));
53
+ return false;
54
+ }
55
+ let terminal = false;
56
+ for await (const event of inner) {
57
+ if (options?.signal?.aborted && event.type !== "error" && event.type !== "done") {
58
+ stream.push({ type: "error", reason: "aborted", error: "aborted" });
59
+ return false;
60
+ }
61
+ if (event.type === "start") {
62
+ if (!state.started) stream.push(event);
63
+ state.started = true;
64
+ continue;
65
+ }
66
+ if (event.type === "audio") {
67
+ stream.push({ ...event, span: shiftSpan(event.span, offset) });
68
+ continue;
69
+ }
70
+ if (event.type === "done") {
71
+ usages.push(event.usage);
72
+ terminal = true;
73
+ continue;
74
+ }
75
+ stream.push(event);
76
+ return false;
77
+ }
78
+ if (terminal) return true;
79
+ try {
80
+ usages.push(await inner.result());
81
+ return true;
82
+ } catch (error) {
83
+ stream.push(failure(error));
84
+ return false;
85
+ }
86
+ }
87
+ function speakInSentences(speak, model, input, options) {
88
+ const stream = new SpeechEventStream();
89
+ void pump(speak, model, input, options, stream);
90
+ return stream;
91
+ }
92
+ async function pump(speak, model, input, options, stream) {
93
+ if (options?.signal?.aborted) {
94
+ stream.push({ type: "error", reason: "aborted", error: "aborted" });
95
+ return;
96
+ }
97
+ const usages = [];
98
+ const state = { started: false };
99
+ let offset = 0;
100
+ let played = false;
101
+ try {
102
+ for await (const sentence of sentencesOf(input.text)) {
103
+ if (options?.signal?.aborted) {
104
+ stream.push({ type: "error", reason: "aborted", error: "aborted" });
105
+ return;
106
+ }
107
+ played = true;
108
+ const ok = await playSentence(
109
+ speak,
110
+ model,
111
+ input,
112
+ sentence,
113
+ offset,
114
+ options,
115
+ stream,
116
+ usages,
117
+ state
118
+ );
119
+ if (!ok) return;
120
+ offset += sentence.length;
121
+ }
122
+ if (!played) {
123
+ const text = typeof input.text === "string" ? input.text : "";
124
+ const ok = await playSentence(speak, model, input, text, 0, options, stream, usages, state);
125
+ if (!ok) return;
126
+ }
127
+ stream.push({ type: "done", usage: mergeUsage(usages) });
128
+ } catch (error) {
129
+ stream.push(failure(error));
130
+ }
131
+ }
132
+ export {
133
+ speakInSentences
134
+ };
135
+ //# sourceMappingURL=chunk-speech.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/chunk-speech.ts"],
4
+ "sourcesContent": ["import type { ProviderRequestOptions, Usage } from '@earendil-works/pi-ai';\nimport type { TextSpan } from './audio.ts';\nimport { blankUsage } from './blank-usage.ts';\nimport { VoiceRequestError } from './errors.ts';\nimport type { SpeechModel } from './models.ts';\nimport { sentencesOf } from './sentences.ts';\nimport {\n type SpeechApiProvider,\n type SpeechEvent,\n SpeechEventStream,\n type SpeechInput,\n} from './speech.ts';\nimport { addAudioCounters } from './usage.ts';\n\nfunction shiftSpan(span: TextSpan, offset: number): TextSpan {\n if ('entryId' in span) {\n return {\n entryId: span.entryId,\n contentIndex: span.contentIndex,\n start: span.start + offset,\n end: span.end + offset,\n };\n }\n return { itemId: span.itemId, start: span.start + offset, end: span.end + offset };\n}\n\nfunction mergeUsage(usages: Usage[]): Usage {\n const total = blankUsage();\n for (const usage of usages) {\n total.input += usage.input;\n total.output += usage.output;\n total.cacheRead += usage.cacheRead;\n total.cacheWrite += usage.cacheWrite;\n total.totalTokens += usage.totalTokens;\n total.cost.input += usage.cost.input;\n total.cost.output += usage.cost.output;\n total.cost.cacheRead += usage.cost.cacheRead;\n total.cost.cacheWrite += usage.cost.cacheWrite;\n total.cost.total += usage.cost.total;\n if (usage.reasoning !== undefined) total.reasoning = (total.reasoning ?? 0) + usage.reasoning;\n if (usage.cacheWrite1h !== undefined) {\n total.cacheWrite1h = (total.cacheWrite1h ?? 0) + usage.cacheWrite1h;\n }\n addAudioCounters(total, usage);\n }\n return total;\n}\n\nfunction failure(error: unknown): SpeechEvent {\n if (error instanceof VoiceRequestError) {\n return { type: 'error', reason: error.reason, error: error.message };\n }\n const message = error instanceof Error ? error.message : String(error);\n return { type: 'error', reason: 'error', error: message };\n}\n\nasync function playSentence(\n speak: SpeechApiProvider['speak'],\n model: SpeechModel<string>,\n input: SpeechInput,\n sentence: string,\n offset: number,\n options: ProviderRequestOptions | undefined,\n stream: SpeechEventStream,\n usages: Usage[],\n state: { started: boolean }\n): Promise<boolean> {\n let inner: SpeechEventStream;\n try {\n inner = speak(model, { ...input, text: sentence }, options);\n } catch (error) {\n stream.push(failure(error));\n return false;\n }\n let terminal = false;\n for await (const event of inner) {\n if (options?.signal?.aborted && event.type !== 'error' && event.type !== 'done') {\n stream.push({ type: 'error', reason: 'aborted', error: 'aborted' });\n return false;\n }\n if (event.type === 'start') {\n if (!state.started) stream.push(event);\n state.started = true;\n continue;\n }\n if (event.type === 'audio') {\n stream.push({ ...event, span: shiftSpan(event.span, offset) });\n continue;\n }\n if (event.type === 'done') {\n usages.push(event.usage);\n terminal = true;\n continue;\n }\n stream.push(event);\n return false;\n }\n if (terminal) return true;\n try {\n usages.push(await inner.result());\n return true;\n } catch (error) {\n stream.push(failure(error));\n return false;\n }\n}\n\nexport function speakInSentences(\n speak: SpeechApiProvider['speak'],\n model: SpeechModel<string>,\n input: SpeechInput,\n options?: ProviderRequestOptions\n): SpeechEventStream {\n const stream = new SpeechEventStream();\n void pump(speak, model, input, options, stream);\n return stream;\n}\n\nasync function pump(\n speak: SpeechApiProvider['speak'],\n model: SpeechModel<string>,\n input: SpeechInput,\n options: ProviderRequestOptions | undefined,\n stream: SpeechEventStream\n) {\n if (options?.signal?.aborted) {\n stream.push({ type: 'error', reason: 'aborted', error: 'aborted' });\n return;\n }\n const usages: Usage[] = [];\n const state = { started: false };\n let offset = 0;\n let played = false;\n try {\n for await (const sentence of sentencesOf(input.text)) {\n if (options?.signal?.aborted) {\n stream.push({ type: 'error', reason: 'aborted', error: 'aborted' });\n return;\n }\n played = true;\n const ok = await playSentence(\n speak,\n model,\n input,\n sentence,\n offset,\n options,\n stream,\n usages,\n state\n );\n if (!ok) return;\n offset += sentence.length;\n }\n if (!played) {\n const text = typeof input.text === 'string' ? input.text : '';\n const ok = await playSentence(speak, model, input, text, 0, options, stream, usages, state);\n if (!ok) return;\n }\n stream.push({ type: 'done', usage: mergeUsage(usages) });\n } catch (error) {\n stream.push(failure(error));\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  import { SpeechEventStream } from "./speech.js";
25
25
  import { TranscriptionEventStream } from "./transcription.js";
26
26
  import { addAudioCounters, priceAudio } from "./usage.js";
27
+ import { energyVad } from "./vad.js";
27
28
  export {
28
29
  SpeechEventStream,
29
30
  TranscriptionEventStream,
@@ -35,6 +36,7 @@ export {
35
36
  convertAudioSource,
36
37
  decodeAudioBase64,
37
38
  encodeAudioBase64,
39
+ energyVad,
38
40
  getVoiceModel,
39
41
  listVoiceModels,
40
42
  listen,
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["export type {\n AudioChunk,\n AudioEncoding,\n AudioFormat,\n AudioSink,\n AudioSource,\n AudioTag,\n CharAlignment,\n TextSpan,\n} from './audio.ts';\nexport { resolveVoiceAuth } from './auth.ts';\nexport { blankUsage } from './blank-usage.ts';\nexport {\n convertAudio,\n convertAudioSource,\n decodeAudioBase64,\n encodeAudioBase64,\n} from './convert.ts';\nexport { VoiceRequestError } from './errors.ts';\nexport type {\n Delivery,\n DeliverySupport,\n KnownRealtimeApi,\n KnownSpeechApi,\n KnownTranscriptionApi,\n RealtimeApi,\n RealtimeModel,\n SpeechApi,\n SpeechModel,\n TranscriptionApi,\n TranscriptionModel,\n VoiceInfo,\n VoiceModel,\n VoiceModelType,\n} from './models.ts';\nexport {\n getVoiceModel,\n listVoiceModels,\n registerVoiceModel,\n} from './models.ts';\nexport type {\n RealtimeApiProvider,\n RealtimeContext,\n RealtimeEvent,\n RealtimeOptions,\n RealtimeSession,\n RealtimeToken,\n} from './realtime.ts';\nexport {\n connectRealtime,\n listen,\n registerRealtimeApiProvider,\n registerSpeechApiProvider,\n registerTranscriptionApiProvider,\n speak,\n transcribe,\n} from './registry.ts';\nexport type { SpeechApiProvider, SpeechEvent, SpeechInput } from './speech.ts';\nexport { SpeechEventStream } from './speech.ts';\nexport type {\n AudioInput,\n ListenOptions,\n Transcript,\n TranscriptionApiProvider,\n TranscriptionEvent,\n TranscriptSegment,\n} from './transcription.ts';\nexport { TranscriptionEventStream } from './transcription.ts';\nexport type {\n AudioCost,\n AudioCounters,\n AudioUsage,\n RealtimeAudioCost,\n SpeechAudioCost,\n TranscriptionAudioCost,\n} from './usage.ts';\nexport { addAudioCounters, priceAudio } from './usage.ts';\n"],
5
- "mappings": "AAUA,SAAS,wBAAwB;AACjC,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,yBAAyB;AAiBlC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AASP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,yBAAyB;AASlC,SAAS,gCAAgC;AASzC,SAAS,kBAAkB,kBAAkB;",
4
+ "sourcesContent": ["export type {\n AudioChunk,\n AudioEncoding,\n AudioFormat,\n AudioSink,\n AudioSource,\n AudioTag,\n CharAlignment,\n TextSpan,\n} from './audio.ts';\nexport { resolveVoiceAuth } from './auth.ts';\nexport { blankUsage } from './blank-usage.ts';\nexport {\n convertAudio,\n convertAudioSource,\n decodeAudioBase64,\n encodeAudioBase64,\n} from './convert.ts';\nexport { VoiceRequestError } from './errors.ts';\nexport type {\n Delivery,\n DeliverySupport,\n KnownRealtimeApi,\n KnownSpeechApi,\n KnownTranscriptionApi,\n RealtimeApi,\n RealtimeModel,\n SpeechApi,\n SpeechModel,\n TranscriptionApi,\n TranscriptionModel,\n VoiceInfo,\n VoiceModel,\n VoiceModelType,\n} from './models.ts';\nexport {\n getVoiceModel,\n listVoiceModels,\n registerVoiceModel,\n} from './models.ts';\nexport type {\n RealtimeApiProvider,\n RealtimeContext,\n RealtimeEvent,\n RealtimeOptions,\n RealtimeSession,\n RealtimeToken,\n} from './realtime.ts';\nexport {\n connectRealtime,\n listen,\n registerRealtimeApiProvider,\n registerSpeechApiProvider,\n registerTranscriptionApiProvider,\n speak,\n transcribe,\n} from './registry.ts';\nexport type { SpeechApiProvider, SpeechEvent, SpeechInput } from './speech.ts';\nexport { SpeechEventStream } from './speech.ts';\nexport type {\n AudioInput,\n ListenOptions,\n Transcript,\n TranscriptionApiProvider,\n TranscriptionEvent,\n TranscriptSegment,\n} from './transcription.ts';\nexport { TranscriptionEventStream } from './transcription.ts';\nexport type {\n AudioCost,\n AudioCounters,\n AudioUsage,\n RealtimeAudioCost,\n SpeechAudioCost,\n TranscriptionAudioCost,\n} from './usage.ts';\nexport { addAudioCounters, priceAudio } from './usage.ts';\nexport type { VadHook, VadUpdate } from './vad.ts';\nexport { energyVad } from './vad.ts';\n"],
5
+ "mappings": "AAUA,SAAS,wBAAwB;AACjC,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,yBAAyB;AAiBlC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AASP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,yBAAyB;AASlC,SAAS,gCAAgC;AASzC,SAAS,kBAAkB,kBAAkB;AAE7C,SAAS,iBAAiB;",
6
6
  "names": []
7
7
  }
package/dist/registry.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { speakInSentences } from "./chunk-speech.js";
2
+ import { listenWithVad } from "./vad-listen.js";
1
3
  const transcriptionApis = /* @__PURE__ */ new Map();
2
4
  const speechApis = /* @__PURE__ */ new Map();
3
5
  const realtimeApis = /* @__PURE__ */ new Map();
@@ -33,11 +35,13 @@ function transcribe(model, input, options) {
33
35
  function listen(model, audio, options) {
34
36
  const listenModel = transcriptionApis.get(model.api)?.provider.listen;
35
37
  if (!listenModel) throw missing(model.api);
38
+ if (model.turnDetection === "none") return listenWithVad(listenModel, model, audio, options);
36
39
  return listenModel(model, audio, options);
37
40
  }
38
41
  function speak(model, input, options) {
39
42
  const speakModel = speechApis.get(model.api)?.provider.speak;
40
43
  if (!speakModel) throw missing(model.api);
44
+ if (!model.textStreaming) return speakInSentences(speakModel, model, input, options);
41
45
  return speakModel(model, input, options);
42
46
  }
43
47
  function connectRealtime(model, context, options) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/registry.ts"],
4
- "sourcesContent": ["import type { ProviderRequestOptions } from '@earendil-works/pi-ai';\nimport type { AudioSource } from './audio.ts';\nimport type { RealtimeModel, SpeechModel, TranscriptionModel } from './models.ts';\nimport type {\n RealtimeApiProvider,\n RealtimeContext,\n RealtimeOptions,\n RealtimeSession,\n} from './realtime.ts';\nimport type { SpeechApiProvider, SpeechEventStream, SpeechInput } from './speech.ts';\nimport type {\n AudioInput,\n ListenOptions,\n Transcript,\n TranscriptionApiProvider,\n TranscriptionEventStream,\n} from './transcription.ts';\n\ninterface TranscriptionEntry {\n provider: TranscriptionApiProvider;\n}\n\ninterface SpeechEntry {\n provider: SpeechApiProvider;\n}\n\ninterface RealtimeEntry {\n provider: RealtimeApiProvider;\n}\n\nconst transcriptionApis = new Map<string, TranscriptionEntry>();\nconst speechApis = new Map<string, SpeechEntry>();\nconst realtimeApis = new Map<string, RealtimeEntry>();\n\nfunction missing(api: string): Error {\n return new Error(`No API provider registered for api: ${api}`);\n}\n\nexport function registerTranscriptionApiProvider(provider: TranscriptionApiProvider): () => void {\n const entry = { provider };\n transcriptionApis.set(provider.api, entry);\n return () => {\n if (transcriptionApis.get(provider.api) === entry) transcriptionApis.delete(provider.api);\n };\n}\n\nexport function registerSpeechApiProvider(provider: SpeechApiProvider): () => void {\n const entry = { provider };\n speechApis.set(provider.api, entry);\n return () => {\n if (speechApis.get(provider.api) === entry) speechApis.delete(provider.api);\n };\n}\n\nexport function registerRealtimeApiProvider(provider: RealtimeApiProvider): () => void {\n const entry = { provider };\n realtimeApis.set(provider.api, entry);\n return () => {\n if (realtimeApis.get(provider.api) === entry) realtimeApis.delete(provider.api);\n };\n}\n\nexport function transcribe(\n model: TranscriptionModel<string>,\n input: AudioInput,\n options?: ProviderRequestOptions\n): Promise<Transcript> {\n const transcribeModel = transcriptionApis.get(model.api)?.provider.transcribe;\n if (!transcribeModel) return Promise.reject(missing(model.api));\n return transcribeModel(model, input, options);\n}\n\nexport function listen(\n model: TranscriptionModel<string>,\n audio: AudioSource,\n options?: ListenOptions\n): TranscriptionEventStream {\n const listenModel = transcriptionApis.get(model.api)?.provider.listen;\n if (!listenModel) throw missing(model.api);\n return listenModel(model, audio, options);\n}\n\nexport function speak(\n model: SpeechModel<string>,\n input: SpeechInput,\n options?: ProviderRequestOptions\n): SpeechEventStream {\n const speakModel = speechApis.get(model.api)?.provider.speak;\n if (!speakModel) throw missing(model.api);\n return speakModel(model, input, options);\n}\n\nexport function connectRealtime(\n model: RealtimeModel<string>,\n context: RealtimeContext,\n options?: RealtimeOptions\n): Promise<RealtimeSession> {\n const connect = realtimeApis.get(model.api)?.provider.connect;\n if (!connect) return Promise.reject(missing(model.api));\n return connect(model, context, options);\n}\n"],
5
- "mappings": "AA8BA,MAAM,oBAAoB,oBAAI,IAAgC;AAC9D,MAAM,aAAa,oBAAI,IAAyB;AAChD,MAAM,eAAe,oBAAI,IAA2B;AAEpD,SAAS,QAAQ,KAAoB;AACnC,SAAO,IAAI,MAAM,uCAAuC,GAAG,EAAE;AAC/D;AAEO,SAAS,iCAAiC,UAAgD;AAC/F,QAAM,QAAQ,EAAE,SAAS;AACzB,oBAAkB,IAAI,SAAS,KAAK,KAAK;AACzC,SAAO,MAAM;AACX,QAAI,kBAAkB,IAAI,SAAS,GAAG,MAAM,MAAO,mBAAkB,OAAO,SAAS,GAAG;AAAA,EAC1F;AACF;AAEO,SAAS,0BAA0B,UAAyC;AACjF,QAAM,QAAQ,EAAE,SAAS;AACzB,aAAW,IAAI,SAAS,KAAK,KAAK;AAClC,SAAO,MAAM;AACX,QAAI,WAAW,IAAI,SAAS,GAAG,MAAM,MAAO,YAAW,OAAO,SAAS,GAAG;AAAA,EAC5E;AACF;AAEO,SAAS,4BAA4B,UAA2C;AACrF,QAAM,QAAQ,EAAE,SAAS;AACzB,eAAa,IAAI,SAAS,KAAK,KAAK;AACpC,SAAO,MAAM;AACX,QAAI,aAAa,IAAI,SAAS,GAAG,MAAM,MAAO,cAAa,OAAO,SAAS,GAAG;AAAA,EAChF;AACF;AAEO,SAAS,WACd,OACA,OACA,SACqB;AACrB,QAAM,kBAAkB,kBAAkB,IAAI,MAAM,GAAG,GAAG,SAAS;AACnE,MAAI,CAAC,gBAAiB,QAAO,QAAQ,OAAO,QAAQ,MAAM,GAAG,CAAC;AAC9D,SAAO,gBAAgB,OAAO,OAAO,OAAO;AAC9C;AAEO,SAAS,OACd,OACA,OACA,SAC0B;AAC1B,QAAM,cAAc,kBAAkB,IAAI,MAAM,GAAG,GAAG,SAAS;AAC/D,MAAI,CAAC,YAAa,OAAM,QAAQ,MAAM,GAAG;AACzC,SAAO,YAAY,OAAO,OAAO,OAAO;AAC1C;AAEO,SAAS,MACd,OACA,OACA,SACmB;AACnB,QAAM,aAAa,WAAW,IAAI,MAAM,GAAG,GAAG,SAAS;AACvD,MAAI,CAAC,WAAY,OAAM,QAAQ,MAAM,GAAG;AACxC,SAAO,WAAW,OAAO,OAAO,OAAO;AACzC;AAEO,SAAS,gBACd,OACA,SACA,SAC0B;AAC1B,QAAM,UAAU,aAAa,IAAI,MAAM,GAAG,GAAG,SAAS;AACtD,MAAI,CAAC,QAAS,QAAO,QAAQ,OAAO,QAAQ,MAAM,GAAG,CAAC;AACtD,SAAO,QAAQ,OAAO,SAAS,OAAO;AACxC;",
4
+ "sourcesContent": ["import type { ProviderRequestOptions } from '@earendil-works/pi-ai';\nimport type { AudioSource } from './audio.ts';\nimport { speakInSentences } from './chunk-speech.ts';\nimport type { RealtimeModel, SpeechModel, TranscriptionModel } from './models.ts';\nimport type {\n RealtimeApiProvider,\n RealtimeContext,\n RealtimeOptions,\n RealtimeSession,\n} from './realtime.ts';\nimport type { SpeechApiProvider, SpeechEventStream, SpeechInput } from './speech.ts';\nimport type {\n AudioInput,\n ListenOptions,\n Transcript,\n TranscriptionApiProvider,\n TranscriptionEventStream,\n} from './transcription.ts';\nimport { listenWithVad } from './vad-listen.ts';\n\ninterface TranscriptionEntry {\n provider: TranscriptionApiProvider;\n}\n\ninterface SpeechEntry {\n provider: SpeechApiProvider;\n}\n\ninterface RealtimeEntry {\n provider: RealtimeApiProvider;\n}\n\nconst transcriptionApis = new Map<string, TranscriptionEntry>();\nconst speechApis = new Map<string, SpeechEntry>();\nconst realtimeApis = new Map<string, RealtimeEntry>();\n\nfunction missing(api: string): Error {\n return new Error(`No API provider registered for api: ${api}`);\n}\n\nexport function registerTranscriptionApiProvider(provider: TranscriptionApiProvider): () => void {\n const entry = { provider };\n transcriptionApis.set(provider.api, entry);\n return () => {\n if (transcriptionApis.get(provider.api) === entry) transcriptionApis.delete(provider.api);\n };\n}\n\nexport function registerSpeechApiProvider(provider: SpeechApiProvider): () => void {\n const entry = { provider };\n speechApis.set(provider.api, entry);\n return () => {\n if (speechApis.get(provider.api) === entry) speechApis.delete(provider.api);\n };\n}\n\nexport function registerRealtimeApiProvider(provider: RealtimeApiProvider): () => void {\n const entry = { provider };\n realtimeApis.set(provider.api, entry);\n return () => {\n if (realtimeApis.get(provider.api) === entry) realtimeApis.delete(provider.api);\n };\n}\n\nexport function transcribe(\n model: TranscriptionModel<string>,\n input: AudioInput,\n options?: ProviderRequestOptions\n): Promise<Transcript> {\n const transcribeModel = transcriptionApis.get(model.api)?.provider.transcribe;\n if (!transcribeModel) return Promise.reject(missing(model.api));\n return transcribeModel(model, input, options);\n}\n\nexport function listen(\n model: TranscriptionModel<string>,\n audio: AudioSource,\n options?: ListenOptions\n): TranscriptionEventStream {\n const listenModel = transcriptionApis.get(model.api)?.provider.listen;\n if (!listenModel) throw missing(model.api);\n if (model.turnDetection === 'none') return listenWithVad(listenModel, model, audio, options);\n return listenModel(model, audio, options);\n}\n\nexport function speak(\n model: SpeechModel<string>,\n input: SpeechInput,\n options?: ProviderRequestOptions\n): SpeechEventStream {\n const speakModel = speechApis.get(model.api)?.provider.speak;\n if (!speakModel) throw missing(model.api);\n if (!model.textStreaming) return speakInSentences(speakModel, model, input, options);\n return speakModel(model, input, options);\n}\n\nexport function connectRealtime(\n model: RealtimeModel<string>,\n context: RealtimeContext,\n options?: RealtimeOptions\n): Promise<RealtimeSession> {\n const connect = realtimeApis.get(model.api)?.provider.connect;\n if (!connect) return Promise.reject(missing(model.api));\n return connect(model, context, options);\n}\n"],
5
+ "mappings": "AAEA,SAAS,wBAAwB;AAgBjC,SAAS,qBAAqB;AAc9B,MAAM,oBAAoB,oBAAI,IAAgC;AAC9D,MAAM,aAAa,oBAAI,IAAyB;AAChD,MAAM,eAAe,oBAAI,IAA2B;AAEpD,SAAS,QAAQ,KAAoB;AACnC,SAAO,IAAI,MAAM,uCAAuC,GAAG,EAAE;AAC/D;AAEO,SAAS,iCAAiC,UAAgD;AAC/F,QAAM,QAAQ,EAAE,SAAS;AACzB,oBAAkB,IAAI,SAAS,KAAK,KAAK;AACzC,SAAO,MAAM;AACX,QAAI,kBAAkB,IAAI,SAAS,GAAG,MAAM,MAAO,mBAAkB,OAAO,SAAS,GAAG;AAAA,EAC1F;AACF;AAEO,SAAS,0BAA0B,UAAyC;AACjF,QAAM,QAAQ,EAAE,SAAS;AACzB,aAAW,IAAI,SAAS,KAAK,KAAK;AAClC,SAAO,MAAM;AACX,QAAI,WAAW,IAAI,SAAS,GAAG,MAAM,MAAO,YAAW,OAAO,SAAS,GAAG;AAAA,EAC5E;AACF;AAEO,SAAS,4BAA4B,UAA2C;AACrF,QAAM,QAAQ,EAAE,SAAS;AACzB,eAAa,IAAI,SAAS,KAAK,KAAK;AACpC,SAAO,MAAM;AACX,QAAI,aAAa,IAAI,SAAS,GAAG,MAAM,MAAO,cAAa,OAAO,SAAS,GAAG;AAAA,EAChF;AACF;AAEO,SAAS,WACd,OACA,OACA,SACqB;AACrB,QAAM,kBAAkB,kBAAkB,IAAI,MAAM,GAAG,GAAG,SAAS;AACnE,MAAI,CAAC,gBAAiB,QAAO,QAAQ,OAAO,QAAQ,MAAM,GAAG,CAAC;AAC9D,SAAO,gBAAgB,OAAO,OAAO,OAAO;AAC9C;AAEO,SAAS,OACd,OACA,OACA,SAC0B;AAC1B,QAAM,cAAc,kBAAkB,IAAI,MAAM,GAAG,GAAG,SAAS;AAC/D,MAAI,CAAC,YAAa,OAAM,QAAQ,MAAM,GAAG;AACzC,MAAI,MAAM,kBAAkB,OAAQ,QAAO,cAAc,aAAa,OAAO,OAAO,OAAO;AAC3F,SAAO,YAAY,OAAO,OAAO,OAAO;AAC1C;AAEO,SAAS,MACd,OACA,OACA,SACmB;AACnB,QAAM,aAAa,WAAW,IAAI,MAAM,GAAG,GAAG,SAAS;AACvD,MAAI,CAAC,WAAY,OAAM,QAAQ,MAAM,GAAG;AACxC,MAAI,CAAC,MAAM,cAAe,QAAO,iBAAiB,YAAY,OAAO,OAAO,OAAO;AACnF,SAAO,WAAW,OAAO,OAAO,OAAO;AACzC;AAEO,SAAS,gBACd,OACA,SACA,SAC0B;AAC1B,QAAM,UAAU,aAAa,IAAI,MAAM,GAAG,GAAG,SAAS;AACtD,MAAI,CAAC,QAAS,QAAO,QAAQ,OAAO,QAAQ,MAAM,GAAG,CAAC;AACtD,SAAO,QAAQ,OAAO,SAAS,OAAO;AACxC;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,88 @@
1
+ const abbreviations = /* @__PURE__ */ new Set([
2
+ "mr",
3
+ "mrs",
4
+ "ms",
5
+ "dr",
6
+ "prof",
7
+ "sr",
8
+ "jr",
9
+ "st",
10
+ "vs",
11
+ "etc",
12
+ "eg",
13
+ "ie",
14
+ "fig",
15
+ "no"
16
+ ]);
17
+ function wordBefore(text, index) {
18
+ let start = index;
19
+ while (start > 0 && /[A-Za-z]/.test(text[start - 1] ?? "")) start -= 1;
20
+ return text.slice(start, index).toLowerCase();
21
+ }
22
+ function boundary(text, index) {
23
+ const ch = text[index];
24
+ if (ch === "\n") return true;
25
+ if (ch !== "." && ch !== "!" && ch !== "?" && ch !== "\u2026") return false;
26
+ const previous = text[index - 1];
27
+ const next = text[index + 1];
28
+ if ((ch === "." || ch === "\u2026") && previous !== void 0 && next !== void 0 && previous >= "0" && previous <= "9" && next >= "0" && next <= "9") {
29
+ return false;
30
+ }
31
+ if (ch === "." && next === ".") return false;
32
+ if (ch === ".") {
33
+ const word = wordBefore(text, index);
34
+ if (abbreviations.has(word) || /^[A-Za-z]$/.test(word)) return false;
35
+ }
36
+ return true;
37
+ }
38
+ function splitSentences(buffer, flush) {
39
+ const sentences = [];
40
+ let start = 0;
41
+ let index = 0;
42
+ while (index < buffer.length) {
43
+ if (!boundary(buffer, index)) {
44
+ index += 1;
45
+ continue;
46
+ }
47
+ const ch = buffer[index];
48
+ const next = index + 1;
49
+ if (ch !== "\n" && next >= buffer.length && !flush) break;
50
+ if (ch !== "\n" && next < buffer.length && !/\s/.test(buffer[next] ?? "")) {
51
+ index += 1;
52
+ continue;
53
+ }
54
+ let end = next;
55
+ while (end < buffer.length && /\s/.test(buffer[end] ?? "")) end += 1;
56
+ const sentence = buffer.slice(start, end);
57
+ if (sentence.trim().length === 0) {
58
+ index = Math.max(end, index + 1);
59
+ continue;
60
+ }
61
+ sentences.push(sentence);
62
+ start = end;
63
+ index = end;
64
+ }
65
+ const rest = buffer.slice(start);
66
+ if (!flush) return { sentences, rest };
67
+ if (rest.length > 0) sentences.push(rest);
68
+ return { sentences, rest: "" };
69
+ }
70
+ async function* sentencesOf(text) {
71
+ if (typeof text === "string") {
72
+ for (const sentence of splitSentences(text, true).sentences) yield sentence;
73
+ return;
74
+ }
75
+ let buffer = "";
76
+ for await (const part of text) {
77
+ buffer += part;
78
+ const split = splitSentences(buffer, false);
79
+ buffer = split.rest;
80
+ for (const sentence of split.sentences) yield sentence;
81
+ }
82
+ for (const sentence of splitSentences(buffer, true).sentences) yield sentence;
83
+ }
84
+ export {
85
+ sentencesOf,
86
+ splitSentences
87
+ };
88
+ //# sourceMappingURL=sentences.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/sentences.ts"],
4
+ "sourcesContent": ["const abbreviations = new Set([\n 'mr',\n 'mrs',\n 'ms',\n 'dr',\n 'prof',\n 'sr',\n 'jr',\n 'st',\n 'vs',\n 'etc',\n 'eg',\n 'ie',\n 'fig',\n 'no',\n]);\n\nfunction wordBefore(text: string, index: number): string {\n let start = index;\n while (start > 0 && /[A-Za-z]/.test(text[start - 1] ?? '')) start -= 1;\n return text.slice(start, index).toLowerCase();\n}\n\nfunction boundary(text: string, index: number): boolean {\n const ch = text[index];\n if (ch === '\\n') return true;\n if (ch !== '.' && ch !== '!' && ch !== '?' && ch !== '\u2026') return false;\n const previous = text[index - 1];\n const next = text[index + 1];\n if (\n (ch === '.' || ch === '\u2026') &&\n previous !== undefined &&\n next !== undefined &&\n previous >= '0' &&\n previous <= '9' &&\n next >= '0' &&\n next <= '9'\n ) {\n return false;\n }\n if (ch === '.' && next === '.') return false;\n if (ch === '.') {\n const word = wordBefore(text, index);\n if (abbreviations.has(word) || /^[A-Za-z]$/.test(word)) return false;\n }\n return true;\n}\n\nexport function splitSentences(\n buffer: string,\n flush: boolean\n): { sentences: string[]; rest: string } {\n const sentences: string[] = [];\n let start = 0;\n let index = 0;\n while (index < buffer.length) {\n if (!boundary(buffer, index)) {\n index += 1;\n continue;\n }\n const ch = buffer[index];\n const next = index + 1;\n if (ch !== '\\n' && next >= buffer.length && !flush) break;\n if (ch !== '\\n' && next < buffer.length && !/\\s/.test(buffer[next] ?? '')) {\n index += 1;\n continue;\n }\n let end = next;\n while (end < buffer.length && /\\s/.test(buffer[end] ?? '')) end += 1;\n const sentence = buffer.slice(start, end);\n if (sentence.trim().length === 0) {\n index = Math.max(end, index + 1);\n continue;\n }\n sentences.push(sentence);\n start = end;\n index = end;\n }\n const rest = buffer.slice(start);\n if (!flush) return { sentences, rest };\n if (rest.length > 0) sentences.push(rest);\n return { sentences, rest: '' };\n}\n\nexport async function* sentencesOf(text: string | AsyncIterable<string>): AsyncGenerator<string> {\n if (typeof text === 'string') {\n for (const sentence of splitSentences(text, true).sentences) yield sentence;\n return;\n }\n let buffer = '';\n for await (const part of text) {\n buffer += part;\n const split = splitSentences(buffer, false);\n buffer = split.rest;\n for (const sentence of split.sentences) yield sentence;\n }\n for (const sentence of splitSentences(buffer, true).sentences) yield sentence;\n}\n"],
5
+ "mappings": "AAAA,MAAM,gBAAgB,oBAAI,IAAI;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,SAAS,WAAW,MAAc,OAAuB;AACvD,MAAI,QAAQ;AACZ,SAAO,QAAQ,KAAK,WAAW,KAAK,KAAK,QAAQ,CAAC,KAAK,EAAE,EAAG,UAAS;AACrE,SAAO,KAAK,MAAM,OAAO,KAAK,EAAE,YAAY;AAC9C;AAEA,SAAS,SAAS,MAAc,OAAwB;AACtD,QAAM,KAAK,KAAK,KAAK;AACrB,MAAI,OAAO,KAAM,QAAO;AACxB,MAAI,OAAO,OAAO,OAAO,OAAO,OAAO,OAAO,OAAO,SAAK,QAAO;AACjE,QAAM,WAAW,KAAK,QAAQ,CAAC;AAC/B,QAAM,OAAO,KAAK,QAAQ,CAAC;AAC3B,OACG,OAAO,OAAO,OAAO,aACtB,aAAa,UACb,SAAS,UACT,YAAY,OACZ,YAAY,OACZ,QAAQ,OACR,QAAQ,KACR;AACA,WAAO;AAAA,EACT;AACA,MAAI,OAAO,OAAO,SAAS,IAAK,QAAO;AACvC,MAAI,OAAO,KAAK;AACd,UAAM,OAAO,WAAW,MAAM,KAAK;AACnC,QAAI,cAAc,IAAI,IAAI,KAAK,aAAa,KAAK,IAAI,EAAG,QAAO;AAAA,EACjE;AACA,SAAO;AACT;AAEO,SAAS,eACd,QACA,OACuC;AACvC,QAAM,YAAsB,CAAC;AAC7B,MAAI,QAAQ;AACZ,MAAI,QAAQ;AACZ,SAAO,QAAQ,OAAO,QAAQ;AAC5B,QAAI,CAAC,SAAS,QAAQ,KAAK,GAAG;AAC5B,eAAS;AACT;AAAA,IACF;AACA,UAAM,KAAK,OAAO,KAAK;AACvB,UAAM,OAAO,QAAQ;AACrB,QAAI,OAAO,QAAQ,QAAQ,OAAO,UAAU,CAAC,MAAO;AACpD,QAAI,OAAO,QAAQ,OAAO,OAAO,UAAU,CAAC,KAAK,KAAK,OAAO,IAAI,KAAK,EAAE,GAAG;AACzE,eAAS;AACT;AAAA,IACF;AACA,QAAI,MAAM;AACV,WAAO,MAAM,OAAO,UAAU,KAAK,KAAK,OAAO,GAAG,KAAK,EAAE,EAAG,QAAO;AACnE,UAAM,WAAW,OAAO,MAAM,OAAO,GAAG;AACxC,QAAI,SAAS,KAAK,EAAE,WAAW,GAAG;AAChC,cAAQ,KAAK,IAAI,KAAK,QAAQ,CAAC;AAC/B;AAAA,IACF;AACA,cAAU,KAAK,QAAQ;AACvB,YAAQ;AACR,YAAQ;AAAA,EACV;AACA,QAAM,OAAO,OAAO,MAAM,KAAK;AAC/B,MAAI,CAAC,MAAO,QAAO,EAAE,WAAW,KAAK;AACrC,MAAI,KAAK,SAAS,EAAG,WAAU,KAAK,IAAI;AACxC,SAAO,EAAE,WAAW,MAAM,GAAG;AAC/B;AAEA,gBAAuB,YAAY,MAA8D;AAC/F,MAAI,OAAO,SAAS,UAAU;AAC5B,eAAW,YAAY,eAAe,MAAM,IAAI,EAAE,UAAW,OAAM;AACnE;AAAA,EACF;AACA,MAAI,SAAS;AACb,mBAAiB,QAAQ,MAAM;AAC7B,cAAU;AACV,UAAM,QAAQ,eAAe,QAAQ,KAAK;AAC1C,aAAS,MAAM;AACf,eAAW,YAAY,MAAM,UAAW,OAAM;AAAA,EAChD;AACA,aAAW,YAAY,eAAe,QAAQ,IAAI,EAAE,UAAW,OAAM;AACvE;",
6
+ "names": []
7
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/transcription.ts"],
4
- "sourcesContent": ["import type { ProviderRequestOptions, Usage } from '@earendil-works/pi-ai';\nimport { EventStream } from '@earendil-works/pi-ai';\nimport type { AudioSource } from './audio.ts';\nimport { blankUsage } from './blank-usage.ts';\nimport { VoiceRequestError } from './errors.ts';\nimport type { TranscriptionModel } from './models.ts';\nimport { streamResult } from './stream-result.ts';\n\nexport type AudioInput = { data: Uint8Array; mimeType: string } | { url: string };\n\nexport interface TranscriptSegment {\n text: string;\n start: number;\n end: number;\n speaker?: string;\n}\n\nexport interface Transcript {\n text: string;\n language?: string;\n segments?: TranscriptSegment[];\n usage: Usage;\n}\n\nexport type TranscriptionEvent =\n | { type: 'speech_start'; at: number }\n | { type: 'partial'; segmentId: string; text: string }\n | { type: 'final'; segmentId: string; text: string; confidence?: number }\n | { type: 'turn_end'; text: string; at: number }\n | { type: 'done'; transcript: Transcript }\n | { type: 'error'; reason: 'aborted' | 'error'; error: string };\n\nexport interface ListenOptions extends ProviderRequestOptions {\n language?: string;\n vocabulary?: string[];\n}\n\nfunction blankTranscript(): Transcript {\n return { text: '', usage: blankUsage() };\n}\n\nexport class TranscriptionEventStream extends EventStream<TranscriptionEvent, Transcript> {\n readonly #settled = streamResult<Transcript>();\n\n constructor() {\n super(\n (event) => event.type === 'done' || event.type === 'error',\n (event) => (event.type === 'done' ? event.transcript : blankTranscript())\n );\n }\n\n override push(event: TranscriptionEvent): void {\n super.push(event);\n if (event.type === 'done') this.#settled.finish(event.transcript);\n else if (event.type === 'error')\n this.#settled.fail(new VoiceRequestError(event.reason, event.error));\n }\n\n override end(result?: Transcript): void {\n super.end(result);\n if (result !== undefined) this.#settled.finish(result);\n else\n this.#settled.fail(\n new VoiceRequestError('error', 'Transcription ended without a transcript')\n );\n }\n\n override result(): Promise<Transcript> {\n return this.#settled.result;\n }\n}\n\nexport interface TranscriptionApiProvider<TApi extends string = string> {\n api: TApi;\n transcribe?: (\n model: TranscriptionModel<TApi>,\n input: AudioInput,\n options?: ProviderRequestOptions\n ) => Promise<Transcript>;\n listen?: (\n model: TranscriptionModel<TApi>,\n audio: AudioSource,\n options?: ListenOptions\n ) => TranscriptionEventStream;\n}\n"],
5
- "mappings": "AACA,SAAS,mBAAmB;AAE5B,SAAS,kBAAkB;AAC3B,SAAS,yBAAyB;AAElC,SAAS,oBAAoB;AA+B7B,SAAS,kBAA8B;AACrC,SAAO,EAAE,MAAM,IAAI,OAAO,WAAW,EAAE;AACzC;AAEO,MAAM,iCAAiC,YAA4C;AAAA,EAC/E,WAAW,aAAyB;AAAA,EAE7C,cAAc;AACZ;AAAA,MACE,CAAC,UAAU,MAAM,SAAS,UAAU,MAAM,SAAS;AAAA,MACnD,CAAC,UAAW,MAAM,SAAS,SAAS,MAAM,aAAa,gBAAgB;AAAA,IACzE;AAAA,EACF;AAAA,EAES,KAAK,OAAiC;AAC7C,UAAM,KAAK,KAAK;AAChB,QAAI,MAAM,SAAS,OAAQ,MAAK,SAAS,OAAO,MAAM,UAAU;AAAA,aACvD,MAAM,SAAS;AACtB,WAAK,SAAS,KAAK,IAAI,kBAAkB,MAAM,QAAQ,MAAM,KAAK,CAAC;AAAA,EACvE;AAAA,EAES,IAAI,QAA2B;AACtC,UAAM,IAAI,MAAM;AAChB,QAAI,WAAW,OAAW,MAAK,SAAS,OAAO,MAAM;AAAA;AAEnD,WAAK,SAAS;AAAA,QACZ,IAAI,kBAAkB,SAAS,0CAA0C;AAAA,MAC3E;AAAA,EACJ;AAAA,EAES,SAA8B;AACrC,WAAO,KAAK,SAAS;AAAA,EACvB;AACF;",
4
+ "sourcesContent": ["import type { ProviderRequestOptions, Usage } from '@earendil-works/pi-ai';\nimport { EventStream } from '@earendil-works/pi-ai';\nimport type { AudioSource } from './audio.ts';\nimport { blankUsage } from './blank-usage.ts';\nimport { VoiceRequestError } from './errors.ts';\nimport type { TranscriptionModel } from './models.ts';\nimport { streamResult } from './stream-result.ts';\nimport type { VadHook } from './vad.ts';\n\nexport type AudioInput = { data: Uint8Array; mimeType: string } | { url: string };\n\nexport interface TranscriptSegment {\n text: string;\n start: number;\n end: number;\n speaker?: string;\n}\n\nexport interface Transcript {\n text: string;\n language?: string;\n segments?: TranscriptSegment[];\n usage: Usage;\n}\n\nexport type TranscriptionEvent =\n | { type: 'speech_start'; at: number }\n | { type: 'partial'; segmentId: string; text: string }\n | { type: 'final'; segmentId: string; text: string; confidence?: number }\n | { type: 'turn_end'; text: string; at: number }\n | { type: 'done'; transcript: Transcript }\n | { type: 'error'; reason: 'aborted' | 'error'; error: string };\n\nexport interface ListenOptions extends ProviderRequestOptions {\n language?: string;\n vocabulary?: string[];\n vad?: VadHook;\n}\n\nfunction blankTranscript(): Transcript {\n return { text: '', usage: blankUsage() };\n}\n\nexport class TranscriptionEventStream extends EventStream<TranscriptionEvent, Transcript> {\n readonly #settled = streamResult<Transcript>();\n\n constructor() {\n super(\n (event) => event.type === 'done' || event.type === 'error',\n (event) => (event.type === 'done' ? event.transcript : blankTranscript())\n );\n }\n\n override push(event: TranscriptionEvent): void {\n super.push(event);\n if (event.type === 'done') this.#settled.finish(event.transcript);\n else if (event.type === 'error')\n this.#settled.fail(new VoiceRequestError(event.reason, event.error));\n }\n\n override end(result?: Transcript): void {\n super.end(result);\n if (result !== undefined) this.#settled.finish(result);\n else\n this.#settled.fail(\n new VoiceRequestError('error', 'Transcription ended without a transcript')\n );\n }\n\n override result(): Promise<Transcript> {\n return this.#settled.result;\n }\n}\n\nexport interface TranscriptionApiProvider<TApi extends string = string> {\n api: TApi;\n transcribe?: (\n model: TranscriptionModel<TApi>,\n input: AudioInput,\n options?: ProviderRequestOptions\n ) => Promise<Transcript>;\n listen?: (\n model: TranscriptionModel<TApi>,\n audio: AudioSource,\n options?: ListenOptions\n ) => TranscriptionEventStream;\n}\n"],
5
+ "mappings": "AACA,SAAS,mBAAmB;AAE5B,SAAS,kBAAkB;AAC3B,SAAS,yBAAyB;AAElC,SAAS,oBAAoB;AAiC7B,SAAS,kBAA8B;AACrC,SAAO,EAAE,MAAM,IAAI,OAAO,WAAW,EAAE;AACzC;AAEO,MAAM,iCAAiC,YAA4C;AAAA,EAC/E,WAAW,aAAyB;AAAA,EAE7C,cAAc;AACZ;AAAA,MACE,CAAC,UAAU,MAAM,SAAS,UAAU,MAAM,SAAS;AAAA,MACnD,CAAC,UAAW,MAAM,SAAS,SAAS,MAAM,aAAa,gBAAgB;AAAA,IACzE;AAAA,EACF;AAAA,EAES,KAAK,OAAiC;AAC7C,UAAM,KAAK,KAAK;AAChB,QAAI,MAAM,SAAS,OAAQ,MAAK,SAAS,OAAO,MAAM,UAAU;AAAA,aACvD,MAAM,SAAS;AACtB,WAAK,SAAS,KAAK,IAAI,kBAAkB,MAAM,QAAQ,MAAM,KAAK,CAAC;AAAA,EACvE;AAAA,EAES,IAAI,QAA2B;AACtC,UAAM,IAAI,MAAM;AAChB,QAAI,WAAW,OAAW,MAAK,SAAS,OAAO,MAAM;AAAA;AAEnD,WAAK,SAAS;AAAA,QACZ,IAAI,kBAAkB,SAAS,0CAA0C;AAAA,MAC3E;AAAA,EACJ;AAAA,EAES,SAA8B;AACrC,WAAO,KAAK,SAAS;AAAA,EACvB;AACF;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,4 @@
1
+ import type { ProviderRequestOptions } from '@earendil-works/pi-ai';
2
+ import type { SpeechModel } from './models.ts';
3
+ import { type SpeechApiProvider, SpeechEventStream, type SpeechInput } from './speech.ts';
4
+ export declare function speakInSentences(speak: SpeechApiProvider['speak'], model: SpeechModel<string>, input: SpeechInput, options?: ProviderRequestOptions): SpeechEventStream;
@@ -13,3 +13,5 @@ export type { AudioInput, ListenOptions, Transcript, TranscriptionApiProvider, T
13
13
  export { TranscriptionEventStream } from './transcription.ts';
14
14
  export type { AudioCost, AudioCounters, AudioUsage, RealtimeAudioCost, SpeechAudioCost, TranscriptionAudioCost, } from './usage.ts';
15
15
  export { addAudioCounters, priceAudio } from './usage.ts';
16
+ export type { VadHook, VadUpdate } from './vad.ts';
17
+ export { energyVad } from './vad.ts';
@@ -0,0 +1,5 @@
1
+ export declare function splitSentences(buffer: string, flush: boolean): {
2
+ sentences: string[];
3
+ rest: string;
4
+ };
5
+ export declare function sentencesOf(text: string | AsyncIterable<string>): AsyncGenerator<string>;
@@ -2,6 +2,7 @@ import type { ProviderRequestOptions, Usage } from '@earendil-works/pi-ai';
2
2
  import { EventStream } from '@earendil-works/pi-ai';
3
3
  import type { AudioSource } from './audio.ts';
4
4
  import type { TranscriptionModel } from './models.ts';
5
+ import type { VadHook } from './vad.ts';
5
6
  export type AudioInput = {
6
7
  data: Uint8Array;
7
8
  mimeType: string;
@@ -47,6 +48,7 @@ export type TranscriptionEvent = {
47
48
  export interface ListenOptions extends ProviderRequestOptions {
48
49
  language?: string;
49
50
  vocabulary?: string[];
51
+ vad?: VadHook;
50
52
  }
51
53
  export declare class TranscriptionEventStream extends EventStream<TranscriptionEvent, Transcript> {
52
54
  #private;
@@ -0,0 +1,7 @@
1
+ import type { AudioSource } from './audio.ts';
2
+ import type { TranscriptionModel } from './models.ts';
3
+ import { type ListenOptions, TranscriptionEventStream } from './transcription.ts';
4
+ import { type VadHook } from './vad.ts';
5
+ type ListenModel = (model: TranscriptionModel<string>, audio: AudioSource, options?: ListenOptions) => TranscriptionEventStream;
6
+ export declare function listenWithVad(listenModel: ListenModel, model: TranscriptionModel<string>, audio: AudioSource, options?: ListenOptions, vad?: VadHook): TranscriptionEventStream;
7
+ export {};
@@ -0,0 +1,13 @@
1
+ import type { AudioChunk } from './audio.ts';
2
+ export interface VadUpdate {
3
+ speechStart?: number;
4
+ turnEnd?: number;
5
+ }
6
+ export interface VadHook {
7
+ push(chunk: AudioChunk): VadUpdate | undefined;
8
+ }
9
+ export interface EnergyVadOptions {
10
+ threshold?: number;
11
+ silenceMs?: number;
12
+ }
13
+ export declare function energyVad(options?: EnergyVadOptions): VadHook;
@@ -0,0 +1,223 @@
1
+ import {
2
+ TranscriptionEventStream
3
+ } from "./transcription.js";
4
+ import { energyVad } from "./vad.js";
5
+ function tick() {
6
+ return new Promise((resolve) => {
7
+ setTimeout(resolve, 0);
8
+ });
9
+ }
10
+ function audioGate() {
11
+ const queued = [];
12
+ let resolveWait;
13
+ const source = {
14
+ async *[Symbol.asyncIterator]() {
15
+ while (true) {
16
+ const ready = queued.shift();
17
+ if (ready !== void 0) {
18
+ if (ready.chunk === null) return;
19
+ yield ready.chunk;
20
+ continue;
21
+ }
22
+ const chunk = await new Promise((resolve) => {
23
+ resolveWait = resolve;
24
+ });
25
+ if (chunk === null) return;
26
+ yield chunk;
27
+ }
28
+ }
29
+ };
30
+ return {
31
+ source,
32
+ push(chunk) {
33
+ if (resolveWait) {
34
+ const resolve = resolveWait;
35
+ resolveWait = void 0;
36
+ resolve(chunk);
37
+ return;
38
+ }
39
+ queued.push({ chunk });
40
+ }
41
+ };
42
+ }
43
+ function joinSegments(parts) {
44
+ let text = "";
45
+ for (const part of parts) {
46
+ if (text.length === 0 || /\s$/.test(text) || /^\s/.test(part)) text += part;
47
+ else text += ` ${part}`;
48
+ }
49
+ return text;
50
+ }
51
+ function liveText(session) {
52
+ const parts = [...session.segments.values()];
53
+ if (parts.length > 0) return joinSegments(parts);
54
+ return session.pendingDone?.transcript.text ?? session.providerTurn ?? "";
55
+ }
56
+ function turnText(session) {
57
+ const live = liveText(session);
58
+ if (session.closed.length > 0 && live.startsWith(session.closed)) {
59
+ return live.slice(session.closed.length).trim();
60
+ }
61
+ if (session.providerTurn && !live.startsWith(session.providerTurn)) return session.providerTurn;
62
+ return live.trim();
63
+ }
64
+ function closeTurn(session, at) {
65
+ if (!session.turnOpen || session.stopped) return;
66
+ const text = turnText(session);
67
+ if (text.length === 0) return;
68
+ session.closed = liveText(session);
69
+ session.turnOpen = false;
70
+ session.started = false;
71
+ session.pendingEnd = void 0;
72
+ session.heldEnd = void 0;
73
+ session.providerTurn = void 0;
74
+ session.stream.push({ type: "turn_end", text, at });
75
+ }
76
+ function openTurn(session, at) {
77
+ if (session.started || session.stopped) return;
78
+ session.stream.push({ type: "speech_start", at });
79
+ session.started = true;
80
+ session.turnOpen = true;
81
+ session.pendingStart = void 0;
82
+ }
83
+ function applyEvent(session, event) {
84
+ if (event.type === "speech_start") {
85
+ if (!session.started) session.stream.push(event);
86
+ session.started = true;
87
+ session.turnOpen = true;
88
+ session.pendingStart = void 0;
89
+ return;
90
+ }
91
+ if (event.type === "turn_end") {
92
+ session.providerTurn = event.text;
93
+ session.pendingEnd = event.at;
94
+ return;
95
+ }
96
+ if (event.type === "partial" || event.type === "final") {
97
+ if (!session.started) openTurn(session, session.pendingStart ?? session.lastAt);
98
+ session.segments.set(event.segmentId, event.text);
99
+ session.stream.push(event);
100
+ return;
101
+ }
102
+ if (event.type === "error") {
103
+ session.stream.push(event);
104
+ session.stopped = true;
105
+ return;
106
+ }
107
+ if (event.type === "done") session.pendingDone = event;
108
+ }
109
+ function absorb(session, events, update) {
110
+ if (session.stopped) return;
111
+ if (update?.speechStart !== void 0) session.pendingStart = update.speechStart;
112
+ if (update?.turnEnd !== void 0) session.pendingEnd = update.turnEnd;
113
+ const textual = events.some(
114
+ (event) => event.type === "partial" || event.type === "final" || event.type === "done"
115
+ );
116
+ if (session.pendingStart !== void 0 && (textual || events.length === 0)) {
117
+ openTurn(session, session.pendingStart);
118
+ }
119
+ for (const event of events) {
120
+ applyEvent(session, event);
121
+ if (session.stopped) return;
122
+ }
123
+ if (session.pendingEnd !== void 0) {
124
+ session.heldEnd = session.pendingEnd;
125
+ session.pendingEnd = void 0;
126
+ }
127
+ if (session.heldEnd !== void 0) closeTurn(session, session.heldEnd);
128
+ }
129
+ function finish(session) {
130
+ if (session.stopped) return;
131
+ if (session.pendingStart !== void 0) openTurn(session, session.pendingStart);
132
+ if (session.turnOpen) closeTurn(session, session.heldEnd ?? session.pendingEnd ?? session.lastAt);
133
+ if (session.pendingDone) session.stream.push(session.pendingDone);
134
+ else {
135
+ session.stream.push({
136
+ type: "error",
137
+ reason: "error",
138
+ error: "Transcription ended without a transcript"
139
+ });
140
+ }
141
+ }
142
+ function listenWithVad(listenModel, model, audio, options, vad = options?.vad ?? energyVad()) {
143
+ const stream = new TranscriptionEventStream();
144
+ void pump(listenModel, model, audio, options, stream, vad);
145
+ return stream;
146
+ }
147
+ async function pump(listenModel, model, audio, options, stream, vad) {
148
+ if (options?.signal?.aborted) {
149
+ stream.push({ type: "error", reason: "aborted", error: "aborted" });
150
+ return;
151
+ }
152
+ const gate = audioGate();
153
+ let inner;
154
+ try {
155
+ inner = listenModel(model, gate.source, options);
156
+ } catch (error) {
157
+ const message = error instanceof Error ? error.message : String(error);
158
+ stream.push({ type: "error", reason: "error", error: message });
159
+ return;
160
+ }
161
+ const inbox = [];
162
+ let readerClosed = false;
163
+ const reading = (async () => {
164
+ for await (const event of inner) inbox.push(event);
165
+ readerClosed = true;
166
+ })();
167
+ const session = {
168
+ stream,
169
+ segments: /* @__PURE__ */ new Map(),
170
+ closed: "",
171
+ turnOpen: false,
172
+ started: false,
173
+ stopped: false,
174
+ lastAt: 0
175
+ };
176
+ const take = (update) => {
177
+ const events = inbox.splice(0, inbox.length);
178
+ absorb(session, events, update);
179
+ };
180
+ try {
181
+ await tick();
182
+ take();
183
+ for await (const chunk of audio) {
184
+ if (session.stopped) break;
185
+ if (options?.signal?.aborted) {
186
+ gate.push(null);
187
+ await tick();
188
+ take();
189
+ if (!session.stopped) {
190
+ stream.push({ type: "error", reason: "aborted", error: "aborted" });
191
+ session.stopped = true;
192
+ }
193
+ break;
194
+ }
195
+ session.lastAt = chunk.timestamp;
196
+ const update = vad.push(chunk);
197
+ gate.push(chunk);
198
+ await tick();
199
+ take(update);
200
+ }
201
+ gate.push(null);
202
+ if (!session.stopped) {
203
+ let attempt = 0;
204
+ do {
205
+ await tick();
206
+ take();
207
+ attempt += 1;
208
+ } while (!readerClosed && attempt < 50);
209
+ finish(session);
210
+ } else await tick();
211
+ } catch (error) {
212
+ gate.push(null);
213
+ if (!session.stopped) {
214
+ const message = error instanceof Error ? error.message : String(error);
215
+ stream.push({ type: "error", reason: "error", error: message });
216
+ }
217
+ }
218
+ await reading;
219
+ }
220
+ export {
221
+ listenWithVad
222
+ };
223
+ //# sourceMappingURL=vad-listen.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/vad-listen.ts"],
4
+ "sourcesContent": ["import type { AudioChunk, AudioSource } from './audio.ts';\nimport type { TranscriptionModel } from './models.ts';\nimport {\n type ListenOptions,\n type TranscriptionEvent,\n TranscriptionEventStream,\n} from './transcription.ts';\nimport { energyVad, type VadHook, type VadUpdate } from './vad.ts';\n\ntype ListenModel = (\n model: TranscriptionModel<string>,\n audio: AudioSource,\n options?: ListenOptions\n) => TranscriptionEventStream;\n\ninterface Gate {\n source: AudioSource;\n push(chunk: AudioChunk | null): void;\n}\n\nfunction tick(): Promise<void> {\n return new Promise((resolve) => {\n setTimeout(resolve, 0);\n });\n}\n\nfunction audioGate(): Gate {\n const queued: { chunk: AudioChunk | null }[] = [];\n let resolveWait: ((chunk: AudioChunk | null) => void) | undefined;\n const source: AudioSource = {\n async *[Symbol.asyncIterator]() {\n while (true) {\n const ready = queued.shift();\n if (ready !== undefined) {\n if (ready.chunk === null) return;\n yield ready.chunk;\n continue;\n }\n const chunk = await new Promise<AudioChunk | null>((resolve) => {\n resolveWait = resolve;\n });\n if (chunk === null) return;\n yield chunk;\n }\n },\n };\n return {\n source,\n push(chunk) {\n if (resolveWait) {\n const resolve = resolveWait;\n resolveWait = undefined;\n resolve(chunk);\n return;\n }\n queued.push({ chunk });\n },\n };\n}\n\nfunction joinSegments(parts: string[]): string {\n let text = '';\n for (const part of parts) {\n if (text.length === 0 || /\\s$/.test(text) || /^\\s/.test(part)) text += part;\n else text += ` ${part}`;\n }\n return text;\n}\n\ninterface Session {\n stream: TranscriptionEventStream;\n segments: Map<string, string>;\n closed: string;\n turnOpen: boolean;\n started: boolean;\n pendingStart?: number;\n pendingEnd?: number;\n heldEnd?: number;\n providerTurn?: string;\n pendingDone?: Extract<TranscriptionEvent, { type: 'done' }>;\n stopped: boolean;\n lastAt: number;\n}\n\nfunction liveText(session: Session): string {\n const parts = [...session.segments.values()];\n if (parts.length > 0) return joinSegments(parts);\n return session.pendingDone?.transcript.text ?? session.providerTurn ?? '';\n}\n\nfunction turnText(session: Session): string {\n const live = liveText(session);\n if (session.closed.length > 0 && live.startsWith(session.closed)) {\n return live.slice(session.closed.length).trim();\n }\n if (session.providerTurn && !live.startsWith(session.providerTurn)) return session.providerTurn;\n return live.trim();\n}\n\nfunction closeTurn(session: Session, at: number) {\n if (!session.turnOpen || session.stopped) return;\n const text = turnText(session);\n if (text.length === 0) return;\n session.closed = liveText(session);\n session.turnOpen = false;\n session.started = false;\n session.pendingEnd = undefined;\n session.heldEnd = undefined;\n session.providerTurn = undefined;\n session.stream.push({ type: 'turn_end', text, at });\n}\n\nfunction openTurn(session: Session, at: number) {\n if (session.started || session.stopped) return;\n session.stream.push({ type: 'speech_start', at });\n session.started = true;\n session.turnOpen = true;\n session.pendingStart = undefined;\n}\n\nfunction applyEvent(session: Session, event: TranscriptionEvent) {\n if (event.type === 'speech_start') {\n if (!session.started) session.stream.push(event);\n session.started = true;\n session.turnOpen = true;\n session.pendingStart = undefined;\n return;\n }\n if (event.type === 'turn_end') {\n session.providerTurn = event.text;\n session.pendingEnd = event.at;\n return;\n }\n if (event.type === 'partial' || event.type === 'final') {\n if (!session.started) openTurn(session, session.pendingStart ?? session.lastAt);\n session.segments.set(event.segmentId, event.text);\n session.stream.push(event);\n return;\n }\n if (event.type === 'error') {\n session.stream.push(event);\n session.stopped = true;\n return;\n }\n if (event.type === 'done') session.pendingDone = event;\n}\n\nfunction absorb(session: Session, events: TranscriptionEvent[], update: VadUpdate | undefined) {\n if (session.stopped) return;\n if (update?.speechStart !== undefined) session.pendingStart = update.speechStart;\n if (update?.turnEnd !== undefined) session.pendingEnd = update.turnEnd;\n const textual = events.some(\n (event) => event.type === 'partial' || event.type === 'final' || event.type === 'done'\n );\n if (session.pendingStart !== undefined && (textual || events.length === 0)) {\n openTurn(session, session.pendingStart);\n }\n for (const event of events) {\n applyEvent(session, event);\n if (session.stopped) return;\n }\n if (session.pendingEnd !== undefined) {\n session.heldEnd = session.pendingEnd;\n session.pendingEnd = undefined;\n }\n if (session.heldEnd !== undefined) closeTurn(session, session.heldEnd);\n}\n\nfunction finish(session: Session) {\n if (session.stopped) return;\n if (session.pendingStart !== undefined) openTurn(session, session.pendingStart);\n if (session.turnOpen) closeTurn(session, session.heldEnd ?? session.pendingEnd ?? session.lastAt);\n if (session.pendingDone) session.stream.push(session.pendingDone);\n else {\n session.stream.push({\n type: 'error',\n reason: 'error',\n error: 'Transcription ended without a transcript',\n });\n }\n}\n\nexport function listenWithVad(\n listenModel: ListenModel,\n model: TranscriptionModel<string>,\n audio: AudioSource,\n options?: ListenOptions,\n vad: VadHook = options?.vad ?? energyVad()\n): TranscriptionEventStream {\n const stream = new TranscriptionEventStream();\n void pump(listenModel, model, audio, options, stream, vad);\n return stream;\n}\n\nasync function pump(\n listenModel: ListenModel,\n model: TranscriptionModel<string>,\n audio: AudioSource,\n options: ListenOptions | undefined,\n stream: TranscriptionEventStream,\n vad: VadHook\n) {\n if (options?.signal?.aborted) {\n stream.push({ type: 'error', reason: 'aborted', error: 'aborted' });\n return;\n }\n const gate = audioGate();\n let inner: TranscriptionEventStream;\n try {\n inner = listenModel(model, gate.source, options);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n stream.push({ type: 'error', reason: 'error', error: message });\n return;\n }\n const inbox: TranscriptionEvent[] = [];\n let readerClosed = false;\n const reading = (async () => {\n for await (const event of inner) inbox.push(event);\n readerClosed = true;\n })();\n const session: Session = {\n stream,\n segments: new Map(),\n closed: '',\n turnOpen: false,\n started: false,\n stopped: false,\n lastAt: 0,\n };\n const take = (update?: VadUpdate) => {\n const events = inbox.splice(0, inbox.length);\n absorb(session, events, update);\n };\n try {\n await tick();\n take();\n for await (const chunk of audio) {\n if (session.stopped) break;\n if (options?.signal?.aborted) {\n gate.push(null);\n await tick();\n take();\n if (!session.stopped) {\n stream.push({ type: 'error', reason: 'aborted', error: 'aborted' });\n session.stopped = true;\n }\n break;\n }\n session.lastAt = chunk.timestamp;\n const update = vad.push(chunk);\n gate.push(chunk);\n await tick();\n take(update);\n }\n gate.push(null);\n if (!session.stopped) {\n let attempt = 0;\n do {\n await tick();\n take();\n attempt += 1;\n } while (!readerClosed && attempt < 50);\n finish(session);\n } else await tick();\n } catch (error) {\n gate.push(null);\n if (!session.stopped) {\n const message = error instanceof Error ? error.message : String(error);\n stream.push({ type: 'error', reason: 'error', error: message });\n }\n }\n await reading;\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
package/dist/vad.js ADDED
@@ -0,0 +1,57 @@
1
+ function width(encoding) {
2
+ if (encoding === "pcm16") return 2;
3
+ if (encoding === "pcmf32") return 4;
4
+ return void 0;
5
+ }
6
+ function durationMs(chunk) {
7
+ const bytes = width(chunk.format.encoding);
8
+ if (bytes === void 0 || chunk.format.sampleRate <= 0 || chunk.format.channels <= 0) return 0;
9
+ const samples = chunk.data.byteLength / bytes / chunk.format.channels;
10
+ return samples / chunk.format.sampleRate * 1e3;
11
+ }
12
+ function rms(chunk) {
13
+ const bytes = width(chunk.format.encoding);
14
+ if (bytes === void 0) return void 0;
15
+ const samples = Math.floor(chunk.data.byteLength / bytes);
16
+ if (samples === 0) return 0;
17
+ const view = new DataView(chunk.data.buffer, chunk.data.byteOffset, chunk.data.byteLength);
18
+ let sum = 0;
19
+ for (let index = 0; index < samples; index += 1) {
20
+ const sample = bytes === 2 ? view.getInt16(index * 2, true) / 32768 : view.getFloat32(index * 4, true);
21
+ sum += sample * sample;
22
+ }
23
+ return Math.sqrt(sum / samples);
24
+ }
25
+ function energyVad(options = {}) {
26
+ const threshold = options.threshold ?? 0.02;
27
+ const silenceMs = options.silenceMs ?? 500;
28
+ let speaking = false;
29
+ let silence = 0;
30
+ return {
31
+ push(chunk) {
32
+ const level = rms(chunk);
33
+ if (level === void 0) {
34
+ if (chunk.data.byteLength === 0 || speaking) return void 0;
35
+ speaking = true;
36
+ return { speechStart: chunk.timestamp };
37
+ }
38
+ const duration = durationMs(chunk);
39
+ if (level >= threshold) {
40
+ silence = 0;
41
+ if (speaking) return void 0;
42
+ speaking = true;
43
+ return { speechStart: chunk.timestamp };
44
+ }
45
+ if (!speaking) return void 0;
46
+ silence += duration;
47
+ if (silence < silenceMs) return void 0;
48
+ speaking = false;
49
+ silence = 0;
50
+ return { turnEnd: chunk.timestamp + duration };
51
+ }
52
+ };
53
+ }
54
+ export {
55
+ energyVad
56
+ };
57
+ //# sourceMappingURL=vad.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/vad.ts"],
4
+ "sourcesContent": ["import type { AudioChunk } from './audio.ts';\n\nexport interface VadUpdate {\n speechStart?: number;\n turnEnd?: number;\n}\n\nexport interface VadHook {\n push(chunk: AudioChunk): VadUpdate | undefined;\n}\n\nexport interface EnergyVadOptions {\n threshold?: number;\n silenceMs?: number;\n}\n\nfunction width(encoding: AudioChunk['format']['encoding']): number | undefined {\n if (encoding === 'pcm16') return 2;\n if (encoding === 'pcmf32') return 4;\n return undefined;\n}\n\nfunction durationMs(chunk: AudioChunk): number {\n const bytes = width(chunk.format.encoding);\n if (bytes === undefined || chunk.format.sampleRate <= 0 || chunk.format.channels <= 0) return 0;\n const samples = chunk.data.byteLength / bytes / chunk.format.channels;\n return (samples / chunk.format.sampleRate) * 1000;\n}\n\nfunction rms(chunk: AudioChunk): number | undefined {\n const bytes = width(chunk.format.encoding);\n if (bytes === undefined) return undefined;\n const samples = Math.floor(chunk.data.byteLength / bytes);\n if (samples === 0) return 0;\n const view = new DataView(chunk.data.buffer, chunk.data.byteOffset, chunk.data.byteLength);\n let sum = 0;\n for (let index = 0; index < samples; index += 1) {\n const sample =\n bytes === 2 ? view.getInt16(index * 2, true) / 32768 : view.getFloat32(index * 4, true);\n sum += sample * sample;\n }\n return Math.sqrt(sum / samples);\n}\n\nexport function energyVad(options: EnergyVadOptions = {}): VadHook {\n const threshold = options.threshold ?? 0.02;\n const silenceMs = options.silenceMs ?? 500;\n let speaking = false;\n let silence = 0;\n return {\n push(chunk) {\n const level = rms(chunk);\n if (level === undefined) {\n if (chunk.data.byteLength === 0 || speaking) return undefined;\n speaking = true;\n return { speechStart: chunk.timestamp };\n }\n const duration = durationMs(chunk);\n if (level >= threshold) {\n silence = 0;\n if (speaking) return undefined;\n speaking = true;\n return { speechStart: chunk.timestamp };\n }\n if (!speaking) return undefined;\n silence += duration;\n if (silence < silenceMs) return undefined;\n speaking = false;\n silence = 0;\n return { turnEnd: chunk.timestamp + duration };\n },\n };\n}\n"],
5
+ "mappings": "AAgBA,SAAS,MAAM,UAAgE;AAC7E,MAAI,aAAa,QAAS,QAAO;AACjC,MAAI,aAAa,SAAU,QAAO;AAClC,SAAO;AACT;AAEA,SAAS,WAAW,OAA2B;AAC7C,QAAM,QAAQ,MAAM,MAAM,OAAO,QAAQ;AACzC,MAAI,UAAU,UAAa,MAAM,OAAO,cAAc,KAAK,MAAM,OAAO,YAAY,EAAG,QAAO;AAC9F,QAAM,UAAU,MAAM,KAAK,aAAa,QAAQ,MAAM,OAAO;AAC7D,SAAQ,UAAU,MAAM,OAAO,aAAc;AAC/C;AAEA,SAAS,IAAI,OAAuC;AAClD,QAAM,QAAQ,MAAM,MAAM,OAAO,QAAQ;AACzC,MAAI,UAAU,OAAW,QAAO;AAChC,QAAM,UAAU,KAAK,MAAM,MAAM,KAAK,aAAa,KAAK;AACxD,MAAI,YAAY,EAAG,QAAO;AAC1B,QAAM,OAAO,IAAI,SAAS,MAAM,KAAK,QAAQ,MAAM,KAAK,YAAY,MAAM,KAAK,UAAU;AACzF,MAAI,MAAM;AACV,WAAS,QAAQ,GAAG,QAAQ,SAAS,SAAS,GAAG;AAC/C,UAAM,SACJ,UAAU,IAAI,KAAK,SAAS,QAAQ,GAAG,IAAI,IAAI,QAAQ,KAAK,WAAW,QAAQ,GAAG,IAAI;AACxF,WAAO,SAAS;AAAA,EAClB;AACA,SAAO,KAAK,KAAK,MAAM,OAAO;AAChC;AAEO,SAAS,UAAU,UAA4B,CAAC,GAAY;AACjE,QAAM,YAAY,QAAQ,aAAa;AACvC,QAAM,YAAY,QAAQ,aAAa;AACvC,MAAI,WAAW;AACf,MAAI,UAAU;AACd,SAAO;AAAA,IACL,KAAK,OAAO;AACV,YAAM,QAAQ,IAAI,KAAK;AACvB,UAAI,UAAU,QAAW;AACvB,YAAI,MAAM,KAAK,eAAe,KAAK,SAAU,QAAO;AACpD,mBAAW;AACX,eAAO,EAAE,aAAa,MAAM,UAAU;AAAA,MACxC;AACA,YAAM,WAAW,WAAW,KAAK;AACjC,UAAI,SAAS,WAAW;AACtB,kBAAU;AACV,YAAI,SAAU,QAAO;AACrB,mBAAW;AACX,eAAO,EAAE,aAAa,MAAM,UAAU;AAAA,MACxC;AACA,UAAI,CAAC,SAAU,QAAO;AACtB,iBAAW;AACX,UAAI,UAAU,UAAW,QAAO;AAChC,iBAAW;AACX,gBAAU;AACV,aAAO,EAAE,SAAS,MAAM,YAAY,SAAS;AAAA,IAC/C;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/pi-audio",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Voice bindings for pi: speech-to-text, text-to-speech and realtime speech for pi-coding-agent plugins and slicc-agent",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",