webui适配视频功能; 统一模板命名规范;

This commit is contained in:
puke
2025-11-12 20:01:09 +08:00
parent 7443cbf9c2
commit f7e3162a4a
26 changed files with 272 additions and 90 deletions

View File

@@ -37,15 +37,29 @@ comfyui:
# Image prompt prefix (optional)
prompt_prefix: "Minimalist black-and-white matchstick figure style illustration, clean lines, simple sketch style"
# Video-specific configuration
video:
# Required: Default workflow to use (no fallback)
# Options: runninghub/video_wan2.1_fusionx.json (recommended, no local setup)
# selfhost/video_wan2.1_fusionx.json (requires local ComfyUI)
default_workflow: runninghub/video_wan2.1_fusionx.json
# Video prompt prefix (optional)
prompt_prefix: "Minimalist black-and-white matchstick figure style illustration, clean lines, simple sketch style"
# ==================== Template Configuration ====================
# Configure default template for video generation
template:
# Default frame template to use when not explicitly specified
# Determines video aspect ratio and layout style
# Template naming convention:
# - static_*.html: Static style templates (no AI-generated media)
# - image_*.html: Templates requiring AI-generated images
# - video_*.html: Templates requiring AI-generated videos
# Options:
# - 1080x1920 (vertical/portrait): default.html, modern.html, elegant.html, etc.
# - 1080x1080 (square): minimal_framed.html, magazine_cover.html, etc.
# - 1920x1080 (horizontal/landscape): film.html, full.html, etc.
# - 1080x1920 (vertical/portrait): image_default.html, image_modern.html, image_elegant.html, static_simple.html, etc.
# - 1080x1080 (square): image_minimal_framed.html, etc.
# - 1920x1080 (horizontal/landscape): image_film.html, image_full.html, etc.
# See templates/ directory for all available templates
default_template: "1080x1920/default.html"
default_template: "1080x1920/image_default.html"

View File

@@ -117,9 +117,10 @@ class CustomPipeline(BasePipeline):
VideoGenerationResult
Image Generation Logic:
- If template has {{image}} → automatically generates images
- If template has no {{image}} → skips image generation (faster, cheaper)
- To customize: Override the template_requires_image logic in your subclass
- image_*.html templates → automatically generates images
- video_*.html templates → automatically generates videos
- static_*.html templates → skips media generation (faster, cheaper)
- To customize: Override the template type detection logic in your subclass
"""
logger.info("Starting CustomPipeline")
logger.info(f"Input text length: {len(text)} chars")
@@ -151,23 +152,27 @@ class CustomPipeline(BasePipeline):
frame_template = template_config.get("default_template", "1080x1920/default.html")
# ========== Step 0.5: Check template requirements ==========
# Detect if template requires {{image}} parameter
# This allows skipping the entire image generation pipeline for text-only templates
# Detect template type by filename prefix
from pathlib import Path
from pixelle_video.services.frame_html import HTMLFrameGenerator
from pixelle_video.utils.template_util import resolve_template_path
from pixelle_video.utils.template_util import resolve_template_path, get_template_type
template_path = resolve_template_path(frame_template)
generator = HTMLFrameGenerator(template_path)
template_requires_image = generator.requires_image()
template_name = Path(frame_template).name
template_type = get_template_type(template_name)
template_requires_image = (template_type == "image")
# Read media size from template meta tags
template_path = resolve_template_path(frame_template)
generator = HTMLFrameGenerator(template_path)
image_width, image_height = generator.get_media_size()
logger.info(f"📐 Media size from template: {image_width}x{image_height}")
if template_requires_image:
if template_type == "image":
logger.info(f"📸 Template requires image generation")
else:
logger.info(f" Template does not require images - skipping image generation pipeline")
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")
# ========== Step 1: Process content (CUSTOMIZE THIS) ==========
@@ -197,8 +202,8 @@ class CustomPipeline(BasePipeline):
# ========== Step 2: Generate image prompts (CONDITIONAL - CUSTOMIZE THIS) ==========
self._report_progress(progress_callback, "generating_image_prompts", 0.25)
# IMPORTANT: Check if template actually needs images
# If your template doesn't use {{image}}, you can skip this entire step!
# IMPORTANT: Check if template is image type
# If your template is static_*.html, you can skip this entire step!
if template_requires_image:
# Template requires images - generate image prompts using LLM
from pixelle_video.utils.content_generators import generate_image_prompts

View File

@@ -282,8 +282,8 @@ class StandardPipeline(BasePipeline):
logger.info(f"🎬 Template requires video generation")
elif template_media_type == "image":
logger.info(f"📸 Template requires image generation")
else: # text
logger.info(f"Template does not require media - skipping media generation pipeline")
else: # static
logger.info(f"Static template - skipping media generation pipeline")
logger.info(f" 💡 Benefits: Faster generation + Lower cost + No ComfyUI dependency")
try:
@@ -525,35 +525,23 @@ class StandardPipeline(BasePipeline):
- Media generation API calls
- ComfyUI dependency
Template naming rules:
Template naming convention:
- static_*.html: Static style template (returns "static")
- image_*.html: Image template (returns "image")
- video_*.html: Video template (returns "video")
- Other templates with {{image}}: Image template (returns "image")
- Other templates without {{image}}: Text-only template (returns "text")
Args:
frame_template: Template path (e.g., "1080x1920/default.html" or "1080x1920/video_default.html")
frame_template: Template path (e.g., "1080x1920/image_default.html" or "1080x1920/video_default.html")
Returns:
"video", "image", or "text"
"static", "image", or "video"
"""
from pixelle_video.services.frame_html import HTMLFrameGenerator
from pixelle_video.utils.template_util import resolve_template_path
from pixelle_video.utils.template_util import get_template_type
# Check if template name starts with video_
# Determine type by template filename prefix
template_name = Path(frame_template).name
if template_name.startswith("video_"):
logger.debug(f"Template '{frame_template}' is video template (video_ prefix)")
return "video"
template_type = get_template_type(template_name)
# Check if template contains {{image}}
template_path = resolve_template_path(frame_template)
generator = HTMLFrameGenerator(template_path)
requires_image = generator.requires_image()
if requires_image:
logger.debug(f"Template '{frame_template}' is image template (has {{image}})")
return "image"
else:
logger.debug(f"Template '{frame_template}' is text-only template")
return "text"
logger.debug(f"Template '{frame_template}' is {template_type} template")
return template_type

