Files
João Henrique 2c9bdf2ff0 feat: criado monitoramento contínuo de trilhas com varredura recur
- criado monitoramento contínuo de trilhas com varredura recursiva e baixa prioridade
- gerado e validado plano editorial revisável do depoimento da Erika sem aplicação na timeline
- adicionada leitura e comparação estrutural da timeline após aplicar planos de edição
- limitadas as classificações de trilhas às duas maiores probabilidades de cada categoria
- limpo o catálogo musical para reiniciar as análises sem remover dados de vídeo

Resumo:
- 34 arquivos alterados
- 6 novos
- 28 modificados
- 0 removidos

 28 files changed, 833 insertions(+), 75 deletions(-)

Arquivos:
  - .jhonny/analises.db
  - code/cep-plugin/index.html
  - code/cep-plugin/main.js
  - code/engine/analisar_trilhas.py
  - code/engine/aplicar_plano_de_edicao.py
  - code/engine/consultar_detalhes_de_trilha.py
  - code/engine/editor/__init__.py
  - code/engine/editor/aplicador_de_plano_de_edicao.py
  - code/engine/editor/backup_de_sequencia.py
  - code/engine/editor/escrita/escrita_no_editor.py
  - code/engine/editor/modelos.py
  - code/engine/editor/validacao_semantica.py
  - code/engine/integracoes/audio/classificador_de_musica_essentia.py
  - code/engine/integracoes/audio/modelos_de_analise_musical.py
  - code/engine/integracoes/audio/provider_de_analise_musical_essentia.py
  - code/engine/persistencia/esquema.py
  - code/engine/persistencia/repositorio_de_trilhas.py
  - code/engine/testes/duplos_de_premiere.py
  - code/engine/testes/test_aplicador_de_plano_de_edicao.py
  - code/engine/testes/test_aplicar_plano_de_edicao.py
  - code/engine/testes/test_escrita_no_editor.py
  - code/engine/testes/test_leitor_de_plano_de_edicao.py
  - code/engine/testes/test_validador_semantico_de_plano.py
  - code/plugins/premiere-pro/skills/editar-por-voz/SKILL.md
  - code/plugins/premiere-pro/skills/editar-por-voz/criterios/08-formato-de-saida.md
  - code/plugins/premiere-pro/skills/editar-por-voz/criterios/10-revisao-humana.md
  - code/src/tools/timeline.ts
  - code/tests/tools/tool-modules.test.ts
  - .jhonny/planos/
  - code/.jhonny/analises.db-shm
  - code/.jhonny/analises.db-wal
  - code/engine/editor/verificador_da_aplicacao.py
  - code/engine/testes/test_verificador_da_aplicacao.py
  - code/plugins/premiere-pro/skills/editar-por-voz/criterios/11-gancho.md
2026-09-10 15:09:27 -04:00

592 lines
22 KiB
Python

