dazpy 2.8.0__tar.gz → 2.9.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. {dazpy-2.8.0/dazpy.egg-info → dazpy-2.9.3}/PKG-INFO +202 -13
  2. dazpy-2.8.0/PKG-INFO → dazpy-2.9.3/README.md +199 -27
  3. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/__init__.py +62 -3
  4. dazpy-2.9.3/dazpy/_batch.py +219 -0
  5. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/_camera.py +46 -0
  6. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/_client.py +81 -12
  7. dazpy-2.9.3/dazpy/_client_aio.py +447 -0
  8. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/_element.py +46 -2
  9. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/_light.py +2 -2
  10. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/_node.py +139 -1
  11. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/_pose.py +30 -4
  12. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/_property.py +26 -11
  13. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/_render.py +384 -6
  14. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/_render_api.py +23 -4
  15. dazpy-2.9.3/dazpy/_render_api_aio.py +150 -0
  16. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/_scene.py +24 -3
  17. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/_scene_state.py +167 -10
  18. dazpy-2.9.3/dazpy/_shot_geometry.py +81 -0
  19. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/_skeleton.py +131 -0
  20. dazpy-2.9.3/dazpy/aio.py +20 -0
  21. dazpy-2.9.3/dazpy/cinematics.py +423 -0
  22. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/exceptions.py +25 -0
  23. dazpy-2.9.3/dazpy/lighting.py +227 -0
  24. dazpy-2.9.3/dazpy/materials.py +270 -0
  25. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/math3.py +167 -0
  26. dazpy-2.9.3/dazpy/poses.py +88 -0
  27. dazpy-2.8.0/README.md → dazpy-2.9.3/dazpy.egg-info/PKG-INFO +216 -12
  28. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy.egg-info/SOURCES.txt +20 -0
  29. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy.egg-info/requires.txt +3 -0
  30. {dazpy-2.8.0 → dazpy-2.9.3}/pyproject.toml +4 -1
  31. {dazpy-2.8.0 → dazpy-2.9.3}/tests/test_api.py +274 -1
  32. {dazpy-2.8.0 → dazpy-2.9.3}/tests/test_comfyui_client.py +38 -2
  33. {dazpy-2.8.0 → dazpy-2.9.3}/tests/test_comfyui_enhance_integration.py +7 -1
  34. {dazpy-2.8.0 → dazpy-2.9.3}/tests/test_dazpy.py +2657 -113
  35. dazpy-2.9.3/tests/test_dazpy_aio.py +399 -0
  36. {dazpy-2.8.0 → dazpy-2.9.3}/tests/test_file_watcher.py +2 -1
  37. dazpy-2.9.3/tests/test_math3.py +183 -0
  38. dazpy-2.9.3/tests/test_render_api.py +77 -0
  39. dazpy-2.9.3/tests/test_sprite_matrix_canvas_convert.py +110 -0
  40. dazpy-2.9.3/tests/test_sprite_matrix_integration.py +134 -0
  41. dazpy-2.9.3/tests/test_sprite_matrix_paths.py +86 -0
  42. dazpy-2.9.3/tests/test_sprite_matrix_presets.py +86 -0
  43. dazpy-2.9.3/tests/test_sprite_matrix_render_shot.py +80 -0
  44. dazpy-2.9.3/tests/test_sprite_matrix_render_util.py +73 -0
  45. dazpy-2.9.3/tests/test_sprite_matrix_schema.py +234 -0
  46. dazpy-2.9.3/tests/test_sprite_matrix_stylize_stage.py +121 -0
  47. dazpy-2.9.3/tests/test_sprite_matrix_workflow_builder.py +383 -0
  48. {dazpy-2.8.0 → dazpy-2.9.3}/tests/test_workflow_builder.py +7 -1
  49. dazpy-2.8.0/dazpy/_batch.py +0 -107
  50. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/_animation.py +0 -0
  51. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/_bone.py +0 -0
  52. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/_dforce.py +0 -0
  53. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/_geometry.py +0 -0
  54. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/_interaction.py +0 -0
  55. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/_material.py +0 -0
  56. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/_modifier.py +0 -0
  57. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/_morph.py +0 -0
  58. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/_polling.py +0 -0
  59. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/_result.py +0 -0
  60. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/_scene_events.py +0 -0
  61. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/_script_builder.py +0 -0
  62. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/_timeline.py +0 -0
  63. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/_undo.py +0 -0
  64. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/_viewport.py +0 -0
  65. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy/py.typed +0 -0
  66. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy.egg-info/dependency_links.txt +0 -0
  67. {dazpy-2.8.0 → dazpy-2.9.3}/dazpy.egg-info/top_level.txt +0 -0
  68. {dazpy-2.8.0 → dazpy-2.9.3}/setup.cfg +0 -0
  69. {dazpy-2.8.0 → dazpy-2.9.3}/tests/test_dazpy_integration.py +0 -0
  70. {dazpy-2.8.0 → dazpy-2.9.3}/tests/test_dsp_runner.py +0 -0
  71. {dazpy-2.8.0 → dazpy-2.9.3}/tests/test_performance.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dazpy
3
- Version: 2.8.0
3
+ Version: 2.9.3
4
4
  Summary: Python SDK for the DAZ Studio Script Server
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/bluemoonfoundry/daz-script-server
@@ -12,10 +12,12 @@ Requires-Dist: requests>=2.28
12
12
  Provides-Extra: docs
13
13
  Requires-Dist: sphinx>=7.0; extra == "docs"
14
14
  Requires-Dist: sphinx-rtd-theme>=2.0; extra == "docs"
