Added video studio router and endpoints. Added research router and endpoints. Added youtube router and endpoints. Added onboarding utils router and endpoints. Added onboarding utils service. Added onboarding utils models. Added onboarding utils routes. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils.
This commit is contained in:
@@ -26,6 +26,76 @@ YOUTUBE_AUDIO_DIR.mkdir(parents=True, exist_ok=True)
|
||||
# Initialize audio service
|
||||
audio_service = StoryAudioGenerationService(output_dir=str(YOUTUBE_AUDIO_DIR))
|
||||
|
||||
# WaveSpeed Minimax Speech voice ids include language-specific voices
|
||||
# Ref: https://wavespeed.ai/docs/docs-api/minimax/minimax_speech_voice_id
|
||||
LANGUAGE_CODE_TO_LANGUAGE_BOOST = {
|
||||
"en": "English",
|
||||
"es": "Spanish",
|
||||
"fr": "French",
|
||||
"de": "German",
|
||||
"pt": "Portuguese",
|
||||
"it": "Italian",
|
||||
"hi": "Hindi",
|
||||
"ar": "Arabic",
|
||||
"ru": "Russian",
|
||||
"ja": "Japanese",
|
||||
"ko": "Korean",
|
||||
"zh": "Chinese",
|
||||
"vi": "Vietnamese",
|
||||
"id": "Indonesian",
|
||||
"tr": "Turkish",
|
||||
"nl": "Dutch",
|
||||
"pl": "Polish",
|
||||
"th": "Thai",
|
||||
"uk": "Ukrainian",
|
||||
"el": "Greek",
|
||||
"cs": "Czech",
|
||||
"fi": "Finnish",
|
||||
"ro": "Romanian",
|
||||
}
|
||||
|
||||
# Default language-specific Minimax voices (first-choice). We keep English on the existing "persona" voices.
|
||||
LANGUAGE_BOOST_TO_DEFAULT_VOICE_ID = {
|
||||
"Spanish": "Spanish_male_1_v1",
|
||||
"French": "French_male_1_v1",
|
||||
"German": "German_male_1_v1",
|
||||
"Portuguese": "Portuguese_male_1_v1",
|
||||
"Italian": "Italian_male_1_v1",
|
||||
"Hindi": "Hindi_male_1_v1",
|
||||
"Arabic": "Arabic_male_1_v1",
|
||||
"Russian": "Russian_male_1_v1",
|
||||
"Japanese": "Japanese_male_1_v1",
|
||||
"Korean": "Korean_male_1_v1",
|
||||
"Chinese": "Chinese_male_1_v1",
|
||||
"Vietnamese": "Vietnamese_male_1_v1",
|
||||
"Indonesian": "Indonesian_male_1_v1",
|
||||
"Turkish": "Turkish_male_1_v1",
|
||||
"Dutch": "Dutch_male_1_v1",
|
||||
"Polish": "Polish_male_1_v1",
|
||||
"Thai": "Thai_male_1_v1",
|
||||
"Ukrainian": "Ukrainian_male_1_v1",
|
||||
"Greek": "Greek_male_1_v1",
|
||||
"Czech": "Czech_male_1_v1",
|
||||
"Finnish": "Finnish_male_1_v1",
|
||||
"Romanian": "Romanian_male_1_v1",
|
||||
}
|
||||
|
||||
|
||||
def _resolve_language_boost(language: Optional[str], explicit_language_boost: Optional[str]) -> str:
|
||||
"""
|
||||
Determine the effective WaveSpeed `language_boost`.
|
||||
- If user explicitly provided language_boost, use it (including "auto").
|
||||
- Else if language code provided, map to the WaveSpeed boost label.
|
||||
- Else default to English (backwards compatible).
|
||||
"""
|
||||
if explicit_language_boost is not None and str(explicit_language_boost).strip() != "":
|
||||
return str(explicit_language_boost).strip()
|
||||
|
||||
if language is not None and str(language).strip() != "":
|
||||
lang_code = str(language).strip().lower()
|
||||
return LANGUAGE_CODE_TO_LANGUAGE_BOOST.get(lang_code, "auto")
|
||||
|
||||
return "English"
|
||||
|
||||
def select_optimal_emotion(scene_title: str, narration: str, video_plan_context: Optional[Dict[str, Any]] = None) -> str:
|
||||
"""
|
||||
@@ -153,6 +223,7 @@ class YouTubeAudioRequest(BaseModel):
|
||||
scene_title: str
|
||||
text: str
|
||||
voice_id: Optional[str] = None # Will auto-select based on content if not provided
|
||||
language: Optional[str] = None # Language code for multilingual audio (e.g., "en", "es", "fr")
|
||||
speed: float = 1.0
|
||||
volume: float = 1.0
|
||||
pitch: float = 0.0
|
||||
@@ -164,7 +235,7 @@ class YouTubeAudioRequest(BaseModel):
|
||||
bitrate: int = 256000 # Highest quality: 256kbps (valid values: 32000, 64000, 128000, 256000)
|
||||
channel: Optional[str] = "2" # Stereo for richer audio (valid values: "1" or "2")
|
||||
format: Optional[str] = "mp3" # Universal format for web
|
||||
language_boost: Optional[str] = "English" # Optimize for English content
|
||||
language_boost: Optional[str] = None # If not provided, inferred from `language` (or defaults to English)
|
||||
enable_sync_mode: bool = True
|
||||
# Context for intelligent voice/emotion selection
|
||||
video_plan_context: Optional[Dict[str, Any]] = None # Optional video plan for context-aware voice selection
|
||||
@@ -224,13 +295,24 @@ async def generate_youtube_scene_audio(
|
||||
|
||||
logger.info(f"[YouTubeAudio] Text preprocessing: {len(request.text)} -> {len(processed_text)} characters")
|
||||
|
||||
effective_language_boost = _resolve_language_boost(request.language, request.language_boost)
|
||||
|
||||
# Intelligent voice and emotion selection based on content analysis
|
||||
if not request.voice_id:
|
||||
selected_voice = select_optimal_voice(
|
||||
request.scene_title,
|
||||
processed_text,
|
||||
request.video_plan_context
|
||||
)
|
||||
# If non-English language is selected, default to the language-specific Minimax voice_id.
|
||||
# Otherwise keep the existing English persona voice selection logic.
|
||||
if effective_language_boost in LANGUAGE_BOOST_TO_DEFAULT_VOICE_ID and effective_language_boost not in ["English", "auto"]:
|
||||
selected_voice = LANGUAGE_BOOST_TO_DEFAULT_VOICE_ID[effective_language_boost]
|
||||
logger.info(
|
||||
f"[VoiceSelection] Using language-specific default voice '{selected_voice}' "
|
||||
f"(language_boost={effective_language_boost}, language={request.language})"
|
||||
)
|
||||
else:
|
||||
selected_voice = select_optimal_voice(
|
||||
request.scene_title,
|
||||
processed_text,
|
||||
request.video_plan_context
|
||||
)
|
||||
else:
|
||||
selected_voice = request.voice_id
|
||||
|
||||
@@ -244,7 +326,10 @@ async def generate_youtube_scene_audio(
|
||||
else:
|
||||
selected_emotion = request.emotion
|
||||
|
||||
logger.info(f"[YouTubeAudio] Voice selection: {selected_voice}, Emotion: {selected_emotion}")
|
||||
logger.info(
|
||||
f"[YouTubeAudio] Voice selection: {selected_voice}, Emotion: {selected_emotion}, "
|
||||
f"language={request.language}, language_boost={effective_language_boost}"
|
||||
)
|
||||
|
||||
# Build kwargs for optional parameters - use defaults if None
|
||||
# WaveSpeed API requires specific values, so we provide sensible defaults
|
||||
@@ -252,7 +337,11 @@ async def generate_youtube_scene_audio(
|
||||
optional_kwargs = {}
|
||||
|
||||
# DEBUG: Log what values we received
|
||||
logger.info(f"[YouTubeAudio] Request parameters: sample_rate={request.sample_rate}, bitrate={request.bitrate}, channel={request.channel}, format={request.format}, language_boost={request.language_boost}")
|
||||
logger.info(
|
||||
f"[YouTubeAudio] Request parameters: sample_rate={request.sample_rate}, bitrate={request.bitrate}, "
|
||||
f"channel={request.channel}, format={request.format}, language_boost={request.language_boost}, "
|
||||
f"effective_language_boost={effective_language_boost}, language={request.language}"
|
||||
)
|
||||
|
||||
# sample_rate: Use provided value or omit (WaveSpeed will use default)
|
||||
if request.sample_rate is not None:
|
||||
@@ -276,9 +365,9 @@ async def generate_youtube_scene_audio(
|
||||
if request.format is not None:
|
||||
optional_kwargs["format"] = request.format
|
||||
|
||||
# language_boost: Use provided value or omit (WaveSpeed will use default)
|
||||
if request.language_boost is not None:
|
||||
optional_kwargs["language_boost"] = request.language_boost
|
||||
# language_boost: always send resolved value (improves pronunciation and helps multilingual voices)
|
||||
if effective_language_boost is not None and str(effective_language_boost).strip() != "":
|
||||
optional_kwargs["language_boost"] = effective_language_boost
|
||||
|
||||
logger.info(f"[YouTubeAudio] Final optional_kwargs: {optional_kwargs}")
|
||||
|
||||
|
||||
@@ -287,7 +287,7 @@ async def create_video_plan(
|
||||
|
||||
# Check for existing YouTube creator avatar in asset library
|
||||
asset_service = ContentAssetService(db)
|
||||
existing_avatars = asset_service.get_assets(
|
||||
existing_avatars, _ = asset_service.get_user_assets(
|
||||
user_id=user_id,
|
||||
asset_type=AssetType.IMAGE,
|
||||
source_module=AssetSource.YOUTUBE_CREATOR,
|
||||
@@ -685,11 +685,12 @@ async def render_single_scene_video(
|
||||
async def get_render_status(
|
||||
task_id: str,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
) -> Dict[str, Any]:
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get the status of a video rendering task.
|
||||
|
||||
Returns current progress, status, and result when complete.
|
||||
Returns None if task not found (matches podcast pattern for graceful handling).
|
||||
"""
|
||||
try:
|
||||
require_authenticated_user(current_user)
|
||||
@@ -697,24 +698,17 @@ async def get_render_status(
|
||||
logger.debug(f"[YouTubeAPI] Getting render status for task: {task_id}")
|
||||
task_status = task_manager.get_task_status(task_id)
|
||||
if not task_status:
|
||||
logger.warning(
|
||||
f"[YouTubeAPI] Task {task_id} not found. "
|
||||
f"Available tasks: {list(task_manager.task_storage.keys())[:5]}..."
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail={
|
||||
"error": "Task not found",
|
||||
"message": "The render task was not found. It may have expired, been cleaned up, or the server may have restarted.",
|
||||
"task_id": task_id,
|
||||
"user_action": "Please try rendering again."
|
||||
}
|
||||
# Log at DEBUG level - null is expected when tasks expire or server restarts
|
||||
# This prevents log spam from frontend polling for expired/completed tasks
|
||||
# Return None instead of raising 404 to match podcast pattern for graceful frontend handling
|
||||
logger.debug(
|
||||
f"[YouTubeAPI] Task {task_id} not found (may have expired or been cleaned up). "
|
||||
f"Available tasks: {len(task_manager.task_storage)}"
|
||||
)
|
||||
return None
|
||||
|
||||
return task_status
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"[YouTubeAPI] Error getting render status: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
@@ -1201,6 +1195,12 @@ def _execute_scene_video_render_task(
|
||||
result=result,
|
||||
)
|
||||
|
||||
# Verify the task status was updated correctly (matches podcast pattern)
|
||||
updated_status = task_manager.get_task_status(task_id)
|
||||
logger.info(
|
||||
f"[YouTubeRenderer] Task status after update: task_id={task_id}, status={updated_status.get('status') if updated_status else 'None'}, has_result={bool(updated_status.get('result') if updated_status else False)}, video_url={updated_status.get('result', {}).get('video_url') if updated_status else 'N/A'}"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"[YouTubeRenderer] ✅ Single-scene render {task_id} completed (scene {scene_num}), cost=${total_cost:.2f}"
|
||||
)
|
||||
@@ -1348,27 +1348,37 @@ async def list_videos(
|
||||
List videos for the current user from the asset library (source: youtube_creator).
|
||||
Used to rescue/persist scene videos after reloads.
|
||||
"""
|
||||
user_id = require_authenticated_user(current_user)
|
||||
asset_service = ContentAssetService(db)
|
||||
try:
|
||||
user_id = require_authenticated_user(current_user)
|
||||
asset_service = ContentAssetService(db)
|
||||
|
||||
assets = asset_service.get_assets(
|
||||
user_id=user_id,
|
||||
asset_type=AssetType.VIDEO,
|
||||
source_module=AssetSource.YOUTUBE_CREATOR,
|
||||
limit=100,
|
||||
)
|
||||
assets, _ = asset_service.get_user_assets(
|
||||
user_id=user_id,
|
||||
asset_type=AssetType.VIDEO,
|
||||
source_module=AssetSource.YOUTUBE_CREATOR,
|
||||
limit=100,
|
||||
)
|
||||
|
||||
videos = []
|
||||
for asset in assets:
|
||||
videos.append({
|
||||
"scene_number": asset.asset_metadata.get("scene_number") if asset.asset_metadata else None,
|
||||
"video_url": asset.file_url,
|
||||
"filename": asset.filename,
|
||||
"created_at": asset.created_at,
|
||||
"resolution": asset.asset_metadata.get("resolution") if asset.asset_metadata else None,
|
||||
})
|
||||
videos = []
|
||||
for asset in assets:
|
||||
try:
|
||||
videos.append({
|
||||
"scene_number": asset.asset_metadata.get("scene_number") if asset.asset_metadata else None,
|
||||
"video_url": asset.file_url,
|
||||
"filename": asset.filename,
|
||||
"created_at": asset.created_at.isoformat() if asset.created_at else None,
|
||||
"resolution": asset.asset_metadata.get("resolution") if asset.asset_metadata else None,
|
||||
})
|
||||
except Exception as asset_error:
|
||||
logger.warning(f"[YouTubeAPI] Error processing asset {asset.id if hasattr(asset, 'id') else 'unknown'}: {asset_error}")
|
||||
continue # Skip this asset and continue with others
|
||||
|
||||
return VideoListResponse(videos=videos)
|
||||
logger.info(f"[YouTubeAPI] Listed {len(videos)} videos for user {user_id}")
|
||||
return VideoListResponse(videos=videos)
|
||||
except Exception as e:
|
||||
logger.error(f"[YouTubeAPI] Error listing videos: {e}", exc_info=True)
|
||||
# Return empty list on error rather than failing completely
|
||||
return VideoListResponse(videos=[], success=False, message=f"Failed to list videos: {str(e)}")
|
||||
|
||||
|
||||
def _execute_combine_video_task(
|
||||
|
||||
Reference in New Issue
Block a user