feat(legendas): sub-frases por vírgula e empacotamento em compound clip

split_into_subphrases divide a frase na vírgula — onde a fala respira —
mas funde de volta o pedaço curto ("né?", "Então..."), que lê como parte
da frase anterior e não como bloco próprio.

wrap_titles_in_compound empacota os títulos de uma sub-frase num compound
clip, replicando a estrutura que o próprio Final Cut produz: o primeiro
título vira âncora do spine em offset 0, os demais penduram nele por lane,
e um ref-clip toma o lugar deles na lane original. Os offsets dos filhos
são rebaseados para o espaço de tempo da âncora, senão cada palavra
escorregaria pela diferença entre os dois start.

Junto: _filter_children_for_segment passa a filtrar também o <video> do
Clipe de Ajuste. Sem isso, cada corte subsequente duplicava o zoom em
todos os pedaços resultantes com o offset original intacto, e as cópias
desenhavam empilhadas na mesma posição da timeline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
João Henrique
2026-08-26 16:35:21 -04:00
co-authored by Claude Opus 5
parent 635d1bb553
commit 688bdeddb6
3 changed files with 157 additions and 0 deletions
+15
View File
@@ -41,6 +41,15 @@ class CutMixin:
cut (silence removal, filler removal) duplicates it into every
resulting piece, so the same word shows up several times across the
edited timeline instead of once where it was placed.
A lane-nested ``<video>`` zoom (the "Clipe de Ajuste" adjustment
layer ``add_zoom`` creates, ``role`` starting with ``"adjustments."``)
is the exact same phantom-duplicate case, keyed on ``offset``+
``duration`` like a keyword. Left unfiltered, every further cut
duplicates the zoom into every resulting piece with its original
offset untouched — each copy then draws at the same absolute
position, so two "Clipe de Ajuste" bars appear stacked on top of
each other in the timeline instead of the one real zoom window.
"""
seg_end = seg_start + seg_duration
to_remove = []
@@ -54,6 +63,12 @@ class CutMixin:
title_offset = TimeValue.from_timecode(child.get('offset', '0s'))
if title_offset < seg_start or title_offset >= seg_end:
to_remove.append(child)
elif tag == 'video' and (child.get('role') or '').startswith('adjustments.'):
v_offset = TimeValue.from_timecode(child.get('offset', '0s'))
v_dur = TimeValue.from_timecode(child.get('duration', '0s'))
v_end = v_offset + v_dur
if v_end <= seg_start or v_offset >= seg_end:
to_remove.append(child)
elif tag == 'keyword':
kw_start = TimeValue.from_timecode(child.get('start', '0s'))
kw_dur = TimeValue.from_timecode(child.get('duration', '0s'))