Cristobal299 commited on
Commit
e21daf7
·
verified ·
1 Parent(s): d8a55e9

Upload 4 files

Browse files
Files changed (3) hide show
  1. app.py +44 -16
  2. gradio_theme_fenix.py +32 -49
  3. packages.txt +1 -0
app.py CHANGED
@@ -5,7 +5,7 @@ Leer hebreo + español · escuchar (voces gratis) · estudiar con IA (Groq).
5
  Los libros se cargan desde la carpeta libros/ (un JSON por libro).
6
  Secret opcional para la pestaña Estudiar: GROQ_API_KEY
7
  """
8
- import os, glob, json, tempfile, html, asyncio
9
  import urllib.request
10
  import gradio as gr
11
  import edge_tts
@@ -16,13 +16,18 @@ except Exception:
16
 
17
  from gradio_theme_fenix import fenix_theme, FENIX_CSS
18
 
19
- # Voz única y alegre de la app (edge-tts, gratis). Cámbiala aquí si quieres otra:
20
- # es: es-ES-ElviraNeural · es-ES-XimenaNeural · es-MX-DaliaNeural · es-CO-SalomeNeural
21
- # he: he-IL-HilaNeural · he-IL-AvriNeural
22
- VOZ_ES = "es-ES-ElviraNeural"
23
- VOZ_HE = "he-IL-HilaNeural"
24
- VOZ_RATE = "+8%" # un poco más ágil = más alegre
25
- VOZ_PITCH = "+10Hz" # tono algo más alto = más alegre
 
 
 
 
 
26
 
27
  MODELO_ESTUDIA = "openai/gpt-oss-120b"
28
  CARPETA = "libros"
@@ -89,26 +94,49 @@ def render(nombre, cap):
89
  return f"<div class='pasaje'>{''.join(filas)}</div>", " ".join(plano_es), " ".join(plano_he)
90
 
91
 
92
- # ---------- voces (edge-tts: voz única y alegre, gratis) ----------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  async def a_voz(texto, idioma):
94
  if not texto or not texto.strip():
95
  return None
96
  voz = VOZ_HE if idioma == "he" else VOZ_ES
97
  ruta = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False).name
 
98
  try:
99
  com = edge_tts.Communicate(texto[:4000], voz, rate=VOZ_RATE, pitch=VOZ_PITCH)
100
  await com.save(ruta)
101
- if os.path.getsize(ruta) > 0:
102
- return ruta
103
  except Exception:
104
- pass
105
- if gTTS: # respaldo por si edge-tts falla
106
  try:
107
  gTTS(text=texto[:4500], lang=("iw" if idioma == "he" else "es")).save(ruta)
108
- return ruta
109
  except Exception:
110
- return None
111
- return None
 
 
112
 
113
 
114
  async def leer_es(t):
 
5
  Los libros se cargan desde la carpeta libros/ (un JSON por libro).
6
  Secret opcional para la pestaña Estudiar: GROQ_API_KEY
7
  """
8
+ import os, glob, json, tempfile, html, asyncio, subprocess, shutil
9
  import urllib.request
10
  import gradio as gr
11
  import edge_tts
 
16
 
17
  from gradio_theme_fenix import fenix_theme, FENIX_CSS
18
 
19
+ # Voz única de la app (edge-tts, gratis). Álvaro (España, masculina).
20
+ # otras es: es-ES-ElviraNeural · es-MX-DaliaNeural · es-CO-SalomeNeural
21
+ # he: he-IL-AvriNeural · he-IL-HilaNeural
22
+ VOZ_ES = "es-ES-AlvaroNeural"
23
+ VOZ_HE = "he-IL-AvriNeural"
24
+ VOZ_RATE = "+6%" # ritmo
25
+ VOZ_PITCH = "+0Hz" # tono
26
+
27
+ # Retoque de sonido con ffmpeg (bass, estéreo, efecto). Sube/baja a gusto:
28
+ AUDIO_BASS_DB = 10 # refuerzo de graves (dB)
29
+ AUDIO_ECO = "0.8:0.85:60:0.3" # efecto (eco/espacio); "" para quitarlo
30
+ AUDIO_ESTEREO = True # salida en 2 canales
31
 
32
  MODELO_ESTUDIA = "openai/gpt-oss-120b"
33
  CARPETA = "libros"
 
94
  return f"<div class='pasaje'>{''.join(filas)}</div>", " ".join(plano_es), " ".join(plano_he)
95
 
96
 
97
+ # ---------- voces (edge-tts + retoque de sonido, gratis) ----------
98
+ def procesar_audio(ruta):
99
+ """Aplica graves, estéreo y efecto con ffmpeg. Si algo falla, devuelve el original."""
100
+ if not shutil.which("ffmpeg"):
101
+ return ruta
102
+ filtros = [f"bass=g={AUDIO_BASS_DB}"]
103
+ if AUDIO_ECO:
104
+ filtros.append(f"aecho={AUDIO_ECO}")
105
+ salida = ruta[:-4] + "_fx.mp3"
106
+ cmd = ["ffmpeg", "-y", "-i", ruta, "-af", ", ".join(filtros)]
107
+ if AUDIO_ESTEREO:
108
+ cmd += ["-ac", "2"]
109
+ cmd.append(salida)
110
+ try:
111
+ subprocess.run(cmd, check=True, capture_output=True, timeout=60)
112
+ if os.path.getsize(salida) > 0:
113
+ return salida
114
+ except Exception:
115
+ pass
116
+ return ruta
117
+
118
+
119
  async def a_voz(texto, idioma):
