Fase 0 do roteiro de reestruturação (Engine/docs/10_MAPA_REESTRUTURACAO.md): move code/WHISPERX (2,6 GB de backups órfãos, sem uso ativo, sem .gitmodules) para ~/Archives/G-ART-WHISPERX-backup fora do workspace git; traz admin/ para o gate de lint de run_after_fix.sh; corrige fcpxml/writer/adjustment.py, que gerava um wrapper <adjustment> inexistente no DTD 1.13 (filtros agora vão direto no <clip>, na ordem exigida), com teste de regressão novo. Achado à parte: .gitignore tinha uma regra solta "models/" (pensada só para o cache do Whisper em code/models/) que também escondia do git todo o pacote fcpxml/models/ — nunca commitado, sem proteção nenhuma. Corrigida para /code/models/, ancorada na raiz. Docs atualizados no mesmo commit (02_MODULES, 09_MANUTENCAO, 10_MAPA_REESTRUTURACAO, 05_EXPERIENCIAS #34 e #36), conforme a regra do CLAUDE.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
141 lines
5.1 KiB
Python
141 lines
5.1 KiB
Python
"""Clip de ajuste (adjustment layer) — criação do elemento FCPXML.
|
|
|
|
No Final Cut, uma "camada de ajuste" é um ``<clip>`` que carrega filtros
|
|
(``filter-video`` / ``filter-audio``) diretamente como filhos — o DTD do
|
|
FCPXML 1.13 não define nenhum elemento ``<adjustment>`` como wrapper (ver
|
|
``<!ELEMENT clip>`` em ``FCPXMLv1_13.dtd``: ``filter-video``/``filter-audio``
|
|
vêm depois de ``audio-channel-source*`` e antes de ``metadata?``, sem
|
|
elemento intermediário). Tudo que está abaixo do clip na timeline herda
|
|
esses filtros — é como se o efeito fosse aplicado a uma faixa inteira de
|
|
uma vez.
|
|
|
|
Esta classe monta esse elemento a partir de dados de alto nível (duração +
|
|
lista de ``EfeitoAjuste``), cuidando de criar os recursos ``<effect>``
|
|
correspondentes na seção ``<resources>`` e de referenciá-los pelos filtros.
|
|
"""
|
|
|
|
import xml.etree.ElementTree as ET
|
|
from typing import Callable, List, Optional
|
|
|
|
from ..models.timeline import EfeitoAjuste
|
|
from ..models.timing import TimeValue
|
|
|
|
|
|
def _para_racional(tempo) -> str:
|
|
"""Aceita ``TimeValue`` ou uma string FCPXML já formatada ("90/30s")."""
|
|
if isinstance(tempo, TimeValue):
|
|
return tempo.to_fcpxml()
|
|
if tempo is None:
|
|
return "0/1s"
|
|
return str(tempo)
|
|
|
|
|
|
def _id_recurso_unico(resources: ET.Element, prefixo: str = "r_ajuste") -> str:
|
|
"""Gera um ``id`` de recurso ainda ausente em ``resources``."""
|
|
existentes = {r.get("id") for r in resources.findall("*") if r.get("id")}
|
|
contador = 1
|
|
while f"{prefixo}_{contador}" in existentes:
|
|
contador += 1
|
|
return f"{prefixo}_{contador}"
|
|
|
|
|
|
class ClipDeAjuste:
|
|
"""Cria um clip de ajuste (adjustment layer) pronto para a spine.
|
|
|
|
Exemplo::
|
|
|
|
from fcpxml.models.timing import TimeValue
|
|
from fcpxml.models.timeline import EfeitoAjuste, ParametroEfeito
|
|
from fcpxml.writer.adjustment import ClipDeAjuste
|
|
|
|
efeito = EfeitoAjuste(
|
|
nome="Color Curves", uid="...UUID...", tipo="video",
|
|
parametros=[ParametroEfeito(nome="Amount", valor="0.5",
|
|
chave=".../9999")],
|
|
)
|
|
clip = ClipDeAjuste(
|
|
nome="Ajuste de cor",
|
|
duracao=TimeValue(300, 30),
|
|
efeitos=[efeito],
|
|
).criar(resources)
|
|
spine.append(clip)
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
nome: str,
|
|
duracao,
|
|
efeitos: List[EfeitoAjuste],
|
|
offset=None,
|
|
formato_tc: str = "NDF",
|
|
):
|
|
self.nome = nome
|
|
self.duracao = duracao
|
|
self.efeitos = efeitos
|
|
self.offset = offset
|
|
self.formato_tc = formato_tc
|
|
|
|
def criar(
|
|
self,
|
|
resources: ET.Element,
|
|
proximo_id: Optional[Callable[[], str]] = None,
|
|
) -> ET.Element:
|
|
"""Monta o ``<clip>`` de ajuste e seus recursos ``<effect>``.
|
|
|
|
``resources`` é a seção ``<resources>`` do documento (onde os
|
|
``<effect>`` são registrados). ``proximo_id`` é um gerador opcional
|
|
de ids de recurso; sem ele, usa um id único baseado em ``resources``.
|
|
"""
|
|
def gerar_id() -> str:
|
|
if proximo_id:
|
|
return proximo_id()
|
|
return _id_recurso_unico(resources)
|
|
|
|
filtros: List[ET.Element] = []
|
|
for efeito in self.efeitos:
|
|
efeito_id = self._garantir_recurso(resources, efeito, gerar_id)
|
|
filtros.append(self._montar_filtro(efeito, efeito_id))
|
|
|
|
clip = ET.Element(
|
|
"clip",
|
|
name=self.nome,
|
|
duration=_para_racional(self.duracao),
|
|
tcFormat=self.formato_tc,
|
|
)
|
|
if self.offset is not None:
|
|
clip.set("offset", _para_racional(self.offset))
|
|
|
|
# O DTD exige filter-video* antes de filter-audio* como filhos
|
|
# diretos do clip (sem wrapper <adjustment>).
|
|
for filtro in sorted(filtros, key=lambda f: f.tag != "filter-video"):
|
|
clip.append(filtro)
|
|
return clip
|
|
|
|
def _garantir_recurso(
|
|
self, resources: ET.Element, efeito: EfeitoAjuste, gerar_id: Callable[[], str]
|
|
) -> str:
|
|
"""Devolve o ``id`` do ``<effect>`` de *efeito*, criando-o se ausente."""
|
|
for existente in resources.findall("effect"):
|
|
if existente.get("uid") == efeito.uid:
|
|
return existente.get("id")
|
|
efeito_id = gerar_id()
|
|
recurso = ET.SubElement(resources, "effect")
|
|
recurso.set("id", efeito_id)
|
|
recurso.set("name", efeito.nome)
|
|
recurso.set("uid", efeito.uid)
|
|
return efeito_id
|
|
|
|
def _montar_filtro(self, efeito: EfeitoAjuste, efeito_id: str) -> ET.Element:
|
|
"""Monta o ``<filter-video>``/``<filter-audio>`` de um efeito."""
|
|
tag = "filter-video" if efeito.tipo == "video" else "filter-audio"
|
|
filtro = ET.Element(tag, ref=efeito_id, name=efeito.nome)
|
|
for parametro in efeito.parametros:
|
|
param = ET.SubElement(filtro, "param")
|
|
param.set("name", parametro.nome)
|
|
if parametro.chave:
|
|
param.set("key", parametro.chave)
|
|
param.set("value", parametro.valor)
|
|
if parametro.metadado:
|
|
param.set("metadata", parametro.metadado)
|
|
return filtro
|