Update app.py
Browse files
app.py
CHANGED
|
@@ -2,7 +2,7 @@ import json, textwrap, time
|
|
| 2 |
from datetime import datetime
|
| 3 |
import gradio as gr
|
| 4 |
|
| 5 |
-
# ------------- SAMPLE WEEK (edit live on-air) ----------------
|
| 6 |
STORIES = [
|
| 7 |
{
|
| 8 |
"title": "Microsoft 365 Premium launches ($19.99/mo) with integrated Copilot",
|
|
@@ -79,11 +79,10 @@ STORIES = [
|
|
| 79 |
}
|
| 80 |
]
|
| 81 |
|
| 82 |
-
# Derive categories
|
| 83 |
def all_categories():
|
| 84 |
return sorted(list({s["category"] for s in STORIES}))
|
| 85 |
|
| 86 |
-
#
|
| 87 |
def fmt_story(story: dict) -> str:
|
| 88 |
if not story: return "No story selected."
|
| 89 |
date = datetime.fromisoformat(story["date"]).strftime("%b %d, %Y")
|
|
@@ -111,8 +110,8 @@ def teleprompter_text(story: dict) -> str:
|
|
| 111 |
Why it matters: {story['why_it_matters']}"""
|
| 112 |
return textwrap.fill(text, width=90)
|
| 113 |
|
| 114 |
-
def list_rows(
|
| 115 |
-
return [[s["date"], s["category"], s["title"]] for s in
|
| 116 |
|
| 117 |
def filter_and_search(category, query):
|
| 118 |
items = STORIES
|
|
@@ -123,22 +122,7 @@ def filter_and_search(category, query):
|
|
| 123 |
items = [s for s in items if q in s["title"].lower() or q in s["summary"].lower()]
|
| 124 |
return items
|
| 125 |
|
| 126 |
-
def pick(idx, items):
|
| 127 |
-
if not items:
|
| 128 |
-
return "No stories here.", "", "", ""
|
| 129 |
-
i = max(0, min(int(idx), len(items)-1))
|
| 130 |
-
story = items[i]
|
| 131 |
-
# Ticker content (first 12 headlines)
|
| 132 |
-
headlines = " • ".join([s["title"] for s in items[:12]])
|
| 133 |
-
return (
|
| 134 |
-
fmt_story(story),
|
| 135 |
-
teleprompter_text(story),
|
| 136 |
-
build_ticker_html(headlines),
|
| 137 |
-
build_prompter_html(teleprompter_text(story), speed=40, is_playing=True)
|
| 138 |
-
)
|
| 139 |
-
|
| 140 |
def build_ticker_html(text):
|
| 141 |
-
# Controlled by script.js via dataset attributes
|
| 142 |
return f"""
|
| 143 |
<link rel="stylesheet" href="style.css">
|
| 144 |
<div class="zen-ticker shell">
|
|
@@ -148,9 +132,7 @@ def build_ticker_html(text):
|
|
| 148 |
"""
|
| 149 |
|
| 150 |
def build_prompter_html(text, speed=40, is_playing=True):
|
| 151 |
-
# speed in px/sec; play state toggled via JS buttons
|
| 152 |
playing = "true" if is_playing else "false"
|
| 153 |
-
# Escape < & > minimally for HTML block
|
| 154 |
safe = text.replace("&","&").replace("<","<").replace(">",">")
|
| 155 |
return f"""
|
| 156 |
<link rel="stylesheet" href="style.css">
|
|
@@ -173,6 +155,19 @@ def build_prompter_html(text, speed=40, is_playing=True):
|
|
| 173 |
<script src="script.js"></script>
|
| 174 |
"""
|
| 175 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
def add_story(json_blob):
|
| 177 |
try:
|
| 178 |
obj = json.loads(json_blob)
|
|
@@ -200,20 +195,22 @@ def countdown(minutes):
|
|
| 200 |
yield f"{mm:02d}:{ss:02d}"
|
| 201 |
time.sleep(1)
|
| 202 |
|
| 203 |
-
# -------------------------- UI ----------------------------
|
| 204 |
-
with gr.Blocks(css="style.css",
|
| 205 |
gr.HTML("""<link rel="stylesheet" href="style.css"><script src="script.js"></script>""")
|
| 206 |
|
| 207 |
with gr.Row(elem_classes="row-top"):
|
| 208 |
gr.Markdown("### 🛰️ ZEN Weekly Live Radar PRO — Paste stories, run teleprompter, scroll ticker")
|
| 209 |
|
| 210 |
with gr.Row():
|
| 211 |
-
with gr.Column(scale=1, elem_classes="card"
|
| 212 |
category = gr.Dropdown(choices=["All"] + all_categories(), value="All", label="Category")
|
| 213 |
query = gr.Textbox(label="Search", placeholder="Filter titles & summaries…")
|
| 214 |
list_state = gr.State([])
|
|
|
|
| 215 |
table = gr.Dataframe(headers=["Date","Category","Title"], interactive=False, wrap=True)
|
| 216 |
-
|
|
|
|
| 217 |
refresh = gr.Button("Refresh")
|
| 218 |
|
| 219 |
gr.Markdown("#### Add a story (JSON)")
|
|
@@ -241,28 +238,51 @@ with gr.Blocks(css="style.css", fill_height=True, title="ZEN Weekly Live Radar P
|
|
| 241 |
prompter_html = gr.HTML(build_prompter_html(teleprompter_text(STORIES[0])))
|
| 242 |
ticker_html = gr.HTML(build_ticker_html(" • ".join([s["title"] for s in STORIES[:12]])))
|
| 243 |
|
| 244 |
-
# ----- Callbacks -----
|
| 245 |
def refresh_list(cat, q):
|
| 246 |
items = filter_and_search(cat, q)
|
| 247 |
rows = list_rows(items)
|
| 248 |
mx = max(0, len(items)-1)
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
|
| 253 |
def on_select(i, cat, q, items):
|
| 254 |
-
items
|
| 255 |
-
|
| 256 |
-
|
|
|
|
| 257 |
|
| 258 |
-
refresh.click(
|
| 259 |
-
|
| 260 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
|
| 262 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
|
| 264 |
add_btn.click(add_story, [blob], [add_status, category]).then(
|
| 265 |
-
refresh_list, [category, query], [list_state, table,
|
| 266 |
)
|
| 267 |
|
| 268 |
start.click(countdown, [mins], [time_left])
|
|
|
|
| 2 |
from datetime import datetime
|
| 3 |
import gradio as gr
|
| 4 |
|
| 5 |
+
# ----------------- SAMPLE WEEK (edit live on-air) -----------------
|
| 6 |
STORIES = [
|
| 7 |
{
|
| 8 |
"title": "Microsoft 365 Premium launches ($19.99/mo) with integrated Copilot",
|
|
|
|
| 79 |
}
|
| 80 |
]
|
| 81 |
|
|
|
|
| 82 |
def all_categories():
|
| 83 |
return sorted(list({s["category"] for s in STORIES}))
|
| 84 |
|
| 85 |
+
# ----------------- Utility fns -----------------
|
| 86 |
def fmt_story(story: dict) -> str:
|
| 87 |
if not story: return "No story selected."
|
| 88 |
date = datetime.fromisoformat(story["date"]).strftime("%b %d, %Y")
|
|
|
|
| 110 |
Why it matters: {story['why_it_matters']}"""
|
| 111 |
return textwrap.fill(text, width=90)
|
| 112 |
|
| 113 |
+
def list_rows(items):
|
| 114 |
+
return [[s["date"], s["category"], s["title"]] for s in items]
|
| 115 |
|
| 116 |
def filter_and_search(category, query):
|
| 117 |
items = STORIES
|
|
|
|
| 122 |
items = [s for s in items if q in s["title"].lower() or q in s["summary"].lower()]
|
| 123 |
return items
|
| 124 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
def build_ticker_html(text):
|
|
|
|
| 126 |
return f"""
|
| 127 |
<link rel="stylesheet" href="style.css">
|
| 128 |
<div class="zen-ticker shell">
|
|
|
|
| 132 |
"""
|
| 133 |
|
| 134 |
def build_prompter_html(text, speed=40, is_playing=True):
|
|
|
|
| 135 |
playing = "true" if is_playing else "false"
|
|
|
|
| 136 |
safe = text.replace("&","&").replace("<","<").replace(">",">")
|
| 137 |
return f"""
|
| 138 |
<link rel="stylesheet" href="style.css">
|
|
|
|
| 155 |
<script src="script.js"></script>
|
| 156 |
"""
|
| 157 |
|
| 158 |
+
def pick(idx, items):
|
| 159 |
+
if not items:
|
| 160 |
+
return ("No stories here.", build_prompter_html(""), build_ticker_html(""), 0)
|
| 161 |
+
i = max(0, min(int(idx), len(items)-1))
|
| 162 |
+
story = items[i]
|
| 163 |
+
headlines = " • ".join([s["title"] for s in items[:12]])
|
| 164 |
+
return (
|
| 165 |
+
fmt_story(story),
|
| 166 |
+
build_prompter_html(teleprompter_text(story)),
|
| 167 |
+
build_ticker_html(headlines),
|
| 168 |
+
i
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
def add_story(json_blob):
|
| 172 |
try:
|
| 173 |
obj = json.loads(json_blob)
|
|
|
|
| 195 |
yield f"{mm:02d}:{ss:02d}"
|
| 196 |
time.sleep(1)
|
| 197 |
|
| 198 |
+
# --------------------------- UI ----------------------------
|
| 199 |
+
with gr.Blocks(css="style.css", title="ZEN Weekly Live Radar PRO") as demo:
|
| 200 |
gr.HTML("""<link rel="stylesheet" href="style.css"><script src="script.js"></script>""")
|
| 201 |
|
| 202 |
with gr.Row(elem_classes="row-top"):
|
| 203 |
gr.Markdown("### 🛰️ ZEN Weekly Live Radar PRO — Paste stories, run teleprompter, scroll ticker")
|
| 204 |
|
| 205 |
with gr.Row():
|
| 206 |
+
with gr.Column(scale=1, elem_classes="card"):
|
| 207 |
category = gr.Dropdown(choices=["All"] + all_categories(), value="All", label="Category")
|
| 208 |
query = gr.Textbox(label="Search", placeholder="Filter titles & summaries…")
|
| 209 |
list_state = gr.State([])
|
| 210 |
+
|
| 211 |
table = gr.Dataframe(headers=["Date","Category","Title"], interactive=False, wrap=True)
|
| 212 |
+
|
| 213 |
+
slider = gr.Slider(label="Select", minimum=0, maximum=max(0, len(STORIES)-1), step=1, value=0)
|
| 214 |
refresh = gr.Button("Refresh")
|
| 215 |
|
| 216 |
gr.Markdown("#### Add a story (JSON)")
|
|
|
|
| 238 |
prompter_html = gr.HTML(build_prompter_html(teleprompter_text(STORIES[0])))
|
| 239 |
ticker_html = gr.HTML(build_ticker_html(" • ".join([s["title"] for s in STORIES[:12]])))
|
| 240 |
|
| 241 |
+
# --------------- Callbacks ----------------
|
| 242 |
def refresh_list(cat, q):
|
| 243 |
items = filter_and_search(cat, q)
|
| 244 |
rows = list_rows(items)
|
| 245 |
mx = max(0, len(items)-1)
|
| 246 |
+
sel_md, prom_html, tick_html, idx_val = pick(0, items)
|
| 247 |
+
return (
|
| 248 |
+
items, # list_state
|
| 249 |
+
rows, # table
|
| 250 |
+
gr.Slider.update(maximum=mx, value=0),
|
| 251 |
+
sel_md, # story_md
|
| 252 |
+
prom_html, # prompter_html
|
| 253 |
+
tick_html # ticker_html
|
| 254 |
+
)
|
| 255 |
|
| 256 |
def on_select(i, cat, q, items):
|
| 257 |
+
# items may be stale; recompute from cat/q for safety
|
| 258 |
+
items = filter_and_search(cat, q)
|
| 259 |
+
sel_md, prom_html, tick_html, _ = pick(i, items)
|
| 260 |
+
return sel_md, prom_html, tick_html
|
| 261 |
|
| 262 |
+
refresh.click(
|
| 263 |
+
refresh_list,
|
| 264 |
+
inputs=[category, query],
|
| 265 |
+
outputs=[list_state, table, slider, story_md, prompter_html, ticker_html]
|
| 266 |
+
)
|
| 267 |
+
category.change(
|
| 268 |
+
refresh_list,
|
| 269 |
+
inputs=[category, query],
|
| 270 |
+
outputs=[list_state, table, slider, story_md, prompter_html, ticker_html]
|
| 271 |
+
)
|
| 272 |
+
query.submit(
|
| 273 |
+
refresh_list,
|
| 274 |
+
inputs=[category, query],
|
| 275 |
+
outputs=[list_state, table, slider, story_md, prompter_html, ticker_html]
|
| 276 |
+
)
|
| 277 |
|
| 278 |
+
slider.release(
|
| 279 |
+
on_select,
|
| 280 |
+
inputs=[slider, category, query, list_state],
|
| 281 |
+
outputs=[story_md, prompter_html, ticker_html]
|
| 282 |
+
)
|
| 283 |
|
| 284 |
add_btn.click(add_story, [blob], [add_status, category]).then(
|
| 285 |
+
refresh_list, [category, query], [list_state, table, slider, story_md, prompter_html, ticker_html]
|
| 286 |
)
|
| 287 |
|
| 288 |
start.click(countdown, [mins], [time_left])
|