> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mavera.io/llms.txt
> Use this file to discover all available pages before exploring further.

# YouTube Pre-Roll Ad Testing

> Upload pre-roll ad candidates, analyze opening 5 seconds with Video Analysis, then run Focus Groups for skip-or-watch predictions

## Scenario

The first 5 seconds of a YouTube pre-roll ad determine everything — viewers either watch or hit Skip. This job takes your pre-roll ad candidates, runs Video Analysis focused on the opening 5 seconds (hook score, visual complexity, text legibility, emotional trigger), then puts the results through a Focus Group asking the core question: "Would you skip this ad?" The result is a skip-or-watch prediction with specific improvement recommendations from each persona before you commit media budget.

## Architecture

```mermaid theme={"dark"}
flowchart LR
    A[Upload pre-roll candidates] --> B["Mavera POST /video-analysis (first 5s)"] --> C["POST /focus-groups"] --> D[Skip prediction + improvement notes]
```

## Code

<CodeGroup>
  ```python Python theme={"dark"}
  import os, requests, time

  MV = os.environ["MAVERA_API_KEY"]
  MV_BASE = "https://app.mavera.io/api/v1"
  MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  PRE_ROLL_CANDIDATES = [
      {"path": "ads/preroll_a_product_demo.mp4", "label": "Product Demo — Fast Cuts"},
      {"path": "ads/preroll_b_testimonial.mp4", "label": "Customer Testimonial — Face First"},
      {"path": "ads/preroll_c_problem_hook.mp4", "label": "Problem Hook — Text Overlay"},
      {"path": "ads/preroll_d_humor.mp4", "label": "Humor — Unexpected Opening"},
  ]

  # 1. Upload and analyze each candidate
  analyses = []
  for candidate in PRE_ROLL_CANDIDATES:
      upload = requests.post(f"{MV_BASE}/assets",
          headers={"Authorization": f"Bearer {MV}"},
          files={"file": (
              candidate["label"] + ".mp4",
              open(candidate["path"], "rb"),
              "video/mp4",
          )},
      ).json()

      analysis = requests.post(f"{MV_BASE}/video-analysis", headers=MV_H, json={
          "asset_id": upload["id"],
          "analysis_types": [
              "hook_score", "visual_complexity", "text_legibility",
              "emotional_arc", "cognitive_load", "pacing",
          ],
          "options": {"focus_window": "0-5s"},
          "metadata": {"label": candidate["label"], "format": "pre-roll"},
      }).json()

      analyses.append({"id": analysis["id"], "label": candidate["label"]})
      time.sleep(0.5)

  # 2. Poll all analyses
  results = []
  for a in analyses:
      for _ in range(30):
          time.sleep(3)
          status = requests.get(
              f"{MV_BASE}/video-analysis/{a['id']}", headers=MV_H
          ).json()
          if status.get("status") == "completed":
              break
      r = status.get("results", {})
      results.append({
          "label": a["label"],
          "hook_score": r.get("hook_score", {}).get("score", 0),
          "hook_assessment": r.get("hook_score", {}).get("assessment", ""),
          "visual_complexity": r.get("visual_complexity", {}).get("score", 0),
          "text_legibility": r.get("text_legibility", {}).get("score", 0),
          "emotional_trigger": r.get("emotional_arc", {}).get("peak_emotion", "none"),
          "cognitive_load": r.get("cognitive_load", {}).get("average", 0),
      })

  # 3. Create Focus Group personas
  persona_ids = []
  for name, desc in [
      ("Impatient Scroller", "25-year-old who skips every ad. Watches YouTube on mobile during commute. Only stops for humor or shock value. Hates sales pitches."),
      ("Intentional Viewer", "35-year-old who watches ads if they're relevant. Desktop viewer, often in research mode. Will watch 15s if the topic matches their search."),
      ("Background Listener", "28-year-old who plays YouTube in the background while working. Audio matters more than visual. Will skip if the first sound is a jingle."),
  ]:
      p = requests.post(f"{MV_BASE}/personas", headers=MV_H, json={
          "name": name, "description": desc,
      }).json()
      persona_ids.append(p["id"])
      time.sleep(0.3)

  # 4. Focus Group — "Would you skip?"
  result_block = "\n\n".join(
      f"AD {i+1}: \"{r['label']}\"\n"
      f"  Hook Score: {r['hook_score']}/100 — {r['hook_assessment']}\n"
      f"  Visual Complexity: {r['visual_complexity']}/10 | Text Legibility: {r['text_legibility']}/10\n"
      f"  Emotional Trigger: {r['emotional_trigger']} | Cognitive Load: {r['cognitive_load']}/10"
      for i, r in enumerate(results)
  )

  fg = requests.post(f"{MV_BASE}/focus-groups", headers=MV_H, json={
      "name": "Pre-Roll Skip Test",
      "persona_ids": persona_ids,
      "questions": [
          f"You're watching a YouTube video and these pre-roll ads play. For each, answer: Would you SKIP or WATCH? Why?\n\n{result_block}",
          "Which ad's first 2 seconds grabbed you? What specifically hooked you?",
          "Rewrite the opening line of the weakest ad to make you NOT skip.",
          "Rate each ad 1-10 on 'Would I click through to the website?'",
      ],
      "responses_per_persona": 2,
  }).json()

  for _ in range(20):
      time.sleep(5)
      data = requests.get(f"{MV_BASE}/focus-groups/{fg['id']}", headers=MV_H).json()
      if data.get("status") == "completed":
          break

  print("PRE-ROLL AD SKIP TEST")
  print("=" * 60)
  for r in results:
      skip_predict = "WATCH" if r["hook_score"] > 70 else "LIKELY SKIP" if r["hook_score"] > 45 else "SKIP"
      print(f"  {r['label']:<45} Hook:{r['hook_score']:>3}/100  → {skip_predict}")

  print("\nFOCUS GROUP RESPONSES:")
  for resp in data.get("responses", []):
      print(f"\n[{resp.get('persona_id', '?')[:20]}] {resp.get('question', '')[:60]}...")
      print(f"  → {resp.get('answer', '')[:300]}")
  ```

  ```javascript JavaScript theme={"dark"}
  const MV = process.env.MAVERA_API_KEY;
  const MV_BASE = "https://app.mavera.io/api/v1";
  const MV_H = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
  const fs = await import("fs");

  const PRE_ROLL_CANDIDATES = [
    { path: "ads/preroll_a_product_demo.mp4", label: "Product Demo — Fast Cuts" },
    { path: "ads/preroll_b_testimonial.mp4", label: "Customer Testimonial — Face First" },
    { path: "ads/preroll_c_problem_hook.mp4", label: "Problem Hook — Text Overlay" },
    { path: "ads/preroll_d_humor.mp4", label: "Humor — Unexpected Opening" },
  ];

  // 1. Upload and analyze
  const analyses = [];
  for (const c of PRE_ROLL_CANDIDATES) {
    const formData = new FormData();
    formData.append("file", new Blob([fs.readFileSync(c.path)], { type: "video/mp4" }), `${c.label}.mp4`);
    const upload = await fetch(`${MV_BASE}/assets`, {
      method: "POST", headers: { Authorization: `Bearer ${MV}` }, body: formData,
    }).then(r => r.json());

    const analysis = await fetch(`${MV_BASE}/video-analysis`, {
      method: "POST", headers: MV_H,
      body: JSON.stringify({
        asset_id: upload.id,
        analysis_types: ["hook_score", "visual_complexity", "text_legibility", "emotional_arc", "cognitive_load", "pacing"],
        options: { focus_window: "0-5s" },
        metadata: { label: c.label, format: "pre-roll" },
      }),
    }).then(r => r.json());
    analyses.push({ id: analysis.id, label: c.label });
    await new Promise(r => setTimeout(r, 500));
  }

  // 2. Poll
  const results = [];
  for (const a of analyses) {
    let status;
    for (let i = 0; i < 30; i++) {
      await new Promise(r => setTimeout(r, 3000));
      status = await fetch(`${MV_BASE}/video-analysis/${a.id}`, { headers: MV_H }).then(r => r.json());
      if (status.status === "completed") break;
    }
    const r = status.results || {};
    results.push({
      label: a.label, hookScore: r.hook_score?.score || 0,
      hookAssessment: r.hook_score?.assessment || "",
      visualComplexity: r.visual_complexity?.score || 0,
      textLegibility: r.text_legibility?.score || 0,
      emotionalTrigger: r.emotional_arc?.peak_emotion || "none",
      cognitiveLoad: r.cognitive_load?.average || 0,
    });
  }

  // 3. Personas
  const personaIds = [];
  for (const [name, desc] of [
    ["Impatient Scroller", "25yo, skips every ad, mobile, commute. Only humor/shock stops scroll."],
    ["Intentional Viewer", "35yo, watches relevant ads, desktop, research mode. Will watch 15s if relevant."],
    ["Background Listener", "28yo, background YouTube while working. Audio > visual. Skips jingles."],
  ]) {
    const p = await fetch(`${MV_BASE}/personas`, { method: "POST", headers: MV_H,
      body: JSON.stringify({ name, description: desc }) }).then(r => r.json());
    personaIds.push(p.id);
    await new Promise(r => setTimeout(r, 300));
  }

  // 4. Focus Group
  const resultBlock = results.map((r, i) =>
    `AD ${i + 1}: "${r.label}"\n  Hook: ${r.hookScore}/100 — ${r.hookAssessment}\n  Visual: ${r.visualComplexity}/10 | Text: ${r.textLegibility}/10\n  Emotion: ${r.emotionalTrigger} | Cog Load: ${r.cognitiveLoad}/10`
  ).join("\n\n");

  const fg = await fetch(`${MV_BASE}/focus-groups`, {
    method: "POST", headers: MV_H,
    body: JSON.stringify({
      name: "Pre-Roll Skip Test", persona_ids: personaIds,
      questions: [
        `Pre-roll ads below. SKIP or WATCH each? Why?\n\n${resultBlock}`,
        "Which first 2 seconds grabbed you? What hooked you?",
        "Rewrite the weakest ad's opener to make you NOT skip.",
        "Rate each 1-10: 'Would I click through?'",
      ],
      responses_per_persona: 2,
    }),
  }).then(r => r.json());

  let data;
  for (let i = 0; i < 20; i++) {
    await new Promise(r => setTimeout(r, 5000));
    data = await fetch(`${MV_BASE}/focus-groups/${fg.id}`, { headers: MV_H }).then(r => r.json());
    if (data.status === "completed") break;
  }

  console.log("PRE-ROLL AD SKIP TEST");
  console.log("=".repeat(60));
  for (const r of results) {
    const verdict = r.hookScore > 70 ? "WATCH" : r.hookScore > 45 ? "LIKELY SKIP" : "SKIP";
    console.log(`  ${r.label.padEnd(45)} Hook:${String(r.hookScore).padStart(3)}/100  → ${verdict}`);
  }
  console.log("\nFOCUS GROUP RESPONSES:");
  for (const resp of data.responses || []) {
    console.log(`\n[${(resp.persona_id || "?").slice(0, 20)}] ${(resp.question || "").slice(0, 60)}...`);
    console.log(`  → ${(resp.answer || "").slice(0, 300)}`);
  }
  ```
