// Karaoke-Timing: aus dem Satz + ElevenLabs-`alignment` pro Wort-/Objekt-Chip // ein Zeitfenster (Medienzeit in Sekunden) berechnen, damit beim Vorlesen das // gerade gesprochene Chip markiert werden kann. // // alignment-Format (ElevenLabs /with-timestamps): // { characters: string[], // character_start_times_seconds: number[], // character_end_times_seconds: number[] } // `characters.join('')` entspricht dem vertonten Klartext (Platzhalter durch Labels ersetzt). const CHIP_RE = /\{\{([^.]+)\.(w|o):([0-9a-f-]{36})\}\}/g // Liefert [{ id, label, start, end }] – ein Eintrag pro Chip-Vorkommen im Satz. export function buildChipTimings(sentence, alignment) { if (!sentence || !alignment) return [] const chars = alignment.characters const starts = alignment.character_start_times_seconds const ends = alignment.character_end_times_seconds if (!Array.isArray(chars) || !Array.isArray(starts) || !Array.isArray(ends) || !chars.length) return [] const plain = chars.join('') // Satz in derselben Reihenfolge nach Chip-Tokens absuchen; Labels sequenziell // im Klartext lokalisieren (toleriert kleine Whitespace-Unterschiede). const timings = [] let cursor = 0 for (const m of sentence.matchAll(CHIP_RE)) { const label = m[1] const id = m[3] const startIdx = plain.indexOf(label, cursor) if (startIdx === -1) continue const endIdx = startIdx + label.length - 1 cursor = endIdx + 1 const start = starts[startIdx] const end = ends[Math.min(endIdx, ends.length - 1)] if (typeof start !== 'number' || typeof end !== 'number') continue timings.push({ id, label, start, end }) } return timings } // Id des Chips, dessen Zeitfenster t (Sekunden) enthält – sonst null. export function activeChipIdAt(timings, t) { if (!timings?.length || typeof t !== 'number') return null for (const c of timings) { if (t >= c.start && t < c.end) return c.id } return null }