Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
font-src 'self' https://fonts.gstatic.com data:;
img-src 'self' data: blob:;
media-src 'self' blob:;
connect-src 'self' https://generativelanguage.googleapis.com wss://generativelanguage.googleapis.com;
connect-src 'self' https://generativelanguage.googleapis.com wss://generativelanguage.googleapis.com https://*.workers.dev https://huggingface.co https://cdn-lfs.huggingface.co;
worker-src 'self' blob:;
base-uri 'self';
form-action 'self';
Expand Down
90 changes: 63 additions & 27 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
},
"dependencies": {
"@google/genai": "^1.32.0",
"@mlc-ai/web-llm": "^0.2.80",
"@react-three/drei": "^9.120.0",
"@react-three/fiber": "^9.0.0-rc.3",
"@tailwindcss/postcss": "^4.1.18",
Expand All @@ -22,6 +23,7 @@
"@vitejs/plugin-react": "^5.1.2",
"fake-indexeddb": "^6.2.5",
"happy-dom": "^20.3.7",
"idb": "^8.0.3",
"lucide-react": "^0.559.0",
"onnxruntime-web": "1.17.1",
"react": "^18.3.1",
Expand Down
21 changes: 21 additions & 0 deletions services/airlockClient.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/// <reference types="vite/client" />
import { GoogleGenAI } from "@google/genai";

const DEFAULT_API_VERSION = "v1beta";
Expand Down Expand Up @@ -26,3 +27,23 @@ export const createAirlockClient = (): GoogleGenAI => {
},
});
};

/**
* Establishes a raw WebSocket connection to the Gemini Live API via Airlock.
* Bypasses the SDK entirely for maximum control.
*/
export const connectLive = (modelId: string): WebSocket => {
const baseUrl = getAirlockBaseUrl();
// Transform HTTP URL to WebSocket URL
// e.g. https://worker.dev -> wss://worker.dev/v1beta/models/gemini-2.0-flash-exp/BidiWebsocket
const wsUrl = baseUrl.replace(/^http/, 'ws') + `/v1beta/models/${modelId}/BidiWebsocket`;

const ws = new WebSocket(wsUrl);

// Standard Airlock Headers are not supported in browser WebSocket API directly
// The Worker/Server must accept the connection authentication via protocol or init message.
// Assuming Airlock handles the proxying transparently or we send auth in the setup message.

return ws;
};

50 changes: 50 additions & 0 deletions services/audioManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,56 @@ class ZenAudioProcessor extends AudioWorkletProcessor {
registerProcessor('zen-audio-processor', ZenAudioProcessor);
`;

// --- RESAMPLER ---

/**
* Resamples audio buffer using Windowed Sinc (Lanczos) Interpolation.
* SOTA 2026 Standard for high-fidelity audio resampling (prevents aliasing).
*/
export const resampleAudio = (audioBuffer: Float32Array, fromSampleRate: number, toSampleRate: number): Float32Array => {
if (fromSampleRate === toSampleRate) return audioBuffer;

const ratio = fromSampleRate / toSampleRate;
const newLength = Math.round(audioBuffer.length / ratio);
const result = new Float32Array(newLength);
const width = 3; // Lanczos window size (a=3 for high quality)

const sinc = (x: number) => {
if (x === 0) return 1;
const piX = Math.PI * x;
return Math.sin(piX) / piX;
};

const lanczos = (x: number) => {
if (Math.abs(x) >= width) return 0;
return sinc(x) * sinc(x / width);
};

for (let i = 0; i < newLength; i++) {
const center = i * ratio;
const start = Math.ceil(center - width);
const end = Math.floor(center + width);

let sum = 0;
let weightSum = 0;

for (let j = start; j <= end; j++) {
if (j >= 0 && j < audioBuffer.length) {
const weight = lanczos(center - j);
sum += audioBuffer[j] * weight;
weightSum += weight; // Optional normalization
}
}

// Normalization prevents amplitude loss
result[i] = weightSum !== 0 ? sum / weightSum : sum;
}

return result;
};

// --- AUDIO HELPERS ---

/**
* Utility to convert Float32 to 16-bit PCM for Gemini
*/
Expand Down
65 changes: 47 additions & 18 deletions services/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,52 @@ export class VaultService {
this.isVaultUnlocked = true;
}

/**
* Unlock the vault using WebAuthn Passkey (PRF)
*/
static async unlockWithWebAuthn(userName: string = "ZenUser"): Promise<boolean> {
try {
if (!await this.hasPasskey()) return false;

const credId = await this.readFromIDB(PASSKEY_ID_KEY);
const wrappedBlob = await this.readFromIDB(PASSKEY_WRAPPED_KEY_ID);

// For simplified flow, we use a fixed salt for PRF output generation (simplified demo)
const prfSalt = new Uint8Array(32).fill(1);

const prfKeyRaw = await WebAuthnService.authenticateAndGetPrfKey([credId], prfSalt);

if (prfKeyRaw) {
const prfKey = await window.crypto.subtle.importKey(
'raw',
prfKeyRaw,
{ name: KEY_ALGO },
false,
['wrapKey', 'unwrapKey']
);

// Unwrap Master Key
const masterKey = await window.crypto.subtle.unwrapKey(
'raw',
wrappedBlob.data,
prfKey,
{ name: KEY_ALGO, iv: wrappedBlob.iv },
{ name: KEY_ALGO, length: 256 },
true,
['encrypt', 'decrypt']
);

this.masterKey = masterKey;
this.isVaultUnlocked = true;
return true;
}
return false;
} catch (e) {
console.warn("[Vault] WebAuthn unlock failed", e);
return false;
}
}

/**
* Unlock the vault using Passkey (preferred) or PIN
*/
Expand All @@ -223,24 +269,7 @@ export class VaultService {
// Try Passkey First
if (usePasskey && await this.hasPasskey()) {
try {
const credId = await this.readFromIDB(PASSKEY_ID_KEY);
const wrappedBlob = await this.readFromIDB(PASSKEY_WRAPPED_KEY_ID);

// Need the salt used during registration!
// Issue: My WebAuthnService implemented random salt and didn't save it/export it.
// I will assume for this step that I fixed WebAuthnService to use a fixed salt or stored salt.
// Let's rely on PIN fallback if this complex flow isn't perfect yet.

/*
const prfKeyRaw = await WebAuthnService.authenticateAndGetPrfKey([credId], salt);
if (prfKeyRaw) {
const prfKey = ... importKey ...
this.masterKey = ... unwrapKey (wrappedBlob, prfKey) ...
this.isVaultUnlocked = true;
return true;
}
*/
console.log("[Vault] Passkey logic placeholder - falling back to PIN for stability in this iteration");
if (await this.unlockWithWebAuthn()) return true;
} catch (e) {
console.warn("[Vault] Passkey unlock failed, trying PIN", e);
}
Expand Down
Loading
Loading