feat(camera-ai): bundle portable runtime data

This commit is contained in:
KINDNICK
2026-08-07 00:51:13 +09:00
parent 80ff929a1c
commit 1e21e2b57f
751 changed files with 57865 additions and 29 deletions
@@ -472,6 +472,30 @@ def quat_to_rotation_6d(q: np.ndarray) -> np.ndarray:
return np.concatenate((first_column, second_column), axis=-1)
def rotation_6d_to_quat(values: np.ndarray) -> np.ndarray:
"""Convert compact rotation columns to normalized XYZW quaternions."""
first = values[..., :3]
second = values[..., 3:6]
first = first / np.maximum(np.linalg.norm(first, axis=-1, keepdims=True), 1e-8)
second = second - np.sum(first * second, axis=-1, keepdims=True) * first
second = second / np.maximum(np.linalg.norm(second, axis=-1, keepdims=True), 1e-8)
third = np.cross(first, second)
matrix = np.stack((first, second, third), axis=-1)
result = np.empty(values.shape[:-1] + (4,), dtype=np.float32)
trace = np.trace(matrix, axis1=-2, axis2=-1)
positive = trace > 0.0
root = np.sqrt(np.maximum(trace[positive] + 1.0, 1e-8)) * 2.0
result[positive, 3] = 0.25 * root
result[positive, 0] = (matrix[positive, 2, 1] - matrix[positive, 1, 2]) / root
result[positive, 1] = (matrix[positive, 0, 2] - matrix[positive, 2, 0]) / root
result[positive, 2] = (matrix[positive, 1, 0] - matrix[positive, 0, 1]) / root
fallback = ~positive
result[fallback] = np.asarray((0.0, 0.0, 1.0, 0.0), dtype=np.float32)
result /= np.maximum(np.linalg.norm(result, axis=-1, keepdims=True), 1e-8)
return result
def extract_audio_features(
path: Path,
timeline_times: np.ndarray,
@@ -0,0 +1,815 @@
#!/usr/bin/env python
"""Packaged command-line facade for the high-quality camera generator.
Machine-readable job events are written as one compact JSON object per line,
prefixed with ``CWAI_EVENT ``. Human diagnostics remain on stderr so Unity can
parse stdout without scraping prose emitted by the historical scripts.
"""
from __future__ import annotations
import argparse
import contextlib
import dataclasses
import _thread
import importlib
import io
import json
import os
import platform
import sys
import threading
import time
import traceback
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Callable, Iterable, TextIO
import cw_camera_runtime
WORKER_VERSION = "0.1.3"
EVENT_SCHEMA_VERSION = "cw-camera-worker-event-v1"
PROTOCOL_VERSION = "1"
EVENT_PREFIX = "CWAI_EVENT "
EXECUTABLE_NAME = "CWCameraWorker.exe"
DEFAULT_HEARTBEAT_SECONDS = 5.0
GENERATION_STAGES = (
"catalog_and_policy",
"preparation_cache_lookup",
"target_preparation",
"template_preparation",
"model_audio_preparation",
"shot_planning",
"candidate_cache_lookup",
"candidate_retarget_and_safety",
"candidate_cache_publish",
"output_serialization",
"metadata_assembly",
"metadata_write",
)
PREPARATION_STAGES = (
"catalog_and_policy",
"cache_lookup",
"load_prepared_songs",
"build_template_library",
"fit_shot_classifier",
"fit_hierarchical_planner",
"build_beat_onset_cache",
"verify_build_inputs_stable",
"publish_and_verify",
)
class WorkerUsageError(ValueError):
"""Raised when wrapper-level command arguments are malformed."""
class WorkerCancelled(RuntimeError):
"""Raised when Unity creates the cancellation sentinel for this job."""
def _encoding_safe_text(stream: TextIO, value: object) -> str:
"""Return text that a strict, locale-encoded stream can always write."""
text = str(value)
encoding = getattr(stream, "encoding", None) or "ascii"
try:
return text.encode(encoding, errors="backslashreplace").decode(encoding)
except LookupError:
return text.encode("ascii", errors="backslashreplace").decode("ascii")
def _write_text_safely(stream: TextIO, value: object) -> None:
"""Write diagnostics without letting the host code page hide an error."""
text = str(value)
try:
stream.write(text)
except UnicodeEncodeError:
stream.write(_encoding_safe_text(stream, text))
stream.flush()
def _write_line_safely(stream: TextIO, value: object) -> None:
_write_text_safely(stream, str(value) + "\n")
class CancellationGuard:
def __init__(self, cancel_file: str = "") -> None:
self.path = Path(cancel_file).resolve() if cancel_file else None
self.requested = False
def poll(self) -> bool:
if self.requested:
return True
self.requested = bool(self.path is not None and self.path.is_file())
return self.requested
def raise_if_cancelled(self) -> None:
if self.poll():
raise WorkerCancelled("Camera worker cancellation was requested.")
class EventEmitter:
"""Write ordered, flush-on-line JSON events for one worker job."""
def __init__(
self,
command: str,
job_id: str,
stream: TextIO | None = None,
clock: Callable[[], float] = time.perf_counter,
) -> None:
self.command = str(command)
self.job_id = str(job_id)
self.stream = stream if stream is not None else sys.stdout
self._clock = clock
self._started = float(clock())
self._sequence = 0
self._lock = threading.Lock()
@property
def elapsed_seconds(self) -> float:
return max(0.0, float(self._clock()) - self._started)
def emit(self, event_type: str, **fields: Any) -> dict[str, Any]:
with self._lock:
if event_type == "progress":
if "phase" not in fields and "stage" in fields:
fields["phase"] = fields["stage"]
if "progress" not in fields and "fraction" in fields:
fields["progress"] = fields["fraction"]
payload = {
"schemaVersion": EVENT_SCHEMA_VERSION,
"protocolVersion": PROTOCOL_VERSION,
"sequence": self._sequence,
"jobId": self.job_id,
"command": self.command,
"type": str(event_type),
"timestampUtc": datetime.now(timezone.utc).isoformat(),
"elapsedSeconds": self.elapsed_seconds,
**fields,
}
self._sequence += 1
line = EVENT_PREFIX + json.dumps(
payload,
# JSON escapes preserve the original Unicode value while
# keeping the wire protocol safe for Windows code pages such
# as CP949. Unity's JSON parser restores the escaped text.
ensure_ascii=True,
allow_nan=False,
separators=(",", ":"),
)
_write_line_safely(self.stream, line)
return payload
class Heartbeat:
"""Emit liveness events while a long native-library stage is silent."""
def __init__(
self,
emitter: EventEmitter,
interval_seconds: float,
cancellation: CancellationGuard | None = None,
) -> None:
self._emitter = emitter
self._interval = max(0.2, float(interval_seconds))
self._cancellation = cancellation
self._stop = threading.Event()
self._thread: threading.Thread | None = None
def __enter__(self) -> "Heartbeat":
self._thread = threading.Thread(
target=self._run,
name="CWCameraWorkerHeartbeat",
daemon=True,
)
self._thread.start()
return self
def __exit__(self, *_: object) -> None:
self._stop.set()
if self._thread is not None:
self._thread.join(timeout=self._interval + 1.0)
def _run(self) -> None:
while not self._stop.wait(self._interval):
if self._cancellation is not None and self._cancellation.poll():
_thread.interrupt_main()
return
self._emitter.emit("heartbeat")
@contextlib.contextmanager
def temporary_argv(program: str, values: Iterable[str]):
previous = sys.argv
sys.argv = [program, *list(values)]
try:
yield
finally:
sys.argv = previous
def install_legacy_pickle_aliases(generator_module: Any) -> None:
"""Allow trusted caches written by direct ``generate_hybrid.py`` runs.
Older automatic preparation bundles pickle ``ShotTemplate`` as
``__main__.ShotTemplate``. The packaged entry point has a different
``__main__`` module, so expose generator dataclasses there before loading
the already hash-validated local cache.
"""
main_module = sys.modules.get("__main__")
if main_module is None:
return
for name, value in vars(generator_module).items():
if not isinstance(value, type) or not dataclasses.is_dataclass(value):
continue
if getattr(value, "__module__", "") not in {
generator_module.__name__,
"__main__",
}:
continue
setattr(main_module, name, value)
def discover_runtime_root(explicit: str = "") -> Path:
"""Find the sidecar data/model root without embedding machine paths."""
candidates: list[Path] = []
if explicit:
candidates.append(Path(explicit))
environment = os.environ.get("CWAI_RUNTIME_ROOT", "").strip()
if environment:
candidates.append(Path(environment))
executable = Path(sys.executable).resolve()
candidates.extend((Path.cwd(), executable.parent))
# Frozen utility layout: CameraAI~/Tools~/CWCameraWorker/CWCameraWorker.exe
# ships its read-only RuntimeData~ sibling two levels above the executable.
candidates.extend(
(
executable.parent.parent.parent / "RuntimeData~",
executable.parent.parent / "RuntimeData~",
)
)
if not cw_camera_runtime.is_frozen_runtime():
candidates.append(Path(__file__).resolve().parents[2])
resolved: list[Path] = []
for candidate in candidates:
path = candidate.resolve()
if path not in resolved:
resolved.append(path)
for path in resolved:
if (
(path / "DatasetExports").is_dir()
or (path / "reports" / "training_index.json").is_file()
):
return path
return resolved[0] if resolved else Path.cwd().resolve()
def default_cache_root(runtime_root: Path) -> Path:
override = os.environ.get("CWAI_CACHE_ROOT", "").strip()
if override:
return Path(override).resolve()
resolved = Path(runtime_root).resolve()
if resolved.name == "RuntimeData~":
return Path.cwd().resolve() / ".cw-ai-cache"
return resolved / ".cache"
def has_option(values: list[str], option: str) -> bool:
return any(value == option or value.startswith(option + "=") for value in values)
def option_value(values: list[str], option: str) -> str | None:
result: str | None = None
for index, value in enumerate(values):
if value.startswith(option + "="):
result = value.split("=", 1)[1]
elif value == option and index + 1 < len(values):
result = values[index + 1]
return result
def augment_generate_arguments(values: list[str], runtime_root: Path) -> list[str]:
"""Supply portable sidecar defaults while preserving explicit options."""
result = list(values)
cache_root = default_cache_root(runtime_root)
defaults = (
("--training-index", runtime_root / "reports" / "training_index.json"),
("--cut-ranker-model", runtime_root / "models" / "cut_ranker_v2.json"),
("--cache-dir", cache_root / "camera_director"),
("--preparation-cache-root", cache_root / "hybrid_preparation"),
("--candidate-cache-root", cache_root / "hybrid_candidates"),
(
"--single-shot-cache-root",
cache_root / "hybrid_single_shot_candidates",
),
)
for option, path in defaults:
if not has_option(result, option):
result.extend((option, str(path)))
dataset_root = runtime_root / "DatasetExports"
if not has_option(result, "--data") and dataset_root.is_dir():
result.extend(("--data", str(dataset_root)))
return result
def augment_prepare_arguments(values: list[str], runtime_root: Path) -> list[str]:
result = list(values)
cache_root = default_cache_root(runtime_root)
defaults = (
("--training-index", runtime_root / "reports" / "training_index.json"),
("--cache-dir", cache_root / "camera_director"),
("--output-root", cache_root / "hybrid_preparation"),
)
for option, path in defaults:
if not has_option(result, option):
result.extend((option, str(path)))
dataset_root = runtime_root / "DatasetExports"
if not has_option(result, "--data") and dataset_root.is_dir():
result.extend(("--data", str(dataset_root)))
return result
def extract_worker_options(values: list[str]) -> tuple[dict[str, Any], list[str]]:
"""Remove worker-only options before forwarding generator arguments."""
options: dict[str, Any] = {
"job_id": uuid.uuid4().hex,
"runtime_root": "",
"heartbeat_seconds": DEFAULT_HEARTBEAT_SECONDS,
"cancel_file": "",
"event_format": "jsonl",
}
remaining: list[str] = []
aliases = {
"--job-id": "job_id",
"--worker-job-id": "job_id",
"--runtime-root": "runtime_root",
"--heartbeat-seconds": "heartbeat_seconds",
"--cancel-file": "cancel_file",
"--event-format": "event_format",
}
index = 0
while index < len(values):
value = values[index]
matched = False
for flag, name in aliases.items():
if value == flag:
if index + 1 >= len(values):
raise WorkerUsageError(f"{flag} requires a value.")
options[name] = values[index + 1]
index += 2
matched = True
break
if value.startswith(flag + "="):
options[name] = value.split("=", 1)[1]
index += 1
matched = True
break
if matched:
continue
remaining.append(value)
index += 1
try:
options["heartbeat_seconds"] = float(options["heartbeat_seconds"])
except (TypeError, ValueError) as error:
raise WorkerUsageError("--heartbeat-seconds must be numeric.") from error
if options["heartbeat_seconds"] <= 0:
raise WorkerUsageError("--heartbeat-seconds must be positive.")
if not str(options["job_id"]).strip():
raise WorkerUsageError("--job-id must not be empty.")
if str(options["event_format"]).strip().lower() != "jsonl":
raise WorkerUsageError("--event-format currently supports only 'jsonl'.")
return options, remaining
def _forward_legacy_tail(captured: io.StringIO) -> None:
lines = [line for line in captured.getvalue().splitlines() if line.strip()]
if lines:
_write_line_safely(sys.stderr, "[legacy] " + lines[-1])
def _reporting_generation_timer(
generator: Any,
emitter: EventEmitter,
cancellation: CancellationGuard,
) -> type:
original = generator.SequentialStageTimer
class ReportingSequentialStageTimer(original):
def mark(self, completed_stage: str) -> float:
elapsed = super().mark(completed_stage)
cancellation.raise_if_cancelled()
try:
index = GENERATION_STAGES.index(completed_stage) + 1
except ValueError:
index = 0
emitter.emit(
"progress",
stage=completed_stage,
stageSeconds=elapsed,
completedStages=index,
totalStages=len(GENERATION_STAGES),
fraction=(index / len(GENERATION_STAGES) if index else None),
)
return elapsed
return ReportingSequentialStageTimer
def run_generate(values: list[str]) -> int:
if values in (("--help",), ("-h",)):
values = list(values)
options, forwarded = extract_worker_options(list(values))
runtime_root = discover_runtime_root(str(options["runtime_root"]))
forwarded = augment_generate_arguments(forwarded, runtime_root)
emitter = EventEmitter("generate", str(options["job_id"]))
cancellation = CancellationGuard(str(options["cancel_file"]))
emitter.emit(
"started",
workerVersion=WORKER_VERSION,
runtimeRoot=str(runtime_root),
)
original_timer: type | None = None
captured = io.StringIO()
try:
cancellation.raise_if_cancelled()
with Heartbeat(
emitter,
float(options["heartbeat_seconds"]),
cancellation,
):
emitter.emit("progress", stage="loading_runtime", fraction=0.0)
generator = importlib.import_module("generate_hybrid")
install_legacy_pickle_aliases(generator)
original_timer = generator.SequentialStageTimer
generator.SequentialStageTimer = _reporting_generation_timer(
generator,
emitter,
cancellation,
)
with temporary_argv("generate_hybrid.py", forwarded):
with contextlib.redirect_stdout(captured):
generator.main()
_forward_legacy_tail(captured)
output_value = option_value(forwarded, "--output")
output = Path(output_value).resolve() if output_value else None
metadata: dict[str, Any] = {}
if output is not None and (output / "metadata.json").is_file():
metadata = json.loads(
(output / "metadata.json").read_text(encoding="utf-8")
)
emitter.emit(
"result",
success=True,
outputPath=str(output) if output is not None else "",
outputDirectory=str(output) if output is not None else "",
metadataPath=(
str(output / "metadata.json") if output is not None else ""
),
frameCount=metadata.get("frameCount"),
shotCount=metadata.get("shotCount"),
seed=metadata.get("seed"),
)
return 0
except SystemExit as error:
code = int(error.code or 0)
if code == 0:
emitter.emit("result", success=True, help=True)
return 0
emitter.emit(
"error",
success=False,
code="invalid_arguments",
message=f"Generator argument parsing failed with exit code {code}.",
)
return code
except (KeyboardInterrupt, WorkerCancelled):
emitter.emit(
"cancelled",
success=False,
message="Camera generation was cancelled.",
)
return 130
except Exception as error:
if captured.getvalue():
_forward_legacy_tail(captured)
emitter.emit(
"error",
success=False,
code=type(error).__name__,
message=str(error),
)
_write_text_safely(sys.stderr, traceback.format_exc())
return 1
finally:
if original_timer is not None:
generator.SequentialStageTimer = original_timer
def _reporting_preparation_timer(
cache_module: Any,
emitter: EventEmitter,
cancellation: CancellationGuard,
) -> type:
original = cache_module.StageTimer
class ReportingStageTimer(original):
@contextlib.contextmanager
def stage(self, name: str):
with super().stage(name):
yield
cancellation.raise_if_cancelled()
try:
index = PREPARATION_STAGES.index(name) + 1
except ValueError:
index = 0
emitter.emit(
"progress",
stage=name,
completedStages=index,
totalStages=len(PREPARATION_STAGES),
fraction=(index / len(PREPARATION_STAGES) if index else None),
)
return ReportingStageTimer
def run_prepare_cache(values: list[str]) -> int:
options, forwarded = extract_worker_options(list(values))
runtime_root = discover_runtime_root(str(options["runtime_root"]))
forwarded = augment_prepare_arguments(forwarded, runtime_root)
emitter = EventEmitter("prepare-cache", str(options["job_id"]))
cancellation = CancellationGuard(str(options["cancel_file"]))
emitter.emit(
"started",
workerVersion=WORKER_VERSION,
runtimeRoot=str(runtime_root),
)
original_timer: type | None = None
try:
cancellation.raise_if_cancelled()
with Heartbeat(
emitter,
float(options["heartbeat_seconds"]),
cancellation,
):
emitter.emit("progress", stage="loading_runtime", fraction=0.0)
generator = importlib.import_module("generate_hybrid")
install_legacy_pickle_aliases(generator)
builder = importlib.import_module("build_hybrid_preparation_cache")
cache_module = builder.preparation_cache
original_timer = cache_module.StageTimer
cache_module.StageTimer = _reporting_preparation_timer(
cache_module,
emitter,
cancellation,
)
with temporary_argv(
"build_hybrid_preparation_cache.py",
forwarded,
):
args = builder.parse_args()
report = builder.run(args)
emitter.emit(
"result",
success=True,
cacheHit=bool(report.get("cacheHit")),
cacheKey=report.get("cacheKey"),
cachePath=report.get("cachePath"),
targetSongId=report.get("targetSongId"),
)
return 0
except SystemExit as error:
code = int(error.code or 0)
if code == 0:
emitter.emit("result", success=True, help=True)
return 0
emitter.emit(
"error",
success=False,
code="invalid_arguments",
message=f"Cache builder argument parsing failed with exit code {code}.",
)
return code
except (KeyboardInterrupt, WorkerCancelled):
emitter.emit(
"cancelled",
success=False,
message="Preparation-cache build was cancelled.",
)
return 130
except Exception as error:
emitter.emit(
"error",
success=False,
code=type(error).__name__,
message=str(error),
)
_write_text_safely(sys.stderr, traceback.format_exc())
return 1
finally:
if original_timer is not None:
cache_module.StageTimer = original_timer
def version_payload() -> dict[str, Any]:
build_identity = cw_camera_runtime.load_build_identity()
return {
"name": "CWCameraWorker",
"executableName": EXECUTABLE_NAME,
"workerVersion": WORKER_VERSION,
"eventSchemaVersion": EVENT_SCHEMA_VERSION,
"protocolVersion": PROTOCOL_VERSION,
"python": platform.python_version(),
"frozen": cw_camera_runtime.is_frozen_runtime(),
"buildIdentitySchemaVersion": build_identity.get("schemaVersion"),
"buildCreatedUtc": build_identity.get("createdUtc"),
}
def run_version(values: list[str]) -> int:
parser = argparse.ArgumentParser(prog="CWCameraWorker version")
parser.add_argument("--json", action="store_true")
args = parser.parse_args(values)
payload = version_payload()
if args.json:
_write_line_safely(
sys.stdout,
json.dumps(
payload,
ensure_ascii=True,
allow_nan=False,
separators=(",", ":"),
),
)
else:
_write_line_safely(
sys.stdout,
f"CWCameraWorker {payload['workerVersion']}",
)
return 0
def dependency_checks() -> list[dict[str, Any]]:
checks: list[dict[str, Any]] = []
for name in (
"numpy",
"scipy",
"sklearn",
"librosa",
"joblib",
"soundfile",
):
try:
module = importlib.import_module(name)
checks.append(
{
"name": "dependency:" + name,
"ok": True,
"detail": str(getattr(module, "__version__", "available")),
}
)
except Exception as error:
checks.append(
{
"name": "dependency:" + name,
"ok": False,
"detail": f"{type(error).__name__}: {error}",
}
)
try:
generator = importlib.import_module("generate_hybrid")
install_legacy_pickle_aliases(generator)
importlib.import_module("build_hybrid_preparation_cache")
checks.append(
{
"name": "workerModules",
"ok": True,
"detail": "generator and cache builder imported",
}
)
except Exception as error:
checks.append(
{
"name": "workerModules",
"ok": False,
"detail": f"{type(error).__name__}: {error}",
}
)
return checks
def doctor_payload(runtime_root: Path) -> dict[str, Any]:
runtime_root = Path(runtime_root).resolve()
resources = (
("datasetRoot", runtime_root / "DatasetExports", "directory"),
(
"trainingIndex",
runtime_root / "reports" / "training_index.json",
"file",
),
(
"cutRankerModel",
runtime_root / "models" / "cut_ranker_v2.json",
"file",
),
)
checks = dependency_checks()
for name, path, kind in resources:
ok = path.is_dir() if kind == "directory" else path.is_file()
checks.append({"name": name, "ok": ok, "detail": str(path)})
if cw_camera_runtime.is_frozen_runtime():
identity = cw_camera_runtime.load_build_identity()
checks.append(
{
"name": "buildIdentity",
"ok": bool(identity),
"detail": str(identity.get("schemaVersion") or "missing"),
}
)
return {
**version_payload(),
"runtimeRoot": str(runtime_root),
"ok": all(bool(check["ok"]) for check in checks),
"checks": checks,
}
def run_doctor(values: list[str]) -> int:
parser = argparse.ArgumentParser(prog="CWCameraWorker doctor")
parser.add_argument("--json", action="store_true")
parser.add_argument("--runtime-root", default="")
args = parser.parse_args(values)
payload = doctor_payload(discover_runtime_root(args.runtime_root))
if args.json:
_write_line_safely(
sys.stdout,
json.dumps(
payload,
ensure_ascii=True,
allow_nan=False,
separators=(",", ":"),
),
)
else:
_write_line_safely(
sys.stdout,
"CWCameraWorker doctor: " + ("OK" if payload["ok"] else "FAILED"),
)
for check in payload["checks"]:
marker = "OK" if check["ok"] else "FAIL"
_write_line_safely(
sys.stdout,
f"[{marker}] {check['name']}: {check['detail']}",
)
return 0 if payload["ok"] else 2
def print_help() -> None:
print(
"Usage: CWCameraWorker <command> [options]\n\n"
"Commands:\n"
" generate Generate a complete camera edit or one-shot composite.\n"
" prepare-cache Build the high-quality preparation cache.\n"
" version Print worker version (`version --json` for JSON).\n"
" doctor Validate dependencies and runtime sidecar assets.\n\n"
"Worker options for generate/prepare-cache:\n"
" --runtime-root PATH DatasetExports/models/reports sidecar root.\n"
" --job-id ID Stable Unity job correlation ID.\n"
" --cancel-file PATH Sentinel created by Unity to cancel.\n"
" --heartbeat-seconds N Liveness event interval (default 5)."
)
def main(argv: list[str] | None = None) -> int:
values = list(sys.argv[1:] if argv is None else argv)
if not values or values[0] in {"-h", "--help", "help"}:
print_help()
return 0
command, command_values = values[0], values[1:]
try:
if command == "generate":
return run_generate(command_values)
if command == "prepare-cache":
return run_prepare_cache(command_values)
if command == "version":
return run_version(command_values)
if command == "doctor":
return run_doctor(command_values)
raise WorkerUsageError(f"Unknown command: {command}")
except WorkerUsageError as error:
_write_line_safely(sys.stderr, f"CWCameraWorker: {error}")
return 2
if __name__ == "__main__":
raise SystemExit(main())
@@ -2141,25 +2141,13 @@ def build_template_library(
(16.0 / 9.0, "assumed_16_9"),
)
frame_count = len(prepared.targets)
source_camera = np.fromfile(
record.song_dir / "camera.f32",
dtype="<f4",
).reshape(frame_count, 9)
source_root = np.fromfile(
record.song_dir / "root.f32",
dtype="<f4",
).reshape(frame_count, 14)
camera_relative_rotation = train.quat_multiply(
train.quat_conjugate(source_root[:, 3:7]),
source_camera[:, 3:7],
)
camera_relative_rotation /= np.maximum(
np.linalg.norm(
camera_relative_rotation,
axis=1,
keepdims=True,
),
1e-8,
# Portable RuntimeData deliberately contains only prepared arrays.
# Targets already store camera position/rotation in the character
# local frame, so deriving the template from them avoids requiring
# the original camera/root binary artifacts.
source_camera = prepared.targets
camera_relative_rotation = train.rotation_6d_to_quat(
prepared.targets[:, 3:9]
)
shot_file = json.loads(
(record.song_dir / "shots.json").read_text(encoding="utf-8")
@@ -2198,8 +2186,8 @@ def build_template_library(
maximum_window=61,
)
rotation_path = raw_rotation_path[pose_slice]
fov_path = source_camera[start:end, 7][pose_slice].copy()
dutch_path = source_camera[start:end, 8][pose_slice].copy()
fov_path = source_camera[start:end, 9][pose_slice].copy()
dutch_path = source_camera[start:end, 10][pose_slice].copy()
source_joint_path = canonical_joints[start:end][pose_slice]
source_camera_path = (
motion_path + source_joint_path[:, 2]
@@ -130,7 +130,16 @@ def audio_fingerprint(record: train.SongRecord) -> str:
f"Invalid audioFingerprint for cut reference {record.id}."
)
return declared
return sha256_file(record.audio_path)
if record.audio_path.is_file():
return sha256_file(record.audio_path)
# Portable RuntimeData omits raw audio; the catalog fingerprint is the
# authoritative identity in that mode.
prepared = record.song.get("preparedFile", "prepared_v1.npz")
if (record.song_dir / prepared).is_file():
return hashlib.sha256(
f"prepared:{record.id}:{prepared}".encode("utf-8")
).hexdigest()
raise FileNotFoundError(f"No audio or declared fingerprint for {record.id}.")
def character_id(record: train.SongRecord) -> str:
@@ -156,7 +165,16 @@ def authored_boundaries(record: train.SongRecord) -> tuple[int, ...]:
raise ValueError(
f"Cut reference {record.id} is {sample_rate} fps; expected 60 fps."
)
values = np.fromfile(record.song_dir / "shot_index.i32", dtype="<i4")
shot_index_path = record.song_dir / "shot_index.i32"
if shot_index_path.is_file():
values = np.fromfile(shot_index_path, dtype="<i4")
else:
prepared_path = record.portable_prepared_path
if not prepared_path.is_file():
raise FileNotFoundError(f"No cut source for {record.id}.")
with np.load(prepared_path, allow_pickle=False) as data:
cuts = np.asarray(data["cuts"], dtype=np.float32)
values = np.cumsum(cuts > 0.5, dtype=np.int64)
if values.shape != (frame_count,):
raise ValueError(
f"Cut reference {record.id} shot_index length {len(values)} != "
@@ -953,6 +953,36 @@ def rotation_6d_to_matrix(values: np.ndarray) -> np.ndarray:
return np.stack((first, second, third), axis=-1)
def rotation_6d_to_quat(values: np.ndarray) -> np.ndarray:
"""Convert the compact camera rotation representation to XYZW quaternions."""
matrix = rotation_6d_to_matrix(values)
result = np.empty(matrix.shape[:-2] + (4,), dtype=np.float32)
trace = np.trace(matrix, axis1=-2, axis2=-1)
positive = trace > 0.0
root = np.sqrt(np.maximum(trace[positive] + 1.0, 1e-8)) * 2.0
result[positive, 3] = 0.25 * root
result[positive, 0] = (matrix[positive, 2, 1] - matrix[positive, 1, 2]) / root
result[positive, 1] = (matrix[positive, 0, 2] - matrix[positive, 2, 0]) / root
result[positive, 2] = (matrix[positive, 1, 0] - matrix[positive, 0, 1]) / root
remaining = ~positive
for axis in range(3):
mask = remaining & (matrix[..., axis, axis] >= matrix[..., (axis + 1) % 3, (axis + 1) % 3])
mask &= matrix[..., axis, axis] >= matrix[..., (axis + 2) % 3, (axis + 2) % 3]
if not np.any(mask):
continue
j = (axis + 1) % 3
k = (axis + 2) % 3
root = np.sqrt(np.maximum(1.0 + matrix[mask, axis, axis] - matrix[mask, j, j] - matrix[mask, k, k], 1e-8)) * 2.0
result[mask, axis] = 0.25 * root
result[mask, 3] = (matrix[mask, k, j] - matrix[mask, j, k]) / root
result[mask, j] = (matrix[mask, j, axis] + matrix[mask, axis, j]) / root
result[mask, k] = (matrix[mask, k, axis] + matrix[mask, axis, k]) / root
remaining &= ~mask
result /= np.maximum(np.linalg.norm(result, axis=-1, keepdims=True), 1e-8)
return result
@torch.no_grad()
def evaluate(
model: nn.Module,