From 5003df3dea15ff094532b514f958b55b0ad2abfc Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 23 Oct 2023 12:56:19 -0400 Subject: [PATCH 01/12] Add Russian localization --- CHANGELOG.md | 4 + README.md | 2 + .../Languages/Cultures.cs | 3 +- .../Languages/Resources.ru.resx | 681 ++++++++++++++++++ 4 files changed, 689 insertions(+), 1 deletion(-) create mode 100644 StabilityMatrix.Avalonia/Languages/Resources.ru.resx diff --git a/CHANGELOG.md b/CHANGELOG.md index a819072d..e993cffd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to Stability Matrix will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html). +## v2.5.6 +### Added +- Added Russian UI language option, thanks to aolko for the translation + ## v2.5.5 ### Added - Added Spanish UI language options, thanks to Carlos Baena and Lautaroturina for the translations diff --git a/README.md b/README.md index ba38d454..9e668e43 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,8 @@ Stability Matrix is now available in the following languages, thanks to our comm - 🇪🇸 Español - Carlos Baena - Lautaroturina +- 🇷🇺 Русский + - aolko If you would like to contribute a translation, please create an issue or contact us on Discord. Include an email where we'll send an invite to our [POEditor](https://poeditor.com/) project. diff --git a/StabilityMatrix.Avalonia/Languages/Cultures.cs b/StabilityMatrix.Avalonia/Languages/Cultures.cs index eb39a9f0..b9d00f61 100644 --- a/StabilityMatrix.Avalonia/Languages/Cultures.cs +++ b/StabilityMatrix.Avalonia/Languages/Cultures.cs @@ -24,7 +24,8 @@ public static class Cultures ["zh-Hant"] = new("zh-Hant"), ["it-IT"] = new("it-IT"), ["fr-FR"] = new("fr-FR"), - ["es"] = new("es") + ["es"] = new("es"), + ["ru"] = new("ru") }; public static IReadOnlyList SupportedCultures => diff --git a/StabilityMatrix.Avalonia/Languages/Resources.ru.resx b/StabilityMatrix.Avalonia/Languages/Resources.ru.resx new file mode 100644 index 00000000..149c0a0b --- /dev/null +++ b/StabilityMatrix.Avalonia/Languages/Resources.ru.resx @@ -0,0 +1,681 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Запустить + + + Выйти + + + Сохранить + + + Отмена + + + Язык + + + Чтобы применить новый язык требуется перезапуск + + + Перезапустить + + + Перезапустить позже + + + Требуется перезапуск + + + Неизвестный пакет + + + Импорт + + + Тип пакета + + + Версия + + + Тип версии + + + Релизы + + + Ветки + + + Перетащите модели сюда для импорта + + + Усиление акцента + + + Ослабление акцента + + + Вложения / Текстовые Инверсии + + + Сети (Лора / ЛиКОРИС) + + + Комментарии + + + Показывать пискельную сетку при максимальном масштабе + + + Шаги + + + Шаги - Базовые + + + Шаги - Обработчик + + + Внимание к запросу + + + Сила шумоподавления + + + Ширина + + + Высота + + + Обработчик + + + VAE + + + Модель + + + Соединиться + + + Соединяюсь... + + + Закрыть + + + Жду соединения... + + + Доступно обновление + + + Поддержать на Патреоне + + + Дискорд сервер проекта + + + Загрузки + + + Установить + + + Пропустить установку в первый раз + + + Произошла неизвестная ошибка + + + Выйти из программы + + + Название + + + Установка с таким названием уже существует. + + + Пожалуйста выберите другое название или другой путь установки. + + + Продвинутые настройки + + + Коммит + + + Способ расшаривания моделей + + + Версия PyTorch + + + Закрыть диалог по завершении + + + Папка с данными + + + Папка в которую будут установлены модели, Лоры, интерфейсы, настройки, и прочее. + + + Вы можете столкнуться с ошибками при использовании диска FAT32 или exFAT. Выберите другой диск для более плавной работы. + + + Портативный режим + + + В портативном режиме все данные и настройки будут храниться в той же папке, что и приложение. Вы сможете переместить приложение вместе с его папкой с данными ("Data") в другое место или на другой компьютер. + + + Продолжить + + + Пред. картинка + + + След. картинка + + + Описание модели + + + Доступна новая версия Stability Matrix! + + + Импорт последней - + + + Все версии + + + Поиск моделей, #тегов или @авторов + + + Поиск + + + Сортировка + + + Промежуток + + + Тип модели + + + Базовая модель + + + Показывать 18+ контент + + + Данные предоставлены CivitAI + + + Страница + + + Первая страница + + + Пред. страницаыдущая + + + Следущая страница + + + Последняя страница + + + Переименовать + + + Удалить + + + Открыть на CivitAI + + + Связанная модель + + + Локальная модель + + + Открыть в проводнике + + + Новая... + + + Папка + + + Перетащите файл сюда для импорта + + + Импортировать как связанные + + + Искать связанные данные при новом локальном импорте + + + Индексирую... + + + Папка с моделями + + + Категории + + + Давайте приступим + + + Я прочёл и согласен с + + + Лицензионным соглашением. + + + Найти связанные данные + + + Показать картинки моделей + + + Внешний вид + + + Тема + + + Управление моделями + + + Удалить ссылки на расшаренные модели при выключении + + + Выберите эту настройку если у вас возникают проблемы с перемещением Stability Matrix на другой диск + + + Сбросить кэш моделей + + + Перестраивает кэш установленных моделей. Используйте если модели некорректно названы в Просмотровщике моделей + + + Среда для пакетов + + + Редактировать + + + Переменные среды + + + Встроенный Python + + + Проверить версию + + + Интеграции + + + Discord Rich Presence + + + Система + + + Добавить ярлык Stability Matrix в меню "Пуск" + + + Использует текущее местоположение приложения, вы можете запустить это снова, если переместите приложение + + + Доступно только на Windows + + + Добавить для Текущего пользователя + + + Добавить для всех + + + Выберите новую папку с данными ("Data") + + + Не перемещает существующие данные + + + Выберите папку + + + О программе + + + Stability Matrix + + + Уведомления о лицензиях и открытом исходном коде + + + Нажмите "Запуск" чтобы начать! + + + Стоп + + + Послать команду + + + Команда + + + Отправить + + + Требуется команда + + + Подтвердить? + + + Да + + + Нет + + + Открыть веб-интерфейс + + + Добро пожаловать в Stability Matrix! + + + Выберите желаемый интерфейс и нажмите "Установить" чтобы начать работу + + + Устанавливаю + + + Перехожу на страницу запуска + + + Скачиваю пакет... + + + Скачивание завершено + + + Установка завершена + + + Устанавливаю необходимые компоненты... + + + Устанавливаю зависимости пакета... + + + Открыть в проводнике + + + Открыть в файндере + + + Удалить + + + Проверить обновления + + + Обновить + + + Добавить пакет + + + Добавьте пакет чтобы начать! + + + Переменная + + + Значение + + + Удалить + + + Подробнее + + + Стэк вызовов (Callstack) + + + Внутреннее исключение (Inner exception) + + + Поиск... + + + ОК + + + Повторить + + + Версия Python + + + Перезапустить + + + Подтвердите удаление + + + Это приведет к удалению папки пакета и всего её содержимого, включая любые сгенерированные картинки и файлы, которые вы, возможно, добавили. + + + Удаляю пакет... + + + Пакет удалён + + + Некоторые файлы не удалось удалить. Пожалуйста, закройте все открытые файлы в каталоге пакетов и повторите попытку. + + + Неправильный тип пакета + + + Обновляю {0} + + + Обновление завершено + + + {0} был обновлён до последней версии + + + Ошибка обновления {0} + + + Ошибка обновления + + + Открыть в браузере + + + Ошибка установки пакета + + + Ветка + + + Автоматически прокручивать до конца + + + Лицензия + + + Расшаривание моделей... + + + Пожалуйста выберите папку с данными + + + Название папки с данными + + + Текущая папка: + + + Приложение перезапустится после обновления + + + Напомнить позже + + + Установить сейчас + + + Заметки о выпуске + + + Открыть проект... + + + Сохранить как... + + + Восстановить вид по умолчанию + + \ No newline at end of file From 6bbb58305c76f4190596b430e639b8969378c719 Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 23 Oct 2023 13:04:09 -0400 Subject: [PATCH 02/12] Update ru-RU locale name --- StabilityMatrix.Avalonia/Languages/Cultures.cs | 2 +- .../Languages/{Resources.ru.resx => Resources.ru-ru.resx} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename StabilityMatrix.Avalonia/Languages/{Resources.ru.resx => Resources.ru-ru.resx} (100%) diff --git a/StabilityMatrix.Avalonia/Languages/Cultures.cs b/StabilityMatrix.Avalonia/Languages/Cultures.cs index b9d00f61..77fb993a 100644 --- a/StabilityMatrix.Avalonia/Languages/Cultures.cs +++ b/StabilityMatrix.Avalonia/Languages/Cultures.cs @@ -25,7 +25,7 @@ public static class Cultures ["it-IT"] = new("it-IT"), ["fr-FR"] = new("fr-FR"), ["es"] = new("es"), - ["ru"] = new("ru") + ["ru-RU"] = new("ru-RU") }; public static IReadOnlyList SupportedCultures => diff --git a/StabilityMatrix.Avalonia/Languages/Resources.ru.resx b/StabilityMatrix.Avalonia/Languages/Resources.ru-ru.resx similarity index 100% rename from StabilityMatrix.Avalonia/Languages/Resources.ru.resx rename to StabilityMatrix.Avalonia/Languages/Resources.ru-ru.resx From 63c166f7cc4acea8b9e4ff3e88e3fef7fa3d0440 Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 23 Oct 2023 13:08:50 -0400 Subject: [PATCH 03/12] Add titlecase display for language options --- .../Converters/CultureInfoDisplayConverter.cs | 28 +++++++++++++++++++ .../Views/SettingsPage.axaml | 7 ++++- 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 StabilityMatrix.Avalonia/Converters/CultureInfoDisplayConverter.cs diff --git a/StabilityMatrix.Avalonia/Converters/CultureInfoDisplayConverter.cs b/StabilityMatrix.Avalonia/Converters/CultureInfoDisplayConverter.cs new file mode 100644 index 00000000..d51a3d0a --- /dev/null +++ b/StabilityMatrix.Avalonia/Converters/CultureInfoDisplayConverter.cs @@ -0,0 +1,28 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace StabilityMatrix.Avalonia.Converters; + +public class CultureInfoDisplayConverter : IValueConverter +{ + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is not CultureInfo cultureInfo) + return null; + + return cultureInfo.TextInfo.ToTitleCase(cultureInfo.NativeName); + } + + /// + public object ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture + ) + { + throw new NotImplementedException(); + } +} diff --git a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml index 21ab36ac..7a1e324f 100644 --- a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml @@ -10,12 +10,17 @@ xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels" xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" + xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters" d:DataContext="{x:Static mocks:DesignData.SettingsViewModel}" d:DesignHeight="700" d:DesignWidth="800" x:CompileBindings="True" x:DataType="vm:SettingsViewModel" mc:Ignorable="d"> + + + + @@ -46,7 +51,7 @@ From b0d901926879aa290293d1527c0f19815a94d27f Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 29 Oct 2023 18:30:23 -0400 Subject: [PATCH 04/12] Add new generic parsing for generationparams --- .../Models/GenerationParameters.cs | 80 ++++++++++++------- 1 file changed, 49 insertions(+), 31 deletions(-) diff --git a/StabilityMatrix.Core/Models/GenerationParameters.cs b/StabilityMatrix.Core/Models/GenerationParameters.cs index 9ab6c778..e1f69789 100644 --- a/StabilityMatrix.Core/Models/GenerationParameters.cs +++ b/StabilityMatrix.Core/Models/GenerationParameters.cs @@ -1,12 +1,11 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; -using System.Text.RegularExpressions; using StabilityMatrix.Core.Models.Api.Comfy; namespace StabilityMatrix.Core.Models; [JsonSerializable(typeof(GenerationParameters))] -public partial record GenerationParameters +public record GenerationParameters { public string? PositivePrompt { get; set; } public string? NegativePrompt { get; set; } @@ -51,43 +50,68 @@ public partial record GenerationParameters } // Join lines before last line, split at 'Negative prompt: ' - var joinedLines = string.Join("\n", lines[..^1]); + var joinedLines = string.Join("\n", lines[..^1]).Trim(); - var splitFirstPart = joinedLines.Split("Negative prompt: "); - if (splitFirstPart.Length != 2) - { - generationParameters = null; - return false; - } + var splitFirstPart = joinedLines.Split("Negative prompt: ", 2); - var positivePrompt = splitFirstPart[0]; - var negativePrompt = splitFirstPart[1]; + var positivePrompt = splitFirstPart.ElementAtOrDefault(0); + var negativePrompt = splitFirstPart.ElementAtOrDefault(1); // Parse last line - var match = ParseLastLineRegex().Match(lastLine); - if (!match.Success) - { - generationParameters = null; - return false; - } + var lineFields = ParseLine(lastLine); generationParameters = new GenerationParameters { PositivePrompt = positivePrompt, NegativePrompt = negativePrompt, - Steps = int.Parse(match.Groups["Steps"].Value), - Sampler = match.Groups["Sampler"].Value, - CfgScale = double.Parse(match.Groups["CfgScale"].Value), - Seed = ulong.Parse(match.Groups["Seed"].Value), - Height = int.Parse(match.Groups["Height"].Value), - Width = int.Parse(match.Groups["Width"].Value), - ModelHash = match.Groups["ModelHash"].Value, - ModelName = match.Groups["ModelName"].Value, + Steps = int.Parse(lineFields.GetValueOrDefault("Steps", "0")), + Sampler = lineFields.GetValueOrDefault("Sampler"), + CfgScale = double.Parse(lineFields.GetValueOrDefault("CFG scale", "0")), + Seed = ulong.Parse(lineFields.GetValueOrDefault("Seed", "0")), + ModelHash = lineFields.GetValueOrDefault("Model hash"), + ModelName = lineFields.GetValueOrDefault("Model"), }; + if (lineFields.GetValueOrDefault("Size") is { } size) + { + var split = size.Split('x', 2); + if (split.Length == 2) + { + generationParameters = generationParameters with + { + Width = int.Parse(split[0]), + Height = int.Parse(split[1]) + }; + } + } + return true; } + /// + /// Parse A1111 metadata fields in a single line where + /// fields are separated by commas and key-value pairs are separated by colons. + /// i.e. "key1: value1, key2: value2" + /// + internal static Dictionary ParseLine(string fields) + { + var dict = new Dictionary(); + + foreach (var field in fields.Split(',')) + { + var split = field.Split(':', 2); + if (split.Length < 2) + continue; + + var key = split[0].Trim(); + var value = split[1].Trim(); + + dict.Add(key, value); + } + + return dict; + } + /// /// Converts current string to and . /// @@ -145,10 +169,4 @@ public partial record GenerationParameters Sampler = "DPM++ 2M Karras" }; } - - // Example: Steps: 30, Sampler: DPM++ 2M Karras, CFG scale: 7, Seed: 2216407431, Size: 640x896, Model hash: eb2h052f91, Model: anime_v1 - [GeneratedRegex( - """^Steps: (?\d+), Sampler: (?.+?), CFG scale: (?\d+(\.\d+)?), Seed: (?\d+), Size: (?\d+)x(?\d+), Model hash: (?.+?), Model: (?.+)$""" - )] - private static partial Regex ParseLastLineRegex(); } From 53e7ce172a5f4a61edecfe95cc196865b385e08a Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 29 Oct 2023 18:30:39 -0400 Subject: [PATCH 05/12] Add unit tests for GenerationParameters parsing --- .../Models/GenerationParametersTests.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 StabilityMatrix.Tests/Models/GenerationParametersTests.cs diff --git a/StabilityMatrix.Tests/Models/GenerationParametersTests.cs b/StabilityMatrix.Tests/Models/GenerationParametersTests.cs new file mode 100644 index 00000000..555bb3a7 --- /dev/null +++ b/StabilityMatrix.Tests/Models/GenerationParametersTests.cs @@ -0,0 +1,47 @@ +using StabilityMatrix.Core.Models; + +namespace StabilityMatrix.Tests.Models; + +[TestClass] +public class GenerationParametersTests +{ + [TestMethod] + public void TestParse_NoNegative() + { + const string data = """ + test123 + Steps: 20, Sampler: Euler a, CFG scale: 7, Seed: 3589107295, Size: 1024x1028, Model hash: 9aa0c3e54d, Model: nightvisionXL_v0770_BakedVAE, VAE hash: 235745af8d, VAE: sdxl_vae.safetensors, Style Selector Enabled: True, Style Selector Randomize: False, Style Selector Style: base, Version: 1.6.0 + """; + + Assert.IsTrue(GenerationParameters.TryParse(data, out var result)); + + Assert.AreEqual("test123", result.PositivePrompt); + Assert.IsNull(result.NegativePrompt); + Assert.AreEqual(20, result.Steps); + Assert.AreEqual("Euler a", result.Sampler); + Assert.AreEqual(7, result.CfgScale); + Assert.AreEqual(3589107295, result.Seed); + Assert.AreEqual(1024, result.Width); + Assert.AreEqual(1028, result.Height); + Assert.AreEqual("9aa0c3e54d", result.ModelHash); + Assert.AreEqual("nightvisionXL_v0770_BakedVAE", result.ModelName); + } + + [TestMethod] + public void TestParseLineFields() + { + const string lastLine = + @"Steps: 30, Sampler: DPM++ 2M Karras, CFG scale: 7, Seed: 2216407431, Size: 640x896, Model hash: eb2h052f91, Model: anime_v1"; + + var fields = GenerationParameters.ParseLine(lastLine); + + Assert.AreEqual(7, fields.Count); + Assert.AreEqual("30", fields["Steps"]); + Assert.AreEqual("DPM++ 2M Karras", fields["Sampler"]); + Assert.AreEqual("7", fields["CFG scale"]); + Assert.AreEqual("2216407431", fields["Seed"]); + Assert.AreEqual("640x896", fields["Size"]); + Assert.AreEqual("eb2h052f91", fields["Model hash"]); + Assert.AreEqual("anime_v1", fields["Model"]); + } +} From 3469f29228195849d08caf280c9423cb7d99aeda Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 29 Oct 2023 18:33:13 -0400 Subject: [PATCH 06/12] Fix trailing newlines, more unit tests --- .../Models/GenerationParameters.cs | 4 ++-- .../Models/GenerationParametersTests.cs | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/StabilityMatrix.Core/Models/GenerationParameters.cs b/StabilityMatrix.Core/Models/GenerationParameters.cs index e1f69789..90ebd380 100644 --- a/StabilityMatrix.Core/Models/GenerationParameters.cs +++ b/StabilityMatrix.Core/Models/GenerationParameters.cs @@ -54,8 +54,8 @@ public record GenerationParameters var splitFirstPart = joinedLines.Split("Negative prompt: ", 2); - var positivePrompt = splitFirstPart.ElementAtOrDefault(0); - var negativePrompt = splitFirstPart.ElementAtOrDefault(1); + var positivePrompt = splitFirstPart.ElementAtOrDefault(0)?.Trim(); + var negativePrompt = splitFirstPart.ElementAtOrDefault(1)?.Trim(); // Parse last line var lineFields = ParseLine(lastLine); diff --git a/StabilityMatrix.Tests/Models/GenerationParametersTests.cs b/StabilityMatrix.Tests/Models/GenerationParametersTests.cs index 555bb3a7..d22caf8c 100644 --- a/StabilityMatrix.Tests/Models/GenerationParametersTests.cs +++ b/StabilityMatrix.Tests/Models/GenerationParametersTests.cs @@ -5,6 +5,29 @@ namespace StabilityMatrix.Tests.Models; [TestClass] public class GenerationParametersTests { + [TestMethod] + public void TestParse() + { + const string data = """ + test123 + Negative prompt: test, easy negative + Steps: 20, Sampler: Euler a, CFG scale: 7, Seed: 3589107295, Size: 1024x1028, Model hash: 9aa0c3e54d, Model: nightvisionXL_v0770_BakedVAE, VAE hash: 235745af8d, VAE: sdxl_vae.safetensors, Style Selector Enabled: True, Style Selector Randomize: False, Style Selector Style: base, Version: 1.6.0 + """; + + Assert.IsTrue(GenerationParameters.TryParse(data, out var result)); + + Assert.AreEqual("test123", result.PositivePrompt); + Assert.AreEqual("test, easy negative", result.NegativePrompt); + Assert.AreEqual(20, result.Steps); + Assert.AreEqual("Euler a", result.Sampler); + Assert.AreEqual(7, result.CfgScale); + Assert.AreEqual(3589107295, result.Seed); + Assert.AreEqual(1024, result.Width); + Assert.AreEqual(1028, result.Height); + Assert.AreEqual("9aa0c3e54d", result.ModelHash); + Assert.AreEqual("nightvisionXL_v0770_BakedVAE", result.ModelName); + } + [TestMethod] public void TestParse_NoNegative() { From 6aa297c308feb4879112da753d08f8c839a48877 Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 30 Oct 2023 15:12:19 -0400 Subject: [PATCH 07/12] Add FilePath.Extension property --- StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs b/StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs index 3e7e3e39..ffe856df 100644 --- a/StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs +++ b/StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs @@ -33,6 +33,9 @@ public class FilePath : FileSystemPath, IPathObject [JsonIgnore] public string NameWithoutExtension => Path.GetFileNameWithoutExtension(Info.Name); + [JsonIgnore] + public string Extension => Info.Extension; + /// /// Get the directory of the file. /// From 2f290aea571a186d6e62a351ea2b1ccdd266c881 Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 30 Oct 2023 15:12:56 -0400 Subject: [PATCH 08/12] Simplify image indexing with new FilePath methods --- StabilityMatrix.Core/Services/ImageIndexService.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/StabilityMatrix.Core/Services/ImageIndexService.cs b/StabilityMatrix.Core/Services/ImageIndexService.cs index 2e917e63..8543c24f 100644 --- a/StabilityMatrix.Core/Services/ImageIndexService.cs +++ b/StabilityMatrix.Core/Services/ImageIndexService.cs @@ -61,10 +61,11 @@ public class ImageIndexService : IImageIndexService await Task.Run(() => { - var files = imagesDir.Info + var files = imagesDir .EnumerateFiles("*.*", SearchOption.AllDirectories) - .Where(info => LocalImageFile.SupportedImageExtensions.Contains(info.Extension)) - .Select(info => new FilePath(info)); + .Where( + file => LocalImageFile.SupportedImageExtensions.Contains(file.Extension) + ); Parallel.ForEach( files, From 9a07a7dff15d430b7f34a7d97724fce85502605d Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 30 Oct 2023 15:58:41 -0400 Subject: [PATCH 09/12] Simplify LocalImageFile comparisons with record --- .../Models/Database/LocalImageFile.cs | 32 ++++--------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/StabilityMatrix.Core/Models/Database/LocalImageFile.cs b/StabilityMatrix.Core/Models/Database/LocalImageFile.cs index 7e468c87..b966c999 100644 --- a/StabilityMatrix.Core/Models/Database/LocalImageFile.cs +++ b/StabilityMatrix.Core/Models/Database/LocalImageFile.cs @@ -1,5 +1,4 @@ -using LiteDB; -using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.FileInterfaces; using JsonSerializer = System.Text.Json.JsonSerializer; using Size = System.Drawing.Size; @@ -9,7 +8,7 @@ namespace StabilityMatrix.Core.Models.Database; /// /// Represents a locally indexed image file. /// -public class LocalImageFile +public record LocalImageFile { public required string AbsolutePath { get; init; } @@ -120,34 +119,15 @@ public class LocalImageFile private sealed class LocalImageFileEqualityComparer : IEqualityComparer { - public bool Equals(LocalImageFile? x, LocalImageFile? y) + /// + public bool Equals(LocalImageFile x, LocalImageFile y) { - if (ReferenceEquals(x, y)) - return true; - if (ReferenceEquals(x, null)) - return false; - if (ReferenceEquals(y, null)) - return false; - if (x.GetType() != y.GetType()) - return false; - return x.AbsolutePath == y.AbsolutePath - && x.ImageType == y.ImageType - && x.CreatedAt.Equals(y.CreatedAt) - && x.LastModifiedAt.Equals(y.LastModifiedAt) - && Equals(x.GenerationParameters, y.GenerationParameters) - && Nullable.Equals(x.ImageSize, y.ImageSize); + return x == y; } public int GetHashCode(LocalImageFile obj) { - return HashCode.Combine( - obj.AbsolutePath, - obj.ImageType, - obj.CreatedAt, - obj.LastModifiedAt, - obj.GenerationParameters, - obj.ImageSize - ); + return obj.GetHashCode(); } } From 167353bab2e857cdc985c56e1cba0431ee6722f5 Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 30 Oct 2023 15:59:18 -0400 Subject: [PATCH 10/12] Use ImageSearcher for Outputs and ImageFolderCard --- .../Helpers/ImageSearcher.cs | 95 +++++++++++++++++++ .../Inference/ImageFolderCardViewModel.cs | 11 ++- .../ViewModels/OutputsPageViewModel.cs | 47 ++++----- 3 files changed, 117 insertions(+), 36 deletions(-) create mode 100644 StabilityMatrix.Avalonia/Helpers/ImageSearcher.cs diff --git a/StabilityMatrix.Avalonia/Helpers/ImageSearcher.cs b/StabilityMatrix.Avalonia/Helpers/ImageSearcher.cs new file mode 100644 index 00000000..b5d280fe --- /dev/null +++ b/StabilityMatrix.Avalonia/Helpers/ImageSearcher.cs @@ -0,0 +1,95 @@ +using System; +using FuzzySharp; +using FuzzySharp.PreProcess; +using StabilityMatrix.Core.Models.Database; + +namespace StabilityMatrix.Avalonia.Helpers; + +public class ImageSearcher +{ + public int MinimumFuzzScore { get; init; } = 80; + + public ImageSearchOptions SearchOptions { get; init; } = ImageSearchOptions.All; + + public Func GetPredicate(string? searchQuery) + { + if (string.IsNullOrEmpty(searchQuery)) + { + return _ => true; + } + + return file => + { + if (file.FileName.Contains(searchQuery, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if ( + SearchOptions.HasFlag(ImageSearchOptions.FileName) + && Fuzz.WeightedRatio(searchQuery, file.FileName, PreprocessMode.Full) + > MinimumFuzzScore + ) + { + return true; + } + + // Generation params + if (file.GenerationParameters is { } parameters) + { + if ( + SearchOptions.HasFlag(ImageSearchOptions.PositivePrompt) + && ( + parameters.PositivePrompt?.Contains( + searchQuery, + StringComparison.OrdinalIgnoreCase + ) ?? false + ) + || SearchOptions.HasFlag(ImageSearchOptions.NegativePrompt) + && ( + parameters.NegativePrompt?.Contains( + searchQuery, + StringComparison.OrdinalIgnoreCase + ) ?? false + ) + || SearchOptions.HasFlag(ImageSearchOptions.Seed) + && parameters.Seed + .ToString() + .StartsWith(searchQuery, StringComparison.OrdinalIgnoreCase) + || SearchOptions.HasFlag(ImageSearchOptions.Sampler) + && ( + parameters.Sampler?.StartsWith( + searchQuery, + StringComparison.OrdinalIgnoreCase + ) ?? false + ) + || SearchOptions.HasFlag(ImageSearchOptions.ModelName) + && ( + parameters.ModelName?.StartsWith( + searchQuery, + StringComparison.OrdinalIgnoreCase + ) ?? false + ) + ) + { + return true; + } + } + + return false; + }; + } + + [Flags] + public enum ImageSearchOptions + { + None = 0, + FileName = 1 << 0, + PositivePrompt = 1 << 1, + NegativePrompt = 1 << 2, + Seed = 1 << 3, + Sampler = 1 << 4, + ModelName = 1 << 5, + All = int.MaxValue + } +} diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs index 998c4c5c..7502f496 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs @@ -66,17 +66,18 @@ public partial class ImageFolderCardViewModel : ViewModelBase this.settingsManager = settingsManager; this.notificationService = notificationService; - var predicate = this.WhenPropertyChanged(vm => vm.SearchQuery) + var searcher = new ImageSearcher(); + + // Observable predicate from SearchQuery changes + var searchPredicate = this.WhenPropertyChanged(vm => vm.SearchQuery) .Throttle(TimeSpan.FromMilliseconds(50))! - .Select, Func>( - p => file => SearchPredicate(file, p.Value) - ) + .Select(property => searcher.GetPredicate(property.Value)) .AsObservable(); imageIndexService.InferenceImages.ItemsSource .Connect() .DeferUntilLoaded() - .Filter(predicate) + .Filter(searchPredicate) .SortBy(file => file.LastModifiedAt, SortDirection.Descending) .Bind(LocalImages) .Subscribe(); diff --git a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs index 17011e90..d001b861 100644 --- a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs @@ -17,6 +17,7 @@ using FluentAvalonia.UI.Controls; using Microsoft.Extensions.Logging; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Extensions; +using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; @@ -51,8 +52,8 @@ public partial class OutputsPageViewModel : PageViewModelBase public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Grid, IsFilled = true }; - public SourceCache OutputsCache { get; } = - new(p => p.ImageFile.AbsolutePath); + public SourceCache OutputsCache { get; } = + new(file => file.AbsolutePath); public IObservableCollection Outputs { get; set; } = new ObservableCollectionExtended(); @@ -101,35 +102,20 @@ public partial class OutputsPageViewModel : PageViewModelBase this.navigationService = navigationService; this.logger = logger; - var predicate = this.WhenPropertyChanged(vm => vm.SearchQuery) + var searcher = new ImageSearcher(); + + // Observable predicate from SearchQuery changes + var searchPredicate = this.WhenPropertyChanged(vm => vm.SearchQuery) .Throttle(TimeSpan.FromMilliseconds(50))! - .Select, Func>( - propertyValue => - output => - { - if (string.IsNullOrWhiteSpace(propertyValue.Value)) - return true; - - return output.ImageFile.FileName.Contains( - propertyValue.Value, - StringComparison.OrdinalIgnoreCase - ) - || ( - output.ImageFile.GenerationParameters?.PositivePrompt != null - && output.ImageFile.GenerationParameters.PositivePrompt.Contains( - propertyValue.Value, - StringComparison.OrdinalIgnoreCase - ) - ); - } - ) + .Select(property => searcher.GetPredicate(property.Value)) .AsObservable(); OutputsCache .Connect() .DeferUntilLoaded() - .Filter(predicate) - .SortBy(x => x.ImageFile.CreatedAt, SortDirection.Descending) + .Filter(searchPredicate) + .SortBy(file => file.CreatedAt, SortDirection.Descending) + .Transform(file => new OutputImageViewModel(file)) .Bind(Outputs) .WhenPropertyChanged(p => p.IsSelected) .Subscribe(_ => @@ -314,7 +300,7 @@ public partial class OutputsPageViewModel : PageViewModelBase return; } - OutputsCache.Remove(item); + OutputsCache.Remove(item.ImageFile); // Invalidate cache if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader) @@ -378,7 +364,7 @@ public partial class OutputsPageViewModel : PageViewModelBase { continue; } - OutputsCache.Remove(output); + OutputsCache.Remove(output.ImageFile); // Invalidate cache if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader) @@ -402,11 +388,10 @@ public partial class OutputsPageViewModel : PageViewModelBase return; } - var list = Directory + var files = Directory .EnumerateFiles(directory, "*.png", SearchOption.AllDirectories) - .Select(file => new OutputImageViewModel(LocalImageFile.FromPath(file))) - .OrderByDescending(f => f.ImageFile.CreatedAt); + .Select(file => LocalImageFile.FromPath(file)); - OutputsCache.EditDiff(list, (x, y) => x.ImageFile.AbsolutePath == y.ImageFile.AbsolutePath); + OutputsCache.EditDiff(files, LocalImageFile.Comparer); } } From a04ef6e8c42289129a65506b799c8782087f0821 Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 30 Oct 2023 15:59:36 -0400 Subject: [PATCH 11/12] Add LocalImageFile tests --- .../Models/LocalImageFileTests.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 StabilityMatrix.Tests/Models/LocalImageFileTests.cs diff --git a/StabilityMatrix.Tests/Models/LocalImageFileTests.cs b/StabilityMatrix.Tests/Models/LocalImageFileTests.cs new file mode 100644 index 00000000..725df8cf --- /dev/null +++ b/StabilityMatrix.Tests/Models/LocalImageFileTests.cs @@ -0,0 +1,33 @@ +using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.Database; + +namespace StabilityMatrix.Tests.Models; + +[TestClass] +public class LocalImageFileTests +{ + [TestMethod] + public void TestComparer() + { + var comparer = LocalImageFile.Comparer; + + var file1 = new LocalImageFile + { + AbsolutePath = "same", + GenerationParameters = new GenerationParameters { Width = 10 } + }; + + var file2 = new LocalImageFile + { + AbsolutePath = "same", + GenerationParameters = new GenerationParameters { Width = 10 } + }; + + var file3 = new LocalImageFile { AbsolutePath = "different" }; + + Assert.IsTrue(comparer.Equals(file1, file2)); + Assert.AreEqual(comparer.GetHashCode(file1), comparer.GetHashCode(file2)); + Assert.IsFalse(comparer.Equals(file1, file3)); + Assert.AreNotEqual(comparer.GetHashCode(file1), comparer.GetHashCode(file3)); + } +} From 1b3eef75f4b7d353641a1426efa372340ed9051e Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 30 Oct 2023 16:35:49 -0400 Subject: [PATCH 12/12] Add IEquatable overload for EditDiff --- .../DesignData/MockImageIndexService.cs | 3 +- .../ViewModels/OutputsPageViewModel.cs | 3 +- .../Extensions/DynamicDataExtensions.cs | 35 +++++++++++++++++++ .../Models/Database/LocalImageFile.cs | 20 ++--------- .../Services/ImageIndexService.cs | 3 +- .../Models/LocalImageFileTests.cs | 33 ----------------- 6 files changed, 43 insertions(+), 54 deletions(-) create mode 100644 StabilityMatrix.Core/Extensions/DynamicDataExtensions.cs delete mode 100644 StabilityMatrix.Tests/Models/LocalImageFileTests.cs diff --git a/StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs b/StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs index d6538b55..39474975 100644 --- a/StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs +++ b/StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using DynamicData; +using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Services; @@ -51,7 +52,7 @@ public class MockImageIndexService : IImageIndexService ) }; - indexCollection.ItemsSource.EditDiff(toAdd, LocalImageFile.Comparer); + indexCollection.ItemsSource.EditDiff(toAdd); return Task.CompletedTask; } diff --git a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs index d001b861..730de23b 100644 --- a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs @@ -25,6 +25,7 @@ using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.OutputsPage; using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Models; @@ -392,6 +393,6 @@ public partial class OutputsPageViewModel : PageViewModelBase .EnumerateFiles(directory, "*.png", SearchOption.AllDirectories) .Select(file => LocalImageFile.FromPath(file)); - OutputsCache.EditDiff(files, LocalImageFile.Comparer); + OutputsCache.EditDiff(files); } } diff --git a/StabilityMatrix.Core/Extensions/DynamicDataExtensions.cs b/StabilityMatrix.Core/Extensions/DynamicDataExtensions.cs new file mode 100644 index 00000000..d2aa0d65 --- /dev/null +++ b/StabilityMatrix.Core/Extensions/DynamicDataExtensions.cs @@ -0,0 +1,35 @@ +using DynamicData; + +namespace StabilityMatrix.Core.Extensions; + +public static class DynamicDataExtensions +{ + /// + /// Loads the cache with the specified items in an optimised manner i.e. calculates the differences between the old and new items + /// in the list and amends only the differences. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The items to add, update or delete. + /// source. + public static void EditDiff( + this ISourceCache source, + IEnumerable allItems + ) + where TObject : IEquatable + where TKey : notnull + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (allItems is null) + { + throw new ArgumentNullException(nameof(allItems)); + } + + source.EditDiff(allItems, (x, y) => x.Equals(y)); + } +} diff --git a/StabilityMatrix.Core/Models/Database/LocalImageFile.cs b/StabilityMatrix.Core/Models/Database/LocalImageFile.cs index b966c999..1abde8dd 100644 --- a/StabilityMatrix.Core/Models/Database/LocalImageFile.cs +++ b/StabilityMatrix.Core/Models/Database/LocalImageFile.cs @@ -1,4 +1,5 @@ -using StabilityMatrix.Core.Helper; +using DynamicData.Tests; +using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.FileInterfaces; using JsonSerializer = System.Text.Json.JsonSerializer; using Size = System.Drawing.Size; @@ -116,21 +117,4 @@ public record LocalImageFile public static readonly HashSet SupportedImageExtensions = new() { ".png", ".jpg", ".jpeg", ".webp" }; - - private sealed class LocalImageFileEqualityComparer : IEqualityComparer - { - /// - public bool Equals(LocalImageFile x, LocalImageFile y) - { - return x == y; - } - - public int GetHashCode(LocalImageFile obj) - { - return obj.GetHashCode(); - } - } - - public static IEqualityComparer Comparer { get; } = - new LocalImageFileEqualityComparer(); } diff --git a/StabilityMatrix.Core/Services/ImageIndexService.cs b/StabilityMatrix.Core/Services/ImageIndexService.cs index 8543c24f..c07a6e9d 100644 --- a/StabilityMatrix.Core/Services/ImageIndexService.cs +++ b/StabilityMatrix.Core/Services/ImageIndexService.cs @@ -4,6 +4,7 @@ using AsyncAwaitBestPractices; using DynamicData; using Microsoft.Extensions.Logging; using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Database; @@ -79,7 +80,7 @@ public class ImageIndexService : IImageIndexService var indexElapsed = stopwatch.Elapsed; - indexCollection.ItemsSource.EditDiff(toAdd, LocalImageFile.Comparer); + indexCollection.ItemsSource.EditDiff(toAdd); // End stopwatch.Stop(); diff --git a/StabilityMatrix.Tests/Models/LocalImageFileTests.cs b/StabilityMatrix.Tests/Models/LocalImageFileTests.cs deleted file mode 100644 index 725df8cf..00000000 --- a/StabilityMatrix.Tests/Models/LocalImageFileTests.cs +++ /dev/null @@ -1,33 +0,0 @@ -using StabilityMatrix.Core.Models; -using StabilityMatrix.Core.Models.Database; - -namespace StabilityMatrix.Tests.Models; - -[TestClass] -public class LocalImageFileTests -{ - [TestMethod] - public void TestComparer() - { - var comparer = LocalImageFile.Comparer; - - var file1 = new LocalImageFile - { - AbsolutePath = "same", - GenerationParameters = new GenerationParameters { Width = 10 } - }; - - var file2 = new LocalImageFile - { - AbsolutePath = "same", - GenerationParameters = new GenerationParameters { Width = 10 } - }; - - var file3 = new LocalImageFile { AbsolutePath = "different" }; - - Assert.IsTrue(comparer.Equals(file1, file2)); - Assert.AreEqual(comparer.GetHashCode(file1), comparer.GetHashCode(file2)); - Assert.IsFalse(comparer.Equals(file1, file3)); - Assert.AreNotEqual(comparer.GetHashCode(file1), comparer.GetHashCode(file3)); - } -}