Spaces:
Paused
Paused
Upload 4 files
Browse files- app.py +44 -16
- gradio_theme_fenix.py +32 -49
- 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
|
| 20 |
-
# es: es-ES-ElviraNeural · es-
|
| 21 |
-
# he: he-IL-
|
| 22 |
-
VOZ_ES = "es-ES-
|
| 23 |
-
VOZ_HE = "he-IL-
|
| 24 |
-
VOZ_RATE = "+
|
| 25 |
-
VOZ_PITCH = "+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 102 |
-
return ruta
|
| 103 |
except Exception:
|
| 104 |
-
|
| 105 |
-
if gTTS: # respaldo
|
| 106 |
try:
|
| 107 |
gTTS(text=texto[:4500], lang=("iw" if idioma == "he" else "es")).save(ruta)
|
| 108 |
-
|
| 109 |
except Exception:
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
| 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="
|
| 23 |
-
block_shadow="0 12px
|
| 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 |
-
|
|
|
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
#cabecera p { margin:.2em 0 0; color:#5b6f96; font-style:italic; }
|
| 49 |
|
| 50 |
-
|
| 51 |
-
.
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
.pasaje .
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
.pasaje .cab { text-align:center; color:#3f5b8f; margin-bottom:10px; }
|
| 59 |
-
.pasaje .cab .h { font-size:1.
|
| 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 |
-
/*
|
| 66 |
-
@media (
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 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
|