fix: troca except Exception genérico por exceções específicas nos scripts do Scanner
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
88f9af28cf
commit
1156619937
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user