"""LoRA UI construction and event wiring for the main Gradio surface. Keep this module deliberately concrete: it owns the standard LoRA controls and wires them to app-level callbacks, but it does not abstract Hub and Civitai into one transport or invent a generic UI framework. Search/discovery state remains non-authoritative; exact session definitions remain the generation boundary. """ from __future__ import annotations from dataclasses import dataclass from typing import Any, Callable import gradio as gr from .gradio_policy import INTERNAL_API_VISIBILITY @dataclass(frozen=True) class LoraUI: dropdown: Any strength: Any prepare_button: Any remove_selected_session_button: Any prepare_status: Any hub_quick_search: Any hub_search_status: Any hub_full_query: Any hub_full_author: Any hub_full_tags: Any hub_pipeline_filter: Any hub_pipeline_tags: Any hub_full_sort: Any hub_full_limit: Any hub_full_search_button: Any hub_full_cancel_button: Any hub_full_results: Any hub_full_status: Any hf_name: Any hf_repo: Any hf_revision: Any hf_inspect_button: Any hf_file: Any hf_add_button: Any hf_status: Any civitai_api_key: Any civitai_gallery_items: Any civitai_cursor_state: Any civitai_search_state: Any civitai_query: Any civitai_search_button: Any civitai_cancel_button: Any civitai_tag: Any civitai_username: Any civitai_base_model: Any civitai_sort: Any civitai_period: Any civitai_gallery: Any civitai_load_more: Any civitai_search_status: Any civitai_url: Any civitai_url_resolve: Any civitai_version_state: Any civitai_version: Any civitai_file: Any civitai_detail: Any civitai_name: Any civitai_apply_triggers: Any civitai_load_examples: Any civitai_example_prompt: Any civitai_apply_example: Any civitai_add: Any civitai_status: Any @dataclass(frozen=True) class LoraHandlers: apply_hub_quick_selection: Callable inspect_hub_repo: Callable search_hf_models: Callable apply_hf_full_selection: Callable search_civitai_models: Callable load_more_civitai_models: Callable inspect_civitai_gallery_selection: Callable resolve_civitai_url: Callable inspect_civitai_version_selection: Callable apply_civitai_trigger_words: Callable load_civitai_example_prompts: Callable apply_civitai_example_prompt: Callable add_civitai_session_lora: Callable add_hf_session_lora: Callable remove_selected_session_loras: Callable prepare_selected_loras: Callable invalidate_prepared_loras: Callable def build_lora_ui(*, lora_dropdown_factory: Callable[[], Any], default_strength: float, hub_backend, civitai_backend) -> LoraUI: """Render the LoRA accordion and return concrete component handles.""" with gr.Accordion("LoRA", open=False): gr.Markdown( "Optional built-in or session LoRAs. Discovery/search state is never generation authority; " "a selected source is copied into an exact session definition first. Hub/Civitai acquisition runs on CPU before GPU allocation." ) dropdown = lora_dropdown_factory() strength = gr.Slider( minimum=0.0, maximum=2.0, step=0.05, value=default_strength, interactive=True, label="LoRA strength", info="Applied equally to all selected adapters. LoRA weights are not fused into base weights.", ) with gr.Row(): prepare_button = gr.Button("Prepare selected LoRAs", variant="secondary", interactive=True, scale=3) remove_selected_session_button = gr.Button("Remove selected session LoRAs", variant="secondary", interactive=True, scale=2) prepare_status = gr.Markdown( "Selected HF/Civitai files are prepared and verified on CPU before generation; GPU quota is not used for source acquisition." ) with gr.Accordion("Add session LoRA", open=False): with gr.Tabs(): with gr.Tab("Hugging Face Hub"): gr.Markdown( "Quick search is the lightweight route. Full discovery uses bounded `HfApi.list_models()` metadata search. " "You can also paste an HF model page or `.safetensors` file URL; Inspect canonicalizes it to repo/revision/file identity." ) hub_quick_search = hub_backend.HuggingFaceHubSearch( label="Quick Hub search · public models", placeholder="Search model repositories...", result_limit=10, ) hub_search_status = gr.Markdown( "Search failure never disables manual repo entry. Private/gated repos may be entered directly and inspected with the Space token." ) with gr.Accordion("Full Hub discovery", open=False): with gr.Row(): hub_full_query = gr.Textbox(label="Query", value="LTX", placeholder="LTX / motion / style …") hub_full_author = gr.Textbox(label="Author · optional", placeholder="owner or organization") hub_full_tags = gr.Textbox( label="HF tags · optional", placeholder="comma-separated tags, e.g. lora, safetensors", ) with gr.Row(): hub_pipeline_filter = gr.Checkbox( value=True, label="Filter by pipeline tag", info="Advisory only. Turn this off when Hub metadata is wrong or missing.", ) hub_pipeline_tags = gr.CheckboxGroup( choices=list(hub_backend.HF_VIDEO_PIPELINE_TAGS), value=list(hub_backend.HF_VIDEO_PIPELINE_TAGS), label="Accepted video pipeline hints", ) with gr.Row(): hub_full_sort = gr.Dropdown( choices=list(hub_backend.HF_DISCOVERY_SORT_CHOICES), value="trending_score", label="Sort", ) hub_full_limit = gr.Slider(5, 60, value=30, step=5, label="Result limit") with gr.Row(): hub_full_search_button = gr.Button("Search Hub", variant="secondary", scale=4) hub_full_cancel_button = gr.Button("Cancel", variant="stop", scale=1) hub_full_results = gr.Dropdown( choices=[], value=None, label="Full discovery results", interactive=True ) hub_full_status = gr.Markdown() hf_name = gr.Textbox(label="Display name", placeholder="My LTX-2.5 LoRA") hf_repo = gr.Textbox(label="HF repo ID or URL", placeholder="owner/repo or https://huggingface.co/owner/repo/blob/main/file.safetensors") hf_revision = gr.Textbox(label="Revision · optional", placeholder="commit / tag / branch") hf_inspect_button = gr.Button("Inspect HF source", variant="secondary") hf_file = gr.Dropdown( label="Safetensors file", choices=[], value=None, allow_custom_value=True, interactive=True, info="Inspect the repo to list .safetensors files, or type a repository-relative path manually.", ) hf_add_button = gr.Button("Add HF LoRA", variant="secondary") hf_status = gr.Markdown() with gr.Tab("Civitai"): if civitai_backend.api_key_from_env(): gr.Markdown( "Type is fixed to **LORA**. Base Model defaults to **Auto · LTX 2.x family** and can be narrowed when Civitai exposes individual LTX values. " "Owner `CIVITAI_API_KEY` Secret is active automatically; the key is never shown or serialized." ) civitai_api_key = gr.State("") else: gr.Markdown( "Type is fixed to **LORA**. Base Model defaults to **Auto · LTX 2.x family** and can be narrowed when Civitai exposes individual LTX values. " "No owner `CIVITAI_API_KEY` Secret was found. Public discovery works anonymously; enter a key below when an authenticated download requires one." ) civitai_api_key = gr.Textbox( label="Civitai API Key · optional · session only", type="password", placeholder="Enter only if needed for authenticated Civitai access", info="Used only by backend Civitai metadata/download requests in this browser session. Not exported to settings, logs, diagnostics, or LoRA definitions.", ) civitai_gallery_items = gr.State([]) civitai_cursor_state = gr.State("") civitai_search_state = gr.State({}) with gr.Row(): civitai_query = gr.Textbox(label="Search", placeholder="Search Civitai LoRAs...", scale=4) civitai_search_button = gr.Button("Search Civitai", variant="secondary", scale=2) civitai_cancel_button = gr.Button("Cancel", variant="stop", scale=1) with gr.Row(): civitai_tag = gr.Textbox(label="Tag · optional", placeholder="tag") civitai_username = gr.Textbox(label="Username · optional", placeholder="creator") civitai_base_model = gr.Dropdown( choices=[("Auto · LTX 2.x family", "__auto_ltx__"), ("All base models", "__all__")], value="__auto_ltx__", label="Base Model", interactive=True, ) with gr.Row(): civitai_sort = gr.Dropdown( choices=list(civitai_backend.SORT_CHOICES), value=civitai_backend.DEFAULT_SORT, label="Sort", interactive=True, ) civitai_period = gr.Dropdown( choices=list(civitai_backend.PERIOD_CHOICES), value=civitai_backend.DEFAULT_PERIOD, label="Period", interactive=True, ) civitai_gallery = gr.Gallery( value=[], label="Civitai LoRA discovery", columns=4, height=620, object_fit="cover", format="webp", allow_preview=False, buttons=[], interactive=False, ) with gr.Row(): civitai_load_more = gr.Button("Load more", variant="secondary", scale=1) civitai_search_status = gr.Markdown( "Search results use standard Gradio Gallery. **Load more** appends the next Civitai cursor batch." ) with gr.Row(): civitai_url = gr.Textbox( label="Civitai URL · optional direct route", placeholder="https://civitai.com/models/...?...modelVersionId=...", scale=5, ) civitai_url_resolve = gr.Button("Resolve URL", variant="secondary", scale=1) civitai_version_state = gr.State({}) civitai_version = gr.Dropdown(choices=[], value=None, label="Model version", interactive=True) civitai_file = gr.Dropdown(choices=[], value=None, label="Exact .safetensors file", interactive=True) civitai_detail = gr.Markdown( "Select a Gallery result or resolve a Civitai URL. Version/file identity is verified before registration." ) civitai_name = gr.Textbox(label="Display name", placeholder="My Civitai LTX LoRA") with gr.Row(): civitai_apply_triggers = gr.Button("Apply trigger words", variant="secondary") civitai_load_examples = gr.Button("Load example prompts", variant="secondary") civitai_example_prompt = gr.Dropdown( choices=[], value=None, label="Example prompt", interactive=True ) civitai_apply_example = gr.Button("Apply example prompt", variant="secondary") civitai_add = gr.Button("Add Civitai LoRA", variant="secondary") civitai_status = gr.Markdown( "Trigger words and example prompts are applied only by explicit button presses; selecting a result never changes the prompt." ) return LoraUI( dropdown=dropdown, strength=strength, prepare_button=prepare_button, remove_selected_session_button=remove_selected_session_button, prepare_status=prepare_status, hub_quick_search=hub_quick_search, hub_search_status=hub_search_status, hub_full_query=hub_full_query, hub_full_author=hub_full_author, hub_full_tags=hub_full_tags, hub_pipeline_filter=hub_pipeline_filter, hub_pipeline_tags=hub_pipeline_tags, hub_full_sort=hub_full_sort, hub_full_limit=hub_full_limit, hub_full_search_button=hub_full_search_button, hub_full_cancel_button=hub_full_cancel_button, hub_full_results=hub_full_results, hub_full_status=hub_full_status, hf_name=hf_name, hf_repo=hf_repo, hf_revision=hf_revision, hf_inspect_button=hf_inspect_button, hf_file=hf_file, hf_add_button=hf_add_button, hf_status=hf_status, civitai_api_key=civitai_api_key, civitai_gallery_items=civitai_gallery_items, civitai_cursor_state=civitai_cursor_state, civitai_search_state=civitai_search_state, civitai_query=civitai_query, civitai_search_button=civitai_search_button, civitai_cancel_button=civitai_cancel_button, civitai_tag=civitai_tag, civitai_username=civitai_username, civitai_base_model=civitai_base_model, civitai_sort=civitai_sort, civitai_period=civitai_period, civitai_gallery=civitai_gallery, civitai_load_more=civitai_load_more, civitai_search_status=civitai_search_status, civitai_url=civitai_url, civitai_url_resolve=civitai_url_resolve, civitai_version_state=civitai_version_state, civitai_version=civitai_version, civitai_file=civitai_file, civitai_detail=civitai_detail, civitai_name=civitai_name, civitai_apply_triggers=civitai_apply_triggers, civitai_load_examples=civitai_load_examples, civitai_example_prompt=civitai_example_prompt, civitai_apply_example=civitai_apply_example, civitai_add=civitai_add, civitai_status=civitai_status, ) def wire_lora_events( ui: LoraUI, *, prompt, custom_lora_state, prepared_lora_state, handlers: LoraHandlers, ) -> None: """Wire LoRA discovery/registration events without owning backend semantics.""" ui.hub_quick_search.change( handlers.apply_hub_quick_selection, inputs=[ui.hub_quick_search, ui.hf_repo, ui.hf_file], outputs=[ui.hf_repo, ui.hf_file, ui.hub_search_status], queue=False, api_visibility=INTERNAL_API_VISIBILITY, ) ui.hf_inspect_button.click( handlers.inspect_hub_repo, inputs=[ui.hf_repo, ui.hf_revision, ui.hf_file], outputs=[ui.hf_repo, ui.hf_revision, ui.hf_file, ui.hub_search_status], queue=False, api_visibility=INTERNAL_API_VISIBILITY, ) hub_full_search_event = ui.hub_full_search_button.click( handlers.search_hf_models, inputs=[ ui.hub_full_query, ui.hub_full_author, ui.hub_full_tags, ui.hub_full_sort, ui.hub_pipeline_filter, ui.hub_pipeline_tags, ui.hub_full_limit, ], outputs=[ui.hub_full_results, ui.hub_full_status], queue=True, concurrency_limit=1, api_visibility=INTERNAL_API_VISIBILITY, ) ui.hub_full_cancel_button.click( lambda: "Hub search cancelled. Existing discovery results and session LoRAs were not changed.", inputs=None, outputs=[ui.hub_full_status], cancels=[hub_full_search_event], queue=False, api_visibility=INTERNAL_API_VISIBILITY, ) ui.hub_full_results.input( handlers.apply_hf_full_selection, inputs=[ui.hub_full_results, ui.hf_repo, ui.hf_file], outputs=[ui.hf_repo, ui.hf_file, ui.hub_search_status], queue=False, api_visibility=INTERNAL_API_VISIBILITY, ) civitai_search_inputs = [ ui.civitai_query, ui.civitai_tag, ui.civitai_username, ui.civitai_sort, ui.civitai_period, ui.civitai_base_model, ui.civitai_api_key, ] civitai_search_outputs = [ ui.civitai_gallery, ui.civitai_gallery_items, ui.civitai_cursor_state, ui.civitai_search_state, ui.civitai_base_model, ui.civitai_search_status, ] civitai_search_event = ui.civitai_search_button.click( handlers.search_civitai_models, inputs=civitai_search_inputs, outputs=civitai_search_outputs, queue=True, concurrency_limit=1, concurrency_id="civitai-discovery", api_visibility=INTERNAL_API_VISIBILITY, ) civitai_query_submit_event = ui.civitai_query.submit( handlers.search_civitai_models, inputs=civitai_search_inputs, outputs=civitai_search_outputs, queue=True, concurrency_limit=1, concurrency_id="civitai-discovery", api_visibility=INTERNAL_API_VISIBILITY, ) civitai_load_more_event = ui.civitai_load_more.click( handlers.load_more_civitai_models, inputs=[ui.civitai_gallery_items, ui.civitai_cursor_state, ui.civitai_search_state, ui.civitai_api_key], outputs=[ui.civitai_gallery, ui.civitai_gallery_items, ui.civitai_cursor_state, ui.civitai_search_status], queue=True, concurrency_limit=1, concurrency_id="civitai-discovery", api_visibility=INTERNAL_API_VISIBILITY, ) ui.civitai_cancel_button.click( lambda: "Civitai search cancelled. Existing Gallery results and session LoRAs were not changed.", inputs=None, outputs=[ui.civitai_search_status], cancels=[civitai_search_event, civitai_query_submit_event, civitai_load_more_event], queue=False, api_visibility=INTERNAL_API_VISIBILITY, ) ui.civitai_gallery.select( handlers.inspect_civitai_gallery_selection, inputs=[ui.civitai_gallery_items, ui.civitai_name, ui.civitai_api_key], outputs=[ui.civitai_version, ui.civitai_file, ui.civitai_version_state, ui.civitai_name, ui.civitai_detail], queue=False, api_visibility=INTERNAL_API_VISIBILITY, ) ui.civitai_url_resolve.click( handlers.resolve_civitai_url, inputs=[ui.civitai_url, ui.civitai_name, ui.civitai_api_key], outputs=[ui.civitai_version, ui.civitai_file, ui.civitai_version_state, ui.civitai_name, ui.civitai_detail], queue=False, api_visibility=INTERNAL_API_VISIBILITY, ) ui.civitai_version.input( handlers.inspect_civitai_version_selection, inputs=[ui.civitai_version, ui.civitai_name, ui.civitai_api_key], outputs=[ui.civitai_version, ui.civitai_file, ui.civitai_version_state, ui.civitai_name, ui.civitai_detail], queue=False, api_visibility=INTERNAL_API_VISIBILITY, ) ui.civitai_apply_triggers.click( handlers.apply_civitai_trigger_words, inputs=[prompt, ui.civitai_version_state], outputs=[prompt, ui.civitai_status], queue=False, api_visibility=INTERNAL_API_VISIBILITY, ) ui.civitai_load_examples.click( handlers.load_civitai_example_prompts, inputs=[ui.civitai_version_state, ui.civitai_api_key], outputs=[ui.civitai_example_prompt, ui.civitai_status], queue=False, api_visibility=INTERNAL_API_VISIBILITY, ) ui.civitai_apply_example.click( handlers.apply_civitai_example_prompt, inputs=[prompt, ui.civitai_example_prompt], outputs=[prompt, ui.civitai_status], queue=False, api_visibility=INTERNAL_API_VISIBILITY, ) civitai_add_event = ui.civitai_add.click( handlers.add_civitai_session_lora, inputs=[custom_lora_state, ui.civitai_name, ui.civitai_version_state, ui.civitai_file, ui.dropdown], outputs=[custom_lora_state, ui.dropdown, ui.civitai_status], queue=False, api_visibility=INTERNAL_API_VISIBILITY, ) civitai_add_event.success( handlers.invalidate_prepared_loras, inputs=[], outputs=[prepared_lora_state, ui.prepare_status], queue=False, api_visibility=INTERNAL_API_VISIBILITY, ) hf_add_event = ui.hf_add_button.click( handlers.add_hf_session_lora, inputs=[custom_lora_state, ui.hf_name, ui.hf_repo, ui.hf_file, ui.hf_revision, ui.dropdown], outputs=[custom_lora_state, ui.dropdown, ui.hf_status], queue=False, api_visibility=INTERNAL_API_VISIBILITY, ) hf_add_event.success( handlers.invalidate_prepared_loras, inputs=[], outputs=[prepared_lora_state, ui.prepare_status], queue=False, api_visibility=INTERNAL_API_VISIBILITY, ) ui.remove_selected_session_button.click( handlers.remove_selected_session_loras, inputs=[custom_lora_state, ui.dropdown], outputs=[custom_lora_state, ui.dropdown, prepared_lora_state, ui.prepare_status], queue=False, api_visibility=INTERNAL_API_VISIBILITY, ) ui.prepare_button.click( handlers.prepare_selected_loras, inputs=[ui.dropdown, custom_lora_state, ui.civitai_api_key], outputs=[prepared_lora_state, ui.prepare_status], api_visibility=INTERNAL_API_VISIBILITY, ) ui.dropdown.change( handlers.invalidate_prepared_loras, inputs=[], outputs=[prepared_lora_state, ui.prepare_status], queue=False, api_visibility=INTERNAL_API_VISIBILITY, )