liuyiyang01 commited on
Commit
634d430
·
1 Parent(s): 81c671d

initial app

Browse files
Files changed (3) hide show
  1. app.py +784 -0
  2. requirements.txt +6 -0
  3. scene_1.png +0 -3
app.py ADDED
@@ -0,0 +1,784 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import requests
3
+ import json
4
+ import os
5
+ import subprocess
6
+ import uuid
7
+ import time
8
+ import cv2
9
+ from typing import Optional, List
10
+ import numpy as np
11
+ from datetime import datetime, timedelta
12
+ from collections import defaultdict
13
+
14
+ os.environ["SPACES_QUEUE_ENABLED"] = "true"
15
+
16
+ # 后端API配置(可配置化)
17
+ BACKEND_URL = os.getenv("BACKEND_URL", "http://47.95.6.204:51001/")
18
+ API_ENDPOINTS = {
19
+ "submit_task": f"{BACKEND_URL}/predict/video",
20
+ "query_status": f"{BACKEND_URL}/predict/task",
21
+ "terminate_task": f"{BACKEND_URL}/predict/terminate"
22
+ }
23
+
24
+ # 模拟场景配置
25
+ SCENE_CONFIGS = {
26
+ "scene_1": {
27
+ "description": "scene_1",
28
+ "objects": ["milk carton", "ceramic bowl", "mug"],
29
+ "preview_image": "assets/scene_1.png"
30
+ },
31
+ }
32
+
33
+ # 可用模型列表
34
+ MODEL_CHOICES = [
35
+ "gr1",
36
+ # "GR00T-N1",
37
+ # "GR00T-1.5",
38
+ # "Pi0",
39
+ # "DP+CLIP",
40
+ # "AcT+CLIP"
41
+ ]
42
+
43
+ ###############################################################################
44
+
45
+ SESSION_TASKS = {}
46
+ IP_REQUEST_RECORDS = defaultdict(list)
47
+ IP_LIMIT = 5 # 每分钟最多请求次数
48
+
49
+ def is_request_allowed(ip: str) -> bool:
50
+ now = datetime.now()
51
+ IP_REQUEST_RECORDS[ip] = [t for t in IP_REQUEST_RECORDS[ip] if now - t < timedelta(minutes=1)]
52
+ if len(IP_REQUEST_RECORDS[ip]) < IP_LIMIT:
53
+ IP_REQUEST_RECORDS[ip].append(now)
54
+ return True
55
+ return False
56
+ ###############################################################################
57
+
58
+
59
+ # 日志文件路径
60
+ LOG_DIR = "/opt/gradio-frontend/logs"
61
+ os.makedirs(LOG_DIR, exist_ok=True)
62
+ ACCESS_LOG = os.path.join(LOG_DIR, "access.log")
63
+ SUBMISSION_LOG = os.path.join(LOG_DIR, "submissions.log")
64
+
65
+ def log_access(user_ip: str = None, user_agent: str = None):
66
+ """记录用户访问日志"""
67
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
68
+ log_entry = {
69
+ "timestamp": timestamp,
70
+ "type": "access",
71
+ "user_ip": user_ip or "unknown",
72
+ "user_agent": user_agent or "unknown"
73
+ }
74
+
75
+ with open(ACCESS_LOG, "a") as f:
76
+ f.write(json.dumps(log_entry) + "\n")
77
+
78
+ def log_submission(scene: str, prompt: str, model: str, max_step: int, user: str = "anonymous", res: str = "unknown"):
79
+ """记录用户提交日志"""
80
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
81
+ log_entry = {
82
+ "timestamp": timestamp,
83
+ "type": "submission",
84
+ "user": user,
85
+ "scene": scene,
86
+ "prompt": prompt,
87
+ "model": model,
88
+ "max_step": str(max_step),
89
+ "res": res
90
+ }
91
+
92
+ with open(SUBMISSION_LOG, "a") as f:
93
+ f.write(json.dumps(log_entry) + "\n")
94
+
95
+ def read_logs(log_type: str = "all", max_entries: int = 50) -> list:
96
+ """读取日志文件"""
97
+ logs = []
98
+
99
+ if log_type in ["all", "access"]:
100
+ try:
101
+ with open(ACCESS_LOG, "r") as f:
102
+ for line in f:
103
+ logs.append(json.loads(line.strip()))
104
+ except FileNotFoundError:
105
+ pass
106
+
107
+ if log_type in ["all", "submission"]:
108
+ try:
109
+ with open(SUBMISSION_LOG, "r") as f:
110
+ for line in f:
111
+ logs.append(json.loads(line.strip()))
112
+ except FileNotFoundError:
113
+ pass
114
+
115
+ # 按时间戳排序,最新的在前
116
+ logs.sort(key=lambda x: x["timestamp"], reverse=True)
117
+ return logs[:max_entries]
118
+
119
+ def format_logs_for_display(logs: list) -> str:
120
+ """格式化日志用于显示"""
121
+ if not logs:
122
+ return "暂无日志记录"
123
+
124
+ markdown = "### 系统访问日志\n\n"
125
+ markdown += "| 时间 | 类型 | 用户/IP | 详细信息 |\n"
126
+ markdown += "|------|------|---------|----------|\n"
127
+
128
+ for log in logs:
129
+ timestamp = log.get("timestamp", "unknown")
130
+ log_type = "访问" if log.get("type") == "access" else "提交"
131
+
132
+ if log_type == "访问":
133
+ user = log.get("user_ip", "unknown")
134
+ details = f"User-Agent: {log.get('user_agent', 'unknown')}"
135
+ else:
136
+ user = log.get("user", "anonymous")
137
+ result = log.get('res', 'unknown')
138
+ if result != "success":
139
+ if len(result) > 40: # Adjust this threshold as needed
140
+ result = f"{result[:20]}...{result[-20:]}"
141
+ details = f"场景: {log.get('scene', 'unknown')}, 指令: {log.get('prompt', '')}, 模型: {log.get('model', 'unknown')}, max step: {log.get('max_step', '300')}, result: {result}"
142
+
143
+ markdown += f"| {timestamp} | {log_type} | {user} | {details} |\n"
144
+
145
+ return markdown
146
+
147
+
148
+
149
+ ###############################################################################
150
+
151
+
152
+ def stream_simulation_results(result_folder: str, task_id: str, fps: int = 30):
153
+ """
154
+ 流式输出仿真结果,同时监控图片文件夹和后端任务状态
155
+
156
+ 参数:
157
+ result_folder: 包含生成图片的文件夹路径
158
+ task_id: 后端任务ID��于状态查询
159
+ fps: 输出视频的帧率
160
+
161
+ 生成:
162
+ 生成的视频文件路径 (分段输出)
163
+ """
164
+ # 初始化变量
165
+ result_folder = os.path.join(result_folder, "image")
166
+ os.makedirs(result_folder, exist_ok=True)
167
+ frame_buffer: List[np.ndarray] = []
168
+ frames_per_segment = fps * 2 # 每2秒60帧
169
+ processed_files = set()
170
+ width, height = 0, 0
171
+ last_status_check = 0
172
+ status_check_interval = 5 # 每5秒检查一次后端状态
173
+ max_time = 240
174
+
175
+ while max_time > 0:
176
+ max_time -= 1
177
+ current_time = time.time()
178
+
179
+ # 定期检查后端状态
180
+ if current_time - last_status_check > status_check_interval:
181
+ status = get_task_status(task_id)
182
+ print("status: ", status)
183
+ if status.get("status") == "completed":
184
+ # 确保处理完所有已生成的图片
185
+ process_remaining_images(result_folder, processed_files, frame_buffer)
186
+ if frame_buffer:
187
+ yield create_video_segment(frame_buffer, fps, width, height)
188
+ break
189
+ elif status.get("status") == "failed":
190
+ raise gr.Error(f"任务执行失败: {status.get('result', '未知错误')}")
191
+ elif status.get("status") == "terminated":
192
+ break
193
+ last_status_check = current_time
194
+
195
+ # 处理新生成的图片
196
+ current_files = sorted(
197
+ [f for f in os.listdir(result_folder)
198
+ if f.lower().endswith(('.png', '.jpg', '.jpeg'))],
199
+ key=lambda x: os.path.splitext(x)[0] # 按文件名排序
200
+ )
201
+
202
+ new_files = [f for f in current_files if f not in processed_files]
203
+ has_new_frames = False
204
+
205
+ for filename in new_files:
206
+ try:
207
+ img_path = os.path.join(result_folder, filename)
208
+ frame = cv2.imread(img_path)
209
+ if frame is not None:
210
+ if width == 0: # 第一次获取图像尺寸
211
+ height, width = frame.shape[:2]
212
+
213
+ frame_buffer.append(frame)
214
+ processed_files.add(filename)
215
+ has_new_frames = True
216
+ except Exception as e:
217
+ print(f"Error processing {filename}: {e}")
218
+
219
+ # 如果有新帧且积累够60帧,输出视频片段
220
+ if has_new_frames and len(frame_buffer) >= frames_per_segment:
221
+ segment_frames = frame_buffer[:frames_per_segment]
222
+ frame_buffer = frame_buffer[frames_per_segment:]
223
+ yield create_video_segment(segment_frames, fps, width, height)
224
+
225
+ time.sleep(1) # 避免过于频繁检查
226
+
227
+ if max_time <= 0:
228
+ raise gr.Error("timeout 240s")
229
+
230
+ def create_video_segment(frames: List[np.ndarray], fps: int, width: int, height: int) -> str:
231
+ """创建视频片段"""
232
+ os.makedirs("/opt/gradio_demo/tasks/video_chunk", exist_ok=True)
233
+ segment_name = f"/opt/gradio_demo/tasks/video_chunk/output_{uuid.uuid4()}.mp4"
234
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
235
+ out = cv2.VideoWriter(segment_name, fourcc, fps, (width, height))
236
+
237
+ for frame in frames:
238
+ out.write(frame)
239
+ out.release()
240
+
241
+ return segment_name
242
+
243
+ def process_remaining_images(result_folder: str, processed_files: set, frame_buffer: List[np.ndarray]):
244
+ """处理剩余的图片"""
245
+ current_files = sorted(
246
+ [f for f in os.listdir(result_folder)
247
+ if f.lower().endswith(('.png', '.jpg', '.jpeg'))],
248
+ key=lambda x: os.path.splitext(x)[0]
249
+ )
250
+
251
+ new_files = [f for f in current_files if f not in processed_files]
252
+
253
+ for filename in new_files:
254
+ try:
255
+ img_path = os.path.join(result_folder, filename)
256
+ frame = cv2.imread(img_path)
257
+ if frame is not None:
258
+ frame_buffer.append(frame)
259
+ processed_files.add(filename)
260
+ except Exception as e:
261
+ print(f"Error processing remaining {filename}: {e}")
262
+
263
+
264
+
265
+
266
+ ###############################################################################
267
+
268
+
269
+
270
+ def submit_to_backend(
271
+ scene: str,
272
+ prompt: str,
273
+ model: str,
274
+ max_step: int,
275
+ user: str = "Gradio-user",
276
+ ) -> dict:
277
+ job_id = str(uuid.uuid4())
278
+
279
+ data = {
280
+ "scene_type": scene,
281
+ "instruction": prompt,
282
+ "model_type": model,
283
+ "max_step": str(max_step)
284
+ }
285
+
286
+ payload = {
287
+ "user": user,
288
+ "task": "robot_manipulation",
289
+ "job_id": job_id,
290
+ "data": json.dumps(data)
291
+ }
292
+
293
+ try:
294
+ headers = {"Content-Type": "application/json"}
295
+ response = requests.post(
296
+ API_ENDPOINTS["submit_task"],
297
+ json=payload,
298
+ headers=headers,
299
+ timeout=10
300
+ )
301
+ return response.json()
302
+ except Exception as e:
303
+ return {"status": "error", "message": str(e)}
304
+
305
+ def get_task_status(task_id: str) -> dict:
306
+ """
307
+ 查询任务状态
308
+ """
309
+ try:
310
+ response = requests.get(
311
+ f"{API_ENDPOINTS['query_status']}/{task_id}",
312
+ timeout=5
313
+ )
314
+ return response.json()
315
+ except Exception as e:
316
+ return {"status": "error", "message": str(e)}
317
+
318
+ def terminate_task(task_id: str) -> Optional[dict]:
319
+ """
320
+ 终止任务
321
+ """
322
+ try:
323
+ response = requests.post(
324
+ f"{API_ENDPOINTS['terminate_task']}/{task_id}",
325
+ timeout=3
326
+ )
327
+ return response.json()
328
+ except Exception as e:
329
+ print(f"Error terminate task: {e}")
330
+ return None
331
+
332
+ def convert_to_h264(video_path):
333
+ """
334
+ 将视频转换为 H.264 编码的 MP4 格式
335
+ 生成新文件路径在原路径基础上添加 _h264 后缀)
336
+ """
337
+ base, ext = os.path.splitext(video_path)
338
+ video_path_h264 = f"{base}_h264.mp4"
339
+
340
+ try:
341
+ # 构建 FFmpeg 命令
342
+ ffmpeg_cmd = [
343
+ "ffmpeg",
344
+ "-i", video_path,
345
+ "-c:v", "libx264",
346
+ "-preset", "slow",
347
+ "-crf", "23",
348
+ "-c:a", "aac",
349
+ "-movflags", "+faststart",
350
+ video_path_h264
351
+ ]
352
+
353
+ # 执行 FFmpeg 命令
354
+ subprocess.run(ffmpeg_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
355
+
356
+ # 检查输出文件是否存在
357
+ if not os.path.exists(video_path_h264):
358
+ raise FileNotFoundError(f"H.264 编码文件未生成: {video_path_h264}")
359
+
360
+ return video_path_h264
361
+
362
+ except subprocess.CalledProcessError as e:
363
+ raise gr.Error(f"FFmpeg 转换失败: {e.stderr}")
364
+ except Exception as e:
365
+ raise gr.Error(f"转换过程中发生错误: {str(e)}")
366
+
367
+ def run_simulation(
368
+ scene: str,
369
+ prompt: str,
370
+ model: str,
371
+ max_step: int,
372
+ history: list,
373
+ request: gr.Request
374
+ ) -> dict:
375
+ """运行仿真并更新历史记录"""
376
+ # 获取当前时间
377
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
378
+ scene_desc = SCENE_CONFIGS.get(scene, {}).get("description", scene)
379
+
380
+ # 记录用户提交
381
+ user_ip = request.client.host if request else "unknown"
382
+ session_id = request.session_hash
383
+
384
+ if not is_request_allowed(user_ip):
385
+ log_submission(scene, prompt, model, max_step, user_ip, "IP blocked temporarily")
386
+ raise gr.Error("Too many requests from this IP. Please wait and try again one minute later.")
387
+
388
+ # 提交任务到后端
389
+ submission_result = submit_to_backend(scene, prompt, model, max_step, user_ip)
390
+ print("submission_result: ", submission_result)
391
+
392
+ if submission_result.get("status") != "pending":
393
+ log_submission(scene, prompt, model, max_step, user_ip, "Submission failed")
394
+ raise gr.Error(f"Submission failed: {submission_result.get('message', 'unknown issue')}")
395
+
396
+ try:
397
+ task_id = submission_result["task_id"]
398
+ SESSION_TASKS[session_id] = task_id
399
+
400
+ gr.Info(f"Simulation started, task_id: {task_id}")
401
+ time.sleep(5)
402
+ # 获取任务状态
403
+ status = get_task_status(task_id)
404
+ print("first status: ", status)
405
+ result_folder = status.get("result", "")
406
+ except Exception as e:
407
+ log_submission(scene, prompt, model, max_step, user_ip, str(e))
408
+ raise gr.Error(f"error occurred when parsing submission result from backend: {str(e)}")
409
+
410
+
411
+ if not os.path.exists(result_folder):
412
+ log_submission(scene, prompt, model, max_step, user_ip, "Result folder provided by backend doesn't exist")
413
+ raise gr.Error(f"Result folder provided by backend doesn't exist: <PATH>{result_folder}")
414
+
415
+
416
+ # 流式输出视频片段
417
+ try:
418
+ for video_path in stream_simulation_results(result_folder, task_id):
419
+ if video_path:
420
+ yield video_path, history
421
+ except Exception as e:
422
+ log_submission(scene, prompt, model, max_step, user_ip, str(e))
423
+ raise gr.Error(f"Error while streaming: {str(e)}")
424
+
425
+ # 获取任务状态
426
+ status = get_task_status(task_id)
427
+ print("status: ", status)
428
+ if status.get("status") == "completed":
429
+ video_path = os.path.join(status.get("result"), "manipulation.mp4")
430
+ print("video_path: ", video_path)
431
+ video_path = convert_to_h264(video_path)
432
+
433
+ # 创建新的历史记录条目
434
+ new_entry = {
435
+ "timestamp": timestamp,
436
+ "scene": scene,
437
+ "model": model,
438
+ "prompt": prompt,
439
+ "max_step": max_step,
440
+ "video_path": video_path,
441
+ "task_id": task_id
442
+ }
443
+
444
+ # 将新条目添加到历史记录顶部
445
+ updated_history = history + [new_entry]
446
+
447
+ # 限制历史记录数量,避免内存问题
448
+ if len(updated_history) > 10:
449
+ updated_history = updated_history[:10]
450
+
451
+ print("updated_history:", updated_history)
452
+ log_submission(scene, prompt, model, max_step, user_ip, "success")
453
+ gr.Info("Simulation completed successfully!")
454
+ yield None, updated_history
455
+
456
+ elif status.get("status") == "failed":
457
+ log_submission(scene, prompt, model, max_step, user_ip, status.get('result', 'backend error'))
458
+ raise gr.Error(f"Task execution failed: {status.get('result', 'backend unknown issue')}")
459
+ yield None, history
460
+
461
+ elif status.get("status") == "terminated":
462
+ log_submission(scene, prompt, model, max_step, user_ip, "user end terminated")
463
+ yield None, history
464
+
465
+ else:
466
+ log_submission(scene, prompt, model, max_step, user_ip, "missing task's status from backend (Pending?)")
467
+ raise gr.Error("missing task's status from backend (Pending?)")
468
+ yield None, history
469
+
470
+
471
+ ###############################################################################
472
+
473
+
474
+ def update_history_display(history: list) -> list:
475
+ """更新历史记录显示"""
476
+ print("更新历史记录显示")
477
+ updates = []
478
+
479
+ for i in range(10):
480
+ if i < len(history): # 如果有历史记录,更新对应槽位
481
+ entry = history[i]
482
+ updates.extend([
483
+ gr.update(visible=True), # 更新 Column 可见性
484
+ gr.update(visible=True, label=f"# {i+1} | {entry['scene']} | {entry['model']} | {entry['prompt']}", open=(i+1==len(history))), # 更新 Accordion
485
+ gr.update(value=entry['video_path'], visible=True, autoplay=False), # 更新 Video
486
+ gr.update(value=f"{entry['timestamp']}") # 更新详细 Markdown
487
+ ])
488
+ else: # 如果没有历史记录,隐藏槽位
489
+ updates.extend([
490
+ gr.update(visible=False), # 隐藏 Column
491
+ gr.update(visible=False), # 隐藏 Accordion
492
+ gr.update(value=None, visible=False), # 清空 Video
493
+ gr.update(value="") # 清空详细 Markdown
494
+ ])
495
+ print("更新完成!")
496
+ return updates
497
+
498
+ def update_scene_display(scene: str) -> tuple[str, Optional[str]]:
499
+ """更新场景描述和预览图"""
500
+ config = SCENE_CONFIGS.get(scene, {})
501
+ desc = config.get("description", "No description")
502
+ objects = ", ".join(config.get("objects", []))
503
+ image = config.get("preview_image", None)
504
+
505
+ markdown = f"**{desc}** \nObjects in this scene: {objects}"
506
+ return markdown, image
507
+
508
+ def update_log_display():
509
+ """更新日志显示"""
510
+ logs = read_logs()
511
+ return format_logs_for_display(logs)
512
+
513
+ ###############################################################################
514
+
515
+
516
+ def cleanup_session(request: gr.Request):
517
+ session_id = request.session_hash
518
+ task_id = SESSION_TASKS.pop(session_id, None)
519
+
520
+ if task_id:
521
+ try:
522
+ status = get_task_status(task_id)
523
+ print("clean up check status: ", status)
524
+ if status.get("status") == "pending":
525
+ res = terminate_task(task_id)
526
+ if res.get("status") == "success":
527
+ print(f"已终止任务 {task_id}")
528
+ else:
529
+ print(f"终止任务失败 {task_id}: {res.get('status', 'unknown issue')}")
530
+ except Exception as e:
531
+ print(f"终止任务失败 {task_id}: {e}")
532
+
533
+
534
+
535
+ ###############################################################################
536
+
537
+ header_html = """
538
+ <div style="display: flex; justify-content: space-between; align-items: center; width: 100%; margin-bottom: 20px; padding: 20px; background: linear-gradient(135deg, #528bdb 0%, #a7b5d0 100%); border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
539
+ <div style="display: flex; align-items: center;">
540
+ <img src="https://www.shlab.org.cn/static/img/index_14.685f6559.png" alt="Institution Logo" style="height: 60px; margin-right: 20px;">
541
+ <div>
542
+ <h1 style="margin: 0; color: #2c3e50; font-weight: 600;">🤖 InternManip Model Inference Demo</h1>
543
+ <p style="margin: 4px 0 0 0; color: #5d6d7e; font-size: 0.9em;">Model trained on InternManip framework</p>
544
+ </div>
545
+ </div>
546
+ <div style="display: flex; gap: 15px; align-items: center;">
547
+ <a href="https://github.com/InternRobotics" target="_blank" style="text-decoration: none; transition: transform 0.2s;" onmouseover="this.style.transform='scale(1.1)'" onmouseout="this.style.transform='scale(1)'">
548
+ <img src="https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" alt="GitHub" style="height: 30px;">
549
+ </a>
550
+ <a href="https://huggingface.co/InternRobotics" target="_blank" style="text-decoration: none; transition: transform 0.2s;" onmouseover="this.style.transform='scale(1.1)'" onmouseout="this.style.transform='scale(1)'">
551
+ <img src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg" alt="HuggingFace" style="height: 30px;">
552
+ </a>
553
+ <a href="https://huggingface.co/spaces/OpenRobotLab/InternNav-eval-demo" target="_blank">
554
+ <button style="padding: 8px 15px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 500; transition: all 0.2s;"
555
+ onmouseover="this.style.backgroundColor='#2980b9'; this.style.transform='scale(1.05)'"
556
+ onmouseout="this.style.backgroundColor='#3498db'; this.style.transform='scale(1)'">
557
+ Go to InternNav Demo
558
+ </button>
559
+ </a>
560
+ </div>
561
+ </div>
562
+ """
563
+
564
+ ###############################################################################
565
+
566
+
567
+ # 自定义CSS样式
568
+ custom_css = """
569
+ #simulation-panel {
570
+ border-radius: 8px;
571
+ padding: 20px;
572
+ background: #f9f9f9;
573
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
574
+ }
575
+ #result-panel {
576
+ border-radius: 8px;
577
+ padding: 20px;
578
+ background: #f0f8ff;
579
+ }
580
+ .dark #simulation-panel { background: #2a2a2a; }
581
+ .dark #result-panel { background: #1a2a3a; }
582
+
583
+ .history-container {
584
+ max-height: 600px;
585
+ overflow-y: auto;
586
+ margin-top: 20px;
587
+ }
588
+
589
+ .history-accordion {
590
+ margin-bottom: 10px;
591
+ }
592
+
593
+ .logs-container {
594
+ max-height: 500px;
595
+ overflow-y: auto;
596
+ margin-top: 20px;
597
+ padding: 15px;
598
+ background: #f5f5f5;
599
+ border-radius: 8px;
600
+ }
601
+
602
+ .dark .logs-container {
603
+ background: #2a2a2a;
604
+ }
605
+
606
+ .log-table {
607
+ width: 100%;
608
+ border-collapse: collapse;
609
+ }
610
+
611
+ .log-table th, .log-table td {
612
+ padding: 8px 12px;
613
+ border: 1px solid #ddd;
614
+ text-align: left;
615
+ }
616
+
617
+ .dark .log-table th, .dark .log-table td {
618
+ border-color: #444;
619
+ }
620
+ """
621
+
622
+ # 创建Gradio界面
623
+ with gr.Blocks(title="InternManip Model Inference Demo", css=custom_css) as demo:
624
+ gr.HTML(header_html)
625
+
626
+ # # 标题和描述
627
+ # gr.Markdown("""
628
+ # # 🤖 InternManip Model Inference Demo
629
+ # ### Model trained on InternManip framework
630
+ # """)
631
+
632
+ # 存储历史记录的组件变量
633
+ history_state = gr.State([])
634
+
635
+ with gr.Row():
636
+ # 左侧控制面板
637
+ with gr.Column(elem_id="simulation-panel"):
638
+ gr.Markdown("### Simulation Settings")
639
+
640
+ # 场景选择
641
+ scene_dropdown = gr.Dropdown(
642
+ label="Choose a scene",
643
+ choices=list(SCENE_CONFIGS.keys()),
644
+ value="scene_1",
645
+ interactive=True
646
+ )
647
+
648
+ # 场景描述预览
649
+ scene_description = gr.Markdown("")
650
+ scene_preview = gr.Image(
651
+ label="Scene Preview",
652
+ elem_classes=["scene-preview"],
653
+ interactive=False
654
+ )
655
+
656
+ scene_dropdown.change(
657
+ update_scene_display,
658
+ inputs=scene_dropdown,
659
+ outputs=[scene_description, scene_preview]
660
+ )
661
+
662
+ # 操作指令输入
663
+ prompt_input = gr.Textbox(
664
+ label="Manipulation Prompt",
665
+ value="Move the milk carton to the top of the ceramic bowl.",
666
+ placeholder="Example: 'Move the milk carton to the top of the ceramic bowl.'",
667
+ lines=2,
668
+ max_lines=4
669
+ )
670
+
671
+ # 模型选择
672
+ model_dropdown = gr.Dropdown(
673
+ label="Chose a pretrained model",
674
+ choices=MODEL_CHOICES,
675
+ value=MODEL_CHOICES[0]
676
+ )
677
+
678
+ with gr.Accordion("Advance Settings", open=False):
679
+ max_steps = gr.Slider(
680
+ minimum=50,
681
+ maximum=500,
682
+ value=300,
683
+ step=10,
684
+ label="Max Steps"
685
+ )
686
+
687
+ # 提交按钮
688
+ submit_btn = gr.Button("Apply and Start Simulation", variant="primary")
689
+
690
+ # 右侧结果面板
691
+ with gr.Column(elem_id="result-panel"):
692
+ gr.Markdown("### Result")
693
+
694
+ # progress_instruction = gr.Markdown("### Please click the botton on the left column to start.")
695
+
696
+ # 视频输出
697
+ video_output = gr.Video(
698
+ label="Live",
699
+ interactive=False,
700
+ format="mp4",
701
+ autoplay=True,
702
+ streaming=True
703
+ )
704
+
705
+ # 历史记录显示区域
706
+ with gr.Column() as history_container:
707
+ gr.Markdown("### History")
708
+ gr.Markdown("#### History will be reset after refresh")
709
+
710
+ # 预创建10个历史记录槽位
711
+ history_slots = []
712
+ for i in range(10):
713
+ with gr.Column(visible=False) as slot:
714
+ with gr.Accordion(visible=False, open=False) as accordion:
715
+ video = gr.Video(interactive=False) # 用于播放视频
716
+ detail_md = gr.Markdown() # 用于显示详细信息
717
+ history_slots.append((slot, accordion, video, detail_md)) # 存储所有相关组件
718
+
719
+ # 添加日志显示区域
720
+ with gr.Accordion("查看系统访问日志(DEV ONLY)", open=False):
721
+ logs_display = gr.Markdown()
722
+ refresh_logs_btn = gr.Button("刷新日志", variant="secondary")
723
+
724
+ refresh_logs_btn.click(
725
+ update_log_display,
726
+ outputs=logs_display
727
+ )
728
+
729
+ # 示例
730
+ gr.Examples(
731
+ examples=[
732
+ ["scene_1", "Move the milk carton to the top of the ceramic bowl.", "gr1", 300],
733
+ ],
734
+ inputs=[scene_dropdown, prompt_input, model_dropdown, max_steps],
735
+ label="Examples"
736
+ )
737
+
738
+ # 提交处理
739
+ submit_btn.click(
740
+ fn=run_simulation,
741
+ inputs=[scene_dropdown, prompt_input, model_dropdown, max_steps, history_state],
742
+ outputs=[video_output, history_state],
743
+ queue=True
744
+ ).then(
745
+ fn=update_history_display,
746
+ inputs=history_state,
747
+ outputs=[comp for slot in history_slots for comp in slot],
748
+ queue=True
749
+ ).then(
750
+ fn=update_log_display,
751
+ outputs=logs_display
752
+ )
753
+
754
+ # 初始化场景描述和日志
755
+ demo.load(
756
+ fn=lambda: update_scene_display("scene_1"),
757
+ outputs=[scene_description, scene_preview]
758
+ ).then(
759
+ fn=update_log_display,
760
+ outputs=logs_display
761
+ )
762
+
763
+ # 记录访问
764
+ def record_access(request: gr.Request):
765
+ user_ip = request.client.host if request else "unknown"
766
+ user_agent = request.headers.get("user-agent", "unknown")
767
+ log_access(user_ip, user_agent)
768
+ return update_log_display()
769
+
770
+ demo.load(
771
+ fn=record_access,
772
+ inputs=None,
773
+ outputs=logs_display,
774
+ queue=False
775
+ )
776
+
777
+ demo.queue(default_concurrency_limit=8)
778
+
779
+ demo.unload(fn=cleanup_session)
780
+
781
+
782
+ # 启动应用
783
+ if __name__ == "__main__":
784
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ requests
2
+ typing-extensions
3
+ jsonlib-python3
4
+ opencv-python
5
+ numpy
6
+ python-dateutil
scene_1.png DELETED

Git LFS Details

  • SHA256: 552204d8423a9053ab6e7aa79b3466af21415192300989cd3d143938eea322f3
  • Pointer size: 131 Bytes
  • Size of remote file: 301 kB