cddb07a176
docs / deploy (push) Has been cancelled
docs / changes (push) Has been cancelled
docs / check-and-build (push) Has been cancelled
build container image / cpu (push) Has been cancelled
build container image / cuda (push) Has been cancelled
build container image / rocm (push) Has been cancelled
frontend checks / frontend-checks (push) Has been cancelled
frontend tests / frontend-tests (push) Has been cancelled
lfs checks / lfs-check (push) Has been cancelled
python checks / python-checks (push) Has been cancelled
python tests / py3.12: macos-default (push) Has been cancelled
python tests / py3.11: windows-cpu (push) Has been cancelled
python tests / py3.12: windows-cpu (push) Has been cancelled
python tests / py3.11: linux-cpu (push) Has been cancelled
typegen checks / typegen-checks (push) Has been cancelled
uv lock checks / uv-lock-checks (push) Has been cancelled
openapi checks / openapi-checks (push) Has been cancelled
python tests / py3.11: macos-default (push) Has been cancelled
python tests / py3.12: linux-cpu (push) Has been cancelled
243 lines
9.4 KiB
Python
243 lines
9.4 KiB
Python
import asyncio
|
|
import logging
|
|
import threading
|
|
from pathlib import Path
|
|
from typing import Optional, Union
|
|
|
|
import torch
|
|
from dynamicprompts.generators import CombinatorialPromptGenerator, RandomPromptGenerator
|
|
from fastapi import Body, HTTPException
|
|
from fastapi.routing import APIRouter
|
|
from pydantic import BaseModel, Field
|
|
from pyparsing import ParseException
|
|
from transformers import AutoProcessor, AutoTokenizer, LlavaOnevisionForConditionalGeneration, LlavaOnevisionProcessor
|
|
|
|
from invokeai.app.api.auth_dependencies import CurrentUserOrDefault
|
|
from invokeai.app.api.dependencies import ApiDependencies
|
|
from invokeai.app.api.routers._access import assert_image_read_access
|
|
from invokeai.app.api.routers.image_move_maintenance import assert_image_move_maintenance_inactive
|
|
from invokeai.app.services.image_files.image_files_common import ImageFileNotFoundException
|
|
from invokeai.app.services.model_records.model_records_base import UnknownModelException
|
|
from invokeai.app.util.dynamicprompts import find_missing_wildcards
|
|
from invokeai.backend.llava_onevision_pipeline import LlavaOnevisionPipeline
|
|
from invokeai.backend.model_manager.taxonomy import ModelType
|
|
from invokeai.backend.text_llm_pipeline import DEFAULT_SYSTEM_PROMPT, TextLLMPipeline
|
|
from invokeai.backend.util.devices import TorchDevice
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
utilities_router = APIRouter(prefix="/v1/utilities", tags=["utilities"])
|
|
|
|
# The underlying model loader is not thread-safe, so we serialize load_model calls.
|
|
_model_load_lock = threading.Lock()
|
|
|
|
|
|
class DynamicPromptsResponse(BaseModel):
|
|
prompts: list[str]
|
|
error: Optional[str] = None
|
|
|
|
|
|
@utilities_router.post(
|
|
"/dynamicprompts",
|
|
operation_id="parse_dynamicprompts",
|
|
responses={
|
|
200: {"model": DynamicPromptsResponse},
|
|
},
|
|
)
|
|
async def parse_dynamicprompts(
|
|
current_user: CurrentUserOrDefault,
|
|
prompt: str = Body(description="The prompt to parse with dynamicprompts"),
|
|
max_prompts: int = Body(ge=1, le=10000, default=1000, description="The max number of prompts to generate"),
|
|
combinatorial: bool = Body(default=True, description="Whether to use the combinatorial generator"),
|
|
seed: int | None = Body(None, description="The seed to use for random generation. Only used if not combinatorial"),
|
|
) -> DynamicPromptsResponse:
|
|
"""Creates a batch process"""
|
|
max_prompts = min(max_prompts, 10000)
|
|
generator: Union[RandomPromptGenerator, CombinatorialPromptGenerator]
|
|
error: Optional[str] = None
|
|
|
|
# An unknown wildcard used as a variant value sends the combinatorial generator into an infinite
|
|
# loop, so bail out early with a clear message instead of hanging the request (and with it the UI
|
|
# preview). The random generator handles unknown wildcards gracefully, so only the combinatorial
|
|
# path is guarded.
|
|
if combinatorial:
|
|
missing_wildcards = find_missing_wildcards(prompt)
|
|
if missing_wildcards:
|
|
wildcards = ", ".join(missing_wildcards)
|
|
return DynamicPromptsResponse(prompts=[prompt], error=f"No values found for wildcard(s): {wildcards}")
|
|
|
|
try:
|
|
if combinatorial:
|
|
generator = CombinatorialPromptGenerator()
|
|
prompts = generator.generate(prompt, max_prompts=max_prompts)
|
|
else:
|
|
generator = RandomPromptGenerator(seed=seed)
|
|
prompts = generator.generate(prompt, num_images=max_prompts)
|
|
except ParseException as e:
|
|
prompts = [prompt]
|
|
error = str(e)
|
|
return DynamicPromptsResponse(prompts=prompts if prompts else [""], error=error)
|
|
|
|
|
|
# --- Expand Prompt ---
|
|
|
|
|
|
class ExpandPromptRequest(BaseModel):
|
|
prompt: str
|
|
model_key: str
|
|
max_tokens: int = Field(default=300, ge=1, le=2048)
|
|
system_prompt: str | None = None
|
|
|
|
|
|
class ExpandPromptResponse(BaseModel):
|
|
expanded_prompt: str
|
|
error: str | None = None
|
|
|
|
|
|
def _resolve_model_path(model_config_path: str) -> Path:
|
|
"""Resolve a model config path to an absolute path."""
|
|
model_path = Path(model_config_path)
|
|
if model_path.is_absolute():
|
|
return model_path.resolve()
|
|
base_models_path = ApiDependencies.invoker.services.configuration.models_path
|
|
return (base_models_path / model_path).resolve()
|
|
|
|
|
|
def _run_expand_prompt(prompt: str, model_key: str, max_tokens: int, system_prompt: str | None) -> str:
|
|
"""Run text LLM inference synchronously (called from thread)."""
|
|
model_manager = ApiDependencies.invoker.services.model_manager
|
|
model_config = model_manager.store.get_model(model_key)
|
|
|
|
if model_config.type != ModelType.TextLLM:
|
|
raise ValueError(f"Model '{model_key}' is not a TextLLM model (got {model_config.type})")
|
|
|
|
with _model_load_lock:
|
|
loaded_model = model_manager.load.load_model(model_config)
|
|
|
|
with torch.no_grad(), loaded_model.model_on_device() as (_, model):
|
|
model_abs_path = _resolve_model_path(model_config.path)
|
|
tokenizer = AutoTokenizer.from_pretrained(model_abs_path, local_files_only=True)
|
|
|
|
pipeline = TextLLMPipeline(model, tokenizer)
|
|
model_device = next(model.parameters()).device
|
|
output = pipeline.run(
|
|
prompt=prompt,
|
|
system_prompt=system_prompt or DEFAULT_SYSTEM_PROMPT,
|
|
max_new_tokens=max_tokens,
|
|
device=model_device,
|
|
dtype=TorchDevice.choose_torch_dtype(),
|
|
)
|
|
|
|
return output
|
|
|
|
|
|
@utilities_router.post(
|
|
"/expand-prompt",
|
|
operation_id="expand_prompt",
|
|
responses={
|
|
200: {"model": ExpandPromptResponse},
|
|
},
|
|
)
|
|
async def expand_prompt(current_user: CurrentUserOrDefault, body: ExpandPromptRequest) -> ExpandPromptResponse:
|
|
"""Expand a brief prompt into a detailed image generation prompt using a text LLM."""
|
|
try:
|
|
expanded = await asyncio.to_thread(
|
|
_run_expand_prompt,
|
|
body.prompt,
|
|
body.model_key,
|
|
body.max_tokens,
|
|
body.system_prompt,
|
|
)
|
|
return ExpandPromptResponse(expanded_prompt=expanded)
|
|
except UnknownModelException:
|
|
raise HTTPException(status_code=404, detail=f"Model '{body.model_key}' not found")
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=422, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Error expanding prompt: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
# --- Image to Prompt ---
|
|
|
|
|
|
class ImageToPromptRequest(BaseModel):
|
|
image_name: str
|
|
model_key: str
|
|
instruction: str = "Describe this image in detail for use as an AI image generation prompt."
|
|
|
|
|
|
class ImageToPromptResponse(BaseModel):
|
|
prompt: str
|
|
error: str | None = None
|
|
|
|
|
|
def _run_image_to_prompt(image_name: str, model_key: str, instruction: str) -> str:
|
|
"""Run LLaVA OneVision inference synchronously (called from thread)."""
|
|
model_manager = ApiDependencies.invoker.services.model_manager
|
|
model_config = model_manager.store.get_model(model_key)
|
|
|
|
if model_config.type != ModelType.LlavaOnevision:
|
|
raise ValueError(f"Model '{model_key}' is not a LLaVA OneVision model (got {model_config.type})")
|
|
|
|
with _model_load_lock:
|
|
loaded_model = model_manager.load.load_model(model_config)
|
|
|
|
# Load the image from InvokeAI's image store
|
|
image = ApiDependencies.invoker.services.images.get_pil_image(image_name)
|
|
image = image.convert("RGB")
|
|
|
|
with torch.no_grad(), loaded_model.model_on_device() as (_, model):
|
|
if not isinstance(model, LlavaOnevisionForConditionalGeneration):
|
|
raise TypeError(f"Expected LlavaOnevisionForConditionalGeneration, got {type(model).__name__}")
|
|
|
|
model_abs_path = _resolve_model_path(model_config.path)
|
|
processor = AutoProcessor.from_pretrained(model_abs_path, local_files_only=True)
|
|
if not isinstance(processor, LlavaOnevisionProcessor):
|
|
raise TypeError(f"Expected LlavaOnevisionProcessor, got {type(processor).__name__}")
|
|
|
|
pipeline = LlavaOnevisionPipeline(model, processor)
|
|
model_device = next(model.parameters()).device
|
|
output = pipeline.run(
|
|
prompt=instruction,
|
|
images=[image],
|
|
device=model_device,
|
|
dtype=TorchDevice.choose_torch_dtype(),
|
|
)
|
|
|
|
return output
|
|
|
|
|
|
@utilities_router.post(
|
|
"/image-to-prompt",
|
|
operation_id="image_to_prompt",
|
|
responses={
|
|
200: {"model": ImageToPromptResponse},
|
|
},
|
|
)
|
|
async def image_to_prompt(current_user: CurrentUserOrDefault, body: ImageToPromptRequest) -> ImageToPromptResponse:
|
|
"""Generate a descriptive prompt from an image using a vision-language model."""
|
|
assert_image_move_maintenance_inactive()
|
|
|
|
# Reuse the image-read access check so non-owners can't probe stored images
|
|
# via this endpoint (mirrors the policy in routers/images.py).
|
|
assert_image_read_access(body.image_name, current_user)
|
|
|
|
try:
|
|
prompt = await asyncio.to_thread(
|
|
_run_image_to_prompt,
|
|
body.image_name,
|
|
body.model_key,
|
|
body.instruction,
|
|
)
|
|
return ImageToPromptResponse(prompt=prompt)
|
|
except UnknownModelException:
|
|
raise HTTPException(status_code=404, detail=f"Model '{body.model_key}' not found")
|
|
except ImageFileNotFoundException:
|
|
raise HTTPException(status_code=404, detail=f"Image '{body.image_name}' not found")
|
|
except (ValueError, TypeError) as e:
|
|
raise HTTPException(status_code=422, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Error generating prompt from image: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|