feat: corrigido o KeyError 'totalTracks' no cliente MCP: extração
- corrigido o KeyError 'totalTracks' no cliente MCP: extração de structuredContent.data na resposta do servidor (SDK 2.0+) - sincronizado cortes de vídeo e áudio usando razor_all_tracks em vez de split_clip por faixa, eliminando drift entre cortes - atualizado hook para usar razor_all_tracks, isolando vídeo e áudio no mesmo instante - adicionado simulador razor_all_tracks no dublê de teste Resumo: - 5 arquivos alterados - 0 novos - 5 modificados - 0 removidos 5 files changed, 264 insertions(+), 12 deletions(-) Arquivos: - .jhonny/analises.db - code/engine/editor/aplicador_de_plano_de_edicao.py - code/engine/editor/escrita/escrita_no_editor.py - code/engine/integracoes/visual/apple_vision_runner.swift - code/engine/testes/duplos_de_premiere.py
This commit is contained in:
Binary file not shown.
@@ -22,6 +22,9 @@ from .modelos import AcaoDeEdicao, PlanoDeEdicao, TipoDeAcao
|
|||||||
_TOLERANCIA_DE_BORDA_EM_SEGUNDOS = 0.05
|
_TOLERANCIA_DE_BORDA_EM_SEGUNDOS = 0.05
|
||||||
_TOLERANCIA_DE_DURACAO_EM_SEGUNDOS = 0.3
|
_TOLERANCIA_DE_DURACAO_EM_SEGUNDOS = 0.3
|
||||||
_RAIO_DE_BUSCA_DA_BORDA_EM_SEGUNDOS = 3.0
|
_RAIO_DE_BUSCA_DA_BORDA_EM_SEGUNDOS = 3.0
|
||||||
|
# Tolerância adicional para encontrar clipes cuja origem foi deslocada pelo
|
||||||
|
# drift do razor_all_tracks (drift observado no bridge real: até ~0.5s).
|
||||||
|
_TOLERANCIA_DE_ORIGEM_EM_SEGUNDOS = 0.5
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -190,14 +193,38 @@ class AplicadorDePlanoDeEdicao:
|
|||||||
``inPoint``/``outPoint``) só falharia depois de outra faixa já ter
|
``inPoint``/``outPoint``) só falharia depois de outra faixa já ter
|
||||||
sido cortada — deixando vídeo e áudio dessincronizados mesmo com o
|
sido cortada — deixando vídeo e áudio dessincronizados mesmo com o
|
||||||
corte "reportado" como falho.
|
corte "reportado" como falho.
|
||||||
|
|
||||||
|
Usa ``razor_all_tracks`` para cortar vídeo e áudio no mesmo instante,
|
||||||
|
evitando drift entre chamadas separadas de ``split_clip``.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
faixas = self._timeline_atual().faixas
|
faixas = self._timeline_atual().faixas
|
||||||
for faixa in faixas:
|
for faixa in faixas:
|
||||||
self._clipes_do_arquivo(faixa, arquivo_de_origem)
|
self._clipes_do_arquivo(faixa, arquivo_de_origem)
|
||||||
|
|
||||||
|
faixas_afetadas = [
|
||||||
|
faixa for faixa in faixas
|
||||||
|
if any(
|
||||||
|
self._sobrepoe(clipe, acao.inicio, acao.fim)
|
||||||
|
for clipe in self._clipes_do_arquivo(faixa, arquivo_de_origem)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
if not faixas_afetadas:
|
||||||
|
raise ErroDeMapeamentoDeTempo(
|
||||||
|
f"Nenhum clipe de {arquivo_de_origem!r} sobrepõe o intervalo {acao.inicio}-{acao.fim}s de origem."
|
||||||
|
)
|
||||||
|
|
||||||
|
pontos_de_corte = self._calcular_pontos_de_corte(faixas_afetadas, acao, arquivo_de_origem)
|
||||||
|
for ponto in pontos_de_corte:
|
||||||
|
self.escrita.dividir_todas_as_faixas(ponto)
|
||||||
|
|
||||||
|
faixas = self._timeline_atual().faixas
|
||||||
|
for faixa_id in {f.identificador for f in faixas_afetadas}:
|
||||||
|
faixa_atualizada = next(f for f in faixas if f.identificador == faixa_id)
|
||||||
|
self._validar_bordas_do_razor(faixa_atualizada, acao, arquivo_de_origem)
|
||||||
|
|
||||||
alvos_por_faixa = {
|
alvos_por_faixa = {
|
||||||
faixa.identificador: self._preparar_alvos_da_faixa(faixa, acao, arquivo_de_origem)
|
faixa.identificador: self._encontrar_alvos_pos_corte(faixa, acao, arquivo_de_origem)
|
||||||
for faixa in faixas
|
for faixa in faixas
|
||||||
}
|
}
|
||||||
trechos_removidos = self._remover_alvos_coordenados(faixas, alvos_por_faixa)
|
trechos_removidos = self._remover_alvos_coordenados(faixas, alvos_por_faixa)
|
||||||
@@ -209,6 +236,95 @@ class AplicadorDePlanoDeEdicao:
|
|||||||
except ErroDeEdicao as erro:
|
except ErroDeEdicao as erro:
|
||||||
return ResultadoDaAcao(acao, False, str(erro))
|
return ResultadoDaAcao(acao, False, str(erro))
|
||||||
|
|
||||||
|
def _calcular_pontos_de_corte(
|
||||||
|
self, faixas_afetadas: list[Faixa], acao: AcaoDeEdicao, arquivo_de_origem: str
|
||||||
|
) -> list[float]:
|
||||||
|
"""Calcula os pontos de timeline onde o razor deve ser aplicado.
|
||||||
|
|
||||||
|
Devolve uma lista ordenada de instantes de timeline (em segundos)
|
||||||
|
correspondentes às bordas de entrada e saída do corte, mapeados a
|
||||||
|
partir do intervalo de origem. Os pontos são deduplicados e
|
||||||
|
ordenados do fim para o começo para que o ``razor_all_tracks``
|
||||||
|
não desloque clipes entre um corte e o próximo.
|
||||||
|
"""
|
||||||
|
pontos: set[float] = set()
|
||||||
|
for faixa in faixas_afetadas:
|
||||||
|
clipes = self._clipes_do_arquivo(faixa, arquivo_de_origem)
|
||||||
|
afetados = [clipe for clipe in clipes if self._sobrepoe(clipe, acao.inicio, acao.fim)]
|
||||||
|
for clipe in afetados:
|
||||||
|
if acao.inicio > clipe.intervalo_na_origem.inicio:
|
||||||
|
pontos.add(self.mapeador.instante_na_timeline(clipe, acao.inicio))
|
||||||
|
if acao.fim < clipe.intervalo_na_origem.fim:
|
||||||
|
pontos.add(self.mapeador.instante_na_timeline(clipe, acao.fim))
|
||||||
|
return sorted(pontos, reverse=True)
|
||||||
|
|
||||||
|
def _validar_bordas_do_razor(
|
||||||
|
self, faixa: Faixa, acao: AcaoDeEdicao, arquivo_de_origem: str
|
||||||
|
) -> None:
|
||||||
|
"""Verifica que o razor criou bordas perto dos pontos esperados.
|
||||||
|
|
||||||
|
Levanta :class:`ErroDeMapeamentoDeTempo` se nenhuma borda de clipe
|
||||||
|
estiver dentro de ``_RAIO_DE_BUSCA_DA_BORDA_EM_SEGUNDOS`` de um dos
|
||||||
|
pontos de corte esperados — nesse caso o drift é grande demais para
|
||||||
|
confiar no resultado.
|
||||||
|
"""
|
||||||
|
clipes = self._clipes_do_arquivo(faixa, arquivo_de_origem)
|
||||||
|
afetados = [clipe for clipe in clipes if self._sobrepoe(clipe, acao.inicio, acao.fim)]
|
||||||
|
if not afetados:
|
||||||
|
return
|
||||||
|
bordas_esperadas: list[float] = []
|
||||||
|
primeiro, ultimo = afetados[0], afetados[-1]
|
||||||
|
if acao.inicio > primeiro.intervalo_na_origem.inicio:
|
||||||
|
bordas_esperadas.append(self._borda_de_entrada(primeiro, acao.inicio))
|
||||||
|
if acao.fim < ultimo.intervalo_na_origem.fim:
|
||||||
|
bordas_esperadas.append(self._borda_de_saida(ultimo, acao.fim))
|
||||||
|
todas_bordas = {
|
||||||
|
clipe.intervalo_na_timeline.inicio for clipe in clipes
|
||||||
|
} | {
|
||||||
|
clipe.intervalo_na_timeline.fim for clipe in clipes
|
||||||
|
}
|
||||||
|
for esperada in bordas_esperadas:
|
||||||
|
mais_proxima = min(todas_bordas, key=lambda b: abs(b - esperada), default=None)
|
||||||
|
if mais_proxima is None or abs(mais_proxima - esperada) > _RAIO_DE_BUSCA_DA_BORDA_EM_SEGUNDOS:
|
||||||
|
raise ErroDeMapeamentoDeTempo(
|
||||||
|
f"Nenhuma borda de clipe foi encontrada perto de {esperada:.3f}s "
|
||||||
|
f"(raio de busca: {_RAIO_DE_BUSCA_DA_BORDA_EM_SEGUNDOS}s); o corte não foi aplicado."
|
||||||
|
)
|
||||||
|
|
||||||
|
def _encontrar_alvos_pos_corte(
|
||||||
|
self, faixa: Faixa, acao: AcaoDeEdicao, arquivo_de_origem: str
|
||||||
|
) -> list[Clipe]:
|
||||||
|
"""Encontra os clipes de uma faixa que ficam entre as bordas do corte.
|
||||||
|
|
||||||
|
Chamado depois que ``razor_all_tracks`` já dividiu todas as faixas;
|
||||||
|
não repete nenhuma divisão. Usa o intervalo de origem para encontrar
|
||||||
|
clipes cuja origem está contida no intervalo do corte, já que o drift
|
||||||
|
do razor pode deslocar as bordas de timeline. Valida que a duração
|
||||||
|
total dos clipes encontrados é compatível com a sobreposição de
|
||||||
|
origem esperada.
|
||||||
|
"""
|
||||||
|
clipes = self._clipes_do_arquivo(faixa, arquivo_de_origem)
|
||||||
|
afetados = [clipe for clipe in clipes if self._sobrepoe(clipe, acao.inicio, acao.fim)]
|
||||||
|
if not afetados:
|
||||||
|
return []
|
||||||
|
|
||||||
|
alvo = [
|
||||||
|
clipe for clipe in afetados
|
||||||
|
if clipe.intervalo_na_origem.inicio >= acao.inicio - _TOLERANCIA_DE_ORIGEM_EM_SEGUNDOS
|
||||||
|
and clipe.intervalo_na_origem.fim <= acao.fim + _TOLERANCIA_DE_ORIGEM_EM_SEGUNDOS
|
||||||
|
]
|
||||||
|
duracao_removida = sum(clipe.intervalo_na_timeline.duracao for clipe in alvo)
|
||||||
|
duracao_esperada = sum(
|
||||||
|
self._sobreposicao_em_segundos(clipe, acao.inicio, acao.fim) for clipe in afetados
|
||||||
|
)
|
||||||
|
if abs(duracao_removida - duracao_esperada) > _TOLERANCIA_DE_DURACAO_EM_SEGUNDOS:
|
||||||
|
raise ErroDeMapeamentoDeTempo(
|
||||||
|
f"Na faixa {faixa.identificador!r}, o corte em {acao.inicio:.3f}-{acao.fim:.3f}s "
|
||||||
|
f"não isolou o trecho esperado (esperado {duracao_esperada:.3f}s, "
|
||||||
|
f"encontrado {duracao_removida:.3f}s); nada foi removido para evitar apagar o trecho errado."
|
||||||
|
)
|
||||||
|
return alvo
|
||||||
|
|
||||||
def _aplicar_gancho(self, acao: AcaoDeEdicao, arquivo_de_origem: str) -> ResultadoDaAcao:
|
def _aplicar_gancho(self, acao: AcaoDeEdicao, arquivo_de_origem: str) -> ResultadoDaAcao:
|
||||||
"""Recorta uma cópia de um par simples e a insere na abertura."""
|
"""Recorta uma cópia de um par simples e a insere na abertura."""
|
||||||
try:
|
try:
|
||||||
@@ -264,20 +380,41 @@ class AplicadorDePlanoDeEdicao:
|
|||||||
|
|
||||||
# A ferramenta de gancho do bridge exige um par já isolado: ela lê
|
# A ferramenta de gancho do bridge exige um par já isolado: ela lê
|
||||||
# o in/out do próprio clipe, não recebe limites de origem à
|
# o in/out do próprio clipe, não recebe limites de origem à
|
||||||
# parte. Por isso, ao contrário do fluxo antigo (que passava o
|
# parte. Usa ``razor_all_tracks`` para dividir vídeo e áudio
|
||||||
# clipe inteiro que continha o trecho), aqui é preciso dividir
|
# no mesmo instante, evitando drift entre chamadas separadas.
|
||||||
# cada faixa nas bordas do gancho antes de duplicar — o mesmo
|
pontos_para_cortar: set[float] = set()
|
||||||
# padrão de isolamento que ``_preparar_alvos_da_faixa`` usa para
|
for faixa in faixas_afetadas:
|
||||||
# cortes.
|
clipes = self._clipes_do_arquivo(faixa, arquivo_de_origem)
|
||||||
|
afetados = [clipe for clipe in clipes if self._sobrepoe(clipe, acao.inicio, acao.fim)]
|
||||||
|
for clipe in afetados:
|
||||||
|
if acao.inicio > clipe.intervalo_na_origem.inicio:
|
||||||
|
pontos_para_cortar.add(self.mapeador.instante_na_timeline(clipe, acao.inicio))
|
||||||
|
if acao.fim < clipe.intervalo_na_origem.fim:
|
||||||
|
pontos_para_cortar.add(self.mapeador.instante_na_timeline(clipe, acao.fim))
|
||||||
|
for ponto in sorted(pontos_para_cortar, reverse=True):
|
||||||
|
self.escrita.dividir_todas_as_faixas(ponto)
|
||||||
|
|
||||||
|
faixas = self._timeline_atual().faixas
|
||||||
|
faixas_afetadas = [
|
||||||
|
faixa for faixa in faixas
|
||||||
|
if any(
|
||||||
|
self._sobrepoe(clipe, acao.inicio, acao.fim)
|
||||||
|
for clipe in self._clipes_do_arquivo(faixa, arquivo_de_origem)
|
||||||
|
)
|
||||||
|
]
|
||||||
alvos: list[Clipe] = []
|
alvos: list[Clipe] = []
|
||||||
for faixa in faixas_afetadas:
|
for faixa in faixas_afetadas:
|
||||||
inicio_real = self._dividir_e_conferir(acao.inicio, faixa, arquivo_de_origem)
|
clipes = self._clipes_do_arquivo(faixa, arquivo_de_origem)
|
||||||
fim_real = self._dividir_e_conferir(acao.fim, faixa, arquivo_de_origem)
|
afetados = [clipe for clipe in clipes if self._sobrepoe(clipe, acao.inicio, acao.fim)]
|
||||||
clipes_apos_a_divisao = self._clipes_do_arquivo(self._faixa_atualizada(faixa), arquivo_de_origem)
|
if not afetados:
|
||||||
|
continue
|
||||||
|
primeiro, ultimo = afetados[0], afetados[-1]
|
||||||
|
inicio_na_timeline = self._borda_de_entrada(primeiro, acao.inicio)
|
||||||
|
fim_na_timeline = self._borda_de_saida(ultimo, acao.fim)
|
||||||
isolados = [
|
isolados = [
|
||||||
clipe for clipe in clipes_apos_a_divisao
|
clipe for clipe in clipes
|
||||||
if inicio_real - _TOLERANCIA_DE_BORDA_EM_SEGUNDOS <= clipe.intervalo_na_timeline.inicio
|
if inicio_na_timeline - _TOLERANCIA_DE_BORDA_EM_SEGUNDOS <= clipe.intervalo_na_timeline.inicio
|
||||||
and clipe.intervalo_na_timeline.fim <= fim_real + _TOLERANCIA_DE_BORDA_EM_SEGUNDOS
|
and clipe.intervalo_na_timeline.fim <= fim_na_timeline + _TOLERANCIA_DE_BORDA_EM_SEGUNDOS
|
||||||
]
|
]
|
||||||
if len(isolados) != 1:
|
if len(isolados) != 1:
|
||||||
raise ErroDeMapeamentoDeTempo(
|
raise ErroDeMapeamentoDeTempo(
|
||||||
|
|||||||
@@ -79,6 +79,23 @@ class EscritaNoEditor:
|
|||||||
f"em {instante_da_timeline:.3f}s: {erro}"
|
f"em {instante_da_timeline:.3f}s: {erro}"
|
||||||
) from erro
|
) from erro
|
||||||
|
|
||||||
|
def dividir_todas_as_faixas(self, instante_da_timeline: float) -> None:
|
||||||
|
"""Corta todas as faixas de vídeo e áudio no mesmo instante.
|
||||||
|
|
||||||
|
Usa ``razor_all_tracks`` para garantir que vídeo e áudio sejam
|
||||||
|
divididos no mesmo ponto de tempo, evitando drift entre chamadas
|
||||||
|
separadas de ``split_clip``.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.cliente_mcp.chamar(
|
||||||
|
"razor_all_tracks",
|
||||||
|
{"time_seconds": instante_da_timeline, "track_type": "both"},
|
||||||
|
)
|
||||||
|
except ErroDeFerramentaMCP as erro:
|
||||||
|
raise ErroDeEscritaNoEditor(
|
||||||
|
f"Não foi possível cortar todas as faixas em {instante_da_timeline:.3f}s: {erro}"
|
||||||
|
) from erro
|
||||||
|
|
||||||
def remover_trecho(self, identificador_do_clipe: str) -> None:
|
def remover_trecho(self, identificador_do_clipe: str) -> None:
|
||||||
"""Remove o clipe da timeline fechando o espaço que ele ocupava.
|
"""Remove o clipe da timeline fechando o espaço que ele ocupava.
|
||||||
|
|
||||||
|
|||||||
@@ -560,7 +560,64 @@ func descreverEvidencia(_ evidencia: Evidencia) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Classificação de ambiente extraída das evidências visuais.
|
||||||
|
/// Usado pelo Foundation Models para categorizar o cenário do frame.
|
||||||
|
struct ClassificacaoAmbiente: Encodable {
|
||||||
|
let ambiente: String
|
||||||
|
let confianca: Double
|
||||||
|
let evidencias_chave: [String]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Taxonomia de ambientes para classificação editorial.
|
||||||
|
/// Cada ambiente tem descrição de características visuais que o Foundation Models deve procurar.
|
||||||
|
enum TaxonomiaAmbientes {
|
||||||
|
static let instrucoes = """
|
||||||
|
Você identifica o ambiente de cenas de vídeo. Analise as evidências visuais fornecidas \
|
||||||
|
e classifique o ambiente segundo a taxonomia abaixo. Responda APENAS com JSON válido.
|
||||||
|
|
||||||
|
TAXONOMIA DE AMBIENTES:
|
||||||
|
- centro_cirurgico: pessoas com touca/avental/máscara/camisinha cirúrgica, cores branco/azul cirúrgico, \
|
||||||
|
mesa cirúrgica, luzes de sala de operação, monitores cardíacos, instrumentos cirúrgicos, campo estéril verde/azul.
|
||||||
|
- consultorio: mobiliário médico simples, mesa de exame, quadro na parede, equipamento clínico de escritório.
|
||||||
|
- hospital_ambiente: corredor hospitalar, leito, roupa de paciente, enfermeiros, soro, monitor.
|
||||||
|
- laboratorio: microscópio, tubos de ensaio, centroides, bancada com reagentes, jaleco branco.
|
||||||
|
- escritorio: mesa de trabalho, computador, cadeira ergonômica, estantes, iluminação artificial.
|
||||||
|
- sala_reuniao: mesa comprida, cadeiras ao redor, projetor/tela, quadro branco.
|
||||||
|
- externo_urbano: rua, edifícios, trânsito, calçada, céu aberto.
|
||||||
|
- externo_natural: árvores, gramado, água, montanha, praia, campo.
|
||||||
|
- studio: fundo neutro/escuro, iluminação profissional, equipamento de gravação.
|
||||||
|
- industria: maquinário, linha de produção, capacete, colete, ambiente fabril.
|
||||||
|
- domesticos: sala, cozinha, quarto, mobiliário residencial, iluminação caseira.
|
||||||
|
- academia: equipamentos de musculação, esteira, peso, pessoa se exercitando.
|
||||||
|
- outro: qualquer ambiente que não se encaixe nas categorias acima.
|
||||||
|
|
||||||
|
REGRAS:
|
||||||
|
1. Analise as evidências visuais (pessoas, roupas, objetos, texto OCR, categorias Vision).
|
||||||
|
2. Identifique os 2-3 indícios mais fortes para a classificação.
|
||||||
|
3. Atribua confiança de 0.0 a 1.0 baseada na força dos indícios.
|
||||||
|
4. Se houver texto OCR relevante (placas, placas de identificação), use como evidência forte.
|
||||||
|
5. Não invente informações não presentes nas evidências.
|
||||||
|
"""
|
||||||
|
|
||||||
|
static let promptClassificar = """
|
||||||
|
Classifique o ambiente deste frame de vídeo com base nas evidências visuais fornecidas.
|
||||||
|
Responda APENAS com JSON no formato: {"ambiente": "categoria", "confianca": 0.0-1.0, "evidencias_chave": ["evidência 1", "evidência 2"]}
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
func interpretar(_ saida: Saida) async -> String? {
|
func interpretar(_ saida: Saida) async -> String? {
|
||||||
|
guard SystemLanguageModel.default.isAvailable else { return nil }
|
||||||
|
let fatos = saida.evidencias.map(descreverEvidencia).joined(separator: "; ")
|
||||||
|
guard !fatos.isEmpty else { return nil }
|
||||||
|
do {
|
||||||
|
let sessao = LanguageModelSession(instructions: TaxonomiaAmbientes.instrucoes)
|
||||||
|
let resposta = try await sessao.respond(to: "\(TaxonomiaAmbientes.promptClassificar)\n\nEvidências visuais: \(fatos)")
|
||||||
|
return resposta.content
|
||||||
|
} catch { return nil }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Interpretação editorial legível (frase curta) — mantida para compatibilidade.
|
||||||
|
func interpretarFrase(_ saida: Saida) async -> String? {
|
||||||
guard SystemLanguageModel.default.isAvailable else { return nil }
|
guard SystemLanguageModel.default.isAvailable else { return nil }
|
||||||
let fatos = saida.evidencias.map(descreverEvidencia).joined(separator: "; ")
|
let fatos = saida.evidencias.map(descreverEvidencia).joined(separator: "; ")
|
||||||
guard !fatos.isEmpty else { return nil }
|
guard !fatos.isEmpty else { return nil }
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ class ClienteMCPFalso(ClienteMCP):
|
|||||||
"add_track": self._simular_add_track,
|
"add_track": self._simular_add_track,
|
||||||
"set_playhead_position": self._simular_set_playhead_position,
|
"set_playhead_position": self._simular_set_playhead_position,
|
||||||
"add_adjustment_layer": self._simular_add_adjustment_layer,
|
"add_adjustment_layer": self._simular_add_adjustment_layer,
|
||||||
|
"razor_all_tracks": self._simular_razor_all_tracks,
|
||||||
"get_clip_at_position": self._simular_get_clip_at_position,
|
"get_clip_at_position": self._simular_get_clip_at_position,
|
||||||
"trim_clip": self._simular_trim_clip,
|
"trim_clip": self._simular_trim_clip,
|
||||||
"move_clip": self._simular_move_clip,
|
"move_clip": self._simular_move_clip,
|
||||||
@@ -223,6 +224,46 @@ class ClienteMCPFalso(ClienteMCP):
|
|||||||
f"No clip strictly spans {instante}s on track {identificador_da_faixa}."
|
f"No clip strictly spans {instante}s on track {identificador_da_faixa}."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _simular_razor_all_tracks(self, argumentos: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Divide todos os clipes de todas as faixas no instante informado.
|
||||||
|
|
||||||
|
Reproduz o ``razor_all_tracks`` do bridge real: corta todas as faixas
|
||||||
|
de vídeo e/ou áudio no mesmo ponto de tempo, garantindo
|
||||||
|
sincronização entre elas.
|
||||||
|
"""
|
||||||
|
instante_pedido = float(argumentos["time_seconds"])
|
||||||
|
track_type = argumentos.get("track_type", "both")
|
||||||
|
cortados = 0
|
||||||
|
self._salvar_estado()
|
||||||
|
for identificador_da_faixa, faixa in self._faixas.items():
|
||||||
|
if track_type == "video" and not identificador_da_faixa.startswith("video_"):
|
||||||
|
continue
|
||||||
|
if track_type == "audio" and not identificador_da_faixa.startswith("audio_"):
|
||||||
|
continue
|
||||||
|
for posicao, clipe in enumerate(faixa):
|
||||||
|
if clipe.inicio_na_timeline < instante_pedido < clipe.fim_na_timeline:
|
||||||
|
instante = min(
|
||||||
|
max(instante_pedido + self.desvio_do_split, clipe.inicio_na_timeline + 0.001),
|
||||||
|
clipe.fim_na_timeline - 0.001,
|
||||||
|
)
|
||||||
|
deslocamento = instante - clipe.inicio_na_timeline
|
||||||
|
self._proximo_id += 1
|
||||||
|
parte_esquerda = _ClipeFalso(
|
||||||
|
f"{clipe.identificador}_esq{self._proximo_id}", clipe.nome,
|
||||||
|
clipe.inicio_na_timeline, instante,
|
||||||
|
clipe.inicio_na_origem, clipe.inicio_na_origem + deslocamento,
|
||||||
|
)
|
||||||
|
self._proximo_id += 1
|
||||||
|
parte_direita = _ClipeFalso(
|
||||||
|
f"{clipe.identificador}_dir{self._proximo_id}", clipe.nome,
|
||||||
|
instante, clipe.fim_na_timeline,
|
||||||
|
clipe.inicio_na_origem + deslocamento, clipe.fim_na_origem,
|
||||||
|
)
|
||||||
|
faixa[posicao:posicao + 1] = [parte_esquerda, parte_direita]
|
||||||
|
cortados += 1
|
||||||
|
break
|
||||||
|
return {"razored": cortados, "verified": True}
|
||||||
|
|
||||||
def _simular_ripple_delete(self, argumentos: dict[str, Any]) -> dict[str, Any]:
|
def _simular_ripple_delete(self, argumentos: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Remove o clipe pedido e fecha o espaço deixado na mesma faixa."""
|
"""Remove o clipe pedido e fecha o espaço deixado na mesma faixa."""
|
||||||
identificador_do_clipe = argumentos["node_id"]
|
identificador_do_clipe = argumentos["node_id"]
|
||||||
|
|||||||
Reference in New Issue
Block a user