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

# Ad Creative Audit

> Upload a quarter's worth of video ads, score each with Video Analysis, then synthesize a ranked audit report with Mave

## Mavera Surfaces

| Surface                                             | Role                                                                   |
| --------------------------------------------------- | ---------------------------------------------------------------------- |
| **Files** (`POST /files/upload-url`, `POST /files`) | Upload each video ad to Mavera                                         |
| **Video Analysis** (`POST /video-analyses`)         | Frame-level scoring: engagement, emotion, attention, brand recall, CTA |
| **Mave** (`POST /mave/chat`)                        | Synthesize all scores into a ranked quarterly audit                    |

***

## What Value Does Mavera Add?

| Value                 | How                                                                                                              |
| --------------------- | ---------------------------------------------------------------------------------------------------------------- |
| **Insurance**         | Every ad gets an objective score before you commit next quarter's budget. No more "I think Ad 3 was fine."       |
| **Opening new doors** | Side-by-side ranking surfaces patterns you'd never spot manually — like a CTA placement trend across 8 ads.      |
| **Saving time**       | A manual creative review meeting takes 2–4 hours. This pipeline runs in minutes and produces a shareable report. |

***

## When to Use This

* End of quarter: score everything you shipped, rank it, carry learnings into next quarter's briefs.
* Pre-budget allocation: prove which creative styles deserve more spend.
* Agency handoff: give your agency a data-backed scorecard instead of subjective feedback.
* Creative retrospective: identify which elements (hooks, music, CTA timing) correlated with higher scores.

***

## What You Need

