模板支持自定义参数

This commit is contained in:
puke
2025-11-05 23:01:46 +08:00
parent 15899afb6f
commit 2a07aa9567
8 changed files with 334 additions and 18 deletions

View File

@@ -4,7 +4,7 @@ Storyboard data models for video generation
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime from datetime import datetime
from typing import List, Optional from typing import List, Optional, Dict, Any
@dataclass @dataclass
@@ -36,6 +36,7 @@ class StoryboardConfig:
# Frame template (includes size information in path) # Frame template (includes size information in path)
frame_template: str = "1080x1920/default.html" # Template path with size (e.g., "1080x1920/default.html") frame_template: str = "1080x1920/default.html" # Template path with size (e.g., "1080x1920/default.html")
template_params: Optional[Dict[str, Any]] = None # Custom template parameters (e.g., {"accent_color": "#ff0000"})
@dataclass @dataclass

View File

@@ -7,7 +7,7 @@ This is the default pipeline for general-purpose video generation.
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, Dict, Any
from loguru import logger from loguru import logger
@@ -84,6 +84,7 @@ class StandardPipeline(BasePipeline):
# === Frame Template (determines video size) === # === Frame Template (determines video size) ===
frame_template: Optional[str] = None, frame_template: Optional[str] = None,
template_params: Optional[Dict[str, Any]] = None, # Custom template parameters
# === Image Style === # === Image Style ===
prompt_prefix: Optional[str] = None, prompt_prefix: Optional[str] = None,
@@ -206,7 +207,8 @@ class StandardPipeline(BasePipeline):
image_width=image_width, image_width=image_width,
image_height=image_height, image_height=image_height,
image_workflow=image_workflow, image_workflow=image_workflow,
frame_template=frame_template or "1080x1920/default.html" frame_template=frame_template or "1080x1920/default.html",
template_params=template_params
) )
# Create storyboard # Create storyboard

View File

