feat: criado repositório jhonny-editor no Gitea
criado repositório jhonny-editor no Gitea adicionado script admin/deploy.command com commit automático atualizado admin/DEV-NOTES.md com template limpo Resumo: - 48 arquivos alterados - 26 novos - 19 modificados - 3 removidos 22 files changed, 658 insertions(+), 568 deletions(-) Arquivos: - AGENTS.md - admin/DEV-NOTES.md - admin/OpenCut.command - admin/commit.command - admin/deploy.command - admin/update.command - code/cep-plugin/index.html - code/cep-plugin/main.js - code/cep-plugin/styles.css - code/engine/ARQUITETURA.md - code/engine/arquitetura/README.md - code/engine/gerar_relatorio_timeline.py - code/engine/integracoes/apple_speech/apple_speech_transcriber.swift - code/engine/integracoes/apple_speech/provider_de_transcricao_apple.py - code/engine/integracoes/midia/__init__.py - code/engine/integracoes/whisper/provider_de_transcricao_local.py - code/engine/scanner/__init__.py - code/engine/scanner/coordenacao/__init__.py - code/engine/scanner/descoberta/__init__.py - code/engine/scanner/modelos.py - code/engine/scanner/transcricao_da_timeline.py - code/src/tools/discovery.ts - :memory:.ses - admin/Jhonny.command - admin/inativos/OpenCut.command - admin/inativos/update.command - code/docs/glossario-analise-emocional.md - code/docs/levantamento-apple-vision-e-apple-intelligence.md - code/docs/levantamento-ferramentas-analise-visual-local.md - code/engine/arquitetura/biblioteca-inteligente-de-videos.md - code/engine/executar_scanner.py - code/engine/integracoes/huggingface/ - code/engine/integracoes/midia/extracao_de_metadados.py - code/engine/integracoes/visual/ - code/engine/requirements-visual.txt - code/engine/scanner/configuracao_visual.py - code/engine/scanner/descoberta/descoberta_de_arquivos.py - code/engine/scanner/metadados.py - code/engine/scanner/relatorio_visual.py - code/engine/scanner/retakes/ - code/engine/scanner/visual.py - code/engine/testes/test_analise_visual_local.py - code/engine/testes/test_arquivos_e_metadados.py - code/engine/testes/test_provider_de_transcricao_apple.py - code/relatorios/analise-brools/ - code/relatorios/analise-visual/ - code/relatorios/audio/arquivos/ - code/relatorios/transcricao-timeline.md
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
import json
|
||||
import tempfile
|
||||
|
||||
from engine.dominio import Clipe, Faixa, IntervaloDeTempo, Timeline
|
||||
from engine.integracoes.visual.contratos import (AnalisadorDeFrame, AnalisadorDeSequenciaDeQuadros,
|
||||
DetectorDeIntervalosDeCena,
|
||||
ExtratorDeQuadros)
|
||||
from engine.integracoes.visual.modelos import QuadroDeVideo
|
||||
from engine.integracoes.visual.extrator_open_cv import ExtratorDeQuadrosOpenCV
|
||||
from engine.integracoes.visual.apple_vision import (AnalisadorAppleVisionNativo,
|
||||
AnalisadorSequenciaAppleVisionNativo)
|
||||
from engine.scanner.coordenacao import ContextoDeAnalise
|
||||
from engine.scanner.modelos import Cena, EvidenciaVisual
|
||||
from engine.scanner.visual import AnaliseVisualDaTimeline, DetectorDeCenas, ResultadoVisualDoClipe
|
||||
from engine.scanner.relatorio_visual import gerar_relatorio_visual, gerar_resumo_visual_markdown
|
||||
from engine.scanner.configuracao_visual import ConfiguracaoVisualDoScanner
|
||||
|
||||
|
||||
class ExtratorSimulado(ExtratorDeQuadros):
|
||||
def extrair(self, clipe):
|
||||
return [
|
||||
QuadroDeVideo(0.0, 0, 1920, 1080, "frame-0"),
|
||||
QuadroDeVideo(1.0, 1, 1920, 1080, "frame-1"),
|
||||
]
|
||||
|
||||
|
||||
class AnalisadorSimulado(AnalisadorDeFrame):
|
||||
nome = "simulado"
|
||||
|
||||
def analisar(self, quadro):
|
||||
return [EvidenciaVisual("qualidade", quadro.timestamp, quadro.timestamp,
|
||||
{"nitidez_laplaciana": 42.0}, provider=self.nome)]
|
||||
|
||||
|
||||
class DetectorSimulado(DetectorDeIntervalosDeCena):
|
||||
nome = "simulado"
|
||||
|
||||
def detectar(self, clipe, quadros):
|
||||
return [Cena(0.0, 1.0, referencias=(0.0,)), Cena(1.0, 2.0, referencias=(1.0,))]
|
||||
|
||||
|
||||
class AnalisadorTemporalSimulado(AnalisadorDeSequenciaDeQuadros):
|
||||
nome = "temporal_simulado"
|
||||
|
||||
def analisar(self, quadros):
|
||||
return [EvidenciaVisual("continuidade", quadros[0].timestamp, quadros[-1].timestamp,
|
||||
{"possivel_descontinuidade": False}, provider=self.nome)]
|
||||
|
||||
|
||||
class ExecutorAppleSimulado:
|
||||
def executar(self, carga, timeout):
|
||||
self.carga, self.timeout = carga, timeout
|
||||
return {"evidencias": [{"tipo": "pessoa", "valor": {"ocupacao_do_quadro": 0.3},
|
||||
"confianca": 0.9, "modelo": "VNDetectHumanRectanglesRequest"}],
|
||||
"avisos": ["ocr: indisponível"]}
|
||||
|
||||
|
||||
class ExecutorAppleSequenciaSimulado:
|
||||
def executar(self, carga, timeout):
|
||||
self.carga, self.timeout = carga, timeout
|
||||
return {"evidencias": [{"tipo": "similaridade_visual", "valor": {
|
||||
"frame_inicial": 0, "frame_final": 1, "distancia_feature_print": 0.2,
|
||||
"possivel_mudanca_de_cena": False,
|
||||
}, "modelo": "VNGenerateImageFeaturePrintRequest"}], "avisos": []}
|
||||
|
||||
|
||||
class TesteAnaliseVisualLocal(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.clipe = Clipe("clip-1", "camera", IntervaloDeTempo(0, 2), arquivo="/video.mp4")
|
||||
|
||||
def test_detector_envia_cada_frame_aos_analisadores_e_consolida_cenas(self):
|
||||
detector = DetectorDeCenas(ExtratorSimulado(), [AnalisadorSimulado()], [DetectorSimulado()])
|
||||
|
||||
resultado = detector.detectar(self.clipe)
|
||||
|
||||
self.assertEqual(resultado.identificador_do_clipe, "clip-1")
|
||||
self.assertEqual([item.inicio for item in resultado.evidencias], [0.0, 1.0])
|
||||
self.assertEqual(len(resultado.cenas), 1)
|
||||
self.assertEqual((resultado.cenas[0].inicio, resultado.cenas[0].fim), (0.0, 2.0))
|
||||
|
||||
def test_etapa_da_timeline_guarda_evidencias_por_clipe(self):
|
||||
detector = DetectorDeCenas(ExtratorSimulado(), [AnalisadorSimulado()], [DetectorSimulado()])
|
||||
timeline = Timeline("timeline-1", "Principal", faixas=[
|
||||
Faixa("video-1", "V1", "video", 0, [self.clipe]),
|
||||
])
|
||||
contexto = ContextoDeAnalise(timeline=timeline)
|
||||
|
||||
AnaliseVisualDaTimeline(detector).executar(contexto)
|
||||
|
||||
self.assertEqual(len(contexto.caracteristicas_visuais["clip-1"]["evidencias"]), 2)
|
||||
self.assertEqual(len(contexto.cenas), 1)
|
||||
|
||||
def test_detector_executa_analisadores_de_sequencia_uma_vez_por_clipe(self):
|
||||
detector = DetectorDeCenas(ExtratorSimulado(), analisadores_de_sequencia=[AnalisadorTemporalSimulado()])
|
||||
|
||||
resultado = detector.detectar(self.clipe)
|
||||
|
||||
self.assertEqual(len(resultado.evidencias), 1)
|
||||
self.assertEqual(resultado.evidencias[0].tipo, "continuidade")
|
||||
|
||||
def test_fluxo_produz_cena_visual_com_observacoes_e_continuidade(self):
|
||||
detector = DetectorDeCenas(
|
||||
ExtratorSimulado(), [AnalisadorSimulado()],
|
||||
analisadores_de_sequencia=[AnalisadorTemporalSimulado()],
|
||||
)
|
||||
contexto = ContextoDeAnalise(timeline=Timeline(
|
||||
"timeline-1", "Principal", faixas=[Faixa("video-1", "V1", "video", 0, [self.clipe])]
|
||||
))
|
||||
|
||||
AnaliseVisualDaTimeline(detector).executar(contexto)
|
||||
|
||||
cena = contexto.cenas_visuais[0]
|
||||
self.assertEqual(cena.identificador_do_clipe, "clip-1")
|
||||
self.assertEqual([item.tipo for item in cena.observacoes], ["qualidade", "qualidade", "continuidade"])
|
||||
self.assertTrue(contexto.caracteristicas_visuais["clip-1"]["disponivel"])
|
||||
|
||||
def test_falha_de_adapter_e_registrada_sem_interromper_outro_clipe(self):
|
||||
class ExtratorQueFalha(ExtratorDeQuadros):
|
||||
def extrair(self, clipe):
|
||||
if clipe.identificador == "clip-1":
|
||||
raise RuntimeError("OpenCV não instalado")
|
||||
return [QuadroDeVideo(0.0, 0, 1920, 1080, "frame")]
|
||||
|
||||
segundo = Clipe("clip-2", "camera-2", IntervaloDeTempo(2, 3), arquivo="/video-2.mp4")
|
||||
timeline = Timeline("timeline-1", "Principal", faixas=[
|
||||
Faixa("video-1", "V1", "video", 0, [self.clipe, segundo]),
|
||||
])
|
||||
contexto = ContextoDeAnalise(timeline=timeline)
|
||||
detector = DetectorDeCenas(ExtratorQueFalha(), [AnalisadorSimulado()])
|
||||
|
||||
AnaliseVisualDaTimeline(detector).executar(contexto)
|
||||
|
||||
self.assertFalse(contexto.caracteristicas_visuais["clip-1"]["disponivel"])
|
||||
self.assertTrue(contexto.caracteristicas_visuais["clip-2"]["disponivel"])
|
||||
self.assertEqual(len(contexto.avisos), 1)
|
||||
|
||||
def test_adapter_apple_converte_fatos_e_preserva_avisos_por_recurso(self):
|
||||
executor = ExecutorAppleSimulado()
|
||||
quadro = QuadroDeVideo(0.0, 0, 1920, 1080, "frame", Path("/frame.jpg"))
|
||||
|
||||
evidencias = AnalisadorAppleVisionNativo(executor=executor).analisar(quadro)
|
||||
|
||||
self.assertEqual(evidencias[0].tipo, "pessoa")
|
||||
self.assertEqual(evidencias[0].modelo, "VNDetectHumanRectanglesRequest")
|
||||
self.assertEqual(evidencias[1].tipo, "diagnostico_apple_vision")
|
||||
self.assertTrue(executor.carga["resumir"])
|
||||
|
||||
def test_adapter_apple_de_sequencia_converte_indices_em_intervalos(self):
|
||||
executor = ExecutorAppleSequenciaSimulado()
|
||||
quadros = [QuadroDeVideo(2.0, 0, 1920, 1080, "frame", Path("/frame-0.jpg")),
|
||||
QuadroDeVideo(3.0, 1, 1920, 1080, "frame", Path("/frame-1.jpg"))]
|
||||
|
||||
evidencias = AnalisadorSequenciaAppleVisionNativo(executor=executor).analisar(quadros)
|
||||
|
||||
self.assertEqual(evidencias[0].tipo, "similaridade_visual")
|
||||
self.assertEqual((evidencias[0].inicio, evidencias[0].fim), (2.0, 3.0))
|
||||
self.assertEqual(evidencias[0].valor["distancia_feature_print"], 0.2)
|
||||
self.assertEqual(executor.carga["frames"], ["/frame-0.jpg", "/frame-1.jpg"])
|
||||
|
||||
def test_evidencia_rejeita_intervalo_invalido(self):
|
||||
with self.assertRaises(ValueError):
|
||||
EvidenciaVisual("objeto", 2.0, 1.0, {})
|
||||
|
||||
def test_relatorio_visual_separa_frames_de_evidencias_temporais(self):
|
||||
resultado = ResultadoVisualDoClipe("clip-1", [
|
||||
EvidenciaVisual("categoria", 2.0, 2.0, {"identificador": "drink"}),
|
||||
EvidenciaVisual("similaridade_visual", 2.0, 3.0, {"distancia": 0.2}),
|
||||
])
|
||||
with tempfile.TemporaryDirectory() as pasta:
|
||||
caminho = gerar_relatorio_visual(self.clipe, resultado, Path(pasta) / "visual.json", 1.0)
|
||||
dados = json.loads(caminho.read_text(encoding="utf-8"))
|
||||
|
||||
self.assertEqual(dados["clipe"]["identificador"], "clip-1")
|
||||
self.assertEqual(dados["observacoes_por_frame"][0]["timestamp_na_origem"], 2.0)
|
||||
self.assertEqual(dados["observacoes_por_frame"][0]["evidencias"][0]["tipo"], "categoria")
|
||||
self.assertEqual(dados["evidencias_temporais"][0]["tipo"], "similaridade_visual")
|
||||
|
||||
def test_resumo_visual_markdown_lista_frames_e_transicoes(self):
|
||||
resultado = ResultadoVisualDoClipe("clip-1", [
|
||||
EvidenciaVisual("categoria", 2.0, 2.0, {"identificador": "drink"}),
|
||||
EvidenciaVisual("similaridade_visual", 2.0, 3.0, {"distancia": 0.2}),
|
||||
])
|
||||
with tempfile.TemporaryDirectory() as pasta:
|
||||
caminho = gerar_resumo_visual_markdown(self.clipe, resultado, Path(pasta) / "visual.md", 1.0)
|
||||
conteudo = caminho.read_text(encoding="utf-8")
|
||||
|
||||
self.assertIn("Origem 2.000s", conteudo)
|
||||
self.assertIn("similaridade_visual", conteudo)
|
||||
|
||||
def test_configuracao_visual_valida_o_perfil_do_painel(self):
|
||||
perfil = ConfiguracaoVisualDoScanner.de_dict({
|
||||
"intervalo_em_segundos": 5,
|
||||
"faixas_de_video": [2, 0, 2],
|
||||
"recursos_vision": ["faces", "pessoas"],
|
||||
"analisar_continuidade": False,
|
||||
})
|
||||
|
||||
self.assertEqual(perfil.faixas_de_video, (0, 2))
|
||||
self.assertEqual(perfil.para_dict()["recursos_vision"], ["faces", "pessoas"])
|
||||
self.assertFalse(perfil.analisar_continuidade)
|
||||
|
||||
def test_extrator_respeita_intervalo_na_origem_e_limite_de_frames(self):
|
||||
clipe = Clipe("clip-1", "camera", IntervaloDeTempo(0, 2),
|
||||
intervalo_na_origem=IntervaloDeTempo(2.3, 5.0), arquivo="/video.mp4")
|
||||
extrator = ExtratorDeQuadrosOpenCV(intervalo_em_segundos=1.0, maximo_de_quadros=1,
|
||||
cv2_module=object())
|
||||
|
||||
inicio, fim = extrator._intervalo_de_origem(clipe, 10.0)
|
||||
|
||||
self.assertEqual(extrator._timestamps(inicio, fim), [2.3])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,86 @@
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from engine.dominio import Clipe, Faixa, IntervaloDeTempo, Timeline
|
||||
from engine.scanner.coordenacao import ContextoDeAnalise
|
||||
from engine.scanner.descoberta import ArquivoDescoberto, DescobertaDeArquivos
|
||||
from engine.scanner.metadados import ExtracaoDeMetadados
|
||||
from engine.integracoes.midia.extracao_de_metadados import ExtracaoDeMetadados as Adapter
|
||||
|
||||
|
||||
class TesteArquivosEMetadados(unittest.TestCase):
|
||||
def _contexto(self, *arquivos: str | None) -> ContextoDeAnalise:
|
||||
clipes = [Clipe(str(i), f"clipe-{i}", IntervaloDeTempo(0, 1), arquivo=arquivo)
|
||||
for i, arquivo in enumerate(arquivos)]
|
||||
timeline = Timeline("timeline", "Principal", faixas=[Faixa("v1", "V1", "video", 0, clipes)])
|
||||
return ContextoDeAnalise(timeline=timeline)
|
||||
|
||||
def test_descoberta_normaliza_caminho_e_marca_offline(self):
|
||||
with tempfile.TemporaryDirectory() as pasta:
|
||||
arquivo = Path(pasta) / "camera.mp4"
|
||||
arquivo.touch()
|
||||
contexto = self._contexto(str(arquivo), str(Path(pasta) / "ausente.mp4"), None)
|
||||
|
||||
DescobertaDeArquivos().executar(contexto)
|
||||
|
||||
self.assertEqual(contexto.timeline.faixas[0].clipes[0].arquivo, str(arquivo.resolve()))
|
||||
self.assertFalse(contexto.timeline.faixas[0].clipes[0].offline)
|
||||
self.assertTrue(contexto.timeline.faixas[0].clipes[1].offline)
|
||||
self.assertTrue(contexto.timeline.faixas[0].clipes[2].offline)
|
||||
self.assertEqual(len(contexto.erros), 2)
|
||||
|
||||
def test_descoberta_identifica_arquivo_duplicado(self):
|
||||
with tempfile.TemporaryDirectory() as pasta:
|
||||
arquivo = Path(pasta) / "camera.mov"
|
||||
arquivo.touch()
|
||||
contexto = self._contexto(str(arquivo), str(arquivo))
|
||||
|
||||
DescobertaDeArquivos().executar(contexto)
|
||||
|
||||
self.assertEqual(len(contexto.avisos), 1)
|
||||
|
||||
def test_extracao_converte_ffprobe_e_usa_cache(self):
|
||||
dados = {"format": {"format_name": "mov,mp4,m4a", "duration": "12.5", "size": "900"},
|
||||
"streams": [{"codec_type": "video", "codec_name": "h264", "width": 1920,
|
||||
"height": 1080, "avg_frame_rate": "30000/1001"},
|
||||
{"codec_type": "audio", "codec_name": "aac", "channels": 2,
|
||||
"sample_rate": "48000"}]}
|
||||
with tempfile.TemporaryDirectory() as pasta:
|
||||
arquivo = Path(pasta) / "camera.mp4"
|
||||
arquivo.touch()
|
||||
adapter = Adapter(ffprobe="ffprobe-simulado")
|
||||
processo = type("Processo", (), {"stdout": json.dumps(dados)})()
|
||||
with patch("engine.integracoes.midia.extracao_de_metadados.subprocess.run", return_value=processo) as run:
|
||||
primeiro = adapter.extrair(arquivo)
|
||||
segundo = adapter.extrair(arquivo)
|
||||
|
||||
self.assertEqual(primeiro.codec_de_video, "h264")
|
||||
self.assertEqual(primeiro.codec_de_audio, "aac")
|
||||
self.assertEqual((primeiro.largura, primeiro.altura), (1920, 1080))
|
||||
self.assertAlmostEqual(primeiro.taxa_de_quadros, 29.97002997, places=5)
|
||||
self.assertEqual(primeiro.duracao, 12.5)
|
||||
self.assertIs(primeiro, segundo)
|
||||
run.assert_called_once()
|
||||
|
||||
def test_etapa_de_metadados_ignora_arquivos_offline(self):
|
||||
class ExtratorSimulado:
|
||||
def extrair(self, arquivo):
|
||||
return "metadados"
|
||||
|
||||
with tempfile.TemporaryDirectory() as pasta:
|
||||
arquivo = Path(pasta) / "camera.mp4"
|
||||
arquivo.touch()
|
||||
contexto = self._contexto(str(arquivo), str(Path(pasta) / "ausente.mp4"))
|
||||
DescobertaDeArquivos().executar(contexto)
|
||||
|
||||
ExtracaoDeMetadados(ExtratorSimulado()).executar(contexto)
|
||||
|
||||
self.assertEqual(contexto.metadados_dos_arquivos["0"], "metadados")
|
||||
self.assertNotIn("1", contexto.metadados_dos_arquivos)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,32 @@
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from engine.integracoes.apple_speech import ProviderDeTranscricaoApple
|
||||
|
||||
|
||||
class TesteProviderDeTranscricaoApple(unittest.TestCase):
|
||||
@patch("engine.integracoes.apple_speech.provider_de_transcricao_apple.subprocess.run")
|
||||
def test_transcreve_em_pt_br_no_dispositivo_e_preserva_timestamps(self, executar):
|
||||
executar.return_value = type("Resultado", (), {
|
||||
"stdout": json.dumps({"segmentos": [
|
||||
{"inicio": 1.25, "fim": 2.75, "texto": " Olá mundo ", "confianca": 0.93},
|
||||
]}),
|
||||
})()
|
||||
|
||||
with tempfile.TemporaryDirectory() as pasta:
|
||||
arquivo = Path(pasta) / "video.mp4"
|
||||
arquivo.write_bytes(b"video")
|
||||
clipe = type("Clipe", (), {"arquivo": str(arquivo)})()
|
||||
resultado = ProviderDeTranscricaoApple(executavel="transcritor").transcrever(clipe)
|
||||
|
||||
self.assertEqual(resultado[0].texto, "Olá mundo")
|
||||
self.assertEqual((resultado[0].inicio, resultado[0].fim), (1.25, 2.75))
|
||||
comando = executar.call_args.args[0]
|
||||
self.assertEqual(comando, ["transcritor", str(arquivo), "pt-BR", "true"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user