diff --git a/Endpoints/ModelProxyEndpoints.cs b/Endpoints/ModelProxyEndpoints.cs index 3d81015..3cd1048 100644 --- a/Endpoints/ModelProxyEndpoints.cs +++ b/Endpoints/ModelProxyEndpoints.cs @@ -44,14 +44,33 @@ public static void MapModelProxyEndpoints(this WebApplication app) return Results.Ok(new { models = new object[0] }); }); - app.MapGet("/api/hf/search", async (string? q, HttpClient httpClient) => + app.MapGet("/api/hf/search", async (string? q, string? pipeline_tag, HttpClient httpClient) => { try { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); - var query = string.IsNullOrWhiteSpace(q) ? "llama" : q; - var requestUrl = $"https://huggingface.co/api/models?search={Uri.EscapeDataString(query)}&filter=gguf&sort=downloads&direction=-1&limit=20"; + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + var query = string.IsNullOrWhiteSpace(q) ? (string.IsNullOrWhiteSpace(pipeline_tag) ? "llama" : "") : q; + string requestUrl; + if (!string.IsNullOrWhiteSpace(pipeline_tag)) + { + if (pipeline_tag.Equals("gguf", StringComparison.OrdinalIgnoreCase)) + { + var qParam = string.IsNullOrWhiteSpace(query) ? "llama" : query; + requestUrl = $"https://huggingface.co/api/models?search={Uri.EscapeDataString(qParam)}&filter=gguf&sort=downloads&direction=-1&limit=20"; + } + else + { + var qParam = Uri.EscapeDataString(query); + requestUrl = $"https://huggingface.co/api/models?search={qParam}&pipeline_tag={Uri.EscapeDataString(pipeline_tag)}&sort=downloads&direction=-1&limit=20"; + } + } + else + { + var qParam = string.IsNullOrWhiteSpace(query) ? "llama" : query; + requestUrl = $"https://huggingface.co/api/models?search={Uri.EscapeDataString(qParam)}&filter=gguf&sort=downloads&direction=-1&limit=20"; + } + using var req = new HttpRequestMessage(HttpMethod.Get, requestUrl); req.Headers.UserAgent.ParseAdd("LocalLLMServerManager/3.5.0"); var response = await httpClient.SendAsync(req, cts.Token); @@ -69,6 +88,58 @@ public static void MapModelProxyEndpoints(this WebApplication app) } }); + app.MapGet("/api/civitai/download", async (string fileUrl, string? modelType, string? fileName, HttpClient httpClient) => + { + try + { + var safeFileName = string.IsNullOrWhiteSpace(fileName) ? "model.safetensors" : fileName; + var targetDir = LocalLLMServerManager.Shared.Services.DownloadManager.ResolveTargetDirectory(modelType, safeFileName); + Directory.CreateDirectory(targetDir); + var targetPath = Path.Combine(targetDir, safeFileName); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + using var response = await httpClient.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead, cts.Token); + if (response.IsSuccessStatusCode) + { + using var stream = await response.Content.ReadAsStreamAsync(cts.Token); + using var fileStream = File.Create(targetPath); + await stream.CopyToAsync(fileStream, cts.Token); + return Results.Ok(new { status = "success", path = targetPath }); + } + return Results.StatusCode((int)response.StatusCode); + } + catch (Exception ex) + { + return Results.Problem(ex.Message); + } + }); + + app.MapGet("/api/hf/download", async (string fileUrl, string? pipelineTag, string? fileName, HttpClient httpClient) => + { + try + { + var safeFileName = string.IsNullOrWhiteSpace(fileName) ? "model.safetensors" : fileName; + var targetDir = LocalLLMServerManager.Shared.Services.DownloadManager.ResolveTargetDirectory(pipelineTag, safeFileName); + Directory.CreateDirectory(targetDir); + var targetPath = Path.Combine(targetDir, safeFileName); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + using var response = await httpClient.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead, cts.Token); + if (response.IsSuccessStatusCode) + { + using var stream = await response.Content.ReadAsStreamAsync(cts.Token); + using var fileStream = File.Create(targetPath); + await stream.CopyToAsync(fileStream, cts.Token); + return Results.Ok(new { status = "success", path = targetPath }); + } + return Results.StatusCode((int)response.StatusCode); + } + catch (Exception ex) + { + return Results.Problem(ex.Message); + } + }); + app.MapGet("/api/hf/model", async (string repoId, HttpClient httpClient) => { try diff --git a/LocalLLMServerManager.Shared/Interfaces/IHuggingFaceSearchService.cs b/LocalLLMServerManager.Shared/Interfaces/IHuggingFaceSearchService.cs index 3391313..8c08346 100644 --- a/LocalLLMServerManager.Shared/Interfaces/IHuggingFaceSearchService.cs +++ b/LocalLLMServerManager.Shared/Interfaces/IHuggingFaceSearchService.cs @@ -7,6 +7,8 @@ namespace LocalLLMServerManager.Shared.Interfaces; public interface IHuggingFaceSearchService { - Task> SearchRepositoriesAsync(string apiBase, string query, HttpClient http); + Task> SearchRepositoriesAsync(string apiBase, string query, string? pipelineTag, HttpClient http); + Task> SearchRepositoriesAsync(string apiBase, string query, HttpClient http) => SearchRepositoriesAsync(apiBase, query, null, http); + Task> SearchModelsAsync(string query, string? pipelineTag = null, System.Threading.CancellationToken ct = default); Task> FetchQuantizationsAsync(string apiBase, string repoId, HttpClient http); } diff --git a/LocalLLMServerManager.Shared/Services/DownloadManager.cs b/LocalLLMServerManager.Shared/Services/DownloadManager.cs new file mode 100644 index 0000000..39ed613 --- /dev/null +++ b/LocalLLMServerManager.Shared/Services/DownloadManager.cs @@ -0,0 +1,45 @@ +using System; +using System.IO; + +namespace LocalLLMServerManager.Shared.Services; + +public static class DownloadManager +{ + public static string ResolveTargetDirectory(string? modelTypeOrPipelineTag, string? fileName = null, string? rootPath = null) + { + var baseDir = rootPath ?? AppContext.BaseDirectory; + var tagOrType = (modelTypeOrPipelineTag ?? "").ToLowerInvariant(); + var file = (fileName ?? "").ToLowerInvariant(); + + // Video models -> ComfyUI/models/diffusion_models + if (tagOrType.Contains("video") || tagOrType.Contains("text-to-video") || tagOrType.Contains("image-to-video") || + file.Contains("wan") || file.Contains("ltx") || file.Contains("hunyuanvideo")) + { + return Path.Combine(baseDir, "ComfyUI", "models", "diffusion_models"); + } + + // TTS / Audio models -> models/tts + if (tagOrType.Contains("speech") || tagOrType.Contains("tts") || tagOrType.Contains("audio") || + tagOrType.Contains("text-to-speech") || tagOrType.Contains("text-to-audio") || + tagOrType.Contains("automatic-speech-recognition") || + file.Contains("kokoro") || file.Contains("f5-tts") || file.Contains("stable-audio")) + { + return Path.Combine(baseDir, "models", "tts"); + } + + // 3D models -> models/3d + if (tagOrType.Contains("3d") || tagOrType.Contains("text-to-3d") || file.Contains("trellis") || file.Contains("hunyuan3d")) + { + return Path.Combine(baseDir, "models", "3d"); + } + + // LoRA -> models/Lora + if (tagOrType.Contains("lora")) + { + return Path.Combine(baseDir, "models", "Lora"); + } + + // Default / Checkpoints / LLMs + return Path.Combine(baseDir, "models", "checkpoints"); + } +} diff --git a/LocalLLMServerManager.Shared/Services/HuggingFaceSearchService.cs b/LocalLLMServerManager.Shared/Services/HuggingFaceSearchService.cs index cef28a1..3627db0 100644 --- a/LocalLLMServerManager.Shared/Services/HuggingFaceSearchService.cs +++ b/LocalLLMServerManager.Shared/Services/HuggingFaceSearchService.cs @@ -11,11 +11,17 @@ namespace LocalLLMServerManager.Shared.Services; public class HuggingFaceSearchService : IHuggingFaceSearchService { public async Task> SearchRepositoriesAsync(string apiBase, string query, HttpClient http) + { + return await SearchRepositoriesAsync(apiBase, query, null, http); + } + + public async Task> SearchRepositoriesAsync(string apiBase, string query, string? pipelineTag, HttpClient http) { var result = new List(); try { - var url = $"{apiBase}/api/hf/search?q={Uri.EscapeDataString(query)}"; + var tagParam = string.IsNullOrWhiteSpace(pipelineTag) ? "" : $"&pipeline_tag={Uri.EscapeDataString(pipelineTag)}"; + var url = $"{apiBase}/api/hf/search?q={Uri.EscapeDataString(query)}{tagParam}"; var response = await http.GetAsync(url); if (response.IsSuccessStatusCode) { @@ -29,9 +35,48 @@ public async Task> SearchRepositoriesAsync(string apiB string author = repo?["author"]?.ToString() ?? "Community"; int downloads = repo?["downloads"]?.GetValue() ?? 0; int likes = repo?["likes"]?.GetValue() ?? 0; + string tag = repo?["pipeline_tag"]?.ToString() ?? ""; + if (!string.IsNullOrEmpty(id)) + { + result.Add(new HuggingFaceRepoItem(id, author, likes, $"{downloads:N0} downloads", tag)); + } + } + } + } + } + catch { } + + return result; + } + + public async Task> SearchModelsAsync(string query, string? pipelineTag = null, System.Threading.CancellationToken ct = default) + { + using var http = new HttpClient(); + var tagParam = string.IsNullOrWhiteSpace(pipelineTag) ? "" : $"&pipeline_tag={Uri.EscapeDataString(pipelineTag)}"; + var requestUrl = $"https://huggingface.co/api/models?search={Uri.EscapeDataString(query)}{tagParam}&sort=downloads&direction=-1&limit=20"; + var result = new List(); + try + { + using var req = new HttpRequestMessage(HttpMethod.Get, requestUrl); + req.Headers.UserAgent.ParseAdd("LocalLLMServerManager/3.5.0"); + var response = await http.SendAsync(req, ct); + if (response.IsSuccessStatusCode) + { + var jsonStr = await response.Content.ReadAsStringAsync(ct); + var doc = JsonNode.Parse(jsonStr); + var repos = doc?.AsArray(); + if (repos != null) + { + foreach (var repo in repos) + { + string id = repo?["id"]?.ToString() ?? ""; + string author = repo?["author"]?.ToString() ?? "Community"; + int downloads = repo?["downloads"]?.GetValue() ?? 0; + int likes = repo?["likes"]?.GetValue() ?? 0; + string tag = repo?["pipeline_tag"]?.ToString() ?? ""; if (!string.IsNullOrEmpty(id)) { - result.Add(new HuggingFaceRepoItem(id, author, likes, $"{downloads:N0} downloads")); + result.Add(new HuggingFaceRepoItem(id, author, likes, $"{downloads:N0} downloads", tag)); } } } diff --git a/LocalLLMServerManager.Shared/ViewModels/HuggingFaceSearchViewModel.cs b/LocalLLMServerManager.Shared/ViewModels/HuggingFaceSearchViewModel.cs index 6aaf6f1..07a6be7 100644 --- a/LocalLLMServerManager.Shared/ViewModels/HuggingFaceSearchViewModel.cs +++ b/LocalLLMServerManager.Shared/ViewModels/HuggingFaceSearchViewModel.cs @@ -13,6 +13,7 @@ public partial class HuggingFaceSearchViewModel : ObservableObject private readonly IHuggingFaceSearchService _hfSearchService; [ObservableProperty] private string _hfSearchQuery = ""; + [ObservableProperty] private string? _selectedPipelineTag = null; public ObservableCollection HuggingFaceResults { get; } = new(); [ObservableProperty] private bool _isHfModalOpen = false; @@ -31,11 +32,18 @@ public async Task SearchHuggingFaceAsync() await SearchHuggingFaceAsync("http://127.0.0.1:5246", new HttpClient()); } + [RelayCommand] + public async Task SelectCategoryAsync(string? tag) + { + SelectedPipelineTag = string.IsNullOrWhiteSpace(tag) ? null : tag; + await SearchHuggingFaceAsync(); + } + public async Task SearchHuggingFaceAsync(string apiBase, HttpClient http) { try { - var results = await _hfSearchService.SearchRepositoriesAsync(apiBase, HfSearchQuery, http); + var results = await _hfSearchService.SearchRepositoriesAsync(apiBase, HfSearchQuery, SelectedPipelineTag, http); HuggingFaceResults.Clear(); foreach (var r in results) { diff --git a/LocalLLMServerManager.Shared/ViewModels/MainViewModel.cs b/LocalLLMServerManager.Shared/ViewModels/MainViewModel.cs index df1125a..fdb8b4a 100644 --- a/LocalLLMServerManager.Shared/ViewModels/MainViewModel.cs +++ b/LocalLLMServerManager.Shared/ViewModels/MainViewModel.cs @@ -22,7 +22,8 @@ public record HuggingFaceRepoItem( string Id, string Author, int Likes, - string Downloads + string Downloads, + string PipelineTag = "" ); public record HfFileQuantItem( diff --git a/LocalLLMServerManager.Shared/Views/Controls/HuggingFaceTabControl.axaml b/LocalLLMServerManager.Shared/Views/Controls/HuggingFaceTabControl.axaml index 7a2cf8b..c921911 100644 --- a/LocalLLMServerManager.Shared/Views/Controls/HuggingFaceTabControl.axaml +++ b/LocalLLMServerManager.Shared/Views/Controls/HuggingFaceTabControl.axaml @@ -4,9 +4,9 @@ x:Class="LocalLLMServerManager.Shared.Views.Controls.HuggingFaceTabControl" x:DataType="vm:HuggingFaceSearchViewModel"> - - - + + - + + +