File size: 12,879 Bytes
5b6e956
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
"""
Input Validation Utilities
===========================

Validation functions for user inputs in Nano Banana Streamlit.
Ensures data integrity and provides clear error messages.
"""

from typing import Optional, List, Tuple
from pathlib import Path
from PIL import Image

from config.settings import Settings
from utils.logging_utils import get_logger


logger = get_logger(__name__)


# =============================================================================
# PARAMETER VALIDATION
# =============================================================================

def validate_temperature(temperature: float) -> Tuple[bool, Optional[str]]:
    """
    Validate temperature parameter.

    Args:
        temperature: Temperature value to validate

    Returns:
        Tuple of (is_valid, error_message)
        error_message is None if valid
    """
    if not isinstance(temperature, (int, float)):
        return False, "Temperature must be a number"

    if temperature < Settings.MIN_TEMPERATURE or temperature > Settings.MAX_TEMPERATURE:
        return False, f"Temperature must be between {Settings.MIN_TEMPERATURE} and {Settings.MAX_TEMPERATURE}"

    return True, None


def validate_aspect_ratio(aspect_ratio: str) -> Tuple[bool, Optional[str]]:
    """
    Validate aspect ratio parameter.

    Args:
        aspect_ratio: Aspect ratio string (display name or value)

    Returns:
        Tuple of (is_valid, error_message)
    """
    if not isinstance(aspect_ratio, str):
        return False, "Aspect ratio must be a string"

    # Check if it's a display name
    if aspect_ratio in Settings.ASPECT_RATIOS:
        return True, None

    # Check if it's a ratio value
    if aspect_ratio in Settings.ASPECT_RATIOS.values():
        return True, None

    return False, f"Invalid aspect ratio: {aspect_ratio}"


def validate_backend(backend: str) -> Tuple[bool, Optional[str]]:
    """
    Validate backend parameter.

    Args:
        backend: Backend name

    Returns:
        Tuple of (is_valid, error_message)
    """
    if not isinstance(backend, str):
        return False, "Backend must be a string"

    if backend not in Settings.AVAILABLE_BACKENDS:
        return False, f"Invalid backend: {backend}. Must be one of {Settings.AVAILABLE_BACKENDS}"

    return True, None


def validate_prompt(prompt: str, min_length: int = 1, max_length: int = 5000) -> Tuple[bool, Optional[str]]:
    """
    Validate text prompt.

    Args:
        prompt: Text prompt
        min_length: Minimum required length (default: 1)
        max_length: Maximum allowed length (default: 5000)

    Returns:
        Tuple of (is_valid, error_message)
    """
    if not isinstance(prompt, str):
        return False, "Prompt must be a string"

    prompt = prompt.strip()

    if len(prompt) < min_length:
        return False, f"Prompt must be at least {min_length} character(s)"

    if len(prompt) > max_length:
        return False, f"Prompt must be at most {max_length} characters"

    return True, None


def validate_character_name(name: str) -> Tuple[bool, Optional[str]]:
    """
    Validate character name.

    Args:
        name: Character name

    Returns:
        Tuple of (is_valid, error_message)
    """
    if not isinstance(name, str):
        return False, "Character name must be a string"

    name = name.strip()

    if len(name) < 1:
        return False, "Character name cannot be empty"

    if len(name) > 100:
        return False, "Character name must be at most 100 characters"

    return True, None


# =============================================================================
# IMAGE VALIDATION
# =============================================================================

def validate_image(image: Image.Image) -> Tuple[bool, Optional[str]]:
    """
    Validate PIL Image object.

    Checks:
    - Is valid Image instance
    - Has reasonable dimensions
    - Is in supported format

    Args:
        image: PIL Image to validate

    Returns:
        Tuple of (is_valid, error_message)
    """
    if not isinstance(image, Image.Image):
        return False, "Invalid image object"

    # Check dimensions
    width, height = image.size

    if width < 1 or height < 1:
        return False, "Image has invalid dimensions"

    if width > 8192 or height > 8192:
        return False, "Image is too large (max 8192x8192 pixels)"

    # Check mode (format)
    if image.mode not in ['RGB', 'RGBA', 'L', 'P']:
        return False, f"Unsupported image mode: {image.mode}"

    return True, None


def validate_image_file(file_path: Path) -> Tuple[bool, Optional[str]]:
    """
    Validate image file path and format.

    Args:
        file_path: Path to image file

    Returns:
        Tuple of (is_valid, error_message)
    """
    if not isinstance(file_path, Path):
        try:
            file_path = Path(file_path)
        except Exception:
            return False, "Invalid file path"

    # Check exists
    if not file_path.exists():
        return False, f"File not found: {file_path}"

    # Check is file (not directory)
    if not file_path.is_file():
        return False, f"Not a file: {file_path}"

    # Check extension
    valid_extensions = {'.png', '.jpg', '.jpeg', '.webp', '.bmp'}
    if file_path.suffix.lower() not in valid_extensions:
        return False, f"Unsupported file format: {file_path.suffix}"

    # Try to open as image
    try:
        with Image.open(file_path) as img:
            return validate_image(img)
    except Exception as e:
        return False, f"Cannot open as image: {e}"


def validate_image_upload_size(file_size_bytes: int) -> Tuple[bool, Optional[str]]:
    """
    Validate uploaded file size.

    Args:
        file_size_bytes: File size in bytes

    Returns:
        Tuple of (is_valid, error_message)
    """
    max_bytes = Settings.MAX_IMAGE_UPLOAD_SIZE * 1024 * 1024  # Convert MB to bytes

    if file_size_bytes > max_bytes:
        max_mb = Settings.MAX_IMAGE_UPLOAD_SIZE
        actual_mb = file_size_bytes / (1024 * 1024)
        return False, f"File too large: {actual_mb:.1f}MB (max: {max_mb}MB)"

    return True, None


