gnosticdev commited on
Commit
eba6bc8
·
verified ·
1 Parent(s): 53d884a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +221 -319
app.py CHANGED
@@ -1,335 +1,237 @@
1
- import os
2
- import asyncio
3
- import logging
4
- import tempfile
5
- import requests
6
- from datetime import datetime
7
- import edge_tts
8
  import gradio as gr
9
  import torch
10
  from transformers import GPT2Tokenizer, GPT2LMHeadModel
11
  from keybert import KeyBERT
12
- from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip
13
- import re
14
- import json
15
- import uuid
16
- import threading
17
- from queue import Queue
18
- import time
19
 
20
- # Configuración de logging
21
- logging.basicConfig(level=logging.INFO)
22
  logger = logging.getLogger(__name__)
23
 
24
- # Directorio persistente para archivos
25
- PERSIST_DIR = "persistent_data"
26
- os.makedirs(PERSIST_DIR, exist_ok=True)
27
-
28
- # Cola de procesamiento
29
- processing_queue = Queue()
30
- task_status = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
- # Clase para manejar tareas
33
- class VideoTask:
34
- def __init__(self, task_id, prompt_type, input_text, musica_file=None):
35
- self.task_id = task_id
36
- self.prompt_type = prompt_type
37
- self.input_text = input_text
38
- self.musica_file = musica_file
39
- self.status = "pending"
40
- self.progress = 0
41
- self.result = None
42
- self.error = None
43
- self.steps_completed = []
44
-
45
- def to_dict(self):
46
- return {
47
- "task_id": self.task_id,
48
- "status": self.status,
49
- "progress": self.progress,
50
- "result": self.result,
51
- "error": self.error,
52
- "steps_completed": self.steps_completed
53
- }
54
 
55
- # Worker thread para procesar videos
56
- def video_processor_worker():
57
- while True:
58
- try:
59
- task = processing_queue.get(timeout=1)
60
- if task is None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  break
62
-
63
- logger.info(f"Procesando tarea: {task.task_id}")
64
- process_video_task(task)
65
-
66
- except:
67
- time.sleep(0.1)
68
- continue
69
-
70
- # Iniciar worker thread
71
- worker_thread = threading.Thread(target=video_processor_worker, daemon=True)
72
- worker_thread.start()
73
-
74
- def save_task_state(task):
75
- """Guarda el estado de la tarea en un archivo JSON"""
76
- task_file = os.path.join(PERSIST_DIR, f"{task.task_id}.json")
77
- with open(task_file, 'w') as f:
78
- json.dump(task.to_dict(), f)
79
-
80
- def load_task_state(task_id):
81
- """Carga el estado de una tarea desde archivo"""
82
- task_file = os.path.join(PERSIST_DIR, f"{task_id}.json")
83
- if os.path.exists(task_file):
84
- with open(task_file, 'r') as f:
85
- return json.load(f)
86
- return None
87
-
88
- def process_video_task(task):
89
- """Procesa una tarea de video paso a paso"""
 
 
 
 
 
90
  try:
91
- task.status = "processing"
92
- task.progress = 0
93
- save_task_state(task)
94
-
95
- # Paso 1: Generar guión
96
- task.progress = 10
97
- save_task_state(task)
98
-
99
- if task.prompt_type == "Generar Guion con IA":
100
- guion = generate_script_simple(task.input_text)
101
- else:
102
- guion = task.input_text.strip()
103
-
104
- task.steps_completed.append("guion_generado")
105
- task.progress = 20
106
- save_task_state(task)
107
-
108
- # Paso 2: Generar audio TTS
109
- audio_path = os.path.join(PERSIST_DIR, f"{task.task_id}_audio.mp3")
110
- success = asyncio.run(text_to_speech_simple(guion, audio_path))
111
-
112
- if not success:
113
- raise Exception("Error generando audio")
114
-
115
- task.steps_completed.append("audio_generado")
116
- task.progress = 40
117
- save_task_state(task)
118
-
119
- # Paso 3: Buscar videos (simplificado)
120
- keywords = extract_keywords_simple(guion)
121
- video_urls = search_videos_simple(keywords)
122
-
123
- task.steps_completed.append("videos_encontrados")
124
- task.progress = 60
125
- save_task_state(task)
126
-
127
- # Paso 4: Crear video final (simplificado)
128
- output_path = os.path.join(PERSIST_DIR, f"{task.task_id}_final.mp4")
129
-
130
- # Simulación de creación de video
131
- # En producción, aquí iría tu lógica de moviepy
132
- create_simple_video(video_urls, audio_path, output_path, task.musica_file)
133
-
134
- task.steps_completed.append("video_creado")
135
- task.progress = 100
136
- task.status = "completed"
137
- task.result = output_path
138
- save_task_state(task)
139
-
140
  except Exception as e:
