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

# Pre-Publish Creative Testing

> Gate video publishing with Mavera quality thresholds and synthetic focus group feedback before going public on Vimeo

### Scenario

Your team uploads a draft video to Vimeo as a private link for stakeholder review — but the review is subjective and inconsistent. This job intercepts the draft after upload, pulls it via the Vimeo API, runs Video Analysis to score it against your quality thresholds, and if any metric falls below the threshold, automatically triggers a Focus Group asking: "What could make this more compelling?" The result is a publish-or-revise decision backed by quantitative scores and synthetic panel feedback — before the video goes public.

### Architecture

```mermaid theme={"dark"}
flowchart LR
    A["Upload draft to Vimeo (private)"] --> B["GET /videos/{id}"] --> C["Mavera POST /assets"] --> D["POST /video-analysis"] --> E[Score vs threshold] --> F["POST /focus-groups (if below)"] --> G[Publish decision + revision notes]
```

### Code

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

  VM = os.environ["VIMEO_ACCESS_TOKEN"]
  MV = os.environ["MAVERA_API_KEY"]
  VM_BASE = "https://api.vimeo.com"
  MV_BASE = "https://app.mavera.io/api/v1"
  VM_H = {"Authorization": f"Bearer {VM}", "Accept": "application/vnd.vimeo.*+json;version=3.4"}
  MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  DRAFT_VIDEO_ID = "123456789"
  THRESHOLDS = {
      "message_clarity": 70,
      "emotional_impact": 65,
      "hook_score": 60,
      "behavioral_effectiveness": 60,
  }

  # 1. Fetch the draft video from Vimeo
  draft = requests.get(f"{VM_BASE}/videos/{DRAFT_VIDEO_ID}", headers=VM_H, params={
      "fields": "uri,name,link,duration,privacy,status,pictures.sizes",
  }).json()

  if "error" in draft:
      raise SystemExit(f"Vimeo API error: {draft.get('developer_message', draft.get('error', ''))}")

  print(f"Draft: \"{draft['name']}\" | Duration: {draft.get('duration', 0)}s | Privacy: {draft.get('privacy', {}).get('view', 'unknown')}")

  # 2. Upload to Mavera and run Video Analysis
  upload = requests.post(f"{MV_BASE}/assets", headers=MV_H, json={
      "url": draft["link"], "name": f"[DRAFT] {draft['name'][:70]}", "type": "video",
  }).json()

  analysis = requests.post(f"{MV_BASE}/video-analysis", headers=MV_H, json={
      "asset_id": upload["id"],
      "analysis_types": list(THRESHOLDS.keys()) + ["pacing", "cognitive_load", "emotional_arc"],
      "metadata": {"vimeo_id": DRAFT_VIDEO_ID, "stage": "pre-publish"},
  }).json()

  # 3. Poll for results
  for _ in range(30):
      time.sleep(3)
      status = requests.get(
          f"{MV_BASE}/video-analysis/{analysis['id']}", headers=MV_H
      ).json()
      if status.get("status") == "completed":
          break

  results = status.get("results", {})
  scores = {
      metric: results.get(metric, {}).get("score", 0)
      for metric in THRESHOLDS
  }

  # 4. Check thresholds
  failures = {
      metric: {"score": scores[metric], "threshold": threshold}
      for metric, threshold in THRESHOLDS.items()
      if scores[metric] < threshold
  }

  print(f"\nSCORECARD:")
  for metric, threshold in THRESHOLDS.items():
      score = scores[metric]
      verdict = "PASS" if score >= threshold else "FAIL"
      print(f"  {metric:<30} {score:>3}/100  (threshold: {threshold})  [{verdict}]")

  if not failures:
      print("\n✓ ALL THRESHOLDS MET — Ready to publish")
  else:
      print(f"\n✗ {len(failures)} THRESHOLD(S) FAILED — Triggering Focus Group review")

      # 5. Create Focus Group for improvement feedback
      persona_ids = []
      for name, desc in [
          ("Target Customer", "35-year-old marketing director evaluating SaaS tools. Watches product videos to assess vendor quality. Judges professionalism, clarity, and whether the video respects their time."),
          ("Creative Director", "Senior creative with 15 years in video production. Evaluates pacing, visual storytelling, emotional arc, and hook effectiveness. Gives specific, actionable feedback."),
          ("Skeptical Buyer", "CFO who dislikes marketing fluff. Wants data, proof points, and clear ROI messaging. Will disengage immediately if the video feels like a hard sell."),
      ]:
          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)

      failure_summary = "\n".join(
          f"- {m}: scored {f['score']}/100 (needs {f['threshold']}+)"
          for m, f in failures.items()
      )
      arc_summary = results.get("emotional_arc", {}).get("summary", "No arc data available")

      fg = requests.post(f"{MV_BASE}/focus-groups", headers=MV_H, json={
          "name": f"Pre-Publish Review — {draft['name'][:40]}",
          "persona_ids": persona_ids,
          "questions": [
              f"This video failed quality thresholds:\n{failure_summary}\n\nEmotional arc: {arc_summary}\n\nWhat specifically could make this video more compelling?",
              "If you could change only ONE thing about the first 5 seconds, what would it be?",
              "What is this video trying to make you feel? Is it working? What emotion is missing?",
              "Would you share this video with a colleague? Why or why not? What would change your answer?",
          ],
          "responses_per_persona": 2,
      }).json()

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

      print("\nFOCUS GROUP FEEDBACK:")
      for resp in fg_data.get("responses", []):
          print(f"\n[{resp.get('persona_name', '?')}] {resp.get('question', '')[:60]}...")
          print(f"  → {resp.get('answer', '')[:400]}")
  ```

  ```javascript JavaScript theme={"dark"}
  const VM = process.env.VIMEO_ACCESS_TOKEN;
  const MV = process.env.MAVERA_API_KEY;
  const VM_BASE = "https://api.vimeo.com";
  const MV_BASE = "https://app.mavera.io/api/v1";
  const VM_H = { Authorization: `Bearer ${VM}`, Accept: "application/vnd.vimeo.*+json;version=3.4" };
  const MV_H = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };

  const DRAFT_VIDEO_ID = "123456789";
  const THRESHOLDS = {
    message_clarity: 70, emotional_impact: 65,
    hook_score: 60, behavioral_effectiveness: 60,
  };

  // 1. Fetch draft
  const draft = await fetch(
    `${VM_BASE}/videos/${DRAFT_VIDEO_ID}?fields=uri,name,link,duration,privacy,status`,
    { headers: VM_H }
  ).then(r => r.json());

  if (draft.error) throw new Error(draft.developer_message || draft.error);
  console.log(`Draft: "${draft.name}" | ${draft.duration}s | ${draft.privacy?.view}`);

  // 2. Mavera analysis
  const upload = await fetch(`${MV_BASE}/assets`, {
    method: "POST", headers: MV_H,
    body: JSON.stringify({ url: draft.link, name: `[DRAFT] ${draft.name.slice(0, 70)}`, type: "video" }),
  }).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: [...Object.keys(THRESHOLDS), "pacing", "cognitive_load", "emotional_arc"],
      metadata: { vimeo_id: DRAFT_VIDEO_ID, stage: "pre-publish" },
    }),
  }).then(r => r.json());

  // 3. Poll
  let status;
  for (let i = 0; i < 30; i++) {
    await new Promise(r => setTimeout(r, 3000));
    status = await fetch(
      `${MV_BASE}/video-analysis/${analysis.id}`, { headers: MV_H }
    ).then(r => r.json());
    if (status.status === "completed") break;
  }

  const results = status.results || {};
  const scores = Object.fromEntries(
    Object.keys(THRESHOLDS).map(m => [m, results[m]?.score || 0])
  );

  // 4. Threshold check
  const failures = {};
  for (const [metric, threshold] of Object.entries(THRESHOLDS)) {
    if (scores[metric] < threshold) {
      failures[metric] = { score: scores[metric], threshold };
    }
  }

  console.log("\nSCORECARD:");
  for (const [metric, threshold] of Object.entries(THRESHOLDS)) {
    const verdict = scores[metric] >= threshold ? "PASS" : "FAIL";
    console.log(`  ${metric.padEnd(30)} ${String(scores[metric]).padStart(3)}/100  (threshold: ${threshold})  [${verdict}]`);
  }

  if (Object.keys(failures).length === 0) {
    console.log("\nALL THRESHOLDS MET — Ready to publish");
  } else {
    console.log(`\n${Object.keys(failures).length} THRESHOLD(S) FAILED — Triggering Focus Group`);

    // 5. Focus Group
    const personaIds = [];
    for (const [name, desc] of [
      ["Target Customer", "35yo marketing director evaluating SaaS. Judges professionalism, clarity, time-respect."],
      ["Creative Director", "15yr video production veteran. Evaluates pacing, storytelling, arc, hooks. Gives specific fixes."],
      ["Skeptical Buyer", "CFO who hates fluff. Wants data, proof, ROI. Disengages at hard sells."],
    ]) {
      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));
    }

    const failureSummary = Object.entries(failures)
      .map(([m, f]) => `- ${m}: scored ${f.score}/100 (needs ${f.threshold}+)`).join("\n");
    const arcSummary = results.emotional_arc?.summary || "No arc data";

    const fg = await fetch(`${MV_BASE}/focus-groups`, {
      method: "POST", headers: MV_H,
      body: JSON.stringify({
        name: `Pre-Publish Review — ${draft.name.slice(0, 40)}`,
        persona_ids: personaIds,
        questions: [
          `Failed thresholds:\n${failureSummary}\n\nArc: ${arcSummary}\n\nWhat could make this more compelling?`,
          "Change ONE thing about the first 5 seconds — what?",
          "What is this video trying to make you feel? Is it working?",
          "Would you share this with a colleague? What would change your answer?",
        ],
        responses_per_persona: 2,
      }),
    }).then(r => r.json());

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

    console.log("\nFOCUS GROUP FEEDBACK:");
    for (const resp of fgData.responses || []) {
      console.log(`\n[${resp.persona_name || "?"}] ${(resp.question || "").slice(0, 60)}...`);
      console.log(`  → ${(resp.answer || "").slice(0, 400)}`);
    }
  }
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
Draft: "Q2 Product Launch — Feature Overview" | Duration: 94s | Privacy: nobody

