File size: 7,669 Bytes
ec28fe4
 
4fbab42
6f14400
 
ec28fe4
a75f4f7
6f14400
 
ec28fe4
a75f4f7
454a7d7
6f14400
ec28fe4
4fbab42
f04b500
 
4fbab42
 
 
ec28fe4
a75f4f7
4d60fda
a75f4f7
 
 
 
 
 
 
6f14400
4fbab42
454a7d7
a75f4f7
 
4fbab42
454a7d7
 
f04b500
ec28fe4
6f14400
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4fbab42
 
4d60fda
4fbab42
f04b500
4fbab42
f04b500
4fbab42
 
 
 
454a7d7
 
ec28fe4
454a7d7
6f14400
 
ec28fe4
 
454a7d7
 
ec28fe4
6f14400
4fbab42
6f14400
 
 
 
 
 
 
 
 
 
 
 
ec28fe4
 
6f14400
 
 
 
 
 
 
4fbab42
 
 
 
 
 
 
 
 
6f14400
454a7d7
f04b500
454a7d7
 
ec28fe4
6f14400
 
 
 
 
f04b500
 
 
 
 
6f14400
 
f04b500
 
ec28fe4
f04b500
4fbab42
6f14400
 
f04b500
 
6f14400
f04b500
 
6f14400
f04b500
 
 
 
6f14400
f04b500
 
 
6f14400
f04b500
 
 
6f14400
4d60fda
f04b500
4d60fda
 
 
 
 
 
 
 
6f14400
 
 
 
 
 
 
 
 
 
 
f04b500
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
import os
import spaces
import torch
import tempfile
import asyncio
import gradio as gr
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
import edge_tts
import pdfplumber

MODEL_ID = "Qwen/Qwen2.5-Coder-32B-Instruct"
API_KEY  = os.environ.get("BRAIN_API_KEY", "")
TTS_VOZ  = "es-ES-AlvaroNeural"

SYSTEM_DEFAULT = (
    "Eres F\u00e9nix Brain, experto en Python, Gradio, FastAPI y HuggingFace Spaces. "
    "Responde siempre en espa\u00f1ol con c\u00f3digo limpio y funcional."
)

print("Cargando tokenizer...")
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)

print("Configurando cuantizaci\u00f3n (4-bits)...")
quantization_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4"
)

print("Cargando modelo...")
model = AutoModelForCausalLM.from_pretrained(
    MODEL_ID,
    quantization_config=quantization_config,
    device_map="auto",
    low_cpu_mem_usage=True,
    trust_remote_code=True,
)
print("Modelo 32B listo \u2705")


async def _tts_async(texto: str, voz: str) -> str:
    tmp = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False)
    await edge_tts.Communicate(texto[:3000], voz).save(tmp.name)
    return tmp.name

def texto_a_voz(texto: str, voz: str) -> str:
    try:
        return asyncio.new_event_loop().run_until_complete(_tts_async(texto, voz))
    except Exception as e:
        print(f"TTS error: {e}")
        return None


def _leer_archivo(path: str) -> str:
    if path.lower().endswith(".pdf"):
        try:
            with pdfplumber.open(path) as pdf:
                return "\n".join(p.extract_text() or "" for p in pdf.pages[:10])
        except Exception as e:
            return f"[Error leyendo PDF: {e}]"
    else:
        try:
            with open(path, "r", encoding="utf-8", errors="ignore") as f:
                return f.read()[:8000]
        except Exception as e:
            return f"[Error leyendo archivo: {e}]"


@spaces.GPU
def generar(prompt: str, system: str, key: str, max_tokens: int = 1024) -> str:
    """Endpoint API para La Forja."""
    if API_KEY and key != API_KEY:
        return "\u274c Unauthorized"
    if not prompt.strip():
        return "\u274c Prompt vac\u00edo"
    messages = [
        {"role": "system", "content": system or SYSTEM_DEFAULT},
        {"role": "user",   "content": prompt},
    ]
    text   = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
    inputs = tokenizer([text], return_tensors="pt").to(model.device)
    with torch.no_grad():
        out = model.generate(
            **inputs, max_new_tokens=max_tokens,
            temperature=0.2, do_sample=True,
            pad_token_id=tokenizer.eos_token_id,
        )
    gen = out[0][inputs["input_ids"].shape[1]:]
    return tokenizer.decode(gen, skip_special_tokens=True)


