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
+36
View File
@@ -260,6 +260,42 @@ class TestBackToActions:
)
assert result["actions"] == []
def test_consecutive_inactive_phrases_merge_into_one_cut(self):
"""Two deactivated phrases in a row must not leave the pause between
them (2.0-2.3 here) uncut — a phrase-by-phrase cut would strand it as
a tiny surviving sliver clip in the final timeline."""
review = build_phrase_review(
_timeline([_segment(0, 2), _segment(2.3, 4), _segment(4.5, 6)])
)
review["phrases"][0]["active"] = False
review["phrases"][1]["active"] = False
result = phrase_review_to_actions(review)
cuts = [(a["start"], a["end"]) for a in result["actions"] if a["kind"] == "cut"]
assert cuts == [(0.0, 4.0)]
def test_inactive_run_at_the_end_still_flushes(self):
"""A run of deactivated phrases with nothing active after it must
still produce its cut — regression for merging logic that only
flushed on hitting the next active phrase."""
review = build_phrase_review(_timeline([_segment(0, 2), _segment(2.3, 4)]))
review["phrases"][0]["active"] = False
review["phrases"][1]["active"] = False
result = phrase_review_to_actions(review)
cuts = [(a["start"], a["end"]) for a in result["actions"] if a["kind"] == "cut"]
assert cuts == [(0.0, 4.0)]
def test_isolated_inactive_phrases_stay_separate_cuts(self):
"""An active phrase between two inactive ones must not be swallowed —
only truly CONSECUTIVE inactive phrases merge."""
review = build_phrase_review(
_timeline([_segment(0, 2), _segment(2.3, 4), _segment(4.5, 6)])
)
review["phrases"][0]["active"] = False
review["phrases"][2]["active"] = False
result = phrase_review_to_actions(review)
cuts = [(a["start"], a["end"]) for a in result["actions"] if a["kind"] == "cut"]
assert cuts == [(0.0, 2.0), (4.5, 6.0)]
class TestManualZooms:
def test_manual_zoom_becomes_an_action_without_a_scale(self):