ai_qc/backend/profile_config.py
nickviljoen 3fec052c12 Create frontend and backend folder structure for deployment
Organized the application into separate frontend and backend directories for cleaner deployment and better separation of concerns.

Frontend Directory (frontend/):
- index.html: Single-page web interface (renamed from web_ui.html)
- README.md: Frontend deployment guide
- Total size: ~113 KB (self-contained)
- Smart base path detection (works at / or /ai_qc/)
- No configuration changes required

Backend Directory (backend/):
- All Python files (api_server.py, llm_config.py, etc.)
- visual_qc_apps/: 33 QC check modules
- profiles/: 6 QC profile configurations
- brand_guidelines/: Reference asset storage
- config/: Environment configurations
- scripts/: Deployment automation
- uploads/, output/: Data directories
- requirements.txt, ai_qc.service, apache_config.conf
- Complete documentation

New Documentation:
- FOLDER_STRUCTURE.md: Comprehensive guide to new structure
- frontend/README.md: Frontend deployment instructions
- backend/BACKEND_README.md: Backend deployment guide

Deployment Mapping:
- frontend/ → /var/www/html/ai_qc/ (web root)
- backend/ → /opt/ai_qc/ (application directory)

Benefits:
- Clear separation of concerns
- Backend code not in web-accessible directory
- Independent frontend/backend updates
- Matches server's existing patterns (/opt/veo3, /opt/voice2text)
- Industry-standard architecture
- Easy to deploy and maintain

Original files preserved in root directory for reference.
Ready for production deployment following MIGRATION_GUIDE.md.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 11:55:53 +02:00

326 lines
No EOL
11 KiB
Python
Executable file

