Nucleo propio: oye con whisper.cpp, piensa con un modelo de Ollama, habla con Piper, y hace RAG sobre los apuntes del usuario. 100% local, sin cuentas ni claves. Escrito bajo una restriccion dura, 4 GB de VRAM: el cerebro y whisper comparten tarjeta y solo caben porque estan dimensionados para ello. El RAG usa embeddings estaticos con busqueda hibrida; la voz clonada se sirve de una cache de frases. Incluye instalador (install.sh), requisitos, y documentacion del stack, del manejo de root y de las acciones. Los apuntes indexados y el diario NO se incluyen: son privados y el .gitignore los bloquea.
87 lines
2.7 KiB
Python
87 lines
2.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Renderiza el reactor a PNG sin arrancar Newelle.
|
|
|
|
Extrae el metodo _draw_reactor REAL de call.py (no una copia) y lo ejecuta
|
|
con un self falso y niveles de audio simulados. Sirve para iterar el diseño
|
|
sin pagar el build de flatpak cada vez.
|
|
|
|
python3 preview_reactor.py [salida.png]
|
|
"""
|
|
import ast
|
|
import math
|
|
import sys
|
|
import textwrap
|
|
from pathlib import Path
|
|
|
|
import cairo
|
|
|
|
CALL_PY = Path(__file__).parent / "newelle" / "src" / "ui" / "widgets" / "call.py"
|
|
SIZE = 240
|
|
|
|
|
|
def load_draw_func():
|
|
"""Saca _draw_reactor del fuente y lo compila aislado."""
|
|
tree = ast.parse(CALL_PY.read_text())
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.FunctionDef) and node.name == "_draw_reactor":
|
|
src = textwrap.dedent(ast.get_source_segment(CALL_PY.read_text(), node))
|
|
ns = {"math": math}
|
|
exec(compile(src, "call.py", "exec"), ns)
|
|
return ns["_draw_reactor"]
|
|
raise SystemExit("no encuentro _draw_reactor en call.py")
|
|
|
|
|
|
class FakePanel:
|
|
"""El minimo que _draw_reactor toca de self."""
|
|
|
|
def __init__(self, levels, phase, user=False, assistant=False):
|
|
self.reactor_levels = levels
|
|
self.reactor_phase = phase
|
|
self.user_speaking = user
|
|
self.assistant_speaking = assistant
|
|
|
|
|
|
def frame(draw, panel, path):
|
|
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, SIZE, SIZE)
|
|
cr = cairo.Context(surface)
|
|
# fondo del modo llamada, para juzgar el contraste real
|
|
cr.set_source_rgb(0.04, 0.09, 0.13)
|
|
cr.paint()
|
|
draw(panel, None, cr, SIZE, SIZE)
|
|
surface.write_to_png(str(path))
|
|
|
|
|
|
def main():
|
|
draw = load_draw_func()
|
|
out = Path(sys.argv[1] if len(sys.argv) > 1 else "reactor.png")
|
|
|
|
escenas = [
|
|
("reposo", FakePanel([0.04] * 12, 0.0)),
|
|
("escuchando", FakePanel(
|
|
[0.25, 0.55, 0.85, 0.60, 0.35, 0.70, 0.95, 0.45, 0.30, 0.65, 0.50, 0.20],
|
|
1.1, user=True)),
|
|
("respondiendo", FakePanel(
|
|
[0.40, 0.30, 0.55, 0.45, 0.60, 0.35, 0.50, 0.40, 0.55, 0.30, 0.45, 0.35],
|
|
2.4, assistant=True)),
|
|
]
|
|
|
|
# tira horizontal con las tres escenas
|
|
strip = cairo.ImageSurface(cairo.FORMAT_ARGB32, SIZE * 3, SIZE)
|
|
scr = cairo.Context(strip)
|
|
scr.set_source_rgb(0.04, 0.09, 0.13)
|
|
scr.paint()
|
|
|
|
for i, (nombre, panel) in enumerate(escenas):
|
|
tmp = Path(f"/tmp/_reactor_{nombre}.png")
|
|
frame(draw, panel, tmp)
|
|
img = cairo.ImageSurface.create_from_png(str(tmp))
|
|
scr.set_source_surface(img, i * SIZE, 0)
|
|
scr.paint()
|
|
tmp.unlink(missing_ok=True)
|
|
|
|
strip.write_to_png(str(out))
|
|
print(f"escrito {out} ({SIZE * 3}x{SIZE}) — reposo | escuchando | respondiendo")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|