Google AI Classroom Part 7: Serverless Map-Reduce-Map Video Intelligence & Rubric-Driven Practical Task Auto-Grading with Gemini 4 Argon
Author: Cyrus Wong (éŧäŋå―Ĩ) - Google Developer Expert - Cloud (AI), Senior Lecturer at HKIIT
The 80-Hour Lab Grading Bottleneck & The HTTP 429 Quota Wall
In hands-on cloud computing, DevSecOps, and full-stack software engineering courses, students spend 2 to 3 hours building infrastructure in the AWS/GCP Console, writing Terraform modules in VS Code, and debugging container deployments in the terminal.
Reviewing a single 40-student cohort's 2-hour screen recordings manually requires 80 hours of instructor time per lab.
However, when our engineering team first attempted to automate post-class video grading by firing 40 concurrent multimodal video analysis calls against the Gemini API at the end of a lab session, two hard distributed systems walls appeared immediately:
- The Serverless Execution Timeout Wall (
540s): Even a 2nd-Gen Cloud Run-backed Cloud Function caps synchronous execution at 540 seconds (9 minutes). Sequentially evaluating 40 multi-megabyte lab recordings inside a single monolithic worker inevitably times out mid-cohort, leaving half the class ungraded and corrupting aggregate progress counters. - Gemini API HTTP 429 RESOURCE_EXHAUSTED Rate-Limit Spikes: Sending 40 uncompressed 30-FPS screen recordings simultaneously floods regional Tokens-Per-Minute (TPM) and Requests-Per-Minute (RPM) quotas, triggering cascading HTTP 429 rate-limit errors.
- LLM Scoring Drift & Hallucinated Arithmetic: When evaluating multi-step practical exams, raw LLM JSON responses occasionally award 25/20 points on a rubric step, omit a milestone when a student stops early, or miscalculate the sum of individual step scores.
In Part 7 of our 10-part Google Developer Expert engineering series, we walk through how we solved all three challenges by combining:
- Cloud Run FFmpeg 1-FPS H.264 Timelapse Compilation (shrinking a 2-hour lab into a <20 MB forensic MP4 and slashing multimodal video token consumption by 90%+).
- Google Cloud Tasks Rate-Limited Fan-Out (
maxDispatchesPerSecond: 2,maxConcurrentDispatches: 4) for zero-timeout, zero-429 distributed video processing. - Two-Stage Map-Reduce-Map Cohort Discovery (
processVideoAnalysisJobâanalyzeSingleVideoTaskâgenerateLabTaskPrompt), enabling teachers to select ð§ Gemini 4 Argon (gemini-4-argon), ⥠Gemini 3.8 Flash (gemini-3.8-flash), or ðŠķ Gemini 3.5 Flash-Lite (gemini-3.5-flash-lite) to automatically discover what milestones a class achieved and synthesize a tool-calling grading rubric. - A 4-Tab Practical Task & Deterministic Rubric Grading Engine (
extractTaskDemoSteps.js,evaluateTaskSubmission.js,TaskEditorModal.jsx, andTaskGradingMatrixView.jsx) with Ultrafast Stream-Copy Retroactive Video Slicing (-c copy) and a deterministic mathematical guardrail (normalizeRubricEvaluation/reconcileStepEvaluationsWithRubric).
From Periodic Screenshots to Forensic 1-FPS H.264 MP4s (80% Storage & 90% Token Reduction)
Why record a 30-FPS desktop video stream when a student coding in VS Code or configuring a VPC in the browser changes meaningful visual state every 5 to 15 seconds?
Before Gemini ever evaluates a student's work, StudentView.jsx and our high-memory Cloud Run worker processVideoJob.js (2 vCPU, 8 GiB RAM, 540s timeout) transform periodic screenshots into a razor-sharp, tamper-evident 1-FPS H.264 MP4 timelapse that is 80% smaller than continuous screen recordings:
Engineering Highlights of the Cloud Run FFmpeg Pipeline
Single-Pass Client Geometric Scaling: On 4K Retina displays, raw canvas captures can exceed 3 MB. Instead of an expensive multi-pass compression loop in the browser, StudentView.jsx computes a single-pass geometric scaling ratio:
$$\text{scale} = \sqrt{\frac{\text{maxImageSize}}{\text{blob.size}}} \times 0.9$$
This guarantees every uploaded frame stays well beneath the 2 MB storage.rules threshold while preserving crisp terminal text legibility at up to 1920px (1080p).
Memory-Bounded Sharp SVG Watermarking (BATCH_SIZE = 15): Inside processVideoJob.js, downloading and compositing 720 high-resolution frames concurrently via Promise.all() would spike heap memory and crash the container. We process frames in chunks of BATCH_SIZE = 15, using sharp to burn a 40px forensic SVG banner onto every frame (YYYY-MM-DD HH:mm:ss | Class ID | Student Email | [SCREEN]/[WEBCAM]) and rounding dimensions to even integers (w - (w % 2)) required by the H.264 yuv420p planar chroma format.
Why Burn-In Timestamps Matter for Gemini 4: Because the compiled MP4 plays back at 1 frame per second (where 1 second of video equals 15 seconds of real classroom time), a naive LLM looking only at the MP4 container's playback clock would miscalculate how long a student spent on a task by a factor of $15\times$. Burning the real wall-clock timestamp directly into every frame allows Gemini 4 Argon (gemini-4-argon) and Gemini 3.8 Flash (gemini-3.8-flash) to read the exact on-screen clock via optical character recognition!
Two-Stage Map-Reduce-Map Cohort Video Discovery with Cloud Tasks & Gemini 4 Argon
What happens when an instructor runs an open-ended hands-on cloud lab - where students explore AWS/GCP services at their own pace - and wants AI to:
- Watch all 40 student timelapse videos without hitting rate limits (Phase 1: Map),
- Discover the common coursework milestones, point weights, and technical blockers encountered across the entire cohort (Phase 2: Reduce), and
- Re-evaluate every student against that newly synthesized rubric to populate a live Coursework Milestone Matrix (Phase 3: Second Map)?
In functions/ai_flows/index.mjs, generateLabTaskPrompt.js, VideoLibrary.jsx, and VideoAnalysisJobs.jsx, we built a Serverless Map-Reduce-Map Pipeline where teachers can select gemini-4-argon, gemini-3.8-flash, or gemini-3.5-flash-lite at both the Map and Reduce stages:
Production Code 1: The Reduce Stage - Synthesizing a Cohort Grading Rubric from 40 Student Videos
Let's examine functions/ai_flows/generateLabTaskPrompt.js. When Phase 1 (the exploratory Map pass) finishes, each student's raw activity observation is stored in aiJobs/{jobId}. Because Firestore's in query operator accepts at most 30 document IDs per query, generateLabTaskPrompt fetches the completed child jobs in 30-document chunks, compiles them into a cohort-wide activity corpus, and invokes generateWithResilience using the teacher's chosen model (such as gemini-4-argon or gemini-3.8-flash):
// Source: functions/ai_flows/generateLabTaskPrompt.js
export const generateLabTaskPrompt = onCall(
{
region: FUNCTION_REGION,
cors: CORS_ORIGINS,
timeoutSeconds: 120,
memory: '1GiB',
},
async (request) => {
if (!request.auth) {
throw new HttpsError('unauthenticated', 'The function must be called while authenticated.');
}
const { jobId, model } = request.data || {};
const masterJobRef = db.collection('videoAnalysisJobs').doc(jobId);
const jobDoc = await masterJobRef.get();
const jobData = jobDoc.data();
const classId = jobData.classId || 'unknown_class';
const aiJobIds = jobData.aiJobIds || [];
let completedJobs = [];
// Fetch child aiJobs via 30-document batched FieldPath.documentId() queries
if (aiJobIds.length > 0) {
for (let i = 0; i < aiJobIds.length; i += 30) {
const batchIds = aiJobIds.slice(i, i + 30);
const querySnapshot = await db.collection('aiJobs').where(FieldPath.documentId(), 'in', batchIds).get();
querySnapshot.forEach((doc) => {
const data = doc.data();
if (data.status === 'completed' && data.result && typeof data.result === 'string' && data.result.trim()) {
completedJobs.push(data);
}
});
}
}
// Compile observations across all students in the cohort
const compiledObservations = completedJobs.map((job, idx) => {
const studentIdentifier = job.studentEmail || job.studentUid || `Student ${idx + 1}`;
return `### Student ${idx + 1}: ${studentIdentifier}\n${job.result.trim()}`;
}).join('\n\n---\n\n');
const preferredModel = model || 'gemini-3.8-flash';
const { response, modelUsed } = await generateWithResilience(
{
prompt: [{ text: metaPrompt }],
config: { temperature: 0.2 },
},
preferredModel
);
const usage = response?.usage || response?.usageMetadata || {};
const cost = calculateCost(usage, modelUsed || preferredModel);
await logJob({
classId,
jobType: 'generateLabTaskPrompt',
status: 'completed',
promptText: metaPrompt,
mediaPaths: [],
usage: {
inputTokens: usage.inputTokens ?? usage.promptTokenCount ?? 0,
outputTokens: usage.outputTokens ?? usage.candidatesTokenCount ?? 0,
},
cost,
modelUsed: modelUsed || preferredModel,
result: generatedPrompt.trim().substring(0, 300),
masterJobId: jobId,
});
return {
generatedPrompt: generatedPrompt.trim(),
modelUsed,
summaryCount: completedJobs.length,
classId,
jobId,
};
}
);
The 3 Prompts Powering the Map-Reduce-Map Video Pipeline
To make this architecture concrete and reproducible, here are the exact prompts executed at each stage of the Map-Reduce-Map workflow:
1. Phase 1 (Exploratory Map Prompt) - Unsupervised Activity Discovery
In Phase 1, the lecturer does not need to write a lab rubric in advance. Each Cloud Tasks worker (analyzeSingleVideoTask â analyzeSingleVideoFlow in functions/ai_flows/analysisFlows.js) wraps the exploratory template (admin/prompts/videos/Summarize Student's Work.md) with the class's time-lapse capture interval (captureInterval = 15s) and dispatches it in parallel across all 40 student videos:
# Summarize Student's Activity
**You are an AI teaching assistant. Your task is to summarize the activity of each student based on their video.**
## Video Context
* The video is a fast-forwarded time-lapse of screen captures, not a real-time recording (compiled at 1 frame per second from screenshots captured roughly every {{captureInterval}} seconds).
* Each frame includes a burned-in timestamp (date and time). All calculations of duration (e.g., for attendance, concentration time) **MUST** be based on these in-frame timestamps.
* **Do not use the video's playback length for any time-based calculations.**
## Attendance & Engagement Analysis
* Compare the timeframe of the student's activity (derived from the in-frame timestamps) with the scheduled lesson time ({{startTime}} to {{endTime}}) to estimate attendance. Note any late starts, early leaves, or gaps in screen sharing.
* Based on the student's on-screen activity and focus, estimate the total "concentration minutes" during the observed period using the in-frame timestamps, and call the `recordActualWorkingTime` tool.
## Analysis Guidelines
* Identify the main tasks, cloud consoles, CLI commands, and code files the student is working on throughout the video.
* Describe the student's pace, milestones achieved, and any test scores or verification outputs visible on screen.
* Note any periods of inactivity, error messages, or technical struggle.
## Actions
* Call `recordActualWorkingTime` to record your estimate of the student's concentration minutes.
* Call `recordLessonSummary` to record a concise, informative summary of their activity, progress, and blockers.
* Provide the exact same summary as your final text response.
2. Phase 2 (Reduce Meta-Prompt) - Synthesizing a Cohort Rubric from 40 Observations
Once the Phase 1 Map workers finish, clicking âĻ Generate Lab Task Prompt in VideoAnalysisJobs.jsx invokes generateLabTaskPrompt.js, which injects all 40 student summaries into {{compiledObservations}} inside admin/prompts/videos/Two-Stage Map-Reduce Lab Rubric Synthesizer.md:
You are an expert Computer Science university curriculum designer and AI prompt engineer for classroom invigilation and automated lab assessment.
Below are student activity observations collected from an initial video analysis across a class session (Class ID: "{{classId}}"):
=== BEGIN OBSERVED STUDENT ACTIVITIES ===
{{compiledObservations}}
=== END OBSERVED STUDENT ACTIVITIES ===
Based on these actual classroom observations, analyze the student activities and generate an optimized, comprehensive, ready-to-use Markdown task prompt for a high-precision second-pass evaluation of this lab.
The generated prompt MUST strictly follow this Markdown structure:
# [Descriptive Lab Title Based on Discovered Coursework]
**You are an AI teaching assistant evaluating student screen recording time-lapses for [Lab Topic/Subject].**
## Video Timing Rules
* The video is a fast-forward time-lapse with on-screen timestamps.
* All duration, attendance, and task timing calculations **MUST** be derived from the in-frame date/time stamps, NOT the video playback length.
## Coursework Tasks to Evaluate
[Break down the lab into 3-5 clearly numbered, specific tasks discovered from the student observations. Include specific platforms (e.g. AWS, Azure, Cloud Shell, VS Code), repositories, scripts, resource names, and milestones].
## Milestones, Scores & Rubrics
[Detail any test numbers, scoring points (e.g., 10 pts, 20 pts), or completion verifications discovered from the observations].
## Known Blockers & Technical Obstacles to Watch For
[List the common technical errors, blockers, or pitfalls students encountered, such as backend errors, incorrect regions, or setup issues].
## Required Tool Actions
1. **Working Time**: Call 'recordActualWorkingTime' with the total estimated active concentration minutes (clamped to lesson duration).
2. **Task Durations**: For each identified task above, call 'recordTaskDuration' with studentUid, classId, taskName, and estimated durationMinutes spent on that task.
3. **Student Summary**: Call 'recordLessonSummary' with a structured bulleted summary of their progress across each task, points earned, blockers encountered,
Comments
No comments yet. Start the discussion.