crayonmlx 0.1.0a1__tar.gz
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.
- crayonmlx-0.1.0a1/PKG-INFO +201 -0
- crayonmlx-0.1.0a1/README.md +185 -0
- crayonmlx-0.1.0a1/pyproject.toml +49 -0
- crayonmlx-0.1.0a1/pyproject.toml.orig +42 -0
- crayonmlx-0.1.0a1/src/crayonmlx/__init__.py +3 -0
- crayonmlx-0.1.0a1/src/crayonmlx/__main__.py +3 -0
- crayonmlx-0.1.0a1/src/crayonmlx/app.py +134 -0
- crayonmlx-0.1.0a1/src/crayonmlx/cli.py +12 -0
- crayonmlx-0.1.0a1/src/crayonmlx/engines/__init__.py +1 -0
- crayonmlx-0.1.0a1/src/crayonmlx/engines/base.py +17 -0
- crayonmlx-0.1.0a1/src/crayonmlx/engines/mflux.py +91 -0
- crayonmlx-0.1.0a1/src/crayonmlx/engines/process.py +59 -0
- crayonmlx-0.1.0a1/src/crayonmlx/engines/registry.py +20 -0
- crayonmlx-0.1.0a1/src/crayonmlx/engines/worker.py +137 -0
- crayonmlx-0.1.0a1/src/crayonmlx/generation/__init__.py +1 -0
- crayonmlx-0.1.0a1/src/crayonmlx/generation/queue.py +87 -0
- crayonmlx-0.1.0a1/src/crayonmlx/models/__init__.py +1 -0
- crayonmlx-0.1.0a1/src/crayonmlx/models/catalog.py +40 -0
- crayonmlx-0.1.0a1/src/crayonmlx/models/downloads.py +63 -0
- crayonmlx-0.1.0a1/src/crayonmlx/models/identity.py +114 -0
- crayonmlx-0.1.0a1/src/crayonmlx/models/runtime.py +158 -0
- crayonmlx-0.1.0a1/src/crayonmlx/paths.py +33 -0
- crayonmlx-0.1.0a1/src/crayonmlx/presets/__init__.py +1 -0
- crayonmlx-0.1.0a1/src/crayonmlx/presets/catalog.py +70 -0
- crayonmlx-0.1.0a1/src/crayonmlx/presets/defaults.py +24 -0
- crayonmlx-0.1.0a1/src/crayonmlx/presets/legacy_defaults.py +37 -0
- crayonmlx-0.1.0a1/src/crayonmlx/presets/legacy_styles.py +50 -0
- crayonmlx-0.1.0a1/src/crayonmlx/presets/library.py +56 -0
- crayonmlx-0.1.0a1/src/crayonmlx/presets/repository.py +77 -0
- crayonmlx-0.1.0a1/src/crayonmlx/presets/styles.py +28 -0
- crayonmlx-0.1.0a1/src/crayonmlx/py.typed +0 -0
- crayonmlx-0.1.0a1/src/crayonmlx/schemas.py +131 -0
- crayonmlx-0.1.0a1/src/crayonmlx/storage/__init__.py +1 -0
- crayonmlx-0.1.0a1/src/crayonmlx/storage/database.py +112 -0
- crayonmlx-0.1.0a1/src/crayonmlx/storage/location.py +69 -0
- crayonmlx-0.1.0a1/src/crayonmlx/storage/secrets.py +21 -0
- crayonmlx-0.1.0a1/src/crayonmlx/web/__init__.py +1 -0
- crayonmlx-0.1.0a1/src/crayonmlx/web/folders.py +86 -0
- crayonmlx-0.1.0a1/src/crayonmlx/web/index.html +420 -0
- crayonmlx-0.1.0a1/src/crayonmlx/web/library_routes.py +78 -0
- crayonmlx-0.1.0a1/src/crayonmlx/web/model_routes.py +70 -0
- crayonmlx-0.1.0a1/src/crayonmlx/web/routes.py +177 -0
- crayonmlx-0.1.0a1/src/crayonmlx/web/static/api.js +53 -0
- crayonmlx-0.1.0a1/src/crayonmlx/web/static/app.js +88 -0
- crayonmlx-0.1.0a1/src/crayonmlx/web/static/forms.js +105 -0
- crayonmlx-0.1.0a1/src/crayonmlx/web/static/generation.js +175 -0
- crayonmlx-0.1.0a1/src/crayonmlx/web/static/library.js +305 -0
- crayonmlx-0.1.0a1/src/crayonmlx/web/static/preservation.js +74 -0
- crayonmlx-0.1.0a1/src/crayonmlx/web/static/preset-editor.js +260 -0
- crayonmlx-0.1.0a1/src/crayonmlx/web/static/presets.js +210 -0
- crayonmlx-0.1.0a1/src/crayonmlx/web/static/style.css +784 -0
- crayonmlx-0.1.0a1/src/crayonmlx/web/storage_routes.py +24 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: crayonmlx
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Local MLX studio for bringing drawings to life on Apple Silicon
|
|
5
|
+
Author: Nikolay Nechaev
|
|
6
|
+
Author-email: Nikolay Nechaev <godlikeguest@gmail.com>
|
|
7
|
+
Requires-Dist: mflux>=0.24,<0.25
|
|
8
|
+
Requires-Dist: fastapi>=0.162.0
|
|
9
|
+
Requires-Dist: huggingface-hub>=1.1.6,<2
|
|
10
|
+
Requires-Dist: pillow>=13.0.0
|
|
11
|
+
Requires-Dist: platformdirs>=4.14.0
|
|
12
|
+
Requires-Dist: python-multipart>=0.0.33
|
|
13
|
+
Requires-Dist: uvicorn[standard]>=0.54.0
|
|
14
|
+
Requires-Python: >=3.12
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# CrayonMLX
|
|
18
|
+
|
|
19
|
+
A local web studio for transforming children's drawings on Apple Silicon.
|
|
20
|
+
Choose what to change: rendering style, characters, background, framing, and light.
|
|
21
|
+
Keep the child's original ideas and explicitly protect the details that matter.
|
|
22
|
+
Python + FastAPI, plain HTML/CSS/JavaScript, native MLX inference through MFLUX.
|
|
23
|
+
|
|
24
|
+
**Early alpha.** The UI and orchestration are tested independently of model weights.
|
|
25
|
+
Actual image quality, memory requirements and checkpoint compatibility depend on your model.
|
|
26
|
+
|
|
27
|
+
## Install and run
|
|
28
|
+
|
|
29
|
+
Requires an Apple Silicon Mac, macOS compatible with MLX, and [uv](https://docs.astral.sh/uv/).
|
|
30
|
+
From a checkout:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
uv tool install .
|
|
34
|
+
crayonmlx
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Open http://127.0.0.1:8787. Choose another port with `crayonmlx --port 8788`.
|
|
38
|
+
After the package is published, installation will be `uv tool install crayonmlx`.
|
|
39
|
+
The project is not published yet. MFLUX libraries are installed with the app;
|
|
40
|
+
model weights are **not** bundled or downloaded at startup.
|
|
41
|
+
|
|
42
|
+
For development:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
uv sync
|
|
46
|
+
uv run crayonmlx
|
|
47
|
+
uv run pytest
|
|
48
|
+
uv run ruff check .
|
|
49
|
+
uv build
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Workflow
|
|
53
|
+
|
|
54
|
+
1. In **Settings**, choose absolute paths for the model folder and output folder.
|
|
55
|
+
Any accessible location, including an external disk, is supported.
|
|
56
|
+
2. Put each full Diffusers or MFLUX checkpoint in its own subfolder, or select a
|
|
57
|
+
checkpoint folder itself. HF cache users can select a concrete snapshot directory.
|
|
58
|
+
An individual `.safetensors` or `.gguf` file is not a complete checkpoint.
|
|
59
|
+
3. Alternatively download a repository and revision in **Models**. Private/gated
|
|
60
|
+
repositories need a Hugging Face token in Settings and access granted on HF.
|
|
61
|
+
Downloads run in the background. Incomplete downloads are hidden and can be resumed
|
|
62
|
+
by requesting the same repository again. Download status is session-local.
|
|
63
|
+
4. Upload a drawing; start your own combination or select a saved full preset, then choose one local model. Its variant and MFLUX
|
|
64
|
+
adapter are detected automatically. Unrecognized or ambiguous checkpoints are
|
|
65
|
+
shown in Models with an explanation and cannot be used for generation.
|
|
66
|
+
5. Assemble independent blocks: **overall style**, **characters/objects**, **background**,
|
|
67
|
+
**composition**, **lighting**, and **editing/preservation instructions**. Each block
|
|
68
|
+
offers saved templates, “Do not change”, and editable text. Overall style does not
|
|
69
|
+
dictate a new character or location. Subject-specific styling takes precedence
|
|
70
|
+
for the original foreground subjects only.
|
|
71
|
+
6. Check what must remain: identity, clothing, poses, colors, subjects/objects,
|
|
72
|
+
placement/scale, lettering/decorations, and original background. Choosing a new
|
|
73
|
+
background unchecks background preservation; choosing a new composition unchecks
|
|
74
|
+
placement preservation. The UI tells you when this happens. An unchecked option
|
|
75
|
+
allows adaptation; it does not itself request a particular replacement.
|
|
76
|
+
7. Choose a generation configuration to apply its parameters, then adjust values
|
|
77
|
+
for this run if needed.
|
|
78
|
+
8. Manage full presets, prompt templates and generation configurations in the
|
|
79
|
+
**Presets** tab. Each library supports creation, editing and deletion.
|
|
80
|
+
Quantization includes None (omit the MFLUX quantization flag), 4 bit and 8 bit.
|
|
81
|
+
9. Generate. History keeps the source, result, composed prompt, resolved seed, model
|
|
82
|
+
path, and all parameters as an immutable snapshot. History is read-only.
|
|
83
|
+
|
|
84
|
+
## Build a transformation
|
|
85
|
+
|
|
86
|
+
For example, turn the original characters into anime characters standing in Tokyo:
|
|
87
|
+
|
|
88
|
+
| Block | Selection |
|
|
89
|
+
| --- | --- |
|
|
90
|
+
| Overall style | Do not change, or choose a treatment for the whole scene |
|
|
91
|
+
| Characters/objects | Anime |
|
|
92
|
+
| Background | Tokyo |
|
|
93
|
+
| Composition | Entire subject |
|
|
94
|
+
| Lighting | Neon |
|
|
95
|
+
| Preservation | Identity, clothing, poses, colors, original subjects, lettering |
|
|
96
|
+
|
|
97
|
+
The city replaces the surrounding background; a house that is the main subject
|
|
98
|
+
remains that house. Framing and lighting do not introduce new protagonists.
|
|
99
|
+
Use **Show final prompt** to inspect the complete request before generating.
|
|
100
|
+
|
|
101
|
+
Included treatments: Huntrix, Roblox, anime, Studio Ghibli, realistic painting,
|
|
102
|
+
Disney (including Frozen and Encanto), Pixar (including Soul), and pixel art.
|
|
103
|
+
They are independently available for the whole scene and the existing main subjects.
|
|
104
|
+
Backgrounds include Seoul, Tokyo, Paris, Saint Petersburg, New York, Kyoto, forest,
|
|
105
|
+
mountains, sea, and space. Lighting and composition have their own libraries.
|
|
106
|
+
The interface is currently in Russian; generation instructions are in English.
|
|
107
|
+
|
|
108
|
+
The three libraries remain independent:
|
|
109
|
+
|
|
110
|
+
- **Prompt templates** supply one editable block of instructions.
|
|
111
|
+
- **Generation configurations** supply numerical rendering parameters.
|
|
112
|
+
- **Full presets** store a copy of all blocks, preservation choices, and parameters.
|
|
113
|
+
|
|
114
|
+
Updating a template does not rewrite saved full presets or historical jobs. Built-in
|
|
115
|
+
library upgrades replace only unmodified factory entries; edited user entries are
|
|
116
|
+
kept. Deleted entries are not reinstalled on every launch.
|
|
117
|
+
|
|
118
|
+
## Supported MLX adapters
|
|
119
|
+
|
|
120
|
+
- **MFLUX / Qwen Image Edit**: `qwen-image-edit` (2509) and `qwen-image-edit-2511`.
|
|
121
|
+
Qwen uses reference-image editing; it does **not** apply image strength.
|
|
122
|
+
- **MFLUX / FLUX.1 image-to-image**: `dev`, `schnell`, `krea-dev`.
|
|
123
|
+
FLUX schnell does not apply guidance. Higher FLUX image strength preserves more of the source image.
|
|
124
|
+
|
|
125
|
+
The server identifies the selected checkpoint independently of presets. It uses
|
|
126
|
+
MFLUX metadata, component configs, and the original repository folder name (including
|
|
127
|
+
HF snapshot paths). Keep the original HF name if the checkpoint has no MFLUX manifest.
|
|
128
|
+
Conflicting or unsupported metadata is rejected before a job is created. Pre-quantized MFLUX models can reduce download size and RAM needs.
|
|
129
|
+
The UI quantization setting is passed to MFLUX; checkpoint quantization may be fixed.
|
|
130
|
+
Generation is explicitly offline: missing components fail rather than download silently.
|
|
131
|
+
Only MLX engines are in scope; no PyTorch/MPS or other inference backends.
|
|
132
|
+
|
|
133
|
+
## Data
|
|
134
|
+
|
|
135
|
+
Default application data on macOS: `~/Library/Application Support/crayonmlx/`.
|
|
136
|
+
On first launch you can override it with `CRAYONMLX_DATA_DIR=/absolute/path crayonmlx`.
|
|
137
|
+
Settings → Internal data lets you copy settings, presets, history and the HF token
|
|
138
|
+
into an empty folder and use it after restart. The source is retained; model/image
|
|
139
|
+
paths are unchanged. Transfers require no active jobs or downloads and freeze
|
|
140
|
+
further writes until restart. The chosen location is recorded in `location.json`
|
|
141
|
+
in the platform's CrayonMLX configuration folder and takes precedence over the
|
|
142
|
+
bootstrap environment variable on subsequent launches.
|
|
143
|
+
|
|
144
|
+
- `crayonmlx.sqlite3`: settings and durable generation history.
|
|
145
|
+
- `presets/*.json`: full presets, initialized once.
|
|
146
|
+
- `prompt-templates/*.json`: reusable categorized prompt fragments.
|
|
147
|
+
- `generation-configs/*.json`: independent render configurations.
|
|
148
|
+
- `hf-token`: credential file with mode `0600`; never returned to the browser.
|
|
149
|
+
- Each output job folder: `source.png`, `result.png`, `generation.json` and execution measurements. Worker logs live in `runtime/<session>/worker.log`.
|
|
150
|
+
|
|
151
|
+
Changing the output directory affects new jobs. Old history keeps its original paths.
|
|
152
|
+
Moving or deleting those files makes their previews unavailable. Jobs run sequentially
|
|
153
|
+
and snapshot paths/parameters at submission. On restart, unfinished jobs become failed
|
|
154
|
+
and remain visible in read-only history. Graceful shutdown waits for current work to finish.
|
|
155
|
+
One separate MLX process keeps the current model resident between jobs. Models
|
|
156
|
+
are unloaded after 15 idle minutes by default; Settings accepts a custom timeout or
|
|
157
|
+
None to disable automatic unloading. Switching model or requested quantization
|
|
158
|
+
releases the previous process before loading another model. Manual load/unload
|
|
159
|
+
and enable/disable controls are available in Models. Disabling preserves files and
|
|
160
|
+
lets an already running generation finish.
|
|
161
|
+
|
|
162
|
+
Generation reports preparation, loading, completed denoising steps, decoding, and
|
|
163
|
+
elapsed time. MLX active allocations, cache, and peak are reported separately;
|
|
164
|
+
these are not the full process RSS. Successful history entries record execution
|
|
165
|
+
measurements separately from the requested configuration, including effective
|
|
166
|
+
quantization when reported by MFLUX.
|
|
167
|
+
|
|
168
|
+
The server binds to loopback only. There is no public API or user/account system.
|
|
169
|
+
HF tokens are intended only for downloading models, not for uploads or inference.
|
|
170
|
+
|
|
171
|
+
## Code layout
|
|
172
|
+
|
|
173
|
+
All application sources, including the frontend, are in `src/crayonmlx/`:
|
|
174
|
+
|
|
175
|
+
```text
|
|
176
|
+
app.py service composition and lifecycle
|
|
177
|
+
schemas.py validated request/configuration values
|
|
178
|
+
engines/ render contract, registry, MFLUX adapters
|
|
179
|
+
models/ local catalog and HF downloads
|
|
180
|
+
presets/ atomic JSON storage and starting prompts
|
|
181
|
+
generation/ serialized render jobs
|
|
182
|
+
storage/ SQLite history/settings and local credential
|
|
183
|
+
web/ internal UI routes, HTML, CSS and JS modules
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
To add an MLX engine, implement `engines/base.py` and register it in `engines/registry.py`.
|
|
187
|
+
Tests use a small injected renderer; they do not claim to test model inference.
|
|
188
|
+
Builds include frontend assets and expose the `crayonmlx` command for `uv tool install`.
|
|
189
|
+
|
|
190
|
+
## Publishing and contributing
|
|
191
|
+
|
|
192
|
+
See [PUBLISHING.md](PUBLISHING.md) for the maintainer guide: local build verification,
|
|
193
|
+
TestPyPI rehearsal, PyPI publication, and installation with `uv tool`.
|
|
194
|
+
The package has not been published by this setup process.
|
|
195
|
+
|
|
196
|
+
Keep application code in `src/crayonmlx/`, use small modules with clear responsibilities,
|
|
197
|
+
and add tests for changes to data compatibility, job snapshots, or engine behavior.
|
|
198
|
+
Run `uv run pytest` and `uv run ruff check src tests` before submitting changes.
|
|
199
|
+
Do not commit downloaded models, generated images, application data, or access tokens.
|
|
200
|
+
A project license has not yet been selected; add the chosen license before presenting
|
|
201
|
+
this repository as a licensed open-source release.
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# CrayonMLX
|
|
2
|
+
|
|
3
|
+
A local web studio for transforming children's drawings on Apple Silicon.
|
|
4
|
+
Choose what to change: rendering style, characters, background, framing, and light.
|
|
5
|
+
Keep the child's original ideas and explicitly protect the details that matter.
|
|
6
|
+
Python + FastAPI, plain HTML/CSS/JavaScript, native MLX inference through MFLUX.
|
|
7
|
+
|
|
8
|
+
**Early alpha.** The UI and orchestration are tested independently of model weights.
|
|
9
|
+
Actual image quality, memory requirements and checkpoint compatibility depend on your model.
|
|
10
|
+
|
|
11
|
+
## Install and run
|
|
12
|
+
|
|
13
|
+
Requires an Apple Silicon Mac, macOS compatible with MLX, and [uv](https://docs.astral.sh/uv/).
|
|
14
|
+
From a checkout:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
uv tool install .
|
|
18
|
+
crayonmlx
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Open http://127.0.0.1:8787. Choose another port with `crayonmlx --port 8788`.
|
|
22
|
+
After the package is published, installation will be `uv tool install crayonmlx`.
|
|
23
|
+
The project is not published yet. MFLUX libraries are installed with the app;
|
|
24
|
+
model weights are **not** bundled or downloaded at startup.
|
|
25
|
+
|
|
26
|
+
For development:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
uv sync
|
|
30
|
+
uv run crayonmlx
|
|
31
|
+
uv run pytest
|
|
32
|
+
uv run ruff check .
|
|
33
|
+
uv build
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Workflow
|
|
37
|
+
|
|
38
|
+
1. In **Settings**, choose absolute paths for the model folder and output folder.
|
|
39
|
+
Any accessible location, including an external disk, is supported.
|
|
40
|
+
2. Put each full Diffusers or MFLUX checkpoint in its own subfolder, or select a
|
|
41
|
+
checkpoint folder itself. HF cache users can select a concrete snapshot directory.
|
|
42
|
+
An individual `.safetensors` or `.gguf` file is not a complete checkpoint.
|
|
43
|
+
3. Alternatively download a repository and revision in **Models**. Private/gated
|
|
44
|
+
repositories need a Hugging Face token in Settings and access granted on HF.
|
|
45
|
+
Downloads run in the background. Incomplete downloads are hidden and can be resumed
|
|
46
|
+
by requesting the same repository again. Download status is session-local.
|
|
47
|
+
4. Upload a drawing; start your own combination or select a saved full preset, then choose one local model. Its variant and MFLUX
|
|
48
|
+
adapter are detected automatically. Unrecognized or ambiguous checkpoints are
|
|
49
|
+
shown in Models with an explanation and cannot be used for generation.
|
|
50
|
+
5. Assemble independent blocks: **overall style**, **characters/objects**, **background**,
|
|
51
|
+
**composition**, **lighting**, and **editing/preservation instructions**. Each block
|
|
52
|
+
offers saved templates, “Do not change”, and editable text. Overall style does not
|
|
53
|
+
dictate a new character or location. Subject-specific styling takes precedence
|
|
54
|
+
for the original foreground subjects only.
|
|
55
|
+
6. Check what must remain: identity, clothing, poses, colors, subjects/objects,
|
|
56
|
+
placement/scale, lettering/decorations, and original background. Choosing a new
|
|
57
|
+
background unchecks background preservation; choosing a new composition unchecks
|
|
58
|
+
placement preservation. The UI tells you when this happens. An unchecked option
|
|
59
|
+
allows adaptation; it does not itself request a particular replacement.
|
|
60
|
+
7. Choose a generation configuration to apply its parameters, then adjust values
|
|
61
|
+
for this run if needed.
|
|
62
|
+
8. Manage full presets, prompt templates and generation configurations in the
|
|
63
|
+
**Presets** tab. Each library supports creation, editing and deletion.
|
|
64
|
+
Quantization includes None (omit the MFLUX quantization flag), 4 bit and 8 bit.
|
|
65
|
+
9. Generate. History keeps the source, result, composed prompt, resolved seed, model
|
|
66
|
+
path, and all parameters as an immutable snapshot. History is read-only.
|
|
67
|
+
|
|
68
|
+
## Build a transformation
|
|
69
|
+
|
|
70
|
+
For example, turn the original characters into anime characters standing in Tokyo:
|
|
71
|
+
|
|
72
|
+
| Block | Selection |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| Overall style | Do not change, or choose a treatment for the whole scene |
|
|
75
|
+
| Characters/objects | Anime |
|
|
76
|
+
| Background | Tokyo |
|
|
77
|
+
| Composition | Entire subject |
|
|
78
|
+
| Lighting | Neon |
|
|
79
|
+
| Preservation | Identity, clothing, poses, colors, original subjects, lettering |
|
|
80
|
+
|
|
81
|
+
The city replaces the surrounding background; a house that is the main subject
|
|
82
|
+
remains that house. Framing and lighting do not introduce new protagonists.
|
|
83
|
+
Use **Show final prompt** to inspect the complete request before generating.
|
|
84
|
+
|
|
85
|
+
Included treatments: Huntrix, Roblox, anime, Studio Ghibli, realistic painting,
|
|
86
|
+
Disney (including Frozen and Encanto), Pixar (including Soul), and pixel art.
|
|
87
|
+
They are independently available for the whole scene and the existing main subjects.
|
|
88
|
+
Backgrounds include Seoul, Tokyo, Paris, Saint Petersburg, New York, Kyoto, forest,
|
|
89
|
+
mountains, sea, and space. Lighting and composition have their own libraries.
|
|
90
|
+
The interface is currently in Russian; generation instructions are in English.
|
|
91
|
+
|
|
92
|
+
The three libraries remain independent:
|
|
93
|
+
|
|
94
|
+
- **Prompt templates** supply one editable block of instructions.
|
|
95
|
+
- **Generation configurations** supply numerical rendering parameters.
|
|
96
|
+
- **Full presets** store a copy of all blocks, preservation choices, and parameters.
|
|
97
|
+
|
|
98
|
+
Updating a template does not rewrite saved full presets or historical jobs. Built-in
|
|
99
|
+
library upgrades replace only unmodified factory entries; edited user entries are
|
|
100
|
+
kept. Deleted entries are not reinstalled on every launch.
|
|
101
|
+
|
|
102
|
+
## Supported MLX adapters
|
|
103
|
+
|
|
104
|
+
- **MFLUX / Qwen Image Edit**: `qwen-image-edit` (2509) and `qwen-image-edit-2511`.
|
|
105
|
+
Qwen uses reference-image editing; it does **not** apply image strength.
|
|
106
|
+
- **MFLUX / FLUX.1 image-to-image**: `dev`, `schnell`, `krea-dev`.
|
|
107
|
+
FLUX schnell does not apply guidance. Higher FLUX image strength preserves more of the source image.
|
|
108
|
+
|
|
109
|
+
The server identifies the selected checkpoint independently of presets. It uses
|
|
110
|
+
MFLUX metadata, component configs, and the original repository folder name (including
|
|
111
|
+
HF snapshot paths). Keep the original HF name if the checkpoint has no MFLUX manifest.
|
|
112
|
+
Conflicting or unsupported metadata is rejected before a job is created. Pre-quantized MFLUX models can reduce download size and RAM needs.
|
|
113
|
+
The UI quantization setting is passed to MFLUX; checkpoint quantization may be fixed.
|
|
114
|
+
Generation is explicitly offline: missing components fail rather than download silently.
|
|
115
|
+
Only MLX engines are in scope; no PyTorch/MPS or other inference backends.
|
|
116
|
+
|
|
117
|
+
## Data
|
|
118
|
+
|
|
119
|
+
Default application data on macOS: `~/Library/Application Support/crayonmlx/`.
|
|
120
|
+
On first launch you can override it with `CRAYONMLX_DATA_DIR=/absolute/path crayonmlx`.
|
|
121
|
+
Settings → Internal data lets you copy settings, presets, history and the HF token
|
|
122
|
+
into an empty folder and use it after restart. The source is retained; model/image
|
|
123
|
+
paths are unchanged. Transfers require no active jobs or downloads and freeze
|
|
124
|
+
further writes until restart. The chosen location is recorded in `location.json`
|
|
125
|
+
in the platform's CrayonMLX configuration folder and takes precedence over the
|
|
126
|
+
bootstrap environment variable on subsequent launches.
|
|
127
|
+
|
|
128
|
+
- `crayonmlx.sqlite3`: settings and durable generation history.
|
|
129
|
+
- `presets/*.json`: full presets, initialized once.
|
|
130
|
+
- `prompt-templates/*.json`: reusable categorized prompt fragments.
|
|
131
|
+
- `generation-configs/*.json`: independent render configurations.
|
|
132
|
+
- `hf-token`: credential file with mode `0600`; never returned to the browser.
|
|
133
|
+
- Each output job folder: `source.png`, `result.png`, `generation.json` and execution measurements. Worker logs live in `runtime/<session>/worker.log`.
|
|
134
|
+
|
|
135
|
+
Changing the output directory affects new jobs. Old history keeps its original paths.
|
|
136
|
+
Moving or deleting those files makes their previews unavailable. Jobs run sequentially
|
|
137
|
+
and snapshot paths/parameters at submission. On restart, unfinished jobs become failed
|
|
138
|
+
and remain visible in read-only history. Graceful shutdown waits for current work to finish.
|
|
139
|
+
One separate MLX process keeps the current model resident between jobs. Models
|
|
140
|
+
are unloaded after 15 idle minutes by default; Settings accepts a custom timeout or
|
|
141
|
+
None to disable automatic unloading. Switching model or requested quantization
|
|
142
|
+
releases the previous process before loading another model. Manual load/unload
|
|
143
|
+
and enable/disable controls are available in Models. Disabling preserves files and
|
|
144
|
+
lets an already running generation finish.
|
|
145
|
+
|
|
146
|
+
Generation reports preparation, loading, completed denoising steps, decoding, and
|
|
147
|
+
elapsed time. MLX active allocations, cache, and peak are reported separately;
|
|
148
|
+
these are not the full process RSS. Successful history entries record execution
|
|
149
|
+
measurements separately from the requested configuration, including effective
|
|
150
|
+
quantization when reported by MFLUX.
|
|
151
|
+
|
|
152
|
+
The server binds to loopback only. There is no public API or user/account system.
|
|
153
|
+
HF tokens are intended only for downloading models, not for uploads or inference.
|
|
154
|
+
|
|
155
|
+
## Code layout
|
|
156
|
+
|
|
157
|
+
All application sources, including the frontend, are in `src/crayonmlx/`:
|
|
158
|
+
|
|
159
|
+
```text
|
|
160
|
+
app.py service composition and lifecycle
|
|
161
|
+
schemas.py validated request/configuration values
|
|
162
|
+
engines/ render contract, registry, MFLUX adapters
|
|
163
|
+
models/ local catalog and HF downloads
|
|
164
|
+
presets/ atomic JSON storage and starting prompts
|
|
165
|
+
generation/ serialized render jobs
|
|
166
|
+
storage/ SQLite history/settings and local credential
|
|
167
|
+
web/ internal UI routes, HTML, CSS and JS modules
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
To add an MLX engine, implement `engines/base.py` and register it in `engines/registry.py`.
|
|
171
|
+
Tests use a small injected renderer; they do not claim to test model inference.
|
|
172
|
+
Builds include frontend assets and expose the `crayonmlx` command for `uv tool install`.
|
|
173
|
+
|
|
174
|
+
## Publishing and contributing
|
|
175
|
+
|
|
176
|
+
See [PUBLISHING.md](PUBLISHING.md) for the maintainer guide: local build verification,
|
|
177
|
+
TestPyPI rehearsal, PyPI publication, and installation with `uv tool`.
|
|
178
|
+
The package has not been published by this setup process.
|
|
179
|
+
|
|
180
|
+
Keep application code in `src/crayonmlx/`, use small modules with clear responsibilities,
|
|
181
|
+
and add tests for changes to data compatibility, job snapshots, or engine behavior.
|
|
182
|
+
Run `uv run pytest` and `uv run ruff check src tests` before submitting changes.
|
|
183
|
+
Do not commit downloaded models, generated images, application data, or access tokens.
|
|
184
|
+
A project license has not yet been selected; add the chosen license before presenting
|
|
185
|
+
this repository as a licensed open-source release.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "crayonmlx"
|
|
3
|
+
version = "0.1.0a1"
|
|
4
|
+
description = "Local MLX studio for bringing drawings to life on Apple Silicon"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"mflux>=0.24,<0.25",
|
|
9
|
+
"fastapi>=0.162.0",
|
|
10
|
+
"huggingface-hub>=1.1.6,<2",
|
|
11
|
+
"pillow>=13.0.0",
|
|
12
|
+
"platformdirs>=4.14.0",
|
|
13
|
+
"python-multipart>=0.0.33",
|
|
14
|
+
"uvicorn[standard]>=0.54.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[[project.authors]]
|
|
18
|
+
name = "Nikolay Nechaev"
|
|
19
|
+
email = "godlikeguest@gmail.com"
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
crayonmlx = "crayonmlx.cli:main"
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["uv_build>=0.12.19,<0.13.0"]
|
|
26
|
+
build-backend = "uv_build"
|
|
27
|
+
|
|
28
|
+
[dependency-groups]
|
|
29
|
+
dev = [
|
|
30
|
+
"httpx>=0.28.1",
|
|
31
|
+
"pytest>=9.2.0",
|
|
32
|
+
"ruff>=0.17.3",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[tool.ruff]
|
|
36
|
+
line-length = 110
|
|
37
|
+
target-version = "py312"
|
|
38
|
+
|
|
39
|
+
[tool.ruff.lint]
|
|
40
|
+
select = [
|
|
41
|
+
"E4",
|
|
42
|
+
"E7",
|
|
43
|
+
"E9",
|
|
44
|
+
"F",
|
|
45
|
+
"I",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "crayonmlx"
|
|
3
|
+
version = "0.1.0a1"
|
|
4
|
+
description = "Local MLX studio for bringing drawings to life on Apple Silicon"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Nikolay Nechaev", email = "godlikeguest@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"mflux>=0.24,<0.25",
|
|
12
|
+
"fastapi>=0.162.0",
|
|
13
|
+
"huggingface-hub>=1.1.6,<2",
|
|
14
|
+
"pillow>=13.0.0",
|
|
15
|
+
"platformdirs>=4.14.0",
|
|
16
|
+
"python-multipart>=0.0.33",
|
|
17
|
+
"uvicorn[standard]>=0.54.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
crayonmlx = "crayonmlx.cli:main"
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["uv_build>=0.12.19,<0.13.0"]
|
|
25
|
+
build-backend = "uv_build"
|
|
26
|
+
|
|
27
|
+
[dependency-groups]
|
|
28
|
+
dev = [
|
|
29
|
+
"httpx>=0.28.1",
|
|
30
|
+
"pytest>=9.2.0",
|
|
31
|
+
"ruff>=0.17.3",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[tool.ruff]
|
|
35
|
+
line-length = 110
|
|
36
|
+
target-version = "py312"
|
|
37
|
+
|
|
38
|
+
[tool.ruff.lint]
|
|
39
|
+
select = ["E4", "E7", "E9", "F", "I"]
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Application composition: construct services once and close workers on shutdown."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from fastapi import FastAPI, Request
|
|
10
|
+
from fastapi.responses import FileResponse, JSONResponse
|
|
11
|
+
from fastapi.staticfiles import StaticFiles
|
|
12
|
+
from starlette.middleware.trustedhost import TrustedHostMiddleware
|
|
13
|
+
|
|
14
|
+
from crayonmlx.engines.registry import default_engines
|
|
15
|
+
from crayonmlx.generation.queue import GenerationQueue
|
|
16
|
+
from crayonmlx.models.downloads import Downloads
|
|
17
|
+
from crayonmlx.models.runtime import ModelRuntime
|
|
18
|
+
from crayonmlx.paths import data_directory
|
|
19
|
+
from crayonmlx.presets.library import ConfigRepository, PromptRepository
|
|
20
|
+
from crayonmlx.presets.repository import PresetRepository
|
|
21
|
+
from crayonmlx.storage.database import Database
|
|
22
|
+
from crayonmlx.storage.secrets import TokenStore
|
|
23
|
+
from crayonmlx.web.folders import router as folders_router
|
|
24
|
+
from crayonmlx.web.library_routes import router as library_router
|
|
25
|
+
from crayonmlx.web.model_routes import router as model_router
|
|
26
|
+
from crayonmlx.web.routes import router
|
|
27
|
+
from crayonmlx.web.storage_routes import router as storage_router
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class Services:
|
|
32
|
+
root: Path
|
|
33
|
+
database: Database
|
|
34
|
+
presets: PresetRepository
|
|
35
|
+
prompts: PromptRepository
|
|
36
|
+
configs: ConfigRepository
|
|
37
|
+
tokens: TokenStore
|
|
38
|
+
engines: dict
|
|
39
|
+
queue: GenerationQueue
|
|
40
|
+
downloads: Downloads
|
|
41
|
+
runtime: ModelRuntime
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def create_app(data_dir: Path | None = None, engines: dict | None = None) -> FastAPI:
|
|
45
|
+
@asynccontextmanager
|
|
46
|
+
async def lifespan(app):
|
|
47
|
+
root = (data_dir or data_directory()).resolve()
|
|
48
|
+
database = Database(root)
|
|
49
|
+
runtime = ModelRuntime(root / "runtime", database.settings)
|
|
50
|
+
registry = engines if engines is not None else default_engines(runtime)
|
|
51
|
+
tokens = TokenStore(root)
|
|
52
|
+
queue = GenerationQueue(database, registry)
|
|
53
|
+
downloads = Downloads(tokens)
|
|
54
|
+
app.state.services = Services(
|
|
55
|
+
root,
|
|
56
|
+
database,
|
|
57
|
+
PresetRepository(root),
|
|
58
|
+
PromptRepository(root),
|
|
59
|
+
ConfigRepository(root),
|
|
60
|
+
tokens,
|
|
61
|
+
registry,
|
|
62
|
+
queue,
|
|
63
|
+
downloads,
|
|
64
|
+
runtime,
|
|
65
|
+
)
|
|
66
|
+
try:
|
|
67
|
+
yield
|
|
68
|
+
finally:
|
|
69
|
+
queue.close()
|
|
70
|
+
runtime.close()
|
|
71
|
+
downloads.close()
|
|
72
|
+
|
|
73
|
+
app = FastAPI(title="CrayonMLX", lifespan=lifespan, docs_url=None, redoc_url=None, openapi_url=None)
|
|
74
|
+
app.add_middleware(TrustedHostMiddleware, allowed_hosts=["localhost", "127.0.0.1", "[::1]", "testserver"])
|
|
75
|
+
|
|
76
|
+
write_lock = asyncio.Lock()
|
|
77
|
+
app.state.pending_data_dir = None
|
|
78
|
+
|
|
79
|
+
@app.middleware("http")
|
|
80
|
+
async def local_requests(request: Request, call_next):
|
|
81
|
+
# Local disk operations must not be triggered by a form on a remote website.
|
|
82
|
+
if request.method not in {"GET", "HEAD", "OPTIONS"}:
|
|
83
|
+
origin = request.headers.get("origin")
|
|
84
|
+
if (origin and origin != str(request.base_url).rstrip("/")) or request.headers.get(
|
|
85
|
+
"sec-fetch-site"
|
|
86
|
+
) == "cross-site":
|
|
87
|
+
return JSONResponse(
|
|
88
|
+
{"detail": "Разрешены только запросы из локального интерфейса"}, status_code=403
|
|
89
|
+
)
|
|
90
|
+
if request.method not in {"GET", "HEAD", "OPTIONS"}:
|
|
91
|
+
async with write_lock:
|
|
92
|
+
if app.state.pending_data_dir:
|
|
93
|
+
return JSONResponse(
|
|
94
|
+
{
|
|
95
|
+
"detail": "Папка данных перенесена. Перезапустите CrayonMLX, чтобы продолжить работу."
|
|
96
|
+
},
|
|
97
|
+
status_code=409,
|
|
98
|
+
)
|
|
99
|
+
response = await call_next(request)
|
|
100
|
+
else:
|
|
101
|
+
response = await call_next(request)
|
|
102
|
+
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
103
|
+
response.headers["Content-Security-Policy"] = (
|
|
104
|
+
"default-src 'self'; img-src 'self' blob:; style-src 'self'; script-src 'self'; frame-ancestors 'none'"
|
|
105
|
+
)
|
|
106
|
+
return response
|
|
107
|
+
|
|
108
|
+
@app.exception_handler(ValueError)
|
|
109
|
+
async def invalid_value(request, error):
|
|
110
|
+
return JSONResponse({"detail": str(error)}, status_code=422)
|
|
111
|
+
|
|
112
|
+
@app.exception_handler(OSError)
|
|
113
|
+
async def filesystem_error(request, error):
|
|
114
|
+
logging.getLogger(__name__).exception("Filesystem operation failed")
|
|
115
|
+
return JSONResponse(
|
|
116
|
+
{
|
|
117
|
+
"detail": "Не удалось прочитать или записать файл. Проверьте путь, права доступа и свободное место."
|
|
118
|
+
},
|
|
119
|
+
status_code=400,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
web = Path(__file__).parent / "web"
|
|
123
|
+
app.include_router(router)
|
|
124
|
+
app.include_router(library_router)
|
|
125
|
+
app.include_router(folders_router)
|
|
126
|
+
app.include_router(model_router)
|
|
127
|
+
app.include_router(storage_router)
|
|
128
|
+
app.mount("/static", StaticFiles(directory=web / "static"), name="static")
|
|
129
|
+
|
|
130
|
+
@app.get("/")
|
|
131
|
+
def index():
|
|
132
|
+
return FileResponse(web / "index.html")
|
|
133
|
+
|
|
134
|
+
return app
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Local-only application entry point."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
import uvicorn
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
parser = argparse.ArgumentParser(description="CrayonMLX — локальная студия для рисунков")
|
|
10
|
+
parser.add_argument("--port", type=int, default=8787)
|
|
11
|
+
arguments = parser.parse_args()
|
|
12
|
+
uvicorn.run("crayonmlx.app:create_app", factory=True, host="127.0.0.1", port=arguments.port)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CrayonMLX engines components."""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Render contract. Adapters render through the application-owned model runtime."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
from crayonmlx.schemas import Preset
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RenderEngine(Protocol):
|
|
10
|
+
id: str
|
|
11
|
+
name: str
|
|
12
|
+
base_models: tuple[str, ...]
|
|
13
|
+
supports_strength: bool
|
|
14
|
+
|
|
15
|
+
def available(self) -> bool: ...
|
|
16
|
+
|
|
17
|
+
def render(self, preset: Preset, model: Path, source: Path, output: Path) -> dict | None: ...
|