InferenceWorker/HandModelDownloads.cs
using HumanoidMocap.Inference;
using System.Security.Cryptography;
using System.Text.Json;

namespace HumanoidMocap.Worker;

/// <summary>Only the explicitly selected backend and its crop detector are downloaded.</summary>
public static class HandModelDownloads
{
    public sealed record Asset(string Path,string Url,long Bytes,string Sha256);
    static readonly Asset Detector=new("hand_landmarker.task",
        "https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task",
        7819105,"fbc2a30080c3c557093b5ddfc334698132eb341044ccee322ccf8bcf3607cde1");
    static readonly Asset WildHands=new("wildhands/wildhands.ckpt",
        "https://drive.usercontent.google.com/download?id=1FJWBrMmTKjKAo6j5DQS1KYpqqFbAbJ9Q&export=download&confirm=t",
        855094722,"cac3f9a9334da852f3993e95b4ec088dcc6c69f0337db63dacd83e4642880a7b");
    static readonly Asset Wilor=new("wilor/wilor_final.ckpt",
        "https://huggingface.co/spaces/rolpotamias/WiLoR/resolve/99fe3d7acff8104ecca1055df7467709506c2fa6/pretrained_models/wilor_final.ckpt",
        2564989533,"3e97aafc7dd08d883a4cc5a027df61fdb6fda6136dbd1319405413862ada6bb2");
    static readonly Asset MobileHand=new("mobilehand/hmr_model_freihand_auc.pth",
        "https://raw.githubusercontent.com/gmntu/mobilehand/51c112364013b803c38955b55a1572b0d402894c/model/hmr_model_freihand_auc.pth",
        15152098,MobileHandModel.CheckpointSha256);

    public static async Task Ensure(string folder,string backend,CancellationToken token)
    {
        var assets=backend switch{"mediapipe"=>new[]{Detector},"mobilehand"=>new[]{Detector,MobileHand},"wildhands"=>new[]{Detector,WildHands},"wilor"=>new[]{Detector,Wilor},_=>throw new NotSupportedException("Select MediaPipe, MobileHand, WildHands or WiLoR. ACE is not downloaded or loaded by this worker.")};
        using var http=new HttpClient{Timeout=TimeSpan.FromHours(1)};
        foreach(var asset in assets)
        {
            var path=Path.Combine(folder,asset.Path);Directory.CreateDirectory(Path.GetDirectoryName(path)!);
            if(!File.Exists(path))await ModelDownload.Fetch(http,asset.Url,path,asset.Bytes,asset.Sha256,Console.WriteLine,token);
            else await Verify(path,asset,token);
            Console.WriteLine("Verified "+Path.GetFileName(path));
        }
        File.WriteAllText(Path.Combine(folder,backend+"-models.json"),JsonSerializer.Serialize(new{backend,assets,verifiedUtc=DateTime.UtcNow},new JsonSerializerOptions{WriteIndented=true}));
    }
    static async Task Verify(string path,Asset asset,CancellationToken token)
    {
        if(new FileInfo(path).Length!=asset.Bytes)throw new InvalidDataException("Unexpected model size; original preserved: "+path);
        var hash=await Task.Run(()=>FileChecksum.Sha256(path),token);
        if(!hash.Equals(asset.Sha256,StringComparison.OrdinalIgnoreCase))throw new InvalidDataException("Model checksum mismatch; original preserved: "+path);
    }
}