Files
animation/plans/iclone-api-bridge-plan-2026-07-21.md
T
jeremy 3ba86b2ea8 feat: clothing lane, character sources, and DCC bridges
Bulk import of the working lanes that were living untracked on the PC.

Content:
- characters/  Lena/male body lanes, bakes, texture work, run logs
- clothing/    garment pipeline, configs, gates, contract docs
- garments/    MD-authored garment sources (.zprj/.zpac)
- UAL-Lib/     Universal Animation Library 2 source (.blend/.fbx/.glb)
- tools/       blender_bridge, iclone_bridge, md_bridge, tailor, glm_agent
- docs/, plans/, dev/, .agents/plans/

Repo hygiene:
- .gitattributes: LFS now covers .blend, .zprj, .zpac, .obj, .npy and the
  Reallusion .iAvatar/.ccAvatar/.ccRestore containers. Without this the
  ~3.8 GB in this commit would land as raw blobs. .png/.jpg are left out
  on purpose — ~250 are already tracked raw and converting them would
  rewrite every one without shrinking history.
- .gitignore: exclude /accurig/ (~1 GB AccuRig program files, redistributable
  from Reallusion, nothing authored here) and /dev/null/ (git-lfs hook copies
  dropped by a `>/dev/null` redirect on Windows).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 15:55:43 -07:00

12 KiB

Plan: iClone 8 Python API bridge — control iClone from the terminal

Status: ready for implementation · Author: Fable 5 session 2026-07-21 · Implementer: GLM session

0. Context (you have no other context — read this fully)

You are on Jeremy's Windows 11 PC. This repo (C:\Users\Jeremy\tinqs\animation) is the animation bridge for the ariki-game project: animations are authored in iClone 8, exported as FBX into exchange/, and retargeted with tools/cc_retarget.py. Today that pipeline is entirely manual on the iClone side (Jeremy clicks Export in the UI).

Goal: establish a programmatic connection to iClone 8 so terminal agents (Claude, GLM, scripts) can drive it — load motions, query the scene, trigger FBX exports — without Jeremy clicking through the UI each time.

Why a bridge is needed: iClone 8 has an embedded Python API (RLPy module) but no external/network API of any kind. Scripts only run inside iClone, loaded either from the Script menu or as auto-loading plugins. The standard pattern (used by MotionLIVE etc.) is a plugin that runs a localhost socket server inside iClone and executes commands sent to it. That is what you will build.

Verified environment facts (checked 2026-07-21 — trust these)

  • iClone 8 install: A:\Program Files (x86)\iClone 8\ (note: A: drive, not C:).
  • Plugin auto-load folder: A:\Program Files (x86)\iClone 8\Bin64\OpenPlugin\verified writable without elevation. Currently contains only stock plugins (AIStudio, MotionLIVE, VideoMocap). Do not touch those.
  • API stub for reference: A:\Program Files (x86)\iClone 8\Bin64\RLPy.py — this file lists every RLPy class/method with docstrings. It is your API ground truth; grep it before using any RLPy call. Do not trust API names from memory or old forum posts.
  • Embedded Python: Bin64 contains BOTH python38.dll and python310.dll — do not assume which one hosts plugins. Write the plugin in 3.8-compatible syntax (no match, no X | Y unions, no 3.9+ stdlib) and report the real version via the bridge itself (sys.version) in your results.
  • Qt: iClone embeds Qt 5.15 / PySide2 (shiboken2 available in-process).
  • Client-side Python: 3.12.10 on PATH (python). Client must be stdlib-only.
  • iClone was RUNNING when this plan was written. A restart is required to load the new plugin — Jeremy must do the restart himself (he may have unsaved work). Never kill the iClone process.

Reference docs (fetch only if stuck; the RLPy.py stub usually suffices)

1. Architecture

