Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions docs/Features/Presets.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

(TODO: Info about how presets work and all that)

Presets allow you to save and reuse sets of generation parameters across different workflows. When you create a new preset, you choose the parameters to be applied by that preset when you select it later. You can choose multiple presets, they "overlay" their settings.

When you have presets selected, the parameters they include are locked from editing until you either unselect the presets or "apply" them to your prompt.

# Official SDXL Preset Pack

These are the presets that were used on DreamStudio (Stability AI's web generation platform) around the time that SDXL was launched to the public in 2023.
Expand All @@ -17,3 +21,32 @@ These are the presets that were used on DreamStudio (Stability AI's web generati

- You can click on any preset to use it, or unclick it to disable it.
- (Note: if it behaves weird, refresh the page. Alpha projects do silly things sometimes!)

## Linking Models and LoRA to a Preset

It's common for models and LoRA to have specific recommended values for CFG scale, steps, resolution, sampler, etc. Or, you may just have your own preferences for getting the "look" you want from them.

To do this, create a preset with your preferred settings. Then, go to the *Edit Metadata* dialog for the model or LoRA and pick the preset from the dropdown and save. After that, any time you select that model or LoRA, the linked preset is also selected and can be applied.

Some nuances of this feature:

1. Presets linked to *models* are considered *mutually exclusive* to one another. So, if you select a model, its preset is selected, and if you click another model that has its own preset, the first model's preset is unselected and the new model's preset is selected. This avoids accidentally "stacking" incompatible presets for different models. (LoRA presets don't work this way -- they are just selected with the LoRA and are *not* automatically de-selected if you disable the LoRA.)

2. You *can* have a single Preset linked to multiple models or LoRA, but you can only link one preset at a time to each model or LoRA.

3. These links are *user-level* setting, not global.

4. If a model's linked preset specifies that a given LoRA be included at a certain weight, and that LoRA *also* has a preset, that preset will also be selected. (This only nests one level -- LoRA presets can load other LoRA, but it won't select those other LoRAs' presets.)

5. If you enable the User Setting `AutoApplyModelPresets`, rather than *selecting* the appropriate preset for the model and downstream LoRA, it will immediately *apply* those settings. This is very convenient if you just want to flip between models and have your preferred weights, steps, CFG scale, etc. be set instantly.

6. If you enable the User Setting `IgnoreNestedZeroWeightLoraPresets`, the logic in (3) above is slightly adjusted, so any LoRA in the model's preset that has a weight of `0` will *not* have its own preset loaded. The reasoning for this option is as follows:

- There are many situations where a LoRA is on the same platform as a model, but is incompatible with the model. For example, you shouldn't use an LCM LoRA with a model that already has LCM or DMD "built in."
- When flipping between models, it can be a chore to remember to add or remove these "process" LoRA that apply to some models but not others.
- Presets don't support allowing you to *remove* a LoRA.
- However, you *can add* a LoRA with a weight of `0`. The LoRA still takes up VRAM, but has no impact on the generation.
- So, if you keep accidentally leaving an LCM model active when you switch to a DMD model, you can set the DMD model's preset to "include" the LCM model, but with a weight of `0`.
- It's also likely you'd want to set a preset for the LCM LoRA, because when you *normally* add it, you want to override the usual step count, etc.
- But in this situation with the DMD model, you want the LCM LoRA to be effectively disabled *and* you wouldn't want its preset to override your DMD model's preset.
- If this is not a situation you regularly have to deal with, you can leave this option disabled; it's primarily for users who have lower-end devices who prefer to stick with low-step configurations regardless which model they are using.
6 changes: 6 additions & 0 deletions src/Core/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,12 @@ public class UserUIData : AutoConfiguration
[ConfigComment("If enabled, shifting to next/previous image (eg with arrow keys) in history or batch view,\ncycles at the ends (jumps from the start to the end or vice versa).\nIf disabled, shifting will simply stop at the ends.\nIf 'only arrow keys', cycling happens when you press the arrow keys, but not other actions (eg deleting an image will not cycle).")]
[ManualSettingsOptions(Vals = ["true", "false", "only_arrows"], ManualNames = ["Enabled", "Disabled", "Only Arrow Keys"])]
public string ImageShiftingCycles = "true";

[ConfigComment("When a model with a linked preset is selected, should the preset parameters be applied immediately? By default, the preset is just selected for review.")]
public bool AutoApplyModelPresets = false;

[ConfigComment("If enabled, when applying the model's preset that includes a LoRA with a weight of 0, the LoRA's own preset is ignored.")]
public bool IgnoreModelPresetZeroWeightLoraPresets = false;
}

[ConfigComment("Settings related to the user interface, entirely contained to the frontend.")]
Expand Down
13 changes: 13 additions & 0 deletions src/Pages/_Generate/GenTabModals.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,19 @@
</select></div>
Description: <textarea class="auto-text auto-text-block translate" id="edit_model_description" rows="3" placeholder="Model description text..."></textarea>
<div id="edit_model_image_block"></div>
<div id="edit_model_override_preset_block" class="input-group">
<div class="input-group-content flex-column">
<div class="d-flex flex-row">
<div class="header-label translate">Linked Preset</div>
@WebUtil.TextPopoverButton("linkedpreset", "If set, this preset will be selected when you choose this model. Enable auto-apply in settings to instead apply it automatically.")
</div>
<div class="d-flex flex-row gap-2">
<div id="edit_model_override_preset" class="position-relative d-block w-100"></div>
<button type="button" class="btn btn-sm btn-secondary basic-button translate" onclick="applyPresetOverride()" id="edit_model_preset_set_btn" title="Set to currently-selected preset">Set</button>
<button type="button" class="btn btn-sm btn-danger basic-button translate disabled-dim" onclick="clearPresetOverride()" id="edit_model_preset_clear_btn">Clear</button>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary basic-button translate" onclick="save_edit_model()">Save</button>
Expand Down
8 changes: 4 additions & 4 deletions src/WebAPI/AdminAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ public static JObject AutoConfigToParamData(AutoConfiguration config, bool hideR
JObject output = [];
foreach ((string key, AutoConfiguration.Internal.SingleFieldData data) in config.InternalData.SharedData.Fields)
{
if (data.Field.GetCustomAttribute<SettingHiddenAttribute>() is not null)
{
continue;
}
bool isSecret = data.Field.GetCustomAttribute<ValueIsSecretAttribute>() is not null;
string typeName = data.IsSection ? "group" : T2IParamTypes.SharpTypeToDataType(data.Field.FieldType, false).ToString();
if (typeName is null || typeName == T2IParamDataType.UNSET.ToString())
Expand All @@ -66,10 +70,6 @@ public static JObject AutoConfigToParamData(AutoConfiguration config, bool hideR
{
val = AutoConfigToParamData(subConf);
}
if (data.Field.GetCustomAttribute<SettingHiddenAttribute>() is not null)
{
continue;
}
if (hideRestricted && data.Field.GetCustomAttribute<ValueIsRestrictedAttribute>() is not null)
{
continue;
Expand Down
17 changes: 16 additions & 1 deletion src/WebAPI/BasicAPIFeatures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -226,14 +226,29 @@ public static async Task<JObject> InstallConfirmWS(Session session, WebSocket so
public static async Task<JObject> GetMyUserData(Session session)
{
Settings.User.AutoCompleteData settings = session.User.Settings.AutoComplete;
JObject itemPresetLinks = new JObject();
try
{
string rawJson = session.User.GetGenericData("itempresetlinks", "data");
if (!string.IsNullOrWhiteSpace(rawJson))
{
itemPresetLinks = JObject.Parse(rawJson);
}
}
catch
{
// If JSON parsing fails, just return empty object
itemPresetLinks = new JObject();
}
return new JObject()
{
["user_name"] = session.User.UserID,
["presets"] = new JArray(session.User.GetAllPresets().Select(p => p.NetData()).ToArray()),
["language"] = session.User.Settings.Language,
["permissions"] = JArray.FromObject(session.User.GetPermissions()),
["starred_models"] = JObject.Parse(session.User.GetGenericData("starred_models", "full") ?? "{}"),
["autocompletions"] = string.IsNullOrWhiteSpace(settings.Source) ? null : new JArray(AutoCompleteListHelper.GetData(settings.Source, settings.EscapeParens, settings.Suffix, settings.SpacingMode))
["autocompletions"] = string.IsNullOrWhiteSpace(settings.Source) ? null : new JArray(AutoCompleteListHelper.GetData(settings.Source, settings.EscapeParens, settings.Suffix, settings.SpacingMode)),
["itemPresetLinks"] = itemPresetLinks
};
}

Expand Down
63 changes: 62 additions & 1 deletion src/WebAPI/ModelsAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public static void Register()
API.RegisterAPICall(TestPromptFill, false, Permissions.FundamentalModelAccess);
API.RegisterAPICall(EditWildcard, true, Permissions.EditWildcards);
API.RegisterAPICall(EditModelMetadata, true, Permissions.EditModelMetadata);
API.RegisterAPICall(SaveItemPresetLink, true, Permissions.ManagePresets);
API.RegisterAPICall(ClearItemPresetLink, true, Permissions.ManagePresets);
API.RegisterAPICall(DoModelDownloadWS, true, Permissions.DownloadModels);
API.RegisterAPICall(GetModelHash, true, Permissions.EditModelMetadata);
API.RegisterAPICall(ForwardMetadataRequest, false, Permissions.EditModelMetadata);
Expand Down Expand Up @@ -540,8 +542,8 @@ public static async Task<JObject> EditModelMetadata(Session session,
actualModel.Metadata.LoraDefaultWeight = lora_default_weight;
actualModel.Metadata.LoraDefaultConfinement = lora_default_confinement;
actualModel.Metadata.PredictionType = string.IsNullOrWhiteSpace(prediction_type) ? null : prediction_type;
handler.ResetMetadataFrom(actualModel);
}
handler.ResetMetadataFrom(actualModel);
_ = Utilities.RunCheckedTask(() => actualModel.ResaveModel(), "model resave");
Interlocked.Increment(ref ModelEditID);
return new JObject() { ["success"] = true };
Expand Down Expand Up @@ -889,4 +891,63 @@ void doMoveNow(string oldPath)
Interlocked.Increment(ref ModelEditID);
return new JObject() { ["success"] = true };
}

/// <summary>Saves a reference to a preset for a model or LoRA to the user's data.</summary>
/// <param name="session">The user session context.</param>
/// <param name="itemType">The type of item: 'model' or 'lora'.</param>
/// <param name="itemName">The name/identifier of the model or LoRA.</param>
/// <param name="presetTitle">The title of the preset to link, or empty string to clear the link.</param>
/// <returns>A JSON object containing the success status, the key that was saved/cleared, and the preset title.</returns>
public static async Task<JObject> SaveItemPresetLink(Session session, string itemType, string itemName, string presetTitle)
{
string key = $"{itemType}:{itemName}";
try
{
string rawJson = session.User.GetGenericData("itempresetlinks", "data") ?? "{}";
JObject links = JObject.Parse(rawJson);
if (!string.IsNullOrWhiteSpace(presetTitle))
{
links[key] = presetTitle;
}
else if (links.ContainsKey(key))
{
links.Remove(key);
}
string newJson = links.ToString(Newtonsoft.Json.Formatting.None);
session.User.SaveGenericData("itempresetlinks", "data", newJson);
return new JObject() { ["success"] = true, ["key"] = key, ["preset"] = presetTitle };
}
catch (Exception ex)
{
Logs.Error($"Error saving preset link: {ex.Message}");
return new JObject() { ["success"] = false, ["error"] = ex.Message };
}
}

/// <summary>Clears a reference to a preset for a model or LoRA from the user's data.</summary>
/// <param name="session">The user session context.</param>
/// <param name="itemType">The type of item: 'model' or 'lora'.</param>
/// <param name="itemName">The name/identifier of the model or LoRA.</param>
/// <returns>A JSON object containing the success status and the key that was cleared.</returns>
public static async Task<JObject> ClearItemPresetLink(Session session, string itemType, string itemName)
{
string key = $"{itemType}:{itemName}";
try
{
string rawJson = session.User.GetGenericData("itempresetlinks", "data") ?? "{}";
JObject links = JObject.Parse(rawJson);
if (links.ContainsKey(key))
{
links.Remove(key);
string newJson = links.ToString(Newtonsoft.Json.Formatting.None);
session.User.SaveGenericData("itempresetlinks", "data", newJson);
}
return new JObject() { ["success"] = true, ["key"] = key };
}
catch (Exception ex)
{
Logs.Error($"Error clearing preset link: {ex.Message}");
return new JObject() { ["success"] = false, ["error"] = ex.Message };
}
}
}
9 changes: 9 additions & 0 deletions src/wwwroot/css/genpage.css
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,10 @@ body {
font-size: 80%;
color: var(--text-soft);
}
.model_preset {
color: var(--text-soft);
}

.refresh-button {
margin: 0.5rem;
background-color: var(--button-background);
Expand Down Expand Up @@ -1660,3 +1664,8 @@ body {
height: 1lh;
padding-top: 0 !important;
}
#edit_model_override_preset button.disabled-dim {
opacity: 0.5;
cursor: not-allowed;
}

2 changes: 2 additions & 0 deletions src/wwwroot/js/genpage/gentab/loras.js
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,8 @@ class LoraHelper {
}
else {
this.selected.push(new SelectedLora(name, null, null, data));
// Handle LoRA preset selection/application when adding a LoRA
selectOrApplyLoraPresetOnSelection(name);
}
this.rebuildParams();
this.rebuildUI();
Expand Down
Loading