ujwal55 commited on
Commit
029f5c7
·
1 Parent(s): 95df8b8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +247 -61
app.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- Physics Chapter Video Generator
3
  Creates educational videos by combining title cards with relevant content.
4
  """
5
 
@@ -8,22 +8,89 @@ from pathlib import Path
8
  from typing import List, Optional
9
  import gradio as gr
10
  import random
11
- import urllib.request
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  # ---------------- CONFIG ----------------
14
- TITLE_DUR = 3
15
- SIZE = "1280x720"
16
  FPS = 30
17
- CRF = 28
18
- PRESET = "ultrafast"
19
- YT_MAX_RESULTS = 2
20
- MAX_VIDEO_LENGTH = 30
21
- MAX_TOPICS = 8
22
- PROXY_URL = "http://brd-customer-hl_0443f347-zone-freemium:[email protected]:33335"
23
  # ----------------------------------------
24
 
25
- print("CWD:", os.getcwd())
26
-
27
  # ---------- helpers ----------
28
  def run_cmd(cmd: list[str], timeout: int = 120) -> bool:
29
  """Run command with timeout and proper error handling"""
@@ -40,50 +107,98 @@ def run_cmd(cmd: list[str], timeout: int = 120) -> bool:
40
  print(f"Command failed:\n{' '.join(cmd)}\nError:\n{e.stderr if hasattr(e, 'stderr') else str(e)}")
41
  return False
42
 
43
- def youtube_search_with_proxy(query: str, max_results: int) -> List[str]:
44
- """Search YouTube using proxy via urllib + monkey-patching"""
45
  try:
46
- import ssl
 
 
 
47
  from youtube_search import YoutubeSearch
48
-
49
- proxy = PROXY_URL
50
- opener = urllib.request.build_opener(
51
- urllib.request.ProxyHandler({'http': proxy, 'https': proxy})
52
- )
53
- urllib.request.install_opener(opener)
54
-
55
- results = YoutubeSearch(query, max_results=max_results).to_dict()
56
- return ["https://www.youtube.com" + r["url_suffix"] for r in results]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  except Exception as e:
58
- print(f"YouTube search failed with proxy: {e}")
59
  return []
60
 
61
- def yt_urls(query: str, max_results: int) -> List[str]:
62
- return youtube_search_with_proxy(query, max_results)
63
 
64
  def safe_filename(name: str) -> str:
65
- return re.sub(r"[^\w\-\.]", "_", name)[:50]
 
 
66
 
67
  def dl_video(url: str, out: Path) -> bool:
68
- """Download video using yt-dlp with proxy"""
69
  out.parent.mkdir(exist_ok=True)
70
-
 
 
 
 
71
  cmd = [
72
  "yt-dlp",
73
  "--match-filter", f"duration<{MAX_VIDEO_LENGTH}",
74
- "-f", "mp4",
75
  "--merge-output-format", "mp4",
76
  "-o", str(out),
77
  "--no-playlist",
78
- "--proxy", PROXY_URL,
 
 
 
 
 
 
 
 
79
  url,
80
  ]
 
81
  return run_cmd(cmd, timeout=60)
82
 
 
83
  def make_card(text: str, out: Path, dur: int = TITLE_DUR) -> bool:
 
 
84
  wrapped = textwrap.wrap(text, width=25)
85
  safe_text = "\\n".join(w.replace("'", r"\\'") for w in wrapped)
86
 
 
87
  cmd = [
88
  "ffmpeg",
89
  "-loglevel", "error",
@@ -102,82 +217,115 @@ def make_card(text: str, out: Path, dur: int = TITLE_DUR) -> bool:
102
  return run_cmd(cmd)
103
 
104
  def extract_topics(text: str) -> List[str]:
 
105
  topics = []
106
  for line in text.splitlines():
107
  line = line.strip()
108
  if not line or len(topics) >= MAX_TOPICS:
109
  continue
 
 
110
  if re.match(r"^\d+[\.)]\s+.+", line):
111
- topics.append(re.sub(r"^\d+[\.)]\s*", "", line))
 
 
112
  elif re.match(r"^#+\s+.+", line):
113
- topics.append(re.sub(r"^#+\s*", "", line))
 
 
114
  elif line.isupper() and 3 <= len(line) <= 50:
115
  topics.append(line.title())
 
116
  elif len(line) > 3 and not line.startswith(('http', 'www')):
117
  topics.append(line)
 
118
  return topics[:MAX_TOPICS]
119
 
120
  def create_physics_video(chapter_text: str, progress=gr.Progress()) -> Optional[str]:
 
121
  if not chapter_text.strip():
122
  return None
123
 
124
  progress(0, desc="Extracting topics...")
125
  topics = extract_topics(chapter_text)
 
126
  if not topics:
127
  return None
128
-
 
129
  with tempfile.TemporaryDirectory() as temp_dir:
130
  temp_path = Path(temp_dir)
131
  concat_paths: List[Path] = []
132
-
133
- total_steps = len(topics) * 2 + 3
134
  current_step = 0
135
-
136
- progress(current_step / total_steps, desc="Creating opening card...")
 
137
  opening = temp_path / "00_opening.mp4"
138
  if make_card("Physics Chapter Overview", opening):
139
  concat_paths.append(opening)
140
  current_step += 1
141
-
 
142
  for idx, topic in enumerate(topics, 1):
 
143
  progress(current_step/total_steps, desc=f"Creating card for: {topic[:30]}...")
144
  card = temp_path / f"title_{idx:02d}.mp4"
145
  if make_card(topic, card):
146
  concat_paths.append(card)
147
  current_step += 1
148
-
 
149
  progress(current_step/total_steps, desc=f"Searching video for: {topic[:30]}...")
150
  video_found = False
151
- for url in yt_urls(f"{topic} physics explanation", YT_MAX_RESULTS):
 
 
 
 
 
 
152
  vid_id_match = re.search(r"(?:v=|be/|shorts/)([\w\-]{11})", url)
153
  if not vid_id_match:
154
  continue
 
155
  vid_path = temp_path / f"{safe_filename(vid_id_match.group(1))}.mp4"
156
  if dl_video(url, vid_path):
157
  concat_paths.append(vid_path)
158
  video_found = True
159
  break
160
-
161
  if not video_found:
 
162
  placeholder = temp_path / f"placeholder_{idx:02d}.mp4"
163
  if make_card(f"Exploring: {topic}", placeholder, dur=5):
164
  concat_paths.append(placeholder)
 
165
  current_step += 1
166
-
167
- progress(current_step / total_steps, desc="Creating closing card...")
 
168
  closing = temp_path / "zz_closing.mp4"
169
  if make_card("Thank you for learning!", closing):
170
  concat_paths.append(closing)
171
  current_step += 1
172
-
173
  if len(concat_paths) < 2:
174
  return None
175
-
 
176
  list_file = temp_path / "list.txt"
177
- list_file.write_text("".join(f"file '{p.absolute()}'\n" for p in concat_paths), encoding="utf-8")
 
 
 
 
 
178
  output_path = "physics_chapter_video.mp4"
179
-
180
- progress(current_step / total_steps, desc="Creating final video...")
 
181
  cmd = [
182
  "ffmpeg",
183
  "-loglevel", "error",
@@ -187,51 +335,89 @@ def create_physics_video(chapter_text: str, progress=gr.Progress()) -> Optional[
187
  "-movflags", "+faststart",
188
  "-y", output_path,
189
  ]
190
-
191
  if run_cmd(cmd, timeout=300):
192
  return output_path
 
193
  return None
194
 
195
  # Gradio Interface
196
  def create_interface():
 
197
  with gr.Blocks(title="Physics Video Generator", theme=gr.themes.Soft()) as app:
198
  gr.Markdown("""
