action_transfer.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. import os
  2. import time
  3. from pathlib import Path
  4. from typing import Any
  5. from moviepy.editor import VideoFileClip
  6. import streamlit as st
  7. from loguru import logger
  8. import httpx
  9. from web.i18n import tr, get_language
  10. from web.pipelines.base import PipelineUI, register_pipeline_ui
  11. from web.components.content_input import render_version_info
  12. from web.utils.async_helpers import run_async
  13. from web.utils.streamlit_helpers import check_and_warn_selfhost_workflow
  14. from pixelle_video.config import config_manager
  15. from pixelle_video.utils.os_util import create_task_output_dir
  16. class ActionTransferPipelineUI(PipelineUI):
  17. """
  18. UI for the Action transfer Video Generation Pipeline.
  19. Generates videos from user-provided assets (images&text&video).
  20. """
  21. name = "action_transfer"
  22. icon = "💃"
  23. @property
  24. def display_name(self):
  25. return tr("pipeline.action_transfer.name")
  26. @property
  27. def description(self):
  28. return tr("pipeline.action_transfer.description")
  29. def render(self, pixelle_video: Any):
  30. # Three-column layout
  31. left_col,middle_col,right_col = st.columns([1, 1, 1])
  32. # ====================================================================
  33. # Left Column: Video Upload
  34. # ====================================================================
  35. with left_col:
  36. video_params = self.render_action_transfer_video_input(pixelle_video)
  37. render_version_info()
  38. # ====================================================================
  39. # Middle Column: Image Upload & Prompt
  40. # ====================================================================
  41. with middle_col:
  42. assets_params = self.render_action_transfer_assets_input(pixelle_video)
  43. # ====================================================================
  44. # Right Column: Output Preview
  45. # ====================================================================
  46. with right_col:
  47. video_params = {
  48. **video_params,
  49. **assets_params
  50. }
  51. self._render_output_preview(pixelle_video, video_params)
  52. def render_action_transfer_video_input(self, pixelle_video) -> dict:
  53. with st.container(border=True):
  54. st.markdown(f"**{tr('action_transfer.video_upload')}**")
  55. with st.expander(tr("help.feature_description"), expanded=False):
  56. st.markdown(f"**{tr('help.what')}**")
  57. st.markdown(tr("action_transfer.assets.video_what"))
  58. st.markdown(f"**{tr('help.how')}**")
  59. st.markdown(tr("action_transfer.assets.video_how"))
  60. # File uploader for multiple files
  61. uploaded_files = st.file_uploader(
  62. tr("action_transfer.assets.video_upload"),
  63. type=["mp4","mkv","mov"],
  64. accept_multiple_files=True,
  65. help=tr("action_transfer.assets.video_upload_help"),
  66. key="action_reference_files"
  67. )
  68. # Save uploaded files to temp directory with unique session ID
  69. video_asset_paths = []
  70. if uploaded_files:
  71. import uuid
  72. session_id = str(uuid.uuid4()).replace('-', '')[:12]
  73. temp_dir = Path(f"temp/assets_{session_id}")
  74. temp_dir.mkdir(parents=True, exist_ok=True)
  75. for uploaded_file in uploaded_files:
  76. file_path = temp_dir / uploaded_file.name
  77. with open(file_path, "wb") as f:
  78. f.write(uploaded_file.getbuffer())
  79. video_asset_paths.append(str(file_path.absolute()))
  80. st.success(tr("action_transfer.assets.video_sucess"))
  81. # Preview uploaded assets
  82. with st.expander(tr("action_transfer.assets.preview"), expanded=True):
  83. # Show in a grid (3 columns)
  84. cols = st.columns(3)
  85. for i, (file, path) in enumerate(zip(uploaded_files, video_asset_paths)):
  86. with cols[i % 3]:
  87. # Check if image
  88. ext = Path(path).suffix.lower()
  89. if ext in [".mp4", ".mkv", ".mov"]:
  90. st.video(file)
  91. else:
  92. st.info(tr("action_transfer.assets.video_empty_hint"))
  93. # Get the video length (rounded down).
  94. if video_asset_paths:
  95. clip = VideoFileClip(video_asset_paths[0])
  96. int_duration = int(clip.duration)
  97. duration = min(int_duration, 30)
  98. else:
  99. duration = 0
  100. return {
  101. "video_assets": video_asset_paths,
  102. "duration": duration
  103. }
  104. def render_action_transfer_assets_input(self, pixelle_video) -> dict:
  105. with st.container(border=True):
  106. st.markdown(f"**{tr('action_transfer.image_upload')}**")
  107. with st.expander(tr("help.feature_description"), expanded=False):
  108. st.markdown(f"**{tr('help.what')}**")
  109. st.markdown(tr("action_transfer.assets.image_what"))
  110. st.markdown(f"**{tr('help.how')}**")
  111. st.markdown(tr("action_transfer.assets.image_how"))
  112. # File uploader for multiple files
  113. uploaded_files = st.file_uploader(
  114. tr("action_transfer.assets.image_upload"),
  115. type=["jpg", "jpeg", "png", "webp"],
  116. accept_multiple_files=True,
  117. help=tr("action_transfer.assets.image_upload_help"),
  118. key="image_files"
  119. )
  120. # Save uploaded files to temp directory with unique session ID
  121. image_asset_paths = []
  122. if uploaded_files:
  123. import uuid
  124. session_id = str(uuid.uuid4()).replace('-', '')[:12]
  125. temp_dir = Path(f"temp/assets_{session_id}")
  126. temp_dir.mkdir(parents=True, exist_ok=True)
  127. for uploaded_file in uploaded_files:
  128. file_path = temp_dir / uploaded_file.name
  129. with open(file_path, "wb") as f:
  130. f.write(uploaded_file.getbuffer())
  131. image_asset_paths.append(str(file_path.absolute()))
  132. st.success(tr("action_transfer.assets.image_sucess"))
  133. # Preview uploaded assets
  134. with st.expander(tr("action_transfer.assets.preview"), expanded=True):
  135. # Show in a grid (3 columns)
  136. cols = st.columns(3)
  137. for i, (file, path) in enumerate(zip(uploaded_files, image_asset_paths)):
  138. with cols[i % 3]:
  139. # Check if image
  140. ext = Path(path).suffix.lower()
  141. if ext in [".jpg", ".jpeg", ".png", ".webp"]:
  142. st.image(file, caption=file.name, use_container_width=True)
  143. else:
  144. st.info(tr("action_transfer.assets.image_empty_hint"))
  145. def list_action_transfer_workflows():
  146. result = []
  147. for source in ("runninghub", "selfhost"):
  148. dir_path = os.path.join("workflows", source)
  149. if not os.path.isdir(dir_path):
  150. continue
  151. for fname in os.listdir(dir_path):
  152. if fname.startswith("af_") and fname.endswith(".json"):
  153. display = f"{fname} - {'Runninghub' if source == 'runninghub' else 'Selfhost'}"
  154. result.append({
  155. "key": f"{source}/{fname}",
  156. "display_name": display
  157. })
  158. return result
  159. prompt_text = st.text_area(
  160. tr("action_transfer.input_text"),
  161. placeholder=tr("action_transfer.input.topic_placeholder"),
  162. height=200,
  163. help=tr("input.text_help_audio"),
  164. key="prompt_box"
  165. )
  166. transfer_workflows = list_action_transfer_workflows()
  167. workflow_options = [wf["display_name"] for wf in transfer_workflows]
  168. workflow_keys = [wf["key"] for wf in transfer_workflows]
  169. default_workflow_index = 0
  170. workflow_display = st.selectbox(
  171. tr("action_transfer.workflow_select"),
  172. workflow_options if workflow_options else ["No workflow found"],
  173. index=default_workflow_index,
  174. label_visibility="collapsed",
  175. key="action_transfer_workflow_select"
  176. )
  177. if workflow_options:
  178. workflow_selected_index = workflow_options.index(workflow_display)
  179. workflow_key = workflow_keys[workflow_selected_index]
  180. else:
  181. workflow_key = None
  182. # Check and warn for selfhost workflow (auto popup if not confirmed)
  183. check_and_warn_selfhost_workflow(workflow_key)
  184. return {
  185. "image_assets": image_asset_paths,
  186. "prompt_text": prompt_text,
  187. "workflow_key": workflow_key
  188. }
  189. def _render_output_preview(self, pixelle_video: Any, video_params: dict):
  190. """Render output preview section"""
  191. with st.container(border=True):
  192. st.markdown(f"**{tr('section.video_generation')}**")
  193. # Check configuration
  194. if not config_manager.validate():
  195. st.warning(tr("settings.not_configured"))
  196. image_assets = video_params.get("image_assets", [])
  197. video_assets = video_params.get("video_assets", [])
  198. prompt_text = video_params.get("prompt_text", "")
  199. duration = video_params.get("duration")
  200. workflow_key = video_params.get("workflow_key")
  201. logger.info(f" - video_params: {video_params}")
  202. if not video_assets:
  203. st.info(tr("action_transfer.assets.video_warning"))
  204. st.button(
  205. tr("btn.generate"),
  206. type="primary",
  207. use_container_width=True,
  208. disabled=True,
  209. key="action_transfer_generate_video_disabled"
  210. )
  211. return
  212. if not image_assets:
  213. st.info(tr("action_transfer.assets.image_warning"))
  214. st.button(
  215. tr("btn.generate"),
  216. type="primary",
  217. use_container_width=True,
  218. disabled=True,
  219. key="action_transfer_generate_image_disabled"
  220. )
  221. return
  222. if not prompt_text:
  223. st.info(tr("action_transfer.assets.prompt_warning"))
  224. st.button(
  225. tr("btn.generate"),
  226. type="primary",
  227. use_container_width=True,
  228. disabled=True,
  229. key="action_transfer_generate"
  230. )
  231. return
  232. # Generate button
  233. if st.button(tr("btn.generate"), type="primary", use_container_width=True, key="transfer_generate"):
  234. if not config_manager.validate():
  235. st.error(tr("settings.not_configured"))
  236. st.stop()
  237. progress_bar = st.progress(0)
  238. status_text = st.empty()
  239. start_time = time.time()
  240. try:
  241. async def generate_audio_visual_video():
  242. task_dir, task_id = create_task_output_dir()
  243. logger.info(f"[Initialization] Task Directory: {task_dir}")
  244. kit = await pixelle_video._get_or_create_comfykit()
  245. import json
  246. from pathlib import Path
  247. status_text.text(tr("progress.generation"))
  248. progress_bar.progress(10)
  249. image_path = image_assets[0]
  250. video_path = video_assets[0]
  251. second = duration
  252. prompt = prompt_text
  253. workflow_path = Path("workflows") / workflow_key
  254. if not workflow_path.exists():
  255. raise Exception(f"The workflow file does not exist: {workflow_path}")
  256. with open(workflow_path, 'r', encoding='utf-8') as f:
  257. workflow_config = json.load(f)
  258. workflow_params = {
  259. "video": video_path,
  260. "image": image_path,
  261. "prompt": prompt,
  262. "second": second
  263. }
  264. if workflow_config.get("source") == "runninghub" and "workflow_id" in workflow_config:
  265. workflow_input = workflow_config["workflow_id"]
  266. else:
  267. workflow_input = str(workflow_path)
  268. video_result = await kit.execute(workflow_input, workflow_params)
  269. generated_video_url = None
  270. if hasattr(video_result, 'videos') and video_result.videos:
  271. generated_video_url = video_result.videos[0]
  272. elif hasattr(video_result, 'outputs') and video_result.outputs:
  273. for node_id, node_output in video_result.outputs.items():
  274. if isinstance(node_output, dict) and 'videos' in node_output:
  275. videos = node_output['videos']
  276. if videos and len(videos) > 0:
  277. generated_video_url = videos[0]
  278. break
  279. if not generated_video_url:
  280. raise Exception("The workflow did not return a video. Please check the workflow configuration.")
  281. final_video_path = os.path.join(task_dir, "final.mp4")
  282. timeout = httpx.Timeout(300.0)
  283. async with httpx.AsyncClient(timeout=timeout) as client:
  284. response = await client.get(generated_video_url)
  285. response.raise_for_status()
  286. with open(final_video_path, 'wb') as f:
  287. f.write(response.content)
  288. progress_bar.progress(100)
  289. status_text.text(tr("status.success"))
  290. return final_video_path
  291. # Execute async generation
  292. final_video_path = run_async(generate_audio_visual_video())
  293. total_time = time.time() - start_time
  294. progress_bar.progress(100)
  295. status_text.text(tr("status.success"))
  296. # Display result
  297. st.success(tr("status.video_generated", path=final_video_path))
  298. st.markdown("---")
  299. # Video info
  300. if os.path.exists(final_video_path):
  301. file_size_mb = os.path.getsize(final_video_path) / (1024 * 1024)
  302. info_text = (
  303. f"⏱️ {tr('info.generation_time')} {total_time:.1f}s "
  304. f"📦 {file_size_mb:.2f}MB"
  305. )
  306. st.caption(info_text)
  307. st.markdown("---")
  308. # Video preview
  309. st.video(final_video_path)
  310. # Download button
  311. with open(final_video_path, "rb") as video_file:
  312. video_bytes = video_file.read()
  313. video_filename = os.path.basename(final_video_path)
  314. st.download_button(
  315. label="⬇️ 下载视频" if get_language() == "zh_CN" else "⬇️ Download Video",
  316. data=video_bytes,
  317. file_name=video_filename,
  318. mime="video/mp4",
  319. use_container_width=True
  320. )
  321. else:
  322. st.error(tr("status.video_not_found", path=final_video_path))
  323. except Exception as e:
  324. logger.exception(e)
  325. status_text.text("")
  326. progress_bar.empty()
  327. st.error(tr("status.error", error=str(e)))
  328. st.stop()
  329. register_pipeline_ui(ActionTransferPipelineUI)