fix(voz): frases desativadas em sequência deixavam fatias sobrando no corte

phrase_review_to_actions() cortava cada frase desativada isoladamente
(start..end da própria frase) — quando várias seguidas estavam desativadas,
a pausa ENTRE elas não pertencia a nenhuma frase e sobrevivia como um
clipe minúsculo (0,1-0,5s) na timeline final. Confirmado no projeto
Mastopexia real: 29 cuts individuais geravam mais de uma dezena de fatias
sub-segundo; agrupar frases desativadas consecutivas num único cut (do
início da primeira ao fim da última) reduziu para 3 cuts e 4 fatias
residuais (menores, provavelmente do padding do remove_media_silence —
registrado como dívida separada em 09_MANUTENCAO.md §2.5).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
João Henrique
2026-08-21 18:49:13 -04:00
co-authored by Claude Sonnet 5
parent fd791e116a
commit 8257155fd3
4 changed files with 113 additions and 10 deletions
+32 -9
View File
@@ -386,18 +386,40 @@ def phrase_review_to_actions(review: dict) -> dict:
actions: List[dict] = []
emphasis_spans: List[dict] = []
inactive_run: List[dict] = []
def flush_inactive_run() -> None:
"""One cut per RUN of consecutive deactivated phrases, not one per
phrase. A phrase-by-phrase cut leaves the pause BETWEEN two
deactivated phrases uncut — that gap was never anyone's content, so
nothing asked for it to survive, but it does anyway: a 0.1-0.5s
sliver clip in the final timeline for every such gap. Spanning the
whole run absorbs those gaps into the one cut."""
if not inactive_run:
return
if len(inactive_run) == 1:
reason = inactive_run[0]["reason"] or "desativada na revisão"
else:
reason = (
f"desativadas na revisão ({len(inactive_run)} frases): "
+ "; ".join(p["text"][:40] for p in inactive_run if p["text"])
)
actions.append(
VoiceAction(
kind="cut",
start=inactive_run[0]["start"],
end=inactive_run[-1]["end"],
reason=reason,
speaker=inactive_run[0]["speaker"],
).as_dict()
)
inactive_run.clear()
for phrase in phrases:
if not phrase["active"]:
actions.append(
VoiceAction(
kind="cut",
start=phrase["start"],
end=phrase["end"],
reason=phrase["reason"] or "desativada na revisão",
speaker=phrase["speaker"],
).as_dict()
)
inactive_run.append(phrase)
continue
flush_inactive_run()
# Head and tail the editor trimmed off — each becomes its own cut, so a
# false start disappears without taking the line with it.
@@ -436,6 +458,7 @@ def phrase_review_to_actions(review: dict) -> dict:
"text": phrase["text"],
}
)
flush_inactive_run()
# Hand-placed punch-ins carry no scale on purpose: an omitted scale lets the
# applier use the shape configured in "Análise de Voz" (zoom_scale, ease in