limn 0.1.0__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.
- limn-0.1.0/.coverage +0 -0
- limn-0.1.0/.gitignore +29 -0
- limn-0.1.0/CLAUDE.md +70 -0
- limn-0.1.0/PKG-INFO +130 -0
- limn-0.1.0/README.md +98 -0
- limn-0.1.0/SPEC.md +152 -0
- limn-0.1.0/pyproject.toml +119 -0
- limn-0.1.0/src/limn/__init__.py +3 -0
- limn-0.1.0/src/limn/__main__.py +6 -0
- limn-0.1.0/src/limn/cli.py +150 -0
- limn-0.1.0/src/limn/config.py +199 -0
- limn-0.1.0/src/limn/core.py +111 -0
- limn-0.1.0/src/limn/providers/__init__.py +47 -0
- limn-0.1.0/src/limn/providers/base.py +66 -0
- limn-0.1.0/src/limn/providers/gemini.py +119 -0
- limn-0.1.0/src/limn/providers/openai_compat.py +134 -0
- limn-0.1.0/src/limn/providers/swarmui.py +89 -0
- limn-0.1.0/tests/__init__.py +0 -0
- limn-0.1.0/tests/conftest.py +52 -0
- limn-0.1.0/tests/test_cli.py +115 -0
- limn-0.1.0/tests/test_config.py +104 -0
- limn-0.1.0/tests/test_core.py +98 -0
- limn-0.1.0/tests/test_providers.py +242 -0
- limn-0.1.0/uv.lock +1229 -0
limn-0.1.0/.coverage
ADDED
|
Binary file
|
limn-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
|
|
11
|
+
# Config / secrets
|
|
12
|
+
limn.yaml
|
|
13
|
+
.env
|
|
14
|
+
|
|
15
|
+
# Generated images / scratch
|
|
16
|
+
outputs/
|
|
17
|
+
*.png
|
|
18
|
+
*.jpg
|
|
19
|
+
*.jpeg
|
|
20
|
+
*.webp
|
|
21
|
+
!docs/**/*.png
|
|
22
|
+
|
|
23
|
+
# macOS
|
|
24
|
+
.DS_Store
|
|
25
|
+
._*
|
|
26
|
+
|
|
27
|
+
# Editors
|
|
28
|
+
.vscode/
|
|
29
|
+
.idea/
|
limn-0.1.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Limn — agent handover / working notes
|
|
2
|
+
|
|
3
|
+
This file is auto-loaded when you open a Claude Code session in this repo. It
|
|
4
|
+
tells you what Limn is, where things stand, and how to start. **`SPEC.md` is the
|
|
5
|
+
source of truth** — read it before writing any code.
|
|
6
|
+
|
|
7
|
+
## What Limn is
|
|
8
|
+
|
|
9
|
+
A small, privacy-first **text-to-image** tool: type a prompt, get an image. No
|
|
10
|
+
account, minimal options, bring your own provider. Surfaces (in order): a CLI, a
|
|
11
|
+
simple web UI (generate / view / save / delete / regenerate), then a Tauri
|
|
12
|
+
desktop app with bring-your-own-provider, plus a rate-limited, non-storing demo.
|
|
13
|
+
|
|
14
|
+
## Current status
|
|
15
|
+
|
|
16
|
+
**M1 (CLI) built and published to PyPI as `limn` 0.1.0.** The package lives in
|
|
17
|
+
`src/limn/`: `config.py` (layered ~/.limn.yaml < ./limn.yaml < flags, `${VAR}`
|
|
18
|
+
expansion, per-provider `providers:` overlays), `providers/` (swarmui,
|
|
19
|
+
openai, openai-compatible, gemini/imagen — all plain `requests`, no SDKs),
|
|
20
|
+
`core.py` (generate + save with slug naming / no-overwrite), `cli.py` (Typer,
|
|
21
|
+
single command). Tests in `tests/` (45, all green), basedpyright zero errors,
|
|
22
|
+
ruff clean. Verified end-to-end against the real Imagen API.
|
|
23
|
+
|
|
24
|
+
Design choices made during M1 (beyond SPEC):
|
|
25
|
+
- Providers raise `ProviderError` and the CLI exits non-zero — **no** silent
|
|
26
|
+
text-image fallback like slide-stream (that only makes sense in a video
|
|
27
|
+
pipeline).
|
|
28
|
+
- `openai`/`dalle` and `openai-compatible` share one class; the compatible
|
|
29
|
+
variant *requires* `base_url` so a stray OPENAI_API_KEY never sends prompts
|
|
30
|
+
to the wrong server.
|
|
31
|
+
- Imagen (Gemini API) gets nearest-aspect-ratio mapping from `--size`;
|
|
32
|
+
seed/negative are warn-and-ignore on backends that lack them (OpenAI,
|
|
33
|
+
Gemini API).
|
|
34
|
+
|
|
35
|
+
## Machine gotcha (this repo lives on an exFAT drive)
|
|
36
|
+
|
|
37
|
+
`uv sync` with a local `.venv` fails here: macOS writes AppleDouble `._*`
|
|
38
|
+
files (SIP-protected `com.apple.provenance` xattr) into binary wheels (ruff,
|
|
39
|
+
basedpyright) and uv's RECORD validation rejects them. Workaround used:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
export UV_PROJECT_ENVIRONMENT="$HOME/.venvs/limn" # venv on internal disk
|
|
43
|
+
uv sync --all-groups
|
|
44
|
+
uv run --no-sync ruff check src tests && uv run --no-sync basedpyright && uv run --no-sync pytest
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Next milestones
|
|
48
|
+
|
|
49
|
+
M2 (web UI, `limn serve` behind a `[serve]` extra, like `slide-stream serve`),
|
|
50
|
+
M3 (hosted demo), M4 (Tauri desktop). Release flow: bump version in
|
|
51
|
+
`pyproject.toml` + `src/limn/__init__.py`, `uv build`, `twine upload dist/*`.
|
|
52
|
+
|
|
53
|
+
## Key decisions already made
|
|
54
|
+
|
|
55
|
+
- **Standalone**, not a slide-stream dependency (slide-stream is a video tool).
|
|
56
|
+
Reuse the *pattern*, not the package. Extract a shared image-providers lib
|
|
57
|
+
later only if it clearly pays off.
|
|
58
|
+
- **Generative providers only** (Limn *makes* images) — no Pexels/Unsplash stock
|
|
59
|
+
unless the user asks.
|
|
60
|
+
- **Minimal knobs** on purpose: prompt, size, count, seed. Advanced params live
|
|
61
|
+
in the provider. Don't add options without a reason.
|
|
62
|
+
- **Privacy-first:** no account, no telemetry, no server-side storage of prompts
|
|
63
|
+
or images; the demo is rate-limited and ephemeral.
|
|
64
|
+
|
|
65
|
+
## Reference
|
|
66
|
+
|
|
67
|
+
- Companion project: `../slide-stream` (on this machine) —
|
|
68
|
+
https://github.com/michael-borck/slide-stream, published on PyPI. Its image
|
|
69
|
+
providers, layered config, and `serve` web UI are the templates to adapt.
|
|
70
|
+
- Repo: https://github.com/michael-borck/limn (public).
|
limn-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: limn
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A small, privacy-first text-to-image tool: type a prompt, get an image. Bring your own provider.
|
|
5
|
+
Project-URL: Homepage, https://github.com/michael-borck/limn
|
|
6
|
+
Project-URL: Repository, https://github.com/michael-borck/limn
|
|
7
|
+
Project-URL: Issues, https://github.com/michael-borck/limn/issues
|
|
8
|
+
Author-email: Michael Borck <michael.borck@curtin.edu.au>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: ai,cli,dall-e,image,imagen,swarmui,text-to-image
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
25
|
+
Requires-Dist: requests>=2.31.0
|
|
26
|
+
Requires-Dist: rich>=13.0.0
|
|
27
|
+
Requires-Dist: typer>=0.9.0
|
|
28
|
+
Provides-Extra: serve
|
|
29
|
+
Requires-Dist: fastapi>=0.110.0; extra == 'serve'
|
|
30
|
+
Requires-Dist: uvicorn>=0.29.0; extra == 'serve'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# 🖼️ Limn
|
|
34
|
+
|
|
35
|
+
> *to **limn**: to depict or describe; to paint.*
|
|
36
|
+
|
|
37
|
+
A small, privacy-first tool that turns a text description into an image. Type a
|
|
38
|
+
prompt, get a picture — no account, minimal options, bring your own provider.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install limn
|
|
42
|
+
|
|
43
|
+
limn "a red bicycle against a brick wall" -o bike.png
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- **Type a description → get an image.** That's the whole thing.
|
|
47
|
+
- **No account, private by default.** Your prompts and images are yours. The
|
|
48
|
+
only network egress is the generation request to the provider *you* chose.
|
|
49
|
+
- **Bring your own provider** — self-hosted (SwarmUI / any OpenAI-compatible
|
|
50
|
+
endpoint) or a cloud key (Google Imagen, OpenAI / DALL·E). Swap with one
|
|
51
|
+
setting.
|
|
52
|
+
- **Minimal by design.** Prompt, size, count, seed. Want power? Install
|
|
53
|
+
something bigger — Limn stays lean.
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
Pick whichever provider you already have:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Google Imagen (cheap, ~$0.02/image on the Fast tier)
|
|
61
|
+
export GEMINI_API_KEY=...
|
|
62
|
+
limn "watercolour fox" --provider gemini
|
|
63
|
+
|
|
64
|
+
# OpenAI Images
|
|
65
|
+
export OPENAI_API_KEY=...
|
|
66
|
+
limn "watercolour fox" --provider openai
|
|
67
|
+
|
|
68
|
+
# Self-hosted SwarmUI
|
|
69
|
+
export SWARMUI_BASE_URL=https://image.example.org
|
|
70
|
+
limn "watercolour fox" --provider swarmui
|
|
71
|
+
|
|
72
|
+
# Any OpenAI-compatible /v1/images endpoint (LocalAI, ...)
|
|
73
|
+
export OPENAI_BASE_URL=http://localhost:8080/v1
|
|
74
|
+
limn "watercolour fox" --provider openai-compatible
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Then set your provider once so you never pass `--provider` again:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
limn --init-config # writes a commented ./limn.yaml template
|
|
81
|
+
mv limn.yaml ~/.limn.yaml # personal defaults, used everywhere
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Usage
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
limn "a red bicycle against a brick wall" -o bike.png
|
|
88
|
+
limn "watercolour fox" --provider swarmui --size 1024x1024 --count 4 --seed 42
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
| Option | What |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `-p, --provider` | `swarmui`, `openai-compatible`, `openai`, `gemini` |
|
|
94
|
+
| `-m, --model` | Model name (provider-specific) |
|
|
95
|
+
| `-s, --size` | e.g. `1024x1024` (Imagen maps to the nearest aspect ratio) |
|
|
96
|
+
| `-n, --count` | Number of images (1–10) |
|
|
97
|
+
| `--seed` | Reproducibility, where the backend supports it (SwarmUI) |
|
|
98
|
+
| `--negative` | Negative prompt, where supported (SwarmUI) |
|
|
99
|
+
| `-o, --out` | Output file; default is a slug of the prompt, never overwritten |
|
|
100
|
+
| `-c, --config` | Explicit config file |
|
|
101
|
+
|
|
102
|
+
## Config
|
|
103
|
+
|
|
104
|
+
Layered, later wins: built-in defaults → `~/.limn.yaml` (your provider + keys,
|
|
105
|
+
set once) → `./limn.yaml` → CLI flags. Secrets are referenced as `${VAR}` so no
|
|
106
|
+
plaintext keys live in files:
|
|
107
|
+
|
|
108
|
+
```yaml
|
|
109
|
+
provider: swarmui
|
|
110
|
+
size: [1024, 1024]
|
|
111
|
+
|
|
112
|
+
providers: # keep several configured; swap with `provider:`
|
|
113
|
+
swarmui:
|
|
114
|
+
base_url: https://image.example.org
|
|
115
|
+
model: juggernautXL_v9
|
|
116
|
+
api_key: "${SWARMUI_TOKEN}"
|
|
117
|
+
gemini:
|
|
118
|
+
model: imagen-4.0-fast-generate-001
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Roadmap
|
|
122
|
+
|
|
123
|
+
A **CLI** (this release), then a simple **web UI** (`limn serve`: type →
|
|
124
|
+
generate → view → save / delete / regenerate), then a **Tauri desktop app**
|
|
125
|
+
with bring-your-own-provider, plus a rate-limited, non-storing **demo**.
|
|
126
|
+
|
|
127
|
+
📄 See **[SPEC.md](SPEC.md)** for the full specification.
|
|
128
|
+
|
|
129
|
+
Companion to [slide-stream](https://github.com/michael-borck/slide-stream),
|
|
130
|
+
which shares the same image-provider approach.
|
limn-0.1.0/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# 🖼️ Limn
|
|
2
|
+
|
|
3
|
+
> *to **limn**: to depict or describe; to paint.*
|
|
4
|
+
|
|
5
|
+
A small, privacy-first tool that turns a text description into an image. Type a
|
|
6
|
+
prompt, get a picture — no account, minimal options, bring your own provider.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install limn
|
|
10
|
+
|
|
11
|
+
limn "a red bicycle against a brick wall" -o bike.png
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
- **Type a description → get an image.** That's the whole thing.
|
|
15
|
+
- **No account, private by default.** Your prompts and images are yours. The
|
|
16
|
+
only network egress is the generation request to the provider *you* chose.
|
|
17
|
+
- **Bring your own provider** — self-hosted (SwarmUI / any OpenAI-compatible
|
|
18
|
+
endpoint) or a cloud key (Google Imagen, OpenAI / DALL·E). Swap with one
|
|
19
|
+
setting.
|
|
20
|
+
- **Minimal by design.** Prompt, size, count, seed. Want power? Install
|
|
21
|
+
something bigger — Limn stays lean.
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
Pick whichever provider you already have:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Google Imagen (cheap, ~$0.02/image on the Fast tier)
|
|
29
|
+
export GEMINI_API_KEY=...
|
|
30
|
+
limn "watercolour fox" --provider gemini
|
|
31
|
+
|
|
32
|
+
# OpenAI Images
|
|
33
|
+
export OPENAI_API_KEY=...
|
|
34
|
+
limn "watercolour fox" --provider openai
|
|
35
|
+
|
|
36
|
+
# Self-hosted SwarmUI
|
|
37
|
+
export SWARMUI_BASE_URL=https://image.example.org
|
|
38
|
+
limn "watercolour fox" --provider swarmui
|
|
39
|
+
|
|
40
|
+
# Any OpenAI-compatible /v1/images endpoint (LocalAI, ...)
|
|
41
|
+
export OPENAI_BASE_URL=http://localhost:8080/v1
|
|
42
|
+
limn "watercolour fox" --provider openai-compatible
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then set your provider once so you never pass `--provider` again:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
limn --init-config # writes a commented ./limn.yaml template
|
|
49
|
+
mv limn.yaml ~/.limn.yaml # personal defaults, used everywhere
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
limn "a red bicycle against a brick wall" -o bike.png
|
|
56
|
+
limn "watercolour fox" --provider swarmui --size 1024x1024 --count 4 --seed 42
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
| Option | What |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `-p, --provider` | `swarmui`, `openai-compatible`, `openai`, `gemini` |
|
|
62
|
+
| `-m, --model` | Model name (provider-specific) |
|
|
63
|
+
| `-s, --size` | e.g. `1024x1024` (Imagen maps to the nearest aspect ratio) |
|
|
64
|
+
| `-n, --count` | Number of images (1–10) |
|
|
65
|
+
| `--seed` | Reproducibility, where the backend supports it (SwarmUI) |
|
|
66
|
+
| `--negative` | Negative prompt, where supported (SwarmUI) |
|
|
67
|
+
| `-o, --out` | Output file; default is a slug of the prompt, never overwritten |
|
|
68
|
+
| `-c, --config` | Explicit config file |
|
|
69
|
+
|
|
70
|
+
## Config
|
|
71
|
+
|
|
72
|
+
Layered, later wins: built-in defaults → `~/.limn.yaml` (your provider + keys,
|
|
73
|
+
set once) → `./limn.yaml` → CLI flags. Secrets are referenced as `${VAR}` so no
|
|
74
|
+
plaintext keys live in files:
|
|
75
|
+
|
|
76
|
+
```yaml
|
|
77
|
+
provider: swarmui
|
|
78
|
+
size: [1024, 1024]
|
|
79
|
+
|
|
80
|
+
providers: # keep several configured; swap with `provider:`
|
|
81
|
+
swarmui:
|
|
82
|
+
base_url: https://image.example.org
|
|
83
|
+
model: juggernautXL_v9
|
|
84
|
+
api_key: "${SWARMUI_TOKEN}"
|
|
85
|
+
gemini:
|
|
86
|
+
model: imagen-4.0-fast-generate-001
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Roadmap
|
|
90
|
+
|
|
91
|
+
A **CLI** (this release), then a simple **web UI** (`limn serve`: type →
|
|
92
|
+
generate → view → save / delete / regenerate), then a **Tauri desktop app**
|
|
93
|
+
with bring-your-own-provider, plus a rate-limited, non-storing **demo**.
|
|
94
|
+
|
|
95
|
+
📄 See **[SPEC.md](SPEC.md)** for the full specification.
|
|
96
|
+
|
|
97
|
+
Companion to [slide-stream](https://github.com/michael-borck/slide-stream),
|
|
98
|
+
which shares the same image-provider approach.
|
limn-0.1.0/SPEC.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Limn — specification
|
|
2
|
+
|
|
3
|
+
> *to **limn** (verb): to depict or describe; to paint.*
|
|
4
|
+
|
|
5
|
+
A small, privacy-first tool that turns a text description into an image. Type a
|
|
6
|
+
prompt, get a picture. No account, minimal options, bring your own provider.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 1. Vision
|
|
11
|
+
|
|
12
|
+
Most image tools are either a single locked-in web app (an account, your data on
|
|
13
|
+
their servers) or a full studio (ComfyUI/SwarmUI — powerful but heavy). **Limn
|
|
14
|
+
sits deliberately in between:** the simplest possible "text → image" surface,
|
|
15
|
+
that you own.
|
|
16
|
+
|
|
17
|
+
- **Type a description → get an image.** That's the whole interaction.
|
|
18
|
+
- **No account, no lock-in, privacy by default.** Your prompts and images are
|
|
19
|
+
yours. On the desktop, nothing leaves your machine except the generation
|
|
20
|
+
request to *your* chosen provider.
|
|
21
|
+
- **Bring your own provider.** Point Limn at a self-hosted server (SwarmUI /
|
|
22
|
+
any OpenAI-compatible endpoint) or a cloud key (Google Imagen, OpenAI /
|
|
23
|
+
DALL·E). Swap it with one setting.
|
|
24
|
+
- **Minimal by design.** A handful of knobs (prompt, size, count, seed). If you
|
|
25
|
+
want inpainting, ControlNet, and node graphs, install something bigger —
|
|
26
|
+
Limn stays lean.
|
|
27
|
+
|
|
28
|
+
Think "Black Forest Labs playground, but standalone, private, and yours."
|
|
29
|
+
|
|
30
|
+
## 2. Principles
|
|
31
|
+
|
|
32
|
+
1. **Simple beats powerful.** One prompt in, one image out. Advanced parameters
|
|
33
|
+
live in the provider, not in Limn.
|
|
34
|
+
2. **Local-first & private.** No account, no telemetry, no server-side storage
|
|
35
|
+
of prompts or images (desktop is fully local). The hosted demo is
|
|
36
|
+
rate-limited and stores nothing.
|
|
37
|
+
3. **Provider-agnostic.** The same thin provider abstraction as `slide-stream`'s
|
|
38
|
+
image layer — self-hosted or cloud, chosen by config.
|
|
39
|
+
4. **Progressive surfaces.** The same core powers a CLI, a web UI, and a desktop
|
|
40
|
+
app; each is optional.
|
|
41
|
+
|
|
42
|
+
## 3. Surfaces (roadmap)
|
|
43
|
+
|
|
44
|
+
### v1 — CLI
|
|
45
|
+
```
|
|
46
|
+
limn "a red bicycle against a brick wall" -o bike.png
|
|
47
|
+
limn "watercolour fox" --provider swarmui --size 1024x1024 --count 4 --seed 42
|
|
48
|
+
```
|
|
49
|
+
Options: `--provider`, `--model`, `--size`, `--count`, `--seed`, `--negative`,
|
|
50
|
+
`--out`. Provider selection + API keys from a config file and/or environment
|
|
51
|
+
variables. Prints the saved path(s).
|
|
52
|
+
|
|
53
|
+
### v2 — Web UI (`limn serve`)
|
|
54
|
+
A single page: type a prompt → **Generate** → the image appears in-app →
|
|
55
|
+
**Save** (to disk), **Delete** (from the session gallery), or **Generate
|
|
56
|
+
another**. A running gallery of the session's images (prompt, thumbnail, seed).
|
|
57
|
+
FastAPI + a self-contained HTML page (same shape as `slide-stream serve`).
|
|
58
|
+
Local-first (binds `127.0.0.1`, auto-opens the browser); optional token when
|
|
59
|
+
hosted.
|
|
60
|
+
|
|
61
|
+
### v3 — Desktop (Tauri + Python sidecar)
|
|
62
|
+
A native, installable app. Users set **their own image provider** in Settings
|
|
63
|
+
(server URL or API key). Images are saved locally; the app stores nothing
|
|
64
|
+
remotely. Tauri (Rust) shell wrapping the Python core as a sidecar, with the
|
|
65
|
+
runtime bootstrapped on first run via `uv` (thin installer, not a 1GB bundle) —
|
|
66
|
+
the pattern explored for slide-stream. Not on the Mac App Store (runtime
|
|
67
|
+
download rules); distributed as a signed `.dmg` / installer.
|
|
68
|
+
|
|
69
|
+
### Demo — "try before install"
|
|
70
|
+
A hosted instance (`app.limn.<domain>`) that is **rate-limited and
|
|
71
|
+
non-storing**: generate a few images to try it, nothing saved server-side, with
|
|
72
|
+
a banner inviting users to install locally for full control and privacy. Same
|
|
73
|
+
`--demo` flag idea as slide-stream.
|
|
74
|
+
|
|
75
|
+
## 4. Providers (v1 set)
|
|
76
|
+
|
|
77
|
+
Generative backends only (Limn *makes* images; it is not a stock-photo
|
|
78
|
+
browser):
|
|
79
|
+
|
|
80
|
+
| Provider | What | Config |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `swarmui` | Self-hosted SwarmUI native API | `base_url`, `model`, optional Bearer `api_key` |
|
|
83
|
+
| `openai-compatible` | Any `/v1/images` endpoint (LocalAI, …) | `base_url`, `model`, `api_key` |
|
|
84
|
+
| `openai` / `dalle` | OpenAI Images (DALL·E) | `OPENAI_API_KEY`, `model` |
|
|
85
|
+
| `gemini` / `imagen` | Google Imagen (cheap, ~$0.02/img) | `GEMINI_API_KEY`, `model` |
|
|
86
|
+
| `local` *(later)* | Bundled local diffusion (optional heavy extra) | — |
|
|
87
|
+
|
|
88
|
+
Each provider is a thin client: prompt (+ size/count/seed/negative) → image
|
|
89
|
+
bytes. Modeled on `slide-stream`'s image providers, but **standalone** — Limn
|
|
90
|
+
does not depend on slide-stream (a video tool). If code-sharing becomes
|
|
91
|
+
worthwhile, extract a small shared `image-providers` library later.
|
|
92
|
+
|
|
93
|
+
## 5. Architecture
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
limn/ Python package
|
|
97
|
+
providers/ thin per-backend clients (swarmui, openai, gemini, …)
|
|
98
|
+
core.py generate(prompt, opts) -> image bytes / files
|
|
99
|
+
config.py layered config (~/.limn.yaml + ./limn.yaml + env)
|
|
100
|
+
cli.py Typer CLI
|
|
101
|
+
serve.py FastAPI web UI (optional [serve] extra)
|
|
102
|
+
desktop/ Tauri shell + Python sidecar (later)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- **Python 3.10+**, `typer`, `requests`/`httpx`, `pillow`.
|
|
106
|
+
- **Web:** `fastapi` + `uvicorn` behind a `[serve]` extra.
|
|
107
|
+
- **Desktop:** Tauri (Rust) + Python sidecar, `uv`-bootstrapped runtime.
|
|
108
|
+
- **Packaging:** `uv` / hatchling; publish to PyPI (`pip install limn`).
|
|
109
|
+
|
|
110
|
+
## 6. Config
|
|
111
|
+
|
|
112
|
+
Layered, later wins (mirrors slide-stream): built-in defaults →
|
|
113
|
+
`~/.limn.yaml` (your provider + keys, set once) → `./limn.yaml` → CLI flags.
|
|
114
|
+
Secrets referenced as `${VAR}` so no plaintext keys in files.
|
|
115
|
+
|
|
116
|
+
```yaml
|
|
117
|
+
provider: swarmui
|
|
118
|
+
base_url: https://image.example.org
|
|
119
|
+
model: juggernautXL_v9
|
|
120
|
+
api_key: "${SWARMUI_TOKEN}"
|
|
121
|
+
size: [1024, 1024]
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## 7. Privacy & governance
|
|
125
|
+
|
|
126
|
+
- **No account. No telemetry.** Nothing is required to run it.
|
|
127
|
+
- **No server-side storage** of prompts or images. Desktop = fully local;
|
|
128
|
+
the demo is ephemeral and rate-limited.
|
|
129
|
+
- **Your provider, your data.** The only network egress is the generation
|
|
130
|
+
request to the provider *you* configured.
|
|
131
|
+
- Clear, short docs on what leaves the machine and when.
|
|
132
|
+
|
|
133
|
+
## 8. Non-goals
|
|
134
|
+
|
|
135
|
+
- Not a full editing studio (no inpainting / ControlNet / node graphs — use
|
|
136
|
+
ComfyUI/SwarmUI).
|
|
137
|
+
- Not a model host — Limn calls a provider you point it at.
|
|
138
|
+
- Not a prompt-engineering IDE — minimal knobs on purpose.
|
|
139
|
+
|
|
140
|
+
## 9. Milestones
|
|
141
|
+
|
|
142
|
+
1. **M1 — CLI:** `limn "prompt" -o out.png`, provider layer (swarmui + one
|
|
143
|
+
cloud), layered config. Publish to PyPI.
|
|
144
|
+
2. **M2 — Web UI:** `limn serve` with generate / view / save / delete /
|
|
145
|
+
regenerate + session gallery.
|
|
146
|
+
3. **M3 — Demo:** hosted, rate-limited, non-storing instance + landing page.
|
|
147
|
+
4. **M4 — Desktop:** Tauri app with BYO-provider settings, local saves.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
*Companion project to [slide-stream](https://github.com/michael-borck/slide-stream),
|
|
152
|
+
which shares the same image-provider approach.*
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "limn"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A small, privacy-first text-to-image tool: type a prompt, get an image. Bring your own provider."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Michael Borck", email = "michael.borck@curtin.edu.au" },
|
|
13
|
+
]
|
|
14
|
+
keywords = ["ai", "image", "text-to-image", "cli", "swarmui", "imagen", "dall-e"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Intended Audience :: End Users/Desktop",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Multimedia :: Graphics",
|
|
28
|
+
]
|
|
29
|
+
requires-python = ">=3.10"
|
|
30
|
+
dependencies = [
|
|
31
|
+
"typer>=0.9.0",
|
|
32
|
+
"rich>=13.0.0",
|
|
33
|
+
"requests>=2.31.0",
|
|
34
|
+
"pyyaml>=6.0.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
# Web UI (limn serve) — M2
|
|
39
|
+
serve = [
|
|
40
|
+
"fastapi>=0.110.0",
|
|
41
|
+
"uvicorn>=0.29.0",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[project.scripts]
|
|
45
|
+
limn = "limn.cli:app"
|
|
46
|
+
|
|
47
|
+
[project.urls]
|
|
48
|
+
Homepage = "https://github.com/michael-borck/limn"
|
|
49
|
+
Repository = "https://github.com/michael-borck/limn"
|
|
50
|
+
Issues = "https://github.com/michael-borck/limn/issues"
|
|
51
|
+
|
|
52
|
+
[tool.hatch.build.targets.wheel]
|
|
53
|
+
packages = ["src/limn"]
|
|
54
|
+
|
|
55
|
+
[tool.ruff]
|
|
56
|
+
target-version = "py310"
|
|
57
|
+
line-length = 88
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint]
|
|
60
|
+
select = [
|
|
61
|
+
"E", # pycodestyle errors
|
|
62
|
+
"W", # pycodestyle warnings
|
|
63
|
+
"F", # pyflakes
|
|
64
|
+
"I", # isort
|
|
65
|
+
"B", # flake8-bugbear
|
|
66
|
+
"C4", # flake8-comprehensions
|
|
67
|
+
"UP", # pyupgrade
|
|
68
|
+
]
|
|
69
|
+
ignore = [
|
|
70
|
+
"E501", # line too long
|
|
71
|
+
"B008", # function calls in argument defaults (Typer idiom)
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
[tool.ruff.lint.per-file-ignores]
|
|
75
|
+
"__init__.py" = ["F401"]
|
|
76
|
+
|
|
77
|
+
[tool.ruff.lint.isort]
|
|
78
|
+
known-first-party = ["limn"]
|
|
79
|
+
|
|
80
|
+
[tool.basedpyright]
|
|
81
|
+
include = ["src", "tests"]
|
|
82
|
+
exclude = ["**/__pycache__", "**/._*"]
|
|
83
|
+
venvPath = "."
|
|
84
|
+
venv = ".venv"
|
|
85
|
+
pythonVersion = "3.10"
|
|
86
|
+
pythonPlatform = "All"
|
|
87
|
+
typeCheckingMode = "basic"
|
|
88
|
+
reportMissingImports = true
|
|
89
|
+
reportMissingTypeStubs = false
|
|
90
|
+
reportUnusedImport = true
|
|
91
|
+
reportUnusedClass = true
|
|
92
|
+
reportUnusedFunction = true
|
|
93
|
+
reportUnusedVariable = true
|
|
94
|
+
reportDuplicateImport = true
|
|
95
|
+
|
|
96
|
+
[tool.pytest.ini_options]
|
|
97
|
+
testpaths = ["tests"]
|
|
98
|
+
python_files = ["test_*.py", "*_test.py"]
|
|
99
|
+
addopts = [
|
|
100
|
+
"--strict-markers",
|
|
101
|
+
"--strict-config",
|
|
102
|
+
"--cov=limn",
|
|
103
|
+
"--cov-report=term-missing",
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
[tool.coverage.run]
|
|
107
|
+
source = ["limn"]
|
|
108
|
+
branch = true
|
|
109
|
+
|
|
110
|
+
[dependency-groups]
|
|
111
|
+
dev = [
|
|
112
|
+
"basedpyright>=1.29.4",
|
|
113
|
+
"build>=1.2.2",
|
|
114
|
+
"pytest>=8.4.0",
|
|
115
|
+
"pytest-cov>=6.0.0",
|
|
116
|
+
"pytest-mock>=3.14.0",
|
|
117
|
+
"ruff>=0.12.0",
|
|
118
|
+
"twine>=6.1.0",
|
|
119
|
+
]
|