feat: copiada a skill editar-por-voz e seus critérios para as skil

- copiada a skill editar-por-voz e seus critérios para as skills do projeto
- adaptada a skill editar-por-voz para usar o banco SQLite como fonte oficial
- corrigida a cópia do JSON do tipo de vídeo para priorizar clipboard Unicode
- criado carregador único do contexto de edição por voz a partir do SQLite
- criada entrada para registrar planos no SQLite e reaproveitar o mesmo plano na aplicação
- habilitado carregamento do plano editorial diretamente do SQLite na interface
- corrigido o bloqueio visual da etapa de carregamento do plano salvo
- configurado carregamento automático do plano salvo após selecionar a sequência
- documentada a sequência como raiz de retomada do fluxo de edição
- criada a estrutura local para análise de músicas instrumentais com Essentia

Resumo:
- 22 arquivos alterados
- 11 novos
- 11 modificados
- 0 removidos

 11 files changed, 270 insertions(+), 34 deletions(-)

Arquivos:
  - .jhonny/analises.db
  - CONTEXT.md
  - code/cep-plugin/index.html
  - code/cep-plugin/main.js
  - code/engine/README.md
  - code/engine/aplicar_plano_de_edicao.py
  - code/engine/integracoes/audio/__init__.py
  - code/engine/persistencia/consultas.py
  - code/engine/requirements-audio.txt
  - code/engine/testes/test_consultas_de_persistencia.py
  - code/tests/cep-tipos-video-encoding.test.ts
  - code/.jhonny/
  - code/engine/carregar_plano_de_edicao.py
  - code/engine/integracoes/audio/contratos.py
  - code/engine/integracoes/audio/modelos_de_analise_musical.py
  - code/engine/integracoes/audio/provider_de_analise_musical_essentia.py
  - code/engine/preparar_edicao_por_voz.py
  - code/engine/registrar_plano_de_edicao.py
  - code/engine/testes/test_analisador_de_musica_essentia.py
  - code/engine/testes/test_carregar_plano_de_edicao.py
  - code/engine/testes/test_registrar_plano_de_edicao.py
  - code/plugins/premiere-pro/skills/editar-por-voz/
This commit is contained in:
João Henrique
2026-09-10 13:09:01 -04:00
parent eda1b1aadc
commit a9e5b5ff58
33 changed files with 1811 additions and 34 deletions
+99
View File
@@ -135,6 +135,105 @@ class ConsultasDeAnalises:
},
}
def consultar_contexto_da_edicao(
self,
video_id: str | None = None,
incluir_palavras: bool = True,
) -> dict | None:
"""
Carrega a edição concluída mais recente e seu contexto editorial.
Quando ``video_id`` não é informado, seleciona a edição concluída mais
recente do banco. O retorno reúne a configuração editorial, o vídeo,
os clipes, as falas e o histórico resumido de planos, para que um
agente possa iniciar uma edição sem escrever SQL nem procurar arquivos
auxiliares.
Parâmetros:
video_id: Restringe a busca a um vídeo quando informado.
incluir_palavras: Inclui palavras da transcrição para decisões
que dependem de limites precisos.
Retorna:
Contexto completo da edição mais recente, ou None quando não há
edição concluída compatível.
Pode gerar:
ErroDeConsultaInvalida: quando ``video_id`` for vazio ou a
configuração persistida não for um JSON válido.
"""
if video_id is not None:
self._validar_texto_obrigatorio("video_id", video_id)
condicao = "WHERE video_id = ?" if video_id is not None else ""
parametros = (video_id,) if video_id is not None else ()
edicao = self.conexao.execute(
f"""SELECT id, video_id, sequencia, origem, tipo_de_video,
configuracao, concluida_em
FROM edicoes_de_video
{condicao}
ORDER BY concluida_em DESC, id DESC
LIMIT 1""",
parametros,
).fetchone()
if edicao is None:
return None
try:
configuracao = json.loads(edicao["configuracao"])
except (TypeError, json.JSONDecodeError) as erro:
raise ErroDeConsultaInvalida(
f"A configuração da edição {edicao['id']} não é um JSON válido."
) from erro
if not isinstance(configuracao, dict):
raise ErroDeConsultaInvalida(
f"A configuração da edição {edicao['id']} precisa ser um objeto."
)
identificador_do_video = edicao["video_id"]
clipes = [
dict(linha) for linha in self.conexao.execute(
"""SELECT id, faixa_id, nome, inicio_na_timeline, fim_na_timeline,
inicio_na_origem, fim_na_origem, arquivo, offline
FROM clipes
WHERE video_id = ?
ORDER BY inicio_na_timeline, id""",
(identificador_do_video,),
).fetchall()
]
planos = [
dict(linha) for linha in self.conexao.execute(
"""SELECT p.id, p.origem, p.tipo_de_video, p.modelo_da_ia,
p.intencao, p.criado_em,
COUNT(a.id) AS total_de_acoes,
(SELECT COUNT(*) FROM aplicacoes_do_plano ap
WHERE ap.plano_id = p.id AND ap.sucesso = 1)
AS aplicacoes_com_sucesso
FROM planos_de_edicao p
LEFT JOIN acoes_do_plano a ON a.plano_id = p.id
WHERE p.video_id = ?
GROUP BY p.id
ORDER BY p.criado_em DESC, p.id DESC""",
(identificador_do_video,),
).fetchall()
]
return {
"edicao": {
"id": edicao["id"],
"video_id": identificador_do_video,
"sequencia": edicao["sequencia"],
"origem": edicao["origem"],
"tipo_de_video": edicao["tipo_de_video"],
"configuracao": configuracao,
"concluida_em": edicao["concluida_em"],
},
"video": self.consultar_resumo_do_video(identificador_do_video),
"analise": self.consultar_status_da_analise(identificador_do_video),
"clipes": clipes,
"falas": self.listar_falas_no_intervalo(
identificador_do_video, incluir_palavras=incluir_palavras,
),
"planos": planos,
}
def listar_intervalos_de_preview_dos_falantes(self, video_id: str) -> list[dict]:
"""
Lista os intervalos dos falantes convertidos para o arquivo de vídeo.