JoaoK90 commited on
Commit
60b89d3
·
verified ·
1 Parent(s): 7e09039

Connect Space portal to verified Windows Style-SALAD session

Browse files

Frontend portal only; inference stays on the Windows PC. Preserve the existing Space SDK, visibility and hardware.

Files changed (7) hide show
  1. README.md +25 -5
  2. app.js +62 -0
  3. app.py +50 -0
  4. config.json +5 -0
  5. index.html +39 -0
  6. requirements.txt +2 -0
  7. styles.css +38 -0
README.md CHANGED
@@ -3,13 +3,33 @@ title: Controllable Human Motion Generation
3
  emoji: 🌍
4
  colorFrom: red
5
  colorTo: indigo
6
- sdk: gradio
7
- sdk_version: 6.30.0
8
- python_version: '3.12'
9
- app_file: app.py
10
  pinned: false
11
  license: mit
12
  short_description: Based on style salad model
 
 
 
 
13
  ---
14
 
15
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  emoji: 🌍
4
  colorFrom: red
5
  colorTo: indigo
 
 
 
 
6
  pinned: false
7
  license: mit
8
  short_description: Based on style salad model
9
+ sdk: gradio
10
+ sdk_version: 6.15.0
11
+ python_version: '3.12'
12
+ app_file: app.py
13
  ---
14
 
