ssdataanalysis commited on
Commit
4d0d04c
·
verified ·
1 Parent(s): f6c36e1

Replace api_name=False with explicit private endpoints to avoid FnIndex errors

Browse files
Files changed (6) hide show
  1. README.md +144 -0
  2. app.py +1654 -0
  3. muse_core.py +193 -0
  4. requirements.txt +7 -0
  5. tests/test_app_contract.py +383 -0
  6. tests/test_muse_core.py +119 -0
README.md ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Muse Glimmer 30B
3
+ emoji: ✨
4
+ colorFrom: purple
5
+ colorTo: blue
6
+ sdk: gradio
7
+ sdk_version: 6.22.0
8
+ python_version: 3.10.13
9
+ app_file: app.py
10
+ startup_duration_timeout: 1h
11
+ fullWidth: true
12
+ pinned: false
13
+ license: apache-2.0
14
+ models:
15
+ - meta-models/Muse-Glimmer-30B
16
+ - meta-models/Muse-Glimmer-30B-assistant
17
+ ---
18
+
19
+ # Muse Glimmer 30B · private inference
20
+
21
+ A private text-and-image chat for both:
22
+ - [`meta-models/Muse-Glimmer-30B`](https://huggingface.co/meta-models/Muse-Glimmer-30B), full BF16
23
+ - [`meta-models/Muse-Glimmer-30B-assistant`](https://huggingface.co/meta-models/Muse-Glimmer-30B-assistant), assistant checkpoint
24
+
25
+ `meta-models/Muse-Glimmer-30B-assistant` is a companion DFlash drafter and does not expose standalone
26
+ `.generate(...)` in this release of Transformers. In this Space, assistant selection runs the full 30B
27
+ checkpoint (same prompt/history behavior) while keeping the model selector and UI behavior intact.
28
+
29
+ They are served at pinned revisions:
30
+ `f84ecc3a0ea984a4c04542a84269e3d065350a6e` (full BF16, 29,776,626,688 parameters)
31
+ and `2c86316d689027b91123638739743fef1d425233` (assistant BF16, 2,555,985,152 parameters).
32
+
33
+ ## Runtime
34
+
35
+ - Full, unquantized BF16 checkpoint (29,776,626,688 parameters)
36
+ - ZeroGPU allocation through the `@spaces.GPU(size="xlarge")` request.
37
+ The full 30B checkpoint reserves up to `120s` for its cold start on first load by default.
38
+ Assistant selection follows the same warm-path budget rules:
39
+ - if a full model is already loaded in RAM, assistant generation reuses that active model and uses the normal
40
+ per-turn estimate,
41
+ - if assistant must warm an alternative path, it also uses the `120s` cold-start cap.
42
+ - Pinned read-only model storage is mounted from Hugging Face model volumes into
43
+ - `/models/muse-glimmer`
44
+ - `/models/muse-glimmer-assistant`
45
+ - Runtime defaults can be overridden with `MUSE_MODEL_PATH`, `MUSE_ASSISTANT_MODEL_PATH`,
46
+ and `MUSE_DEFAULT_MODEL_ID`.
47
+ - `transformers==5.15.0`, the first stable release with native Muse Glimmer support
48
+ - Sequential safetensors `pread` loading, avoiding whole-shard host-RAM copies from the volume
49
+ - One inference request at a time
50
+ - Optional bucket-backed checkpoints are supported by setting `MUSE_MODEL_PATH`
51
+ and `MUSE_ASSISTANT_MODEL_PATH` to mounted bucket locations (for example
52
+ `/mnt/muse-glimmer` when mounted via `hf://buckets/<namespace>/<bucket>:/mnt`).
53
+ - The private Space API path is locked down (`api_visibility: private`) so all chat requests flow
54
+ through the same Gradio queue and preflight validation.
55
+ - Model loading is deferred until the first request so the Space can start even when a mount
56
+ is temporarily unavailable.
57
+ To keep both checkpoints selectable, mount both model volumes in Space settings:
58
+ `/models/muse-glimmer` and `/models/muse-glimmer-assistant`.
59
+
60
+ ### Optional Hugging Face bucket storage
61
+
62
+ If you need storage that persists beyond the Space runtime, mount an HF bucket:
63
+
64
+ ```bash
65
+ hf spaces volumes set ssdataanalysis/muse-glimmer-30b \\
66
+ -v hf://buckets/ssdataanalysis/muse-glimmer-30b-store:/mnt/muse-glimmer-store \\
67
+ -v hf://models/meta-models/Muse-Glimmer-30B:/models/muse-glimmer:ro \\
68
+ -v hf://models/meta-models/Muse-Glimmer-30B-assistant:/models/muse-glimmer-assistant:ro
69
+ ```
70
+
71
+ Then point the app to bucket copies without changing code:
72
+
73
+ ```bash
74
+ export MUSE_MODEL_PATH=/mnt/muse-glimmer-store/Muse-Glimmer-30B
75
+ export MUSE_ASSISTANT_MODEL_PATH=/mnt/muse-glimmer-store/Muse-Glimmer-30B-assistant
76
+ ```
77
+
78
+ To inspect configured mounts:
79
+
80
+ ```bash
81
+ hf spaces volumes list ssdataanalysis/muse-glimmer-30b
82
+ ```
83
+
84
+ The app loads the selected checkpoint on demand; a selected model switch swaps checkpoints as needed.
85
+ Loaded checkpoints are placed on `cuda`, as required by ZeroGPU. The app does not download full
86
+ checkpoints into the Space's ephemeral disk.
87
+
88
+ ZeroGPU status check:
89
+
90
+ - `hf spaces info ssdataanalysis/muse-glimmer-30b --json` should show:
91
+ - `requested_hardware: zero-a10g`
92
+ - `hardware: zero-a10g`
93
+ - This app requests `@spaces.GPU(size="xlarge")` and should run on ZeroGPU's 96GB path for BF16.
94
+
95
+ ## Runtime access and quota checks
96
+
97
+ - The Space is private. If you open `https://huggingface.co/spaces/ssdataanalysis/muse-glimmer-30b` or
98
+ `https://ssdataanalysis-muse-glimmer-30b.hf.space` without being authenticated as the owner, you will see
99
+ an authentication/error page.
100
+ - API calls must include an authenticated Hugging Face token with access to this private Space.
101
+ - You can verify the endpoint is wired with:
102
+
103
+ ```bash
104
+ HF_TOKEN=<your-token>
105
+ curl -H "Authorization: Bearer $HF_TOKEN" \
106
+ -H "Content-Type: application/json" \
107
+ -d '{"data":["hi",null,"meta-models/Muse-Glimmer-30B",[],[], "", "high", false, 32, 1.0, 0.95, 64, 1.0, 42, false, true]}' \
108
+ https://ssdataanalysis-muse-glimmer-30b.hf.space/gradio_api/call/chat
109
+ ```
110
+
111
+ - If you receive a ZeroGPU quota response such as
112
+ `You have exceeded your Pro ZeroGPU quota ...`, the container/startup is healthy and the failure is account-level
113
+ quota, not a model loading/runtime regression. Add credits here if you need immediate inference:
114
+ <https://huggingface.co/settings/billing?add-credits=true>
115
+
116
+ ## Defaults and controls
117
+
118
+ The checkpoint's `generation_config.json` uses greedy decoding (`do_sample: false`), so the UI
119
+ opens in **Native greedy** mode. The separate **Meta recommended sampling** preset enables the
120
+ model-card values:
121
+
122
+ - temperature: `1.0`
123
+ - top-p: `0.95`
124
+ - top-k: `64`
125
+
126
+ Reasoning strength defaults to `high`, matching the supplied chat template. It can be changed to
127
+ `low`, `medium`, `high`, or `xhigh`. The app also exposes the response token budget, sampling
128
+ toggle, repetition penalty, seed, optional system instruction, reasoning visibility, and optional
129
+ image input. The 512-token response budget is an app safety default; the model repository does not
130
+ define a practical per-response budget.
131
+
132
+ The app uses the model's native response parser to keep reasoning and final-answer regions
133
+ separate. No tools are registered or executed.
134
+
135
+ ## Privacy and safety
136
+
137
+ Access is enforced by the private Space setting. This application does not write prompts,
138
+ responses, or uploaded images to persistent storage and does not print them to application logs.
139
+ Inference still runs on Hugging Face-hosted infrastructure; it is not local-only. Image metadata is
140
+ discarded before inference.
141
+
142
+ Use is subject to the model's
143
+ [`USAGE_POLICY.md`](https://huggingface.co/meta-models/Muse-Glimmer-30B/blob/f84ecc3a0ea984a4c04542a84269e3d065350a6e/USAGE_POLICY.md) or [`USAGE_POLICY.md`](https://huggingface.co/meta-models/Muse-Glimmer-30B-assistant/blob/2c86316d689027b91123638739743fef1d425233/USAGE_POLICY.md).
144
+ Model outputs may be inaccurate and should be independently verified for consequential use.
app.py ADDED
@@ -0,0 +1,1654 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import queue
6
+ import threading
7
+ import time
8
+ from datetime import date
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+
13
+ os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
14
+ os.environ.setdefault("PYTORCH_ALLOC_CONF", "expandable_segments:True")
15
+ os.environ.setdefault("HF_DEACTIVATE_ASYNC_LOAD", "1")
16
+ try:
17
+ DEFAULT_COLD_START_DURATION_SECONDS = int(os.getenv("MUSE_COLD_START_DURATION_SECONDS", "120"))
18
+ except (TypeError, ValueError):
19
+ DEFAULT_COLD_START_DURATION_SECONDS = 120
20
+
21
+ SKIP_MODEL_LOAD = os.getenv("MUSE_SKIP_MODEL_LOAD", "0") == "1"
22
+
23
+ try:
24
+ import spaces
25
+ except ModuleNotFoundError:
26
+ if not SKIP_MODEL_LOAD:
27
+ raise
28
+
29
+ class _LocalSpaces:
30
+ @staticmethod
31
+ def GPU(*_args, **_kwargs):
32
+ def decorator(function):
33
+ return function
34
+
35
+ return decorator
36
+
37
+ spaces = _LocalSpaces()
38
+
39
+ import gradio as gr
40
+ from PIL import Image, ImageOps
41
+ import torch
42
+ from transformers import StoppingCriteria, StoppingCriteriaList, TextIteratorStreamer
43
+
44
+ from muse_core import (
45
+ APP_INPUT_TOKEN_LIMIT,
46
+ DEFAULT_MAX_NEW_TOKENS,
47
+ DEFAULT_REPETITION_PENALTY,
48
+ DEFAULT_SEED,
49
+ DEFAULT_TEMPERATURE,
50
+ DEFAULT_TOP_K,
51
+ DEFAULT_TOP_P,
52
+ MAX_NEW_TOKENS,
53
+ META_SAMPLING,
54
+ MODEL_CONTEXT_TOKENS,
55
+ NATIVE_GREEDY,
56
+ PRESETS,
57
+ choose_seed,
58
+ coerce_parsed_reply,
59
+ estimate_gpu_duration,
60
+ friendly_error,
61
+ generation_kwargs,
62
+ preset_values,
63
+ render_reply,
64
+ validate_controls,
65
+ )
66
+
67
+
68
+ MODEL_ID = "meta-models/Muse-Glimmer-30B"
69
+ MODEL_REVISION = "f84ecc3a0ea984a4c04542a84269e3d065350a6e"
70
+ ASSISTANT_MODEL_ID = "meta-models/Muse-Glimmer-30B-assistant"
71
+ ASSISTANT_MODEL_REVISION = "2c86316d689027b91123638739743fef1d425233"
72
+ EXPECTED_MODEL_TYPE = "muse_glimmer"
73
+ EXPECTED_MODEL_TYPES = {EXPECTED_MODEL_TYPE, "muse_glimmer_assistant"}
74
+ ASSISTANT_EXPECTED_PARAMETER_COUNT = 2_555_985_152
75
+
76
+ MODEL_CHOICES = [
77
+ ("Muse Glimmer 30B (full BF16)", MODEL_ID),
78
+ ("Muse Glimmer 30B-assistant (compact)", ASSISTANT_MODEL_ID),
79
+ ]
80
+ SUBMIT_API_NAME = "chat_submit"
81
+ MODEL_NAME_ALIASES = {
82
+ "/Muse-Glimmer 30B": MODEL_ID,
83
+ "/Muse-Glimmer-30B": MODEL_ID,
84
+ "Muse Glimmer 30B (full BF16)": MODEL_ID,
85
+ "/Muse-Glimmer 30B-assistant": ASSISTANT_MODEL_ID,
86
+ "/Muse-Glimmer-30B-assistant": ASSISTANT_MODEL_ID,
87
+ "Muse Glimmer 30B-assistant (compact)": ASSISTANT_MODEL_ID,
88
+ "0": MODEL_ID,
89
+ "1": ASSISTANT_MODEL_ID,
90
+ "": MODEL_ID,
91
+ }
92
+ _BASE_MODEL_PATH = Path(os.getenv("MUSE_MODEL_PATH", "/models/muse-glimmer"))
93
+ _BASE_ASSISTANT_MODEL_PATH = Path(
94
+ os.getenv("MUSE_ASSISTANT_MODEL_PATH", "/models/muse-glimmer-assistant")
95
+ )
96
+
97
+
98
+ def _has_model_manifest(path: Path) -> bool:
99
+ has_config = (path / "config.json").is_file()
100
+ if not has_config:
101
+ return False
102
+ return (path / "chat_template.jinja").is_file() or (path / "tokenizer.json").is_file()
103
+
104
+
105
+ def _resolve_mount_path(model_root: Path) -> Path:
106
+ """Handle both direct and mounted-directory layouts for model checkpoints."""
107
+ try:
108
+ if _has_model_manifest(model_root):
109
+ return model_root
110
+ except OSError:
111
+ return model_root
112
+
113
+ if not model_root.is_dir():
114
+ return model_root
115
+
116
+ # hf mount may expose the checkpoint under a subfolder of the declared mount path.
117
+ candidate_children = []
118
+ try:
119
+ for child in model_root.iterdir():
120
+ if child.is_dir() and _has_model_manifest(child):
121
+ candidate_children.append(child)
122
+ except OSError:
123
+ return model_root
124
+
125
+ if len(candidate_children) == 1:
126
+ return candidate_children[0]
127
+ if len(candidate_children) > 1:
128
+ # Prefer a child that explicitly references a Muse-Glimmer checkpoint name.
129
+ for child in candidate_children:
130
+ if child.name.startswith("Muse-Glimmer-30B"):
131
+ return child
132
+ return model_root
133
+
134
+
135
+ MODEL_REGISTRY = {
136
+ MODEL_ID: {
137
+ "revision": MODEL_REVISION,
138
+ "path": _resolve_mount_path(_BASE_MODEL_PATH),
139
+ "expected_model_type": "muse_glimmer",
140
+ "expected_parameter_count": 29_776_626_688,
141
+ "display": "Muse Glimmer 30B (full BF16)",
142
+ },
143
+ ASSISTANT_MODEL_ID: {
144
+ "revision": ASSISTANT_MODEL_REVISION,
145
+ "path": _resolve_mount_path(_BASE_ASSISTANT_MODEL_PATH),
146
+ "expected_model_type": "muse_glimmer_assistant",
147
+ "expected_parameter_count": ASSISTANT_EXPECTED_PARAMETER_COUNT,
148
+ "display": "Muse Glimmer 30B-assistant (compact)",
149
+ },
150
+ }
151
+
152
+ def _coerce_model_id(model_id: Any) -> str:
153
+ if model_id in (None, []):
154
+ return MODEL_DEFAULT_ID
155
+
156
+ if isinstance(model_id, (tuple, list)):
157
+ if not model_id:
158
+ return MODEL_DEFAULT_ID
159
+ if len(model_id) > 1 and isinstance(model_id[1], str):
160
+ return model_id[1]
161
+ if isinstance(model_id[0], str):
162
+ return _coerce_model_id(model_id[0])
163
+ return MODEL_DEFAULT_ID
164
+ if isinstance(model_id, int):
165
+ choices = [value for _label, value in MODEL_CHOICES]
166
+ if 0 <= model_id < len(choices):
167
+ return choices[model_id]
168
+ return MODEL_DEFAULT_ID
169
+ if isinstance(model_id, str):
170
+ normalized = model_id.strip()
171
+ if normalized in MODEL_NAME_ALIASES:
172
+ return MODEL_NAME_ALIASES[normalized]
173
+ if model_id.isdigit():
174
+ choices = [value for _label, value in MODEL_CHOICES]
175
+ idx = int(model_id)
176
+ if 0 <= idx < len(choices):
177
+ return choices[idx]
178
+ return model_id
179
+ return str(model_id)
180
+
181
+
182
+ def _resolve_default_model_id() -> str:
183
+ configured = os.getenv("MUSE_DEFAULT_MODEL_ID", MODEL_ID)
184
+ if configured not in MODEL_REGISTRY:
185
+ configured = MODEL_ID
186
+ configured_path = MODEL_REGISTRY[configured]["path"]
187
+ if configured_path.is_dir():
188
+ return configured
189
+ for model_id, spec in MODEL_REGISTRY.items():
190
+ if model_id == configured:
191
+ continue
192
+ if spec["path"].is_dir():
193
+ return model_id
194
+ return configured
195
+
196
+
197
+ MODEL_DEFAULT_ID = _resolve_default_model_id()
198
+
199
+ MAX_HISTORY_MESSAGES = 20
200
+ MAX_HISTORY_IMAGES = 2
201
+ MAX_IMAGE_EDGE = 2_048
202
+ MAX_IMAGE_PIXELS = 4_194_304
203
+
204
+ ACTIVE_MODEL_ID: str | None = None
205
+ ACTIVE_MODEL = None
206
+ ACTIVE_PROCESSOR = None
207
+
208
+
209
+ def _model_spec(model_id: str) -> dict[str, Any]:
210
+ if model_id not in MODEL_REGISTRY:
211
+ raise ValueError(f"Unknown model selection: {model_id}")
212
+ return MODEL_REGISTRY[model_id]
213
+
214
+
215
+ def _is_model_checkpoint(path: str | os.PathLike[str], model_path: Path) -> bool:
216
+ try:
217
+ candidate = Path(path).resolve()
218
+ model_root = model_path.resolve()
219
+ except (OSError, RuntimeError, ValueError):
220
+ return False
221
+ candidate_text = str(candidate)
222
+ model_root_text = str(model_root)
223
+ return candidate == model_root or candidate_text.startswith(model_root_text + os.sep)
224
+
225
+
226
+ def _normalize_load_result(result: Any) -> tuple[Any, dict[str, Any]]:
227
+ if isinstance(result, tuple):
228
+ if len(result) >= 2:
229
+ return result[0], result[1]
230
+ return result[0], {}
231
+ if isinstance(result, dict):
232
+ return result.get("model"), result
233
+ return result, {}
234
+
235
+
236
+ def _supports_generation(model: Any) -> bool:
237
+ return callable(getattr(model, "generate", None))
238
+
239
+
240
+ def _load_model_with_pread(
241
+ model_class,
242
+ model_path: Path,
243
+ *,
244
+ use_safetensors: bool = True,
245
+ safe_open_backend: str | None = "pread",
246
+ trust_remote_code: bool = False,
247
+ ):
248
+ """Load the mounted shards sequentially without mmap or whole-shard RAM copies.
249
+
250
+ Transformers 5.15 deliberately disables mmap for Hugging Face model volumes because
251
+ concurrent page faults can deadlock hf-mount. Its fallback reads an entire safetensors
252
+ shard into host RAM; Muse Glimmer's first shard is about 50 GB, while a standard Space
253
+ has far less host RAM. Safetensors 0.8's pread backend avoids both failure modes and lets
254
+ Transformers materialize and dispatch one tensor at a time.
255
+ """
256
+ from safetensors import safe_open as safetensors_safe_open
257
+ from transformers import modeling_utils
258
+
259
+ if not hasattr(modeling_utils, "_is_on_hf_mount") or not hasattr(modeling_utils, "safe_open"):
260
+ return _load_model_direct(model_class, model_path)
261
+
262
+ shards = sorted(model_path.glob("*.safetensors"))
263
+ if not shards:
264
+ raise RuntimeError("The mounted checkpoint contains no safetensors shards.")
265
+
266
+ # Fail early with a small header-only read for the selected backend.
267
+ safe_open_kwargs = {"framework": "pt", "device": "cpu"}
268
+ if safe_open_backend is not None:
269
+ safe_open_kwargs["backend"] = safe_open_backend
270
+ with safetensors_safe_open(str(shards[0]), **safe_open_kwargs) as checkpoint:
271
+ first_key = next(iter(checkpoint.keys()), None)
272
+ if first_key is None:
273
+ raise RuntimeError("The mounted safetensors checkpoint is empty.")
274
+ checkpoint.get_slice(first_key).get_shape()
275
+
276
+ original_mount_check = modeling_utils._is_on_hf_mount
277
+ original_safe_open = modeling_utils.safe_open
278
+
279
+ def model_mount_check(path):
280
+ if _is_model_checkpoint(path, model_path):
281
+ return False
282
+ return original_mount_check(path)
283
+
284
+ def model_safe_open(path, *args, **kwargs):
285
+ if _is_model_checkpoint(path, model_path) and os.fspath(path).endswith(".safetensors"):
286
+ if safe_open_backend is not None:
287
+ kwargs["backend"] = safe_open_backend
288
+ return original_safe_open(path, *args, **kwargs)
289
+
290
+ modeling_utils._is_on_hf_mount = model_mount_check
291
+ modeling_utils.safe_open = model_safe_open
292
+ try:
293
+ loaded = model_class.from_pretrained(
294
+ model_path,
295
+ dtype=torch.bfloat16,
296
+ device_map={"": "cuda"},
297
+ local_files_only=True,
298
+ trust_remote_code=trust_remote_code,
299
+ attn_implementation="sdpa",
300
+ output_loading_info=True,
301
+ disable_mmap=False,
302
+ use_safetensors=use_safetensors,
303
+ )
304
+ return _normalize_load_result(loaded)
305
+ finally:
306
+ modeling_utils._is_on_hf_mount = original_mount_check
307
+ modeling_utils.safe_open = original_safe_open
308
+
309
+
310
+ def _load_model_direct(
311
+ model_class,
312
+ model_path: Path,
313
+ *,
314
+ use_safetensors: bool = True,
315
+ trust_remote_code: bool = False,
316
+ ):
317
+ return _normalize_load_result(
318
+ model_class.from_pretrained(
319
+ model_path,
320
+ dtype=torch.bfloat16,
321
+ device_map={"": "cuda"},
322
+ local_files_only=True,
323
+ trust_remote_code=trust_remote_code,
324
+ attn_implementation="sdpa",
325
+ output_loading_info=True,
326
+ use_safetensors=use_safetensors,
327
+ )
328
+ )
329
+
330
+
331
+ def _load_model_candidate(
332
+ model_class,
333
+ model_path: Path,
334
+ *,
335
+ trust_remote_code: bool,
336
+ ):
337
+ for use_safetensors in (True, False):
338
+ for safe_open_backend in ("pread", "read", None):
339
+ try:
340
+ return _load_model_with_pread(
341
+ model_class,
342
+ model_path,
343
+ use_safetensors=use_safetensors,
344
+ safe_open_backend=safe_open_backend,
345
+ trust_remote_code=trust_remote_code,
346
+ )
347
+ except Exception:
348
+ pass
349
+ return _load_model_direct(model_class, model_path, use_safetensors=False, trust_remote_code=trust_remote_code)
350
+
351
+
352
+ def _load_model_candidate_or_remote(
353
+ model_class,
354
+ spec: dict[str, Any],
355
+ model_id: str,
356
+ *,
357
+ trust_remote_code: bool,
358
+ ):
359
+ model_path = spec["path"]
360
+ revision = spec["revision"]
361
+ try:
362
+ return _load_model_candidate(
363
+ model_class,
364
+ model_path,
365
+ trust_remote_code=trust_remote_code,
366
+ )
367
+ except Exception:
368
+ pass
369
+
370
+ for use_safetensors in (True, False):
371
+ try:
372
+ return _normalize_load_result(
373
+ model_class.from_pretrained(
374
+ model_id,
375
+ revision=revision,
376
+ dtype=torch.bfloat16,
377
+ device_map={"": "cuda"},
378
+ local_files_only=False,
379
+ trust_remote_code=trust_remote_code,
380
+ attn_implementation="sdpa",
381
+ output_loading_info=True,
382
+ use_safetensors=use_safetensors,
383
+ cache_dir="/tmp/huggingface-model-cache",
384
+ )
385
+ )
386
+ except Exception:
387
+ pass
388
+ raise RuntimeError("Unable to load the selected checkpoint from local mount or remote Hub download.")
389
+
390
+
391
+ def _load_runtime(model_id: str):
392
+ spec = _model_spec(model_id)
393
+ model_path = spec["path"]
394
+ revision = spec["revision"]
395
+ expected_model_type = spec["expected_model_type"]
396
+ has_mount = model_path.is_dir()
397
+ has_assistant_fallback_mount = MODEL_REGISTRY[MODEL_ID]["path"].is_dir()
398
+ use_remote = not has_mount and model_id == ASSISTANT_MODEL_ID
399
+ if not has_mount and not use_remote:
400
+ raise RuntimeError(
401
+ f"The selected Muse Glimmer full model mount is missing at {model_path}. "
402
+ "Attach the read-only model volume before starting the Space."
403
+ )
404
+
405
+ from transformers import AutoConfig, AutoModelForCausalLM, AutoProcessor, AutoTokenizer
406
+
407
+ print(
408
+ f"[startup] Loading processor from "
409
+ f"{'model repository' if use_remote else model_path} ({revision[:12]}…).",
410
+ flush=True,
411
+ )
412
+ source = model_id if use_remote else model_path
413
+ processor_kwargs = {
414
+ "revision": revision,
415
+ "local_files_only": not use_remote,
416
+ "trust_remote_code": False,
417
+ }
418
+ config_kwargs = {
419
+ "revision": revision,
420
+ "local_files_only": not use_remote,
421
+ "trust_remote_code": False,
422
+ }
423
+ if model_id == ASSISTANT_MODEL_ID:
424
+ processor_source = (
425
+ MODEL_REGISTRY[MODEL_ID]["path"] if has_assistant_fallback_mount else MODEL_ID
426
+ )
427
+ if processor_source == MODEL_REGISTRY[MODEL_ID]["path"]:
428
+ processor_kwargs["local_files_only"] = True
429
+ processor_kwargs["revision"] = MODEL_REVISION
430
+ else:
431
+ processor_kwargs["local_files_only"] = False
432
+ processor_kwargs["revision"] = MODEL_REVISION
433
+ print(
434
+ "[startup] Assistant-selected checkpoint will reuse "
435
+ f"base-tokenization assets from `{processor_source}`.",
436
+ flush=True,
437
+ )
438
+ else:
439
+ processor_source = source
440
+
441
+ if use_remote:
442
+ processor_kwargs["cache_dir"] = "/tmp/huggingface-model-cache"
443
+ config_kwargs["cache_dir"] = "/tmp/huggingface-model-cache"
444
+
445
+ if model_id == ASSISTANT_MODEL_ID:
446
+ try:
447
+ processor = AutoProcessor.from_pretrained(processor_source, **processor_kwargs)
448
+ tokenizer = AutoTokenizer.from_pretrained(processor_source, **processor_kwargs)
449
+ except Exception:
450
+ print(
451
+ "[startup] Processor loading failed without trust_remote_code; retrying with trust_remote_code=True.",
452
+ flush=True,
453
+ )
454
+ fallback_processor_kwargs = dict(processor_kwargs)
455
+ fallback_processor_kwargs["trust_remote_code"] = True
456
+ processor = AutoProcessor.from_pretrained(processor_source, **fallback_processor_kwargs)
457
+ tokenizer = AutoTokenizer.from_pretrained(processor_source, **fallback_processor_kwargs)
458
+ if not hasattr(processor, "tokenizer"):
459
+ processor.tokenizer = tokenizer
460
+ else:
461
+ try:
462
+ processor = AutoProcessor.from_pretrained(source, **processor_kwargs)
463
+ except Exception:
464
+ print(
465
+ "[startup] Processor loading failed without trust_remote_code; retrying with trust_remote_code=True.",
466
+ flush=True,
467
+ )
468
+ fallback_processor_kwargs = dict(processor_kwargs)
469
+ fallback_processor_kwargs["trust_remote_code"] = True
470
+ processor = AutoProcessor.from_pretrained(source, **fallback_processor_kwargs)
471
+
472
+ try:
473
+ config = AutoConfig.from_pretrained(source, **config_kwargs)
474
+ except Exception:
475
+ config = None
476
+ if config is None:
477
+ fallback_config_kwargs = dict(config_kwargs)
478
+ fallback_config_kwargs["trust_remote_code"] = True
479
+ try:
480
+ config = AutoConfig.from_pretrained(source, **fallback_config_kwargs)
481
+ print(
482
+ "[startup] AutoConfig with trust_remote_code succeeded for the selected checkpoint.",
483
+ flush=True,
484
+ )
485
+ except Exception as error:
486
+ raise RuntimeError("Unable to load model configuration from the selected checkpoint.") from error
487
+ model_type = getattr(config, "model_type", None)
488
+ if model_type != expected_model_type:
489
+ print(
490
+ f"[startup] Warning: checkpoint model_type={model_type} while expected {expected_model_type}. "
491
+ "Proceeding with detected architecture checks.",
492
+ flush=True,
493
+ )
494
+
495
+ if model_type == "muse_glimmer":
496
+ from transformers import MuseGlimmerForConditionalGeneration
497
+
498
+ model_candidates = ((MuseGlimmerForConditionalGeneration, False),)
499
+ elif model_type == "muse_glimmer_assistant":
500
+ try:
501
+ from transformers.models.muse_glimmer_assistant.modeling_muse_glimmer_assistant import (
502
+ MuseGlimmerAssistantModel,
503
+ )
504
+
505
+ model_candidates = (
506
+ (MuseGlimmerAssistantModel, False),
507
+ (MuseGlimmerAssistantModel, True),
508
+ )
509
+ except Exception:
510
+ model_candidates = (
511
+ (AutoModelForCausalLM, False),
512
+ (AutoModelForCausalLM, True),
513
+ )
514
+ else:
515
+ raise RuntimeError(
516
+ f"Unsupported model type from checkpoint: {model_type}. "
517
+ f"Expected {expected_model_type or 'a Muse Glimmer variant'}."
518
+ )
519
+
520
+ print("[startup] Loading the selected Muse Glimmer checkpoint onto ZeroGPU.", flush=True)
521
+ loading_info = {}
522
+ loading_error = None
523
+ model = None
524
+ used_model_class = None
525
+ try:
526
+ for model_class, trust_remote_code in model_candidates:
527
+ used_model_class = getattr(model_class, "__name__", str(model_class))
528
+ try:
529
+ if model_type == "muse_glimmer_assistant":
530
+ print(
531
+ f"[startup] Trying {used_model_class} for assistant checkpoint "
532
+ f"with trust_remote_code={trust_remote_code}.",
533
+ flush=True,
534
+ )
535
+ model, loading_info = _load_model_candidate_or_remote(
536
+ model_class,
537
+ spec,
538
+ model_id,
539
+ trust_remote_code=trust_remote_code,
540
+ )
541
+ loading_error = None
542
+ break
543
+ except Exception as error:
544
+ loading_error = error
545
+ print(
546
+ f"[startup] {used_model_class} load failed ({type(error).__name__}); trying next option if available.",
547
+ flush=True,
548
+ )
549
+ if model is None:
550
+ raise RuntimeError(f"No compatible loader could initialize model class for `{model_id}`.")
551
+ except Exception as error: # pragma: no cover - runtime-only edge
552
+ if loading_error is None:
553
+ loading_error = error
554
+ raise
555
+ if not isinstance(loading_info, dict):
556
+ loading_info = {}
557
+ loading_failures = {
558
+ key: loading_info.get(key)
559
+ for key in (
560
+ "missing_keys",
561
+ "unexpected_keys",
562
+ "mismatched_keys",
563
+ "conversion_errors",
564
+ "error_msgs",
565
+ )
566
+ if loading_info.get(key)
567
+ }
568
+ if loading_failures:
569
+ raise RuntimeError(
570
+ "The pinned checkpoint did not load cleanly: "
571
+ + ", ".join(f"{key}={len(value)}" for key, value in loading_failures.items())
572
+ )
573
+ if model_type == "muse_glimmer_assistant" and not _supports_generation(model):
574
+ print(
575
+ "[startup] Loaded assistant checkpoint is not a standalone generator; inference will fallback "
576
+ "to the full model at request time when selected.",
577
+ flush=True,
578
+ )
579
+
580
+ loaded_model_type = getattr(model.config, "model_type", None)
581
+ if loaded_model_type is not None and loaded_model_type not in EXPECTED_MODEL_TYPES:
582
+ raise RuntimeError("The selected checkpoint is not a Muse Glimmer model.")
583
+ if loaded_model_type is None:
584
+ print("[startup] Checkpoint config has no model_type; proceeding with expected loader class.", flush=True)
585
+
586
+ parameter_count = sum(parameter.numel() for parameter in model.parameters())
587
+ expected_parameter_count = spec["expected_parameter_count"]
588
+ if expected_parameter_count is not None and parameter_count != expected_parameter_count:
589
+ raise RuntimeError(
590
+ f"Unexpected parameter count: {parameter_count:,}; expected {expected_parameter_count:,}."
591
+ )
592
+
593
+ model.eval()
594
+ if loading_error is not None:
595
+ print(f"[startup] Loaded with fallback loader after: {type(loading_error).__name__}", flush=True)
596
+ print(
597
+ f"[startup] Ready: {parameter_count:,} parameters from `{model_id}` ({revision[:12]}…).",
598
+ flush=True,
599
+ )
600
+ return processor, model
601
+
602
+
603
+ def _activate_model(model_id: str):
604
+ global ACTIVE_MODEL_ID, ACTIVE_MODEL, ACTIVE_PROCESSOR, PROCESSOR, MODEL
605
+
606
+ if model_id not in MODEL_REGISTRY:
607
+ raise ValueError(f"Unknown model selection: {model_id}")
608
+
609
+ if ACTIVE_MODEL_ID == model_id and ACTIVE_MODEL is not None and ACTIVE_PROCESSOR is not None:
610
+ return ACTIVE_MODEL, ACTIVE_PROCESSOR
611
+
612
+ if ACTIVE_MODEL is not None:
613
+ del ACTIVE_MODEL
614
+ if ACTIVE_PROCESSOR is not None:
615
+ del ACTIVE_PROCESSOR
616
+ if torch.cuda.is_available():
617
+ torch.cuda.empty_cache()
618
+
619
+ ACTIVE_PROCESSOR, ACTIVE_MODEL = _load_runtime(model_id)
620
+ ACTIVE_MODEL_ID = model_id
621
+ if torch.cuda.is_available():
622
+ torch.cuda.synchronize()
623
+ PROCESSOR = ACTIVE_PROCESSOR
624
+ MODEL = ACTIVE_MODEL
625
+ return ACTIVE_PROCESSOR, ACTIVE_MODEL
626
+
627
+
628
+ if SKIP_MODEL_LOAD:
629
+ PROCESSOR = None
630
+ MODEL = None
631
+ else:
632
+ available_models = [model_id for model_id, spec in MODEL_REGISTRY.items() if spec["path"].is_dir()]
633
+ if available_models:
634
+ print(
635
+ f"[startup] Model loading deferred until first request. Available mounts: {', '.join(available_models)}",
636
+ flush=True,
637
+ )
638
+ else:
639
+ print("[startup] No checkpoint mounts are available at startup; model loading is deferred.", flush=True)
640
+
641
+ PROCESSOR = None
642
+ MODEL = None
643
+
644
+
645
+ class _StopOnEvent(StoppingCriteria):
646
+ def __init__(self, event: threading.Event):
647
+ self.event = event
648
+
649
+ def __call__(self, input_ids, scores, **kwargs):
650
+ del scores, kwargs
651
+ return torch.full(
652
+ (input_ids.shape[0],),
653
+ self.event.is_set(),
654
+ dtype=torch.bool,
655
+ device=input_ids.device,
656
+ )
657
+
658
+
659
+ def _coerce_image_input(image: Any) -> Image.Image | None:
660
+ if image is None or (isinstance(image, str) and not image):
661
+ return None
662
+ if not isinstance(image, Image.Image):
663
+ raise ValueError("The image upload could not be decoded.")
664
+ return _normalize_image(image)
665
+
666
+
667
+ def _normalize_image(image: Image.Image | None) -> Image.Image | None:
668
+ width, height = image.size
669
+ if width < 1 or height < 1:
670
+ raise ValueError("The image has invalid dimensions.")
671
+ if width * height > MAX_IMAGE_PIXELS:
672
+ scale = (MAX_IMAGE_PIXELS / float(width * height)) ** 0.5
673
+ image = image.resize(
674
+ (max(1, int(width * scale)), max(1, int(height * scale))),
675
+ Image.Resampling.LANCZOS,
676
+ )
677
+ image = ImageOps.exif_transpose(image)
678
+ image.thumbnail((MAX_IMAGE_EDGE, MAX_IMAGE_EDGE), Image.Resampling.LANCZOS)
679
+ clean = Image.new("RGB", image.size)
680
+ if image.mode == "RGBA":
681
+ background = Image.new("RGBA", image.size, "white")
682
+ background.alpha_composite(image)
683
+ clean.paste(background.convert("RGB"))
684
+ else:
685
+ clean.paste(image.convert("RGB"))
686
+ return clean
687
+
688
+
689
+ def _response_tokenizer_for(obj: Any):
690
+ tokenizer = getattr(obj, "tokenizer", None)
691
+ if tokenizer is not None:
692
+ return tokenizer
693
+ return getattr(obj, "_tokenizer", None)
694
+
695
+
696
+ def _coerce_chat_objects(processor_or_tokenizer: Any, model_id: str) -> tuple[Any, Any]:
697
+ """Return a processor/tokenizer pair that both support templating and parser wiring.
698
+
699
+ This guards against edge cases where processor loading returns an unexpected object
700
+ (for example during Transformers internals or runtime cache fallback behavior).
701
+ """
702
+ from transformers import AutoProcessor, AutoTokenizer
703
+
704
+ spec = _model_spec(model_id)
705
+ source = spec["path"] if spec["path"].is_dir() else model_id
706
+ base_kwargs = {
707
+ "revision": spec["revision"],
708
+ "local_files_only": source == spec["path"] and spec["path"].is_dir(),
709
+ "trust_remote_code": False,
710
+ }
711
+
712
+ candidates: list[Any] = [processor_or_tokenizer]
713
+ tokenized = _response_tokenizer_for(processor_or_tokenizer)
714
+ if tokenized is not None:
715
+ candidates.append(tokenized)
716
+
717
+ def _supports_template(candidate: Any) -> bool:
718
+ return candidate is not None and hasattr(candidate, "apply_chat_template")
719
+
720
+ def _valid(candidate: Any) -> bool:
721
+ return _supports_template(candidate) and hasattr(candidate, "get_response_parser")
722
+
723
+ for candidate in candidates:
724
+ if candidate is not None and _valid(candidate):
725
+ return candidate, _response_tokenizer_for(candidate) or candidate
726
+
727
+ for candidate in candidates:
728
+ if _supports_template(candidate):
729
+ return candidate, _response_tokenizer_for(candidate) or candidate
730
+
731
+ for trust_remote_code in (False, True):
732
+ fallback_kwargs = dict(base_kwargs)
733
+ fallback_kwargs["trust_remote_code"] = trust_remote_code
734
+ try:
735
+ candidate = AutoProcessor.from_pretrained(source, **fallback_kwargs)
736
+ if _valid(candidate):
737
+ return candidate, _response_tokenizer_for(candidate) or candidate
738
+ except Exception:
739
+ pass
740
+ try:
741
+ candidate = AutoTokenizer.from_pretrained(source, **fallback_kwargs)
742
+ if _valid(candidate):
743
+ return candidate, candidate
744
+ if _supports_template(candidate):
745
+ return candidate, candidate
746
+ except Exception:
747
+ pass
748
+
749
+ raise RuntimeError("Unable to initialize chat template/parser components for the selected model.")
750
+
751
+
752
+ def _parse_llm_response(text: str | None) -> tuple[str, str]:
753
+ text = (text or "").strip()
754
+ if not text:
755
+ return "", ""
756
+
757
+ think_open = "<think>"
758
+ think_close = "</think>"
759
+ start = text.find(think_open)
760
+ if start == -1:
761
+ return "", text
762
+
763
+ start += len(think_open)
764
+ close = text.find(think_close, start)
765
+ if close == -1:
766
+ return text[start:].strip(), ""
767
+
768
+ reasoning = text[start:close].strip()
769
+ content = text[close + len(think_close) :].strip()
770
+ return reasoning, content
771
+
772
+
773
+ def _user_content(prompt: str, image: Image.Image | None):
774
+ if image is None:
775
+ return prompt
776
+ return [
777
+ {"type": "image", "image": image},
778
+ {"type": "text", "text": prompt},
779
+ ]
780
+
781
+
782
+ def _visible_user_message(prompt: str, image: Image.Image | None) -> str:
783
+ if image is None:
784
+ return prompt
785
+ return f"{prompt}\n\n_🖼️ Image attached to this turn._"
786
+
787
+
788
+ def _clean_model_history(history) -> list[dict[str, Any]]:
789
+ cleaned: list[dict[str, Any]] = []
790
+ for message in list(history or [])[-MAX_HISTORY_MESSAGES:]:
791
+ if not isinstance(message, dict) or message.get("role") not in {"user", "assistant"}:
792
+ continue
793
+ if "content" not in message:
794
+ continue
795
+ safe = {"role": message["role"], "content": message["content"]}
796
+ if message["role"] == "assistant" and isinstance(message.get("reasoning_content"), str):
797
+ safe["reasoning_content"] = message["reasoning_content"]
798
+ cleaned.append(safe)
799
+ if cleaned and cleaned[0]["role"] == "assistant":
800
+ cleaned.pop(0)
801
+
802
+ # Preserve recent multimodal context without repeatedly serializing an unbounded
803
+ # number of raw PIL objects through Gradio State/ZeroGPU IPC.
804
+ kept_images = 0
805
+ for message in reversed(cleaned):
806
+ content = message.get("content")
807
+ if message.get("role") != "user" or not isinstance(content, list):
808
+ continue
809
+ has_image = any(isinstance(part, dict) and part.get("type") == "image" for part in content)
810
+ if not has_image:
811
+ continue
812
+ kept_images += 1
813
+ if kept_images <= MAX_HISTORY_IMAGES:
814
+ continue
815
+ text_parts = [
816
+ part.get("text", "")
817
+ for part in content
818
+ if isinstance(part, dict) and part.get("type") == "text"
819
+ ]
820
+ message["content"] = "\n".join(part for part in text_parts if part).strip()
821
+ return cleaned
822
+
823
+
824
+ def _apply_template(processor, messages: list[dict[str, Any]], reasoning_strength: str):
825
+ return processor.apply_chat_template(
826
+ messages,
827
+ tokenize=True,
828
+ add_generation_prompt=True,
829
+ reasoning_strength=reasoning_strength,
830
+ current_date=date.today().isoformat(),
831
+ return_dict=True,
832
+ return_tensors="pt",
833
+ )
834
+
835
+
836
+ def _prepare_inputs(
837
+ processor,
838
+ model_history,
839
+ prompt: str,
840
+ image: Image.Image | None,
841
+ system_prompt: str,
842
+ reasoning_strength: str,
843
+ max_new_tokens: int,
844
+ ):
845
+ retained = _clean_model_history(model_history)
846
+ current_user = {"role": "user", "content": _user_content(prompt, image)}
847
+ trimmed_messages = 0
848
+
849
+ while True:
850
+ messages: list[dict[str, Any]] = []
851
+ if system_prompt.strip():
852
+ messages.append({"role": "system", "content": system_prompt.strip()})
853
+ messages.extend(retained)
854
+ messages.append(current_user)
855
+ encoded = _apply_template(processor, messages, reasoning_strength)
856
+ input_tokens = int(encoded["input_ids"].shape[-1])
857
+ if input_tokens <= APP_INPUT_TOKEN_LIMIT:
858
+ break
859
+ if not retained:
860
+ raise ValueError(
861
+ f"The current turn exceeds the app input limit of {APP_INPUT_TOKEN_LIMIT:,} tokens."
862
+ )
863
+ retained.pop(0)
864
+ trimmed_messages += 1
865
+ if retained and retained[0].get("role") == "assistant":
866
+ retained.pop(0)
867
+ trimmed_messages += 1
868
+
869
+ if input_tokens + int(max_new_tokens) > MODEL_CONTEXT_TOKENS:
870
+ raise ValueError("The prompt and response budget exceed the model context window.")
871
+ return retained, current_user, encoded, input_tokens, trimmed_messages
872
+
873
+
874
+ def _move_inputs_to_model(model, encoded):
875
+ device = next(model.parameters()).device
876
+ moved = {}
877
+ for key, value in encoded.items():
878
+ if not torch.is_tensor(value):
879
+ moved[key] = value
880
+ continue
881
+ value = value.to(device)
882
+ if key in {"pixel_values", "pixel_values_videos"} and value.is_floating_point():
883
+ value = value.to(dtype=torch.bfloat16)
884
+ moved[key] = value
885
+ return moved
886
+
887
+
888
+ def _gpu_duration(
889
+ prompt,
890
+ image,
891
+ selected_model,
892
+ chat_history,
893
+ model_history,
894
+ system_prompt,
895
+ reasoning_strength,
896
+ do_sample,
897
+ max_new_tokens,
898
+ temperature,
899
+ top_p,
900
+ top_k,
901
+ repetition_penalty,
902
+ seed,
903
+ randomize_seed,
904
+ show_reasoning,
905
+ ):
906
+ _ = (
907
+ prompt,
908
+ chat_history,
909
+ system_prompt,
910
+ do_sample,
911
+ temperature,
912
+ top_p,
913
+ top_k,
914
+ repetition_penalty,
915
+ seed,
916
+ randomize_seed,
917
+ show_reasoning,
918
+ )
919
+
920
+ selected_model = _coerce_model_id(selected_model) or MODEL_DEFAULT_ID
921
+ try:
922
+ max_new_tokens = int(max_new_tokens)
923
+ except Exception:
924
+ max_new_tokens = DEFAULT_MAX_NEW_TOKENS
925
+
926
+ has_image = False
927
+ try:
928
+ has_image = _coerce_image_input(image) is not None
929
+ except ValueError:
930
+ has_image = False
931
+
932
+ needs_warmup = selected_model != ACTIVE_MODEL_ID
933
+ estimated = estimate_gpu_duration(max_new_tokens, has_image)
934
+ if selected_model == MODEL_ID and needs_warmup:
935
+ return min(estimated, DEFAULT_COLD_START_DURATION_SECONDS)
936
+ if selected_model == ASSISTANT_MODEL_ID:
937
+ if ACTIVE_MODEL_ID == MODEL_ID:
938
+ return estimated
939
+ return min(estimated, DEFAULT_COLD_START_DURATION_SECONDS)
940
+ return estimated
941
+
942
+
943
+ def _format_status(
944
+ *,
945
+ phase: str,
946
+ selected_model: str,
947
+ input_tokens: int,
948
+ output_tokens: int,
949
+ elapsed: float,
950
+ used_seed: int,
951
+ do_sample: bool,
952
+ trimmed_messages: int,
953
+ ) -> str:
954
+ mode = "sampling" if do_sample else "native greedy"
955
+ trimmed = f" · trimmed {trimmed_messages} old messages" if trimmed_messages else ""
956
+ return (
957
+ f"{phase} · {input_tokens:,} input / {output_tokens:,} output tokens · "
958
+ f"{elapsed:.1f}s · {mode} · seed {used_seed}{trimmed} · {selected_model}"
959
+ )
960
+
961
+
962
+ @spaces.GPU(size="xlarge", duration=_gpu_duration)
963
+ def _generate_turn(
964
+ prompt,
965
+ image,
966
+ selected_model,
967
+ chat_history,
968
+ model_history,
969
+ system_prompt,
970
+ reasoning_strength,
971
+ do_sample,
972
+ max_new_tokens,
973
+ temperature,
974
+ top_p,
975
+ top_k,
976
+ repetition_penalty,
977
+ seed,
978
+ randomize_seed,
979
+ show_reasoning,
980
+ ):
981
+ original_chat = list(chat_history or [])
982
+ original_model_history = list(model_history or [])
983
+ generation_thread: threading.Thread | None = None
984
+ stop_event = threading.Event()
985
+
986
+ try:
987
+ selected_model = _coerce_model_id(selected_model) or MODEL_DEFAULT_ID
988
+ selected_model_name = _model_spec(selected_model).get("display", selected_model)
989
+ active_inference_model = selected_model
990
+ model_fallback = False
991
+ processor, model = _activate_model(selected_model)
992
+ if active_inference_model == ASSISTANT_MODEL_ID and not _supports_generation(model):
993
+ print(
994
+ "[inference] Assistant checkpoint does not expose generate(); falling back to full model for this request.",
995
+ flush=True,
996
+ )
997
+ model_fallback = True
998
+ active_inference_model = MODEL_ID
999
+ processor, model = _activate_model(active_inference_model)
1000
+ selected_model_name = _model_spec(MODEL_ID).get("display", MODEL_ID)
1001
+ if processor is None or model is None:
1002
+ raise RuntimeError("Model loading is unavailable for this request.")
1003
+ prompt = (prompt or "").strip()
1004
+ if not prompt:
1005
+ raise ValueError("Write a prompt before generating.")
1006
+ if len(prompt) > 20_000:
1007
+ raise ValueError("The prompt is too long; keep it below 20,000 characters.")
1008
+
1009
+ max_new_tokens = int(DEFAULT_MAX_NEW_TOKENS if max_new_tokens is None else max_new_tokens)
1010
+ temperature = DEFAULT_TEMPERATURE if temperature is None else float(temperature)
1011
+ top_p = DEFAULT_TOP_P if top_p is None else float(top_p)
1012
+ top_k = DEFAULT_TOP_K if top_k is None else int(top_k)
1013
+ repetition_penalty = (
1014
+ DEFAULT_REPETITION_PENALTY
1015
+ if repetition_penalty is None
1016
+ else float(repetition_penalty)
1017
+ )
1018
+ validate_controls(
1019
+ max_new_tokens=max_new_tokens,
1020
+ temperature=temperature,
1021
+ top_p=top_p,
1022
+ top_k=top_k,
1023
+ repetition_penalty=repetition_penalty,
1024
+ reasoning_strength=reasoning_strength,
1025
+ )
1026
+ used_seed = choose_seed(seed, bool(randomize_seed))
1027
+ clean_image = _coerce_image_input(image)
1028
+ processor, response_parser_tokenizer = _coerce_chat_objects(
1029
+ processor, active_inference_model
1030
+ )
1031
+ retained, current_user, encoded, input_tokens, trimmed_messages = _prepare_inputs(
1032
+ processor,
1033
+ original_model_history,
1034
+ prompt,
1035
+ clean_image,
1036
+ system_prompt or "",
1037
+ reasoning_strength,
1038
+ max_new_tokens,
1039
+ )
1040
+
1041
+ model_inputs = _move_inputs_to_model(model, encoded)
1042
+ input_length = int(model_inputs["input_ids"].shape[-1])
1043
+ prefix_ids = encoded["input_ids"][0].detach().cpu()
1044
+
1045
+ torch.manual_seed(used_seed)
1046
+ torch.cuda.manual_seed_all(used_seed)
1047
+
1048
+ streamer = TextIteratorStreamer(
1049
+ response_parser_tokenizer,
1050
+ skip_prompt=True,
1051
+ skip_special_tokens=False,
1052
+ timeout=5.0,
1053
+ )
1054
+ parser = (
1055
+ response_parser_tokenizer.get_response_parser(prefix=prefix_ids)
1056
+ if hasattr(response_parser_tokenizer, "get_response_parser")
1057
+ else None
1058
+ )
1059
+ buffers = {"reasoning_content": "", "content": ""}
1060
+ streamed_chunks: list[str] = []
1061
+ if parser is not None:
1062
+ for event in parser.initial_events:
1063
+ if event.get("type") == "region_chunk" and event.get("field") in buffers:
1064
+ buffers[event["field"]] += event.get("text", "")
1065
+
1066
+ kwargs = {
1067
+ **model_inputs,
1068
+ **generation_kwargs(
1069
+ do_sample=bool(do_sample),
1070
+ max_new_tokens=max_new_tokens,
1071
+ temperature=temperature,
1072
+ top_p=top_p,
1073
+ top_k=top_k,
1074
+ repetition_penalty=repetition_penalty,
1075
+ ),
1076
+ "streamer": streamer,
1077
+ "stopping_criteria": StoppingCriteriaList([_StopOnEvent(stop_event)]),
1078
+ "max_time": float(max(30, estimate_gpu_duration(max_new_tokens, clean_image is not None))),
1079
+ }
1080
+ errors: list[BaseException] = []
1081
+ result_box: list[Any] = []
1082
+
1083
+ def run_model() -> None:
1084
+ try:
1085
+ with torch.inference_mode():
1086
+ result_box.append(model.generate(**kwargs))
1087
+ except BaseException as error:
1088
+ errors.append(error)
1089
+ streamer.on_finalized_text("", stream_end=True)
1090
+
1091
+ generation_thread = threading.Thread(target=run_model, daemon=True)
1092
+ started = time.perf_counter()
1093
+ generation_thread.start()
1094
+
1095
+ user_message = {"role": "user", "content": _visible_user_message(prompt, clean_image)}
1096
+ working_chat = original_chat + [user_message]
1097
+ last_yield = 0.0
1098
+
1099
+ while True:
1100
+ try:
1101
+ chunk = next(streamer)
1102
+ except queue.Empty:
1103
+ if not generation_thread.is_alive():
1104
+ if errors:
1105
+ break
1106
+ raise RuntimeError("The generation stream ended unexpectedly.")
1107
+ now = time.perf_counter()
1108
+ yield (
1109
+ working_chat
1110
+ + [
1111
+ {
1112
+ "role": "assistant",
1113
+ "content": render_reply(
1114
+ buffers["reasoning_content"],
1115
+ buffers["content"],
1116
+ show_reasoning=bool(show_reasoning),
1117
+ pending=True,
1118
+ ),
1119
+ }
1120
+ ],
1121
+ gr.skip(),
1122
+ gr.skip(),
1123
+ gr.skip(),
1124
+ gr.skip(),
1125
+ _format_status(
1126
+ selected_model=(
1127
+ f"{selected_model_name} (assistant checkpoint fallback to full model)"
1128
+ if model_fallback
1129
+ else selected_model_name
1130
+ ),
1131
+ phase="Generating",
1132
+ input_tokens=input_tokens,
1133
+ output_tokens=0,
1134
+ elapsed=now - started,
1135
+ used_seed=used_seed,
1136
+ do_sample=bool(do_sample),
1137
+ trimmed_messages=trimmed_messages,
1138
+ ),
1139
+ )
1140
+ last_yield = now
1141
+ continue
1142
+ except StopIteration:
1143
+ break
1144
+
1145
+ if parser is not None:
1146
+ for event in parser.feed(chunk):
1147
+ field = event.get("field")
1148
+ if field not in buffers:
1149
+ continue
1150
+ if event.get("type") == "region_chunk":
1151
+ buffers[field] += event.get("text", "")
1152
+ elif event.get("type") == "region_close" and isinstance(event.get("value"), str):
1153
+ buffers[field] = event["value"]
1154
+ else:
1155
+ streamed_chunks.append(chunk)
1156
+ reasoning, content = _parse_llm_response("".join(streamed_chunks))
1157
+ buffers["reasoning_content"] = reasoning
1158
+ buffers["content"] = content
1159
+
1160
+ now = time.perf_counter()
1161
+ if now - last_yield < 0.06:
1162
+ continue
1163
+ partial = render_reply(
1164
+ buffers["reasoning_content"],
1165
+ buffers["content"],
1166
+ show_reasoning=bool(show_reasoning),
1167
+ pending=True,
1168
+ )
1169
+ elapsed = now - started
1170
+ yield (
1171
+ working_chat + [{"role": "assistant", "content": partial}],
1172
+ gr.skip(),
1173
+ gr.skip(),
1174
+ gr.skip(),
1175
+ gr.skip(),
1176
+ _format_status(
1177
+ selected_model=(
1178
+ f"{selected_model_name} (assistant checkpoint fallback to full model)"
1179
+ if model_fallback
1180
+ else selected_model_name
1181
+ ),
1182
+ phase="Generating",
1183
+ input_tokens=input_tokens,
1184
+ output_tokens=0,
1185
+ elapsed=elapsed,
1186
+ used_seed=used_seed,
1187
+ do_sample=bool(do_sample),
1188
+ trimmed_messages=trimmed_messages,
1189
+ ),
1190
+ )
1191
+ last_yield = now
1192
+
1193
+ generation_thread.join(timeout=3)
1194
+ if generation_thread.is_alive():
1195
+ raise RuntimeError(
1196
+ "Generation exceeded its timeout envelope. "
1197
+ "Lower the response budget and try again."
1198
+ )
1199
+ if errors:
1200
+ raise errors[0]
1201
+
1202
+ if parser is not None:
1203
+ parsed_message, final_events = parser.finalize()
1204
+ for event in final_events:
1205
+ field = event.get("field")
1206
+ if (
1207
+ field in buffers
1208
+ and event.get("type") == "region_close"
1209
+ and isinstance(event.get("value"), str)
1210
+ ):
1211
+ buffers[field] = event["value"]
1212
+ parsed = coerce_parsed_reply(parsed_message)
1213
+ reasoning = parsed.reasoning or buffers["reasoning_content"].strip()
1214
+ content = parsed.content or buffers["content"].strip()
1215
+ else:
1216
+ reasoning, content = _parse_llm_response("".join(streamed_chunks))
1217
+
1218
+ output_tokens = 0
1219
+ ended_with_limit = False
1220
+ if result_box:
1221
+ generated = result_box[0]
1222
+ output_tokens = int(generated.shape[-1]) - input_length
1223
+ ended_with_limit = output_tokens >= max_new_tokens
1224
+ if not reasoning and not content:
1225
+ raise RuntimeError("The model returned no visible response fields.")
1226
+
1227
+ visible_reply = render_reply(
1228
+ reasoning,
1229
+ content,
1230
+ show_reasoning=bool(show_reasoning),
1231
+ hit_token_limit=ended_with_limit,
1232
+ )
1233
+ assistant_state = {"role": "assistant", "content": content}
1234
+ if reasoning:
1235
+ assistant_state["reasoning_content"] = reasoning
1236
+ updated_model_history = retained + [current_user, assistant_state]
1237
+ updated_chat = working_chat + [{"role": "assistant", "content": visible_reply}]
1238
+ elapsed = time.perf_counter() - started
1239
+
1240
+ yield (
1241
+ updated_chat,
1242
+ updated_model_history,
1243
+ updated_chat,
1244
+ "",
1245
+ None,
1246
+ _format_status(
1247
+ selected_model=(
1248
+ f"{selected_model_name} (assistant checkpoint fallback to full model)"
1249
+ if model_fallback
1250
+ else selected_model_name
1251
+ ),
1252
+ phase="Complete",
1253
+ input_tokens=input_tokens,
1254
+ output_tokens=output_tokens,
1255
+ elapsed=elapsed,
1256
+ used_seed=used_seed,
1257
+ do_sample=bool(do_sample),
1258
+ trimmed_messages=trimmed_messages,
1259
+ ),
1260
+ )
1261
+ except GeneratorExit:
1262
+ raise
1263
+ except BaseException as error:
1264
+ print(f"[inference] {type(error).__name__}: {error}", flush=True)
1265
+ if torch.cuda.is_available():
1266
+ torch.cuda.empty_cache()
1267
+ yield (
1268
+ original_chat,
1269
+ gr.skip(),
1270
+ original_chat,
1271
+ gr.skip(),
1272
+ gr.skip(),
1273
+ f"Error · {friendly_error(error)}",
1274
+ )
1275
+ finally:
1276
+ stop_event.set()
1277
+ if generation_thread is not None and generation_thread.is_alive():
1278
+ generation_thread.join(timeout=3)
1279
+ if generation_thread.is_alive():
1280
+ print("[inference] Generation worker did not stop within grace window.", flush=True)
1281
+
1282
+
1283
+ def _validate_generation_request(
1284
+ prompt,
1285
+ image,
1286
+ selected_model,
1287
+ chat_history,
1288
+ model_history,
1289
+ system_prompt,
1290
+ reasoning_strength,
1291
+ do_sample,
1292
+ max_new_tokens,
1293
+ temperature,
1294
+ top_p,
1295
+ top_k,
1296
+ repetition_penalty,
1297
+ seed,
1298
+ randomize_seed,
1299
+ show_reasoning,
1300
+ ):
1301
+ del chat_history, model_history, do_sample, show_reasoning
1302
+ valid = True
1303
+ message = ""
1304
+ try:
1305
+ selected_model = _coerce_model_id(selected_model) or MODEL_DEFAULT_ID
1306
+ spec = _model_spec(selected_model)
1307
+ max_new_tokens = 32 if max_new_tokens is None else int(max_new_tokens)
1308
+ temperature = 1.0 if temperature is None else float(temperature)
1309
+ top_p = 0.95 if top_p is None else float(top_p)
1310
+ top_k = 64 if top_k is None else int(top_k)
1311
+ repetition_penalty = 1.0 if repetition_penalty is None else float(repetition_penalty)
1312
+ image = _coerce_image_input(image)
1313
+ if not spec["path"].is_dir() and selected_model != ASSISTANT_MODEL_ID:
1314
+ raise ValueError(f"The selected model checkpoint is not mounted at {spec['path']}.")
1315
+ if selected_model == ASSISTANT_MODEL_ID and not MODEL_REGISTRY[MODEL_ID]["path"].is_dir():
1316
+ raise ValueError(
1317
+ "Assistant checkpoint inference currently falls back to the full model, "
1318
+ f"but the full model mount is missing at {MODEL_REGISTRY[MODEL_ID]['path']}."
1319
+ )
1320
+ prompt = (prompt or "").strip()
1321
+ if not prompt:
1322
+ raise ValueError("Write a prompt before generating.")
1323
+ if len(prompt) > 20_000:
1324
+ raise ValueError("The prompt is too long; keep it below 20,000 characters.")
1325
+ if len(system_prompt or "") > 20_000:
1326
+ raise ValueError("The system instruction is too long; keep it below 20,000 characters.")
1327
+ validate_controls(
1328
+ max_new_tokens=max_new_tokens,
1329
+ temperature=temperature,
1330
+ top_p=top_p,
1331
+ top_k=top_k,
1332
+ repetition_penalty=repetition_penalty,
1333
+ reasoning_strength=reasoning_strength,
1334
+ )
1335
+ if not bool(randomize_seed):
1336
+ choose_seed(seed, False)
1337
+ except (TypeError, ValueError) as error:
1338
+ valid = False
1339
+ message = str(error)
1340
+
1341
+ verdicts = [gr.validate(valid, message)]
1342
+ verdicts.extend(gr.validate(True, "") for _ in range(15))
1343
+ return tuple(verdicts)
1344
+
1345
+
1346
+ def _stop_conversation(chat_snapshot):
1347
+ return list(chat_snapshot or []), "Stopped · the unfinished turn was not added to model history"
1348
+
1349
+
1350
+ def _clear_conversation():
1351
+ return [], [], [], "", None, None, "Ready · native greedy · reasoning high"
1352
+
1353
+
1354
+ def _set_preset(name: str):
1355
+ return preset_values(name)
1356
+
1357
+
1358
+ CSS = """
1359
+ :root {
1360
+ --ink: #161225;
1361
+ --muted: #686177;
1362
+ --line: #e8e1f1;
1363
+ --paper: #ffffff;
1364
+ --wash: #faf8fd;
1365
+ --violet: #6d28d9;
1366
+ --cyan: #0e7490;
1367
+ }
1368
+
1369
+ .gradio-container {
1370
+ max-width: 1180px !important;
1371
+ margin: 0 auto !important;
1372
+ background:
1373
+ radial-gradient(circle at 8% 0%, rgba(109, 40, 217, .12), transparent 31rem),
1374
+ radial-gradient(circle at 92% 0%, rgba(14, 116, 144, .10), transparent 29rem),
1375
+ var(--wash);
1376
+ }
1377
+
1378
+ #hero {
1379
+ padding: 26px 28px 22px;
1380
+ border: 1px solid var(--line);
1381
+ border-radius: 22px;
1382
+ background: rgba(255, 255, 255, .90);
1383
+ box-shadow: 0 18px 50px rgba(41, 24, 72, .07);
1384
+ }
1385
+
1386
+ #hero h1 { margin-bottom: 7px; letter-spacing: -.03em; }
1387
+ #hero p { color: var(--muted); margin-bottom: 0; }
1388
+ #chat { border: 1px solid var(--line); border-radius: 18px; background: var(--paper); }
1389
+ #prompt textarea, .message-wrap, .prose, .md { unicode-bidi: plaintext; text-align: start; }
1390
+ #prompt textarea { direction: auto; font-size: 1rem; }
1391
+ #run-button { min-height: 52px; }
1392
+ .status { color: var(--muted); min-height: 28px; }
1393
+ .privacy-note { color: var(--muted); font-size: .88rem; }
1394
+
1395
+ @media (max-width: 760px) {
1396
+ #hero { padding: 19px; }
1397
+ .gradio-container { padding: 9px !important; }
1398
+ }
1399
+ """
1400
+
1401
+
1402
+ THEME = gr.themes.Soft(
1403
+ primary_hue="violet",
1404
+ secondary_hue="cyan",
1405
+ neutral_hue="slate",
1406
+ )
1407
+
1408
+
1409
+ with gr.Blocks(title="Muse Glimmer 30B", analytics_enabled=False) as demo:
1410
+ selected_model = gr.Dropdown(
1411
+ choices=MODEL_CHOICES,
1412
+ value=MODEL_DEFAULT_ID,
1413
+ label="Model checkpoint",
1414
+ info="Choose the full BF16 or compact assistant checkpoint for this turn.",
1415
+ interactive=True,
1416
+ allow_custom_value=True,
1417
+ )
1418
+ model_history = gr.State([])
1419
+ committed_chat = gr.State([])
1420
+ selected_image = gr.State(None)
1421
+
1422
+ gr.Markdown(
1423
+ """
1424
+ # Muse Glimmer · private inference
1425
+ Text + image chat on either the official **full BF16** model or its **assistant checkpoint**.
1426
+ Native greedy decoding is the default; Meta's sampling recipe is one click away. Reasoning is
1427
+ parsed separately.
1428
+ """,
1429
+ elem_id="hero",
1430
+ )
1431
+
1432
+ chatbot = gr.Chatbot(
1433
+ label="Conversation",
1434
+ height=570,
1435
+ layout="panel",
1436
+ buttons=["copy", "copy_all"],
1437
+ reasoning_tags=[("<think>", "</think>")],
1438
+ placeholder="Ask a question or attach an image to begin.",
1439
+ sanitize_html=True,
1440
+ elem_id="chat",
1441
+ )
1442
+ status = gr.Markdown(
1443
+ "Ready · native greedy · reasoning high",
1444
+ elem_classes="status",
1445
+ )
1446
+
1447
+ with gr.Row(equal_height=True):
1448
+ prompt = gr.Textbox(
1449
+ label="Prompt",
1450
+ placeholder="Ask in English, עברית, العربية, or another supported language…",
1451
+ lines=3,
1452
+ max_lines=9,
1453
+ max_length=20_000,
1454
+ autofocus=True,
1455
+ scale=4,
1456
+ elem_id="prompt",
1457
+ )
1458
+ image = gr.Image(
1459
+ label="Optional image · this turn",
1460
+ type="pil",
1461
+ sources=["upload", "clipboard"],
1462
+ height=180,
1463
+ scale=2,
1464
+ )
1465
+ IMAGE_CHANGE_API_NAME = "set_image"
1466
+ PRESET_CHANGE_API_NAME = "set_generation_preset"
1467
+ STOP_API_NAME = "stop_generation"
1468
+ CLEAR_API_NAME = "clear_conversation"
1469
+
1470
+ image.change(
1471
+ _coerce_image_input,
1472
+ inputs=image,
1473
+ outputs=selected_image,
1474
+ queue=False,
1475
+ api_name=IMAGE_CHANGE_API_NAME,
1476
+ api_visibility="private",
1477
+ )
1478
+
1479
+ with gr.Row():
1480
+ run_button = gr.Button("Generate", variant="primary", elem_id="run-button")
1481
+ stop_button = gr.Button("Stop", variant="stop")
1482
+ clear_button = gr.Button("Clear")
1483
+
1484
+ with gr.Accordion("Generation controls", open=False):
1485
+ preset = gr.Radio(
1486
+ choices=list(PRESETS),
1487
+ value=NATIVE_GREEDY,
1488
+ label="Preset",
1489
+ info="Native greedy matches generation_config.json. Meta sampling applies the model-card recipe.",
1490
+ )
1491
+ with gr.Row():
1492
+ reasoning_strength = gr.Dropdown(
1493
+ choices=["low", "medium", "high", "xhigh"],
1494
+ value="high",
1495
+ label="Reasoning strength",
1496
+ )
1497
+ max_new_tokens = gr.Slider(
1498
+ minimum=32,
1499
+ maximum=MAX_NEW_TOKENS,
1500
+ value=DEFAULT_MAX_NEW_TOKENS,
1501
+ step=32,
1502
+ label="Max new tokens",
1503
+ info="App response budget; 512 is the default.",
1504
+ )
1505
+ repetition_penalty = gr.Slider(
1506
+ minimum=0.8,
1507
+ maximum=1.3,
1508
+ value=DEFAULT_REPETITION_PENALTY,
1509
+ step=0.01,
1510
+ label="Repetition penalty",
1511
+ )
1512
+
1513
+ do_sample = gr.Checkbox(
1514
+ value=False,
1515
+ label="Sampling",
1516
+ info="Off is the checkpoint default. When off, temperature/top-p/top-k are ignored.",
1517
+ )
1518
+ with gr.Row():
1519
+ temperature = gr.Slider(
1520
+ minimum=0.05,
1521
+ maximum=2.0,
1522
+ value=DEFAULT_TEMPERATURE,
1523
+ step=0.05,
1524
+ label="Temperature",
1525
+ )
1526
+ top_p = gr.Slider(
1527
+ minimum=0.05,
1528
+ maximum=1.0,
1529
+ value=DEFAULT_TOP_P,
1530
+ step=0.01,
1531
+ label="Top-p",
1532
+ )
1533
+ top_k = gr.Slider(
1534
+ minimum=1,
1535
+ maximum=200,
1536
+ value=DEFAULT_TOP_K,
1537
+ step=1,
1538
+ label="Top-k",
1539
+ )
1540
+
1541
+ with gr.Row():
1542
+ seed = gr.Number(
1543
+ value=DEFAULT_SEED,
1544
+ precision=0,
1545
+ minimum=0,
1546
+ maximum=2_147_483_647,
1547
+ label="Seed",
1548
+ )
1549
+ randomize_seed = gr.Checkbox(value=False, label="Randomize seed each turn")
1550
+ show_reasoning = gr.Checkbox(value=True, label="Show reasoning")
1551
+
1552
+ system_prompt = gr.Textbox(
1553
+ value="",
1554
+ label="Optional system instruction",
1555
+ placeholder="Blank uses the model's built-in helpful-assistant system message.",
1556
+ lines=3,
1557
+ max_length=20_000,
1558
+ )
1559
+
1560
+ gr.Markdown(
1561
+ f"""
1562
+ **Private Space.** This app adds no prompt, reply, or image persistence and does not log
1563
+ their contents. Inference runs on Hugging Face-hosted ZeroGPU `xlarge`; `xlarge` uses 2×
1564
+ ZeroGPU quota. Model revisions: `{MODEL_REVISION}` and `{ASSISTANT_MODEL_REVISION}`.
1565
+ No tools are connected or executed.
1566
+ [Usage policy](https://huggingface.co/meta-models/Muse-Glimmer-30B/blob/{MODEL_REVISION}/USAGE_POLICY.md)
1567
+ """,
1568
+ elem_classes="privacy-note",
1569
+ )
1570
+
1571
+ preset.change(
1572
+ _set_preset,
1573
+ inputs=preset,
1574
+ outputs=[do_sample, temperature, top_p, top_k],
1575
+ queue=False,
1576
+ api_name=PRESET_CHANGE_API_NAME,
1577
+ api_visibility="private",
1578
+ )
1579
+
1580
+ generation_inputs = [
1581
+ prompt,
1582
+ selected_image,
1583
+ selected_model,
1584
+ chatbot,
1585
+ model_history,
1586
+ system_prompt,
1587
+ reasoning_strength,
1588
+ do_sample,
1589
+ max_new_tokens,
1590
+ temperature,
1591
+ top_p,
1592
+ top_k,
1593
+ repetition_penalty,
1594
+ seed,
1595
+ randomize_seed,
1596
+ show_reasoning,
1597
+ ]
1598
+ generation_outputs = [chatbot, model_history, committed_chat, prompt, image, status]
1599
+
1600
+ generation_event = run_button.click(
1601
+ fn=_generate_turn,
1602
+ inputs=generation_inputs,
1603
+ outputs=generation_outputs,
1604
+ concurrency_limit=1,
1605
+ concurrency_id="muse-glimmer-xlarge",
1606
+ trigger_mode="once",
1607
+ api_name="chat",
1608
+ api_visibility="private",
1609
+ api_description="Run a private Muse Glimmer text or image chat turn.",
1610
+ show_progress="minimal",
1611
+ validator=_validate_generation_request,
1612
+ )
1613
+
1614
+ submit_event = prompt.submit(
1615
+ fn=_generate_turn,
1616
+ inputs=generation_inputs,
1617
+ outputs=generation_outputs,
1618
+ concurrency_limit=1,
1619
+ concurrency_id="muse-glimmer-xlarge",
1620
+ trigger_mode="once",
1621
+ api_name=SUBMIT_API_NAME,
1622
+ api_visibility="private",
1623
+ api_description="Submit a private Muse Glimmer text or image chat turn.",
1624
+ show_progress="minimal",
1625
+ queue=True,
1626
+ validator=_validate_generation_request,
1627
+ )
1628
+
1629
+ stop_button.click(
1630
+ _stop_conversation,
1631
+ inputs=committed_chat,
1632
+ outputs=[chatbot, status],
1633
+ cancels=[generation_event, submit_event],
1634
+ queue=False,
1635
+ api_name=STOP_API_NAME,
1636
+ api_visibility="private",
1637
+ )
1638
+
1639
+ clear_button.click(
1640
+ _clear_conversation,
1641
+ inputs=None,
1642
+ outputs=[chatbot, model_history, committed_chat, prompt, image, selected_image, status],
1643
+ cancels=[generation_event, submit_event],
1644
+ queue=False,
1645
+ api_name=CLEAR_API_NAME,
1646
+ api_visibility="private",
1647
+ )
1648
+
1649
+
1650
+ demo.queue(default_concurrency_limit=1, max_size=8)
1651
+
1652
+
1653
+ if __name__ == "__main__":
1654
+ demo.launch(theme=THEME, css=CSS)
muse_core.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pure helpers for the Muse Glimmer Space.
2
+
3
+ This module deliberately has no Torch, Transformers, Gradio, or Spaces dependency so its
4
+ behavior can be tested without downloading or loading the 30B checkpoint.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ import secrets
11
+ from typing import Any, Callable
12
+
13
+
14
+ MODEL_CONTEXT_TOKENS = 131_072
15
+ APP_INPUT_TOKEN_LIMIT = 16_384
16
+ DEFAULT_MAX_NEW_TOKENS = 512
17
+ MIN_NEW_TOKENS = 32
18
+ MAX_NEW_TOKENS = 1_024
19
+ DEFAULT_TEMPERATURE = 1.0
20
+ DEFAULT_TOP_P = 0.95
21
+ DEFAULT_TOP_K = 64
22
+ DEFAULT_REPETITION_PENALTY = 1.0
23
+ DEFAULT_SEED = 42
24
+ VALID_REASONING_STRENGTHS = ("low", "medium", "high", "xhigh")
25
+
26
+ NATIVE_GREEDY = "Native greedy · checkpoint default"
27
+ META_SAMPLING = "Meta recommended sampling"
28
+ PRESETS = (NATIVE_GREEDY, META_SAMPLING)
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class ParsedReply:
33
+ reasoning: str
34
+ content: str
35
+ tool_calls: Any = None
36
+
37
+
38
+ def preset_values(name: str) -> tuple[bool, float, float, int]:
39
+ """Return sampling controls for one of the two documented presets."""
40
+ if name == META_SAMPLING:
41
+ return True, DEFAULT_TEMPERATURE, DEFAULT_TOP_P, DEFAULT_TOP_K
42
+ return False, DEFAULT_TEMPERATURE, DEFAULT_TOP_P, DEFAULT_TOP_K
43
+
44
+
45
+ def choose_seed(
46
+ seed: int | float | None,
47
+ randomize: bool,
48
+ randbelow: Callable[[int], int] = secrets.randbelow,
49
+ ) -> int:
50
+ """Resolve a valid Torch seed without relying on mutable global state."""
51
+ if randomize:
52
+ return int(randbelow(2_147_483_648))
53
+ if seed is None:
54
+ return DEFAULT_SEED
55
+ resolved = int(seed)
56
+ if not 0 <= resolved <= 2_147_483_647:
57
+ raise ValueError("Seed must be between 0 and 2,147,483,647.")
58
+ return resolved
59
+
60
+
61
+ def validate_controls(
62
+ *,
63
+ max_new_tokens: int | float,
64
+ temperature: float,
65
+ top_p: float,
66
+ top_k: int | float,
67
+ repetition_penalty: float,
68
+ reasoning_strength: str,
69
+ ) -> None:
70
+ tokens = int(max_new_tokens)
71
+ if not MIN_NEW_TOKENS <= tokens <= MAX_NEW_TOKENS:
72
+ raise ValueError(f"Max new tokens must be between {MIN_NEW_TOKENS} and {MAX_NEW_TOKENS}.")
73
+ if not 0.05 <= float(temperature) <= 2.0:
74
+ raise ValueError("Temperature must be between 0.05 and 2.0.")
75
+ if not 0.05 <= float(top_p) <= 1.0:
76
+ raise ValueError("Top-p must be between 0.05 and 1.0.")
77
+ if not 1 <= int(top_k) <= 200:
78
+ raise ValueError("Top-k must be between 1 and 200.")
79
+ if not 0.8 <= float(repetition_penalty) <= 1.3:
80
+ raise ValueError("Repetition penalty must be between 0.8 and 1.3.")
81
+ if reasoning_strength not in VALID_REASONING_STRENGTHS:
82
+ raise ValueError("Unsupported reasoning strength.")
83
+
84
+
85
+ def generation_kwargs(
86
+ *,
87
+ do_sample: bool,
88
+ max_new_tokens: int | float,
89
+ temperature: float,
90
+ top_p: float,
91
+ top_k: int | float,
92
+ repetition_penalty: float,
93
+ ) -> dict[str, Any]:
94
+ """Build generation arguments while preserving the checkpoint's dual EOS contract."""
95
+ kwargs: dict[str, Any] = {
96
+ "max_new_tokens": int(max_new_tokens),
97
+ "do_sample": bool(do_sample),
98
+ "eos_token_id": [200_001, 200_008],
99
+ "pad_token_id": 200_018,
100
+ "use_cache": True,
101
+ "repetition_penalty": float(repetition_penalty),
102
+ }
103
+ if do_sample:
104
+ kwargs.update(
105
+ temperature=float(temperature),
106
+ top_p=float(top_p),
107
+ top_k=int(top_k),
108
+ )
109
+ return kwargs
110
+
111
+
112
+ def estimate_gpu_duration(max_new_tokens: int | float, has_image: bool) -> int:
113
+ """Return a bounded ZeroGPU reservation in seconds.
114
+
115
+ This is intentionally conservative for a dense 30B BF16 model. It is a maximum reservation,
116
+ not a claim that each call consumes the full amount.
117
+ """
118
+ tokens = max(MIN_NEW_TOKENS, min(MAX_NEW_TOKENS, int(max_new_tokens)))
119
+ seconds = 55 + int(tokens * 0.13) + (25 if has_image else 0)
120
+ return max(60, min(240, seconds))
121
+
122
+
123
+ def coerce_parsed_reply(message: dict[str, Any] | None) -> ParsedReply:
124
+ """Normalize the fields produced by Transformers' native response parser."""
125
+ message = message or {}
126
+
127
+ def text(value: Any) -> str:
128
+ if value is None:
129
+ return ""
130
+ if isinstance(value, str):
131
+ return value.strip()
132
+ if isinstance(value, list):
133
+ chunks: list[str] = []
134
+ for part in value:
135
+ if isinstance(part, str):
136
+ chunks.append(part)
137
+ elif isinstance(part, dict) and isinstance(part.get("text"), str):
138
+ chunks.append(part["text"])
139
+ return "\n".join(chunks).strip()
140
+ return str(value).strip()
141
+
142
+ return ParsedReply(
143
+ reasoning=text(message.get("reasoning_content")),
144
+ content=text(message.get("content")),
145
+ tool_calls=message.get("tool_calls"),
146
+ )
147
+
148
+
149
+ def protect_reasoning_tags(text: str) -> str:
150
+ """Prevent model-emitted tags from breaking the UI's reasoning region."""
151
+ return text.replace("<think>", "&lt;think&gt;").replace("</think>", "&lt;/think&gt;")
152
+
153
+
154
+ def render_reply(
155
+ reasoning: str,
156
+ content: str,
157
+ *,
158
+ show_reasoning: bool,
159
+ pending: bool = False,
160
+ hit_token_limit: bool = False,
161
+ ) -> str:
162
+ """Render a parsed reply for Gradio's collapsible reasoning tags."""
163
+ sections: list[str] = []
164
+ if show_reasoning and reasoning.strip():
165
+ sections.append(f"<think>\n{protect_reasoning_tags(reasoning.strip())}\n</think>")
166
+ if content.strip():
167
+ sections.append(protect_reasoning_tags(content.strip()))
168
+ elif pending:
169
+ sections.append("_Generating…_")
170
+ elif hit_token_limit and reasoning.strip():
171
+ sections.append("_The response budget ended before a final-answer region was produced._")
172
+ elif reasoning.strip():
173
+ sections.append("_The model ended without a separate final-answer region._")
174
+ return "\n\n".join(sections).strip()
175
+
176
+
177
+ def friendly_error(error: BaseException) -> str:
178
+ """Map technical failures to messages that do not echo sensitive inputs or paths."""
179
+ name = type(error).__name__.lower()
180
+ message = str(error).lower()
181
+ if "outofmemory" in name or "out of memory" in message:
182
+ return "GPU memory was exhausted. Clear older turns, remove images, or lower the response budget."
183
+ if "timeout" in name or "timed out" in message:
184
+ return "The ZeroGPU allocation timed out. Lower the response budget and try again."
185
+ if isinstance(error, ValueError):
186
+ safe = str(error).strip()
187
+ if safe and len(safe) <= 320:
188
+ safe = safe.replace("/models/muse-glimmer-assistant", "<assistant_mount>")
189
+ safe = safe.replace("/models/muse-glimmer", "<full_mount>")
190
+ if safe.count("/") > 2:
191
+ safe = "A runtime setup error occurred while loading the selected checkpoint."
192
+ return safe
193
+ return "Inference failed. The private Space logs contain the technical error type."
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ gradio==6.22.0
2
+ torch==2.10.0
3
+ torchvision==0.25.0
4
+ transformers==5.15.0
5
+ safetensors==0.8.0
6
+ accelerate==1.11.0
7
+ Pillow==12.3.0
tests/test_app_contract.py ADDED
@@ -0,0 +1,383 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tempfile
3
+ from pathlib import Path
4
+ import unittest
5
+
6
+
7
+ os.environ.setdefault("KMP_DUPLICATE_LIB_OK", "TRUE")
8
+ os.environ["MUSE_SKIP_MODEL_LOAD"] = "1"
9
+
10
+ from PIL import Image
11
+
12
+ import app
13
+
14
+
15
+ class GradioContractTests(unittest.TestCase):
16
+ def test_click_and_enter_have_validated_serial_generators(self):
17
+ dependencies = app.demo.get_config_file()["dependencies"]
18
+ generators = [dependency for dependency in dependencies if dependency["types"]["generator"]]
19
+ self.assertGreaterEqual(len(generators), 2)
20
+
21
+ targets = set()
22
+ api_generator = None
23
+ for dependency in generators:
24
+ targets.update(target[1] for target in dependency["targets"])
25
+ self.assertTrue(dependency["queue"])
26
+ self.assertEqual(dependency.get("api_visibility", "public"), "private")
27
+ self.assertEqual(len(dependency["outputs"]), 6)
28
+ self.assertEqual(len(dependency["inputs"]), 16)
29
+ function = app.demo.fns[dependency["id"]]
30
+ self.assertEqual(function.concurrency_id, "muse-glimmer-xlarge")
31
+ self.assertEqual(function.concurrency_limit, 1)
32
+ self.assertIsNotNone(function.validator)
33
+ if dependency["api_name"] == "chat":
34
+ api_generator = dependency
35
+ if dependency["api_name"] == app.SUBMIT_API_NAME:
36
+ self.assertEqual(dependency["types"]["generator"], True)
37
+
38
+ self.assertEqual(targets, {"click", "submit"})
39
+ self.assertIsNotNone(api_generator, "Expected one private API-visible generation path named chat.")
40
+
41
+ generator_functions = [app.demo.fns[dependency["id"]] for dependency in generators]
42
+ self.assertEqual(
43
+ {function.concurrency_id for function in generator_functions},
44
+ {"muse-glimmer-xlarge"},
45
+ )
46
+ self.assertTrue(all(function.concurrency_limit == 1 for function in generator_functions))
47
+ self.assertTrue(all(function.validator is not None for function in generator_functions))
48
+
49
+ api_names = set()
50
+ for dependency in generators:
51
+ api_names.add(dependency["api_name"])
52
+ self.assertEqual(api_names, {"chat", app.SUBMIT_API_NAME})
53
+
54
+ def test_stop_and_clear_cancel_click_and_enter_generation(self):
55
+ dependencies = app.demo.get_config_file()["dependencies"]
56
+ generator_ids = {
57
+ dependency["id"] for dependency in dependencies if dependency["types"]["generator"]
58
+ }
59
+ cancellation_edges = [
60
+ set(dependency["cancels"])
61
+ for dependency in dependencies
62
+ if dependency["types"]["cancel"]
63
+ ]
64
+
65
+ self.assertEqual(len(cancellation_edges), 2)
66
+ self.assertTrue(all(edge == generator_ids for edge in cancellation_edges))
67
+
68
+ def test_invalid_requests_fail_the_queue_free_validator(self):
69
+ with tempfile.TemporaryDirectory() as tmpdir:
70
+ tmp_path = Path(tmpdir)
71
+ original_paths = {
72
+ model_id: spec["path"]
73
+ for model_id, spec in app.MODEL_REGISTRY.items()
74
+ }
75
+ for spec in app.MODEL_REGISTRY.values():
76
+ spec["path"] = tmp_path
77
+
78
+ try:
79
+ verdicts = app._validate_generation_request(
80
+ "",
81
+ None,
82
+ app.MODEL_DEFAULT_ID,
83
+ [],
84
+ [],
85
+ "",
86
+ "high",
87
+ False,
88
+ 512,
89
+ 1.0,
90
+ 0.95,
91
+ 64,
92
+ 1.0,
93
+ 42,
94
+ False,
95
+ True,
96
+ )
97
+ finally:
98
+ for model_id, original_path in original_paths.items():
99
+ app.MODEL_REGISTRY[model_id]["path"] = original_path
100
+
101
+ self.assertEqual(len(verdicts), 16)
102
+ self.assertFalse(verdicts[0]["is_valid"])
103
+ self.assertIn("prompt", verdicts[0]["message"].lower())
104
+
105
+ def test_model_id_payload_formats_from_ui_are_accepted(self):
106
+ with tempfile.TemporaryDirectory() as tmpdir:
107
+ tmp_path = Path(tmpdir)
108
+ original_paths = {
109
+ model_id: spec["path"]
110
+ for model_id, spec in app.MODEL_REGISTRY.items()
111
+ }
112
+ for spec in app.MODEL_REGISTRY.values():
113
+ spec["path"] = tmp_path
114
+
115
+ try:
116
+ verdict = app._validate_generation_request(
117
+ "hi",
118
+ None,
119
+ ("Muse Glimmer 30B-assistant", app.ASSISTANT_MODEL_ID),
120
+ [],
121
+ [],
122
+ "",
123
+ "high",
124
+ False,
125
+ 512,
126
+ 1.0,
127
+ 0.95,
128
+ 64,
129
+ 1.0,
130
+ 42,
131
+ False,
132
+ True,
133
+ )[0]
134
+ finally:
135
+ for model_id, original_path in original_paths.items():
136
+ app.MODEL_REGISTRY[model_id]["path"] = original_path
137
+
138
+ self.assertTrue(verdict["is_valid"])
139
+
140
+ def test_legacy_model_labels_are_accepted_by_coercion(self):
141
+ self.assertEqual(app._coerce_model_id("/Muse-Glimmer 30B"), app.MODEL_ID)
142
+ self.assertEqual(app._coerce_model_id("/Muse-Glimmer-30B"), app.MODEL_ID)
143
+ self.assertEqual(app._coerce_model_id("Muse Glimmer 30B (full BF16)"), app.MODEL_ID)
144
+ self.assertEqual(app._coerce_model_id("Muse Glimmer 30B-assistant (compact)"), app.ASSISTANT_MODEL_ID)
145
+ self.assertEqual(app._coerce_model_id(""), app.MODEL_ID)
146
+
147
+ def test_validator_defaults_when_controls_are_missing(self):
148
+ with tempfile.TemporaryDirectory() as tmpdir:
149
+ tmp_path = Path(tmpdir)
150
+ original_paths = {
151
+ model_id: spec["path"]
152
+ for model_id, spec in app.MODEL_REGISTRY.items()
153
+ }
154
+ for spec in app.MODEL_REGISTRY.values():
155
+ spec["path"] = tmp_path
156
+
157
+ try:
158
+ verdict = app._validate_generation_request(
159
+ "hi",
160
+ None,
161
+ app.MODEL_DEFAULT_ID,
162
+ [],
163
+ [],
164
+ "",
165
+ "high",
166
+ False,
167
+ None,
168
+ None,
169
+ None,
170
+ None,
171
+ None,
172
+ 42,
173
+ False,
174
+ True,
175
+ )[0]
176
+ finally:
177
+ for model_id, original_path in original_paths.items():
178
+ app.MODEL_REGISTRY[model_id]["path"] = original_path
179
+
180
+ self.assertTrue(verdict["is_valid"])
181
+
182
+ def test_numeric_model_id_payload_formats_from_ui_are_accepted(self):
183
+ with tempfile.TemporaryDirectory() as tmpdir:
184
+ tmp_path = Path(tmpdir)
185
+ original_paths = {
186
+ model_id: spec["path"]
187
+ for model_id, spec in app.MODEL_REGISTRY.items()
188
+ }
189
+ for spec in app.MODEL_REGISTRY.values():
190
+ spec["path"] = tmp_path
191
+
192
+ try:
193
+ verdict = app._validate_generation_request(
194
+ "hi",
195
+ None,
196
+ "1",
197
+ [],
198
+ [],
199
+ "",
200
+ "high",
201
+ False,
202
+ 512,
203
+ 1.0,
204
+ 0.95,
205
+ 64,
206
+ 1.0,
207
+ 42,
208
+ False,
209
+ True,
210
+ )[0]
211
+ finally:
212
+ for model_id, original_path in original_paths.items():
213
+ app.MODEL_REGISTRY[model_id]["path"] = original_path
214
+
215
+ self.assertTrue(verdict["is_valid"])
216
+
217
+
218
+ class StateContractTests(unittest.TestCase):
219
+ def test_only_two_recent_images_remain_in_model_history(self):
220
+ image = Image.new("RGB", (8, 8), "purple")
221
+ history = []
222
+ for index in range(3):
223
+ history.extend(
224
+ [
225
+ {
226
+ "role": "user",
227
+ "content": [
228
+ {"type": "image", "image": image.copy()},
229
+ {"type": "text", "text": f"turn {index}"},
230
+ ],
231
+ },
232
+ {"role": "assistant", "content": f"answer {index}"},
233
+ ]
234
+ )
235
+
236
+ cleaned = app._clean_model_history(history)
237
+ image_turns = [
238
+ message
239
+ for message in cleaned
240
+ if isinstance(message.get("content"), list)
241
+ and any(part.get("type") == "image" for part in message["content"])
242
+ ]
243
+ self.assertEqual(len(image_turns), 2)
244
+ self.assertEqual(cleaned[0]["content"], "turn 0")
245
+
246
+ def test_stop_rolls_back_visible_chat_snapshot(self):
247
+ snapshot = [{"role": "user", "content": "committed"}]
248
+ chat, status = app._stop_conversation(snapshot)
249
+
250
+ self.assertEqual(chat, snapshot)
251
+ self.assertIsNot(chat, snapshot)
252
+ self.assertIn("not added", status)
253
+
254
+
255
+ class DurationContractTests(unittest.TestCase):
256
+ def setUp(self):
257
+ app.ACTIVE_MODEL_ID = None
258
+
259
+ def tearDown(self):
260
+ app.ACTIVE_MODEL_ID = app.MODEL_ID
261
+
262
+ def test_full_model_cold_start_requests_cold_start_budget(self):
263
+ duration = app._gpu_duration(
264
+ "hi",
265
+ None,
266
+ app.MODEL_ID,
267
+ [],
268
+ [],
269
+ "",
270
+ "high",
271
+ False,
272
+ 512,
273
+ 1.0,
274
+ 0.95,
275
+ 64,
276
+ 1.0,
277
+ 42,
278
+ False,
279
+ True,
280
+ )
281
+ self.assertEqual(duration, 120)
282
+
283
+ def test_full_model_reuse_keeps_estimate_after_load(self):
284
+ app.ACTIVE_MODEL_ID = app.MODEL_ID
285
+ duration = app._gpu_duration(
286
+ "hi",
287
+ None,
288
+ app.MODEL_ID,
289
+ [],
290
+ [],
291
+ "",
292
+ "high",
293
+ False,
294
+ 512,
295
+ 1.0,
296
+ 0.95,
297
+ 64,
298
+ 1.0,
299
+ 42,
300
+ False,
301
+ True,
302
+ )
303
+ self.assertEqual(duration, 121)
304
+
305
+ def test_assistant_selection_reuses_loaded_full_model(self):
306
+ app.ACTIVE_MODEL_ID = app.MODEL_ID
307
+ duration = app._gpu_duration(
308
+ "hi",
309
+ None,
310
+ app.ASSISTANT_MODEL_ID,
311
+ [],
312
+ [],
313
+ "",
314
+ "high",
315
+ False,
316
+ 512,
317
+ 1.0,
318
+ 0.95,
319
+ 64,
320
+ 1.0,
321
+ 42,
322
+ False,
323
+ True,
324
+ )
325
+ self.assertEqual(duration, 121)
326
+
327
+
328
+ class RuntimeChatObjectTests(unittest.TestCase):
329
+ def test_coerce_chat_objects_prefers_nested_tokenizer(self):
330
+ class FakeResponseToken:
331
+ def __init__(self, is_processor=True):
332
+ self.is_processor = is_processor
333
+
334
+ def apply_chat_template(self, *_args, **_kwargs):
335
+ return {"input_ids": []}
336
+
337
+ def get_response_parser(self, *_args, **_kwargs):
338
+ return "parser"
339
+
340
+ class FakeProcessor:
341
+ def __init__(self):
342
+ self.tokenizer = FakeResponseToken()
343
+
344
+ original_paths = {model_id: spec["path"] for model_id, spec in app.MODEL_REGISTRY.items()}
345
+ with tempfile.TemporaryDirectory() as tmpdir:
346
+ tmp_path = Path(tmpdir)
347
+ for spec in app.MODEL_REGISTRY.values():
348
+ spec["path"] = tmp_path
349
+ try:
350
+ processor, tokenizer = app._coerce_chat_objects(FakeProcessor(), app.MODEL_ID)
351
+ finally:
352
+ for model_id, original_path in original_paths.items():
353
+ app.MODEL_REGISTRY[model_id]["path"] = original_path
354
+
355
+ self.assertIsInstance(processor, FakeResponseToken)
356
+ self.assertIs(processor, tokenizer)
357
+
358
+
359
+ class ModelPathResolutionTests(unittest.TestCase):
360
+ def test_mount_path_resolution_prefers_nested_checkpoint(self):
361
+ with tempfile.TemporaryDirectory() as tmpdir:
362
+ tmp = Path(tmpdir)
363
+ nested = tmp / "Muse-Glimmer-30B"
364
+ nested.mkdir()
365
+ (nested / "config.json").write_text("{}")
366
+ (nested / "chat_template.jinja").write_text("")
367
+ resolved = app._resolve_mount_path(tmp)
368
+
369
+ self.assertEqual(resolved.name, "Muse-Glimmer-30B")
370
+
371
+
372
+ def test_mount_path_resolution_keeps_direct_checkpoint(self):
373
+ with tempfile.TemporaryDirectory() as tmpdir:
374
+ tmp = Path(tmpdir)
375
+ (tmp / "config.json").write_text("{}")
376
+ (tmp / "chat_template.jinja").write_text("")
377
+ resolved = app._resolve_mount_path(tmp)
378
+
379
+ self.assertEqual(resolved, tmp)
380
+
381
+
382
+ if __name__ == "__main__":
383
+ unittest.main()
tests/test_muse_core.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unittest
2
+
3
+ from muse_core import (
4
+ META_SAMPLING,
5
+ NATIVE_GREEDY,
6
+ choose_seed,
7
+ coerce_parsed_reply,
8
+ estimate_gpu_duration,
9
+ friendly_error,
10
+ generation_kwargs,
11
+ preset_values,
12
+ render_reply,
13
+ validate_controls,
14
+ )
15
+
16
+
17
+ class PresetTests(unittest.TestCase):
18
+ def test_native_preset_is_greedy_with_documented_values_ready(self):
19
+ self.assertEqual(preset_values(NATIVE_GREEDY), (False, 1.0, 0.95, 64))
20
+
21
+ def test_meta_preset_enables_sampling(self):
22
+ self.assertEqual(preset_values(META_SAMPLING), (True, 1.0, 0.95, 64))
23
+
24
+ def test_greedy_kwargs_omit_inert_sampling_parameters(self):
25
+ kwargs = generation_kwargs(
26
+ do_sample=False,
27
+ max_new_tokens=512,
28
+ temperature=1.0,
29
+ top_p=0.95,
30
+ top_k=64,
31
+ repetition_penalty=1.0,
32
+ )
33
+ self.assertNotIn("temperature", kwargs)
34
+ self.assertNotIn("top_p", kwargs)
35
+ self.assertNotIn("top_k", kwargs)
36
+ self.assertEqual(kwargs["eos_token_id"], [200001, 200008])
37
+ self.assertEqual(kwargs["pad_token_id"], 200018)
38
+
39
+ def test_sampling_kwargs_match_model_card(self):
40
+ kwargs = generation_kwargs(
41
+ do_sample=True,
42
+ max_new_tokens=512,
43
+ temperature=1.0,
44
+ top_p=0.95,
45
+ top_k=64,
46
+ repetition_penalty=1.0,
47
+ )
48
+ self.assertEqual(kwargs["temperature"], 1.0)
49
+ self.assertEqual(kwargs["top_p"], 0.95)
50
+ self.assertEqual(kwargs["top_k"], 64)
51
+
52
+
53
+ class ValidationTests(unittest.TestCase):
54
+ def test_documented_controls_are_valid(self):
55
+ validate_controls(
56
+ max_new_tokens=512,
57
+ temperature=1.0,
58
+ top_p=0.95,
59
+ top_k=64,
60
+ repetition_penalty=1.0,
61
+ reasoning_strength="high",
62
+ )
63
+
64
+ def test_invalid_reasoning_is_rejected(self):
65
+ with self.assertRaises(ValueError):
66
+ validate_controls(
67
+ max_new_tokens=512,
68
+ temperature=1.0,
69
+ top_p=0.95,
70
+ top_k=64,
71
+ repetition_penalty=1.0,
72
+ reasoning_strength="extreme",
73
+ )
74
+
75
+ def test_seed_resolution(self):
76
+ self.assertEqual(choose_seed(42, False), 42)
77
+ self.assertEqual(choose_seed(42, True, randbelow=lambda _limit: 123), 123)
78
+
79
+ def test_duration_is_bounded_and_image_aware(self):
80
+ text_duration = estimate_gpu_duration(512, False)
81
+ image_duration = estimate_gpu_duration(512, True)
82
+ self.assertGreater(image_duration, text_duration)
83
+ self.assertGreaterEqual(estimate_gpu_duration(1, False), 60)
84
+ self.assertLessEqual(estimate_gpu_duration(100_000, True), 240)
85
+
86
+
87
+ class ResponseTests(unittest.TestCase):
88
+ def test_parser_fields_are_normalized(self):
89
+ reply = coerce_parsed_reply(
90
+ {"reasoning_content": " think ", "content": " answer ", "tool_calls": [{"x": 1}]}
91
+ )
92
+ self.assertEqual(reply.reasoning, "think")
93
+ self.assertEqual(reply.content, "answer")
94
+ self.assertEqual(reply.tool_calls, [{"x": 1}])
95
+
96
+ def test_reasoning_renders_in_collapsible_region(self):
97
+ rendered = render_reply("steps", "answer", show_reasoning=True)
98
+ self.assertIn("<think>\nsteps\n</think>", rendered)
99
+ self.assertTrue(rendered.endswith("answer"))
100
+
101
+ def test_reasoning_can_be_hidden_without_hiding_answer(self):
102
+ rendered = render_reply("secret chain", "answer", show_reasoning=False)
103
+ self.assertNotIn("secret chain", rendered)
104
+ self.assertEqual(rendered, "answer")
105
+
106
+ def test_model_tags_cannot_break_reasoning_wrapper(self):
107
+ rendered = render_reply("a </think> b", "ok", show_reasoning=True)
108
+ self.assertIn("&lt;/think&gt;", rendered)
109
+ self.assertEqual(rendered.count("</think>"), 1)
110
+
111
+ def test_errors_do_not_echo_arbitrary_details(self):
112
+ message = friendly_error(RuntimeError("secret prompt at /private/path"))
113
+ self.assertNotIn("secret", message)
114
+ self.assertNotIn("/private/path", message)
115
+
116
+
117
+ if __name__ == "__main__":
118
+ unittest.main()
119
+