lzanardos9 commited on
Commit
45132f0
·
verified ·
1 Parent(s): 8454772

Upload 16 files

Browse files
GravityLLM-Space-Demo/.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ .DS_Store
4
+ .env
5
+ outputs/
6
+ *.json.tmp
GravityLLM-Space-Demo/LICENSE ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
10
+
11
+ "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
12
+
13
+ "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
14
+
15
+ "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
16
+
17
+ "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
18
+
19
+ "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
20
+
21
+ "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work.
22
+
23
+ "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
24
+
25
+ "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems.
26
+
27
+ "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
28
+
29
+ 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
30
+
31
+ 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work.
32
+
33
+ 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
34
+ (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
35
+ (b) You must cause any modified files to carry prominent notices stating that You changed the files; and
36
+ (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work; and
37
+ (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file.
38
+
39
+ 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work shall be under the terms and conditions of this License.
40
+
41
+ 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor.
42
+
43
+ 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
44
+
45
+ 8. Limitation of Liability. In no event and under no legal theory shall any Contributor be liable to You for damages arising from the use of the Work.
46
+
47
+ 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer support or warranty protection only on Your own behalf.
GravityLLM-Space-Demo/README.md ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: GravityLLM Studio
3
+ emoji: 🌌
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: gradio
7
+ sdk_version: 6.8.0
8
+ python_version: "3.10"
9
+ app_file: app.py
10
+ fullWidth: true
11
+ header: default
12
+ suggested_hardware: cpu-basic
13
+ short_description: Spatial9 immersive scene generation with branded GravityLLM UI, schema validation, and spatial preview.
14
+ tags:
15
+ - gravityllm
16
+ - spatial-audio
17
+ - immersive-audio
18
+ - spatial9
19
+ - iamf
20
+ - gradio
21
+ - json
22
+ - demo
23
+ - music-tech
24
+ ---
25
+
26
+ ![GravityLLM banner](assets/gravityllm_space_banner.png)
27
+
28
+ # GravityLLM Studio
29
+
30
+ A branded Hugging Face Space for **constraint-conditioned immersive scene generation**.
31
+
32
+ This Space accepts a **music-constraint payload** and returns a **Spatial9Scene JSON** scene. It includes:
33
+
34
+ - a polished GravityLLM studio UI
35
+ - your Spatial9 logo in the hero section
36
+ - remote inference through Hugging Face `InferenceClient`
37
+ - optional JSON-schema grammar constraints
38
+ - built-in validation against `schemas/scene.schema.json`
39
+ - a live top-down spatial preview
40
+ - a deterministic fallback rules engine so the demo still works before the trained model is online
41
+
42
+ ## How to connect your model
43
+
44
+ Set the following Space secrets or variables:
45
+
46
+ - `GRAVITYLLM_MODEL_ID` → your model repo id, for example `your-org/GravityLLM-AutoPosition`
47
+ - `HF_TOKEN` → only required if the model is gated or private
48
+ - `GRAVITYLLM_BACKEND` → optional default: `hybrid`, `remote-model`, or `rules-engine demo`
49
+
50
+ ## Files
51
+
52
+ - `app.py` — the Gradio app
53
+ - `schemas/scene.schema.json` — the contract used for validation and optional grammar guidance
54
+ - `examples/` — ready-to-run sample payloads
55
+ - `assets/` — logo and banner assets
56
+ - `utils/scene_tools.py` — validation, heuristics, JSON extraction, plotting
57
+
58
+ ## Recommended workflow
59
+
60
+ 1. Upload your GravityLLM **Model repo**
61
+ 2. Train and push the final weights
62
+ 3. Upload this **Space repo**
63
+ 4. Set `GRAVITYLLM_MODEL_ID`
64
+ 5. Launch the Space
65
+
66
+ ## Notes
67
+
68
+ This Space is designed to be usable in two states:
69
+ - **before model launch** → rules-engine fallback
70
+ - **after model launch** → remote GravityLLM inference
GravityLLM-Space-Demo/__pycache__/app.cpython-311.pyc ADDED
Binary file (22.1 kB). View file
 
GravityLLM-Space-Demo/app.py ADDED
@@ -0,0 +1,437 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import json
5
+ import os
6
+ import tempfile
7
+ from pathlib import Path
8
+ from typing import Any, Dict
9
+
10
+ import gradio as gr
11
+ from huggingface_hub import InferenceClient
12
+ from huggingface_hub.errors import HfHubHTTPError, InferenceTimeoutError
13
+
14
+ from utils.scene_tools import (
15
+ SCHEMA,
16
+ extract_first_json_block,
17
+ heuristic_scene,
18
+ parse_json_text,
19
+ plot_scene,
20
+ scene_markdown,
21
+ scene_table,
22
+ validate_scene,
23
+ )
24
+
25
+ ROOT = Path(__file__).resolve().parent
26
+ ASSETS = ROOT / "assets"
27
+ EXAMPLES = ROOT / "examples"
28
+
29
+ APP_TITLE = "GravityLLM"
30
+ DEFAULT_MODEL_ID = os.getenv("GRAVITYLLM_MODEL_ID", "your-namespace/GravityLLM-AutoPosition")
31
+ DEFAULT_BACKEND = os.getenv("GRAVITYLLM_BACKEND", "hybrid")
32
+ HF_TOKEN = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACEHUB_API_TOKEN")
33
+
34
+ SYSTEM_PREFIX = (
35
+ "You are GravityLLM (Spatial9 AutoPosition SLM). "
36
+ "Generate ONLY valid JSON matching the Spatial9Scene schema. "
37
+ "No markdown. No explanation. No code fences.\n\n"
38
+ )
39
+
40
+ EXAMPLE_FILES = {
41
+ "Club Drop": EXAMPLES / "club_drop.json",
42
+ "Cinematic Break": EXAMPLES / "cinematic_break.json",
43
+ "Podcast Voice": EXAMPLES / "podcast_voice.json",
44
+ }
45
+
46
+
47
+ def logo_data_uri() -> str:
48
+ logo_bytes = (ASSETS / "spatial9_logo.png").read_bytes()
49
+ return "data:image/png;base64," + base64.b64encode(logo_bytes).decode("utf-8")
50
+
51
+
52
+ def load_example(name: str) -> str:
53
+ path = EXAMPLE_FILES.get(name, next(iter(EXAMPLE_FILES.values())))
54
+ return path.read_text(encoding="utf-8")
55
+
56
+
57
+ def build_prompt(payload: Dict[str, Any]) -> str:
58
+ return SYSTEM_PREFIX + "INPUT:\n" + json.dumps(payload, ensure_ascii=False, indent=2) + "\nOUTPUT:\n"
59
+
60
+
61
+ def remote_generate(
62
+ payload: Dict[str, Any],
63
+ model_id: str,
64
+ temperature: float,
65
+ top_p: float,
66
+ max_new_tokens: int,
67
+ use_grammar: bool,
68
+ ) -> tuple[Dict[str, Any], str]:
69
+ prompt = build_prompt(payload)
70
+ client = InferenceClient(model=model_id, token=HF_TOKEN)
71
+
72
+ call_kwargs = dict(
73
+ prompt=prompt,
74
+ model=model_id,
75
+ max_new_tokens=max_new_tokens,
76
+ temperature=temperature,
77
+ top_p=top_p,
78
+ repetition_penalty=1.05,
79
+ return_full_text=False,
80
+ )
81
+ if use_grammar:
82
+ call_kwargs["grammar"] = {"type": "json", "value": SCHEMA}
83
+
84
+ try:
85
+ response = client.text_generation(**call_kwargs)
86
+ scene = parse_json_text(response)
87
+ return scene, f"remote-model ({model_id})"
88
+ except Exception as first_error:
89
+ if use_grammar:
90
+ try:
91
+ call_kwargs.pop("grammar", None)
92
+ response = client.text_generation(**call_kwargs)
93
+ scene = parse_json_text(response)
94
+ return scene, f"remote-model ({model_id}, grammar-fallback)"
95
+ except Exception as second_error:
96
+ raise RuntimeError(f"{type(first_error).__name__}: {first_error}\n\nFallback: {type(second_error).__name__}: {second_error}") from second_error
97
+ raise
98
+
99
+
100
+ def write_download_file(scene: Dict[str, Any]) -> str:
101
+ fd, path = tempfile.mkstemp(prefix="gravityllm_scene_", suffix=".json")
102
+ os.close(fd)
103
+ Path(path).write_text(json.dumps(scene, ensure_ascii=False, indent=2), encoding="utf-8")
104
+ return path
105
+
106
+
107
+ def generate_scene(
108
+ payload_text: str,
109
+ model_id: str,
110
+ backend: str,
111
+ temperature: float,
112
+ top_p: float,
113
+ max_new_tokens: int,
114
+ use_grammar: bool,
115
+ ):
116
+ try:
117
+ payload = parse_json_text(payload_text)
118
+ except Exception as exc:
119
+ msg = f"### Invalid input JSON\n\n- {type(exc).__name__}: {exc}"
120
+ return "", msg, None, [], None, "Fix the input JSON and try again."
121
+
122
+ backend = backend or DEFAULT_BACKEND
123
+ model_id = model_id.strip() or DEFAULT_MODEL_ID
124
+
125
+ scene = None
126
+ backend_used = "rules-engine demo"
127
+ status = "Scene generated."
128
+
129
+ if backend in {"remote-model", "hybrid"}:
130
+ try:
131
+ scene, backend_used = remote_generate(payload, model_id, temperature, top_p, max_new_tokens, use_grammar)
132
+ status = f"Generated from remote model: {model_id}"
133
+ except (InferenceTimeoutError, HfHubHTTPError, RuntimeError, ValueError, json.JSONDecodeError) as exc:
134
+ if backend == "remote-model":
135
+ msg = f"### Remote generation failed\n\n- {type(exc).__name__}: {exc}"
136
+ return "", msg, None, [], None, "Remote inference failed."
137
+ scene = heuristic_scene(payload)
138
+ backend_used = "rules-engine demo (remote fallback)"
139
+ status = f"Remote generation failed; heuristic fallback used. Details: {type(exc).__name__}: {exc}"
140
+
141
+ if scene is None:
142
+ scene = heuristic_scene(payload)
143
+
144
+ valid, errors = validate_scene(scene)
145
+ download_path = write_download_file(scene)
146
+ figure = plot_scene(scene)
147
+ table = scene_table(scene)
148
+ summary = scene_markdown(scene, valid, errors, backend_used)
149
+ return json.dumps(scene, ensure_ascii=False, indent=2), summary, figure, table, download_path, status
150
+
151
+
152
+ def validate_only(scene_text: str):
153
+ try:
154
+ scene = parse_json_text(scene_text)
155
+ except Exception as exc:
156
+ return f"### Invalid scene JSON\n\n- {type(exc).__name__}: {exc}", None, []
157
+ valid, errors = validate_scene(scene)
158
+ summary = scene_markdown(scene, valid, errors, "manual validation")
159
+ return summary, plot_scene(scene), scene_table(scene)
160
+
161
+
162
+ def build_payload(target_format, style, section, bpm, energy, max_objects):
163
+ payload = {
164
+ "target_format": target_format,
165
+ "max_objects": int(max_objects),
166
+ "style": style,
167
+ "section": section,
168
+ "global": {"bpm": int(bpm), "energy": float(energy)},
169
+ "stems": [
170
+ {"id": "lead", "class": "lead_vocal", "lufs": -17.0, "transient": 0.25, "band_energy": {"low": 0.08, "mid": 0.67, "high": 0.25}, "leadness": 0.96},
171
+ {"id": "kick", "class": "kick", "lufs": -10.6, "transient": 0.96, "band_energy": {"low": 0.82, "mid": 0.12, "high": 0.06}, "leadness": 0.22},
172
+ {"id": "bass", "class": "bass", "lufs": -12.5, "transient": 0.58, "band_energy": {"low": 0.86, "mid": 0.10, "high": 0.04}, "leadness": 0.30},
173
+ {"id": "pad", "class": "pad", "lufs": -21.5, "transient": 0.05, "band_energy": {"low": 0.20, "mid": 0.50, "high": 0.30}, "leadness": 0.08},
174
+ {"id": "fx", "class": "fx", "lufs": -24.0, "transient": 0.22, "band_energy": {"low": 0.10, "mid": 0.24, "high": 0.66}, "leadness": 0.04},
175
+ ],
176
+ "rules": [
177
+ {"type": "anchor", "track_class": "lead_vocal", "az_deg": 0, "el_deg": 10, "dist_m": 1.6},
178
+ {"type": "mono_low_end", "hz_below": 120},
179
+ {"type": "width_pref", "track_class": "pad", "min_width": 0.75},
180
+ ],
181
+ }
182
+ return json.dumps(payload, ensure_ascii=False, indent=2)
183
+
184
+
185
+ hero_html = f"""
186
+ <div class="hero-wrap">
187
+ <div class="hero-left">
188
+ <div class="hero-logo-card">
189
+ <img class="hero-logo" src="{logo_data_uri()}" alt="Spatial9 logo"/>
190
+ </div>
191
+ </div>
192
+ <div class="hero-right">
193
+ <div class="eyebrow">SPATIAL9 • HUGGING FACE SPACE</div>
194
+ <h1>GravityLLM Studio</h1>
195
+ <p class="hero-copy">
196
+ Constraint-conditioned immersive scene generation with schema-guided JSON output,
197
+ remote Hugging Face inference, heuristic fallback, and a live spatial preview.
198
+ </p>
199
+ <div class="hero-chips">
200
+ <span>IAMF Ready</span>
201
+ <span>Schema Validated</span>
202
+ <span>Spatial Preview</span>
203
+ <span>Branded Demo</span>
204
+ </div>
205
+ </div>
206
+ </div>
207
+ """
208
+
209
+ css = """
210
+ :root {
211
+ --g-bg: #f6f9ff;
212
+ --g-panel: rgba(255,255,255,0.86);
213
+ --g-panel-strong: rgba(255,255,255,0.96);
214
+ --g-line: #dbe7f6;
215
+ --g-ink: #15233d;
216
+ --g-sub: #5f728f;
217
+ --g-accent: #1f6fe5;
218
+ --g-accent-2: #0f9bb9;
219
+ }
220
+ .gradio-container {
221
+ background:
222
+ radial-gradient(circle at top left, rgba(55,120,246,0.10), transparent 32%),
223
+ radial-gradient(circle at bottom right, rgba(15,155,185,0.10), transparent 28%),
224
+ var(--g-bg);
225
+ }
226
+ .hero-wrap {
227
+ display: grid;
228
+ grid-template-columns: 280px 1fr;
229
+ gap: 28px;
230
+ padding: 22px 8px 12px 8px;
231
+ align-items: center;
232
+ }
233
+ .hero-logo-card {
234
+ background: linear-gradient(180deg, rgba(255,255,255,0.96), rgba(247,250,255,0.90));
235
+ border: 1px solid var(--g-line);
236
+ box-shadow: 0 18px 42px rgba(31,58,114,0.08);
237
+ border-radius: 28px;
238
+ padding: 24px;
239
+ display: flex;
240
+ justify-content: center;
241
+ align-items: center;
242
+ min-height: 170px;
243
+ }
244
+ .hero-logo {
245
+ width: 100%;
246
+ max-width: 220px;
247
+ object-fit: contain;
248
+ }
249
+ .hero-right h1 {
250
+ font-size: 2.5rem;
251
+ margin: 0;
252
+ color: var(--g-ink);
253
+ }
254
+ .hero-copy {
255
+ color: var(--g-sub);
256
+ font-size: 1.06rem;
257
+ line-height: 1.6;
258
+ max-width: 780px;
259
+ }
260
+ .eyebrow {
261
+ color: var(--g-accent);
262
+ font-size: 0.92rem;
263
+ letter-spacing: 0.14em;
264
+ font-weight: 700;
265
+ margin-bottom: 8px;
266
+ }
267
+ .hero-chips {
268
+ display: flex;
269
+ flex-wrap: wrap;
270
+ gap: 10px;
271
+ margin-top: 14px;
272
+ }
273
+ .hero-chips span {
274
+ background: rgba(239,246,255,0.96);
275
+ border: 1px solid #cfe0fb;
276
+ color: #28558f;
277
+ border-radius: 999px;
278
+ padding: 8px 12px;
279
+ font-size: 0.9rem;
280
+ font-weight: 600;
281
+ }
282
+ .card-note {
283
+ color: var(--g-sub);
284
+ }
285
+ .block-panel {
286
+ background: var(--g-panel);
287
+ border: 1px solid var(--g-line);
288
+ border-radius: 22px;
289
+ padding: 10px;
290
+ }
291
+ footer {visibility: hidden;}
292
+ @media (max-width: 900px) {
293
+ .hero-wrap {grid-template-columns: 1fr;}
294
+ }
295
+ """
296
+
297
+ with gr.Blocks(
298
+ title=f"{APP_TITLE} Studio",
299
+ fill_width=True,
300
+ css=css,
301
+ theme=gr.themes.Soft(
302
+ primary_hue="blue",
303
+ secondary_hue="cyan",
304
+ neutral_hue="slate",
305
+ radius_size="lg",
306
+ ),
307
+ ) as demo:
308
+ gr.HTML(hero_html)
309
+
310
+ with gr.Tabs():
311
+ with gr.Tab("GravityLLM Studio"):
312
+ with gr.Row():
313
+ with gr.Column(scale=11):
314
+ example_name = gr.Dropdown(
315
+ choices=list(EXAMPLE_FILES.keys()),
316
+ value="Club Drop",
317
+ label="Example payload",
318
+ )
319
+ load_btn = gr.Button("Load Example", variant="secondary")
320
+ payload_box = gr.Code(
321
+ value=load_example("Club Drop"),
322
+ language="json",
323
+ label="Constraint + stem feature payload",
324
+ lines=26,
325
+ )
326
+
327
+ with gr.Column(scale=6):
328
+ model_id = gr.Textbox(
329
+ value=DEFAULT_MODEL_ID,
330
+ label="Model repo or endpoint",
331
+ info="Set your Hugging Face model repo id or inference endpoint URL.",
332
+ )
333
+ backend = gr.Dropdown(
334
+ choices=["hybrid", "remote-model", "rules-engine demo"],
335
+ value=DEFAULT_BACKEND if DEFAULT_BACKEND in {"hybrid", "remote-model", "rules-engine demo"} else "hybrid",
336
+ label="Backend",
337
+ )
338
+ temperature = gr.Slider(0.0, 1.2, value=0.2, step=0.05, label="Temperature")
339
+ top_p = gr.Slider(0.1, 1.0, value=0.9, step=0.05, label="Top-p")
340
+ max_new_tokens = gr.Slider(128, 1400, value=900, step=16, label="Max new tokens")
341
+ use_grammar = gr.Checkbox(
342
+ value=True,
343
+ label="Use JSON schema grammar when remote backend supports it",
344
+ )
345
+ run_btn = gr.Button("Generate Spatial Scene", variant="primary")
346
+ status = gr.Textbox(label="Status", interactive=False)
347
+
348
+ with gr.Row():
349
+ with gr.Column(scale=9):
350
+ output_box = gr.Code(language="json", label="Generated Spatial9Scene JSON", lines=26)
351
+ download = gr.File(label="Download scene JSON")
352
+ with gr.Column(scale=7):
353
+ summary = gr.Markdown("### Ready\n\nLoad an example or paste your own payload.")
354
+ plot = gr.Plot(label="Spatial scene preview")
355
+
356
+ object_table = gr.Dataframe(
357
+ headers=["id", "class", "az_deg", "el_deg", "dist_m", "width", "gain_db"],
358
+ datatype=["str", "str", "number", "number", "number", "number", "number"],
359
+ row_count=(0, "dynamic"),
360
+ col_count=(7, "fixed"),
361
+ label="Object inspector",
362
+ )
363
+
364
+ with gr.Tab("Prompt Builder"):
365
+ gr.Markdown("Build a starter payload, then send it to GravityLLM Studio.")
366
+ with gr.Row():
367
+ target_format = gr.Dropdown(["iamf", "binaural", "5.1.4", "7.1.4"], value="iamf", label="Target format")
368
+ style = gr.Dropdown(["club", "cinematic", "podcast", "live", "intimate"], value="club", label="Style")
369
+ section = gr.Dropdown(["intro", "verse", "break", "drop", "full"], value="drop", label="Section")
370
+ with gr.Row():
371
+ bpm = gr.Slider(0, 200, value=128, step=1, label="BPM")
372
+ energy = gr.Slider(0.0, 1.0, value=0.92, step=0.01, label="Energy")
373
+ max_objects_builder = gr.Slider(1, 32, value=10, step=1, label="Max objects")
374
+ build_btn = gr.Button("Build Payload", variant="primary")
375
+ builder_output = gr.Code(language="json", label="Starter payload", lines=24)
376
+ send_to_studio_btn = gr.Button("Send to Studio", variant="secondary")
377
+
378
+ with gr.Tab("Validate Existing Scene"):
379
+ scene_input = gr.Code(language="json", label="Paste a Spatial9Scene JSON", lines=24)
380
+ validate_btn = gr.Button("Validate Scene", variant="primary")
381
+ validate_summary = gr.Markdown()
382
+ validate_plot = gr.Plot()
383
+ validate_table = gr.Dataframe(
384
+ headers=["id", "class", "az_deg", "el_deg", "dist_m", "width", "gain_db"],
385
+ datatype=["str", "str", "number", "number", "number", "number", "number"],
386
+ row_count=(0, "dynamic"),
387
+ col_count=(7, "fixed"),
388
+ label="Validated object inspector",
389
+ )
390
+
391
+ with gr.Tab("About"):
392
+ gr.Image(value=str(ASSETS / "gravityllm_space_banner.png"), label="GravityLLM banner", show_download_button=False, show_fullscreen_button=False)
393
+ gr.Markdown(
394
+ """
395
+ ### What this Space does
396
+
397
+ - Turns **constraints + stem descriptors** into **Spatial9Scene JSON**
398
+ - Can call a remote Hugging Face model repo through `InferenceClient`
399
+ - Falls back to a deterministic **rules engine** so the demo stays usable
400
+ - Validates outputs against the included JSON schema
401
+ - Renders a spatial top-down preview of object positions
402
+
403
+ ### Environment variables
404
+
405
+ - `GRAVITYLLM_MODEL_ID` — model repo id or endpoint URL
406
+ - `HF_TOKEN` — required if the model is gated or private
407
+ - `GRAVITYLLM_BACKEND` — optional default: `hybrid`, `remote-model`, or `rules-engine demo`
408
+
409
+ ### Recommended setup
410
+
411
+ 1. Upload your GravityLLM model repo.
412
+ 2. Train and push weights.
413
+ 3. Upload this Space repo.
414
+ 4. Set `GRAVITYLLM_MODEL_ID` in the Space settings.
415
+ """
416
+ )
417
+
418
+ load_btn.click(fn=load_example, inputs=example_name, outputs=payload_box)
419
+ build_btn.click(
420
+ fn=build_payload,
421
+ inputs=[target_format, style, section, bpm, energy, max_objects_builder],
422
+ outputs=builder_output,
423
+ )
424
+ send_to_studio_btn.click(fn=lambda x: x, inputs=builder_output, outputs=payload_box)
425
+ run_btn.click(
426
+ fn=generate_scene,
427
+ inputs=[payload_box, model_id, backend, temperature, top_p, max_new_tokens, use_grammar],
428
+ outputs=[output_box, summary, plot, object_table, download, status],
429
+ )
430
+ validate_btn.click(
431
+ fn=validate_only,
432
+ inputs=scene_input,
433
+ outputs=[validate_summary, validate_plot, validate_table],
434
+ )
435
+
436
+ if __name__ == "__main__":
437
+ demo.launch()
GravityLLM-Space-Demo/assets/gravityllm_space_banner.png ADDED
GravityLLM-Space-Demo/assets/spatial9_logo.png ADDED
GravityLLM-Space-Demo/examples/cinematic_break.json ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "target_format": "iamf",
3
+ "max_objects": 8,
4
+ "style": "cinematic",
5
+ "section": "break",
6
+ "global": {
7
+ "bpm": 90,
8
+ "energy": 0.45
9
+ },
10
+ "stems": [
11
+ {
12
+ "id": "d1",
13
+ "class": "dialogue",
14
+ "lufs": -18.0,
15
+ "transient": 0.35,
16
+ "band_energy": {
17
+ "low": 0.05,
18
+ "mid": 0.75,
19
+ "high": 0.2
20
+ },
21
+ "leadness": 0.98
22
+ },
23
+ {
24
+ "id": "p1",
25
+ "class": "pad",
26
+ "lufs": -24.0,
27
+ "transient": 0.05,
28
+ "band_energy": {
29
+ "low": 0.25,
30
+ "mid": 0.55,
31
+ "high": 0.2
32
+ },
33
+ "leadness": 0.1
34
+ },
35
+ {
36
+ "id": "fx1",
37
+ "class": "fx",
38
+ "lufs": -26.0,
39
+ "transient": 0.15,
40
+ "band_energy": {
41
+ "low": 0.1,
42
+ "mid": 0.25,
43
+ "high": 0.65
44
+ },
45
+ "leadness": 0.05
46
+ },
47
+ {
48
+ "id": "a1",
49
+ "class": "ambience",
50
+ "lufs": -28.0,
51
+ "transient": 0.03,
52
+ "band_energy": {
53
+ "low": 0.15,
54
+ "mid": 0.55,
55
+ "high": 0.3
56
+ },
57
+ "leadness": 0.02
58
+ }
59
+ ],
60
+ "rules": [
61
+ {
62
+ "type": "anchor",
63
+ "track_class": "dialogue",
64
+ "az_deg": 0,
65
+ "el_deg": 5,
66
+ "dist_m": 1.8
67
+ },
68
+ {
69
+ "type": "keep_dialogue_clear",
70
+ "band_hz": [
71
+ 1000,
72
+ 4000
73
+ ]
74
+ },
75
+ {
76
+ "type": "width_pref",
77
+ "track_class": "pad",
78
+ "min_width": 0.8
79
+ }
80
+ ]
81
+ }
GravityLLM-Space-Demo/examples/club_drop.json ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "target_format": "iamf",
3
+ "max_objects": 10,
4
+ "style": "club",
5
+ "section": "drop",
6
+ "global": {
7
+ "bpm": 128,
8
+ "energy": 0.92
9
+ },
10
+ "stems": [
11
+ {
12
+ "id": "v1",
13
+ "class": "lead_vocal",
14
+ "lufs": -16.8,
15
+ "transient": 0.25,
16
+ "band_energy": {
17
+ "low": 0.1,
18
+ "mid": 0.6,
19
+ "high": 0.3
20
+ },
21
+ "leadness": 0.95
22
+ },
23
+ {
24
+ "id": "k1",
25
+ "class": "kick",
26
+ "lufs": -10.5,
27
+ "transient": 0.95,
28
+ "band_energy": {
29
+ "low": 0.8,
30
+ "mid": 0.15,
31
+ "high": 0.05
32
+ },
33
+ "leadness": 0.25
34
+ },
35
+ {
36
+ "id": "b1",
37
+ "class": "bass",
38
+ "lufs": -12.2,
39
+ "transient": 0.55,
40
+ "band_energy": {
41
+ "low": 0.85,
42
+ "mid": 0.12,
43
+ "high": 0.03
44
+ },
45
+ "leadness": 0.35
46
+ },
47
+ {
48
+ "id": "p1",
49
+ "class": "pad",
50
+ "lufs": -20.3,
51
+ "transient": 0.1,
52
+ "band_energy": {
53
+ "low": 0.2,
54
+ "mid": 0.5,
55
+ "high": 0.3
56
+ },
57
+ "leadness": 0.1
58
+ },
59
+ {
60
+ "id": "s1",
61
+ "class": "synth_lead",
62
+ "lufs": -18.0,
63
+ "transient": 0.4,
64
+ "band_energy": {
65
+ "low": 0.1,
66
+ "mid": 0.55,
67
+ "high": 0.35
68
+ },
69
+ "leadness": 0.75
70
+ },
71
+ {
72
+ "id": "fx1",
73
+ "class": "fx",
74
+ "lufs": -23.0,
75
+ "transient": 0.2,
76
+ "band_energy": {
77
+ "low": 0.1,
78
+ "mid": 0.3,
79
+ "high": 0.6
80
+ },
81
+ "leadness": 0.05
82
+ }
83
+ ],
84
+ "rules": [
85
+ {
86
+ "type": "anchor",
87
+ "track_class": "lead_vocal",
88
+ "az_deg": 0,
89
+ "el_deg": 10,
90
+ "dist_m": 1.6
91
+ },
92
+ {
93
+ "type": "mono_low_end",
94
+ "hz_below": 120
95
+ },
96
+ {
97
+ "type": "width_pref",
98
+ "track_class": "pad",
99
+ "min_width": 0.75
100
+ },
101
+ {
102
+ "type": "avoid_band_masking",
103
+ "mask_target": "lead_vocal",
104
+ "band_hz": [
105
+ 1500,
106
+ 4500
107
+ ]
108
+ }
109
+ ]
110
+ }
GravityLLM-Space-Demo/examples/podcast_voice.json ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "target_format": "binaural",
3
+ "max_objects": 5,
4
+ "style": "podcast",
5
+ "section": "full",
6
+ "global": {
7
+ "bpm": 0,
8
+ "energy": 0.28
9
+ },
10
+ "stems": [
11
+ {
12
+ "id": "host",
13
+ "class": "dialogue",
14
+ "lufs": -18.5,
15
+ "transient": 0.22,
16
+ "band_energy": {
17
+ "low": 0.12,
18
+ "mid": 0.7,
19
+ "high": 0.18
20
+ },
21
+ "leadness": 0.97
22
+ },
23
+ {
24
+ "id": "guest",
25
+ "class": "back_vocal",
26
+ "lufs": -19.8,
27
+ "transient": 0.18,
28
+ "band_energy": {
29
+ "low": 0.1,
30
+ "mid": 0.68,
31
+ "high": 0.22
32
+ },
33
+ "leadness": 0.82
34
+ },
35
+ {
36
+ "id": "bed",
37
+ "class": "pad",
38
+ "lufs": -29.0,
39
+ "transient": 0.02,
40
+ "band_energy": {
41
+ "low": 0.18,
42
+ "mid": 0.52,
43
+ "high": 0.3
44
+ },
45
+ "leadness": 0.04
46
+ },
47
+ {
48
+ "id": "stinger",
49
+ "class": "fx",
50
+ "lufs": -25.5,
51
+ "transient": 0.3,
52
+ "band_energy": {
53
+ "low": 0.05,
54
+ "mid": 0.25,
55
+ "high": 0.7
56
+ },
57
+ "leadness": 0.08
58
+ }
59
+ ],
60
+ "rules": [
61
+ {
62
+ "type": "anchor",
63
+ "track_class": "dialogue",
64
+ "az_deg": 0,
65
+ "el_deg": 4,
66
+ "dist_m": 1.4
67
+ },
68
+ {
69
+ "type": "width_pref",
70
+ "track_class": "pad",
71
+ "min_width": 0.7
72
+ },
73
+ {
74
+ "type": "keep_dialogue_clear",
75
+ "band_hz": [
76
+ 1200,
77
+ 3800
78
+ ]
79
+ }
80
+ ]
81
+ }
GravityLLM-Space-Demo/requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ gradio>=6.8.0,<7
2
+ huggingface_hub>=1.5.0
3
+ jsonschema>=4.23.0
4
+ matplotlib>=3.8.0
5
+ Pillow>=10.0.0
GravityLLM-Space-Demo/schemas/scene.schema.json ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "Spatial9Scene",
4
+ "type": "object",
5
+ "required": [
6
+ "version",
7
+ "bed",
8
+ "objects",
9
+ "constraints_applied"
10
+ ],
11
+ "properties": {
12
+ "version": {
13
+ "type": "string"
14
+ },
15
+ "bed": {
16
+ "type": "object",
17
+ "required": [
18
+ "layout",
19
+ "loudness_target_lufs",
20
+ "room_preset"
21
+ ],
22
+ "properties": {
23
+ "layout": {
24
+ "type": "string",
25
+ "enum": [
26
+ "binaural",
27
+ "5.1.4",
28
+ "7.1.4",
29
+ "iamf"
30
+ ]
31
+ },
32
+ "loudness_target_lufs": {
33
+ "type": "number"
34
+ },
35
+ "room_preset": {
36
+ "type": "string"
37
+ }
38
+ },
39
+ "additionalProperties": false
40
+ },
41
+ "objects": {
42
+ "type": "array",
43
+ "minItems": 1,
44
+ "maxItems": 32,
45
+ "items": {
46
+ "type": "object",
47
+ "required": [
48
+ "id",
49
+ "class",
50
+ "az_deg",
51
+ "el_deg",
52
+ "dist_m",
53
+ "width",
54
+ "gain_db",
55
+ "reverb_send",
56
+ "early_reflections",
57
+ "motion"
58
+ ],
59
+ "properties": {
60
+ "id": {
61
+ "type": "string"
62
+ },
63
+ "class": {
64
+ "type": "string",
65
+ "enum": [
66
+ "lead_vocal",
67
+ "back_vocal",
68
+ "kick",
69
+ "snare",
70
+ "hihat",
71
+ "bass",
72
+ "drums_bus",
73
+ "pad",
74
+ "synth_lead",
75
+ "guitar",
76
+ "piano",
77
+ "strings",
78
+ "brass",
79
+ "fx",
80
+ "dialogue",
81
+ "ambience",
82
+ "other"
83
+ ]
84
+ },
85
+ "az_deg": {
86
+ "type": "number",
87
+ "minimum": -180,
88
+ "maximum": 180
89
+ },
90
+ "el_deg": {
91
+ "type": "number",
92
+ "minimum": -45,
93
+ "maximum": 90
94
+ },
95
+ "dist_m": {
96
+ "type": "number",
97
+ "minimum": 0.5,
98
+ "maximum": 15.0
99
+ },
100
+ "width": {
101
+ "type": "number",
102
+ "minimum": 0.0,
103
+ "maximum": 1.0
104
+ },
105
+ "gain_db": {
106
+ "type": "number",
107
+ "minimum": -60,
108
+ "maximum": 12
109
+ },
110
+ "reverb_send": {
111
+ "type": "number",
112
+ "minimum": 0.0,
113
+ "maximum": 1.0
114
+ },
115
+ "early_reflections": {
116
+ "type": "number",
117
+ "minimum": 0.0,
118
+ "maximum": 1.0
119
+ },
120
+ "motion": {
121
+ "type": "array",
122
+ "maxItems": 64,
123
+ "items": {
124
+ "type": "object",
125
+ "required": [
126
+ "t",
127
+ "az_deg",
128
+ "el_deg",
129
+ "dist_m"
130
+ ],
131
+ "properties": {
132
+ "t": {
133
+ "type": "number",
134
+ "minimum": 0.0,
135
+ "maximum": 1.0
136
+ },
137
+ "az_deg": {
138
+ "type": "number",
139
+ "minimum": -180,
140
+ "maximum": 180
141
+ },
142
+ "el_deg": {
143
+ "type": "number",
144
+ "minimum": -45,
145
+ "maximum": 90
146
+ },
147
+ "dist_m": {
148
+ "type": "number",
149
+ "minimum": 0.5,
150
+ "maximum": 15.0
151
+ }
152
+ },
153
+ "additionalProperties": false
154
+ }
155
+ }
156
+ },
157
+ "additionalProperties": false
158
+ }
159
+ },
160
+ "constraints_applied": {
161
+ "type": "array",
162
+ "items": {
163
+ "type": "string"
164
+ },
165
+ "maxItems": 64
166
+ }
167
+ },
168
+ "additionalProperties": false
169
+ }
GravityLLM-Space-Demo/utils/__init__.py ADDED
File without changes
GravityLLM-Space-Demo/utils/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (188 Bytes). View file
 
