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: - 32 arquivos alterados - 16 novos - 15 modificados - 0 removidos 16 files changed, 638 insertions(+), 61 deletions(-) Arquivos: - code/engine/skills/boas-praticas-oo.md -> .agents/skills/boas-praticas-oo/SKILL.md - AGENTS.md - code/cep-plugin/index.html - code/cep-plugin/main.js - code/cep-plugin/styles.css - code/engine/integracoes/visual/apple_vision.py - code/engine/integracoes/visual/apple_vision_runner.swift - code/engine/integracoes/visual/extrator_ffmpeg.py - code/engine/scanner/__init__.py - code/engine/scanner/configuracao_visual.py - code/engine/scanner/coordenacao/__init__.py - code/engine/scanner/retakes/adaptador_de_falas.py - code/engine/scanner/retakes/agrupador_de_takes.py - code/engine/scanner/retakes/detector_de_reinicios.py - code/engine/scanner/retakes/modelos_de_retakes.py - code/engine/scanner/visual.py - .jhonny/ - .retakes/ - CODING_STANDARDS.md - code/engine/executar_retakes.py - code/engine/persistencia/ - code/engine/scanner/retakes/__init__.py - code/engine/scanner/retakes/carregador_de_artefatos.py - code/engine/scanner/retakes/classificador_de_retakes.py - code/engine/scanner/retakes/coordenador_de_retakes.py - code/engine/scanner/retakes/deteccao_de_retakes.py - code/engine/scanner/retakes/gerador_de_evidencias.py - code/engine/scanner/retakes/repositorio_de_retakes.py - code/engine/testes/test_consultas_de_persistencia.py - code/engine/testes/test_deteccao_de_retakes.py - code/engine/testes/test_persistencia_sqlite.py - code/relatorios/audio/arquivos/92f97877259a86a8095b1eecafdefe357212a12247d04b78df60e0c0ae38b2ea/
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
"""Persistência dos grupos de retakes.
|
||||
|
||||
Na primeira versão, grava os grupos em um arquivo JSON idempotente, no
|
||||
mesmo espírito do cache de transcrição da engine. O `RepositorioDeRetakes`
|
||||
define o contrato (``registrar``/``carregar``/``ja_analisado``) para que a
|
||||
camada de banco de dados (SQLite/Postgres, prevista na Etapa 8 da
|
||||
arquitetura) possa ser plugada depois sem alterar o coordenador.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from dataclasses import asdict
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
from .modelos_de_retakes import (
|
||||
STATUS_DE_REVISAO_VALIDOS,
|
||||
GrupoDeRetake,
|
||||
)
|
||||
|
||||
# Versões da transição e das regras para a idempotência.
|
||||
VERSAO_REGRA_FALAS = 1
|
||||
VERSAO_REGRA_TEXTO = 1
|
||||
VERSAO_MODELO_PADRAO = "lexico"
|
||||
|
||||
|
||||
class GrupoDeRetakeInexistente(LookupError):
|
||||
"""Erro levantado quando um grupo de retakes não existe no repositório."""
|
||||
|
||||
def __init__(self, video_id: str, grupo_id: str) -> None:
|
||||
super().__init__(
|
||||
f"Grupo {grupo_id} não encontrado para o vídeo {video_id}.")
|
||||
self.video_id = video_id
|
||||
self.grupo_id = grupo_id
|
||||
|
||||
|
||||
class RepositorioDeRetakes:
|
||||
"""Persiste e recupera grupos de retakes em um arquivo JSON."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
diretoria_de_trabalho: str | Path = ".retakes",
|
||||
versao_transcricao: str = "1",
|
||||
versao_regras: str = f"regras:{VERSAO_REGRA_FALAS}.{VERSAO_REGRA_TEXTO}",
|
||||
versao_modelo: str = VERSAO_MODELO_PADRAO,
|
||||
) -> None:
|
||||
self.diretoria_de_trabalho = Path(diretoria_de_trabalho)
|
||||
self.versao_transcricao = versao_transcricao
|
||||
self.versao_regras = versao_regras
|
||||
self.versao_modelo = versao_modelo
|
||||
|
||||
def _caminho(self, video_id: str) -> Path:
|
||||
return self.diretoria_de_trabalho / f"retakes-{self._seguro(video_id)}.json"
|
||||
|
||||
@staticmethod
|
||||
def _seguro(valor: str) -> str:
|
||||
return "".join(c if c.isalnum() or c in "._-" else "_" for c in valor) or "video"
|
||||
|
||||
def _hash_de_identificacao(self, video_id: str, versao_visual: str = "sem_visual") -> str:
|
||||
origem = "|".join([video_id, self.versao_transcricao, self.versao_regras,
|
||||
self.versao_modelo, versao_visual])
|
||||
return hashlib.sha256(origem.encode("utf-8")).hexdigest()[:12]
|
||||
|
||||
def ja_analisado(self, video_id: str, versao_visual: str = "sem_visual") -> bool:
|
||||
"""Verdadeiro se o vídeo já foi analisado com exatamente estas versões.
|
||||
|
||||
A ``versao_visual`` separa análises com e sem cenas visuais, de modo
|
||||
que uma mudança de configuração visual força uma nova análise (§10).
|
||||
"""
|
||||
caminho = self._caminho(video_id)
|
||||
if not caminho.is_file():
|
||||
return False
|
||||
try:
|
||||
dados = json.loads(caminho.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return False
|
||||
return dados.get("_hash") == self._hash_de_identificacao(video_id, versao_visual)
|
||||
|
||||
def registrar(
|
||||
self,
|
||||
video_id: str,
|
||||
grupos: Iterable[GrupoDeRetake],
|
||||
versao_visual: str = "sem_visual",
|
||||
) -> Path:
|
||||
"""Grava os grupos de forma idempotente e retorna o arquivo gerado."""
|
||||
self.diretoria_de_trabalho.mkdir(parents=True, exist_ok=True)
|
||||
dados = {
|
||||
"video_id": video_id,
|
||||
"versao_transcricao": self.versao_transcricao,
|
||||
"versao_regras": self.versao_regras,
|
||||
"versao_modelo": self.versao_modelo,
|
||||
"versao_visual": versao_visual,
|
||||
"_hash": self._hash_de_identificacao(video_id, versao_visual),
|
||||
"grupos_de_retakes": [asdict(grupo) for grupo in grupos],
|
||||
}
|
||||
caminho = self._caminho(video_id)
|
||||
caminho.write_text(json.dumps(dados, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8")
|
||||
return caminho
|
||||
|
||||
def carregar(self, video_id: str) -> list[GrupoDeRetake]:
|
||||
"""Carrega os grupos já persistidos para o vídeo."""
|
||||
caminho = self._caminho(video_id)
|
||||
if not caminho.is_file():
|
||||
return []
|
||||
dados = json.loads(caminho.read_text(encoding="utf-8"))
|
||||
grupos: list[GrupoDeRetake] = []
|
||||
for item in dados.get("grupos_de_retakes", []):
|
||||
tomadas = item.get("tomadas", [])
|
||||
evidencias = item.get("evidencias", [])
|
||||
grupos.append(GrupoDeRetake(
|
||||
id=item["id"],
|
||||
video_id=item.get("video_id", video_id),
|
||||
faixa_id=item.get("faixa_id", ""),
|
||||
tipo=item["tipo"],
|
||||
confianca=item["confianca"],
|
||||
tomadas=[self._tomada_do_dict(t) for t in tomadas],
|
||||
evidencias=[self._evidencia_do_dict(e) for e in evidencias],
|
||||
criado_em=item.get("criado_em", ""),
|
||||
status_de_revisao=item.get("status_de_revisao", "pendente_de_revisao"),
|
||||
))
|
||||
return grupos
|
||||
|
||||
def atualizar_status_de_revisao(
|
||||
self,
|
||||
video_id: str,
|
||||
grupo_id: str,
|
||||
novo_status: str,
|
||||
) -> GrupoDeRetake:
|
||||
"""Registra a decisão manual do usuário sobre um grupo de retakes.
|
||||
|
||||
A revisão não altera a detecção automática: apenas grava o estado
|
||||
escolhido (confirmado, não é retake, ignorado ou pendente) no arquivo
|
||||
já existente do vídeo, preservando os demais grupos.
|
||||
|
||||
Levanta ``ValueError`` se o status não for válido e
|
||||
``GrupoDeRetakeInexistente`` se o grupo não existir.
|
||||
"""
|
||||
if novo_status not in STATUS_DE_REVISAO_VALIDOS:
|
||||
validos = ", ".join(sorted(STATUS_DE_REVISAO_VALIDOS))
|
||||
raise ValueError(f"Status de revisão inválido: {novo_status!r}. "
|
||||
f"Válidos: {validos}.")
|
||||
caminho = self._caminho(video_id)
|
||||
if not caminho.is_file():
|
||||
raise GrupoDeRetakeInexistente(video_id, grupo_id)
|
||||
dados = json.loads(caminho.read_text(encoding="utf-8"))
|
||||
grupos = dados.get("grupos_de_retakes", [])
|
||||
alvo = next((g for g in grupos if g.get("id") == grupo_id), None)
|
||||
if alvo is None:
|
||||
raise GrupoDeRetakeInexistente(video_id, grupo_id)
|
||||
alvo["status_de_revisao"] = novo_status
|
||||
caminho.write_text(json.dumps(dados, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8")
|
||||
return self.carregar(video_id)[grupos.index(alvo)]
|
||||
|
||||
@staticmethod
|
||||
def _tomada_do_dict(item: dict) -> object:
|
||||
from .modelos_de_retakes import TomadaDeRetake
|
||||
return TomadaDeRetake(
|
||||
ordem=item["ordem"],
|
||||
fala_id=item["fala_id"],
|
||||
segmento_id=item.get("segmento_id", ""),
|
||||
inicio=item["inicio"],
|
||||
fim=item["fim"],
|
||||
texto=item.get("texto", ""),
|
||||
similaridade_com_anterior=item.get("similaridade_com_anterior", 0.0),
|
||||
confianca=item.get("confianca", 0.0),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _evidencia_do_dict(item: dict) -> object:
|
||||
from .modelos_de_retakes import EvidenciaDeRetake
|
||||
return EvidenciaDeRetake(
|
||||
tipo=item["tipo"],
|
||||
descricao=item.get("descricao", ""),
|
||||
valor=item.get("valor"),
|
||||
)
|
||||
Reference in New Issue
Block a user