141
- logger.error(f"Error procesando tarea {task.task_id}: {str(e)}")
142
- task.status = "error"
143
- task.error = str(e)
144
- save_task_state(task)
145
-
146
- # Funciones simplificadas
147
- def generate_script_simple(prompt):
148
- """Versión simplificada de generación de guión"""
149
- # Aquí puedes usar tu lógica GPT-2 existente
150
- return f"Este es un video sobre {prompt}. Es fascinante y educativo."
151
-
152
- async def text_to_speech_simple(text, output_path):
153
- """TTS simplificado"""
154
- try:
155
- communicate = edge_tts.Communicate(text, "es-ES-JuanNeural")
156
- await communicate.save(output_path)
157
- return os.path.exists(output_path)
158
- except:
159
- return False
160
-
161
- def extract_keywords_simple(text):
162
- """Extracción simple de keywords"""
163
- words = text.lower().split()
164
- # Filtrar palabras comunes
165
- keywords = [w for w in words if len(w) > 4][:3]
166
- return keywords if keywords else ["nature", "video", "background"]
167
-
168
- def search_videos_simple(keywords):
169
- """Búsqueda simplificada de videos"""
170
- # Aquí iría tu lógica de Pexels
171
- # Por ahora retornamos URLs de ejemplo
172
- return ["video1.mp4", "video2.mp4"]
173
-
174
- def create_simple_video(video_urls, audio_path, output_path, music_path=None):
175
- """Creación simplificada de video"""
176
- # Aquí iría tu lógica de MoviePy
177
- # Por ahora creamos un archivo dummy
178
- with open(output_path, 'w') as f:
179
- f.write("dummy video content")
180
- time.sleep(2) # Simular procesamiento
181
-
182
- # Interfaz Gradio mejorada
183
- def submit_video_request(prompt_type, prompt_ia, prompt_manual, musica_file):
184
- """Envía una solicitud de video a la cola"""
185
- input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
186
-
187
- if not input_text or not input_text.strip():
188
- return None, "Por favor ingresa un texto"
189
-
190
- task_id = str(uuid.uuid4())
191
- task = VideoTask(task_id, prompt_type, input_text, musica_file)
192
-
193
- # Guardar estado inicial
194
- task_status[task_id] = task
195
- save_task_state(task)
196
-
197
- # Añadir a la cola
198
- processing_queue.put(task)
199
-
200
- return task_id, f"Tarea creada: {task_id}"
201
-
202
- def check_video_status(task_id):
203
- """Verifica el estado de una tarea"""
204
- if not task_id:
205
- return "No hay ID de tarea", None, None
206
-
207
- # Intentar cargar desde archivo
208
- task_data = load_task_state(task_id)
209
-
210
- if not task_data:
211
- return "Tarea no encontrada", None, None
212
-
213
- status = task_data['status']
214
- progress = task_data['progress']
215
-
216
- if status == "pending":
217
- return f"⏳ En cola... ({progress}%)", None, None
218
- elif status == "processing":
219
- steps = ", ".join(task_data['steps_completed'])
220
- return f"🔄 Procesando... ({progress}%) - Completado: {steps}", None, None
221
- elif status == "completed":
222
- video_path = task_data['result']
223
- if os.path.exists(video_path):
224
- return "✅ Video completado!", video_path, video_path
225
- else:
226
- return "❌ Video completado pero archivo no encontrado", None, None
227
- elif status == "error":
228
- return f"❌ Error: {task_data['error']}", None, None
229
-
230
- return "Estado desconocido", None, None
231
-
232
- # Interfaz Gradio
233
- with gr.Blocks(title="Generador de Videos con IA") as app:
234
- gr.Markdown("# 🎬 Generador de Videos con IA (Sistema de Cola)")
235
-
236
  with gr.Tabs():