SCORECARD:
  message_clarity                 72/100  (threshold: 70)  [PASS]
  emotional_impact                48/100  (threshold: 65)  [FAIL]
  hook_score                      55/100  (threshold: 60)  [FAIL]
  behavioral_effectiveness        63/100  (threshold: 60)  [PASS]

2 THRESHOLD(S) FAILED — Triggering Focus Group review

FOCUS GROUP FEEDBACK:

[Target Customer] Failed thresholds: emotional_impact 48, hook_score 55...
  → The video opens with a logo animation — that's 3 seconds of dead air where
    I'm already deciding to close the tab. Start with the problem I have, not
    your brand. Show me a frustrated user, then show me the fix. The emotional
    impact is low because it's a feature list disguised as a video.

[Creative Director] Change ONE thing about the first 5 seconds...
  → Kill the logo intro entirely. Open on a tight shot of someone's face
    reacting to a problem — frustration, confusion, overwhelm. You have 2
    seconds to create emotional investment. The current opener is visual
    wallpaper. Sound design matters too: add a tension cue in the first second.

[Skeptical Buyer] Would you share this with a colleague?...
  → No. It doesn't give me ammunition for an internal business case. I need
    this video to say "this saves 4 hours/week" or "this reduces error rates by
    30%." Right now it says "look at our features." Add one concrete metric in
    the first 15 seconds and I'll forward it to my team.
