using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using Unity.Cinemachine; using UnityEditor; using UnityEngine; using UnityEngine.Playables; using UnityEngine.SceneManagement; using UnityEngine.Timeline; namespace Streamingle.Editor { public static class TimelineCameraDatasetExporter { private const int SampleRate = 60; private const string TimelineRootName = "TimeLine"; private const string MotionDirectorName = "Motion"; private const string SchemaVersion = "1.3"; private const string GenerationInputKind = "generation_input"; internal const string SourceFingerprintSchema = "cw-ai-camera-source-v2-dependency-hash"; internal const string LegacySourceFingerprintSchema = "cw-ai-camera-source-v1-semantic"; private const string GenerationInputDirectoryPrefix = "GenerationInput_60fps_"; private static readonly CultureInfo Invariant = CultureInfo.InvariantCulture; [Serializable] public sealed class GenerationInputSummary { public bool isValid; public string error; public string timelineName; public string characterName; public string characterPath; public string audioClipName; public string audioAssetPath; public double duration; public bool usesStructuredMotionDirector; } [MenuItem("Tools/Streamingle/Timeline/Export Camera Dataset (60 FPS)")] public static void ExportFromMenu() { try { var outputPath = ExportAll60FpsForCli(); Debug.Log($"Timeline camera dataset export complete: {outputPath}"); EditorUtility.RevealInFinder(outputPath); } catch (Exception exception) { Debug.LogException(exception); throw; } } /// /// Entry point intended for UniCLI Eval. /// Returns the absolute dataset directory. /// public static string ExportAll60FpsForCli() { var projectRoot = Directory.GetParent(Application.dataPath)?.FullName ?? throw new InvalidOperationException("Unable to resolve the Unity project root."); var outputRoot = Path.Combine( projectRoot, "DatasetExports", $"TimelineCamera_60fps_{DateTime.Now:yyyyMMdd_HHmmss}"); return ExportActiveScene60FpsToForCli(outputRoot); } /// /// Exports one selected performance Timeline as a camera-generation input. /// The Timeline only needs character animation and audio; authored camera /// tracks and Cinemachine shots are deliberately ignored. /// public static string ExportGenerationInput60FpsForCli( PlayableDirector sourceDirector) { var projectRoot = Directory.GetParent(Application.dataPath)?.FullName ?? throw new InvalidOperationException( "Unable to resolve the Unity project root."); var datasetRoot = Path.Combine(projectRoot, "DatasetExports"); if (TryComputeGenerationInputSourceFingerprint( sourceDirector, out var sourceFingerprint, out var fingerprintError)) { if (TryFindReusableGenerationInputExport( datasetRoot, sourceFingerprint, out var reusableExport, out _)) { Debug.Log( $"[CW-AI] Reusing unchanged generation input: " + $"{reusableExport}"); return reusableExport; } } else if (!string.IsNullOrWhiteSpace(fingerprintError)) { Debug.Log( $"[CW-AI] Generation input reuse skipped: " + $"{fingerprintError}"); } var outputRoot = Path.Combine( datasetRoot, $"{GenerationInputDirectoryPrefix}" + $"{DateTime.Now:yyyyMMdd_HHmmss_fff}"); return ExportGenerationInput60FpsToForCli( sourceDirector, outputRoot, true); } /// /// Read-only probe for a simple generator UI. It never evaluates the /// Timeline or writes files. /// public static GenerationInputSummary GetGenerationInputSummaryForEditor( PlayableDirector sourceDirector) { var summary = new GenerationInputSummary(); try { if (sourceDirector == null) { throw new ArgumentNullException(nameof(sourceDirector)); } if (sourceDirector.playableAsset is not TimelineAsset timeline) { throw new InvalidOperationException( "The selected PlayableDirector must reference a TimelineAsset."); } if (!sourceDirector.gameObject.scene.IsValid() || !sourceDirector.gameObject.scene.isLoaded) { throw new InvalidOperationException( "The selected PlayableDirector must belong to a loaded scene."); } var song = BuildGenerationInputContext(sourceDirector); var duration = ResolveGenerationInputDuration(song); var audio = FindActiveAudio(song.MainTimeline, 0, duration); ValidateAudioSource(audio); summary.isValid = true; summary.error = string.Empty; summary.timelineName = timeline.name; summary.characterName = song.CharacterAnimator.name; summary.characterPath = GetHierarchyPath( song.CharacterAnimator.transform); summary.audioClipName = audio.Clip?.name ?? string.Empty; summary.audioAssetPath = audio.AssetPath; summary.duration = duration; summary.usesStructuredMotionDirector = song.MotionDirector != song.MainDirector; } catch (Exception exception) { summary.isValid = false; summary.error = exception.Message; } return summary; } /// /// Explicit-output overload used by editor automation and tests. /// Produces a single-entry manifest even when other Directors exist in /// the open scene. /// public static string ExportGenerationInput60FpsToForCli( PlayableDirector sourceDirector, string outputDirectory, bool requireAudio = true) { if (EditorApplication.isPlayingOrWillChangePlaymode) { throw new InvalidOperationException( "Generation input export must run in Edit Mode."); } if (sourceDirector == null) { throw new ArgumentNullException(nameof(sourceDirector)); } if (sourceDirector.playableAsset is not TimelineAsset) { throw new InvalidOperationException( "The selected PlayableDirector must reference a TimelineAsset."); } if (!sourceDirector.gameObject.scene.IsValid() || !sourceDirector.gameObject.scene.isLoaded) { throw new InvalidOperationException( "The selected PlayableDirector must belong to a loaded scene."); } if (string.IsNullOrWhiteSpace(outputDirectory)) { throw new ArgumentException( "An output directory is required.", nameof(outputDirectory)); } var outputRoot = Path.GetFullPath(outputDirectory); if (Directory.Exists(outputRoot) && Directory.EnumerateFileSystemEntries(outputRoot).Any()) { throw new IOException( $"The output directory is not empty: '{outputRoot}'."); } var song = BuildGenerationInputContext(sourceDirector); TryComputeGenerationInputSourceFingerprint( song, out var sourceFingerprint, out _); var sourceScene = sourceDirector.gameObject.scene; var sourceSceneWasDirty = sourceScene.isDirty; var songs = new List { song }; var sharedJoints = FindSharedJoints(songs); if (sharedJoints.Count == 0) { throw new InvalidOperationException( "The selected character has no exportable joints."); } Directory.CreateDirectory(outputRoot); var activationObjects = BuildActivationChain( song.Root, song.MotionDirector.transform); var activationStates = activationObjects .Select(gameObject => gameObject.activeSelf) .ToArray(); var activationChanged = new bool[activationObjects.Count]; SongMetadata exportedSong; try { for (var index = 0; index < activationObjects.Count; index++) { if (activationObjects[index].activeSelf) { continue; } activationObjects[index].SetActive(true); activationChanged[index] = true; } exportedSong = ExportSong( song, 0, 1, sharedJoints, outputRoot, requireAudio, false, GenerationInputKind); } finally { for (var index = activationObjects.Count - 1; index >= 0; index--) { if (!activationChanged[index]) { continue; } activationObjects[index].SetActive(activationStates[index]); } if (!sourceSceneWasDirty && sourceScene.isDirty) { foreach (var gameObject in activationObjects) { EditorUtility.ClearDirty(gameObject); } } EditorUtility.ClearProgressBar(); } var scene = sourceScene; var manifest = new DatasetManifest { schemaVersion = SchemaVersion, role = GenerationInputKind, inputKind = GenerationInputKind, hasAuthoredCamera = false, createdUtc = DateTime.UtcNow.ToString("O", Invariant), unityVersion = Application.unityVersion, scenePath = scene.path, sampleRate = SampleRate, byteOrder = "little-endian", floatFormat = "IEEE-754 float32", timeFormat = "IEEE-754 float64", jointLayout = "[frame, bone, xyz]", cameraLayout = "[frame, position.xyz, rotation.xyzw, fieldOfViewDegrees, dutchDegrees]", rootLayout = "[frame, characterRoot.position.xyz, characterRoot.rotation.xyzw, " + "hips.position.xyz, hips.rotation.xyzw]", audioFeatureLayout = "[frame, rms, onset]", cameraSamplingPolicy = "No authored camera. camera.f32 contains NaN placeholders, " + "shot_index.i32 contains -1, and shots.json is empty.", sourceFingerprint = sourceFingerprint, discoveredCameraTrackCount = 0, exportedCameraTrackCount = 0, skippedCameraTracks = new List(), sharedBones = sharedJoints.Select(joint => joint.Name).ToList(), songs = new List { exportedSong } }; WriteJson(Path.Combine(outputRoot, "dataset_manifest.json"), manifest); return outputRoot; } /// /// Exports the active scene to an explicit directory. /// Intended for resumable cross-project batch collection. /// public static string ExportActiveScene60FpsToForCli(string outputDirectory) { return ExportActiveScene60FpsToForCli(outputDirectory, true); } public static string ExportActiveScene60FpsToForCli( string outputDirectory, bool requireAudio) { return ExportActiveScene60FpsToForCli( outputDirectory, requireAudio, false); } public static string ExportActiveScene60FpsToForCli( string outputDirectory, bool requireAudio, bool includeMutedCameraTracks) { if (EditorApplication.isPlayingOrWillChangePlaymode) { throw new InvalidOperationException("Dataset export must run in Edit Mode."); } if (string.IsNullOrWhiteSpace(outputDirectory)) { throw new ArgumentException( "An output directory is required.", nameof(outputDirectory)); } var outputRoot = Path.GetFullPath(outputDirectory); if (Directory.Exists(outputRoot) && Directory.EnumerateFileSystemEntries(outputRoot).Any()) { throw new IOException( $"The output directory is not empty: '{outputRoot}'."); } var scene = SceneManager.GetActiveScene(); var sourceFingerprint = ComputeSceneCameraSourceFingerprint(scene); var skippedCameraTracks = new List(); var timelineRoot = TryFindTimelineRoot(scene); var songs = timelineRoot != null ? BuildSongContexts( timelineRoot, includeMutedCameraTracks, skippedCameraTracks) : new List(); var usesSongActivationIsolation = songs.Count > 0; if (songs.Count == 0) { songs = BuildCombinedTimelineContexts( scene, includeMutedCameraTracks, skippedCameraTracks); } if (songs.Count == 0) { throw new InvalidOperationException( $"Scene '{scene.name}' does not contain a supported camera Timeline."); } var sharedJoints = FindSharedJoints(songs); if (sharedJoints.Count == 0) { throw new InvalidOperationException( "The song Animators do not share any exportable joints."); } Directory.CreateDirectory(outputRoot); var originalTimelineRootActive = timelineRoot != null && timelineRoot.gameObject.activeSelf; var originalSongActiveStates = usesSongActivationIsolation ? songs.Select(song => song.Root.gameObject.activeSelf).ToArray() : Array.Empty(); var exportedSongs = new List(); try { if (usesSongActivationIsolation) { // Only the song currently being sampled should be active. foreach (var song in songs) { song.Root.gameObject.SetActive(false); } timelineRoot.gameObject.SetActive(true); } for (var songIndex = 0; songIndex < songs.Count; songIndex++) { var song = songs[songIndex]; if (usesSongActivationIsolation) { song.Root.gameObject.SetActive(true); } try { exportedSongs.Add(ExportSong( song, songIndex, songs.Count, sharedJoints, outputRoot, requireAudio)); } finally { if (usesSongActivationIsolation) { song.Root.gameObject.SetActive(false); } } } } finally { if (usesSongActivationIsolation) { for (var index = 0; index < songs.Count; index++) { songs[index].Root.gameObject.SetActive(originalSongActiveStates[index]); } timelineRoot.gameObject.SetActive(originalTimelineRootActive); } EditorUtility.ClearProgressBar(); } var manifest = new DatasetManifest { schemaVersion = SchemaVersion, role = "training_dataset", inputKind = "authored_camera_timeline", hasAuthoredCamera = true, createdUtc = DateTime.UtcNow.ToString("O", Invariant), unityVersion = Application.unityVersion, scenePath = scene.path, sampleRate = SampleRate, byteOrder = "little-endian", floatFormat = "IEEE-754 float32", timeFormat = "IEEE-754 float64", jointLayout = "[frame, bone, xyz]", cameraLayout = "[frame, position.xyz, rotation.xyzw, fieldOfViewDegrees, dutchDegrees]", rootLayout = "[frame, characterRoot.position.xyz, characterRoot.rotation.xyzw, " + "hips.position.xyz, hips.rotation.xyzw]", audioFeatureLayout = string.Empty, cameraSamplingPolicy = "Per authored Cinemachine track; muted variants included by batch; " + "procedural CameraState sampled deterministically; overlaps blended.", sourceFingerprintSchema = SourceFingerprintSchema, sourceFingerprint = sourceFingerprint, discoveredCameraTrackCount = songs.Count + skippedCameraTracks.Count, exportedCameraTrackCount = songs.Count, skippedCameraTracks = skippedCameraTracks, sharedBones = sharedJoints.Select(joint => joint.Name).ToList(), songs = exportedSongs }; WriteJson(Path.Combine(outputRoot, "dataset_manifest.json"), manifest); return outputRoot; } private static SongMetadata ExportSong( SongContext song, int songIndex, int songCount, IReadOnlyList sharedJoints, string outputRoot, bool requireAudio, bool hasAuthoredCamera = true, string inputKind = "authored_camera_timeline") { var folderName = $"{songIndex + 1:D2}_{SanitizeFileName(song.SongName)}"; var songDirectory = Path.Combine(outputRoot, folderName); Directory.CreateDirectory(songDirectory); var shots = hasAuthoredCamera ? BuildShots(song) : new List(); if (hasAuthoredCamera && shots.Count == 0) { throw new InvalidOperationException( $"'{song.SongName}' does not contain Cinemachine shots."); } var sampleStart = hasAuthoredCamera ? Math.Max(0.0, shots.Min(shot => shot.Start)) : 0.0; var sampleEnd = hasAuthoredCamera ? Math.Min( song.MainDirector.duration, shots.Max(shot => shot.End)) : ResolveGenerationInputDuration(song); if (sampleEnd <= sampleStart) { throw new InvalidOperationException( $"'{song.SongName}' has an invalid sampling interval."); } var audio = FindActiveAudio( song.MainTimeline, sampleStart, sampleEnd); var audioSourcePath = ResolveAudioSource(audio, requireAudio); // The end is exclusive so every exported frame has a valid timeline time. var frameCount = (int)Math.Ceiling( (sampleEnd - sampleStart) * SampleRate - 1e-8); var boundAnimators = song.MotionTimeline .GetOutputTracks() .OfType() .Where(track => !track.mutedInHierarchy) .Select(track => song.MotionDirector.GetGenericBinding(track) as Animator) .Where(animator => animator != null) .Distinct() .ToArray(); var originalCullingModes = boundAnimators .Select(animator => animator.cullingMode) .ToArray(); var cameraTracks = song.MotionTimeline .GetOutputTracks() .OfType() .ToArray(); var originalCameraTrackMutes = cameraTracks .Select(track => track.muted) .ToArray(); var originalMotionTime = song.MotionDirector.time; var originalCurrentTimeOverride = CinemachineCore.CurrentTimeOverride; var originalDeltaTimeOverride = CinemachineCore.UniformDeltaTimeOverride; var jointsPath = Path.Combine(songDirectory, "joints_world.f32"); var cameraPath = Path.Combine(songDirectory, "camera.f32"); var rootPath = Path.Combine(songDirectory, "root.f32"); var shotIndexPath = Path.Combine(songDirectory, "shot_index.i32"); var timePath = Path.Combine(songDirectory, "time.f64"); var audioFeaturesPath = Path.Combine( songDirectory, "audio_features.f32"); var previewRows = new List { "frame,time,shotIndex,shotName,camX,camY,camZ,fov,dutch," + "hipsX,hipsY,hipsZ,headX,headY,headZ" }; var previewFrames = BuildPreviewFrameSet(frameCount); var missingCameraFrames = 0; var missingJointSamples = 0; var missingRootSamples = 0; var previouslyActiveCameras = new HashSet(); var exportedShots = new List(); ShotRuntime previousDominantShot = null; ShotInfo currentShotInfo = null; try { foreach (var animator in boundAnimators) { animator.cullingMode = AnimatorCullingMode.AlwaysAnimate; } foreach (var cameraTrack in cameraTracks) { cameraTrack.muted = true; } song.MotionDirector.RebuildGraph(); using var jointsWriter = CreateBinaryWriter(jointsPath); using var cameraWriter = CreateBinaryWriter(cameraPath); using var rootWriter = CreateBinaryWriter(rootPath); using var shotIndexWriter = CreateBinaryWriter(shotIndexPath); using var timeWriter = CreateBinaryWriter(timePath); for (var frame = 0; frame < frameCount; frame++) { if ((frame & 255) == 0) { var songProgress = frame / (float)Math.Max(1, frameCount); EditorUtility.DisplayProgressBar( "Timeline Camera Dataset Export", $"{song.SongName}: {frame:N0} / {frameCount:N0} frames", (songIndex + songProgress) / songCount); } var time = sampleStart + frame / (double)SampleRate; CinemachineCore.CurrentTimeOverride = (float)time; CinemachineCore.UniformDeltaTimeOverride = 1f / SampleRate; song.MotionDirector.time = time; song.MotionDirector.Evaluate(); timeWriter.Write(time); WriteJointFrame( jointsWriter, song.CharacterAnimator, sharedJoints, ref missingJointSamples); WriteRootFrame( rootWriter, song.CharacterAnimator, sharedJoints, ref missingRootSamples); var cameraSample = hasAuthoredCamera ? EvaluateCameraSample( shots, time, song.CameraBrain != null ? song.CameraBrain.DefaultWorldUp : Vector3.up, previouslyActiveCameras) : new CameraSample(); if (cameraSample.DominantShot != previousDominantShot) { if (currentShotInfo != null) { CloseShotInfo(currentShotInfo, time); } currentShotInfo = cameraSample.IsValid ? CreateShotInfo( cameraSample.DominantShot, exportedShots.Count, time) : null; if (currentShotInfo != null) { exportedShots.Add(currentShotInfo); } previousDominantShot = cameraSample.DominantShot; } if (!cameraSample.IsValid || currentShotInfo == null) { missingCameraFrames++; shotIndexWriter.Write(-1); WriteInvalidCameraFrame(cameraWriter); } else { shotIndexWriter.Write(currentShotInfo.index); WriteCameraFrame(cameraWriter, cameraSample.State); } if (previewFrames.Contains(frame)) { previewRows.Add(BuildPreviewRow( frame, time, currentShotInfo?.index ?? -1, currentShotInfo?.cameraName ?? string.Empty, cameraSample.IsValid ? cameraSample.State : (CameraState?)null, song.CharacterAnimator, sharedJoints)); } } if (currentShotInfo != null) { CloseShotInfo(currentShotInfo, sampleEnd); } } finally { CinemachineCore.CurrentTimeOverride = originalCurrentTimeOverride; CinemachineCore.UniformDeltaTimeOverride = originalDeltaTimeOverride; for (var index = 0; index < boundAnimators.Length; index++) { boundAnimators[index].cullingMode = originalCullingModes[index]; } for (var index = 0; index < cameraTracks.Length; index++) { cameraTracks[index].muted = originalCameraTrackMutes[index]; } try { song.MotionDirector.RebuildGraph(); song.MotionDirector.time = originalMotionTime; song.MotionDirector.Evaluate(); } catch (Exception exception) { Debug.LogWarning( $"[CW-AI] Timeline restore warning for " + $"'{song.SongName}': {exception}"); } } File.WriteAllLines( Path.Combine(songDirectory, "preview.csv"), previewRows); var audioFeaturesStatus = string.Empty; if (!hasAuthoredCamera) { audioFeaturesStatus = WriteAudioFeatures( audioFeaturesPath, audio, sampleStart, frameCount, song.SongName); } var audioFile = CopyAudioSource(audioSourcePath, songDirectory); var copiedAudioPath = string.IsNullOrWhiteSpace(audioFile) ? string.Empty : Path.Combine(songDirectory, audioFile); var audioFingerprint = File.Exists(copiedAudioPath) ? ComputeFileSha256(copiedAudioPath) : string.Empty; var jointsFingerprint = hasAuthoredCamera ? string.Empty : ComputeFileSha256(jointsPath); var cameraFingerprint = hasAuthoredCamera ? string.Empty : ComputeFileSha256(cameraPath); var rootFingerprint = hasAuthoredCamera ? string.Empty : ComputeFileSha256(rootPath); var shotIndexFingerprint = hasAuthoredCamera ? string.Empty : ComputeFileSha256(shotIndexPath); var timeFingerprint = hasAuthoredCamera ? string.Empty : ComputeFileSha256(timePath); var audioFeaturesFingerprint = !hasAuthoredCamera && File.Exists(audioFeaturesPath) ? ComputeFileSha256(audioFeaturesPath) : string.Empty; var shotFile = new ShotFile { timelineName = song.SongName, shots = exportedShots }; WriteJson(Path.Combine(songDirectory, "shots.json"), shotFile); var metadata = new SongMetadata { songName = song.SongName, role = hasAuthoredCamera ? "training_dataset" : GenerationInputKind, inputKind = inputKind, hasAuthoredCamera = hasAuthoredCamera, folderName = folderName, mainTimelineAssetPath = AssetDatabase.GetAssetPath(song.MainTimeline), motionTimelineAssetPath = AssetDatabase.GetAssetPath(song.MotionTimeline), characterPath = GetHierarchyPath(song.CharacterAnimator.transform), cameraTrackName = song.CameraTrack?.name ?? string.Empty, cameraTrackIndex = hasAuthoredCamera ? song.CameraTrackIndex : -1, cameraTrackMuted = hasAuthoredCamera && song.CameraTrack != null && song.CameraTrack.mutedInHierarchy, audioAssetPath = audio.AssetPath, audioFile = audioFile, audioFingerprint = audioFingerprint, audioStart = audio.Start, audioDuration = audio.Duration, mainDuration = song.MainDirector.duration, motionDuration = song.MotionDirector.duration, sampleRate = SampleRate, sampleStart = sampleStart, sampleEndExclusive = sampleEnd, frameCount = frameCount, sharedBoneCount = sharedJoints.Count, skeletonType = song.CharacterAnimator.isHuman ? "Humanoid" : "Generic", shotCount = exportedShots.Count, missingCameraFrames = missingCameraFrames, missingJointSamples = missingJointSamples, missingRootSamples = missingRootSamples, jointsFile = "joints_world.f32", jointsFingerprint = jointsFingerprint, cameraFile = "camera.f32", cameraFingerprint = cameraFingerprint, rootFile = "root.f32", rootFingerprint = rootFingerprint, shotIndexFile = "shot_index.i32", shotIndexFingerprint = shotIndexFingerprint, timeFile = "time.f64", timeFingerprint = timeFingerprint, shotsFile = "shots.json", previewFile = "preview.csv", audioFeaturesFile = hasAuthoredCamera ? string.Empty : "audio_features.f32", audioFeaturesFingerprint = audioFeaturesFingerprint, audioFeatureLayout = hasAuthoredCamera ? string.Empty : "[frame, rms, onset]", audioFeaturesStatus = audioFeaturesStatus }; WriteJson(Path.Combine(songDirectory, "metadata.json"), metadata); return metadata; } private static string CopyAudioSource( string sourcePath, string songDirectory) { if (string.IsNullOrWhiteSpace(sourcePath)) { return string.Empty; } var extension = Path.GetExtension(sourcePath).ToLowerInvariant(); var fileName = $"audio_source{extension}"; File.Copy( sourcePath, Path.Combine(songDirectory, fileName), true); return fileName; } private static string ResolveAudioSource( AudioInfo audio, bool requireAudio) { if (requireAudio) { return ValidateAudioSource(audio); } if (string.IsNullOrWhiteSpace(audio.AssetPath)) { return string.Empty; } try { return ValidateAudioSource(audio); } catch (FileNotFoundException) { return string.Empty; } } private static string ValidateAudioSource(AudioInfo audio) { if (string.IsNullOrWhiteSpace(audio.AssetPath)) { throw new InvalidOperationException( "The active Timeline audio clip has no AssetDatabase path."); } var projectRoot = Directory.GetParent(Application.dataPath)?.FullName ?? throw new InvalidOperationException( "Unable to resolve the Unity project root."); var sourcePath = Path.GetFullPath( Path.Combine(projectRoot, audio.AssetPath)); if (!File.Exists(sourcePath)) { throw new FileNotFoundException( "The Timeline source audio file was not found.", sourcePath); } return sourcePath; } private static void WriteJointFrame( BinaryWriter writer, Animator animator, IReadOnlyList joints, ref int missingJointSamples) { foreach (var joint in joints) { var transform = ResolveJoint(animator, joint); if (transform == null) { missingJointSamples++; writer.Write(float.NaN); writer.Write(float.NaN); writer.Write(float.NaN); continue; } WriteVector3(writer, transform.position); } } private static void WriteRootFrame( BinaryWriter writer, Animator animator, IReadOnlyList joints, ref int missingRootSamples) { var characterRoot = animator.transform; var hips = FindSemanticJoint(animator, joints, "Hips", "Pelvis"); WriteVector3(writer, characterRoot.position); WriteQuaternion(writer, characterRoot.rotation); if (hips == null) { missingRootSamples++; WriteInvalidVector3(writer); WriteInvalidQuaternion(writer); return; } WriteVector3(writer, hips.position); WriteQuaternion(writer, hips.rotation); } private static void WriteCameraFrame( BinaryWriter writer, CameraState state) { WriteVector3(writer, state.GetCorrectedPosition()); WriteQuaternion(writer, state.GetCorrectedOrientation()); writer.Write(state.Lens.FieldOfView); writer.Write(state.Lens.Dutch); } private static void WriteInvalidCameraFrame(BinaryWriter writer) { for (var index = 0; index < 9; index++) { writer.Write(float.NaN); } } private static string BuildPreviewRow( int frame, double time, int shotIndex, string cameraName, CameraState? cameraState, Animator animator, IReadOnlyList joints) { var cameraPosition = cameraState.HasValue ? cameraState.Value.GetCorrectedPosition() : new Vector3(float.NaN, float.NaN, float.NaN); var lens = cameraState?.Lens ?? LensSettings.Default; var hips = FindSemanticJoint(animator, joints, "Hips", "Pelvis"); var head = FindSemanticJoint(animator, joints, "Head"); return string.Join(",", frame.ToString(Invariant), time.ToString("R", Invariant), shotIndex.ToString(Invariant), EscapeCsv(cameraName), cameraPosition.x.ToString("R", Invariant), cameraPosition.y.ToString("R", Invariant), cameraPosition.z.ToString("R", Invariant), lens.FieldOfView.ToString("R", Invariant), lens.Dutch.ToString("R", Invariant), (hips?.position.x ?? float.NaN).ToString("R", Invariant), (hips?.position.y ?? float.NaN).ToString("R", Invariant), (hips?.position.z ?? float.NaN).ToString("R", Invariant), (head?.position.x ?? float.NaN).ToString("R", Invariant), (head?.position.y ?? float.NaN).ToString("R", Invariant), (head?.position.z ?? float.NaN).ToString("R", Invariant)); } private static HashSet BuildPreviewFrameSet(int frameCount) { return new HashSet { 0, Math.Max(0, frameCount / 4), Math.Max(0, frameCount / 2), Math.Max(0, frameCount * 3 / 4), Math.Max(0, frameCount - 1) }; } private static List BuildShots(SongContext song) { var track = song.CameraTrack; if (track == null) { return new List(); } var result = new List(); var orderedClips = track.GetClips().OrderBy(clip => clip.start).ToArray(); foreach (var timelineClip in orderedClips) { if (timelineClip.end <= timelineClip.start + 1e-8 || timelineClip.asset is not CinemachineShot shotAsset) { continue; } var virtualCamera = shotAsset.VirtualCamera.Resolve(song.MotionDirector); result.Add(new ShotRuntime { Clip = timelineClip, Start = timelineClip.start, End = timelineClip.end, VirtualCamera = virtualCamera, }); } return result; } private static CameraSample EvaluateCameraSample( IReadOnlyList shots, double time, Vector3 worldUp, HashSet previouslyActiveCameras) { var activeShots = shots .Where(shot => time >= shot.Start - 1e-7 && time < shot.End - 1e-7 && shot.VirtualCamera != null) .Take(2) .ToArray(); var activeCameras = new HashSet( activeShots.Select(shot => shot.VirtualCamera)); foreach (var virtualCamera in activeCameras) { var isCameraCut = !previouslyActiveCameras.Contains(virtualCamera); if (isCameraCut) { virtualCamera.PreviousStateIsValid = false; } virtualCamera.InternalUpdateCameraState( worldUp, isCameraCut ? -1f : 1f / SampleRate); } previouslyActiveCameras.Clear(); previouslyActiveCameras.UnionWith(activeCameras); if (activeShots.Length == 0) { return new CameraSample(); } if (activeShots.Length == 1) { return new CameraSample { IsValid = true, DominantShot = activeShots[0], State = activeShots[0].VirtualCamera.State }; } var outgoing = activeShots[0]; var incoming = activeShots[1]; if (incoming.Start < outgoing.Start) { (outgoing, incoming) = (incoming, outgoing); } var incomingWeight = Mathf.Clamp01( incoming.Clip.EvaluateMixIn(time) * incoming.Clip.EvaluateMixOut(time)); return new CameraSample { IsValid = true, DominantShot = incomingWeight >= 0.5f ? incoming : outgoing, State = CameraState.Lerp( outgoing.VirtualCamera.State, incoming.VirtualCamera.State, incomingWeight) }; } private static ShotInfo CreateShotInfo( ShotRuntime source, int index, double start) { return new ShotInfo { index = index, clipName = source.Clip.displayName, cameraName = source.VirtualCamera.name, cameraPath = GetHierarchyPath(source.VirtualCamera.transform), start = start, end = start, duration = 0, easeInDuration = source.Clip.easeInDuration, easeOutDuration = source.Clip.easeOutDuration }; } private static void CloseShotInfo(ShotInfo shot, double end) { shot.end = end; shot.duration = Math.Max(0, end - shot.start); } private static AudioInfo FindActiveAudio( TimelineAsset timeline, double sampleStart, double sampleEnd) { var best = timeline .GetOutputTracks() .OfType() .Where(track => !track.mutedInHierarchy) .SelectMany(track => track.GetClips()) .Select(clip => new { Clip = clip, Asset = clip.asset as AudioPlayableAsset, Overlap = Math.Max( 0, Math.Min(sampleEnd, clip.end) - Math.Max(sampleStart, clip.start)) }) .Where(item => item.Asset?.clip != null) .OrderByDescending(item => item.Overlap) .ThenByDescending(item => item.Clip.duration) .ThenBy(item => item.Clip.start) .FirstOrDefault(); return best == null ? new AudioInfo { Clip = null, AssetPath = string.Empty, Start = 0, Duration = 0, ClipIn = 0, TimeScale = 1 } : new AudioInfo { Clip = best.Asset.clip, AssetPath = AssetDatabase.GetAssetPath(best.Asset.clip), Start = best.Clip.start, Duration = best.Clip.duration, ClipIn = best.Clip.clipIn, TimeScale = best.Clip.timeScale }; } private static SongContext BuildGenerationInputContext( PlayableDirector sourceDirector) { var mainTimeline = sourceDirector.playableAsset as TimelineAsset ?? throw new InvalidOperationException( "The selected PlayableDirector has no TimelineAsset."); var motionDirector = sourceDirector; var motionTimeline = mainTimeline; var characterAnimator = FindCharacterAnimator( motionTimeline, motionDirector); // Existing performance scenes commonly keep audio/activation on the // song root Director and character animation on a child named Motion. var structuredMotionDirector = sourceDirector.transform .Find(MotionDirectorName) ?.GetComponent(); if (structuredMotionDirector?.playableAsset is TimelineAsset structuredMotionTimeline) { var structuredAnimator = FindCharacterAnimator( structuredMotionTimeline, structuredMotionDirector); if (structuredAnimator != null) { motionDirector = structuredMotionDirector; motionTimeline = structuredMotionTimeline; characterAnimator = structuredAnimator; } } if (characterAnimator == null) { throw new InvalidOperationException( "No character Animator is bound to an active AnimationTrack " + "on the selected Timeline or its Motion child Timeline."); } var duration = ResolveFiniteDuration(sourceDirector, mainTimeline); var audio = FindActiveAudio(mainTimeline, 0, duration); var audioName = string.IsNullOrWhiteSpace(audio.AssetPath) ? string.Empty : Path.GetFileNameWithoutExtension(audio.AssetPath); var songName = string.IsNullOrWhiteSpace(audioName) ? mainTimeline.name : audioName; return new SongContext { SongName = songName, Root = sourceDirector.transform, MainDirector = sourceDirector, MotionDirector = motionDirector, MainTimeline = mainTimeline, MotionTimeline = motionTimeline, CharacterAnimator = characterAnimator, CameraTrack = null, CameraTrackIndex = -1, CameraBrain = null }; } private static double ResolveGenerationInputDuration(SongContext song) { return ResolveFiniteDuration(song.MainDirector, song.MainTimeline); } private static double ResolveFiniteDuration( PlayableDirector director, TimelineAsset timeline) { var duration = director.duration; if (double.IsNaN(duration) || double.IsInfinity(duration) || duration <= 0) { duration = timeline.duration; } if (double.IsNaN(duration) || double.IsInfinity(duration) || duration <= 0) { throw new InvalidOperationException( $"Timeline '{timeline.name}' has no finite positive duration."); } return duration; } private static List BuildActivationChain( params Transform[] transforms) { var values = new HashSet(); foreach (var transform in transforms.Where(value => value != null)) { var current = transform; while (current != null) { values.Add(current.gameObject); current = current.parent; } } return values .OrderBy(value => GetHierarchyDepth(value.transform)) .ThenBy(value => GetHierarchyPath(value.transform), StringComparer.Ordinal) .ToList(); } private static int GetHierarchyDepth(Transform transform) { var depth = 0; while (transform != null) { depth++; transform = transform.parent; } return depth; } private static string WriteAudioFeatures( string path, AudioInfo audio, double sampleStart, int frameCount, string songName) { var features = new float[checked(frameCount * 2)]; var clip = audio?.Clip; if (clip == null) { WriteFloatArray(path, features); Debug.LogWarning( $"[CW-AI] '{songName}' has no readable AudioClip. " + "audio_features.f32 contains zero placeholders."); return "missing_audio_clip_zero_fallback"; } var channelCount = Math.Max(1, clip.channels); var valueCount = (long)clip.samples * channelCount; if (valueCount <= 0 || valueCount > int.MaxValue) { WriteFloatArray(path, features); Debug.LogWarning( $"[CW-AI] AudioClip '{clip.name}' is too large or empty. " + "audio_features.f32 contains zero placeholders."); return "invalid_audio_size_zero_fallback"; } try { var samples = new float[(int)valueCount]; if (!clip.GetData(samples, 0)) { WriteFloatArray(path, features); Debug.LogWarning( $"[CW-AI] AudioClip.GetData failed for '{clip.name}'. " + "Use Decompress On Load when the importer does not expose " + "PCM samples. audio_features.f32 contains zeros."); return "get_data_failed_zero_fallback"; } var previousRms = 0f; var frequency = Math.Max(1, clip.frequency); var timeScale = Math.Max(1e-6, Math.Abs(audio.TimeScale)); var windowSamples = Math.Max( 1, (int)Math.Ceiling(frequency * timeScale / SampleRate)); for (var frame = 0; frame < frameCount; frame++) { var timelineTime = sampleStart + frame / (double)SampleRate; var rms = 0f; if (timelineTime >= audio.Start && timelineTime < audio.Start + audio.Duration) { var localTime = audio.ClipIn + (timelineTime - audio.Start) * audio.TimeScale; var firstSample = (int)Math.Floor(localTime * frequency); if (firstSample >= 0 && firstSample < clip.samples) { var lastSample = Math.Min( clip.samples, firstSample + windowSamples); double squareSum = 0; long scalarCount = 0; for (var sample = firstSample; sample < lastSample; sample++) { var scalarOffset = sample * channelCount; for (var channel = 0; channel < channelCount; channel++) { var value = samples[scalarOffset + channel]; squareSum += value * value; scalarCount++; } } if (scalarCount > 0) { rms = (float)Math.Sqrt(squareSum / scalarCount); } } } features[frame * 2] = rms; features[frame * 2 + 1] = Math.Max(0f, rms - previousRms); previousRms = rms; } WriteFloatArray(path, features); return "ok"; } catch (Exception exception) { WriteFloatArray(path, features); Debug.LogWarning( $"[CW-AI] Audio feature extraction failed for '{clip.name}': " + $"{exception.Message}. audio_features.f32 contains zeros."); return "exception_zero_fallback"; } } private static void WriteFloatArray(string path, IReadOnlyList values) { using var writer = CreateBinaryWriter(path); foreach (var value in values) { writer.Write(value); } } private static List BuildSongContexts( Transform timelineRoot, bool includeMutedCameraTracks, List skippedCameraTracks) { var result = new List(); foreach (Transform child in timelineRoot) { var mainDirector = child.GetComponent(); var motionTransform = child.Find(MotionDirectorName); var motionDirector = motionTransform != null ? motionTransform.GetComponent() : null; if (mainDirector?.playableAsset is not TimelineAsset mainTimeline || motionDirector?.playableAsset is not TimelineAsset motionTimeline) { continue; } var characterAnimator = FindCharacterAnimator( motionTimeline, motionDirector); if (characterAnimator == null || !characterAnimator.isHuman) { RecordSkippedCameraTracks( motionTimeline, includeMutedCameraTracks, "No bound Humanoid character Animator was found.", skippedCameraTracks); Debug.LogWarning( $"[CW-AI] Skipping '{child.name}': no bound Humanoid " + "character Animator was found."); continue; } var cameraTracks = SelectCameraTracks( motionTimeline, motionDirector, includeMutedCameraTracks, skippedCameraTracks); for (var trackIndex = 0; trackIndex < cameraTracks.Count; trackIndex++) { var cameraTrack = cameraTracks[trackIndex]; result.Add(new SongContext { SongName = BuildCameraVariantName( child.name, cameraTrack, trackIndex, cameraTracks.Count), Root = child, MainDirector = mainDirector, MotionDirector = motionDirector, MainTimeline = mainTimeline, MotionTimeline = motionTimeline, CharacterAnimator = characterAnimator, CameraTrack = cameraTrack, CameraTrackIndex = trackIndex, CameraBrain = motionDirector.GetGenericBinding(cameraTrack) as CinemachineBrain }); } } return result; } private static List BuildCombinedTimelineContexts( Scene scene, bool includeMutedCameraTracks, List skippedCameraTracks) { var result = new List(); var directors = Resources.FindObjectsOfTypeAll() .Where(director => director.gameObject.scene == scene && director.playableAsset is TimelineAsset) .OrderBy(director => GetHierarchyPath(director.transform)); foreach (var director in directors) { var timeline = director.playableAsset as TimelineAsset; var cameraTracks = SelectCameraTracks( timeline, director, includeMutedCameraTracks, skippedCameraTracks); if (cameraTracks.Count == 0) { continue; } var characterAnimator = FindCharacterAnimator( timeline, director); if (characterAnimator == null) { RecordSkippedCameraTracks( timeline, includeMutedCameraTracks, "No exportable character Animator was found.", skippedCameraTracks, cameraTracks); continue; } var audio = FindActiveAudio( timeline, 0, timeline.duration); var audioName = string.IsNullOrWhiteSpace(audio.AssetPath) ? string.Empty : Path.GetFileNameWithoutExtension(audio.AssetPath); var baseName = string.IsNullOrWhiteSpace(audioName) ? timeline.name : audioName; for (var trackIndex = 0; trackIndex < cameraTracks.Count; trackIndex++) { var cameraTrack = cameraTracks[trackIndex]; result.Add(new SongContext { SongName = BuildCameraVariantName( baseName, cameraTrack, trackIndex, cameraTracks.Count), Root = director.transform, MainDirector = director, MotionDirector = director, MainTimeline = timeline, MotionTimeline = timeline, CharacterAnimator = characterAnimator, CameraTrack = cameraTrack, CameraTrackIndex = trackIndex, CameraBrain = director.GetGenericBinding(cameraTrack) as CinemachineBrain }); } } return result; } private static List SelectCameraTracks( TimelineAsset timeline, PlayableDirector director, bool includeMutedCameraTracks, List skippedCameraTracks) { var candidates = timeline .GetOutputTracks() .OfType() .Where(track => includeMutedCameraTracks || !track.mutedInHierarchy) .Where(track => track .GetClips() .Any(clip => clip.asset is CinemachineShot)) .ToList(); var result = new List(); foreach (var track in candidates) { var clips = track .GetClips() .Where(clip => clip.asset is CinemachineShot) .OrderBy(clip => clip.start) .ToArray(); var unresolvedCount = clips.Count(clip => ((CinemachineShot)clip.asset) .VirtualCamera.Resolve(director) == null); var coverageEnd = clips[0].end; var hasCoverageGap = false; foreach (var clip in clips.Skip(1)) { if (clip.start > coverageEnd + 1e-7) { hasCoverageGap = true; break; } coverageEnd = Math.Max(coverageEnd, clip.end); } if (unresolvedCount > 0 || hasCoverageGap) { skippedCameraTracks.Add(new SkippedCameraTrack { timelineAssetPath = AssetDatabase.GetAssetPath(timeline), trackName = track.name, muted = track.mutedInHierarchy, reason = $"unresolvedShots={unresolvedCount}; " + $"coverageGap={hasCoverageGap}" }); Debug.LogWarning( $"[CW-AI] Skipping camera track '{track.name}' in " + $"'{timeline.name}': unresolvedShots={unresolvedCount}, " + $"coverageGap={hasCoverageGap}."); continue; } result.Add(track); } return result; } private static void RecordSkippedCameraTracks( TimelineAsset timeline, bool includeMutedCameraTracks, string reason, List skippedCameraTracks, IReadOnlyCollection tracks = null) { var candidates = tracks ?? timeline .GetOutputTracks() .OfType() .Where(track => includeMutedCameraTracks || !track.mutedInHierarchy) .Where(track => track .GetClips() .Any(clip => clip.asset is CinemachineShot)) .ToArray(); foreach (var track in candidates) { skippedCameraTracks.Add(new SkippedCameraTrack { timelineAssetPath = AssetDatabase.GetAssetPath(timeline), trackName = track.name, muted = track.mutedInHierarchy, reason = reason }); } } private static string BuildCameraVariantName( string baseName, CinemachineTrack track, int trackIndex, int trackCount) { if (trackCount <= 1) { return baseName; } var mutedSuffix = track.mutedInHierarchy ? " [muted]" : string.Empty; return $"{baseName} - Camera {trackIndex + 1:D2} " + $"({track.name}){mutedSuffix}"; } private static Animator FindCharacterAnimator( TimelineAsset timeline, PlayableDirector director) { return timeline .GetOutputTracks() .OfType() .Where(track => !track.mutedInHierarchy) .Select(track => director.GetGenericBinding(track) as Animator) .Where(IsExportableCharacterAnimator) .Distinct() .OrderByDescending(animator => animator.isHuman) .ThenByDescending(animator => animator.avatar != null && animator.avatar.isValid) .ThenByDescending(animator => animator.GetComponentsInChildren(true).Length) .FirstOrDefault(); } private static bool IsExportableCharacterAnimator(Animator animator) { if (animator == null) { return false; } var path = $"/{GetHierarchyPath(animator.transform)}/"; if (path.IndexOf( "/Cam/", StringComparison.OrdinalIgnoreCase) >= 0 || path.IndexOf( "/Cams/", StringComparison.OrdinalIgnoreCase) >= 0 || path.IndexOf( "/Camera/", StringComparison.OrdinalIgnoreCase) >= 0 || animator.GetComponent() != null) { return false; } if (path.IndexOf( "Missing Prefab with guid:", StringComparison.OrdinalIgnoreCase) >= 0 || animator.name.IndexOf( "Placeholder for referenced Animator", StringComparison.OrdinalIgnoreCase) >= 0) { return false; } return animator.isHuman || (animator.avatar != null && animator.avatar.isValid) || animator.GetComponentsInChildren(true).Length > 1; } private static List FindSharedJoints( IReadOnlyList songs) { if (songs.All(song => song.CharacterAnimator.isHuman)) { return Enum.GetValues(typeof(HumanBodyBones)) .Cast() .Where(bone => bone != HumanBodyBones.LastBone) .Where(bone => songs.All(song => song.CharacterAnimator.GetBoneTransform(bone) != null)) .Select(bone => new JointDefinition { Name = bone.ToString(), HumanBone = bone }) .ToList(); } if (songs.Any(song => song.CharacterAnimator.isHuman)) { throw new InvalidOperationException( "Mixing Humanoid and Generic skeletons in one export is not supported."); } var sharedPaths = FindAnimatedTransformPaths(songs[0]); foreach (var song in songs.Skip(1)) { var songPaths = new HashSet( FindAnimatedTransformPaths(song), StringComparer.Ordinal); sharedPaths.RemoveAll(path => !songPaths.Contains(path)); } return sharedPaths .Where(path => ResolveRelativePath( songs[0].CharacterAnimator.transform, path) != null) .Select(path => new JointDefinition { Name = path, RelativePath = path }) .ToList(); } private static List FindAnimatedTransformPaths(SongContext song) { var paths = new HashSet(StringComparer.Ordinal); var tracks = song.MotionTimeline .GetOutputTracks() .OfType() .Where(track => !track.mutedInHierarchy && song.MotionDirector.GetGenericBinding(track) == song.CharacterAnimator); foreach (var track in tracks) { var clips = track.GetClips() .Select(clip => clip.asset as AnimationPlayableAsset) .Where(asset => asset?.clip != null) .Select(asset => asset.clip) .ToList(); if (track.infiniteClip != null) { clips.Add(track.infiniteClip); } foreach (var clip in clips.Distinct()) { foreach (var binding in AnimationUtility.GetCurveBindings(clip)) { if (binding.type == typeof(Transform)) { paths.Add(binding.path); } } } } return paths .OrderBy(path => string.IsNullOrEmpty(path) ? 0 : path.Count(character => character == '/') + 1) .ThenBy(path => path, StringComparer.Ordinal) .ToList(); } private static Transform ResolveJoint( Animator animator, JointDefinition joint) { return joint.HumanBone.HasValue ? animator.GetBoneTransform(joint.HumanBone.Value) : ResolveRelativePath(animator.transform, joint.RelativePath); } private static Transform ResolveRelativePath(Transform root, string path) { return string.IsNullOrEmpty(path) ? root : root.Find(path); } private static Transform FindSemanticJoint( Animator animator, IReadOnlyList joints, params string[] names) { foreach (var name in names) { var exact = joints.FirstOrDefault(joint => string.Equals(joint.Name, name, StringComparison.OrdinalIgnoreCase) || string.Equals( Path.GetFileName(joint.Name), name, StringComparison.OrdinalIgnoreCase)); if (exact != null) { return ResolveJoint(animator, exact); } } foreach (var name in names) { var partial = joints.FirstOrDefault(joint => Path.GetFileName(joint.Name) .IndexOf(name, StringComparison.OrdinalIgnoreCase) >= 0); if (partial != null) { return ResolveJoint(animator, partial); } } return null; } private static Transform TryFindTimelineRoot(Scene scene) { var root = Resources.FindObjectsOfTypeAll() .Where(gameObject => gameObject.scene == scene) .FirstOrDefault(gameObject => gameObject.name == TimelineRootName); return root != null ? root.transform : null; } internal static string ComputeSceneCameraSourceFingerprint(Scene scene) { return ComputeSceneCameraSourceFingerprint( scene, out _); } internal static string ComputeSceneCameraSourceFingerprint( Scene scene, out string legacyFingerprint) { legacyFingerprint = ComputeLegacySceneCameraSourceFingerprint(scene); var dependencyRecords = AssetDatabase .GetDependencies(scene.path, true) .Where(IsCameraSourceDependency) .Select(assetPath => assetPath.Replace('\\', '/') + "|" + AssetDatabase .GetAssetDependencyHash(assetPath) .ToString()) .ToArray(); return ComposeSceneCameraSourceFingerprint( legacyFingerprint, dependencyRecords); } internal static string ComputeLegacySceneCameraSourceFingerprint( Scene scene) { var lines = new List(); var directors = Resources.FindObjectsOfTypeAll() .Where(director => director.gameObject.scene == scene && director.playableAsset is TimelineAsset) .OrderBy(director => GetHierarchyPath(director.transform)); foreach (var director in directors) { var timeline = (TimelineAsset)director.playableAsset; var timelinePath = AssetDatabase.GetAssetPath(timeline); var outputTracks = timeline.GetOutputTracks().ToArray(); lines.Add( $"director|{GetHierarchyPath(director.transform)}|" + $"{timelinePath}"); for (var trackIndex = 0; trackIndex < outputTracks.Length; trackIndex++) { if (outputTracks[trackIndex] is not CinemachineTrack track) { continue; } lines.Add( $"track|{trackIndex}|{track.name}|" + $"{track.mutedInHierarchy}|{track.TrackPriority}"); foreach (var clip in track .GetClips() .OrderBy(item => item.start) .ThenBy(item => item.end)) { var shot = clip.asset as CinemachineShot; var virtualCamera = shot?.VirtualCamera.Resolve(director); var virtualCameraPath = virtualCamera != null ? GetHierarchyPath(virtualCamera.transform) : ""; lines.Add( $"clip|{clip.displayName}|" + $"{clip.start.ToString("R", Invariant)}|" + $"{clip.end.ToString("R", Invariant)}|" + virtualCameraPath); } } } using var sha256 = SHA256.Create(); var bytes = sha256.ComputeHash( Encoding.UTF8.GetBytes(string.Join("\n", lines))); return string.Concat(bytes.Select(item => item.ToString("x2"))); } internal static string ComposeSceneCameraSourceFingerprint( string semanticFingerprint, IEnumerable dependencyRecords) { if (string.IsNullOrWhiteSpace(semanticFingerprint)) { throw new ArgumentException( "A semantic source fingerprint is required.", nameof(semanticFingerprint)); } var records = (dependencyRecords ?? Array.Empty()) .Where(record => !string.IsNullOrWhiteSpace(record)) .Select(record => record.Trim().Replace('\\', '/')) .Distinct(StringComparer.Ordinal) .OrderBy(record => record, StringComparer.Ordinal); var payload = string.Join( "\n", new[] { "schema|" + SourceFingerprintSchema, "semantic|" + semanticFingerprint.Trim() }.Concat(records.Select(record => "dependency|" + record))); using var sha256 = SHA256.Create(); var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(payload)); return string.Concat(bytes.Select(item => item.ToString("x2"))); } internal static bool TryMatchSceneCameraSourceFingerprint( string storedSchema, string storedFingerprint, string currentStrongFingerprint, string currentLegacyFingerprint, out bool legacyFingerprintAccepted, out string error) { legacyFingerprintAccepted = false; if (string.IsNullOrWhiteSpace(storedFingerprint)) { error = "The manifest source fingerprint is missing."; return false; } if (string.IsNullOrWhiteSpace(storedSchema) || string.Equals( storedSchema, LegacySourceFingerprintSchema, StringComparison.Ordinal)) { if (!string.Equals( storedFingerprint, currentLegacyFingerprint, StringComparison.Ordinal)) { error = "The loaded scene legacy semantic fingerprint does " + "not match the manifest."; return false; } legacyFingerprintAccepted = true; error = string.Empty; return true; } if (!string.Equals( storedSchema, SourceFingerprintSchema, StringComparison.Ordinal)) { error = $"Unsupported source fingerprint schema '{storedSchema}'."; return false; } if (!string.Equals( storedFingerprint, currentStrongFingerprint, StringComparison.Ordinal)) { error = "The loaded scene strong dependency fingerprint does " + "not match the manifest."; return false; } error = string.Empty; return true; } private static bool IsCameraSourceDependency(string assetPath) { if (string.IsNullOrWhiteSpace(assetPath)) { return false; } switch (Path.GetExtension(assetPath).ToLowerInvariant()) { case ".unity": case ".playable": case ".anim": case ".prefab": case ".controller": case ".overridecontroller": case ".mask": case ".fbx": case ".dae": case ".obj": case ".blend": case ".wav": case ".mp3": case ".flac": case ".ogg": return true; case ".asset": var assetType = AssetDatabase.GetMainAssetTypeAtPath(assetPath); var fullName = assetType?.FullName ?? string.Empty; return assetType != null && (typeof(TimelineAsset).IsAssignableFrom(assetType) || typeof(AnimationClip).IsAssignableFrom(assetType) || typeof(RuntimeAnimatorController) .IsAssignableFrom(assetType) || typeof(AvatarMask).IsAssignableFrom(assetType) || fullName.StartsWith( "Unity.Cinemachine.", StringComparison.Ordinal)); default: return false; } } internal static bool TryComputeGenerationInputSourceFingerprint( PlayableDirector sourceDirector, out string fingerprint, out string error) { fingerprint = string.Empty; error = string.Empty; try { if (sourceDirector == null) { error = "The source PlayableDirector is missing."; return false; } var scene = sourceDirector.gameObject.scene; if (!scene.IsValid() || !scene.isLoaded) { error = "The source PlayableDirector is not in a loaded scene."; return false; } if (scene.isDirty) { error = "the source scene has unsaved changes; a fresh export is safer"; return false; } return TryComputeGenerationInputSourceFingerprint( BuildGenerationInputContext(sourceDirector), out fingerprint, out error); } catch (Exception exception) { fingerprint = string.Empty; error = exception.Message; return false; } } private static bool TryComputeGenerationInputSourceFingerprint( SongContext song, out string fingerprint, out string error) { fingerprint = string.Empty; error = string.Empty; try { var scene = song.MainDirector.gameObject.scene; if (!scene.IsValid() || !scene.isLoaded || scene.isDirty) { error = "the source scene is unsaved or dirty; a fresh export is required"; return false; } if (string.IsNullOrWhiteSpace(scene.path)) { error = "the source scene has not been saved"; return false; } var lines = new List(); if (!TryAppendAssetPathFingerprint( lines, "scene", scene.path, out error) || !TryAppendPersistentAssetFingerprint( lines, "mainTimeline", song.MainTimeline, out error) || !TryAppendPersistentAssetFingerprint( lines, "motionTimeline", song.MotionTimeline, out error)) { return false; } if (EditorUtility.IsDirty(song.MainDirector) || EditorUtility.IsDirty(song.MotionDirector) || EditorUtility.IsDirty(song.CharacterAnimator)) { error = "a Director or Animator has unsaved changes"; return false; } AppendDirectorFingerprint(lines, "mainDirector", song.MainDirector); AppendDirectorFingerprint( lines, "motionDirector", song.MotionDirector); if (!TryAppendTimelineFingerprint( lines, "main", song.MainTimeline, song.MainDirector, scene, out error)) { return false; } if (song.MotionTimeline != song.MainTimeline && !TryAppendTimelineFingerprint( lines, "motion", song.MotionTimeline, song.MotionDirector, scene, out error)) { return false; } if (!TryAppendRigFingerprint( lines, song.CharacterAnimator, scene, out error)) { return false; } lines.Add( $"duration|" + ResolveGenerationInputDuration(song) .ToString("R", Invariant)); fingerprint = ComputeTextSha256(lines); return true; } catch (Exception exception) { fingerprint = string.Empty; error = exception.Message; return false; } } private static bool TryAppendTimelineFingerprint( List lines, string label, TimelineAsset timeline, PlayableDirector director, Scene sourceScene, out string error) { var rootTracks = timeline.GetRootTracks().ToArray(); for (var index = 0; index < rootTracks.Length; index++) { if (!TryAppendTrackFingerprint( lines, $"{label}/track[{index}]", rootTracks[index], director, sourceScene, out error)) { return false; } } error = string.Empty; return true; } private static bool TryAppendTrackFingerprint( List lines, string label, TrackAsset track, PlayableDirector director, Scene sourceScene, out string error) { if (track == null) { error = $"{label} is missing"; return false; } var trackType = track.GetType(); var isGroup = trackType == typeof(GroupTrack); var isMarker = trackType == typeof(MarkerTrack); var isAnimation = trackType == typeof(AnimationTrack); var isAudio = trackType == typeof(AudioTrack); var isCinemachine = trackType == typeof(CinemachineTrack); var isControl = trackType == typeof(ControlTrack); var isRecorder = trackType == typeof(UnityEditor.Recorder.Timeline.RecorderTrack); // This project track only writes facial blendshape weights, which // are not part of the joint/root generation input. Its serialized // track, clips, binding and scene dependencies are still hashed. var isBlendshapeGaze = string.Equals( trackType.FullName, "Streamingle.Gaze.BlendshapeGazeTrack", StringComparison.Ordinal) && string.Equals( trackType.Assembly.GetName().Name, "Streamingle.Gaze.Runtime", StringComparison.Ordinal); var isSubtitle = string.Equals( trackType.FullName, "Streamingle.Subtitles.SubtitleTrack", StringComparison.Ordinal) && string.Equals( trackType.Assembly.GetName().Name, "Streamingle.Subtitles.Runtime", StringComparison.Ordinal); if (!isGroup && !isMarker && !isAnimation && !isAudio && !isCinemachine && !isControl && !isRecorder && !isBlendshapeGaze && !isSubtitle) { error = $"custom Timeline track '{trackType.FullName}' cannot be " + "fingerprinted safely"; return false; } if (EditorUtility.IsDirty(track)) { error = $"Timeline track '{track.name}' has unsaved changes"; return false; } if (track.GetMarkers().Any()) { error = $"Timeline track '{track.name}' contains notifications or " + "custom markers"; return false; } lines.Add( $"{label}|{trackType.FullName}|{track.name}|" + $"muted={track.muted}|mutedInHierarchy={track.mutedInHierarchy}|" + $"locked={track.locked}|lockedInHierarchy={track.lockedInHierarchy}"); if (!isGroup && !isMarker) { var binding = director.GetGenericBinding(track); if (isAnimation && binding != null && binding is not Animator) { error = $"Animation track '{track.name}' has an unsupported binding"; return false; } if (isAudio && binding != null && binding is not AudioSource) { error = $"Audio track '{track.name}' has an unsupported binding"; return false; } if (!TryAppendBindingFingerprint( lines, $"{label}/binding", binding, sourceScene, out error)) { return false; } } var clips = track.GetClips() .OrderBy(value => value.start) .ThenBy(value => value.end) .ThenBy(value => value.displayName, StringComparer.Ordinal) .ToArray(); for (var clipIndex = 0; clipIndex < clips.Length; clipIndex++) { if (!TryAppendTimelineClipFingerprint( lines, $"{label}/clip[{clipIndex}]", clips[clipIndex], isAnimation, isAudio, isCinemachine, out error)) { return false; } } if (isAnimation) { var animationTrack = (AnimationTrack)track; lines.Add( $"{label}/animation|trackOffset={animationTrack.trackOffset}"); if (animationTrack.infiniteClip != null && !TryAppendPersistentAssetFingerprint( lines, $"{label}/infiniteClip", animationTrack.infiniteClip, out error)) { return false; } if (animationTrack.avatarMask != null && !TryAppendPersistentAssetFingerprint( lines, $"{label}/avatarMask", animationTrack.avatarMask, out error)) { return false; } } var childTracks = track.GetChildTracks().ToArray(); for (var childIndex = 0; childIndex < childTracks.Length; childIndex++) { if (!TryAppendTrackFingerprint( lines, $"{label}/child[{childIndex}]", childTracks[childIndex], director, sourceScene, out error)) { return false; } } error = string.Empty; return true; } private static bool TryAppendTimelineClipFingerprint( List lines, string label, TimelineClip clip, bool isAnimationTrack, bool isAudioTrack, bool isCinemachineTrack, out string error) { var asset = clip.asset; if (asset == null) { error = $"Timeline clip '{clip.displayName}' has no PlayableAsset"; return false; } if ((isAnimationTrack && asset.GetType() != typeof(AnimationPlayableAsset)) || (isAudioTrack && asset.GetType() != typeof(AudioPlayableAsset)) || (isCinemachineTrack && asset.GetType() != typeof(CinemachineShot))) { error = $"custom PlayableAsset '{asset.GetType().FullName}' cannot be " + "fingerprinted safely"; return false; } lines.Add( $"{label}|{clip.displayName}|" + $"start={clip.start.ToString("R", Invariant)}|" + $"duration={clip.duration.ToString("R", Invariant)}|" + $"end={clip.end.ToString("R", Invariant)}|" + $"clipIn={clip.clipIn.ToString("R", Invariant)}|" + $"timeScale={clip.timeScale.ToString("R", Invariant)}|" + $"easeIn={clip.easeInDuration.ToString("R", Invariant)}|" + $"easeOut={clip.easeOutDuration.ToString("R", Invariant)}|" + $"blendIn={clip.blendInDuration.ToString("R", Invariant)}|" + $"blendOut={clip.blendOutDuration.ToString("R", Invariant)}|" + $"preExtrapolation={clip.preExtrapolationMode}|" + $"postExtrapolation={clip.postExtrapolationMode}"); if (!TryAppendPersistentAssetFingerprint( lines, $"{label}/playable", asset, out error)) { return false; } if (clip.curves != null && !TryAppendPersistentAssetFingerprint( lines, $"{label}/curves", clip.curves, out error)) { return false; } if (asset is AnimationPlayableAsset animationAsset) { if (animationAsset.clip == null || !TryAppendPersistentAssetFingerprint( lines, $"{label}/animationClip", animationAsset.clip, out error)) { if (string.IsNullOrWhiteSpace(error)) { error = $"Animation clip '{clip.displayName}' has no source clip"; } return false; } lines.Add( $"{label}/animationSettings|" + $"position={FormatVector3(animationAsset.position)}|" + $"euler={FormatVector3(animationAsset.eulerAngles)}|" + $"removeStartOffset={animationAsset.removeStartOffset}|" + $"applyFootIK={animationAsset.applyFootIK}|" + $"loop={animationAsset.loop}|" + $"useTrackMatchFields={animationAsset.useTrackMatchFields}|" + $"matchTargetFields={animationAsset.matchTargetFields}"); } else if (asset is AudioPlayableAsset audioAsset) { if (audioAsset.clip == null || !TryAppendPersistentAssetFingerprint( lines, $"{label}/audioClip", audioAsset.clip, out error)) { if (string.IsNullOrWhiteSpace(error)) { error = $"Audio clip '{clip.displayName}' has no source clip"; } return false; } lines.Add($"{label}/audioSettings|loop={audioAsset.loop}"); } error = string.Empty; return true; } private static bool TryAppendBindingFingerprint( List lines, string label, UnityEngine.Object binding, Scene sourceScene, out string error) { if (binding == null) { lines.Add($"{label}|"); error = string.Empty; return true; } GameObject gameObject = null; if (binding is GameObject boundGameObject) { gameObject = boundGameObject; } else if (binding is Component component) { gameObject = component.gameObject; } if (gameObject != null) { if (gameObject.scene != sourceScene) { error = $"Timeline binding '{binding.name}' belongs to another scene"; return false; } lines.Add( $"{label}|{binding.GetType().FullName}|" + GetHierarchyPath(gameObject.transform)); error = string.Empty; return true; } if (EditorUtility.IsPersistent(binding)) { return TryAppendPersistentAssetFingerprint( lines, label, binding, out error); } error = $"Timeline binding '{binding.name}' cannot be fingerprinted safely"; return false; } private static bool TryAppendRigFingerprint( List lines, Animator animator, Scene sourceScene, out string error) { if (animator == null || animator.gameObject.scene != sourceScene) { error = "The character rig is not part of the source scene."; return false; } lines.Add( $"character|{GetHierarchyPath(animator.transform)}|" + $"isHuman={animator.isHuman}|applyRootMotion={animator.applyRootMotion}|" + $"updateMode={animator.updateMode}|" + $"worldPosition={FormatVector3(animator.transform.position)}|" + $"worldRotation={FormatQuaternion(animator.transform.rotation)}|" + $"worldScale={FormatVector3(animator.transform.lossyScale)}"); var hierarchyPaths = animator .GetComponentsInChildren(true) .Select(transform => GetRelativeHierarchyPath(animator.transform, transform)) .OrderBy(value => value, StringComparer.Ordinal); foreach (var hierarchyPath in hierarchyPaths) { lines.Add($"rigNode|{hierarchyPath}"); } var ancestor = animator.transform.parent; while (ancestor != null) { lines.Add( $"rigAncestor|{GetHierarchyPath(ancestor)}|" + $"position={FormatVector3(ancestor.localPosition)}|" + $"rotation={FormatQuaternion(ancestor.localRotation)}|" + $"scale={FormatVector3(ancestor.localScale)}|" + $"active={ancestor.gameObject.activeSelf}"); ancestor = ancestor.parent; } if (animator.avatar != null && !TryAppendPersistentAssetFingerprint( lines, "character/avatar", animator.avatar, out error)) { return false; } if (animator.runtimeAnimatorController != null && !TryAppendPersistentAssetFingerprint( lines, "character/controller", animator.runtimeAnimatorController, out error)) { return false; } error = string.Empty; return true; } private static void AppendDirectorFingerprint( List lines, string label, PlayableDirector director) { lines.Add( $"{label}|{GetHierarchyPath(director.transform)}|" + $"playOnAwake={director.playOnAwake}|" + $"timeUpdateMode={director.timeUpdateMode}|" + $"extrapolationMode={director.extrapolationMode}"); } private static bool TryAppendPersistentAssetFingerprint( List lines, string label, UnityEngine.Object asset, out string error) { if (asset == null) { error = $"Asset '{label}' is missing."; return false; } if (EditorUtility.IsDirty(asset)) { error = $"Asset '{asset.name}' has unsaved changes."; return false; } var assetPath = AssetDatabase.GetAssetPath(asset); if (string.IsNullOrWhiteSpace(assetPath)) { error = $"Asset '{asset.name}' is not persistent and cannot be " + "fingerprinted safely."; return false; } return TryAppendAssetPathFingerprint( lines, label, assetPath, out error); } private static bool TryAppendAssetPathFingerprint( List lines, string label, string assetPath, out string error) { if (string.IsNullOrWhiteSpace(assetPath) || AssetDatabase.LoadMainAssetAtPath(assetPath) == null) { error = $"Asset path '{assetPath}' is not available."; return false; } var dependencyHash = AssetDatabase.GetAssetDependencyHash(assetPath).ToString(); if (string.IsNullOrWhiteSpace(dependencyHash) || dependencyHash.All(character => character == '0')) { error = $"Asset dependency hash for '{assetPath}' is unavailable."; return false; } lines.Add($"{label}|{assetPath}|{dependencyHash}"); error = string.Empty; return true; } private static string GetRelativeHierarchyPath( Transform root, Transform value) { if (value == root) { return "."; } var names = new List(); var current = value; while (current != null && current != root) { names.Add(current.name); current = current.parent; } if (current != root) { return $"/{GetHierarchyPath(value)}"; } names.Reverse(); return string.Join("/", names); } private static string FormatVector3(Vector3 value) { return $"{value.x.ToString("R", Invariant)}," + $"{value.y.ToString("R", Invariant)}," + value.z.ToString("R", Invariant); } private static string FormatQuaternion(Quaternion value) { return $"{value.x.ToString("R", Invariant)}," + $"{value.y.ToString("R", Invariant)}," + $"{value.z.ToString("R", Invariant)}," + value.w.ToString("R", Invariant); } private static string ComputeTextSha256(IEnumerable lines) { using var sha256 = SHA256.Create(); var bytes = sha256.ComputeHash( Encoding.UTF8.GetBytes(string.Join("\n", lines))); return string.Concat(bytes.Select(item => item.ToString("x2"))); } internal static bool TryFindReusableGenerationInputExport( string datasetRoot, string sourceFingerprint, out string exportDirectory, out string error) { exportDirectory = string.Empty; error = string.Empty; try { if (string.IsNullOrWhiteSpace(datasetRoot) || string.IsNullOrWhiteSpace(sourceFingerprint) || !Directory.Exists(datasetRoot)) { error = "No reusable generation input directory is available."; return false; } var candidates = Directory .EnumerateDirectories( Path.GetFullPath(datasetRoot), $"{GenerationInputDirectoryPrefix}*", SearchOption.TopDirectoryOnly) .OrderByDescending( Path.GetFileName, StringComparer.Ordinal) .ToArray(); foreach (var candidate in candidates) { if (!TryValidateReusableGenerationInputExport( candidate, sourceFingerprint, out _)) { continue; } exportDirectory = Path.GetFullPath(candidate); return true; } error = "No complete generation input matches the current source."; return false; } catch (Exception exception) { exportDirectory = string.Empty; error = exception.Message; return false; } } internal static bool TryValidateReusableGenerationInputExport( string candidateDirectory, string sourceFingerprint, out string error) { error = string.Empty; try { if (string.IsNullOrWhiteSpace(candidateDirectory) || string.IsNullOrWhiteSpace(sourceFingerprint) || !Directory.Exists(candidateDirectory)) { error = "The candidate directory or source fingerprint is invalid."; return false; } var candidateRoot = Path.GetFullPath(candidateDirectory); var manifestPath = Path.Combine( candidateRoot, "dataset_manifest.json"); if (!File.Exists(manifestPath)) { error = "dataset_manifest.json is missing."; return false; } var manifest = JsonUtility.FromJson( File.ReadAllText(manifestPath, Encoding.UTF8)); if (manifest == null || !string.Equals( manifest.schemaVersion, SchemaVersion, StringComparison.Ordinal) || !string.Equals( manifest.role, GenerationInputKind, StringComparison.Ordinal) || !string.Equals( manifest.inputKind, GenerationInputKind, StringComparison.Ordinal) || manifest.hasAuthoredCamera || manifest.sampleRate != SampleRate || manifest.discoveredCameraTrackCount != 0 || manifest.exportedCameraTrackCount != 0 || !string.Equals( manifest.sourceFingerprint, sourceFingerprint, StringComparison.Ordinal) || manifest.sharedBones == null || manifest.sharedBones.Count == 0 || manifest.songs == null || manifest.songs.Count != 1) { error = "The manifest is not a matching generation input export."; return false; } if (manifest.skippedCameraTracks != null && manifest.skippedCameraTracks.Count != 0) { error = "The generation input manifest contains camera tracks."; return false; } var song = manifest.songs[0]; if (!ValidateGenerationInputSongMetadata( song, manifest.sharedBones.Count, out error) || !TryResolveChildPath( candidateRoot, song.folderName, out var songDirectory)) { if (string.IsNullOrWhiteSpace(error)) { error = "The song directory path is invalid."; } return false; } if (!ValidateFileSize( songDirectory, song.jointsFile, checked((long)song.frameCount * song.sharedBoneCount * 3 * sizeof(float)), out error) || !ValidateFileSize( songDirectory, song.cameraFile, checked((long)song.frameCount * 9 * sizeof(float)), out error) || !ValidateFileSize( songDirectory, song.rootFile, checked((long)song.frameCount * 14 * sizeof(float)), out error) || !ValidateFileSize( songDirectory, song.shotIndexFile, checked((long)song.frameCount * sizeof(int)), out error) || !ValidateFileSize( songDirectory, song.timeFile, checked((long)song.frameCount * sizeof(double)), out error) || !ValidateFileSize( songDirectory, song.audioFeaturesFile, checked((long)song.frameCount * 2 * sizeof(float)), out error) || !ValidateRequiredFile( songDirectory, song.previewFile, out error) || !ValidateRequiredFile( songDirectory, song.shotsFile, out error) || !ValidateRequiredFile( songDirectory, "metadata.json", out error) || !ValidateRequiredFile( songDirectory, song.audioFile, out error)) { return false; } if (!ValidateFileSha256( songDirectory, song.jointsFile, song.jointsFingerprint, out error) || !ValidateFileSha256( songDirectory, song.cameraFile, song.cameraFingerprint, out error) || !ValidateFileSha256( songDirectory, song.rootFile, song.rootFingerprint, out error) || !ValidateFileSha256( songDirectory, song.shotIndexFile, song.shotIndexFingerprint, out error) || !ValidateFileSha256( songDirectory, song.timeFile, song.timeFingerprint, out error) || !ValidateFileSha256( songDirectory, song.audioFeaturesFile, song.audioFeaturesFingerprint, out error) || !ValidateFileSha256( songDirectory, song.audioFile, song.audioFingerprint, out error)) { return false; } if (!TryResolveChildPath( songDirectory, "metadata.json", out var metadataPath) || !TryResolveChildPath( songDirectory, song.shotsFile, out var shotsPath)) { error = "A generation input file path is invalid."; return false; } var metadata = JsonUtility.FromJson( File.ReadAllText(metadataPath, Encoding.UTF8)); if (!GenerationInputMetadataMatches(song, metadata)) { error = "metadata.json does not match dataset_manifest.json."; return false; } var shots = JsonUtility.FromJson( File.ReadAllText(shotsPath, Encoding.UTF8)); if (shots?.shots == null || shots.shots.Count != 0) { error = "A generation input must contain no authored shots."; return false; } if (!ValidateGenerationInputPlaceholders( songDirectory, song, out error)) { return false; } return true; } catch (Exception exception) { error = exception.Message; return false; } } private static bool ValidateGenerationInputSongMetadata( SongMetadata song, int manifestBoneCount, out string error) { if (song == null || !string.Equals( song.role, GenerationInputKind, StringComparison.Ordinal) || !string.Equals( song.inputKind, GenerationInputKind, StringComparison.Ordinal) || song.hasAuthoredCamera || song.sampleRate != SampleRate || song.frameCount <= 0 || song.sharedBoneCount <= 0 || song.sharedBoneCount != manifestBoneCount || song.shotCount != 0 || song.missingCameraFrames != song.frameCount || song.missingJointSamples != 0 || song.missingRootSamples != 0 || string.IsNullOrWhiteSpace(song.folderName) || string.IsNullOrWhiteSpace(song.jointsFile) || string.IsNullOrWhiteSpace(song.cameraFile) || string.IsNullOrWhiteSpace(song.rootFile) || string.IsNullOrWhiteSpace(song.shotIndexFile) || string.IsNullOrWhiteSpace(song.timeFile) || string.IsNullOrWhiteSpace(song.shotsFile) || string.IsNullOrWhiteSpace(song.previewFile) || string.IsNullOrWhiteSpace(song.audioFeaturesFile) || string.IsNullOrWhiteSpace(song.audioFile) || !IsSha256(song.audioFingerprint) || !IsSha256(song.jointsFingerprint) || !IsSha256(song.cameraFingerprint) || !IsSha256(song.rootFingerprint) || !IsSha256(song.shotIndexFingerprint) || !IsSha256(song.timeFingerprint) || !IsSha256(song.audioFeaturesFingerprint)) { error = "The generation input song metadata is incomplete."; return false; } error = string.Empty; return true; } private static bool GenerationInputMetadataMatches( SongMetadata manifestSong, SongMetadata metadata) { return metadata != null && string.Equals( metadata.role, GenerationInputKind, StringComparison.Ordinal) && string.Equals( metadata.inputKind, GenerationInputKind, StringComparison.Ordinal) && !metadata.hasAuthoredCamera && metadata.sampleRate == manifestSong.sampleRate && metadata.frameCount == manifestSong.frameCount && metadata.sharedBoneCount == manifestSong.sharedBoneCount && metadata.shotCount == manifestSong.shotCount && metadata.missingCameraFrames == manifestSong.missingCameraFrames && metadata.missingJointSamples == manifestSong.missingJointSamples && metadata.missingRootSamples == manifestSong.missingRootSamples && string.Equals( metadata.folderName, manifestSong.folderName, StringComparison.Ordinal) && string.Equals( metadata.jointsFile, manifestSong.jointsFile, StringComparison.Ordinal) && string.Equals( metadata.jointsFingerprint, manifestSong.jointsFingerprint, StringComparison.OrdinalIgnoreCase) && string.Equals( metadata.cameraFile, manifestSong.cameraFile, StringComparison.Ordinal) && string.Equals( metadata.cameraFingerprint, manifestSong.cameraFingerprint, StringComparison.OrdinalIgnoreCase) && string.Equals( metadata.rootFile, manifestSong.rootFile, StringComparison.Ordinal) && string.Equals( metadata.rootFingerprint, manifestSong.rootFingerprint, StringComparison.OrdinalIgnoreCase) && string.Equals( metadata.shotIndexFile, manifestSong.shotIndexFile, StringComparison.Ordinal) && string.Equals( metadata.shotIndexFingerprint, manifestSong.shotIndexFingerprint, StringComparison.OrdinalIgnoreCase) && string.Equals( metadata.timeFile, manifestSong.timeFile, StringComparison.Ordinal) && string.Equals( metadata.timeFingerprint, manifestSong.timeFingerprint, StringComparison.OrdinalIgnoreCase) && string.Equals( metadata.shotsFile, manifestSong.shotsFile, StringComparison.Ordinal) && string.Equals( metadata.previewFile, manifestSong.previewFile, StringComparison.Ordinal) && string.Equals( metadata.audioFeaturesFile, manifestSong.audioFeaturesFile, StringComparison.Ordinal) && string.Equals( metadata.audioFeaturesFingerprint, manifestSong.audioFeaturesFingerprint, StringComparison.OrdinalIgnoreCase) && string.Equals( metadata.audioFile, manifestSong.audioFile, StringComparison.Ordinal) && string.Equals( metadata.audioFingerprint, manifestSong.audioFingerprint, StringComparison.OrdinalIgnoreCase); } private static bool ValidateGenerationInputPlaceholders( string songDirectory, SongMetadata song, out string error) { if (!TryResolveChildPath( songDirectory, song.cameraFile, out var cameraPath) || !TryResolveChildPath( songDirectory, song.shotIndexFile, out var shotIndexPath)) { error = "A placeholder file path is invalid."; return false; } using (var cameraReader = new BinaryReader(File.OpenRead(cameraPath))) { for (var index = 0; index < 9; index++) { if (!float.IsNaN(cameraReader.ReadSingle())) { error = "camera.f32 is not a generation-input placeholder."; return false; } } } using (var shotReader = new BinaryReader(File.OpenRead(shotIndexPath))) { if (shotReader.ReadInt32() != -1) { error = "shot_index.i32 is not a generation-input placeholder."; return false; } if (song.frameCount > 1) { shotReader.BaseStream.Seek(-sizeof(int), SeekOrigin.End); if (shotReader.ReadInt32() != -1) { error = "shot_index.i32 has an invalid final placeholder."; return false; } } } error = string.Empty; return true; } private static bool ValidateFileSize( string directory, string relativePath, long expectedBytes, out string error) { if (!TryResolveChildPath( directory, relativePath, out var fullPath) || !File.Exists(fullPath)) { error = $"Required file '{relativePath}' is missing."; return false; } var actualBytes = new FileInfo(fullPath).Length; if (actualBytes != expectedBytes) { error = $"File '{relativePath}' has {actualBytes} bytes; " + $"expected {expectedBytes}."; return false; } error = string.Empty; return true; } private static bool ValidateRequiredFile( string directory, string relativePath, out string error) { if (!TryResolveChildPath( directory, relativePath, out var fullPath) || !File.Exists(fullPath) || new FileInfo(fullPath).Length == 0) { error = $"Required file '{relativePath}' is missing or empty."; return false; } error = string.Empty; return true; } private static bool ValidateFileSha256( string directory, string relativePath, string expectedFingerprint, out string error) { if (!IsSha256(expectedFingerprint) || !TryResolveChildPath( directory, relativePath, out var fullPath) || !File.Exists(fullPath)) { error = $"Required fingerprint for '{relativePath}' is missing."; return false; } var actualFingerprint = ComputeFileSha256(fullPath); if (!string.Equals( actualFingerprint, expectedFingerprint, StringComparison.OrdinalIgnoreCase)) { error = $"File fingerprint for '{relativePath}' does not match."; return false; } error = string.Empty; return true; } private static bool TryResolveChildPath( string directory, string relativePath, out string fullPath) { fullPath = string.Empty; if (string.IsNullOrWhiteSpace(directory) || string.IsNullOrWhiteSpace(relativePath) || Path.IsPathRooted(relativePath)) { return false; } var root = Path.GetFullPath(directory) .TrimEnd( Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; var candidate = Path.GetFullPath(Path.Combine(root, relativePath)); var comparison = Path.DirectorySeparatorChar == '\\' ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; if (!candidate.StartsWith(root, comparison)) { return false; } fullPath = candidate; return true; } private static bool IsSha256(string value) { return !string.IsNullOrWhiteSpace(value) && value.Length == 64 && value.All(character => (character >= '0' && character <= '9') || (character >= 'a' && character <= 'f') || (character >= 'A' && character <= 'F')); } private static BinaryWriter CreateBinaryWriter(string path) { return new BinaryWriter(new FileStream( path, FileMode.Create, FileAccess.Write, FileShare.None, 1024 * 1024, FileOptions.SequentialScan)); } private static string ComputeFileSha256(string path) { using var stream = new FileStream( path, FileMode.Open, FileAccess.Read, FileShare.Read, 1024 * 1024, FileOptions.SequentialScan); using var sha256 = SHA256.Create(); return string.Concat( sha256.ComputeHash(stream) .Select(value => value.ToString("x2"))); } private static void WriteJson(string path, T value) { File.WriteAllText(path, JsonUtility.ToJson(value, true)); } private static void WriteVector3(BinaryWriter writer, Vector3 value) { writer.Write(value.x); writer.Write(value.y); writer.Write(value.z); } private static void WriteQuaternion(BinaryWriter writer, Quaternion value) { writer.Write(value.x); writer.Write(value.y); writer.Write(value.z); writer.Write(value.w); } private static void WriteInvalidVector3(BinaryWriter writer) { for (var index = 0; index < 3; index++) { writer.Write(float.NaN); } } private static void WriteInvalidQuaternion(BinaryWriter writer) { for (var index = 0; index < 4; index++) { writer.Write(float.NaN); } } private static string GetHierarchyPath(Transform transform) { var names = new List(); while (transform != null) { names.Add(transform.name); transform = transform.parent; } names.Reverse(); return string.Join("/", names); } private static string SanitizeFileName(string value) { var invalidCharacters = Path.GetInvalidFileNameChars(); var sanitized = new string(value .Select(character => invalidCharacters.Contains(character) ? '_' : character) .ToArray()); return string.IsNullOrWhiteSpace(sanitized) ? "Song" : sanitized; } private static string EscapeCsv(string value) { if (!value.Contains(",") && !value.Contains("\"") && !value.Contains("\r") && !value.Contains("\n")) { return value; } return $"\"{value.Replace("\"", "\"\"")}\""; } private sealed class SongContext { public string SongName; public Transform Root; public PlayableDirector MainDirector; public PlayableDirector MotionDirector; public TimelineAsset MainTimeline; public TimelineAsset MotionTimeline; public Animator CharacterAnimator; public CinemachineTrack CameraTrack; public int CameraTrackIndex; public CinemachineBrain CameraBrain; } private sealed class JointDefinition { public string Name; public HumanBodyBones? HumanBone; public string RelativePath; } private sealed class ShotRuntime { public TimelineClip Clip; public double Start; public double End; public CinemachineVirtualCameraBase VirtualCamera; } private sealed class CameraSample { public bool IsValid; public ShotRuntime DominantShot; public CameraState State; } private sealed class AudioInfo { public AudioClip Clip; public string AssetPath; public double Start; public double Duration; public double ClipIn; public double TimeScale; } [Serializable] private sealed class SkippedCameraTrack { public string timelineAssetPath; public string trackName; public bool muted; public string reason; } [Serializable] private sealed class DatasetManifest { public string schemaVersion; public string role; public string inputKind; public bool hasAuthoredCamera; public string createdUtc; public string unityVersion; public string scenePath; public int sampleRate; public string byteOrder; public string floatFormat; public string timeFormat; public string jointLayout; public string cameraLayout; public string rootLayout; public string audioFeatureLayout; public string cameraSamplingPolicy; public string sourceFingerprintSchema; public string sourceFingerprint; public int discoveredCameraTrackCount; public int exportedCameraTrackCount; public List skippedCameraTracks; public List sharedBones; public List songs; } [Serializable] private sealed class SongMetadata { public string songName; public string role; public string inputKind; public bool hasAuthoredCamera; public string folderName; public string mainTimelineAssetPath; public string motionTimelineAssetPath; public string characterPath; public string cameraTrackName; public int cameraTrackIndex; public bool cameraTrackMuted; public string audioAssetPath; public string audioFile; public string audioFingerprint; public double audioStart; public double audioDuration; public double mainDuration; public double motionDuration; public int sampleRate; public double sampleStart; public double sampleEndExclusive; public int frameCount; public int sharedBoneCount; public string skeletonType; public int shotCount; public int missingCameraFrames; public int missingJointSamples; public int missingRootSamples; public string jointsFile; public string jointsFingerprint; public string cameraFile; public string cameraFingerprint; public string rootFile; public string rootFingerprint; public string shotIndexFile; public string shotIndexFingerprint; public string timeFile; public string timeFingerprint; public string shotsFile; public string previewFile; public string audioFeaturesFile; public string audioFeaturesFingerprint; public string audioFeatureLayout; public string audioFeaturesStatus; } [Serializable] private sealed class ShotFile { public string timelineName; public List shots; } [Serializable] private sealed class ShotInfo { public int index; public string clipName; public string cameraName; public string cameraPath; public double start; public double end; public double duration; public double easeInDuration; public double easeOutDuration; } } }