# nanoodle — full reference > Reusable AI apps for NanoGPT users. Sign in with NanoGPT using OAuth, connect text, image, video, audio and LLM steps in your browser, and run them on your own NanoGPT balance. Turn a workflow into an app to use again, share its link, or export one self-contained HTML file. No separate nanoodle account, no install, and no analytics. This file is generated by `scripts/gen-llms-full.mjs` in [nanoodlecom/nanoodle](https://github.com/nanoodlecom/nanoodle) — do not edit by hand. Every section is copied or extracted verbatim from the repository's README.md and index.html, and from the public READMEs of [nanoodlecom/nanoodle-js](https://github.com/nanoodlecom/nanoodle-js) and [nanoodlecom/nanoodle-py](https://github.com/nanoodlecom/nanoodle-py). The short index lives at [llms.txt](https://nanoodle.com/llms.txt). ## nanoodle (the site) Reusable AI apps for NanoGPT users. Sign in with your NanoGPT account, connect text, image, video, audio and LLM steps into a workflow, and run it on your own NanoGPT balance. Turn a workflow you want to use again into an app, share its link, or export it as one self-contained `.html` file. **Try it now: [nanoodle.com](https://nanoodle.com)** — sign in with [NanoGPT](https://nano-gpt.com) using OAuth. No separate nanoodle account or install; live model calls use your NanoGPT balance. Pasting an API key is also supported. Open an example, change its input, and run it. Choose **Create app** to keep the inputs and outputs together for next time, then share or export it. - **Visual node editor** — drag ports together, compatible inputs glow and snap by type; disconnected groups run in parallel. - **Any graph becomes an app** — auto-generated inputs → Run → outputs, shareable as a URL or exported as one self-contained `.html` file. - **Browser-based editor and app runtime** — no nanoodle model proxy, analytics, or tracking; model requests go directly to NanoGPT. - **Six UI languages** — EN/ES/FR/DE/PT/JA, auto-detected with a switcher. ### Privacy architecture nanoodle is a folder of static HTML pages — three core pages plus a changelog, guides and landing pages. The editor and its apps run in the browser; model generation happens at NanoGPT: - **No analytics, no tracking, no third-party scripts.** The only dependency that isn't hand-written is vendored into `vendor/` and served from the same origin — no CDNs, no external gateways. - **Model calls go directly from your browser to [NanoGPT](https://nano-gpt.com)** using your OAuth token or API key. NanoGPT receives the inputs and credentials needed for those calls; there is no nanoodle backend in that path receiving your prompts, outputs, or credentials. - **Your key and your workflows live in your browser's localStorage.** Sign in via NanoGPT OAuth (PKCE) or paste an API key; either way the credential is stored on your device and used to authenticate requests to NanoGPT. - **Exported apps are self-contained files with no key inside.** The person you send one to signs in with NanoGPT or supplies their own key. Share links (`#g=` / `#a=`) encode the graph in the URL fragment, which browsers omit from requests to the hosting server. Anyone you give the full link to can read its graph and any included inputs or samples. - A per-path Content-Security-Policy (`_headers`) pins which origins each page may talk to, so "no tracking" is enforced by the browser, not just promised in a README. These promises describe the browser editor and its apps. The separate hosted service at `mcp.nanoodle.com` receives the inputs for tools called there; see its own documentation before using it. The privacy page at `/legal` describes the browser path in plain terms. ### Share something useful or report a problem [Share a workflow](https://github.com/nanoodlecom/nanoodle/issues/new?template=share-workflow.yml) you use with NanoGPT, or [report a workflow problem](https://github.com/nanoodlecom/nanoodle/issues/new?template=workflow-problem.yml). Tell us the task, what happened, and whether you have used it again. Cost is useful if you know it. These are voluntary public GitHub submissions; remove API keys, OAuth tokens, and private inputs or media before posting. Nothing is submitted from the editor automatically. ### Hosted vs. self-hosted **Hosted:** https://nanoodle.com serves exactly the files in this repo as a static site (Cloudflare Pages / Workers assets — see `wrangler.jsonc`, `_headers`, `_redirects`, `sw.js`). There is no server-side code; hosting adds nothing but HTTPS and the CSP headers. **Self-hosted:** serve the folder with any static file server: ```sh python3 -m http.server 8000 # http://localhost:8000/ → the editor # http://localhost:8000/play.html → the app builder ``` Notes for self-hosting: - OAuth sign-in needs an `http(s)` origin; pasting an API key also works from plain `file://`. - `_headers` and `_redirects` are Cloudflare conventions. On another host, reproduce the CSP headers yourself or skip them (the app works without, you just lose the browser-enforced guarantee). - Exported `.html` apps are standalone and need no hosting at all. ### Referral codes The default config routes 10% of usage as referral credits to the maintainer. If you self-host, swap the key. Concretely, two things carry a referral code: - `invitation_code` in the OAuth authorize URL (`index.html`, `play.html`, search for `invitation_code`) - the "create an account" links to `nano-gpt.com/r/...` This costs you nothing — NanoGPT pays it out of their side, and per their program, signing up through a referral link gives you a 5% discount. It does credit the maintainer for accounts and usage originating here. Replace the code with your own (or delete the parameter and use bare `nano-gpt.com` links) if you'd rather not. ### The pages - **`index.html`** — the editor (site root, `/app`). A NanoGPT workflow canvas: Text, Join, LLM, Image, Edit, Vision, video, music, speech and more. Drag port to port (compatible inputs glow and snap by type), run disconnected groups in parallel, share a graph via URL, save/load JSON. UI in six languages (EN/ES/FR/DE/PT/JA), auto-detected with a switcher. - **`play.html`** — the app builder (`/play`, or ✨ **Create app** in the editor). Turns any workflow into a standalone app: auto-generated inputs → Run → outputs. Restyle it with [patchling](https://github.com/255BITS/patchling), share via `#a=` link, or export a self-contained `.html`. - **`legal.html`** — terms, privacy, FAQ (`/legal`). - **`changelog.html`** — release notes (`/changelog`), generated from `updates.json` by `scripts/gen-changelog.mjs`, plus an Atom feed (`feed.xml`). - **`guide/`** — static docs (`/guide/`): the `noodle-graph.json` format, running workflows headlessly, self-hosting, share links, the Custom endpoint localhost contract, and a how-to for every reviewed gallery workflow (`/guide/examples/`). Zero scripts; same-origin images and media only. - The rest is landing pages: comparisons (`nanoodle-vs-comfyui.html`, `nanoodle-vs-n8n.html`, `comfyui-alternative.html`, `comfyui-alternative-no-gpu.html`) and localized home pages (`es/`, `fr/`, `de/`, `pt/`, `ja/`). ### Run workflows from code A saved graph (`noodle-graph.json`) — or any share link — doesn't need the browser: two zero-dependency sibling libraries re-execute it headlessly — [nanoodle-js](https://github.com/nanoodlecom/nanoodle-js) (`npm install nanoodle` 0.8.0, Node ≥ 20) and [nanoodle-py](https://github.com/nanoodlecom/nanoodle-py) (`pip install nanoodle` 0.5.0, stdlib-only). Same graphs, same results; useful for scripts, servers, and agent skills. Walkthrough: [Run workflows headlessly](https://nanoodle.com/guide/run-headless). ```js import { Workflow } from "nanoodle"; const wf = await Workflow.load("https://nanoodle.com/#g=…"); // or noodle-graph.json const result = await wf.run({ Text: "NIGHT RIDE" }); await result.get("Image").save("night-ride.png"); ``` ```bash npx nanoodle inspect "https://nanoodle.com/#g=..." # offline, no key npx nanoodle run "https://nanoodle.com/#g=..." --out ./out ``` ### Ecosystem Everything lives under the [nanoodlecom](https://github.com/nanoodlecom) GitHub org: | Repo | What it is | | --- | --- | | [nanoodle](https://github.com/nanoodlecom/nanoodle) | NanoGPT workflow editor, app builder, and site (this repo) | | [nanoodle-js](https://github.com/nanoodlecom/nanoodle-js) | Zero-dependency JS executor — use when running saved graphs from Node or the CLI | | [nanoodle-py](https://github.com/nanoodlecom/nanoodle-py) | Zero-dependency Python executor — use when your scripts speak Python | | [nanoodle-mcp](https://github.com/nanoodlecom/nanoodle-mcp) | MCP server exposing saved graphs as tools — use when an AI agent should run your workflows | | [run-noodle-action](https://github.com/nanoodlecom/run-noodle-action) | GitHub Action — use when a graph should run in CI | | [nanoodle-skill](https://github.com/nanoodlecom/nanoodle-skill) | One agent skill that teaches an agent to build any graph — use when the agent should author workflows itself | | [noodle-skills](https://github.com/nanoodlecom/noodle-skills) | Prebuilt one-task agent skills — use when you want a ready-made skill, no graph editing | | [awesome-noodles](https://github.com/nanoodlecom/awesome-noodles) | Gallery of ready-to-open graphs — use when you want examples to start from | | [built-with-nanoodle](https://github.com/nanoodlecom/built-with-nanoodle) | Showcase of finished apps, every one still ours — use when you want to see what nanoodle makes, or to put yours on the list | | [noodle-embed](https://github.com/nanoodlecom/noodle-embed) | `` web component — use when a page should render a workflow read-only | | [mp4cat](https://github.com/nanoodlecom/mp4cat) | Lossless mp4 concat, browser or Node — use when you just need to join clips, no ffmpeg | Naming note: the package is `nanoodle` on **both** registries while the repos are `nanoodle-js` / `nanoodle-py` — so it's `npm install nanoodle` and `pip install nanoodle`. ### Development No build step. Edit the HTML files, refresh the browser. `scripts/check-*.mjs` are offline pre-commit checks (wired via `.githooks/`, `git config core.hooksPath .githooks`) covering the export bundler, the OAuth flow, run-engine compatibility, pricing, i18n coverage and more. They spend no API credits — everything runs against recorded fixtures. `scripts/check-js-parity.mjs` dual-runs the same graphs through play.html’s `RUNTIME_JS` and the sibling [`nanoodle-js`](https://github.com/nanoodlecom/nanoodle-js) package and asserts identical NanoGPT request bodies — the safety net for eventually replacing the inlined processor with the package. Skips if `nanoodle-js` isn’t checked out next to this repo (or set `NANOODLE_JS`). play.html also embeds a generated bundle of that package (the `njs-engine` script block, `scripts/gen-js-engine.mjs`, freshness-checked pre-commit) and routes network nodes through it by default — `?engine=play` (or `localStorage.njs_engine = "0"`) opts back into the built-in runners; `scripts/check-njs-delegation.mjs` asserts the delegated path produces byte-identical requests. Exported apps carry the bundle too, so the same flag works there. The editor honors the same flag: index.html lazy-loads the same bundle as `vendor/njs-engine.js` (emitted by the same generator), and known library gaps are vetoed back to the built-in runners per run; `scripts/check-njs-editor-delegation.mjs` pins that path to the built-in runners byte-for-byte. `updates.json` is the in-app changelog behind the 📣 button. It's opt-in per commit: add an `Update: one polished line` to a commit message and the `post-commit` hook folds it in. Commits without one stay silent. Edit the JSON by hand anytime; `scripts/check-updates.mjs` keeps it valid. ### License [MIT](https://github.com/nanoodlecom/nanoodle/blob/main/LICENSE). ## Node types in the editor The editor registers 27 node types. Titles and one-line descriptions below are taken verbatim from the node registry in index.html (the same text the in-editor quick-add menu shows); the `key` is the type identifier used in noodle-graph.json and in the executor libraries' supported-node tables. ### Inputs - `text` — 📝 Text: Text you type — usually a prompt - `upload` — 📷 Image input: Upload or capture an image - `aupload` — 🎤 Audio input: Upload an audio clip - `vupload` — 📹 Video input: Upload a video clip - `choice` — 🔘 Choice: A dropdown the app user picks from — the selected line is the value on the wire ### Text - `join` — 🔗 Join: Glue two texts together - `llm` — 🧠 LLM: Text (+ images, audio) → text — any chat model - `endpoint` — 🔌 Custom endpoint: POST to a URL you own — a Choice can pick the path ### Image - `image` — 🖼 Image: Text → image - `edit` — ✂️ Edit: Image(s) + text → image - `inpaint` — 🩹 Inpaint: Brush a region of an image → repaint just that area - `resize` — 📐 Resize / crop: Image → image — scale, crop, fit an aspect - `vision` — 👁 Vision: Image → text (describe / ask) ### Video - `tvideo` — 🎬 Text→Video: Text → video (takes ~1 min to generate) - `ivideo` — 📽️ Image→Video: Image (+text) → video (renders ~1 min) - `vedit` — 🎞️ Video edit: Video (+text) → video — edit / extend / upscale - `vframes` — 🎞 Video → frames: Pull still frames out of a video - `combine` — 🎬 Combine videos: Join clips end-to-end into one video - `soundtrack` — 🎚 Soundtrack: Video + audio → video (add music / narration) - `lipsync` — 🎭 Avatar / lipsync: Image + audio → talking video ### Audio - `music` — 🎵 Music: Text → music - `remix` — 🎛 Remix audio: Audio + text → audio (cover / extend) - `tts` — 🗣️ Speech: Text → speech (TTS) - `trim` — ✂️ Trim audio: Cut audio to a start + length - `extractaudio` — 🔊 Extract audio: Pull the soundtrack off a video (→ WAV) - `transcribe` — 🎙️ Transcribe: Audio → text (speech-to-text) ### Notes - `comment` — 💬 Comment: A sticky note to label your graph ## nanoodle-js — run workflows from JavaScript / Node.js Repository: https://github.com/nanoodlecom/nanoodle-js · package: `nanoodle` (`npm install nanoodle`). Curated from the repository README; see it for the full docs (specs, agent skills, error model, cost fields). **Run visual AI workflows from Node.js.** Design them in the [nanoodle](https://nanoodle.com) editor, save as `noodle-graph.json`, then load and re-run them here — same graph, same [NanoGPT](https://nano-gpt.com) API, your own key. Zero runtime dependencies. Library + CLI in one install. Looking for Python? → **[nanoodle-py](https://github.com/nanoodlecom/nanoodle-py)** ### At a glance **Build once, run anywhere.** The browser app is for designing and testing. This package is for automating the same workflows in scripts, servers, and agents. | | | |---|---| | **Package** | `nanoodle` on npm | | **Runtime** | Node ≥ 20 · built-in `fetch` · no deps | | **Sibling** | [Python package](https://github.com/nanoodlecom/nanoodle-py) (same graphs, same semantics) | | **Editor** | [nanoodle.com](https://nanoodle.com) — wire nodes, hit 💾, download the graph | ### Install ```bash npm install nanoodle # library + CLI npx nanoodle --help # or run the CLI without installing ``` ### Quickstart (library) ```js import { Workflow } from "nanoodle"; const wf = await Workflow.load("noodle-graph.json"); // key from NANOGPT_API_KEY // …or load any nanoodle share link — the URL is the package: // const wf = await Workflow.load("https://nanoodle.com/#g=…"); const result = await wf.run({ Text: "a cozy ramen shop on a rainy night" }); await result.get("Image").save("ramen.png"); // media: MediaRef (url + bytes()/save()) console.log(result.costUsd, result.remainingBalance); ``` With the app’s starter graph (text → LLM prompt-writer → image), that’s the whole program. ### Quickstart (CLI) No graph yet? Scaffold the starter (text → LLM prompt-writer → image) and inspect it — both offline, no key needed: ```bash npx nanoodle init # writes ./noodle-graph.json npx nanoodle inspect noodle-graph.json # shows inputs, outputs, settings ``` Then run (calls NanoGPT and spends from your balance): ```bash export NANOGPT_API_KEY=... # or --key K, or --env-file .env npx nanoodle run noodle-graph.json --input Text="a cozy ramen shop on a rainy night" npx nanoodle run graph.json --input n2.system=@style.txt --set n3.size=1k --out ./renders ``` Media outputs are saved under `--out` (default `./noodle-out`, created only when there is media to save); a JSON run summary always goes to stdout, progress lines to stderr; exit code `0` on success, `1` on failure. - `--input k=@path` — read a file (media as media; `.txt` / `.md` / `.json` as text) - `--set k=v` — override a setting (`n3.model=flux-pro`) - `--out dir` — where media outputs land (default `./noodle-out`) - `--timeout ms` — overall run timeout - `--json` — quiet mode: skip the stderr progress lines (the JSON summary is printed either way) - `--env-file path` — load `NANOGPT_API_KEY` from a `.env`-style file (`--key` wins if both are set) #### The URL is the package Every nanoodle share link is a runnable artifact — paste one straight from a README, a chat, or a tweet, wherever a `graph.json` path is accepted: ```bash npx nanoodle inspect "https://nanoodle.com/#g=..." # what does it need? (offline) npx nanoodle run "https://nanoodle.com/play.html#a=..." --input Text=hi # run it ``` Workflow links (`#g=`/`#j=`) and app links (`#a=`, graph only — the app shell stays in the browser) both work, as do da.gd/TinyURL short links (resolved by reading redirect headers; no credentials are ever sent). Direct links decode fully offline. Quote the URL — `#` starts a comment in most shells. Links mangled in transit (a character flipped or dropped by a chat app or a copy/paste) are recovered best-effort: the graph's nodes and wires are salvaged and a warning is printed. Only damage inside the graph itself makes a link unrecoverable. ### Supported nodes | runs | node types | |---|---| | local | text, upload (image/audio/video), choice, join, comment | | local media† | resize, vframes, combine, soundtrack, trim, extractaudio | | NanoGPT | llm (incl. vision + audio input), image, edit, inpaint*, vision, tvideo, ivideo, vedit, lipsync, music, remix, tts, transcribe | † **local media** prefers a pure-JS path that matches the browser (lossless mp4 remux, PCM-WAV trim, PNG resize). **ffmpeg** on `PATH` is the fallback for everything else (soft dependency — not an npm package); clear error if it’s required and missing. \* **inpaint:** the mask is composited onto black at the source image’s pixel size before send (same as the browser app) — white = repaint, black = keep. Brush-style white-on-transparent masks and plain B/W masks both work. ## nanoodle-py — run workflows from Python Repository: https://github.com/nanoodlecom/nanoodle-py · package: `nanoodle` (`pip install nanoodle`). Curated from the repository README; see it for the full docs (specs, agent skills, error model, cost fields). **Run visual AI workflows from Python.** Design them in the [nanoodle](https://nanoodle.com) editor, save as `noodle-graph.json`, then load and re-run them here — same graph, same [NanoGPT](https://nano-gpt.com) API, your own key. Zero runtime dependencies (stdlib only). Library + CLI in one install. Looking for JavaScript / Node? → **[nanoodle-js](https://github.com/nanoodlecom/nanoodle-js)** Running graphs in GitHub CI → [run-noodle-action](https://github.com/nanoodlecom/run-noodle-action) · saved graphs as AI-agent tools → [nanoodle-mcp](https://github.com/nanoodlecom/nanoodle-mcp) · Agent Skills → [nanoodle-skill](https://github.com/nanoodlecom/nanoodle-skill) / [noodle-skills](https://github.com/nanoodlecom/noodle-skills) ### At a glance **Build once, run anywhere.** The browser app is for designing and testing. This package is for automating the same workflows in scripts, servers, and agents. | | | |---|---| | **Package** | `nanoodle` on PyPI | | **Runtime** | Python ≥ 3.9 · stdlib only · no deps | | **Sibling** | [JavaScript package](https://github.com/nanoodlecom/nanoodle-js) (same graphs, same semantics) | | **Editor** | [nanoodle.com](https://nanoodle.com) — wire nodes, hit 💾, download the graph | ### Install ```bash pip install nanoodle export NANOGPT_API_KEY=... # nano-gpt.com API key (or OAuth access token) ``` Requires **0.2.0+** for share-link loading, the local media nodes (resize/vframes/combine/soundtrack/trim/extractaudio), and x402 `--pay`. ### Quickstart (library) ```python from nanoodle import Workflow wf = Workflow.load("noodle-graph.json") result = wf.run({"Text": "a cozy ramen shop on a rainy night"}) img = result["Image"] # media: MediaRef (url + bytes()/save()) img.save("ramen." + img.suggested_extension()) # extension matches the actual MIME (often jpg) print(result.cost_usd, result.remaining_balance) ``` With the app’s starter graph (text → LLM prompt-writer → image), that’s the whole program. #### The URL is the package Every nanoodle share link is a runnable artifact. Anywhere a `graph.json` path is accepted — `Workflow.load` or the CLI — a share link works just as well: ```python wf = Workflow.load("https://nanoodle.com/#g=...") # workflow link wf = Workflow.load("https://nanoodle.com/play.html#a=...") # app link (graph only) ``` Workflow links (`#g=`/`#j=`) and app links (`#a=`, graph only — the app shell stays in the browser) both decode, as do `da.gd`/TinyURL short links (resolved by reading redirect headers; no credentials are ever sent). Direct fragment links decode **fully offline** — zero network I/O, stdlib only. Paste one straight from a README, a chat, or a tweet. Links mangled in transit (a character flipped or dropped by a chat app or a copy/paste) are recovered best-effort: the graph's nodes and wires are salvaged and a warning is surfaced on `wf.warnings`. Only damage inside the graph itself makes a link unrecoverable. #### Discover a workflow’s interface ```python wf.inputs # [InputSpec(key="Text", node_id="n1", field="text", kind="textarea", ...)] wf.outputs # [OutputSpec(key="Image", node_id="n3", type="image", ports=["image"])] wf.settings # [SettingSpec(key="n3.size", kind="select", default="1k", ...)] ``` Input keys are flexible (case-insensitive): the node’s custom name, `nodeId.field` (`"n2.system"`), or the input’s label when unique. A workflow with exactly one required input also accepts a bare value: `wf.run("hello")`. An input node the author marked **optional** in the editor (the checkbox, saved as `fields.optional`) is skippable: `spec.optional` is `True`, and a run that omits it proceeds with an empty value that consumers drop — an optional style reference costs you nothing when you leave it out. ##### Breaking in 0.5.0: named nodes now name their input key A node with a custom name that surfaces exactly one input uses that name as the input key, even when the input is optional. This matches nanoodle-js 0.8.0, so one set of keys now works in both languages. It **renames advertised keys** on published workflows. Run `nanoodle-py inspect ` to see the current keys, or use `nodeId.field` (`"n2.system"`), which never changes. | workflow | 0.4.0 key | 0.5.0 key | old key still resolves? | |---|---|---|---| | pr-describe | `System prompt` | `Drafter` | no — now ambiguous across 3 nodes | | pr-describe | `System prompt 2` | `Auditor` | no | | pr-describe | `System prompt 3` | `Final PR body` | no | | jingle | `System prompt` | `Lyric writer` | no — now ambiguous across 2 nodes | | jingle | `System prompt 2` | `Style writer` | no | | visual-judge | `System prompt` | `Verdict` | yes — the label is unique | | narrated-poem | `System prompt` | `Poet` | yes | | video-teaser | `System prompt` | `Shot writer` | yes | Where the old label still names exactly one input it keeps resolving, so those calls need no change. Where two or more nodes shared it, the old key now raises `ambiguous` (or `unknown input` for the numbered forms) instead of silently picking one — the call fails loudly and costs nothing. #### Media inputs ```python from nanoodle import media_from_file wf.run({"Image": media_from_file("photo.jpg")}) # local file wf.run({"Image": "https://example.com/photo.jpg"}) # hosted or data: URL wf.run({"Image": raw_bytes}) # raw bytes (MIME sniffed) ``` Media is sent inline as base64 (NanoGPT has no upload endpoint). Files over ~4.4 MB (~3.5 MB for transcription) are refused locally with a clear error before any paid call. #### Settings, progress, errors ```python result = wf.run( {"Text": "sunset harbor"}, settings={"n3.model": "flux-dev", "n3.size": "1k"}, timeout=600, on_progress=lambda evt: print(evt["type"], evt.get("name", "")), ) ``` `timeout=` bounds the whole run. When it fires, `run()` returns straight away and the in-flight nodes stop polling within about a second, so the process is free to exit. Without `timeout=`, each node still waits out its own limit (video 600 s, audio 300 s). The deadline bounds the run, and nothing else. Media a node already produced stays fetchable after it: `result["Image"].save("out.png")` works once the deadline has passed, and works for a lane that finished while another lane timed out. `run()` raises `RunError` when an output (sink) node fails — `error.result` still has partial results, per-node statuses, and cost so far. Failures in lanes no output depends on only appear in `result.errors`. Unknown/unsupported node types, missing required inputs, bad keys, and a missing API key all fail **before** anything is spent. #### Prompt length caps Many image and video models reject a prompt over a fixed character count (HTTP 400, `prompt_too_long`) before anything is charged. In a graph that prompt is usually written by an upstream LLM, so there is nothing you can shorten. nanoodle trims the prompt to the model's cap at a sentence boundary and tells you it did: ```python result = wf.run({"Text": "..."}, on_progress=print) # {'node_id': 'n3', 'name': 'Image', 'from': 1440, 'to': 780, 'cap': 800, 'type': 'prompt-trimmed'} result.prompt_trims # the same records, for a caller that passed no on_progress ``` Every trim is also reported as a `RuntimeWarning`. Reporting can never fail your run: if you run with warnings as errors (`PYTHONWARNINGS=error`), the same sentence goes to stderr instead, and the run continues. A cap counts **UTF-16 code units**, which is what the model route counts and what nanoodle-js reports — not Python code points. One emoji is 1 code point and 2 code units, so `"🎉" * 450` is 450 to `len()` and 900 to the API. `from` and `to` in the trim record are code units for the same reason, and `nanoodle.prompt_caps.utf16_len` measures them. A cap this library does not know yet is learned from the live 400 and applied on the next run of the same `Workflow`. That applies to image, video and audio nodes. An `llm` or `vision` node is never fitted (its real limit is tokens), so its rejection is relayed exactly as the API worded it, with no promise that a retry would behave differently. Your own prompts are never rewritten, summarised or added to — the library only ever cuts an over-length prompt at the end, and always says so. `PROMPT_CAPS`, `prompt_cap`, `fit_prompt_text`, `is_prompt_too_long` and `prompt_cap_from_error` are public, for callers that do their own orchestration. ### CLI Installed as `nanoodle-py` (and `python -m nanoodle` always works): ```bash nanoodle-py inspect graph.json nanoodle-py run graph.json --input Text="a cozy ramen shop" --set n3.size=1k --out ./out nanoodle-py run graph.json --input n2.system=@style.txt --json nanoodle-py run graph.json --env-file .env --input Text="hello" # NANOGPT_API_KEY from a .env file nanoodle-py inspect "https://nanoodle.com/#g=..." # a share link works too (quote it — # is a shell comment) ``` - `--out DIR` — save media outputs to files - `--json` — machine-readable result - `--env-file PATH` — load `.env`-style `KEY=VALUE` lines (existing env vars win) With `--json`, a **failed** run still prints the same JSON on stdout — per-node `status` and `error`, the outputs that did complete, the cost already spent, any prompt trims (`promptTrims`) and any Nano deposit the run asked for (`payments`, empty unless you ran `--pay`) — and exits 1. Without `--json` a failed run prints `error: …` on stderr and exits 1, as before. That includes a failure caught **before** the first node runs (a missing required input, an unknown key, an unreadable graph). Nothing executed, so `nodes` is `{}` and `costUsd` is `0.0`, and the reason is in `errors[0].message`: ```json {"outputs": {"Answer": null}, "costUsd": 0.0, "costExact": true, "remainingBalance": null, "nodes": {}, "errors": [{"node_id": null, "name": null, "message": "missing required input: Answer"}], "promptTrims": [], "payments": []} ``` ### Supported nodes | runs | node types | |---|---| | local | text, upload (image/audio/video), choice, join, comment | | local media† | resize, vframes, combine, soundtrack, trim, extractaudio | | NanoGPT | llm (incl. vision + audio input), image, edit, inpaint*, vision, tvideo, ivideo, vedit, lipsync, music, remix, tts, transcribe | † **local media** needs **ffmpeg** on `PATH` (soft dependency — not a PyPI package). Same behaviour as the browser app; clear error if ffmpeg is missing. \* **inpaint:** the browser app composites the mask onto black at the source pixel size; this library passes your mask through verbatim. Supply a black/white mask matching the source dimensions.