release(camera-ai): publish 0.4.13 radial bounds fix

This commit is contained in:
KINDNICK 2026-08-16 01:33:54 +09:00
parent b65e54a7e8
commit 9cfdf3d947
13 changed files with 130 additions and 48 deletions

View File

@ -1,5 +1,14 @@
# Changelog # Changelog
## 0.4.13 - 2026-08-16
- Enforced radial distance bounds on the actual float32 camera coordinates,
preventing oblique wide-shot plateaus from drifting a few micrometres past
their authored safety annulus during publication.
- Made a final distance postcondition failure reject only that camera
candidate while preserving fail-closed handling for real generator
invariants. Rebuilt the self-contained Windows worker as 0.1.9.
## 0.4.12 - 2026-08-16 ## 0.4.12 - 2026-08-16
- Made packaged catalog freshness checks portable across Windows Git - Made packaged catalog freshness checks portable across Windows Git

View File

@ -17,7 +17,7 @@ must remain together.
In Unity Package Manager, choose **Add package from git URL** and enter: In Unity Package Manager, choose **Add package from git URL** and enter:
```text ```text
https://kindnick-git.duckdns.org/mingle/streamingle-unity-utilities.git?path=/CameraAI~#v0.1.18 https://kindnick-git.duckdns.org/mingle/streamingle-unity-utilities.git?path=/CameraAI~#v0.1.19
``` ```
The initial package download is large because the frozen Windows worker is The initial package download is large because the frozen Windows worker is
@ -26,7 +26,7 @@ it, remove the package lock entry, and add the package again.
## Reference data ## Reference data
Version 0.1.18 includes a compact, read-only `RuntimeData~` bundle with the Version 0.1.19 includes a compact, read-only `RuntimeData~` bundle with the
263 prepared reference songs, cut policy, and ranker model. An artist 263 prepared reference songs, cut policy, and ranker model. An artist
workstation does not need a separate `CW-AI` checkout or Python installation. workstation does not need a separate `CW-AI` checkout or Python installation.
If a newer access-controlled library is available, it remains an optional If a newer access-controlled library is available, it remains an optional

View File

@ -16,7 +16,7 @@ Cinemachine Track이나 기존 카메라 애니메이션은 필요하지 않으
Unity Package Manager의 `Add package from git URL`에는 다음 주소를 사용할 수 Unity Package Manager의 `Add package from git URL`에는 다음 주소를 사용할 수
있습니다. 있습니다.
`https://kindnick-git.duckdns.org/mingle/streamingle-unity-utilities.git?path=/CameraAI~#v0.1.18` `https://kindnick-git.duckdns.org/mingle/streamingle-unity-utilities.git?path=/CameraAI~#v0.1.19`
배포 패키지에는 Windows x64용 `CWCameraWorker` 폴더 전체가 포함됩니다. 배포 패키지에는 Windows x64용 `CWCameraWorker` 폴더 전체가 포함됩니다.
Python은 따로 설치하지 않아도 되지만, Git 패키지의 대용량 바이너리를 받으려면 Python은 따로 설치하지 않아도 되지만, Git 패키지의 대용량 바이너리를 받으려면

Binary file not shown.

View File

