Move the HF model list from ~/Documents/ into tests/enrich_hf_validation/test_data/
and commit the pipeline validation baseline artifacts (report.json,
preprocessing_audit.json, README snapshots) into baselines/.
Update config.py and run_validation.py defaults to use repo-relative paths
via os.path.dirname(__file__) instead of ~/Documents/ hardcode.
Originates from changes in 8fb00998 (validation pipeline audit).
- Rename md_to_html.py → readme_processor.py (file no longer just HTML conversion)
- _extract_section: include YAML frontmatter, use heading-level-aware forward
walk (sub-headings under # are included), increase walk limit past 30 lines
- _is_heading: exclude </hN> closing tags from boundary detection
- _heading_level: new helper for heading-level-aware section matching
- css: yield 0 for heading like closing tags, was unexpectedly caught by _is_heading
- extract_gallery_images: fix YAML block scalar (text: >-) prompt extraction;
use endswith instead of == to detect the block marker
- _strip_widget_section: add to clean_readme_for_llm (widget text is handled
by post-processor, not needed in LLM prompt)
- _strip_standalone_images: keep markdown image URLs intact for LLM preview
extraction (was stripping to alt text only)
- list_base_models: switch from scanner-cache aggregation to
CivitaiBaseModelService.get_base_models() - always returns full list
- Ollama: add num_ctx=32768 to payload options so thinking models have room
to both reason and produce output
- Add tests/agent_cli/test_readme_processor.py: 59 tests covering extraction,
cleaning, section matching, heading detection
- Update existing tests for behavioral changes
- Add clean_readme_for_llm() to strip noise from README before LLM injection
- Keep widget section text (valuable tag signal) and unmarked code blocks (trigger words)
- Preserve standalone image alt text instead of removing entirely
- Switch Ollama to native /api/chat with think:false to fix empty content on thinking models
- Extract Sample Gallery table images and deduplicate with widget images
- Only strip code blocks with explicit language tags (bash)
- Add notes and usage_tips fields to SKILL.md output format and post-processor
- Clean up dead code, fix regex edge cases, remove double type annotation
- Replace hardcoded provider list with PROVIDER_PRESETS (OpenAI, Ollama,
DeepSeek, Groq, OpenRouter, OpenCode Go, Custom)
- Load model lists from models.dev/api.json catalog at startup
- Add Combobox vanilla JS component for model/base-URL selection
- Fetch local Ollama models via live API instead of catalog
- Hide API key values from frontend (boolean-only llm_api_key_set)
- Add i18n translations for all 9+ locales
- Update snapshot tests for new response fields
Widget entries with unquoted multi-line YAML scalars (e.g. "text: two samurais...\n continuation") were not parsed, leaving gallery image prompts empty. Add a third branch for plain scalar format alongside the existing quoted and >- folded block handlers.
- Add extract_gallery_images() to parse YAML widget entries from README
frontmatter, convert relative image URLs to absolute HF URLs, and
build civitai.images-compatible entries with prompt metadata
- LLM now extracts recommended_width/recommended_height from README
(e.g. "Best Dimensions"), used as gallery image dimensions
- extract_gallery_images() accepts default_width/height parameters,
falling back to 512x512 when LLM provides no recommendation
- Frontend ShowcaseView.js: defensive NaN guard for 0 width/height
- post_processor: consistently merge civitai updates across triggers,
description, and gallery blocks with distinct variable names
- SKILL.md: add recommended_width/recommended_height to output schema
- 62 tests pass, including gallery extraction and dimension tests
- Add identify_model_type() helper to determine lora/checkpoint/embedding
- Pass priority_tags from user settings to LLM prompt for tag relevance
- SKILL.md: instruct LLM to exclude technical/generic HF tags, cross-reference
against priority_tags; forbid ['None'] placeholder for trigger words
- post_processor: fix preview_url not updated after download (now writes local
.webp path to metadata); write trigger words to civitai.trainedWords instead
of top-level; sanitize ['None']/'null'/'n/a' placeholder values to []
- download_preview() now returns str | None (local path) instead of bool
- Update tests for new return type and nested civitai.trainedWords structure
Merge skill.yaml (metadata) and prompt.md (prompt template) into a
single SKILL.md file with YAML frontmatter, matching the agent-skill
convention used by opencode and Claude Code.
- Add frontmatter parser (_parse_skill_file) to SkillRegistry
- Remove skill.yaml, prompt.md, empty skills/__init__.py
- Remove obsolete load_handler method
- Update tests for new format and cleaned-up fields
Introduce an agent skill framework for LLM-driven metadata enrichment:
- AgentCLI (py/agent_cli/): in-process wrappers around internal services
using standard relative imports, eliminating the need for sys.path hacks
- LLMService: centralized BYOK (bring-your-own-key) LLM client supporting
OpenAI, Ollama, and custom OpenAI-compatible endpoints
- PostProcessor: deterministic engine that applies LLM output via AgentCLI
(replaces old handler.py + _BASE_MODEL_ALIASES approach)
- SkillRegistry: filesystem-based skill discovery (skill.yaml + prompt.md)
- AgentService: orchestrates skill execution with WebSocket progress
- Frontend AgentManager: WebSocket listeners, skill execution, config UI
- Context menu entries (single + bulk) for "Enrich Metadata (Agent)"
- Settings UI for AI Provider configuration (BYOK)
- Full i18n support across 9 locales
Bug fixes found during review:
- aiohttp.web.json_response: status_code= -> status=
- settings_modal cancelEditApiKey: wrong argument position
- AgentManager.isLlmConfigured: allow Ollama without API key
- PostProcessor._merge_tags: lowercase all tags to match TagUpdateService
Security hardening:
- Validate repo format with strict regex (reject .. traversal)
- Validate filename rejects path separators and ..
- Validate relative_path rejects absolute paths and ..
- Verify model_root is within configured scanner roots using
realpath + os.sep guard to prevent prefix-match bypass
- Add realpath-based escape detection for final dest_path
Bug fixes:
- Fix WebSocket leak in _downloadHfSingle: wrap ws.close() in
try/finally so it closes even if downloadHfModel() throws
- Same fix for batch HF download per-file WebSocket loop
Frontend hardening:
- Tighten HF repo regex: require huggingface.co for full URLs,
reject bare .. patterns
- Add 12 unit tests for detectUrlType() covering HF resolve,
HF repo, CivitAI, CivArchive, direct HTTP, edge cases
Cache corruption (NULL model_name/file_name from legacy DB rows or partial
writes) caused format_response to raise KeyError/AttributeError, failing the
entire /loras/list request and showing no models in the UI.
Fix across three layers:
- format_response (lora/checkpoint/embedding): replace direct dict[] access
with .get() fallbacks; return None for entries missing file_path
- handlers: filter None entries from list/excluded/fetch/duplicate/conflict
endpoints instead of letting them crash or appear as null in responses
- model_scanner: always use validate_batch repaired copies (previously
discarded when no invalid entries, leaving None values in raw_data)
- persistent_model_cache: add or-empty-string guards on read and write for
nullable TEXT columns (model_name, file_name, folder, base_model, etc.)
git clean -fd in _perform_git_update deleted untracked, non-ignored
directories (wildcards, stats, backups, civitai, caches, logs) during
portable-mode updates, since released tags do not list them in .gitignore.
Add -e excludes for all user-managed paths to both nightly and stable
update branches. Add regression tests for both paths.
Move NodeRegistry from a single global _nodes dict to a per-client
(_tab_nodes) structure so that multiple ComfyUI browser tabs no
longer overwrite each other's workflow node data during a
lora_registry_refresh cycle. The merged result is a union of all
known tabs' target nodes, eliminating the non-deterministic failure
where send-to-workflow could randomly target a tab lacking valid
targets.
- NodeRegistry.register_nodes(sid, nodes) replaces per-tab data
without affecting other tabs.
- NodeRegistry.get_merged_registry() returns the union across all
connected clients, together with tab_count / per-tab metadata.
- prepare_for_refresh() snapshots the current active sockets; caller
re-reads before merging so that newly-connected tabs are not pruned.
- workflow_registry.js sends api.clientId in the POST body so the
backend can identify which tab is registering.
- Add &withMeta=true to image info URL so API returns full generation
metadata (resources with hash/type) instead of null meta
- Fix checkpoint assignment guard: check modelId instead of id so non-
checkpoint types (upscaler) are not wrongly set as recipe checkpoint
- Skip modelVersionIds loop when resources/civitaiResources already
provided LoRAs, preventing hash-resolved duplicates
- Fix int/str type comparison in CivArchive get_model_version so
version ID matching works correctly
When refreshing updates with a folder filter, versions already present in
other folders were excluded from the is_in_library check, making them
appear as available updates. When the user tried to download, the global
check found the file already exists and returned 'model already exists'.
Fix by also collecting the cross-folder version set when folder_path is
provided, and using the union (folder-filtered + cross-folder) for
is_in_library in both _build_record_from_remote and
_merge_with_local_versions.
aria2 default --fsync=true calls fsync() after each write, which blocks
the entire single-threaded process on large files under Docker overlay.
Add --fsync=false to eliminate this blocking source.
Relax aiohttp session timeout: total=30 → sock_connect=10, sock_read=60
so that transient I/O delays don't cut off legitimate tellStatus RPCs.
Increase retry params (4 attempts, 3s delay) to give aria2 more recovery
time when blocked on synchronous I/O.
Root cause: aria2c subprocess stderr pipe (64 KB buffer) was never
drained. When enough error/warning output accumulated, aria2's write()
blocked, freezing the entire process including its RPC handler. The
tellStatus call then timed out after 30s with asyncio.TimeoutError(),
producing the empty error message in 'Failed to query aria2 download
status: '.
Fixes:
- Drain stderr in a background task so pipe never fills up
- Retry get_status() RPC calls up to 3 times on transient failure
- In the failure path, preserve .safetensors when .aria2 is absent
(the download was likely complete on disk)
Sort by Most/Fewest versions first now works when Group by model is off.
- Backend: group items by modelId (respecting version_grouping setting),
count versions per group, sort groups by count, expand groups with
versions sorted by version id descending
- CSS: remove rule that hid the sort option in non-grouped mode
- Tests: add 3 tests covering desc, asc, and same_base variants
- group_by_model dedup now counts versions per group and attaches
version_count; respects update_flag_strategy (same_base) by
sub-grouping on base_model
- Card footer shows clickable 'x versions' link instead of version
name when grouped (hides HIGH/LOW badges); clicking triggers
View Local Versions without page reload
- Added 'Local Versions' sort option (versions_count), auto-hidden
when group_by_model is off
- Sort preference is saved/restored separately for normal and
grouped modes
- VLM flow (triggerVlmView, clearCustomFilter) uses resetAndReload()
via API instead of window.location.reload()
- Fixed cache mutation bug: version_count is now set on a shallow
copy, not the cached dict, preventing stale version_count leaking
into VLM responses
- i18n: all 9 locale files translated
Adds a 'Group by Model' toggle in Layout Settings. When enabled, only the
latest version (highest civitai.id) of each Civitai model is shown as a
single card — older versions sharing the same modelId are hidden.
Backend dedup runs in BaseModelService.get_paginated_data() before
filtering/pagination, ensuring correct paginated results. The setting
is persisted via the existing settings pipeline and passed as a query
parameter to the listing endpoint.
Includes:
- Backend: dedup logic, route param parsing, settings default
- Frontend: API param, SettingsManager wiring, toggle UI
- i18n: translations for all 10 locales
- Tests: unit test covering dedup on/off and standalone items
- Inject #customFilterIndicator DOM in beforeEach (raw template
renderer doesn't process Jinja2 {% include %} tags)
- Fix selector from #customFilterText to .customFilterText
- Replace recipe modal's custom tag display/edit with shared
renderCompactTags/setupTagEditMode from ModelTags and utils
- Remove 300+ lines of duplicated tag display and editing code
- Parameterize setupTagEditMode with saveHandler/onSaved/showSuggestions
options for recipe-specific save flow (updateRecipeMetadata + dirty state)
- Scope all DOM queries in ModelTags.js via options.container / this.closest
to prevent cross-modal element conflicts
- Fix edit button alignment (justify-content: flex-start)
- Fix tag tooltip selector scoping in setupTagTooltip
- Add width: 100% to #recipeTagsContainer for edit container full width
Backend changes:
- Add civitai_api_key to _NO_SYNC_KEYS, return only boolean civitai_api_key_set
- Clean up known template placeholder on load to prevent false positive
Frontend changes:
- Replace type=password with type=text + CSS masking (-webkit-text-security)
- Replace pre-filled input with status display (Configured/Not configured)
- Add inline edit view with Save/Cancel buttons
- Re-add eye toggle via CSS class toggle (not type switching)
- Use CSS transitions for smooth status/edit view switching
This prevents Chromium/Vivaldi password manager from triggering
'save password' prompts when opening the settings modal.
- Add 5 new Tabler SVG icons (currency-dollar, brush, user, git-merge, license)
- Implement Set 2 rendering in ModelModal.js (standalone UI) with green/red
permission indicators and preview_tooltip.js (ComfyUI widget)
- Add use_new_license_icons setting (default: true) with toggle in settings UI
- ComfyUI tooltip reads setting directly from preview-url API response to
eliminate race conditions and respect standalone settings changes
- Remove the now-unused separate ComfyUI setting loramanager.license_icon_style
- Add CSS for both standalone (lora-modal.css) and widget (lm_styles.css)
- i18n: translate licenseIcons keys into all 10 supported languages
- Fix test to use classic style explicitly for continued coverage
- Remove pin/unpin and auto-hide hover mechanism (isPinned, isHovering,
hoverTimeout, showSidebar/hideSidebar, updateAutoHideState, etc.)
- Remove global show_folder_sidebar setting (SettingsManager,
PageControls, recipes, backend default)
- Simplify sidebar visibility to a single per-page toggle:
· Dedicated chevron-left button in header to hide sidebar
· Edge indicator (chevron-right) to restore when hidden
· No dropdown, no hover area, no pin button
- Add _migrateOldSettings() to convert old sidebarPinned and
show_folder_sidebar states to per-page sidebarDisabled
- Fix sidebar flicker on page load: CSS defaults to off-screen,
JS explicitly sets .visible or .hidden-by-setting
- Remove obsolete CSS classes: auto-hide, hover-active, collapsed
- Remove i18n keys: pinSidebar, unpinSidebar, moreOptions
- Update test mocks for the new initialize() interface
When a model is already classified as civitai_deleted=True via
.metadata.json but re-enters the failure block through the
civarchive/sqlite provider path (not the default provider),
needs_save was never set to True because civitai_api_not_found
and sqlite_attempted were both False. The flags were never
persisted to SQLite, causing the model to be re-fetched on
every restart.
Also demoted duplicate INFO/ERROR logging in fetch_and_update_model
to DEBUG (the use case already logs at WARNING), and added
exc_info=True to the fetch_all_civitai error handler.
When CivArchive returns HTTP 429 with a large retry_after, the bulk
metadata refresh would block for hours because:
1. FallbackMetadataProvider raised RateLimitError instead of continuing
to the next provider (e.g., SQLite archive was never reached).
2. _RateLimitRetryHelper retried long-rate-limit 429s 3 times — all
futile since the hourly cap hasn't reset.
3. The batch loop had no awareness of persistent rate-limiting,
causing 192+ models to each hammer the same rate-limited endpoint.
Changes:
- FallbackMetadataProvider: all 6 methods now continue to next provider
on RateLimitError instead of raising (model_metadata_provider.py)
- fetch_and_update_model: deleted-model path also continues on
RateLimitError so sqlite provider gets a chance (metadata_sync_service.py)
- _RateLimitRetryHelper: when retry_after >= 120s, only 1 attempt is
made — retries are futile for hour-scale rate limits
- BulkMetadataRefreshUseCase: tracks consecutive rate-limit failures
and aborts early after 3 (bulk_metadata_refresh_use_case.py)
Tests: updated test_fallback_respects_retry_limit for new continue
behavior; added tests for large/small retry_after thresholds.
Build a civitai_image_id → recipe_id mapping once during cache
initialization instead of scanning all recipes on every
check_image_exists and import_from_url call.
- RecipeCache gains an image_id_map field populated by
_build_image_id_map() during cache init
- check_image_exists and import_from_url duplicate detection
now use the precomputed map (O(k) / O(1) vs O(n))
- Map is persisted in SQLite cache_metadata for fast startup
- Incrementally updated on add/remove/bulk_remove paths
- Fix: conn.close() before cache_metadata query (dead connection)
- Change _get_stats_file_path() to use get_settings_dir()/stats/ instead of
first loras root directory
- Add _migrate_from_old_location() to copy existing stats from loras root
to new location on first access, then clean up old file
- Add 'stats' to update protection skip lists (clean, extract, tracking)
to prevent data loss during ZIP/git upgrades in portable mode
- Add usage_stats entry to backup targets and restore resolver so stats
are included in automatic snapshots
Bug: when scrolling down on recipes page, any operation with
preserveScroll: true would fetch only page 1 data then restore
scroll position to beyond the loaded items, leaving the grid empty.
Fix:
- Remove preserveScroll: true from all 7 must-refresh trigger
paths (filter, search, sort, import, settings reload, sync,
rebuild cache, sidebar folder nav)
- Replace full list refresh with updateSingleItem() for repair
and bulk missing-LoRA download operations
- Update tests to match new scroll-free behavior