Spaces:
Running on Zero
Running on Zero
Upload folder using huggingface_hub
Browse files
README.md
CHANGED
|
@@ -17,7 +17,7 @@ tags:
|
|
| 17 |
models:
|
| 18 |
- google/gemma-4-12B-it
|
| 19 |
- bosonai/higgs-audio-v2-generation-3B-base
|
| 20 |
-
- nvidia/
|
| 21 |
- openai/whisper-large-v3-turbo
|
| 22 |
---
|
| 23 |
|
|
@@ -30,40 +30,40 @@ with its voice.**
|
|
| 30 |
Built for the [HF Build Small Hackathon](https://huggingface.co/build-small-hackathon):
|
| 31 |
every model runs **on this Space itself** via ZeroGPU. No cloud APIs, no keys.
|
| 32 |
|
| 33 |
-
## The stack (Σ
|
| 34 |
|
| 35 |
| Role | Model | Params |
|
| 36 |
|---|---|---|
|
| 37 |
-
| 🧠
|
|
|
|
| 38 |
| 🗣️ Expressive voice | [bosonai/higgs-audio-v2-generation-3B-base](https://huggingface.co/bosonai/higgs-audio-v2-generation-3B-base) | 3B |
|
| 39 |
-
| 👂 Speech recognition | [
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
> ships only for the SGLang-Omni serving stack (needs a persistent GPU), so
|
| 59 |
-
> we use v2 — same family, natively in transformers.
|
| 60 |
|
| 61 |
## How a turn works
|
| 62 |
|
| 63 |
-
1. **You talk** (or type).
|
| 64 |
-
2. **
|
| 65 |
-
fresh facts; if so it writes search queries and
|
| 66 |
-
(DuckDuckGo, keyless). Timeless
|
|
|
|
| 67 |
3. **It teaches in steps.** Gemma 4 emits a JSON lesson script — each step is
|
| 68 |
a sentence to *say* plus whiteboard ops to *draw* (boxes, arrows, curves,
|
| 69 |
axes, highlights in a 100×75 coordinate space).
|
|
@@ -86,7 +86,7 @@ fallback if the sidecar is ever unavailable.
|
|
| 86 |
|
| 87 |
## Running it
|
| 88 |
|
| 89 |
-
- **On Spaces (the real thing):** select **ZeroGPU** hardware. All
|
| 90 |
models load at startup and get packed by ZeroGPU; each turn runs in a
|
| 91 |
single `@spaces.GPU(duration=59)` generator call, sized so even
|
| 92 |
**logged-out visitors** (120s/day ZeroGPU quota) get a full lesson. Sign in
|
|
|
|
| 17 |
models:
|
| 18 |
- google/gemma-4-12B-it
|
| 19 |
- bosonai/higgs-audio-v2-generation-3B-base
|
| 20 |
+
- nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16
|
| 21 |
- openai/whisper-large-v3-turbo
|
| 22 |
---
|
| 23 |
|
|
|
|
| 30 |
Built for the [HF Build Small Hackathon](https://huggingface.co/build-small-hackathon):
|
| 31 |
every model runs **on this Space itself** via ZeroGPU. No cloud APIs, no keys.
|
| 32 |
|
| 33 |
+
## The stack (Σ 19.8B params — well under the 32B cap)
|
| 34 |
|
| 35 |
| Role | Model | Params |
|
| 36 |
|---|---|---|
|
| 37 |
+
| 🧠 Teacher + vision | [google/gemma-4-12B-it](https://huggingface.co/google/gemma-4-12B-it) | 12B |
|
| 38 |
+
| 🔎 Research planner | [nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16](https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16) | 4B |
|
| 39 |
| 🗣️ Expressive voice | [bosonai/higgs-audio-v2-generation-3B-base](https://huggingface.co/bosonai/higgs-audio-v2-generation-3B-base) | 3B |
|
| 40 |
+
| 👂 Speech recognition | [openai/whisper-large-v3-turbo](https://huggingface.co/openai/whisper-large-v3-turbo) | 0.8B |
|
| 41 |
+
|
| 42 |
+
**Gemma teaches. Nemotron researches.** Every turn, NVIDIA's Nemotron 3 Nano
|
| 43 |
+
decides whether the question needs fresh facts and writes the search queries —
|
| 44 |
+
the agentic step that turns a chatbot into a researcher. Gemma 4 then teaches
|
| 45 |
+
from what Nemotron found, Higgs speaks it, Whisper listens.
|
| 46 |
+
|
| 47 |
+
### Engineering notes (the honest kind)
|
| 48 |
+
|
| 49 |
+
- We first shipped **Nemotron 3.5 ASR** as the ears. ZeroGPU said no, twice:
|
| 50 |
+
NeMo in the main process crashes the forked GPU workers ("GPU task
|
| 51 |
+
aborted"), and lazy-loading it inside workers cost every fresh worker
|
| 52 |
+
30-60 s on its first voice turn. We then ran it in an isolated **CPU
|
| 53 |
+
sidecar** — measured RTF ≈ 24 on the Space's shared CPU (a 10 s clip took
|
| 54 |
+
~240 s, int8-quantized). Not interactive. So Nemotron moved to the role
|
| 55 |
+
where it shines on this hardware — the research brain — and Whisper turbo
|
| 56 |
+
took the ears (preloads with everything, transcribes in ~1 s).
|
| 57 |
+
- Higgs Audio **v3** TTS ships only for the SGLang-Omni serving stack (needs
|
| 58 |
+
a persistent GPU), so we use v2 — same family, natively in transformers.
|
|
|
|
|
|
|
| 59 |
|
| 60 |
## How a turn works
|
| 61 |
|
| 62 |
+
1. **You talk** (or type). Whisper turbo transcribes you on-device.
|
| 63 |
+
2. **Nemotron researches.** NVIDIA's Nemotron 3 Nano decides whether the
|
| 64 |
+
question needs fresh facts; if so it writes the search queries and Tutori
|
| 65 |
+
pulls snippets + page text from the web (DuckDuckGo, keyless). Timeless
|
| 66 |
+
topics skip straight to teaching.
|
| 67 |
3. **It teaches in steps.** Gemma 4 emits a JSON lesson script — each step is
|
| 68 |
a sentence to *say* plus whiteboard ops to *draw* (boxes, arrows, curves,
|
| 69 |
axes, highlights in a 100×75 coordinate space).
|
|
|
|
| 86 |
|
| 87 |
## Running it
|
| 88 |
|
| 89 |
+
- **On Spaces (the real thing):** select **ZeroGPU** hardware. All four
|
| 90 |
models load at startup and get packed by ZeroGPU; each turn runs in a
|
| 91 |
single `@spaces.GPU(duration=59)` generator call, sized so even
|
| 92 |
**logged-out visitors** (120s/day ZeroGPU quota) get a full lesson. Sign in
|
app.py
CHANGED
|
@@ -9,9 +9,6 @@ this Space itself (ZeroGPU) — 16.6B parameters total, zero cloud APIs.
|
|
| 9 |
|
| 10 |
import json
|
| 11 |
import os
|
| 12 |
-
import subprocess
|
| 13 |
-
import sys
|
| 14 |
-
import threading
|
| 15 |
import uuid
|
| 16 |
from pathlib import Path
|
| 17 |
|
|
@@ -46,76 +43,6 @@ BOARD_JS = (ROOT / "static" / "board.js").read_text()
|
|
| 46 |
CSS = (ROOT / "static" / "style.css").read_text()
|
| 47 |
|
| 48 |
|
| 49 |
-
class AsrSidecar:
|
| 50 |
-
"""Client for the Nemotron ASR sidecar process (see asr_sidecar.py).
|
| 51 |
-
|
| 52 |
-
The sidecar loads NeMo once, on CPU, in its own process — so voice turns
|
| 53 |
-
transcribe in a couple of seconds, before any GPU is even requested.
|
| 54 |
-
"""
|
| 55 |
-
|
| 56 |
-
def __init__(self, script):
|
| 57 |
-
self.script = script
|
| 58 |
-
self.proc = subprocess.Popen(
|
| 59 |
-
[sys.executable, str(script)],
|
| 60 |
-
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
| 61 |
-
stderr=None, text=True, bufsize=1,
|
| 62 |
-
)
|
| 63 |
-
self.lock = threading.Lock()
|
| 64 |
-
self.ready = False
|
| 65 |
-
threading.Thread(target=self._await_ready, daemon=True).start()
|
| 66 |
-
|
| 67 |
-
def _await_ready(self):
|
| 68 |
-
line = self.proc.stdout.readline()
|
| 69 |
-
if '"ready"' in (line or ""):
|
| 70 |
-
self.ready = True
|
| 71 |
-
print("[tutori] Nemotron ASR sidecar ready", flush=True)
|
| 72 |
-
|
| 73 |
-
def transcribe(self, path, timeout=45):
|
| 74 |
-
if not self.ready or self.proc.poll() is not None:
|
| 75 |
-
return None
|
| 76 |
-
with self.lock:
|
| 77 |
-
try:
|
| 78 |
-
self.proc.stdin.write(json.dumps({"path": path}) + "\n")
|
| 79 |
-
self.proc.stdin.flush()
|
| 80 |
-
except Exception:
|
| 81 |
-
return None
|
| 82 |
-
box = {}
|
| 83 |
-
|
| 84 |
-
def _read():
|
| 85 |
-
box["line"] = self.proc.stdout.readline()
|
| 86 |
-
|
| 87 |
-
t = threading.Thread(target=_read, daemon=True)
|
| 88 |
-
t.start()
|
| 89 |
-
t.join(timeout)
|
| 90 |
-
line = box.get("line")
|
| 91 |
-
if not line:
|
| 92 |
-
# the late reply would desync the protocol — restart instead
|
| 93 |
-
print("[tutori] sidecar timed out; restarting it", flush=True)
|
| 94 |
-
try:
|
| 95 |
-
self.proc.kill()
|
| 96 |
-
except Exception:
|
| 97 |
-
pass
|
| 98 |
-
self.ready = False
|
| 99 |
-
self.__init__(self.script)
|
| 100 |
-
return None
|
| 101 |
-
if not line:
|
| 102 |
-
return None
|
| 103 |
-
try:
|
| 104 |
-
out = json.loads(line)
|
| 105 |
-
except Exception:
|
| 106 |
-
return None
|
| 107 |
-
if out.get("error"):
|
| 108 |
-
print(f"[tutori] sidecar ASR error: {out['error']}", flush=True)
|
| 109 |
-
return None
|
| 110 |
-
return out.get("text") or None
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
SIDECAR = None
|
| 114 |
-
if ENGINE.MODELS_INFO.get("mode") == "zerogpu" and \
|
| 115 |
-
os.environ.get("TUTORI_SIDECAR", "1") == "1":
|
| 116 |
-
print("[tutori] starting Nemotron ASR sidecar …", flush=True)
|
| 117 |
-
SIDECAR = AsrSidecar(ROOT / "asr_sidecar.py")
|
| 118 |
-
|
| 119 |
HEAD = f"""
|
| 120 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 121 |
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@500;600;700&family=Inter:wght@400;500;600&family=Space+Grotesk:wght@600;700&display=swap" rel="stylesheet">
|
|
@@ -132,8 +59,9 @@ HEADER_HTML = f"""
|
|
| 132 |
<span class="badge hot">⚡ ZeroGPU · no cloud APIs</span>
|
| 133 |
<span class="badge">🧠 Gemma 4 12B</span>
|
| 134 |
<span class="badge">🗣️ Higgs Audio v2 TTS 3B</span>
|
| 135 |
-
<span class="badge">
|
| 136 |
-
<span class="badge">
|
|
|
|
| 137 |
</div>
|
| 138 |
</div>
|
| 139 |
"""
|
|
@@ -201,20 +129,6 @@ def run_turn(audio_path, typed_text, snapshot, chat, convo, profile,
|
|
| 201 |
|
| 202 |
yield render(detail="Thinking…")
|
| 203 |
|
| 204 |
-
# Nemotron sidecar hears you BEFORE any GPU is requested — the transcript
|
| 205 |
-
# appears in a couple of seconds and the GPU window is spent on teaching.
|
| 206 |
-
if audio_path and SIDECAR is not None:
|
| 207 |
-
yield render("thinking", "Listening…")
|
| 208 |
-
heard = SIDECAR.transcribe(audio_path)
|
| 209 |
-
if heard:
|
| 210 |
-
typed_text = f"{heard} {typed_text or ''}".strip()
|
| 211 |
-
question_for_context = typed_text
|
| 212 |
-
chat[-2]["content"] = f"🎙️ {typed_text}"
|
| 213 |
-
audio_path = None
|
| 214 |
-
yield render("thinking", "Heard you!")
|
| 215 |
-
# if the sidecar couldn't transcribe, the audio passes through to the
|
| 216 |
-
# engine's GPU fallback ASR
|
| 217 |
-
|
| 218 |
seq = 0
|
| 219 |
error = None
|
| 220 |
print(f"[tutori] turn {turn_id}: starting engine", flush=True)
|
|
@@ -356,8 +270,8 @@ with gr.Blocks(
|
|
| 356 |
gr.HTML(
|
| 357 |
'<div id="tutori-foot">Built small for the '
|
| 358 |
'<a href="https://huggingface.co/build-small-hackathon" target="_blank">'
|
| 359 |
-
"HF Build Small Hackathon</a> · Gemma 4
|
| 360 |
-
"=
|
| 361 |
"Tutori can see it 👀"
|
| 362 |
'<div id="made-by">Made by SSH/ProCreations</div></div>'
|
| 363 |
)
|
|
|
|
| 9 |
|
| 10 |
import json
|
| 11 |
import os
|
|
|
|
|
|
|
|
|
|
| 12 |
import uuid
|
| 13 |
from pathlib import Path
|
| 14 |
|
|
|
|
| 43 |
CSS = (ROOT / "static" / "style.css").read_text()
|
| 44 |
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
HEAD = f"""
|
| 47 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 48 |
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@500;600;700&family=Inter:wght@400;500;600&family=Space+Grotesk:wght@600;700&display=swap" rel="stylesheet">
|
|
|
|
| 59 |
<span class="badge hot">⚡ ZeroGPU · no cloud APIs</span>
|
| 60 |
<span class="badge">🧠 Gemma 4 12B</span>
|
| 61 |
<span class="badge">🗣️ Higgs Audio v2 TTS 3B</span>
|
| 62 |
+
<span class="badge">🔎 Nemotron 3 Nano 4B</span>
|
| 63 |
+
<span class="badge">👂 Whisper v3 turbo 0.8B</span>
|
| 64 |
+
<span class="badge">Σ 19.8B params — Build Small ✅</span>
|
| 65 |
</div>
|
| 66 |
</div>
|
| 67 |
"""
|
|
|
|
| 129 |
|
| 130 |
yield render(detail="Thinking…")
|
| 131 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
seq = 0
|
| 133 |
error = None
|
| 134 |
print(f"[tutori] turn {turn_id}: starting engine", flush=True)
|
|
|
|
| 270 |
gr.HTML(
|
| 271 |
'<div id="tutori-foot">Built small for the '
|
| 272 |
'<a href="https://huggingface.co/build-small-hackathon" target="_blank">'
|
| 273 |
+
"HF Build Small Hackathon</a> · Gemma 4 teaches · Nemotron 3 researches · Higgs speaks · Whisper listens "
|
| 274 |
+
"= 19.8B params, all on this Space · draw on the board with your mouse, "
|
| 275 |
"Tutori can see it 👀"
|
| 276 |
'<div id="made-by">Made by SSH/ProCreations</div></div>'
|
| 277 |
)
|
engine.py
CHANGED
|
@@ -1,12 +1,13 @@
|
|
| 1 |
"""
|
| 2 |
Tutori engine — the real, ZeroGPU-backed brains.
|
| 3 |
|
| 4 |
-
Models (
|
| 5 |
* google/gemma-4-12B-it ............ agent / lesson planner / vision (12B)
|
| 6 |
* bosonai/higgs-audio-v2-generation-3B-base ... expressive speech (3B)
|
| 7 |
(v3 TTS only ships for the SGLang-Omni serving stack, which needs a
|
| 8 |
persistent GPU — incompatible with ZeroGPU. v2 is the same Higgs Audio
|
| 9 |
family, natively supported by transformers.)
|
|
|
|
| 10 |
* openai/whisper-large-v3-turbo .... speech recognition (0.8B)
|
| 11 |
|
| 12 |
run_turn() is a generator that yields event dicts; app.py turns those into
|
|
@@ -32,7 +33,8 @@ import soundfile as sf
|
|
| 32 |
import spaces
|
| 33 |
import torch
|
| 34 |
from PIL import Image
|
| 35 |
-
from transformers import (
|
|
|
|
| 36 |
HiggsAudioV2ForConditionalGeneration,
|
| 37 |
TextIteratorStreamer, pipeline)
|
| 38 |
|
|
@@ -43,6 +45,7 @@ if os.environ.get("TUTORI_DEBUG") == "1":
|
|
| 43 |
LLM_ID = "google/gemma-4-12B-it"
|
| 44 |
TTS_ID = "bosonai/higgs-audio-v2-generation-3B-base"
|
| 45 |
ASR_ID = "openai/whisper-large-v3-turbo"
|
|
|
|
| 46 |
|
| 47 |
print(f"[tutori] torch {torch.__version__} | spaces {getattr(spaces, '__version__', '?')}",
|
| 48 |
flush=True)
|
|
@@ -71,6 +74,12 @@ asr_pipe = pipeline(
|
|
| 71 |
dtype=torch.bfloat16, device="cuda",
|
| 72 |
)
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
print("[tutori] all models ready")
|
| 75 |
|
| 76 |
|
|
@@ -959,6 +968,19 @@ def synthesize(text):
|
|
| 959 |
# web research (DuckDuckGo, no key needed)
|
| 960 |
# --------------------------------------------------------------------------
|
| 961 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 962 |
def decide_search(question, profile, notes):
|
| 963 |
today = datetime.date.today().isoformat()
|
| 964 |
notes_hint = f"\nEXISTING NOTES (from earlier this session): {notes[:500]}" if notes else ""
|
|
@@ -969,12 +991,17 @@ def decide_search(question, profile, notes):
|
|
| 969 |
f"Learner profile: {json.dumps(profile)[:400]}{notes_hint}"},
|
| 970 |
]
|
| 971 |
try:
|
| 972 |
-
raw =
|
| 973 |
-
obj = parse_lesson_json(raw) or {}
|
| 974 |
-
if obj.get("search") and isinstance(obj.get("queries"), list):
|
| 975 |
-
return [str(q)[:120] for q in obj["queries"][:2]]
|
| 976 |
except Exception as e:
|
| 977 |
-
print(f"[tutori]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 978 |
return []
|
| 979 |
|
| 980 |
|
|
@@ -1195,6 +1222,6 @@ def tts_only(text):
|
|
| 1195 |
|
| 1196 |
MODELS_INFO = {
|
| 1197 |
"llm": LLM_ID, "tts": TTS_ID, "asr": ASR_ID,
|
| 1198 |
-
"total_params": "
|
| 1199 |
"mode": "zerogpu",
|
| 1200 |
}
|
|
|
|
| 1 |
"""
|
| 2 |
Tutori engine — the real, ZeroGPU-backed brains.
|
| 3 |
|
| 4 |
+
Models (19.8B params total, all running on the Space itself — no API calls):
|
| 5 |
* google/gemma-4-12B-it ............ agent / lesson planner / vision (12B)
|
| 6 |
* bosonai/higgs-audio-v2-generation-3B-base ... expressive speech (3B)
|
| 7 |
(v3 TTS only ships for the SGLang-Omni serving stack, which needs a
|
| 8 |
persistent GPU — incompatible with ZeroGPU. v2 is the same Higgs Audio
|
| 9 |
family, natively supported by transformers.)
|
| 10 |
+
* nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16 ... research planner (4B)
|
| 11 |
* openai/whisper-large-v3-turbo .... speech recognition (0.8B)
|
| 12 |
|
| 13 |
run_turn() is a generator that yields event dicts; app.py turns those into
|
|
|
|
| 33 |
import spaces
|
| 34 |
import torch
|
| 35 |
from PIL import Image
|
| 36 |
+
from transformers import (AutoModelForCausalLM, AutoModelForMultimodalLM,
|
| 37 |
+
AutoProcessor, AutoTokenizer,
|
| 38 |
HiggsAudioV2ForConditionalGeneration,
|
| 39 |
TextIteratorStreamer, pipeline)
|
| 40 |
|
|
|
|
| 45 |
LLM_ID = "google/gemma-4-12B-it"
|
| 46 |
TTS_ID = "bosonai/higgs-audio-v2-generation-3B-base"
|
| 47 |
ASR_ID = "openai/whisper-large-v3-turbo"
|
| 48 |
+
RESEARCH_ID = "nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16"
|
| 49 |
|
| 50 |
print(f"[tutori] torch {torch.__version__} | spaces {getattr(spaces, '__version__', '?')}",
|
| 51 |
flush=True)
|
|
|
|
| 74 |
dtype=torch.bfloat16, device="cuda",
|
| 75 |
)
|
| 76 |
|
| 77 |
+
print("[tutori] loading Nemotron 3 Nano research planner …")
|
| 78 |
+
research_tok = AutoTokenizer.from_pretrained(RESEARCH_ID)
|
| 79 |
+
research_llm = AutoModelForCausalLM.from_pretrained(
|
| 80 |
+
RESEARCH_ID, dtype=torch.bfloat16, device_map="cuda"
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
print("[tutori] all models ready")
|
| 84 |
|
| 85 |
|
|
|
|
| 968 |
# web research (DuckDuckGo, no key needed)
|
| 969 |
# --------------------------------------------------------------------------
|
| 970 |
|
| 971 |
+
def nemotron_generate(messages, max_new_tokens=220):
|
| 972 |
+
"""Tutori's research planner runs on NVIDIA Nemotron 3 Nano."""
|
| 973 |
+
ids = research_tok.apply_chat_template(
|
| 974 |
+
messages, add_generation_prompt=True, return_tensors="pt"
|
| 975 |
+
).to(research_llm.device)
|
| 976 |
+
with torch.inference_mode():
|
| 977 |
+
out = research_llm.generate(
|
| 978 |
+
ids, max_new_tokens=max_new_tokens, do_sample=False,
|
| 979 |
+
pad_token_id=research_tok.eos_token_id,
|
| 980 |
+
)
|
| 981 |
+
return research_tok.decode(out[0][ids.shape[-1]:], skip_special_tokens=True)
|
| 982 |
+
|
| 983 |
+
|
| 984 |
def decide_search(question, profile, notes):
|
| 985 |
today = datetime.date.today().isoformat()
|
| 986 |
notes_hint = f"\nEXISTING NOTES (from earlier this session): {notes[:500]}" if notes else ""
|
|
|
|
| 991 |
f"Learner profile: {json.dumps(profile)[:400]}{notes_hint}"},
|
| 992 |
]
|
| 993 |
try:
|
| 994 |
+
raw = nemotron_generate(msgs, max_new_tokens=220)
|
|
|
|
|
|
|
|
|
|
| 995 |
except Exception as e:
|
| 996 |
+
print(f"[tutori] Nemotron planner failed ({e!r}); falling back to Gemma")
|
| 997 |
+
try:
|
| 998 |
+
raw = llm_generate(msgs, max_new_tokens=96, temperature=0.0)
|
| 999 |
+
except Exception as e2:
|
| 1000 |
+
print(f"[tutori] search decision failed: {e2}")
|
| 1001 |
+
return []
|
| 1002 |
+
obj = parse_lesson_json(raw) or {}
|
| 1003 |
+
if obj.get("search") and isinstance(obj.get("queries"), list):
|
| 1004 |
+
return [str(q)[:120] for q in obj["queries"][:2]]
|
| 1005 |
return []
|
| 1006 |
|
| 1007 |
|
|
|
|
| 1222 |
|
| 1223 |
MODELS_INFO = {
|
| 1224 |
"llm": LLM_ID, "tts": TTS_ID, "asr": ASR_ID,
|
| 1225 |
+
"total_params": "19.8B (12B + 4B + 3B + 0.8B)",
|
| 1226 |
"mode": "zerogpu",
|
| 1227 |
}
|