Save local changes (GSC/Bing integrations) before merging PR #354

This commit is contained in:
ajaysi
2026-02-13 13:11:27 +05:30
parent 43e66835ac
commit 08a1f4a1d8
144 changed files with 8310 additions and 2748 deletions

View File

@@ -0,0 +1,97 @@
# Brand Avatar API Documentation
## Overview
The Brand Avatar API provides endpoints for generating, varying, and enhancing brand avatars using WaveSpeed AI.
**Base URL**: `/api/onboarding/assets`
## Endpoints
### 1. Generate Avatar
Generate a new brand avatar from a text prompt.
- **URL**: `/generate-avatar`
- **Method**: `POST`
- **Body** (`application/json`):
```json
{
"prompt": "A professional tech entrepreneur, studio lighting",
"style_preset": "Cinematic",
"aspect_ratio": "1:1",
"model": "ideogram-v3-turbo",
"provider": "wavespeed"
}
```
- **Response**:
```json
{
"success": true,
"image_url": "/api/assets/{user_id}/avatars/{filename}.png",
"image_base64": "...",
"asset_id": 123
}
```
### 2. Create Variation
Create a variation of an existing avatar/image.
- **URL**: `/create-variation`
- **Method**: `POST`
- **Content-Type**: `multipart/form-data`
- **Form Data**:
- `prompt` (text): Description of the variation (e.g., "same person but smiling")
- `file` (file): The reference image file
- **Response**:
```json
{
"success": true,
"image_url": "/api/assets/{user_id}/avatars/{filename}.png",
"image_base64": "...",
"asset_id": 124
}
```
### 3. Enhance Avatar
Upscale and enhance an existing avatar image.
- **URL**: `/enhance-avatar`
- **Method**: `POST`
- **Content-Type**: `multipart/form-data`
- **Form Data**:
- `file` (file): The image file to enhance
- **Response**:
```json
{
"success": true,
"image_url": "/api/assets/{user_id}/avatars/{filename}.png",
"image_base64": "...",
"asset_id": 125
}
```
### 4. Enhance Prompt
Optimize a simple prompt into a detailed, high-quality prompt using WaveSpeed.
- **URL**: `/enhance-prompt`
- **Method**: `POST`
- **Body**:
```json
{
"prompt": "man in suit"
}
```
- **Response**:
```json
{
"success": true,
"original_prompt": "man in suit",
"optimized_prompt": "A professional portrait of a man in a tailored navy blue suit, confident expression, studio lighting, 4k resolution..."
}
```
## Providers
- **Default Provider**: `wavespeed`
- **Models**:
- Generation: `ideogram-v3-turbo` (default), `qwen-image`
- Editing/Variation: `qwen-edit-plus` (default)
- Enhancement: `nano-banana-pro-edit-ultra` (4K upscale)

View File

@@ -100,6 +100,8 @@ class OnboardingCompletionService:
except Exception as e:
logger.warning(f"Failed to schedule website analysis task creation for user {user_id}: {e}")
# Schedule onboarding full-site SEO audit (non-blocking) ~10 minutes after completion
try:
from services.database import SessionLocal

View File

