File size: 11,597 Bytes
7aaa385
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import os
import random
import re
import json
import urllib.request
import urllib.error
import base64
import io
import time
import numpy as np
from PIL import Image
import torch


class DolphinMultiActionPromptNode_V32:
    @classmethod
    def INPUT_TYPES(s):
        return {
            "required": {
                "image": ("IMAGE",),
                "mode": (["๐Ÿค– Auto Vision+LLM", "โœ๏ธ Manual Override"], {"default": "๐Ÿค– Auto Vision+LLM"}),
                "character_name": ("STRING", {"multiline": False, "default": "AUTO"}),
                "artistic_vibe": ("STRING", {"multiline": True, "default": "cinematic lighting, high-speed action, dark fantasy"}),

                "master_story": ("STRING", {
                    "multiline": True,
                    "default": "์–ด๋‘์šด ๊ณจ๋ชฉ๊ธธ. ๊ฐ‘์ž๊ธฐ ๋‚˜ํƒ€๋‚œ ์ ๋“ค์„ ํ–ฅํ•ด ๋Œ์ง„ํ•œ๋‹ค, ํ™”๋ คํ•˜๊ฒŒ ๊ฒ€์„ ํœ˜๋‘˜๋Ÿฌ ์ ์„ ์“ฐ๋Ÿฌ๋œจ๋ฆฐ๋‹ค, ๋‚ ์•„์˜ค๋Š” ์ด์•Œ์„ ํŠ•๊ฒจ๋‚ธ๋‹ค, ์ ์—๊ฒŒ ๋‹ค๊ฐ€๊ฐ€ ์ˆจํ†ต์„ ๋Š๋Š”๋‹ค."
                }),

                "openrouter_api_key": ("STRING", {"multiline": False, "default": ""}),
                "openrouter_model": ("STRING", {"multiline": False, "default": "qwen/qwen-2-vl-72b-instruct"}),
                "creativity": ("FLOAT", {"default": 0.85, "min": 0.1, "max": 1.5, "step": 0.05}),
                "max_tokens": ("INT", {"default": 1500, "min": 256, "max": 8192, "step": 64}),
                "retries": ("INT", {"default": 2, "min": 0, "max": 5}),
                "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
            },
        }

    RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING")
    RETURN_NAMES = (
        "prompt_1 (Clip 1: 0-5s)",
        "prompt_2 (Clip 2: 0-5s)",
        "prompt_3 (Clip 3: 0-5s)",
        "prompt_4 (Clip 4: 0-5s)",
        "raw_llm_output",
    )
    FUNCTION = "generate_sequence"
    CATEGORY = "Dolphin"

    # -----------------------------------------------------------------
    def _encode_image(self, image):
        img_tensor = image[0]
        i = 255. * img_tensor.cpu().numpy()
        img_pil = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
        if img_pil.mode != "RGB":
            img_pil = img_pil.convert("RGB")
        buffered = io.BytesIO()
        img_pil.save(buffered, format="JPEG", quality=90)
        b64 = base64.b64encode(buffered.getvalue()).decode('utf-8')
        return f"data:image/jpeg;base64,{b64}"

    def _split_sentences(self, master_story):
        clean_story = re.sub(r'([.!?,\n])', r'\1|', master_story)
        raw_sentences = clean_story.split('|')
        return [s.strip() for s in raw_sentences if len(s.strip()) > 1]

    def _build_chunks(self, sentences):
        n = len(sentences)
        if n == 0:
            base = "dynamic high-speed action"
            return (base, base, base, base)
        if n == 1:
            s = sentences[0]
            return (
                f"Phase 1: Rapid approach and high-speed dynamic movement. DO NOT stand still. (Target: {s})",
                f"Phase 2: Swift, explosive execution of the action. (Target: {s})",
                f"Phase 3: The climax at full 1x real-time speed. Lightning fast! (Target: {s})",
                f"Phase 4: Fast-paced completion and quick recovery. (Target: {s})",
            )
        if n == 2:
            return (
                f"Phase 1: High-speed buildup and rapid preparation. (Target: {sentences[0]})",
                f"Phase 2: Explosively execute -> {sentences[0]}",
                f"Phase 3: Rapid transition, sprinting or moving quickly. (Target: {sentences[1]})",
                f"Phase 4: Lightning-fast execution -> {sentences[1]}",
            )
        if n == 3:
            return (
                f"Phase 1: Start this action rapidly -> {sentences[0]}",
                f"Phase 2: Explosively complete -> {sentences[0]}",
                sentences[1],
                sentences[2],
            )
        # n >= 4: ๊ท ๋“ฑ ๋ถ„๋ฐฐ
        k, m = divmod(n, 4)
        chunks = []
        start = 0
        for idx in range(4):
            end = start + k + (1 if idx < m else 0)
            chunks.append(" ".join(sentences[start:end]))
            start = end
        return tuple(chunks)

    def _call_llm(self, url, payload, api_key, retries, timeout=120):
        last_err = None
        for attempt in range(retries + 1):
            try:
                req = urllib.request.Request(
                    url,
                    data=json.dumps(payload).encode('utf-8'),
                    headers={'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'}
                )
                response = urllib.request.urlopen(req, timeout=timeout)
                body = json.loads(response.read().decode('utf-8'))
                return body['choices'][0]['message']['content'].strip(), None
            except Exception as e:
                last_err = e
                if attempt < retries:
                    time.sleep(1.5 * (attempt + 1))
        return None, last_err

    # -----------------------------------------------------------------
    def generate_sequence(self, image, mode, character_name, artistic_vibe, master_story,
                          openrouter_api_key, openrouter_model, creativity, max_tokens, retries, seed):

        user_defined_name = "" if character_name.upper() in ["AUTO", ""] else character_name.strip()

        def build_final(action, master_scene, name):
            tags_list = []
            if name:
                tags_list.append(name)
            tag_block = ", ".join(tags_list)

            sentence_list = []
            if master_scene.strip():
                sentence_list.append(master_scene.strip().strip(",. "))
            if action.strip():
                sentence_list.append(action.strip())
            sentence_block = " ".join(sentence_list)

            if tag_block and sentence_block:
                return f"{tag_block}\n{sentence_block}"
            elif tag_block:
                return tag_block
            return sentence_block

        # ---- Manual Override ----
        if mode == "โœ๏ธ Manual Override":
            fp = build_final(master_story, "", user_defined_name)
            return (fp, fp, fp, fp, "[Manual Override]")

        random.seed(seed)

        base64_image = self._encode_image(image)
        sentences = self._split_sentences(master_story)
        chunk_1, chunk_2, chunk_3, chunk_4 = self._build_chunks(sentences)

        sys_prompt = (
            "You are an Elite Action Director prioritizing RAW SPEED and KINETIC ENERGY.\n"
            f"1. CHARACTER: If NAME is 'AUTO', assign a name. If '{user_defined_name}', use it.\n"
            "2. VISUAL ANALYSIS: You MUST base your descriptions EXACTLY on the character's clothing and weapons in the attached IMAGE.\n"
            "3. MASTER SCENE: Write a 1-sentence environment description (lighting, weather).\n"
            "4. SPEED-FOCUSED CHOREOGRAPHY (CRITICAL):\n"
            "   - ๐Ÿšซ BAN SLOW-MOTION TRIGGERS: NEVER use words like 'micro-expressions', 'muscle tension', 'slowly turning', 'floating', or 'gradually'. These cause AI video models to render in slow-motion.\n"
            "   - โœ… FORCE 1x REAL-TIME SPEED: Describe large, sweeping, high-velocity movements. Use aggressive verbs (dashing, sprinting, whipping, snapping).\n"
            "   - โœ… KINETIC ADVERBS: Inject phrases like 'in a flash', 'at lightning speed', 'with explosive real-time velocity' into EVERY part.\n"
            "   - Example: 'suddenly dashes forward at full speed and delivers a lightning-fast horizontal strike, moving so quickly the rain splatters'.\n"
            "   - Strictly confine the actions. DO NOT animate future events early.\n"
            "   - ๐Ÿ”ฅ OUTPUT RULE: DO NOT quote the Korean text. Only output English.\n"
            "Format EXACTLY:\nCHARACTER: [Name]\nMASTER SCENE: [Description]\n"
            "PART 1: [0-1s] [Action A] [2-3s] [Action B] [4-5s] [Action C]\n"
            "PART 2: [0-1s] [Action D] [2-3s] [Action E] [4-5s] [Action F]\n"
            "PART 3: [0-1s] [Action G] [2-3s] [Action H] [4-5s] [Action I]\n"
            "PART 4: [0-1s] [Action J] [2-3s] [Action K] [4-5s] [Action L]"
        )

        usr_text = (
            f"NAME: {character_name}\n"
            f"VIBE: {artistic_vibe}\n\n"
            "=== HIGH-SPEED ACTION SCRIPT ===\n"
            f"โ–ถ For PART 1 (0-5s), ONLY animate this: \"{chunk_1}\"\n"
            f"โ–ถ For PART 2 (5-10s), ONLY animate this: \"{chunk_2}\"\n"
            f"โ–ถ For PART 3 (10-15s), ONLY animate this: \"{chunk_3}\"\n"
            f"โ–ถ For PART 4 (15-20s), ONLY animate this: \"{chunk_4}\"\n"
            "CRITICAL: Keep the action moving FAST. Avoid still poses or micro-details that look like slow-mo!"
        )

        url = "https://openrouter.ai/api/v1/chat/completions"
        payload = {
            "model": openrouter_model.strip(),
            "messages": [
                {"role": "system", "content": sys_prompt},
                {"role": "user", "content": [
                    {"type": "text", "text": usr_text},
                    {"type": "image_url", "image_url": {"url": base64_image}}
                ]}
            ],
            "temperature": creativity,
            "max_tokens": max_tokens,
        }

        if not openrouter_api_key.strip():
            err_msg = "โš ๏ธ API Error: OpenRouter API key is empty."
            return (err_msg, err_msg, err_msg, err_msg, err_msg)

        llm_prompt, err = self._call_llm(url, payload, openrouter_api_key, retries)
        if llm_prompt:
            print(f"\nโœ… [Dolphin V32 - Action Speed Optimized]\n{llm_prompt}\n")
        else:
            print(f"โŒ [์—๋Ÿฌ] API ํ˜ธ์ถœ ์‹คํŒจ: {err}")
            llm_prompt = ""

        final_char_name = user_defined_name
        r_master = ""
        p1 = p2 = p3 = p4 = ""

        if llm_prompt and "[removed]" not in llm_prompt:
            cl = re.sub(r'[*#]', '', llm_prompt)
            m_char = re.search(r'CHARACTER:\s*(.*?)(?=MASTER SCENE|$)', cl, re.I | re.S)
            m_master = re.search(r'MASTER SCENE:\s*(.*?)(?=PART 1|$)', cl, re.I | re.S)

            m1 = re.search(r'PART 1:\s*(.*?)(?=PART 2|$)', cl, re.I | re.S)
            m2 = re.search(r'PART 2:\s*(.*?)(?=PART 3|$)', cl, re.I | re.S)
            m3 = re.search(r'PART 3:\s*(.*?)(?=PART 4|$)', cl, re.I | re.S)
            m4 = re.search(r'PART 4:\s*(.*?)(?=\n\n|===|Note:|$)', cl, re.I | re.S)

            if not user_defined_name and m_char:
                final_char_name = m_char.group(1).strip()

            r_master = m_master.group(1).strip() if m_master else ""

            # ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ํ•ด๋‹น ์ฒญํฌ(์˜๋ฌธ ์ง€์‹œ๋ฌธ)๋ฅผ ํด๋ฐฑ์œผ๋กœ ์‚ฌ์šฉํ•ด ๋น„๋””์˜ค ํ”„๋กฌํ”„ํŠธ๊ฐ€ ๋น„์ง€ ์•Š๋„๋ก ํ•จ
            p1 = m1.group(1).strip() if m1 else chunk_1
            p2 = m2.group(1).strip() if m2 else chunk_2
            p3 = m3.group(1).strip() if m3 else chunk_3
            p4 = m4.group(1).strip() if m4 else chunk_4
        else:
            # API ์‹คํŒจ ์‹œ์—๋„ ์Šคํฌ๋ฆฝํŠธ ์ฒญํฌ๋ฅผ ํด๋ฐฑ์œผ๋กœ ๋ฐ˜ํ™˜ (์™„์ „ ์‹คํŒจ๋ณด๋‹ค ์œ ์šฉ)
            p1, p2, p3, p4 = chunk_1, chunk_2, chunk_3, chunk_4

        return (
            build_final(p1, r_master, final_char_name),
            build_final(p2, r_master, final_char_name),
            build_final(p3, r_master, final_char_name),
            build_final(p4, r_master, final_char_name),
            llm_prompt if llm_prompt else "โš ๏ธ API Error / empty response",
        )