import os import re import html from datetime import datetime import gradio as gr from huggingface_hub import HfApi, hf_hub_download, hf_hub_url # ========================================================= # CONFIG # ========================================================= REPO_ID = os.getenv("REPO_ID") REPO_TYPE = "dataset" HF_TOKEN = os.getenv("HF_TOKEN") # optional if repo is public api = HfApi(token=HF_TOKEN) # ========================================================= # HELPERS # ========================================================= def format_ts(ts: int) -> str: try: return datetime.fromtimestamp(ts).strftime("%d %b %Y, %I:%M %p") except Exception: return "Unknown date" def natural_story_title(folder_name: str) -> str: # job_1775798146_Sunbeam_s_Secret -> Sunbeam s Secret m = re.match(r"job_(\d+)_(.+)$", folder_name) if not m: return folder_name.replace("_", " ") raw = m.group(2).replace("_", " ").strip() return raw def extract_job_ts(folder_name: str) -> int: m = re.match(r"job_(\d+)_", folder_name) return int(m.group(1)) if m else 0 def build_resolve_url(path: str) -> str: return hf_hub_url( repo_id=REPO_ID, filename=path, repo_type=REPO_TYPE, ) def list_repo_files(): """ Return all file paths from the dataset repo. """ files = [] # list_repo_tree is the recommended API for tree listing on HF Hub. tree = api.list_repo_tree( repo_id=REPO_ID, repo_type=REPO_TYPE, recursive=True, expand=True, ) for item in tree: path = getattr(item, "path", None) if path and not path.endswith("/"): files.append(path) return files def collect_stories(): repo_files = list_repo_files() stories = {} for path in repo_files: parts = path.split("/") if len(parts) < 2: continue folder = parts[0] if not folder.startswith("job_"): continue if folder not in stories: stories[folder] = { "folder": folder, "title": natural_story_title(folder), "timestamp": extract_job_ts(folder), "markdown_path": None, "video_path": None, "csv_path": None, "image_paths": [], } lower = path.lower() if lower.endswith(".md"): stories[folder]["markdown_path"] = path elif lower.endswith("full_video.mp4"): stories[folder]["video_path"] = path elif lower.endswith("dataset.csv"): stories[folder]["csv_path"] = path elif "/images/" in lower and lower.endswith((".png", ".jpg", ".jpeg", ".webp")): stories[folder]["image_paths"].append(path) # Sort images inside each story for _, story in stories.items(): story["image_paths"] = sorted( story["image_paths"], key=lambda p: [ int(x) if x.isdigit() else x.lower() for x in re.split(r"(\d+)", p) ] ) # Newest first sorted_stories = sorted( stories.values(), key=lambda x: x["timestamp"], reverse=True, ) return sorted_stories def download_markdown(path: str) -> str: if not path: return "_No story markdown found._" try: local_path = hf_hub_download( repo_id=REPO_ID, filename=path, repo_type=REPO_TYPE, token=HF_TOKEN, ) with open(local_path, "r", encoding="utf-8") as f: return f.read() except Exception as e: return f"_Failed to load markdown: {e}_" def make_story_card(story: dict) -> str: title = html.escape(story["title"]) subtitle = html.escape(story["folder"]) created = html.escape(format_ts(story["timestamp"])) story_md = download_markdown(story["markdown_path"]) story_md_html = gr.Markdown().postprocess(story_md) video_html = "" if story["video_path"]: video_url = build_resolve_url(story["video_path"]) video_html = f"""
""" gallery_html = "" if story["image_paths"]: imgs = [] for img_path in story["image_paths"]: img_url = build_resolve_url(img_path) img_name = html.escape(os.path.basename(img_path)) imgs.append( f""" {img_name} """ ) gallery_html = f"""
{''.join(imgs)}
""" dataset_badge = "" if story["csv_path"]: csv_url = build_resolve_url(story["csv_path"]) dataset_badge = ( f'dataset.csv' ) return f"""

{title}

{subtitle}
{video_html}

Story

{story_md_html}

Images