237
- with gr.TabItem("Crear Video"):
238
- with gr.Row():
239
- with gr.Column():
240
- prompt_type = gr.Radio(
241
- ["Generar Guion con IA", "Usar Mi Guion"],
242
- label="Método de Entrada",
243
- value="Generar Guion con IA"
244
- )
245
-
246
- prompt_ia = gr.Textbox(
247
- label="Tema para IA",
248
- placeholder="Describe el tema del video..."
249
- )
250
-
251
- prompt_manual = gr.Textbox(
252
- label="Tu Guion Completo",
253
- placeholder="Escribe tu guion aquí...",
254
- visible=False
255
- )
256
-
257
- musica_input = gr.Audio(
258
- label="Música de fondo (opcional)",
259
- type="filepath"
260
- )
261
-
262
- submit_btn = gr.Button("📤 Enviar a Cola", variant="primary")
263
-
264
- with gr.Column():
265
- task_id_output = gr.Textbox(
266
- label="ID de Tarea",
267
- interactive=False
268
- )
269
- submit_status = gr.Textbox(
270
- label="Estado de Envío",
271
- interactive=False
272
- )
273
-
274
- with gr.TabItem("Verificar Estado"):
275
- with gr.Row():
276
- with gr.Column():
277
- task_id_input = gr.Textbox(
278
- label="ID de Tarea",
279
- placeholder="Pega aquí el ID de tu tarea..."
280
- )
281
- check_btn = gr.Button("🔍 Verificar Estado")
282
- auto_check = gr.Checkbox(
283
- label="Verificar automáticamente cada 5 segundos"
284
- )
285
-
286
- with gr.Column():
287
- status_output = gr.Textbox(
288
- label="Estado Actual",
289
- interactive=False
290
- )
291
- video_output = gr.Video(
292
- label="Video Generado",
293
- interactive=False
294
- )
295
- download_output = gr.File(
296
- label="Descargar Video",
297
- interactive=False
298
- )
299
-
300
- # Eventos
301
- prompt_type.change(
302
- lambda x: (
303
- gr.update(visible=x == "Generar Guion con IA"),
304
- gr.update(visible=x == "Usar Mi Guion")
305
- ),
306
- inputs=prompt_type,
307
- outputs=[prompt_ia, prompt_manual]
308
- )
309
-
310
- submit_btn.click(
311
- submit_video_request,
312
- inputs=[prompt_type, prompt_ia, prompt_manual, musica_input],
313
- outputs=[task_id_output, submit_status]
314
- )
315
-
316
- check_btn.click(
317
- check_video_status,
318
- inputs=[task_id_input],
319
- outputs=[status_output, video_output, download_output]
320
- )
321
-
322
- # Auto-check cada 5 segundos si está activado
323
- def auto_check_status(task_id, should_check):
324
- if should_check and task_id:
325
- return check_video_status(task_id)
326
- return gr.update(), gr.update(), gr.update()
327
-
328
- auto_check.change(
329
- lambda x: gr.update(visible=x),
330
- inputs=[auto_check],
331
- outputs=[check_btn]
332
  )
 
 
333
 
334
  if __name__ == "__main__":
335
- app.launch(server_name="0.0.0.0", server_port=7860)
 
1
+ import os, re, math, uuid, time, shutil, logging, tempfile, threading, requests, numpy as np
2
+ from datetime import datetime, timedelta
3
+ from collections import Counter
4
+
 
 
 
