> ## 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.

# Video Thumbnail A/B with Focus Groups

> Pull YouTube thumbnails, run Focus Groups with viewer personas, and get click-prediction scores with specific visual feedback

## Scenario

YouTube thumbnails drive 80%+ of click decisions, but you can't A/B test them natively. This job pulls your video thumbnails (and optionally competitor thumbnails) via `videos.list`, extracts the thumbnail URLs, then runs a Focus Group where personas evaluate each thumbnail with the core question: "Which would you click? Rate 1-10." The result is a click-prediction scorecard with specific visual feedback from each persona — before you commit to a thumbnail.

## Architecture

```mermaid theme={"dark"}
flowchart LR
    A["YouTube videos.list (thumbnails)"] --> B[Extract thumbnail URLs] --> C["POST /api/v1/focus-groups"] --> D[Click likelihood ratings + feedback]
```

## Code

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

  YT = os.environ["YOUTUBE_API_KEY"]
  MV = os.environ["MAVERA_API_KEY"]
  YT_BASE = "https://www.googleapis.com/youtube/v3"
  MV_BASE = "https://app.mavera.io/api/v1"
  MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  VIDEO_IDS = ["VIDEO_ID_1", "VIDEO_ID_2", "VIDEO_ID_3", "VIDEO_ID_4"]

  # 1. Fetch video details with thumbnails (1 quota unit)
  details = requests.get(f"{YT_BASE}/videos", params={
      "key": YT, "id": ",".join(VIDEO_IDS),
      "part": "snippet,statistics",
  }).json()

  if "error" in details:
      raise SystemExit(f"YouTube API error: {details['error']['message']}")

  thumbnails = []
  for video in details.get("items", []):
      snippet = video["snippet"]
      stats = video.get("statistics", {})
      thumb_url = (
          snippet.get("thumbnails", {}).get("maxres", {}).get("url")
          or snippet.get("thumbnails", {}).get("high", {}).get("url")
          or snippet.get("thumbnails", {}).get("medium", {}).get("url", "")
      )

      thumbnails.append({
          "video_id": video["id"],
          "title": snippet["title"],
          "thumbnail_url": thumb_url,
          "views": int(stats.get("viewCount", 0)),
          "ctr_context": f"{snippet['channelTitle']} — {snippet['title'][:60]}",
      })

  print(f"Collected {len(thumbnails)} thumbnails")

  # 2. Create Focus Group personas
  persona_ids = []
  for name, desc in [
      ("Casual Browser", "Scrolls YouTube homepage on phone. Clicks based on thumbnail emotion and curiosity gap. Ignores text-heavy thumbnails. Prefers faces and bright colors."),
      ("Search-Intent Viewer", "Types specific queries into YouTube search. Clicks thumbnails that visually confirm the video matches their query. Values clarity over creativity."),
      ("Subscriber", "Regular subscriber who sees these in their feed. Already trusts the channel. Clicks based on topic interest — thumbnail is secondary but influences priority."),
      ("Competitor's Audience", "Watches competitor channels. Unfamiliar with this brand. Thumbnail must prove higher quality or unique angle to earn a click away from known creators."),
  ]:
      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)

  # 3. Build thumbnail descriptions for focus group
  thumb_block = "\n\n".join(
      f"THUMBNAIL {chr(65+i)}: \"{t['title']}\"\n"
      f"  Image URL: {t['thumbnail_url']}\n"
      f"  Channel context: {t['ctr_context']}\n"
      f"  Current views: {t['views']:,}"
      for i, t in enumerate(thumbnails)
  )

  # 4. Run Focus Group
  fg = requests.post(f"{MV_BASE}/focus-groups", headers=MV_H, json={
      "name": "YouTube Thumbnail A/B Test",
      "persona_ids": persona_ids,
      "questions": [
          f"You see these thumbnails on YouTube. Which would you click FIRST? Rate each 1-10 for click likelihood.\n\n{thumb_block}",
          "What specific visual element in your top-rated thumbnail made you want to click?",
          "Which thumbnail would you NEVER click? What turns you off about it?",
          "If you could redesign the lowest-rated thumbnail, what would you change? Be specific about colors, text, faces, and composition.",
          "Imagine all these thumbnails appeared in a row on the YouTube homepage. Which stands out most and why?",
      ],
      "responses_per_persona": 2,
  }).json()

  # 5. Poll for results
  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

  # 6. Output
  print("\nTHUMBNAIL A/B FOCUS GROUP RESULTS")
  print("=" * 60)
  for t in thumbnails:
      print(f"  [{t['video_id'][:11]}] {t['title'][:50]}")
      print(f"    Thumbnail: {t['thumbnail_url']}")
      print(f"    Views: {t['views']:,}\n")

  print("PERSONA RESPONSES:")
  for resp in data.get("responses", []):
      persona_name = next(
          (name for pid, name in zip(persona_ids, ["Casual Browser", "Search-Intent", "Subscriber", "Competitor's Audience"])
           if pid == resp.get("persona_id")),
          "Unknown"
      )
      print(f"\n[{persona_name}] {resp.get('question', '')[:60]}...")
      print(f"  → {resp.get('answer', '')[:350]}")
  ```

  ```javascript JavaScript theme={"dark"}
  const YT = process.env.YOUTUBE_API_KEY;
  const MV = process.env.MAVERA_API_KEY;
  const YT_BASE = "https://www.googleapis.com/youtube/v3";
  const MV_BASE = "https://app.mavera.io/api/v1";
  const MV_H = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };

  const VIDEO_IDS = ["VIDEO_ID_1", "VIDEO_ID_2", "VIDEO_ID_3", "VIDEO_ID_4"];

  // 1. Fetch thumbnails (1 quota unit)
  const details = await fetch(
    `${YT_BASE}/videos?key=${YT}&id=${VIDEO_IDS.join(",")}&part=snippet,statistics`
  ).then(r => r.json());

  if (details.error) throw new Error(details.error.message);

  const thumbnails = (details.items || []).map(v => {
    const thumbs = v.snippet.thumbnails || {};
    return {
      videoId: v.id, title: v.snippet.title,
      thumbnailUrl: thumbs.maxres?.url || thumbs.high?.url || thumbs.medium?.url || "",
      views: parseInt(v.statistics?.viewCount || "0", 10),
      ctrContext: `${v.snippet.channelTitle} — ${v.snippet.title.slice(0, 60)}`,
    };
  });

  console.log(`Collected ${thumbnails.length} thumbnails`);

  // 2. Personas
  const personaNames = ["Casual Browser", "Search-Intent Viewer", "Subscriber", "Competitor's Audience"];
  const personaDescs = [
    "Scrolls homepage on phone. Clicks on emotion/curiosity. Ignores text-heavy. Prefers faces, bright colors.",
    "Types specific queries. Clicks thumbnails that confirm match. Values clarity over creativity.",
    "Regular subscriber, sees in feed. Trusts channel. Clicks on topic interest.",
    "Watches competitor channels. Unfamiliar with brand. Needs proof of quality to click.",
  ];
  const personaIds = [];
  for (let i = 0; i < personaNames.length; i++) {
    const p = await fetch(`${MV_BASE}/personas`, {
      method: "POST", headers: MV_H,
      body: JSON.stringify({ name: personaNames[i], description: personaDescs[i] }),
    }).then(r => r.json());
    personaIds.push(p.id);
    await new Promise(r => setTimeout(r, 300));
  }

  // 3. Thumbnail descriptions
  const thumbBlock = thumbnails.map((t, i) =>
    `THUMBNAIL ${String.fromCharCode(65 + i)}: "${t.title}"\n  Image: ${t.thumbnailUrl}\n  Context: ${t.ctrContext}\n  Views: ${t.views.toLocaleString()}`
  ).join("\n\n");

  // 4. Focus Group
  const fg = await fetch(`${MV_BASE}/focus-groups`, {
    method: "POST", headers: MV_H,
    body: JSON.stringify({
      name: "YouTube Thumbnail A/B Test", persona_ids: personaIds,
      questions: [
        `Rate each thumbnail 1-10 for click likelihood:\n\n${thumbBlock}`,
        "What visual element in your top pick made you want to click?",
        "Which would you NEVER click? What turns you off?",
        "Redesign the lowest-rated: what changes to colors, text, faces, composition?",
        "All thumbnails in a row on the homepage — which stands out and why?",
      ],
      responses_per_persona: 2,
    }),
  }).then(r => r.json());

  // 5. Poll
  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;
  }

  // 6. Output
  console.log("\nTHUMBNAIL A/B FOCUS GROUP RESULTS");
  console.log("=".repeat(60));
  thumbnails.forEach(t => {
    console.log(`  [${t.videoId.slice(0, 11)}] ${t.title.slice(0, 50)}`);
    console.log(`    Thumbnail: ${t.thumbnailUrl}\n    Views: ${t.views.toLocaleString()}\n`);
  });

  console.log("PERSONA RESPONSES:");
  for (const resp of data.responses || []) {
    const idx = personaIds.indexOf(resp.persona_id);
    const name = idx >= 0 ? personaNames[idx] : "Unknown";
    console.log(`\n[${name}] ${(resp.question || "").slice(0, 60)}...`);
    console.log(`  → ${(resp.answer || "").slice(0, 350)}`);
  }
  ```
</CodeGroup>

## Example Output

```text theme={"dark"}
THUMBNAIL A/B FOCUS GROUP RESULTS
============================================================
  [dQw4w9WgXc] How to Build a Morning Routine That Sticks
    Thumbnail: https://i.ytimg.com/vi/.../maxresdefault.jpg
    Views: 245,000

  [xYz1234567] 5 Nutrition Myths Debunked by Science
    Thumbnail: https://i.ytimg.com/vi/.../maxresdefault.jpg
    Views: 89,000

  [aBc9876543] I Tried Every Protein Powder — Here's the Winner
    Thumbnail: https://i.ytimg.com/vi/.../maxresdefault.jpg
    Views: 412,000

  [mNoPqRsTuV] Beginner Workout Plan (No Equipment)
    Thumbnail: https://i.ytimg.com/vi/.../maxresdefault.jpg
    Views: 178,000

