AI & Computer Vision, lesson 47 of 52
Capstone Kick-off: Define Your Product
Introduction
Everything you've learned comes together now. Over the last six lessons your team builds a complete computer-vision product, from idea to live demo. Today you write the brief: who it's for, what it does, how you'll know it works, and what you won't build. It's the cheapest moment in the whole project to discover that an idea is too big.
By the end of this lesson you'll be able to:
- Describe a product as input, detection, trigger and output
- Write a one-sentence problem for a real person
- Write success criteria with a metric, an operator, a number and a unit, including false alarms and misses
- Name risks, each with a plan
- Count the scope and cut until it fits
- Use Claude as a critic and not as an author
Working principle
The capstone
For the last six sessions you build one product from camera to output, in teams, with everything you have learned:
lesson 47 define -> lesson 48 data -> lesson 49 train and improve -> lesson 50 test at a venue -> lesson 51 demo -> lesson 52 look back and aheadYour product follows the pipeline you know: input (camera and place) → detection (what the model finds) → trigger (the rule that turns detections into events, lesson 31) → output (a dashboard, an alert, a highlight, hardware, a report).
Why a brief first
A brief is a short document that says what you will build, for whom, and how you will know it works. Teams that skip it spend week 4 arguing about what they were building. Teams that write it can say "no" to ideas that do not fit.
A good problem is small and real
| Weak | Better |
|---|---|
| "Use AI to improve football" | "Our school basketball club loses count of shots in practice. Count them automatically." |
| "Track everything on the pitch" | "Detect the ball and the goal zone, and announce goals." |
| "Make a smart stadium" | "Light a lamp when the ball crosses the finish line." |
Start with a person who has a problem, not with a technology you want to use.
Measurable success
A criterion is measurable if someone else can run a test and say yes or no.
| Not measurable | Measurable |
|---|---|
| "It should be accurate" | "Shot recall at least 0.9" |
| "It shouldn't cry wolf" | "At most 1 false count per 10-minute practice" |
| "It should be fast" | "Beep within 1.5 seconds of the shot" |
Always include both kinds of error: false alarms and misses (lesson 36, lesson 40). Choose the numbers before you measure, so that you can't change them to look good.
Scope: do less, finish it
You have about four working sessions. A product that works at a small scope beats an unfinished big one every time. Count your ingredients. Each extra class to detect, extra trigger, piece of hardware, tracking or camera mapping costs days. Cut until it fits, and write what you cut in the "not for" and "later" lists.
Risks and plans
A risk is something that could go wrong. For each, write what you will do about it. "The ball is the same colour as the floor" with the plan "test on the real floor in lesson 48 and collect extra images there" is a good risk. "Something might break" is not.
Use the whole toolbox
Your final product must show these, in whatever form fits:
| Piece | From |
|---|---|
| Data you collected and labelled, with consent | lesson 11 to lesson 14, lesson 28, lesson 48 |
| A trained detector, compared with a baseline | lesson 18 to lesson 24, lesson 49 |
| A trigger and an output | lesson 31 to lesson 36 |
| A stress test and a failure policy | lesson 36 |
| A model card with per-group results | lesson 43 |
| A debrief of a real run | lesson 40, lesson 50 |
Claude's role
Claude can help in three ways: as a reviewer of your plan (Cell 5), as a writer inside the product (commentary or summaries, lesson 37), and as a second opinion on pictures (lesson 38). It should never be the author of your brief, your code or your model card. The work is yours; you have to be able to explain every line.
Ethics goes in the plan
Every brief includes: who is filmed and how consent was given (lesson 14), what the product is not for, and what happens to the data afterwards. A product that cannot say this is not ready to build.
What you'll need
- Your team
- The product brief template below
- Optional: Claude, to ask hard questions about your brief (lesson 37 set-up)
Build it
Cell 1: a brief is data, and data can be checked
We write the brief as a JSON file, then a program checks it. This is how real teams stop "make it good" from sneaking into a plan.
"""A product brief as data, with a linter that checks it is specific, measurable and small enough."""
import json
import re
REQUIRED = ["name", "team", "problem", "user", "input", "detects", "trigger", "output", "data_plan",
"success", "risks", "not_for", "milestones", "claude_use"]
# Rough cost of each ingredient, in "sprint days" (our own estimate: tune it with your teacher)
COSTS = {"extra_class": 2, "extra_trigger": 2, "hardware": 3, "claude": 1, "dashboard": 2, "pose": 3, "tracking": 2, "homography": 2}
BUDGET = 12
def measurable(criterion):
"""A success criterion needs a metric, a comparison and a number: e.g. {'metric': 'goal recall', 'op': '>=', 'target': 0.9, 'unit': 'fraction'}."""
return (isinstance(criterion, dict) and criterion.get("metric") and criterion.get("op") in (">=", "<=", ">", "<", "==")
and isinstance(criterion.get("target"), (int, float)) and not isinstance(criterion.get("target"), bool)
and criterion.get("unit"))
def lint(brief):
problems = []
for k in REQUIRED:
v = brief.get(k)
if v in (None, "", [], {}):
problems.append(f"missing: {k}")
elif isinstance(v, str) and re.search(r"\b(todo|tbd|fill in|lorem|etc\.?)\b", v.lower()):
problems.append(f"vague or placeholder text in: {k}")
if isinstance(brief.get("problem"), str) and len(brief["problem"].split()) > 40:
problems.append("problem: more than 40 words. Say it in one or two sentences.")
crit = brief.get("success") or []
if crit:
bad = [c.get("metric", "?") if isinstance(c, dict) else str(c) for c in crit if not measurable(c)]
for b in bad:
problems.append(f"success criterion is not measurable (needs metric, operator, number and unit): {b}")
if len(crit) < 3:
problems.append("give at least 3 success criteria")
metrics = " ".join(str(c.get("metric", "")).lower() for c in crit if isinstance(c, dict))
if "false" not in metrics:
problems.append("no criterion about false alarms: how many are acceptable?")
if not any(w in metrics for w in ("recall", "miss", "caught", "detect")):
problems.append("no criterion about misses: how many real events must it catch?")
risks = brief.get("risks") or []
if risks and len(risks) < 3:
problems.append("name at least 3 risks")
for r in risks:
if not (isinstance(r, dict) and r.get("risk") and r.get("plan")):
problems.append(f"each risk needs a plan: {r}")
break
dp = str(brief.get("data_plan", "")).lower()
if dp and "consent" not in dp:
problems.append("data_plan does not mention consent (lesson 14)")
ms = brief.get("milestones") or {}
if ms and not all(k in ms for k in ("lesson 48", "lesson 49", "lesson 50", "lesson 51")):
problems.append("milestones must cover lesson 48, lesson 49, lesson 50 and lesson 51")
return problems
def scope_points(brief):
"""Add up the ingredients the team chose. Over the budget means: cut something."""
parts = brief.get("components", [])
unknown = [p for p in parts if p not in COSTS]
total = sum(COSTS.get(p, 0) for p in parts)
return {"points": total, "budget": BUDGET, "over": total > BUDGET, "unknown": unknown}
def to_markdown(b):
rows = "\n".join(f"| {c['metric']} | {c['op']} {c['target']} {c['unit']} |" for c in b["success"])
risks = "\n".join(f"- **{r['risk']}** → {r['plan']}" for r in b["risks"])
ms = "\n".join(f"- **{k}:** {v}" for k, v in b["milestones"].items())
return f"""# {b['name']}
Team: {b['team']}
## The problem
{b['problem']}
## Who uses it
{b['user']}
## How it works
- **Input:** {b['input']}
- **Detects:** {b['detects']}
- **Trigger:** {b['trigger']}
- **Output:** {b['output']}
- **Claude:** {b['claude_use']}
## Data plan
{b['data_plan']}
## How we will know it works
| Measure | Target |
|---|---|
{rows}
## Risks
{risks}
## Not for
{b['not_for']}
## Milestones
{ms}
"""Notice what is checked: every field is present; no placeholder words ("TBD", "etc."); the problem is short; every success criterion has a metric, an operator, a number and a unit; there is at least one criterion about false alarms and one about misses (lesson 36, lesson 40); there are at least three risks and each has a plan; the data plan mentions consent (lesson 14); and the milestones cover lesson 48 to lesson 51.
Cell 2: an example brief that passes
Read it carefully. It is a basketball shot counter: it reuses the goal trigger from lesson 31 on a different sport.
{
"name": "Net Counter: basketball shot counter",
"team": "Team Hoop",
"problem": "Our school basketball club counts shots by hand during practice, and people lose count. We want a camera that counts every ball that goes through the hoop.",
"user": "The club coach, on a laptop at the side of the court, and the players who see the count on a screen.",
"input": "One phone camera on a tripod behind the hoop, 1080p, indoor court under normal lights.",
"detects": "The basketball (one class: ball) and the hoop zone.",
"trigger": "Ball seen above the hoop zone, then inside it for 3 frames, then below it, within 2 seconds: count one shot.",
"output": "A live count on a web dashboard, a short beep, and a CSV of shot times.",
"data_plan": "About 200 images from our own practices, labelled in Roboflow. Only players with signed parent consent (lesson 14) are filmed; others practise on the far court.",
"success": [
{"metric": "shot recall (real shots counted)", "op": ">=", "target": 0.9, "unit": "fraction"},
{"metric": "false counts per 10-minute practice", "op": "<=", "target": 1, "unit": "count"},
{"metric": "delay between shot and beep", "op": "<=", "target": 1.5, "unit": "seconds"}
],
"risks": [
{"risk": "Ball hidden by the net or a player", "plan": "Treat a missing ball as neutral (lesson 31) and test with a hidden-ball clip"},
{"risk": "Orange ball on an orange floor", "plan": "Test on the real floor in lesson 48; collect extra images there"},
{"risk": "Not enough shots in the data", "plan": "Ask for 20 extra shots in the data sprint"}
],
"not_for": "Not for scoring matches or ranking players. Not used on children without consent.",
"milestones": {"lesson 48": "200 labelled images, split and augmented", "lesson 49": "Model trained, beats the lesson 24 baseline, stress test run",
"lesson 50": "Run at the gym, 10 minutes, log and video", "lesson 51": "Live demo and Q&A"},
"claude_use": "Claude writes the end-of-practice summary from the CSV (lesson 37 style) and critiques our brief.",
"components": ["extra_class", "extra_trigger", "dashboard", "claude"]
}Cell 3: write your own, then lint it
import json
from brief import lint, scope_points, to_markdown
brief = json.load(open("my_brief.json"))
for problem in lint(brief):
print("FIX:", problem)
print(scope_points(brief))
open("PRODUCT_BRIEF.md", "w").write(to_markdown(brief))Keep fixing until lint prints nothing and the scope is within budget. The scope points are our own rough estimate of sprint days (an extra class 2, an extra trigger 2, hardware 3, Claude 1, a dashboard 2, pose 3, tracking 2, homography 2; a budget of 12). They are a conversation starter, not a law: argue about them with your teacher. If you go over, cut something.
Cell 4: tests
import copy, json
from brief import lint, measurable, scope_points, to_markdown, COSTS
from critique import build_message, critique, parse_review, SYSTEM
from fake_client import FakeClient
bad = 0
def check(name, ok, extra=""):
global bad; bad += not ok; print("PASS" if ok else "FAIL", name, extra)
good = json.load(open("example_brief.json"))
check("the example brief has no problems", lint(good) == [], lint(good))
def broken(**kw):
b = copy.deepcopy(good); b.update(kw); return b
check("missing field caught", any("missing: trigger" in p for p in lint(broken(trigger=""))))
check("placeholder caught", any("vague" in p for p in lint(broken(output="a dashboard, alerts, etc."))))
check("long problem caught", any("40 words" in p for p in lint(broken(problem="word " * 50))))
check("'it should be accurate' is not measurable", any("not measurable" in p for p in lint(broken(success=good["success"][:2] + ["it should be accurate"]))))
check("target without a unit is not measurable", not measurable({"metric": "recall", "op": ">=", "target": 0.9}))
check("target as text is not measurable", not measurable({"metric": "recall", "op": ">=", "target": "high", "unit": "fraction"}))
check("a true boolean is not a number", not measurable({"metric": "x", "op": ">=", "target": True, "unit": "u"}))
check("fewer than 3 criteria caught", any("at least 3" in p for p in lint(broken(success=good["success"][:2]))))
no_fa = [c for c in good["success"] if "false" not in c["metric"]] + [{"metric": "extra recall check", "op": ">=", "target": 0.8, "unit": "fraction"}]
check("no false-alarm criterion caught", any("false alarms" in p for p in lint(broken(success=no_fa))))
no_miss = [c for c in good["success"] if "recall" not in c["metric"]] + [{"metric": "a second false alarm check", "op": "<=", "target": 2, "unit": "count"}]
check("no misses criterion caught", any("misses" in p for p in lint(broken(success=no_miss))))
check("two risks is too few", any("3 risks" in p for p in lint(broken(risks=good["risks"][:2]))))
check("a risk without a plan is caught", any("needs a plan" in p for p in lint(broken(risks=good["risks"][:2] + [{"risk": "bad weather"}]))))
check("no mention of consent caught", any("consent" in p for p in lint(broken(data_plan="We will film practices."))))
check("missing milestone caught", any("milestones" in p for p in lint(broken(milestones={"lesson 48": "x", "lesson 49": "y"}))))
check("scope: 7 points is within budget", not scope_points(good)["over"])
big = broken(components=["extra_class", "extra_trigger", "hardware", "claude", "dashboard", "pose", "tracking"])
check("scope: everything at once is over budget", scope_points(big)["over"], scope_points(big))
check("scope: unknown ingredient reported", scope_points(broken(components=["teleporter"]))["unknown"] == ["teleporter"])
md = to_markdown(good)
check("markdown has the measures table", "| shot recall (real shots counted) | >= 0.9 fraction |" in md and "## Milestones" in md)
# Claude critique (pretend client)
reply = json.dumps([{"field": "trigger", "problem": "Two seconds may be too long for a fast layup.", "question": "How did you choose 2 seconds?"},
{"field": "risks", "problem": "No risk about the camera being knocked.", "question": "What happens if the tripod moves?"}])
fc = FakeClient(reply)
items = critique(good, fc)
check("critique returns the items", len(items) == 2 and items[0]["field"] == "trigger")
check("system prompt forbids rewriting", "Do NOT rewrite" in fc.calls[0]["system"])
check("the brief is sent to Claude", '"name": "Net Counter' in fc.calls[0]["messages"][0]["content"])
for name, text in [("not json", "Looks great!"), ("too many items", json.dumps([{"field": "name", "problem": "p", "question": "q"}] * 6)),
("unknown field", json.dumps([{"field": "colour", "problem": "p", "question": "q"}])),
("missing question", json.dumps([{"field": "name", "problem": "p"}])),
("a pasteable essay", json.dumps([{"field": "name", "problem": "p" * 400, "question": "q"}]))]:
try:
parse_review(text, set(good)); check(f"rejects {name}", False)
except (ValueError, json.JSONDecodeError):
check(f"rejects {name}", True)
print("failures:", bad)All 27 should print PASS. They show every kind of weak brief being caught: a vague success line ("it should be accurate"), a target with no unit, no false-alarm criterion, two risks, no consent, a missing milestone, and a scope that tries to do everything.
Cell 5: ask Claude to critique, not to write
Claude can read your brief the way a tough reviewer would. The rule is it points, you write: it may name a weakness and ask a question, but it may not rewrite your brief.
"""Ask Claude to CRITIQUE the brief (find weak spots). It must not rewrite it: the team writes the brief."""
import json
import re
MODEL = "claude-sonnet-5-5"
SYSTEM = """You are a tough but kind reviewer of student computer-vision project briefs.
Rules:
- Find at most 5 weaknesses: vague words, things that cannot be measured, missing risks, too much for 4 sessions, privacy or consent gaps.
- Do NOT rewrite the brief and do NOT write sentences the students could paste in. Point at the problem and ask a question or give a hint.
- Answer ONLY with JSON: a list of objects like {"field": "<name of the field>", "problem": "<one sentence>", "question": "<one question for the team>"}."""
def build_message(brief):
return "Here is the brief as JSON:\n" + json.dumps(brief, indent=2) + "\n\nReview it."
def parse_review(text, allowed_fields):
m = re.search(r"\[.*\]", text, re.S)
if not m:
raise ValueError("no JSON list in the answer")
items = json.loads(m.group(0))
if not isinstance(items, list) or len(items) > 5:
raise ValueError("expected a list of at most 5 items")
for it in items:
if not isinstance(it, dict) or not all(isinstance(it.get(k), str) and it.get(k) for k in ("field", "problem", "question")):
raise ValueError(f"bad item: {it}")
if it["field"] not in allowed_fields:
raise ValueError(f"unknown field: {it['field']}")
if len(it["problem"]) > 300 or len(it["question"]) > 300:
raise ValueError("an item is too long to be a hint")
return items
def critique(brief, client, model=MODEL):
msg = client.messages.create(model=model, max_tokens=700, system=SYSTEM,
messages=[{"role": "user", "content": build_message(brief)}])
text = "".join(b.text for b in msg.content if b.type == "text")
return parse_review(text, allowed_fields=set(brief))Use it like this (needs the API key from lesson 37):
from critique import critique
from commentary import get_client # from lesson 37
for item in critique(brief, get_client()):
print(item["field"], "->", item["problem"])
print(" question:", item["question"])The system message forbids rewriting and the answer is checked: at most five items, each pointing at a real field, each short. A reply that is not a short hint is rejected. Cell 5's live call was not run in preparing this lesson; the checks around it were. Argue with Claude's questions. It will sometimes be wrong.
Cell 6: the markdown version
to_markdown turns the brief into the document you hand in. Here is the example's:
# Net Counter: basketball shot counter
Team: Team Hoop
## The problem
Our school basketball club counts shots by hand during practice, and people lose count. We want a camera that counts every ball that goes through the hoop.
## Who uses it
The club coach, on a laptop at the side of the court, and the players who see the count on a screen.
## How it works
- **Input:** One phone camera on a tripod behind the hoop, 1080p, indoor court under normal lights.
- **Detects:** The basketball (one class: ball) and the hoop zone.
- **Trigger:** Ball seen above the hoop zone, then inside it for 3 frames, then below it, within 2 seconds: count one shot.
- **Output:** A live count on a web dashboard, a short beep, and a CSV of shot times.
- **Claude:** Claude writes the end-of-practice summary from the CSV (lesson 37 style) and critiques our brief.
## Data plan
About 200 images from our own practices, labelled in Roboflow. Only players with signed parent consent (lesson 14) are filmed; others practise on the far court.
## How we will know it works
| Measure | Target |
|---|---|
| shot recall (real shots counted) | >= 0.9 fraction |
| false counts per 10-minute practice | <= 1 count |
| delay between shot and beep | <= 1.5 seconds |
## Risks
- **Ball hidden by the net or a player** → Treat a missing ball as neutral (lesson 31) and test with a hidden-ball clip
- **Orange ball on an orange floor** → Test on the real floor in lesson 48; collect extra images there
- **Not enough shots in the data** → Ask for 20 extra shots in the data sprint
## Not for
Not for scoring matches or ranking players. Not used on children without consent.
## Milestones
- **lesson 48:** 200 labelled images, split and augmented
- **lesson 49:** Model trained, beats the lesson 24 baseline, stress test run
- **lesson 50:** Run at the gym, 10 minutes, log and video
- **lesson 51:** Live demo and Q&AQuestions: which success criterion in the example would you make stricter? What is the riskiest ingredient? What would you cut first if you ran out of time? Is the "not for" line honest?
If it goes wrong
| Error | Cause | Fix |
|---|---|---|
| Problem is 'improve sport with AI' | Too vague | Name a person and what they do by hand today |
| Lint says 'not measurable' | Criterion has no number or unit | Write metric, operator, number, unit |
| No false-alarm criterion | Teams think only of detection | Ask: how many wrong alerts per practice are acceptable? |
| Scope is over budget | Too many ingredients | Remove the least important ingredient and list it under 'later' |
| Plan needs a venue nobody can book | No access | Choose a school-ground version, or change the idea |
| Team pastes Claude's text | Misuse of the critic | Ask them to explain each line aloud; rewrite in their words |
| All teams copy the example | Easy path | Require a different sport, trigger or output |
| Brief hides consent | Forgot lesson 14 | Name who is filmed and how consent was given |
Try it
- Add a second brief option and compare them with the scope points
- Write the 'later' list: what you will do if you finish early
- Define a test plan for each success criterion
Check what you learnt
What makes a success criterion measurable?
Why set limits for both false alarms and misses?
Your scope is too big. What do you do?
What may Claude do with your brief?
What goes in the 'not for' line?
Conclusion
A capstone starts with a brief: a small, real problem for real users, measurable success criteria (including misses and false alarms), a scope you can actually finish, risks with plans, and a clear "not for" line. Claude can challenge the brief; your team writes it.
What next?
Next: Data sprint: capture & annotate. You'll run a data sprint for your product.