5
  import gradio as gr
6
  import torch
7
  from transformers import GPT2Tokenizer, GPT2LMHeadModel
8
  from keybert import KeyBERT
9
+ from TTS.api import TTS
10
+ from moviepy.editor import (
11
+ VideoFileClip, AudioFileClip, concatenate_videoclips, concatenate_audioclips,
12
+ CompositeAudioClip, AudioClip, TextClip, CompositeVideoClip, VideoClip, vfx
13
+ )
 
 
14
 
15
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
 
16
  logger = logging.getLogger(__name__)
17
 
18
+ PEXELS_API_KEY = os.getenv("PEXELS_API_KEY")
19
+ if not PEXELS_API_KEY:
20
+ raise RuntimeError("Debes definir PEXELS_API_KEY en Variables & secrets")
21
+
22
+ tokenizer = GPT2Tokenizer.from_pretrained("datificate/gpt2-small-spanish")
23
+ gpt2 = GPT2LMHeadModel.from_pretrained("datificate/gpt2-small-spanish").eval()
24
+ if tokenizer.pad_token is None:
25
+ tokenizer.pad_token = tokenizer.eos_token
26
+ kw_model = KeyBERT("distilbert-base-multilingual-cased")
27
+ tts_engine = TTS(model_name="tts_models/es/css10/vits", progress_bar=False, gpu=False)
28
+
29
+ RESULTS_DIR = "video_results"
30
+ os.makedirs(RESULTS_DIR, exist_ok=True)
31
+ TASKS = {}
32
+
33
+ # ───────── helpers ────────────────────────────────────────────────────────────────
34
+ def gpt2_script(prompt: str, mx: int = 160) -> str:
35
+ ins = f"Escribe un guion corto, interesante y coherente sobre: {prompt}"
36
+ inp = tokenizer(ins, return_tensors="pt", truncation=True, max_length=512)
37
+ out = gpt2.generate(
38
+ **inp, max_length=mx + inp["input_ids"].shape[1], do_sample=True,
39
+ top_p=0.9, top_k=40, temperature=0.7, no_repeat_ngram_size=3,
40
+ pad_token_id=tokenizer.pad_token_id, eos_token_id=tokenizer.eos_token_id,
41
+ )
42
+ txt = tokenizer.decode(out[0], skip_special_tokens=True)
43
+ return txt.split("sobre:")[-1].strip()[:mx]
44
 