PERSONA RESPONSES:

[Casual Browser] Rate each thumbnail 1-10 for click likelihood...
  → A: 6/10 — sunrise photo is calming but doesn't create urgency.
    B: 8/10 — red "X" marks on food items create instant curiosity.
    C: 9/10 — face with surprised expression + product grid = clickable.
    D: 5/10 — generic gym stock photo. Seen this a thousand times.

[Search-Intent Viewer] What visual element made you click?...
  → Thumbnail C: The grid layout showing all products immediately tells
    me this video answers my exact query. I don't need to guess what the
    video covers — the thumbnail IS the table of contents.

[Competitor's Audience] Which would you NEVER click?...
  → Thumbnail D. It looks identical to every other fitness channel. If I
    already watch [Competitor], why would I click a generic thumbnail from
    a channel I don't recognize? Give me a reason to switch.

[Subscriber] Redesign the lowest-rated thumbnail...
  → Thumbnail D: Replace stock gym photo with the creator's face doing the
    workout. Add text overlay "0 Equipment" in bold yellow. Use a split
    frame: left side = living room, right side = results. The current
    design says "another workout video" — the redesign says "YOUR workout."
```

## Error Handling

<AccordionGroup>
  <Accordion title="Thumbnail resolution fallback">Not all videos have `maxres` thumbnails (1280×720). The code falls back to `high` (480×360) then `medium` (320×180). Custom thumbnails require the channel owner to verify their account.</Accordion>
  <Accordion title="Focus Group with image URLs">Mavera Focus Groups analyze thumbnail descriptions and URLs contextually. For pixel-level visual analysis, upload thumbnails as image assets via `POST /assets` first and reference the asset IDs.</Accordion>
  <Accordion title="Quota cost">This entire job uses 1 quota unit (`videos.list` with up to 50 IDs per call). The most quota-efficient job alongside trending analysis.</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>
