Ionite
1 year ago
committed by
GitHub
15 changed files with 733 additions and 34 deletions
@ -0,0 +1,20 @@
|
||||
name: Remove old artifacts |
||||
|
||||
on: |
||||
workflow_dispatch: |
||||
schedule: |
||||
# Every day at 1am |
||||
- cron: '0 1 * * *' |
||||
|
||||
jobs: |
||||
remove-old-artifacts: |
||||
runs-on: ubuntu-latest |
||||
timeout-minutes: 10 |
||||
|
||||
steps: |
||||
- name: Remove old artifacts |
||||
uses: c-hive/gha-remove-artifacts@v1 |
||||
with: |
||||
age: '1 day' |
||||
skip-tags: true |
||||
skip-recent: 1 |
@ -0,0 +1,18 @@
|
||||
using System.Threading.Tasks; |
||||
using Refit; |
||||
using StabilityMatrix.Models.Api; |
||||
|
||||
namespace StabilityMatrix.Api; |
||||
|
||||
[Headers("User-Agent: StabilityMatrix")] |
||||
public interface IA3WebApi |
||||
{ |
||||
[Get("/internal/ping")] |
||||
Task<string> GetPing(); |
||||
|
||||
[Post("/sdapi/v1/txt2img")] |
||||
Task<ImageResponse> TextToImage([Body] TextToImageRequest request); |
||||
|
||||
[Get("/sdapi/v1/progress")] |
||||
Task<ProgressResponse> GetProgress([Body] ProgressRequest request); |
||||
} |
@ -0,0 +1,50 @@
|
||||
using System; |
||||
using System.Diagnostics; |
||||
using System.Threading.Tasks; |
||||
using System.Windows.Threading; |
||||
|
||||
namespace StabilityMatrix.Helper; |
||||
|
||||
public class AsyncDispatcherTimer : DispatcherTimer |
||||
{ |
||||
public AsyncDispatcherTimer() |
||||
{ |
||||
Tick += AsyncDispatcherTimer_Tick; |
||||
} |
||||
|
||||
private async void AsyncDispatcherTimer_Tick(object? sender, EventArgs e) |
||||
{ |
||||
if (TickTask == null) |
||||
{ |
||||
// no task to run |
||||
return; |
||||
} |
||||
|
||||
if (IsRunning && !IsReentrant) |
||||
{ |
||||
// previous task hasn't completed |
||||
return; |
||||
} |
||||
|
||||
try |
||||
{ |
||||
IsRunning = true; |
||||
await TickTask.Invoke(); |
||||
} |
||||
catch (Exception) |
||||
{ |
||||
Debug.WriteLine("Task Failed"); |
||||
throw; |
||||
} |
||||
finally |
||||
{ |
||||
// allow it to run again |
||||
IsRunning = false; |
||||
} |
||||
} |
||||
|
||||
public bool IsReentrant { get; set; } |
||||
public bool IsRunning { get; private set; } |
||||
|
||||
public Func<Task>? TickTask { get; set; } |
||||
} |
@ -0,0 +1,14 @@
|
||||
using System.Collections.Generic; |
||||
using System.Text.Json.Serialization; |
||||
using StabilityMatrix.ViewModels; |
||||
|
||||
namespace StabilityMatrix.Models.Api; |
||||
|
||||
public class ImageResponse |
||||
{ |
||||
[JsonPropertyName("images")] |
||||
public string[] Images { get; set; } |
||||
|
||||
[JsonPropertyName("info")] |
||||
public string? Info { get; set; } |
||||
} |
@ -0,0 +1,9 @@
|
||||
using System.Text.Json.Serialization; |
||||
|
||||
namespace StabilityMatrix.Models.Api; |
||||
|
||||
public class ProgressRequest |
||||
{ |
||||
[JsonPropertyName("skip_current_image")] |
||||
public bool? SkipCurrentImage { get; set; } |
||||
} |
@ -0,0 +1,24 @@
|
||||
using System.Text.Json.Serialization; |
||||
|
||||
namespace StabilityMatrix.Models.Api; |
||||
|
||||
public class ProgressResponse |
||||
{ |
||||
// Range from 0 to 1 |
||||
[JsonPropertyName("progress")] |
||||
public float Progress { get; set; } |
||||
|
||||
// ETA in seconds |
||||
[JsonPropertyName("eta_relative")] |
||||
public float EtaRelative { get; set; } |
||||
|
||||
// state: dict |
||||
|
||||
// The current image in base64 format. opts.show_progress_every_n_steps is required for this to work |
||||
[JsonPropertyName("current_image")] |
||||
public string? CurrentImage { get; set; } |
||||
|
||||
[JsonPropertyName("textinfo")] |
||||
public string? TextInfo { get; set; } |
||||
|
||||
} |
@ -0,0 +1,134 @@
|
||||
using System.Collections.Generic; |
||||
using System.Text.Json.Serialization; |
||||
|
||||
namespace StabilityMatrix.Models.Api; |
||||
|
||||
|
||||
public class TextToImageRequest |
||||
{ |
||||
[JsonPropertyName("enable_hr")] |
||||
public bool? EnableHr { get; set; } |
||||
|
||||
[JsonPropertyName("denoising_strength")] |
||||
public int? DenoisingStrength { get; set; } |
||||
|
||||
[JsonPropertyName("firstphase_width")] |
||||
public int? FirstPhaseWidth { get; set; } |
||||
|
||||
[JsonPropertyName("firstphase_height")] |
||||
public int? FirstPhaseHeight { get; set; } |
||||
|
||||
[JsonPropertyName("hr_scale")] |
||||
public int? HrScale { get; set; } |
||||
|
||||
[JsonPropertyName("hr_upscaler")] |
||||
public string? HrUpscaler { get; set; } |
||||
|
||||
[JsonPropertyName("hr_second_pass_steps")] |
||||
public int? HrSecondPassSteps { get; set; } |
||||
|
||||
[JsonPropertyName("hr_resize_x")] |
||||
public int? HrResizeX { get; set; } |
||||
|
||||
[JsonPropertyName("hr_resize_y")] |
||||
public int? HrResizeY { get; set; } |
||||
|
||||
[JsonPropertyName("prompt")] |
||||
public string Prompt { get; set; } |
||||
|
||||
[JsonPropertyName("styles")] |
||||
public string?[] Styles { get; set; } |
||||
|
||||
[JsonPropertyName("seed")] |
||||
public int? Seed { get; set; } |
||||
|
||||
[JsonPropertyName("subseed")] |
||||
public int? Subseed { get; set; } |
||||
|
||||
[JsonPropertyName("subseed_strength")] |
||||
public int? SubseedStrength { get; set; } |
||||
|
||||
[JsonPropertyName("seed_resize_from_h")] |
||||
public int? SeedResizeFromH { get; set; } |
||||
|
||||
[JsonPropertyName("seed_resize_from_w")] |
||||
public int? SeedResizeFromW { get; set; } |
||||
|
||||
[JsonPropertyName("sampler_name")] |
||||
public string? SamplerName { get; set; } |
||||
|
||||
[JsonPropertyName("batch_size")] |
||||
public int? BatchSize { get; set; } |
||||
|
||||
[JsonPropertyName("n_iter")] |
||||
public int? NIter { get; set; } |
||||
|
||||
[JsonPropertyName("steps")] |
||||
public int? Steps { get; set; } |
||||
|
||||
[JsonPropertyName("cfg_scale")] |
||||
public int? CfgScale { get; set; } |
||||
|
||||
[JsonPropertyName("width")] |
||||
public int? Width { get; set; } |
||||
|
||||
[JsonPropertyName("height")] |
||||
public int? Height { get; set; } |
||||
|
||||
[JsonPropertyName("restore_faces")] |
||||
public bool? RestoreFaces { get; set; } |
||||
|
||||
[JsonPropertyName("tiling")] |
||||
public bool? Tiling { get; set; } |
||||
|
||||
[JsonPropertyName("do_not_save_samples")] |
||||
public bool? DoNotSaveSamples { get; set; } |
||||
|
||||
[JsonPropertyName("do_not_save_grid")] |
||||
public bool? DoNotSaveGrid { get; set; } |
||||
|
||||
[JsonPropertyName("negative_prompt")] |
||||
public string? NegativePrompt { get; set; } |
||||
|
||||
[JsonPropertyName("eta")] |
||||
public int? Eta { get; set; } |
||||
|
||||
[JsonPropertyName("s_min_uncond")] |
||||
public int? SMinUncond { get; set; } |
||||
|
||||
[JsonPropertyName("s_churn")] |
||||
public int? SChurn { get; set; } |
||||
|
||||
[JsonPropertyName("s_tmax")] |
||||
public int? STmax { get; set; } |
||||
|
||||
[JsonPropertyName("s_tmin")] |
||||
public int? STmin { get; set; } |
||||
|
||||
[JsonPropertyName("s_noise")] |
||||
public int? SNoise { get; set; } |
||||
|
||||
[JsonPropertyName("override_settings")] |
||||
public Dictionary<string, string>? OverrideSettings { get; set; } |
||||
|
||||
[JsonPropertyName("override_settings_restore_afterwards")] |
||||
public bool? OverrideSettingsRestoreAfterwards { get; set; } |
||||
|
||||
[JsonPropertyName("script_args")] |
||||
public string[]? ScriptArgs { get; set; } |
||||
|
||||
[JsonPropertyName("sampler_index")] |
||||
public string? SamplerIndex { get; set; } |
||||
|
||||
[JsonPropertyName("script_name")] |
||||
public string? ScriptName { get; set; } |
||||
|
||||
[JsonPropertyName("send_images")] |
||||
public bool? SendImages { get; set; } |
||||
|
||||
[JsonPropertyName("save_images")] |
||||
public bool? SaveImages { get; set; } |
||||
|
||||
[JsonPropertyName("alwayson_scripts")] |
||||
public Dictionary<string, string>? AlwaysOnScripts { get; set; } |
||||
} |
@ -0,0 +1,157 @@
|
||||
<Page |
||||
Background="{DynamicResource ApplicationBackgroundBrush}" |
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" |
||||
Title="TextToImagePage" |
||||
d:DataContext="{d:DesignInstance Type=viewModels:TextToImageViewModel, |
||||
IsDesignTimeCreatable=True}" |
||||
d:DesignHeight="700" |
||||
d:DesignWidth="1100" |
||||
mc:Ignorable="d" |
||||
x:Class="StabilityMatrix.TextToImagePage" |
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
xmlns:local="clr-namespace:StabilityMatrix" |
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
xmlns:models="clr-namespace:StabilityMatrix.Models" |
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" |
||||
xmlns:viewModels="clr-namespace:StabilityMatrix.ViewModels" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
||||
|
||||
<Grid Margin="20"> |
||||
<Grid.RowDefinitions> |
||||
<RowDefinition Height="*" /> |
||||
<RowDefinition Height="Auto" /> |
||||
</Grid.RowDefinitions> |
||||
|
||||
<!-- Controls --> |
||||
<Grid> |
||||
<Grid.ColumnDefinitions> |
||||
<ColumnDefinition Width="*" /> |
||||
<ColumnDefinition Width="*" /> |
||||
</Grid.ColumnDefinitions> |
||||
|
||||
<ScrollViewer |
||||
HorizontalAlignment="Stretch" |
||||
Margin="8,8,8,16" |
||||
VerticalAlignment="Stretch"> |
||||
<StackPanel> |
||||
<ui:Card HorizontalAlignment="Stretch" Padding="8"> |
||||
<StackPanel Margin="8"> |
||||
<Label |
||||
Content="Prompt" |
||||
FontSize="14" |
||||
Margin="2" /> |
||||
<ui:AutoSuggestBox |
||||
Margin="8" |
||||
Name="PositivePromptBox" |
||||
PlaceholderText="Prompt" |
||||
QuerySubmitted="PositivePromptBox_OnQuerySubmitted" |
||||
SuggestionChosen="PositivePromptBox_OnSuggestionChosen" |
||||
TextChanged="PositivePromptBox_OnTextChanged" /> |
||||
<Label |
||||
Content="Negative Prompt" |
||||
FontSize="14" |
||||
Margin="2" /> |
||||
<ui:AutoSuggestBox |
||||
Margin="8" |
||||
Name="NegativePromptBox" |
||||
PlaceholderText="Negative Prompt" |
||||
QuerySubmitted="NegativePromptBox_OnQuerySubmitted" |
||||
SuggestionChosen="NegativePromptBox_OnSuggestionChosen" |
||||
TextChanged="NegativePromptBox_OnTextChanged" /> |
||||
|
||||
<Grid Margin="8"> |
||||
<Grid.ColumnDefinitions> |
||||
<ColumnDefinition Width="*" /> |
||||
<ColumnDefinition Width="Auto" /> |
||||
</Grid.ColumnDefinitions> |
||||
<Label |
||||
Content="Steps" |
||||
FontSize="14" |
||||
Margin="2" |
||||
VerticalAlignment="Center" /> |
||||
<TextBox |
||||
FontSize="14" |
||||
Grid.Column="1" |
||||
Margin="8" |
||||
Text="{Binding GenerationSteps}" |
||||
VerticalAlignment="Bottom" |
||||
Width="64" /> |
||||
</Grid> |
||||
|
||||
<Slider |
||||
IsSnapToTickEnabled="True" |
||||
Maximum="150" |
||||
Minimum="0" |
||||
TickFrequency="5" |
||||
TickPlacement="BottomRight" |
||||
Value="{Binding GenerationSteps}" /> |
||||
</StackPanel> |
||||
</ui:Card> |
||||
</StackPanel> |
||||
</ScrollViewer> |
||||
|
||||
<!-- Image Viewer --> |
||||
<ui:Card |
||||
Grid.Column="1" |
||||
HorizontalAlignment="Stretch" |
||||
Margin="8,8,8,16" |
||||
Padding="8" |
||||
VerticalAlignment="Top"> |
||||
<Grid> |
||||
<Grid.RowDefinitions> |
||||
<RowDefinition Height="0.2*" /> |
||||
<RowDefinition Height="0.7*" /> |
||||
</Grid.RowDefinitions> |
||||
<StackPanel |
||||
Grid.Row="0" |
||||
Orientation="Vertical" |
||||
VerticalAlignment="Top"> |
||||
<Label |
||||
Content="Image" |
||||
FontSize="14" |
||||
Margin="2" /> |
||||
<ProgressBar |
||||
Maximum="100" |
||||
Value="{Binding ProgressValue, FallbackValue=10}" |
||||
Visibility="{Binding ProgressBarVisibility, FallbackValue=Visible}" |
||||
Width="500" /> |
||||
</StackPanel> |
||||
<Grid |
||||
Background="{DynamicResource ApplicationBackgroundBrush}" |
||||
Grid.Row="1" |
||||
MinHeight="512" |
||||
VerticalAlignment="Stretch"> |
||||
<ui:ProgressRing |
||||
Background="DarkRed" |
||||
Grid.Row="0" |
||||
Height="72" |
||||
HorizontalAlignment="Center" |
||||
IsIndeterminate="True" |
||||
Margin="8" |
||||
VerticalAlignment="Center" |
||||
Visibility="{Binding ProgressRingVisibility, FallbackValue=Collapsed}" |
||||
Width="72" |
||||
x:Name="ImageProgressRing" /> |
||||
<Image |
||||
HorizontalAlignment="Stretch" |
||||
Margin="8" |
||||
Source="{Binding ImagePreview}" |
||||
Stretch="Uniform" |
||||
VerticalAlignment="Stretch" |
||||
Visibility="{Binding ImagePreviewVisibility, FallbackValue=Visible}" /> |
||||
</Grid> |
||||
</Grid> |
||||
</ui:Card> |
||||
</Grid> |
||||
|
||||
<ui:Button |
||||
Appearance="Primary" |
||||
Command="{Binding TextToImageGenerateCommand}" |
||||
Content="Generate Image" |
||||
Grid.Row="1" |
||||
HorizontalAlignment="Center" |
||||
Name="TextToImageButton" /> |
||||
</Grid> |
||||
|
||||
</Page> |
@ -0,0 +1,52 @@
|
||||
using System.Windows.Controls; |
||||
using StabilityMatrix.ViewModels; |
||||
using Wpf.Ui.Controls.AutoSuggestBoxControl; |
||||
|
||||
namespace StabilityMatrix; |
||||
|
||||
public sealed partial class TextToImagePage : Page |
||||
{ |
||||
private TextToImageViewModel ViewModel => (TextToImageViewModel) DataContext; |
||||
|
||||
public TextToImagePage(TextToImageViewModel viewModel) |
||||
{ |
||||
InitializeComponent(); |
||||
DataContext = viewModel; |
||||
} |
||||
|
||||
private void PositivePromptBox_OnQuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args) |
||||
{ |
||||
} |
||||
|
||||
private void PositivePromptBox_OnSuggestionChosen(AutoSuggestBox sender, AutoSuggestBoxSuggestionChosenEventArgs args) |
||||
{ |
||||
} |
||||
|
||||
private void PositivePromptBox_OnTextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) |
||||
{ |
||||
// Update the prompt text when the user types |
||||
if (args.Reason == AutoSuggestionBoxTextChangeReason.UserInput) |
||||
{ |
||||
var fullText = sender.Text; |
||||
ViewModel.PositivePromptText = fullText; |
||||
} |
||||
} |
||||
|
||||
private void NegativePromptBox_OnQuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args) |
||||
{ |
||||
} |
||||
|
||||
private void NegativePromptBox_OnSuggestionChosen(AutoSuggestBox sender, AutoSuggestBoxSuggestionChosenEventArgs args) |
||||
{ |
||||
} |
||||
|
||||
private void NegativePromptBox_OnTextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) |
||||
{ |
||||
// Update the prompt text when the user types |
||||
if (args.Reason == AutoSuggestionBoxTextChangeReason.UserInput) |
||||
{ |
||||
var fullText = sender.Text; |
||||
ViewModel.NegativePromptText = fullText; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,148 @@
|
||||
using System; |
||||
using System.IO; |
||||
using System.Threading.Tasks; |
||||
using System.Windows; |
||||
using System.Windows.Media.Imaging; |
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using CommunityToolkit.Mvvm.Input; |
||||
using Microsoft.Extensions.Logging; |
||||
using NLog; |
||||
using StabilityMatrix.Api; |
||||
using StabilityMatrix.Helper; |
||||
using StabilityMatrix.Models.Api; |
||||
using ILogger = NLog.ILogger; |
||||
|
||||
namespace StabilityMatrix.ViewModels; |
||||
|
||||
public partial class TextToImageViewModel : ObservableObject |
||||
{ |
||||
private readonly ILogger<TextToImageViewModel> logger; |
||||
private readonly IA3WebApi a3WebApi; |
||||
private AsyncDispatcherTimer? progressQueryTimer; |
||||
|
||||
[ObservableProperty] |
||||
private bool isGenerating; |
||||
|
||||
[ObservableProperty] |
||||
[NotifyPropertyChangedFor(nameof(ProgressRingVisibility))] |
||||
[NotifyPropertyChangedFor(nameof(ImagePreviewVisibility))] |
||||
private bool isProgressRingActive; |
||||
|
||||
public Visibility ProgressRingVisibility => IsProgressRingActive ? Visibility.Visible : Visibility.Collapsed; |
||||
|
||||
public Visibility ImagePreviewVisibility => IsProgressRingActive ? Visibility.Collapsed : Visibility.Visible; |
||||
|
||||
[ObservableProperty] |
||||
[NotifyPropertyChangedFor(nameof(ProgressBarVisibility))] |
||||
private int progressValue; |
||||
|
||||
[ObservableProperty] |
||||
private string positivePromptText; |
||||
|
||||
[ObservableProperty] |
||||
private string negativePromptText; |
||||
|
||||
[ObservableProperty] |
||||
private int generationSteps; |
||||
|
||||
[ObservableProperty] |
||||
private BitmapImage? imagePreview; |
||||
|
||||
public Visibility ProgressBarVisibility => ProgressValue > 0 ? Visibility.Visible : Visibility.Collapsed; |
||||
|
||||
public TextToImageViewModel(IA3WebApi a3WebApi, ILogger<TextToImageViewModel> logger) |
||||
{ |
||||
this.logger = logger; |
||||
this.a3WebApi = a3WebApi; |
||||
positivePromptText = "Positive"; |
||||
negativePromptText = "Negative"; |
||||
generationSteps = 10; |
||||
} |
||||
|
||||
private void StartProgressTracking(TimeSpan? interval = null) |
||||
{ |
||||
progressQueryTimer = new AsyncDispatcherTimer |
||||
{ |
||||
Interval = interval ?? TimeSpan.FromMilliseconds(150), |
||||
IsReentrant = false, |
||||
TickTask = OnProgressTrackingTick, |
||||
}; |
||||
progressQueryTimer.Start(); |
||||
} |
||||
|
||||
private void StopProgressTracking() |
||||
{ |
||||
IsProgressRingActive = false; |
||||
ProgressValue = 0; |
||||
progressQueryTimer?.Stop(); |
||||
} |
||||
|
||||
private async Task OnProgressTrackingTick() |
||||
{ |
||||
var request = new ProgressRequest(); |
||||
var response = await a3WebApi.GetProgress(request); |
||||
var progress = response.Progress; |
||||
logger.LogInformation("Image Progress: {ResponseProgress}, ETA: {ResponseEtaRelative} s", response.Progress, response.EtaRelative); |
||||
if (Math.Abs(progress - 1.0) < 0.01) |
||||
{ |
||||
ProgressValue = 100; |
||||
progressQueryTimer?.Stop(); |
||||
} |
||||
else |
||||
{ |
||||
// Update progress |
||||
ProgressValue = (int) Math.Clamp(Math.Ceiling(progress * 100), 0, 100); |
||||
// Update preview image |
||||
var result = response.CurrentImage; |
||||
if (result != null) |
||||
{ |
||||
// Stop indeterminate progress ring |
||||
IsProgressRingActive = false; |
||||
// Set preview image |
||||
var bitmap = Base64ToBitmap(result); |
||||
ImagePreview = bitmap; |
||||
} |
||||
} |
||||
} |
||||
|
||||
private static BitmapImage Base64ToBitmap(string base64String) |
||||
{ |
||||
var imageBytes = Convert.FromBase64String(base64String); |
||||
|
||||
var bitmapImage = new BitmapImage(); |
||||
bitmapImage.BeginInit(); |
||||
|
||||
using var ms = new MemoryStream(imageBytes, 0, imageBytes.Length); |
||||
bitmapImage.StreamSource = ms; |
||||
bitmapImage.CacheOption = BitmapCacheOption.OnLoad; |
||||
bitmapImage.EndInit(); |
||||
return bitmapImage; |
||||
} |
||||
|
||||
[RelayCommand] |
||||
private async void TextToImageGenerate() |
||||
{ |
||||
// Start indeterminate progress ring |
||||
IsProgressRingActive = true; |
||||
|
||||
var request = new TextToImageRequest |
||||
{ |
||||
Prompt = PositivePromptText, |
||||
NegativePrompt = NegativePromptText, |
||||
Steps = GenerationSteps, |
||||
}; |
||||
var task = a3WebApi.TextToImage(request); |
||||
|
||||
// Progress track while waiting for response |
||||
StartProgressTracking(); |
||||
var response = await task; |
||||
StopProgressTracking(); |
||||
|
||||
// Decode base64 image |
||||
var result = response.Images[0]; |
||||
var bitmap = Base64ToBitmap(result); |
||||
|
||||
ImagePreview = bitmap; |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue