LIVE Commerce Analytics / Builder session 3 of 8
Learn LIVE Commerce Analytics with Phoebe · Builder session 3 of 8

See it: what the camera shows

Session 2 left you 7,200 JPEGs, one per second of stream, and a scene-cut count per minute. Tonight the frames become columns. A hand landmarker says where the host's fingers are, a pose landmarker says whether she is in frame and how close, an open-vocabulary detector prompted with the words "serum bottle" says where the product is, and an OCR model reads the price card. Each of those is a per-frame answer; the minute table wants one number per minute, so the session is as much about aggregation as detection. And before any of it runs, the licence page: the most accurate detector on the shelf cannot be shipped inside a closed-source MCN dashboard without a cheque, and that decision is made tonight, not at launch.

🟡 Core Data scientists Anyone choosing a detector they can legally ship 45 min
0-3 · Welcome 3-20 · Detectors and the licence question 20-40 · Build-along: five columns from frames 40-45 · Q&A
Part 0

Where we are in the builder track

Session 1 fixed the grain and the table. Session 2 got the recording onto one clock and produced the 1 fps frames, the audio, and the first content column, scene_cuts. Tonight fills the visual columns from those frames. Session 4 does the same for the audio. From session 5 on, nothing reads a frame or a word again; it all reads the minute table. So the columns you build tonight are the only thing the rest of the track will ever know about what the camera showed, which is why the aggregation rules matter as much as the model.

Live = presented in the 45 minutes Self-study = the full depth, after class 🔍 Every diagram zooms

★ What you walk out with tonight. Five minute-level columns computed from frames: product_in_hand, close_up_ratio, host_on_camera, scene_cuts, price_overlay. A working pipeline that produces them from your own recording with MediaPipe 1.0.1, Grounding DINO 1.0 and supervision 0.30.5, all Apache-2.0 or MIT. A validation habit that reports precision and recall per column from 200 hand-labelled frames. And a licence decision you can defend to a lawyer: which detector routes can run inside a closed-source internal tool without buying anything, and which cannot.

LiveWhat these columns are worth before you build them3 min▶

A detector budget is easier to justify with the number it protects. In the Lumen Glow data, comparing minutes within the same stream so the room size and the host cancel out, the visual columns move the click-through rate on the pinned card: a minute with the product in hand runs +1.38 points of CTR, a minute of close-up runs +1.01, and a minute with the price on screen runs +0.97. Those are among the largest within-stream CTR effects in the table; only the CTA and demo segments beat them.

The same three columns do almost nothing to the click-to-order rate: product in hand +0.04 points, close-up +0.46, price overlay -0.13, none of them distinguishable from zero. The camera earns the click. What the host says earns the order. Session 4 measures that half.

The bench in session 8 turns the click effect into money. Cueing the host to have the product in hand on every CTA is worth +9.7 percent of GMV on a Friday night, as a mean over 40 seeded replays. You cannot cue what you cannot measure, and you cannot measure it without tonight's column.

Self-studyThe vocabulary tonight uses4 min▶
  • Closed-vocabulary detection - a detector trained on a fixed list of classes and blind to anything else. YOLO26's COCO weights know "person" and "bottle" and have never heard of Dew Serum.
  • Open-vocabulary detection - a detector that takes a text prompt at run time and finds whatever the prompt names. "serum bottle", "pump dispenser", the SKU name on the label. No training, one string.
  • Landmark - a named keypoint on a body part. MediaPipe's HandLandmarker returns 21 per hand, PoseLandmarker returns 33 per body, each with normalised x, y and a depth estimate.
  • Zone - a polygon on the frame that reports which detections fall inside it. supervision's PolygonZone is the standard implementation.
  • Tracker id - a stable integer attached to one object across consecutive frames, so "the same bottle" can be followed. ByteTrack is the usual algorithm.
  • OCR - optical character recognition, reading text off pixels. Florence-2's OCR_WITH_REGION task returns each string with its box.
  • Licence: AGPL-3.0 - copyleft that reaches over the network. Software that uses an AGPL component must offer its own source to users, including users who only reach it through a server. Apache-2.0 and MIT are permissive: use, modify, ship closed, keep the notice.
Part 1 · covers the four detector routes, their licences, and how each column is computed