@spaces.GPU
def chat_fn(mensaje: str, archivo, historial: list, system_prompt: str, voz: str, activar_voz: bool):
    if not mensaje.strip() and archivo is None:
        return historial, "", None

    texto_archivo = ""
    if archivo is not None:
        contenido = _leer_archivo(archivo.name)
        nombre = os.path.basename(archivo.name)
        texto_archivo = f"\n\n[Archivo: {nombre}]\n{contenido}"

    prompt_completo = mensaje + texto_archivo

    messages = [{"role": "system", "content": system_prompt or SYSTEM_DEFAULT}]
    for h in historial:
        if isinstance(h, dict):
            messages.append(h)
        else:
            messages.append({"role": "user",      "content": h[0]})
            messages.append({"role": "assistant", "content": h[1]})
    messages.append({"role": "user", "content": prompt_completo})

    text   = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
    inputs = tokenizer([text], return_tensors="pt").to(model.device)
    with torch.no_grad():
        out = model.generate(
            **inputs, max_new_tokens=1024, temperature=0.2,
            do_sample=True, pad_token_id=tokenizer.eos_token_id,
        )
    gen = out[0][inputs["input_ids"].shape[1]:]
    respuesta = tokenizer.decode(gen, skip_special_tokens=True)

    historial = historial + [
        {"role": "user",      "content": mensaje + (" \U0001f4c4" if archivo else "")},
        {"role": "assistant", "content": respuesta},
    ]

    audio_path = texto_a_voz(respuesta, voz) if activar_voz else None
    return historial, "", audio_path


VOCES = {
    "\U0001f1ea\U0001f1f8 \u00c1lvaro (ES)": "es-ES-AlvaroNeural",
    "\U0001f1ea\U0001f1f8 Elvira (ES)":      "es-ES-ElviraNeural",
    "\U0001f1f2\U0001f1fd Jorge (MX)":       "es-MX-JorgeNeural",
    "\U0001f1f2\U0001f1fd Dalia (MX)":       "es-MX-DaliaNeural",
    "\U0001f1e6\U0001f1f7 Tom\u00e1s (AR)":  "es-AR-TomasNeural",
}

with gr.Blocks(title="\U0001f9e0 F\u00e9nix Brain", theme=gr.themes.Base()) as demo:
    gr.Markdown("# \U0001f9e0 F\u00e9nix Brain\nQwen2.5-Coder-32B-Instruct (4-bit) \u00b7 Documentos \u00b7 Voz")

    with gr.Tab("\U0001f4ac Chat"):
        with gr.Row():
            with gr.Column(scale=3):
                system_box = gr.Textbox(label="System prompt", value=SYSTEM_DEFAULT, lines=2)
                chatbot    = gr.Chatbot(height=450, type="messages", label="F\u00e9nix Brain")
                msg_box    = gr.Textbox(placeholder="Escribe tu mensaje\u2026", label="Mensaje", lines=2)
                with gr.Row():
                    send_btn  = gr.Button("\U0001f4e8 Enviar", variant="primary")
                    clear_btn = gr.Button("\U0001f5d1\ufe0f Limpiar")
            with gr.Column(scale=1):
                archivo_in  = gr.File(label="\U0001f4c4 Archivo", file_types=[".pdf", ".txt", ".py", ".md", ".json"])
                activar_voz = gr.Checkbox(label="\U0001f50a Voz activada", value=True)
                voz_sel     = gr.Dropdown(list(VOCES.keys()), value="\U0001f1ea\U0001f1f8 \u00c1lvaro (ES)", label="Voz")
                audio_out   = gr.Audio(label="\U0001f50a Respuesta", autoplay=True)

    def _chat(mensaje, archivo, historial, system, voz_nombre, activar):
        voz = VOCES.get(voz_nombre, TTS_VOZ)
        return chat_fn(mensaje, archivo, historial, system, voz, activar)

    send_btn.click(_chat, [msg_box, archivo_in, chatbot, system_box, voz_sel, activar_voz], [chatbot, msg_box, audio_out])
    msg_box.submit(_chat, [msg_box, archivo_in, chatbot, system_box, voz_sel, activar_voz], [chatbot, msg_box, audio_out])
    clear_btn.click(lambda: ([], "", None), outputs=[chatbot, msg_box, audio_out])

    # ── Endpoint API oculto para La Forja ────────────────────────────────────
    with gr.Tab("\U0001f50c API (La Forja)"):
        gr.Markdown("### Endpoint `/generar` para La Forja")
        _p   = gr.Textbox(label="prompt",     visible=False)
        _s   = gr.Textbox(label="system",     visible=False)
        _k   = gr.Textbox(label="key",        visible=False)
        _mt  = gr.Number( label="max_tokens", visible=False, value=1024)
        _out = gr.Textbox(label="response",   visible=False)
        _btn = gr.Button("generar",           visible=False)
        _btn.click(generar, inputs=[_p, _s, _k, _mt], outputs=[_out], api_name="generar")
        gr.Markdown("""
```python
from gradio_client import Client
client = Client("Cristobal299/Chat")
result = client.predict(
    prompt="Tu pregunta",
    system="",
    key="TU_BRAIN_API_KEY",
    max_tokens=1024,
    api_name="/generar"
)
print(result)
```
""")

demo.launch(server_name="0.0.0.0", server_port=7860)