feat: removido o tamanho do modelo do seletor da tela Editar vídeo
- 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
This commit is contained in:
@@ -8,6 +8,7 @@ from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import re
|
||||
from types import SimpleNamespace
|
||||
|
||||
# O painel chama este arquivo diretamente, portanto o Python inclui apenas
|
||||
# ``code/engine`` no caminho de importação. O pacote público fica em ``code``.
|
||||
@@ -167,7 +168,7 @@ def executar(entrada: Path, saida: Path) -> Path:
|
||||
analisador_de_metricas_de_voz=analisador_de_metricas_de_voz).executar(
|
||||
Timeline(timeline.identificador, timeline.nome, faixas=faixas_de_audio))
|
||||
# Persiste a transcrição (segmentos + falas) no banco de análises.
|
||||
repositorio_de_analises.registrar_transcricoes(timeline.identificador, transcricoes)
|
||||
repositorio_de_analises.substituir_transcricoes(timeline.identificador, transcricoes)
|
||||
for transcricao in transcricoes:
|
||||
transcricoes_por_clipe[transcricao.identificador_do_clipe] = [
|
||||
{"inicio": item.inicio, "fim": item.fim, "texto": item.texto,
|
||||
@@ -193,9 +194,98 @@ def executar(entrada: Path, saida: Path) -> Path:
|
||||
return saida
|
||||
|
||||
|
||||
def executar_complemento(entrada: Path, relatorio: Path, acao: str) -> Path:
|
||||
"""Processa locutores ou métricas usando a transcrição já persistida."""
|
||||
pedido = json.loads(entrada.read_text(encoding="utf-8"))
|
||||
dados_relatorio = json.loads(relatorio.read_text(encoding="utf-8"))
|
||||
timeline = ConversorDeTimeline().converter(pedido["timeline"])
|
||||
clipes = {clipe.identificador: clipe for faixa in timeline.faixas for clipe in faixa.clipes}
|
||||
partes_por_arquivo: dict[str, list] = {}
|
||||
diarizacao_por_parte: dict[str, list] = {}
|
||||
if acao == "diarizacao":
|
||||
if not os.environ.get("HF_TOKEN"):
|
||||
raise RuntimeError("Token do Hugging Face não configurado para identificar locutores.")
|
||||
from engine.integracoes.huggingface import ProviderDeDiarizacaoHuggingFace
|
||||
diarizador = ProviderDeDiarizacaoHuggingFace(os.environ.get("HF_DIARIZATION_MODEL", MODELO_DIARIZACAO_PADRAO))
|
||||
analisador = None
|
||||
elif acao == "metricas_de_fala":
|
||||
from engine.integracoes.audio import AnalisadorDeMetricasDeVoz
|
||||
diarizador = None
|
||||
analisador = AnalisadorDeMetricasDeVoz()
|
||||
else:
|
||||
raise ValueError(f"Complemento desconhecido: {acao}")
|
||||
|
||||
arquivos_audio = dados_relatorio["artefatos"].get("audio", [])
|
||||
repositorio_de_analises = RepositorioDeAnalisesSQLite(CAMINHO_DO_BANCO)
|
||||
total = max(1, len(arquivos_audio))
|
||||
for indice_arquivo, nome_arquivo in enumerate(arquivos_audio, 1):
|
||||
caminho_relatorio = relatorio.parent / nome_arquivo
|
||||
dados_audio = json.loads(caminho_relatorio.read_text(encoding="utf-8"))
|
||||
for grupo in dados_audio.get("segmentos_por_clipe", []):
|
||||
clipe = clipes.get(grupo.get("clipe"))
|
||||
if not clipe or not clipe.arquivo:
|
||||
continue
|
||||
caminho_origem = str(clipe.arquivo)
|
||||
if caminho_origem not in partes_por_arquivo:
|
||||
partes_por_arquivo[caminho_origem] = ExtracaoDeAudio().extrair(
|
||||
caminho_origem, relatorio.parent / ".cache-audio-complementar" / str(abs(hash(caminho_origem))))
|
||||
partes = partes_por_arquivo[caminho_origem]
|
||||
inicio_origem = clipe.intervalo_na_origem.inicio if clipe.intervalo_na_origem else 0.0
|
||||
fim_anterior = None
|
||||
for segmento in grupo.get("segmentos", []):
|
||||
inicio = inicio_origem + segmento["inicio"] - clipe.intervalo_na_timeline.inicio
|
||||
fim = inicio_origem + segmento["fim"] - clipe.intervalo_na_timeline.inicio
|
||||
parte = next((item for item in partes if item.inicio <= inicio <= item.fim), partes[-1])
|
||||
inicio_local = max(0.0, inicio - parte.inicio)
|
||||
fim_local = max(inicio_local, min(fim, parte.fim) - parte.inicio)
|
||||
chave = str(parte.caminho)
|
||||
if acao == "diarizacao":
|
||||
if chave not in diarizacao_por_parte:
|
||||
diarizacao_por_parte[chave] = diarizador.analisar(parte.caminho)
|
||||
falas = diarizacao_por_parte[chave]
|
||||
sobreposicoes = [(min(fim_local, saida) - max(inicio_local, entrada), falante)
|
||||
for entrada, saida, falante in falas
|
||||
if entrada < fim_local and saida > inicio_local]
|
||||
segmento["falante"] = max(sobreposicoes, default=(0.0, None))[1]
|
||||
repositorio_de_analises.atualizar_enriquecimento_de_transcricao(
|
||||
timeline.identificador,
|
||||
clipe.identificador,
|
||||
segmento["inicio"],
|
||||
segmento["fim"],
|
||||
falante=segmento.get("falante"),
|
||||
)
|
||||
else:
|
||||
palavras = [SimpleNamespace(inicio=0.0, fim=0.0)
|
||||
for _ in segmento.get("texto", "").split()]
|
||||
segmento["caracteristicas_acusticas"] = analisador.analisar(
|
||||
parte.caminho, inicio_local, fim_local, palavras,
|
||||
segmento["inicio"], fim_anterior)
|
||||
repositorio_de_analises.atualizar_enriquecimento_de_transcricao(
|
||||
timeline.identificador,
|
||||
clipe.identificador,
|
||||
segmento["inicio"],
|
||||
segmento["fim"],
|
||||
caracteristicas_acusticas=segmento["caracteristicas_acusticas"],
|
||||
)
|
||||
fim_anterior = segmento["fim"]
|
||||
dados_audio.setdefault("transcricao", {})["diarizacao" if acao == "diarizacao" else "metricas_de_fala"] = True
|
||||
caminho_relatorio.write_text(json.dumps(dados_audio, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(json.dumps({"evento": "progresso", "etapa": "Complementando transcrição",
|
||||
"percentual": round(100 * indice_arquivo / total, 1)}, ensure_ascii=False), flush=True)
|
||||
dados_relatorio.setdefault("perfil", {}).setdefault("transcricao", {})[
|
||||
"diarizacao" if acao == "diarizacao" else "metricas_de_fala"] = True
|
||||
dados_relatorio.write_text(json.dumps(dados_relatorio, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
return relatorio
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("entrada", type=Path)
|
||||
parser.add_argument("saida", type=Path)
|
||||
parser.add_argument("--complemento", choices=("diarizacao", "metricas_de_fala"))
|
||||
args = parser.parse_args()
|
||||
print(json.dumps({"evento": "concluido", "arquivo": str(executar(args.entrada, args.saida))}, ensure_ascii=False))
|
||||
if args.complemento:
|
||||
caminho = executar_complemento(args.entrada, args.saida, args.complemento)
|
||||
else:
|
||||
caminho = executar(args.entrada, args.saida)
|
||||
print(json.dumps({"evento": "concluido", "arquivo": str(caminho)}, ensure_ascii=False))
|
||||
|
||||
Reference in New Issue
Block a user