Tutorials
Catch AI Video Slop in 15 Minutes with Pegasus

James Le
Learn how to build an automated QC pipeline for AI video using TwelveLabs' Pegasus model, with step-by-step code for brief-match verdicts, timestamped defect detection, and lip-sync verification, in about 15 minutes.
Learn how to build an automated QC pipeline for AI video using TwelveLabs' Pegasus model, with step-by-step code for brief-match verdicts, timestamped defect detection, and lip-sync verification, in about 15 minutes.

In this article
Join our newsletter
Receive the latest advancements, tutorials, and industry insights in video understanding
AI로 영상을 검색하고, 분석하고, 탐색하세요.
2026. 7. 10.
6 min
링크 복사하기
Here is an AI-generated clip of a magician shuffling cards. Mid-shuffle, his fingers warp, merge, and melt into the deck. And here is Pegasus catching it: real API output, unedited.
The demo flagging this exact clip, with a clickable defect timeline
Timestamped, typed, machine-readable: the kind of verdict you can wire straight into an automation loop. In the next 15 minutes you'll build the check that produced it, plus a whole-clip pass/fail check, against real slop clips you can download. If you'd rather see it before building it, the live playground runs all five checks in your browser.
Step 0 — Setup (2 minutes)
Create a free TwelveLabs account and copy your API key from the dashboard.
Install the SDK:
from twelvelabs import TwelveLabs client = TwelveLabs(api_key="YOUR_API_KEY")
That's the whole setup. No index, no cluster, no config file.
Step 1 — Two modes, one decision (1 minute)
Pegasus 1.5 gives you two ways to run QC. Picking the right one is the only architectural decision in this tutorial:
Verdict — prompt-based
| Timeline — Segment /
| |
|---|---|---|
You send | An engineered prompt | A segment schema (no prompt) |
You get | One JSON answer for the whole clip | Timestamped segments with typed fields |
Best for | "Did I get what I ordered?" | "Where exactly is it broken?" |
Rule of thumb: QC over time almost always wants Timeline. A whole-clip FAIL tells you to re-roll; a timestamped FAIL tells you what to trim, seek, or regenerate. We'll build one of each.
Step 2 — Upload a clip as an asset (2 minutes)
Pegasus 1.5 works directly on assets, no indexing step. Upload once, reuse the asset_id for every check:
import time CLIP_URL = "<https://pegasus-qc-playground.vercel.app/clips/card-shuffle.mp4>" # our warped-hands clip — or bring your own def upload(url): asset = client.assets.create(method="url", url=url) while client.assets.retrieve(asset.id).status != "ready": time.sleep(3) return asset cards = upload(CLIP_URL) # for the Timeline check product = upload("<https://pegasus-qc-playground.vercel.app/clips/product-broll.mp4>") # for the Verdict check
All five sample clips from the demo are free to use and provenance-logged (model, prompt, and seed for each; see the PROVENANCE file in the demo so you know the slop is real, not staged).
Step 3 — Your first Verdict check: brief-match (3 minutes)
The highest-volume QC question in production is the simplest: did the generator produce what was ordered? Send the original brief along with the clip, and demand structured JSON back:
import json from twelvelabs.types import VideoContext_AssetId BRIEF = ( 'A red ceramic coffee mug with the word "SUMMIT" printed on it in white capital ' "letters, steam rising from the coffee, on a rustic wooden desk next to an open " "silver laptop and a small white plate holding two croissants, camera slowly pans " "left to right, warm morning light through a window" ) PROMPT = f"""You are a broadcast generation-QA engineer inspecting a SINGLE freshly generated video clip (one beat) BEFORE it is composited into a final cut. Your only job is to answer: did the generator produce what was requested? The clip was ordered with this brief: "{BRIEF}" Judge ONLY whether the brief was fulfilled. Do NOT score creative quality or taste. Respond with ONLY valid JSON: {{ "brief_fulfilled": true/false, "subject_present": true/false, "action_present": true/false, "setting_correct": true/false, "missing_or_wrong": ["<each element that is missing or wrong>"], "confidence": 0.0-1.0, "notes": "<one sentence>" }}""" res = client.analyze( video=VideoContext_AssetId(asset_id=product.id), model_name="pegasus1.5", prompt=PROMPT, temperature=0.2, # low = deterministic, better for QC ) verdict = json.loads(res.data)
Run it against our product-shot clip and you get this back, again, real output:
{ "brief_fulfilled": false, "subject_present": true, "action_present": false, "setting_correct": true, "missing_or_wrong": [ "The laptop is closed, not open as requested.", "The plate holds only one croissant, not two.", "The camera is static; there is no slow pan left to right." ], "confidence": 0.95, "notes": "The generator failed to render the laptop as open, included only one croissant instead of two, and did not execute the requested camera pan." }
It counted the croissants. That missing_or_wrong array is your automated re-prompt: feed it back to the generator and order exactly what was missed.
That was your first API call. One round-trip, one structured verdict. Everything else is variations.
Step 4 — Level up to Timeline: the AV-defect audit (5 minutes)
Now the centerpiece. Instead of a prompt, Segment mode takes segment definitions: a schema describing what to find and what to extract for each hit:
from twelvelabs.types import AsyncResponseFormat SEGMENT_DEFINITION = { "id": "av_defects", "description": ( "Segment the clip wherever a technical audio-visual defect is visible or " "audible. Include flickering, visual artifacts, warping or morphing objects, " "malformed hands or faces, duplicated limbs, sudden resolution or exposure " "jumps, audio dropout, distortion, or lip/audio sync drift. Do NOT judge " "creative quality (hook, brand fit, aesthetics)." ), "fields": [ {"name": "defect_type", "type": "string", "description": "The primary defect in this segment", "enum": ["flicker", "artifact", "warping", "malformed_anatomy", "duplicated_limb", "exposure_jump", "audio_dropout", "distortion", "sync_drift", "other"]}, {"name": "severity", "type": "string", "description": "How disqualifying the defect is", "enum": ["minor", "major"]}, {"name": "description", "type": "string", "description": "One-sentence description of what is wrong in this segment"}, ], } task = client.analyze_async.tasks.create( video=VideoContext_AssetId(asset_id=cards.id), model_name="pegasus1.5", analysis_mode="time_based_metadata", temperature=0.2, min_segment_duration=2.0, response_format=AsyncResponseFormat( type="segment_definitions", segment_definitions=[SEGMENT_DEFINITION], ), ) while True: task = client.analyze_async.tasks.retrieve(task.task_id) if task.status in ("ready", "failed"): break time.sleep(5) data = json.loads(task.result.data) for seg in data["av_defects"]: print(f'{seg["start_time"]}s–{seg["end_time"]}s', seg["metadata"])
The anatomy, quickly: the definition's id becomes the top-level key in the response; each field needs a type and a description (use enum to lock categorical values); min_segment_duration keeps segments meaningful (2s minimum). A clean clip returns an empty list, so PASS is falsifiable.
Three gotchas that will otherwise cost you a support ticket:
No
promptin this mode. Pass one and you get a 400. Your instructions live in the segment and fielddescriptions.Every field requires a
description. It's not decoration — see the box below.finish_reason == "length"means the JSON was truncated: raisemax_tokens(up to 98,304) or trim fields.
Your field descriptions ARE the prompt. Engineer them like one.
While building the demo, our first lip-sync check passed a clip where TTS audio played over a completely closed mouth. The field description, "True if lips articulate the words that are heard," let the model answer generously without really looking. Three schema lessons later, the same model on the same clip returns this instead:
{ "mouth_visibly_articulating": false, "lips_match_words": false, "native_lipsync_not_tts_over_silence": false, "speech_audible": true, "spoken_words": "Our new product launches this Friday, and we could not be more excited about it.", "notes": "The woman's mouth remains closed or in a static smile throughout the clip. She does not articulate any words, indicating the audio is a voiceover or TTS layered over the footage." }
The three lessons, in the order they cost us time: (1) Force visual attention: word booleans as "True ONLY if you can SEE..." rather than descriptions a model can satisfy from audio alone. (2) Gate applicability: the speech_audible field is what keeps a silent product shot from "failing" a lip-sync check; judge lips_match_words only where it's true. (3) Field order matters: when we put the audio gate first, answering "speech is audible" anchored a speaking-person prior and the mouth booleans followed it, consistently.
Visual judgments go before audio anchors, and each field says which modality to judge. When a segment check misses, sharpen the schema before you blame the model. The full evolution is logged in the demo's provenance file.
For a deeper dive, the official Segment quickstart notebook covers scoping with time_ranges, multiple definitions per request, and more.
Step 5 — The full check suite (3 minutes)
You now know both modes, which means you can build all five production checks. The complete prompt pack (every prompt and segment definition below, copy-paste ready) ships with the demo repo.
Check | Mode | Use it when |
|---|---|---|
Brief-match | Verdict | Gating generations against the order (highest volume) |
Content verification | Verdict | Scripted dialogue — right character, right line |
AV-defect audit | Timeline | Technical artifacts you want timestamped |
Lip-sync verification | Timeline | Any talking head — catches TTS-over-still-mouth |
Content log | Timeline | Cataloging what you keep, scene by scene |
One more trick worth knowing: segment definitions accept up to four reference images via media_sources, referenced in descriptions as <@name> for example "Segment where the on-screen product matches <@intended_product>". That turns brief-match into brand- and character-consistency QC.
Step 6 — Turn it into a pipeline (3 minutes)
Here's the beat that changes this from a demo into infrastructure. Because Timeline verdicts carry timestamps, you don't re-roll failed clips, you re-roll failed windows:
for seg in data["av_defects"]: if seg["metadata"]["severity"] == "major": # surgical: re-generate only the broken beat, keep the rest regenerate(clip, retry_window=(seg["start_time"], seg["end_time"]))
This is the exact pattern running in production today at generative-video studios doing tens of thousands of QC calls: generate -> QC -> auto-route failures back to the generator -> composite only green clips. Zero human review between generation and edit. It's only possible because the verdict says where, not just whether.
Where to go next
Scale it: batch your checks, scope long inputs with
start_time/end_timeor per-definitiontime_ranges, and pack up to 10 definitions into one request.Wire it to your generator: the QC call doesn't care whether the clip came from Fal, Kling, Runway, or Veo — if it has a URL, it can be checked.
Fork the notebook: runnable Colab with all five checks.
Play first, build second: the live QC Playground runs every check in this post against every sample clip, and shows the exact API call behind each verdict.
Why this layer matters: the companion piece The QC Node: Why Every AI Video Pipeline Needs a Quality-Control Layer
Fifteen minutes, two modes, five checks — and nothing broken gets past your pipeline again.
Here is an AI-generated clip of a magician shuffling cards. Mid-shuffle, his fingers warp, merge, and melt into the deck. And here is Pegasus catching it: real API output, unedited.
The demo flagging this exact clip, with a clickable defect timeline
Timestamped, typed, machine-readable: the kind of verdict you can wire straight into an automation loop. In the next 15 minutes you'll build the check that produced it, plus a whole-clip pass/fail check, against real slop clips you can download. If you'd rather see it before building it, the live playground runs all five checks in your browser.
Step 0 — Setup (2 minutes)
Create a free TwelveLabs account and copy your API key from the dashboard.
Install the SDK:
from twelvelabs import TwelveLabs client = TwelveLabs(api_key="YOUR_API_KEY")
That's the whole setup. No index, no cluster, no config file.
Step 1 — Two modes, one decision (1 minute)
Pegasus 1.5 gives you two ways to run QC. Picking the right one is the only architectural decision in this tutorial:
Verdict — prompt-based
| Timeline — Segment /
| |
|---|---|---|
You send | An engineered prompt | A segment schema (no prompt) |
You get | One JSON answer for the whole clip | Timestamped segments with typed fields |
Best for | "Did I get what I ordered?" | "Where exactly is it broken?" |
Rule of thumb: QC over time almost always wants Timeline. A whole-clip FAIL tells you to re-roll; a timestamped FAIL tells you what to trim, seek, or regenerate. We'll build one of each.
Step 2 — Upload a clip as an asset (2 minutes)
Pegasus 1.5 works directly on assets, no indexing step. Upload once, reuse the asset_id for every check:
import time CLIP_URL = "<https://pegasus-qc-playground.vercel.app/clips/card-shuffle.mp4>" # our warped-hands clip — or bring your own def upload(url): asset = client.assets.create(method="url", url=url) while client.assets.retrieve(asset.id).status != "ready": time.sleep(3) return asset cards = upload(CLIP_URL) # for the Timeline check product = upload("<https://pegasus-qc-playground.vercel.app/clips/product-broll.mp4>") # for the Verdict check
All five sample clips from the demo are free to use and provenance-logged (model, prompt, and seed for each; see the PROVENANCE file in the demo so you know the slop is real, not staged).
Step 3 — Your first Verdict check: brief-match (3 minutes)
The highest-volume QC question in production is the simplest: did the generator produce what was ordered? Send the original brief along with the clip, and demand structured JSON back:
import json from twelvelabs.types import VideoContext_AssetId BRIEF = ( 'A red ceramic coffee mug with the word "SUMMIT" printed on it in white capital ' "letters, steam rising from the coffee, on a rustic wooden desk next to an open " "silver laptop and a small white plate holding two croissants, camera slowly pans " "left to right, warm morning light through a window" ) PROMPT = f"""You are a broadcast generation-QA engineer inspecting a SINGLE freshly generated video clip (one beat) BEFORE it is composited into a final cut. Your only job is to answer: did the generator produce what was requested? The clip was ordered with this brief: "{BRIEF}" Judge ONLY whether the brief was fulfilled. Do NOT score creative quality or taste. Respond with ONLY valid JSON: {{ "brief_fulfilled": true/false, "subject_present": true/false, "action_present": true/false, "setting_correct": true/false, "missing_or_wrong": ["<each element that is missing or wrong>"], "confidence": 0.0-1.0, "notes": "<one sentence>" }}""" res = client.analyze( video=VideoContext_AssetId(asset_id=product.id), model_name="pegasus1.5", prompt=PROMPT, temperature=0.2, # low = deterministic, better for QC ) verdict = json.loads(res.data)
Run it against our product-shot clip and you get this back, again, real output:
{ "brief_fulfilled": false, "subject_present": true, "action_present": false, "setting_correct": true, "missing_or_wrong": [ "The laptop is closed, not open as requested.", "The plate holds only one croissant, not two.", "The camera is static; there is no slow pan left to right." ], "confidence": 0.95, "notes": "The generator failed to render the laptop as open, included only one croissant instead of two, and did not execute the requested camera pan." }
It counted the croissants. That missing_or_wrong array is your automated re-prompt: feed it back to the generator and order exactly what was missed.
That was your first API call. One round-trip, one structured verdict. Everything else is variations.
Step 4 — Level up to Timeline: the AV-defect audit (5 minutes)
Now the centerpiece. Instead of a prompt, Segment mode takes segment definitions: a schema describing what to find and what to extract for each hit:
from twelvelabs.types import AsyncResponseFormat SEGMENT_DEFINITION = { "id": "av_defects", "description": ( "Segment the clip wherever a technical audio-visual defect is visible or " "audible. Include flickering, visual artifacts, warping or morphing objects, " "malformed hands or faces, duplicated limbs, sudden resolution or exposure " "jumps, audio dropout, distortion, or lip/audio sync drift. Do NOT judge " "creative quality (hook, brand fit, aesthetics)." ), "fields": [ {"name": "defect_type", "type": "string", "description": "The primary defect in this segment", "enum": ["flicker", "artifact", "warping", "malformed_anatomy", "duplicated_limb", "exposure_jump", "audio_dropout", "distortion", "sync_drift", "other"]}, {"name": "severity", "type": "string", "description": "How disqualifying the defect is", "enum": ["minor", "major"]}, {"name": "description", "type": "string", "description": "One-sentence description of what is wrong in this segment"}, ], } task = client.analyze_async.tasks.create( video=VideoContext_AssetId(asset_id=cards.id), model_name="pegasus1.5", analysis_mode="time_based_metadata", temperature=0.2, min_segment_duration=2.0, response_format=AsyncResponseFormat( type="segment_definitions", segment_definitions=[SEGMENT_DEFINITION], ), ) while True: task = client.analyze_async.tasks.retrieve(task.task_id) if task.status in ("ready", "failed"): break time.sleep(5) data = json.loads(task.result.data) for seg in data["av_defects"]: print(f'{seg["start_time"]}s–{seg["end_time"]}s', seg["metadata"])
The anatomy, quickly: the definition's id becomes the top-level key in the response; each field needs a type and a description (use enum to lock categorical values); min_segment_duration keeps segments meaningful (2s minimum). A clean clip returns an empty list, so PASS is falsifiable.
Three gotchas that will otherwise cost you a support ticket:
No
promptin this mode. Pass one and you get a 400. Your instructions live in the segment and fielddescriptions.Every field requires a
description. It's not decoration — see the box below.finish_reason == "length"means the JSON was truncated: raisemax_tokens(up to 98,304) or trim fields.
Your field descriptions ARE the prompt. Engineer them like one.
While building the demo, our first lip-sync check passed a clip where TTS audio played over a completely closed mouth. The field description, "True if lips articulate the words that are heard," let the model answer generously without really looking. Three schema lessons later, the same model on the same clip returns this instead:
{ "mouth_visibly_articulating": false, "lips_match_words": false, "native_lipsync_not_tts_over_silence": false, "speech_audible": true, "spoken_words": "Our new product launches this Friday, and we could not be more excited about it.", "notes": "The woman's mouth remains closed or in a static smile throughout the clip. She does not articulate any words, indicating the audio is a voiceover or TTS layered over the footage." }
The three lessons, in the order they cost us time: (1) Force visual attention: word booleans as "True ONLY if you can SEE..." rather than descriptions a model can satisfy from audio alone. (2) Gate applicability: the speech_audible field is what keeps a silent product shot from "failing" a lip-sync check; judge lips_match_words only where it's true. (3) Field order matters: when we put the audio gate first, answering "speech is audible" anchored a speaking-person prior and the mouth booleans followed it, consistently.
Visual judgments go before audio anchors, and each field says which modality to judge. When a segment check misses, sharpen the schema before you blame the model. The full evolution is logged in the demo's provenance file.
For a deeper dive, the official Segment quickstart notebook covers scoping with time_ranges, multiple definitions per request, and more.
Step 5 — The full check suite (3 minutes)
You now know both modes, which means you can build all five production checks. The complete prompt pack (every prompt and segment definition below, copy-paste ready) ships with the demo repo.
Check | Mode | Use it when |
|---|---|---|
Brief-match | Verdict | Gating generations against the order (highest volume) |
Content verification | Verdict | Scripted dialogue — right character, right line |
AV-defect audit | Timeline | Technical artifacts you want timestamped |
Lip-sync verification | Timeline | Any talking head — catches TTS-over-still-mouth |
Content log | Timeline | Cataloging what you keep, scene by scene |
One more trick worth knowing: segment definitions accept up to four reference images via media_sources, referenced in descriptions as <@name> for example "Segment where the on-screen product matches <@intended_product>". That turns brief-match into brand- and character-consistency QC.
Step 6 — Turn it into a pipeline (3 minutes)
Here's the beat that changes this from a demo into infrastructure. Because Timeline verdicts carry timestamps, you don't re-roll failed clips, you re-roll failed windows:
for seg in data["av_defects"]: if seg["metadata"]["severity"] == "major": # surgical: re-generate only the broken beat, keep the rest regenerate(clip, retry_window=(seg["start_time"], seg["end_time"]))
This is the exact pattern running in production today at generative-video studios doing tens of thousands of QC calls: generate -> QC -> auto-route failures back to the generator -> composite only green clips. Zero human review between generation and edit. It's only possible because the verdict says where, not just whether.
Where to go next
Scale it: batch your checks, scope long inputs with
start_time/end_timeor per-definitiontime_ranges, and pack up to 10 definitions into one request.Wire it to your generator: the QC call doesn't care whether the clip came from Fal, Kling, Runway, or Veo — if it has a URL, it can be checked.
Fork the notebook: runnable Colab with all five checks.
Play first, build second: the live QC Playground runs every check in this post against every sample clip, and shows the exact API call behind each verdict.
Why this layer matters: the companion piece The QC Node: Why Every AI Video Pipeline Needs a Quality-Control Layer
Fifteen minutes, two modes, five checks — and nothing broken gets past your pipeline again.
Here is an AI-generated clip of a magician shuffling cards. Mid-shuffle, his fingers warp, merge, and melt into the deck. And here is Pegasus catching it: real API output, unedited.
The demo flagging this exact clip, with a clickable defect timeline
Timestamped, typed, machine-readable: the kind of verdict you can wire straight into an automation loop. In the next 15 minutes you'll build the check that produced it, plus a whole-clip pass/fail check, against real slop clips you can download. If you'd rather see it before building it, the live playground runs all five checks in your browser.
Step 0 — Setup (2 minutes)
Create a free TwelveLabs account and copy your API key from the dashboard.
Install the SDK:
from twelvelabs import TwelveLabs client = TwelveLabs(api_key="YOUR_API_KEY")
That's the whole setup. No index, no cluster, no config file.
Step 1 — Two modes, one decision (1 minute)
Pegasus 1.5 gives you two ways to run QC. Picking the right one is the only architectural decision in this tutorial:
Verdict — prompt-based
| Timeline — Segment /
| |
|---|---|---|
You send | An engineered prompt | A segment schema (no prompt) |
You get | One JSON answer for the whole clip | Timestamped segments with typed fields |
Best for | "Did I get what I ordered?" | "Where exactly is it broken?" |
Rule of thumb: QC over time almost always wants Timeline. A whole-clip FAIL tells you to re-roll; a timestamped FAIL tells you what to trim, seek, or regenerate. We'll build one of each.
Step 2 — Upload a clip as an asset (2 minutes)
Pegasus 1.5 works directly on assets, no indexing step. Upload once, reuse the asset_id for every check:
import time CLIP_URL = "<https://pegasus-qc-playground.vercel.app/clips/card-shuffle.mp4>" # our warped-hands clip — or bring your own def upload(url): asset = client.assets.create(method="url", url=url) while client.assets.retrieve(asset.id).status != "ready": time.sleep(3) return asset cards = upload(CLIP_URL) # for the Timeline check product = upload("<https://pegasus-qc-playground.vercel.app/clips/product-broll.mp4>") # for the Verdict check
All five sample clips from the demo are free to use and provenance-logged (model, prompt, and seed for each; see the PROVENANCE file in the demo so you know the slop is real, not staged).
Step 3 — Your first Verdict check: brief-match (3 minutes)
The highest-volume QC question in production is the simplest: did the generator produce what was ordered? Send the original brief along with the clip, and demand structured JSON back:
import json from twelvelabs.types import VideoContext_AssetId BRIEF = ( 'A red ceramic coffee mug with the word "SUMMIT" printed on it in white capital ' "letters, steam rising from the coffee, on a rustic wooden desk next to an open " "silver laptop and a small white plate holding two croissants, camera slowly pans " "left to right, warm morning light through a window" ) PROMPT = f"""You are a broadcast generation-QA engineer inspecting a SINGLE freshly generated video clip (one beat) BEFORE it is composited into a final cut. Your only job is to answer: did the generator produce what was requested? The clip was ordered with this brief: "{BRIEF}" Judge ONLY whether the brief was fulfilled. Do NOT score creative quality or taste. Respond with ONLY valid JSON: {{ "brief_fulfilled": true/false, "subject_present": true/false, "action_present": true/false, "setting_correct": true/false, "missing_or_wrong": ["<each element that is missing or wrong>"], "confidence": 0.0-1.0, "notes": "<one sentence>" }}""" res = client.analyze( video=VideoContext_AssetId(asset_id=product.id), model_name="pegasus1.5", prompt=PROMPT, temperature=0.2, # low = deterministic, better for QC ) verdict = json.loads(res.data)
Run it against our product-shot clip and you get this back, again, real output:
{ "brief_fulfilled": false, "subject_present": true, "action_present": false, "setting_correct": true, "missing_or_wrong": [ "The laptop is closed, not open as requested.", "The plate holds only one croissant, not two.", "The camera is static; there is no slow pan left to right." ], "confidence": 0.95, "notes": "The generator failed to render the laptop as open, included only one croissant instead of two, and did not execute the requested camera pan." }
It counted the croissants. That missing_or_wrong array is your automated re-prompt: feed it back to the generator and order exactly what was missed.
That was your first API call. One round-trip, one structured verdict. Everything else is variations.
Step 4 — Level up to Timeline: the AV-defect audit (5 minutes)
Now the centerpiece. Instead of a prompt, Segment mode takes segment definitions: a schema describing what to find and what to extract for each hit:
from twelvelabs.types import AsyncResponseFormat SEGMENT_DEFINITION = { "id": "av_defects", "description": ( "Segment the clip wherever a technical audio-visual defect is visible or " "audible. Include flickering, visual artifacts, warping or morphing objects, " "malformed hands or faces, duplicated limbs, sudden resolution or exposure " "jumps, audio dropout, distortion, or lip/audio sync drift. Do NOT judge " "creative quality (hook, brand fit, aesthetics)." ), "fields": [ {"name": "defect_type", "type": "string", "description": "The primary defect in this segment", "enum": ["flicker", "artifact", "warping", "malformed_anatomy", "duplicated_limb", "exposure_jump", "audio_dropout", "distortion", "sync_drift", "other"]}, {"name": "severity", "type": "string", "description": "How disqualifying the defect is", "enum": ["minor", "major"]}, {"name": "description", "type": "string", "description": "One-sentence description of what is wrong in this segment"}, ], } task = client.analyze_async.tasks.create( video=VideoContext_AssetId(asset_id=cards.id), model_name="pegasus1.5", analysis_mode="time_based_metadata", temperature=0.2, min_segment_duration=2.0, response_format=AsyncResponseFormat( type="segment_definitions", segment_definitions=[SEGMENT_DEFINITION], ), ) while True: task = client.analyze_async.tasks.retrieve(task.task_id) if task.status in ("ready", "failed"): break time.sleep(5) data = json.loads(task.result.data) for seg in data["av_defects"]: print(f'{seg["start_time"]}s–{seg["end_time"]}s', seg["metadata"])
The anatomy, quickly: the definition's id becomes the top-level key in the response; each field needs a type and a description (use enum to lock categorical values); min_segment_duration keeps segments meaningful (2s minimum). A clean clip returns an empty list, so PASS is falsifiable.
Three gotchas that will otherwise cost you a support ticket:
No
promptin this mode. Pass one and you get a 400. Your instructions live in the segment and fielddescriptions.Every field requires a
description. It's not decoration — see the box below.finish_reason == "length"means the JSON was truncated: raisemax_tokens(up to 98,304) or trim fields.
Your field descriptions ARE the prompt. Engineer them like one.
While building the demo, our first lip-sync check passed a clip where TTS audio played over a completely closed mouth. The field description, "True if lips articulate the words that are heard," let the model answer generously without really looking. Three schema lessons later, the same model on the same clip returns this instead:
{ "mouth_visibly_articulating": false, "lips_match_words": false, "native_lipsync_not_tts_over_silence": false, "speech_audible": true, "spoken_words": "Our new product launches this Friday, and we could not be more excited about it.", "notes": "The woman's mouth remains closed or in a static smile throughout the clip. She does not articulate any words, indicating the audio is a voiceover or TTS layered over the footage." }
The three lessons, in the order they cost us time: (1) Force visual attention: word booleans as "True ONLY if you can SEE..." rather than descriptions a model can satisfy from audio alone. (2) Gate applicability: the speech_audible field is what keeps a silent product shot from "failing" a lip-sync check; judge lips_match_words only where it's true. (3) Field order matters: when we put the audio gate first, answering "speech is audible" anchored a speaking-person prior and the mouth booleans followed it, consistently.
Visual judgments go before audio anchors, and each field says which modality to judge. When a segment check misses, sharpen the schema before you blame the model. The full evolution is logged in the demo's provenance file.
For a deeper dive, the official Segment quickstart notebook covers scoping with time_ranges, multiple definitions per request, and more.
Step 5 — The full check suite (3 minutes)
You now know both modes, which means you can build all five production checks. The complete prompt pack (every prompt and segment definition below, copy-paste ready) ships with the demo repo.
Check | Mode | Use it when |
|---|---|---|
Brief-match | Verdict | Gating generations against the order (highest volume) |
Content verification | Verdict | Scripted dialogue — right character, right line |
AV-defect audit | Timeline | Technical artifacts you want timestamped |
Lip-sync verification | Timeline | Any talking head — catches TTS-over-still-mouth |
Content log | Timeline | Cataloging what you keep, scene by scene |
One more trick worth knowing: segment definitions accept up to four reference images via media_sources, referenced in descriptions as <@name> for example "Segment where the on-screen product matches <@intended_product>". That turns brief-match into brand- and character-consistency QC.
Step 6 — Turn it into a pipeline (3 minutes)
Here's the beat that changes this from a demo into infrastructure. Because Timeline verdicts carry timestamps, you don't re-roll failed clips, you re-roll failed windows:
for seg in data["av_defects"]: if seg["metadata"]["severity"] == "major": # surgical: re-generate only the broken beat, keep the rest regenerate(clip, retry_window=(seg["start_time"], seg["end_time"]))
This is the exact pattern running in production today at generative-video studios doing tens of thousands of QC calls: generate -> QC -> auto-route failures back to the generator -> composite only green clips. Zero human review between generation and edit. It's only possible because the verdict says where, not just whether.
Where to go next
Scale it: batch your checks, scope long inputs with
start_time/end_timeor per-definitiontime_ranges, and pack up to 10 definitions into one request.Wire it to your generator: the QC call doesn't care whether the clip came from Fal, Kling, Runway, or Veo — if it has a URL, it can be checked.
Fork the notebook: runnable Colab with all five checks.
Play first, build second: the live QC Playground runs every check in this post against every sample clip, and shows the exact API call behind each verdict.
Why this layer matters: the companion piece The QC Node: Why Every AI Video Pipeline Needs a Quality-Control Layer
Fifteen minutes, two modes, five checks — and nothing broken gets past your pipeline again.