{gallery_html or '
No images found.
'}
""" def build_showcase_html(): stories = collect_stories() if not stories: return """

No stories found

Could not find any job_* folders in the repo.

""" cards = [make_story_card(story) for story in stories] return f"""
OhamLab Story Showcase

OhamLab Enabled

Latest generated stories, images, and videos from the dataset repo.

{len(stories)}
Stories
{''.join(cards)}
""" def refresh_showcase(): return build_showcase_html(), f"Last refreshed: {datetime.now().strftime('%d %b %Y, %I:%M:%S %p')}" # ========================================================= # CUSTOM CSS # ========================================================= CUSTOM_CSS = """ :root { --bg: #f5f7fb; --panel: #ffffff; --panel-2: #f9fafc; --border: rgba(0,0,0,0.08); --text: #1a1f36; --muted: #6b7280; --accent: #2563eb; --accent-2: #0ea5e9; --shadow: 0 8px 24px rgba(0,0,0,0.06); } .gradio-container { max-width: 1500px !important; } body, .gradio-container { background: radial-gradient(circle at top left, rgba(124,156,255,0.16), transparent 25%), radial-gradient(circle at top right, rgba(144,224,239,0.10), transparent 20%), linear-gradient(180deg, #0a0f1d, #0e1422 45%, #0b1020); } .page-wrap { padding: 6px 4px 30px 4px; } .hero { display: flex; align-items: center; justify-content: space-between; gap: 24px; background: white; border: 1px solid var(--border); border-radius: 24px; padding: 28px; margin-bottom: 24px; box-shadow: var(--shadow); } .eyebrow { color: var(--accent-2); font-size: 12px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; margin-bottom: 8px; } .hero h1 { margin: 0; color: var(--text); font-size: 36px; line-height: 1.1; } .hero p { margin: 10px 0 0 0; color: var(--muted); font-size: 16px; } .hero-stat { min-width: 130px; text-align: center; background: rgba(255,255,255,0.04); border: 1px solid var(--border); border-radius: 20px; padding: 18px; } .stat-num { color: var(--text); font-size: 34px; font-weight: 800; } .stat-label { color: var(--muted); font-size: 13px; } .stories-list { display: flex; flex-direction: column; gap: 22px; } .story-card { background: white; border: 1px solid var(--border); border-radius: 24px; padding: 22px; box-shadow: var(--shadow); backdrop-filter: blur(8px); } .story-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 18px; margin-bottom: 18px; } .story-header h2 { margin: 0; color: var(--text); font-size: 28px; line-height: 1.2; } .story-sub { color: var(--muted); font-size: 13px; margin-top: 6px; word-break: break-word; } .story-meta { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; } .meta-pill { display: inline-flex; align-items: center; justify-content: center; text-decoration: none; color: var(--text); background: rgba(124,156,255,0.12); border: 1px solid rgba(124,156,255,0.25); border-radius: 999px; padding: 8px 12px; font-size: 12px; font-weight: 600; } .story-video-wrap { margin-bottom: 20px; } .story-video { width: 100%; max-height: 580px; border-radius: 18px; background: #000; border: 1px solid var(--border); } .story-content { display: grid; grid-template-columns: 1.15fr 0.85fr; gap: 20px; } .story-text, .story-visuals { background: rgba(255,255,255,0.025); border: 1px solid var(--border); border-radius: 20px; padding: 18px; } .story-text h3, .story-visuals h3 { margin: 0 0 14px 0; color: var(--text); font-size: 18px; } .story-markdown { color: var(--text); line-height: 1.7; font-size: 15px; } .story-markdown p, .story-markdown li, .story-markdown h1, .story-markdown h2, .story-markdown h3, .story-markdown h4, .story-markdown h5, .story-markdown h6, .story-markdown blockquote { color: var(--text) !important; } .story-gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 12px; } .story-image-link { display: block; text-decoration: none; } .story-image { width: 100%; height: 180px; object-fit: cover; border-radius: 16px; border: 1px solid var(--border); transition: transform 0.18s ease, box-shadow 0.18s ease; background: rgba(255,255,255,0.02); } .story-image:hover { transform: translateY(-2px); box-shadow: 0 10px 22px rgba(0,0,0,0.22); } .empty-box, .empty-state { background: rgba(255,255,255,0.025); border: 1px dashed var(--border); border-radius: 18px; padding: 22px; color: var(--muted); } @media (max-width: 1100px) { .story-content { grid-template-columns: 1fr; } .story-header { flex-direction: column; } .story-meta { justify-content: flex-start; } .hero { flex-direction: column; align-items: flex-start; } } """ # ========================================================= # UI # ========================================================= with gr.Blocks( title="Story Factory Showcase", theme=gr.themes.Soft(), css=CUSTOM_CSS, ) as demo: showcase_state = gr.State(value=None) with gr.Row(): refresh_btn = gr.Button("Refresh Stories", variant="primary") refreshed_at = gr.Markdown("Loading...") showcase_html = gr.HTML() demo.load( fn=refresh_showcase, inputs=[], outputs=[showcase_html, refreshed_at], ) refresh_btn.click( fn=refresh_showcase, inputs=[], outputs=[showcase_html, refreshed_at], ) demo.launch()