Batch 2: Simplify to single profile, fix multi-file batch execution
- Replace 3 profiles with single "H&M Image Check" (dimension_check + image_quality) - Remove filename_parse check (pattern didn't match actual filenames) - Create DimensionCheck class for image dimension validation - Fix configure page to route multi-file uploads to batch endpoint - Auto-select single profile, show file list on configure page Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9ce44981eb
commit
1c582ffcf4
5 changed files with 140 additions and 84 deletions
|
|
@ -3,8 +3,9 @@
|
|||
# Base check class
|
||||
from .base_check import BaseCheck
|
||||
|
||||
# Sample checks
|
||||
# Checks
|
||||
from .sample_filename_check import FilenameCheck
|
||||
from .sample_quality_check import QualityCheck
|
||||
from .dimension_check import DimensionCheck
|
||||
|
||||
__all__ = ['BaseCheck', 'FilenameCheck', 'QualityCheck']
|
||||
__all__ = ['BaseCheck', 'FilenameCheck', 'QualityCheck', 'DimensionCheck']
|
||||
|
|
|
|||
86
modules/hm_qc/checks/dimension_check.py
Normal file
86
modules/hm_qc/checks/dimension_check.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
"""
|
||||
Dimension Check.
|
||||
|
||||
Validates image dimensions and integrity using PIL.
|
||||
"""
|
||||
import os
|
||||
from typing import Dict, Any
|
||||
from PIL import Image
|
||||
from .base_check import BaseCheck
|
||||
|
||||
|
||||
class DimensionCheck(BaseCheck):
|
||||
"""Check image dimensions and validate integrity."""
|
||||
|
||||
# Minimum acceptable dimensions (in pixels)
|
||||
MIN_WIDTH = 100
|
||||
MIN_HEIGHT = 100
|
||||
|
||||
def __init__(self, name: str = "dimension_check", weight: float = 50.0, config: Dict[str, Any] = None):
|
||||
super().__init__(name, weight, config)
|
||||
|
||||
def run(self, file_path: str, context: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Run dimension validation on an image file."""
|
||||
try:
|
||||
ext = os.path.splitext(file_path)[1].lower()
|
||||
|
||||
if ext not in ['.jpg', '.jpeg', '.png', '.psd']:
|
||||
return self._create_result(
|
||||
status='skipped',
|
||||
score=100.0,
|
||||
message='Dimension check only applies to image files',
|
||||
details={'file_type': ext}
|
||||
)
|
||||
|
||||
img = Image.open(file_path)
|
||||
width, height = img.size
|
||||
mode = img.mode
|
||||
fmt = img.format or ext.replace('.', '').upper()
|
||||
|
||||
# Store in context for other checks
|
||||
context['image_dimensions'] = {'width': width, 'height': height}
|
||||
context['image_info'] = {'mode': mode, 'format': fmt}
|
||||
|
||||
# Validate dimensions
|
||||
issues = []
|
||||
if width < self.MIN_WIDTH:
|
||||
issues.append(f'Width {width}px is below minimum {self.MIN_WIDTH}px')
|
||||
if height < self.MIN_HEIGHT:
|
||||
issues.append(f'Height {height}px is below minimum {self.MIN_HEIGHT}px')
|
||||
|
||||
if issues:
|
||||
return self._create_result(
|
||||
status='failed',
|
||||
score=30.0,
|
||||
message=f'Image dimensions too small: {width}x{height}',
|
||||
details={
|
||||
'width': width,
|
||||
'height': height,
|
||||
'format': fmt,
|
||||
'mode': mode,
|
||||
'issues': issues
|
||||
},
|
||||
recommendations=['Provide a higher resolution image']
|
||||
)
|
||||
|
||||
return self._create_result(
|
||||
status='passed',
|
||||
score=100.0,
|
||||
message=f'Image dimensions valid: {width}x{height} ({fmt})',
|
||||
details={
|
||||
'width': width,
|
||||
'height': height,
|
||||
'format': fmt,
|
||||
'mode': mode,
|
||||
'megapixels': round((width * height) / 1_000_000, 2)
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Dimension check error: {e}")
|
||||
return self._create_result(
|
||||
status='error',
|
||||
score=0.0,
|
||||
message=f'Error reading image: {str(e)}',
|
||||
details={'error': str(e)}
|
||||
)
|
||||
|
|
@ -11,7 +11,7 @@ import logging
|
|||
from datetime import datetime
|
||||
from typing import Dict, List, Any
|
||||
from .scoring import ScoringEngine
|
||||
from .checks import FilenameCheck, QualityCheck
|
||||
from .checks import FilenameCheck, QualityCheck, DimensionCheck
|
||||
from core.utils.progress_tracker import UnifiedProgressTracker
|
||||
from core.models.qc_report import QCReport
|
||||
from core.models.database import db
|
||||
|
|
@ -126,10 +126,11 @@ class QCExecutor:
|
|||
checks = []
|
||||
profile_checks = self.profile.get('checks', [])
|
||||
|
||||
# Map check names to classes (in real implementation, this would be dynamic)
|
||||
check_map = {
|
||||
'filename_parse': FilenameCheck,
|
||||
'quality_check': QualityCheck
|
||||
'quality_check': QualityCheck,
|
||||
'image_quality': QualityCheck,
|
||||
'dimension_check': DimensionCheck
|
||||
}
|
||||
|
||||
for check_config in profile_checks:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# HM QC Profiles with Weighted Scoring
|
||||
# HM QC Profiles
|
||||
#
|
||||
# Each profile defines:
|
||||
# - name: Profile display name
|
||||
|
|
@ -6,81 +6,22 @@
|
|||
# - checks: List of checks with weights and LLM configuration
|
||||
|
||||
profiles:
|
||||
standard_pdf:
|
||||
name: "Standard PDF QC (Demo)"
|
||||
description: "Demo profile with 2 sample checks"
|
||||
checks:
|
||||
- name: "filename_parse"
|
||||
weight: 40
|
||||
enabled: true
|
||||
llm_provider: null
|
||||
description: "Validate H&M filename conventions"
|
||||
|
||||
- name: "quality_check"
|
||||
weight: 60
|
||||
enabled: true
|
||||
llm_provider: "openai"
|
||||
llm_model: "gpt-4o"
|
||||
description: "AI-powered quality assessment"
|
||||
|
||||
standard_image:
|
||||
name: "Standard Image QC"
|
||||
hm_image_check:
|
||||
name: "H&M Image Check"
|
||||
description: "Quality checks for H&M image assets (JPG, PNG, PSD)"
|
||||
checks:
|
||||
- name: "image_parse"
|
||||
weight: 5
|
||||
enabled: true
|
||||
llm_provider: null
|
||||
description: "Parse image metadata and properties"
|
||||
|
||||
- name: "filename_parse"
|
||||
weight: 10
|
||||
enabled: true
|
||||
llm_provider: null
|
||||
description: "Validate H&M filename conventions"
|
||||
|
||||
- name: "dimension_check"
|
||||
weight: 15
|
||||
weight: 50
|
||||
enabled: true
|
||||
llm_provider: null
|
||||
description: "Verify image dimensions match filename"
|
||||
description: "Verify image dimensions and integrity"
|
||||
|
||||
- name: "image_quality"
|
||||
weight: 30
|
||||
weight: 50
|
||||
enabled: true
|
||||
llm_provider: "openai"
|
||||
llm_model: "gpt-4o"
|
||||
description: "Assess image quality and resolution"
|
||||
description: "AI-powered image quality and legibility assessment"
|
||||
|
||||
- name: "censorship_check"
|
||||
weight: 40
|
||||
enabled: true
|
||||
llm_provider: "openai"
|
||||
llm_model: "gpt-4o"
|
||||
description: "Check body coverage requirements (CEN markets only)"
|
||||
|
||||
quick_check:
|
||||
name: "Quick Check"
|
||||
description: "Fast validation of essential requirements only"
|
||||
checks:
|
||||
- name: "filename_parse"
|
||||
weight: 30
|
||||
enabled: true
|
||||
llm_provider: null
|
||||
description: "Validate filename conventions"
|
||||
|
||||
- name: "dimension_check"
|
||||
weight: 35
|
||||
enabled: true
|
||||
llm_provider: null
|
||||
description: "Verify dimensions"
|
||||
|
||||
- name: "language_validate"
|
||||
weight: 35
|
||||
enabled: true
|
||||
llm_provider: "openai"
|
||||
llm_model: "gpt-4o"
|
||||
description: "Validate language"
|
||||
|
||||
# Note: Weights should sum to approximately 100 for each profile
|
||||
# Note: Weights should sum to 100 for each profile
|
||||
# Higher weight = more important to overall score
|
||||
|
|
|
|||
|
|
@ -4,28 +4,39 @@
|
|||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h2><i class="bi bi-gear me-2"></i>Configure QC Checks</h2>
|
||||
<p class="text-muted">Select a QC profile and customize settings</p>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h2 class="mb-0"><i class="bi bi-gear me-2"></i>Configure QC Checks</h2>
|
||||
<p class="text-muted mb-0">Review settings and start execution</p>
|
||||
</div>
|
||||
<a href="{{ url_for('hm_qc.upload') }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>Back
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-list-check me-2"></i>
|
||||
Select QC Profile
|
||||
QC Settings
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="configForm">
|
||||
<div class="mb-3">
|
||||
<label for="profile" class="form-label">QC Profile</label>
|
||||
<select class="form-select" id="profile" required>
|
||||
<option value="">-- Select Profile --</option>
|
||||
{% for profile_id, profile_data in profiles.items() %}
|
||||
<option value="{{ profile_id }}">
|
||||
{{ profile_data.name }} - {{ profile_data.description }}
|
||||
<option value="{{ profile_id }}" {% if loop.first %}selected{% endif %}>
|
||||
{{ profile_data.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% for profile_id, profile_data in profiles.items() %}
|
||||
{% if loop.first %}
|
||||
<div class="form-text">{{ profile_data.description }}</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
|
|
@ -49,11 +60,18 @@
|
|||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
Session Info
|
||||
<i class="bi bi-files me-2"></i>
|
||||
Files to Check
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>Session ID:</strong><br><small class="text-muted">{{ session_id }}</small></p>
|
||||
<p><strong>{{ file_count }}</strong> file{{ 's' if file_count != 1 }} uploaded</p>
|
||||
{% if filenames %}
|
||||
<ul class="list-unstyled small text-muted" style="max-height: 200px; overflow-y: auto;">
|
||||
{% for f in filenames %}
|
||||
<li><i class="bi bi-file-earmark me-1"></i>{{ f }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -64,6 +82,7 @@
|
|||
{% block extra_scripts %}
|
||||
<script>
|
||||
const sessionId = '{{ session_id }}';
|
||||
const fileCount = {{ file_count }};
|
||||
|
||||
document.getElementById('configForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -80,8 +99,17 @@
|
|||
startBtn.disabled = true;
|
||||
startBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Starting...';
|
||||
|
||||
// Use batch endpoint if multiple files
|
||||
const endpoint = fileCount > 1
|
||||
? `${BASE_URL}/hm-qc/execute/batch`
|
||||
: `${BASE_URL}/hm-qc/execute`;
|
||||
|
||||
const resultsUrl = fileCount > 1
|
||||
? `${BASE_URL}/hm-qc/results/batch/${sessionId}`
|
||||
: `${BASE_URL}/hm-qc/results/${sessionId}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/hm-qc/execute`, {
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
|
|
@ -95,8 +123,7 @@
|
|||
|
||||
if (data.success) {
|
||||
showNotification('QC execution started!', 'success');
|
||||
// Redirect to results page which will show progress
|
||||
window.location.href = `${BASE_URL}/hm-qc/results/${sessionId}`;
|
||||
window.location.href = resultsUrl;
|
||||
} else {
|
||||
throw new Error(data.error || 'Failed to start execution');
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue