feat: initial commit - Jhonny Editor

- Adicionado estrutura completa do projeto
- Configurado MCP server para Premiere Pro
- Adicionado documentação e skills
- Configurado Gitignore para o projeto
This commit is contained in:
João Henrique
2026-09-08 09:59:31 -04:00
commit b541f502ba
1507 changed files with 387650 additions and 0 deletions
+641
View File
@@ -0,0 +1,641 @@
# Instrução de Arquitetura da Engine
Este documento define o papel de cada classe, suas responsabilidades e o que ela não deve fazer.
# Estrutura do módulo `scanner`
O módulo `scanner` será responsável por analisar uma timeline completa antes que qualquer decisão de edição seja tomada.
Ele deverá apenas **descobrir, processar, organizar e salvar informações** sobre o material audiovisual.
O scanner **não deverá decidir quais trechos serão cortados**, nem aplicar cortes na timeline. Essas responsabilidades pertencem aos módulos posteriores, como o motor de decisão, o gerador de plano e o aplicador.
```text
scanner/
├── coordenacao/
├── descoberta/
├── metadados/
├── audio/
├── transcricao/
├── visual/
├── cenas/
├── eventos/
├── retakes/
└── persistencia/
```
---
# 1. Submódulo `coordenacao`
O submódulo `coordenacao` será responsável por organizar a execução do scanner.
Ele não fará a análise técnica dos vídeos. Sua função será controlar o fluxo, compartilhar o contexto e garantir que as análises sejam executadas na ordem correta.
## `AnalisadorDeTimeline`
### Papel
Será a classe principal de entrada do scanner.
Ela receberá uma timeline ou uma representação dela e iniciará o processo completo de análise.
### Responsabilidades
* Receber a timeline que será analisada.
* Criar o contexto inicial da análise.
* Montar ou receber o pipeline de análises.
* Iniciar a execução do pipeline.
* Retornar o resultado completo da análise.
* Informar o status geral do processo.
* Tratar erros gerais de execução.
* Permitir que a análise seja iniciada, interrompida ou retomada futuramente.
### Não deve fazer
* Extrair áudio diretamente.
* Transcrever vídeos.
* Detectar cenas.
* Analisar imagens.
* Detectar retakes.
* Decidir cortes.
* Alterar a timeline original.
---
## `PipelineDoScanner`
### Papel
Será responsável por executar as análises na ordem definida.
Ele funcionará como o controlador do fluxo interno do scanner.
### Responsabilidades
* Receber uma lista de componentes de análise.
* Executar cada componente na ordem correta.
* Entregar o mesmo contexto para a próxima análise.
* Registrar quais análises já foram executadas.
* Permitir execução parcial ou completa.
* Identificar falhas em etapas específicas.
* Permitir que determinadas análises sejam opcionais.
* Permitir futuramente execução paralela quando não houver dependências entre as análises.
### Exemplo de ordem
```text
Descoberta da timeline
↓
Descoberta de clipes
↓
Extração de metadados
↓
Extração de áudio
↓
Transcrição
↓
Análise visual
↓
Detecção de cenas
↓
Detecção de eventos
↓
Detecção de retakes
↓
Persistência
```
### Não deve fazer
* Implementar algoritmos de análise.
* Conhecer detalhes do Whisper, OpenCV, FFmpeg ou outros providers.
* Fazer chamadas diretas para APIs de IA.
* Tomar decisões de edição.
---
## `ContextoDeAnalise`
### Papel
Será o objeto que transportará todos os dados durante o processo de análise.
Ele funcionará como um estado compartilhado entre as classes do scanner.
### Responsabilidades
* Armazenar a timeline analisada.
* Armazenar sequências, faixas e clipes.
* Armazenar caminhos dos arquivos.
* Armazenar metadados técnicos.
* Armazenar áudios extraídos.
* Armazenar transcrições.
* Armazenar quadros extraídos.
* Armazenar cenas detectadas.
* Armazenar eventos encontrados.
* Armazenar possíveis retakes.
* Armazenar avisos, erros e status.
* Armazenar informações de execução.
* Permitir que os resultados sejam serializados.
### Não deve fazer
* Executar análises.
* Chamar providers diretamente.
* Decidir cortes.
* Alterar a timeline no editor.
---
# 2. Submódulo `descoberta`
O submódulo `descoberta` será responsável por identificar o que existe na timeline e onde os arquivos estão localizados.
## `DescobertaDaTimeline`
### Papel
Será responsável por descobrir a estrutura lógica da timeline.
### Responsabilidades
* Identificar a sequência ativa.
* Identificar outras sequências, quando necessário.
* Identificar as faixas de vídeo.
* Identificar as faixas de áudio.
* Identificar os clipes presentes em cada faixa.
* Identificar a ordem dos clipes.
* Identificar o posicionamento temporal dos clipes.
* Identificar os vínculos entre áudio e vídeo.
* Identificar transições e elementos existentes.
* Identificar clipes desativados ou ocultos.
* Criar uma representação interna da timeline.
### Não deve fazer
* Analisar o conteúdo visual.
* Transcrever o áudio.
* Detectar retakes.
* Decidir se um clipe será mantido ou removido.
---
## `DescobertaDeClipes`
### Papel
Será responsável por transformar os elementos encontrados na timeline em objetos de clipe que possam ser analisados pelo sistema.
### Responsabilidades
* Criar uma representação individual para cada clipe.
* Registrar o identificador do clipe.
* Registrar a faixa em que o clipe está.
* Registrar o tempo de início e fim na timeline.
* Registrar o tempo de início e fim no arquivo original.
* Registrar a duração.
* Registrar a posição relativa.
* Registrar vínculos com outros clipes.
* Identificar clipes de vídeo, áudio, imagens ou outros tipos de mídia.
### Não deve fazer
* Analisar o conteúdo do clipe.
* Gerar transcrição.
* Detectar cenas.
* Alterar a posição do clipe.
---
## `DescobertaDeArquivos`
### Papel
Será responsável por localizar os arquivos físicos relacionados aos clipes.
### Responsabilidades
* Resolver o caminho original do arquivo.
* Verificar se o arquivo existe.
* Identificar arquivos offline.
* Identificar arquivos duplicados.
* Identificar arquivos substituídos ou relinkados.
* Normalizar caminhos.
* Registrar permissões de acesso.
* Identificar o tipo de mídia.
* Preparar os arquivos para os providers.
### Não deve fazer
* Extrair metadados detalhados.
* Transcrever áudio.
* Analisar imagens.
* Corrigir automaticamente arquivos ausentes sem autorização.
---
# 3. Submódulo `metadados`
## `ExtracaoDeMetadados`
### Papel
Será responsável por extrair informações técnicas dos arquivos de mídia.
### Responsabilidades
* Identificar resolução.
* Identificar largura e altura.
* Identificar taxa de quadros.
* Identificar duração.
* Identificar codec de vídeo.
* Identificar codec de áudio.
* Identificar quantidade de canais.
* Identificar taxa de amostragem.
* Identificar profundidade de bits.
* Identificar orientação.
* Identificar timecode.
* Identificar tamanho do arquivo.
* Identificar formato do contêiner.
* Identificar informações de gravação, quando disponíveis.
* Registrar erros de leitura.
### Não deve fazer
* Avaliar se a imagem está boa.
* Avaliar se o áudio está ruim.
* Detectar retakes.
* Decidir quais arquivos serão usados na edição.
---
# 4. Submódulo `audio`
## `ExtracaoDeAudio`
### Papel
Será responsável por preparar o áudio dos vídeos para as demais análises.
### Responsabilidades
* Extrair o áudio dos arquivos de vídeo.
* Gerar arquivos temporários ou intermediários.
* Normalizar o formato de áudio quando necessário.
* Definir taxa de amostragem adequada.
* Separar canais quando necessário.
* Associar o áudio extraído ao clipe original.
* Registrar o caminho do áudio gerado.
* Evitar extrações repetidas.
* Controlar arquivos temporários.
* Validar se o áudio foi extraído corretamente.
### Não deve fazer
* Transcrever o áudio.
* Avaliar o conteúdo da fala.
* Decidir se há um retake.
* Alterar o áudio da timeline.
---
## `AnaliseDeAudio`
### Papel
Será responsável por analisar tecnicamente e temporalmente o áudio.
### Responsabilidades
* Detectar silêncio.
* Detectar pausas.
* Medir volume.
* Medir energia sonora.
* Identificar picos de áudio.
* Identificar possíveis distorções.
* Identificar ruído.
* Identificar clipping.
* Identificar trechos com baixa inteligibilidade.
* Identificar início e fim de fala.
* Identificar sobreposição de vozes, quando possível.
* Produzir marcadores temporais de eventos sonoros.
### Não deve fazer
* Transcrever o áudio.
* Decidir automaticamente quais trechos serão cortados.
* Substituir a análise de conteúdo feita pela transcrição.
---
# 5. Submódulo `transcricao`
## `TranscricaoDeAudio`
### Papel
Será responsável por transformar o áudio em texto sincronizado com o tempo do vídeo.
### Responsabilidades
* Enviar o áudio para o provider de transcrição.
* Receber segmentos transcritos.
* Registrar texto, início e fim de cada segmento.
* Registrar palavras individuais, quando disponíveis.
* Registrar nível de confiança.
* Identificar locutores, quando suportado.
* Associar a transcrição ao clipe correto.
* Detectar falhas de transcrição.
* Permitir transcrição parcial.
* Reaproveitar transcrições já existentes.
* Preservar a sincronização temporal.
### Não deve fazer
* Decidir se uma fala deve ser cortada.
* Interpretar sozinho se o trecho é um retake.
* Alterar a timeline.
* Implementar diretamente o modelo de transcrição.
O modelo utilizado deverá ficar no módulo `providers/transcricao/`.
---
# 6. Submódulo `visual`
## `ExtracaoDeQuadros`
### Papel
Será responsável por selecionar e extrair quadros representativos dos vídeos.
### Responsabilidades
* Extrair o primeiro quadro.
* Extrair o quadro central.
* Extrair o último quadro.
* Extrair quadros em intervalos regulares.
* Extrair quadros próximos a eventos.
* Extrair quadros próximos a mudanças de cena.
* Redimensionar imagens para análise.
* Evitar extrações duplicadas.
* Associar cada quadro ao tempo exato do vídeo.
* Armazenar os quadros temporariamente ou em cache.
### Não deve fazer
* Interpretar o conteúdo da imagem.
* Classificar a qualidade visual.
* Detectar retakes.
* Escolher o melhor quadro para a edição.
---
## `AnaliseVisual`
### Papel
Será responsável por coordenar a interpretação do conteúdo visual dos quadros e dos vídeos.
### Responsabilidades
* Analisar enquadramento.
* Identificar objetos.
* Identificar pessoas.
* Identificar rostos, quando permitido e necessário.
* Avaliar foco.
* Avaliar exposição.
* Avaliar estabilidade.
* Identificar movimentos de câmera.
* Identificar mudanças de composição.
* Descrever o conteúdo visual.
* Comparar quadros.
* Gerar características visuais que possam ser utilizadas na detecção de cenas e retakes.
### Não deve fazer
* Detectar cortes diretamente, salvo quando isso fizer parte do provider visual.
* Decidir quais tomadas serão utilizadas.
* Alterar o vídeo.
* Implementar diretamente os modelos de visão.
---
# 7. Submódulo `cenas`
## `DeteccaoDeCenas`
### Papel
Será responsável por identificar mudanças de cena e possíveis limites entre tomadas.
### Responsabilidades
* Detectar cortes abruptos.
* Detectar transições.
* Detectar mudanças graduais.
* Identificar possíveis inícios e finais de tomadas.
* Combinar resultados de diferentes providers.
* Comparar mudanças visuais entre quadros.
* Utilizar informações de áudio quando necessário.
* Registrar o tempo de cada cena.
* Registrar o nível de confiança.
* Identificar cenas semelhantes.
* Evitar duplicidade entre resultados de providers.
### Não deve fazer
* Decidir qual cena será utilizada na edição.
* Excluir clipes.
* Aplicar cortes.
* Depender de apenas uma ferramenta específica.
A classe deverá trabalhar com contratos de providers, por exemplo:
```text
DeteccaoDeCenas
↓
ProviderDeDeteccaoDeCenas
├── ProviderPySceneDetect
├── ProviderOpenCV
└── ProviderModeloDeIA
```
---
# 8. Submódulo `eventos`
## `DeteccaoDeEventos`
### Papel
Será responsável por identificar acontecimentos relevantes dentro dos clipes.
### Responsabilidades
* Identificar início de fala.
* Identificar fim de fala.
* Identificar pausas.
* Identificar silêncio.
* Identificar risadas.
* Identificar tosse.
* Identificar interrupções.
* Identificar erros de fala.
* Identificar mudanças de assunto.
* Identificar entrada ou saída de pessoas.
* Identificar alterações importantes de imagem.
* Identificar problemas técnicos.
* Registrar cada evento com início, fim e confiança.
### Não deve fazer
* Decidir automaticamente o corte.
* Remover eventos.
* Alterar a timeline.
* Confundir evento detectado com decisão de edição.
O evento será apenas uma informação para o motor de decisão utilizar posteriormente.
---
# 9. Submódulo `retakes`
## `DeteccaoDeRetakes`
### Papel
Será responsável por identificar possíveis repetições ou versões alternativas de uma mesma gravação.
### Responsabilidades
* Comparar transcrições.
* Comparar características visuais.
* Comparar áudio.
* Comparar duração.
* Comparar sequência de falas.
* Comparar enquadramento.
* Identificar tomadas próximas temporalmente.
* Identificar grupos de tomadas semelhantes.
* Identificar possíveis erros repetidos.
* Identificar versões alternativas da mesma fala.
* Calcular nível de similaridade.
* Registrar evidências que justificam a possibilidade de retake.
* Classificar o resultado como possível, provável ou confirmado, quando houver evidências suficientes.
### Não deve fazer
* Decidir qual retake será utilizado.
* Excluir automaticamente uma tomada.
* Aplicar cortes.
* Considerar apenas a similaridade visual.
* Tratar toda repetição como erro.
O resultado deverá ser algo semelhante a:
```text
Grupo de retakes:
- Tomada 01
- Tomada 02
- Tomada 03
Evidências:
- Transcrição semelhante
- Enquadramento semelhante
- Áudio semelhante
- Intervalo temporal próximo
Confiança:
0.87
```
---
# 10. Submódulo `persistencia`
## `PersistenciaDaAnalise`
### Papel
Será responsável por salvar os resultados produzidos pelo scanner.
### Responsabilidades
* Salvar a estrutura descoberta da timeline.
* Salvar os metadados.
* Salvar os caminhos dos arquivos.
* Salvar as transcrições.
* Salvar os resultados visuais.
* Salvar as cenas.
* Salvar os eventos.
* Salvar os possíveis retakes.
* Salvar logs e avisos.
* Permitir retomada de uma análise interrompida.
* Evitar processamento duplicado.
* Versionar os resultados quando necessário.
* Exportar os dados em formato estruturado, como JSON.
### Não deve fazer
* Gerar plano de corte.
* Aplicar alterações no editor.
* Decidir quais clipes serão mantidos.
* Alterar os arquivos originais.
---
# Visão final das responsabilidades
```text
AnalisadorDeTimeline
Inicia a análise completa.
PipelineDoScanner
Controla a ordem de execução.
ContextoDeAnalise
Transporta e armazena os dados.
DescobertaDaTimeline
Descobre a estrutura da timeline.
DescobertaDeClipes
Representa os clipes encontrados.
DescobertaDeArquivos
Localiza os arquivos físicos.
ExtracaoDeMetadados
Obtém informações técnicas.
ExtracaoDeAudio
Prepara o áudio.
AnaliseDeAudio
Analisa características sonoras.
TranscricaoDeAudio
Converte fala em texto sincronizado.
ExtracaoDeQuadros
Seleciona quadros para análise.
AnaliseVisual
Interpreta o conteúdo visual.
DeteccaoDeCenas
Identifica limites e mudanças de cena.
DeteccaoDeEventos
Identifica acontecimentos relevantes.
DeteccaoDeRetakes
Identifica possíveis repetições.
PersistenciaDaAnalise
Salva todos os resultados.
```
A regra mais importante para o programador será:
> **Nenhuma classe do scanner deverá tomar decisões de edição. O scanner apenas coleta e organiza evidências. A decisão sobre cortar, manter, substituir ou reorganizar trechos será feita por outro módulo.**
+13
View File
@@ -0,0 +1,13 @@
# Engine
Núcleo Python orientado a objetos para o fluxo de scanner.
```python
from engine import Scanner
from engine.domain import Content
result = Scanner().scan(Content("asset-1", " texto "))
assert result.valid
```
As integrações reais devem implementar os protocolos em `content_analyzer.py`, `decision_engine.py`, `edit_plan.py`, `plan_applicator.py`, `validator.py` e `persistence.py`. O pacote não cria dependências externas por padrão.
+5
View File
@@ -0,0 +1,5 @@
"""Motor OO do sistema."""
from .scanner.coordenacao import AnalisadorDeTimeline, ContextoDeAnalise, PipelineDoScanner
__all__ = ["AnalisadorDeTimeline", "ContextoDeAnalise", "PipelineDoScanner"]
+56
View File
@@ -0,0 +1,56 @@
# Arquitetura do Sistema
Esta pasta concentra a especificação arquitetural do novo sistema Python orientado a objetos.
## Objetivo
Documentar previamente:
- a estrutura geral do sistema;
- os módulos e submódulos;
- as responsabilidades de cada classe;
- o que cada classe não deve fazer;
- os fluxos entre os módulos;
- as regras de dependência e integração;
- as decisões arquiteturais do projeto.
## Regra de nomenclatura
Todos os nomes de módulos, classes, métodos, funções e variáveis serão sempre em PT-BR.
## Organização da documentação
Cada módulo principal deverá possuir um documento próprio nesta pasta.
```text
arquitetura/
├── README.md
├── scanner.md
├── integracao-com-premiere.md
├── leitura-da-timeline-via-mcp.md
├── plano-primeira-etapa-scanner-e-mcp.md
├── planos-proximas-etapas.md
├── analisador-de-conteudo.md
├── motor-de-decisao.md
├── gerador-de-plano-de-edicao.md
├── aplicador-de-plano.md
├── validador.md
├── providers-de-ia.md
├── modelo-de-dominio.md
├── persistencia.md
├── configuracao.md
├── logging.md
└── testes.md
```
Os documentos serão criados conforme cada módulo for projetado. Não devemos escrever código de implementação antes de definir sua arquitetura neste diretório.
## Princípios gerais
1. O domínio deve ser independente de infraestrutura e de ferramentas externas.
2. Cada módulo deve ter uma responsabilidade clara.
3. As dependências devem ser recebidas por abstrações bem definidas.
4. Integrações externas devem ser implementadas por adaptadores substituíveis.
5. O fluxo entre módulos deve ser explícito e documentado.
6. Cada classe deve declarar suas responsabilidades e suas proibições.
7. O sistema deve ser testável sem depender de serviços externos reais.
@@ -0,0 +1,682 @@
Sim. **O ideal é tratar o MCP como um módulo completo de integração com o Premiere**, e não como uma única classe responsável por tudo.
O MCP será utilizado para várias finalidades:
* Ler a timeline ativa;
* Ler sequências, faixas e clipes;
* Obter propriedades dos clipes;
* Criar, mover, cortar ou excluir elementos;
* Aplicar alterações na timeline;
* Executar comandos no Premiere;
* Consultar o estado atual do projeto;
* Validar se uma operação foi executada corretamente.
Por isso, uma única classe como `ClienteMCP` ficaria sobrecarregada rapidamente.
## Estrutura recomendada
```text
integracoes/
└── premiere/
├── __init__.py
│
├── cliente_mcp.py
├── sessao_mcp.py
├── erros_mcp.py
│
├── leitura/
│ ├── acesso_a_timeline.py
│ ├── acesso_a_sequencia.py
│ ├── acesso_a_faixas.py
│ └── acesso_a_clipes.py
│
├── escrita/
│ ├── executor_de_comandos.py
│ ├── manipulador_de_clipes.py
│ ├── manipulador_de_timeline.py
│ └── aplicador_de_operacoes.py
│
├── conversores/
│ ├── conversor_de_timeline.py
│ ├── conversor_de_clipes.py
│ └── conversor_de_respostas.py
│
└── contratos/
├── acesso_ao_editor.py
└── executor_do_editor.py
```
A ideia principal é separar:
> **Comunicação com o MCP**, **leitura do Premiere**, **execução de comandos** e **conversão dos dados**.
---
# 1. `ClienteMCP`
Arquivo:
```text
integracoes/premiere/cliente_mcp.py
```
Classe:
```python
class ClienteMCP:
"""Responsável pela comunicação técnica com o servidor MCP do Premiere."""
```
Essa classe deve cuidar somente da comunicação.
Responsabilidades:
* Abrir conexão;
* Encerrar conexão;
* Enviar uma chamada;
* Receber a resposta;
* Controlar timeout;
* Tratar erros de comunicação;
* Registrar logs;
* Identificar falhas de conexão;
* Possivelmente reconectar.
Exemplo conceitual:
```python
class ClienteMCP:
"""Executa chamadas técnicas contra o servidor MCP."""
def conectar(self) -> None:
"""Estabelece a conexão com o MCP."""
def desconectar(self) -> None:
"""Encerra a conexão com o MCP."""
def chamar(self, nome_da_ferramenta: str, argumentos: dict) -> dict:
"""Executa uma ferramenta MCP e retorna sua resposta."""
```
Ele **não deve saber o que é uma timeline, um clipe ou um retake**.
Para ele, existe apenas:
```text
chamar ferramenta → receber resposta
```
---
# 2. `SessaoMCP`
Arquivo:
```text
integracoes/premiere/sessao_mcp.py
```
Classe:
```python
class SessaoMCP:
"""Controla o estado de uma sessão de comunicação com o Premiere."""
```
Pode ser útil para armazenar:
* Identificação da sessão;
* Estado da conexão;
* Projeto atual;
* Sequência ativa;
* Última operação executada;
* Ferramentas disponíveis;
* Informações de capacidade do MCP.
Exemplo:
```python
class SessaoMCP:
"""Representa o estado atual da integração com o Premiere."""
def __init__(self):
self.conectado = False
self.projeto_atual = None
self.sequencia_ativa = None
```
Essa classe não é obrigatória na primeira versão, mas será útil quando a integração crescer.
---
# 3. Módulo de leitura
O módulo de leitura seria responsável por transformar comandos técnicos do MCP em operações compreensíveis pelo sistema.
## `AcessoATimeline`
Arquivo:
```text
integracoes/premiere/leitura/acesso_a_timeline.py
```
Classe:
```python
class AcessoATimeline:
"""Fornece operações de leitura da timeline do Premiere."""
```
Métodos possíveis:
```python
class AcessoATimeline:
"""Lê a estrutura da timeline ativa."""
def obter_timeline_ativa(self):
"""Retorna os dados da timeline ativa."""
def obter_sequencia_ativa(self):
"""Retorna a sequência atualmente selecionada."""
def obter_faixas(self):
"""Retorna as faixas de vídeo e áudio."""
def obter_clipes(self):
"""Retorna os clipes presentes na timeline."""
def obter_detalhes_do_clipe(self, identificador_do_clipe):
"""Retorna os detalhes de um clipe específico."""
```
O fluxo seria:
```text
AcessoATimeline
↓
ClienteMCP
↓
MCP do Premiere
```
O scanner chamaria:
```python
timeline = acesso_a_timeline.obter_timeline_ativa()
```
E não:
```python
cliente_mcp.chamar("alguma_ferramenta_interna", {...})
```
Isso é importante porque o restante do sistema não deve conhecer os detalhes do MCP.
---
## Outras classes de leitura
Podemos dividir conforme a complexidade:
```text
leitura/
├── acesso_a_timeline.py
├── acesso_a_sequencia.py
├── acesso_a_faixas.py
├── acesso_a_clipes.py
├── acesso_a_projeto.py
└── acesso_a_itens_de_midia.py
```
Entretanto, no início, não é necessário criar todas imediatamente.
Podemos começar com:
```text
AcessoAoEditor
```
e depois dividir quando a classe crescer demais.
---
# 4. `AcessoAoEditor`
Eu recomendaria inicialmente uma classe de fachada chamada `AcessoAoEditor`.
Arquivo:
```text
integracoes/premiere/acesso_ao_editor.py
```
Classe:
```python
class AcessoAoEditor:
"""Oferece uma interface simplificada para consultar o Premiere."""
```
Ela funcionaria como uma porta de entrada para as operações de leitura:
```python
class AcessoAoEditor:
"""Centraliza o acesso estruturado aos dados do Premiere."""
def __init__(self, acesso_a_timeline, acesso_a_clipes):
self.acesso_a_timeline = acesso_a_timeline
self.acesso_a_clipes = acesso_a_clipes
def obter_timeline_ativa(self):
"""Obtém a timeline ativa."""
return self.acesso_a_timeline.obter_timeline_ativa()
def obter_clipes(self):
"""Obtém os clipes da timeline ativa."""
return self.acesso_a_clipes.obter_clipes()
```
Assim, o scanner dependeria de:
```python
AcessoAoEditor
```
e não diretamente de:
```python
ClienteMCP
```
---
# 5. Módulo de escrita
A leitura e a escrita devem ser separadas.
```text
integracoes/premiere/
├── leitura/
└── escrita/
```
Isso evita misturar:
* Consultar dados;
* Alterar dados;
* Executar comandos destrutivos.
## `ExecutorDeComandos`
Arquivo:
```text
integracoes/premiere/escrita/executor_de_comandos.py
```
Classe:
```python
class ExecutorDeComandos:
"""Executa comandos de alteração no Premiere por meio do MCP."""
```
Responsabilidades:
* Executar uma operação;
* Enviar parâmetros;
* Receber resultado;
* Identificar falhas;
* Retornar confirmação da operação.
Exemplo:
```python
class ExecutorDeComandos:
"""Executa comandos no editor."""
def executar(self, nome_do_comando: str, argumentos: dict):
"""Executa um comando no Premiere."""
```
Mas essa classe não deveria decidir **qual comando deve ser executado**. Ela apenas executa o comando que recebeu.
---
## `AplicadorDeOperacoes`
Arquivo:
```text
integracoes/premiere/escrita/aplicador_de_operacoes.py
```
Classe:
```python
class AplicadorDeOperacoes:
"""Aplica operações de edição estruturadas na timeline."""
```
Essa classe recebe operações já definidas pelo plano de edição:
```python
class AplicadorDeOperacoes:
"""Aplica operações de edição no Premiere."""
def aplicar_corte(self, operacao):
"""Aplica uma operação de corte."""
def mover_clipe(self, operacao):
"""Move um clipe na timeline."""
def excluir_clipe(self, operacao):
"""Exclui um clipe da timeline."""
def aplicar_plano(self, plano):
"""Aplica todas as operações de um plano de edição."""
```
O fluxo seria:
```text
AplicadorDeOperacoes
↓
ExecutorDeComandos
↓
ClienteMCP
↓
MCP do Premiere
```
---
# 6. `ConversorDeTimeline`
Arquivo:
```text
integracoes/premiere/conversores/conversor_de_timeline.py
```
Classe:
```python
class ConversorDeTimeline:
"""Converte os dados brutos do Premiere para o modelo interno do sistema."""
```
Essa classe é muito importante.
O MCP pode retornar dados em um formato específico, por exemplo:
```json
{
"sequence": {
"name": "Sequência 01",
"timebase": 25
},
"tracks": [],
"clips": []
}
```
Mas o sistema não deveria depender diretamente desse formato.
O conversor transforma isso em objetos próprios:
```python
class ConversorDeTimeline:
"""Converte uma resposta do Premiere para o domínio interno."""
def converter(self, dados_brutos):
"""Converte os dados brutos em uma timeline do sistema."""
```
Assim, se futuramente o MCP mudar, somente a integração e os conversores precisarão ser ajustados.
O restante do sistema continua funcionando.
---
# 7. Contratos para desacoplar o sistema
O ideal é criar contratos para que o scanner não dependa de uma implementação específica.
## `AcessoAoEditor`
Arquivo:
```text
integracoes/premiere/contratos/acesso_ao_editor.py
```
Exemplo:
```python
from abc import ABC, abstractmethod
class AcessoAoEditor(ABC):
"""Define as operações de leitura necessárias para acessar um editor."""
@abstractmethod
def obter_timeline_ativa(self):
"""Obtém a timeline ativa do editor."""
raise NotImplementedError
@abstractmethod
def obter_clipes(self):
"""Obtém os clipes da timeline."""
raise NotImplementedError
```
Depois podemos ter:
```text
AcessoAoEditor
├── AcessoAoPremiere
├── AcessoAoOpenCut
└── AcessoAoEditorSimulado
```
O último é especialmente útil para testes.
Por exemplo:
```python
class AcessoAoEditorSimulado(AcessoAoEditor):
"""Fornece dados fictícios para testes sem abrir o Premiere."""
```
---
# 8. Como o scanner usaria isso
A classe `DescobertaDaTimeline` não deveria conhecer MCP.
Ela deveria conhecer apenas uma interface de acesso ao editor.
```python
class DescobertaDaTimeline:
"""Descobre a estrutura da timeline a partir do editor."""
def __init__(self, acesso_ao_editor):
self.acesso_ao_editor = acesso_ao_editor
def executar(self, contexto):
"""Lê a timeline e armazena os dados no contexto."""
timeline = self.acesso_ao_editor.obter_timeline_ativa()
contexto.timeline = timeline
return contexto
```
O encadeamento seria:
```text
DescobertaDaTimeline
↓
AcessoAoPremiere
↓
AcessoATimeline
↓
ClienteMCP
↓
MCP do Premiere
```
Ou, usando uma fachada:
```text
DescobertaDaTimeline
↓
AcessoAoEditor
↓
ClienteMCP
↓
MCP do Premiere
```
---
# 9. Arquitetura completa recomendada
```text
projeto/
│
├── scanner/
│ ├── coordenacao/
│ │ ├── analisador_de_timeline.py
│ │ ├── pipeline_do_scanner.py
│ │ └── contexto_de_analise.py
│ │
│ ├── descoberta/
│ │ ├── descoberta_da_timeline.py
│ │ ├── descoberta_de_clipes.py
│ │ └── descoberta_de_arquivos.py
│ │
│ ├── metadados/
│ ├── audio/
│ ├── transcricao/
│ ├── visual/
│ ├── cenas/
│ └── retakes/
│
├── providers/
│ ├── transcricao/
│ ├── analise_visual/
│ ├── deteccao_de_cenas/
│ └── analise_de_audio/
│
├── integracoes/
│ └── premiere/
│ ├── cliente_mcp.py
│ ├── sessao_mcp.py
│ ├── erros_mcp.py
│ │
│ ├── leitura/
│ │ ├── acesso_a_timeline.py
│ │ ├── acesso_a_sequencia.py
│ │ ├── acesso_a_faixas.py
│ │ └── acesso_a_clipes.py
│ │
│ ├── escrita/
│ │ ├── executor_de_comandos.py
│ │ ├── aplicador_de_operacoes.py
│ │ └── manipulador_de_timeline.py
│ │
│ ├── conversores/
│ │ ├── conversor_de_timeline.py
│ │ ├── conversor_de_clipes.py
│ │ └── conversor_de_respostas.py
│ │
│ └── contratos/
│ ├── acesso_ao_editor.py
│ └── executor_do_editor.py
│
├── dominio/
├── configuracao/
├── persistencia/
└── testes/
```
## Minha recomendação prática
Para a primeira versão, não criaria todas as classes imediatamente. Começaria assim:
```text
integracoes/
└── premiere/
├── cliente_mcp.py
├── acesso_ao_editor.py
├── executor_de_comandos.py
├── conversor_de_timeline.py
└── erros_mcp.py
```
Depois, quando as responsabilidades crescerem:
```text
acesso_ao_editor.py
```
poderá ser dividido em:
```text
leitura/
├── acesso_a_timeline.py
├── acesso_a_clipes.py
└── acesso_a_projeto.py
```
E:
```text
executor_de_comandos.py
```
poderá ser dividido em:
```text
escrita/
├── aplicador_de_operacoes.py
├── manipulador_de_clipes.py
└── manipulador_de_timeline.py
```
## Regra central
A arquitetura deve seguir esta separação:
```text
Scanner
define o que precisa ser analisado
Integração com o Premiere
acessa e altera o editor
Cliente MCP
faz a comunicação técnica
Conversores
transformam dados externos em dados internos
Providers
executam análises usando tecnologias específicas
Motor de decisão
decide o que fazer com os resultados
Aplicador de plano
transforma decisões em operações no editor
```
Portanto, a resposta direta é:
> **MCP deve ser um módulo de integração completo, com `ClienteMCP` como classe de comunicação central, classes especializadas de leitura e escrita, conversores e contratos.** Ele não deve ser uma única classe gigante nem ficar misturado dentro do scanner.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,117 @@
# Plano de leitura de vídeo com tecnologias Apple
## Conclusão
O caminho recomendado é um pipeline híbrido, local e substituível:
1. `FFmpeg` continua extraindo áudio e amostras de vídeo.
2. `Speech` faz a transcrição temporal do áudio no macOS, preferencialmente com `supportsOnDeviceRecognition` e `requiresOnDeviceRecognition` quando disponíveis.
3. `Vision` analisa os quadros: OCR, pessoas/objetos, rostos, códigos, poses e mudanças de cena conforme a necessidade.
4. `FoundationModels` (Apple Intelligence) recebe um pacote compacto de evidências — transcrição, descrições dos quadros, OCR e metadados — e produz resumo, tópicos, classificação e sugestões editoriais estruturadas.
O modelo de linguagem não deve receber o arquivo de vídeo inteiro como entrada direta. A documentação do Foundation Models descreve geração e entendimento de texto, geração estruturada, ferramentas e análise de imagens; a análise de vídeo deve ser orquestrada pelo nosso pipeline, ou por um provider multimodal próprio no futuro.
## O que já existe no Engine
- `engine/scanner/analise.py` já define `ProviderDeTranscricao` e `ProviderDeAnaliseVisual`.
- `engine/scanner/transcricao_da_timeline.py` já divide o resultado por clipe e corrige os offsets das partes.
- `engine/integracoes/midia/extracao_de_audio.py` já gera WAV mono, 16 kHz e blocos de até 600 s.
- `engine/integracoes/apple_speech/` já possui um executável Swift usando `SFSpeechRecognizer` e um provider Python.
- `engine/integracoes/whisper/` fornece fallback local.
O relatório Apple atual confirma que o adaptador está integrado ao fluxo, mas também evidencia uma falha operacional a investigar: a execução reportada não encontrou fala em todos os intervalos. Antes de comparar qualidade, devemos validar permissão, disponibilidade do locale, formato/volume do WAV e se o modo on-device foi realmente ativado.
## Tecnologias disponíveis
### Speech
`SFSpeechRecognizer` aceita arquivos existentes com `SFSpeechURLRecognitionRequest`, fornece segmentos com timestamp e expõe `supportsOnDeviceRecognition`. Há limite documentado para tarefas longas, portanto a divisão existente em blocos é adequada. O provider deve manter os offsets, a confiança e o locale.
### Vision
Vision é a camada Apple para análise de fotos e vídeos. Para o primeiro corte, implementar apenas:
- `RecognizeTextRequest` para texto em tela;
- detecção de pessoas/objetos ou classificação, se a decisão editorial exigir;
- amostragem temporal de quadros e agrupamento de resultados semelhantes;
- detecção de mudança de cena, caso a implementação determinística atual ainda não cubra o caso.
Vision não deve ser chamado em todos os frames. O sampler deve escolher, por exemplo, um frame a cada 1–2 segundos e frames próximos a cortes, mantendo `timestamp`, `confidence` e a origem do frame.
### Foundation Models / Apple Intelligence
`FoundationModels` fornece o LLM local que alimenta Apple Intelligence. É adequado para resumir a transcrição, extrair entidades/tópicos, classificar trechos, sugerir títulos e gerar estruturas Swift com `@Generable`. A disponibilidade precisa ser verificada em runtime por `SystemLanguageModel.default`; Apple Intelligence precisa estar habilitado e o sistema/dispositivo precisa ser compatível.
O contexto deve ser limitado e particionado. A documentação técnica da Apple indica janela de contexto de 4096 tokens para o modelo on-device; enviar o vídeo inteiro ou uma transcrição longa em uma única solicitação não é seguro. O contrato deve prever `truncation`, `modelUnavailable`, `guardrail` e `timeout`.
### App Intents
É uma opção posterior para expor ações do Engine ao Siri/Apple Intelligence — por exemplo, “resumir o clipe selecionado” ou “encontrar trechos em que se fala de X”. Não é a API de leitura do vídeo; é a camada de descoberta e ação.
## Arquitetura proposta
```text
Timeline/clip
├─ AudioExtractor ──> AppleSpeechProvider | WhisperProvider
├─ FrameSampler ────> VisionProvider
└─ MediaEvidenceStore
└─ FoundationModelsProvider
└─ AnalysisResult / EditorialPlan
```
Adicionar interfaces no domínio, mantendo o scanner independente:
```python
class ProviderDeQuadros(Protocol):
def amostrar(self, clipe: Any) -> list[QuadroDeVideo]: ...
class ProviderDeAnaliseSemantica(Protocol):
def interpretar(self, evidencias: PacoteDeEvidencias) -> ResultadoSemantico: ...
```
`QuadroDeVideo` deve conter `timestamp`, caminho temporário ou bytes, dimensões e índice. `EvidenciaDeVideo` deve conter tipo (`transcricao`, `ocr`, `objeto`, `cena`), intervalo temporal, valor, confiança e provider. O resultado do Foundation Models deve ser estruturado e validado antes de entrar em `caracteristicas_visuais`, `cenas` ou plano de edição.
## Implementação em fases
### Fase 1 — endurecer Apple Speech
- Corrigir o build/instalação do executável Swift para o ambiente do usuário.
- Tornar `somente_no_dispositivo=True` a opção explícita de privacidade.
- Capturar stderr, código de saída, locale, disponibilidade e modo efetivo no resultado.
- Testar WAV com fala conhecida em `pt-BR`, inclusive blocos menores que um minuto.
- Comparar Apple Speech, Whisper e Groq usando o mesmo áudio e medir WER, latência e falhas.
### Fase 2 — Vision
- Criar um pequeno helper Swift ou um app/CLI macOS que receba vídeo, timestamps e operações.
- Usar AVFoundation para ler frames; usar Vision por frame.
- Devolver JSON versionado, com timestamp absoluto e confiança.
- Adicionar testes com fixtures de texto em tela, pessoa e quadro sem conteúdo.
### Fase 3 — Foundation Models
- Criar um processo Swift residente (mais eficiente que iniciar um processo por trecho).
- Receber JSON de evidências via stdin/stdout ou IPC existente.
- Usar `LanguageModelSession` e geração guiada para um `ResultadoSemantico` fixo.
- Fazer chunking da transcrição e uma segunda etapa de consolidação.
- Persistir prompt/model-version/evidence revision para reprodutibilidade.
### Fase 4 — integração editorial
- Alimentar o `EditorialContextPack` com evidências citáveis e intervalos temporais.
- Manter análise e plano como operações read-only até validação.
- Só depois conectar resultados a rough-cut, marcadores ou legendas via APIs já existentes do Premiere.
## Decisão recomendada agora
Implementar primeiro `AppleSpeechProvider` robusto e `VisionEvidenceProvider`; deixar `FoundationModelsProvider` como uma etapa semântica posterior. Isso entrega leitura real de áudio e imagem imediatamente, preserva fallback para Windows/Whisper e evita acoplar o Engine Python a APIs Apple que só existem no macOS.
## Fontes oficiais
- [Foundation Models](https://developer.apple.com/documentation/FoundationModels)
- [Generating content and performing tasks with Foundation Models](https://developer.apple.com/documentation/FoundationModels/generating-content-and-performing-tasks-with-foundation-models)
- [Foundation Models updates](https://developer.apple.com/documentation/Updates/FoundationModels)
- [Built-in intelligence](https://developer.apple.com/documentation/technologyoverviews/built-in-intelligence)
- [SFSpeechRecognizer](https://developer.apple.com/documentation/speech/sfspeechrecognizer)
- [Apple Intelligence para desenvolvedores](https://developer.apple.com/apple-intelligence/)
- [TN3193 — context window](https://developer.apple.com/documentation/Technotes/tn3193-managing-the-on-device-foundation-model-s-context-window)
@@ -0,0 +1,366 @@
# Plano de Desenvolvimento — Primeira Etapa
## Scanner e leitura da timeline via MCP
## Status
Plano de desenvolvimento da primeira etapa. Este documento organiza a implementação futura; não autoriza a criação imediata de código sem que cada fase esteja preparada e validada.
## Objetivo
Construir o primeiro fluxo funcional do sistema capaz de ler a timeline ativa do Premiere por meio do MCP e transformá-la em uma representação interna confiável.
Ao final desta etapa, o sistema deverá conseguir:
1. comunicar-se com o MCP;
2. verificar a disponibilidade do Premiere;
3. ler a sequência ativa, faixas e clipes;
4. converter respostas externas em objetos do domínio;
5. validar dados incompletos ou inválidos;
6. executar a descoberta por meio do `Scanner`;
7. registrar erros e informações relevantes;
8. testar todo o fluxo sem depender do Premiere real.
## Limites da etapa
### Incluído
- conexão e chamadas técnicas ao MCP;
- sessão e erros da integração;
- leitura da timeline, sequência, faixas e clipes;
- conversores de dados externos;
- entidades e objetos de valor necessários;
- contrato de acesso ao editor;
- contexto, pipeline e descoberta da timeline;
- configuração mínima do scanner;
- logs técnicos e de fluxo;
- testes unitários, de integração simulada e de conversores;
- fixtures baseadas em respostas reais do MCP.
### Não incluído
- corte, exclusão ou movimentação de clipes;
- qualquer escrita no Premiere;
- plano de edição;
- decisão automática;
- transcrição;
- análise visual;
- análise de áudio;
- detecção de cenas;
- detecção de retakes;
- uso obrigatório de provider de IA;
- otimizações prematuras ou execução paralela.
## Estrutura planejada
```text
engine/
├── arquitetura/
│ └── documentação da arquitetura
├── scanner/
│ ├── coordenacao/
│ ├── contratos/
│ ├── modelos/
│ ├── configuracao/
│ └── descoberta/
├── integracoes/
│ └── premiere/
│ ├── cliente_mcp.py
│ ├── sessao_mcp.py
│ ├── erros_mcp.py
│ ├── leitura/
│ ├── conversores/
│ └── contratos/
├── dominio/
│ ├── entidades/
│ └── objetos_de_valor/
├── configuracao/
├── persistencia/
├── logging/
└── testes/
```
## Fases de desenvolvimento
### Fase 0 — Preparação e confirmação arquitetural
**Objetivo:** garantir que o desenho está coerente antes da implementação.
**Atividades:**
- revisar `scanner.md`, `integracao-com-premiere.md` e `leitura-da-timeline-via-mcp.md`;
- revisar a skill `boas-praticas-oo.md`;
- confirmar que todos os identificadores internos serão em PT-BR;
- definir os contratos antes das implementações concretas;
- confirmar que o scanner não terá dependência direta do MCP;
- identificar quais classes são realmente necessárias na primeira versão.
**Entrega:** arquitetura aprovada e sem responsabilidades sobrepostas.
### Fase 1 — Descoberta da API real do MCP
**Objetivo:** conhecer o contrato externo antes de criar adaptadores definitivos.
**Atividades:**
- identificar como o MCP é iniciado;
- verificar como a conexão é estabelecida;
- listar as ferramentas disponíveis;
- identificar ferramentas para projeto, sequência, faixas e clipes;
- registrar argumentos, respostas e erros;
- observar formato dos identificadores e dos tempos;
- capturar respostas reais anonimizadas para fixtures;
- confirmar se a comunicação é síncrona ou assíncrona;
- confirmar se existe estado de sessão.
**Regra:** nomes de ferramentas e campos externos não serão inventados. Eles ficarão isolados na integração.
**Entrega:** inventário do MCP e conjunto inicial de fixtures reais.
### Fase 2 — Contratos e modelos do domínio
**Objetivo:** definir as interfaces internas antes dos adaptadores.
**Contratos:**
- `ContratoDeAcessoAoEditor`;
- contrato de etapa do scanner;
- contrato de conversor, quando necessário.
**Entidades:**
- `Projeto`;
- `Sequencia`;
- `Timeline`;
- `Faixa`;
- `Clipe`.
**Objetos de valor:**
- `IntervaloDeTempo`;
- `TipoDeFaixa`;
- `TipoDeMidia`;
- identificadores internos e externos, se necessário.
**Invariantes mínimas:**
- intervalo não pode iniciar antes de zero;
- fim não pode ser anterior ao início;
- identificadores devem ser preservados;
- posição na timeline e posição na origem são distintas;
- clipe pode estar offline sem invalidar toda a timeline;
- nome não é identificador único.
**Entrega:** modelo interno independente de MCP e editor.
### Fase 3 — Cliente e sessão MCP
**Objetivo:** encapsular a comunicação técnica externa.
**Classes:**
- `ClienteMCP`;
- `SessaoMCP`;
- `ErroMCP`;
- `ErroDeConexaoMCP`;
- `ErroDeFerramentaMCP`;
- `ErroDeRespostaMCP`;
- `FerramentaMCPNaoEncontrada`.
**Responsabilidades:**
- conectar e desconectar;
- informar estado da conexão;
- chamar ferramentas;
- controlar timeout;
- retornar resposta bruta;
- traduzir falhas técnicas em erros específicos;
- registrar logs técnicos.
**Restrições:**
- não criar entidades de domínio;
- não conhecer timeline, clipe, cena ou retake;
- não decidir qual ferramenta usar para uma regra de negócio;
- não misturar leitura e escrita.
**Entrega:** comunicação técnica testável com cliente simulado.
### Fase 4 — Leitura estruturada do Premiere
**Objetivo:** oferecer operações de alto nível para o restante do sistema.
**Classes iniciais:**
- `AcessoAoEditor`;
- `AcessoATimeline`;
- `AcessoAClipes`.
**Operações iniciais:**
- obter timeline ativa;
- obter sequência ativa;
- obter faixas;
- obter clipes;
- obter detalhes de um clipe.
Essas classes poderão começar com uma fachada simples e ser divididas somente quando houver crescimento real de responsabilidade.
**Entrega:** leitura sem que o scanner conheça nomes de ferramentas MCP.
### Fase 5 — Conversão e validação
**Objetivo:** transformar respostas externas em objetos internos confiáveis.
**Classes:**
- `ConversorDeTimeline`;
- `ConversorDeFaixas`;
- `ConversorDeClipes`.
**Atividades:**
- normalizar nomes de campos;
- converter tempos e identificadores;
- aplicar valores padrão seguros;
- validar campos obrigatórios;
- preservar campos externos relevantes;
- tratar tipos desconhecidos;
- diferenciar resposta incompleta de timeline vazia;
- gerar erros de validação estruturados.
**Entrega:** timeline de domínio construída a partir de fixtures externas.
### Fase 6 — Núcleo do scanner
**Objetivo:** executar a descoberta por meio do fluxo arquitetural definido.
**Classes:**
- `ContextoDeAnalise`;
- `Analisador`;
- `ResultadoDaAnalise`;
- `StatusDaAnalise`;
- `ErroDeAnalise`;
- `DescobertaDaTimeline`;
- `PipelineDoScanner`;
- `AnalisadorDeTimeline`.
**Fluxo:**
```text
AnalisadorDeTimeline
↓
ContextoDeAnalise
↓
PipelineDoScanner
↓
DescobertaDaTimeline
↓
ContratoDeAcessoAoEditor
↓
Timeline do domínio
↓
Contexto atualizado
```
Na primeira versão, o pipeline poderá conter somente `DescobertaDaTimeline`.
**Entrega:** scanner capaz de retornar o contexto com a timeline lida.
### Fase 7 — Testes e integração simulada
**Objetivo:** garantir comportamento sem depender do Premiere real.
**Implementar:**
- `AcessoAoEditorSimulado`;
- fixtures de projeto, sequência, faixas e clipes;
- testes do cliente MCP;
- testes dos acessos de leitura;
- testes dos conversores;
- testes das entidades e objetos de valor;
- testes do pipeline;
- testes da descoberta;
- testes de integração simulada.
**Cenários obrigatórios:**
- timeline válida;
- timeline vazia;
- nenhuma sequência ativa;
- sequência sem clipes;
- mídia offline;
- resposta incompleta;
- intervalo inválido;
- ferramenta inexistente;
- timeout;
- MCP indisponível;
- campos adicionais desconhecidos.
**Entrega:** suíte automatizada reproduzível.
### Fase 8 — Validação com Premiere real
**Objetivo:** confirmar o fluxo contra o ambiente real.
**Atividades:**
- conectar ao MCP real;
- listar ferramentas e comparar com o inventário;
- ler projeto e sequência reais;
- comparar resposta real com fixtures;
- validar faixas e clipes;
- testar projeto vazio;
- testar mídia offline;
- verificar logs sem dados sensíveis;
- confirmar que nenhuma ferramenta de escrita foi chamada.
**Entrega:** relatório de validação da leitura real.
## Critérios de conclusão
A primeira etapa estará concluída quando:
- o scanner depender apenas de contratos internos;
- o MCP estiver isolado no módulo de integração;
- a timeline ativa puder ser lida do Premiere;
- faixas e clipes forem convertidos para o domínio;
- intervalos da timeline e da origem forem preservados separadamente;
- mídia offline for representada sem interromper toda a leitura;
- timeline vazia for tratada como resultado válido quando apropriado;
- erros técnicos e de validação forem distinguíveis;
- o fluxo puder ser executado com integração simulada;
- os testes automatizados estiverem passando;
- os logs forem úteis e seguros;
- nenhuma alteração for feita na timeline;
- todos os identificadores internos respeitarem PT-BR;
- a implementação estiver aderente à skill de boas práticas OO.
## Ordem resumida
```text
Arquitetura
↓
API real do MCP
↓
Contratos e domínio
↓
ClienteMCP
↓
AcessoAoEditor
↓
Conversores
↓
Contexto e pipeline
↓
DescobertaDaTimeline
↓
Testes simulados
↓
Validação com Premiere real
```
## Regra final
O MCP é o mecanismo de comunicação com o editor. O scanner é o módulo que organiza a descoberta. O domínio representa os dados. Os conversores isolam formatos externos. Nenhuma dessas responsabilidades deve ser concentrada em uma única classe.
@@ -0,0 +1,264 @@
# Planos das Próximas Etapas
## Visão geral
O desenvolvimento será incremental. Cada etapa deverá produzir uma capacidade funcional verificável, com testes e integração controlada. Nenhuma etapa deverá antecipar responsabilidades de etapas posteriores.
## Etapa 1 — Concluir a leitura da timeline
### Objetivo
Produzir uma representação interna completa e confiável da sequência ativa do Premiere.
### Implementar
- `SessaoMCP`;
- `AcessoASequencia`;
- `AcessoAFaixas`;
- `AcessoAClipes`;
- `ConversorDeFaixas`;
- `ConversorDeClipes`;
- `ConversorDeRespostas`;
- `ResultadoDaAnalise`;
- `StatusDaAnalise`;
- `ErroDeAnalise`;
- validação estruturada;
- fixtures de respostas reais;
- persistência inicial do resultado.
### Resultado esperado
```text
Premiere → MCP → Domínio → ContextoDeAnalise
```
Sem cortes, alterações ou decisões editoriais.
## Etapa 2 — Descoberta de arquivos e metadados
### Objetivo
Relacionar cada clipe ao arquivo de origem e obter suas características técnicas.
### Implementar
- `DescobertaDeArquivos`;
- `ExtracaoDeMetadados`;
- adaptador para leitura de arquivos;
- identificação de mídia offline;
- codecs, resolução, duração e taxa de quadros;
- validação de arquivos inacessíveis;
- cache de metadados;
- testes com arquivos reais e simulados.
### Regra
O módulo não deverá avaliar qualidade artística nem decidir se um clipe deve ser usado.
## Etapa 3 — Extração e análise de áudio
### Objetivo
Preparar o áudio e gerar informações temporais sobre o sinal sonoro.
### Implementar
- `ExtracaoDeAudio`;
- `AnaliseDeAudio`;
- contratos de provider de áudio;
- adaptador para FFmpeg ou ferramenta definida;
- silêncio, pausas, volume, clipping e ruído;
- marcadores temporais;
- arquivos intermediários e limpeza segura;
- testes sem dependência do Premiere.
### Regra
Esta etapa não transcreve e não decide cortes.
## Etapa 4 — Transcrição
### Objetivo
Produzir texto sincronizado com o áudio dos clipes.
### Implementar
- `TranscricaoDeAudio`;
- `ProviderDeTranscricao`;
- primeiro adaptador de transcrição;
- segmentos, palavras e confiança;
- associação entre transcrição e clipe;
- cache e retomada;
- tratamento de falha parcial;
- testes com provider simulado.
### Regra
O scanner armazenará a transcrição, mas não decidirá cortes com base nela.
## Etapa 5 — Extração e análise visual
### Objetivo
Obter quadros representativos e características visuais dos clipes.
### Implementar
- `ExtracaoDeQuadros`;
- `AnaliseVisual`;
- `ProviderDeAnaliseVisual`;
- seleção temporal de quadros;
- cache de imagens;
- foco, exposição, estabilidade, pessoas e composição;
- confiança e referências temporais;
- testes com provider simulado.
## Etapa 6 — Cenas e eventos
### Objetivo
Consolidar sinais de vídeo, áudio e texto em cenas e eventos temporais.
### Implementar
- `DeteccaoDeCenas`;
- `DeteccaoDeEventos`;
- contratos de providers;
- combinação de resultados;
- deduplicação;
- confiança;
- eventos de fala, silêncio, pausas e mudanças visuais;
- testes de consolidação.
### Regra
Evento detectado é informação. Não é decisão de edição.
## Etapa 7 — Detecção de retakes
### Objetivo
Identificar possíveis tomadas repetidas ou alternativas.
### Implementar
- `DeteccaoDeRetakes`;
- comparação de transcrições;
- comparação visual;
- comparação de áudio;
- agrupamento de tomadas semelhantes;
- nível de similaridade;
- vínculos entre clipes e grupos de retake;
- testes com casos positivos e negativos.
## Etapa 8 — Persistência e reprocessamento
### Objetivo
Permitir salvar, consultar e retomar análises.
### Implementar
- contrato de repositório da análise;
- armazenamento da versão do scanner;
- armazenamento da configuração utilizada;
- revisão da timeline;
- cache por etapa;
- retomada após falha;
- invalidação seletiva;
- persistência de erros e avisos.
## Etapa 9 — Motor de decisão
### Objetivo
Interpretar os resultados do scanner e escolher ações editoriais.
### Implementar somente após o scanner
- `MotorDeDecisao`;
- regras editoriais;
- critérios de seleção;
- explicação das decisões;
- conflitos entre regras;
- nível de confiança;
- saída estruturada.
### Regra
O motor de decisão não deverá consultar diretamente o MCP. Ele trabalhará apenas com o resultado persistido do scanner.
## Etapa 10 — Plano e aplicação de edição
### Objetivo
Transformar decisões em operações verificáveis e aplicá-las com segurança.
### Implementar
- `GeradorDePlanoDeEdicao`;
- modelo de operação;
- validação do plano;
- `AplicadorDeOperacoes`;
- executor de comandos MCP;
- confirmação após cada operação;
- rollback ou estratégia de recuperação;
- modo de simulação;
- auditoria completa.
### Regra
Nenhuma escrita deverá ser liberada sem validação explícita do plano e confirmação do estado do Premiere.
## Ordem de prioridade
```text
Leitura completa da timeline
↓
Arquivos e metadados
↓
Áudio
↓
Transcrição
↓
Análise visual
↓
Cenas e eventos
↓
Retakes
↓
Persistência e retomada
↓
Motor de decisão
↓
Plano de edição
↓
Aplicação no Premiere
```
## Critério para avançar de etapa
Só avançaremos quando a etapa atual tiver:
- responsabilidade documentada;
- contratos definidos;
- implementação isolada;
- testes automatizados;
- integração simulada;
- tratamento de erros;
- logs adequados;
- validação contra o ambiente real, quando aplicável;
- nenhum acoplamento indevido com etapas futuras.
## Próximo plano imediato
O próximo incremento será a conclusão da leitura da timeline, com foco em:
1. separar os modelos de domínio em módulos próprios;
2. criar `ConversorDeFaixas`;
3. criar `ConversorDeClipes`;
4. adicionar validação estruturada;
5. salvar fixtures reais do MCP;
6. ampliar os testes;
7. executar novamente a leitura contra o Premiere.
+643
View File
@@ -0,0 +1,643 @@
Para começar, eu não criaria todas as classes de análise imediatamente. O ideal é definir primeiro as **classes básicas e estruturais do módulo `scanner`**. Elas formarão a base sobre a qual as análises específicas serão construídas.
Eu dividiria em quatro grupos:
1. Coordenação do processo.
2. Representação dos dados.
3. Contratos das análises.
4. Controle de execução e resultados.
# Estrutura básica inicial
```text
scanner/
├── __init__.py
│
├── coordenacao/
│ ├── __init__.py
│ ├── analisador_de_timeline.py
│ ├── pipeline_do_scanner.py
│ └── contexto_de_analise.py
│
├── contratos/
│ ├── __init__.py
│ └── analisador.py
│
├── modelos/
│ ├── __init__.py
│ ├── resultado_da_analise.py
│ ├── status_da_analise.py
│ └── erro_de_analise.py
│
└── configuracao/
├── __init__.py
└── configuracao_do_scanner.py
```
A seguir está o papel detalhado de cada classe.
---
# 1. `AnalisadorDeTimeline`
Arquivo:
```text
scanner/coordenacao/analisador_de_timeline.py
```
## Papel principal
É a **classe de entrada do scanner**.
Ela representa o processo completo de análise de uma timeline. O restante do sistema deverá utilizar essa classe para iniciar uma análise, sem precisar conhecer todas as etapas internas.
## O que deve fazer
* Receber a timeline que será analisada.
* Receber as configurações do scanner.
* Receber ou montar o pipeline.
* Criar o contexto inicial.
* Iniciar a execução do pipeline.
* Acompanhar o status geral da análise.
* Retornar o resultado final.
* Permitir informar progresso.
* Permitir cancelamento futuro.
* Tratar falhas gerais do processo.
* Registrar informações gerais da execução.
* Identificar se a análise já foi realizada anteriormente.
* Permitir retomar uma análise interrompida, quando essa funcionalidade existir.
## O que não deve fazer
* Extrair áudio diretamente.
* Executar transcrição.
* Detectar cenas.
* Analisar imagens.
* Detectar retakes.
* Implementar chamadas para OpenCV, FFmpeg, Whisper ou APIs.
* Decidir quais trechos serão cortados.
* Alterar a timeline.
## Exemplo conceitual
```python
class AnalisadorDeTimeline:
"""Ponto de entrada para a análise completa de uma timeline."""
def __init__(self, pipeline, configuracao):
self.pipeline = pipeline
self.configuracao = configuracao
def analisar(self, timeline):
"""Inicia a análise da timeline e retorna o resultado."""
contexto = ContextoDeAnalise(
timeline=timeline,
configuracao=self.configuracao
)
return self.pipeline.executar(contexto)
```
---
# 2. `PipelineDoScanner`
Arquivo:
```text
scanner/coordenacao/pipeline_do_scanner.py
```
## Papel principal
É a classe responsável por **controlar a ordem de execução das análises**.
Ela não sabe como cada análise funciona. Apenas sabe quais análises precisam ser executadas e em qual sequência.
## O que deve fazer
* Receber uma lista de análises.
* Executar as análises na ordem configurada.
* Entregar o contexto para cada análise.
* Atualizar o status da execução.
* Registrar a análise atualmente em execução.
* Registrar análises concluídas.
* Identificar falhas.
* Permitir interromper o processo.
* Permitir executar somente determinadas análises.
* Permitir ignorar análises opcionais.
* Permitir futuramente executar análises independentes em paralelo.
* Garantir que uma análise só seja executada quando suas dependências estiverem disponíveis.
* Retornar o contexto atualizado.
## Exemplo
```python
class PipelineDoScanner:
"""Executa as análises do scanner em uma ordem definida."""
def __init__(self, analises):
self.analises = analises
def executar(self, contexto):
"""Executa todas as análises configuradas."""
for analise in self.analises:
contexto = analise.executar(contexto)
return contexto
```
## O que não deve fazer
* Conhecer detalhes dos providers.
* Saber como o áudio é extraído.
* Saber como o Whisper funciona.
* Implementar algoritmos de detecção de cenas.
* Tomar decisões de edição.
* Manipular diretamente a timeline.
---
# 3. `ContextoDeAnalise`
Arquivo:
```text
scanner/coordenacao/contexto_de_analise.py
```
## Papel principal
É o objeto que **transporta os dados entre as análises**.
Cada análise recebe o mesmo contexto, lê os dados de que precisa e adiciona seus próprios resultados.
Ele evita que cada classe precise receber dezenas de parâmetros separados.
## O que deve armazenar
### Dados da origem
* Timeline analisada.
* Identificador da análise.
* Data e hora de início.
* Configuração utilizada.
* Versão do scanner.
* Identificador do projeto ou sequência.
### Dados descobertos
* Sequências.
* Faixas de vídeo.
* Faixas de áudio.
* Clipes.
* Arquivos relacionados.
* Relações entre áudio e vídeo.
* Elementos desativados.
* Elementos offline.
### Dados técnicos
* Metadados dos arquivos.
* Duração.
* Resolução.
* Taxa de quadros.
* Codecs.
* Timecodes.
* Informações de áudio.
### Dados processados
* Áudios extraídos.
* Quadros extraídos.
* Transcrições.
* Características visuais.
* Características de áudio.
* Cenas detectadas.
* Eventos detectados.
* Possíveis retakes.
### Controle da execução
* Status atual.
* Análise em execução.
* Análises concluídas.
* Erros.
* Avisos.
* Percentual de progresso.
* Tempo de execução.
## O que não deve fazer
* Executar análises.
* Chamar providers.
* Implementar algoritmos.
* Decidir cortes.
* Alterar a timeline.
* Fazer persistência diretamente.
## Exemplo
```python
class ContextoDeAnalise:
"""Armazena os dados compartilhados durante a análise."""
def __init__(self, timeline, configuracao):
self.timeline = timeline
self.configuracao = configuracao
self.clipes = []
self.arquivos = []
self.metadados = {}
self.audios = {}
self.quadros = {}
self.transcricoes = {}
self.cenas = []
self.eventos = []
self.retakes = []
self.status = "nao_iniciada"
self.analise_atual = None
self.analises_concluidas = []
self.erros = []
self.avisos = []
```
---
# 4. `Analisador`
Arquivo:
```text
scanner/contratos/analisador.py
```
## Papel principal
É o **contrato comum das classes de análise**.
Ele define que toda análise precisa possuir um método padronizado, como `executar()`.
Não é uma análise concreta. É uma classe-base ou interface.
## O que deve definir
* Método `executar(contexto)`.
* Identificação da análise.
* Nome amigável.
* Dependências, quando necessário.
* Indicação se a análise é obrigatória ou opcional.
* Validação básica do contexto.
* Possibilidade de informar progresso.
* Possibilidade de verificar cancelamento.
## Exemplo
```python
from abc import ABC, abstractmethod
class Analisador(ABC):
"""Define o contrato comum das análises do scanner."""
nome = "analisador"
@abstractmethod
def executar(self, contexto):
"""Executa a análise sobre o contexto."""
raise NotImplementedError
def validar_contexto(self, contexto):
"""Valida se o contexto possui os dados necessários."""
return True
```
As classes específicas poderão seguir esse contrato:
```python
class DeteccaoDeCenas(Analisador):
"""Detecta cenas utilizando um provider especializado."""
nome = "deteccao_de_cenas"
def executar(self, contexto):
"""Executa a detecção de cenas."""
return contexto
```
---
# 5. `ResultadoDaAnalise`
Arquivo:
```text
scanner/modelos/resultado_da_analise.py
```
## Papel principal
Representa o **resultado produzido por uma análise individual**.
Ele será útil para que o pipeline saiba se uma análise terminou corretamente, quais dados produziu e se houve problemas.
## O que deve armazenar
* Nome da análise.
* Status.
* Data e hora de início.
* Data e hora de término.
* Duração.
* Quantidade de itens processados.
* Quantidade de resultados produzidos.
* Avisos.
* Erros.
* Dados resumidos.
* Identificador da execução.
## Exemplo
```python
class ResultadoDaAnalise:
"""Representa o resultado de uma análise individual."""
def __init__(self, nome, status="concluida"):
self.nome = nome
self.status = status
self.inicio = None
self.fim = None
self.duracao = None
self.itens_processados = 0
self.resultados_produzidos = 0
self.avisos = []
self.erros = []
self.dados = {}
```
## Exemplo de resultado
```json
{
"nome": "deteccao_de_cenas",
"status": "concluida",
"itens_processados": 48,
"resultados_produzidos": 17,
"duracao": 12.4,
"avisos": [],
"erros": []
}
```
---
# 6. `StatusDaAnalise`
Arquivo:
```text
scanner/modelos/status_da_analise.py
```
## Papel principal
Centraliza os estados possíveis de uma análise.
Isso evita que cada classe utilize textos diferentes para representar o mesmo estado.
## Estados possíveis
```text
NAO_INICIADA
AGUARDANDO
EM_EXECUCAO
CONCLUIDA
CONCLUIDA_COM_AVISOS
FALHOU
CANCELADA
IGNORADA
```
## Exemplo
```python
from enum import Enum
class StatusDaAnalise(Enum):
"""Define os estados possíveis de uma análise."""
NAO_INICIADA = "nao_iniciada"
AGUARDANDO = "aguardando"
EM_EXECUCAO = "em_execucao"
CONCLUIDA = "concluida"
CONCLUIDA_COM_AVISOS = "concluida_com_avisos"
FALHOU = "falhou"
CANCELADA = "cancelada"
IGNORADA = "ignorada"
```
---
# 7. `ErroDeAnalise`
Arquivo:
```text
scanner/modelos/erro_de_analise.py
```
## Papel principal
Representa erros ocorridos durante o processo de análise de maneira estruturada.
Em vez de armazenar apenas uma mensagem solta, o sistema poderá saber exatamente onde e por que o erro aconteceu.
## O que deve armazenar
* Nome da análise.
* Código do erro.
* Mensagem.
* Detalhes técnicos.
* Arquivo relacionado.
* Clipe relacionado.
* Provider envolvido.
* Data e hora.
* Indicação se o erro interrompe o pipeline.
* Sugestão de recuperação, quando possível.
## Exemplo
```python
class ErroDeAnalise:
"""Representa um erro ocorrido durante uma análise."""
def __init__(
self,
mensagem,
codigo=None,
nome_da_analise=None,
arquivo=None,
interrompe_pipeline=False
):
self.mensagem = mensagem
self.codigo = codigo
self.nome_da_analise = nome_da_analise
self.arquivo = arquivo
self.interrompe_pipeline = interrompe_pipeline
```
---
# 8. `ConfiguracaoDoScanner`
Arquivo:
```text
scanner/configuracao/configuracao_do_scanner.py
```
## Papel principal
Armazena as configurações que controlam como o scanner deverá funcionar.
Ela não deve conter regras específicas de um provider. As configurações dos providers podem ficar em seus próprios módulos.
## O que deve controlar
* Quais análises serão executadas.
* Ordem das análises.
* Análises obrigatórias.
* Análises opcionais.
* Uso de cache.
* Uso de arquivos temporários.
* Diretório de trabalho.
* Nível de detalhamento.
* Quantidade de quadros extraídos.
* Intervalo de amostragem.
* Limite de duração.
* Execução paralela.
* Comportamento diante de erros.
* Persistência automática.
* Retomada de análise.
* Nível de logging.
## Exemplo
```python
class ConfiguracaoDoScanner:
"""Define as configurações gerais do scanner."""
def __init__(self):
self.executar_transcricao = True
self.executar_analise_visual = True
self.executar_deteccao_de_cenas = True
self.executar_deteccao_de_retakes = True
self.usar_cache = True
self.continuar_em_caso_de_erro = True
self.salvar_resultados_automaticamente = True
```
---
# Como essas classes se relacionam
```text
AnalisadorDeTimeline
↓
PipelineDoScanner
↓
Analisador
↓
Classes específicas de análise
↓
ContextoDeAnalise
↓
ResultadoDaAnalise
```
A configuração controla o comportamento:
```text
ConfiguracaoDoScanner
↓
AnalisadorDeTimeline
↓
PipelineDoScanner
```
E os erros são registrados durante a execução:
```text
Classe de análise
↓
ErroDeAnalise
↓
ContextoDeAnalise
↓
ResultadoDaAnalise
```
# Fluxo de execução
```text
1. AnalisadorDeTimeline recebe a timeline.
2. Cria o ContextoDeAnalise.
3. Carrega a ConfiguracaoDoScanner.
4. Monta o PipelineDoScanner.
5. O pipeline executa cada Analisador.
6. Cada análise lê e atualiza o contexto.
7. Cada análise produz um ResultadoDaAnalise.
8. Erros e avisos são registrados.
9. O contexto final é retornado.
10. O resultado poderá ser salvo pelo módulo de persistência.
```
# Classes que eu criaria primeiro
Para a primeira implementação, começaria somente com estas:
```text
scanner/
├── coordenacao/
│ ├── analisador_de_timeline.py
│ ├── pipeline_do_scanner.py
│ └── contexto_de_analise.py
│
├── contratos/
│ └── analisador.py
│
└── modelos/
├── resultado_da_analise.py
└── status_da_analise.py
```
Depois acrescentaria:
```text
ErroDeAnalise
ConfiguracaoDoScanner
```
E somente então começaria a implementar as análises concretas:
```text
DescobertaDaTimeline
DescobertaDeClipes
DescobertaDeArquivos
ExtracaoDeMetadados
ExtracaoDeAudio
TranscricaoDeAudio
ExtracaoDeQuadros
AnaliseVisual
AnaliseDeAudio
DeteccaoDeCenas
DeteccaoDeEventos
DeteccaoDeRetakes
PersistenciaDaAnalise
```
A ideia central é:
> **As classes básicas não analisam o vídeo diretamente. Elas criam a estrutura que permite que todas as análises funcionem de forma organizada, substituível, testável e independente dos providers.**
+3
View File
@@ -0,0 +1,3 @@
from .entidades import Clipe, Faixa, IntervaloDeTempo, Projeto, Timeline
__all__ = ["Clipe", "Faixa", "IntervaloDeTempo", "Projeto", "Timeline"]
@@ -0,0 +1,3 @@
from .modelos import Clipe, Faixa, IntervaloDeTempo, Projeto, Timeline
__all__ = ["Clipe", "Faixa", "IntervaloDeTempo", "Projeto", "Timeline"]
+57
View File
@@ -0,0 +1,57 @@
from dataclasses import dataclass, field
from typing import Any
@dataclass(frozen=True)
class IntervaloDeTempo:
inicio: float
fim: float
def __post_init__(self) -> None:
if self.inicio < 0 or self.fim < self.inicio:
raise ValueError("Intervalo de tempo inválido.")
@property
def duracao(self) -> float:
return self.fim - self.inicio
@dataclass
class Clipe:
identificador: str
nome: str
intervalo_na_timeline: IntervaloDeTempo
intervalo_na_origem: IntervaloDeTempo | None = None
arquivo: str | None = None
identificador_da_faixa: str | None = None
offline: bool = False
metadados: dict[str, Any] = field(default_factory=dict)
@dataclass
class Faixa:
identificador: str
nome: str
tipo: str
indice: int
clipes: list[Clipe] = field(default_factory=list)
@dataclass
class Timeline:
identificador: str
nome: str
duracao: float | None = None
taxa_de_quadros: float | None = None
largura: int | None = None
altura: int | None = None
faixas: list[Faixa] = field(default_factory=list)
metadados: dict[str, Any] = field(default_factory=dict)
@dataclass
class Projeto:
identificador: str
nome: str
caminho: str | None = None
sequencias: list[Any] = field(default_factory=list)
+27
View File
@@ -0,0 +1,27 @@
from pathlib import Path
from engine.integracoes.midia import ExtracaoDeAudio
from engine.integracoes.premiere.cliente_mcp import ClienteMCPPorStdio
from engine.integracoes.premiere.conversores import ConversorDeTimeline
from engine.integracoes.apple_speech import ProviderDeTranscricaoApple
from engine.scanner.transcricao_da_timeline import TranscricaoDaTimeline
ARQUIVO = Path('/Volumes/Merongo/PROJETOS/03 - Mastopexia/0E6A8290.MP4')
SAIDA = Path('/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/relatorios')
cliente = ClienteMCPPorStdio(['node', 'dist/index.js'])
cliente.conectar()
bruta = cliente.chamar('get_active_sequence', {})['structuredContent']['data']
timeline = ConversorDeTimeline().converter(bruta)
for faixa in timeline.faixas:
for clipe in faixa.clipes:
if clipe.nome == '0E6A8290.MP4': clipe.arquivo = str(ARQUIVO)
provider = ProviderDeTranscricaoApple('/private/tmp/apple-speech-transcriber', 'pt-BR')
resultados = TranscricaoDaTimeline(provider, ExtracaoDeAudio(duracao_da_parte=600), SAIDA/'audio-apple').executar(timeline)
linhas = [f'# Relatório de transcrição — {timeline.nome}', '', 'Provider: Apple Speech (macOS)',
f'Duração: {bruta["end"]:.3f} s', '']
for resultado in resultados:
linhas += [f'## {resultado.identificador_do_clipe} — {resultado.inicio_na_timeline:.3f}s–{resultado.fim_na_timeline:.3f}s',
f'Origem: {resultado.arquivo}', '', resultado.texto or '_Sem fala detectada._', '']
(SAIDA/'transcricao-timeline-apple.md').write_text('\n'.join(linhas), encoding='utf-8')
print(SAIDA/'transcricao-timeline-apple.md')
+29
View File
@@ -0,0 +1,29 @@
import json
from pathlib import Path
from engine.integracoes.midia import ExtracaoDeAudio
from engine.integracoes.premiere.cliente_mcp import ClienteMCPPorStdio
from engine.integracoes.premiere.conversores import ConversorDeTimeline
from engine.integracoes.whisper import ProviderDeTranscricaoLocal
from engine.scanner.transcricao_da_timeline import TranscricaoDaTimeline
ARQUIVO = Path('/Volumes/Merongo/PROJETOS/03 - Mastopexia/0E6A8290.MP4')
MODELO = Path('/Volumes/Merongo/Applications/Trasncritor/small')
SAIDA = Path('/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/relatorios')
cliente = ClienteMCPPorStdio(['node', 'dist/index.js'])
cliente.conectar()
bruta = cliente.chamar('get_active_sequence', {})['structuredContent']['data']
timeline = ConversorDeTimeline().converter(bruta)
for faixa in timeline.faixas:
for clipe in faixa.clipes:
if clipe.nome == '0E6A8290.MP4': clipe.arquivo = str(ARQUIVO)
provider = ProviderDeTranscricaoLocal(str(MODELO))
resultados = TranscricaoDaTimeline(provider, ExtracaoDeAudio(duracao_da_parte=600), SAIDA/'audio-local').executar(timeline)
linhas = [f'# Relatório de transcrição — {timeline.nome}', '', 'Provider: faster-whisper local (modelo small)',
f'Duração: {bruta["end"]:.3f} s', '']
for resultado in resultados:
linhas += [f'## {resultado.identificador_do_clipe} — {resultado.inicio_na_timeline:.3f}s–{resultado.fim_na_timeline:.3f}s',
f'Origem: {resultado.arquivo}', '', resultado.texto or '_Sem fala detectada._', '']
(SAIDA/'transcricao-timeline-local.md').write_text('\n'.join(linhas), encoding='utf-8')
print(SAIDA/'transcricao-timeline-local.md')
+36
View File
@@ -0,0 +1,36 @@
import json
from pathlib import Path
from engine.integracoes.midia import ExtracaoDeAudio
from engine.integracoes.groq import ProviderDeTranscricaoGroq
from engine.integracoes.premiere.cliente_mcp import ClienteMCPPorStdio
from engine.integracoes.premiere.conversores import ConversorDeTimeline
from engine.scanner.transcricao_da_timeline import TranscricaoDaTimeline
ARQUIVO = Path('/Volumes/Merongo/PROJETOS/03 - Mastopexia/0E6A8290.MP4')
CHAVE = json.loads((Path.home()/'.premiere-mcp/config.json').read_text()).get('groqApiKey','')
SAIDA = Path('/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/relatorios')
cliente = ClienteMCPPorStdio(['node', 'dist/index.js'])
cliente.conectar()
bruta = cliente.chamar('get_active_sequence', {})['structuredContent']['data']
timeline = ConversorDeTimeline().converter(bruta)
for faixa in timeline.faixas:
for clipe in faixa.clipes:
if clipe.nome == '0E6A8290.MP4': clipe.arquivo = str(ARQUIVO)
provider = ProviderDeTranscricaoGroq(CHAVE)
transcricoes = TranscricaoDaTimeline(provider, ExtracaoDeAudio(duracao_da_parte=600), SAIDA/'audio').executar(timeline)
linhas = [f'# Relatório de transcrição — {timeline.nome}', '', f'Duração: {bruta["end"]:.3f} s',
f'Clipes processados: {sum(len(f.clipes) for f in timeline.faixas if f.tipo == "video")}', '']
for faixa in timeline.faixas:
if faixa.tipo != 'video': continue
for clipe in faixa.clipes:
if clipe.nome != '0E6A8290.MP4': continue
resultado = next((r for r in transcricoes if r.identificador_do_clipe == clipe.identificador), None)
intervalo = clipe.intervalo_na_origem
texto = resultado.texto if resultado else ''
linhas += [f'## {clipe.identificador} — {clipe.intervalo_na_timeline.inicio:.3f}s–{clipe.intervalo_na_timeline.fim:.3f}s',
f'Origem: {intervalo.inicio:.3f}s–{intervalo.fim:.3f}s', '', texto or '_Sem fala detectada._', '']
(SAIDA/'transcricao-timeline.md').write_text('\n'.join(linhas), encoding='utf-8')
print(SAIDA/'transcricao-timeline.md')
+1
View File
@@ -0,0 +1 @@
"""Integrações externas da engine."""
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>apple-speech-transcriber</string>
<key>CFBundleIdentifier</key>
<string>com.genialsistemas.apple-speech-transcriber</string>
<key>CFBundleName</key>
<string>Apple Speech Transcriber</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>O scanner precisa reconhecer a fala dos clipes da timeline para gerar a transcrição.</string>
</dict>
</plist>
@@ -0,0 +1,3 @@
from .provider_de_transcricao_apple import ProviderDeTranscricaoApple
__all__ = ["ProviderDeTranscricaoApple"]
@@ -0,0 +1,44 @@
import AppKit
import Foundation
import Speech
final class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
guard CommandLine.arguments.count >= 3 else { finalizar("uso: arquivo locale [somente_no_dispositivo]", 2); return }
let url = URL(fileURLWithPath: CommandLine.arguments[1])
let locale = Locale(identifier: CommandLine.arguments[2])
let somenteNoDispositivo = CommandLine.arguments.count > 3 && CommandLine.arguments[3] == "true"
guard let recognizer = SFSpeechRecognizer(locale: locale), recognizer.isAvailable else {
finalizar("Apple Speech indisponível para o locale solicitado", 3); return
}
SFSpeechRecognizer.requestAuthorization { status in
guard status == .authorized else { self.finalizar("Permissão do Apple Speech não concedida", 4); return }
let request = SFSpeechURLRecognitionRequest(url: url)
request.shouldReportPartialResults = false
if somenteNoDispositivo && recognizer.supportsOnDeviceRecognition { request.requiresOnDeviceRecognition = true }
recognizer.recognitionTask(with: request) { result, error in
if let result = result, result.isFinal {
let segmentos = result.bestTranscription.segments.map {
["inicio": $0.timestamp, "fim": $0.timestamp + $0.duration,
"texto": $0.substring, "confianca": $0.confidence] as [String: Any]
}
let data = try! JSONSerialization.data(withJSONObject: ["segmentos": segmentos])
print(String(data: data, encoding: .utf8)!)
}
if error != nil || result?.isFinal == true { self.finalizar(nil, 0) }
}
}
}
private func finalizar(_ mensagem: String?, _ codigo: Int32) {
if let mensagem { fputs(mensagem + "\n", stderr) }
NSApplication.shared.terminate(nil)
exit(codigo)
}
}
let app = NSApplication.shared
let delegate = AppDelegate()
app.delegate = delegate
app.setActivationPolicy(.accessory)
app.run()
@@ -0,0 +1,28 @@
import json
from pathlib import Path
import subprocess
from typing import Any
from ...scanner.modelos import SegmentoDeTranscricao
class ProviderDeTranscricaoApple:
"""Provider nativo macOS Speech; o áudio não passa por API Groq."""
def __init__(self, executavel: str = "/private/tmp/AppleSpeechTranscriber.app/Contents/MacOS/apple-speech-transcriber", locale: str = "pt-BR",
somente_no_dispositivo: bool = False) -> None:
self.executavel = executavel
self.locale = locale
self.somente_no_dispositivo = somente_no_dispositivo
def transcrever(self, clipe: Any) -> list[SegmentoDeTranscricao]:
arquivo = Path(clipe.arquivo)
if not arquivo.is_file():
raise FileNotFoundError(f"Arquivo de áudio não encontrado: {arquivo}")
resultado = subprocess.run(
[self.executavel, str(arquivo), self.locale, str(self.somente_no_dispositivo).lower()],
check=True, capture_output=True, text=True,
)
dados = json.loads(resultado.stdout)
return [SegmentoDeTranscricao(float(s["inicio"]), float(s["fim"]), str(s["texto"]), s.get("confianca"))
for s in dados.get("segmentos", [])]
+3
View File
@@ -0,0 +1,3 @@
from .provider_de_transcricao_groq import ProviderDeTranscricaoGroq
__all__ = ["ProviderDeTranscricaoGroq"]
@@ -0,0 +1,66 @@
import json
import mimetypes
import ssl
import uuid
from pathlib import Path
from typing import Any, Callable
from urllib import request
from ...scanner.modelos import SegmentoDeTranscricao
class ProviderDeTranscricaoGroq:
"""Provider Groq compatível com o contrato de transcrição do scanner."""
endpoint = "https://api.groq.com/openai/v1/audio/transcriptions"
def __init__(self, api_key: str, modelo: str = "whisper-large-v3-turbo",
idioma: str = "pt", tempo_limite: float = 120.0,
requisicao: Callable[..., Any] | None = None) -> None:
if not api_key.strip():
raise ValueError("A chave da API Groq é obrigatória.")
self.api_key = api_key.strip()
self.modelo = modelo
self.idioma = idioma
self.tempo_limite = tempo_limite
self._requisicao = requisicao or request.urlopen
try:
import certifi
self._contexto_ssl = ssl.create_default_context(cafile=certifi.where())
except ImportError:
self._contexto_ssl = ssl.create_default_context()
def transcrever(self, clipe: Any) -> list[SegmentoDeTranscricao]:
arquivo = getattr(clipe, "arquivo", None)
if not arquivo:
raise ValueError("O clipe não possui arquivo de origem para transcrição.")
caminho = Path(arquivo)
if not caminho.is_file():
raise FileNotFoundError(f"Arquivo do clipe não encontrado: {caminho}")
corpo, tipo = self._multipart(caminho)
req = request.Request(self.endpoint, data=corpo, method="POST", headers={
"Authorization": f"Bearer {self.api_key}",
"Content-Type": tipo,
})
argumentos = {"timeout": self.tempo_limite}
if self._requisicao is request.urlopen:
argumentos["context"] = self._contexto_ssl
with self._requisicao(req, **argumentos) as resposta:
dados = json.loads(resposta.read().decode("utf-8"))
return [SegmentoDeTranscricao(float(item["start"]), float(item["end"]),
str(item.get("text", "")).strip(),
None)
for item in dados.get("segments", [])]
def _multipart(self, caminho: Path) -> tuple[bytes, str]:
limite = "----engine-groq-" + uuid.uuid4().hex
mime = mimetypes.guess_type(caminho.name)[0] or "application/octet-stream"
partes: list[bytes] = []
campos = {"model": self.modelo, "language": self.idioma,
"response_format": "verbose_json", "timestamp_granularities[]": "segment"}
for nome, valor in campos.items():
partes.append(f"--{limite}\r\nContent-Disposition: form-data; name=\"{nome}\"\r\n\r\n{valor}\r\n".encode())
partes.append(f"--{limite}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{caminho.name}\"\r\nContent-Type: {mime}\r\n\r\n".encode())
partes.append(caminho.read_bytes())
partes.append(f"\r\n--{limite}--\r\n".encode())
return b"".join(partes), f"multipart/form-data; boundary={limite}"
@@ -0,0 +1,3 @@
from .extracao_de_audio import ExtracaoDeAudio, ParteDeAudio
__all__ = ["ExtracaoDeAudio", "ParteDeAudio"]
@@ -0,0 +1,63 @@
from dataclasses import dataclass
from pathlib import Path
import subprocess
@dataclass(frozen=True)
class ParteDeAudio:
caminho: Path
inicio: float
fim: float
class ExtracaoDeAudio:
"""Prepara mídia local para providers de transcrição."""
def __init__(self, ffmpeg: str = "ffmpeg", ffprobe: str = "ffprobe",
duracao_da_parte: float = 600.0) -> None:
self.ffmpeg = ffmpeg
self.ffprobe = ffprobe
self.duracao_da_parte = duracao_da_parte
def duracao(self, arquivo: str | Path) -> float:
caminho = self._validar(arquivo)
resultado = subprocess.run(
[self.ffprobe, "-v", "error", "-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1", str(caminho)],
check=True, capture_output=True, text=True,
)
return float(resultado.stdout.strip())
def extrair(self, arquivo: str | Path, destino: str | Path) -> list[ParteDeAudio]:
return self.extrair_intervalo(arquivo, destino, 0.0, None)
def extrair_intervalo(self, arquivo: str | Path, destino: str | Path,
inicio: float = 0.0, fim: float | None = None) -> list[ParteDeAudio]:
origem = self._validar(arquivo)
pasta = Path(destino)
pasta.mkdir(parents=True, exist_ok=True)
duracao_total = self.duracao(origem)
inicio = max(0.0, inicio)
fim = min(fim if fim is not None else duracao_total, duracao_total)
total = fim
partes: list[ParteDeAudio] = []
inicio = 0.0
indice = 0
while inicio < total:
fim_da_parte = min(inicio + self.duracao_da_parte, total)
saida = pasta / f"parte-{indice:04d}.wav"
subprocess.run([
self.ffmpeg, "-y", "-ss", str(inicio), "-i", str(origem),
"-t", str(fim_da_parte - inicio), "-vn", "-ac", "1", "-ar", "16000",
"-c:a", "pcm_s16le", str(saida),
], check=True, capture_output=True, text=True)
partes.append(ParteDeAudio(saida, inicio, fim_da_parte))
inicio = fim_da_parte
indice += 1
return partes
def _validar(self, arquivo: str | Path) -> Path:
caminho = Path(arquivo)
if not caminho.is_file():
raise FileNotFoundError(f"Arquivo de mídia não encontrado: {caminho}")
return caminho
@@ -0,0 +1,7 @@
from .cliente_mcp import ClienteMCP, ClienteMCPPorStdio
from .erros_mcp import ErroMCP, ErroDeConexaoMCP, ErroDeRespostaMCP, ErroDeFerramentaMCP
__all__ = ["ClienteMCP", "ClienteMCPPorStdio", "ErroMCP", "ErroDeConexaoMCP", "ErroDeRespostaMCP", "ErroDeFerramentaMCP"]
from .sessao_mcp import SessaoMCP
__all__ = ["SessaoMCP"]
@@ -0,0 +1,71 @@
from abc import ABC, abstractmethod
import json
import subprocess
from typing import Any
class ClienteMCP(ABC):
"""Define a comunicação técnica com o servidor MCP."""
@abstractmethod
def conectar(self) -> None: ...
@abstractmethod
def desconectar(self) -> None: ...
@abstractmethod
def esta_conectado(self) -> bool: ...
@abstractmethod
def chamar(self, nome_da_ferramenta: str, argumentos: dict[str, Any] | None = None) -> dict[str, Any]: ...
class ClienteMCPPorStdio(ClienteMCP):
"""Cliente MCP para o servidor local do Premiere via stdio."""
def __init__(self, comando: list[str], tempo_limite: float = 30.0) -> None:
self.comando = comando
self.tempo_limite = tempo_limite
self._processo: subprocess.Popen[str] | None = None
self._proximo_id = 1
def conectar(self) -> None:
if self._processo is not None:
return
self._processo = subprocess.Popen(
self.comando,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
)
self._enviar("initialize", {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "engine", "version": "0.1.0"}})
def desconectar(self) -> None:
if self._processo is not None:
self._processo.terminate()
self._processo = None
def esta_conectado(self) -> bool:
return self._processo is not None and self._processo.poll() is None
def chamar(self, nome_da_ferramenta: str, argumentos: dict[str, Any] | None = None) -> dict[str, Any]:
if not self.esta_conectado():
raise RuntimeError("Cliente MCP não está conectado.")
resposta = self._enviar("tools/call", {"name": nome_da_ferramenta, "arguments": argumentos or {}})
if resposta.get("error"):
raise RuntimeError(f"Erro MCP: {resposta['error']}")
return resposta.get("result", {})
def _enviar(self, metodo: str, parametros: dict[str, Any]) -> dict[str, Any]:
if self._processo is None or self._processo.stdin is None or self._processo.stdout is None:
raise RuntimeError("Processo MCP indisponível.")
identificador = self._proximo_id
self._proximo_id += 1
self._processo.stdin.write(json.dumps({"jsonrpc": "2.0", "id": identificador, "method": metodo, "params": parametros}) + "\n")
self._processo.stdin.flush()
linha = self._processo.stdout.readline()
if not linha:
raise RuntimeError("O MCP encerrou sem retornar resposta.")
return json.loads(linha)
@@ -0,0 +1,5 @@
from .conversor_de_timeline import ConversorDeTimeline
from .conversor_de_faixas import ConversorDeFaixas
from .conversor_de_clipes import ConversorDeClipes
__all__ = ["ConversorDeTimeline", "ConversorDeFaixas", "ConversorDeClipes"]
@@ -0,0 +1,26 @@
from typing import Any
from ....dominio import Clipe, IntervaloDeTempo
from ....scanner.modelos import ErroDeAnalise
class ConversorDeClipes:
def converter(self, dados: dict[str, Any], identificador_da_faixa: str, indice: int = 0) -> Clipe:
inicio = dados.get("start", dados.get("timeline_start"))
fim = dados.get("end", dados.get("timeline_end"))
identificador = dados.get("nodeId", dados.get("id"))
if identificador is None or inicio is None or fim is None:
raise ErroDeAnalise("resposta_incompleta", "Clipe sem identificador ou intervalo obrigatório.",
f"clipes[{indice}]", {"campos": list(dados)})
try:
intervalo = IntervaloDeTempo(float(inicio), float(fim))
origem_inicio = dados.get("inPoint", dados.get("source_start"))
origem_fim = dados.get("outPoint", dados.get("source_end"))
origem = (IntervaloDeTempo(float(origem_inicio), float(origem_fim))
if origem_inicio is not None and origem_fim is not None else None)
except (TypeError, ValueError) as exc:
raise ErroDeAnalise("intervalo_invalido", str(exc), f"clipes[{indice}]") from exc
return Clipe(str(identificador), str(dados.get("name", "")), intervalo, origem,
dados.get("sourceFile"), identificador_da_faixa,
bool(dados.get("offline", False)), dict(dados.get("metadata", {})))
@@ -0,0 +1,17 @@
from typing import Any
from ....dominio import Faixa
from .conversor_de_clipes import ConversorDeClipes
class ConversorDeFaixas:
def __init__(self, conversor_de_clipes: ConversorDeClipes | None = None) -> None:
self.conversor_de_clipes = conversor_de_clipes or ConversorDeClipes()
def converter(self, dados: dict[str, Any], tipo: str, indice: int) -> Faixa:
identificador = str(dados.get("id", f"{tipo}_{indice}"))
faixa = Faixa(identificador, str(dados.get("name", "")), tipo,
int(dados.get("index", indice)))
faixa.clipes = [self.conversor_de_clipes.converter(clipe, identificador, i)
for i, clipe in enumerate(dados.get("clips", []))]
return faixa
@@ -0,0 +1,30 @@
from typing import Any
from ....dominio import Timeline
from .conversor_de_faixas import ConversorDeFaixas
class ConversorDeTimeline:
"""Converte a resposta externa do MCP em objetos do domínio."""
def __init__(self, conversor_de_faixas: ConversorDeFaixas | None = None) -> None:
self.conversor_de_faixas = conversor_de_faixas or ConversorDeFaixas()
def converter(self, dados_brutos: dict[str, Any]) -> Timeline:
if not isinstance(dados_brutos, dict) or dados_brutos.get("id") is None:
from ....scanner.modelos import ErroDeAnalise
raise ErroDeAnalise("timeline_invalida", "A timeline precisa de um identificador.", "timeline.id")
timeline = Timeline(
identificador=str(dados_brutos["id"]),
nome=str(dados_brutos.get("name", "")),
duracao=dados_brutos.get("duration"),
taxa_de_quadros=dados_brutos.get("frameRate"),
largura=dados_brutos.get("frameSizeHorizontal"),
altura=dados_brutos.get("frameSizeVertical"),
)
faixas_de_video = dados_brutos.get("videoTracks", dados_brutos.get("video_tracks", []))
faixas_de_audio = dados_brutos.get("audioTracks", dados_brutos.get("audio_tracks", []))
for tipo, faixas in (("video", faixas_de_video), ("audio", faixas_de_audio)):
timeline.faixas.extend(self.conversor_de_faixas.converter(f, tipo, i)
for i, f in enumerate(faixas))
return timeline
@@ -0,0 +1,14 @@
class ErroMCP(Exception):
"""Erro geral da integração com o MCP."""
class ErroDeConexaoMCP(ErroMCP):
"""Erro ao conectar ao MCP."""
class ErroDeFerramentaMCP(ErroMCP):
"""Erro ao executar uma ferramenta MCP."""
class ErroDeRespostaMCP(ErroMCP):
"""Erro quando a resposta do MCP é inválida."""
@@ -0,0 +1,3 @@
from .acesso_ao_editor import AcessoAClipes, AcessoAoEditor, AcessoAFaixas, AcessoASequencia, AcessoATimeline
__all__ = ["AcessoAoEditor", "AcessoATimeline", "AcessoASequencia", "AcessoAFaixas", "AcessoAClipes"]
@@ -0,0 +1,47 @@
from typing import Any
from ..cliente_mcp import ClienteMCP
class AcessoATimeline:
"""Realiza consultas técnicas da timeline por meio do MCP."""
def __init__(self, cliente_mcp: ClienteMCP, nome_da_ferramenta: str = "get_active_sequence") -> None:
self.cliente_mcp = cliente_mcp
self.nome_da_ferramenta = nome_da_ferramenta
def obter_timeline_ativa(self) -> dict[str, Any]:
resposta = self.cliente_mcp.chamar(self.nome_da_ferramenta, {})
conteudo_estruturado = resposta.get("structuredContent", {})
return conteudo_estruturado.get("data", resposta)
class AcessoASequencia:
"""Consulta somente os dados da sequência ativa."""
def __init__(self, acesso_a_timeline: AcessoATimeline) -> None:
self.acesso_a_timeline = acesso_a_timeline
def obter_sequencia_ativa(self) -> dict[str, Any]:
return self.acesso_a_timeline.obter_timeline_ativa()
class AcessoAFaixas:
def obter_faixas(self, sequencia: dict[str, Any]) -> list[dict[str, Any]]:
return list(sequencia.get("videoTracks", sequencia.get("video_tracks", []))) + list(
sequencia.get("audioTracks", sequencia.get("audio_tracks", [])))
class AcessoAClipes:
def obter_clipes(self, faixa: dict[str, Any]) -> list[dict[str, Any]]:
return list(faixa.get("clips", []))
class AcessoAoEditor:
"""Expõe operações de leitura compreensíveis pelo domínio."""
def __init__(self, acesso_a_timeline: AcessoATimeline) -> None:
self.acesso_a_timeline = acesso_a_timeline
def obter_timeline_ativa(self) -> dict[str, Any]:
return self.acesso_a_timeline.obter_timeline_ativa()
@@ -0,0 +1,17 @@
from contextlib import AbstractContextManager
from .cliente_mcp import ClienteMCP
class SessaoMCP(AbstractContextManager["SessaoMCP"]):
"""Garante o ciclo de vida de uma conexão MCP durante uma leitura."""
def __init__(self, cliente: ClienteMCP) -> None:
self.cliente = cliente
def __enter__(self) -> "SessaoMCP":
self.cliente.conectar()
return self
def __exit__(self, tipo, valor, traceback) -> None:
self.cliente.desconectar()
@@ -0,0 +1,3 @@
from .provider_de_transcricao_local import ProviderDeTranscricaoLocal
__all__ = ["ProviderDeTranscricaoLocal"]
@@ -0,0 +1,21 @@
from pathlib import Path
from typing import Any
from ...scanner.modelos import SegmentoDeTranscricao
class ProviderDeTranscricaoLocal:
"""Provider local usando faster-whisper, sem transmitir mídia."""
def __init__(self, modelo: str, dispositivo: str = "cpu", tipo_de_calculo: str = "int8",
idioma: str = "pt") -> None:
from faster_whisper import WhisperModel
self.modelo = WhisperModel(modelo, device=dispositivo, compute_type=tipo_de_calculo)
self.idioma = idioma
def transcrever(self, clipe: Any) -> list[SegmentoDeTranscricao]:
arquivo = Path(clipe.arquivo)
if not arquivo.is_file():
raise FileNotFoundError(f"Arquivo de áudio não encontrado: {arquivo}")
segmentos, _ = self.modelo.transcribe(str(arquivo), language=self.idioma, vad_filter=True)
return [SegmentoDeTranscricao(float(s.start), float(s.end), s.text.strip()) for s in segmentos]
+7
View File
@@ -0,0 +1,7 @@
from .coordenacao import AnalisadorDeTimeline, ContextoDeAnalise, PipelineDoScanner
__all__ = ["AnalisadorDeTimeline", "ContextoDeAnalise", "PipelineDoScanner"]
from .modelos import Cena, ErroDeAnalise, Evento, ResultadoDaAnalise, SegmentoDeTranscricao, StatusDaAnalise
from .transcricao_da_timeline import TranscricaoDaTimeline, TranscricaoDoClipe
__all__ = ["Cena", "ErroDeAnalise", "Evento", "ResultadoDaAnalise", "SegmentoDeTranscricao", "StatusDaAnalise", "TranscricaoDaTimeline", "TranscricaoDoClipe"]
+67
View File
@@ -0,0 +1,67 @@
from typing import Any, Protocol
from .modelos import Cena, SegmentoDeTranscricao
class ProviderDeTranscricao(Protocol):
def transcrever(self, clipe: Any) -> list[SegmentoDeTranscricao]: ...
class ProviderDeAnaliseVisual(Protocol):
def analisar(self, clipe: Any, quadros: list[Any] | None = None) -> dict[str, Any]: ...
class TranscricaoDeAudio:
nome = "transcricao_de_audio"
def __init__(self, provider: ProviderDeTranscricao) -> None:
self.provider = provider
def executar(self, contexto):
if contexto.timeline is None:
return contexto
for faixa in contexto.timeline.faixas:
for clipe in faixa.clipes:
contexto.transcricoes[clipe.identificador] = self.provider.transcrever(clipe)
return contexto
class AnaliseVisual:
nome = "analise_visual"
def __init__(self, provider: ProviderDeAnaliseVisual) -> None:
self.provider = provider
def executar(self, contexto):
if contexto.timeline is None:
return contexto
for faixa in contexto.timeline.faixas:
for clipe in faixa.clipes:
contexto.caracteristicas_visuais[clipe.identificador] = self.provider.analisar(clipe)
return contexto
class DeteccaoDeCenas:
nome = "deteccao_de_cenas"
def executar(self, contexto):
cenas: list[Cena] = []
for faixa in (contexto.timeline.faixas if contexto.timeline else []):
for clipe in faixa.clipes:
cenas.append(Cena(clipe.intervalo_na_timeline.inicio,
clipe.intervalo_na_timeline.fim,
contexto.caracteristicas_visuais.get(clipe.identificador, {}).get("confianca")))
contexto.cenas = self._consolidar(cenas)
return contexto
@staticmethod
def _consolidar(cenas: list[Cena]) -> list[Cena]:
resultado: list[Cena] = []
for cena in sorted(cenas, key=lambda item: item.inicio):
if resultado and cena.inicio <= resultado[-1].fim:
anterior = resultado[-1]
resultado[-1] = Cena(anterior.inicio, max(anterior.fim, cena.fim),
anterior.confianca or cena.confianca)
else:
resultado.append(cena)
return resultado
@@ -0,0 +1,46 @@
from dataclasses import dataclass, field
from typing import Any, Protocol
from ...dominio import Timeline
@dataclass
class ContextoDeAnalise:
timeline: Timeline | None = None
dados_da_timeline: dict[str, Any] | None = None
erros: list[str] = field(default_factory=list)
avisos: list[str] = field(default_factory=list)
analises_concluidas: list[str] = field(default_factory=list)
transcricoes: dict[str, list[Any]] = field(default_factory=dict)
caracteristicas_visuais: dict[str, dict[str, Any]] = field(default_factory=dict)
cenas: list[Any] = field(default_factory=list)
eventos: list[Any] = field(default_factory=list)
class Analisador(Protocol):
nome: str
def executar(self, contexto: ContextoDeAnalise) -> ContextoDeAnalise: ...
class PipelineDoScanner:
"""Executa análises em ordem, sem conhecer sua implementação."""
def __init__(self, analises: list[Analisador]) -> None:
self.analises = analises
def executar(self, contexto: ContextoDeAnalise) -> ContextoDeAnalise:
for analise in self.analises:
contexto = analise.executar(contexto)
contexto.analises_concluidas.append(analise.nome)
return contexto
class AnalisadorDeTimeline:
"""Ponto de entrada da análise da timeline."""
def __init__(self, pipeline: PipelineDoScanner) -> None:
self.pipeline = pipeline
def analisar(self) -> ContextoDeAnalise:
return self.pipeline.executar(ContextoDeAnalise())
@@ -0,0 +1,3 @@
from .descoberta_da_timeline import DescobertaDaTimeline
__all__ = ["DescobertaDaTimeline"]
@@ -0,0 +1,19 @@
from ...integracoes.premiere.conversores import ConversorDeTimeline
from ...integracoes.premiere.leitura import AcessoAoEditor
from ..coordenacao import ContextoDeAnalise
class DescobertaDaTimeline:
"""Descobre e converte a timeline ativa do editor."""
nome = "descoberta_da_timeline"
def __init__(self, acesso_ao_editor: AcessoAoEditor, conversor: ConversorDeTimeline) -> None:
self.acesso_ao_editor = acesso_ao_editor
self.conversor = conversor
def executar(self, contexto: ContextoDeAnalise) -> ContextoDeAnalise:
dados_brutos = self.acesso_ao_editor.obter_timeline_ativa()
contexto.dados_da_timeline = dados_brutos
contexto.timeline = self.conversor.converter(dados_brutos)
return contexto
+56
View File
@@ -0,0 +1,56 @@
from dataclasses import dataclass, field
from enum import Enum
from typing import Any
class StatusDaAnalise(str, Enum):
CONCLUIDA = "concluida"
CONCLUIDA_COM_AVISOS = "concluida_com_avisos"
FALHOU = "falhou"
@dataclass(frozen=True)
class ErroDeAnalise(Exception):
codigo: str
mensagem: str
caminho: str | None = None
detalhes: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
Exception.__init__(self, self.mensagem)
@dataclass
class ResultadoDaAnalise:
status: StatusDaAnalise
timeline: Any = None
erros: list[ErroDeAnalise] = field(default_factory=list)
avisos: list[str] = field(default_factory=list)
@dataclass(frozen=True)
class SegmentoDeTranscricao:
inicio: float
fim: float
texto: str
confianca: float | None = None
def __post_init__(self) -> None:
if self.inicio < 0 or self.fim < self.inicio:
raise ValueError("Intervalo de transcrição inválido.")
@dataclass(frozen=True)
class Cena:
inicio: float
fim: float
confianca: float | None = None
referencias: tuple[float, ...] = ()
@dataclass(frozen=True)
class Evento:
tipo: str
inicio: float
fim: float
confianca: float | None = None
@@ -0,0 +1,63 @@
from dataclasses import asdict, dataclass
import json
from pathlib import Path
from typing import Any, Callable
from ..integracoes.midia import ExtracaoDeAudio
from .modelos import SegmentoDeTranscricao
@dataclass(frozen=True)
class TranscricaoDoClipe:
identificador_do_clipe: str
arquivo: str
inicio_na_timeline: float
fim_na_timeline: float
segmentos: list[SegmentoDeTranscricao]
@property
def texto(self) -> str:
return " ".join(s.texto for s in self.segmentos if s.texto).strip()
class TranscricaoDaTimeline:
"""Transcreve somente os intervalos efetivamente usados na timeline."""
def __init__(self, provider: Any, extrator: ExtracaoDeAudio | None = None,
diretoria_de_trabalho: str | Path = ".transcricao") -> None:
self.provider = provider
self.extrator = extrator or ExtracaoDeAudio()
self.diretoria_de_trabalho = Path(diretoria_de_trabalho)
def executar(self, timeline: Any) -> list[TranscricaoDoClipe]:
resultados: list[TranscricaoDoClipe] = []
for faixa in timeline.faixas:
for clipe in faixa.clipes:
if not clipe.arquivo or clipe.offline:
continue
intervalo = clipe.intervalo_na_timeline
pasta = self.diretoria_de_trabalho / clipe.identificador
origem = clipe.intervalo_na_origem
partes = self.extrator.extrair_intervalo(
clipe.arquivo, pasta, origem.inicio if origem else 0.0,
origem.fim if origem else None,
)
segmentos: list[SegmentoDeTranscricao] = []
for parte in partes:
for segmento in self.provider.transcrever(type("Audio", (), {"arquivo": str(parte.caminho)})()):
base_origem = origem.inicio if origem else 0.0
inicio = max(intervalo.inicio, intervalo.inicio + (parte.inicio - base_origem) + segmento.inicio)
fim = min(intervalo.fim, intervalo.inicio + (parte.inicio - base_origem) + segmento.fim)
if inicio < intervalo.fim and fim > inicio:
segmentos.append(SegmentoDeTranscricao(inicio, fim, segmento.texto, segmento.confianca))
resultados.append(TranscricaoDoClipe(clipe.identificador, clipe.arquivo,
intervalo.inicio, intervalo.fim, segmentos))
return resultados
@staticmethod
def gerar_relatorio(resultados: list[TranscricaoDoClipe], destino: str | Path) -> Path:
caminho = Path(destino)
dados = [{**asdict(item), "segmentos": [asdict(s) for s in item.segmentos], "texto": item.texto}
for item in resultados]
caminho.write_text(json.dumps(dados, ensure_ascii=False, indent=2), encoding="utf-8")
return caminho
File diff suppressed because it is too large Load Diff
View File
@@ -0,0 +1,27 @@
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from engine.integracoes.midia import ExtracaoDeAudio
class TesteExtracaoDeAudio(unittest.TestCase):
def test_rejeita_arquivo_inexistente(self):
with self.assertRaises(FileNotFoundError):
ExtracaoDeAudio().extrair("/arquivo/inexistente.mp4", "/tmp/audio")
@patch("engine.integracoes.midia.extracao_de_audio.subprocess.run")
def test_prepara_audio_mono_16khz_e_divide_em_partes(self, executar):
executar.side_effect = [type("R", (), {"stdout": "12.0\n"})(), None, None]
with tempfile.TemporaryDirectory() as pasta:
video = Path(pasta) / "video.mp4"
video.write_bytes(b"video")
partes = ExtracaoDeAudio(duracao_da_parte=10).extrair(video, Path(pasta) / "audio")
self.assertEqual([(p.inicio, p.fim) for p in partes], [(0.0, 10.0), (10.0, 12.0)])
comando = executar.call_args_list[1].args[0]
self.assertIn("-ac", comando)
self.assertIn("1", comando)
self.assertIn("-ar", comando)
self.assertIn("16000", comando)
+76
View File
@@ -0,0 +1,76 @@
import unittest
from engine.dominio import IntervaloDeTempo
from engine.integracoes.premiere.conversores import ConversorDeTimeline
from engine.scanner import ErroDeAnalise
from engine.scanner.analise import AnaliseVisual, DeteccaoDeCenas, TranscricaoDeAudio
from engine.scanner.modelos import SegmentoDeTranscricao
from engine.scanner.coordenacao import AnalisadorDeTimeline, PipelineDoScanner
from engine.scanner.descoberta import DescobertaDaTimeline
class AcessoAoEditorSimulado:
nome = "acesso_ao_editor_simulado"
def obter_timeline_ativa(self):
return {"id": "seq-1", "name": "Principal", "duration": 10.0, "video_tracks": [{"id": "v1", "name": "V1", "clips": [{"id": "c1", "name": "Camera", "timeline_start": 0.0, "timeline_end": 10.0}]}], "audio_tracks": []}
class TestePrimeiraEtapa(unittest.TestCase):
def test_intervalo_rejeita_fim_anterior_ao_inicio(self):
with self.assertRaises(ValueError):
IntervaloDeTempo(2, 1)
def test_scanner_descobre_timeline_simulada(self):
etapa = DescobertaDaTimeline(AcessoAoEditorSimulado(), ConversorDeTimeline())
contexto = AnalisadorDeTimeline(PipelineDoScanner([etapa])).analisar()
self.assertEqual(contexto.timeline.nome, "Principal")
self.assertEqual(contexto.timeline.faixas[0].clipes[0].identificador, "c1")
self.assertEqual(contexto.analises_concluidas, ["descoberta_da_timeline"])
def test_converte_formato_real_da_timeline_do_premiere(self):
dados = {"id": "seq-1", "name": "Principal", "frameSizeHorizontal": 3840,
"frameSizeVertical": 2160, "end": 196.66,
"videoTracks": [{"index": 0, "name": "Vídeo 1", "clips": [
{"nodeId": "clip-1", "name": "camera.mp4", "start": 0,
"end": 196.66, "inPoint": 0, "outPoint": 196.66}]}],
"audioTracks": [{"index": 0, "name": "Áudio 1", "clips": []}]}
timeline = ConversorDeTimeline().converter(dados)
self.assertEqual((timeline.largura, timeline.altura), (3840, 2160))
self.assertEqual(timeline.faixas[0].clipes[0].identificador, "clip-1")
self.assertEqual(timeline.faixas[0].clipes[0].intervalo_na_origem.fim, 196.66)
def test_preserva_midia_offline_sem_interromper_a_timeline(self):
dados = {"id": "seq-1", "videoTracks": [{"clips": [
{"id": "c1", "name": "offline.mov", "start": 0, "end": 2, "offline": True}
]}]}
clipe = ConversorDeTimeline().converter(dados).faixas[0].clipes[0]
self.assertTrue(clipe.offline)
self.assertIsNone(clipe.intervalo_na_origem)
def test_resposta_incompleta_produz_erro_estruturado(self):
with self.assertRaises(ErroDeAnalise) as contexto:
ConversorDeTimeline().converter({"id": "seq-1", "videoTracks": [{"clips": [{"id": "c1"}]}]})
self.assertEqual(contexto.exception.codigo, "resposta_incompleta")
self.assertEqual(contexto.exception.caminho, "clipes[0]")
def test_transcreve_clipes_e_consolida_cenas(self):
class TranscritorSimulado:
def transcrever(self, clipe):
return [SegmentoDeTranscricao(0, clipe.intervalo_na_timeline.duracao, "Olá", 0.98)]
class VisaoSimulada:
def analisar(self, clipe, quadros=None):
return {"confianca": 0.91, "foco": 0.88}
etapa = DescobertaDaTimeline(AcessoAoEditorSimulado(), ConversorDeTimeline())
pipeline = PipelineDoScanner([etapa, TranscricaoDeAudio(TranscritorSimulado()),
AnaliseVisual(VisaoSimulada()), DeteccaoDeCenas()])
contexto = AnalisadorDeTimeline(pipeline).analisar()
self.assertEqual(contexto.transcricoes["c1"][0].texto, "Olá")
self.assertEqual(len(contexto.cenas), 1)
self.assertEqual(contexto.cenas[0].confianca, 0.91)
if __name__ == "__main__":
unittest.main()