"""Module API Routes - All AI processing endpoints""" from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, BackgroundTasks, Body from sqlalchemy.orm import Session from typing import Optional, List, Union, Any from uuid import UUID from pydantic import BaseModel import json from app.database import get_db from app.models.job import Job from app.models.user import User from app.services import ( image_generator, image_upscaler, background_remover, video_generator, video_upscaler, subtitle_processor, voice_to_text, text_to_speech, alt_text_generator, prompt_studio, markdown_tools, sound_effects ) from app.workers.tasks import process_video_generation, process_image_upscaling router = APIRouter() # ============== REQUEST MODELS ============== class ImageGenerateRequest(BaseModel): prompt: str provider: str = "openai" model: Optional[str] = None # Generic provider_options accepts any key-value pairs provider_options: Optional[dict] = None # Keep backward compatibility fields width: Optional[int] = None height: Optional[int] = None style: Optional[str] = None quality: Optional[str] = None negative_prompt: Optional[str] = None aspect_ratio: Optional[str] = None style_preset: Optional[str] = None reference_asset_id: Optional[str] = None def get_merged_options(self) -> dict: """Merge backward-compatible fields with provider_options""" options = self.provider_options.copy() if self.provider_options else {} # Add backward-compatible fields if not in provider_options if self.width and 'width' not in options: options['width'] = self.width if self.height and 'height' not in options: options['height'] = self.height if self.style and 'style' not in options: options['style'] = self.style if self.quality and 'quality' not in options: options['quality'] = self.quality if self.negative_prompt and 'negative_prompt' not in options: options['negative_prompt'] = self.negative_prompt if self.aspect_ratio and 'aspect_ratio' not in options: options['aspect_ratio'] = self.aspect_ratio if self.style_preset and 'style_preset' not in options: options['style_preset'] = self.style_preset if self.reference_asset_id and 'reference_asset_id' not in options: options['reference_asset_id'] = self.reference_asset_id return options class VideoGenerateRequest(BaseModel): prompt: Optional[str] = None provider: str = "runway" model: Optional[str] = None # Generic provider_options provider_options: Optional[dict] = None # Backward compatibility fields duration: Optional[Union[int, str]] = None aspect_ratio: Optional[str] = None resolution: Optional[str] = None camera_control: Optional[dict] = None frame_position: Optional[str] = None first_frame_asset_id: Optional[str] = None last_frame_asset_id: Optional[str] = None reference_asset_ids: Optional[List[str]] = None input_asset_id: Optional[str] = None def get_merged_options(self) -> dict: """Merge backward-compatible fields with provider_options""" options = self.provider_options.copy() if self.provider_options else {} # Add backward-compatible fields if not in provider_options if self.duration and 'duration' not in options: options['duration'] = self.duration if self.aspect_ratio and 'aspect_ratio' not in options: options['aspect_ratio'] = self.aspect_ratio if self.resolution and 'resolution' not in options: options['resolution'] = self.resolution if self.camera_control and 'camera_control' not in options: options['camera_control'] = self.camera_control if self.frame_position and 'frame_position' not in options: options['frame_position'] = self.frame_position if self.first_frame_asset_id and 'first_frame_asset_id' not in options: options['first_frame_asset_id'] = self.first_frame_asset_id if self.last_frame_asset_id and 'last_frame_asset_id' not in options: options['last_frame_asset_id'] = self.last_frame_asset_id if self.reference_asset_ids and 'reference_asset_ids' not in options: options['reference_asset_ids'] = self.reference_asset_ids if self.input_asset_id and 'input_asset_id' not in options: options['input_asset_id'] = self.input_asset_id return options class TextToSpeechRequest(BaseModel): text: str voice_id: str = "21m00Tcm4TlvDq8ikWAM" model_id: str = "eleven_multilingual_v2" stability: float = 0.5 similarity_boost: float = 0.5 style: float = 0.0 use_speaker_boost: bool = True speed: float = 1.0 output_format: str = "mp3_44100_128" class SoundEffectRequest(BaseModel): text: str duration_seconds: Optional[float] = None prompt_influence: float = 0.3 class ImageUpscaleRequest(BaseModel): asset_id: str scale: int = 2 model: str = "Standard V2" output_format: str = "png" crop_to_fill: bool = False # Face enhancement face_enhancement: bool = False face_enhancement_creativity: Optional[float] = None face_enhancement_strength: Optional[float] = None # Frontend matches denoise_strength: Optional[float] = None sharpen: Optional[float] = None # Legacy / Other params detail: Optional[float] = None focus_boost: Optional[float] = None strength: Optional[float] = None subject_detection: Optional[str] = None class VideoUpscaleRequest(BaseModel): asset_id: str scale: int = 2 model: str = "auto" frame_interpolation: int = 1 # New Topaz parameters fps: Optional[float] = None sharpening: Optional[int] = None # 0-100 recover_detail: Optional[int] = None # 0-100 add_noise: Optional[int] = None # 0-100 video_type: Optional[str] = "Progressive" # Progressive, Interlaced, Interlaced Progressive video_type: Optional[str] = "Progressive" # Progressive, Interlaced, Interlaced Progressive face_enhancement: bool = False class FrameExtractionRequest(BaseModel): asset_id: str timestamp: float class RemoveBackgroundRequest(BaseModel): asset_id: str output_format: str = "png" refine_mask: bool = True class VoiceToTextRequest(BaseModel): asset_id: str output_format: str = "txt" translate: bool = False target_language: str = "EN-US" class AltTextRequest(BaseModel): asset_id: str class PromptEnhanceRequest(BaseModel): prompt: str style: str = "cinematic" provider: str = "openai" include_negative: bool = True include_technical: bool = True language: str = "en" # CinePrompt Studio Advanced Fields application: Optional[str] = None camera: Optional[str] = None lens: Optional[str] = None aspect_ratio: Optional[str] = "16:9" creative_freedom: float = 0.3 class MermaidRenderRequest(BaseModel): code: str output_format: str = "svg" theme: str = "default" background: str = "transparent" class MermaidGenerateRequest(BaseModel): description: str diagram_type: str = "flowchart" style: str = "detailed" render: bool = True class MarkdownConvertRequest(BaseModel): content: str output_format: str = "html" theme: str = "github" class MarkdownGenerateRequest(BaseModel): topic: str content_type: str = "article" length: str = "medium" include_toc: bool = True # ============== IMAGE MODULES ============== def job_response(job: Job) -> dict: """Format job for API response""" return { "id": str(job.id), "module": job.module, "action": job.action, "status": job.status, "progress": job.progress or 0, "input_data": job.input_data, "output_data": job.output_data, "input_asset_ids": [str(a) for a in job.input_asset_ids] if job.input_asset_ids else None, "output_asset_ids": [str(a) for a in job.output_asset_ids] if job.output_asset_ids else None, "error_message": job.error_message, "api_provider": job.api_provider, "api_model": job.api_model, "created_at": job.created_at.isoformat() if job.created_at else None, "completed_at": job.completed_at.isoformat() if job.completed_at else None, } @router.post("/image/generate") async def generate_image( request: ImageGenerateRequest, background_tasks: BackgroundTasks, db: Session = Depends(get_db) ): """Generate an image using various AI providers Providers: openai, dalle3, stable-diffusion, leonardo, ideogram, flux, gemini, nano-banana Supports iterative editing with reference_asset_id for nano-banana/gemini providers """ from app.models.asset import Asset import base64 user = db.query(User).filter(User.email == "test@forge.ai").first() input_data = request.model_dump(exclude_none=True) # If reference_asset_id is provided, load the image and convert to base64 if request.reference_asset_id: asset = db.query(Asset).filter(Asset.id == request.reference_asset_id).first() if asset and asset.file_path: import os if os.path.exists(asset.file_path): with open(asset.file_path, "rb") as f: image_data = f.read() # Convert to base64 for the generator input_data["reference_image"] = base64.b64encode(image_data).decode("utf-8") # Remove reference_asset_id from input_data (we've converted it) del input_data["reference_asset_id"] job = Job( user_id=user.id if user else None, module="image_generator", action="generate", input_data=input_data, status="queued", progress=0 ) db.add(job) db.commit() db.refresh(job) background_tasks.add_task(image_generator.generate, str(job.id)) return job_response(job) @router.post("/image/upscale") async def upscale_image( request: ImageUpscaleRequest, background_tasks: BackgroundTasks, db: Session = Depends(get_db) ): """Upscale an image using Topaz Labs Models: proteus, artemis, gaia, iris, nyx, rhea, theia, auto """ user = db.query(User).filter(User.email == "test@forge.ai").first() # Validate asset exists from app.models.asset import Asset asset = db.query(Asset).filter(Asset.id == UUID(request.asset_id)).first() if not asset: raise HTTPException(status_code=404, detail="Asset not found") job = Job( user_id=user.id if user else None, module="image_upscaler", action="upscale", input_data={ "scale": request.scale, "model": request.model, "face_enhancement": request.face_enhancement, # Use new fields mapped from frontend "denoise": request.denoise_strength, # Map denoise_strength -> denoise for backend service "sharpen": request.sharpen, # Optional extra params "face_enhancement_creativity": request.face_enhancement_creativity, "face_enhancement_strength": request.face_enhancement_strength, "output_format": request.output_format }, input_asset_ids=[asset.id], status="queued" ) db.add(job) db.commit() db.refresh(job) # Offload to Celery Worker (Redis) for scalability process_image_upscaling.delay(str(job.id)) return job_response(job) @router.post("/image/remove-background") async def remove_background( request: RemoveBackgroundRequest, background_tasks: BackgroundTasks, db: Session = Depends(get_db) ): """Remove background from image""" user = db.query(User).filter(User.email == "test@forge.ai").first() # Validate asset exists from app.models.asset import Asset asset = db.query(Asset).filter(Asset.id == UUID(request.asset_id)).first() if not asset: raise HTTPException(status_code=404, detail="Asset not found") job = Job( user_id=user.id if user else None, module="background_remover", action="remove", input_data={ "output_format": request.output_format, "refine_mask": request.refine_mask }, input_asset_ids=[asset.id], status="queued" ) db.add(job) db.commit() db.refresh(job) background_tasks.add_task(background_remover.remove_background, str(job.id)) return job_response(job) # ============== VIDEO MODULES ============== @router.post("/video/generate") async def generate_video( request: VideoGenerateRequest, background_tasks: BackgroundTasks, db: Session = Depends(get_db) ): """Generate video using Runway or Google Veo Runway: gen3_alpha, gen3_alpha_turbo, gen4 Veo: veo-3.1-generate-preview, veo-3.1-fast """ user = db.query(User).filter(User.email == "test@forge.ai").first() input_asset_ids = [] if request.input_asset_id: input_asset_ids.append(UUID(request.input_asset_id)) job = Job( user_id=user.id if user else None, module="video_generator", action="generate", input_data=request.model_dump(exclude_none=True), input_asset_ids=input_asset_ids if input_asset_ids else None, status="queued" ) db.add(job) db.commit() db.refresh(job) # Offload to Celery Worker (Redis) for scalability process_video_generation.delay(str(job.id)) return job_response(job) @router.post("/video/upscale") async def upscale_video( request: VideoUpscaleRequest, background_tasks: BackgroundTasks, db: Session = Depends(get_db) ): """Upscale video using Topaz Labs""" user = db.query(User).filter(User.email == "test@forge.ai").first() # Validate asset exists from app.models.asset import Asset asset = db.query(Asset).filter(Asset.id == UUID(request.asset_id)).first() if not asset: raise HTTPException(status_code=404, detail="Asset not found") job = Job( user_id=user.id if user else None, module="video_upscaler", action="upscale", input_data={ "scale": request.scale, "model": request.model, "frame_interpolation": request.frame_interpolation, "fps": request.fps, "sharpening": request.sharpening, "recover_detail": request.recover_detail, "add_noise": request.add_noise, "video_type": request.video_type, "face_enhancement": request.face_enhancement }, input_asset_ids=[asset.id], status="queued" ) db.add(job) db.commit() db.refresh(job) background_tasks.add_task(video_upscaler.upscale, str(job.id)) return job_response(job) @router.post("/video/extract-frame") async def extract_frame_endpoint( request: FrameExtractionRequest, db: Session = Depends(get_db) ): """Extract a single frame from a video""" from app.services import frame_extractor try: # Since extract_frame is sync (using subprocess), we can run it directly or in threadpool # For simplicity in FastAPI, just calling it is fine if it's fast (< few sec). # Topaz upscaler uses async + background tasks because it takes minutes. # fast-seeking ffmpeg extract is usually < 1s. new_asset = frame_extractor.extract_frame(request.asset_id, request.timestamp) return new_asset except Exception as e: import traceback traceback.print_exc() raise HTTPException(status_code=400, detail=str(e)) @router.get("/video/subtitles/config") async def get_subtitle_config(): """Get available subtitle configuration options""" return subtitle_processor.get_subtitle_config() @router.post("/video/subtitles") async def generate_subtitles( file: UploadFile = File(...), source_language: str = Form("auto"), target_language: Optional[str] = Form(None), burn_subtitles: bool = Form(False), whisper_model: str = Form("base"), output_format: str = Form("srt"), # Styling options font: str = Form("Arial"), font_size: int = Form(24), text_color: str = Form("white"), outline_color: str = Form("black"), outline_width: float = Form(2.0), background_color: Optional[str] = Form(None), background_opacity: float = Form(0.0), position: str = Form("bottom"), alignment: str = Form("center"), margin_v: int = Form(30), margin_h: int = Form(20), shadow: int = Form(0), bold: bool = Form(False), italic: bool = Form(False), font_preset: Optional[str] = Form(None), word_timestamps: bool = Form(False), subtitle_file: UploadFile = File(None), subtitle_asset_id: Optional[str] = Form(None), background_tasks: BackgroundTasks = None, db: Session = Depends(get_db) ): """ Generate subtitles for a video using OpenAI Whisper. - **file**: Video file to process - **source_language**: Language of the video (auto for detection) - **target_language**: Language to translate to (optional) - **burn_subtitles**: Burn subtitles into the video - **whisper_model**: Whisper model size (tiny, base, small, medium, large) - **output_format**: Output subtitle format (srt, vtt, ass) Styling Options (for burning): - font: Font family (Arial, Helvetica, etc.) - checks system availability - font_size: Font size in points - text_color: Primary text color - outline_color: Text outline color - outline_width: Outline thickness (0-5) - background_color: Background box color - background_opacity: Background opacity (0-1) - position: Vertical position (bottom/top/center) - alignment: Horizontal alignment (left/center/right) - margin_v: Vertical margin from edge - margin_h: Horizontal margin - shadow: Shadow depth (0-4) - bold: Use bold text - italic: Use italic text - font_preset: Predefined style preset (default/cinematic/documentary/news/social_media/minimal/bold) - word_timestamps: Include word-level timestamps - subtitle_file: Optional subtitle file (SRT) to burn instead of generating - subtitle_asset_id: Optional asset ID of existing subtitle to burn """ user = db.query(User).filter(User.email == "test@forge.ai").first() # Fallback to a default user or handle None if test user doesn't exist if not user: # Try to find any admin user or proceed with None (if Asset/Job models allow null user_id) user = db.query(User).order_by(User.id).first() import structlog logger = structlog.get_logger() logger.info("Subtitle generation request received", filename=file.filename, source_language=source_language, target_language=target_language, burn_subtitles=burn_subtitles, font=font, has_subtitle_file=bool(subtitle_file), subtitle_asset_id=subtitle_asset_id) from app.api.v1.assets import process_upload try: asset = await process_upload(file=file, source_module="subtitle_processor", db=db, user=user, overwrite=True) # Process optional subtitle file upload input_sub_id = subtitle_asset_id if subtitle_file: sub_asset = await process_upload(file=subtitle_file, source_module="subtitle_processor", db=db, user=user, overwrite=True, allow_extensions=['srt', 'vtt', 'ass']) input_sub_id = str(sub_asset.id) job = Job( user_id=user.id if user else None, module="subtitle_processor", action="generate", input_data={ "source_language": source_language, "target_language": target_language, "burn_subtitles": burn_subtitles, "whisper_model": whisper_model, "output_format": output_format, "font": font, "font_size": font_size, "text_color": text_color, "outline_color": outline_color, "outline_width": outline_width, "background_color": background_color, "background_opacity": background_opacity, "position": position, "alignment": alignment, "margin_v": margin_v, "margin_h": margin_h, "shadow": shadow, "bold": bold, "italic": italic, "font_preset": font_preset, "word_timestamps": word_timestamps, "subtitle_asset_id": input_sub_id }, input_asset_ids=[asset.id], status="pending" ) db.add(job) db.commit() db.refresh(job) if background_tasks: background_tasks.add_task(subtitle_processor.process, str(job.id)) return job_response(job) except Exception as e: logger.error("Failed to initiate subtitle job", error=str(e), exc_info=True) raise HTTPException(status_code=500, detail=f"Failed to start subtitle processing: {str(e)}") # ============== AUDIO MODULES ============== @router.post("/audio/voice-to-text") async def transcribe_audio( request: VoiceToTextRequest, background_tasks: BackgroundTasks, db: Session = Depends(get_db) ): """Transcribe audio to text using Whisper""" user = db.query(User).filter(User.email == "test@forge.ai").first() # Validate asset exists from app.models.asset import Asset asset = db.query(Asset).filter(Asset.id == UUID(request.asset_id)).first() if not asset: raise HTTPException(status_code=404, detail="Asset not found") job = Job( user_id=user.id if user else None, module="voice_to_text", action="transcribe", input_data={ "output_format": request.output_format, "translate": request.translate, "target_language": request.target_language }, input_asset_ids=[asset.id], status="queued" ) db.add(job) db.commit() db.refresh(job) background_tasks.add_task(voice_to_text.transcribe, str(job.id)) return job_response(job) @router.post("/audio/text-to-speech") async def synthesize_speech( request: TextToSpeechRequest, background_tasks: BackgroundTasks, db: Session = Depends(get_db) ): """Convert text to speech using ElevenLabs Models: eleven_multilingual_v2, eleven_flash_v2_5, eleven_turbo_v2_5, eleven_v3 """ user = db.query(User).filter(User.email == "test@forge.ai").first() job = Job( user_id=user.id if user else None, module="text_to_speech", action="synthesize", input_data=request.model_dump(), status="queued" ) db.add(job) db.commit() db.refresh(job) background_tasks.add_task(text_to_speech.synthesize, str(job.id)) return job_response(job) @router.post("/audio/speech-to-speech") async def convert_voice( file: UploadFile = File(...), voice_id: str = Form(...), background_tasks: BackgroundTasks = None, db: Session = Depends(get_db) ): """Convert voice to another voice using ElevenLabs""" user = db.query(User).filter(User.email == "test@forge.ai").first() from app.api.v1.assets import process_upload asset = await process_upload(file=file, source_module="speech_to_speech", db=db, user=user, overwrite=True) job = Job( user_id=user.id if user else None, module="speech_to_speech", action="convert", input_data={"voice_id": voice_id}, input_asset_ids=[asset.id], status="queued" ) db.add(job) db.commit() db.refresh(job) if background_tasks: background_tasks.add_task(text_to_speech.speech_to_speech, str(job.id)) return job_response(job) @router.post("/audio/sound-effects") async def generate_sound_effect( request: SoundEffectRequest, background_tasks: BackgroundTasks, db: Session = Depends(get_db) ): """Generate sound effects from text description using ElevenLabs Describe the sound you want - explosions, footsteps, ambient sounds, etc. Max duration: 22 seconds """ user = db.query(User).filter(User.email == "test@forge.ai").first() job = Job( user_id=user.id if user else None, module="sound_effects", action="generate", input_data=request.model_dump(), status="queued" ) db.add(job) db.commit() db.refresh(job) background_tasks.add_task(sound_effects.generate_sound_effect_job, str(job.id)) return job_response(job) @router.get("/audio/sound-effects/formats") async def get_sound_effect_formats(): """Get available output formats for sound effects""" generator = sound_effects.get_sound_effects_generator() return await generator.get_available_formats() # ============== TEXT MODULES ============== @router.post("/text/alt-text") async def generate_alt_text( request: AltTextRequest, background_tasks: BackgroundTasks, db: Session = Depends(get_db) ): """Generate alt text for image using GPT-4 Vision""" user = db.query(User).filter(User.email == "test@forge.ai").first() # Validate asset exists from app.models.asset import Asset asset = db.query(Asset).filter(Asset.id == UUID(request.asset_id)).first() if not asset: raise HTTPException(status_code=404, detail="Asset not found") job = Job( user_id=user.id if user else None, module="alt_text_generator", action="generate", input_data={}, input_asset_ids=[asset.id], status="queued" ) db.add(job) db.commit() db.refresh(job) background_tasks.add_task(alt_text_generator.generate, str(job.id)) return job_response(job) @router.get("/image/providers") def get_image_providers(): """Get all image providers with their capabilities (legacy format)""" from app.services.image_generator import IMAGE_PROVIDERS, STABILITY_STYLE_PRESETS # Add Stability style presets to the config providers = IMAGE_PROVIDERS.copy() if "stable-diffusion" in providers: providers["stable-diffusion"]["style_presets"] = STABILITY_STYLE_PRESETS return providers @router.get("/capabilities/image") def get_image_provider_capabilities(): """Get all image provider configurations with detailed controls""" from app.providers.image_providers import get_image_provider_configs return get_image_provider_configs() @router.get("/capabilities/video") def get_video_provider_capabilities(): """Get all video provider configurations with detailed controls""" from app.providers.video_providers import get_video_provider_configs return get_video_provider_configs() @router.get("/capabilities/image/{provider_id}") def get_image_provider_config(provider_id: str): """Get specific image provider configuration""" from app.providers.image_providers import IMAGE_PROVIDER_CONFIGS if provider_id not in IMAGE_PROVIDER_CONFIGS: raise HTTPException(status_code=404, detail="Provider not found") return IMAGE_PROVIDER_CONFIGS[provider_id].model_dump(by_alias=True) @router.get("/capabilities/video/{provider_id}") def get_video_provider_config(provider_id: str): """Get specific video provider configuration""" from app.providers.video_providers import VIDEO_PROVIDER_CONFIGS if provider_id not in VIDEO_PROVIDER_CONFIGS: raise HTTPException(status_code=404, detail="Provider not found") return VIDEO_PROVIDER_CONFIGS[provider_id].model_dump(by_alias=True) @router.post("/text/enhance-prompt") async def enhance_prompt( request: PromptEnhanceRequest, db: Session = Depends(get_db) ): """Enhance a prompt using AI (Gemini/OpenAI) Styles: cinematic, photographic, artistic, product, fantasy, minimal, vintage, futuristic, anime, portrait, landscape, abstract, fashion, architecture, food Providers: openai, gpt-image-1, stable-diffusion, midjourney, flux, leonardo """ result = await prompt_studio.enhance( prompt=request.prompt, style=request.style, provider=request.provider, include_negative=request.include_negative, include_technical=request.include_technical, language=request.language, # Advanced CinePrompt params application=request.application, camera=request.camera, lens=request.lens, aspect_ratio=request.aspect_ratio, creative_freedom=request.creative_freedom ) return result @router.get("/text/prompt-styles") async def get_prompt_styles(): """Get available prompt enhancement styles""" return prompt_studio.get_available_styles() @router.get("/text/cine-options") async def get_cine_options(): """Get CinePrompt Studio data (cameras, lenses, etc.)""" return prompt_studio.get_cine_options() # ============== MARKDOWN & MERMAID MODULES ============== @router.post("/text/mermaid/render") async def render_mermaid_diagram(request: MermaidRenderRequest): """Render Mermaid diagram code to SVG/PNG Themes: default, dark, forest, neutral Formats: svg, png """ result = await markdown_tools.render_mermaid( code=request.code, output_format=request.output_format, theme=request.theme, background=request.background ) return result @router.post("/text/mermaid/generate") async def generate_mermaid_diagram(request: MermaidGenerateRequest): """Generate Mermaid diagram from natural language description Diagram types: flowchart, sequence, class, state, er, journey, gantt, pie, mindmap, timeline, gitgraph Styles: simple, detailed, complex """ result = await markdown_tools.generate_mermaid_with_ai( description=request.description, diagram_type=request.diagram_type, style=request.style ) # Optionally render the diagram if request.render and result.get("success") and result.get("code"): render_result = await markdown_tools.render_mermaid(result["code"]) result["rendered"] = render_result return result @router.get("/text/mermaid/templates") async def get_mermaid_templates(): """Get available Mermaid diagram templates""" return markdown_tools.get_mermaid_templates() @router.get("/text/mermaid/templates/{diagram_type}") async def get_mermaid_template(diagram_type: str): """Get a specific Mermaid template""" template = markdown_tools.get_mermaid_template(diagram_type) if not template: raise HTTPException(status_code=404, detail=f"Template not found: {diagram_type}") return template @router.post("/text/markdown/convert") async def convert_markdown(request: MarkdownConvertRequest): """Convert Markdown to HTML or plain text Output formats: html, plain Themes: github (for HTML) """ result = await markdown_tools.convert_markdown( content=request.content, output_format=request.output_format, theme=request.theme ) return result @router.post("/text/markdown/generate") async def generate_markdown_content(request: MarkdownGenerateRequest): """Generate Markdown content using AI Content types: article, documentation, readme, tutorial, report Length: short, medium, long """ result = await markdown_tools.generate_markdown_with_ai( topic=request.topic, content_type=request.content_type, length=request.length, include_toc=request.include_toc ) return result # ============== UTILITY ENDPOINTS ============== @router.get("/voices") async def get_elevenlabs_voices(): """Get available ElevenLabs voices""" voices = await text_to_speech.get_voices() return voices @router.get("/models/{provider}") async def get_provider_models(provider: str): """Get available models for a provider""" models = { # Image providers "openai": ["gpt-image-1", "dall-e-3", "dall-e-2"], "stable-diffusion": ["sd3-large", "sd3-medium", "sdxl-1.0", "stable-cascade"], "leonardo": ["phoenix-1", "kino-xl", "anime-xl"], "ideogram": ["V_2", "V_2_TURBO"], "flux": ["flux-pro-1.1", "flux-dev", "flux-schnell"], "gemini": ["gemini-2.0-flash-exp"], # Video providers "runway": ["gen3_alpha", "gen3_alpha_turbo", "gen4"], "veo": [ "veo-3.1-generate-preview", "veo-3.1-fast-generate-preview", "veo-3.0-generate-001", "veo-3.0-fast-generate-001", "veo-2.0-generate-001" ], # Upscaling "topaz-image": ["proteus", "artemis", "gaia", "iris", "nyx", "rhea", "theia", "auto"], "topaz-video": ["auto", "proteus", "artemis"], # Audio "elevenlabs": [ "eleven_multilingual_v2", "eleven_flash_v2_5", "eleven_turbo_v2_5", "eleven_v3", "eleven_monolingual_v1" ] } return models.get(provider, []) @router.get("/models") async def get_all_models(): """Get all available models organized by category""" return { "image": { "openai": { "models": ["gpt-image-1", "dall-e-3"], "default": "gpt-image-1", "features": ["quality", "background", "transparent"] }, "stable-diffusion": { "models": ["sd3-large", "sd3-medium", "sdxl-1.0"], "default": "sd3-large", "features": ["negative_prompt", "style_preset", "img2img"] }, "flux": { "models": ["flux-pro-1.1", "flux-dev", "flux-schnell"], "default": "flux-pro-1.1", "features": ["img2img"] } }, "video": { "runway": { "models": ["gen3_alpha", "gen3_alpha_turbo", "gen4"], "default": "gen3_alpha_turbo", "features": ["camera_control", "image_to_video"] }, "veo": { "models": ["veo-3.1-generate-preview", "veo-3.1-fast-generate-preview", "veo-3.0-generate-001"], "default": "veo-3.1-generate-preview", "features": ["audio", "reference_images", "video_extension", "frame_interpolation"] } }, "audio": { "elevenlabs": { "models": ["eleven_multilingual_v2", "eleven_flash_v2_5", "eleven_turbo_v2_5", "eleven_v3"], "default": "eleven_multilingual_v2", "features": ["32_languages", "voice_cloning", "voice_settings"] } } }