View File

@@ -77,21 +77,6 @@ class HTMLFrameGenerator:
self._check_linux_dependencies()
logger.debug(f"Loaded HTML template: {template_path} (size: {self.width}x{self.height})")
def requires_image(self) -> bool:
"""
Detect if template requires {{image}} parameter
This method checks if the template uses the {{image}} variable.
If the template doesn't use images, the entire image generation
pipeline can be skipped, significantly improving:
- Generation speed (no image generation API calls)
- Cost efficiency (no LLM calls for image prompts)
- Dependency requirements (no ComfyUI needed)
Returns:
True if template contains {{image}}, False otherwise
"""
return '{{image}}' in self.template
def _check_linux_dependencies(self):
"""Check Linux system dependencies and warn if missing"""

View File

@@ -18,6 +18,7 @@ import os
from pathlib import Path
from typing import List, Tuple, Optional, Literal
from pydantic import BaseModel, Field
import logging
from pixelle_video.utils.os_util import (
get_resource_path,
@@ -26,6 +27,8 @@ from pixelle_video.utils.os_util import (
resource_exists
)
logger = logging.getLogger(__name__)
def parse_template_size(template_path: str) -> Tuple[int, int]:
"""
@@ -316,7 +319,7 @@ def resolve_template_path(template_input: Optional[str]) -> str:
Args:
template_input: Can be:
- None: Use default "1080x1920/default.html"
- None: Use default "1080x1920/image_default.html"
- "template.html": Use default size + this template
- "1080x1920/template.html": Full relative path
- "templates/1080x1920/template.html": Absolute-ish path (legacy)
@@ -330,15 +333,15 @@ def resolve_template_path(template_input: Optional[str]) -> str:
Examples:
>>> resolve_template_path(None)
'templates/1080x1920/default.html'
>>> resolve_template_path("modern.html")
'templates/1080x1920/modern.html'
>>> resolve_template_path("1920x1080/default.html")
'templates/1920x1080/default.html'
'templates/1080x1920/image_default.html'
>>> resolve_template_path("image_modern.html")
'templates/1080x1920/image_modern.html'
>>> resolve_template_path("1920x1080/image_default.html")
'templates/1920x1080/image_default.html'
"""
# Default case
if template_input is None:
template_input = "1080x1920/default.html"
template_input = "1080x1920/image_default.html"
# Parse input to extract size and template name
size = None
@@ -359,6 +362,18 @@ def resolve_template_path(template_input: Optional[str]) -> str:
size = "1080x1920"
template_name = template_input
# Backward compatibility: migrate "default.html" to "image_default.html"
if template_name == "default.html":
migrated_name = "image_default.html"
try:
# Try migrated name first
path = get_resource_path("templates", size, migrated_name)
logger.info(f"Backward compatibility: migrated '{template_input}' to '{size}/{migrated_name}'")
return path
except FileNotFoundError:
# Fall through to try original name
logger.warning(f"Migrated template '{size}/{migrated_name}' not found, trying original name")
# Use resource API to resolve path (custom > default)
try:
return get_resource_path("templates", size, template_name)
@@ -367,6 +382,120 @@ def resolve_template_path(template_input: Optional[str]) -> str:
raise FileNotFoundError(
f"Template not found: {size}/{template_name}\n"
f"Available sizes: {available_sizes}\n"
f"Hint: Use format 'SIZExSIZE/template.html' (e.g., '1080x1920/default.html')"
f"Hint: Use format 'SIZExSIZE/template.html' (e.g., '1080x1920/image_default.html')"
)
def get_template_type(template_name: str) -> Literal['static', 'image', 'video']:
"""
Detect template type from template filename
Template naming convention:
- static_*.html: Static style templates (no AI-generated media)
- image_*.html: Templates requiring AI-generated images
- video_*.html: Templates requiring AI-generated videos
Args:
template_name: Template filename like "image_default.html" or "video_simple.html"
Returns:
Template type: 'static', 'image', or 'video'
Examples:
>>> get_template_type("static_simple.html")
'static'
>>> get_template_type("image_default.html")
'image'
>>> get_template_type("video_simple.html")
'video'
"""
name = Path(template_name).name
if name.startswith("static_"):
return "static"
elif name.startswith("video_"):
return "video"
elif name.startswith("image_"):
return "image"
else:
# Fallback: try to detect from legacy names
logger.warning(
f"Template '{template_name}' doesn't follow naming convention (static_/image_/video_). "
f"Defaulting to 'image' type."
)
return "image"
def filter_templates_by_type(
templates: List[TemplateInfo],
template_type: Literal['static', 'image', 'video']
) -> List[TemplateInfo]:
"""
Filter templates by type
Args:
templates: List of TemplateInfo objects
template_type: Type to filter by ('static', 'image', or 'video')
Returns:
Filtered list of TemplateInfo objects
Examples:
>>> all_templates = get_all_templates_with_info()
>>> image_templates = filter_templates_by_type(all_templates, 'image')
>>> len(image_templates) > 0
True
"""
filtered = []
for t in templates:
template_name = t.display_info.name
if get_template_type(template_name) == template_type:
filtered.append(t)
return filtered
def get_templates_grouped_by_size_and_type(
template_type: Optional[Literal['static', 'image', 'video']] = None
) -> dict:
"""
Get templates grouped by size, optionally filtered by type
Args:
template_type: Optional type filter ('static', 'image', or 'video')
Returns:
Dict with size as key, list of TemplateInfo as value
Ordered by orientation priority: portrait > landscape > square
Examples:
>>> # Get all templates
>>> all_grouped = get_templates_grouped_by_size_and_type()
>>> # Get only image templates
>>> image_grouped = get_templates_grouped_by_size_and_type('image')
"""
from collections import defaultdict
templates = get_all_templates_with_info()
# Filter by type if specified
if template_type is not None:
templates = filter_templates_by_type(templates, template_type)
grouped = defaultdict(list)
for t in templates:
grouped[t.display_info.size].append(t)
# Sort groups by orientation priority: portrait > landscape > square
orientation_priority = {'portrait': 0, 'landscape': 1, 'square': 2}
sorted_grouped = {}
for size in sorted(grouped.keys(), key=lambda s: (
orientation_priority.get(grouped[s][0].display_info.orientation, 3),
s
)):
sorted_grouped[size] = sorted(grouped[size], key=lambda t: t.display_info.name)
return sorted_grouped

View File

@@ -684,13 +684,41 @@ def main():
st.markdown(f"🔗 [{tr('template.preview_link')}]({template_docs_url})")
# Import template utilities
from pixelle_video.utils.template_util import get_templates_grouped_by_size
from pixelle_video.utils.template_util import get_templates_grouped_by_size_and_type, get_template_type
# Get templates grouped by size
grouped_templates = get_templates_grouped_by_size()
# Template type selector
st.markdown(f"**{tr('template.type_selector')}**")
template_type_options = {
'static': tr('template.type.static'),
'image': tr('template.type.image'),
'video': tr('template.type.video')
}
# Radio buttons in horizontal layout
selected_template_type = st.radio(
tr('template.type_selector'),
options=list(template_type_options.keys()),
format_func=lambda x: template_type_options[x],
index=1, # Default to 'image'
key="template_type_selector",
label_visibility="collapsed",
horizontal=True
)
# Display hint based on selected type (below radio buttons)
if selected_template_type == 'static':
st.info(tr('template.type.static_hint'))
elif selected_template_type == 'image':
st.info(tr('template.type.image_hint'))
elif selected_template_type == 'video':
st.info(tr('template.type.video_hint'))
# Get templates grouped by size, filtered by selected type
grouped_templates = get_templates_grouped_by_size_and_type(selected_template_type)
if not grouped_templates:
st.error("No templates found. Please ensure templates are in templates/ directory with proper structure (e.g., templates/1080x1920/default.html).")
st.warning(f"No {template_type_options[selected_template_type]} templates found. Please select a different type or add templates.")
st.stop()
# Build display options with group separators
@@ -707,7 +735,19 @@ def main():
# Get default template from config
template_config = pixelle_video.config.get("template", {})
config_default_template = template_config.get("default_template", "1080x1920/default.html")
config_default_template = template_config.get("default_template", "1080x1920/image_default.html")
# Backward compatibility
if config_default_template == "1080x1920/default.html":
config_default_template = "1080x1920/image_default.html"
# Determine type-specific default template
type_default_templates = {
'static': '1080x1920/static_default.html',
'image': '1080x1920/image_default.html',
'video': '1080x1920/video_default.html'
}
type_specific_default = type_default_templates.get(selected_template_type, config_default_template)
for size, templates in grouped_templates.items():
if not templates:
@@ -733,10 +773,12 @@ def main():
display_options.append(display_name)
template_paths_ordered.append(t.template_path) # Add to ordered list
# Set default based on config (priority: config > first default.html in portrait)
# Set default: priority is config > type-specific default > first in portrait
if t.template_path == config_default_template:
default_index = current_index
elif default_index == 0 and "default.html" in t.display_info.name and t.display_info.orientation == 'portrait':
elif default_index == 0 and t.template_path == type_specific_default:
default_index = current_index
elif default_index == 0 and t.display_info.orientation == 'portrait':
default_index = current_index
current_index += 1
@@ -789,20 +831,11 @@ def main():
# Detect template media type
from pathlib import Path
template_name = Path(frame_template).name
from pixelle_video.utils.template_util import get_template_type
if template_name.startswith("video_"):
# Video template
template_media_type = "video"
template_requires_media = True
elif generator_for_params.requires_image():
# Image template
template_media_type = "image"
template_requires_media = True
else:
# Text-only template
template_media_type = "text"
template_requires_media = False
template_name = Path(frame_template).name
template_media_type = get_template_type(template_name)
template_requires_media = (template_media_type in ["image", "video"])
# Store in session state for workflow filtering
st.session_state['template_media_type'] = template_media_type
@@ -1009,7 +1042,9 @@ def main():
# If user has a saved preference in config, try to match it
comfyui_config = config_manager.get_comfyui_config()
saved_workflow = comfyui_config["image"]["default_workflow"]
# Select config based on template type (image or video)
media_config_key = "video" if template_media_type == "video" else "image"
saved_workflow = comfyui_config.get(media_config_key, {}).get("default_workflow", "")
if saved_workflow and saved_workflow in workflow_keys:
default_workflow_index = workflow_keys.index(saved_workflow)
@@ -1040,8 +1075,8 @@ def main():
st.info(f"📐 {size_info_text}")
# Prompt prefix input
# Get current prompt_prefix from config
current_prefix = comfyui_config["image"]["prompt_prefix"]
# Get current prompt_prefix from config (based on media type)
current_prefix = comfyui_config.get(media_config_key, {}).get("prompt_prefix", "")
# Prompt prefix input (temporary, not saved to config)
prompt_prefix = st.text_area(
@@ -1268,6 +1303,18 @@ def main():
# Video preview
if os.path.exists(result.video_path):
st.video(result.video_path)
# Download button
with open(result.video_path, "rb") as video_file:
video_bytes = video_file.read()
video_filename = os.path.basename(result.video_path)
st.download_button(
label="⬇️ 下载视频" if get_language() == "zh_CN" else "⬇️ Download Video",
data=video_bytes,
file_name=video_filename,
mime="video/mp4",
use_container_width=True
)
else:
st.error(tr("status.video_not_found", path=result.video_path))

View File

@@ -86,8 +86,15 @@
"template.modern": "Modern",
"template.neon": "Neon",
"template.what": "Controls the visual layout and design style of each frame (title, text, image arrangement)",
"template.how": "Place .html template files in templates/SIZE/ directories (e.g., templates/1080x1920/). Templates are automatically grouped by size. Custom CSS styles are supported.\n\n**Note**\n\nAt least one of the following browsers must be installed on your computer for proper operation:\n1. Google Chrome (Windows, macOS)\n2. Chromium Browser (Linux)\n3. Microsoft Edge",
"template.how": "Place .html template files in templates/SIZE/ directories (e.g., templates/1080x1920/). Templates are automatically grouped by size. Custom CSS styles are supported.\n\n**Template Naming Convention**\n\n- `static_*.html` → Static style templates (no AI-generated media)\n- `image_*.html` → Image generation templates (AI-generated images)\n- `video_*.html` → Video generation templates (AI-generated videos)\n\n**Note**\n\nAt least one of the following browsers must be installed on your computer for proper operation:\n1. Google Chrome (Windows, macOS)\n2. Chromium Browser (Linux)\n3. Microsoft Edge",
"template.size_info": "Template Size",
"template.type_selector": "Template Type",
"template.type.static": "📄 Static Style",
"template.type.image": "🖼️ Generate Images",
"template.type.video": "🎬 Generate Videos",
"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.video_hint": "AI automatically generates video clips matching the narration content. Video size is determined by the template.",
"orientation.portrait": "Portrait",
"orientation.landscape": "Landscape",

View File

@@ -86,8 +86,15 @@
"template.modern": "现代",
"template.neon": "霓虹",
"template.what": "控制视频每一帧的视觉布局和设计风格(标题、文本、图片的排版样式)",
"template.how": "将 .html 模板文件放入 templates/尺寸/ 目录(如 templates/1080x1920/),系统会自动按尺寸分组。支持自定义 CSS 样式。\n\n**注意**\n\n您的计算机上必须安装以下至少一种浏览器才能正常运行\n1. Google ChromeWindows、MacOS\n2. Chromium 浏览器Linux\n3. Microsoft Edge",
"template.how": "将 .html 模板文件放入 templates/尺寸/ 目录(如 templates/1080x1920/),系统会自动按尺寸分组。支持自定义 CSS 样式。\n\n**模板命名规范**\n\n- `static_*.html` → 静态样式模板无需AI生成媒体\n- `image_*.html` → 生成插图模板AI生成图片\n- `video_*.html` → 生成视频模板AI生成视频\n\n**注意**\n\n您的计算机上必须安装以下至少一种浏览器才能正常运行\n1. Google ChromeWindows、MacOS\n2. Chromium 浏览器Linux\n3. Microsoft Edge",
"template.size_info": "模板尺寸",
"template.type_selector": "分镜类型",
"template.type.static": "📄 静态样式",
"template.type.image": "🖼️ 生成插图",
"template.type.video": "🎬 生成视频",
"template.type.static_hint": "使用模板自带样式无需AI生成媒体。可在模板中自定义背景图片等参数。",
"template.type.image_hint": "AI自动根据文案内容生成与之匹配的插图插图尺寸由模板决定。",
"template.type.video_hint": "AI自动根据文案内容生成与之匹配的视频片段视频尺寸由模板决定。",
"orientation.portrait": "竖屏",
"orientation.landscape": "横屏",