salomonsky commited on
Commit
1912ecc
verified
1 Parent(s): b0b63c8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +134 -267
app.py CHANGED
@@ -3,20 +3,17 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Creador de Video con gTTS</title>
7
  <style>
8
  :root {
9
- --primary-color: #2c3e50;
10
- --secondary-color: #16a085;
11
  --background-color: #ecf0f1;
12
  --text-color: #34495e;
13
  --white-color: #ffffff;
14
  --border-color: #bdc3c7;
15
- --error-color: #e74c3c;
16
- --success-color: #27ae60;
17
- --disabled-color: #95a5a6;
18
  }
19
-
20
  body {
21
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
22
  background-color: var(--background-color);
@@ -25,310 +22,180 @@
25
  padding: 20px;
26
  display: flex;
27
  justify-content: center;
28
- align-items: flex-start;
29
  min-height: 100vh;
30
  }
31
-
32
  .container {
33
  width: 100%;
34
- max-width: 700px;
35
  background-color: var(--white-color);
36
- padding: 20px 30px 30px;
37
  border-radius: 12px;
38
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
39
- margin: 20px 0;
40
  }
41
-
42
  h1 {
43
  text-align: center;
44
  color: var(--primary-color);
45
  margin-bottom: 25px;
46
  }
47
-
48
- h2 {
49
- color: var(--secondary-color);
50
- border-bottom: 2px solid var(--secondary-color);
51
- padding-bottom: 5px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  margin-top: 20px;
53
- margin-bottom: 15px;
54
- font-size: 1.2em;
55
  }
