Skip to content

Templates & compound clips

CompoundClip is how you package a multi-track scene (title + subtitle + sound effect, say) into one reusable unit with a small, documented, typed set of knobs — the mechanism this whole framework leans on for "reusable video templates" and for letting an agent fill in a scene without touching its internal structure.

Building a template

inner = vk.Timeline()

inner.add_clip(
    vk.TextClip(
        id="speaker_name", text="Default Speaker",
        duration=vk.Time.from_seconds(6),
        style=vk.TextStyle(font_size=48, color="#ffffff"),
    ),
    track_index=0,
)
inner.add_clip(
    vk.TextClip(
        id="speaker_role", text="Default Title",
        timeline_start=vk.Time.from_seconds(0.5), duration=vk.Time.from_seconds(5.5),
        style=vk.TextStyle(font_size=28, color="#94a3b8"),
    ),
    track_index=1,
)
inner.add_clip(
    vk.AudioClip(id="sfx_whoosh", source="assets/whoosh.wav", duration=vk.Time.from_seconds(1.5)),
    track_index=0,
)

compound = vk.CompoundClip(
    id="tpl_lower_third",
    duration=vk.Time.from_seconds(6),
    inner_timeline=inner,
)

compound.expose_parameter(
    name="speaker_name", target_clip_id="speaker_name", target_variable="text",
    label="Speaker Name", description="Name of the person being interviewed",
    required=True,
)
compound.expose_parameter(
    name="speaker_role", target_clip_id="speaker_role", target_variable="text",
    label="Speaker Role", description="Professional title or company affiliation",
    default="Software Engineer",
)

expose_parameter(name, target_clip_id, target_variable, *, label=None, description=None, default=None, required=False) registers one mapping: top-level parameter name → a specific inner clip's field or variable. label/description exist specifically so a UI form builder or an AI agent can present the parameter to a user without inspecting the inner timeline at all.

target_variable can reach nested fields

target_variable isn't limited to a clip's own top-level fields. A dotted path ("style.color", "transform.opacity") reaches one or more levels into a clip's nested Pydantic models:

compound.expose_parameter(
    name="accent_color", target_clip_id="speaker_name", target_variable="style.color",
)

A handful of fields are protected and can never be a parameter target, at any position in the dotted path: id, clip_type, linked_clip_id, compound_clip_id, inner_timeline, exposed_parameters. Targeting one of these — or any other broken mapping (missing clip, missing property) — is a template-authoring error and is checked with a strict flag: it always raises TemplateParameterError at export/flatten time, but set_parameter(...) itself only raises for an unknown parameter name (see below) — a valid name with a broken target is silently skipped unless you call apply_parameters(strict=True) yourself. See 10-pitfalls.md for the exact rule.

Consuming a template

lower_third.set_parameter("speaker_name", "Dr. Jane Doe")
lower_third.set_parameter("speaker_role", "Principal AI Researcher")

root_timeline = vk.Timeline()
lower_third.timeline_start = vk.Time.from_seconds(3)
root_timeline.add_clip(lower_third)   # auto-creates the audio companion

set_parameter(name, value) sets the value and immediately applies it to the target clip — you don't need a separate "commit" step, and reading lower_third.parameters afterward reflects what's live. It raises TemplateParameterError immediately if name isn't a registered exposed parameter (or a clip_id.property path) — but not if name is valid and its mapping is simply broken (see the callout above).

apply_parameters(*, strict=False) re-propagates all current parameters to the inner timeline (mostly useful after directly mutating compound.parameters, which set_parameter normally does for you, or to validate a template you just authored). With strict=True — what export_to_video()/export_to_resolve() use internally — a parameter that fails to apply (missing target clip, missing property, protected field) raises TemplateParameterError instead of being silently skipped.

Discovering what a template needs (agent-facing)

This is the pattern for an agent that receives a CompoundClip it didn't build and needs to know what to fill in, without reading the inner timeline's Python:

for p in compound.exposed_parameters:
    print(p.name, p.label, p.description, p.required, p.default)

print(compound.get_set_parameters())            # {name: value, ...} currently set
print(compound.get_unset_parameters())          # [ExposedParameter, ...] not yet set
print(compound.get_missing_required_parameters())  # [name, ...] required + no value + no default
print(compound.get_child_variables())           # {clip_id: {var_name: Variable}} — every inner CodedVisualClip's own variables, for templates that also contain coded visuals

ExposedParameter and Variable are both plain Pydantic models — the whole set above serializes to JSON directly (p.model_dump()), so an agent can hand a template's "what do I need to fill in" schema to an LLM or a UI form without any bespoke serialization code.

The audio companion

Adding a CompoundClip through Timeline.add_clip automatically creates a linked CompoundAudioClip on an audio track (create_audio_companion() is what does this under the hood, but you normally never call it yourself). This is the mechanism that lets a compound's own inner audio tracks get mixed correctly when flattened, and why a CompoundClip "reserves a seat" on the audio lane the moment you add it — even if the inner timeline turns out to have no audio at all.

Timeline.remove_clip on a compound removes both the compound and its companion together. Timeline.sync_companions() re-aligns every companion's start/duration/speed with its compound after edits that might have moved one but not the other.

speed on a compound

Setting compound.speed = 2.0 compresses the entire inner timeline proportionally when flattened: 4 seconds of inner content at 2x now occupies 2 seconds of outer-timeline span, and every leaf clip inside is retimed to match (both its position and its own effective playback speed). This composes across nesting: a 2x compound nested inside a 3x compound flattens to 6x. speed=1.0 (the default) is a pure no-op.

Flattening

CompoundClip.duration is enforced: inner content is trimmed to the compound's own duration when flattened, even if the inner timeline is longer. The compound's own transform is composed onto each child's transform (compose_transforms in visualkit.engine.pipeline) — but see the callout in 03-clips-reference.md: composing an animated child (keyframes or an animation preset) with a non-identity compound transform is refused with a clear NotImplementedError rather than silently rendered wrong. Give the child its final position directly, or keep the compound's own transform at identity, if the child is animated.

flatten() never mutates the timeline you call it on — see 02-concepts.md — so a template's defaults are never baked in by exporting from it.