Four ways to see the serum

9 min live

There is no single "vision model" for this job. There are four routes with different strengths and, more importantly, different licences, and a working pipeline uses three of them. Read the grid by licence first and by accuracy second, because the accuracy of a detector you cannot ship is zero.

Four detector routes, read by licence first versions read 2026-09-24; each tile says what it gives a minute column, and what it costs to ship Closed vocabulary · YOLO26 ultralytics 8.4.161, fixed class list (person, bottle, ...) gives: person box, generic bottle box, built-in tracking cannot name Dew Serum without training a custom model AGPL-3.0, or paid Enterprise: commercial products, closed-source internal tools, SaaS on a YOLO backend Landmarks · MediaPipe 1.0.1 HandLandmarker: 21 points per hand, normalised x, y, z PoseLandmarker: 33 points, visibility and presence each gives: host_on_camera, close_up_ratio, where the hand is licence: Apache-2.0 runs on CPU at 1 fps without complaint; .task model files Open vocabulary · prompt with the SKU Grounding DINO 1.0 (Apache-2.0): accuracy ceiling OWLv2 (Apache-2.0): simplest baseline Florence-2 (MIT): boxes plus OCR_WITH_REGION in one model gives: a box for "serum bottle", the price card text YOLOE is open-vocab too but ships in ultralytics: AGPL Utilities · zones and ids supervision 0.30.5 (MIT): PolygonZone, LineZone trackers 2.6.0 (Apache-2.0): ByteTrackTracker sv.ByteTrack deprecated in 0.28, removed in 0.31 gives: hand in the product zone, stable bottle id glue, not a model; works on any detector's boxes Tonight's pipeline uses three of the four: landmarks + open vocabulary + utilities. Nothing AGPL. An MCN's internal dashboard is a closed-source internal tool. The red tile needs an Enterprise licence there.
🔍 Click to zoom - four detector routes with what each gives a column and what it costs to ship.
LiveThe licence question first3 min▶

Ultralytics' own licence page draws the line. AGPL-3.0 fits research, personal and fully open-source use. The Enterprise licence is required for commercial products, closed-source internal tools, and SaaS or APIs with a YOLO backend, and its pricing is quote-based. YOLOE, the open-vocabulary model, ships inside the same package, so the same terms apply to it.

Now place the thing you are building. An MCN's stream-quality dashboard, run on the MCN's own servers for the MCN's own coaches, is a closed-source internal tool. That is on the Enterprise side of the line by the licensor's own wording. So the choice is one of three, and it is made before a single frame is processed:

  • Buy Enterprise. Get the quote, budget it, ship YOLO26 and YOLOE with a clear conscience.
  • Open-source the pipeline. AGPL is satisfied if the code that uses YOLO is offered to its users. For an internal tool that means publishing it, which some MCNs will not do and most brands will not allow.
  • Use Apache-2.0 and MIT models. MediaPipe for landmarks, Grounding DINO or OWLv2 for the SKU, Florence-2 for OCR, supervision and trackers for the glue. Tonight's build-along takes this route, and the accuracy it gives up is smaller than the licence it keeps.

Read the licence page before the benchmark table. Every detector comparison online ranks on mAP. None of them ranks on whether your legal team will sign. A model with a lower mAP that you can ship beats a model with a higher mAP that you cannot, by exactly the size of the effect you are trying to measure.

LiveHow each column is computed3 min▶

Each column is one rule applied per frame, then one aggregation per minute. The rules are deliberately simple; the sophistication is in the detectors underneath, not in the arithmetic on top.

ColumnPer-frame ruleFromPer-minute
host_on_cameraA person is present: any pose landmarks returned, or a person boxPoseLandmarkerShare of frames
close_up_ratioPerson box area over frame area. When the hip landmarks are missing, the shot is shoulders-and-up: use the box from the visible landmarksPoseLandmarkerMean
product_in_handA hand landmark lies inside, or within a margin of, a box the open-vocab detector returned for "serum bottle" or the SKU nameHandLandmarker + Grounding DINO + PolygonZoneShare of frames
price_overlayFlorence-2 OCR_WITH_REGION returns a string containing a currency symbol and digitsFlorence-2Share of frames
scene_cutsA scene boundary, from the full-rate videoPySceneDetect, session 2Count

