diff --git a/AIDevGallery.Utils/ModelUrl.cs b/AIDevGallery.Utils/ModelUrl.cs index 6bd1b536..ed57ce54 100644 --- a/AIDevGallery.Utils/ModelUrl.cs +++ b/AIDevGallery.Utils/ModelUrl.cs @@ -104,9 +104,9 @@ public HuggingFaceUrl(string modelNameOrUrl) modelNameOrUrl = modelNameOrUrl.Trim(); - if (modelNameOrUrl.StartsWith("https://", StringComparison.InvariantCulture)) + if (modelNameOrUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) { - if (!modelNameOrUrl.StartsWith("https://huggingface.co", StringComparison.InvariantCulture)) + if (!modelNameOrUrl.StartsWith("https://huggingface.co", StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("Invalid URL", nameof(modelNameOrUrl)); } @@ -174,7 +174,7 @@ public GitHubUrl(string url) url = url.Trim(); FullUrl = url; - if (!url.StartsWith("https://github.com/", StringComparison.InvariantCulture)) + if (!url.StartsWith("https://github.com/", StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("Invalid URL", nameof(url)); } @@ -248,7 +248,11 @@ public static class UrlHelpers /// The full URL as a string. public static string GetFullUrl(string url) { - if (url.StartsWith("https://github.com", StringComparison.InvariantCulture)) + if (url.StartsWith("https://github.com", StringComparison.OrdinalIgnoreCase)) + { + return url; + } + else if (url.StartsWith("local", StringComparison.OrdinalIgnoreCase)) { return url; } diff --git a/AIDevGallery/AIDevGallery.csproj b/AIDevGallery/AIDevGallery.csproj index 864cb9e8..6633f4a8 100644 --- a/AIDevGallery/AIDevGallery.csproj +++ b/AIDevGallery/AIDevGallery.csproj @@ -85,22 +85,16 @@ + - - - - onnxruntime-genai.dll - PreserveNewest - false - - + + - @@ -250,6 +244,7 @@ + diff --git a/AIDevGallery/Assets/ModelIcons/onnx.svg b/AIDevGallery/Assets/ModelIcons/onnx.svg new file mode 100644 index 00000000..f57db075 --- /dev/null +++ b/AIDevGallery/Assets/ModelIcons/onnx.svg @@ -0,0 +1,12 @@ + + \ No newline at end of file diff --git a/AIDevGallery/Controls/ModelSelectionControl.xaml b/AIDevGallery/Controls/ModelSelectionControl.xaml index 68d78c3c..aa0f9a5b 100644 --- a/AIDevGallery/Controls/ModelSelectionControl.xaml +++ b/AIDevGallery/Controls/ModelSelectionControl.xaml @@ -1,6 +1,7 @@ + Visibility="{x:Bind helpers:ModelDetailsHelper.ShowWhenOnnxModel(ModelDetails)}"> @@ -123,13 +124,15 @@ Source="{x:Bind utils:AppUtils.GetModelSourceImageFromUrl(ModelDetails.Url)}"> - - + - - + + @@ -216,20 +219,20 @@ Icon="{ui:FontIcon Glyph=}" Tag="{x:Bind ModelDetails}" Text="Copy as path" - Visibility="{x:Bind helpers:ModelDetailsHelper.ShowWhenDownloadedModel(ModelDetails)}" /> + Visibility="{x:Bind helpers:ModelDetailsHelper.ShowWhenOnnxModel(ModelDetails)}" /> - + Visibility="{x:Bind helpers:ModelDetailsHelper.ShowWhenOnnxModel(ModelDetails)}" /> + + Visibility="{x:Bind helpers:ModelDetailsHelper.ShowWhenOnnxModel(ModelDetails)}" /> @@ -333,8 +336,7 @@ Source="{x:Bind utils:AppUtils.GetModelSourceImageFromUrl(ModelDetails.Url)}"> - - + @@ -519,8 +521,7 @@ Source="{x:Bind utils:AppUtils.GetModelSourceImageFromUrl(ModelDetails.Url)}"> - - + diff --git a/AIDevGallery/Controls/ModelSelectionControl.xaml.cs b/AIDevGallery/Controls/ModelSelectionControl.xaml.cs index e6f4ac4e..7ed96fad 100644 --- a/AIDevGallery/Controls/ModelSelectionControl.xaml.cs +++ b/AIDevGallery/Controls/ModelSelectionControl.xaml.cs @@ -103,7 +103,7 @@ private void ResetAndLoadModelList(ModelDetails? selectedModel = null) if (AvailableModels.Count > 0) { var modelIds = AvailableModels.Select(s => s.ModelDetails.Id); - var modelOrApiUsageHistory = App.AppData.UsageHistory.Where(id => modelIds.Contains(id)); + var modelOrApiUsageHistory = App.AppData.UsageHistoryV2?.FirstOrDefault(u => modelIds.Contains(u.Id)); ModelDetails? modelToPreselect = null; @@ -112,20 +112,33 @@ private void ResetAndLoadModelList(ModelDetails? selectedModel = null) modelToPreselect = AvailableModels.Where(m => m.ModelDetails.Id == selectedModel.Id).FirstOrDefault()?.ModelDetails; } - if (modelToPreselect != null) + if (modelToPreselect == null && modelOrApiUsageHistory != default) { - SetSelectedModel(selectedModel); - } - else if (modelOrApiUsageHistory.Any()) - { - // select most recently used if there is one - var modelId = modelOrApiUsageHistory.First(); - SetSelectedModel(AvailableModels.Where(s => s.ModelDetails.Id == modelId).First().ModelDetails); + var models = AvailableModels.Where(am => am.ModelDetails.Id == modelOrApiUsageHistory.Id).ToList(); + if (models.Count > 0) + { + if (modelOrApiUsageHistory.HardwareAccelerator != null) + { + var model = models.FirstOrDefault(m => m.ModelDetails.HardwareAccelerators.Contains(modelOrApiUsageHistory.HardwareAccelerator.Value)); + if (model != null) + { + modelToPreselect = model.ModelDetails; + } + } + + if (modelToPreselect == null) + { + modelToPreselect = models.FirstOrDefault()?.ModelDetails; + } + } } - else + + if (modelToPreselect == null) { - SetSelectedModel(AvailableModels[0].ModelDetails); + modelToPreselect = AvailableModels[0].ModelDetails; } + + SetSelectedModel(modelToPreselect); } else { @@ -134,7 +147,7 @@ private void ResetAndLoadModelList(ModelDetails? selectedModel = null) } } - private void SetSelectedModel(ModelDetails? modelDetails) + private void SetSelectedModel(ModelDetails? modelDetails, HardwareAccelerator? accelerator = null) { if (modelDetails != null) { @@ -167,7 +180,13 @@ private void SetViewSelection(ModelDetails modelDetails) if (IsSelectionEnabled) { ModelSelectionItemsView.DeselectAll(); - ModelSelectionItemsView.Select(AvailableModels.IndexOf(AvailableModels.First(a => a.ModelDetails.Id == modelDetails.Id))); + + var models = AvailableModels.Where(a => a.ModelDetails == modelDetails).ToList(); + + if (models.Count != 0) + { + ModelSelectionItemsView.Select(AvailableModels.IndexOf(models.First())); + } } } diff --git a/AIDevGallery/Helpers/ModelDetailsHelper.cs b/AIDevGallery/Helpers/ModelDetailsHelper.cs index db0d0f4a..6e05d6f6 100644 --- a/AIDevGallery/Helpers/ModelDetailsHelper.cs +++ b/AIDevGallery/Helpers/ModelDetailsHelper.cs @@ -155,11 +155,21 @@ public static Visibility ShowWhenOllama(ModelDetails modelDetails) return modelDetails.HardwareAccelerators.Contains(HardwareAccelerator.OLLAMA) ? Visibility.Visible : Visibility.Collapsed; } - public static Visibility ShowWhenDownloadedModel(ModelDetails modelDetails) + private static bool IsOnnxModel(ModelDetails modelDetails) { return modelDetails.HardwareAccelerators.Contains(HardwareAccelerator.CPU) || modelDetails.HardwareAccelerators.Contains(HardwareAccelerator.DML) - || modelDetails.HardwareAccelerators.Contains(HardwareAccelerator.QNN) + || modelDetails.HardwareAccelerators.Contains(HardwareAccelerator.QNN); + } + + public static Visibility ShowWhenOnnxModel(ModelDetails modelDetails) + { + return IsOnnxModel(modelDetails) ? Visibility.Visible : Visibility.Collapsed; + } + + public static Visibility ShowWhenDownloadedModel(ModelDetails modelDetails) + { + return IsOnnxModel(modelDetails) && !modelDetails.IsUserAdded ? Visibility.Visible : Visibility.Collapsed; } } \ No newline at end of file diff --git a/AIDevGallery/MainWindow.xaml.cs b/AIDevGallery/MainWindow.xaml.cs index a8e38a13..9e7dabb3 100644 --- a/AIDevGallery/MainWindow.xaml.cs +++ b/AIDevGallery/MainWindow.xaml.cs @@ -57,6 +57,10 @@ public void NavigateToPage(object? obj) { NavigateToApiOrModelPage(modelTypes[0]); } + else if (obj is ModelDetails) + { + Navigate("Models", obj); + } else { Navigate("Home"); diff --git a/AIDevGallery/Models/BaseSampleNavigationParameters.cs b/AIDevGallery/Models/BaseSampleNavigationParameters.cs index 5be22b10..650180ad 100644 --- a/AIDevGallery/Models/BaseSampleNavigationParameters.cs +++ b/AIDevGallery/Models/BaseSampleNavigationParameters.cs @@ -16,6 +16,7 @@ internal abstract class BaseSampleNavigationParameters(TaskCompletionSource samp public TaskCompletionSource SampleLoadedCompletionSource { get; set; } = sampleLoadedCompletionSource; protected abstract string ChatClientModelPath { get; } + protected abstract HardwareAccelerator ChatClientHardwareAccelerator { get; } protected abstract LlmPromptTemplate? ChatClientPromptTemplate { get; } public void NotifyCompletion() @@ -35,7 +36,11 @@ public void NotifyCompletion() return new OllamaChatClient(OllamaHelper.GetOllamaUrl(), modelId); } - return await GenAIModel.CreateAsync(ChatClientModelPath, ChatClientPromptTemplate, CancellationToken).ConfigureAwait(false); + return await GenAIModel.CreateAsync( + ChatClientModelPath, + ChatClientPromptTemplate, + ChatClientHardwareAccelerator == HardwareAccelerator.QNN ? "qnn" : null, + CancellationToken).ConfigureAwait(false); } internal abstract void SendSampleInteractionEvent(string? customInfo = null); diff --git a/AIDevGallery/Models/CachedModel.cs b/AIDevGallery/Models/CachedModel.cs index f946f26a..bc077955 100644 --- a/AIDevGallery/Models/CachedModel.cs +++ b/AIDevGallery/Models/CachedModel.cs @@ -20,11 +20,16 @@ internal class CachedModel public CachedModel(ModelDetails details, string path, bool isFile, long modelSize) { Details = details; - if (details.Url.StartsWith("https://github.com", StringComparison.InvariantCulture)) + if (details.Url.StartsWith("https://github.com", StringComparison.OrdinalIgnoreCase)) { Url = details.Url; Source = CachedModelSource.GitHub; } + else if (details.Url.StartsWith("local", StringComparison.OrdinalIgnoreCase)) + { + Url = details.Url; + Source = CachedModelSource.Local; + } else { Url = new HuggingFaceUrl(details.Url).FullUrl; @@ -42,5 +47,6 @@ public CachedModel(ModelDetails details, string path, bool isFile, long modelSiz internal enum CachedModelSource { GitHub, - HuggingFace + HuggingFace, + Local } \ No newline at end of file diff --git a/AIDevGallery/Models/MultiModelSampleNavigationParameters.cs b/AIDevGallery/Models/MultiModelSampleNavigationParameters.cs index 8a2bed8e..a3cb89d4 100644 --- a/AIDevGallery/Models/MultiModelSampleNavigationParameters.cs +++ b/AIDevGallery/Models/MultiModelSampleNavigationParameters.cs @@ -22,6 +22,7 @@ internal class MultiModelSampleNavigationParameters( public HardwareAccelerator[] HardwareAccelerators { get; } = hardwareAccelerators; protected override string ChatClientModelPath => ModelPaths[0]; + protected override HardwareAccelerator ChatClientHardwareAccelerator => HardwareAccelerators[0]; protected override LlmPromptTemplate? ChatClientPromptTemplate => promptTemplates[0]; internal override void SendSampleInteractionEvent(string? customInfo = null) diff --git a/AIDevGallery/Models/SampleNavigationParameters.cs b/AIDevGallery/Models/SampleNavigationParameters.cs index cdd4a799..5749fe89 100644 --- a/AIDevGallery/Models/SampleNavigationParameters.cs +++ b/AIDevGallery/Models/SampleNavigationParameters.cs @@ -23,6 +23,7 @@ internal class SampleNavigationParameters( public string SampleId => sampleId; protected override string ChatClientModelPath => ModelPath; + protected override HardwareAccelerator ChatClientHardwareAccelerator => HardwareAccelerator; protected override LlmPromptTemplate? ChatClientPromptTemplate => promptTemplate; internal override void SendSampleInteractionEvent(string? customInfo = null) diff --git a/AIDevGallery/Models/Samples.cs b/AIDevGallery/Models/Samples.cs index 12eb90ca..a16c48c2 100644 --- a/AIDevGallery/Models/Samples.cs +++ b/AIDevGallery/Models/Samples.cs @@ -111,7 +111,7 @@ public string Icon icon = "GitHub.dark.svg"; } } - else if (Url.StartsWith("ollama", StringComparison.InvariantCultureIgnoreCase)) + else if (Url.StartsWith("ollama", StringComparison.OrdinalIgnoreCase)) { if (App.Current.RequestedTheme == Microsoft.UI.Xaml.ApplicationTheme.Light) { @@ -122,6 +122,10 @@ public string Icon icon = "ollama.dark.svg"; } } + else if (Url.StartsWith("local", StringComparison.OrdinalIgnoreCase)) + { + icon = "onnx.svg"; + } else { icon = "HuggingFace.svg"; diff --git a/AIDevGallery/Pages/Models/AddModelPage.xaml b/AIDevGallery/Pages/Models/AddModelPage.xaml index b2297212..5e78d54d 100644 --- a/AIDevGallery/Pages/Models/AddModelPage.xaml +++ b/AIDevGallery/Pages/Models/AddModelPage.xaml @@ -68,6 +68,10 @@ IsEnabled="{Binding ElementName=SearchTextBox, Path=Text, Converter={StaticResource EmptyStringToObjectConverter}}" Style="{StaticResource AccentButtonStyle}" /> + p.Dml != null)) - ) + var pathComponents = config.RFilename.Split("/"); + string modelPath = string.Empty; + if (pathComponents.Length > 1) { - var pathComponents = config.RFilename.Split("/"); - string modelPath = string.Empty; - if (pathComponents.Length > 1) - { - modelPath = string.Join("/", pathComponents.Take(pathComponents.Length - 1)); - } - - var modelUrl = $"https://huggingface.co/{result.Id}/tree/main/{modelPath}"; + modelPath = string.Join("/", pathComponents.Take(pathComponents.Length - 1)); + } - bool isDmlModel = genAIConfig.Model.Decoder.SessionOptions.ProviderOptions.Any(p => p.Dml != null); + var modelUrl = $"https://huggingface.co/{result.Id}/tree/main/{modelPath}"; - var curratedModel = ModelTypeHelpers.ModelDetails.Values.Where(m => m.Url == modelUrl).FirstOrDefault(); + var curratedModel = ModelTypeHelpers.ModelDetails.Values.Where(m => m.Url == modelUrl).FirstOrDefault(); - var filesToDownload = await ModelInformationHelper.GetDownloadFilesFromHuggingFace(new HuggingFaceUrl(modelUrl)); + var filesToDownload = await ModelInformationHelper.GetDownloadFilesFromHuggingFace(new HuggingFaceUrl(modelUrl)); - var details = curratedModel ?? new ModelDetails() - { - Id = "useradded-languagemodel-" + Guid.NewGuid().ToString(), - Name = result.Id + " " + (isDmlModel ? "DML" : "CPU"), - Url = modelUrl, - Description = "TODO", - HardwareAccelerators = [isDmlModel ? HardwareAccelerator.DML : HardwareAccelerator.CPU], - IsUserAdded = true, - PromptTemplate = GetTemplateFromName(result.Id), - Size = filesToDownload.Sum(f => f.Size), - ReadmeUrl = readmeUrl != null ? $"https://huggingface.co/{result.Id}/blob/main/{readmeUrl}" : null - }; - - string? licenseKey = null; - if (result.Tags != null) + var details = curratedModel ?? new ModelDetails() + { + Id = "useradded-languagemodel-" + Guid.NewGuid().ToString(), + Name = result.Id + " " + accelerator.ToString(), + Url = modelUrl, + Description = "Model downloaded from HuggingFace", + HardwareAccelerators = [accelerator], + IsUserAdded = true, + PromptTemplate = GetTemplateFromName(result.Id), + Size = filesToDownload.Sum(f => f.Size), + ReadmeUrl = readmeUrl != null ? $"https://huggingface.co/{result.Id}/blob/main/{readmeUrl}" : null + }; + + string? licenseKey = null; + if (result.Tags != null) + { + var licenseTag = result.Tags.Where(t => t.StartsWith("license:", StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); + if (licenseTag != null) { - var licenseTag = result.Tags.Where(t => t.StartsWith("license:", StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); - if (licenseTag != null) - { - licenseKey = licenseTag.Split(":").Last(); - } + licenseKey = licenseTag.Split(":").Last(); } + } - if (curratedModel == null) - { - details.License = licenseKey; - } + if (curratedModel == null) + { + details.License = licenseKey; + } - ResultState state = ResultState.NotDownloaded; + ResultState state = ResultState.NotDownloaded; - if (App.ModelCache.IsModelCached(details.Url)) - { - state = ResultState.Downloaded; - } - else if (App.ModelCache.DownloadQueue.GetDownload(details.Url) != null) - { - state = ResultState.Downloading; - } + if (App.ModelCache.IsModelCached(details.Url)) + { + state = ResultState.Downloaded; + } + else if (App.ModelCache.DownloadQueue.GetDownload(details.Url) != null) + { + state = ResultState.Downloading; + } - DispatcherQueue.TryEnqueue(() => + DispatcherQueue.TryEnqueue(() => + { + this.results.Add(new Result { - this.results.Add(new Result - { - Details = details, - SearchResult = result, - License = LicenseInfo.GetLicenseInfo(licenseKey), - State = state, - HFUrl = $"https://huggingface.co/{result.Id}" - }); + Details = details, + SearchResult = result, + License = LicenseInfo.GetLicenseInfo(licenseKey), + State = state, + HFUrl = $"https://huggingface.co/{result.Id}" }); - } + }); if (actionBlock.InputCount == 0) { @@ -195,9 +190,9 @@ private async Task SearchModels(string query, CancellationToken cancellationToke continue; } - var configs = result.Siblings.Where(r => r.RFilename.EndsWith("genai_config.json", StringComparison.InvariantCultureIgnoreCase)); + var configs = result.Siblings.Where(r => r.RFilename.EndsWith("genai_config.json", StringComparison.OrdinalIgnoreCase)); - var readmeSiblings = result.Siblings.Where(r => r.RFilename.EndsWith("readme.md", StringComparison.InvariantCultureIgnoreCase)); + var readmeSiblings = result.Siblings.Where(r => r.RFilename.EndsWith("readme.md", StringComparison.OrdinalIgnoreCase)); string? readmeUrl = null; if (readmeSiblings.Any()) @@ -331,6 +326,157 @@ private void SearchTextBox_Loaded(object sender, RoutedEventArgs e) { this.Focus(FocusState.Programmatic); } + + private HardwareAccelerator GetHardwareAcceleratorFromConfig(string configContents) + { + if (configContents.Contains(""""backend_path": "QnnHtp.dll"""", StringComparison.OrdinalIgnoreCase)) + { + return HardwareAccelerator.QNN; + } + + var config = JsonSerializer.Deserialize(configContents, SourceGenerationContext.Default.GenAIConfig); + if (config == null) + { + throw new FileLoadException("genai_config.json is not valid"); + } + + if (config.Model.Decoder.SessionOptions.ProviderOptions.Any(p => p.Dml != null)) + { + return HardwareAccelerator.DML; + } + + return HardwareAccelerator.CPU; + } + + private async void AddLocalClicked(object sender, RoutedEventArgs e) + { + var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow); + var picker = new FolderPicker(); + picker.FileTypeFilter.Add("*"); + WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd); + var folder = await picker.PickSingleFolderAsync(); + + if (folder != null) + { + var files = Directory.GetFiles(folder.Path); + var config = files.Where(r => Path.GetFileName(r) == "genai_config.json").FirstOrDefault(); + + if (string.IsNullOrEmpty(config) || App.ModelCache.Models.Any(m => m.Path == folder.Path)) + { + var message = string.IsNullOrEmpty(config) ? + "The folder does not contain a model you can add. Ensure \"genai_config.json\" is present in the selected directory" : + "This model is already added"; + + ContentDialog confirmFolderDialog = new() + { + Title = "Can't add model", + Content = message, + XamlRoot = this.Content.XamlRoot, + CloseButtonText = "OK" + }; + + await confirmFolderDialog.ShowAsync(); + return; + } + + HardwareAccelerator accelerator = HardwareAccelerator.CPU; + + try + { + string configContents = string.Empty; + configContents = await File.ReadAllTextAsync(config); + accelerator = GetHardwareAcceleratorFromConfig(configContents); + } + catch (Exception ex) + { + ContentDialog confirmFolderDialog = new() + { + Title = "Can't read genai_config.json", + Content = ex.Message, + XamlRoot = this.Content.XamlRoot, + CloseButtonText = "OK" + }; + + await confirmFolderDialog.ShowAsync(); + return; + } + + var nameTextBox = new TextBox() + { + Text = Path.GetFileName(folder.Path), + Width = 300, + HorizontalAlignment = HorizontalAlignment.Left, + Margin = new Thickness(0, 0, 0, 10), + Header = "Model name" + }; + + ContentDialog nameModelDialog = new() + { + Title = "Add model", + Content = new StackPanel() + { + Orientation = Orientation.Vertical, + Spacing = 8, + Children = + { + new TextBlock() + { + Text = $"Adding ONNX model from \n \"{folder.Path}\"", + TextWrapping = TextWrapping.WrapWholeWords + }, + nameTextBox + } + }, + XamlRoot = this.Content.XamlRoot, + CloseButtonText = "Cancel", + PrimaryButtonText = "Add", + DefaultButton = ContentDialogButton.Primary, + Style = Application.Current.Resources["DefaultContentDialogStyle"] as Style + }; + + string modelName = nameTextBox.Text; + + nameTextBox.TextChanged += (s, e) => + { + if (string.IsNullOrEmpty(nameTextBox.Text)) + { + nameModelDialog.IsPrimaryButtonEnabled = false; + } + else + { + modelName = nameTextBox.Text; + nameModelDialog.IsPrimaryButtonEnabled = true; + } + }; + + var result = await nameModelDialog.ShowAsync(); + if (result != ContentDialogResult.Primary) + { + return; + } + + DirectoryInfo dirInfo = new DirectoryInfo(folder.Path); + long dirSize = await Task.Run(() => dirInfo.EnumerateFiles("*", SearchOption.AllDirectories).Sum(file => file.Length)); + + var details = new ModelDetails() + { + Id = "useradded-local-languagemodel-" + Guid.NewGuid().ToString(), + Name = modelName, + Url = $"local-file:///{folder.Path}", + Description = "Localy added GenAI Model", + HardwareAccelerators = [accelerator], + IsUserAdded = true, + PromptTemplate = GetTemplateFromName(folder.Path), + Size = dirSize, + ReadmeUrl = null, + License = "unknown" + }; + + await App.ModelCache.AddLocalModelToCache(details, folder.Path); + + App.MainWindow.NavigateToPage(details); + } + } } internal partial class Result : ObservableObject diff --git a/AIDevGallery/Pages/Models/ModelPage.xaml b/AIDevGallery/Pages/Models/ModelPage.xaml index 04c4d3ca..f1891671 100644 --- a/AIDevGallery/Pages/Models/ModelPage.xaml +++ b/AIDevGallery/Pages/Models/ModelPage.xaml @@ -4,6 +4,7 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="using:AIDevGallery.Controls" + xmlns:local="using:AIDevGallery.Pages" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters" @@ -13,6 +14,7 @@ xmlns:utils="using:AIDevGallery.Utils" mc:Ignorable="d"> + @@ -98,9 +100,10 @@ FontSize=16}"> models) { - var latestModelOrApiUsageHistory = App.AppData.UsageHistory.FirstOrDefault(id => models.Any(m => m.Id == id)); + var latestModelOrApiUsageHistory = App.AppData.UsageHistoryV2?.FirstOrDefault(u => models.Any(m => m.Id == u.Id)); - if (latestModelOrApiUsageHistory != null) + if (latestModelOrApiUsageHistory != default) { // select most recently used if there is one - return models.First(m => m.Id == latestModelOrApiUsageHistory); + return models.First(m => m.Id == latestModelOrApiUsageHistory.Id); } return models.FirstOrDefault(); @@ -227,7 +227,8 @@ await App.AppData.AddMru( SubItemId = selectedModelDetails!.Id, DisplayName = scenario.Name }, - selectedModelDetails.Id); + selectedModelDetails.Id, + selectedModelDetails.HardwareAccelerators.First()); } } diff --git a/AIDevGallery/Pages/SettingsPage.xaml.cs b/AIDevGallery/Pages/SettingsPage.xaml.cs index 1c4d8a52..8476c9ac 100644 --- a/AIDevGallery/Pages/SettingsPage.xaml.cs +++ b/AIDevGallery/Pages/SettingsPage.xaml.cs @@ -68,7 +68,7 @@ private void GetStorageInfo() long totalCacheSize = 0; - foreach (var cachedModel in App.ModelCache.Models.OrderBy(m => m.Details.Name)) + foreach (var cachedModel in App.ModelCache.Models.Where(m => m.Path.StartsWith(cacheFolderPath, StringComparison.OrdinalIgnoreCase)).OrderBy(m => m.Details.Name)) { cachedModels.Add(cachedModel); totalCacheSize += cachedModel.ModelSize; diff --git a/AIDevGallery/ProjectGenerator/Generator.cs b/AIDevGallery/ProjectGenerator/Generator.cs index fd6e4029..d4dfc568 100644 --- a/AIDevGallery/ProjectGenerator/Generator.cs +++ b/AIDevGallery/ProjectGenerator/Generator.cs @@ -276,45 +276,32 @@ static void AddPackageReference(ProjectItemGroupElement itemGroup, string packag { packageReferenceItem.AddMetadata("PrivateAssets", "all", true); } - else if (packageName == "Microsoft.AI.DirectML" || - packageName == "Microsoft.ML.OnnxRuntime.DirectML" || - packageName == "Microsoft.ML.OnnxRuntimeGenAI.DirectML") + else if (packageName == "Microsoft.ML.OnnxRuntime.DirectML" || + packageName == "Microsoft.ML.OnnxRuntimeGenAI.DirectML") { packageReferenceItem.Condition = "$(Platform) == 'x64'"; } - else if (packageName == "Microsoft.ML.OnnxRuntime.Qnn" || - packageName == "Microsoft.ML.OnnxRuntimeGenAI" || - packageName == "Microsoft.ML.OnnxRuntimeGenAI.Managed") + else if (packageName == "Microsoft.ML.OnnxRuntime.QNN" || + packageName == "Microsoft.ML.OnnxRuntimeGenAI.QNN" || + packageName == "Microsoft.ML.OnnxRuntimeGenAI") { packageReferenceItem.Condition = "$(Platform) == 'ARM64'"; } var versionStr = PackageVersionHelpers.PackageVersions[packageName]; packageReferenceItem.AddMetadata("Version", versionStr, true); - - if (packageName == "Microsoft.ML.OnnxRuntimeGenAI") - { - var noneItem = itemGroup.AddItem("None", "$(PKGMicrosoft_ML_OnnxRuntimeGenAI)\\runtimes\\win-arm64\\native\\onnxruntime-genai.dll"); - noneItem.Condition = "$(Platform) == 'ARM64'"; - noneItem.AddMetadata("Link", "onnxruntime-genai.dll", false); - noneItem.AddMetadata("CopyToOutputDirectory", "PreserveNewest", false); - noneItem.AddMetadata("Visible", "false", false); - - packageReferenceItem.AddMetadata("GeneratePathProperty", "true", true); - packageReferenceItem.AddMetadata("ExcludeAssets", "all", true); - } } foreach (var packageName in packageReferences) { if (packageName == "Microsoft.ML.OnnxRuntime.DirectML") { - AddPackageReference(itemGroup, "Microsoft.ML.OnnxRuntime.Qnn"); + AddPackageReference(itemGroup, "Microsoft.ML.OnnxRuntime.QNN"); } else if (packageName == "Microsoft.ML.OnnxRuntimeGenAI.DirectML") { - AddPackageReference(itemGroup, "Microsoft.ML.OnnxRuntime.Qnn"); - AddPackageReference(itemGroup, "Microsoft.ML.OnnxRuntimeGenAI"); + AddPackageReference(itemGroup, "Microsoft.ML.OnnxRuntime.QNN"); + AddPackageReference(itemGroup, "Microsoft.ML.OnnxRuntimeGenAI.QNN"); AddPackageReference(itemGroup, "Microsoft.ML.OnnxRuntimeGenAI.Managed"); } diff --git a/AIDevGallery/Samples/Definitions/Models/languagemodels.modelgroup.json b/AIDevGallery/Samples/Definitions/Models/languagemodels.modelgroup.json index 7be048c3..2b01c95c 100644 --- a/AIDevGallery/Samples/Definitions/Models/languagemodels.modelgroup.json +++ b/AIDevGallery/Samples/Definitions/Models/languagemodels.modelgroup.json @@ -32,7 +32,6 @@ "Description": "Phi 4 Mini CPU Accuracy Level 4", "HardwareAccelerator": "CPU", "Size": 4930563630, - "SupportedOnQualcomm": false, "Icon": "Microsoft.svg", "ParameterSize": "3.8B", "PromptTemplate": "Phi3", diff --git a/AIDevGallery/Samples/Open Source Models/Image Models/ESRGAN/SuperResolution.xaml.cs b/AIDevGallery/Samples/Open Source Models/Image Models/ESRGAN/SuperResolution.xaml.cs index 4ce5047f..c9d322c6 100644 --- a/AIDevGallery/Samples/Open Source Models/Image Models/ESRGAN/SuperResolution.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Image Models/ESRGAN/SuperResolution.xaml.cs @@ -12,6 +12,7 @@ using System; using System.Collections.Generic; using System.Drawing; +using System.IO; using System.Threading.Tasks; using Windows.Storage.Pickers; @@ -26,6 +27,9 @@ namespace AIDevGallery.Samples.OpenSourceModels.ESRGAN; SharedCodeEnum.NarratorHelper, SharedCodeEnum.DeviceUtils, ], + AssetFilenames = [ + "Enhance.png" + ], NugetPackageReferences = [ "System.Drawing.Common", "Microsoft.ML.OnnxRuntime.DirectML", @@ -56,7 +60,10 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa { var hardwareAccelerator = sampleParams.HardwareAccelerator; await InitModel(sampleParams.ModelPath, hardwareAccelerator); + sampleParams.NotifyCompletion(); + + await EnhanceImage(Path.Join(Windows.ApplicationModel.Package.Current.InstalledLocation.Path, "Assets", "Enhance.png")); } private Task InitModel(string modelPath, HardwareAccelerator hardwareAccelerator) diff --git a/AIDevGallery/Samples/Open Source Models/Image Models/Faster RCNN/ObjectDetection.xaml.cs b/AIDevGallery/Samples/Open Source Models/Image Models/Faster RCNN/ObjectDetection.xaml.cs index a9a569c5..889e17aa 100644 --- a/AIDevGallery/Samples/Open Source Models/Image Models/Faster RCNN/ObjectDetection.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Image Models/Faster RCNN/ObjectDetection.xaml.cs @@ -11,6 +11,7 @@ using System; using System.Collections.Generic; using System.Drawing; +using System.IO; using System.Linq; using System.Threading.Tasks; using Windows.Storage.Pickers; @@ -54,7 +55,7 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa sampleParams.NotifyCompletion(); // Loads inference on default image - await DetectObjects(Windows.ApplicationModel.Package.Current.InstalledLocation.Path + "\\Assets\\pose_default.png"); + await DetectObjects(Path.Join(Windows.ApplicationModel.Package.Current.InstalledLocation.Path, "Assets", "pose_default.png")); } // diff --git a/AIDevGallery/Samples/Open Source Models/Image Models/SINet/DetectBackground.xaml.cs b/AIDevGallery/Samples/Open Source Models/Image Models/SINet/DetectBackground.xaml.cs index 88e1f2d4..f4ed2959 100644 --- a/AIDevGallery/Samples/Open Source Models/Image Models/SINet/DetectBackground.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Image Models/SINet/DetectBackground.xaml.cs @@ -12,6 +12,7 @@ using System; using System.Collections.Generic; using System.Drawing; +using System.IO; using System.Threading.Tasks; using Windows.Storage.Pickers; @@ -65,7 +66,7 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa sampleParams.NotifyCompletion(); - await Detect(Windows.ApplicationModel.Package.Current.InstalledLocation.Path + "\\Assets\\detection_default.png"); + await Detect(Path.Join(Windows.ApplicationModel.Package.Current.InstalledLocation.Path, "Assets", "detection_default.png")); } private Task InitModel(string modelPath, HardwareAccelerator hardwareAccelerator) diff --git a/AIDevGallery/Samples/Open Source Models/Image Models/YOLOv4/YOLOObjectionDetection.xaml.cs b/AIDevGallery/Samples/Open Source Models/Image Models/YOLOv4/YOLOObjectionDetection.xaml.cs index 5301c39d..c0245e0b 100644 --- a/AIDevGallery/Samples/Open Source Models/Image Models/YOLOv4/YOLOObjectionDetection.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Image Models/YOLOv4/YOLOObjectionDetection.xaml.cs @@ -12,6 +12,7 @@ using System; using System.Collections.Generic; using System.Drawing; +using System.IO; using System.Threading.Tasks; using Windows.Storage.Pickers; @@ -65,7 +66,7 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa sampleParams.NotifyCompletion(); // Loads inference on default image - await DetectObjects(Windows.ApplicationModel.Package.Current.InstalledLocation.Path + "\\Assets\\team.jpg"); + await DetectObjects(Path.Join(Windows.ApplicationModel.Package.Current.InstalledLocation.Path, "Assets", "team.jpg")); } private Task InitModel(string modelPath, HardwareAccelerator hardwareAccelerator) diff --git a/AIDevGallery/Samples/Open Source Models/Multimodal Models/DescribeImage.xaml.cs b/AIDevGallery/Samples/Open Source Models/Multimodal Models/DescribeImage.xaml.cs index fc234183..3d55d2f3 100644 --- a/AIDevGallery/Samples/Open Source Models/Multimodal Models/DescribeImage.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Multimodal Models/DescribeImage.xaml.cs @@ -12,6 +12,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Threading; @@ -51,12 +52,15 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa await InitModel(sampleParams.ModelPath, sampleParams.CancellationToken); sampleParams.NotifyCompletion(); + // // Load default image if (!sampleParams.CancellationToken.IsCancellationRequested) { - imageFile = await StorageFile.GetFileFromPathAsync(Windows.ApplicationModel.Package.Current.InstalledLocation.Path + "\\Assets\\team.jpg"); + imageFile = await StorageFile.GetFileFromPathAsync(Path.Join(Windows.ApplicationModel.Package.Current.InstalledLocation.Path, "Assets", "team.jpg")); LoadImage(this.imageFile); } + + // } private async Task InitModel(string modelPath, CancellationToken ct) diff --git a/AIDevGallery/Samples/SharedCode/IChatClient/GenAIModel.cs b/AIDevGallery/Samples/SharedCode/IChatClient/GenAIModel.cs index 934b1d87..ae2a37f7 100644 --- a/AIDevGallery/Samples/SharedCode/IChatClient/GenAIModel.cs +++ b/AIDevGallery/Samples/SharedCode/IChatClient/GenAIModel.cs @@ -31,6 +31,7 @@ internal class GenAIModel : IChatClient private LlmPromptTemplate? _template; private static readonly SemaphoreSlim _createSemaphore = new(1, 1); private static OgaHandle? _ogaHandle; + private Config? _config; private static ChatOptions GetDefaultChatOptions() { @@ -53,7 +54,7 @@ private GenAIModel(string modelDir) _metadata = new ChatClientMetadata("GenAIChatClient", new Uri($"file:///{modelDir}")); } - public static async Task CreateAsync(string modelDir, LlmPromptTemplate? template = null, CancellationToken cancellationToken = default) + public static async Task CreateAsync(string modelDir, LlmPromptTemplate? template = null, string? provider = null, CancellationToken cancellationToken = default) { #pragma warning disable CA2000 // Dispose objects before losing scope var model = new GenAIModel(modelDir); @@ -66,7 +67,7 @@ private GenAIModel(string modelDir) await _createSemaphore.WaitAsync(cancellationToken); lockAcquired = true; cancellationToken.ThrowIfCancellationRequested(); - await model.InitializeAsync(modelDir, cancellationToken); + await model.InitializeAsync(modelDir, provider, cancellationToken); } catch { @@ -98,6 +99,7 @@ public void Dispose() _model?.Dispose(); _tokenizer?.Dispose(); _ogaHandle?.Dispose(); + _config?.Dispose(); } private string GetPrompt(IEnumerable history) @@ -263,12 +265,18 @@ void TransferMetadataValue(string propertyName, object defaultValue) } } - private Task InitializeAsync(string modelDir, CancellationToken cancellationToken = default) + private Task InitializeAsync(string modelDir, string? provider = null, CancellationToken cancellationToken = default) { return Task.Run( () => { - _model = new Model(modelDir); + _config = new Config(modelDir); + if (!string.IsNullOrEmpty(provider)) + { + _config.AppendProvider(provider); + } + + _model = new Model(_config); cancellationToken.ThrowIfCancellationRequested(); _tokenizer = new Tokenizer(_model); }, diff --git a/AIDevGallery/Samples/WCRAPIs/BackgroundRemover.xaml.cs b/AIDevGallery/Samples/WCRAPIs/BackgroundRemover.xaml.cs index 4e67148b..e810d16d 100644 --- a/AIDevGallery/Samples/WCRAPIs/BackgroundRemover.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/BackgroundRemover.xaml.cs @@ -66,7 +66,7 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa private async Task LoadDefaultImage() { - var file = await StorageFile.GetFileFromPathAsync(Windows.ApplicationModel.Package.Current.InstalledLocation.Path + "\\Assets\\pose_default.png"); + var file = await StorageFile.GetFileFromPathAsync(System.IO.Path.Join(Windows.ApplicationModel.Package.Current.InstalledLocation.Path, "Assets", "pose_default.png")); using var stream = await file.OpenReadAsync(); await SetImage(stream); } diff --git a/AIDevGallery/Samples/WCRAPIs/ImageDescription.xaml.cs b/AIDevGallery/Samples/WCRAPIs/ImageDescription.xaml.cs index 5e297a36..6d2ada44 100644 --- a/AIDevGallery/Samples/WCRAPIs/ImageDescription.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/ImageDescription.xaml.cs @@ -71,7 +71,7 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa private async Task LoadDefaultImage() { - var file = await StorageFile.GetFileFromPathAsync(Windows.ApplicationModel.Package.Current.InstalledLocation.Path + "\\Assets\\Road.png"); + var file = await StorageFile.GetFileFromPathAsync(Path.Join(Windows.ApplicationModel.Package.Current.InstalledLocation.Path, "Assets", "Road.png")); using var stream = await file.OpenReadAsync(); await SetImage(stream); } diff --git a/AIDevGallery/Samples/WCRAPIs/IncreaseFidelity.xaml.cs b/AIDevGallery/Samples/WCRAPIs/IncreaseFidelity.xaml.cs index fbdbf478..38a6cc6d 100644 --- a/AIDevGallery/Samples/WCRAPIs/IncreaseFidelity.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/IncreaseFidelity.xaml.cs @@ -10,6 +10,7 @@ using Microsoft.UI.Xaml.Media.Imaging; using Microsoft.Windows.Management.Deployment; using System; +using System.IO; using System.Linq; using System.Threading.Tasks; using Windows.ApplicationModel.DataTransfer; @@ -59,7 +60,7 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa private async Task LoadDefaultImage() { - var file = await StorageFile.GetFileFromPathAsync(Windows.ApplicationModel.Package.Current.InstalledLocation.Path + "\\Assets\\Enhance.png"); + var file = await StorageFile.GetFileFromPathAsync(Path.Join(Windows.ApplicationModel.Package.Current.InstalledLocation.Path, "Assets", "Enhance.png")); using var stream = await file.OpenReadAsync(); await SetImage(stream); } diff --git a/AIDevGallery/Utils/AppData.cs b/AIDevGallery/Utils/AppData.cs index b1daae48..0dc55a2d 100644 --- a/AIDevGallery/Utils/AppData.cs +++ b/AIDevGallery/Utils/AppData.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using AIDevGallery.Models; using AIDevGallery.Telemetry; using Microsoft.Windows.AI.ContentModeration; using Microsoft.Windows.AI.Generative; @@ -20,8 +21,7 @@ internal class AppData public required LinkedList MostRecentlyUsedItems { get; set; } public CustomParametersState? LastCustomParamtersState { get; set; } - // model or api ids - public required LinkedList UsageHistory { get; set; } + public LinkedList? UsageHistoryV2 { get; set; } public bool IsDiagnosticDataEnabled { get; set; } @@ -73,8 +73,10 @@ public async Task SaveAsync() await File.WriteAllTextAsync(GetConfigFilePath(), str); } - public async Task AddMru(MostRecentlyUsedItem item, string? modelOrApiId = null) + public async Task AddMru(MostRecentlyUsedItem item, string? modelOrApiId = null, HardwareAccelerator? hardwareAccelerator = null) { + UsageHistoryV2 ??= new LinkedList(); + foreach (var toRemove in MostRecentlyUsedItems.Where(i => i.ItemId == item.ItemId).ToArray()) { MostRecentlyUsedItems.Remove(toRemove); @@ -87,8 +89,13 @@ public async Task AddMru(MostRecentlyUsedItem item, string? modelOrApiId = null) if (!string.IsNullOrWhiteSpace(modelOrApiId)) { - UsageHistory.Remove(modelOrApiId); - UsageHistory.AddFirst(modelOrApiId); + var existingItem = UsageHistoryV2.Where(u => u.Id == modelOrApiId).FirstOrDefault(); + if (existingItem != default) + { + UsageHistoryV2.Remove(existingItem); + } + + UsageHistoryV2.AddFirst(new UsageHistory(modelOrApiId, hardwareAccelerator)); } MostRecentlyUsedItems.AddFirst(item); @@ -104,7 +111,7 @@ private static AppData GetDefault() { ModelCachePath = cacheDir, MostRecentlyUsedItems = new(), - UsageHistory = new() + UsageHistoryV2 = new() }; } } @@ -122,4 +129,6 @@ internal class CustomParametersState public LanguageModelSkill? ModelSkill { get; set; } public SeverityLevel? InputContentModeration { get; set; } public SeverityLevel? OutputContentModeration { get; set; } -} \ No newline at end of file +} + +internal record UsageHistory(string Id, HardwareAccelerator? HardwareAccelerator); \ No newline at end of file diff --git a/AIDevGallery/Utils/AppUtils.cs b/AIDevGallery/Utils/AppUtils.cs index d3efb156..c99326e2 100644 --- a/AIDevGallery/Utils/AppUtils.cs +++ b/AIDevGallery/Utils/AppUtils.cs @@ -169,16 +169,21 @@ public static string GetHardwareAcceleratorDescription(HardwareAccelerator hardw } } - public static string GetModelSourceNameFromUrl(string url) + public static string GetModelSourceOriginFromUrl(string url) { - if (url.StartsWith("https://huggingface.co", StringComparison.InvariantCultureIgnoreCase)) + if (url.StartsWith("https://huggingface.co", StringComparison.OrdinalIgnoreCase)) { - return "Hugging Face"; + return "This model was downloaded from Hugging Face"; } - if (url.StartsWith("https://github.co", StringComparison.InvariantCultureIgnoreCase)) + if (url.StartsWith("https://github.co", StringComparison.OrdinalIgnoreCase)) { - return "GitHub"; + return "This model was downloaded from GitHub"; + } + + if (url.StartsWith("local", StringComparison.OrdinalIgnoreCase)) + { + return "This model was added by you"; } return string.Empty; @@ -213,7 +218,7 @@ public static Uri GetLicenseUrlFromModel(ModelDetails model) public static ImageSource GetModelSourceImageFromUrl(string url) { - if (url.StartsWith("https://github", StringComparison.InvariantCultureIgnoreCase)) + if (url.StartsWith("https://github", StringComparison.OrdinalIgnoreCase)) { if (App.Current.RequestedTheme == Microsoft.UI.Xaml.ApplicationTheme.Light) { @@ -224,7 +229,7 @@ public static ImageSource GetModelSourceImageFromUrl(string url) return new SvgImageSource(new Uri("ms-appx:///Assets/ModelIcons/GitHub.dark.svg")); } } - else if (url.StartsWith("ollama", StringComparison.InvariantCultureIgnoreCase)) + else if (url.StartsWith("ollama", StringComparison.OrdinalIgnoreCase)) { if (App.Current.RequestedTheme == Microsoft.UI.Xaml.ApplicationTheme.Light) { @@ -235,6 +240,10 @@ public static ImageSource GetModelSourceImageFromUrl(string url) return new SvgImageSource(new Uri("ms-appx:///Assets/ModelIcons/ollama.dark.svg")); } } + else if (url.StartsWith("local", StringComparison.OrdinalIgnoreCase)) + { + return new SvgImageSource(new Uri("ms-appx:///Assets/ModelIcons/onnx.svg")); + } else { return new SvgImageSource(new Uri("ms-appx:///Assets/ModelIcons/HuggingFace.svg")); diff --git a/AIDevGallery/Utils/ModelCache.cs b/AIDevGallery/Utils/ModelCache.cs index e66a4748..2ec01019 100644 --- a/AIDevGallery/Utils/ModelCache.cs +++ b/AIDevGallery/Utils/ModelCache.cs @@ -82,6 +82,13 @@ public async Task SetCacheFolderPath(string newPath, List? models = return download; } + public async Task AddLocalModelToCache(ModelDetails modelDetails, string modelPath, bool isFile = false) + { + var cachedModel = new CachedModel(modelDetails, modelPath, isFile, modelDetails.Size); + await CacheStore.AddModel(cachedModel); + return cachedModel; + } + public CachedModel? GetCachedModel(string url) { url = UrlHelpers.GetFullUrl(url); @@ -112,6 +119,13 @@ public async Task DeleteModelFromCache(CachedModel model) { ModelDeletedEvent.Log(model.Url); await CacheStore.RemoveModel(model); + + if (model.Url.StartsWith("local", System.StringComparison.OrdinalIgnoreCase)) + { + // do not delete models added by user that are not in the cache folder + return; + } + if (model.IsFile && File.Exists(model.Path)) { File.Delete(model.Path); diff --git a/Directory.Packages.props b/Directory.Packages.props index 73630693..117f428d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,6 @@ - @@ -22,12 +21,13 @@ - + - - - + + + +