File size: 43,932 Bytes
1e1d69b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 | # ---
# Filename: ../combined_test_codex/ComfyUI_INSTARAW/nodes/api_nodes/creative_api.py
# ---
# ---
# Filename: ../ComfyUI_INSTARAW/creative_api.py
# Creative Prompt Generation API - Gemini & Grok Integration
# ---
"""
Backend API endpoint for creative prompt generation using Gemini and Grok APIs.
Supports both inspiration-based generation and character-consistent generation.
"""
import os
import json
import hashlib
import aiohttp
from aiohttp import web
from server import PromptServer
CORS_HEADERS = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
}
# === Gemini Integration ===
async def generate_with_gemini(system_prompt, user_prompt, model="gemini-2.5-pro", api_key=None, temperature=0.9, top_p=0.9, images=None):
"""
Generate creative prompts using Google Gemini API.
Returns a list of {positive, negative, tags} dictionaries.
Uses the NEW Google Genai SDK pattern (matching gemini_native.py).
Args:
images: List of base64-encoded image strings (for vision/img2img mode)
"""
try:
from google import genai
from google.genai import types
import base64
except ImportError:
raise ImportError("The 'google-genai' library is required. Run: pip install -U google-genai")
# Use provided API key or fall back to environment variable
if not api_key or api_key.strip() == "":
api_key = os.environ.get("GEMINI_API_KEY")
if not api_key or api_key.strip() == "":
raise ValueError("Gemini API Key is missing. Provide it in the node or set GEMINI_API_KEY env var.")
try:
# NEW SDK pattern: use Client (matching gemini_native.py)
client = genai.Client(api_key=api_key)
# Build parts list (text + optional images)
parts = [types.Part.from_text(text=f"{system_prompt}\n\n{user_prompt}")]
# Add images if provided
if images and len(images) > 0:
for img_base64 in images:
try:
# Decode base64 to bytes
image_bytes = base64.b64decode(img_base64)
parts.append(types.Part.from_bytes(data=image_bytes, mime_type='image/png'))
print(f"[RPG Creative API] Added image to Gemini vision request (base64 length: {len(img_base64)})")
except Exception as img_error:
print(f"[RPG Creative API] Failed to decode image: {img_error}")
contents = [types.Content(role="user", parts=parts)]
# Safety settings
safety_settings = [
types.SafetySetting(category=cat, threshold="BLOCK_NONE")
for cat in ["HARM_CATEGORY_HATE_SPEECH", "HARM_CATEGORY_DANGEROUS_CONTENT",
"HARM_CATEGORY_HARASSMENT", "HARM_CATEGORY_SEXUALLY_EXPLICIT"]
]
# Generation config (matching gemini_native.py pattern)
config = types.GenerateContentConfig(
temperature=temperature,
top_p=top_p,
candidate_count=1,
safety_settings=safety_settings,
response_mime_type="application/json"
)
# Call API (synchronous call, but wrapped in async function)
response = client.models.generate_content(
model=f"models/{model}",
contents=contents,
config=config
)
if not response.candidates:
raise Exception("Gemini returned no candidates (likely blocked by safety filters)")
# Parse JSON response
result_text = response.text
return parse_prompt_json(result_text)
except Exception as e:
print(f"[RPG Creative API] Gemini error: {e}")
raise
# === Grok Integration ===
async def generate_with_grok(system_prompt, user_prompt, model="grok-4", api_key=None, temperature=0.9, top_p=0.9, images=None):
"""
Generate creative prompts using xAI Grok API.
Returns a list of {positive, negative, tags} dictionaries.
Args:
images: List of base64-encoded image strings (for vision/img2img mode)
"""
# Use provided API key or fall back to environment variable
if not api_key or api_key.strip() == "":
api_key = os.environ.get("XAI_API_KEY")
if not api_key or api_key.strip() == "":
raise ValueError("Grok API Key is missing. Provide it in the node or set XAI_API_KEY env var.")
try:
base_url = os.environ.get("XAI_API_BASE", "https://api.x.ai")
url = f"{base_url.rstrip('/')}/v1/chat/completions"
# Build user message content (text + optional images)
if images and len(images) > 0:
# Vision mode: multimodal content array
user_content = [
{"type": "text", "text": user_prompt}
]
for img_base64 in images:
try:
# Grok vision API expects base64 images in this format
user_content.append({
"type": "image_url",
"image_url": {
"url": f"data:image/png;base64,{img_base64}"
}
})
print(f"[RPG Creative API] Added image to Grok vision request (base64 length: {len(img_base64)})")
except Exception as img_error:
print(f"[RPG Creative API] Failed to add image to Grok request: {img_error}")
else:
# Text-only mode
user_content = user_prompt
payload = {
"model": model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_content},
],
"temperature": temperature,
"top_p": top_p,
"response_format": {"type": "json_object"},
}
headers = {
"Authorization": f"Bearer {api_key.strip()}",
"Content-Type": "application/json",
}
timeout = aiohttp.ClientTimeout(total=300)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.post(url, json=payload, headers=headers) as resp:
body = await resp.text()
if resp.status >= 400:
raise RuntimeError(f"Grok API error {resp.status}: {body}")
try:
data = json.loads(body)
except json.JSONDecodeError as e:
raise RuntimeError(f"Grok API returned invalid JSON: {e}")
choices = data.get("choices") or []
if not choices:
raise RuntimeError("Grok API returned no choices")
first_choice = choices[0]
message = first_choice.get("message") or {}
content = message.get("content") or first_choice.get("text", "")
if not content:
raise RuntimeError("Grok API returned empty content")
return parse_prompt_json(content)
except Exception as e:
print(f"[RPG Creative API] Grok error: {e}")
raise
# === Helper Functions ===
def parse_prompt_json(text):
"""
Parse JSON array of prompts from API response.
Handles markdown code blocks and extracts JSON.
"""
# Try to find JSON in markdown code blocks
if "```json" in text:
start = text.find("```json") + 7
end = text.find("```", start)
text = text[start:end].strip()
elif "```" in text:
start = text.find("```") + 3
end = text.find("```", start)
text = text[start:end].strip()
try:
data = json.loads(text)
if isinstance(data, list):
return data
elif isinstance(data, dict) and "prompts" in data:
return data["prompts"]
else:
return [data]
except json.JSONDecodeError:
# If JSON parsing fails, create a single entry from the text
return [{
"positive": text.strip(),
"negative": "",
"tags": []
}]
def build_system_prompt(is_sdxl=False, character_description="", generation_mode="img2img", affect_elements=None, user_text_input="", random_inspiration_prompts=None, generation_style="reality"):
"""
Build system prompt for creative generation.
Args:
is_sdxl: Whether to optimize for SDXL
character_description: Detailed character description for consistency
generation_mode: "img2img" or "txt2img"
affect_elements: List of elements to modify (img2img only): ["background", "outfit", "pose", "lighting"]
user_text_input: User's custom input (txt2img only)
random_inspiration_prompts: List of random prompts for inspiration (txt2img only)
generation_style: "reality" (strict adherence) or "creative" (flexible inspiration)
"""
base_prompt = """You are an expert AI prompt engineer specializing in creating high-quality, detailed prompts for REALISTIC photographic image generation models.
IMPORTANT: All images are REALISTIC photography. Do not include artistic styles, illustrations, or non-photographic elements."""
if is_sdxl:
base_prompt += "\n\nIMPORTANT: Generate prompts optimized for SDXL (Stable Diffusion XL). Use SDXL-specific quality tags like 'masterpiece', 'best quality', 'photorealistic', 'high resolution'."
# Character consistency
if character_description and character_description.strip():
base_prompt += f"\n\nCHARACTER CONSISTENCY: All prompts must accurately describe this character with exact physical features:\n{character_description}"
# Mode-specific instructions
if generation_mode == "img2img":
base_prompt += "\n\nMODE: Image-to-Image Generation"
if affect_elements and len(affect_elements) > 0:
# Inclusion mode - only modify checked elements
elements_str = ", ".join(affect_elements)
base_prompt += f"\n\nIMPORTANT INSTRUCTION: You are modifying ONLY these elements: {elements_str}"
base_prompt += "\n\nFor all OTHER elements NOT in this list, describe them EXACTLY as they appear in the input image. Do not change or modify them."
base_prompt += f"\n\nElements to MODIFY creatively: {elements_str}"
base_prompt += "\nElements to KEEP as-is: Everything else (describe exactly as shown)"
else:
# No checkboxes - describe as-is
base_prompt += "\n\nIMPORTANT INSTRUCTION: Describe the input images EXACTLY as they appear. Create detailed, accurate descriptions that capture all visual elements faithfully."
elif generation_mode == "txt2img":
base_prompt += "\n\nMODE: Text-to-Image Generation (creating new prompts from scratch)"
if random_inspiration_prompts and len(random_inspiration_prompts) > 0:
if generation_style == "reality":
# Reality Mode: Strict adherence to library prompts
base_prompt += "\n\n🎯 REALITY MODE: You must ONLY use elements, words, and concepts from these reference prompts. Stay precise and constrained to what's provided:"
for i, prompt in enumerate(random_inspiration_prompts[:5]): # Limit to 5 for context
pos = prompt.get("prompt", {}).get("positive", "")
tags = ", ".join(prompt.get("tags", [])[:5])
base_prompt += f"\n\nReference {i+1}:"
base_prompt += f"\nPrompt: {pos[:200]}..." # Truncate long prompts
base_prompt += f"\nTags: {tags}"
base_prompt += "\n\nIMPORTANT: Your generated prompts should ONLY combine and rearrange elements from these references. Do not introduce new concepts or elements not present in these prompts."
else:
# Creative Mode: Flexible inspiration
base_prompt += "\n\n✨ CREATIVE MODE: Use these prompts as creative inspiration for generating diverse, high-quality variations. Feel free to be flexible and creative:"
for i, prompt in enumerate(random_inspiration_prompts[:5]): # Limit to 5 for context
pos = prompt.get("prompt", {}).get("positive", "")
tags = ", ".join(prompt.get("tags", [])[:5])
base_prompt += f"\n\nInspiration {i+1}:"
base_prompt += f"\nPrompt: {pos[:200]}..." # Truncate long prompts
base_prompt += f"\nTags: {tags}"
base_prompt += "\n\nYou can be creative and add new elements while maintaining the overall style and quality of the inspiration prompts."
if user_text_input and user_text_input.strip():
base_prompt += f"\n\nUSER INPUT: Incorporate this user guidance into your prompts:\n{user_text_input}"
base_prompt += """\n\nOUTPUT FORMAT: You MUST return a valid JSON array of prompt objects. Do not include any other text or markdown. Each object must have these keys:
- "positive": A string containing the detailed positive prompt for realistic photography
- "negative": A string containing the negative prompt (avoid: unrealistic, illustration, painting, drawing, art, artistic, low quality, deformed, etc.)
- "tags": An array of strings representing relevant tags
Example:
[
{
"positive": "masterpiece, best quality, photorealistic, high resolution, professional photography, 1girl, blonde hair, blue eyes, smiling, park background, natural sunlight, bokeh, sharp focus",
"negative": "unrealistic, illustration, painting, drawing, art, artistic, low quality, deformed, bad anatomy, blurry, amateur",
"tags": ["portrait", "photorealistic", "outdoor", "natural_lighting"]
}
]
"""
return base_prompt
def build_user_prompt(generation_count, generation_mode="img2img", images_data=None):
"""
Build user prompt for creative generation.
Args:
generation_count: Number of prompts to generate
generation_mode: "img2img" or "txt2img"
images_data: For img2img, information about the images (optional)
"""
if generation_mode == "img2img":
if images_data and len(images_data) > 0:
return f"""Generate {generation_count} detailed, accurate prompts for the {len(images_data)} input image(s) provided.
Follow the instructions in the system prompt regarding which elements to modify and which to keep as-is.
Each prompt should:
1. Accurately describe all visual elements as specified
2. Maintain photorealistic quality
3. Be detailed and professional
4. Follow the modification rules (if any elements are specified to be changed)
Generate {generation_count} unique prompt(s)."""
else:
return f"""Generate {generation_count} high-quality prompts for realistic photographic image generation based on the input images.
Each prompt should be detailed, professional, and capture all visual elements accurately.
Generate {generation_count} unique prompt(s)."""
else: # txt2img
return f"""Generate {generation_count} high-quality, creative prompts for realistic photographic image generation.
Using the inspiration prompts and user input provided (if any), create diverse, professional prompts that:
1. Are detailed and vivid
2. Focus on realistic photographic elements
3. Include appropriate quality tags
4. Specify lighting, composition, and technical details
5. Are varied and creative while maintaining high quality
Generate {generation_count} unique prompts."""
# === API Endpoint ===
async def _generate_creative_prompts(request):
"""
POST /instaraw/generate_creative_prompts
Body:
{
// Legacy parameters (still supported)
"source_prompts": [{id, prompt: {positive, negative}}, ...],
"inspiration_count": 3,
"character_reference": "",
// New unified parameters
"generation_count": 5,
"is_sdxl": false,
"model": "gemini-2.5-pro",
"gemini_api_key": "",
"grok_api_key": "",
"temperature": 0.9,
"top_p": 0.9,
"force_regenerate": false,
// Character likeness (NEW)
"character_description": "Detailed character description...",
"use_character_likeness": true,
// Generation mode (NEW)
"generation_mode": "img2img", // or "txt2img"
// img2img parameters (NEW)
"images": ["base64_image_data..."], // Images for vision models
"affect_elements": ["background", "outfit"], // Which elements to modify
// txt2img parameters (NEW)
"random_inspiration_prompts": [{prompt, tags}, ...], // Random library prompts
"user_text_input": "Custom user guidance..."
}
Returns:
{
"success": true,
"prompts": [{positive, negative, tags}, ...]
}
"""
try:
data = await request.json()
# Core parameters
generation_count = int(data.get("generation_count", 5))
is_sdxl = bool(data.get("is_sdxl", False))
model = data.get("model", "gemini-2.5-pro")
gemini_api_key = data.get("gemini_api_key", "")
grok_api_key = data.get("grok_api_key", "")
temperature = float(data.get("temperature", 0.9))
top_p = float(data.get("top_p", 0.9))
temperature = max(0.0, min(2.0, temperature))
top_p = max(0.0, min(1.0, top_p))
force_regenerate = bool(data.get("force_regenerate", False))
# NEW: Character likeness
character_description = data.get("character_description", "")
use_character_likeness = bool(data.get("use_character_likeness", False))
if not use_character_likeness:
character_description = ""
# NEW: Generation mode
generation_mode = data.get("generation_mode", "img2img")
# NEW: img2img parameters
images = data.get("images", [])
multi_images = data.get("multi_images", []) # New format: [{label, base64}, ...]
affect_elements = data.get("affect_elements", [])
# If multi_images provided, convert to images array for backward compatibility
# Also build image labels description for the prompt
image_labels_context = ""
if multi_images and len(multi_images) > 0:
images = [img.get("base64") for img in multi_images if img.get("base64")]
labels = [img.get("label", f"image_{i+1}") for i, img in enumerate(multi_images)]
if len(labels) > 1:
image_labels_context = f"\n\nYou are provided with {len(labels)} reference images: {', '.join(labels)}. Consider all images when generating the prompt."
print(f"[RPG Creative API] Multi-image mode: {len(multi_images)} images with labels: {labels}")
# NEW: txt2img parameters
random_inspiration_prompts = data.get("random_inspiration_prompts", [])
user_text_input = data.get("user_text_input", "")
# NEW: Generation style (Reality vs Creative)
generation_style = data.get("generation_style", "reality")
# Legacy support: convert old parameters to new format
source_prompts = data.get("source_prompts", [])
inspiration_count = int(data.get("inspiration_count", 0))
character_reference = data.get("character_reference", "")
if character_reference and not character_description:
character_description = character_reference
use_character_likeness = True
if source_prompts and len(source_prompts) > 0 and not random_inspiration_prompts:
random_inspiration_prompts = source_prompts[:inspiration_count] if inspiration_count > 0 else []
# Custom system prompt override
custom_system_prompt = (data.get("system_prompt") or "").strip()
# Build prompts
if custom_system_prompt:
system_prompt = custom_system_prompt
else:
system_prompt = build_system_prompt(
is_sdxl=is_sdxl,
character_description=character_description,
generation_mode=generation_mode,
affect_elements=affect_elements,
user_text_input=user_text_input,
random_inspiration_prompts=random_inspiration_prompts,
generation_style=generation_style
)
user_prompt = build_user_prompt(
generation_count=generation_count,
generation_mode=generation_mode,
images_data=images
)
# Append multi-image context if available
if image_labels_context:
user_prompt = user_prompt + image_labels_context
# Check cache (skip if force_regenerate is True)
cache_key = hashlib.sha256(
f"{system_prompt}_{user_prompt}_{model}_{temperature}_{top_p}".encode("utf-8")
).hexdigest()
cache_dir = os.path.join(os.path.dirname(__file__), "..", "..", "cache")
os.makedirs(cache_dir, exist_ok=True)
cache_file = os.path.join(cache_dir, f"{cache_key}_creative.json")
if not force_regenerate and os.path.exists(cache_file):
print(f"[RPG Creative API] Using cached result: {cache_key[:8]}")
with open(cache_file, 'r', encoding='utf-8') as f:
prompts = json.load(f)
return web.json_response({"success": True, "prompts": prompts}, headers=CORS_HEADERS)
if force_regenerate:
print(f"[RPG Creative API] Force regenerate enabled - bypassing cache for {cache_key[:8]}")
# Generate with appropriate API
print(f"[RPG Creative API] Generating {generation_count} prompts - Mode: {generation_mode}, Model: {model}")
if use_character_likeness:
print(f"[RPG Creative API] Using character likeness (description length: {len(character_description)} chars)")
if generation_mode == "img2img" and affect_elements:
print(f"[RPG Creative API] Affecting elements: {affect_elements}")
if generation_mode == "txt2img":
print(f"[RPG Creative API] Inspiration prompts: {len(random_inspiration_prompts)}, User input: {len(user_text_input)} chars")
if images and len(images) > 0:
if len(images) > 1:
print(f"[RPG Creative API] 🖼️ Multi-image vision mode: {len(images)} images for combined prompt generation")
else:
print(f"[RPG Creative API] 🖼️ Processing {len(images)} image(s) for vision mode - First image base64 length: {len(images[0])} chars")
if model.startswith("gemini"):
prompts = await generate_with_gemini(system_prompt, user_prompt, model, gemini_api_key, temperature, top_p, images=images)
elif model.startswith("grok"):
prompts = await generate_with_grok(system_prompt, user_prompt, model, grok_api_key, temperature, top_p, images=images)
else:
raise ValueError(f"Unsupported model: {model}")
# Cache result
with open(cache_file, 'w', encoding='utf-8') as f:
json.dump(prompts, f, indent=2)
print(f"[RPG Creative API] Generated {len(prompts)} prompts with {model}")
return web.json_response({"success": True, "prompts": prompts}, headers=CORS_HEADERS)
except Exception as e:
print(f"[RPG Creative API] Error: {e}")
import traceback
traceback.print_exc()
return web.json_response({
"success": False,
"error": str(e)
}, status=500, headers=CORS_HEADERS)
@PromptServer.instance.routes.post("/instaraw/generate_creative_prompts")
async def generate_creative_prompts_endpoint(request):
return await _generate_creative_prompts(request)
@PromptServer.instance.routes.post("/instaraw/generate_creative_prompts/")
async def generate_creative_prompts_endpoint_slash(request):
return await _generate_creative_prompts(request)
@PromptServer.instance.routes.options("/instaraw/generate_creative_prompts")
@PromptServer.instance.routes.options("/instaraw/generate_creative_prompts/")
async def generate_creative_prompts_options(request):
return web.Response(headers=CORS_HEADERS)
# === Character Description Generation ===
# In-memory cache for character descriptions
CHARACTER_DESCRIPTION_CACHE = {}
def get_character_system_prompt(complexity="balanced"):
"""
Generate system prompt based on complexity level.
Complexity levels: concise (50-75 words), balanced (100-150 words), detailed (200-250 words)
"""
base_instruction = """You are an expert at analyzing images and generating character descriptions for image generation prompts.
Generate a character description focusing on PERMANENT physical features:
- Facial features (face shape, eyes, nose, lips, skin tone)
- Hair (color, length, style, texture)
- Body type and build
- Age and ethnicity
- Distinctive features (scars, tattoos, piercings, etc.)
DO NOT include clothing, background, pose, or temporary features.
DO NOT use tags like "1girl, solo" or similar categorization prefixes."""
if complexity == "concise":
length_instruction = "\nOUTPUT: A concise description (50-75 words) focusing only on the most essential and distinctive physical features."
elif complexity == "detailed":
length_instruction = "\nOUTPUT: A comprehensive, detailed description (200-250 words) covering all physical aspects with nuanced detail and specific characteristics."
else: # balanced
length_instruction = "\nOUTPUT: A balanced description (100-150 words) covering key physical features in natural language."
return base_instruction + length_instruction
async def generate_character_description_with_gemini(user_prompt, model="gemini-2.5-pro", api_key=None, character_image=None, complexity="balanced", custom_system_prompt=None, temperature=0.7, top_p=0.9):
"""
Generate plain text character description using Gemini.
Returns raw text, not JSON.
Uses the NEW Google Genai SDK pattern (matching gemini_native.py)
"""
try:
from google import genai
from google.genai import types
import base64
except ImportError:
raise ImportError("The 'google-genai' library is required. Run: pip install -U google-genai")
if not api_key or api_key.strip() == "":
api_key = os.environ.get("GEMINI_API_KEY")
if not api_key or api_key.strip() == "":
raise ValueError("Gemini API Key is missing")
try:
# NEW SDK pattern: use Client instead of configure()
client = genai.Client(api_key=api_key)
# Use custom system prompt if provided, otherwise generate based on complexity
if custom_system_prompt:
system_instruction = custom_system_prompt
else:
system_instruction = get_character_system_prompt(complexity)
# Build parts list (text + optional image)
parts = [types.Part.from_text(text=f"{system_instruction}\n\n{user_prompt}")]
# Add image if provided (matching gemini_native.py pattern)
if character_image:
# Extract base64 data (remove data URL prefix if present)
if character_image.startswith("data:"):
base64_data = character_image.split(",", 1)[1]
else:
base64_data = character_image
# Decode base64 to bytes
image_bytes = base64.b64decode(base64_data)
parts.append(types.Part.from_bytes(data=image_bytes, mime_type='image/png'))
contents = [types.Content(role="user", parts=parts)]
# Safety settings
safety_settings = [
types.SafetySetting(category=cat, threshold="BLOCK_NONE")
for cat in ["HARM_CATEGORY_HATE_SPEECH", "HARM_CATEGORY_DANGEROUS_CONTENT",
"HARM_CATEGORY_HARASSMENT", "HARM_CATEGORY_SEXUALLY_EXPLICIT"]
]
# Generation config with thinking support
# Gemini 3.0 uses thinking_level (HIGH/LOW), Gemini 2.5 uses thinking_budget
if model == "gemini-3-pro-preview":
# Gemini 3.0 format
thinking_config = types.ThinkingConfig(thinking_level="HIGH")
elif model == "gemini-2.5-pro":
# Gemini 2.5 format (unlimited thinking)
thinking_config = types.ThinkingConfig(thinking_budget=-1)
else:
# No thinking for other models
thinking_config = None
config_params = {
"temperature": temperature,
"top_p": top_p,
"candidate_count": 1,
"safety_settings": safety_settings
}
# Only add thinking_config if it's not None
if thinking_config:
config_params["thinking_config"] = thinking_config
config = types.GenerateContentConfig(**config_params)
# NEW SDK pattern: use client.models.generate_content()
response = client.models.generate_content(
model=f"models/{model}",
contents=contents,
config=config
)
if not response.candidates:
raise Exception("Gemini returned no candidates (likely blocked by safety filters)")
# Return raw text, not JSON
return response.text.strip()
except Exception as e:
print(f"[RPG Character API] Gemini error: {e}")
raise
async def generate_character_description_with_grok(user_prompt, model="grok-4-fast-reasoning", api_key=None, character_image=None, complexity="balanced", custom_system_prompt=None, temperature=0.7, top_p=0.9):
"""
Generate plain text character description using Grok.
Supports vision like grok_native.py
Returns raw text, not JSON.
"""
if not api_key or api_key.strip() == "":
api_key = os.environ.get("XAI_API_KEY")
if not api_key or api_key.strip() == "":
raise ValueError("Grok API Key is missing")
try:
base_url = os.environ.get("XAI_API_BASE", "https://api.x.ai").rstrip("/")
url = f"{base_url}/v1/chat/completions"
# Use custom system prompt if provided, otherwise generate based on complexity
if custom_system_prompt:
system_instruction = custom_system_prompt
else:
system_instruction = get_character_system_prompt(complexity)
# Build messages array (like grok_native.py)
messages = [
{"role": "system", "content": system_instruction}
]
# Build user content (text + image if provided)
user_content = [
{"type": "text", "text": user_prompt}
]
# Add image if provided (same format as grok_native.py line 127-132)
if character_image:
# character_image is already base64 from JavaScript
# Extract just the base64 part (remove "data:image/png;base64," prefix if present)
if character_image.startswith("data:"):
base64_data = character_image.split(",", 1)[1]
else:
base64_data = character_image
user_content.append({
"type": "image_url",
"image_url": {
"url": f"data:image/png;base64,{base64_data}",
"detail": "high"
}
})
messages.append({"role": "user", "content": user_content})
payload = {
"model": model,
"messages": messages,
"temperature": temperature,
"top_p": top_p,
}
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
timeout = aiohttp.ClientTimeout(total=300)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.post(url, json=payload, headers=headers) as resp:
body = await resp.text()
if resp.status >= 400:
raise RuntimeError(f"Grok API error {resp.status}: {body}")
try:
data = json.loads(body)
except json.JSONDecodeError as e:
raise RuntimeError(f"Grok API returned invalid JSON: {e}")
choices = data.get("choices") or []
if not choices:
raise RuntimeError("Grok API returned no choices")
first_choice = choices[0]
message = first_choice.get("message") or {}
content = message.get("content")
# Handle different response formats
if isinstance(content, list):
text = "".join(
part.get("text", "") if isinstance(part, dict) else str(part)
for part in content
).strip()
if text:
return text
elif isinstance(content, str) and content.strip():
return content.strip()
# Fallback
legacy = first_choice.get("text")
if legacy:
return legacy.strip()
raise RuntimeError("Unable to extract text from Grok response")
except Exception as e:
print(f"[RPG Character API] Grok error: {e}")
raise
def build_character_description_prompt():
"""Build system prompt for character description generation."""
return """You are an expert at analyzing images and generating detailed character descriptions for image generation prompts.
Your task is to generate a comprehensive, detailed character description that can be used consistently across multiple image generation prompts.
IMPORTANT: Focus on PERMANENT, CONSISTENT physical features that define the character:
- Facial features (face shape, eyes, nose, lips, skin tone)
- Hair (color, length, style, texture)
- Body type and build
- Age and ethnicity
- Distinctive features (scars, tattoos, piercings, etc.)
DO NOT include:
- Clothing or outfit (this varies per image)
- Background or setting
- Pose or action
- Temporary features (makeup, accessories)
OUTPUT FORMAT: Return a single detailed paragraph (100-150 words) describing the character's permanent physical features. This description will be inserted into other prompts to ensure character consistency.
Example output:
"A young woman in her mid-20s with an athletic build and olive skin tone. She has striking almond-shaped green eyes, high cheekbones, and full lips. Her dark brown hair falls in natural waves to shoulder length with subtle copper highlights. She has a defined jawline, straight nose, and arched eyebrows. Her features suggest Mediterranean heritage. She stands approximately 5'7" with a toned, balanced physique and confident posture."
"""
async def _generate_character_description(request):
"""
POST /instaraw/generate_character_description
Body:
{
"character_image": "base64_string_or_null",
"character_text": "manual_description_or_null",
"model": "gemini-2.5-pro",
"temperature": 0.7,
"top_p": 0.9,
"gemini_api_key": "",
"grok_api_key": "",
"force_regenerate": false
}
Returns:
{
"success": true,
"description": "Detailed character description...",
"cached": false,
"cache_key": "hash_of_inputs"
}
"""
try:
data = await request.json()
character_image = data.get("character_image")
character_text = data.get("character_text")
model = data.get("model", "gemini-2.5-pro")
temperature = float(data.get("temperature", 0.7))
top_p = float(data.get("top_p", 0.9))
gemini_api_key = data.get("gemini_api_key", "")
grok_api_key = data.get("grok_api_key", "")
force_regenerate = bool(data.get("force_regenerate", False))
complexity = data.get("complexity", "balanced")
custom_system_prompt = data.get("custom_system_prompt", "")
# Validation
if not character_image and not character_text:
return web.json_response({
"success": False,
"error": "Either character_image or character_text must be provided"
}, status=400, headers=CORS_HEADERS)
# Generate cache key (include complexity and custom prompt in key)
cache_input = character_image if character_image else character_text
cache_key = hashlib.sha256(f"{cache_input}_{model}_{complexity}_{custom_system_prompt}".encode("utf-8")).hexdigest()
# Check cache
if not force_regenerate and cache_key in CHARACTER_DESCRIPTION_CACHE:
print(f"[RPG Character API] Using cached character description: {cache_key[:8]}")
return web.json_response({
"success": True,
"description": CHARACTER_DESCRIPTION_CACHE[cache_key],
"cached": True,
"cache_key": cache_key
}, headers=CORS_HEADERS)
# Build user prompt
if character_image:
user_prompt = "Analyze this image and generate a detailed character description following the instructions."
else:
user_prompt = f"Enhance this character description into a detailed, structured format suitable for image generation:\n\n{character_text}"
# Generate description using dedicated function
print(f"[RPG Character API] Generating character description with {model} (complexity: {complexity})...")
if model.startswith("gemini"):
description = await generate_character_description_with_gemini(
user_prompt,
model=model,
api_key=gemini_api_key,
character_image=character_image,
complexity=complexity,
custom_system_prompt=custom_system_prompt if custom_system_prompt else None,
temperature=temperature,
top_p=top_p
)
elif model.startswith("grok"):
description = await generate_character_description_with_grok(
user_prompt,
model=model,
api_key=grok_api_key,
character_image=character_image,
complexity=complexity,
custom_system_prompt=custom_system_prompt if custom_system_prompt else None,
temperature=temperature,
top_p=top_p
)
else:
raise ValueError(f"Unsupported model: {model}")
if not description or description.strip() == "":
raise ValueError("Generated description is empty. API may have failed.")
# Cache the description
CHARACTER_DESCRIPTION_CACHE[cache_key] = description
print(f"[RPG Character API] ✅ Generated character description ({len(description)} chars)")
return web.json_response({
"success": True,
"description": description,
"cached": False,
"cache_key": cache_key
}, headers=CORS_HEADERS)
except Exception as e:
print(f"[RPG Character API] Error: {e}")
return web.json_response({
"success": False,
"error": str(e)
}, status=500, headers=CORS_HEADERS)
# === Random Prompt Selection ===
PROMPTS_DB_CACHE = None
PROMPTS_DB_URL = "https://instara.s3.us-east-1.amazonaws.com/prompts.db.json"
async def load_prompts_database():
"""Load and cache the prompts database from remote URL."""
global PROMPTS_DB_CACHE
if PROMPTS_DB_CACHE is not None:
return PROMPTS_DB_CACHE
print("[RPG Prompts API] Loading prompts database from remote URL...")
try:
timeout = aiohttp.ClientTimeout(total=60)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(PROMPTS_DB_URL) as resp:
if resp.status >= 400:
raise RuntimeError(f"Failed to load prompts database: HTTP {resp.status}")
data = await resp.json()
PROMPTS_DB_CACHE = data
print(f"[RPG Prompts API] Loaded {len(data)} prompts from database")
return data
except Exception as e:
print(f"[RPG Prompts API] Error loading database: {e}")
raise
async def _get_random_prompts(request):
"""
POST /instaraw/get_random_prompts
Body:
{
"count": 5,
"filters": {
"content_type": "person", // optional
"safety_level": "sfw", // optional
"shot_type": "portrait" // optional
}
}
Returns:
{
"success": true,
"prompts": [
{
"id": "...",
"tags": ["..."],
"prompt": {
"positive": "...",
"negative": "..."
},
"classification": {...}
}
]
}
"""
try:
import random
data = await request.json()
count = int(data.get("count", 5))
filters = data.get("filters", {})
# Load database
prompts_db = await load_prompts_database()
# Apply filters
filtered_prompts = prompts_db
if "content_type" in filters and filters["content_type"]:
content_type = filters["content_type"]
filtered_prompts = [
p for p in filtered_prompts
if p.get("classification", {}).get("content_type") == content_type
]
if "safety_level" in filters and filters["safety_level"]:
safety_level = filters["safety_level"]
filtered_prompts = [
p for p in filtered_prompts
if p.get("classification", {}).get("safety_level") == safety_level
]
if "shot_type" in filters and filters["shot_type"]:
shot_type = filters["shot_type"]
filtered_prompts = [
p for p in filtered_prompts
if p.get("classification", {}).get("shot_type") == shot_type
]
# Random selection
if len(filtered_prompts) == 0:
return web.json_response({
"success": False,
"error": "No prompts match the specified filters"
}, status=400, headers=CORS_HEADERS)
selected_count = min(count, len(filtered_prompts))
selected_prompts = random.sample(filtered_prompts, selected_count)
print(f"[RPG Prompts API] Selected {selected_count} random prompts from {len(filtered_prompts)} filtered")
return web.json_response({
"success": True,
"prompts": selected_prompts
}, headers=CORS_HEADERS)
except Exception as e:
print(f"[RPG Prompts API] Error: {e}")
return web.json_response({
"success": False,
"error": str(e)
}, status=500, headers=CORS_HEADERS)
# === Register New Endpoints ===
@PromptServer.instance.routes.post("/instaraw/generate_character_description")
async def generate_character_description_endpoint(request):
return await _generate_character_description(request)
@PromptServer.instance.routes.post("/instaraw/generate_character_description/")
async def generate_character_description_endpoint_slash(request):
return await _generate_character_description(request)
@PromptServer.instance.routes.options("/instaraw/generate_character_description")
@PromptServer.instance.routes.options("/instaraw/generate_character_description/")
async def generate_character_description_options(request):
return web.Response(headers=CORS_HEADERS)
@PromptServer.instance.routes.post("/instaraw/get_random_prompts")
async def get_random_prompts_endpoint(request):
return await _get_random_prompts(request)
@PromptServer.instance.routes.post("/instaraw/get_random_prompts/")
async def get_random_prompts_endpoint_slash(request):
return await _get_random_prompts(request)
@PromptServer.instance.routes.options("/instaraw/get_random_prompts")
@PromptServer.instance.routes.options("/instaraw/get_random_prompts/")
async def get_random_prompts_options(request):
return web.Response(headers=CORS_HEADERS)
print("[RPG Creative API] Endpoint registered: POST /instaraw/generate_creative_prompts")
print("[RPG Character API] Endpoint registered: POST /instaraw/generate_character_description")
print("[RPG Prompts API] Endpoint registered: POST /instaraw/get_random_prompts")
# Add these lines to the end of the file
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {} |