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/
160 lines
5.4 KiB
Python
160 lines
5.4 KiB
Python
"""Converte a transcrição existente em falas ordenadas para a análise.
|
|
|
|
O módulo de retakes não realiza transcrição. Ele recebe a saída do
|
|
``TranscricaoDaTimeline`` (lista de ``TranscricaoDoClipe``) e a converte
|
|
em ``Fala``s ordenadas ao longo da timeline, preservando o vínculo com o
|
|
segmento/clipe de origem e as palavras alinhadas quando disponíveis.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Iterable
|
|
|
|
from ..transcricao_da_timeline import TranscricaoDoClipe
|
|
from .modelos_de_retakes import Fala
|
|
|
|
# Sinais de erro mais comuns vindos dos providers de transcrição.
|
|
_PALAVRAS_DE_ERRO = {"", "...", "…"}
|
|
|
|
_VOYELS = "aeiouáéíóúâêôãõàèìòù"
|
|
_CONSOANTES = "bcdfghjklmnpqrstvwxyz"
|
|
_PADRAO_SILABA = re.compile(
|
|
rf"([{_VOYELS}][^aeiouáéíóúâêôãõàèìòù]*)|([{_CONSOANTES}]+[aeiouáéíóúâêôãõàèìòù]?)",
|
|
re.IGNORECASE,
|
|
)
|
|
|
|
|
|
def _normalizar(texto: str) -> str:
|
|
"""Minúsculas, sem pontuação e sem espaços duplicados."""
|
|
sem_pontuacao = re.sub(r"[^\w\s]", " ", texto)
|
|
return " ".join(sem_pontuacao.lower().split())
|
|
|
|
|
|
def _prefixo_comum(a: list[str], b: list[str]) -> int:
|
|
n = 0
|
|
for x, y in zip(a, b):
|
|
if x != y:
|
|
break
|
|
n += 1
|
|
return n
|
|
|
|
|
|
def _sucesso_de_silabas(a: list[str], b: list[str]) -> float:
|
|
if not a or not b:
|
|
return 0.0
|
|
silabas_a = [_PADRAO_SILABA.findall(p)[0][0] for p in a]
|
|
silabas_b = [_PADRAO_SILABA.findall(p)[0][0] for p in b]
|
|
n = _prefixo_comum(silabas_a, silabas_b)
|
|
return n / max(len(silabas_a), len(silabas_b))
|
|
|
|
|
|
def _e_ruido(texto: str) -> bool:
|
|
texto_limpo = _normalizar(texto)
|
|
if texto_limpo in _PALAVRAS_DE_ERRO:
|
|
return True
|
|
# Fala em branco ou "vazia" por provedor.
|
|
return not texto_limpo
|
|
|
|
|
|
class AdaptadorDeFalas:
|
|
"""Transforma a transcrição da timeline em falas prontas para análise.
|
|
|
|
Recebe uma coleção de ``TranscricaoDoClipe`` (uma por faixa de áudio do
|
|
vídeo) e devolve as falas ordenadas por tempo. O ``video_id`` é um rótulo
|
|
informado pelo chamador; quando ausente, usa o identificador da timeline.
|
|
"""
|
|
|
|
def __init__(self, video_id: str | None = None, faixa_id: str | None = None) -> None:
|
|
self.video_id = video_id
|
|
self.faixa_id = faixa_id
|
|
|
|
@staticmethod
|
|
def normalizar(texto: str) -> str:
|
|
"""Normaliza um texto (minúsculas, sem pontuação, sem espaços duplos)."""
|
|
return _normalizar(texto)
|
|
|
|
def converter(
|
|
self,
|
|
transcricoes: Iterable[TranscricaoDoClipe],
|
|
video_id: str | None = None,
|
|
faixa_id: str | None = None,
|
|
) -> list[Fala]:
|
|
video_id = video_id or self.video_id or "video"
|
|
faixa_id = faixa_id or self.faixa_id or "faixa"
|
|
|
|
falas: list[Fala] = []
|
|
for transcricao in transcricoes:
|
|
segmento_id = transcricao.identificador_do_clipe
|
|
for segmento in transcricao.segmentos:
|
|
if _e_ruido(segmento.texto):
|
|
continue
|
|
falas.append(self._fala_de_segmento(segmento, segmento_id, video_id, faixa_id))
|
|
|
|
return self._ordenar(falas)
|
|
|
|
def converter_de_segmentos(
|
|
self,
|
|
segmentos_por_clipe: Iterable[tuple[str, Iterable[object]]],
|
|
video_id: str | None = None,
|
|
faixa_id: str | None = None,
|
|
) -> list[Fala]:
|
|
"""Converte ``(segmento_id, [SegmentoDeTranscricao])`` em falas.
|
|
|
|
Usado quando a transcrição chega no ``ContextoDeAnalise`` do scanner
|
|
já como segmentos individuais (padrão do pipeline), em vez de
|
|
``TranscricaoDoClipe``.
|
|
"""
|
|
video_id = video_id or self.video_id or "video"
|
|
faixa_id = faixa_id or self.faixa_id or "faixa"
|
|
|
|
falas: list[Fala] = []
|
|
for segmento_id, segmentos in segmentos_por_clipe:
|
|
for segmento in segmentos:
|
|
if _e_ruido(getattr(segmento, "texto", "")):
|
|
continue
|
|
falas.append(self._fala_de_segmento(segmento, segmento_id, video_id, faixa_id))
|
|
return self._ordenar(falas)
|
|
|
|
@staticmethod
|
|
def _fala_de_segmento(
|
|
segmento: object,
|
|
segmento_id: str,
|
|
video_id: str,
|
|
faixa_id: str,
|
|
) -> Fala:
|
|
texto = str(getattr(segmento, "texto", "")).strip()
|
|
inicio = float(getattr(segmento, "inicio", 0.0))
|
|
fim = float(getattr(segmento, "fim", inicio))
|
|
return Fala(
|
|
id=f"{segmento_id}:{inicio:.3f}",
|
|
video_id=video_id,
|
|
faixa_id=faixa_id,
|
|
segmento_id=segmento_id,
|
|
ordem=-1, # preenchida na ordenação
|
|
inicio=inicio,
|
|
fim=fim,
|
|
texto=texto,
|
|
texto_normalizado=_normalizar(texto),
|
|
palavras=getattr(segmento, "palavras", ()) or (),
|
|
falante=getattr(segmento, "falante", None),
|
|
)
|
|
|
|
@staticmethod
|
|
def _ordenar(falas: list[Fala]) -> list[Fala]:
|
|
falas.sort(key=lambda item: (item.inicio, item.fim))
|
|
for indice, fala in enumerate(falas):
|
|
falas[indice] = Fala(
|
|
id=fala.id,
|
|
video_id=fala.video_id,
|
|
faixa_id=fala.faixa_id,
|
|
segmento_id=fala.segmento_id,
|
|
ordem=indice,
|
|
inicio=fala.inicio,
|
|
fim=fala.fim,
|
|
texto=fala.texto,
|
|
texto_normalizado=fala.texto_normalizado,
|
|
palavras=fala.palavras,
|
|
falante=fala.falante,
|
|
)
|
|
return falas |