┌─ this repo ──────────────────┐        ┌─ inside iClone 8 (Qt main thread) ─────────┐
│ tools/iclone_bridge.py       │  TCP   │ OpenPlugin\TinqsBridge\main.py             │
│  (client, py3.12, stdlib)    │◄──────►│  socket thread: accept + read/write ONLY   │
│                              │ 127.0. │  queue → QTimer(50ms, main thread) drains, │
│ tools/iclone_bridge/         │ 0.1:   │  exec()s code with RLPy in namespace,      │
│  TinqsBridge/main.py (source)│ 18800  │  posts JSON reply back to socket thread    │
│ tools/install_iclone_bridge  │        │                                            │
│  .ps1 (copies to OpenPlugin) │        └────────────────────────────────────────────┘
└──────────────────────────────┘

Non-negotiable threading rule: RLPy is not thread-safe and must only be called on iClone's Qt main thread. The socket thread NEVER touches RLPy. It pushes requests onto a queue.Queue; a PySide2 QTimer created in initialize_plugin() (which runs on the main thread) fires every ~50 ms, drains the queue, executes, and hands the response back (a second queue or per-request threading.Event — your choice).

2. Wire protocol (keep it this simple)

Newline-delimited JSON over TCP, localhost only, one request/response pair at a time.

  • Request: {"id": 1, "code": "<python source>"}
  • Success: {"id": 1, "ok": true, "result": <json>, "stdout": "<captured prints>"}
  • Failure: {"id": 1, "ok": false, "error": "<full traceback>", "stdout": "..."}

