@ai-ecoverse/pi-audio 1.3.0 → 1.5.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/index.js +2 -0
- package/dist/index.js.map +2 -2
- package/dist/openai/catalog.js +126 -0
- package/dist/openai/catalog.js.map +7 -0
- package/dist/openai/index.js +5 -0
- package/dist/openai/index.js.map +7 -0
- package/dist/openai/listen.js +200 -0
- package/dist/openai/listen.js.map +7 -0
- package/dist/openai/speak.js +91 -0
- package/dist/openai/speak.js.map +7 -0
- package/dist/openai/transcribe.js +112 -0
- package/dist/openai/transcribe.js.map +7 -0
- package/dist/openai/wire.js +201 -0
- package/dist/openai/wire.js.map +7 -0
- 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/index.d.ts +2 -0
- package/dist/types/openai/catalog.d.ts +1 -0
- package/dist/types/openai/index.d.ts +1 -0
- package/dist/types/openai/listen.d.ts +4 -0
- package/dist/types/openai/speak.d.ts +4 -0
- package/dist/types/openai/transcribe.d.ts +4 -0
- package/dist/types/openai/wire.d.ts +52 -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/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/package.json +5 -1
|
@@ -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;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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 @@
|
|
|
1
|
+
export declare function registerOpenAI(): () => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { registerOpenAI } from './catalog.ts';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AudioChunk } from '../audio.ts';
|
|
2
|
+
import type { TranscriptionModel } from '../models.ts';
|
|
3
|
+
import { type ListenOptions, TranscriptionEventStream } from '../transcription.ts';
|
|
4
|
+
export declare function listenOpenAI(model: TranscriptionModel<string>, audio: AsyncIterable<AudioChunk>, options?: ListenOptions): TranscriptionEventStream;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ProviderRequestOptions } from '@earendil-works/pi-ai';
|
|
2
|
+
import type { SpeechModel } from '../models.ts';
|
|
3
|
+
import { SpeechEventStream, type SpeechInput } from '../speech.ts';
|
|
4
|
+
export declare function speakOpenAI(model: SpeechModel<string>, input: SpeechInput, options?: ProviderRequestOptions): SpeechEventStream;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ProviderRequestOptions } from '@earendil-works/pi-ai';
|
|
2
|
+
import type { TranscriptionModel } from '../models.ts';
|
|
3
|
+
import type { AudioInput, Transcript } from '../transcription.ts';
|
|
4
|
+
export declare function transcribeOpenAI(model: TranscriptionModel<string>, input: AudioInput, options?: ProviderRequestOptions): Promise<Transcript>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ProviderRequestOptions } from '@earendil-works/pi-ai';
|
|
2
|
+
export interface AudioFile {
|
|
3
|
+
data: Uint8Array;
|
|
4
|
+
mimeType: string;
|
|
5
|
+
filename: string;
|
|
6
|
+
format: string;
|
|
7
|
+
}
|
|
8
|
+
export interface JsonBody {
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
interface HeaderModel {
|
|
12
|
+
provider: string;
|
|
13
|
+
headers?: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
export declare function isOpenRouter(model: {
|
|
16
|
+
provider: string;
|
|
17
|
+
baseUrl: string;
|
|
18
|
+
}): boolean;
|
|
19
|
+
export declare function endpoint(baseUrl: string, path: string): string;
|
|
20
|
+
export declare function realtimeUrl(baseUrl: string): string;
|
|
21
|
+
export declare function audioFile(mimeType: string): {
|
|
22
|
+
format: string;
|
|
23
|
+
filename: string;
|
|
24
|
+
};
|
|
25
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
26
|
+
export declare function stringField(record: Record<string, unknown>, key: string): string | undefined;
|
|
27
|
+
export declare function numberField(record: Record<string, unknown>, key: string): number | undefined;
|
|
28
|
+
export declare function asVoiceError(error: unknown): {
|
|
29
|
+
reason: 'aborted' | 'error';
|
|
30
|
+
error: string;
|
|
31
|
+
};
|
|
32
|
+
export declare function hasAuth(headers: Record<string, string | null> | undefined): boolean;
|
|
33
|
+
export declare function requestHeaders(model: HeaderModel, options: ProviderRequestOptions | undefined): Record<string, string>;
|
|
34
|
+
export declare function requestSignal(options: ProviderRequestOptions | undefined): AbortSignal | undefined;
|
|
35
|
+
export declare function encodeMultipart(file: AudioFile, fields: Record<string, string>): Uint8Array;
|
|
36
|
+
export declare function multipartType(): string;
|
|
37
|
+
export declare function readAudio(input: {
|
|
38
|
+
data: Uint8Array;
|
|
39
|
+
mimeType: string;
|
|
40
|
+
} | {
|
|
41
|
+
url: string;
|
|
42
|
+
}, fetchImpl: typeof fetch, signal: AbortSignal | undefined): Promise<{
|
|
43
|
+
data: Uint8Array;
|
|
44
|
+
mimeType: string;
|
|
45
|
+
}>;
|
|
46
|
+
export declare function audioPayload(file: AudioFile, openRouter: boolean, modelId: string): JsonBody;
|
|
47
|
+
export declare function applyPayload(payload: unknown, model: unknown, options: ProviderRequestOptions | undefined): Promise<unknown>;
|
|
48
|
+
export declare function voiceFetch(model: HeaderModel & {
|
|
49
|
+
id: string;
|
|
50
|
+
}, options: ProviderRequestOptions | undefined, url: string, body: BodyInit, contentType: string): Promise<Response>;
|
|
51
|
+
export declare function readJson(response: Response): Promise<unknown>;
|
|
52
|
+
export {};
|
|
@@ -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
|
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/pi-audio",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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",
|
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
"./worklet": {
|
|
34
34
|
"types": "./dist/types/worklet/convert.d.ts",
|
|
35
35
|
"default": "./dist/worklet/convert.js"
|
|
36
|
+
},
|
|
37
|
+
"./openai": {
|
|
38
|
+
"types": "./dist/types/openai/index.d.ts",
|
|
39
|
+
"default": "./dist/openai/index.js"
|
|
36
40
|
}
|
|
37
41
|
},
|
|
38
42
|
"files": [
|