nanoodle 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.
- nanoodle-0.1.0/LICENSE +21 -0
- nanoodle-0.1.0/MANIFEST.in +5 -0
- nanoodle-0.1.0/PKG-INFO +160 -0
- nanoodle-0.1.0/README.md +134 -0
- nanoodle-0.1.0/docs/DESIGN.md +70 -0
- nanoodle-0.1.0/docs/SPEC-engine.md +127 -0
- nanoodle-0.1.0/docs/SPEC-format.md +70 -0
- nanoodle-0.1.0/docs/SPEC-io.md +45 -0
- nanoodle-0.1.0/docs/agent-skills.md +132 -0
- nanoodle-0.1.0/pyproject.toml +48 -0
- nanoodle-0.1.0/setup.cfg +4 -0
- nanoodle-0.1.0/src/nanoodle/__init__.py +24 -0
- nanoodle-0.1.0/src/nanoodle/__main__.py +198 -0
- nanoodle-0.1.0/src/nanoodle/engine.py +937 -0
- nanoodle-0.1.0/src/nanoodle/errors.py +29 -0
- nanoodle-0.1.0/src/nanoodle/graph.py +226 -0
- nanoodle-0.1.0/src/nanoodle/iodef.py +337 -0
- nanoodle-0.1.0/src/nanoodle/media.py +165 -0
- nanoodle-0.1.0/src/nanoodle/transport.py +78 -0
- nanoodle-0.1.0/src/nanoodle/workflow.py +414 -0
- nanoodle-0.1.0/src/nanoodle.egg-info/PKG-INFO +160 -0
- nanoodle-0.1.0/src/nanoodle.egg-info/SOURCES.txt +23 -0
- nanoodle-0.1.0/src/nanoodle.egg-info/dependency_links.txt +1 -0
- nanoodle-0.1.0/src/nanoodle.egg-info/entry_points.txt +2 -0
- nanoodle-0.1.0/src/nanoodle.egg-info/top_level.txt +1 -0
nanoodle-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nanoodle contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
nanoodle-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nanoodle
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Run nanoodle visual AI workflows from Python β zero-dependency executor for saved noodle-graph.json files
|
|
5
|
+
Author: 255BITS
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://nanoodle.io
|
|
8
|
+
Project-URL: Repository, https://github.com/255BITS/nanoodle-py
|
|
9
|
+
Keywords: nanoodle,nanogpt,workflow,ai,generative,image,video,audio,llm
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# nanoodle
|
|
28
|
+
|
|
29
|
+
Run [nanoodle](https://nanoodle.io) AI workflows server-side. Build a workflow visually in the
|
|
30
|
+
nanoodle editor, hit πΎ to download `noodle-graph.json`, and re-execute it anywhere Python runs β
|
|
31
|
+
against the same [NanoGPT](https://nano-gpt.com) API the app uses.
|
|
32
|
+
|
|
33
|
+
- **Zero runtime dependencies** (Python >= 3.9, stdlib only)
|
|
34
|
+
- Same execution semantics as the app: topological order, concurrent lanes, wired-field overrides
|
|
35
|
+
- Text, image, video (submit + poll), audio (sync + async poll), vision, transcription
|
|
36
|
+
- Cost tracking per node and per run
|
|
37
|
+
|
|
38
|
+
## Quickstart
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install nanoodle
|
|
42
|
+
export NANOGPT_API_KEY=... # nano-gpt.com API key (or OAuth access token)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from nanoodle import Workflow
|
|
47
|
+
|
|
48
|
+
wf = Workflow.load("noodle-graph.json")
|
|
49
|
+
result = wf.run({"Text": "a cozy ramen shop on a rainy night"})
|
|
50
|
+
result["Image"].save("ramen.png") # media outputs are MediaRef (url + bytes()/save())
|
|
51
|
+
print(result.cost_usd, result.remaining_balance)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
With the starter graph from the app (text β LLM prompt-writer β image), that's the whole program.
|
|
55
|
+
|
|
56
|
+
### Discover a workflow's interface
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
wf.inputs # [InputSpec(key="Text", node_id="n1", field="text", kind="textarea", ...)]
|
|
60
|
+
wf.outputs # [OutputSpec(key="Image", node_id="n3", type="image", ports=["image"])]
|
|
61
|
+
wf.settings # [SettingSpec(key="n3.size", kind="select", default="1k", ...)]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Input keys resolve flexibly (case-insensitive): the node's custom name, `nodeId.field`
|
|
65
|
+
(`"n2.system"`), or the input's label when unique. A workflow with exactly one required
|
|
66
|
+
input also accepts a bare value: `wf.run("hello")`.
|
|
67
|
+
|
|
68
|
+
### Media inputs
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from nanoodle import media_from_file
|
|
72
|
+
|
|
73
|
+
wf.run({"Image": media_from_file("photo.jpg")}) # local file
|
|
74
|
+
wf.run({"Image": "https://example.com/photo.jpg"}) # hosted or data: URL
|
|
75
|
+
wf.run({"Image": raw_bytes}) # raw bytes (MIME sniffed)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Media is sent inline as base64 (NanoGPT has no upload endpoint); files over ~4.4 MB
|
|
79
|
+
(~3.5 MB for transcription) are refused locally with a clear error before any paid call.
|
|
80
|
+
|
|
81
|
+
### Settings, progress, errors
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
result = wf.run(
|
|
85
|
+
{"Text": "sunset harbor"},
|
|
86
|
+
settings={"n3.model": "flux-dev", "n3.size": "1k"},
|
|
87
|
+
timeout=600,
|
|
88
|
+
on_progress=lambda evt: print(evt["type"], evt.get("name", "")),
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`run()` raises `RunError` when an output (sink) node failed β `error.result` still carries
|
|
93
|
+
the partial results, per-node statuses, and cost so far. Failures in lanes no output depends
|
|
94
|
+
on only surface in `result.errors`. Unknown/unsupported node types, missing required inputs,
|
|
95
|
+
bad keys, and a missing API key all fail **before** anything is spent.
|
|
96
|
+
|
|
97
|
+
### CLI
|
|
98
|
+
|
|
99
|
+
Installed as `nanoodle-py` (and `python -m nanoodle` always works):
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
nanoodle-py inspect graph.json
|
|
103
|
+
nanoodle-py run graph.json --input Text="a cozy ramen shop" --set n3.size=1k --out ./out
|
|
104
|
+
nanoodle-py run graph.json --input n2.system=@style.txt --json
|
|
105
|
+
nanoodle-py run graph.json --env-file .env --input Text="hello" # NANOGPT_API_KEY from a .env file
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`--out DIR` saves media outputs to files; `--json` prints a machine-readable result;
|
|
109
|
+
`--env-file PATH` loads `.env`-style `KEY=VALUE` lines (existing environment variables win).
|
|
110
|
+
|
|
111
|
+
## Supported nodes
|
|
112
|
+
|
|
113
|
+
| runs | node types |
|
|
114
|
+
|---|---|
|
|
115
|
+
| local | text, upload (image/audio/video), choice, join, comment |
|
|
116
|
+
| NanoGPT | llm (incl. vision + audio input), image, draw, edit, inpaint*, vision, tvideo, ivideo, vedit, lipsync, music, remix, tts, transcribe |
|
|
117
|
+
| **not supported** (browser-only media processing) | resize, vframes, combine, soundtrack, trim, extractaudio |
|
|
118
|
+
|
|
119
|
+
Workflows containing unsupported node types load with a warning and fail fast at `run()` with
|
|
120
|
+
`UnsupportedNodeError` β before any network call.
|
|
121
|
+
|
|
122
|
+
\* inpaint caveat: the browser app composites the mask onto black at the source's pixel size;
|
|
123
|
+
this library passes your mask through verbatim, so supply a black/white mask matching the
|
|
124
|
+
source dimensions.
|
|
125
|
+
|
|
126
|
+
## Use it as an agent skill
|
|
127
|
+
|
|
128
|
+
A saved workflow plus a short `SKILL.md` playbook makes a skill any coding agent can run β
|
|
129
|
+
Claude Code (`.claude/skills/<name>/SKILL.md`) or anything that reads markdown and runs shell.
|
|
130
|
+
Recipe + copy-pasteable template: [docs/agent-skills.md](docs/agent-skills.md); complete
|
|
131
|
+
example: [examples/agent-skill/poster-generator/](examples/agent-skill/poster-generator/).
|
|
132
|
+
|
|
133
|
+
## Cost
|
|
134
|
+
|
|
135
|
+
You bring your own NanoGPT API key; NanoGPT bills your balance per generation and reports the
|
|
136
|
+
price on each response. `result.cost_usd` totals it and `result.cost_exact` turns `False` when
|
|
137
|
+
any call omitted a price (the total is then a floor). `result.remaining_balance` is the freshest
|
|
138
|
+
balance the API reported. A price of 0 means known-included (subscription), not unknown.
|
|
139
|
+
No telemetry, no analytics, and your API key is never logged.
|
|
140
|
+
|
|
141
|
+
## Testing
|
|
142
|
+
|
|
143
|
+
Tests run fully offline against a mock NanoGPT server (`tests/harness/`):
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
python -m unittest discover -s tests -t .
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
An opt-in live probe (spends a fraction of a cent) exists for hand-verification:
|
|
150
|
+
`python3 scripts/live-spot-check.py` (add `--image` to also run the starter graph's image step).
|
|
151
|
+
|
|
152
|
+
## Docs
|
|
153
|
+
|
|
154
|
+
Design contract and format/engine/io specs live in [`docs/`](docs/): `DESIGN.md`,
|
|
155
|
+
`SPEC-format.md`, `SPEC-engine.md`, `SPEC-io.md`.
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
MIT β see [LICENSE](LICENSE). Not affiliated with NanoGPT. Build workflows at
|
|
160
|
+
[nanoodle.io](https://nanoodle.io).
|
nanoodle-0.1.0/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# nanoodle
|
|
2
|
+
|
|
3
|
+
Run [nanoodle](https://nanoodle.io) AI workflows server-side. Build a workflow visually in the
|
|
4
|
+
nanoodle editor, hit πΎ to download `noodle-graph.json`, and re-execute it anywhere Python runs β
|
|
5
|
+
against the same [NanoGPT](https://nano-gpt.com) API the app uses.
|
|
6
|
+
|
|
7
|
+
- **Zero runtime dependencies** (Python >= 3.9, stdlib only)
|
|
8
|
+
- Same execution semantics as the app: topological order, concurrent lanes, wired-field overrides
|
|
9
|
+
- Text, image, video (submit + poll), audio (sync + async poll), vision, transcription
|
|
10
|
+
- Cost tracking per node and per run
|
|
11
|
+
|
|
12
|
+
## Quickstart
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install nanoodle
|
|
16
|
+
export NANOGPT_API_KEY=... # nano-gpt.com API key (or OAuth access token)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from nanoodle import Workflow
|
|
21
|
+
|
|
22
|
+
wf = Workflow.load("noodle-graph.json")
|
|
23
|
+
result = wf.run({"Text": "a cozy ramen shop on a rainy night"})
|
|
24
|
+
result["Image"].save("ramen.png") # media outputs are MediaRef (url + bytes()/save())
|
|
25
|
+
print(result.cost_usd, result.remaining_balance)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
With the starter graph from the app (text β LLM prompt-writer β image), that's the whole program.
|
|
29
|
+
|
|
30
|
+
### Discover a workflow's interface
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
wf.inputs # [InputSpec(key="Text", node_id="n1", field="text", kind="textarea", ...)]
|
|
34
|
+
wf.outputs # [OutputSpec(key="Image", node_id="n3", type="image", ports=["image"])]
|
|
35
|
+
wf.settings # [SettingSpec(key="n3.size", kind="select", default="1k", ...)]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Input keys resolve flexibly (case-insensitive): the node's custom name, `nodeId.field`
|
|
39
|
+
(`"n2.system"`), or the input's label when unique. A workflow with exactly one required
|
|
40
|
+
input also accepts a bare value: `wf.run("hello")`.
|
|
41
|
+
|
|
42
|
+
### Media inputs
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from nanoodle import media_from_file
|
|
46
|
+
|
|
47
|
+
wf.run({"Image": media_from_file("photo.jpg")}) # local file
|
|
48
|
+
wf.run({"Image": "https://example.com/photo.jpg"}) # hosted or data: URL
|
|
49
|
+
wf.run({"Image": raw_bytes}) # raw bytes (MIME sniffed)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Media is sent inline as base64 (NanoGPT has no upload endpoint); files over ~4.4 MB
|
|
53
|
+
(~3.5 MB for transcription) are refused locally with a clear error before any paid call.
|
|
54
|
+
|
|
55
|
+
### Settings, progress, errors
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
result = wf.run(
|
|
59
|
+
{"Text": "sunset harbor"},
|
|
60
|
+
settings={"n3.model": "flux-dev", "n3.size": "1k"},
|
|
61
|
+
timeout=600,
|
|
62
|
+
on_progress=lambda evt: print(evt["type"], evt.get("name", "")),
|
|
63
|
+
)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`run()` raises `RunError` when an output (sink) node failed β `error.result` still carries
|
|
67
|
+
the partial results, per-node statuses, and cost so far. Failures in lanes no output depends
|
|
68
|
+
on only surface in `result.errors`. Unknown/unsupported node types, missing required inputs,
|
|
69
|
+
bad keys, and a missing API key all fail **before** anything is spent.
|
|
70
|
+
|
|
71
|
+
### CLI
|
|
72
|
+
|
|
73
|
+
Installed as `nanoodle-py` (and `python -m nanoodle` always works):
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
nanoodle-py inspect graph.json
|
|
77
|
+
nanoodle-py run graph.json --input Text="a cozy ramen shop" --set n3.size=1k --out ./out
|
|
78
|
+
nanoodle-py run graph.json --input n2.system=@style.txt --json
|
|
79
|
+
nanoodle-py run graph.json --env-file .env --input Text="hello" # NANOGPT_API_KEY from a .env file
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`--out DIR` saves media outputs to files; `--json` prints a machine-readable result;
|
|
83
|
+
`--env-file PATH` loads `.env`-style `KEY=VALUE` lines (existing environment variables win).
|
|
84
|
+
|
|
85
|
+
## Supported nodes
|
|
86
|
+
|
|
87
|
+
| runs | node types |
|
|
88
|
+
|---|---|
|
|
89
|
+
| local | text, upload (image/audio/video), choice, join, comment |
|
|
90
|
+
| NanoGPT | llm (incl. vision + audio input), image, draw, edit, inpaint*, vision, tvideo, ivideo, vedit, lipsync, music, remix, tts, transcribe |
|
|
91
|
+
| **not supported** (browser-only media processing) | resize, vframes, combine, soundtrack, trim, extractaudio |
|
|
92
|
+
|
|
93
|
+
Workflows containing unsupported node types load with a warning and fail fast at `run()` with
|
|
94
|
+
`UnsupportedNodeError` β before any network call.
|
|
95
|
+
|
|
96
|
+
\* inpaint caveat: the browser app composites the mask onto black at the source's pixel size;
|
|
97
|
+
this library passes your mask through verbatim, so supply a black/white mask matching the
|
|
98
|
+
source dimensions.
|
|
99
|
+
|
|
100
|
+
## Use it as an agent skill
|
|
101
|
+
|
|
102
|
+
A saved workflow plus a short `SKILL.md` playbook makes a skill any coding agent can run β
|
|
103
|
+
Claude Code (`.claude/skills/<name>/SKILL.md`) or anything that reads markdown and runs shell.
|
|
104
|
+
Recipe + copy-pasteable template: [docs/agent-skills.md](docs/agent-skills.md); complete
|
|
105
|
+
example: [examples/agent-skill/poster-generator/](examples/agent-skill/poster-generator/).
|
|
106
|
+
|
|
107
|
+
## Cost
|
|
108
|
+
|
|
109
|
+
You bring your own NanoGPT API key; NanoGPT bills your balance per generation and reports the
|
|
110
|
+
price on each response. `result.cost_usd` totals it and `result.cost_exact` turns `False` when
|
|
111
|
+
any call omitted a price (the total is then a floor). `result.remaining_balance` is the freshest
|
|
112
|
+
balance the API reported. A price of 0 means known-included (subscription), not unknown.
|
|
113
|
+
No telemetry, no analytics, and your API key is never logged.
|
|
114
|
+
|
|
115
|
+
## Testing
|
|
116
|
+
|
|
117
|
+
Tests run fully offline against a mock NanoGPT server (`tests/harness/`):
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
python -m unittest discover -s tests -t .
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
An opt-in live probe (spends a fraction of a cent) exists for hand-verification:
|
|
124
|
+
`python3 scripts/live-spot-check.py` (add `--image` to also run the starter graph's image step).
|
|
125
|
+
|
|
126
|
+
## Docs
|
|
127
|
+
|
|
128
|
+
Design contract and format/engine/io specs live in [`docs/`](docs/): `DESIGN.md`,
|
|
129
|
+
`SPEC-format.md`, `SPEC-engine.md`, `SPEC-io.md`.
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT β see [LICENSE](LICENSE). Not affiliated with NanoGPT. Build workflows at
|
|
134
|
+
[nanoodle.io](https://nanoodle.io).
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# nanoodle executor libraries β DESIGN (binding decisions)
|
|
2
|
+
|
|
3
|
+
Two sibling repos, both MIT, both ZERO runtime dependencies:
|
|
4
|
+
- /home/ntc/dev/nanoodle-js β npm package `nanoodle` (ESM, Node >= 20, built-in fetch, node:test)
|
|
5
|
+
- /home/ntc/dev/nanoodle-py β PyPI package `nanoodle` (Python >= 3.9, stdlib urllib only, unittest)
|
|
6
|
+
|
|
7
|
+
Read SPEC-format.md, SPEC-engine.md, SPEC-io.md in this directory first. They are the contract.
|
|
8
|
+
|
|
9
|
+
## Public API β JS
|
|
10
|
+
```js
|
|
11
|
+
import { Workflow, NanoodleError, UnsupportedNodeError, RunError } from "nanoodle";
|
|
12
|
+
|
|
13
|
+
const wf = await Workflow.load("noodle-graph.json", { apiKey }); // path, or pass a parsed object / JSON string to Workflow.fromJSON(objOrString, opts)
|
|
14
|
+
wf.inputs // [{ key, nodeId, field, kind, label, optional, def, options? }] key = resolved friendly name
|
|
15
|
+
wf.outputs // [{ key, nodeId, type, ports }]
|
|
16
|
+
wf.settings // [{ key, nodeId, field, kind, def, options? }]
|
|
17
|
+
|
|
18
|
+
const result = await wf.run({ "Text": "a cozy ramen shop" }, { settings: { "n3.model": "..." }, timeoutMs, signal, onProgress });
|
|
19
|
+
result.get("Image") // primary output value of that sink node
|
|
20
|
+
result.outputs // { [key]: value } (plus node-id keys)
|
|
21
|
+
result.costUsd, result.costExact, result.remainingBalance
|
|
22
|
+
result.nodes // per-node { status, out, error, costUsd, ms }
|
|
23
|
+
result.errors // [] of { nodeId, name, message }
|
|
24
|
+
```
|
|
25
|
+
- run() REJECTS with RunError when any SINK node failed (RunError carries .result with partials). Non-sink failure that no sink depends on β warning in result.errors only. (A failed non-sink makes its downstream sinks fail with "upstream failed: <name>" β so effectively any failure that matters rejects.)
|
|
26
|
+
- Media values: class MediaRef { url (data: or https), mime?, async bytes(), async save(path), toString() β url }. Text outputs are plain strings. Inputs accept: string (text), data: URL, https URL, Buffer/Uint8Array (+ mime option via {data, mime}), or local file path via Workflow helpers (mediaFromFile(path)).
|
|
27
|
+
- onProgress(evt): { type: "node-start"|"node-done"|"node-error"|"poll", nodeId, name, ... }.
|
|
28
|
+
- Constructor opts: { apiKey = process.env.NANOGPT_API_KEY, baseUrl = "https://nano-gpt.com", fetch = globalThis.fetch, pollIntervals, timeouts } β injectable fetch/baseUrl is what the test harness uses.
|
|
29
|
+
|
|
30
|
+
CLI (bin/nanoodle.mjs, "nanoodle" bin entry):
|
|
31
|
+
```
|
|
32
|
+
nanoodle run graph.json --input Text="a cozy ramen shop" --input n2.system=@file.txt --set n3.size=1k --out ./out [--json]
|
|
33
|
+
nanoodle inspect graph.json # prints inputs/outputs/settings + node table
|
|
34
|
+
```
|
|
35
|
+
--out saves media outputs to files (fetch https, decode data:), prints text outputs; --json prints machine-readable result.
|
|
36
|
+
|
|
37
|
+
## Public API β Python (mirror, pythonic)
|
|
38
|
+
```python
|
|
39
|
+
from nanoodle import Workflow, NanoodleError, UnsupportedNodeError, RunError, media_from_file
|
|
40
|
+
|
|
41
|
+
wf = Workflow.load("noodle-graph.json", api_key=None) # env NANOGPT_API_KEY fallback; Workflow.from_dict(d) too
|
|
42
|
+
wf.inputs / wf.outputs / wf.settings # lists of dataclasses, same fields as JS
|
|
43
|
+
result = wf.run({"Text": "a cozy ramen shop"}, settings=None, timeout=None, on_progress=None)
|
|
44
|
+
result["Image"] # __getitem__ = outputs lookup (friendly key or node id)
|
|
45
|
+
result.outputs, result.cost_usd, result.cost_exact, result.remaining_balance, result.nodes, result.errors
|
|
46
|
+
```
|
|
47
|
+
- Sync API (urllib + concurrent.futures ThreadPoolExecutor for node concurrency). Same RunError semantics.
|
|
48
|
+
- MediaRef: .url, .mime, .bytes(), .save(path), __str__ β url.
|
|
49
|
+
- Injectable transport: Workflow(..., base_url=..., http=callable) for the harness (default small urllib wrapper).
|
|
50
|
+
- CLI: `python -m nanoodle run|inspect ...` mirroring the JS flags.
|
|
51
|
+
|
|
52
|
+
## Shared behaviors (both)
|
|
53
|
+
- run() input validation UPFRONT: unknown input key β error listing valid keys; missing required input with empty field default β error naming it; no API key while graph has network nodes β error BEFORE any node runs.
|
|
54
|
+
- Unsupported node types (resize/vframes/combine/soundtrack/trim/extractaudio) and unknown types that must RUN β UnsupportedNodeError at run start (fail fast, before spending), naming node + type. Workflow.load only warns.
|
|
55
|
+
- No locale suffix, no catalog fetch, no seed skip-cache, no telemetry/analytics of ANY kind. Never log the API key. Media over 4.4MB inline β clear local error.
|
|
56
|
+
- Version: 0.1.0.
|
|
57
|
+
|
|
58
|
+
## Repo layout (each)
|
|
59
|
+
README.md (quickstart: download the save from https://nanoodle.io β 3-line usage; the starter-graph example; supported node matrix; cost note; link to nanoodle app)
|
|
60
|
+
LICENSE (MIT, "Copyright (c) 2026 nanoodle contributors")
|
|
61
|
+
src/... (js: src/*.mjs re-exported from src/index.mjs; py: src/nanoodle/*.py)
|
|
62
|
+
tests/ (harness + unit tests; node:test / unittest β NO third-party test deps)
|
|
63
|
+
tests/fixtures/*.json (starter graph copy + purpose-built graphs: join/choice chain, llm-vision, edit multi-image, video poll, tts binary, music async-poll, transcribe, unsupported-node, cycle, field-override, duplicate-names, unknown-key errors)
|
|
64
|
+
tests/harness/ mock NanoGPT server (js: node:http; py: http.server in a thread) β canned per-endpoint responses, records every request (method/path/headers/body) for payload assertions, scriptable sequences (video pendingβpendingβcompleted; audio runIdβpoll; 401/402 errors; binary audio response with x-cost header).
|
|
65
|
+
scripts/live-spot-check.{mjs,py} β OPT-IN live test: reads NANOGPT_API_KEY from env or --env-file, runs fixture textβllm (model zai-org/glm-5.2, maxTokens 60) and prints text + cost; --image flag additionally runs the starter graph image step. NEVER run by CI.
|
|
66
|
+
.github/workflows/test.yml β offline unit tests only, on push.
|
|
67
|
+
.gitignore (node_modules, dist, __pycache__, .env, out/)
|
|
68
|
+
|
|
69
|
+
## Test plan (harness-first; the workflow phase will extend it)
|
|
70
|
+
Cover at minimum: graph load/aliases/link-migration/unknown-type; topo order + cycle error; concurrency (two parallel lanes both hit server); wiring incl. field override via textarea port; input derivation + all key-resolution orders + ambiguity errors + bare-scalar single-input; settings override + wired-field-refused; every network node type's payload EXACTLY (assert recorded body against spec) + response parse incl. b64 mime sniff, draw message.images, video poll loop + failure + timeout, audio JSON-url/JSON-runId-poll/binary branches, transcribe multipart (assert field name "file"); cost extraction priority incl. zero-cost-kept and header fallback; 401/402/500 error mapping; RunError partial results; MediaRef bytes/save; unsupported node fail-fast BEFORE any network call; no Authorization header leak in errors/repr.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Nanoodle Run Engine β server-side re-implementation spec
|
|
2
|
+
|
|
3
|
+
Blueprint = play.html RUNTIME_JS (the exported-app runtime). Line refs are play.html.
|
|
4
|
+
|
|
5
|
+
## Endpoints & auth
|
|
6
|
+
```
|
|
7
|
+
NANOGPT = https://nano-gpt.com
|
|
8
|
+
IMG_ENDPOINT POST {NANOGPT}/v1/images/generations (note: NOT /api/v1)
|
|
9
|
+
CHAT_ENDPOINT POST {NANOGPT}/api/v1/chat/completions
|
|
10
|
+
VIDEO submit POST {NANOGPT}/api/generate-video
|
|
11
|
+
VIDEO poll GET {NANOGPT}/api/video/status?requestId=<id>
|
|
12
|
+
AUDIO speech POST {NANOGPT}/api/v1/audio/speech (music + tts + remix)
|
|
13
|
+
AUDIO poll GET {NANOGPT}/api/tts/status?<qs>
|
|
14
|
+
TRANSCRIBE POST {NANOGPT}/api/v1/audio/transcriptions (multipart)
|
|
15
|
+
Catalogs (public, no key): /api/v1/models?detailed=true, /api/v1/image-models,
|
|
16
|
+
/api/v1/video-models, /api/v1/audio-models
|
|
17
|
+
```
|
|
18
|
+
Every JSON call: `Content-Type: application/json` + BOTH `Authorization: Bearer <key>` and `x-api-key: <key>`.
|
|
19
|
+
Multipart transcribe: the two auth headers, NO explicit Content-Type (let the http lib set boundary). File form field MUST be named "file"; also fields `model`, optional `language`.
|
|
20
|
+
|
|
21
|
+
All media is inlined as base64 `data:` URLs in JSON bodies (no upload endpoint). MEDIA_INLINE_MAX = 4.4MB β guard locally and raise a clear error above it. Chat runs NON-STREAMING (no `stream` key); parse `r.json()` once.
|
|
22
|
+
|
|
23
|
+
Model strings pass through VERBATIM (`body.model = node.fields.model`). Missing model β error "pick a model first". Endpoint choice is by node TYPE, never by model lookup. The catalog is optional/best-effort (capability gating + clamps); the library must run fine with no catalog fetch.
|
|
24
|
+
|
|
25
|
+
## Per-node payloads
|
|
26
|
+
|
|
27
|
+
### llm β CHAT_ENDPOINT (genChat 1481-1504, run 2574-2603)
|
|
28
|
+
```
|
|
29
|
+
body = { model, messages, temperature: 0.8 }
|
|
30
|
+
if maxTokens body.max_tokens = +maxTokens
|
|
31
|
+
if format == "JSON" body.response_format = { type: "json_object" }
|
|
32
|
+
if reasoningEffort set and != "default" body.reasoning_effort = value
|
|
33
|
+
```
|
|
34
|
+
messages: optional {role:"system", content: fields.system} then user message.
|
|
35
|
+
User content = plain string prompt, UNLESS wired images (img1,img2,... sorted by index) or wired audio:
|
|
36
|
+
then array `[{type:"text",text:prompt}, {type:"image_url",image_url:{url:<value verbatim>}}..., audioPart?]`.
|
|
37
|
+
audioPart = `{type:"input_audio", input_audio:{data:<base64 body, no data: prefix>, format:"mp3"|"wav"|...}}`.
|
|
38
|
+
prompt = wired `prompt` port value ?? fields.prompt; error "no prompt" if empty.
|
|
39
|
+
Parse: `j.choices[0].message.content` (if array, join `.map(p=>p.text)`); throw "no text in response" if null/empty.
|
|
40
|
+
(withLocale non-English system suffix: SKIP in the library / make opt-in.)
|
|
41
|
+
|
|
42
|
+
### vision β same as llm: one user message [{text: q||"Describe this image."}, {image_url: inp.image}]
|
|
43
|
+
|
|
44
|
+
### image / edit / inpaint β IMG_ENDPOINT (genImage 1465-1480)
|
|
45
|
+
```
|
|
46
|
+
body = { model, size: fields.size || "1024x1024", n: variations||1, response_format: "b64_json" }
|
|
47
|
+
if prompt body.prompt = prompt
|
|
48
|
+
if source image body.imageDataUrl = <string OR array of strings (edit multi-ref)>
|
|
49
|
+
if mask body.maskDataUrl = <mask data URL; white = repaint>
|
|
50
|
+
+ seed (when numeric), + customCivitaiAir (model "custom-civitai"), + LoRA params
|
|
51
|
+
```
|
|
52
|
+
- image node: `variations` β n (multi output = j.data list).
|
|
53
|
+
- edit: sources from wired `image, image2, ...` ports; single β string, multiple β array. Prompt may be empty for upscaler models β do not hard-require.
|
|
54
|
+
- inpaint: source+mask from ports or fields. (Browser composites mask onto black at source size via canvas; library v1: pass mask through verbatim and document the caveat.)
|
|
55
|
+
Parse: `j.data[]` β `d.b64_json ? "data:<sniffed mime>;base64,"+b64 : d.url`. Sniff mime from magic bytes (PNG \x89PNG, JPEG \xFF\xD8, GIF, WEBP RIFF....WEBP; default image/png). Throw "no image in response" if empty.
|
|
56
|
+
|
|
57
|
+
### draw β CHAT_ENDPOINT (genChatImage 1508-1523)
|
|
58
|
+
body = {model, messages, temperature:0.8} (no response_format). Wired images like llm.
|
|
59
|
+
Parse: images from `j.choices[0].message.images[]` β `im.image_url.url || im.url || im`; text from message.content (may be null β fine).
|
|
60
|
+
|
|
61
|
+
### tvideo / ivideo / vedit / lipsync β submit + poll (genVideo 1527-1588)
|
|
62
|
+
```
|
|
63
|
+
body = { model, prompt }
|
|
64
|
+
+ dims from node fields: aspect β aspect_ratio, duration β duration, resolution β resolution
|
|
65
|
+
(catalog can rename e.g. aspectβorientation / durationβseconds; WITHOUT catalog use the standard names)
|
|
66
|
+
+ ivideo/lipsync source image β body.imageDataUrl (data: or https, verbatim)
|
|
67
|
+
+ ivideo wired endframe β body.last_image
|
|
68
|
+
+ vedit source: https URL β body.videoUrl ; local data β body.videoDataUrl
|
|
69
|
+
+ lipsync audio: https β body.audioUrl ; local β body.audioDataUrl
|
|
70
|
+
+ Object.assign(body, fields.modelOpts || {}) (per-model knobs incl. seed)
|
|
71
|
+
+ tvideo wired ref1.. β body.reference_images (array) β catalog may rename key; default "reference_images"
|
|
72
|
+
```
|
|
73
|
+
Submit response: `runId = j.runId || j.id`. Then poll every 5s: GET /api/video/status?requestId=<runId>.
|
|
74
|
+
status = (s.data?.status || s.status).toUpperCase().
|
|
75
|
+
COMPLETED|SUCCEEDED β url = s.data?.output?.video?.url || out.url || out.video?.[0]?.url (out = s.data?.output or s).
|
|
76
|
+
FAILED|ERROR|CANCELED β raise "video failed: " + (s.data?.error || status). Timeout 600s.
|
|
77
|
+
|
|
78
|
+
### music / tts / remix β AUDIO speech endpoint (genAudio 1591-1637)
|
|
79
|
+
```
|
|
80
|
+
body = { model, input: <wired text ?? fields.prompt> } + params
|
|
81
|
+
music params: lyrics, instrumental(bool), duration(number), negative_prompt, seed, response_format(default "mp3")
|
|
82
|
+
tts params: voice, speed(omit when 1), instructions, response_format(default "mp3")
|
|
83
|
+
remix params: lyrics, duration, response_format + body.audio = <source: https as-is | local data URL>
|
|
84
|
+
+ merge fields.extraJson (parsed object) verbatim last
|
|
85
|
+
Omit empty params. (Catalog gating of voice/duration: skip in v1 β send what the node has.)
|
|
86
|
+
```
|
|
87
|
+
Response handling:
|
|
88
|
+
- content-type JSON β url = j.url||j.audioUrl||j.data?.url||j.data?.audioUrl; if none but j.runId||j.id β poll
|
|
89
|
+
GET /api/tts/status?runId=..&model=..&cost=..&paymentSource=..&isApiRequest=true every 3s;
|
|
90
|
+
status lowercase: completed|succeeded β s.audioUrl||s.url||s.data?.audioUrl||s.data?.url;
|
|
91
|
+
error|failed|content_policy_violation β raise. Timeout 300s.
|
|
92
|
+
- else BINARY body β the audio bytes; mime from response content-type (pin from requested format if generic).
|
|
93
|
+
Library returns bytes+mime (a data: URL or MediaRef), not an object URL.
|
|
94
|
+
|
|
95
|
+
### transcribe β multipart (1641-1666)
|
|
96
|
+
FormData: file=<audio blob> (field name "file"), model, language?. Local guard: >3.5MB raise.
|
|
97
|
+
Parse: `j.transcription ?? j.text ?? j.data?.transcription ?? j.data?.text`.
|
|
98
|
+
|
|
99
|
+
## Cost extraction (costFromJson 998-1013)
|
|
100
|
+
USD priority: j.cost (if >0) β j.x_nanogpt_pricing.(costUsd|cost|amount) β j.metadata?.cost β header x-cost / x-nano-cost.
|
|
101
|
+
Balance: header x-remaining-balance (wins) β j.remainingBalance β x_nanogpt_pricing.remainingBalance.
|
|
102
|
+
Present-but-zero = known-included (subscription), keep 0. Absent β cost unknown (mark total inexact).
|
|
103
|
+
|
|
104
|
+
## HTTP errors (922-935)
|
|
105
|
+
- 401/403 β auth error ("API key rejected").
|
|
106
|
+
- 402 OR body matching /insufficient|balance|funds|not enough|payment required/i β out-of-funds error.
|
|
107
|
+
- else β error "<status>: <body first 160 chars>".
|
|
108
|
+
No streaming retries needed (engine is non-streaming). Poll GET failures: silently continue the loop until timeout.
|
|
109
|
+
|
|
110
|
+
## Execution (runGraph 3000-3133)
|
|
111
|
+
1. Alias/filter nodes (materialize): audioβtts, drop unknown types + orphaned links, migrate music/tts inbound "text" port β "prompt".
|
|
112
|
+
2. Kahn topological order; cyclic β error naming the cyclic nodes.
|
|
113
|
+
3. Concurrency: node starts when ITS deps finish (siblings run concurrently). Library: same semantics (asyncio / Promise per node).
|
|
114
|
+
4. Input resolution per node: for declared ports, value = srcNode.out[from.port]. Dynamic families: img\d+ / image\d*, vid\d+, clip\d+, audio, endframe, ref\d+, frame\d+ (vframes outputs). ANY other inbound link = FIELD OVERRIDE: run with fields = {...fields, [port]: value} (that's how wired prompt/system/lyrics/q override typed values).
|
|
115
|
+
5. Node failure: record error for that node, continue independent lanes (library default: collect; raise at end if a sink failed β see DESIGN).
|
|
116
|
+
6. comment nodes never run. Fixed-seed skip-cache: optional for a library (stateless one-shot runs don't need it) β SKIP in v1.
|
|
117
|
+
|
|
118
|
+
## Local nodes to IMPLEMENT (pure logic)
|
|
119
|
+
- text: out.text = fields.text
|
|
120
|
+
- upload/aupload/vupload: out = the stored/provided data URL
|
|
121
|
+
- choice: options = fields.options.split("\n") non-empty trimmed; out = fields.selected if in options else first; error if no options
|
|
122
|
+
- join: [a,b].filter(non-empty).join(sep) where sep = fields.sep ?? " ", literal "\\n" in sep means newline
|
|
123
|
+
- comment: skip
|
|
124
|
+
|
|
125
|
+
## Local nodes UNSUPPORTED in v1 (browser media ops β raise UnsupportedNodeError naming node + type)
|
|
126
|
+
resize, vframes, combine, soundtrack, trim, extractaudio.
|
|
127
|
+
Error message must say: "node type 'X' does local media processing that requires the nanoodle browser app; not supported by this library yet".
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Nanoodle Workflow JSON Format (the downloadable "save")
|
|
2
|
+
|
|
3
|
+
Verified against /home/ntc/dev/nanoodle/index.html (editor). The πΎ Save button writes
|
|
4
|
+
`noodle-graph.json` = exactly `JSON.stringify(serializeGraph(), null, 2)` β no wrapper.
|
|
5
|
+
|
|
6
|
+
## Top-level shape
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"v": 1,
|
|
10
|
+
"nodes": [ { "id":"n1", "type":"text", "x":60, "y":130, "fields":{...}, "w":220, "sizes":{"system":120}, "name":"optional custom name" } ],
|
|
11
|
+
"links": [ { "id":"l1", "from":{"node":"n1","port":"text"}, "to":{"node":"n2","port":"prompt"} } ],
|
|
12
|
+
"nid": 4, "lid": 3,
|
|
13
|
+
"view": { "panX":40, "panY":60, "scale":1 }
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
- `v` β always 1.
|
|
17
|
+
- Node: `id` (string "nN"), `type` (NODE_TYPES key), `x/y/w/sizes` layout-only (ignore), `fields` (param bag, type-specific), `name` (optional user label β the display name).
|
|
18
|
+
- Link: `{id, from:{node,port}, to:{node,port}}`.
|
|
19
|
+
- `nid/lid/view` β editor counters/camera; ignore for execution.
|
|
20
|
+
- ALL keys except `nodes` are optional to a loader: `d.nodes||[]`, `d.links||[]`. Accept `{nodes, links}` minimal form.
|
|
21
|
+
|
|
22
|
+
## Loader semantics (applyGraphData, index.html:8815-8860) β REPLICATE THESE
|
|
23
|
+
- Type alias: `audio` β `tts` (legacy).
|
|
24
|
+
- Unknown node type β node silently skipped (library: keep node but error only if it must run β see DESIGN; simplest faithful behavior: drop unknown-type nodes AND their links, but surface a warning).
|
|
25
|
+
- Links kept only if BOTH endpoints exist after node filtering.
|
|
26
|
+
- Migration: links into a `music`/`tts` node with `to.port === "text"` are rewritten to `"prompt"`.
|
|
27
|
+
- Media fields are `data:` URLs inline in `fields` (image/audio/video/mask). Share links may have them blanked to "".
|
|
28
|
+
|
|
29
|
+
## Node type registry (NODE_TYPES, index.html:5015-5679)
|
|
30
|
+
Port kinds: text | image | audio | video. Only matching kinds connect.
|
|
31
|
+
Every `<textarea>` field is also a wireable text input port with port name == field name
|
|
32
|
+
(EXCEPT on node types `text`, `choice`, comments, and the `extraJson` field). A wire into a
|
|
33
|
+
field-port overrides the typed field value at run time.
|
|
34
|
+
|
|
35
|
+
| type | title | static inputs | dynamic inputs | outputs | key fields |
|
|
36
|
+
|---|---|---|---|---|---|
|
|
37
|
+
| text | Text | β | β | text:text | text (the literal value; NOT wireable) |
|
|
38
|
+
| upload | Image input | β | β | image:image | image (data: URL) |
|
|
39
|
+
| aupload | Audio input | β | β | audio:audio | audio (data: URL) |
|
|
40
|
+
| vupload | Video input | β | β | video:video | video (data: URL) |
|
|
41
|
+
| choice | Choice | β | β | text:text | options (newline-separated), selected |
|
|
42
|
+
| join | Join | a:text, b:text | β | text:text | sep (default " "; literal "\n" means newline) |
|
|
43
|
+
| llm | LLM | β | img1..:image (vision), audio:audio | text:text | model, system, prompt, temperature, maxTokens, format(Text\|JSON), reasoningEffort, showThinking |
|
|
44
|
+
| image | Image | β | β | image:image | model, prompt, size, variations, seed, customCivitaiAir |
|
|
45
|
+
| draw | Draw | β | img1..:image | image:image, text:text | model, system, prompt, showThinking |
|
|
46
|
+
| edit | Edit | β | image,image2..:image | image:image | model, prompt, size, seed |
|
|
47
|
+
| inpaint | Inpaint | image:image, mask:image | β | image:image | model, prompt, size, seed, brush |
|
|
48
|
+
| resize | Resize/crop | image:image | β | image:image | mode(fit\|fill\|exact), width, height (LOCAL) |
|
|
49
|
+
| vision | Vision | image:image | β | text:text | model, q |
|
|
50
|
+
| tvideo | TextβVideo | β | ref1..:image | video:video | model, prompt, duration, aspect, resolution, modelOpts |
|
|
51
|
+
| ivideo | ImageβVideo | image:image | endframe:image | video:video | model, prompt, duration, aspect, resolution, modelOpts |
|
|
52
|
+
| vedit | Video edit | video:video | β | video:video | model, prompt, resolution, modelOpts |
|
|
53
|
+
| vframes | Videoβframes | video:video | β | frame1..frameN:image (dynamic, fields.frames) | frames(1-12), gap, dir(end\|start) (LOCAL) |
|
|
54
|
+
| combine | Combine videos | β | clip1..:video | video:video | dedup (LOCAL) |
|
|
55
|
+
| soundtrack | Soundtrack | video:video, audio:audio | β | video:video | loop (LOCAL) |
|
|
56
|
+
| lipsync | Avatar/lipsync | image:image, audio:audio | β | video:video | model, prompt, resolution, modelOpts |
|
|
57
|
+
| music | Music | β | β | audio:audio | model, prompt, lyrics, instrumental, duration, negative_prompt, seed, extraJson |
|
|
58
|
+
| remix | Remix audio | audio:audio | β | audio:audio | model, prompt, lyrics, duration, extraJson |
|
|
59
|
+
| tts | Speech | β | β | audio:audio | model, prompt, voice, speed, instructions, extraJson |
|
|
60
|
+
| trim | Trim audio | audio:audio | β | audio:audio | start, length (LOCAL) |
|
|
61
|
+
| extractaudio | Extract audio | video:video | β | audio:audio | start, length (LOCAL) |
|
|
62
|
+
| transcribe | Transcribe | audio:audio | β | text:text | model, language |
|
|
63
|
+
| comment | Comment | none (note:true, never runs) | β | β | text, color |
|
|
64
|
+
|
|
65
|
+
Display name resolution (play.html `displayName`): `node.name` (trimmed, if set) β NODE_TYPES[type].title β type β "?".
|
|
66
|
+
|
|
67
|
+
## Canonical fixture
|
|
68
|
+
/home/ntc/dev/nanoodle/noodle-graph.json β starter graph: text("a cozy ramen shop on a rainy night")
|
|
69
|
+
β llm(model "zai-org/glm-5.2", system prompt-writer) β image(model "nano-banana-2-lite", size "1k", variations "1").
|
|
70
|
+
Wire n1.textβn2.prompt, n2.textβn3.prompt.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Nanoodle Workflow Public Interface (inputs / outputs / naming)
|
|
2
|
+
|
|
3
|
+
## Inputs β deriveInputs (play.html 3140-3206)
|
|
4
|
+
An input = an INPUT_SPECS field on a node that is NOT fed by a wire (`links.some(l => l.to.node===id && l.to.port===field)` β hidden).
|
|
5
|
+
|
|
6
|
+
INPUT_SPECS table (only these node types contribute; kind in parens):
|
|
7
|
+
- text: text (textarea)
|
|
8
|
+
- upload: image (image) Β· aupload: audio (audio) Β· vupload: video (video)
|
|
9
|
+
- llm: prompt (textarea, required); system (textarea, OPTIONAL, def "You are a helpful, concise assistant.")
|
|
10
|
+
- image: prompt Β· tvideo: prompt Β· music: prompt Β· remix: prompt Β· tts: prompt (all textarea, required)
|
|
11
|
+
- draw: prompt (required); system (optional)
|
|
12
|
+
- inpaint (special): prompt ("What to paint in"); image and/or mask when not wired
|
|
13
|
+
- choice (special): field `selected`, kind choice, options from fields.options newline-split
|
|
14
|
+
|
|
15
|
+
Required unless marked optional. `def` = prefilled default.
|
|
16
|
+
|
|
17
|
+
## Input NAMING (for run({name: value}) resolution)
|
|
18
|
+
- Node display name = node.name (trimmed) β NODE_TYPES[type].title β type.
|
|
19
|
+
- The app labels an input with its generic spec label ("Image prompt", "Text", ...), EXCEPT:
|
|
20
|
+
when a node contributes exactly ONE required input AND has a custom name, that custom name is the label (PR #138).
|
|
21
|
+
- upload nodes feeding role ports get role labels ("End frame", "Reference N", "Image N") β UI nicety; library can skip.
|
|
22
|
+
- Unique key = (nodeId, field). LIBRARY RESOLUTION ORDER for a user-supplied key (case-insensitive, trimmed):
|
|
23
|
+
1. exact node custom name (if that node has exactly one derived input β that input; ambiguous β error listing candidates)
|
|
24
|
+
2. "nodeId.field" (e.g. "n2.prompt") and bare nodeId (if single input on the node)
|
|
25
|
+
3. the input's label / field name if unique across inputs
|
|
26
|
+
Unknown key β error listing available input names.
|
|
27
|
+
- If the workflow has exactly one required input, allow a bare scalar: run("hello").
|
|
28
|
+
|
|
29
|
+
## Settings β deriveSettings (play.html 3229-3328)
|
|
30
|
+
Per-node knobs (model, size, temperature, duration, voice, seed, ...) that are NOT part of IO shape.
|
|
31
|
+
Library: expose overrides via run(..., settings={...}) resolved the same way (name/nodeId.field), applied as
|
|
32
|
+
node.fields[field] = value before execution. Same wired-hides rule (cannot override a wired field via settings).
|
|
33
|
+
|
|
34
|
+
## Outputs β deriveOutputs (play.html 3208-3214)
|
|
35
|
+
Output nodes = nodes with a non-empty outputs list AND no outgoing link (sinks). No explicit marker.
|
|
36
|
+
Result value = node.out[NODE_TYPES[type].outputs[0].name] (primary port). draw also has secondary text; expose all ports.
|
|
37
|
+
Result keying: displayName(node) (custom name β type title); on duplicate display names, suffix " 2", " 3" in topo order; always ALSO keyed by node id.
|
|
38
|
+
Intermediate nodes still run; expose them under result.nodes / result.steps for debugging.
|
|
39
|
+
|
|
40
|
+
## Key & auth
|
|
41
|
+
- One key: NanoGPT API key (or OAuth access token β identical usage).
|
|
42
|
+
- Headers on every call: Authorization: Bearer <key> AND x-api-key: <key>.
|
|
43
|
+
- Library: constructor arg api_key, fallback env NANOGPT_API_KEY. No key + graph has network nodes β clear upfront error.
|
|
44
|
+
- Balance: /api/check-balance POST {} returns {usd_balance} with ACAO * (works server-side fine). Optional helper.
|
|
45
|
+
- Whole-graph runs only (like exported apps). No subset/runGroup in v1.
|