diff --git a/.gitignore b/.gitignore index 749dc95..1a74ec8 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,10 @@ repositories/ workflows/* !workflows/*_default.json !workflows/README.md + +# Templates - ignore user customizations but keep presets +templates/* +!templates/default.html +!templates/modern.html +!templates/neon.html +!templates/README.md diff --git a/config.example.yaml b/config.example.yaml index b585b5f..b29f02b 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -56,6 +56,16 @@ image: default: default # Default preset name (uses workflows/image_default.json) comfyui_url: http://127.0.0.1:8188 # Local ComfyUI server # 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 ==================== # 1. LLM Configuration: diff --git a/reelforge/i18n/locales/en_US.json b/reelforge/i18n/locales/en_US.json index 9ab0171..7bae6bb 100644 --- a/reelforge/i18n/locales/en_US.json +++ b/reelforge/i18n/locales/en_US.json @@ -56,7 +56,12 @@ "voice.previewing": "Generating voice preview...", "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.description": "Style Description", "style.description_placeholder": "Describe the illustration style you want (any language)...", @@ -67,9 +72,10 @@ "style.generated_prompt": "Generated prompt: {prompt}", "template.title": "📐 Storyboard Template", - "template.classic": "Classic", + "template.default": "Default", "template.modern": "Modern", "template.neon": "Neon", + "template.custom_help": "💡 Custom: Place .html files in templates/ folder", "video.title": "🎬 Video Settings", "video.frames": "Scenes", @@ -81,6 +87,7 @@ "bgm.none": "🔇 No BGM", "bgm.preview": "▶ Preview Music", "bgm.preview_failed": "❌ Music file not found: {file}", + "bgm.custom_help": "💡 Custom: Place audio files in bgm/ folder", "btn.generate": "🎬 Generate Video", "btn.save_config": "💾 Save Configuration", diff --git a/reelforge/i18n/locales/zh_CN.json b/reelforge/i18n/locales/zh_CN.json index 3e42858..5ecb863 100644 --- a/reelforge/i18n/locales/zh_CN.json +++ b/reelforge/i18n/locales/zh_CN.json @@ -56,7 +56,12 @@ "voice.previewing": "正在生成语音预览...", "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.description": "风格描述", "style.description_placeholder": "描述您想要的插图风格(任何语言)...", @@ -67,9 +72,10 @@ "style.generated_prompt": "生成的提示词:{prompt}", "template.title": "📐 分镜模板", - "template.classic": "Classic", - "template.modern": "Modern", - "template.neon": "Neon", + "template.default": "默认", + "template.modern": "现代", + "template.neon": "霓虹", + "template.custom_help": "💡 自定义:将 .html 文件放入 templates/ 文件夹", "video.title": "🎬 视频设置", "video.frames": "分镜数", @@ -81,6 +87,7 @@ "bgm.none": "🔇 无背景音乐", "bgm.preview": "▶ 试听音乐", "bgm.preview_failed": "❌ 音乐文件未找到:{file}", + "bgm.custom_help": "💡 自定义:将音频文件放入 bgm/ 文件夹", "btn.generate": "🎬 生成视频", "btn.save_config": "💾 保存配置", diff --git a/reelforge/models/storyboard.py b/reelforge/models/storyboard.py index a5a0fee..48e0727 100644 --- a/reelforge/models/storyboard.py +++ b/reelforge/models/storyboard.py @@ -27,6 +27,7 @@ class StoryboardConfig: # Image parameters image_width: int = 1024 image_height: int = 1024 + image_preset: Optional[str] = None # Image workflow preset (None = use default) # Frame template frame_template: Optional[str] = None # HTML template name or path (None = use PIL) diff --git a/reelforge/prompts/image_generation.py b/reelforge/prompts/image_generation.py index fc51fc0..8dbdf6b 100644 --- a/reelforge/prompts/image_generation.py +++ b/reelforge/prompts/image_generation.py @@ -50,7 +50,6 @@ IMAGE_PROMPT_GENERATION_PROMPT = """# 角色定位 ## 图像提示词规范 - 语言:**必须使用英文**(用于 AI 图像生成模型) -- 画面风格:{style_description} - 描述结构:scene + character action + emotion + symbolic elements - 描述长度:确保描述清晰完整且富有创意(建议 50-100 个英文单词) @@ -100,8 +99,7 @@ IMAGE_PROMPT_GENERATION_PROMPT = """# 角色定位 5. **图像提示词必须使用英文**(for AI image generation models) 6. 图像提示词必须准确反映对应旁白的具体内容和情感 7. 每个图像都要有创意性和视觉冲击力,避免千篇一律 -8. 严格遵守上述指定的画面风格要求({style_description}) -9. 确保视觉画面能增强文案的说服力和观众的理解度 +8. 确保视觉画面能增强文案的说服力和观众的理解度 现在,请为上述 {narrations_count} 个旁白创作对应的 {narrations_count} 个**英文**图像提示词。只输出JSON,不要其他内容。 """ @@ -110,54 +108,24 @@ IMAGE_PROMPT_GENERATION_PROMPT = """# 角色定位 def build_image_prompt_prompt( narrations: List[str], min_words: int, - max_words: int, - image_style_preset: Optional[str] = None, - image_style_description: Optional[str] = None + max_words: int ) -> str: """ Build image prompt generation prompt + Note: Style/prefix will be applied later via prompt_prefix in config. + Args: narrations: List of narrations min_words: Minimum 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: - Formatted prompt + Formatted prompt for LLM - Examples: - # 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) + Example: >>> 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": narrations}, ensure_ascii=False, @@ -168,7 +136,6 @@ def build_image_prompt_prompt( narrations_json=narrations_json, narrations_count=len(narrations), min_words=min_words, - max_words=max_words, - style_description=style_desc + max_words=max_words ) diff --git a/reelforge/service.py b/reelforge/service.py index bb2b52d..8f70a57 100644 --- a/reelforge/service.py +++ b/reelforge/service.py @@ -66,7 +66,6 @@ class ReelForgeCore: # Content generation services self.narration_generator = None self.image_prompt_generator = None - self.generate_final_image_prompt = None # Frame processing services self.frame_composer = None @@ -111,11 +110,9 @@ class ReelForgeCore: # 5. Initialize content generation services from reelforge.services.narration_generator import NarrationGeneratorService from reelforge.services.image_prompt_generator import ImagePromptGeneratorService - from reelforge.services.final_image_prompt import FinalImagePromptService self.narration_generator = NarrationGeneratorService(self) self.image_prompt_generator = ImagePromptGeneratorService(self) - self.generate_final_image_prompt = FinalImagePromptService(self) # 6. Initialize frame processing services from reelforge.services.frame_composer import FrameComposerService diff --git a/reelforge/services/__init__.py b/reelforge/services/__init__.py index 62b4813..7268c4b 100644 --- a/reelforge/services/__init__.py +++ b/reelforge/services/__init__.py @@ -14,11 +14,6 @@ from reelforge.services.image_prompt_generator import ImagePromptGeneratorServic from reelforge.services.frame_composer import FrameComposerService from reelforge.services.storyboard_processor import StoryboardProcessorService from reelforge.services.video_generator import VideoGeneratorService -from reelforge.services.final_image_prompt import ( - FinalImagePromptService, - StylePreset, - PresetValue -) __all__ = [ "BaseService", @@ -31,8 +26,5 @@ __all__ = [ "FrameComposerService", "StoryboardProcessorService", "VideoGeneratorService", - "FinalImagePromptService", - "StylePreset", - "PresetValue", ] diff --git a/reelforge/services/final_image_prompt.py b/reelforge/services/final_image_prompt.py deleted file mode 100644 index 1febf8f..0000000 --- a/reelforge/services/final_image_prompt.py +++ /dev/null @@ -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 - diff --git a/reelforge/services/image_prompt_generator.py b/reelforge/services/image_prompt_generator.py index 4d5b7ff..786d0e3 100644 --- a/reelforge/services/image_prompt_generator.py +++ b/reelforge/services/image_prompt_generator.py @@ -28,8 +28,6 @@ class ImagePromptGeneratorService: self, narrations: List[str], config: StoryboardConfig, - image_style_preset: str = None, - image_style_description: str = None, batch_size: int = 10, max_retries: int = 3, progress_callback: Optional[Callable] = None @@ -40,14 +38,12 @@ class ImagePromptGeneratorService: Args: narrations: List of narrations 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) max_retries: Max retry attempts per batch (default: 3) progress_callback: Optional callback(completed, total, message) for progress updates Returns: - List of image prompts with style applied + List of image prompts with prompt_prefix applied (from config) Raises: ValueError: If batch fails after max_retries @@ -117,28 +113,20 @@ class ImagePromptGeneratorService: base_prompts = all_base_prompts logger.info(f"✅ All batches completed. Total prompts: {len(base_prompts)}") - # 5. Apply style to each prompt using FinalImagePromptService - from reelforge.services.final_image_prompt import StylePreset + # 5. Apply prompt prefix to each prompt + from reelforge.utils.prompt_helper import build_image_prompt - # Convert style preset name to enum if provided - style_preset_enum = None - if image_style_preset: - try: - style_preset_enum = StylePreset[image_style_preset.upper()] - except KeyError: - logger.warning(f"Unknown style preset: {image_style_preset}") + # Get prompt prefix from config + image_config = self.core.config.get("image", {}) + prompt_prefix = image_config.get("prompt_prefix", "") - # Apply style to each base prompt + # Apply prefix to each base prompt final_prompts = [] for base_prompt in base_prompts: - final_prompt = await self.core.generate_final_image_prompt( - prompt=base_prompt, - style_preset=style_preset_enum, - custom_style_description=image_style_description or "" - ) + final_prompt = build_image_prompt(base_prompt, prompt_prefix) 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 async def _generate_batch_prompts( @@ -170,9 +158,7 @@ class ImagePromptGeneratorService: prompt = build_image_prompt_prompt( narrations=batch_narrations, min_words=config.min_image_prompt_words, - max_words=config.max_image_prompt_words, - image_style_preset=None, - image_style_description=None + max_words=config.max_image_prompt_words ) # 2. Call LLM diff --git a/reelforge/services/storyboard_processor.py b/reelforge/services/storyboard_processor.py index 268095a..445b86d 100644 --- a/reelforge/services/storyboard_processor.py +++ b/reelforge/services/storyboard_processor.py @@ -140,9 +140,10 @@ class StoryboardProcessorService: """Step 2: Generate image using ComfyKit""" 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( prompt=frame.image_prompt, + preset=config.image_preset, # Pass preset from config (None = use default) width=config.image_width, height=config.image_height ) @@ -201,19 +202,17 @@ class StoryboardProcessorService: from pathlib import Path # Resolve template path - template_name = config.frame_template - if not template_name.endswith('.html'): - template_name = f"{template_name}.html" + template_filename = config.frame_template # Try templates/ directory first - template_path = Path(f"templates/{template_name}") + template_path = Path(f"templates/{template_filename}") if not template_path.exists(): - # Try as absolute path - template_path = Path(template_name) + # Try as absolute/relative path + template_path = Path(template_filename) if not template_path.exists(): raise FileNotFoundError( - f"Template not found: {template_name}. " - f"Available templates: classic, modern, minimal" + f"Template not found: {template_filename}. " + f"Built-in templates: default.html, modern.html, neon.html" ) # Get storyboard for content metadata diff --git a/reelforge/services/video.py b/reelforge/services/video.py index 08e77a6..34a5a53 100644 --- a/reelforge/services/video.py +++ b/reelforge/services/video.py @@ -101,7 +101,7 @@ class VideoService: - "filter": Slower but handles different formats bgm_path: Background music file path (optional) - 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 bgm_volume: BGM volume level (0.0-1.0), default 0.2 bgm_mode: BGM playback mode @@ -504,11 +504,11 @@ class VideoService: 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: 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) Returns: @@ -521,8 +521,8 @@ class VideoService: if os.path.exists(bgm_path): return os.path.abspath(bgm_path) - # Try as preset in bgm/ directory - preset_path = f"bgm/{bgm_path}.mp3" + # Try as filename in bgm/ directory + preset_path = f"bgm/{bgm_path}" if os.path.exists(preset_path): return os.path.abspath(preset_path) @@ -532,9 +532,9 @@ class VideoService: os.path.abspath(preset_path) ] - # List available presets + # List available BGM files 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( f"BGM file not found: '{bgm_path}'\n" @@ -546,10 +546,10 @@ class VideoService: def _list_available_bgm(self) -> list[str]: """ - List available preset BGM files + List available BGM files in bgm/ directory Returns: - List of preset names (without .mp3 extension) + List of filenames (with extensions) """ bgm_dir = "bgm" if not os.path.exists(bgm_dir): @@ -557,7 +557,9 @@ class VideoService: try: 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: return [] diff --git a/reelforge/services/video_generator.py b/reelforge/services/video_generator.py index 4d765b3..f8b7876 100644 --- a/reelforge/services/video_generator.py +++ b/reelforge/services/video_generator.py @@ -66,8 +66,7 @@ class VideoGeneratorService: # === Image Parameters === image_width: int = 1024, image_height: int = 1024, - image_style_preset: Optional[str] = None, - image_style_description: Optional[str] = None, + image_preset: Optional[str] = None, # === Video Parameters === video_width: int = 1080, @@ -77,6 +76,9 @@ class VideoGeneratorService: # === Frame Template === frame_template: Optional[str] = None, + # === Image Style === + prompt_prefix: Optional[str] = None, + # === BGM Parameters === bgm_path: Optional[str] = None, bgm_volume: float = 0.2, @@ -119,17 +121,19 @@ class VideoGeneratorService: image_width: Generated image width (default 1024) image_height: Generated image height (default 1024) - image_style_preset: Preset style name (e.g., "minimal", "concept", "cinematic") - image_style_description: Custom style description (overrides preset) + image_preset: Image workflow preset (e.g., "flux", "sdxl", None = use default) video_width: Final video width (default 1080) video_height: Final video height (default 1920) video_fps: Video frame rate (default 30) - frame_template: HTML template name or path (None = use PIL) - e.g., "classic", "modern", "minimal", or custom path + frame_template: HTML template filename or path (None = use PIL) + 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_mode: BGM mode "once" or "loop" (default "loop") @@ -211,6 +215,7 @@ class VideoGeneratorService: voice_id=voice_id, image_width=image_width, image_height=image_height, + image_preset=image_preset, frame_template=frame_template ) @@ -248,25 +253,36 @@ class VideoGeneratorService: # Step 2: Generate image prompts self._report_progress(progress_callback, "generating_image_prompts", 0.15) - # Create progress callback wrapper for image prompt generation (15%-30% range) - def image_prompt_progress(completed: int, total: int, message: str): - # Map batch progress to 15%-30% range - batch_progress = completed / total if total > 0 else 0 - overall_progress = 0.15 + (batch_progress * 0.15) # 15% -> 30% - self._report_progress( - progress_callback, - "generating_image_prompts", - overall_progress, - extra_info=message - ) + # Override prompt_prefix if provided (temporarily modify config) + original_prefix = None + if prompt_prefix is not None: + image_config = self.core.config.get("image", {}) + original_prefix = image_config.get("prompt_prefix") + image_config["prompt_prefix"] = prompt_prefix + logger.info(f"Using custom prompt_prefix: '{prompt_prefix}'") - image_prompts = await self.core.image_prompt_generator.generate_image_prompts( - narrations=narrations, - config=config, - image_style_preset=image_style_preset, - image_style_description=image_style_description, - progress_callback=image_prompt_progress - ) + try: + # Create progress callback wrapper for image prompt generation (15%-30% range) + def image_prompt_progress(completed: int, total: int, message: str): + # Map batch progress to 15%-30% range + batch_progress = completed / total if total > 0 else 0 + 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") # Step 3: Create frames diff --git a/reelforge/utils/prompt_helper.py b/reelforge/utils/prompt_helper.py new file mode 100644 index 0000000..c88fcb8 --- /dev/null +++ b/reelforge/utils/prompt_helper.py @@ -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 + diff --git a/start_web.sh b/start_web.sh index d42f464..f483a5e 100755 --- a/start_web.sh +++ b/start_web.sh @@ -15,5 +15,5 @@ if [ ! -f config.yaml ]; then fi # Start Streamlit -uv run streamlit run web.py --server.port=8501 --server.address=localhost +uv run streamlit run web.py diff --git a/templates/README.md b/templates/README.md deleted file mode 100644 index 894bc99..0000000 --- a/templates/README.md +++ /dev/null @@ -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 - - - -
- - - - -