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.
Files changed (52) hide show
  1. crayonmlx-0.1.0a1/PKG-INFO +201 -0
  2. crayonmlx-0.1.0a1/README.md +185 -0
  3. crayonmlx-0.1.0a1/pyproject.toml +49 -0
  4. crayonmlx-0.1.0a1/pyproject.toml.orig +42 -0
  5. crayonmlx-0.1.0a1/src/crayonmlx/__init__.py +3 -0
  6. crayonmlx-0.1.0a1/src/crayonmlx/__main__.py +3 -0
  7. crayonmlx-0.1.0a1/src/crayonmlx/app.py +134 -0
  8. crayonmlx-0.1.0a1/src/crayonmlx/cli.py +12 -0
  9. crayonmlx-0.1.0a1/src/crayonmlx/engines/__init__.py +1 -0
  10. crayonmlx-0.1.0a1/src/crayonmlx/engines/base.py +17 -0
  11. crayonmlx-0.1.0a1/src/crayonmlx/engines/mflux.py +91 -0
  12. crayonmlx-0.1.0a1/src/crayonmlx/engines/process.py +59 -0
  13. crayonmlx-0.1.0a1/src/crayonmlx/engines/registry.py +20 -0
  14. crayonmlx-0.1.0a1/src/crayonmlx/engines/worker.py +137 -0
  15. crayonmlx-0.1.0a1/src/crayonmlx/generation/__init__.py +1 -0
  16. crayonmlx-0.1.0a1/src/crayonmlx/generation/queue.py +87 -0
  17. crayonmlx-0.1.0a1/src/crayonmlx/models/__init__.py +1 -0
  18. crayonmlx-0.1.0a1/src/crayonmlx/models/catalog.py +40 -0
  19. crayonmlx-0.1.0a1/src/crayonmlx/models/downloads.py +63 -0
  20. crayonmlx-0.1.0a1/src/crayonmlx/models/identity.py +114 -0
  21. crayonmlx-0.1.0a1/src/crayonmlx/models/runtime.py +158 -0
  22. crayonmlx-0.1.0a1/src/crayonmlx/paths.py +33 -0
  23. crayonmlx-0.1.0a1/src/crayonmlx/presets/__init__.py +1 -0
  24. crayonmlx-0.1.0a1/src/crayonmlx/presets/catalog.py +70 -0
  25. crayonmlx-0.1.0a1/src/crayonmlx/presets/defaults.py +24 -0
  26. crayonmlx-0.1.0a1/src/crayonmlx/presets/legacy_defaults.py +37 -0
  27. crayonmlx-0.1.0a1/src/crayonmlx/presets/legacy_styles.py +50 -0
  28. crayonmlx-0.1.0a1/src/crayonmlx/presets/library.py +56 -0
  29. crayonmlx-0.1.0a1/src/crayonmlx/presets/repository.py +77 -0
  30. crayonmlx-0.1.0a1/src/crayonmlx/presets/styles.py +28 -0
  31. crayonmlx-0.1.0a1/src/crayonmlx/py.typed +0 -0
  32. crayonmlx-0.1.0a1/src/crayonmlx/schemas.py +131 -0
  33. crayonmlx-0.1.0a1/src/crayonmlx/storage/__init__.py +1 -0
  34. crayonmlx-0.1.0a1/src/crayonmlx/storage/database.py +112 -0
  35. crayonmlx-0.1.0a1/src/crayonmlx/storage/location.py +69 -0
  36. crayonmlx-0.1.0a1/src/crayonmlx/storage/secrets.py +21 -0
  37. crayonmlx-0.1.0a1/src/crayonmlx/web/__init__.py +1 -0
  38. crayonmlx-0.1.0a1/src/crayonmlx/web/folders.py +86 -0
  39. crayonmlx-0.1.0a1/src/crayonmlx/web/index.html +420 -0
  40. crayonmlx-0.1.0a1/src/crayonmlx/web/library_routes.py +78 -0
  41. crayonmlx-0.1.0a1/src/crayonmlx/web/model_routes.py +70 -0
  42. crayonmlx-0.1.0a1/src/crayonmlx/web/routes.py +177 -0
  43. crayonmlx-0.1.0a1/src/crayonmlx/web/static/api.js +53 -0
  44. crayonmlx-0.1.0a1/src/crayonmlx/web/static/app.js +88 -0
  45. crayonmlx-0.1.0a1/src/crayonmlx/web/static/forms.js +105 -0
  46. crayonmlx-0.1.0a1/src/crayonmlx/web/static/generation.js +175 -0
  47. crayonmlx-0.1.0a1/src/crayonmlx/web/static/library.js +305 -0
  48. crayonmlx-0.1.0a1/src/crayonmlx/web/static/preservation.js +74 -0
  49. crayonmlx-0.1.0a1/src/crayonmlx/web/static/preset-editor.js +260 -0
  50. crayonmlx-0.1.0a1/src/crayonmlx/web/static/presets.js +210 -0
  51. crayonmlx-0.1.0a1/src/crayonmlx/web/static/style.css +784 -0
  52. 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,3 @@
1
+ """CrayonMLX — a local drawing transformation studio for Apple Silicon."""
2
+
3
+ __version__ = "0.1.0a1"
@@ -0,0 +1,3 @@
1
+ from crayonmlx.cli import main
2
+
3
+ main()
@@ -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: ...