Building an Automated YouTube Audit System: From 5-Hour Manual Process to One-Click Pipeline
At AIR Media-Tech, someone on the team used to spend 5+ hours manually auditing a single YouTube channel. They would pull data from YouTube Studio, classify the channel's niche, find competitors by hand, analyze content patterns, and write a report.
I built a system that does all of it automatically. Click a button, come back later, download the full audit report.
This post covers the architecture, the AI pipeline, and the hardest problems I solved building it.
What AI Audit does
AI Audit takes a YouTube channel URL and runs a 7-step analysis pipeline:
Step 1: Scrape. Pulls all channel data via the YouTube API. Up to 500 videos per format (shorts, long-form, lives) from separate playlists. If the channel has connected YouTube Analytics via OAuth, the system pre-fetches 60 days of reporting data: per-video metrics (views, watch time, likes, shares, revenue, average view duration) across 3 time windows, plus per-playlist metrics.
Step 2: Vertical Detection. OpenAI classifies the channel using AIR's proprietary methodology (40 fields across 4 groups: vertical, format, creator archetype, and monetization signals). The AI outputs structured JSON with confidence scores. If the channel is in a non-English language, all analysis happens in English internally and gets translated for the output.
Step 3: Competitor Search. The 3-stage pipeline I built for Oxys, now hardened: AI internet search finds candidates, YouTube API verifies they exist, AI verifies they are genuine competitors using real data. Users can approve, reject, or add competitors manually.
Step 4: Deep Analysis. Nine AI analysis jobs run in sequence: Packaging (title/thumbnail/hook analysis), Retention, Portfolio Efficiency (classify each video as SCALE/OPTIMIZE/MAINTAIN/TEST/PAUSE), Cross-Format Flow, Distribution, Community, Monetization (covering 27 revenue streams), Cross-Platform, and Gap Synthesis.
A custom StratifiedVideoSampler selects 40-50 representative videos across top, bottom, middle, and recent buckets. This prevents the AI from only analyzing the channel's biggest hits or most recent uploads.
Step 5: Report Generation. Ten report jobs produce the final output: MLA (multi-layer analysis), Growth Playbook, Risk Register, Roadmap, Forecast, Opportunity Snapshot, Market Proof, AIR Partnership Offer, Cover Hook, and Final Assembly. The assembled report includes an evidence index linking every claim to specific data.
Progressive ZIP downloads are available after every step. You do not have to wait for the full pipeline to get useful data.
The technical stack
AI Audit started as a Streamlit app inside Oxys. When it outgrew that environment, I rebuilt it as a standalone Laravel application to match AIR's main product (AIR Translate).
| Layer | Technology |
|---|---|
| Backend | Laravel 13 + PHP 8.4 |
| Frontend | Vue 3 + Inertia v3 + Vite + Tailwind 4 |
| Database | SQLite (local), MySQL (production) |
| Queue | Redis + Laravel Horizon (9 workers across 4 queues) |
| AI (analysis) | OpenAI gpt-4.1-mini |
| AI (search) | Gemini 3 Flash Preview |
| YouTube | YouTube Data API v3 + YouTube Analytics API + YouTube Reporting API |
Why two AI providers
OpenAI handles all analysis and generation. It produces more reliable structured JSON output and does not randomly refuse requests.
Gemini handles internet search. It can ground its responses in real web results, which OpenAI cannot do. For competitor discovery and platform volatility research, grounded search is essential.
This split is the same multi-model approach from Oxys. Route tasks to the model that does them best.
Queue architecture
The pipeline has 25+ jobs that run in sequence. Each step can take 30 seconds to 10 minutes depending on the channel size. Running this synchronously would time out any web request.
I split the work across 4 Redis queues managed by Laravel Horizon:
| Queue | Workers | What runs there |
|---|---|---|
scraping | 2 | YouTube API calls (rate-limited, slow) |
ai | 3 | All AI analysis and generation (most contention) |
processing | 2 | ZIP packaging, CSV generation (CPU/IO bound) |
default | 2 | Catch-all for future jobs |
Every job implements ShouldBeUnique (no duplicate concurrent execution) and uses updateOrCreate for all database writes (idempotent retries). If a job fails and the queue retries it, nothing gets corrupted.
The Reporting API problem
YouTube's Reporting API provides the deepest data: impressions, end screen clicks, per-video revenue breakdowns. But it delivers that data as daily CSV dumps that can contain over a million rows per day for large channels.
My first approach loaded all rows into memory. It crashed on large channels.
The rewrite uses stream aggregation: process the CSV line by line, aggregate in memory by date and video ID, and write to the database in batches. This handles 1.25 million rows per day without running out of memory.
There was also the pre-fetch timing problem. The Reporting API does not serve data on demand. You request a report type, and it becomes available hours later. My solution: pre-fetch all report types when the channel first connects via OAuth. Store the data in a cache on the YouTubeChannel model. When an audit needs reporting data, it reads from cache instead of making API calls.
Daily append jobs update the cache at 06:00 UTC so it stays current without hammering the API.
Hardest problems I solved
Gemini response truncation
Gemini sometimes returns truncated JSON. The response looks valid at first glance but is missing the closing brackets. My vertical detection pipeline was silently storing partial data until I added strict JSON schema validation on every response.
The fix: set responseMimeType: application/json and thinkingBudget: 0 on every Gemini request. Validate the parsed result against the expected schema. If validation fails, reject the response and retry.
Boolean string coercion in PHP
YouTube's API returns boolean-like values as strings: "TRUE" and "FALSE". In PHP, the string "FALSE" is truthy because it is a non-empty string. My has_membership, has_affiliate, and similar fields were all evaluating as true regardless of the actual value.
This one cost me two hours of debugging. The fix was a one-line cast function, but finding it required tracing through the entire data pipeline.
Rate limiting across three APIs
AI Audit hits three different APIs, each with different rate limits:
- YouTube Data API: quota-based (10,000 units/day)
- YouTube Reporting API: 60 requests/minute
- OpenAI and Gemini: tokens per minute
I implemented per-API rate limiting with exponential backoff (3 attempts: 2s, 4s, 8s delays). The Reporting API downloads are throttled to 60/min. OAuth tokens refresh per report type to prevent 401 errors on long-running fetches.
The YouTube Data API quota was the tightest constraint. I added caching for getJobStatus() calls (1-hour cache) and skip API calls entirely when all reporting sections already have sufficient data. This cut quota usage by about 95%.
The result
A full channel audit runs from scrape to final report without human intervention. The output covers a growth playbook, risk register, roadmap, revenue analysis across 27 monetization streams, and an evidence index that links every recommendation to specific channel data.
The system runs on a single 8GB server with Laravel Horizon managing 9 queue workers. It handles concurrent audits and recovers from API failures without manual restarts.
What used to take one person 5+ hours now takes zero human time. The AI processes more data than manual analysis ever could: 500 videos per format, 60 days of analytics, 27 revenue streams. Patterns that would take a person days to spot come out in the report automatically.
It is currently in production at ai-audit.airmedia.tech, serving AIR's partnership and content teams.