chingshuai commited on
Commit
d9ffb99
·
1 Parent(s): a6c3d88

删除不必要的资源文件

Browse files
README.md CHANGED
@@ -10,10 +10,69 @@ pinned: false
10
  short_description: Text-to-3D and Image-to-3D Generation
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
14
 
 
 
 
15
 
16
- ```bash
17
- # huggingface官方会运行这个
18
- pip install --no-cache-dir pip -U && pip install --no-cache-dir datasets "huggingface-hub>=0.30" "hf-transfer>=0.1.4" "protobuf<4" "click<8.1" "pydantic~=1.0"
19
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  short_description: Text-to-3D and Image-to-3D Generation
11
  ---
12
 
 
13
 
14
+ <p align="center">
15
+ <img src="./assets/banner.png" alt="Banner" width="100%">
16
+ </p>
17
 
18
+ <div align="center">
19
+ <a href="https://hunyuan.tencent.com/motion" target="_blank">
20
+ <img src="https://img.shields.io/badge/Official%20Site-333399.svg?logo=homepage" height="22px" alt="Official Site">
21
+ </a>
22
+ <a href="https://huggingface.co/spaces/tencent/HY-Motion-1.0" target="_blank">
23
+ <img src="https://img.shields.io/badge/%F0%9F%A4%97%20Demo-276cb4.svg" height="22px" alt="HuggingFace Space">
24
+ </a>
25
+ <a href="https://huggingface.co/tencent/HY-Motion-1.0" target="_blank">
26
+ <img src="https://img.shields.io/badge/%F0%9F%A4%97%20Models-d96902.svg" height="22px" alt="HuggingFace Models">
27
+ </a>
28
+ <a href="https://arxiv.org/pdf/2512.23464" target="_blank">
29
+ <img src="https://img.shields.io/badge/Report-b5212f.svg?logo=arxiv" height="22px" alt="ArXiv Report">
30
+ </a>
31
+ <a href="https://x.com/TencentHunyuan" target="_blank">
32
+ <img src="https://img.shields.io/badge/Hunyuan-black.svg?logo=x" height="22px" alt="X (Twitter)">
33
+ </a>
34
+ </div>
35
+
36
+
37
+ # HY-Motion 1.0: Scaling Flow Matching Models for 3D Motion Generation
38
+
39
+
40
+ <p align="center">
41
+ <img src="./assets/teaser.png" alt="Teaser" width="90%">
42
+ </p>
43
+
44
+
45
+ ## 🔥 News
46
+ - **Dec 30, 2025**: 🤗 We released the inference code and pretrained models of [HY-Motion 1.0](https://huggingface.co/tencent/HY-Motion-1.0). Please give it a try via our [HuggingFace Space](https://huggingface.co/spaces/tencent/HY-Motion-1.0) and our [Official Site](https://hunyuan.tencent.com/motion)!
47
+
48
+
49
+ ## **Introduction**
50
+
51
+ **HY-Motion 1.0** is a series of text-to-3D human motion generation models based on Diffusion Transformer (DiT) and Flow Matching. It allows developers to generate skeleton-based 3D character animations from simple text prompts, which can be directly integrated into various 3D animation pipelines. This model series is the first to scale DiT-based text-to-motion models to the billion-parameter level, achieving significant improvements in instruction-following capabilities and motion quality over existing open-source models.
52
+
53
+ ### Key Features
54
+ - **State-of-the-Art Performance**: Achieves state-of-the-art performance in both instruction-following capability and generated motion quality.
55
+
56
+ - **Billion-Scale Models**: We are the first to successfully scale DiT-based models to the billion-parameter level for text-to-motion generation. This results in superior instruction understanding and following capabilities, outperforming comparable open-source models.
57
+
58
+ - **Advanced Three-Stage Training**: Our models are trained using a comprehensive three-stage process:
59
+
60
+ - *Large-Scale Pre-training*: Trained on over 3,000 hours of diverse motion data to learn a broad motion prior.
61
+
62
+ - *High-Quality Fine-tuning*: Fine-tuned on 400 hours of curated, high-quality 3D motion data to enhance motion detail and smoothness.
63
+
64
+ - *Reinforcement Learning*: Utilizes Reinforcement Learning from human feedback and reward models to further refine instruction-following and motion naturalness.
65
+
66
+
67
+
68
+ <p align="center">
69
+ <img src="./assets/pipeline.png" alt="System Overview" width="100%">
70
+ </p>
71
+
72
+ <p align="center">
73
+ <img src="./assets/arch.png" alt="Architecture" width="100%">
74
+ </p>
75
+
76
+ <p align="center">
77
+ <img src="./assets/sotacomp.png" alt="ComparisonSoTA" width="100%">
78
+ </p>
gradio_app.py CHANGED
@@ -342,7 +342,7 @@ HTML_OUTPUT_PLACEHOLDER = """
342
  """
343
 
344
 
345
- def load_examples_from_txt(txt_path: str):
346
  """Load examples from txt file."""
347
 
348
  def _parse_line(line: str) -> Optional[Tuple[str, float]]:
@@ -351,7 +351,8 @@ def load_examples_from_txt(txt_path: str):
351
  parts = line.split("#")
352
  if len(parts) >= 2:
353
  text = parts[0].strip()
354
- duration = int(parts[1]) / 20.0
 
355
  else:
356
  text = line.strip()
357
  duration = 5.0
 
342
  """
343
 
344
 
345
+ def load_examples_from_txt(txt_path: str, example_record_fps=20, max_duration=12):
346
  """Load examples from txt file."""
347
 
348
  def _parse_line(line: str) -> Optional[Tuple[str, float]]:
 
351
  parts = line.split("#")
352
  if len(parts) >= 2:
353
  text = parts[0].strip()
354
+ duration = int(parts[1]) / example_record_fps
355
+ duration = min(duration, max_duration)
356
  else:
357
  text = line.strip()
358
  duration = 5.0
scripts/gradio/static/scripts3d/create_ground.js DELETED
@@ -1,191 +0,0 @@
1
- import * as THREE from "three";
2
-
3
- // extract common adaptive logic
4
- function getAdaptiveGridSize(sample_data, default_size = 5) {
5
- if (sample_data) {
6
- const bounds = calculateDataBounds(sample_data);
7
- const grid_size = Math.max(bounds.maxRange * 3, 5); // 1.5x margin
8
- console.log(`Adaptive ground size: ${grid_size.toFixed(2)}, data range: ${bounds.maxRange.toFixed(2)}`);
9
- return grid_size;
10
- }
11
- return default_size;
12
- }
13
-
14
- function createBaseChessboard(
15
- grid_size = 5,
16
- divisions = 10,
17
- white = "#ffffff",
18
- black = "#444444",
19
- texture_size = 1024,
20
- sample_data = null,
21
- ) {
22
- // Use adaptive sizing if sample_data provided, otherwise use fixed grid_size
23
- if (sample_data) {
24
- grid_size = getAdaptiveGridSize(sample_data, grid_size);
25
- }
26
-
27
- // Create chessboard texture with enhanced visual style
28
- // Ensure texture_size is divisible by divisions to avoid sub-pixel rendering
29
- var adjusted_texture_size = Math.floor(texture_size / divisions) * divisions;
30
- var canvas = document.createElement("canvas");
31
- canvas.width = canvas.height = adjusted_texture_size;
32
- var context = canvas.getContext("2d");
33
-
34
- // Disable anti-aliasing for crisp edges
35
- context.imageSmoothingEnabled = false;
36
-
37
- var step = adjusted_texture_size / divisions; // Now guaranteed to be an integer
38
- for (var i = 0; i < divisions; i++) {
39
- for (var j = 0; j < divisions; j++) {
40
- context.fillStyle = (i + j) % 2 === 0 ? white : black;
41
- context.fillRect(i * step, j * step, step, step);
42
- }
43
- }
44
-
45
- var texture = new THREE.CanvasTexture(canvas);
46
- // Use NearestFilter for sharp/crisp edges between chess squares
47
- texture.wrapS = THREE.RepeatWrapping;
48
- texture.wrapT = THREE.RepeatWrapping;
49
- texture.magFilter = THREE.NearestFilter;
50
- texture.minFilter = THREE.NearestFilter;
51
- texture.generateMipmaps = false;
52
-
53
- // Create plane geometry
54
- var planeGeometry = new THREE.PlaneGeometry(grid_size, grid_size);
55
-
56
- // Enhanced material with better visual properties
57
- var planeMaterial = new THREE.MeshStandardMaterial({
58
- map: texture,
59
- side: THREE.DoubleSide,
60
- transparent: true,
61
- opacity: 0.85,
62
- roughness: 0.9,
63
- metalness: 0.1,
64
- emissiveIntensity: 0.05,
65
- });
66
-
67
- // Create grid mesh
68
- var plane = new THREE.Mesh(planeGeometry, planeMaterial);
69
- plane.receiveShadow = true;
70
-
71
- return plane;
72
- }
73
-
74
- function getChessboard(...args) {
75
- var plane = createBaseChessboard(...args);
76
- plane.rotation.x = -Math.PI; // rotate to make the plane horizontal
77
- return plane;
78
- }
79
-
80
- function getChessboardXZ(...args) {
81
- var plane = createBaseChessboard(...args);
82
- plane.rotation.x = -Math.PI / 2; // rotate to make the plane horizontal
83
- return plane;
84
- }
85
-
86
- function getCoordinate(axisLength) {
87
- // create a group to store the coordinate axes
88
- var axes = new THREE.Group();
89
-
90
- // define the material of the axes
91
- var materialX = new THREE.LineBasicMaterial({ color: 0xff0000 }); // red X axis
92
- var materialY = new THREE.LineBasicMaterial({ color: 0x00ff00 }); // green Y axis
93
- var materialZ = new THREE.LineBasicMaterial({ color: 0x0000ff }); // blue Z axis
94
-
95
- // create axis lines (X axis, Y axis, Z axis)
96
- var xAxisGeometry = new THREE.BufferGeometry().setFromPoints([
97
- new THREE.Vector3(0, 0, 0),
98
- new THREE.Vector3(axisLength, 0, 0),
99
- ]);
100
- var yAxisGeometry = new THREE.BufferGeometry().setFromPoints([
101
- new THREE.Vector3(0, 0, 0),
102
- new THREE.Vector3(0, axisLength, 0),
103
- ]);
104
- var zAxisGeometry = new THREE.BufferGeometry().setFromPoints([
105
- new THREE.Vector3(0, 0, 0),
106
- new THREE.Vector3(0, 0, axisLength),
107
- ]);
108
-
109
- var xAxis = new THREE.Line(xAxisGeometry, materialX);
110
- var yAxis = new THREE.Line(yAxisGeometry, materialY);
111
- var zAxis = new THREE.Line(zAxisGeometry, materialZ);
112
-
113
- // add axes to the group
114
- axes.add(xAxis);
115
- axes.add(yAxis);
116
- axes.add(zAxis);
117
-
118
- return axes;
119
- }
120
-
121
- function calculateDataBounds(sample_data) {
122
- let minX = Infinity,
123
- maxX = -Infinity;
124
- let minY = Infinity,
125
- maxY = -Infinity;
126
- let minZ = Infinity,
127
- maxZ = -Infinity;
128
-
129
- // iterate through sample_data to find the maximum and minimum values
130
- if (sample_data && sample_data.length > 0) {
131
- sample_data.forEach((frame) => {
132
- if (frame.positions && Array.isArray(frame.positions)) {
133
- frame.positions.forEach((pos) => {
134
- // support multiple position data formats
135
- let x, y, z;
136
- if (typeof pos === "object") {
137
- x = pos.x !== undefined ? pos.x : pos[0];
138
- y = pos.y !== undefined ? pos.y : pos[1];
139
- z = pos.z !== undefined ? pos.z : pos[2];
140
- } else if (Array.isArray(pos)) {
141
- [x, y, z] = pos;
142
- }
143
-
144
- if (x !== undefined && y !== undefined && z !== undefined) {
145
- minX = Math.min(minX, x);
146
- maxX = Math.max(maxX, x);
147
- minY = Math.min(minY, y);
148
- maxY = Math.max(maxY, y);
149
- minZ = Math.min(minZ, z);
150
- maxZ = Math.max(maxZ, z);
151
- }
152
- });
153
- }
154
- });
155
- }
156
-
157
- // if no valid data is found, use default values
158
- if (minX === Infinity || maxX === -Infinity) {
159
- minX = maxX = minY = maxY = minZ = maxZ = 0;
160
- }
161
-
162
- const rangeX = Math.abs(maxX - minX);
163
- const rangeY = Math.abs(maxY - minY);
164
- const rangeZ = Math.abs(maxZ - minZ);
165
-
166
- // calculate the maximum range of the XZ plane (the ground mainly cares about the movement of the X and Z axes)
167
- const maxRange = Math.max(rangeX, rangeZ);
168
-
169
- // add debug information
170
- console.log(
171
- `Data boundaries: X[${minX.toFixed(2)}, ${maxX.toFixed(2)}], Y[${minY.toFixed(2)}, ${maxY.toFixed(2)}], Z[${minZ.toFixed(2)}, ${maxZ.toFixed(2)}]`,
172
- );
173
- console.log(
174
- `Ranges: X=${rangeX.toFixed(2)}, Y=${rangeY.toFixed(2)}, Z=${rangeZ.toFixed(2)}, Max=${maxRange.toFixed(2)}`,
175
- );
176
-
177
- return {
178
- minX,
179
- maxX,
180
- minY,
181
- maxY,
182
- minZ,
183
- maxZ,
184
- rangeX,
185
- rangeY,
186
- rangeZ,
187
- maxRange,
188
- };
189
- }
190
-
191
- export { calculateDataBounds, getChessboard, getChessboardXZ, getCoordinate };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/gradio/static/scripts3d/create_scene.js DELETED
@@ -1,195 +0,0 @@
1
- import * as THREE from "three";
2
- import { getChessboard, getChessboardXZ, getCoordinate } from "./create_ground.js";
3
-
4
- function create_plane(scene) {
5
- const planeGeometry = new THREE.PlaneGeometry(20, 20);
6
- const planeMaterial = new THREE.MeshStandardMaterial({ color: 0x808080 });
7
- const plane = new THREE.Mesh(planeGeometry, planeMaterial);
8
- plane.position.y = -1;
9
- plane.receiveShadow = true; // make the plane receive shadows
10
- scene.add(plane);
11
- }
12
-
13
- function create_cube(scene) {
14
- // add a cube
15
- const cubeGeometry = new THREE.BoxGeometry();
16
- const cubeMaterial = new THREE.MeshPhongMaterial({ color: 0xffffff });
17
- const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
18
- cube.position.y = 1;
19
- cube.castShadow = true; // make the cube cast shadows
20
- scene.add(cube);
21
- }
22
-
23
- function create_scene(scene, camera, renderer, use_ground = true, axis_up = "z", axis_forward = "-y") {
24
- const width = document.querySelector(".container").offsetWidth;
25
- const height = width;
26
-
27
- // Camera setup based on axis orientation
28
- if (axis_up == "z") {
29
- camera.up.set(0, 0, 1);
30
- if (axis_forward == "-y") {
31
- camera.position.set(0, -3, 3);
32
- } else if (axis_forward == "y") {
33
- camera.position.set(0, 3, 3);
34
- }
35
- camera.lookAt(new THREE.Vector3(0, 0, 1.5));
36
- } else if (axis_up == "y") {
37
- camera.up.set(0, 1, 0);
38
- if (axis_forward == "z") {
39
- camera.position.set(0, 2.5, 5);
40
- } else if (axis_forward == "-z") {
41
- camera.position.set(0, 2.5, -5);
42
- }
43
- camera.lookAt(new THREE.Vector3(0, 1, 0));
44
- }
45
-
46
- scene.background = new THREE.Color(0x000000);
47
-
48
- // ===== Fog for depth perception =====
49
- // Using FogExp2 for natural exponential falloff, density ~0.06
50
- scene.fog = new THREE.FogExp2(0x424242, 0.06);
51
-
52
- // ===== Shadow Configuration =====
53
- renderer.shadowMap.enabled = true;
54
- renderer.shadowMap.type = THREE.PCFSoftShadowMap;
55
-
56
- // ===== Enhanced Lighting Setup =====
57
-
58
- // 1. Hemisphere Light - natural sky/ground ambient
59
- const hemisphereLight = new THREE.HemisphereLight(
60
- 0xffffff, // sky color
61
- 0x444444, // ground color
62
- 1.8 // intensity
63
- );
64
- hemisphereLight.position.set(0, 2, 0);
65
- scene.add(hemisphereLight);
66
-
67
- // 2. Main Directional Light (key light with shadows)
68
- const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
69
- if (axis_up == "z") {
70
- if (axis_forward == "-y") {
71
- directionalLight.position.set(-3, 1, 5);
72
- } else if (axis_forward == "y") {
73
- directionalLight.position.set(3, 1, 5);
74
- }
75
- } else if (axis_up == "y") {
76
- if (axis_forward == "z") {
77
- directionalLight.position.set(3, 5, 4);
78
- } else if (axis_forward == "-z") {
79
- directionalLight.position.set(3, 5, -4);
80
- }
81
- }
82
- directionalLight.castShadow = true;
83
- directionalLight.shadow.mapSize.width = 2048;
84
- directionalLight.shadow.mapSize.height = 2048;
85
- directionalLight.shadow.camera.near = 0.5;
86
- directionalLight.shadow.camera.far = 50;
87
- directionalLight.shadow.camera.left = -10;
88
- directionalLight.shadow.camera.right = 10;
89
- directionalLight.shadow.camera.top = 10;
90
- directionalLight.shadow.camera.bottom = -10;
91
- directionalLight.shadow.bias = -0.0001;
92
- scene.add(directionalLight);
93
-
94
- // 3. Fill Light (softer, from opposite side)
95
- const fillLight = new THREE.DirectionalLight(0xaaccff, 0.4);
96
- fillLight.position.set(-3, 3, -2);
97
- scene.add(fillLight);
98
-
99
- // 4. Rim Light (back light for depth)
100
- const rimLight = new THREE.DirectionalLight(0xffeedd, 0.3);
101
- rimLight.position.set(0, 4, -5);
102
- scene.add(rimLight);
103
-
104
- // ===== Ground Setup =====
105
- if (use_ground) {
106
- if (axis_up == "z") {
107
- var plane = getChessboard(50, 50, '#ffffff', '#3a3a3a', 1024);
108
- plane.name = 'ground';
109
- plane.receiveShadow = true;
110
- scene.add(plane);
111
- } else if (axis_up == "y") {
112
- var plane = getChessboardXZ(50, 50, '#ffffff', '#3a3a3a', 1024);
113
- plane.name = 'ground';
114
- plane.receiveShadow = true;
115
- scene.add(plane);
116
- }
117
-
118
- // Optional: coordinate axes helper
119
- // var coord = getCoordinate(1);
120
- // scene.add(coord);
121
- }
122
-
123
- return 0;
124
- }
125
-
126
- function fitCameraToScene(scene, camera, controls = null, opts = {}) {
127
- const { margin = 1.05, axis_up = "y", excludeNames = ["ground"] } = opts;
128
-
129
- const box = new THREE.Box3();
130
- const tmp = new THREE.Box3();
131
- let has = false;
132
-
133
- scene.traverse((obj) => {
134
- if (!obj || !obj.visible) return;
135
- if (obj.isLight) return;
136
- const t = obj.type || "";
137
- if (t.endsWith("Helper")) return;
138
- if (excludeNames && excludeNames.includes(obj.name)) return;
139
-
140
- if (obj.isMesh) {
141
- if (obj.geometry && obj.geometry.type === "PlaneGeometry") return;
142
- try {
143
- tmp.setFromObject(obj);
144
- if (!tmp.isEmpty()) {
145
- if (!has) {
146
- box.copy(tmp);
147
- has = true;
148
- } else {
149
- box.union(tmp);
150
- }
151
- }
152
- } catch (_) {}
153
- }
154
- });
155
-
156
- if (!has || box.isEmpty()) return;
157
-
158
- const sphere = new THREE.Sphere();
159
- box.getBoundingSphere(sphere);
160
- const center = sphere.center.clone();
161
- const radius = Math.max(sphere.radius, 1e-3);
162
-
163
- const vFov = THREE.MathUtils.degToRad(camera.fov);
164
- const hFov = 2 * Math.atan(Math.tan(vFov / 2) * camera.aspect);
165
- const distV = radius / Math.sin(vFov / 2);
166
- const distH = radius / Math.sin(hFov / 2);
167
- const dist = Math.max(distV, distH) * margin;
168
-
169
- // 25° top-down view (azimuth 45°, elevation 25°)
170
- const elev = THREE.MathUtils.degToRad(25);
171
- const azim = Math.PI / 4;
172
- const horiz = Math.cos(elev);
173
- let dir;
174
-
175
- if (axis_up === "y") {
176
- dir = new THREE.Vector3(Math.sin(azim) * horiz, Math.sin(elev), Math.cos(azim) * horiz);
177
- camera.up.set(0, 1, 0);
178
- } else {
179
- dir = new THREE.Vector3(Math.sin(azim) * horiz, Math.cos(azim) * horiz, Math.sin(elev));
180
- camera.up.set(0, 0, 1);
181
- }
182
-
183
- camera.position.copy(center).add(dir.multiplyScalar(dist));
184
- camera.updateProjectionMatrix();
185
- camera.lookAt(center);
186
-
187
- if (controls) {
188
- controls.target.copy(center);
189
- controls.minDistance = Math.max(radius * 0.2, 0.1);
190
- controls.maxDistance = Math.max(dist * 3, controls.minDistance + 0.1);
191
- controls.update();
192
- }
193
- }
194
-
195
- export { create_scene, fitCameraToScene };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/gradio/static/scripts3d/draw_skeleton.js DELETED
@@ -1,121 +0,0 @@
1
- import * as THREE from "three";
2
-
3
- const defaultEdges = [
4
- [1, 0],
5
- [2, 1],
6
- [3, 2],
7
- [4, 3],
8
- [5, 1],
9
- [6, 5],
10
- [7, 6],
11
- [8, 1],
12
- [9, 8],
13
- [10, 9],
14
- [11, 10],
15
- [12, 8],
16
- [13, 12],
17
- [14, 13],
18
- [15, 0],
19
- [16, 0],
20
- [17, 15],
21
- [18, 16],
22
- [19, 14],
23
- [20, 19],
24
- [21, 14],
25
- [22, 11],
26
- [23, 22],
27
- [24, 11],
28
- ];
29
-
30
- var geometries = [];
31
-
32
- function clearGeometries(scene) {
33
- geometries.forEach((obj) => {
34
- scene.remove(obj);
35
- if (obj.geometry) obj.geometry.dispose();
36
- if (obj.material) obj.material.dispose();
37
- });
38
- geometries = [];
39
- }
40
-
41
- function drawJoints(keypoints, scene, radius_joint) {
42
- const sphereGeometry = new THREE.SphereGeometry(radius_joint, 32, 32);
43
- const sphereMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000 });
44
-
45
- keypoints.forEach((point) => {
46
- // Check visibility if confidence score exists
47
- if (point.length > 3 && point[3] < 0.1) {
48
- return;
49
- }
50
-
51
- const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
52
- sphere.position.set(point[0], point[1], point[2]);
53
- geometries.push(sphere);
54
- scene.add(sphere);
55
- });
56
- }
57
-
58
- function drawLimbs(keypoints, edges, scene, radius_limb) {
59
- const ellipsoidGeometry = new THREE.SphereGeometry(radius_limb, 32, 32);
60
- const ellipsoidMaterial = new THREE.MeshStandardMaterial({ color: 0x0000ff });
61
-
62
- edges.forEach((edge) => {
63
- const idx1 = edge[0];
64
- const idx2 = edge[1];
65
-
66
- // Validate indices
67
- if (idx1 >= keypoints.length || idx2 >= keypoints.length) {
68
- return;
69
- }
70
-
71
- // Check visibility
72
- const p1 = keypoints[idx1];
73
- const p2 = keypoints[idx2];
74
- if (
75
- (p1.length > 3 && p1[3] < 0.1) ||
76
- (p2.length > 3 && p2[3] < 0.1)
77
- ) {
78
- return;
79
- }
80
-
81
- const start = new THREE.Vector3(p1[0], p1[1], p1[2]);
82
- const end = new THREE.Vector3(p2[0], p2[1], p2[2]);
83
-
84
- const direction = new THREE.Vector3().subVectors(end, start);
85
- const length = direction.length();
86
-
87
- // create an ellipsoid
88
- const ellipsoid = new THREE.Mesh(ellipsoidGeometry, ellipsoidMaterial);
89
-
90
- // scale: x,y = 1 (radius_limb), z matches length
91
- ellipsoid.scale.set(1, 1, length / 2 / radius_limb);
92
-
93
- // position: midpoint
94
- ellipsoid.position.addVectors(start, end).multiplyScalar(0.5);
95
-
96
- // rotation: point to end
97
- ellipsoid.lookAt(end);
98
-
99
- geometries.push(ellipsoid);
100
- scene.add(ellipsoid);
101
- });
102
- }
103
-
104
- function drawSingleSkeleton(keypoints, edges, scene, radius_joint, radius_limb) {
105
- drawJoints(keypoints, scene, radius_joint);
106
- drawLimbs(keypoints, edges, scene, radius_limb);
107
- }
108
-
109
- function visualizeSkeleton(keypoints, scene, radius_joint = 0.02, radius_limb = 0.03) {
110
- clearGeometries(scene);
111
- drawSingleSkeleton(keypoints, defaultEdges, scene, radius_joint, radius_limb);
112
- }
113
-
114
- function visualizeAllSkeleton(infos, scene, radius_joint = 0.02, radius_limb = 0.03) {
115
- clearGeometries(scene);
116
- infos.forEach((info) => {
117
- drawSingleSkeleton(info.keypoints3d, info.edges, scene, radius_joint, radius_limb);
118
- });
119
- }
120
-
121
- export { visualizeAllSkeleton, visualizeSkeleton };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/gradio/static/scripts3d/load_smpl.js DELETED
@@ -1,126 +0,0 @@
1
- import * as THREE from "three";
2
-
3
- const NUM_SKIN_WEIGHTS = 4;
4
-
5
- async function load_smpl_with_shapes(shapes, gender) {
6
- const urls = {
7
- neutral: [
8
- "/static/assets/dump_smplh/v_template.bin",
9
- "/static/assets/dump_smplh/faces.bin",
10
- "/static/assets/dump_smplh/skinWeights.bin",
11
- "/static/assets/dump_smplh/skinIndice.bin",
12
- "/static/assets/dump_smplh/j_template.bin",
13
- ],
14
- }[gender];
15
- const gender_color = {
16
- neutral: 0xffffff,
17
- male: 0x6495ed, // Cornflower blue (lighter blue)
18
- female: 0xff6b81, // Light coral (softer red)
19
- };
20
-
21
- console.log(shapes.length);
22
- const geometry = new THREE.BufferGeometry();
23
- const buffers = await Promise.all(urls.map((url) => fetch(url).then((response) => response.arrayBuffer())));
24
- const v_template = new Float32Array(buffers[0]);
25
- const offsets = await Promise.all(
26
- shapes.map((_, i) =>
27
- fetch("/static/assets/dump_smplh/shapeoffset_" + i + ".bin")
28
- .then((response) => response.arrayBuffer())
29
- .then((buffer) => new Float32Array(buffer)),
30
- ),
31
- );
32
- const offsets_j = await Promise.all(
33
- shapes.map((_, i) =>
34
- fetch("/static/assets/dump_smplh/shapeoffset_j_" + i + ".bin")
35
- .then((response) => response.arrayBuffer())
36
- .then((buffer) => new Float32Array(buffer)),
37
- ),
38
- );
39
- offsets.forEach((offset, i) => {
40
- for (let j = 0; j < v_template.length / 3; j++) {
41
- v_template[3 * j] += offset[3 * j] * shapes[i];
42
- v_template[3 * j + 1] += offset[3 * j + 1] * shapes[i];
43
- v_template[3 * j + 2] += offset[3 * j + 2] * shapes[i];
44
- }
45
- });
46
- const faces = new Uint16Array(buffers[1]);
47
- const skinWeights = new Float32Array(buffers[2]);
48
- const skinIndices = new Uint16Array(buffers[3]);
49
-
50
- const keypoints = new Float32Array(buffers[4]);
51
- for (let i = 0; i < keypoints.length / 3; i++) {
52
- console.log("keypoints", keypoints[3 * i], keypoints[3 * i + 1], keypoints[3 * i + 2]);
53
- }
54
-
55
- offsets_j.forEach((offset_j, i) => {
56
- console.log("shape id", i, shapes[i]);
57
- console.log("keypoints", keypoints[0], keypoints[1], keypoints[2]);
58
- console.log("offset_j", offset_j[0], offset_j[1], offset_j[2]);
59
-
60
- for (let j = 0; j < keypoints.length / 3; j++) {
61
- keypoints[3 * j] += offset_j[3 * j] * shapes[i];
62
- keypoints[3 * j + 1] += offset_j[3 * j + 1] * shapes[i];
63
- keypoints[3 * j + 2] += offset_j[3 * j + 2] * shapes[i];
64
- }
65
- });
66
-
67
- // edges contain the skeleton link relationship
68
- // const edges = [-1, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 9, 12, 13, 14, 16, 17, 18, 19, 20, 21];
69
- const edges = [
70
- -1, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 9, 12, 13, 14, 16, 17, 18, 19, 20, 22, 23, 20, 25, 26, 20, 28, 29, 20,
71
- 31, 32, 20, 34, 35, 21, 37, 38, 21, 40, 41, 21, 43, 44, 21, 46, 47, 21, 49, 50,
72
- ];
73
- // assume jointPositions is a J x 3 array, each element is an array containing X, Y, Z coordinates
74
- var rootBone = new THREE.Bone();
75
- rootBone.position.set(keypoints[0], keypoints[1], keypoints[2]);
76
- // scene.add(rootBone);
77
- var bones = [rootBone];
78
- // create bones
79
- for (let i = 1; i < keypoints.length / 3; i++) {
80
- const bone = new THREE.Bone();
81
- const parentIndex = edges[i];
82
- bone.position.set(
83
- keypoints[3 * i] - keypoints[3 * parentIndex],
84
- keypoints[3 * i + 1] - keypoints[3 * parentIndex + 1],
85
- keypoints[3 * i + 2] - keypoints[3 * parentIndex + 2],
86
- );
87
- console.log(i, bone.position);
88
- bones.push(bone);
89
- bones[parentIndex].add(bone);
90
- }
91
- var skeleton = new THREE.Skeleton(bones);
92
- geometry.setIndex(new THREE.BufferAttribute(faces, 1));
93
-
94
- geometry.setAttribute("position", new THREE.BufferAttribute(v_template, 3));
95
- geometry.setAttribute("skinIndex", new THREE.BufferAttribute(skinIndices, NUM_SKIN_WEIGHTS));
96
- geometry.setAttribute("skinWeight", new THREE.BufferAttribute(skinWeights, NUM_SKIN_WEIGHTS));
97
-
98
- geometry.computeVertexNormals();
99
- console.log(geometry);
100
- const material = new THREE.MeshStandardMaterial({
101
- color: gender_color[gender],
102
- skinning: true,
103
- side: THREE.DoubleSide,
104
- });
105
- var mesh = new THREE.SkinnedMesh(geometry, material);
106
- mesh.castShadow = true;
107
- mesh.receiveShadow = true;
108
- mesh.add(bones[0]);
109
- mesh.bind(skeleton);
110
- return { bones, skeleton, mesh };
111
- }
112
-
113
- function reshapeArrayTo2D(float32Array, rows) {
114
- const twoDArray = [];
115
- const cols = float32Array.length / rows;
116
- for (let i = 0; i < rows; i++) {
117
- const row = new Float32Array(cols);
118
- for (let j = 0; j < cols; j++) {
119
- row[j] = float32Array[i * cols + j];
120
- }
121
- twoDArray.push(row);
122
- }
123
- return twoDArray;
124
- }
125
-
126
- export { load_smpl_with_shapes };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/gradio/static/scripts3d/load_wooden.js DELETED
@@ -1,167 +0,0 @@
1
- import * as THREE from 'three';
2
-
3
- const NUM_SKIN_WEIGHTS = 4;
4
-
5
- // SMPL-H joint names (52 joints)
6
- const SMPLH_JOINT_NAMES = [
7
- "Pelvis", "L_Hip", "R_Hip", "Spine1",
8
- "L_Knee", "R_Knee", "Spine2",
9
- "L_Ankle", "R_Ankle", "Spine3",
10
- "L_Foot", "R_Foot", "Neck", "L_Collar", "R_Collar", "Head",
11
- "L_Shoulder", "R_Shoulder", "L_Elbow", "R_Elbow",
12
- "L_Wrist", "R_Wrist",
13
- "L_Index1", "L_Index2", "L_Index3",
14
- "L_Middle1", "L_Middle2", "L_Middle3",
15
- "L_Pinky1", "L_Pinky2", "L_Pinky3",
16
- "L_Ring1", "L_Ring2", "L_Ring3",
17
- "L_Thumb1", "L_Thumb2", "L_Thumb3",
18
- "R_Index1", "R_Index2", "R_Index3",
19
- "R_Middle1", "R_Middle2", "R_Middle3",
20
- "R_Pinky1", "R_Pinky2", "R_Pinky3",
21
- "R_Ring1", "R_Ring2", "R_Ring3",
22
- "R_Thumb1", "R_Thumb2", "R_Thumb3",
23
- ];
24
-
25
- // Default kintree (parent indices) for SMPL-H 52 joints
26
- const DEFAULT_EDGES = [-1, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 9, 12, 13, 14, 16, 17, 18, 19, 20, 22, 23, 20, 25, 26, 20, 28, 29, 20, 31, 32, 20, 34, 35, 21, 37, 38, 21, 40, 41, 21, 43, 44, 21, 46, 47, 21, 49, 50];
27
-
28
- /**
29
- * Load wooden model from binary files
30
- * @param {Array} shapes - Shape parameters (unused for wooden model)
31
- * @param {string} gender - Gender parameter (unused for wooden model)
32
- * @returns {Object} { bones, skeleton, mesh, jointNames }
33
- */
34
- async function load_wooden(shapes, gender, basePath = '/static/assets/dump_wooden') {
35
- console.log("Loading wooden model...");
36
- console.log(`Using base path: ${basePath}`);
37
-
38
- const urls = [
39
- `${basePath}/v_template.bin`,
40
- `${basePath}/faces.bin`,
41
- `${basePath}/skinWeights.bin`,
42
- `${basePath}/skinIndice.bin`,
43
- `${basePath}/j_template.bin`,
44
- `${basePath}/uvs.bin`,
45
- ];
46
-
47
- // Try to load kintree
48
- let edges = [...DEFAULT_EDGES];
49
- try {
50
- const kintreeResponse = await fetch(`${basePath}/kintree.bin`);
51
- if (kintreeResponse.ok) {
52
- const kintreeBuffer = await kintreeResponse.arrayBuffer();
53
- edges = Array.from(new Int32Array(kintreeBuffer));
54
- console.log(`Loaded kintree with ${edges.length} joints`);
55
- }
56
- } catch (e) {
57
- console.log('Using default kintree');
58
- }
59
-
60
- // Try to load joint names
61
- let jointNames = [...SMPLH_JOINT_NAMES];
62
- try {
63
- const namesResponse = await fetch(`${basePath}/joint_names.json`);
64
- if (namesResponse.ok) {
65
- jointNames = await namesResponse.json();
66
- console.log(`Loaded ${jointNames.length} joint names`);
67
- }
68
- } catch (e) {
69
- console.log('Using default joint names');
70
- }
71
-
72
- // Load main buffers
73
- const buffers = await Promise.all(urls.map(url => fetch(url).then(response => response.arrayBuffer())));
74
- const v_template = new Float32Array(buffers[0]);
75
- const faces = new Uint16Array(buffers[1]);
76
- const skinWeights = new Float32Array(buffers[2]);
77
- const skinIndices = new Uint16Array(buffers[3]);
78
- const keypoints = new Float32Array(buffers[4]);
79
- const uvs = new Float32Array(buffers[5]);
80
-
81
- console.log(`Vertices: ${v_template.length / 3}, Faces: ${faces.length / 3}, Joints: ${keypoints.length / 3}`);
82
-
83
- // Create geometry
84
- const geometry = new THREE.BufferGeometry();
85
- geometry.setAttribute('position', new THREE.BufferAttribute(v_template, 3));
86
- geometry.setIndex(new THREE.BufferAttribute(faces, 1));
87
- geometry.setAttribute('skinIndex', new THREE.BufferAttribute(skinIndices, NUM_SKIN_WEIGHTS));
88
- geometry.setAttribute('skinWeight', new THREE.BufferAttribute(skinWeights, NUM_SKIN_WEIGHTS));
89
- geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));
90
-
91
- // Create bones
92
- const numJoints = keypoints.length / 3;
93
-
94
- // Ensure edges array matches joint count
95
- while (edges.length < numJoints) {
96
- edges.push(0);
97
- }
98
-
99
- // Root bone
100
- var rootBone = new THREE.Bone();
101
- rootBone.position.set(keypoints[0], keypoints[1], keypoints[2]);
102
- rootBone.name = jointNames[0] || 'Pelvis';
103
- var bones = [rootBone];
104
-
105
- // Create child bones
106
- for (let i = 1; i < numJoints; i++) {
107
- const bone = new THREE.Bone();
108
- const parentIndex = edges[i];
109
-
110
- if (parentIndex >= 0 && parentIndex < i) {
111
- bone.position.set(
112
- keypoints[3 * i] - keypoints[3 * parentIndex],
113
- keypoints[3 * i + 1] - keypoints[3 * parentIndex + 1],
114
- keypoints[3 * i + 2] - keypoints[3 * parentIndex + 2]
115
- );
116
- bone.name = jointNames[i] || `Joint_${i}`;
117
- bones.push(bone);
118
- bones[parentIndex].add(bone);
119
- console.log(`Joint ${i} (${bone.name}): parent=${parentIndex}, pos=${bone.position.toArray()}`);
120
- } else {
121
- console.warn(`Invalid parent index ${parentIndex} for joint ${i}, attaching to root`);
122
- bone.position.set(0, 0, 0);
123
- bone.name = jointNames[i] || `Joint_${i}`;
124
- bones.push(bone);
125
- bones[0].add(bone);
126
- }
127
- }
128
-
129
- var skeleton = new THREE.Skeleton(bones);
130
-
131
- geometry.computeVertexNormals();
132
-
133
- // --- Texture Loading ---
134
- const textureLoader = new THREE.TextureLoader();
135
-
136
- async function loadTextureAsync(url, isSRGB = true) {
137
- const tex = await textureLoader.loadAsync(url);
138
- tex.flipY = false;
139
- if (isSRGB) tex.colorSpace = THREE.SRGBColorSpace;
140
- return tex;
141
- }
142
-
143
- const [baseColorMap] = await Promise.all([
144
- loadTextureAsync(`${basePath}/Boy_lambert4_BaseColor.webp`, true),
145
- ]);
146
-
147
- // Create material - PBR with textures (optimized for dark mode)
148
- const material = new THREE.MeshStandardMaterial({
149
- map: baseColorMap,
150
- roughness: 0.6, // Lower roughness for better light reflection
151
- metalness: 0.2, // Lower metalness for more natural look
152
- envMapIntensity: 1.5, // Enhanced environment lighting
153
- });
154
-
155
- var mesh = new THREE.SkinnedMesh(geometry, material);
156
- mesh.castShadow = true;
157
- mesh.receiveShadow = true;
158
- mesh.add(bones[0]);
159
- mesh.bind(skeleton);
160
-
161
- console.log(`Wooden model loaded: ${numJoints} joints, ${v_template.length / 3} vertices`);
162
-
163
- return { bones, skeleton, mesh, jointNames, edges };
164
- }
165
-
166
- export { DEFAULT_EDGES, load_wooden, NUM_SKIN_WEIGHTS, SMPLH_JOINT_NAMES };
167
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/gradio/templates/element/blank.html DELETED
@@ -1,53 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
-
4
- <head>
5
- <title>{% block title %} {% endblock %}</title>
6
- <meta charset="UTF-8">
7
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
- <!-- Add Bootstrap CSS (CDN) -->
9
- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
10
- <!-- Add jQuery and Bootstrap JS (CDN) -->
11
- <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
12
- <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.10.2/dist/umd/popper.min.js"></script>
13
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.min.js"></script>
14
- <style>
15
- /* Dark mode base */
16
- html, body {
17
- background: #1a1a2e !important;
18
- color: #e2e8f0;
19
- margin: 0;
20
- padding: 0;
21
- }
22
-
23
- .container {
24
- padding: 0;
25
- border: none;
26
- background: #1a1a2e;
27
- }
28
-
29
- .alert-success {
30
- display: none;
31
- }
32
-
33
- {% block style %}
34
- {% endblock %}
35
- </style>
36
- </head>
37
-
38
- <body>
39
-
40
- {% block content_block %}
41
- {% endblock %}
42
-
43
- {% block script_block %}
44
- {% endblock %}
45
-
46
- <div class="container alert-success mt-3" role="alert">
47
- {% block help %}
48
-
49
- {% endblock %}
50
- </div>
51
- </body>
52
-
53
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/gradio/templates/error_file_not_found.html DELETED
@@ -1,64 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="zh-CN">
3
-
4
- <head>
5
- <meta charset="UTF-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>File not found - 404</title>
8
- <style>
9
- * {
10
- margin: 0;
11
- padding: 0;
12
- box-sizing: border-box;
13
- }
14
-
15
- body {
16
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
17
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
18
- min-height: 100vh;
19
- display: flex;
20
- align-items: center;
21
- justify-content: center;
22
- padding: 20px;
23
- }
24
-
25
- .container {
26
- background: white;
27
- border-radius: 12px;
28
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
29
- max-width: 500px;
30
- width: 100%;
31
- overflow: hidden;
32
- text-align: center;
33
- padding: 40px;
34
- }
35
-
36
- h1 {
37
- color: #e74c3c;
38
- font-size: 4em;
39
- margin-bottom: 20px;
40
- }
41
-
42
- h2 {
43
- color: #333;
44
- font-size: 1.5em;
45
- margin-bottom: 15px;
46
- }
47
-
48
- p {
49
- color: #666;
50
- font-size: 1.1em;
51
- line-height: 1.6;
52
- }
53
- </style>
54
- </head>
55
-
56
- <body>
57
- <div class="container">
58
- <h1>404</h1>
59
- <h2>Oops! File Not Found</h2>
60
- <p>We couldn't find the file you're looking for.<br>Please check the URL or try again later.</p>
61
- </div>
62
- </body>
63
-
64
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/gradio/templates/index_smpl_gradio.html DELETED
@@ -1,938 +0,0 @@
1
- {% extends 'element/blank.html' %}
2
-
3
- {% block content_block %}
4
-
5
- <div class="container mt-3">
6
- <!-- Caption container -->
7
- {% if not hide_captions %}
8
- <div class="caption-container">
9
- <div class="motion-info" id="motion-info">
10
- <div class="loading">
11
- <i class="fas fa-spinner fa-spin"></i> Loading action descriptions...
12
- </div>
13
- </div>
14
- </div>
15
- {% endif %}
16
-
17
- <!-- 3D container -->
18
- <div class="vis3d-wrapper" style="position: relative;">
19
- <div class="d-flex justify-content-center" id="vis3d">
20
- </div>
21
- </div>
22
-
23
- <!-- Playback control panel -->
24
- <div class="control-panel-embedded mt-3">
25
- <div class="control-row-compact">
26
- <div class="control-group">
27
- <button id="playPauseBtn" class="control-btn" title="Play/Pause">
28
- <i class="fas fa-play"></i>
29
- </button>
30
- <button id="resetBtn" class="control-btn" title="Reset to start">
31
- <i class="fas fa-step-backward"></i>
32
- </button>
33
- </div>
34
-
35
- <div class="progress-group">
36
- <input type="range" id="progressSlider" class="progress-slider" min="0" max="100" value="0">
37
- </div>
38
-
39
- <div class="info-group">
40
- <div class="frame-info">
41
- <span id="currentFrame">0</span> / <span id="totalFrames">0</span>
42
- </div>
43
- <div class="loading-status" id="loadingStatus">
44
- <i class="fas fa-spinner fa-spin"></i> Loading...
45
- </div>
46
- </div>
47
-
48
- <div class="speed-group">
49
- <label for="speedSlider">Speed:</label>
50
- <input type="range" id="speedSlider" class="speed-slider" min="0.1" max="3" step="0.1" value="1">
51
- <span id="speedValue">1.0x</span>
52
- </div>
53
- </div>
54
- </div>
55
- </div>
56
-
57
- <!-- Add Font Awesome for icons -->
58
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
59
-
60
- <script type="importmap">
61
- {
62
- "imports": {
63
- "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
64
- "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
65
- }
66
- }
67
- </script>
68
-
69
- <script type="module">
70
- import * as THREE from 'three';
71
- import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
72
- import { getChessboardXZ, getCoordinate } from '/static/scripts3d/create_ground.js';
73
- import { create_scene, fitCameraToScene } from '/static/scripts3d/create_scene.js';
74
- import { load_smpl_with_shapes } from '/static/scripts3d/load_smpl.js';
75
-
76
- let scene, camera, renderer;
77
- let controls;
78
- let infos;
79
- let currentFrame = 0;
80
- let total_frame = 0;
81
- const baseIntervalTime = 30;
82
- var model_mesh = {};
83
-
84
- let isPlaying = false;
85
- let lastFrameTime = 0;
86
- let playbackSpeed = 1.0;
87
- let animationId = null;
88
- let modelsLoaded = false;
89
- let expectedModelCount = 0;
90
- let loadedModelCount = 0;
91
-
92
- let ignoreGlobalTrans = false;
93
- let currentOffsets = [];
94
-
95
- const updateFrame = () => {
96
- if (!infos || currentFrame >= total_frame || !modelsLoaded) return;
97
-
98
- const info = infos[currentFrame];
99
- let allModelsReady = true;
100
-
101
- info.forEach(smpl_params => {
102
- if (!(smpl_params.id in model_mesh)) {
103
- allModelsReady = false;
104
- }
105
- });
106
-
107
- if (!allModelsReady) {
108
- return;
109
- }
110
-
111
- const offsets = computeOffsets(info.length);
112
- currentOffsets = offsets;
113
-
114
- info.forEach((smpl_params, b) => {
115
- const bones = model_mesh[smpl_params.id];
116
- const mesh = bones[0].parent;
117
-
118
- if (ignoreGlobalTrans) {
119
- mesh.position.set(-offsets[b], 0, 0);
120
- } else {
121
- mesh.position.set(
122
- smpl_params.Th[0][0] - offsets[b],
123
- smpl_params.Th[0][1],
124
- smpl_params.Th[0][2]
125
- );
126
- }
127
-
128
- var axis = new THREE.Vector3(smpl_params.Rh[0][0], smpl_params.Rh[0][1], smpl_params.Rh[0][2]);
129
- var angle = axis.length();
130
- axis.normalize();
131
-
132
- var poses_offset = 0;
133
- if (smpl_params.poses[0].length == 69) {
134
- poses_offset = -3;
135
- }
136
- for (let i = 0; i < bones.length; i++) {
137
- var axis = new THREE.Vector3(
138
- smpl_params.poses[0][poses_offset + 3 * i],
139
- smpl_params.poses[0][poses_offset + 3 * i + 1],
140
- smpl_params.poses[0][poses_offset + 3 * i + 2]);
141
- var angle = axis.length();
142
- axis.normalize();
143
- var quaternion = new THREE.Quaternion().setFromAxisAngle(axis, angle);
144
- bones[i].quaternion.copy(quaternion);
145
- }
146
- });
147
-
148
- updateUI();
149
- }
150
-
151
- const playLoop = (currentTime) => {
152
- if (isPlaying && currentTime - lastFrameTime >= (baseIntervalTime / playbackSpeed)) {
153
- currentFrame += 1;
154
- if (currentFrame >= total_frame) {
155
- currentFrame = 0;
156
- }
157
- updateFrame();
158
- lastFrameTime = currentTime;
159
- }
160
-
161
- if (isPlaying) {
162
- animationId = requestAnimationFrame(playLoop);
163
- }
164
- }
165
-
166
- const updateUI = () => {
167
- document.getElementById('currentFrame').textContent = currentFrame;
168
- document.getElementById('totalFrames').textContent = total_frame;
169
-
170
- if (total_frame > 0) {
171
- const progress = (currentFrame / total_frame) * 100;
172
- document.getElementById('progressSlider').value = progress;
173
- }
174
- }
175
-
176
- const updateLoadingStatus = () => {
177
- const loadingElement = document.getElementById('loadingStatus');
178
- if (!loadingElement) return;
179
-
180
- if (modelsLoaded) {
181
- loadingElement.innerHTML = '<i class="fas fa-check"></i> Model loaded';
182
- loadingElement.className = 'loading-status complete';
183
- setTimeout(() => {
184
- loadingElement.className = 'loading-status hidden';
185
- }, 3000);
186
- } else {
187
- loadingElement.innerHTML = `<i class="fas fa-spinner fa-spin"></i> Loading models... (${loadedModelCount}/${expectedModelCount})`;
188
- loadingElement.className = 'loading-status';
189
- }
190
- }
191
-
192
- const updatePlayPauseButton = () => {
193
- const playPauseBtn = document.getElementById('playPauseBtn');
194
- if (playPauseBtn) {
195
- if (isPlaying) {
196
- playPauseBtn.innerHTML = '<i class="fas fa-pause"></i>';
197
- playPauseBtn.title = 'Pause';
198
- } else {
199
- playPauseBtn.innerHTML = '<i class="fas fa-play"></i>';
200
- playPauseBtn.title = 'Play';
201
- }
202
- }
203
- }
204
-
205
- const enablePlaybackControls = () => {
206
- const playPauseBtn = document.getElementById('playPauseBtn');
207
- const resetBtn = document.getElementById('resetBtn');
208
- const progressSlider = document.getElementById('progressSlider');
209
- const speedSlider = document.getElementById('speedSlider');
210
-
211
- [playPauseBtn, resetBtn, progressSlider, speedSlider].forEach(element => {
212
- if (element) {
213
- element.disabled = false;
214
- element.style.opacity = '1';
215
- element.style.cursor = 'pointer';
216
- }
217
- });
218
-
219
- updatePlayPauseButton();
220
- }
221
-
222
- const playAnimation = () => {
223
- if (!isPlaying && total_frame > 0 && modelsLoaded) {
224
- isPlaying = true;
225
- lastFrameTime = performance.now();
226
- animationId = requestAnimationFrame(playLoop);
227
- updatePlayPauseButton();
228
- }
229
- }
230
-
231
- const pauseAnimation = () => {
232
- isPlaying = false;
233
- if (animationId) {
234
- cancelAnimationFrame(animationId);
235
- animationId = null;
236
- }
237
- updatePlayPauseButton();
238
- }
239
-
240
- const resetAnimation = () => {
241
- pauseAnimation();
242
- currentFrame = 0;
243
- updateFrame();
244
- updatePlayPauseButton();
245
- }
246
-
247
- const initPlaybackControls = () => {
248
- const playPauseBtn = document.getElementById('playPauseBtn');
249
- const resetBtn = document.getElementById('resetBtn');
250
- const progressSlider = document.getElementById('progressSlider');
251
- const speedSlider = document.getElementById('speedSlider');
252
- const speedValue = document.getElementById('speedValue');
253
-
254
- [playPauseBtn, resetBtn, progressSlider, speedSlider].forEach(element => {
255
- if (element) {
256
- element.disabled = true;
257
- element.style.opacity = '0.5';
258
- element.style.cursor = 'not-allowed';
259
- }
260
- });
261
-
262
- updatePlayPauseButton();
263
-
264
- playPauseBtn.addEventListener('click', () => {
265
- if (!modelsLoaded) return;
266
- if (isPlaying) {
267
- pauseAnimation();
268
- } else {
269
- playAnimation();
270
- }
271
- });
272
-
273
- resetBtn.addEventListener('click', () => {
274
- if (!modelsLoaded) return;
275
- resetAnimation();
276
- });
277
-
278
- let wasPlaying = false;
279
- progressSlider.addEventListener('mousedown', () => {
280
- if (!modelsLoaded) return;
281
- wasPlaying = isPlaying;
282
- if (isPlaying) pauseAnimation();
283
- });
284
-
285
- progressSlider.addEventListener('input', (e) => {
286
- if (!modelsLoaded) return;
287
- const progress = parseFloat(e.target.value);
288
- currentFrame = Math.floor((progress / 100) * total_frame);
289
- if (currentFrame >= total_frame) currentFrame = total_frame - 1;
290
- if (currentFrame < 0) currentFrame = 0;
291
- updateFrame();
292
- });
293
-
294
- progressSlider.addEventListener('mouseup', () => {
295
- if (!modelsLoaded) return;
296
- if (wasPlaying) playAnimation();
297
- });
298
-
299
- speedSlider.addEventListener('input', (e) => {
300
- playbackSpeed = parseFloat(e.target.value);
301
- speedValue.textContent = playbackSpeed.toFixed(1) + 'x';
302
- });
303
-
304
- document.addEventListener('keydown', (e) => {
305
- if (!modelsLoaded) return;
306
- switch (e.code) {
307
- case 'Space':
308
- e.preventDefault();
309
- if (isPlaying) {
310
- pauseAnimation();
311
- } else {
312
- playAnimation();
313
- }
314
- break;
315
- case 'ArrowLeft':
316
- e.preventDefault();
317
- if (currentFrame > 0) {
318
- currentFrame--;
319
- updateFrame();
320
- }
321
- break;
322
- case 'ArrowRight':
323
- e.preventDefault();
324
- if (currentFrame < total_frame - 1) {
325
- currentFrame++;
326
- updateFrame();
327
- }
328
- break;
329
- case 'Home':
330
- e.preventDefault();
331
- resetAnimation();
332
- break;
333
- }
334
- });
335
- }
336
-
337
- async function waitAndFetchData() {
338
- try {
339
- const waitResponse = await fetch('/wait_for_data');
340
- const waitResult = await waitResponse.json();
341
-
342
- if (waitResult.status === 'ready') {
343
- console.log(`Data ready with ${waitResult.frames} frames`);
344
- fetchSMPLData();
345
- } else {
346
- console.log('Timeout waiting for data, trying direct fetch...');
347
- fetchSMPLData();
348
- }
349
- } catch (error) {
350
- console.error('Error waiting for data:', error);
351
- fetchSMPLData();
352
- }
353
- }
354
-
355
- waitAndFetchData();
356
-
357
- function fetchSMPLData() {
358
- fetch('/query_smpl/{{ folder_name }}/{{ file_name }}')
359
- .then(response => response.json())
360
- .then(async datas => {
361
- if (!datas || datas.length === 0) {
362
- console.log('No data received, retrying in 2 seconds...');
363
- setTimeout(fetchSMPLData, 2000);
364
- return;
365
- }
366
-
367
- console.log(`Received ${datas.length} frames of SMPL data`);
368
- infos = datas;
369
- total_frame = datas.length;
370
-
371
- updateGroundWithData(datas);
372
- document.getElementById('progressSlider').max = 100;
373
- updateUI();
374
- updatePlayPauseButton();
375
-
376
- expectedModelCount = infos[0].length;
377
-
378
- loadedModelCount = 0;
379
- modelsLoaded = false;
380
- updateLoadingStatus();
381
-
382
- infos[0].forEach(data => {
383
- load_smpl_with_shapes(data.shapes[0], data.gender).then(result => {
384
- scene.add(result.mesh);
385
-
386
- const skeletonHelper = new THREE.SkeletonHelper(result.mesh);
387
- scene.add(skeletonHelper);
388
-
389
- model_mesh[data.id] = result.bones;
390
-
391
- loadedModelCount++;
392
-
393
- if (loadedModelCount === expectedModelCount) {
394
- modelsLoaded = true;
395
- updateLoadingStatus();
396
- updateFrame();
397
- enablePlaybackControls();
398
- fitCameraToScene(scene, camera, controls, { axis_up: 'y', excludeNames: ['ground'] });
399
- setTimeout(() => playAnimation(), 500);
400
- } else {
401
- updateLoadingStatus();
402
- }
403
- }).catch(err => {
404
- console.error("Failed to load SMPL model:", err);
405
- });
406
- });
407
-
408
- initPlaybackControls();
409
- animate();
410
- });
411
- }
412
-
413
- function updateGroundWithData(smplData) {
414
- const sampleData = smplData.map(frame => {
415
- const offsets = computeOffsets(frame.length);
416
- return {
417
- positions: frame.map((person, b) => ({
418
- x: person.Th[0][0] - offsets[b],
419
- y: person.Th[0][1],
420
- z: person.Th[0][2]
421
- }))
422
- };
423
- });
424
-
425
- const objectsToRemove = [];
426
- scene.traverse((child) => {
427
- if (child.isMesh && child.geometry &&
428
- (child.geometry.type === 'PlaneGeometry' || child.name === 'ground')) {
429
- objectsToRemove.push(child);
430
- }
431
- });
432
-
433
- objectsToRemove.forEach(obj => {
434
- scene.remove(obj);
435
- if (obj.geometry) obj.geometry.dispose();
436
- if (obj.material) {
437
- if (obj.material.map) obj.material.map.dispose();
438
- obj.material.dispose();
439
- }
440
- });
441
-
442
- const adaptiveGround = getChessboardXZ(5, 10, '#ffffff', '#444444', 1024, sampleData);
443
- adaptiveGround.name = 'ground';
444
- scene.add(adaptiveGround);
445
- }
446
-
447
- init();
448
-
449
- function init() {
450
- const width = document.querySelector('.container').offsetWidth;
451
- const height = width * 13 / 16;
452
- scene = new THREE.Scene();
453
- camera = new THREE.PerspectiveCamera(60, width / height, 0.01, 100);
454
- renderer = new THREE.WebGLRenderer({ antialias: true, logarithmicDepthBuffer: true });
455
-
456
- create_scene(scene, camera, renderer, true, 'y', 'z');
457
- renderer.setPixelRatio(window.devicePixelRatio);
458
- renderer.setSize(width, height);
459
- var container = document.getElementById('vis3d');
460
- container.appendChild(renderer.domElement);
461
-
462
- controls = new OrbitControls(camera, renderer.domElement);
463
- controls.minDistance = 1;
464
- controls.maxDistance = 10;
465
- fitCameraToScene(scene, camera, controls, { axis_up: 'y', excludeNames: ['ground'] });
466
- }
467
-
468
- function animate() {
469
- requestAnimationFrame(animate);
470
- renderer.render(scene, camera);
471
- }
472
-
473
- function computeOffsets(batchSize) {
474
- const spacing = 2.0;
475
- const total_width = (batchSize - 1) * spacing;
476
- const start_x = -total_width / 2;
477
- const offsets = [];
478
- for (let i = 0; i < batchSize; i++) {
479
- offsets.push(start_x + i * spacing);
480
- }
481
- return offsets;
482
- }
483
-
484
- </script>
485
-
486
- <style>
487
- /* Navigation Controls */
488
- .nav-controls {
489
- position: absolute;
490
- top: 10px;
491
- left: 10px;
492
- display: flex;
493
- gap: 10px;
494
- z-index: 1000;
495
- }
496
-
497
- .nav-btn {
498
- background: rgba(102, 126, 234, 0.9);
499
- color: white;
500
- border: none;
501
- border-radius: 6px;
502
- padding: 8px 12px;
503
- font-size: 12px;
504
- cursor: pointer;
505
- transition: all 0.3s ease;
506
- display: flex;
507
- align-items: center;
508
- gap: 4px;
509
- }
510
-
511
- .nav-btn:hover {
512
- background: rgba(102, 126, 234, 1);
513
- transform: translateY(-1px);
514
- }
515
-
516
- .container {
517
- position: relative;
518
- }
519
-
520
- /* Control Panel Styles */
521
- .control-panel-embedded {
522
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
523
- border-radius: 12px;
524
- padding: 15px;
525
- color: white;
526
- margin-top: 15px;
527
- box-shadow: 0 4px 15px rgba(102, 126, 234, 0.2);
528
- }
529
-
530
- .control-row-compact {
531
- display: flex;
532
- align-items: center;
533
- justify-content: space-between;
534
- gap: 15px;
535
- margin-bottom: 0;
536
- }
537
-
538
- .control-group {
539
- display: flex;
540
- gap: 10px;
541
- flex-shrink: 0;
542
- }
543
-
544
- .progress-group {
545
- flex: 1;
546
- margin: 0 15px;
547
- }
548
-
549
- .info-group {
550
- display: flex;
551
- align-items: center;
552
- gap: 10px;
553
- flex-shrink: 0;
554
- }
555
-
556
- .speed-group {
557
- display: flex;
558
- align-items: center;
559
- gap: 8px;
560
- flex-shrink: 0;
561
- }
562
-
563
- .control-btn {
564
- background: rgba(255, 255, 255, 0.2);
565
- border: 2px solid rgba(255, 255, 255, 0.3);
566
- border-radius: 50px;
567
- width: 50px;
568
- height: 50px;
569
- color: white;
570
- font-size: 18px;
571
- cursor: pointer;
572
- transition: all 0.3s ease;
573
- display: flex;
574
- align-items: center;
575
- justify-content: center;
576
- }
577
-
578
- .control-btn:hover {
579
- background: rgba(255, 255, 255, 0.3);
580
- border-color: rgba(255, 255, 255, 0.6);
581
- transform: scale(1.1);
582
- }
583
-
584
- .control-btn:active {
585
- transform: scale(0.95);
586
- }
587
-
588
- .control-btn:disabled,
589
- .progress-slider:disabled,
590
- .speed-slider:disabled {
591
- pointer-events: none;
592
- opacity: 0.5 !important;
593
- cursor: not-allowed !important;
594
- }
595
-
596
- .frame-info {
597
- background: rgba(255, 255, 255, 0.2);
598
- padding: 6px 12px;
599
- border-radius: 20px;
600
- font-family: 'Courier New', monospace;
601
- font-weight: bold;
602
- font-size: 14px;
603
- }
604
-
605
- .loading-status {
606
- background: rgba(255, 255, 255, 0.2);
607
- padding: 6px 12px;
608
- border-radius: 20px;
609
- font-size: 12px;
610
- color: #ffeb3b;
611
- display: flex;
612
- align-items: center;
613
- gap: 6px;
614
- }
615
-
616
- .loading-status.hidden {
617
- display: none;
618
- }
619
-
620
- .loading-status.complete {
621
- color: #4caf50;
622
- }
623
-
624
- .progress-slider {
625
- width: 100%;
626
- height: 6px;
627
- border-radius: 3px;
628
- background: rgba(255, 255, 255, 0.3);
629
- outline: none;
630
- cursor: pointer;
631
- }
632
-
633
- .progress-slider::-webkit-slider-thumb {
634
- appearance: none;
635
- width: 20px;
636
- height: 20px;
637
- border-radius: 50%;
638
- background: white;
639
- cursor: pointer;
640
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
641
- }
642
-
643
- .progress-slider::-moz-range-thumb {
644
- width: 20px;
645
- height: 20px;
646
- border-radius: 50%;
647
- background: white;
648
- cursor: pointer;
649
- border: none;
650
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
651
- }
652
-
653
- .speed-slider {
654
- width: 80px;
655
- height: 4px;
656
- border-radius: 2px;
657
- background: rgba(255, 255, 255, 0.3);
658
- outline: none;
659
- cursor: pointer;
660
- }
661
-
662
- .speed-slider::-webkit-slider-thumb {
663
- appearance: none;
664
- width: 16px;
665
- height: 16px;
666
- border-radius: 50%;
667
- background: white;
668
- cursor: pointer;
669
- }
670
-
671
- .speed-slider::-moz-range-thumb {
672
- width: 16px;
673
- height: 16px;
674
- border-radius: 50%;
675
- background: white;
676
- cursor: pointer;
677
- border: none;
678
- }
679
-
680
- #speedValue {
681
- font-family: 'Courier New', monospace;
682
- font-weight: bold;
683
- min-width: 40px;
684
- }
685
-
686
- /* Responsive design */
687
- @media (max-width: 768px) {
688
- .control-row-compact {
689
- flex-direction: column;
690
- gap: 10px;
691
- }
692
-
693
- .progress-group {
694
- margin: 10px 0;
695
- }
696
-
697
- .info-group,
698
- .speed-group {
699
- justify-content: center;
700
- }
701
- }
702
-
703
- /* Original styles */
704
- .caption-container {
705
- margin-top: 10px;
706
- margin-bottom: 10px;
707
- width: 100%;
708
- }
709
-
710
- .motion-info {
711
- background-color: #ffffff;
712
- border-radius: 10px;
713
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
714
- overflow: hidden;
715
- }
716
-
717
- .loading {
718
- padding: 20px;
719
- text-align: center;
720
- color: #666;
721
- font-style: italic;
722
- }
723
-
724
- .file-info {
725
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
726
- color: white;
727
- padding: 20px;
728
- }
729
-
730
- .file-info h4 {
731
- margin: 0 0 10px 0;
732
- font-size: 1.2em;
733
- font-weight: 600;
734
- }
735
-
736
- .file-detail {
737
- margin: 5px 0;
738
- font-size: 0.9em;
739
- opacity: 0.9;
740
- }
741
-
742
- .captions-section {
743
- padding: 20px;
744
- }
745
-
746
- .captions-title {
747
- margin: 0 0 15px 0;
748
- color: #333;
749
- font-size: 1.1em;
750
- font-weight: 600;
751
- border-bottom: 2px solid #667eea;
752
- padding-bottom: 8px;
753
- }
754
-
755
- .caption-item {
756
- background-color: #f8f9ff;
757
- border: 1px solid #e1e5f7;
758
- border-radius: 8px;
759
- margin-bottom: 15px;
760
- padding: 15px;
761
- transition: all 0.3s ease;
762
- }
763
-
764
- .caption-item:hover {
765
- transform: translateY(-2px);
766
- box-shadow: 0 4px 15px rgba(102, 126, 234, 0.15);
767
- }
768
-
769
- .caption-content {
770
- margin-bottom: 10px;
771
- }
772
-
773
- .no-captions {
774
- text-align: center;
775
- padding: 30px;
776
- color: #666;
777
- font-style: italic;
778
- }
779
-
780
- .error-message {
781
- background-color: #fee;
782
- color: #c33;
783
- padding: 15px;
784
- border-radius: 5px;
785
- text-align: center;
786
- }
787
-
788
- .caption-rewritten {
789
- font-size: 1em;
790
- font-weight: 600;
791
- color: #333;
792
- margin-bottom: 5px;
793
- }
794
-
795
- .caption-original-toggle {
796
- font-size: 0.85em;
797
- color: #667eea;
798
- cursor: pointer;
799
- display: inline-flex;
800
- align-items: center;
801
- gap: 4px;
802
- padding: 4px 0;
803
- user-select: none;
804
- transition: color 0.2s;
805
- }
806
-
807
- .caption-original-toggle:hover {
808
- color: #764ba2;
809
- }
810
-
811
- .caption-original-toggle i {
812
- transition: transform 0.2s;
813
- }
814
-
815
- .caption-original-toggle.expanded i {
816
- transform: rotate(180deg);
817
- }
818
-
819
- .caption-original {
820
- display: none !important;
821
- font-size: 0.9em;
822
- color: #666;
823
- background: #f0f0f5;
824
- padding: 8px 12px;
825
- border-radius: 6px;
826
- margin-top: 6px;
827
- line-height: 1.4;
828
- }
829
-
830
- .caption-original.show {
831
- display: block !important;
832
- }
833
-
834
- .original-label {
835
- font-weight: 600;
836
- color: #888;
837
- }
838
- </style>
839
-
840
-
841
- <!-- Only load caption fetching script if captions are not hidden -->
842
- {% if not hide_captions %}
843
- <script>
844
- function createCaptionItem(caption, index) {
845
- const hasOriginal = caption['short caption'];
846
- const rewritten = caption['short caption+'] || caption['short caption'] || 'No caption';
847
-
848
- return `
849
- <div class="caption-item">
850
- <div class="caption-content">
851
- <div class="caption-rewritten">${rewritten}</div>
852
- ${hasOriginal && caption['short caption+'] ? `
853
- <div class="caption-original-toggle" data-toggle="caption-original">
854
- <i class="fas fa-chevron-down"></i> Show original text
855
- </div>
856
- <div class="caption-original">
857
- <span class="original-label">Original:</span> ${caption['short caption']}
858
- </div>
859
- ` : ''}
860
- </div>
861
- </div>
862
- `;
863
- }
864
-
865
- document.addEventListener('click', (e) => {
866
- const toggle = e.target.closest('[data-toggle="caption-original"]');
867
- if (toggle) {
868
- const originalDiv = toggle.nextElementSibling;
869
- if (originalDiv && originalDiv.classList.contains('caption-original')) {
870
- originalDiv.classList.toggle('show');
871
- toggle.classList.toggle('expanded');
872
- }
873
- }
874
- });
875
-
876
- function fetchMotionInfo() {
877
- fetch('/query_caption/{{ folder_name }}/{{ file_name }}')
878
- .then(response => response.json())
879
- .then(data => {
880
- const motionInfoElement = document.getElementById('motion-info');
881
-
882
- if (data && (data.filename || data.motion_path || data.result)) {
883
- let html = '';
884
-
885
- if (data.filename || data.motion_path) {
886
- html += `
887
- <div class="file-info">
888
- <h4>File Information</h4>
889
- ${data.filename ? `<div class="file-detail"><strong>Filename:</strong> ${data.filename}</div>` : ''}
890
- ${data.motion_path ? `<div class="file-detail"><strong>Motion Path:</strong> ${data.motion_path}</div>` : ''}
891
- </div>
892
- `;
893
- }
894
-
895
- if (data.result && Array.isArray(data.result) && data.result.length > 0) {
896
- html += `
897
- <div class="captions-section">
898
- <h5 class="captions-title">Motion Captions (${data.result.length})</h5>
899
- ${data.result.map((caption, index) => createCaptionItem(caption, index)).join('')}
900
- </div>
901
- `;
902
- } else if (data.result) {
903
- html += `
904
- <div class="captions-section">
905
- <div class="no-captions">No captions available for this motion</div>
906
- </div>
907
- `;
908
- }
909
-
910
- if (!html && data.caption) {
911
- html = `
912
- <div class="captions-section">
913
- <h5 class="captions-title">Caption</h5>
914
- <div class="caption-item">
915
- <div class="caption-content">
916
- <div class="caption-short">${data.caption}</div>
917
- </div>
918
- </div>
919
- </div>
920
- `;
921
- }
922
-
923
- motionInfoElement.innerHTML = html || '<div class="no-captions">No motion information available</div>';
924
- } else {
925
- motionInfoElement.innerHTML = '<div class="no-captions">No motion information available</div>';
926
- }
927
- })
928
- .catch(error => {
929
- console.error('Error fetching motion information:', error);
930
- document.getElementById('motion-info').innerHTML = '<div class="error-message">Error loading motion information</div>';
931
- });
932
- }
933
-
934
- document.addEventListener('DOMContentLoaded', fetchMotionInfo);
935
- </script>
936
- {% endif %}
937
-
938
- {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/gradio/templates/index_wooden_gradio.html DELETED
@@ -1,1033 +0,0 @@
1
- {% extends 'element/blank.html' %}
2
-
3
- {% block content_block %}
4
-
5
- <!-- Fullscreen 3D container -->
6
- <div class="fullscreen-container">
7
- <!-- 3D viewport -->
8
- <div id="vis3d"></div>
9
-
10
- <!-- Floating caption overlay (centered at top) -->
11
- {% if not hide_captions %}
12
- <div class="caption-overlay">
13
- <div class="motion-info" id="motion-info">
14
- <div class="loading">
15
- <i class="fas fa-spinner fa-spin"></i> Loading action descriptions...
16
- </div>
17
- </div>
18
- </div>
19
- {% endif %}
20
-
21
- <!-- Floating progress control panel (centered at bottom) -->
22
- <div class="control-overlay">
23
- <div class="control-row-minimal">
24
- <div class="progress-container">
25
- <input type="range" id="progressSlider" class="progress-slider-minimal" min="0" max="100" value="0">
26
- </div>
27
- <div class="frame-counter">
28
- <span id="currentFrame">0</span> / <span id="totalFrames">0</span>
29
- </div>
30
- </div>
31
- </div>
32
-
33
- <!-- Loading status overlay -->
34
- <div class="loading-overlay" id="loadingStatus">
35
- <i class="fas fa-spinner fa-spin"></i> Loading...
36
- </div>
37
-
38
- <!-- Hidden controls for functionality -->
39
- <div style="display: none;">
40
- <button id="playPauseBtn"></button>
41
- <button id="resetBtn"></button>
42
- <input type="range" id="speedSlider" min="0.1" max="3" step="0.1" value="1">
43
- <span id="speedValue">1.0x</span>
44
- </div>
45
- </div>
46
-
47
- <!-- Add Font Awesome for icons -->
48
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
49
-
50
- <script type="importmap">
51
- {
52
- "imports": {
53
- "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
54
- "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
55
- }
56
- }
57
- </script>
58
-
59
- <script type="module">
60
- import * as THREE from 'three';
61
- import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
62
- import { getChessboardXZ } from '/static/scripts3d/create_ground.js';
63
- import { create_scene, fitCameraToScene } from '/static/scripts3d/create_scene.js';
64
- import { load_wooden, SMPLH_JOINT_NAMES } from '/static/scripts3d/load_wooden.js';
65
-
66
- let scene, camera, renderer;
67
- let controls;
68
- let infos;
69
- let currentFrame = 0;
70
- let total_frame = 0;
71
- const baseIntervalTime = 30;
72
- var model_mesh = {};
73
-
74
- let isPlaying = false;
75
- let lastFrameTime = 0;
76
- let playbackSpeed = 1.0;
77
- let animationId = null;
78
- let modelsLoaded = false;
79
- let expectedModelCount = 0;
80
- let loadedModelCount = 0;
81
-
82
- let ignoreGlobalTrans = false;
83
- let currentOffsets = [];
84
-
85
- const updateFrame = () => {
86
- if (!infos || currentFrame >= total_frame || !modelsLoaded) return;
87
-
88
- const info = infos[currentFrame];
89
- let allModelsReady = true;
90
-
91
- info.forEach(smpl_params => {
92
- if (!(smpl_params.id in model_mesh)) {
93
- allModelsReady = false;
94
- }
95
- });
96
-
97
- if (!allModelsReady) {
98
- return;
99
- }
100
-
101
- const offsets = computeOffsets(info.length);
102
- currentOffsets = offsets;
103
-
104
- info.forEach((smpl_params, b) => {
105
- const bones = model_mesh[smpl_params.id];
106
- const meshContainer = bones[0].parent;
107
-
108
- if (ignoreGlobalTrans) {
109
- meshContainer.position.set(-offsets[b], 0, 0);
110
- } else {
111
- meshContainer.position.set(
112
- smpl_params.Th[0][0] - offsets[b],
113
- smpl_params.Th[0][1],
114
- smpl_params.Th[0][2]
115
- );
116
- }
117
-
118
- var axis = new THREE.Vector3(smpl_params.Rh[0][0], smpl_params.Rh[0][1], smpl_params.Rh[0][2]);
119
- var angle = axis.length();
120
- axis.normalize();
121
- var quaternion = new THREE.Quaternion().setFromAxisAngle(axis, angle);
122
- bones[0].quaternion.copy(quaternion);
123
-
124
- var poses_offset = 0;
125
-
126
- if (smpl_params.poses[0].length == 69) {
127
- poses_offset = -3;
128
- }
129
-
130
- for (let i = 1; i < bones.length; i++) {
131
- const startIndex = poses_offset + 3 * i;
132
-
133
- if (startIndex + 2 < smpl_params.poses[0].length) {
134
- var axis = new THREE.Vector3(
135
- smpl_params.poses[0][startIndex],
136
- smpl_params.poses[0][startIndex + 1],
137
- smpl_params.poses[0][startIndex + 2]
138
- );
139
- var angle = axis.length();
140
-
141
- if (angle > 1e-6) {
142
- axis.normalize();
143
- var quaternion = new THREE.Quaternion().setFromAxisAngle(axis, angle);
144
- bones[i].quaternion.copy(quaternion);
145
- } else {
146
- bones[i].quaternion.set(0, 0, 0, 1);
147
- }
148
- }
149
- }
150
- });
151
-
152
- updateUI();
153
- }
154
-
155
- const playLoop = (currentTime) => {
156
- if (isPlaying && currentTime - lastFrameTime >= (baseIntervalTime / playbackSpeed)) {
157
- currentFrame += 1;
158
- if (currentFrame >= total_frame) {
159
- currentFrame = 0;
160
- }
161
- updateFrame();
162
- lastFrameTime = currentTime;
163
- }
164
-
165
- if (isPlaying) {
166
- animationId = requestAnimationFrame(playLoop);
167
- }
168
- }
169
-
170
- const updateUI = () => {
171
- document.getElementById('currentFrame').textContent = currentFrame;
172
- document.getElementById('totalFrames').textContent = total_frame;
173
-
174
- if (total_frame > 0) {
175
- const progress = (currentFrame / total_frame) * 100;
176
- document.getElementById('progressSlider').value = progress;
177
- }
178
- }
179
-
180
- const updateLoadingStatus = () => {
181
- const loadingElement = document.getElementById('loadingStatus');
182
- if (!loadingElement) return;
183
-
184
- if (modelsLoaded) {
185
- loadingElement.innerHTML = '<i class="fas fa-check"></i> Ready';
186
- loadingElement.className = 'loading-overlay complete';
187
- setTimeout(() => {
188
- loadingElement.className = 'loading-overlay hidden';
189
- }, 1500);
190
- } else {
191
- loadingElement.innerHTML = `<i class="fas fa-spinner fa-spin"></i> Loading... (${loadedModelCount}/${expectedModelCount})`;
192
- loadingElement.className = 'loading-overlay';
193
- }
194
- }
195
-
196
- const updatePlayPauseButton = () => {
197
- const playPauseBtn = document.getElementById('playPauseBtn');
198
- if (playPauseBtn) {
199
- if (isPlaying) {
200
- playPauseBtn.innerHTML = '<i class="fas fa-pause"></i>';
201
- playPauseBtn.title = 'Pause';
202
- } else {
203
- playPauseBtn.innerHTML = '<i class="fas fa-play"></i>';
204
- playPauseBtn.title = 'Play';
205
- }
206
- }
207
- }
208
-
209
- const enablePlaybackControls = () => {
210
- const playPauseBtn = document.getElementById('playPauseBtn');
211
- const resetBtn = document.getElementById('resetBtn');
212
- const progressSlider = document.getElementById('progressSlider');
213
- const speedSlider = document.getElementById('speedSlider');
214
-
215
- [playPauseBtn, resetBtn, progressSlider, speedSlider].forEach(element => {
216
- if (element) {
217
- element.disabled = false;
218
- element.style.opacity = '1';
219
- element.style.cursor = 'pointer';
220
- }
221
- });
222
-
223
- updatePlayPauseButton();
224
- }
225
-
226
- const playAnimation = () => {
227
- if (!isPlaying && total_frame > 0 && modelsLoaded) {
228
- isPlaying = true;
229
- lastFrameTime = performance.now();
230
- animationId = requestAnimationFrame(playLoop);
231
- updatePlayPauseButton();
232
- }
233
- }
234
-
235
- const pauseAnimation = () => {
236
- isPlaying = false;
237
- if (animationId) {
238
- cancelAnimationFrame(animationId);
239
- animationId = null;
240
- }
241
- updatePlayPauseButton();
242
- }
243
-
244
- const resetAnimation = () => {
245
- pauseAnimation();
246
- currentFrame = 0;
247
- updateFrame();
248
- updatePlayPauseButton();
249
- }
250
-
251
- const initPlaybackControls = () => {
252
- const progressSlider = document.getElementById('progressSlider');
253
-
254
- // Progress slider controls
255
- let wasPlaying = false;
256
- progressSlider.addEventListener('mousedown', () => {
257
- if (!modelsLoaded) return;
258
- wasPlaying = isPlaying;
259
- if (isPlaying) pauseAnimation();
260
- });
261
-
262
- progressSlider.addEventListener('input', (e) => {
263
- if (!modelsLoaded) return;
264
- const progress = parseFloat(e.target.value);
265
- currentFrame = Math.floor((progress / 100) * total_frame);
266
- if (currentFrame >= total_frame) currentFrame = total_frame - 1;
267
- if (currentFrame < 0) currentFrame = 0;
268
- updateFrame();
269
- });
270
-
271
- progressSlider.addEventListener('mouseup', () => {
272
- if (!modelsLoaded) return;
273
- if (wasPlaying) playAnimation();
274
- });
275
-
276
- // Touch support for mobile
277
- progressSlider.addEventListener('touchstart', () => {
278
- if (!modelsLoaded) return;
279
- wasPlaying = isPlaying;
280
- if (isPlaying) pauseAnimation();
281
- });
282
-
283
- progressSlider.addEventListener('touchend', () => {
284
- if (!modelsLoaded) return;
285
- if (wasPlaying) playAnimation();
286
- });
287
-
288
- speedSlider.addEventListener('input', (e) => {
289
- playbackSpeed = parseFloat(e.target.value);
290
- speedValue.textContent = playbackSpeed.toFixed(1) + 'x';
291
- });
292
-
293
- document.addEventListener('keydown', (e) => {
294
- if (!modelsLoaded) return;
295
- switch (e.code) {
296
- case 'Space':
297
- e.preventDefault();
298
- if (isPlaying) {
299
- pauseAnimation();
300
- } else {
301
- playAnimation();
302
- }
303
- break;
304
- case 'ArrowLeft':
305
- e.preventDefault();
306
- if (currentFrame > 0) {
307
- currentFrame--;
308
- updateFrame();
309
- }
310
- break;
311
- case 'ArrowRight':
312
- e.preventDefault();
313
- if (currentFrame < total_frame - 1) {
314
- currentFrame++;
315
- updateFrame();
316
- }
317
- break;
318
- case 'Home':
319
- e.preventDefault();
320
- resetAnimation();
321
- break;
322
- }
323
- });
324
- }
325
-
326
- async function waitAndFetchData() {
327
- try {
328
- const waitResponse = await fetch('/wait_for_data');
329
- const waitResult = await waitResponse.json();
330
-
331
- if (waitResult.status === 'ready') {
332
- console.log(`Data ready with ${waitResult.frames} frames`);
333
- fetchSMPLData();
334
- } else {
335
- console.log('Timeout waiting for data, trying direct fetch...');
336
- fetchSMPLData();
337
- }
338
- } catch (error) {
339
- console.error('Error waiting for data:', error);
340
- fetchSMPLData();
341
- }
342
- }
343
-
344
- waitAndFetchData();
345
-
346
- function fetchSMPLData() {
347
- fetch('/query_smpl/{{ folder_name }}/{{ file_name }}')
348
- .then(response => response.json())
349
- .then(async datas => {
350
- if (!datas || datas.length === 0) {
351
- console.log('No data received, retrying in 2 seconds...');
352
- setTimeout(fetchSMPLData, 2000);
353
- return;
354
- }
355
-
356
- console.log(`Received ${datas.length} frames of SMPL data`);
357
- infos = datas;
358
- total_frame = datas.length;
359
-
360
- // updateGroundWithData(datas);
361
- document.getElementById('progressSlider').max = 100;
362
- updateUI();
363
- updatePlayPauseButton();
364
-
365
- expectedModelCount = infos[0].length;
366
-
367
- loadedModelCount = 0;
368
- modelsLoaded = false;
369
- updateLoadingStatus();
370
-
371
- infos[0].forEach(data => {
372
- load_wooden(null, null).then(result => {
373
- scene.add(result.mesh);
374
-
375
- result.mesh.castShadow = true;
376
- result.mesh.receiveShadow = true;
377
-
378
- model_mesh[data.id] = result.bones;
379
-
380
- loadedModelCount++;
381
-
382
- if (loadedModelCount === expectedModelCount) {
383
- modelsLoaded = true;
384
- updateLoadingStatus();
385
- updateFrame();
386
- enablePlaybackControls();
387
- fitCameraToScene(scene, camera, controls, { axis_up: 'y', excludeNames: ['ground'] });
388
- setTimeout(() => playAnimation(), 500);
389
- } else {
390
- updateLoadingStatus();
391
- }
392
- }).catch(err => {
393
- console.error("Failed to load wooden model:", err);
394
- });
395
- });
396
-
397
- initPlaybackControls();
398
- animate();
399
- });
400
- }
401
-
402
- function updateGroundWithData(smplData) {
403
- // const sampleData = smplData.map(frame => {
404
- // const offsets = computeOffsets(frame.length);
405
- // return {
406
- // positions: frame.map((person, b) => ({
407
- // x: person.Th[0][0] - offsets[b],
408
- // y: person.Th[0][1],
409
- // z: person.Th[0][2]
410
- // }))
411
- // };
412
- // });
413
-
414
- const objectsToRemove = [];
415
- // scene.traverse((child) => {
416
- // if (child.isMesh && child.geometry &&
417
- // (child.geometry.type === 'PlaneGeometry' || child.name === 'ground')) {
418
- // objectsToRemove.push(child);
419
- // }
420
- // });
421
-
422
- objectsToRemove.forEach(obj => {
423
- scene.remove(obj);
424
- if (obj.geometry) obj.geometry.dispose();
425
- if (obj.material) {
426
- if (obj.material.map) obj.material.map.dispose();
427
- obj.material.dispose();
428
- }
429
- });
430
-
431
- // Larger grid (50x50), darker colors for dark mode scene
432
- // const adaptiveGround = getChessboardXZ(50, 100, '#ffffff', '#3a3a3a', 1024);
433
- // adaptiveGround.name = 'ground';
434
- // adaptiveGround.receiveShadow = true;
435
- // scene.add(adaptiveGround);
436
- }
437
-
438
- init();
439
-
440
- function init() {
441
- const width = window.innerWidth;
442
- const height = window.innerHeight;
443
- scene = new THREE.Scene();
444
- camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 50);
445
- renderer = new THREE.WebGLRenderer({ antialias: true, logarithmicDepthBuffer: true });
446
-
447
- create_scene(scene, camera, renderer, true, 'y', 'z');
448
-
449
- renderer.shadowMap.enabled = true;
450
- renderer.shadowMap.type = THREE.PCFSoftShadowMap;
451
-
452
- scene.background = new THREE.Color(0x424242);
453
-
454
- // Using FogExp2 for more natural exponential falloff
455
- // Density 0.06 provides subtle depth without obscuring close objects
456
- scene.fog = new THREE.FogExp2(0x424242, 0.06);
457
-
458
- // Remove existing lights to reconfigure
459
- scene.children = scene.children.filter(child => !child.isLight);
460
-
461
-
462
- // 1. Hemisphere Light - provides natural sky/ground color gradient
463
- const hemisphereLight = new THREE.HemisphereLight(
464
- 0xffffff, // sky color (white)
465
- 0x444444, // ground color (dark gray)
466
- 1.2 // intensity
467
- );
468
- hemisphereLight.position.set(0, 2, 0);
469
- scene.add(hemisphereLight);
470
-
471
- // 2. Main Directional Light (key light with shadows)
472
- const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
473
- directionalLight.position.set(3, 5, 4);
474
- directionalLight.castShadow = true;
475
- directionalLight.shadow.mapSize.width = 2048;
476
- directionalLight.shadow.mapSize.height = 2048;
477
- directionalLight.shadow.camera.near = 0.5;
478
- directionalLight.shadow.camera.far = 50;
479
- directionalLight.shadow.camera.left = -10;
480
- directionalLight.shadow.camera.right = 10;
481
- directionalLight.shadow.camera.top = 10;
482
- directionalLight.shadow.camera.bottom = -10;
483
- directionalLight.shadow.bias = -0.0001;
484
- scene.add(directionalLight);
485
-
486
- // 3. Fill Light (softer, from opposite side)
487
- const fillLight = new THREE.DirectionalLight(0xaaccff, 0.5);
488
- fillLight.position.set(-3, 3, -2);
489
- scene.add(fillLight);
490
-
491
- // 4. Rim/Back Light (for depth separation)
492
- const rimLight = new THREE.DirectionalLight(0xffeedd, 0.4);
493
- rimLight.position.set(0, 4, -5);
494
- scene.add(rimLight);
495
-
496
- // 5. Optional: Spot Light for dramatic focus
497
- const spotLight = new THREE.SpotLight(0xffffff, 0.6);
498
- spotLight.position.set(0, 6, 3);
499
- spotLight.angle = Math.PI / 8;
500
- spotLight.penumbra = 0.5;
501
- spotLight.decay = 2;
502
- spotLight.distance = 30;
503
- spotLight.castShadow = true;
504
- spotLight.shadow.mapSize.width = 1024;
505
- spotLight.shadow.mapSize.height = 1024;
506
- // scene.add(spotLight); // Uncomment to enable
507
-
508
- // Tone mapping for better color reproduction
509
- renderer.toneMapping = THREE.ACESFilmicToneMapping;
510
- renderer.toneMappingExposure = 1.0;
511
- renderer.outputColorSpace = THREE.SRGBColorSpace;
512
-
513
- renderer.setPixelRatio(window.devicePixelRatio);
514
- renderer.setSize(width, height);
515
- var container = document.getElementById('vis3d');
516
- container.appendChild(renderer.domElement);
517
-
518
- // Handle window resize for fullscreen
519
- window.addEventListener('resize', onWindowResize);
520
-
521
- controls = new OrbitControls(camera, renderer.domElement);
522
- controls.minDistance = 1;
523
- controls.maxDistance = 15;
524
- controls.enableDamping = true;
525
- controls.dampingFactor = 0.05;
526
- controls.target.set(0, 1, 0);
527
- fitCameraToScene(scene, camera, controls, { axis_up: 'y', excludeNames: ['ground'] });
528
-
529
- // Click to play/pause (with drag detection)
530
- let isDragging = false;
531
- let mouseDownTime = 0;
532
-
533
- renderer.domElement.addEventListener('mousedown', () => {
534
- isDragging = false;
535
- mouseDownTime = Date.now();
536
- });
537
-
538
- renderer.domElement.addEventListener('mousemove', () => {
539
- if (Date.now() - mouseDownTime > 150) {
540
- isDragging = true;
541
- }
542
- });
543
-
544
- renderer.domElement.addEventListener('mouseup', (e) => {
545
- // Only toggle play/pause on short clicks (not drags)
546
- if (!isDragging && Date.now() - mouseDownTime < 300) {
547
- if (modelsLoaded) {
548
- isPlaying ? pauseAnimation() : playAnimation();
549
- }
550
- }
551
- });
552
-
553
- // Double-click to reset
554
- renderer.domElement.addEventListener('dblclick', () => {
555
- if (modelsLoaded) {
556
- pauseAnimation();
557
- currentFrame = 0;
558
- updateFrame();
559
- }
560
- });
561
- }
562
-
563
- function animate() {
564
- requestAnimationFrame(animate);
565
- // Update controls for damping effect
566
- if (controls && controls.enableDamping) {
567
- controls.update();
568
- }
569
- renderer.render(scene, camera);
570
- }
571
-
572
- function onWindowResize() {
573
- const width = window.innerWidth;
574
- const height = window.innerHeight;
575
- camera.aspect = width / height;
576
- camera.updateProjectionMatrix();
577
- renderer.setSize(width, height);
578
- }
579
-
580
- function computeOffsets(batchSize) {
581
- const spacing = 2.0;
582
- const total_width = (batchSize - 1) * spacing;
583
- const start_x = -total_width / 2;
584
- const offsets = [];
585
- for (let i = 0; i < batchSize; i++) {
586
- offsets.push(start_x + i * spacing);
587
- }
588
- return offsets;
589
- }
590
-
591
- </script>
592
-
593
- <style>
594
- /* Fullscreen dark mode base styles */
595
- * {
596
- margin: 0;
597
- padding: 0;
598
- box-sizing: border-box;
599
- }
600
-
601
- html, body {
602
- width: 100%;
603
- height: 100%;
604
- overflow: hidden;
605
- background: #424242 !important;
606
- color: #e2e8f0;
607
- }
608
-
609
- /* Fullscreen container for 3D scene */
610
- .fullscreen-container {
611
- position: fixed;
612
- top: 0;
613
- left: 0;
614
- width: 100vw;
615
- height: 100vh;
616
- background: #424242;
617
- overflow: hidden;
618
- }
619
-
620
- #vis3d {
621
- position: absolute;
622
- top: 0;
623
- left: 0;
624
- width: 100%;
625
- height: 100%;
626
- background: #424242;
627
- }
628
-
629
- #vis3d canvas {
630
- display: block;
631
- width: 100% !important;
632
- height: 100% !important;
633
- }
634
-
635
- /* Floating caption overlay - centered at top, width adapts to content */
636
- .caption-overlay {
637
- position: absolute;
638
- top: 20px;
639
- left: 50%;
640
- transform: translateX(-50%);
641
- width: auto;
642
- max-width: 90%;
643
- z-index: 100;
644
- pointer-events: auto;
645
- }
646
-
647
- .motion-info {
648
- background-color: rgba(45, 55, 72, 0.85);
649
- backdrop-filter: blur(10px);
650
- -webkit-backdrop-filter: blur(10px);
651
- border-radius: 20px;
652
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
653
- overflow: hidden;
654
- max-height: 40vh;
655
- overflow-y: auto;
656
- display: inline-block;
657
- }
658
-
659
- /* Floating progress control panel - centered at bottom */
660
- .control-overlay {
661
- position: absolute;
662
- bottom: 30px;
663
- left: 50%;
664
- transform: translateX(-50%);
665
- width: 80%;
666
- max-width: 600px;
667
- z-index: 100;
668
- background: rgba(0, 0, 0, 0.4);
669
- backdrop-filter: blur(8px);
670
- -webkit-backdrop-filter: blur(8px);
671
- padding: 15px 20px;
672
- border-radius: 12px;
673
- }
674
-
675
- .control-row-minimal {
676
- display: flex;
677
- align-items: center;
678
- gap: 20px;
679
- }
680
-
681
- .progress-container {
682
- flex: 1;
683
- }
684
-
685
- .progress-slider-minimal {
686
- width: 100%;
687
- height: 8px;
688
- border-radius: 4px;
689
- background: rgba(255, 255, 255, 0.3);
690
- outline: none;
691
- cursor: pointer;
692
- -webkit-appearance: none;
693
- appearance: none;
694
- }
695
-
696
- .progress-slider-minimal::-webkit-slider-runnable-track {
697
- width: 100%;
698
- height: 8px;
699
- border-radius: 4px;
700
- background: rgba(255, 255, 255, 0.3);
701
- }
702
-
703
- .progress-slider-minimal::-webkit-slider-thumb {
704
- -webkit-appearance: none;
705
- appearance: none;
706
- width: 20px;
707
- height: 20px;
708
- border-radius: 50%;
709
- background: #4a9eff;
710
- cursor: pointer;
711
- border: 2px solid white;
712
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
713
- margin-top: -6px;
714
- }
715
-
716
- .progress-slider-minimal::-moz-range-track {
717
- width: 100%;
718
- height: 8px;
719
- border-radius: 4px;
720
- background: rgba(255, 255, 255, 0.3);
721
- }
722
-
723
- .progress-slider-minimal::-moz-range-thumb {
724
- width: 20px;
725
- height: 20px;
726
- border-radius: 50%;
727
- background: #4a9eff;
728
- cursor: pointer;
729
- border: 2px solid white;
730
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
731
- }
732
-
733
- .frame-counter {
734
- font-family: 'SF Mono', 'Consolas', monospace;
735
- font-size: 14px;
736
- font-weight: 500;
737
- color: white;
738
- text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
739
- white-space: nowrap;
740
- min-width: 80px;
741
- text-align: right;
742
- }
743
-
744
- /* Loading overlay - centered */
745
- .loading-overlay {
746
- position: absolute;
747
- top: 50%;
748
- left: 50%;
749
- transform: translate(-50%, -50%);
750
- background: rgba(0, 0, 0, 0.7);
751
- backdrop-filter: blur(8px);
752
- -webkit-backdrop-filter: blur(8px);
753
- color: white;
754
- padding: 15px 25px;
755
- border-radius: 10px;
756
- font-size: 14px;
757
- z-index: 200;
758
- display: flex;
759
- align-items: center;
760
- gap: 10px;
761
- }
762
-
763
- .loading-overlay.hidden {
764
- display: none;
765
- }
766
-
767
- .loading-overlay.complete {
768
- background: rgba(76, 175, 80, 0.85);
769
- }
770
-
771
- /* Caption content styles */
772
- .loading {
773
- padding: 10px 18px;
774
- text-align: center;
775
- color: #a0aec0;
776
- font-style: italic;
777
- white-space: nowrap;
778
- }
779
-
780
- .file-info {
781
- background: linear-gradient(135deg, rgba(102, 126, 234, 0.9) 0%, rgba(118, 75, 162, 0.9) 100%);
782
- color: white;
783
- padding: 15px 20px;
784
- }
785
-
786
- .file-info h4 {
787
- margin: 0 0 8px 0;
788
- font-size: 1.1em;
789
- font-weight: 600;
790
- }
791
-
792
- .file-detail {
793
- margin: 4px 0;
794
- font-size: 0.85em;
795
- opacity: 0.9;
796
- }
797
-
798
- .captions-section {
799
- padding: 12px 20px;
800
- white-space: nowrap;
801
- }
802
-
803
- .captions-title {
804
- margin: 0 0 12px 0;
805
- color: #e2e8f0;
806
- font-size: 1em;
807
- font-weight: 600;
808
- border-bottom: 2px solid #667eea;
809
- padding-bottom: 6px;
810
- }
811
-
812
- .caption-item {
813
- background: transparent;
814
- border: none;
815
- border-radius: 0;
816
- margin-bottom: 6px;
817
- padding: 0;
818
- color: #f0f4f8;
819
- font-size: 1em;
820
- font-weight: 500;
821
- line-height: 1.5;
822
- }
823
-
824
- .caption-item:last-child {
825
- margin-bottom: 0;
826
- }
827
-
828
- .caption-item:hover {
829
- background: transparent;
830
- border-color: transparent;
831
- }
832
-
833
- .caption-content {
834
- margin-bottom: 0;
835
- }
836
-
837
- .no-captions {
838
- text-align: center;
839
- padding: 20px;
840
- color: #a0aec0;
841
- font-style: italic;
842
- }
843
-
844
- .error-message {
845
- background-color: rgba(255, 100, 100, 0.2);
846
- color: #ff6b6b;
847
- padding: 12px;
848
- border-radius: 6px;
849
- text-align: center;
850
- }
851
-
852
- .caption-rewritten {
853
- font-size: 0.95em;
854
- font-weight: 500;
855
- color: #f0f4f8;
856
- margin-bottom: 0;
857
- line-height: 1.5;
858
- }
859
-
860
- .caption-original-toggle {
861
- font-size: 0.8em;
862
- color: #7c9eff;
863
- cursor: pointer;
864
- display: inline-flex;
865
- align-items: center;
866
- gap: 4px;
867
- padding: 4px 0;
868
- user-select: none;
869
- transition: color 0.2s;
870
- margin-top: 6px;
871
- }
872
-
873
- .caption-original-toggle:hover {
874
- color: #a0b4ff;
875
- }
876
-
877
- .caption-original-toggle i {
878
- transition: transform 0.2s;
879
- }
880
-
881
- .caption-original-toggle.expanded i {
882
- transform: rotate(180deg);
883
- }
884
-
885
- .caption-original {
886
- display: none !important;
887
- font-size: 0.85em;
888
- color: #a0aec0;
889
- background: rgba(0, 0, 0, 0.2);
890
- padding: 8px 12px;
891
- border-radius: 6px;
892
- margin-top: 8px;
893
- line-height: 1.4;
894
- }
895
-
896
- .caption-original.show {
897
- display: block !important;
898
- }
899
-
900
- .original-label {
901
- font-weight: 600;
902
- color: #718096;
903
- }
904
-
905
- /* Scrollbar styling for motion-info */
906
- .motion-info::-webkit-scrollbar {
907
- width: 6px;
908
- }
909
-
910
- .motion-info::-webkit-scrollbar-track {
911
- background: rgba(255, 255, 255, 0.1);
912
- border-radius: 3px;
913
- }
914
-
915
- .motion-info::-webkit-scrollbar-thumb {
916
- background: rgba(255, 255, 255, 0.3);
917
- border-radius: 3px;
918
- }
919
-
920
- .motion-info::-webkit-scrollbar-thumb:hover {
921
- background: rgba(255, 255, 255, 0.5);
922
- }
923
- </style>
924
-
925
-
926
- <!-- Only load caption fetching script if captions are not hidden -->
927
- {% if not hide_captions %}
928
- <script>
929
- function createCaptionItem(caption, index) {
930
- const hasOriginal = caption['short caption'];
931
- const rewritten = caption['short caption+'] || caption['short caption'] || 'No caption';
932
-
933
- return `
934
- <div class="caption-item">
935
- <div class="caption-content">
936
- <div class="caption-rewritten">${rewritten}</div>
937
- ${hasOriginal && caption['short caption+'] ? `
938
- <div class="caption-original-toggle" data-toggle="caption-original">
939
- <i class="fas fa-chevron-down"></i> Show original text
940
- </div>
941
- <div class="caption-original">
942
- <span class="original-label">Original:</span> ${caption['short caption']}
943
- </div>
944
- ` : ''}
945
- </div>
946
- </div>
947
- `;
948
- }
949
-
950
- function createCaptionItemSimple(caption, index) {
951
- const hasOriginal = caption['short caption'];
952
- const rewritten = caption['short caption+'] || caption['short caption'] || 'No caption';
953
-
954
- return `
955
- <div class="caption-item" style="text-align: center;">
956
- ${rewritten}
957
- </div>
958
- `;
959
- }
960
-
961
- document.addEventListener('click', (e) => {
962
- const toggle = e.target.closest('[data-toggle="caption-original"]');
963
- if (toggle) {
964
- const originalDiv = toggle.nextElementSibling;
965
- if (originalDiv && originalDiv.classList.contains('caption-original')) {
966
- originalDiv.classList.toggle('show');
967
- toggle.classList.toggle('expanded');
968
- }
969
- }
970
- });
971
-
972
- function fetchMotionInfo() {
973
- fetch('/query_caption/{{ folder_name }}/{{ file_name }}')
974
- .then(response => response.json())
975
- .then(data => {
976
- const motionInfoElement = document.getElementById('motion-info');
977
-
978
- if (data && (data.filename || data.motion_path || data.result)) {
979
- let html = '';
980
-
981
- if (data.filename || data.motion_path) {
982
- html += `
983
- <div class="file-info">
984
- <h4>File Information</h4>
985
- ${data.filename ? `<div class="file-detail"><strong>Filename:</strong> ${data.filename}</div>` : ''}
986
- ${data.motion_path ? `<div class="file-detail"><strong>Motion Path:</strong> ${data.motion_path}</div>` : ''}
987
- </div>
988
- `;
989
- }
990
-
991
- if (data.result && Array.isArray(data.result) && data.result.length > 0) {
992
- html += `
993
- <div class="captions-section">
994
- ${data.result.map((caption, index) => createCaptionItemSimple(caption, index)).join('')}
995
- </div>
996
- `;
997
- } else if (data.result) {
998
- html += `
999
- <div class="captions-section">
1000
- <div class="no-captions">No captions available for this motion</div>
1001
- </div>
1002
- `;
1003
- }
1004
-
1005
- if (!html && data.caption) {
1006
- html = `
1007
- <div class="captions-section">
1008
- <h5 class="captions-title">Caption</h5>
1009
- <div class="caption-item">
1010
- <div class="caption-content">
1011
- <div class="caption-short">${data.caption}</div>
1012
- </div>
1013
- </div>
1014
- </div>
1015
- `;
1016
- }
1017
-
1018
- motionInfoElement.innerHTML = html || '<div class="no-captions">No motion information available</div>';
1019
- } else {
1020
- motionInfoElement.innerHTML = '<div class="no-captions">No motion information available</div>';
1021
- }
1022
- })
1023
- .catch(error => {
1024
- console.error('Error fetching motion information:', error);
1025
- document.getElementById('motion-info').innerHTML = '<div class="error-message">Error loading motion information</div>';
1026
- });
1027
- }
1028
-
1029
- document.addEventListener('DOMContentLoaded', fetchMotionInfo);
1030
- </script>
1031
- {% endif %}
1032
-
1033
- {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/gradio/vis_gradio.py DELETED
@@ -1,103 +0,0 @@
1
- import os
2
- import sys
3
- from os import path as osp
4
-
5
- from flask import Flask, jsonify, render_template, request
6
-
7
- sys.path.append(osp.dirname(osp.dirname(osp.dirname(__file__))))
8
- from hymotion.utils.visualize_mesh_web import (
9
- get_cached_captions,
10
- get_cached_smpl_frames,
11
- get_output_dir,
12
- sanitize_filename,
13
- sanitize_folder_name,
14
- safe_path_join,
15
- )
16
-
17
- template_folder = os.path.join(os.path.dirname(__file__), "templates")
18
- static_folder = os.path.join(os.path.dirname(__file__), "static")
19
-
20
- app = Flask(__name__, template_folder=template_folder, static_folder=static_folder)
21
-
22
-
23
- @app.route("/")
24
- def home():
25
- return "HMotion Visualization Server is Running. Use /view/<path> to access content.", 200
26
-
27
-
28
- @app.route("/view/")
29
- @app.route("/view/<path:full_path>")
30
- def index(full_path: str = ""):
31
- hide_captions = request.args.get("hide_captions", "0") == "1"
32
-
33
- # security check
34
- if ".." in full_path or full_path.startswith("/"):
35
- return "Invalid path", 403
36
- if "/" in full_path:
37
- raw_folder, raw_file = full_path.rsplit("/", 1)
38
- else:
39
- raw_folder, raw_file = "", full_path
40
-
41
- folder_name = sanitize_folder_name(raw_folder)
42
- file_name = sanitize_filename(raw_file)
43
-
44
- # remove possible suffix
45
- for suffix in [".npz", ".json", ".h5"]:
46
- if file_name.endswith(suffix):
47
- file_name = file_name[: -len(suffix)]
48
- break
49
-
50
- base_dir = get_output_dir(folder_name)
51
- target_meta = safe_path_join(base_dir, f"{file_name}_meta.json")
52
- target_npz = safe_path_join(base_dir, f"{file_name}.npz")
53
-
54
- # check if meta file exists
55
- if os.path.isfile(target_meta) or os.path.isfile(target_npz):
56
- return render_template(
57
- "index_wooden_gradio.html",
58
- folder_name=folder_name,
59
- file_name=file_name,
60
- mirror_name=None,
61
- hide_captions=hide_captions,
62
- next_file=None,
63
- )
64
- else:
65
- return (
66
- render_template(
67
- "error_file_not_found.html",
68
- folder_name=folder_name,
69
- file_name=file_name,
70
- full_path=target_meta,
71
- file_type="meta.json",
72
- original_folder=folder_name,
73
- original_file=full_path,
74
- ),
75
- 404,
76
- )
77
-
78
-
79
- @app.route("/query_smpl/<path:folder_name>/<file_name>")
80
- def query_smpl(folder_name: str, file_name: str):
81
- smpl_data = get_cached_smpl_frames(folder_name, file_name)
82
- return jsonify(smpl_data)
83
-
84
-
85
- @app.route("/query_caption/<path:folder_name>/<file_name>")
86
- def query_caption(folder_name: str, file_name: str):
87
- hide_captions = request.args.get("hide_captions", "0") == "1"
88
- if hide_captions:
89
- return jsonify({"result": []})
90
- captions = get_cached_captions(folder_name, file_name)
91
- return jsonify({"result": captions})
92
-
93
-
94
- if __name__ == "__main__":
95
- import argparse
96
-
97
- parser = argparse.ArgumentParser()
98
- parser.add_argument("--host", type=str, default="0.0.0.0")
99
- parser.add_argument("--port", type=int, default=8081)
100
- args = parser.parse_args()
101
-
102
- print(f">>> Starting Flask server on {args.host}:{args.port}")
103
- app.run(host=args.host, port=args.port, debug=False, threaded=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/gradio/vis_routes.py DELETED
@@ -1,91 +0,0 @@
1
- import os
2
-
3
- from fastapi import APIRouter, HTTPException, Request
4
- from fastapi.responses import HTMLResponse, JSONResponse
5
- from fastapi.templating import Jinja2Templates
6
-
7
- from hymotion.utils.visualize_mesh_web import (
8
- get_cached_captions,
9
- get_cached_smpl_frames,
10
- get_output_dir,
11
- safe_path_join,
12
- sanitize_filename,
13
- sanitize_folder_name,
14
- )
15
-
16
- # Define Router
17
- router = APIRouter()
18
-
19
- # Define template directory
20
- current_dir = os.path.dirname(os.path.abspath(__file__))
21
- templates = Jinja2Templates(directory=os.path.join(current_dir, "templates"))
22
-
23
-
24
- @router.get("/wait_for_data")
25
- async def wait_for_data():
26
- return {"status": "ready", "frames": 0}
27
-
28
-
29
- @router.get("/view/{full_path:path}", response_class=HTMLResponse)
30
- async def view_visualization(request: Request, full_path: str):
31
- hide_captions = request.query_params.get("hide_captions", "0") == "1"
32
-
33
- # Security check and path parsing logic
34
- if ".." in full_path or full_path.startswith("/"):
35
- raise HTTPException(status_code=403, detail="Invalid path")
36
-
37
- if "/" in full_path:
38
- raw_folder, raw_file = full_path.rsplit("/", 1)
39
- else:
40
- raw_folder, raw_file = "", full_path
41
-
42
- folder_name = sanitize_folder_name(raw_folder)
43
- file_name = sanitize_filename(raw_file)
44
-
45
- for suffix in [".npz", ".json", ".h5"]:
46
- if file_name.endswith(suffix):
47
- file_name = file_name[: -len(suffix)]
48
- break
49
-
50
- base_dir = get_output_dir(folder_name)
51
- target_meta = safe_path_join(base_dir, f"{file_name}_meta.json")
52
- target_npz = safe_path_join(base_dir, f"{file_name}.npz")
53
-
54
- if os.path.isfile(target_meta) or os.path.isfile(target_npz):
55
- # FastAPI template rendering needs to pass in request
56
- return templates.TemplateResponse(
57
- "index_wooden_gradio.html",
58
- {
59
- "request": request,
60
- "folder_name": folder_name,
61
- "file_name": file_name,
62
- "hide_captions": hide_captions,
63
- },
64
- )
65
- else:
66
- return templates.TemplateResponse(
67
- "error_file_not_found.html",
68
- {
69
- "request": request,
70
- "folder_name": folder_name,
71
- "file_name": file_name,
72
- "full_path": target_meta,
73
- "file_type": "meta.json",
74
- },
75
- status_code=404,
76
- )
77
-
78
-
79
- @router.get("/query_smpl/{folder_name:path}/{file_name}")
80
- async def query_smpl(folder_name: str, file_name: str):
81
- smpl_data = get_cached_smpl_frames(folder_name, file_name)
82
- return JSONResponse(content=smpl_data)
83
-
84
-
85
- @router.get("/query_caption/{folder_name:path}/{file_name}")
86
- async def query_caption(request: Request, folder_name: str, file_name: str):
87
- hide_captions = request.query_params.get("hide_captions", "0") == "1"
88
- if hide_captions:
89
- return JSONResponse({"result": []})
90
- captions = get_cached_captions(folder_name, file_name)
91
- return JSONResponse({"result": captions})