feat: criado repositório jhonny-editor no Gitea
criado repositório jhonny-editor no Gitea adicionado script admin/deploy.command com commit automático atualizado admin/DEV-NOTES.md com template limpo Resumo: - 4 arquivos alterados - 2 novos - 2 modificados - 0 removidos 2 files changed, 48 insertions(+) Arquivos: - .jhonny/analises.db - code/engine/persistencia/esquema.py - code/engine/analisar_trilhas.py - code/engine/persistencia/repositorio_de_trilhas.py
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,70 @@
|
|||||||
|
"""Executa a análise em lote de uma pasta de trilhas e emite progresso JSON."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
CAMINHO_DO_CODIGO = Path(__file__).resolve().parent.parent
|
||||||
|
if str(CAMINHO_DO_CODIGO) not in sys.path:
|
||||||
|
sys.path.insert(0, str(CAMINHO_DO_CODIGO))
|
||||||
|
|
||||||
|
from engine.integracoes.audio import AnalisadorDeMusicaInstrumentalEssentia, ClassificadorDeMusicaEssentia
|
||||||
|
from engine.persistencia.conexao import abrir_banco
|
||||||
|
from engine.persistencia.repositorio_de_trilhas import RepositorioDeTrilhasSQLite
|
||||||
|
|
||||||
|
EXTENSOES_DE_AUDIO = {".wav", ".mp3", ".flac", ".m4a", ".aiff", ".ogg"}
|
||||||
|
|
||||||
|
|
||||||
|
def emitir(evento: str, **dados: object) -> None:
|
||||||
|
"""Emite uma linha de progresso consumível pelo painel CEP."""
|
||||||
|
print("@@PROGRESS " + json.dumps({"evento": evento, **dados}, ensure_ascii=False), flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def executar(pasta: Path, banco: Path, diretorio_dos_modelos: Path | None) -> int:
|
||||||
|
"""Analisa todos os arquivos musicais diretamente encontrados na pasta."""
|
||||||
|
arquivos = tuple(sorted(
|
||||||
|
arquivo for arquivo in pasta.iterdir()
|
||||||
|
if arquivo.is_file() and arquivo.suffix.lower() in EXTENSOES_DE_AUDIO
|
||||||
|
))
|
||||||
|
conexao = abrir_banco(banco)
|
||||||
|
try:
|
||||||
|
repositorio = RepositorioDeTrilhasSQLite(conexao)
|
||||||
|
trilhas = repositorio.registrar_pasta_e_arquivos(pasta, arquivos)
|
||||||
|
classificador = ClassificadorDeMusicaEssentia(diretorio_dos_modelos) if diretorio_dos_modelos else None
|
||||||
|
analisador = AnalisadorDeMusicaInstrumentalEssentia(classificador=classificador)
|
||||||
|
emitir("lote_iniciado", total=len(trilhas), pasta=str(pasta))
|
||||||
|
for ordem, trilha in enumerate(trilhas, start=1):
|
||||||
|
repositorio.iniciar_analise(trilha.identificador)
|
||||||
|
emitir("trilha_iniciada", nome=trilha.nome_do_arquivo, indice=ordem, total=len(trilhas), percentual=(ordem - 1) * 100 / max(1, len(trilhas)))
|
||||||
|
try:
|
||||||
|
resultado = analisador.analisar(trilha.caminho)
|
||||||
|
repositorio.registrar_analise(trilha, resultado)
|
||||||
|
emitir("trilha_concluida", nome=trilha.nome_do_arquivo, indice=ordem, total=len(trilhas), percentual=ordem * 100 / max(1, len(trilhas)))
|
||||||
|
except (OSError, RuntimeError, ValueError) as erro:
|
||||||
|
repositorio.registrar_erro(trilha.identificador, str(erro))
|
||||||
|
emitir("trilha_com_erro", nome=trilha.nome_do_arquivo, indice=ordem, total=len(trilhas), erro=str(erro), percentual=ordem * 100 / max(1, len(trilhas)))
|
||||||
|
emitir("lote_concluido", total=len(trilhas), percentual=100)
|
||||||
|
return 0
|
||||||
|
finally:
|
||||||
|
conexao.close()
|
||||||
|
|
||||||
|
|
||||||
|
def principal() -> None:
|
||||||
|
"""Lê argumentos e executa o lote de análise."""
|
||||||
|
argumentos = argparse.ArgumentParser()
|
||||||
|
argumentos.add_argument("pasta", type=Path)
|
||||||
|
argumentos.add_argument("--banco", type=Path, default=Path(".jhonny/analises.db"))
|
||||||
|
argumentos.add_argument("--modelos", type=Path, default=None)
|
||||||
|
opcoes = argumentos.parse_args()
|
||||||
|
try:
|
||||||
|
raise SystemExit(executar(opcoes.pasta, opcoes.banco, opcoes.modelos))
|
||||||
|
except (OSError, ValueError) as erro:
|
||||||
|
emitir("lote_com_erro", erro=str(erro))
|
||||||
|
raise SystemExit(1) from erro
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
principal()
|
||||||
@@ -61,6 +61,54 @@ CREATE TABLE IF NOT EXISTS arquivos (
|
|||||||
UNIQUE (video_id, caminho)
|
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,
|
||||||
|
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 (
|
CREATE TABLE IF NOT EXISTS analises_versao (
|
||||||
video_id TEXT NOT NULL REFERENCES videos(id) ON DELETE CASCADE,
|
video_id TEXT NOT NULL REFERENCES videos(id) ON DELETE CASCADE,
|
||||||
etapa TEXT NOT NULL,
|
etapa TEXT NOT NULL,
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
"""Persistência do catálogo independente de trilhas musicais."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
from ..integracoes.audio.modelos_de_analise_musical import ResultadoDaAnaliseMusical
|
||||||
|
from .conexao import abrir_banco
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class TrilhaMusical:
|
||||||
|
"""Representa um arquivo de música catalogado, sem vínculo com vídeo."""
|
||||||
|
|
||||||
|
identificador: int
|
||||||
|
caminho: Path
|
||||||
|
nome_do_arquivo: str
|
||||||
|
status: str
|
||||||
|
erro: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class RepositorioDeTrilhasSQLite:
|
||||||
|
"""Salva pastas, trilhas e versões das análises musicais no SQLite."""
|
||||||
|
|
||||||
|
def __init__(self, banco: str | Path | sqlite3.Connection = ".jhonny/analises.db") -> None:
|
||||||
|
"""Abre o banco informado e garante que o esquema esteja disponível."""
|
||||||
|
self.conexao = banco if isinstance(banco, sqlite3.Connection) else abrir_banco(banco)
|
||||||
|
|
||||||
|
def registrar_pasta_e_arquivos(self, pasta: str | Path, arquivos: Iterable[Path]) -> list[TrilhaMusical]:
|
||||||
|
"""Registra a pasta e torna sua lista de arquivos pronta para análise."""
|
||||||
|
caminho_da_pasta = Path(pasta).expanduser().resolve(strict=True)
|
||||||
|
if not caminho_da_pasta.is_dir():
|
||||||
|
raise NotADirectoryError(f"A pasta de trilhas não existe: {caminho_da_pasta}")
|
||||||
|
arquivos_materializados = tuple(Path(arquivo).resolve() for arquivo in arquivos)
|
||||||
|
with self.conexao:
|
||||||
|
cursor = self.conexao.execute(
|
||||||
|
"""INSERT INTO pastas_de_trilhas(caminho) VALUES (?)
|
||||||
|
ON CONFLICT(caminho) DO UPDATE SET atualizada_em = strftime('%Y-%m-%dT%H:%M:%fZ','now')""",
|
||||||
|
(str(caminho_da_pasta),),
|
||||||
|
)
|
||||||
|
pasta_id = cursor.lastrowid or self.conexao.execute(
|
||||||
|
"SELECT id FROM pastas_de_trilhas WHERE caminho = ?", (str(caminho_da_pasta),)
|
||||||
|
).fetchone()[0]
|
||||||
|
resultado: list[TrilhaMusical] = []
|
||||||
|
for arquivo in arquivos_materializados:
|
||||||
|
linha = self.conexao.execute(
|
||||||
|
"""INSERT INTO trilhas_musicais(pasta_id, caminho, nome_do_arquivo, tamanho_em_bytes)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
ON CONFLICT(caminho) DO UPDATE SET nome_do_arquivo = excluded.nome_do_arquivo,
|
||||||
|
tamanho_em_bytes = excluded.tamanho_em_bytes, atualizada_em = strftime('%Y-%m-%dT%H:%M:%fZ','now')""",
|
||||||
|
(pasta_id, str(arquivo), arquivo.name, arquivo.stat().st_size),
|
||||||
|
)
|
||||||
|
identificador = linha.lastrowid or self.conexao.execute(
|
||||||
|
"SELECT id FROM trilhas_musicais WHERE caminho = ?", (str(arquivo),)
|
||||||
|
).fetchone()[0]
|
||||||
|
registro = self.conexao.execute(
|
||||||
|
"SELECT id, caminho, nome_do_arquivo, status, erro FROM trilhas_musicais WHERE id = ?",
|
||||||
|
(identificador,),
|
||||||
|
).fetchone()
|
||||||
|
resultado.append(TrilhaMusical(registro[0], Path(registro[1]), registro[2], registro[3], registro[4]))
|
||||||
|
return resultado
|
||||||
|
|
||||||
|
def iniciar_analise(self, trilha_id: int) -> None:
|
||||||
|
"""Marca uma trilha como em análise e remove erro anterior."""
|
||||||
|
with self.conexao:
|
||||||
|
self.conexao.execute(
|
||||||
|
"UPDATE trilhas_musicais SET status = 'analisando', erro = NULL, atualizada_em = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?",
|
||||||
|
(trilha_id,),
|
||||||
|
)
|
||||||
|
|
||||||
|
def registrar_analise(self, trilha: TrilhaMusical, resultado: ResultadoDaAnaliseMusical) -> None:
|
||||||
|
"""Grava descritores e etiquetas produzidos pelo áudio da trilha."""
|
||||||
|
bruto = json.dumps({"generos": resultado.generos, "humores": resultado.humores}, ensure_ascii=False)
|
||||||
|
with self.conexao:
|
||||||
|
self.conexao.execute(
|
||||||
|
"UPDATE trilhas_musicais SET hash_do_conteudo = ?, status = 'concluida', erro = NULL, atualizada_em = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?",
|
||||||
|
(self._calcular_hash(trilha.caminho), trilha.identificador),
|
||||||
|
)
|
||||||
|
cursor = self.conexao.execute(
|
||||||
|
"""INSERT INTO analises_musicais
|
||||||
|
(trilha_id, provedor, modelo, duracao_em_segundos, batidas_por_minuto, tonalidade, modo, intensidade, dancabilidade, resultado_bruto_json)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||||
|
(trilha.identificador, resultado.provedor, resultado.modelo, resultado.duracao_em_segundos,
|
||||||
|
resultado.batidas_por_minuto, resultado.tonalidade, resultado.modo, resultado.intensidade,
|
||||||
|
resultado.dancabilidade, bruto),
|
||||||
|
)
|
||||||
|
analise_id = cursor.lastrowid
|
||||||
|
for tipo, etiquetas in (("genero", resultado.pontuacoes_de_genero), ("humor", resultado.pontuacoes_de_humor)):
|
||||||
|
self.conexao.executemany(
|
||||||
|
"INSERT INTO etiquetas_musicais(analise_id, tipo, etiqueta, confianca, ordem) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
((analise_id, tipo, item.etiqueta, item.confianca, ordem) for ordem, item in enumerate(etiquetas)),
|
||||||
|
)
|
||||||
|
|
||||||
|
def registrar_erro(self, trilha_id: int, erro: str) -> None:
|
||||||
|
"""Marca uma trilha como erro sem interromper o lote inteiro."""
|
||||||
|
with self.conexao:
|
||||||
|
self.conexao.execute(
|
||||||
|
"UPDATE trilhas_musicais SET status = 'erro', erro = ?, atualizada_em = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?",
|
||||||
|
(erro[:2000], trilha_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _calcular_hash(caminho: Path) -> str:
|
||||||
|
"""Calcula SHA-256 em blocos para não carregar o áudio inteiro na memória."""
|
||||||
|
resumo = hashlib.sha256()
|
||||||
|
with caminho.open("rb") as arquivo:
|
||||||
|
for bloco in iter(lambda: arquivo.read(1024 * 1024), b""):
|
||||||
|
resumo.update(bloco)
|
||||||
|
return resumo.hexdigest()
|
||||||
Reference in New Issue
Block a user