@ -1,7 +1,7 @@
{ {
"schemaVersion": "cw-camera-worker-build-identity-v1", "schemaVersion": "cw-camera-worker-build-identity-v1",
"workerVersion": "0.1.8", "workerVersion": "0.1.9",
"createdUtc": "2026-08-15T15:56:01.810396+00:00", "createdUtc": "2026-08-15T16:31:20.228140+00:00",
"python": "3.12.13", "python": "3.12.13",
"sourceRootRelative": "cwai_sources/repository", "sourceRootRelative": "cwai_sources/repository",
"sourceSha256": { "sourceSha256": {
@ -10,9 +10,9 @@
"MachineLearning/CameraDirector/camera_kinematics.py": "e94eefcb56c4ebdccbc57ae8d3650109b2c912e3642892ac13add9409db21a7f", "MachineLearning/CameraDirector/camera_kinematics.py": "e94eefcb56c4ebdccbc57ae8d3650109b2c912e3642892ac13add9409db21a7f",
"MachineLearning/CameraDirector/camera_runtime_data.py": "8ef2e84a9d79b24c3f3232b1f18da49741988765044dffc6075f6e6dd5ff001d", "MachineLearning/CameraDirector/camera_runtime_data.py": "8ef2e84a9d79b24c3f3232b1f18da49741988765044dffc6075f6e6dd5ff001d",
"MachineLearning/CameraDirector/cw_camera_cli.py": "b523fdf6820b3b203dcdef80877a6ad9576fba41d7285c790c300e1813789012", "MachineLearning/CameraDirector/cw_camera_cli.py": "b523fdf6820b3b203dcdef80877a6ad9576fba41d7285c790c300e1813789012",
"MachineLearning/CameraDirector/cw_camera_runtime.py": "5cc252ff45b2a80dffbeea14ad37013ba4a428c4f6fe31a617957eec6c823429", "MachineLearning/CameraDirector/cw_camera_runtime.py": "83968ed0b952a6ecf4673d31070deea677fff712c0dcae21878d527d0e67b7d4",
"MachineLearning/CameraDirector/data_driven_cut_planner.py": "36cf86a80577278d2e966cb0ebf7c3110b5b1dda5abd1d8a6b0a76e31d10e32b", "MachineLearning/CameraDirector/data_driven_cut_planner.py": "36cf86a80577278d2e966cb0ebf7c3110b5b1dda5abd1d8a6b0a76e31d10e32b",
"MachineLearning/CameraDirector/generate_hybrid.py": "bd37744b7b6573bf14be4f0ec5202f1465e11bff71ccabec210533e135b546c3", "MachineLearning/CameraDirector/generate_hybrid.py": "af94ea04f1a05f9248086790293d88e7cb2b9d0123d6e126ab275c87b9022760",
"MachineLearning/CameraDirector/hybrid_candidate_cache.py": "a0a5b6a8f612f18cb2875394f17ed89950e1e62e38c1226457d9e35848f6e380", "MachineLearning/CameraDirector/hybrid_candidate_cache.py": "a0a5b6a8f612f18cb2875394f17ed89950e1e62e38c1226457d9e35848f6e380",
"MachineLearning/CameraDirector/hybrid_cut_reference.py": "306e650dc193fd0d9c42fd52d3f14eef112d1a0da2a27c13b88256d42d8542aa", "MachineLearning/CameraDirector/hybrid_cut_reference.py": "306e650dc193fd0d9c42fd52d3f14eef112d1a0da2a27c13b88256d42d8542aa",
"MachineLearning/CameraDirector/hybrid_preparation_cache.py": "e4d062a48cf39e01ea97951d103a37817c860e94f440f49f080c622b4969fb5a", "MachineLearning/CameraDirector/hybrid_preparation_cache.py": "e4d062a48cf39e01ea97951d103a37817c860e94f440f49f080c622b4969fb5a",
@ -20,11 +20,11 @@
"MachineLearning/CameraDirector/planner.py": "8575788c0ff1e984f8238354dcd6de2dab9f62afeb71fa4b77ebfc7c66400372", "MachineLearning/CameraDirector/planner.py": "8575788c0ff1e984f8238354dcd6de2dab9f62afeb71fa4b77ebfc7c66400372",
"MachineLearning/CameraDirector/shot_features.py": "fd912b00320ea53ca682c010de6ed1102edcd021ceb4822a5785033a500786fe", "MachineLearning/CameraDirector/shot_features.py": "fd912b00320ea53ca682c010de6ed1102edcd021ceb4822a5785033a500786fe",
"MachineLearning/CameraDirector/train.py": "070d1c4159c56a2107ae0d383ab0b716b12c13186d45383e94b83d3ba6305d03", "MachineLearning/CameraDirector/train.py": "070d1c4159c56a2107ae0d383ab0b716b12c13186d45383e94b83d3ba6305d03",
"MachineLearning/CameraDirector/trajectory_quality.py": "20cd2ddc1b2349f8c33f6d1460580f37964a521eaa86ceee5b8ae457512ba49d" "MachineLearning/CameraDirector/trajectory_quality.py": "3d2c0a1b6eff8d629dff4211aec38b46c8fd450220f72a31e91f015cd3cc8565"
}, },
"preparationLogicIdentifier": "1cb808e207d54854267a1020a658812c79e9bad5a5296f78140228b3a15eeb3b", "preparationLogicIdentifier": "1cb808e207d54854267a1020a658812c79e9bad5a5296f78140228b3a15eeb3b",
"candidateLogicIdentifier": "e768283e8496360ff703b5135f85c51520a888787612b15456def131d5ee1d89", "candidateLogicIdentifier": "b3479b263b24f1419ee1221527878df73b6cf82addb5683753f6e26da632f640",
"generationCodeIdentifier": "80e9eeca5aed9e17a9cfb61b235f954ed4eb24308a1a1ca962e424dbb54bea88", "generationCodeIdentifier": "9d4c7b2d8192f5185ef6ee96378774ffd1365cdc9df0e4b7c7a934b199d4c66b",
"generationCodeFiles": [ "generationCodeFiles": [
"adjacent_transition.py", "adjacent_transition.py",
"camera_kinematics.py", "camera_kinematics.py",

View File

@ -19,7 +19,7 @@ from typing import Any, Iterable
BUILD_IDENTITY_SCHEMA_VERSION = "cw-camera-worker-build-identity-v1" BUILD_IDENTITY_SCHEMA_VERSION = "cw-camera-worker-build-identity-v1"
BUILD_IDENTITY_FILE = "cw_camera_worker_build_identity.json" BUILD_IDENTITY_FILE = "cw_camera_worker_build_identity.json"
WORKER_VERSION = "0.1.8" WORKER_VERSION = "0.1.9"
def is_frozen_runtime() -> bool: def is_frozen_runtime() -> bool:

View File

@ -167,7 +167,8 @@ CANDIDATE_CACHE_STAGE_VERSIONS = {
"adjacent-transition-v3-no-forced-side-alternation" "adjacent-transition-v3-no-forced-side-alternation"
), ),
"trajectoryRetargeting": ( "trajectoryRetargeting": (
"retarget-v28-speed-adaptive-final-radial-post-kinematic-audit-" "retarget-v29-float32-bound-speed-adaptive-final-radial-post-kinematic-"
"audit-"
"residual-dynamics-semantic-exact-tracking-causal-entry-minimum-jerk" "residual-dynamics-semantic-exact-tracking-causal-entry-minimum-jerk"
), ),
"compositionSafety": ( "compositionSafety": (
@ -181,7 +182,8 @@ CANDIDATE_CACHE_STAGE_VERSIONS = {
"sequenceContinuation": SEED_VARIATION_SEQUENCE_CONTEXT_POLICY, "sequenceContinuation": SEED_VARIATION_SEQUENCE_CONTEXT_POLICY,
} }
TRAJECTORY_RETARGETING_VERSION = ( TRAJECTORY_RETARGETING_VERSION = (
"c2-position-speed-adaptive-final-radial-post-kinematic-audit-v15" "c2-position-float32-bound-speed-adaptive-final-radial-post-kinematic-"
"audit-v16"
) )
TEMPLATE_MOTION_SMOOTHING_VERSION = ( TEMPLATE_MOTION_SMOOTHING_VERSION = (
"savgol61-nearest-edge-step-guard-v1" "savgol61-nearest-edge-step-guard-v1"
@ -416,6 +418,10 @@ class ShotControl:
body_follow_strength: float = DEFAULT_BODY_FOLLOW_STRENGTH body_follow_strength: float = DEFAULT_BODY_FOLLOW_STRENGTH
class CandidateRetargetSafetyRejection(RuntimeError):
"""A candidate-local publishability failure that permits trying another rank."""
@dataclass(frozen=True) @dataclass(frozen=True)
class RealizedShotState: class RealizedShotState:
template: ShotTemplate template: ShotTemplate
@ -6868,7 +6874,9 @@ def retarget_template_to_shot(
<= smooth_maximum_distance + 1e-6 <= smooth_maximum_distance + 1e-6
) )
if not final_distance_bounds_passed: if not final_distance_bounds_passed:
raise RuntimeError("Final camera distance audit violated its bounds.") raise CandidateRetargetSafetyRejection(
"Final camera distance audit violated its bounds."
)
radial_distance_clamp_metrics = { radial_distance_clamp_metrics = {
**final_radial_distance_clamp_metrics, **final_radial_distance_clamp_metrics,
"initialPass": initial_radial_distance_clamp_metrics, "initialPass": initial_radial_distance_clamp_metrics,
@ -8444,25 +8452,28 @@ def evaluate_safe_candidate_pool(
safety_distance_scale: float = 1.0, safety_distance_scale: float = 1.0,
*, *,
cache_eligible: bool, cache_eligible: bool,
) -> dict: ) -> dict | None:
candidate_result = retarget_template_to_shot( try:
candidate_template, candidate_result = retarget_template_to_shot(
previous, candidate_template,
target, previous,
joints, target,
root, joints,
start, root,
end, start,
target_aspect_ratio, end,
effective_composition, target_aspect_ratio,
body_follow_strength=control.body_follow_strength, effective_composition,
body_follow_smoothing_seconds=body_follow_smoothing_seconds, body_follow_strength=control.body_follow_strength,
body_follow_dead_zone_meters=body_follow_dead_zone_meters, body_follow_smoothing_seconds=body_follow_smoothing_seconds,
distance_meters=control.distance_meters, body_follow_dead_zone_meters=body_follow_dead_zone_meters,
motion_intensity=effective_motion_intensity, distance_meters=control.distance_meters,
safety_distance_scale=safety_distance_scale, motion_intensity=effective_motion_intensity,
front_facing_reference=front_facing_reference, safety_distance_scale=safety_distance_scale,
) front_facing_reference=front_facing_reference,
)
except CandidateRetargetSafetyRejection:
return None
candidate_result["candidateRank"] = candidate_rank candidate_result["candidateRank"] = candidate_rank
candidate_result["selectionScore"] = candidate_selection_score( candidate_result["selectionScore"] = candidate_selection_score(
candidate_result, candidate_result,
@ -8569,6 +8580,11 @@ def evaluate_safe_candidate_pool(
) )
if not seed_variation_needs_more_candidates(safe_results): if not seed_variation_needs_more_candidates(safe_results):
break break
if not candidate_results:
raise CandidateRetargetSafetyRejection(
"All bounded retarget candidates failed their final safety audit."
)
else: else:
safe_results = selectable_safe_candidate_results( safe_results = selectable_safe_candidate_results(
candidate_results, candidate_results,

View File

@ -16,7 +16,7 @@ import numpy as np
TRAJECTORY_QUALITY_VERSION = "trajectory-quality-v2" TRAJECTORY_QUALITY_VERSION = "trajectory-quality-v2"
C2_RESAMPLING_POLICY_VERSION = "clamped-cubic-c2-endpoint-speed-guard-v1" C2_RESAMPLING_POLICY_VERSION = "clamped-cubic-c2-endpoint-speed-guard-v1"
RADIAL_CLAMP_POLICY_VERSION = "quintic-bound-identity-c2-speed-adaptive-v2" RADIAL_CLAMP_POLICY_VERSION = "quintic-bound-identity-c2-speed-adaptive-v3"
DYNAMICS_POLICY_VERSION = ( DYNAMICS_POLICY_VERSION = (
"camera-motion-stabilized-screen-residual-cost-hard-gate-v3" "camera-motion-stabilized-screen-residual-cost-hard-gate-v3"
) )
@ -698,6 +698,59 @@ def smooth_radial_distance_clamp(
): ):
raise RuntimeError("smooth radial clamp failed to enforce its bounds") raise RuntimeError("smooth radial clamp failed to enforce its bounds")
# The generated camera is published as float32. An oblique vector that is
# exactly on a radial plateau in float64 can acquire a norm a few ULPs past
# that plateau after its components are rounded independently. Audit and
# repair the actual published representation, not only the temporary
# float64 curve. The inward margin is microscopic (tens of micrometres at
# the largest supported radii) while remaining comfortably larger than the
# worst three-component float32 norm error.
published_result = result.astype(np.float32)
published_radii = np.linalg.norm(
published_result.astype(np.float64),
axis=1,
)
bound_span = upper_bound - lower_bound
publication_margin = min(
bound_span * 0.25,
max(
1e-6,
16.0
* float(np.finfo(np.float32).eps)
* max(1.0, abs(lower_bound), abs(upper_bound)),
),
)
lower_publication_violation = (
(published_radii < lower_bound) if lower_bound > 0.0 else np.zeros(
len(published_radii),
dtype=bool,
)
)
upper_publication_violation = published_radii > upper_bound
if np.any(lower_publication_violation):
published_result[lower_publication_violation] = (
directions[lower_publication_violation]
* (lower_bound + publication_margin)
).astype(np.float32)
if np.any(upper_publication_violation):
published_result[upper_publication_violation] = (
directions[upper_publication_violation]
* (upper_bound - publication_margin)
).astype(np.float32)
publication_projection = (
lower_publication_violation | upper_publication_violation
)
published_radii = np.linalg.norm(
published_result.astype(np.float64),
axis=1,
)
if np.any(published_radii < lower_bound - tolerance) or np.any(
published_radii > upper_bound + tolerance
):
raise RuntimeError(
"smooth radial clamp failed to enforce float32 publication bounds"
)
metadata: dict[str, object] = { metadata: dict[str, object] = {
"version": TRAJECTORY_QUALITY_VERSION, "version": TRAJECTORY_QUALITY_VERSION,
"policyVersion": RADIAL_CLAMP_POLICY_VERSION, "policyVersion": RADIAL_CLAMP_POLICY_VERSION,
@ -724,13 +777,17 @@ def smooth_radial_distance_clamp(
"directionFallbackFrameCount": direction_fallback_count, "directionFallbackFrameCount": direction_fallback_count,
"minimumInputDistanceMeters": float(np.min(radii)), "minimumInputDistanceMeters": float(np.min(radii)),
"maximumInputDistanceMeters": float(np.max(radii)), "maximumInputDistanceMeters": float(np.max(radii)),
"minimumOutputDistanceMeters": float(np.min(output_radii)), "minimumOutputDistanceMeters": float(np.min(published_radii)),
"maximumOutputDistanceMeters": float(np.max(output_radii)), "maximumOutputDistanceMeters": float(np.max(published_radii)),
"float32BoundaryProjectionFrameCount": int(
np.count_nonzero(publication_projection)
),
"float32BoundaryProjectionMarginMeters": publication_margin,
"maximumRadialAdjustmentMeters": float( "maximumRadialAdjustmentMeters": float(
np.max(np.abs(mapped_radii - radii)) np.max(np.abs(published_radii - radii))
), ),
} }
return result.astype(np.float32), metadata return published_result, metadata
def _persistent_direction_reversals( def _persistent_direction_reversals(

View File

@ -1,4 +1,4 @@
../../Scripts/wheel.exe,sha256=9jTRWXNV9efL3HxLn6B_Q7zW31wwIW3r808tmvMnVhQ,108448 ../../Scripts/wheel.exe,sha256=33fhzLGTaJQ0McB6oI5dG_iQAgUgrifXlD5WqsCV_T4,108448
wheel-0.45.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 wheel-0.45.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
wheel-0.45.1.dist-info/LICENSE.txt,sha256=MMI2GGeRCPPo6h0qZYx8pBe9_IkcmO8aifpP8MmChlQ,1107 wheel-0.45.1.dist-info/LICENSE.txt,sha256=MMI2GGeRCPPo6h0qZYx8pBe9_IkcmO8aifpP8MmChlQ,1107
wheel-0.45.1.dist-info/METADATA,sha256=mKz84H7m7jsxJyzeIcTVORiTb0NPMV39KvOIYhGgmjA,2313 wheel-0.45.1.dist-info/METADATA,sha256=mKz84H7m7jsxJyzeIcTVORiTb0NPMV39KvOIYhGgmjA,2313

View File

@ -1,7 +1,7 @@
{ {
"schemaVersion": "cw-camera-worker-distribution-build-v1", "schemaVersion": "cw-camera-worker-distribution-build-v1",
"createdUtc": "2026-08-15T15:57:09.6006631Z", "createdUtc": "2026-08-15T16:32:26.2657601Z",
"workerVersion": "0.1.8", "workerVersion": "0.1.9",
"protocolVersion": "1", "protocolVersion": "1",
"buildIdentitySchemaVersion": "cw-camera-worker-build-identity-v1", "buildIdentitySchemaVersion": "cw-camera-worker-build-identity-v1",
"buildEnvironment": { "buildEnvironment": {
@ -65,9 +65,9 @@
], ],
"payload": { "payload": {
"fileCount": 646, "fileCount": 646,
"bytes": 267778040, "bytes": 267781759,
"executableSha256": "559fb67e5dee12cedb0daf1a1f900460fbcf3ff51ece0e826da6ff353506d2ac", "executableSha256": "7cdbaaba556b54215c77f2c64b4005df55e94e8a7feaeff06def5cd6e9449f21",
"buildIdentitySha256": "fe1e6e3fd8b22a4874bcf8dc5a93528f6e6aa359b42a6438bc0aba932f18a3bc", "buildIdentitySha256": "de655829a299d6ec264217ae6c4a2ab2ec89821e4cffed8499c23b79caece1da",
"sanityRange": { "sanityRange": {
"minimumFileCount": 500, "minimumFileCount": 500,
"maximumFileCount": 1000, "maximumFileCount": 1000,

View File

@ -1,6 +1,6 @@
{ {
"name": "com.mingle.cw-ai", "name": "com.mingle.cw-ai",
"version": "0.4.12", "version": "0.4.13",
"displayName": "Mingle Camera Work AI", "displayName": "Mingle Camera Work AI",
"description": "Self-contained high-quality Unity Timeline camera generation with an embedded prepared reference library, per-shot editable clips, and A/B review tools.", "description": "Self-contained high-quality Unity Timeline camera generation with an embedded prepared reference library, per-shot editable clips, and A/B review tools.",
"unity": "6000.0", "unity": "6000.0",

View File

@ -37,7 +37,7 @@ Install **Git and Git LFS before opening Unity**, then use **Add package from
git URL** with: git URL** with:
```text ```text
https://kindnick-git.duckdns.org/mingle/streamingle-unity-utilities.git?path=/CameraAI~#v0.1.18 https://kindnick-git.duckdns.org/mingle/streamingle-unity-utilities.git?path=/CameraAI~#v0.1.19
``` ```
The Camera AI package includes the complete Windows x64 The Camera AI package includes the complete Windows x64