@@ -10,22 +10,36 @@ from sqlalchemy.orm import Session
from pydantic import BaseModel
from loguru import logger
from .step4_persona_routes import _extract_user_id
from middleware.auth_middleware import get_current_user
import base64
import os
from pathlib import Path
from utils.file_storage import save_file_safely, generate_unique_filename
from services.database import get_db, WORKSPACE_DIR
from utils.asset_tracker import save_asset_to_library
from models.content_asset_models import ContentAsset, AssetType, AssetSource
from sqlalchemy import desc
from services.llm_providers.main_image_generation import (
generate_image_with_provider,
enhance_image_prompt,
generate_image_variation
generate_image_variation,
generate_image_enhance
)
from services.llm_providers.main_audio_generation import clone_voice, qwen3_voice_clone, cosyvoice_voice_clone, qwen3_voice_design
import asyncio
import random
import string
router = APIRouter()
router = APIRouter(prefix="/onboarding/assets")
# --- Models ---
class VoiceDesignRequest(BaseModel):
user_id: Optional[str] = None
text: str
voice_description: str
language: str = "auto"
class AvatarPromptRequest(BaseModel):
user_id: Optional[str] = None
prompt: str
@@ -34,6 +48,9 @@ class AvatarPromptRequest(BaseModel):
negative_prompt: Optional[str] = None
num_inference_steps: int = 30
guidance_scale: float = 7.5
model: Optional[str] = None
rendering_speed: Optional[str] = None
provider: Optional[str] = None
class AvatarEnhanceRequest(BaseModel):
user_id: Optional[str] = None
@@ -47,14 +64,108 @@ class VoiceCloneRequest(BaseModel):
# --- Routes ---
@router.get("/latest-avatar")
async def get_latest_avatar(
current_user: Dict[str, Any] = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Get the latest generated brand avatar for the user."""
try:
user_id = _extract_user_id(current_user)
# Search for assets that are either:
# 1. Saved with source_module=BRAND_AVATAR_GENERATOR (new)
# 2. Saved with source_module=STORY_WRITER but have metadata category='brand_avatar' (legacy)
# Fetch candidates (limit to recent 20 to avoid performance issues)
candidates = db.query(ContentAsset).filter(
ContentAsset.user_id == user_id,
ContentAsset.asset_type == AssetType.IMAGE,
ContentAsset.source_module.in_([
AssetSource.BRAND_AVATAR_GENERATOR,
AssetSource.STORY_WRITER
])
).order_by(desc(ContentAsset.created_at)).limit(50).all()
asset = None
for candidate in candidates:
# Check for direct match (new assets)
if candidate.source_module == AssetSource.BRAND_AVATAR_GENERATOR:
asset = candidate
break
# Check for legacy match (metadata category)
if candidate.source_module == AssetSource.STORY_WRITER:
meta = candidate.asset_metadata or {}
if meta.get('category') == 'brand_avatar':
asset = candidate
break
if not asset:
return {"success": False, "message": "No avatar found"}
# Fallback to metadata prompt if main column is empty (legacy support)
prompt = asset.prompt
if not prompt and asset.asset_metadata:
prompt = asset.asset_metadata.get('prompt', '')
return {
"success": True,
"image_url": asset.file_url,
"prompt": prompt,
"asset_id": asset.id,
"provider": asset.provider
}
except Exception as e:
logger.error(f"Failed to fetch latest avatar: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/latest-voice-clone")
async def get_latest_voice_clone(
current_user: Dict[str, Any] = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Get the latest generated voice clone for the user."""
try:
user_id = _extract_user_id(current_user)
# Fetch latest voice clone asset
asset = db.query(ContentAsset).filter(
ContentAsset.user_id == user_id,
ContentAsset.asset_type == AssetType.AUDIO,
ContentAsset.source_module == AssetSource.VOICE_CLONER
).order_by(desc(ContentAsset.created_at)).first()
if not asset:
# Try to find legacy assets or assets that might have been saved differently
# For example, voice designs might be saved as VOICE_CLONER too?
# Or check for 'voice_design' logic if needed, but 'voice_cloner' is primary
return {"success": False, "message": "No voice clone found"}
meta = asset.asset_metadata or {}
return {
"success": True,
"custom_voice_id": meta.get("custom_voice_id"),
"preview_audio_url": meta.get("preview_url") or asset.file_url,
"asset_id": asset.id,
"voice_name": meta.get("voice_name"),
"engine": meta.get("engine")
}
except Exception as e:
logger.error(f"Failed to fetch latest voice clone: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/generate-avatar")
async def generate_avatar(
request: AvatarPromptRequest,
current_user: Dict[str, Any] = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Generate a brand avatar using available image providers."""
try:
user_id = _extract_user_id(request.user_id)
user_id = _extract_user_id(current_user)
logger.info(f"Generating avatar for user {user_id} with prompt: {request.prompt}")
@@ -66,6 +177,9 @@ async def generate_avatar(
num_inference_steps=request.num_inference_steps,
guidance_scale=request.guidance_scale,
style_preset=request.style_preset,
model=request.model,
rendering_speed=request.rendering_speed,
provider=request.provider,
user_id=user_id
)
@@ -78,42 +192,66 @@ async def generate_avatar(
image_data = result.get("image_base64")
if not image_data and result.get("image_url"):
# TODO: Download image from URL if needed, or just store URL
pass
try:
import httpx
async with httpx.AsyncClient() as client:
response = await client.get(result["image_url"], timeout=30.0)
response.raise_for_status()
image_data = response.content
except ImportError:
# Fallback to requests if httpx is not installed
import requests
response = requests.get(result["image_url"], timeout=30.0)
response.raise_for_status()
image_data = response.content
except Exception as e:
logger.error(f"Failed to download image from URL: {e}")
raise HTTPException(status_code=500, detail=f"Failed to download generated image: {str(e)}")
if image_data:
# Decode if needed (usually it's already base64 string)
# Save file
filename = generate_unique_filename("avatar", "png")
file_path = save_file_safely(
base64.b64decode(image_data) if isinstance(image_data, str) else image_data,
user_id,
"avatars",
# If image_data is bytes (from URL download), pass it directly
# If it's base64 string (from API), decode it
content_to_save = base64.b64decode(image_data) if isinstance(image_data, str) else image_data
# Construct user assets directory
user_assets_dir = Path(WORKSPACE_DIR) / f"workspace_{user_id}" / "assets" / "avatars"
saved_path, error = save_file_safely(
content_to_save,
user_assets_dir,
filename
)
if error or not saved_path:
raise HTTPException(status_code=500, detail=f"Failed to save image file: {error}")
# Construct public URL
image_url = f"/api/assets/{user_id}/avatars/{filename}"
# Save to Asset Library
asset_id = save_asset_to_library(
db=db,
user_id=user_id,
file_path=file_path,
asset_type="image",
category="brand_avatar",
meta_data={
"prompt": request.prompt,
source_module="brand_avatar_generator",
filename=filename,
file_url=image_url,
file_path=str(saved_path),
prompt=request.prompt,
asset_metadata={
"provider": result.get("provider", "unknown"),
"style": request.style_preset
"style": request.style_preset,
"category": "brand_avatar"
}
)
# Construct public URL (this depends on your static file serving setup)
# Assuming /api/assets/{user_id}/avatars/{filename}
image_url = f"/api/assets/{user_id}/avatars/{filename}"
return {
"success": True,
"image_url": image_url,
"image_base64": image_data, # Optional: return base64 for immediate display
"image_base64": image_data if isinstance(image_data, str) else base64.b64encode(image_data).decode('utf-8'),
"asset_id": asset_id
}
@@ -126,14 +264,15 @@ async def generate_avatar(
@router.post("/enhance-prompt")
async def enhance_prompt_route(
request: AvatarEnhanceRequest
request: AvatarEnhanceRequest,
current_user: Dict[str, Any] = Depends(get_current_user)
):
"""Enhance a simple prompt into a detailed midjourney-style prompt."""
try:
user_id = _extract_user_id(request.user_id)
user_id = _extract_user_id(current_user)
logger.info(f"Enhancing prompt for user {user_id}: {request.prompt}")
enhanced_prompt = await enhance_image_prompt(request.prompt)
enhanced_prompt = await enhance_image_prompt(request.prompt, user_id=user_id)
return {
"success": True,
@@ -145,52 +284,347 @@ async def enhance_prompt_route(
raise HTTPException(status_code=500, detail=str(e))
@router.post("/create-variation")
async def create_variation_route(
prompt: str = Form(...),
file: UploadFile = File(...),
user_id: Optional[str] = Form(None), # Ignored in favor of authenticated user
current_user: Dict[str, Any] = Depends(get_current_user)
):
"""Generate a variation of an existing avatar."""
try:
user_id = _extract_user_id(current_user)
logger.info(f"Creating variation for user {user_id} with prompt: {prompt}")
# Read file
file_content = await file.read()
result = await generate_image_variation(
image=file_content,
prompt=prompt,
user_id=user_id
)
if not result.get("success"):
raise HTTPException(status_code=500, detail=result.get("error", "Variation generation failed"))
# Save result
image_data = result.get("image_base64")
if image_data:
filename = generate_unique_filename("avatar_variation", "png")
content_to_save = base64.b64decode(image_data)
# Construct user assets directory
user_assets_dir = Path(WORKSPACE_DIR) / f"workspace_{user_id}" / "assets" / "avatars"
saved_path, error = save_file_safely(
content_to_save,
user_assets_dir,
filename
)
if error or not saved_path:
raise HTTPException(status_code=500, detail=f"Failed to save variation file: {error}")
# Construct public URL
image_url = f"/api/assets/{user_id}/avatars/{filename}"
# Save to Asset Library
asset_id = save_asset_to_library(
db=next(get_db()),
user_id=user_id,
asset_type="image",
source_module="brand_avatar_variation",
filename=filename,
file_url=image_url,
file_path=str(saved_path),
asset_metadata={
"prompt": prompt,
"provider": "wavespeed",
"original_filename": file.filename,
"category": "brand_avatar_variation"
}
)
return {
"success": True,
"image_url": image_url,
"image_base64": image_data,
"asset_id": asset_id
}
return {"success": False, "error": "No image data returned"}
except Exception as e:
logger.error(f"Variation generation failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/enhance-avatar")
async def enhance_avatar_route(
file: UploadFile = File(...),
user_id: Optional[str] = Form(None), # Ignored in favor of authenticated user
current_user: Dict[str, Any] = Depends(get_current_user)
):
"""Enhance/Upscale an existing avatar."""
try:
user_id = _extract_user_id(current_user)
logger.info(f"Enhancing avatar for user {user_id}")
# Read file
file_content = await file.read()
result = await generate_image_enhance(
image=file_content,
user_id=user_id
)
if not result.get("success"):
raise HTTPException(status_code=500, detail=result.get("error", "Enhancement failed"))
# Save result
image_data = result.get("image_base64")
if image_data:
filename = generate_unique_filename("avatar_enhanced", "png")
content_to_save = base64.b64decode(image_data)
# Construct user assets directory
user_assets_dir = Path(WORKSPACE_DIR) / f"workspace_{user_id}" / "assets" / "avatars"
saved_path, error = save_file_safely(
content_to_save,
user_assets_dir,
filename
)
if error or not saved_path:
raise HTTPException(status_code=500, detail=f"Failed to save enhanced file: {error}")
# Construct public URL
image_url = f"/api/assets/{user_id}/avatars/{filename}"
# Save to Asset Library
asset_id = save_asset_to_library(
db=next(get_db()),
user_id=user_id,
asset_type="image",
source_module="brand_avatar_enhancer",
filename=filename,
file_url=image_url,
file_path=str(saved_path),
asset_metadata={
"provider": "wavespeed",
"category": "brand_avatar_enhanced",
"original_filename": file.filename
}
)
return {
"success": True,
"image_url": image_url,
"image_base64": image_data,
"asset_id": asset_id
}
return {"success": False, "error": "No image data returned"}
except Exception as e:
logger.error(f"Avatar enhancement failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/create-voice-clone")
async def create_voice_clone(
voice_name: str = Form(...),
description: str = Form(None),
engine: str = Form("qwen3"),
file: UploadFile = File(...),
user_id: Optional[str] = Form(None),
user_id: Optional[str] = Form(None), # Ignored in favor of authenticated user
current_user: Dict[str, Any] = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Create a voice clone from an audio file."""
try:
user_id = _extract_user_id(user_id)
logger.info(f"Creating voice clone '{voice_name}' for user {user_id}")
user_id = _extract_user_id(current_user)
logger.info(f"Creating voice clone '{voice_name}' (engine={engine}) for user {user_id}")
# 1. Save uploaded audio file
file_content = await file.read()
filename = generate_unique_filename("voice_sample", Path(file.filename).suffix.lstrip("."))
file_path = save_file_safely(file_content, user_id, "voice_samples", filename)
# 2. Call Voice Cloning API (Placeholder for actual implementation)
# TODO: Integrate with Minimax or CosyVoice API
# For now, we simulate success
user_voice_dir = Path(WORKSPACE_DIR) / f"workspace_{user_id}" / "assets" / "voice_samples"
saved_path, error = save_file_safely(file_content, user_voice_dir, filename)
# 3. Save to Asset Library
if error or not saved_path:
raise HTTPException(status_code=500, detail=f"Failed to save voice sample: {error}")
file_path = str(saved_path)
# 2. Call Voice Cloning API
preview_audio_bytes = None
custom_voice_id = None
loop = asyncio.get_event_loop()
# Default preview text
preview_text = "Hello! This is a preview of my cloned voice using AI technology. I hope you like it!"
if engine.lower() == "minimax":
# Generate valid voice ID for Minimax (alphanumeric, starts with letter, 8+ chars)
random_suffix = ''.join(random.choices(string.ascii_letters + string.digits, k=8))
custom_voice_id = f"vc_{random_suffix}"
logger.info(f"Cloning voice with Minimax, ID: {custom_voice_id}")
# Run blocking call in executor
result = await loop.run_in_executor(
None,
lambda: clone_voice(
audio_bytes=file_content,
custom_voice_id=custom_voice_id,
text=preview_text,
user_id=user_id
)
)
preview_audio_bytes = result.preview_audio_bytes
elif engine.lower() == "cosyvoice":
logger.info("Cloning voice with CosyVoice")
result = await loop.run_in_executor(
None,
lambda: cosyvoice_voice_clone(
audio_bytes=file_content,
text=preview_text,
user_id=user_id
)
)
preview_audio_bytes = result.preview_audio_bytes
# CosyVoice doesn't persist ID on provider side, but we need one for DB
asset_uuid = ''.join(random.choices(string.ascii_letters + string.digits, k=8))
custom_voice_id = f"vc_cosy_{asset_uuid}"
else: # qwen3 (default)
logger.info("Cloning voice with Qwen3")
result = await loop.run_in_executor(
None,
lambda: qwen3_voice_clone(
audio_bytes=file_content,
text=preview_text,
user_id=user_id
)
)
preview_audio_bytes = result.preview_audio_bytes
# Qwen3 doesn't persist ID on provider side
asset_uuid = ''.join(random.choices(string.ascii_letters + string.digits, k=8))
custom_voice_id = f"vc_qwen_{asset_uuid}"
# 3. Save Preview Audio (if generated)
preview_url = None
if preview_audio_bytes:
preview_filename = f"preview_{filename}"
# Ensure it ends with .wav
if not preview_filename.endswith(".wav"):
preview_filename = str(Path(preview_filename).with_suffix('.wav'))
user_voice_dir = Path(WORKSPACE_DIR) / f"workspace_{user_id}" / "assets" / "voice_samples"
saved_preview_path, error = save_file_safely(preview_audio_bytes, user_voice_dir, preview_filename)
if not error and saved_preview_path:
preview_url = f"/api/assets/{user_id}/voice_samples/{preview_filename}"
# 4. Save to Asset Library
asset_id = save_asset_to_library(
db=db,
user_id=user_id,
file_path=file_path,
asset_type="audio",
category="voice_clone",
meta_data={
source_module="voice_cloner",
filename=filename,
file_url=f"/api/assets/{user_id}/voice_samples/{filename}",
asset_metadata={
"voice_name": voice_name,
"engine": engine,
"description": description,
"original_filename": file.filename
"original_filename": file.filename,
"custom_voice_id": custom_voice_id,
"preview_url": preview_url,
"category": "voice_clone"
}
)
return {
"success": True,
"custom_voice_id": f"vc_{asset_id}", # Mock ID
"preview_audio_url": f"/api/assets/{user_id}/voice_samples/{filename}",
"custom_voice_id": custom_voice_id,
"preview_audio_url": preview_url or f"/api/assets/{user_id}/voice_samples/{filename}",
"asset_id": asset_id,
"message": "Voice clone created successfully (simulated)"
"message": "Voice clone created successfully"
}
except Exception as e:
logger.error(f"Voice cloning failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/create-voice-design")
async def create_voice_design(
request: VoiceDesignRequest,
current_user: Dict[str, Any] = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Create a voice from text description (Voice Design)."""
try:
user_id = _extract_user_id(current_user)
logger.info(f"Designing voice for user {user_id}")
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(
None,
lambda: qwen3_voice_design(
text=request.text,
voice_description=request.voice_description,
language=request.language,
user_id=user_id
)
)
# Save the result to a temporary file
filename = generate_unique_filename("voice_design_preview", "wav")
user_voice_dir = Path(WORKSPACE_DIR) / f"workspace_{user_id}" / "assets" / "voice_samples"
saved_path, error = save_file_safely(result.preview_audio_bytes, user_voice_dir, filename)
if error or not saved_path:
raise HTTPException(status_code=500, detail=f"Failed to save voice design: {error}")
# Generate URL
preview_url = f"/api/assets/{user_id}/voice_samples/{filename}"
# Save to Asset Library
asset_id = save_asset_to_library(
db=db,
user_id=user_id,
file_path=str(saved_path),
asset_type="audio",
source_module="voice_cloner",
filename=filename,
file_url=preview_url,
asset_metadata={
"voice_description": request.voice_description,
"text": request.text,
"language": request.language,
"engine": "qwen3-design",
"category": "voice_design",
"preview_url": preview_url
}
)
return {
"success": True,
"preview_audio_url": preview_url,
"asset_id": asset_id,
"message": "Voice generated successfully"
}
except Exception as e:
logger.error(f"Voice design failed: {e}")
raise HTTPException(status_code=500, detail=str(e))