@@ -12,6 +12,7 @@ Linux Environment Requirements:
""" """
import os import os
import re
import uuid import uuid
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
from pathlib import Path from pathlib import Path
@@ -103,6 +104,156 @@ class HTMLFrameGenerator:
logger.debug(f"Template loaded: {len(content)} chars") logger.debug(f"Template loaded: {len(content)} chars")
return content return content
def parse_template_parameters(self) -> Dict[str, Dict[str, Any]]:
"""
Parse custom parameters from HTML template
Supports syntax: {{param:type=default}}
- {{param}} -> text type, no default
- {{param=value}} -> text type, with default
- {{param:type}} -> specified type, no default
- {{param:type=value}} -> specified type, with default
Supported types: text, number, color, bool
Returns:
Dictionary of custom parameters with their configurations:
{
'param_name': {
'type': 'text' | 'number' | 'color' | 'bool',
'default': Any,
'label': str # same as param_name
}
}
"""
# Preset parameters that should be ignored
PRESET_PARAMS = {'title', 'text', 'image', 'content_title', 'content_author',
'content_subtitle', 'content_genre'}
# Pattern: {{param_name:type=default}} or {{param_name=default}} or {{param_name:type}} or {{param_name}}
# Param name: must start with letter or underscore, can contain letters, digits, underscores
PARAM_PATTERN = r'\{\{([a-zA-Z_][a-zA-Z0-9_]*)(?::([a-z]+))?(?:=([^}]+))?\}\}'
params = {}
for match in re.finditer(PARAM_PATTERN, self.template):
param_name = match.group(1)
param_type = match.group(2) or 'text' # Default to text
default_value = match.group(3)
# Skip preset parameters
if param_name in PRESET_PARAMS:
continue
# Skip if already parsed (use first occurrence)
if param_name in params:
continue
# Validate type
if param_type not in {'text', 'number', 'color', 'bool'}:
logger.warning(f"Unknown parameter type '{param_type}' for '{param_name}', defaulting to 'text'")
param_type = 'text'
# Parse default value based on type
parsed_default = self._parse_default_value(param_type, default_value)
params[param_name] = {
'type': param_type,
'default': parsed_default,
'label': param_name, # Use param name as label
}
if params:
logger.debug(f"Parsed {len(params)} custom parameter(s) from template: {list(params.keys())}")
return params
def _parse_default_value(self, param_type: str, value_str: Optional[str]) -> Any:
"""
Parse default value based on parameter type
Args:
param_type: Type of parameter (text, number, color, bool)
value_str: String value to parse (can be None)
Returns:
Parsed value with appropriate type
"""
if value_str is None:
# No default value specified, return type-specific defaults
return {
'text': '',
'number': 0,
'color': '#000000',
'bool': False,
}.get(param_type, '')
if param_type == 'number':
try:
# Try int first, then float
if '.' in value_str:
return float(value_str)
else:
return int(value_str)
except ValueError:
logger.warning(f"Invalid number value '{value_str}', using 0")
return 0
elif param_type == 'bool':
# Accept: true/false, 1/0, yes/no, on/off (case-insensitive)
return value_str.lower() in {'true', '1', 'yes', 'on'}
elif param_type == 'color':
# Auto-add # if missing
if value_str.startswith('#'):
return value_str
else:
return f'#{value_str}'
else: # text
return value_str
def _replace_parameters(self, html: str, values: Dict[str, Any]) -> str:
"""
Replace parameter placeholders with actual values
Supports DSL syntax: {{param:type=default}}
- If value provided in values dict, use it
- Otherwise, use default value from placeholder
- If no default, use empty string
Args:
html: HTML template content
values: Dictionary of parameter values
Returns:
HTML with placeholders replaced
"""
PARAM_PATTERN = r'\{\{([a-zA-Z_][a-zA-Z0-9_]*)(?::([a-z]+))?(?:=([^}]+))?\}\}'
def replacer(match):
param_name = match.group(1)
param_type = match.group(2) or 'text'
default_value_str = match.group(3)
# Check if value is provided
if param_name in values:
value = values[param_name]
# Convert bool to string for HTML
if isinstance(value, bool):
return 'true' if value else 'false'
return str(value) if value is not None else ''
# Use default value from placeholder if available
elif default_value_str:
return default_value_str
# No value and no default
else:
return ''
return re.sub(PARAM_PATTERN, replacer, html)
def _find_chrome_executable(self) -> Optional[str]: def _find_chrome_executable(self) -> Optional[str]:
""" """
Find suitable Chrome/Chromium executable, preferring non-snap versions Find suitable Chrome/Chromium executable, preferring non-snap versions
@@ -248,11 +399,8 @@ class HTMLFrameGenerator:
if ext: if ext:
context.update(ext) context.update(ext)
# Replace variables in HTML # Replace variables in HTML (supports DSL syntax: {{param:type=default}})
html = self.template html = self._replace_parameters(self.template, context)
for key, value in context.items():
placeholder = f"{{{{{key}}}}}"
html = html.replace(placeholder, str(value) if value is not None else "")
# Use provided output path or auto-generate # Use provided output path or auto-generate
if output_path is None: if output_path is None:

View File

