优化自定义逻辑
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "💾 保存配置",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 []
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
38
reelforge/utils/prompt_helper.py
Normal file
38
reelforge/utils/prompt_helper.py
Normal 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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
256
web.py
@@ -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,
|
||||||
))
|
))
|
||||||
|
|||||||
Reference in New Issue
Block a user