MiniWeb-demo / src /miniweb /app /playback.py
sparklabuofu's picture
Update the demo
22de727 verified
Raw History Blame Contribute Delete
7.65 kB
"""Deterministic on-screen info streams for the shared mini-player.
Every [data-mini-player] video gets a timeline of "what's on screen right now"
segments (lower-third overlay rendered by player.js). Most segments are generic
scene lines, but a few short spans carry FACTS — a promo code, a reference
number, an attendance figure, a contact address — that are visible ONLY while
the playhead is inside that span. Tasks built on this force the agent to scrub
the seek bar to locate the span (search_by_playback macro).
Fully deterministic: the timeline is a pure function of (key, duration), where
the key is the page path + video title. Annotators and verifiers can GET the
same endpoint to know ground truth; parallel sessions see identical streams.
"""
import random
import zlib
from flask import Blueprint, jsonify, request
playback_bp = Blueprint("playback", __name__)
_FILLER = [
"Wide shot of the presenter on stage",
"Slide: agenda overview",
"Close-up: product demo on screen",
"Audience view, ambient music",
"Presenter walks through the main charts",
"Screen share: dashboard walkthrough",
"B-roll: office exterior",
"Panel discussion continues",
"Slide: key takeaways",
"Q&A — audience microphone",
"Whiteboard sketch of the architecture",
"Split screen: interviewer and guest",
"Timelapse footage plays",
"Live coding session on screen",
"Slide: quarterly numbers",
"Speaker anecdote, light laughter",
"Demo continues at normal speed",
"Graph animates on screen",
"Closing remarks begin",
"Credits and thank-you slide",
]
# Audio flavor: same segment structure/values, narration-style wording.
_FILLER_AUDIO = [
"Theme music plays",
"Host intro continues",
"Interview segment",
"Ad read: sponsor message",
"Host tells a listener story",
"Music bed under discussion",
"Caller question on air",
"Co-host banter",
"Deep-dive into the main topic",
"Short musical interlude",
"Host recaps the last episode",
"Sound clip plays",
"Listener mail segment",
"Host sets up the next guest",
"Discussion gets technical",
"Quick news roundup",
"Host shares a recommendation",
"Outro music begins",
"Closing thoughts",
"Credits and thank-yous",
]
_FIRST = ["Maya", "Daniel", "Aisha", "Tom", "Ingrid", "Rafael", "Yuki", "Omar",
"Petra", "Liam", "Sofia", "Chen"]
_LAST = ["Torres", "Novak", "Okoye", "Lindqvist", "Ferreira", "Tanaka",
"Haddad", "Kowalski", "Brennan", "Vargas"]
_DOMAINS = ["brightpeak.io", "northwind-events.com", "cascadia-media.org",
"summitline.co", "harborlight.tv"]
_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
# Promo prefix -> discount fraction. Codes are checksum-signed, so any site can
# verify a code it has never seen (validate_promo) with no shared registry —
# the e-commerce cart redeems codes found by scrubbing any video on any site.
PROMO_DISCOUNTS = {"SAVE": 0.10, "STREAM": 0.12, "VIP": 0.15, "EARLY": 0.20}
def _checksum_char(base):
return _ALPHABET[zlib.crc32(("miniweb-promo|" + base).encode("utf-8")) % len(_ALPHABET)]
def make_promo_code(rng):
"""PREFIX-XXXXXXC — six random chars plus a checksum character."""
prefix = rng.choice(list(PROMO_DISCOUNTS))
body = "".join(rng.choice(_ALPHABET) for _ in range(6))
base = f"{prefix}-{body}"
return base + _checksum_char(base)
def validate_promo(code):
"""Discount fraction (e.g. 0.10) for a playback promo code, else None."""
code = (code or "").strip().upper()
prefix, sep, rest = code.partition("-")
if not sep or prefix not in PROMO_DISCOUNTS or len(rest) != 7:
return None
if _checksum_char(code[:-1]) != code[-1]:
return None
return PROMO_DISCOUNTS[prefix]
def _make_facts(rng, flavor="video"):
"""Candidate fact generators — each returns (label, stream text, value).
The flavor changes ONLY the wording; value generation and rng consumption
are identical, so the same key yields the same values in either flavor.
"""
promo = make_promo_code(rng)
ref = f"{rng.randint(10000, 99999)}"
email = f"{rng.choice(['events', 'support', 'press', 'booking'])}@{rng.choice(_DOMAINS)}"
count = f"{rng.randint(1200, 48000):,}"
name = f"{rng.choice(_FIRST)} {rng.choice(_LAST)}"
amount = f"${rng.randint(90, 4900):,}"
if flavor == "audio":
pool = [
("promo code", f"Host reads out promo code {promo} — redeem at the online store", promo),
("reference number", f"Host gives reference number #{ref}", ref),
("contact email", f"Contact mentioned on air: {email}", email),
("live viewer count", f"Host: {count} listening live right now", count),
("guest name", f"Guest introduced: {name}", name),
("prize amount", f"Prize announced: {amount}", amount),
]
else:
pool = [
("promo code", f"On screen: promo code {promo} — redeem at the online store", promo),
("reference number", f"Overlay: reference #{ref}", ref),
("contact email", f"Lower third: contact {email}", email),
("live viewer count", f"Ticker: {count} watching live", count),
("guest name", f"Name card: {name}", name),
("prize amount", f"Banner: prize pool {amount}", amount),
]
rng.shuffle(pool)
return pool
def generate_timeline(key, duration, flavor="video"):
"""[{start, end, text, kind, (label, value)}] covering 0..duration."""
duration = max(30, min(int(duration or 0), 36000))
rng = random.Random(zlib.crc32(("miniweb-playback|" + key).encode("utf-8")))
n_facts = 2 if duration < 180 else 3
span = max(4, min(int(duration * 0.06), 25)) # facts visible only briefly
# Fact spans: distinct slots away from 0:00 so nothing is visible pre-scrub.
slots = []
for _ in range(200):
if len(slots) >= n_facts:
break
s = rng.randint(int(duration * 0.08), max(int(duration * 0.92) - span, int(duration * 0.08) + 1))
if all(abs(s - o) > span * 3 for o in slots):
slots.append(s)
slots.sort()
facts = _make_facts(rng, flavor)[:len(slots)]
segments = []
cursor = 0
fi = 0
filler = (_FILLER_AUDIO if flavor == "audio" else _FILLER)[:]
rng.shuffle(filler)
fill_i = 0
while cursor < duration:
if fi < len(slots) and cursor >= slots[fi] - 1:
label, text, value = facts[fi]
end = min(slots[fi] + span, duration)
segments.append({"start": slots[fi], "end": end, "text": text,
"kind": "fact", "label": label, "value": value})
cursor = end
fi += 1
continue
nxt = slots[fi] if fi < len(slots) else duration
end = min(cursor + rng.randint(8, 22), nxt, duration)
segments.append({"start": cursor, "end": end,
"text": filler[fill_i % len(filler)], "kind": "scene"})
fill_i += 1
cursor = end
return {"duration": duration, "segments": segments}
@playback_bp.route("/timeline")
def api_timeline():
key = (request.args.get("key") or "").strip()
duration = request.args.get("duration", type=float) or 0
flavor = "audio" if request.args.get("flavor") == "audio" else "video"
if not key:
return jsonify({"error": "key required"}), 400
return jsonify(generate_timeline(key, duration, flavor))
def register_playback_routes(app):
app.register_blueprint(playback_bp, url_prefix="/_player")