diff --git a/py/services/recipe_scanner.py b/py/services/recipe_scanner.py index f1fdfd2b..2d11a455 100644 --- a/py/services/recipe_scanner.py +++ b/py/services/recipe_scanner.py @@ -637,7 +637,9 @@ class RecipeScanner: try: if hash_value: lora['inLibrary'] = self._lora_scanner.has_hash(hash_value) - lora['preview_url'] = self._lora_scanner.get_preview_url_by_hash(hash_value) + lora['preview_url'] = self._normalize_preview_url( + self._lora_scanner.get_preview_url_by_hash(hash_value) + ) lora['localPath'] = self._lora_scanner.get_path_by_hash(hash_value) elif version_entry: lora['inLibrary'] = True @@ -650,16 +652,34 @@ class RecipeScanner: if version_entry.get('sha256') and not lora.get('hash'): lora['hash'] = version_entry.get('sha256') - preview_url = version_entry.get('preview_url') + preview_url = self._normalize_preview_url(version_entry.get('preview_url')) if preview_url: lora.setdefault('preview_url', preview_url) else: lora.setdefault('inLibrary', False) + + if lora.get('preview_url'): + lora['preview_url'] = self._normalize_preview_url(lora['preview_url']) except Exception as exc: # pragma: no cover - defensive logging logger.debug("Error enriching lora entry %s: %s", hash_value, exc) return lora + def _normalize_preview_url(self, preview_url: Optional[str]) -> Optional[str]: + """Return a preview URL that is reachable from the browser.""" + + if not preview_url or not isinstance(preview_url, str): + return preview_url + + normalized = preview_url.strip() + if normalized.startswith("/api/lm/previews?path="): + return normalized + + if os.path.isabs(normalized): + return config.get_preview_static_url(normalized) + + return normalized + async def get_local_lora(self, name: str) -> Optional[Dict[str, Any]]: """Lookup a local LoRA model by name.""" diff --git a/tests/services/test_recipe_scanner.py b/tests/services/test_recipe_scanner.py index d5da17c7..45d0ce33 100644 --- a/tests/services/test_recipe_scanner.py +++ b/tests/services/test_recipe_scanner.py @@ -247,3 +247,29 @@ def test_enrich_uses_version_index_when_hash_missing(recipe_scanner): assert enriched["localPath"] == file_path assert enriched["file_name"] == Path(file_path).stem assert enriched["preview_url"] == registered["preview_url"] + + +def test_enrich_formats_absolute_preview_paths(recipe_scanner, tmp_path): + scanner, stub = recipe_scanner + version_id = 88 + preview_path = tmp_path / "loras" / "version-entry.preview.jpeg" + preview_path.parent.mkdir(parents=True, exist_ok=True) + preview_path.write_text("preview") + model_path = tmp_path / "loras" / "version-entry.safetensors" + model_path.write_text("weights") + + stub.register_model( + "absolute-preview", + { + "sha256": "feedface", + "file_path": str(model_path), + "preview_url": str(preview_path), + "civitai": {"id": version_id}, + }, + ) + + lora = {"hash": "", "file_name": "", "modelVersionId": version_id, "strength": 0.5} + + enriched = scanner._enrich_lora_entry(dict(lora)) + + assert enriched["preview_url"] == config.get_preview_static_url(str(preview_path))