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:
@@ -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.**
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Motor OO do sistema."""
|
||||
|
||||
from .scanner.coordenacao import AnalisadorDeTimeline, ContextoDeAnalise, PipelineDoScanner
|
||||
|
||||
__all__ = ["AnalisadorDeTimeline", "ContextoDeAnalise", "PipelineDoScanner"]
|
||||
@@ -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.
|
||||
@@ -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.**
|
||||
@@ -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"]
|
||||
@@ -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)
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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", [])]
|
||||
@@ -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]
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user