Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:22:06 +08:00

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))