56
-
57
- .controls { display: flex; flex-direction: column; gap: 15px; margin-bottom: 25px; }
58
- textarea, select { width: 100%; padding: 12px; border-radius: 6px; border: 1px solid var(--border-color); font-size: 16px; box-sizing: border-box; background-color: #fff; }
59
- textarea:focus, select:focus { outline: none; border-color: var(--secondary-color); box-shadow: 0 0 5px rgba(22, 160, 133, 0.5); }
60
- textarea { min-height: 100px; resize: vertical; }
61
-
62
- .file-label { display: block; background-color: #f8f9fa; border: 2px dashed var(--border-color); padding: 20px; text-align: center; border-radius: 6px; cursor: pointer; transition: background-color 0.3s, border-color 0.3s; }
63
- .file-label:hover { background-color: #e9ecef; border-color: var(--secondary-color); }
64
- input[type="file"] { display: none; }
65
- #file-count { margin-top: 10px; font-weight: bold; color: var(--primary-color); }
66
-
67
- .button-group { display: flex; flex-direction: column; gap: 10px; }
68
- #generate-btn, #download-btn { padding: 15px 25px; font-size: 18px; font-weight: bold; color: var(--white-color); border: none; border-radius: 6px; cursor: pointer; transition: background-color 0.3s, transform 0.2s; display: block; width: 100%; text-align: center; text-decoration: none; }
69
- #generate-btn { background-color: var(--secondary-color); }
70
- #download-btn { background-color: var(--success-color); }
71
- #generate-btn:hover:not(:disabled) { background-color: #117a65; transform: translateY(-2px); }
72
- #download-btn:hover:not(:disabled) { background-color: #229954; transform: translateY(-2px); }
73
- #generate-btn:disabled, #download-btn:disabled { background-color: var(--disabled-color); cursor: not-allowed; transform: none; }
74
-
75
- #video-container { margin: 20px auto 0; position: relative; overflow: hidden; border-radius: 8px; display: none; background-color: #000; }
76
- #video-container.aspect-16-9 { width: 100%; aspect-ratio: 16 / 9; }
77
- #video-container.aspect-9-16 { height: 65vh; max-height: 700px; aspect-ratio: 9 / 16; max-width: 100%; }
78
-
79
- #video-container img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; opacity: 0; transition: opacity 0.5s ease-in-out; }
80
- #video-container img.fit-cover { object-fit: cover; }
81
- #video-container img.fit-contain { object-fit: contain; }
82
- #video-container img.active { opacity: 1; }
83
-
84
- #status { text-align: center; margin-top: 15px; font-size: 16px; font-weight: 500; min-height: 24px; }
85
- #download-note { text-align: center; font-size: 0.8em; color: #7f8c8d; margin-top: 5px; }
86
-
87
  .status-error { color: var(--error-color); }
88
- .status-success { color: var(--success-color); }
89
- .status-processing { color: var(--secondary-color); }
90
-
91
- .kenburns-zoom { animation: 7s linear 1; }
92
- .zoom-tr { animation-name: zoom-tr; }
93
- .zoom-tl { animation-name: zoom-tl; }
94
-
95
- .kenburns-pan { animation: 7s linear 1; }
96
- .pan-down { animation-name: pan-down; }
97
- .pan-up { animation-name: pan-up; }
98
-
99
- @keyframes zoom-tr { 0% { transform: scale(1) translate(0, 0); } 100% { transform: scale(1.2) translate(-10%, 10%); } }
100
- @keyframes zoom-tl { 0% { transform: scale(1) translate(0, 0); } 100% { transform: scale(1.2) translate(10%, -10%); } }
101
- @keyframes pan-down { 0% { transform: translateY(-12%); } 100% { transform: translateY(12%); } }
102
- @keyframes pan-up { 0% { transform: translateY(12%); } 100% { transform: translateY(-12%); } }
103
-
104
  </style>
105
  </head>
106
  <body>
107
-
108
  <div class="container">
109
- <h1>Creador de Video con gTTS</h1>
110
-
111
- <div class="controls">
112
- <div>
113
- <h2>Paso 1: Escribe tu texto</h2>
114
- <textarea id="text-input" placeholder="Escribe el texto que se convertir谩 en audio aqu铆..."></textarea>
115
- </div>
116
- <div style="display: flex; gap: 15px;">
117
- <div style="flex: 1;">
118
- <h2>Paso 2: Idioma</h2>
119
- <select id="lang-select">
120
- <option value="es-MX" selected>Espa帽ol (M茅xico)</option>
121
- <option value="es-ES">Espa帽ol (Espa帽a)</option>
122
- <option value="en-US">Ingl茅s (USA)</option>
123
- </select>
124
- </div>
125
- <div style="flex: 1;">
126
- <h2>Paso 3: Formato</h2>
127
- <select id="aspect-ratio">
128
- <option value="9:16">9:16 (Vertical)</option>
129
- <option value="16:9">16:9 (Horizontal)</option>
130
- </select>
131
- </div>
132
- </div>
133
- <div>
134
- <h2>Paso 4: Sube tus im谩genes (1-20)</h2>
135
- <label for="image-input" class="file-label">
136
- <span>Haz clic para seleccionar</span>
137
- <div id="file-count">0 im谩genes seleccionadas</div>
138
- </label>
139
- <input type="file" id="image-input" multiple accept="image/*">
140
- </div>
141
- </div>
142
-
143
- <div class="button-group">
144
- <button id="generate-btn">Generar y Reproducir</button>
145
- <a id="download-btn" href="#" style="display: none;" disabled>Descargar Video</a>
146
- <p id="download-note" style="display: none;">Nota: El video descargado ser谩 silencioso. El audio de gTTS es para la vista previa.</p>
147
- </div>
148
-
149
  <div id="status"></div>
150
- <audio id="audio-player" style="display:none;"></audio>
151
- <div id="video-container"></div>
152
- <canvas id="hidden-canvas" style="display: none;"></canvas>
 
153
  </div>
154
 
155
  <script>
156
  const textInput = document.getElementById('text-input');
157
  const langSelect = document.getElementById('lang-select');
158
- const aspectRatioSelect = document.getElementById('aspect-ratio');
159
- const imageInput = document.getElementById('image-input');
160
  const generateBtn = document.getElementById('generate-btn');
161
- const downloadBtn = document.getElementById('download-btn');
162
- const downloadNote = document.getElementById('download-note');
163
- const videoContainer = document.getElementById('video-container');
164
  const statusDiv = document.getElementById('status');
165
- const fileCountDiv = document.getElementById('file-count');
166
- const canvas = document.getElementById('hidden-canvas');
167
- const audioPlayer = document.getElementById('audio-player');
168
-
169
- const SLIDE_ANIMATION_DURATION = 7000;
170
- const ZOOM_ANIMATIONS = ['zoom-tr', 'zoom-tl'];
171
- const PAN_ANIMATIONS = ['pan-down', 'pan-up'];
172
 
173
- let imageElements = [];
174
- let slideInterval = null;
175
- let recordingInterval = null;
176
- let currentImageIndex = 0;
177
- let mediaRecorder;
178
- let recordedChunks = [];
179
 
180
- imageInput.addEventListener('change', () => {
181
- const fileCount = imageInput.files.length;
182
- fileCountDiv.textContent = `${fileCount} ${fileCount === 1 ? 'imagen seleccionada' : 'im谩genes seleccionadas'}`;
183
- });
 
184
 
185
- const setStatus = (message, type = '') => {
186
- statusDiv.textContent = message;
187
- statusDiv.className = type ? `status-${type}` : '';
188
- };
 
 
 
 
 
 
189
 
190
- const stopAllProcesses = () => {
191
- audioPlayer.pause();
192
- if (slideInterval) clearInterval(slideInterval);
193
- if (recordingInterval) clearInterval(recordingInterval);
194
- if (mediaRecorder && mediaRecorder.state === 'recording') mediaRecorder.stop();
195
- slideInterval = null;
196
- recordingInterval = null;
197
- generateBtn.disabled = false;
198
- };
199
-
200
- const fetchAudioViaProxy = async (text, lang) => {
201
- setStatus('Obteniendo audio de gTTS...', 'processing');
202
  const gttsUrl = `https://translate.google.com/translate_tts?ie=UTF-8&q=${encodeURIComponent(text)}&tl=${lang}&client=tw-ob`;
203
  const proxyUrl = `https://api.allorigins.win/raw?url=${encodeURIComponent(gttsUrl)}`;
 
204
  try {
205
  const response = await fetch(proxyUrl);
206
- if (!response.ok) throw new Error(`Error en la red: ${response.statusText}`);
207
- const blob = await response.blob();
208
- return URL.createObjectURL(blob);
209
- } catch (error) {
210
- setStatus(`Error al obtener audio gTTS: ${error.message}. Intenta de nuevo.`, 'error');
211
- return null;
212
- }
213
- };
214
-
215
- const displayNextImage = () => {
216
- if (imageElements.length === 0) return;
217
- imageElements.forEach(img => img.classList.remove('active'));
218
-
219
- const activeIndex = currentImageIndex % imageElements.length;
220
- const currentImg = imageElements[activeIndex];
221
-
222
- const isHorizontal = currentImg.naturalWidth > currentImg.naturalHeight;
223
- let animationClass, fitClass;
224
-
225
- if (isHorizontal && aspectRatioSelect.value === '9:16') {
226
- fitClass = 'fit-contain';
227
- animationClass = 'kenburns-pan ' + PAN_ANIMATIONS[currentImageIndex % PAN_ANIMATIONS.length];
228
- } else {
229
- fitClass = 'fit-cover';
230
- animationClass = 'kenburns-zoom ' + ZOOM_ANIMATIONS[currentImageIndex % ZOOM_ANIMATIONS.length];
231
- }
232
 
233
- currentImg.className = `${fitClass} ${animationClass}`;
234
- setTimeout(() => currentImg.classList.add('active'), 50);
 
 
 
 
235
 
236
- currentImageIndex++;
237
- };
238
-
239
- const startSlideshow = (audioDuration) => {
240
- const numImages = imageElements.length;
241
- let slideDuration = audioDuration * 1000 / numImages;
242
- if (slideDuration < SLIDE_ANIMATION_DURATION) slideDuration = SLIDE_ANIMATION_DURATION;
243
 
244
- videoContainer.style.display = 'block';
245
- const aspectRatio = aspectRatioSelect.value;
246
- videoContainer.className = aspectRatio === '16:9' ? 'aspect-16-9' : 'aspect-9-16';
247
-
248
- currentImageIndex = 0;
249
- displayNextImage();
250
- slideInterval = setInterval(displayNextImage, slideDuration);
251
- };
252
-
253
- const startRecording = () => {
254
- const ctx = canvas.getContext('2d');
255
- const stream = canvas.captureStream(30);
256
- recordedChunks = [];
257
- mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm; codecs=vp9' });
258
- mediaRecorder.ondataavailable = event => {
259
- if (event.data.size > 0) recordedChunks.push(event.data);
260
- };
261
- mediaRecorder.onstop = () => {
262
- const blob = new Blob(recordedChunks, { type: 'video/webm' });
263
- const url = URL.createObjectURL(blob);
264
- downloadBtn.href = url;
265
- downloadBtn.download = 'video_generado.webm';
266
- downloadBtn.style.display = 'block';
267
- downloadNote.style.display = 'block';
268
- downloadBtn.disabled = false;
269
- setStatus('隆Video listo para descargar!', 'success');
270
- };
271
- mediaRecorder.start();
272
- recordingInterval = setInterval(() => {
273
- const activeImg = document.querySelector('#video-container img.active');
274
- if (activeImg) ctx.drawImage(activeImg, 0, 0, canvas.width, canvas.height);
275
- }, 1000 / 30);
276
- };
277
 
278
- generateBtn.addEventListener('click', async () => {
279
- stopAllProcesses();
280
- const text = textInput.value.trim();
281
- const files = imageInput.files;
282
 
283
- if (!text || files.length === 0 || files.length > 20) {
284
- setStatus('Revisa el texto y que hayas subido de 1 a 20 im谩genes.', 'error');
285
- return;
286
- }
287
-
288
- generateBtn.disabled = true;
289
- downloadBtn.style.display = 'none';
290
- downloadNote.style.display = 'none';
291
- downloadBtn.disabled = true;
292
-
293
- const audioUrl = await fetchAudioViaProxy(text, langSelect.value);
294
- if (!audioUrl) {
295
  generateBtn.disabled = false;
296
- return;
297
  }
298
-
299
- audioPlayer.src = audioUrl;
300
-
301
- setStatus('Preparando im谩genes...', 'processing');
302
- videoContainer.innerHTML = '';
303
- const imageLoadPromises = Array.from(files).map(file => {
304
- return new Promise(resolve => {
305
- const img = document.createElement('img');
306
- img.src = URL.createObjectURL(file);
307
- img.onload = () => resolve(img);
308
- videoContainer.appendChild(img);
309
- });
310
- });
311
-
312
- imageElements = await Promise.all(imageLoadPromises);
313
-
314
- audioPlayer.oncanplaythrough = () => {
315
- setStatus('Reproduciendo y grabando video...', 'processing');
316
- const [w, h] = aspectRatioSelect.value.split(':').map(Number);
317
- canvas.width = w === 9 ? 720 : 1280;
318
- canvas.height = w === 9 ? 1280 : 720;
319
- startSlideshow(audioPlayer.duration);
320
- startRecording();
321
- audioPlayer.play();
322
- };
323
-
324
- audioPlayer.onended = () => stopAllProcesses();
325
- audioPlayer.onerror = () => {
326
- setStatus('Error al cargar el audio.', 'error');
327
- stopAllProcesses();
328
- };
329
  });
330
-
331
- window.addEventListener('beforeunload', stopAllProcesses);
332
  </script>
333
  </body>
334
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Generador de MP3 con gTTS</title>
7
  <style>
8
  :root {
9
+ --primary-color: #34495e;
10
+ --secondary-color: #27ae60;
11
  --background-color: #ecf0f1;
12
  --text-color: #34495e;
13
  --white-color: #ffffff;
14
  --border-color: #bdc3c7;
15
+ --error-color: #c0392b;
 
 
16
  }
 
17
  body {
18
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
19
  background-color: var(--background-color);
 
22
  padding: 20px;
23
  display: flex;
24
  justify-content: center;
25
+ align-items: center;
26
  min-height: 100vh;
27
  }
 
28
  .container {
29
  width: 100%;
30
+ max-width: 600px;
31
  background-color: var(--white-color);
32
+ padding: 30px;
33
  border-radius: 12px;
34
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
 
35
  }
 
36
  h1 {
37
  text-align: center;
38
  color: var(--primary-color);
39
  margin-bottom: 25px;
40
  }
41
+ textarea, select {
42
+ width: 100%;
43
+ padding: 12px;
44
+ border-radius: 6px;
45
+ border: 1px solid var(--border-color);
46
+ font-size: 16px;
47
+ box-sizing: border-box;
48
+ margin-bottom: 20px;
49
+ }
50
+ textarea:focus, select:focus {
51
+ outline: none;
52
+ border-color: var(--secondary-color);
53
+ box-shadow: 0 0 5px rgba(39, 174, 96, 0.4);
54
+ }
55
+ textarea {
56
+ min-height: 120px;
57
+ resize: vertical;
58
+ }
59
+ #generate-btn {
60
+ padding: 15px 25px;
61
+ font-size: 18px;
62
+ font-weight: bold;
63
+ color: var(--white-color);
64
+ background-color: var(--secondary-color);
65
+ border: none;
66
+ border-radius: 6px;
67
+ cursor: pointer;
68
+ transition: background-color 0.3s, transform 0.2s;
69
+ display: block;
70
+ width: 100%;
71
+ }
72
+ #generate-btn:hover:not(:disabled) {
73
+ background-color: #229954;
74
+ transform: translateY(-2px);
75
+ }
76
+ #generate-btn:disabled {
77
+ background-color: #95a5a6;
78
+ cursor: not-allowed;
79
+ transform: none;
80
+ }
81
+ .output-area {
82
+ margin-top: 25px;
83
+ text-align: center;
84
+ }
85
+ #audio-preview {
86
+ width: 100%;
87
+ display: none;
88
+ }
89
+ #download-link {
90
+ display: none;
91
+ margin-top: 15px;
92
+ padding: 12px 20px;
93
+ background-color: #3498db;
94
+ color: white;
95
+ text-decoration: none;
96
+ border-radius: 6px;
97
+ font-weight: bold;
98
+ transition: background-color 0.3s;
99
+ }
100
+ #download-link:hover {
101
+ background-color: #2980b9;
102
+ }
103
+ #status {
104
+ text-align: center;
105
  margin-top: 20px;
