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:
João Henrique
2026-09-09 16:57:29 -04:00
parent 245dcd8015
commit 53a8307f38
7 changed files with 154 additions and 40 deletions
@@ -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()