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: - 48 arquivos alterados - 26 novos - 19 modificados - 3 removidos 22 files changed, 658 insertions(+), 568 deletions(-) Arquivos: - AGENTS.md - admin/DEV-NOTES.md - admin/OpenCut.command - admin/commit.command - admin/deploy.command - admin/update.command - code/cep-plugin/index.html - code/cep-plugin/main.js - code/cep-plugin/styles.css - code/engine/ARQUITETURA.md - code/engine/arquitetura/README.md - code/engine/gerar_relatorio_timeline.py - code/engine/integracoes/apple_speech/apple_speech_transcriber.swift - code/engine/integracoes/apple_speech/provider_de_transcricao_apple.py - code/engine/integracoes/midia/__init__.py - code/engine/integracoes/whisper/provider_de_transcricao_local.py - code/engine/scanner/__init__.py - code/engine/scanner/coordenacao/__init__.py - code/engine/scanner/descoberta/__init__.py - code/engine/scanner/modelos.py - code/engine/scanner/transcricao_da_timeline.py - code/src/tools/discovery.ts - :memory:.ses - admin/Jhonny.command - admin/inativos/OpenCut.command - admin/inativos/update.command - code/docs/glossario-analise-emocional.md - code/docs/levantamento-apple-vision-e-apple-intelligence.md - code/docs/levantamento-ferramentas-analise-visual-local.md - code/engine/arquitetura/biblioteca-inteligente-de-videos.md - code/engine/executar_scanner.py - code/engine/integracoes/huggingface/ - code/engine/integracoes/midia/extracao_de_metadados.py - code/engine/integracoes/visual/ - code/engine/requirements-visual.txt - code/engine/scanner/configuracao_visual.py - code/engine/scanner/descoberta/descoberta_de_arquivos.py - code/engine/scanner/metadados.py - code/engine/scanner/relatorio_visual.py - code/engine/scanner/retakes/ - code/engine/scanner/visual.py - code/engine/testes/test_analise_visual_local.py - code/engine/testes/test_arquivos_e_metadados.py - code/engine/testes/test_provider_de_transcricao_apple.py - code/relatorios/analise-brools/ - code/relatorios/analise-visual/ - code/relatorios/audio/arquivos/ - code/relatorios/transcricao-timeline.md
112 lines
3.7 KiB
Python
112 lines
3.7 KiB
Python
"""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 |