- 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 - criado modo explícito de análise do Scanner para separar som, imagem ou ambos - adicionadas métricas opcionais de fala ao Scanner com análise acústica na engine - corrigido o progresso da transcrição do Scanner durante o processamento do Whisper - refatorado o runner Swift do Apple Vision para executar requests em lote com retry CPU-only, corrigindo falhas de CVPixelBuffer/ANE/OCR - documentado o fluxo de edição de vídeo por voz integrado ao Scanner, banco e engine - formalizado o protocolo de edição por chat com perfil editorial e consultas progressivas - gerado plano JSON de corte para depoimento vertical da Erika no Instagram - esquematizado o fluxo de colar e executar planos da IA pela engine após o Scanner - ajustado o plano para manter somente as três etapas existentes da tela de edição - adicionado processamento incremental de locutores e métricas no Scanner com confirmação de reprocessamento - implementado o plano JSON colado na tela e a persistência idempotente das análises do Scanner Resumo: - 8 arquivos alterados - 1 novos - 7 modificados - 0 removidos 7 files changed, 437 insertions(+), 55 deletions(-) Arquivos: - code/cep-plugin/index.html - code/cep-plugin/main.js - code/cep-plugin/styles.css - code/engine/executar_scanner.py - code/engine/persistencia/repositorio_de_analises_sqlite.py - code/engine/testes/test_persistencia_sqlite.py - docs/PLANO-EDICAO-DE-VIDEO-POR-VOZ.md - code/relatorios/plano-edicao-depoimento-erika-instagram.json
196 lines
9.2 KiB
Python
196 lines
9.2 KiB
Python
"""Persistência SQLite de metadados de arquivo, transcrição e evidências visuais.
|
|
|
|
Complementa ``RepositorioDeRetakesSQLite`` gravando o restante do que o
|
|
``ContextoDeAnalise`` do pipeline produz e que hoje só vive em memória
|
|
(metadados técnicos) ou em cache JSON por arquivo (transcrição).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sqlite3
|
|
from pathlib import Path
|
|
from typing import Iterable
|
|
|
|
from ..integracoes.midia.extracao_de_metadados import MetadadosDoArquivo
|
|
from ..scanner.modelos import Cena, EvidenciaVisual, PalavraDeTranscricao, SegmentoDeTranscricao
|
|
from ..scanner.transcricao_da_timeline import TranscricaoDoClipe
|
|
from .conexao import abrir_banco
|
|
|
|
|
|
class RepositorioDeAnalisesSQLite:
|
|
"""Grava metadados de arquivo, transcrição e evidências/cenas visuais."""
|
|
|
|
def __init__(self, banco: str | Path | sqlite3.Connection = ".jhonny/analises.db") -> None:
|
|
self.conexao = banco if isinstance(banco, sqlite3.Connection) else abrir_banco(banco)
|
|
|
|
def _garantir_video(self, video_id: str) -> None:
|
|
self.conexao.execute("INSERT OR IGNORE INTO videos (id) VALUES (?)", (video_id,))
|
|
|
|
def registrar_metadados_de_arquivo(
|
|
self, video_id: str, metadados: MetadadosDoArquivo, hash_do_conteudo: str | None = None,
|
|
) -> None:
|
|
with self.conexao:
|
|
self._garantir_video(video_id)
|
|
self.conexao.execute(
|
|
"""INSERT INTO arquivos
|
|
(video_id, caminho, formato, duracao, codec_de_video, codec_de_audio,
|
|
largura, altura, taxa_de_quadros, canais_de_audio, taxa_de_amostragem,
|
|
tamanho_em_bytes, orientacao, timecode, hash_do_conteudo)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT (video_id, caminho) DO UPDATE SET
|
|
formato = excluded.formato, duracao = excluded.duracao,
|
|
codec_de_video = excluded.codec_de_video,
|
|
codec_de_audio = excluded.codec_de_audio,
|
|
largura = excluded.largura, altura = excluded.altura,
|
|
taxa_de_quadros = excluded.taxa_de_quadros,
|
|
canais_de_audio = excluded.canais_de_audio,
|
|
taxa_de_amostragem = excluded.taxa_de_amostragem,
|
|
tamanho_em_bytes = excluded.tamanho_em_bytes,
|
|
orientacao = excluded.orientacao, timecode = excluded.timecode,
|
|
hash_do_conteudo = excluded.hash_do_conteudo""",
|
|
(video_id, str(metadados.caminho), metadados.formato, metadados.duracao,
|
|
metadados.codec_de_video, metadados.codec_de_audio, metadados.largura,
|
|
metadados.altura, metadados.taxa_de_quadros, metadados.canais_de_audio,
|
|
metadados.taxa_de_amostragem, metadados.tamanho_em_bytes,
|
|
metadados.orientacao, metadados.timecode, hash_do_conteudo),
|
|
)
|
|
|
|
def registrar_transcricoes(
|
|
self, video_id: str, transcricoes: Iterable[TranscricaoDoClipe],
|
|
) -> None:
|
|
with self.conexao:
|
|
self._garantir_video(video_id)
|
|
for transcricao in transcricoes:
|
|
for segmento in transcricao.segmentos:
|
|
self._inserir_segmento(video_id, transcricao.identificador_do_clipe, segmento)
|
|
|
|
def substituir_transcricoes(
|
|
self, video_id: str, transcricoes: Iterable[TranscricaoDoClipe],
|
|
) -> None:
|
|
"""Substitui a transcrição somente dos clipes recebidos.
|
|
|
|
O método torna o reprocessamento idempotente sem apagar análises de
|
|
outros clipes ou faixas que não participaram da execução atual.
|
|
"""
|
|
with self.conexao:
|
|
self._garantir_video(video_id)
|
|
transcricoes_materializadas = tuple(transcricoes)
|
|
for transcricao in transcricoes_materializadas:
|
|
self.conexao.execute(
|
|
"""DELETE FROM segmentos_de_transcricao
|
|
WHERE video_id = ? AND clipe_id = ?""",
|
|
(video_id, transcricao.identificador_do_clipe),
|
|
)
|
|
for segmento in transcricao.segmentos:
|
|
self._inserir_segmento(video_id, transcricao.identificador_do_clipe, segmento)
|
|
|
|
def atualizar_enriquecimento_de_transcricao(
|
|
self,
|
|
video_id: str,
|
|
clipe_id: str,
|
|
inicio: float,
|
|
fim: float,
|
|
*,
|
|
falante: str | None = None,
|
|
caracteristicas_acusticas: dict[str, object] | None = None,
|
|
) -> None:
|
|
"""Atualiza diarização e métricas de um segmento já persistido."""
|
|
campos = []
|
|
parametros: list[object] = []
|
|
if falante is not None:
|
|
campos.append("falante = ?")
|
|
parametros.append(falante)
|
|
if caracteristicas_acusticas is not None:
|
|
campos.append("caracteristicas_acusticas = ?")
|
|
parametros.append(json.dumps(caracteristicas_acusticas, ensure_ascii=False))
|
|
if not campos:
|
|
return
|
|
parametros.extend((video_id, clipe_id, inicio, fim))
|
|
with self.conexao:
|
|
self.conexao.execute(
|
|
f"""UPDATE segmentos_de_transcricao
|
|
SET {', '.join(campos)}
|
|
WHERE video_id = ? AND clipe_id = ? AND inicio = ? AND fim = ?""",
|
|
parametros,
|
|
)
|
|
|
|
def _inserir_segmento(
|
|
self, video_id: str, clipe_id: str, segmento: SegmentoDeTranscricao,
|
|
) -> int:
|
|
cursor = self.conexao.execute(
|
|
"""INSERT INTO segmentos_de_transcricao
|
|
(video_id, clipe_id, inicio, fim, texto, confianca, falante, voz_aparente,
|
|
confianca_voz, emocao, confianca_emocao, caracteristicas_acusticas)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
(video_id, clipe_id, segmento.inicio, segmento.fim, segmento.texto,
|
|
segmento.confianca, segmento.falante, segmento.voz_aparente,
|
|
segmento.confianca_voz, segmento.emocao, segmento.confianca_emocao,
|
|
json.dumps(segmento.caracteristicas_acusticas, ensure_ascii=False)),
|
|
)
|
|
segmento_id = cursor.lastrowid
|
|
for ordem, palavra in enumerate(segmento.palavras):
|
|
self.conexao.execute(
|
|
"""INSERT INTO palavras_de_transcricao
|
|
(segmento_id, ordem, texto, inicio, fim, confianca, falante)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
(segmento_id, ordem, palavra.texto, palavra.inicio, palavra.fim,
|
|
palavra.confianca, palavra.falante),
|
|
)
|
|
return segmento_id
|
|
|
|
def carregar_transcricoes(self, video_id: str, clipe_id: str) -> list[SegmentoDeTranscricao]:
|
|
segmentos: list[SegmentoDeTranscricao] = []
|
|
for linha in self.conexao.execute(
|
|
"""SELECT * FROM segmentos_de_transcricao
|
|
WHERE video_id = ? AND clipe_id = ? ORDER BY inicio""",
|
|
(video_id, clipe_id),
|
|
).fetchall():
|
|
palavras = tuple(
|
|
PalavraDeTranscricao(
|
|
texto=item["texto"], inicio=item["inicio"], fim=item["fim"],
|
|
confianca=item["confianca"], falante=item["falante"],
|
|
)
|
|
for item in self.conexao.execute(
|
|
"SELECT * FROM palavras_de_transcricao WHERE segmento_id = ? ORDER BY ordem",
|
|
(linha["id"],),
|
|
).fetchall()
|
|
)
|
|
segmentos.append(SegmentoDeTranscricao(
|
|
inicio=linha["inicio"], fim=linha["fim"], texto=linha["texto"],
|
|
confianca=linha["confianca"], palavras=palavras,
|
|
emocao=linha["emocao"], confianca_emocao=linha["confianca_emocao"],
|
|
caracteristicas_acusticas=json.loads(linha["caracteristicas_acusticas"] or "{}"),
|
|
falante=linha["falante"], voz_aparente=linha["voz_aparente"],
|
|
confianca_voz=linha["confianca_voz"],
|
|
))
|
|
return segmentos
|
|
|
|
def registrar_evidencias_visuais(
|
|
self, video_id: str, clipe_id: str, evidencias: Iterable[EvidenciaVisual],
|
|
) -> None:
|
|
with self.conexao:
|
|
self._garantir_video(video_id)
|
|
for evidencia in evidencias:
|
|
self.conexao.execute(
|
|
"""INSERT INTO evidencias_visuais
|
|
(video_id, clipe_id, tipo, inicio, fim, valor, confianca, provider, modelo)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
(video_id, clipe_id, evidencia.tipo, evidencia.inicio, evidencia.fim,
|
|
json.dumps(evidencia.valor, ensure_ascii=False), evidencia.confianca,
|
|
evidencia.provider, evidencia.modelo),
|
|
)
|
|
|
|
def registrar_cenas(
|
|
self, video_id: str, cenas: Iterable[Cena], clipe_id: str | None = None,
|
|
) -> None:
|
|
with self.conexao:
|
|
self._garantir_video(video_id)
|
|
for cena in cenas:
|
|
self.conexao.execute(
|
|
"""INSERT INTO cenas (video_id, clipe_id, inicio, fim, confianca, referencias)
|
|
VALUES (?, ?, ?, ?, ?, ?)""",
|
|
(video_id, clipe_id, cena.inicio, cena.fim, cena.confianca,
|
|
json.dumps(list(cena.referencias))),
|
|
)
|