120
  if not texto or not texto.strip():
121
  return None
122
  voz = VOZ_HE if idioma == "he" else VOZ_ES
123
  ruta = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False).name
124
+ ok = False
125
  try:
126
  com = edge_tts.Communicate(texto[:4000], voz, rate=VOZ_RATE, pitch=VOZ_PITCH)
127
  await com.save(ruta)
128
+ ok = os.path.getsize(ruta) > 0
 
129
  except Exception:
130
+ ok = False
131
+ if not ok and gTTS: # respaldo si edge-tts falla
132
  try:
133
  gTTS(text=texto[:4500], lang=("iw" if idioma == "he" else "es")).save(ruta)
134
+ ok = os.path.getsize(ruta) > 0
135
  except Exception:
136
+ ok = False
137
+ if not ok:
138
+ return None
139
+ return procesar_audio(ruta)
140
 
141
 
142
  async def leer_es(t):
gradio_theme_fenix.py CHANGED
@@ -1,8 +1,5 @@
1
  # -*- coding: utf-8 -*-
2
- """
3
- Tema "Fénix Celestial" para la Centralita Torá.
4
- Cielo claro, luz dorada y tipografía serif con soporte hebreo (Frank Ruhl Libre).
5
- """
6
  import gradio as gr
7
 
8
 
@@ -13,71 +10,57 @@ def fenix_theme():
13
  neutral_hue=gr.themes.colors.slate,
14
  font=[gr.themes.GoogleFont("Frank Ruhl Libre"), "Georgia", "serif"],
15
  ).set(
16
- body_background_fill="linear-gradient(180deg,#d7ecff 0%,#e9f4ff 42%,#f7fbff 100%)",
17
- body_background_fill_dark="linear-gradient(180deg,#0a1120 0%,#0f1a2e 100%)",
18
  block_background_fill="rgba(255,255,255,0.74)",
19
- block_background_fill_dark="rgba(18,26,44,0.72)",
20
  block_border_width="1px",
21
  block_border_color="#cfe1ff",
22
- block_radius="18px",
23
- block_shadow="0 12px 34px -14px rgba(70,110,190,0.40)",
24
- block_label_background_fill="linear-gradient(135deg,#eaf2ff,#dbeaff)",
25
- block_label_text_color="#3f5b8f",
26
  button_large_radius="14px",
27
  button_primary_background_fill="linear-gradient(135deg,#7cb2ff 0%,#5a8be0 100%)",
28
  button_primary_background_fill_hover="linear-gradient(135deg,#8fc0ff 0%,#6c9bf0 100%)",
29
  button_primary_text_color="#ffffff",
30
- button_secondary_background_fill="rgba(255,255,255,0.9)",
31
  button_secondary_border_color="#d6c48a",
32
  button_secondary_text_color="#6b5a2a",
33
- input_background_fill="rgba(255,255,255,0.9)",
34
- input_border_color="#cfe1ff",
35
  body_text_color="#24324d",
36
  )
37
 
38
 
39
- # CSS extra para el toque celestial y el texto sagrado
40
  FENIX_CSS = """
41
- .gradio-container { max-width: 900px !important; margin: auto !important; }
 
42
 
43
- #cabecera { text-align:center; padding: 10px 0 4px; }
44
- #cabecera .estrella { font-size: 30px; color:#d9b84a;
45
- text-shadow: 0 0 14px rgba(217,184,74,.6); }
46
- #cabecera h1 { margin:.15em 0 0; font-size: 2.05rem; color:#2c3f66;
47
- letter-spacing:.5px; text-shadow: 0 1px 0 #fff; }
48
- #cabecera p { margin:.2em 0 0; color:#5b6f96; font-style:italic; }
49
 
50
- /* texto sagrado sobre "pergamino celeste" */
51
- .pasaje { line-height:1.7; }
52
- .pasaje .verso { padding:8px 4px; border-bottom:1px dashed rgba(120,150,200,.25); }
53
- .pasaje .num { display:inline-block; min-width:1.6em; color:#c69a34;
54
- font-weight:700; }
55
- .pasaje .he { display:block; direction:rtl; font-size:1.7rem; line-height:1.85;
56
- color:#20365e; margin:2px 0 4px; font-family:'Frank Ruhl Libre',serif; }
57
- .pasaje .es { color:#33445f; font-size:1.05rem; }
 
 
 
 
 
 
58
  .pasaje .cab { text-align:center; color:#3f5b8f; margin-bottom:10px; }
59
- .pasaje .cab .h { font-size:1.9rem; color:#c69a34; }
60
 
61
- .resultado { padding:6px 2px; border-bottom:1px dashed rgba(120,150,200,.25); }
 
62
  .resultado .ref { color:#c69a34; font-weight:700; }
63
  footer { visibility: hidden; }
64
 
65
- /* ---------- responsive (móvil) ---------- */
66
- @media (max-width: 680px) {
67
- .gradio-container { max-width: 100% !important; padding: 0 8px !important; }
68
- #cabecera { padding: 6px 0 2px; }
69
- #cabecera .estrella { font-size: 26px; }
70
- #cabecera h1 { font-size: 1.6rem; }
71
- #cabecera p { font-size: .95rem; }
72
- .pasaje .he { font-size: 1.45rem; line-height:1.7; }
73
- .pasaje .es { font-size: 1rem; }
74
- .pasaje .cab .h { font-size: 1.5rem; }
75
- /* botones a lo ancho y fáciles de tocar */
76
- .gr-button { min-height: 46px !important; font-size: 1rem !important; }
77
- .gr-button, .gr-box, .gr-form { width: 100% !important; }
78
- }
79
- @media (max-width: 420px) {
80
- #cabecera h1 { font-size: 1.4rem; }
81
- .pasaje .he { font-size: 1.3rem; }
82
  }
83
  """
 
