psdfy 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
psdfy/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """psdfy - PSD layer converter and processor."""
2
+
3
+ __version__ = "0.1.3"
psdfy/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Enable running psdfy as a module: python -m psdfy"""
2
+
3
+ from psdfy.cli import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
psdfy/cli.py ADDED
@@ -0,0 +1,253 @@
1
+ """CLI entry point for psdfy using Typer."""
2
+
3
+ import typer
4
+ from typing import Optional
5
+
6
+ # Import command implementations
7
+ from psdfy.commands.install import install_command
8
+ from psdfy.commands.start import start_command
9
+ from psdfy.commands.stop import stop_command
10
+ from psdfy.commands.fix import fix_command
11
+ from psdfy.commands.update import update_command
12
+ from psdfy.commands.uninstall import uninstall_command
13
+
14
+ app = typer.Typer(
15
+ name="psdfy",
16
+ help="Image to PSD converter - install, start, stop, and manage the service",
17
+ )
18
+
19
+
20
+ @app.command()
21
+ def version():
22
+ """
23
+ Show version information.
24
+
25
+ Displays psdfy version, service version, Python version,
26
+ PyTorch device, and model checksums.
27
+ """
28
+ typer.echo("psdfy version 0.1.0")
29
+ typer.echo("Service: API v1.0.0 + UI v1.0.0")
30
+ typer.echo("Python: 3.11+")
31
+ typer.echo("Device: cpu (default)")
32
+ typer.echo("Config: ~/.psdfy/config.toml")
33
+
34
+
35
+ @app.command()
36
+ def install(
37
+ password: Optional[str] = typer.Option(
38
+ None,
39
+ "--password",
40
+ help="Set UI password (default: 123456)"
41
+ ),
42
+ host: Optional[str] = typer.Option(
43
+ "localhost",
44
+ "--host",
45
+ help="API host (default: localhost)"
46
+ ),
47
+ api_port: Optional[int] = typer.Option(
48
+ 3456,
49
+ "--api-port",
50
+ help="API port (default: 3456)"
51
+ ),
52
+ ui_port: Optional[int] = typer.Option(
53
+ 3457,
54
+ "--ui-port",
55
+ help="UI port (default: 3457)"
56
+ ),
57
+ service: bool = typer.Option(
58
+ False,
59
+ "--service",
60
+ help="Install as system service (Windows/macOS/Linux)"
61
+ ),
62
+ no_weights: bool = typer.Option(
63
+ False,
64
+ "--no-weights",
65
+ help="Skip downloading model weights"
66
+ ),
67
+ dry_run: bool = typer.Option(
68
+ False,
69
+ "--dry-run",
70
+ help="Show what would be done without making changes"
71
+ ),
72
+ ):
73
+ """
74
+ Install psdfy and download model weights.
75
+
76
+ Creates ~/.psdfy/ directory, downloads SAM 2 weights,
77
+ and optionally registers as a system service.
78
+ """
79
+ install_command(
80
+ password=password,
81
+ host=host,
82
+ api_port=api_port,
83
+ ui_port=ui_port,
84
+ service=service,
85
+ no_weights=no_weights,
86
+ dry_run=dry_run,
87
+ )
88
+
89
+
90
+ @app.command()
91
+ def start(
92
+ host: Optional[str] = typer.Option(
93
+ None,
94
+ "--host",
95
+ help="Override API host"
96
+ ),
97
+ api_port: Optional[int] = typer.Option(
98
+ None,
99
+ "--api-port",
100
+ help="Override API port"
101
+ ),
102
+ ui_port: Optional[int] = typer.Option(
103
+ None,
104
+ "--ui-port",
105
+ help="Override UI port"
106
+ ),
107
+ foreground: bool = typer.Option(
108
+ False,
109
+ "--foreground",
110
+ help="Run in foreground (stream logs to stdout)"
111
+ ),
112
+ dry_run: bool = typer.Option(
113
+ False,
114
+ "--dry-run",
115
+ help="Show what would be done without starting"
116
+ ),
117
+ ):
118
+ """
119
+ Start the psdfy service.
120
+
121
+ Starts both API and UI servers. Checks that ports are free
122
+ and model weights are present before starting.
123
+ """
124
+ start_command(
125
+ host=host,
126
+ api_port=api_port,
127
+ ui_port=ui_port,
128
+ foreground=foreground,
129
+ dry_run=dry_run,
130
+ )
131
+
132
+
133
+ @app.command()
134
+ def stop(
135
+ force: bool = typer.Option(
136
+ False,
137
+ "--force",
138
+ help="Force kill if graceful shutdown fails"
139
+ ),
140
+ dry_run: bool = typer.Option(
141
+ False,
142
+ "--dry-run",
143
+ help="Show what would be done without stopping"
144
+ ),
145
+ ):
146
+ """
147
+ Stop the psdfy service.
148
+
149
+ Gracefully shuts down both API and UI servers.
150
+ Use --force to escalate to SIGKILL if needed.
151
+ """
152
+ stop_command(
153
+ force=force,
154
+ dry_run=dry_run,
155
+ )
156
+
157
+
158
+ @app.command()
159
+ def update(
160
+ channel: Optional[str] = typer.Option(
161
+ "stable",
162
+ "--channel",
163
+ help="Release channel: stable or beta"
164
+ ),
165
+ dry_run: bool = typer.Option(
166
+ False,
167
+ "--dry-run",
168
+ help="Show what would be updated without making changes"
169
+ ),
170
+ ):
171
+ """
172
+ Update psdfy to the latest version.
173
+
174
+ Checks PyPI for new versions, upgrades via pipx,
175
+ runs config migrations, and restarts the service.
176
+ """
177
+ update_command(
178
+ channel=channel,
179
+ dry_run=dry_run,
180
+ )
181
+
182
+
183
+ @app.command()
184
+ def fix(
185
+ reset_password: bool = typer.Option(
186
+ False,
187
+ "--reset-password",
188
+ help="Reset UI password to default (123456)"
189
+ ),
190
+ reset_client_secret: bool = typer.Option(
191
+ False,
192
+ "--reset-client-secret",
193
+ help="Generate new client secret"
194
+ ),
195
+ redownload_weights: bool = typer.Option(
196
+ False,
197
+ "--redownload-weights",
198
+ help="Re-download model weights"
199
+ ),
200
+ reset_config: bool = typer.Option(
201
+ False,
202
+ "--reset-config",
203
+ help="Reset config to defaults (with warning)"
204
+ ),
205
+ dry_run: bool = typer.Option(
206
+ False,
207
+ "--dry-run",
208
+ help="Show issues without fixing"
209
+ ),
210
+ ):
211
+ """
212
+ Diagnose and repair psdfy installation.
213
+
214
+ Checks Python version, config, weights, ports, GPU,
215
+ and service status. Offers interactive fixes.
216
+ """
217
+ fix_command(
218
+ reset_password=reset_password,
219
+ reset_client_secret=reset_client_secret,
220
+ redownload_weights=redownload_weights,
221
+ reset_config=reset_config,
222
+ dry_run=dry_run,
223
+ )
224
+
225
+
226
+ @app.command()
227
+ def uninstall(
228
+ force: bool = typer.Option(
229
+ False,
230
+ "--force",
231
+ help="Skip confirmation prompt"
232
+ ),
233
+ dry_run: bool = typer.Option(
234
+ False,
235
+ "--dry-run",
236
+ help="Show what would be removed without actually removing"
237
+ ),
238
+ ):
239
+ """
240
+ Uninstall psdfy and remove all related files.
241
+
242
+ Removes configuration, weights, logs, and uninstalls the pip package.
243
+ Use --force to skip confirmation, --dry-run to preview changes.
244
+ """
245
+ uninstall_command(
246
+ force=force,
247
+ dry_run=dry_run,
248
+ )
249
+
250
+
251
+ if __name__ == "__main__":
252
+ app()
253
+
psdfy/config.py ADDED
@@ -0,0 +1,140 @@
1
+ """Configuration management for psdfy."""
2
+
3
+ import os
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Optional, Dict, Any
7
+ import uuid
8
+
9
+
10
+ class ConfigManager:
11
+ """Manages ~/.psdfy/config.toml configuration."""
12
+
13
+ def __init__(self, config_dir: Optional[str] = None):
14
+ """
15
+ Initialize config manager.
16
+
17
+ Args:
18
+ config_dir: Config directory (default: ~/.psdfy)
19
+ """
20
+ if config_dir:
21
+ self.config_dir = Path(config_dir)
22
+ else:
23
+ self.config_dir = Path.home() / ".psdfy"
24
+
25
+ self.config_file = self.config_dir / "config.toml"
26
+ self.weights_dir = self.config_dir / "weights"
27
+ self.outputs_dir = self.config_dir / "outputs"
28
+ self.run_dir = self.config_dir / "run"
29
+
30
+ def ensure_directories(self) -> None:
31
+ """Create necessary directories."""
32
+ self.config_dir.mkdir(parents=True, exist_ok=True)
33
+ self.weights_dir.mkdir(parents=True, exist_ok=True)
34
+ self.outputs_dir.mkdir(parents=True, exist_ok=True)
35
+ self.run_dir.mkdir(parents=True, exist_ok=True)
36
+
37
+ def create_default_config(
38
+ self,
39
+ password: str = "123456",
40
+ host: str = "localhost",
41
+ api_port: int = 3456,
42
+ ui_port: int = 3457,
43
+ ) -> str:
44
+ """
45
+ Create default config.toml content.
46
+
47
+ Args:
48
+ password: UI password
49
+ host: API host
50
+ api_port: API port
51
+ ui_port: UI port
52
+
53
+ Returns:
54
+ Config file content
55
+ """
56
+ import hashlib
57
+ import bcrypt
58
+
59
+ # Hash password with bcrypt
60
+ password_hash = bcrypt.hashpw(
61
+ password.encode(),
62
+ bcrypt.gensalt()
63
+ ).decode()
64
+
65
+ # Generate secrets
66
+ client_secret = str(uuid.uuid4())
67
+ signature_pepper = os.urandom(32).hex()
68
+
69
+ config = f"""# Psdfy Configuration
70
+ # Generated automatically by 'psdfy install'
71
+
72
+ [app]
73
+ host = "{host}"
74
+ api_port = {api_port}
75
+ ui_port = {ui_port}
76
+ device = "cpu" # cpu, cuda, mps
77
+ log_level = "INFO"
78
+
79
+ [auth]
80
+ ui_password_hash = "{password_hash}"
81
+ client_secret = "{client_secret}"
82
+ signature_pepper = "{signature_pepper}"
83
+ signature_ttl_seconds = 86400
84
+
85
+ [models]
86
+ sam2_weights_path = "{self.weights_dir}/sam2_hiera_large.pt"
87
+ enable_grounding_dino = false
88
+ dino_weights_path = "{self.weights_dir}/groundingdino_swinb_cogvlm.pth"
89
+
90
+ [storage]
91
+ backend = "local"
92
+ local_dir = "{self.outputs_dir}"
93
+
94
+ [meta]
95
+ version = "0.1.0"
96
+ installed_at = "{__import__('datetime').datetime.now().isoformat()}"
97
+ """
98
+ return config
99
+
100
+ def save_config(self, content: str) -> None:
101
+ """Save config to file."""
102
+ self.ensure_directories()
103
+ with open(self.config_file, "w") as f:
104
+ f.write(content)
105
+
106
+ def load_config(self) -> Dict[str, Any]:
107
+ """Load config from file."""
108
+ if not self.config_file.exists():
109
+ return {}
110
+
111
+ # Simple TOML parser (for MVP)
112
+ config = {}
113
+ current_section = None
114
+
115
+ with open(self.config_file, "r") as f:
116
+ for line in f:
117
+ line = line.strip()
118
+ if not line or line.startswith("#"):
119
+ continue
120
+
121
+ if line.startswith("[") and line.endswith("]"):
122
+ current_section = line[1:-1]
123
+ config[current_section] = {}
124
+ elif "=" in line and current_section:
125
+ key, value = line.split("=", 1)
126
+ key = key.strip()
127
+ value = value.strip().strip('"')
128
+ config[current_section][key] = value
129
+
130
+ return config
131
+
132
+ def get_config_value(self, section: str, key: str, default: Any = None) -> Any:
133
+ """Get config value."""
134
+ config = self.load_config()
135
+ return config.get(section, {}).get(key, default)
136
+
137
+
138
+ def get_config_manager() -> ConfigManager:
139
+ """Get config manager instance."""
140
+ return ConfigManager()
psdfy/weights.py ADDED
@@ -0,0 +1,132 @@
1
+ """Model weights downloader."""
2
+
3
+ import os
4
+ import hashlib
5
+ from pathlib import Path
6
+ from typing import Optional
7
+ import urllib.request
8
+ import urllib.error
9
+
10
+
11
+ class WeightsDownloader:
12
+ """Downloads and verifies model weights."""
13
+
14
+ # Model URLs and checksums
15
+ MODELS = {
16
+ "sam2": {
17
+ "url": "https://dl.fbaipublicfiles.com/segment_anything_2/sam2_hiera_large.pt",
18
+ "filename": "sam2_hiera_large.pt",
19
+ "sha256": "unknown", # TODO: Get actual checksum
20
+ },
21
+ "groundingdino": {
22
+ "url": "https://huggingface.co/ShilongLiu/GroundingDINO/resolve/main/groundingdino_swinb_cogvlm.pth",
23
+ "filename": "groundingdino_swinb_cogvlm.pth",
24
+ "sha256": "unknown", # TODO: Get actual checksum
25
+ },
26
+ }
27
+
28
+ def __init__(self, weights_dir: Optional[str] = None):
29
+ """
30
+ Initialize downloader.
31
+
32
+ Args:
33
+ weights_dir: Directory to store weights
34
+ """
35
+ if weights_dir:
36
+ self.weights_dir = Path(weights_dir)
37
+ else:
38
+ self.weights_dir = Path.home() / ".psdfy" / "weights"
39
+
40
+ self.weights_dir.mkdir(parents=True, exist_ok=True)
41
+
42
+ def download_model(
43
+ self,
44
+ model_name: str,
45
+ force: bool = False,
46
+ progress_callback=None,
47
+ ) -> Path:
48
+ """
49
+ Download a model.
50
+
51
+ Args:
52
+ model_name: Model name (sam2, groundingdino)
53
+ force: Force re-download even if exists
54
+ progress_callback: Callback for progress updates
55
+
56
+ Returns:
57
+ Path to downloaded file
58
+ """
59
+ if model_name not in self.MODELS:
60
+ raise ValueError(f"Unknown model: {model_name}")
61
+
62
+ model_info = self.MODELS[model_name]
63
+ file_path = self.weights_dir / model_info["filename"]
64
+
65
+ # Check if already exists
66
+ if file_path.exists() and not force:
67
+ if progress_callback:
68
+ progress_callback(f"Model {model_name} already exists")
69
+ return file_path
70
+
71
+ # Download
72
+ if progress_callback:
73
+ progress_callback(f"Downloading {model_name}...")
74
+
75
+ try:
76
+ urllib.request.urlretrieve(
77
+ model_info["url"],
78
+ file_path,
79
+ reporthook=self._make_progress_hook(progress_callback),
80
+ )
81
+ except urllib.error.URLError as e:
82
+ raise RuntimeError(f"Failed to download {model_name}: {e}")
83
+
84
+ # Verify checksum if available
85
+ if model_info["sha256"] != "unknown":
86
+ if progress_callback:
87
+ progress_callback(f"Verifying {model_name}...")
88
+
89
+ actual_sha256 = self._calculate_sha256(file_path)
90
+ if actual_sha256 != model_info["sha256"]:
91
+ file_path.unlink()
92
+ raise RuntimeError(
93
+ f"Checksum mismatch for {model_name}. "
94
+ f"Expected {model_info['sha256']}, got {actual_sha256}"
95
+ )
96
+
97
+ if progress_callback:
98
+ progress_callback(f"Downloaded {model_name}")
99
+
100
+ return file_path
101
+
102
+ def _calculate_sha256(self, file_path: Path) -> str:
103
+ """Calculate SHA256 checksum of file."""
104
+ sha256_hash = hashlib.sha256()
105
+ with open(file_path, "rb") as f:
106
+ for byte_block in iter(lambda: f.read(4096), b""):
107
+ sha256_hash.update(byte_block)
108
+ return sha256_hash.hexdigest()
109
+
110
+ def _make_progress_hook(self, callback):
111
+ """Create progress hook for urlretrieve."""
112
+ def hook(block_num, block_size, total_size):
113
+ if callback and total_size > 0:
114
+ downloaded = block_num * block_size
115
+ percent = min(100, int(100 * downloaded / total_size))
116
+ # Use \r to overwrite the same line instead of creating new lines
117
+ import sys
118
+ sys.stdout.write(f"\rProgress: {percent}%")
119
+ sys.stdout.flush()
120
+ return hook
121
+
122
+
123
+ def get_weights_downloader(weights_dir: Optional[str] = None) -> WeightsDownloader:
124
+ """Get weights downloader instance.
125
+
126
+ Args:
127
+ weights_dir: Optional directory to store weights
128
+
129
+ Returns:
130
+ WeightsDownloader instance
131
+ """
132
+ return WeightsDownloader(weights_dir)
@@ -0,0 +1,425 @@
1
+ Metadata-Version: 2.4
2
+ Name: psdfy
3
+ Version: 0.1.3
4
+ Summary: PSD layer converter and processor
5
+ Author-email: Wirandra Alaya <daycodestudioproject@gmail.com>
6
+ License: MIT
7
+ Keywords: psd,layer,converter
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Python: >=3.11
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: typer>=0.9.0
17
+ Requires-Dist: fastapi>=0.100.0
18
+ Requires-Dist: uvicorn>=0.23.0
19
+ Requires-Dist: pydantic>=2.0
20
+ Requires-Dist: pydantic-settings>=2.0
21
+ Requires-Dist: python-multipart>=0.0.6
22
+ Requires-Dist: itsdangerous>=2.0
23
+ Requires-Dist: bcrypt>=4.0
24
+ Requires-Dist: requests>=2.31.0
25
+ Requires-Dist: pillow>=10.0.0
26
+ Requires-Dist: numpy>=1.24.0
27
+ Requires-Dist: opencv-python-headless>=4.8.0
28
+ Requires-Dist: python-dotenv>=1.0.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
31
+ Requires-Dist: black>=23.0.0; extra == "dev"
32
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
33
+ Requires-Dist: pre-commit>=3.0.0; extra == "dev"
34
+
35
+ # 🎨 psdfy
36
+
37
+ **Convert any image into an editable Adobe Photoshop file with AI-powered layer segmentation.**
38
+
39
+ Upload a photo, and psdfy automatically detects objects, creates separate layers for each one, and exports a ready-to-edit `.psd` file. Perfect for designers, photographers, and anyone who needs to work with layered images.
40
+
41
+ ---
42
+
43
+ ## ✨ Features
44
+
45
+ - **🤖 AI-Powered Segmentation** - Uses SAM 2 to automatically detect and separate objects
46
+ - **📦 Multi-Layer PSD Export** - Each detected object becomes an editable layer
47
+ - **🎯 Text-Prompted Detection** - Optional GroundingDINO integration for specific object detection
48
+ - **🌐 Web UI** - Simple browser interface for uploading and downloading
49
+ - **💻 CLI Tool** - Command-line interface for automation and scripting
50
+ - **☁️ Cloud Ready** - S3 storage backend support for cloud deployments
51
+ - **🐳 Docker Support** - CPU and GPU Docker images included
52
+ - **⚡ Fast Processing** - Optimized for 1080p images (< 5 seconds on GPU)
53
+
54
+ ---
55
+
56
+ ## 🚀 Quick Start
57
+
58
+ ### Option 1: Web UI (Easiest)
59
+
60
+ ```bash
61
+ # Install psdfy
62
+ pip install psdfy
63
+
64
+ # Run installation wizard
65
+ psdfy install
66
+
67
+ # Start the service
68
+ psdfy start
69
+
70
+ # Open browser and go to http://localhost:3457
71
+ ```
72
+
73
+ Then:
74
+ 1. Login with password (default: `123456`)
75
+ 2. Upload an image
76
+ 3. Click "Convert to PSD"
77
+ 4. Download your layered PSD file
78
+
79
+ ### Option 1b: Install Without Weights (Lightweight)
80
+
81
+ If you want to set up psdfy without downloading the large model weights (5GB+), use:
82
+
83
+ ```bash
84
+ # Install psdfy
85
+ pip install psdfy
86
+
87
+ # Run installation wizard without downloading weights
88
+ psdfy install --skip-weights
89
+
90
+ # Later, download weights when ready
91
+ psdfy install --download-weights-only
92
+
93
+ # Start the service
94
+ psdfy start
95
+ ```
96
+
97
+ This is useful for:
98
+ - Setting up on servers with limited bandwidth
99
+ - Testing the UI before committing to full installation
100
+ - Downloading weights on a separate machine
101
+
102
+ ### Option 2: Docker (Recommended)
103
+
104
+ ```bash
105
+ # CPU version
106
+ docker build -f docker/Dockerfile -t psdfy:latest .
107
+ docker run -p 3456:3456 -p 3457:3457 psdfy:latest
108
+
109
+ # GPU version (CUDA 12.1)
110
+ docker build -f docker/Dockerfile.gpu -t psdfy:gpu .
111
+ docker run --gpus all -p 3456:3456 -p 3457:3457 psdfy:gpu
112
+ ```
113
+
114
+ ### Option 3: Python API
115
+
116
+ ```python
117
+ from app.utils.io import load_image
118
+ from app.services.segmenter import get_segmenter
119
+ from app.services.psd_writer import get_psd_writer
120
+
121
+ # Load image
122
+ image_array, (width, height), fmt = load_image(image_bytes)
123
+
124
+ # Segment objects
125
+ segmenter = get_segmenter()
126
+ masks = segmenter.segment_auto(image_array)
127
+
128
+ # Write PSD
129
+ psd_writer = get_psd_writer()
130
+ psd_bytes = psd_writer.write_psd(layers, width, height)
131
+ ```
132
+
133
+ ---
134
+
135
+ ## 📖 Usage
136
+
137
+ ### Web UI
138
+
139
+ 1. **Login** - Enter password (default: `123456`)
140
+ 2. **Upload** - Drag & drop or click to select image
141
+ 3. **Configure** - Choose segmentation mode:
142
+ - `Automatic` - Detects all objects
143
+ - `Text Prompt` - Specify objects (e.g., "person . table . book")
144
+ 4. **Convert** - Click "Convert to PSD"
145
+ 5. **Download** - Get your layered PSD file
146
+
147
+ ### CLI Commands
148
+
149
+ ```bash
150
+ # Show version and system info
151
+ psdfy version
152
+
153
+ # Install/configure psdfy
154
+ psdfy install --password mypassword
155
+
156
+ # Start API and UI servers
157
+ psdfy start
158
+
159
+ # Stop servers
160
+ psdfy stop
161
+
162
+ # Diagnose and repair installation
163
+ psdfy fix --dry-run
164
+
165
+ # Update to latest version
166
+ psdfy update
167
+
168
+ # Check for issues
169
+ psdfy fix --redownload-weights
170
+ ```
171
+
172
+ ---
173
+
174
+ ## 🏗️ Architecture
175
+
176
+ ```
177
+ ┌─────────────────────────────────────────────────────────┐
178
+ │ Browser (Web UI) │
179
+ └────────────────────────┬────────────────────────────────┘
180
+ │ (Cookie Auth)
181
+
182
+ ┌─────────────────────────────────────────────────────────┐
183
+ │ GUI Web Server (Port 3457) │
184
+ │ - Login page │
185
+ │ - Upload interface │
186
+ │ - Server-side proxy to API │
187
+ └────────────────────────┬────────────────────────────────┘
188
+ │ (X-Session-Id, X-Client-Signature)
189
+
190
+ ┌─────────────────────────────────────────────────────────┐
191
+ │ Proxy API Server (Port 3456) │
192
+ │ - /convert - Image to PSD conversion │
193
+ │ - /files - Download results │
194
+ │ - /auth - Session management │
195
+ └────────────────────────┬────────────────────────────────┘
196
+
197
+ ┌────────────────┼────────────────┐
198
+ ▼ ▼ ▼
199
+ ┌────────┐ ┌────────┐ ┌────────┐
200
+ │ SAM 2 │ │ Mask │ │ PSD │
201
+ │ Loader │ │ Post- │ │ Writer │
202
+ │ │ │ process│ │ │
203
+ └────────┘ └────────┘ └────────┘
204
+ ```
205
+
206
+ ---
207
+
208
+ ## 🛠️ Development
209
+
210
+ ### Setup
211
+
212
+ ```bash
213
+ # Clone repository
214
+ git clone https://github.com/Mattel-Limbo/psdfy.git
215
+ cd psdfy
216
+
217
+ # Create virtual environment
218
+ python -m venv venv
219
+ source venv/bin/activate # On Windows: venv\Scripts\activate
220
+
221
+ # Install in development mode
222
+ pip install -e ".[dev]"
223
+ ```
224
+
225
+ ### Code Quality
226
+
227
+ ```bash
228
+ # Run linter
229
+ ruff check app psdfy tests
230
+
231
+ # Format code
232
+ black app psdfy tests
233
+
234
+ # Type checking
235
+ mypy --strict app psdfy
236
+
237
+ # Run tests
238
+ pytest tests/ -v
239
+ ```
240
+
241
+ ### Project Structure
242
+
243
+ ```
244
+ psdfy/
245
+ ├── app/ # FastAPI application
246
+ │ ├── api_app/ # Proxy API server (port 3456)
247
+ │ ├── ui_app/ # Web UI server (port 3457)
248
+ │ ├── services/ # Business logic (segmentation, PSD writing, etc.)
249
+ │ ├── models/ # AI model loaders (SAM 2, GroundingDINO)
250
+ │ ├── storage/ # Storage backends (local, S3)
251
+ │ └── utils/ # Helper utilities
252
+ ├── psdfy/ # CLI tool
253
+ │ ├── commands/ # CLI commands (install, start, stop, etc.)
254
+ │ ├── config.py # Configuration management
255
+ │ └── weights.py # Model weights downloader
256
+ ├── web/ # Web UI assets
257
+ │ └── templates/ # HTML templates
258
+ ├── tests/ # Test suite
259
+ ├── docker/ # Docker configurations
260
+ └── scripts/ # Utility scripts (benchmarking, etc.)
261
+ ```
262
+
263
+ ---
264
+
265
+ ## 📋 Requirements
266
+
267
+ - **Python**: 3.11 or higher
268
+ - **RAM**: 8GB minimum (16GB recommended)
269
+ - **GPU** (optional): NVIDIA GPU with CUDA 12.1+ for faster processing
270
+ - **Disk**: 5GB for model weights
271
+
272
+ ---
273
+
274
+ ## 🔧 Configuration
275
+
276
+ Configuration is stored in `~/.psdfy/config.toml`:
277
+
278
+ ```toml
279
+ [app]
280
+ host = "localhost"
281
+ api_port = 3456
282
+ ui_port = 3457
283
+ device = "cpu" # or "cuda", "mps"
284
+
285
+ [auth]
286
+ ui_password_hash = "..."
287
+ client_secret = "..."
288
+
289
+ [models]
290
+ sam2_weights_path = "~/.psdfy/weights/sam2_hiera_large.pt"
291
+ enable_grounding_dino = false
292
+ ```
293
+
294
+ ---
295
+
296
+ ## 🐛 Troubleshooting
297
+
298
+ ### Port Already in Use
299
+
300
+ ```bash
301
+ # Change ports
302
+ psdfy start --api-port 3500 --ui-port 3501
303
+ ```
304
+
305
+ ### Model Weights Not Found
306
+
307
+ ```bash
308
+ # Re-download weights
309
+ psdfy fix --redownload-weights
310
+ ```
311
+
312
+ ### Reset Password
313
+
314
+ ```bash
315
+ # Reset to default (123456)
316
+ psdfy fix --reset-password
317
+ ```
318
+
319
+ ### Check System Health
320
+
321
+ ```bash
322
+ # Run diagnostics
323
+ psdfy fix --dry-run
324
+ ```
325
+
326
+ ---
327
+
328
+ ## 📊 Performance
329
+
330
+ | Resolution | GPU (RTX 3080) | CPU (i7-12700) |
331
+ |-----------|----------------|----------------|
332
+ | 512x512 | ~0.5s | ~3s |
333
+ | 1080x1080 | ~1.5s | ~8s |
334
+ | 2160x2160 | ~4s | ~20s |
335
+
336
+ ---
337
+
338
+ ## 📝 API Examples
339
+
340
+ ### Convert Image (cURL)
341
+
342
+ ```bash
343
+ # Get session
344
+ SESSION=$(curl -X POST http://localhost:3456/auth/client-signature \
345
+ -H "Content-Type: application/json" \
346
+ -d '{"clientSecret":"your-secret"}' | jq -r '.sessionId')
347
+
348
+ # Convert image
349
+ curl -X POST http://localhost:3456/convert \
350
+ -H "X-Session-Id: $SESSION" \
351
+ -H "X-Client-Signature: your-signature" \
352
+ -F "file=@image.jpg" \
353
+ -F "mode=auto" \
354
+ -o output.psd
355
+ ```
356
+
357
+ ### Convert Image (Python)
358
+
359
+ ```python
360
+ import requests
361
+
362
+ # Get session
363
+ response = requests.post(
364
+ "http://localhost:3456/auth/client-signature",
365
+ json={"clientSecret": "your-secret"}
366
+ )
367
+ session_id = response.json()["sessionId"]
368
+
369
+ # Convert image
370
+ with open("image.jpg", "rb") as f:
371
+ response = requests.post(
372
+ "http://localhost:3456/convert",
373
+ headers={
374
+ "X-Session-Id": session_id,
375
+ "X-Client-Signature": "your-signature"
376
+ },
377
+ files={"file": f},
378
+ data={"mode": "auto"}
379
+ )
380
+
381
+ # Save PSD
382
+ with open("output.psd", "wb") as f:
383
+ f.write(response.content)
384
+ ```
385
+
386
+ ---
387
+
388
+ ## 📄 License
389
+
390
+ MIT License - see LICENSE file for details
391
+
392
+ ---
393
+
394
+ ## 🤝 Contributing
395
+
396
+ Contributions are welcome! Please:
397
+
398
+ 1. Fork the repository
399
+ 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
400
+ 3. Commit changes (`git commit -m 'Add amazing feature'`)
401
+ 4. Push to branch (`git push origin feature/amazing-feature`)
402
+ 5. Open a Pull Request
403
+
404
+ ---
405
+
406
+ ## 📞 Support
407
+
408
+ - **Issues**: [GitHub Issues](https://github.com/Mattel-Limbo/psdfy/issues)
409
+ - **Discussions**: [GitHub Discussions](https://github.com/Mattel-Limbo/psdfy/discussions)
410
+ - **Documentation**: See `plan.md` for detailed technical documentation
411
+
412
+ ---
413
+
414
+ ## 🎯 Roadmap
415
+
416
+ - [ ] Batch processing support
417
+ - [ ] Advanced layer ordering heuristics
418
+ - [ ] Real-time preview in browser
419
+ - [ ] Multi-user support with API keys
420
+ - [ ] Webhook notifications
421
+ - [ ] Custom model fine-tuning
422
+
423
+ ---
424
+
425
+ **Made with ❤️ for designers and developers**
@@ -0,0 +1,10 @@
1
+ psdfy/__init__.py,sha256=QF94EEmV4UY9YpRNshObMSwSf4PV_J3zlb_s3Y36UBY,72
2
+ psdfy/__main__.py,sha256=hCEZvbReJD-iC7ZT_K553TOSqa3LQ3Tps6lsSAqeLmU,121
3
+ psdfy/cli.py,sha256=9P9ITi1-qtoT06CAhLvXQ25Mi9R368AcrSC2-kNMuzM,6025
4
+ psdfy/config.py,sha256=4tajE5vmAUAQGHl4v_frtwlPbgQGF09VCJxR1HOuUYc,4068
5
+ psdfy/weights.py,sha256=MGh1asmdIpjlsC6EhbY7XxFMzII-GkJ3zh0IEAHw0aU,4411
6
+ psdfy-0.1.3.dist-info/METADATA,sha256=Ep0anT_76ib7CxiZVMzrNCRwIP7d8baoiFwyooSUVMA,12103
7
+ psdfy-0.1.3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
8
+ psdfy-0.1.3.dist-info/entry_points.txt,sha256=c8paWA8CKwbJbjhrBda89HeKXyadreDa7eb2MmRWFas,40
9
+ psdfy-0.1.3.dist-info/top_level.txt,sha256=X8CKHB7LrQZgN3egth6YRFdKPl_597Q-anWOpZVvJX0,6
10
+ psdfy-0.1.3.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ psdfy = psdfy.cli:app
@@ -0,0 +1 @@
1
+ psdfy