The one that needs care is close-up. A host who leans in until only her face and the bottle fill the frame has no hips in shot, so a naive "person box" from pose landmarks shrinks to the visible points and the ratio drops when it should rise. Treat missing hips as a signal, not as missing data: shoulders-and-up with no hips is a close-up by construction, and the ratio is computed from the shoulder-to-crown box scaled to the frame.

LiveWhat went wrong the first time2 min▶
Real world

Lumen Glow's first detector was a stock closed-vocabulary model looking for "bottle". It found the water bottle on the host's desk in nearly every frame, so product_in_hand read as true for most of every stream and the column had no variance to correlate with anything. Prompting an open-vocabulary detector with "serum bottle with a pump" and requiring a hand landmark inside the box fixed both problems in one afternoon. The licence review of the first model took longer than the rebuild.

Self-studyThe two API signatures you will type most4 min▶

Both quoted from the fetched documentation. The Grounding DINO call is from the transformers v5.17.0 model docs; note the kwarg name, because the model card on the Hub still shows the older box_threshold while the current processor takes threshold. Signature defaults are 0.25 for both thresholds; the docs' example raises them to 0.4 and 0.3.

★ Grounding DINO, transformers v5.17.0 docs processor = AutoProcessor.from_pretrained("IDEA-Research/grounding-dino-tiny") model = AutoModelForZeroShotObjectDetection.from_pretrained("IDEA-Research/grounding-dino-tiny", device_map="auto") text_labels = [["a cat", "a remote control"]] inputs = processor(images=image, text=text_labels, return_tensors="pt").to(model.device) results = processor.post_process_grounded_object_detection( outputs, inputs.input_ids, threshold=0.4, text_threshold=0.3, target_sizes=[image.size[::-1]]) # signature defaults: threshold=0.25, text_threshold=0.25; returns scores, boxes [x0,y0,x1,y1], labels, text_labels # NOTE: the model card still shows the older kwarg name box_threshold; the v5.17 processor uses `threshold`.

MediaPipe's task options all share one shape: a base_options pointing at a model file, a running mode, and a handful of confidence thresholds. The hand landmarker's, as documented:

★ MediaPipe 1.0.1 HandLandmarkerOptions HandLandmarkerOptions(num_hands=1, min_hand_detection_confidence=0.5, min_hand_presence_confidence=0.5, min_tracking_confidence=0.5) # output per hand: handedness, hand_landmarks (21 points, normalised x,y + z), hand_world_landmarks (metres)

For a host who sells with both hands, set num_hands=2. The landmarks are normalised to the frame, so a box from the detector in pixels has to be divided by the frame width and height before the two are compared, or the landmarks multiplied up. Pick one convention and assert it in the code.

Part 2 · covers aggregation from frames to minutes, validation without ground truth, and the AI-host streams

From frames to a minute column

8 min live

A detector answers per frame. The table wants per minute. Sixty frames go in, one number comes out, and the rule that turns sixty answers into one decides whether the column means anything. Booleans become shares, ratios become means, events become counts, and a minute the detector barely saw becomes null rather than zero.

One minute of frames at 1 fps, and the four rules that turn them into one row filled ticks: product in hand on that frame · hollow: not · the detector answers 60 times, the table gets one answer second 1 second 60 frames/000001.jpg ... 000060.jpg Booleans: share frames true / usable frames in hand, on camera, price on screen Ratios: mean mean over usable frames close_up_ratio (box area / frame area) Events: count boundaries in the minute scene_cuts, from the full-rate video (session 2) Under 30 usable frames in the minute: the column is null, not zero. A zero would claim never in hand about a minute the detector barely saw. The hand-near-product rule one frame serum zone hand landmark box + margin = PolygonZone landmark inside = in hand Same bottle across frames: trackers.ByteTrackTracker sv.ByteTrack: gone since 0.31 One row out stream_id, t in_hand · close_up · on_camera · cuts · price_overlay · usable_frames joins on nothing else Keep usable_frames as a column. It is the denominator of every share, and the audit trail when a minute looks odd.
🔍 Click to zoom - sixty per-frame answers become one row, with the zone rule for "in hand" and the null rule for thin minutes.
LiveThe aggregation rules3 min▶
  • Booleans become shares. A detector says true or false per frame. Sixty of those in a minute are a proportion, and the proportion is what carries information: a minute where the product was in hand for 55 frames is different from one where it was for 12. Round it to 0 or 1 only when a downstream method needs a flag, and keep the share beside it.
  • Ratios become means. Close-up ratio is already a number per frame; average it over the usable frames. A median is defensible too, but the mean is what the course table carries, so use the mean to reconcile.
  • Events become counts. Scene cuts are boundaries; a minute has an integer number of them. Session 2 built this one.
  • Thin minutes become null. If fewer than 30 of the minute's 60 frames were usable, whether because the detector crashed, the frames were black, or you sampled down for speed, write null. A share of 3 out of 4 frames is not a share, it is a coin flip with a decimal point. Session 5 skips nulls; it cannot skip a zero it does not know is fake.