</CodeGroup>

## Example Output

```text theme={"dark"}
PRE-ROLL AD SKIP TEST
============================================================
  Product Demo — Fast Cuts                      Hook: 52/100  → LIKELY SKIP
  Customer Testimonial — Face First             Hook: 78/100  → WATCH
  Problem Hook — Text Overlay                   Hook: 85/100  → WATCH
  Humor — Unexpected Opening                    Hook: 91/100  → WATCH

FOCUS GROUP RESPONSES:

[Impatient Scroller] Pre-roll ads below. SKIP or WATCH each?...
  → AD 1: SKIP — opens with a product shot, my finger is already on Skip.
    AD 2: WATCH — face in first frame creates social obligation to listen.
    AD 3: WATCH — text overlay "You're wasting $400/month" hits a nerve.
    AD 4: WATCH — unexpected visual makes me curious. I'll give it 5 more seconds.

[Intentional Viewer] Rate each 1-10: 'Would I click through?'...
  → AD 1: 4/10 — generic product demo. AD 2: 7/10 — real person builds trust.
    AD 3: 9/10 — problem framing matches my search intent. AD 4: 6/10 — funny
    but unclear what the product does.

[Background Listener] Rewrite the weakest ad's opener...
  → AD 1 original: [product spinning on white background, corporate music]
    Rewrite: Open with a voice saying "Stop. You're about to skip this, but
    you're also about to waste $400 this month." — audio-first grabs background
    listeners like me.
```

## Error Handling

<AccordionGroup>
  <Accordion title="Focus window option">The `focus_window: "0-5s"` option restricts analysis to the opening seconds. If unsupported, the full video is analyzed — results are still valid but include post-skip content.</Accordion>
  <Accordion title="Pre-roll format requirements">YouTube pre-roll ads are 6s (bumper), 15s, or 30s. Ensure uploaded files match these lengths. Longer files are analyzed in full, diluting the skip-window focus.</Accordion>
  <Accordion title="Focus Group persona calibration">The "Impatient Scroller" persona skews negative — expected. Weight their feedback for skip prediction, but weight "Intentional Viewer" for click-through prediction.</Accordion>
</AccordionGroup>

***

<CardGroup cols={2}>
  <Card title="YouTube Integration" icon="arrow-left" href="/integrations/youtube" />

  <Card title="Focus Groups API" icon="comments" href="/api-reference/focus-groups" />
</CardGroup>