45
+ def coqui_tts(text: str, path: str):
46
+ text = re.sub(r"[^\w\s.,!?áéíóúñÁÉÍÓÚÑ]", "", text)[:500]
47
+ tts_engine.tts_to_file(text=text, file_path=path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
+ def keywords(text: str) -> list[str]:
50
+ clean = re.sub(r"[^\w\sáéíóúñÁÉÍÓÚÑ]", "", text.lower())
51
+ try:
52
+ kws = kw_model.extract_keywords(clean, stop_words="spanish", top_n=5)
53
+ return [k.replace(" ", "+") for k, _ in kws if k]
54
+ except Exception:
55
+ words = [w for w in clean.split() if len(w) > 4]
56
+ return [w for w, _ in Counter(words).most_common(5)] or ["nature"]
57
+
58
+ def pexels_search(q: str, n: int) -> list[dict]:
59
+ r = requests.get(
60
+ "https://api.pexels.com/videos/search",
61
+ headers={"Authorization": PEXELS_API_KEY},
62
+ params={"query": q, "per_page": n, "orientation": "landscape"},
63
+ timeout=20,
64
+ )
65
+ r.raise_for_status()
66
+ return r.json().get("videos", [])
67
+
68
+ def download(url: str, folder: str) -> str | None:
69
+ name = uuid.uuid4().hex + ".mp4"
70
+ path = os.path.join(folder, name)
71
+ with requests.get(url, stream=True, timeout=60) as r:
72
+ r.raise_for_status()
73
+ with open(path, "wb") as f:
74
+ for chunk in r.iter_content(1024 * 1024):
75
+ f.write(chunk)
76
+ return path if os.path.getsize(path) > 1000 else None
77
+
78
+ def loop_audio(aclip: AudioFileClip, dur: float) -> AudioFileClip:
79
+ if aclip.duration >= dur:
80
+ return aclip.subclip(0, dur)
81
+ loops = math.ceil(dur / aclip.duration)
82
+ return concatenate_audioclips([aclip] * loops).subclip(0, dur)
83
+
84
+ def make_subs_clips(script: str, video_w: int, video_h: int, duration: float):
85
+ sentences = [s.strip() for s in re.split(r"[.!?¿¡]", script) if s.strip()]
86
+ total_words = sum(len(s.split()) for s in sentences) or 1
87
+ word_time = duration / total_words
88
+ clips, cursor = [], 0.0
89
+ for sent in sentences:
90
+ n_words = len(sent.split())
91
+ dur = n_words * word_time
92
+ txt_clip = (
93
+ TextClip(sent, fontsize=int(video_h * 0.05), color="white",
94
+ stroke_color="black", stroke_width=2, method="caption",
95
+ size=(int(video_w * 0.9), None))
96
+ .set_start(cursor)
97
+ .set_duration(dur)
98
+ .set_position(("center", video_h * 0.85))
99
+ )
100
+ clips.append(txt_clip)
101
+ cursor += dur
102
+ return clips
103
+
104
+ def make_grain_clip(size: tuple[int, int], duration: float):
105
+ w, h = size
106
+ def frame(_t):
107
+ noise = np.random.randint(0, 256, (h, w, 1), dtype=np.uint8)
108
+ return np.repeat(noise, 3, axis=2)
109
+ return VideoClip(frame, duration=duration).set_opacity(0.15)
110
+
111
+ # ───────── video builder ──────────────────────────────────────────────────────────
112
+ def build_video(text: str, gen_script: bool, music_fp: str | None) -> str:
113
+ tmp = tempfile.mkdtemp()
114
+ script = gpt2_script(text) if gen_script else text.strip()
115
+ voice_path = os.path.join(tmp, "voice.mp3")
116
+ coqui_tts(script, voice_path)
117
+ voice_clip = AudioFileClip(voice_path)
118
+ adur = voice_clip.duration
119
+
120
+ vids = []
121
+ for kw in keywords(script):
122
+ if len(vids) >= 8:
123
+ break
124
+ for v in pexels_search(kw, 2):
125
+ best = max(v["video_files"], key=lambda x: x["width"] * x["height"])
126
+ p = download(best["link"], tmp)
127
+ if p:
128
+ vids.append(p)
129
+ if len(vids) >= 8:
130
  break
131
+ if not vids:
132
+ raise RuntimeError("Sin vídeos disponibles")
133
+
134
+ segs, acc = [], 0
135
+ for path in vids:
136
+ if acc >= adur + 2:
137
+ break
138
+ clip = VideoFileClip(path)
139
+ seg = clip.subclip(0, min(8, clip.duration))
140
+ segs.append(seg)
141
+ acc += seg.duration
142
+ base = concatenate_videoclips(segs, method="chain")
143
+ if base.duration < adur:
144
+ loops = math.ceil(adur / base.duration)
145
+ base = concatenate_videoclips([base] * loops, method="chain")
146
+ base = base.subclip(0, adur)
147
+
148
+ if music_fp:
149
+ mclip = loop_audio(AudioFileClip(music_fp), adur).volumex(0.2)
150
+ audio = CompositeAudioClip([mclip, voice_clip])
151
+ else:
152
+ audio = voice_clip
153
+
154
+ subs = make_subs_clips(script, base.w, base.h, adur)
155
+ grain = make_grain_clip((base.w, base.h), adur)
156
+ final_vid = CompositeVideoClip([base, grain, *subs]).set_audio(audio)
157
+
158
+ out_path = os.path.join(tmp, "final.mp4")
159
+ final_vid.write_videofile(out_path, fps=24, codec="libx264", audio_codec="aac", logger=None)
160
+ return out_path
161
+
162
+ # ───────── async tasks ────────────────────────────────────────────────────────────
163
+ def worker(tid: str, mode: str, topic: str, user_script: str, music: str | None):
164
  try:
165
+ txt = topic if mode == "Generar Guion con IA" else user_script
166
+ res_tmp = build_video(txt, mode == "Generar Guion con IA", music)
167
+ final_path = os.path.join(RESULTS_DIR, f"{tid}.mp4")
168
+ shutil.copy2(res_tmp, final_path)
169
+ TASKS[tid] = {"status": "done", "result": final_path, "ts": datetime.utcnow()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  except Exception as e:
171
+ TASKS[tid] = {"status": "error", "error": str(e), "ts": datetime.utcnow()}
172
+
173
+ def submit(mode, topic, user_script, music):
174
+ content = topic if mode == "Generar Guion con IA" else user_script
175
+ if not content.strip():
176
+ return "", "Ingresa texto"
177
+ tid = uuid.uuid4().hex[:8]
178
+ TASKS[tid] = {"status": "processing", "ts": datetime.utcnow()}
179
+ threading.Thread(target=worker, args=(tid, mode, topic, user_script, music), daemon=True).start()
180
+ return tid, f"Tarea {tid} creada"
181
+
182
+ def check(tid):
183
+ if tid not in TASKS:
184
+ return None, None, "ID inválido"
185
+ info = TASKS[tid]
186
+ stat = info["status"]
187
+ if stat == "processing":
188
+ return None, None, "Procesando..."
189
+ if stat == "error":
190
+ return None, None, f"Error: {info['error']}"
191
+ return info["result"], info["result"], "Vídeo listo 🎉"
192
+
193
+ # ───────── janitor thread ─────────────────────────────────────────────────────────
194
+ def janitor():
195
+ while True:
196
+ now = datetime.utcnow()
197
+ for fname in os.listdir(RESULTS_DIR):
198
+ fpath = os.path.join(RESULTS_DIR, fname)
199
+ try:
200
+ mtime = datetime.utcfromtimestamp(os.path.getmtime(fpath))
201
+ if now - mtime > timedelta(hours=24):
202
+ os.remove(fpath)
203
+ for k, v in list(TASKS.items()):
204
+ if v.get("result") == fpath:
205
+ del TASKS[k]
206
+ except Exception:
207
+ pass
208
+ time.sleep(3600)
209
+
210
+ threading.Thread(target=janitor, daemon=True).start()
211
+
212
+ # ───────── gradio ui ─────────────────────────────────────────────────────────────
213
+ with gr.Blocks(title="Generador de Vídeos IA") as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  with gr.Tabs():
215
+ with gr.TabItem("Crear Vídeo"):
216
+ mode = gr.Radio(["Generar Guion con IA", "Usar Mi Guion"], value="Generar Guion con IA")
217
+ topic = gr.Textbox(label="Tema")
218
+ user_script = gr.Textbox(label="Guion Completo", visible=False)
219
+ music = gr.Audio(type="filepath", label="Música (opcional)")
220
+ btn = gr.Button("Generar")
221
+ tid_out = gr.Textbox(label="ID de tarea")
222
+ msg = gr.Textbox(label="Estado")
223
+ with gr.TabItem("Revisar Estado"):
224
+ tid_in = gr.Textbox(label="ID de tarea")
225
+ chk = gr.Button("Verificar")
226
+ vid = gr.Video()
227
+ dlf = gr.File()
228
+
229
+ mode.change(
230
+ lambda m: (gr.update(visible=m == "Generar Guion con IA"), gr.update(visible=m != "Generar Guion con IA")),
231
+ mode, [topic, user_script]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  )
233
+ btn.click(submit, [mode, topic, user_script, music], [tid_out, msg])
234
+ chk.click(check, tid_in, [vid, dlf, msg])
235
 
236
  if __name__ == "__main__":
237
+ demo.launch()