15
+ Provides-Extra: aio
16
+ Requires-Dist: httpx>=0.24; extra == "aio"
15
17
 
16
18
  # DazScript Server
17
19
 
18
- **Version 2.8.0** | DAZ Studio 4.5+ | DAZ Studio 6.25+ | Windows & macOS
20
+ **Version 2.9.3** | DAZ Studio 4.5+ | DAZ Studio 6.25+ | Windows & macOS
19
21
 
20
22
  [![Docs](https://img.shields.io/badge/docs-dazpy%20SDK-blue)](https://bluemoonfoundry.github.io/daz-script-server/)
21
23
  [![HTTP API](https://img.shields.io/badge/docs-HTTP%20API%20reference-blue)](https://bluemoonfoundry.github.io/daz-script-server/api-reference/)
@@ -78,11 +80,26 @@ print(response.json())
78
80
 
79
81
  ---
80
82
 
83
+ ## Examples
84
+
85
+ **Complete example collection:** [daz-script-server-examples repository](https://github.com/bluemoonfoundry/daz-script-server-examples)
86
+
87
+ Production-ready examples ranging from beginner tutorials to advanced pipeline implementations:
88
+ - **48 examples** across 9 categories
89
+ - Individual READMEs with skill level classification
90
+ - Requirements.txt per example
91
+ - Comprehensive searchable table
92
+
93
+ Categories: fundamentals, character, animation, rendering, geometry, export, ml_data, ai_vision, bvh
94
+
95
+ ---
96
+
81
97
  ## 📋 Table of Contents
82
98
 
83
99
  ### Getting Started
84
100
  - [Quick Start](#-quick-start)
85
101
  - [Why This Exists](#why-this-exists)
102
+ - [What's New in v2.9.0](#whats-new-in-v290)
86
103
  - [What's New in v2.8.0](#whats-new-in-v280)
87
104
  - [What's New in v2.7.2](#whats-new-in-v272)
88
105
  - [What's New in v2.7.1](#whats-new-in-v271)
@@ -170,6 +187,134 @@ DAZ Studio is powerful for 3D content creation, but automation is limited to man
170
187
 
171
188
  ---
172
189
 
190
+ ## What's New in v2.9.0
191
+
192
+ ### 🎬 dazpy.cinematics — camera shot builders
193
+
194
+ `dazpy.cinematics` packages common camera choreography on top of
195
+ `DazCamera`/`DazScene`: `apply_static_shot()` places/aims/configures a
196
+ camera in one call; `apply_orbit_camera()` sweeps a camera around a target
197
+ across a frame range; `apply_frame_subject()` frames a subject at a named
198
+ shot distance (`close_up` / `medium` / `full_body`); and
199
+ `apply_animated_shot()` writes real, interpolated DAZ Studio keyframes at a
200
+ sequence of waypoints instead of baking a value on every frame.
201
+
202
+ ```python
203
+ from dazpy.cinematics import OrbitCamera, apply_orbit_camera
204
+
205
+ apply_orbit_camera(scene, OrbitCamera(target=figure, radius=250, frame_start=0, frame_end=90))
206
+ ```
207
+
208
+ ### 💡 dazpy.lighting — three-point rigs and HDRI environments
209
+
210
+ `apply_three_point_light_setup()` creates and places a key/fill/rim light
211
+ rig around a target in one call, via angle/distance placement or explicit
212
+ world-space positions per light. `apply_hdri_environment()` applies
213
+ image-based dome lighting — map, intensity, rotation, dome/scene mode,
214
+ visibility, and IBL resolution — validating the image path up front and
215
+ verifying a post-apply readback so a silent failure never passes as success.
216
+
217
+ ### 🧍 dazpy.poses — apply_pose, reset_transforms, zero_figure
218
+
219
+ Common pose operations without hand-assembling a `DazPose`:
220
+ `apply_pose()` applies a pose object or a JSON file path directly to a
221
+ skeleton; `reset_transforms()` zeroes a node's local position/rotation and
222
+ resets scale to 1.0; `zero_figure()` drives every bone rotation and morph
223
+ to zero while leaving the figure's root transform untouched by default.
224
+
225
+ ### 📐 dazpy.math3.AxisRemap — coordinate-space conversion
226
+
227
+ A generic signed-axis-permutation converter for `Vec3`/`Quat`/
228
+ `BoundingBox` between coordinate conventions, with a ready-made
229
+ `Y_UP_TO_Z_UP` preset for moving data from DAZ Studio (Y-up) into
230
+ Blender/glTF-style Z-up pipelines.
231
+
232
+ ```python
233
+ from dazpy.math3 import Y_UP_TO_Z_UP
234
+
235
+ blender_pos = Y_UP_TO_Z_UP.apply_vec3(daz_pos)
236
+ ```
237
+
238
+ ### 🦴 DazSkeleton.bone_rotations_quat()
239
+
240
+ Returns local-space quaternion rotations for every bone in one HTTP
241
+ round-trip — the composable counterpart to `bone_rotations()` for callers
242
+ feeding the result into another rotation (e.g. `AxisRemap.apply_quat()`),
243
+ sidestepping the per-bone Euler rotation-order tracking that would
244
+ otherwise be required.
245
+
246
+ ### 🎞️ Frame-level progress streaming for animation renders
247
+
248
+ The `GET /render/:id/progress` SSE stream now emits a progress event at
249
+ each frame boundary during an animation render, instead of only start/
250
+ terminal events — the DAZ SDK exposes no per-percent render progress
251
+ signal, so this frame-level marker is the most granular progress available.
252
+
253
+ ### ⚡ Async client — `dazpy.aio.AsyncDazClient`
254
+
255
+ Async frameworks (FastAPI, FastMCP, ComfyUI custom nodes, asyncio/Temporal
256
+ workflows) previously had to wrap every `DazClient` call in
257
+ `asyncio.to_thread()` or hand-roll an `httpx.AsyncClient` wrapper.
258
+ `dazpy.aio.AsyncDazClient` mirrors `DazClient`'s full method surface —
259
+ `execute`, `execute_file`, inline/file async submit/status/result/list/cancel, render
260
+ submit/batch/animation, USD export, and server-health endpoints — as
261
+ native `async def` methods backed by `httpx.AsyncClient`, including the
262
+ same `retry_on_busy`/`max_wait` backoff semantics (via `asyncio.sleep`
263
+ instead of blocking `time.sleep`).
264
+
265
+ ```python
266
+ from dazpy.aio import AsyncDazClient
267
+
268
+ async def main():
269
+ async with AsyncDazClient() as client:
270
+ request_id = await client.execute_file_async_submit("C:/jobs/pose-probe.dsa")
271
+ result = await client.get_request_result(request_id, wait=True)
272
+ print(result)
273
+ ```
274
+
275
+ Requires the optional `httpx` dependency: `pip install dazpy[aio]`.
276
+
277
+ ### `DazClient` connection pooling + `close()`
278
+
279
+ `DazClient` now issues requests through a pooled `requests.Session` instead
280
+ of a fresh connection per call, and supports `close()` / use as a context
281
+ manager (`with DazClient() as client:`) for symmetry with the new async
282
+ client's connection lifecycle.
283
+
284
+ ### Truthful Iray/Viewport render-engine selector
285
+
286
+ `DazRenderSettings.render_engine_state()` now reads the effective
287
+ `DzRenderOptions.renderType` value and the active renderer class/name as separate
288
+ facts. Its schema-1 record includes the raw enum value/name, normalized
289
+ `verified_iray` / `verified_non_iray` / `unavailable` status, and field-level
290
+ live-readback provenance. A retained `NVIDIA Iray` active-renderer name never
291
+ overrides a Viewport/OpenGL `renderType`.
292
+
293
+ `DazRenderSettings.set_render_engine("iray" | "viewport")` is an explicit
294
+ persistent setter. It selects the registered `DzIrayRenderer` plus `Software` for
295
+ Iray, or `ScreenShot` for Viewport, calls `applyChanges()`, and returns only after
296
+ exact readback. Missing renderers, mutation/apply faults, malformed replies, and
297
+ readback disagreement raise `RenderError`; unknown engine names raise `ValueError`
298
+ before dispatch.
299
+
300
+ ```python
301
+ settings = DazRenderSettings(client)
302
+ before = settings.render_engine_state()
303
+ change = settings.set_render_engine("iray")
304
+ assert change["readback"]["status"] == "verified_iray"
305
+ ```
306
+
307
+ The `engine` field on `/render`, `/render/batch`, and `/render/animation` also
308
+ accepts `viewport` and uses the same fail-closed mutation/readback rules. These
309
+ operations intentionally persist the requested engine. DAZ documents `ScreenShot`
310
+ and `HardwareAssisted` as Viewport/OpenGL operations and `Software` as the active
311
+ software-renderer operation in the
312
+ [DzRenderOptions reference](https://docs.daz3d.com/public/software/dazstudio/4/referenceguide/scripting/api_reference/object_index/renderoptions_dz);
313
+ renderer lookup and activation are documented by
314
+ [DzRenderMgr](https://docs.daz3d.com/public/software/dazstudio/4/referenceguide/scripting/api_reference/object_index/rendermgr_dz).
315
+
316
+ ---
317
+
173
318
  ## What's New in v2.8.0
174
319
 
175
320
  ### 🚦 Fail Fast When DAZ Studio Is Busy — `503 STUDIO_BUSY`
@@ -318,7 +463,7 @@ change. For dirty scenes it serialises via `Scene.saveScene()` and restores
318
463
  the original filename immediately. The `method` field in the response tells
319
464
  you which strategy was used.
320
465
 
321
- See `docs/examples/fundamentals/scene_save_copy.py` for a complete example
466
+ See [`fundamentals/scene_save_copy`](https://github.com/bluemoonfoundry/daz-script-server-examples/tree/main/fundamentals/scene_save_copy) in the examples repo for a complete example
322
467
  with `--compare` and `--dry-run` options.
323
468
 
324
469
  ---
@@ -370,7 +515,7 @@ snapshot = scene.scene_snapshot()
370
515
  profiles = build_rig_profiles_from_snapshot(snapshot)
371
516
  ```
372
517
 
373
- **Body measurement accuracy improvements** in `docs/examples/body_measurements.py`:
518
+ **Body measurement accuracy improvements** in [`geometry/body_measurements`](https://github.com/bluemoonfoundry/daz-script-server-examples/tree/main/geometry/body_measurements):
374
519
 
375
520
  - Slices now use centroid-weighted torso loop selection to filter arm cross-sections, correcting a ~70 cm overestimate on A-pose figures.
376
521
  - The heuristic fallback for unlabelled figures (e.g. a character named
@@ -550,11 +695,14 @@ print(anim.frame_range()) # (start, end)
550
695
  anim.bake_pose_to_keyframes(start=0, end=60)
551
696
  ```
552
697
 
553
- ### 📐 math3 — Vec3, Quat, BoundingBox
698
+ ### 📐 math3 — Vec3, Quat, BoundingBox, AxisRemap
554
699
 
555
700
  New module **`dazpy.math3`** provides lightweight value types returned throughout
556
701
  the SDK: `Vec3` (positions, translations), `Quat` (rotations), and `BoundingBox`
557
702
  (geometry bounds). All support arithmetic operators and round-trip through JSON.
703
+ `AxisRemap` converts `Vec3`/`Quat`/`BoundingBox` values between axis conventions
704
+ via signed-axis permutations (e.g. the built-in `Y_UP_TO_Z_UP` preset for
705
+ converting DAZ Studio's Y-up scene data to Z-up tools like Blender).
558
706
 
559
707
  ### 🌐 Posed Vertex Export & USD Scene Export
560
708
 
@@ -562,7 +710,7 @@ the SDK: `Vec3` (positions, translations), `Quat` (rotations), and `BoundingBox`
562
710
  positions with skinning and morphs already applied — enabling downstream export
563
711
  pipelines to read the final mesh without re-solving the rig.
564
712
 
565
- New example **`docs/examples/scene_to_usd.py`** uses this to export a full live
713
+ New example **[`export/scene_to_usd`](https://github.com/bluemoonfoundry/daz-script-server-examples/tree/main/export/scene_to_usd)** uses this to export a full live
566
714
  DAZ Studio scene to Pixar USD: posed meshes, cameras, lights, PBR materials, and
567
715
  strand-based hair as `UsdGeom.BasisCurves`. Pass `--morphs` to write blend
568
716
  shapes as `UsdSkel` targets.
@@ -600,8 +748,8 @@ Studio instance) now ship in the repo root.
600
748
 
601
749
  ### 📖 Docs & Ergonomics
602
750
 
603
- - API reference pages for `DazPose`, `DazAnimation`, `Vec3`, `Quat`, and `BoundingBox`
604
- - Every script in `docs/examples/` has an `if __name__ == "__main__":` guard and
751
+ - API reference pages for `DazPose`, `DazAnimation`, `Vec3`, `Quat`, `BoundingBox`, and `AxisRemap`
752
+ - Every script in [daz-script-server-examples](https://github.com/bluemoonfoundry/daz-script-server-examples) has an `if __name__ == "__main__":` guard and
605
753
  argparse `--help`, making all examples safe to import and self-documenting
606
754
 
607
755
  ---
@@ -654,7 +802,7 @@ See the [full migration guide](MIGRATION.md) for upgrade steps and rollback inst
654
802
 
655
803
  Long-running operations (renders, exports, batch jobs) no longer need to block the HTTP connection:
656
804
 
657
- - **`POST /execute/async`** — Submit any inline script asynchronously; returns a `request_id` immediately
805
+ - **`POST /execute/async`** — Submit inline `script` or host-side `scriptFile` work asynchronously; returns a `request_id` immediately
658
806
  - **`POST /scripts/:id/async`** — Submit a registered script asynchronously
659
807
  - **`GET /requests/:id/status`** — Poll for progress (`queued`, `running`, `completed`, `failed`, `cancelled`)
660
808
  - **`GET /requests/:id/result`** — Fetch the final result; supports `?wait=true` to long-poll until complete
@@ -665,6 +813,8 @@ Long-running operations (renders, exports, batch jobs) no longer need to block t
665
813
 
666
814
  **TTL:** Completed, failed, and cancelled requests are automatically purged after 1 hour (cleanup timer fires every 5 minutes).
667
815
 
816
+ **Stale running requests:** If a request has been `running` for more than 30 minutes without completing — e.g. a render blocked behind a DAZ Studio modal dialog — it's automatically marked `failed` so polling clients aren't left waiting forever. The underlying DAZ Studio operation may still be stuck; check the DAZ Studio window directly if this happens.
817
+
668
818
  ---
669
819
 
670
820
  ## What's New in v1.2.0
@@ -712,7 +862,7 @@ automation code without authoring DazScript by hand.
712
862
  Download the `.whl` file from the [latest release](https://github.com/bluemoonfoundry/daz-script-server/releases/latest) and install it:
713
863
 
714
864
  ```bash
715
- pip install dazpy-2.8.0-py3-none-any.whl
865
+ pip install dazpy-2.9.0-py3-none-any.whl
716
866
  ```
717
867
 
718
868
  Or install directly from the repo for development:
@@ -1636,10 +1786,36 @@ result = requests.get(f"{BASE}/requests/{req_id}/result?wait=true&timeout=300",
1636
1786
 
1637
1787
  ### `POST /execute/async`
1638
1788
 
1639
- **Purpose:** Submit an inline script for asynchronous execution
1789
+ **Purpose:** Submit an inline script or host-side script file for asynchronous execution
1640
1790
  **Authentication:** Required (if enabled)
1641
1791
 
1642
- **Request Body:** Same as `POST /execute`
1792
+ **Request Body:** Same as `POST /execute`, plus optional `reportFile` for
1793
+ structured job observation.
1794
+
1795
+ `reportFile` is an absolute host-side path to a JSONL file owned by this job.
1796
+ The server truncates it when the request is accepted. The script may then write
1797
+ one JSON object per line. The path is injected into the script's argument object
1798
+ as reserved field `getArguments()[0].__dssReportFile`, so callers do not have to
1799
+ duplicate it inside `args`:
1800
+
1801
+ ```jsonl
1802
+ {"type":"progress","value":0.25,"current":1,"total":4,"phase":"render","message":"Plate 1"}
1803
+ {"type":"log","level":"info","message":"Loaded scene"}
1804
+ {"type":"output","path":"C:/renders/run/plate-1.png","kind":"image","label":"plate"}
1805
+ ```
1806
+
1807
+ Progress events require either `value` (0–1) or `current` plus `total`. Log
1808
+ events require `message`. Output events require `path`; repeating a path updates
1809
+ that manifest entry. A `manifest` event may provide an `outputs` array. Use a
1810
+ unique report file for every job.
1811
+ `reportFile` may not be the submitted `scriptFile`, because the server owns and
1812
+ truncates the former before the job is queued.
1813
+
1814
+ DAZ Studio 6 ingests report events from polling workers, so progress, logs, and
1815
+ outputs are visible while the main thread is occupied by the script. DAZ Studio
1816
+ 4's JSON parser is main-thread-only; Studio 4 therefore ingests the final report
1817
+ after execution returns and does not expose report-driven progress while the job
1818
+ is still running.
1643
1819
 
1644
1820
  **Response (immediate):**
1645
1821
  ```json
@@ -1673,7 +1849,15 @@ result = requests.get(f"{BASE}/requests/{req_id}/result?wait=true&timeout=300",
1673
1849
  {
1674
1850
  "request_id": "a3f2b891",
1675
1851
  "status": "running",
1676
- "progress": 0.0,
1852
+ "progress": 0.25,
1853
+ "observation": {
1854
+ "progress": {"value": 0.25, "current": 1, "total": 4, "phase": "render"},
1855
+ "log_tail": [{"level": "info", "source": "script", "message": "Loaded scene"}],
1856
+ "log_total": 1,
1857
+ "log_truncated": false,
1858
+ "output_manifest": {"count": 0, "outputs": []},
1859
+ "report_file": "C:/renders/run/job.jsonl"
1860
+ },
1677
1861
  "elapsed_ms": 1240,
1678
1862
  "queue_position": 0
1679
1863
  }
@@ -1711,6 +1895,11 @@ result = requests.get(f"{BASE}/requests/{req_id}/result?wait=true&timeout=300",
1711
1895
  }
1712
1896
  ```
1713
1897
 
1898
+ Completed, failed, and cancelled results also include `observation`. The log
1899
+ tail is capped at the newest 100 entries; `log_total` and `log_truncated` make
1900
+ the loss explicit. Ordinary captured `print()` output is folded into the same
1901
+ tail at completion while remaining available in the legacy `output` field.
1902
+
1714
1903
  Returns HTTP 404 if the request ID is unknown or has been purged (TTL: 1 hour).
1715
1904
 
1716
1905
  ---
@@ -1,21 +1,6 @@
1
- Metadata-Version: 2.4
2
- Name: dazpy
3
- Version: 2.8.0
4
- Summary: Python SDK for the DAZ Studio Script Server
5
- License: MIT
6
- Project-URL: Homepage, https://github.com/bluemoonfoundry/daz-script-server
7
- Project-URL: Repository, https://github.com/bluemoonfoundry/daz-script-server
8
- Project-URL: Bug Tracker, https://github.com/bluemoonfoundry/daz-script-server/issues
9
- Requires-Python: >=3.10
10
- Description-Content-Type: text/markdown
11
- Requires-Dist: requests>=2.28
12
- Provides-Extra: docs
13
- Requires-Dist: sphinx>=7.0; extra == "docs"
14
- Requires-Dist: sphinx-rtd-theme>=2.0; extra == "docs"
15
-
16
1
  # DazScript Server
17
2
 
18
- **Version 2.8.0** | DAZ Studio 4.5+ | DAZ Studio 6.25+ | Windows & macOS
3
+ **Version 2.9.3** | DAZ Studio 4.5+ | DAZ Studio 6.25+ | Windows & macOS
19
4
 
20
5
  [![Docs](https://img.shields.io/badge/docs-dazpy%20SDK-blue)](https://bluemoonfoundry.github.io/daz-script-server/)
21
6
  [![HTTP API](https://img.shields.io/badge/docs-HTTP%20API%20reference-blue)](https://bluemoonfoundry.github.io/daz-script-server/api-reference/)
@@ -78,11 +63,26 @@ print(response.json())
78
63
 
79
64
  ---
80
65
 
66
+ ## Examples
67
+
68
+ **Complete example collection:** [daz-script-server-examples repository](https://github.com/bluemoonfoundry/daz-script-server-examples)
69
+
70
+ Production-ready examples ranging from beginner tutorials to advanced pipeline implementations:
71
+ - **48 examples** across 9 categories
72
+ - Individual READMEs with skill level classification
73
+ - Requirements.txt per example
74
+ - Comprehensive searchable table
75
+
76
+ Categories: fundamentals, character, animation, rendering, geometry, export, ml_data, ai_vision, bvh
77
+
78
+ ---
79
+
81
80
  ## 📋 Table of Contents
82
81
 
83
82
  ### Getting Started
84
83
  - [Quick Start](#-quick-start)
85
84
  - [Why This Exists](#why-this-exists)
85
+ - [What's New in v2.9.0](#whats-new-in-v290)
86
86
  - [What's New in v2.8.0](#whats-new-in-v280)
87
87
  - [What's New in v2.7.2](#whats-new-in-v272)
88
88
  - [What's New in v2.7.1](#whats-new-in-v271)
@@ -170,6 +170,134 @@ DAZ Studio is powerful for 3D content creation, but automation is limited to man
170
170
 
171
171
  ---
172
172
 
173
+ ## What's New in v2.9.0
174
+
175
+ ### 🎬 dazpy.cinematics — camera shot builders
176
+
177
+ `dazpy.cinematics` packages common camera choreography on top of
178
+ `DazCamera`/`DazScene`: `apply_static_shot()` places/aims/configures a
179
+ camera in one call; `apply_orbit_camera()` sweeps a camera around a target
180
+ across a frame range; `apply_frame_subject()` frames a subject at a named
181
+ shot distance (`close_up` / `medium` / `full_body`); and
182
+ `apply_animated_shot()` writes real, interpolated DAZ Studio keyframes at a
183
+ sequence of waypoints instead of baking a value on every frame.
184
+
185
+ ```python
186
+ from dazpy.cinematics import OrbitCamera, apply_orbit_camera
187
+
188
+ apply_orbit_camera(scene, OrbitCamera(target=figure, radius=250, frame_start=0, frame_end=90))
189
+ ```
190
+
191
+ ### 💡 dazpy.lighting — three-point rigs and HDRI environments
192
+
193
+ `apply_three_point_light_setup()` creates and places a key/fill/rim light
194
+ rig around a target in one call, via angle/distance placement or explicit
195
+ world-space positions per light. `apply_hdri_environment()` applies
196
+ image-based dome lighting — map, intensity, rotation, dome/scene mode,
197
+ visibility, and IBL resolution — validating the image path up front and
198
+ verifying a post-apply readback so a silent failure never passes as success.
199
+
200
+ ### 🧍 dazpy.poses — apply_pose, reset_transforms, zero_figure
201
+
202
+ Common pose operations without hand-assembling a `DazPose`:
203
+ `apply_pose()` applies a pose object or a JSON file path directly to a
204
+ skeleton; `reset_transforms()` zeroes a node's local position/rotation and
205
+ resets scale to 1.0; `zero_figure()` drives every bone rotation and morph
206
+ to zero while leaving the figure's root transform untouched by default.
207
+
208
+ ### 📐 dazpy.math3.AxisRemap — coordinate-space conversion
209
+
210
+ A generic signed-axis-permutation converter for `Vec3`/`Quat`/
211
+ `BoundingBox` between coordinate conventions, with a ready-made
212
+ `Y_UP_TO_Z_UP` preset for moving data from DAZ Studio (Y-up) into
213
+ Blender/glTF-style Z-up pipelines.
214
+
215
+ ```python
216
+ from dazpy.math3 import Y_UP_TO_Z_UP
217
+
218
+ blender_pos = Y_UP_TO_Z_UP.apply_vec3(daz_pos)
219
+ ```
220
+
221
+ ### 🦴 DazSkeleton.bone_rotations_quat()
222
+
223
+ Returns local-space quaternion rotations for every bone in one HTTP
224
+ round-trip — the composable counterpart to `bone_rotations()` for callers
225
+ feeding the result into another rotation (e.g. `AxisRemap.apply_quat()`),
226
+ sidestepping the per-bone Euler rotation-order tracking that would
227
+ otherwise be required.
228
+
229
+ ### 🎞️ Frame-level progress streaming for animation renders
230
+
231
+ The `GET /render/:id/progress` SSE stream now emits a progress event at
232
+ each frame boundary during an animation render, instead of only start/
233
+ terminal events — the DAZ SDK exposes no per-percent render progress
234
+ signal, so this frame-level marker is the most granular progress available.
235
+
236
+ ### ⚡ Async client — `dazpy.aio.AsyncDazClient`
237
+
238
+ Async frameworks (FastAPI, FastMCP, ComfyUI custom nodes, asyncio/Temporal
239
+ workflows) previously had to wrap every `DazClient` call in
240
+ `asyncio.to_thread()` or hand-roll an `httpx.AsyncClient` wrapper.
241
+ `dazpy.aio.AsyncDazClient` mirrors `DazClient`'s full method surface —
242
+ `execute`, `execute_file`, inline/file async submit/status/result/list/cancel, render
243
+ submit/batch/animation, USD export, and server-health endpoints — as
244
+ native `async def` methods backed by `httpx.AsyncClient`, including the
245
+ same `retry_on_busy`/`max_wait` backoff semantics (via `asyncio.sleep`
246
+ instead of blocking `time.sleep`).
247
+
248
+ ```python
249
+ from dazpy.aio import AsyncDazClient
250
+
251
+ async def main():
252
+ async with AsyncDazClient() as client:
253
+ request_id = await client.execute_file_async_submit("C:/jobs/pose-probe.dsa")
254
+ result = await client.get_request_result(request_id, wait=True)
255
+ print(result)
256
+ ```
257
+
258
+ Requires the optional `httpx` dependency: `pip install dazpy[aio]`.
259
+
260
+ ### `DazClient` connection pooling + `close()`
261
+
262
+ `DazClient` now issues requests through a pooled `requests.Session` instead
263
+ of a fresh connection per call, and supports `close()` / use as a context
264
+ manager (`with DazClient() as client:`) for symmetry with the new async
265
+ client's connection lifecycle.
266
+
267
+ ### Truthful Iray/Viewport render-engine selector
268
+
269
+ `DazRenderSettings.render_engine_state()` now reads the effective
270
+ `DzRenderOptions.renderType` value and the active renderer class/name as separate
271
+ facts. Its schema-1 record includes the raw enum value/name, normalized
272
+ `verified_iray` / `verified_non_iray` / `unavailable` status, and field-level
273
+ live-readback provenance. A retained `NVIDIA Iray` active-renderer name never
274
+ overrides a Viewport/OpenGL `renderType`.
275
+
276
+ `DazRenderSettings.set_render_engine("iray" | "viewport")` is an explicit
277
+ persistent setter. It selects the registered `DzIrayRenderer` plus `Software` for
278
+ Iray, or `ScreenShot` for Viewport, calls `applyChanges()`, and returns only after
279
+ exact readback. Missing renderers, mutation/apply faults, malformed replies, and
280
+ readback disagreement raise `RenderError`; unknown engine names raise `ValueError`
281
+ before dispatch.
282
+
283
+ ```python
284
+ settings = DazRenderSettings(client)
285
+ before = settings.render_engine_state()
286
+ change = settings.set_render_engine("iray")
287
+ assert change["readback"]["status"] == "verified_iray"
288
+ ```
289
+
290
+ The `engine` field on `/render`, `/render/batch`, and `/render/animation` also
291
+ accepts `viewport` and uses the same fail-closed mutation/readback rules. These
292
+ operations intentionally persist the requested engine. DAZ documents `ScreenShot`
293
+ and `HardwareAssisted` as Viewport/OpenGL operations and `Software` as the active
294
+ software-renderer operation in the
295
+ [DzRenderOptions reference](https://docs.daz3d.com/public/software/dazstudio/4/referenceguide/scripting/api_reference/object_index/renderoptions_dz);
296
+ renderer lookup and activation are documented by
297
+ [DzRenderMgr](https://docs.daz3d.com/public/software/dazstudio/4/referenceguide/scripting/api_reference/object_index/rendermgr_dz).
298
+
299
+ ---
300
+
173
301
  ## What's New in v2.8.0
174
302
 
175
303
  ### 🚦 Fail Fast When DAZ Studio Is Busy — `503 STUDIO_BUSY`
@@ -318,7 +446,7 @@ change. For dirty scenes it serialises via `Scene.saveScene()` and restores
318
446
  the original filename immediately. The `method` field in the response tells
319
447
  you which strategy was used.
320
448
 
321
- See `docs/examples/fundamentals/scene_save_copy.py` for a complete example
449
+ See [`fundamentals/scene_save_copy`](https://github.com/bluemoonfoundry/daz-script-server-examples/tree/main/fundamentals/scene_save_copy) in the examples repo for a complete example
322
450
  with `--compare` and `--dry-run` options.
323
451
 
324
452
  ---
@@ -370,7 +498,7 @@ snapshot = scene.scene_snapshot()
370
498
  profiles = build_rig_profiles_from_snapshot(snapshot)
371
499
  ```
372
500
 
373
- **Body measurement accuracy improvements** in `docs/examples/body_measurements.py`:
501
+ **Body measurement accuracy improvements** in [`geometry/body_measurements`](https://github.com/bluemoonfoundry/daz-script-server-examples/tree/main/geometry/body_measurements):
374
502
 
375
503
  - Slices now use centroid-weighted torso loop selection to filter arm cross-sections, correcting a ~70 cm overestimate on A-pose figures.
376
504
  - The heuristic fallback for unlabelled figures (e.g. a character named
@@ -550,11 +678,14 @@ print(anim.frame_range()) # (start, end)
550
678
  anim.bake_pose_to_keyframes(start=0, end=60)
551
679
  ```
552
680
 
553
- ### 📐 math3 — Vec3, Quat, BoundingBox
681
+ ### 📐 math3 — Vec3, Quat, BoundingBox, AxisRemap
554
682
 
555
683
  New module **`dazpy.math3`** provides lightweight value types returned throughout
556
684
  the SDK: `Vec3` (positions, translations), `Quat` (rotations), and `BoundingBox`
557
685
  (geometry bounds). All support arithmetic operators and round-trip through JSON.
686
+ `AxisRemap` converts `Vec3`/`Quat`/`BoundingBox` values between axis conventions
687
+ via signed-axis permutations (e.g. the built-in `Y_UP_TO_Z_UP` preset for
688
+ converting DAZ Studio's Y-up scene data to Z-up tools like Blender).
558
689
 
559
690
  ### 🌐 Posed Vertex Export & USD Scene Export
560
691
 
@@ -562,7 +693,7 @@ the SDK: `Vec3` (positions, translations), `Quat` (rotations), and `BoundingBox`
562
693
  positions with skinning and morphs already applied — enabling downstream export
563
694
  pipelines to read the final mesh without re-solving the rig.
564
695
 
565
- New example **`docs/examples/scene_to_usd.py`** uses this to export a full live
696
+ New example **[`export/scene_to_usd`](https://github.com/bluemoonfoundry/daz-script-server-examples/tree/main/export/scene_to_usd)** uses this to export a full live
566
697
  DAZ Studio scene to Pixar USD: posed meshes, cameras, lights, PBR materials, and
567
698
  strand-based hair as `UsdGeom.BasisCurves`. Pass `--morphs` to write blend
568
699
  shapes as `UsdSkel` targets.
@@ -600,8 +731,8 @@ Studio instance) now ship in the repo root.
600
731
 
601
732
  ### 📖 Docs & Ergonomics
602
733
 
603
- - API reference pages for `DazPose`, `DazAnimation`, `Vec3`, `Quat`, and `BoundingBox`
604
- - Every script in `docs/examples/` has an `if __name__ == "__main__":` guard and
734
+ - API reference pages for `DazPose`, `DazAnimation`, `Vec3`, `Quat`, `BoundingBox`, and `AxisRemap`
735
+ - Every script in [daz-script-server-examples](https://github.com/bluemoonfoundry/daz-script-server-examples) has an `if __name__ == "__main__":` guard and
605
736
  argparse `--help`, making all examples safe to import and self-documenting
606
737
 
607
738
  ---
@@ -654,7 +785,7 @@ See the [full migration guide](MIGRATION.md) for upgrade steps and rollback inst
654
785
 
655
786
  Long-running operations (renders, exports, batch jobs) no longer need to block the HTTP connection:
656
787
 
657
- - **`POST /execute/async`** — Submit any inline script asynchronously; returns a `request_id` immediately
788
+ - **`POST /execute/async`** — Submit inline `script` or host-side `scriptFile` work asynchronously; returns a `request_id` immediately
658
789
  - **`POST /scripts/:id/async`** — Submit a registered script asynchronously
659
790
  - **`GET /requests/:id/status`** — Poll for progress (`queued`, `running`, `completed`, `failed`, `cancelled`)
660
791
  - **`GET /requests/:id/result`** — Fetch the final result; supports `?wait=true` to long-poll until complete
@@ -665,6 +796,8 @@ Long-running operations (renders, exports, batch jobs) no longer need to block t
665
796
 
666
797
  **TTL:** Completed, failed, and cancelled requests are automatically purged after 1 hour (cleanup timer fires every 5 minutes).
667
798
 
799
+ **Stale running requests:** If a request has been `running` for more than 30 minutes without completing — e.g. a render blocked behind a DAZ Studio modal dialog — it's automatically marked `failed` so polling clients aren't left waiting forever. The underlying DAZ Studio operation may still be stuck; check the DAZ Studio window directly if this happens.
800
+
668
801
  ---
669
802
 
670
803
  ## What's New in v1.2.0
@@ -712,7 +845,7 @@ automation code without authoring DazScript by hand.
712
845
  Download the `.whl` file from the [latest release](https://github.com/bluemoonfoundry/daz-script-server/releases/latest) and install it:
713
846
 
714
847
  ```bash
715
- pip install dazpy-2.8.0-py3-none-any.whl
848
+ pip install dazpy-2.9.0-py3-none-any.whl
716
849
  ```
717
850
 
718
851
  Or install directly from the repo for development:
@@ -1636,10 +1769,36 @@ result = requests.get(f"{BASE}/requests/{req_id}/result?wait=true&timeout=300",
1636
1769
 
1637
1770
  ### `POST /execute/async`
1638
1771
 
1639
- **Purpose:** Submit an inline script for asynchronous execution
1772
+ **Purpose:** Submit an inline script or host-side script file for asynchronous execution
1640
1773
  **Authentication:** Required (if enabled)
1641
1774
 
1642
- **Request Body:** Same as `POST /execute`
1775
+ **Request Body:** Same as `POST /execute`, plus optional `reportFile` for
1776
+ structured job observation.
1777
+
1778
+ `reportFile` is an absolute host-side path to a JSONL file owned by this job.
1779
+ The server truncates it when the request is accepted. The script may then write
1780
+ one JSON object per line. The path is injected into the script's argument object
1781
+ as reserved field `getArguments()[0].__dssReportFile`, so callers do not have to
1782
+ duplicate it inside `args`:
1783
+
1784
+ ```jsonl
1785
+ {"type":"progress","value":0.25,"current":1,"total":4,"phase":"render","message":"Plate 1"}
1786
+ {"type":"log","level":"info","message":"Loaded scene"}
1787
+ {"type":"output","path":"C:/renders/run/plate-1.png","kind":"image","label":"plate"}
1788
+ ```
1789
+
1790
+ Progress events require either `value` (0–1) or `current` plus `total`. Log
1791
+ events require `message`. Output events require `path`; repeating a path updates
1792
+ that manifest entry. A `manifest` event may provide an `outputs` array. Use a
1793
+ unique report file for every job.
1794
+ `reportFile` may not be the submitted `scriptFile`, because the server owns and
1795
+ truncates the former before the job is queued.
1796
+
1797
+ DAZ Studio 6 ingests report events from polling workers, so progress, logs, and
1798
+ outputs are visible while the main thread is occupied by the script. DAZ Studio
1799
+ 4's JSON parser is main-thread-only; Studio 4 therefore ingests the final report
1800
+ after execution returns and does not expose report-driven progress while the job
1801
+ is still running.
1643
1802
 
1644
1803
  **Response (immediate):**
1645
1804
  ```json
@@ -1673,7 +1832,15 @@ result = requests.get(f"{BASE}/requests/{req_id}/result?wait=true&timeout=300",
1673
1832
  {
1674
1833
  "request_id": "a3f2b891",
1675
1834
  "status": "running",
1676
- "progress": 0.0,
1835
+ "progress": 0.25,
1836
+ "observation": {
1837
+ "progress": {"value": 0.25, "current": 1, "total": 4, "phase": "render"},
1838
+ "log_tail": [{"level": "info", "source": "script", "message": "Loaded scene"}],
1839
+ "log_total": 1,
1840
+ "log_truncated": false,
1841
+ "output_manifest": {"count": 0, "outputs": []},
1842
+ "report_file": "C:/renders/run/job.jsonl"
1843
+ },
1677
1844
  "elapsed_ms": 1240,
1678
1845
  "queue_position": 0
1679
1846
  }
@@ -1711,6 +1878,11 @@ result = requests.get(f"{BASE}/requests/{req_id}/result?wait=true&timeout=300",
1711
1878
  }
1712
1879
  ```
1713
1880
 
1881
+ Completed, failed, and cancelled results also include `observation`. The log
1882
+ tail is capped at the newest 100 entries; `log_total` and `log_truncated` make
1883
+ the loss explicit. Ordinary captured `print()` output is folded into the same
1884
+ tail at completion while remaining available in the legacy `output` field.
1885
+
1714
1886
  Returns HTTP 404 if the request ID is unknown or has been purged (TTL: 1 hour).
1715
1887
 
1716
1888
  ---