import gradio as gr
import os
# 配置视频文件路径和场景信息
SCENES = {
"basketball": {
"ego_query": "videos/basketball/basketball_ego_original.mp4",
"exo_target": "videos/basketball/basketball_exo_original.mp4",
"objects": ["ball", "hoop"]
},
"cooking": {
"ego_query": "videos/cooking/kitchen_ego_original.mp4",
"exo_target": "videos/cooking/kitchen_exo_original.mp4",
"objects": ["food", "knife_block"]
},
"health": {
"ego_query": "videos/health/health_ego_original.mp4",
"exo_target": "videos/health/health_exo_original.mp4",
"objects": ["instruction_manual", "transport_box", "test_strip"]
}
}
def update_scene(scene):
"""更新场景时的回调函数"""
ego_video = SCENES[scene]["ego_query"]
exo_video = SCENES[scene]["exo_target"]
objects = SCENES[scene]["objects"]
# 更新物体选择下拉菜单
object_dropdown = gr.Dropdown(
choices=objects,
value=None,
label="Select Object",
interactive=True
)
return ego_video, exo_video, object_dropdown
def update_videos_with_object(scene, selected_object):
"""选择物体时更新视频"""
if selected_object is None:
# 如果没有选择物体,返回原始视频
ego_video = SCENES[scene]["ego_query"]
exo_video = SCENES[scene]["exo_target"]
else:
# 这里假设有mask视频的命名规则
# 例如:ball_ego_query_mask.mp4, ball_exo_target_mask.mp4
ego_video = f"videos/{scene}/{selected_object}_ego_query_mask.mp4"
exo_video = f"videos/{scene}/{selected_object}_exo_target_mask.mp4"
# 如果mask视频不存在,回退到原始视频
if not os.path.exists(ego_video):
ego_video = SCENES[scene]["ego_query"]
if not os.path.exists(exo_video):
exo_video = SCENES[scene]["exo_target"]
# 返回视频路径
return ego_video, exo_video
def sync_video_playback():
"""JavaScript代码:检测两个视频都加载完毕后同步播放"""
return """
() => {
const egoVideo = document.querySelector('#ego_video video');
const exoVideo = document.querySelector('#exo_video video');
if (!egoVideo || !exoVideo) {
setTimeout(() => {
const egoVideo = document.querySelector('#ego_video video');
const exoVideo = document.querySelector('#exo_video video');
if (egoVideo && exoVideo) {
let egoLoaded = false;
let exoLoaded = false;
const checkBothLoaded = () => {
if (egoLoaded && exoLoaded) {
egoVideo.currentTime = 0;
exoVideo.currentTime = 0;
Promise.all([egoVideo.play(), exoVideo.play()]).then(() => {
console.log('Both videos started playing synchronously');
}).catch(error => console.error('Error playing videos:', error));
}
};
const onEgoLoaded = () => { egoLoaded = true; checkBothLoaded(); };
const onExoLoaded = () => { exoLoaded = true; checkBothLoaded(); };
egoVideo.addEventListener('loadeddata', onEgoLoaded, { once: true });
exoVideo.addEventListener('loadeddata', onExoLoaded, { once: true });
if (egoVideo.readyState >= 2) onEgoLoaded();
if (exoVideo.readyState >= 2) onExoLoaded();
}
}, 100);
return;
}
let egoLoaded = false;
let exoLoaded = false;
const checkBothLoaded = () => {
if (egoLoaded && exoLoaded) {
egoVideo.currentTime = 0;
exoVideo.currentTime = 0;
Promise.all([egoVideo.play(), exoVideo.play()]).then(() => {
console.log('Both videos started playing synchronously');
}).catch(error => console.error('Error playing videos:', error));
}
};
const onEgoLoaded = () => { egoLoaded = true; checkBothLoaded(); };
const onExoLoaded = () => { exoLoaded = true; checkBothLoaded(); };
egoVideo.addEventListener('loadeddata', onEgoLoaded, { once: true });
exoVideo.addEventListener('loadeddata', onExoLoaded, { once: true });
if (egoVideo.readyState >= 2) onEgoLoaded();
if (exoVideo.readyState >= 2) onExoLoaded();
}
"""
def clear_loading_message():
"""清除加载消息"""
return ""
# 创建 Gradio 界面
with gr.Blocks(title="ObjectRelator", theme=gr.themes.Soft()) as demo:
# 标题
gr.Markdown("""
✨ ObjectRelator ✨
👤 Enabling Cross-View Object Relation Understanding Across Ego-Centric and Exo-Centric Perspectives 📹
""", elem_id="title")
# 控制面板
with gr.Row(elem_id="control_panel"):
with gr.Column(scale=1):
scene_dropdown = gr.Dropdown(
choices=list(SCENES.keys()),
value="basketball",
label="Select Scene",
interactive=True,
elem_id="scene_dropdown"
)
with gr.Column(scale=1):
object_dropdown = gr.Dropdown(
choices=SCENES["basketball"]["objects"],
value=None,
label="Select Object",
interactive=True,
elem_id="object_dropdown"
)
# 视频显示区域
with gr.Row(elem_id="video_container"):
with gr.Column():
gr.Markdown("""
👤
Ego View
""")
ego_video = gr.Video(
value=SCENES["basketball"]["ego_query"],
autoplay=False,
loop=True,
show_download_button=False,
show_share_button=False,
elem_id="ego_video"
)
with gr.Column():
gr.Markdown("""
📹
Exo View
""")
exo_video = gr.Video(
value=SCENES["basketball"]["exo_target"],
autoplay=False,
loop=True,
show_download_button=False,
show_share_button=False,
elem_id="exo_video"
)
# 场景选择事件处理
scene_dropdown.change(
fn=update_scene,
inputs=[scene_dropdown],
outputs=[ego_video, exo_video, object_dropdown]
).then(
fn=None,
js=sync_video_playback()
)
# 物体选择事件处理
object_dropdown.change(
fn=update_videos_with_object,
inputs=[scene_dropdown, object_dropdown],
outputs=[ego_video, exo_video]
).then(
fn=None,
js=sync_video_playback()
)
# 页面加载完成后执行同步播放
demo.load(fn=None, js=sync_video_playback())
# 自定义CSS样式和初始JavaScript
demo.css = """
/* 全局样式 */
.gradio-container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
/* 主内容区域 */
.contain {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
padding: 30px;
backdrop-filter: blur(10px);
}
/* 标题样式 */
#title {
margin-bottom: 30px;
}
/* 控制面板样式 */
#control_panel {
margin-bottom: 25px;
padding: 20px;
background: linear-gradient(145deg, #f8f9fa, #e9ecef);
border-radius: 15px;
box-shadow: inset 5px 5px 10px #d1d5db, inset -5px -5px 10px #ffffff;
}
/* 下拉菜单样式 */
#scene_dropdown, #object_dropdown {
margin: 10px;
}
.gr-dropdown {
border-radius: 10px !important;
box-shadow: 0 4px 8px rgba(0,0,0,0.1) !important;
transition: all 0.3s ease !important;
}
.gr-dropdown:hover {
transform: translateY(-2px) !important;
box-shadow: 0 6px 12px rgba(0,0,0,0.15) !important;
}
/* 视频容器样式 */
#video_container {
gap: 30px;
margin-top: 20px;
}
/* 视频样式 */
video {
border-radius: 15px !important;
box-shadow: 0 10px 30px rgba(0,0,0,0.2) !important;
transition: all 0.3s ease !important;
border: 3px solid transparent !important;
background: linear-gradient(white, white) padding-box,
linear-gradient(45deg, #667eea, #764ba2) border-box !important;
}
video:hover {
transform: scale(1.02) !important;
box-shadow: 0 15px 40px rgba(0,0,0,0.3) !important;
}
/* 视频标题样式 */
.gr-markdown h3 {
background: linear-gradient(45deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-weight: bold;
text-shadow: none;
}
/* 响应式设计 */
@media (max-width: 768px) {
.gradio-container {
padding: 10px;
}
.contain {
padding: 20px;
}
#video_container {
flex-direction: column;
}
video {
width: 100% !important;
}
}
/* 加载动画 */
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.6; }
100% { opacity: 1; }
}
.loading {
animation: pulse 1.5s infinite;
}
/* 按钮和交互元素的美化 */
.gr-button {
border-radius: 10px !important;
background: linear-gradient(45deg, #667eea, #764ba2) !important;
border: none !important;
color: white !important;
font-weight: 600 !important;
transition: all 0.3s ease !important;
}
.gr-button:hover {
transform: translateY(-2px) !important;
box-shadow: 0 8px 15px rgba(102, 126, 234, 0.3) !important;
}
"""
if __name__ == "__main__":
demo.launch(share=True)