| | import json, textwrap, time |
| | from datetime import datetime |
| | import gradio as gr |
| |
|
| | |
| | STORIES = [ |
| | { |
| | "title": "Microsoft 365 Premium launches ($19.99/mo) with integrated Copilot", |
| | "date": "2025-10-01", |
| | "category": "Agents & Productivity", |
| | "summary": "Microsoft bundles Office + Copilot Pro into a single individual plan with higher usage limits and advanced features (Researcher, Analyst, Actions). Copilot Pro sunsets. Agents require Azure + metered credits.", |
| | "talk_track": [ |
| | "Copilot becomes the personal default.", |
| | "Premium unlocks higher usage ceilings.", |
| | "Agents: mind the Azure/Copilot credit metering." |
| | ], |
| | "why_it_matters": "Mainstreaming AI assistants with clear pricing signals; agent workflows move into production norms.", |
| | "sources": [ |
| | ["Reuters", "https://www.reuters.com/technology/microsoft-launches-ai-powered-365-premium-bundle-1999-per-month-2025-10-01/"], |
| | ["Microsoft pricing", "https://www.microsoft.com/en-us/microsoft-365-copilot/pricing"] |
| | ] |
| | }, |
| | { |
| | "title": "Google 2K Nest cams for Gemini for Home + Home Premium subscription", |
| | "date": "2025-10-01", |
| | "category": "Edge & Devices", |
| | "summary": "New Nest cams (wider FOV, better low light) and descriptive AI alerts + daily Home Brief. Google Home Premium replaces Nest Aware tiers.", |
| | "talk_track": [ |
| | "First hardware built for Gemini for Home.", |
| | "Events → summaries → routines.", |
| | "Sub changes: Home Premium instead of Nest Aware." |
| | ], |
| | "why_it_matters": "House-scale agents: sensor streams become structured inputs for daily briefings and automation.", |
| | "sources": [ |
| | ["The Verge", "https://www.theverge.com/"] |
| | ] |
| | }, |
| | { |
| | "title": "OpenAI updates GPT-5 Instant behavior for crisis-support signals", |
| | "date": "2025-10-03", |
| | "category": "Models & Alignment", |
| | "summary": "Behavioral tuning for GPT-5 Instant improves distress detection and de-escalation, guided by experts.", |
| | "talk_track": [ |
| | "Alignment over raw capability.", |
| | "Important for support, education, first-touch contexts." |
| | ], |
| | "why_it_matters": "Stability + safety polish for high-throughput deployments.", |
| | "sources": [ |
| | ["OpenAI release notes", "https://help.openai.com/en/articles/9624314-model-release-notes"] |
| | ] |
| | }, |
| | { |
| | "title": "Anthropic policy: personal Claude chats used for training (opt-out)", |
| | "date": "2025-10-01", |
| | "category": "Policy & Privacy", |
| | "summary": "Non-enterprise Claude users are opted-in for training; opt-out available in settings. Enterprise/edu/gov excluded.", |
| | "talk_track": [ |
| | "Flip the privacy toggle before sensitive work.", |
| | "Audit retention + compliance posture." |
| | ], |
| | "why_it_matters": "Data governance becomes a weekly habit, not a yearly policy read.", |
| | "sources": [ |
| | ["Tom’s Guide explainer", "https://www.tomsguide.com/"] |
| | ] |
| | }, |
| | { |
| | "title": "NVIDIA: Cosmos world models + Newton physics + Isaac GR00T updates", |
| | "date": "2025-10-02", |
| | "category": "Robotics & Physical AI", |
| | "summary": "Open world-model family, open Newton physics in Isaac Lab, updated GR00T reasoning to accelerate robotics R&D.", |
| | "talk_track": [ |
| | "Synthetic data + open physics to speed loops.", |
| | "Bridging sim-to-real for smaller labs." |
| | ], |
| | "why_it_matters": "Lower barrier to realistic robot learning → more pilots in 2026.", |
| | "sources": [ |
| | ["NVIDIA Newsroom", "https://nvidianews.nvidia.com/"] |
| | ] |
| | } |
| | ] |
| |
|
| | |
| | def all_categories(): |
| | return sorted(list({s["category"] for s in STORIES})) |
| |
|
| | def fmt_story(story: dict) -> str: |
| | if not story: |
| | return "No story selected." |
| | date = datetime.fromisoformat(story["date"]).strftime("%b %d, %Y") |
| | bullets = "\n".join([f"- {b}" for b in story.get("talk_track", [])]) |
| | srcs = "\n".join([f"- [{t}]({u})" for t, u in story.get("sources", [])]) |
| | return ( |
| | f"### {story['title']}\n" |
| | f"**Date:** {date} **Category:** {story['category']}\n\n" |
| | f"**Summary:** {story['summary']}\n\n" |
| | f"**On-air notes:**\n{bullets}\n\n" |
| | f"**Why it matters:** {story['why_it_matters']}\n\n" |
| | f"**Sources:**\n{srcs}\n" |
| | ) |
| |
|
| | def teleprompter_text(story: dict) -> str: |
| | text = f"{story['title']}\n\n{story['summary']}\n\nWhy it matters: {story['why_it_matters']}" |
| | return textwrap.fill(text, width=90) |
| |
|
| | def list_rows(items): |
| | return [[s["date"], s["category"], s["title"]] for s in items] |
| |
|
| | def filter_and_search(category, query): |
| | items = STORIES |
| | if category and category != "All": |
| | items = [s for s in items if s["category"] == category] |
| | if query: |
| | q = query.lower().strip() |
| | items = [s for s in items if q in s["title"].lower() or q in s["summary"].lower()] |
| | return items |
| |
|
| | def build_ticker_html(text): |
| | return ( |
| | '<link rel="stylesheet" href="style.css">' |
| | '<div class="zen-ticker shell">' |
| | f' <div class="zen-ticker__track" id="zenTicker" data-speed="38">{text}</div>' |
| | "</div>" |
| | '<script src="script.js"></script>' |
| | ) |
| |
|
| | def build_prompter_html(text, speed=40, is_playing=True): |
| | playing = "true" if is_playing else "false" |
| | safe = text.replace("&","&").replace("<","<").replace(">",">") |
| | return ( |
| | '<link rel="stylesheet" href="style.css">' |
| | '<div class="prompter">' |
| | ' <div class="prompter__viewport">' |
| | ' <div class="prompter__scroll" id="prompterScroll"' |
| | f' data-speed="{speed}"' |
| | f' data-playing="{playing}">' |
| | f' <pre class="prompter__text">{safe}</pre>' |
| | ' </div>' |
| | ' </div>' |
| | ' <div class="prompter__controls">' |
| | ' <button id="pPlayPause" class="btn">⏯</button>' |
| | ' <button id="pSlower" class="btn">–</button>' |
| | ' <button id="pFaster" class="btn">+</button>' |
| | ' <button id="pFullscreen" class="btn">⤢</button>' |
| | f' <span class="prompter__speed" id="pSpeed">{speed}px/s</span>' |
| | ' </div>' |
| | '</div>' |
| | '<script src="script.js"></script>' |
| | ) |
| |
|
| | def pick(idx, items): |
| | if not items: |
| | return ("No stories here.", build_prompter_html(""), build_ticker_html(""), 0) |
| | i = max(0, min(int(idx), len(items) - 1)) |
| | story = items[i] |
| | headlines = " • ".join([s["title"] for s in items[:12]]) |
| | return ( |
| | fmt_story(story), |
| | build_prompter_html(teleprompter_text(story)), |
| | build_ticker_html(headlines), |
| | i |
| | ) |
| |
|
| | def add_story(json_blob): |
| | |
| | try: |
| | obj = json.loads(json_blob) |
| | if not isinstance(obj, dict): |
| | raise ValueError("JSON must be an object") |
| | if "title" not in obj or not obj["title"]: |
| | raise ValueError("Missing 'title'") |
| | if "summary" not in obj or not obj["summary"]: |
| | raise ValueError("Missing 'summary'") |
| | obj.setdefault("date", datetime.utcnow().date().isoformat()) |
| | obj.setdefault("category", "Misc") |
| | obj.setdefault("talk_track", []) |
| | obj.setdefault("why_it_matters", "") |
| | obj.setdefault("sources", []) |
| | STORIES.insert(0, obj) |
| | return "✅ Added.", ["All"] + all_categories() |
| | except Exception as e: |
| | return f"❌ {e}", ["All"] + all_categories() |
| |
|
| | def countdown(minutes): |
| | try: |
| | total = int(float(minutes) * 60) |
| | except Exception: |
| | total = 0 |
| | total = max(0, min(total, 90 * 60)) |
| | for t in range(total, -1, -1): |
| | mm, ss = divmod(t, 60) |
| | yield f"{mm:02d}:{ss:02d}" |
| | time.sleep(1) |
| |
|
| | |
| | with gr.Blocks(css="style.css", title="ZEN Weekly Live Radar PRO") as demo: |
| | gr.HTML('<link rel="stylesheet" href="style.css"><script src="script.js"></script>') |
| |
|
| | with gr.Row(elem_classes="row-top"): |
| | gr.Markdown("### 🛰️ ZEN Weekly Live Radar PRO — Paste stories, run teleprompter, scroll ticker") |
| |
|
| | with gr.Row(): |
| | with gr.Column(scale=1, elem_classes="card"): |
| | category = gr.Dropdown(choices=["All"] + all_categories(), value="All", label="Category") |
| | query = gr.Textbox(label="Search", placeholder="Filter titles & summaries…") |
| | list_state = gr.State([]) |
| |
|
| | table = gr.Dataframe(headers=["Date","Category","Title"], interactive=False) |
| | slider = gr.Slider(label="Select", minimum=0, maximum=max(0, len(STORIES)-1), step=1, value=0) |
| | refresh = gr.Button("Refresh") |
| |
|
| | gr.Markdown("#### Add a story (JSON)") |
| | template = { |
| | "title": "Sample: Your breaking item", |
| | "date": datetime.utcnow().date().isoformat(), |
| | "category": "Breaking", |
| | "summary": "One-line explainer.", |
| | "talk_track": ["Point 1", "Point 2"], |
| | "why_it_matters": "The punchline.", |
| | "sources": [["Source Name", "https://example.com"]] |
| | } |
| | gr.Code(value=json.dumps(template, indent=2), language="json", label="Template") |
| | blob = gr.Textbox(lines=10, placeholder="Paste JSON here…") |
| | add_btn = gr.Button("Add") |
| | add_status = gr.Markdown() |
| |
|
| | gr.Markdown("#### On-air countdown") |
| | mins = gr.Number(label="Minutes", value=5) |
| | start = gr.Button("Start") |
| | time_left = gr.Textbox(label="Time left", value="05:00", interactive=False) |
| |
|
| | with gr.Column(scale=2, elem_classes="card"): |
| | story_md = gr.Markdown(fmt_story(STORIES[0])) |
| | prompter_html = gr.HTML(build_prompter_html(teleprompter_text(STORIES[0]))) |
| | ticker_html = gr.HTML(build_ticker_html(" • ".join([s["title"] for s in STORIES[:12]]))) |
| |
|
| | |
| | def refresh_list(cat, q): |
| | items = filter_and_search(cat, q) |
| | rows = list_rows(items) |
| | mx = max(0, len(items) - 1) |
| | sel_md, prom_html, tick_html, _ = pick(0, items) |
| | return ( |
| | items, |
| | gr.update(value=rows), |
| | gr.update(maximum=mx, value=0), |
| | sel_md, |
| | prom_html, |
| | tick_html |
| | ) |
| |
|
| | def on_select(i, cat, q, items): |
| | items = filter_and_search(cat, q) |
| | sel_md, prom_html, tick_html, _ = pick(i, items) |
| | return sel_md, prom_html, tick_html |
| |
|
| | refresh.click( |
| | refresh_list, |
| | inputs=[category, query], |
| | outputs=[list_state, table, slider, story_md, prompter_html, ticker_html] |
| | ) |
| | category.change( |
| | refresh_list, |
| | inputs=[category, query], |
| | outputs=[list_state, table, slider, story_md, prompter_html, ticker_html] |
| | ) |
| | query.submit( |
| | refresh_list, |
| | inputs=[category, query], |
| | outputs=[list_state, table, slider, story_md, prompter_html, ticker_html] |
| | ) |
| | slider.release( |
| | on_select, |
| | inputs=[slider, category, query, list_state], |
| | outputs=[story_md, prompter_html, ticker_html] |
| | ) |
| | add_btn.click(add_story, [blob], [add_status, category]).then( |
| | refresh_list, [category, query], [list_state, table, slider, story_md, prompter_html, ticker_html] |
| | ) |
| | start.click(countdown, [mins], [time_left]) |
| |
|
| | if __name__ == "__main__": |
| | demo.queue().launch(ssr_mode=False) |
| |
|