feat(ui): redesign voice cockpit with clinical companion drawer, Svelte 5 migration, and Gemini 3.8 voice

- Re-architect VoiceSession into full-bleed studio cockpit with overlay slide-over companion drawer
- Eliminate width squishing defect on LiveInsightPanel and add native dark/light theme support
- Integrate authentic Google Gemini 3.8 Flash TTS with Fenrir persona and fallback audio pipeline
- Implement comprehensive Playwright E2E visual regression suite across Desktop, iPad, and iPhone (21 tests, 45 snapshots)
- Migrate core application to Svelte 5 runes (, , ) with reactive stores
- Maintain .NET 8 Azure Functions serverless endpoints and clean build hygiene
This commit is contained in:
jpremill committed 2026-09-29 23:35:05 -04:00
1 parent c82b112624
commit 1feea40684
148 files changed
+11230 -1706

No files matched your search

+99
View File
@@ -0,0 +1,99 @@
import { describe, test, expect } from 'vitest';
import {
floatTo16BitPCM,
encodeBase64,
decodeBase64,
decodeAudioData
} from '../services/audioUtils';
describe('Audio Utilities & PCM DSP', () => {
test('floatTo16BitPCM converts Float32Array to 16-bit PCM buffer with proper clamping', () => {
// Input with values within range, positive clamp (>1), negative clamp (<-1), and zero
const input = new Float32Array([0.0, 0.5, 1.0, 1.5, -0.5, -1.0, -1.5]);
const buffer = floatTo16BitPCM(input);
expect(buffer.byteLength).toBe(input.length * 2);
const view = new DataView(buffer);
// 0.0 -> 0
expect(view.getInt16(0, true)).toBe(0);
// 0.5 -> 0.5 * 32767 = 16383.5 -> 16383
expect(Math.abs(view.getInt16(2, true) - 16383)).toBeLessThanOrEqual(1);
// 1.0 -> 32767
expect(view.getInt16(4, true)).toBe(0x7fff);
// 1.5 clamped to 1.0 -> 32767
expect(view.getInt16(6, true)).toBe(0x7fff);
// -0.5 -> -0.5 * 32768 = -16384
expect(Math.abs(view.getInt16(8, true) - -16384)).toBeLessThanOrEqual(1);
// -1.0 -> -32768
expect(view.getInt16(10, true)).toBe(-0x8000);
// -1.5 clamped to -1.0 -> -32768
expect(view.getInt16(12, true)).toBe(-0x8000);
});
test('encodeBase64 and decodeBase64 perform lossless roundtrip for arbitrary byte arrays', () => {
const rawBytes = new Uint8Array([0, 1, 255, 128, 42, 64, 18, 99, 127]);
const encoded = encodeBase64(rawBytes);
expect(typeof encoded).toBe('string');
expect(encoded.length).toBeGreaterThan(0);
const decoded = decodeBase64(encoded);
expect(decoded.length).toBe(rawBytes.length);
expect(Array.from(decoded)).toEqual(Array.from(rawBytes));
});
test('decodeAudioData converts raw PCM Int16 bytes into AudioBuffer channels', async () => {
// Construct mock AudioContext with createBuffer
const sampleRate = 24000;
const numChannels = 1;
const mockChannelData = new Float32Array(4);
const mockAudioBuffer = {
length: 4,
numberOfChannels: 1,
sampleRate,
getChannelData: () => mockChannelData
};
const mockCtx = {
createBuffer: (channels: number, length: number, rate: number) => {
expect(channels).toBe(1);
expect(length).toBe(4);
expect(rate).toBe(sampleRate);
return mockAudioBuffer;
}
} as unknown as AudioContext;
// 4 16-bit samples: 0, 32767, -32768, 16384
const int16Samples = new Int16Array([0, 32767, -32768, 16384]);
const byteData = new Uint8Array(int16Samples.buffer);
const result = await decodeAudioData(byteData, mockCtx, sampleRate, numChannels);
expect(result).toBe(mockAudioBuffer);
// Verify channel data normalized between -1.0 and 1.0
expect(mockChannelData[0]).toBe(0);
expect(mockChannelData[1]).toBeCloseTo(1.0, 2);
expect(mockChannelData[2]).toBe(-1.0);
expect(mockChannelData[3]).toBeCloseTo(0.5, 2);
});
test('decodeAudioData handles uneven byte lengths gracefully', async () => {
const mockChannelData = new Float32Array(2);
const mockAudioBuffer = {
length: 2,
numberOfChannels: 1,
sampleRate: 24000,
getChannelData: () => mockChannelData
};
const mockCtx = {
createBuffer: () => mockAudioBuffer
} as unknown as AudioContext;
// 5 bytes (uneven) -> trimmed to 4 bytes (2 samples)
const unevenBytes = new Uint8Array([0, 0, 255, 127, 42]);
const result = await decodeAudioData(unevenBytes, mockCtx);
expect(result).toBe(mockAudioBuffer);
});
});