chore(phase-0): stabilise foundation for Studio build

- Extract WAV assembly (buildWav, mergeFloat32Arrays, decodeFloat32Chunk,
  SAMPLE_RATE) into web/lib/audio/wav.ts so it can be reused by the
  Studio playback engine and library waveform previews
- Add server/waveform.py with compute_peaks() / write_peaks() — reads
  any WAV, mixes to mono, returns min/max peak arrays matching the
  WaveformPeaks TypeScript type
- Add server/ids.py with prefixed URL-safe ID helpers (gen_id, proj_id,
  asset_id, etc.) using stdlib secrets — no new dependency
- Add docs/studio-build-plan.md — full execution spec covering stack
  decisions, data models, API contract, component hierarchy, phase
  breakdown and acceptance criteria
- Ignore data/ directory (generated audio, waveforms, SQLite DB)
This commit is contained in:
2026-05-02 17:24:45 +01:00
parent 0236807928
commit 47e0c7e512
6 changed files with 1098 additions and 48 deletions
+1 -48
View File
@@ -1,8 +1,8 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { buildWav, decodeFloat32Chunk, mergeFloat32Arrays, SAMPLE_RATE } from "@/lib/audio/wav";
const SAMPLE_RATE = 24_000;
const DEFAULT_PREBUFFER_SECS = 5.0;
const DEFAULT_REBUFFER_THRESHOLD_SECS = 1.0;
const DEFAULT_RESUME_THRESHOLD_SECS = 3.0;
@@ -30,53 +30,6 @@ interface UseStreamingGenerationOptions {
resumeThresholdSecs?: number;
}
function mergeFloat32Arrays(chunks: Float32Array<ArrayBuffer>[]): Float32Array<ArrayBuffer> {
const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
const out = new Float32Array(total);
let offset = 0;
for (const chunk of chunks) {
out.set(chunk, offset);
offset += chunk.length;
}
return out;
}
function buildWav(samples: Float32Array<ArrayBuffer>, sampleRate: number): Blob {
const dataSize = samples.length * 4;
const buffer = new ArrayBuffer(44 + dataSize);
const view = new DataView(buffer);
const writeString = (offset: number, value: string) => {
for (let i = 0; i < value.length; i += 1) {
view.setUint8(offset + i, value.charCodeAt(i));
}
};
writeString(0, "RIFF");
view.setUint32(4, 36 + dataSize, true);
writeString(8, "WAVE");
writeString(12, "fmt ");
view.setUint32(16, 16, true);
view.setUint16(20, 3, true);
view.setUint16(22, 1, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, sampleRate * 4, true);
view.setUint16(32, 4, true);
view.setUint16(34, 32, true);
writeString(36, "data");
view.setUint32(40, dataSize, true);
new Float32Array(buffer, 44).set(samples);
return new Blob([buffer], { type: "audio/wav" });
}
function decodeFloat32Chunk(data: string): Float32Array<ArrayBuffer> {
const raw = atob(data);
const bytes = new Uint8Array(raw.length);
for (let i = 0; i < raw.length; i += 1) {
bytes[i] = raw.charCodeAt(i);
}
return new Float32Array(bytes.buffer as ArrayBuffer);
}
export function useStreamingGeneration({
onLog,
onStart,