feat: Add comprehensive timeline editor with frame editing and regeneration capabilities
This commit is contained in:
@@ -27,19 +27,41 @@ from loguru import logger
|
||||
|
||||
from pixelle_video.models.progress import ProgressEvent
|
||||
from pixelle_video.models.storyboard import Storyboard, StoryboardFrame, StoryboardConfig
|
||||
from pixelle_video.services.quality import (
|
||||
QualityGate,
|
||||
QualityConfig,
|
||||
RetryManager,
|
||||
RetryConfig,
|
||||
QualityError,
|
||||
)
|
||||
|
||||
|
||||
class FrameProcessor:
|
||||
"""Frame processor"""
|
||||
|
||||
def __init__(self, pixelle_video_core):
|
||||
def __init__(
|
||||
self,
|
||||
pixelle_video_core,
|
||||
quality_config: Optional[QualityConfig] = None,
|
||||
retry_config: Optional[RetryConfig] = None,
|
||||
enable_quality_check: bool = True,
|
||||
):
|
||||
"""
|
||||
Initialize
|
||||
|
||||
Args:
|
||||
pixelle_video_core: PixelleVideoCore instance
|
||||
quality_config: Quality evaluation configuration
|
||||
retry_config: Retry behavior configuration
|
||||
enable_quality_check: Whether to enable quality checking
|
||||
"""
|
||||
self.core = pixelle_video_core
|
||||
self.enable_quality_check = enable_quality_check
|
||||
self.quality_gate = QualityGate(
|
||||
llm_service=pixelle_video_core.llm if hasattr(pixelle_video_core, 'llm') else None,
|
||||
config=quality_config or QualityConfig()
|
||||
)
|
||||
self.retry_manager = RetryManager(config=retry_config or RetryConfig())
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
@@ -199,11 +221,14 @@ class FrameProcessor:
|
||||
frame: StoryboardFrame,
|
||||
config: StoryboardConfig
|
||||
):
|
||||
"""Step 2: Generate media (image or video) using ComfyKit"""
|
||||
"""
|
||||
Step 2: Generate media (image or video) using ComfyKit
|
||||
|
||||
Enhanced with quality evaluation and retry logic.
|
||||
"""
|
||||
logger.debug(f" 2/4: Generating media for frame {frame.index}...")
|
||||
|
||||
# Determine media type based on workflow
|
||||
# video_ prefix in workflow name indicates video generation
|
||||
workflow_name = config.media_workflow or ""
|
||||
is_video_workflow = "video_" in workflow_name.lower()
|
||||
media_type = "video" if is_video_workflow else "image"
|
||||
@@ -213,57 +238,87 @@ class FrameProcessor:
|
||||
# Build media generation parameters
|
||||
media_params = {
|
||||
"prompt": frame.image_prompt,
|
||||
"workflow": config.media_workflow, # Pass workflow from config (None = use default)
|
||||
"workflow": config.media_workflow,
|
||||
"media_type": media_type,
|
||||
"width": config.media_width,
|
||||
"height": config.media_height,
|
||||
"index": frame.index + 1, # 1-based index for workflow
|
||||
"index": frame.index + 1,
|
||||
}
|
||||
|
||||
# For video workflows: pass audio duration as target video duration
|
||||
# This ensures video length matches audio length from the source
|
||||
if is_video_workflow and frame.duration:
|
||||
media_params["duration"] = frame.duration
|
||||
logger.info(f" → Generating video with target duration: {frame.duration:.2f}s (from TTS audio)")
|
||||
logger.info(f" → Generating video with target duration: {frame.duration:.2f}s")
|
||||
|
||||
# Call Media generation
|
||||
media_result = await self.core.media(**media_params)
|
||||
# Define generation operation
|
||||
async def generate_and_download():
|
||||
media_result = await self.core.media(**media_params)
|
||||
local_path = await self._download_media(
|
||||
media_result.url,
|
||||
frame.index,
|
||||
config.task_id,
|
||||
media_type=media_result.media_type
|
||||
)
|
||||
return (media_result, local_path)
|
||||
|
||||
# Store media type
|
||||
# Define quality evaluator
|
||||
async def evaluate_quality(result):
|
||||
media_result, local_path = result
|
||||
if media_result.is_video:
|
||||
return await self.quality_gate.evaluate_video(
|
||||
local_path, frame.image_prompt, frame.narration
|
||||
)
|
||||
else:
|
||||
return await self.quality_gate.evaluate_image(
|
||||
local_path, frame.image_prompt, frame.narration
|
||||
)
|
||||
|
||||
# Execute with retry and quality check
|
||||
if self.enable_quality_check:
|
||||
try:
|
||||
retry_result = await self.retry_manager.execute_with_retry(
|
||||
operation=generate_and_download,
|
||||
quality_evaluator=evaluate_quality,
|
||||
operation_name=f"frame_{frame.index}_media",
|
||||
)
|
||||
media_result, local_path = retry_result.result
|
||||
|
||||
# Store quality metrics on frame
|
||||
if retry_result.quality_score:
|
||||
frame.quality_score = retry_result.quality_score.overall_score
|
||||
frame.quality_issues = retry_result.quality_score.issues
|
||||
frame.retry_count = retry_result.attempts - 1 # first attempt is not a retry
|
||||
|
||||
except QualityError as e:
|
||||
logger.warning(f" ⚠ Quality check failed after retries: {e}")
|
||||
# Still try to use the last result if available
|
||||
media_result, local_path = await generate_and_download()
|
||||
frame.quality_issues = [str(e)]
|
||||
else:
|
||||
# Quality check disabled - just generate
|
||||
media_result, local_path = await generate_and_download()
|
||||
|
||||
# Store results on frame
|
||||
frame.media_type = media_result.media_type
|
||||
|
||||
if media_result.is_image:
|
||||
# Download image to local (pass task_id)
|
||||
local_path = await self._download_media(
|
||||
media_result.url,
|
||||
frame.index,
|
||||
config.task_id,
|
||||
media_type="image"
|
||||
)
|
||||
frame.image_path = local_path
|
||||
logger.debug(f" ✓ Image generated: {local_path}")
|
||||
|
||||
elif media_result.is_video:
|
||||
# Download video to local (pass task_id)
|
||||
local_path = await self._download_media(
|
||||
media_result.url,
|
||||
frame.index,
|
||||
config.task_id,
|
||||
media_type="video"
|
||||
)
|
||||
frame.video_path = local_path
|
||||
|
||||
# Update duration from video if available
|
||||
if media_result.duration:
|
||||
frame.duration = media_result.duration
|
||||
logger.debug(f" ✓ Video generated: {local_path} (duration: {frame.duration:.2f}s)")
|
||||
else:
|
||||
# Get video duration from file
|
||||
frame.duration = await self._get_video_duration(local_path)
|
||||
logger.debug(f" ✓ Video generated: {local_path} (duration: {frame.duration:.2f}s)")
|
||||
|
||||
logger.debug(f" ✓ Video generated: {local_path} (duration: {frame.duration:.2f}s)")
|
||||
else:
|
||||
raise ValueError(f"Unknown media type: {media_result.media_type}")
|
||||
|
||||
# Log quality result
|
||||
if frame.quality_score is not None:
|
||||
logger.info(
|
||||
f" 📊 Quality: {frame.quality_score:.2f} "
|
||||
f"(retries: {frame.retry_count}, issues: {len(frame.quality_issues or [])})"
|
||||
)
|
||||
|
||||
async def _step_compose_frame(
|
||||
self,
|
||||
|
||||
Reference in New Issue
Block a user