{"id":50925352,"url":"https://github.com/leolobato/orcaslicer-headless","last_synced_at":"2026-06-16T22:03:41.993Z","repository":{"id":349872581,"uuid":"1182748539","full_name":"leolobato/orcaslicer-headless","owner":"leolobato","description":"A REST API that wraps OrcaSlicer to provide headless 3D print slicing","archived":false,"fork":false,"pushed_at":"2026-05-12T19:08:40.000Z","size":1659,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-12T21:10:41.336Z","etag":null,"topics":["3d-printing","bambulab","makers","orcaslicer","slicer"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/leolobato.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-15T23:07:23.000Z","updated_at":"2026-05-12T19:09:41.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/leolobato/orcaslicer-headless","commit_stats":null,"previous_names":["leolobato/orcaslicer-cli","leolobato/orcaslicer-headless"],"tags_count":14,"template":false,"template_full_name":null,"purl":"pkg:github/leolobato/orcaslicer-headless","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leolobato%2Forcaslicer-headless","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leolobato%2Forcaslicer-headless/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leolobato%2Forcaslicer-headless/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leolobato%2Forcaslicer-headless/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/leolobato","download_url":"https://codeload.github.com/leolobato/orcaslicer-headless/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leolobato%2Forcaslicer-headless/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34425024,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-16T02:00:06.860Z","response_time":126,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["3d-printing","bambulab","makers","orcaslicer","slicer"],"created_at":"2026-06-16T22:03:41.262Z","updated_at":"2026-06-16T22:03:41.985Z","avatar_url":"https://github.com/leolobato.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# OrcaSlicer Headless\n\nA REST API that wraps [OrcaSlicer](https://github.com/SoftFever/OrcaSlicer) to provide headless 3D print slicing. Upload a `.3mf` file with Bambu Lab profile IDs and get back a sliced `.3mf` with generated G-code, or import an `.stl` into a preview draft, adjust its layout, materialize it to `.3mf`, and slice it through the same pipeline.\n\n## Quick Start\n\n### Using the pre-built image (recommended)\n\n```bash\ndocker run -d -p 8070:8070 -v ./data:/data ghcr.io/leolobato/orcaslicer-headless:latest\n```\n\nOr with Docker Compose, create a `docker-compose.yml`:\n\n```yaml\nservices:\n  orcaslicer-headless:\n    image: ghcr.io/leolobato/orcaslicer-headless:latest\n    ports:\n      - \"8070:8070\"\n    volumes:\n      - ./data:/data\n```\n\nThen run:\n\n```bash\ndocker compose up\n```\n\n### Building from source\n\n```bash\ngit clone https://github.com/leolobato/orcaslicer-headless.git\ncd orcaslicer-headless\ndocker compose up --build\n```\n\n\u003e **Note:** Building from source compiles `libslic3r` and the `orca-headless` binary from the OrcaSlicer C++ source (vendored as a git submodule pinned to v2.3.2). Expect a 10–15 minute first build with BuildKit cache mounts; subsequent builds reuse the deps layer.\n\n---\n\nThe API will be available at `http://localhost:8070`.\n\n## Architecture\n\nThe service is a thin Python (FastAPI) layer over a purpose-built C++ binary\n(`orca-headless`) that links `libslic3r` directly. The Python side owns\nprofile loading, the token cache, and HTTP routing; the C++ side owns\nslicing and 3MF reads/writes through the same code paths the OrcaSlicer\nGUI uses.\n\n- **`app/`** — FastAPI app, profile resolution, token cache (`/data/cache`),\n  request adapters into `orca-headless`.\n- **`cpp/orca-headless`** — compiled from `vendor/OrcaSlicer` (pinned at\n  v2.3.2). Subcommands include `slice`, `use-set`, `stl-draft`,\n  `dump-profiles`, and `dump-options`.\n- **Token cache** — every uploaded `.3mf` is stored once by sha256;\n  subsequent calls (inspect, slice, thumbnail) reference the token.\n- **STL draft cache** — preview-first STL imports keep the original STL,\n  current draft `.3mf`, and scene metadata together until the draft is\n  accepted or expires.\n\n### Why a custom binary instead of OrcaSlicer's built-in CLI\n\n**The headline reason is GUI parity.** This API exists to slice files exactly\nthe way the OrcaSlicer GUI would, so the user gets identical gcode whether\nthey hit \"Slice\" in the desktop app or send the same project here. OrcaSlicer\nships a `--slice` CLI mode on its main GUI binary, but it is *not* the same\ncode path as the GUI — it's a side-mode that skips parts of the GUI's\nload-time setup, applies its own normalization, and surfaces results\ndifferently. An earlier version of this project shelled out to that CLI, and\nevery category of bug we fixed in production turned out to be the same\nshape: *the CLI accepted/produced something the GUI wouldn't, or rejected\nsomething the GUI handled, and we had to reimplement that piece in Python or\nwork around it.* Examples of code we wrote then deleted when we cut over to\nthe headless binary:\n\n- A `_sanitize_3mf` pass that clamped values the CLI rejected but the GUI\n  silently corrected.\n- A `_normalize_filament_vector_shapes` shim that wrapped scalar\n  `filament_notes` into a list because the CLI's `coStrings` loader was\n  strict in ways the GUI's `Preset` chain was not.\n- A whole `app/normalize.py` (~140 lines) replicating `Preset::normalize`\n  for per-filament keys the CLI didn't reshape on its own.\n- `_write_bbl_machine_full_shims` to materialize files the AppImage's CLI\n  reads at slice time but doesn't ship.\n- A regex-driven failure parser that scraped boost-log lines plus\n  `result.json` because the CLI's failure surfacing didn't match what the\n  GUI tells the user.\n\nEvery one of those was a divergence between two consumers of `libslic3r`\n(the GUI and the CLI), and every one was a maintenance liability — fixing\nthe next slicer release meant porting our workaround forward too. The\nheadless binary eliminates the divergence by calling the same `libslic3r`\nentry points the GUI uses (`PresetBundle::construct_full_config`,\n`Print::process`, `bbs_3mf` readers/writers) so when GUI behaviour changes\nfor a given config, we inherit the change at the source.\n\nThe cost is a ~12-minute first build of `libslic3r` and its transitive deps\nwhen there's no Docker layer cache. The Dockerfile uses BuildKit cache\nmounts so subsequent rebuilds only recompile what changed.\n\n## API Endpoints\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| GET | `/health` | API status and version |\n| GET | `/profiles/machines` | List machine profiles (printers) |\n| GET | `/profiles/processes` | List process profiles. Filter: `?machine={setting_id}` |\n| GET | `/profiles/filaments` | List filament profiles. Filter: `?machine={setting_id}\u0026ams_assignable=true` |\n| GET | `/profiles/machines/{setting_id}` | Fully-resolved machine profile with inheritance chain |\n| GET | `/profiles/processes/{setting_id}` | Fully-resolved process profile with inheritance chain |\n| GET | `/profiles/filaments/{setting_id}` | Fully-resolved filament profile with inheritance chain |\n| GET | `/profiles/plate-types` | List supported bed surface types |\n| POST | `/profiles/resolve-for-machine` | GUI-equivalent process / filament / plate-type fallbacks for a target machine (alias \u003e default \u003e type/layer-height match) |\n| POST | `/profiles/filaments` | Import a custom filament profile JSON |\n| POST | `/profiles/filaments/resolve-import` | Preview filament import resolution without saving |\n| DELETE | `/profiles/filaments/{setting_id}` | Delete a custom filament profile |\n| POST | `/profiles/reload` | Hot-reload all profiles from disk |\n| POST | `/3mf` | Upload a `.3mf` to the token cache; returns `{token, sha256, size}` |\n| GET | `/3mf/{token}` | Download cached `.3mf` bytes |\n| DELETE | `/3mf/{token}` | Drop a cached upload |\n| GET | `/3mf/{token}/inspect` | Structured summary (plates, filaments, used-filament dispatch, estimate, thumbnails) |\n| GET | `/3mf/{token}/plates/{n}/thumbnail` | PNG bytes of the plate thumbnail (`?kind=main\\|small\\|top\\|pick\\|no_light`) |\n| POST | `/stl/import` | Import an STL into a preview draft session and return bed/object scene metadata |\n| POST | `/stl/{draft_token}/layout` | Apply a draft layout action (`auto_orient`, `rotate_x_90`, `rotate_x_minus_90`, `rotate_y_90`, `rotate_y_minus_90`, `rotate_z_90`, `rotate_z_minus_90`, `center`, `arrange`, `reset`) |\n| POST | `/stl/{draft_token}/3mf` | Materialize an accepted STL draft as a normal cached 3MF token |\n| POST | `/slice/v2` | Slice a cached `.3mf`, returns `{output_token, estimate, settings_transfer}` |\n| POST | `/slice-stream/v2` | Same as `/slice/v2` but streams progress via SSE |\n\nAll profile identifiers use `setting_id` values (e.g. `GM014`, `GP004`, `GFSA00`).\n\n### Slicing example\n\n```bash\n# 1. Upload — get a cache token\nTOK=$(curl -s -X POST http://localhost:8070/3mf \\\n  -F \"file=@model.3mf\" \\\n  | python3 -c \"import json,sys; print(json.load(sys.stdin)['token'])\")\n\n# 2. Slice via JSON body\nOUT=$(curl -s -X POST http://localhost:8070/slice/v2 \\\n  -H 'Content-Type: application/json' \\\n  -d \"{\n    \\\"input_token\\\": \\\"$TOK\\\",\n    \\\"machine_id\\\": \\\"GM014\\\",\n    \\\"process_id\\\": \\\"GP004\\\",\n    \\\"filament_settings_ids\\\": [\\\"GFSA00\\\"],\n    \\\"auto_center\\\": false\n  }\" | python3 -c \"import json,sys; print(json.load(sys.stdin)['output_token'])\")\n\n# 3. Download the sliced .3mf\ncurl -s -o sliced.3mf http://localhost:8070/3mf/$OUT\n```\n\nThe token cache is content-addressed (sha256-keyed): repeated uploads of the same bytes resolve to the same token. `auto_center=false` keeps the model in its 3MF-stored position, matching the GUI's behaviour on import.\n\n### STL slicing example\n\n`/slice/v2` slices cached `.3mf` tokens. STL files first go through a draft\nsession so callers can preview and adjust the same Orca model placement that\nwill be sliced. A draft becomes sliceable only after `POST /stl/{draft}/3mf`\nreturns a normal 3MF cache token.\n\n```bash\n# 1. Import the STL into a draft session\nDRAFT=$(curl -s -X POST http://localhost:8070/stl/import \\\n  -F \"machine_id=GM020\" \\\n  -F \"process_id=GP000\" \\\n  -F \"center=true\" \\\n  -F \"arrange=true\" \\\n  -F \"auto_orient=false\" \\\n  -F \"file=@part.stl;type=model/stl\" \\\n  | python3 -c \"import json,sys; print(json.load(sys.stdin)['draft_token'])\")\n\n# 2. Optional: apply the same layout actions exposed in the preview UI\ncurl -s -X POST http://localhost:8070/stl/$DRAFT/layout \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"action\":\"auto_orient\"}' \u003e/dev/null\n\n# 3. Accept/materialize the draft into the normal 3MF token cache\nTOK=$(curl -s -X POST http://localhost:8070/stl/$DRAFT/3mf \\\n  | python3 -c \"import json,sys; print(json.load(sys.stdin)['input_token'])\")\n\n# 4. Slice the materialized 3MF\nOUT=$(curl -s -X POST http://localhost:8070/slice/v2 \\\n  -H 'Content-Type: application/json' \\\n  -d \"{\n    \\\"input_token\\\": \\\"$TOK\\\",\n    \\\"machine_id\\\": \\\"GM020\\\",\n    \\\"process_id\\\": \\\"GP000\\\",\n    \\\"filament_settings_ids\\\": [\\\"GFL99\\\"],\n    \\\"auto_center\\\": false\n  }\" | python3 -c \"import json,sys; print(json.load(sys.stdin)['output_token'])\")\n\ncurl -s -o sliced-from-stl.3mf http://localhost:8070/3mf/$OUT\n```\n\n### STL draft preview API\n\nSTL support is preview-first. Upload an STL with `POST /stl/import`, apply optional layout actions through `POST /stl/{draft_token}/layout`, then materialize the accepted draft with `POST /stl/{draft_token}/3mf`. The materialized token is a normal 3MF token and can be used with `GET /3mf/{token}/inspect` and `POST /slice/v2`.\n\nThe gateway/browser renders the original STL using the scene transforms returned by these endpoints. Apply each object's `mesh_transform` in STL-local space first, then apply `transform` for the Orca instance placement. `orcaslicer-headless` remains the source of truth for import, orientation, arrange, and 3MF generation.\n\n### Custom filament import\n\nYou can import custom filament profiles that inherit from any built-in profile. The API resolves the full inheritance chain, merges parent fields, and produces a standalone profile ready for slicing.\n\n**Payload:**\n\n```json\n{\n  \"name\": \"My Custom PLA\",\n  \"inherits\": \"Bambu PLA Basic @BBL P1S\",\n  \"nozzle_temperature\": [230]\n}\n```\n\nOnly `name` is required. Any field you provide overrides the inherited value.\n\n**Inheritance resolution** works recursively — if a parent itself inherits from another profile, the full chain is walked and merged. When multiple vendors define a profile with the same name, the resolver prefers the same vendor as the child profile before falling back to others.\n\n**ID generation:** If no `setting_id` is provided, it defaults to `name`. If no `filament_id` is provided, one is auto-generated as `\"P\" + md5(name)[:7]` with collision fallback.\n\n**Preview before saving:** Use `POST /profiles/filaments/resolve-import` to see the fully materialized profile (including resolved `filament_id` and `filament_type`) without persisting it. Then call `POST /profiles/filaments` to save.\n\n**AMS assignability:** A filament is assignable to the AMS when it has `instantiation: \"true\"`, a non-empty `setting_id`, and a resolved `filament_id`. Imported profiles meet these criteria automatically.\n\n## Web UI\n\nA built-in web interface is available at `http://localhost:8070/web/` for browsing and managing profiles.\n\n- **Browse** all machine, process, and filament profiles with search filtering\n- **Inspect** any profile to see its fully resolved fields or an inheritance diff view showing what each level in the chain overrides\n- **Filter by machine** using the sidebar dropdown — processes and filaments automatically filter to compatible profiles\n- **Create** custom filament profiles by picking a parent and overriding specific fields (grouped by category: Temperature, Retraction, Speed, etc.)\n- **Edit and delete** existing custom filament profiles\n\nThe UI is served as static files from the same container — no additional setup needed.\n\n## Configuration\n\nEnvironment variables (set in `docker-compose.yml`):\n\n| Variable | Default | Description |\n|---|---|---|\n| `ORCA_HEADLESS_BINARY` | `/opt/orca-headless/bin/orca-headless` | Path to the compiled `orca-headless` binary |\n| `PROFILES_DIR` | `/opt/orcaslicer/profiles` | Path to vendor profile directory |\n| `USER_PROFILES_DIR` | `/data` | Path for imported/custom profiles |\n| `CACHE_DIR` | `/data/cache` | Path for the token cache (uploaded + sliced 3MFs) |\n| `CACHE_MAX_BYTES` | `10737418240` (10 GB) | Token cache size cap; oldest evicted first |\n| `CACHE_MAX_FILES` | `200` | Token cache entry-count cap |\n| `STL_DRAFT_CACHE_DIR` | `/data/cache/stl-drafts` | Path for in-progress STL draft sessions |\n| `STL_DRAFT_TTL_SECONDS` | `86400` | Fixed lifetime for STL draft sessions before cleanup |\n| `LOG_LEVEL` | `INFO` | Logging level |\n\n## Known Caveats\n\nThese don't affect output correctness in any case observed so far, but they're worth knowing:\n\n- **Multi-filament start-XY can pick the opposite endpoint of an axis.** When the GUI begins a perimeter at one end of the model's bounding box on a given axis, our slice may begin at the other end. Time, weight, layer count, and toolpath geometry still match within the parity tolerances; the start-point pick is a libslic3r ordering heuristic and not stable across config equivalences.\n- **~0.6% structural diff on the fidelity baseline.** Fixture 01 produces 157 internal-solid-infill regions in our output vs the GUI's 137 — likely a `FullPrintConfig::defaults()` vs `PresetBundle::full_config()` discrepancy upstream of slicing. Cosmetic, and currently within the parity tolerance.\n\n## Related Projects\n\nOrcaSlicer Headless is the **headless slicing engine and profile catalog** in a suite of self-hosted projects that together replace the Bambu Handy app for printers in **Developer Mode** — keeping everything on your LAN, with no Bambu cloud.\n\n**Self-hosted services**\n\n- **[bambu-gateway](https://github.com/leolobato/bambu-gateway)** — Printer control plane and slicing web app. Talks to printers over MQTT/FTPS to monitor status, send commands, and upload jobs. Slices and prints 3MF files from the browser using `orcaslicer-headless`.\n- **OrcaSlicer Headless** — this project.\n- **[bambu-spool-helper](https://github.com/leolobato/bambu-spool-helper)** — Bridge between [Spoolman](https://github.com/Donkie/Spoolman) and the printer's AMS. Links real spools to Bambu filament profiles (via `orcaslicer-headless`) and pushes the settings to a chosen tray over MQTT.\n\n**iOS apps**\n\n- **[bambu-gateway-ios](https://github.com/leolobato/bambu-gateway-ios)** — Phone client for `bambu-gateway`. Browse printers, import 3MF files (including from MakerWorld), preview G-code, and start prints. Live Activities and push notifications for print state changes.\n- **[spool-browser](https://github.com/leolobato/spool-browser)** — Phone client for `bambu-spool-helper` and Spoolman. Browse the spool inventory, link Bambu profiles to spools, activate filaments on the AMS, and print physical spool labels over Bluetooth.\n\n## License\n\nThis project is licensed under the [GNU Affero General Public License v3.0](LICENSE) — the same license as [OrcaSlicer](https://github.com/SoftFever/OrcaSlicer).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fleolobato%2Forcaslicer-headless","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fleolobato%2Forcaslicer-headless","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fleolobato%2Forcaslicer-headless/lists"}