studio-console 1.3.4__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.
VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.3.4
docker-compose.yml ADDED
@@ -0,0 +1,282 @@
1
+ # Studio - Production
2
+ # Pre-built images from GHCR. No build: directives.
3
+ # GPU workers: docker compose -f docker-compose.yml -f workers/docker-compose.gpu.yml up -d
4
+ #
5
+ # Usage:
6
+ # docker compose up -d # Core (postgres + api + ui + nginx)
7
+ # docker compose --profile worker-general up -d # Core + a specific worker
8
+ # docker compose --profile workers up -d # Core + all workers (if using COMPOSE_PROFILES)
9
+ #
10
+ # nginx is always the front door on SHS_NGINX_PORT (default 80).
11
+ # api and ui are internal — not bound to host ports.
12
+ # Scaling is handled by console via docker-compose.override.yml.
13
+
14
+ services:
15
+ postgres:
16
+ image: pgvector/pgvector:0.8.3-pg18-trixie@sha256:6232c5ea178707f278d060b51747c30f310164595674e95d5f9d100f4a48b56c
17
+
18
+ environment:
19
+ POSTGRES_USER: ${POSTGRES_USER:-postgres}
20
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
21
+ POSTGRES_DB: selfhost_studio
22
+ PGDATA: /var/lib/postgresql/data
23
+ ports:
24
+ - "${POSTGRES_PORT:-5432}:5432"
25
+ volumes:
26
+ - ${SHS_DB_DATA:?SHS_DB_DATA is required - set in .env}:/var/lib/postgresql/data
27
+ healthcheck:
28
+ test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"]
29
+ interval: 5s
30
+ timeout: 5s
31
+ retries: 5
32
+ networks:
33
+ - prod-network
34
+ restart: unless-stopped
35
+ logging:
36
+ driver: json-file
37
+ options:
38
+ max-size: "50m"
39
+ max-file: "3"
40
+
41
+ api:
42
+ image: ghcr.io/selfhosthub/studio-api:${SHS_STUDIO_VERSION:?SHS_STUDIO_VERSION is required - set in .env}
43
+
44
+ env_file:
45
+ - ${SHS_WORKSPACE_DIR}/.env
46
+ environment:
47
+ - SHS_WORKSPACE_ROOT=/workspace
48
+ volumes:
49
+ - ${SHS_STORAGE_ROOT:?SHS_STORAGE_ROOT is required - set in .env}:/workspace
50
+ depends_on:
51
+ postgres:
52
+ condition: service_healthy
53
+ extra_hosts:
54
+ - "host.docker.internal:host-gateway"
55
+ healthcheck:
56
+ test: ["CMD", "curl", "-f", "http://127.0.0.1:8000/health"]
57
+ interval: 30s
58
+ timeout: 10s
59
+ retries: 3
60
+ start_period: 10s
61
+ networks:
62
+ - prod-network
63
+ restart: unless-stopped
64
+ logging:
65
+ driver: json-file
66
+ options:
67
+ max-size: "50m"
68
+ max-file: "3"
69
+
70
+ ui:
71
+ image: ghcr.io/selfhosthub/studio-ui:${SHS_STUDIO_VERSION:?SHS_STUDIO_VERSION is required - set in .env}
72
+
73
+ environment:
74
+ - NEXT_PUBLIC_API_URL=${SHS_PUBLIC_API_URL:-${SHS_API_BASE_URL}}
75
+ - NEXT_PUBLIC_WS_URL=${SHS_WS_URL:?SHS_WS_URL is required - set in .env}
76
+ - NEXT_PUBLIC_API_ENV=production
77
+ # SSR target; internal nginx.
78
+ - SHS_API_BASE_URL=http://nginx:${SHS_NGINX_PORT:-80}
79
+ - SHS_WS_URL=${SHS_WS_URL}
80
+ depends_on:
81
+ - api
82
+ networks:
83
+ - prod-network
84
+ restart: unless-stopped
85
+ logging:
86
+ driver: json-file
87
+ options:
88
+ max-size: "50m"
89
+ max-file: "3"
90
+
91
+ nginx:
92
+ image: nginx:alpine
93
+ ports:
94
+ - "${SHS_NGINX_PORT:-80}:80"
95
+ volumes:
96
+ - ${SHS_WORKSPACE_DIR}/nginx/studio.conf:/etc/nginx/conf.d/default.conf:ro
97
+ depends_on:
98
+ api:
99
+ condition: service_started
100
+ required: false
101
+ ui:
102
+ condition: service_started
103
+ required: false
104
+ healthcheck:
105
+ # 127.0.0.1, not localhost: /etc/hosts maps localhost to ::1 too, and
106
+ # nginx listens only on IPv4 0.0.0.0:80 — wget tries ::1 first and gets
107
+ # 'connection refused', leaving the container perpetually unhealthy.
108
+ test: ["CMD", "wget", "-qO-", "http://127.0.0.1/nginx-health"]
109
+ interval: 10s
110
+ timeout: 5s
111
+ retries: 3
112
+ networks:
113
+ - prod-network
114
+ restart: unless-stopped
115
+ logging:
116
+ driver: json-file
117
+ options:
118
+ max-size: "50m"
119
+ max-file: "3"
120
+
121
+ # ===========================================================================
122
+ # Workers - each has its own profile, activated via COMPOSE_PROFILES in .env
123
+ # ===========================================================================
124
+
125
+ worker-general:
126
+ image: ghcr.io/selfhosthub/studio-worker-general:${SHS_STUDIO_VERSION:?SHS_STUDIO_VERSION is required - set in .env}
127
+
128
+ profiles: [worker-general]
129
+
130
+ env_file:
131
+ - ${SHS_WORKSPACE_DIR}/.env
132
+ environment:
133
+ - SHS_API_BASE_URL=http://api:8000
134
+ - SHS_PUBLIC_BASE_URL=${SHS_PUBLIC_BASE_URL:?SHS_PUBLIC_BASE_URL is required - set in .env}
135
+ - SHS_WORKER_SHARED_SECRET=${SHS_WORKER_SHARED_SECRET:?SHS_WORKER_SHARED_SECRET is required - set in .env}
136
+ - SHS_WORKSPACE_ROOT=/workspace
137
+ - SHS_WORKER_TYPE=general
138
+ volumes:
139
+ - ${SHS_STORAGE_ROOT}:/workspace
140
+ extra_hosts:
141
+ - "host.docker.internal:host-gateway"
142
+ networks:
143
+ - prod-network
144
+ restart: unless-stopped
145
+ logging:
146
+ driver: json-file
147
+ options:
148
+ max-size: "50m"
149
+ max-file: "3"
150
+
151
+ worker-transfer:
152
+ image: ghcr.io/selfhosthub/studio-worker-transfer:${SHS_STUDIO_VERSION:?SHS_STUDIO_VERSION is required - set in .env}
153
+
154
+ profiles: [worker-transfer]
155
+
156
+ env_file:
157
+ - ${SHS_WORKSPACE_DIR}/.env
158
+ environment:
159
+ - SHS_API_BASE_URL=http://api:8000
160
+ - SHS_PUBLIC_BASE_URL=${SHS_PUBLIC_BASE_URL:?SHS_PUBLIC_BASE_URL is required - set in .env}
161
+ - SHS_WORKER_SHARED_SECRET=${SHS_WORKER_SHARED_SECRET:?SHS_WORKER_SHARED_SECRET is required - set in .env}
162
+ - SHS_WORKSPACE_ROOT=/workspace
163
+ - SHS_WORKER_TYPE=transfer
164
+ volumes:
165
+ - ${SHS_STORAGE_ROOT}:/workspace
166
+ extra_hosts:
167
+ - "host.docker.internal:host-gateway"
168
+ networks:
169
+ - prod-network
170
+ restart: unless-stopped
171
+ logging:
172
+ driver: json-file
173
+ options:
174
+ max-size: "50m"
175
+ max-file: "3"
176
+
177
+ worker-video:
178
+ image: ghcr.io/selfhosthub/studio-worker-video:${SHS_STUDIO_VERSION:?SHS_STUDIO_VERSION is required - set in .env}
179
+
180
+ profiles: [worker-video]
181
+
182
+ env_file:
183
+ - ${SHS_WORKSPACE_DIR}/.env
184
+ environment:
185
+ - SHS_API_BASE_URL=http://api:8000
186
+ - SHS_PUBLIC_BASE_URL=${SHS_PUBLIC_BASE_URL:?SHS_PUBLIC_BASE_URL is required - set in .env}
187
+ - SHS_WORKER_SHARED_SECRET=${SHS_WORKER_SHARED_SECRET:?SHS_WORKER_SHARED_SECRET is required - set in .env}
188
+ - SHS_WORKSPACE_ROOT=/workspace
189
+ - SHS_WORKER_TYPE=video
190
+ - SHS_WHISPER_MODEL=${SHS_WHISPER_MODEL:-base}
191
+ volumes:
192
+ - ${SHS_STORAGE_ROOT}:/workspace
193
+ extra_hosts:
194
+ - "host.docker.internal:host-gateway"
195
+ networks:
196
+ - prod-network
197
+ restart: unless-stopped
198
+ logging:
199
+ driver: json-file
200
+ options:
201
+ max-size: "50m"
202
+ max-file: "3"
203
+
204
+ worker-audio:
205
+ image: ghcr.io/selfhosthub/studio-worker-audio:${SHS_STUDIO_VERSION:?SHS_STUDIO_VERSION is required - set in .env}
206
+
207
+ profiles: [worker-audio]
208
+
209
+ env_file:
210
+ - ${SHS_WORKSPACE_DIR}/.env
211
+ environment:
212
+ - SHS_API_BASE_URL=http://api:8000
213
+ - SHS_PUBLIC_BASE_URL=${SHS_PUBLIC_BASE_URL:?SHS_PUBLIC_BASE_URL is required - set in .env}
214
+ - SHS_WORKER_SHARED_SECRET=${SHS_WORKER_SHARED_SECRET:?SHS_WORKER_SHARED_SECRET is required - set in .env}
215
+ - SHS_WORKSPACE_ROOT=/workspace
216
+ - SHS_WORKER_TYPE=audio
217
+ - HF_HOME=/models/huggingface
218
+ volumes:
219
+ - ${SHS_STORAGE_ROOT}:/workspace
220
+ - ${SHS_MODELS_ROOT:?SHS_MODELS_ROOT is required - set in .env}:/models
221
+ extra_hosts:
222
+ - "host.docker.internal:host-gateway"
223
+ networks:
224
+ - prod-network
225
+ restart: unless-stopped
226
+ logging:
227
+ driver: json-file
228
+ options:
229
+ max-size: "50m"
230
+ max-file: "3"
231
+
232
+ # ComfyUI worker (proxy to operator-managed ComfyUI server)
233
+ # Deploy with SHS_WORKER_TYPE=comfyui-image (default) or SHS_WORKER_TYPE=comfyui-video
234
+ worker-comfyui-image:
235
+ image: ghcr.io/selfhosthub/studio-worker-comfyui:${SHS_STUDIO_VERSION:?SHS_STUDIO_VERSION is required - set in .env}
236
+
237
+ profiles: [worker-comfyui-image]
238
+
239
+ env_file:
240
+ - ${SHS_WORKSPACE_DIR}/.env
241
+ environment:
242
+ - SHS_API_BASE_URL=http://api:8000
243
+ - SHS_PUBLIC_BASE_URL=${SHS_PUBLIC_BASE_URL:?SHS_PUBLIC_BASE_URL is required - set in .env}
244
+ - SHS_WORKER_SHARED_SECRET=${SHS_WORKER_SHARED_SECRET:?SHS_WORKER_SHARED_SECRET is required - set in .env}
245
+ - SHS_WORKSPACE_ROOT=/workspace
246
+ - SHS_WORKER_TYPE=comfyui-image
247
+ - SHS_COMFYUI_URL=${SHS_COMFYUI_URL:-}
248
+ volumes:
249
+ - ${SHS_STORAGE_ROOT}:/workspace
250
+ extra_hosts:
251
+ - "host.docker.internal:host-gateway"
252
+ networks:
253
+ - prod-network
254
+ restart: unless-stopped
255
+ logging:
256
+ driver: json-file
257
+ options:
258
+ max-size: "50m"
259
+ max-file: "3"
260
+
261
+ # ===========================================================================
262
+ # Cloudflare Tunnel - activated via COMPOSE_PROFILES=cloudflared
263
+ # ===========================================================================
264
+
265
+ cloudflared:
266
+ image: cloudflare/cloudflared:latest
267
+ profiles: [cloudflared]
268
+ command: tunnel --no-autoupdate run --token ${CLOUDFLARE_TUNNEL_TOKEN:-}
269
+ depends_on:
270
+ - nginx
271
+ networks:
272
+ - prod-network
273
+ restart: unless-stopped
274
+ logging:
275
+ driver: json-file
276
+ options:
277
+ max-size: "50m"
278
+ max-file: "3"
279
+
280
+ networks:
281
+ prod-network:
282
+ driver: bridge
@@ -0,0 +1,89 @@
1
+ upstream studio_api {
2
+ ip_hash;
3
+ {{API_UPSTREAM}}
4
+ }
5
+
6
+ upstream studio_ui {
7
+ {{UI_UPSTREAM}}
8
+ }
9
+
10
+ server {
11
+ listen 80;
12
+
13
+ # Compression — all responses are proxied from upstreams, so gzip_proxied
14
+ # must be set or nginx skips them by default.
15
+ gzip on;
16
+ gzip_vary on;
17
+ gzip_proxied any;
18
+ gzip_comp_level 5;
19
+ gzip_min_length 256;
20
+ gzip_types
21
+ application/json
22
+ application/javascript
23
+ text/javascript
24
+ text/css
25
+ text/plain
26
+ application/xml
27
+ image/svg+xml
28
+ application/rss+xml
29
+ font/ttf
30
+ font/otf
31
+ application/font-woff;
32
+
33
+ # Health endpoint for the nginx container itself
34
+ location /nginx-health {
35
+ access_log off;
36
+ return 200 "ok\n";
37
+ add_header Content-Type text/plain;
38
+ }
39
+
40
+ # API health check (top-level route, not under /api)
41
+ location = /health {
42
+ proxy_pass http://studio_api;
43
+ proxy_http_version 1.1;
44
+ proxy_set_header Host $host;
45
+ proxy_set_header X-Real-IP $remote_addr;
46
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
47
+ proxy_set_header X-Forwarded-Proto $scheme;
48
+ }
49
+
50
+ # API + WebSocket + static uploads (org media served by API)
51
+ location ~ ^/(api|ws|uploads)(/|$) {
52
+ proxy_pass http://studio_api;
53
+ proxy_http_version 1.1;
54
+ proxy_set_header Host $host;
55
+ proxy_set_header X-Real-IP $remote_addr;
56
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
57
+ proxy_set_header X-Forwarded-Proto $scheme;
58
+
59
+ # WebSocket upgrade
60
+ proxy_set_header Upgrade $http_upgrade;
61
+ proxy_set_header Connection $connection_upgrade;
62
+ proxy_read_timeout 86400s;
63
+ proxy_send_timeout 86400s;
64
+ }
65
+
66
+ # Hashed Next.js assets — content-hashed filenames, safe to cache forever.
67
+ location /_next/static/ {
68
+ proxy_pass http://studio_ui;
69
+ proxy_http_version 1.1;
70
+ proxy_set_header Host $host;
71
+ add_header Cache-Control "public, max-age=31536000, immutable";
72
+ }
73
+
74
+ # UI (everything else)
75
+ location / {
76
+ proxy_pass http://studio_ui;
77
+ proxy_http_version 1.1;
78
+ proxy_set_header Host $host;
79
+ proxy_set_header X-Real-IP $remote_addr;
80
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
81
+ proxy_set_header X-Forwarded-Proto $scheme;
82
+ }
83
+ }
84
+
85
+ # WebSocket connection upgrade map (referenced above)
86
+ map $http_upgrade $connection_upgrade {
87
+ default upgrade;
88
+ "" close;
89
+ }
@@ -0,0 +1,17 @@
1
+ # studio_console/__init__.py
2
+ """Studio Console — operator management CLI."""
3
+
4
+ from pathlib import Path
5
+
6
+ _VERSION_FILE = Path(__file__).resolve().parent.parent / "VERSION"
7
+ __version__: str = _VERSION_FILE.read_text().strip() if _VERSION_FILE.exists() else "dev"
8
+
9
+
10
+ def main() -> None:
11
+ from .cli import main as _main
12
+
13
+ _main()
14
+
15
+
16
+ __all__ = ["main", "__version__"]
17
+
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ main()
studio_console/cli.py ADDED
@@ -0,0 +1,176 @@
1
+ # studio_console/cli.py
2
+ """Argument parser and main entry point."""
3
+
4
+ import argparse
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from .commands import (
10
+ cmd_backup,
11
+ cmd_build,
12
+ cmd_config_set,
13
+ cmd_config_unset,
14
+ cmd_health,
15
+ cmd_links,
16
+ cmd_logs,
17
+ cmd_reset_password,
18
+ cmd_restart,
19
+ cmd_restore,
20
+ cmd_self_update,
21
+ cmd_show_config,
22
+ cmd_start,
23
+ cmd_stop,
24
+ cmd_upgrade,
25
+ cmd_workers,
26
+ config_menu,
27
+ )
28
+ from .commands_container import container_menu
29
+ from .commands_launch import cmd_launch_core, cmd_launch_full, cmd_set_core_db_url
30
+ from .env import _workspace_dir, detect_context, env_path
31
+ from .tui import NavBack, NavExit
32
+ from .wizard import wizard, wizard_non_interactive
33
+
34
+
35
+ def build_parser() -> argparse.ArgumentParser:
36
+ parser = argparse.ArgumentParser(
37
+ prog="studio-console",
38
+ description="Studio Console — operator management CLI",
39
+ )
40
+ sub = parser.add_subparsers(dest="command")
41
+
42
+ p_build = sub.add_parser("build", help="Build Docker images from local source")
43
+ p_build.add_argument(
44
+ "images",
45
+ nargs="*",
46
+ default=None,
47
+ help="Images to build (e.g. api ui worker-general). Omit for all.",
48
+ )
49
+
50
+ sub.add_parser("start", help="Start services")
51
+ sub.add_parser("stop", help="Stop services")
52
+
53
+ p_restart = sub.add_parser("restart", help="Restart service or all")
54
+ p_restart.add_argument("service", nargs="?", default=None)
55
+
56
+ sub.add_parser("health", help="Health check")
57
+
58
+ p_config = sub.add_parser("config", help="Show or set config")
59
+ config_sub = p_config.add_subparsers(dest="config_action")
60
+ p_set = config_sub.add_parser("set", help="Set a config value")
61
+ p_set.add_argument("key")
62
+ p_set.add_argument("value")
63
+ p_unset = config_sub.add_parser("unset", help="Remove a config value")
64
+ p_unset.add_argument("key")
65
+
66
+ p_logs = sub.add_parser("logs", help="View logs")
67
+ p_logs.add_argument("service", nargs="?", default=None)
68
+
69
+ sub.add_parser("workers", help="List/scale workers")
70
+ sub.add_parser("reset-password", help="Reset admin password")
71
+
72
+ p_backup = sub.add_parser("backup", help="Backup database + files")
73
+ p_backup.add_argument(
74
+ "what",
75
+ nargs="?",
76
+ choices=["all", "db", "orgs"],
77
+ default="all",
78
+ help="What to back up (default: all)",
79
+ )
80
+
81
+ p_restore = sub.add_parser("restore", help="Restore from backup")
82
+ p_restore.add_argument("path", nargs="?", default=None)
83
+
84
+ sub.add_parser("upgrade", help="Pull latest Studio version + restart")
85
+ sub.add_parser("links", help="Print service URLs")
86
+ p_launch = sub.add_parser("launch-full", help="Launch the self-contained full image and open its console")
87
+ p_launch.add_argument("--tag", help="Image tag to launch (default: latest)")
88
+ p_launch.add_argument("--workspace", help="Host data dir (default: ~/.studio)")
89
+ p_launch_core = sub.add_parser("launch-core", help="Launch the core image (external Postgres) and open its console")
90
+ p_launch_core.add_argument("--tag", help="Image tag to launch (default: latest)")
91
+ p_launch_core.add_argument("--workspace", help="Host data dir (default: ~/.studio-core)")
92
+ p_core_db = sub.add_parser("core-db-url", help="Set core's external database URL for the next launch-core")
93
+ p_core_db.add_argument("url", nargs="?", default=None, help="postgresql+asyncpg://user:pass@host:5432/db (prompts if omitted)")
94
+ sub.add_parser("wizard", help="Run setup wizard")
95
+ sub.add_parser("init", help="Non-interactive setup from env vars (undocumented)")
96
+ sub.add_parser("self-update", help="Update studio-console to the latest version")
97
+ sub.add_parser("version", help="Print version")
98
+
99
+
100
+ return parser
101
+
102
+
103
+ def main() -> None:
104
+ parser = build_parser()
105
+ args = parser.parse_args()
106
+
107
+ context = detect_context()
108
+ ef = env_path(context)
109
+
110
+ if args.command is None:
111
+ try:
112
+ if context in ("container", "runpod"):
113
+ # In-container mode: entrypoint owns provisioning, console is
114
+ # purely an operator tool. No wizard, no compose-shaped commands.
115
+ container_menu(context, ef)
116
+ else:
117
+ if not ef.exists():
118
+ if not wizard(context, ef):
119
+ return # wizard aborted
120
+ # Always show the main menu (wizard saves .env, then we land here)
121
+ config_menu(context, ef)
122
+ except (NavBack, NavExit, KeyboardInterrupt):
123
+ print()
124
+ return
125
+
126
+ dispatch = {
127
+ "build": lambda: cmd_build(ef, args.images if args.images else None),
128
+ "start": lambda: sys.exit(0 if cmd_start(context, ef) is not False else 1),
129
+ "stop": lambda: cmd_stop(context, ef),
130
+ "restart": lambda: cmd_restart(context, ef, args.service),
131
+ "health": lambda: cmd_health(context, ef),
132
+ "config": lambda: (
133
+ cmd_config_set(ef, args.key, args.value)
134
+ if getattr(args, "config_action", None) == "set"
135
+ else cmd_config_unset(ef, args.key)
136
+ if getattr(args, "config_action", None) == "unset"
137
+ else cmd_show_config(context, ef)
138
+ ),
139
+ "logs": lambda: cmd_logs(context, ef, args.service),
140
+ "workers": lambda: cmd_workers(context, ef),
141
+ "reset-password": lambda: cmd_reset_password(context, ef),
142
+ "backup": lambda: cmd_backup(context, ef, args.what),
143
+ "restore": lambda: cmd_restore(context, ef, args.path),
144
+ "upgrade": lambda: cmd_upgrade(context, ef),
145
+ "links": lambda: cmd_links(context, ef),
146
+ "launch-full": lambda: sys.exit(
147
+ 0 if cmd_launch_full(
148
+ context,
149
+ args.tag,
150
+ Path(os.path.expanduser(args.workspace)) if args.workspace else None,
151
+ ) else 1
152
+ ),
153
+ "launch-core": lambda: sys.exit(
154
+ 0 if cmd_launch_core(
155
+ context,
156
+ args.tag,
157
+ Path(os.path.expanduser(args.workspace)) if args.workspace else None,
158
+ ) else 1
159
+ ),
160
+ "core-db-url": lambda: sys.exit(0 if cmd_set_core_db_url(context, args.url) else 1),
161
+ "wizard": lambda: wizard(context, ef),
162
+ "init": lambda: sys.exit(0 if wizard_non_interactive(context, ef) else 1),
163
+ "self-update": lambda: cmd_self_update(context),
164
+ "version": lambda: cmd_version(),
165
+ }
166
+
167
+ handler = dispatch.get(args.command)
168
+ if handler:
169
+ handler()
170
+ else:
171
+ parser.print_help()
172
+
173
+
174
+ def cmd_version() -> None:
175
+ from . import __version__
176
+ print(f"studio-console {__version__}")
@@ -0,0 +1 @@
1
+ # studio_console/cloudflare/__init__.py