From 0c76ad96e8b8a1bd1e1a7c0a7ee5ea38318bab94 Mon Sep 17 00:00:00 2001 From: Divided by Zer0 Date: Thu, 31 Aug 2023 19:07:04 +0200 Subject: [PATCH] Feat: Support for all Textual Inversions on CivitAI (#112) --- PopupInfoPanel.gd | 6 + StableHordeClient.gd | 20 +- StableHordeClient.tscn | 174 +++++++++- .../civitai_ti_model_fetch.gd | 115 +++++++ .../civitai_ti_reference.gd | 216 ++++++++++++ .../stable_horde_client.gd | 14 + project.godot | 18 + src/KudosCost.gd | 1 + src/Lora/Lora.gd | 2 + src/Lora/TIInject.gd | 31 ++ src/Lora/TIInject.tscn | 27 ++ src/Lora/TextualInversion.gd | 313 ++++++++++++++++++ src/ParamBus.gd | 16 +- 13 files changed, 948 insertions(+), 5 deletions(-) create mode 100644 addons/stable_horde_client/civitai_ti_model_fetch.gd create mode 100644 addons/stable_horde_client/civitai_ti_reference.gd create mode 100644 src/Lora/TIInject.gd create mode 100644 src/Lora/TIInject.tscn create mode 100644 src/Lora/TextualInversion.gd diff --git a/PopupInfoPanel.gd b/PopupInfoPanel.gd index e3d400a..769d58e 100644 --- a/PopupInfoPanel.gd +++ b/PopupInfoPanel.gd @@ -32,8 +32,10 @@ const DESCRIPTIONS = { "ControlType": "By selecting a Control Type, you will request that the horde utilize the ControlNet technology to process the source image, which will provide significantly more accurate conversion, at the cost of processing time. Using this option will triple the kudos cost for this request and limit the amount of steps you can use.", "ImageIsControl": "When this option is selected, the source image will be treated the intermediate step of a ControlNet processing.", "FetchFromCivitAI": "Will search CivitAI for LoRa which match the provided text.", + "FetchTIsFromCivitAI": "Will search CivitAI for Textual Inversions which match the provided text.", "ShowAllModels": "Will display an list of all known models, from which to select one manually.", "ShowAllLoras": "Will display an list of all known LoRas, from which to select one manually.", + "ShowAllTIs": "Will display an list of all known Textual Inversions, from which to select one manually.", } const META_DESCRIPTIONS = { @@ -46,6 +48,10 @@ const META_DESCRIPTIONS = { "LoRaHover": "Click to show information about this LoRa.", "LoRaDelete": "Click to remove this LoRa from the list.", "LoRaTrigger": "Click to inject this LoRa's triggers into your prompt.", + "TIHover": "Click to show information about this Textual Inversion.", + "TIDelete": "Click to remove this Textual Inversion from the list.", + "TITrigger": "Click to inject this Textual Inversion's triggers into your prompt.", + "TIEmbed": "Click to inject this Textual Inversion's embedding and strength into your prompt. Use this only if you've not autoinjecting this into your prompt.", } var current_hovered_node: Control diff --git a/StableHordeClient.gd b/StableHordeClient.gd index a16abfc..8c3673a 100644 --- a/StableHordeClient.gd +++ b/StableHordeClient.gd @@ -76,6 +76,7 @@ onready var image_is_control = $"%ImageIsControl" # model onready var model = $"%Model" onready var lora = $"%Lora" +onready var ti = $"%TextualInversions" # post-processing onready var pp = $"%PP" # ratings @@ -113,6 +114,7 @@ func _ready(): cancel_button.connect("pressed",self,"_on_CancelButton_pressed") model.connect("prompt_inject_requested",self,"_on_prompt_inject") lora.connect("prompt_inject_requested",self,"_on_prompt_inject") + ti.connect("prompt_inject_requested",self,"_on_prompt_inject") # Ratings EventBus.connect("shared_toggled", self, "_on_shared_toggled") best_of.connect("toggled",self,"on_bestof_toggled") @@ -180,7 +182,8 @@ func _ready(): image_preview, options.shared, control_type, - lora + lora, + ti ) # _models_node: ModelSelection, # _img2img_node: CheckButton, @@ -269,7 +272,7 @@ func _on_image_process_update(stats: Dictionary) -> void: stats_format["restarted"] = " Restarted:" + str(stats.restarted) + '.' progress_text.text = " {waiting} Waiting. {processing} Processing.{restarted} {finished} Finished. ETA {eta} sec. Elapsed {elapsed} sec.".format(stats_format) if stats.queue_position == 0: - status_text.bbcode_text = "Thank you for using the horde! "\ + status_text.bbcode_text = "Thank you for using the AI Horde! "\ + "If you enjoy this service join us in [url=discord]discord[/url] or subscribe on [url=patreon]patreon[/url] if you haven't already." status_text.modulate = Color(0,1,0) elif stats.wait_time > 200 or stats.elapsed_time / 1000> 150: @@ -518,6 +521,8 @@ func _connect_hover_signals() -> void: $"%FetchFromCivitAI", $"%ShowAllModels", $"%ShowAllLoras", + $"%FetchTIsFromCivitAI", + $"%ShowAllTIs", ]: node.connect("mouse_entered", EventBus, "_on_node_hovered", [node]) node.connect("mouse_exited", EventBus, "_on_node_unhovered", [node]) @@ -612,7 +617,9 @@ func _on_generation_rating_failed(message: String) -> void: func _on_nsfw_toggled(button_pressed: bool) -> void: lora.lora_reference_node.nsfw = button_pressed + ti.ti_reference_node.nsfw = button_pressed lora.update_selected_loras_label() + ti.update_selected_tis_label() func _accept_settings() -> void: for slider_config in [ @@ -641,6 +648,10 @@ func _accept_settings() -> void: var loras = lora.selected_loras_list globals.set_setting("loras",loras) stable_horde_client.set("lora", loras) + var tis = ti.selected_tis_list + globals.set_setting("tis",tis) + stable_horde_client.set("tis", tis) + print_debug(tis) stable_horde_client.set("api_key", options.get_api_key()) stable_horde_client.set("karras", karras.pressed) globals.set_setting("karras", karras.pressed) @@ -656,6 +667,7 @@ func _accept_settings() -> void: stable_horde_client.set("gen_seed", seed_edit.text) stable_horde_client.set("post_processing", globals.config.get_value("Parameters", "post_processing", stable_horde_client.post_processing)) stable_horde_client.set("lora", globals.config.get_value("Parameters", "loras", stable_horde_client.lora)) + stable_horde_client.set("tis", globals.config.get_value("Parameters", "tis", stable_horde_client.tis)) if prompt_line_edit.text == '': prompt_line_edit.text = _get_random_placeholder_prompt() stable_horde_client.prompt = prompt_line_edit.text @@ -691,6 +703,10 @@ func _on_load_from_disk_gensettings_loaded(settings) -> void: lora.replace_loras(settings["loras"]) else: lora.replace_loras([]) + if settings.has("tis"): + ti.replace_tis(settings["tis"]) + else: + ti.replace_tis([]) denoising_strength.set_value(settings.get("denoising_strength", 0.7)) if settings.has("control_type"): for idx in range(control_type.get_item_count()): diff --git a/StableHordeClient.tscn b/StableHordeClient.tscn index c2ac0a6..eb26335 100644 --- a/StableHordeClient.tscn +++ b/StableHordeClient.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=54 format=2] +[gd_scene load_steps=56 format=2] [ext_resource path="res://StableHordeClient.gd" type="Script" id=1] [ext_resource path="res://addons/stable_horde_client/stable_horde_client.gd" type="Script" id=2] @@ -42,6 +42,8 @@ [ext_resource path="res://theme/fonts/noto/type/type_body_strong.tres" type="DynamicFont" id=40] [ext_resource path="res://Panel.gd" type="Script" id=41] [ext_resource path="res://theme/components/dark/small_normal_0.tres" type="StyleBox" id=42] +[ext_resource path="res://src/Lora/TextualInversion.gd" type="Script" id=43] +[ext_resource path="res://src/Lora/TIInject.tscn" type="PackedScene" id=44] [sub_resource type="StreamTexture" id=9] flags = 4 @@ -847,6 +849,176 @@ unique_name_in_owner = true script = ExtResource( 28 ) showcase_index = 1 +[node name="TextualInversions" type="VBoxContainer" parent="Margin/Panel/Display/Panels/Controls/Basic"] +unique_name_in_owner = true +margin_top = 84.0 +margin_right = 320.0 +margin_bottom = 116.0 +script = ExtResource( 43 ) + +[node name="TIAutocompleteHBC" type="HBoxContainer" parent="Margin/Panel/Display/Panels/Controls/Basic/TextualInversions"] +margin_right = 320.0 +margin_bottom = 32.0 + +[node name="TISelectLabel" type="Label" parent="Margin/Panel/Display/Panels/Controls/Basic/TextualInversions/TIAutocompleteHBC"] +margin_top = 6.0 +margin_right = 50.0 +margin_bottom = 26.0 +rect_min_size = Vector2( 50, 0 ) +custom_fonts/font = ExtResource( 40 ) +text = "Textual Inversions" + +[node name="TIAutoComplete" parent="Margin/Panel/Display/Panels/Controls/Basic/TextualInversions/TIAutocompleteHBC" instance=ExtResource( 26 )] +unique_name_in_owner = true +margin_left = 54.0 +margin_right = 248.0 +margin_bottom = 32.0 +size_flags_horizontal = 3 +text = "" +placeholder_text = "TI Name" +seek_keys = [ "name", "description", "id" ] +popup_position = 1 + +[node name="FetchTIsFromCivitAI" type="Button" parent="Margin/Panel/Display/Panels/Controls/Basic/TextualInversions/TIAutocompleteHBC"] +unique_name_in_owner = true +margin_left = 252.0 +margin_right = 284.0 +margin_bottom = 32.0 +rect_min_size = Vector2( 32, 32 ) +disabled = true +icon = ExtResource( 14 ) +flat = true +icon_align = 1 +expand_icon = true + +[node name="ShowAllTIs" type="Button" parent="Margin/Panel/Display/Panels/Controls/Basic/TextualInversions/TIAutocompleteHBC"] +unique_name_in_owner = true +margin_left = 288.0 +margin_right = 320.0 +margin_bottom = 32.0 +rect_min_size = Vector2( 32, 32 ) +icon = ExtResource( 15 ) +flat = true +expand_icon = true + +[node name="TIPopupInfo" type="PopupPanel" parent="Margin/Panel/Display/Panels/Controls/Basic/TextualInversions"] +margin_top = 36.0 +margin_right = 512.0 +margin_bottom = 153.0 + +[node name="TIPopupInfoLabel" type="RichTextLabel" parent="Margin/Panel/Display/Panels/Controls/Basic/TextualInversions/TIPopupInfo"] +margin_left = 6.0 +margin_top = 6.0 +margin_right = 506.0 +margin_bottom = 111.0 +rect_min_size = Vector2( 500, 0 ) +bbcode_enabled = true +bbcode_text = "This is some model info + +blah blah blah + +url: https://example.com" +text = "This is some model info + +blah blah blah + +url: https://example.com" +fit_content_height = true +deselect_on_focus_loss_enabled = false + +[node name="SelectedTIs" type="RichTextLabel" parent="Margin/Panel/Display/Panels/Controls/Basic/TextualInversions"] +unique_name_in_owner = true +visible = false +margin_top = 36.0 +margin_right = 320.0 +margin_bottom = 86.0 +rect_min_size = Vector2( 0, 50 ) +bbcode_enabled = true +fit_content_height = true + +[node name="TITriggerSelection" type="PopupMenu" parent="Margin/Panel/Display/Panels/Controls/Basic/TextualInversions/SelectedTIs"] +unique_name_in_owner = true +margin_left = 43.0 +margin_top = -63.0 +margin_right = 63.0 +margin_bottom = -43.0 +hide_on_checkable_item_selection = false + +[node name="TIInfoCard" type="PopupPanel" parent="Margin/Panel/Display/Panels/Controls/Basic/TextualInversions/SelectedTIs"] +unique_name_in_owner = true +margin_left = 151.0 +margin_top = -63.0 +margin_right = 671.0 +margin_bottom = 51.0 +custom_styles/panel = ExtResource( 39 ) + +[node name="VBoxContainer" type="VBoxContainer" parent="Margin/Panel/Display/Panels/Controls/Basic/TextualInversions/SelectedTIs/TIInfoCard"] +margin_left = 10.0 +margin_top = 10.0 +margin_right = 614.0 +margin_bottom = 483.0 + +[node name="TIInfoLabel" type="RichTextLabel" parent="Margin/Panel/Display/Panels/Controls/Basic/TextualInversions/SelectedTIs/TIInfoCard/VBoxContainer"] +unique_name_in_owner = true +margin_right = 604.0 +margin_bottom = 105.0 +rect_min_size = Vector2( 500, 0 ) +focus_mode = 2 +bbcode_enabled = true +bbcode_text = "This is some model info + +blah blah blah + +url: https://example.com" +text = "This is some model info + +blah blah blah + +url: https://example.com" +fit_content_height = true +selection_enabled = true + +[node name="TIModelStrength" parent="Margin/Panel/Display/Panels/Controls/Basic/TextualInversions/SelectedTIs/TIInfoCard/VBoxContainer" instance=ExtResource( 13 )] +unique_name_in_owner = true +margin_left = 0.0 +margin_top = 109.0 +margin_right = 604.0 +margin_bottom = 137.0 +slider_name = "Model Strength" + +[node name="TIInject" parent="Margin/Panel/Display/Panels/Controls/Basic/TextualInversions/SelectedTIs/TIInfoCard/VBoxContainer" instance=ExtResource( 44 )] + +[node name="HBoxContainer" type="HBoxContainer" parent="Margin/Panel/Display/Panels/Controls/Basic/TextualInversions/SelectedTIs/TIInfoCard/VBoxContainer"] +margin_top = 173.0 +margin_right = 604.0 +margin_bottom = 473.0 + +[node name="TIShowcase0" type="TextureRect" parent="Margin/Panel/Display/Panels/Controls/Basic/TextualInversions/SelectedTIs/TIInfoCard/VBoxContainer/HBoxContainer"] +unique_name_in_owner = true +margin_right = 300.0 +margin_bottom = 300.0 +rect_min_size = Vector2( 300, 300 ) +expand = true +stretch_mode = 6 + +[node name="TICivitAIShowcase0" type="HTTPRequest" parent="Margin/Panel/Display/Panels/Controls/Basic/TextualInversions/SelectedTIs/TIInfoCard/VBoxContainer/HBoxContainer/TIShowcase0"] +unique_name_in_owner = true +script = ExtResource( 28 ) + +[node name="TIShowcase1" type="TextureRect" parent="Margin/Panel/Display/Panels/Controls/Basic/TextualInversions/SelectedTIs/TIInfoCard/VBoxContainer/HBoxContainer"] +unique_name_in_owner = true +margin_left = 304.0 +margin_right = 604.0 +margin_bottom = 300.0 +rect_min_size = Vector2( 300, 300 ) +expand = true +stretch_mode = 6 + +[node name="TICivitAIShowcase1" type="HTTPRequest" parent="Margin/Panel/Display/Panels/Controls/Basic/TextualInversions/SelectedTIs/TIInfoCard/VBoxContainer/HBoxContainer/TIShowcase1"] +unique_name_in_owner = true +script = ExtResource( 28 ) +showcase_index = 1 + [node name="TrustedWorkers" type="CheckButton" parent="Margin/Panel/Display/Panels/Controls/Basic"] unique_name_in_owner = true margin_top = 124.0 diff --git a/addons/stable_horde_client/civitai_ti_model_fetch.gd b/addons/stable_horde_client/civitai_ti_model_fetch.gd new file mode 100644 index 0000000..6331a9e --- /dev/null +++ b/addons/stable_horde_client/civitai_ti_model_fetch.gd @@ -0,0 +1,115 @@ +class_name CivitAITextualInversionModelFetch +extends StableHordeHTTPRequest + +signal ti_info_retrieved(ti_details) +signal ti_info_gathering_finished +var url: String +var default_ids : Array + +func _ready() -> void: + service_name = "CivitAI" + # We pick the first reference immediately as we enter the scene + timeout = 60 + +func fetch_metadata(final_url: String) -> void: + if state != States.READY: + push_warning("CivitAI Textual Inversion Reference currently working. Cannot do more than 1 request at a time with the same Stable Horde Model Reference.") + return + state = States.WORKING + var error = request(final_url, [], false, HTTPClient.METHOD_GET) + if error != OK: + var error_msg := "Something went wrong when initiating the request" + push_error(error_msg) + state = States.READY + emit_signal("request_failed",error_msg) + +# Function to overwrite to process valid return from the horde +func process_request(json_ret) -> void: + if typeof(json_ret) != TYPE_DICTIONARY: + var error_msg : String = "Unexpected model reference received" + push_error("Unexpected model reference received" + ': ' + str(json_ret)) + emit_signal("request_failed",error_msg) + state = States.READY + return + if not json_ret.has("items"): + # Quick hack to treat individual items the same way + json_ret["items"] = [json_ret] + for entry in json_ret["items"]: + var ti = _parse_civitai_ti_data(entry) + emit_signal("ti_info_retrieved", ti) + emit_signal("ti_info_gathering_finished") + +func _parse_civitai_ti_data(civitai_entry) -> Dictionary: + var ti_details = { + "name": civitai_entry["name"], + "id": int(civitai_entry["id"]), + "description": civitai_entry["description"], + "unusable": '', + "nsfw": civitai_entry["nsfw"], + "sha256": null, + } + if not ti_details["description"]: + ti_details["description"] = '' + var html_to_bbcode = { + "

": '', + "

": '\n', + "": '[/b]', + "": '[b]', + "": '[/b]', + "": '[b]', + "": '[/i]', + "": '[i]', + "": '[/i]', + "": '[i]', + "
": '\n', + "
": '\n', + "
": '\n', + "

": '[b][color=yellow]', + "

": '[/color][/b]\n', + "

": '[b]', + "

": '[/b]\n', + "

": '', + "

": '', + "": '[u]', + "": '[/u]', + "": '[code]', + "": '[/code]', + "
    ": '[ul]', + "
": '[/ul]', + "
    ": '[ol]', + "
": '[/ol]', + "
  • ": '', + "
  • ": '\n', + "<": '<', + ">": '>', + } + for repl in html_to_bbcode: + ti_details["description"] = ti_details["description"].replace(repl,html_to_bbcode[repl]) + if ti_details["description"].length() > 500: + ti_details["description"] = ti_details["description"].left(700) + ' [...]' + var versions = civitai_entry.get("modelVersions", {}) + if versions.size() == 0: + return ti_details + ti_details["triggers"] = versions[0]["trainedWords"] + ti_details["version"] = versions[0]["name"] + ti_details["base_model"] = versions[0]["baseModel"] + for file in versions[0]["files"]: + ti_details["size_mb"] = round(file["sizeKB"] / 1024) + # We only store these two to check if they would be present in the workers + ti_details["sha256"] = file.get("hashes", {}).get("SHA256") + ti_details["url"] = file.get("downloadUrl", "") + # If these two fields are not defined, the workers are not going to download it + # so we ignore it as well + var is_default = int(ti_details["id"]) in default_ids + if not is_default and not ti_details["sha256"]: + ti_details["unusable"] = 'Attention! This Textual Inversion is unusable because it does not provide file validation.' + elif not ti_details["url"]: + ti_details["unusable"] = 'Attention! This Textual Inversion is unusable because it appears to have no valid safetensors upload.' + elif not is_default and ti_details["size_mb"] > 150: + ti_details["unusable"] = 'Attention! This Textual Inversion is unusable because is exceeds the max 150Mb filesize we allow on the AI Horde.' + ti_details["images"] = [] + for img in versions[0]["images"]: + if img["nsfw"] in ["Mature", "X"]: + continue + ti_details["images"].append(img["url"]) + return ti_details diff --git a/addons/stable_horde_client/civitai_ti_reference.gd b/addons/stable_horde_client/civitai_ti_reference.gd new file mode 100644 index 0000000..91239a7 --- /dev/null +++ b/addons/stable_horde_client/civitai_ti_reference.gd @@ -0,0 +1,216 @@ +class_name CivitAITIReference +extends StableHordeHTTPRequest + +signal reference_retrieved(models_list) + +export(String) var tis_refence_url := "https://civitai.com/api/v1/models?types=TextualInversion&sort=Highest%20Rated&primaryFileOnly=true&limit=100" + + +var ti_reference := {} +var ti_id_index := {} +var models_retrieved = false +var nsfw = true setget set_nsfw +var initialized := false +var default_ids : Array + + +func _ready() -> void: + service_name = "CivitAI" + # We pick the first reference immediately as we enter the scene + timeout = 60 + _load_from_file() + # We do not call it from here, as set_nsfw() will also call it + #get_ti_reference() + +func _get_url(query) -> String: + var final_url : String = '' + if typeof(query) == TYPE_ARRAY: + var idsq = '&ids='.join(query) + final_url = "https://civitai.com/api/v1/models?limit=100&" + idsq + elif query.is_valid_integer(): + final_url = "https://civitai.com/api/v1/models/" + query +# elif query == '': +# initialized = false + else: + final_url = tis_refence_url + '&nsfw=' + str(nsfw).to_lower() + '&query=' + query + return final_url + +func seek_online(query: String) -> void: + if query == '': + return + fetch_ti_metadata(query) + +func fetch_next_page(json_ret: Dictionary) -> void: + var next_page_url = json_ret["metadata"]["nextPage"] + var error = request(next_page_url, [], false, HTTPClient.METHOD_GET) + if error != OK: + var error_msg := "Something went wrong when initiating the request" + push_error(error_msg) + emit_signal("request_failed",error_msg) + +func fetch_ti_metadata(query) -> void: + var new_fetch = CivitAITextualInversionModelFetch.new() + new_fetch.connect("ti_info_retrieved",self,"_on_ti_info_retrieved") + new_fetch.connect("ti_info_gathering_finished",self,"_on_ti_info_gathering_finished", [new_fetch]) + new_fetch.default_ids = default_ids + add_child(new_fetch) + new_fetch.fetch_metadata(_get_url(query)) + +# Function to overwrite to process valid return from the horde +func process_request(json_ret) -> void: + if typeof(json_ret) == TYPE_ARRAY: + default_ids = json_ret + fetch_ti_metadata(default_ids) + state = States.READY + return + if typeof(json_ret) != TYPE_DICTIONARY: + var error_msg : String = "Unexpected model reference received" + push_error("Unexpected model reference received" + ': ' + str(json_ret)) + emit_signal("request_failed",error_msg) + state = States.READY + return + if not json_ret.has("items"): + # Quick hack to treat individual items the same way + json_ret["items"] = [json_ret] + for entry in json_ret["items"]: + if initialized: + var ti = _parse_civitai_ti_data(entry) + if ti.has("size_mb"): + _store_ti(ti) + _store_to_file() + emit_signal("reference_retrieved", ti_reference) + initialized = true + state = States.READY + +func _on_ti_info_retrieved(ti_details: Dictionary) -> void: + _store_ti(ti_details) + _store_to_file() + +func _on_ti_info_gathering_finished(fetch_node: CivitAITextualInversionModelFetch) -> void: + fetch_node.queue_free() + for child in get_children(): + if not child is CivitAITextualInversionModelFetch: + continue + if not child.is_queued_for_deletion(): + return + _store_to_file() + emit_signal("reference_retrieved", ti_reference) + +func is_ti(ti_name: String) -> bool: + if ti_id_index.has(int(ti_name)): + return true + return(ti_reference.has(ti_name)) + +func get_ti_info(ti_name: String) -> Dictionary: + if ti_id_index.has(int(ti_name)): + return ti_reference[ti_id_index[int(ti_name)]] + return ti_reference.get(ti_name, {}) + +func get_ti_name(ti_name: String) -> String: + if ti_id_index.has(int(ti_name)): + return ti_reference[ti_id_index[int(ti_name)]]["name"] + return ti_reference.get(ti_name, {}).get("name", 'N/A') + +func _store_to_file() -> void: + var file = File.new() + file.open("user://civitai_ti_reference", File.WRITE) + file.store_var(ti_reference) + file.close() + +func _load_from_file() -> void: + var file = File.new() + file.open("user://civitai_ti_reference", File.READ) + var filevar = file.get_var() + if filevar: + ti_reference = filevar + for ti in ti_reference.values(): + ti_id_index[int(ti["id"])] = ti["name"] + ti["cached"] = true + # Temporary while changing approach + var unusable = ti.get("unusable", false) + if typeof(unusable) == TYPE_BOOL and unusable == false: + ti["unusable"] = 'Attention! This Textual Inversion is unusable because it does not provide file validation.' + elif typeof(unusable) == TYPE_BOOL: + ti["unusable"] = '' + file.close() + emit_signal("reference_retrieved", ti_reference) + +func _parse_civitai_ti_data(civitai_entry) -> Dictionary: + var ti_details = { + "name": civitai_entry["name"], + "id": int(civitai_entry["id"]), + "description": civitai_entry["description"], + "unusable": '', + } + if not ti_details["description"]: + ti_details["description"] = '' + var html_to_bbcode = { + "

    ": '', + "

    ": '\n', + "
    ": '[/b]', + "": '[b]', + "": '[/b]', + "": '[b]', + "": '[/i]', + "": '[i]', + "": '[/i]', + "": '[i]', + "
    ": '\n', + "
    ": '\n', + "
    ": '\n', + "

    ": '[b][color=yellow]', + "

    ": '[/color][/b]\n', + "

    ": '[b]', + "

    ": '[/b]\n', + "

    ": '', + "

    ": '', + "": '[u]', + "": '[/u]', + "": '[code]', + "": '[/code]', + "
      ": '[ul]', + "
    ": '[/ul]', + "
      ": '[ol]', + "
    ": '[/ol]', + "
  • ": '', + "
  • ": '\n', + "<": '<', + ">": '>', + } + for repl in html_to_bbcode: + ti_details["description"] = ti_details["description"].replace(repl,html_to_bbcode[repl]) + if ti_details["description"].length() > 500: + ti_details["description"] = ti_details["description"].left(700) + ' [...]' + var versions = civitai_entry.get("modelVersions", {}) + if versions.size() == 0: + return ti_details + ti_details["triggers"] = versions[0]["trainedWords"] + ti_details["version"] = versions[0]["name"] + ti_details["base_model"] = versions[0]["baseModel"] + for file in versions[0]["files"]: + ti_details["size_mb"] = round(file["sizeKB"] / 1024) + # We only store these two to check if they would be present in the workers + ti_details["sha256"] = file.get("hashes", {}).get("SHA256") + ti_details["url"] = file.get("downloadUrl", "") + # If these two fields are not defined, the workers are not going to download it + # so we ignore it as well + if not ti_details["sha256"]: + ti_details["unusable"] = 'Attention! This Textual Inversion is unusable because it does not provide file validation.' + elif not ti_details["url"]: + ti_details["unusable"] = 'Attention! This Textual Inversion is unusable because it appears to have no valid safetensors upload.' + elif ti_details["size_mb"] > 150: + ti_details["unusable"] = 'Attention! This Textual Inversion is unusable because is exceeds the max 150Mb filesize we allow on the AI Horde.' + ti_details["images"] = [] + for img in versions[0]["images"]: + if img["nsfw"] in ["Mature", "X"]: + continue + ti_details["images"].append(img["url"]) + return ti_details + +func set_nsfw(value) -> void: + nsfw = value + +func _store_ti(ti_data: Dictionary) -> void: + var ti_name = ti_data["name"] + ti_reference[ti_name] = ti_data + ti_id_index[int(ti_data["id"])] = ti_name diff --git a/addons/stable_horde_client/stable_horde_client.gd b/addons/stable_horde_client/stable_horde_client.gd index ebbab3c..235e3f4 100644 --- a/addons/stable_horde_client/stable_horde_client.gd +++ b/addons/stable_horde_client/stable_horde_client.gd @@ -70,6 +70,7 @@ export(Array) var post_processing := [] # Loras to use. Each entry needs to be a dictionary in the form of #{"name": String, "model": float, "clip": float} export(Array) var lora := [] +export(Array) var tis := [] # If set to True, will enable the karras noise scheduler export(bool) var karras := true # If set to True, will activate the HiRes Fix @@ -135,6 +136,8 @@ func generate(replacement_prompt := '', replacement_params := {}) -> void: imgen_params["control_type"] = control_type if lora.size() > 0: imgen_params["loras"] = _get_loras_payload() + if tis.size() > 0: + imgen_params["tis"] = _get_tis_payload() for param in replacement_params: imgen_params[param] = replacement_params[param] var submit_dict = { @@ -341,3 +344,14 @@ func _get_loras_payload() -> Array: loras_array.append(new_item) return loras_array +func _get_tis_payload() -> Array: + """We replace the name with the ID, to ensure we find it easy on the worker""" + var tis_array = [] + for item in tis: + var new_item = item.duplicate() + if new_item.has("id") and not new_item["name"].is_valid_integer(): + new_item["original_name"] = str(new_item["name"]) + new_item["name"] = str(new_item["id"]) + tis_array.append(new_item) + return tis_array + diff --git a/project.godot b/project.godot index 4fb17f3..ce8bdef 100644 --- a/project.godot +++ b/project.godot @@ -29,6 +29,16 @@ _global_script_classes=[ { "language": "GDScript", "path": "res://addons/stable_horde_client/civitai_showcase.gd" }, { +"base": "StableHordeHTTPRequest", +"class": "CivitAITIReference", +"language": "GDScript", +"path": "res://addons/stable_horde_client/civitai_ti_reference.gd" +}, { +"base": "StableHordeHTTPRequest", +"class": "CivitAITextualInversionModelFetch", +"language": "GDScript", +"path": "res://addons/stable_horde_client/civitai_ti_model_fetch.gd" +}, { "base": "VBoxContainer", "class": "ConfigSlider", "language": "GDScript", @@ -94,6 +104,11 @@ _global_script_classes=[ { "language": "GDScript", "path": "res://addons/stable_horde_client/stable_horde_rate_generation.gd" }, { +"base": "Control", +"class": "TISelection", +"language": "GDScript", +"path": "res://src/Lora/TextualInversion.gd" +}, { "base": "Reference", "class": "ToolConsts", "language": "GDScript", @@ -109,6 +124,8 @@ _global_script_class_icons={ "CivitAILoraReference": "", "CivitAIModelFetch": "", "CivitAIShowcase": "", +"CivitAITIReference": "", +"CivitAITextualInversionModelFetch": "", "ConfigSlider": "", "GridTextureRect": "", "LoraSelection": "", @@ -122,6 +139,7 @@ _global_script_class_icons={ "StableHordeModelShowcase": "", "StableHordeModels": "", "StableHordeRateGeneration": "", +"TISelection": "", "ToolConsts": "", "Utils": "" } diff --git a/src/KudosCost.gd b/src/KudosCost.gd index 1e9fe9e..c463277 100644 --- a/src/KudosCost.gd +++ b/src/KudosCost.gd @@ -42,6 +42,7 @@ func _init_calculate_kudos() -> void: shared = ParamBus.get_shared() control_type = ParamBus.get_control_type() lora = ParamBus.get_loras() + tis = ParamBus.get_tis() generate() func _on_kudos_calculated(kudos_payload: Dictionary) -> void: diff --git a/src/Lora/Lora.gd b/src/Lora/Lora.gd index 13e328b..f8c7877 100644 --- a/src/Lora/Lora.gd +++ b/src/Lora/Lora.gd @@ -248,9 +248,11 @@ func _on_show_all_loras_pressed() -> void: func _on_lora_model_strength_value_changed(value) -> void: selected_loras_list[viewed_lora_index]["model"] = value + emit_signal("loras_modified", selected_loras_list) func _on_lora_clip_strength_value_changed(value) -> void: selected_loras_list[viewed_lora_index]["clip"] = value + emit_signal("loras_modified", selected_loras_list) func _on_fetch_from_civitai_pressed() -> void: fetch_from_civitai.disabled = true diff --git a/src/Lora/TIInject.gd b/src/Lora/TIInject.gd new file mode 100644 index 0000000..0aba533 --- /dev/null +++ b/src/Lora/TIInject.gd @@ -0,0 +1,31 @@ +extends HBoxContainer + +signal value_changed(value) + +onready var ti_inject_button := $"%TIInjectButton" +onready var ti_inject_label = $"%TIInjectLabel" + +func _ready(): + var popup = ti_inject_button.get_popup() + popup.connect("index_pressed", self, "on_index_pressed") + +func on_index_pressed(index) -> void: + match index: + 0: + emit_signal("value_changed", "prompt") + ti_inject_label.text = "Prompt" + 1: + emit_signal("value_changed", "negprompt") + ti_inject_label.text = "Negative prompt" + 2: + emit_signal("value_changed", null) + ti_inject_label.text = "No" + +func set_value(value): + match value: + "prompt": + ti_inject_label.text = "Prompt" + "negprompt": + ti_inject_label.text = "Negative prompt" + null: + ti_inject_label.text = "No" diff --git a/src/Lora/TIInject.tscn b/src/Lora/TIInject.tscn new file mode 100644 index 0000000..c92cf64 --- /dev/null +++ b/src/Lora/TIInject.tscn @@ -0,0 +1,27 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://src/Lora/TIInject.gd" type="Script" id=1] + +[node name="TIInject" type="HBoxContainer"] +unique_name_in_owner = true +margin_right = 40.0 +margin_bottom = 40.0 +script = ExtResource( 1 ) + +[node name="TIInjectButton" type="MenuButton" parent="."] +unique_name_in_owner = true +margin_right = 130.0 +margin_bottom = 40.0 +focus_mode = 2 +text = "Inject Embedding" +flat = false +align = 0 +items = [ "Prompt", null, 0, false, false, 0, 0, null, "", false, "Negative prompt", null, 0, false, false, 1, 0, null, "", false, "No", null, 0, false, false, 2, 0, null, "", false ] + +[node name="TIInjectLabel" type="Label" parent="."] +unique_name_in_owner = true +margin_left = 134.0 +margin_top = 10.0 +margin_right = 183.0 +margin_bottom = 30.0 +text = "Prompt" diff --git a/src/Lora/TextualInversion.gd b/src/Lora/TextualInversion.gd new file mode 100644 index 0000000..bc1ae3c --- /dev/null +++ b/src/Lora/TextualInversion.gd @@ -0,0 +1,313 @@ +class_name TISelection +extends Control + +enum TICompatible { + YES=0 + NO + MAYBE +} + +signal prompt_inject_requested(tokens) +signal tis_modified(tis_list) + +var ti_reference_node: CivitAITIReference +var selected_tis_list : Array = [] +var viewed_ti_index : int = 0 +var civitai_search_initiated = false +var current_models := [] + +onready var ti_auto_complete = $"%TIAutoComplete" +onready var stable_horde_models := $"%StableHordeModels" +onready var ti_trigger_selection := $"%TITriggerSelection" +onready var ti_info_card := $"%TIInfoCard" +onready var ti_info_label := $"%TIInfoLabel" +onready var civitai_showcase0 = $"%TICivitAIShowcase0" +onready var civitai_showcase1 = $"%TICivitAIShowcase1" +onready var ti_showcase0 = $"%TIShowcase0" +onready var ti_showcase1 = $"%TIShowcase1" +onready var selected_tis = $"%SelectedTIs" +onready var show_all_tis = $"%ShowAllTIs" +onready var ti_model_strength = $"%TIModelStrength" +onready var ti_inject = $"%TIInject" +onready var fetch_tis_from_civitai = $"%FetchTIsFromCivitAI" + +func _ready(): + # warning-ignore:return_value_discarded + EventBus.connect("model_selected",self,"on_model_selection_changed") + ti_reference_node = CivitAITIReference.new() + ti_reference_node.nsfw = globals.config.get_value("Parameters", "nsfw") + # warning-ignore:return_value_discarded + ti_reference_node.connect("reference_retrieved",self, "_on_reference_retrieved") + add_child(ti_reference_node) + # warning-ignore:return_value_discarded + # warning-ignore:return_value_discarded + ti_auto_complete.connect("item_selected", self,"_on_ti_selected") + # warning-ignore:return_value_discarded + ti_trigger_selection.connect("id_pressed", self,"_on_trigger_selection_id_pressed") + # warning-ignore:return_value_discarded + civitai_showcase0.connect("showcase_retrieved",self, "_on_showcase0_retrieved") + civitai_showcase1.connect("showcase_retrieved",self, "_on_showcase1_retrieved") + # warning-ignore:return_value_discarded + selected_tis.connect("meta_clicked",self,"_on_selected_tis_meta_clicked") + selected_tis.connect("meta_hover_started",self,"_on_selected_tis_meta_hover_started") + selected_tis.connect("meta_hover_ended",self,"_on_selected_tis_meta_hover_ended") + ti_info_label.connect("meta_clicked",self,"_on_ti_info_label_meta_clicked") + show_all_tis.connect("pressed",self,"_on_show_all_tis_pressed") + ti_info_card.connect("hide",self,"_on_ti_info_card_hide") + ti_model_strength.connect("value_changed",self,"_on_ti_model_strength_value_changed") + ti_inject.connect("value_changed",self,"_on_ti_inject_value_changed") + fetch_tis_from_civitai.connect("pressed",self,"_on_fetch_tis_from_civitai_pressed") + _on_reference_retrieved(ti_reference_node.ti_reference) + selected_tis_list = globals.config.get_value("Parameters", "tis", []) + update_selected_tis_label() + +func replace_tis(tis: Array) -> void: + selected_tis_list = tis + for ti in selected_tis_list: + ti["name"] = ti_reference_node.get_ti_name(ti["name"]) + update_selected_tis_label() + emit_signal("tis_modified", selected_tis_list) + +func _on_ti_selected(ti_name: String) -> void: + if selected_tis_list.size() >= 5: + return + selected_tis_list.append( + { + "name": ti_name, + "strength": 1.0, + "inject_ti": "prompt", + "id": ti_reference_node.get_ti_info(ti_name)["id"], + } + ) + update_selected_tis_label() + EventBus.emit_signal("ti_selected", ti_reference_node.get_ti_info(ti_name)) + emit_signal("tis_modified", selected_tis_list) + +func _on_reference_retrieved(model_reference: Dictionary): + ti_auto_complete.selections = model_reference + fetch_tis_from_civitai.disabled = false + if civitai_search_initiated: + civitai_search_initiated = false + ti_auto_complete.initiate_search() + +func _show_ti_details(ti_name: String) -> void: + var ti_reference := ti_reference_node.get_ti_info(ti_name) + if ti_reference.empty(): + ti_info_label.bbcode_text = "No ti info could not be retrieved at this time." + else: + civitai_showcase0.get_model_showcase(ti_reference) + civitai_showcase1.get_model_showcase(ti_reference) + var fmt = { + "name": ti_reference['name'], + "description": ti_reference['description'], + "version": ti_reference['version'], + "trigger": ", ".join(ti_reference['triggers']), + "url": "https://civitai.com/models/" + str(ti_reference['id']), + "unusable": "", + } + var compatibility = check_baseline_compatibility(ti_name) + if ti_reference.get("unusable"): + fmt["unusable"] = "[color=red]" + ti_reference.get("unusable") + "[/color]\n" + elif compatibility == TICompatible.NO: + fmt["unusable"] = "[color=red]This Textual Inversion base model version is incompatible with the selected Model[/color]\n" + elif compatibility == TICompatible.MAYBE: + fmt["unusable"] = "[color=yellow]You have selected multiple models of varying base versions. This Textual Inversion is not compatible with all of them and will be ignored by the incompatible ones.[/color]\n" + elif not ti_reference_node.nsfw and ti_reference.get("nsfw", false): + fmt["unusable"] = "[color=#FF00FF]SFW workers which pick up the request, will ignore this Textual Inversion.[/color]\n" + var label_text = "{unusable}[b]Name: {name}[/b]\nDescription: {description}\nVersion: {version}\n".format(fmt) + label_text += "\nTriggers: {trigger}".format(fmt) + label_text += "\nCivitAI page: [url={url}]{url}[/url]".format(fmt) + ti_info_label.bbcode_text = label_text + ti_info_card.rect_size = Vector2(0,0) + ti_info_card.popup() + ti_info_card.rect_global_position = get_global_mouse_position() + Vector2(30,-ti_info_card.rect_size.y/2) + +func _on_selected_tis_meta_clicked(meta) -> void: + var meta_split = meta.split(":") + match meta_split[0]: + "hover": + viewed_ti_index = int(meta_split[1]) + ti_model_strength.set_value(selected_tis_list[viewed_ti_index]["strength"]) + ti_inject.set_value(selected_tis_list[viewed_ti_index].get("inject_ti")) + _show_ti_details(selected_tis_list[viewed_ti_index]["name"]) + "delete": + selected_tis_list.remove(int(meta_split[1])) + update_selected_tis_label() + emit_signal("tis_modified", selected_tis_list) + "trigger": + _on_ti_trigger_pressed(int(meta_split[1])) + "embed": + _on_ti_embed_pressed(int(meta_split[1])) + +func _on_selected_tis_meta_hover_started(meta: String) -> void: + var meta_split = meta.split(":") + var info = '' + match meta_split[0]: + "hover": + info = "TIHover" + "delete": + info = "TIDelete" + "trigger": + info = "TITrigger" + "embed": + info = "TIEmbed" + "inject": + info = "TIInject" + EventBus.emit_signal("rtl_meta_hovered",selected_tis,info) + +func _on_selected_tis_meta_hover_ended(_meta: String) -> void: + EventBus.emit_signal("rtl_meta_unhovered",selected_tis) + +func _on_ti_info_label_meta_clicked(meta) -> void: + OS.shell_open(meta) + +func update_selected_tis_label() -> void: + var bbtext := [] + var indexes_to_remove = [] + for index in range(selected_tis_list.size()): + var ti_text = "[url={ti_hover}]{ti_name}[/url]{strengths}{inject} ([url={ti_embed}]E[/url])([url={ti_trigger}]T[/url])([url={ti_remove}]X[/url])" + var ti_name = selected_tis_list[index]["name"] + # This might happen for example when we added a NSFW ti + # but then disabled NSFW which refreshed tis to only show SFW + if not ti_reference_node.is_ti(ti_name): + indexes_to_remove.append(index) + continue + var ti_reference = ti_reference_node.get_ti_info(ti_name) + if ti_reference["triggers"].size() == 0: + ti_text = "[url={ti_hover}]{ti_name}[/url]{strengths}{inject} ([url={ti_remove}]X[/url])" + var compatibility = check_baseline_compatibility(ti_name) + if ti_reference.get("unusable"): + ti_text = "[color=red]" + ti_text + "[/color]" + elif compatibility == TICompatible.NO: + ti_text = "[color=red]" + ti_text + "[/color]" + elif compatibility == TICompatible.MAYBE: + ti_text = "[color=yellow]" + ti_text + "[/color]" + elif not ti_reference_node.nsfw and ti_reference.get("nsfw", false): + ti_text = "[color=#FF00FF]" + ti_text + "[/color]" + + var strengths_string = '' + if selected_tis_list[index]["strength"] != 1: + strengths_string += ' S:'+str(selected_tis_list[index]["strength"]) + var inject_string = '' + if selected_tis_list[index].get("inject_ti"): + var inject_type = '' + if selected_tis_list[index].get("inject_ti") == 'prompt': + inject_type = 'p' + else: + inject_type = 'n' + inject_string += ' I:' + inject_type + var ti_fmt = { + "ti_name": ti_name.left(25), + "ti_hover": 'hover:' + str(index), + "ti_remove": 'delete:' + str(index), + "ti_trigger": 'trigger:' + str(index), + "ti_embed": 'embed:' + str(index), + "strengths": strengths_string, + "inject": inject_string, + } + bbtext.append(ti_text.format(ti_fmt)) + selected_tis.bbcode_text = ", ".join(bbtext) + indexes_to_remove.invert() + for index in indexes_to_remove: + selected_tis_list.remove(index) + if selected_tis_list.size() > 0: + selected_tis.show() + else: + selected_tis.hide() + +func _on_ti_trigger_pressed(index: int) -> void: + var ti_reference := ti_reference_node.get_ti_info(selected_tis_list[index]["name"]) + var selected_triggers: Array = [] + if ti_reference['triggers'].size() == 1: + selected_triggers = [ti_reference['triggers'][0]] + else: + ti_trigger_selection.clear() + for t in ti_reference['triggers']: + ti_trigger_selection.add_check_item(t) + ti_trigger_selection.add_item("Select") + ti_trigger_selection.popup() +# ti_trigger_selection.rect_global_position = ti_trigger.rect_global_position + if selected_triggers.size() > 0: + emit_signal("prompt_inject_requested", selected_triggers) + +func _on_ti_embed_pressed(index: int) -> void: + var ti_reference := ti_reference_node.get_ti_info(selected_tis_list[index]["name"]) + var inject_format = { + "ti_id": ti_reference['id'], + "ti_strength": selected_tis_list[index]["strength"], + } + var ti_id: String = "(embedding:{ti_id}:{ti_strength})".format(inject_format) + emit_signal("prompt_inject_requested", [ti_id]) + +func _on_trigger_selection_id_pressed(id: int) -> void: + if ti_trigger_selection.is_item_checkable(id): + ti_trigger_selection.toggle_item_checked(id) + else: + var selected_triggers:= [] + for iter in range (ti_trigger_selection.get_item_count()): + if ti_trigger_selection.is_item_checked(iter): + selected_triggers.append(ti_trigger_selection.get_item_text(iter)) + emit_signal("prompt_inject_requested", selected_triggers) + + +func _on_showcase0_retrieved(img:ImageTexture, _model_name) -> void: + ti_showcase0.texture = img + ti_showcase0.rect_min_size = Vector2(300,300) + +func _on_showcase1_retrieved(img:ImageTexture, _model_name) -> void: + ti_showcase1.texture = img + ti_showcase1.rect_min_size = Vector2(300,300) + +func clear_textures() -> void: + ti_showcase1.texture = null + ti_showcase0.texture = null + +func _on_ti_info_card_hide() -> void: + clear_textures() + update_selected_tis_label() + +func _on_show_all_tis_pressed() -> void: + ti_auto_complete.select_from_all() + +func _on_ti_model_strength_value_changed(value) -> void: + selected_tis_list[viewed_ti_index]["strength"] = value + emit_signal("tis_modified", selected_tis_list) + +func _on_ti_inject_value_changed(value) -> void: + if not value: + selected_tis_list[viewed_ti_index].erase("inject_ti") + else: + selected_tis_list[viewed_ti_index]["inject_ti"] = value + emit_signal("tis_modified", selected_tis_list) + +func _on_fetch_tis_from_civitai_pressed() -> void: + fetch_tis_from_civitai.disabled = true + civitai_search_initiated = true + ti_reference_node.seek_online(ti_auto_complete.text) + +func on_model_selection_changed(models_list) -> void: + current_models = models_list + update_selected_tis_label() + +func check_baseline_compatibility(ti_name) -> int: + var baselines = [] + for model in current_models: + if not model["baseline"] in baselines: + baselines.append(model["baseline"]) + if baselines.size() == 0: + return TICompatible.MAYBE + var ti_to_model_baseline_map = { + "SD 1.5": "stable diffusion 1", + "SD 2.1 768": "stable diffusion 2", + "SD 2.1 512": "stable diffusion 2", + "Other": null, + } + var ti_baseline = ti_to_model_baseline_map[ti_reference_node.get_ti_info(ti_name)["base_model"]] + if ti_baseline == null: + return TICompatible.NO + if ti_baseline in baselines: + if baselines.size() > 1: + return TICompatible.MAYBE + else: + return TICompatible.YES + return TICompatible.NO diff --git a/src/ParamBus.gd b/src/ParamBus.gd index 38ca9ae..a9cc989 100644 --- a/src/ParamBus.gd +++ b/src/ParamBus.gd @@ -44,6 +44,8 @@ signal shared_changed(boolean) signal control_type_changed(text) # warning-ignore:unused_signal signal loras_changed(list) +# warning-ignore:unused_signal +signal tis_changed(list) var api_key_node: LineEdit var prompt_node: TextEdit @@ -69,6 +71,7 @@ var source_image_node: TextureRect var shared_node: CheckButton var control_type_node: OptionButton var loras_node: LoraSelection +var tis_node: TISelection func setup( _api_key_node: LineEdit, @@ -94,7 +97,8 @@ func setup( _source_image_node: TextureRect, _shared_node: CheckButton, _control_type_node: OptionButton, - _loras_node: LoraSelection + _loras_node: LoraSelection, + _tis_node: TISelection ) -> void: api_key_node = _api_key_node prompt_node = _prompt_node @@ -120,6 +124,7 @@ func setup( shared_node = _shared_node control_type_node = _control_type_node loras_node = _loras_node + tis_node = _tis_node for le_node in [prompt_node, negprompt_node, gen_seed_node, api_key_node]: # warning-ignore:return_value_discarded le_node.connect("text_changed", self, "_on_line_edit_changed", [le_node]) @@ -150,6 +155,7 @@ func setup( models_node.connect("model_modified",self,"_on_listnode_changed", [models_node]) # warning-ignore:return_value_discarded loras_node.connect("loras_modified",self,"_on_listnode_changed", [loras_node]) + tis_node.connect("tis_modified",self,"_on_listnode_changed", [tis_node]) func get_prompt() -> String: @@ -225,7 +231,11 @@ func get_control_type() -> String: func get_loras() -> Array: return loras_node.selected_loras_list -func _on_line_edit_changed(_value, line_edit_node) -> void: +func get_tis() -> Array: + print_debug(tis_node.selected_tis_list) + return tis_node.selected_tis_list + +func _on_line_edit_changed(line_edit_node) -> void: match line_edit_node: prompt_node, negprompt_node: emit_signal("prompt_changed", get_prompt()) @@ -275,4 +285,6 @@ func _on_listnode_changed(_thing_list: Array, thing_node: Node) -> void: emit_signal("models_changed", get_models()) loras_node: emit_signal("loras_changed", get_loras()) + tis_node: + emit_signal("tis_changed", get_tis()) emit_signal("params_changed")