Ship Assistent 0.14.0: chat sessions, ask-only hops, and context compression.
Per-chat Generate session with sparse deltas; drop Cards/Civitai/wanted hops; rolling history summary via the same Ollama model with a budget chip and /compress. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+4
-631
@@ -2,22 +2,16 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using FreneticUtilities.FreneticExtensions;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SwarmUI.Accounts;
|
||||
using SwarmUI.Core;
|
||||
using SwarmUI.Text2Image;
|
||||
using SwarmUI.Utils;
|
||||
using SwarmUI.WebAPI;
|
||||
|
||||
namespace Mrleo1nid.SwarmAssistent;
|
||||
|
||||
/// <summary>Server-side model inventory, assistant cards and Civitai lookups.</summary>
|
||||
/// <summary>Server-side model inventory (LoRA / checkpoint / wildcard lists).</summary>
|
||||
public partial class SwarmAssistentExtension
|
||||
{
|
||||
const int MaxLorasInInventoryFallback = 150;
|
||||
@@ -25,444 +19,6 @@ public partial class SwarmAssistentExtension
|
||||
const int MaxCheckpointsInInventoryFallback = 60;
|
||||
const int InventoryBlurbMaxFallback = 140;
|
||||
|
||||
static string ModelWeightPath(string setName, string modelName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(modelName) || !Program.T2IModelSets.TryGetValue(setName, out T2IModelHandler handler))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (!handler.Models.TryGetValue(modelName, out T2IModel model) && !handler.Models.TryGetValue(modelName.Replace('\\', '/'), out model))
|
||||
{
|
||||
// Try suffix match
|
||||
model = handler.Models.Values.FirstOrDefault(m =>
|
||||
string.Equals(m.Name, modelName, StringComparison.OrdinalIgnoreCase)
|
||||
|| m.Name.EndsWith("/" + modelName, StringComparison.OrdinalIgnoreCase)
|
||||
|| Path.GetFileNameWithoutExtension(m.Name) == Path.GetFileNameWithoutExtension(modelName));
|
||||
}
|
||||
if (model is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
try
|
||||
{
|
||||
// SwarmUI T2IModel exposes RawFilePath in recent builds.
|
||||
return model.RawFilePath;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static string CardPathForWeight(string weightPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(weightPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
string dir = Path.GetDirectoryName(weightPath);
|
||||
string stem = Path.GetFileNameWithoutExtension(weightPath);
|
||||
if (string.IsNullOrWhiteSpace(dir) || string.IsNullOrWhiteSpace(stem))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return Path.Combine(dir, $"{stem}.assistent.json");
|
||||
}
|
||||
|
||||
static string SetNameForKind(string kind)
|
||||
{
|
||||
return (kind ?? "").Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"lora" => "LoRA",
|
||||
"checkpoint" or "ckpt" or "stable-diffusion" => "Stable-Diffusion",
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
JObject ReadCardObject(string kind, string name)
|
||||
{
|
||||
string set = SetNameForKind(kind);
|
||||
string weight = ModelWeightPath(set, name);
|
||||
string card = CardPathForWeight(weight);
|
||||
if (card is null || !File.Exists(card))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
try
|
||||
{
|
||||
return JObject.Parse(File.ReadAllText(card, Encoding.UTF8));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<JObject> AssistentGetCard(Session session, string kind, string name)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
if (string.IsNullOrWhiteSpace(kind) || string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return new JObject { ["error"] = "kind and name required" };
|
||||
}
|
||||
JObject card = ReadCardObject(kind, name);
|
||||
string set = SetNameForKind(kind);
|
||||
string weight = ModelWeightPath(set, name);
|
||||
return new JObject
|
||||
{
|
||||
["success"] = true,
|
||||
["kind"] = kind,
|
||||
["name"] = name,
|
||||
["has_card"] = card is not null,
|
||||
["weight_path"] = weight,
|
||||
["card"] = card,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<JObject> AssistentSaveCard(Session session, string kind, string name, JObject card, bool enqueue_wanted = false)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
if (card is null)
|
||||
{
|
||||
return new JObject { ["error"] = "card required" };
|
||||
}
|
||||
kind = (kind ?? card["kind"]?.ToString() ?? "").Trim();
|
||||
name = (name ?? card["name"]?.ToString() ?? "").Trim();
|
||||
if (string.IsNullOrWhiteSpace(kind) || string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return new JObject { ["error"] = "kind and name required" };
|
||||
}
|
||||
card["kind"] = kind;
|
||||
card["name"] = name;
|
||||
|
||||
string set = SetNameForKind(kind);
|
||||
string weight = ModelWeightPath(set, name);
|
||||
if (!string.IsNullOrWhiteSpace(weight) && File.Exists(weight))
|
||||
{
|
||||
string path = CardPathForWeight(weight);
|
||||
File.WriteAllText(path, card.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8);
|
||||
_ = IngestCardToMemory(card, name);
|
||||
return new JObject { ["success"] = true, ["path"] = path, ["installed"] = true };
|
||||
}
|
||||
|
||||
// Not installed — draft into wanted-cards + optionally enqueue download for next up.
|
||||
Directory.CreateDirectory(WantedCardsDir());
|
||||
string rawVid = card["version_id"]?.ToString() ?? "draft";
|
||||
string vid = Regex.IsMatch(rawVid, @"^\d+$") ? rawVid : "draft";
|
||||
string draft = Path.Combine(WantedCardsDir(), $"{vid}.assistent.json");
|
||||
File.WriteAllText(draft, card.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8);
|
||||
if (enqueue_wanted || !string.IsNullOrWhiteSpace(card["civitai_url"]?.ToString()))
|
||||
{
|
||||
await AssistentEnqueueWanted(session, kind, card["civitai_url"]?.ToString(), card["version_id"]?.Value<int?>() ?? 0, card["title"]?.ToString() ?? name, card);
|
||||
}
|
||||
_ = IngestCardToMemory(card, name);
|
||||
return new JObject { ["success"] = true, ["path"] = draft, ["installed"] = false, ["wanted"] = true };
|
||||
}
|
||||
|
||||
async Task IngestCardToMemory(JObject card, string name)
|
||||
{
|
||||
if (Memory is null || card is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
string kind = (card["kind"]?.ToString() ?? "lora").Trim().ToLowerInvariant();
|
||||
string key = (card["name"]?.ToString() ?? name ?? "").Trim();
|
||||
List<string> bits = [];
|
||||
foreach (string field in new[] { "when", "avoid", "prompt_hint", "notes" })
|
||||
{
|
||||
string v = card[field]?.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(v))
|
||||
{
|
||||
bits.Add($"{field}: {v.Trim()}");
|
||||
}
|
||||
}
|
||||
if (card["triggers"] is JArray tr)
|
||||
{
|
||||
string joined = string.Join(", ", tr.Select(t => t?.ToString()).Where(s => !string.IsNullOrWhiteSpace(s)));
|
||||
if (!string.IsNullOrWhiteSpace(joined))
|
||||
{
|
||||
bits.Add("triggers: " + joined);
|
||||
}
|
||||
}
|
||||
if (bits.Count == 0 || string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return;
|
||||
}
|
||||
string text = $"{kind} {key}. " + string.Join(" ", bits);
|
||||
string baseUrl = NormalizeBaseUrl(Config.LoadSettings()["base_url"]?.ToString());
|
||||
string embedModel = Config.LoadSettings()["embed_model"]?.ToString()
|
||||
?? Config.LoadAssistant(Config.DefaultPersonaId())["embed_model"]?.ToString();
|
||||
await Memory.UpsertTextAsync(baseUrl, "card", key, text, "user", card, embedModel, AssistentMemory.SharedPersona);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Debug($"IngestCardToMemory: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<JObject> AssistentGetCardMeta(Session session, string kind, string name, int version_id = 0, bool fetch = false)
|
||||
{
|
||||
string set = SetNameForKind(kind);
|
||||
string weight = ModelWeightPath(set, name);
|
||||
JObject civitai = null;
|
||||
JArray exampleUrls = [];
|
||||
JArray previewUrls = [];
|
||||
bool hasSidecar = false;
|
||||
string fetchError = null;
|
||||
bool fetched = false;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(weight))
|
||||
{
|
||||
string stem = Path.GetFileNameWithoutExtension(weight);
|
||||
string dir = Path.GetDirectoryName(weight);
|
||||
string side = Path.Combine(dir ?? "", $"{stem}.civitai.json");
|
||||
if (File.Exists(side))
|
||||
{
|
||||
hasSidecar = true;
|
||||
try
|
||||
{
|
||||
civitai = JObject.Parse(File.ReadAllText(side, Encoding.UTF8));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
foreach (string suffix in new[] { ".preview.jpg", ".preview.png", ".preview.jpeg", ".jpg", ".png", ".webp" })
|
||||
{
|
||||
string prev = Path.Combine(dir ?? "", stem + suffix);
|
||||
if (File.Exists(prev))
|
||||
{
|
||||
// Swarm View path — relative URL works in the same origin browser session.
|
||||
previewUrls.Add($"View/Models/{(kind == "lora" ? "Lora" : "Stable-Diffusion")}/{Path.GetFileName(prev)}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (civitai is not null)
|
||||
{
|
||||
if (version_id <= 0)
|
||||
{
|
||||
version_id = civitai["id"]?.Value<int?>() ?? 0;
|
||||
}
|
||||
CollectExampleUrls(civitai, exampleUrls);
|
||||
}
|
||||
|
||||
string hash = null;
|
||||
string trigger = null;
|
||||
try
|
||||
{
|
||||
if (Program.T2IModelSets.TryGetValue(set, out T2IModelHandler h)
|
||||
&& (h.Models.TryGetValue(name, out T2IModel m)
|
||||
|| h.Models.TryGetValue(name.Replace('\\', '/'), out m)))
|
||||
{
|
||||
trigger = m.Metadata?.TriggerPhrase;
|
||||
hash = m.Metadata?.Hash;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (fetch && civitai is null)
|
||||
{
|
||||
string apiKey = session.User.GetGenericData("civitai_api", "key") ?? "";
|
||||
if (string.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
fetchError = "Civitai: нет ключа в User Settings";
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
JObject remote = null;
|
||||
if (version_id > 0)
|
||||
{
|
||||
remote = await FetchCivitaiModelVersion(apiKey, version_id);
|
||||
}
|
||||
if (remote is null && !string.IsNullOrWhiteSpace(hash))
|
||||
{
|
||||
string sha = hash.Trim().ToLowerInvariant();
|
||||
if (sha.StartsWith("sha256:"))
|
||||
{
|
||||
sha = sha["sha256:".Length..];
|
||||
}
|
||||
if (sha.Length == 64)
|
||||
{
|
||||
remote = await FetchCivitaiByHash(apiKey, sha);
|
||||
}
|
||||
else
|
||||
{
|
||||
fetchError ??= "Civitai: хеш модели не SHA256";
|
||||
}
|
||||
}
|
||||
if (remote is not null)
|
||||
{
|
||||
civitai = remote;
|
||||
fetched = true;
|
||||
version_id = remote["id"]?.Value<int?>() ?? version_id;
|
||||
CollectExampleUrls(remote, exampleUrls);
|
||||
if (!string.IsNullOrWhiteSpace(weight))
|
||||
{
|
||||
try
|
||||
{
|
||||
string stem = Path.GetFileNameWithoutExtension(weight);
|
||||
string dir = Path.GetDirectoryName(weight);
|
||||
string side = Path.Combine(dir ?? "", $"{stem}.civitai.json");
|
||||
File.WriteAllText(side, remote.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8);
|
||||
hasSidecar = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Debug($"AssistentGetCardMeta write sidecar: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (fetchError is null)
|
||||
{
|
||||
fetchError = string.IsNullOrWhiteSpace(hash)
|
||||
? "Civitai: нет hash и version_id"
|
||||
: "Хеш не найден на Civitai";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
fetchError = $"Civitai: {ex.Message}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JObject card = ReadCardObject(kind, name);
|
||||
return new JObject
|
||||
{
|
||||
["success"] = true,
|
||||
["kind"] = kind,
|
||||
["name"] = name,
|
||||
["version_id"] = version_id,
|
||||
["trigger_phrase"] = trigger,
|
||||
["has_card"] = card is not null,
|
||||
["has_sidecar"] = hasSidecar,
|
||||
["fetched"] = fetched,
|
||||
["fetch_error"] = fetchError,
|
||||
["card"] = card,
|
||||
["civitai"] = civitai,
|
||||
["example_urls"] = exampleUrls,
|
||||
["preview_urls"] = previewUrls,
|
||||
["weight_path"] = weight,
|
||||
["hash"] = hash,
|
||||
};
|
||||
}
|
||||
|
||||
static void CollectExampleUrls(JObject civitai, JArray exampleUrls)
|
||||
{
|
||||
if (civitai?["images"] is not JArray imgs)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (JToken img in imgs.Take(6))
|
||||
{
|
||||
string u = img?["url"]?.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(u))
|
||||
{
|
||||
exampleUrls.Add(u);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async Task<JObject> FetchCivitaiByHash(string apiKey, string sha)
|
||||
{
|
||||
string[] hosts = ["civitai.red", "civitai.com"];
|
||||
Exception last = null;
|
||||
foreach (string host in hosts)
|
||||
{
|
||||
try
|
||||
{
|
||||
string url = $"https://{host}/api/v1/model-versions/by-hash/{sha}";
|
||||
using HttpRequestMessage req = new(HttpMethod.Get, url);
|
||||
if (!string.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey.Trim());
|
||||
}
|
||||
using HttpResponseMessage resp = await HttpClient.SendAsync(req);
|
||||
string body = await resp.Content.ReadAsStringAsync();
|
||||
if (resp.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
last = new Exception($"HTTP {(int)resp.StatusCode}: {Clip(body, 160)}");
|
||||
if ((int)resp.StatusCode is 401 or 403)
|
||||
{
|
||||
throw last;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return JObject.Parse(body);
|
||||
}
|
||||
catch (Exception ex) when (ex is not HttpRequestException && ex.Message.Contains("401"))
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
last = ex;
|
||||
}
|
||||
}
|
||||
if (last is not null)
|
||||
{
|
||||
throw last;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async Task<JObject> FetchCivitaiModelVersion(string apiKey, int versionId)
|
||||
{
|
||||
string[] hosts = ["civitai.red", "civitai.com"];
|
||||
Exception last = null;
|
||||
foreach (string host in hosts)
|
||||
{
|
||||
try
|
||||
{
|
||||
string url = $"https://{host}/api/v1/model-versions/{versionId}";
|
||||
using HttpRequestMessage req = new(HttpMethod.Get, url);
|
||||
if (!string.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey.Trim());
|
||||
}
|
||||
using HttpResponseMessage resp = await HttpClient.SendAsync(req);
|
||||
string body = await resp.Content.ReadAsStringAsync();
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
last = new Exception($"HTTP {(int)resp.StatusCode}: {Clip(body, 160)}");
|
||||
if ((int)resp.StatusCode is 401 or 403)
|
||||
{
|
||||
throw last;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return JObject.Parse(body);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
last = ex;
|
||||
if (ex.Message.Contains("401") || ex.Message.Contains("403"))
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (last is not null)
|
||||
{
|
||||
throw last;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Server-side LoRA / checkpoint / wildcard inventory (not DOM scrape).
|
||||
/// Pass rescan=true after downloads so new files appear (calls Program.RefreshAllModelSets).</summary>
|
||||
public async Task<JObject> AssistentListInventory(Session session, bool rescan = false)
|
||||
@@ -553,8 +109,6 @@ public partial class SwarmAssistentExtension
|
||||
{
|
||||
string weight = null;
|
||||
try { weight = model.RawFilePath; } catch { /* ignore */ }
|
||||
string cardPath = CardPathForWeight(weight);
|
||||
bool hasCard = !string.IsNullOrWhiteSpace(cardPath) && File.Exists(cardPath);
|
||||
|
||||
string usage = model.Metadata?.UsageHint;
|
||||
string desc = model.Metadata?.Description;
|
||||
@@ -571,29 +125,10 @@ public partial class SwarmAssistentExtension
|
||||
}
|
||||
|
||||
string blurb = null;
|
||||
if (hasCard)
|
||||
string raw = !string.IsNullOrWhiteSpace(usage) ? usage : desc;
|
||||
if (!string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
try
|
||||
{
|
||||
JObject card = JObject.Parse(File.ReadAllText(cardPath, Encoding.UTF8));
|
||||
string fromCard = (card["notes"] ?? card["when"] ?? card["prompt_hint"])?.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(fromCard))
|
||||
{
|
||||
blurb = Clip(fromCard.Trim(), CfgInt("inventory_blurb_max", InventoryBlurbMaxFallback));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore bad card json
|
||||
}
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(blurb))
|
||||
{
|
||||
string raw = !string.IsNullOrWhiteSpace(usage) ? usage : desc;
|
||||
if (!string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
blurb = Clip(CollapseWs(raw), CfgInt("inventory_blurb_max", InventoryBlurbMaxFallback));
|
||||
}
|
||||
blurb = Clip(CollapseWs(raw), CfgInt("inventory_blurb_max", InventoryBlurbMaxFallback));
|
||||
}
|
||||
|
||||
JArray tags = null;
|
||||
@@ -618,7 +153,6 @@ public partial class SwarmAssistentExtension
|
||||
["architecture"] = model.ModelClass?.ID,
|
||||
["compat_class"] = model.ModelClass?.CompatClass?.ID,
|
||||
["hash"] = model.Metadata?.Hash ?? "",
|
||||
["has_card"] = hasCard,
|
||||
["krea_likely"] = LooksLikeKreaArch(model),
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(weight))
|
||||
@@ -665,165 +199,4 @@ public partial class SwarmAssistentExtension
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
/// <summary>Search Civitai for LoRAs (prefers Krea 2 base). Uses Swarm-stored civitai_api key.</summary>
|
||||
public async Task<JObject> AssistentSearchCivitai(Session session, string query, int limit = 8)
|
||||
{
|
||||
string q = (query ?? "").Trim();
|
||||
if (string.IsNullOrWhiteSpace(q))
|
||||
{
|
||||
return new JObject { ["error"] = "query is required" };
|
||||
}
|
||||
limit = Math.Clamp(limit, 1, 20);
|
||||
string apiKey = session.User.GetGenericData("civitai_api", "key") ?? "";
|
||||
HashSet<string> installedNames = CollectInstalledLoraNames();
|
||||
HashSet<string> installedHashes = CollectInstalledLoraHashes();
|
||||
|
||||
string[] hosts = ["civitai.red", "civitai.com"];
|
||||
Exception lastEx = null;
|
||||
foreach (string host in hosts)
|
||||
{
|
||||
try
|
||||
{
|
||||
string url = $"https://{host}/api/v1/models?limit={limit}&types=LORA&query={Uri.EscapeDataString(q)}";
|
||||
using HttpRequestMessage req = new(HttpMethod.Get, url);
|
||||
if (!string.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey.Trim());
|
||||
}
|
||||
using HttpResponseMessage resp = await HttpClient.SendAsync(req);
|
||||
string body = await resp.Content.ReadAsStringAsync();
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
lastEx = new Exception($"HTTP {(int)resp.StatusCode}: {Clip(body, 200)}");
|
||||
continue;
|
||||
}
|
||||
JObject parsed = JObject.Parse(body);
|
||||
JArray items = parsed["items"] as JArray ?? [];
|
||||
JArray results = [];
|
||||
foreach (JToken item in items)
|
||||
{
|
||||
if (item is not JObject mo)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
JObject card = BuildCivitaiCard(mo, installedNames, installedHashes);
|
||||
if (card is not null)
|
||||
{
|
||||
results.Add(card);
|
||||
}
|
||||
}
|
||||
// Prefer Krea-compatible first
|
||||
JArray sorted = new(results.OrderByDescending(t => LooksLikeKrea(t["base_model"]?.ToString())).ThenBy(t => t["name"]?.ToString()));
|
||||
return new JObject
|
||||
{
|
||||
["success"] = true,
|
||||
["query"] = q,
|
||||
["host"] = host,
|
||||
["results"] = sorted,
|
||||
["has_civitai_key"] = !string.IsNullOrWhiteSpace(apiKey),
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lastEx = ex;
|
||||
}
|
||||
}
|
||||
return new JObject { ["error"] = $"Civitai search failed: {lastEx?.Message ?? "unknown"}" };
|
||||
}
|
||||
|
||||
static bool LooksLikeKrea(string text) => !string.IsNullOrEmpty(text) && Regex.IsMatch(text, @"krea", RegexOptions.IgnoreCase);
|
||||
|
||||
static HashSet<string> CollectInstalledLoraNames()
|
||||
{
|
||||
HashSet<string> names = new(StringComparer.OrdinalIgnoreCase);
|
||||
if (!Program.T2IModelSets.TryGetValue("LoRA", out T2IModelHandler handler))
|
||||
{
|
||||
return names;
|
||||
}
|
||||
foreach (T2IModel m in handler.Models.Values)
|
||||
{
|
||||
names.Add(m.Name);
|
||||
string leaf = m.Name.Replace('\\', '/').AfterLast('/');
|
||||
if (!string.IsNullOrEmpty(leaf))
|
||||
{
|
||||
names.Add(leaf);
|
||||
names.Add(Path.GetFileNameWithoutExtension(leaf));
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
static HashSet<string> CollectInstalledLoraHashes()
|
||||
{
|
||||
HashSet<string> hashes = new(StringComparer.OrdinalIgnoreCase);
|
||||
if (!Program.T2IModelSets.TryGetValue("LoRA", out T2IModelHandler handler))
|
||||
{
|
||||
return hashes;
|
||||
}
|
||||
foreach (T2IModel m in handler.Models.Values)
|
||||
{
|
||||
string h = m.Metadata?.Hash;
|
||||
if (!string.IsNullOrWhiteSpace(h))
|
||||
{
|
||||
hashes.Add(h.Trim().ToLowerInvariant());
|
||||
}
|
||||
}
|
||||
return hashes;
|
||||
}
|
||||
|
||||
static JObject BuildCivitaiCard(JObject model, HashSet<string> installedNames, HashSet<string> installedHashes)
|
||||
{
|
||||
string name = model["name"]?.ToString() ?? "";
|
||||
JArray versions = model["modelVersions"] as JArray;
|
||||
JObject ver = versions?.FirstOrDefault() as JObject;
|
||||
if (ver is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
string baseModel = ver["baseModel"]?.ToString() ?? "";
|
||||
JArray trained = ver["trainedWords"] as JArray ?? [];
|
||||
List<string> triggers = trained.Select(t => t?.ToString()).Where(s => !string.IsNullOrWhiteSpace(s)).Take(8).ToList();
|
||||
JObject file = null;
|
||||
foreach (JToken f in ver["files"] as JArray ?? [])
|
||||
{
|
||||
if (f is JObject fo && (fo["primary"]?.Value<bool>() == true || (fo["name"]?.ToString() ?? "").EndsWith(".safetensors", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
file = fo;
|
||||
break;
|
||||
}
|
||||
}
|
||||
file ??= (ver["files"] as JArray)?.FirstOrDefault() as JObject;
|
||||
string downloadUrl = file?["downloadUrl"]?.ToString() ?? ver["downloadUrl"]?.ToString() ?? "";
|
||||
string fileName = file?["name"]?.ToString() ?? "";
|
||||
string sha = file?["hashes"]?["SHA256"]?.ToString() ?? file?["hashes"]?["AutoV2"]?.ToString() ?? "";
|
||||
string saveName = string.IsNullOrWhiteSpace(fileName)
|
||||
? Regex.Replace(name, @"[^\w\-.]+", "_").Trim('_')
|
||||
: Path.GetFileNameWithoutExtension(fileName);
|
||||
|
||||
bool already = false;
|
||||
if (!string.IsNullOrWhiteSpace(sha) && installedHashes.Contains(sha.Trim().ToLowerInvariant()))
|
||||
{
|
||||
already = true;
|
||||
}
|
||||
else if (installedNames.Contains(saveName) || installedNames.Contains(name) || installedNames.Contains(fileName))
|
||||
{
|
||||
already = true;
|
||||
}
|
||||
|
||||
return new JObject
|
||||
{
|
||||
["id"] = model["id"],
|
||||
["version_id"] = ver["id"],
|
||||
["name"] = name,
|
||||
["base_model"] = baseModel,
|
||||
["krea_likely"] = LooksLikeKrea(baseModel),
|
||||
["triggers"] = new JArray(triggers),
|
||||
["download_url"] = downloadUrl,
|
||||
["file_name"] = saveName,
|
||||
["sha256"] = sha,
|
||||
["already_installed"] = already,
|
||||
["n_sfw"] = model["nsfw"]?.Value<bool>() ?? false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user