- Instalado essentia-tensorflow no venv whisperx-transcricao (usado pelo painel CEP), corrigindo erro "A classificação musical precisa do suporte TensorFlow do Essentia" no catálogo de trilhas. - Ligada a classificação de emoção da fala (ProviderDeEmocaoHuggingFace) ao scanner via nova flag `classificar_emocao`, e adicionado emoção/confiança como critério de desempate secundário no zoom da skill editar-por-voz. - Zoom do caminho Python (aplicar_plano_de_edicao.py) passou a usar adjustment layer numa faixa de vídeo própria, em vez de escalar o clipe inteiro via set_clip_properties — mesma técnica já usada pelo executor .mjs do painel CEP. - Adicionado campo "Carregar por ID" na aba Editar Vídeo do painel CEP (step 2), para carregar um plano do SQLite direto pelo plano_id quando ele não está associado a nenhum video_id conhecido (ex.: plano de um vídeo que este banco nunca escaneou). - Trocado o campo de ID numérico por uma lista suspensa (novo listar_planos_de_edicao.py + RepositorioDePlanos.listar_todos_os_planos) que mostra data/hora e a intenção registrada pela skill em cada plano salvo, para escolher sem decorar o id. Resumo: - 15 arquivos alterados - 2 novos - 13 modificados - 0 removidos 13 files changed, 451 insertions(+), 65 deletions(-) Arquivos: - .jhonny/analises.db - code/cep-plugin/index.html - code/cep-plugin/main.js - code/engine/editor/aplicador_de_plano_de_edicao.py - code/engine/editor/escrita/escrita_no_editor.py - code/engine/executar_scanner.py - code/engine/persistencia/repositorio_de_planos.py - code/engine/scanner/configuracao_visual.py - code/engine/testes/duplos_de_premiere.py - code/engine/testes/test_aplicador_de_plano_de_edicao.py - code/engine/testes/test_escrita_no_editor.py - code/plugins/premiere-pro/skills/editar-por-voz/criterios/01-leitura-do-json.md - code/plugins/premiere-pro/skills/editar-por-voz/criterios/05-zoom.md - code/engine/listar_planos_de_edicao.py - code/engine/testes/test_configuracao_visual_do_scanner.py
138 lines
6.8 KiB
Python
138 lines
6.8 KiB
Python
"""Perfil declarativo que liga a configuração do painel à leitura visual."""
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
from ..integracoes.midia import RESOLUCAO_PADRAO_DO_PROXY
|
|
|
|
|
|
RECURSOS_VISION = frozenset((
|
|
"faces", "qualidade_facial", "pessoas", "pose", "maos", "ocr", "categorias",
|
|
"estetica", "codigos", "horizonte", "retangulos", "contornos", "segmentacao_de_pessoas",
|
|
))
|
|
|
|
# Sensibilidades aceitas para a análise visual auxiliar e o limiar do
|
|
# detector de cortes (PySceneDetect). Valores MENORES de limiar são mais
|
|
# sensíveis — detectam mais cortes. O default segue o PySceneDetect (27).
|
|
SENSIBILIDADES_VISUAIS: dict[str, float] = {
|
|
"muito_baixa": 45.0,
|
|
"baixa": 35.0,
|
|
"media": 27.0,
|
|
"alta": 20.0,
|
|
"muito_alta": 14.0,
|
|
}
|
|
|
|
|
|
def _inteiro_ou_nulo(valor: Any) -> int | None:
|
|
"""Converte um valor de perfil em int, preservando None."""
|
|
if valor is None or valor == "":
|
|
return None
|
|
return int(valor)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ConfiguracaoVisualDoScanner:
|
|
"""Interface curta para uma execução de leitura visual da timeline."""
|
|
|
|
intervalo_em_segundos: float = 1.0
|
|
resolucao_maxima: int = 1080
|
|
faixas_de_video: tuple[int, ...] = ()
|
|
faixas_de_audio: tuple[int, ...] = ()
|
|
modo_de_analise: str = "ambos"
|
|
metricas_de_fala: bool = False
|
|
# Classificação de emoção da fala (ProviderDeEmocaoHuggingFace) — desligada
|
|
# por padrão, mesmo motivo de metricas_de_fala: custo de processamento.
|
|
classificar_emocao: bool = False
|
|
recursos_vision: frozenset[str] = RECURSOS_VISION
|
|
detectar_cenas: bool = True
|
|
analisar_continuidade: bool = True
|
|
preparar_reenquadramento: bool = False
|
|
interpretar_cena: bool = True
|
|
# Análise visual auxiliar (PySceneDetect) — desligada por padrão.
|
|
usar_analise_visual: bool = False
|
|
detectar_cortes: bool = True
|
|
sensibilidade: str = "media"
|
|
# Leitura via proxy compactado. Para arquivos de origem pesados (MOV de
|
|
# vários GB), ler os frames de uma cópia leve é muito mais rápido.
|
|
usar_proxy: bool = True
|
|
resolucao_do_proxy: int = RESOLUCAO_PADRAO_DO_PROXY
|
|
# Limita quantos frames por clipe recebem a interpretação editorial da Apple
|
|
# Intelligence (Foundation Models) — a etapa mais cara. None interpreta todos.
|
|
maximo_de_frames_interpretados: int | None = None
|
|
# Assinatura facial (InsightFace) — desligada por padrão. Nem o OpenCV nem
|
|
# o Apple Vision reconhecem que o mesmo rosto aparece em clipes diferentes;
|
|
# esse recurso existe só para isso, então fica fora do custo padrão.
|
|
identidade_facial: bool = False
|
|
|
|
def __post_init__(self) -> None:
|
|
if self.modo_de_analise not in {"som", "imagem", "ambos"}:
|
|
raise ValueError("modo_de_analise deve ser som, imagem ou ambos.")
|
|
if not 0.04 <= self.intervalo_em_segundos <= 60:
|
|
raise ValueError("intervalo_em_segundos deve estar entre 0,04 e 60.")
|
|
if self.resolucao_maxima != 0 and not 240 <= self.resolucao_maxima <= 8640:
|
|
raise ValueError("resolucao_maxima deve ser 0 (original) ou estar entre 240 e 8640.")
|
|
desconhecidos = self.recursos_vision - RECURSOS_VISION
|
|
if desconhecidos:
|
|
raise ValueError(f"Recursos Vision desconhecidos: {', '.join(sorted(desconhecidos))}")
|
|
if any(indice < 0 for indice in self.faixas_de_video + self.faixas_de_audio):
|
|
raise ValueError("Índices de faixa não podem ser negativos.")
|
|
if self.sensibilidade not in SENSIBILIDADES_VISUAIS:
|
|
raise ValueError(
|
|
f"Sensibilidade inválida: {self.sensibilidade}. "
|
|
f"Use um de {', '.join(SENSIBILIDADES_VISUAIS)}.")
|
|
|
|
@property
|
|
def limiar_de_corte(self) -> float:
|
|
"""Limiar do ContentDetector para a sensibilidade configurada."""
|
|
return SENSIBILIDADES_VISUAIS[self.sensibilidade]
|
|
|
|
@classmethod
|
|
def de_dict(cls, dados: dict[str, Any]) -> "ConfiguracaoVisualDoScanner":
|
|
"""Lê o mesmo JSON produzido pelo painel, sem vazar detalhes de adapters."""
|
|
return cls(
|
|
intervalo_em_segundos=float(dados.get("intervalo_em_segundos", 1)),
|
|
resolucao_maxima=int(dados.get("resolucao_maxima", 1080)),
|
|
faixas_de_video=tuple(sorted(set(int(item) for item in dados.get("faixas_de_video", ())))),
|
|
faixas_de_audio=tuple(sorted(set(int(item) for item in dados.get("faixas_de_audio", ())))),
|
|
modo_de_analise=str(dados.get("modo_de_analise", "ambos")),
|
|
metricas_de_fala=bool(dados.get("metricas_de_fala", False)),
|
|
classificar_emocao=bool(dados.get("classificar_emocao", False)),
|
|
recursos_vision=frozenset(dados.get("recursos_vision", RECURSOS_VISION)),
|
|
detectar_cenas=bool(dados.get("detectar_cenas", True)),
|
|
analisar_continuidade=bool(dados.get("analisar_continuidade", True)),
|
|
preparar_reenquadramento=bool(dados.get("preparar_reenquadramento", False)),
|
|
interpretar_cena=bool(dados.get("interpretar_cena", True)),
|
|
usar_analise_visual=bool(dados.get("usar_analise_visual", False)),
|
|
detectar_cortes=bool(dados.get("detectar_cortes", True)),
|
|
sensibilidade=str(dados.get("sensibilidade", "media")),
|
|
usar_proxy=bool(dados.get("usar_proxy", True)),
|
|
resolucao_do_proxy=int(dados.get("resolucao_do_proxy", RESOLUCAO_PADRAO_DO_PROXY)),
|
|
maximo_de_frames_interpretados=_inteiro_ou_nulo(dados.get("maximo_de_frames_interpretados", None)),
|
|
identidade_facial=bool(dados.get("identidade_facial", False)),
|
|
)
|
|
|
|
def para_dict(self) -> dict[str, Any]:
|
|
"""Serializa a configuração no mesmo formato de JSON consumido pelo painel."""
|
|
return {
|
|
"versao": 1,
|
|
"intervalo_em_segundos": self.intervalo_em_segundos,
|
|
"resolucao_maxima": self.resolucao_maxima,
|
|
"faixas_de_video": list(self.faixas_de_video),
|
|
"faixas_de_audio": list(self.faixas_de_audio),
|
|
"modo_de_analise": self.modo_de_analise,
|
|
"metricas_de_fala": self.metricas_de_fala,
|
|
"classificar_emocao": self.classificar_emocao,
|
|
"recursos_vision": sorted(self.recursos_vision),
|
|
"detectar_cenas": self.detectar_cenas,
|
|
"analisar_continuidade": self.analisar_continuidade,
|
|
"preparar_reenquadramento": self.preparar_reenquadramento,
|
|
"interpretar_cena": self.interpretar_cena,
|
|
"usar_analise_visual": self.usar_analise_visual,
|
|
"detectar_cortes": self.detectar_cortes,
|
|
"sensibilidade": self.sensibilidade,
|
|
"usar_proxy": self.usar_proxy,
|
|
"resolucao_do_proxy": self.resolucao_do_proxy,
|
|
"maximo_de_frames_interpretados": self.maximo_de_frames_interpretados,
|
|
"identidade_facial": self.identidade_facial,
|
|
}
|