Mavera Surfaces
| Surface | Role |
|---|---|
Files (POST /files/upload-url, POST /files) | Upload each hook variant |
Video Analysis (POST /video-analyses) | Frame-level scoring with chunk_duration: 3 to isolate the hook |
Mave (POST /mave/chat) | Compare first-chunk metrics across variants and recommend winners |
What Value Does Mavera Add?
| Value | How |
|---|---|
| Insurance | Test 10 hooks before committing media budget. Kill weak openers with data, not opinion. |
| Opening new doors | First-chunk emotional intensity and cognitive load scores reveal why a hook works — not just whether it does. |
| Saving time | Running 10 hook variants through human A/B testing takes weeks. This sprint takes minutes. |
When to Use This
- You have a hero ad concept and need to decide which opening 3 seconds to ship.
- You’re producing short-form content (TikTok, Reels, Shorts) where the hook is the ad.
- You want to quantify the “scroll-stopping power” of different opening strategies.
- You’re building a hook playbook for your creative team and need data to back guidelines.
What You Need
| Requirement | Details |
|---|---|
| Mavera API key | Starts with mvra_live_. Get one at Developer Settings. |
| Workspace ID | From your dashboard URL (ws_...). |
| 10 video variants | Same ad with 10 different hooks (first 3 seconds). MP4 or MOV, 6–15 s each. |
| Credits | ~100–200 per video + ~15–30 for Mave. See Credits Estimate. |
| Python 3.8+ or Node.js 18+ | requests for Python; native fetch for Node. |
MAVERA_API_KEY=mvra_live_your_key_here
MAVERA_WORKSPACE_ID=ws_your_workspace_id
Why 10 variants? Fewer than 5 doesn’t give enough signal. More than 15 adds cost without proportional insight. 10 is the sweet spot for a sprint.
The Flow
1
Prepare 10 hook variants
Same base ad, 10 different openings. Vary strategy: question, statistic, pain point, humor, visual shock, testimonial, product close-up, text overlay, music sting, silent open.
2
Upload all variants via Files API
Name by hook strategy:
hook_question.mp4, hook_humor.mp4.3
Run Video Analysis with chunk_duration: 3
The first chunk isolates exactly the hook. Higher
frames_per_chunk (5) gives denser sampling.4
Extract and compare first-chunk metrics
Pull
chunks[0] from each. Compare emotional_intensity, cognitive_load, engagement, attention.5
Synthesize with Mave
Feed the comparison table into Mave: “Rank these hooks. Which opening strategy wins and why?”
Stage 1 — Upload Hook Variants
import os, time, json, glob, requests
API_KEY = os.environ["MAVERA_API_KEY"]
WORKSPACE_ID = os.environ["MAVERA_WORKSPACE_ID"]
BASE = "https://app.mavera.io/api/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
HOOK_STRATEGIES = [
"question", "statistic", "pain_point", "humor", "visual_shock",
"testimonial", "product_closeup", "text_overlay", "music_sting", "silent_open",
]
def upload_video(path: str) -> dict:
with open(path, "rb") as f:
content = f.read()
name = os.path.basename(path)
mime = "video/mp4" if path.lower().endswith(".mp4") else "video/quicktime"
url_resp = requests.post(f"{BASE}/files/upload-url", headers=HEADERS, json={
"file_name": name, "file_type": mime, "file_size": len(content), "workspace_id": WORKSPACE_ID,
}).json()
if "error" in url_resp:
raise Exception(url_resp["error"]["message"])
requests.put(url_resp["upload_url"], data=content, headers={"Content-Type": mime}).raise_for_status()
file_rec = requests.post(f"{BASE}/files", headers=HEADERS, json={
"name": name, "type": mime, "url": url_resp["public_url"],
"workspace_id": WORKSPACE_ID, "file_size": len(content),
}).json()
if "error" in file_rec:
raise Exception(file_rec["error"]["message"])
return {"id": file_rec["id"], "name": name}
def upload_hook_variants(directory: str) -> list[dict]:
paths = sorted(glob.glob(os.path.join(directory, "*.mp4"))
+ glob.glob(os.path.join(directory, "*.mov")))
if not paths:
raise FileNotFoundError(f"No video files in {directory}")
assets = []
for i, p in enumerate(paths):
asset = upload_video(p)
asset["strategy"] = HOOK_STRATEGIES[i] if i < len(HOOK_STRATEGIES) else f"variant_{i+1}"
print(f" [{i+1}/{len(paths)}] {asset['name']} ({asset['strategy']}) → {asset['id']}")
assets.append(asset)
return assets
const fs = require("fs");
const path = require("path");
const API_KEY = process.env.MAVERA_API_KEY;
const WORKSPACE_ID = process.env.MAVERA_WORKSPACE_ID;
const BASE = "https://app.mavera.io/api/v1";
const HEADERS = { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" };
const HOOK_STRATEGIES = [
"question", "statistic", "pain_point", "humor", "visual_shock",
"testimonial", "product_closeup", "text_overlay", "music_sting", "silent_open",
];
async function uploadVideo(videoPath) {
const content = fs.readFileSync(videoPath);
const name = path.basename(videoPath);
const mime = videoPath.toLowerCase().endsWith(".mp4") ? "video/mp4" : "video/quicktime";
const urlResp = await fetch(`${BASE}/files/upload-url`, {
method: "POST", headers: HEADERS,
body: JSON.stringify({ file_name: name, file_type: mime, file_size: content.length, workspace_id: WORKSPACE_ID }),
}).then((r) => r.json());
if (urlResp.error) throw new Error(urlResp.error.message);
await fetch(urlResp.upload_url, { method: "PUT", body: content, headers: { "Content-Type": mime } });
const fileRec = await fetch(`${BASE}/files`, {
method: "POST", headers: HEADERS,
body: JSON.stringify({ name, type: mime, url: urlResp.public_url, workspace_id: WORKSPACE_ID, file_size: content.length }),
}).then((r) => r.json());
if (fileRec.error) throw new Error(fileRec.error.message);
return { id: fileRec.id, name };
}
async function uploadHookVariants(directory) {
const files = fs.readdirSync(directory).filter((f) => /\.(mp4|mov)$/i.test(f)).sort().map((f) => path.join(directory, f));
if (!files.length) throw new Error(`No video files in ${directory}`);
const assets = [];
for (let i = 0; i < files.length; i++) {
const asset = await uploadVideo(files[i]);
asset.strategy = HOOK_STRATEGIES[i] || `variant_${i + 1}`;
console.log(` [${i + 1}/${files.length}] ${asset.name} (${asset.strategy}) → ${asset.id}`);
assets.push(asset);
}
return assets;
}
Stage 2 — Video Analysis with Short Chunks
The key parameter:chunk_duration: 3. Combined with frames_per_chunk: 5 for maximum density in the hook window.
def create_hook_analysis(asset_id: str, strategy: str) -> dict:
resp = requests.post(f"{BASE}/video-analyses", headers=HEADERS, json={
"title": f"Hook Sprint: {strategy}", "asset_id": asset_id,
"goal": "Measure emotional intensity, cognitive load, and attention in the opening hook",
"brand": "Brand", "product": "Product",
"primary_intent": "Stop the scroll and drive watch-through",
"chunk_duration": 3, "frames_per_chunk": 5, "workspace_id": WORKSPACE_ID,
}).json()
if "error" in resp:
raise Exception(resp["error"]["message"])
return resp
def poll_analysis(analysis_id: str, timeout_min: int = 20) -> dict:
for _ in range(timeout_min * 4):
resp = requests.get(f"{BASE}/video-analyses/{analysis_id}", headers=HEADERS).json()
if "error" in resp:
raise Exception(resp["error"]["message"])
if resp["status"] == "COMPLETED":
return resp
if resp["status"] == "FAILED":
raise Exception(f"Analysis {analysis_id} failed")
time.sleep(15)
raise TimeoutError(f"Analysis {analysis_id} timed out")
def analyze_all_hooks(assets: list[dict]) -> list[dict]:
jobs = []
for asset in assets:
job = create_hook_analysis(asset["id"], asset["strategy"])
print(f" Created analysis {job['id']} for {asset['strategy']}")
jobs.append({"analysis_id": job["id"], "name": asset["name"], "strategy": asset["strategy"]})
results = []
for job in jobs:
result = poll_analysis(job["analysis_id"])
metrics = result.get("results", {}).get("full_video_metrics", {})
results.append({"name": job["name"], "strategy": job["strategy"],
"metrics": metrics, "chunks": metrics.get("chunks", [])})
print(f" Completed {job['strategy']}")
return results
async function createHookAnalysis(assetId, strategy) {
const resp = await fetch(`${BASE}/video-analyses`, {
method: "POST", headers: HEADERS,
body: JSON.stringify({
title: `Hook Sprint: ${strategy}`, asset_id: assetId,
goal: "Measure emotional intensity, cognitive load, and attention in the opening hook",
brand: "Brand", product: "Product",
primary_intent: "Stop the scroll and drive watch-through",
chunk_duration: 3, frames_per_chunk: 5, workspace_id: WORKSPACE_ID,
}),
}).then((r) => r.json());
if (resp.error) throw new Error(resp.error.message);
return resp;
}
async function pollAnalysis(analysisId, timeoutMin = 20) {
for (let i = 0; i < timeoutMin * 4; i++) {
const resp = await fetch(`${BASE}/video-analyses/${analysisId}`, { headers: HEADERS }).then((r) => r.json());
if (resp.error) throw new Error(resp.error.message);
if (resp.status === "COMPLETED") return resp;
if (resp.status === "FAILED") throw new Error(`Analysis ${analysisId} failed`);
await new Promise((r) => setTimeout(r, 15000));
}
throw new Error(`Analysis ${analysisId} timed out`);
}
async function analyzeAllHooks(assets) {
const jobs = [];
for (const asset of assets) {
const job = await createHookAnalysis(asset.id, asset.strategy);
console.log(` Created analysis ${job.id} for ${asset.strategy}`);
jobs.push({ analysisId: job.id, name: asset.name, strategy: asset.strategy });
}
const results = [];
for (const job of jobs) {
const result = await pollAnalysis(job.analysisId);
const metrics = result.results?.full_video_metrics || {};
results.push({ name: job.name, strategy: job.strategy, metrics, chunks: metrics.chunks || [] });
console.log(` Completed ${job.strategy}`);
}
return results;
}
Setting
chunk_duration below 3 may not provide enough frames for reliable scoring. 3 seconds is the minimum recommended.Stage 3 — Extract and Compare First-Chunk Metrics
The first chunk (chunks[0]) contains exactly the hook.
def extract_first_chunks(results: list[dict]) -> list[dict]:
hook_data = []
for r in results:
chunk = r["chunks"][0] if r["chunks"] else {}
hook_data.append({
"strategy": r["strategy"], "name": r["name"],
"emotional_intensity": chunk.get("emotional_intensity", 0),
"cognitive_load": chunk.get("cognitive_load", 0),
"engagement": chunk.get("engagement", 0),
"attention": chunk.get("attention", 0),
})
return sorted(hook_data, key=lambda x: x["emotional_intensity"], reverse=True)
def compute_hook_score(h: dict) -> float:
"""Weighted composite: emotion 40% + engagement 30% + attention 20% + inverse cog. load 10%."""
return (h["emotional_intensity"] * 0.4 + (h["engagement"] / 10) * 0.3
+ h["attention"] * 0.2 + (10 - h["cognitive_load"]) * 0.1)
def format_hook_table(hook_data: list[dict]) -> str:
lines = ["| Rank | Hook Strategy | Emotion | Cog. Load | Engagement | Attention |",
"|------|---------------|---------|-----------|------------|-----------|"]
for i, h in enumerate(hook_data):
lines.append(f"| {i+1} | {h['strategy']} | {h['emotional_intensity']}/10 "
f"| {h['cognitive_load']}/10 | {h['engagement']}/100 | {h['attention']}/10 |")
return "\n".join(lines)
def format_composite_ranking(hook_data: list[dict]) -> str:
scored = sorted(hook_data, key=compute_hook_score, reverse=True)
lines = ["| Rank | Hook Strategy | Composite | Emotion | Engagement | Attention | Cog. Load |",
"|------|---------------|-----------|---------|------------|-----------|-----------|"]
for i, h in enumerate(scored):
lines.append(f"| {i+1} | {h['strategy']} | {compute_hook_score(h):.1f}/10 "
f"| {h['emotional_intensity']}/10 | {h['engagement']}/100 "
f"| {h['attention']}/10 | {h['cognitive_load']}/10 |")
return "\n".join(lines)
function extractFirstChunks(results) {
return results
.map((r) => {
const chunk = r.chunks[0] || {};
return {
strategy: r.strategy, name: r.name,
emotionalIntensity: chunk.emotional_intensity || 0,
cognitiveLoad: chunk.cognitive_load || 0,
engagement: chunk.engagement || 0,
attention: chunk.attention || 0,
};
})
.sort((a, b) => b.emotionalIntensity - a.emotionalIntensity);
}
function computeHookScore(h) {
return h.emotionalIntensity * 0.4 + (h.engagement / 10) * 0.3 + h.attention * 0.2 + (10 - h.cognitiveLoad) * 0.1;
}
function formatHookTable(hookData) {
const lines = ["| Rank | Hook Strategy | Emotion | Cog. Load | Engagement | Attention |",
"|------|---------------|---------|-----------|------------|-----------|"];
hookData.forEach((h, i) => lines.push(
`| ${i + 1} | ${h.strategy} | ${h.emotionalIntensity}/10 | ${h.cognitiveLoad}/10 | ${h.engagement}/100 | ${h.attention}/10 |`
));
return lines.join("\n");
}
function formatCompositeRanking(hookData) {
const scored = [...hookData].sort((a, b) => computeHookScore(b) - computeHookScore(a));
const lines = ["| Rank | Hook Strategy | Composite | Emotion | Engagement | Attention | Cog. Load |",
"|------|---------------|-----------|---------|------------|-----------|-----------|"];
scored.forEach((h, i) => lines.push(
`| ${i + 1} | ${h.strategy} | ${computeHookScore(h).toFixed(1)}/10 | ${h.emotionalIntensity}/10 | ${h.engagement}/100 | ${h.attention}/10 | ${h.cognitiveLoad}/10 |`
));
return lines.join("\n");
}
Composite Score Weights
| Metric | Weight | Why |
|---|---|---|
| Emotional intensity | 40% | High-emotion hooks stop the scroll |
| Engagement | 30% | Predicts watch-through |
| Attention | 20% | Keeps the viewer in the first critical seconds |
| Inverse cognitive load | 10% | Hooks requiring too much processing lose viewers |
Cognitive load is inversely weighted — lower is better for hooks. A hook requiring mental effort to decode loses viewers before the message lands.
Stage 4 — Mave Synthesis
def generate_hook_report(hook_data: list[dict]) -> str:
raw_table = format_hook_table(hook_data)
composite = format_composite_ranking(hook_data)
prompt = f"""You are a creative director specializing in short-form video hooks.
## First-Chunk Metrics (0–3 seconds)
{raw_table}
## Composite Hook Score
{composite}
## Your Task
Produce a hook optimization report:
1. **Winner** — Which hook strategy wins and why? Cite specific scores.
2. **Runner-Up** — Second-best hook and what it does differently.
3. **Worst Performer** — Which strategy failed and why?
4. **Emotional Intensity Analysis** — What drives high emotion in the first 3 seconds?
5. **Cognitive Load Traps** — Which hooks overloaded viewers?
6. **Pattern Recognition** — Patterns across top 3 and bottom 3 hooks?
7. **Hook Playbook** — 5 rules for scroll-stopping hooks, derived from this data.
8. **Recommended A/B Test** — 2 hooks for paid testing and the hypothesis to validate.
Reference strategies by name. Cite scores."""
resp = requests.post(f"{BASE}/mave/chat", headers=HEADERS,
json={"message": prompt}, timeout=180).json()
if "error" in resp:
raise Exception(resp["error"]["message"])
return resp["content"]
async function generateHookReport(hookData) {
const rawTable = formatHookTable(hookData);
const composite = formatCompositeRanking(hookData);
const prompt = `You are a creative director specializing in short-form video hooks.
## First-Chunk Metrics (0–3 seconds)
${rawTable}
## Composite Hook Score
${composite}
## Your Task
Produce a hook optimization report:
1. **Winner** — Which hook strategy wins? Cite scores.
2. **Runner-Up** — Second-best and what it does differently.
3. **Worst Performer** — Which strategy failed and why?
4. **Emotional Intensity Analysis** — What drives high emotion in 3 seconds?
5. **Cognitive Load Traps** — Which hooks overloaded viewers?
6. **Pattern Recognition** — Patterns across top 3 and bottom 3?
7. **Hook Playbook** — 5 rules for scroll-stopping hooks from this data.
8. **Recommended A/B Test** — 2 hooks for paid testing, with hypothesis.
Reference strategies by name. Cite scores.`;
const resp = await fetch(`${BASE}/mave/chat`, {
method: "POST", headers: HEADERS,
body: JSON.stringify({ message: prompt }), signal: AbortSignal.timeout(180000),
}).then((r) => r.json());
if (resp.error) throw new Error(resp.error.message);
return resp.content;
}
Running the Full Sprint
def run_hook_sprint(variant_directory: str = "./hook_variants"):
assets = upload_hook_variants(variant_directory)
results = analyze_all_hooks(assets)
hook_data = extract_first_chunks(results)
print(format_composite_ranking(hook_data))
report = generate_hook_report(hook_data)
with open("hook_analysis_report.md", "w") as f:
f.write(f"# Hook Analysis Sprint — {time.strftime('%Y-%m-%d')}\n\n")
f.write(format_composite_ranking(hook_data))
f.write(f"\n\n---\n\n{report}")
print("Saved to hook_analysis_report.md")
return hook_data, report
if __name__ == "__main__":
import sys
run_hook_sprint(sys.argv[1] if len(sys.argv) > 1 else "./hook_variants")
async function runHookSprint(dir = "./hook_variants") {
const assets = await uploadHookVariants(dir);
const results = await analyzeAllHooks(assets);
const hookData = extractFirstChunks(results);
console.log(formatCompositeRanking(hookData));
const report = await generateHookReport(hookData);
const date = new Date().toISOString().split("T")[0];
fs.writeFileSync("hook_analysis_report.md",
`# Hook Analysis Sprint\n\n**Variants:** ${hookData.length} | **Date:** ${date}\n\n` +
`${formatCompositeRanking(hookData)}\n\n---\n\n${report}`);
return { hookData, report };
}
runHookSprint(process.argv[2] || "./hook_variants");
Example Output
# Hook Analysis Sprint Report
**Variants tested:** 10 | **Date:** 2026-03-17
| Rank | Hook Strategy | Composite | Emotion | Cog. Load | Engagement | Attention |
|------|---------------|-----------|---------|-----------|------------|-----------|
| 1 | pain_point | 8.8/10 | 9/10 | 3/10 | 87/100 | 9/10 |
| 2 | question | 7.8/10 | 8/10 | 4/10 | 82/100 | 8/10 |
| 3 | visual_shock | 7.5/10 | 9/10 | 6/10 | 79/100 | 9/10 |
| ... | ... | ... | ... | ... | ... | ... |
| 10 | silent_open | 3.5/10 | 3/10 | 2/10 | 31/100 | 4/10 |
## Winner
**pain_point** — composite 8.8/10. Highest emotion (9/10) with low cognitive
load (3/10). Emotional urgency + simplicity is the winning formula...
Variations
Platform-specific hook testing
Platform-specific hook testing
Run the same 10 hooks with different
primary_intent values for TikTok vs YouTube to see if the same hooks win on both platforms.Top 3 → Focus Group validation
Top 3 → Focus Group validation
After the sprint, run the top 3 hooks through a Focus Group for simulated audience preference data.
5-second hooks for longer-form content
5-second hooks for longer-form content
For YouTube pre-rolls or TV spots, change
chunk_duration to 5.Track hook performance over time
Track hook performance over time
Append each sprint to a CSV for quarter-over-quarter trends.
Isolate audio vs visual impact
Isolate audio vs visual impact
Create audio-muted and audio-only (black screen) versions. Compare to separate audio vs visual emotional drivers.
Credits Estimate
| Stage | Typical Cost | Notes |
|---|---|---|
| File uploads (×10) | 0 | Free |
| Video Analysis (×10) | 100–200 each | Short videos (6–15 s) cost less |
| Mave synthesis | 15–30 | Single research query |
| 10-variant sprint | ~1,015–2,030 | Conservative upper bound |
| 5-variant sprint | ~515–1,030 | Smaller test batch |
Short hook variants (6–10 seconds) keep Video Analysis costs low. You don’t need the full ad — just the hook plus a few seconds of context.
See Also
Ad Creative Audit
Score a full quarter of ads, not just hooks
Competitor Reel
Analyze competitor hooks alongside your own
Video + Focus Group Double
Layer hook scores with synthetic audience reactions
Video Analysis
Full metrics reference and chunk configuration
Mave Agent
Research agent for synthesis and recommendations
Credits & Budget
Pre-flight checks and budget alerts