The course table stores product_in_hand and price_overlay as 0 or 1 per minute, which is the share rounded at one half. Your own pipeline should keep both: the share for modelling, the flag for reconciling against the table and for the cue rules in session 8.

LiveValidating a detector without ground truth3 min▶

Nobody hands you labelled frames of your host. You make them, and it is cheaper than it sounds. Sample 200 frames at random across streams, hosts and hours; sit with a coffee and mark each one by hand for each boolean column and a rough close-up bucket. Then run the detector on the same 200 and report precision and recall per column, not one accuracy number: a detector that never says "in hand" has 90 percent accuracy on a stream where the product is rarely held, and is useless.

Keep the 200 labels in the repository. They are the regression test for every future change of model, threshold or prompt, and they are the answer when someone asks how much to trust a column. A detector whose recall on price cards is low will understate the price-overlay effect, and knowing that before session 6 is worth more than a better model after it.

Label the disagreements twice. Frames where two of you disagree on "in hand" are the frames the rule is unclear on, not the frames the detector is wrong on. Write the rule down (touching counts, hovering does not, the margin is one hand-width) and re-label. The detector's zone margin is that rule in pixels.

LiveWhat the AI-host streams look like on camera2 min▶

Two of the six Lumen Glow hosts are AI avatars, and the camera columns spot them before anyone reads the host name. Their scene cuts sit at 1 to 3 per minute against 2 to 7 for the human hosts, because an avatar has no operator switching to the product camera. Their speech rate holds at a steady 158 words a minute with almost no variance, where a human host's rate moves with the segment and collapses in dead air. The columns are honest about it: the avatars are consistent, and consistency is not what sells.

On TikTok Shop this is a compliance question before it is an analytics question. The Requirements for High-Quality Videos and LIVEs, effective 2026-09-17 in the US, say "Do not use non-real-time verbal interaction such as AI-generated voices, audio recordings, or radio" and "Do not use animated figures or content that covers more than 50% of the screen." A fully AI-voiced avatar host is not permitted on a promotional LIVE there; the AI streams in the data show what the trade would be on a platform that allows it. Leader session 4 covers the rule; tonight's close_up_ratio is, incidentally, the measurement of "more than 50% of the screen".

Self-studyWhat a cloud VLM labeller costs against a local detector3 min▶

There is a fourth way to get columns from frames: send the video to a vision-language model and ask it. Gemini 3.8 Flash reads video at about 100 tokens per second at low resolution and is priced at $0.75 per 1M input tokens under promotional pricing that runs through 2026-12-31, which works out to about $0.006 per minute of video. Session 4 uses it as a segment labeller, where a model that understands "she is now doing the price pitch" earns its keep.

For tonight's columns it is the wrong tool for three reasons. A VLM returns a sentence, not a box, so "in hand" becomes a judgement you cannot audit against a zone margin. Its answer per frame is not stable across runs, so the 200-frame validation drifts. And the price is per minute for as long as you keep running it, where a local Apache-2.0 landmarker costs the electricity. Use the VLM for what needs language, the detector for what needs geometry, and keep the receipts for both.

Build-along

Five columns from frames

★ 20 min · everyone builds

Twenty minutes, six steps, nothing AGPL. You run landmarkers and an open-vocabulary detector on the 1 fps frames from session 2, apply the zone rule, aggregate to the minute, and reconcile against the columns the course table already carries. On the course data the columns are precomputed, so the reconcile step passes on the first run; on your own recording the same code overwrites them with what your camera actually showed.

