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>
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:
Bin64contains BOTHpython38.dllandpython310.dll— do not assume which one hosts plugins. Write the plugin in 3.8-compatible syntax (nomatch, noX | Yunions, 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 (
shiboken2available 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)
- Plugin structure: https://wiki.reallusion.com/IC_Python_API:Your_First_iClone_Python_Plugin
- IC8 API wiki: https://wiki.reallusion.com/IC8_Python_API
- Official samples (QTimer/menu/PySide2 patterns): https://github.com/reallusion/iClone
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 dictnsshared across all requests for the life of the iClone session, pre-seeded withRLPyimported. 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 responseresult(JSON-serialize; onTypeErrorfall back torepr()). Otherwiseresultis null. Clearns["result"]before each exec so stale values don't leak. - Capture stdout/stderr during exec (
contextlib.redirect_stdout/stderr) intostdout. - Any exception →
ok:falsewithtraceback.format_exc(); the bridge itself must survive and serve the next request.
3. Deliverables
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 writestraceback.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 varTINQS_ICLONE_BRIDGE_PORTread inside iClone. If the bind fails, log it clearly and return without crashing the plugin loader. - Socket-accept loop on a
daemon=Truethread; 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.
tools/iclone_bridge.py— client, Python 3.12, stdlib only. API:run(code, port=18800, timeout=30.0) -> dictandping(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 "..."--pingsends a snippet returning{"product": ..., "version": ..., "python": sys.version}— find the real product/version getters by greppingRLPy.py(look atRApplicationandRGlobal; 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).tools/install_iclone_bridge.ps1— copiestools/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.docs/iclone-bridge.md— short usage doc: what it is, install/update procedure, CLI examples, theresultconvention, 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.- 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
- Grep
A:\Program Files (x86)\iClone 8\Bin64\RLPy.pyfor the calls you need (RApplication,RGlobal,RScene,RFileIO) and note exact signatures. - Write plugin + client + installer.
- Run the installer. Verify the files landed in OpenPlugin.
- 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.
- 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).
- Write docs + results report.
5. Acceptance criteria (all must pass, transcript goes in the results report)
python tools/iclone_bridge.py --ping→ ok, prints product name, iClone version, and embedded Python version.- Scene query:
--execa snippet listing scene object names viaRScene(e.g. avatars + props; exact API from the stub) returns a JSON list without error. - Namespace persistence:
--exec "x = 41"then--exec "result = x + 1"→42. - Error resilience:
--exec "1/0"→ok:falsewith a ZeroDivisionError traceback, AND a follow-up--pingstill succeeds. - stdout capture:
--exec "print('hi'); result = True"→stdoutcontainshi. - 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/updatingTinqsBridge\. - 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 newdocs/iclone-bridge.md), orexchange/. - 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.pystub 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.pyon disk still need an iClone restart to load. Batch your plugin edits to minimize restart requests.