chore: adiciona .gitignore e commit.command

This commit is contained in:
João Henrique
2026-08-18 08:25:29 -04:00
parent 68958fde00
commit 8fca456ceb
215 changed files with 65752 additions and 0 deletions
+201
View File
@@ -0,0 +1,201 @@
import SwiftUI
import UniformTypeIdentifiers
/// Guia "Legendas" — configura e gera legendas dinâmicas como clipes de título
/// editáveis no Final Cut Pro (template "Essencial - Título"). Cada palavra
/// vira um clipe de título posicionado: as palavras da frase vão surgindo
/// conforme são faladas, se acumulam num bloco centralizado, e somem todas
/// juntas no fim da frase. Sem Compound Clip.
struct CaptionsView: View {
@State private var projectPath: String?
@State private var clipName = ""
// Posicionamento do bloco
@State private var bandHeight: Double = 0.22
@State private var blockCenterY: Double = -167
// Estilo
@State private var font = "Helvetica Neue"
@State private var fontSize: Double = 90
@State private var activeColor = Color.white
@State private var inactiveColor = Color(white: 0.7)
@State private var isGenerating = false
@State private var resultPath: String?
@State private var errorMessage: String?
private let fontChoices = [
"Helvetica Neue", "Helvetica", "Arial", "Avenir Next",
"Futura", "SF Pro Display", "Georgia", "Impact",
]
var body: some View {
Form {
Section("Projeto do Final Cut Pro") {
HStack {
Text(projectName)
.foregroundStyle(projectPath == nil ? .secondary : .primary)
.lineLimit(1)
Spacer()
Button("Escolher…") { pickProjectFile() }
}
if let projectPath {
HStack {
Image(systemName: "doc.text").foregroundStyle(.secondary)
Text(projectPath).font(.caption).foregroundStyle(.secondary).lineLimit(1)
}
}
TextField("Nome do clipe (opcional — vazio = todos os clipes)", text: $clipName)
.textFieldStyle(.roundedBorder)
}
Section {
VStack(alignment: .leading, spacing: 6) {
HStack {
Text("Altura do bloco").font(.callout.weight(.medium))
Spacer()
Text("\(Int(bandHeight * 100))%").font(.caption).foregroundStyle(.secondary).monospacedDigit()
}
Slider(value: $bandHeight, in: 0.10...0.50, step: 0.01)
Text("Quanto da altura do quadro a frase pode ocupar antes de quebrar em outro bloco. Maior = mais palavras juntas na tela.")
.font(.caption2).foregroundStyle(.secondary)
}
.padding(.vertical, 4)
VStack(alignment: .leading, spacing: 6) {
HStack {
Text("Altura na tela").font(.callout.weight(.medium))
Spacer()
Text("\(Int(blockCenterY))").font(.caption).foregroundStyle(.secondary).monospacedDigit()
}
Slider(value: $blockCenterY, in: -700...300, step: 1)
Text("Posição vertical do bloco. 0 é o centro do quadro; valores negativos descem.")
.font(.caption2).foregroundStyle(.secondary)
}
.padding(.vertical, 4)
} header: {
Text("Posicionamento do Bloco")
} footer: {
Text("Cada palavra vira um clipe de título solto na timeline (sem Compound Clip), posicionado para não sobrepor as outras palavras da frase.")
.font(.caption2).foregroundStyle(.secondary)
}
Section("Estilo do Texto") {
Picker("Fonte", selection: $font) {
ForEach(fontChoices, id: \.self) { Text($0).tag($0) }
}
VStack(alignment: .leading, spacing: 6) {
HStack {
Text("Tamanho da fonte").font(.callout.weight(.medium))
Spacer()
Text("\(Int(fontSize))pt").font(.caption).foregroundStyle(.secondary).monospacedDigit()
}
Slider(value: $fontSize, in: 20...200, step: 1)
}
.padding(.vertical, 4)
ColorPicker("Cor A", selection: $activeColor, supportsOpacity: true)
ColorPicker("Cor B", selection: $inactiveColor, supportsOpacity: true)
Text("Tamanho, cor e estilo variam por palavra seguindo um ritmo fixo, calibrado a partir de um projeto real do Final Cut. As cores acima entram nesse ritmo.")
.font(.caption2).foregroundStyle(.secondary)
}
Section {
Button {
generate()
} label: {
if isGenerating { ProgressView().controlSize(.small) }
Label("Gerar Legendas Dinâmicas", systemImage: "captions.bubble.fill")
}
.disabled(isGenerating || projectPath == nil)
.frame(maxWidth: .infinity)
.buttonStyle(.borderedProminent)
if let errorMessage {
Label(errorMessage, systemImage: "exclamationmark.triangle.fill")
.foregroundStyle(.red)
}
if let resultPath {
Text(resultPath).font(.caption).lineLimit(1)
HStack {
Button("Abrir no Final Cut Pro") { NSWorkspace.shared.open(URL(fileURLWithPath: resultPath)) }
Button("Mostrar no Finder") {
NSWorkspace.shared.activateFileViewerSelecting([URL(fileURLWithPath: resultPath)])
}
}
}
} footer: {
Text("Usa a transcrição local (Whisper) já feita na aba Projeto/Transcrição. Se ainda não houver transcrição salva, ela é gerada automaticamente.")
.font(.caption2).foregroundStyle(.secondary)
}
}
.formStyle(.grouped)
}
private var projectName: String {
guard let p = projectPath else { return "Nenhum projeto selecionado" }
return URL(fileURLWithPath: p).lastPathComponent
}
private func pickProjectFile() {
let panel = NSOpenPanel()
panel.canChooseFiles = true
panel.canChooseDirectories = false
panel.allowsMultipleSelection = false
panel.prompt = "Selecionar"
panel.message = "Selecione o arquivo de projeto (.fcpxml) exportado pelo Final Cut Pro."
if panel.runModal() == .OK, let url = panel.url {
let ext = url.pathExtension.lowercased()
if ext == "fcpxml" || ext == "xml" || ext == "fcpxmld" {
projectPath = url.path
} else {
errorMessage = "Selecione um arquivo .fcpxml ou .xml do Final Cut Pro."
}
}
}
private func generate() {
guard let projectPath else { return }
isGenerating = true
errorMessage = nil
resultPath = nil
var args: [String: Any] = [
"path": projectPath,
"band_height": bandHeight,
"block_center_y": blockCenterY,
"font": font,
"font_size": Int(fontSize),
"active_color": activeColor.fcpxmlColorString,
"inactive_color": inactiveColor.fcpxmlColorString,
]
let trimmedClip = clipName.trimmingCharacters(in: .whitespaces)
if !trimmedClip.isEmpty {
args["clip_name"] = trimmedClip
}
PythonBridge.call(command: "generate_dynamic_subtitles", arguments: args) { result, err in
DispatchQueue.main.async {
isGenerating = false
if result?["ok"] as? Bool == true {
resultPath = result?["path"] as? String
} else {
errorMessage = result?["error"] as? String ?? err ?? "Falha ao gerar legendas dinâmicas."
}
}
}
}
}
extension Color {
/// Converts to FCPXML's "R G B A" space-separated 0-1 string (sRGB).
var fcpxmlColorString: String {
let ns = NSColor(self).usingColorSpace(.sRGB) ?? NSColor(self)
return String(
format: "%.4g %.4g %.4g %.4g",
ns.redComponent, ns.greenComponent, ns.blueComponent, ns.alphaComponent
)
}
}