Execution semantics:

  • exec(code, ns, ns) — one persistent namespace dict ns shared across all requests for the life of the iClone session, pre-seeded with RLPy imported. Same dict as globals AND locals (avoids the exec-scoping trap where nested functions can't see top-level names).
  • If the code sets a variable named result, that is the response result (JSON-serialize; on TypeError fall back to repr()). Otherwise result is null. Clear ns["result"] before each exec so stale values don't leak.
  • Capture stdout/stderr during exec (contextlib.redirect_stdout/stderr) into stdout.
  • Any exception → ok:false with traceback.format_exc(); the bridge itself must survive and serve the next request.

3. Deliverables

  1. tools/iclone_bridge/TinqsBridge/main.py — the plugin (source of truth lives in the repo; the installer copies it out). Requirements:
    • initialize_plugin() (mandatory — iClone refuses to load the plugin without it). Wrap its entire body in try/except that writes traceback.format_exc() to the log file — plugin load failures are otherwise near-silent.
    • TCP server on 127.0.0.1:18800, SO_REUSEADDR, port overridable via env var TINQS_ICLONE_BRIDGE_PORT read inside iClone. If the bind fails, log it clearly and return without crashing the plugin loader.
    • Socket-accept loop on a daemon=True thread; QTimer executor per §1. Keep module-level references to the timer and thread (a GC'd QTimer silently stops).
    • Log to %TEMP%\tinqs_iclone_bridge.log (append, timestamped lines: startup, bind result, each request id + ok/error, shutdown).
    • Optional nice-to-have, skip if it costs you more than ~20 min: a Plugins ▸ Tinqs Bridge menu entry showing status (port, requests served) via RLPy.RUi.AddMenu + shiboken2.wrapInstance (see the GitHub samples). The log file is the required status surface; the menu is garnish.
  2. tools/iclone_bridge.py — client, Python 3.12, stdlib only. API: run(code, port=18800, timeout=30.0) -> dict and ping(port, timeout) -> dict. CLI (match the repo's argparse style):
    python tools/iclone_bridge.py --ping
    python tools/iclone_bridge.py --exec "result = 1 + 1"
    python tools/iclone_bridge.py --file some_script.py
    python tools/iclone_bridge.py --port 18801 --timeout 120 --exec "..."
    
    --ping sends a snippet returning {"product": ..., "version": ..., "python": sys.version} — find the real product/version getters by grepping RLPy.py (look at RApplication and RGlobal; do not guess). Exit 0 on ok:true, 1 on ok:false or connection failure, with a readable message either way (mention "is iClone running? was it restarted after install?" on connection-refused).
  3. tools/install_iclone_bridge.ps1 — copies tools/iclone_bridge/TinqsBridge/A:\Program Files (x86)\iClone 8\Bin64\OpenPlugin\TinqsBridge\ (overwrite), prints what it did and reminds that iClone must be restarted. Idempotent.
  4. docs/iclone-bridge.md — short usage doc: what it is, install/update procedure, CLI examples, the result convention, the persistent-namespace behavior, the log file location, the threading rule for anyone extending the plugin, and the "long-running code freezes the iClone UI" warning.
  5. Results report plans/iclone-api-bridge-results-2026-07-21.md: actual embedded Python version discovered, RLPy calls used for ping, smoke-test transcript (§5), any deviations from this plan with reasons, and known limitations.

4. Implementation order

  1. Grep A:\Program Files (x86)\iClone 8\Bin64\RLPy.py for the calls you need (RApplication, RGlobal, RScene, RFileIO) and note exact signatures.
  2. Write plugin + client + installer.
  3. Run the installer. Verify the files landed in OpenPlugin.
  4. Stop and ask Jeremy to save his iClone work and restart iClone. Do not proceed to smoke tests until he confirms. Never kill or restart the process yourself.
  5. Run smoke tests (§5). If the plugin didn't load: check the log file first, then iClone's Script ▸ Console Log window (ask Jeremy to read it out if needed).
  6. Write docs + results report.

5. Acceptance criteria (all must pass, transcript goes in the results report)

  1. python tools/iclone_bridge.py --ping → ok, prints product name, iClone version, and embedded Python version.
  2. Scene query: --exec a snippet listing scene object names via RScene (e.g. avatars + props; exact API from the stub) returns a JSON list without error.
  3. Namespace persistence: --exec "x = 41" then --exec "result = x + 1"42.
  4. Error resilience: --exec "1/0"ok:false with a ZeroDivisionError traceback, AND a follow-up --ping still succeeds.
  5. stdout capture: --exec "print('hi'); result = True"stdout contains hi.
  6. Bridge survives ≥ 2 sequential client connections (each CLI call is a new connection).

6. Guardrails — do NOT

  • Do not kill, restart, or send input to the running iClone process. Restarts are Jeremy's job (step 4.4).
  • Do not modify anything in OpenPlugin\ other than creating/updating TinqsBridge\.
  • Do not modify existing repo tools (cc_retarget.py, loop_qc.py, loop_fix.py, dance_profile.py, etc.), anything in .claude/, docs/ (except the new docs/iclone-bridge.md), or exchange/.
  • Do not bind to anything other than 127.0.0.1. This is an arbitrary-code-execution endpoint by design; localhost-only is the security model.
  • Do not install any packages — plugin uses iClone's embedded stdlib + PySide2; client uses Python 3.12 stdlib.
  • Do not commit or push. Leave everything in the working tree for Jeremy's review.

7. Known traps

  • RLPy off the main thread crashes or corrupts iClone. The QTimer pattern in §1 is mandatory; the socket thread only moves bytes.
  • QTimer lifetime: keep a module-level reference; also create it in initialize_plugin() (main thread), never in the socket thread.
  • initialize_plugin() missing, misnamed, or raising → plugin silently fails to load. Hence the try/except-to-logfile around everything.
  • exec scoping: pass the same dict as globals and locals, or nested functions/comprehensions in user code will throw NameError on top-level names.
  • Commands run synchronously on the Qt main thread → iClone's UI freezes for the duration. Fine for seconds-long operations (an FBX export blocks anyway); document it.
  • JSON can't serialize RLPy objects — always try/except the dumps and fall back to repr.
  • Stale port from a previous iClone instance: SO_REUSEADDR + a clear log line beats a cryptic bind exception.
  • Windows Firewall shouldn't prompt for a 127.0.0.1 bind, but if a prompt appears Jeremy should allow it (note in docs).
  • Socket recv: requests may arrive fragmented — read until \n, decode UTF-8.
  • Both python38.dll and python310.dll exist in Bin64 — write for 3.8, verify via ping.
  • Old wiki pages mix iClone 7 and 8 APIs; several IC7 calls were renamed/removed. The local RLPy.py stub outranks every web source.
  • After the bridge is up once, you can iterate on executor behavior by exec-ing new code through the bridge itself (hot-swap) — but changes to main.py on disk still need an iClone restart to load. Batch your plugin edits to minimize restart requests.