diff --git a/CHANGELOG.md b/CHANGELOG.md index 5daa2ae2..08474b65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,10 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2 - Fixed Inference Prompt Completion window sometimes not showing while typing - Fixed "Show Model Images" toggle on Checkpoints page sometimes displaying cut-off model images +## 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/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/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/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/Languages/Cultures.cs b/StabilityMatrix.Avalonia/Languages/Cultures.cs index eb39a9f0..77fb993a 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-RU"] = new("ru-RU") }; public static IReadOnlyList SupportedCultures => diff --git a/StabilityMatrix.Avalonia/Languages/Resources.ru-ru.resx b/StabilityMatrix.Avalonia/Languages/Resources.ru-ru.resx new file mode 100644 index 00000000..149c0a0b --- /dev/null +++ b/StabilityMatrix.Avalonia/Languages/Resources.ru-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 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 9d743a50..eba86524 100644 --- a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs @@ -19,6 +19,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; @@ -26,6 +27,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; @@ -53,8 +55,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(); @@ -106,35 +108,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(_ => @@ -320,7 +307,7 @@ public partial class OutputsPageViewModel : PageViewModelBase return; } - OutputsCache.Remove(item); + OutputsCache.Remove(item.ImageFile); // Invalidate cache if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader) @@ -384,7 +371,7 @@ public partial class OutputsPageViewModel : PageViewModelBase { continue; } - OutputsCache.Remove(output); + OutputsCache.Remove(output.ImageFile); // Invalidate cache if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader) @@ -482,11 +469,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); } } diff --git a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml index 18d65bb8..b3e47a2b 100644 --- a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml @@ -1,4 +1,4 @@ - + + + + @@ -51,7 +56,7 @@ 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 7e468c87..1abde8dd 100644 --- a/StabilityMatrix.Core/Models/Database/LocalImageFile.cs +++ b/StabilityMatrix.Core/Models/Database/LocalImageFile.cs @@ -1,4 +1,4 @@ -using LiteDB; +using DynamicData.Tests; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.FileInterfaces; using JsonSerializer = System.Text.Json.JsonSerializer; @@ -9,7 +9,7 @@ namespace StabilityMatrix.Core.Models.Database; /// /// Represents a locally indexed image file. /// -public class LocalImageFile +public record LocalImageFile { public required string AbsolutePath { get; init; } @@ -117,40 +117,4 @@ public class LocalImageFile public static readonly HashSet SupportedImageExtensions = new() { ".png", ".jpg", ".jpeg", ".webp" }; - - private sealed class LocalImageFileEqualityComparer : IEqualityComparer - { - 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); - } - - public int GetHashCode(LocalImageFile obj) - { - return HashCode.Combine( - obj.AbsolutePath, - obj.ImageType, - obj.CreatedAt, - obj.LastModifiedAt, - obj.GenerationParameters, - obj.ImageSize - ); - } - } - - public static IEqualityComparer Comparer { get; } = - new LocalImageFileEqualityComparer(); } 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. /// diff --git a/StabilityMatrix.Core/Models/GenerationParameters.cs b/StabilityMatrix.Core/Models/GenerationParameters.cs index 9ab6c778..90ebd380 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)?.Trim(); + var negativePrompt = splitFirstPart.ElementAtOrDefault(1)?.Trim(); // 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(); } diff --git a/StabilityMatrix.Core/Services/ImageIndexService.cs b/StabilityMatrix.Core/Services/ImageIndexService.cs index 2e917e63..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; @@ -61,10 +62,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, @@ -78,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/GenerationParametersTests.cs b/StabilityMatrix.Tests/Models/GenerationParametersTests.cs new file mode 100644 index 00000000..d22caf8c --- /dev/null +++ b/StabilityMatrix.Tests/Models/GenerationParametersTests.cs @@ -0,0 +1,70 @@ +using StabilityMatrix.Core.Models; + +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() + { + 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"]); + } +}