Upload 16 files
Browse files- GravityLLM-Space-Demo/.gitignore +6 -0
- GravityLLM-Space-Demo/LICENSE +47 -0
- GravityLLM-Space-Demo/README.md +70 -0
- GravityLLM-Space-Demo/__pycache__/app.cpython-311.pyc +0 -0
- GravityLLM-Space-Demo/app.py +437 -0
- GravityLLM-Space-Demo/assets/gravityllm_space_banner.png +0 -0
- GravityLLM-Space-Demo/assets/spatial9_logo.png +0 -0
- GravityLLM-Space-Demo/examples/cinematic_break.json +81 -0
- GravityLLM-Space-Demo/examples/club_drop.json +110 -0
- GravityLLM-Space-Demo/examples/podcast_voice.json +81 -0
- GravityLLM-Space-Demo/requirements.txt +5 -0
- GravityLLM-Space-Demo/schemas/scene.schema.json +169 -0
- GravityLLM-Space-Demo/utils/__init__.py +0 -0
- GravityLLM-Space-Demo/utils/__pycache__/__init__.cpython-311.pyc +0 -0
- GravityLLM-Space-Demo/utils/__pycache__/scene_tools.cpython-311.pyc +0 -0
- GravityLLM-Space-Demo/utils/scene_tools.py +307 -0
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 |
+

|
| 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
|