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"]);
+ }
+}