rahul7star commited on
Commit
6289469
·
verified ·
1 Parent(s): 8b320ee

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +551 -0
app.py ADDED
@@ -0,0 +1,551 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import html
4
+ from datetime import datetime
5
+
6
+ import gradio as gr
7
+ from huggingface_hub import HfApi, hf_hub_download, hf_hub_url
8
+
9
+ # =========================================================
10
+ # CONFIG
11
+ # =========================================================
12
+ REPO_ID = "rahul7star/ltx-story-factory"
13
+ REPO_TYPE = "dataset"
14
+ HF_TOKEN = os.getenv("HF_TOKEN") # optional if repo is public
15
+
16
+ api = HfApi(token=HF_TOKEN)
17
+
18
+
19
+ # =========================================================
20
+ # HELPERS
21
+ # =========================================================
22
+ def format_ts(ts: int) -> str:
23
+ try:
24
+ return datetime.fromtimestamp(ts).strftime("%d %b %Y, %I:%M %p")
25
+ except Exception:
26
+ return "Unknown date"
27
+
28
+
29
+ def natural_story_title(folder_name: str) -> str:
30
+ # job_1775798146_Sunbeam_s_Secret -> Sunbeam s Secret
31
+ m = re.match(r"job_(\d+)_(.+)$", folder_name)
32
+ if not m:
33
+ return folder_name.replace("_", " ")
34
+ raw = m.group(2).replace("_", " ").strip()
35
+ return raw
36
+
37
+
38
+ def extract_job_ts(folder_name: str) -> int:
39
+ m = re.match(r"job_(\d+)_", folder_name)
40
+ return int(m.group(1)) if m else 0
41
+
42
+
43
+ def build_resolve_url(path: str) -> str:
44
+ return hf_hub_url(
45
+ repo_id=REPO_ID,
46
+ filename=path,
47
+ repo_type=REPO_TYPE,
48
+ )
49
+
50
+
51
+ def list_repo_files():
52
+ """
53
+ Return all file paths from the dataset repo.
54
+ """
55
+ files = []
56
+
57
+ # list_repo_tree is the recommended API for tree listing on HF Hub.
58
+ tree = api.list_repo_tree(
59
+ repo_id=REPO_ID,
60
+ repo_type=REPO_TYPE,
61
+ recursive=True,
62
+ expand=True,
63
+ )
64
+
65
+ for item in tree:
66
+ path = getattr(item, "path", None)
67
+ if path and not path.endswith("/"):
68
+ files.append(path)
69
+
70
+ return files
71
+
72
+
73
+ def collect_stories():
74
+ """
75
+ Build a story index from repo contents.
76
+
77
+ Expected folder structure:
78
+ job_1775798146_Sunbeam_s_Secret/
79
+ Sunbeam_s_Secret.md
80
+ full_video.mp4
81
+ dataset.csv
82
+ images/img_1.webp
83
+ images/img_2.webp
84
+ ...
85
+ """
86
+ repo_files = list_repo_files()
87
+ stories = {}
88
+
89
+ for path in repo_files:
90
+ parts = path.split("/")
91
+ if len(parts) < 2:
92
+ continue
93
+
94
+ folder = parts[0]
95
+ if not folder.startswith("job_"):
96
+ continue
97
+
98
+ if folder not in stories:
99
+ stories[folder] = {
100
+ "folder": folder,
101
+ "title": natural_story_title(folder),
102
+ "timestamp": extract_job_ts(folder),
103
+ "markdown_path": None,
104
+ "video_path": None,
105
+ "csv_path": None,
106
+ "image_paths": [],
107
+ }
108
+
109
+ lower = path.lower()
110
+
111
+ if lower.endswith(".md"):
112
+ stories[folder]["markdown_path"] = path
113
+ elif lower.endswith("full_video.mp4"):
114
+ stories[folder]["video_path"] = path
115
+ elif lower.endswith("dataset.csv"):
116
+ stories[folder]["csv_path"] = path
117
+ elif "/images/" in lower and lower.endswith((".png", ".jpg", ".jpeg", ".webp")):
118
+ stories[folder]["image_paths"].append(path)
119
+
120
+ # Sort images inside each story
121
+ for _, story in stories.items():
122
+ story["image_paths"] = sorted(
123
+ story["image_paths"],
124
+ key=lambda p: [
125
+ int(x) if x.isdigit() else x.lower()
126
+ for x in re.split(r"(\d+)", p)
127
+ ]
128
+ )
129
+
130
+ # Newest first
131
+ sorted_stories = sorted(
132
+ stories.values(),
133
+ key=lambda x: x["timestamp"],
134
+ reverse=True,
135
+ )
136
+
137
+ return sorted_stories
138
+
139
+
140
+ def download_markdown(path: str) -> str:
141
+ if not path:
142
+ return "_No story markdown found._"
143
+
144
+ try:
145
+ local_path = hf_hub_download(
146
+ repo_id=REPO_ID,
147
+ filename=path,
148
+ repo_type=REPO_TYPE,
149
+ token=HF_TOKEN,
150
+ )
151
+ with open(local_path, "r", encoding="utf-8") as f:
152
+ return f.read()
153
+ except Exception as e:
154
+ return f"_Failed to load markdown: {e}_"
155
+
156
+
157
+ def make_story_card(story: dict) -> str:
158
+ title = html.escape(story["title"])
159
+ subtitle = html.escape(story["folder"])
160
+ created = html.escape(format_ts(story["timestamp"]))
161
+
162
+ story_md = download_markdown(story["markdown_path"])
163
+ story_md_html = gr.Markdown().postprocess(story_md)
164
+
165
+ video_html = ""
166
+ if story["video_path"]:
167
+ video_url = build_resolve_url(story["video_path"])
168
+ video_html = f"""
169
+ <div class="story-video-wrap">
170
+ <video class="story-video" controls preload="metadata">
171
+ <source src="{video_url}" type="video/mp4">
172
+ Your browser does not support the video tag.
173
+ </video>
174
+ </div>
175
+ """
176
+
177
+ gallery_html = ""
178
+ if story["image_paths"]:
179
+ imgs = []
180
+ for img_path in story["image_paths"]:
181
+ img_url = build_resolve_url(img_path)
182
+ img_name = html.escape(os.path.basename(img_path))
183
+ imgs.append(
184
+ f"""
185
+ <a href="{img_url}" target="_blank" class="story-image-link">
186
+ <img src="{img_url}" alt="{img_name}" class="story-image" loading="lazy">
187
+ </a>
188
+ """
189
+ )
190
+ gallery_html = f"""
191
+ <div class="story-gallery">
192
+ {''.join(imgs)}
193
+ </div>
194
+ """
195
+
196
+ dataset_badge = ""
197
+ if story["csv_path"]:
198
+ csv_url = build_resolve_url(story["csv_path"])
199
+ dataset_badge = (
200
+ f'<a class="meta-pill" href="{csv_url}" target="_blank">dataset.csv</a>'
201
+ )
202
+
203
+ return f"""
204
+ <section class="story-card">
205
+ <div class="story-header">
206
+ <div>
207
+ <h2>{title}</h2>
208
+ <div class="story-sub">{subtitle}</div>
209
+ </div>
210
+ <div class="story-meta">
211
+ <span class="meta-pill">{created}</span>
212
+ {dataset_badge}
213
+ <span class="meta-pill">{len(story["image_paths"])} images</span>
214
+ </div>
215
+ </div>
216
+
217
+ {video_html}
218
+
219
+ <div class="story-content">
220
+ <div class="story-text">
221
+ <h3>Story</h3>
222
+ <div class="story-markdown">{story_md_html}</div>
223
+ </div>
224
+
225
+ <div class="story-visuals">
226
+ <h3>Images</h3>
227
+ {gallery_html or '<div class="empty-box">No images found.</div>'}
228
+ </div>
229
+ </div>
230
+ </section>
231
+ """
232
+
233
+
234
+ def build_showcase_html():
235
+ stories = collect_stories()
236
+
237
+ if not stories:
238
+ return """
239
+ <div class="empty-state">
240
+ <h2>No stories found</h2>
241
+ <p>Could not find any <code>job_*</code> folders in the repo.</p>
242
+ </div>
243
+ """
244
+
245
+ cards = [make_story_card(story) for story in stories]
246
+
247
+ return f"""
248
+ <div class="page-wrap">
249
+ <div class="hero">
250
+ <div>
251
+ <div class="eyebrow">Story Showcase</div>
252
+ <h1>LTX Story Factory</h1>
253
+ <p>Latest generated stories, images, and videos from the dataset repo.</p>
254
+ </div>
255
+ <div class="hero-stat">
256
+ <div class="stat-num">{len(stories)}</div>
257
+ <div class="stat-label">Stories</div>
258
+ </div>
259
+ </div>
260
+
261
+ <div class="stories-list">
262
+ {''.join(cards)}
263
+ </div>
264
+ </div>
265
+ """
266
+
267
+
268
+ def refresh_showcase():
269
+ return build_showcase_html(), f"Last refreshed: {datetime.now().strftime('%d %b %Y, %I:%M:%S %p')}"
270
+
271
+
272
+ # =========================================================
273
+ # CUSTOM CSS
274
+ # =========================================================
275
+ CUSTOM_CSS = """
276
+ :root {
277
+ --bg: #0b1020;
278
+ --panel: #121a2b;
279
+ --panel-2: #182339;
280
+ --border: rgba(255,255,255,0.08);
281
+ --text: #eef2ff;
282
+ --muted: #a9b4d0;
283
+ --accent: #7c9cff;
284
+ --accent-2: #90e0ef;
285
+ --shadow: 0 10px 30px rgba(0,0,0,0.25);
286
+ }
287
+
288
+ .gradio-container {
289
+ max-width: 1500px !important;
290
+ }
291
+
292
+ body, .gradio-container {
293
+ background:
294
+ radial-gradient(circle at top left, rgba(124,156,255,0.16), transparent 25%),
295
+ radial-gradient(circle at top right, rgba(144,224,239,0.10), transparent 20%),
296
+ linear-gradient(180deg, #0a0f1d, #0e1422 45%, #0b1020);
297
+ }
298
+
299
+ .page-wrap {
300
+ padding: 6px 4px 30px 4px;
301
+ }
302
+
303
+ .hero {
304
+ display: flex;
305
+ align-items: center;
306
+ justify-content: space-between;
307
+ gap: 24px;
308
+ background: linear-gradient(135deg, rgba(124,156,255,0.18), rgba(144,224,239,0.10));
309
+ border: 1px solid var(--border);
310
+ border-radius: 24px;
311
+ padding: 28px;
312
+ margin-bottom: 24px;
313
+ box-shadow: var(--shadow);
314
+ }
315
+
316
+ .eyebrow {
317
+ color: var(--accent-2);
318
+ font-size: 12px;
319
+ font-weight: 700;
320
+ letter-spacing: 0.12em;
321
+ text-transform: uppercase;
322
+ margin-bottom: 8px;
323
+ }
324
+
325
+ .hero h1 {
326
+ margin: 0;
327
+ color: var(--text);
328
+ font-size: 36px;
329
+ line-height: 1.1;
330
+ }
331
+
332
+ .hero p {
333
+ margin: 10px 0 0 0;
334
+ color: var(--muted);
335
+ font-size: 16px;
336
+ }
337
+
338
+ .hero-stat {
339
+ min-width: 130px;
340
+ text-align: center;
341
+ background: rgba(255,255,255,0.04);
342
+ border: 1px solid var(--border);
343
+ border-radius: 20px;
344
+ padding: 18px;
345
+ }
346
+
347
+ .stat-num {
348
+ color: var(--text);
349
+ font-size: 34px;
350
+ font-weight: 800;
351
+ }
352
+
353
+ .stat-label {
354
+ color: var(--muted);
355
+ font-size: 13px;
356
+ }
357
+
358
+ .stories-list {
359
+ display: flex;
360
+ flex-direction: column;
361
+ gap: 22px;
362
+ }
363
+
364
+ .story-card {
365
+ background: linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.015));
366
+ border: 1px solid var(--border);
367
+ border-radius: 24px;
368
+ padding: 22px;
369
+ box-shadow: var(--shadow);
370
+ backdrop-filter: blur(8px);
371
+ }
372
+
373
+ .story-header {
374
+ display: flex;
375
+ justify-content: space-between;
376
+ align-items: flex-start;
377
+ gap: 18px;
378
+ margin-bottom: 18px;
379
+ }
380
+
381
+ .story-header h2 {
382
+ margin: 0;
383
+ color: var(--text);
384
+ font-size: 28px;
385
+ line-height: 1.2;
386
+ }
387
+
388
+ .story-sub {
389
+ color: var(--muted);
390
+ font-size: 13px;
391
+ margin-top: 6px;
392
+ word-break: break-word;
393
+ }
394
+
395
+ .story-meta {
396
+ display: flex;
397
+ flex-wrap: wrap;
398
+ gap: 8px;
399
+ justify-content: flex-end;
400
+ }
401
+
402
+ .meta-pill {
403
+ display: inline-flex;
404
+ align-items: center;
405
+ justify-content: center;
406
+ text-decoration: none;
407
+ color: var(--text);
408
+ background: rgba(124,156,255,0.12);
409
+ border: 1px solid rgba(124,156,255,0.25);
410
+ border-radius: 999px;
411
+ padding: 8px 12px;
412
+ font-size: 12px;
413
+ font-weight: 600;
414
+ }
415
+
416
+ .story-video-wrap {
417
+ margin-bottom: 20px;
418
+ }
419
+
420
+ .story-video {
421
+ width: 100%;
422
+ max-height: 580px;
423
+ border-radius: 18px;
424
+ background: #000;
425
+ border: 1px solid var(--border);
426
+ }
427
+
428
+ .story-content {
429
+ display: grid;
430
+ grid-template-columns: 1.15fr 0.85fr;
431
+ gap: 20px;
432
+ }
433
+
434
+ .story-text,
435
+ .story-visuals {
436
+ background: rgba(255,255,255,0.025);
437
+ border: 1px solid var(--border);
438
+ border-radius: 20px;
439
+ padding: 18px;
440
+ }
441
+
442
+ .story-text h3,
443
+ .story-visuals h3 {
444
+ margin: 0 0 14px 0;
445
+ color: var(--text);
446
+ font-size: 18px;
447
+ }
448
+
449
+ .story-markdown {
450
+ color: var(--text);
451
+ line-height: 1.7;
452
+ font-size: 15px;
453
+ }
454
+
455
+ .story-markdown p,
456
+ .story-markdown li,
457
+ .story-markdown h1,
458
+ .story-markdown h2,
459
+ .story-markdown h3,
460
+ .story-markdown h4,
461
+ .story-markdown h5,
462
+ .story-markdown h6,
463
+ .story-markdown blockquote {
464
+ color: var(--text) !important;
465
+ }
466
+
467
+ .story-gallery {
468
+ display: grid;
469
+ grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
470
+ gap: 12px;
471
+ }
472
+
473
+ .story-image-link {
474
+ display: block;
475
+ text-decoration: none;
476
+ }
477
+
478
+ .story-image {
479
+ width: 100%;
480
+ height: 180px;
481
+ object-fit: cover;
482
+ border-radius: 16px;
483
+ border: 1px solid var(--border);
484
+ transition: transform 0.18s ease, box-shadow 0.18s ease;
485
+ background: rgba(255,255,255,0.02);
486
+ }
487
+
488
+ .story-image:hover {
489
+ transform: translateY(-2px);
490
+ box-shadow: 0 10px 22px rgba(0,0,0,0.22);
491
+ }
492
+
493
+ .empty-box,
494
+ .empty-state {
495
+ background: rgba(255,255,255,0.025);
496
+ border: 1px dashed var(--border);
497
+ border-radius: 18px;
498
+ padding: 22px;
499
+ color: var(--muted);
500
+ }
501
+
502
+ @media (max-width: 1100px) {
503
+ .story-content {
504
+ grid-template-columns: 1fr;
505
+ }
506
+
507
+ .story-header {
508
+ flex-direction: column;
509
+ }
510
+
511
+ .story-meta {
512
+ justify-content: flex-start;
513
+ }
514
+
515
+ .hero {
516
+ flex-direction: column;
517
+ align-items: flex-start;
518
+ }
519
+ }
520
+ """
521
+
522
+
523
+ # =========================================================
524
+ # UI
525
+ # =========================================================
526
+ with gr.Blocks(
527
+ title="LTX Story Factory Showcase",
528
+ theme=gr.themes.Soft(),
529
+ css=CUSTOM_CSS,
530
+ ) as demo:
531
+ showcase_state = gr.State(value=None)
532
+
533
+ with gr.Row():
534
+ refresh_btn = gr.Button("Refresh Stories", variant="primary")
535
+ refreshed_at = gr.Markdown("Loading...")
536
+
537
+ showcase_html = gr.HTML()
538
+
539
+ demo.load(
540
+ fn=refresh_showcase,
541
+ inputs=[],
542
+ outputs=[showcase_html, refreshed_at],
543
+ )
544
+
545
+ refresh_btn.click(
546
+ fn=refresh_showcase,
547
+ inputs=[],
548
+ outputs=[showcase_html, refreshed_at],
549
+ )
550
+
551
+ demo.launch()