Cristobal299 commited on
Commit
8bda87b
·
verified ·
1 Parent(s): 1c5478a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +60 -90
app.py CHANGED
@@ -1,7 +1,7 @@
1
  # -*- coding: utf-8 -*-
2
  """
3
  Centralita Torá — HuggingFace Space (Gradio)
4
- Leer hebreo + español · escuchar (Canto Gregoriano-Hebraico con Voz Divina Especial)
5
  """
6
  import os, glob, json, tempfile, html, asyncio, subprocess, shutil, re
7
  import urllib.request
@@ -14,15 +14,11 @@ except Exception:
14
 
15
  from gradio_theme_fenix import fenix_theme, FENIX_CSS
16
 
17
- # --- CONFIGURACIÓN DE VOZ Y CANTO ---
18
- CANTO_GREGORIANO_HEBRAICO = True
19
-
20
  VOZ_DIVINA = "es-ES-AlvaroNeural"
21
- VOZ_NARRADOR = "es-ES-AlvaroNeural" # Usamos Álvaro con procesamiento acústico sagrado especial
22
-
23
- VOZ_HE = "he-IL-AvriNeural"
24
 
25
- # Perillas de entonación para Narrador
26
  VOZ_RATE = "-10%"
27
  VOZ_PITCH = "+5Hz"
28
 
@@ -37,50 +33,16 @@ GROQ_URL = "https://api.groq.com/openai/v1/chat/completions"
37
  GROQ_KEY = os.environ.get("GROQ_API_KEY")
38
 
39
 
40
- # --- MOTOR FONÉTICO GREGORIANO-HEBRAICO ---
41
- def transformar_a_canto_gregoriano_hebraico(texto_sagrado: str) -> str:
42
- if not texto_sagrado:
43
- return ""
44
-
45
- texto = texto_sagrado
46
-
47
- # Melismas gregorianos en vocales tónicas
48
- melismas = {
49
-
50
- 'ó': 'o-o-o-o', 'ú': 'u-u-u-u',
51
-
52
- 'Ó': 'O-o-o-o', 'Ú': 'U-u-u-u',
53
- }
54
- for vocal_con_tilde, melisma in melismas.items():
55
- texto = texto.replace(vocal_con_tilde, melisma)
56
-
57
- # Cierres litúrgicos
58
- def cierre_liturgico(match):
59
- vocal = match.group(1)
60
- puntuacion = match.group(2)
61
- return f"{vocal}-{vocal}-{vocal}{puntuacion} ... "
62
-
63
- texto = re.sub(r'([aeiouAEIOU])([,.:;])', cierre_liturgico, texto)
64
-
65
- # Énfasis místico
66
- texto = re.sub(r'\bDios\b', 'Di-o-o-os', texto, flags=re.IGNORECASE)
67
- texto = re.sub(r'\bSeñor\b', 'Se-ñoo-or', texto, flags=re.IGNORECASE)
68
- texto = re.sub(r'\bIsrael\b', 'Is-ra-e-el', texto, flags=re.IGNORECASE)
69
-
70
- return texto
71
-
72
-
73
- # --- PROCESAMIENTO ACÚSTICO FFMEPG ---
74
  def procesar_audio_narrador(ruta):
75
  """Efecto catedral estándar para el narrador."""
76
  if not shutil.which("ffmpeg"):
77
  return ruta
78
 
79
  filtros = [
80
- "atempo=0.22",
81
  "bass=g=8:f=133",
82
  "treble=g=-22",
83
- "vibrato=f=3.5:d=0.55",
84
  "aecho=0.75:0.70:120|240:0.4|0.25"
85
  ]
86
  salida = ruta[:-4] + "_narrador.mp3"
@@ -97,18 +59,18 @@ def procesar_audio_narrador(ruta):
97
  def procesar_audio_divino(ruta):
98
  """
99
  Efecto ACÚSTICO SAGRADO Y PROFUNDO para la Voz de Dios.
100
- Tono más grave, resonancia sub-bass, eco más amplio y pureza vocal.
101
  """
