feat: etapa 5 do assistente — revisão de ênfases com timeline
Transforma a etapa "colar decisões" numa tela de lapidação: a sugestão da IA chega carregada e o editor afina frase a frase o que é ênfase e o que fica fora. Essa marcação é o norte da etapa 6 — só as frases com ênfase recebem zoom e legenda dinâmica; as demais ficam com legenda comum. O campo de colar o JSON sobe para a etapa 4, então a numeração das etapas não muda e a etapa 6 segue intacta. Backend (fcpxml/phrase_review.py): - build_phrase_review funde o _voice_timeline.json com as actions da IA - trim por frase que anda em fronteira de palavra; corte parcial da IA chega como trim em vez de ser arredondado fora - phrase_review_to_actions volta a cuts/zooms + emphasis_spans - merge_saved_decisions reaplica só as decisões salvas sobre uma revisão remontada da análise atual, para reprocessar a voz não ficar mascarado - resolve_source acha a mídia: o voice timeline guarda só o nome do arquivo App (SwiftUI): - layout de sala de edição: preview em cima, inspector à direita, timeline atravessando embaixo com seis trilhas rotuladas - preview enquadra no formato de entrega lido do .fcpxml (fonte horizontal, projeto vertical), com alternância para a mídia original - reprodução pula os trechos removidos e para no fim do trecho - zoom manual por trecho marcado, sem guardar escala: a forma vem das configurações de Análise de Voz no render - emoção da fala exposta por frase Correções encontradas no caminho: - VideoPlayer (AVKit) aborta em runtime no app compilado por swiftc; trocado por AVPlayerLayer (ver Engine/docs/05_EXPERIENCIAS.md #22) - teste que ainda afirmava o default zoom scale=1.3 removido do parser (#21) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e7748c2c58
commit
1bebee4359
@@ -11,6 +11,7 @@ import pytest
|
||||
|
||||
from fcpxml.voice_timeline import (
|
||||
VOICE_TIMELINE_VERSION,
|
||||
annotate_emotions,
|
||||
build_voice_timeline,
|
||||
enrich_words,
|
||||
load_voice_timeline,
|
||||
@@ -60,6 +61,14 @@ class TestEnrichWords:
|
||||
assert all(w["energy_norm"] == 0.0 for w in enriched)
|
||||
assert all(w["pitch_delta"] == 0.0 for w in enriched)
|
||||
|
||||
def test_emotion_labels_are_added_when_enabled(self):
|
||||
enriched = enrich_words(_TRANSCRIPT["words"], _PITCH, _ENERGY)
|
||||
emotional = annotate_emotions(enriched, enabled=True, sensitivity=0.1)
|
||||
loudest = max(emotional, key=lambda w: w["arousal"])
|
||||
assert loudest["word"] == "seguranca"
|
||||
assert loudest["emotion"] in {"excited", "tense", "neutral"}
|
||||
assert 0.0 <= loudest["emotion_confidence"] <= 1.0
|
||||
|
||||
|
||||
class TestBuildVoiceTimeline:
|
||||
@pytest.fixture
|
||||
@@ -74,6 +83,7 @@ class TestBuildVoiceTimeline:
|
||||
for key in ("version", "source", "language", "scales", "summary", "speakers", "segments"):
|
||||
assert key in timeline
|
||||
assert timeline["version"] == VOICE_TIMELINE_VERSION
|
||||
assert "emotion" in timeline["layers"]
|
||||
|
||||
def test_scales_document_every_word_metric(self, timeline):
|
||||
word = timeline["segments"][0]["words"][0]
|
||||
@@ -106,6 +116,8 @@ class TestBuildVoiceTimeline:
|
||||
quiet_segment = timeline["segments"][0]
|
||||
assert loud_segment["avg_energy"] > quiet_segment["avg_energy"]
|
||||
assert loud_segment["peak_emphasis"] >= max(w["emphasis"] for w in loud_segment["words"])
|
||||
assert "emotion" in loud_segment
|
||||
assert "arousal" in loud_segment
|
||||
|
||||
def test_peak_moments_are_sorted_by_emphasis(self, timeline):
|
||||
peaks = timeline["summary"]["peak_moments"]
|
||||
|
||||
Reference in New Issue
Block a user