/** * Turning the voices' training clips into the snippet matrices a numbl * classifier takes. Each clip is reduced to the classifier rate once, * each included onset is refined to the foot of its energy rise, and a * fixed window is cut, exactly as the audio-spike-sorting benchmark cuts * its snippets. Voices whose clips have no usable audio contribute * nothing and are reported, since the classifier cannot land hits on * them; hits learned from takes (which store features, not audio) do not * reach this path. */ import type { Voice } from '../store.ts'; import { downsampleBox, refineOnset } from './dsp.ts'; import { CLF_FS, CLF_PRE, CLF_POST, type TrainingArrays } from './types.ts'; export interface VoiceTraining { training: TrainingArrays; /** Voice id per training type index (label k maps to voiceIds[k-1]). */ voiceIds: string[]; /** Names of voices that contributed no snippets. */ skipped: string[]; } export async function buildVoiceTraining( voices: Voice[], getClipAudio: (id: string) => Promise<{ samples: Float32Array; sr: number } | null>, ): Promise { const snipLen = CLF_PRE + CLF_POST; const perVoice: { voice: Voice; snips: Float32Array[] }[] = []; for (const voice of voices) { const snips: Float32Array[] = []; for (const clip of voice.clips) { const audio = await getClipAudio(clip.id); if (!audio) continue; const x = downsampleBox(audio.samples, audio.sr, CLF_FS); const excluded = new Set(clip.excluded); clip.onsets.forEach((t, i) => { if (excluded.has(i)) return; const s = refineOnset(x, CLF_FS, t); if (s - CLF_PRE < 0 || s + CLF_POST > x.length) return; snips.push(x.slice(s - CLF_PRE, s + CLF_POST)); }); } perVoice.push({ voice, snips }); } const contributing = perVoice.filter((p) => p.snips.length > 0); const training: TrainingArrays = { names: contributing.map((p) => p.voice.name), pre: CLF_PRE, snips: contributing.map((p) => { const data = new Float32Array(snipLen * p.snips.length); p.snips.forEach((s, j) => data.set(s, j * snipLen)); return { data, snipLen, count: p.snips.length }; }), }; return { training, voiceIds: contributing.map((p) => p.voice.id), skipped: perVoice.filter((p) => p.snips.length === 0).map((p) => p.voice.name), }; }