feat: removido o tamanho do modelo do seletor da tela Editar vídeo
- removido o tamanho do modelo do seletor da tela Editar vídeo - acelerada a transcrição do Scanner com inferência Whisper em lote e cache de hashes - garantido um resultado separado para cada clipe de áudio selecionado no Scanner - adicionado cancelamento do processamento do Scanner pela barra de progresso - exibido estado visual próprio para Scanner cancelado na barra de progresso - reorganizada a tela Scanner em blocos separados de imagem e som Resumo: - 7 arquivos alterados - 1 novos - 6 modificados - 0 removidos 6 files changed, 92 insertions(+), 40 deletions(-) Arquivos: - .jhonny/analises.db - code/cep-plugin/index.html - code/cep-plugin/main.js - code/cep-plugin/styles.css - code/engine/integracoes/whisper/provider_de_transcricao_local.py - code/engine/scanner/transcricao_da_timeline.py - code/engine/testes/test_provider_de_transcricao_local.py
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
"""Adapta o faster-whisper para a transcrição local do Scanner."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -5,23 +7,31 @@ from ...scanner.modelos import PalavraDeTranscricao, SegmentoDeTranscricao
|
||||
|
||||
|
||||
class ProviderDeTranscricaoLocal:
|
||||
"""Provider local usando faster-whisper, sem transmitir mídia."""
|
||||
"""Executa transcrição local em lote, sem transmitir a mídia."""
|
||||
|
||||
def __init__(self, modelo: str, dispositivo: str = "cpu", tipo_de_calculo: str = "int8",
|
||||
idioma: str = "pt") -> None:
|
||||
idioma: str = "pt", tamanho_do_lote: int = 16) -> None:
|
||||
"""Carrega o modelo local e configura o tamanho do lote de inferência."""
|
||||
if tamanho_do_lote < 1:
|
||||
raise ValueError("O tamanho do lote deve ser maior que zero.")
|
||||
self.nome_do_modelo = Path(modelo).name
|
||||
if "faster-whisper-" in modelo:
|
||||
self.nome_do_modelo = modelo.split("faster-whisper-", 1)[1].split("/", 1)[0]
|
||||
from faster_whisper import WhisperModel
|
||||
from faster_whisper import BatchedInferencePipeline, WhisperModel
|
||||
self.modelo = WhisperModel(modelo, device=dispositivo, compute_type=tipo_de_calculo)
|
||||
self.pipeline = BatchedInferencePipeline(model=self.modelo)
|
||||
self.idioma = idioma
|
||||
self.tamanho_do_lote = tamanho_do_lote
|
||||
|
||||
def transcrever(self, clipe: Any) -> list[SegmentoDeTranscricao]:
|
||||
"""Transcreve o áudio com VAD, timestamps e inferência em lote."""
|
||||
arquivo = Path(clipe.arquivo)
|
||||
if not arquivo.is_file():
|
||||
raise FileNotFoundError(f"Arquivo de áudio não encontrado: {arquivo}")
|
||||
segmentos, _ = self.modelo.transcribe(str(arquivo), language=self.idioma,
|
||||
vad_filter=True, word_timestamps=True)
|
||||
segmentos, _ = self.pipeline.transcribe(
|
||||
str(arquivo), language=self.idioma, vad_filter=True,
|
||||
word_timestamps=True, batch_size=self.tamanho_do_lote,
|
||||
)
|
||||
return [SegmentoDeTranscricao(float(s.start), float(s.end), s.text.strip(),
|
||||
None, tuple(PalavraDeTranscricao(w.word.strip(), float(w.start), float(w.end),
|
||||
getattr(w, "probability", None))
|
||||
|
||||
@@ -50,24 +50,23 @@ class TranscricaoDaTimeline:
|
||||
clipes.append(clipe)
|
||||
|
||||
transcricoes: dict[str, list[SegmentoDeTranscricao]] = {}
|
||||
chaves_por_arquivo: dict[str, str] = {}
|
||||
for clipe in clipes:
|
||||
chave = self._chave_do_arquivo(clipe.arquivo)
|
||||
chave = chaves_por_arquivo.get(clipe.arquivo)
|
||||
if chave is None:
|
||||
chave = self._chave_do_arquivo(clipe.arquivo)
|
||||
chaves_por_arquivo[clipe.arquivo] = chave
|
||||
if chave not in transcricoes:
|
||||
transcricoes[chave] = self._transcrever_arquivo(clipe.arquivo, chave)
|
||||
|
||||
resultados: list[TranscricaoDoClipe] = []
|
||||
vistos: set[tuple[str, float, float, float, float]] = set()
|
||||
for clipe in clipes:
|
||||
intervalo = clipe.intervalo_na_timeline
|
||||
origem = clipe.intervalo_na_origem
|
||||
inicio_origem = origem.inicio if origem else 0.0
|
||||
fim_origem = origem.fim if origem else float("inf")
|
||||
chave = (self._chave_do_arquivo(clipe.arquivo), inicio_origem, fim_origem,
|
||||
chave = (chaves_por_arquivo[clipe.arquivo], inicio_origem, fim_origem,
|
||||
intervalo.inicio, intervalo.fim)
|
||||
# Vídeo e áudio vinculados podem representar o mesmo corte.
|
||||
if chave in vistos:
|
||||
continue
|
||||
vistos.add(chave)
|
||||
segmentos = []
|
||||
for segmento in transcricoes[chave[0]]:
|
||||
fim = min(segmento.fim, fim_origem)
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Testes do provider local de transcrição em lote."""
|
||||
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from engine.integracoes.whisper import ProviderDeTranscricaoLocal
|
||||
|
||||
|
||||
class TesteDoProviderDeTranscricaoLocal(unittest.TestCase):
|
||||
"""Verifica a configuração e a conversão dos segmentos do provider."""
|
||||
|
||||
def test_usa_pipeline_em_lote_com_vad_e_timestamps_de_palavras(self):
|
||||
"""Mantém as opções de qualidade ao usar inferência em lote."""
|
||||
chamadas = {}
|
||||
|
||||
class ModeloFalso:
|
||||
"""Representa o modelo externo durante o teste."""
|
||||
|
||||
def __init__(self, *argumentos, **opcoes):
|
||||
chamadas["modelo"] = (argumentos, opcoes)
|
||||
|
||||
class PipelineFalso:
|
||||
"""Representa o pipeline em lote durante o teste."""
|
||||
|
||||
def __init__(self, model):
|
||||
chamadas["pipeline_modelo"] = model
|
||||
|
||||
def transcribe(self, *argumentos, **opcoes):
|
||||
chamadas["transcricao"] = (argumentos, opcoes)
|
||||
palavra = SimpleNamespace(word="Olá", start=0.0, end=0.5, probability=0.99)
|
||||
segmento = SimpleNamespace(start=0.0, end=0.5, text=" Olá ", words=[palavra])
|
||||
return iter([segmento]), SimpleNamespace(duration=0.5)
|
||||
|
||||
modulo_falso = types.SimpleNamespace(
|
||||
BatchedInferencePipeline=PipelineFalso,
|
||||
WhisperModel=ModeloFalso,
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as pasta:
|
||||
arquivo = Path(pasta) / "audio.wav"
|
||||
arquivo.write_bytes(b"audio")
|
||||
with patch.dict(sys.modules, {"faster_whisper": modulo_falso}):
|
||||
provider = ProviderDeTranscricaoLocal("small", tamanho_do_lote=8)
|
||||
resultado = provider.transcrever(SimpleNamespace(arquivo=str(arquivo)))
|
||||
|
||||
self.assertEqual(resultado[0].texto, "Olá")
|
||||
self.assertEqual(chamadas["transcricao"][1]["batch_size"], 8)
|
||||
self.assertTrue(chamadas["transcricao"][1]["vad_filter"])
|
||||
self.assertTrue(chamadas["transcricao"][1]["word_timestamps"])
|
||||
|
||||
def test_rejeita_tamanho_de_lote_invalido(self):
|
||||
"""Impede configuração silenciosa de um lote inválido."""
|
||||
with self.assertRaises(ValueError):
|
||||
ProviderDeTranscricaoLocal("small", tamanho_do_lote=0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user