@@ -213,6 +213,10 @@ class FrameProcessor:
ext["content_subtitle"] = content_metadata.subtitle or "" ext["content_subtitle"] = content_metadata.subtitle or ""
ext["content_genre"] = content_metadata.genre or "" ext["content_genre"] = content_metadata.genre or ""
# Add custom template parameters
if config.template_params:
ext.update(config.template_params)
# Generate frame using HTML (size is auto-parsed from template path) # Generate frame using HTML (size is auto-parsed from template path)
generator = HTMLFrameGenerator(template_path) generator = HTMLFrameGenerator(template_path)
composed_path = await generator.generate_frame( composed_path = await generator.generate_frame(

View File

@@ -12,7 +12,7 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 1080px; width: 1080px;
background: #764ba2; background: {{accent_color:color=#764ba2}};
font-family: 'PingFang SC', 'Source Han Sans', 'Microsoft YaHei', sans-serif; font-family: 'PingFang SC', 'Source Han Sans', 'Microsoft YaHei', sans-serif;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
@@ -174,7 +174,7 @@
.video-title-dot:nth-child(3) { background: #FFE66D; } .video-title-dot:nth-child(3) { background: #FFE66D; }
.video-title { .video-title {
font-size: 72px; font-size: {{title_font_size:number=72}}px;
font-weight: bold; font-weight: bold;
color: white; color: white;
line-height: 1.3; line-height: 1.3;
@@ -558,7 +558,7 @@
<div class="author-desc">Open Source Omnimodal AI Creative Agent</div> <div class="author-desc">Open Source Omnimodal AI Creative Agent</div>
</div> </div>
<div class="logo-wrapper"> <div class="logo-wrapper">
<div class="logo">Pixelle-Video</div> <div class="logo">{{custom_brand=Pixelle-Video}}</div>
<div class="logo-dots"> <div class="logo-dots">
<div class="logo-dot"></div> <div class="logo-dot"></div>
<div class="logo-dot"></div> <div class="logo-dot"></div>

View File

@@ -767,6 +767,86 @@ def main():
video_width, video_height = parse_template_size(frame_template) video_width, video_height = parse_template_size(frame_template)
st.caption(tr("template.video_size_info", width=video_width, height=video_height)) st.caption(tr("template.video_size_info", width=video_width, height=video_height))
# Custom template parameters (for video generation)
from pixelle_video.services.frame_html import HTMLFrameGenerator
template_path_for_params = f"templates/{frame_template}"
generator_for_params = HTMLFrameGenerator(template_path_for_params)
custom_params_for_video = generator_for_params.parse_template_parameters()
custom_values_for_video = {}
if custom_params_for_video:
st.markdown("📝 " + tr("template.custom_parameters"))
# Render custom parameter inputs in 2 columns
video_custom_col1, video_custom_col2 = st.columns(2)
param_items = list(custom_params_for_video.items())
mid_point = (len(param_items) + 1) // 2
# Left column parameters
with video_custom_col1:
for param_name, config in param_items[:mid_point]:
param_type = config['type']
default = config['default']
label = config['label']
if param_type == 'text':
custom_values_for_video[param_name] = st.text_input(
label,
value=default,
key=f"video_custom_{param_name}"
)
elif param_type == 'number':
custom_values_for_video[param_name] = st.number_input(
label,
value=default,
key=f"video_custom_{param_name}"
)
elif param_type == 'color':
custom_values_for_video[param_name] = st.color_picker(
label,
value=default,
key=f"video_custom_{param_name}"
)
elif param_type == 'bool':
custom_values_for_video[param_name] = st.checkbox(
label,
value=default,
key=f"video_custom_{param_name}"
)
# Right column parameters
with video_custom_col2:
for param_name, config in param_items[mid_point:]:
param_type = config['type']
default = config['default']
label = config['label']
if param_type == 'text':
custom_values_for_video[param_name] = st.text_input(
label,
value=default,
key=f"video_custom_{param_name}"
)
elif param_type == 'number':
custom_values_for_video[param_name] = st.number_input(
label,
value=default,
key=f"video_custom_{param_name}"
)
elif param_type == 'color':
custom_values_for_video[param_name] = st.color_picker(
label,
value=default,
key=f"video_custom_{param_name}"
)
elif param_type == 'bool':
custom_values_for_video[param_name] = st.checkbox(
label,
value=default,
key=f"video_custom_{param_name}"
)
# Template preview expander # Template preview expander
with st.expander(tr("template.preview_title"), expanded=False): with st.expander(tr("template.preview_title"), expanded=False):
col1, col2 = st.columns(2) col1, col2 = st.columns(2)
@@ -797,21 +877,96 @@ def main():
template_width, template_height = parse_template_size(f"templates/{frame_template}") template_width, template_height = parse_template_size(f"templates/{frame_template}")
st.info(f"📐 {tr('template.size_info')}: {template_width} × {template_height}") st.info(f"📐 {tr('template.size_info')}: {template_width} × {template_height}")
# Parse and render custom parameters
from pixelle_video.services.frame_html import HTMLFrameGenerator
template_path = f"templates/{frame_template}"
generator = HTMLFrameGenerator(template_path)
custom_params = generator.parse_template_parameters()
preview_custom_values = {}
if custom_params:
st.markdown("📝 " + tr("template.custom_parameters"))
# Render custom parameter inputs in 2 columns
custom_col1, custom_col2 = st.columns(2)
param_items = list(custom_params.items())
mid_point = (len(param_items) + 1) // 2
# Left column parameters
with custom_col1:
for param_name, config in param_items[:mid_point]:
param_type = config['type']
default = config['default']
label = config['label']
if param_type == 'text':
preview_custom_values[param_name] = st.text_input(
label,
value=default,
key=f"preview_custom_{param_name}"
)
elif param_type == 'number':
preview_custom_values[param_name] = st.number_input(
label,
value=default,
key=f"preview_custom_{param_name}"
)
elif param_type == 'color':
preview_custom_values[param_name] = st.color_picker(
label,
value=default,
key=f"preview_custom_{param_name}"
)
elif param_type == 'bool':
preview_custom_values[param_name] = st.checkbox(
label,
value=default,
key=f"preview_custom_{param_name}"
)
# Right column parameters
with custom_col2:
for param_name, config in param_items[mid_point:]:
param_type = config['type']
default = config['default']
label = config['label']
if param_type == 'text':
preview_custom_values[param_name] = st.text_input(
label,
value=default,
key=f"preview_custom_{param_name}"
)
elif param_type == 'number':
preview_custom_values[param_name] = st.number_input(
label,
value=default,
key=f"preview_custom_{param_name}"
)
elif param_type == 'color':
preview_custom_values[param_name] = st.color_picker(
label,
value=default,
key=f"preview_custom_{param_name}"
)
elif param_type == 'bool':
preview_custom_values[param_name] = st.checkbox(
label,
value=default,
key=f"preview_custom_{param_name}"
)
# Preview button # Preview button
if st.button(tr("template.preview_button"), key="btn_preview_template", use_container_width=True): if st.button(tr("template.preview_button"), key="btn_preview_template", use_container_width=True):
with st.spinner(tr("template.preview_generating")): with st.spinner(tr("template.preview_generating")):
try: try:
from pixelle_video.services.frame_html import HTMLFrameGenerator
# Use the currently selected template (size is auto-parsed)
template_path = f"templates/{frame_template}"
generator = HTMLFrameGenerator(template_path)
# Generate preview (size is auto-determined from template) # Generate preview (size is auto-determined from template)
preview_path = run_async(generator.generate_frame( preview_path = run_async(generator.generate_frame(
title=preview_title, title=preview_title,
text=preview_text, text=preview_text,
image=preview_image image=preview_image,
ext=preview_custom_values if preview_custom_values else None
)) ))
# Display preview # Display preview
@@ -908,6 +1063,10 @@ def main():
"progress_callback": update_progress, "progress_callback": update_progress,
} }
# Add custom template parameters if any
if custom_values_for_video:
video_params["template_params"] = custom_values_for_video
# Add ref_audio if uploaded # Add ref_audio if uploaded
if ref_audio_path: if ref_audio_path:
video_params["ref_audio"] = str(ref_audio_path) video_params["ref_audio"] = str(ref_audio_path)

View File

@@ -100,6 +100,7 @@
"template.preview_failed": "❌ Preview failed: {error}", "template.preview_failed": "❌ Preview failed: {error}",
"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",
"video.title": "🎬 Video Settings", "video.title": "🎬 Video Settings",
"video.frames": "Scenes", "video.frames": "Scenes",

View File

@@ -100,6 +100,7 @@
"template.preview_failed": "❌ 预览失败:{error}", "template.preview_failed": "❌ 预览失败:{error}",
"template.preview_image_help": "支持本地路径或 URL", "template.preview_image_help": "支持本地路径或 URL",
"template.preview_caption": "模板预览:{template}", "template.preview_caption": "模板预览:{template}",
"template.custom_parameters": "自定义参数",
"video.title": "🎬 视频设置", "video.title": "🎬 视频设置",
"video.frames": "分镜数", "video.frames": "分镜数",