"""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 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()