Profil: Begrüßung in Zielsprache, Kategorie-Punkte-Übersicht, ruhigerer Header (kein rotierender Avatar/Online-Dot), Notch-Fix und kompaktere Aktivitäts-Heatmap. Außerdem Capacitor-iOS-Projekt und diverse Auth/Feed/Audio-Verbesserungen aus dem Premium-Redesign. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
49 lines
1.9 KiB
JavaScript
49 lines
1.9 KiB
JavaScript
// 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
|
||
}
|