"""Comparação textual entre falas. Responsável pelos algoritmos de similaridade: Jaccard (conjunto de palavras), sequência (ordem das palavras via maior subsequência comum) e similaridade do início/fim da frase. Não decide nada sozinho — apenas produz métricas para o classificador. """ from __future__ import annotations import re from .modelos_de_retakes import Fala, ResultadoDaComparacao 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 _lcs(a: list[str], b: list[str]) -> int: """Tamanho da maior subsequência comum preservando a ordem.""" anterior = [0] * (len(b) + 1) for palavra_a in a: atual = [0] * (len(b) + 1) for j, palavra_b in enumerate(b): if palavra_a == palavra_b: atual[j + 1] = anterior[j] + 1 else: atual[j + 1] = max(atual[j], anterior[j + 1]) anterior = atual return anterior[-1] class ComparadorDeFalas: """Compara duas falas e calcula as métricas de similaridade textual.""" @staticmethod def normalizar(texto: str) -> str: return _normalizar(texto) @staticmethod def _palavras_de(fala: Fala) -> list[str]: return fala.texto_normalizado.split() or _normalizar(fala.texto).split() def comparar(self, fala_a: Fala, fala_b: Fala) -> ResultadoDaComparacao: palavras_a = self._palavras_de(fala_a) palavras_b = self._palavras_de(fala_b) if not palavras_a or not palavras_b: return ResultadoDaComparacao(fala_a.id, fala_b.id) # 1. Jaccard — conjunto de palavras (rápido, ótimo candidato). conjunto_a = set(palavras_a) conjunto_b = set(palavras_b) intersecao = len(conjunto_a & conjunto_b) uniao = len(conjunto_a | conjunto_b) jaccard = intersecao / uniao if uniao else 0.0 # 2. Sequência — ordem das palavras via LCS normalizada. lcs = _lcs(palavras_a, palavras_b) sequencia = lcs / max(len(palavras_a), len(palavras_b)) # 3. Início e final da frase. inicio = self._similaridade_de_prefijo(palavras_a, palavras_b) final = self._similaridade_de_sufixo(palavras_a, palavras_b) # 4. Similaridade final ponderada. fim = 0.45 * sequencia + 0.30 * jaccard + 0.15 * inicio + 0.10 * final return ResultadoDaComparacao( fala_a_id=fala_a.id, fala_b_id=fala_b.id, similaridade_jaccard=round(jaccard, 4), similaridade_de_sequencia=round(sequencia, 4), similaridade_do_inicio=round(inicio, 4), similaridade_do_final=round(final, 4), similaridade_final=round(min(1.0, fim), 4), ) @staticmethod def _similaridade_de_prefijo(a: list[str], b: list[str]) -> float: if not a or not b: return 0.0 n = 0 for x, y in zip(a, b): if x != y: break n += 1 return n / max(len(a), len(b)) @staticmethod def _similaridade_de_sufixo(a: list[str], b: list[str]) -> float: if not a or not b: return 0.0 n = 0 for x, y in zip(reversed(a), reversed(b)): if x != y: break n += 1 return n / max(len(a), len(b)) @staticmethod def prefixo_comum_em_palavras(texto_a: str, texto_b: str) -> int: a = _normalizar(texto_a).split() b = _normalizar(texto_b).split() n = 0 for x, y in zip(a, b): if x != y: break n += 1 return n