UGC-style ad: test five hooks on one body with Timeline plans
Join five 3-second hooks to one 12-second body clip with Timeline 1.0. Plan each cut unbilled, then render the winners at $0.10 a minute.

To test five opening hooks on a UGC-style ad, keep one body clip and put each hook in front of it with Timeline 1.0: one document per hook, each with two video[] slots. POST /v1/timeline-1.0/plan checks all five without charging you, and a render is $0.10 per output minute, rounded up, so each 15-second variant is one billable minute. Five variants rendered would be 5 x $0.10 = $0.50.
Source: Timeline 1.0: assemble clips into one MP4. Confirm the live rate in GET /v1/catalog. This post is about assembly; making the hook clips themselves is a separate step.
Why test hooks and not whole ads
Most of what makes a UGC-style ad work is its first seconds. If you change only the opening, you can read the result as a hook test: same offer, same voice, same ending. Rebuilding five whole ads confuses hook with everything else. A Timeline document is data, so five variants are one template with one value changed.
One template, five documents
The audio spine carries the voice. Each variant needs a spine of the same length as the output: here 3 seconds of hook plus 12 of body is 15, so audio.duration_seconds is 15. If hook voice differs per variant, audio.parts[] joins up to 20 gapless slices at the sample level, no re-TTS. If the voice is only in the body, use one spine file and set the hook slots without audio concerns.
The script builds five documents, plans each and prints the planned duration and cost estimate. A document is invalid if video[0].start is not 0 or if slot starts do not increase, and the plan says so before any money moves.
import json, os, urllib.request
BODY = "https://media.sume.com/artifacts/artf_demo/body-12s.mp4"
SPINE = "https://media.sume.com/artifacts/artf_demo/voice-15s.wav"
HOOKS = [f"https://media.sume.com/artifacts/artf_demo/hook-{i}.mp4" for i in range(1, 6)]
def plan(hook):
doc = {
"audio": {"url": SPINE, "duration_seconds": 15},
"video": [
{"source_url": hook, "start": 0, "duration": 3},
{"source_url": BODY, "start": 3, "duration": 12},
],
}
req = urllib.request.Request(
"https://api.sume.com/v1/timeline-1.0/plan",
data=json.dumps(doc).encode(),
headers={"Authorization": "Bearer " + os.environ["SUME_API_KEY"],
"Content-Type": "application/json"},
)
with urllib.request.urlopen(req) as r:
return json.load(r)
for hook in HOOKS:
p = plan(hook)
print(hook.rsplit("/", 1)[1], p["duration_seconds"], p["estimated_cost_usd_micros"])
What the numbers should be
Every plan should report 15 seconds and one billable minute, because the plan is computed from the numbers you declare, not from the files. It cannot see that a hook file is really 2.5 seconds long. If a source is shorter than its slot, the render pads or loops it and records a soft warning in warnings[], so open the warnings of each render before you ship it.
| Item | Count | Rate | Total |
|---|---|---|---|
| Plan calls | 5 | Unbilled | $0.00 |
| Renders, 15 s each | 5 | $0.10 per ceil minute | $0.50 |
| Captions on each (optional) | 5 | $0.20 | $1.00 |
| Plans, renders and captions together | 15 calls | mixed | $1.50 |
After the test
A hard cut from hook to body is the default. If a hook ends on a different framing from the body, you can add a transition to the body slot, since transitions are allowed only on slots after the first: fade, wipeleft, wiperight, slideup, slidedown or dissolve, at most 1 second and at most half of the shorter neighbouring slot. Keep the same transition in all five documents, otherwise you are testing two things at once.
Count the money before you start. Plans are unbilled, a render reserves ceil(audio.duration_seconds / 60) minutes, and a standalone caption job is $0.20, so five captioned variants come to $1.50 in total. Plan all five, fix whatever the plans reject, and only then render.
Keep the winning hook and drop the rest. If a hook wins on watch time, make three new hooks in its style and test again. The stored post on ten ad variants from one avatar take covers a larger version of this with trims and captions.
Sources
Related posts
More in Use cases
- UGC-style ad: a music bed that ducks under the voice
Add a looped music bed to a UGC-style ad and lower it under the voice with soundtrack.duck_db in a Timeline 1.0 render. Needs a real voice spine.
- Video ad end card: add a fade-in and fade-out with Timeline
Close an ad with a 1-second fade-out and open it with a 0.5-second fade-in using output.fade_in_seconds and fade_out_seconds in a Timeline 1.0 render.
- Which Sume video model makes a 15-second 9:16 ad? A catalog filter
List the models in GET /v1/videos/models that accept 15 seconds at 9:16 with a short Python filter, instead of trusting a stale table.
- Yard sign artwork via API: 3:2 art, a quoted headline, a print check
Generate yard sign artwork at 3:2 on Sume with GPT Image 2.5 or Ideogram 4.5, keep the headline to a few words, quote it, and check the pixel size for print.
Written by Sume