@ai-ecoverse/pi-audio 1.2.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.
- package/README.md +2 -0
- package/dist/chunk-speech.js +135 -0
- package/dist/chunk-speech.js.map +7 -0
- package/dist/convert.js +294 -0
- package/dist/convert.js.map +7 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +2 -2
- package/dist/registry.js +4 -0
- package/dist/registry.js.map +2 -2
- package/dist/sentences.js +88 -0
- package/dist/sentences.js.map +7 -0
- package/dist/transcription.js.map +2 -2
- package/dist/types/chunk-speech.d.ts +4 -0
- package/dist/types/convert.d.ts +5 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/sentences.d.ts +5 -0
- package/dist/types/transcription.d.ts +2 -0
- package/dist/types/vad-listen.d.ts +7 -0
- package/dist/types/vad.d.ts +13 -0
- package/dist/types/worklet/convert.d.ts +30 -0
- package/dist/vad-listen.js +223 -0
- package/dist/vad-listen.js.map +7 -0
- package/dist/vad.js +57 -0
- package/dist/vad.js.map +7 -0
- package/dist/worklet/convert.js +80 -0
- package/dist/worklet/convert.js.map +7 -0
- package/package.json +5 -1
|
@@ -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;
|
|
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;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AudioChunk, AudioFormat, AudioSource } from './audio.ts';
|
|
2
|
+
export declare function encodeAudioBase64(data: Uint8Array): string;
|
|
3
|
+
export declare function decodeAudioBase64(value: string): Uint8Array;
|
|
4
|
+
export declare function convertAudio(chunk: AudioChunk, format: AudioFormat): AudioChunk;
|
|
5
|
+
export declare function convertAudioSource(source: AudioSource, format: AudioFormat): AudioSource;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export type { AudioChunk, AudioEncoding, AudioFormat, AudioSink, AudioSource, AudioTag, CharAlignment, TextSpan, } from './audio.ts';
|
|
2
2
|
export { resolveVoiceAuth } from './auth.ts';
|
|
3
3
|
export { blankUsage } from './blank-usage.ts';
|
|
4
|
+
export { convertAudio, convertAudioSource, decodeAudioBase64, encodeAudioBase64, } from './convert.ts';
|
|
4
5
|
export { VoiceRequestError } from './errors.ts';
|
|
5
6
|
export type { Delivery, DeliverySupport, KnownRealtimeApi, KnownSpeechApi, KnownTranscriptionApi, RealtimeApi, RealtimeModel, SpeechApi, SpeechModel, TranscriptionApi, TranscriptionModel, VoiceInfo, VoiceModel, VoiceModelType, } from './models.ts';
|
|
6
7
|
export { getVoiceModel, listVoiceModels, registerVoiceModel, } from './models.ts';
|
|
@@ -12,3 +13,5 @@ export type { AudioInput, ListenOptions, Transcript, TranscriptionApiProvider, T
|
|
|
12
13
|
export { TranscriptionEventStream } from './transcription.ts';
|
|
13
14
|
export type { AudioCost, AudioCounters, AudioUsage, RealtimeAudioCost, SpeechAudioCost, TranscriptionAudioCost, } from './usage.ts';
|
|
14
15
|
export { addAudioCounters, priceAudio } from './usage.ts';
|
|
16
|
+
export type { VadHook, VadUpdate } from './vad.ts';
|
|
17
|
+
export { energyVad } from './vad.ts';
|
|
@@ -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,30 @@
|
|
|
1
|
+
import type { AudioFormat } from '../audio.ts';
|
|
2
|
+
export declare const AUDIO_CONVERT_PROCESSOR = "pi-audio-convert";
|
|
3
|
+
export interface AudioConvertProcessorOptions {
|
|
4
|
+
processorOptions?: {
|
|
5
|
+
format?: AudioFormat;
|
|
6
|
+
sampleRate?: number;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
interface WorkletScope {
|
|
10
|
+
AudioWorkletProcessor?: new (options?: AudioConvertProcessorOptions) => {
|
|
11
|
+
port?: {
|
|
12
|
+
postMessage(message: unknown): void;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
registerProcessor?: (name: string, processor: new (options?: AudioConvertProcessorOptions) => object) => void;
|
|
16
|
+
}
|
|
17
|
+
declare const WorkletBase: new (options?: AudioConvertProcessorOptions) => {
|
|
18
|
+
port?: {
|
|
19
|
+
postMessage(message: unknown): void;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export declare class AudioConvertProcessor extends WorkletBase {
|
|
23
|
+
#private;
|
|
24
|
+
constructor(options?: AudioConvertProcessorOptions);
|
|
25
|
+
process(inputs: Array<ArrayLike<Float32Array | undefined> | undefined>): boolean;
|
|
26
|
+
take(): Uint8Array[];
|
|
27
|
+
}
|
|
28
|
+
export declare function registerAudioConvertWorklet(target?: WorkletScope): void;
|
|
29
|
+
export declare function audioConvertWorkletUrl(): string;
|
|
30
|
+
export {};
|
|
@@ -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
|
package/dist/vad.js.map
ADDED
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { convertAudio } from "../convert.js";
|
|
2
|
+
const AUDIO_CONVERT_PROCESSOR = "pi-audio-convert";
|
|
3
|
+
const scope = globalThis;
|
|
4
|
+
const WorkletBase = scope.AudioWorkletProcessor ?? class {
|
|
5
|
+
port;
|
|
6
|
+
};
|
|
7
|
+
class AudioConvertProcessor extends WorkletBase {
|
|
8
|
+
#format;
|
|
9
|
+
#sampleRate;
|
|
10
|
+
#frames = 0;
|
|
11
|
+
#pending = [];
|
|
12
|
+
constructor(options) {
|
|
13
|
+
super(options);
|
|
14
|
+
this.#format = options?.processorOptions?.format ?? {
|
|
15
|
+
encoding: "pcm16",
|
|
16
|
+
sampleRate: 24e3,
|
|
17
|
+
channels: 1
|
|
18
|
+
};
|
|
19
|
+
const sampleRate = options?.processorOptions?.sampleRate;
|
|
20
|
+
this.#sampleRate = sampleRate !== void 0 && Number.isFinite(sampleRate) && sampleRate > 0 ? sampleRate : 48e3;
|
|
21
|
+
}
|
|
22
|
+
process(inputs) {
|
|
23
|
+
const input = inputs[0];
|
|
24
|
+
const channels = [];
|
|
25
|
+
if (input) {
|
|
26
|
+
for (let index = 0; index < input.length; index++) {
|
|
27
|
+
const channel = input[index];
|
|
28
|
+
if (channel) channels.push(channel);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const frames = channels[0]?.length ?? 0;
|
|
32
|
+
if (channels.length === 0 || frames === 0) return true;
|
|
33
|
+
const count = Math.min(channels.length, 2);
|
|
34
|
+
const data = new Uint8Array(frames * count * 4);
|
|
35
|
+
const view = new DataView(data.buffer);
|
|
36
|
+
for (let frame = 0; frame < frames; frame++) {
|
|
37
|
+
for (let channel = 0; channel < count; channel++) {
|
|
38
|
+
view.setFloat32((frame * count + channel) * 4, channels[channel]?.[frame] ?? 0, true);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const converted = convertAudio(
|
|
42
|
+
{
|
|
43
|
+
data,
|
|
44
|
+
format: {
|
|
45
|
+
encoding: "pcmf32",
|
|
46
|
+
sampleRate: this.#sampleRate,
|
|
47
|
+
channels: count === 1 ? 1 : 2
|
|
48
|
+
},
|
|
49
|
+
timestamp: this.#frames / this.#sampleRate * 1e3
|
|
50
|
+
},
|
|
51
|
+
this.#format
|
|
52
|
+
);
|
|
53
|
+
this.#frames += frames;
|
|
54
|
+
this.#pending.push(converted.data);
|
|
55
|
+
this.port?.postMessage({
|
|
56
|
+
data: converted.data,
|
|
57
|
+
format: converted.format,
|
|
58
|
+
timestamp: converted.timestamp
|
|
59
|
+
});
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
take() {
|
|
63
|
+
return this.#pending.splice(0);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function registerAudioConvertWorklet(target = scope) {
|
|
67
|
+
if (typeof target.registerProcessor !== "function") return;
|
|
68
|
+
target.registerProcessor(AUDIO_CONVERT_PROCESSOR, AudioConvertProcessor);
|
|
69
|
+
}
|
|
70
|
+
function audioConvertWorkletUrl() {
|
|
71
|
+
return new URL(import.meta.url).href;
|
|
72
|
+
}
|
|
73
|
+
registerAudioConvertWorklet();
|
|
74
|
+
export {
|
|
75
|
+
AUDIO_CONVERT_PROCESSOR,
|
|
76
|
+
AudioConvertProcessor,
|
|
77
|
+
audioConvertWorkletUrl,
|
|
78
|
+
registerAudioConvertWorklet
|
|
79
|
+
};
|
|
80
|
+
//# sourceMappingURL=convert.js.map
|