Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions invokeai/app/services/model_records/model_records_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
from abc import ABC, abstractmethod
from enum import Enum
from pathlib import Path
from typing import List, Optional, Set, Union
from typing import Any, List, Optional, Set, Union

from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, field_validator

from invokeai.app.services.shared.pagination import PaginatedResults
from invokeai.app.util.model_exclude_null import BaseModelExcludeNull
Expand Down Expand Up @@ -77,6 +77,19 @@ class ModelRecordChanges(BaseModelExcludeNull):
source: Optional[str] = Field(description="original source of the model", default=None)
source_type: Optional[ModelSourceType] = Field(description="type of model source", default=None)
source_api_response: Optional[str] = Field(description="metadata from remote source", default=None)
source_url: Optional[str] = Field(description="Optional URL for the model (e.g. download page)", default=None)

@field_validator("source_url", mode="before")
@classmethod
def validate_source_url(cls, v: Any) -> Optional[str]:
if v is None or v == "":
return None
if not isinstance(v, str):
raise ValueError("source_url must be a string")
if not v.startswith(("https://", "http://")):
raise ValueError("source_url must be an http or https URL")
return v

name: Optional[str] = Field(description="Name of the model.", default=None)
path: Optional[str] = Field(description="Path to the model.", default=None)
description: Optional[str] = Field(description="Model description", default=None)
Expand Down
90 changes: 54 additions & 36 deletions invokeai/backend/image_util/controlnet_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,43 +14,61 @@ def _get_processor_invocation_class(processor_type: str):
"""Get the invocation class for a processor type."""
# Import processor invocation classes on demand
processor_class_map = {
"canny_image_processor": lambda: __import__(
"invokeai.app.invocations.canny", fromlist=["CannyEdgeDetectionInvocation"]
).CannyEdgeDetectionInvocation,
"hed_image_processor": lambda: __import__(
"invokeai.app.invocations.hed", fromlist=["HEDEdgeDetectionInvocation"]
).HEDEdgeDetectionInvocation,
"mlsd_image_processor": lambda: __import__(
"invokeai.app.invocations.mlsd", fromlist=["MLSDDetectionInvocation"]
).MLSDDetectionInvocation,
"depth_anything_image_processor": lambda: __import__(
"invokeai.app.invocations.depth_anything", fromlist=["DepthAnythingDepthEstimationInvocation"]
).DepthAnythingDepthEstimationInvocation,
"normalbae_image_processor": lambda: __import__(
"invokeai.app.invocations.normal_bae", fromlist=["NormalMapInvocation"]
).NormalMapInvocation,
"pidi_image_processor": lambda: __import__(
"invokeai.app.invocations.pidi", fromlist=["PiDiNetEdgeDetectionInvocation"]
).PiDiNetEdgeDetectionInvocation,
"lineart_image_processor": lambda: __import__(
"invokeai.app.invocations.lineart", fromlist=["LineartEdgeDetectionInvocation"]
).LineartEdgeDetectionInvocation,
"lineart_anime_image_processor": lambda: __import__(
"invokeai.app.invocations.lineart_anime", fromlist=["LineartAnimeEdgeDetectionInvocation"]
).LineartAnimeEdgeDetectionInvocation,
"content_shuffle_image_processor": lambda: __import__(
"invokeai.app.invocations.content_shuffle", fromlist=["ContentShuffleInvocation"]
).ContentShuffleInvocation,
"dw_openpose_image_processor": lambda: __import__(
"invokeai.app.invocations.dw_openpose", fromlist=["DWOpenposeDetectionInvocation"]
).DWOpenposeDetectionInvocation,
"mediapipe_face_processor": lambda: __import__(
"invokeai.app.invocations.mediapipe_face", fromlist=["MediaPipeFaceDetectionInvocation"]
).MediaPipeFaceDetectionInvocation,
"canny_image_processor": lambda: (
__import__(
"invokeai.app.invocations.canny", fromlist=["CannyEdgeDetectionInvocation"]
).CannyEdgeDetectionInvocation
),
"hed_image_processor": lambda: (
__import__(
"invokeai.app.invocations.hed", fromlist=["HEDEdgeDetectionInvocation"]
).HEDEdgeDetectionInvocation
),
"mlsd_image_processor": lambda: (
__import__("invokeai.app.invocations.mlsd", fromlist=["MLSDDetectionInvocation"]).MLSDDetectionInvocation
),
"depth_anything_image_processor": lambda: (
__import__(
"invokeai.app.invocations.depth_anything", fromlist=["DepthAnythingDepthEstimationInvocation"]
).DepthAnythingDepthEstimationInvocation
),
"normalbae_image_processor": lambda: (
__import__("invokeai.app.invocations.normal_bae", fromlist=["NormalMapInvocation"]).NormalMapInvocation
),
"pidi_image_processor": lambda: (
__import__(
"invokeai.app.invocations.pidi", fromlist=["PiDiNetEdgeDetectionInvocation"]
).PiDiNetEdgeDetectionInvocation
),
"lineart_image_processor": lambda: (
__import__(
"invokeai.app.invocations.lineart", fromlist=["LineartEdgeDetectionInvocation"]
).LineartEdgeDetectionInvocation
),
"lineart_anime_image_processor": lambda: (
__import__(
"invokeai.app.invocations.lineart_anime", fromlist=["LineartAnimeEdgeDetectionInvocation"]
).LineartAnimeEdgeDetectionInvocation
),
"content_shuffle_image_processor": lambda: (
__import__(
"invokeai.app.invocations.content_shuffle", fromlist=["ContentShuffleInvocation"]
).ContentShuffleInvocation
),
"dw_openpose_image_processor": lambda: (
__import__(
"invokeai.app.invocations.dw_openpose", fromlist=["DWOpenposeDetectionInvocation"]
).DWOpenposeDetectionInvocation
),
"mediapipe_face_processor": lambda: (
__import__(
"invokeai.app.invocations.mediapipe_face", fromlist=["MediaPipeFaceDetectionInvocation"]
).MediaPipeFaceDetectionInvocation
),
# Note: zoe_depth_image_processor doesn't have a processor invocation implementation
"color_map_image_processor": lambda: __import__(
"invokeai.app.invocations.color_map", fromlist=["ColorMapInvocation"]
).ColorMapInvocation,
"color_map_image_processor": lambda: (
__import__("invokeai.app.invocations.color_map", fromlist=["ColorMapInvocation"]).ColorMapInvocation
),
}