| Requirement                        | Details                                                                                              |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------- |
| **Mavera API key**                 | Starts with `mvra_live_`. Get one at [Developer Settings](https://app.mavera.io/settings/developer). |
| **Workspace ID**                   | From your dashboard URL (`ws_...`).                                                                  |
| **4–12 video ads**                 | MP4 or MOV, 15–60 s each. More is fine — the pipeline scales linearly.                               |
| **Credits**                        | \~100–250 per video + \~15–30 for Mave. See [Credits Estimate](#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
```

***

## The Flow

<Steps>
  <Step title="Collect video files">
    Gather all ads shipped last quarter into a single directory. Name them descriptively — `q4_hero_30s.mp4`, `q4_retargeting_15s.mp4` — because filenames appear in the Mave prompt.
  </Step>

  <Step title="Upload each video via Files API">
    Presigned URL → PUT bytes → create file record. Collect asset IDs.
  </Step>

  <Step title="Run Video Analysis on every ad">
    Create analyses in a batch, then poll until all complete.
  </Step>

  <Step title="Normalize, rank, and synthesize with Mave">
    Build a sorted metrics table, feed it to Mave: "Rank these ads by performance potential. Which elements should we keep, and which should we change?"
  </Step>
</Steps>

```mermaid theme={"dark"}
flowchart LR
    A["Collect Ads"] --> B["Upload"]
    B --> C["Video Analysis"]
    C --> D["Rank"]
    D --> E["Mave Report"]
```

***

## Stage 1 — Upload Videos

<CodeGroup>
  ```python Python theme={"dark"}
  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"}


  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_all(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 p in paths:
          asset = upload_video(p)
          print(f"  Uploaded {asset['name']} → {asset['id']}")
          assets.append(asset)
      return assets
  ```

  ```javascript JavaScript theme={"dark"}
  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" };

  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 uploadAll(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 (const fp of files) { const a = await uploadVideo(fp); console.log(`  Uploaded ${a.name} → ${a.id}`); assets.push(a); }
    return assets;
  }
  ```
</CodeGroup>

<Tip>
  Name your files descriptively (`q4_hero_30s.mp4`, not `video_3.mp4`). Filenames appear in the Mave prompt and make the final report far more readable.
</Tip>

***

## Stage 2 — Batch Video Analysis

Create all analyses up front, then poll. Mavera processes them concurrently — you wait roughly once, not per-ad.

<CodeGroup>
  ```python Python theme={"dark"}
  def create_analysis(asset_id: str, label: str) -> dict:
      resp = requests.post(f"{BASE}/video-analyses", headers=HEADERS, json={
          "title": f"Q4 Audit: {label}", "asset_id": asset_id,
          "goal": "Score ad effectiveness: engagement, emotion, attention, brand recall, CTA",
          "brand": "Brand", "product": "Product",
          "primary_intent": "Drive purchase consideration",
          "chunk_duration": 5, "frames_per_chunk": 3, "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_batch(assets: list[dict]) -> list[dict]:
      jobs = []
      for asset in assets:
          job = create_analysis(asset["id"], asset["name"])
          print(f"  Created analysis {job['id']} for {asset['name']}")
          jobs.append({"analysis_id": job["id"], "name": asset["name"]})

      results = []
      for job in jobs:
          result = poll_analysis(job["analysis_id"])
          metrics = result.get("results", {}).get("full_video_metrics", {})
          print(f"  Completed {job['name']}: overall={metrics.get('overall_score')}")
          results.append({"name": job["name"], "analysis_id": job["analysis_id"], "metrics": metrics})
      return results
  ```

  ```javascript JavaScript theme={"dark"}
  async function createAnalysis(assetId, label) {
    const resp = await fetch(`${BASE}/video-analyses`, {
      method: "POST", headers: HEADERS,
      body: JSON.stringify({
        title: `Q4 Audit: ${label}`, asset_id: assetId,
        goal: "Score ad effectiveness: engagement, emotion, attention, brand recall, CTA",
        brand: "Brand", product: "Product", primary_intent: "Drive purchase consideration",
        chunk_duration: 5, frames_per_chunk: 3, 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 analyzeBatch(assets) {
    const jobs = [];
    for (const asset of assets) {
      const job = await createAnalysis(asset.id, asset.name);
      console.log(`  Created analysis ${job.id} for ${asset.name}`);
      jobs.push({ analysisId: job.id, name: asset.name });
    }
    const results = [];
    for (const job of jobs) {
      const result = await pollAnalysis(job.analysisId);
      const metrics = result.results?.full_video_metrics || {};
      console.log(`  Completed ${job.name}: overall=${metrics.overall_score}`);
      results.push({ name: job.name, analysisId: job.analysisId, metrics });
    }
    return results;
  }
  ```
</CodeGroup>

***

## Stage 3 — Rank and Format

<CodeGroup>
  ```python Python theme={"dark"}
  def rank_results(results: list[dict]) -> list[dict]:
      ranked = sorted(results, key=lambda r: r["metrics"].get("overall_score", 0), reverse=True)
      for i, r in enumerate(ranked):
          r["rank"] = i + 1
      return ranked


  def format_metrics_table(ranked: list[dict]) -> str:
      lines = ["| Rank | Ad | Overall | Emotion | Attention | Brand Recall | CTA |",
               "|------|----|---------|---------|-----------|--------------|----|"]
      for r in ranked:
          m = r["metrics"]
          lines.append(f"| {r['rank']} | {r['name']} | {m.get('overall_score', '—')}/100 "
                        f"| {m.get('emotional_impact', '—')}/10 | {m.get('attention_score', '—')}/10 "
                        f"| {m.get('brand_recall_likelihood', '—')} | {m.get('cta_effectiveness', '—')}/10 |")
      return "\n".join(lines)


  def format_chunk_highlights(results: list[dict]) -> str:
      highlights = []
      for r in results:
          chunks = r["metrics"].get("chunks", [])
          if not chunks:
              continue
          best = max(chunks, key=lambda c: c.get("engagement", 0))
          worst = min(chunks, key=lambda c: c.get("engagement", 0))
          highlights.append(f"- **{r['name']}**: peak at {best.get('start_time', '?')}s "
                            f"({best.get('engagement', '?')}), low at {worst.get('start_time', '?')}s "
                            f"({worst.get('engagement', '?')})")
      return "\n".join(highlights)
  ```

  ```javascript JavaScript theme={"dark"}
  function rankResults(results) {
    const ranked = [...results].sort((a, b) => (b.metrics.overall_score || 0) - (a.metrics.overall_score || 0));
    ranked.forEach((r, i) => (r.rank = i + 1));
    return ranked;
  }

  function formatMetricsTable(ranked) {
    const lines = ["| Rank | Ad | Overall | Emotion | Attention | Brand Recall | CTA |",
                    "|------|----|---------|---------|-----------|--------------|----|"];
    for (const r of ranked) {
      const m = r.metrics;
      lines.push(`| ${r.rank} | ${r.name} | ${m.overall_score ?? "—"}/100 ` +
        `| ${m.emotional_impact ?? "—"}/10 | ${m.attention_score ?? "—"}/10 ` +
        `| ${m.brand_recall_likelihood ?? "—"} | ${m.cta_effectiveness ?? "—"}/10 |`);
    }
    return lines.join("\n");
  }

  function formatChunkHighlights(results) {
    return results.filter((r) => r.metrics.chunks?.length).map((r) => {
      const chunks = r.metrics.chunks;
      const best = chunks.reduce((a, b) => (b.engagement || 0) > (a.engagement || 0) ? b : a);
      const worst = chunks.reduce((a, b) => (b.engagement || 0) < (a.engagement || 0) ? b : a);
      return `- **${r.name}**: peak at ${best.start_time ?? "?"}s (${best.engagement ?? "?"}), low at ${worst.start_time ?? "?"}s (${worst.engagement ?? "?"})`;
    }).join("\n");
  }
  ```
</CodeGroup>

***

## Stage 4 — Mave Synthesis

<CodeGroup>
  ```python Python theme={"dark"}
  def generate_audit(ranked: list[dict]) -> str:
      table = format_metrics_table(ranked)
      highlights = format_chunk_highlights(ranked)

      prompt = f"""You are a senior creative strategist conducting a quarterly ad creative audit.

  ## Ranked Ad Performance
  {table}

  ## Per-Ad Chunk Highlights
  {highlights}

  ## Your Task
  Produce a quarterly creative audit:
  1. **Executive Summary** — 3-sentence overview of the quarter's creative performance.
  2. **Ranked Scorecard** — Table with commentary on each ad's strengths and weaknesses.
  3. **Elements to Keep** — Which creative elements (hooks, music, pacing, CTA placement, talent) appear in top ads? Be specific.
  4. **Elements to Change** — Which elements correlate with lower scores? Patterns, not individual ads.
  5. **Hook Analysis** — Compare the first chunk across all ads. Which opening strategies won?
  6. **Brand Recall Deep Dive** — Why did some ads score higher? Logo placement? Early mention?
  7. **Recommendations for Next Quarter** — 5 specific, actionable briefs for the creative team.

  Cite specific ads by name. Use the scores above as evidence."""

      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"]
  ```

  ```javascript JavaScript theme={"dark"}
  async function generateAudit(ranked) {
    const table = formatMetricsTable(ranked);
    const highlights = formatChunkHighlights(ranked);

    const prompt = `You are a senior creative strategist conducting a quarterly ad creative audit.

  ## Ranked Ad Performance
  ${table}

  ## Per-Ad Chunk Highlights
  ${highlights}

  ## Your Task
  Produce a quarterly creative audit:
  1. **Executive Summary** — 3-sentence overview.
  2. **Ranked Scorecard** — Table with commentary on strengths and weaknesses.
  3. **Elements to Keep** — Which creative elements appear in top ads?
  4. **Elements to Change** — Which elements correlate with lower scores?
  5. **Hook Analysis** — Compare first chunks. Which opening strategies won?
  6. **Brand Recall Deep Dive** — Why did some ads score higher?
  7. **Recommendations for Next Quarter** — 5 specific briefs for the creative team.

  Cite specific ads by name. Use scores as evidence.`;

    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;
  }
  ```
</CodeGroup>

***

## Running the Full Pipeline

<CodeGroup>
  ```python Python theme={"dark"}
  def run_audit(ad_directory: str = "./q4_ads"):
      print("=== Quarterly Ad Creative Audit ===\n")

      print("Stage 1: Uploading videos...")
      assets = upload_all(ad_directory)
      print(f"  Uploaded {len(assets)} ads\n")

      print("Stage 2: Running Video Analysis (batch)...")
      results = analyze_batch(assets)
      print(f"  All {len(results)} analyses complete\n")

      print("Stage 3: Ranking...")
      ranked = rank_results(results)
      print(format_metrics_table(ranked))

      print("\nStage 4: Generating audit with Mave...")
      report = generate_audit(ranked)

      with open("quarterly_creative_audit.md", "w") as f:
          f.write(f"# Quarterly Creative Audit — {time.strftime('%B %Y')}\n\n{report}")
      print("Saved to quarterly_creative_audit.md")
      return ranked, report

  if __name__ == "__main__":
      import sys
      run_audit(sys.argv[1] if len(sys.argv) > 1 else "./q4_ads")
  ```

  ```javascript JavaScript theme={"dark"}
  async function runAudit(adDirectory = "./q4_ads") {
    console.log("=== Quarterly Ad Creative Audit ===\n");

    console.log("Stage 1: Uploading videos...");
    const assets = await uploadAll(adDirectory);
    console.log(`  Uploaded ${assets.length} ads\n`);

    console.log("Stage 2: Running Video Analysis (batch)...");
    const results = await analyzeBatch(assets);
    console.log(`  All ${results.length} analyses complete\n`);

    console.log("Stage 3: Ranking...");
    const ranked = rankResults(results);
    console.log(formatMetricsTable(ranked));

    console.log("\nStage 4: Generating audit with Mave...");
    const report = await generateAudit(ranked);

    const month = new Date().toLocaleString("default", { month: "long", year: "numeric" });
    fs.writeFileSync("quarterly_creative_audit.md", `# Quarterly Creative Audit — ${month}\n\n${report}`);
    console.log("Saved to quarterly_creative_audit.md");
    return { ranked, report };
  }

  runAudit(process.argv[2] || "./q4_ads");
  ```
</CodeGroup>

***

## Example Output

```markdown theme={"dark"}
# Quarterly Creative Audit — March 2026

## Executive Summary
Q4 shipped 8 ads averaging 71/100. Top: q4_hero_30s (89/100, pain-point hook,
early brand mention). Bottom two: slow establishing shots, <40/100 first-chunk.

| Rank | Ad              | Overall | Emotion | CTA  |
|------|-----------------|---------|---------|------|
| 1    | q4_hero_30s     | 89/100  | 9/10    | 8/10 |
| 2    | q4_promo_15s    | 82/100  | 8/10    | 9/10 |
| 3    | q4_story_45s    | 74/100  | 8/10    | 6/10 |

## Keep: Pain-point hooks, early brand mention, fast cuts (≤3s shots)
## Change: Slow establishing shots (5+ seconds), late CTA (after 25s)
```

***

## Variations

<AccordionGroup>
  <Accordion title="Monthly cadence instead of quarterly">
    Run the same pipeline monthly with 2–4 ads. Track score trends by appending to a CSV:

    ```python theme={"dark"}
    import csv
    with open("audit_trend.csv", "a", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=["date", "ad", "overall", "emotion", "attention"])
        for r in ranked:
            writer.writerow({"date": time.strftime("%Y-%m-%d"), "ad": r["name"],
                             "overall": r["metrics"].get("overall_score"),
                             "emotion": r["metrics"].get("emotional_impact"),
                             "attention": r["metrics"].get("attention_score")})
    ```
  </Accordion>

  <Accordion title="Filter by ad type">
    Separate brand-awareness from performance/retargeting ads — different goals shouldn't share a ranking scale.

    ```python theme={"dark"}
    brand_ads = [r for r in results if "brand" in r["name"].lower()]
    perf_ads = [r for r in results if "retarget" in r["name"].lower() or "promo" in r["name"].lower()]
    brand_report = generate_audit(rank_results(brand_ads))
    perf_report = generate_audit(rank_results(perf_ads))
    ```
  </Accordion>

  <Accordion title="Add Focus Group validation for top and bottom">
    Take #1 and last-place ads into a Focus Group to validate scores with simulated audience reactions.
  </Accordion>

  <Accordion title="Webhook instead of polling">
    Pass `webhook_url` when creating analyses. Mavera POSTs to your endpoint on completion.

    ```python theme={"dark"}
    resp = requests.post(f"{BASE}/video-analyses", headers=HEADERS, json={
        "webhook_url": "https://your-server.com/hooks/analysis-complete",
        # ... other fields ...
    })
    ```
  </Accordion>

  <Accordion title="Include chunk-level timelines in the report">
    For deeper audits, include per-chunk engagement curves in the Mave prompt.

    ```python theme={"dark"}
    def format_timeline(results):
        return "\n".join(
            f"- {r['name']}: [{', '.join(f'{c.get(\"start_time\", 0)}s:{c.get(\"engagement\", 0)}' for c in r['metrics'].get('chunks', []))}]"
            for r in results
        )
    ```
  </Accordion>
</AccordionGroup>

***

## Credits Estimate

| Stage               | Typical Cost    | Notes                             |
| ------------------- | --------------- | --------------------------------- |
| File uploads (×N)   | 0               | Free                              |
| Video Analysis (×N) | 100–250 each    | Depends on video length (15–60 s) |
| Mave synthesis      | 15–30           | Single research query             |
| **8-ad audit**      | **\~850–2,050** | Conservative upper bound          |
| **4-ad audit**      | **\~430–1,030** | Smaller batch                     |

<Tip>
  Start with your 4 most important ads to validate the pipeline at lower cost. Scale to the full quarter once you trust the scoring.
</Tip>

<Warning>
  Video Analysis cost scales with video length. A 60-second ad costs \~2.5× a 15-second ad. If budget-constrained, trim videos to the first 30 seconds before upload.
</Warning>

***

## See Also

<CardGroup cols={2}>
  <Card title="Hook Analysis Sprint" icon="stopwatch" href="/playbooks/hook-analysis-sprint">
    Zoom into the first 3 seconds across 10 variants
  </Card>

  <Card title="Competitor Reel" icon="film" href="/playbooks/competitor-reel">
    Benchmark your ads against competitors
  </Card>

  <Card title="Video + Focus Group Double" icon="layer-group" href="/playbooks/video-focus-group-double">
    Layer AI scoring with synthetic audience interpretation
  </Card>

  <Card title="Video Analysis" icon="video" href="/features/video-analysis">
    Metrics reference, chunk options, and chat endpoint
  </Card>

  <Card title="Mave Agent" icon="brain" href="/features/mave-agent">
    Research agent with sources and validation
  </Card>

  <Card title="Credits & Budget" icon="coins" href="/cookbooks/credits-budget-alerts">
    Pre-flight checks and budget alerts
  </Card>
</CardGroup>
