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:
nickviljoen 2026-03-21 16:50:35 +02:00
parent 9ce44981eb
commit 1c582ffcf4
5 changed files with 140 additions and 84 deletions

View file

@ -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']

View 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)}
)

View file

@ -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:

View file

@ -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

View file

@ -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');
}