Install the permissive stack. Pin the versions; the trackers move between packages and the kwarg names change between releases, and a pin is the only thing that makes next month's run reproduce tonight's.

★ Versions read 2026-09-24 pip install "mediapipe==1.0.1" "supervision==0.30.5" "trackers==2.6.0" \ "transformers==5.17.0" "torch" "pillow" "pandas" "numpy" # Grounding DINO 1.0 weights: IDEA-Research/grounding-dino-tiny (Apache-2.0) # Florence-2 for OCR: microsoft/Florence-2-large (MIT), needs trust_remote_code=True # MediaPipe .task files: hand_landmarker.task, pose_landmarker.task from the MediaPipe model pages

Landmarks on every frame. Both landmarkers in VIDEO running mode, fed the 1 fps frames in order with a timestamp in milliseconds, so the tracker inside each task can use the previous frame. Frame n is second n, so the timestamp is n times 1000.

★ MediaPipe 1.0.1, hand and pose, VIDEO mode import mediapipe as mp from mediapipe.tasks.python import vision from pathlib import Path import pandas as pd RM = vision.RunningMode.VIDEO hand_opts = vision.HandLandmarkerOptions( base_options=mp.tasks.BaseOptions(model_asset_path="models/hand_landmarker.task"), running_mode=RM, num_hands=2, min_hand_detection_confidence=0.5, min_hand_presence_confidence=0.5, min_tracking_confidence=0.5) pose_opts = vision.PoseLandmarkerOptions( base_options=mp.tasks.BaseOptions(model_asset_path="models/pose_landmarker.task"), running_mode=RM, num_poses=1, output_segmentation_masks=False) rows = [] frames = sorted(Path("frames").glob("*.jpg")) # 000001.jpg is second 1 with vision.HandLandmarker.create_from_options(hand_opts) as hands, \ vision.PoseLandmarker.create_from_options(pose_opts) as pose: for f in frames: n = int(f.stem) img = mp.Image.create_from_file(str(f)) h = hands.detect_for_video(img, n * 1000) p = pose.detect_for_video(img, n * 1000) lm = p.pose_landmarks[0] if p.pose_landmarks else None on_cam = lm is not None # close-up: box of visible landmarks over the frame; no hips visible = shoulders-and-up shot if on_cam: vis = [q for q in lm if q.visibility > 0.5] xs, ys = [q.x for q in vis], [q.y for q in vis] hips_visible = lm[23].visibility > 0.5 or lm[24].visibility > 0.5 box = (max(xs) - min(xs)) * (max(ys) - min(ys)) # normalised, so already / frame area close = box if hips_visible else max(box, 0.5) # shoulders-and-up is a close-up by construction else: close = None hand_pts = [(q.x, q.y) for hl in h.hand_landmarks for q in hl] rows.append({"sec": n, "host_on_camera": on_cam, "close_up_ratio": close, "hand_pts": hand_pts}) fr = pd.DataFrame(rows)

Find the serum by name. Grounding DINO 1.0 with the SKU as the prompt. The processor's post-processing takes threshold in transformers 5.17; the model card still says box_threshold, and passing that name errors. Take the highest-scoring box per frame; the host holds one bottle at a time.

★ Grounding DINO 1.0, one prompt, one box per frame import torch from PIL import Image from transformers import AutoProcessor, AutoModelForZeroShotObjectDetection processor = AutoProcessor.from_pretrained("IDEA-Research/grounding-dino-tiny") model = AutoModelForZeroShotObjectDetection.from_pretrained("IDEA-Research/grounding-dino-tiny", device_map="auto") text_labels = [["serum bottle"]] # or the SKU name printed on the label boxes = {} for f in frames: image = Image.open(f).convert("RGB") inputs = processor(images=image, text=text_labels, return_tensors="pt").to(model.device) with torch.no_grad(): outputs = model(**inputs) res = processor.post_process_grounded_object_detection( outputs, inputs.input_ids, threshold=0.4, text_threshold=0.3, target_sizes=[image.size[::-1]])[0] if len(res["boxes"]): i = int(res["scores"].argmax()) boxes[int(f.stem)] = (res["boxes"][i].tolist(), image.size) # [x0,y0,x1,y1] in pixels, (W,H)

