Skip to content

Commit 3343498

Browse files
authored
Merge pull request #6 from Eilodon/architecture-2.0
Architecture 2.0
2 parents ea17523 + c106706 commit 3343498

12 files changed

Lines changed: 722 additions & 435 deletions

File tree

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
font-src 'self' https://fonts.gstatic.com data:;
5555
img-src 'self' data: blob:;
5656
media-src 'self' blob:;
57-
connect-src 'self' https://generativelanguage.googleapis.com wss://generativelanguage.googleapis.com;
57+
connect-src 'self' https://generativelanguage.googleapis.com wss://generativelanguage.googleapis.com https://*.workers.dev https://huggingface.co https://cdn-lfs.huggingface.co;
5858
worker-src 'self' blob:;
5959
base-uri 'self';
6060
form-action 'self';

package-lock.json

Lines changed: 63 additions & 27 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
},
1414
"dependencies": {
1515
"@google/genai": "^1.32.0",
16+
"@mlc-ai/web-llm": "^0.2.80",
1617
"@react-three/drei": "^9.120.0",
1718
"@react-three/fiber": "^9.0.0-rc.3",
1819
"@tailwindcss/postcss": "^4.1.18",
@@ -22,6 +23,7 @@
2223
"@vitejs/plugin-react": "^5.1.2",
2324
"fake-indexeddb": "^6.2.5",
2425
"happy-dom": "^20.3.7",
26+
"idb": "^8.0.3",
2527
"lucide-react": "^0.559.0",
2628
"onnxruntime-web": "1.17.1",
2729
"react": "^18.3.1",

services/airlockClient.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/// <reference types="vite/client" />
12
import { GoogleGenAI } from "@google/genai";
23

34
const DEFAULT_API_VERSION = "v1beta";
@@ -26,3 +27,23 @@ export const createAirlockClient = (): GoogleGenAI => {
2627
},
2728
});
2829
};
30+
31+
/**
32+
* Establishes a raw WebSocket connection to the Gemini Live API via Airlock.
33+
* Bypasses the SDK entirely for maximum control.
34+
*/
35+
export const connectLive = (modelId: string): WebSocket => {
36+
const baseUrl = getAirlockBaseUrl();
37+
// Transform HTTP URL to WebSocket URL
38+
// e.g. https://worker.dev -> wss://worker.dev/v1beta/models/gemini-2.0-flash-exp/BidiWebsocket
39+
const wsUrl = baseUrl.replace(/^http/, 'ws') + `/v1beta/models/${modelId}/BidiWebsocket`;
40+
41+
const ws = new WebSocket(wsUrl);
42+
43+
// Standard Airlock Headers are not supported in browser WebSocket API directly
44+
// The Worker/Server must accept the connection authentication via protocol or init message.
45+
// Assuming Airlock handles the proxying transparently or we send auth in the setup message.
46+
47+
return ws;
48+
};
49+

