All notable changes to DazScript Server are documented here.
Async script submissions may name a per-job reportFile. The server truncates
it at submission, incrementally ingests its JSONL events while Daz's main thread
is occupied, and exposes one observation record through status and result:
structured progress, the most recent 100 log entries with truncation metadata,
and a deduplicated output manifest. Request lists carry a bounded observation
summary. Both DazClient and AsyncDazClient accept report_file= for inline
and file-backed async jobs. Live report ingestion is available on Studio 6;
Studio 4 parses the final report on the main thread after execution returns.
Batch gains add_operation()/add_prelude() with configurable size
limits, plus DazElement.snapshot()/set_properties(),
DazNode.set_transform(), DazSkeleton.set_state(), a single-call
zero_figure() default path, and one-request async batch submission via
execute_batch_async(). Main-thread wait time is now exposed in
GET /metrics. See docs/api/batch.rst and docs/quickstart.rst for
usage and the whole-batch failure/no-rollback semantics.
DazScene.find_skeleton() now retries Scene.getSkeletonList() on a
transient miss instead of immediately raising NodeNotFoundError -- the
lookup has been observed to momentarily omit a skeleton that is actually
present, under load from a burst of other main-thread script calls.
POST /execute/async now implements the scriptFile form already described
by the API schema. File-backed requests keep the path in the queue and call
DzScript::loadFromFile() when execution starts, preserving
getScriptFileName() and relative include() behavior instead of flattening
the file to inline source. DazClient and AsyncDazClient expose this as
execute_file_async_submit(); the existing status/result/list/cancel methods
manage the returned request id.
On Studio 6, the result-capturing evaluate() wrapper now forwards the
request's explicit filename; Studio otherwise treats the wrapper as anonymous
source and returns an empty getScriptFileName(). Script enqueue
handlers also submit directly from the HTTP worker into the mutex-protected
queue on Studio 6, so a new async request returns while Daz's main thread is
occupied and can be cancelled before it starts. Studio 4 retains main-thread
submission because its JSON fallback uses QScriptEngine. UI logging remains
queued to the main thread.
- dazpy.cinematics — CinematicStaticShot, OrbitCamera, FrameSubject,
CinematicAnimatedShot — domain-level camera-shot builders on top of
DazCamera/DazScene.apply_static_shot()places/aims/configures a camera in one call (position,look_attarget or explicit rotation, focal length, depth of field, aspect/pixel dimensions).apply_orbit_camera()sweeps a camera around a target across a frame range by baking a per-frame position/aim on every timeline frame (widening the scene's animation range as needed soScene.setFrame()doesn't clamp past frame 30 on a fresh scene).apply_frame_subject()frames a subject at a named shot distance (close_up/medium/full_body).apply_animated_shot()(CinematicAnimatedShot) is the interpolated-keyframe counterpart toOrbitCamera— it writes real DAZ Studio keyframes at eachCameraKeyframewaypoint (set_position_at_frame()/set_rotation_at_frame()) and lets DAZ Studio interpolate between them via its own animation curves, instead of a per-frame bake. - dazpy.lighting — ThreePointLightSetup, HDRIEnvironment —
apply_three_point_light_setup()creates and places a conventional key/fill/rim light rig around a target (Vec3orDazNode), via angle/distance spherical placement or explicit world-space positions per light.apply_hdri_environment()(HDRIEnvironment) applies image-based dome lighting — environment map, intensity, dome rotation, dome/scene/both mode, dome visibility, and IBL sampling resolution — validating the image path exists before any DazScript call (an invalid path passed to the underlyingsetMap()can hang or crash DAZ Studio via a blocking file-not-found dialog) and verifying a post-apply "Environment Intensity" readback so a silent no-op on an unexpected DAZ Studio build surfaces as aRenderErrorinstead of failing invisibly. - dazpy.poses — apply_pose, reset_transforms, zero_figure —
apply_pose()applies aDazPose(or loads one from a JSON path first) to a skeleton in one call.reset_transforms()zeroes a node's local position/rotation and resets scale to 1.0.zero_figure()drives every bone rotation and morph on a skeleton to zero without touching the figure's root transform by default; an opt-ininclude_props=Truealso zeroes node-level numeric properties viaDazPose.apply_full()for rigs that route transforms through built-in dials, at the cost of that root-transform guarantee. - dazpy.math3.AxisRemap — a generic signed-axis-permutation converter
for
Vec3/Quat/BoundingBoxbetween coordinate conventions (e.g. DAZ Studio's Y-up to Blender/glTF-style Z-up), with a ready-madeY_UP_TO_Z_UPpreset. Reflective remaps (determinant-1) are supported for vectors/bounding boxes but correctly rejected forapply_quat(), since a reflection has no quaternion representation. - DazSkeleton.bone_rotations_quat() — returns local-space quaternion
rotations for every bone in one HTTP round-trip (
{bone: {x, y, z, w}}), as an alternative tobone_rotations()for callers who will compose the result with another rotation (e.g. viaAxisRemap.apply_quat()) — this avoids the per-bone Euler rotation-order ambiguity thatDazBone.rotation_orderwould otherwise require tracking. - dazpy.DazCamera — aperture / bokeh controls —
f_stop(DOF blur intensity, backed by the "F/Stop" property),aperture_blades(bokeh blade count;0= circular, 3+ = polygonal), andaperture_blade_rotation(polygon rotation angle), completing the last item split off from #20. Property labels confirmed against a live DAZ Studio instance. (GH #25) - dazpy.materials — IrayMaterial, TextureMap, SurfaceProperty —
declarative Iray Uber Base material setup built on
DazMaterial:apply_iray_material()sets base color, metallic, roughness, glossy reflectivity, cutout opacity, bump, and top coat weight, plus texture-slot assignment (apply_texture_map(), with file-existence validation before any DazScript call) and generic named-channel get/set (get_surface_property()/set_surface_property()) for channels not covered by the typed fields. (GH #31) - Frame-level progress streaming for animation renders — the
GET /render/:id/progressSSE stream now emits a progress event at each frame boundary during an animation render, instead of only start/terminal events.buildAnimationRenderScript()prints a[DAZPY_FRAME] n/totalmarker right before each frame'sdoRender()call;DzScriptServerPane::onMessagePosted()intercepts it (only while a render is actually running) and reports it viaRenderProgressBroker:: notifyProgress()aspercent = 100 * (frame-1) / total. The DAZ SDK exposes norenderProgress(int)signal and Iray posts no parseable intra-frame progress text through the debug-message channel, so this print-marker is the only real progress source available — single-frame renders still only get the 0% start / terminal finish events. (GH #31)
New dazpy.aio module with AsyncDazClient, an httpx.AsyncClient-backed
mirror of DazClient's full method surface (execute, execute_file,
async submit/status/result/list/cancel, render submit/batch/animation, USD
export, server-health endpoints) as async def methods, including
asyncio-native retry_on_busy/max_wait backoff and an async context
manager (async with AsyncDazClient() as client:) for connection-pool
lifecycle. Requires the optional httpx dependency (pip install dazpy[aio]). Removes the need for async callers (FastAPI, FastMCP,
ComfyUI, Temporal) to wrap synchronous DazClient calls in
asyncio.to_thread().
DazClient now uses a pooled requests.Session for connection reuse
instead of a fresh connection per call, and gained close() and context
manager support (with DazClient() as client:), mirroring
AsyncDazClient's connection-lifecycle handling.
Live testing plus code inspection (daz-script-server-aaa) found that
/scene/save-copy completely bypassed SceneSaving busy tracking when the
scene was already clean: that branch calls QFile::copy() directly and
never touches DzScene::saveScene(), so the sceneSaveStarting/sceneSaved
signals that drive busy detection never fired — any request landing during
that copy got no 503 STUDIO_BUSY fast-fail at all. (A live SSE capture
confirmed the dirty-scene branch, which does call saveScene(), fires
these signals reliably — the gap was specific to the clean-scene shortcut.)
SceneEventBroker::enterBusy()/exitBusy() are now public, and
handleSaveCopy() brackets its entire body (both branches) with an RAII
BusyScope guard, giving explicit, signal-independent busy coverage for
this handler.
doRender()'s C++ signature is bool doRender(...), but the DAZ SDK's own
docs have no "Returns:" section for it at all -- its return value is
unspecified. dazpy.DazRenderSettings.render() guessed at its meaning
(err === 0 || err === true) and was found, live, to report success: true
for a render the user cancelled mid-progress from the DAZ Studio UI. The
async render endpoints (/render/submit, /render/animation, backing
DazClient.render_submit()/render_animation_submit()) were worse --
buildRenderScript()/buildAnimationRenderScript() in
src/DzScriptServerPane.cpp didn't check the return value at all and
hardcoded success: true unconditionally.
Both now derive success from DzRenderMgr.renderFinished(bool succeeded),
the SDK's explicit, named completion signal -- already trusted elsewhere in
this codebase (SceneEventBroker.cpp) as the "guaranteed exit path" that
fires correctly across error/cancel cases. The animation loop tracks
per-frame success and reports the AND of all frames.
DzIrayPropertyHolder.findCanvasDefinition(name, true) implicitly
reassigns the Active Canvas enum property to whichever canvas was most
recently created/looked-up (confirmed against a live instance). Once any
non-Beauty canvas (Depth, MaterialID, ...) existed, doRender() would save
that canvas's pass as the primary output file instead of the true beauty
image -- producing a flat, per-material-region "clown render" in place of
the expected beauty render. Manual use via the Render Settings UI didn't
hit this because the UI's canvas list keeps Active Canvas on Beauty
unless a user deliberately changes it.
DazRenderSettings.render() now forces Active Canvas back to Beauty
(creating it if missing) immediately before doRender() whenever
renderToCanvases is on, guaranteeing the primary saved output always
matches the method's documented "render the scene" contract regardless of
what canvases were added or last touched.
QRegExp was removed from Qt6 Core in favor of the separate Qt5Compat
module, which lives under a different include path
(QtCore5Compat/qregexp.h, not QtCore/qregexp.h) — breaking the nightly
SDK6 build with a missing-header error on all three Qt6 platforms (Windows,
macOS Intel, macOS Apple Silicon). The one QRegExp use (parsing
[DAZPY_FRAME] n/total progress markers) now uses QRegularExpression
when building against SDK6, guarded by the existing DAZ_SDK_MAJOR_VERSION
pattern; SDK4 keeps QRegExp, since it targets Qt 4.8, which predates
QRegularExpression entirely.
The example scripts formerly under this repo's docs/examples/ now live in
their own repo,
daz-script-server-examples,
kept up to date with the current dazpy API and organized by category with
per-example READMEs. docs/examples/ now just points there.
sprite_matrix and comfyui_enhance are the exception — this repo's own
test suite imports them directly as unit-test fixtures, so they moved to
tests/fixtures/rendering/ instead; the examples repo carries its own
documented, standalone copies of both for general use.
DazRenderSettings.render_engine_state() reads DzRenderOptions.renderType
and the active renderer class/name as separate facts, returning a schema-1
record with a normalized verified_iray / verified_non_iray /
unavailable status and field-level live-readback provenance. A retained
NVIDIA Iray active-renderer name never overrides a Viewport/OpenGL
renderType.
DazRenderSettings.set_render_engine("iray" | "viewport") is an explicit
persistent setter that selects the registered DzIrayRenderer plus
Software for Iray, or ScreenShot for Viewport, applies the change, and
returns only after exact readback. Missing renderers, mutation/apply
faults, malformed replies, and readback disagreement raise RenderError;
unknown engine names raise ValueError before dispatch.
The engine field on /render, /render/batch, and /render/animation
also accepts viewport and uses the same fail-closed mutation/readback
rules.
DAZ Studio's Qt main thread is single-threaded: if a user drives DAZ Studio
through its own UI (loading a scene, saving, clearing the scene, rendering)
at the same time the HTTP API is in use, requests that need the main thread
used to block indefinitely via Qt::BlockingQueuedConnection, with no
timeout and no signal to the caller.
/execute, /scripts/:id/execute, the async-enqueue endpoints, and the
render-submit endpoints (/render, /render/batch, /render/animation)
now check DAZ Studio's busy state first and fail fast with 503 and
error_code: "STUDIO_BUSY" (plus a Retry-After header) instead of
hanging. Busy state is tracked via SceneEventBroker's existing DzScene
signal connections (scene load/save/clear, render — start/finish pairs).
dazpy gains a DazBusyError exception hierarchy — StudioBusyError
(503) and ConcurrencyLimitError (429) — each carrying reason and
retry_after. This also fixes a pre-existing bug where 429 CONCURRENT_LIMIT_EXCEEDED responses were misreported as
ScriptRuntimeError. Opt-in retry_on_busy/max_wait keyword arguments
(capped linear backoff, off by default) are available on execute,
execute_file, execute_async_submit, render_submit,
render_batch_submit, and render_animation_submit.
Design: docs/superpowers/specs/2026-07-22-studio-busy-handling-design.md.
Plan: docs/superpowers/plans/2026-07-22-studio-busy-handling.md.
No functional changes to dazpy's behavior. 2.7.1's version bump missed
three places still reading 2.7.0: dazpy.__version__ itself (already
baked into the published 2.7.1 wheel, which is why this needed a new
version rather than a doc-only fix), openapi.yaml's info.version plus
two response examples, and the plugin's own DZSRV_VERSION_STR
(common_version.h — what /status//health actually return at
runtime). All three now correctly say 2.7.2.
No functional changes. The 2.7.0 dazpy wheel was uploaded to PyPI before
a README fix landed (see 2.7.0 below), and PyPI has no way to update an
already-published version's metadata/description — the project page stayed
stuck showing pre-2.7.0 text. Republished under a new version to fix the
displayed description; 2.7.1's dazpy source is identical to 2.7.0's.
Plugin binaries are rebuilt and retagged alongside it since this repo's
release pipeline ties both to one version number, but contain no code
changes either.
DazRenderSettingsIray Canvas support (list_canvases(),add_canvas(),remove_canvas(),canvases_enabled,canvas_output_paths()) — enumerate, create, and remove Iray Canvases (normal/depth/material-ID/etc. passes) and resolve their output file paths for a given render, closing GitHub issue #19's Canvas gap. Backed byApp.getRenderMgr().getRenderElementObjects()[1], confirmed against a live DAZ Studio instance — no server-side changes needed.DazScene.create_camera()/DazScene.create_light()— factory methods to create new camera and light nodes and add them to the scene, returningDazCamera/DazLightwrappers usable with the rest of the existing API (set_position,aim_at,set_color, etc).create_light()accepts"spot","point", or"distant".DazNode.delete()/DazNode.reparent()— remove a node from the scene entirely, or move it to a new parent in the hierarchy via theremoveNodeChild/addNodeChildpattern, optionally preserving its world-space transform (preserve_world_transform=Trueby default).DazProperty.raw_value— read/write a property's own dial value, excluding anyDzERCLinkcontroller contributions. Use this instead of.valuewhenever a save/restore round trip needs to be exact (see Fixed, below, for why.valueis unsafe for that).DazScene.overview()— one-call top-level scene snapshot (root figures, cameras, lights, open scene file, primary selection, node count).DazScene.node_hierarchy(root, max_depth=None)— one-call descendant tree rooted at a single named node (typically a figure), with atypeon every entry and an optional recursion-depth limit. Complements the existingnode_tree(), which always starts from every scene root.DazSceneState— full scene checkpoint: every skeleton's complete pose (viaDazPose, so bone-level, not just the root transform) plus camera and light transforms/key properties, captured and restored viaraw_valuethroughout so repeated capture/apply cycles are idempotent.DazCamera.lens_shift_x/.lens_shift_y— read/write properties backed byDzCamera's "Lens Shift X/Y (mm)" controls, the same values the render engine uses — needed for depth-based effects (fog, DOF-driven line weight) to match the actual render.DazScene.export_fbx()/.export_obj()— synchronous export via DAZ Studio's nativeDzFbxExporter/DzObjExporter, unlike the async job patternexport_usd_submit()uses — there's no custom pipeline behind these to poll on.RunSilentis forced on to avoid the native exporter's modal options dialog hanging the HTTP handler's thread.DazViewport.draw_style()/.set_draw_style()— controls the viewport's preview quality (Wireframe .. NVIDIA Iray) viagetUserDrawStyle()/setUserDrawStyle(), distinct fromDazRenderSettings.active_engine()below.set_draw_style()reads the style back after setting it and raisesValueErrorif it didn't take, sincesetUserDrawStyle()silently no-ops on an unrecognized label.DazRenderSettings.active_engine()/.set_active_engine()— query and switch the Render Settings "Engine" dropdown. Correctly distinguishesDzRenderOptions.renderType(which governs "viewport"/ "multi_pass_opengl") fromrenderMgr.getActiveRenderer()(which only applies in Software mode) — the two are easy to conflate since the UI presents them as one dropdown.set_active_engine()raisesRenderErrorfor a pluggable renderer name that isn't registered (e.g. the Filament plugin not installed).DazRenderSettingsIray quality/samples control —max_samples/max_time_secs/qualityproperties andset_quality_preset()(draft/preview/good/final), backed by the active Iray renderer's property holder since these aren't exposed onDzRenderOptions. PlusDazClient.list_requests(), wrapping the existingGET /requestsendpoint to list all tracked async requests by status.- Per-frame animation render submission —
POST /render/animationandDazClient.render_animation_submit(), mirroring the existing single-render pattern as one trackable async request that loops a start/end frame range, writing each frame tooutput_pathwith its{frame}token substituted (zero-padded). DazNode.fit_to()/.unfit()/.fitted_items()— clothing/prop fit-to-figure control, matching whatvangard-daz-mcp's hand-rolled fit/unfit/list-fitted scripts previously did outsidedazpy.DazProperty.get_keys()/.remove_key()/.clear_keys()— keyframe curve introspection and single-key removal on an animated property, without hand-rolled DazScript.- dForce simulation control — a
DazDForcemodifier proxy (freeze_simulation/freeze()/unfreeze(), backed byDzDForceModifier's "Freeze Simulation" property), wired intoDazNode's modifier discrimination plus a newdforce_modifiers()filter, andDazScene.run_dforce_simulation()/.is_simulating()/.clear_dforce_simulation()driven byDzSimulationMgr. Long-running simulations use the existing async execute-and-poll path. - General scene-change event stream client —
DazClient. stream_scene_events()plusdazpy/_scene_events.py(SceneEvent/watch_scene_events()/wait_for_scene_event()), mirroring the existing render-progress stream so callers can watch or block on node/light/camera/selection/scene/time/render events over the server's existingGET /scene/eventsSSE endpoint without hand-rolling SSE parsing. DazElement.numeric_properties()/.class_name—numeric_properties()returns{label: value}for every numeric property on an element in one round trip, instead of needing one round trip per property afterlist_properties().class_nameexposes the DazScript class name (e.g."DzFigure") for any element.
-
ERC-inflation fix in
DazPosedidn't cover bone rotation channels — the earlier fix that switched morphs and node properties togetRawValue()/setRawValue()left boneXRotControl/YRotControl/ZRotControlchannels on plaingetValue()/setValue(). For rigs where one bone's rotation is ERC-driven from another (e.g. auto-follow bend/twist ratios common on Genesis figures), repeated capture/apply cycles kept inflating that bone's rotation the same way the earlier fix addressed for morphs/props.capture()/apply()/apply_full()now feature-detect raw value support on bone rotation controls too. -
DazViewport.capture()left state unrestored if the viewport disappeared mid-wait — the finish script'sif (!vp) return null;guard ran before any of the overlay/selection/Tonemapper restore lines, so if the active viewport became unavailable (closed/changed) during the real wall-clockconvergence_waitsleep between the prepare and finish scripts, restoration was skipped entirely -- a state the old atomic single-script capture could never leave the viewport in. Scene-level restoration (primary selection, Tonemapper/Environment node visibility) no longer depends onvpand always runs; only the viewport-specific properties and the capture itself are skipped (returning no image) ifvpis gone. -
DazViewport.capture()couldn't restore a bone selection — the two-pass capture's primary-selection restore serialises only the selected node's name across the prepare/finish round trip and re-resolves it viaScene.findNode(), which resolves top-level scene nodes but not bones or other non-node selectable items (e.g. a bone selected via the Joint Editor), so the original selection was silently lost aftercapture(). Now also captures the owning skeleton's name for bone selections (isBoneSelectingNode()/getSkeleton()) and falls back toskeleton.findBone(name)when the directfindNode()lookup misses. -
DazPose.apply_full()didn't zero absent node properties — the node-property loop only calledsetValue/setRawValueif (v !== undefined), with no else-zero branch, unlike the bones loop (explicitelse setValue(0)) and morphs loop (_v = v !== undefined ? v : 0) right above it in the same function. This broke the documented contract thatapply_full()drives every channel absent from the pose to zero, and also affectedDazSceneState.apply()'s "clean baseline" restore, which relies onapply_full(). Now matches the bones/morphs pattern. -
DazSceneState.apply()didn't isolateapply_full()errors per skeleton — the per-skeleton try/except only wrappedscene.find_skeleton(); a followingpose.apply_full(skel)failure (transient HTTP/DazScript error, stale skeleton reference) propagated uncaught and aborted restoration of all remaining skeletons, cameras, and lights, contradicting the docstring's promise that failures are reported inerrorsrather than raising. Nowapply_full()is wrapped in its own try/except so a single skeleton's failure is recorded and the loop continues. -
build_hug_recipe()swapped far-shoulder targets between actors — with mismatched anchors (e.g.a_anchor="r_hand",b_anchor="l_hand"), actor_a's hand target used the far shoulder derived from actor_b's anchor side instead of actor_a's own, producing an anatomically wrong or self-intersecting embrace. Invisible with the defaultr_hand/r_handanchors (both sides happen to compute the same far shoulder), which is all the prior test covered. Now each actor's far shoulder is derived from its own anchor's side. -
DazRenderSettings.render()/.render_and_wait()broke truthiness callers — these switched from returningboolto aRenderOutcomedataclass with no__bool__, so existing code written against the old contract (if rs.render():, including downstream projects likevangard-daz-mcp) always evaluated truthy and silently treated failed renders as successful.RenderOutcome.__bool__now reflectssuccess, soif rs.render():behaves the same as before; use.success/.output_pathdirectly if you need the full outcome. -
DazViewport.capture(..., backdrop_color=...)raisedReferenceErrorrestoring the background — the two-pass capture's finish script referenced the JS variableprevBg, butprevBgwas only declared in the separate prepare-scriptexecute()call (a different HTTP round trip with no shared JS scope), so restoring the background always threw.prevBgis now round-tripped through Python (returned from the prepare script as plain JSON, alongsideaxesOn/selectionName/etc.) and substituted directly into the finish script. -
DazPose.capture()/.apply()/.apply_full()inflate ERC-driven properties on every round trip — node-level properties captured viagetValue()(e.g. a "Scale" dial fed by dozens ofDzERCLink-linked morphs) return the post-ERC computed total, butapply()/apply_full()wrote that total back viasetValue(), which sets the property's raw slot — so the ERC links add their contribution again on top. Each capture/apply cycle compounded the drift (observed inflating a custom character's Scale dial from 100% to 270% over a handful of cycles indaz-mcp-server's test suite, via its use ofdaz_save_pose/daz_load_pose). Now usesgetRawValue()/setRawValue()(seeDazProperty.raw_valueabove) for props and morphs, which round-trip correctly regardless of ERC links. -
/rendercamera selection — the native render endpoint calledApp.getViewportMgr()when acameraNamewas supplied, which is undefined in this DAZ Studio version and made every camera-targeted render fail immediately with aTypeError. Switched toMainWindow.getViewportMgr(), matching the pattern already used bydazpy's own render/viewport helpers. -
/renderblack output withcameraName— after the above fix, renders submitted with an explicit camera completed without error but produced a black image, because the render camera actually read bydoRender()comes fromopts.camera, not the viewport's active camera. The endpoint only updated the viewport (setActiveCamera) and never setopts.camera, so the renderer used a stale/null camera. Now sets both, matchingdazpy/_render.pyand this repo's own camera-preset scripts. Affects both/renderand/render/batch(they sharebuildRenderScript()). -
DazClient.status()/.health()/.metrics()misreported auth failures — these called the internal_get()helper directly without the 401/403 checkexecute()/render_submit()/etc. already had, so a 401 with no response body surfaced as a raw JSON-decode error instead ofAuthenticationError. -
ScriptErrordropped captured output on failure — callers building diagnostics from a failed script (e.g.daz-mcp-server'sdaz_execute) lost theprint()output that led up to the error.ScriptError/ScriptRuntimeError/ScriptSyntaxErrornow accept and surface an optionaloutputlist via.diagnostic. -
Dropped the unsupported
3delightrender engine option — DAZ Studio no longer ships 3Delight; removed it from the engine allow-list and docs so a render request naming it fails fast with a clear error instead of silently mapping to a renderer that isn't there.
- DAZ Studio 6 (Qt6) plugin — new build target (
--sdk-version 6) linking against the DS6 SDK and the matching Qt6 devkit. At runtime the plugin resolves Qt6 symbols against DAZ Studio 6's own bundled DLLs; the devkit is only needed at link time. SeeCLAUDE.mdforaqtinstall-based setup. - Multi-platform release artifacts — DS4 and DS6 plugins for Windows,
macOS Intel, and macOS Apple Silicon are now included in the standard release
(previously nightly-only). Six artifacts per release:
DazScriptServer-ds{4,6}-{windows,macos-Intel,macos-AppleSilicon}.{dll,dylib}. POST /scene/save-copy— save the current scene to a new path without changing the scene's internal filename pointer or dirty flag. UsesQFile::copy()for clean scenes (byte-identical file, zero state change) and a serialise-then-restore approach for dirty scenes. Response includes amethodfield ("copy","serialize", or"serialize+restore").DazScene.save_copy(path)— Python wrapper around the new endpoint. Seedocs/examples/fundamentals/scene_save_copy.pyfor--compare/--dry-runusage.DazViewport— new dazpy class for capturing the DAZ Studio viewport programmatically:capture(path),capture(path, backdrop_color=(R,G,B)),capture_sprite(path)(alpha-matted via u2net),get_size(),set_size(w,h),is_available().examples/capture_viewport.py— CLI for all three capture modes (raw/clean/sprite) with--mode,--output,--backdrop,--no-alpha-matting,--daz-url, and--dry-runflags.- ComfyUI enhancement pipeline (
examples/comfyui_enhance/) — end-to-end DAZ Studio → ComfyUI img2img pipeline: captures the viewport, uploads to ComfyUI, queues an SDXL img2img workflow, streams progress, and saves the enhanced result. Includes--checkpoint,--dry-run, and--no-watchflags. - Body measurements — 19 new measurements in
docs/examples/body_measurements.pyfor G8, G8.1, and G9 figures:- 9 vertical lengths: total height, inseam, torso length, arm length, thigh, shin, forearm, upper arm, head height.
- 10 circumferences: neck, chest, underbust, waist, high hip, hip, thigh, knee, calf, ankle.
- 2 breadths: shoulder width, across-back and across-chest.
- Pass
--figure-type G9F(orG8F,G9M, etc.) to force calibration when the scene label lacks a gender keyword.
- Jupyter notebook (
notebooks/dazpy_intro.ipynb) — interactive dazpy exploration against a live DAZ Studio instance. Launch with./notebook.sh(macOS/Linux) or.\notebook.ps1(Windows). Sections: Quick Connect → Scene Info → Move/Rotate Nodes → Render → API Browser → Jupyter Introspection → ipywidgets Bone Rotator → Morph/Property Explorer → Scene Tree Pretty-Printer. - PyPI publishing —
dazpyis now published to PyPI automatically on each tagged release via OIDC (no stored API keys).
- Fixed hang on server stop: listener thread is now joined before the
httplib::Serverobject is destroyed, preventing a crash/deadlock when toggling the server off while requests are in flight. - Fixed body measurement bone name mismatches for G8.1 waist/underbust anchors, G9 neck circumference anchor, and G9 shoulder-width bones.
- Fixed 3D sleeve length calculation (was using a 2D projection, underestimating sleeve length on posed figures).
- Fixed high-bust anchor placed above the bust peak rather than at the shoulder on some G9 figures.
POST /render— submit a DAZ Studio render job and receive arequest_idfor async tracking. Acceptswidth,height,output_path, and optional per-figure morph overrides (figure_morphs).GET /render/:id/progress— Server-Sent Events stream that delivers real-time render progress for a job:stage,progress(0–1),elapsed_ms, andoutput_pathon completion.POST /render/batch— submit multiple render variants in a single request. Each variant can override morphs and output path; renders execute sequentially on the DAZ Studio main thread.- Plugin route registration interface — companion plugins can register their
own HTTP routes into the running server via a
DzScriptServerPanepointer published onqApp. Supports path parameters (e.g./export/:id/status). POST /render/:id/cancel— cancel a queued or running render job by itsrequest_id. Returns 400 if the ID belongs to a non-render request, so callers get a clear error rather than silently cancelling the wrong job.dazpyrender API —render(),render_variants(),RenderVariant, andFigureMorphs— high-level Python wrappers around the new render endpoints with SSE progress streaming and result polling.RenderResult.request_id— therequest_idis now populated on the result returned byrender(..., wait=False), enabling cancellation viaclient.cancel_render(result.request_id).DazClient.cancel_render(request_id)— cancel a render job by ID.DazScene.find_camera_by_label(label)andDazScene.find_light_by_label(label)— look up a camera or light by its Scene-panel label.find_camera_by_labelreturns the same label string accepted by thecameraparameter ofrender().DazScene.undo_last()andDazScene.redo_last()— step the DAZ Studio undo stack programmatically (equivalent to Ctrl+Z / Ctrl+Y). Distinct from the existingscene.undo()context manager, which groups changes into a single step.docs/examples/vn_render_workflow.py— four visual-novel render patterns (basic, batch morphs, interleaved scene setup, multi-figure) with full inline documentation.docs/examples/README.md— index and usage guide for all example scripts.
- CRT heap mismatch crash when the catch-all route dispatcher forwarded to a
companion plugin compiled against a different MSVC runtime (
msvcr100vsucrtbase). Route data now flows throughQByteArray/QMap<QString,QString>instead ofstd::string. - Companion plugin routes were silently dropped when the route was registered after the server had already started listening.
- Data race in the catch-all dispatcher under concurrent requests.
- Increased job-completion poll timeout to 300 s for long-running export jobs.
GET /scene/events— Server-Sent Events stream that delivers real-time DAZ Studio scene-change notifications to connected clients. Supported event categories:node,skeleton,light,camera,selection,scene,time,render. Use the optional?filter=query parameter to subscribe to a subset of categories.SceneEventBroker— internal Qt class that connects to allDzScenesignals and dispatches JSON-serialized events to per-client queues. High-frequency signals (timeChanging,nodeSelectionListChanged) are debounced (150 ms / 50 ms) to prevent flooding during playback or multi-select operations.- Keepalive comments — a
:keepaliveSSE comment is sent every 3 seconds of idle time so clients can reliably detect disconnects.
dazpy._pose—DazPose— snapshot and restore full skeleton poses in one call. Supports named save slots, linear interpolation between poses, and local/world-space round-tripping.dazpy._animation—DazAnimation— read and write keyframe animation data. Covers per-bone rotation/translation tracks, timeline range queries, frame stepping, and baking sampled poses to keyframes.dazpy.math3—Vec3,Quat,BoundingBox— lightweight value types for 3-D math returned by the SDK (bone positions, bounding volumes, rotations).DazGeometry.vertex_positions_posed()andDazGeometry.vertex_positions_posed_all()— fetch world-space deformed vertex positions from the object's cached geometry pipeline (skinning + morphs already applied).docs/examples/scene_to_usd.py— export a live DAZ Studio scene to Pixar USD. Captures fully posed mesh vertices, cameras, lights, PBR materials, and strand hair asUsdGeom.BasisCurves. Optional--morphsflag writes blend shapes asUsdSkeltargets.docs/examples/bvh_import.py— parse a BVH motion-capture file and apply each frame to a DAZ skeleton, with automatic bone-name mapping.docs/examples/bvh_discover.py— inspect a loaded DAZ figure and print its bone hierarchy to help build BVH-to-DAZ bone maps.docs/examples/bvh_bone_maps.py— canonical BVH ↔ DAZ bone-name tables for Genesis 8 / Genesis 9; importable by other scripts.docs/examples/animation_mixing.py— blend two stored poses at a configurable weight and apply the result to a live figure.docs/examples/batch_operations.py— run a sequence of scene mutations (morph dials, material swaps, camera moves) as a single batched HTTP request.docs/examples/geometry_analysis.py— query mesh vertex count, bounding box, and posed vertex positions for a named figure.docs/examples/keyframe_baking.py— sample a figure's pose at every frame and write explicit rotation keyframes, replacing any procedural animation.- API docs — new Sphinx pages for
DazPose,DazAnimation,Vec3,Quat, andBoundingBox; updateddocs/api/index.rsttoctree. - Test suite —
tests_dazpy.py(unit) andtests_dazpy_integration.py(integration, requires a live DAZ Studio instance). __main__guards and--help— every script indocs/examples/now has anif __name__ == "__main__":guard and an argparse--helpentry, making the examples safe to import as modules and self-documenting at the command line.
webcam_expression_mirror.py— live webcam expression mirroring example. Streams MediaPipe face landmarks to a Genesis 9 figure's FACS morph controls at up to 10 fps. Features EMA smoothing, a live AU bar chart overlay, headless (--no-preview) mode, and automatic morph reset on exit.include/JsonStd.h— header-onlyJsonStdnamespace consolidating allstd::stringJSON helpers (escape,variantToJson,msecToIso,currentTime,qstrToStd). Eliminates five copies of identical helper functions spread acrossAsyncRequestManager,DzScriptServerPane,RequestHandlers,IPWhitelistService, andRequestValidator.
dazpy.__version__was hardcoded to"0.1.0"regardless of the installed package version. Now correctly returns"2.1.0".
v2.0 is a complete internal rewrite of the plugin. The external HTTP API is fully backward-compatible with v1.x; only the plugin internals were overhauled.
- Extracted
AuthenticationService— token load, validation, and regeneration moved out of the main pane class - Extracted
RateLimiterService— per-IP sliding-window rate limiting as a standalone, thread-safe component - Extracted
IPWhitelistService— exact-match IP filtering with mutex-protected state - Extracted
MetricsCollector— uptime, counters, and success rate in one place AsyncRequestManagersubsystem — dedicated class for the full async request lifecycle (queue, status, result, cancellation, TTL cleanup)- Request handler architecture —
RequestContext,RequestValidator, andRequestProcessorreplace the monolithic dispatch code inDzScriptServerPane - Settings management —
ServerSettings/ServerConfigcentralise all defaults, ranges, and QSettings keys; eliminates magic numbers throughout
- Active-request race condition —
m_nActiveRequestsis now atomically incremented/decremented underQMutex; concurrent HTTP threads could previously bypass the concurrency limit - DzScript memory safety —
DzScriptobjects are now destroyed on the main Qt thread viaQt::BlockingQueuedConnection; previously rawdeletecould be called from HTTP threads, violating Qt object-tree rules killRender()threading — cancel signal now routes through the main thread instead of being called directly from an HTTP thread- Signal/slot leak on error paths —
onMessagePostedconnections are always disconnected even when early-return paths fire
- 72-test Python integration suite (
tests.py) - Performance benchmark suite (
test-performance.py) with--quickmode for CI - Windows/macOS cross-compilation verified in CI
- Async execution —
POST /execute/async,POST /scripts/:id/async,GET /requests/:id/status,GET /requests/:id/result,DELETE /requests/:id,GET /requests - Long-poll support via
GET /requests/:id/result?wait=true - Automatic TTL cleanup: completed/failed/cancelled requests purged after 1 hour
- IP Whitelist — exact-match per-IP access control (HTTP 403 on block)
- Per-IP rate limiting — sliding window algorithm; configurable max requests and time window (HTTP 429 on violation)
- Script Registry —
POST /scripts/register,GET /scripts,POST /scripts/:id/execute,DELETE /scripts/:id; in-memory, session-scoped - Active request counter in UI ("Active Requests: X / Y")
- Auto-start option: start server when pane opens
- Configurable concurrent request limit, body size limit, script length limit
- All settings persisted via QSettings
- Improved error messages with actionable guidance
- JavaScript/Node.js and PowerShell client examples added
GET /metrics— total requests, success/failure counts, success rate, uptime; counters persist across restarts via QSettingsGET /health— structured health check for load balancers and uptime monitors- Request IDs (8-character UUID) included in every response and log entry
- Initial release
POST /execute— inline script (script) and file-based (scriptFile) execution with optionalargsGET /status— liveness check- Token-based authentication (
X-API-Token/Authorization: Bearer) - Token auto-generated via OS crypto APIs; stored at
~/.daz3d/dazscriptserver_token.txtwithchmod 600on Unix/macOS - Configurable host, port, and execution timeout
- Output capture (
print()→output[]array) - Request log with timestamps, IP, status, duration, and request ID
- Windows (
CryptoAPI) and macOS/Linux (/dev/urandom) secure RNG JsonBuilder— type-safe JSON construction with auto-escaping