میں نے ایک وجہ سے اپنے اسکرین تجزیہ آلے کی تعمیر کی: میں نے کچھ براہ راست عملی کرنا چاہا جو ایک اسکرین ریکارڈنگ کو دیکھ سکتا ہے، ایک کام کی رفتار کے طور پر کیا ہوا ہے، اور پھر اس سمجھ کو خود کارکردگی کے آرٹویٹ میں تبدیل کر سکتے ہیں (n8n flows، مرحلے کی فہرستیں، ساختہ خلاصہ، پورے پائپ لائن). قیمت کا حصہ JSON پیدا کرنے یا ایک رپورٹ rendering نہیں ہے. یہ multimodal سمجھنے کا مرحلہ ہے-ہر اضافی فریم آپ کو ایک ماڈل کو بھیجتے ہیں حقیقی پیسے ہے. اسکرین ریکارڈنگ فہرست نمائش کے لئے سب سے بدترین صورت میں انٹرویو توسیع ہے: ایک سٹیٹک UI کے طویل عرصے، پھر ناگہاں مائیکرو بلڈز جہاں صارف کلک کرتا ہے، دو حروف لکھتا ہے، ایک dropdown کھولتا ہے، ایک modal flashes، یا ایک ٹیب swaps. لہذا میں نے فریمز کو " ڈیٹا پوائنٹس" کے طور پر علاج کرنے سے روک دیا اور انہیں ایک بجٹ کے طور پر علاج کرنا شروع کر دیا. سب سے پہلے غلط کیا گیا تھا (یہ ناکامی جس نے تبدیلی کو مجبور کیا) میرا پہلا کاٹنا واضح تھا: ہر Nth فریم کا نمونہ. یہ ورژن دو طریقوں میں ناکام ہوگیا، اور دونوں مصنوعات میں ظاہر تھے. میرے ڈسپلے میں ایک عام اپ لوڈ 6-12 منٹ کی ریکارڈنگ ہے. ان میں سے بہت سے ریکارڈنگ میں، صارف سوچنے کے لئے وقفے لگتا ہے، ایک صفحہ پڑھتا ہے، یا کورسر صرف وہاں بیٹھا ہے. Failure #1: it wasted frames on dead air. سب سے بدترین واقعہ (جس نے مجھے نمبروں کو دوبارہ لکھنے کا سبب بنایا) ایک مختصر ایڈمن کام کے جریان کی ریکارڈنگ تھی جہاں ایک اہم اجازت ڈپازٹ فوری طور پر کھول دیا اور بند کیا گیا تھا. uniform sampler نے ڈپازٹ سے پہلے اور فوری طور پر بعد میں فریمز پکڑ لیا - لہذا ماڈل نے کبھی اجازت کے انتخاب کو نہیں دیکھا. Failure #2: it missed the “blink-and-you-miss-it” UI moments. یہ مجموعہ - سٹیٹک وقت کے لئے ادائیگی کرتے ہوئے اب بھی حقیقی کارروائی کو بھولنا - غیر قابل قبول تھا. میں نے ایک sampler کی ضرورت تھی جو فریمز خرچ کرتا ہے جہاں ویڈیو بدل رہا ہے اور سٹیٹک UI پر رینج ادا کرنے سے روکتا ہے. اہم خیال: فریمز خرچ کرتے ہیں جہاں معلومات موجود ہے نجی نقطہ نظر "ہر Nth فریم نمونہ ہے." یہ انصاف محسوس کرتا ہے. یہ بھی غلط ہے. ایک سکرین ریکارڈنگ ایک فلم نہیں ہے. یہ ایک لائبریری کے قریب ہے: طویل مستحکم ریاستوں کو مختصر ٹرانزیشنز کی طرف سے پوائنٹ کیا جاتا ہے. آپ مستحکم UI کے لئے ادائیگی کرتے ہیں. آپ کو مختصر دھماکوں میں کمی ملتی ہے جہاں کام کے بہاؤ واقعی ہوتا ہے. ایک مثال (میں اسے ایک بار استعمال کروں گا اور آگے بڑھوں گا): مساوی طور پر نمائش کرنا ہر ٹیم کو ایک ہی بونس ادا کرنے کی طرح ہے، اثر کے بغیر. حل یہ ہے کہ قیمت میں تبدیلی کا اندازہ کریں، حصوں میں وقت گروپ کریں، اور ان حصوں میں ایک مقررہ کلید فریم budgets تقسیم کریں. Runtime آرکیٹیکل: جہاں sampler بیٹھتا ہے عملی طور پر، نظام دو تعاون کرنے والے حصوں میں تقسیم کیا جاتا ہے: ایک Python تجزیہ سروس (Cloud Run سروس کے طور پر ڈسپلے کیا جاتا ہے) جو اپ لوڈ کردہ ویڈیو کو پروسیسنگ کرتا ہے، کلید فریمز کا انتخاب کرتا ہے، Multimodal تجزیہ چلاتا ہے، اور ایک ساختہ نتائج کا استعمال بوجھ پیدا کرتا ہے. ایک Next.js اپلی کیشن جو webhook کے ذریعے نتائج حاصل کرتا ہے اور اسے برقرار رکھتا ہے (اور ڈسپلے بورڈ UI کو چلاتا ہے). نمونہ لینے والا نمونہ لینے سے پہلے نمونہ کرنے والے کے اندر اندر رہتا ہے، مہنگا ماڈل کال. غیر واضح نقطہ: نمبروں کو ایک "ایک اچھا آپٹمیشن" نہیں ہے. یہ ایک کنٹرول سطح ہے. یہ "یہ ویڈیو کتنا طویل ہے؟" کو "میں کتنا تجزیہ ادا کرنا چاہتا ہوں؟" میں تبدیل کرتا ہے. تجزیہ کار ایک دستخط ویب ہاک کے ذریعے نتائج کو Next.js اپلی کیشن میں واپس بھیجتا ہے- HMAC-SHA256 درست JSON بائٹز پر، کے ساتھ تصدیق اس کے علاوہ، آپ کو اس بات کا یقین ہے کہ آپ کیا کرسکتے ہیں ( )، ایک پائٹون ڈائٹ نہیں ہے جو HTTP لائبریری کی طرف سے دوبارہ سیریز کیا جاتا ہے. یہ ناپسندیدہ تصدیق ناکامیاں پیدا کرتا ہے جو روحوں کی طرح محسوس ہوتا ہے. لیکن ویب ہاک سمیٹ ایک مختلف پوسٹ ہے-یہ یہاں اہم ہے کہ کیا ہوتا ہے payload analyst سے باہر نکلتا ہے. timingSafeEqual data=body_bytes پہلے Adaptive keyframe sampling: score → segment → allocate → pick indexes نمبروں کو ایک چار مرحلے کے پائپ لائن ہے: اس کے علاوہ، اس کا مطلب یہ ہے کہ اس کی قیمت میں تبدیلیاں ہوسکتی ہیں. ٹائم لائن کو "بھی زیادہ سے زیادہ مستحکم" اور "ہاکی تبدیلی" چلانے میں تقسیم کریں. گارڈرایلز کے ساتھ سیکشنز کے درمیان ایک مقررہ کلید فریم بوجھ.Allocate a fixed keyframe budget across segments with guardrails. ہر سیکنڈ کے اندر کنکریٹ فریم انڈیکس کا انتخاب کریں. یہ ساخت یہ عملی رکھتا ہے کہ کیا ہے. سکریننگ سستا ہے. حصص لائن ہے. توسیع پیشہ ورانہ ہے. نکالنے کا مرحلہ میکانی ہے. مرحلہ 1 - سکریننگ: سستی بصری تبدیلی اگر اسکورنگ بہت زیادہ خرچ کرتا ہے تو، میں صرف پائپ لائن میں پہلے سے ہی اکاؤنٹ منتقل کر رہا ہوں. اسکرین ریکارڈنگ کے لئے سب سے زیادہ قابل اعتماد بیس لائن سیگنال ہے : frame difference energy Grayscale میں تبدیل کریں. مسلسل فریموں کے درمیان مطلق فرق کا حساب کریں. ڈیفالٹ تصویر کے اوسط کو لے لو (استاد طور پر معیاری کریں). یہ پکڑتے ہیں: Cursor کی حرکت ٹائپنگ (Blinking caret اور ٹیکسٹ اپ ڈیٹ) Dropdowns اور ماڈل صفحات کے مترادفات ملک میں تبدیلیاں یہ کامل نہیں ہے، لیکن یہ تیزی سے ہے اور "کچھ ہوا" کے ساتھ اچھی طرح سے منسلک ہے. مرحلہ 2 — سیکشن تشکیل: ایک صداقت سے سکرین کے بہاؤ میں تبدیل کریں فی فریم پوائنٹس پکی ہیں.میں ڈسکاؤنٹر کو آواز کی پیروی کرنے کے لئے نہیں کرنا چاہتا. لہذا میں hysteresis کے ساتھ ایک سادہ ریاست کی مشین کا استعمال کرتے ہوئے حصوں: ایک رولنگ اوسط سکور برقرار رکھیں. ایک " گرم" حصے میں منتقل جب رولنگ پوائنٹ ایک درجہ بندی سے اوپر اضافہ ہوتا ہے. ٹرانسپورٹ واپس "چلنے" جب یہ ایک کم حد کے نیچے آتا ہے. ایک کم از کم سیکنڈ کی لمبائی کو قائم کریں تاکہ میں سینکڑوں مائیکرو سیکنڈ پیدا نہ کروں. یہ تعلیمی تبدیلی کے نقطہ نظر کی تشخیص نہیں ہے. یہ انجینئرنگ ہے: مستحکم رویہ، کم ٹوننگ اوورچ اور پیشہ ورانہ پیداوار. مرحلہ 3 - گارڈرایل کے ساتھ بجٹ خالص نسبتا توسیع کافی نہیں ہے. یہ ختم کرنے پر ناکام ہے اور یہ مختصر حصوں کو بھوکا کر سکتا ہے. لہذا میری تقسیم تین قواعد ہیں: ہر حصے کو ایک سطح ملتی ہے (min_frames_per_segment). کوئی سیکنڈ ایک cap (max_frames_per_segment) سے زیادہ نہیں ہوسکتا. باقی سرمایہ کاروں کو مجموعی طور پر تقسیم کیا جاتا ہے. یہ تقسیم کو مستحکم بناتا ہے اور بیماریوں کو روکتا ہے. مرحلہ 4 - حصوں کے اندر انڈیکس کا انتخاب ایک بار جب ایک سیکشن کو K فریم دیا گیا ہے تو، میں سیکشن پر پھیلنے والے K فریموں کو منتخب کرتا ہوں: ہمیشہ سیکشن کا آغاز شامل کریں (تاریخ کے موضوعات). ہمیشہ سیکشن کے اختتام کو شامل کریں (final state matters). باقی حصوں کو مساوی طور پر تقسیم کردہ اشارے کے ساتھ بھر دیں. اگر مجھے بعد میں زیادہ وفاداری کی ضرورت ہے تو، میں اسکور کے مقامی زیادہ سے زیادہ کی طرف منحصر ہوسکتا ہوں، لیکن مساوی طور پر فاصلہ انتخاب ایک مضبوط بنیادی لائن ہے اور کوڈ کو براہ راست رکھتا ہے. مکمل چلایا جا سکتا ایپلی کیشن (Scoring + Segmentation + Allocation + Extraction) اس پوسٹ کو کاپی کرنے کے لئے، یہاں ایک واحد پائٹون سکرپٹ ہے جو آپ کسی بھی MP4 کے خلاف چل سکتے ہیں. ادویات : OpenPython کے لئے Numpy کے چلیں : python adaptive_keyframes.py --video input.mp4 --budget 60 --out ./keyframes یہاں ہے : adaptive_keyframes.py import argparse import os from dataclasses import dataclass from typing import List, Tuple import cv2 import numpy as np @dataclass class Segment: start: int # inclusive frame index end: int # exclusive frame index score: float @property def length(self) -> int: return max(0, self.end - self.start) def frame_diff_score(prev_bgr: np.ndarray, curr_bgr: np.ndarray) -> float: """Cheap per-frame change score in [0, 1] (roughly). Uses grayscale mean absolute difference normalized by 255. """ prev_gray = cv2.cvtColor(prev_bgr, cv2.COLOR_BGR2GRAY) curr_gray = cv2.cvtColor(curr_bgr, cv2.COLOR_BGR2GRAY) diff = cv2.absdiff(prev_gray, curr_gray) return float(diff.mean() / 255.0) def compute_scores( cap: cv2.VideoCapture, stride: int = 1, max_frames: int | None = None, ) -> Tuple[List[float], int]: """Return (scores, total_frames_read). scores[i] is the change score between frame i and i+stride (based on sampled reads). """ scores: List[float] = [] ok, prev = cap.read() if not ok: return scores, 0 frame_idx = 1 frames_read = 1 while True: # Skip stride-1 frames between comparisons. for _ in range(stride - 1): ok = cap.grab() if not ok: return scores, frames_read frame_idx += 1 frames_read += 1 if max_frames is not None and frames_read >= max_frames: return scores, frames_read ok, curr = cap.read() if not ok: return scores, frames_read frames_read += 1 s = frame_diff_score(prev, curr) scores.append(s) prev = curr frame_idx += 1 if max_frames is not None and frames_read >= max_frames: return scores, frames_read def segment_scores( scores: List[float], window: int = 8, hot_thresh: float = 0.030, cold_thresh: float = 0.020, min_len: int = 12, ) -> List[Segment]: """Convert per-step scores into segments with a utility score. Uses a rolling mean with hysteresis to avoid segment flicker. """ if not scores: return [] # Rolling mean via cumulative sum. x = np.array(scores, dtype=np.float32) c = np.cumsum(np.insert(x, 0, 0.0)) def roll_mean(i: int) -> float: j0 = max(0, i - window + 1) n = i - j0 + 1 return float((c[i + 1] - c[j0]) / n) segments: List[Segment] = [] state_hot = False seg_start = 0 seg_scores: List[float] = [] for i in range(len(scores)): rm = roll_mean(i) if state_hot: seg_scores.append(scores[i]) if rm < cold_thresh: # Close hot segment at i+1 seg_end = i + 1 if seg_end - seg_start < min_len: # Too short: merge into previous if possible, else keep. pass segments.append(Segment(seg_start, seg_end, float(np.mean(seg_scores) if seg_scores else 0.0))) # Start cold state_hot = False seg_start = seg_end seg_scores = [] else: if rm > hot_thresh: # Close cold segment seg_end = i + 1 cold_score = float(np.mean(scores[seg_start:seg_end]) if seg_end > seg_start else 0.0) segments.append(Segment(seg_start, seg_end, cold_score)) # Start hot state_hot = True seg_start = seg_end seg_scores = [] # Close tail tail_end = len(scores) if tail_end > seg_start: tail_score = float(np.mean(scores[seg_start:tail_end])) segments.append(Segment(seg_start, tail_end, tail_score)) # Merge very short segments to keep output stable. merged: List[Segment] = [] for seg in segments: if not merged: merged.append(seg) continue if seg.length < min_len: prev = merged[-1] combined = Segment(prev.start, seg.end, (prev.score * prev.length + seg.score * seg.length) / max(1, (prev.length + seg.length))) merged[-1] = combined else: merged.append(seg) # One more pass: ensure non-empty and strictly increasing. cleaned: List[Segment] = [] for seg in merged: if seg.length <= 0: continue if cleaned and seg.start < cleaned[-1].end: seg = Segment(cleaned[-1].end, seg.end, seg.score) if seg.length > 0: cleaned.append(seg) return cleaned def allocate_frames( segments: List[Segment], budget: int, min_frames_per_segment: int = 1, max_frames_per_segment: int = 30, ) -> List[int]: """Allocate keyframes to segments using floor + proportional + cap.""" if budget <= 0 or not segments: return [] n = len(segments) min_total = min_frames_per_segment * n # If budget is smaller than the floor, distribute 1-by-1. if min_total >= budget: alloc = [0] * n for i in range(budget): alloc[i % n] += 1 return alloc utilities = np.array([max(0.0, s.score) for s in segments], dtype=np.float64) total_u = float(utilities.sum()) alloc = [min_frames_per_segment] * n remaining = budget - min_total if total_u == 0.0: raw = np.full(n, remaining / n, dtype=np.float64) else: raw = utilities * (remaining / total_u) # Add integer parts. for i in range(n): add = int(raw[i]) alloc[i] = min(max_frames_per_segment, alloc[i] + add) allocated = sum(alloc) # Distribute leftover by fractional parts, respecting caps. if allocated < budget: frac = raw - np.floor(raw) order = np.argsort(-frac) # descending fractional idx = 0 safety = 0 while allocated < budget and safety < 10_000: i = int(order[idx % n]) if alloc[i] < max_frames_per_segment: alloc[i] += 1 allocated += 1 idx += 1 safety += 1 # If we somehow exceeded budget due to caps/floor interplay, trim from lowest utility. if allocated > budget: order = np.argsort(utilities) # ascending utility idx = 0 safety = 0 while allocated > budget and safety < 10_000: i = int(order[idx % n]) if alloc[i] > 0 and alloc[i] > min_frames_per_segment: alloc[i] -= 1 allocated -= 1 idx += 1 safety += 1 return alloc def pick_indices_for_segment(seg: Segment, k: int) -> List[int]: """Pick k indices in [seg.start, seg.end] over the score-step domain. Note: scores are defined between frames; we later map these to actual frames. """ if k <= 0 or seg.length <= 0: return [] if k == 1: return [seg.start] # Evenly spaced across [start, end-1] xs = np.linspace(seg.start, seg.end - 1, num=k) idxs = sorted({int(round(x)) for x in xs}) # Ensure exactly k by filling gaps if rounding collapsed points. while len(idxs) < k: # Insert midpoints between existing points. candidates = [] for a, b in zip(idxs, idxs[1:]): if b - a >= 2: candidates.append((a + b) // 2) if not candidates: # Fall back: walk forward. x = idxs[-1] if x + 1 < seg.end: idxs.append(x + 1) else: break else: for c in candidates: if c not in idxs and seg.start <= c < seg.end: idxs.append(c) if len(idxs) >= k: break idxs = sorted(idxs) # Trim if we overshot. return idxs[:k] def select_keyframe_indices(segments: List[Segment], alloc: List[int], stride: int = 1) -> List[int]: """Return concrete frame indices (0-based) to extract from the video.""" chosen: List[int] = [] for seg, k in zip(segments, alloc): step_idxs = pick_indices_for_segment(seg, k) # Map score-step domain to frame indices. # score i corresponds to diff between frame i and i+stride; # picking frame i is a reasonable representative. for si in step_idxs: chosen.append(si * stride) chosen = sorted(set(chosen)) return chosen def extract_frames(video_path: str, frame_indices: List[int], out_dir: str) -> None: os.makedirs(out_dir, exist_ok=True) cap = cv2.VideoCapture(video_path) if not cap.isOpened(): raise RuntimeError(f"Failed to open video: {video_path}") frame_set = set(frame_indices) max_idx = max(frame_set) if frame_set else -1 idx = 0 saved = 0 while idx <= max_idx: ok, frame = cap.read() if not ok: break if idx in frame_set: path = os.path.join(out_dir, f"frame_{idx:06d}.jpg") ok2 = cv2.imwrite(path, frame) if not ok2: raise RuntimeError(f"Failed to write: {path}") saved += 1 idx += 1 cap.release() if saved == 0 and frame_indices: raise RuntimeError("No frames were saved; check indices and video decoding") def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--video", required=True, help="Path to input video") ap.add_argument("--out", required=True, help="Output directory for keyframes") ap.add_argument("--budget", type=int, default=60, help="Total keyframes to extract") ap.add_argument("--stride", type=int, default=2, help="Compare every Nth frame for scoring") ap.add_argument("--window", type=int, default=8, help="Rolling window for segmentation") ap.add_argument("--hot", type=float, default=0.030, help="Enter hot segment threshold") ap.add_argument("--cold", type=float, default=0.020, help="Exit hot segment threshold") args = ap.parse_args() cap = cv2.VideoCapture(args.video) if not cap.isOpened(): raise RuntimeError(f"Failed to open video: {args.video}") scores, frames_read = compute_scores(cap, stride=args.stride) cap.release() segments = segment_scores(scores, window=args.window, hot_thresh=args.hot, cold_thresh=args.cold) alloc = allocate_frames(segments, budget=args.budget, min_frames_per_segment=1, max_frames_per_segment=max(2, args.budget)) keyframes = select_keyframe_indices(segments, alloc, stride=args.stride) # Keep within a hard limit (rounding/uniqueness can change count). if len(keyframes) > args.budget: keyframes = keyframes[: args.budget] extract_frames(args.video, keyframes, args.out) print(f"frames_read={frames_read}") print(f"scores={len(scores)} segments={len(segments)}") print(f"budget={args.budget} selected={len(keyframes)}") if segments: hot_share = sum(1 for s in segments if s.score > args.hot) / len(segments) print(f"segment_hot_share={hot_share:.2f}") if __name__ == "__main__": main() اس سکرین کا مقصد براہ راست ہے: یہ ایک مستحکم سیکشن کی فہرست پیدا کرتا ہے. یہ یقینی بناتا ہے کہ آپ کبھی بھی اپنے فریم budgets سے تجاوز نہیں کریں گے. یہ downstream تجزیہ کے لئے deterministic frame files لکھتا ہے. میری پیداوار کی سروس میں، نکالنے والی فریموں کو Multimodal Call (Gemini via میرے تقاضوں میں) اور پیدا ہونے والے تجزیہ پے پال لوڈ signed webhook کے ذریعے Next.js اپلی کیشن کو واپس بھیجا جاتا ہے. google-generativeai عملی ٹوننگ نوٹس (پہلے ڈیمو کے بعد اہم چیزیں) ایک بار جب آپ کو شکل کام کرتا ہے، فاتحین بدلے جاتے ہیں کہ غریب حقیقی دنیا کی ریکارڈنگ کے تحت ٹوننگ رویے. 1) ایک قدم منتخب کریں جو آپ کے مواد سے ملتا ہے اگر آپ 30 FPS ریکارڈنگ پر ہر فریم کو حاصل کرتے ہیں تو، آپ ہر جگہ چھوٹے کورسر حرکتوں کو تلاش کریں گے.یہ ہمیشہ برا نہیں ہے، لیکن یہ آپ کے "تبدیل" سگنل کو بڑھ سکتا ہے. 2-5 کی ایک قدم اسکرین ریکارڈنگ کے لئے ایک اچھا آغاز نقطہ ہے. آپ اب بھی UI تبدیلیوں کے لئے حساس ہیں، لیکن آپ sub-frame صدا کو ضائع کرتے ہیں. 2) Hysteresis سیکشن فلکر کو روکتا ہے استعمال کریں اور (دو درجہ بندی) اہم ہے. ایک ہی درجہ بندی کے ساتھ، آپ کو ٹھنڈا / ٹھنڈا کے درمیان مسلسل کاٹنے کے ارد گرد بونس کریں گے. hot_thresh cold_thresh ہسٹیسس آپ کو مستحکم حصوں فراہم کرتا ہے اور بجٹ کو پیشہ ورانہ طریقے سے چلتا ہے. 3) سطحوں اور چھتوں کو اختیاری نہیں ہیں ایک زمین کے بغیر، مختصر حصوں کو صفر تک ختم کر سکتے ہیں اور آپ کو صحیح مائکرو برش آپ کے بارے میں دلچسپی رکھتے ہیں کے بارے میں بھول جائیں گے. ایک چھت کے بغیر، ایک طویل "مجهز" سیکشن آپ کے پورے بجٹ کو استعمال کرسکتا ہے اور آپ کام کے عمل کے باقی حصے سے رابطے کو کھو سکتے ہیں. 4) آپ بھیجنے والے بائٹس کو ہمیشہ دستخط کریں اگر آپ دستخط کریں بھیجیں ، HTTP لائبریری آپ نے دستخط کیا ہے کے مقابلے میں مختلف کلید ترتیب / فاصلے کے ساتھ serializing کر سکتے ہیں. json.dumps(payload) json=payload (تصویر کے مترادفات) ) بوجھوں کی کل کلاس کو ہٹا دیتا ہے. یہ یہاں اہم ہے کیونکہ تجزیہ کی کارکردگی - جس میں آپ کی احتیاط سے منتخب کلید فریمز ہیں - پائپ لائن میں سب سے زیادہ مہنگی آرٹافٹ ہے. data=body_bytes مصنوعات میں اس ڈیزائن کی پیمائش کیوں نمونہ کار کام کرتا ہے کیونکہ یہ دو سخت محدودیتوں کا احترام کرتا ہے: منتخب شدہ فریم کے ساتھ قیمتوں کی پیمائش، ویڈیوز کی لمبائی نہیں ہے. ایک بار بجٹ مقرر کیا گیا ہے تو، مہنگی Multimodal قدم ایک اوپر حد ہے. ٹائم پیشہ ورانہ رہتا ہے. سکورنگ ایک لائیو پاس ہے؛ حصص ایک لائیو پاس ہے؛ توسیع چھوٹی سی مسلسل عوامل کے ساتھ لائیو ہے. سب سے اہم: نمبروں نے میرے نظام کو ایسا لگتا ہے جیسے میں کام کرسکتا ہوں. اس بات پر بحث کرنے کے بجائے کہ کیا 12 منٹ کی ویڈیو "بہت طویل" ہے، میں فیصلہ کرتا ہوں کہ میں کتنے فریمز خریدنے کے لئے تیار ہوں. یہ تمام نقطہ نظر ہے: تعلیمی توسیع منصفانہ نمائش کو شکست دیتا ہے.