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>
70 lines
2.1 KiB
Swift
70 lines
2.1 KiB
Swift
import SwiftUI
|
|
|
|
@main
|
|
struct GArtApp: App {
|
|
var body: some Scene {
|
|
WindowGroup {
|
|
ContentView()
|
|
.frame(minWidth: 720, minHeight: 560)
|
|
}
|
|
.windowStyle(.automatic)
|
|
}
|
|
}
|
|
|
|
enum ActiveTab: Hashable {
|
|
case wizard
|
|
case project
|
|
case captions
|
|
case voiceAnalysis
|
|
case models
|
|
case about
|
|
}
|
|
|
|
struct ContentView: View {
|
|
@State private var activeTab: ActiveTab? = .wizard
|
|
|
|
var body: some View {
|
|
NavigationSplitView {
|
|
List(selection: $activeTab) {
|
|
Label("Assistente", systemImage: "wand.and.stars")
|
|
.tag(ActiveTab.wizard)
|
|
Section("Avançado") {
|
|
Label("Projeto", systemImage: "film")
|
|
.tag(ActiveTab.project)
|
|
Label("Legendas", systemImage: "captions.bubble")
|
|
.tag(ActiveTab.captions)
|
|
Label("Análise de Voz", systemImage: "waveform")
|
|
.tag(ActiveTab.voiceAnalysis)
|
|
Label("Modelos", systemImage: "tray.and.arrow.down")
|
|
.tag(ActiveTab.models)
|
|
}
|
|
Label("Sobre", systemImage: "info.circle")
|
|
.tag(ActiveTab.about)
|
|
}
|
|
.listStyle(.sidebar)
|
|
.navigationSplitViewColumnWidth(min: 180, ideal: 200)
|
|
} detail: {
|
|
switch activeTab {
|
|
case .wizard, nil:
|
|
WizardView().id(UUID())
|
|
.navigationTitle("Assistente")
|
|
case .project:
|
|
ProjectView().id(UUID())
|
|
.navigationTitle("Projeto")
|
|
case .captions:
|
|
CaptionsView().id(UUID())
|
|
.navigationTitle("Legendas")
|
|
case .voiceAnalysis:
|
|
VoiceAnalysisView().id(UUID())
|
|
.navigationTitle("Análise de Voz")
|
|
case .models:
|
|
ModelDownloadView().id(UUID())
|
|
.navigationTitle("Modelos")
|
|
case .about:
|
|
AboutView()
|
|
.navigationTitle("Sobre")
|
|
}
|
|
}
|
|
}
|
|
}
|