AI Researcher and Video Studio implementation complete

This commit is contained in:
ajaysi
2026-01-05 15:49:51 +05:30
parent b134e9dc7e
commit 0b63ae7fc1
200 changed files with 39535 additions and 1375 deletions

View File

@@ -6,6 +6,8 @@ from .edit_service import EditStudioService, EditStudioRequest
from .upscale_service import UpscaleStudioService, UpscaleStudioRequest
from .control_service import ControlStudioService, ControlStudioRequest
from .social_optimizer_service import SocialOptimizerService, SocialOptimizerRequest
from .compression_service import ImageCompressionService, CompressionRequest, CompressionResult
from .format_converter_service import ImageFormatConverterService, FormatConversionRequest, FormatConversionResult
from .transform_service import (
TransformStudioService,
TransformImageToVideoRequest,
@@ -25,6 +27,12 @@ __all__ = [
"ControlStudioRequest",
"SocialOptimizerService",
"SocialOptimizerRequest",
"ImageCompressionService",
"CompressionRequest",
"CompressionResult",
"ImageFormatConverterService",
"FormatConversionRequest",
"FormatConversionResult",
"TransformStudioService",
"TransformImageToVideoRequest",
"TalkingAvatarRequest",

View File

@@ -0,0 +1,367 @@
"""Image Compression Service for optimizing image file sizes."""
from __future__ import annotations
import base64
import io
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Literal
from PIL import Image, ExifTags
from utils.logger_utils import get_service_logger
logger = get_service_logger("image_studio.compression")
@dataclass
class CompressionRequest:
"""Request model for image compression."""
image_base64: str
quality: int = 85 # 1-100, where 100 is best quality
format: str = "jpeg" # jpeg, png, webp, avif
target_size_kb: Optional[int] = None # Target file size in KB
strip_metadata: bool = True
progressive: bool = True # Progressive JPEG
optimize: bool = True # Optimize encoding
@dataclass
class CompressionResult:
"""Result of compression operation."""
success: bool
image_base64: str
original_size_kb: float
compressed_size_kb: float
compression_ratio: float
format: str
width: int
height: int
quality_used: int
metadata_stripped: bool
class ImageCompressionService:
"""Service for image compression and optimization."""
SUPPORTED_FORMATS = ["jpeg", "jpg", "png", "webp"]
# Format-specific options
FORMAT_OPTIONS = {
"jpeg": {"quality": (1, 100), "progressive": True, "optimize": True},
"jpg": {"quality": (1, 100), "progressive": True, "optimize": True},
"png": {"compress_level": (0, 9), "optimize": True},
"webp": {"quality": (1, 100), "lossless": False},
}
def __init__(self):
logger.info("[Compression] ImageCompressionService initialized")
def _decode_image(self, image_base64: str) -> tuple[Image.Image, int]:
"""Decode base64 image and return PIL Image and original size."""
# Handle data URL format
if "," in image_base64:
image_base64 = image_base64.split(",", 1)[1]
image_bytes = base64.b64decode(image_base64)
original_size = len(image_bytes)
image = Image.open(io.BytesIO(image_bytes))
return image, original_size
def _strip_exif(self, image: Image.Image) -> Image.Image:
"""Remove EXIF metadata from image."""
# Create a new image without EXIF data
data = list(image.getdata())
image_without_exif = Image.new(image.mode, image.size)
image_without_exif.putdata(data)
return image_without_exif
def _compress_to_target_size(
self,
image: Image.Image,
target_size_kb: int,
format: str,
min_quality: int = 10,
max_quality: int = 95,
) -> tuple[bytes, int]:
"""Compress image to target file size using binary search."""
target_bytes = target_size_kb * 1024
low, high = min_quality, max_quality
best_result = None
best_quality = max_quality
while low <= high:
mid = (low + high) // 2
compressed = self._compress_image(image, format, mid, True, True)
if len(compressed) <= target_bytes:
best_result = compressed
best_quality = mid
low = mid + 1 # Try higher quality
else:
high = mid - 1 # Try lower quality
if best_result is None:
# Even minimum quality exceeds target, return min quality result
best_result = self._compress_image(image, format, min_quality, True, True)
best_quality = min_quality
return best_result, best_quality
def _compress_image(
self,
image: Image.Image,
format: str,
quality: int,
progressive: bool,
optimize: bool,
) -> bytes:
"""Compress image with given settings."""
buffer = io.BytesIO()
# Handle format-specific options
save_kwargs: Dict[str, Any] = {}
format_lower = format.lower()
if format_lower in ["jpeg", "jpg"]:
# Convert to RGB if necessary (JPEG doesn't support alpha)
if image.mode in ("RGBA", "P"):
image = image.convert("RGB")
save_kwargs["format"] = "JPEG"
save_kwargs["quality"] = quality
save_kwargs["optimize"] = optimize
if progressive:
save_kwargs["progressive"] = True
elif format_lower == "png":
save_kwargs["format"] = "PNG"
save_kwargs["optimize"] = optimize
# PNG uses compress_level (0-9) instead of quality
compress_level = max(0, min(9, (100 - quality) // 11))
save_kwargs["compress_level"] = compress_level
elif format_lower == "webp":
save_kwargs["format"] = "WEBP"
save_kwargs["quality"] = quality
save_kwargs["method"] = 6 # Best compression
else:
raise ValueError(f"Unsupported format: {format}")
image.save(buffer, **save_kwargs)
return buffer.getvalue()
async def compress(
self,
request: CompressionRequest,
user_id: Optional[str] = None,
) -> CompressionResult:
"""Compress an image with specified settings."""
logger.info(f"[Compression] Processing compression request for user: {user_id}")
try:
# Decode image
image, original_size = self._decode_image(request.image_base64)
original_size_kb = original_size / 1024
logger.info(f"[Compression] Original size: {original_size_kb:.2f} KB, dimensions: {image.size}")
# Strip metadata if requested
if request.strip_metadata:
image = self._strip_exif(image)
# Validate format
format_lower = request.format.lower()
if format_lower not in self.SUPPORTED_FORMATS:
raise ValueError(f"Unsupported format: {request.format}. Supported: {self.SUPPORTED_FORMATS}")
# Compress to target size or with quality setting
if request.target_size_kb:
compressed_bytes, quality_used = self._compress_to_target_size(
image,
request.target_size_kb,
format_lower,
)
else:
compressed_bytes = self._compress_image(
image,
format_lower,
request.quality,
request.progressive,
request.optimize,
)
quality_used = request.quality
compressed_size_kb = len(compressed_bytes) / 1024
compression_ratio = (1 - compressed_size_kb / original_size_kb) * 100 if original_size_kb > 0 else 0
# Encode result
mime_type = "image/jpeg" if format_lower in ["jpeg", "jpg"] else f"image/{format_lower}"
result_base64 = f"data:{mime_type};base64,{base64.b64encode(compressed_bytes).decode()}"
logger.info(f"[Compression] Compressed: {original_size_kb:.2f}KB → {compressed_size_kb:.2f}KB ({compression_ratio:.1f}% reduction)")
return CompressionResult(
success=True,
image_base64=result_base64,
original_size_kb=round(original_size_kb, 2),
compressed_size_kb=round(compressed_size_kb, 2),
compression_ratio=round(compression_ratio, 2),
format=format_lower,
width=image.width,
height=image.height,
quality_used=quality_used,
metadata_stripped=request.strip_metadata,
)
except Exception as e:
logger.error(f"[Compression] Failed to compress image: {e}")
raise
async def compress_batch(
self,
requests: List[CompressionRequest],
user_id: Optional[str] = None,
) -> List[CompressionResult]:
"""Compress multiple images with same or individual settings."""
logger.info(f"[Compression] Processing batch of {len(requests)} images for user: {user_id}")
results = []
for i, request in enumerate(requests):
try:
result = await self.compress(request, user_id)
results.append(result)
logger.info(f"[Compression] Batch item {i+1}/{len(requests)} complete")
except Exception as e:
logger.error(f"[Compression] Batch item {i+1} failed: {e}")
# Return partial success
results.append(CompressionResult(
success=False,
image_base64="",
original_size_kb=0,
compressed_size_kb=0,
compression_ratio=0,
format="",
width=0,
height=0,
quality_used=0,
metadata_stripped=False,
))
return results
async def estimate_compression(
self,
image_base64: str,
format: str = "jpeg",
quality: int = 85,
) -> Dict[str, Any]:
"""Estimate compression results without actually compressing."""
try:
image, original_size = self._decode_image(image_base64)
original_size_kb = original_size / 1024
# Quick estimation based on format and quality
if format.lower() in ["jpeg", "jpg"]:
# JPEG compression ratio estimate
estimated_ratio = 0.1 + (quality / 100) * 0.4 # 10-50% of original
elif format.lower() == "webp":
# WebP is typically 25-34% smaller than JPEG
estimated_ratio = 0.08 + (quality / 100) * 0.35
else: # PNG
estimated_ratio = 0.7 + (quality / 100) * 0.2 # PNG is less compressible
estimated_size_kb = original_size_kb * estimated_ratio
return {
"original_size_kb": round(original_size_kb, 2),
"estimated_size_kb": round(estimated_size_kb, 2),
"estimated_reduction_percent": round((1 - estimated_ratio) * 100, 1),
"width": image.width,
"height": image.height,
"format": format.lower(),
}
except Exception as e:
logger.error(f"[Compression] Estimation failed: {e}")
raise
def get_supported_formats(self) -> List[Dict[str, Any]]:
"""Get list of supported compression formats with details."""
return [
{
"id": "jpeg",
"name": "JPEG",
"extension": ".jpg",
"description": "Best for photos. Lossy compression with excellent size reduction.",
"supports_transparency": False,
"quality_range": [1, 100],
"recommended_quality": 85,
"use_cases": ["Photos", "Blog images", "Email", "Social media"],
},
{
"id": "png",
"name": "PNG",
"extension": ".png",
"description": "Best for graphics with transparency. Lossless compression.",
"supports_transparency": True,
"quality_range": [1, 100],
"recommended_quality": 90,
"use_cases": ["Logos", "Icons", "Graphics", "Screenshots"],
},
{
"id": "webp",
"name": "WebP",
"extension": ".webp",
"description": "Modern format with excellent compression. 25-34% smaller than JPEG.",
"supports_transparency": True,
"quality_range": [1, 100],
"recommended_quality": 80,
"use_cases": ["Web images", "Fast loading", "Modern browsers"],
},
]
def get_presets(self) -> List[Dict[str, Any]]:
"""Get compression presets for common use cases."""
return [
{
"id": "web",
"name": "Web Optimized",
"description": "Balanced quality and size for web pages",
"format": "webp",
"quality": 80,
"strip_metadata": True,
},
{
"id": "email",
"name": "Email Friendly",
"description": "Small file size for email attachments (<200KB target)",
"format": "jpeg",
"quality": 70,
"target_size_kb": 200,
"strip_metadata": True,
},
{
"id": "social",
"name": "Social Media",
"description": "Optimized for social platforms",
"format": "jpeg",
"quality": 85,
"strip_metadata": True,
},
{
"id": "high_quality",
"name": "High Quality",
"description": "Minimal compression for quality-critical images",
"format": "png",
"quality": 95,
"strip_metadata": False,
},
{
"id": "maximum",
"name": "Maximum Compression",
"description": "Smallest possible file size",
"format": "webp",
"quality": 60,
"strip_metadata": True,
},
]

View File

@@ -1,17 +1,10 @@
"""Create Studio service for AI-powered image generation."""
import os
from typing import Optional, Dict, Any, List, Literal
from dataclasses import dataclass
from services.llm_providers.image_generation import (
ImageGenerationOptions,
ImageGenerationResult,
HuggingFaceImageProvider,
GeminiImageProvider,
StabilityImageProvider,
WaveSpeedImageProvider,
)
from services.llm_providers.main_image_generation import generate_image
from services.llm_providers.image_generation import ImageGenerationResult
from .templates import TemplateManager, ImageTemplate, Platform, TemplateCategory
from utils.logger_utils import get_service_logger
@@ -75,29 +68,8 @@ class CreateStudioService:
self.template_manager = TemplateManager()
logger.info("[Create Studio] Initialized with template manager")
def _get_provider_instance(self, provider_name: str, api_key: Optional[str] = None):
"""Get provider instance by name.
Args:
provider_name: Name of the provider
api_key: Optional API key (uses env vars if not provided)
Returns:
Provider instance
Raises:
ValueError: If provider is not supported
"""
if provider_name == "stability":
return StabilityImageProvider(api_key=api_key or os.getenv("STABILITY_API_KEY"))
elif provider_name == "wavespeed":
return WaveSpeedImageProvider(api_key=api_key or os.getenv("WAVESPEED_API_KEY"))
elif provider_name == "huggingface":
return HuggingFaceImageProvider(api_token=api_key or os.getenv("HF_API_KEY"))
elif provider_name == "gemini":
return GeminiImageProvider(api_key=api_key or os.getenv("GEMINI_API_KEY"))
else:
raise ValueError(f"Unsupported provider: {provider_name}")
# Removed _get_provider_instance() - now using unified entry point
# Provider selection is handled by main_image_generation.generate_image()
def _select_provider_and_model(
self,
@@ -289,30 +261,17 @@ class CreateStudioService:
logger.info("[Create Studio] Starting generation: prompt=%s, template=%s",
request.prompt[:100], request.template_id)
# Pre-flight validation: Check subscription and usage limits
if user_id:
from services.database import get_db
from services.subscription import PricingService
from services.subscription.preflight_validator import validate_image_generation_operations
from fastapi import HTTPException
db = next(get_db())
try:
pricing_service = PricingService(db)
logger.info(f"[Create Studio] 🛂 Running pre-flight validation for user {user_id}")
validate_image_generation_operations(
pricing_service=pricing_service,
user_id=user_id,
num_images=request.num_variations
)
logger.info(f"[Create Studio] ✅ Pre-flight validation passed - proceeding with generation")
except HTTPException as http_ex:
logger.error(f"[Create Studio] ❌ Pre-flight validation failed - blocking generation")
raise
finally:
db.close()
else:
logger.warning("[Create Studio] ⚠️ No user_id provided - skipping pre-flight validation")
# Pre-flight validation: Reuse unified helper
# Note: Validation for num_variations will be done per-image in generate_image()
# We validate once upfront to fail fast if user has no credits
if user_id and request.num_variations > 0:
from services.llm_providers.main_image_generation import _validate_image_operation
_validate_image_operation(
user_id=user_id,
operation_type="create-studio-generation",
num_operations=request.num_variations,
log_prefix="[Create Studio]"
)
# Load template if specified
template = None
@@ -337,36 +296,37 @@ class CreateStudioService:
# Select provider and model
provider_name, model = self._select_provider_and_model(request, template)
# Get provider instance
try:
provider = self._get_provider_instance(provider_name)
except Exception as e:
logger.error("[Create Studio] ❌ Failed to initialize provider %s: %s",
provider_name, str(e))
raise RuntimeError(f"Provider initialization failed: {str(e)}")
# Generate images
# Generate images using unified entry point
# This ensures consistent validation, tracking, and error handling
results = []
for i in range(request.num_variations):
logger.info("[Create Studio] Generating variation %d/%d",
i + 1, request.num_variations)
try:
# Prepare options
options = ImageGenerationOptions(
prompt=prompt,
negative_prompt=request.negative_prompt,
width=width,
height=height,
guidance_scale=request.guidance_scale,
steps=request.steps,
seed=request.seed + i if request.seed else None,
model=model,
extra={"style_preset": request.style_preset} if request.style_preset else {}
)
# Prepare options for unified entry point
options = {
"provider": provider_name,
"model": model,
"width": width,
"height": height,
"negative_prompt": request.negative_prompt,
"guidance_scale": request.guidance_scale,
"steps": request.steps,
"seed": request.seed + i if request.seed else None,
}
# Generate image
result: ImageGenerationResult = provider.generate(options)
# Add style preset to extra if specified
if request.style_preset:
options["extra"] = {"style_preset": request.style_preset}
# Generate image using unified entry point
# This handles validation, provider selection, generation, and tracking automatically
result: ImageGenerationResult = generate_image(
prompt=prompt,
options=options,
user_id=user_id
)
results.append({
"image_bytes": result.image_bytes,

View File

@@ -11,6 +11,7 @@ from typing import Any, Dict, Literal, Optional
from PIL import Image
from services.llm_providers.main_image_editing import edit_image as huggingface_edit_image
from services.llm_providers.main_image_generation import generate_image_edit
from services.stability_service import StabilityAIService
from utils.logger_utils import get_service_logger
@@ -213,6 +214,249 @@ class EditStudioService:
def list_operations(self) -> Dict[str, Dict[str, Any]]:
"""Expose supported operations for UI rendering."""
return self.SUPPORTED_OPERATIONS
def get_available_models(
self,
operation: Optional[str] = None,
tier: Optional[str] = None,
) -> Dict[str, Any]:
"""Get available WaveSpeed editing models.
Args:
operation: Filter by operation type (e.g., "general_edit")
tier: Filter by tier ("budget", "mid", "premium")
Returns:
Dictionary with models and metadata
"""
from services.llm_providers.image_generation.wavespeed_edit_provider import WaveSpeedEditProvider
provider = WaveSpeedEditProvider()
all_models = provider.get_available_models()
# Filter by operation if specified
if operation:
filtered = provider.get_models_by_operation(operation)
all_models = {k: v for k, v in all_models.items() if k in filtered}
# Filter by tier if specified
if tier:
filtered = provider.get_models_by_tier(tier)
all_models = {k: v for k, v in all_models.items() if k in filtered}
# Format for API response
models_list = []
for model_id, model_info in all_models.items():
models_list.append({
"id": model_id,
"name": model_info.get("name", model_id),
"description": model_info.get("description", ""),
"cost": model_info.get("cost", 0.02),
"cost_8k": model_info.get("cost_8k"), # Optional
"tier": model_info.get("tier", "mid"),
"max_resolution": model_info.get("max_resolution", [2048, 2048]),
"capabilities": model_info.get("capabilities", []),
"use_cases": self._get_use_cases_for_model(model_id, model_info),
"features": self._get_features_for_model(model_info),
"supports_multi_image": model_info.get("supports_multi_image", False),
"supports_controlnet": model_info.get("supports_controlnet", False),
"languages": model_info.get("languages", ["en"]),
})
return {
"models": models_list,
"total": len(models_list),
}
def recommend_model(
self,
operation: str,
image_resolution: Optional[Dict[str, int]] = None,
user_tier: Optional[str] = None,
preferences: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Recommend best model for given operation and context.
Args:
operation: Operation type (e.g., "general_edit")
image_resolution: Dict with "width" and "height"
user_tier: User subscription tier ("free", "pro", "enterprise")
preferences: Dict with "prioritize_cost" or "prioritize_quality"
Returns:
Dictionary with recommended model and alternatives
"""
from services.llm_providers.image_generation.wavespeed_edit_provider import WaveSpeedEditProvider
provider = WaveSpeedEditProvider()
available_models = provider.get_models_by_operation(operation)
if not available_models:
# Fallback to all models if operation doesn't match
available_models = provider.get_available_models()
# Filter by resolution if provided
if image_resolution:
width = image_resolution.get("width", 0)
height = image_resolution.get("height", 0)
max_dimension = max(width, height)
# Filter models that support this resolution
filtered = {}
for model_id, model_info in available_models.items():
max_res = model_info.get("max_resolution", (2048, 2048))
max_supported = max(max_res[0], max_res[1])
if max_dimension <= max_supported:
filtered[model_id] = model_info
available_models = filtered
if not available_models:
# No models match, return first available
all_models = provider.get_available_models()
if all_models:
first_model_id = list(all_models.keys())[0]
return {
"recommended_model": first_model_id,
"reason": "No specific match found, using default model",
"alternatives": [],
}
else:
raise ValueError("No models available")
# Apply preferences
prioritize_cost = preferences and preferences.get("prioritize_cost", False)
prioritize_quality = preferences and preferences.get("prioritize_quality", False)
# Score models
scored_models = []
for model_id, model_info in available_models.items():
score = 0
cost = model_info.get("cost", 0.02)
tier = model_info.get("tier", "mid")
max_res = model_info.get("max_resolution", (2048, 2048))
max_resolution = max(max_res[0], max_res[1])
# Cost scoring (lower is better)
if prioritize_cost:
score += (1.0 / cost) * 100 # Invert cost for scoring
else:
score += (1.0 / cost) * 50 # Less weight if not prioritizing
# Quality scoring (higher resolution = better)
if prioritize_quality:
score += max_resolution / 10 # Higher weight for quality
else:
score += max_resolution / 20 # Lower weight
# Tier preference based on user tier
if user_tier == "free":
if tier == "budget":
score += 50
elif tier == "mid":
score += 20
elif user_tier in ["pro", "enterprise"]:
if tier == "premium":
score += 50
elif tier == "mid":
score += 30
scored_models.append((model_id, model_info, score))
# Sort by score (highest first)
scored_models.sort(key=lambda x: x[2], reverse=True)
# Get recommended model
recommended_id, recommended_info, recommended_score = scored_models[0]
# Build reason
reasons = []
if prioritize_cost:
reasons.append("Lowest cost option")
if prioritize_quality:
reasons.append("Best quality")
if image_resolution:
reasons.append(f"Supports {image_resolution.get('width')}×{image_resolution.get('height')} resolution")
if user_tier == "free" and recommended_info.get("tier") == "budget":
reasons.append("Budget-friendly for free tier")
reason = ", ".join(reasons) if reasons else "Best match for your requirements"
# Get alternatives (top 2-3)
alternatives = []
for model_id, model_info, score in scored_models[1:4]:
alt_reason = f"Alternative: {model_info.get('tier', 'mid').title()} tier"
if model_info.get("cost", 0) < recommended_info.get("cost", 0):
alt_reason += ", lower cost"
elif model_info.get("cost", 0) > recommended_info.get("cost", 0):
alt_reason += ", higher quality"
alternatives.append({
"model_id": model_id,
"name": model_info.get("name", model_id),
"cost": model_info.get("cost", 0.02),
"reason": alt_reason,
})
return {
"recommended_model": recommended_id,
"reason": reason,
"alternatives": alternatives,
}
def _get_use_cases_for_model(self, model_id: str, model_info: Dict[str, Any]) -> list:
"""Get use cases for a model based on its capabilities."""
use_cases_map = {
"general_edit": ["Quick edits", "Style changes", "Background replacement"],
"style_transfer": ["Apply artistic styles", "Style transformations"],
"text_edit": ["Add text to images", "Edit text in images"],
"multi_image": ["Batch editing", "Consistent character work"],
"high_res": ["Professional work", "Print materials", "4K/8K editing"],
"professional": ["Marketing campaigns", "Brand assets"],
"typography": ["Text-heavy edits", "Typography generation"],
"portrait_retouching": ["Portrait edits", "Beauty retouching"],
"fashion_edit": ["Fashion photography", "Outfit changes"],
"product_edit": ["E-commerce", "Product photography"],
}
capabilities = model_info.get("capabilities", [])
use_cases = []
for cap in capabilities:
if cap in use_cases_map:
use_cases.extend(use_cases_map[cap])
# Remove duplicates
return list(set(use_cases)) if use_cases else ["General image editing"]
def _get_features_for_model(self, model_info: Dict[str, Any]) -> list:
"""Get feature list for a model."""
features = []
if model_info.get("supports_multi_image"):
max_images = model_info.get("api_params", {}).get("max_images", 0)
if max_images:
features.append(f"Multi-image ({max_images} images)")
else:
features.append("Multi-image support")
if model_info.get("supports_controlnet"):
features.append("ControlNet support")
languages = model_info.get("languages", [])
if len(languages) > 1:
features.append(f"Multilingual ({', '.join(languages)})")
elif "multilingual" in languages:
features.append("Multilingual support")
max_res = model_info.get("max_resolution", (2048, 2048))
if max(max_res) >= 4096:
features.append("4K/8K support")
elif max(max_res) >= 2048:
features.append("2K support")
api_params = model_info.get("api_params", {})
if api_params.get("supports_guidance_scale"):
features.append("Guidance scale control")
return features if features else ["Standard editing"]
async def process_edit(
self,
@@ -221,6 +465,9 @@ class EditStudioService:
) -> Dict[str, Any]:
"""Process edit request and return normalized response."""
# Pre-flight validation: Use specific validator for editing operations
# Note: Editing uses validate_image_editing_operations (different from generation)
# This is intentional as editing may have different subscription limits
if user_id:
from services.database import get_db
from services.subscription import PricingService
@@ -386,29 +633,109 @@ class EditStudioService:
mask_bytes: Optional[bytes],
user_id: Optional[str],
) -> bytes:
"""Execute Hugging Face powered general editing (synchronous API)."""
"""Execute general editing - routes to WaveSpeed (unified entry) or HuggingFace (legacy).
If model is a WaveSpeed model (qwen-edit-plus, nano-banana-pro-edit-ultra, seedream-v4.5-edit),
uses unified entry point. Otherwise falls back to HuggingFace for backward compatibility.
"""
if not request.prompt:
raise ValueError("Prompt is required for general edits")
options = {
"provider": request.provider or "huggingface",
"model": request.model,
"guidance_scale": request.guidance_scale,
"steps": request.steps,
"seed": request.seed,
}
# huggingface edit is synchronous - run in thread
result = await asyncio.to_thread(
huggingface_edit_image,
image_bytes,
request.prompt,
options,
user_id,
mask_bytes, # Optional mask for selective editing
# Check if model is a WaveSpeed editing model
from services.llm_providers.image_generation.wavespeed_edit_provider import WaveSpeedEditProvider
provider = WaveSpeedEditProvider()
wavespeed_models = set(provider.get_available_models().keys())
# Also check if provider is explicitly set to "wavespeed"
is_wavespeed = (
request.provider == "wavespeed" or
(request.model and request.model in wavespeed_models)
)
# Auto-detect: If no model specified and operation is general_edit, recommend one
if not request.model and not is_wavespeed and request.operation == "general_edit":
# Auto-select recommended model
try:
# Get image dimensions for recommendation
with Image.open(io.BytesIO(image_bytes)) as img:
image_resolution = {"width": img.width, "height": img.height}
recommendation = self.recommend_model(
operation=request.operation,
image_resolution=image_resolution,
preferences={"prioritize_cost": True}, # Default to cost-optimized
)
recommended_model = recommendation.get("recommended_model")
if recommended_model and recommended_model in wavespeed_models:
logger.info(f"[Edit Studio] Auto-selected model: {recommended_model} (reason: {recommendation.get('reason')})")
request.model = recommended_model
is_wavespeed = True
except Exception as e:
logger.warning(f"[Edit Studio] Auto-detection failed: {e}, falling back to HuggingFace")
if is_wavespeed:
# Use unified entry point for WaveSpeed models
logger.info(f"[Edit Studio] Using WaveSpeed unified entry for model={request.model}")
# Convert image bytes to base64
import base64
image_base64 = base64.b64encode(image_bytes).decode("utf-8")
# Prepare options for unified entry point
edit_options = {
"mask_base64": None,
"negative_prompt": request.negative_prompt,
"width": None, # Will be determined from image if needed
"height": None,
"guidance_scale": request.guidance_scale,
"steps": request.steps,
"seed": request.seed,
}
# Add mask if provided
if mask_bytes:
edit_options["mask_base64"] = base64.b64encode(mask_bytes).decode("utf-8")
# Extract dimensions from image if needed
with Image.open(io.BytesIO(image_bytes)) as img:
edit_options["width"] = img.width
edit_options["height"] = img.height
# Call unified entry point (synchronous, so run in thread)
result = await asyncio.to_thread(
generate_image_edit,
image_base64=image_base64,
prompt=request.prompt,
operation=request.operation or "general_edit",
model=request.model, # Will auto-select if None
options=edit_options,
user_id=user_id,
)
return result.image_bytes
else:
# Fall back to HuggingFace for backward compatibility
logger.info("[Edit Studio] Using HuggingFace (legacy) for general edit")
options = {
"provider": request.provider or "huggingface",
"model": request.model,
"guidance_scale": request.guidance_scale,
"steps": request.steps,
"seed": request.seed,
}
return result.image_bytes
# huggingface edit is synchronous - run in thread
result = await asyncio.to_thread(
huggingface_edit_image,
image_bytes,
request.prompt,
options,
user_id,
mask_bytes, # Optional mask for selective editing
)
return result.image_bytes
@staticmethod
def _extract_image_bytes(result: Any) -> bytes:

View File

@@ -0,0 +1,266 @@
"""Face Swap Studio service for AI-powered face swapping."""
from __future__ import annotations
import base64
import io
from dataclasses import dataclass
from typing import Any, Dict, Optional
from PIL import Image
from services.llm_providers.main_image_generation import generate_face_swap
from utils.logger_utils import get_service_logger
logger = get_service_logger("image_studio.face_swap")
@dataclass
class FaceSwapStudioRequest:
"""Request model for face swap operations."""
base_image_base64: str
face_image_base64: str
model: Optional[str] = None
target_face_index: Optional[int] = None
target_gender: Optional[str] = None
options: Optional[Dict[str, Any]] = None
class FaceSwapService:
"""Service for face swap operations."""
def __init__(self):
pass
def get_available_models(
self,
tier: Optional[str] = None,
) -> Dict[str, Any]:
"""Get available WaveSpeed face swap models.
Args:
tier: Filter by tier ("budget", "mid", "premium")
Returns:
Dictionary with models and metadata
"""
from services.llm_providers.image_generation.wavespeed_face_swap_provider import WaveSpeedFaceSwapProvider
provider = WaveSpeedFaceSwapProvider()
all_models = provider.get_available_models()
# Filter by tier if specified
if tier:
filtered = provider.get_models_by_tier(tier)
all_models = {k: v for k, v in all_models.items() if k in filtered}
# Format for API response
models_list = []
for model_id, model_info in all_models.items():
models_list.append({
"id": model_id,
"name": model_info.get("name", model_id),
"description": model_info.get("description", ""),
"cost": model_info.get("cost", 0.025),
"tier": model_info.get("tier", "mid"),
"capabilities": model_info.get("capabilities", []),
"use_cases": self._get_use_cases_for_model(model_id, model_info),
"features": model_info.get("features", []),
"max_faces": model_info.get("max_faces", 1),
})
return {
"models": models_list,
"total": len(models_list),
}
def recommend_model(
self,
base_image_resolution: Optional[Dict[str, int]] = None,
face_image_resolution: Optional[Dict[str, int]] = None,
user_tier: Optional[str] = None,
preferences: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Recommend best model for face swap.
Args:
base_image_resolution: Dict with "width" and "height" of base image
face_image_resolution: Dict with "width" and "height" of face image
user_tier: User subscription tier ("free", "pro", "enterprise")
preferences: Dict with "prioritize_cost" or "prioritize_quality"
Returns:
Dictionary with recommended model and alternatives
"""
from services.llm_providers.image_generation.wavespeed_face_swap_provider import WaveSpeedFaceSwapProvider
provider = WaveSpeedFaceSwapProvider()
available_models = provider.get_available_models()
if not available_models:
raise ValueError("No models available")
# Apply preferences
prioritize_cost = preferences and preferences.get("prioritize_cost", False)
prioritize_quality = preferences and preferences.get("prioritize_quality", False)
# Score models
scored_models = []
for model_id, model_info in available_models.items():
score = 0
cost = model_info.get("cost", 0.025)
tier = model_info.get("tier", "mid")
# Cost scoring (lower is better)
if prioritize_cost:
score += (1.0 / cost) * 100
else:
score += (1.0 / cost) * 50
# Quality scoring (higher cost = better quality for face swap)
if prioritize_quality:
score += cost * 20
else:
score += cost * 10
# Tier preference based on user tier
if user_tier == "free":
if tier == "budget":
score += 50
elif tier == "mid":
score += 20
elif user_tier in ["pro", "enterprise"]:
if tier == "premium":
score += 50
elif tier == "mid":
score += 30
scored_models.append((model_id, model_info, score))
# Sort by score (highest first)
scored_models.sort(key=lambda x: x[2], reverse=True)
# Get recommended model
recommended_id, recommended_info, recommended_score = scored_models[0]
# Build reason
reasons = []
if prioritize_cost:
reasons.append("Lowest cost option")
if prioritize_quality:
reasons.append("Best quality")
if user_tier == "free" and recommended_info.get("tier") == "budget":
reasons.append("Budget-friendly for free tier")
reason = ", ".join(reasons) if reasons else "Best match for your requirements"
# Get alternatives (top 2-3)
alternatives = []
for model_id, model_info, score in scored_models[1:4]:
alt_reason = f"Alternative: {model_info.get('tier', 'mid').title()} tier"
if model_info.get("cost", 0) < recommended_info.get("cost", 0):
alt_reason += ", lower cost"
elif model_info.get("cost", 0) > recommended_info.get("cost", 0):
alt_reason += ", higher quality"
alternatives.append({
"model_id": model_id,
"name": model_info.get("name", model_id),
"cost": model_info.get("cost", 0.025),
"reason": alt_reason,
})
return {
"recommended_model": recommended_id,
"reason": reason,
"alternatives": alternatives,
}
def _get_use_cases_for_model(self, model_id: str, model_info: Dict[str, Any]) -> list:
"""Get use cases for a model based on its capabilities."""
use_cases_map = {
"face_swap": ["Portrait editing", "Fun swaps", "Social media"],
"head_swap": ["Casting and concept design", "Privacy and anonymization", "Photo exploration"],
"full_head_replacement": ["Full head replacement", "Hair included", "Casting mockups"],
"realistic_blending": ["Professional work", "Marketing", "Entertainment"],
"multi_face": ["Group photos", "Family photos", "Team photos", "Creative projects", "Content creation"],
"face_enhancement": ["High-quality results", "Professional work", "Marketing campaigns"],
"identity_preservation": ["Character consistency", "Brand identity"],
}
capabilities = model_info.get("capabilities", [])
use_cases = []
for cap in capabilities:
if cap in use_cases_map:
use_cases.extend(use_cases_map[cap])
return list(set(use_cases)) if use_cases else ["General face swapping"]
async def process_face_swap(
self,
request: FaceSwapStudioRequest,
user_id: Optional[str] = None,
) -> Dict[str, Any]:
"""Process face swap request.
Args:
request: Face swap request
user_id: User ID for tracking
Returns:
Dictionary with result image and metadata
"""
# Auto-detect model if not specified
selected_model = request.model
if not selected_model:
try:
# Get image dimensions for recommendation
base_img = Image.open(io.BytesIO(base64.b64decode(request.base_image_base64.split(",", 1)[1] if "," in request.base_image_base64 else request.base_image_base64)))
face_img = Image.open(io.BytesIO(base64.b64decode(request.face_image_base64.split(",", 1)[1] if "," in request.face_image_base64 else request.face_image_base64)))
base_resolution = {"width": base_img.width, "height": base_img.height}
face_resolution = {"width": face_img.width, "height": face_img.height}
recommendation = self.recommend_model(
base_image_resolution=base_resolution,
face_image_resolution=face_resolution,
preferences={"prioritize_cost": True},
)
selected_model = recommendation.get("recommended_model")
logger.info(f"[Face Swap] Auto-selected model: {selected_model} (reason: {recommendation.get('reason')})")
except Exception as e:
logger.warning(f"[Face Swap] Auto-detection failed: {e}, using default model")
# Use first available model as fallback
from services.llm_providers.image_generation.wavespeed_face_swap_provider import WaveSpeedFaceSwapProvider
provider = WaveSpeedFaceSwapProvider()
all_models = provider.get_available_models()
if all_models:
selected_model = list(all_models.keys())[0]
# Prepare options
options = request.options or {}
if request.target_face_index is not None:
options["target_face_index"] = request.target_face_index
if request.target_gender:
options["target_gender"] = request.target_gender
# Call unified entry point
result = generate_face_swap(
base_image_base64=request.base_image_base64,
face_image_base64=request.face_image_base64,
model=selected_model,
options=options,
user_id=user_id,
)
# Convert result to base64
result_base64 = base64.b64encode(result.image_bytes).decode("utf-8")
result_data_url = f"data:image/png;base64,{result_base64}"
return {
"success": True,
"image_base64": result_data_url,
"width": result.width,
"height": result.height,
"provider": result.provider,
"model": result.model,
"metadata": result.metadata or {},
}

View File

@@ -0,0 +1,403 @@
"""Image Format Converter Service for converting between image formats."""
from __future__ import annotations
import base64
import io
from dataclasses import dataclass
from typing import Any, Dict, List, Optional
from PIL import Image, ImageCms
from utils.logger_utils import get_service_logger
logger = get_service_logger("image_studio.format_converter")
@dataclass
class FormatConversionRequest:
"""Request model for format conversion."""
image_base64: str
target_format: str # png, jpeg, jpg, webp, gif, bmp, tiff
preserve_transparency: bool = True
quality: Optional[int] = None # For lossy formats (1-100)
color_space: Optional[str] = None # sRGB, Adobe RGB, etc.
strip_metadata: bool = False # Keep metadata by default for conversion
optimize: bool = True
progressive: bool = True # For JPEG
@dataclass
class FormatConversionResult:
"""Result of format conversion."""
success: bool
image_base64: str
original_format: str
target_format: str
original_size_kb: float
converted_size_kb: float
width: int
height: int
transparency_preserved: bool
metadata_preserved: bool
color_space: Optional[str] = None
class ImageFormatConverterService:
"""Service for converting images between formats."""
SUPPORTED_FORMATS = {
"png": {
"name": "PNG",
"description": "Lossless format with transparency support",
"supports_transparency": True,
"supports_lossy": False,
"mime_type": "image/png",
},
"jpeg": {
"name": "JPEG",
"description": "Lossy format, best for photos",
"supports_transparency": False,
"supports_lossy": True,
"mime_type": "image/jpeg",
},
"jpg": {
"name": "JPEG",
"description": "Lossy format, best for photos",
"supports_transparency": False,
"supports_lossy": True,
"mime_type": "image/jpeg",
},
"webp": {
"name": "WebP",
"description": "Modern format with excellent compression",
"supports_transparency": True,
"supports_lossy": True,
"mime_type": "image/webp",
},
"gif": {
"name": "GIF",
"description": "Supports animation and transparency",
"supports_transparency": True,
"supports_lossy": False,
"mime_type": "image/gif",
},
"bmp": {
"name": "BMP",
"description": "Uncompressed bitmap format",
"supports_transparency": False,
"supports_lossy": False,
"mime_type": "image/bmp",
},
"tiff": {
"name": "TIFF",
"description": "High-quality format for print",
"supports_transparency": True,
"supports_lossy": False,
"mime_type": "image/tiff",
},
}
def __init__(self):
logger.info("[Format Converter] ImageFormatConverterService initialized")
def _decode_image(self, image_base64: str) -> tuple[Image.Image, int, str]:
"""Decode base64 image and return PIL Image, size, and format."""
# Handle data URL format
if "," in image_base64:
image_base64 = image_base64.split(",", 1)[1]
image_bytes = base64.b64decode(image_base64)
original_size = len(image_bytes)
image = Image.open(io.BytesIO(image_bytes))
original_format = image.format.lower() if image.format else "unknown"
return image, original_size, original_format
def _strip_exif(self, image: Image.Image) -> Image.Image:
"""Remove EXIF metadata from image."""
data = list(image.getdata())
image_without_exif = Image.new(image.mode, image.size)
image_without_exif.putdata(data)
return image_without_exif
def _convert_color_space(
self,
image: Image.Image,
target_color_space: str,
) -> Image.Image:
"""Convert image color space."""
try:
# Get current color space
if hasattr(image, 'info') and 'icc_profile' in image.info:
# Image has ICC profile
try:
src_profile = ImageCms.ImageCmsProfile(io.BytesIO(image.info['icc_profile']))
if target_color_space.lower() == "srgb":
dst_profile = ImageCms.createProfile("sRGB")
elif target_color_space.lower() == "adobe rgb":
dst_profile = ImageCms.createProfile("Adobe RGB")
else:
return image # Unknown color space
transform = ImageCms.ImageCmsTransform(src_profile, dst_profile, image.mode, image.mode)
image = ImageCms.applyTransform(image, transform)
except Exception as e:
logger.warning(f"[Format Converter] Color space conversion failed: {e}")
else:
# No ICC profile, assume sRGB
logger.info("[Format Converter] No ICC profile found, assuming sRGB")
except Exception as e:
logger.warning(f"[Format Converter] Color space conversion error: {e}")
return image
def _convert_image(
self,
image: Image.Image,
target_format: str,
quality: Optional[int],
preserve_transparency: bool,
optimize: bool,
progressive: bool,
) -> bytes:
"""Convert image to target format."""
buffer = io.BytesIO()
format_lower = target_format.lower()
# Handle format-specific conversions
save_kwargs: Dict[str, Any] = {}
# Check if source has transparency and target doesn't support it
has_transparency = image.mode in ("RGBA", "LA", "P") and (
"transparency" in image.info or image.mode == "RGBA"
)
if format_lower in ["jpeg", "jpg"]:
# JPEG doesn't support transparency
if has_transparency and preserve_transparency:
# Convert to RGB, losing transparency
if image.mode in ("RGBA", "LA"):
# Create white background
rgb_image = Image.new("RGB", image.size, (255, 255, 255))
if image.mode == "RGBA":
rgb_image.paste(image, mask=image.split()[3]) # Use alpha channel as mask
else:
rgb_image.paste(image)
image = rgb_image
elif image.mode == "P":
image = image.convert("RGB")
else:
image = image.convert("RGB")
save_kwargs["format"] = "JPEG"
if quality:
save_kwargs["quality"] = quality
else:
save_kwargs["quality"] = 95 # Default high quality
save_kwargs["optimize"] = optimize
if progressive:
save_kwargs["progressive"] = True
elif format_lower == "png":
save_kwargs["format"] = "PNG"
save_kwargs["optimize"] = optimize
# PNG compression level (0-9)
if quality:
compress_level = max(0, min(9, (100 - quality) // 11))
save_kwargs["compress_level"] = compress_level
else:
save_kwargs["compress_level"] = 6 # Default
elif format_lower == "webp":
save_kwargs["format"] = "WEBP"
if quality:
save_kwargs["quality"] = quality
else:
save_kwargs["quality"] = 80 # Default
save_kwargs["method"] = 6 # Best compression
if preserve_transparency and has_transparency:
# WebP supports transparency
if image.mode not in ("RGBA", "LA"):
image = image.convert("RGBA")
elif format_lower == "gif":
save_kwargs["format"] = "GIF"
# GIF conversion
if image.mode != "P":
# Convert to palette mode for GIF
image = image.convert("P", palette=Image.ADAPTIVE)
save_kwargs["optimize"] = optimize
if preserve_transparency and has_transparency:
save_kwargs["transparency"] = 255 # Preserve transparency
elif format_lower == "bmp":
save_kwargs["format"] = "BMP"
if image.mode in ("RGBA", "LA", "P") and has_transparency:
# BMP doesn't support transparency, convert to RGB
if image.mode == "RGBA":
rgb_image = Image.new("RGB", image.size, (255, 255, 255))
rgb_image.paste(image, mask=image.split()[3])
image = rgb_image
else:
image = image.convert("RGB")
elif format_lower == "tiff":
save_kwargs["format"] = "TIFF"
save_kwargs["compression"] = "tiff_lzw" # Lossless compression
if preserve_transparency and has_transparency:
# TIFF supports transparency
if image.mode not in ("RGBA", "LA"):
image = image.convert("RGBA")
else:
raise ValueError(f"Unsupported target format: {target_format}")
image.save(buffer, **save_kwargs)
return buffer.getvalue()
async def convert(
self,
request: FormatConversionRequest,
user_id: Optional[str] = None,
) -> FormatConversionResult:
"""Convert an image to target format."""
logger.info(f"[Format Converter] Processing conversion request for user: {user_id}")
try:
# Decode image
image, original_size, original_format = self._decode_image(request.image_base64)
original_size_kb = original_size / 1024
logger.info(f"[Format Converter] Original: {original_format}, Target: {request.target_format}, Size: {original_size_kb:.2f} KB")
# Validate target format
format_lower = request.target_format.lower()
if format_lower not in self.SUPPORTED_FORMATS:
raise ValueError(f"Unsupported format: {request.target_format}. Supported: {list(self.SUPPORTED_FORMATS.keys())}")
# Check transparency preservation
has_transparency = image.mode in ("RGBA", "LA", "P") and (
"transparency" in image.info or image.mode == "RGBA"
)
target_supports_transparency = self.SUPPORTED_FORMATS[format_lower]["supports_transparency"]
transparency_preserved = (
has_transparency and
target_supports_transparency and
request.preserve_transparency
)
# Color space conversion
if request.color_space:
image = self._convert_color_space(image, request.color_space)
# Strip metadata if requested
metadata_preserved = not request.strip_metadata
if request.strip_metadata:
image = self._strip_exif(image)
# Convert format
converted_bytes = self._convert_image(
image,
format_lower,
request.quality,
request.preserve_transparency,
request.optimize,
request.progressive,
)
converted_size_kb = len(converted_bytes) / 1024
# Encode result
mime_type = self.SUPPORTED_FORMATS[format_lower]["mime_type"]
result_base64 = f"data:{mime_type};base64,{base64.b64encode(converted_bytes).decode()}"
logger.info(f"[Format Converter] Converted: {original_size_kb:.2f}KB → {converted_size_kb:.2f}KB")
return FormatConversionResult(
success=True,
image_base64=result_base64,
original_format=original_format,
target_format=format_lower,
original_size_kb=round(original_size_kb, 2),
converted_size_kb=round(converted_size_kb, 2),
width=image.width,
height=image.height,
transparency_preserved=transparency_preserved,
metadata_preserved=metadata_preserved,
color_space=request.color_space,
)
except Exception as e:
logger.error(f"[Format Converter] Failed to convert image: {e}")
raise
async def convert_batch(
self,
requests: List[FormatConversionRequest],
user_id: Optional[str] = None,
) -> List[FormatConversionResult]:
"""Convert multiple images."""
logger.info(f"[Format Converter] Processing batch of {len(requests)} images for user: {user_id}")
results = []
for i, request in enumerate(requests):
try:
result = await self.convert(request, user_id)
results.append(result)
logger.info(f"[Format Converter] Batch item {i+1}/{len(requests)} complete")
except Exception as e:
logger.error(f"[Format Converter] Batch item {i+1} failed: {e}")
results.append(FormatConversionResult(
success=False,
image_base64="",
original_format="",
target_format="",
original_size_kb=0,
converted_size_kb=0,
width=0,
height=0,
transparency_preserved=False,
metadata_preserved=False,
))
return results
def get_supported_formats(self) -> List[Dict[str, Any]]:
"""Get list of supported formats with details."""
return [
{
"id": fmt_id,
"name": fmt_info["name"],
"description": fmt_info["description"],
"supports_transparency": fmt_info["supports_transparency"],
"supports_lossy": fmt_info["supports_lossy"],
"mime_type": fmt_info["mime_type"],
}
for fmt_id, fmt_info in self.SUPPORTED_FORMATS.items()
]
def get_format_recommendations(self, source_format: str) -> List[Dict[str, Any]]:
"""Get format recommendations based on source format."""
recommendations = {
"png": [
{"format": "webp", "reason": "60% smaller file size, maintains transparency"},
{"format": "jpeg", "reason": "Best for photos, smaller file size"},
],
"jpeg": [
{"format": "webp", "reason": "25-34% smaller with similar quality"},
{"format": "png", "reason": "Lossless, supports transparency"},
],
"jpg": [
{"format": "webp", "reason": "25-34% smaller with similar quality"},
{"format": "png", "reason": "Lossless, supports transparency"},
],
"webp": [
{"format": "png", "reason": "Better compatibility, lossless"},
{"format": "jpeg", "reason": "Universal compatibility"},
],
}
source_lower = source_format.lower()
return recommendations.get(source_lower, [])

View File

@@ -7,6 +7,9 @@ from .edit_service import EditStudioService, EditStudioRequest
from .upscale_service import UpscaleStudioService, UpscaleStudioRequest
from .control_service import ControlStudioService, ControlStudioRequest
from .social_optimizer_service import SocialOptimizerService, SocialOptimizerRequest
from .face_swap_service import FaceSwapService, FaceSwapStudioRequest
from .compression_service import ImageCompressionService, CompressionRequest, CompressionResult
from .format_converter_service import ImageFormatConverterService, FormatConversionRequest, FormatConversionResult
from .transform_service import (
TransformStudioService,
TransformImageToVideoRequest,
@@ -29,6 +32,9 @@ class ImageStudioManager:
self.upscale_service = UpscaleStudioService()
self.control_service = ControlStudioService()
self.social_optimizer_service = SocialOptimizerService()
self.face_swap_service = FaceSwapService()
self.compression_service = ImageCompressionService()
self.format_converter_service = ImageFormatConverterService()
self.transform_service = TransformStudioService()
logger.info("[Image Studio Manager] Initialized successfully")
@@ -69,6 +75,99 @@ class ImageStudioManager:
def get_edit_operations(self) -> Dict[str, Any]:
"""Expose edit operations for UI."""
return self.edit_service.list_operations()
def get_edit_models(
self,
operation: Optional[str] = None,
tier: Optional[str] = None,
) -> Dict[str, Any]:
"""Get available editing models.
Args:
operation: Filter by operation type
tier: Filter by tier (budget, mid, premium)
Returns:
Dictionary with models and metadata
"""
return self.edit_service.get_available_models(operation=operation, tier=tier)
def recommend_edit_model(
self,
operation: str,
image_resolution: Optional[Dict[str, int]] = None,
user_tier: Optional[str] = None,
preferences: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Recommend best editing model for given context.
Args:
operation: Operation type
image_resolution: Image dimensions
user_tier: User subscription tier
preferences: User preferences (prioritize_cost, prioritize_quality)
Returns:
Dictionary with recommended model and alternatives
"""
return self.edit_service.recommend_model(
operation=operation,
image_resolution=image_resolution,
user_tier=user_tier,
preferences=preferences,
)
# ====================
# FACE SWAP STUDIO
# ====================
async def face_swap(
self,
request: FaceSwapStudioRequest,
user_id: Optional[str] = None,
) -> Dict[str, Any]:
"""Run Face Swap Studio operations."""
logger.info("[Image Studio] Face swap request from user: %s", user_id)
return await self.face_swap_service.process_face_swap(request, user_id=user_id)
def get_face_swap_models(
self,
tier: Optional[str] = None,
) -> Dict[str, Any]:
"""Get available face swap models.
Args:
tier: Filter by tier (budget, mid, premium)
Returns:
Dictionary with models and metadata
"""
return self.face_swap_service.get_available_models(tier=tier)
def recommend_face_swap_model(
self,
base_image_resolution: Optional[Dict[str, int]] = None,
face_image_resolution: Optional[Dict[str, int]] = None,
user_tier: Optional[str] = None,
preferences: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Recommend best face swap model for given context.
Args:
base_image_resolution: Base image dimensions
face_image_resolution: Face image dimensions
user_tier: User subscription tier
preferences: User preferences (prioritize_cost, prioritize_quality)
Returns:
Dictionary with recommended model and alternatives
"""
return self.face_swap_service.recommend_model(
base_image_resolution=base_image_resolution,
face_image_resolution=face_image_resolution,
user_tier=user_tier,
preferences=preferences,
)
# ====================
# UPSCALE STUDIO
@@ -377,3 +476,72 @@ class ImageStudioManager:
"""Estimate cost for transform operation."""
return self.transform_service.estimate_cost(operation, resolution, duration)
# ====================
# COMPRESSION STUDIO
# ====================
async def compress_image(
self,
request: CompressionRequest,
user_id: Optional[str] = None,
) -> CompressionResult:
"""Compress an image with specified settings."""
logger.info("[Image Studio] Compress image request from user: %s", user_id)
return await self.compression_service.compress(request, user_id=user_id)
async def compress_batch(
self,
requests: List[CompressionRequest],
user_id: Optional[str] = None,
) -> List[CompressionResult]:
"""Compress multiple images."""
logger.info("[Image Studio] Batch compress request (%d images) from user: %s", len(requests), user_id)
return await self.compression_service.compress_batch(requests, user_id=user_id)
async def estimate_compression(
self,
image_base64: str,
format: str = "jpeg",
quality: int = 85,
) -> Dict[str, Any]:
"""Estimate compression results without compressing."""
return await self.compression_service.estimate_compression(image_base64, format, quality)
def get_compression_formats(self) -> List[Dict[str, Any]]:
"""Get supported compression formats."""
return self.compression_service.get_supported_formats()
def get_compression_presets(self) -> List[Dict[str, Any]]:
"""Get compression presets for common use cases."""
return self.compression_service.get_presets()
# ====================
# FORMAT CONVERTER
# ====================
async def convert_format(
self,
request: FormatConversionRequest,
user_id: Optional[str] = None,
) -> FormatConversionResult:
"""Convert an image to target format."""
logger.info("[Image Studio] Convert format request from user: %s", user_id)
return await self.format_converter_service.convert(request, user_id=user_id)
async def convert_format_batch(
self,
requests: List[FormatConversionRequest],
user_id: Optional[str] = None,
) -> List[FormatConversionResult]:
"""Convert multiple images."""
logger.info("[Image Studio] Batch convert format request (%d images) from user: %s", len(requests), user_id)
return await self.format_converter_service.convert_batch(requests, user_id=user_id)
def get_supported_formats(self) -> List[Dict[str, Any]]:
"""Get supported conversion formats."""
return self.format_converter_service.get_supported_formats()
def get_format_recommendations(self, source_format: str) -> List[Dict[str, Any]]:
"""Get format recommendations based on source format."""
return self.format_converter_service.get_format_recommendations(source_format)

View File

@@ -36,18 +36,16 @@ class UpscaleStudioService:
request: UpscaleStudioRequest,
user_id: Optional[str] = None,
) -> Dict[str, Any]:
# Pre-flight validation: Reuse unified helper
# Note: Using image-generation validation since upscaling uses same subscription limits
if user_id:
from services.database import get_db
from services.subscription import PricingService
from services.subscription.preflight_validator import validate_image_upscale_operations
db = next(get_db())
try:
pricing_service = PricingService(db)
logger.info("[Upscale Studio] 🛂 Running pre-flight validation for user %s", user_id)
validate_image_upscale_operations(pricing_service=pricing_service, user_id=user_id)
finally:
db.close()
from services.llm_providers.main_image_generation import _validate_image_operation
_validate_image_operation(
user_id=user_id,
operation_type="image-upscale",
num_operations=1,
log_prefix="[Upscale Studio]"
)
image_bytes = self._decode_base64(request.image_base64)
if not image_bytes: