Spaces:
Sleeping
Sleeping
| import os | |
| import subprocess | |
| import shutil | |
| from pathlib import Path | |
| from typing import List, Dict, Optional | |
| import numpy as np | |
| class AssetProcessor: | |
| """Base class for all asset-level transformations (ImageMagick, Inkscape, etc.).""" | |
| def __init__(self, name: str): | |
| self.name = name | |
| self.available = self._check_availability() | |
| def _check_availability(self) -> bool: | |
| return shutil.which(self.name.lower()) is not None | |
| def process(self, input_path: Path, output_path: Path, options: Dict = None) -> bool: | |
| """Process a single asset.""" | |
| raise NotImplementedError("Subclasses must implement process()") | |
| class ImageMagickProcessor(AssetProcessor): | |
| def __init__(self): | |
| # On Linux it's often 'magick' or 'convert' | |
| name = "magick" if shutil.which("magick") else "convert" | |
| super().__init__(name) | |
| def process(self, input_path: Path, output_path: Path, options: Dict = None) -> bool: | |
| if not self.available: | |
| return False | |
| # Example options: {"filters": "-sepia-tone 80% -charcoal 2"} | |
| filters = options.get("filters", "").split() if options else [] | |
| cmd = [self.name, str(input_path)] + filters + [str(output_path)] | |
| try: | |
| subprocess.run(cmd, check=True, capture_output=True) | |
| return True | |
| except Exception as e: | |
| print(f" [AssetProcessor] ImageMagick error: {e}") | |
| return False | |
| class InkscapeProcessor(AssetProcessor): | |
| def __init__(self): | |
| super().__init__("inkscape") | |
| def process(self, input_path: Path, output_path: Path, options: Dict = None) -> bool: | |
| if not self.available: | |
| return False | |
| # Export SVG to PNG | |
| cmd = [self.name, "--export-type=png", "--export-filename=" + str(output_path), str(input_path)] | |
| try: | |
| subprocess.run(cmd, check=True, capture_output=True) | |
| return True | |
| except Exception as e: | |
| print(f" [AssetProcessor] Inkscape error: {e}") | |
| return False | |
| class AsepriteProcessor(AssetProcessor): | |
| def __init__(self): | |
| super().__init__("aseprite") | |
| def process(self, input_path: Path, output_path: Path, options: Dict = None) -> bool: | |
| if not self.available: | |
| return False | |
| # Example: Pixel art conversion | |
| # aseprite -b input.png --save-as output.png | |
| cmd = [self.name, "-b", str(input_path), "--save-as", str(output_path)] | |
| try: | |
| subprocess.run(cmd, check=True, capture_output=True) | |
| return True | |
| except Exception as e: | |
| print(f" [AssetProcessor] Aseprite error: {e}") | |
| return False | |
| class OpenCVAssetProcessor(AssetProcessor): | |
| """Bridge to VFXProcessor for static asset processing.""" | |
| def __init__(self): | |
| super().__init__("python") # Uses python/cv2 | |
| try: | |
| import cv2 | |
| self.available = True | |
| except ImportError: | |
| self.available = False | |
| def process(self, input_path: Path, output_path: Path, options: Dict = None) -> bool: | |
| if not self.available: return False | |
| try: | |
| import cv2 | |
| from vfx.opencv_effects import VFXProcessor | |
| img = cv2.imread(str(input_path)) | |
| if img is None: return False | |
| # Convert BGR to RGB for VFXProcessor | |
| img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) | |
| profile = options.get("profile", "none") | |
| vfx = VFXProcessor(profile) | |
| processed_rgb = vfx.process_frame(img_rgb, 0.0) | |
| # Convert back to BGR for saving | |
| processed_bgr = cv2.cvtColor(processed_rgb, cv2.COLOR_RGB2BGR) | |
| cv2.imwrite(str(output_path), processed_bgr) | |
| return True | |
| except Exception as e: | |
| print(f" [AssetProcessor] OpenCV error: {e}") | |
| return False | |
| def get_processor(name: str) -> Optional[AssetProcessor]: | |
| processors = { | |
| "imagemagick": ImageMagickProcessor, | |
| "inkscape": InkscapeProcessor, | |
| "aseprite": AsepriteProcessor, | |
| "opencv": OpenCVAssetProcessor | |
| } | |
| cls = processors.get(name.lower()) | |
| return cls() if cls else None | |
| def apply_artistic_profile(story_dir: Path, profile_name: str): | |
| """ | |
| Applies an artistic profile to all images in a story directory. | |
| Profiles: 'pixel_art', 'sketch', 'watercolor', 'vhs_static' | |
| """ | |
| img_dir = story_dir / "assets" / "images" | |
| if not img_dir.exists(): return | |
| output_dir = story_dir / "assets" / "images_processed" | |
| if profile_name == "none": | |
| if output_dir.exists(): | |
| print(f" 🧹 Profil 'none' demandé : suppression des images processées.") | |
| shutil.rmtree(output_dir) | |
| return | |
| output_dir.mkdir(exist_ok=True) | |
| # --- CACHE CHECK --- | |
| cache_file = output_dir / ".last_profile" | |
| if cache_file.exists(): | |
| try: | |
| last_profile = cache_file.read_text().strip() | |
| # If profile is same and some images exist, skip | |
| if last_profile == profile_name and any(output_dir.glob("scene_*.png")): | |
| print(f" ✨ Profil '{profile_name}' déjà appliqué. On saute le re-processing.") | |
| return | |
| except: pass | |
| processor = None | |
| options = {} | |
| # ... (rest of the logic remains) | |
| if profile_name == "pixel_art": | |
| processor = get_processor("aseprite") | |
| elif profile_name == "sketch": | |
| processor = get_processor("imagemagick") | |
| options = {"filters": "-colorspace gray -edge 1 -negate"} | |
| elif profile_name == "oil_paint": | |
| processor = get_processor("imagemagick") | |
| options = {"filters": "-paint 3"} | |
| elif profile_name == "charcoal": | |
| processor = get_processor("imagemagick") | |
| options = {"filters": "-charcoal 2"} | |
| elif profile_name == "vintage": | |
| processor = get_processor("imagemagick") | |
| options = {"filters": "-sepia-tone 80%"} | |
| elif profile_name == "night_vision": | |
| processor = get_processor("imagemagick") | |
| options = {"filters": "-colorspace gray -fill #00ff00 -tint 100"} | |
| elif profile_name == "vhs_static": | |
| processor = get_processor("opencv") | |
| options = {"profile": "vhs_pro"} | |
| if not processor or not processor.available: | |
| print(f" [AssetProcessor] Profile '{profile_name}' not available (tool missing).") | |
| return | |
| print(f" 🎨 Applying Artistic Profile: {profile_name}...") | |
| processed_count = 0 | |
| for img_path in img_dir.glob("scene_*.png"): | |
| out_path = output_dir / img_path.name | |
| if processor.process(img_path, out_path, options): | |
| processed_count += 1 | |
| # Save to cache if at least one image was processed | |
| if processed_count > 0: | |
| cache_file.write_text(profile_name) | |