using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Newtonsoft.Json.Linq; using SwarmUI.Accounts; namespace Mrleo1nid.SwarmAssistent; /// Queue of models the assistant wants downloaded (merged into gpu-rent models.yaml on next up/capture). public partial class SwarmAssistentExtension { static readonly object WantedFileLock = new(); string WantedModelsPath() => Path.Combine(DataRoot(), ".gpu-rent-wanted-models.yaml"); string WantedCardsDir() => Path.Combine(DataRoot(), ".gpu-rent-wanted-cards"); public async Task AssistentEnqueueWanted(Session session, string kind, string url, int version_id = 0, string title = null, JObject card = null) { await Task.CompletedTask; kind = (kind ?? "lora").Trim().ToLowerInvariant(); if (kind is not ("lora" or "checkpoint" or "vae" or "embedding" or "controlnet" or "upscaler" or "clip")) { kind = "lora"; } url = (url ?? "").Trim(); if (string.IsNullOrWhiteSpace(url) && version_id > 0) { url = $"https://civitai.red/models/0?modelVersionId={version_id}"; } if (string.IsNullOrWhiteSpace(url)) { return new JObject { ["error"] = "url or version_id required" }; } if (version_id <= 0) { Match m = Regex.Match(url, @"modelVersionId=(\d+)", RegexOptions.IgnoreCase); if (m.Success) { version_id = int.Parse(m.Groups[1].Value); } } string path = WantedModelsPath(); Directory.CreateDirectory(Path.GetDirectoryName(path) ?? DataRoot()); lock (WantedFileLock) { Dictionary> sections = LoadWantedYaml(File.Exists(path) ? File.ReadAllText(path, Encoding.UTF8) : ""); if (version_id > 0) { foreach (List list in sections.Values) { if (list.Any(e => e.VersionId == version_id)) { return new JObject { ["success"] = true, ["already"] = true, ["path"] = path, ["version_id"] = version_id }; } } } else { foreach (List list in sections.Values) { if (list.Any(e => string.Equals(e.Url, url, StringComparison.OrdinalIgnoreCase))) { return new JObject { ["success"] = true, ["already"] = true, ["path"] = path }; } } } if (!sections.TryGetValue(kind, out List bucket)) { bucket = []; sections[kind] = bucket; } bucket.Add(new WantedEntry { Url = url, Title = title, VersionId = version_id }); File.WriteAllText(path, WriteWantedYaml(sections), Encoding.UTF8); } if (card is not null && version_id > 0) { Directory.CreateDirectory(WantedCardsDir()); string draft = Path.Combine(WantedCardsDir(), $"{version_id}.assistent.json"); File.WriteAllText(draft, card.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8); } return new JObject { ["success"] = true, ["path"] = path, ["version_id"] = version_id }; } sealed class WantedEntry { public string Url; public string Title; public int VersionId; } static Dictionary> LoadWantedYaml(string raw) { Dictionary> sections = new(StringComparer.OrdinalIgnoreCase); string currentKind = null; WantedEntry cur = null; void Flush() { if (cur is null || string.IsNullOrWhiteSpace(cur.Url) || string.IsNullOrWhiteSpace(currentKind)) { cur = null; return; } if (!sections.TryGetValue(currentKind, out List list)) { list = []; sections[currentKind] = list; } list.Add(cur); cur = null; } foreach (string line in (raw ?? "").Split('\n')) { string t = line.TrimEnd(); if (string.IsNullOrWhiteSpace(t) || t.TrimStart().StartsWith('#')) { continue; } Match kindLine = Regex.Match(t, @"^([A-Za-z0-9_-]+):\s*$"); if (kindLine.Success && !t.TrimStart().StartsWith('-')) { Flush(); currentKind = kindLine.Groups[1].Value.Trim().ToLowerInvariant(); continue; } Match urlLine = Regex.Match(t, @"^\s*-\s*url:\s*[""']?(.+?)[""']?\s*$"); if (urlLine.Success) { Flush(); cur = new WantedEntry { Url = urlLine.Groups[1].Value.Trim() }; continue; } if (cur is null) { continue; } Match titleLine = Regex.Match(t, @"^\s*title:\s*[""']?(.+?)[""']?\s*$"); if (titleLine.Success) { cur.Title = titleLine.Groups[1].Value.Trim(); continue; } Match vidLine = Regex.Match(t, @"^\s*version_id:\s*(\d+)\s*$"); if (vidLine.Success && int.TryParse(vidLine.Groups[1].Value, out int vid)) { cur.VersionId = vid; } } Flush(); return sections; } static string WriteWantedYaml(Dictionary> sections) { StringBuilder sb = new(); sb.AppendLine("# Assistent wanted queue — merged into local models.yaml on gpu-rent up/capture"); string[] order = ["checkpoint", "lora", "vae", "embedding", "controlnet", "upscaler", "clip"]; HashSet seen = new(StringComparer.OrdinalIgnoreCase); foreach (string kind in order.Concat(sections.Keys.OrderBy(k => k, StringComparer.OrdinalIgnoreCase))) { if (!seen.Add(kind) || !sections.TryGetValue(kind, out List list) || list.Count == 0) { continue; } sb.AppendLine($"{kind}:"); foreach (WantedEntry e in list) { sb.AppendLine($" - url: \"{e.Url.Replace("\"", "%22")}\""); if (!string.IsNullOrWhiteSpace(e.Title)) { sb.AppendLine($" title: \"{e.Title.Replace("\"", "'")}\""); } if (e.VersionId > 0) { sb.AppendLine($" version_id: {e.VersionId}"); } } } return sb.ToString(); } }