mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
252 lines
6.4 KiB
JavaScript
252 lines
6.4 KiB
JavaScript
import { GeminiSessionController } from '../voice/geminiSession.js';
|
|
import { createNarrationScheduler } from '../voice/narration.js';
|
|
|
|
const endpoint =
|
|
'wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContentConstrained';
|
|
const credential = {
|
|
token: 'auth_tokens/test',
|
|
model: 'gemini-3.8-live',
|
|
websocketUrl: endpoint,
|
|
config: { generationConfig: { responseModalities: ['AUDIO'] } },
|
|
};
|
|
const manualCredential = {
|
|
...credential,
|
|
config: {
|
|
...credential.config,
|
|
realtimeInputConfig: { automaticActivityDetection: { disabled: true } },
|
|
},
|
|
};
|
|
const tick = () => new Promise((resolve) => setImmediate(resolve));
|
|
function deferred() {
|
|
let resolve;
|
|
const promise = new Promise((done) => {
|
|
resolve = done;
|
|
});
|
|
return { promise, resolve };
|
|
}
|
|
function harness(overrides = {}) {
|
|
const { autoSetup = true, ...controllerOverrides } = overrides;
|
|
const events = [];
|
|
const sockets = [];
|
|
const media = [];
|
|
const tracks = [];
|
|
const captures = [];
|
|
const calls = [];
|
|
const requests = [];
|
|
const state = { playing: 0, ducked: false, stopped: 0 };
|
|
let clock = 0;
|
|
let nextTimer = 0;
|
|
const timers = new Map();
|
|
const earcon = { playing: false, starts: 0, disposals: 0 };
|
|
const advance = (ms) => {
|
|
const target = clock + ms;
|
|
for (;;) {
|
|
const due = [...timers]
|
|
.filter(([, timer]) => timer.at <= target)
|
|
.sort((a, b) => a[1].at - b[1].at)[0];
|
|
if (!due) break;
|
|
timers.delete(due[0]);
|
|
clock = due[1].at;
|
|
due[1].fn();
|
|
}
|
|
clock = target;
|
|
};
|
|
let radioControl;
|
|
const radio = {
|
|
setVoiceDucked(value) {
|
|
state.ducked = value;
|
|
},
|
|
pause() {},
|
|
subscribePlaybackControls(callback) {
|
|
radioControl = callback;
|
|
return () => {
|
|
radioControl = null;
|
|
};
|
|
},
|
|
async playForVoice() {
|
|
state.playing++;
|
|
return true;
|
|
},
|
|
stopPlayback() {
|
|
state.stopped++;
|
|
},
|
|
};
|
|
class Socket {
|
|
constructor(url) {
|
|
this.url = url;
|
|
this.readyState = 0;
|
|
this.bufferedAmount = 0;
|
|
this.sent = [];
|
|
sockets.push(this);
|
|
queueMicrotask(() => {
|
|
this.readyState = 1;
|
|
this.onopen?.();
|
|
});
|
|
}
|
|
send(raw) {
|
|
const value = JSON.parse(raw);
|
|
this.sent.push(value);
|
|
if (value.setup && autoSetup)
|
|
queueMicrotask(() => this.message({ setupComplete: {} }));
|
|
}
|
|
message(value) {
|
|
this.onmessage?.({ data: JSON.stringify(value) });
|
|
}
|
|
error() {
|
|
this.onerror?.();
|
|
}
|
|
remoteClose() {
|
|
this.readyState = 3;
|
|
this.onclose?.();
|
|
}
|
|
close() {
|
|
this.readyState = 3;
|
|
this.closed = true;
|
|
}
|
|
}
|
|
const makeStream = () => {
|
|
const track = {
|
|
enabled: true,
|
|
readyState: 'live',
|
|
stop() {
|
|
this.readyState = 'ended';
|
|
},
|
|
};
|
|
tracks.push(track);
|
|
return { getTracks: () => [track], getAudioTracks: () => [track] };
|
|
};
|
|
const c = new GeminiSessionController({
|
|
emit: (event) => events.push(event),
|
|
runAction: async (name, args, options) => {
|
|
calls.push({ name, args, options });
|
|
return { ok: true, action: name };
|
|
},
|
|
ui: { root: { remove() {} } },
|
|
radioLayer: radio,
|
|
now: () => clock,
|
|
setTimer: (fn, ms) => {
|
|
const id = ++nextTimer;
|
|
timers.set(id, { fn, at: clock + ms });
|
|
return id;
|
|
},
|
|
clearTimer: (id) => timers.delete(id),
|
|
WebSocketClass: Socket,
|
|
fetchImpl: async (_url, init) => {
|
|
requests.push(init);
|
|
return Response.json(
|
|
JSON.parse(init.body).inputMode === 'push-to-talk'
|
|
? manualCredential
|
|
: credential,
|
|
);
|
|
},
|
|
getUserMedia: async () => makeStream(),
|
|
createInput: ({ readStream, operations }) => ({
|
|
...operations,
|
|
setMicrophoneEnabled(enabled) {
|
|
for (const track of readStream()?.getAudioTracks() || [])
|
|
track.enabled = enabled;
|
|
},
|
|
updateVoiceButtonLabel() {},
|
|
setVoiceSpeaker() {},
|
|
startVoiceVisualizer() {},
|
|
startAssistantVoiceVisualizer() {},
|
|
cancelPushToTalkHold() {},
|
|
stopVoiceVisualizer() {},
|
|
detachBindings() {},
|
|
bindPushToTalkShortcut() {},
|
|
resetSession() {
|
|
this.pushToTalkMode = false;
|
|
this.pushToTalkKeyHeld = false;
|
|
this.spaceKeyHeld = false;
|
|
},
|
|
}),
|
|
createNarration: (options) =>
|
|
createNarrationScheduler({
|
|
...options,
|
|
now: () => clock,
|
|
setTimer: (fn, ms) => {
|
|
const id = ++nextTimer;
|
|
timers.set(id, { fn, at: clock + ms });
|
|
return id;
|
|
},
|
|
clearTimer: (id) => timers.delete(id),
|
|
}),
|
|
createProgressEarcon: ({ isMicrophoneMuted }) => ({
|
|
available: isMicrophoneMuted,
|
|
start() {
|
|
earcon.playing = true;
|
|
earcon.starts++;
|
|
},
|
|
stop() {
|
|
earcon.playing = false;
|
|
},
|
|
dispose() {
|
|
this.stop();
|
|
earcon.disposals++;
|
|
},
|
|
}),
|
|
createAudio: (callbacks) => {
|
|
const audio = {
|
|
callbacks,
|
|
pending: false,
|
|
closed: false,
|
|
played: [],
|
|
async initialize() {},
|
|
connectMicrophone(stream, enabled, metadata) {
|
|
media.push({ stream, enabled });
|
|
this.connected = true;
|
|
this.setMicrophoneEnabled(enabled, metadata);
|
|
},
|
|
setMicrophoneEnabled(value, metadata) {
|
|
if (!this.connected) return;
|
|
if (value && !this.enabled) {
|
|
this.metadata = metadata;
|
|
callbacks.onAudioStart(metadata);
|
|
}
|
|
if (!value && this.enabled) callbacks.onAudioEnd(this.metadata);
|
|
this.enabled = value;
|
|
},
|
|
play(data, mime) {
|
|
this.pending = true;
|
|
this.played.push({ data, mime });
|
|
},
|
|
drain() {
|
|
this.pending = false;
|
|
callbacks.onDrain();
|
|
},
|
|
clearPlayback() {
|
|
this.pending = false;
|
|
this.cleared = true;
|
|
},
|
|
diagnostics() {
|
|
return { outputChunks: this.played.length };
|
|
},
|
|
close() {
|
|
this.closed = true;
|
|
},
|
|
};
|
|
captures.push(audio);
|
|
return audio;
|
|
},
|
|
...controllerOverrides,
|
|
});
|
|
return {
|
|
c,
|
|
sockets,
|
|
tracks,
|
|
makeStream,
|
|
events,
|
|
media,
|
|
captures,
|
|
calls,
|
|
requests,
|
|
state,
|
|
advance,
|
|
timers,
|
|
earcon,
|
|
radioControl: (value) => radioControl?.(value),
|
|
};
|
|
}
|
|
|
|
export { harness, tick, deferred };
|