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
Code
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]}")
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)}`);
}
}
Example Output
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
Private video access
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.Video still processing
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.Threshold calibration
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.
What’s Next
Vimeo Integration
Back to Vimeo integration overview
Engagement Scoring Correlation
Correlate Mavera scores with real engagement
Video Analysis API
Full reference for POST /api/v1/video-analysis
Focus Groups API
Full reference for POST /api/v1/focus-groups