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:
@@ -1,3 +1,4 @@
|
||||
from .extracao_de_audio import ExtracaoDeAudio, ParteDeAudio
|
||||
from .extracao_de_metadados import ExtracaoDeMetadados, MetadadosDoArquivo
|
||||
|
||||
__all__ = ["ExtracaoDeAudio", "ParteDeAudio"]
|
||||
__all__ = ["ExtracaoDeAudio", "ExtracaoDeMetadados", "MetadadosDoArquivo", "ParteDeAudio"]
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user