Spaces:
Running
Running
| 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""" | |
| <div class="story-video-wrap"> | |
| <video class="story-video" controls preload="metadata"> | |
| <source src="{video_url}" type="video/mp4"> | |
| Your browser does not support the video tag. | |
| </video> | |
| </div> | |
| """ | |
| 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""" | |
| <a href="{img_url}" target="_blank" class="story-image-link"> | |
| <img src="{img_url}" alt="{img_name}" class="story-image" loading="lazy"> | |
| </a> | |
| """ | |
| ) | |
| gallery_html = f""" | |
| <div class="story-gallery"> | |
| {''.join(imgs)} | |
| </div> | |
| """ | |
| dataset_badge = "" | |
| if story["csv_path"]: | |
| csv_url = build_resolve_url(story["csv_path"]) | |
| dataset_badge = ( | |
| f'<a class="meta-pill" href="{csv_url}" target="_blank">dataset.csv</a>' | |
| ) | |
| return f""" | |
| <section class="story-card"> | |
| <div class="story-header"> | |
| <div> | |
| <h2>{title}</h2> | |
| <div class="story-sub">{subtitle}</div> | |
| </div> | |
| <div class="story-meta"> | |
| <span class="meta-pill">{created}</span> | |
| {dataset_badge} | |
| <span class="meta-pill">{len(story["image_paths"])} images</span> | |
| </div> | |
| </div> | |
| {video_html} | |
| <div class="story-content"> | |
| <div class="story-text"> | |
| <h3>Story</h3> | |
| <div class="story-markdown">{story_md_html}</div> | |
| </div> | |
| <div class="story-visuals"> | |
| <h3>Images</h3> | |
| {gallery_html or '<div class="empty-box">No images found.</div>'} | |
| </div> | |
| </div> | |
| </section> | |
| """ | |
| def build_showcase_html(): | |
| stories = collect_stories() | |
| if not stories: | |
| return """ | |
| <div class="empty-state"> | |
| <h2>No stories found</h2> | |
| <p>Could not find any <code>job_*</code> folders in the repo.</p> | |
| </div> | |
| """ | |
| cards = [make_story_card(story) for story in stories] | |
| return f""" | |
| <div class="page-wrap"> | |
| <div class="hero"> | |
| <div> | |
| <div class="eyebrow">OhamLab Story Showcase</div> | |
| <h1>OhamLab Enabled</h1> | |
| <p>Latest generated stories, images, and videos from the dataset repo.</p> | |
| </div> | |
| <div class="hero-stat"> | |
| <div class="stat-num">{len(stories)}</div> | |
| <div class="stat-label">Stories</div> | |
| </div> | |
| </div> | |
| <div class="stories-list"> | |
| {''.join(cards)} | |
| </div> | |
| </div> | |
| """ | |
| 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() |