@oh-my-pi/pi-coding-agent 17.1.0 → 17.1.1
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/CHANGELOG.md +24 -0
- package/dist/cli.js +3686 -4013
- package/dist/types/config/settings-schema.d.ts +58 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +3 -0
- package/dist/types/live/attestation.d.ts +2 -0
- package/dist/types/live/controller.d.ts +10 -2
- package/dist/types/live/protocol.d.ts +1 -1
- package/dist/types/live/transport.d.ts +6 -19
- package/dist/types/live/visualizer.d.ts +8 -11
- package/dist/types/modes/components/assistant-message.d.ts +1 -0
- package/dist/types/modes/components/custom-message.d.ts +1 -1
- package/dist/types/modes/components/message-frame.d.ts +8 -4
- package/dist/types/modes/components/session-account-selector.d.ts +11 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/session/agent-session-types.d.ts +8 -1
- package/dist/types/session/agent-session.d.ts +20 -1
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/eval-runner.d.ts +2 -0
- package/dist/types/session/messages.d.ts +2 -0
- package/dist/types/session/session-tools.d.ts +15 -0
- package/dist/types/session/streaming-output.d.ts +8 -0
- package/dist/types/slash-commands/helpers/session-pin.d.ts +9 -0
- package/dist/types/stt/index.d.ts +0 -2
- package/dist/types/stt/stt-controller.d.ts +7 -0
- package/dist/types/tiny/title-client.d.ts +10 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/computer/protocol.d.ts +43 -0
- package/dist/types/tools/computer/supervisor.d.ts +32 -0
- package/dist/types/tools/computer/worker-entry.d.ts +1 -0
- package/dist/types/tools/computer/worker.d.ts +15 -0
- package/dist/types/tools/computer-renderer.d.ts +22 -0
- package/dist/types/tools/computer.d.ts +71 -0
- package/dist/types/tools/context.d.ts +2 -0
- package/dist/types/tools/default-renderer.d.ts +21 -0
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tts/streaming-player.d.ts +10 -43
- package/dist/types/utils/tools-manager.d.ts +1 -2
- package/package.json +12 -12
- package/src/cli/args.ts +1 -0
- package/src/cli/setup-cli.ts +2 -14
- package/src/cli.ts +8 -0
- package/src/config/model-registry.ts +6 -0
- package/src/config/settings-schema.ts +61 -0
- package/src/eval/executor-base.ts +1 -0
- package/src/eval/js/executor.ts +2 -0
- package/src/exec/bash-executor.ts +1 -0
- package/src/extensibility/extensions/wrapper.ts +68 -12
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -1
- package/src/live/attestation.ts +91 -0
- package/src/live/controller.ts +76 -23
- package/src/live/protocol.test.ts +3 -3
- package/src/live/protocol.ts +1 -1
- package/src/live/transport.ts +72 -140
- package/src/live/visualizer.ts +114 -134
- package/src/modes/components/assistant-message.ts +7 -2
- package/src/modes/components/custom-message.ts +4 -1
- package/src/modes/components/message-frame.ts +14 -8
- package/src/modes/components/session-account-selector.ts +62 -0
- package/src/modes/components/tool-execution.ts +17 -110
- package/src/modes/controllers/input-controller.ts +47 -39
- package/src/modes/controllers/live-command-controller.ts +82 -5
- package/src/modes/controllers/selector-controller.ts +62 -0
- package/src/modes/interactive-mode.ts +19 -0
- package/src/modes/types.ts +1 -0
- package/src/prompts/system/computer-safety.md +14 -0
- package/src/prompts/tools/computer.md +26 -0
- package/src/sdk.ts +5 -0
- package/src/session/agent-session-types.ts +9 -0
- package/src/session/agent-session.ts +56 -0
- package/src/session/auth-storage.ts +1 -0
- package/src/session/eval-runner.ts +5 -0
- package/src/session/messages.ts +3 -0
- package/src/session/session-tools.ts +37 -0
- package/src/session/streaming-output.ts +52 -5
- package/src/slash-commands/builtin-registry.ts +165 -9
- package/src/slash-commands/helpers/session-pin.ts +44 -0
- package/src/stt/downloader.ts +0 -2
- package/src/stt/index.ts +0 -2
- package/src/stt/stt-controller.ts +57 -146
- package/src/system-prompt.ts +4 -0
- package/src/tiny/title-client.ts +22 -0
- package/src/tools/bash-interactive.ts +90 -86
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/computer/protocol.ts +28 -0
- package/src/tools/computer/supervisor.ts +258 -0
- package/src/tools/computer/worker-entry.ts +25 -0
- package/src/tools/computer/worker.ts +135 -0
- package/src/tools/computer-renderer.ts +108 -0
- package/src/tools/computer.ts +433 -0
- package/src/tools/context.ts +2 -0
- package/src/tools/default-renderer.ts +139 -0
- package/src/tools/essential-tools.ts +1 -0
- package/src/tools/index.ts +5 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/xdev.ts +54 -26
- package/src/tts/streaming-player.ts +81 -340
- package/src/utils/clipboard.ts +1 -30
- package/src/utils/mac-file-urls.applescript +37 -0
- package/src/utils/tool-choice.ts +14 -0
- package/src/utils/tools-manager.ts +1 -19
- package/dist/types/stt/recorder.d.ts +0 -30
- package/dist/types/stt/transcriber.d.ts +0 -14
- package/dist/types/stt/wav.d.ts +0 -29
- package/dist/types/tts/player.d.ts +0 -32
- package/src/live/audio-worklet.txt +0 -59
- package/src/live/browser-runtime.txt +0 -221
- package/src/stt/recorder.ts +0 -551
- package/src/stt/transcriber.ts +0 -60
- package/src/stt/wav.ts +0 -173
- package/src/tts/player.ts +0 -137
package/src/utils/tool-choice.ts
CHANGED
|
@@ -12,6 +12,19 @@ export function buildNamedToolChoice(toolName: string, model?: Model<Api>): Tool
|
|
|
12
12
|
return { type: "tool", name: toolName };
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
if (toolName === "computer" && model.supportsComputerUse === true) {
|
|
16
|
+
if (
|
|
17
|
+
model.api === "openai-codex-responses" ||
|
|
18
|
+
model.api === "openai-responses" ||
|
|
19
|
+
model.api === "azure-openai-responses"
|
|
20
|
+
) {
|
|
21
|
+
return { type: "computer" };
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
// Models without native computer support receive the tool as a regular
|
|
26
|
+
// function tool, so the generic named-function forcing below applies.
|
|
27
|
+
|
|
15
28
|
if (
|
|
16
29
|
model.api === "openai-codex-responses" ||
|
|
17
30
|
model.api === "openai-responses" ||
|
|
@@ -39,6 +52,7 @@ export function buildNamedToolChoice(toolName: string, model?: Model<Api>): Tool
|
|
|
39
52
|
*/
|
|
40
53
|
export function isToolChoiceActive(toolChoice: ToolChoice | undefined, tools: readonly { name: string }[]): boolean {
|
|
41
54
|
if (!toolChoice || typeof toolChoice === "string") return true;
|
|
55
|
+
if (toolChoice.type === "computer") return tools.some(tool => tool.name === "computer");
|
|
42
56
|
const name =
|
|
43
57
|
toolChoice.type === "tool"
|
|
44
58
|
? toolChoice.name
|
|
@@ -73,16 +73,6 @@ interface ToolConfig {
|
|
|
73
73
|
getAssetName: (version: string, plat: string, architecture: string) => string | null;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
// ffmpeg static-binary asset names (eugeneware/ffmpeg-static direct binaries).
|
|
77
|
-
// Maps node arch (arm64|x64) only; everything else is unsupported.
|
|
78
|
-
export function ffmpegAssetName(_version: string, plat: string, architecture: string): string | null {
|
|
79
|
-
if (architecture !== "arm64" && architecture !== "x64") return null;
|
|
80
|
-
if (plat === "darwin") return `ffmpeg-darwin-${architecture}`;
|
|
81
|
-
if (plat === "linux") return `ffmpeg-linux-${architecture}`;
|
|
82
|
-
if (plat === "win32") return architecture === "x64" ? "ffmpeg-win32-x64" : null;
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
76
|
const TOOLS: Record<string, ToolConfig> = {
|
|
87
77
|
sd: {
|
|
88
78
|
name: "sd",
|
|
@@ -139,14 +129,6 @@ const TOOLS: Record<string, ToolConfig> = {
|
|
|
139
129
|
return null;
|
|
140
130
|
},
|
|
141
131
|
},
|
|
142
|
-
ffmpeg: {
|
|
143
|
-
name: "ffmpeg",
|
|
144
|
-
repo: "eugeneware/ffmpeg-static",
|
|
145
|
-
binaryName: "ffmpeg",
|
|
146
|
-
tagPrefix: "",
|
|
147
|
-
isDirectBinary: true,
|
|
148
|
-
getAssetName: ffmpegAssetName,
|
|
149
|
-
},
|
|
150
132
|
};
|
|
151
133
|
|
|
152
134
|
// CLI packages installed via uv/pip
|
|
@@ -164,7 +146,7 @@ const PYTHON_TOOLS: Record<string, PythonPackageToolConfig> = {
|
|
|
164
146
|
},
|
|
165
147
|
};
|
|
166
148
|
|
|
167
|
-
export type ToolName = "sd" | "sg" | "yt-dlp" | "trafilatura"
|
|
149
|
+
export type ToolName = "sd" | "sg" | "yt-dlp" | "trafilatura";
|
|
168
150
|
|
|
169
151
|
// Get the path to a tool (system-wide or in our tools dir)
|
|
170
152
|
export function getToolPath(tool: ToolName): string | null {
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
export interface RecordingHandle {
|
|
2
|
-
stop(): Promise<void>;
|
|
3
|
-
}
|
|
4
|
-
/**
|
|
5
|
-
* Returns available recording tools in priority order.
|
|
6
|
-
*/
|
|
7
|
-
export declare function detectRecordingTools(): string[];
|
|
8
|
-
export interface ResolvedRecorder {
|
|
9
|
-
tool: "sox" | "ffmpeg" | "arecord" | "powershell";
|
|
10
|
-
bin: string;
|
|
11
|
-
}
|
|
12
|
-
export declare function detectRecorder(): ResolvedRecorder | null;
|
|
13
|
-
/**
|
|
14
|
-
* Ensure a recorder is available, downloading the static ffmpeg binary when
|
|
15
|
-
* nothing is already present. Returns the resolved recorder.
|
|
16
|
-
*/
|
|
17
|
-
export declare function ensureRecorder(onProgress?: (p: {
|
|
18
|
-
stage: string;
|
|
19
|
-
percent?: number;
|
|
20
|
-
}) => void, signal?: AbortSignal): Promise<ResolvedRecorder>;
|
|
21
|
-
export declare function startRecording(outputPath: string): Promise<RecordingHandle>;
|
|
22
|
-
/**
|
|
23
|
-
* Verify a recorded audio file is usable.
|
|
24
|
-
* Returns the file size in bytes, or throws.
|
|
25
|
-
*/
|
|
26
|
-
export declare function verifyRecordingFile(filePath: string): Promise<number>;
|
|
27
|
-
export interface StreamingRecordingHandle {
|
|
28
|
-
stop(): Promise<void>;
|
|
29
|
-
}
|
|
30
|
-
export declare function startStreamingRecording(onAudio: (samples: Float32Array) => void): Promise<StreamingRecordingHandle | null>;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export interface TranscribeOptions {
|
|
2
|
-
modelName?: string;
|
|
3
|
-
language?: string;
|
|
4
|
-
signal?: AbortSignal;
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* Transcribe a WAV file using the local ONNX Whisper worker.
|
|
8
|
-
*
|
|
9
|
-
* Decodes the WAV to a 16 kHz mono Float32Array in-process (no Python, no
|
|
10
|
-
* ffmpeg) and routes it to the warm speech worker, which keeps the model loaded
|
|
11
|
-
* across calls. Honors `options.signal` (abort) and applies an internal timeout
|
|
12
|
-
* with the same semantics as the previous Python path.
|
|
13
|
-
*/
|
|
14
|
-
export declare function transcribe(audioPath: string, options?: TranscribeOptions): Promise<string>;
|
package/dist/types/stt/wav.d.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal WAV (RIFF/PCM) decoder producing the Float32Array @ 16 kHz mono that
|
|
3
|
-
* transformers.js `automatic-speech-recognition` expects. Ports the decode/
|
|
4
|
-
* mono-mix/resample logic from the retired Python `transcribe.py` (which read
|
|
5
|
-
* via the stdlib `wave` module) so STT no longer shells out to Python.
|
|
6
|
-
*
|
|
7
|
-
* Supported sample formats: PCM uint8 (8-bit), int16 (16-bit), int32 (32-bit),
|
|
8
|
-
* and IEEE float32 (format tag 3). Any number of channels is mixed down to mono.
|
|
9
|
-
*/
|
|
10
|
-
/** transformers.js Whisper feature extractor operates at 16 kHz. */
|
|
11
|
-
export declare const TARGET_SAMPLE_RATE = 16000;
|
|
12
|
-
/**
|
|
13
|
-
* Resample via linear interpolation, mirroring the Python `np.interp` over
|
|
14
|
-
* `linspace(0, n-1, targetLen)` against `arange(n)`.
|
|
15
|
-
*/
|
|
16
|
-
export declare function resampleLinear(input: Float32Array, fromRate: number, toRate: number): Float32Array;
|
|
17
|
-
/**
|
|
18
|
-
* Decode a WAV byte buffer into a 16 kHz mono Float32Array suitable for the
|
|
19
|
-
* transformers.js Whisper pipeline.
|
|
20
|
-
*/
|
|
21
|
-
export declare function decodeWavToMono16k(buffer: ArrayBuffer): Float32Array;
|
|
22
|
-
/**
|
|
23
|
-
* Decode interleaved little-endian signed 16-bit PCM bytes into normalized
|
|
24
|
-
* [-1, 1] mono float samples. The live recorder streams raw s16le frames from
|
|
25
|
-
* sox/ffmpeg/arecord stdout (no RIFF container), so this is the hot-path
|
|
26
|
-
* counterpart to {@link decodeWavToMono16k}. `bytes` MUST be 2-byte aligned;
|
|
27
|
-
* callers buffer any trailing odd byte across chunk boundaries.
|
|
28
|
-
*/
|
|
29
|
-
export declare function decodePcmS16LE(bytes: Uint8Array): Float32Array;
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
export interface PlayerCommand {
|
|
2
|
-
cmd: string;
|
|
3
|
-
args: string[];
|
|
4
|
-
}
|
|
5
|
-
/** Injection seam for {@link playerCommandsFor} — defaults to real PATH/tools lookups. */
|
|
6
|
-
export interface PlayerLookup {
|
|
7
|
-
which?: (bin: string) => string | null;
|
|
8
|
-
ffmpeg?: () => string | null;
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Build the ordered list of playback commands to try for `filePath` on the
|
|
12
|
-
* given platform. Pure + injectable so the selection logic is testable without
|
|
13
|
-
* spawning anything.
|
|
14
|
-
*
|
|
15
|
-
* - darwin: `afplay` (always present on macOS).
|
|
16
|
-
* - win32: PowerShell `Media.SoundPlayer.PlaySync()` (no extra deps).
|
|
17
|
-
* - linux/other POSIX: `paplay` (PulseAudio) → `aplay` (ALSA) → the bundled
|
|
18
|
-
* static `ffmpeg` (`-f pulse` then `-f alsa`). Empty result means nothing is
|
|
19
|
-
* available and the caller should surface an install hint.
|
|
20
|
-
*/
|
|
21
|
-
export declare function playerCommandsFor(platform: NodeJS.Platform, filePath: string, lookup?: PlayerLookup): PlayerCommand[];
|
|
22
|
-
export interface PlayAudioOptions {
|
|
23
|
-
signal?: AbortSignal;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Play `filePath` through the speakers, trying each candidate command in order
|
|
27
|
-
* and returning on the first clean exit. Throws an actionable Error if no
|
|
28
|
-
* player exists or every candidate fails (with the collected stderr).
|
|
29
|
-
*/
|
|
30
|
-
export declare function playAudioFile(filePath: string, options?: PlayAudioOptions): Promise<void>;
|
|
31
|
-
/** Best-effort temp-file cleanup used by callers after playback. */
|
|
32
|
-
export declare function removeTempFile(filePath: string): Promise<void>;
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
class OmpPcmSourceProcessor extends AudioWorkletProcessor {
|
|
2
|
-
constructor() {
|
|
3
|
-
super();
|
|
4
|
-
this.frames = [];
|
|
5
|
-
this.frameOffset = 0;
|
|
6
|
-
this.queuedSamples = 0;
|
|
7
|
-
this.muted = false;
|
|
8
|
-
this.maxQueuedSamples = Math.floor(sampleRate * 0.75);
|
|
9
|
-
this.port.onmessage = event => {
|
|
10
|
-
const message = event.data;
|
|
11
|
-
if (message.type === "mute") {
|
|
12
|
-
this.muted = message.muted;
|
|
13
|
-
this.clear();
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
if (message.type !== "audio" || this.muted || !(message.samples instanceof Float32Array)) return;
|
|
17
|
-
let samples = message.samples;
|
|
18
|
-
if (samples.length > this.maxQueuedSamples) samples = samples.slice(samples.length - this.maxQueuedSamples);
|
|
19
|
-
this.frames.push(samples);
|
|
20
|
-
this.queuedSamples += samples.length;
|
|
21
|
-
while (this.queuedSamples > this.maxQueuedSamples && this.frames.length > 1) {
|
|
22
|
-
const stale = this.frames.shift();
|
|
23
|
-
this.queuedSamples -= stale.length - this.frameOffset;
|
|
24
|
-
this.frameOffset = 0;
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
clear() {
|
|
30
|
-
this.frames.length = 0;
|
|
31
|
-
this.frameOffset = 0;
|
|
32
|
-
this.queuedSamples = 0;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
process(_inputs, outputs) {
|
|
36
|
-
const output = outputs[0];
|
|
37
|
-
const channel = output && output[0];
|
|
38
|
-
if (!channel) return true;
|
|
39
|
-
channel.fill(0);
|
|
40
|
-
if (this.muted) return true;
|
|
41
|
-
let outputOffset = 0;
|
|
42
|
-
while (outputOffset < channel.length && this.frames.length > 0) {
|
|
43
|
-
const frame = this.frames[0];
|
|
44
|
-
const available = frame.length - this.frameOffset;
|
|
45
|
-
const count = Math.min(available, channel.length - outputOffset);
|
|
46
|
-
channel.set(frame.subarray(this.frameOffset, this.frameOffset + count), outputOffset);
|
|
47
|
-
outputOffset += count;
|
|
48
|
-
this.frameOffset += count;
|
|
49
|
-
this.queuedSamples -= count;
|
|
50
|
-
if (this.frameOffset === frame.length) {
|
|
51
|
-
this.frames.shift();
|
|
52
|
-
this.frameOffset = 0;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
registerProcessor("omp-pcm-source", OmpPcmSourceProcessor);
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
(() => {
|
|
2
|
-
const INPUT_SAMPLE_RATE = 16_000;
|
|
3
|
-
const LEVEL_INTERVAL_MS = 50;
|
|
4
|
-
const OPEN_TIMEOUT_MS = 20_000;
|
|
5
|
-
let context;
|
|
6
|
-
let peer;
|
|
7
|
-
let channel;
|
|
8
|
-
let sourceNode;
|
|
9
|
-
let sourceTrack;
|
|
10
|
-
let remoteSource;
|
|
11
|
-
let analyser;
|
|
12
|
-
let levelWaveform;
|
|
13
|
-
let levelTimer;
|
|
14
|
-
let disconnectTimer;
|
|
15
|
-
let workletUrl;
|
|
16
|
-
let closing = false;
|
|
17
|
-
let muted = false;
|
|
18
|
-
let openSettled = false;
|
|
19
|
-
let openResolve;
|
|
20
|
-
let openReject;
|
|
21
|
-
let previousInputSample;
|
|
22
|
-
let inputPhase = 0;
|
|
23
|
-
const openPromise = new Promise((resolve, reject) => {
|
|
24
|
-
openResolve = resolve;
|
|
25
|
-
openReject = reject;
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
function reportFailure(message) {
|
|
29
|
-
if (closing) return;
|
|
30
|
-
if (!openSettled) {
|
|
31
|
-
openSettled = true;
|
|
32
|
-
openReject(new Error(message));
|
|
33
|
-
}
|
|
34
|
-
Promise.resolve(window.__ompLiveFailure(message)).catch(() => {});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function waitForIceGathering() {
|
|
38
|
-
if (peer.iceGatheringState === "complete") return Promise.resolve();
|
|
39
|
-
return new Promise((resolve, reject) => {
|
|
40
|
-
const timeout = setTimeout(() => {
|
|
41
|
-
peer.removeEventListener("icegatheringstatechange", onChange);
|
|
42
|
-
reject(new Error("Timed out gathering WebRTC ICE candidates"));
|
|
43
|
-
}, 10_000);
|
|
44
|
-
function onChange() {
|
|
45
|
-
if (peer.iceGatheringState !== "complete") return;
|
|
46
|
-
clearTimeout(timeout);
|
|
47
|
-
peer.removeEventListener("icegatheringstatechange", onChange);
|
|
48
|
-
resolve();
|
|
49
|
-
}
|
|
50
|
-
peer.addEventListener("icegatheringstatechange", onChange);
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function decodePcm(base64) {
|
|
55
|
-
const encoded = atob(base64);
|
|
56
|
-
const bytes = new Uint8Array(encoded.length);
|
|
57
|
-
for (let index = 0; index < encoded.length; index++) bytes[index] = encoded.charCodeAt(index);
|
|
58
|
-
const view = new DataView(bytes.buffer);
|
|
59
|
-
const samples = new Float32Array(bytes.length / 4);
|
|
60
|
-
for (let index = 0; index < samples.length; index++) samples[index] = view.getFloat32(index * 4, true);
|
|
61
|
-
return samples;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function resample(samples) {
|
|
65
|
-
if (samples.length === 0) return new Float32Array();
|
|
66
|
-
const step = INPUT_SAMPLE_RATE / context.sampleRate;
|
|
67
|
-
const prefix = previousInputSample === undefined ? 0 : 1;
|
|
68
|
-
const input = new Float32Array(samples.length + prefix);
|
|
69
|
-
if (prefix === 1) input[0] = previousInputSample;
|
|
70
|
-
input.set(samples, prefix);
|
|
71
|
-
if (prefix === 0) inputPhase = 0;
|
|
72
|
-
const available = input.length - 1;
|
|
73
|
-
if (available <= 0) {
|
|
74
|
-
previousInputSample = input[0];
|
|
75
|
-
return new Float32Array();
|
|
76
|
-
}
|
|
77
|
-
const outputLength = Math.max(0, Math.ceil((available - inputPhase) / step));
|
|
78
|
-
const output = new Float32Array(outputLength);
|
|
79
|
-
let written = 0;
|
|
80
|
-
while (inputPhase < available) {
|
|
81
|
-
const index = Math.floor(inputPhase);
|
|
82
|
-
const fraction = inputPhase - index;
|
|
83
|
-
output[written++] = input[index] + (input[index + 1] - input[index]) * fraction;
|
|
84
|
-
inputPhase += step;
|
|
85
|
-
}
|
|
86
|
-
inputPhase -= available;
|
|
87
|
-
previousInputSample = input[input.length - 1];
|
|
88
|
-
return written === output.length ? output : output.slice(0, written);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function reportLevel() {
|
|
92
|
-
if (!analyser || !levelWaveform) return;
|
|
93
|
-
analyser.getFloatTimeDomainData(levelWaveform);
|
|
94
|
-
let sum = 0;
|
|
95
|
-
for (const sample of levelWaveform) sum += sample * sample;
|
|
96
|
-
const level = Math.min(1, Math.max(0, Math.sqrt(sum / levelWaveform.length)));
|
|
97
|
-
Promise.resolve(window.__ompLiveOutputLevel(level)).catch(() => {});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function attachRemoteTrack(event) {
|
|
101
|
-
if (closing || event.track.kind !== "audio") return;
|
|
102
|
-
if (remoteSource) remoteSource.disconnect();
|
|
103
|
-
const stream = new MediaStream([event.track]);
|
|
104
|
-
remoteSource = context.createMediaStreamSource(stream);
|
|
105
|
-
analyser = context.createAnalyser();
|
|
106
|
-
analyser.fftSize = 1024;
|
|
107
|
-
levelWaveform = new Float32Array(analyser.fftSize);
|
|
108
|
-
remoteSource.connect(analyser);
|
|
109
|
-
analyser.connect(context.destination);
|
|
110
|
-
clearInterval(levelTimer);
|
|
111
|
-
levelTimer = setInterval(reportLevel, LEVEL_INTERVAL_MS);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async function start(workletSource) {
|
|
115
|
-
if (peer) throw new Error("Live browser runtime already started");
|
|
116
|
-
context = new AudioContext({ latencyHint: "interactive" });
|
|
117
|
-
await context.resume();
|
|
118
|
-
workletUrl = URL.createObjectURL(new Blob([workletSource], { type: "text/javascript" }));
|
|
119
|
-
await context.audioWorklet.addModule(workletUrl);
|
|
120
|
-
sourceNode = new AudioWorkletNode(context, "omp-pcm-source", {
|
|
121
|
-
numberOfInputs: 0,
|
|
122
|
-
numberOfOutputs: 1,
|
|
123
|
-
outputChannelCount: [1],
|
|
124
|
-
});
|
|
125
|
-
const destination = context.createMediaStreamDestination();
|
|
126
|
-
sourceNode.connect(destination);
|
|
127
|
-
sourceTrack = destination.stream.getAudioTracks()[0];
|
|
128
|
-
sourceTrack.enabled = !muted;
|
|
129
|
-
|
|
130
|
-
peer = new RTCPeerConnection();
|
|
131
|
-
peer.addTrack(sourceTrack, destination.stream);
|
|
132
|
-
peer.ontrack = attachRemoteTrack;
|
|
133
|
-
peer.onconnectionstatechange = () => {
|
|
134
|
-
clearTimeout(disconnectTimer);
|
|
135
|
-
if (peer.connectionState === "failed" || peer.connectionState === "closed") {
|
|
136
|
-
reportFailure(`WebRTC peer connection ${peer.connectionState}`);
|
|
137
|
-
} else if (peer.connectionState === "disconnected") {
|
|
138
|
-
disconnectTimer = setTimeout(() => {
|
|
139
|
-
if (peer && peer.connectionState === "disconnected") reportFailure("WebRTC peer connection disconnected");
|
|
140
|
-
}, 2_000);
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
channel = peer.createDataChannel("oai-events");
|
|
144
|
-
channel.onopen = () => {
|
|
145
|
-
if (openSettled) return;
|
|
146
|
-
openSettled = true;
|
|
147
|
-
openResolve();
|
|
148
|
-
};
|
|
149
|
-
channel.onerror = () => reportFailure("Live data channel failed");
|
|
150
|
-
channel.onclose = () => reportFailure("Live data channel closed unexpectedly");
|
|
151
|
-
channel.onmessage = event => {
|
|
152
|
-
if (typeof event.data !== "string") return;
|
|
153
|
-
Promise.resolve(window.__ompLiveServerEvent(event.data)).catch(() => {});
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
const offer = await peer.createOffer();
|
|
157
|
-
await peer.setLocalDescription(offer);
|
|
158
|
-
await waitForIceGathering();
|
|
159
|
-
if (!peer.localDescription || !peer.localDescription.sdp) throw new Error("Browser produced an empty WebRTC offer");
|
|
160
|
-
return peer.localDescription.sdp;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
async function acceptAnswer(sdp) {
|
|
164
|
-
if (!peer) throw new Error("Live browser runtime has not started");
|
|
165
|
-
await peer.setRemoteDescription({ type: "answer", sdp });
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
async function waitForOpen() {
|
|
169
|
-
let timeout;
|
|
170
|
-
try {
|
|
171
|
-
await Promise.race([
|
|
172
|
-
openPromise,
|
|
173
|
-
new Promise((_, reject) => {
|
|
174
|
-
timeout = setTimeout(() => reject(new Error("Timed out waiting for the live data channel to open")), OPEN_TIMEOUT_MS);
|
|
175
|
-
}),
|
|
176
|
-
]);
|
|
177
|
-
} finally {
|
|
178
|
-
clearTimeout(timeout);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function send(payload) {
|
|
183
|
-
if (!channel || channel.readyState !== "open") throw new Error("Live data channel is not open");
|
|
184
|
-
channel.send(payload);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function pushAudio(base64) {
|
|
188
|
-
if (!sourceNode || muted) return;
|
|
189
|
-
const output = resample(decodePcm(base64));
|
|
190
|
-
if (output.length === 0) return;
|
|
191
|
-
sourceNode.port.postMessage({ type: "audio", samples: output }, [output.buffer]);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function setMuted(nextMuted) {
|
|
195
|
-
muted = nextMuted;
|
|
196
|
-
previousInputSample = undefined;
|
|
197
|
-
inputPhase = 0;
|
|
198
|
-
if (sourceTrack) sourceTrack.enabled = !muted;
|
|
199
|
-
if (sourceNode) sourceNode.port.postMessage({ type: "mute", muted });
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
async function close() {
|
|
203
|
-
if (closing) return;
|
|
204
|
-
closing = true;
|
|
205
|
-
clearInterval(levelTimer);
|
|
206
|
-
clearTimeout(disconnectTimer);
|
|
207
|
-
if (channel) channel.close();
|
|
208
|
-
if (peer) peer.close();
|
|
209
|
-
if (remoteSource) remoteSource.disconnect();
|
|
210
|
-
if (analyser) analyser.disconnect();
|
|
211
|
-
if (sourceNode) {
|
|
212
|
-
sourceNode.port.postMessage({ type: "mute", muted: true });
|
|
213
|
-
sourceNode.disconnect();
|
|
214
|
-
}
|
|
215
|
-
if (sourceTrack) sourceTrack.stop();
|
|
216
|
-
if (workletUrl) URL.revokeObjectURL(workletUrl);
|
|
217
|
-
if (context && context.state !== "closed") await context.close();
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
window.ompCodexLive = { start, acceptAnswer, waitForOpen, send, pushAudio, setMuted, close };
|
|
221
|
-
})();
|