重构pipeline的UI架构,支持后续pipeline的动态拓展

This commit is contained in:
puke
2025-12-03 11:33:18 +08:00
parent b58f529ce0
commit 6e99612a68
10 changed files with 784 additions and 562 deletions

View File

@@ -18,11 +18,14 @@ Each pipeline implements a specific video generation approach.
""" """
from pixelle_video.pipelines.base import BasePipeline from pixelle_video.pipelines.base import BasePipeline
from pixelle_video.pipelines.linear import LinearVideoPipeline, PipelineContext
from pixelle_video.pipelines.standard import StandardPipeline from pixelle_video.pipelines.standard import StandardPipeline
from pixelle_video.pipelines.custom import CustomPipeline from pixelle_video.pipelines.custom import CustomPipeline
__all__ = [ __all__ = [
"BasePipeline", "BasePipeline",
"LinearVideoPipeline",
"PipelineContext",
"StandardPipeline", "StandardPipeline",
"CustomPipeline", "CustomPipeline",
] ]

View File

@@ -0,0 +1,161 @@
# Copyright (C) 2025 AIDC-AI
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Linear Video Pipeline Base Class
This module defines the template method pattern for linear video generation workflows.
It introduces `PipelineContext` for state management and `LinearVideoPipeline` for
process orchestration.
"""
from dataclasses import dataclass, field
from typing import Optional, List, Dict, Any, Callable
from loguru import logger
from pixelle_video.pipelines.base import BasePipeline
from pixelle_video.models.storyboard import (
Storyboard,
VideoGenerationResult,
StoryboardConfig
)
from pixelle_video.models.progress import ProgressEvent
@dataclass
class PipelineContext:
"""
Context object holding the state of a single pipeline execution.
This object is passed between steps in the LinearVideoPipeline lifecycle.
"""
# === Input ===
input_text: str
params: Dict[str, Any]
progress_callback: Optional[Callable[[ProgressEvent], None]] = None
# === Task State ===
task_id: Optional[str] = None
task_dir: Optional[str] = None
# === Content ===
title: Optional[str] = None
narrations: List[str] = field(default_factory=list)
# === Visuals ===
image_prompts: List[Optional[str]] = field(default_factory=list)
# === Configuration & Storyboard ===
config: Optional[StoryboardConfig] = None
storyboard: Optional[Storyboard] = None
# === Output ===
final_video_path: Optional[str] = None
result: Optional[VideoGenerationResult] = None
class LinearVideoPipeline(BasePipeline):
"""
Base class for linear video generation pipelines using the Template Method pattern.
This class orchestrates the video generation process into distinct lifecycle steps:
1. setup_environment
2. generate_content
3. determine_title
4. plan_visuals
5. initialize_storyboard
6. produce_assets
7. post_production
8. finalize
Subclasses should override specific steps to customize behavior while maintaining
the overall workflow structure.
"""
async def __call__(
self,
text: str,
progress_callback: Optional[Callable[[ProgressEvent], None]] = None,
**kwargs
) -> VideoGenerationResult:
"""
Execute the pipeline using the template method.
"""
# 1. Initialize context
ctx = PipelineContext(
input_text=text,
params=kwargs,
progress_callback=progress_callback
)
try:
# === Phase 1: Preparation ===
await self.setup_environment(ctx)
# === Phase 2: Content Creation ===
await self.generate_content(ctx)
await self.determine_title(ctx)
# === Phase 3: Visual Planning ===
await self.plan_visuals(ctx)
await self.initialize_storyboard(ctx)
# === Phase 4: Asset Production ===
await self.produce_assets(ctx)
# === Phase 5: Post Production ===
await self.post_production(ctx)
# === Phase 6: Finalization ===
return await self.finalize(ctx)
except Exception as e:
await self.handle_exception(ctx, e)
raise
# ==================== Lifecycle Methods ====================
async def setup_environment(self, ctx: PipelineContext):
"""Step 1: Setup task directory and environment."""
pass
async def generate_content(self, ctx: PipelineContext):
"""Step 2: Generate or process script/narrations."""
pass
async def determine_title(self, ctx: PipelineContext):
"""Step 3: Determine or generate video title."""
pass
async def plan_visuals(self, ctx: PipelineContext):
"""Step 4: Generate image prompts or visual descriptions."""
pass
async def initialize_storyboard(self, ctx: PipelineContext):
"""Step 5: Create Storyboard object and frames."""
pass
async def produce_assets(self, ctx: PipelineContext):
"""Step 6: Generate audio, images, and render frames (Core processing)."""
pass
async def post_production(self, ctx: PipelineContext):
"""Step 7: Concatenate videos and add BGM."""
pass
async def finalize(self, ctx: PipelineContext) -> VideoGenerationResult:
"""Step 8: Create result object and persist metadata."""
raise NotImplementedError("finalize must be implemented by subclass")
async def handle_exception(self, ctx: PipelineContext, error: Exception):
"""Handle exceptions during pipeline execution."""
logger.error(f"Pipeline execution failed: {error}")

View File

@@ -15,16 +15,18 @@ Standard Video Generation Pipeline
Standard workflow for generating short videos from topic or fixed script. Standard workflow for generating short videos from topic or fixed script.
This is the default pipeline for general-purpose video generation. This is the default pipeline for general-purpose video generation.
Refactored to use LinearVideoPipeline (Template Method Pattern).
""" """
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Optional, Callable, Literal from typing import Optional, Callable, Literal, List
import asyncio
import shutil
from loguru import logger from loguru import logger
import asyncio
from pixelle_video.pipelines.base import BasePipeline from pixelle_video.pipelines.linear import LinearVideoPipeline, PipelineContext
from pixelle_video.models.progress import ProgressEvent from pixelle_video.models.progress import ProgressEvent
from pixelle_video.models.storyboard import ( from pixelle_video.models.storyboard import (
Storyboard, Storyboard,
@@ -39,13 +41,20 @@ from pixelle_video.utils.content_generators import (
split_narration_script, split_narration_script,
generate_image_prompts, generate_image_prompts,
) )
from pixelle_video.utils.os_util import (
create_task_output_dir,
get_task_final_video_path
)
from pixelle_video.utils.template_util import get_template_type
from pixelle_video.utils.prompt_helper import build_image_prompt
from pixelle_video.services.video import VideoService
# Parallel limit for RunningHub workflows (Call by sequential if set to 1) # Parallel limit for RunningHub workflows (Call by sequential if set to 1)
RUNNING_HUB_PARALLEL_LIMIT = 1 RUNNING_HUB_PARALLEL_LIMIT = 1
class StandardPipeline(BasePipeline): class StandardPipeline(LinearVideoPipeline):
""" """
Standard video generation pipeline Standard video generation pipeline
@@ -66,405 +75,265 @@ class StandardPipeline(BasePipeline):
- "fixed": Use provided script as-is (each line = one narration) - "fixed": Use provided script as-is (each line = one narration)
""" """
async def __call__( # ==================== Lifecycle Methods ====================
self,
# === Input (Required) === async def setup_environment(self, ctx: PipelineContext):
text: str, """Step 1: Setup task directory and environment."""
media_width: int, # Required: Media width (from template) text = ctx.input_text
media_height: int, # Required: Media height (from template) mode = ctx.params.get("mode", "generate")
# === Processing Mode ===
mode: Literal["generate", "fixed"] = "generate",
# === Optional Title ===
title: Optional[str] = None,
# === Basic Config ===
n_scenes: int = 5, # Only used in generate mode; ignored in fixed mode
# === TTS Parameters (supports both old and new parameter names) ===
tts_inference_mode: Optional[str] = None, # "local" or "comfyui" (web UI)
voice_id: Optional[str] = None, # For backward compatibility (deprecated)
tts_voice: Optional[str] = None, # Voice ID for local mode (web UI)
tts_workflow: Optional[str] = None,
tts_speed: float = 1.2,
ref_audio: Optional[str] = None, # Reference audio for voice cloning
output_path: Optional[str] = None,
# === LLM Parameters ===
min_narration_words: int = 5,
max_narration_words: int = 20,
min_image_prompt_words: int = 30,
max_image_prompt_words: int = 60,
# === Media Workflow ===
media_workflow: Optional[str] = None,
# === Video Parameters ===
video_fps: int = 30,
# === Frame Template (determines video size) ===
frame_template: Optional[str] = None,
# === Template Custom Parameters ===
template_params: Optional[dict] = None, # Custom template parameters
# === Image Style ===
prompt_prefix: Optional[str] = None,
# === BGM Parameters ===
bgm_path: Optional[str] = None,
bgm_volume: float = 0.2,
bgm_mode: Literal["once", "loop"] = "loop",
# === Advanced Options ===
content_metadata: Optional[ContentMetadata] = None,
progress_callback: Optional[Callable[[ProgressEvent], None]] = None,
) -> VideoGenerationResult:
"""
Generate short video from text input
Args:
text: Text input (required)
- For generate mode: topic/theme (e.g., "如何提高学习效率")
- For fixed mode: complete narration script (each line is a narration)
mode: Processing mode (default "generate")
- "generate": LLM generates narrations from topic, creates n_scenes
- "fixed": Use existing script as-is, each line becomes a narration
Note: In fixed mode, n_scenes is ignored (uses actual line count)
title: Video title (optional)
- If provided, use it as the video title
- If not provided:
* generate mode → use text as title
* fixed mode → LLM generates title from script
n_scenes: Number of storyboard scenes (default 5)
Only effective in generate mode; ignored in fixed mode
voice_id: TTS voice ID (default "[Chinese] zh-CN Yunjian")
tts_workflow: TTS workflow filename (e.g., "tts_edge.json", None = use default)
tts_speed: TTS speed multiplier (1.0 = normal, 1.2 = 20% faster, default 1.2)
ref_audio: Reference audio path for voice cloning (optional)
output_path: Output video path (auto-generated if None)
min_narration_words: Min narration length (generate mode only)
max_narration_words: Max narration length (generate mode only)
min_image_prompt_words: Min image prompt length
max_image_prompt_words: Max image prompt length
media_width: Media width (image or video, required)
media_height: Media height (image or video, required)
media_workflow: Media workflow filename (image or video, e.g., "image_flux.json", "video_wan.json", None = use default)
video_fps: Video frame rate (default 30)
frame_template: HTML template path with size (None = use default "1080x1920/default.html")
Format: "SIZExSIZE/template.html" (e.g., "1080x1920/default.html", "1920x1080/modern.html")
Video size is automatically determined from template path
template_params: Custom template parameters (optional dict)
e.g., {"accent_color": "#ff0000", "author": "John Doe"}
prompt_prefix: Image prompt prefix (overrides config.yaml if provided)
e.g., "anime style, vibrant colors" or "" for no prefix
bgm_path: BGM path (filename like "default.mp3", custom path, or None)
bgm_volume: BGM volume 0.0-1.0 (default 0.2)
bgm_mode: BGM mode "once" or "loop" (default "loop")
content_metadata: Content metadata (optional, for display)
progress_callback: Progress callback function(ProgressEvent)
Returns:
VideoGenerationResult with video path and metadata
"""
# ========== Step 0: Process text and determine title ==========
logger.info(f"🚀 Starting StandardPipeline in '{mode}' mode") logger.info(f"🚀 Starting StandardPipeline in '{mode}' mode")
logger.info(f" Text length: {len(text)} chars") logger.info(f" Text length: {len(text)} chars")
# Create isolated task directory
task_dir, task_id = create_task_output_dir()
ctx.task_id = task_id
ctx.task_dir = task_dir
logger.info(f"📁 Task directory created: {task_dir}")
logger.info(f" Task ID: {task_id}")
# Determine final video path
output_path = ctx.params.get("output_path")
if output_path is None:
ctx.final_video_path = get_task_final_video_path(task_id)
else:
# We will copy to this path in finalize/post_production
# For internal processing, we still use the task dir path?
# Actually StandardPipeline logic used get_task_final_video_path as the target for concat
# and then copied. Let's stick to that.
ctx.final_video_path = get_task_final_video_path(task_id)
logger.info(f" Will copy final video to: {output_path}")
async def generate_content(self, ctx: PipelineContext):
"""Step 2: Generate or process script/narrations."""
mode = ctx.params.get("mode", "generate")
text = ctx.input_text
n_scenes = ctx.params.get("n_scenes", 5)
min_words = ctx.params.get("min_narration_words", 5)
max_words = ctx.params.get("max_narration_words", 20)
if mode == "generate":
self._report_progress(ctx.progress_callback, "generating_narrations", 0.05)
ctx.narrations = await generate_narrations_from_topic(
self.llm,
topic=text,
n_scenes=n_scenes,
min_words=min_words,
max_words=max_words
)
logger.info(f"✅ Generated {len(ctx.narrations)} narrations")
else: # fixed
self._report_progress(ctx.progress_callback, "splitting_script", 0.05)
ctx.narrations = await split_narration_script(text)
logger.info(f"✅ Split script into {len(ctx.narrations)} segments (by lines)")
logger.info(f" Note: n_scenes={n_scenes} is ignored in fixed mode")
async def determine_title(self, ctx: PipelineContext):
"""Step 3: Determine or generate video title."""
# Note: Swapped order with generate_content in base class call,
# but in StandardPipeline original code, title was determined BEFORE narrations.
# However, LinearVideoPipeline defines generate_content BEFORE determine_title.
# This is fine as they are independent in StandardPipeline logic.
title = ctx.params.get("title")
mode = ctx.params.get("mode", "generate")
text = ctx.input_text
if title:
ctx.title = title
logger.info(f" Title: '{title}' (user-specified)")
else:
self._report_progress(ctx.progress_callback, "generating_title", 0.01)
if mode == "generate":
ctx.title = await generate_title(self.llm, text, strategy="auto")
logger.info(f" Title: '{ctx.title}' (auto-generated)")
else: # fixed
ctx.title = await generate_title(self.llm, text, strategy="llm")
logger.info(f" Title: '{ctx.title}' (LLM-generated)")
async def plan_visuals(self, ctx: PipelineContext):
"""Step 4: Generate image prompts or visual descriptions."""
# Detect template type to determine if media generation is needed
frame_template = ctx.params.get("frame_template") or "1080x1920/default.html"
template_name = Path(frame_template).name
template_type = get_template_type(template_name)
template_requires_media = (template_type in ["image", "video"])
if template_type == "image":
logger.info(f"📸 Template requires image generation")
elif template_type == "video":
logger.info(f"🎬 Template requires video generation")
else: # static
logger.info(f"⚡ Static template - skipping media generation pipeline")
logger.info(f" 💡 Benefits: Faster generation + Lower cost + No ComfyUI dependency")
# Only generate image prompts if template requires media
if template_requires_media:
self._report_progress(ctx.progress_callback, "generating_image_prompts", 0.15)
prompt_prefix = ctx.params.get("prompt_prefix")
min_words = ctx.params.get("min_image_prompt_words", 30)
max_words = ctx.params.get("max_image_prompt_words", 60)
# Override prompt_prefix if provided
original_prefix = None
if prompt_prefix is not None:
image_config = self.core.config.get("comfyui", {}).get("image", {})
original_prefix = image_config.get("prompt_prefix")
image_config["prompt_prefix"] = prompt_prefix
logger.info(f"Using custom prompt_prefix: '{prompt_prefix}'")
try:
# Create progress callback wrapper for image prompt generation
def image_prompt_progress(completed: int, total: int, message: str):
batch_progress = completed / total if total > 0 else 0
overall_progress = 0.15 + (batch_progress * 0.15)
self._report_progress(
ctx.progress_callback,
"generating_image_prompts",
overall_progress,
extra_info=message
)
# Generate base image prompts
base_image_prompts = await generate_image_prompts(
self.llm,
narrations=ctx.narrations,
min_words=min_words,
max_words=max_words,
progress_callback=image_prompt_progress
)
# Apply prompt prefix
image_config = self.core.config.get("comfyui", {}).get("image", {})
prompt_prefix_to_use = prompt_prefix if prompt_prefix is not None else image_config.get("prompt_prefix", "")
ctx.image_prompts = []
for base_prompt in base_image_prompts:
final_prompt = build_image_prompt(base_prompt, prompt_prefix_to_use)
ctx.image_prompts.append(final_prompt)
finally:
# Restore original prompt_prefix
if original_prefix is not None:
image_config["prompt_prefix"] = original_prefix
logger.info(f"✅ Generated {len(ctx.image_prompts)} image prompts")
else:
# Static template - skip image prompt generation entirely
ctx.image_prompts = [None] * len(ctx.narrations)
logger.info(f"⚡ Skipped image prompt generation (static template)")
logger.info(f" 💡 Savings: {len(ctx.narrations)} LLM calls + {len(ctx.narrations)} media generations")
async def initialize_storyboard(self, ctx: PipelineContext):
"""Step 5: Create Storyboard object and frames."""
# === Handle TTS parameter compatibility === # === Handle TTS parameter compatibility ===
# Support both old API (voice_id) and new API (tts_inference_mode + tts_voice) tts_inference_mode = ctx.params.get("tts_inference_mode")
tts_voice = ctx.params.get("tts_voice")
voice_id = ctx.params.get("voice_id")
tts_workflow = ctx.params.get("tts_workflow")
final_voice_id = None final_voice_id = None
final_tts_workflow = tts_workflow final_tts_workflow = tts_workflow
if tts_inference_mode: if tts_inference_mode:
# New API from web UI # New API from web UI
if tts_inference_mode == "local": if tts_inference_mode == "local":
# Local Edge TTS mode - use tts_voice
final_voice_id = tts_voice or "zh-CN-YunjianNeural" final_voice_id = tts_voice or "zh-CN-YunjianNeural"
final_tts_workflow = None # Don't use workflow in local mode final_tts_workflow = None
logger.debug(f"TTS Mode: local (voice={final_voice_id})") logger.debug(f"TTS Mode: local (voice={final_voice_id})")
elif tts_inference_mode == "comfyui": elif tts_inference_mode == "comfyui":
# ComfyUI workflow mode final_voice_id = None
final_voice_id = None # Don't use voice_id in ComfyUI mode
# tts_workflow already set from parameter
logger.debug(f"TTS Mode: comfyui (workflow={final_tts_workflow})") logger.debug(f"TTS Mode: comfyui (workflow={final_tts_workflow})")
else: else:
# Old API (backward compatibility) # Old API
final_voice_id = voice_id or tts_voice or "zh-CN-YunjianNeural" final_voice_id = voice_id or tts_voice or "zh-CN-YunjianNeural"
# tts_workflow already set from parameter
logger.debug(f"TTS Mode: legacy (voice_id={final_voice_id}, workflow={final_tts_workflow})") logger.debug(f"TTS Mode: legacy (voice_id={final_voice_id}, workflow={final_tts_workflow})")
# Determine final title # Create config
if title: ctx.config = StoryboardConfig(
final_title = title task_id=ctx.task_id,
logger.info(f" Title: '{title}' (user-specified)") n_storyboard=len(ctx.narrations), # Use actual length
else: min_narration_words=ctx.params.get("min_narration_words", 5),
self._report_progress(progress_callback, "generating_title", 0.01) max_narration_words=ctx.params.get("max_narration_words", 20),
if mode == "generate": min_image_prompt_words=ctx.params.get("min_image_prompt_words", 30),
final_title = await generate_title(self.llm, text, strategy="auto") max_image_prompt_words=ctx.params.get("max_image_prompt_words", 60),
logger.info(f" Title: '{final_title}' (auto-generated)") video_fps=ctx.params.get("video_fps", 30),
else: # fixed tts_inference_mode=tts_inference_mode or "local",
final_title = await generate_title(self.llm, text, strategy="llm") voice_id=final_voice_id,
logger.info(f" Title: '{final_title}' (LLM-generated)") tts_workflow=final_tts_workflow,
tts_speed=ctx.params.get("tts_speed", 1.2),
# ========== Step 0.5: Create isolated task directory ========== ref_audio=ctx.params.get("ref_audio"),
from pixelle_video.utils.os_util import ( media_width=ctx.params.get("media_width"),
create_task_output_dir, media_height=ctx.params.get("media_height"),
get_task_final_video_path media_workflow=ctx.params.get("media_workflow"),
) frame_template=ctx.params.get("frame_template") or "1080x1920/default.html",
template_params=ctx.params.get("template_params")
task_dir, task_id = create_task_output_dir()
logger.info(f"📁 Task directory created: {task_dir}")
logger.info(f" Task ID: {task_id}")
# Determine final video path
user_specified_output = None
if output_path is None:
output_path = get_task_final_video_path(task_id)
else:
user_specified_output = output_path
output_path = get_task_final_video_path(task_id)
logger.info(f" Will copy final video to: {user_specified_output}")
# Create storyboard config
config = StoryboardConfig(
task_id=task_id,
n_storyboard=n_scenes,
min_narration_words=min_narration_words,
max_narration_words=max_narration_words,
min_image_prompt_words=min_image_prompt_words,
max_image_prompt_words=max_image_prompt_words,
video_fps=video_fps,
tts_inference_mode=tts_inference_mode or "local", # TTS inference mode (CRITICAL FIX)
voice_id=final_voice_id, # Use processed voice_id
tts_workflow=final_tts_workflow, # Use processed workflow
tts_speed=tts_speed,
ref_audio=ref_audio,
media_width=media_width,
media_height=media_height,
media_workflow=media_workflow,
frame_template=frame_template or "1080x1920/default.html",
template_params=template_params # Custom template parameters
) )
# Create storyboard # Create storyboard
storyboard = Storyboard( ctx.storyboard = Storyboard(
title=final_title, title=ctx.title,
config=config, config=ctx.config,
content_metadata=content_metadata, content_metadata=ctx.params.get("content_metadata"),
created_at=datetime.now() created_at=datetime.now()
) )
try: # Create frames
# ========== Step 1: Generate/Split narrations ========== for i, (narration, image_prompt) in enumerate(zip(ctx.narrations, ctx.image_prompts)):
if mode == "generate": frame = StoryboardFrame(
self._report_progress(progress_callback, "generating_narrations", 0.05) index=i,
narrations = await generate_narrations_from_topic( narration=narration,
self.llm, image_prompt=image_prompt,
topic=text, created_at=datetime.now()
n_scenes=n_scenes,
min_words=min_narration_words,
max_words=max_narration_words
)
logger.info(f"✅ Generated {len(narrations)} narrations")
else: # fixed
self._report_progress(progress_callback, "splitting_script", 0.05)
narrations = await split_narration_script(text)
logger.info(f"✅ Split script into {len(narrations)} segments (by lines)")
logger.info(f" Note: n_scenes={n_scenes} is ignored in fixed mode")
# ========== Step 2: Check template type and conditionally generate image prompts ==========
# Detect template type to determine if media generation is needed
from pathlib import Path
from pixelle_video.utils.template_util import get_template_type
template_name = Path(config.frame_template).name
template_type = get_template_type(template_name)
template_requires_media = (template_type in ["image", "video"])
if template_type == "image":
logger.info(f"📸 Template requires image generation")
elif template_type == "video":
logger.info(f"🎬 Template requires video generation")
else: # static
logger.info(f"⚡ Static template - skipping media generation pipeline")
logger.info(f" 💡 Benefits: Faster generation + Lower cost + No ComfyUI dependency")
# Only generate image prompts if template requires media
if template_requires_media:
self._report_progress(progress_callback, "generating_image_prompts", 0.15)
# Override prompt_prefix if provided
original_prefix = None
if prompt_prefix is not None:
image_config = self.core.config.get("comfyui", {}).get("image", {})
original_prefix = image_config.get("prompt_prefix")
image_config["prompt_prefix"] = prompt_prefix
logger.info(f"Using custom prompt_prefix: '{prompt_prefix}'")
try:
# Create progress callback wrapper for image prompt generation
def image_prompt_progress(completed: int, total: int, message: str):
batch_progress = completed / total if total > 0 else 0
overall_progress = 0.15 + (batch_progress * 0.15)
self._report_progress(
progress_callback,
"generating_image_prompts",
overall_progress,
extra_info=message
)
# Generate base image prompts
base_image_prompts = await generate_image_prompts(
self.llm,
narrations=narrations,
min_words=min_image_prompt_words,
max_words=max_image_prompt_words,
progress_callback=image_prompt_progress
)
# Apply prompt prefix
from pixelle_video.utils.prompt_helper import build_image_prompt
image_config = self.core.config.get("comfyui", {}).get("image", {})
prompt_prefix_to_use = prompt_prefix if prompt_prefix is not None else image_config.get("prompt_prefix", "")
image_prompts = []
for base_prompt in base_image_prompts:
final_prompt = build_image_prompt(base_prompt, prompt_prefix_to_use)
image_prompts.append(final_prompt)
finally:
# Restore original prompt_prefix
if original_prefix is not None:
image_config["prompt_prefix"] = original_prefix
logger.info(f"✅ Generated {len(image_prompts)} image prompts")
else:
# Static template - skip image prompt generation entirely
image_prompts = [None] * len(narrations)
logger.info(f"⚡ Skipped image prompt generation (static template)")
logger.info(f" 💡 Savings: {len(narrations)} LLM calls + {len(narrations)} media generations")
# ========== Step 3: Create frames ==========
for i, (narration, image_prompt) in enumerate(zip(narrations, image_prompts)):
frame = StoryboardFrame(
index=i,
narration=narration,
image_prompt=image_prompt,
created_at=datetime.now()
)
storyboard.frames.append(frame)
# ========== Step 4: Process each frame ==========
# Check if using RunningHub workflows for parallel processing
# Enable parallel if either TTS or Image uses RunningHub (most time-consuming parts)
is_runninghub = (
(config.tts_workflow and config.tts_workflow.startswith("runninghub/")) or
(config.media_workflow and config.media_workflow.startswith("runninghub/"))
) )
ctx.storyboard.frames.append(frame)
async def produce_assets(self, ctx: PipelineContext):
"""Step 6: Generate audio, images, and render frames (Core processing)."""
storyboard = ctx.storyboard
config = ctx.config
# Check if using RunningHub workflows for parallel processing
is_runninghub = (
(config.tts_workflow and config.tts_workflow.startswith("runninghub/")) or
(config.media_workflow and config.media_workflow.startswith("runninghub/"))
)
if is_runninghub and RUNNING_HUB_PARALLEL_LIMIT > 1:
logger.info(f"🚀 Using parallel processing for RunningHub workflows (max {RUNNING_HUB_PARALLEL_LIMIT} concurrent)")
if is_runninghub and RUNNING_HUB_PARALLEL_LIMIT > 1: semaphore = asyncio.Semaphore(RUNNING_HUB_PARALLEL_LIMIT)
logger.info(f"🚀 Using parallel processing for RunningHub workflows (max {RUNNING_HUB_PARALLEL_LIMIT} concurrent)") completed_count = 0
logger.info(f" TTS: {'runninghub' if config.tts_workflow and config.tts_workflow.startswith('runninghub/') else 'local'}")
logger.info(f" Media: {'runninghub' if config.media_workflow and config.media_workflow.startswith('runninghub/') else 'local'}") async def process_frame_with_semaphore(i: int, frame: StoryboardFrame):
nonlocal completed_count
semaphore = asyncio.Semaphore(RUNNING_HUB_PARALLEL_LIMIT) async with semaphore:
completed_count = 0
async def process_frame_with_semaphore(i: int, frame: StoryboardFrame):
nonlocal completed_count
async with semaphore:
base_progress = 0.2
frame_range = 0.6
per_frame_progress = frame_range / len(storyboard.frames)
# Create frame-specific progress callback
def frame_progress_callback(event: ProgressEvent):
overall_progress = base_progress + (per_frame_progress * completed_count) + (per_frame_progress * event.progress)
if progress_callback:
adjusted_event = ProgressEvent(
event_type=event.event_type,
progress=overall_progress,
frame_current=i+1,
frame_total=len(storyboard.frames),
step=event.step,
action=event.action
)
progress_callback(adjusted_event)
# Report frame start
self._report_progress(
progress_callback,
"processing_frame",
base_progress + (per_frame_progress * completed_count),
frame_current=i+1,
frame_total=len(storyboard.frames)
)
processed_frame = await self.core.frame_processor(
frame=frame,
storyboard=storyboard,
config=config,
total_frames=len(storyboard.frames),
progress_callback=frame_progress_callback
)
completed_count += 1
logger.info(f"✅ Frame {i+1} completed ({processed_frame.duration:.2f}s) [{completed_count}/{len(storyboard.frames)}]")
return i, processed_frame
# Create all tasks and execute in parallel
tasks = [process_frame_with_semaphore(i, frame) for i, frame in enumerate(storyboard.frames)]
results = await asyncio.gather(*tasks)
# Update frames in order and calculate total duration
for idx, processed_frame in sorted(results, key=lambda x: x[0]):
storyboard.frames[idx] = processed_frame
storyboard.total_duration += processed_frame.duration
logger.info(f"✅ All frames processed in parallel (total duration: {storyboard.total_duration:.2f}s)")
else:
# Serial processing for non-RunningHub workflows
logger.info("⚙️ Using serial processing (non-RunningHub workflow)")
for i, frame in enumerate(storyboard.frames):
base_progress = 0.2 base_progress = 0.2
frame_range = 0.6 frame_range = 0.6
per_frame_progress = frame_range / len(storyboard.frames) per_frame_progress = frame_range / len(storyboard.frames)
# Create frame-specific progress callback # Create frame-specific progress callback
def frame_progress_callback(event: ProgressEvent): def frame_progress_callback(event: ProgressEvent):
overall_progress = base_progress + (per_frame_progress * i) + (per_frame_progress * event.progress) overall_progress = base_progress + (per_frame_progress * completed_count) + (per_frame_progress * event.progress)
if progress_callback: if ctx.progress_callback:
adjusted_event = ProgressEvent( adjusted_event = ProgressEvent(
event_type=event.event_type, event_type=event.event_type,
progress=overall_progress, progress=overall_progress,
frame_current=event.frame_current, frame_current=i+1,
frame_total=event.frame_total, frame_total=len(storyboard.frames),
step=event.step, step=event.step,
action=event.action action=event.action
) )
progress_callback(adjusted_event) ctx.progress_callback(adjusted_event)
# Report frame start # Report frame start
self._report_progress( self._report_progress(
progress_callback, ctx.progress_callback,
"processing_frame", "processing_frame",
base_progress + (per_frame_progress * i), base_progress + (per_frame_progress * completed_count),
frame_current=i+1, frame_current=i+1,
frame_total=len(storyboard.frames) frame_total=len(storyboard.frames)
) )
@@ -476,110 +345,136 @@ class StandardPipeline(BasePipeline):
total_frames=len(storyboard.frames), total_frames=len(storyboard.frames),
progress_callback=frame_progress_callback progress_callback=frame_progress_callback
) )
storyboard.total_duration += processed_frame.duration
logger.info(f"✅ Frame {i+1} completed ({processed_frame.duration:.2f}s)") completed_count += 1
logger.info(f"✅ Frame {i+1} completed ({processed_frame.duration:.2f}s) [{completed_count}/{len(storyboard.frames)}]")
return i, processed_frame
# ========== Step 5: Concatenate videos ========== # Create all tasks and execute in parallel
self._report_progress(progress_callback, "concatenating", 0.85) tasks = [process_frame_with_semaphore(i, frame) for i, frame in enumerate(storyboard.frames)]
segment_paths = [frame.video_segment_path for frame in storyboard.frames] results = await asyncio.gather(*tasks)
from pixelle_video.services.video import VideoService # Update frames in order and calculate total duration
video_service = VideoService() for idx, processed_frame in sorted(results, key=lambda x: x[0]):
storyboard.frames[idx] = processed_frame
storyboard.total_duration += processed_frame.duration
final_video_path = video_service.concat_videos( logger.info(f"✅ All frames processed in parallel (total duration: {storyboard.total_duration:.2f}s)")
videos=segment_paths, else:
output=output_path, # Serial processing for non-RunningHub workflows
bgm_path=bgm_path, logger.info("⚙️ Using serial processing (non-RunningHub workflow)")
bgm_volume=bgm_volume,
bgm_mode=bgm_mode
)
storyboard.final_video_path = final_video_path for i, frame in enumerate(storyboard.frames):
storyboard.completed_at = datetime.now() base_progress = 0.2
frame_range = 0.6
# Copy to user-specified path if provided per_frame_progress = frame_range / len(storyboard.frames)
if user_specified_output:
import shutil # Create frame-specific progress callback
Path(user_specified_output).parent.mkdir(parents=True, exist_ok=True) def frame_progress_callback(event: ProgressEvent):
shutil.copy2(final_video_path, user_specified_output) overall_progress = base_progress + (per_frame_progress * i) + (per_frame_progress * event.progress)
logger.info(f"📹 Final video copied to: {user_specified_output}") if ctx.progress_callback:
final_video_path = user_specified_output adjusted_event = ProgressEvent(
storyboard.final_video_path = user_specified_output event_type=event.event_type,
progress=overall_progress,
logger.success(f"🎬 Video generation completed: {final_video_path}") frame_current=event.frame_current,
frame_total=event.frame_total,
# ========== Step 6: Create result ========== step=event.step,
self._report_progress(progress_callback, "completed", 1.0) action=event.action
)
video_path_obj = Path(final_video_path) ctx.progress_callback(adjusted_event)
file_size = video_path_obj.stat().st_size
# Report frame start
result = VideoGenerationResult( self._report_progress(
video_path=final_video_path, ctx.progress_callback,
storyboard=storyboard, "processing_frame",
duration=storyboard.total_duration, base_progress + (per_frame_progress * i),
file_size=file_size frame_current=i+1,
) frame_total=len(storyboard.frames)
)
logger.info(f"✅ Generated video: {final_video_path}")
logger.info(f" Duration: {storyboard.total_duration:.2f}s") processed_frame = await self.core.frame_processor(
logger.info(f" Size: {file_size / (1024*1024):.2f} MB") frame=frame,
logger.info(f" Frames: {len(storyboard.frames)}") storyboard=storyboard,
config=config,
# ========== Step 7: Persist metadata and storyboard ========== total_frames=len(storyboard.frames),
await self._persist_task_data( progress_callback=frame_progress_callback
storyboard=storyboard, )
result=result, storyboard.total_duration += processed_frame.duration
input_params={ logger.info(f"✅ Frame {i+1} completed ({processed_frame.duration:.2f}s)")
"text": text,
"mode": mode, async def post_production(self, ctx: PipelineContext):
"title": title, """Step 7: Concatenate videos and add BGM."""
"n_scenes": n_scenes, self._report_progress(ctx.progress_callback, "concatenating", 0.85)
"tts_inference_mode": tts_inference_mode,
"tts_voice": tts_voice, storyboard = ctx.storyboard
"voice_id": voice_id, segment_paths = [frame.video_segment_path for frame in storyboard.frames]
"tts_workflow": tts_workflow,
"tts_speed": tts_speed, video_service = VideoService()
"ref_audio": ref_audio,
"media_workflow": media_workflow, final_video_path = video_service.concat_videos(
"prompt_prefix": prompt_prefix, videos=segment_paths,
"frame_template": frame_template, output=ctx.final_video_path,
"template_params": template_params, bgm_path=ctx.params.get("bgm_path"),
"bgm_path": bgm_path, bgm_volume=ctx.params.get("bgm_volume", 0.2),
"bgm_volume": bgm_volume, bgm_mode=ctx.params.get("bgm_mode", "loop")
"bgm_mode": bgm_mode, )
}
) storyboard.final_video_path = final_video_path
storyboard.completed_at = datetime.now()
return result
# Copy to user-specified path if provided
except Exception as e: user_specified_output = ctx.params.get("output_path")
logger.error(f"❌ Video generation failed: {e}") if user_specified_output:
raise Path(user_specified_output).parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(final_video_path, user_specified_output)
async def _persist_task_data( logger.info(f"📹 Final video copied to: {user_specified_output}")
self, ctx.final_video_path = user_specified_output
storyboard: Storyboard, storyboard.final_video_path = user_specified_output
result: VideoGenerationResult,
input_params: dict logger.success(f"🎬 Video generation completed: {ctx.final_video_path}")
):
async def finalize(self, ctx: PipelineContext) -> VideoGenerationResult:
"""Step 8: Create result object and persist metadata."""
self._report_progress(ctx.progress_callback, "completed", 1.0)
video_path_obj = Path(ctx.final_video_path)
file_size = video_path_obj.stat().st_size
result = VideoGenerationResult(
video_path=ctx.final_video_path,
storyboard=ctx.storyboard,
duration=ctx.storyboard.total_duration,
file_size=file_size
)
ctx.result = result
logger.info(f"✅ Generated video: {ctx.final_video_path}")
logger.info(f" Duration: {ctx.storyboard.total_duration:.2f}s")
logger.info(f" Size: {file_size / (1024*1024):.2f} MB")
logger.info(f" Frames: {len(ctx.storyboard.frames)}")
# Persist metadata
await self._persist_task_data(ctx)
return result
async def _persist_task_data(self, ctx: PipelineContext):
""" """
Persist task metadata and storyboard to filesystem Persist task metadata and storyboard to filesystem
Args:
storyboard: Complete storyboard
result: Video generation result
input_params: Input parameters used for generation
""" """
try: try:
storyboard = ctx.storyboard
result = ctx.result
task_id = storyboard.config.task_id task_id = storyboard.config.task_id
if not task_id: if not task_id:
logger.warning("No task_id in storyboard, skipping persistence") logger.warning("No task_id in storyboard, skipping persistence")
return return
# Build metadata # Build metadata
# If user didn't provide a title, use the generated one from storyboard input_with_title = ctx.params.copy()
input_with_title = input_params.copy() input_with_title["text"] = ctx.input_text # Ensure text is included
if not input_with_title.get("title"): if not input_with_title.get("title"):
input_with_title["title"] = storyboard.title input_with_title["title"] = storyboard.title
@@ -617,4 +512,3 @@ class StandardPipeline(BasePipeline):
except Exception as e: except Exception as e:
logger.error(f"Failed to persist task data: {e}") logger.error(f"Failed to persist task data: {e}")
# Don't raise - persistence failure shouldn't break video generation # Don't raise - persistence failure shouldn't break video generation

View File

@@ -3,7 +3,6 @@
"t": { "t": {
"app.title": "⚡ Pixelle-Video - AI Auto Short Video Engine", "app.title": "⚡ Pixelle-Video - AI Auto Short Video Engine",
"app.subtitle": "Powered by Pixelle.AI", "app.subtitle": "Powered by Pixelle.AI",
"section.content_input": "📝 Video Script", "section.content_input": "📝 Video Script",
"section.bgm": "🎵 Background Music", "section.bgm": "🎵 Background Music",
"section.tts": "🎤 Voiceover", "section.tts": "🎤 Voiceover",
@@ -12,29 +11,22 @@
"section.media": "🎨 Media Generation", "section.media": "🎨 Media Generation",
"section.template": "📐 Storyboard Template", "section.template": "📐 Storyboard Template",
"section.video_generation": "🎬 Generate Video", "section.video_generation": "🎬 Generate Video",
"input_mode.topic": "💡 Topic", "input_mode.topic": "💡 Topic",
"input_mode.custom": "✍️ Custom Content", "input_mode.custom": "✍️ Custom Content",
"mode.generate": "💡 AI Creation", "mode.generate": "💡 AI Creation",
"mode.fixed": "✍️ Custom Script", "mode.fixed": "✍️ Custom Script",
"input.topic": "Topic", "input.topic": "Topic",
"input.topic_placeholder": "AI automatically creates specified number of narrations\nExample: How to build passive income, 如何增加被动收入", "input.topic_placeholder": "AI automatically creates specified number of narrations\nExample: How to build passive income, 如何增加被动收入",
"input.topic_help": "Enter a topic, AI will generate content based on it", "input.topic_help": "Enter a topic, AI will generate content based on it",
"input.text": "Text Input", "input.text": "Text Input",
"input.text_help_generate": "Enter topic or theme (AI will create narrations)", "input.text_help_generate": "Enter topic or theme (AI will create narrations)",
"input.text_help_fixed": "Enter complete narration script (used directly without modification, one narration per line)", "input.text_help_fixed": "Enter complete narration script (used directly without modification, one narration per line)",
"input.content": "Content", "input.content": "Content",
"input.content_placeholder": "Used directly without modification, one narration per line\nExample:\nHello everyone, today I'll share three study tips\nThe first tip is focus training, meditate for 10 minutes daily\nThe second tip is active recall, review immediately after learning", "input.content_placeholder": "Used directly without modification, one narration per line\nExample:\nHello everyone, today I'll share three study tips\nThe first tip is focus training, meditate for 10 minutes daily\nThe second tip is active recall, review immediately after learning",
"input.content_help": "Provide your own content for video generation", "input.content_help": "Provide your own content for video generation",
"input.title": "Title (Optional)", "input.title": "Title (Optional)",
"input.title_placeholder": "Video title (auto-generated if empty)", "input.title_placeholder": "Video title (auto-generated if empty)",
"input.title_help": "Optional: Custom title for the video", "input.title_help": "Optional: Custom title for the video",
"voice.title": "🎤 Voice Selection", "voice.title": "🎤 Voice Selection",
"voice.male_professional": "🎤 Male-Professional", "voice.male_professional": "🎤 Male-Professional",
"voice.male_young": "🎙️ Male-Young", "voice.male_young": "🎙️ Male-Young",
@@ -43,7 +35,6 @@
"voice.preview": "▶ Preview Voice", "voice.preview": "▶ Preview Voice",
"voice.previewing": "Generating voice preview...", "voice.previewing": "Generating voice preview...",
"voice.preview_failed": "Preview failed: {error}", "voice.preview_failed": "Preview failed: {error}",
"style.workflow": "Workflow Selection", "style.workflow": "Workflow Selection",
"style.workflow_what": "Determines how each frame's illustration is generated and its effect (e.g., using FLUX, SD models)", "style.workflow_what": "Determines how each frame's illustration is generated and its effect (e.g., using FLUX, SD models)",
"style.workflow_how": "Place the exported image_xxx.json workflow file(API format) into the workflows/selfhost/ folder (for local ComfyUI) or the workflows/runninghub/ folder (for cloud)", "style.workflow_how": "Place the exported image_xxx.json workflow file(API format) into the workflows/selfhost/ folder (for local ComfyUI) or the workflows/runninghub/ folder (for cloud)",
@@ -75,7 +66,6 @@
"style.preview_failed_general": "Failed to generate preview image", "style.preview_failed_general": "Failed to generate preview image",
"style.final_prompt_label": "Final Prompt", "style.final_prompt_label": "Final Prompt",
"style.generated_prompt": "Generated prompt: {prompt}", "style.generated_prompt": "Generated prompt: {prompt}",
"template.selector": "Template Selection", "template.selector": "Template Selection",
"template.select": "Select Template", "template.select": "Select Template",
"template.select_help": "Select template and video size", "template.select_help": "Select template and video size",
@@ -95,7 +85,6 @@
"template.type.static_hint": "Uses template's built-in styles, no AI-generated media required. You can customize background images and other parameters in the template.", "template.type.static_hint": "Uses template's built-in styles, no AI-generated media required. You can customize background images and other parameters in the template.",
"template.type.image_hint": "AI automatically generates illustrations matching the narration content. Image size is determined by the template.", "template.type.image_hint": "AI automatically generates illustrations matching the narration content. Image size is determined by the template.",
"template.type.video_hint": "AI automatically generates video clips matching the narration content. Video size is determined by the template.", "template.type.video_hint": "AI automatically generates video clips matching the narration content. Video size is determined by the template.",
"orientation.portrait": "Portrait", "orientation.portrait": "Portrait",
"orientation.landscape": "Landscape", "orientation.landscape": "Landscape",
"orientation.square": "Square", "orientation.square": "Square",
@@ -114,16 +103,13 @@
"template.preview_image_help": "Supports local path or URL", "template.preview_image_help": "Supports local path or URL",
"template.preview_caption": "Template Preview: {template}", "template.preview_caption": "Template Preview: {template}",
"template.custom_parameters": "Custom Parameters", "template.custom_parameters": "Custom Parameters",
"image.not_required": "Current template does not require image generation", "image.not_required": "Current template does not require image generation",
"image.not_required_hint": "The selected template is text-only and does not need images. Benefits: ⚡ Faster generation 💰 Lower cost", "image.not_required_hint": "The selected template is text-only and does not need images. Benefits: ⚡ Faster generation 💰 Lower cost",
"video.title": "🎬 Video Settings", "video.title": "🎬 Video Settings",
"video.frames": "Scenes", "video.frames": "Scenes",
"video.frames_help": "More scenes = longer video", "video.frames_help": "More scenes = longer video",
"video.frames_label": "Scenes: {n}", "video.frames_label": "Scenes: {n}",
"video.frames_fixed_mode_hint": "💡 Fixed mode: scene count is determined by actual script segments", "video.frames_fixed_mode_hint": "💡 Fixed mode: scene count is determined by actual script segments",
"bgm.selector": "Music Selection", "bgm.selector": "Music Selection",
"bgm.none": "🔇 No BGM", "bgm.none": "🔇 No BGM",
"bgm.volume": "Volume", "bgm.volume": "Volume",
@@ -132,13 +118,11 @@
"bgm.preview_failed": "❌ Music file not found: {file}", "bgm.preview_failed": "❌ Music file not found: {file}",
"bgm.what": "Adds background music to your video, making it more atmospheric and professional", "bgm.what": "Adds background music to your video, making it more atmospheric and professional",
"bgm.how": "Place audio files (MP3/WAV/FLAC, etc.) in the bgm/ folder for automatic detection", "bgm.how": "Place audio files (MP3/WAV/FLAC, etc.) in the bgm/ folder for automatic detection",
"btn.generate": "🎬 Generate Video", "btn.generate": "🎬 Generate Video",
"btn.save_config": "💾 Save Configuration", "btn.save_config": "💾 Save Configuration",
"btn.reset_config": "🔄 Reset to Default", "btn.reset_config": "🔄 Reset to Default",
"btn.save_and_start": "Save and Start", "btn.save_and_start": "Save and Start",
"btn.test_connection": "Test Connection", "btn.test_connection": "Test Connection",
"status.initializing": "🔧 Initializing...", "status.initializing": "🔧 Initializing...",
"status.generating": "🚀 Generating video...", "status.generating": "🚀 Generating video...",
"status.success": "✅ Video generated successfully!", "status.success": "✅ Video generated successfully!",
@@ -149,7 +133,6 @@
"status.config_reset": "✅ Configuration reset to default", "status.config_reset": "✅ Configuration reset to default",
"status.connection_success": "✅ Connected", "status.connection_success": "✅ Connected",
"status.connection_failed": "❌ Connection failed", "status.connection_failed": "❌ Connection failed",
"progress.generating_title": "Generating title...", "progress.generating_title": "Generating title...",
"progress.generating_narrations": "Generating narrations...", "progress.generating_narrations": "Generating narrations...",
"progress.splitting_script": "Splitting script...", "progress.splitting_script": "Splitting script...",
@@ -167,11 +150,9 @@
"progress.concatenating": "Concatenating video...", "progress.concatenating": "Concatenating video...",
"progress.finalizing": "Finalizing...", "progress.finalizing": "Finalizing...",
"progress.completed": "✅ Completed", "progress.completed": "✅ Completed",
"error.input_required": "❌ Please provide topic or content", "error.input_required": "❌ Please provide topic or content",
"error.api_key_required": "❌ Please enter API Key", "error.api_key_required": "❌ Please enter API Key",
"error.missing_field": "Please enter {field}", "error.missing_field": "Please enter {field}",
"info.duration": "Duration", "info.duration": "Duration",
"info.file_size": "File Size", "info.file_size": "File Size",
"info.frames": "Scenes", "info.frames": "Scenes",
@@ -180,7 +161,6 @@
"info.video_information": "📊 Video Information", "info.video_information": "📊 Video Information",
"info.no_video_yet": "Video preview will appear here after generation", "info.no_video_yet": "Video preview will appear here after generation",
"info.generation_time": "Generation Time", "info.generation_time": "Generation Time",
"settings.title": "⚙️ System Configuration (Required)", "settings.title": "⚙️ System Configuration (Required)",
"settings.not_configured": "⚠️ Please complete system configuration before generating videos", "settings.not_configured": "⚠️ Please complete system configuration before generating videos",
"settings.llm.title": "🤖 Large Language Model", "settings.llm.title": "🤖 Large Language Model",
@@ -193,7 +173,6 @@
"settings.llm.base_url_help": "API service address", "settings.llm.base_url_help": "API service address",
"settings.llm.model": "Model", "settings.llm.model": "Model",
"settings.llm.model_help": "Model name", "settings.llm.model_help": "Model name",
"settings.comfyui.title": "🔧 ComfyUI Configuration", "settings.comfyui.title": "🔧 ComfyUI Configuration",
"settings.comfyui.local_title": "Local/Self-hosted ComfyUI", "settings.comfyui.local_title": "Local/Self-hosted ComfyUI",
"settings.comfyui.cloud_title": "RunningHub Cloud", "settings.comfyui.cloud_title": "RunningHub Cloud",
@@ -203,17 +182,14 @@
"settings.comfyui.runninghub_api_key_help": "Visit https://runninghub.ai to register and get API Key", "settings.comfyui.runninghub_api_key_help": "Visit https://runninghub.ai to register and get API Key",
"settings.comfyui.runninghub_hint": "No local ComfyUI? Use RunningHub Cloud:", "settings.comfyui.runninghub_hint": "No local ComfyUI? Use RunningHub Cloud:",
"settings.comfyui.runninghub_get_api_key": "Get RunningHub API Key", "settings.comfyui.runninghub_get_api_key": "Get RunningHub API Key",
"tts.inference_mode": "Synthesis Mode", "tts.inference_mode": "Synthesis Mode",
"tts.mode.local": "Local Synthesis", "tts.mode.local": "Local Synthesis",
"tts.mode.comfyui": "ComfyUI Synthesis", "tts.mode.comfyui": "ComfyUI Synthesis",
"tts.mode.local_hint": "💡 Using Edge TTS, no configuration required, ready to use", "tts.mode.local_hint": "💡 Using Edge TTS, no configuration required, ready to use",
"tts.mode.comfyui_hint": "⚙️ Using ComfyUI workflows, flexible and powerful", "tts.mode.comfyui_hint": "⚙️ Using ComfyUI workflows, flexible and powerful",
"tts.voice_selector": "Voice Selection", "tts.voice_selector": "Voice Selection",
"tts.speed": "Speed", "tts.speed": "Speed",
"tts.speed_label": "{speed}x", "tts.speed_label": "{speed}x",
"tts.voice.zh_CN_XiaoxiaoNeural": "zh-CN-XiaoxiaoNeural", "tts.voice.zh_CN_XiaoxiaoNeural": "zh-CN-XiaoxiaoNeural",
"tts.voice.zh_CN_XiaoyiNeural": "zh-CN-XiaoyiNeural", "tts.voice.zh_CN_XiaoyiNeural": "zh-CN-XiaoyiNeural",
"tts.voice.zh_CN_YunjianNeural": "zh-CN-YunjianNeural", "tts.voice.zh_CN_YunjianNeural": "zh-CN-YunjianNeural",
@@ -228,7 +204,6 @@
"tts.voice.en_US_DavisNeural": "en-US-DavisNeural", "tts.voice.en_US_DavisNeural": "en-US-DavisNeural",
"tts.voice.en_GB_SoniaNeural": "en-GB-SoniaNeural", "tts.voice.en_GB_SoniaNeural": "en-GB-SoniaNeural",
"tts.voice.en_GB_RyanNeural": "en-GB-RyanNeural", "tts.voice.en_GB_RyanNeural": "en-GB-RyanNeural",
"tts.selector": "Workflow Selection", "tts.selector": "Workflow Selection",
"tts.what": "Converts narration text to natural human-like speech (some workflows support reference audio for voice cloning)", "tts.what": "Converts narration text to natural human-like speech (some workflows support reference audio for voice cloning)",
"tts.how": "Place the exported tts_xxx.json workflow file(API format) into the workflows/selfhost/ folder (for local ComfyUI) or the workflows/runninghub/ folder (for cloud)", "tts.how": "Place the exported tts_xxx.json workflow file(API format) into the workflows/selfhost/ folder (for local ComfyUI) or the workflows/runninghub/ folder (for cloud)",
@@ -241,29 +216,21 @@
"tts.previewing": "Generating TTS preview...", "tts.previewing": "Generating TTS preview...",
"tts.preview_success": "✅ Preview generated successfully!", "tts.preview_success": "✅ Preview generated successfully!",
"tts.preview_failed": "❌ Preview failed: {error}", "tts.preview_failed": "❌ Preview failed: {error}",
"welcome.first_time": "🎉 Welcome to Pixelle-Video! Please complete basic configuration", "welcome.first_time": "🎉 Welcome to Pixelle-Video! Please complete basic configuration",
"welcome.config_hint": "💡 First-time setup requires API Key configuration, you can modify it in advanced settings later", "welcome.config_hint": "💡 First-time setup requires API Key configuration, you can modify it in advanced settings later",
"wizard.llm_required": "🤖 Large Language Model Configuration (Required)", "wizard.llm_required": "🤖 Large Language Model Configuration (Required)",
"wizard.image_optional": "🎨 Image Generation Configuration (Optional)", "wizard.image_optional": "🎨 Image Generation Configuration (Optional)",
"wizard.image_hint": "💡 If not configured, default template will be used (no AI image generation)", "wizard.image_hint": "💡 If not configured, default template will be used (no AI image generation)",
"wizard.configure_image": "Configure Image Generation (Recommended)", "wizard.configure_image": "Configure Image Generation (Recommended)",
"label.required": "(Required)", "label.required": "(Required)",
"label.optional": "(Optional)", "label.optional": "(Optional)",
"help.feature_description": "💡 Feature Description", "help.feature_description": "💡 Feature Description",
"help.what": "Purpose", "help.what": "Purpose",
"help.how": "Customization", "help.how": "Customization",
"language.select": "🌐 Language", "language.select": "🌐 Language",
"version.title": "📦 Version Info", "version.title": "📦 Version Info",
"version.current": "Current Version", "version.current": "Current Version",
"github.title": "⭐ Open Source Support", "github.title": "⭐ Open Source Support",
"history.page_title": "📚 Generation History", "history.page_title": "📚 Generation History",
"history.total_tasks": "Total Tasks", "history.total_tasks": "Total Tasks",
"history.completed_count": "Completed", "history.completed_count": "Completed",
@@ -321,7 +288,6 @@
"history.action.delete_success": "✅ Task deleted", "history.action.delete_success": "✅ Task deleted",
"history.action.delete_failed": "❌ Deletion failed: {error}", "history.action.delete_failed": "❌ Deletion failed: {error}",
"history.page_info": "Page {page} / {total_pages}", "history.page_info": "Page {page} / {total_pages}",
"batch.mode_label": "🔢 Batch Generation Mode", "batch.mode_label": "🔢 Batch Generation Mode",
"batch.mode_help": "Generate multiple videos, one topic per line", "batch.mode_help": "Generate multiple videos, one topic per line",
"batch.section_title": "Batch Topics Input", "batch.section_title": "Batch Topics Input",
@@ -364,7 +330,9 @@
"batch.failed_list": "❌ Failed Tasks", "batch.failed_list": "❌ Failed Tasks",
"batch.task": "Task", "batch.task": "Task",
"batch.error": "Error", "batch.error": "Error",
"batch.error_detail": "View detailed error stack" "batch.error_detail": "View detailed error stack",
"pipeline.standard.name": "Standard Video",
"pipeline.demo.name": "Demo Feature",
"pipeline.demo.description": "A demo pipeline with a custom layout"
} }
} }

View File

@@ -3,7 +3,6 @@
"t": { "t": {
"app.title": "⚡ Pixelle-Video - AI 全自动短视频引擎", "app.title": "⚡ Pixelle-Video - AI 全自动短视频引擎",
"app.subtitle": "Pixelle.AI 提供支持", "app.subtitle": "Pixelle.AI 提供支持",
"section.content_input": "📝 视频脚本", "section.content_input": "📝 视频脚本",
"section.bgm": "🎵 背景音乐", "section.bgm": "🎵 背景音乐",
"section.tts": "🎤 配音合成", "section.tts": "🎤 配音合成",
@@ -12,29 +11,22 @@
"section.media": "🎨 媒体生成", "section.media": "🎨 媒体生成",
"section.template": "📐 分镜模板", "section.template": "📐 分镜模板",
"section.video_generation": "🎬 生成视频", "section.video_generation": "🎬 生成视频",
"input_mode.topic": "💡 主题", "input_mode.topic": "💡 主题",
"input_mode.custom": "✍️ 自定义内容", "input_mode.custom": "✍️ 自定义内容",
"mode.generate": "💡 AI 创作", "mode.generate": "💡 AI 创作",
"mode.fixed": "✍️ 自行创作", "mode.fixed": "✍️ 自行创作",
"input.topic": "主题", "input.topic": "主题",
"input.topic_placeholder": "AI 自动创作指定数量的旁白\n例如如何增加被动收入、How to build passive income", "input.topic_placeholder": "AI 自动创作指定数量的旁白\n例如如何增加被动收入、How to build passive income",
"input.topic_help": "输入一个主题AI 将根据主题生成内容", "input.topic_help": "输入一个主题AI 将根据主题生成内容",
"input.text": "文本输入", "input.text": "文本输入",
"input.text_help_generate": "输入主题或话题AI 将创作旁白)", "input.text_help_generate": "输入主题或话题AI 将创作旁白)",
"input.text_help_fixed": "输入完整的旁白脚本(直接使用,不做改写,每行一个旁白)", "input.text_help_fixed": "输入完整的旁白脚本(直接使用,不做改写,每行一个旁白)",
"input.content": "内容", "input.content": "内容",
"input.content_placeholder": "直接使用,不做改写,每行一个旁白\n例如\n大家好今天跟你分享三个学习技巧\n第一个技巧是专注力训练每天冥想10分钟\n第二个技巧是主动回忆学完立即复述", "input.content_placeholder": "直接使用,不做改写,每行一个旁白\n例如\n大家好今天跟你分享三个学习技巧\n第一个技巧是专注力训练每天冥想10分钟\n第二个技巧是主动回忆学完立即复述",
"input.content_help": "提供您自己的内容用于视频生成", "input.content_help": "提供您自己的内容用于视频生成",
"input.title": "标题(可选)", "input.title": "标题(可选)",
"input.title_placeholder": "视频标题(留空则自动生成)", "input.title_placeholder": "视频标题(留空则自动生成)",
"input.title_help": "可选:自定义视频标题", "input.title_help": "可选:自定义视频标题",
"voice.title": "🎤 语音选择", "voice.title": "🎤 语音选择",
"voice.male_professional": "🎤 男声-专业", "voice.male_professional": "🎤 男声-专业",
"voice.male_young": "🎙️ 男声-年轻", "voice.male_young": "🎙️ 男声-年轻",
@@ -43,7 +35,6 @@
"voice.preview": "▶ 试听语音", "voice.preview": "▶ 试听语音",
"voice.previewing": "正在生成语音预览...", "voice.previewing": "正在生成语音预览...",
"voice.preview_failed": "预览失败:{error}", "voice.preview_failed": "预览失败:{error}",
"style.workflow": "工作流选择", "style.workflow": "工作流选择",
"style.workflow_what": "决定视频中每帧插图的生成方式和效果(如使用 FLUX、SD 等模型)", "style.workflow_what": "决定视频中每帧插图的生成方式和效果(如使用 FLUX、SD 等模型)",
"style.workflow_how": "将导出的 image_xxx.json 工作流文件API格式放入 workflows/selfhost/(本地 ComfyUI或 workflows/runninghub/(云端)文件夹", "style.workflow_how": "将导出的 image_xxx.json 工作流文件API格式放入 workflows/selfhost/(本地 ComfyUI或 workflows/runninghub/(云端)文件夹",
@@ -75,7 +66,6 @@
"style.preview_failed_general": "预览图片生成失败", "style.preview_failed_general": "预览图片生成失败",
"style.final_prompt_label": "最终提示词", "style.final_prompt_label": "最终提示词",
"style.generated_prompt": "生成的提示词:{prompt}", "style.generated_prompt": "生成的提示词:{prompt}",
"template.selector": "模板选择", "template.selector": "模板选择",
"template.select": "选择模板", "template.select": "选择模板",
"template.select_help": "选择模板和视频尺寸", "template.select_help": "选择模板和视频尺寸",
@@ -95,7 +85,6 @@
"template.type.static_hint": "使用模板自带样式无需AI生成媒体。可在模板中自定义背景图片等参数。", "template.type.static_hint": "使用模板自带样式无需AI生成媒体。可在模板中自定义背景图片等参数。",
"template.type.image_hint": "AI自动根据文案内容生成与之匹配的插图插图尺寸由模板决定。", "template.type.image_hint": "AI自动根据文案内容生成与之匹配的插图插图尺寸由模板决定。",
"template.type.video_hint": "AI自动根据文案内容生成与之匹配的视频片段视频尺寸由模板决定。", "template.type.video_hint": "AI自动根据文案内容生成与之匹配的视频片段视频尺寸由模板决定。",
"orientation.portrait": "竖屏", "orientation.portrait": "竖屏",
"orientation.landscape": "横屏", "orientation.landscape": "横屏",
"orientation.square": "方形", "orientation.square": "方形",
@@ -114,16 +103,13 @@
"template.preview_image_help": "支持本地路径或 URL", "template.preview_image_help": "支持本地路径或 URL",
"template.preview_caption": "模板预览:{template}", "template.preview_caption": "模板预览:{template}",
"template.custom_parameters": "自定义参数", "template.custom_parameters": "自定义参数",
"image.not_required": "当前模板不需要插图生成", "image.not_required": "当前模板不需要插图生成",
"image.not_required_hint": "您选择的模板是纯文本模板,无需生成图片。这将:⚡ 加快生成速度 💰 降低生成成本", "image.not_required_hint": "您选择的模板是纯文本模板,无需生成图片。这将:⚡ 加快生成速度 💰 降低生成成本",
"video.title": "🎬 视频设置", "video.title": "🎬 视频设置",
"video.frames": "分镜数", "video.frames": "分镜数",
"video.frames_help": "更多分镜 = 更长视频", "video.frames_help": "更多分镜 = 更长视频",
"video.frames_label": "分镜数:{n}", "video.frames_label": "分镜数:{n}",
"video.frames_fixed_mode_hint": "💡 固定模式:分镜数由脚本实际段落数决定", "video.frames_fixed_mode_hint": "💡 固定模式:分镜数由脚本实际段落数决定",
"bgm.selector": "音乐选择", "bgm.selector": "音乐选择",
"bgm.none": "🔇 无背景音乐", "bgm.none": "🔇 无背景音乐",
"bgm.volume": "音量", "bgm.volume": "音量",
@@ -132,13 +118,11 @@
"bgm.preview_failed": "❌ 音乐文件未找到:{file}", "bgm.preview_failed": "❌ 音乐文件未找到:{file}",
"bgm.what": "为视频添加背景音乐,让视频更有氛围感和专业性", "bgm.what": "为视频添加背景音乐,让视频更有氛围感和专业性",
"bgm.how": "将音频文件MP3/WAV/FLAC 等)放入 bgm/ 文件夹即可自动识别", "bgm.how": "将音频文件MP3/WAV/FLAC 等)放入 bgm/ 文件夹即可自动识别",
"btn.generate": "🎬 生成视频", "btn.generate": "🎬 生成视频",
"btn.save_config": "💾 保存配置", "btn.save_config": "💾 保存配置",
"btn.reset_config": "🔄 重置默认", "btn.reset_config": "🔄 重置默认",
"btn.save_and_start": "保存并开始", "btn.save_and_start": "保存并开始",
"btn.test_connection": "测试连接", "btn.test_connection": "测试连接",
"status.initializing": "🔧 正在初始化...", "status.initializing": "🔧 正在初始化...",
"status.generating": "🚀 正在生成视频...", "status.generating": "🚀 正在生成视频...",
"status.success": "✅ 视频生成成功!", "status.success": "✅ 视频生成成功!",
@@ -149,7 +133,6 @@
"status.config_reset": "✅ 配置已重置为默认值", "status.config_reset": "✅ 配置已重置为默认值",
"status.connection_success": "✅ 连接成功", "status.connection_success": "✅ 连接成功",
"status.connection_failed": "❌ 连接失败", "status.connection_failed": "❌ 连接失败",
"progress.generating_title": "生成标题...", "progress.generating_title": "生成标题...",
"progress.generating_narrations": "生成旁白...", "progress.generating_narrations": "生成旁白...",
"progress.splitting_script": "切分脚本...", "progress.splitting_script": "切分脚本...",
@@ -167,11 +150,9 @@
"progress.concatenating": "正在拼接视频...", "progress.concatenating": "正在拼接视频...",
"progress.finalizing": "完成中...", "progress.finalizing": "完成中...",
"progress.completed": "✅ 生成完成", "progress.completed": "✅ 生成完成",
"error.input_required": "❌ 请提供主题或内容", "error.input_required": "❌ 请提供主题或内容",
"error.api_key_required": "❌ 请填写 API Key", "error.api_key_required": "❌ 请填写 API Key",
"error.missing_field": "请填写 {field}", "error.missing_field": "请填写 {field}",
"info.duration": "时长", "info.duration": "时长",
"info.file_size": "文件大小", "info.file_size": "文件大小",
"info.frames": "分镜数", "info.frames": "分镜数",
@@ -180,7 +161,6 @@
"info.video_information": "📊 视频信息", "info.video_information": "📊 视频信息",
"info.no_video_yet": "生成视频后,预览将显示在这里", "info.no_video_yet": "生成视频后,预览将显示在这里",
"info.generation_time": "生成耗时", "info.generation_time": "生成耗时",
"settings.title": "⚙️ 系统配置(必需)", "settings.title": "⚙️ 系统配置(必需)",
"settings.not_configured": "⚠️ 请先完成系统配置才能生成视频", "settings.not_configured": "⚠️ 请先完成系统配置才能生成视频",
"settings.llm.title": "🤖 大语言模型", "settings.llm.title": "🤖 大语言模型",
@@ -193,7 +173,6 @@
"settings.llm.base_url_help": "API 服务地址", "settings.llm.base_url_help": "API 服务地址",
"settings.llm.model": "Model", "settings.llm.model": "Model",
"settings.llm.model_help": "模型名称", "settings.llm.model_help": "模型名称",
"settings.comfyui.title": "🔧 ComfyUI 配置", "settings.comfyui.title": "🔧 ComfyUI 配置",
"settings.comfyui.local_title": "本地/自建 ComfyUI", "settings.comfyui.local_title": "本地/自建 ComfyUI",
"settings.comfyui.cloud_title": "RunningHub 云端", "settings.comfyui.cloud_title": "RunningHub 云端",
@@ -203,17 +182,14 @@
"settings.comfyui.runninghub_api_key_help": "访问 https://runninghub.ai 注册并获取 API Key", "settings.comfyui.runninghub_api_key_help": "访问 https://runninghub.ai 注册并获取 API Key",
"settings.comfyui.runninghub_hint": "没有本地 ComfyUI可用 RunningHub 云端:", "settings.comfyui.runninghub_hint": "没有本地 ComfyUI可用 RunningHub 云端:",
"settings.comfyui.runninghub_get_api_key": "点此获取 RunningHub API Key", "settings.comfyui.runninghub_get_api_key": "点此获取 RunningHub API Key",
"tts.inference_mode": "合成方式", "tts.inference_mode": "合成方式",
"tts.mode.local": "本地合成", "tts.mode.local": "本地合成",
"tts.mode.comfyui": "ComfyUI 合成", "tts.mode.comfyui": "ComfyUI 合成",
"tts.mode.local_hint": "💡 使用 Edge TTS无需配置开箱即用请确保网络环境可用", "tts.mode.local_hint": "💡 使用 Edge TTS无需配置开箱即用请确保网络环境可用",
"tts.mode.comfyui_hint": "⚙️ 使用 ComfyUI 工作流,灵活强大", "tts.mode.comfyui_hint": "⚙️ 使用 ComfyUI 工作流,灵活强大",
"tts.voice_selector": "音色选择", "tts.voice_selector": "音色选择",
"tts.speed": "语速", "tts.speed": "语速",
"tts.speed_label": "{speed}x", "tts.speed_label": "{speed}x",
"tts.voice.zh_CN_XiaoxiaoNeural": "女声-温柔(晓晓)", "tts.voice.zh_CN_XiaoxiaoNeural": "女声-温柔(晓晓)",
"tts.voice.zh_CN_XiaoyiNeural": "女声-甜美(晓伊)", "tts.voice.zh_CN_XiaoyiNeural": "女声-甜美(晓伊)",
"tts.voice.zh_CN_YunjianNeural": "男声-专业(云健)", "tts.voice.zh_CN_YunjianNeural": "男声-专业(云健)",
@@ -228,7 +204,6 @@
"tts.voice.en_US_DavisNeural": "男声-友好Davis", "tts.voice.en_US_DavisNeural": "男声-友好Davis",
"tts.voice.en_GB_SoniaNeural": "女声-英式Sonia", "tts.voice.en_GB_SoniaNeural": "女声-英式Sonia",
"tts.voice.en_GB_RyanNeural": "男声-英式Ryan", "tts.voice.en_GB_RyanNeural": "男声-英式Ryan",
"tts.selector": "工作流选择", "tts.selector": "工作流选择",
"tts.what": "将旁白文本转换为真人般的自然语音(部分工作流支持参考音频克隆声音)", "tts.what": "将旁白文本转换为真人般的自然语音(部分工作流支持参考音频克隆声音)",
"tts.how": "将导出的 tts_xxx.json 工作流文件API格式放入 workflows/selfhost/(本地 ComfyUI或 workflows/runninghub/(云端)文件夹", "tts.how": "将导出的 tts_xxx.json 工作流文件API格式放入 workflows/selfhost/(本地 ComfyUI或 workflows/runninghub/(云端)文件夹",
@@ -241,29 +216,21 @@
"tts.previewing": "正在生成 TTS 预览...", "tts.previewing": "正在生成 TTS 预览...",
"tts.preview_success": "✅ 预览生成成功!", "tts.preview_success": "✅ 预览生成成功!",
"tts.preview_failed": "❌ 预览失败:{error}", "tts.preview_failed": "❌ 预览失败:{error}",
"welcome.first_time": "🎉 欢迎使用 Pixelle-Video请先完成基础配置", "welcome.first_time": "🎉 欢迎使用 Pixelle-Video请先完成基础配置",
"welcome.config_hint": "💡 首次使用需要配置 API Key后续可以在高级设置中修改", "welcome.config_hint": "💡 首次使用需要配置 API Key后续可以在高级设置中修改",
"wizard.llm_required": "🤖 大语言模型配置(必需)", "wizard.llm_required": "🤖 大语言模型配置(必需)",
"wizard.image_optional": "🎨 图像生成配置(可选)", "wizard.image_optional": "🎨 图像生成配置(可选)",
"wizard.image_hint": "💡 如果不配置图像生成,将使用默认模板(无 AI 生图)", "wizard.image_hint": "💡 如果不配置图像生成,将使用默认模板(无 AI 生图)",
"wizard.configure_image": "配置图像生成(推荐)", "wizard.configure_image": "配置图像生成(推荐)",
"label.required": "(必需)", "label.required": "(必需)",
"label.optional": "(可选)", "label.optional": "(可选)",
"help.feature_description": "💡 功能说明", "help.feature_description": "💡 功能说明",
"help.what": "作用", "help.what": "作用",
"help.how": "自定义方式", "help.how": "自定义方式",
"language.select": "🌐 语言", "language.select": "🌐 语言",
"version.title": "📦 版本信息", "version.title": "📦 版本信息",
"version.current": "当前版本", "version.current": "当前版本",
"github.title": "⭐ 开源支持", "github.title": "⭐ 开源支持",
"history.page_title": "📚 生成历史", "history.page_title": "📚 生成历史",
"history.total_tasks": "总任务数", "history.total_tasks": "总任务数",
"history.completed_count": "已完成", "history.completed_count": "已完成",
@@ -321,7 +288,6 @@
"history.action.delete_success": "✅ 任务已删除", "history.action.delete_success": "✅ 任务已删除",
"history.action.delete_failed": "❌ 删除失败:{error}", "history.action.delete_failed": "❌ 删除失败:{error}",
"history.page_info": "第 {page} 页 / 共 {total_pages} 页", "history.page_info": "第 {page} 页 / 共 {total_pages} 页",
"batch.mode_label": "🔢 批量生成模式", "batch.mode_label": "🔢 批量生成模式",
"batch.mode_help": "批量生成多个视频,每行一个主题", "batch.mode_help": "批量生成多个视频,每行一个主题",
"batch.section_title": "批量主题输入", "batch.section_title": "批量主题输入",
@@ -364,7 +330,9 @@
"batch.failed_list": "❌ 失败的任务", "batch.failed_list": "❌ 失败的任务",
"batch.task": "任务", "batch.task": "任务",
"batch.error": "错误信息", "batch.error": "错误信息",
"batch.error_detail": "查看详细错误堆栈" "batch.error_detail": "查看详细错误堆栈",
"pipeline.standard.name": "标准视频",
"pipeline.demo.name": "演示功能",
"pipeline.demo.description": "具有自定义布局的演示 Pipeline"
} }
} }

View File

@@ -31,9 +31,6 @@ from web.state.session import init_session_state, init_i18n, get_pixelle_video
# Import components # Import components
from web.components.header import render_header from web.components.header import render_header
from web.components.settings import render_advanced_settings from web.components.settings import render_advanced_settings
from web.components.content_input import render_content_input, render_bgm_section, render_version_info
from web.components.style_config import render_style_config
from web.components.output_preview import render_output_preview
# Page config # Page config
st.set_page_config( st.set_page_config(
@@ -59,42 +56,28 @@ def main():
# Render system configuration (LLM + ComfyUI) # Render system configuration (LLM + ComfyUI)
render_advanced_settings() render_advanced_settings()
# Three-column layout # ========================================================================
left_col, middle_col, right_col = st.columns([1, 1, 1]) # Pipeline Selection & Delegation
# ========================================================================
from web.pipelines import get_all_pipeline_uis
# ======================================================================== # Get all registered pipelines
# Left Column: Content Input & BGM pipelines = get_all_pipeline_uis()
# ========================================================================
with left_col:
# Content input (mode, text, title, n_scenes)
content_params = render_content_input()
# BGM selection (bgm_path, bgm_volume)
bgm_params = render_bgm_section()
# Version info & GitHub link
render_version_info()
# ======================================================================== # Use Tabs for pipeline selection
# Middle Column: Style Configuration # Note: st.tabs returns a list of containers, one for each tab
# ======================================================================== tab_labels = [f"{p.icon} {p.display_name}" for p in pipelines]
with middle_col: tabs = st.tabs(tab_labels)
# Style configuration (TTS, template, workflow, etc.)
style_params = render_style_config(pixelle_video)
# ======================================================================== # Render each pipeline in its corresponding tab
# Right Column: Output Preview for i, pipeline in enumerate(pipelines):
# ======================================================================== with tabs[i]:
with right_col: # Show description if available
# Combine all parameters if pipeline.description:
video_params = { st.caption(pipeline.description)
**content_params,
**bgm_params, # Delegate rendering
**style_params pipeline.render(pixelle_video)
}
# Render output preview (generate button, progress, video preview)
render_output_preview(pixelle_video, video_params)
if __name__ == "__main__": if __name__ == "__main__":

35
web/pipelines/__init__.py Normal file
View File

@@ -0,0 +1,35 @@
# Copyright (C) 2025 AIDC-AI
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Pipeline UI Package
Exports registry functions and automatically registers available pipelines.
"""
from web.pipelines.base import (
PipelineUI,
register_pipeline_ui,
get_pipeline_ui,
get_all_pipeline_uis
)
# Import all pipeline UI modules to ensure they register themselves
from web.pipelines import standard
from web.pipelines import demo
__all__ = [
"PipelineUI",
"register_pipeline_ui",
"get_pipeline_ui",
"get_all_pipeline_uis"
]