The hand-near-product rule as a zone. Expand the detected box by a margin and make it a PolygonZone. Turn each hand landmark into a tiny detection and ask the zone whether it is inside. Landmarks are normalised and the box is in pixels, so scale the landmarks up; assert the convention once.

★ supervision 0.30.5 PolygonZone around the product box import numpy as np import supervision as sv MARGIN = 0.15 # of box width; "one hand-width" in your labelling rule def in_hand(sec): if sec not in boxes: return False (x0, y0, x1, y1), (W, H) = boxes[sec] m = MARGIN * (x1 - x0) zone = sv.PolygonZone(polygon=np.array([[x0-m, y0-m], [x1+m, y0-m], [x1+m, y1+m], [x0-m, y1+m]]).astype(int), triggering_anchors=(sv.Position.CENTER,)) pts = fr.loc[fr.sec == sec, "hand_pts"].iloc[0] if not pts: return False assert all(0 <= x <= 1 and 0 <= y <= 1 for x, y in pts), "landmarks must be normalised" xyxy = np.array([[x*W - 1, y*H - 1, x*W + 1, y*H + 1] for x, y in pts]) # 2 px squares at each landmark return bool(zone.trigger(sv.Detections(xyxy=xyxy)).any()) fr["product_in_hand"] = fr.sec.map(in_hand)

Aggregate to the minute. Shares for booleans, mean for the ratio, count for cuts, and null where fewer than 30 frames were usable. Price overlay comes from Florence-2's OCR_WITH_REGION run the same way as step 3; the per-frame rule is "a currency symbol followed by digits appeared anywhere", and it is merged in here as a boolean column called price_seen.

★ pandas: sixty answers to one row import re fr["t"] = (fr.sec - 1) // 60 + 1 # minute 1 is seconds 1..60 fr["usable"] = fr.host_on_camera.notna() # the detector ran and returned # price_seen: from Florence-2 OCR_WITH_REGION per frame, e.g. bool(re.search(r"[$£€]\s?\d", text)) g = fr[fr.usable].groupby("t") minute = pd.DataFrame({ "usable_frames": g.size(), "product_in_hand_share": g.product_in_hand.mean(), "host_on_camera": g.host_on_camera.mean(), "close_up_ratio": g.close_up_ratio.mean(), "price_overlay_share": g.price_seen.mean(), }).reindex(range(1, 121)) minute.loc[minute.usable_frames.fillna(0) < 30, minute.columns.drop("usable_frames")] = None minute["scene_cuts"] = scene_cuts # session 2's per-minute count minute["product_in_hand"] = (minute.product_in_hand_share >= 0.5).astype("Int64") minute["price_overlay"] = (minute.price_overlay_share >= 0.5).astype("Int64") minute.round(2).head()

Reconcile against the course table, then overwrite with your own. The 7,200-row table already carries product_in_hand, close_up_ratio, price_overlay and scene_cuts for every row. On the course data your pipeline should reproduce them; on your own recording it replaces them, and host_on_camera joins the table as a new column either way.

★ Same key, same columns, your numbers m = pd.read_csv("data/streams.csv") cols = ["product_in_hand", "close_up_ratio", "price_overlay", "scene_cuts"] assert m[cols].notna().all().all() and len(m) == 7200 # precomputed for the course data mine = minute.reset_index().rename(columns={"index": "t"}).assign(stream_id=STREAM_ID) m = m.merge(mine[["stream_id", "t", "host_on_camera", "usable_frames"] + cols], on=["stream_id", "t"], how="left", suffixes=("", "_mine")) for c in cols: # your detector wins where it ran m[c] = m[f"{c}_mine"].where(m[f"{c}_mine"].notna(), m[c]) m = m.drop(columns=[f"{c}_mine" for c in cols]) m.to_csv("data/derived/minute_with_vision.csv", index=False)

Run the 200-frame check before you trust a single column. Precision and recall per column, on frames you labelled by hand, committed next to the code. When a share looks wrong in session 5, the first question will be whether the detector or the host did it, and the labels are the only way to answer.

Homework

Before session 4

