- corrigida a reprodução do trecho de áudio do falante na tela Refinar usando os tempos da origem do WAV. - alterado o passo de geração de JSON para concluir a edição e salvar a configuração editorial no banco. Resumo: - 9 arquivos alterados - 3 novos - 6 modificados - 0 removidos 6 files changed, 136 insertions(+), 109 deletions(-) Arquivos: - .jhonny/analises.db - code/cep-plugin/index.html - code/cep-plugin/main.js - code/engine/persistencia/__init__.py - code/engine/persistencia/esquema.py - code/engine/testes/test_persistencia_sqlite.py - code/engine/persistencia/repositorio_de_edicoes.py - code/engine/registrar_edicao.py - code/tests/cep-refinar-preview.test.ts
512 lines
19 KiB
Python
512 lines
19 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)
|
|
);
|
|
|
|
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);
|
|
|
|
-- 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], ...] = (
|
|
("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()
|