199
  # Physics Video Generator
200
 
201
- Turn your physics topics into an engaging educational video.
 
 
 
 
 
202
  """)
 
203
  with gr.Row():
204
  with gr.Column():
205
  chapter_input = gr.Textbox(
206
  label="Chapter Topics",
207
- placeholder="1. Newton's First Law\n2. Force\n3. Momentum",
 
 
 
 
 
 
 
 
 
 
208
  lines=10,
209
  max_lines=15
210
  )
 
211
  generate_btn = gr.Button("Create Physics Video", variant="primary", size="lg")
 
212
  with gr.Column():
213
  video_output = gr.Video(label="Your Physics Video")
214
- gr.Markdown("Each video is limited to ~30 sec/topic.")
215
-
 
 
 
 
 
 
 
216
  generate_btn.click(
217
  fn=create_physics_video,
218
  inputs=[chapter_input],
219
  outputs=[video_output],
220
  show_progress=True
221
  )
222
-
 
223
  gr.Examples(
224
  examples=[
225
- ["1. Newton's First Law\n2. Newton's Second Law\n3. Newton's Third Law"],
226
- ["# Thermodynamics\n# Heat Transfer\n# Entropy"],
227
- ["QUANTUM MECHANICS\nWAVE-PARTICLE DUALITY\nUNCERTAINTY PRINCIPLE"]
 
228
  ],
229
  inputs=[chapter_input],
230
  label="Example Topics"
231
  )
 
232
  return app
233
 
234
  if __name__ == "__main__":
 
 
 
 
235
  app = create_interface()
236
- app.queue(max_size=3)
237
- app.launch(server_name="0.0.0.0", server_port=7860)
 
 
 
 
 
1
  """
