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