if processor_type in processor_class_map:
Expand Down
18 changes: 17 additions & 1 deletion invokeai/backend/model_manager/configs/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
Type,
)

from pydantic import BaseModel, ConfigDict, Field, Tag
from pydantic import BaseModel, ConfigDict, Field, Tag, field_validator
from pydantic_core import PydanticUndefined

from invokeai.app.util.misc import uuid_string
Expand Down Expand Up @@ -77,6 +77,22 @@ class Config_Base(ABC, BaseModel):
default=None,
description="The original API response from the source, as stringified JSON.",
)
source_url: str | None = Field(
default=None,
description="Optional URL for the model (e.g. download page or model page).",
)

@field_validator("source_url", mode="before")
@classmethod
def validate_source_url(cls, v: Any) -> str | None:
if v is None or v == "":
return None
if not isinstance(v, str):
raise ValueError("source_url must be a string")
if not v.startswith(("https://", "http://")):
raise ValueError("source_url must be an http or https URL")
return v

cover_image: str | None = Field(
default=None,
description="Url for image to preview model",
Expand Down
1 change: 1 addition & 0 deletions invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1210,6 +1210,7 @@
"settings": "Settings",
"simpleModelPlaceholder": "URL or path to a local file or diffusers folder",
"source": "Source",
"sourceUrl": "Source URL",
"sigLip": "SigLIP",
"spandrelImageToImage": "Image to Image (Spandrel)",
"starterBundles": "Starter Bundles",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ export const ModelEdit = memo(({ modelConfig }: Props) => {
<Textarea {...form.register('description')} minH={32} />
</FormControl>
</Flex>
<Flex gap="4" alignItems="center">
<FormControl flexDir="column" alignItems="flex-start" gap={1}>
<FormLabel>{t('modelManager.sourceUrl')}</FormLabel>
<Input {...form.register('source_url')} type="url" size="md" placeholder="https://" />
</FormControl>
</Flex>
<Heading as="h3" fontSize="md" mt="4">
{t('modelManager.modelSettings')}
</Heading>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { Flex, Heading, Spacer, Text } from '@invoke-ai/ui-library';
import { Flex, Heading, Link, Spacer, Text } from '@invoke-ai/ui-library';
import { useIsModelManagerEnabled } from 'features/modelManagerV2/hooks/useIsModelManagerEnabled';
import ModelImageUpload from 'features/modelManagerV2/subpanels/ModelPanel/Fields/ModelImageUpload';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import type { AnyModelConfig } from 'services/api/types';

const isSafeUrl = (url: string): boolean => {
return url.startsWith('https://') || url.startsWith('http://');
};

type Props = PropsWithChildren<{
modelConfig: AnyModelConfig;
}>;
Expand All @@ -30,6 +34,14 @@ export const ModelHeader = memo(({ modelConfig, children }: Props) => {
{t('modelManager.source')}: {modelConfig.source}
</Text>
)}
{'source_url' in modelConfig && modelConfig.source_url && isSafeUrl(modelConfig.source_url) && (
<Text variant="subtext" noOfLines={1} wordBreak="break-all">
{t('modelManager.sourceUrl')}:{' '}
<Link href={modelConfig.source_url} isExternal color="invokeBlue.300">
{modelConfig.source_url}
</Link>
</Text>
)}
<Text noOfLines={3}>{modelConfig.description}</Text>
</Flex>
</Flex>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,7 @@ describe('Graph', () => {
source: '/home/bat/invokeai-4.0.0/models/sdxl/main/stable-diffusion-xl-1.0-inpainting-0.1',
source_type: 'path',
source_api_response: null,
source_url: null,
cover_image: null,
type: 'main',
trigger_phrases: null,
Expand Down
Loading
Loading