i2v.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. import os
  2. import time
  3. from pathlib import Path
  4. from typing import Any
  5. import streamlit as st
  6. from loguru import logger
  7. import httpx
  8. from web.i18n import tr, get_language
  9. from web.pipelines.base import PipelineUI, register_pipeline_ui
  10. from web.components.content_input import render_version_info
  11. from web.utils.async_helpers import run_async
  12. from web.utils.streamlit_helpers import check_and_warn_selfhost_workflow
  13. from pixelle_video.config import config_manager
  14. from pixelle_video.utils.os_util import create_task_output_dir
  15. class ImageToVideoPipelineUI(PipelineUI):
  16. """
  17. UI for the Image To Video Video Generation Pipeline.
  18. Generates videos from user-provided assets (images&text).
  19. """
  20. name = "image_to_video"
  21. icon = "🎥"
  22. @property
  23. def display_name(self):
  24. return tr("pipeline.i2v.name")
  25. @property
  26. def description(self):
  27. return tr("pipeline.i2v.description")
  28. def render(self, pixelle_video: Any):
  29. # Two-column layout
  30. left_col,right_col = st.columns([1, 1])
  31. # ====================================================================
  32. # Left Column: Asset Upload
  33. # ====================================================================
  34. with left_col:
  35. asset_params = self.render_audio_visual_input(pixelle_video)
  36. render_version_info()
  37. # ====================================================================
  38. # Right Column: Output Preview
  39. # ====================================================================
  40. with right_col:
  41. video_params = {
  42. **asset_params
  43. }
  44. self._render_output_preview(pixelle_video, video_params)
  45. def render_audio_visual_input(self, pixelle_video) -> dict:
  46. with st.container(border=True):
  47. st.markdown(f"**{tr('i2v.video_generation')}**")
  48. with st.expander(tr("help.feature_description"), expanded=False):
  49. st.markdown(f"**{tr('help.what')}**")
  50. st.markdown(tr("i2v.assets.image_what"))
  51. st.markdown(f"**{tr('help.how')}**")
  52. st.markdown(tr("i2v.assets.how"))
  53. def list_i2v_workflows():
  54. result = []
  55. for source in ("runninghub", "selfhost"):
  56. dir_path = os.path.join("workflows", source)
  57. if not os.path.isdir(dir_path):
  58. continue
  59. for fname in os.listdir(dir_path):
  60. if fname.startswith("i2v_") and fname.endswith(".json"):
  61. display = f"{fname} - {'Runninghub' if source == 'runninghub' else 'Selfhost'}"
  62. result.append({
  63. "key": f"{source}/{fname}",
  64. "display_name": display
  65. })
  66. return result
  67. # File uploader for multiple files
  68. uploaded_files = st.file_uploader(
  69. tr("i2v.assets.upload"),
  70. type=["jpg", "jpeg", "png", "webp"],
  71. accept_multiple_files=True,
  72. help=tr("i2v.assets.upload_help"),
  73. key="material_files"
  74. )
  75. # Save uploaded files to temp directory with unique session ID
  76. audio_asset_paths = []
  77. if uploaded_files:
  78. import uuid
  79. session_id = str(uuid.uuid4()).replace('-', '')[:12]
  80. temp_dir = Path(f"temp/assets_{session_id}")
  81. temp_dir.mkdir(parents=True, exist_ok=True)
  82. for uploaded_file in uploaded_files:
  83. file_path = temp_dir / uploaded_file.name
  84. with open(file_path, "wb") as f:
  85. f.write(uploaded_file.getbuffer())
  86. audio_asset_paths.append(str(file_path.absolute()))
  87. st.success(tr("i2v.assets.character_sucess"))
  88. # Preview uploaded assets
  89. with st.expander(tr("i2v.assets.preview"), expanded=True):
  90. # Show in a grid (3 columns)
  91. cols = st.columns(3)
  92. for i, (file, path) in enumerate(zip(uploaded_files, audio_asset_paths)):
  93. with cols[i % 3]:
  94. # Check if image
  95. ext = Path(path).suffix.lower()
  96. if ext in [".jpg", ".jpeg", ".png", ".webp"]:
  97. st.image(file, caption=file.name, use_container_width=True)
  98. else:
  99. st.info(tr("i2v.assets.character_empty_hint"))
  100. prompt_text = st.text_area(
  101. tr("i2v.input_text"),
  102. placeholder=tr("i2v.input.topic_placeholder"),
  103. height=200,
  104. help=tr("input.text_help_audio"),
  105. key="audio_box"
  106. )
  107. i2v_workflows = list_i2v_workflows()
  108. workflow_options = [wf["display_name"] for wf in i2v_workflows]
  109. workflow_keys = [wf["key"] for wf in i2v_workflows]
  110. default_workflow_index = 0
  111. workflow_display = st.selectbox(
  112. tr("i2v.workflow_select"),
  113. workflow_options if workflow_options else ["No workflow found"],
  114. index=default_workflow_index,
  115. label_visibility="collapsed",
  116. key="i2v_workflow_select"
  117. )
  118. if workflow_options:
  119. workflow_selected_index = workflow_options.index(workflow_display)
  120. workflow_key = workflow_keys[workflow_selected_index]
  121. else:
  122. workflow_key = None
  123. # Check and warn for selfhost workflow (auto popup if not confirmed)
  124. check_and_warn_selfhost_workflow(workflow_key)
  125. return {
  126. "audio_assets": audio_asset_paths,
  127. "prompt_text": prompt_text,
  128. "workflow_key": workflow_key
  129. }
  130. def _render_output_preview(self, pixelle_video: Any, video_params: dict):
  131. """Render output preview section"""
  132. with st.container(border=True):
  133. st.markdown(f"**{tr('section.video_generation')}**")
  134. # Check configuration
  135. if not config_manager.validate():
  136. st.warning(tr("settings.not_configured"))
  137. audio_assets = video_params.get("audio_assets", [])
  138. prompt_text = video_params.get("prompt_text", "")
  139. workflow_key = video_params.get("workflow_key")
  140. logger.info(f" - video_params: {video_params}")
  141. if not audio_assets:
  142. st.info(tr("i2v.assets.image_warning"))
  143. st.button(
  144. tr("btn.generate"),
  145. type="primary",
  146. use_container_width=True,
  147. disabled=True,
  148. key="audio_visual_generate_disabled"
  149. )
  150. return
  151. if not prompt_text:
  152. st.info(tr("i2v.assets.prompt_warning"))
  153. st.button(
  154. tr("btn.generate"),
  155. type="primary",
  156. use_container_width=True,
  157. disabled=True,
  158. key="audio_visual_generate"
  159. )
  160. return
  161. # Generate button
  162. if st.button(tr("btn.generate"), type="primary", use_container_width=True, key="i2v_generate"):
  163. if not config_manager.validate():
  164. st.error(tr("settings.not_configured"))
  165. st.stop()
  166. progress_bar = st.progress(0)
  167. status_text = st.empty()
  168. start_time = time.time()
  169. try:
  170. async def generate_audio_visual_video():
  171. task_dir, task_id = create_task_output_dir()
  172. logger.info(f"[Initialization] Task Directory: {task_dir}")
  173. kit = await pixelle_video._get_or_create_comfykit()
  174. import json
  175. from pathlib import Path
  176. status_text.text(tr("progress.generation"))
  177. progress_bar.progress(10)
  178. image_path = audio_assets[0]
  179. prompt = prompt_text
  180. workflow_path = Path("workflows") / workflow_key
  181. if not workflow_path.exists():
  182. raise Exception(f"The workflow file does not exist: {workflow_path}")
  183. with open(workflow_path, 'r', encoding='utf-8') as f:
  184. workflow_config = json.load(f)
  185. workflow_params = {
  186. "image": image_path,
  187. "prompt": prompt
  188. }
  189. if workflow_config.get("source") == "runninghub" and "workflow_id" in workflow_config:
  190. workflow_input = workflow_config["workflow_id"]
  191. else:
  192. workflow_input = str(workflow_path)
  193. video_result = await kit.execute(workflow_input, workflow_params)
  194. generated_video_url = None
  195. if hasattr(video_result, 'videos') and video_result.videos:
  196. generated_video_url = video_result.videos[0]
  197. elif hasattr(video_result, 'outputs') and video_result.outputs:
  198. for node_id, node_output in video_result.outputs.items():
  199. if isinstance(node_output, dict) and 'videos' in node_output:
  200. videos = node_output['videos']
  201. if videos and len(videos) > 0:
  202. generated_video_url = videos[0]
  203. break
  204. if not generated_video_url:
  205. raise Exception("The workflow did not return a video. Please check the workflow configuration.")
  206. final_video_path = os.path.join(task_dir, "final.mp4")
  207. timeout = httpx.Timeout(300.0)
  208. async with httpx.AsyncClient(timeout=timeout) as client:
  209. response = await client.get(generated_video_url)
  210. response.raise_for_status()
  211. with open(final_video_path, 'wb') as f:
  212. f.write(response.content)
  213. progress_bar.progress(100)
  214. status_text.text(tr("status.success"))
  215. return final_video_path
  216. # Execute async generation
  217. final_video_path = run_async(generate_audio_visual_video())
  218. total_time = time.time() - start_time
  219. progress_bar.progress(100)
  220. status_text.text(tr("status.success"))
  221. # Display result
  222. st.success(tr("status.video_generated", path=final_video_path))
  223. st.markdown("---")
  224. # Video info
  225. if os.path.exists(final_video_path):
  226. file_size_mb = os.path.getsize(final_video_path) / (1024 * 1024)
  227. info_text = (
  228. f"⏱️ {tr('info.generation_time')} {total_time:.1f}s "
  229. f"📦 {file_size_mb:.2f}MB"
  230. )
  231. st.caption(info_text)
  232. st.markdown("---")
  233. # Video preview
  234. st.video(final_video_path)
  235. # Download button
  236. with open(final_video_path, "rb") as video_file:
  237. video_bytes = video_file.read()
  238. video_filename = os.path.basename(final_video_path)
  239. st.download_button(
  240. label="⬇️ 下载视频" if get_language() == "zh_CN" else "⬇️ Download Video",
  241. data=video_bytes,
  242. file_name=video_filename,
  243. mime="video/mp4",
  244. use_container_width=True
  245. )
  246. else:
  247. st.error(tr("status.video_not_found", path=final_video_path))
  248. except Exception as e:
  249. logger.exception(e)
  250. status_text.text("")
  251. progress_bar.empty()
  252. st.error(tr("status.error", error=str(e)))
  253. st.stop()
  254. register_pipeline_ui(ImageToVideoPipelineUI)