rishuXori commited on
Commit
0eb7e13
Β·
1 Parent(s): 6f9e9da

Relase Commit

Browse files
.gitattributes CHANGED
@@ -33,3 +33,9 @@ saved_model/**/* 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
 
 
 
 
 
 
 
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
36
+ assets/demo_vid1.mp4 filter=lfs diff=lfs merge=lfs -text
37
+ assets/model.mp4 filter=lfs diff=lfs merge=lfs -text
38
+ assets/meloni-ref-cut.mp4 filter=lfs diff=lfs merge=lfs -text
39
+ assets/Trump-ref.mp4 filter=lfs diff=lfs merge=lfs -text
40
+ generated_demos/Trump-gen-sample.mp4 filter=lfs diff=lfs merge=lfs -text
41
+ generated_demos/Meloni-gen-sample.mp4 filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ .env
2
+ requirements.txt
3
+ venv
4
+ *.ipynb
Dockerfile CHANGED
@@ -1,20 +1,24 @@
 
1
  FROM python:3.13.5-slim
2
 
3
  WORKDIR /app
4
 
 
5
  RUN apt-get update && apt-get install -y \
6
  build-essential \
7
  curl \
8
  git \
9
  && rm -rf /var/lib/apt/lists/*
10
 
 
11
  COPY requirements.txt ./
12
- COPY src/ ./src/
13
 
 
14
  RUN pip3 install -r requirements.txt
15
 
16
  EXPOSE 8501
17
 
18
  HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
19
 
20
- ENTRYPOINT ["streamlit", "run", "src/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
 
1
+
2
  FROM python:3.13.5-slim
3
 
4
  WORKDIR /app
5
 
6
+ # System dependencies
7
  RUN apt-get update && apt-get install -y \
8
  build-essential \
9
  curl \
10
  git \
11
  && rm -rf /var/lib/apt/lists/*
12
 
13
+ # Copy requirements + source
14
  COPY requirements.txt ./
15
+ COPY . ./
16
 
17
+ # Install Python deps
18
  RUN pip3 install -r requirements.txt
19
 
20
  EXPOSE 8501
21
 
22
  HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
23
 
24
+ ENTRYPOINT ["streamlit", "run", "src/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0", "--server.enableXsrfProtection=false"]
README.md CHANGED
@@ -1,12 +1,12 @@
1
  ---
2
- title: Streamlit Template Space
3
  emoji: πŸš€
4
  colorFrom: red
5
  colorTo: red
6
  sdk: docker
7
  app_port: 8501
8
  tags:
9
- - streamlit
10
  pinned: false
11
  short_description: Streamlit template space
12
  ---
 
1
  ---
2
+ title: Ori Avatar Demo
3
  emoji: πŸš€
4
  colorFrom: red
5
  colorTo: red
6
  sdk: docker
7
  app_port: 8501
8
  tags:
9
+ - streamlit
10
  pinned: false
11
  short_description: Streamlit template space
12
  ---
assets/.DS_Store ADDED
Binary file (6.15 kB). View file
 
assets/Trump-ref.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7b26e0a42c910ead2d3040ecea0b7c7eabc16a6bec2539e603d322bdfefa7530
3
+ size 908337
assets/meloni-ref-cut.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0ed2df903bd8c659197a9769f1250855c86fac6496a8ea64054428598e55b4b4
3
+ size 3392218
assets/model.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:451f774067d073711ed7fc3ed0909dc51beab5114f3b35cf6f3b49560c455182
3
+ size 3876228
generated_demos/Meloni-gen-sample.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2b6aa16456398c1f91c44ddd6e258e8adc804770f34406be6c2dd03e44395f77
3
+ size 5912313
generated_demos/Trump-gen-sample.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cdbbd9bb69f9add51ab71cf95d723a0a0e29f7374200d4a0eea9041dd3d57bb0
3
+ size 4899397
requirements.txt CHANGED
@@ -1,3 +1,4 @@
1
- altair
2
- pandas
3
- streamlit
 
 
1
+
2
+ python-dotenv==1.2.1
3
+ requests==2.32.5
4
+ streamlit==1.51.0
src/streamlit_app.py CHANGED
@@ -1,40 +1,770 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
  import streamlit as st
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
 
 
2
  import streamlit as st
3
+ import requests
4
+ import tempfile
5
+ import os
6
+ import re
7
+ from pathlib import Path
8
+ from typing import Optional, Dict, List
9
+ from dotenv import load_dotenv
10
+
11
+ load_dotenv()
12
+
13
+ # Configuration
14
+ API_KEY = os.getenv("MUSETALK_API_KEY")
15
+ CHECK_RATE_LIMIT_URL = os.getenv("CHECK_RATE_LIMIT_URL")
16
+ TASK_UPLOAD_URL = os.getenv("TASK_UPLOAD_URL")
17
+ DEMO_VIDEOS_PATH = "./assets" # Change this path as needed
18
+ GENERATED_DEMO_VIDEOS_PATH = "./generated_demos" # Path for generated demo videos showcase
19
+ REQUEST_TIMEOUT = 3600
20
+ MAX_FILE_SIZE_MB = 10
21
+ MAX_TEXT_LENGTH = 300
22
+
23
+ LANGUAGE_VOICES: Dict[str, List[str]] = {
24
+ "Hindi": ["Kavya", "Priya", "Jyoti"],
25
+ "Telugu": ["Nandini", "Rashmi", "Riya"],
26
+ "English": ["Maria", "Ishita", "Aditi"],
27
+ "Tamil": ["Jessica", "Jasmine", "Rashmi", "Amy", "Fatima"],
28
+ "Kannada": ["Manisha", "Hema", "Uma"],
29
+ "Bhojpuri": ["Anju", "Gayatri", "Radha"],
30
+ "Bengali": ["Madhumita", "Shrabonti", "Subhashree"],
31
+ "Maithili": ["Jaanki", "Shital", "Anuradha"],
32
+ "Magahi": ["Amrita", "Anu", "Radhika"],
33
+ "Gujarati": ["Hetvi", "Parul", "Janvi"],
34
+ "Marathi": ['Vashnavi', "Varsha", "Savita"],
35
+ "Malayalam": ["Nandini", "Uma", "Hema", "Manisha"],
36
+ "Chattisgarhi": ["Kusum", "Mamata", "Kamla"]
37
+ }
38
+
39
+ def init_state():
40
+ """Initialize session state variables"""
41
+ if "email_verified" not in st.session_state:
42
+ st.session_state.email_verified = False
43
+ if "email" not in st.session_state:
44
+ st.session_state.email = ""
45
+ if "rate_limit_info" not in st.session_state:
46
+ st.session_state.rate_limit_info = {}
47
+ if "generated_path" not in st.session_state:
48
+ st.session_state.generated_path = None
49
+ if "processing" not in st.session_state:
50
+ st.session_state.processing = False
51
+ if "selected_demo_video" not in st.session_state:
52
+ st.session_state.selected_demo_video = None
53
+ if "reference_audio" not in st.session_state:
54
+ st.session_state.reference_audio = None
55
+ if "reference_audio_path" not in st.session_state:
56
+ st.session_state.reference_audio_path = None
57
+
58
+ def validate_email(email: str) -> bool:
59
+ """Validate email format using regex"""
60
+ pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
61
+ return re.match(pattern, email) is not None
62
+
63
+ def _on_text_change():
64
+ """
65
+ Called whenever the text area value changes (typing or paste).
66
+ Sets session flags used by the UI to show warnings.
67
+ """
68
+ text = st.session_state.get("text_input", "") or ""
69
+ st.session_state["text_exceeded"] = len(text) > MAX_TEXT_LENGTH
70
+ st.session_state["text_exceeded_count"] = len(text)
71
+
72
+ def check_rate_limit(email: str) -> Optional[Dict]:
73
+ """Check rate limit for email via API"""
74
+ try:
75
+ response = requests.post(
76
+ CHECK_RATE_LIMIT_URL,
77
+ json={"email": email},
78
+ headers={"Authorization": f"Bearer {API_KEY}"},
79
+ timeout=10
80
+ )
81
+ if response.status_code == 200:
82
+ return response.json()
83
+ else:
84
+ st.error(f"Error checking rate limit: {response.json().get('detail', 'Unknown error')}")
85
+ return None
86
+ except Exception as e:
87
+ st.error(f"Failed to connect to server: {str(e)}")
88
+ return None
89
+
90
+ def check_audio_duration(audio_file) -> tuple[bool, str, float]:
91
+ """Check if audio file duration is between 5 and 300 seconds"""
92
+ try:
93
+ import wave
94
+ import contextlib
95
+
96
+ # Save uploaded file temporarily to check duration
97
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp_file:
98
+ audio_file.seek(0)
99
+ tmp_file.write(audio_file.read())
100
+ tmp_file.flush()
101
+ tmp_path = tmp_file.name
102
+
103
+ # Get audio duration
104
+ with contextlib.closing(wave.open(tmp_path, 'r')) as f:
105
+ frames = f.getnframes()
106
+ rate = f.getframerate()
107
+ duration = frames / float(rate)
108
+
109
+ audio_file.seek(0) # Reset file pointer
110
+
111
+ if duration < 5:
112
+ os.unlink(tmp_path)
113
+ return False, f"Audio too short ({duration:.1f}s). Minimum 5 seconds required.", duration
114
+ elif duration > 300:
115
+ os.unlink(tmp_path)
116
+ return False, f"Audio too long ({duration:.1f}s). Maximum 300 seconds allowed.", duration
117
+
118
+ return True, f"Audio duration: {duration:.1f} seconds", duration
119
+
120
+ except Exception as e:
121
+ return False, f"Error checking audio duration: {str(e)}", 0
122
+
123
+ def check_file_size(file_obj) -> tuple[bool, str]:
124
+ """Check if uploaded file is within size limits"""
125
+ try:
126
+ file_obj.seek(0, os.SEEK_END)
127
+ file_size = file_obj.tell()
128
+ file_obj.seek(0)
129
+
130
+ file_size_mb = file_size / (1024 * 1024)
131
+
132
+ if file_size_mb > MAX_FILE_SIZE_MB:
133
+ return False, f"File size ({file_size_mb:.2f} MB) exceeds maximum of {MAX_FILE_SIZE_MB} MB"
134
+
135
+ return True, f"File size: {file_size_mb:.2f} MB"
136
+ except Exception as e:
137
+ return False, f"Error checking file size: {str(e)}"
138
+
139
+ def get_demo_videos() -> List[Dict[str, str]]:
140
+ """Get list of demo videos from demo folder"""
141
+ demo_videos = []
142
+ if os.path.exists(DEMO_VIDEOS_PATH):
143
+ for file in os.listdir(DEMO_VIDEOS_PATH):
144
+ if file.endswith(('.mp4', '.avi', '.mov')):
145
+ demo_videos.append({
146
+ "name": file,
147
+ "path": os.path.join(DEMO_VIDEOS_PATH, file)
148
+ })
149
+ return demo_videos
150
+
151
+ def get_generated_demo_videos() -> List[Dict[str, str]]:
152
+ """Get list of generated demo videos for showcase"""
153
+ generated_demos = []
154
+ if os.path.exists(GENERATED_DEMO_VIDEOS_PATH):
155
+ for file in sorted(os.listdir(GENERATED_DEMO_VIDEOS_PATH)):
156
+ if file.endswith(('.mp4', '.avi', '.mov')):
157
+ generated_demos.append({
158
+ "name": file,
159
+ "path": os.path.join(GENERATED_DEMO_VIDEOS_PATH, file)
160
+ })
161
+ return generated_demos
162
+
163
+ def stream_post_upload(video_source, filename: str, text: str, voice_name: str, language: str, email: str, reference_audio_path: Optional[str] = None) -> Optional[str]:
164
+ """
165
+ Stream multipart/form-data POST to backend
166
+ video_source can be file object or path string (for demo videos)
167
+ reference_audio_path: path to cloned voice audio file (if using voice cloning)
168
+ """
169
+ try:
170
+ headers = {"Authorization": f"Bearer {API_KEY}"}
171
+ lang_dict = {
172
+ "Hindi": "hi",
173
+ "Telugu": "te",
174
+ "English": "en",
175
+ "Tamil": "ta",
176
+ "Kannada": "kn",
177
+ "Bhojpuri": "bho",
178
+ "Bengali": "bn",
179
+ "Maithili": "mai",
180
+ "Magahi": "mag",
181
+ "Gujarati": "gu",
182
+ "Marathi": "mr",
183
+ "Malayalam": "ml",
184
+ "Chattisgarhi": "hne"
185
+ }
186
+
187
+ data = {
188
+ "text": text,
189
+ "email": email,
190
+ "language": lang_dict.get(language, "hi")
191
+ }
192
+
193
+ # Add voice_name (can be None if using voice cloning)
194
+ data["voice_name"] = voice_name if voice_name else ""
195
+
196
+ files = {}
197
+
198
+ # Handle demo video (path string) or uploaded file (file object)
199
+ if isinstance(video_source, str):
200
+ # Demo video - open file
201
+ video_file = open(video_source, "rb")
202
+ files["video"] = (filename, video_file, "video/mp4")
203
+ else:
204
+ # Uploaded file
205
+ video_source.seek(0)
206
+ files["video"] = (filename, video_source, "video/mp4")
207
+
208
+ # Add voice_cloning file if provided
209
+ if reference_audio_path and os.path.exists(reference_audio_path):
210
+ audio_file = open(reference_audio_path, "rb")
211
+ files["voice_cloning"] = ("reference.wav", audio_file, "audio/wav")
212
+
213
+ try:
214
+ resp = requests.post(
215
+ TASK_UPLOAD_URL,
216
+ headers=headers,
217
+ data=data,
218
+ files=files,
219
+ stream=True,
220
+ timeout=REQUEST_TIMEOUT
221
+ )
222
+ finally:
223
+ # Close any opened files
224
+ if isinstance(video_source, str):
225
+ video_file.close()
226
+ if reference_audio_path and 'audio_file' in locals():
227
+ audio_file.close()
228
+
229
+ if not resp.ok:
230
+ try:
231
+ err = resp.json()
232
+ except:
233
+ err = resp.text
234
+ raise RuntimeError(f"Server returned {resp.status_code}: {err}")
235
+
236
+ # Check if email was sent
237
+ email_sent = resp.headers.get('X-Email-Sent', 'true') != 'false'
238
+
239
+ # Save streamed response
240
+ tmp_out = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4")
241
+ tmp_out_path = tmp_out.name
242
+ tmp_out.close()
243
+
244
+ with open(tmp_out_path, "wb") as out_f:
245
+ for chunk in resp.iter_content(chunk_size=8192):
246
+ if chunk:
247
+ out_f.write(chunk)
248
+
249
+ return tmp_out_path, email_sent
250
+
251
+ except requests.exceptions.Timeout:
252
+ raise TimeoutError("Server timeout. Please try again later.")
253
+ except requests.exceptions.ConnectionError:
254
+ raise ConnectionError("Unable to connect to server.")
255
+ except Exception as e:
256
+ raise RuntimeError(f"Upload failed: {str(e)}")
257
+
258
+
259
+ # Page config
260
+ st.set_page_config(page_title="ORI-FaceX", layout="wide")
261
+ init_state()
262
+
263
+ st.title("FaceX")
264
+ st.markdown("Transform your videos with AI-powered lip-sync and expressive voices!")
265
+
266
+ # Sidebar with info and rate limit display
267
+ with st.sidebar:
268
+ st.header("ℹ️ How It Works")
269
+ st.markdown("""
270
+ 1. **Enter your email** to verify eligibility
271
+ 2. **Select a language** and voice
272
+ 3. **Choose** demo video or upload your own
273
+ 4. **Enter text** in your chosen language (max 300 characters)
274
+ - Use [this tool](https://www.easyhindityping.com/english-to-hindi-translation) for easy typing in Indian languages
275
+ 5. **Generate** and download your video
276
+ """)
277
+
278
+ st.markdown("---")
279
+
280
+ # Show rate limit info if email verified
281
+ if st.session_state.email_verified:
282
+ st.success(f"πŸ“§ **Email:** {st.session_state.email}")
283
+ info = st.session_state.rate_limit_info
284
+ current = info.get('current_count', 0)
285
+ remaining = info.get('remaining', 5)
286
+
287
+ st.metric("Videos Used", f"{current}/5")
288
+ st.metric("Remaining", remaining)
289
+
290
+ # Progress bar
291
+ progress = current / 5
292
+ st.progress(progress)
293
+
294
+ if remaining == 0:
295
+ st.error("⚠️ Limit reached!")
296
+
297
+ st.markdown("---")
298
+ st.warning("⚠️ **Use responsibly!** Do not create misleading or harmful content.")
299
+
300
+ st.markdown("---")
301
+ st.markdown("Reach out to us at ai-team@oriserve.com")
302
+
303
+ st.markdown("---")
304
+ st.markdown("### πŸ“š Reference")
305
+ st.code("@article{musetalk,\n title={MuseTalk...},\n year={2025}\n}", language="text")
306
+
307
+ # Generated Demo Videos Showcase Section
308
+ st.markdown("---")
309
+ st.subheader("πŸŽ₯ See What's Possible")
310
+ st.markdown("Check out these examples of AI-generated videos using TalkMorph:")
311
+
312
+ generated_demos = get_generated_demo_videos()
313
+
314
+ if generated_demos:
315
+ # Create tabs or columns to display generated demos with consistent sizing
316
+ demo_cols = st.columns(3)
317
+
318
+ for idx, demo in enumerate(generated_demos):
319
+ with demo_cols[idx % 3]:
320
+ # Use container with fixed aspect ratio
321
+ with st.container():
322
+ st.video(demo["path"], start_time=0)
323
+ # Remove file extension and format name nicely
324
+ display_name = os.path.splitext(demo["name"])[0].replace("_", " ").title()
325
+ st.caption(f"**{display_name}**")
326
+
327
+ if len(generated_demos) > 3:
328
+ st.markdown("*More examples available - scroll down to see all*")
329
+ else:
330
+ st.info(f"πŸ“ No demo videos found. Add videos to `{GENERATED_DEMO_VIDEOS_PATH}` to showcase examples.")
331
+
332
+ # Main content
333
+ st.markdown("---")
334
+
335
+ # Email Verification Section
336
+ if not st.session_state.email_verified:
337
+ st.subheader("πŸ“§ Email Verification")
338
+ st.info("Enter your email to check eligibility. Each email can generate up to **5 videos**.")
339
+
340
+ col1, col2 = st.columns([3, 1])
341
+
342
+ with col1:
343
+ email_input = st.text_input(
344
+ "Email Address",
345
+ placeholder="your.email@example.com",
346
+ key="email_input",
347
+ disabled=st.session_state.processing
348
+ )
349
+
350
+ with col2:
351
+ st.write("") # Spacing
352
+ st.write("") # Spacing
353
+ verify_btn = st.button("Verify Email", type="primary", disabled=st.session_state.processing)
354
+
355
+ if verify_btn:
356
+ if not email_input:
357
+ st.error("Please enter your email address")
358
+ elif not validate_email(email_input):
359
+ st.error("❌ Invalid email format. Please enter a valid email.")
360
+ else:
361
+ with st.spinner("Checking your email..."):
362
+ rate_limit_info = check_rate_limit(email_input)
363
+
364
+ if rate_limit_info:
365
+ if rate_limit_info['can_proceed']:
366
+ st.session_state.email = email_input
367
+ st.session_state.email_verified = True
368
+ st.session_state.rate_limit_info = rate_limit_info
369
+ st.success(f"βœ… Email verified! You have {rate_limit_info['remaining']} videos remaining.")
370
+ st.rerun()
371
+ else:
372
+ st.error(f"❌ {rate_limit_info['message']}")
373
+ st.info("πŸ’‘ Try with a different email address to continue.")
374
 
375
+ # Video Generation Section (only after email verification)
376
+ else:
377
+ # Show locked email with change option
378
+ col1, col2 = st.columns([4, 1])
379
+ with col1:
380
+ st.text_input(
381
+ "πŸ“§ Email Address (Verified)",
382
+ value=st.session_state.email,
383
+ disabled=True,
384
+ key="email_locked"
385
+ )
386
+ with col2:
387
+ st.write("") # Spacing
388
+ st.write("") # Spacing
389
+ if st.button("Change Email", disabled=st.session_state.processing):
390
+ st.session_state.email_verified = False
391
+ st.session_state.email = ""
392
+ st.session_state.rate_limit_info = {}
393
+ st.rerun()
394
+
395
+ st.markdown("---")
396
+ st.subheader("🎬 Generate Your Video")
397
+
398
+ # Language selection
399
+ language = st.selectbox(
400
+ "🌍 Select Language",
401
+ options=list(LANGUAGE_VOICES.keys()),
402
+ disabled=st.session_state.processing
403
+ )
404
+
405
+ # Voice Mode Selection
406
+ st.markdown("---")
407
+ voice_mode = st.radio(
408
+ "πŸŽ™οΈ Voice Selection Mode",
409
+ options=["Default Voice", "Clone Voice"],
410
+ horizontal=True,
411
+ disabled=st.session_state.processing,
412
+ help="Choose a pre-configured voice or clone a custom voice from audio"
413
+ )
414
+
415
+ voice_name = None
416
+ reference_audio = None
417
+
418
+ if voice_mode == "Default Voice":
419
+ # Get voices for selected language
420
+ available_voices = LANGUAGE_VOICES.get(language, [])
421
+ voice_name = st.selectbox(
422
+ "Select Voice",
423
+ options=available_voices,
424
+ disabled=st.session_state.processing
425
+ )
426
+ st.session_state.reference_audio = None
427
+ st.session_state.reference_audio_path = None
428
+
429
+ else: # Clone Voice
430
+ st.info("πŸ“’ **Voice Cloning:** Provide a reference audio (5-300 seconds) to clone the voice")
431
+
432
+ audio_source = st.radio(
433
+ "Reference Audio Source",
434
+ options=["Upload Audio File", "Record Audio"],
435
+ horizontal=True,
436
+ disabled=st.session_state.processing
437
+ )
438
+
439
+ if audio_source == "Upload Audio File":
440
+ reference_audio = st.file_uploader(
441
+ "Upload Reference Audio (.wav format, 5-300 seconds)",
442
+ type=["wav"],
443
+ disabled=st.session_state.processing,
444
+ key="audio_uploader"
445
+ )
446
+
447
+ if reference_audio:
448
+ # Check audio duration
449
+ is_valid, duration_msg, duration = check_audio_duration(reference_audio)
450
+
451
+ if is_valid:
452
+ st.success(f"βœ… {duration_msg}")
453
+
454
+ # Save to temporary file in session state
455
+ if st.session_state.reference_audio_path:
456
+ try:
457
+ os.unlink(st.session_state.reference_audio_path)
458
+ except:
459
+ pass
460
+
461
+ tmp_audio = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
462
+ reference_audio.seek(0)
463
+ tmp_audio.write(reference_audio.read())
464
+ tmp_audio.flush()
465
+ tmp_audio.close()
466
+
467
+ st.session_state.reference_audio = reference_audio
468
+ st.session_state.reference_audio_path = tmp_audio.name
469
+
470
+ # Show audio player
471
+ st.audio(reference_audio, format="audio/wav")
472
+ else:
473
+ st.error(f"❌ {duration_msg}")
474
+ st.session_state.reference_audio = None
475
+ st.session_state.reference_audio_path = None
476
+
477
+ else: # Record Audio
478
+ st.warning("🎀 **Note:** Recorded audio must be between 5-300 seconds")
479
+ reference_audio = st.audio_input(
480
+ "Record Reference Audio (speak for 5-300 seconds)",
481
+ disabled=st.session_state.processing,
482
+ key="audio_recorder"
483
+ )
484
+
485
+ if reference_audio:
486
+ # Check audio duration
487
+ is_valid, duration_msg, duration = check_audio_duration(reference_audio)
488
+
489
+ if is_valid:
490
+ st.success(f"βœ… {duration_msg}")
491
+
492
+ # Save to temporary file in session state
493
+ if st.session_state.reference_audio_path:
494
+ try:
495
+ os.unlink(st.session_state.reference_audio_path)
496
+ except:
497
+ pass
498
+
499
+ tmp_audio = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
500
+ reference_audio.seek(0)
501
+ tmp_audio.write(reference_audio.read())
502
+ tmp_audio.flush()
503
+ tmp_audio.close()
504
+
505
+ st.session_state.reference_audio = reference_audio
506
+ st.session_state.reference_audio_path = tmp_audio.name
507
+
508
+ # Show audio player
509
+ st.audio(reference_audio, format="audio/wav")
510
+ else:
511
+ st.error(f"❌ {duration_msg}")
512
+ st.session_state.reference_audio = None
513
+ st.session_state.reference_audio_path = None
514
+
515
+ # Video source selection
516
+ st.markdown("---")
517
+ video_source_option = st.radio(
518
+ "πŸ“Ή Video Source",
519
+ options=["Upload Video", "Use Demo Video"],
520
+ horizontal=True,
521
+ disabled=st.session_state.processing
522
+ )
523
+
524
+ video_file = None
525
+ demo_video_path = None
526
+
527
+ if video_source_option == "Upload Video":
528
+ video_file = st.file_uploader(
529
+ f"Upload video (mp4) - Max {MAX_FILE_SIZE_MB} MB",
530
+ type=["mp4"],
531
+ disabled=st.session_state.processing
532
+ )
533
+
534
+ if video_file:
535
+ is_valid, size_msg = check_file_size(video_file)
536
+ if is_valid:
537
+ st.info(size_msg)
538
+ else:
539
+ st.error(size_msg)
540
+
541
+ else: # Use Demo Video
542
+ demo_videos = get_demo_videos()
543
+
544
+ if not demo_videos:
545
+ st.warning(f"⚠️ No demo videos found in `{DEMO_VIDEOS_PATH}` folder.")
546
+ else:
547
+ st.markdown("**Select a demo video:**")
548
+
549
+ # Display demo videos in a more compact grid
550
+ cols = st.columns(3)
551
+ for idx, demo in enumerate(demo_videos):
552
+ with cols[idx % 3]:
553
+ # Use expander for cleaner look
554
+ with st.expander(f"πŸ“Ή {demo['name']}", expanded=False):
555
+ st.video(demo["path"], start_time=0)
556
+ if st.button(
557
+ "βœ… Use this video",
558
+ key=f"demo_{idx}",
559
+ disabled=st.session_state.processing,
560
+ use_container_width=True
561
+ ):
562
+ st.session_state.selected_demo_video = demo["path"]
563
+ st.success(f"Selected!")
564
+
565
+ if st.session_state.selected_demo_video:
566
+ demo_video_path = st.session_state.selected_demo_video
567
+ st.success(f"βœ… Using demo video: **{os.path.basename(demo_video_path)}**")
568
+
569
+ # Text input with character counter
570
+ st.markdown("---")
571
+ st.markdown(f"**πŸ“ Enter Text in {language}**")
572
+ st.info("πŸ’‘ **Need help typing in Indian languages?** Use [this typing tool](https://www.easyhindityping.com/english-to-hindi-translation) to easily convert your text to the desired script.")
573
+
574
+ # Initialize session state for tracking
575
+ if "prev_text_length" not in st.session_state:
576
+ st.session_state.prev_text_length = 0
577
+
578
+ text_input = st.text_area(
579
+ f"Enter your text (Max {MAX_TEXT_LENGTH} characters)",
580
+ value="",
581
+ height=150,
582
+ disabled=st.session_state.processing,
583
+ key="text_input_area",
584
+ help="You can paste any length of text. We'll validate it when you submit.",
585
+ label_visibility="collapsed"
586
+ )
587
+
588
+ # Character count
589
+ char_count = len(text_input)
590
+
591
+ # Detect paste (large jump in character count)
592
+ if st.session_state.prev_text_length > 0:
593
+ char_diff = char_count - st.session_state.prev_text_length
594
+ if char_diff > 50: # Likely a paste operation
595
+ st.info(f"πŸ“‹ Pasted text detected ({char_diff} characters added)")
596
+
597
+ st.session_state.prev_text_length = char_count
598
+
599
+ # Real-time character counter with color coding
600
+ if char_count > MAX_TEXT_LENGTH:
601
+ excess = char_count - MAX_TEXT_LENGTH
602
+ st.error(f"❌ **Text exceeds limit by {excess} characters!** {char_count}/{MAX_TEXT_LENGTH} - Please trim before submitting")
603
+ elif char_count > MAX_TEXT_LENGTH - 20:
604
+ st.warning(f"⚠️ **Approaching limit:** {char_count}/{MAX_TEXT_LENGTH} characters")
605
+ elif char_count > 0:
606
+ st.info(f"✍️ **Characters:** {char_count}/{MAX_TEXT_LENGTH}")
607
+ else:
608
+ st.info(f"πŸ“ **Characters:** 0/{MAX_TEXT_LENGTH}")
609
+
610
+ # Submit button
611
+ st.markdown("---")
612
+ submit_btn = st.button(
613
+ "πŸš€ Generate Video",
614
+ type="primary",
615
+ disabled=st.session_state.processing
616
+ )
617
+
618
+ # Status area
619
+ status_placeholder = st.empty()
620
+ progress_placeholder = st.empty()
621
+
622
+ # Result area
623
+ video_placeholder = st.empty()
624
+ download_placeholder = st.empty()
625
+
626
+ # Show last generated video if available
627
+ if st.session_state.generated_path and os.path.exists(st.session_state.generated_path):
628
+ with video_placeholder.container():
629
+ st.success("βœ… Generated video available below:")
630
+ # Use columns to constrain video width
631
+ col1, col2, col3 = st.columns([1, 2, 1])
632
+ with col2:
633
+ st.video(st.session_state.generated_path, start_time=0)
634
+ with download_placeholder.container():
635
+ # Center the download button
636
+ col1, col2, col3 = st.columns([1, 2, 1])
637
+ with col2:
638
+ with open(st.session_state.generated_path, "rb") as f:
639
+ st.download_button(
640
+ "⬇️ Download Video",
641
+ data=f,
642
+ file_name="generated.mp4",
643
+ mime="video/mp4",
644
+ use_container_width=True
645
+ )
646
+
647
+ # Handle submission
648
+ if submit_btn:
649
+ # Validation
650
+ if video_source_option == "Upload Video" and not video_file:
651
+ st.error("❌ Please upload a video first.")
652
+ elif video_source_option == "Use Demo Video" and not demo_video_path:
653
+ st.error("❌ Please select a demo video first.")
654
+ elif voice_mode == "Clone Voice" and not st.session_state.reference_audio_path:
655
+ st.error("❌ Please provide a reference audio for voice cloning (5-300 seconds).")
656
+ elif not text_input.strip():
657
+ st.error("❌ Please enter text.")
658
+ elif len(text_input) > MAX_TEXT_LENGTH:
659
+ excess = len(text_input) - MAX_TEXT_LENGTH
660
+ st.error(f"❌ **Text exceeds limit by {excess} characters!** Current: {len(text_input)}/{MAX_TEXT_LENGTH}")
661
+ st.warning(f"πŸ’‘ **Tip:** Please trim your text to exactly {MAX_TEXT_LENGTH} characters or less before submitting.")
662
+ else:
663
+ # Check file size for uploaded videos
664
+ if video_source_option == "Upload Video":
665
+ is_valid, size_msg = check_file_size(video_file)
666
+ if not is_valid:
667
+ st.error(size_msg)
668
+ st.stop()
669
+
670
+ # Start processing
671
+ st.session_state.processing = True
672
+ status_placeholder.info("""🎬 Processing your video...
673
+ You will receive an email with the video link...""")
674
+ progress_placeholder.progress(0)
675
+
676
+ try:
677
+ # Determine video source
678
+ if video_source_option == "Upload Video":
679
+ video_source = video_file
680
+ filename = video_file.name
681
+ else:
682
+ video_source = demo_video_path
683
+ filename = os.path.basename(demo_video_path)
684
+
685
+ # Upload and process
686
+ generated_path, email_sent = stream_post_upload(
687
+ video_source=video_source,
688
+ filename=filename,
689
+ text=text_input,
690
+ voice_name=voice_name,
691
+ language=language,
692
+ email=st.session_state.email,
693
+ reference_audio_path=st.session_state.reference_audio_path
694
+ )
695
+
696
+ progress_placeholder.progress(100)
697
+
698
+ if generated_path and os.path.exists(generated_path):
699
+ st.session_state.generated_path = generated_path
700
+ status_placeholder.success("βœ… Video generated successfully!")
701
+
702
+ # Show email status
703
+ if email_sent:
704
+ st.info(f"πŸ“§ Download link sent to {st.session_state.email}")
705
+ else:
706
+ st.warning("⚠️ Video generated but email notification failed. Download below.")
707
+
708
+ # Update rate limit info
709
+ new_info = check_rate_limit(st.session_state.email)
710
+ if new_info:
711
+ st.session_state.rate_limit_info = new_info
712
+
713
+ # Display video
714
+ video_placeholder.empty()
715
+ download_placeholder.empty()
716
+
717
+ with video_placeholder.container():
718
+ st.success("βœ… Generated video:")
719
+ # Use columns to constrain video width
720
+ col1, col2, col3 = st.columns([1, 2, 1])
721
+ with col2:
722
+ st.video(generated_path, start_time=0)
723
+
724
+ with download_placeholder.container():
725
+ # Center the download button
726
+ col1, col2, col3 = st.columns([1, 2, 1])
727
+ with col2:
728
+ with open(generated_path, "rb") as f:
729
+ st.download_button(
730
+ "⬇️ Download Video",
731
+ data=f,
732
+ file_name="generated.mp4",
733
+ mime="video/mp4",
734
+ use_container_width=True
735
+ )
736
+
737
+ # Check if limit reached
738
+ if st.session_state.rate_limit_info.get('remaining', 0) == 0:
739
+ st.error("⚠️ You've reached your limit of 5 videos. Please use a different email to continue.")
740
+ else:
741
+ status_placeholder.error("❌ No video was generated.")
742
+
743
+ except TimeoutError as e:
744
+ status_placeholder.error("⏱️ Server Timeout")
745
+ st.error(f"Server is under heavy load. Please try again later.\n\n{str(e)}")
746
+
747
+ except ConnectionError as e:
748
+ status_placeholder.error("❌ Connection Error")
749
+ st.error(str(e))
750
+
751
+ except RuntimeError as e:
752
+ status_placeholder.error("❌ Request Failed")
753
+ st.error(str(e))
754
+
755
+ except Exception as e:
756
+ status_placeholder.error("❌ Unexpected Error")
757
+ st.error(f"An unexpected error occurred: {str(e)}")
758
+
759
+ finally:
760
+ st.session_state.processing = False
761
+ progress_placeholder.empty()
762
+
763
+ # Cleanup temporary audio file after successful generation
764
+ if st.session_state.reference_audio_path and os.path.exists(st.session_state.reference_audio_path):
765
+ try:
766
+ os.unlink(st.session_state.reference_audio_path)
767
+ st.session_state.reference_audio_path = None
768
+ st.session_state.reference_audio = None
769
+ except:
770
+ pass