2
+ Physics Chapter Video Generator - Improved Proxy Implementation
3
  Creates educational videos by combining title cards with relevant content.
4
  """
5
 
 
8
  from typing import List, Optional
9
  import gradio as gr
10
  import random
11
+ import requests
12
+ import time
13
+ from requests.adapters import HTTPAdapter
14
+ from urllib3.util.retry import Retry
15
+
16
+ print("CWD:", os.getcwd())
17
+ print("cookies.txt exists:", os.path.exists("./cookies/cookies.txt"))
18
+
19
+
20
+ # --- Oxylabs Proxy Configuration ---
21
+ PROXY_USERNAME = "ujwal_CmiMZ"
22
+ PROXY_PASSWORD = "xJv4DChht5P6y+u"
23
+ PROXY_COUNTRY = "US"
24
+
25
+ # List of proxy endpoints (Oxylabs DC Proxies)
26
+ PROXY_PORTS = [8001, 8002, 8003, 8004, 8005]
27
+ PROXY_HOST = "dc.oxylabs.io"
28
+
29
+ # User agents to rotate for better bot detection avoidance
30
+ USER_AGENTS = [
31
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
32
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
33
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0",
34
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"
35
+ ]
36
+
37
+ def get_random_proxy():
38
+ """Get a random proxy with proper formatting"""
39
+ port = random.choice(PROXY_PORTS)
40
+ return f"http://user-{PROXY_USERNAME}-country-{PROXY_COUNTRY}:{PROXY_PASSWORD}@{PROXY_HOST}:{port}"
41
+
42
+ def get_session_with_proxy():
43
+ """Create a requests session with proxy and anti-bot measures"""
44
+ session = requests.Session()
45
+
46
+ # Set up proxy
47
+ proxy_url = get_random_proxy()
48
+ session.proxies = {
49
+ "http": proxy_url,
50
+ "https": proxy_url,
51
+ }
52
+
53
+ # Set up headers to avoid bot detection
54
+ session.headers.update({
55
+ 'User-Agent': random.choice(USER_AGENTS),
56
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
57
+ 'Accept-Language': 'en-US,en;q=0.5',
58
+ 'Accept-Encoding': 'gzip, deflate',
59
+ 'Connection': 'keep-alive',
60
+ 'Upgrade-Insecure-Requests': '1',
61
+ 'Sec-Fetch-Dest': 'document',
62
+ 'Sec-Fetch-Mode': 'navigate',
63
+ 'Sec-Fetch-Site': 'none',
64
+ 'Cache-Control': 'max-age=0'
65
+ })
66
+
67
+ # Set up retry strategy
68
+ retry_strategy = Retry(
69
+ total=3,
70
+ backoff_factor=1,
71
+ status_forcelist=[429, 500, 502, 503, 504],
72
+ )
73
+ adapter = HTTPAdapter(max_retries=retry_strategy)
74
+ session.mount("http://", adapter)
75
+ session.mount("https://", adapter)
76
+
77
+ # Disable SSL warnings
78
+ session.verify = False
79
+
80
+ return session
81
+
82
 
83
  # ---------------- CONFIG ----------------
84
+ TITLE_DUR = 3 # seconds
85
+ SIZE = "1280x720" # resolution for cards
86
  FPS = 30
87
+ CRF = 28 # Higher CRF for smaller files in HF Spaces
88
+ PRESET = "ultrafast" # Fastest encoding for HF Spaces
89
+ YT_MAX_RESULTS = 2 # Reduced for faster processing
90
+ MAX_VIDEO_LENGTH = 30 # Max seconds per video clip
91
+ MAX_TOPICS = 8 # Limit topics for HF Spaces resources
 
92
  # ----------------------------------------
93
 
 
 
94
  # ---------- helpers ----------
95
  def run_cmd(cmd: list[str], timeout: int = 120) -> bool:
96
  """Run command with timeout and proper error handling"""
 
107
  print(f"Command failed:\n{' '.join(cmd)}\nError:\n{e.stderr if hasattr(e, 'stderr') else str(e)}")
108
  return False
109
 
110
+ def yt_urls(query: str, max_results: int) -> List[str]:
111
+ """Get YouTube URLs for search query via proxy with better bot avoidance"""
112
  try:
113
+ # Add random delay to avoid rate limiting
114
+ time.sleep(random.uniform(1, 3))
115
+
116
+ # Import here to avoid issues with monkey patching
117
  from youtube_search import YoutubeSearch
118
+
119
+ # Create session with proxy
120
+ session = get_session_with_proxy()
121
+
122
+ # Monkey-patch requests to use our session
123
+ original_get = requests.get
124
+ original_post = requests.post
125
+
126
+ def proxied_get(*args, **kwargs):
127
+ # Remove conflicting parameters
128
+ kwargs.pop('proxies', None)
129
+ kwargs.pop('verify', None)
130
+ kwargs.pop('headers', None)
131
+ return session.get(*args, **kwargs)
132
+
133
+ def proxied_post(*args, **kwargs):
134
+ kwargs.pop('proxies', None)
135
+ kwargs.pop('verify', None)
136
+ kwargs.pop('headers', None)
137
+ return session.post(*args, **kwargs)
138
+
139
+ # Apply monkey patch
140
+ requests.get = proxied_get
141
+ requests.post = proxied_post
142
+
143
+ try:
144
+ # Perform search with modified query to avoid detection
145
+ search_query = f"physics {query} education tutorial"
146
+ results = YoutubeSearch(search_query, max_results=max_results).to_dict()
147
+ urls = ["https://www.youtube.com" + r["url_suffix"] for r in results]
148
+ return urls
149
+ finally:
150
+ # Always restore original functions
151
+ requests.get = original_get
152
+ requests.post = original_post
153
+ session.close()
154
+
155
  except Exception as e:
156
+ print(f"YouTube search failed: {e}")
157
  return []
158
 
 
 
159
 
160
  def safe_filename(name: str) -> str:
161
+ """Create safe filename"""
162
+ return re.sub(r"[^\w\-\.]", "_", name)[:50] # Limit length
163
+
164
 
165
  def dl_video(url: str, out: Path) -> bool:
166
+ """Download video with length limit using rotating proxy and better headers"""
167
  out.parent.mkdir(exist_ok=True)
168
+ proxy = get_random_proxy()
169
+
170
+ # Add random delay before download
171
+ time.sleep(random.uniform(0.5, 2))
172
+
173
  cmd = [
174
  "yt-dlp",
175
  "--match-filter", f"duration<{MAX_VIDEO_LENGTH}",
176
+ "-f", "best[height<=720][ext=mp4]/best[ext=mp4]/best",
177
  "--merge-output-format", "mp4",
178
  "-o", str(out),
179
  "--no-playlist",
180
+ "--proxy", proxy,
181
+ "--user-agent", random.choice(USER_AGENTS),
182
+ "--referer", "https://www.google.com/",
183
+ "--add-header", "Accept-Language:en-US,en;q=0.9",
184
+ "--add-header", "Accept-Encoding:gzip, deflate, br",
185
+ "--socket-timeout", "30",
186
+ "--retries", "3",
187
+ "--fragment-retries", "3",
188
+ "--cookies", "./cookies/cookies.txt",
189
  url,
190
  ]
191
+
192
  return run_cmd(cmd, timeout=60)
193
 
194
+
195
  def make_card(text: str, out: Path, dur: int = TITLE_DUR) -> bool:
196
+ """Create title card with text"""
197
+ # Wrap text for better display
198
  wrapped = textwrap.wrap(text, width=25)
199
  safe_text = "\\n".join(w.replace("'", r"\\'") for w in wrapped)
200
 
201
+
202
  cmd = [
203
  "ffmpeg",
204
  "-loglevel", "error",
 
217
  return run_cmd(cmd)
218
 
219
  def extract_topics(text: str) -> List[str]:
220
+ """Extract topics from input text"""
221
  topics = []
222
  for line in text.splitlines():
223
  line = line.strip()
224
  if not line or len(topics) >= MAX_TOPICS:
225
  continue
226
+
227
+ # Match numbered lists
228
  if re.match(r"^\d+[\.)]\s+.+", line):
229
+ topic = re.sub(r"^\d+[\.)]\s*", "", line)
230
+ topics.append(topic)
231
+ # Match markdown headers
232
  elif re.match(r"^#+\s+.+", line):
233
+ topic = re.sub(r"^#+\s*", "", line)
234
+ topics.append(topic)
235
+ # Match all caps titles
236
  elif line.isupper() and 3 <= len(line) <= 50:
237
  topics.append(line.title())
238
+ # Match regular lines as topics
239
  elif len(line) > 3 and not line.startswith(('http', 'www')):
240
  topics.append(line)
241
+
242
  return topics[:MAX_TOPICS]
243
 
244
  def create_physics_video(chapter_text: str, progress=gr.Progress()) -> Optional[str]:
245
+ """Generate educational physics video from chapter topics"""
246
  if not chapter_text.strip():
247
  return None
248
 
249
  progress(0, desc="Extracting topics...")
250
  topics = extract_topics(chapter_text)
251
+
252
  if not topics:
253
  return None
254
+
255
+ # Create temporary directory
256
  with tempfile.TemporaryDirectory() as temp_dir:
257
  temp_path = Path(temp_dir)
258
  concat_paths: List[Path] = []
259
+
260
+ total_steps = len(topics) * 2 + 3 # topics * (card + video) + opening + closing + concat
261
  current_step = 0
262
+
263
+ # Opening card
264
+ progress(current_step/total_steps, desc="Creating opening card...")
265
  opening = temp_path / "00_opening.mp4"
266
  if make_card("Physics Chapter Overview", opening):
267
  concat_paths.append(opening)
268
  current_step += 1
269
+
270
+ # Process each topic
271
  for idx, topic in enumerate(topics, 1):
272
+ # Create title card
273
  progress(current_step/total_steps, desc=f"Creating card for: {topic[:30]}...")
274
  card = temp_path / f"title_{idx:02d}.mp4"
275
  if make_card(topic, card):
276
  concat_paths.append(card)
277
  current_step += 1
278
+
279
+ # Try to download video with delays between attempts
280
  progress(current_step/total_steps, desc=f"Searching video for: {topic[:30]}...")
281
  video_found = False
282
+
283
+ urls = yt_urls(f"{topic} physics explanation", YT_MAX_RESULTS)
284
+ for url_idx, url in enumerate(urls):
285
+ # Add delay between video download attempts
286
+ if url_idx > 0:
287
+ time.sleep(random.uniform(2, 5))
288
+
289
  vid_id_match = re.search(r"(?:v=|be/|shorts/)([\w\-]{11})", url)
290
  if not vid_id_match:
291
  continue
292
+
293
  vid_path = temp_path / f"{safe_filename(vid_id_match.group(1))}.mp4"
294
  if dl_video(url, vid_path):
295
  concat_paths.append(vid_path)
296
  video_found = True
297
  break
298
+
299
  if not video_found:
300
+ # Create a placeholder card if no video found
301
  placeholder = temp_path / f"placeholder_{idx:02d}.mp4"
302
  if make_card(f"Exploring: {topic}", placeholder, dur=5):
303
  concat_paths.append(placeholder)
304
+
305
  current_step += 1
306
+
307
+ # Closing card
308
+ progress(current_step/total_steps, desc="Creating closing card...")
309
  closing = temp_path / "zz_closing.mp4"
310
  if make_card("Thank you for learning!", closing):
311
  concat_paths.append(closing)
312
  current_step += 1
313
+
314
  if len(concat_paths) < 2:
315
  return None
316
+
317
+ # Create concat file
318
  list_file = temp_path / "list.txt"
319
+ list_file.write_text(
320
+ "".join(f"file '{p.absolute()}'\n" for p in concat_paths),
321
+ encoding="utf-8"
322
+ )
323
+
324
+ # Final output path
325
  output_path = "physics_chapter_video.mp4"
326
+
327
+ # Concatenate videos
328
+ progress(current_step/total_steps, desc="Creating final video...")
329
  cmd = [
330
  "ffmpeg",
331
  "-loglevel", "error",
 
335
  "-movflags", "+faststart",
336
  "-y", output_path,
337
  ]
338
+
339
  if run_cmd(cmd, timeout=300):
340
  return output_path
341
+
342
  return None
343
 
344
  # Gradio Interface
345
  def create_interface():
346
+ """Setup the web interface"""
347
  with gr.Blocks(title="Physics Video Generator", theme=gr.themes.Soft()) as app:
348
  gr.Markdown("""