45 min
  1. Hand-label 200 random frames from your own recording for product in hand, host on camera and price on screen. Run tonight's pipeline on them and report precision and recall per column. Commit the labels.
  2. Change the Grounding DINO prompt from "serum bottle" to the exact SKU name on the label, then to "bottle". Rerun on the 200 frames and write down what each change does to precision and to recall. One of them will find the water bottle.
  3. Write a one-paragraph licence memo for your own organisation: which of the four routes it can run in an internal dashboard without buying anything, quoting the Ultralytics licence page's wording on closed-source internal tools.
  4. From the course table, compute the mean close_up_ratio and mean scene_cuts for human hosts and AI hosts separately. Then look up which host type sells better per thousand views in session 1's funnel table, and notice that the more consistent camera is the one that sells less.
Check yourself

Three questions

An MCN wants to ship a stream-quality dashboard for its own coaches. It runs on the MCN's servers and the code is not published. Which detector stack can it ship without buying a licence?

The Ultralytics licence page names closed-source internal tools as a case that requires the Enterprise licence, and YOLOE ships inside the same package. The permissive stack can run closed, on a server, without a cheque; the price is a little accuracy and a little more glue code.

The detector says true or false per frame. Why does the minute table carry product_in_hand as a share of frames rather than a single true or false?

Aggregation should lose as little as possible. The share is the natural statistic of sixty booleans; the course table's 0/1 flag is the share rounded at one half, and your own pipeline should keep both, along with usable_frames as the denominator.

Within a stream, what do the camera columns move, and what do they leave alone?

The within-stream CTOR effects for the same three columns are +0.04, +0.46 and -0.13 points, none distinguishable from zero. Session 4's speech columns are where CTOR moves. That split is why session 8's cue "product in hand on every CTA" works: it pairs the camera's click with the words' order.

Sources covered

What tonight maps to

✓ Four detector routes with versions and licences: ultralytics 8.4.161 YOLO26 and YOLOE, MediaPipe 1.0.1, Grounding DINO 1.0, OWLv2, Florence-2, supervision 0.30.5, trackers 2.6.0 Read by licence first. The build-along uses the Apache-2.0 and MIT routes end to end.
✓ Five minute columns and their aggregation: product_in_hand, close_up_ratio, host_on_camera, scene_cuts, price_overlay Share for booleans, mean for ratios, count for cuts, null under 30 usable frames, usable_frames kept as the denominator.
✓ The AGPL decision for a closed-source internal tool Ultralytics' own wording on which uses require Enterprise; the three ways out; the one taken tonight.
◐ OCR and price cards Florence-2 OCR_WITH_REGION named and the per-frame rule stated; the full prompt loop is left to the reader, it mirrors step 3.
◐ VLM labelling and its cost Gemini 3.8 Flash priced tonight at about $0.006 per minute of video; session 4 uses it as a segment labeller where a sentence is the right output.
· Training a custom detector Not covered by design. An open-vocabulary prompt with the SKU name reaches the accuracy the columns need without a training set, and keeps the licence question simple.

Builder session 3 cheat sheet

Licence firstUltralytics: AGPL-3.0 or Enterprise. Closed-source internal tools need Enterprise. YOLOE is in the same package.
The permissive stackMediaPipe 1.0.1 (Apache), Grounding DINO 1.0 / OWLv2 (Apache), Florence-2 (MIT), supervision 0.30.5 (MIT), trackers 2.6.0 (Apache).
LandmarksHandLandmarker 21 points, PoseLandmarker 33. VIDEO mode, detect_for_video(img, timestamp_ms), frame n at n x 1000.
Name the SKUtext_labels [["serum bottle"]], threshold=0.4, text_threshold=0.3. The kwarg is threshold in v5.17, not box_threshold.
In handDetector box + margin = PolygonZone; any hand landmark inside triggers it. Landmarks normalised, boxes in pixels: scale once, assert once.
AggregateBooleans to shares, ratios to means, cuts to counts. Under 30 usable frames: null, not zero. Keep usable_frames.
Validate200 hand-labelled frames, precision and recall per column, labels committed. Re-run on every model or prompt change.
What it is worthWithin a stream: CTR +1.38 in hand, +1.01 close-up, +0.97 price on screen. CTOR unmoved. Camera earns the click.