优化自定义逻辑

This commit is contained in:
puke
2025-10-26 17:29:42 +08:00
committed by puke
parent 30cb9d9c18
commit 5acf0a53b6
18 changed files with 277 additions and 730 deletions

7
.gitignore vendored
View File

@@ -72,3 +72,10 @@ repositories/
workflows/* workflows/*
!workflows/*_default.json !workflows/*_default.json
!workflows/README.md !workflows/README.md
# Templates - ignore user customizations but keep presets
templates/*
!templates/default.html
!templates/modern.html
!templates/neon.html
!templates/README.md

View File

@@ -56,6 +56,16 @@ image:
default: default # Default preset name (uses workflows/image_default.json) default: default # Default preset name (uses workflows/image_default.json)
comfyui_url: http://127.0.0.1:8188 # Local ComfyUI server comfyui_url: http://127.0.0.1:8188 # Local ComfyUI server
# runninghub_api_key: "" # Optional: RunningHub cloud API key # runninghub_api_key: "" # Optional: RunningHub cloud API key
# Prompt prefix - automatically added to all image prompts
# Leave empty ("") if you don't want any prefix
prompt_prefix: "Pure white background, minimalist illustration, matchstick figure style, black and white line drawing, simple clean lines"
# Common examples:
# prompt_prefix: "" # No prefix
# prompt_prefix: "anime style, vibrant colors, cel shading" # Anime style
# prompt_prefix: "watercolor painting, soft edges, artistic" # Watercolor
# prompt_prefix: "photorealistic, 8k, professional photography" # Realistic
# ==================== Notes ==================== # ==================== Notes ====================
# 1. LLM Configuration: # 1. LLM Configuration:

View File

@@ -56,7 +56,12 @@
"voice.previewing": "Generating voice preview...", "voice.previewing": "Generating voice preview...",
"voice.preview_failed": "Preview failed: {error}", "voice.preview_failed": "Preview failed: {error}",
"style.title": "🎨 Illustration Style", "style.title": "🎨 Image Settings",
"style.workflow": "ComfyUI Workflow",
"style.workflow_help": "💡 Custom: Place image_xxx.json in workflows/ folder",
"style.prompt_prefix": "Style Prompt Prefix",
"style.prompt_prefix_placeholder": "Enter style prefix (leave empty for config default)",
"style.prompt_prefix_help": "This text will be automatically added before all image generation prompts. To permanently change, edit config.yaml",
"style.custom": "Custom", "style.custom": "Custom",
"style.description": "Style Description", "style.description": "Style Description",
"style.description_placeholder": "Describe the illustration style you want (any language)...", "style.description_placeholder": "Describe the illustration style you want (any language)...",
@@ -67,9 +72,10 @@
"style.generated_prompt": "Generated prompt: {prompt}", "style.generated_prompt": "Generated prompt: {prompt}",
"template.title": "📐 Storyboard Template", "template.title": "📐 Storyboard Template",
"template.classic": "Classic", "template.default": "Default",
"template.modern": "Modern", "template.modern": "Modern",
"template.neon": "Neon", "template.neon": "Neon",
"template.custom_help": "💡 Custom: Place .html files in templates/ folder",
"video.title": "🎬 Video Settings", "video.title": "🎬 Video Settings",
"video.frames": "Scenes", "video.frames": "Scenes",
@@ -81,6 +87,7 @@
"bgm.none": "🔇 No BGM", "bgm.none": "🔇 No BGM",
"bgm.preview": "▶ Preview Music", "bgm.preview": "▶ Preview Music",
"bgm.preview_failed": "❌ Music file not found: {file}", "bgm.preview_failed": "❌ Music file not found: {file}",
"bgm.custom_help": "💡 Custom: Place audio files in bgm/ folder",
"btn.generate": "🎬 Generate Video", "btn.generate": "🎬 Generate Video",
"btn.save_config": "💾 Save Configuration", "btn.save_config": "💾 Save Configuration",

View File

@@ -56,7 +56,12 @@
"voice.previewing": "正在生成语音预览...", "voice.previewing": "正在生成语音预览...",
"voice.preview_failed": "预览失败:{error}", "voice.preview_failed": "预览失败:{error}",
"style.title": "🎨 插图风格", "style.title": "🎨 插图设置",
"style.workflow": "生图工作流",
"style.workflow_help": "💡 自定义:将 image_xxx.json 放入 workflows/ 文件夹",
"style.prompt_prefix": "风格提示词前缀",
"style.prompt_prefix_placeholder": "输入风格前缀(留空则使用配置文件默认值)",
"style.prompt_prefix_help": "此文本将自动添加到所有图像生成提示词之前。要永久修改,请编辑 config.yaml",
"style.custom": "自定义", "style.custom": "自定义",
"style.description": "风格描述", "style.description": "风格描述",
"style.description_placeholder": "描述您想要的插图风格(任何语言)...", "style.description_placeholder": "描述您想要的插图风格(任何语言)...",
@@ -67,9 +72,10 @@
"style.generated_prompt": "生成的提示词:{prompt}", "style.generated_prompt": "生成的提示词:{prompt}",
"template.title": "📐 分镜模板", "template.title": "📐 分镜模板",
"template.classic": "Classic", "template.default": "默认",
"template.modern": "Modern", "template.modern": "现代",
"template.neon": "Neon", "template.neon": "霓虹",
"template.custom_help": "💡 自定义:将 .html 文件放入 templates/ 文件夹",
"video.title": "🎬 视频设置", "video.title": "🎬 视频设置",
"video.frames": "分镜数", "video.frames": "分镜数",
@@ -81,6 +87,7 @@
"bgm.none": "🔇 无背景音乐", "bgm.none": "🔇 无背景音乐",
"bgm.preview": "▶ 试听音乐", "bgm.preview": "▶ 试听音乐",
"bgm.preview_failed": "❌ 音乐文件未找到:{file}", "bgm.preview_failed": "❌ 音乐文件未找到:{file}",
"bgm.custom_help": "💡 自定义:将音频文件放入 bgm/ 文件夹",
"btn.generate": "🎬 生成视频", "btn.generate": "🎬 生成视频",
"btn.save_config": "💾 保存配置", "btn.save_config": "💾 保存配置",

View File

@@ -27,6 +27,7 @@ class StoryboardConfig:
# Image parameters # Image parameters
image_width: int = 1024 image_width: int = 1024
image_height: int = 1024 image_height: int = 1024
image_preset: Optional[str] = None # Image workflow preset (None = use default)
# Frame template # Frame template
frame_template: Optional[str] = None # HTML template name or path (None = use PIL) frame_template: Optional[str] = None # HTML template name or path (None = use PIL)

View File

@@ -50,7 +50,6 @@ IMAGE_PROMPT_GENERATION_PROMPT = """# 角色定位
## 图像提示词规范 ## 图像提示词规范
- 语言:**必须使用英文**(用于 AI 图像生成模型) - 语言:**必须使用英文**(用于 AI 图像生成模型)
- 画面风格:{style_description}
- 描述结构scene + character action + emotion + symbolic elements - 描述结构scene + character action + emotion + symbolic elements
- 描述长度:确保描述清晰完整且富有创意(建议 50-100 个英文单词) - 描述长度:确保描述清晰完整且富有创意(建议 50-100 个英文单词)
@@ -100,8 +99,7 @@ IMAGE_PROMPT_GENERATION_PROMPT = """# 角色定位
5. **图像提示词必须使用英文**for AI image generation models 5. **图像提示词必须使用英文**for AI image generation models
6. 图像提示词必须准确反映对应旁白的具体内容和情感 6. 图像提示词必须准确反映对应旁白的具体内容和情感
7. 每个图像都要有创意性和视觉冲击力,避免千篇一律 7. 每个图像都要有创意性和视觉冲击力,避免千篇一律
8. 严格遵守上述指定的画面风格要求({style_description} 8. 确保视觉画面能增强文案的说服力和观众的理解度
9. 确保视觉画面能增强文案的说服力和观众的理解度
现在,请为上述 {narrations_count} 个旁白创作对应的 {narrations_count} 个**英文**图像提示词。只输出JSON不要其他内容。 现在,请为上述 {narrations_count} 个旁白创作对应的 {narrations_count} 个**英文**图像提示词。只输出JSON不要其他内容。
""" """
@@ -110,54 +108,24 @@ IMAGE_PROMPT_GENERATION_PROMPT = """# 角色定位
def build_image_prompt_prompt( def build_image_prompt_prompt(
narrations: List[str], narrations: List[str],
min_words: int, min_words: int,
max_words: int, max_words: int
image_style_preset: Optional[str] = None,
image_style_description: Optional[str] = None
) -> str: ) -> str:
""" """
Build image prompt generation prompt Build image prompt generation prompt
Note: Style/prefix will be applied later via prompt_prefix in config.
Args: Args:
narrations: List of narrations narrations: List of narrations
min_words: Minimum word count min_words: Minimum word count
max_words: Maximum word count max_words: Maximum word count
image_style_preset: Preset style name (e.g., "minimal", "stick_figure", "concept")
Available presets: see IMAGE_STYLE_PRESETS
image_style_description: Custom style description (overrides preset if provided)
Example: "warm scenes, soft lighting, professional photography"
Returns: Returns:
Formatted prompt Formatted prompt for LLM
Examples: Example:
# Use preset style
>>> build_image_prompt_prompt(narrations, 50, 100, image_style_preset="minimal")
# Use custom style
>>> build_image_prompt_prompt(
... narrations, 50, 100,
... image_style_description="cyberpunk style, neon colors, futuristic"
... )
# Use default style (stick_figure)
>>> build_image_prompt_prompt(narrations, 50, 100) >>> build_image_prompt_prompt(narrations, 50, 100)
""" """
# Determine style description
if image_style_description:
# Custom description takes priority
style_desc = image_style_description
elif image_style_preset:
# Use preset
if image_style_preset not in IMAGE_STYLE_PRESETS:
raise ValueError(
f"Unknown preset '{image_style_preset}'. "
f"Available presets: {list(IMAGE_STYLE_PRESETS.keys())}"
)
style_desc = IMAGE_STYLE_PRESETS[image_style_preset]["description"]
else:
# Use default preset
style_desc = IMAGE_STYLE_PRESETS[DEFAULT_IMAGE_STYLE]["description"]
narrations_json = json.dumps( narrations_json = json.dumps(
{"narrations": narrations}, {"narrations": narrations},
ensure_ascii=False, ensure_ascii=False,
@@ -168,7 +136,6 @@ def build_image_prompt_prompt(
narrations_json=narrations_json, narrations_json=narrations_json,
narrations_count=len(narrations), narrations_count=len(narrations),
min_words=min_words, min_words=min_words,
max_words=max_words, max_words=max_words
style_description=style_desc
) )

View File

@@ -66,7 +66,6 @@ class ReelForgeCore:
# Content generation services # Content generation services
self.narration_generator = None self.narration_generator = None
self.image_prompt_generator = None self.image_prompt_generator = None
self.generate_final_image_prompt = None
# Frame processing services # Frame processing services
self.frame_composer = None self.frame_composer = None
@@ -111,11 +110,9 @@ class ReelForgeCore:
# 5. Initialize content generation services # 5. Initialize content generation services
from reelforge.services.narration_generator import NarrationGeneratorService from reelforge.services.narration_generator import NarrationGeneratorService
from reelforge.services.image_prompt_generator import ImagePromptGeneratorService from reelforge.services.image_prompt_generator import ImagePromptGeneratorService
from reelforge.services.final_image_prompt import FinalImagePromptService
self.narration_generator = NarrationGeneratorService(self) self.narration_generator = NarrationGeneratorService(self)
self.image_prompt_generator = ImagePromptGeneratorService(self) self.image_prompt_generator = ImagePromptGeneratorService(self)
self.generate_final_image_prompt = FinalImagePromptService(self)
# 6. Initialize frame processing services # 6. Initialize frame processing services
from reelforge.services.frame_composer import FrameComposerService from reelforge.services.frame_composer import FrameComposerService

View File

@@ -14,11 +14,6 @@ from reelforge.services.image_prompt_generator import ImagePromptGeneratorServic
from reelforge.services.frame_composer import FrameComposerService from reelforge.services.frame_composer import FrameComposerService
from reelforge.services.storyboard_processor import StoryboardProcessorService from reelforge.services.storyboard_processor import StoryboardProcessorService
from reelforge.services.video_generator import VideoGeneratorService from reelforge.services.video_generator import VideoGeneratorService
from reelforge.services.final_image_prompt import (
FinalImagePromptService,
StylePreset,
PresetValue
)
__all__ = [ __all__ = [
"BaseService", "BaseService",
@@ -31,8 +26,5 @@ __all__ = [
"FrameComposerService", "FrameComposerService",
"StoryboardProcessorService", "StoryboardProcessorService",
"VideoGeneratorService", "VideoGeneratorService",
"FinalImagePromptService",
"StylePreset",
"PresetValue",
] ]

View File

@@ -1,196 +0,0 @@
"""
Final Image Prompt Service
Generates final complete image prompts by converting style descriptions
and combining them with base prompts in consistent order.
"""
from collections import namedtuple
from enum import Enum
from typing import Optional
from loguru import logger
# Define preset value structure
PresetValue = namedtuple('PresetValue', ['display_name', 'prompt'])
class StylePreset(Enum):
"""Predefined style presets for image generation"""
STICK_FIGURE = PresetValue(
display_name="Stick Figure",
prompt=(
"Pure white background, minimalist illustration, matchstick figure style, "
"black and white line drawing, simple clean lines"
),
)
MINIMAL = PresetValue(
display_name="Minimal",
prompt=(
"Simple and clean background, minimal design, soft colors, "
"professional look, modern aesthetic, uncluttered composition"
),
)
FUTURISTIC = PresetValue(
display_name="Futuristic",
prompt=(
"Futuristic sci-fi style, high-tech city background, "
"blue and silver tones, technology sense, soft neon lights, "
"cyberpunk aesthetics, digital art, advanced technology"
),
)
CINEMATIC = PresetValue(
display_name="Cinematic",
prompt=(
"Cinematic lighting, dramatic composition, film grain, "
"professional photography, depth of field, movie still quality"
),
)
class FinalImagePromptService:
"""
Final Image Prompt Service
Generates the final complete image prompt by:
1. Converting style description (preset or custom)
2. Combining style + base prompt in correct order
This ensures:
- Consistent style conversion logic across all scenarios
- Consistent prompt concatenation order (style first, then prompt)
- Single source of truth for image prompt generation
Usage:
# With preset style
final = await reelforge.generate_final_image_prompt(
prompt="A peaceful mountain landscape",
style_preset=StylePreset.FUTURISTIC
)
# With custom style (any language)
final = await reelforge.generate_final_image_prompt(
prompt="A coffee cup on table",
custom_style_description="温馨的咖啡馆,暖色调"
)
# Only prompt (no style)
final = await reelforge.generate_final_image_prompt(
prompt="A sunset over the ocean"
)
"""
def __init__(self, reelforge_core):
"""
Initialize service
Args:
reelforge_core: ReelForgeCore instance for accessing LLM
"""
self.core = reelforge_core
async def __call__(
self,
prompt: str = "",
style_preset: Optional[StylePreset] = None,
custom_style_description: str = ""
) -> str:
"""
Generate final image prompt with style
Priority:
1. custom_style_description (if provided) → convert via LLM
2. style_preset (if provided) → use predefined English prompt
3. Neither → just return prompt
Concatenation:
- Style part (if exists) comes first
- Base prompt (if exists) comes second
- Join with comma: "{style_part}, {prompt}"
Args:
prompt: Base prompt (optional, e.g., "A peaceful landscape")
style_preset: Preset style from StylePreset enum (optional)
custom_style_description: Custom description in any language (optional)
Overrides style_preset if provided
Returns:
Final complete image prompt in English
Examples:
# With preset style (IDE autocomplete!)
final = await service(
prompt="A mountain landscape",
style_preset=StylePreset.FUTURISTIC
)
# Returns: "Futuristic sci-fi style..., A mountain landscape"
# With custom style (any language)
final = await service(
prompt="A coffee cup",
custom_style_description="温馨的咖啡馆,暖色调"
)
# Returns: "Cozy coffee shop interior..., A coffee cup"
# Only prompt
final = await service(prompt="A sunset scene")
# Returns: "A sunset scene"
# Only style
final = await service(style_preset=StylePreset.MINIMAL)
# Returns: "Simple and clean background..."
"""
# Step 1: Determine style part
style_part = ""
if custom_style_description:
# Priority 1: Custom description (convert via LLM)
logger.debug(f"Converting custom style description: {custom_style_description}")
style_part = await self._convert_custom_style(custom_style_description)
elif style_preset:
# Priority 2: Preset style (use prompt from enum value)
style_part = style_preset.value.prompt
logger.debug(f"Using preset style: {style_preset.name}")
# Step 2: Combine parts with comma
parts = [p for p in [style_part, prompt] if p]
final_prompt = ", ".join(parts)
if final_prompt:
logger.debug(f"Final image prompt: {final_prompt}")
else:
logger.warning("Generated empty image prompt")
return final_prompt
async def _convert_custom_style(self, description: str) -> str:
"""
Convert custom style description to English image prompt via LLM
Args:
description: User's style description in any language
Returns:
Converted English image prompt suitable for image generation models
"""
from reelforge.prompts import build_style_conversion_prompt
# Build prompt using template
llm_prompt = build_style_conversion_prompt(description)
style_prompt = await self.core.llm(llm_prompt)
# Clean up the result (remove extra whitespace, newlines)
style_prompt = " ".join(style_prompt.strip().split())
logger.debug(f"Converted custom style to: {style_prompt}")
return style_prompt

View File

@@ -28,8 +28,6 @@ class ImagePromptGeneratorService:
self, self,
narrations: List[str], narrations: List[str],
config: StoryboardConfig, config: StoryboardConfig,
image_style_preset: str = None,
image_style_description: str = None,
batch_size: int = 10, batch_size: int = 10,
max_retries: int = 3, max_retries: int = 3,
progress_callback: Optional[Callable] = None progress_callback: Optional[Callable] = None
@@ -40,14 +38,12 @@ class ImagePromptGeneratorService:
Args: Args:
narrations: List of narrations narrations: List of narrations
config: Storyboard configuration config: Storyboard configuration
image_style_preset: Preset style name (e.g., "minimal", "futuristic")
image_style_description: Custom style description (overrides preset)
batch_size: Max narrations per batch (default: 10) batch_size: Max narrations per batch (default: 10)
max_retries: Max retry attempts per batch (default: 3) max_retries: Max retry attempts per batch (default: 3)
progress_callback: Optional callback(completed, total, message) for progress updates progress_callback: Optional callback(completed, total, message) for progress updates
Returns: Returns:
List of image prompts with style applied List of image prompts with prompt_prefix applied (from config)
Raises: Raises:
ValueError: If batch fails after max_retries ValueError: If batch fails after max_retries
@@ -117,28 +113,20 @@ class ImagePromptGeneratorService:
base_prompts = all_base_prompts base_prompts = all_base_prompts
logger.info(f"✅ All batches completed. Total prompts: {len(base_prompts)}") logger.info(f"✅ All batches completed. Total prompts: {len(base_prompts)}")
# 5. Apply style to each prompt using FinalImagePromptService # 5. Apply prompt prefix to each prompt
from reelforge.services.final_image_prompt import StylePreset from reelforge.utils.prompt_helper import build_image_prompt
# Convert style preset name to enum if provided # Get prompt prefix from config
style_preset_enum = None image_config = self.core.config.get("image", {})
if image_style_preset: prompt_prefix = image_config.get("prompt_prefix", "")
try:
style_preset_enum = StylePreset[image_style_preset.upper()]
except KeyError:
logger.warning(f"Unknown style preset: {image_style_preset}")
# Apply style to each base prompt # Apply prefix to each base prompt
final_prompts = [] final_prompts = []
for base_prompt in base_prompts: for base_prompt in base_prompts:
final_prompt = await self.core.generate_final_image_prompt( final_prompt = build_image_prompt(base_prompt, prompt_prefix)
prompt=base_prompt,
style_preset=style_preset_enum,
custom_style_description=image_style_description or ""
)
final_prompts.append(final_prompt) final_prompts.append(final_prompt)
logger.info(f"Generated {len(final_prompts)} final image prompts with style applied") logger.info(f"Generated {len(final_prompts)} final image prompts with prefix applied")
return final_prompts return final_prompts
async def _generate_batch_prompts( async def _generate_batch_prompts(
@@ -170,9 +158,7 @@ class ImagePromptGeneratorService:
prompt = build_image_prompt_prompt( prompt = build_image_prompt_prompt(
narrations=batch_narrations, narrations=batch_narrations,
min_words=config.min_image_prompt_words, min_words=config.min_image_prompt_words,
max_words=config.max_image_prompt_words, max_words=config.max_image_prompt_words
image_style_preset=None,
image_style_description=None
) )
# 2. Call LLM # 2. Call LLM

View File

@@ -140,9 +140,10 @@ class StoryboardProcessorService:
"""Step 2: Generate image using ComfyKit""" """Step 2: Generate image using ComfyKit"""
logger.debug(f" 2/4: Generating image for frame {frame.index}...") logger.debug(f" 2/4: Generating image for frame {frame.index}...")
# Call Image generation (using default preset) # Call Image generation (with optional preset)
image_url = await self.core.image( image_url = await self.core.image(
prompt=frame.image_prompt, prompt=frame.image_prompt,
preset=config.image_preset, # Pass preset from config (None = use default)
width=config.image_width, width=config.image_width,
height=config.image_height height=config.image_height
) )
@@ -201,19 +202,17 @@ class StoryboardProcessorService:
from pathlib import Path from pathlib import Path
# Resolve template path # Resolve template path
template_name = config.frame_template template_filename = config.frame_template
if not template_name.endswith('.html'):
template_name = f"{template_name}.html"
# Try templates/ directory first # Try templates/ directory first
template_path = Path(f"templates/{template_name}") template_path = Path(f"templates/{template_filename}")
if not template_path.exists(): if not template_path.exists():
# Try as absolute path # Try as absolute/relative path
template_path = Path(template_name) template_path = Path(template_filename)
if not template_path.exists(): if not template_path.exists():
raise FileNotFoundError( raise FileNotFoundError(
f"Template not found: {template_name}. " f"Template not found: {template_filename}. "
f"Available templates: classic, modern, minimal" f"Built-in templates: default.html, modern.html, neon.html"
) )
# Get storyboard for content metadata # Get storyboard for content metadata

View File

@@ -101,7 +101,7 @@ class VideoService:
- "filter": Slower but handles different formats - "filter": Slower but handles different formats
bgm_path: Background music file path (optional) bgm_path: Background music file path (optional)
- None: No BGM - None: No BGM
- "default" or "happy": Use built-in BGM from bgm/ folder - Filename (e.g., "default.mp3", "happy.mp3"): Use built-in BGM from bgm/ folder
- Custom path: Use custom BGM file - Custom path: Use custom BGM file
bgm_volume: BGM volume level (0.0-1.0), default 0.2 bgm_volume: BGM volume level (0.0-1.0), default 0.2
bgm_mode: BGM playback mode bgm_mode: BGM playback mode
@@ -504,11 +504,11 @@ class VideoService:
def _resolve_bgm_path(self, bgm_path: str) -> str: def _resolve_bgm_path(self, bgm_path: str) -> str:
""" """
Resolve BGM path (preset name or custom path) Resolve BGM path (filename or custom path)
Args: Args:
bgm_path: Can be: bgm_path: Can be:
- Preset name (e.g., "default", "happy"): auto-resolved from bgm/ directory - Filename with extension (e.g., "default.mp3", "happy.mp3"): auto-resolved from bgm/ directory
- Custom file path (absolute or relative) - Custom file path (absolute or relative)
Returns: Returns:
@@ -521,8 +521,8 @@ class VideoService:
if os.path.exists(bgm_path): if os.path.exists(bgm_path):
return os.path.abspath(bgm_path) return os.path.abspath(bgm_path)
# Try as preset in bgm/ directory # Try as filename in bgm/ directory
preset_path = f"bgm/{bgm_path}.mp3" preset_path = f"bgm/{bgm_path}"
if os.path.exists(preset_path): if os.path.exists(preset_path):
return os.path.abspath(preset_path) return os.path.abspath(preset_path)
@@ -532,9 +532,9 @@ class VideoService:
os.path.abspath(preset_path) os.path.abspath(preset_path)
] ]
# List available presets # List available BGM files
available_bgm = self._list_available_bgm() available_bgm = self._list_available_bgm()
available_msg = f"\n Available presets: {', '.join(available_bgm)}" if available_bgm else "" available_msg = f"\n Available BGM files: {', '.join(available_bgm)}" if available_bgm else ""
raise FileNotFoundError( raise FileNotFoundError(
f"BGM file not found: '{bgm_path}'\n" f"BGM file not found: '{bgm_path}'\n"
@@ -546,10 +546,10 @@ class VideoService:
def _list_available_bgm(self) -> list[str]: def _list_available_bgm(self) -> list[str]:
""" """
List available preset BGM files List available BGM files in bgm/ directory
Returns: Returns:
List of preset names (without .mp3 extension) List of filenames (with extensions)
""" """
bgm_dir = "bgm" bgm_dir = "bgm"
if not os.path.exists(bgm_dir): if not os.path.exists(bgm_dir):
@@ -557,7 +557,9 @@ class VideoService:
try: try:
files = os.listdir(bgm_dir) files = os.listdir(bgm_dir)
return [f[:-4] for f in files if f.endswith('.mp3')] # Return all audio files (mp3, wav, ogg, flac, etc.)
audio_extensions = ('.mp3', '.wav', '.ogg', '.flac', '.m4a', '.aac')
return [f for f in files if f.lower().endswith(audio_extensions)]
except Exception: except Exception:
return [] return []

View File

@@ -66,8 +66,7 @@ class VideoGeneratorService:
# === Image Parameters === # === Image Parameters ===
image_width: int = 1024, image_width: int = 1024,
image_height: int = 1024, image_height: int = 1024,
image_style_preset: Optional[str] = None, image_preset: Optional[str] = None,
image_style_description: Optional[str] = None,
# === Video Parameters === # === Video Parameters ===
video_width: int = 1080, video_width: int = 1080,
@@ -77,6 +76,9 @@ class VideoGeneratorService:
# === Frame Template === # === Frame Template ===
frame_template: Optional[str] = None, frame_template: Optional[str] = None,
# === Image Style ===
prompt_prefix: Optional[str] = None,
# === BGM Parameters === # === BGM Parameters ===
bgm_path: Optional[str] = None, bgm_path: Optional[str] = None,
bgm_volume: float = 0.2, bgm_volume: float = 0.2,
@@ -119,17 +121,19 @@ class VideoGeneratorService:
image_width: Generated image width (default 1024) image_width: Generated image width (default 1024)
image_height: Generated image height (default 1024) image_height: Generated image height (default 1024)
image_style_preset: Preset style name (e.g., "minimal", "concept", "cinematic") image_preset: Image workflow preset (e.g., "flux", "sdxl", None = use default)
image_style_description: Custom style description (overrides preset)
video_width: Final video width (default 1080) video_width: Final video width (default 1080)
video_height: Final video height (default 1920) video_height: Final video height (default 1920)
video_fps: Video frame rate (default 30) video_fps: Video frame rate (default 30)
frame_template: HTML template name or path (None = use PIL) frame_template: HTML template filename or path (None = use PIL)
e.g., "classic", "modern", "minimal", or custom path e.g., "default.html", "modern.html", "neon.html", or custom path
bgm_path: BGM path ("default", "happy", custom path, or None) 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_volume: BGM volume 0.0-1.0 (default 0.2)
bgm_mode: BGM mode "once" or "loop" (default "loop") bgm_mode: BGM mode "once" or "loop" (default "loop")
@@ -211,6 +215,7 @@ class VideoGeneratorService:
voice_id=voice_id, voice_id=voice_id,
image_width=image_width, image_width=image_width,
image_height=image_height, image_height=image_height,
image_preset=image_preset,
frame_template=frame_template frame_template=frame_template
) )
@@ -248,25 +253,36 @@ class VideoGeneratorService:
# Step 2: Generate image prompts # Step 2: Generate image prompts
self._report_progress(progress_callback, "generating_image_prompts", 0.15) self._report_progress(progress_callback, "generating_image_prompts", 0.15)
# Create progress callback wrapper for image prompt generation (15%-30% range) # Override prompt_prefix if provided (temporarily modify config)
def image_prompt_progress(completed: int, total: int, message: str): original_prefix = None
# Map batch progress to 15%-30% range if prompt_prefix is not None:
batch_progress = completed / total if total > 0 else 0 image_config = self.core.config.get("image", {})
overall_progress = 0.15 + (batch_progress * 0.15) # 15% -> 30% original_prefix = image_config.get("prompt_prefix")
self._report_progress( image_config["prompt_prefix"] = prompt_prefix
progress_callback, logger.info(f"Using custom prompt_prefix: '{prompt_prefix}'")
"generating_image_prompts",
overall_progress,
extra_info=message
)
image_prompts = await self.core.image_prompt_generator.generate_image_prompts( try:
narrations=narrations, # Create progress callback wrapper for image prompt generation (15%-30% range)
config=config, def image_prompt_progress(completed: int, total: int, message: str):
image_style_preset=image_style_preset, # Map batch progress to 15%-30% range
image_style_description=image_style_description, batch_progress = completed / total if total > 0 else 0
progress_callback=image_prompt_progress overall_progress = 0.15 + (batch_progress * 0.15) # 15% -> 30%
) self._report_progress(
progress_callback,
"generating_image_prompts",
overall_progress,
extra_info=message
)
image_prompts = await self.core.image_prompt_generator.generate_image_prompts(
narrations=narrations,
config=config,
progress_callback=image_prompt_progress
)
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") logger.info(f"✅ Generated {len(image_prompts)} image prompts")
# Step 3: Create frames # Step 3: Create frames

View File

@@ -0,0 +1,38 @@
"""
Prompt helper utilities
Simple utilities for building prompts with optional prefixes.
"""
def build_image_prompt(prompt: str, prefix: str = "") -> str:
"""
Build final image prompt with optional prefix
Args:
prompt: User's raw prompt
prefix: Optional prefix to add before the prompt
Returns:
Final prompt with prefix applied (if provided)
Examples:
>>> build_image_prompt("a cat", "")
'a cat'
>>> build_image_prompt("a cat", "anime style")
'anime style, a cat'
>>> build_image_prompt("a cat", " anime style ")
'anime style, a cat'
"""
prefix = prefix.strip() if prefix else ""
prompt = prompt.strip() if prompt else ""
if prefix and prompt:
return f"{prefix}, {prompt}"
elif prefix:
return prefix
else:
return prompt

View File

@@ -15,5 +15,5 @@ if [ ! -f config.yaml ]; then
fi fi
# Start Streamlit # Start Streamlit
uv run streamlit run web.py --server.port=8501 --server.address=localhost uv run streamlit run web.py

View File

@@ -1,274 +0,0 @@
# HTML Templates Guide
## 📸 Preset Templates
ReelForge provides the following preset templates:
| Template | Style | Preview |
|----------|-------|---------|
| `classic` | Classic black & white, minimalist professional | Clean white background with elegant typography |
| `modern` | Modern gradient with glassmorphism effects | Purple-blue gradient with frosted glass style |
| `minimal` | Minimalist with ample whitespace | Light gray background with refined design |
### Usage
```python
from reelforge.service import reelforge
await reelforge.initialize()
# Use preset template
result = await reelforge.generate_video(
topic="为什么阅读改变命运",
frame_template="classic" # or "modern", "minimal"
)
```
---
## 🎨 Creating Custom Templates
### Method 1: Generate with LLM (Recommended ⭐, Zero Code)
**For users who want unique styles but don't want to write code**
#### Step 1: Open Prompt File
Open [`prompts/generate_html_template.txt`](../prompts/generate_html_template.txt) in the project root directory.
Or view online: [View Prompt File](https://github.com/xxx/ReelForge/blob/main/prompts/generate_html_template.txt)
#### Step 2: Copy Prompt, Modify Style Description
Find this section in the prompt:
```
## Visual Style
[👉 Replace this section with your desired style]
```
Replace with your desired style, for example:
```
## Visual Style
Cyberpunk style with neon lights, dark background, purple and blue gradients
```
#### Step 3: Paste to LLM Platform
Copy the entire prompt and paste it into any of these platforms:
- 💬 ChatGPT: https://chat.openai.com
- 💬 Claude: https://claude.ai
- 💬 Tongyi Qianwen: https://tongyi.aliyun.com
- 💬 DeepSeek: https://chat.deepseek.com
- 💬 Doubao: https://www.doubao.com
- 💬 Or any LLM you prefer
#### Step 4: Save HTML
Copy the HTML code returned by the LLM.
⚠️ **Note:** If the LLM returns HTML wrapped in \`\`\`html\`\`\`, only copy the HTML inside (excluding \`\`\`html and \`\`\`)
Save as:
```
templates/my-cyberpunk.html
```
#### Step 5: Use Template
```python
await reelforge.generate_video(
topic="...",
frame_template="my-cyberpunk" # Use your template
)
```
**🎉 Done! Your video now uses your custom style!**
---
### Example Style Descriptions
Here are some style descriptions you can use or adapt:
- "Cyberpunk style with neon lights, dark background, purple gradient"
- "Fresh and clean style, light blue background, rounded cards, soft shadows"
- "Professional business style, dark blue with gold accents, serious and dignified"
- "Cute cartoon style, pink theme, rounded fonts, rainbow gradient"
- "Vintage retro style, sepia tones, aged paper texture, rounded borders"
- "Tech minimalist style, pure white background, thin borders, subtle animations"
- "Warm reading style, beige background, book texture, soft lighting"
---
### Method 2: Write HTML Manually (Full Control)
**For users familiar with HTML/CSS**
Create an HTML file directly:
```html
<!-- templates/my-style.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {
margin: 0;
width: 1080px;
height: 1920px;
background: #ffffff;
font-family: 'PingFang SC', 'Source Han Sans', 'Microsoft YaHei', sans-serif;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 60px;
box-sizing: border-box;
}
.topic {
font-size: 72px;
font-weight: bold;
color: #333;
text-align: center;
}
.image-container {
width: 100%;
height: 900px;
display: flex;
align-items: center;
justify-content: center;
}
.image-container img {
max-width: 100%;
max-height: 100%;
border-radius: 20px;
}
.text {
font-size: 42px;
color: #666;
text-align: center;
line-height: 1.8;
}
.book-info {
font-size: 36px;
color: #999;
text-align: center;
margin-top: 30px;
}
</style>
</head>
<body>
<div class="topic">{{topic}}</div>
<div class="image-container">
<img src="{{image}}" alt="Frame Image">
</div>
<div>
<div class="text">{{text}}</div>
<div class="book-info">《{{book_title}}》 - {{book_author}}</div>
</div>
</body>
</html>
```
**Available Variables:**
Required (always available):
- `{{topic}}` - Video topic
- `{{text}}` - Current frame narration text
- `{{image}}` - AI-generated image path
Optional (available via `ext` parameter):
- `{{book_title}}` - Book title
- `{{book_author}}` - Author
- `{{book_cover}}` - Book cover path
- `{{book_rating}}` - Book rating
- And any other custom fields you pass via `ext`
---
## 🤝 Share Your Templates
Created a beautiful template? Share it with the community!
1. Fork this repository
2. Add your HTML to `templates/community/`
3. Submit a Pull Request
Excellent templates will be featured in the official template gallery!
---
## ❓ FAQ
**Q: What if the HTML generated by LLM has syntax errors?**
A: Regenerate, or emphasize "ensure HTML syntax is correct" in the prompt
**Q: What if I'm not satisfied with the generated style?**
A: Modify the style description in the prompt, or manually edit the HTML file
**Q: Can I use external CSS frameworks (like Tailwind)?**
A: Yes, but you need CDN links. We recommend inline CSS for offline compatibility
**Q: Where should template files be placed?**
A: In the `templates/` directory. The filename is the template name (without .html extension)
**Q: What's the recommended canvas size?**
A: 1080x1920 (9:16 portrait for TikTok/Douyin)
**Q: Can I use custom fonts?**
A: Yes, but system fonts are recommended (PingFang SC, Microsoft YaHei, etc.) for compatibility
---
## 📚 Technical Details
### How HTML Rendering Works
1. Load HTML template file
2. Replace variables (`{{topic}}`, `{{text}}`, etc.) with actual data
3. Render HTML to image using `html2image` (Chrome headless)
4. Save as frame image
### Performance
- First render: ~1-2 seconds (browser initialization)
- Subsequent renders: ~0.5-1 second per frame
- For 5-frame videos: acceptable total time
### Browser Requirements
`html2image` uses Chrome/Chromium headless mode. It will automatically find:
- System Chrome
- System Chromium
- Or download a lightweight browser engine
---
## 💡 Tips
1. **Preview before generating**: Open HTML in browser to preview effect
2. **Maintain aspect ratio**: Keep 1080x1920 for best results
3. **Watch text length**: Ensure long text wraps properly (`word-wrap`, `overflow-wrap`)
4. **Consider contrast**: Ensure text is readable against background
5. **Use relative units**: `%` and `vh/vw` for better responsiveness
---
## 🎨 Inspiration Gallery
Looking for inspiration? Check out:
- [Official Template Gallery](https://reelforge.ai/templates) (coming soon)
- [Community Templates](./community/) (coming soon)
- Design sites: Dribbble, Behance, Pinterest
---
Need help? Open an [Issue](https://github.com/xxx/ReelForge/issues) or join our [Discord](https://discord.gg/xxx)!

256
web.py
View File

@@ -77,51 +77,32 @@ def init_i18n():
# Preview Cache Functions # Preview Cache Functions
# ============================================================================ # ============================================================================
def generate_style_preview_cached( def generate_style_preview_cached(prompt_prefix: str):
style_preset_name: str,
custom_style_description: str
):
""" """
Generate and cache visual style preview Generate and cache visual style preview
Args: Args:
style_preset_name: Preset style name ("minimal", "futuristic", etc.) or empty prompt_prefix: Prompt prefix to test
custom_style_description: Custom description or empty
Returns: Returns:
Tuple of (image_path, converted_prompt_if_custom) Generated image path
""" """
from reelforge.services.final_image_prompt import StylePreset from reelforge.utils.prompt_helper import build_image_prompt
reelforge = get_reelforge() reelforge = get_reelforge()
# Convert style preset name to enum # Build final prompt with prefix
style_preset = None test_prompt = "A peaceful mountain landscape"
if style_preset_name: final_prompt = build_image_prompt(test_prompt, prompt_prefix)
try:
style_preset = StylePreset[style_preset_name.upper()]
except KeyError:
pass
# Generate final prompt using the new service
final_prompt = run_async(reelforge.generate_final_image_prompt(
prompt="A peaceful mountain landscape",
style_preset=style_preset,
custom_style_description=custom_style_description
))
# Generate preview image (small size for speed) # Generate preview image (small size for speed)
# Using default preset (workflows/image_default.json)
preview_image_path = run_async(reelforge.image( preview_image_path = run_async(reelforge.image(
prompt=final_prompt, prompt=final_prompt,
width=512, width=512,
height=512 height=512
)) ))
# Return converted prompt only for custom mode return preview_image_path
converted_prompt = final_prompt if custom_style_description else None
return preview_image_path, converted_prompt
# ============================================================================ # ============================================================================
@@ -452,53 +433,7 @@ def main():
st.markdown("---") st.markdown("---")
# Background music # Voice selection (moved from middle column)
st.markdown(f"**{tr('bgm.title')}**")
# Dynamically scan bgm folder for music files (support common audio formats)
bgm_folder = Path("bgm")
bgm_files = []
if bgm_folder.exists():
audio_extensions = ["*.mp3", "*.wav", "*.flac", "*.m4a", "*.aac", "*.ogg"]
for ext in audio_extensions:
bgm_files.extend([f.name for f in bgm_folder.glob(ext)])
bgm_files.sort()
# Add special "None" option
bgm_options = [tr("bgm.none")] + bgm_files
# Default to "default.mp3" if exists, otherwise first option
default_index = 0
if "default.mp3" in bgm_files:
default_index = bgm_options.index("default.mp3")
bgm_choice = st.selectbox(
"BGM",
bgm_options,
index=default_index,
label_visibility="collapsed"
)
# BGM preview button (only if BGM is not "None")
if bgm_choice != tr("bgm.none"):
if st.button(tr("bgm.preview"), key="preview_bgm", use_container_width=True):
bgm_file_path = f"bgm/{bgm_choice}"
if os.path.exists(bgm_file_path):
st.audio(bgm_file_path)
else:
st.error(tr("bgm.preview_failed", file=bgm_choice))
# Extract filename without extension for bgm_path (service layer expects stem only)
bgm_path = None if bgm_choice == tr("bgm.none") else Path(bgm_choice).stem
# ========================================================================
# Middle Column: Custom Settings (Voice & Visual Style & Template)
# ========================================================================
with middle_col:
with st.container(border=True):
st.markdown(f"**{tr('section.style_settings')}**")
# Voice selection
st.markdown(f"**{tr('voice.title')}**") st.markdown(f"**{tr('voice.title')}**")
voice_id = st.selectbox( voice_id = st.selectbox(
"Voice", "Voice",
@@ -538,63 +473,108 @@ def main():
except Exception as e: except Exception as e:
st.error(tr("voice.preview_failed", error=str(e))) st.error(tr("voice.preview_failed", error=str(e)))
logger.exception(e) logger.exception(e)
# ========================================================================
# Middle Column: Custom Settings (BGM & Visual Style & Template)
# ========================================================================
with middle_col:
with st.container(border=True):
st.markdown(f"**{tr('section.style_settings')}**")
st.markdown("---") # Background music (moved from left column)
st.markdown(f"**{tr('bgm.title')}**")
st.caption(tr("bgm.custom_help"))
# Visual style (Illustration Style) # Dynamically scan bgm folder for music files (support common audio formats)
st.markdown(f"**{tr('style.title')}**") bgm_folder = Path("bgm")
bgm_files = []
if bgm_folder.exists():
audio_extensions = ["*.mp3", "*.wav", "*.flac", "*.m4a", "*.aac", "*.ogg"]
for ext in audio_extensions:
bgm_files.extend([f.name for f in bgm_folder.glob(ext)])
bgm_files.sort()
# Get available presets dynamically from StylePreset enum # Add special "None" option
from reelforge.services.final_image_prompt import StylePreset bgm_options = [tr("bgm.none")] + bgm_files
# Build preset options (enum values + custom) # Default to "default.mp3" if exists, otherwise first option
preset_options = [preset.name.lower() for preset in StylePreset] default_index = 0
preset_options.append("custom") if "default.mp3" in bgm_files:
default_index = bgm_options.index("default.mp3")
# Dynamic display name function bgm_choice = st.selectbox(
def get_preset_display(preset_name: str) -> str: "BGM",
if preset_name == "custom": bgm_options,
return tr("style.custom") index=default_index,
# Find the enum by name and get its display_name
for preset in StylePreset:
if preset.name.lower() == preset_name:
return preset.value.display_name
return preset_name.replace("_", " ").title()
# Change from radio to selectbox (dropdown)
style_preset = st.selectbox(
"Style Preset",
preset_options,
format_func=get_preset_display,
label_visibility="collapsed" label_visibility="collapsed"
) )
# Custom style description input (only show for custom mode) # BGM preview button (only if BGM is not "None")
custom_style_description = "" if bgm_choice != tr("bgm.none"):
if style_preset == "custom": if st.button(tr("bgm.preview"), key="preview_bgm", use_container_width=True):
custom_style_description = st.text_area( bgm_file_path = f"bgm/{bgm_choice}"
tr("style.description"), if os.path.exists(bgm_file_path):
placeholder=tr("style.description_placeholder"), st.audio(bgm_file_path)
height=80, else:
label_visibility="collapsed" st.error(tr("bgm.preview_failed", file=bgm_choice))
)
# Use full filename for bgm_path (including extension)
bgm_path = None if bgm_choice == tr("bgm.none") else bgm_choice
# Visual style (Workflow + Prompt Prefix)
st.markdown(f"**{tr('style.title')}**")
# 1. ComfyUI Workflow selection
st.caption(tr("style.workflow"))
st.caption(tr("style.workflow_help"))
# Dynamically scan workflows folder for image_*.json files
workflows_folder = Path("workflows")
workflow_files = []
if workflows_folder.exists():
workflow_files = sorted([f.name for f in workflows_folder.glob("image_*.json")])
# Default to "image_default.json" if exists, otherwise first option
default_workflow_index = 0
if "image_default.json" in workflow_files:
default_workflow_index = workflow_files.index("image_default.json")
workflow_filename = st.selectbox(
"Workflow",
workflow_files if workflow_files else ["image_default.json"],
index=default_workflow_index,
label_visibility="collapsed",
key="image_preset_select"
)
# Extract preset name from filename: "image_default.json" -> "default"
image_preset = workflow_filename.replace("image_", "").replace(".json", "") if workflow_filename else None
# 2. Prompt prefix input
st.caption(tr("style.prompt_prefix"))
# Get current prompt_prefix from config
image_config = config_manager.config.get("image", {})
current_prefix = image_config.get("prompt_prefix", "")
# Prompt prefix input (temporary, not saved to config)
prompt_prefix = st.text_area(
"Prompt Prefix",
value=current_prefix,
placeholder=tr("style.prompt_prefix_placeholder"),
height=80,
label_visibility="collapsed",
help=tr("style.prompt_prefix_help")
)
# Visual style preview button # Visual style preview button
if st.button(tr("style.preview"), key="preview_style", use_container_width=True): if st.button(tr("style.preview"), key="preview_style", use_container_width=True):
with st.spinner(tr("style.previewing")): with st.spinner(tr("style.previewing")):
try: try:
# Generate preview using cached function # Generate preview using cached function
preset_name = style_preset if style_preset != "custom" else "" preview_image_path = generate_style_preview_cached(prompt_prefix)
custom_desc = custom_style_description if style_preset == "custom" else ""
preview_image_path, generated_prompt = generate_style_preview_cached(
style_preset_name=preset_name,
custom_style_description=custom_desc
)
# Show generated prompt if custom mode
if generated_prompt:
st.info(tr("style.generated_prompt", prompt=generated_prompt))
# Display preview (support both URL and local path) # Display preview (support both URL and local path)
if preview_image_path: if preview_image_path:
@@ -609,21 +589,39 @@ def main():
img_html = f'<div class="preview-image"><img src="data:image/png;base64,{img_data}" alt="Style Preview"/></div>' img_html = f'<div class="preview-image"><img src="data:image/png;base64,{img_data}" alt="Style Preview"/></div>'
st.markdown(img_html, unsafe_allow_html=True) st.markdown(img_html, unsafe_allow_html=True)
st.caption(tr("style.preview_caption")) st.caption("Preview with test prompt: 'A peaceful mountain landscape'")
# Show the final prompt used
from reelforge.utils.prompt_helper import build_image_prompt
test_prompt = "A peaceful mountain landscape"
final_prompt = build_image_prompt(test_prompt, prompt_prefix)
st.info(f"Final prompt used: {final_prompt}")
else: else:
st.error("Failed to generate preview image") st.error("Failed to generate preview image")
except Exception as e: except Exception as e:
st.error(tr("style.preview_failed", error=str(e))) st.error(tr("style.preview_failed", error=str(e)))
logger.exception(e) logger.exception(e)
st.markdown("---")
# Frame template (moved from right column) # Frame template (moved from right column)
st.markdown(f"**{tr('template.title')}**") st.markdown(f"**{tr('template.title')}**")
st.caption(tr("template.custom_help"))
# Dynamically scan templates folder for HTML files
templates_folder = Path("templates")
template_files = []
if templates_folder.exists():
template_files = sorted([f.name for f in templates_folder.glob("*.html")])
# Default to default.html if exists, otherwise first option
default_template_index = 0
if "default.html" in template_files:
default_template_index = template_files.index("default.html")
frame_template = st.selectbox( frame_template = st.selectbox(
"Template", "Template",
["classic", "modern", "neon"], template_files if template_files else ["default.html"],
format_func=lambda x: tr(f"template.{x}"), index=default_template_index,
label_visibility="collapsed" label_visibility="collapsed"
) )
@@ -688,26 +686,16 @@ def main():
status_text.text(message) status_text.text(message)
progress_bar.progress(min(int(event.progress * 100), 99)) # Cap at 99% until complete progress_bar.progress(min(int(event.progress * 100), 99)) # Cap at 99% until complete
# Prepare image style parameters # Generate video (directly pass parameters)
style_preset_param = None
style_description_param = None
if style_preset == "custom":
# Custom mode: pass user description as-is
style_description_param = custom_style_description
else:
# Preset mode: pass preset name
style_preset_param = style_preset
result = run_async(reelforge.generate_video( result = run_async(reelforge.generate_video(
text=text, text=text,
mode=mode, mode=mode,
title=title if title else None, title=title if title else None,
n_scenes=n_scenes, n_scenes=n_scenes,
voice_id=voice_id, voice_id=voice_id,
image_style_preset=style_preset_param, image_preset=image_preset, # Pass image_preset
image_style_description=style_description_param,
frame_template=frame_template, frame_template=frame_template,
prompt_prefix=prompt_prefix, # Pass prompt_prefix
bgm_path=bgm_path, bgm_path=bgm_path,
progress_callback=update_progress, progress_callback=update_progress,
)) ))