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 = {}