Add files via upload
This commit is contained in:
+179
@@ -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.
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user