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
113 lines
4.1 KiB
Python
113 lines
4.1 KiB
Python
"""Comparação semântica entre falas (opcional).
|
|
|
|
A similaridade textual não cobre casos em que as palavras são diferentes
|
|
mas a ideia é a mesma. Este módulo define um protocolo para providers de
|
|
embeddings e uma implementação local (via Hugging Face Transformers), além
|
|
de um fallback puramente lexical usado quando nenhum provider está
|
|
disponível.
|
|
|
|
O `ComparadorSemantico` nunca decide sozinho que há retake — apenas
|
|
fornece uma métrica adicional para o classificador.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
from typing import Protocol
|
|
|
|
from .modelos_de_retakes import Fala
|
|
|
|
|
|
class ProviderDeSimilaridadeSemantica(Protocol):
|
|
"""Contrato para quem calcula similaridade semântica entre dois textos."""
|
|
|
|
def similaridade(self, texto_a: str, texto_b: str) -> float: ...
|
|
|
|
|
|
class ComparadorLexicalSemantico:
|
|
"""Fallback puramente lexical para quando não há embeddings.
|
|
|
|
Reconstrói uma "similaridade semântica" a partir de palavras que
|
|
compartilham a mesma raiz (stemming simples por prefixo comum) e de
|
|
sinônimos frequentes em pt-PT. Destina-se a permitir executar a análise
|
|
sem carregar modelos pesados.
|
|
"""
|
|
|
|
_PARES_SINONIMOS = (
|
|
({"mostrar", "explicar", "demonstrar", "apresentar"},),
|
|
({"sistema", "programa", "aplicativo", "software"},),
|
|
({"configurar", "configuracao", "instalar", "ajustar"},),
|
|
)
|
|
|
|
def similaridade(self, texto_a: str, texto_b: str) -> float:
|
|
palavras_a = {p for p in self._palavras(texto_a)}
|
|
palavras_b = {p for p in self._palavras(texto_b)}
|
|
if not palavras_a or not palavras_b:
|
|
return 0.0
|
|
|
|
intersecao = 0.0
|
|
for palavra_a in palavras_a:
|
|
for palavra_b in palavras_b:
|
|
if palavra_a == palavra_b:
|
|
intersecao += 1.0
|
|
elif self._mesma_raiz(palavra_a, palavra_b):
|
|
intersecao += 0.7
|
|
elif self._mesmo_sinonimo(palavra_a, palavra_b):
|
|
intersecao += 0.6
|
|
tam = max(len(palavras_a), len(palavras_b))
|
|
return round(min(1.0, intersecao / tam), 4)
|
|
|
|
@staticmethod
|
|
def _palavras(texto: str) -> list[str]:
|
|
return [p for p in texto.lower().split()]
|
|
|
|
@staticmethod
|
|
def _mesma_raiz(a: str, b: str) -> bool:
|
|
raiz = min(len(a), len(b), 4)
|
|
return raiz >= 4 and a[:raiz] == b[:raiz]
|
|
|
|
@staticmethod
|
|
def _mesmo_sinonimo(a: str, b: str) -> bool:
|
|
return any(a in grupo and b in grupo for grupo in ComparadorLexicalSemantico._PARES_SINONIMOS)
|
|
|
|
|
|
class ComparadorSemantico:
|
|
"""Calcula similaridade semântica usando um provider injetável.
|
|
|
|
Quando o provider é ``None``, cai no ``ComparadorLexicalSemantico``
|
|
para não bloquear a análise na ausência de modelos locais.
|
|
"""
|
|
|
|
def __init__(self, provider: ProviderDeSimilaridadeSemantica | None = None) -> None:
|
|
self.provider = provider or ComparadorLexicalSemantico()
|
|
|
|
def comparar(self, fala_a: Fala, fala_b: Fala) -> float:
|
|
return float(self.provider.similaridade(fala_a.texto, fala_b.texto))
|
|
|
|
|
|
class ProviderDeSimilaridadeHuggingFace:
|
|
"""Embeds os textos localmente com um modelo de embeddings.
|
|
|
|
O modelo é carregado de forma preguiçosa para não custar nada até ser
|
|
de fato necessário (zona de dúvida do classificador).
|
|
"""
|
|
|
|
def __init__(self, modelo: str = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2") -> None:
|
|
self.modelo = modelo
|
|
self._encoder = None
|
|
|
|
def similaridade(self, texto_a: str, texto_b: str) -> float:
|
|
if self._encoder is None:
|
|
from sentence_transformers import SentenceTransformer
|
|
self._encoder = SentenceTransformer(self.modelo)
|
|
embeddings = self._encoder.encode([texto_a, texto_b], normalize_embeddings=True)
|
|
return float(_cosseno(embeddings[0], embeddings[1]))
|
|
|
|
|
|
def _cosseno(a: list[float], b: list[float]) -> float:
|
|
produto = sum(x * y for x, y in zip(a, b))
|
|
norma_a = math.sqrt(sum(x * x for x in a))
|
|
norma_b = math.sqrt(sum(y * y for y in b))
|
|
if norma_a == 0 or norma_b == 0:
|
|
return 0.0
|
|
return max(0.0, min(1.0, produto / (norma_a * norma_b))) |