Files
gart/code/fcpxml/writer/adjustment.py
João HenriqueandClaude Sonnet 5 d13f643ebc chore(fase0): higiene do repositório + corrige gitignore que escondia fcpxml/models/
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>
2026-09-23 08:28:44 -04:00

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