模板支持自定义参数
This commit is contained in:
@@ -4,7 +4,7 @@ Storyboard data models for video generation
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -36,6 +36,7 @@ class StoryboardConfig:
|
||||
|
||||
# Frame template (includes size information in path)
|
||||
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
|
||||
|
||||
@@ -7,7 +7,7 @@ This is the default pipeline for general-purpose video generation.
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional, Callable, Literal
|
||||
from typing import Optional, Callable, Literal, Dict, Any
|
||||
|
||||
from loguru import logger
|
||||
|
||||
@@ -84,6 +84,7 @@ class StandardPipeline(BasePipeline):
|
||||
|
||||
# === Frame Template (determines video size) ===
|
||||
frame_template: Optional[str] = None,
|
||||
template_params: Optional[Dict[str, Any]] = None, # Custom template parameters
|
||||
|
||||
# === Image Style ===
|
||||
prompt_prefix: Optional[str] = None,
|
||||
@@ -206,7 +207,8 @@ class StandardPipeline(BasePipeline):
|
||||
image_width=image_width,
|
||||
image_height=image_height,
|
||||
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
|
||||
|
||||
@@ -12,6 +12,7 @@ Linux Environment Requirements:
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
from typing import Dict, Any, Optional
|
||||
from pathlib import Path
|
||||
@@ -103,6 +104,156 @@ class HTMLFrameGenerator:
|
||||
logger.debug(f"Template loaded: {len(content)} chars")
|
||||
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]:
|
||||
"""
|
||||
Find suitable Chrome/Chromium executable, preferring non-snap versions
|
||||
@@ -248,11 +399,8 @@ class HTMLFrameGenerator:
|
||||
if ext:
|
||||
context.update(ext)
|
||||
|
||||
# Replace variables in HTML
|
||||
html = self.template
|
||||
for key, value in context.items():
|
||||
placeholder = f"{{{{{key}}}}}"
|
||||
html = html.replace(placeholder, str(value) if value is not None else "")
|
||||
# Replace variables in HTML (supports DSL syntax: {{param:type=default}})
|
||||
html = self._replace_parameters(self.template, context)
|
||||
|
||||
# Use provided output path or auto-generate
|
||||
if output_path is None:
|
||||
|
||||
@@ -213,6 +213,10 @@ class FrameProcessor:
|
||||
ext["content_subtitle"] = content_metadata.subtitle 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)
|
||||
generator = HTMLFrameGenerator(template_path)
|
||||
composed_path = await generator.generate_frame(
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 1080px;
|
||||
background: #764ba2;
|
||||
background: {{accent_color:color=#764ba2}};
|
||||
font-family: 'PingFang SC', 'Source Han Sans', 'Microsoft YaHei', sans-serif;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@@ -174,7 +174,7 @@
|
||||
.video-title-dot:nth-child(3) { background: #FFE66D; }
|
||||
|
||||
.video-title {
|
||||
font-size: 72px;
|
||||
font-size: {{title_font_size:number=72}}px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
line-height: 1.3;
|
||||
@@ -558,7 +558,7 @@
|
||||
<div class="author-desc">Open Source Omnimodal AI Creative Agent</div>
|
||||
</div>
|
||||
<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-dot"></div>
|
||||
<div class="logo-dot"></div>
|
||||
|
||||
173
web/app.py
173
web/app.py
@@ -767,6 +767,86 @@ def main():
|
||||
video_width, video_height = parse_template_size(frame_template)
|
||||
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
|
||||
with st.expander(tr("template.preview_title"), expanded=False):
|
||||
col1, col2 = st.columns(2)
|
||||
@@ -797,21 +877,96 @@ def main():
|
||||
template_width, template_height = parse_template_size(f"templates/{frame_template}")
|
||||
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
|
||||
if st.button(tr("template.preview_button"), key="btn_preview_template", use_container_width=True):
|
||||
with st.spinner(tr("template.preview_generating")):
|
||||
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)
|
||||
preview_path = run_async(generator.generate_frame(
|
||||
title=preview_title,
|
||||
text=preview_text,
|
||||
image=preview_image
|
||||
image=preview_image,
|
||||
ext=preview_custom_values if preview_custom_values else None
|
||||
))
|
||||
|
||||
# Display preview
|
||||
@@ -908,6 +1063,10 @@ def main():
|
||||
"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
|
||||
if ref_audio_path:
|
||||
video_params["ref_audio"] = str(ref_audio_path)
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
"template.preview_failed": "❌ Preview failed: {error}",
|
||||
"template.preview_image_help": "Supports local path or URL",
|
||||
"template.preview_caption": "Template Preview: {template}",
|
||||
"template.custom_parameters": "Custom Parameters",
|
||||
|
||||
"video.title": "🎬 Video Settings",
|
||||
"video.frames": "Scenes",
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
"template.preview_failed": "❌ 预览失败:{error}",
|
||||
"template.preview_image_help": "支持本地路径或 URL",
|
||||
"template.preview_caption": "模板预览:{template}",
|
||||
"template.custom_parameters": "自定义参数",
|
||||
|
||||
"video.title": "🎬 视频设置",
|
||||
"video.frames": "分镜数",
|
||||
|
||||
Reference in New Issue
Block a user