57
web/pipelines/base.py Normal file
View File

@@ -0,0 +1,57 @@
# Copyright (C) 2025 AIDC-AI
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Pipeline UI Base & Registry
Defines the PipelineUI protocol and the registration mechanism.
"""
from typing import Dict, Any, List, Type
class PipelineUI:
"""
Base class for Pipeline UI plugins.
Each pipeline should implement a subclass to define its own full-page UI.
"""
name: str = "base"
display_name: str = "Base Pipeline"
icon: str = "🔌"
description: str = ""
def render(self, pixelle_video: Any):
"""
Render the full page content for this pipeline (below settings).
Args:
pixelle_video: The initialized PixelleVideoCore instance.
"""
raise NotImplementedError
# ==================== Registry ====================
_pipeline_uis: Dict[str, PipelineUI] = {}
def register_pipeline_ui(ui_class: Type[PipelineUI]):
"""Register a pipeline UI class"""
instance = ui_class()
_pipeline_uis[instance.name] = instance
def get_pipeline_ui(name: str) -> PipelineUI:
"""Get a pipeline UI instance by name"""
return _pipeline_uis.get(name)
def get_all_pipeline_uis() -> List[PipelineUI]:
"""Get all registered pipeline UI instances"""
return list(_pipeline_uis.values())

69
web/pipelines/demo.py Normal file
View File

@@ -0,0 +1,69 @@
# Copyright (C) 2025 AIDC-AI
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Demo Pipeline UI
Implements a custom layout for the Demo Pipeline.
"""
import streamlit as st
from typing import Any
from web.i18n import tr
from web.pipelines.base import PipelineUI, register_pipeline_ui
class DemoPipelineUI(PipelineUI):
"""
Demo UI to verify the full-page plugin system.
Uses a completely different layout (2 columns).
"""
name = "demo"
icon = ""
@property
def display_name(self):
return tr("pipeline.demo.name")
@property
def description(self):
return tr("pipeline.demo.description")
def render(self, pixelle_video: Any):
st.markdown("### ✨ Demo Pipeline Custom Layout")
st.info("This pipeline uses a custom 2-column layout, demonstrating full UI control.")
col1, col2 = st.columns([2, 1])
with col1:
with st.container(border=True):
st.subheader("1. Input")
topic = st.text_input("Enter Topic", placeholder="e.g. AI News")
mood = st.selectbox("Mood", ["Happy", "Serious", "Funny"])
st.markdown("---")
st.subheader("2. Settings")
# Simplified settings for demo
n_scenes = st.slider("Scenes", 3, 10, 5)
with col2:
with st.container(border=True):
st.subheader("3. Generate")
if st.button("🚀 Generate Demo Video", type="primary", use_container_width=True):
# Mock generation logic or call backend
st.success(f"Generating video for '{topic}' ({mood}) with {n_scenes} scenes...")
st.balloons()
# Register self
register_pipeline_ui(DemoPipelineUI)