GravityLLM-Space-Demo/utils/__pycache__/scene_tools.cpython-311.pyc ADDED
Binary file (20.7 kB). View file
 
GravityLLM-Space-Demo/utils/scene_tools.py ADDED
@@ -0,0 +1,307 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import math
5
+ import re
6
+ from pathlib import Path
7
+ from typing import Any, Dict, List, Tuple
8
+
9
+ import matplotlib.pyplot as plt
10
+ from jsonschema import Draft7Validator
11
+
12
+
13
+ SCHEMA_PATH = Path(__file__).resolve().parents[1] / "schemas" / "scene.schema.json"
14
+ SCHEMA: Dict[str, Any] = json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
15
+ VALIDATOR = Draft7Validator(SCHEMA)
16
+
17
+ CLASS_DEFAULTS = {
18
+ "lead_vocal": {"az": 0, "el": 10, "dist": 1.6, "width": 0.15, "gain": 0.0, "rev": 0.18, "er": 0.22},
19
+ "dialogue": {"az": 0, "el": 5, "dist": 1.5, "width": 0.10, "gain": 0.0, "rev": 0.08, "er": 0.18},
20
+ "back_vocal": {"az": -18, "el": 8, "dist": 1.9, "width": 0.25, "gain": -1.2, "rev": 0.16, "er": 0.16},
21
+ "kick": {"az": 0, "el": 0, "dist": 2.2, "width": 0.0, "gain": 0.0, "rev": 0.02, "er": 0.05},
22
+ "snare": {"az": 8, "el": 4, "dist": 2.4, "width": 0.1, "gain": -0.3, "rev": 0.05, "er": 0.07},
23
+ "hihat": {"az": 22, "el": 7, "dist": 2.7, "width": 0.18, "gain": -1.0, "rev": 0.06, "er": 0.08},
24
+ "bass": {"az": 0, "el": -5, "dist": 2.6, "width": 0.05, "gain": -0.5, "rev": 0.03, "er": 0.06},
25
+ "drums_bus": {"az": 0, "el": 2, "dist": 2.8, "width": 0.25, "gain": -0.6, "rev": 0.08, "er": 0.10},
26
+ "pad": {"az": -70, "el": 18, "dist": 4.8, "width": 0.82, "gain": -4.0, "rev": 0.28, "er": 0.12},
27
+ "synth_lead": {"az": 24, "el": 15, "dist": 2.0, "width": 0.35, "gain": -1.0, "rev": 0.12, "er": 0.10},
28
+ "guitar": {"az": -28, "el": 10, "dist": 2.8, "width": 0.28, "gain": -1.5, "rev": 0.09, "er": 0.11},
29
+ "piano": {"az": -14, "el": 8, "dist": 2.6, "width": 0.34, "gain": -1.5, "rev": 0.09, "er": 0.10},
30
+ "fx": {"az": 105, "el": 28, "dist": 6.2, "width": 0.66, "gain": -7.0, "rev": 0.42, "er": 0.08},
31
+ "ambience": {"az": -120, "el": 15, "dist": 8.0, "width": 0.90, "gain": -8.0, "rev": 0.55, "er": 0.10},
32
+ "other": {"az": 0, "el": 8, "dist": 3.0, "width": 0.25, "gain": -2.0, "rev": 0.10, "er": 0.10},
33
+ }
34
+
35
+ SPATIAL_CLASSES = list(CLASS_DEFAULTS)
36
+
37
+
38
+ def clip(value: float, lo: float, hi: float) -> float:
39
+ return max(lo, min(hi, value))
40
+
41
+
42
+ def extract_first_json_block(text: str) -> str:
43
+ match = re.search(r"\{.*\}", text, flags=re.DOTALL)
44
+ return match.group(0).strip() if match else text.strip()
45
+
46
+
47
+ def parse_json_text(text: str) -> Dict[str, Any]:
48
+ return json.loads(extract_first_json_block(text))
49
+
50
+
51
+ def validate_scene(scene: Dict[str, Any]) -> Tuple[bool, List[str]]:
52
+ errors = sorted(VALIDATOR.iter_errors(scene), key=lambda e: list(e.path))
53
+ if not errors:
54
+ return True, []
55
+ messages = []
56
+ for err in errors[:50]:
57
+ path = ".".join(str(x) for x in err.path)
58
+ messages.append(f"{path or '<root>'}: {err.message}")
59
+ return False, messages
60
+
61
+
62
+ def make_motion(az: float, el: float, dist: float, sweep: float = 0.0) -> List[Dict[str, float]]:
63
+ if abs(sweep) < 0.001:
64
+ return [
65
+ {"t": 0.0, "az_deg": round(az, 2), "el_deg": round(el, 2), "dist_m": round(dist, 2)},
66
+ {"t": 1.0, "az_deg": round(az, 2), "el_deg": round(el, 2), "dist_m": round(dist, 2)},
67
+ ]
68
+ return [
69
+ {"t": 0.0, "az_deg": round(az - sweep / 2, 2), "el_deg": round(el, 2), "dist_m": round(dist, 2)},
70
+ {"t": 1.0, "az_deg": round(az + sweep / 2, 2), "el_deg": round(el, 2), "dist_m": round(dist, 2)},
71
+ ]
72
+
73
+
74
+ def find_anchor_rules(payload: Dict[str, Any]) -> Dict[str, Dict[str, float]]:
75
+ anchors: Dict[str, Dict[str, float]] = {}
76
+ for rule in payload.get("rules", []):
77
+ if rule.get("type") == "anchor":
78
+ key = rule.get("track_class")
79
+ if key:
80
+ anchors[key] = {
81
+ "az": float(rule.get("az_deg", 0.0)),
82
+ "el": float(rule.get("el_deg", 0.0)),
83
+ "dist": float(rule.get("dist_m", 1.6)),
84
+ }
85
+ return anchors
86
+
87
+
88
+ def width_preference(payload: Dict[str, Any], track_class: str) -> float | None:
89
+ for rule in payload.get("rules", []):
90
+ if rule.get("type") == "width_pref" and rule.get("track_class") == track_class:
91
+ value = rule.get("min_width")
92
+ if value is not None:
93
+ return float(value)
94
+ return None
95
+
96
+
97
+ def target_layout(payload: Dict[str, Any]) -> str:
98
+ fmt = str(payload.get("target_format", "iamf")).lower()
99
+ if fmt in {"binaural", "iamf", "5.1.4", "7.1.4"}:
100
+ return fmt
101
+ return "iamf"
102
+
103
+
104
+ def room_preset(payload: Dict[str, Any]) -> str:
105
+ style = str(payload.get("style", "neutral")).lower()
106
+ mapping = {
107
+ "club": "club_medium",
108
+ "cinematic": "cinema_large",
109
+ "film": "cinema_large",
110
+ "podcast": "studio_dry",
111
+ "live": "stage_wide",
112
+ "intimate": "studio_small",
113
+ }
114
+ return mapping.get(style, "studio_neutral")
115
+
116
+
117
+ def heuristic_scene(payload: Dict[str, Any]) -> Dict[str, Any]:
118
+ anchors = find_anchor_rules(payload)
119
+ objects: List[Dict[str, Any]] = []
120
+ constraints_applied: List[str] = []
121
+ max_objects = int(payload.get("max_objects", 10))
122
+ style = str(payload.get("style", "neutral")).lower()
123
+
124
+ for rule in payload.get("rules", []):
125
+ rtype = rule.get("type")
126
+ if rtype == "anchor":
127
+ constraints_applied.append(
128
+ f"anchor:{rule.get('track_class')}@{rule.get('az_deg')}/{rule.get('el_deg')}/{rule.get('dist_m')}"
129
+ )
130
+ elif rtype == "mono_low_end":
131
+ constraints_applied.append(f"mono_low_end<{rule.get('hz_below', 120)}Hz")
132
+ elif rtype == "width_pref":
133
+ constraints_applied.append(f"{rule.get('track_class')}_width>={rule.get('min_width')}")
134
+ elif rtype == "keep_dialogue_clear":
135
+ band = rule.get("band_hz", [1000, 4000])
136
+ constraints_applied.append(f"keep_dialogue_clear_{band[0]}-{band[1]}Hz")
137
+ elif rtype == "avoid_band_masking":
138
+ band = rule.get("band_hz", [1500, 4500])
139
+ constraints_applied.append(f"avoid_masking_{rule.get('mask_target')}_{band[0]}-{band[1]}Hz")
140
+
141
+ for idx, stem in enumerate(payload.get("stems", [])[:max_objects]):
142
+ cls = stem.get("class", "other")
143
+ defaults = dict(CLASS_DEFAULTS.get(cls, CLASS_DEFAULTS["other"]))
144
+ if cls in anchors:
145
+ defaults["az"] = anchors[cls]["az"]
146
+ defaults["el"] = anchors[cls]["el"]
147
+ defaults["dist"] = anchors[cls]["dist"]
148
+
149
+ width_min = width_preference(payload, cls)
150
+ if width_min is not None:
151
+ defaults["width"] = max(defaults["width"], width_min)
152
+
153
+ leadness = float(stem.get("leadness", 0.0))
154
+ transient = float(stem.get("transient", 0.0))
155
+ lufs = float(stem.get("lufs", -20.0))
156
+
157
+ if leadness > 0.8 and cls not in {"kick", "bass", "dialogue", "lead_vocal"}:
158
+ defaults["dist"] = clip(defaults["dist"] - 0.35, 0.8, 15.0)
159
+ defaults["gain"] = clip(defaults["gain"] + 0.8, -60.0, 12.0)
160
+
161
+ if transient > 0.7 and cls in {"fx", "synth_lead"}:
162
+ defaults["az"] += 12.0
163
+
164
+ if lufs < -24 and cls in {"fx", "ambience", "pad"}:
165
+ defaults["dist"] = clip(defaults["dist"] + 0.5, 0.5, 15.0)
166
+
167
+ # Add style-specific touch
168
+ sweep = 0.0
169
+ if style == "club" and cls in {"fx", "synth_lead"}:
170
+ sweep = 28.0 if cls == "fx" else 10.0
171
+ elif style in {"cinematic", "film"} and cls in {"fx", "ambience"}:
172
+ sweep = 18.0
173
+ elif style == "podcast" and cls in {"dialogue", "back_vocal"}:
174
+ sweep = 0.0
175
+
176
+ obj = {
177
+ "id": str(stem.get("id", f"obj_{idx+1}")),
178
+ "class": cls if cls in SPATIAL_CLASSES else "other",
179
+ "az_deg": round(clip(defaults["az"], -180, 180), 2),
180
+ "el_deg": round(clip(defaults["el"], -45, 90), 2),
181
+ "dist_m": round(clip(defaults["dist"], 0.5, 15.0), 2),
182
+ "width": round(clip(defaults["width"], 0.0, 1.0), 2),
183
+ "gain_db": round(clip(defaults["gain"], -60, 12), 2),
184
+ "reverb_send": round(clip(defaults["rev"], 0.0, 1.0), 2),
185
+ "early_reflections": round(clip(defaults["er"], 0.0, 1.0), 2),
186
+ "motion": make_motion(defaults["az"], defaults["el"], defaults["dist"], sweep=sweep),
187
+ }
188
+
189
+ # Low-end mono safety
190
+ if cls in {"kick", "bass"}:
191
+ obj["az_deg"] = 0.0
192
+ obj["width"] = 0.0 if cls == "kick" else min(obj["width"], 0.08)
193
+ obj["motion"] = make_motion(0.0, obj["el_deg"], obj["dist_m"], sweep=0.0)
194
+
195
+ objects.append(obj)
196
+
197
+ scene = {
198
+ "version": "1.0",
199
+ "bed": {
200
+ "layout": target_layout(payload),
201
+ "loudness_target_lufs": -16.0 if payload.get("style") == "cinematic" else -14.0,
202
+ "room_preset": room_preset(payload),
203
+ },
204
+ "objects": objects or [
205
+ {
206
+ "id": "placeholder",
207
+ "class": "other",
208
+ "az_deg": 0.0,
209
+ "el_deg": 0.0,
210
+ "dist_m": 2.0,
211
+ "width": 0.2,
212
+ "gain_db": 0.0,
213
+ "reverb_send": 0.1,
214
+ "early_reflections": 0.1,
215
+ "motion": make_motion(0.0, 0.0, 2.0),
216
+ }
217
+ ],
218
+ "constraints_applied": constraints_applied,
219
+ }
220
+ return scene
221
+
222
+
223
+ def scene_stats(scene: Dict[str, Any]) -> Dict[str, Any]:
224
+ objects = scene.get("objects", [])
225
+ dominant = sorted(objects, key=lambda o: float(o.get("gain_db", -99.0)), reverse=True)[:3]
226
+ return {
227
+ "layout": scene.get("bed", {}).get("layout", "unknown"),
228
+ "room_preset": scene.get("bed", {}).get("room_preset", "unknown"),
229
+ "object_count": len(objects),
230
+ "dominant": ", ".join(f"{o.get('id')} ({o.get('class')})" for o in dominant) or "n/a",
231
+ }
232
+
233
+
234
+ def scene_table(scene: Dict[str, Any]) -> List[List[Any]]:
235
+ rows = []
236
+ for obj in scene.get("objects", []):
237
+ rows.append([
238
+ obj.get("id"),
239
+ obj.get("class"),
240
+ obj.get("az_deg"),
241
+ obj.get("el_deg"),
242
+ obj.get("dist_m"),
243
+ obj.get("width"),
244
+ obj.get("gain_db"),
245
+ ])
246
+ return rows
247
+
248
+
249
+ def scene_markdown(scene: Dict[str, Any], valid: bool, errors: List[str], backend_used: str) -> str:
250
+ stats = scene_stats(scene)
251
+ badge = "✅ Schema valid" if valid else "⚠️ Validation issues"
252
+ lines = [
253
+ f"### {badge}",
254
+ "",
255
+ f"- **Backend:** {backend_used}",
256
+ f"- **Layout:** `{stats['layout']}`",
257
+ f"- **Room preset:** `{stats['room_preset']}`",
258
+ f"- **Objects:** `{stats['object_count']}`",
259
+ f"- **Top objects:** {stats['dominant']}",
260
+ ]
261
+ if errors:
262
+ lines.append("")
263
+ lines.append("**Validation messages**")
264
+ for err in errors[:8]:
265
+ lines.append(f"- {err}")
266
+ return "\n".join(lines)
267
+
268
+
269
+ def plot_scene(scene: Dict[str, Any]):
270
+ fig = plt.figure(figsize=(7.0, 7.0))
271
+ ax = fig.add_subplot(111)
272
+ ax.set_facecolor("#f8fbff")
273
+ fig.patch.set_facecolor("#f8fbff")
274
+
275
+ # rings
276
+ for r in [2, 4, 6, 8, 10]:
277
+ circle = plt.Circle((0, 0), r, color="#d9e5f7", fill=False, linewidth=1)
278
+ ax.add_artist(circle)
279
+
280
+ # axes
281
+ ax.axhline(0, color="#d9e5f7", linewidth=1)
282
+ ax.axvline(0, color="#d9e5f7", linewidth=1)
283
+
284
+ # labels
285
+ ax.text(0, 10.7, "Front", ha="center", va="bottom", fontsize=11, color="#4a5b77")
286
+ ax.text(10.7, 0, "Right", ha="left", va="center", fontsize=11, color="#4a5b77")
287
+ ax.text(-10.7, 0, "Left", ha="right", va="center", fontsize=11, color="#4a5b77")
288
+ ax.text(0, -10.7, "Rear", ha="center", va="top", fontsize=11, color="#4a5b77")
289
+
290
+ palette = ["#1d4ed8", "#0891b2", "#7c3aed", "#ea580c", "#0f766e", "#be123c", "#0369a1", "#4d7c0f"]
291
+
292
+ for idx, obj in enumerate(scene.get("objects", [])):
293
+ az = math.radians(float(obj.get("az_deg", 0.0)))
294
+ dist = float(obj.get("dist_m", 1.0))
295
+ x = dist * math.sin(az)
296
+ y = dist * math.cos(az)
297
+ size = 120 + 220 * float(obj.get("width", 0.2))
298
+ color = palette[idx % len(palette)]
299
+ ax.scatter([x], [y], s=size, c=color, alpha=0.85, edgecolors="white", linewidths=1.5, zorder=3)
300
+ ax.text(x, y + 0.42, f"{obj.get('id')}\n{obj.get('class')}", ha="center", va="bottom", fontsize=9, color="#1f2937")
301
+
302
+ ax.set_xlim(-11, 11)
303
+ ax.set_ylim(-11, 11)
304
+ ax.set_xticks([])
305
+ ax.set_yticks([])
306
+ ax.set_title("Spatial Scene Preview", fontsize=15, color="#15233d", pad=12)
307
+ return fig