commit eff779a1df62be354232bad50ecf73d303ab0690 Author: Felipe Canin Novaes Date: Sat Feb 28 13:05:17 2026 -0300 adiciona diretrizes de geração de código e workflow ao repositório diff --git a/.github/AI_CODE_GUIDELINES.md b/.github/AI_CODE_GUIDELINES.md new file mode 100644 index 0000000..fa5d715 --- /dev/null +++ b/.github/AI_CODE_GUIDELINES.md @@ -0,0 +1,160 @@ +# AI Code Generation Guidelines + +## Princípios Fundamentais + +Você deve priorizar: + +- Simplicidade +- Reutilização +- Clareza +- Manutenibilidade +- Aderência ao projeto existente + +Evite complexidade desnecessária. + +--- + +## 1. Evitar Overengineering + +Antes de implementar qualquer solução, verifique: + +- Existe uma forma mais simples? +- Esta solução resolve apenas o problema atual? +- Estou adicionando abstrações desnecessárias? + +Regras: + +- NÃO crie abstrações "para o futuro" +- NÃO use padrões complexos sem necessidade clara +- Prefira soluções diretas e legíveis + +--- + +## 2. Não Reinventar a Roda + +Antes de implementar algo novo: + +Verifique, nesta ordem: + +1. Código existente no projeto +2. Bibliotecas já instaladas no projeto +3. Bibliotecas populares e consolidadas +4. Recursos nativos da linguagem + +Regras: + +- Prefira bibliotecas consolidadas +- NÃO reimplemente funcionalidades comuns +- Use soluções padrão da comunidade + +--- + +## 3. Consultar Documentação + +Antes de usar qualquer biblioteca, framework ou API: + +- Consulte a documentação oficial +- Use a forma recomendada +- Evite hacks e workarounds + +Se não souber algo: + +- NÃO invente +- Admita que não sabe +- Peça mais informações + +--- + +## 4. Não Duplicar Código + +Antes de escrever uma função: + +Verifique: + +- Já existe algo similar? +- Pode ser reutilizado? +- Pode ser refatorado? + +Regras: + +- NÃO copie e cole código +- Reutilize funções existentes +- Extraia código comum para funções reutilizáveis + +--- + +## 5. Separação de Responsabilidades + +Não coloque tudo em um único arquivo. + +Organize por responsabilidade: + +Exemplo: + +- controllers/ +- services/ +- repositories/ +- utils/ +- models/ + +Regras: + +- Uma função deve ter uma única responsabilidade +- Um arquivo deve ter um propósito claro +- Evite arquivos grandes demais + +--- + +## 6. Seguir o Padrão do Projeto + +Antes de criar código novo: + +Analise: + +- Estrutura de pastas +- Convenções de nomes +- Estilo do código +- Arquitetura existente + +Regras: + +- Siga o padrão existente +- NÃO introduza novos padrões sem necessidade + +--- + +## 7. Código Legível + +Priorize: + +- Nomes claros +- Código simples +- Facilidade de entendimento + +Evite: + +- Código "inteligente demais" +- Truques desnecessários + +--- + +## 8. Pensamento Crítico Obrigatório + +Antes de gerar código, você deve se perguntar: + +- Isso já existe? +- Isso é a forma mais simples? +- Isso é consistente com o projeto? +- Isso é necessário? + +Se a resposta for não clara, investigue antes. + +--- + +## 9. Em Caso de Dúvida + +Pare e pergunte. + +Não assuma. +Não invente. +Não improvise. diff --git a/.github/AI_WORKFLOW.md b/.github/AI_WORKFLOW.md new file mode 100644 index 0000000..93a33e6 --- /dev/null +++ b/.github/AI_WORKFLOW.md @@ -0,0 +1,16 @@ +# Workflow obrigatório + +Antes de escrever código: + +1. Leia o projeto +2. Entenda a arquitetura +3. Procure código existente +4. Procure bibliotecas existentes + +Depois disso: + +5. Proponha a solução +6. Explique por que é a melhor opção +7. Só então escreva o código + +Nunca escreva código imediatamente sem análise. \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..d672e35 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "simple-multimidia-track-audio-editor" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..748e520 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "simple-multimidia-track-audio-editor" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/DEVELOPMENT_PLAN.md b/DEVELOPMENT_PLAN.md new file mode 100644 index 0000000..b27bb00 --- /dev/null +++ b/DEVELOPMENT_PLAN.md @@ -0,0 +1,264 @@ +# Plano de Desenvolvimento — Simple Multimedia Track Audio Editor + +**Versão:** 1.0 +**Data:** 28/02/2026 +**Referência:** PRD v1.1 + +--- + +## Estratégia Geral + +O desenvolvimento segue a **Clean Architecture** de dentro para fora: as camadas mais internas (domínio) são implementadas e testadas primeiro, sem qualquer dependência de I/O, frameworks ou processos externos. A UI é sempre a última camada a ser construída. + +``` +Fase 1: Setup + └── Fase 2: Domain (testável, sem I/O) + └── Fase 3: Application (testável com mocks) + └── Fase 4: Adapters (integração com FFmpeg) + └── Fase 5: Infrastructure (processo real) + └── Fase 6: UI (montagem final) +``` + +Cada fase deve estar **compilando e com testes passando** antes de avançar para a próxima. + +--- + +## Fase 1 — Setup do Projeto + +**Objetivo:** Preparar o ambiente antes de escrever qualquer lógica de negócio. + +### Tarefas + +- [ ] Atualizar `Cargo.toml` com as dependências: + ```toml + eframe = "0.27" + anyhow = "1" + serde = { version = "1", features = ["derive"] } + tokio = { version = "1", features = ["process", "rt-multi-thread", "macros"] } + serde_json = "1" + ``` +- [ ] Criar a estrutura de pastas: + ``` + src/ + ├── domain/ + │ ├── entities/ + │ └── value_objects/ + ├── application/ + │ ├── use_cases/ + │ └── ports/ + ├── adapters/ + │ ├── ffmpeg/ + │ └── filesystem/ + ├── infrastructure/ + │ └── process/ + └── ui/ + ├── components/ + └── app.rs + ``` +- [ ] Declarar os módulos em `src/main.rs` +- [ ] Verificar que o projeto compila (`cargo check`) + +--- + +## Fase 2 — Domain (núcleo puro) + +**Objetivo:** Modelar o problema de negócio sem nenhum acoplamento a frameworks, I/O ou processos externos. + +> Regra: nenhum `use std::process`, nenhum `use eframe`, nenhuma chamada de rede ou filesystem nessa camada. + +### Value Objects (`src/domain/value_objects/`) + +| Tipo | Implementação | +|---|---| +| `FilePath` | Newtype sobre `PathBuf`; derivar `Clone`, `Debug`, `Serialize`, `Deserialize` | +| `TrackId` | Newtype opaco sobre `u32`; derivar `Clone`, `Copy`, `Debug`, `PartialEq`, `Eq`, `Hash` | +| `SyncOffset` | Newtype sobre `i64` (milissegundos, **nunca `f64`**); implementar método `from_seconds_str` e `as_ms` | +| `TrackLanguage` | Newtype sobre `String` (ex: `"por"`, `"eng"`); validar formato ISO 639-2 | + +### Entities (`src/domain/entities/`) + +| Tipo | Campos principais | +|---|---| +| `VideoFile` | `path: FilePath` | +| `AudioTrack` | `id: TrackId`, `path: FilePath`, `offset: SyncOffset`, `language: TrackLanguage` | +| `SubtitleTrack` | `id: TrackId`, `path: FilePath`, `offset: SyncOffset`, `language: TrackLanguage` | +| `MediaTrackInfo` | `id: TrackId`, `kind: TrackKind` (enum: Video/Audio/Subtitle), `codec: String`, `language: Option` | +| `MkvOutput` | `path: FilePath` | +| `Project` | Entidade raiz — ver abaixo | + +#### Estrutura de `Project` + +```rust +pub struct Project { + pub source: VideoFile, + pub tracks: Vec, // faixas externas adicionadas pelo usuário + pub existing_tracks: Vec, // faixas lidas do arquivo via ffprobe + pub output: MkvOutput, +} +``` + +### Testes obrigatórios + +- [ ] `SyncOffset::from_seconds_str("1.2")` → `SyncOffset(1200)` +- [ ] `SyncOffset::from_seconds_str("-0.5")` → `SyncOffset(-500)` +- [ ] `Project` não aceita `output.path` igual a `source.path` +- [ ] `TrackId` é opaco (não expõe indexação interna) + +--- + +## Fase 3 — Application (casos de uso e ports) + +**Objetivo:** Definir o que o sistema faz sem saber como. Ports são traits; use cases orquestram entidades. + +### Ports (`src/application/ports/`) + +```rust +// MediaInfoPort: inspeciona faixas de um arquivo de mídia +pub trait MediaInfoPort { + fn probe(&self, path: &FilePath) -> Result>; +} + +// MediaProcessorPort: executa o processamento final +pub trait MediaProcessorPort { + fn execute(&self, args: Vec) -> Result<()>; +} + +// FileSystemPort: abstrai acesso ao sistema de arquivos +pub trait FileSystemPort { + fn exists(&self, path: &FilePath) -> bool; +} +``` + +### Use Cases (`src/application/use_cases/`) + +Implementar nesta ordem (dependência crescente): + +| # | Use Case | Descrição | +|---|---|---| +| 1 | `LoadMediaInfo` | Usa `MediaInfoPort` para popular `Project::existing_tracks` | +| 2 | `AddAudioTrack` | Adiciona `AudioTrack` externo ao `Project::tracks` | +| 3 | `AddSubtitle` | Adiciona `SubtitleTrack` externo ao `Project::tracks` | +| 4 | `AdjustSync` | Altera `SyncOffset` de uma faixa existente pelo `TrackId` | +| 5 | `EditExistingTrackSync` | Ajusta offset de faixa já presente no arquivo original | +| 6 | `SetTrackLanguage` | Altera idioma de uma faixa pelo `TrackId` | +| 7 | `GenerateOutput` | Constrói o comando final e delega ao `MediaProcessorPort` | + +### Testes obrigatórios + +- [ ] Usar mocks dos ports (sem chamar nenhum processo externo) +- [ ] `GenerateOutput` sempre produz comando com `-c copy` (verificado via mock) +- [ ] `AdjustSync` com `TrackId` inexistente retorna erro +- [ ] `AddAudioTrack` em `Project` sem `source` retorna erro + +--- + +## Fase 4 — Adapters (implementações concretas) + +**Objetivo:** Conectar o domínio ao FFmpeg e ao filesystem real. + +### FFmpeg (`src/adapters/ffmpeg/`) + +#### `FfmpegCommandBuilder` + +Responsabilidade única: converter `Project` em `Vec` de argumentos para o FFmpeg. + +Regras invariantes: +- `-c copy` **sempre presente** (RNF-01 — nunca opcional, nunca configurável) +- `SyncOffset(i64 ms)` → `-itsoffset 1.200` (conversão feita **somente aqui**) +- `TrackId` → `-map 0:a:N` (mapeamento de índice feito **somente aqui**) + +Exemplo de saída esperada: +``` +ffmpeg -i input.mkv -itsoffset 1.200 -i audio_pt.aac -map 0:v -map 0:a -map 1:a -c copy -metadata:s:a:1 language=por output.mkv +``` + +- [ ] Implementar `FfmpegCommandBuilder::build(project: &Project) -> Vec` +- [ ] Testes: verificar presença de `-c copy`, ordem dos `-map`, formato de `-itsoffset` + +#### `FfprobeGateway` + +Implementa `MediaInfoPort`. Executa: +``` +ffprobe -v quiet -print_format json -show_streams +``` +e mapeia a saída JSON para `Vec`. + +- [ ] Implementar parsing de JSON via `serde_json` +- [ ] Mapear `codec_type` para `TrackKind` +- [ ] Mapear `tags.language` para `Option` + +#### `FfmpegGateway` + +Implementa `MediaProcessorPort`. Executa o processo real do FFmpeg e captura stderr. + +- [ ] Capturar stderr para exibição de erros (RF-07) +- [ ] Retornar erro com mensagem legível em caso de código de saída não-zero + +### Filesystem (`src/adapters/filesystem/`) + +- [ ] `FilePickerAdapter`: abstrai seleção de arquivo via diálogo nativo (usar crate `rfd`) + +--- + +## Fase 5 — Infrastructure + +**Objetivo:** Execução real e não-bloqueante de processos externos. + +### Módulo `src/infrastructure/process/` + +- [ ] Implementar execução via `tokio::process::Command` (assíncrono, RNF-05) +- [ ] Capturar stdout e stderr em stream (para exibir progresso em tempo real — RF-07) + +### Validação na inicialização + +- [ ] Verificar se `ffmpeg` está disponível no `PATH` (DT-06) +- [ ] Verificar se `ffprobe` está disponível no `PATH` +- [ ] Exibir mensagem clara ao usuário se algum dos dois estiver ausente + +--- + +## Fase 6 — UI (camada mais externa) + +**Objetivo:** Interface gráfica que conecta o usuário ao `Project` via use cases. + +> Todo estado da aplicação vive no `Project`. A UI apenas lê e dispara use cases. +> Termos técnicos do FFmpeg **nunca aparecem na interface** (ver PRD seção 13). + +### Componentes (`src/ui/components/`), em ordem de construção + +| # | Componente | Descrição | +|---|---|---| +| 1 | `VideoSelector` | Seletor de arquivo de vídeo; dispara `LoadMediaInfo` ao confirmar | +| 2 | `ExistingTrackList` | Lista faixas detectadas (áudio/legenda); permite editar offset de cada uma | +| 3 | `AddAudioTrackForm` | Formulário para adicionar faixa de áudio externa (arquivo, idioma, offset) | +| 4 | `AddSubtitleForm` | Formulário para adicionar legenda externa (arquivo, idioma, offset) | +| 5 | `SyncOffsetField` | Campo de offset em segundos (ex: `-1.2s`); converte para `SyncOffset(ms)` internamente | +| 6 | `LanguageField` | Seletor/input de idioma (ex: `por`, `eng`) | +| 7 | `OutputSelector` | Campo de caminho de saída + extensão `.mkv` forçada | +| 8 | `ExecutionPanel` | Botão "Gerar MKV", exibição de progresso e erros legíveis | + +### App (`src/ui/app.rs`) + +- [ ] Implementar `eframe::App` para `App` +- [ ] `App` contém `project: Project` como única fonte de verdade do estado +- [ ] Despachar eventos de UI para os use cases correspondentes +- [ ] Integrar execução assíncrona (tokio) com o loop de UI do eframe + +--- + +## Critérios de Conclusão (v1.0) + +Alinhados com o PRD seção 12: + +- [ ] É possível selecionar um arquivo de vídeo base +- [ ] Ao selecionar o vídeo, as faixas existentes são listadas automaticamente (via `ffprobe`) +- [ ] É possível adicionar uma ou mais faixas de áudio externas +- [ ] É possível adicionar uma ou mais faixas de legenda +- [ ] É possível definir offset de sincronização por faixa ao adicionar uma nova faixa +- [ ] É possível editar o offset de sincronização de uma faixa já existente no arquivo +- [ ] É possível atribuir idioma a cada faixa +- [ ] O arquivo MKV é gerado corretamente ao confirmar +- [ ] Erros do FFmpeg são exibidos de forma legível +- [ ] A interface não trava durante o processamento +- [ ] Nenhum reencoding ocorre (verificável via `ffprobe` no arquivo de saída) +- [ ] O flag `-c copy` está sempre presente no comando gerado (verificável em modo debug) diff --git a/PRD.md b/PRD.md new file mode 100644 index 0000000..86e3b8b --- /dev/null +++ b/PRD.md @@ -0,0 +1,380 @@ +# PRD — Simple Multimedia Track Audio Editor + +**Versão:** 1.1 +**Data:** 28/02/2026 +**Status:** Em desenvolvimento +**Revisão:** Incorporados feedbacks de arquitetura — modelo de sessão, ffprobe, tipos fortes e regra de no-reencode + +--- + +## 1. Visão do Produto + +Um editor simples de faixas multimídia que permite ao usuário combinar vídeo, áudio e legendas em um único arquivo de saída no formato MKV, sem realizar reencoding — apenas mux e ajuste de timestamps. + +--- + +## 2. Problema + +Usuários domésticos e de automação precisam de uma ferramenta simples para: + +- Adicionar faixas de áudio alternativas a um vídeo (ex: dublagem, comentários) +- Adicionar legendas +- Ajustar a sincronização entre faixas +- Gerar um arquivo final organizado e compatível + +As ferramentas existentes são complexas (ex: interface direta do FFmpeg via CLI) ou pesadas demais para esse caso de uso. + +--- + +## 3. Objetivos + +- Fornecer uma interface gráfica simples e intuitiva +- Abstrair a complexidade do FFmpeg para o usuário final +- Gerar arquivos MKV com múltiplas faixas de áudio e legendas +- Permitir ajuste de sincronização por faixa +- Não realizar reencoding (apenas mux) +- Ser multiplataforma (Linux, Windows, macOS) + +--- + +## 4. Fora do Escopo (v1.0) + +- Reencoding / transcodificação de vídeo ou áudio +- Preview de vídeo embutido +- Detecção automática de sincronização +- Remoção de faixas existentes do arquivo original +- Seleção de faixa padrão no container +- Edição de corte ou splice de vídeo + +--- + +## 5. Usuários-Alvo + +- Usuário doméstico que deseja adicionar dublagem ou áudio alternativo a vídeos +- Operadores de automação de mídia que precisam empacotar faixas em lote +- Usuários técnicos confortáveis com instalação de dependências (FFmpeg) + +--- + +## 6. Requisitos Funcionais + +### RF-01 — Adicionar faixa de áudio + +- O usuário pode selecionar um arquivo de áudio externo +- O áudio é mapeado ao container de saída via `ffmpeg -map` +- Suporta múltiplas faixas de áudio + +### RF-02 — Adicionar legenda + +- O usuário pode selecionar um arquivo de legenda (ex: `.srt`, `.ass`) +- A legenda é mapeada ao container via `ffmpeg -map` +- Suporta múltiplas faixas de legenda + +### RF-03 — Ajustar sincronização + +- O usuário pode definir um offset por faixa, expresso em **segundos com uma casa decimal** na interface (ex: `1.2s`, `-0.5s`) +- Internamente armazenado como `SyncOffset(i64)` em milissegundos — sem ponto flutuante +- Implementado via `ffmpeg -itsoffset`; a conversão ms→string é feita exclusivamente no `FfmpegCommandBuilder` +- Aceita valores positivos e negativos + +### RF-04 — Definir idioma da faixa + +- O usuário pode atribuir um código de idioma a cada faixa (ex: `por`, `eng`) +- Implementado via `ffmpeg -metadata:s` + +### RF-05 — Selecionar arquivo de vídeo base + +- O usuário seleciona o arquivo de vídeo de entrada +- O stream de vídeo é preservado sem modificação + +### RF-06 — Gerar arquivo de saída + +- O usuário define o caminho e nome do arquivo de saída +- O container de saída é sempre MKV +- A geração é executada via `ffmpeg` em processo externo + +### RF-07 — Feedback de execução + +- A interface exibe o progresso e resultado da operação +- Erros do FFmpeg (via stderr) são exibidos ao usuário de forma legível + +### RF-08 — Editar faixas existentes de áudio e legenda + +- O usuário pode selecionar e editar faixas de áudio ou legenda já presentes no arquivo de vídeo de entrada +- É possível ajustar o offset de sincronização de uma faixa existente, sem necessidade de adicionar uma nova faixa +- A faixa original é remapeada com o novo timestamp via `ffmpeg -itsoffset` combinado com `-map` +- Útil para corrigir atrasos em faixas de dublagem ou legenda já incorporadas ao arquivo + +--- + +## 7. Requisitos Não Funcionais + +### RNF-01 — Sem reencoding (regra central do domínio) + +Nenhum stream de vídeo ou áudio deve ser recodificado. Apenas mux e ajuste de timestamps são permitidos. + +**Esta é a propriedade mais importante do produto.** O flag `-c copy` (ou equivalente por stream) **nunca é opcional**. Deve ser emitido pelo `FfmpegCommandBuilder` de forma incondicional, independente de qualquer configuração do usuário. + +O domínio deve expor uma invariante que o garanta: + +``` +impl FfmpegCommandBuilder { + // -c copy é sempre adicionado. Não existe API para desativá-lo. + fn build(&self, project: &Project) -> Vec { ... } +} +``` + +Qualquer violação dessa regra invalida a proposta de valor do produto. + +### RNF-02 — Desempenho + +A operação de mux deve ser concluída em tempo proporcional ao tamanho do arquivo, sem bloqueio da interface. + +### RNF-03 — Compatibilidade + +O arquivo de saída deve ser compatível com players modernos que suportam MKV (ex: VLC, mpv, Jellyfin). + +### RNF-04 — Dependência externa + +FFmpeg deve estar instalado no sistema. A aplicação não o embute por padrão, porém pode ser distribuída junto. + +### RNF-05 — Interface responsiva + +A interface não deve travar durante a execução do FFmpeg. A execução deve ser assíncrona. + +--- + +## 8. Arquitetura Técnica + +``` +Interface (egui/eframe) + ↓ +Backend Rust (validação, construção de comandos) + ↓ +Processo externo (FFmpeg via std::process::Command) + ↓ +Arquivo de saída (.mkv) +``` + +### Stack + +| Camada | Tecnologia | Motivo | +|--------------|-------------------------------|---------------------------------------------| +| Interface | `egui` / `eframe` | Nativo, simples, multiplataforma | +| Backend | Rust (`std::process::Command`)| Estável, sem dependências externas | +| Erros | `anyhow` | Propagação de erros simplificada | +| Configuração | `serde` | Serialização de perfis e histórico | +| Async | `tokio` (opcional) | Execução não bloqueante da interface | +| Mídia | FFmpeg (externo) | Maduro, estável, amplamente testado | +| Container | MKV | Melhor suporte a múltiplas faixas | + +### Dependências Cargo + +```toml +eframe = "0.27" +anyhow = "1" +serde = { version = "1", features = ["derive"] } +tokio = { version = "1", features = ["process"] } # opcional +``` + +--- + +## 9. Arquitetura de Projeto — Clean Architecture + +A organização do código segue os princípios da **Clean Architecture**, garantindo separação de responsabilidades, testabilidade e baixo acoplamento entre camadas. + +### Regra de dependência + +As dependências sempre apontam de fora para dentro. Camadas internas não conhecem camadas externas. + +``` +┌──────────────────────────────────────────┐ +│ Infrastructure / UI │ ← egui, FFmpeg, filesystem +│ ┌────────────────────────────────────┐ │ +│ │ Adapters │ │ ← FfmpegGateway, FilePicker +│ │ ┌──────────────────────────────┐ │ │ +│ │ │ Application │ │ │ ← Use Cases +│ │ │ ┌────────────────────────┐ │ │ │ +│ │ │ │ Domain │ │ │ │ ← Entities, Value Objects +│ │ │ └────────────────────────┘ │ │ │ +│ │ └──────────────────────────────┘ │ │ +│ └────────────────────────────────────┘ │ +└──────────────────────────────────────────┘ +``` + +### Camadas + +#### Domain (núcleo) + +Contém as entidades e objetos de valor do negócio. Não depende de nada externo. + +| Tipo | Exemplos | +|----------------|-----------------------------------------------------------------------------------| +| Entities | `Project`, `VideoFile`, `AudioTrack`, `SubtitleTrack`, `MkvOutput`, `MediaTrackInfo` | +| Value Objects | `TrackId`, `SyncOffset`, `TrackLanguage`, `FilePath` | + +> **`Project`** é a entidade central do domínio. Representa a sessão de edição completa do usuário e deve ser a única fonte de verdade do estado em memória. +> +> ``` +> Project +> ├── source: VideoFile +> ├── tracks: Vec // faixas externas adicionadas +> ├── existing_tracks: Vec // faixas lidas do arquivo via ffprobe +> └── output: MkvOutput +> ``` +> +> **`TrackId`** abstrai os índices de stream do FFmpeg. Internamente é um identificador opaco (`u32` ou `String`). Nenhuma lógica de negócio deve depender de índices FFmpeg diretamente — isso é responsabilidade do adapter. +> +> **`SyncOffset`** armazena o offset de sincronização em **milissegundos como inteiro** (`i64`). Nunca como `f64`. O uso de ponto flutuante acumula erro de precisão; o adapter é responsável por converter para o formato exigido pelo FFmpeg (`-itsoffset`). + +#### Application (casos de uso) + +Contém as regras de negócio da aplicação. Orquestra as entidades do domínio. +Define traits (ports) que as camadas externas devem implementar. + +| Tipo | Exemplos | +|------------|-------------------------------------------------------------------------------------------------------------------| +| Use Cases | `LoadMediaInfo`, `AddAudioTrack`, `AddSubtitle`, `AdjustSync`, `EditExistingTrackSync`, `SetTrackLanguage`, `GenerateOutput` | +| Ports | `MediaProcessorPort`, `MediaInfoPort`, `FileSystemPort` | + +> **`MediaInfoPort`** é o port responsável por inspecionar arquivos de mídia existentes. Deve ser definido na camada Application e implementado na camada Adapters via `FfprobeGateway`. +> +> ``` +> trait MediaInfoPort { +> fn probe(path: &FilePath) -> Result>; +> } +> ``` +> +> `MediaTrackInfo` contém: `TrackId`, tipo de stream (vídeo/áudio/legenda), codec, idioma detectado. +> +> **`LoadMediaInfo`** é o caso de uso disparado quando o usuário seleciona um arquivo de vídeo. Lê as faixas existentes e popula o `Project::existing_tracks`. + +#### Adapters + +Implementam os ports definidos na camada de Application. Traduzem dados entre o domínio e o mundo externo. + +| Tipo | Exemplos | +|--------------------|---------------------------------------------------------------------------| +| Gateway | `FfmpegCommandBuilder`, `FfmpegGateway`, `FfprobeGateway` | +| Presenter | `ErrorPresenter` (formata stderr do FFmpeg) | +| File Adapter | `FilePickerAdapter` | + +> **`FfprobeGateway`** implementa `MediaInfoPort`. Executa `ffprobe -v quiet -print_format json -show_streams` e mapeia a saída para `Vec`. Não conhece o domínio além das structs que está populando. +> +> **`FfmpegCommandBuilder`** é responsável por converter `TrackId` para índices `-map 0:a:N` do FFmpeg e `SyncOffset` (ms inteiro) para o formato `-itsoffset 1.200` aceito pelo binário. **Toda conversão de tipos internos para argumentos FFmpeg fica aqui e somente aqui.** + +#### Infrastructure / UI + +Camada mais externa. Contém o framework de UI e a execução real de processos. + +| Tipo | Exemplos | +|--------------|-------------------------------------------------------| +| UI | `App` (eframe), componentes egui | +| Process | Execução de `std::process::Command` / `tokio::process`| +| Filesystem | Leitura e escrita de arquivos | + +--- + +### Estrutura de pastas + +``` +src/ +├── domain/ +│ ├── entities/ # Project, VideoFile, AudioTrack, SubtitleTrack, MkvOutput, MediaTrackInfo +│ └── value_objects/ # TrackId, SyncOffset (ms/i64), TrackLanguage, FilePath +├── application/ +│ ├── use_cases/ # LoadMediaInfo, AddAudioTrack, AddSubtitle, AdjustSync, GenerateOutput +│ └── ports/ # Traits: MediaProcessorPort, MediaInfoPort, FileSystemPort +├── adapters/ +│ ├── ffmpeg/ # FfmpegCommandBuilder, FfmpegGateway, FfprobeGateway +│ └── filesystem/ # FilePickerAdapter +├── infrastructure/ +│ └── process/ # Execução real do FFmpeg / ffprobe +├── ui/ +│ ├── app.rs # eframe App (ponto de entrada da interface) +│ └── components/ # Componentes egui reutilizáveis +└── main.rs +``` + +--- + +## 10. Débitos Técnicos + +| ID | Descrição | Impacto | Mitigação | +|-----|--------------------------------------|------------------|--------------------------------------| +| DT-01 | FFmpeg não embutido | Usuário precisa instalar | Distribuir junto com a aplicação | +| DT-02 | Dependência de processo externo | Menor controle interno | Encapsular via módulo de serviço | +| DT-03 | Parsing de erros do FFmpeg (stderr) | Necessário tratamento manual | Parsear saída e exibir mensagem amigável | +| DT-04 | Compatibilidade de codecs | Alguns codecs podem não ser aceitos | Usar MKV como container padrão | +| DT-05 | Performance de processo externo | Pequeno overhead | Aceitável — mux é rápido | +| DT-06 | ffprobe como dependência adicional | Parsing de JSON da saída do ffprobe | Validar presença de ffprobe na inicialização; exibir mensagem clara se ausente | + +--- + +## 11. Funcionalidades Futuras (Backlog) + +- Preview de vídeo embutido na interface +- Detecção automática de sincronização entre faixas +- Seleção de faixa padrão no container MKV +- Remoção de faixas existentes do arquivo original +- Suporte a perfis de configuração salvos +- Modo de processamento em lote (batch) + +--- + +## 12. Critérios de Aceitação (v1.0) + +- [ ] É possível selecionar um arquivo de vídeo base +- [ ] Ao selecionar o vídeo, as faixas existentes são listadas automaticamente (via `ffprobe`) +- [ ] É possível adicionar uma ou mais faixas de áudio externas +- [ ] É possível adicionar uma ou mais faixas de legenda +- [ ] É possível definir offset de sincronização por faixa ao adicionar uma nova faixa +- [ ] É possível editar o offset de sincronização de uma faixa de áudio ou legenda já existente no arquivo +- [ ] É possível atribuir idioma a cada faixa +- [ ] O arquivo MKV é gerado corretamente ao confirmar +- [ ] Erros do FFmpeg são exibidos de forma legível +- [ ] A interface não trava durante o processamento +- [ ] Nenhum reencoding ocorre (verificável via `ffprobe`) +- [ ] O flag `-c copy` está sempre presente no comando gerado (verificável em modo debug) + +--- + +## 13. Princípios de UX + +> Este software é um **tradutor** entre a confusão do FFmpeg e a ordem que o usuário precisa. + +O FFmpeg é uma ferramenta de poder absurdo — e opacidade equivalente. O usuário não entende `track index`, `stream mapping` ou `itsoffset`. O usuário entende: + +- *"Áudio em português está 1.2 segundos atrasado"* +- *"Quero adicionar a legenda em inglês"* +- *"Gerar o arquivo final"* + +Toda decisão de UX deve partir dessa perspectiva. Termos técnicos do FFmpeg nunca devem aparecer na interface. + +| FFmpeg (interno) | Interface (usuário) | +|-----------------------|--------------------------------------| +| `-itsoffset -1200ms` | "Adiantar 1.2s" | +| `stream 0:a:1` | "Faixa de áudio 2 — Português" | +| `-c copy` | *(invisível — nunca exposto)* | +| `ffprobe output` | Lista de faixas detectadas | + +--- + +## 14. Insight Arquitetural — MKV como Banco de Dados + +> Você não está editando vídeo. Você está **editando metadados de um container**. + +O MKV (Matroska) é estruturalmente um banco de dados multimídia. Suas operações são: + +- **INSERT** uma faixa de áudio ou legenda +- **UPDATE** o timestamp (offset) de uma faixa existente +- **SELECT** as faixas para inspecionar via ffprobe + +O arquivo de saída não é uma criação nova — é uma nova **visão** do container original com faixas adicionadas e metadados ajustados, sem tocar nos bytes de mídia. + +Esse modelo mental simplifica a implementação: +- Sem buffers de vídeo +- Sem pipelines de transcoding +- Apenas mapeamento de streams e metadados + +Quando a equipe pensa assim, o código fica limpo, as abstrações fazem sentido e os bugs diminuem. diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..95d23b8 --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[tools] +rust = "latest" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +}