"""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 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(Fala( id=f"{segmento_id}:{segmento.inicio:.3f}", video_id=video_id, faixa_id=faixa_id, segmento_id=segmento_id, ordem=-1, # preenchida após a ordenação inicio=segmento.inicio, fim=segmento.fim, texto=segmento.texto, texto_normalizado=_normalizar(segmento.texto), palavras=segmento.palavras, falante=segmento.falante, )) 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