""" Batch Agent Orchestrator — DefectDiffu Edition Usage: # Scenario B: Global description for entire folder python batch_agent_orchestrator.py \ --input-dir "C:/TestingImage/vcsel_batch" \ --product-desc "VCSEL laser diode with emission aperture and surrounding mesa" \ --output-dir "C:/AgentOutput" \ --defectdiffu-ckpt "./defectdiffu_ckpt.pt" \ --vae-path "./sd-vae-ft-mse" \ --device cuda # Scenario A: CSV manifest python batch_agent_orchestrator.py \\ --input-dir "C:/TestingImage" \\ --manifest "C:/products.csv" \\ --output-dir "C:/AgentOutput" \\ --defectdiffu-ckpt "./defectdiffu_ckpt.pt" \\ --vae-path "./sd-vae-ft-mse" # Mode 3: Infer from folder names python batch_agent_orchestrator.py \\ --input-dir "C:/TestingImage" \\ --output-dir "C:/AgentOutput" \\ --defectdiffu-ckpt "./defectdiffu_ckpt.pt" \\ --vae-path "./sd-vae-ft-mse" """ import os import sys import csv import json import argparse import traceback from pathlib import Path from datetime import datetime from typing import Dict, List, Optional from tqdm import tqdm import numpy as np from PIL import Image SCRIPT_DIR = Path(__file__).parent if str(SCRIPT_DIR) not in sys.path: sys.path.insert(0, str(SCRIPT_DIR)) from artiagent_orchestrator import ArtiAgentOrchestrator def infer_product_from_path(image_path: Path) -> str: """Infer product description from folder structure or filename.""" parent = image_path.parent.name.lower() if parent and parent not in ['.', '', 'images', 'imgs', 'data', 'input']: return parent.replace('_', ' ').replace('-', ' ') stem = image_path.stem.lower() for keyword in ['vcsel', 'lens', 'die', 'photodiode', 'sensor', 'chip', 'led', 'laser', 'optical']: if keyword in stem: return keyword return "electronic component" def load_manifest(manifest_path: str) -> Dict[str, str]: """Load CSV manifest mapping image paths to product descriptions.""" manifest = {} with open(manifest_path, 'r', encoding='utf-8') as f: reader = csv.DictReader(f) for row in reader: img_path = row.get('image_path', row.get('path', row.get('image', ''))).strip() desc = row.get('product_description', row.get('description', row.get('product', ''))).strip() if img_path and desc: manifest[Path(img_path).resolve()] = desc print(f"[Batch] Loaded manifest with {len(manifest)} entries") return manifest def discover_images(input_dir: str, extensions=('.png', '.jpg', '.jpeg', '.bmp', '.tif', '.tiff')) -> List[Path]: """Recursively discover all images in input directory.""" input_path = Path(input_dir) images = [] for ext in extensions: images.extend(input_path.rglob(f"*{ext}")) images.extend(input_path.rglob(f"*{ext.upper()}")) unique = sorted(set(images)) print(f"[Batch] Discovered {len(unique)} images in {input_dir}") return unique def run_batch( input_dir: str, output_dir: str, defectdiffu_ckpt: str, vae_path: str, product_desc: Optional[str] = None, manifest_path: Optional[str] = None, defect_type: Optional[str] = None, max_defects_per_image: int = 3, device: str = 'cuda', vlm_model: str = 'gemma3:12b', image_size: int = 512, num_steps: int = 50, resume: bool = False, save_failed: bool = True ): """Run agent orchestrator over all images in input directory.""" timestamp = datetime.now().strftime("%Y%m%d_%H%M") output_path = Path(output_dir) / timestamp output_path.mkdir(parents=True, exist_ok=True) print(f"[Batch] Output folder: {output_path}") manifest = {} if product_desc and not manifest_path: print(f"[Batch] Scenario B Active: Global Description = '{product_desc}'") elif manifest_path and os.path.exists(manifest_path): manifest = load_manifest(manifest_path) print(f"[Batch] Scenario A Active: CSV Manifest ({len(manifest)} entries)") elif product_desc: print(f"[Batch] Scenario B Active (Fallback): Global Description = '{product_desc}'") else: print("[Batch] Scenario C Active: Folder Name Inference (no description provided)") images = discover_images(input_dir) if not images: print("[Batch] No images found. Exiting.") return progress_file = output_path / "batch_progress.json" processed_ids = set() if resume and progress_file.exists(): with open(progress_file, 'r') as f: progress = json.load(f) processed_ids = set(progress.get('processed_paths', [])) print(f"[Batch] Resuming: {len(processed_ids)} images already processed") orchestrator = ArtiAgentOrchestrator( device=device, output_dir=str(output_path), vlm_model=vlm_model, defectdiffu_ckpt=defectdiffu_ckpt, vae_path=vae_path, image_size=image_size, num_steps=num_steps ) stats = { 'total': len(images), 'processed': 0, 'successful': 0, 'failed': 0, 'defects_generated': 0, 'start_time': datetime.now().isoformat(), 'processed_paths': [], 'failed_images': [] } if resume: images = [img for img in images if str(img.resolve()) not in processed_ids] print(f"[Batch] Processing {len(images)} images...") print(f"[Batch] Max defects per image: {max_defects_per_image}") print("=" * 70) for img_path in tqdm(images, desc="Agent Batch Processing"): img_key = str(img_path.resolve()) try: if img_key in manifest: desc = manifest[img_key] source = "manifest" elif product_desc: desc = product_desc source = "global" else: desc = infer_product_from_path(img_path) source = "inferred" print(f"\\n[Batch] Processing: {img_path.name} | desc source: {source}") if source in ['inferred', 'global']: print(f"[Batch] Using description: '{desc}'") result = orchestrator.run( product_description=desc, image_path=str(img_path), max_defects=max_defects_per_image, defect_type=defect_type ) successful_defects = sum(1 for r in result['results'] if r['success']) stats['processed'] += 1 stats['successful'] += 1 if successful_defects > 0 else 0 stats['defects_generated'] += successful_defects stats['processed_paths'].append(img_key) if successful_defects == 0: stats['failed'] += 1 stats['failed_images'].append({'path': img_key, 'reason': 'no_defects_generated'}) if stats['processed'] % 5 == 0: with open(progress_file, 'w') as f: json.dump(stats, f, indent=2) except Exception as e: stats['failed'] += 1 stats['failed_images'].append({'path': img_key, 'reason': str(e)}) print(f"[Batch] FAILED: {img_path.name} -> {str(e)}") if save_failed: fail_dir = output_path / "_failed" / img_path.stem fail_dir.mkdir(parents=True, exist_ok=True) with open(fail_dir / "error.txt", 'w') as f: f.write(traceback.format_exc()) with open(progress_file, 'w') as f: json.dump(stats, f, indent=2) orchestrator.cleanup() elapsed = (datetime.now() - datetime.fromisoformat(stats['start_time'])).total_seconds() hours = int(elapsed // 3600) minutes = int((elapsed % 3600) // 60) seconds = int(elapsed % 60) print("\\n" + "=" * 70) print("BATCH ORCHESTRATION COMPLETE") print("=" * 70) print(f"Total images: {stats['total']}") print(f"Processed: {stats['processed']}") print(f"Successful: {stats['successful']}") print(f"Failed: {stats['failed']}") print(f"Defects generated: {stats['defects_generated']}") print(f"Total time: {hours}h {minutes}m {seconds}s") print(f"Output directory: {output_path}") print("=" * 70) def main(): parser = argparse.ArgumentParser(description='Batch Agent-Driven Defect Generation (DefectDiffu)') # Input / Output parser.add_argument('--input-dir', required=True, help='Directory containing clean product images') parser.add_argument('--output-dir', required=True, help='Output directory for all defect images') # DefectDiffu model paths (REQUIRED) parser.add_argument('--defectdiffu-ckpt', required=True, help='Path to trained DefectDiffu checkpoint') parser.add_argument('--vae-path', required=True, help='Path to Stable Diffusion VAE (e.g. stabilityai/sd-vae-ft-mse)') # Description sources parser.add_argument('--product-desc', default=None, help='[Scenario B] ONE global description applied to ALL images in the folder') parser.add_argument('--manifest', default=None, help='[Scenario A] CSV manifest with columns: image_path,product_description') # Defect control parser.add_argument('--defect-type', default=None, help='Specify defect type for batch generation (e.g., bubble, scratch)') parser.add_argument('--max-defects-per-image', type=int, default=3, help='Maximum defects to generate per image (default: 3)') # Generation control parser.add_argument('--device', default='cuda', help='Device (cuda/cpu)') parser.add_argument('--vlm-model', default='gemma3:12b', help='Local VLM model') parser.add_argument('--image-size', type=int, default=512, help='DefectDiffu generation resolution (default: 512)') parser.add_argument('--num-steps', type=int, default=50, help='Denoising steps for DefectDiffu (default: 50)') parser.add_argument('--resume', action='store_true', help='Resume from previous batch run') parser.add_argument('--no-save-failed', action='store_true', help='Do not save failed case logs') args = parser.parse_args() run_batch( input_dir=args.input_dir, output_dir=args.output_dir, defectdiffu_ckpt=args.defectdiffu_ckpt, vae_path=args.vae_path, product_desc=args.product_desc, manifest_path=args.manifest, defect_type=args.defect_type, max_defects_per_image=args.max_defects_per_image, device=args.device, vlm_model=args.vlm_model, image_size=args.image_size, num_steps=args.num_steps, resume=args.resume, save_failed=not args.no_save_failed ) if __name__ == "__main__": main()