Add files via upload

This commit is contained in:
Rimor
2026-03-14 02:38:37 +03:00
committed by GitHub
parent d7147b880c
commit 4ac267ce0f
18 changed files with 1276 additions and 0 deletions
+179
View File
@@ -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__ = []
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+50
View File
@@ -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
+254
View File
@@ -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()
+16
View File
@@ -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."]
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
+777
View File
@@ -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 = `<span class="diana-name">🧝 ${diana.libraryName}</span><span style="font-size: 10px;" id="diana-status-badge">●</span>`;
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);
}
});