102
  if not shutil.which("ffmpeg"):
103
  return ruta
104
 
105
  filtros = [
106
- "atempo=0.80", # Más pausado y solemne
107
- "asetrate=44100*0.45", # Bajar tono (más grave y imponente)
108
- "aresample=144000", # Reajustar sample rate
109
- "bass=g=15:f=200", # Graves profundos sub-bass
110
- "treble=g=-10", # Sonido cálido, sin aristas agudas
111
-
112
  ]
113
  salida = ruta[:-4] + "_divino.mp3"
114
  cmd = ["ffmpeg", "-y", "-i", ruta, "-af", ", ".join(filtros), "-ac", "2", salida]
@@ -163,11 +125,10 @@ def mezclar_fondo(voz_path):
163
  return voz_path
164
 
165
 
166
- # --- SEPARACIÓN DE BLOQUES (NARRADOR vs DIOS) ---
167
  def segmentar_texto_divino(texto):
168
  """
169
- Detecta patrones como 'Y dijo Dios: ...' o 'Dijo Dios ...'
170
- y divide el texto en partes ('narrador' o 'dios').
171
  """
172
  patron = r'((?:Y\s+dijo\s+Dios|dijo\s+Dios|Y\s+llamó\s+Dios|Y\s+bendijo\s+Dios)[^:,.–—]*[:,.–—]?\s*)([^.\n]+)'
173
  bloques = []
@@ -178,8 +139,8 @@ def segmentar_texto_divino(texto):
178
  if start > ultimo_idx:
179
  bloques.append(("narrador", texto[ultimo_idx:start]))
180
 
181
- intro_dios = match.group(1) # Ej: "Y dijo Dios: "
182
- palabras_dios = match.group(2) # Ej: "Sea la luz"
183
 
184
  bloques.append(("narrador", intro_dios))
185
  bloques.append(("dios", palabras_dios))
@@ -191,17 +152,15 @@ def segmentar_texto_divino(texto):
191
  return bloques if bloques else [("narrador", texto)]
192
 
193
 
194
- # --- GENERACIÓN DE AUDIO DINÁMICA ---
195
  async def generar_bloque_audio(texto, es_divino):
196
- texto_tts = transformar_a_canto_gregoriano_hebraico(texto) if CANTO_GREGORIANO_HEBRAICO else texto
197
  ruta = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False).name
198
-
199
  voz = VOZ_DIVINA if es_divino else VOZ_NARRADOR
200
  pitch = "-5Hz" if es_divino else VOZ_PITCH
201
  rate = "-9%" if es_divino else VOZ_RATE
202
 
203
  try:
204
- com = edge_tts.Communicate(texto_tts[:4000], voz, rate=rate, pitch=pitch)
 
205
  await com.save(ruta)
206
  except Exception:
207
  return None
@@ -212,20 +171,10 @@ async def generar_bloque_audio(texto, es_divino):
212
  return procesar_audio_narrador(ruta)
213
 
214
 
215
- async def a_voz(texto, idioma):
216
  if not texto or not texto.strip():
217
  return None
218
 
219
- if idioma == "he":
220
- ruta = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False).name
221
- try:
222
- com = edge_tts.Communicate(texto[:4000], VOZ_HE, rate=VOZ_RATE, pitch=VOZ_PITCH)
223
- await com.save(ruta)
224
- return mezclar_fondo(ruta)
225
- except Exception:
226
- return None
227
-
228
- # Procesamiento para Español con Voz Divina Diferenciada
229
  bloques = segmentar_texto_divino(texto)
230
  audios_segmentos = []
231
 
@@ -243,17 +192,47 @@ async def a_voz(texto, idioma):
243
  return audio_final
244
 
245
 
246
- async def leer_es(t):
247
- return await a_voz(t, "es")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
 
 
 
249
 
250
- async def leer_he(t):
251
- return await a_voz(t, "he")
 
 
 
 
 
 
 
 
 
 
 
 
252
 
253
 
