Files
wehub-resource-sync 690333661e
Deploy Documentation / deploy (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:19:21 +08:00

513 lines
22 KiB
Python

import os
import time
from pathlib import Path
from typing import Any
from moviepy.editor import VideoFileClip
import streamlit as st
from loguru import logger
import httpx
from web.i18n import tr, get_language
from web.pipelines.base import PipelineUI, register_pipeline_ui
from web.pipelines.api_workflows import (
is_api_workflow,
list_api_media_workflows,
list_local_media_workflows,
render_api_video_controls,
workflow_select_help,
workflow_source_help,
workflow_source_label,
)
from web.components.content_input import render_version_info
from web.utils.async_helpers import run_async
from web.utils.history_persistence import save_web_generation_history
from web.utils.streamlit_helpers import check_and_warn_selfhost_workflow
from pixelle_video.config import config_manager
from pixelle_video.utils.os_util import create_task_output_dir
class ActionTransferPipelineUI(PipelineUI):
"""
UI for the Action transfer Video Generation Pipeline.
Generates videos from user-provided assets (images&text&video).
"""
name = "action_transfer"
icon = "💃"
@property
def display_name(self):
return tr("pipeline.action_transfer.name")
@property
def description(self):
return tr("pipeline.action_transfer.description")
def render(self, pixelle_video: Any):
# Three-column layout
left_col,middle_col,right_col = st.columns([1, 1, 1])
# ====================================================================
# Left Column: Video Upload
# ====================================================================
with left_col:
video_params = self.render_action_transfer_video_input(pixelle_video)
render_version_info()
# ====================================================================
# Middle Column: Image Upload & Prompt
# ====================================================================
with middle_col:
assets_params = self.render_action_transfer_assets_input(pixelle_video)
# ====================================================================
# Right Column: Output Preview
# ====================================================================
with right_col:
video_params = {
**video_params,
**assets_params
}
self._render_output_preview(pixelle_video, video_params)
def render_action_transfer_video_input(self, pixelle_video) -> dict:
with st.container(border=True):
st.markdown(f"**{tr('action_transfer.video_upload')}**")
with st.expander(tr("help.feature_description"), expanded=False):
st.markdown(f"**{tr('help.what')}**")
st.markdown(tr("action_transfer.assets.video_what"))
st.markdown(f"**{tr('help.how')}**")
st.markdown(tr("action_transfer.assets.video_how"))
# File uploader for multiple files
uploaded_files = st.file_uploader(
tr("action_transfer.assets.video_upload"),
type=["mp4","mkv","mov"],
accept_multiple_files=True,
help=tr("action_transfer.assets.video_upload_help"),
key="action_reference_files"
)
# Save uploaded files to temp directory with unique session ID
video_asset_paths = []
if uploaded_files:
import uuid
session_id = str(uuid.uuid4()).replace('-', '')[:12]
temp_dir = Path(f"temp/assets_{session_id}")
temp_dir.mkdir(parents=True, exist_ok=True)
for uploaded_file in uploaded_files:
file_path = temp_dir / uploaded_file.name
with open(file_path, "wb") as f:
f.write(uploaded_file.getbuffer())
video_asset_paths.append(str(file_path.absolute()))
st.success(tr("action_transfer.assets.video_sucess"))
# Preview uploaded assets
with st.expander(tr("action_transfer.assets.preview"), expanded=True):
# Show in a grid (3 columns)
cols = st.columns(3)
for i, (file, path) in enumerate(zip(uploaded_files, video_asset_paths)):
with cols[i % 3]:
# Check if image
ext = Path(path).suffix.lower()
if ext in [".mp4", ".mkv", ".mov"]:
st.video(file)
else:
st.info(tr("action_transfer.assets.video_empty_hint"))
# Get the video length (rounded down).
if video_asset_paths:
clip = VideoFileClip(video_asset_paths[0])
int_duration = int(clip.duration)
duration = min(int_duration, 30)
else:
duration = 0
return {
"video_assets": video_asset_paths,
"duration": duration
}
def render_action_transfer_assets_input(self, pixelle_video) -> dict:
with st.container(border=True):
st.markdown(f"**{tr('action_transfer.image_upload')}**")
with st.expander(tr("help.feature_description"), expanded=False):
st.markdown(f"**{tr('help.what')}**")
st.markdown(tr("action_transfer.assets.image_what"))
st.markdown(f"**{tr('help.how')}**")
st.markdown(tr("action_transfer.assets.image_how"))
# File uploader for multiple files
uploaded_files = st.file_uploader(
tr("action_transfer.assets.image_upload"),
type=["jpg", "jpeg", "png", "webp"],
accept_multiple_files=True,
help=tr("action_transfer.assets.image_upload_help"),
key="image_files"
)
# Save uploaded files to temp directory with unique session ID
image_asset_paths = []
if uploaded_files:
import uuid
session_id = str(uuid.uuid4()).replace('-', '')[:12]
temp_dir = Path(f"temp/assets_{session_id}")
temp_dir.mkdir(parents=True, exist_ok=True)
for uploaded_file in uploaded_files:
file_path = temp_dir / uploaded_file.name
with open(file_path, "wb") as f:
f.write(uploaded_file.getbuffer())
image_asset_paths.append(str(file_path.absolute()))
st.success(tr("action_transfer.assets.image_sucess"))
# Preview uploaded assets
with st.expander(tr("action_transfer.assets.preview"), expanded=True):
# Show in a grid (3 columns)
cols = st.columns(3)
for i, (file, path) in enumerate(zip(uploaded_files, image_asset_paths)):
with cols[i % 3]:
# Check if image
ext = Path(path).suffix.lower()
if ext in [".jpg", ".jpeg", ".png", ".webp"]:
st.image(file, caption=file.name, use_container_width=True)
else:
st.info(tr("action_transfer.assets.image_empty_hint"))
def list_action_transfer_workflows():
if workflow_source == "api":
return list_api_media_workflows(
pixelle_video,
"video",
required_adapter_abilities=["action_transfer"],
verified_only=True,
)
return list_local_media_workflows(
pixelle_video,
"video",
workflow_source,
key_prefix="af_",
)
prompt_text = st.text_area(
tr("action_transfer.input_text"),
placeholder=tr("action_transfer.input.topic_placeholder"),
height=200,
help=tr("input.text_help_audio"),
key="prompt_box"
)
source_options = []
if list_local_media_workflows(pixelle_video, "video", "runninghub", key_prefix="af_"):
source_options.append("runninghub")
if list_local_media_workflows(pixelle_video, "video", "selfhost", key_prefix="af_"):
source_options.append("selfhost")
if list_api_media_workflows(
pixelle_video,
"video",
required_adapter_abilities=["action_transfer"],
verified_only=True,
):
source_options.append("api")
if not source_options:
source_options = ["runninghub"]
st.warning(
"没有找到可用的动作迁移工作流或 API 模型。"
if get_language() == "zh_CN"
else "No available action-transfer workflow or API model was found."
)
source_key = "action_transfer_workflow_source"
if st.session_state.get(source_key) not in source_options:
st.session_state.pop(source_key, None)
workflow_source = st.radio(
"生成来源" if get_language() == "zh_CN" else "Generation source",
source_options,
format_func=workflow_source_label,
horizontal=True,
key=source_key,
help=workflow_source_help("动作迁移" if get_language() == "zh_CN" else "action transfer"),
)
transfer_workflows = list_action_transfer_workflows()
if workflow_source != "api" and not transfer_workflows:
st.warning(
"当前来源下没有动作迁移工作流(需要 af_*.json)。"
if get_language() == "zh_CN"
else "No action-transfer workflow is available for this source (requires af_*.json)."
)
if workflow_source == "api" and not transfer_workflows:
st.caption(
"当前已接入的 API 视频模型没有已验证的动作迁移数据契约,暂不展示 API 模型。"
if get_language() == "zh_CN"
else "No verified API action-transfer contract is available yet, so API video models are hidden here."
)
workflow_options = [wf["display_name"] for wf in transfer_workflows]
workflow_keys = [wf["key"] for wf in transfer_workflows]
default_workflow_index = 0
workflow_display = st.selectbox(
tr("action_transfer.workflow_select"),
workflow_options if workflow_options else ["No workflow found"],
index=default_workflow_index,
label_visibility="visible",
key="action_transfer_workflow_select",
help=workflow_select_help(),
)
if workflow_options:
workflow_selected_index = workflow_options.index(workflow_display)
workflow_key = workflow_keys[workflow_selected_index]
workflow_info = transfer_workflows[workflow_selected_index]
else:
workflow_key = None
workflow_info = None
# Check and warn for selfhost workflow (auto popup if not confirmed)
if workflow_key and not is_api_workflow(workflow_key):
check_and_warn_selfhost_workflow(workflow_key)
api_video_params = render_api_video_controls(
workflow_info,
key_prefix="action_transfer",
default_duration=5,
) if is_api_workflow(workflow_key) else {}
return {
"image_assets": image_asset_paths,
"prompt_text": prompt_text,
"workflow_key": workflow_key,
"api_video_params": api_video_params,
}
def _render_output_preview(self, pixelle_video: Any, video_params: dict):
"""Render output preview section"""
with st.container(border=True):
st.markdown(f"**{tr('section.video_generation')}**")
# Check configuration
if not config_manager.validate():
st.warning(tr("settings.not_configured"))
image_assets = video_params.get("image_assets", [])
video_assets = video_params.get("video_assets", [])
prompt_text = video_params.get("prompt_text", "")
duration = video_params.get("duration")
workflow_key = video_params.get("workflow_key")
api_video_params = video_params.get("api_video_params") or {}
logger.info(f" - video_params: {video_params}")
if not video_assets:
st.info(tr("action_transfer.assets.video_warning"))
st.button(
tr("btn.generate"),
type="primary",
use_container_width=True,
disabled=True,
key="action_transfer_generate_video_disabled"
)
return
if not image_assets:
st.info(tr("action_transfer.assets.image_warning"))
st.button(
tr("btn.generate"),
type="primary",
use_container_width=True,
disabled=True,
key="action_transfer_generate_image_disabled"
)
return
if not prompt_text:
st.info(tr("action_transfer.assets.prompt_warning"))
st.button(
tr("btn.generate"),
type="primary",
use_container_width=True,
disabled=True,
key="action_transfer_generate"
)
return
# Generate button
if st.button(tr("btn.generate"), type="primary", use_container_width=True, key="transfer_generate"):
if not config_manager.validate():
st.error(tr("settings.not_configured"))
st.stop()
progress_bar = st.progress(0)
status_text = st.empty()
start_time = time.time()
try:
async def generate_audio_visual_video():
task_dir, task_id = create_task_output_dir()
logger.info(f"[Initialization] Task Directory: {task_dir}")
import json
from pathlib import Path
status_text.text(tr("progress.generation"))
progress_bar.progress(10)
image_path = image_assets[0]
video_path = video_assets[0]
second = duration
prompt = prompt_text
final_video_path = os.path.join(task_dir, "final.mp4")
if is_api_workflow(workflow_key):
media_params = {
**api_video_params,
"prompt": prompt,
"workflow": workflow_key,
"media_type": "video",
"output_path": final_video_path,
"duration": second,
"first_clip_path": video_path,
"reference_image_path": image_path,
}
media_result = await pixelle_video.media(
**media_params,
)
progress_bar.progress(100)
status_text.text(tr("status.success"))
await save_web_generation_history(
pixelle_video,
task_id=task_id,
video_path=media_result.url,
pipeline="action_transfer",
title="动作迁移" if get_language() == "zh_CN" else "Action Transfer",
input_params={
"text": prompt,
"prompt_text": prompt,
"image_assets": image_assets,
"video_assets": video_assets,
"duration": second,
"workflow_key": workflow_key,
"api_video_params": api_video_params,
},
)
return media_result.url
kit = await pixelle_video._get_or_create_comfykit()
workflow_path = Path("workflows") / workflow_key
if not workflow_path.exists():
raise Exception(f"The workflow file does not exist: {workflow_path}")
with open(workflow_path, 'r', encoding='utf-8') as f:
workflow_config = json.load(f)
workflow_params = {
"video": video_path,
"image": image_path,
"prompt": prompt,
"second": second
}
if workflow_config.get("source") == "runninghub" and "workflow_id" in workflow_config:
workflow_input = workflow_config["workflow_id"]
else:
workflow_input = str(workflow_path)
video_result = await kit.execute(workflow_input, workflow_params)
generated_video_url = None
if hasattr(video_result, 'videos') and video_result.videos:
generated_video_url = video_result.videos[0]
elif hasattr(video_result, 'outputs') and video_result.outputs:
for node_id, node_output in video_result.outputs.items():
if isinstance(node_output, dict) and 'videos' in node_output:
videos = node_output['videos']
if videos and len(videos) > 0:
generated_video_url = videos[0]
break
if not generated_video_url:
raise Exception("The workflow did not return a video. Please check the workflow configuration.")
timeout = httpx.Timeout(300.0)
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.get(generated_video_url)
response.raise_for_status()
with open(final_video_path, 'wb') as f:
f.write(response.content)
progress_bar.progress(100)
status_text.text(tr("status.success"))
await save_web_generation_history(
pixelle_video,
task_id=task_id,
video_path=final_video_path,
pipeline="action_transfer",
title="动作迁移" if get_language() == "zh_CN" else "Action Transfer",
input_params={
"text": prompt,
"prompt_text": prompt,
"image_assets": image_assets,
"video_assets": video_assets,
"duration": second,
"workflow_key": workflow_key,
},
)
return final_video_path
# Execute async generation
final_video_path = run_async(generate_audio_visual_video())
total_time = time.time() - start_time
progress_bar.progress(100)
status_text.text(tr("status.success"))
# Display result
st.success(tr("status.video_generated", path=final_video_path))
st.markdown("---")
# Video info
if os.path.exists(final_video_path):
file_size_mb = os.path.getsize(final_video_path) / (1024 * 1024)
info_text = (
f"⏱️ {tr('info.generation_time')} {total_time:.1f}s "
f"📦 {file_size_mb:.2f}MB"
)
st.caption(info_text)
st.markdown("---")
# Video preview
st.video(final_video_path)
# Download button
with open(final_video_path, "rb") as video_file:
video_bytes = video_file.read()
video_filename = os.path.basename(final_video_path)
st.download_button(
label="⬇️ 下载视频" if get_language() == "zh_CN" else "⬇️ Download Video",
data=video_bytes,
file_name=video_filename,
mime="video/mp4",
use_container_width=True
)
else:
st.error(tr("status.video_not_found", path=final_video_path))
except Exception as e:
logger.exception(e)
status_text.text("")
progress_bar.empty()
st.error(tr("status.error", error=str(e)))
st.stop()
register_pipeline_ui(ActionTransferPipelineUI)