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 +3 -0
- psdfy/__main__.py +6 -0
- psdfy/cli.py +253 -0
- psdfy/config.py +140 -0
- psdfy/weights.py +132 -0
- psdfy-0.1.3.dist-info/METADATA +425 -0
- psdfy-0.1.3.dist-info/RECORD +10 -0
- psdfy-0.1.3.dist-info/WHEEL +5 -0
- psdfy-0.1.3.dist-info/entry_points.txt +2 -0
- psdfy-0.1.3.dist-info/top_level.txt +1 -0
psdfy/__init__.py
ADDED
psdfy/__main__.py
ADDED
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 @@
|
|
|
1
|
+
psdfy
|