# =============================================================================
# GENERATION REQUEST VALIDATION
# =============================================================================

def validate_generation_request(
    prompt: str,
    backend: str,
    aspect_ratio: str,
    temperature: float,
    input_images: Optional[List[Image.Image]] = None
) -> Tuple[bool, Optional[str]]:
    """
    Validate a complete generation request.

    Validates all parameters required for image generation.

    Args:
        prompt: Text prompt
        backend: Backend name
        aspect_ratio: Aspect ratio
        temperature: Temperature value
        input_images: Optional list of input images

    Returns:
        Tuple of (is_valid, error_message)
        error_message is None if valid
    """
    # Validate prompt
    valid, error = validate_prompt(prompt)
    if not valid:
        return False, f"Invalid prompt: {error}"

    # Validate backend
    valid, error = validate_backend(backend)
    if not valid:
        return False, f"Invalid backend: {error}"

    # Validate aspect ratio
    valid, error = validate_aspect_ratio(aspect_ratio)
    if not valid:
        return False, f"Invalid aspect ratio: {error}"

    # Validate temperature
    valid, error = validate_temperature(temperature)
    if not valid:
        return False, f"Invalid temperature: {error}"

    # Validate input images if provided
    if input_images:
        if not isinstance(input_images, list):
            return False, "Input images must be a list"

        if len(input_images) > 3:
            return False, "Maximum 3 input images allowed"

        for idx, img in enumerate(input_images, 1):
            valid, error = validate_image(img)
            if not valid:
                return False, f"Invalid input image {idx}: {error}"

    return True, None


def validate_character_forge_request(
    character_name: str,
    initial_image: Optional[Image.Image],
    face_image: Optional[Image.Image],
    body_image: Optional[Image.Image],
    image_type: str,
    backend: str
) -> Tuple[bool, Optional[str]]:
    """
    Validate a Character Forge generation request.

    Args:
        character_name: Name for character
        initial_image: Initial image (for Face Only / Full Body modes)
        face_image: Face image (for Face+Body Separate mode)
        body_image: Body image (for Face+Body Separate mode)
        image_type: Type of input ("Face Only", "Full Body", "Face + Body (Separate)")
        backend: Backend to use

    Returns:
        Tuple of (is_valid, error_message)
    """
    # Validate character name
    valid, error = validate_character_name(character_name)
    if not valid:
        return False, error

    # Validate backend
    valid, error = validate_backend(backend)
    if not valid:
        return False, error

    # Validate images based on mode
    if image_type == "Face + Body (Separate)":
        if face_image is None:
            return False, "Face image is required for Face+Body Separate mode"
        if body_image is None:
            return False, "Body image is required for Face+Body Separate mode"

        valid, error = validate_image(face_image)
        if not valid:
            return False, f"Invalid face image: {error}"

        valid, error = validate_image(body_image)
        if not valid:
            return False, f"Invalid body image: {error}"

    else:  # Face Only or Full Body
        if initial_image is None:
            return False, f"Initial image is required for {image_type} mode"

        valid, error = validate_image(initial_image)
        if not valid:
            return False, f"Invalid initial image: {error}"

    return True, None


# =============================================================================
# BACKEND AVAILABILITY VALIDATION
# =============================================================================

def validate_backend_available(backend: str, api_key: Optional[str] = None) -> Tuple[bool, Optional[str]]:
    """
    Check if a backend is available and properly configured.

    Args:
        backend: Backend name
        api_key: API key (for Gemini backend)

    Returns:
        Tuple of (is_available, error_message)
    """
    # Validate backend name first
    valid, error = validate_backend(backend)
    if not valid:
        return False, error

    # Check Gemini API
    if backend == Settings.BACKEND_GEMINI:
        if not api_key:
            return False, "Gemini API key not configured. Please set GEMINI_API_KEY or enter it in settings."
        return True, None

    # Check OmniGen2
    if backend == Settings.BACKEND_OMNIGEN2:
        # Try to check if server is running
        try:
            import requests
            response = requests.get(f"{Settings.OMNIGEN2_BASE_URL}/health", timeout=2)
            if response.ok:
                data = response.json()
                if data.get('status') == 'healthy':
                    return True, None
                else:
                    return False, "OmniGen2 server is not healthy. Check server.log for details."
            else:
                return False, f"OmniGen2 server returned error: {response.status_code}"
        except Exception as e:
            return False, f"OmniGen2 server not responding. Start it with: omnigen2_plugin/server.bat start"

    return False, f"Unknown backend: {backend}"


# =============================================================================
# HELPER FUNCTIONS
# =============================================================================

def raise_if_invalid(is_valid: bool, error_message: Optional[str], exception_type=ValueError):
    """
    Raise an exception if validation failed.

    Helper function for turning validation results into exceptions.

    Args:
        is_valid: Validation result
        error_message: Error message (if invalid)
        exception_type: Exception class to raise (default: ValueError)

    Raises:
        exception_type: If is_valid is False
    """
    if not is_valid:
        logger.error(f"Validation failed: {error_message}")
        raise exception_type(error_message)


def log_validation_error(validation_result: Tuple[bool, Optional[str]], context: str = ""):
    """
    Log a validation error if validation failed.

    Args:
        validation_result: Result tuple from validation function
        context: Optional context string for the log message
    """
    is_valid, error_message = validation_result
    if not is_valid:
        if context:
            logger.warning(f"Validation failed [{context}]: {error_message}")
        else:
            logger.warning(f"Validation failed: {error_message}")