254
  # --- CARGA Y MANEJO DE LIBROS ---
255
- # Nombres de libros en español, para mostrar lindo en el desplegable
256
- # aunque el archivo se llame en inglés (Genesis.json, Exodus.json, etc.)
257
  NOMBRES_ES = {
258
  "Genesis": "Génesis", "Exodus": "Éxodo", "Leviticus": "Levítico",
259
  "Numbers": "Números", "Deuteronomy": "Deuteronomio", "Joshua": "Josué",
@@ -271,13 +250,6 @@ NOMBRES_ES = {
271
 
272
 
273
  def _convertir_traduccion_plana(book_id: str, data: dict) -> dict:
274
- """
275
- Convierte el formato plano que genera el traductor
276
- ({"translation": "...", "missing_strong_numbers": [...]})
277
- a la estructura de capítulos/versículos que usa la Centralita.
278
- No tenemos separación real de capítulos en este formato, así que
279
- todo el libro se muestra como Capítulo 1, con una línea = un versículo.
280
- """
281
  texto = data.get("translation", "") or ""
282
  lineas = [l.strip() for l in texto.split("\n") if l.strip()]
283
  versiculos = [{"n": i + 1, "he": "", "es": linea} for i, linea in enumerate(lineas)]
@@ -297,11 +269,9 @@ def cargar_libros():
297
  d = json.load(f)
298
  book_id = os.path.splitext(os.path.basename(ruta))[0]
299
  if "capitulos" in d:
300
- # Formato completo ya listo (id/es/heb/capitulos)
301
  lid = d.get("id") or book_id
302
  libros[lid] = d
303
  elif "translation" in d:
304
- # Formato plano del traductor (translation + missing_strong_numbers)
305
  libros[book_id] = _convertir_traduccion_plana(book_id, d)
306
  else:
307
  print(f"Formato no reconocido en {ruta}, se omite.")
@@ -388,7 +358,7 @@ with gr.Blocks(theme=fenix_theme(), css=FENIX_CSS, title="Centralita Torá") as
388
  st_cap = gr.State(1)
389
 
390
  gr.HTML("<div id='cabecera'><div class='estrella'>✡</div>"
391
- "<h1>Centralita Torá</h1><p>hebreo y español · canto Trope con voz celectial diferenciada</p></div>")
392
 
393
  with gr.Tab("Leer"):
394
  with gr.Row():
@@ -397,8 +367,8 @@ with gr.Blocks(theme=fenix_theme(), css=FENIX_CSS, title="Centralita Torá") as
397
  ver_btn = gr.Button("Ver capítulo", variant="primary")
398
  salida = gr.HTML()
399
  with gr.Row():
400
- b_es = gr.Button("🔊 Canto Trope + Voz celectial", variant="secondary")
401
- b_he = gr.Button("🔊 עברית (Hebreo)", variant="secondary")
402
  audio = gr.Audio(label="Audio Litúrgico", autoplay=True)
403
 
404
  def ver(nombre, cap):
@@ -407,7 +377,7 @@ with gr.Blocks(theme=fenix_theme(), css=FENIX_CSS, title="Centralita Torá") as
407
 
408
  ver_btn.click(ver, [dd, ncap], [salida, st_es, st_he, st_nombre, st_cap])
409
  b_es.click(leer_es, st_es, audio)
410
- b_he.click(leer_he, st_he, audio)
411
 
412
  with gr.Tab("Buscar"):
413
  q = gr.Textbox(label="Palabra o frase (hebreo o español)", placeholder="ej. luz / אור")
 
1
  # -*- coding: utf-8 -*-
2
  """
3
  Centralita Torá — HuggingFace Space (Gradio)
4
+ Leer hebreo + español · escuchar (Canto Trope Real y Voz Divina Especial en Español)
5
  """
6
  import os, glob, json, tempfile, html, asyncio, subprocess, shutil, re
7
  import urllib.request
 
14
 
15
  from gradio_theme_fenix import fenix_theme, FENIX_CSS
16
 
17
+ # --- CONFIGURACIÓN DE VOZ Y AUDIO ---
 
 
18
  VOZ_DIVINA = "es-ES-AlvaroNeural"
19
+ VOZ_NARRADOR = "es-ES-AlvaroNeural"
 
 
20
 
21
+ # Perillas de entonación para Narrador en Español
22
  VOZ_RATE = "-10%"
23
  VOZ_PITCH = "+5Hz"
24
 
 
33
  GROQ_KEY = os.environ.get("GROQ_API_KEY")
34
 
35
 
36
+ # --- PROCESAMIENTO ACÚSTICO FFMEPG (ESPAÑOL) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  def procesar_audio_narrador(ruta):
38
  """Efecto catedral estándar para el narrador."""
39
  if not shutil.which("ffmpeg"):
40
  return ruta
41
 
42
  filtros = [
43
+ "atempo=0.90",
44
  "bass=g=8:f=133",
45
  "treble=g=-22",
 
46
  "aecho=0.75:0.70:120|240:0.4|0.25"
47
  ]
48
  salida = ruta[:-4] + "_narrador.mp3"
 
59
  def procesar_audio_divino(ruta):
60
  """
61
  Efecto ACÚSTICO SAGRADO Y PROFUNDO para la Voz de Dios.
62
+ Tono más grave, resonancia sub-bass, eco más amplio.
63
  """
64
  if not shutil.which("ffmpeg"):
65
  return ruta
66
 
67
  filtros = [
68
+ "atempo=0.85",
69
+ "asetrate=44100*0.85",
70
+ "aresample=44100",
71
+ "bass=g=15:f=200",
72
+ "treble=g=-10",
73
+ "aecho=0.8:0.8:150|300:0.5|0.3"
74
  ]
75
  salida = ruta[:-4] + "_divino.mp3"
76
  cmd = ["ffmpeg", "-y", "-i", ruta, "-af", ", ".join(filtros), "-ac", "2", salida]
 
125
  return voz_path
126
 
127
 
128
+ # --- SEPARACIÓN DE BLOQUES (NARRADOR vs DIOS) PARA ESPAÑOL ---
129
  def segmentar_texto_divino(texto):
130
  """
131
+ Detecta patrones para dividir el texto entre 'narrador' y 'dios'.
 
132
  """
133
  patron = r'((?:Y\s+dijo\s+Dios|dijo\s+Dios|Y\s+llamó\s+Dios|Y\s+bendijo\s+Dios)[^:,.–—]*[:,.–—]?\s*)([^.\n]+)'
134
  bloques = []
 
139
  if start > ultimo_idx:
140
  bloques.append(("narrador", texto[ultimo_idx:start]))
141
 
142
+ intro_dios = match.group(1)
143
+ palabras_dios = match.group(2)
144
 
145
  bloques.append(("narrador", intro_dios))
146
  bloques.append(("dios", palabras_dios))
 
152
  return bloques if bloques else [("narrador", texto)]
153
 
154
 
 
155
  async def generar_bloque_audio(texto, es_divino):
 
156
  ruta = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False).name
 
157
  voz = VOZ_DIVINA if es_divino else VOZ_NARRADOR
158
  pitch = "-5Hz" if es_divino else VOZ_PITCH
159
  rate = "-9%" if es_divino else VOZ_RATE
160
 
161
  try:
162
+ # El texto se pasa limpio, sin corrupciones en las vocales.
163
+ com = edge_tts.Communicate(texto[:4000], voz, rate=rate, pitch=pitch)
164
  await com.save(ruta)
165
  except Exception:
166
  return None
 
171
  return procesar_audio_narrador(ruta)
172
 
173
 
174
+ async def leer_es(texto):
175
  if not texto or not texto.strip():
176
  return None
177
 
 
 
 
 
 
 
 
 
 
 
178
  bloques = segmentar_texto_divino(texto)
179
  audios_segmentos = []
180
 
 
192
  return audio_final
193
 
194
 
195
+ # --- REPRODUCTOR DE HEBREO LITÚRGICO REAL (TROPE) ---
196
+ async def leer_he(nombre_espanol, capitulo):
197
+ libro_id = id_por_nombre(nombre_espanol)
198
+
199
+ if not libro_id:
200
+ return None
201
+
202
+ MAPEO_TORAH = {
203
+ "Genesis": "01",
204
+ "Exodus": "02",
205
+ "Leviticus": "03",
206
+ "Numbers": "04",
207
+ "Deuteronomy": "05"
208
+ }
209
+
210
+ carpeta_audios = "audios_torah"
211
+ if not os.path.exists(carpeta_audios):
212
+ os.makedirs(carpeta_audios)
213
+
214
+ ruta_local = os.path.join(carpeta_audios, f"{libro_id}_{capitulo}.mp3")
215
 
216
+ if os.path.exists(ruta_local):
217
+ return mezclar_fondo(ruta_local)
218
 
219
+ if libro_id in MAPEO_TORAH:
220
+ id_mechon = MAPEO_TORAH[libro_id]
221
+ cap_formateado = f"{int(capitulo):02d}"
222
+ url_audio = f"https://mechon-mamre.org/mp3/t{id_mechon}{cap_formateado}.mp3"
223
+
224
+ try:
225
+ print(f"Descargando canto real con Trope desde: {url_audio}")
226
+ urllib.request.urlretrieve(url_audio, ruta_local)
227
+ return mezclar_fondo(ruta_local)
228
+ except Exception as e:
229
+ print(f"Error descargando el audio: {e}")
230
+ return None
231
+
232
+ return None
233
 
234
 
235
  # --- CARGA Y MANEJO DE LIBROS ---
 
 
236
  NOMBRES_ES = {
237
  "Genesis": "Génesis", "Exodus": "Éxodo", "Leviticus": "Levítico",
238
  "Numbers": "Números", "Deuteronomy": "Deuteronomio", "Joshua": "Josué",
 
250
 
251
 
252
  def _convertir_traduccion_plana(book_id: str, data: dict) -> dict:
 
 
 
 
 
 
 
253
  texto = data.get("translation", "") or ""
254
  lineas = [l.strip() for l in texto.split("\n") if l.strip()]
255
  versiculos = [{"n": i + 1, "he": "", "es": linea} for i, linea in enumerate(lineas)]
 
269
  d = json.load(f)
270
  book_id = os.path.splitext(os.path.basename(ruta))[0]
271
  if "capitulos" in d:
 
272
  lid = d.get("id") or book_id
273
  libros[lid] = d
274
  elif "translation" in d:
 
275
  libros[book_id] = _convertir_traduccion_plana(book_id, d)
276
  else:
277
  print(f"Formato no reconocido en {ruta}, se omite.")
 
358
  st_cap = gr.State(1)
359
 
360
  gr.HTML("<div id='cabecera'><div class='estrella'>✡</div>"
361
+ "<h1>Centralita Torá</h1><p>hebreo y español · canto Trope real y Voz Divina limpia</p></div>")
362
 
363
  with gr.Tab("Leer"):
364
  with gr.Row():
 
367
  ver_btn = gr.Button("Ver capítulo", variant="primary")
368
  salida = gr.HTML()
369
  with gr.Row():
370
+ b_es = gr.Button("🔊 Narrador + Voz Divina (Español)", variant="secondary")
371
+ b_he = gr.Button("🔊 עברית (Hebreo con Trope Real)", variant="secondary")
372
  audio = gr.Audio(label="Audio Litúrgico", autoplay=True)
373
 
374
  def ver(nombre, cap):
 
377
 
378
  ver_btn.click(ver, [dd, ncap], [salida, st_es, st_he, st_nombre, st_cap])
379
  b_es.click(leer_es, st_es, audio)
380
+ b_he.click(leer_he, inputs=[st_nombre, st_cap], outputs=[audio])
381
 
382
  with gr.Tab("Buscar"):
383
  q = gr.Textbox(label="Palabra o frase (hebreo o español)", placeholder="ej. luz / אור")