106
+ font-weight: 500;
107
+ min-height: 24px;
108
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  .status-error { color: var(--error-color); }
110
+ .status-processing { color: var(--primary-color); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  </style>
112
  </head>
113
  <body>
 
114
  <div class="container">
115
+ <h1>Generador de MP3 con gTTS</h1>
116
+ <textarea id="text-input" placeholder="Escribe aqu铆 el texto para convertir a audio..."></textarea>
117
+ <select id="lang-select">
118
+ <option value="es-MX" selected>Espa帽ol (M茅xico)</option>
119
+ <option value="es-ES">Espa帽ol (Espa帽a)</option>
120
+ <option value="en-US">Ingl茅s (USA)</option>
121
+ <option value="fr-FR">Franc茅s (Francia)</option>
122
+ <option value="it-IT">Italiano (Italia)</option>
123
+ <option value="de-DE">Alem谩n (Alemania)</option>
124
+ </select>
125
+ <button id="generate-btn">Generar y Descargar MP3</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  <div id="status"></div>
127
+ <div class="output-area">
128
+ <audio id="audio-preview" controls></audio>
129
+ <a id="download-link" href="#" download="audio_gtts.mp3">Descargar Archivo MP3</a>
130
+ </div>
131
  </div>
132
 
133
  <script>
134
  const textInput = document.getElementById('text-input');
135
  const langSelect = document.getElementById('lang-select');
 
 
136
  const generateBtn = document.getElementById('generate-btn');
 
 
 
137
  const statusDiv = document.getElementById('status');
138
+ const audioPreview = document.getElementById('audio-preview');
139
+ const downloadLink = document.getElementById('download-link');
140
+
141
+ let currentAudioUrl = null;
 
 
 
142
 
143
+ generateBtn.addEventListener('click', async () => {
144
+ const text = textInput.value.trim();
145
+ const lang = langSelect.value;
 
 
 
146
 
147
+ if (!text) {
148
+ statusDiv.textContent = 'Por favor, escribe un texto.';
149
+ statusDiv.className = 'status-error';
150
+ return;
151
+ }
152
 
153
+ // Limpiar estado anterior
154
+ generateBtn.disabled = true;
155
+ statusDiv.textContent = 'Contactando con el servicio de gTTS...';
156
+ statusDiv.className = 'status-processing';
157
+ audioPreview.style.display = 'none';
158
+ downloadLink.style.display = 'none';
159
+ if (currentAudioUrl) {
160
+ URL.revokeObjectURL(currentAudioUrl);
161
+ currentAudioUrl = null;
162
+ }
163
 
164
+ // Construir las URLs
 
 
 
 
 
 
 
 
 
 
 
165
  const gttsUrl = `https://translate.google.com/translate_tts?ie=UTF-8&q=${encodeURIComponent(text)}&tl=${lang}&client=tw-ob`;
166
  const proxyUrl = `https://api.allorigins.win/raw?url=${encodeURIComponent(gttsUrl)}`;
167
+
168
  try {
169
  const response = await fetch(proxyUrl);
170
+ if (!response.ok) {
171
+ throw new Error(`Error de red. El servidor proxy respondi贸 con estado: ${response.status}`);
172
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
+ const blob = await response.blob();
175
+
176
+ // Google puede devolver un error como HTML. Verificamos que sea un audio.
177
+ if (!blob.type.startsWith('audio/')) {
178
+ throw new Error('La respuesta de gTTS no fue un archivo de audio. El texto podr铆a ser muy largo o contener caracteres no v谩lidos.');
179
+ }
180
 
181
+ currentAudioUrl = URL.createObjectURL(blob);
182
+
183
+ audioPreview.src = currentAudioUrl;
184
+ audioPreview.style.display = 'block';
 
 
 
185
 
186
+ downloadLink.href = currentAudioUrl;
187
+ downloadLink.style.display = 'inline-block';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
+ statusDiv.textContent = '隆Audio MP3 generado con 茅xito!';
190
+ statusDiv.className = '';
 
 
191
 
192
+ } catch (error) {
193
+ statusDiv.textContent = `Error: ${error.message}`;
194
+ statusDiv.className = 'status-error';
195
+ } finally {
 
 
 
 
 
 
 
 
196
  generateBtn.disabled = false;
 
197
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  });
 
 
199
  </script>
200
  </body>
201
  </html>