Was macht Konversations-Videountertitel?
Überblick
Untertitel sind eine eigene Ebene: Sie enthalten jedes gesprochene Wort und hängen nie davon ab, was im Bild gezeichnet ist. Bezeichnungen von Dingen im Bild (Teilenamen, Vor-/Nachteile, Pfeile) sind eine andere Ebene mit einer anderen visuellen Sprache (Hinweise neben dem Objekt); schneide niemals ein Wort aus den Untertiteln, weil „es bereits auf dem Bildschirm ist“.
Pipeline: words.json + forbid.json (Kästchen, die Untertitel nicht abdecken dürfen) + config.json + Profil
→ plan → manifest.json (kanonische Daten) → burn (eingebrannte Vorschau = visuelle Wahrheit) / export srt|ass|vtt → validate.
Format-Rollen (nicht verwechseln):
| Format | Enthält | Enthält NICHT |
|---|---|---|
manifest.json | Wörter, Zeitangaben, Layout, placement, akzeptierte Überlappungen, Hebungen mit Gründen, Render-Prüfungen: Daten-Quelle der Wahrheit | Pixel |
.srt | Text + Timing | Gewicht, Farbe, Umriss, aktives Wort, Position. Versprich niemals Stil in SRT |
.ass | Familie + Fett/\b700, Größe umgerechnet in libass-Metriken, Umriss, Hintergrundbox, aktives Wort pro Zeitscheibe, Position | Überblendungen; benötigt die Schriftart: subtitles=x.ass:fontsdir=fonts |
.vtt | ::cue CSS (Familie, Gewicht, Umriss via text-shadow, Hintergrund) + Klassen .active_<spk> / .em_<spk> / .dim / .u pro Zeitscheibe | exakte Position; die Seite muss die Schriftart mit @font-face laden |
eingebranntes preview.mp4 | alles: die einzige Quelle visueller Wahrheit | — |
Wann verwenden
- Dialog-Sketche, Interviews, Erklärvideos mit Sprecher: vertikal oder quadratisch, maximal eine oder zwei Zeilen.
- Du hast Wortzeitangaben (oder Audio: optionale Cloud-STT über Umgebungsvariablen) und weißt, wo Gesichter / Hände / Schlüsselrequisiten sind (Kästchen; für 3D-Szenen ein flaches Farb-Matte rendern und boxen).
- Nicht für Lyrik-Videos (hüpfende, skalierende Wörter werden bewusst nicht unterstützt).
Algorithmus
Segmentierung (chunk im Profil): Harte Trennung bei Pause > pause_s (0,35 s), Satzende, Sprecherwechsel; weiche Trennung bei Komma, sobald ein Chunk 2+ Wörter hat; begrenzt durch max_words, Pixelbreite (max_width_frac der Safe Zone) und max_dur_s (~1,2 s Sprache liest sich gut). Ein Chunk endet nie auf einem Funktionswort (не, в, а, and, the…): es geht zum nächsten Chunk. Ein einzelnes Wort schließt sich dem Nachbarn im selben Satz an, wenn es passt. Wortdauern werden auf das 2,5-fache des Medians begrenzt, damit ein Wort nicht in eine Pause hängt. Timing: Eine Einheit erscheint ≤ 50 ms vor ihrem ersten Wort, schneidet nie das vorherige Wort, bleibt hold_s nach ihrem letzten Wort, mindestens min_show_s.
Platzierung — placement: {mode: fixed_bottom_center, collision_lift: false} (Standard für Dialog).
- Eine Grundlinie für das gesamte Video:
baseline = safe.bottom - baseline_margin_px(oder explizit setzen). Jede Einheit ist unten an dieser Grundlinie ausgerichtet (y + h = baseline) und horizontal zentriert (x + w/2 = W/2). Nichts bewegt sich: keine Hebung, keine seitliche Verschiebung, kein x pro Sprecher. Die Zeilenbreite ist so begrenzt, dass die zentrierte Zeile innerhalb der Safe Zone bleibt. - Eine Zeile, die an der Grundlinie ein verbotenes Kästchen kreuzt, bleibt bestehen: Die Einheit erhält
overlap_accepted: [classes]und das QA-Blatt zeichnet einen gelben Rahmen „Überlappung akzeptiert (feste Grundlinie)“. Information, kein Fehler. Das Kreuzen der Plattform-UI ist weiterhin ein Fehler (Schrift verkleinern oder Phrase aufteilen). - Option
collision_lift: true(Modusbaseline_lift): x unter dem Sprecher (align: speaker) oder zentriert mit links/rechts Ausweichmöglichkeiten; wenn jedes x kollidiert, um ganzelift_step_pxSchritte heben, bis Platz ist. Die Einheit erhältlift = {px, reason: [classes an der Grundlinie getroffen], t0, t1}, das Manifest listet alle Hebungen incollision_lifts. Kein Grund → keine Hebung. Nur verwenden, wenn die Aufgabe verlangt, Gesichter um jeden Preis freizuhalten. - Niemals ein Wort fallen lassen.
Aussehen. Umriss (outline_px) + weicher Schatten + ein leichter Hintergrund, dessen Alpha pro Einheit nur so weit erhöht wird, wie nötig, um die dunkelste leuchtende Farbe ≥ 4,5:1 (WCAG) gegen das 90. Perzentil der Pixel dahinter zu halten. Sprecher unterscheiden sich durch die Farbe des aktiven Wortes und, im Hybrid-/Kartenmodus, durch einen 4 px Balken in der Sprecherfarbe; keine Sprechblasen, keine schweren Namensschilder. Ruhige Bewegung: nur Alpha-Überblendungen, keine Skalierung, keine vertikalen Rutschen.
QA (scripts/validate.py, auch durch demo ausgeführt):
- Schema: das Manifest gegen
templates/manifest.schema.json; - Zeilen ≤ 2;
- Platzierung: in
fixed_bottom_centerist die Streuung von y-Grundlinie und x-Zentrum über alle Einheiten 0 (0,5 px Toleranz) und es gibt keine Hebungen; inbaseline_liftjede Hebung auf dem Raster, hat einen Grund, und die Einheit kollidiert tatsächlich an der Grundlinie; - Null Überlappungen mit der Plattform-UI auf jedem Frame; verbotene Kästchen: 0 in
baseline_lift, gemeldet alsoverlap_accepted(Info) infixed_bottom_center; - jedes Wort genau einmal, in Reihenfolge, sichtbar während es gesprochen wird;
- Kontrast ≥ 4,5 und kein Bounce, beides gemessen pro Frame durch
burn; - Schriftarten aufgelöst: Datei vorhanden, Pillow-Familie und -Stil, OS/2-Gewicht,
fc-scan, und die libassfontselectwählt die deklarierte Datei und das Gewicht aus; - Pixel: auf dem eingebrannten Frame UND auf dem libass-Render des ASS ist die aktive Farbe vorhanden, der Glyphenboden ist innerhalb von 3 px des Layouts, die Strichbreite ist innerhalb von 1 px einer Referenz-Rasterung des deklarierten Gewichts, und der Umriss ist innerhalb von 1 px von
outline_px.
Profile (profiles/*.json)
| Profil | Was | Am besten geeignet für |
|---|---|---|
karaoke | 2–4 Wörter gleichzeitig, gesprochenes Wort in Sprecherfarbe, Rest abgedunkelt | stabilste; feste Spuren, Erklärvideos |
reveal | Wörter werden beim Sprechen eingeblendet; Layout vorab festgelegt; ≤ 2 Zeilen | ruhige Erzählung |
cards | Ein Gedanke pro Karte (≤ 8 Wörter, 2 Zeilen), dichtes Panel, Schlüsselwort farbig | langsame, zitierfähige Zeilen; liest voraus |
kinetic | Ein Token nach dem anderen, größere Schrift | nur Pointen; wenig Kontext |
hybrid (Standard) | ≤ 3 Wörter, Wörter werden eingeblendet, Unterstreichung unter dem gesprochenen Wort, Hervorhebungsfarbe, Sprecherbalken | Dialog |
Optionen aus den R&D-Systemen als Parameter: Tempo = chunk.max_dur_s: 1,2 + timing.min_show_s: 0,83 (an in hybrid); Betonung = emphasis: "energy" (Wortlautstärke z-Score pro Sprecher aus --audio), oder "keyword" mit einer keywords-Liste.
Befehle
python -m subkit demo examples/fixture # 5 Profile + hybrid_lift -> Vorschauen, srt/ass/vtt, validation_report.{md,json}
python -m subkit plan --words W --forbid F --config C [--profile hybrid] [--audio A] -o manifest.json
python -m subkit burn manifest.json --forbid F --video in.mp4 [--audio A] -o preview.mp4 --qa qa.jpg
python -m subkit export manifest.json --format ass -o captions.ass # srt | ass | vtt
python scripts/check_clean.py # vor einem Commit: keine Medien/Schlüssel/Absolute Pfade
ELEVENLABS_API_KEY=... python -m subkit stt audio.wav -o words.json # optional; oder --provider groq mit GROQ_API_KEY
Fallstricke
- libass ≠ Pillow-Schriftgröße. libass dimensioniert nach OS/2 winAscent+winDescent und setzt die Grundlinie winAscent unter die Zeilenoberkante; export konvertiert beides, sonst kommt ASS-Text ~35 % kleiner, dünner und tiefer heraus.
- Variable Schriftarten in ASS fallen auf die Standardinstanz zurück (normalerweise Regular). Liefere eine statische Instanz des verwendeten Gewichts (hier
Onest-Bold.ttf, 700) und übergibfontsdir. ffmpeg -ssvor-isetzt Zeitstempel zurück: Der Untertitel-Filter rendert dann t=0. Suche nach-ibeim Einbrennen von ASS.\k/\kf-Karaoke ist in Zentisekunden und färbt jedes gesprochene Wort dauerhaft; für „nur das aktuelle Wort“ werden Zeitscheiben ausgegeben (wie export es tut).- SRT hat keinen Stil: Wenn eine Lieferung Gewicht oder Hervorhebung benötigt, liefere ASS/VTT oder das eingebrannte Video aus.
- Eine schwebende Zeile (x pro Sprecher, Hysterese, „schönere“ Hebungen) wirkt bei Dialog wie Zittern. Behalte den Standard-Festmodus bei; in
baseline_liftprüft der Validator erneut, dass jede Hebung wirklich kollidiert. - Kontrast wird gegen die hellste Farbe und die hellsten 10 % der Pixel dahinter gemessen, nicht den Durchschnitt: Feuer, Schnee und Glanzlichter bestehen Durchschnitte nicht.
- Lasse nicht auf dem Bildschirm gezeichnete Wörter fallen. Verschiebe die gezeichnete Bezeichnung stattdessen in eine Anmerkungsebene.
- Safe Zones in
templates/safe_zones.jsonsind ungefähr; Plattform-UIs ändern sich, also überprüfe sie vor der Veröffentlichung erneut.
Checkliste
- words.json deckt die gesamte Sprache ab (Cloud STT oder ein Forced Aligner); Sprecher gekennzeichnet
- forbid.json: Gesichter, Hände, Feuer/Requisiten pro Einstellung (+
speakers-Kästchen füralign: speaker) - config: Größe, fps,
safe_zone, Sprecherfarben (verschiedene Farbtöne, beide ≥ 4,5:1 auf dem Hintergrund) -
plan→ Manifest; leseoverlap_accepted(fest) odercollision_lifts(Hebungsmodus, jede mit echtem Grund) -
burn→ betrachte das QA-Blatt (cyan = Grundlinie, gelb = Überlappung akzeptiert, orange = Hebung mit Grund) und echte Frames -
validatealle BESTANDEN; liefere gebranntes Video aus (+ ASS/VTT, falls ein Player sie rendert; SRT nur als Klartext)