15
+ This English portal connects visitors to Style-SALAD running on the owner's Windows PC. The Space hosts only the entry page. Inference, uploads, datasets, checkpoints and generation outputs stay on the PC; no hosted GPU is requested by this repository.
16
+
17
+ Choose one of 100 movement styles, sample a reference with a seed, choose a reference ID, or upload raw HumanML3D features `(T,263)` at 20 fps with at least 41 frames. Describe the action in English and download MP4, BVH, NPY, NPZ and JSON outputs.
18
+
19
+ The colored checkerboard stays fixed in world coordinates while the camera follows the generated root. MP4 is rendered from the 22 joints reconstructed from the generated features. BVH is exported from those same joints through inverse kinematics; it is not the input to video rendering.
20
+
21
+ The computer and local application must remain online. Temporary Gradio share links can expire. Update `config.json` with the current public HTTPS `app_url`, keeping `temporary` and `updated_at`; restarting the Space loads the new address. Never use a private IP or `localhost` for external visitors.
22
+
23
+ `app.py` reuses `index.html`, `styles.css` and `app.js` with Gradio's supported `HTML` component (`css_template`, `js_on_load` and props). The portal owns no inference API and downloads no model weights. `config.json` is passed as a component prop to avoid colliding with Gradio's own `/config` route. The browser connects directly to the local studio through its HTTPS iframe.
24
+
25
+ Run the portal locally with Gradio 6.15.0 installed:
26
+
27
+ ```powershell
28
+ $env:GRADIO_SERVER_NAME = '127.0.0.1'
29
+ $env:GRADIO_SERVER_PORT = '7862'
30
+ python app.py
31
+ ```
32
+
33
+ This repository does not change the Space's hardware or billing settings. Use the free CPU configuration for the entry page; model computation remains on the local GPU.
34
+
35
+ The adapter retains its MIT license. The [Style-SALAD code](https://github.com/junhyukjeon/style-salad) and checkpoint have their own terms, including [CC BY-NC-ND 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/) for the checkpoint. Third-party components and datasets retain their own terms. Availability of 100 style labels does not guarantee equal quality beyond the released 47-style subset.
app.js ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ async function connectApplication() {
4
+ document.documentElement.lang = "en";
5
+ const status = element.querySelector(".status");
6
+ const statusText = element.querySelector("#status-text");
7
+ const frame = element.querySelector("#application-frame");
8
+ const open = element.querySelector("#open-app");
9
+ const placeholder = element.querySelector("#placeholder");
10
+ const setStatus = (state, text) => { status.dataset.state = state; statusText.textContent = text; };
11
+
12
+ // Gradio owns /config. Pass the repo's config.json through the HTML props.
13
+ const config = props.session;
14
+ if (!config || typeof config !== "object") {
15
+ setStatus("offline", "Session information unavailable");
16
+ return;
17
+ }
18
+
19
+ if (!config.app_url) {
20
+ setStatus("pending", "Session being prepared");
21
+ return;
22
+ }
23
+
24
+ let app;
25
+ try {
26
+ app = new URL(config.app_url);
27
+ if (app.protocol !== "https:" || app.username || app.password ||
28
+ /^(localhost|127\.|0\.|10\.|192\.168\.|169\.254\.|172\.(1[6-9]|2\d|3[01])\.)/i.test(app.hostname) ||
29
+ app.hostname.includes(":") || !app.hostname.includes(".")) {
30
+ throw new Error("An external HTTPS URL is required");
31
+ }
32
+ app.search = "";
33
+ app.hash = "";
34
+ if (!app.pathname.endsWith("/")) app.pathname += "/";
35
+ } catch (_) {
36
+ setStatus("offline", "Session address unavailable");
37
+ return;
38
+ }
39
+
40
+ open.href = app.href;
41
+ open.hidden = false;
42
+ frame.src = app.href;
43
+ frame.hidden = false;
44
+ placeholder.hidden = true;
45
+ setStatus("pending", "Checking the session…");
46
+
47
+ const controller = new AbortController();
48
+ const timeout = setTimeout(() => controller.abort(), 10000);
49
+ try {
50
+ const response = await fetch(new URL("config", app), { signal: controller.signal, cache: "no-store" });
51
+ if (!response.ok) throw new Error("Session unavailable");
52
+ const metadata = await response.json();
53
+ if (!metadata || !Array.isArray(metadata.components)) throw new Error("Unexpected response");
54
+ setStatus("online", config.temporary ? "Studio available · temporary link" : "Studio available");
55
+ } catch (_) {
56
+ setStatus("offline", "Availability unconfirmed · try opening the studio");
57
+ } finally {
58
+ clearTimeout(timeout);
59
+ }
60
+ }
61
+
62
+ connectApplication();
app.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Gradio Space portal. Motion inference stays on the owner's Windows GPU."""
2
+
3
+ from pathlib import Path
4
+ import json
5
+ import os
6
+ import re
7
+
8
+ import gradio as gr
9
+
10
+ ROOT = Path(__file__).resolve().parent
11
+
12
+
13
+ def create_portal() -> gr.Blocks:
14
+ """Reuse the English portal through Gradio's scoped HTML component API."""
15
+ document = (ROOT / "index.html").read_text(encoding="utf-8-sig")
16
+ body = re.search(r"<body[^>]*>(.*?)</body>", document, flags=re.S | re.I)
17
+ if body is None:
18
+ raise ValueError("The portal must contain an HTML body")
19
+ html = re.sub(r"<script\b[^>]*>.*?</script>", "", body.group(1), flags=re.S | re.I)
20
+ css = (ROOT / "styles.css").read_text(encoding="utf-8-sig")
21
+ css = css.replace(":root", ".portal").replace("body {", ".portal {")
22
+ javascript = (ROOT / "app.js").read_text(encoding="utf-8-sig")
23
+ config = json.loads((ROOT / "config.json").read_text(encoding="utf-8-sig"))
24
+
25
+ with gr.Blocks(title="Style-SALAD · Human Motion", analytics_enabled=False) as portal:
26
+ gr.HTML(
27
+ value=f'<div class="portal">{html}</div>',
28
+ css_template=css,
29
+ js_on_load=javascript,
30
+ apply_default_css=False,
31
+ session=config,
32
+ elem_id="motion-portal",
33
+ container=False,
34
+ padding=False,
35
+ )
36
+ return portal
37
+
38
+
39
+ demo = create_portal()
40
+
41
+ if __name__ == "__main__":
42
+ demo.launch(
43
+ server_name=os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"),
44
+ server_port=int(os.environ.get("GRADIO_SERVER_PORT", "7860")),
45
+ share=False,
46
+ ssr_mode=False,
47
+ footer_links=[],
48
+ css=".gradio-container {padding: 0 !important; max-width: 100% !important;} body {margin: 0; background: #f4f3ee;}",
49
+ head='<meta name="description" content="Academic human motion generation with Style-SALAD on a local GPU.">',
50
+ )
config.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "app_url": "https://5454ee2cf7716abe10.gradio.live/",
3
+ "temporary": true,
4
+ "updated_at": "2026-10-14T13:58:34.574067+00:00"
5
+ }
index.html ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="color-scheme" content="light">
7
+ <meta name="description" content="Academic human motion generation with Style-SALAD, style selection and inference on a local GPU.">
8
+ <title>Style-SALAD · Human Motion</title>
9
+ <link rel="stylesheet" href="styles.css">
10
+ </head>
11
+ <body>
12
+ <main>
13
+ <header class="hero">
14
+ <p class="eyebrow">HTI · Academic project</p>
15
+ <h1>An action.<br>A way to move.</h1>
16
+ <p class="intro">Choose a movement style and describe an action. Style-SALAD creates a new animation to watch and download as MP4, BVH and motion data.</p>
17
+ <div class="status" role="status" aria-live="polite"><span class="dot" aria-hidden="true"></span><span id="status-text">Loading session information…</span></div>
18
+ </header>
19
+
20
+ <section class="instructions" aria-label="How to use">
21
+ <article><span class="step">01</span><h2>Choose the style</h2><p>Select one of 100 styles. Sample a reference with the seed, choose a specific ID from that style, or upload a compatible <code>.npy</code>.</p></article>
22
+ <article><span class="step">02</span><h2>Describe the action</h2><p>Write in English. For example: “A person walks forward and turns left.”</p></article>
23
+ <article><span class="step">03</span><h2>Generate and watch</h2><p>Wait for your turn in the queue, compare the video with the reference, and download MP4, BVH, features and generation parameters.</p></article>
24
+ </section>
25
+
26
+ <section class="application" aria-labelledby="application-title">
27
+ <div class="application-heading"><h2 id="application-title">Generate motion</h2><a id="open-app" class="open-link" target="_blank" rel="noopener noreferrer" hidden>Open in another tab ↗</a></div>
28
+ <div id="placeholder" class="placeholder"><div class="motion-mark" aria-hidden="true">↗</div><h3 id="placeholder-title">Preparing the session</h3><p id="placeholder-text">The studio will appear here when the local application and external connection are available.</p></div>
29
+ <iframe id="application-frame" title="Style-SALAD motion generation studio" allow="autoplay; clipboard-write; fullscreen" referrerpolicy="no-referrer" hidden></iframe>
30
+ <p class="availability">Generation runs on the project host's local GPU. The computer and application must stay online. Share sessions can expire; this Gradio Space is the entry page and does not guarantee continuous availability.</p>
31
+ </section>
32
+
33
+ <aside class="input-note"><strong>Before uploading</strong><p>Use raw HumanML3D features <code>(T, 263)</code> at 20 fps with at least 41 frames. The reference uses a centered window of up to 196 frames. Videos, webcam, BVH, FBX, positions <code>(T, 22, 3)</code> and object-based <code>results.npy</code> files need conversion. Uploads are sent to the host computer.</p></aside>
34
+
35
+ <footer><p>Academic demonstration · Results depend on the action and reference. Availability of 100 style labels does not guarantee equal quality beyond the released 47-style subset.</p><p><a href="https://github.com/junhyukjeon/style-salad" target="_blank" rel="noopener noreferrer">Style-SALAD code and research ↗</a> · <a href="https://creativecommons.org/licenses/by-nc-nd/4.0/" target="_blank" rel="noopener noreferrer">Checkpoint: CC BY-NC-ND 4.0 ↗</a></p><p class="license">The adapter retains its MIT license. Third-party components and datasets retain their own terms; a supported input format does not grant rights to uploaded data.</p></footer>
36
+ </main>
37
+ <script src="app.js"></script>
38
+ </body>
39
+ </html>
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # The Gradio SDK version is pinned in README.md.
2
+ # This portal needs no model, CUDA, or dataset packages.
styles.css ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root { font-family: Inter, "Segoe UI", system-ui, sans-serif; color: #20313c; background: #f4f3ee; line-height: 1.55; }
2
+ * { box-sizing: border-box; }
3
+ body { margin: 0; }
4
+ main { width: min(1160px, calc(100% - 48px)); margin: 0 auto; padding: 66px 0 30px; }
5
+ .eyebrow { color: #3c665f; font-size: 13px; font-weight: 700; letter-spacing: .13em; text-transform: uppercase; margin: 0 0 20px; }
6
+ h1 { font-size: clamp(36px, 6vw, 64px); line-height: 1.08; font-weight: 650; letter-spacing: -.045em; margin: 0 0 25px; }
7
+ .intro { font-size: 19px; max-width: 690px; color: #59676e; margin: 0; }
8
+ .status { display: inline-flex; align-items: center; gap: 9px; margin-top: 24px; border: 1px solid #d8dfd8; border-radius: 999px; padding: 7px 13px; font-size: 13px; background: #fafbf8; }
9
+ .dot { width: 8px; height: 8px; border-radius: 50%; background: #939f99; }
10
+ .status[data-state="online"] .dot { background: #2d8a69; }
11
+ .status[data-state="offline"] .dot { background: #b67c35; }
12
+ .instructions { display: grid; grid-template-columns: repeat(3, 1fr); gap: 30px; margin: 55px 0 38px; }
13
+ .instructions article { border-top: 1px solid #ccd5cf; padding-top: 18px; }
14
+ .step { font-size: 12px; color: #68837a; letter-spacing: .1em; }
15
+ h2 { font-size: 18px; margin: 9px 0 10px; font-weight: 650; }
16
+ .instructions p { font-size: 14px; color: #606e75; margin: 0; }
17
+ code { font-size: .91em; background: #e9ede7; padding: 2px 4px; border-radius: 4px; white-space: nowrap; }
18
+ .application { border: 1px solid #d8dfd9; border-radius: 16px; background: #fff; overflow: hidden; }
19
+ .application-heading { display: flex; justify-content: space-between; align-items: center; gap: 18px; padding: 17px 24px; border-bottom: 1px solid #e5eae5; }
20
+ .application-heading h2 { margin: 0; }
21
+ a { color: #386b63; text-decoration: none; }
22
+ a:hover { text-decoration: underline; }
23
+ a:focus-visible { outline: 3px solid #377f73; outline-offset: 5px; }
24
+ .open-link { font-size: 13px; font-weight: 600; }
25
+ .placeholder { padding: 60px 24px; text-align: center; background: #fafbf8; }
26
+ .motion-mark { width: 55px; height: 55px; margin: auto; border: 1px solid #d4ded4; border-radius: 14px; color: #547a69; font-size: 35px; line-height: 50px; }
27
+ .placeholder h3 { font-size: 18px; font-weight: 600; margin: 16px 0 8px; }
28
+ .placeholder p { color: #69756f; font-size: 14px; max-width: 560px; margin: auto; }
29
+ iframe { width: 100%; height: 1050px; border: 0; display: block; background: #fff; }
30
+ [hidden] { display: none !important; }
31
+ .availability { padding: 15px 24px 18px; margin: 0; background: #f5f8f4; border-top: 1px solid #e1e8df; color: #5a6a5f; font-size: 12px; }
32
+ .input-note { margin: 24px 0 35px; padding: 18px 22px; border-left: 3px solid #92a99b; background: #eaeee7; border-radius: 0 8px 8px 0; }
33
+ .input-note strong { font-size: 14px; }
34
+ .input-note p { margin: 7px 0 0; font-size: 13px; color: #58675e; }
35
+ footer { border-top: 1px solid #d9ded5; padding-top: 20px; color: #728077; font-size: 12px; }
36
+ footer p { margin: 0 0 8px; }
37
+ .license { max-width: 800px; }
38
+ @media (max-width: 720px) { main { width: calc(100% - 32px); padding-top: 36px; } .intro { font-size: 17px; } .instructions { grid-template-columns: 1fr; gap: 20px; margin-top: 35px; } .application-heading { padding: 16px; flex-wrap: wrap; } .availability { padding: 14px 16px; } iframe { height: 1250px; } }