84
web/pipelines/standard.py Normal file
View File

@@ -0,0 +1,84 @@
# Copyright (C) 2025 AIDC-AI
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Standard Pipeline UI
Implements the classic 3-column layout for the Standard Pipeline.
"""
import streamlit as st
from typing import Any
from web.i18n import tr
from web.pipelines.base import PipelineUI, register_pipeline_ui
# Import components
from web.components.content_input import render_content_input, render_bgm_section, render_version_info
from web.components.style_config import render_style_config
from web.components.output_preview import render_output_preview
class StandardPipelineUI(PipelineUI):
"""
UI for the Standard Video Generation Pipeline.
Implements the classic 3-column layout.
"""
name = "standard"
icon = "🎬"
@property
def display_name(self):
return tr("pipeline.standard.name")
def render(self, pixelle_video: Any):
# Three-column layout
left_col, middle_col, right_col = st.columns([1, 1, 1])
# ====================================================================
# Left Column: Content Input & BGM
# ====================================================================
with left_col:
# Content input (mode, text, title, n_scenes)
content_params = render_content_input()
# BGM selection (bgm_path, bgm_volume)
bgm_params = render_bgm_section()
# Version info & GitHub link
render_version_info()
# ====================================================================
# Middle Column: Style Configuration
# ====================================================================
with middle_col:
# Style configuration (TTS, template, workflow, etc.)
style_params = render_style_config(pixelle_video)
# ====================================================================
# Right Column: Output Preview
# ====================================================================
with right_col:
# Combine all parameters
video_params = {
"pipeline": self.name,
**content_params,
**bgm_params,
**style_params
}
# Render output preview (generate button, progress, video preview)
render_output_preview(pixelle_video, video_params)
# Register self
register_pipeline_ui(StandardPipelineUI)