Recovered state: integrated TrendSurferAgent, restored frontend/backend files, and cleaned up recovery scripts
This commit is contained in:
@@ -10,6 +10,8 @@ from typing import List, Dict, Any, Optional
|
||||
from pathlib import Path
|
||||
from loguru import logger
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from services.user_workspace_manager import UserWorkspaceManager
|
||||
|
||||
|
||||
class StoryAudioGenerationService:
|
||||
@@ -26,14 +28,33 @@ class StoryAudioGenerationService:
|
||||
if output_dir:
|
||||
self.output_dir = Path(output_dir)
|
||||
else:
|
||||
# Default to backend/story_audio directory
|
||||
base_dir = Path(__file__).parent.parent.parent
|
||||
self.output_dir = base_dir / "story_audio"
|
||||
# Default to root/data/media/story_audio directory
|
||||
base_dir = Path(__file__).resolve().parents[3]
|
||||
self.output_dir = base_dir / "data" / "media" / "story_audio"
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
logger.info(f"[StoryAudioGeneration] Initialized with output directory: {self.output_dir}")
|
||||
|
||||
def _get_user_audio_dir(self, user_id: str, db: Optional[Session] = None) -> Path:
|
||||
"""
|
||||
Get the audio directory for a specific user.
|
||||
Falls back to default output_dir if workspace not found.
|
||||
"""
|
||||
if db and user_id:
|
||||
try:
|
||||
workspace_manager = UserWorkspaceManager(db)
|
||||
workspace = workspace_manager.get_user_workspace(user_id)
|
||||
if workspace:
|
||||
# Use content/story_audio inside user workspace
|
||||
user_audio_dir = Path(workspace['workspace_path']) / "content" / "story_audio"
|
||||
user_audio_dir.mkdir(parents=True, exist_ok=True)
|
||||
return user_audio_dir
|
||||
except Exception as e:
|
||||
logger.warning(f"[StoryAudioGeneration] Failed to resolve user workspace path for {user_id}: {e}")
|
||||
|
||||
return self.output_dir
|
||||
|
||||
def _generate_audio_filename(self, scene_number: int, scene_title: str) -> str:
|
||||
"""Generate a unique filename for a scene audio file."""
|
||||
# Clean scene title for filename
|
||||
@@ -136,7 +157,8 @@ class StoryAudioGenerationService:
|
||||
provider: str = "gtts",
|
||||
lang: str = "en",
|
||||
slow: bool = False,
|
||||
rate: int = 150
|
||||
rate: int = 150,
|
||||
db: Optional[Session] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate audio narration for a single story scene.
|
||||
@@ -148,6 +170,7 @@ class StoryAudioGenerationService:
|
||||
lang (str): Language code for TTS (default: "en").
|
||||
slow (bool): Whether to speak slowly (default: False, gTTS only).
|
||||
rate (int): Speech rate (default: 150, pyttsx3 only).
|
||||
db (Session, optional): Database session.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Audio metadata including file path, URL, and scene info.
|
||||
@@ -163,9 +186,12 @@ class StoryAudioGenerationService:
|
||||
logger.info(f"[StoryAudioGeneration] Generating audio for scene {scene_number}: {scene_title}")
|
||||
logger.debug(f"[StoryAudioGeneration] Audio narration: {audio_narration[:100]}...")
|
||||
|
||||
# Determine output directory (user workspace or default)
|
||||
output_dir = self._get_user_audio_dir(user_id, db)
|
||||
|
||||
# Generate audio filename
|
||||
audio_filename = self._generate_audio_filename(scene_number, scene_title)
|
||||
audio_path = self.output_dir / audio_filename
|
||||
audio_path = output_dir / audio_filename
|
||||
|
||||
# Generate audio based on provider
|
||||
success = False
|
||||
@@ -226,7 +252,8 @@ class StoryAudioGenerationService:
|
||||
lang: str = "en",
|
||||
slow: bool = False,
|
||||
rate: int = 150,
|
||||
progress_callback: Optional[callable] = None
|
||||
progress_callback: Optional[callable] = None,
|
||||
db: Optional[Session] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Generate audio narration for multiple story scenes.
|
||||
@@ -239,6 +266,7 @@ class StoryAudioGenerationService:
|
||||
slow (bool): Whether to speak slowly (default: False, gTTS only).
|
||||
rate (int): Speech rate (default: 150, pyttsx3 only).
|
||||
progress_callback (callable, optional): Callback function for progress updates.
|
||||
db (Session, optional): Database session.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: List of audio metadata for each scene.
|
||||
@@ -260,7 +288,8 @@ class StoryAudioGenerationService:
|
||||
provider=provider,
|
||||
lang=lang,
|
||||
slow=slow,
|
||||
rate=rate
|
||||
rate=rate,
|
||||
db=db
|
||||
)
|
||||
|
||||
audio_results.append(audio_result)
|
||||
@@ -307,6 +336,7 @@ class StoryAudioGenerationService:
|
||||
format: Optional[str] = None,
|
||||
language_boost: Optional[str] = None,
|
||||
enable_sync_mode: Optional[bool] = True,
|
||||
db: Optional[Session] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate AI audio for a single scene using main_audio_generation.
|
||||
@@ -322,6 +352,7 @@ class StoryAudioGenerationService:
|
||||
pitch (float): Speech pitch (-12 to 12, default: 0.0).
|
||||
emotion (str): Emotion for speech (default: "happy").
|
||||
english_normalization (bool): Enable English text normalization for better number reading (default: False).
|
||||
db (Session, optional): Database session.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Audio metadata including file path, URL, and scene info.
|
||||
@@ -354,9 +385,12 @@ class StoryAudioGenerationService:
|
||||
enable_sync_mode=enable_sync_mode,
|
||||
)
|
||||
|
||||
# Determine output directory (user workspace or default)
|
||||
output_dir = self._get_user_audio_dir(user_id, db)
|
||||
|
||||
# Save audio to file
|
||||
audio_filename = self._generate_audio_filename(scene_number, scene_title)
|
||||
audio_path = self.output_dir / audio_filename
|
||||
audio_path = output_dir / audio_filename
|
||||
|
||||
with open(audio_path, "wb") as f:
|
||||
f.write(result.audio_bytes)
|
||||
|
||||
@@ -10,10 +10,12 @@ import uuid
|
||||
from typing import List, Dict, Any, Optional
|
||||
from pathlib import Path
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from services.llm_providers.main_image_generation import generate_image
|
||||
from services.llm_providers.image_generation import ImageGenerationResult
|
||||
from utils.logger_utils import get_service_logger
|
||||
from services.user_workspace_manager import UserWorkspaceManager
|
||||
|
||||
logger = get_service_logger("story_writer.image_generation")
|
||||
|
||||
@@ -32,14 +34,33 @@ class StoryImageGenerationService:
|
||||
if output_dir:
|
||||
self.output_dir = Path(output_dir)
|
||||
else:
|
||||
# Default to backend/story_images directory
|
||||
base_dir = Path(__file__).parent.parent.parent
|
||||
self.output_dir = base_dir / "story_images"
|
||||
# Default to root/data/media/story_images directory
|
||||
base_dir = Path(__file__).resolve().parents[3]
|
||||
self.output_dir = base_dir / "data" / "media" / "story_images"
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
logger.info(f"[StoryImageGeneration] Initialized with output directory: {self.output_dir}")
|
||||
|
||||
def _get_user_image_dir(self, user_id: str, db: Optional[Session] = None) -> Path:
|
||||
"""
|
||||
Get the image directory for a specific user.
|
||||
Falls back to default output_dir if workspace not found.
|
||||
"""
|
||||
if db and user_id:
|
||||
try:
|
||||
workspace_manager = UserWorkspaceManager(db)
|
||||
workspace = workspace_manager.get_user_workspace(user_id)
|
||||
if workspace:
|
||||
# Use media/story_images inside user workspace
|
||||
user_image_dir = Path(workspace['workspace_path']) / "media" / "story_images"
|
||||
user_image_dir.mkdir(parents=True, exist_ok=True)
|
||||
return user_image_dir
|
||||
except Exception as e:
|
||||
logger.warning(f"[StoryImageGeneration] Failed to resolve user workspace path for {user_id}: {e}")
|
||||
|
||||
return self.output_dir
|
||||
|
||||
def _generate_image_filename(self, scene_number: int, scene_title: str) -> str:
|
||||
"""Generate a unique filename for a scene image."""
|
||||
# Clean scene title for filename
|
||||
@@ -134,7 +155,8 @@ class StoryImageGenerationService:
|
||||
width: int = 1024,
|
||||
height: int = 1024,
|
||||
model: Optional[str] = None,
|
||||
progress_callback: Optional[callable] = None
|
||||
progress_callback: Optional[callable] = None,
|
||||
db: Optional[Session] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Generate images for multiple story scenes.
|
||||
@@ -147,6 +169,7 @@ class StoryImageGenerationService:
|
||||
height (int): Image height (default: 1024).
|
||||
model (str, optional): Model to use for image generation.
|
||||
progress_callback (callable, optional): Callback function for progress updates.
|
||||
db (Session, optional): Database session.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: List of image metadata for each scene.
|
||||
@@ -168,7 +191,8 @@ class StoryImageGenerationService:
|
||||
provider=provider,
|
||||
width=width,
|
||||
height=height,
|
||||
model=model
|
||||
model=model,
|
||||
db=db
|
||||
)
|
||||
|
||||
image_results.append(image_result)
|
||||
|
||||
@@ -419,10 +419,17 @@ You have written approximately {current_word_count} words so far, leaving approx
|
||||
width: int = 1024,
|
||||
height: int = 1024,
|
||||
model: Optional[str] = None,
|
||||
db: Optional[Session] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Generate images for story scenes."""
|
||||
image_service = StoryImageGenerationService()
|
||||
return image_service.generate_scene_images(
|
||||
scenes=scenes, user_id=user_id, provider=provider, width=width, height=height, model=model
|
||||
scenes=scenes,
|
||||
user_id=user_id,
|
||||
provider=provider,
|
||||
width=width,
|
||||
height=height,
|
||||
model=model,
|
||||
db=db,
|
||||
)
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ from typing import List, Dict, Any, Optional
|
||||
from pathlib import Path
|
||||
from loguru import logger
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from services.user_workspace_manager import UserWorkspaceManager
|
||||
|
||||
|
||||
class StoryVideoGenerationService:
|
||||
@@ -26,14 +28,34 @@ class StoryVideoGenerationService:
|
||||
if output_dir:
|
||||
self.output_dir = Path(output_dir)
|
||||
else:
|
||||
# Default to backend/story_videos directory
|
||||
base_dir = Path(__file__).parent.parent.parent
|
||||
self.output_dir = base_dir / "story_videos"
|
||||
# Default to root/workspace/media/story_videos directory
|
||||
# services/story_writer/video_generation_service.py -> services -> backend -> root
|
||||
root_dir = Path(__file__).parent.parent.parent.parent
|
||||
self.output_dir = root_dir / "workspace" / "media" / "story_videos"
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
logger.info(f"[StoryVideoGeneration] Initialized with output directory: {self.output_dir}")
|
||||
|
||||
def _get_user_video_dir(self, user_id: str, db: Optional[Session] = None) -> Path:
|
||||
"""
|
||||
Get the video directory for a specific user.
|
||||
Falls back to default output_dir if workspace not found.
|
||||
"""
|
||||
if db and user_id:
|
||||
try:
|
||||
workspace_manager = UserWorkspaceManager(db)
|
||||
workspace = workspace_manager.get_user_workspace(user_id)
|
||||
if workspace:
|
||||
# Use media/story_videos inside user workspace
|
||||
user_video_dir = Path(workspace['workspace_path']) / "media" / "story_videos"
|
||||
user_video_dir.mkdir(parents=True, exist_ok=True)
|
||||
return user_video_dir
|
||||
except Exception as e:
|
||||
logger.warning(f"[StoryVideoGeneration] Failed to resolve user workspace path for {user_id}: {e}")
|
||||
|
||||
return self.output_dir
|
||||
|
||||
def _generate_video_filename(self, story_title: str = "story") -> str:
|
||||
"""Generate a unique filename for a story video."""
|
||||
# Clean story title for filename
|
||||
@@ -41,7 +63,7 @@ class StoryVideoGenerationService:
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
return f"story_{clean_title}_{unique_id}.mp4"
|
||||
|
||||
def save_scene_video(self, video_bytes: bytes, scene_number: int, user_id: str) -> Dict[str, str]:
|
||||
def save_scene_video(self, video_bytes: bytes, scene_number: int, user_id: str, db: Optional[Session] = None) -> Dict[str, str]:
|
||||
"""
|
||||
Save individual scene video bytes to file.
|
||||
|
||||
@@ -49,6 +71,7 @@ class StoryVideoGenerationService:
|
||||
video_bytes: Raw video file bytes (mp4/webm format)
|
||||
scene_number: Scene number for naming
|
||||
user_id: Clerk user ID for naming
|
||||
db: Database session for workspace resolution
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: Video metadata with video_url and video_filename
|
||||
@@ -59,7 +82,9 @@ class StoryVideoGenerationService:
|
||||
timestamp = str(uuid.uuid4())[:8]
|
||||
filename = f"scene_{scene_number}_{clean_user_id}_{timestamp}.mp4"
|
||||
|
||||
video_path = self.output_dir / filename
|
||||
# Resolve output directory (user workspace or default)
|
||||
output_dir = self._get_user_video_dir(user_id, db)
|
||||
video_path = output_dir / filename
|
||||
|
||||
# Write video bytes to file
|
||||
with open(video_path, 'wb') as f:
|
||||
@@ -89,7 +114,8 @@ class StoryVideoGenerationService:
|
||||
audio_path: str,
|
||||
user_id: str,
|
||||
duration: Optional[float] = None,
|
||||
fps: int = 24
|
||||
fps: int = 24,
|
||||
db: Optional[Session] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate a video clip for a single story scene.
|
||||
@@ -101,6 +127,7 @@ class StoryVideoGenerationService:
|
||||
user_id (str): Clerk user ID for subscription checking (for future usage tracking).
|
||||
duration (float, optional): Video duration in seconds. If None, uses audio duration.
|
||||
fps (int): Frames per second for video (default: 24).
|
||||
db (Session, optional): Database session.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Video metadata including file path, URL, and scene info.
|
||||
@@ -175,7 +202,10 @@ class StoryVideoGenerationService:
|
||||
|
||||
# Generate video filename
|
||||
video_filename = f"scene_{scene_number}_{scene_title.replace(' ', '_').replace('/', '_')[:50]}_{uuid.uuid4().hex[:8]}.mp4"
|
||||
video_path = self.output_dir / video_filename
|
||||
|
||||
# Determine output directory (user workspace or default)
|
||||
output_dir = self._get_user_video_dir(user_id, db)
|
||||
video_path = output_dir / video_filename
|
||||
|
||||
# Write video file
|
||||
video_clip.write_videofile(
|
||||
|
||||
Reference in New Issue
Block a user