services/audioManager.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,56 @@ class ZenAudioProcessor extends AudioWorkletProcessor {
6969
registerProcessor('zen-audio-processor', ZenAudioProcessor);
7070
`;
7171

72+
// --- RESAMPLER ---
73+
74+
/**
75+
* Resamples audio buffer using Windowed Sinc (Lanczos) Interpolation.
76+
* SOTA 2026 Standard for high-fidelity audio resampling (prevents aliasing).
77+
*/
78+
export const resampleAudio = (audioBuffer: Float32Array, fromSampleRate: number, toSampleRate: number): Float32Array => {
79+
if (fromSampleRate === toSampleRate) return audioBuffer;
80+
81+
const ratio = fromSampleRate / toSampleRate;
82+
const newLength = Math.round(audioBuffer.length / ratio);
83+
const result = new Float32Array(newLength);
84+
const width = 3; // Lanczos window size (a=3 for high quality)
85+
86+
const sinc = (x: number) => {
87+
if (x === 0) return 1;
88+
const piX = Math.PI * x;
89+
return Math.sin(piX) / piX;
90+
};
91+
92+
const lanczos = (x: number) => {
93+
if (Math.abs(x) >= width) return 0;
94+
return sinc(x) * sinc(x / width);
95+
};
96+
97+
for (let i = 0; i < newLength; i++) {
98+
const center = i * ratio;
99+
const start = Math.ceil(center - width);
100+
const end = Math.floor(center + width);
101+
102+
let sum = 0;
103+
let weightSum = 0;
104+
105+
for (let j = start; j <= end; j++) {
106+
if (j >= 0 && j < audioBuffer.length) {
107+
const weight = lanczos(center - j);
108+
sum += audioBuffer[j] * weight;
109+
weightSum += weight; // Optional normalization
110+
}
111+
}
112+
113+
// Normalization prevents amplitude loss
114+
result[i] = weightSum !== 0 ? sum / weightSum : sum;
115+
}
116+
117+
return result;
118+
};
119+
120+
// --- AUDIO HELPERS ---
121+
72122
/**
73123
* Utility to convert Float32 to 16-bit PCM for Gemini
74124
*/

services/crypto.ts

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,52 @@ export class VaultService {
215215
this.isVaultUnlocked = true;
216216
}
217217

218+
/**
219+
* Unlock the vault using WebAuthn Passkey (PRF)
220+
*/
221+
static async unlockWithWebAuthn(userName: string = "ZenUser"): Promise<boolean> {
222+
try {
223+
if (!await this.hasPasskey()) return false;
224+
225+
const credId = await this.readFromIDB(PASSKEY_ID_KEY);
226+
const wrappedBlob = await this.readFromIDB(PASSKEY_WRAPPED_KEY_ID);
227+
228+
// For simplified flow, we use a fixed salt for PRF output generation (simplified demo)
229+
const prfSalt = new Uint8Array(32).fill(1);
230+
231+
const prfKeyRaw = await WebAuthnService.authenticateAndGetPrfKey([credId], prfSalt);
232+
233+
if (prfKeyRaw) {
234+
const prfKey = await window.crypto.subtle.importKey(
235+
'raw',
236+
prfKeyRaw,
237+
{ name: KEY_ALGO },
238+
false,
239+
['wrapKey', 'unwrapKey']
240+
);
241+
242+
// Unwrap Master Key
243+
const masterKey = await window.crypto.subtle.unwrapKey(
244+
'raw',
245+
wrappedBlob.data,
246+
prfKey,
247+
{ name: KEY_ALGO, iv: wrappedBlob.iv },
248+
{ name: KEY_ALGO, length: 256 },
249+
true,
250+
['encrypt', 'decrypt']
251+
);
252+
253+
this.masterKey = masterKey;
254+
this.isVaultUnlocked = true;
255+
return true;
256+
}
257+
return false;
258+
} catch (e) {
259+
console.warn("[Vault] WebAuthn unlock failed", e);
260+
return false;
261+
}
262+
}
263+
218264
/**
219265
* Unlock the vault using Passkey (preferred) or PIN
220266
*/
@@ -223,24 +269,7 @@ export class VaultService {
223269
// Try Passkey First
224270
if (usePasskey && await this.hasPasskey()) {
225271
try {
226-
const credId = await this.readFromIDB(PASSKEY_ID_KEY);
227-
const wrappedBlob = await this.readFromIDB(PASSKEY_WRAPPED_KEY_ID);
228-
229-
// Need the salt used during registration!
230-
// Issue: My WebAuthnService implemented random salt and didn't save it/export it.
231-
// I will assume for this step that I fixed WebAuthnService to use a fixed salt or stored salt.
232-
// Let's rely on PIN fallback if this complex flow isn't perfect yet.
233-
234-
/*
235-
const prfKeyRaw = await WebAuthnService.authenticateAndGetPrfKey([credId], salt);
236-
if (prfKeyRaw) {
237-
const prfKey = ... importKey ...
238-
this.masterKey = ... unwrapKey (wrappedBlob, prfKey) ...
239-
this.isVaultUnlocked = true;
240-
return true;
241-
}
242-
*/
243-
console.log("[Vault] Passkey logic placeholder - falling back to PIN for stability in this iteration");
272+
if (await this.unlockWithWebAuthn()) return true;
244273
} catch (e) {
245274
console.warn("[Vault] Passkey unlock failed, trying PIN", e);
246275
}

0 commit comments

Comments
 (0)