using System; using System.IO; using UnityEngine; namespace OnDeviceAgent.Inference { // Resolves a "Model//..." relative path to a concrete file path. In a player build it resolves under // Application.streamingAssetsPath; in the Editor the weights live only in the com.sky.sentis.* packages // (Models~), so the path is redirected there and nothing is duplicated into Assets/StreamingAssets. The // build step copies the package payloads into StreamingAssets so the player ships them. public static class ModelPathResolver { public static string GetModelFilePath(string relativePath) { var rel = (relativePath ?? string.Empty).Replace('\\', '/').TrimStart('/'); #if UNITY_EDITOR var packagePath = TryResolveInPackage(rel); if (packagePath != null) return packagePath; #endif return Path.Combine(Application.streamingAssetsPath, rel.Replace('/', Path.DirectorySeparatorChar)); } #if UNITY_EDITOR // StreamingAssets subfolder -> owning package. public static readonly (string streamingAssetsSubdir, string packageName)[] PackageMap = { ("Model/E5", "com.sky.sentis.e5"), ("Model/OpenWakeWord", "com.sky.sentis.openwakeword"), ("Model/SileroVad", "com.sky.sentis.silero-vad"), ("Model/Supertonic", "com.sky.sentis.supertonic"), ("Model/Whisper", "com.sky.sentis.whisper"), ("Model/YOLO", "com.sky.sentis.yolox"), }; // Physical root of a package. Embedded packages live at /Packages/, but git and // registry dependencies resolve into Library/PackageCache/@, so the location is // queried from the Package Manager and only falls back to the project-relative path. static string ResolvePackageRoot(string pkg) { var info = UnityEditor.PackageManager.PackageInfo.FindForAssetPath("Packages/" + pkg); if (info != null && !string.IsNullOrEmpty(info.resolvedPath)) return info.resolvedPath; return Path.GetFullPath(Path.Combine("Packages", pkg)); } static string TryResolveInPackage(string rel) { foreach (var (sub, pkg) in PackageMap) { if (rel != sub && !rel.StartsWith(sub + "/", StringComparison.Ordinal)) continue; var tail = rel.Substring(sub.Length).TrimStart('/'); var full = Path.Combine(ResolvePackageRoot(pkg), "Models~", tail.Replace('/', Path.DirectorySeparatorChar)); if (File.Exists(full) || Directory.Exists(full)) return full; return null; // package is known but the file is missing; fall back to StreamingAssets } return null; } // Copies every package's Models~ payload into Assets/StreamingAssets/ so a player build // ships the weights. Called by the build pre-process step. public static void ProvisionToStreamingAssets() { foreach (var (sub, pkg) in PackageMap) { var src = Path.Combine(ResolvePackageRoot(pkg), "Models~"); if (!Directory.Exists(src)) continue; var dst = Path.Combine(Application.streamingAssetsPath, sub.Replace('/', Path.DirectorySeparatorChar)); CopyDir(src, dst); WriteFilesManifest(dst); } } // Lists every copied file under dst (relative, forward slashes), so the Android runtime can stage the // tree out of the APK without directory enumeration. Excludes .meta and the manifest itself. const string FilesManifestFile = "files_manifest.txt"; static void WriteFilesManifest(string dst) { if (!Directory.Exists(dst)) return; var entries = new System.Collections.Generic.List(); foreach (var file in Directory.GetFiles(dst, "*", SearchOption.AllDirectories)) { if (file.EndsWith(".meta", StringComparison.Ordinal)) continue; var rel = file.Substring(dst.Length).TrimStart(Path.DirectorySeparatorChar, '/').Replace('\\', '/'); if (rel == FilesManifestFile) continue; entries.Add(rel); } entries.Sort(StringComparer.Ordinal); File.WriteAllText(Path.Combine(dst, FilesManifestFile), string.Join("\n", entries) + "\n"); } // Deletes Assets/StreamingAssets/Model so the weights never linger in the project tree. public static void RemoveFromStreamingAssets() { var modelRoot = Path.Combine(Application.streamingAssetsPath, "Model"); if (Directory.Exists(modelRoot)) Directory.Delete(modelRoot, true); var meta = modelRoot + ".meta"; if (File.Exists(meta)) File.Delete(meta); } static void CopyDir(string src, string dst) { Directory.CreateDirectory(dst); foreach (var file in Directory.GetFiles(src)) { if (file.EndsWith(".meta", StringComparison.Ordinal)) continue; File.Copy(file, Path.Combine(dst, Path.GetFileName(file)), true); } foreach (var dir in Directory.GetDirectories(src)) CopyDir(dir, Path.Combine(dst, Path.GetFileName(dir))); } #endif } }