Switch to double quotes and reformat codebase with Biome
This commit is contained in:
@@ -82,7 +82,7 @@ function resamplePcm16(pcm: Uint8Array, fromRate: number, toRate: number): Uint8
|
||||
|
||||
export function createAudioEngine(
|
||||
callbacks: AudioEngineCallbacks,
|
||||
_options?: AudioEngineTraceOptions
|
||||
_options?: AudioEngineTraceOptions,
|
||||
): AudioEngine {
|
||||
const native = require("@getpaseo/expo-two-way-audio");
|
||||
const instanceId = nextAudioEngineInstanceId++;
|
||||
@@ -115,7 +115,7 @@ export function createAudioEngine(
|
||||
`volume=${bridgeStats.volumeEvents}ev max=${bridgeStats.volumeMax.toFixed(3)} ` +
|
||||
`play=${bridgeStats.playbackEvents}ev/${bridgeStats.playbackInputBytes}B->${bridgeStats.playbackResampledBytes}B ` +
|
||||
`playMs=${bridgeStats.playbackDurationMs.toFixed(1)} ` +
|
||||
`windowMs=${elapsedMs}`
|
||||
`windowMs=${elapsedMs}`,
|
||||
);
|
||||
bridgeStats.windowStartedAtMs = now;
|
||||
bridgeStats.captureEvents = 0;
|
||||
@@ -166,14 +166,14 @@ export function createAudioEngine(
|
||||
if (!refs.sawFirstMicChunk) {
|
||||
refs.sawFirstMicChunk = true;
|
||||
console.log(
|
||||
`[AudioEngine.native#${instanceId}] firstMicChunk bytes=${pcm.byteLength} head=${toHexPreview(pcm)}`
|
||||
`[AudioEngine.native#${instanceId}] firstMicChunk bytes=${pcm.byteLength} head=${toHexPreview(pcm)}`,
|
||||
);
|
||||
}
|
||||
bridgeStats.captureEvents += 1;
|
||||
bridgeStats.captureBytes += pcm.byteLength;
|
||||
maybeFlushBridgeStats("capture");
|
||||
callbacks.onCaptureData(pcm);
|
||||
}
|
||||
},
|
||||
);
|
||||
const volumeSubscription = native.addExpoTwoWayAudioEventListener(
|
||||
"onInputVolumeLevelData",
|
||||
@@ -187,19 +187,19 @@ export function createAudioEngine(
|
||||
if (!refs.sawFirstVolumeEvent) {
|
||||
refs.sawFirstVolumeEvent = true;
|
||||
console.log(
|
||||
`[AudioEngine.native#${instanceId}] firstInputVolume level=${level.toFixed(3)} muted=${refs.muted}`
|
||||
`[AudioEngine.native#${instanceId}] firstInputVolume level=${level.toFixed(3)} muted=${refs.muted}`,
|
||||
);
|
||||
}
|
||||
maybeFlushBridgeStats("volume");
|
||||
callbacks.onVolumeLevel(level);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const outputVolumeSubscription = native.addExpoTwoWayAudioEventListener(
|
||||
"onOutputVolumeLevelData",
|
||||
(event: any) => {
|
||||
console.log(`[AudioEngine.native#${instanceId}] outputVolume=${event.data}`);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
async function ensureInitialized(): Promise<void> {
|
||||
@@ -217,17 +217,17 @@ export function createAudioEngine(
|
||||
async function ensureMicrophonePermission(): Promise<void> {
|
||||
let permission = await native.getMicrophonePermissionsAsync().catch(() => null);
|
||||
console.log(
|
||||
`[AudioEngine.native#${instanceId}] microphonePermission initial=${permission?.status ?? "unknown"} granted=${String(permission?.granted ?? false)}`
|
||||
`[AudioEngine.native#${instanceId}] microphonePermission initial=${permission?.status ?? "unknown"} granted=${String(permission?.granted ?? false)}`,
|
||||
);
|
||||
if (!permission?.granted) {
|
||||
permission = await native.requestMicrophonePermissionsAsync().catch(() => null);
|
||||
console.log(
|
||||
`[AudioEngine.native#${instanceId}] microphonePermission requested=${permission?.status ?? "unknown"} granted=${String(permission?.granted ?? false)}`
|
||||
`[AudioEngine.native#${instanceId}] microphonePermission requested=${permission?.status ?? "unknown"} granted=${String(permission?.granted ?? false)}`,
|
||||
);
|
||||
}
|
||||
if (!permission?.granted) {
|
||||
throw new Error(
|
||||
"Microphone permission is required to capture audio. Please enable microphone access in system settings."
|
||||
"Microphone permission is required to capture audio. Please enable microphone access in system settings.",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -261,7 +261,7 @@ export function createAudioEngine(
|
||||
console.log(
|
||||
`[AudioEngine.native#${instanceId}] playPCMData: inputRate=${inputRate} inputBytes=${pcm.length} ` +
|
||||
`resampled=${pcm16k.length} durationSec=${durationSec.toFixed(3)} ` +
|
||||
`pcmHead=${toHexPreview(pcm)} resampledHead=${toHexPreview(pcm16k)}`
|
||||
`pcmHead=${toHexPreview(pcm)} resampledHead=${toHexPreview(pcm16k)}`,
|
||||
);
|
||||
maybeFlushBridgeStats("play");
|
||||
|
||||
@@ -352,7 +352,7 @@ export function createAudioEngine(
|
||||
const isRecording = native.toggleRecording(true);
|
||||
refs.captureActive = true;
|
||||
console.log(
|
||||
`[AudioEngine.native#${instanceId}] startCapture toggleRecording(true) => ${String(isRecording)}`
|
||||
`[AudioEngine.native#${instanceId}] startCapture toggleRecording(true) => ${String(isRecording)}`,
|
||||
);
|
||||
} catch (error) {
|
||||
const wrapped = error instanceof Error ? error : new Error(String(error));
|
||||
@@ -365,7 +365,7 @@ export function createAudioEngine(
|
||||
if (refs.captureActive) {
|
||||
const isRecording = native.toggleRecording(false);
|
||||
console.log(
|
||||
`[AudioEngine.native#${instanceId}] stopCapture toggleRecording(false) => ${String(isRecording)}`
|
||||
`[AudioEngine.native#${instanceId}] stopCapture toggleRecording(false) => ${String(isRecording)}`,
|
||||
);
|
||||
}
|
||||
refs.captureActive = false;
|
||||
|
||||
@@ -11,7 +11,7 @@ interface QueuedAudio {
|
||||
reject: (error: Error) => void;
|
||||
}
|
||||
|
||||
function getAudioContextCtor(): (typeof AudioContext) | null {
|
||||
function getAudioContextCtor(): typeof AudioContext | null {
|
||||
if (typeof window === "undefined") {
|
||||
return null;
|
||||
}
|
||||
@@ -26,11 +26,7 @@ function floatToInt16(sample: number): number {
|
||||
return clamped < 0 ? Math.round(clamped * 0x8000) : Math.round(clamped * 0x7fff);
|
||||
}
|
||||
|
||||
function resampleToPcm16(
|
||||
input: Float32Array,
|
||||
inputRate: number,
|
||||
outputRate: number
|
||||
): Uint8Array {
|
||||
function resampleToPcm16(input: Float32Array, inputRate: number, outputRate: number): Uint8Array {
|
||||
if (input.length === 0) {
|
||||
return new Uint8Array(0);
|
||||
}
|
||||
@@ -62,7 +58,7 @@ function parsePcmSampleRate(mimeType: string): number | null {
|
||||
function pcm16LeToAudioBuffer(
|
||||
context: AudioContext,
|
||||
bytes: Uint8Array,
|
||||
sampleRate: number
|
||||
sampleRate: number,
|
||||
): AudioBuffer {
|
||||
const sampleCount = Math.floor(bytes.length / 2);
|
||||
const audioBuffer = context.createBuffer(1, sampleCount, sampleRate);
|
||||
@@ -79,10 +75,7 @@ function pcm16LeToAudioBuffer(
|
||||
return audioBuffer;
|
||||
}
|
||||
|
||||
async function decodeAudioData(
|
||||
context: AudioContext,
|
||||
buffer: ArrayBuffer
|
||||
): Promise<AudioBuffer> {
|
||||
async function decodeAudioData(context: AudioContext, buffer: ArrayBuffer): Promise<AudioBuffer> {
|
||||
const maybePromise = context.decodeAudioData(buffer.slice(0));
|
||||
if (maybePromise && typeof (maybePromise as Promise<AudioBuffer>).then === "function") {
|
||||
return maybePromise as Promise<AudioBuffer>;
|
||||
@@ -94,7 +87,7 @@ async function decodeAudioData(
|
||||
|
||||
export function createAudioEngine(
|
||||
callbacks: AudioEngineCallbacks,
|
||||
_options?: { traceLabel?: string }
|
||||
_options?: { traceLabel?: string },
|
||||
): AudioEngine {
|
||||
const refs: {
|
||||
playbackContext: AudioContext | null;
|
||||
@@ -177,7 +170,7 @@ export function createAudioEngine(
|
||||
? pcm16LeToAudioBuffer(
|
||||
context,
|
||||
new Uint8Array(arrayBuffer),
|
||||
parsePcmSampleRate(type) ?? 24000
|
||||
parsePcmSampleRate(type) ?? 24000,
|
||||
)
|
||||
: await decodeAudioData(context, arrayBuffer);
|
||||
|
||||
@@ -206,9 +199,7 @@ export function createAudioEngine(
|
||||
try {
|
||||
source.start();
|
||||
} catch (error) {
|
||||
settle(() =>
|
||||
reject(error instanceof Error ? error : new Error(String(error)))
|
||||
);
|
||||
settle(() => reject(error instanceof Error ? error : new Error(String(error))));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -297,9 +288,7 @@ export function createAudioEngine(
|
||||
? window.isSecureContext
|
||||
: true;
|
||||
const currentOrigin =
|
||||
typeof window !== "undefined" && window.location
|
||||
? window.location.origin
|
||||
: "unknown";
|
||||
typeof window !== "undefined" && window.location ? window.location.origin : "unknown";
|
||||
const isDesktopApp = isDesktop();
|
||||
|
||||
if (missingNavigator) {
|
||||
@@ -307,7 +296,7 @@ export function createAudioEngine(
|
||||
}
|
||||
if (!secureContext && !isDesktopApp) {
|
||||
throw new Error(
|
||||
`Microphone access requires HTTPS or localhost. Current origin: ${currentOrigin}`
|
||||
`Microphone access requires HTTPS or localhost. Current origin: ${currentOrigin}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ describe("voice runtime", () => {
|
||||
|
||||
expect(adapter.sendVoiceAudioChunk).toHaveBeenLastCalledWith(
|
||||
expect.any(String),
|
||||
"audio/pcm;rate=16000;bits=16"
|
||||
"audio/pcm;rate=16000;bits=16",
|
||||
);
|
||||
|
||||
runtime.onTranscriptionResult("server-1", "hello");
|
||||
@@ -146,7 +146,7 @@ describe("voice runtime", () => {
|
||||
() =>
|
||||
new Promise<number>((resolve) => {
|
||||
resolvePlay = resolve;
|
||||
})
|
||||
}),
|
||||
);
|
||||
const { runtime } = createRuntime({ engine });
|
||||
runtime.registerSession(adapter);
|
||||
@@ -161,7 +161,7 @@ describe("voice runtime", () => {
|
||||
groupId: "group-1",
|
||||
chunkIndex: 0,
|
||||
isLastChunk: false,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
@@ -177,7 +177,7 @@ describe("voice runtime", () => {
|
||||
groupId: "group-1",
|
||||
chunkIndex: 1,
|
||||
isLastChunk: true,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
expect(engine.play).toHaveBeenCalledTimes(1);
|
||||
@@ -197,7 +197,7 @@ describe("voice runtime", () => {
|
||||
() =>
|
||||
new Promise<number>((resolve) => {
|
||||
playResolvers.push(resolve);
|
||||
})
|
||||
}),
|
||||
);
|
||||
const { runtime } = createRuntime({ engine });
|
||||
runtime.registerSession(adapter);
|
||||
@@ -212,7 +212,7 @@ describe("voice runtime", () => {
|
||||
groupId: "group-1",
|
||||
chunkIndex: 0,
|
||||
isLastChunk: false,
|
||||
})
|
||||
}),
|
||||
);
|
||||
runtime.handleAudioOutput(
|
||||
"server-1",
|
||||
@@ -221,7 +221,7 @@ describe("voice runtime", () => {
|
||||
groupId: "group-1",
|
||||
chunkIndex: 1,
|
||||
isLastChunk: true,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
@@ -276,7 +276,7 @@ describe("voice runtime", () => {
|
||||
() =>
|
||||
new Promise<number>((resolve) => {
|
||||
resolvePlay = resolve;
|
||||
})
|
||||
}),
|
||||
);
|
||||
const { runtime } = createRuntime({ engine });
|
||||
runtime.registerSession(adapter);
|
||||
@@ -385,7 +385,7 @@ describe("voice runtime", () => {
|
||||
it("returns an explicit not-ready error when the adapter is missing", async () => {
|
||||
const { runtime } = createRuntime();
|
||||
await expect(runtime.startVoice("server-1", "agent-1")).rejects.toThrow(
|
||||
"Voice runtime is not ready for host server-1"
|
||||
"Voice runtime is not ready for host server-1",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { Buffer } from "buffer";
|
||||
import type {
|
||||
AgentStreamEventPayload,
|
||||
SessionOutboundMessage,
|
||||
} from "@server/shared/messages";
|
||||
import type { AgentStreamEventPayload, SessionOutboundMessage } from "@server/shared/messages";
|
||||
import { resolveVoiceUnavailableMessage } from "@/utils/server-info-capabilities";
|
||||
import type { DaemonServerInfo } from "@/stores/session-store";
|
||||
import type { AudioEngine } from "@/voice/audio-engine-types";
|
||||
@@ -89,10 +86,7 @@ interface RuntimeState {
|
||||
localAboveThresholdActive: boolean;
|
||||
}
|
||||
|
||||
type AudioOutputPayload = Extract<
|
||||
SessionOutboundMessage,
|
||||
{ type: "audio_output" }
|
||||
>["payload"];
|
||||
type AudioOutputPayload = Extract<SessionOutboundMessage, { type: "audio_output" }>["payload"];
|
||||
|
||||
type StreamingPlaybackChunk = {
|
||||
id: string;
|
||||
@@ -156,10 +150,7 @@ const INITIAL_TELEMETRY: VoiceRuntimeTelemetrySnapshot = {
|
||||
|
||||
let nextVoiceRuntimeInstanceId = 1;
|
||||
|
||||
function snapshotsEqual(
|
||||
left: VoiceRuntimeSnapshot,
|
||||
right: VoiceRuntimeSnapshot
|
||||
): boolean {
|
||||
function snapshotsEqual(left: VoiceRuntimeSnapshot, right: VoiceRuntimeSnapshot): boolean {
|
||||
return (
|
||||
left.phase === right.phase &&
|
||||
left.isVoiceMode === right.isVoiceMode &&
|
||||
@@ -172,7 +163,7 @@ function snapshotsEqual(
|
||||
|
||||
function telemetryEqual(
|
||||
left: VoiceRuntimeTelemetrySnapshot,
|
||||
right: VoiceRuntimeTelemetrySnapshot
|
||||
right: VoiceRuntimeTelemetrySnapshot,
|
||||
): boolean {
|
||||
return (
|
||||
left.volume === right.volume &&
|
||||
@@ -252,10 +243,7 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
||||
size: cuePcm16.byteLength,
|
||||
type: "audio/pcm;rate=16000;bits=16",
|
||||
async arrayBuffer() {
|
||||
return cuePcm16.buffer.slice(
|
||||
cuePcm16.byteOffset,
|
||||
cuePcm16.byteOffset + cuePcm16.byteLength
|
||||
);
|
||||
return cuePcm16.buffer.slice(cuePcm16.byteOffset, cuePcm16.byteOffset + cuePcm16.byteLength);
|
||||
},
|
||||
};
|
||||
let lagProbeLastMs = Date.now();
|
||||
@@ -279,7 +267,7 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
||||
`capture=${bridgeStats.captureEvents}ev/${bridgeStats.captureBytes}B ` +
|
||||
`uplink=${bridgeStats.uplinkEvents}ev/${bridgeStats.uplinkRawBytes}B/${bridgeStats.uplinkBase64Chars}c ` +
|
||||
`output=${bridgeStats.outputEvents}ev/${bridgeStats.outputBytes}B groups=${bridgeStats.outputGroups} ` +
|
||||
`jsLagMaxMs=${bridgeStats.jsLagMaxMs} windowMs=${elapsedMs}`
|
||||
`jsLagMaxMs=${bridgeStats.jsLagMaxMs} windowMs=${elapsedMs}`,
|
||||
);
|
||||
bridgeStats.windowStartedAtMs = now;
|
||||
bridgeStats.captureEvents = 0;
|
||||
@@ -308,12 +296,10 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
||||
function patchSnapshot(
|
||||
patch:
|
||||
| Partial<VoiceRuntimeSnapshot>
|
||||
| ((previous: VoiceRuntimeSnapshot) => VoiceRuntimeSnapshot)
|
||||
| ((previous: VoiceRuntimeSnapshot) => VoiceRuntimeSnapshot),
|
||||
): void {
|
||||
const next =
|
||||
typeof patch === "function"
|
||||
? patch(state.snapshot)
|
||||
: { ...state.snapshot, ...patch };
|
||||
typeof patch === "function" ? patch(state.snapshot) : { ...state.snapshot, ...patch };
|
||||
if (snapshotsEqual(next, state.snapshot)) {
|
||||
return;
|
||||
}
|
||||
@@ -325,14 +311,10 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
||||
function patchTelemetry(
|
||||
patch:
|
||||
| Partial<VoiceRuntimeTelemetrySnapshot>
|
||||
| ((
|
||||
previous: VoiceRuntimeTelemetrySnapshot
|
||||
) => VoiceRuntimeTelemetrySnapshot)
|
||||
| ((previous: VoiceRuntimeTelemetrySnapshot) => VoiceRuntimeTelemetrySnapshot),
|
||||
): void {
|
||||
const next =
|
||||
typeof patch === "function"
|
||||
? patch(state.telemetry)
|
||||
: { ...state.telemetry, ...patch };
|
||||
typeof patch === "function" ? patch(state.telemetry) : { ...state.telemetry, ...patch };
|
||||
if (telemetryEqual(next, state.telemetry)) {
|
||||
return;
|
||||
}
|
||||
@@ -353,7 +335,7 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
||||
|
||||
function toPlaybackSource(
|
||||
bytes: Uint8Array,
|
||||
format: string
|
||||
format: string,
|
||||
): { arrayBuffer(): Promise<ArrayBuffer>; size: number; type: string } {
|
||||
const mimeType =
|
||||
format === "pcm"
|
||||
@@ -379,7 +361,9 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
||||
playback.activeGroupId = null;
|
||||
playback.processing = false;
|
||||
if (hadGroups > 0) {
|
||||
console.log(`[VoiceRuntime] resetPlaybackState: cleared ${hadGroups} groups, new gen=${playback.generation}`);
|
||||
console.log(
|
||||
`[VoiceRuntime] resetPlaybackState: cleared ${hadGroups} groups, new gen=${playback.generation}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,11 +394,15 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
||||
|
||||
playback.processing = true;
|
||||
const generation = playback.generation;
|
||||
console.log(`[VoiceRuntime] processPlaybackQueue start gen=${generation} activeGroup=${playback.activeGroupId}`);
|
||||
console.log(
|
||||
`[VoiceRuntime] processPlaybackQueue start gen=${generation} activeGroup=${playback.activeGroupId}`,
|
||||
);
|
||||
try {
|
||||
while (playback.activeGroupId) {
|
||||
if (generation !== playback.generation) {
|
||||
console.log(`[VoiceRuntime] processPlaybackQueue abort: generation changed ${generation} -> ${playback.generation}`);
|
||||
console.log(
|
||||
`[VoiceRuntime] processPlaybackQueue abort: generation changed ${generation} -> ${playback.generation}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -426,17 +414,16 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
||||
|
||||
const nextChunk = group.chunks.get(group.nextChunkToPlay);
|
||||
if (!nextChunk) {
|
||||
if (
|
||||
group.finalChunkIndex !== null &&
|
||||
group.nextChunkToPlay > group.finalChunkIndex
|
||||
) {
|
||||
console.log(`[VoiceRuntime] group=${group.groupId} complete, played=${group.started} chunks=${group.nextChunkToPlay}`);
|
||||
if (group.finalChunkIndex !== null && group.nextChunkToPlay > group.finalChunkIndex) {
|
||||
console.log(
|
||||
`[VoiceRuntime] group=${group.groupId} complete, played=${group.started} chunks=${group.nextChunkToPlay}`,
|
||||
);
|
||||
playback.groups.delete(group.groupId);
|
||||
if (playback.orderedGroupIds[0] === group.groupId) {
|
||||
playback.orderedGroupIds.shift();
|
||||
} else {
|
||||
playback.orderedGroupIds = playback.orderedGroupIds.filter(
|
||||
(value) => value !== group.groupId
|
||||
(value) => value !== group.groupId,
|
||||
);
|
||||
}
|
||||
if (group.started && group.isVoiceMode) {
|
||||
@@ -445,7 +432,9 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
||||
activateNextPlaybackGroup();
|
||||
continue;
|
||||
}
|
||||
console.log(`[VoiceRuntime] group=${group.groupId} waiting for chunk=${group.nextChunkToPlay} (finalChunkIndex=${group.finalChunkIndex})`);
|
||||
console.log(
|
||||
`[VoiceRuntime] group=${group.groupId} waiting for chunk=${group.nextChunkToPlay} (finalChunkIndex=${group.finalChunkIndex})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -453,7 +442,9 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
||||
|
||||
if (group.shouldPlay && !group.started && group.isVoiceMode) {
|
||||
group.started = true;
|
||||
console.log(`[VoiceRuntime] group=${group.groupId} first play starting at chunk=${group.nextChunkToPlay}`);
|
||||
console.log(
|
||||
`[VoiceRuntime] group=${group.groupId} first play starting at chunk=${group.nextChunkToPlay}`,
|
||||
);
|
||||
api.onAssistantAudioStarted(serverId);
|
||||
}
|
||||
|
||||
@@ -461,20 +452,29 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
||||
try {
|
||||
if (group.shouldPlay) {
|
||||
await deps.engine.play(nextChunk.source);
|
||||
console.log(`[VoiceRuntime] played chunk=${group.nextChunkToPlay} id=${nextChunk.id} took=${Date.now() - playStart}ms`);
|
||||
console.log(
|
||||
`[VoiceRuntime] played chunk=${group.nextChunkToPlay} id=${nextChunk.id} took=${Date.now() - playStart}ms`,
|
||||
);
|
||||
} else {
|
||||
console.log(`[VoiceRuntime] SKIPPED chunk=${group.nextChunkToPlay} id=${nextChunk.id} shouldPlay=false`);
|
||||
console.log(
|
||||
`[VoiceRuntime] SKIPPED chunk=${group.nextChunkToPlay} id=${nextChunk.id} shouldPlay=false`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (generation !== playback.generation) {
|
||||
console.log(`[VoiceRuntime] play error + generation changed, aborting`);
|
||||
return;
|
||||
}
|
||||
console.error(`[VoiceRuntime] play error chunk=${group.nextChunkToPlay} took=${Date.now() - playStart}ms:`, error);
|
||||
console.error(
|
||||
`[VoiceRuntime] play error chunk=${group.nextChunkToPlay} took=${Date.now() - playStart}ms:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
if (generation !== playback.generation) {
|
||||
console.log(`[VoiceRuntime] post-play generation changed ${generation} -> ${playback.generation}`);
|
||||
console.log(
|
||||
`[VoiceRuntime] post-play generation changed ${generation} -> ${playback.generation}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -491,7 +491,9 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
||||
if (generation === playback.generation) {
|
||||
playback.processing = false;
|
||||
}
|
||||
console.log(`[VoiceRuntime] processPlaybackQueue exit gen=${generation} currentGen=${playback.generation}`);
|
||||
console.log(
|
||||
`[VoiceRuntime] processPlaybackQueue exit gen=${generation} currentGen=${playback.generation}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -580,7 +582,10 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
||||
if (!cue.active || cue.token !== token) {
|
||||
return;
|
||||
}
|
||||
cue.timeout = setTimeout(playNext, THINKING_TONE_NATIVE_PCM_DURATION_MS + THINKING_TONE_REPEAT_GAP_MS);
|
||||
cue.timeout = setTimeout(
|
||||
playNext,
|
||||
THINKING_TONE_NATIVE_PCM_DURATION_MS + THINKING_TONE_REPEAT_GAP_MS,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -608,11 +613,9 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
||||
bridgeStats.uplinkBase64Chars += base64.length;
|
||||
flushBridgeStats("uplink");
|
||||
|
||||
void activeSession.adapter
|
||||
.sendVoiceAudioChunk(base64, PCM_MIME_TYPE)
|
||||
.catch((error) => {
|
||||
console.error(`[VoiceRuntime#${instanceId}] Failed to send audio chunk:`, error);
|
||||
});
|
||||
void activeSession.adapter.sendVoiceAudioChunk(base64, PCM_MIME_TYPE).catch((error) => {
|
||||
console.error(`[VoiceRuntime#${instanceId}] Failed to send audio chunk:`, error);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -630,17 +633,14 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
||||
|
||||
function publishDisplayVolume(level: number, nowMs: number): void {
|
||||
const previousVolume = state.telemetry.volume;
|
||||
const smoothing =
|
||||
level >= previousVolume ? DISPLAY_VOLUME_ATTACK : DISPLAY_VOLUME_RELEASE;
|
||||
const smoothing = level >= previousVolume ? DISPLAY_VOLUME_ATTACK : DISPLAY_VOLUME_RELEASE;
|
||||
const nextVolume = Math.max(
|
||||
0,
|
||||
Math.min(1, previousVolume + (level - previousVolume) * smoothing)
|
||||
Math.min(1, previousVolume + (level - previousVolume) * smoothing),
|
||||
);
|
||||
const enoughTimeElapsed =
|
||||
nowMs - state.lastDisplayVolumePublishMs >=
|
||||
DISPLAY_VOLUME_PUBLISH_INTERVAL_MS;
|
||||
const enoughChange =
|
||||
Math.abs(nextVolume - previousVolume) >= DISPLAY_VOLUME_CHANGE_EPSILON;
|
||||
nowMs - state.lastDisplayVolumePublishMs >= DISPLAY_VOLUME_PUBLISH_INTERVAL_MS;
|
||||
const enoughChange = Math.abs(nextVolume - previousVolume) >= DISPLAY_VOLUME_CHANGE_EPSILON;
|
||||
|
||||
if (!enoughTimeElapsed && !enoughChange) {
|
||||
return;
|
||||
@@ -748,7 +748,7 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
||||
}
|
||||
if (bridgeStats.captureEvents === 0) {
|
||||
console.log(
|
||||
`[VoiceRuntime#${instanceId}] firstCapturePcm bytes=${chunk.byteLength} phase=${state.snapshot.phase} transportReady=${state.transportReady}`
|
||||
`[VoiceRuntime#${instanceId}] firstCapturePcm bytes=${chunk.byteLength} phase=${state.snapshot.phase} transportReady=${state.transportReady}`,
|
||||
);
|
||||
}
|
||||
uploader.pushPcmChunk(chunk);
|
||||
@@ -771,13 +771,13 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
||||
if (isActive && !state.localAboveThresholdActive) {
|
||||
state.localAboveThresholdActive = true;
|
||||
console.log(
|
||||
`[VoiceRuntime#${instanceId}] localSpeechActive level=${level.toFixed(3)} threshold=${REALTIME_VOICE_VAD_CONFIG.volumeThreshold.toFixed(3)} phase=${state.snapshot.phase} transportReady=${state.transportReady}`
|
||||
`[VoiceRuntime#${instanceId}] localSpeechActive level=${level.toFixed(3)} threshold=${REALTIME_VOICE_VAD_CONFIG.volumeThreshold.toFixed(3)} phase=${state.snapshot.phase} transportReady=${state.transportReady}`,
|
||||
);
|
||||
}
|
||||
if (!isActive && state.localAboveThresholdActive) {
|
||||
state.localAboveThresholdActive = false;
|
||||
console.log(
|
||||
`[VoiceRuntime#${instanceId}] localSpeechInactive phase=${state.snapshot.phase} serverSpeaking=${state.serverSpeechDetected}`
|
||||
`[VoiceRuntime#${instanceId}] localSpeechInactive phase=${state.snapshot.phase} serverSpeaking=${state.serverSpeechDetected}`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
@@ -787,7 +787,7 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
||||
) {
|
||||
state.lastNoServerSpeechLogMs = nowMs;
|
||||
console.log(
|
||||
`[VoiceRuntime#${instanceId}] localSpeechWithoutServerSpeech level=${level.toFixed(3)} phase=${state.snapshot.phase} turnInProgress=${state.turnInProgress} transportReady=${state.transportReady}`
|
||||
`[VoiceRuntime#${instanceId}] localSpeechWithoutServerSpeech level=${level.toFixed(3)} phase=${state.snapshot.phase} turnInProgress=${state.turnInProgress} transportReady=${state.transportReady}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -805,7 +805,9 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
||||
!state.snapshot.isVoiceMode ||
|
||||
!payload.isVoiceMode
|
||||
) {
|
||||
console.log(`[VoiceRuntime#${instanceId}] audio_output DROPPED: activeServer=${state.snapshot.activeServerId} serverId=${serverId} isVoiceMode=${state.snapshot.isVoiceMode} payloadVoice=${payload.isVoiceMode}`);
|
||||
console.log(
|
||||
`[VoiceRuntime#${instanceId}] audio_output DROPPED: activeServer=${state.snapshot.activeServerId} serverId=${serverId} isVoiceMode=${state.snapshot.isVoiceMode} payloadVoice=${payload.isVoiceMode}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -818,14 +820,18 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
||||
console.log(
|
||||
`[VoiceRuntime#${instanceId}] audio_output groupId=${groupId} chunk=${chunkIndex} isLast=${payload.isLastChunk} ` +
|
||||
`base64Chars=${payload.audio.length} decodedBytes=${decoded.byteLength} format=${payload.format} ` +
|
||||
`head=${Array.from(decoded.slice(0, 12)).map((value) => value.toString(16).padStart(2, "0")).join(" ")}`
|
||||
`head=${Array.from(decoded.slice(0, 12))
|
||||
.map((value) => value.toString(16).padStart(2, "0"))
|
||||
.join(" ")}`,
|
||||
);
|
||||
flushBridgeStats("audio_output");
|
||||
|
||||
let group = playback.groups.get(groupId);
|
||||
if (!group) {
|
||||
const shouldPlay = api.shouldPlayVoiceAudio(serverId);
|
||||
console.log(`[VoiceRuntime] new group=${groupId} shouldPlay=${shouldPlay} phase=${state.snapshot.phase} isSpeaking=${state.telemetry.isSpeaking}`);
|
||||
console.log(
|
||||
`[VoiceRuntime] new group=${groupId} shouldPlay=${shouldPlay} phase=${state.snapshot.phase} isSpeaking=${state.telemetry.isSpeaking}`,
|
||||
);
|
||||
group = {
|
||||
groupId,
|
||||
isVoiceMode: payload.isVoiceMode,
|
||||
@@ -1000,10 +1006,7 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
||||
},
|
||||
|
||||
onAssistantAudioStarted(serverId) {
|
||||
if (
|
||||
!state.snapshot.isVoiceMode ||
|
||||
state.snapshot.activeServerId !== serverId
|
||||
) {
|
||||
if (!state.snapshot.isVoiceMode || state.snapshot.activeServerId !== serverId) {
|
||||
return;
|
||||
}
|
||||
stopCue();
|
||||
@@ -1030,10 +1033,7 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
||||
},
|
||||
|
||||
onTranscriptionResult(serverId, text) {
|
||||
if (
|
||||
serverId !== state.snapshot.activeServerId ||
|
||||
!state.snapshot.isVoiceMode
|
||||
) {
|
||||
if (serverId !== state.snapshot.activeServerId || !state.snapshot.isVoiceMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1050,15 +1050,12 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
||||
},
|
||||
|
||||
onServerSpeechStateChanged(serverId, isSpeaking) {
|
||||
if (
|
||||
serverId !== state.snapshot.activeServerId ||
|
||||
!state.snapshot.isVoiceMode
|
||||
) {
|
||||
if (serverId !== state.snapshot.activeServerId || !state.snapshot.isVoiceMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[VoiceRuntime#${instanceId}] onServerSpeechStateChanged isSpeaking=${isSpeaking} phase=${state.snapshot.phase} volume=${state.telemetry.volume}`
|
||||
`[VoiceRuntime#${instanceId}] onServerSpeechStateChanged isSpeaking=${isSpeaking} phase=${state.snapshot.phase} volume=${state.telemetry.volume}`,
|
||||
);
|
||||
state.serverSpeechDetected = isSpeaking;
|
||||
state.serverSpeechStartedAt = isSpeaking ? (state.serverSpeechStartedAt ?? Date.now()) : null;
|
||||
|
||||
Reference in New Issue
Block a user