"""DDL do banco SQLite de análises (vídeo, imagem e áudio)."""
from __future__ import annotations
import sqlite3
_DDL = """
CREATE TABLE IF NOT EXISTS videos (
id TEXT PRIMARY KEY,
nome TEXT,
duracao REAL,
taxa_de_quadros REAL,
largura INTEGER,
altura INTEGER,
criado_em TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
);
CREATE TABLE IF NOT EXISTS faixas (
id TEXT NOT NULL,
video_id TEXT NOT NULL REFERENCES videos(id) ON DELETE CASCADE,
nome TEXT,
tipo TEXT,
indice INTEGER,
PRIMARY KEY (video_id, id)
);
CREATE TABLE IF NOT EXISTS clipes (
id TEXT NOT NULL,
video_id TEXT NOT NULL REFERENCES videos(id) ON DELETE CASCADE,
faixa_id TEXT NOT NULL,
nome TEXT,
inicio_na_timeline REAL NOT NULL,
fim_na_timeline REAL NOT NULL,
inicio_na_origem REAL,
fim_na_origem REAL,
arquivo TEXT,
offline INTEGER NOT NULL DEFAULT 0,
metadados TEXT,
PRIMARY KEY (video_id, id),
FOREIGN KEY (video_id, faixa_id) REFERENCES faixas(video_id, id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_clipes_faixa ON clipes(video_id, faixa_id);
CREATE TABLE IF NOT EXISTS arquivos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
video_id TEXT REFERENCES videos(id) ON DELETE CASCADE,
caminho TEXT NOT NULL,
formato TEXT,
duracao REAL,
codec_de_video TEXT,
codec_de_audio TEXT,
largura INTEGER,
altura INTEGER,
taxa_de_quadros REAL,
canais_de_audio INTEGER,
taxa_de_amostragem INTEGER,
tamanho_em_bytes INTEGER,
orientacao TEXT,
timecode TEXT,
hash_do_conteudo TEXT,
UNIQUE (video_id, caminho)
);
-- Catálogo independente de músicas para uso como trilha de fundo.
CREATE TABLE IF NOT EXISTS pastas_de_trilhas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
caminho TEXT NOT NULL UNIQUE,
criada_em TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
atualizada_em TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
);
CREATE TABLE IF NOT EXISTS trilhas_musicais (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pasta_id INTEGER NOT NULL REFERENCES pastas_de_trilhas(id) ON DELETE CASCADE,
caminho TEXT NOT NULL UNIQUE,
nome_do_arquivo TEXT NOT NULL,
hash_do_conteudo TEXT,
tamanho_em_bytes INTEGER,
status TEXT NOT NULL DEFAULT 'aguardando',
erro TEXT,
criada_em TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
atualizada_em TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
);
CREATE INDEX IF NOT EXISTS idx_trilhas_musicais_pasta ON trilhas_musicais(pasta_id);
CREATE TABLE IF NOT EXISTS analises_musicais (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trilha_id INTEGER NOT NULL REFERENCES trilhas_musicais(id) ON DELETE CASCADE,
provedor TEXT NOT NULL,
modelo TEXT,
duracao_em_segundos REAL,
batidas_por_minuto REAL,
tonalidade TEXT,
modo TEXT,
intensidade REAL,
dancabilidade REAL,
temas_json TEXT,
instrumentos_json TEXT,
etiquetas_json TEXT,
resultado_bruto_json TEXT,
criada_em TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
);
CREATE INDEX IF NOT EXISTS idx_analises_musicais_trilha ON analises_musicais(trilha_id);
CREATE TABLE IF NOT EXISTS etiquetas_musicais (
id INTEGER PRIMARY KEY AUTOINCREMENT,
analise_id INTEGER NOT NULL REFERENCES analises_musicais(id) ON DELETE CASCADE,
tipo TEXT NOT NULL,
etiqueta TEXT NOT NULL,
confianca REAL NOT NULL,
ordem INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_etiquetas_musicais_analise ON etiquetas_musicais(analise_id);
CREATE TABLE IF NOT EXISTS analises_versao (
video_id TEXT NOT NULL REFERENCES videos(id) ON DELETE CASCADE,
etapa TEXT NOT NULL,
hash_versao TEXT NOT NULL,
concluido_em TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
PRIMARY KEY (video_id, etapa)
);
CREATE TABLE IF NOT EXISTS segmentos_de_transcricao (
id INTEGER PRIMARY KEY AUTOINCREMENT,
video_id TEXT NOT NULL REFERENCES videos(id) ON DELETE CASCADE,
clipe_id TEXT NOT NULL,
inicio REAL NOT NULL,
fim REAL NOT NULL,
texto TEXT NOT NULL,
confianca REAL,
falante TEXT,
voz_aparente TEXT,
confianca_voz REAL,
emocao TEXT,
confianca_emocao REAL,
caracteristicas_acusticas TEXT,
energia_rms REAL,
pitch_mediano_hz REAL,
pitch_desvio_hz REAL,
velocidade_de_fala_pps REAL,
maior_pausa_interna_s REAL,
intervalo_anterior_s REAL
);
CREATE INDEX IF NOT EXISTS idx_segmentos_video_clipe
ON segmentos_de_transcricao(video_id, clipe_id);
-- Consulta temporal: "quais falas entre X e Y segundos". É o acesso mais
-- usado pelo agente de edição, por isso tem índice próprio.
CREATE INDEX IF NOT EXISTS idx_segmentos_intervalo
ON segmentos_de_transcricao(video_id, inicio, fim);
CREATE INDEX IF NOT EXISTS idx_segmentos_falante
ON segmentos_de_transcricao(video_id, falante);
CREATE TABLE IF NOT EXISTS palavras_de_transcricao (
id INTEGER PRIMARY KEY AUTOINCREMENT,
segmento_id INTEGER NOT NULL REFERENCES segmentos_de_transcricao(id) ON DELETE CASCADE,
ordem INTEGER NOT NULL,
texto TEXT NOT NULL,
inicio REAL NOT NULL,
fim REAL NOT NULL,
confianca REAL,
falante TEXT
);
CREATE INDEX IF NOT EXISTS idx_palavras_segmento ON palavras_de_transcricao(segmento_id);
CREATE TABLE IF NOT EXISTS evidencias_visuais (
id INTEGER PRIMARY KEY AUTOINCREMENT,
video_id TEXT NOT NULL REFERENCES videos(id) ON DELETE CASCADE,
clipe_id TEXT NOT NULL,
tipo TEXT NOT NULL,
inicio REAL NOT NULL,
fim REAL NOT NULL,
valor TEXT NOT NULL,
confianca REAL,
provider TEXT NOT NULL,
modelo TEXT
);
CREATE INDEX IF NOT EXISTS idx_evidencias_video_clipe
ON evidencias_visuais(video_id, clipe_id);
CREATE TABLE IF NOT EXISTS cenas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
video_id TEXT NOT NULL REFERENCES videos(id) ON DELETE CASCADE,
clipe_id TEXT,
inicio REAL NOT NULL,
fim REAL NOT NULL,
confianca REAL,
referencias TEXT
);
CREATE INDEX IF NOT EXISTS idx_cenas_video ON cenas(video_id);
CREATE TABLE IF NOT EXISTS grupos_de_retake (
id TEXT PRIMARY KEY,
video_id TEXT NOT NULL REFERENCES videos(id) ON DELETE CASCADE,
faixa_id TEXT NOT NULL,
tipo TEXT NOT NULL,
confianca REAL NOT NULL,
criado_em TEXT NOT NULL,
status_de_revisao TEXT NOT NULL DEFAULT 'pendente'
);
CREATE INDEX IF NOT EXISTS idx_grupos_video ON grupos_de_retake(video_id);
CREATE TABLE IF NOT EXISTS tomadas_de_retake (
id INTEGER PRIMARY KEY AUTOINCREMENT,
grupo_id TEXT NOT NULL REFERENCES grupos_de_retake(id) ON DELETE CASCADE,
ordem INTEGER NOT NULL,
fala_id TEXT NOT NULL,
segmento_id TEXT NOT NULL,
inicio REAL NOT NULL,
fim REAL NOT NULL,
texto TEXT NOT NULL,
similaridade_com_anterior REAL NOT NULL,
confianca REAL NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_tomadas_grupo ON tomadas_de_retake(grupo_id);
CREATE TABLE IF NOT EXISTS evidencias_de_retake (
id INTEGER PRIMARY KEY AUTOINCREMENT,
grupo_id TEXT NOT NULL REFERENCES grupos_de_retake(id) ON DELETE CASCADE,
tipo TEXT NOT NULL,
descricao TEXT NOT NULL,
valor REAL
);
CREATE INDEX IF NOT EXISTS idx_evidencias_retake_grupo ON evidencias_de_retake(grupo_id);
-- ---------------------------------------------------------------------------
-- Busca lexical (FTS5)
-- ---------------------------------------------------------------------------
-- Índice de texto completo sobre as falas. Usa ``content=`` (external content)
-- para não duplicar o texto: o FTS5 guarda só o índice invertido e lê o texto
-- da tabela original pelo rowid. ``remove_diacritics 2`` faz "elegancia"
-- encontrar "elegância", que é o comportamento esperado em pt-BR.
CREATE VIRTUAL TABLE IF NOT EXISTS busca_de_falas USING fts5(
texto,
content='segmentos_de_transcricao',
content_rowid='id',
tokenize="unicode61 remove_diacritics 2"
);
CREATE TRIGGER IF NOT EXISTS trg_busca_de_falas_inserir
AFTER INSERT ON segmentos_de_transcricao BEGIN
INSERT INTO busca_de_falas(rowid, texto) VALUES (new.id, new.texto);
END;
CREATE TRIGGER IF NOT EXISTS trg_busca_de_falas_remover
AFTER DELETE ON segmentos_de_transcricao BEGIN
INSERT INTO busca_de_falas(busca_de_falas, rowid, texto)
VALUES ('delete', old.id, old.texto);
END;
CREATE TRIGGER IF NOT EXISTS trg_busca_de_falas_atualizar
AFTER UPDATE OF texto ON segmentos_de_transcricao BEGIN
INSERT INTO busca_de_falas(busca_de_falas, rowid, texto)
VALUES ('delete', old.id, old.texto);
INSERT INTO busca_de_falas(rowid, texto) VALUES (new.id, new.texto);
END;
-- ---------------------------------------------------------------------------
-- Busca semântica (enunciados + embeddings)
-- ---------------------------------------------------------------------------
-- Uma fala do Whisper tem 3 a 8 segundos: curta demais para gerar um embedding
-- com significado estável. O enunciado agrupa falas vizinhas do mesmo falante
-- num bloco de dezenas de segundos, que é a unidade embedável. O vínculo com
-- as falas de origem é preservado em ``falas_do_enunciado`` para que todo
-- acerto semântico volte com timecode utilizável para um corte.
CREATE TABLE IF NOT EXISTS enunciados (
id INTEGER PRIMARY KEY AUTOINCREMENT,
video_id TEXT NOT NULL REFERENCES videos(id) ON DELETE CASCADE,
clipe_id TEXT NOT NULL,
inicio REAL NOT NULL,
fim REAL NOT NULL,
texto TEXT NOT NULL,
falante TEXT,
total_de_falas INTEGER NOT NULL,
criado_em TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
);
CREATE INDEX IF NOT EXISTS idx_enunciados_video ON enunciados(video_id, inicio);
CREATE TABLE IF NOT EXISTS falas_do_enunciado (
enunciado_id INTEGER NOT NULL REFERENCES enunciados(id) ON DELETE CASCADE,
segmento_id INTEGER NOT NULL REFERENCES segmentos_de_transcricao(id) ON DELETE CASCADE,
ordem INTEGER NOT NULL,
PRIMARY KEY (enunciado_id, segmento_id)
);
CREATE INDEX IF NOT EXISTS idx_falas_do_enunciado_segmento
ON falas_do_enunciado(segmento_id);
CREATE TABLE IF NOT EXISTS embeddings_de_enunciado (
enunciado_id INTEGER PRIMARY KEY REFERENCES enunciados(id) ON DELETE CASCADE,
modelo TEXT NOT NULL,
dimensoes INTEGER NOT NULL,
vetor BLOB NOT NULL,
gerado_em TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
);
-- ---------------------------------------------------------------------------
-- Planos de edição
-- ---------------------------------------------------------------------------
-- O plano que a IA devolve é a única evidência de como uma edição foi
-- decidida, e antes disto ele vivia num arquivo temporário que sumia depois de
-- aplicado. Guardá-lo é o que permite comparar o que foi proposto com o que o
-- editor manteve — o único sinal disponível para aprender o estilo de corte.
CREATE TABLE IF NOT EXISTS planos_de_edicao (
id INTEGER PRIMARY KEY AUTOINCREMENT,
video_id TEXT REFERENCES videos(id) ON DELETE CASCADE,
origem TEXT NOT NULL,
tipo_de_video TEXT,
modelo_da_ia TEXT,
intencao TEXT,
criado_em TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
);
CREATE INDEX IF NOT EXISTS idx_planos_video ON planos_de_edicao(video_id, criado_em);
CREATE TABLE IF NOT EXISTS acoes_do_plano (
id INTEGER PRIMARY KEY AUTOINCREMENT,
plano_id INTEGER NOT NULL REFERENCES planos_de_edicao(id) ON DELETE CASCADE,
ordem INTEGER NOT NULL,
tipo TEXT NOT NULL,
inicio REAL,
fim REAL,
motivo TEXT,
-- Cada tipo de ação tem parâmetros próprios e incompatíveis entre si (um
-- zoom tem escala, um texto tem conteúdo e posição). É carga polimórfica
-- de verdade, consumida inteira por quem aplica: por isso fica em JSON.
parametros TEXT
);
CREATE INDEX IF NOT EXISTS idx_acoes_do_plano ON acoes_do_plano(plano_id, ordem);
CREATE TABLE IF NOT EXISTS aplicacoes_do_plano (
id INTEGER PRIMARY KEY AUTOINCREMENT,
plano_id INTEGER NOT NULL REFERENCES planos_de_edicao(id) ON DELETE CASCADE,
sequencia TEXT,
aplicado_em TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
sucesso INTEGER NOT NULL,
acoes_aplicadas INTEGER NOT NULL DEFAULT 0,
mensagem TEXT
);
CREATE INDEX IF NOT EXISTS idx_aplicacoes_plano ON aplicacoes_do_plano(plano_id);
-- Um projeto representa exatamente uma sequência, mas pode selecionar vários
-- arquivos, faixas e clipes pertencentes a ela.
CREATE TABLE IF NOT EXISTS projetos_de_edicao (
id INTEGER PRIMARY KEY AUTOINCREMENT,
video_id TEXT NOT NULL REFERENCES videos(id) ON DELETE CASCADE,
nome TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'aberto',
criado_em TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
atualizado_em TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
);
CREATE INDEX IF NOT EXISTS idx_projetos_video ON projetos_de_edicao(video_id, atualizado_em);
CREATE TABLE IF NOT EXISTS faixas_do_projeto (
projeto_id INTEGER NOT NULL REFERENCES projetos_de_edicao(id) ON DELETE CASCADE,
faixa_id TEXT NOT NULL,
selecionada INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY (projeto_id, faixa_id)
);
CREATE TABLE IF NOT EXISTS clipes_do_projeto (
projeto_id INTEGER NOT NULL REFERENCES projetos_de_edicao(id) ON DELETE CASCADE,
clipe_id TEXT NOT NULL,
selecionado INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY (projeto_id, clipe_id)
);
-- Configuração editorial escolhida no painel, antes de existir um plano da IA.
-- A análise continua normalizada nas tabelas do Scanner; esta tabela guarda
-- somente a decisão do usuário e suas opções para a edição.
CREATE TABLE IF NOT EXISTS edicoes_de_video (
id INTEGER PRIMARY KEY AUTOINCREMENT,
video_id TEXT NOT NULL REFERENCES videos(id) ON DELETE CASCADE,
sequencia TEXT NOT NULL,
origem TEXT,
tipo_de_video TEXT NOT NULL,
configuracao TEXT NOT NULL,
concluida_em TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
);
CREATE INDEX IF NOT EXISTS idx_edicoes_video ON edicoes_de_video(video_id, concluida_em);
-- ---------------------------------------------------------------------------
-- Leituras achatadas
-- ---------------------------------------------------------------------------
-- As tabelas acima guardam cada fato uma única vez, no nível em que ele é
-- verdadeiro: a emoção e o falante valem para a frase, os tempos exatos valem
-- para a palavra. As views abaixo reapresentam esses mesmos dados já juntos,
-- para que o agente de edição leia tudo numa consulta só sem que o banco
-- precise repetir valor em disco. Normalizado para gravar, achatado para ler.
-- Uma linha por fala, com as três análises de áudio e o resumo das palavras.
CREATE VIEW IF NOT EXISTS vw_falas_completas AS
SELECT
s.id AS fala_id,
s.video_id,
s.clipe_id,
s.inicio,
s.fim,
s.fim - s.inicio AS duracao,
s.texto,
s.falante,
s.emocao,
s.confianca_emocao,
s.energia_rms,
s.pitch_mediano_hz,
s.pitch_desvio_hz,
s.velocidade_de_fala_pps,
s.maior_pausa_interna_s,
s.intervalo_anterior_s,
count(p.id) AS total_de_palavras
FROM segmentos_de_transcricao s
LEFT JOIN palavras_de_transcricao p ON p.segmento_id = s.id
GROUP BY s.id;
-- Uma linha por palavra, repetindo o que vale para a frase inteira. É a forma
-- mais plana possível de ler a transcrição — sem custo de duplicação em disco,
-- porque a repetição acontece na leitura e não na gravação.
CREATE VIEW IF NOT EXISTS vw_palavras_completas AS
SELECT
p.id AS palavra_id,
p.segmento_id AS fala_id,
s.video_id,
s.clipe_id,
p.ordem,
p.texto AS palavra,
p.inicio AS palavra_inicio,
p.fim AS palavra_fim,
p.confianca AS palavra_confianca,
s.texto AS frase,
s.inicio AS frase_inicio,
s.fim AS frase_fim,
s.falante,
s.emocao,
s.confianca_emocao,
s.energia_rms,
s.pitch_mediano_hz,
s.velocidade_de_fala_pps
FROM palavras_de_transcricao p
JOIN segmentos_de_transcricao s ON s.id = p.segmento_id;
-- Linha do tempo unificada: fala e imagem no mesmo eixo, para percorrer o
-- vídeo em ordem cronológica lendo áudio e imagem juntos. A geometria bruta
-- (landmarks, bounding boxes) fica de fora de propósito: o que decide corte é
-- o fato editorial, e a geometria continua disponível em evidencias_visuais
-- para quem precisar dela.
CREATE VIEW IF NOT EXISTS vw_linha_do_tempo AS
SELECT video_id, clipe_id, inicio, fim, 'fala' AS tipo,
texto AS descricao, falante AS detalhe, confianca_emocao AS confianca
FROM segmentos_de_transcricao
UNION ALL
SELECT video_id, clipe_id, inicio, fim, 'cena' AS tipo,
'mudança de cena' AS descricao, NULL AS detalhe, confianca
FROM cenas
UNION ALL
SELECT video_id, clipe_id, inicio, fim, tipo,
json_extract(valor, '$.identificador') AS descricao,
NULL AS detalhe, confianca
FROM evidencias_visuais
WHERE tipo = 'categoria'
UNION ALL
SELECT video_id, clipe_id, inicio, fim, tipo,
json_extract(valor, '$.texto') AS descricao,
NULL AS detalhe, confianca
FROM evidencias_visuais
WHERE tipo = 'interpretacao_editorial';
-- Cada fala com o que estava em quadro enquanto ela era dita. O vínculo é por
-- sobreposição de tempo, não por chave estrangeira: fala e imagem são medidas
-- em grades diferentes (a fala é um intervalo, a imagem é amostrada por
-- quadro) e nunca coincidem exatamente. As categorias visuais entram
-- agregadas, e não uma linha por amostra, para a resposta caber num prompt.
CREATE VIEW IF NOT EXISTS vw_falas_com_visual AS
SELECT
f.fala_id,
f.video_id,
f.clipe_id,
f.inicio,
f.fim,
f.texto,
f.falante,
f.emocao,
f.energia_rms,
f.velocidade_de_fala_pps,
(SELECT group_concat(DISTINCT json_extract(e.valor, '$.identificador'))
FROM evidencias_visuais e
WHERE e.video_id = f.video_id
AND e.tipo = 'categoria' AND e.confianca >= 0.5
AND e.inicio BETWEEN f.inicio AND f.fim) AS categorias_em_quadro,
(SELECT count(*)
FROM evidencias_visuais e
WHERE e.video_id = f.video_id
AND e.tipo = 'rosto'
AND e.inicio BETWEEN f.inicio AND f.fim) AS amostras_com_rosto,
(SELECT round(avg(json_extract(e.valor, '$.score_global')), 3)
FROM evidencias_visuais e
WHERE e.video_id = f.video_id
AND e.tipo = 'estetica'
AND e.inicio BETWEEN f.inicio AND f.fim) AS estetica_media,
(SELECT count(*)
FROM cenas c
WHERE c.video_id = f.video_id
AND c.inicio > f.inicio AND c.inicio < f.fim) AS cortes_de_cena_dentro
FROM vw_falas_completas f;
"""
# Colunas acrescentadas depois da primeira versão do esquema. ``CREATE TABLE IF
# NOT EXISTS`` não altera tabela existente, então cada uma é aplicada por
# ``_migrar_colunas`` em bancos que já foram criados.
# Views recriadas a cada abertura do banco. "CREATE VIEW IF NOT EXISTS" não
# atualiza a definição de uma view já existente, então uma correção na
# consulta de uma view só chegaria a bancos novos sem isso — o banco de
# produção ficaria preso para sempre na primeira versão que foi criada nele.
_VIEWS = (
"vw_falas_completas",
"vw_palavras_completas",
"vw_linha_do_tempo",
"vw_falas_com_visual",
)
_COLUNAS_ACRESCENTADAS: tuple[tuple[str, str, str], ...] = (
("analises_musicais", "temas_json", "TEXT"),
("analises_musicais", "instrumentos_json", "TEXT"),
("analises_musicais", "etiquetas_json", "TEXT"),
("segmentos_de_transcricao", "energia_rms", "REAL"),
("segmentos_de_transcricao", "pitch_mediano_hz", "REAL"),
("segmentos_de_transcricao", "pitch_desvio_hz", "REAL"),
("segmentos_de_transcricao", "velocidade_de_fala_pps", "REAL"),
("segmentos_de_transcricao", "maior_pausa_interna_s", "REAL"),
("segmentos_de_transcricao", "intervalo_anterior_s", "REAL"),
("grupos_de_retake", "status_de_revisao", "TEXT NOT NULL DEFAULT 'pendente'"),
)
class ErroDeEsquema(RuntimeError):
"""Falha ao criar ou migrar o esquema do banco de análises."""
def _colunas_existentes(conexao: sqlite3.Connection, tabela: str) -> set[str]:
"""Lê os nomes de coluna já presentes numa tabela."""
return {linha[1] for linha in conexao.execute(f"PRAGMA table_info({tabela})")}
def _migrar_colunas(conexao: sqlite3.Connection) -> None:
"""Acrescenta colunas novas em bancos criados por versões anteriores."""
for tabela, coluna, tipo in _COLUNAS_ACRESCENTADAS:
if not _colunas_existentes(conexao, tabela):
continue
if coluna in _colunas_existentes(conexao, tabela):
continue
conexao.execute(f"ALTER TABLE {tabela} ADD COLUMN {coluna} {tipo}")
def reconstruir_indice_de_busca(conexao: sqlite3.Connection) -> int:
"""
Reindexa do zero a busca lexical a partir das falas já persistidas.
Necessário em bancos criados antes de ``busca_de_falas`` existir: os
gatilhos só cobrem escritas futuras, então as falas antigas precisam ser
carregadas uma vez.
Parâmetros:
conexao: Conexão SQLite já aberta e com o esquema aplicado.
Retorna:
A quantidade de falas presentes no índice depois da reconstrução.
"""
with conexao:
conexao.execute("INSERT INTO busca_de_falas(busca_de_falas) VALUES ('rebuild')")
return int(conexao.execute("SELECT count(*) FROM busca_de_falas").fetchone()[0])
def criar_esquema(conexao: sqlite3.Connection) -> None:
"""
Cria (de forma idempotente) todas as tabelas do banco de análises.
Aplica também as migrações de coluna necessárias em bancos criados por
versões anteriores do esquema, para que abrir um banco antigo baste para
deixá-lo no formato atual.
Parâmetros:
conexao: Conexão SQLite aberta onde o esquema será aplicado.
Pode gerar:
ErroDeEsquema: quando o SQLite recusa a DDL — tipicamente por falta da
extensão FTS5 na build em uso.
"""
try:
_migrar_colunas(conexao)
for view in _VIEWS:
conexao.execute(f"DROP VIEW IF EXISTS {view}")
conexao.executescript(_DDL)
except sqlite3.OperationalError as erro:
raise ErroDeEsquema(f"Não foi possível aplicar o esquema de análises: {erro}") from erro
conexao.commit()