Files
jhonny-editor/code/engine/executar_retakes.py
T
João Henrique 5cc50f00a4 feat: - tornado obrigatório registrar uma linha de resum
- tornado obrigatório registrar uma linha de resumo para cada implementação concluída
- corrigido o Scanner e o Retakes no painel usando o Python do venv (3.14) em vez do /usr/bin/python3 3.9 e injetado o caminho da engine no executar_retakes.py

Resumo:
- 7 arquivos alterados
- 0 novos
- 7 modificados
- 0 removidos

 7 files changed, 57 insertions(+), 8 deletions(-)

Arquivos:
  - .gitignore
  - AGENTS.md
  - CLAUDE.md
  - admin/DEV-NOTES.md
  - admin/deploy.command
  - code/cep-plugin/main.js
  - code/engine/executar_retakes.py
2026-09-08 17:35:07 -04:00

173 lines
6.3 KiB
Python

"""Entrada local do painel CEP para a análise de retakes.
Espelha o ``executar_scanner.py``: recebe um pedido JSON montado pela tela,
executa a análise com as classes existentes em ``scanner/retakes`` e emite
eventos de progresso em JSON (uma linha por evento) na saída padrão.
Ações suportadas no pedido:
- ``analisar``: executa a análise completa usando as transcrições já
geradas pelo scanner (nunca retranscreve).
- ``revisar``: registra a decisão manual do usuário sobre um grupo.
Formato do pedido (exemplo)::
{
"acao": "analisar",
"video_id": "entrevista_01",
"artefatos_de_audio": ["...-audio-faixa-1.json"],
"configuracao": {"criterio": "equilibrado"}
}
Para ``revisar``::
{
"acao": "revisar",
"video_id": "entrevista_01",
"revisao": {"grupo_id": "retake_0001", "acao": "confirmar"}
}
"""
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import asdict
from pathlib import Path
# O painel chama este arquivo com cwd ``code/engine`` e o Python inclui apenas
# essa pasta no caminho de importação. Injetar ``code`` (pai do pacote
# ``engine``) para que os importes da engine funcionem, como em
# ``executar_scanner.py``.
CAMINHO_DO_CODIGO = Path(__file__).resolve().parent.parent
if str(CAMINHO_DO_CODIGO) not in sys.path:
sys.path.insert(0, str(CAMINHO_DO_CODIGO))
from engine.scanner.retakes import (
STATUS_CONFIRMADO,
STATUS_IGNORADO,
STATUS_PENDENTE,
STATUS_REJEITADO,
CarregadorDeArtefatosDeAudio,
ClassificadorDeRetakes,
CoordenadorDeRetakes,
RegrasDeClassificacao,
RepositorioDeRetakes,
)
# Confiança mínima exibida por critério de confirmação da tela (§9 do guia).
LIMIARES_POR_CRITERIO = {
"conservador": 0.80,
"equilibrado": 0.65,
"sensivel": 0.45,
"personalizado": 0.0,
}
STATUS_PADRAO_DO_PEDIDO = {
"confirmar": STATUS_CONFIRMADO,
"rejeitar": STATUS_REJEITADO,
"manter": STATUS_PENDENTE,
"ignorar": STATUS_IGNORADO,
}
def _emitir(etapa: str, percentual: float, detalhe: str = "") -> None:
"""Emite um evento de progresso no mesmo protocolo do scanner."""
print(json.dumps({"evento": "progresso", "etapa": etapa,
"percentual": round(percentual, 1), "clipe": detalhe},
ensure_ascii=False), flush=True)
def _criterio_do_pedido(configuracao: dict) -> str:
"""Valida e devolve o critério de confirmação escolhido na tela."""
criterio = str(configuracao.get("criterio", "equilibrado"))
if criterio not in LIMIARES_POR_CRITERIO:
validos = ", ".join(sorted(LIMIARES_POR_CRITERIO))
raise ValueError(f"Critério desconhecido: {criterio!r}. Válidos: {validos}.")
return criterio
def _grupos_do_dict(grupos) -> list[dict]:
"""Converte os grupos de retakes para o formato consumido pela tela."""
return [asdict(grupo) for grupo in grupos]
def _analisar(pedido: dict, pasta_do_caso: Path) -> dict:
"""Executa a análise completa de retakes para o caso informado."""
configuracao = pedido.get("configuracao", {})
criterio = _criterio_do_pedido(configuracao)
video_id = str(pedido.get("video_id") or "video")
_emitir("Preparando análise", 5)
repositorio = RepositorioDeRetakes(diretoria_de_trabalho=pasta_do_caso / "retakes")
if repositorio.ja_analisado(video_id):
_emitir("Reutilizando análise existente", 100)
return {"reutilizado": True,
"grupos": _grupos_do_dict(repositorio.carregar(video_id))}
_emitir("Carregando transcrição", 20)
carregador = CarregadorDeArtefatosDeAudio()
falas = carregador.carregar(pedido.get("artefatos_de_audio", []), video_id)
_emitir("Comparando trechos", 45)
coordenador = CoordenadorDeRetakes(
classificador_de_retakes=ClassificadorDeRetakes(RegrasDeClassificacao()),
repositorio_de_retakes=repositorio,
)
_emitir("Agrupando tentativas", 65)
resultado = coordenador.analisar(video_id=video_id, falas=falas)
_emitir("Calculando confiança", 85)
limiar = LIMIARES_POR_CRITERIO[criterio]
visiveis = [grupo for grupo in resultado.grupos if grupo.confianca >= limiar]
_emitir("Salvando resultados", 95)
repositorio.registrar(video_id, resultado.grupos)
return {"reutilizado": False, "grupos": _grupos_do_dict(visiveis)}
def _revisar(pedido: dict, pasta_do_caso: Path) -> dict:
"""Registra a decisão manual do usuário sobre um grupo de retakes."""
video_id = str(pedido.get("video_id") or "video")
revisao = pedido.get("revisao", {})
grupo_id = str(revisao.get("grupo_id", ""))
acao = str(revisao.get("acao", ""))
if acao not in STATUS_PADRAO_DO_PEDIDO:
validos = ", ".join(sorted(STATUS_PADRAO_DO_PEDIDO))
raise ValueError(f"Ação de revisão desconhecida: {acao!r}. Válidos: {validos}.")
repositorio = RepositorioDeRetakes(diretoria_de_trabalho=pasta_do_caso / "retakes")
grupo = repositorio.atualizar_status_de_revisao(
video_id, grupo_id, STATUS_PADRAO_DO_PEDIDO[acao])
return {"grupos": _grupos_do_dict([grupo])}
def executar(entrada: Path, pasta_do_caso: Path) -> dict:
"""Lê o pedido, executa a ação e devolve o resultado para a tela."""
pedido = json.loads(entrada.read_text(encoding="utf-8"))
acao = str(pedido.get("acao", "analisar"))
if acao == "revisar":
return _revisar(pedido, pasta_do_caso)
return _analisar(pedido, pasta_do_caso)
def main() -> None:
"""Ponto de entrada da linha de comando usada pelo painel."""
parser = argparse.ArgumentParser(description="Análise de retakes para o painel.")
parser.add_argument("entrada", type=Path, help="Arquivo de pedido JSON.")
parser.add_argument("pasta_do_caso", type=Path,
help="Pasta do caso gerada pelo scanner.")
args = parser.parse_args()
try:
resultado = executar(args.entrada, args.pasta_do_caso)
except Exception as erro: # repassa a causa para a tela decidir como exibir
print(json.dumps({"evento": "erro", "mensagem": str(erro)},
ensure_ascii=False), flush=True)
sys.exit(1)
_emitir("Análise concluída", 100)
print(json.dumps({"evento": "concluido", "resultado": resultado},
ensure_ascii=False), flush=True)
if __name__ == "__main__":
main()