Spaces:
Sleeping
Sleeping
| from typing import Any, Dict, Optional | |
| from fastapi import HTTPException | |
| from loguru import logger | |
| from pydantic import BaseModel | |
| from surreal_commands import get_command_status, submit_command | |
| from open_notebook.domain.notebook import Notebook | |
| from open_notebook.domain.podcast import EpisodeProfile, PodcastEpisode, SpeakerProfile | |
| class PodcastGenerationRequest(BaseModel): | |
| """Request model for podcast generation""" | |
| episode_profile: str | |
| speaker_profile: str | |
| episode_name: str | |
| content: Optional[str] = None | |
| notebook_id: Optional[str] = None | |
| briefing_suffix: Optional[str] = None | |
| class PodcastGenerationResponse(BaseModel): | |
| """Response model for podcast generation""" | |
| job_id: str | |
| status: str | |
| message: str | |
| episode_profile: str | |
| episode_name: str | |
| class PodcastService: | |
| """Service layer for podcast operations""" | |
| async def submit_generation_job( | |
| episode_profile_name: str, | |
| speaker_profile_name: str, | |
| episode_name: str, | |
| notebook_id: Optional[str] = None, | |
| content: Optional[str] = None, | |
| briefing_suffix: Optional[str] = None, | |
| ) -> str: | |
| """Submit a podcast generation job for background processing""" | |
| try: | |
| # Validate episode profile exists | |
| episode_profile = await EpisodeProfile.get_by_name(episode_profile_name) | |
| if not episode_profile: | |
| raise ValueError(f"Episode profile '{episode_profile_name}' not found") | |
| # Validate speaker profile exists | |
| speaker_profile = await SpeakerProfile.get_by_name(speaker_profile_name) | |
| if not speaker_profile: | |
| raise ValueError(f"Speaker profile '{speaker_profile_name}' not found") | |
| # Get content from notebook if not provided directly | |
| if not content and notebook_id: | |
| try: | |
| notebook = await Notebook.get(notebook_id) | |
| # Get notebook context (this may need to be adjusted based on actual Notebook implementation) | |
| content = ( | |
| await notebook.get_context() | |
| if hasattr(notebook, "get_context") | |
| else str(notebook) | |
| ) | |
| except Exception as e: | |
| logger.warning( | |
| f"Failed to get notebook content, using notebook_id as content: {e}" | |
| ) | |
| content = f"Notebook ID: {notebook_id}" | |
| if not content: | |
| raise ValueError( | |
| "Content is required - provide either content or notebook_id" | |
| ) | |
| # Prepare command arguments | |
| command_args = { | |
| "episode_profile": episode_profile_name, | |
| "speaker_profile": speaker_profile_name, | |
| "episode_name": episode_name, | |
| "content": str(content), | |
| "briefing_suffix": briefing_suffix, | |
| } | |
| # Ensure command modules are imported before submitting | |
| # This is needed because submit_command validates against local registry | |
| try: | |
| import commands.podcast_commands # noqa: F401 | |
| except ImportError as import_err: | |
| logger.error(f"Failed to import podcast commands: {import_err}") | |
| raise ValueError("Podcast commands not available") | |
| # Submit command to surreal-commands | |
| job_id = submit_command("open_notebook", "generate_podcast", command_args) | |
| # Convert RecordID to string if needed | |
| if not job_id: | |
| raise ValueError("Failed to get job_id from submit_command") | |
| job_id_str = str(job_id) | |
| logger.info( | |
| f"Submitted podcast generation job: {job_id_str} for episode '{episode_name}'" | |
| ) | |
| return job_id_str | |
| except Exception as e: | |
| logger.error(f"Failed to submit podcast generation job: {e}") | |
| raise HTTPException( | |
| status_code=500, | |
| detail=f"Failed to submit podcast generation job: {str(e)}", | |
| ) | |
| async def get_job_status(job_id: str) -> Dict[str, Any]: | |
| """Get status of a podcast generation job""" | |
| try: | |
| status = await get_command_status(job_id) | |
| return { | |
| "job_id": job_id, | |
| "status": status.status if status else "unknown", | |
| "result": status.result if status else None, | |
| "error_message": getattr(status, "error_message", None) | |
| if status | |
| else None, | |
| "created": str(status.created) | |
| if status and hasattr(status, "created") and status.created | |
| else None, | |
| "updated": str(status.updated) | |
| if status and hasattr(status, "updated") and status.updated | |
| else None, | |
| "progress": getattr(status, "progress", None) if status else None, | |
| } | |
| except Exception as e: | |
| logger.error(f"Failed to get podcast job status: {e}") | |
| raise HTTPException( | |
| status_code=500, detail=f"Failed to get job status: {str(e)}" | |
| ) | |
| async def list_episodes() -> list: | |
| """List all podcast episodes""" | |
| try: | |
| episodes = await PodcastEpisode.get_all(order_by="created desc") | |
| return episodes | |
| except Exception as e: | |
| logger.error(f"Failed to list podcast episodes: {e}") | |
| raise HTTPException( | |
| status_code=500, detail=f"Failed to list episodes: {str(e)}" | |
| ) | |
| async def get_episode(episode_id: str) -> PodcastEpisode: | |
| """Get a specific podcast episode""" | |
| try: | |
| episode = await PodcastEpisode.get(episode_id) | |
| return episode | |
| except Exception as e: | |
| logger.error(f"Failed to get podcast episode {episode_id}: {e}") | |
| raise HTTPException(status_code=404, detail=f"Episode not found: {str(e)}") | |
| class DefaultProfiles: | |
| """Utility class for creating default profiles (if needed beyond migration data)""" | |
| async def create_default_episode_profiles(): | |
| """Create default episode profiles if they don't exist""" | |
| try: | |
| # Check if profiles already exist | |
| existing = await EpisodeProfile.get_all() | |
| if existing: | |
| logger.info(f"Episode profiles already exist: {len(existing)} found") | |
| return existing | |
| # This would create profiles, but since we have migration data, | |
| # this is mainly for future extensibility | |
| logger.info( | |
| "Default episode profiles should be created via database migration" | |
| ) | |
| return [] | |
| except Exception as e: | |
| logger.error(f"Failed to create default episode profiles: {e}") | |
| raise | |
| async def create_default_speaker_profiles(): | |
| """Create default speaker profiles if they don't exist""" | |
| try: | |
| # Check if profiles already exist | |
| existing = await SpeakerProfile.get_all() | |
| if existing: | |
| logger.info(f"Speaker profiles already exist: {len(existing)} found") | |
| return existing | |
| # This would create profiles, but since we have migration data, | |
| # this is mainly for future extensibility | |
| logger.info( | |
| "Default speaker profiles should be created via database migration" | |
| ) | |
| return [] | |
| except Exception as e: | |
| logger.error(f"Failed to create default speaker profiles: {e}") | |
| raise | |