From 11566199375983e02294bf165e8464de60f3f1d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Henrique?= Date: Wed, 9 Sep 2026 23:38:52 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20troca=20except=20Exception=20gen=C3=A9ri?= =?UTF-8?q?co=20por=20exce=C3=A7=C3=B5es=20espec=C3=ADficas=20nos=20script?= =?UTF-8?q?s=20do=20Scanner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit executar_scanner.py: nos dois pontos que processavam clipe-a-clipe (análise visual e transcrição), o except genérico escondia bugs de programação junto com falhas esperadas de mídia/dependência — foi assim que o bug do soundfile ausente passou despercebido até um teste manual. Agora captura só (OSError, RuntimeError, ValueError, json.JSONDecodeError) na visual e (ImportError, OSError, RuntimeError, ValueError) na transcrição; erros de programação propagam e aparecem de verdade. executar_retakes.py e gerar_relatorio_de_analises.py: o except Exception no main() é uma fronteira de processo legítima (traduz qualquer falha em JSON pra tela), mantido, mas agora imprime o traceback completo no stderr antes de reportar — hoje esse traceback se perdia, sobrando só str(erro). Co-Authored-By: Claude Sonnet 5 --- code/engine/executar_retakes.py | 31 +++++++++++++++--- code/engine/executar_scanner.py | 38 +++++++++++++++++++--- code/engine/gerar_relatorio_de_analises.py | 4 ++- 3 files changed, 64 insertions(+), 9 deletions(-) diff --git a/code/engine/executar_retakes.py b/code/engine/executar_retakes.py index 0ded068..9250e99 100644 --- a/code/engine/executar_retakes.py +++ b/code/engine/executar_retakes.py @@ -33,6 +33,7 @@ from __future__ import annotations import argparse import json import sys +import traceback from dataclasses import asdict from pathlib import Path @@ -53,8 +54,12 @@ from engine.scanner.retakes import ( ClassificadorDeRetakes, CoordenadorDeRetakes, RegrasDeClassificacao, - RepositorioDeRetakes, ) +from engine.persistencia import RepositorioDeRetakesSQLite + +# Os retakes são gravados no mesmo banco das demais análises do projeto. O +# caminho é absoluto porque o painel roda este script com cwd ``code``. +CAMINHO_DO_BANCO = Path(__file__).resolve().parent.parent.parent / ".jhonny" / "analises.db" # Confiança mínima exibida por critério de confirmação da tela (§9 do guia). LIMIARES_POR_CRITERIO = { @@ -92,6 +97,23 @@ def _grupos_do_dict(grupos) -> list[dict]: return [asdict(grupo) for grupo in grupos] +def _abrir_repositorio(pasta_do_caso: Path) -> RepositorioDeRetakesSQLite: + """Abre o repositório de retakes no banco de análises do projeto. + + Os retakes ficam no mesmo banco das falas e das evidências visuais, e não + num JSON por caso: é o que permite ao agente de edição ler, numa consulta + só, a fala e o fato de ela ser a terceira tentativa da mesma frase. + + Parâmetros: + pasta_do_caso: Pasta do caso, mantida na assinatura porque o painel a + informa; o banco em si é o do projeto, não o da pasta. + + Retorna: + O repositório SQLite pronto para uso. + """ + return RepositorioDeRetakesSQLite(CAMINHO_DO_BANCO) + + def _analisar(pedido: dict, pasta_do_caso: Path) -> dict: """Executa a análise completa de retakes para o caso informado.""" configuracao = pedido.get("configuracao", {}) @@ -99,7 +121,7 @@ def _analisar(pedido: dict, pasta_do_caso: Path) -> dict: video_id = str(pedido.get("video_id") or "video") _emitir("Preparando análise", 5) - repositorio = RepositorioDeRetakes(diretoria_de_trabalho=pasta_do_caso / "retakes") + repositorio = _abrir_repositorio(pasta_do_caso) if repositorio.ja_analisado(video_id): _emitir("Reutilizando análise existente", 100) return {"reutilizado": True, @@ -134,7 +156,7 @@ def _revisar(pedido: dict, pasta_do_caso: Path) -> dict: 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") + repositorio = _abrir_repositorio(pasta_do_caso) grupo = repositorio.atualizar_status_de_revisao( video_id, grupo_id, STATUS_PADRAO_DO_PEDIDO[acao]) return {"grupos": _grupos_do_dict([grupo])} @@ -158,7 +180,8 @@ def main() -> None: 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 + except Exception as erro: # fronteira do processo: repassa a causa para a tela decidir como exibir + traceback.print_exc() # traceback completo no stderr, para não perder um bug real por trás do "erro" print(json.dumps({"evento": "erro", "mensagem": str(erro)}, ensure_ascii=False), flush=True) sys.exit(1) diff --git a/code/engine/executar_scanner.py b/code/engine/executar_scanner.py index adf5c06..c288c10 100644 --- a/code/engine/executar_scanner.py +++ b/code/engine/executar_scanner.py @@ -54,10 +54,31 @@ CAMINHO_DO_BANCO = CAMINHO_DO_CODIGO.parent / ".jhonny" / "analises.db" def nome_seguro(nome: str) -> str: + """Converte o nome de uma sequência em nome de pasta seguro no filesystem. + + Parâmetros: + nome: Nome original da sequência, como vem do Premiere. + + Retorna: + O nome sem caracteres problemáticos, ou "timeline" quando nada sobra. + """ return re.sub(r"[^A-Za-z0-9._-]+", "-", nome.strip()).strip(".-") or "timeline" def executar(entrada: Path, saida: Path) -> Path: + """Executa uma análise do Scanner conforme o pedido do painel. + + Persiste a timeline, as evidências visuais, as cenas e a transcrição no + banco de análises, e grava os relatórios JSON que o painel exibe. + + Parâmetros: + entrada: Arquivo JSON com o perfil e a timeline a analisar. + saida: Caminho de referência para a pasta onde o relatório será + escrito; o nome final é derivado do nome da sequência. + + Retorna: + O caminho do relatório JSON gerado. + """ pedido = json.loads(entrada.read_text(encoding="utf-8")) configuracao = ConfiguracaoVisualDoScanner.de_dict(pedido["perfil"]) timeline = ConversorDeTimeline().converter(pedido["timeline"]) @@ -100,11 +121,17 @@ def executar(entrada: Path, saida: Path) -> Path: gerar_relatorio_visual(clipe, resultado, arquivo, configuracao.intervalo_em_segundos) resultados.append(json.loads(arquivo.read_text(encoding="utf-8"))) # Grava no banco os fatos visuais e as cenas detectadas deste clipe. - repositorio_de_analises.registrar_evidencias_visuais( + # Substitui em vez de acrescentar: reanalisar o mesmo clipe tem de + # deixar uma cópia só de cada evidência, não empilhar execuções. + repositorio_de_analises.substituir_evidencias_visuais( timeline.identificador, clipe.identificador, resultado.evidencias) - repositorio_de_analises.registrar_cenas( + repositorio_de_analises.substituir_cenas( timeline.identificador, resultado.cenas, clipe.identificador) - except Exception as erro: + except (OSError, RuntimeError, ValueError, json.JSONDecodeError) as erro: + # Falha esperada de mídia/detector (arquivo ilegível, subprocesso do + # Apple Vision indisponível, resposta malformada): não deve descartar + # os demais clipes. Erros de programação (TypeError, AttributeError + # etc.) propagam normalmente para não serem mascarados como isso. resultados.append({"clipe": {"identificador": clipe.identificador, "nome": clipe.nome, "arquivo_original": clipe.arquivo}, "erro": str(erro)}) concluidos += peso @@ -176,7 +203,10 @@ def executar(entrada: Path, saida: Path) -> Path: "confianca": item.confianca, "falante": item.falante, "caracteristicas_acusticas": item.caracteristicas_acusticas} for item in transcricao.segmentos] - except Exception as erro: + except (ImportError, OSError, RuntimeError, ValueError) as erro: + # Dependência ausente (ex.: soundfile), modelo/áudio ilegível, ou + # falha de runtime do Whisper/pyannote: registra e segue sem + # transcrição. Erros de programação propagam normalmente. transcricao_configurada = {**transcricao_configurada, "erro": str(erro)} for faixa in audio: arquivo = f"{caso}-audio-faixa-{faixa['indice'] + 1}.json" diff --git a/code/engine/gerar_relatorio_de_analises.py b/code/engine/gerar_relatorio_de_analises.py index c89fca7..dd5803f 100644 --- a/code/engine/gerar_relatorio_de_analises.py +++ b/code/engine/gerar_relatorio_de_analises.py @@ -15,6 +15,7 @@ from __future__ import annotations import argparse import json import sys +import traceback from pathlib import Path # O painel chama este arquivo com cwd ``code/engine``; injetar ``code`` (pai do @@ -169,7 +170,8 @@ def main() -> None: args = parser.parse_args() try: relatorio = executar(args.banco, args.saida, args.video_id) - except Exception as erro: # repassa a causa para a tela decidir como exibir + except Exception as erro: # fronteira do processo: repassa a causa para a tela decidir como exibir + traceback.print_exc() # traceback completo no stderr, para não perder um bug real por trás do "erro" print(json.dumps({"evento": "erro", "mensagem": str(erro)}, ensure_ascii=False), flush=True) sys.exit(1) print(json.dumps({"evento": "concluido", "arquivo": str(args.saida),