349
  # Physics Video Generator
350
 
351
+ Transform your physics topics into engaging educational videos! This tool will:
352
+ - Create professional title slides for each topic
353
+ - Find relevant educational content
354
+ - Combine everything into a complete video
355
+
356
+ **How to use:** Enter your topics one per line, or use numbered lists, or markdown headers.
357
  """)
358
+
359
  with gr.Row():
360
  with gr.Column():
361
  chapter_input = gr.Textbox(
362
  label="Chapter Topics",
363
+ placeholder="""Enter topics like:
364
+ 1. Newton's Laws of Motion
365
+ 2. Force and Acceleration
366
+ 3. Momentum and Impulse
367
+ 4. Energy Conservation
368
+ 5. Circular Motion
369
+
370
+ Or:
371
+ # Kinematics
372
+ # Dynamics
373
+ # Thermodynamics""",
374
  lines=10,
375
  max_lines=15
376
  )
377
+
378
  generate_btn = gr.Button("Create Physics Video", variant="primary", size="lg")
379
+
380
  with gr.Column():
381
  video_output = gr.Video(label="Your Physics Video")
382
+
383
+ gr.Markdown("""
384
+ ### Important Notes:
385
+ - Processing typically takes 2-5 minutes
386
+ - Videos are optimized for educational use
387
+ - Limited to 8 topics per session
388
+ - Each video segment is capped at 30 seconds
389
+ """)
390
+
391
  generate_btn.click(
392
  fn=create_physics_video,
393
  inputs=[chapter_input],
394
  outputs=[video_output],
395
  show_progress=True
396
  )
397
+
398
+ # Examples
399
  gr.Examples(
400
  examples=[
401
+ ["1. Newton's First Law\n2. Newton's Second Law\n3. Newton's Third Law\n4. Applications of Newton's Laws"],
402
+ ["# Wave Motion\n# Sound Waves\n# Light Waves\n# Electromagnetic Spectrum"],
403
+ ["THERMODYNAMICS\nHEAT TRANSFER\nENTROPY\nCARNOT CYCLE"],
404
+ ["Quantum Mechanics Basics\nWave-Particle Duality\nHeisenberg Uncertainty Principle\nQuantum Tunneling"]
405
  ],
406
  inputs=[chapter_input],
407
  label="Example Topics"
408
  )
409
+
410
  return app
411
 
412
  if __name__ == "__main__":
413
+ # Disable SSL warnings
414
+ import urllib3
415
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
416
+
417
  app = create_interface()
418
+ app.queue(max_size=3) # Limit concurrent users for HF Spaces
419
+ app.launch(
420
+ share=False,
421
+ server_name="0.0.0.0",
422
+ server_port=7860
423
+ )