| """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, |
| ) |
|
|