#!/usr/bin/env python3
"""
Centralized profile configuration module for Visual AI QC.
This script manages the profiles, QC checks, weights, and LLM assignments.
"""
import os
import json
import glob
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, field
# Profiles directory path
PROFILES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'profiles')
# Dynamic QC checks discovery
def discover_qc_checks():
"""Dynamically discover all available QC checks from the visual_qc_apps directory"""
import glob
visual_qc_apps_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'visual_qc_apps')
# Find all directories that contain an app.py file
qc_checks = []
# Look for all subdirectories in visual_qc_apps
app_dirs = glob.glob(os.path.join(visual_qc_apps_dir, '*', 'app.py'))
for app_file in app_dirs:
# Extract the directory name as the check name
check_dir = os.path.dirname(app_file)
check_name = os.path.basename(check_dir)
# Skip template files and utility files
if check_name not in ['__pycache__', 'templates', 'utils']:
qc_checks.append(check_name)
# Sort for consistency
qc_checks.sort()
print(f"Discovered {len(qc_checks)} QC checks: {qc_checks}")
return qc_checks
# List of all available QC checks (dynamically discovered)
try:
QC_CHECKS = discover_qc_checks()
except Exception as e:
print(f"Error discovering QC checks, falling back to static list: {e}")
# Fallback to static list if discovery fails
QC_CHECKS = [
# Original services
'logo_visibility',
'brand_assets_visibility',
'visual_elements_count',
'background_contrast',
'face_visibility',
'new_visibility',
'visual_hierarchy',
'supporting_images',
'curved_edges',
'visuals_left_text_right',
'face_gaze_direction',
'lowercase_text',
'call_to_action',
'word_count',
'imperative_verb',
# New services - added for the new client requirements
'file_naming',
'layer_organization',
'color_format',
'image_resolution',
'safety_area',
'element_alignment',
'animation_transitions',
'aspect_ratio',
'responsiveness',
'dark_mode_legibility',
'print_bleed',
'crop_marks',
'text_readability',
# Custom QC checks for specific requirements
'product_visibility',
'inclusive',
'accessibility',
# Format-specific checks
'curved_edges_print',
'curved_edges_digital'
]
# LLM options
LLM_OPTIONS = ["OpenAI", "Gemini"]
@dataclass
class QCCheckConfig:
"""Configuration for a QC check, including its weight and which LLM to use"""
weight: float = 0.0
llm: str = "Gemini" # Default to Gemini
enabled: bool = True
@dataclass
class Profile:
"""Profile configuration including name, description, and check configs"""
name: str
description: str
checks: Dict[str, QCCheckConfig] = field(default_factory=dict)
pre_analysis_instructions: Optional[str] = None
def get_enabled_checks(self) -> List[str]:
"""Get list of enabled check names"""
return [check_name for check_name, config in self.checks.items() if config.enabled]
def get_check_weights(self) -> Dict[str, float]:
"""Get dictionary of check weights"""
return {check_name: config.weight for check_name, config in self.checks.items() if config.enabled}
def get_check_llm(self, check_name: str) -> str:
"""Get the LLM to use for a specific check"""
if check_name in self.checks:
return self.checks[check_name].llm
return "Gemini" # Default to Gemini if not specified
# Dictionary to store all loaded profiles
PROFILES = {}
def load_profiles():
"""Load all profile JSON files from the profiles directory"""
global PROFILES
PROFILES = {} # Reset profiles dictionary
# Ensure profiles directory exists
os.makedirs(PROFILES_DIR, exist_ok=True)
# Find all JSON files in the profiles directory
profile_files = glob.glob(os.path.join(PROFILES_DIR, '*.json'))
# Load each profile file
for profile_file in profile_files:
try:
with open(profile_file, 'r') as f:
profile_data = json.load(f)
# Extract profile name, description, checks, and pre_analysis_instructions
profile_name = profile_data.get('name', 'Unnamed Profile')
profile_description = profile_data.get('description', '')
profile_checks = profile_data.get('checks', {})
pre_analysis_instructions = profile_data.get('pre_analysis_instructions', None)
# Create a new Profile instance
profile = Profile(
name=profile_name,
description=profile_description,
pre_analysis_instructions=pre_analysis_instructions
)
# Add each check configuration
for check_name, check_config in profile_checks.items():
profile.checks[check_name] = QCCheckConfig(
weight=check_config.get('weight', 0.0),
llm=check_config.get('llm', 'Gemini'),
enabled=check_config.get('enabled', True)
)
# Add profile to the PROFILES dictionary
# Use the filename (without extension) as the profile ID
profile_id = os.path.splitext(os.path.basename(profile_file))[0].lower()
PROFILES[profile_id] = profile
print(f"Loaded profile '{profile_name}' from {profile_file}")
except Exception as e:
print(f"Error loading profile from {profile_file}: {e}")
# If no profiles were loaded, create a default profile
if not PROFILES:
print("No profiles found. Creating default profile.")
default_profile = Profile(
name="All Checks",
description="Run all available QC checks"
)
# Initialize all checks with default values
for check in QC_CHECKS:
default_profile.checks[check] = QCCheckConfig()
PROFILES['default'] = default_profile
# Save the default profile to a file
save_profile('default', default_profile)
def save_profile(profile_id: str, profile: Profile):
"""Save a profile to a JSON file"""
# Create the profile data dictionary
profile_data = {
'name': profile.name,
'description': profile.description,
'checks': {}
}
# Add pre_analysis_instructions if it exists
if profile.pre_analysis_instructions:
profile_data['pre_analysis_instructions'] = profile.pre_analysis_instructions
# Add each check configuration
for check_name, check_config in profile.checks.items():
profile_data['checks'][check_name] = {
'weight': check_config.weight,
'llm': check_config.llm,
'enabled': check_config.enabled
}
# Save to a JSON file
profile_file = os.path.join(PROFILES_DIR, f"{profile_id.lower()}.json")
with open(profile_file, 'w') as f:
json.dump(profile_data, f, indent=4)
print(f"Saved profile '{profile.name}' to {profile_file}")
def get_profile(profile_name: str) -> Profile:
"""Get a profile by name"""
# If profiles haven't been loaded yet, load them
if not PROFILES:
load_profiles()
return PROFILES.get(profile_name.lower(), PROFILES.get('default'))
def add_profile(name: str, description: str, check_configs: Dict[str, Dict[str, Any]]) -> str:
"""Add a new profile and save it to a JSON file
Returns the profile_id that was created
"""
# Create a new Profile instance
profile = Profile(
name=name,
description=description
)
# Add each check configuration
for check_name, config in check_configs.items():
profile.checks[check_name] = QCCheckConfig(
weight=config.get('weight', 0.0),
llm=config.get('llm', 'Gemini'),
enabled=config.get('enabled', True)
)
# Generate a profile_id from the name
profile_id = name.lower().replace(' ', '_')
# Add to the PROFILES dictionary
PROFILES[profile_id] = profile
# Save to a JSON file
save_profile(profile_id, profile)
return profile_id
def update_profile(profile_name: str, updates: Dict[str, Any]) -> bool:
"""Update an existing profile"""
if profile_name not in PROFILES:
return False
profile = PROFILES[profile_name]
if 'name' in updates:
profile.name = updates['name']
if 'description' in updates:
profile.description = updates['description']
if 'checks' in updates:
for check_name, check_config in updates['checks'].items():
if check_name in profile.checks:
if 'weight' in check_config:
profile.checks[check_name].weight = check_config['weight']
if 'llm' in check_config:
profile.checks[check_name].llm = check_config['llm']
if 'enabled' in check_config:
profile.checks[check_name].enabled = check_config['enabled']
# Save the updated profile
save_profile(profile_name, profile)
return True
def delete_profile(profile_name: str) -> bool:
"""Delete a profile file and remove it from memory"""
if profile_name not in PROFILES or profile_name == 'default':
return False
# Remove from memory
profile = PROFILES.pop(profile_name)
# Delete the file
profile_file = os.path.join(PROFILES_DIR, f"{profile_name.lower()}.json")
if os.path.exists(profile_file):
os.remove(profile_file)
print(f"Deleted profile file: {profile_file}")
return True
def get_profile_summary() -> Dict[str, Dict[str, Any]]:
"""Get a summary of all profiles"""
# If profiles haven't been loaded yet, load them
if not PROFILES:
load_profiles()
summary = {}
for profile_name, profile in PROFILES.items():
summary[profile_name] = {
'name': profile.name,
'description': profile.description,
'enabled_checks': profile.get_enabled_checks(),
'total_checks': len(profile.checks),
'enabled_count': len(profile.get_enabled_checks())
}
return summary
def get_check_llm_map(profile_name: str) -> Dict[str, str]:
"""Get a mapping of check names to LLM names for a profile"""
profile = get_profile(profile_name)
return {check_name: config.llm for check_name, config in profile.checks.items()}
# Load profiles when the module is imported
load_profiles()