release(camera-ai): publish 0.4.11 worker bundle

This commit is contained in:
KINDNICK
2026-08-10 08:51:37 +09:00
parent b78c6319b6
commit 8e5e2e8ad0
27 changed files with 98034 additions and 230 deletions
Binary file not shown.
@@ -1,30 +1,30 @@
{
"schemaVersion": "cw-camera-worker-build-identity-v1",
"workerVersion": "0.1.6",
"createdUtc": "2026-08-09T07:15:01.579387+00:00",
"workerVersion": "0.1.7",
"createdUtc": "2026-08-09T23:32:08.208961+00:00",
"python": "3.12.13",
"sourceRootRelative": "cwai_sources/repository",
"sourceSha256": {
"MachineLearning/CameraDirector/adjacent_transition.py": "6c1b62c2996960af23d62e9c3acd3114600effda03273b006f7076c467c4bdfa",
"MachineLearning/CameraDirector/build_hybrid_preparation_cache.py": "f948b1a68352893b078ac1f6ef458df2107565378eed32890276776ca0dfdad6",
"MachineLearning/CameraDirector/build_hybrid_preparation_cache.py": "427b3cd8fa6eb444517281ef2e21ebc5d751985f4ad271eee49617fda0fb7b33",
"MachineLearning/CameraDirector/camera_kinematics.py": "e94eefcb56c4ebdccbc57ae8d3650109b2c912e3642892ac13add9409db21a7f",
"MachineLearning/CameraDirector/camera_runtime_data.py": "29fd30a1c97d384f7bb22d024a4f88178f6245858925c302b059a7a1afdeaac1",
"MachineLearning/CameraDirector/camera_runtime_data.py": "8ef2e84a9d79b24c3f3232b1f18da49741988765044dffc6075f6e6dd5ff001d",
"MachineLearning/CameraDirector/cw_camera_cli.py": "943870a4cdec7e4b830b9c945d690e700902cf2a37d1e49987bba85bf0b0ea87",
"MachineLearning/CameraDirector/cw_camera_runtime.py": "62421300af470389b5e356267d91e4dcb31626762705cd1c1d20c3fa11d271f3",
"MachineLearning/CameraDirector/cw_camera_runtime.py": "7530d24a65a55c235726676b7991b7a375a2739d8b6257f53cc0340f6cc7de1c",
"MachineLearning/CameraDirector/data_driven_cut_planner.py": "36cf86a80577278d2e966cb0ebf7c3110b5b1dda5abd1d8a6b0a76e31d10e32b",
"MachineLearning/CameraDirector/generate_hybrid.py": "b6b1e7d02d96794e57c0ae9867ad18de7c6698d7bac05da1834490e2c0f3b4e3",
"MachineLearning/CameraDirector/generate_hybrid.py": "b7e0053563065ebb6d563e72fa0221d31a9e36c1c94c37992f13974b25bb5dfd",
"MachineLearning/CameraDirector/hybrid_candidate_cache.py": "a0a5b6a8f612f18cb2875394f17ed89950e1e62e38c1226457d9e35848f6e380",
"MachineLearning/CameraDirector/hybrid_cut_reference.py": "53521d9e31d315c011892961887baf0cde6451020bf39c9d080a5072d4707fcb",
"MachineLearning/CameraDirector/hybrid_preparation_cache.py": "1b0df53124109dee2d744777979a0553999cb276cb6f538a17ddce6516e3da8b",
"MachineLearning/CameraDirector/hybrid_cut_reference.py": "306e650dc193fd0d9c42fd52d3f14eef112d1a0da2a27c13b88256d42d8542aa",
"MachineLearning/CameraDirector/hybrid_preparation_cache.py": "e4d062a48cf39e01ea97951d103a37817c860e94f440f49f080c622b4969fb5a",
"MachineLearning/CameraDirector/hybrid_single_shot_cache.py": "f540e603b0ccffb86ee003d441c3889c245a208db8fd8b2b966fdacfeea30950",
"MachineLearning/CameraDirector/planner.py": "8575788c0ff1e984f8238354dcd6de2dab9f62afeb71fa4b77ebfc7c66400372",
"MachineLearning/CameraDirector/shot_features.py": "fd912b00320ea53ca682c010de6ed1102edcd021ceb4822a5785033a500786fe",
"MachineLearning/CameraDirector/train.py": "40e9e9a9ab824d596b14f987b2977f187695c3d5a2a2bde9da67e95d38d78836",
"MachineLearning/CameraDirector/train.py": "070d1c4159c56a2107ae0d383ab0b716b12c13186d45383e94b83d3ba6305d03",
"MachineLearning/CameraDirector/trajectory_quality.py": "20cd2ddc1b2349f8c33f6d1460580f37964a521eaa86ceee5b8ae457512ba49d"
},
"preparationLogicIdentifier": "acba2a46d9d979b013b7154bada6b6a8a7adfd7acd1721876ce6699ed4e03e84",
"candidateLogicIdentifier": "bb9e883085df6a95de09f2181191c83c13cf1b3adff5b64e41b3d60d99350522",
"generationCodeIdentifier": "8973f4642cd18f5645d33871c544e5394bb538ec5db86d4abc0536f50a00a9ed",
"preparationLogicIdentifier": "1cb808e207d54854267a1020a658812c79e9bad5a5296f78140228b3a15eeb3b",
"candidateLogicIdentifier": "8dfa557f33c17f4b59849431523cef3650091dc2db3c6c48074888f1f6aef143",
"generationCodeIdentifier": "58a7d5d440e5bea630c060823fea7280d81d533e5c4741259ee974bce27e6eb4",
"generationCodeFiles": [
"adjacent_transition.py",
"camera_kinematics.py",
@@ -113,6 +113,16 @@ def run(args: argparse.Namespace) -> dict:
target_record,
args.library_split,
)
training_mask_audit = (
generate_hybrid.audio_aligned_template_mask_audit(
training_records,
entries,
)
)
planner_mask_audit = generate_hybrid.audio_aligned_template_mask_audit(
planner_records,
entries,
)
build_inputs = preparation_cache.build_inputs_for_records(
repository_root,
camera_director_root,
@@ -288,6 +298,23 @@ def run(args: argparse.Namespace) -> dict:
),
"buildTimingBeforePublish": timer.snapshot(),
}
training_mask_audit = (
generate_hybrid.finalized_audio_aligned_template_mask_audit(
training_mask_audit,
templates,
)
)
planner_mask_audit = (
generate_hybrid.finalized_audio_aligned_template_mask_audit(
planner_mask_audit,
planner_templates,
)
)
if training_mask_audit is not None:
summary["audioAlignedFrameMasking"] = {
"trainingTemplates": training_mask_audit,
"shotPlanner": planner_mask_audit,
}
with timer.stage("publish_and_verify"):
bundle = preparation_cache.publish_cache_bundle(
@@ -29,10 +29,14 @@ PIPELINE_VERSION = 1
AUDIO_RATE = 24_000
AUDIO_HOP = 400 # Exactly 60 audio feature frames per second.
MEL_BANDS = 32
REVISION_DATASET_PATTERN = re.compile(
r"^(?P<base>.+)_rev_(?P<fingerprint>[0-9a-f]{16})$"
)
GENERATION_INPUT_CACHE_IDENTITY_VERSION = "generation-input-content-v1"
GENERATION_INPUT_CACHE_FORMAT = "cw-ai-generation-input-prepared"
GENERATION_INPUT_CACHE_SCHEMA_VERSION = 1
GENERATION_INPUT_PREPARATION_LOGIC_VERSION = "generation-input-preparation-v2"
AUDIO_ALIGNED_FRAME_MASK_POLICY_VERSION = "audio-aligned-half-open-v1"
GENERATION_INPUT_VOLATILE_MANIFEST_FIELDS = frozenset(
{
"audioAssetPath",
@@ -114,13 +118,25 @@ class SongRecord:
def id(self) -> str:
scene_path = str(self.manifest.get("scenePath", ""))
folder_name = str(self.song["folderName"])
value = f"{scene_path}\0{self.name}\0{folder_name}".encode("utf-8")
value = f"{scene_path}\0{self.name}\0{folder_name}"
if self.is_revision:
value += f"\0revision:{self.dataset_id}"
value = value.encode("utf-8")
return hashlib.sha256(value).hexdigest()[:16]
@property
def dataset_id(self) -> str:
return self.dataset_dir.name
@property
def revision_of_dataset_id(self) -> str | None:
match = REVISION_DATASET_PATTERN.fullmatch(self.dataset_id)
return match.group("base") if match is not None else None
@property
def is_revision(self) -> bool:
return self.revision_of_dataset_id is not None
@property
def song_dir(self) -> Path:
return self.dataset_dir / self.song["folderName"]
@@ -307,6 +323,97 @@ def select_record(
)
def normalize_audio_aligned_frame_mask_ranges(
value: Any,
frame_count: int,
*,
label: str = "audioAlignedFrameMaskRanges",
) -> tuple[tuple[int, int], ...]:
"""Validate canonical half-open frame ranges without mutating source data.
Ranges must be strictly ordered, non-overlapping, and non-adjacent. Adjacent
ranges are rejected because their canonical representation is one merged
range. An empty list is valid and deliberately admits no frames.
"""
if isinstance(frame_count, bool) or not isinstance(frame_count, int):
raise ValueError(f"{label} frame count must be an integer.")
if frame_count < 0:
raise ValueError(f"{label} frame count must be non-negative.")
if not isinstance(value, list):
raise ValueError(f"{label} must be an array of [start, end) ranges.")
normalized: list[tuple[int, int]] = []
previous_end: int | None = None
for index, raw_range in enumerate(value):
range_label = f"{label}[{index}]"
if not isinstance(raw_range, list) or len(raw_range) != 2:
raise ValueError(f"{range_label} must be a two-item array.")
start, end = raw_range
if (
isinstance(start, bool)
or isinstance(end, bool)
or not isinstance(start, int)
or not isinstance(end, int)
):
raise ValueError(f"{range_label} bounds must be integers.")
if not 0 <= start < end <= frame_count:
raise ValueError(
f"{range_label} must satisfy 0 <= start < end <= {frame_count}."
)
if previous_end is not None and start <= previous_end:
raise ValueError(
f"{range_label} overlaps, touches, or is out of order; "
"canonical ranges must be merged and strictly separated."
)
normalized.append((start, end))
previous_end = end
return tuple(normalized)
def audio_aligned_frame_mask_range_index(
start: int,
end: int,
ranges: tuple[tuple[int, int], ...],
) -> int | None:
"""Return the single range containing ``[start, end)``, else ``None``.
A span crossing a range boundary or falling fully outside the mask is
intentionally indistinguishable to consumers: both must fail closed.
"""
if (
isinstance(start, bool)
or isinstance(end, bool)
or not isinstance(start, int)
or not isinstance(end, int)
or start < 0
or end <= start
):
raise ValueError("Frame spans must be non-empty half-open integer ranges.")
for index, (range_start, range_end) in enumerate(ranges):
if range_start <= start and end <= range_end:
return index
return None
def audio_aligned_frame_mask_span_status(
start: int,
end: int,
ranges: tuple[tuple[int, int], ...],
) -> tuple[str, int | None]:
"""Classify a source span as contained, boundary-crossing, or outside."""
range_index = audio_aligned_frame_mask_range_index(start, end, ranges)
if range_index is not None:
return "contained", range_index
intersects = any(
start < range_end and end > range_start
for range_start, range_end in ranges
)
return ("boundary_crossing" if intersects else "outside"), None
def filter_records_by_training_index(
records: Iterable[SongRecord],
index_path: Path,
@@ -348,6 +455,18 @@ def filter_records_by_training_index(
sort_keys=True,
)
)
ineligible_validation = sorted(
song_id
for song_id in validation_ids
if song_id in entries
and entries[song_id].get("usage", {}).get(purpose) is not True
)
if ineligible_validation:
raise ValueError(
f"Validation songs are not eligible for training purpose "
f"{purpose!r}: "
+ ", ".join(ineligible_validation)
)
validation_groups = {
str(entries[song_id].get("evaluationGroupId", ""))
for song_id in validation_ids
@@ -19,7 +19,7 @@ from typing import Any, Iterable
BUILD_IDENTITY_SCHEMA_VERSION = "cw-camera-worker-build-identity-v1"
BUILD_IDENTITY_FILE = "cw_camera_worker_build_identity.json"
WORKER_VERSION = "0.1.6"
WORKER_VERSION = "0.1.7"
def is_frozen_runtime() -> bool:
@@ -31,6 +31,7 @@ import camera_runtime_data as train
SAMPLE_RATE = 60
_SHA256 = re.compile(r"^[0-9a-f]{64}$")
_CHARACTER_ID = re.compile(r"@[0-9A-Za-z_-]+")
_AUDIO_PATTERN_TICKS_PER_FRAME = 1_000_000
@dataclass(frozen=True)
@@ -53,6 +54,9 @@ class CutReferenceCorpus:
same_audio_patterns: tuple[SameAudioCutPattern, ...]
eligible_record_count: int
unique_audio_pattern_count: int
masked_record_count: int
retained_authored_shot_count: int
censored_authored_shot_count: int
def as_metadata(self) -> dict[str, Any]:
weights = np.asarray(self.duration_weights, dtype=np.float64)
@@ -83,6 +87,13 @@ class CutReferenceCorpus:
"sameCharacterPatternCount": sum(
pattern.same_character for pattern in self.same_audio_patterns
),
"audioAlignedMaskedRecordCount": self.masked_record_count,
"audioAlignedRetainedAuthoredShotCount": (
self.retained_authored_shot_count
),
"audioAlignedCensoredAuthoredShotCount": (
self.censored_authored_shot_count
),
}
def weighted_duration_frame_samples(
@@ -129,9 +140,16 @@ def audio_fingerprint(record: train.SongRecord) -> str:
raise ValueError(
f"Invalid audioFingerprint for cut reference {record.id}."
)
return declared
if record.audio_path.is_file():
return sha256_file(record.audio_path)
actual = sha256_file(record.audio_path)
if declared and actual != declared:
raise ValueError(
f"Declared audioFingerprint does not match the materialized "
f"audio for cut reference {record.id}."
)
return actual
if declared:
return declared
# Portable RuntimeData omits raw audio; the catalog fingerprint is the
# authoritative identity in that mode.
prepared = record.song.get("preparedFile", "prepared_v1.npz")
@@ -142,6 +160,104 @@ def audio_fingerprint(record: train.SongRecord) -> str:
raise FileNotFoundError(f"No audio or declared fingerprint for {record.id}.")
def _verified_masked_audio_fingerprint(
record: train.SongRecord,
indexed_fingerprint: str,
) -> str:
"""Bind an explicit frame mask to the materialized source-audio bytes."""
audio_path = record.audio_path
if not audio_path.is_file():
raise ValueError(
f"Masked cut reference {record.id} audio file is missing: {audio_path}"
)
lfs_pointer_prefix = b"version https://git-lfs.github.com/spec/v1"
with audio_path.open("rb") as stream:
if stream.read(len(lfs_pointer_prefix)) == lfs_pointer_prefix:
raise ValueError(
f"Masked cut reference {record.id} audio is still a Git LFS "
f"pointer: {audio_path}"
)
actual = sha256_file(audio_path)
if actual != indexed_fingerprint:
raise ValueError(
f"Masked cut reference {record.id} audioFingerprint does not match "
f"the materialized audio file: {audio_path}"
)
return actual
def _explicit_audio_mask_ranges(
record: train.SongRecord,
entry: Mapping[str, Any],
indexed_fingerprint: str,
) -> tuple[tuple[int, int], ...] | None:
"""Return a strict explicit mask, preserving legacy full-record behavior."""
if "audioAlignedFrameMaskRanges" not in entry:
partial = sorted(
key
for key in ("audioAlignedEndFrame", "audioAlignedFrameCount")
if key in entry
)
if partial:
raise ValueError(
f"Cut reference audio mask for {record.id} is incomplete: "
"audioAlignedFrameMaskRanges is missing while "
+ ", ".join(partial)
+ " is present."
)
return None
manifest_frame_count = record.song.get("frameCount")
indexed_frame_count = entry.get("frameCount")
if (
isinstance(manifest_frame_count, bool)
or not isinstance(manifest_frame_count, int)
or isinstance(indexed_frame_count, bool)
or not isinstance(indexed_frame_count, int)
or manifest_frame_count < 1
or indexed_frame_count != manifest_frame_count
):
raise ValueError(
f"Cut reference audio mask frameCount mismatch for {record.id}."
)
ranges = train.normalize_audio_aligned_frame_mask_ranges(
entry["audioAlignedFrameMaskRanges"],
manifest_frame_count,
label=f"Cut reference audio mask for {record.id}",
)
if not ranges:
raise ValueError(
f"Active cut reference {record.id} has an empty audio frame mask."
)
expected_count = sum(end - start for start, end in ranges)
if "audioAlignedFrameCount" in entry:
aligned_count = entry["audioAlignedFrameCount"]
if (
isinstance(aligned_count, bool)
or not isinstance(aligned_count, int)
or aligned_count != expected_count
):
raise ValueError(
f"Cut reference audioAlignedFrameCount is inconsistent for "
f"{record.id}."
)
if "audioAlignedEndFrame" in entry:
aligned_end = entry["audioAlignedEndFrame"]
if (
isinstance(aligned_end, bool)
or not isinstance(aligned_end, int)
or aligned_end != ranges[-1][1]
):
raise ValueError(
f"Cut reference audioAlignedEndFrame is inconsistent for "
f"{record.id}."
)
_verified_masked_audio_fingerprint(record, indexed_fingerprint)
return ranges
def character_id(record: train.SongRecord) -> str:
value = str(record.song.get("characterPath", ""))
match = _CHARACTER_ID.search(value)
@@ -213,6 +329,35 @@ def _align_pattern(
return tuple(sorted(set(mapped)))
def _audio_relative_boundary_signature(
boundaries: Sequence[int],
source: train.SongRecord,
) -> tuple[int, ...]:
"""Identify one edit pattern in audio-relative micro-frame ticks.
Raw Timeline frame boundaries are not a sufficient identity because two
exports can share those integers while using different ``audioStart``
offsets. Grouping those records would union shifted cuts into false
near-duplicate pairs. Micro-frame ticks preserve sub-frame audio offsets
while remaining deterministic JSON/float input material.
"""
audio_start = float(source.song.get("audioStart", 0.0))
if not math.isfinite(audio_start):
raise ValueError("audioStart must be finite for cut-pattern identity.")
audio_start_ticks = int(
round(
audio_start
* SAMPLE_RATE
* _AUDIO_PATTERN_TICKS_PER_FRAME
)
)
return tuple(
int(boundary) * _AUDIO_PATTERN_TICKS_PER_FRAME - audio_start_ticks
for boundary in boundaries
)
def build_cut_reference_corpus(
target: train.SongRecord,
records: Sequence[train.SongRecord],
@@ -223,9 +368,19 @@ def build_cut_reference_corpus(
target_fingerprint = audio_fingerprint(target)
target_character = character_id(target)
pattern_records: dict[
tuple[str, tuple[int, ...]], list[tuple[train.SongRecord, tuple[int, ...]]]
tuple[str, tuple[int, ...]],
list[
tuple[
train.SongRecord,
tuple[tuple[int, int], ...],
tuple[int, ...],
]
],
] = defaultdict(list)
eligible_count = 0
masked_record_count = 0
retained_authored_shot_count = 0
censored_authored_shot_count = 0
for record in records:
entry = index_entries.get(record.id)
if not isinstance(entry, Mapping):
@@ -241,34 +396,87 @@ def build_cut_reference_corpus(
if not _SHA256.fullmatch(fingerprint):
raise ValueError(f"Invalid indexed audio fingerprint for {record.id}.")
boundaries = authored_boundaries(record)
pattern_records[(fingerprint, boundaries)].append((record, boundaries))
mask_ranges = _explicit_audio_mask_ranges(record, entry, fingerprint)
if mask_ranges is None:
retained_spans = tuple(zip(boundaries, boundaries[1:]))
retained_cuts = boundaries[1:-1]
else:
masked_record_count += 1
retained_spans = tuple(
(left, right)
for left, right in zip(boundaries, boundaries[1:])
if train.audio_aligned_frame_mask_range_index(
left, right, mask_ranges
)
is not None
)
retained_cuts = tuple(
boundary
for boundary in boundaries[1:-1]
if any(start < boundary < end for start, end in mask_ranges)
)
retained_authored_shot_count += len(retained_spans)
censored_authored_shot_count += (
len(boundaries) - 1 - len(retained_spans)
)
pattern_signature = _audio_relative_boundary_signature(boundaries, record)
pattern_records[(fingerprint, pattern_signature)].append(
(record, retained_spans, retained_cuts)
)
eligible_count += 1
durations: list[float] = []
duration_weights: list[float] = []
same_audio_patterns: list[SameAudioCutPattern] = []
for (fingerprint, boundaries), sources in sorted(
for (fingerprint, _pattern_signature), sources in sorted(
pattern_records.items(), key=lambda value: (value[0][0], value[0][1])
):
shot_durations = np.diff(np.asarray(boundaries)) / SAMPLE_RATE
# One unique audio/cut pattern contributes one unit in total, regardless
# of how many duplicated exports or shots it contains.
per_shot_weight = 1.0 / len(shot_durations)
durations.extend(float(value) for value in shot_durations)
duration_weights.extend(per_shot_weight for _ in shot_durations)
retained_spans = sorted(
{
span
for _, source_spans, _ in sources
for span in source_spans
}
)
if retained_spans:
shot_durations = np.asarray(
[right - left for left, right in retained_spans],
dtype=np.float64,
) / SAMPLE_RATE
# One unique audio/cut pattern contributes one unit in total,
# regardless of how many duplicated exports or shots it contains.
per_shot_weight = 1.0 / len(shot_durations)
durations.extend(float(value) for value in shot_durations)
duration_weights.extend(per_shot_weight for _ in shot_durations)
if fingerprint != target_fingerprint:
continue
aligned = _align_pattern(boundaries, sources[0][0], target)
aligned_internal = {
boundary
for source, _, source_cuts in sources
for boundary in _align_pattern(
(0, *source_cuts, int(source.song["frameCount"])),
source,
target,
)[1:-1]
}
if not aligned_internal:
continue
aligned = (0, *sorted(aligned_internal), int(target.song["frameCount"]))
same_character = bool(
target_character
and any(character_id(source) == target_character for source, _ in sources)
and any(
character_id(source) == target_character
for source, _, _ in sources
)
)
same_audio_patterns.append(
SameAudioCutPattern(
boundaries=aligned,
weight=2.0 if same_character else 1.0,
same_character=same_character,
source_record_ids=tuple(sorted(source.id for source, _ in sources)),
source_record_ids=tuple(
sorted(source.id for source, _, _ in sources)
),
)
)
@@ -285,4 +493,7 @@ def build_cut_reference_corpus(
same_audio_patterns=tuple(same_audio_patterns),
eligible_record_count=eligible_count,
unique_audio_pattern_count=len(pattern_records),
masked_record_count=masked_record_count,
retained_authored_shot_count=retained_authored_shot_count,
censored_authored_shot_count=censored_authored_shot_count,
)
@@ -339,7 +339,7 @@ def build_inputs_for_records(
def policy_identity(record: Any) -> dict[str, Any]:
entry = entries[str(record.id)]
return {
identity = {
"songId": str(record.id),
"datasetId": str(record.dataset_id),
"contentFingerprint": entry.get("contentFingerprint"),
@@ -351,6 +351,18 @@ def build_inputs_for_records(
"aspectRatio": entry.get("aspectRatio"),
"aspectSource": entry.get("aspectSource"),
}
if "audioAlignedFrameMaskRanges" in entry:
frame_count = entry.get("frameCount")
ranges = runtime_data.normalize_audio_aligned_frame_mask_ranges(
entry["audioAlignedFrameMaskRanges"],
frame_count,
label=f"Training index audio mask for {record.id}",
)
identity["frameCount"] = frame_count
identity["audioAlignedFrameMaskRanges"] = [
list(frame_range) for frame_range in ranges
]
return identity
def target_identity(record: Any) -> dict[str, Any]:
if bool(getattr(record, "is_generation_input", False)):
@@ -1,4 +1,4 @@
../../Scripts/wheel.exe,sha256=A40JvpbJRmAo327Gh4g5jNGRWEobaBMdktVtpIqzJAk,108448
../../Scripts/wheel.exe,sha256=1GLKUxJuQuNenSR7B5UFoFKuXyK1UHvT9Dqwy-JntDE,108448
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/METADATA,sha256=mKz84H7m7jsxJyzeIcTVORiTb0NPMV39KvOIYhGgmjA,2313
@@ -1,7 +1,7 @@
{
"schemaVersion": "cw-camera-worker-distribution-build-v1",
"createdUtc": "2026-08-09T07:15:49.8790622Z",
"workerVersion": "0.1.6",
"createdUtc": "2026-08-09T23:32:56.7289154Z",
"workerVersion": "0.1.7",
"protocolVersion": "1",
"buildIdentitySchemaVersion": "cw-camera-worker-build-identity-v1",
"buildEnvironment": {
@@ -65,9 +65,9 @@
],
"payload": {
"fileCount": 646,
"bytes": 267657984,
"executableSha256": "09901a07746f98e94d3a4307df83b37044732e15b5001cdc9c7a67133d520847",
"buildIdentitySha256": "34120be099cf1355cafb9af96cdb355ca9cd1b69651f20a050a1e30db1f3948e",
"bytes": 267767197,
"executableSha256": "adbc014b8cd85574367c927b2403ddc41ec561d5c655331598f8a4905b2e5b6a",
"buildIdentitySha256": "6243f071b4a05cacd08b94d2c57ba8fad2a6024bda828c7e8e45d176f45fd696",
"sanityRange": {
"minimumFileCount": 500,
"maximumFileCount": 1000,