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:
João Henrique
2026-09-08 16:13:27 -04:00
parent b541f502ba
commit b9bf3b2863
76 changed files with 16147 additions and 322 deletions
@@ -0,0 +1,106 @@
import json
import subprocess
from dataclasses import dataclass
from fractions import Fraction
from pathlib import Path
from typing import Any
from ...scanner.modelos import ErroDeAnalise
@dataclass(frozen=True)
class MetadadosDoArquivo:
caminho: Path
formato: str | None = None
duracao: float | None = None
codec_de_video: str | None = None
codec_de_audio: str | None = None
largura: int | None = None
altura: int | None = None
taxa_de_quadros: float | None = None
canais_de_audio: int | None = None
taxa_de_amostragem: int | None = None
tamanho_em_bytes: int | None = None
orientacao: str | None = None
timecode: str | None = None
class ExtracaoDeMetadados:
"""Extrai metadados técnicos via ffprobe, com cache por caminho normalizado."""
def __init__(self, ffprobe: str = "ffprobe") -> None:
self.ffprobe = ffprobe
self._cache: dict[Path, MetadadosDoArquivo] = {}
def extrair(self, arquivo: str | Path) -> MetadadosDoArquivo:
caminho = Path(arquivo).expanduser().resolve(strict=False)
if not caminho.is_file():
raise ErroDeAnalise("arquivo_inacessivel", "Arquivo de mídia não encontrado.", str(caminho))
if caminho in self._cache:
return self._cache[caminho]
try:
processo = subprocess.run(
[self.ffprobe, "-v", "error", "-print_format", "json", "-show_format", "-show_streams", str(caminho)],
check=True, capture_output=True, text=True,
)
dados = json.loads(processo.stdout)
except (OSError, subprocess.SubprocessError, json.JSONDecodeError) as exc:
raise ErroDeAnalise("metadados_indisponiveis", f"Não foi possível ler os metadados: {exc}", str(caminho)) from exc
metadados = self._converter(caminho, dados)
self._cache[caminho] = metadados
return metadados
def limpar_cache(self) -> None:
self._cache.clear()
@classmethod
def _converter(cls, caminho: Path, dados: dict[str, Any]) -> MetadadosDoArquivo:
formato = dados.get("format") or {}
streams = dados.get("streams") or []
video = next((item for item in streams if item.get("codec_type") == "video"), {})
audio = next((item for item in streams if item.get("codec_type") == "audio"), {})
duracao = cls._float(formato.get("duration"))
if duracao is None:
duracao = cls._float(video.get("duration") or audio.get("duration"))
return MetadadosDoArquivo(
caminho=caminho,
formato=cls._format_name(formato.get("format_name")),
duracao=duracao,
codec_de_video=video.get("codec_name") or None,
codec_de_audio=audio.get("codec_name") or None,
largura=cls._int(video.get("width")), altura=cls._int(video.get("height")),
taxa_de_quadros=cls._fps(video.get("avg_frame_rate") or video.get("r_frame_rate")),
canais_de_audio=cls._int(audio.get("channels")),
taxa_de_amostragem=cls._int(audio.get("sample_rate")),
tamanho_em_bytes=cls._int(formato.get("size")) or caminho.stat().st_size,
orientacao=video.get("side_data_list", [{}])[0].get("rotation") if video.get("side_data_list") else None,
timecode=(formato.get("tags") or {}).get("timecode") or (video.get("tags") or {}).get("timecode"),
)
@staticmethod
def _float(valor: Any) -> float | None:
try:
return None if valor in (None, "", "N/A") else float(valor)
except (TypeError, ValueError):
return None
@staticmethod
def _int(valor: Any) -> int | None:
try:
return None if valor in (None, "", "N/A") else int(valor)
except (TypeError, ValueError):
return None
@staticmethod
def _fps(valor: Any) -> float | None:
if valor in (None, "", "0/0", "N/A"):
return None
try:
return float(Fraction(str(valor)))
except (ValueError, ZeroDivisionError):
return None
@staticmethod
def _format_name(valor: Any) -> str | None:
return str(valor).split(",", 1)[0] if valor else None