1
  # -*- coding: utf-8 -*-
2
+ """Tema "Fénix Celestial" para la Centralita Torá — cielo, luz dorada, responsive."""
 
 
 
3
  import gradio as gr
4
 
5
 
 
10
  neutral_hue=gr.themes.colors.slate,
11
  font=[gr.themes.GoogleFont("Frank Ruhl Libre"), "Georgia", "serif"],
12
  ).set(
 
 
13
  block_background_fill="rgba(255,255,255,0.74)",
 
14
  block_border_width="1px",
15
  block_border_color="#cfe1ff",
16
+ block_radius="16px",
17
+ block_shadow="0 12px 30px -14px rgba(70,110,190,0.35)",
 
 
18
  button_large_radius="14px",
19
  button_primary_background_fill="linear-gradient(135deg,#7cb2ff 0%,#5a8be0 100%)",
20
  button_primary_background_fill_hover="linear-gradient(135deg,#8fc0ff 0%,#6c9bf0 100%)",
21
  button_primary_text_color="#ffffff",
 
22
  button_secondary_border_color="#d6c48a",
23
  button_secondary_text_color="#6b5a2a",
 
 
24
  body_text_color="#24324d",
25
  )
26
 
27
 
 
28
  FENIX_CSS = """
29
+ * { box-sizing: border-box; }
30
+ html, body, .gradio-container { overflow-x: hidden !important; max-width: 100% !important; }
31
 
32
+ /* fondo celeste (forzado, más fiable que el token del tema) */
33
+ .gradio-container, gradio-app, body {
34
+ background: linear-gradient(180deg,#d7ecff 0%,#e9f4ff 42%,#f7fbff 100%) !important;
35
+ }
36
+ .gradio-container { margin: auto !important; padding: 0 10px !important; }
 
37
 
38
+ #cabecera { text-align:center; padding: 8px 0 2px; }
39
+ #cabecera .estrella { font-size: 28px; color:#d9b84a; text-shadow:0 0 14px rgba(217,184,74,.6); }
40
+ #cabecera h1 { margin:.1em 0 0; font-size: 1.7rem; color:#2c3f66; }
41
+ #cabecera p { margin:.15em 0 0; color:#5b6f96; font-style:italic; font-size:.95rem; }
42
+
43
+ /* texto sagrado mobile-first y SIEMPRE hace wrap */
44
+ .pasaje { line-height:1.7; max-width:100%; }
45
+ .pasaje, .pasaje * { overflow-wrap:anywhere !important; word-break:break-word !important;
46
+ white-space:normal !important; max-width:100% !important; }
47
+ .pasaje .verso { padding:8px 2px; border-bottom:1px dashed rgba(120,150,200,.25); }
48
+ .pasaje .num { display:inline-block; min-width:1.5em; color:#c69a34; font-weight:700; }
49
+ .pasaje .he { display:block; direction:rtl; font-size:1.35rem; line-height:1.8; color:#20365e;
50
+ margin:2px 0 5px; font-family:'Frank Ruhl Libre',serif; }
51
+ .pasaje .es { display:block; color:#33445f; font-size:1rem; }
52
  .pasaje .cab { text-align:center; color:#3f5b8f; margin-bottom:10px; }
53
+ .pasaje .cab .h { font-size:1.6rem; color:#c69a34; }
54
 
55
+ .resultado { padding:6px 2px; border-bottom:1px dashed rgba(120,150,200,.25);
56
+ overflow-wrap:anywhere; word-break:break-word; }
57
  .resultado .ref { color:#c69a34; font-weight:700; }
58
  footer { visibility: hidden; }
59
 
60
+ /* pantallas grandes: agrandar el hebreo */
61
+ @media (min-width: 760px) {
62
+ #cabecera h1 { font-size: 2.05rem; }
63
+ .pasaje .he { font-size: 1.7rem; }
64
+ .pasaje .cab .h { font-size: 1.9rem; }
 
 
 
 
 
 
 
 
 
 
 
 
65
  }
66
  """
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ ffmpeg