Creates the API layer for Open Notebook Creates a services API gateway for the Streamlit front-end Migrates the SurrealDB SDK to the official one Change all database calls to async New podcast framework supporting multiple speaker configurations Implement the surreal-commands library for async processing Improve docker image and docker-compose configurations
195 lines
6.8 KiB
Python
195 lines
6.8 KiB
Python
import time
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from loguru import logger
|
|
from pydantic import BaseModel
|
|
from surreal_commands import CommandInput, CommandOutput, command
|
|
|
|
from open_notebook.config import DATA_FOLDER
|
|
from open_notebook.database.repository import ensure_record_id, repo_query
|
|
from open_notebook.domain.podcast import EpisodeProfile, PodcastEpisode, SpeakerProfile
|
|
|
|
try:
|
|
from podcast_creator import configure, create_podcast
|
|
except ImportError as e:
|
|
logger.error(f"Failed to import podcast_creator: {e}")
|
|
raise ValueError("podcast_creator library not available")
|
|
|
|
|
|
# Add debugging to see if this module is being imported
|
|
logger.info("=== IMPORTING podcast_commands.py ===")
|
|
logger.info("Registering podcast commands...")
|
|
|
|
|
|
def full_model_dump(model):
|
|
if isinstance(model, BaseModel):
|
|
return model.model_dump()
|
|
elif isinstance(model, dict):
|
|
return {k: full_model_dump(v) for k, v in model.items()}
|
|
elif isinstance(model, list):
|
|
return [full_model_dump(item) for item in model]
|
|
else:
|
|
return model
|
|
|
|
|
|
class PodcastGenerationInput(CommandInput):
|
|
episode_profile: str
|
|
speaker_profile: str
|
|
episode_name: str
|
|
content: str
|
|
briefing_suffix: Optional[str] = None
|
|
|
|
|
|
class PodcastGenerationOutput(CommandOutput):
|
|
success: bool
|
|
episode_id: Optional[str] = None
|
|
audio_file_path: Optional[str] = None
|
|
transcript: Optional[dict] = None
|
|
outline: Optional[dict] = None
|
|
processing_time: float
|
|
error_message: Optional[str] = None
|
|
|
|
|
|
@command("generate_podcast", app="open_notebook")
|
|
async def generate_podcast_command(
|
|
input_data: PodcastGenerationInput,
|
|
) -> PodcastGenerationOutput:
|
|
"""
|
|
Real podcast generation using podcast-creator library with Episode Profiles
|
|
"""
|
|
start_time = time.time()
|
|
|
|
try:
|
|
logger.info(
|
|
f"Starting podcast generation for episode: {input_data.episode_name}"
|
|
)
|
|
logger.info(f"Using episode profile: {input_data.episode_profile}")
|
|
|
|
# 1. Load Episode and Speaker profiles from SurrealDB
|
|
episode_profile = await EpisodeProfile.get_by_name(input_data.episode_profile)
|
|
if not episode_profile:
|
|
raise ValueError(
|
|
f"Episode profile '{input_data.episode_profile}' not found"
|
|
)
|
|
|
|
speaker_profile = await SpeakerProfile.get_by_name(
|
|
episode_profile.speaker_config
|
|
)
|
|
if not speaker_profile:
|
|
raise ValueError(
|
|
f"Speaker profile '{episode_profile.speaker_config}' not found"
|
|
)
|
|
|
|
logger.info(f"Loaded episode profile: {episode_profile.name}")
|
|
logger.info(f"Loaded speaker profile: {speaker_profile.name}")
|
|
|
|
# 3. Load all profiles and configure podcast-creator
|
|
episode_profiles = await repo_query("SELECT * FROM episode_profile")
|
|
speaker_profiles = await repo_query("SELECT * FROM speaker_profile")
|
|
|
|
# Transform the surrealdb array into a dictionary for podcast-creator
|
|
episode_profiles_dict = {
|
|
profile["name"]: profile for profile in episode_profiles
|
|
}
|
|
speaker_profiles_dict = {
|
|
profile["name"]: profile for profile in speaker_profiles
|
|
}
|
|
|
|
# 4. Generate briefing
|
|
briefing = episode_profile.default_briefing
|
|
if input_data.briefing_suffix:
|
|
briefing += f"\n\nAdditional instructions: {input_data.briefing_suffix}"
|
|
|
|
# Create the a record for the episose and associate with the ongoing command
|
|
episode = PodcastEpisode(
|
|
name=input_data.episode_name,
|
|
episode_profile=full_model_dump(episode_profile.model_dump()),
|
|
speaker_profile=full_model_dump(speaker_profile.model_dump()),
|
|
command=ensure_record_id(input_data.execution_context.command_id)
|
|
if input_data.execution_context
|
|
else None,
|
|
briefing=briefing,
|
|
content=input_data.content,
|
|
audio_file=None,
|
|
transcript=None,
|
|
outline=None,
|
|
)
|
|
await episode.save()
|
|
|
|
configure("speakers_config", {"profiles": speaker_profiles_dict})
|
|
configure("episode_config", {"profiles": episode_profiles_dict})
|
|
|
|
logger.info("Configured podcast-creator with episode and speaker profiles")
|
|
|
|
logger.info(f"Generated briefing (length: {len(briefing)} chars)")
|
|
|
|
# 5. Create output directory
|
|
output_dir = Path(f"{DATA_FOLDER}/podcasts/episodes/{input_data.episode_name}")
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
logger.info(f"Created output directory: {output_dir}")
|
|
|
|
# 6. Generate podcast using podcast-creator
|
|
logger.info("Starting podcast generation with podcast-creator...")
|
|
|
|
result = await create_podcast(
|
|
content=input_data.content,
|
|
briefing=briefing,
|
|
episode_name=input_data.episode_name,
|
|
output_dir=str(output_dir),
|
|
speaker_config=speaker_profile.name,
|
|
episode_profile=episode_profile.name,
|
|
)
|
|
|
|
episode.audio_file = (
|
|
str(result.get("final_output_file_path")) if result else None
|
|
)
|
|
episode.transcript = {
|
|
"transcript": full_model_dump(result["transcript"]) if result else None
|
|
}
|
|
episode.outline = full_model_dump(result["outline"]) if result else None
|
|
await episode.save()
|
|
|
|
processing_time = time.time() - start_time
|
|
logger.info(
|
|
f"Successfully generated podcast episode: {episode.id} in {processing_time:.2f}s"
|
|
)
|
|
|
|
return PodcastGenerationOutput(
|
|
success=True,
|
|
episode_id=str(episode.id),
|
|
audio_file_path=str(result.get("final_output_file_path"))
|
|
if result
|
|
else None,
|
|
transcript={"transcript": full_model_dump(result["transcript"])}
|
|
if result.get("transcript")
|
|
else None,
|
|
outline=full_model_dump(result["outline"])
|
|
if result.get("outline")
|
|
else None,
|
|
processing_time=processing_time,
|
|
)
|
|
|
|
except Exception as e:
|
|
processing_time = time.time() - start_time
|
|
logger.error(f"Podcast generation failed: {e}")
|
|
logger.exception(e)
|
|
|
|
return PodcastGenerationOutput(
|
|
success=False, processing_time=processing_time, error_message=str(e)
|
|
)
|
|
|
|
|
|
# Add debugging to confirm commands are registered
|
|
logger.info("✅ Podcast commands registered: generate_podcast")
|
|
logger.info("=== FINISHED IMPORTING podcast_commands.py ===")
|
|
|
|
# Let's also verify what the registry contains
|
|
try:
|
|
from surreal_commands import registry
|
|
|
|
commands = registry.list_commands()
|
|
logger.info(f"Registry after podcast import: {commands}")
|
|
except Exception as e:
|
|
logger.error(f"Error checking registry: {e}")
|