重构pipeline的UI架构,支持后续pipeline的动态拓展
This commit is contained in:
@@ -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",
|
||||||
]
|
]
|
||||||
|
|||||||
161
pixelle_video/pipelines/linear.py
Normal file
161
pixelle_video/pipelines/linear.py
Normal 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}")
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
35
web/pipelines/__init__.py
Normal 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
57
web/pipelines/base.py
Normal 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
69
web/pipelines/demo.py
Normal 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
84
web/pipelines/standard.py
Normal 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)
|
||||||
Reference in New Issue
Block a user