添加taskid逻辑
This commit is contained in:
@@ -58,7 +58,6 @@ async def generate_video_sync(
|
|||||||
title=request_body.title,
|
title=request_body.title,
|
||||||
n_scenes=request_body.n_scenes,
|
n_scenes=request_body.n_scenes,
|
||||||
voice_id=request_body.voice_id,
|
voice_id=request_body.voice_id,
|
||||||
use_uuid_filename=True, # API mode: use UUID filename
|
|
||||||
min_narration_words=request_body.min_narration_words,
|
min_narration_words=request_body.min_narration_words,
|
||||||
max_narration_words=request_body.max_narration_words,
|
max_narration_words=request_body.max_narration_words,
|
||||||
min_image_prompt_words=request_body.min_image_prompt_words,
|
min_image_prompt_words=request_body.min_image_prompt_words,
|
||||||
@@ -133,7 +132,6 @@ async def generate_video_async(
|
|||||||
title=request_body.title,
|
title=request_body.title,
|
||||||
n_scenes=request_body.n_scenes,
|
n_scenes=request_body.n_scenes,
|
||||||
voice_id=request_body.voice_id,
|
voice_id=request_body.voice_id,
|
||||||
use_uuid_filename=True, # API mode: use UUID filename
|
|
||||||
min_narration_words=request_body.min_narration_words,
|
min_narration_words=request_body.min_narration_words,
|
||||||
max_narration_words=request_body.max_narration_words,
|
max_narration_words=request_body.max_narration_words,
|
||||||
min_image_prompt_words=request_body.min_image_prompt_words,
|
min_image_prompt_words=request_body.min_image_prompt_words,
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ from typing import List, Optional
|
|||||||
@dataclass
|
@dataclass
|
||||||
class StoryboardConfig:
|
class StoryboardConfig:
|
||||||
"""Storyboard configuration parameters"""
|
"""Storyboard configuration parameters"""
|
||||||
|
|
||||||
|
# Task isolation
|
||||||
|
task_id: Optional[str] = None # Task ID for file isolation (auto-generated if None)
|
||||||
|
|
||||||
n_storyboard: int = 5 # Number of storyboard frames
|
n_storyboard: int = 5 # Number of storyboard frames
|
||||||
min_narration_words: int = 5 # Min narration word count
|
min_narration_words: int = 5 # Min narration word count
|
||||||
max_narration_words: int = 20 # Max narration word count
|
max_narration_words: int = 20 # Max narration word count
|
||||||
|
|||||||
@@ -198,7 +198,8 @@ class HTMLFrameGenerator:
|
|||||||
image: str,
|
image: str,
|
||||||
ext: Optional[Dict[str, Any]] = None,
|
ext: Optional[Dict[str, Any]] = None,
|
||||||
width: int = 1080,
|
width: int = 1080,
|
||||||
height: int = 1920
|
height: int = 1920,
|
||||||
|
output_path: Optional[str] = None
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Generate frame from HTML template
|
Generate frame from HTML template
|
||||||
@@ -210,6 +211,7 @@ class HTMLFrameGenerator:
|
|||||||
ext: Additional data (content_title, content_author, etc.)
|
ext: Additional data (content_title, content_author, etc.)
|
||||||
width: Frame width in pixels
|
width: Frame width in pixels
|
||||||
height: Frame height in pixels
|
height: Frame height in pixels
|
||||||
|
output_path: Custom output path (auto-generated if None)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Path to generated frame image
|
Path to generated frame image
|
||||||
@@ -248,10 +250,21 @@ class HTMLFrameGenerator:
|
|||||||
placeholder = f"{{{{{key}}}}}"
|
placeholder = f"{{{{{key}}}}}"
|
||||||
html = html.replace(placeholder, str(value) if value is not None else "")
|
html = html.replace(placeholder, str(value) if value is not None else "")
|
||||||
|
|
||||||
# Generate unique output path
|
# Use provided output path or auto-generate
|
||||||
from reelforge.utils.os_util import get_output_path
|
if output_path is None:
|
||||||
output_filename = f"frame_{uuid.uuid4().hex[:16]}.png"
|
# Fallback: auto-generate (for backward compatibility)
|
||||||
output_path = get_output_path(output_filename)
|
from reelforge.utils.os_util import get_output_path
|
||||||
|
output_filename = f"frame_{uuid.uuid4().hex[:16]}.png"
|
||||||
|
output_path = get_output_path(output_filename)
|
||||||
|
else:
|
||||||
|
# Ensure parent directory exists
|
||||||
|
import os
|
||||||
|
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||||
|
|
||||||
|
# Extract filename from output_path for html2image
|
||||||
|
import os
|
||||||
|
output_filename = os.path.basename(output_path)
|
||||||
|
output_dir = os.path.dirname(output_path)
|
||||||
|
|
||||||
# Ensure Html2Image is initialized
|
# Ensure Html2Image is initialized
|
||||||
self._ensure_hti(width, height)
|
self._ensure_hti(width, height)
|
||||||
@@ -265,11 +278,11 @@ class HTMLFrameGenerator:
|
|||||||
size=(width, height)
|
size=(width, height)
|
||||||
)
|
)
|
||||||
|
|
||||||
# html2image saves to current directory by default, move to output
|
# html2image saves to current directory by default, move to target directory
|
||||||
import os
|
|
||||||
import shutil
|
import shutil
|
||||||
if os.path.exists(output_filename):
|
temp_file = os.path.join(os.getcwd(), output_filename)
|
||||||
shutil.move(output_filename, output_path)
|
if os.path.exists(temp_file) and temp_file != output_path:
|
||||||
|
shutil.move(temp_file, output_path)
|
||||||
|
|
||||||
logger.info(f"✅ Frame generated: {output_path}")
|
logger.info(f"✅ Frame generated: {output_path}")
|
||||||
return output_path
|
return output_path
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ from loguru import logger
|
|||||||
|
|
||||||
from reelforge.models.progress import ProgressEvent
|
from reelforge.models.progress import ProgressEvent
|
||||||
from reelforge.models.storyboard import Storyboard, StoryboardFrame, StoryboardConfig
|
from reelforge.models.storyboard import Storyboard, StoryboardFrame, StoryboardConfig
|
||||||
from reelforge.utils.os_util import get_temp_path
|
|
||||||
|
|
||||||
|
|
||||||
class FrameProcessor:
|
class FrameProcessor:
|
||||||
@@ -121,11 +120,16 @@ class FrameProcessor:
|
|||||||
"""Step 1: Generate audio using TTS"""
|
"""Step 1: Generate audio using TTS"""
|
||||||
logger.debug(f" 1/4: Generating audio for frame {frame.index}...")
|
logger.debug(f" 1/4: Generating audio for frame {frame.index}...")
|
||||||
|
|
||||||
# Call TTS
|
# Generate output path using task_id
|
||||||
|
from reelforge.utils.os_util import get_task_frame_path
|
||||||
|
output_path = get_task_frame_path(config.task_id, frame.index, "audio")
|
||||||
|
|
||||||
|
# Call TTS with specific output path
|
||||||
audio_path = await self.core.tts(
|
audio_path = await self.core.tts(
|
||||||
text=frame.narration,
|
text=frame.narration,
|
||||||
voice=config.voice_id,
|
voice=config.voice_id,
|
||||||
rate="+20%",
|
rate="+20%",
|
||||||
|
output_path=output_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
frame.audio_path = audio_path
|
frame.audio_path = audio_path
|
||||||
@@ -151,8 +155,8 @@ class FrameProcessor:
|
|||||||
height=config.image_height
|
height=config.image_height
|
||||||
)
|
)
|
||||||
|
|
||||||
# Download image to local
|
# Download image to local (pass task_id)
|
||||||
local_path = await self._download_image(image_url, frame.index)
|
local_path = await self._download_image(image_url, frame.index, config.task_id)
|
||||||
frame.image_path = local_path
|
frame.image_path = local_path
|
||||||
|
|
||||||
logger.debug(f" ✓ Image generated: {local_path}")
|
logger.debug(f" ✓ Image generated: {local_path}")
|
||||||
@@ -166,7 +170,9 @@ class FrameProcessor:
|
|||||||
"""Step 3: Compose frame with subtitle using HTML template"""
|
"""Step 3: Compose frame with subtitle using HTML template"""
|
||||||
logger.debug(f" 3/4: Composing frame {frame.index}...")
|
logger.debug(f" 3/4: Composing frame {frame.index}...")
|
||||||
|
|
||||||
output_path = get_temp_path(f"frame_{frame.index}_composed.png")
|
# Generate output path using task_id
|
||||||
|
from reelforge.utils.os_util import get_task_frame_path
|
||||||
|
output_path = get_task_frame_path(config.task_id, frame.index, "composed")
|
||||||
|
|
||||||
# Use HTML template to compose frame
|
# Use HTML template to compose frame
|
||||||
composed_path = await self._compose_frame_html(frame, storyboard, config, output_path)
|
composed_path = await self._compose_frame_html(frame, storyboard, config, output_path)
|
||||||
@@ -219,7 +225,8 @@ class FrameProcessor:
|
|||||||
image=frame.image_path,
|
image=frame.image_path,
|
||||||
ext=ext,
|
ext=ext,
|
||||||
width=config.video_width,
|
width=config.video_width,
|
||||||
height=config.video_height
|
height=config.video_height,
|
||||||
|
output_path=output_path
|
||||||
)
|
)
|
||||||
|
|
||||||
return composed_path
|
return composed_path
|
||||||
@@ -232,7 +239,9 @@ class FrameProcessor:
|
|||||||
"""Step 4: Create video segment from image + audio"""
|
"""Step 4: Create video segment from image + audio"""
|
||||||
logger.debug(f" 4/4: Creating video segment for frame {frame.index}...")
|
logger.debug(f" 4/4: Creating video segment for frame {frame.index}...")
|
||||||
|
|
||||||
output_path = get_temp_path(f"frame_{frame.index}_segment.mp4")
|
# Generate output path using task_id
|
||||||
|
from reelforge.utils.os_util import get_task_frame_path
|
||||||
|
output_path = get_task_frame_path(config.task_id, frame.index, "segment")
|
||||||
|
|
||||||
# Call video compositor to create video from image + audio
|
# Call video compositor to create video from image + audio
|
||||||
from reelforge.services.video import VideoService
|
from reelforge.services.video import VideoService
|
||||||
@@ -266,9 +275,10 @@ class FrameProcessor:
|
|||||||
estimated_duration = file_size / 2000
|
estimated_duration = file_size / 2000
|
||||||
return max(1.0, estimated_duration) # At least 1 second
|
return max(1.0, estimated_duration) # At least 1 second
|
||||||
|
|
||||||
async def _download_image(self, url: str, frame_index: int) -> str:
|
async def _download_image(self, url: str, frame_index: int, task_id: str) -> str:
|
||||||
"""Download image from URL to local file"""
|
"""Download image from URL to local file"""
|
||||||
output_path = get_temp_path(f"frame_{frame_index}_image.png")
|
from reelforge.utils.os_util import get_task_frame_path
|
||||||
|
output_path = get_task_frame_path(task_id, frame_index, "image")
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
response = await client.get(url)
|
response = await client.get(url)
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ class TTSService(ComfyBaseService):
|
|||||||
rate: Optional[str] = None,
|
rate: Optional[str] = None,
|
||||||
volume: Optional[str] = None,
|
volume: Optional[str] = None,
|
||||||
pitch: Optional[str] = None,
|
pitch: Optional[str] = None,
|
||||||
|
# Output path
|
||||||
|
output_path: Optional[str] = None,
|
||||||
**params
|
**params
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -107,6 +109,7 @@ class TTSService(ComfyBaseService):
|
|||||||
rate: Speech rate (e.g., "+0%", "+50%", "-20%")
|
rate: Speech rate (e.g., "+0%", "+50%", "-20%")
|
||||||
volume: Speech volume (e.g., "+0%")
|
volume: Speech volume (e.g., "+0%")
|
||||||
pitch: Speech pitch (e.g., "+0Hz")
|
pitch: Speech pitch (e.g., "+0Hz")
|
||||||
|
output_path: Custom output path (auto-generated if None)
|
||||||
**params: Additional parameters
|
**params: Additional parameters
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -148,6 +151,7 @@ class TTSService(ComfyBaseService):
|
|||||||
rate=rate,
|
rate=rate,
|
||||||
volume=volume,
|
volume=volume,
|
||||||
pitch=pitch,
|
pitch=pitch,
|
||||||
|
output_path=output_path,
|
||||||
**params
|
**params
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -161,6 +165,7 @@ class TTSService(ComfyBaseService):
|
|||||||
rate=rate,
|
rate=rate,
|
||||||
volume=volume,
|
volume=volume,
|
||||||
pitch=pitch,
|
pitch=pitch,
|
||||||
|
output_path=output_path,
|
||||||
**params
|
**params
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -171,6 +176,7 @@ class TTSService(ComfyBaseService):
|
|||||||
rate: Optional[str] = None,
|
rate: Optional[str] = None,
|
||||||
volume: Optional[str] = None,
|
volume: Optional[str] = None,
|
||||||
pitch: Optional[str] = None,
|
pitch: Optional[str] = None,
|
||||||
|
output_path: Optional[str] = None,
|
||||||
**params
|
**params
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -182,6 +188,7 @@ class TTSService(ComfyBaseService):
|
|||||||
rate: Speech rate (default: +0%)
|
rate: Speech rate (default: +0%)
|
||||||
volume: Speech volume (default: +0%)
|
volume: Speech volume (default: +0%)
|
||||||
pitch: Speech pitch (default: +0Hz)
|
pitch: Speech pitch (default: +0Hz)
|
||||||
|
output_path: Custom output path (auto-generated if None)
|
||||||
**params: Additional parameters (e.g., retry_count, retry_delay)
|
**params: Additional parameters (e.g., retry_count, retry_delay)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -191,8 +198,13 @@ class TTSService(ComfyBaseService):
|
|||||||
|
|
||||||
logger.info(f"🎙️ Using edge-tts (local SDK)")
|
logger.info(f"🎙️ Using edge-tts (local SDK)")
|
||||||
|
|
||||||
# Generate temp file path
|
# Generate output path (use provided path or auto-generate)
|
||||||
output_path = get_temp_path(f"{uuid.uuid4().hex}.mp3")
|
if output_path is None:
|
||||||
|
output_path = get_temp_path(f"{uuid.uuid4().hex}.mp3")
|
||||||
|
else:
|
||||||
|
# Ensure parent directory exists
|
||||||
|
import os
|
||||||
|
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||||
|
|
||||||
# Call edge-tts with output_path to save directly
|
# Call edge-tts with output_path to save directly
|
||||||
try:
|
try:
|
||||||
@@ -223,6 +235,7 @@ class TTSService(ComfyBaseService):
|
|||||||
rate: Optional[str] = None,
|
rate: Optional[str] = None,
|
||||||
volume: Optional[str] = None,
|
volume: Optional[str] = None,
|
||||||
pitch: Optional[str] = None,
|
pitch: Optional[str] = None,
|
||||||
|
output_path: Optional[str] = None,
|
||||||
**params
|
**params
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -237,10 +250,11 @@ class TTSService(ComfyBaseService):
|
|||||||
rate: Speech rate (workflow-specific)
|
rate: Speech rate (workflow-specific)
|
||||||
volume: Speech volume (workflow-specific)
|
volume: Speech volume (workflow-specific)
|
||||||
pitch: Speech pitch (workflow-specific)
|
pitch: Speech pitch (workflow-specific)
|
||||||
|
output_path: Custom output path (downloads if URL returned)
|
||||||
**params: Additional workflow parameters
|
**params: Additional workflow parameters
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Generated audio file path/URL
|
Generated audio file path (local if output_path provided, otherwise URL)
|
||||||
"""
|
"""
|
||||||
logger.info(f"🎙️ Using ComfyUI workflow: {workflow_path}")
|
logger.info(f"🎙️ Using ComfyUI workflow: {workflow_path}")
|
||||||
|
|
||||||
@@ -303,6 +317,25 @@ class TTSService(ComfyBaseService):
|
|||||||
logger.error("No audio file generated")
|
logger.error("No audio file generated")
|
||||||
raise Exception("No audio file generated by workflow")
|
raise Exception("No audio file generated by workflow")
|
||||||
|
|
||||||
|
# If output_path provided and audio_path is URL, download to local
|
||||||
|
if output_path and audio_path.startswith(('http://', 'https://')):
|
||||||
|
import httpx
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Ensure parent directory exists
|
||||||
|
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||||
|
|
||||||
|
logger.info(f"Downloading audio from {audio_path} to {output_path}")
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.get(audio_path)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
with open(output_path, 'wb') as f:
|
||||||
|
f.write(response.content)
|
||||||
|
|
||||||
|
logger.info(f"✅ Generated audio (ComfyUI): {output_path}")
|
||||||
|
return output_path
|
||||||
|
|
||||||
logger.info(f"✅ Generated audio (ComfyUI): {audio_path}")
|
logger.info(f"✅ Generated audio (ComfyUI): {audio_path}")
|
||||||
return audio_path
|
return audio_path
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ class VideoGeneratorService:
|
|||||||
n_scenes: int = 5, # Only used in generate mode; ignored in fixed mode
|
n_scenes: int = 5, # Only used in generate mode; ignored in fixed mode
|
||||||
voice_id: str = "zh-CN-YunjianNeural",
|
voice_id: str = "zh-CN-YunjianNeural",
|
||||||
output_path: Optional[str] = None,
|
output_path: Optional[str] = None,
|
||||||
use_uuid_filename: bool = False, # Use UUID instead of timestamp for filename
|
|
||||||
|
|
||||||
# === LLM Parameters ===
|
# === LLM Parameters ===
|
||||||
min_narration_words: int = 5,
|
min_narration_words: int = 5,
|
||||||
@@ -186,25 +185,31 @@ class VideoGeneratorService:
|
|||||||
final_title = await self.core.title_generator(text, strategy="llm")
|
final_title = await self.core.title_generator(text, strategy="llm")
|
||||||
logger.info(f" Title: '{final_title}' (LLM-generated)")
|
logger.info(f" Title: '{final_title}' (LLM-generated)")
|
||||||
|
|
||||||
# Auto-generate output path if not provided
|
# ========== Step 0.5: Create isolated task directory ==========
|
||||||
if output_path is None:
|
from reelforge.utils.os_util import (
|
||||||
if use_uuid_filename:
|
create_task_output_dir,
|
||||||
# API mode: use UUID for filename
|
get_task_final_video_path
|
||||||
import uuid
|
)
|
||||||
filename = str(uuid.uuid4()).replace('-', '')
|
|
||||||
output_path = f"output/{filename}.mp4"
|
|
||||||
else:
|
|
||||||
# Default mode: use timestamp + title
|
|
||||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
||||||
# Use first 10 chars of final_title for filename
|
|
||||||
safe_name = final_title[:10].replace('/', '_').replace(' ', '_')
|
|
||||||
output_path = f"output/{timestamp}_{safe_name}.mp4"
|
|
||||||
|
|
||||||
# Ensure output directory exists
|
# Create isolated task directory for this video generation
|
||||||
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
|
task_dir, task_id = create_task_output_dir()
|
||||||
|
logger.info(f"📁 Task directory created: {task_dir}")
|
||||||
|
logger.info(f" Task ID: {task_id}")
|
||||||
|
|
||||||
|
# Determine final video path
|
||||||
|
user_specified_output = None
|
||||||
|
if output_path is None:
|
||||||
|
# Use standardized path: output/{task_id}/final.mp4
|
||||||
|
output_path = get_task_final_video_path(task_id)
|
||||||
|
else:
|
||||||
|
# User specified custom path: save it and use task path for generation
|
||||||
|
user_specified_output = output_path
|
||||||
|
output_path = get_task_final_video_path(task_id)
|
||||||
|
logger.info(f" Will copy final video to: {user_specified_output}")
|
||||||
|
|
||||||
# Create storyboard config
|
# Create storyboard config
|
||||||
config = StoryboardConfig(
|
config = StoryboardConfig(
|
||||||
|
task_id=task_id, # Pass task_id for file isolation
|
||||||
n_storyboard=n_scenes,
|
n_storyboard=n_scenes,
|
||||||
min_narration_words=min_narration_words,
|
min_narration_words=min_narration_words,
|
||||||
max_narration_words=max_narration_words,
|
max_narration_words=max_narration_words,
|
||||||
@@ -354,6 +359,16 @@ class VideoGeneratorService:
|
|||||||
storyboard.final_video_path = final_video_path
|
storyboard.final_video_path = final_video_path
|
||||||
storyboard.completed_at = datetime.now()
|
storyboard.completed_at = datetime.now()
|
||||||
|
|
||||||
|
# Copy to user-specified path if provided
|
||||||
|
if user_specified_output:
|
||||||
|
import shutil
|
||||||
|
Path(user_specified_output).parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy2(final_video_path, user_specified_output)
|
||||||
|
logger.info(f"📹 Final video copied to: {user_specified_output}")
|
||||||
|
# Use user-specified path in result
|
||||||
|
final_video_path = user_specified_output
|
||||||
|
storyboard.final_video_path = user_specified_output
|
||||||
|
|
||||||
logger.success(f"🎬 Video generation completed: {final_video_path}")
|
logger.success(f"🎬 Video generation completed: {final_video_path}")
|
||||||
|
|
||||||
# Step 6: Create result
|
# Step 6: Create result
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ Inspired by Pixelle-MCP's os_util.py.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import random
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Optional, Tuple, Literal
|
||||||
|
|
||||||
|
|
||||||
def get_reelforge_root_path() -> str:
|
def get_reelforge_root_path() -> str:
|
||||||
@@ -23,24 +26,12 @@ def ensure_reelforge_root_path() -> str:
|
|||||||
"""
|
"""
|
||||||
Ensure ReelForge root path exists and return the path
|
Ensure ReelForge root path exists and return the path
|
||||||
|
|
||||||
Creates necessary directory structure if needed:
|
|
||||||
- temp/: for temporary files (audio, video, etc.)
|
|
||||||
- data/: for persistent data
|
|
||||||
- output/: for final output files
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Root path as string
|
Root path as string
|
||||||
"""
|
"""
|
||||||
root_path = get_reelforge_root_path()
|
root_path = get_reelforge_root_path()
|
||||||
root_path_obj = Path(root_path)
|
root_path_obj = Path(root_path)
|
||||||
|
|
||||||
# Create directory structure if needed
|
|
||||||
temp_dir = root_path_obj / 'temp'
|
|
||||||
data_dir = root_path_obj / 'data'
|
|
||||||
output_dir = root_path_obj / 'output'
|
output_dir = root_path_obj / 'output'
|
||||||
|
|
||||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
data_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
return root_path
|
return root_path
|
||||||
@@ -165,3 +156,129 @@ def ensure_dir(path: str) -> str:
|
|||||||
os.makedirs(path, exist_ok=True)
|
os.makedirs(path, exist_ok=True)
|
||||||
return os.path.abspath(path)
|
return os.path.abspath(path)
|
||||||
|
|
||||||
|
|
||||||
|
# ========== Task Directory Management ==========
|
||||||
|
|
||||||
|
def create_task_id() -> str:
|
||||||
|
"""
|
||||||
|
Create unique task ID with timestamp + random suffix
|
||||||
|
|
||||||
|
Format: {timestamp}_{random_hex}
|
||||||
|
Example: "20251028_143052_ab3d"
|
||||||
|
|
||||||
|
Collision probability: < 0.0001% (65536 combinations per second)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Task ID string
|
||||||
|
"""
|
||||||
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||||
|
random_suffix = f"{random.randint(0, 0xFFFF):04x}" # 4-digit hex (0000-ffff)
|
||||||
|
return f"{timestamp}_{random_suffix}"
|
||||||
|
|
||||||
|
|
||||||
|
def create_task_output_dir(task_id: Optional[str] = None) -> Tuple[str, str]:
|
||||||
|
"""
|
||||||
|
Create isolated output directory for single video generation task
|
||||||
|
|
||||||
|
Directory structure:
|
||||||
|
output/{task_id}/
|
||||||
|
├── final.mp4 # Final video output
|
||||||
|
├── frames/ # All frame-related files
|
||||||
|
│ ├── 0_audio.mp3
|
||||||
|
│ ├── 0_image.png
|
||||||
|
│ ├── 0_composed.png
|
||||||
|
│ ├── 0_segment.mp4
|
||||||
|
│ └── ...
|
||||||
|
└── metadata.json # Optional: task metadata
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_id: Optional task ID (auto-generated if None)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(task_dir, task_id) tuple
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> task_dir, task_id = create_task_output_dir()
|
||||||
|
>>> # task_dir = "/path/to/project/output/20251028_143052_ab3d"
|
||||||
|
>>> # task_id = "20251028_143052_ab3d"
|
||||||
|
"""
|
||||||
|
if task_id is None:
|
||||||
|
task_id = create_task_id()
|
||||||
|
|
||||||
|
task_dir = get_output_path(task_id)
|
||||||
|
frames_dir = os.path.join(task_dir, "frames")
|
||||||
|
|
||||||
|
# Create directories
|
||||||
|
os.makedirs(frames_dir, exist_ok=True)
|
||||||
|
|
||||||
|
return task_dir, task_id
|
||||||
|
|
||||||
|
|
||||||
|
def get_task_path(task_id: str, *paths: str) -> str:
|
||||||
|
"""
|
||||||
|
Get path within task directory
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_id: Task ID
|
||||||
|
*paths: Path components to join
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Absolute path within task directory
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> get_task_path("20251028_143052_ab3d", "final.mp4")
|
||||||
|
>>> # Returns: "/path/to/project/output/20251028_143052_ab3d/final.mp4"
|
||||||
|
"""
|
||||||
|
task_dir = get_output_path(task_id)
|
||||||
|
if paths:
|
||||||
|
return os.path.join(task_dir, *paths)
|
||||||
|
return task_dir
|
||||||
|
|
||||||
|
|
||||||
|
def get_task_frame_path(
|
||||||
|
task_id: str,
|
||||||
|
frame_index: int,
|
||||||
|
file_type: Literal["audio", "image", "composed", "segment"]
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Get frame file path within task directory
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_id: Task ID
|
||||||
|
frame_index: Frame index (0-based)
|
||||||
|
file_type: File type (audio/image/composed/segment)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Absolute path to frame file
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> get_task_frame_path("20251028_143052_ab3d", 0, "audio")
|
||||||
|
>>> # Returns: ".../output/20251028_143052_ab3d/frames/0_audio.mp3"
|
||||||
|
"""
|
||||||
|
ext_map = {
|
||||||
|
"audio": "mp3",
|
||||||
|
"image": "png",
|
||||||
|
"composed": "png",
|
||||||
|
"segment": "mp4"
|
||||||
|
}
|
||||||
|
|
||||||
|
filename = f"{frame_index}_{file_type}.{ext_map[file_type]}"
|
||||||
|
return get_task_path(task_id, "frames", filename)
|
||||||
|
|
||||||
|
|
||||||
|
def get_task_final_video_path(task_id: str) -> str:
|
||||||
|
"""
|
||||||
|
Get final video path within task directory
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_id: Task ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Absolute path to final video
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> get_task_final_video_path("20251028_143052_ab3d")
|
||||||
|
>>> # Returns: ".../output/20251028_143052_ab3d/final.mp4"
|
||||||
|
"""
|
||||||
|
return get_task_path(task_id, "final.mp4")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user