saltudio commited on
Commit
91939bc
·
verified ·
1 Parent(s): d87ec12

Upload 8 files

Browse files
README.md ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: VidGen Frontend
3
+ emoji: 🎬
4
+ colorFrom: purple
5
+ colorTo: pink
6
+ sdk: streamlit
7
+ sdk_version: "1.29.0"
8
+ app_file: app.py
9
+ pinned: false
10
+ ---
11
+
12
+ # VidGen Frontend
13
+
14
+ Public frontend interface for Google Veo video generation.
15
+
16
+ ## 🚀 Features
17
+
18
+ - Text-to-video and image-to-video generation
19
+ - Real-time job monitoring with progress tracking
20
+ - Batch processing support
21
+ - JSON configuration mode
22
+ - Responsive design
23
+
24
+ ## 🔧 Configuration
25
+
26
+ The frontend connects to a private backend API for video generation processing.
27
+
28
+
29
+ ## 🎯 Usage
30
+
31
+ 1. Enter your Google API key
32
+ 2. Configure generation settings (model, resolution, aspect ratio)
33
+ 3. Enter prompts or upload images for image-to-video mode
34
+ 4. Click "Generate Video" to start processing
35
+ 5. Monitor progress and download results when completed
36
+
37
+ ## 📋 Supported Models
38
+
39
+ - **veo-3.0-fast-generate-001** ✅ (1080p support, 16:9 only)
40
+ - **veo-3.0-generate-001** ✅ (1080p support, 16:9 only)
41
+ - **veo-2.0-generate-001** ⚠️ (720p max resolution)
42
+
43
+ ## 🔒 Security
44
+
45
+ - API keys are processed securely through the private backend
46
+ - No sensitive data stored locally
47
+ - All video processing happens on secure backend servers
48
+
49
+ ---
50
+
51
+ *Powered by Google Veo • Backend API: Private Space*
app.py ADDED
@@ -0,0 +1,329 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # fe-vidgen/app.py
2
+ import os, sys, time, json, pathlib, re, base64, mimetypes, random
3
+ import streamlit as st
4
+ from typing import List, Tuple
5
+
6
+ # ---------- ENV ----------
7
+ os.environ.setdefault("HOME", "/home/user")
8
+ os.environ.setdefault("STREAMLIT_SERVER_ENABLE_XSRF_PROTECTION", "false")
9
+ os.environ.setdefault("STREAMLIT_SERVER_ENABLE_CORS", "false")
10
+ os.environ.setdefault("STREAMLIT_BROWSER_GATHER_USAGE_STATS", "false")
11
+ os.environ.setdefault("STREAMLIT_SERVER_MAX_UPLOAD_SIZE", "1024")
12
+
13
+ st.set_page_config(page_title="VidGen — Google Veo", page_icon="🎬", layout="wide")
14
+
15
+ # ---------- INIT STATE ----------
16
+ def _init_state():
17
+ defaults = {
18
+ "backend": None, "api_key_val": "",
19
+ "jobs": [], "job_idx": 0, "running": False,
20
+ "total": 0, "overall_progress_value": 0.0,
21
+ "images_info": [], "image_order_mode": "Filename (Numeric 1–9)",
22
+ "prompt_main": "",
23
+ "last_model_ui": None, "last_mode_backend": None,
24
+ "last_aspect": None, "last_resolution": None,
25
+ "last_negative": None, "last_person": None,
26
+ "last_audio_on": True, "worker_delay": 1.0,
27
+ }
28
+ for k,v in defaults.items():
29
+ if k not in st.session_state:
30
+ st.session_state[k] = v
31
+ _init_state()
32
+
33
+ # ---------- BACKEND ----------
34
+ @st.cache_resource(ttl=3600, show_spinner=False)
35
+ def load_backend():
36
+ from huggingface_hub import snapshot_download
37
+ hf_token = os.getenv("HF_TOKEN")
38
+ if not hf_token:
39
+ st.error("❌ HF_TOKEN not set in Space Secrets")
40
+ st.stop()
41
+ LOCAL_DIR = snapshot_download(
42
+ repo_id="saltudio/be-vidgen",
43
+ repo_type="space",
44
+ token=hf_token,
45
+ cache_dir="/tmp/hf_vidgen_cache",
46
+ allow_patterns=["src/**"],
47
+ ignore_patterns=["*.md","*.txt","app.py","requirements.txt"],
48
+ )
49
+ SRC_DIR = os.path.join(LOCAL_DIR,"src")
50
+ if SRC_DIR not in sys.path: sys.path.insert(0,SRC_DIR)
51
+ from veo_backend import generate_one_with_ui, get_model_capabilities, MODELS_UI, ASPECTS
52
+ return {"generate_one_with_ui":generate_one_with_ui,
53
+ "get_model_capabilities":get_model_capabilities,
54
+ "MODELS_UI":MODELS_UI, "ASPECTS":ASPECTS}
55
+
56
+ backend = st.session_state.backend or load_backend()
57
+ st.session_state.backend = backend
58
+ generate_one_with_ui = backend["generate_one_with_ui"]
59
+ get_model_capabilities = backend["get_model_capabilities"]
60
+ MODELS_UI = backend["MODELS_UI"]
61
+ ASPECTS = backend["ASPECTS"]
62
+
63
+ # ---------- STYLE ----------
64
+ st.markdown("""
65
+ <style>
66
+ #MainMenu, header, footer {visibility:hidden;}
67
+ .job-card{border:1px solid #2b2f36;background:#12151b;border-radius:12px;padding:12px;margin-bottom:14px;}
68
+ .job-head{font-weight:600;color:#cfd8e3;margin-bottom:6px;}
69
+ .badges{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px;}
70
+ .badge{padding:.2rem .5rem;border-radius:999px;background:#1f2430;color:#9fb0c3;border:1px solid #2b2f36;font-size:.75rem;}
71
+ .floating-donations{position:fixed;right:20px;top:50%;transform:translateY(-50%);z-index:1000;display:flex;flex-direction:column;gap:15px;}
72
+ .floating-btn{width:60px;height:60px;border-radius:50%;display:flex;align-items:center;justify-content:center;text-decoration:none;font-weight:bold;font-size:14px;box-shadow:0 4px 20px rgba(0,0,0,0.3);transition:all .3s ease;}
73
+ .floating-btn:hover{transform:scale(1.1);box-shadow:0 6px 30px rgba(0,0,0,0.4);}
74
+ .kofi-floating{background:linear-gradient(135deg,#ff5f5f,#ff3838);color:#fff;}
75
+ .saweria-floating{background:linear-gradient(135deg,#ffbd44,#f39c12);color:#2c3e50;}
76
+ @media(max-width:768px){.floating-donations{display:none;}}
77
+ </style>
78
+ """,unsafe_allow_html=True)
79
+ st.markdown("""
80
+ <div class="floating-donations">
81
+ <a href="https://ko-fi.com/saltudio" target="_blank" class="floating-btn kofi-floating">☕</a>
82
+ <a href="https://saweria.co/saltudio" target="_blank" class="floating-btn saweria-floating">💝</a>
83
+ </div>
84
+ """,unsafe_allow_html=True)
85
+
86
+ # ---------- CONSTANT NEGATIVE PROMPT ----------
87
+ NEG_PROMPT_DEFAULT = (
88
+ "no text overlays, no subtitles, no watermarks, no copyright logos, "
89
+ "no UI elements, no cartoon or comic effects, no unrealistic body proportions, "
90
+ "no extra limbs, no deformed anatomy, no blurry or out-of-focus areas, "
91
+ "no distorted hands or faces, no glitches, no pixelation, "
92
+ "no low resolution artifacts, no compression noise"
93
+ )
94
+
95
+ # ---------- HELPERS ----------
96
+ def file_to_b64(uploaded_file):
97
+ data = uploaded_file.read(); uploaded_file.seek(0)
98
+ b64 = base64.b64encode(data).decode("ascii")
99
+ mime = mimetypes.guess_type(uploaded_file.name)[0] or "image/jpeg"
100
+ return b64, mime, uploaded_file.name
101
+
102
+ def _extract_number_for_sort(name: str) -> int:
103
+ m = re.search(r"(\d+)", name)
104
+ return int(m.group(1)) if m else 10**9
105
+
106
+ def normalize_b64_front(s: str) -> str:
107
+ if not s: return s
108
+ ss = s.strip()
109
+ if "," in ss and ss.lower().startswith("data:"):
110
+ ss = ss.split(",",1)[1]
111
+ ss = "".join(ss.split())
112
+ rem = len(ss) % 4
113
+ if rem: ss += "=" * (4 - rem)
114
+ return ss
115
+
116
+ def parse_batch_prompts(txt: str, json_mode: bool):
117
+ txt = (txt or "").strip()
118
+ if not txt: return []
119
+ if not json_mode:
120
+ return [ln.strip() for ln in txt.splitlines() if ln.strip()]
121
+ try:
122
+ data = json.loads(txt)
123
+ except Exception:
124
+ return []
125
+ if isinstance(data, dict): data = [data]
126
+ out = []
127
+ for it in data:
128
+ if isinstance(it, str): out.append(it.strip())
129
+ elif isinstance(it, dict): out.append(it.get("prompt") or "")
130
+ return [s for s in out if s]
131
+
132
+ def enhance_prompt_from_image(filename: str, aspect: str, resolution: str) -> str:
133
+ """Prompt singkat, natural, variatif (motion, style, camera, shot, lens, ambiance)."""
134
+ stem = pathlib.Path(filename).stem.replace("_"," ").replace("-"," ")
135
+
136
+ motions = ["gentle breeze", "subtle breathing", "calm parallax shift", "slow light flicker", "soft camera drift"]
137
+ styles = ["cinematic realistic", "dreamy pastel film", "moody noir", "bright daylight", "golden hour glow"]
138
+ cameras = ["eye-level", "smooth dolly forward", "slow pan", "aerial view", "low angle"]
139
+ shots = ["wide shot", "medium shot", "close-up", "over-the-shoulder", "establishing shot"]
140
+ lenses = ["24mm wide lens", "35mm lifestyle lens", "50mm portrait lens", "85mm telephoto lens", "macro lens"]
141
+ ambiances= ["warm tones", "cool night tones", "soft pastel colors", "high contrast palette", "natural balanced light"]
142
+
143
+ motion = random.choice(motions)
144
+ style = random.choice(styles)
145
+ camera = random.choice(cameras)
146
+ shot = random.choice(shots)
147
+ lens = random.choice(lenses)
148
+ ambiance = random.choice(ambiances)
149
+
150
+ # Prompt langsung siap generate
151
+ return f"{stem}, {motion}, {style}, {camera}, {shot}, using {lens}, {ambiance}. Aspect {aspect}, Resolution {resolution}."
152
+
153
+ def ensure_api_key():
154
+ key = st.session_state.api_key_val
155
+ if not key:
156
+ st.error("❌ Provide Google API key in sidebar")
157
+ return None
158
+ os.environ["GOOGLE_API_KEY"] = key
159
+ return key
160
+
161
+ # ---------- SIDEBAR ----------
162
+ with st.sidebar:
163
+ st.header("🔑 API Key")
164
+ api_key = st.text_input("Google API Key", type="password", value=st.session_state.api_key_val)
165
+ if api_key: st.session_state.api_key_val = api_key
166
+
167
+ st.header("Settings")
168
+ model_ui = st.selectbox("Model", MODELS_UI)
169
+ mode_ui = st.radio("Mode", ["Text → Video","Image → Video"])
170
+
171
+ images_info: List[Tuple[str,str,str]] = []
172
+ if mode_ui.startswith("Image"):
173
+ uploaded = st.file_uploader("Upload images", type=["png","jpg","jpeg"], accept_multiple_files=True)
174
+ sort_mode = st.selectbox("Sort images by", ["Filename (Numeric 1–9)", "Upload order"], index=0)
175
+ if uploaded:
176
+ if sort_mode == "Filename (Numeric 1–9)":
177
+ files = sorted(uploaded, key=lambda f: (_extract_number_for_sort(f.name), f.name.lower()))
178
+ else:
179
+ files = list(uploaded)
180
+
181
+ st.caption("Order:")
182
+ for i, f in enumerate(files, 1):
183
+ st.write(f"{i}. {f.name}")
184
+
185
+ images_info = [file_to_b64(f) for f in files]
186
+ st.session_state.images_info = images_info
187
+
188
+ # Auto-fill prompt (variatif) langsung di textarea
189
+ st.session_state.prompt_main = "\n".join([
190
+ enhance_prompt_from_image(fn, "16:9", "1080p") for _,_,fn in images_info
191
+ ])
192
+
193
+ # Resolution & aspect
194
+ want_resolution = st.selectbox("Resolution", ["720p","1080p"], index=1)
195
+ aspect = st.selectbox("Aspect", ASPECTS)
196
+
197
+ @st.cache_data
198
+ def get_caps(model): return get_model_capabilities(model)
199
+ caps = get_caps(model_ui)
200
+ st.caption(f"Max: {caps.get('max_resolution','unknown')}")
201
+ if want_resolution == "1080p" and aspect != "16:9":
202
+ st.warning("⚠️ 1080p → 16:9 only")
203
+ elif want_resolution == "1080p" and not caps.get("supports_1080p", True):
204
+ st.warning("⚠️ Model may fallback to 720p")
205
+
206
+ # Negative prompt (konstan, masih bisa diedit)
207
+ negative_prompt = st.text_area(
208
+ "Negative Prompt",
209
+ value=NEG_PROMPT_DEFAULT,
210
+ height=120,
211
+ help="Constant default applied unless you edit."
212
+ )
213
+ person_generation = st.selectbox("Person Gen", ["auto","allow_adult","allow_all"])
214
+ audio_on = st.checkbox("Enable Audio", True)
215
+ num_results = st.number_input("Results per prompt", 1, 3, 1)
216
+ worker_delay = st.number_input("Worker Delay (s)", 0.0, 10.0, st.session_state.worker_delay, 0.5)
217
+ st.session_state.worker_delay = worker_delay
218
+
219
+ c1, c2 = st.columns(2)
220
+ run_btn = c1.button("🎬 Generate", type="primary", use_container_width=True)
221
+ clr_btn = c2.button("🗑️ Clear", use_container_width=True)
222
+
223
+ # ---------- MAIN ----------
224
+ st.title("🎬 VidGen — Google Veo")
225
+ prompt_text = st.text_area("Prompts", value=st.session_state.prompt_main, height=200)
226
+ json_mode = st.checkbox("JSON Mode")
227
+ batch_prompts = parse_batch_prompts(prompt_text, json_mode)
228
+ st.caption(f"🧾 Parsed prompts: {len(batch_prompts)}")
229
+
230
+ def clear_state():
231
+ st.session_state.jobs=[]; st.session_state.job_idx=0; st.session_state.running=False
232
+ st.session_state.total=0; st.session_state.overall_progress_value=0.0
233
+
234
+ if clr_btn:
235
+ clear_state(); st.session_state.prompt_main=""; st.rerun()
236
+
237
+ # ---------- BUILD & RUN ----------
238
+ if run_btn and not st.session_state.running:
239
+ key = ensure_api_key()
240
+ if key:
241
+ jobs=[]; have_images = bool(st.session_state.images_info)
242
+ if have_images:
243
+ images = st.session_state.images_info
244
+ # Jika textarea kosong, isi ulang sesuai setting saat ini
245
+ prompts = batch_prompts or [enhance_prompt_from_image(fn, aspect, want_resolution) for _,_,fn in images]
246
+ total_pairs = max(len(prompts), len(images))
247
+ for i in range(total_pairs):
248
+ p = prompts[i % len(prompts)]
249
+ b64v, mime, _ = images[i % len(images)]
250
+ jobs.append({"prompt": p, "img_b64": b64v, "img_mime": mime})
251
+ else:
252
+ jobs = [{"prompt": p, "img_b64": None, "img_mime": None} for p in (batch_prompts or [])]
253
+
254
+ expanded = [j.copy() for j in jobs for _ in range(int(num_results))]
255
+ if not expanded:
256
+ st.error("❌ No jobs to run."); st.stop()
257
+
258
+ st.session_state.jobs = expanded
259
+ st.session_state.total = len(expanded)
260
+ st.session_state.job_idx = 0
261
+ st.session_state.running = True
262
+
263
+ # snapshot settings
264
+ st.session_state.last_model_ui = model_ui
265
+ st.session_state.last_mode_backend = "image" if have_images else "text"
266
+ st.session_state.last_aspect = aspect
267
+ st.session_state.last_resolution = want_resolution
268
+ st.session_state.last_negative = negative_prompt
269
+ st.session_state.last_person = person_generation
270
+ st.session_state.last_audio_on = audio_on
271
+ st.session_state.prompt_main = prompt_text
272
+
273
+ st.rerun()
274
+
275
+ # ---------- RUNNER ----------
276
+ overall = st.empty(); cols = st.columns(2)
277
+ if st.session_state.running and st.session_state.total > 0:
278
+ idx = st.session_state.job_idx; total = st.session_state.total
279
+ overall.progress(st.session_state.overall_progress_value)
280
+ if idx > 0 and st.session_state.worker_delay > 0:
281
+ time.sleep(float(st.session_state.worker_delay))
282
+
283
+ col = cols[idx % 2]
284
+ with col:
285
+ neg_text = (st.session_state.last_negative or "").strip()
286
+ neg_badge = f'<span class="badge">Neg: {neg_text}</span>' if neg_text else ""
287
+ st.markdown(
288
+ f"""
289
+ <div class='job-card'>
290
+ <div class='job-head'>Job {idx+1}/{total}</div>
291
+ <div class='badges'>
292
+ <span class='badge'>Mode: {st.session_state.last_mode_backend}</span>
293
+ <span class='badge'>Aspect: {st.session_state.last_aspect}</span>
294
+ <span class='badge'>Res: {st.session_state.last_resolution}</span>
295
+ {neg_badge}
296
+ </div>
297
+ """,
298
+ unsafe_allow_html=True
299
+ )
300
+ box = st.container()
301
+ try:
302
+ job = st.session_state.jobs[idx]
303
+ generate_one_with_ui(
304
+ api_key=st.session_state.api_key_val,
305
+ model_ui=st.session_state.last_model_ui,
306
+ mode=st.session_state.last_mode_backend,
307
+ prompt=job["prompt"],
308
+ negative_prompt=st.session_state.last_negative,
309
+ aspect=st.session_state.last_aspect,
310
+ want_resolution=st.session_state.last_resolution,
311
+ person_generation=None if st.session_state.last_person=="auto" else st.session_state.last_person,
312
+ img_b64=normalize_b64_front(job["img_b64"]),
313
+ img_mime=job["img_mime"],
314
+ enable_audio=st.session_state.last_audio_on,
315
+ ui_box=box
316
+ )
317
+ except Exception as err:
318
+ box.error(f"❌ {err}")
319
+ st.markdown("</div>", unsafe_allow_html=True)
320
+
321
+ st.session_state.job_idx += 1
322
+ st.session_state.overall_progress_value = st.session_state.job_idx / total
323
+ overall.progress(st.session_state.overall_progress_value)
324
+
325
+ if st.session_state.job_idx < total:
326
+ st.rerun()
327
+ else:
328
+ st.session_state.running = False
329
+ st.success("✅ Batch complete")
components/__init__.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Components package for VidGen
3
+ """
4
+
5
+ from .donation_widget import (
6
+ render_donation_widget,
7
+ render_donation_html,
8
+ render_donation_scripts,
9
+ render_sidebar_support,
10
+ render_footer_support
11
+ )
12
+
13
+ __all__ = [
14
+ 'render_donation_widget',
15
+ 'render_donation_html',
16
+ 'render_donation_scripts',
17
+ 'render_sidebar_support',
18
+ 'render_footer_support'
19
+ ]
components/donation_widget.py ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Floating Donation Widget Component
3
+ Separated for better performance and maintainability
4
+ """
5
+
6
+ import streamlit as st
7
+
8
+ def render_donation_widget():
9
+ """Render the floating donation widget with optimized CSS and JS"""
10
+
11
+ st.markdown("""
12
+ <style>
13
+ /* Floating Donation Widgets - Optimized */
14
+ .floating-donations {
15
+ position: fixed;
16
+ right: 20px;
17
+ top: 50%;
18
+ transform: translateY(-50%);
19
+ z-index: 1000;
20
+ display: flex;
21
+ flex-direction: column;
22
+ gap: 15px;
23
+ }
24
+
25
+ .floating-btn {
26
+ width: 60px;
27
+ height: 60px;
28
+ border-radius: 50%;
29
+ display: flex;
30
+ align-items: center;
31
+ justify-content: center;
32
+ text-decoration: none;
33
+ font-weight: bold;
34
+ font-size: 14px;
35
+ box-shadow: 0 4px 20px rgba(0,0,0,0.3);
36
+ transition: all 0.3s ease;
37
+ cursor: pointer;
38
+ border: none;
39
+ position: relative;
40
+ overflow: hidden;
41
+ will-change: transform;
42
+ }
43
+
44
+ .floating-btn:hover {
45
+ transform: scale(1.1);
46
+ box-shadow: 0 6px 30px rgba(0,0,0,0.4);
47
+ text-decoration: none;
48
+ }
49
+
50
+ .floating-btn:before {
51
+ content: '';
52
+ position: absolute;
53
+ top: 0;
54
+ left: -100%;
55
+ width: 100%;
56
+ height: 100%;
57
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
58
+ transition: left 0.5s;
59
+ }
60
+
61
+ .floating-btn:hover:before {
62
+ left: 100%;
63
+ }
64
+
65
+ .kofi-floating {
66
+ background: linear-gradient(135deg, #ff5f5f, #ff3838);
67
+ color: white;
68
+ }
69
+
70
+ .saweria-floating {
71
+ background: linear-gradient(135deg, #ffbd44, #f39c12);
72
+ color: #2c3e50;
73
+ }
74
+
75
+ .floating-tooltip {
76
+ position: absolute;
77
+ right: 70px;
78
+ top: 50%;
79
+ transform: translateY(-50%);
80
+ background: rgba(0,0,0,0.8);
81
+ color: white;
82
+ padding: 8px 12px;
83
+ border-radius: 6px;
84
+ font-size: 12px;
85
+ white-space: nowrap;
86
+ opacity: 0;
87
+ visibility: hidden;
88
+ transition: all 0.3s ease;
89
+ pointer-events: none;
90
+ }
91
+
92
+ .floating-tooltip:after {
93
+ content: '';
94
+ position: absolute;
95
+ left: 100%;
96
+ top: 50%;
97
+ transform: translateY(-50%);
98
+ border: 5px solid transparent;
99
+ border-left-color: rgba(0,0,0,0.8);
100
+ }
101
+
102
+ .floating-btn:hover .floating-tooltip {
103
+ opacity: 1;
104
+ visibility: visible;
105
+ }
106
+
107
+ /* Responsive hiding for mobile */
108
+ @media (max-width: 768px) {
109
+ .floating-donations {
110
+ display: none;
111
+ }
112
+ }
113
+
114
+ /* Ko-fi widget custom styling */
115
+ .kofi-widget-container {
116
+ position: fixed !important;
117
+ bottom: 20px !important;
118
+ right: 20px !important;
119
+ z-index: 999 !important;
120
+ }
121
+
122
+ /* Hide default Ko-fi widget on mobile */
123
+ @media (max-width: 768px) {
124
+ .kofi-widget-container {
125
+ display: none !important;
126
+ }
127
+ }
128
+ </style>
129
+ """, unsafe_allow_html=True)
130
+
131
+ def render_donation_html():
132
+ """Render the HTML structure for donation widgets"""
133
+
134
+ st.markdown("""
135
+ <!-- Floating Donation Widgets -->
136
+ <div class="floating-donations">
137
+ <a href="https://ko-fi.com/saltudio" target="_blank" class="floating-btn kofi-floating" rel="noopener">
138
+ <span>☕</span>
139
+ <div class="floating-tooltip">Support on Ko-fi</div>
140
+ </a>
141
+ <a href="https://saweria.co/saltudio" target="_blank" class="floating-btn saweria-floating" rel="noopener">
142
+ <span>💝</span>
143
+ <div class="floating-tooltip">Support on Saweria</div>
144
+ </a>
145
+ </div>
146
+ """, unsafe_allow_html=True)
147
+
148
+ @st.cache_resource
149
+ def load_donation_scripts():
150
+ """Load donation widget scripts with caching"""
151
+
152
+ return """
153
+ <!-- Ko-fi Floating Widget Script -->
154
+ <script src='https://storage.ko-fi.com/cdn/scripts/overlay-widget.js' async defer></script>
155
+ <script>
156
+ // Lazy load Ko-fi widget
157
+ document.addEventListener('DOMContentLoaded', function() {
158
+ if (typeof kofiWidgetOverlay !== 'undefined') {
159
+ kofiWidgetOverlay.draw('saltudio', {
160
+ 'type': 'floating-chat',
161
+ 'floating-chat.donateButton.text': 'Support me',
162
+ 'floating-chat.donateButton.background-color': '#ff38b8',
163
+ 'floating-chat.donateButton.text-color': '#fff'
164
+ });
165
+ }
166
+ });
167
+
168
+ // Custom Saweria floating widget - optimized
169
+ (function() {
170
+ function initDonationWidgets() {
171
+ const kofiWidget = document.querySelector('.kofi-widget-container');
172
+ const customWidgets = document.querySelector('.floating-donations');
173
+
174
+ if (kofiWidget && customWidgets) {
175
+ customWidgets.style.bottom = '100px';
176
+ customWidgets.style.top = 'auto';
177
+ customWidgets.style.transform = 'none';
178
+ }
179
+
180
+ // Add click tracking with throttling
181
+ let clickThrottle = false;
182
+ document.querySelectorAll('.floating-btn').forEach(btn => {
183
+ btn.addEventListener('click', function(e) {
184
+ if (!clickThrottle) {
185
+ clickThrottle = true;
186
+ const platform = this.classList.contains('kofi-floating') ? 'ko-fi' : 'saweria';
187
+ console.log(`Donation click: ${platform}`);
188
+ setTimeout(() => clickThrottle = false, 1000);
189
+ }
190
+ });
191
+ });
192
+ }
193
+
194
+ // Initialize when DOM is ready
195
+ if (document.readyState === 'loading') {
196
+ document.addEventListener('DOMContentLoaded', initDonationWidgets);
197
+ } else {
198
+ initDonationWidgets();
199
+ }
200
+ })();
201
+ </script>
202
+ """
203
+
204
+ def render_donation_scripts():
205
+ """Render donation widget scripts"""
206
+ scripts = load_donation_scripts()
207
+ st.markdown(scripts, unsafe_allow_html=True)
208
+
209
+ def render_sidebar_support():
210
+ """Render compact support section for sidebar"""
211
+
212
+ st.markdown("""
213
+ <div style="text-align: center;">
214
+ <p style="color: #9fb0c3; font-size: 0.9rem; margin-bottom: 1rem;">
215
+ Help keep this free tool running!
216
+ </p>
217
+ <div style="display: flex; gap: 10px; justify-content: center;">
218
+ <a href="https://ko-fi.com/saltudio" target="_blank" style="text-decoration: none;" rel="noopener">
219
+ <div style="background: linear-gradient(135deg, #ff5f5f, #ff3838); color: white; padding: 8px 16px; border-radius: 20px; font-size: 0.8rem; display: inline-flex; align-items: center; gap: 5px;">
220
+ ☕ Ko-fi
221
+ </div>
222
+ </a>
223
+ <a href="https://saweria.co/saltudio" target="_blank" style="text-decoration: none;" rel="noopener">
224
+ <div style="background: linear-gradient(135deg, #ffbd44, #f39c12); color: #2c3e50; padding: 8px 16px; border-radius: 20px; font-size: 0.8rem; display: inline-flex; align-items: center; gap: 5px;">
225
+ 💝 Saweria
226
+ </div>
227
+ </a>
228
+ </div>
229
+ </div>
230
+ """, unsafe_allow_html=True)
231
+
232
+ def render_footer_support():
233
+ """Render footer support section"""
234
+
235
+ st.markdown("""
236
+ <div style="text-align: center; padding: 20px; color: #9fb0c3;">
237
+ <p>💖 <strong>Enjoying VidGen?</strong> Support the project to keep it free!</p>
238
+ <p style="font-size: 0.9rem;">
239
+ Use the floating buttons on the right or the links in the sidebar to show your appreciation.
240
+ <br>Every coffee helps maintain the servers and develop new features! ☕✨
241
+ </p>
242
+ <p style="font-size: 0.8rem; margin-top: 1rem;">
243
+ Made with ❤️ using Streamlit • Veo • HuggingFace Spaces
244
+ </p>
245
+ </div>
246
+ """, unsafe_allow_html=True)
gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ streamlit>=1.28.0
2
+ huggingface_hub>=0.17.0
3
+ requests>=2.28.0
4
+ google-genai>=0.5.0
utils/error_handler.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Error handling utilities for VidGen
3
+ """
4
+
5
+ import streamlit as st
6
+ import time
7
+ import functools
8
+ from typing import Any, Callable, Optional
9
+
10
+ def retry_on_error(max_retries: int = 3, delay: float = 1.0, backoff: float = 2.0):
11
+ """Decorator to retry functions on specific errors"""
12
+ def decorator(func: Callable) -> Callable:
13
+ @functools.wraps(func)
14
+ def wrapper(*args, **kwargs) -> Any:
15
+ last_exception = None
16
+
17
+ for attempt in range(max_retries):
18
+ try:
19
+ return func(*args, **kwargs)
20
+ except Exception as e:
21
+ last_exception = e
22
+ error_msg = str(e).lower()
23
+
24
+ # Check for retryable errors
25
+ retryable_errors = [
26
+ 'bodystreamBuffer was aborted',
27
+ 'connection error',
28
+ 'timeout',
29
+ 'network error',
30
+ 'temporary failure',
31
+ 'rate limit'
32
+ ]
33
+
34
+ is_retryable = any(err in error_msg for err in retryable_errors)
35
+
36
+ if not is_retryable or attempt == max_retries - 1:
37
+ raise e
38
+
39
+ wait_time = delay * (backoff ** attempt)
40
+ st.warning(f"⚠️ Attempt {attempt + 1} failed: {e}. Retrying in {wait_time:.1f}s...")
41
+ time.sleep(wait_time)
42
+
43
+ raise last_exception
44
+ return wrapper
45
+ return decorator
46
+
47
+ def handle_stream_error(func: Callable) -> Callable:
48
+ """Handle stream buffer errors specifically"""
49
+ @functools.wraps(func)
50
+ def wrapper(*args, **kwargs):
51
+ try:
52
+ return func(*args, **kwargs)
53
+ except Exception as e:
54
+ error_msg = str(e).lower()
55
+ if 'bodystreamBuffer' in error_msg or 'aborted' in error_msg:
56
+ st.error("🔄 Connection interrupted. Please try again.")
57
+ st.info("💡 Tip: Try refreshing the page if the issue persists.")
58
+ return None
59
+ else:
60
+ raise e
61
+ return wrapper
62
+
63
+ class ProgressTracker:
64
+ """Track progress with better error handling"""
65
+
66
+ def __init__(self, total_jobs: int):
67
+ self.total_jobs = total_jobs
68
+ self.current_job = 0
69
+ self.progress_bar = None
70
+ self.status_text = None
71
+
72
+ def start(self):
73
+ """Initialize progress tracking"""
74
+ self.progress_bar = st.progress(0)
75
+ self.status_text = st.empty()
76
+
77
+ def update(self, job_idx: int, status: str = "Processing..."):
78
+ """Update progress"""
79
+ if self.progress_bar and self.status_text:
80
+ progress = (job_idx + 1) / self.total_jobs
81
+ self.progress_bar.progress(progress)
82
+ self.status_text.text(f"{status} ({job_idx + 1}/{self.total_jobs})")
83
+
84
+ def complete(self):
85
+ """Mark as complete"""
86
+ if self.progress_bar and self.status_text:
87
+ self.progress_bar.progress(1.0)
88
+ self.status_text.text("✅ All jobs completed!")
utils/session_manager.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Session state management utilities
3
+ """
4
+
5
+ import streamlit as st
6
+ from typing import Any, Dict, Optional
7
+
8
+ class SessionManager:
9
+ """Manage session state with better error handling"""
10
+
11
+ @staticmethod
12
+ def init_session_state():
13
+ """Initialize session state with default values"""
14
+ defaults = {
15
+ "results": [],
16
+ "jobs": [],
17
+ "job_idx": 0,
18
+ "running": False,
19
+ "api_key_val": "",
20
+ "last_error": None,
21
+ "retry_count": 0,
22
+ "backend_loaded": False,
23
+ "backend": None
24
+ }
25
+
26
+ for key, value in defaults.items():
27
+ if key not in st.session_state:
28
+ st.session_state[key] = value
29
+
30
+ @staticmethod
31
+ def safe_update(updates: Dict[str, Any]):
32
+ """Safely update session state"""
33
+ try:
34
+ st.session_state.update(updates)
35
+ except Exception as e:
36
+ st.error(f"Session update failed: {e}")
37
+
38
+ @staticmethod
39
+ def reset_job_state():
40
+ """Reset job-related state"""
41
+ SessionManager.safe_update({
42
+ "results": [],
43
+ "jobs": [],
44
+ "job_idx": 0,
45
+ "running": False,
46
+ "last_error": None,
47
+ "retry_count": 0
48
+ })
49
+
50
+ @staticmethod
51
+ def get_safe(key: str, default: Any = None) -> Any:
52
+ """Safely get session state value"""
53
+ return st.session_state.get(key, default)