@ai-ecoverse/pi-audio 1.23.0 → 1.25.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 +4 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +2 -2
- package/dist/mixer.js +337 -0
- package/dist/mixer.js.map +7 -0
- package/dist/models.js.map +2 -2
- package/dist/node-host.js +132 -0
- package/dist/node-host.js.map +7 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/mixer.d.ts +15 -0
- package/dist/types/models.d.ts +1 -1
- package/dist/types/node-host.d.ts +34 -0
- package/dist/types/wavespeed/catalog.d.ts +1 -0
- package/dist/types/wavespeed/index.d.ts +1 -0
- package/dist/types/wavespeed/speak.d.ts +4 -0
- package/dist/types/wavespeed/transcribe.d.ts +4 -0
- package/dist/types/wavespeed/wire.d.ts +25 -0
- package/dist/types/worklet/convert.d.ts +9 -6
- package/dist/types/worklet/release.d.ts +13 -0
- package/dist/types/worklet/tap.d.ts +25 -0
- package/dist/wavespeed/catalog.js +60 -0
- package/dist/wavespeed/catalog.js.map +7 -0
- package/dist/wavespeed/index.js +5 -0
- package/dist/wavespeed/index.js.map +7 -0
- package/dist/wavespeed/speak.js +66 -0
- package/dist/wavespeed/speak.js.map +7 -0
- package/dist/wavespeed/transcribe.js +85 -0
- package/dist/wavespeed/transcribe.js.map +7 -0
- package/dist/wavespeed/wire.js +138 -0
- package/dist/wavespeed/wire.js.map +7 -0
- package/dist/worklet/convert.js +23 -0
- package/dist/worklet/convert.js.map +2 -2
- package/dist/worklet/release.js +42 -0
- package/dist/worklet/release.js.map +7 -0
- package/dist/worklet/tap.js +97 -0
- package/dist/worklet/tap.js.map +7 -0
- package/package.json +16 -2
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/node-host.ts"],
|
|
4
|
+
"sourcesContent": ["import { PassThrough } from 'node:stream';\nimport { VoiceRequestError } from './errors.ts';\nimport { type Mixer, type OpenMixerOptions, openMixer } from './mixer.ts';\n\nexport type NodeAudioEngine = 'node-web-audio-api' | 'web-audio-api';\n\nexport interface NodeAudioModule {\n AudioContext: new (options?: Record<string, unknown>) => NodeAudioContext;\n AudioWorkletNode: NonNullable<OpenMixerOptions['workletNode']>;\n}\n\nexport interface NodeAudioContext {\n readonly state: AudioContextState;\n resume(): Promise<void>;\n close(): Promise<void>;\n readonly sampleRate: number;\n readonly currentTime: number;\n readonly destination: AudioDestinationNode;\n readonly audioWorklet: AudioWorklet;\n createGain(): GainNode;\n createBuffer(channels: number, length: number, sampleRate: number): AudioBuffer;\n createBufferSource(): AudioBufferSourceNode;\n decodeAudioData(buffer: ArrayBuffer): Promise<AudioBuffer>;\n}\n\nexport interface NodeAudioOptions {\n engine?: NodeAudioEngine | 'auto';\n sink?: 'none' | 'default';\n sampleRate?: number;\n load?: (engine: NodeAudioEngine) => Promise<NodeAudioModule>;\n}\n\nexport interface NodeAudioSession {\n readonly engine: NodeAudioEngine;\n readonly sink: 'none' | 'default';\n readonly context: BaseAudioContext;\n readonly out: Mixer;\n close(): Promise<void>;\n}\n\nconst TAP_NAME = 'pi-audio-tap.js';\n\nconst AUDIO_TAP_CLASSIC = [\n 'class PiAudioTap extends AudioWorkletProcessor {',\n ' constructor() {',\n ' super();',\n ' this.released = false;',\n ' this.posted = false;',\n ' var port = this.port;',\n ' if (port) {',\n ' port.onmessage = function (event) {',\n ' if (event.data && event.data.type === \"pi-audio-release\") this.released = true;',\n ' }.bind(this);',\n ' }',\n ' }',\n ' process(inputs, outputs) {',\n ' if (this.released) {',\n ' if (!this.posted) {',\n ' this.posted = true;',\n ' if (this.port) this.port.postMessage({ type: \"pi-audio-release\" });',\n ' }',\n ' return true;',\n ' }',\n ' var input = (inputs && inputs[0]) || [];',\n ' var output = (outputs && outputs[0]) || [];',\n ' for (var channel = 0; channel < output.length; channel++) {',\n ' var dst = output[channel];',\n ' if (!dst) continue;',\n ' var src = input[channel] || input[0];',\n ' if (!src) { dst.fill(0); continue; }',\n ' if (src.length === dst.length) dst.set(src);',\n ' else {',\n ' var count = Math.min(src.length, dst.length);',\n ' for (var frame = 0; frame < count; frame++) dst[frame] = src[frame];',\n ' }',\n ' }',\n ' var heard = input[0];',\n ' if (heard && heard.length && this.port) {',\n ' var copy = new Float32Array(heard.length);',\n ' copy.set(heard);',\n ' this.port.postMessage({ samples: copy });',\n ' }',\n ' return true;',\n ' }',\n '}',\n \"registerProcessor('pi-audio-tap', PiAudioTap);\",\n '',\n].join('\\n');\n\nexport async function loadNodeAudioEngine(engine: NodeAudioEngine): Promise<NodeAudioModule> {\n if (engine === 'node-web-audio-api') {\n return (await import('node-web-audio-api')) as unknown as NodeAudioModule;\n }\n return (await import('web-audio-api')) as unknown as NodeAudioModule;\n}\n\nexport async function openNodeAudio(options: NodeAudioOptions = {}): Promise<NodeAudioSession> {\n const engine = options.engine ?? 'auto';\n const sink = options.sink ?? 'none';\n const load = options.load ?? loadNodeAudioEngine;\n const names: NodeAudioEngine[] =\n engine === 'auto' ? ['node-web-audio-api', 'web-audio-api'] : [engine];\n const tapUrl = await writeTap();\n let failure: unknown;\n for (const name of names) {\n try {\n return await startEngine(name, sink, options.sampleRate, load, tapUrl);\n } catch (error) {\n failure = error;\n if (engine !== 'auto') break;\n }\n }\n throw new VoiceRequestError('error', failureText(failure));\n}\n\nasync function startEngine(\n engine: NodeAudioEngine,\n sink: 'none' | 'default',\n sampleRate: number | undefined,\n load: (engine: NodeAudioEngine) => Promise<NodeAudioModule>,\n tapUrl: string\n): Promise<NodeAudioSession> {\n const loaded = await load(engine);\n const built = await nodeContextOptions(engine, sink, sampleRate);\n const context = new loaded.AudioContext(built.options);\n try {\n const out = await openMixer(context as unknown as BaseAudioContext, {\n playback: 'webaudio',\n runtime: 'node',\n tapUrl,\n workletNode: loaded.AudioWorkletNode,\n });\n if (context.state === 'suspended') await context.resume();\n return {\n engine,\n sink,\n context: context as unknown as BaseAudioContext,\n out,\n close: () => closeSession(context, out, built.stream),\n };\n } catch (error) {\n await closeSession(context, undefined, built.stream);\n throw error;\n }\n}\n\nasync function nodeContextOptions(\n engine: NodeAudioEngine,\n sink: 'none' | 'default',\n sampleRate: number | undefined\n): Promise<{ options: Record<string, unknown>; stream?: PassThrough }> {\n const options: Record<string, unknown> = {};\n if (sampleRate !== undefined) options.sampleRate = sampleRate;\n if (sink === 'default') return { options };\n if (engine === 'node-web-audio-api') {\n options.sinkId = { type: 'none' };\n return { options };\n }\n const stream = new PassThrough();\n stream.resume();\n options.sinkId = stream;\n return { options, stream };\n}\n\nasync function closeSession(\n context: NodeAudioContext,\n out: Mixer | undefined,\n stream: PassThrough | undefined\n): Promise<void> {\n if (out) out.close();\n if (stream) stream.destroy();\n await context.close();\n}\n\nfunction failureText(error: unknown): string {\n if (error instanceof Error) return error.message;\n return 'Node audio engine is unavailable';\n}\n\nasync function writeTap(): Promise<string> {\n const { writeFileSync } = await import('node:fs');\n const { tmpdir } = await import('node:os');\n const { join } = await import('node:path');\n const file = join(tmpdir(), TAP_NAME);\n writeFileSync(file, AUDIO_TAP_CLASSIC);\n return file;\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,mBAAmB;AAC5B,SAAS,yBAAyB;AAClC,SAA4C,iBAAiB;AAsC7D,MAAM,WAAW;AAEjB,MAAM,oBAAoB;AAAA,EACxB;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;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;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;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,EAAE,KAAK,IAAI;AAEX,eAAsB,oBAAoB,QAAmD;AAC3F,MAAI,WAAW,sBAAsB;AACnC,WAAQ,MAAM,OAAO,oBAAoB;AAAA,EAC3C;AACA,SAAQ,MAAM,OAAO,eAAe;AACtC;AAEA,eAAsB,cAAc,UAA4B,CAAC,GAA8B;AAC7F,QAAM,SAAS,QAAQ,UAAU;AACjC,QAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAM,QACJ,WAAW,SAAS,CAAC,sBAAsB,eAAe,IAAI,CAAC,MAAM;AACvE,QAAM,SAAS,MAAM,SAAS;AAC9B,MAAI;AACJ,aAAW,QAAQ,OAAO;AACxB,QAAI;AACF,aAAO,MAAM,YAAY,MAAM,MAAM,QAAQ,YAAY,MAAM,MAAM;AAAA,IACvE,SAAS,OAAO;AACd,gBAAU;AACV,UAAI,WAAW,OAAQ;AAAA,IACzB;AAAA,EACF;AACA,QAAM,IAAI,kBAAkB,SAAS,YAAY,OAAO,CAAC;AAC3D;AAEA,eAAe,YACb,QACA,MACA,YACA,MACA,QAC2B;AAC3B,QAAM,SAAS,MAAM,KAAK,MAAM;AAChC,QAAM,QAAQ,MAAM,mBAAmB,QAAQ,MAAM,UAAU;AAC/D,QAAM,UAAU,IAAI,OAAO,aAAa,MAAM,OAAO;AACrD,MAAI;AACF,UAAM,MAAM,MAAM,UAAU,SAAwC;AAAA,MAClE,UAAU;AAAA,MACV,SAAS;AAAA,MACT;AAAA,MACA,aAAa,OAAO;AAAA,IACtB,CAAC;AACD,QAAI,QAAQ,UAAU,YAAa,OAAM,QAAQ,OAAO;AACxD,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO,MAAM,aAAa,SAAS,KAAK,MAAM,MAAM;AAAA,IACtD;AAAA,EACF,SAAS,OAAO;AACd,UAAM,aAAa,SAAS,QAAW,MAAM,MAAM;AACnD,UAAM;AAAA,EACR;AACF;AAEA,eAAe,mBACb,QACA,MACA,YACqE;AACrE,QAAM,UAAmC,CAAC;AAC1C,MAAI,eAAe,OAAW,SAAQ,aAAa;AACnD,MAAI,SAAS,UAAW,QAAO,EAAE,QAAQ;AACzC,MAAI,WAAW,sBAAsB;AACnC,YAAQ,SAAS,EAAE,MAAM,OAAO;AAChC,WAAO,EAAE,QAAQ;AAAA,EACnB;AACA,QAAM,SAAS,IAAI,YAAY;AAC/B,SAAO,OAAO;AACd,UAAQ,SAAS;AACjB,SAAO,EAAE,SAAS,OAAO;AAC3B;AAEA,eAAe,aACb,SACA,KACA,QACe;AACf,MAAI,IAAK,KAAI,MAAM;AACnB,MAAI,OAAQ,QAAO,QAAQ;AAC3B,QAAM,QAAQ,MAAM;AACtB;AAEA,SAAS,YAAY,OAAwB;AAC3C,MAAI,iBAAiB,MAAO,QAAO,MAAM;AACzC,SAAO;AACT;AAEA,eAAe,WAA4B;AACzC,QAAM,EAAE,cAAc,IAAI,MAAM,OAAO,SAAS;AAChD,QAAM,EAAE,OAAO,IAAI,MAAM,OAAO,SAAS;AACzC,QAAM,EAAE,KAAK,IAAI,MAAM,OAAO,WAAW;AACzC,QAAM,OAAO,KAAK,OAAO,GAAG,QAAQ;AACpC,gBAAc,MAAM,iBAAiB;AACrC,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -7,8 +7,12 @@ export { convertAudio, convertAudioSource, decodeAudioBase64, encodeAudioBase64,
|
|
|
7
7
|
export type { EchoCanceller, EchoControl, EchoMode, EchoReport, MicOpen } from './echo.ts';
|
|
8
8
|
export { assertNodeEcho, calibrateEcho, chooseCapture, echoRuntime, measureEcho, micOpen, openEchoMic, } from './echo.ts';
|
|
9
9
|
export { VoiceRequestError } from './errors.ts';
|
|
10
|
+
export type { Mixer, OpenMixerOptions } from './mixer.ts';
|
|
11
|
+
export { openMixer } from './mixer.ts';
|
|
10
12
|
export type { Delivery, DeliverySupport, KnownRealtimeApi, KnownSpeechApi, KnownTranscriptionApi, RealtimeApi, RealtimeModel, SpeechApi, SpeechModel, TranscriptionApi, TranscriptionModel, VoiceInfo, VoiceModel, VoiceModelType, } from './models.ts';
|
|
11
13
|
export { getVoiceModel, listVoiceModels, registerVoiceModel, } from './models.ts';
|
|
14
|
+
export type { NodeAudioEngine, NodeAudioModule, NodeAudioOptions, NodeAudioSession, } from './node-host.ts';
|
|
15
|
+
export { openNodeAudio } from './node-host.ts';
|
|
12
16
|
export type { DeliveryPlan, RealtimeApiProvider, RealtimeContext, RealtimeEvent, RealtimeOptions, RealtimeSession, RealtimeSocket, RealtimeToken, TruncationPlan, } from './realtime.ts';
|
|
13
17
|
export { planDelivery, planTruncation, RealtimeLedger, realtimeSocket } from './realtime.ts';
|
|
14
18
|
export { connectRealtime, listen, registerRealtimeApiProvider, registerSpeechApiProvider, registerTranscriptionApiProvider, speak, transcribe, } from './registry.ts';
|
|
@@ -29,3 +33,5 @@ export type { AudioCost, AudioCounters, AudioUsage, RealtimeAudioCost, SpeechAud
|
|
|
29
33
|
export { addAudioCounters, priceAudio } from './usage.ts';
|
|
30
34
|
export type { VadHook, VadUpdate } from './vad.ts';
|
|
31
35
|
export { energyVad } from './vad.ts';
|
|
36
|
+
export { bindWorkletNode, WORKLET_RELEASE } from './worklet/release.ts';
|
|
37
|
+
export { AUDIO_TAP_PROCESSOR, audioTapWorkletUrl } from './worklet/tap.ts';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AudioOut } from './soundscape.ts';
|
|
2
|
+
import { type WorkletNodeLike } from './worklet/release.ts';
|
|
3
|
+
export interface Mixer extends AudioOut {
|
|
4
|
+
close(): void;
|
|
5
|
+
}
|
|
6
|
+
export interface MixerWorkletNode extends WorkletNodeLike {
|
|
7
|
+
connect(destination: AudioNode): AudioNode;
|
|
8
|
+
}
|
|
9
|
+
export interface OpenMixerOptions {
|
|
10
|
+
playback?: 'webaudio' | 'rtc-loopback';
|
|
11
|
+
runtime?: 'node' | 'browser';
|
|
12
|
+
tapUrl?: string;
|
|
13
|
+
workletNode?: new (context: BaseAudioContext, name: string, options?: AudioWorkletNodeOptions) => MixerWorkletNode;
|
|
14
|
+
}
|
|
15
|
+
export declare function openMixer(context: BaseAudioContext, options?: OpenMixerOptions): Promise<Mixer>;
|
package/dist/types/models.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { BaseModel } from '@earendil-works/pi-ai';
|
|
2
2
|
import type { AudioFormat } from './audio.ts';
|
|
3
3
|
import type { RealtimeAudioCost, SpeechAudioCost, TranscriptionAudioCost } from './usage.ts';
|
|
4
|
-
export type KnownTranscriptionApi = 'openai-transcriptions' | 'openai-realtime-transcription' | 'xai-stt' | 'elevenlabs-scribe' | 'mistral-transcriptions' | 'fish-asr' | 'cloudflare-ai-run';
|
|
4
|
+
export type KnownTranscriptionApi = 'openai-transcriptions' | 'openai-realtime-transcription' | 'xai-stt' | 'elevenlabs-scribe' | 'mistral-transcriptions' | 'fish-asr' | 'cloudflare-ai-run' | 'wavespeed';
|
|
5
5
|
export type TranscriptionApi = KnownTranscriptionApi | (string & {});
|
|
6
6
|
export type KnownSpeechApi = 'openai-speech' | 'xai-tts' | 'elevenlabs-tts' | 'mistral-speech' | 'fish-tts' | 'google-tts' | 'cloudflare-ai-run' | 'wavespeed';
|
|
7
7
|
export type SpeechApi = KnownSpeechApi | (string & {});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type Mixer, type OpenMixerOptions } from './mixer.ts';
|
|
2
|
+
export type NodeAudioEngine = 'node-web-audio-api' | 'web-audio-api';
|
|
3
|
+
export interface NodeAudioModule {
|
|
4
|
+
AudioContext: new (options?: Record<string, unknown>) => NodeAudioContext;
|
|
5
|
+
AudioWorkletNode: NonNullable<OpenMixerOptions['workletNode']>;
|
|
6
|
+
}
|
|
7
|
+
export interface NodeAudioContext {
|
|
8
|
+
readonly state: AudioContextState;
|
|
9
|
+
resume(): Promise<void>;
|
|
10
|
+
close(): Promise<void>;
|
|
11
|
+
readonly sampleRate: number;
|
|
12
|
+
readonly currentTime: number;
|
|
13
|
+
readonly destination: AudioDestinationNode;
|
|
14
|
+
readonly audioWorklet: AudioWorklet;
|
|
15
|
+
createGain(): GainNode;
|
|
16
|
+
createBuffer(channels: number, length: number, sampleRate: number): AudioBuffer;
|
|
17
|
+
createBufferSource(): AudioBufferSourceNode;
|
|
18
|
+
decodeAudioData(buffer: ArrayBuffer): Promise<AudioBuffer>;
|
|
19
|
+
}
|
|
20
|
+
export interface NodeAudioOptions {
|
|
21
|
+
engine?: NodeAudioEngine | 'auto';
|
|
22
|
+
sink?: 'none' | 'default';
|
|
23
|
+
sampleRate?: number;
|
|
24
|
+
load?: (engine: NodeAudioEngine) => Promise<NodeAudioModule>;
|
|
25
|
+
}
|
|
26
|
+
export interface NodeAudioSession {
|
|
27
|
+
readonly engine: NodeAudioEngine;
|
|
28
|
+
readonly sink: 'none' | 'default';
|
|
29
|
+
readonly context: BaseAudioContext;
|
|
30
|
+
readonly out: Mixer;
|
|
31
|
+
close(): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
export declare function loadNodeAudioEngine(engine: NodeAudioEngine): Promise<NodeAudioModule>;
|
|
34
|
+
export declare function openNodeAudio(options?: NodeAudioOptions): Promise<NodeAudioSession>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function registerWaveSpeed(): () => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { registerWaveSpeed } from './catalog.ts';
|
|
@@ -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 speakWaveSpeed(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 transcribeWaveSpeed(model: TranscriptionModel<string>, input: AudioInput, options?: ProviderRequestOptions): Promise<Transcript>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ProviderRequestOptions } from '@earendil-works/pi-ai';
|
|
2
|
+
export declare function taskRecord(body: unknown): Record<string, unknown>;
|
|
3
|
+
export declare function errorText(text: string, status: number): string;
|
|
4
|
+
export declare function isHttpUrl(value: string): boolean;
|
|
5
|
+
export declare function outputText(output: unknown): string | undefined;
|
|
6
|
+
export declare function outputUrl(output: unknown): string | undefined;
|
|
7
|
+
export declare function downloadUrl(body: unknown): string;
|
|
8
|
+
export declare function durationSeconds(record: Record<string, unknown>): number;
|
|
9
|
+
declare function waveFetch(model: {
|
|
10
|
+
provider: string;
|
|
11
|
+
headers?: Record<string, string>;
|
|
12
|
+
id: string;
|
|
13
|
+
}, options: ProviderRequestOptions | undefined, url: string, body: BodyInit | undefined, contentType: string | undefined): Promise<unknown>;
|
|
14
|
+
export declare function finishTask(model: {
|
|
15
|
+
provider: string;
|
|
16
|
+
baseUrl: string;
|
|
17
|
+
headers?: Record<string, string>;
|
|
18
|
+
id: string;
|
|
19
|
+
}, options: ProviderRequestOptions | undefined, payload: unknown, rejected: string): Promise<Record<string, unknown>>;
|
|
20
|
+
export declare function readHttp(model: {
|
|
21
|
+
provider: string;
|
|
22
|
+
headers?: Record<string, string>;
|
|
23
|
+
id: string;
|
|
24
|
+
}, options: ProviderRequestOptions | undefined, url: string): Promise<Response>;
|
|
25
|
+
export { waveFetch };
|
|
@@ -6,22 +6,25 @@ export interface AudioConvertProcessorOptions {
|
|
|
6
6
|
sampleRate?: number;
|
|
7
7
|
};
|
|
8
8
|
}
|
|
9
|
+
interface WorkletPort {
|
|
10
|
+
postMessage(message: unknown): void;
|
|
11
|
+
onmessage: ((event: {
|
|
12
|
+
data?: unknown;
|
|
13
|
+
}) => void) | null;
|
|
14
|
+
}
|
|
9
15
|
interface WorkletScope {
|
|
10
16
|
AudioWorkletProcessor?: new (options?: AudioConvertProcessorOptions) => {
|
|
11
|
-
port?:
|
|
12
|
-
postMessage(message: unknown): void;
|
|
13
|
-
};
|
|
17
|
+
port?: WorkletPort;
|
|
14
18
|
};
|
|
15
19
|
registerProcessor?: (name: string, processor: new (options?: AudioConvertProcessorOptions) => object) => void;
|
|
16
20
|
}
|
|
17
21
|
declare const WorkletBase: new (options?: AudioConvertProcessorOptions) => {
|
|
18
|
-
port?:
|
|
19
|
-
postMessage(message: unknown): void;
|
|
20
|
-
};
|
|
22
|
+
port?: WorkletPort;
|
|
21
23
|
};
|
|
22
24
|
export declare class AudioConvertProcessor extends WorkletBase {
|
|
23
25
|
#private;
|
|
24
26
|
constructor(options?: AudioConvertProcessorOptions);
|
|
27
|
+
release(): void;
|
|
25
28
|
process(inputs: Array<ArrayLike<Float32Array | undefined> | undefined>): boolean;
|
|
26
29
|
take(): Uint8Array[];
|
|
27
30
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const WORKLET_RELEASE = "pi-audio-release";
|
|
2
|
+
export interface WorkletPort {
|
|
3
|
+
postMessage(message: unknown): void;
|
|
4
|
+
onmessage: ((event: {
|
|
5
|
+
data?: unknown;
|
|
6
|
+
}) => void) | null;
|
|
7
|
+
}
|
|
8
|
+
export interface WorkletNodeLike {
|
|
9
|
+
disconnect(): void;
|
|
10
|
+
port: WorkletPort;
|
|
11
|
+
}
|
|
12
|
+
export declare function isWorkletRelease(data: unknown): boolean;
|
|
13
|
+
export declare function bindWorkletNode(node: WorkletNodeLike): () => void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const AUDIO_TAP_PROCESSOR = "pi-audio-tap";
|
|
2
|
+
interface TapPort {
|
|
3
|
+
postMessage(message: unknown): void;
|
|
4
|
+
onmessage: ((event: {
|
|
5
|
+
data?: unknown;
|
|
6
|
+
}) => void) | null;
|
|
7
|
+
}
|
|
8
|
+
interface WorkletScope {
|
|
9
|
+
AudioWorkletProcessor?: new () => {
|
|
10
|
+
port?: TapPort;
|
|
11
|
+
};
|
|
12
|
+
registerProcessor?: (name: string, processor: new () => object) => void;
|
|
13
|
+
}
|
|
14
|
+
interface TapInputs {
|
|
15
|
+
inputs: Array<ArrayLike<Float32Array | undefined> | undefined>;
|
|
16
|
+
outputs: Array<ArrayLike<Float32Array | undefined> | undefined>;
|
|
17
|
+
}
|
|
18
|
+
export declare function copyTap(inputs: TapInputs['inputs'], outputs: TapInputs['outputs']): void;
|
|
19
|
+
export declare function tapSamples(inputs: TapInputs['inputs']): Float32Array | undefined;
|
|
20
|
+
export declare function registerAudioTapWorklet(target?: WorkletScope): void;
|
|
21
|
+
export declare function audioTapWorkletUrl(): string;
|
|
22
|
+
export declare function loadAudioTap(worklet: {
|
|
23
|
+
addModule(module: unknown): Promise<void>;
|
|
24
|
+
}, url?: string): Promise<void>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { registerVoiceModel } from "../models.js";
|
|
2
|
+
import { registerSpeechApiProvider, registerTranscriptionApiProvider } from "../registry.js";
|
|
3
|
+
import { speakWaveSpeed } from "./speak.js";
|
|
4
|
+
import { transcribeWaveSpeed } from "./transcribe.js";
|
|
5
|
+
const tokenCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
6
|
+
const wavespeed = "https://api.wavespeed.ai/api/v3";
|
|
7
|
+
const format = (encoding, sampleRate) => ({
|
|
8
|
+
encoding,
|
|
9
|
+
sampleRate,
|
|
10
|
+
channels: 1
|
|
11
|
+
});
|
|
12
|
+
function registerWaveSpeed() {
|
|
13
|
+
const whisper = {
|
|
14
|
+
id: "wavespeed-ai/openai-whisper",
|
|
15
|
+
name: "wavespeed-ai/openai-whisper",
|
|
16
|
+
api: "wavespeed",
|
|
17
|
+
provider: "wavespeed",
|
|
18
|
+
baseUrl: wavespeed,
|
|
19
|
+
type: "transcription",
|
|
20
|
+
streaming: false,
|
|
21
|
+
turnDetection: "none",
|
|
22
|
+
inputFormats: [format("wav", 16e3), format("mp3", 16e3), format("flac", 16e3)],
|
|
23
|
+
cost: tokenCost,
|
|
24
|
+
audioCost: { perInputMinute: 0.06 }
|
|
25
|
+
};
|
|
26
|
+
const speech = {
|
|
27
|
+
id: "minimax/speech-2.6-turbo",
|
|
28
|
+
name: "minimax/speech-2.6-turbo",
|
|
29
|
+
api: "wavespeed",
|
|
30
|
+
provider: "wavespeed",
|
|
31
|
+
baseUrl: wavespeed,
|
|
32
|
+
type: "speech",
|
|
33
|
+
textStreaming: false,
|
|
34
|
+
outputFormats: [format("mp3", 24e3)],
|
|
35
|
+
cloning: true,
|
|
36
|
+
cost: tokenCost,
|
|
37
|
+
audioCost: { perThousandChars: 0.06 }
|
|
38
|
+
};
|
|
39
|
+
const unregisterModels = [registerVoiceModel(whisper), registerVoiceModel(speech)];
|
|
40
|
+
const unregisterTranscription = registerTranscriptionApiProvider({
|
|
41
|
+
api: "wavespeed",
|
|
42
|
+
transcribe: transcribeWaveSpeed
|
|
43
|
+
});
|
|
44
|
+
const unregisterSpeech = registerSpeechApiProvider({
|
|
45
|
+
api: "wavespeed",
|
|
46
|
+
speak: speakWaveSpeed
|
|
47
|
+
});
|
|
48
|
+
let released = false;
|
|
49
|
+
return () => {
|
|
50
|
+
if (released) return;
|
|
51
|
+
released = true;
|
|
52
|
+
for (const unregister of unregisterModels) unregister();
|
|
53
|
+
unregisterTranscription();
|
|
54
|
+
unregisterSpeech();
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
registerWaveSpeed
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=catalog.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/wavespeed/catalog.ts"],
|
|
4
|
+
"sourcesContent": ["import type { AudioFormat } from '../audio.ts';\nimport { registerVoiceModel, type SpeechModel, type TranscriptionModel } from '../models.ts';\nimport { registerSpeechApiProvider, registerTranscriptionApiProvider } from '../registry.ts';\nimport { speakWaveSpeed } from './speak.ts';\nimport { transcribeWaveSpeed } from './transcribe.ts';\n\nconst tokenCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };\nconst wavespeed = 'https://api.wavespeed.ai/api/v3';\n\nconst format = (encoding: 'wav' | 'mp3' | 'flac', sampleRate: number): AudioFormat => ({\n encoding,\n sampleRate,\n channels: 1,\n});\n\nexport function registerWaveSpeed(): () => void {\n const whisper: TranscriptionModel<string> = {\n id: 'wavespeed-ai/openai-whisper',\n name: 'wavespeed-ai/openai-whisper',\n api: 'wavespeed',\n provider: 'wavespeed',\n baseUrl: wavespeed,\n type: 'transcription',\n streaming: false,\n turnDetection: 'none',\n inputFormats: [format('wav', 16000), format('mp3', 16000), format('flac', 16000)],\n cost: tokenCost,\n audioCost: { perInputMinute: 0.06 },\n };\n const speech: SpeechModel<string> = {\n id: 'minimax/speech-2.6-turbo',\n name: 'minimax/speech-2.6-turbo',\n api: 'wavespeed',\n provider: 'wavespeed',\n baseUrl: wavespeed,\n type: 'speech',\n textStreaming: false,\n outputFormats: [format('mp3', 24000)],\n cloning: true,\n cost: tokenCost,\n audioCost: { perThousandChars: 0.06 },\n };\n const unregisterModels = [registerVoiceModel(whisper), registerVoiceModel(speech)];\n const unregisterTranscription = registerTranscriptionApiProvider({\n api: 'wavespeed',\n transcribe: transcribeWaveSpeed,\n });\n const unregisterSpeech = registerSpeechApiProvider({\n api: 'wavespeed',\n speak: speakWaveSpeed,\n });\n let released = false;\n return () => {\n if (released) return;\n released = true;\n for (const unregister of unregisterModels) unregister();\n unregisterTranscription();\n unregisterSpeech();\n };\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,0BAAqE;AAC9E,SAAS,2BAA2B,wCAAwC;AAC5E,SAAS,sBAAsB;AAC/B,SAAS,2BAA2B;AAEpC,MAAM,YAAY,EAAE,OAAO,GAAG,QAAQ,GAAG,WAAW,GAAG,YAAY,EAAE;AACrE,MAAM,YAAY;AAElB,MAAM,SAAS,CAAC,UAAkC,gBAAqC;AAAA,EACrF;AAAA,EACA;AAAA,EACA,UAAU;AACZ;AAEO,SAAS,oBAAgC;AAC9C,QAAM,UAAsC;AAAA,IAC1C,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,KAAK;AAAA,IACL,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,WAAW;AAAA,IACX,eAAe;AAAA,IACf,cAAc,CAAC,OAAO,OAAO,IAAK,GAAG,OAAO,OAAO,IAAK,GAAG,OAAO,QAAQ,IAAK,CAAC;AAAA,IAChF,MAAM;AAAA,IACN,WAAW,EAAE,gBAAgB,KAAK;AAAA,EACpC;AACA,QAAM,SAA8B;AAAA,IAClC,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,KAAK;AAAA,IACL,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,eAAe;AAAA,IACf,eAAe,CAAC,OAAO,OAAO,IAAK,CAAC;AAAA,IACpC,SAAS;AAAA,IACT,MAAM;AAAA,IACN,WAAW,EAAE,kBAAkB,KAAK;AAAA,EACtC;AACA,QAAM,mBAAmB,CAAC,mBAAmB,OAAO,GAAG,mBAAmB,MAAM,CAAC;AACjF,QAAM,0BAA0B,iCAAiC;AAAA,IAC/D,KAAK;AAAA,IACL,YAAY;AAAA,EACd,CAAC;AACD,QAAM,mBAAmB,0BAA0B;AAAA,IACjD,KAAK;AAAA,IACL,OAAO;AAAA,EACT,CAAC;AACD,MAAI,WAAW;AACf,SAAO,MAAM;AACX,QAAI,SAAU;AACd,eAAW;AACX,eAAW,cAAc,iBAAkB,YAAW;AACtD,4BAAwB;AACxB,qBAAiB;AAAA,EACnB;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { asVoiceError } from "../openai/wire.js";
|
|
2
|
+
import { SpeechEventStream } from "../speech.js";
|
|
3
|
+
import { priceAudio } from "../usage.js";
|
|
4
|
+
import { finishTask, outputUrl, readHttp } from "./wire.js";
|
|
5
|
+
function resolveFormat(model, requested) {
|
|
6
|
+
const fallback = model.outputFormats[0];
|
|
7
|
+
if (!fallback) throw new Error("No output format");
|
|
8
|
+
if (!requested) return fallback;
|
|
9
|
+
const match = model.outputFormats.find(
|
|
10
|
+
(format) => format.encoding === requested.encoding && format.sampleRate === requested.sampleRate && format.channels === requested.channels
|
|
11
|
+
);
|
|
12
|
+
if (!match) throw new Error(`Unsupported speech format ${requested.encoding}`);
|
|
13
|
+
return match;
|
|
14
|
+
}
|
|
15
|
+
async function readText(text, signal) {
|
|
16
|
+
if (typeof text === "string") return text;
|
|
17
|
+
let combined = "";
|
|
18
|
+
for await (const part of text) {
|
|
19
|
+
if (signal?.aborted) throw new DOMException("aborted", "AbortError");
|
|
20
|
+
combined += part;
|
|
21
|
+
}
|
|
22
|
+
return combined;
|
|
23
|
+
}
|
|
24
|
+
async function audioOf(model, options, output) {
|
|
25
|
+
const url = outputUrl(output);
|
|
26
|
+
if (url === void 0) throw new Error("Speech response had no audio");
|
|
27
|
+
const response = await readHttp(model, options, url);
|
|
28
|
+
const data = new Uint8Array(await response.arrayBuffer());
|
|
29
|
+
if (data.byteLength === 0) throw new Error("Speech response had no audio");
|
|
30
|
+
return data;
|
|
31
|
+
}
|
|
32
|
+
function speakWaveSpeed(model, input, options) {
|
|
33
|
+
const stream = new SpeechEventStream();
|
|
34
|
+
void pumpSpeech(model, input, options, stream);
|
|
35
|
+
return stream;
|
|
36
|
+
}
|
|
37
|
+
async function pumpSpeech(model, input, options, stream) {
|
|
38
|
+
try {
|
|
39
|
+
if (options?.signal?.aborted) throw new DOMException("aborted", "AbortError");
|
|
40
|
+
const format = resolveFormat(model, input.format);
|
|
41
|
+
const text = await readText(input.text, options?.signal);
|
|
42
|
+
const payload = { text, voice_id: input.voice, format: "mp3" };
|
|
43
|
+
if (input.language !== void 0) payload.language_boost = input.language;
|
|
44
|
+
const record = await finishTask(model, options, payload, "Speech payload was not JSON");
|
|
45
|
+
const outputs = record.outputs;
|
|
46
|
+
const first = Array.isArray(outputs) ? outputs[0] : void 0;
|
|
47
|
+
const data = await audioOf(model, options, first);
|
|
48
|
+
stream.push({ type: "start", format });
|
|
49
|
+
stream.push({
|
|
50
|
+
type: "audio",
|
|
51
|
+
chunk: { data, format, timestamp: 0 },
|
|
52
|
+
span: { itemId: model.id, start: 0, end: text.length }
|
|
53
|
+
});
|
|
54
|
+
stream.push({
|
|
55
|
+
type: "done",
|
|
56
|
+
usage: priceAudio(model.audioCost, { characters: text.length })
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const voice = asVoiceError(error);
|
|
60
|
+
stream.push({ type: "error", reason: voice.reason, error: voice.error });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
speakWaveSpeed
|
|
65
|
+
};
|
|
66
|
+
//# sourceMappingURL=speak.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/wavespeed/speak.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ProviderRequestOptions } from '@earendil-works/pi-ai';\nimport type { AudioFormat } from '../audio.ts';\nimport type { SpeechModel } from '../models.ts';\nimport { asVoiceError } from '../openai/wire.ts';\nimport { SpeechEventStream, type SpeechInput } from '../speech.ts';\nimport { priceAudio } from '../usage.ts';\nimport { finishTask, outputUrl, readHttp } from './wire.ts';\n\nfunction resolveFormat(\n model: SpeechModel<string>,\n requested: AudioFormat | undefined\n): AudioFormat {\n const fallback = model.outputFormats[0];\n if (!fallback) throw new Error('No output format');\n if (!requested) return fallback;\n const match = model.outputFormats.find(\n (format) =>\n format.encoding === requested.encoding &&\n format.sampleRate === requested.sampleRate &&\n format.channels === requested.channels\n );\n if (!match) throw new Error(`Unsupported speech format ${requested.encoding}`);\n return match;\n}\n\nasync function readText(\n text: string | AsyncIterable<string>,\n signal: AbortSignal | undefined\n): Promise<string> {\n if (typeof text === 'string') return text;\n let combined = '';\n for await (const part of text) {\n if (signal?.aborted) throw new DOMException('aborted', 'AbortError');\n combined += part;\n }\n return combined;\n}\n\nasync function audioOf(\n model: SpeechModel<string>,\n options: ProviderRequestOptions | undefined,\n output: unknown\n): Promise<Uint8Array> {\n const url = outputUrl(output);\n if (url === undefined) throw new Error('Speech response had no audio');\n const response = await readHttp(model, options, url);\n const data = new Uint8Array(await response.arrayBuffer());\n if (data.byteLength === 0) throw new Error('Speech response had no audio');\n return data;\n}\n\nexport function speakWaveSpeed(\n model: SpeechModel<string>,\n input: SpeechInput,\n options?: ProviderRequestOptions\n): SpeechEventStream {\n const stream = new SpeechEventStream();\n void pumpSpeech(model, input, options, stream);\n return stream;\n}\n\nasync function pumpSpeech(\n model: SpeechModel<string>,\n input: SpeechInput,\n options: ProviderRequestOptions | undefined,\n stream: SpeechEventStream\n) {\n try {\n if (options?.signal?.aborted) throw new DOMException('aborted', 'AbortError');\n const format = resolveFormat(model, input.format);\n const text = await readText(input.text, options?.signal);\n const payload: Record<string, unknown> = { text, voice_id: input.voice, format: 'mp3' };\n if (input.language !== undefined) payload.language_boost = input.language;\n const record = await finishTask(model, options, payload, 'Speech payload was not JSON');\n const outputs = record.outputs;\n const first = Array.isArray(outputs) ? outputs[0] : undefined;\n const data = await audioOf(model, options, first);\n stream.push({ type: 'start', format });\n stream.push({\n type: 'audio',\n chunk: { data, format, timestamp: 0 },\n span: { itemId: model.id, start: 0, end: text.length },\n });\n stream.push({\n type: 'done',\n usage: priceAudio(model.audioCost, { characters: text.length }),\n });\n } catch (error) {\n const voice = asVoiceError(error);\n stream.push({ type: 'error', reason: voice.reason, error: voice.error });\n }\n}\n"],
|
|
5
|
+
"mappings": "AAGA,SAAS,oBAAoB;AAC7B,SAAS,yBAA2C;AACpD,SAAS,kBAAkB;AAC3B,SAAS,YAAY,WAAW,gBAAgB;AAEhD,SAAS,cACP,OACA,WACa;AACb,QAAM,WAAW,MAAM,cAAc,CAAC;AACtC,MAAI,CAAC,SAAU,OAAM,IAAI,MAAM,kBAAkB;AACjD,MAAI,CAAC,UAAW,QAAO;AACvB,QAAM,QAAQ,MAAM,cAAc;AAAA,IAChC,CAAC,WACC,OAAO,aAAa,UAAU,YAC9B,OAAO,eAAe,UAAU,cAChC,OAAO,aAAa,UAAU;AAAA,EAClC;AACA,MAAI,CAAC,MAAO,OAAM,IAAI,MAAM,6BAA6B,UAAU,QAAQ,EAAE;AAC7E,SAAO;AACT;AAEA,eAAe,SACb,MACA,QACiB;AACjB,MAAI,OAAO,SAAS,SAAU,QAAO;AACrC,MAAI,WAAW;AACf,mBAAiB,QAAQ,MAAM;AAC7B,QAAI,QAAQ,QAAS,OAAM,IAAI,aAAa,WAAW,YAAY;AACnE,gBAAY;AAAA,EACd;AACA,SAAO;AACT;AAEA,eAAe,QACb,OACA,SACA,QACqB;AACrB,QAAM,MAAM,UAAU,MAAM;AAC5B,MAAI,QAAQ,OAAW,OAAM,IAAI,MAAM,8BAA8B;AACrE,QAAM,WAAW,MAAM,SAAS,OAAO,SAAS,GAAG;AACnD,QAAM,OAAO,IAAI,WAAW,MAAM,SAAS,YAAY,CAAC;AACxD,MAAI,KAAK,eAAe,EAAG,OAAM,IAAI,MAAM,8BAA8B;AACzE,SAAO;AACT;AAEO,SAAS,eACd,OACA,OACA,SACmB;AACnB,QAAM,SAAS,IAAI,kBAAkB;AACrC,OAAK,WAAW,OAAO,OAAO,SAAS,MAAM;AAC7C,SAAO;AACT;AAEA,eAAe,WACb,OACA,OACA,SACA,QACA;AACA,MAAI;AACF,QAAI,SAAS,QAAQ,QAAS,OAAM,IAAI,aAAa,WAAW,YAAY;AAC5E,UAAM,SAAS,cAAc,OAAO,MAAM,MAAM;AAChD,UAAM,OAAO,MAAM,SAAS,MAAM,MAAM,SAAS,MAAM;AACvD,UAAM,UAAmC,EAAE,MAAM,UAAU,MAAM,OAAO,QAAQ,MAAM;AACtF,QAAI,MAAM,aAAa,OAAW,SAAQ,iBAAiB,MAAM;AACjE,UAAM,SAAS,MAAM,WAAW,OAAO,SAAS,SAAS,6BAA6B;AACtF,UAAM,UAAU,OAAO;AACvB,UAAM,QAAQ,MAAM,QAAQ,OAAO,IAAI,QAAQ,CAAC,IAAI;AACpD,UAAM,OAAO,MAAM,QAAQ,OAAO,SAAS,KAAK;AAChD,WAAO,KAAK,EAAE,MAAM,SAAS,OAAO,CAAC;AACrC,WAAO,KAAK;AAAA,MACV,MAAM;AAAA,MACN,OAAO,EAAE,MAAM,QAAQ,WAAW,EAAE;AAAA,MACpC,MAAM,EAAE,QAAQ,MAAM,IAAI,OAAO,GAAG,KAAK,KAAK,OAAO;AAAA,IACvD,CAAC;AACD,WAAO,KAAK;AAAA,MACV,MAAM;AAAA,MACN,OAAO,WAAW,MAAM,WAAW,EAAE,YAAY,KAAK,OAAO,CAAC;AAAA,IAChE,CAAC;AAAA,EACH,SAAS,OAAO;AACd,UAAM,QAAQ,aAAa,KAAK;AAChC,WAAO,KAAK,EAAE,MAAM,SAAS,QAAQ,MAAM,QAAQ,OAAO,MAAM,MAAM,CAAC;AAAA,EACzE;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { VoiceRequestError } from "../errors.js";
|
|
2
|
+
import {
|
|
3
|
+
asVoiceError,
|
|
4
|
+
audioFile,
|
|
5
|
+
encodeMultipart,
|
|
6
|
+
endpoint,
|
|
7
|
+
multipartType
|
|
8
|
+
} from "../openai/wire.js";
|
|
9
|
+
import { priceAudio } from "../usage.js";
|
|
10
|
+
import {
|
|
11
|
+
downloadUrl,
|
|
12
|
+
durationSeconds,
|
|
13
|
+
finishTask,
|
|
14
|
+
isHttpUrl,
|
|
15
|
+
outputText,
|
|
16
|
+
readHttp,
|
|
17
|
+
waveFetch
|
|
18
|
+
} from "./wire.js";
|
|
19
|
+
async function sourceUrl(model, input, options) {
|
|
20
|
+
if ("url" in input) return input.url;
|
|
21
|
+
const named = audioFile(input.mimeType);
|
|
22
|
+
const encoded = encodeMultipart(
|
|
23
|
+
{ data: input.data, mimeType: input.mimeType, filename: named.filename, format: named.format },
|
|
24
|
+
{}
|
|
25
|
+
);
|
|
26
|
+
const copy = new Uint8Array(encoded.byteLength);
|
|
27
|
+
copy.set(encoded);
|
|
28
|
+
const parsed = await waveFetch(
|
|
29
|
+
model,
|
|
30
|
+
options,
|
|
31
|
+
endpoint(model.baseUrl, "media/upload/binary"),
|
|
32
|
+
copy.buffer,
|
|
33
|
+
multipartType()
|
|
34
|
+
);
|
|
35
|
+
return downloadUrl(parsed);
|
|
36
|
+
}
|
|
37
|
+
async function textOf(model, options, output) {
|
|
38
|
+
if (typeof output === "string" && isHttpUrl(output)) {
|
|
39
|
+
const response = await readHttp(model, options, output);
|
|
40
|
+
const text2 = await response.text();
|
|
41
|
+
if (text2 === "") throw new Error("Transcription response had no text");
|
|
42
|
+
return text2;
|
|
43
|
+
}
|
|
44
|
+
const text = outputText(output);
|
|
45
|
+
if (text === void 0 || text === "" || isHttpUrl(text)) {
|
|
46
|
+
throw new Error("Transcription response had no text");
|
|
47
|
+
}
|
|
48
|
+
return text;
|
|
49
|
+
}
|
|
50
|
+
function languageOf(options) {
|
|
51
|
+
const record = options;
|
|
52
|
+
if (record === void 0) return void 0;
|
|
53
|
+
if (typeof record.language !== "string") return void 0;
|
|
54
|
+
return record.language;
|
|
55
|
+
}
|
|
56
|
+
async function transcribeWaveSpeed(model, input, options) {
|
|
57
|
+
try {
|
|
58
|
+
if (options?.signal?.aborted) throw new DOMException("aborted", "AbortError");
|
|
59
|
+
const audio = await sourceUrl(model, input, options);
|
|
60
|
+
const payload = {
|
|
61
|
+
audio,
|
|
62
|
+
task: "transcribe",
|
|
63
|
+
enable_timestamps: false
|
|
64
|
+
};
|
|
65
|
+
const language = languageOf(options);
|
|
66
|
+
if (language !== void 0) payload.language = language;
|
|
67
|
+
const record = await finishTask(model, options, payload, "Transcription payload was not JSON");
|
|
68
|
+
const outputs = record.outputs;
|
|
69
|
+
const first = Array.isArray(outputs) ? outputs[0] : void 0;
|
|
70
|
+
const text = await textOf(model, options, first);
|
|
71
|
+
const meters = {};
|
|
72
|
+
const seconds = durationSeconds(record);
|
|
73
|
+
if (seconds > 0) meters.inputSeconds = seconds;
|
|
74
|
+
const transcript = { text, usage: priceAudio(model.audioCost, meters) };
|
|
75
|
+
if (language !== void 0) transcript.language = language;
|
|
76
|
+
return transcript;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
const voice = asVoiceError(error);
|
|
79
|
+
throw new VoiceRequestError(voice.reason, voice.error);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export {
|
|
83
|
+
transcribeWaveSpeed
|
|
84
|
+
};
|
|
85
|
+
//# sourceMappingURL=transcribe.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/wavespeed/transcribe.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ProviderRequestOptions } from '@earendil-works/pi-ai';\nimport { VoiceRequestError } from '../errors.ts';\nimport type { TranscriptionModel } from '../models.ts';\nimport {\n asVoiceError,\n audioFile,\n encodeMultipart,\n endpoint,\n multipartType,\n} from '../openai/wire.ts';\nimport type { AudioInput, Transcript } from '../transcription.ts';\nimport { type AudioCounters, priceAudio } from '../usage.ts';\nimport {\n downloadUrl,\n durationSeconds,\n finishTask,\n isHttpUrl,\n outputText,\n readHttp,\n waveFetch,\n} from './wire.ts';\n\nasync function sourceUrl(\n model: TranscriptionModel<string>,\n input: AudioInput,\n options: ProviderRequestOptions | undefined\n): Promise<string> {\n if ('url' in input) return input.url;\n const named = audioFile(input.mimeType);\n const encoded = encodeMultipart(\n { data: input.data, mimeType: input.mimeType, filename: named.filename, format: named.format },\n {}\n );\n const copy = new Uint8Array(encoded.byteLength);\n copy.set(encoded);\n const parsed = await waveFetch(\n model,\n options,\n endpoint(model.baseUrl, 'media/upload/binary'),\n copy.buffer,\n multipartType()\n );\n return downloadUrl(parsed);\n}\n\nasync function textOf(\n model: TranscriptionModel<string>,\n options: ProviderRequestOptions | undefined,\n output: unknown\n): Promise<string> {\n if (typeof output === 'string' && isHttpUrl(output)) {\n const response = await readHttp(model, options, output);\n const text = await response.text();\n if (text === '') throw new Error('Transcription response had no text');\n return text;\n }\n const text = outputText(output);\n if (text === undefined || text === '' || isHttpUrl(text)) {\n throw new Error('Transcription response had no text');\n }\n return text;\n}\n\nfunction languageOf(options: ProviderRequestOptions | undefined): string | undefined {\n const record = options as { language?: unknown } | undefined;\n if (record === undefined) return undefined;\n if (typeof record.language !== 'string') return undefined;\n return record.language;\n}\n\nexport async function transcribeWaveSpeed(\n model: TranscriptionModel<string>,\n input: AudioInput,\n options?: ProviderRequestOptions\n): Promise<Transcript> {\n try {\n if (options?.signal?.aborted) throw new DOMException('aborted', 'AbortError');\n const audio = await sourceUrl(model, input, options);\n const payload: Record<string, unknown> = {\n audio,\n task: 'transcribe',\n enable_timestamps: false,\n };\n const language = languageOf(options);\n if (language !== undefined) payload.language = language;\n const record = await finishTask(model, options, payload, 'Transcription payload was not JSON');\n const outputs = record.outputs;\n const first = Array.isArray(outputs) ? outputs[0] : undefined;\n const text = await textOf(model, options, first);\n const meters: AudioCounters = {};\n const seconds = durationSeconds(record);\n if (seconds > 0) meters.inputSeconds = seconds;\n const transcript: Transcript = { text, usage: priceAudio(model.audioCost, meters) };\n if (language !== undefined) transcript.language = language;\n return transcript;\n } catch (error) {\n const voice = asVoiceError(error);\n throw new VoiceRequestError(voice.reason, voice.error);\n }\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,yBAAyB;AAElC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAA6B,kBAAkB;AAC/C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,eAAe,UACb,OACA,OACA,SACiB;AACjB,MAAI,SAAS,MAAO,QAAO,MAAM;AACjC,QAAM,QAAQ,UAAU,MAAM,QAAQ;AACtC,QAAM,UAAU;AAAA,IACd,EAAE,MAAM,MAAM,MAAM,UAAU,MAAM,UAAU,UAAU,MAAM,UAAU,QAAQ,MAAM,OAAO;AAAA,IAC7F,CAAC;AAAA,EACH;AACA,QAAM,OAAO,IAAI,WAAW,QAAQ,UAAU;AAC9C,OAAK,IAAI,OAAO;AAChB,QAAM,SAAS,MAAM;AAAA,IACnB;AAAA,IACA;AAAA,IACA,SAAS,MAAM,SAAS,qBAAqB;AAAA,IAC7C,KAAK;AAAA,IACL,cAAc;AAAA,EAChB;AACA,SAAO,YAAY,MAAM;AAC3B;AAEA,eAAe,OACb,OACA,SACA,QACiB;AACjB,MAAI,OAAO,WAAW,YAAY,UAAU,MAAM,GAAG;AACnD,UAAM,WAAW,MAAM,SAAS,OAAO,SAAS,MAAM;AACtD,UAAMA,QAAO,MAAM,SAAS,KAAK;AACjC,QAAIA,UAAS,GAAI,OAAM,IAAI,MAAM,oCAAoC;AACrE,WAAOA;AAAA,EACT;AACA,QAAM,OAAO,WAAW,MAAM;AAC9B,MAAI,SAAS,UAAa,SAAS,MAAM,UAAU,IAAI,GAAG;AACxD,UAAM,IAAI,MAAM,oCAAoC;AAAA,EACtD;AACA,SAAO;AACT;AAEA,SAAS,WAAW,SAAiE;AACnF,QAAM,SAAS;AACf,MAAI,WAAW,OAAW,QAAO;AACjC,MAAI,OAAO,OAAO,aAAa,SAAU,QAAO;AAChD,SAAO,OAAO;AAChB;AAEA,eAAsB,oBACpB,OACA,OACA,SACqB;AACrB,MAAI;AACF,QAAI,SAAS,QAAQ,QAAS,OAAM,IAAI,aAAa,WAAW,YAAY;AAC5E,UAAM,QAAQ,MAAM,UAAU,OAAO,OAAO,OAAO;AACnD,UAAM,UAAmC;AAAA,MACvC;AAAA,MACA,MAAM;AAAA,MACN,mBAAmB;AAAA,IACrB;AACA,UAAM,WAAW,WAAW,OAAO;AACnC,QAAI,aAAa,OAAW,SAAQ,WAAW;AAC/C,UAAM,SAAS,MAAM,WAAW,OAAO,SAAS,SAAS,oCAAoC;AAC7F,UAAM,UAAU,OAAO;AACvB,UAAM,QAAQ,MAAM,QAAQ,OAAO,IAAI,QAAQ,CAAC,IAAI;AACpD,UAAM,OAAO,MAAM,OAAO,OAAO,SAAS,KAAK;AAC/C,UAAM,SAAwB,CAAC;AAC/B,UAAM,UAAU,gBAAgB,MAAM;AACtC,QAAI,UAAU,EAAG,QAAO,eAAe;AACvC,UAAM,aAAyB,EAAE,MAAM,OAAO,WAAW,MAAM,WAAW,MAAM,EAAE;AAClF,QAAI,aAAa,OAAW,YAAW,WAAW;AAClD,WAAO;AAAA,EACT,SAAS,OAAO;AACd,UAAM,QAAQ,aAAa,KAAK;AAChC,UAAM,IAAI,kBAAkB,MAAM,QAAQ,MAAM,KAAK;AAAA,EACvD;AACF;",
|
|
6
|
+
"names": ["text"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyPayload,
|
|
3
|
+
endpoint,
|
|
4
|
+
isRecord,
|
|
5
|
+
numberField,
|
|
6
|
+
requestHeaders,
|
|
7
|
+
requestSignal,
|
|
8
|
+
stringField
|
|
9
|
+
} from "../openai/wire.js";
|
|
10
|
+
const terminal = /* @__PURE__ */ new Set(["failed", "cancelled", "timeout", "deleted"]);
|
|
11
|
+
function taskRecord(body) {
|
|
12
|
+
if (!isRecord(body) || Array.isArray(body)) throw new Error("WaveSpeed response was not JSON");
|
|
13
|
+
const data = body.data;
|
|
14
|
+
if (isRecord(data) && !Array.isArray(data)) return data;
|
|
15
|
+
return body;
|
|
16
|
+
}
|
|
17
|
+
function errorText(text, status) {
|
|
18
|
+
if (text === "") return `HTTP ${status}`;
|
|
19
|
+
let parsed;
|
|
20
|
+
try {
|
|
21
|
+
parsed = JSON.parse(text);
|
|
22
|
+
} catch {
|
|
23
|
+
return text;
|
|
24
|
+
}
|
|
25
|
+
if (!isRecord(parsed) || Array.isArray(parsed)) return text;
|
|
26
|
+
const message = stringField(parsed, "message");
|
|
27
|
+
if (message !== void 0 && message !== "") return message;
|
|
28
|
+
return text;
|
|
29
|
+
}
|
|
30
|
+
function isHttpUrl(value) {
|
|
31
|
+
if (value.startsWith("https://")) return true;
|
|
32
|
+
if (value.startsWith("http://")) return true;
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
function outputText(output) {
|
|
36
|
+
if (typeof output === "string") return output;
|
|
37
|
+
if (!isRecord(output) || Array.isArray(output)) return void 0;
|
|
38
|
+
return stringField(output, "text") ?? stringField(output, "transcript");
|
|
39
|
+
}
|
|
40
|
+
function outputUrl(output) {
|
|
41
|
+
if (typeof output === "string" && isHttpUrl(output)) return output;
|
|
42
|
+
if (!isRecord(output) || Array.isArray(output)) return void 0;
|
|
43
|
+
const url = stringField(output, "url") ?? stringField(output, "audio");
|
|
44
|
+
if (url === void 0 || !isHttpUrl(url)) return void 0;
|
|
45
|
+
return url;
|
|
46
|
+
}
|
|
47
|
+
function downloadUrl(body) {
|
|
48
|
+
const record = taskRecord(body);
|
|
49
|
+
const url = stringField(record, "download_url") ?? stringField(record, "url");
|
|
50
|
+
if (url === void 0 || url === "") throw new Error("WaveSpeed upload had no url");
|
|
51
|
+
return url;
|
|
52
|
+
}
|
|
53
|
+
function durationSeconds(record) {
|
|
54
|
+
const direct = numberField(record, "duration");
|
|
55
|
+
if (direct !== void 0) return direct;
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
async function waveFetch(model, options, url, body, contentType) {
|
|
59
|
+
const headers = requestHeaders(model, options);
|
|
60
|
+
if (contentType !== void 0) headers["Content-Type"] = contentType;
|
|
61
|
+
const fetchImpl = options?.fetch ?? globalThis.fetch;
|
|
62
|
+
const response = await fetchImpl(url, {
|
|
63
|
+
method: body === void 0 ? "GET" : "POST",
|
|
64
|
+
headers,
|
|
65
|
+
body,
|
|
66
|
+
signal: requestSignal(options)
|
|
67
|
+
});
|
|
68
|
+
if (options?.onResponse !== void 0) {
|
|
69
|
+
await options.onResponse({ status: response.status, headers: {} }, model);
|
|
70
|
+
}
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
const text2 = await response.text();
|
|
73
|
+
throw new Error(errorText(text2, response.status));
|
|
74
|
+
}
|
|
75
|
+
const text = await response.text();
|
|
76
|
+
if (text === "") throw new Error("WaveSpeed response was not JSON");
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(text);
|
|
79
|
+
} catch {
|
|
80
|
+
throw new Error("WaveSpeed response was not JSON");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function failure(record, status) {
|
|
84
|
+
const error = stringField(record, "error");
|
|
85
|
+
if (error === void 0 || error === "") return new Error(`WaveSpeed ${status}`);
|
|
86
|
+
return new Error(error);
|
|
87
|
+
}
|
|
88
|
+
async function finishTask(model, options, payload, rejected) {
|
|
89
|
+
const body = await applyPayload(payload, model, options);
|
|
90
|
+
if (!isRecord(body) || Array.isArray(body)) throw new Error(rejected);
|
|
91
|
+
let record = taskRecord(
|
|
92
|
+
await waveFetch(
|
|
93
|
+
model,
|
|
94
|
+
options,
|
|
95
|
+
endpoint(model.baseUrl, model.id),
|
|
96
|
+
JSON.stringify(body),
|
|
97
|
+
"application/json"
|
|
98
|
+
)
|
|
99
|
+
);
|
|
100
|
+
for (let attempt = 0; attempt < 9; attempt += 1) {
|
|
101
|
+
const status = stringField(record, "status") ?? "";
|
|
102
|
+
if (status === "completed") return record;
|
|
103
|
+
if (terminal.has(status)) throw failure(record, status);
|
|
104
|
+
if (attempt === 8) break;
|
|
105
|
+
const id = stringField(record, "id");
|
|
106
|
+
if (id === void 0 || id === "") throw new Error("WaveSpeed prediction had no id");
|
|
107
|
+
record = taskRecord(
|
|
108
|
+
await waveFetch(
|
|
109
|
+
model,
|
|
110
|
+
options,
|
|
111
|
+
endpoint(model.baseUrl, `predictions/${id}/result`),
|
|
112
|
+
void 0,
|
|
113
|
+
void 0
|
|
114
|
+
)
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
throw new Error("WaveSpeed prediction timed out");
|
|
118
|
+
}
|
|
119
|
+
async function readHttp(model, options, url) {
|
|
120
|
+
const headers = requestHeaders(model, options);
|
|
121
|
+
const fetchImpl = options?.fetch ?? globalThis.fetch;
|
|
122
|
+
const response = await fetchImpl(url, { headers, signal: requestSignal(options) });
|
|
123
|
+
if (!response.ok) throw new Error(errorText("", response.status));
|
|
124
|
+
return response;
|
|
125
|
+
}
|
|
126
|
+
export {
|
|
127
|
+
downloadUrl,
|
|
128
|
+
durationSeconds,
|
|
129
|
+
errorText,
|
|
130
|
+
finishTask,
|
|
131
|
+
isHttpUrl,
|
|
132
|
+
outputText,
|
|
133
|
+
outputUrl,
|
|
134
|
+
readHttp,
|
|
135
|
+
taskRecord,
|
|
136
|
+
waveFetch
|
|
137
|
+
};
|
|
138
|
+
//# sourceMappingURL=wire.js.map
|