ProCreations commited on
Commit
d05b807
·
verified ·
1 Parent(s): 1f311ef

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. README.md +29 -29
  2. app.py +5 -91
  3. engine.py +35 -8
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/nemotron-3.5-asr-streaming-0.6b
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 (Σ 16.4B params — well under the 32B cap)
34
 
35
  | Role | Model | Params |
36
  |---|---|---|
37
- | 🧠 Agent brain + vision | [google/gemma-4-12B-it](https://huggingface.co/google/gemma-4-12B-it) | 12B |
 
38
  | 🗣️ Expressive voice | [bosonai/higgs-audio-v2-generation-3B-base](https://huggingface.co/bosonai/higgs-audio-v2-generation-3B-base) | 3B |
39
- | 👂 Speech recognition | [nvidia/nemotron-3.5-asr-streaming-0.6b](https://huggingface.co/nvidia/nemotron-3.5-asr-streaming-0.6b) | 0.6B |
40
- | 👂 ASR fallback | [openai/whisper-large-v3-turbo](https://huggingface.co/openai/whisper-large-v3-turbo) | 0.8B |
41
-
42
- ### The Nemotron CPU sidecar 🟩
43
-
44
- The fun engineering bit: **NeMo and ZeroGPU don't mix.** Import NeMo in the
45
- main process and ZeroGPU's forked GPU workers die ("GPU task aborted" —
46
- verified the hard way); lazy-load it inside the workers instead and every
47
- fresh worker spends 30-60 s restoring the model on its first voice turn.
48
-
49
- So Nemotron runs in a **sidecar**: an isolated process spawned at startup
50
- (`asr_sidecar.py`) that loads NeMo once and serves transcriptions over a
51
- pipe on **CPU**, where the 0.6B FastConformer transcribes an utterance in
52
- a couple of seconds. Your words appear *before the GPU is even requested*,
53
- the whole `@spaces.GPU` window is spent on teaching, and voice input costs
54
- **zero GPU quota**. Whisper turbo rides along inside the GPU as a silent
55
- fallback if the sidecar is ever unavailable.
56
-
57
- > One more honest swap forced by the same constraint: Higgs Audio **v3** TTS
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). Nemotron ASR transcribes you on-device.
64
- 2. **The agent gathers context.** Gemma 4 decides whether the question needs
65
- fresh facts; if so it writes search queries and pulls snippets from the web
66
- (DuckDuckGo, keyless). Timeless 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,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 three
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">👂 Nemotron 3.5 ASR 0.6B</span>
136
- <span class="badge">Σ 16.4B params Build Small ✅</span>
 
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 12B + Higgs Audio v2 + Nemotron ASR "
360
- "= 16.4B params, all on this Space · draw on the board with your mouse, "
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 (15.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
  * 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 (AutoModelForMultimodalLM, AutoProcessor,
 
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 = llm_generate(msgs, max_new_tokens=96, temperature=0.0)
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] search decision failed: {e}")
 
 
 
 
 
 
 
 
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": "15.8B (12B + 3B + 0.8B)",
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
  }