diff --git a/py/routes/handlers/recipe_handlers.py b/py/routes/handlers/recipe_handlers.py index e9402b97..ac48a325 100644 --- a/py/routes/handlers/recipe_handlers.py +++ b/py/routes/handlers/recipe_handlers.py @@ -907,6 +907,7 @@ class RecipeManagementHandler: extension, civitai_meta_raw, model_version_id, + _original_image_url, ) = await self._download_remote_media(image_url) # Extract embedded EXIF metadata (offloaded to thread pool in this call) @@ -1319,7 +1320,9 @@ class RecipeManagementHandler: "exclude": False, } - async def _download_remote_media(self, image_url: str) -> tuple[bytes, str, Any, Any]: + async def _download_remote_media( + self, image_url: str + ) -> tuple[bytes, str, Any, Any, Optional[str]]: civitai_client = self._civitai_client_getter() downloader = await self._downloader_factory() temp_path = None @@ -1394,11 +1397,16 @@ class RecipeManagementHandler: if mvids and isinstance(civitai_meta_raw, dict): civitai_meta_raw["modelVersionIds"] = mvids + original_url = ( + image_info.get("url") if civitai_image_id and image_info else None + ) + return ( file_obj.read(), extension, civitai_meta_raw, model_ver_id, + original_url, ) except RecipeDownloadError: raise @@ -1550,7 +1558,7 @@ class RecipeManagementHandler: "Could not extract Civitai image ID from URL" ) - image_bytes, extension, civitai_meta_raw, model_version_id = ( + image_bytes, extension, civitai_meta_raw, model_version_id, original_image_url = ( await self._download_remote_media(image_url) ) @@ -1588,6 +1596,51 @@ class RecipeManagementHandler: "Failed to extract embedded metadata: %s", exc ) + if not parsed_embedded and original_image_url: + self._logger.debug( + "Optimized image has no embedded metadata, " + "falling back to original: %s", + original_image_url, + ) + try: + downloader = await self._downloader_factory() + with tempfile.NamedTemporaryFile( + suffix=".png", delete=False + ) as tmp: + orig_tmp_path = tmp.name + try: + success, _ = await downloader.download_file( + original_image_url, orig_tmp_path, use_auth=False + ) + if success: + raw_orig = await asyncio.to_thread( + ExifUtils.extract_image_metadata, orig_tmp_path + ) + if raw_orig: + parser = ( + self._analysis_service._recipe_parser_factory.create_parser( + raw_orig + ) + ) + if parser: + parsed_embedded = await parser.parse_metadata( + raw_orig, recipe_scanner=recipe_scanner + ) + if ( + parsed_embedded + and "gen_params" in parsed_embedded + ): + embedded_gen_params = parsed_embedded[ + "gen_params" + ] + finally: + if os.path.exists(orig_tmp_path): + os.unlink(orig_tmp_path) + except Exception as exc: + self._logger.warning( + "Failed to extract metadata from original image: %s", exc + ) + # Parse CivitAI API meta to discover all resources from modelVersionIds. # Run unconditionally — EXIF parsing succeeds for gen_params but misses # LoRAs (modelVersionIds is NOT in the image EXIF). diff --git a/py/services/recipes/analysis_service.py b/py/services/recipes/analysis_service.py index 04bca09c..5e503424 100644 --- a/py/services/recipes/analysis_service.py +++ b/py/services/recipes/analysis_service.py @@ -176,6 +176,24 @@ class RecipeAnalysisService: self._exif_utils.extract_image_metadata, temp_path ) + if not metadata and civitai_image_id and image_info: + original_url = image_info.get("url") + if original_url: + self._logger.debug( + "Optimized image lacks embedded metadata, " + "falling back to original image: %s", + original_url, + ) + orig_temp_path = self._create_temp_path(suffix=".png") + try: + await self._download_image(original_url, orig_temp_path) + metadata = await asyncio.to_thread( + self._exif_utils.extract_image_metadata, + orig_temp_path, + ) + finally: + self._safe_cleanup(orig_temp_path) + result = await self._parse_metadata( metadata or {}, recipe_scanner=recipe_scanner, diff --git a/templates/components/modals/settings_modal.html b/templates/components/modals/settings_modal.html index f6bbf73b..1dd221de 100644 --- a/templates/components/modals/settings_modal.html +++ b/templates/components/modals/settings_modal.html @@ -104,6 +104,7 @@ id="civitaiApiKey" placeholder="{{ t('settings.civitaiApiKeyPlaceholder') }}" value="{{ settings.get('civitai_api_key', '') }}" + autocomplete="new-password" onblur="settingsManager.saveInputSetting('civitaiApiKey', 'civitai_api_key')" onkeydown="if(event.key === 'Enter') { this.blur(); }" />