```

### Error Handling

<AccordionGroup>
  <Accordion title="Private video access">Private Vimeo videos require the token owner to be the video owner or a team member. The `privacy.view` field should be `nobody` (private link) or `password`. Videos set to `disable` cannot be accessed via API.</Accordion>
  <Accordion title="Video still processing">Newly uploaded Vimeo videos may have `status: "uploading"` or `"transcoding"`. Wait for `status: "available"` before sending to Mavera. Poll `GET /videos/{id}?fields=status` every 10 seconds.</Accordion>
  <Accordion title="Threshold calibration">Start with conservative thresholds (60-70) and tighten as your library builds. Analyze your top 10 performing videos first to establish baseline scores for each metric.</Accordion>
</AccordionGroup>

***

## What's Next

<CardGroup cols={2}>
  <Card title="Vimeo Integration" icon="circle-play" href="/integrations/vimeo">
    Back to Vimeo integration overview
  </Card>

  <Card title="Engagement Scoring Correlation" icon="chart-line" href="/integrations/vimeo/engagement-scoring-correlation">
    Correlate Mavera scores with real engagement
  </Card>

  <Card title="Video Analysis API" icon="chart-bar" href="/api-reference/video-analysis">
    Full reference for POST /api/v1/video-analysis
  </Card>

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