ultimate-gemini-mcp 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ultimate-gemini-mcp might be problematic. Click here for more details.
- src/__init__.py +16 -0
- src/config/__init__.py +32 -0
- src/config/constants.py +77 -0
- src/config/settings.py +143 -0
- src/core/__init__.py +55 -0
- src/core/exceptions.py +60 -0
- src/core/validation.py +161 -0
- src/server.py +166 -0
- src/services/__init__.py +15 -0
- src/services/gemini_client.py +230 -0
- src/services/image_service.py +243 -0
- src/services/imagen_client.py +175 -0
- src/services/prompt_enhancer.py +140 -0
- src/tools/__init__.py +11 -0
- src/tools/batch_generate.py +159 -0
- src/tools/generate_image.py +252 -0
- ultimate_gemini_mcp-1.0.1.dist-info/METADATA +372 -0
- ultimate_gemini_mcp-1.0.1.dist-info/RECORD +21 -0
- ultimate_gemini_mcp-1.0.1.dist-info/WHEEL +4 -0
- ultimate_gemini_mcp-1.0.1.dist-info/entry_points.txt +2 -0
- ultimate_gemini_mcp-1.0.1.dist-info/licenses/LICENSE +31 -0
src/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ultimate Gemini MCP Server
|
|
3
|
+
|
|
4
|
+
A unified MCP server that combines the best features from:
|
|
5
|
+
- Gemini 2.5 Flash Image (with prompt enhancement)
|
|
6
|
+
- Imagen 3, 4, and 4-Ultra
|
|
7
|
+
- Advanced features: batch processing, editing, templates, and more
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
__version__ = "1.0.1"
|
|
11
|
+
__author__ = "Ultimate Gemini MCP"
|
|
12
|
+
|
|
13
|
+
from .config import get_settings
|
|
14
|
+
from .server import create_app, main
|
|
15
|
+
|
|
16
|
+
__all__ = ["create_app", "main", "get_settings", "__version__"]
|
src/config/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Configuration module for Ultimate Gemini MCP."""
|
|
2
|
+
|
|
3
|
+
from .constants import (
|
|
4
|
+
ALL_MODELS,
|
|
5
|
+
ASPECT_RATIOS,
|
|
6
|
+
DEFAULT_GEMINI_MODEL,
|
|
7
|
+
DEFAULT_IMAGEN_MODEL,
|
|
8
|
+
GEMINI_MODELS,
|
|
9
|
+
IMAGE_FORMATS,
|
|
10
|
+
IMAGEN_MODELS,
|
|
11
|
+
MAX_BATCH_SIZE,
|
|
12
|
+
MAX_IMAGES_PER_REQUEST,
|
|
13
|
+
PERSON_GENERATION_OPTIONS,
|
|
14
|
+
)
|
|
15
|
+
from .settings import APIConfig, ServerConfig, Settings, get_settings
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"ALL_MODELS",
|
|
19
|
+
"ASPECT_RATIOS",
|
|
20
|
+
"DEFAULT_GEMINI_MODEL",
|
|
21
|
+
"DEFAULT_IMAGEN_MODEL",
|
|
22
|
+
"GEMINI_MODELS",
|
|
23
|
+
"IMAGE_FORMATS",
|
|
24
|
+
"IMAGEN_MODELS",
|
|
25
|
+
"MAX_BATCH_SIZE",
|
|
26
|
+
"MAX_IMAGES_PER_REQUEST",
|
|
27
|
+
"PERSON_GENERATION_OPTIONS",
|
|
28
|
+
"APIConfig",
|
|
29
|
+
"ServerConfig",
|
|
30
|
+
"Settings",
|
|
31
|
+
"get_settings",
|
|
32
|
+
]
|
src/config/constants.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Constants and model definitions for both Gemini and Imagen APIs.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Final
|
|
6
|
+
|
|
7
|
+
# API Endpoints
|
|
8
|
+
GEMINI_API_BASE: Final[str] = "https://generativelanguage.googleapis.com/v1beta"
|
|
9
|
+
IMAGEN_API_BASE: Final[str] = "https://generativelanguage.googleapis.com/v1beta"
|
|
10
|
+
|
|
11
|
+
# Gemini Models (generateContent API)
|
|
12
|
+
GEMINI_MODELS = {
|
|
13
|
+
"gemini-2.5-flash-image": "gemini-2.5-flash-image",
|
|
14
|
+
"gemini-flash-latest": "gemini-flash-latest", # For prompt enhancement (non-image)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
# Imagen Models (predict API)
|
|
18
|
+
IMAGEN_MODELS = {
|
|
19
|
+
"imagen-4": "models/imagen-4.0-generate-001",
|
|
20
|
+
"imagen-4-fast": "models/imagen-4.0-fast-generate-001",
|
|
21
|
+
"imagen-4-ultra": "models/imagen-4.0-ultra-generate-001",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# All available models
|
|
25
|
+
ALL_MODELS = {**GEMINI_MODELS, **IMAGEN_MODELS}
|
|
26
|
+
|
|
27
|
+
# Default models
|
|
28
|
+
DEFAULT_GEMINI_MODEL = "gemini-2.5-flash-image"
|
|
29
|
+
DEFAULT_IMAGEN_MODEL = "imagen-4-ultra"
|
|
30
|
+
DEFAULT_ENHANCEMENT_MODEL = "gemini-flash-latest"
|
|
31
|
+
|
|
32
|
+
# Aspect ratios
|
|
33
|
+
ASPECT_RATIOS = [
|
|
34
|
+
"1:1", # Square
|
|
35
|
+
"2:3", # Portrait
|
|
36
|
+
"3:2", # Landscape
|
|
37
|
+
"3:4", # Portrait
|
|
38
|
+
"4:3", # Standard landscape
|
|
39
|
+
"4:5", # Portrait
|
|
40
|
+
"5:4", # Landscape
|
|
41
|
+
"9:16", # Vertical mobile
|
|
42
|
+
"16:9", # Widescreen
|
|
43
|
+
"21:9", # Ultrawide
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
# Image formats
|
|
47
|
+
IMAGE_FORMATS = {
|
|
48
|
+
"png": "image/png",
|
|
49
|
+
"jpeg": "image/jpeg",
|
|
50
|
+
"jpg": "image/jpeg",
|
|
51
|
+
"webp": "image/webp",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Person generation options (Imagen API)
|
|
55
|
+
PERSON_GENERATION_OPTIONS = [
|
|
56
|
+
"dont_allow",
|
|
57
|
+
"allow_adult",
|
|
58
|
+
"allow_all",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
# Generation limits
|
|
62
|
+
MAX_IMAGES_PER_REQUEST = 4
|
|
63
|
+
MAX_BATCH_SIZE = 8
|
|
64
|
+
MAX_PROMPT_LENGTH = 8192
|
|
65
|
+
MAX_NEGATIVE_PROMPT_LENGTH = 1024
|
|
66
|
+
|
|
67
|
+
# File size limits
|
|
68
|
+
MAX_IMAGE_SIZE_MB = 20
|
|
69
|
+
MAX_IMAGE_SIZE_BYTES = MAX_IMAGE_SIZE_MB * 1024 * 1024
|
|
70
|
+
|
|
71
|
+
# Timeout settings (in seconds)
|
|
72
|
+
DEFAULT_TIMEOUT = 60
|
|
73
|
+
ENHANCEMENT_TIMEOUT = 30
|
|
74
|
+
BATCH_TIMEOUT = 120
|
|
75
|
+
|
|
76
|
+
# Output settings
|
|
77
|
+
DEFAULT_OUTPUT_DIR = "generated_images"
|
src/config/settings.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration settings for the Ultimate Gemini MCP server.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from pydantic import Field
|
|
10
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
11
|
+
|
|
12
|
+
from .constants import (
|
|
13
|
+
DEFAULT_ENHANCEMENT_MODEL,
|
|
14
|
+
DEFAULT_GEMINI_MODEL,
|
|
15
|
+
DEFAULT_IMAGEN_MODEL,
|
|
16
|
+
DEFAULT_OUTPUT_DIR,
|
|
17
|
+
DEFAULT_TIMEOUT,
|
|
18
|
+
MAX_BATCH_SIZE,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ServerConfig(BaseSettings):
|
|
23
|
+
"""Server configuration settings."""
|
|
24
|
+
|
|
25
|
+
model_config = SettingsConfigDict(
|
|
26
|
+
env_file=".env",
|
|
27
|
+
env_file_encoding="utf-8",
|
|
28
|
+
case_sensitive=False,
|
|
29
|
+
extra="ignore",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Server settings
|
|
33
|
+
transport: str = Field(default="stdio", description="Transport mode: stdio or http")
|
|
34
|
+
host: str = Field(default="localhost", description="Host for HTTP transport")
|
|
35
|
+
port: int = Field(default=8000, description="Port for HTTP transport")
|
|
36
|
+
|
|
37
|
+
# Logging settings
|
|
38
|
+
log_level: str = Field(default="INFO", description="Logging level")
|
|
39
|
+
log_format: str = Field(default="standard", description="Log format: standard, json, detailed")
|
|
40
|
+
|
|
41
|
+
# Output settings
|
|
42
|
+
output_dir: str = Field(
|
|
43
|
+
default=DEFAULT_OUTPUT_DIR, description="Directory for generated images"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_env(cls) -> "ServerConfig":
|
|
48
|
+
"""Load configuration from environment variables."""
|
|
49
|
+
return cls()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class APIConfig(BaseSettings):
|
|
53
|
+
"""API configuration for Gemini and Imagen."""
|
|
54
|
+
|
|
55
|
+
model_config = SettingsConfigDict(
|
|
56
|
+
env_file=".env",
|
|
57
|
+
env_file_encoding="utf-8",
|
|
58
|
+
case_sensitive=False,
|
|
59
|
+
extra="ignore",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# API Keys
|
|
63
|
+
gemini_api_key: str = Field(
|
|
64
|
+
default="",
|
|
65
|
+
alias="GEMINI_API_KEY",
|
|
66
|
+
description="Gemini API key (also accepts GOOGLE_API_KEY)",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Model settings
|
|
70
|
+
default_gemini_model: str = Field(
|
|
71
|
+
default=DEFAULT_GEMINI_MODEL, description="Default Gemini model"
|
|
72
|
+
)
|
|
73
|
+
default_imagen_model: str = Field(
|
|
74
|
+
default=DEFAULT_IMAGEN_MODEL, description="Default Imagen model"
|
|
75
|
+
)
|
|
76
|
+
enhancement_model: str = Field(
|
|
77
|
+
default=DEFAULT_ENHANCEMENT_MODEL, description="Model for prompt enhancement"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Feature flags
|
|
81
|
+
enable_prompt_enhancement: bool = Field(
|
|
82
|
+
default=True, description="Enable automatic prompt enhancement"
|
|
83
|
+
)
|
|
84
|
+
enable_batch_processing: bool = Field(
|
|
85
|
+
default=True, description="Enable batch processing"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Request settings
|
|
89
|
+
request_timeout: int = Field(default=DEFAULT_TIMEOUT, description="API request timeout")
|
|
90
|
+
max_batch_size: int = Field(
|
|
91
|
+
default=MAX_BATCH_SIZE, description="Maximum batch size for parallel requests"
|
|
92
|
+
)
|
|
93
|
+
max_retries: int = Field(default=3, description="Maximum number of retries for failed requests")
|
|
94
|
+
|
|
95
|
+
# Image settings
|
|
96
|
+
default_aspect_ratio: str = Field(default="1:1", description="Default aspect ratio")
|
|
97
|
+
default_output_format: str = Field(default="png", description="Default output format")
|
|
98
|
+
|
|
99
|
+
def __init__(self, **kwargs):
|
|
100
|
+
"""Initialize API configuration with fallback for API key."""
|
|
101
|
+
super().__init__(**kwargs)
|
|
102
|
+
# Fallback to GOOGLE_API_KEY if GEMINI_API_KEY not set
|
|
103
|
+
if not self.gemini_api_key:
|
|
104
|
+
self.gemini_api_key = os.getenv("GOOGLE_API_KEY", "")
|
|
105
|
+
|
|
106
|
+
if not self.gemini_api_key:
|
|
107
|
+
raise ValueError(
|
|
108
|
+
"GEMINI_API_KEY or GOOGLE_API_KEY environment variable is required"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def from_env(cls) -> "APIConfig":
|
|
113
|
+
"""Load API configuration from environment variables."""
|
|
114
|
+
return cls()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class Settings:
|
|
118
|
+
"""Combined settings for the server."""
|
|
119
|
+
|
|
120
|
+
def __init__(self):
|
|
121
|
+
self.server = ServerConfig.from_env()
|
|
122
|
+
self.api = APIConfig.from_env()
|
|
123
|
+
|
|
124
|
+
# Ensure output directory exists
|
|
125
|
+
output_path = Path(self.server.output_dir)
|
|
126
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def output_dir(self) -> Path:
|
|
130
|
+
"""Get output directory as Path object."""
|
|
131
|
+
return Path(self.server.output_dir)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# Global settings instance (lazy initialization)
|
|
135
|
+
_settings: Optional[Settings] = None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def get_settings() -> Settings:
|
|
139
|
+
"""Get or create global settings instance."""
|
|
140
|
+
global _settings
|
|
141
|
+
if _settings is None:
|
|
142
|
+
_settings = Settings()
|
|
143
|
+
return _settings
|
src/core/__init__.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Core modules for Ultimate Gemini MCP."""
|
|
2
|
+
|
|
3
|
+
from .exceptions import (
|
|
4
|
+
APIError,
|
|
5
|
+
AuthenticationError,
|
|
6
|
+
ConfigurationError,
|
|
7
|
+
ContentPolicyError,
|
|
8
|
+
FileOperationError,
|
|
9
|
+
ImageProcessingError,
|
|
10
|
+
RateLimitError,
|
|
11
|
+
UltimateGeminiError,
|
|
12
|
+
ValidationError,
|
|
13
|
+
)
|
|
14
|
+
from .validation import (
|
|
15
|
+
sanitize_filename,
|
|
16
|
+
validate_aspect_ratio,
|
|
17
|
+
validate_base64_image,
|
|
18
|
+
validate_batch_size,
|
|
19
|
+
validate_file_path,
|
|
20
|
+
validate_image_format,
|
|
21
|
+
validate_model,
|
|
22
|
+
validate_negative_prompt,
|
|
23
|
+
validate_number_of_images,
|
|
24
|
+
validate_person_generation,
|
|
25
|
+
validate_prompt,
|
|
26
|
+
validate_prompts_list,
|
|
27
|
+
validate_seed,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
# Exceptions
|
|
32
|
+
"UltimateGeminiError",
|
|
33
|
+
"ConfigurationError",
|
|
34
|
+
"ValidationError",
|
|
35
|
+
"APIError",
|
|
36
|
+
"AuthenticationError",
|
|
37
|
+
"RateLimitError",
|
|
38
|
+
"ContentPolicyError",
|
|
39
|
+
"ImageProcessingError",
|
|
40
|
+
"FileOperationError",
|
|
41
|
+
# Validation
|
|
42
|
+
"validate_prompt",
|
|
43
|
+
"validate_negative_prompt",
|
|
44
|
+
"validate_model",
|
|
45
|
+
"validate_aspect_ratio",
|
|
46
|
+
"validate_number_of_images",
|
|
47
|
+
"validate_image_format",
|
|
48
|
+
"validate_person_generation",
|
|
49
|
+
"validate_seed",
|
|
50
|
+
"validate_file_path",
|
|
51
|
+
"validate_base64_image",
|
|
52
|
+
"validate_prompts_list",
|
|
53
|
+
"validate_batch_size",
|
|
54
|
+
"sanitize_filename",
|
|
55
|
+
]
|
src/core/exceptions.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom exceptions for the Ultimate Gemini MCP server.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class UltimateGeminiError(Exception):
|
|
7
|
+
"""Base exception for all Ultimate Gemini MCP errors."""
|
|
8
|
+
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ConfigurationError(UltimateGeminiError):
|
|
13
|
+
"""Raised when there's a configuration error."""
|
|
14
|
+
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ValidationError(UltimateGeminiError):
|
|
19
|
+
"""Raised when input validation fails."""
|
|
20
|
+
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class APIError(UltimateGeminiError):
|
|
25
|
+
"""Raised when an API request fails."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, message: str, status_code: int | None = None, response_data: dict | None = None):
|
|
28
|
+
super().__init__(message)
|
|
29
|
+
self.status_code = status_code
|
|
30
|
+
self.response_data = response_data
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AuthenticationError(APIError):
|
|
34
|
+
"""Raised when API authentication fails."""
|
|
35
|
+
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class RateLimitError(APIError):
|
|
40
|
+
"""Raised when API rate limit is exceeded."""
|
|
41
|
+
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ContentPolicyError(APIError):
|
|
46
|
+
"""Raised when content violates safety policies."""
|
|
47
|
+
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ImageProcessingError(UltimateGeminiError):
|
|
52
|
+
"""Raised when image processing fails."""
|
|
53
|
+
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class FileOperationError(UltimateGeminiError):
|
|
58
|
+
"""Raised when file operations fail."""
|
|
59
|
+
|
|
60
|
+
pass
|
src/core/validation.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Input validation utilities.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from ..config.constants import (
|
|
11
|
+
ALL_MODELS,
|
|
12
|
+
ASPECT_RATIOS,
|
|
13
|
+
IMAGE_FORMATS,
|
|
14
|
+
MAX_IMAGES_PER_REQUEST,
|
|
15
|
+
MAX_NEGATIVE_PROMPT_LENGTH,
|
|
16
|
+
MAX_PROMPT_LENGTH,
|
|
17
|
+
PERSON_GENERATION_OPTIONS,
|
|
18
|
+
)
|
|
19
|
+
from .exceptions import ValidationError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def validate_prompt(prompt: str) -> None:
|
|
23
|
+
"""Validate prompt text."""
|
|
24
|
+
if not prompt or not prompt.strip():
|
|
25
|
+
raise ValidationError("Prompt cannot be empty")
|
|
26
|
+
|
|
27
|
+
if len(prompt) > MAX_PROMPT_LENGTH:
|
|
28
|
+
raise ValidationError(
|
|
29
|
+
f"Prompt too long: {len(prompt)} characters (max {MAX_PROMPT_LENGTH})"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def validate_negative_prompt(negative_prompt: str | None) -> None:
|
|
34
|
+
"""Validate negative prompt text."""
|
|
35
|
+
if negative_prompt and len(negative_prompt) > MAX_NEGATIVE_PROMPT_LENGTH:
|
|
36
|
+
raise ValidationError(
|
|
37
|
+
f"Negative prompt too long: {len(negative_prompt)} characters "
|
|
38
|
+
f"(max {MAX_NEGATIVE_PROMPT_LENGTH})"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def validate_model(model: str) -> None:
|
|
43
|
+
"""Validate model name."""
|
|
44
|
+
if model not in ALL_MODELS:
|
|
45
|
+
available = ", ".join(ALL_MODELS.keys())
|
|
46
|
+
raise ValidationError(f"Invalid model '{model}'. Available models: {available}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def validate_aspect_ratio(aspect_ratio: str) -> None:
|
|
50
|
+
"""Validate aspect ratio."""
|
|
51
|
+
if aspect_ratio not in ASPECT_RATIOS:
|
|
52
|
+
available = ", ".join(ASPECT_RATIOS)
|
|
53
|
+
raise ValidationError(
|
|
54
|
+
f"Invalid aspect ratio '{aspect_ratio}'. Available: {available}"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def validate_number_of_images(num: int) -> None:
|
|
59
|
+
"""Validate number of images to generate."""
|
|
60
|
+
if not isinstance(num, int) or num < 1:
|
|
61
|
+
raise ValidationError(f"Number of images must be at least 1, got {num}")
|
|
62
|
+
|
|
63
|
+
if num > MAX_IMAGES_PER_REQUEST:
|
|
64
|
+
raise ValidationError(
|
|
65
|
+
f"Number of images exceeds maximum: {num} > {MAX_IMAGES_PER_REQUEST}"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def validate_image_format(format_str: str) -> None:
|
|
70
|
+
"""Validate image format."""
|
|
71
|
+
if format_str.lower() not in IMAGE_FORMATS:
|
|
72
|
+
available = ", ".join(IMAGE_FORMATS.keys())
|
|
73
|
+
raise ValidationError(
|
|
74
|
+
f"Invalid image format '{format_str}'. Available: {available}"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def validate_person_generation(option: str) -> None:
|
|
79
|
+
"""Validate person generation option."""
|
|
80
|
+
if option not in PERSON_GENERATION_OPTIONS:
|
|
81
|
+
available = ", ".join(PERSON_GENERATION_OPTIONS)
|
|
82
|
+
raise ValidationError(
|
|
83
|
+
f"Invalid person generation option '{option}'. Available: {available}"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def validate_seed(seed: int | None) -> None:
|
|
88
|
+
"""Validate seed value."""
|
|
89
|
+
if seed is not None:
|
|
90
|
+
if not isinstance(seed, int):
|
|
91
|
+
raise ValidationError(f"Seed must be an integer, got {type(seed).__name__}")
|
|
92
|
+
if seed < 0:
|
|
93
|
+
raise ValidationError(f"Seed must be non-negative, got {seed}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def validate_file_path(path: str) -> Path:
|
|
97
|
+
"""Validate and return file path."""
|
|
98
|
+
try:
|
|
99
|
+
file_path = Path(path).resolve()
|
|
100
|
+
except Exception as e:
|
|
101
|
+
raise ValidationError(f"Invalid file path '{path}': {e}")
|
|
102
|
+
|
|
103
|
+
if not file_path.exists():
|
|
104
|
+
raise ValidationError(f"File does not exist: {file_path}")
|
|
105
|
+
|
|
106
|
+
if not file_path.is_file():
|
|
107
|
+
raise ValidationError(f"Path is not a file: {file_path}")
|
|
108
|
+
|
|
109
|
+
return file_path
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def validate_base64_image(data: str) -> None:
|
|
113
|
+
"""Validate base64-encoded image data."""
|
|
114
|
+
if not data:
|
|
115
|
+
raise ValidationError("Base64 image data cannot be empty")
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
# Try to decode to verify it's valid base64
|
|
119
|
+
decoded = base64.b64decode(data, validate=True)
|
|
120
|
+
if len(decoded) == 0:
|
|
121
|
+
raise ValidationError("Decoded image data is empty")
|
|
122
|
+
except Exception as e:
|
|
123
|
+
raise ValidationError(f"Invalid base64 image data: {e}")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def validate_prompts_list(prompts: list[str]) -> None:
|
|
127
|
+
"""Validate list of prompts for batch processing."""
|
|
128
|
+
if not isinstance(prompts, list):
|
|
129
|
+
raise ValidationError("Prompts must be a list")
|
|
130
|
+
|
|
131
|
+
if not prompts:
|
|
132
|
+
raise ValidationError("Prompts list cannot be empty")
|
|
133
|
+
|
|
134
|
+
for i, prompt in enumerate(prompts):
|
|
135
|
+
if not isinstance(prompt, str):
|
|
136
|
+
raise ValidationError(f"Prompt at index {i} must be a string")
|
|
137
|
+
try:
|
|
138
|
+
validate_prompt(prompt)
|
|
139
|
+
except ValidationError as e:
|
|
140
|
+
raise ValidationError(f"Invalid prompt at index {i}: {e}")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def sanitize_filename(filename: str) -> str:
|
|
144
|
+
"""Sanitize filename to remove unsafe characters."""
|
|
145
|
+
# Remove or replace unsafe characters
|
|
146
|
+
safe_name = re.sub(r'[<>:"/\\|?*\x00-\x1f]', "_", filename)
|
|
147
|
+
# Remove leading/trailing dots and spaces
|
|
148
|
+
safe_name = safe_name.strip(". ")
|
|
149
|
+
# Ensure filename is not empty
|
|
150
|
+
if not safe_name:
|
|
151
|
+
safe_name = "image"
|
|
152
|
+
return safe_name
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def validate_batch_size(size: int, max_size: int) -> None:
|
|
156
|
+
"""Validate batch size."""
|
|
157
|
+
if not isinstance(size, int) or size < 1:
|
|
158
|
+
raise ValidationError(f"Batch size must be at least 1, got {size}")
|
|
159
|
+
|
|
160
|
+
if size > max_size:
|
|
161
|
+
raise ValidationError(f"Batch size exceeds maximum: {size} > {max_size}")
|