diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..8c59bf4 --- /dev/null +++ b/__init__.py @@ -0,0 +1,179 @@ +import os +import json +import struct +import zlib +import base64 +from pathlib import Path +from PIL import Image +import io +import server +from aiohttp import web +import threading + +# ==================== GLOBAL STATE ==================== +_current_library = None +_available_libraries = [] + +# ==================== ASSET LIBRARY ==================== +class DianaLibrary: + def __init__(self): + self.metadata = {} + self.sprites = {} + self.audio = {} + self.phrases = {} + self.loaded = False + self.name = "Unknown" + self.color = "#9b59b6" + self.bgcolor = "#4a2a5a" + + def load(self, lib_path): + try: + with open(lib_path, 'rb') as f: + header = f.read(8).decode() + if header != "DIANA2.0": + raise ValueError(f"Invalid library format: {header}") + + meta_len = struct.unpack('I', f.read(4))[0] + compressed_meta = f.read(meta_len) + self.metadata = json.loads(zlib.decompress(compressed_meta)) + + self.name = self.metadata.get("name", lib_path.stem) + self.color = self.metadata.get("color", "#9b59b6") + self.bgcolor = self.metadata.get("bgcolor", "#4a2a5a") + + emotions = self.metadata.get("emotions", []) + frame_count = self.metadata.get("frame_count", 16) + + print(f"\033[95m[Diana API]\033[0m 🎨 Loading {self.name}...") + + for emotion in emotions: + frames = [] + for _ in range(frame_count): + frame_len = struct.unpack('I', f.read(4))[0] + compressed = f.read(frame_len) + img_bytes = zlib.decompress(compressed) + + # Преобразуем байты в изображение без numpy + from PIL import Image + import io + img = Image.frombytes('RGBA', (128, 128), img_bytes) + frames.append(img) + + self.sprites[emotion] = frames + print(f"\033[95m[Diana API]\033[0m ✅ {emotion}") + + while True: + pos = f.tell() + check = f.read(9) + if check == b"DIANA_END" or len(check) < 9: + break + f.seek(pos) + + name_len = struct.unpack('I', f.read(4))[0] + name = f.read(name_len).decode() + audio_len = struct.unpack('I', f.read(4))[0] + audio_data = f.read(audio_len) + self.audio[name] = audio_data + print(f"\033[95m[Diana API]\033[0m ✅ {name}") + + self.phrases = self.metadata.get("phrases", {}) + + self.loaded = True + print(f"\033[95m[Diana API]\033[0m ✅ Loaded: {self.name}") + return self + except Exception as e: + print(f"\033[91m[Diana API]\033[0m ❌ Failed to load {lib_path.name}: {e}") + import traceback + traceback.print_exc() + return self + +def scan_libraries(): + global _available_libraries + libs_folder = Path(__file__).parent / "libraries" + if not libs_folder.exists(): + libs_folder.mkdir(exist_ok=True) + print(f"\033[95m[Diana API]\033[0m 📁 Created libraries folder") + + _available_libraries = [] + for lib_file in libs_folder.glob("*.lib"): + _available_libraries.append(lib_file.stem) + print(f"\033[95m[Diana API]\033[0m 📦 Found: {lib_file.name}") + +def load_library(lib_name): + global _current_library + lib_path = Path(__file__).parent / "libraries" / f"{lib_name}.lib" + if not lib_path.exists(): + print(f"\033[91m[Diana API]\033[0m ❌ Library not found: {lib_name}") + return False + + print(f"\033[95m[Diana API]\033[0m 📦 Loading: {lib_name}") + _current_library = DianaLibrary().load(lib_path) + return _current_library.loaded + +# Сканируем при старте +scan_libraries() +if _available_libraries: + load_library(_available_libraries[0]) + +# ==================== API ENDPOINTS ==================== +def add_cors(response): + response.headers['Access-Control-Allow-Origin'] = '*' + return response + +@server.PromptServer.instance.routes.get("/diana/libraries") +async def get_libraries(request): + return web.json_response({ + "libraries": _available_libraries, + "current": _current_library.name if _current_library else None + }) + +@server.PromptServer.instance.routes.post("/diana/library/load") +async def load_library_endpoint(request): + try: + data = await request.json() + lib_name = data.get("library") + if not lib_name: + return web.json_response({"error": "No library"}, status=400) + + if load_library(lib_name): + return web.json_response({"success": True}) + return web.json_response({"error": "Failed"}, status=404) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + +@server.PromptServer.instance.routes.get("/diana/library/current") +async def get_current_library_endpoint(request): + lib = _current_library + if not lib or not lib.loaded: + return web.Response(status=404) + + sprites_base64 = {} + for emotion, frames in lib.sprites.items(): + sprites_base64[emotion] = [] + for img in frames: + img_io = io.BytesIO() + img.save(img_io, format='PNG') + img_io.seek(0) + sprites_base64[emotion].append(base64.b64encode(img_io.read()).decode()) + + audio_base64 = {} + for name, data in lib.audio.items(): + audio_base64[name] = base64.b64encode(data).decode() + + return web.json_response({ + "name": lib.name, + "color": lib.color, + "bgcolor": lib.bgcolor, + "sprites": sprites_base64, + "audio": audio_base64, + "phrases": lib.phrases, + "metadata": { + "emotions": list(lib.sprites.keys()), + "frame_count": len(next(iter(lib.sprites.values()))) if lib.sprites else 0 + } + }) + +# ==================== WEB DIRECTORY ==================== +WEB_DIRECTORY = "./web" + +__all__ = [] \ No newline at end of file diff --git a/diana_lib_packer/audio/complete.mp3 b/diana_lib_packer/audio/complete.mp3 new file mode 100644 index 0000000..65008cd Binary files /dev/null and b/diana_lib_packer/audio/complete.mp3 differ diff --git a/diana_lib_packer/audio/error.mp3 b/diana_lib_packer/audio/error.mp3 new file mode 100644 index 0000000..73bb708 Binary files /dev/null and b/diana_lib_packer/audio/error.mp3 differ diff --git a/diana_lib_packer/audio/greeting.mp3 b/diana_lib_packer/audio/greeting.mp3 new file mode 100644 index 0000000..f89db22 Binary files /dev/null and b/diana_lib_packer/audio/greeting.mp3 differ diff --git a/diana_lib_packer/audio/start.mp3 b/diana_lib_packer/audio/start.mp3 new file mode 100644 index 0000000..f67c6c1 Binary files /dev/null and b/diana_lib_packer/audio/start.mp3 differ diff --git a/diana_lib_packer/audio/waiting.mp3 b/diana_lib_packer/audio/waiting.mp3 new file mode 100644 index 0000000..bd42020 Binary files /dev/null and b/diana_lib_packer/audio/waiting.mp3 differ diff --git a/diana_lib_packer/pack.bat b/diana_lib_packer/pack.bat new file mode 100644 index 0000000..b0065da --- /dev/null +++ b/diana_lib_packer/pack.bat @@ -0,0 +1,50 @@ +@echo off +chcp 65001 >nul +title Diana Library Packer +echo 🧝 Diana Library Packer +echo ========================= + +REM Проверяем, есть ли python в системе +python --version >nul 2>&1 +if errorlevel 1 ( + echo ❌ Python not found! Please install Python first. + pause + exit /b 1 +) + +REM Проверяем, есть ли наш скрипт +if not exist pack_to_lib.py ( + echo ❌ pack_to_lib.py not found in current directory! + pause + exit /b 1 +) + +REM Проверяем наличие PIL (Pillow) +python -c "from PIL import Image" >nul 2>&1 +if errorlevel 1 ( + echo. + echo ⚠️ Pillow library not found! Installing... + pip install pillow + if errorlevel 1 ( + echo ❌ Failed to install Pillow! + pause + exit /b 1 + ) +) + +echo. +echo 📦 Running packer... +echo. + +python pack_to_lib.py + +if errorlevel 1 ( + echo. + echo ❌ Packing failed! Check errors above. +) else ( + echo. + echo ✅ Packing completed successfully! +) + +echo. +pause \ No newline at end of file diff --git a/diana_lib_packer/pack_to_lib.py b/diana_lib_packer/pack_to_lib.py new file mode 100644 index 0000000..2f68d47 --- /dev/null +++ b/diana_lib_packer/pack_to_lib.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +""" +Diana Sprite and Audio Packer +Creates a single .diana file containing all sprites, audio, and phrases +""" + +import os +import json +import struct +import zlib +from PIL import Image +from pathlib import Path + +class DianaPacker: + def __init__(self): + self.version = "DIANA2.0" + self.data = { + "metadata": { + "name": "Diana", + "version": "1.0.0", + "author": "Unknown", + "color": "#9b59b6", + "bgcolor": "#4a2a5a", + "emotions": [], + "phrases": {}, + "frame_count": 16, + "sprite_size": 128, + "voice": { + "pitch": 1.0, + "speed": 1.0, + "format": "mp3" + } + }, + "sprites": {}, + "audio": {} + } + + def add_sprites(self, folder_path): + """Add all PNG sprites from folder""" + folder = Path(folder_path) + if not folder.exists(): + print(f"❌ Sprite folder not found: {folder}") + return + + png_files = sorted(folder.glob("*.png")) + print(f"\n📦 Loading sprites from {folder}...") + + for png_file in png_files: + emotion = png_file.stem + print(f" • {emotion}.png") + + img = Image.open(png_file).convert('RGBA') + + # Split into 128x128 frames (expecting 512x512 = 4x4 frames) + frames = [] + for row in range(4): + for col in range(4): + x1 = col * 128 + y1 = row * 128 + x2 = x1 + 128 + y2 = y1 + 128 + frame = img.crop((x1, y1, x2, y2)) + frames.append(frame) + + self.data["sprites"][emotion] = frames + self.data["metadata"]["emotions"].append(emotion) + print(f" → {len(frames)} frames") + + def add_audio(self, folder_path): + """Add all MP3 audio files from folder WITHOUT compressing them!""" + folder = Path(folder_path) + if not folder.exists(): + print(f"❌ Audio folder not found: {folder}") + return + + mp3_files = sorted(folder.glob("*.mp3")) + print(f"\n🎵 Loading audio from {folder}...") + + for mp3_file in mp3_files: + name = mp3_file.stem + print(f" • {name}.mp3") + + with open(mp3_file, 'rb') as f: + audio_data = f.read() + + # Проверим, что это действительно MP3 + if len(audio_data) > 10: + is_mp3 = (audio_data[:3] == b'ID3') or (audio_data[0] == 0xFF and (audio_data[1] & 0xE0) == 0xE0) + if not is_mp3: + print(f" ⚠️ Warning: {name}.mp3 doesn't look like a valid MP3 file!") + + # Сохраняем как есть + self.data["audio"][name] = audio_data + print(f" → {len(audio_data)} bytes (stored as-is)") + + def add_phrases(self, json_file): + """Add phrases and metadata from JSON file""" + if not Path(json_file).exists(): + print(f"❌ Phrases file not found: {json_file}") + return + + print(f"\n📝 Loading data from {json_file}...") + with open(json_file, 'r', encoding='utf-8') as f: + data = json.load(f) + + # Обновляем метаданные из JSON + if "name" in data: + self.data["metadata"]["name"] = data["name"] + if "author" in data: + self.data["metadata"]["author"] = data["author"] + if "version" in data: + self.data["metadata"]["version"] = data["version"] + if "color" in data: + self.data["metadata"]["color"] = data["color"] + if "bgcolor" in data: + self.data["metadata"]["bgcolor"] = data["bgcolor"] + + # Загружаем фразы (если есть) + if "phrases" in data: + self.data["metadata"]["phrases"] = data["phrases"] + total = sum(len(v) for v in data["phrases"].values()) + print(f" • {total} phrases loaded") + else: + print(f" ⚠️ No phrases found in JSON") + + def pack(self, output_file="diana.lib"): + """Pack everything into a single .diana file""" + print(f"\n📦 Packing to {output_file}...") + + with open(output_file, 'wb') as f: + # Write header + f.write(self.version.encode()) + + # Write metadata (compressed JSON) + meta_json = json.dumps(self.data["metadata"], indent=2, ensure_ascii=False).encode('utf-8') + compressed_meta = zlib.compress(meta_json, level=9) + f.write(struct.pack('I', len(compressed_meta))) + f.write(compressed_meta) + print(f" • Metadata: {len(meta_json)} -> {len(compressed_meta)} bytes") + print(f" Name: {self.data['metadata']['name']}") + print(f" Author: {self.data['metadata']['author']}") + print(f" Colors: {self.data['metadata']['color']} / {self.data['metadata']['bgcolor']}") + + # Write sprites (сжимаем спрайты) + sprite_count = 0 + for emotion, frames in self.data["sprites"].items(): + for frame in frames: + # Получаем байты изображения напрямую из PIL + img_bytes = frame.tobytes() + compressed = zlib.compress(img_bytes, level=9) + f.write(struct.pack('I', len(compressed))) + f.write(compressed) + sprite_count += 1 + print(f" • Sprites: {sprite_count} frames (compressed)") + + # Write audio (НЕ СЖАТЫЕ!) + audio_count = 0 + for name, audio_data in sorted(self.data["audio"].items()): + name_bytes = name.encode('utf-8') + f.write(struct.pack('I', len(name_bytes))) + f.write(name_bytes) + f.write(struct.pack('I', len(audio_data))) + f.write(audio_data) + audio_count += 1 + print(f" • Audio: {audio_count} files (uncompressed)") + + # Write end marker + f.write(b"DIANA_END") + + # Проверим размер + file_size = Path(output_file).stat().st_size + print(f"\n✅ Successfully packed to {output_file}") + print(f"📊 Summary:") + print(f" • Name: {self.data['metadata']['name']}") + print(f" • Emotions: {len(self.data['metadata']['emotions'])}") + print(f" • Frames per emotion: {self.data['metadata']['frame_count']}") + print(f" • Audio files: {len(self.data['audio'])}") + print(f" • Total size: {file_size} bytes") + + # Финальная проверка + audio_size = sum(len(d) for d in self.data['audio'].values()) + print(f" • Audio data: {audio_size} bytes") + +def main(): + """Main packing routine""" + packer = DianaPacker() + + # Default folders (adjust as needed) + sprites_folder = "./sprites" + audio_folder = "./audio" + phrases_file = "./phrases.json" + output_file = "./diana.lib" + + # Check if folders exist + if not Path(sprites_folder).exists(): + print(f"Creating {sprites_folder} directory...") + Path(sprites_folder).mkdir(exist_ok=True) + print(f"Please add your PNG sprites to {sprites_folder}") + return + + if not Path(audio_folder).exists(): + print(f"Creating {audio_folder} directory...") + Path(audio_folder).mkdir(exist_ok=True) + print(f"Please add your MP3 files to {audio_folder}") + return + + # Create sample phrases.json if it doesn't exist + if not Path(phrases_file).exists(): + sample_phrases = { + "name": "Diana", + "author": "Your Name", + "version": "1.0.0", + "color": "#9b59b6", + "bgcolor": "#4a2a5a", + "phrases": { + "greeting": [ + "Hello again, let's create something beautiful.", + "Diana is here. Ready when you are." + ], + "start": [ + "Generating... this might be good.", + "Here we go!" + ], + "complete": [ + "Done! Take a look.", + "Finished. I hope you like it." + ], + "error": [ + "Oops. Something went wrong.", + "Error... maybe clean the queue?" + ], + "waiting": [ + "Still working... patience, my friend.", + "Good things take time." + ] + } + } + with open(phrases_file, 'w', encoding='utf-8') as f: + json.dump(sample_phrases, f, indent=2, ensure_ascii=False) + print(f"Created sample {phrases_file}") + return + + # Load everything + packer.add_sprites(sprites_folder) + packer.add_audio(audio_folder) + packer.add_phrases(phrases_file) + + # Pack + packer.pack(output_file) + + print(f"\n✨ Done! Copy {output_file} to your ComfyUI-Diana/libraries/ folder.") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/diana_lib_packer/phrases.json b/diana_lib_packer/phrases.json new file mode 100644 index 0000000..2c9a476 --- /dev/null +++ b/diana_lib_packer/phrases.json @@ -0,0 +1,16 @@ +{ + "name": "Diana", + "author": "Rimor", + "version": "1.0.0", + "color": "#d04069", + "bgcolor": "#1a1a1a", + "emotions": ["neutral", "waiting", "speaking", "happy", "surprised"], + "frame_count": 16, + "phrases": { + "greeting": ["Hello again, let's create something beautiful.", "Diana is here. Ready when you are.","Your digital muse has arrived."], + "start": ["Generating... this might be good.", "Here we go!", "Starting the magic..."], + "complete": ["Done! Take a look.", "Finished. I hope you like it.","Another masterpiece is born."], + "error": ["Oops. Something went wrong.","Error... maybe clean the queue?", "Memory's full. Let's free some space."], + "waiting": ["Still working... patience, my friend.", "Good things take time.", "I'm still here. Just so you know."] + } +} diff --git a/diana_lib_packer/sprites/happy.png b/diana_lib_packer/sprites/happy.png new file mode 100644 index 0000000..3eb35af Binary files /dev/null and b/diana_lib_packer/sprites/happy.png differ diff --git a/diana_lib_packer/sprites/neutral.png b/diana_lib_packer/sprites/neutral.png new file mode 100644 index 0000000..8a89296 Binary files /dev/null and b/diana_lib_packer/sprites/neutral.png differ diff --git a/diana_lib_packer/sprites/speaking.png b/diana_lib_packer/sprites/speaking.png new file mode 100644 index 0000000..5404c5b Binary files /dev/null and b/diana_lib_packer/sprites/speaking.png differ diff --git a/diana_lib_packer/sprites/surprised.png b/diana_lib_packer/sprites/surprised.png new file mode 100644 index 0000000..f89e101 Binary files /dev/null and b/diana_lib_packer/sprites/surprised.png differ diff --git a/diana_lib_packer/sprites/waiting.png b/diana_lib_packer/sprites/waiting.png new file mode 100644 index 0000000..df5e19f Binary files /dev/null and b/diana_lib_packer/sprites/waiting.png differ diff --git a/libraries/diana.lib b/libraries/diana.lib new file mode 100644 index 0000000..30a2d6e Binary files /dev/null and b/libraries/diana.lib differ diff --git a/libraries/kerrigan.lib b/libraries/kerrigan.lib new file mode 100644 index 0000000..9c39f84 Binary files /dev/null and b/libraries/kerrigan.lib differ diff --git a/libraries/kitty.lib b/libraries/kitty.lib new file mode 100644 index 0000000..b29a3f6 Binary files /dev/null and b/libraries/kitty.lib differ diff --git a/web/js/diana.js b/web/js/diana.js new file mode 100644 index 0000000..085c847 --- /dev/null +++ b/web/js/diana.js @@ -0,0 +1,777 @@ +import { app } from "../../../scripts/app.js"; + +console.log("%c🧝 DIANA: v34.3 - IDLE TIMER", "color: #9b59b6; font-size: 16px; font-weight: bold"); + +let diana = { + container: null, + canvas: null, + ctx: null, + emotionSpan: null, + phraseSpan: null, + pinButton: null, + audioButton: null, + nameSpan: null, + header: null, + libNameSpan: null, + libPrevBtn: null, + libNextBtn: null, + + sprites: {}, + audio: {}, + phrases: {}, + emotions: [], + frameCount: 16, + libraryName: "Unknown", + libraryColor: "#9b59b6", + libraryBgColor: "#1a1a1a", + libraries: [], + currentLibIndex: 0, + + currentEmotion: "neutral", + currentFrame: 0, + speed: 1.0, + ready: false, + audioEnabled: true, + userActivated: false, + workflowActive: false, + + cyclesRemaining: 0, + nextEmotion: null, + phraseTimer: 0, + lastTimestamp: 0, + + animationSpeed: 10, + cycles: { + speaking: 1, + happy: 1, + surprised: 1, + greeting: 1, + drag: 1 + }, + + isPinned: false, + isDragging: false, + + eventCount: 0, + lastTaskId: null +}; + +const emotionIcons = { + 'neutral': '😐', + 'waiting': '⏳', + 'speaking': '🗣️', + 'happy': '😊', + 'surprised': '😲' +}; + +function log(level, module, msg, data = null) { + const timestamp = new Date().toISOString().substr(11, 8); + const prefix = `[${timestamp}] [${module}]`; + data ? console.log(`${prefix} ${msg}`, data) : console.log(`${prefix} ${msg}`); +} + +// ==================== AUDIO TOGGLE ==================== +function toggleAudio() { + if (!diana.userActivated) { + // Первый клик на кнопку (если ещё не активировано) + diana.userActivated = true; + diana.audioEnabled = true; + if (diana.audioButton) { + diana.audioButton.style.color = '#00ff00'; + diana.audioButton.innerHTML = '🔊'; + } + sayGreeting(); + startQueueMonitor(); + resetIdleTimer(); // ← ЗАПУСКАЕМ ТАЙМЕР + + // Удаляем слушатели активации + const events = ['click', 'keydown', 'touchstart', 'mousedown']; + events.forEach(e => document.removeEventListener(e, activateUser)); + + log('system', 'AUDIO', '🔊 Audio enabled (first activation)'); + } else { + // Уже активировано - просто переключаем звук + diana.audioEnabled = !diana.audioEnabled; + + if (diana.audioButton) { + if (diana.audioEnabled) { + diana.audioButton.style.color = '#00ff00'; + diana.audioButton.innerHTML = '🔊'; + log('system', 'AUDIO', '🔊 Audio enabled'); + } else { + diana.audioButton.style.color = '#ff0000'; + diana.audioButton.innerHTML = '🔇'; + log('system', 'AUDIO', '🔇 Audio disabled'); + } + } + resetIdleTimer(); // ← СБРАСЫВАЕМ ТАЙМЕР + } +} + +// ==================== IDLE TIMER WITH RANDOM OFFSET ==================== +let idleTimer = null; +const IDLE_BASE_TIME = 2 * 60 * 1000; // 10 минут в миллисекундах +const RANDOM_OFFSET_MIN = 1 * 60 * 1000; // 1 минута +const RANDOM_OFFSET_MAX = 3 * 60 * 1000; // 3 минуты + +function getRandomIdleTime() { + // Базовое время + случайный оффсет от 1 до 3 минут + const offset = Math.floor(Math.random() * (RANDOM_OFFSET_MAX - RANDOM_OFFSET_MIN + 1)) + RANDOM_OFFSET_MIN; + const totalTime = IDLE_BASE_TIME + offset; + console.log(`%c⏱️ Idle timer: ${IDLE_BASE_TIME/60000}min + ${offset/60000}min = ${totalTime/60000}min`, 'color: #ffaa00'); + return totalTime; +} + +function resetIdleTimer() { + // Очищаем существующий таймер + if (idleTimer) { + clearTimeout(idleTimer); + } + + // Запускаем новый только если пользователь активирован + if (!diana.userActivated) return; + + const idleTime = getRandomIdleTime(); + idleTimer = setTimeout(() => { + // Проверяем, что не идёт генерация + if (!diana.workflowActive) { + console.log('%c⏰ IDLE TIMER TRIGGERED', 'color: #ffaa00; font-weight: bold'); + + // Проигрываем waiting с анимацией speaking + setEmotion('speaking', diana.cycles.speaking || 1, 'neutral'); + playAudio('waiting'); + showRandomPhrase('waiting'); + + // Сбрасываем таймер после срабатывания + resetIdleTimer(); + } else { + // Если идёт генерация, пробуем позже + console.log('%c⏱️ Workflow active, rescheduling idle timer', 'color: #ffaa00'); + resetIdleTimer(); + } + }, idleTime); + + log('system', 'IDLE', `⏱️ Timer set for ${idleTime/60000} minutes`); +} + +// ==================== LIBRARY MANAGEMENT ==================== +async function fetchLibraries() { + try { + const response = await fetch('/diana/libraries'); + const data = await response.json(); + diana.libraries = data.libraries || []; + diana.currentLibIndex = 0; + if (diana.libraries.length > 0) { + await switchToIndex(0); + } + return diana.libraries; + } catch (e) { + log('error', 'LIB', `❌ Failed: ${e.message}`); + return []; + } +} + +async function switchToIndex(index) { + if (index < 0) index = diana.libraries.length - 1; + if (index >= diana.libraries.length) index = 0; + + diana.currentLibIndex = index; + const libName = diana.libraries[index]; + + try { + const response = await fetch('/diana/library/load', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ library: libName }) + }); + const data = await response.json(); + if (data.success) { + await loadCurrentLibrary(); + if (diana.libNameSpan) { + diana.libNameSpan.innerText = libName; + } + log('info', 'LIB', `✅ Switched to ${libName}`); + } + } catch (e) { + log('error', 'LIB', `❌ Switch failed: ${e.message}`); + } +} + +function prevLibrary() { + switchToIndex(diana.currentLibIndex - 1); +} + +function nextLibrary() { + switchToIndex(diana.currentLibIndex + 1); +} + +async function loadCurrentLibrary() { + try { + const response = await fetch('/diana/library/current'); + if (!response.ok) return false; + const data = await response.json(); + + diana.libraryName = data.name; + diana.libraryColor = data.color || '#9b59b6'; + diana.libraryBgColor = data.bgcolor || '#1a1a1a'; + diana.sprites = data.sprites; + diana.audio = data.audio; + diana.phrases = data.phrases; + diana.emotions = data.metadata.emotions; + diana.frameCount = data.metadata.frame_count; + + if (diana.nameSpan) diana.nameSpan.innerText = `🧝 ${diana.libraryName}`; + if (diana.container) diana.container.style.borderColor = diana.libraryColor; + if (diana.header) diana.header.style.backgroundColor = diana.libraryColor; + + log('info', 'LIB', `✅ Loaded: ${diana.libraryName}`); + return true; + } catch (e) { + log('error', 'LIB', `❌ Error: ${e.message}`); + return false; + } +} + +async function fetchConfig() { + try { + const response = await fetch('/diana/config'); + const data = await response.json(); + diana.animationSpeed = data.speed || 12; + diana.cycles = data.cycles || diana.cycles; + return true; + } catch (e) { + return false; + } +} + +// ==================== QUEUE MONITOR WITH OUTPUT DETECTOR ==================== +let lastQueueRunning = 0; +let lastQueuePending = 0; +let lastQueueState = 'idle'; // 'idle', 'running' +let lastProcessedTaskId = null; + +async function checkQueue() { + try { + const response = await fetch('/queue'); + const data = await response.json(); + + const running = data.queue_running?.length || 0; + const pending = data.queue_pending?.length || 0; + + // ===== СТАРТ ===== + if (running > 0 && lastQueueState === 'idle') { + console.log('%c🔥 START', 'color: #9b59b6'); + lastQueueState = 'running'; + lastProcessedTaskId = null; + handleEvent('start'); + lastQueueRunning = running; + lastQueuePending = pending; + return; + } + + // ===== ПРОВЕРКА РЕЗУЛЬТАТА ===== + if (running === 0 && pending === 0 && lastQueueState === 'running') { + console.log('%c⏳ Checking result...', 'color: #9b59b6'); + + // Ждем немного + await new Promise(resolve => setTimeout(resolve, 300)); + + try { + const historyResponse = await fetch('/history'); + const historyData = await historyResponse.json(); + + const tasks = Object.entries(historyData); + if (tasks.length > 0) { + // Берем последнюю задачу + const [taskId, taskData] = tasks[tasks.length - 1]; + + // Проверяем, не обрабатывали ли уже + if (taskId === lastProcessedTaskId) { + lastQueueState = 'idle'; + return; + } + + lastProcessedTaskId = taskId; + + // Проверяем наличие выходных изображений + const hasOutputs = taskData.outputs && + Object.keys(taskData.outputs).length > 0; + + // 🎯 ПРОСТАЯ ЛОГИКА: + // Если есть outputs - успех, иначе - ошибка + if (hasOutputs) { + console.log('%c✅ COMPLETE', 'color: #00ff00'); + handleEvent('complete'); + } else { + console.log('%c❌ ERROR', 'color: #ff0000'); + handleEvent('error'); + } + } + } catch (e) { + console.log('%c❌ History error', 'color: #ff0000'); + } + + // ВСЕГДА сбрасываем состояние + lastQueueState = 'idle'; + } + + lastQueueRunning = running; + lastQueuePending = pending; + + } catch (e) { + console.log('%c❌ Queue error', 'color: #ff0000'); + } +} +//---------------------------------end monitor------------------------------------------ + +function startQueueMonitor() { + setInterval(async () => { + await checkQueue(); + }, 500); + + log('system', 'QUEUE', '👀 Queue monitor started'); +} + +function activateUser() { + if (diana.userActivated) return; + + diana.userActivated = true; + + if (diana.audioButton) { + diana.audioButton.style.color = '#00ff00'; + diana.audioButton.innerHTML = '🔊'; + } + + sayGreeting(); + startQueueMonitor(); + resetIdleTimer(); // ← ЗАПУСКАЕМ ТАЙМЕР + + const events = ['click', 'keydown', 'touchstart', 'mousedown']; + events.forEach(e => document.removeEventListener(e, activateUser)); + + log('system', 'USER', '🌟 User activated by click'); +} + +function handleEvent(eventType) { + // Сбрасываем таймер при любом событии + if (diana.userActivated) { + resetIdleTimer(); + } + + if (!diana.sprites || Object.keys(diana.sprites).length === 0) return; + + let emotion, cycles, nextEmotion, phraseKey; + + switch(eventType) { + case 'start': + diana.workflowActive = true; + emotion = 'speaking'; + cycles = diana.cycles.speaking; + nextEmotion = 'waiting'; + phraseKey = 'start'; + log('event', 'HANDLER', ` → speaking (${cycles}) → waiting`); + break; + + case 'complete': + diana.workflowActive = false; + emotion = 'happy'; + cycles = diana.cycles.happy; + nextEmotion = 'neutral'; + phraseKey = 'complete'; + log('event', 'HANDLER', ` → happy (${cycles}) → neutral`); + break; + + case 'error': + diana.workflowActive = false; + emotion = 'surprised'; + cycles = diana.cycles.surprised; + nextEmotion = 'neutral'; + phraseKey = 'error'; + log('event', 'HANDLER', ` → surprised (${cycles}) → neutral`); + break; + + default: return; + } + + setEmotion(emotion, cycles, nextEmotion); + playAudio(eventType); + showRandomPhrase(phraseKey); +} + +function setEmotion(emotion, cycles, nextEmotion = null) { + if (!diana.sprites[emotion]) emotion = 'neutral'; + + diana.currentEmotion = emotion; + diana.currentFrame = 0; + diana.cyclesRemaining = cycles; + diana.nextEmotion = nextEmotion; + + if (diana.emotionSpan) { + const icon = emotionIcons[emotion] || '🧝'; + diana.emotionSpan.innerText = `${icon} ${emotion}`; + } +} + +function animate(timestamp) { + if (!diana.ctx) return requestAnimationFrame(animate); + + if (diana.lastTimestamp) { + const deltaTime = (timestamp - diana.lastTimestamp) / 1000; + if (diana.phraseTimer > 0) { + diana.phraseTimer -= deltaTime; + if (diana.phraseTimer <= 0 && diana.phraseSpan) { + diana.phraseSpan.innerText = "💭 ..."; + } + } + } + diana.lastTimestamp = timestamp; + + if (diana.sprites[diana.currentEmotion]) { + const frames = diana.sprites[diana.currentEmotion]; + if (frames?.length > 0) { + const frameDelay = 1000 / diana.animationSpeed / diana.speed; + + if (!diana.lastFrameTime || timestamp - diana.lastFrameTime > frameDelay) { + diana.currentFrame = (diana.currentFrame + 1) % frames.length; + diana.lastFrameTime = timestamp; + + if (diana.currentFrame === 0 && diana.cyclesRemaining > 0) { + diana.cyclesRemaining--; + if (diana.cyclesRemaining === 0 && diana.nextEmotion) { + let returnEmotion = diana.nextEmotion; + diana.nextEmotion = null; + setEmotion(returnEmotion, 0, null); + } + } + } + + const img = new Image(); + img.src = 'data:image/png;base64,' + frames[diana.currentFrame]; + img.onload = () => { + diana.ctx.clearRect(0, 0, 128, 128); + diana.ctx.drawImage(img, 0, 0, 128, 128); + }; + } + } else { + diana.ctx.clearRect(0, 0, 128, 128); + diana.ctx.fillStyle = "#333"; + diana.ctx.fillRect(0, 0, 128, 128); + diana.ctx.fillStyle = "#9b59b6"; + diana.ctx.font = "bold 20px Arial"; + diana.ctx.fillText("🧝", 64, 64); + } + + requestAnimationFrame(animate); +} + +async function playAudio(eventType) { + if (!diana.audioEnabled) return; + + const audioMap = { + 'start': 'start', 'complete': 'complete', 'error': 'error', + 'greeting': 'greeting', 'waiting': 'waiting' // ← ДОБАВЛЕНО waiting + }; + + const audioName = audioMap[eventType]; + if (!audioName || !diana.audio[audioName]) return; + + try { + const base64Data = diana.audio[audioName]; + const binaryString = atob(base64Data); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i); + + const blob = new Blob([bytes], { type: 'audio/mpeg' }); + const url = URL.createObjectURL(blob); + const audio = new Audio(url); + audio.volume = 0.7; + await audio.play(); + } catch (e) { + log('error', 'AUDIO', `❌ Playback failed: ${e.message}`); + } +} + +function showRandomPhrase(key) { + if (!diana.phrases[key]?.length) return; + const phrases = diana.phrases[key]; + const phrase = phrases[Math.floor(Math.random() * phrases.length)]; + if (diana.phraseSpan) { + diana.phraseSpan.innerText = `"${phrase}"`; + diana.phraseTimer = 15.0; + } +} + +function sayGreeting() { + if (diana.phraseSpan) { + if (diana.phrases?.greeting?.length) { + const phrases = diana.phrases.greeting; + diana.phraseSpan.innerText = `"${phrases[Math.floor(Math.random() * phrases.length)]}"`; + diana.phraseTimer = 15.0; + } else { + diana.phraseSpan.innerText = '"Diana here!"'; + diana.phraseTimer = 15.0; + } + } + playAudio('greeting'); + setEmotion('speaking', diana.cycles.greeting, 'neutral'); +} + +function handleDragStart() { + // Сбрасываем таймер при начале перетаскивания + if (diana.userActivated) { + resetIdleTimer(); + } + + if (!diana.userActivated || diana.isDragging) return; + + diana.isDragging = true; + let returnEmotion = diana.workflowActive ? 'waiting' : 'neutral'; + setEmotion('surprised', diana.cycles.drag, returnEmotion); +} + +function handleDragEnd() { + // Сбрасываем таймер при окончании перетаскивания + if (diana.userActivated) { + resetIdleTimer(); + } + + if (!diana.isDragging) return; + diana.isDragging = false; +} + +function updateStatusBadge(status) { + const badge = document.getElementById('diana-status-badge'); + if (!badge) return; + const colors = { 'connected': '#00ff00', 'disconnected': '#ffff00', 'error': '#ff0000' }; + badge.style.color = colors[status] || '#ffff00'; +} + +function togglePin() { + diana.isPinned = !diana.isPinned; + if (diana.pinButton) diana.pinButton.style.color = diana.isPinned ? '#ffaa00' : '#ffffff'; + if (diana.isPinned && diana.container) { + diana.container.style.top = "auto"; + diana.container.style.bottom = "20px"; + diana.container.style.left = "auto"; + diana.container.style.right = "20px"; + } + resetIdleTimer(); // ← СБРАСЫВАЕМ ТАЙМЕР +} + +async function createDianaWindow() { + if (document.getElementById('diana-container')) return; + + await fetchLibraries(); + await loadCurrentLibrary(); + await fetchConfig(); + + const container = document.createElement("div"); + container.id = "diana-container"; + container.style.cssText = ` + position: fixed; + top: 80px; + left: 50%; + transform: translateX(-50%); + width: 220px; + background: #1A1A1A; + border: 2px solid ${diana.libraryColor}; + border-radius: 10px; + box-shadow: 0 4px 20px rgba(0,0,0,0.5); + z-index: 999999; + overflow: hidden; + font-family: 'Segoe UI', sans-serif; + user-select: none; + cursor: default; + `; + + const header = document.createElement("div"); + header.style.cssText = ` + background: ${diana.libraryColor}; + color: white; + padding: 8px; + font-weight: bold; + display: flex; + justify-content: space-between; + cursor: ${diana.isPinned ? 'default' : 'move'}; + align-items: center; + `; + diana.header = header; + + const headerLeft = document.createElement("span"); + headerLeft.style.cssText = `display: flex; align-items: center; gap: 5px; cursor: ${diana.isPinned ? 'default' : 'move'};`; + headerLeft.innerHTML = `🧝 ${diana.libraryName}●`; + diana.nameSpan = headerLeft.querySelector('.diana-name'); + + const headerRight = document.createElement("span"); + headerRight.style.cssText = `display: flex; align-items: center; gap: 8px;`; + + const audioBtn = document.createElement("span"); + audioBtn.innerHTML = "🔊"; + audioBtn.style.cssText = `cursor: pointer; padding: 0 4px; font-size: 14px; color: #00ff00;`; + audioBtn.onclick = toggleAudio; + diana.audioButton = audioBtn; + + const pinBtn = document.createElement("span"); + pinBtn.innerHTML = "📌"; + pinBtn.style.cssText = `cursor: pointer; padding: 0 4px; font-size: 14px; color: #ffffff;`; + pinBtn.onclick = togglePin; + diana.pinButton = pinBtn; + + const closeBtn = document.createElement("span"); + closeBtn.innerHTML = "✕"; + closeBtn.style.cssText = `cursor: pointer; padding: 0 4px; font-size: 14px;`; + closeBtn.onclick = () => container.remove(); + + headerRight.appendChild(audioBtn); + headerRight.appendChild(pinBtn); + headerRight.appendChild(closeBtn); + header.appendChild(headerLeft); + header.appendChild(headerRight); + + const canvas = document.createElement("canvas"); + canvas.width = 128; + canvas.height = 128; + canvas.style.cssText = `width: 128px; height: 128px; display: block; margin: 10px auto; background: #1a1a1a; border-radius: 5px;`; + + const emotionDiv = document.createElement("div"); + emotionDiv.id = "diana-emotion"; + emotionDiv.style.cssText = `text-align: center; color: #9b59b6; padding: 5px; font-size: 12px; font-weight: bold; background: #1a1a1a; margin: 0 5px 5px 5px; border-radius: 3px;`; + emotionDiv.innerText = "😐 neutral"; + + const phraseDiv = document.createElement("div"); + phraseDiv.id = "diana-phrase"; + phraseDiv.style.cssText = `text-align: center; color: #aaa; padding: 8px; font-size: 11px; font-style: italic; border-top: 1px solid #333; min-height: 40px; background: #1a1a1a; word-break: break-word;`; + phraseDiv.innerText = "💭 ..."; + + const libSelector = document.createElement("div"); + libSelector.style.cssText = ` + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px; + border-top: 1px solid #333; + background: #1a1a1a; + `; + + const prevBtn = document.createElement("button"); + prevBtn.innerHTML = "◀"; + prevBtn.style.cssText = ` + background: ${diana.libraryColor}; + color: white; + border: none; + border-radius: 4px; + padding: 4px 8px; + cursor: pointer; + font-size: 14px; + font-weight: bold; + opacity: 0.8; + transition: opacity 0.2s; + `; + prevBtn.onmouseover = () => prevBtn.style.opacity = '1'; + prevBtn.onmouseout = () => prevBtn.style.opacity = '0.8'; + prevBtn.onclick = prevLibrary; + diana.libPrevBtn = prevBtn; + + const libNameSpan = document.createElement("span"); + libNameSpan.style.cssText = ` + color: white; + font-size: 12px; + font-weight: bold; + text-align: center; + flex: 1; + margin: 0 8px; + text-transform: uppercase; + `; + libNameSpan.innerText = diana.libraryName; + diana.libNameSpan = libNameSpan; + + const nextBtn = document.createElement("button"); + nextBtn.innerHTML = "▶"; + nextBtn.style.cssText = ` + background: ${diana.libraryColor}; + color: white; + border: none; + border-radius: 4px; + padding: 4px 8px; + cursor: pointer; + font-size: 14px; + font-weight: bold; + opacity: 0.8; + transition: opacity 0.2s; + `; + nextBtn.onmouseover = () => nextBtn.style.opacity = '1'; + nextBtn.onmouseout = () => nextBtn.style.opacity = '0.8'; + nextBtn.onclick = nextLibrary; + diana.libNextBtn = nextBtn; + + libSelector.appendChild(prevBtn); + libSelector.appendChild(libNameSpan); + libSelector.appendChild(nextBtn); + + container.appendChild(header); + container.appendChild(canvas); + container.appendChild(emotionDiv); + container.appendChild(phraseDiv); + container.appendChild(libSelector); + document.body.appendChild(container); + + diana.container = container; + diana.canvas = canvas; + diana.ctx = canvas.getContext("2d"); + diana.emotionSpan = emotionDiv; + diana.phraseSpan = phraseDiv; + + let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; + header.onmousedown = (e) => { + if (diana.isPinned) return; + handleDragStart(); + e.preventDefault(); + pos3 = e.clientX; pos4 = e.clientY; + + document.onmouseup = () => { + handleDragEnd(); + document.onmouseup = null; + document.onmousemove = null; + }; + + document.onmousemove = (e) => { + e.preventDefault(); + pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; + pos3 = e.clientX; pos4 = e.clientY; + + let newTop = container.offsetTop - pos2; + let newLeft = container.offsetLeft - pos1; + newTop = Math.max(0, Math.min(window.innerHeight - container.offsetHeight, newTop)); + newLeft = Math.max(0, Math.min(window.innerWidth - container.offsetWidth, newLeft)); + + container.style.top = newTop + "px"; + container.style.left = newLeft + "px"; + container.style.bottom = "auto"; + container.style.right = "auto"; + }; + }; + + const events = ['click', 'keydown', 'touchstart', 'mousedown']; + events.forEach(e => document.addEventListener(e, activateUser, { once: true })); + + requestAnimationFrame(animate); + log('system', 'MAIN', '✨ Diana ready'); +} + +setTimeout(createDianaWindow, 2000); +window.addEventListener('load', () => setTimeout(createDianaWindow, 1000)); + +app.registerExtension({ + name: "ComfyUI.Diana", + async setup() { + setTimeout(() => { + if (!app.graph) return; + app.graph._nodes.forEach(node => { + if (node.type === "Diana") { + node.color = "#9b59b6"; + node.bgcolor = "#1a1a1a"; + } + }); + }, 1000); + } +}); \ No newline at end of file