Source code for novelai_image_mcp.nai.payload

"""NovelAI V4.5 wire payload mapping."""

from typing import Any

from .constants import is_v4_model
from .models import GenerationRequest, NovelAIGenerationPlan


def _caption(
    text: str,
    *,
    char_captions: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
    return {"caption": {"base_caption": text, "char_captions": char_captions or []}}


[docs] def build_payload( plan: NovelAIGenerationPlan, *, model: str, ) -> dict[str, Any]: """Map an already-resolved plan to NovelAI's V4.5 request shape.""" return { "action": "generate", "input": plan.prompt, "model": model, "parameters": { "width": plan.width, "height": plan.height, "steps": plan.steps, "scale": plan.scale, "sampler": plan.sampler, "seed": plan.seed, "n_samples": 1, "negative_prompt": plan.negative_prompt, "qualityToggle": True, "ucPreset": 0, "params_version": 3, "stream": "msgpack", "v4_prompt": _caption( plan.base_caption, char_captions=list(plan.char_captions), ) | {"use_coords": plan.use_coords, "use_order": True}, "v4_negative_prompt": _caption(plan.negative_prompt) | {"legacy_uc": False}, "characterPrompts": list(plan.character_prompts), "add_original_image": True, "autoSmea": False, "cfg_rescale": 0, "controlnet_strength": 1, "dynamic_thresholding": False, "image_format": "png", "legacy": False, "legacy_uc": False, "legacy_v3_extend": False, "noise_schedule": "karras", "normalize_reference_strength_multiple": True, "prefer_brownian": True, "use_coords": plan.use_coords, }, }
def _enum_value(value: object) -> object: return getattr(value, "value", value)
[docs] def build_generation_payload(request: GenerationRequest) -> dict[str, Any]: """Serialize every supported generation and conditioning option.""" prompt = request.effective_prompt negative_prompt = request.effective_negative_prompt characters = [ { "prompt": item.prompt, "uc": item.negative_prompt, "center": {"x": item.x, "y": item.y}, "enabled": item.enabled, } for item in request.character_prompts ] char_captions = [ { "char_caption": item.prompt, "centers": [{"x": item.x, "y": item.y}], } for item in request.character_prompts if item.enabled ] negative_char_captions = [ { "char_caption": item.negative_prompt, "centers": [{"x": item.x, "y": item.y}], } for item in request.character_prompts if item.enabled and item.negative_prompt ] parameters: dict[str, Any] = { "width": request.width, "height": request.height, "n_samples": request.n_samples, "steps": request.steps, "scale": request.scale, "sampler": _enum_value(request.sampler), "seed": request.seed, "negative_prompt": negative_prompt, "qualityToggle": request.quality, "ucPreset": request.uc_preset, "params_version": 3, "dynamic_thresholding": request.dynamic_thresholding, "cfg_rescale": request.cfg_rescale, "noise_schedule": _enum_value(request.noise_schedule), "add_original_image": request.add_original_image, "controlnet_strength": request.controlnet_strength, "characterPrompts": characters, "skip_cfg_above_sigma": request.skip_cfg_above_sigma, "legacy": request.legacy, "legacy_v3_extend": request.legacy_v3_extend, } optional = { "extra_noise_seed": request.extra_noise_seed, "sm": request.smea, "sm_dyn": request.smea_dynamic, "image": request.image, "mask": request.mask, "strength": request.strength, "noise": request.noise, "controlnet_condition": request.controlnet_condition, "controlnet_model": request.controlnet_model, } parameters.update({ key: value for key, value in optional.items() if value is not None }) if request.references: parameters["reference_image_multiple"] = list(request.references) if request.reference_information: parameters["reference_information_extracted_multiple"] = list( request.reference_information ) if request.reference_strengths: parameters["reference_strength_multiple"] = list( request.reference_strengths ) if is_v4_model(request.model): parameters.update({ "autoSmea": request.auto_smea, "normalize_reference_strength_multiple": ( request.normalize_reference_strengths ), "deliberate_euler_ancestral_bug": (request.deliberate_euler_ancestral_bug), "prefer_brownian": request.prefer_brownian, "use_coords": request.use_coords or bool(characters), "legacy_uc": request.legacy_uc, "stream": "msgpack", }) if request.inpaint_img2img_strength is not None: parameters["inpaintImg2ImgStrength"] = request.inpaint_img2img_strength parameters["v4_prompt"] = { "caption": { "base_caption": request.effective_base_caption, "char_captions": char_captions, }, "use_coords": parameters["use_coords"], "use_order": request.use_order, } parameters["v4_negative_prompt"] = { "caption": { "base_caption": negative_prompt, "char_captions": negative_char_captions, }, "legacy_uc": request.legacy_uc, } else: parameters["sm"] = request.smea or False parameters["sm_dyn"] = request.smea_dynamic or False return { "action": request.action.value, "input": prompt, "model": request.model.value, "parameters": parameters, }