Commit a1ebc651 authored by xuwx1's avatar xuwx1
Browse files

updata lightx2v

parent 5a4db490
Pipeline #3149 canceled with stages
import os
import queue
import socket
import subprocess
import threading
import time
import traceback
import numpy as np
import torch
from loguru import logger
def pseudo_random(a, b):
x = str(time.time()).split(".")[1]
y = int(float("0." + x) * 1000000)
return a + (y % (b - a + 1))
class VideoRecorder:
def __init__(
self,
livestream_url: str,
fps: float = 16.0,
):
self.livestream_url = livestream_url
self.fps = fps
self.video_port = pseudo_random(32000, 40000)
self.ffmpeg_log_level = os.getenv("FFMPEG_LOG_LEVEL", "error")
logger.info(f"VideoRecorder video port: {self.video_port}, ffmpeg_log_level: {self.ffmpeg_log_level}")
self.width = None
self.height = None
self.stoppable_t = None
self.realtime = True
# ffmpeg process for video data and push to livestream
self.ffmpeg_process = None
# TCP connection objects
self.video_socket = None
self.video_conn = None
self.video_thread = None
# queue for send data to ffmpeg process
self.video_queue = queue.Queue()
def init_sockets(self):
# TCP socket for send and recv video data
self.video_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.video_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.video_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
self.video_socket.bind(("127.0.0.1", self.video_port))
self.video_socket.listen(1)
def video_worker(self):
try:
logger.info("Waiting for ffmpeg to connect to video socket...")
self.video_conn, _ = self.video_socket.accept()
logger.info(f"Video connection established from {self.video_conn.getpeername()}")
fail_time, max_fail_time = 0, 10
packet_secs = 1.0 / self.fps
while True:
try:
if self.video_queue is None:
break
data = self.video_queue.get()
if data is None:
logger.info("Video thread received stop signal")
break
# Convert to numpy and scale to [0, 255], convert RGB to BGR for OpenCV/FFmpeg
for i in range(data.shape[0]):
t0 = time.time()
frame = (data[i] * 255).clamp(0, 255).to(torch.uint8).cpu().numpy()
try:
self.video_conn.send(frame.tobytes())
except (BrokenPipeError, OSError, ConnectionResetError) as e:
logger.info(f"Video connection closed, stopping worker: {type(e).__name__}")
return
if self.realtime:
time.sleep(max(0, packet_secs - (time.time() - t0)))
fail_time = 0
except (BrokenPipeError, OSError, ConnectionResetError):
logger.info("Video connection closed during queue processing")
break
except Exception:
logger.error(f"Send video data error: {traceback.format_exc()}")
fail_time += 1
if fail_time > max_fail_time:
logger.error(f"Video push worker thread failed {fail_time} times, stopping...")
break
except Exception:
logger.error(f"Video push worker thread error: {traceback.format_exc()}")
finally:
logger.info("Video push worker thread stopped")
def start_ffmpeg_process_local(self):
"""Start ffmpeg process that connects to our TCP sockets"""
ffmpeg_cmd = [
"ffmpeg",
"-fflags",
"nobuffer",
"-analyzeduration",
"0",
"-probesize",
"32",
"-flush_packets",
"1",
"-f",
"rawvideo",
"-pix_fmt",
"rgb24",
"-color_range",
"pc",
"-colorspace",
"rgb",
"-color_primaries",
"bt709",
"-color_trc",
"iec61966-2-1",
"-r",
str(self.fps),
"-s",
f"{self.width}x{self.height}",
"-i",
f"tcp://127.0.0.1:{self.video_port}",
"-b:v",
"4M",
"-c:v",
"libx264",
"-preset",
"ultrafast",
"-tune",
"zerolatency",
"-g",
f"{self.fps}",
"-pix_fmt",
"yuv420p",
"-f",
"mp4",
self.livestream_url,
"-y",
"-loglevel",
self.ffmpeg_log_level,
]
try:
self.ffmpeg_process = subprocess.Popen(ffmpeg_cmd)
logger.info(f"FFmpeg streaming started with PID: {self.ffmpeg_process.pid}")
logger.info(f"FFmpeg command: {' '.join(ffmpeg_cmd)}")
except Exception as e:
logger.error(f"Failed to start FFmpeg: {e}")
def start_ffmpeg_process_rtmp(self):
"""Start ffmpeg process that connects to our TCP sockets"""
ffmpeg_cmd = [
"ffmpeg",
"-f",
"rawvideo",
"-re",
"-pix_fmt",
"rgb24",
"-r",
str(self.fps),
"-s",
f"{self.width}x{self.height}",
"-i",
f"tcp://127.0.0.1:{self.video_port}",
"-b:v",
"2M",
"-c:v",
"libx264",
"-preset",
"ultrafast",
"-tune",
"zerolatency",
"-g",
f"{self.fps}",
"-pix_fmt",
"yuv420p",
"-f",
"flv",
self.livestream_url,
"-y",
"-loglevel",
self.ffmpeg_log_level,
]
try:
self.ffmpeg_process = subprocess.Popen(ffmpeg_cmd)
logger.info(f"FFmpeg streaming started with PID: {self.ffmpeg_process.pid}")
logger.info(f"FFmpeg command: {' '.join(ffmpeg_cmd)}")
except Exception as e:
logger.error(f"Failed to start FFmpeg: {e}")
def start_ffmpeg_process_whip(self):
"""Start ffmpeg process that connects to our TCP sockets"""
ffmpeg_cmd = [
"ffmpeg",
"-re",
"-fflags",
"nobuffer",
"-analyzeduration",
"0",
"-probesize",
"32",
"-flush_packets",
"1",
"-f",
"rawvideo",
"-re",
"-pix_fmt",
"rgb24",
"-r",
str(self.fps),
"-s",
f"{self.width}x{self.height}",
"-i",
f"tcp://127.0.0.1:{self.video_port}",
"-b:v",
"2M",
"-c:v",
"libx264",
"-preset",
"ultrafast",
"-tune",
"zerolatency",
"-g",
f"{self.fps}",
"-pix_fmt",
"yuv420p",
"-threads",
"1",
"-bf",
"0",
"-f",
"whip",
self.livestream_url,
"-y",
"-loglevel",
self.ffmpeg_log_level,
]
try:
self.ffmpeg_process = subprocess.Popen(ffmpeg_cmd)
logger.info(f"FFmpeg streaming started with PID: {self.ffmpeg_process.pid}")
logger.info(f"FFmpeg command: {' '.join(ffmpeg_cmd)}")
except Exception as e:
logger.error(f"Failed to start FFmpeg: {e}")
def start(self, width: int, height: int):
self.set_video_size(width, height)
duration = 1.0
self.pub_video(torch.zeros((int(self.fps * duration), height, width, 3), dtype=torch.float16))
time.sleep(duration)
def set_video_size(self, width: int, height: int):
if self.width is not None and self.height is not None:
assert self.width == width and self.height == height, "Video size already set"
return
self.width = width
self.height = height
self.init_sockets()
if self.livestream_url.startswith("rtmp://"):
self.start_ffmpeg_process_rtmp()
elif self.livestream_url.startswith("http"):
self.start_ffmpeg_process_whip()
else:
self.start_ffmpeg_process_local()
self.realtime = False
self.video_thread = threading.Thread(target=self.video_worker)
self.video_thread.start()
# Publish ComfyUI Image tensor to livestream
def pub_video(self, images: torch.Tensor):
N, height, width, C = images.shape
assert C == 3, "Input must be [N, H, W, C] with C=3"
logger.info(f"Publishing video [{N}x{width}x{height}]")
self.set_video_size(width, height)
self.video_queue.put(images)
logger.info(f"Published {N} frames")
self.stoppable_t = time.time() + N / self.fps + 3
def stop(self, wait=True):
if wait and self.stoppable_t:
t = self.stoppable_t - time.time()
if t > 0:
logger.warning(f"Waiting for {t} seconds to stop ...")
time.sleep(t)
self.stoppable_t = None
# Send stop signals to queues
if self.video_queue:
self.video_queue.put(None)
# Wait for threads to finish processing queued data (increased timeout)
queue_timeout = 30 # Increased from 5s to 30s to allow sufficient time for large video frames
if self.video_thread and self.video_thread.is_alive():
self.video_thread.join(timeout=queue_timeout)
if self.video_thread.is_alive():
logger.error(f"Video push thread did not stop after {queue_timeout}s")
# Shutdown connections to signal EOF to FFmpeg
# shutdown(SHUT_WR) will wait for send buffer to flush, no explicit sleep needed
if self.video_conn:
try:
self.video_conn.getpeername()
self.video_conn.shutdown(socket.SHUT_WR)
logger.info("Video connection shutdown initiated")
except OSError:
# Connection already closed, skip shutdown
pass
if self.ffmpeg_process:
is_local_file = not self.livestream_url.startswith(("rtmp://", "http"))
# Local MP4 files need time to write moov atom and finalize the container
timeout_seconds = 30 if is_local_file else 10
logger.info(f"Waiting for FFmpeg to finalize file (timeout={timeout_seconds}s, local_file={is_local_file})")
logger.info(f"FFmpeg output: {self.livestream_url}")
try:
returncode = self.ffmpeg_process.wait(timeout=timeout_seconds)
if returncode == 0:
logger.info(f"FFmpeg process exited successfully (exit code: {returncode})")
else:
logger.warning(f"FFmpeg process exited with non-zero code: {returncode}")
except subprocess.TimeoutExpired:
logger.warning(f"FFmpeg process did not exit within {timeout_seconds}s, sending SIGTERM...")
try:
self.ffmpeg_process.terminate() # SIGTERM
returncode = self.ffmpeg_process.wait(timeout=5)
logger.warning(f"FFmpeg process terminated with SIGTERM (exit code: {returncode})")
except subprocess.TimeoutExpired:
logger.error("FFmpeg process still running after SIGTERM, killing with SIGKILL...")
self.ffmpeg_process.kill()
self.ffmpeg_process.wait() # Wait for kill to complete
logger.error("FFmpeg process killed with SIGKILL")
finally:
self.ffmpeg_process = None
if self.video_conn:
try:
self.video_conn.close()
except Exception as e:
logger.debug(f"Error closing video connection: {e}")
finally:
self.video_conn = None
if self.video_socket:
try:
self.video_socket.close()
except Exception as e:
logger.debug(f"Error closing video socket: {e}")
finally:
self.video_socket = None
if self.video_queue:
while self.video_queue.qsize() > 0:
try:
self.video_queue.get_nowait()
except: # noqa
break
self.video_queue = None
logger.info("VideoRecorder stopped and resources cleaned up")
def __del__(self):
self.stop(wait=False)
def create_simple_video(frames=10, height=480, width=640):
video_data = []
for i in range(frames):
frame = np.zeros((height, width, 3), dtype=np.float32)
stripe_height = height // 8
colors = [
[1.0, 0.0, 0.0], # 红色
[0.0, 1.0, 0.0], # 绿色
[0.0, 0.0, 1.0], # 蓝色
[1.0, 1.0, 0.0], # 黄色
[1.0, 0.0, 1.0], # 洋红
[0.0, 1.0, 1.0], # 青色
[1.0, 1.0, 1.0], # 白色
[0.5, 0.5, 0.5], # 灰色
]
for j, color in enumerate(colors):
start_y = j * stripe_height
end_y = min((j + 1) * stripe_height, height)
frame[start_y:end_y, :] = color
offset = int((i / frames) * width)
frame = np.roll(frame, offset, axis=1)
frame = torch.tensor(frame, dtype=torch.float32)
video_data.append(frame)
return torch.stack(video_data, dim=0)
if __name__ == "__main__":
fps = 16
width = 640
height = 480
recorder = VideoRecorder(
# livestream_url="rtmp://localhost/live/test",
# livestream_url="https://reverse.st-oc-01.chielo.org/10.5.64.49:8000/rtc/v1/whip/?app=live&stream=ll_test_video&eip=127.0.0.1:8000",
livestream_url="/path/to/output_video.mp4",
fps=fps,
)
secs = 10 # 10秒视频
interval = 1
for i in range(0, secs, interval):
logger.info(f"{i} / {secs} s")
num_frames = int(interval * fps)
images = create_simple_video(num_frames, height, width)
logger.info(f"images: {images.shape} {images.dtype} {images.min()} {images.max()}")
recorder.pub_video(images)
time.sleep(interval)
recorder.stop()
# -*- coding: utf-8 -*-
import asyncio
import base64
import json
import os
import sys
import aiohttp
from loguru import logger
class VolcEngineTTSClient:
"""
VolcEngine TTS客户端
参数范围说明:
- speech_rate: -50~100 (100代表2倍速, -50代表0.5倍速, 0为正常语速)
- loudness_rate: -50~100 (100代表2倍音量, -50代表0.5倍音量, 0为正常音量)
- emotion_scale: 1-5
"""
def __init__(self, voices_list_file=None):
self.url = "https://openspeech.bytedance.com/api/v3/tts/unidirectional"
self.appid = os.getenv("VOLCENGINE_TTS_APPID")
self.access_token = os.getenv("VOLCENGINE_TTS_ACCESS_TOKEN")
self.proxy = os.getenv("HTTPS_PROXY", None)
if self.proxy:
logger.info(f"volcengine tts use proxy: {self.proxy}")
if voices_list_file is not None:
with open(voices_list_file, "r", encoding="utf-8") as f:
self.voices_list = json.load(f)
else:
self.voices_list = None
def get_voice_list(self):
return self.voices_list
async def tts_http_stream(self, headers, params, audio_save_path):
"""执行TTS流式请求"""
try:
logger.info(f"volcengine tts params: {params}")
audio_data = bytearray()
total_audio_size = 0
async with aiohttp.ClientSession() as session:
async with session.post(self.url, json=params, headers=headers, proxy=self.proxy) as response:
response.raise_for_status()
async for chunk in response.content:
if not chunk:
continue
try:
data = json.loads(chunk.decode("utf-8").strip())
if data.get("code", 0) == 0 and "data" in data and data["data"]:
chunk_audio = base64.b64decode(data["data"])
audio_size = len(chunk_audio)
total_audio_size += audio_size
audio_data.extend(chunk_audio)
continue
if data.get("code", 0) == 0 and "sentence" in data and data["sentence"]:
continue
if data.get("code", 0) == 20000000:
break
if data.get("code", 0) > 0:
logger.warning(f"volcengine tts error response: {data}")
break
except Exception as e:
logger.warning(f"Failed to parse volcengine tts chunk: {e}")
# save audio file
if audio_data:
with open(audio_save_path, "wb") as f:
f.write(audio_data)
logger.info(f"audio saved to {audio_save_path}, audio size: {len(audio_data) / 1024:.2f} KB")
# set correct permissions
os.chmod(audio_save_path, 0o644)
return True
else:
logger.warning("No tts audio data received")
return False
except Exception as e:
logger.warning(f"VolcEngineTTSClient tts request failed: {e}")
return False
async def tts_request(
self,
text,
voice_type="zh_female_vv_uranus_bigtts",
context_texts="",
emotion="",
emotion_scale=4,
speech_rate=0,
loudness_rate=0,
pitch=0,
output="tts_output.mp3",
resource_id="seed-tts-2.0",
app_key="aGjiRDfUWi",
uid="123123",
format="mp3",
sample_rate=24000,
enable_timestamp=True,
):
"""
执行TTS请求
Args:
text: 要转换的文本
voice_type: 声音类型
emotion: 情感类型
emotion_scale: 情感强度 (1-5)
speech_rate: 语速调节 (-50~100, 100代表2倍速, -50代表0.5倍速, 0为正常语速)
loudness_rate: 音量调节 (-50~100, 100代表2倍音量, -50代表0.5倍音量, 0为正常音量)
pitch: 音调调节 (-12~12, 12代表高音调, -12代表低音调, 0为正常音调)
output: 输出文件路径
resource_id: 资源ID
app_key: 应用密钥
uid: 用户ID
format: 音频格式
sample_rate: 采样率
enable_timestamp: 是否启用时间戳
"""
# 验证参数范围
if not (-50 <= speech_rate <= 100):
logger.warning(f"speech_rate {speech_rate} 超出有效范围 [-50, 100],将使用默认值 0")
speech_rate = 0
if not (-50 <= loudness_rate <= 100):
logger.warning(f"loudness_rate {loudness_rate} 超出有效范围 [-50, 100],将使用默认值 0")
loudness_rate = 0
if not (1 <= emotion_scale <= 5):
logger.warning(f"emotion_scale {emotion_scale} 超出有效范围 [1, 5],将使用默认值 3")
emotion_scale = 3
if not (-12 <= pitch <= 12):
logger.warning(f"pitch {pitch} 超出有效范围 [-12, 12],将使用默认值 0")
pitch = 0
headers = {
"X-Api-App-Id": self.appid,
"X-Api-Access-Key": self.access_token,
"X-Api-Resource-Id": resource_id,
"X-Api-App-Key": app_key,
"Content-Type": "application/json",
"Connection": "keep-alive",
}
additions = json.dumps(
{"explicit_language": "zh", "disable_markdown_filter": True, "enable_timestamp": True, "context_texts": [context_texts] if context_texts else None, "post_process": {"pitch": pitch}}
)
payload = {
"user": {"uid": uid},
"req_params": {
"text": text,
"speaker": voice_type,
"audio_params": {
"format": format,
"sample_rate": sample_rate,
"enable_timestamp": enable_timestamp,
"emotion": emotion,
"emotion_scale": emotion_scale,
"speech_rate": speech_rate,
"loudness_rate": loudness_rate,
},
"additions": additions,
},
}
success = await self.tts_http_stream(headers=headers, params=payload, audio_save_path=output)
if success:
logger.info(f"VolcEngineTTSClient tts request for '{text}': success")
else:
logger.warning(f"VolcEngineTTSClient tts request for '{text}': failed")
return success
async def test(args):
"""
TTS测试函数
Args:
args: list, e.g. [text, voice_type, emotion, emotion_scale, speech_rate, loudness_rate, output, resource_id, app_key, uid, format, sample_rate, enable_timestamp]
Provide as many as needed, from left to right.
Parameter ranges:
- speech_rate: -50~100 (100代表2倍速, -50代表0.5倍速, 0为正常语速)
- loudness_rate: -50~100 (100代表2倍音量, -50代表0.5倍音量, 0为正常音量)
- emotion_scale: 1-5
- pitch: -12~12 (12代表高音调, -12代表低音调, 0为正常音调)
"""
client = VolcEngineTTSClient()
# 设置默认参数
params = {
"text": "",
"voice_type": "zh_female_vv_uranus_bigtts",
"context_texts": "",
"emotion": "",
"emotion_scale": 4,
"speech_rate": 0,
"loudness_rate": 0,
"pitch": 12,
"output": "tts_output.mp3",
"resource_id": "seed-tts-2.0",
"app_key": "aGjiRDfUWi",
"uid": "123123",
"format": "mp3",
"sample_rate": 24000,
"enable_timestamp": True,
}
keys = list(params.keys())
# 覆盖默认参数
for i, arg in enumerate(args):
# 类型转换
if keys[i] == "sample_rate":
params[keys[i]] = int(arg)
elif keys[i] == "enable_timestamp":
# 支持多种布尔输入
params[keys[i]] = str(arg).lower() in ("1", "true", "yes", "on")
else:
params[keys[i]] = arg
await client.tts_request(
params["text"],
params["voice_type"],
params["context_texts"],
params["emotion"],
params["emotion_scale"],
params["speech_rate"],
params["loudness_rate"],
params["pitch"],
params["output"],
params["resource_id"],
params["app_key"],
params["uid"],
params["format"],
params["sample_rate"],
params["enable_timestamp"],
)
if __name__ == "__main__":
asyncio.run(test(sys.argv[1:]))
import io
import json
import os
import torch
from PIL import Image
from lightx2v.deploy.common.utils import class_try_catch_async
class BaseDataManager:
def __init__(self):
self.template_images_dir = None
self.template_audios_dir = None
self.template_videos_dir = None
self.template_tasks_dir = None
self.podcast_temp_session_dir = None
self.podcast_output_dir = None
async def init(self):
pass
async def close(self):
pass
def fmt_path(self, base, filename, abs_path=None):
if abs_path:
return abs_path
else:
return os.path.join(base, filename)
def to_device(self, data, device):
if isinstance(data, dict):
return {key: self.to_device(value, device) for key, value in data.items()}
elif isinstance(data, list):
return [self.to_device(item, device) for item in data]
elif isinstance(data, torch.Tensor):
return data.to(device)
else:
return data
async def save_bytes(self, bytes_data, filename, abs_path=None):
raise NotImplementedError
async def load_bytes(self, filename, abs_path=None):
raise NotImplementedError
async def delete_bytes(self, filename, abs_path=None):
raise NotImplementedError
async def presign_url(self, filename, abs_path=None):
return None
async def recurrent_save(self, data, prefix):
if isinstance(data, dict):
return {k: await self.recurrent_save(v, f"{prefix}-{k}") for k, v in data.items()}
elif isinstance(data, list):
return [await self.recurrent_save(v, f"{prefix}-{idx}") for idx, v in enumerate(data)]
elif isinstance(data, torch.Tensor):
save_path = prefix + ".pt"
await self.save_tensor(data, save_path)
return save_path
elif isinstance(data, Image.Image):
save_path = prefix + ".png"
await self.save_image(data, save_path)
return save_path
else:
return data
async def recurrent_load(self, data, device, prefix):
if isinstance(data, dict):
return {k: await self.recurrent_load(v, device, f"{prefix}-{k}") for k, v in data.items()}
elif isinstance(data, list):
return [await self.recurrent_load(v, device, f"{prefix}-{idx}") for idx, v in enumerate(data)]
elif isinstance(data, str) and data == prefix + ".pt":
return await self.load_tensor(data, device)
elif isinstance(data, str) and data == prefix + ".png":
return await self.load_image(data)
else:
return data
async def recurrent_delete(self, data, prefix):
if isinstance(data, dict):
return {k: await self.recurrent_delete(v, f"{prefix}-{k}") for k, v in data.items()}
elif isinstance(data, list):
return [await self.recurrent_delete(v, f"{prefix}-{idx}") for idx, v in enumerate(data)]
elif isinstance(data, str) and data == prefix + ".pt":
await self.delete_bytes(data)
elif isinstance(data, str) and data == prefix + ".png":
await self.delete_bytes(data)
@class_try_catch_async
async def save_object(self, data, filename):
data = await self.recurrent_save(data, filename)
bytes_data = json.dumps(data, ensure_ascii=False).encode("utf-8")
await self.save_bytes(bytes_data, filename)
@class_try_catch_async
async def load_object(self, filename, device):
bytes_data = await self.load_bytes(filename)
data = json.loads(bytes_data.decode("utf-8"))
data = await self.recurrent_load(data, device, filename)
return data
@class_try_catch_async
async def delete_object(self, filename):
bytes_data = await self.load_bytes(filename)
data = json.loads(bytes_data.decode("utf-8"))
await self.recurrent_delete(data, filename)
await self.delete_bytes(filename)
@class_try_catch_async
async def save_tensor(self, data: torch.Tensor, filename):
buffer = io.BytesIO()
torch.save(data.to("cpu"), buffer)
await self.save_bytes(buffer.getvalue(), filename)
@class_try_catch_async
async def load_tensor(self, filename, device):
bytes_data = await self.load_bytes(filename)
buffer = io.BytesIO(bytes_data)
t = torch.load(io.BytesIO(bytes_data))
t = t.to(device)
return t
@class_try_catch_async
async def save_image(self, data: Image.Image, filename):
buffer = io.BytesIO()
data.save(buffer, format="PNG")
await self.save_bytes(buffer.getvalue(), filename)
@class_try_catch_async
async def load_image(self, filename):
bytes_data = await self.load_bytes(filename)
buffer = io.BytesIO(bytes_data)
img = Image.open(buffer).convert("RGB")
return img
def get_delete_func(self, type):
maps = {
"TENSOR": self.delete_bytes,
"IMAGE": self.delete_bytes,
"OBJECT": self.delete_object,
"VIDEO": self.delete_bytes,
}
return maps[type]
def get_template_dir(self, template_type):
if template_type == "audios":
return self.template_audios_dir
elif template_type == "images":
return self.template_images_dir
elif template_type == "videos":
return self.template_videos_dir
elif template_type == "tasks":
return self.template_tasks_dir
else:
raise ValueError(f"Invalid template type: {template_type}")
@class_try_catch_async
async def list_template_files(self, template_type):
template_dir = self.get_template_dir(template_type)
if template_dir is None:
return []
return await self.list_files(base_dir=template_dir)
@class_try_catch_async
async def load_template_file(self, template_type, filename):
template_dir = self.get_template_dir(template_type)
if template_dir is None:
return None
return await self.load_bytes(None, abs_path=os.path.join(template_dir, filename))
@class_try_catch_async
async def template_file_exists(self, template_type, filename):
template_dir = self.get_template_dir(template_type)
if template_dir is None:
return None
return await self.file_exists(None, abs_path=os.path.join(template_dir, filename))
@class_try_catch_async
async def delete_template_file(self, template_type, filename):
template_dir = self.get_template_dir(template_type)
if template_dir is None:
return None
return await self.delete_bytes(None, abs_path=os.path.join(template_dir, filename))
@class_try_catch_async
async def save_template_file(self, template_type, filename, bytes_data):
template_dir = self.get_template_dir(template_type)
if template_dir is None:
return None
abs_path = os.path.join(template_dir, filename)
return await self.save_bytes(bytes_data, None, abs_path=abs_path)
@class_try_catch_async
async def presign_template_url(self, template_type, filename):
template_dir = self.get_template_dir(template_type)
if template_dir is None:
return None
return await self.presign_url(None, abs_path=os.path.join(template_dir, filename))
@class_try_catch_async
async def list_podcast_temp_session_files(self, session_id):
session_dir = os.path.join(self.podcast_temp_session_dir, session_id)
return await self.list_files(base_dir=session_dir)
@class_try_catch_async
async def save_podcast_temp_session_file(self, session_id, filename, bytes_data):
fpath = os.path.join(self.podcast_temp_session_dir, session_id, filename)
await self.save_bytes(bytes_data, None, abs_path=fpath)
@class_try_catch_async
async def load_podcast_temp_session_file(self, session_id, filename):
fpath = os.path.join(self.podcast_temp_session_dir, session_id, filename)
return await self.load_bytes(None, abs_path=fpath)
@class_try_catch_async
async def delete_podcast_temp_session_file(self, session_id, filename):
fpath = os.path.join(self.podcast_temp_session_dir, session_id, filename)
return await self.delete_bytes(None, abs_path=fpath)
@class_try_catch_async
async def save_podcast_output_file(self, filename, bytes_data):
fpath = os.path.join(self.podcast_output_dir, filename)
await self.save_bytes(bytes_data, None, abs_path=fpath)
@class_try_catch_async
async def load_podcast_output_file(self, filename):
fpath = os.path.join(self.podcast_output_dir, filename)
return await self.load_bytes(None, abs_path=fpath)
@class_try_catch_async
async def delete_podcast_output_file(self, filename):
fpath = os.path.join(self.podcast_output_dir, filename)
return await self.delete_bytes(None, abs_path=fpath)
@class_try_catch_async
async def presign_podcast_output_url(self, filename):
fpath = os.path.join(self.podcast_output_dir, filename)
return await self.presign_url(None, abs_path=fpath)
# Import data manager implementations
from .local_data_manager import LocalDataManager # noqa
from .s3_data_manager import S3DataManager # noqa
__all__ = ["BaseDataManager", "LocalDataManager", "S3DataManager"]
import asyncio
import os
import shutil
from loguru import logger
from lightx2v.deploy.common.utils import class_try_catch_async
from lightx2v.deploy.data_manager import BaseDataManager
class LocalDataManager(BaseDataManager):
def __init__(self, local_dir, template_dir):
super().__init__()
self.local_dir = local_dir
self.name = "local"
if not os.path.exists(self.local_dir):
os.makedirs(self.local_dir)
if template_dir:
self.template_images_dir = os.path.join(template_dir, "images")
self.template_audios_dir = os.path.join(template_dir, "audios")
self.template_videos_dir = os.path.join(template_dir, "videos")
self.template_tasks_dir = os.path.join(template_dir, "tasks")
assert os.path.exists(self.template_images_dir), f"{self.template_images_dir} not exists!"
assert os.path.exists(self.template_audios_dir), f"{self.template_audios_dir} not exists!"
assert os.path.exists(self.template_videos_dir), f"{self.template_videos_dir} not exists!"
assert os.path.exists(self.template_tasks_dir), f"{self.template_tasks_dir} not exists!"
# podcast temp session dir and output dir
self.podcast_temp_session_dir = os.path.join(self.local_dir, "podcast_temp_session")
self.podcast_output_dir = os.path.join(self.local_dir, "podcast_output")
os.makedirs(self.podcast_temp_session_dir, exist_ok=True)
os.makedirs(self.podcast_output_dir, exist_ok=True)
@class_try_catch_async
async def save_bytes(self, bytes_data, filename, abs_path=None):
out_path = self.fmt_path(self.local_dir, filename, abs_path)
with open(out_path, "wb") as fout:
fout.write(bytes_data)
return True
@class_try_catch_async
async def load_bytes(self, filename, abs_path=None):
inp_path = self.fmt_path(self.local_dir, filename, abs_path)
with open(inp_path, "rb") as fin:
return fin.read()
@class_try_catch_async
async def delete_bytes(self, filename, abs_path=None):
inp_path = self.fmt_path(self.local_dir, filename, abs_path)
os.remove(inp_path)
logger.info(f"deleted local file {filename}")
return True
@class_try_catch_async
async def file_exists(self, filename, abs_path=None):
filename = self.fmt_path(self.local_dir, filename, abs_path)
return os.path.exists(filename)
@class_try_catch_async
async def list_files(self, base_dir=None):
prefix = base_dir if base_dir else self.local_dir
return os.listdir(prefix)
@class_try_catch_async
async def create_podcast_temp_session_dir(self, session_id):
dir_path = os.path.join(self.podcast_temp_session_dir, session_id)
os.makedirs(dir_path, exist_ok=True)
return dir_path
@class_try_catch_async
async def clear_podcast_temp_session_dir(self, session_id):
session_dir = os.path.join(self.podcast_temp_session_dir, session_id)
if os.path.isdir(session_dir):
shutil.rmtree(session_dir)
logger.info(f"cleared podcast temp session dir {session_dir}")
return True
async def test():
import torch
from PIL import Image
m = LocalDataManager("/data/nvme1/liuliang1/lightx2v/local_data", None)
await m.init()
img = Image.open("/data/nvme1/liuliang1/lightx2v/assets/img_lightx2v.png")
tensor = torch.Tensor([233, 456, 789]).to(dtype=torch.bfloat16, device="cuda:0")
await m.save_image(img, "test_img.png")
print(await m.load_image("test_img.png"))
await m.save_tensor(tensor, "test_tensor.pt")
print(await m.load_tensor("test_tensor.pt", "cuda:0"))
await m.save_object(
{
"images": [img, img],
"tensor": tensor,
"list": [
[2, 0, 5, 5],
{
"1": "hello world",
"2": "world",
"3": img,
"t": tensor,
},
"0609",
],
},
"test_object.json",
)
print(await m.load_object("test_object.json", "cuda:0"))
await m.get_delete_func("OBJECT")("test_object.json")
await m.get_delete_func("TENSOR")("test_tensor.pt")
await m.get_delete_func("IMAGE")("test_img.png")
if __name__ == "__main__":
asyncio.run(test())
import asyncio
import hashlib
import json
import os
import aioboto3
import tos
from botocore.client import Config
from loguru import logger
from lightx2v.deploy.common.utils import class_try_catch_async
from lightx2v.deploy.data_manager import BaseDataManager
class S3DataManager(BaseDataManager):
def __init__(self, config_string, template_dir, max_retries=3):
super().__init__()
self.name = "s3"
self.config = json.loads(config_string)
self.max_retries = max_retries
self.bucket_name = self.config["bucket_name"]
self.aws_access_key_id = self.config["aws_access_key_id"]
self.aws_secret_access_key = self.config["aws_secret_access_key"]
self.endpoint_url = self.config["endpoint_url"]
self.base_path = self.config["base_path"]
self.connect_timeout = self.config.get("connect_timeout", 60)
self.read_timeout = self.config.get("read_timeout", 60)
self.write_timeout = self.config.get("write_timeout", 10)
self.addressing_style = self.config.get("addressing_style", None)
self.region = self.config.get("region", None)
self.cdn_url = self.config.get("cdn_url", "")
self.session = None
self.s3_client = None
self.presign_client = None
if template_dir:
self.template_images_dir = os.path.join(template_dir, "images")
self.template_audios_dir = os.path.join(template_dir, "audios")
self.template_videos_dir = os.path.join(template_dir, "videos")
self.template_tasks_dir = os.path.join(template_dir, "tasks")
# podcast temp session dir and output dir
self.podcast_temp_session_dir = os.path.join(self.base_path, "podcast_temp_session")
self.podcast_output_dir = os.path.join(self.base_path, "podcast_output")
async def init_presign_client(self):
# init tos client for volces.com
if "volces.com" in self.endpoint_url:
self.presign_client = tos.TosClientV2(
self.aws_access_key_id,
self.aws_secret_access_key,
self.endpoint_url.replace("tos-s3-", "tos-"),
self.region,
)
async def init(self):
for i in range(self.max_retries):
try:
logger.info(f"S3DataManager init with config: {self.config} (attempt {i + 1}/{self.max_retries}) ...")
s3_config = {"payload_signing_enabled": True}
if self.addressing_style:
s3_config["addressing_style"] = self.addressing_style
self.session = aioboto3.Session()
self.s3_client = await self.session.client(
"s3",
aws_access_key_id=self.aws_access_key_id,
aws_secret_access_key=self.aws_secret_access_key,
endpoint_url=self.endpoint_url,
config=Config(
signature_version="s3v4",
s3=s3_config,
connect_timeout=self.connect_timeout,
read_timeout=self.read_timeout,
parameter_validation=False,
max_pool_connections=50,
),
).__aenter__()
try:
await self.s3_client.head_bucket(Bucket=self.bucket_name)
logger.info(f"check bucket {self.bucket_name} success")
except Exception as e:
logger.info(f"check bucket {self.bucket_name} error: {e}, try to create it...")
await self.s3_client.create_bucket(Bucket=self.bucket_name)
await self.init_presign_client()
logger.info(f"Successfully init S3 bucket: {self.bucket_name} with timeouts - connect: {self.connect_timeout}s, read: {self.read_timeout}s, write: {self.write_timeout}s")
return
except Exception as e:
logger.warning(f"Failed to connect to S3: {e}")
await asyncio.sleep(1)
async def close(self):
if self.s3_client:
await self.s3_client.__aexit__(None, None, None)
if self.session:
self.session = None
@class_try_catch_async
async def save_bytes(self, bytes_data, filename, abs_path=None):
filename = self.fmt_path(self.base_path, filename, abs_path)
content_sha256 = hashlib.sha256(bytes_data).hexdigest()
await self.s3_client.put_object(
Bucket=self.bucket_name,
Key=filename,
Body=bytes_data,
ChecksumSHA256=content_sha256,
ContentType="application/octet-stream",
)
return True
@class_try_catch_async
async def load_bytes(self, filename, abs_path=None):
filename = self.fmt_path(self.base_path, filename, abs_path)
response = await self.s3_client.get_object(Bucket=self.bucket_name, Key=filename)
return await response["Body"].read()
@class_try_catch_async
async def delete_bytes(self, filename, abs_path=None):
filename = self.fmt_path(self.base_path, filename, abs_path)
await self.s3_client.delete_object(Bucket=self.bucket_name, Key=filename)
logger.info(f"deleted s3 file {filename}")
return True
@class_try_catch_async
async def file_exists(self, filename, abs_path=None):
filename = self.fmt_path(self.base_path, filename, abs_path)
try:
await self.s3_client.head_object(Bucket=self.bucket_name, Key=filename)
return True
except Exception:
return False
@class_try_catch_async
async def list_files(self, base_dir=None):
if base_dir:
prefix = self.fmt_path(self.base_path, None, abs_path=base_dir)
else:
prefix = self.base_path
prefix = prefix + "/" if not prefix.endswith("/") else prefix
# Handle pagination for S3 list_objects_v2
files = []
continuation_token = None
page = 1
while True:
list_kwargs = {"Bucket": self.bucket_name, "Prefix": prefix, "MaxKeys": 1000}
if continuation_token:
list_kwargs["ContinuationToken"] = continuation_token
response = await self.s3_client.list_objects_v2(**list_kwargs)
if "Contents" in response:
page_files = []
for obj in response["Contents"]:
# Remove the prefix from the key to get just the filename
key = obj["Key"]
if key.startswith(prefix):
filename = key[len(prefix) :]
if filename: # Skip empty filenames (the directory itself)
page_files.append(filename)
files.extend(page_files)
else:
logger.warning(f"[S3DataManager.list_files] Page {page}: No files found in this page.")
# Check if there are more pages
if response.get("IsTruncated", False):
continuation_token = response.get("NextContinuationToken")
page += 1
else:
break
return files
@class_try_catch_async
async def presign_url(self, filename, abs_path=None):
filename = self.fmt_path(self.base_path, filename, abs_path)
if self.cdn_url:
return f"{self.cdn_url}/{filename}"
if self.presign_client:
expires = self.config.get("presign_expires", 24 * 60 * 60)
out = await asyncio.to_thread(self.presign_client.pre_signed_url, tos.HttpMethodType.Http_Method_Get, self.bucket_name, filename, expires)
return out.signed_url
else:
return None
@class_try_catch_async
async def create_podcast_temp_session_dir(self, session_id):
pass
@class_try_catch_async
async def clear_podcast_temp_session_dir(self, session_id):
session_dir = os.path.join(self.podcast_temp_session_dir, session_id)
fs = await self.list_files(base_dir=session_dir)
logger.info(f"clear podcast temp session dir {session_dir} with files: {fs}")
for f in fs:
await self.delete_bytes(f, abs_path=os.path.join(session_dir, f))
async def test():
import torch
from PIL import Image
s3_config = {
"aws_access_key_id": "xxx",
"aws_secret_access_key": "xx",
"endpoint_url": "xxx",
"bucket_name": "xxx",
"base_path": "xxx",
"connect_timeout": 10,
"read_timeout": 10,
"write_timeout": 10,
}
m = S3DataManager(json.dumps(s3_config), None)
await m.init()
img = Image.open("../../../assets/img_lightx2v.png")
tensor = torch.Tensor([233, 456, 789]).to(dtype=torch.bfloat16, device="cuda:0")
await m.save_image(img, "test_img.png")
print(await m.load_image("test_img.png"))
await m.save_tensor(tensor, "test_tensor.pt")
print(await m.load_tensor("test_tensor.pt", "cuda:0"))
await m.save_object(
{
"images": [img, img],
"tensor": tensor,
"list": [
[2, 0, 5, 5],
{
"1": "hello world",
"2": "world",
"3": img,
"t": tensor,
},
"0609",
],
},
"test_object.json",
)
print(await m.load_object("test_object.json", "cuda:0"))
print("all files:", await m.list_files())
await m.get_delete_func("OBJECT")("test_object.json")
await m.get_delete_func("TENSOR")("test_tensor.pt")
await m.get_delete_func("IMAGE")("test_img.png")
print("after delete all files", await m.list_files())
await m.close()
if __name__ == "__main__":
asyncio.run(test())
class BaseQueueManager:
def __init__(self):
pass
async def init(self):
pass
async def close(self):
pass
async def put_subtask(self, subtask):
raise NotImplementedError
async def get_subtasks(self, queue, max_batch, timeout):
raise NotImplementedError
async def pending_num(self, queue):
raise NotImplementedError
# Import queue manager implementations
from .local_queue_manager import LocalQueueManager # noqa
from .rabbitmq_queue_manager import RabbitMQQueueManager # noqa
__all__ = ["BaseQueueManager", "LocalQueueManager", "RabbitMQQueueManager"]
import asyncio
import json
import os
import time
import traceback
from loguru import logger
from lightx2v.deploy.common.utils import class_try_catch_async
from lightx2v.deploy.queue_manager import BaseQueueManager
class LocalQueueManager(BaseQueueManager):
def __init__(self, local_dir):
self.local_dir = local_dir
if not os.path.exists(self.local_dir):
os.makedirs(self.local_dir)
async def get_conn(self):
pass
async def del_conn(self):
pass
async def declare_queue(self, queue):
pass
@class_try_catch_async
async def put_subtask(self, subtask):
out_name = self.get_filename(subtask["queue"])
keys = ["queue", "task_id", "worker_name", "inputs", "outputs", "params"]
msg = json.dumps({k: subtask[k] for k in keys}) + "\n"
logger.info(f"Local published subtask: ({subtask['task_id']}, {subtask['worker_name']}) to {subtask['queue']}")
with open(out_name, "a") as fout:
fout.write(msg)
return True
def read_first_line(self, queue):
out_name = self.get_filename(queue)
if not os.path.exists(out_name):
return None
lines = []
with open(out_name) as fin:
lines = fin.readlines()
if len(lines) <= 0:
return None
subtask = json.loads(lines[0])
msgs = "".join(lines[1:])
fout = open(out_name, "w")
fout.write(msgs)
fout.close()
return subtask
@class_try_catch_async
async def get_subtasks(self, queue, max_batch, timeout):
try:
t0 = time.time()
subtasks = []
while True:
subtask = self.read_first_line(queue)
if subtask:
subtasks.append(subtask)
if len(subtasks) >= max_batch:
return subtasks
else:
continue
else:
if len(subtasks) > 0:
return subtasks
if time.time() - t0 > timeout:
return None
await asyncio.sleep(1)
except asyncio.CancelledError:
logger.warning(f"local queue get_subtasks for {queue} cancelled")
return None
except: # noqa
logger.warning(f"local queue get_subtasks for {queue} failed: {traceback.format_exc()}")
return None
def get_filename(self, queue):
return os.path.join(self.local_dir, f"{queue}.jsonl")
@class_try_catch_async
async def pending_num(self, queue):
out_name = self.get_filename(queue)
if not os.path.exists(out_name):
return 0
lines = []
with open(out_name) as fin:
lines = fin.readlines()
return len(lines)
async def test():
q = LocalQueueManager("/data/nvme1/liuliang1/lightx2v/local_queue")
await q.init()
subtask = {
"task_id": "test-subtask-id",
"queue": "test_queue",
"worker_name": "test_worker",
"inputs": {},
"outputs": {},
"params": {},
}
await q.put_subtask(subtask)
await asyncio.sleep(5)
for i in range(2):
subtask = await q.get_subtasks("test_queue", 3, 5)
print("get subtask:", subtask)
if __name__ == "__main__":
asyncio.run(test())
import asyncio
import json
import traceback
import aio_pika
from loguru import logger
from lightx2v.deploy.common.utils import class_try_catch_async
from lightx2v.deploy.queue_manager import BaseQueueManager
class RabbitMQQueueManager(BaseQueueManager):
def __init__(self, conn_url, max_retries=3):
self.conn_url = conn_url
self.max_retries = max_retries
self.conn = None
self.chan = None
self.queues = set()
async def init(self):
await self.get_conn()
async def close(self):
await self.del_conn()
async def get_conn(self):
if self.chan and self.conn:
return
for i in range(self.max_retries):
try:
logger.info(f"Connect to RabbitMQ (attempt {i + 1}/{self.max_retries}..)")
self.conn = await aio_pika.connect_robust(self.conn_url)
self.chan = await self.conn.channel()
self.queues = set()
await self.chan.set_qos(prefetch_count=10)
logger.info("Successfully connected to RabbitMQ")
return
except Exception as e:
logger.warning(f"Failed to connect to RabbitMQ: {e}")
if i < self.max_retries - 1:
await asyncio.sleep(1)
else:
raise
async def declare_queue(self, queue):
if queue not in self.queues:
await self.get_conn()
await self.chan.declare_queue(queue, durable=True)
self.queues.add(queue)
return await self.chan.get_queue(queue)
@class_try_catch_async
async def put_subtask(self, subtask):
queue = subtask["queue"]
await self.declare_queue(queue)
keys = ["queue", "task_id", "worker_name", "inputs", "outputs", "params"]
msg = json.dumps({k: subtask[k] for k in keys}).encode("utf-8")
message = aio_pika.Message(body=msg, delivery_mode=aio_pika.DeliveryMode.PERSISTENT, content_type="application/json")
await self.chan.default_exchange.publish(message, routing_key=queue)
logger.info(f"Rabbitmq published subtask: ({subtask['task_id']}, {subtask['worker_name']}) to {queue}")
return True
async def get_subtasks(self, queue, max_batch, timeout):
try:
q = await self.declare_queue(queue)
subtasks = []
async with q.iterator() as qiter:
async for message in qiter:
await message.ack()
subtask = json.loads(message.body.decode("utf-8"))
subtasks.append(subtask)
if len(subtasks) >= max_batch:
return subtasks
while True:
message = await q.get(no_ack=False, fail=False)
if message:
await message.ack()
subtask = json.loads(message.body.decode("utf-8"))
subtasks.append(subtask)
if len(subtasks) >= max_batch:
return subtasks
else:
return subtasks
except asyncio.CancelledError:
logger.warning(f"rabbitmq get_subtasks for {queue} cancelled")
return None
except: # noqa
logger.warning(f"rabbitmq get_subtasks for {queue} failed: {traceback.format_exc()}")
return None
@class_try_catch_async
async def pending_num(self, queue):
q = await self.declare_queue(queue)
return q.declaration_result.message_count
async def del_conn(self):
if self.chan:
await self.chan.close()
if self.conn:
await self.conn.close()
async def test():
conn_url = "amqp://username:password@127.0.0.1:5672"
q = RabbitMQQueueManager(conn_url)
await q.init()
subtask = {
"task_id": "test-subtask-id",
"queue": "test_queue",
"worker_name": "test_worker",
"inputs": {},
"outputs": {},
"params": {},
}
await q.put_subtask(subtask)
await asyncio.sleep(5)
for i in range(2):
subtask = await q.get_subtasks("test_queue", 3, 5)
print("get subtask:", subtask)
await q.close()
if __name__ == "__main__":
asyncio.run(test())
import argparse
import asyncio
import base64
import copy
import json
import mimetypes
import os
import re
import tempfile
import traceback
import uuid
from contextlib import asynccontextmanager
import uvicorn
from fastapi import Depends, FastAPI, HTTPException, Request, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, Response
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from fastapi.staticfiles import StaticFiles
from loguru import logger
from pydantic import BaseModel
from lightx2v.deploy.common.audio_separator import AudioSeparator
from lightx2v.deploy.common.face_detector import FaceDetector
from lightx2v.deploy.common.pipeline import Pipeline
from lightx2v.deploy.common.podcasts import VolcEnginePodcastClient
from lightx2v.deploy.common.utils import check_params, data_name, fetch_resource, format_image_data, load_inputs
from lightx2v.deploy.common.volcengine_tts import VolcEngineTTSClient
from lightx2v.deploy.data_manager import LocalDataManager, S3DataManager
from lightx2v.deploy.queue_manager import LocalQueueManager, RabbitMQQueueManager
from lightx2v.deploy.server.auth import AuthManager
from lightx2v.deploy.server.metrics import MetricMonitor
from lightx2v.deploy.server.monitor import ServerMonitor, WorkerStatus
from lightx2v.deploy.server.redis_monitor import RedisServerMonitor
from lightx2v.deploy.task_manager import FinishedStatus, LocalTaskManager, PostgresSQLTaskManager, TaskStatus
from lightx2v.utils.service_utils import ProcessManager
# =========================
# Pydantic Models
# =========================
class TTSRequest(BaseModel):
text: str
voice_type: str
context_texts: str = ""
emotion: str = ""
emotion_scale: int = 3
speech_rate: int = 0
pitch: int = 0
loudness_rate: int = 0
resource_id: str = "seed-tts-1.0"
class RefreshTokenRequest(BaseModel):
refresh_token: str
# =========================
# FastAPI Related Code
# =========================
model_pipelines = None
task_manager = None
data_manager = None
queue_manager = None
server_monitor = None
auth_manager = None
metrics_monitor = MetricMonitor()
volcengine_tts_client = None
volcengine_podcast_client = None
face_detector = None
audio_separator = None
@asynccontextmanager
async def lifespan(app: FastAPI):
await task_manager.init()
await task_manager.mark_server_restart()
await data_manager.init()
await queue_manager.init()
await server_monitor.init()
asyncio.create_task(server_monitor.loop())
yield
await server_monitor.close()
await queue_manager.close()
await data_manager.close()
await task_manager.close()
app = FastAPI(lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
logger.error(f"HTTP Exception: {exc.status_code} - {exc.detail} for {request.url}")
return JSONResponse(status_code=exc.status_code, content={"message": exc.detail})
static_dir = os.path.join(os.path.dirname(__file__), "static")
app.mount("/static", StaticFiles(directory=static_dir), name="static")
# 添加assets目录的静态文件服务
assets_dir = os.path.join(os.path.dirname(__file__), "static", "assets")
app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
security = HTTPBearer()
async def verify_user_access(credentials: HTTPAuthorizationCredentials = Depends(security)):
token = credentials.credentials
payload = auth_manager.verify_jwt_token(token)
user_id = payload.get("user_id", None)
if not user_id:
raise HTTPException(status_code=401, detail="Invalid user")
user = await task_manager.query_user(user_id)
# logger.info(f"Verfiy user access: {payload}")
if user is None or user["user_id"] != user_id:
raise HTTPException(status_code=401, detail="Invalid user")
return user
async def verify_user_access_from_query(request: Request):
"""从查询参数中验证用户访问权限"""
# 首先尝试从 Authorization 头部获取 token
auth_header = request.headers.get("Authorization")
token = None
if auth_header and auth_header.startswith("Bearer "):
token = auth_header[7:] # 移除 "Bearer " 前缀
else:
# 如果没有 Authorization 头部,尝试从查询参数获取
token = request.query_params.get("token")
payload = auth_manager.verify_jwt_token(token)
user_id = payload.get("user_id", None)
if not user_id:
raise HTTPException(status_code=401, detail="Invalid user")
user = await task_manager.query_user(user_id)
if user is None or user["user_id"] != user_id:
raise HTTPException(status_code=401, detail="Invalid user")
return user
async def verify_worker_access(credentials: HTTPAuthorizationCredentials = Depends(security)):
token = credentials.credentials
if not auth_manager.verify_worker_token(token):
raise HTTPException(status_code=403, detail="Invalid worker token")
return True
def error_response(e, code):
return JSONResponse({"message": f"error: {e}!"}, status_code=code)
def format_user_response(user):
return {
"user_id": user.get("user_id"),
"id": user.get("id"),
"source": user.get("source"),
"username": user.get("username") or "",
"email": user.get("email") or "",
"homepage": user.get("homepage") or "",
"avatar_url": user.get("avatar_url") or "",
}
def guess_file_type(name, default_type):
content_type, _ = mimetypes.guess_type(name)
if content_type is None:
content_type = default_type
return content_type
@app.get("/", response_class=HTMLResponse)
async def root():
with open(os.path.join(static_dir, "index.html"), "r", encoding="utf-8") as f:
return HTMLResponse(content=f.read())
@app.get("/sitemap.xml", response_class=HTMLResponse)
async def sitemap():
with open(os.path.join(os.path.dirname(__file__), "frontend", "dist", "sitemap.xml"), "r", encoding="utf-8") as f:
return HTMLResponse(content=f.read())
@app.get("/auth/login/github")
async def github_auth(request: Request):
client_id = auth_manager.github_client_id
redirect_uri = f"{request.base_url}"
auth_url = f"https://github.com/login/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}"
return {"auth_url": auth_url}
@app.get("/auth/callback/github")
async def github_callback(request: Request):
try:
code = request.query_params.get("code")
if not code:
return error_response("Missing authorization code", 400)
user_info = await auth_manager.auth_github(code)
user_id = await task_manager.create_user(user_info)
user_info["user_id"] = user_id
user_response = format_user_response(user_info)
access_token, refresh_token = auth_manager.create_tokens(user_response)
logger.info(f"GitHub callback: user_info: {user_response}, access token issued")
return {"access_token": access_token, "refresh_token": refresh_token, "user_info": user_response}
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
@app.get("/auth/login/google")
async def google_auth(request: Request):
client_id = auth_manager.google_client_id
redirect_uri = auth_manager.google_redirect_uri
auth_url = f"https://accounts.google.com/o/oauth2/v2/auth?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&scope=openid%20email%20profile&access_type=offline"
logger.info(f"Google auth: auth_url: {auth_url}")
return {"auth_url": auth_url}
@app.get("/auth/callback/google")
async def google_callback(request: Request):
try:
code = request.query_params.get("code")
if not code:
return error_response("Missing authorization code", 400)
user_info = await auth_manager.auth_google(code)
user_id = await task_manager.create_user(user_info)
user_info["user_id"] = user_id
user_response = format_user_response(user_info)
access_token, refresh_token = auth_manager.create_tokens(user_response)
logger.info(f"Google callback: user_info: {user_response}, access token issued")
return {"access_token": access_token, "refresh_token": refresh_token, "user_info": user_response}
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
@app.get("/auth/login/sms")
async def sms_auth(request: Request):
try:
phone_number = request.query_params.get("phone_number")
if not phone_number:
return error_response("Missing phone number", 400)
ok = await auth_manager.send_sms(phone_number)
if not ok:
return error_response("SMS send failed", 400)
return {"msg": "SMS send successfully"}
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
@app.get("/auth/callback/sms")
async def sms_callback(request: Request):
try:
phone_number = request.query_params.get("phone_number")
verify_code = request.query_params.get("verify_code")
if not phone_number or not verify_code:
return error_response("Missing phone number or verify code", 400)
user_info = await auth_manager.check_sms(phone_number, verify_code)
if not user_info:
return error_response("SMS verify failed", 400)
user_id = await task_manager.create_user(user_info)
user_info["user_id"] = user_id
user_response = format_user_response(user_info)
access_token, refresh_token = auth_manager.create_tokens(user_response)
logger.info(f"SMS callback: user_info: {user_response}, access token issued")
return {"access_token": access_token, "refresh_token": refresh_token, "user_info": user_response}
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
@app.post("/auth/refresh")
async def refresh_access_token(request: RefreshTokenRequest):
try:
payload = auth_manager.verify_refresh_token(request.refresh_token)
user_id = payload.get("user_id")
if not user_id:
raise HTTPException(status_code=401, detail="Invalid refresh token")
user = await task_manager.query_user(user_id)
if user is None or user.get("user_id") != user_id:
raise HTTPException(status_code=401, detail="Invalid user")
user_info = format_user_response(user)
access_token, refresh_token = auth_manager.create_tokens(user_info)
return {"access_token": access_token, "refresh_token": refresh_token, "user_info": user_info}
except HTTPException as exc:
raise exc
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
async def prepare_subtasks(task_id):
# schedule next subtasks and pend, put to message queue
subtasks = await task_manager.next_subtasks(task_id)
for sub in subtasks:
logger.info(f"Prepare ready subtask: ({task_id}, {sub['worker_name']})")
r = await queue_manager.put_subtask(sub)
assert r, "put subtask to queue error"
await server_monitor.pending_subtasks_add(sub["queue"], sub["task_id"])
def format_task(task):
task["status"] = task["status"].name
task["model_cls"] = model_pipelines.outer_model_name(task["model_cls"])
@app.get("/api/v1/model/list")
async def api_v1_model_list(user=Depends(verify_user_access)):
try:
return {"models": model_pipelines.get_model_lists()}
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
@app.post("/api/v1/task/submit")
async def api_v1_task_submit(request: Request, user=Depends(verify_user_access)):
task_id = None
try:
msg = await server_monitor.check_user_busy(user["user_id"], active_new_task=True)
if msg is not True:
return error_response(msg, 400)
params = await request.json()
keys = [params.pop("task"), params.pop("model_cls"), params.pop("stage")]
keys[1] = model_pipelines.inner_model_name(keys[1])
assert len(params["prompt"]) > 0, "valid prompt is required"
# get worker infos, model input names
workers = model_pipelines.get_workers(keys)
inputs = model_pipelines.get_inputs(keys)
outputs = model_pipelines.get_outputs(keys)
types = model_pipelines.get_types(keys)
check_params(params, inputs, outputs, types)
# check if task can be published to queues
queues = [v["queue"] for v in workers.values()]
wait_time = await server_monitor.check_queue_busy(keys, queues)
if wait_time is None:
return error_response(f"Queue busy, please try again later", 500)
# process multimodal inputs data
inputs_data = await load_inputs(params, inputs, types)
# init task (we need task_id before preprocessing to save processed files)
task_id = await task_manager.create_task(keys, workers, params, inputs, outputs, user["user_id"])
logger.info(f"Submit task: {task_id} {params}")
# save multimodal inputs data
for inp, data in inputs_data.items():
await data_manager.save_bytes(data, data_name(inp, task_id))
await prepare_subtasks(task_id)
return {"task_id": task_id, "workers": workers, "params": params, "wait_time": wait_time}
except Exception as e:
traceback.print_exc()
if task_id:
await task_manager.finish_subtasks(task_id, TaskStatus.FAILED, fail_msg=f"submit failed: {e}")
return error_response(str(e), 500)
@app.get("/api/v1/task/query")
async def api_v1_task_query(request: Request, user=Depends(verify_user_access)):
try:
if "task_ids" in request.query_params:
task_ids = request.query_params["task_ids"].split(",")
tasks = []
for task_id in task_ids:
task_id = task_id.strip()
if task_id:
task, subtasks = await task_manager.query_task(task_id, user["user_id"], only_task=False)
if task is not None:
task["subtasks"] = await server_monitor.format_subtask(subtasks)
format_task(task)
tasks.append(task)
return {"tasks": tasks}
# 单个任务查询
task_id = request.query_params["task_id"]
task, subtasks = await task_manager.query_task(task_id, user["user_id"], only_task=False)
if task is None:
return error_response(f"Task {task_id} not found", 404)
task["subtasks"] = await server_monitor.format_subtask(subtasks)
format_task(task)
return task
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
@app.get("/api/v1/task/list")
async def api_v1_task_list(request: Request, user=Depends(verify_user_access)):
try:
user_id = user["user_id"]
page = int(request.query_params.get("page", 1))
page_size = int(request.query_params.get("page_size", 10))
assert page > 0 and page_size > 0, "page and page_size must be greater than 0"
status_filter = request.query_params.get("status", None)
query_params = {"user_id": user_id}
if status_filter and status_filter != "ALL":
query_params["status"] = TaskStatus[status_filter.upper()]
total_tasks = await task_manager.list_tasks(count=True, **query_params)
total_pages = (total_tasks + page_size - 1) // page_size
page_info = {"page": page, "page_size": page_size, "total": total_tasks, "total_pages": total_pages}
if page > total_pages:
return {"tasks": [], "pagination": page_info}
query_params["offset"] = (page - 1) * page_size
query_params["limit"] = page_size
tasks = await task_manager.list_tasks(**query_params)
for task in tasks:
format_task(task)
return {"tasks": tasks, "pagination": page_info}
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
@app.get("/api/v1/task/result_url")
async def api_v1_task_result_url(request: Request, user=Depends(verify_user_access)):
try:
name = request.query_params["name"]
task_id = request.query_params["task_id"]
task = await task_manager.query_task(task_id, user_id=user["user_id"])
assert task is not None, f"Task {task_id} not found"
assert task["status"] == TaskStatus.SUCCEED, f"Task {task_id} not succeed"
assert name in task["outputs"], f"Output {name} not found in task {task_id}"
assert name not in task["params"], f"Output {name} is a stream"
url = await data_manager.presign_url(task["outputs"][name])
if url is None:
url = f"./assets/task/result?task_id={task_id}&name={name}"
return {"url": url}
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
@app.get("/api/v1/task/input_url")
async def api_v1_task_input_url(request: Request, user=Depends(verify_user_access)):
try:
name = request.query_params["name"]
task_id = request.query_params["task_id"]
filename = request.query_params.get("filename", None)
task = await task_manager.query_task(task_id, user_id=user["user_id"])
assert task is not None, f"Task {task_id} not found"
assert name in task["inputs"], f"Input {name} not found in task {task_id}"
if name in task["params"]:
return error_response(f"Input {name} is a stream", 400)
# eg, multi person audio directory input
if filename is not None:
extra_inputs = task["params"]["extra_inputs"][name]
name = f"{name}/{filename}"
assert name in task["inputs"], f"Extra input {name} not found in task {task_id}"
assert name in extra_inputs, f"Filename {filename} not found in extra inputs"
url = await data_manager.presign_url(task["inputs"][name])
if url is None:
url = f"./assets/task/input?task_id={task_id}&name={name}"
if filename is not None:
url += f"&filename={filename}"
return {"url": url}
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
@app.get("/assets/task/result")
async def assets_task_result(request: Request, user=Depends(verify_user_access_from_query)):
try:
name = request.query_params["name"]
task_id = request.query_params["task_id"]
task = await task_manager.query_task(task_id, user_id=user["user_id"])
assert task is not None, f"Task {task_id} not found"
assert task["status"] == TaskStatus.SUCCEED, f"Task {task_id} not succeed"
assert name in task["outputs"], f"Output {name} not found in task {task_id}"
assert name not in task["params"], f"Output {name} is a stream"
data = await data_manager.load_bytes(task["outputs"][name])
# set correct Content-Type
content_type = guess_file_type(name, "application/octet-stream")
headers = {"Content-Disposition": f'attachment; filename="{name}"'}
headers["Cache-Control"] = "public, max-age=3600"
return Response(content=data, media_type=content_type, headers=headers)
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
@app.get("/assets/task/input")
async def assets_task_input(request: Request, user=Depends(verify_user_access_from_query)):
try:
name = request.query_params["name"]
task_id = request.query_params["task_id"]
filename = request.query_params.get("filename", None)
task = await task_manager.query_task(task_id, user_id=user["user_id"])
assert task is not None, f"Task {task_id} not found"
assert name in task["inputs"], f"Input {name} not found in task {task_id}"
if name in task["params"]:
return error_response(f"Input {name} is a stream", 400)
# eg, multi person audio directory input
if filename is not None:
extra_inputs = task["params"]["extra_inputs"][name]
name = f"{name}/{filename}"
assert name in task["inputs"], f"Extra input {name} not found in task {task_id}"
assert name in extra_inputs, f"Filename {filename} not found in extra inputs"
data = await data_manager.load_bytes(task["inputs"][name])
# set correct Content-Type
content_type = guess_file_type(name, "application/octet-stream")
headers = {"Content-Disposition": f'attachment; filename="{name}"'}
headers["Cache-Control"] = "public, max-age=3600"
return Response(content=data, media_type=content_type, headers=headers)
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
@app.get("/api/v1/task/cancel")
async def api_v1_task_cancel(request: Request, user=Depends(verify_user_access)):
try:
task_id = request.query_params["task_id"]
ret = await task_manager.cancel_task(task_id, user_id=user["user_id"])
logger.warning(f"Task {task_id} cancelled: {ret}")
if ret is True:
return {"msg": "Task cancelled successfully"}
else:
return error_response({"error": f"Task {task_id} cancel failed: {ret}"}, 400)
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
@app.get("/api/v1/task/resume")
async def api_v1_task_resume(request: Request, user=Depends(verify_user_access)):
try:
task_id = request.query_params["task_id"]
task = await task_manager.query_task(task_id, user_id=user["user_id"])
keys = [task["task_type"], task["model_cls"], task["stage"]]
if not model_pipelines.check_item_by_keys(keys):
return error_response(f"Model {keys} is not supported now, please submit a new task", 400)
ret = await task_manager.resume_task(task_id, user_id=user["user_id"], all_subtask=False)
if ret is True:
await prepare_subtasks(task_id)
return {"msg": "ok"}
else:
return error_response(f"Task {task_id} resume failed: {ret}", 400)
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
@app.delete("/api/v1/task/delete")
async def api_v1_task_delete(request: Request, user=Depends(verify_user_access)):
try:
task_id = request.query_params["task_id"]
task = await task_manager.query_task(task_id, user["user_id"], only_task=True)
if not task:
return error_response("Task not found", 404)
if task["status"] not in FinishedStatus:
return error_response("Only finished tasks can be deleted", 400)
# delete task record
success = await task_manager.delete_task(task_id, user["user_id"])
if success:
logger.info(f"Task {task_id} deleted by user {user['user_id']}")
return JSONResponse({"message": "Task deleted successfully"})
else:
return error_response("Failed to delete task", 400)
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
@app.post("/api/v1/worker/fetch")
async def api_v1_worker_fetch(request: Request, valid=Depends(verify_worker_access)):
try:
params = await request.json()
logger.info(f"Worker fetching: {params}")
keys = params.pop("worker_keys")
identity = params.pop("worker_identity")
max_batch = params.get("max_batch", 1)
timeout = params.get("timeout", 5)
# check client disconnected
async def check_client(request, fetch_task, identity, queue):
while True:
msg = await request.receive()
if msg["type"] == "http.disconnect":
logger.warning(f"Worker {identity} {queue} disconnected, req: {request.client}, {msg}")
fetch_task.cancel()
await server_monitor.worker_update(queue, identity, WorkerStatus.DISCONNECT)
return
await asyncio.sleep(1)
# get worker info
worker = model_pipelines.get_worker(keys)
await server_monitor.worker_update(worker["queue"], identity, WorkerStatus.FETCHING)
fetch_task = asyncio.create_task(queue_manager.get_subtasks(worker["queue"], max_batch, timeout))
check_task = asyncio.create_task(check_client(request, fetch_task, identity, worker["queue"]))
try:
subtasks = await asyncio.wait_for(fetch_task, timeout=timeout)
except asyncio.TimeoutError:
subtasks = []
fetch_task.cancel()
check_task.cancel()
subtasks = [] if subtasks is None else subtasks
for sub in subtasks:
await server_monitor.pending_subtasks_sub(sub["queue"], sub["task_id"])
valid_subtasks = await task_manager.run_subtasks(subtasks, identity)
valids = [sub["task_id"] for sub in valid_subtasks]
if len(valid_subtasks) > 0:
await server_monitor.worker_update(worker["queue"], identity, WorkerStatus.FETCHED)
logger.info(f"Worker {identity} {keys} {request.client} fetched {valids}")
else:
await server_monitor.worker_update(worker["queue"], identity, WorkerStatus.DISCONNECT)
return {"subtasks": valid_subtasks}
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
@app.post("/api/v1/worker/report")
async def api_v1_worker_report(request: Request, valid=Depends(verify_worker_access)):
try:
params = await request.json()
logger.info(f"{params}")
task_id = params.pop("task_id")
worker_name = params.pop("worker_name")
status = TaskStatus[params.pop("status")]
identity = params.pop("worker_identity")
queue = params.pop("queue")
fail_msg = params.pop("fail_msg", None)
await server_monitor.worker_update(queue, identity, WorkerStatus.REPORT)
ret = await task_manager.finish_subtasks(task_id, status, worker_identity=identity, worker_name=worker_name, fail_msg=fail_msg, should_running=True)
# not all subtasks finished, prepare new ready subtasks
if ret not in [TaskStatus.SUCCEED, TaskStatus.FAILED]:
await prepare_subtasks(task_id)
# all subtasks succeed, delete temp data
elif ret == TaskStatus.SUCCEED:
logger.info(f"Task {task_id} succeed")
task = await task_manager.query_task(task_id)
keys = [task["task_type"], task["model_cls"], task["stage"]]
temps = model_pipelines.get_temps(keys)
for temp in temps:
type = model_pipelines.get_type(temp)
name = data_name(temp, task_id)
await data_manager.get_delete_func(type)(name)
elif ret == TaskStatus.FAILED:
logger.warning(f"Task {task_id} failed")
return {"msg": "ok"}
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
@app.post("/api/v1/worker/ping/subtask")
async def api_v1_worker_ping_subtask(request: Request, valid=Depends(verify_worker_access)):
try:
params = await request.json()
logger.info(f"{params}")
task_id = params.pop("task_id")
worker_name = params.pop("worker_name")
identity = params.pop("worker_identity")
queue = params.pop("queue")
task = await task_manager.query_task(task_id)
if task is None or task["status"] != TaskStatus.RUNNING:
return {"msg": "delete"}
assert await task_manager.ping_subtask(task_id, worker_name, identity)
await server_monitor.worker_update(queue, identity, WorkerStatus.PING)
return {"msg": "ok"}
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
@app.get("/metrics")
async def api_v1_monitor_metrics():
try:
return Response(content=metrics_monitor.get_metrics(), media_type="text/plain")
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
@app.get("/api/v1/template/asset_url/{template_type}/{filename}")
async def api_v1_template_asset_url(template_type: str, filename: str):
"""get template asset URL - no authentication required"""
try:
url = await data_manager.presign_template_url(template_type, filename)
if url is None:
url = f"./assets/template/{template_type}/{filename}"
headers = {"Cache-Control": "public, max-age=3600"}
return Response(content=json.dumps({"url": url}), media_type="application/json", headers=headers)
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
# Template API endpoints
@app.get("/assets/template/{template_type}/{filename}")
async def assets_template(template_type: str, filename: str):
"""get template file - no authentication required"""
try:
if not await data_manager.template_file_exists(template_type, filename):
return error_response(f"template file {template_type} {filename} not found", 404)
data = await data_manager.load_template_file(template_type, filename)
# set media type according to file type
if template_type == "images":
if filename.lower().endswith(".png"):
media_type = "image/png"
elif filename.lower().endswith((".jpg", ".jpeg")):
media_type = "image/jpeg"
else:
media_type = "application/octet-stream"
elif template_type == "audios":
if filename.lower().endswith(".mp3"):
media_type = "audio/mpeg"
elif filename.lower().endswith(".wav"):
media_type = "audio/wav"
else:
media_type = "application/octet-stream"
elif template_type == "videos":
if filename.lower().endswith(".mp4"):
media_type = "video/mp4"
elif filename.lower().endswith(".webm"):
media_type = "video/webm"
elif filename.lower().endswith(".avi"):
media_type = "video/x-msvideo"
else:
media_type = "video/mp4" # default to mp4
else:
media_type = "application/octet-stream"
headers = {"Cache-Control": "public, max-age=3600"}
return Response(content=data, media_type=media_type, headers=headers)
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
@app.get("/api/v1/template/list")
async def api_v1_template_list(request: Request):
"""get template file list (support pagination) - no authentication required"""
try:
# check page params
page = int(request.query_params.get("page", 1))
page_size = int(request.query_params.get("page_size", 12))
if page < 1 or page_size < 1:
return error_response("page and page_size must be greater than 0", 400)
# limit page size
page_size = min(page_size, 100)
all_images = await data_manager.list_template_files("images")
all_audios = await data_manager.list_template_files("audios")
all_videos = await data_manager.list_template_files("videos")
all_images = [] if all_images is None else all_images
all_audios = [] if all_audios is None else all_audios
all_videos = [] if all_videos is None else all_videos
# 创建图片文件名(不含扩展名)到图片信息的映射
all_images_sorted = sorted(all_images)
image_map = {} # 文件名(不含扩展名) -> {"filename": 完整文件名, "url": URL}
for img_name in all_images_sorted:
img_name_without_ext = img_name.rsplit(".", 1)[0] if "." in img_name else img_name
url = await data_manager.presign_template_url("images", img_name)
if url is None:
url = f"./assets/template/images/{img_name}"
image_map[img_name_without_ext] = {"filename": img_name, "url": url}
# 创建音频文件名(不含扩展名)到音频信息的映射
all_audios_sorted = sorted(all_audios)
audio_map = {} # 文件名(不含扩展名) -> {"filename": 完整文件名, "url": URL}
for audio_name in all_audios_sorted:
audio_name_without_ext = audio_name.rsplit(".", 1)[0] if "." in audio_name else audio_name
url = await data_manager.presign_template_url("audios", audio_name)
if url is None:
url = f"./assets/template/audios/{audio_name}"
audio_map[audio_name_without_ext] = {"filename": audio_name, "url": url}
# 合并音频和图片模板,基于文件名前缀匹配
# 获取所有唯一的基础文件名(不含扩展名)
all_base_names = set(list(image_map.keys()) + list(audio_map.keys()))
all_base_names_sorted = sorted(all_base_names)
# 构建合并后的模板列表
merged_templates = []
for base_name in all_base_names_sorted:
template_item = {
"id": base_name, # 使用基础文件名作为ID
"image": image_map.get(base_name),
"audio": audio_map.get(base_name),
}
merged_templates.append(template_item)
# 分页处理
total = len(merged_templates)
total_pages = (total + page_size - 1) // page_size if total > 0 else 1
paginated_templates = []
if page <= total_pages:
start_idx = (page - 1) * page_size
end_idx = start_idx + page_size
paginated_templates = merged_templates[start_idx:end_idx]
# 为了保持向后兼容,仍然返回images和audios字段(但可能为空)
# 同时添加新的merged字段
return {
"templates": {
"images": [], # 保持向后兼容,但设为空
"audios": [], # 保持向后兼容,但设为空
"videos": [], # 保持向后兼容
"merged": paginated_templates, # 新的合并列表
},
"pagination": {"page": page, "page_size": page_size, "total": total, "total_pages": total_pages},
}
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
@app.get("/api/v1/template/tasks")
async def api_v1_template_tasks(request: Request):
"""get template task list (support pagination) - no authentication required"""
try:
# check page params
page = int(request.query_params.get("page", 1))
page_size = int(request.query_params.get("page_size", 12))
category = request.query_params.get("category", None)
search = request.query_params.get("search", None)
if page < 1 or page_size < 1:
return error_response("page and page_size must be greater than 0", 400)
# limit page size
page_size = min(page_size, 100)
all_templates = []
all_categories = set()
template_files = await data_manager.list_template_files("tasks")
template_files = [] if template_files is None else template_files
for template_file in template_files:
try:
bytes_data = await data_manager.load_template_file("tasks", template_file)
template_data = json.loads(bytes_data)
template_data["task"]["model_cls"] = model_pipelines.outer_model_name(template_data["task"]["model_cls"])
all_categories.update(template_data["task"]["tags"])
if category and category not in template_data["task"]["tags"]:
continue
if search and search not in template_data["task"]["params"]["prompt"] + template_data["task"]["params"]["negative_prompt"] + template_data["task"]["model_cls"] + template_data["task"][
"stage"
] + template_data["task"]["task_type"] + ",".join(template_data["task"]["tags"]):
continue
all_templates.append(template_data["task"])
except Exception as e:
logger.warning(f"Failed to load template file {template_file}: {e}")
# page info
total_templates = len(all_templates)
total_pages = (total_templates + page_size - 1) // page_size
paginated_templates = []
if page <= total_pages:
start_idx = (page - 1) * page_size
end_idx = start_idx + page_size
paginated_templates = all_templates[start_idx:end_idx]
return {"templates": paginated_templates, "pagination": {"page": page, "page_size": page_size, "total": total_templates, "total_pages": total_pages}, "categories": list(all_categories)}
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
@app.get("/api/v1/template/{template_id}")
async def api_v1_template_get(template_id: str, user=None):
try:
template_files = await data_manager.list_template_files("tasks")
template_files = [] if template_files is None else template_files
for template_file in template_files:
try:
bytes_data = await data_manager.load_template_file("tasks", template_file)
template_data = json.loads(bytes_data)
if template_data["task"]["task_id"] == template_id:
return template_data["task"]
except Exception as e:
logger.warning(f"Failed to load template file {template_file}: {e}")
continue
return error_response("Template not found", 404)
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
@app.post("/api/v1/share/create")
async def api_v1_share_create(request: Request, user=Depends(verify_user_access)):
try:
params = await request.json()
task_id = params["task_id"]
valid_days = params.get("valid_days", 7)
auth_type = params.get("auth_type", "public")
auth_value = params.get("auth_value", "")
share_type = params.get("share_type", "task")
assert auth_type == "public", "Only public share is supported now"
if share_type == "template":
template = await api_v1_template_get(task_id, user)
assert isinstance(template, dict) and template["task_id"] == task_id, f"Template {task_id} not found"
else:
task = await task_manager.query_task(task_id, user["user_id"], only_task=True)
assert task, f"Task {task_id} not found"
if auth_type == "user_id":
assert auth_value != "", "Target user is required for auth_type = user_id"
target_user = await task_manager.query_user(auth_value)
assert target_user and target_user["user_id"] == auth_value, f"Target user {auth_value} not found"
share_id = await task_manager.create_share(task_id, user["user_id"], share_type, valid_days, auth_type, auth_value)
return {"share_id": share_id, "share_url": f"/share/{share_id}"}
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
@app.get("/api/v1/share/{share_id}")
async def api_v1_share_get(share_id: str):
try:
share_data = await task_manager.query_share(share_id)
assert share_data, f"Share {share_id} not found or expired or deleted"
task_id = share_data["task_id"]
share_type = share_data["share_type"]
assert share_data["auth_type"] == "public", "Only public share is supported now"
if share_type == "template":
task = await api_v1_template_get(task_id, None)
assert isinstance(task, dict) and task["task_id"] == task_id, f"Template {task_id} not found"
else:
task = await task_manager.query_task(task_id, only_task=True)
assert task, f"Task {task_id} not found"
user_info = await task_manager.query_user(share_data["user_id"])
username = user_info.get("username", "用户") if user_info else "用户"
share_info = {
"task_id": task_id,
"share_type": share_type,
"user_id": share_data["user_id"],
"username": username,
"task_type": task["task_type"],
"model_cls": task["model_cls"],
"stage": task["stage"],
"prompt": task["params"].get("prompt", ""),
"negative_prompt": task["params"].get("negative_prompt", ""),
"inputs": task["inputs"],
"outputs": task["outputs"],
"create_t": task["create_t"],
"valid_days": share_data["valid_days"],
"valid_t": share_data["valid_t"],
"auth_type": share_data["auth_type"],
"auth_value": share_data["auth_value"],
"output_video_url": None,
"input_urls": {},
}
for input_name, input_filename in task["inputs"].items():
if share_type == "template":
template_type = "images" if "image" in input_name else "audios"
input_url = await data_manager.presign_template_url(template_type, input_filename)
else:
input_url = await data_manager.presign_url(input_filename)
share_info["input_urls"][input_name] = input_url
for output_name, output_filename in task["outputs"].items():
if share_type == "template":
assert "video" in output_name, "Only video output is supported for template share"
output_url = await data_manager.presign_template_url("videos", output_filename)
else:
output_url = await data_manager.presign_url(output_filename)
share_info["output_video_url"] = output_url
return share_info
except Exception as e:
traceback.print_exc()
return error_response(str(e), 500)
@app.get("/api/v1/voices/list")
async def api_v1_voices_list(request: Request):
try:
version = request.query_params.get("version", "all")
if volcengine_tts_client is None:
return error_response("Volcengine TTS client not loaded", 500)
voices = volcengine_tts_client.get_voice_list()
if voices is None:
return error_response("No voice list found", 404)
if version != "all":
voices = copy.deepcopy(voices)
voices["voices"] = [v for v in voices["voices"] if v["version"] == version]
return voices
except Exception as e:
traceback.print_exc()
return error_response("Failed to get voice list", 500)
@app.post("/api/v1/tts/generate")
async def api_v1_tts_generate(request: TTSRequest):
"""Generate TTS audio from text"""
try:
# Validate parameters
if not request.text.strip():
return JSONResponse({"error": "Text cannot be empty"}, status_code=400)
if not request.voice_type:
return JSONResponse({"error": "Voice type is required"}, status_code=400)
# Generate unique output filename
output_filename = f"tts_output_{uuid.uuid4().hex}.mp3"
output_path = os.path.join(tempfile.gettempdir(), output_filename)
# Generate TTS
success = await volcengine_tts_client.tts_request(
text=request.text,
voice_type=request.voice_type,
context_texts=request.context_texts,
emotion=request.emotion,
emotion_scale=request.emotion_scale,
speech_rate=request.speech_rate,
loudness_rate=request.loudness_rate,
pitch=request.pitch,
output=output_path,
resource_id=request.resource_id,
)
if success and os.path.exists(output_path):
# Return the audio file
return FileResponse(output_path, media_type="audio/mpeg", filename=output_filename)
else:
return JSONResponse({"error": "TTS generation failed"}, status_code=500)
except Exception as e:
logger.error(f"TTS generation error: {e}")
return JSONResponse({"error": f"TTS generation failed: {str(e)}"}, status_code=500)
@app.websocket("/api/v1/podcast/generate")
async def api_v1_podcast_generate_ws(websocket: WebSocket):
await websocket.accept()
def ws_get_user_id():
token = websocket.query_params.get("token")
if not token:
token = websocket.headers.get("authorization") or websocket.headers.get("Authorization")
if token and token.startswith("Bearer "):
token = token[7:]
payload = auth_manager.verify_jwt_token(token)
user_id = payload["user_id"]
return user_id
async def safe_send_json(payload):
try:
await websocket.send_json(payload)
except (WebSocketDisconnect, RuntimeError) as e:
logger.warning(f"WebSocket send skipped: {e}")
try:
user_id = ws_get_user_id()
data = await websocket.receive_text()
request_data = json.loads(data)
# stop request
if request_data.get("type") == "stop":
logger.info("Received stop signal from client")
await safe_send_json({"type": "stopped"})
return
# user input prompt
input_text = request_data.get("input", "")
is_url = input_text.startswith(("http://", "https://"))
if not input_text:
await safe_send_json({"error": "输入不能为空"})
return
session_id = "session_" + str(uuid.uuid4())
params = {
"session_id": session_id,
"data_manager": data_manager,
"text": "" if is_url else input_text,
"input_url": input_text if is_url else "",
"action": 0,
"use_head_music": False,
"use_tail_music": False,
"skip_round_audio_save": False,
}
logger.info(f"WebSocket generating podcast with params: {params}")
# 使用回调函数实时推送音频
async def on_round_complete(round_info):
await safe_send_json({"type": "audio_update", "data": round_info})
params["on_round_complete"] = on_round_complete
# 创建一个任务来处理停止信号
async def listen_for_stop(podcast_task):
while True:
try:
if podcast_task.done():
return
data = await asyncio.wait_for(websocket.receive_text(), timeout=0.1)
request = json.loads(data)
if request.get("type") == "stop":
logger.warning("Stop signal received during podcast generation")
podcast_task.cancel()
return
except asyncio.TimeoutError:
continue
except Exception as e:
logger.warning(f"Stop listener ended: {e}")
return
podcast_task = asyncio.create_task(volcengine_podcast_client.podcast_request(**params))
stop_listener_task = asyncio.create_task(listen_for_stop(podcast_task))
podcast_info = None
try:
podcast_info = await podcast_task
except asyncio.CancelledError:
logger.warning("Podcast generation cancelled by user")
await safe_send_json({"type": "stopped"})
return
finally:
stop_listener_task.cancel()
if podcast_info is None:
await safe_send_json({"error": "播客生成失败,请稍后重试"})
return
audio_path = podcast_info["audio_name"]
rounds = podcast_info["subtitles"]
await task_manager.create_podcast(session_id, user_id, input_text, audio_path, rounds)
audio_url = await data_manager.presign_podcast_output_url(audio_path)
if not audio_url:
audio_url = f"/api/v1/podcast/audio?session_id={session_id}&filename={audio_path}"
logger.info(f"completed podcast generation (session: {session_id})")
await safe_send_json(
{
"type": "complete",
"data": {
"audio_url": audio_url,
"subtitles": podcast_info["subtitles"],
"session_id": session_id,
"user_id": user_id,
},
}
)
except WebSocketDisconnect:
logger.info("WebSocket disconnected")
except Exception:
logger.error(f"Error in websocket: {traceback.format_exc()}")
await safe_send_json({"error": "websocket internal error, please try again later!"})
@app.get("/api/v1/podcast/audio")
async def api_v1_podcast_audio(request: Request, user=Depends(verify_user_access_from_query)):
try:
user_id = user["user_id"]
session_id = request.query_params.get("session_id")
filename = request.query_params.get("filename")
if not session_id or not filename:
return JSONResponse({"error": "session_id and filename are required"}, status_code=400)
ext = os.path.splitext(filename)[1].lower()
assert ext == ".mp3", f"Unsupported file extension: {ext}"
# 解析 Range 头,格式:bytes=start-end 或 bytes=start-
range_header = request.headers.get("Range")
start_byte, end_byte = None, None
if range_header:
match = re.match(r"bytes=(\d+)-(\d*)", range_header)
if match:
start_byte = int(match.group(1))
end_byte = int(match.group(2)) + 1 if match.group(2) else None
podcast_data = await task_manager.query_podcast(session_id, user_id)
if podcast_data:
# generate is finished and save info to database
func = data_manager.load_podcast_output_file
filename = podcast_data["audio_path"]
func_args = (filename,)
else:
func = data_manager.load_podcast_temp_session_file
func_args = (session_id, filename)
logger.debug(f"Serving audio file from {func.__name__} with args: {func_args}, start_byte: {start_byte}, end_byte: {end_byte}")
file_bytes = await func(*func_args)
file_size = len(file_bytes)
file_bytes = file_bytes[start_byte:end_byte]
content_length = len(file_bytes)
media_type = "audio/mpeg"
status_code = 200
headers = {"Content-Length": str(content_length), "Accept-Ranges": "bytes", "Content-Type": media_type, "Content-Disposition": f'attachment; filename="{filename}"'}
if start_byte is not None and start_byte > 0:
status_code = 206 # Partial Content
headers["Content-Range"] = f"bytes {start_byte}-{start_byte + content_length - 1}/{file_size}"
return Response(content=file_bytes, media_type=media_type, status_code=status_code, headers=headers)
except Exception as e:
logger.error(f"Error serving audio: {e}")
traceback.print_exc()
return JSONResponse({"error": str(e)}, status_code=500)
@app.get("/api/v1/podcast/history")
async def api_v1_podcast_history(request: Request, user=Depends(verify_user_access)):
try:
user_id = user["user_id"]
page = int(request.query_params.get("page", 1))
page_size = int(request.query_params.get("page_size", 10))
assert page > 0 and page_size > 0, "page and page_size must be greater than 0"
status = request.query_params.get("status", None) # has_audio, no_audio
query_params = {"user_id": user_id}
if status == "has_audio":
query_params["has_audio"] = True
elif status == "no_audio":
query_params["has_audio"] = False
total_tasks = await task_manager.list_podcasts(count=True, **query_params)
total_pages = (total_tasks + page_size - 1) // page_size
page_info = {"page": page, "page_size": page_size, "total": total_tasks, "total_pages": total_pages}
if page > total_pages:
return {"sessions": [], "pagination": page_info}
query_params["offset"] = (page - 1) * page_size
query_params["limit"] = page_size
sessions = await task_manager.list_podcasts(**query_params)
return {"sessions": sessions, "pagination": page_info}
except Exception as e:
logger.error(f"Error getting podcast history: {e}")
traceback.print_exc()
return {"sessions": []}
@app.get("/api/v1/podcast/session/{session_id}/audio_url")
async def api_v1_podcast_session_audio_url(session_id: str, user=Depends(verify_user_access)):
try:
user_id = user["user_id"]
podcast_data = await task_manager.query_podcast(session_id, user_id)
if not podcast_data:
return JSONResponse({"error": "Podcast session not found"}, status_code=404)
audio_path = podcast_data["audio_path"]
audio_url = await data_manager.presign_podcast_output_url(audio_path)
if not audio_url:
audio_url = f"/api/v1/podcast/audio?session_id={session_id}&filename={audio_path}"
return {"audio_url": audio_url}
except Exception as e:
logger.error(f"Error getting podcast session audio URL: {e}")
traceback.print_exc()
return JSONResponse({"error": str(e)}, status_code=500)
class FaceDetectRequest(BaseModel):
image: str # Base64 encoded image
class AudioSeparateRequest(BaseModel):
audio: str # Base64 encoded audio
num_speakers: int = None # Optional: number of speakers to separate
@app.post("/api/v1/face/detect")
async def api_v1_face_detect(request: FaceDetectRequest, user=Depends(verify_user_access)):
"""Detect faces in image (only detection, no cropping - cropping is done on frontend)
Supports both base64 encoded images and URLs (blob URLs, http URLs, etc.)
"""
try:
if not face_detector:
return error_response("Face detector not initialized", 500)
# 验证输入
if not request.image or not request.image.strip():
logger.error("Face detection request: image is empty")
return error_response("Image input is empty", 400)
image_bytes = None
try:
# Check if input is a URL (blob:, http:, https:, or data: URL)
if request.image.startswith(("http://", "https://")):
timeout = int(os.getenv("REQUEST_TIMEOUT", "10"))
image_bytes = await fetch_resource(request.image, timeout=timeout)
logger.debug(f"Fetched image from URL for face detection: {request.image[:100]}... (size: {len(image_bytes)} bytes)")
else:
encoded = request.image
# Data URL format: "data:image/png;base64,..."
if encoded.startswith("data:image"):
_, encoded = encoded.split(",", 1)
image_bytes = base64.b64decode(encoded)
logger.debug(f"Decoded base64 image: {request.image[:100]}... (size: {len(image_bytes)} bytes)")
# Validate image format before passing to face detector
image_bytes = await asyncio.to_thread(format_image_data, image_bytes)
except Exception as e:
logger.error(f"Failed to decode base64 image: {e}, image length: {len(request.image) if request.image else 0}")
return error_response(f"Invalid image format: {str(e)}", 400)
# Detect faces only (no cropping)
result = await asyncio.to_thread(face_detector.detect_faces, image_bytes, return_image=False)
faces_data = []
for i, face in enumerate(result["faces"]):
faces_data.append(
{
"index": i,
"bbox": face["bbox"], # [x1, y1, x2, y2] - absolute pixel coordinates in original image
"confidence": face["confidence"],
"class_id": face["class_id"],
"class_name": face["class_name"],
# Note: face_image is not included - frontend will crop it based on bbox
}
)
return {"faces": faces_data, "total": len(faces_data)}
except Exception as e:
logger.error(f"Face detection error: {traceback.format_exc()}")
return error_response(f"Face detection failed: {str(e)}", 500)
@app.post("/api/v1/audio/separate")
async def api_v1_audio_separate(request: AudioSeparateRequest, user=Depends(verify_user_access)):
"""Separate different speakers in audio"""
try:
if not audio_separator:
return error_response("Audio separator not initialized", 500)
audio_bytes = None
try:
encoded = request.audio
if encoded.startswith("data:"):
# Remove data URL prefix (e.g., "data:audio/mpeg;base64," or "data:application/octet-stream;base64,")
_, encoded = encoded.split(",", 1)
audio_bytes = await asyncio.to_thread(base64.b64decode, encoded, validate=True)
logger.debug(f"Successfully decoded base64 audio, size: {len(audio_bytes)} bytes")
except Exception as e:
logger.error(f"Failed to decode base64 audio {request.audio[:100]}..., error: {str(e)}")
return error_response(f"Invalid base64 audio data", 400)
# Separate speakers
result = await asyncio.to_thread(audio_separator.separate_speakers, audio_bytes, num_speakers=request.num_speakers)
# Convert audio tensors to base64 strings (without saving to file)
speakers_data = []
for speaker in result["speakers"]:
# Convert audio tensor directly to base64
audio_base64 = await asyncio.to_thread(audio_separator.speaker_audio_to_base64, speaker["audio"], speaker["sample_rate"], format="wav")
speakers_data.append(
{
"speaker_id": speaker["speaker_id"],
"audio": audio_base64, # Base64 encoded audio
"segments": speaker["segments"],
"sample_rate": speaker["sample_rate"],
}
)
return {"speakers": speakers_data, "total": len(speakers_data), "method": result.get("method", "pyannote")}
except Exception as e:
logger.error(f"Audio separation error: {traceback.format_exc()}")
return error_response(f"Audio separation failed: {str(e)}", 500)
# 所有未知路由 fallback 到 index.html (必须在所有API路由之后)
@app.get("/{full_path:path}", response_class=HTMLResponse)
async def vue_fallback(full_path: str):
index_path = os.path.join(static_dir, "index.html")
if os.path.exists(index_path):
return FileResponse(index_path)
return HTMLResponse("<h1>Frontend not found</h1>", status_code=404)
# =========================
# Main Entry
# =========================
if __name__ == "__main__":
ProcessManager.register_signal_handler()
parser = argparse.ArgumentParser()
cur_dir = os.path.dirname(os.path.abspath(__file__))
base_dir = os.path.abspath(os.path.join(cur_dir, "../../.."))
dft_pipeline_json = os.path.join(base_dir, "configs/model_pipeline.json")
dft_task_url = os.path.join(base_dir, "local_task")
dft_data_url = os.path.join(base_dir, "local_data")
dft_queue_url = os.path.join(base_dir, "local_queue")
dft_volcengine_tts_list_json = os.path.join(base_dir, "configs/volcengine_voices_list.json")
parser.add_argument("--pipeline_json", type=str, default=dft_pipeline_json)
parser.add_argument("--task_url", type=str, default=dft_task_url)
parser.add_argument("--data_url", type=str, default=dft_data_url)
parser.add_argument("--queue_url", type=str, default=dft_queue_url)
parser.add_argument("--redis_url", type=str, default="")
parser.add_argument("--template_dir", type=str, default="")
parser.add_argument("--volcengine_tts_list_json", type=str, default=dft_volcengine_tts_list_json)
parser.add_argument("--ip", type=str, default="0.0.0.0")
parser.add_argument("--port", type=int, default=8080)
parser.add_argument("--face_detector_model_path", type=str, default=None)
parser.add_argument("--audio_separator_model_path", type=str, default="")
args = parser.parse_args()
logger.info(f"args: {args}")
model_pipelines = Pipeline(args.pipeline_json)
volcengine_tts_client = VolcEngineTTSClient(args.volcengine_tts_list_json)
volcengine_podcast_client = VolcEnginePodcastClient()
face_detector = FaceDetector(model_path=args.face_detector_model_path)
audio_separator = AudioSeparator(model_path=args.audio_separator_model_path)
auth_manager = AuthManager()
if args.task_url.startswith("/"):
task_manager = LocalTaskManager(args.task_url, metrics_monitor)
elif args.task_url.startswith("postgresql://"):
task_manager = PostgresSQLTaskManager(args.task_url, metrics_monitor)
else:
raise NotImplementedError
if args.data_url.startswith("/"):
data_manager = LocalDataManager(args.data_url, args.template_dir)
elif args.data_url.startswith("{"):
data_manager = S3DataManager(args.data_url, args.template_dir)
else:
raise NotImplementedError
if args.queue_url.startswith("/"):
queue_manager = LocalQueueManager(args.queue_url)
elif args.queue_url.startswith("amqp://"):
queue_manager = RabbitMQQueueManager(args.queue_url)
else:
raise NotImplementedError
if args.redis_url:
server_monitor = RedisServerMonitor(model_pipelines, task_manager, queue_manager, args.redis_url)
else:
server_monitor = ServerMonitor(model_pipelines, task_manager, queue_manager)
uvicorn.run(app, host=args.ip, port=args.port, reload=False, workers=1)
import os
import time
import uuid
import aiohttp
import jwt
from fastapi import HTTPException
from loguru import logger
from lightx2v.deploy.common.aliyun import AlibabaCloudClient
class AuthManager:
def __init__(self):
# Worker access token
self.worker_secret_key = os.getenv("WORKER_SECRET_KEY", "worker-secret-key-change-in-production")
# GitHub OAuth
self.github_client_id = os.getenv("GITHUB_CLIENT_ID", "")
self.github_client_secret = os.getenv("GITHUB_CLIENT_SECRET", "")
# Google OAuth
self.google_client_id = os.getenv("GOOGLE_CLIENT_ID", "")
self.google_client_secret = os.getenv("GOOGLE_CLIENT_SECRET", "")
self.google_redirect_uri = os.getenv("GOOGLE_REDIRECT_URI", "")
self.jwt_algorithm = os.getenv("JWT_ALGORITHM", "HS256")
self.jwt_secret_key = os.getenv("JWT_SECRET_KEY", "your-secret-key-change-in-production")
self.jwt_expiration_hours = int(os.getenv("JWT_EXPIRATION_HOURS", "168"))
self.refresh_token_expiration_days = int(os.getenv("REFRESH_TOKEN_EXPIRATION_DAYS", "30"))
self.refresh_jwt_secret_key = os.getenv("REFRESH_JWT_SECRET_KEY", self.jwt_secret_key)
# Aliyun SMS
self.aliyun_client = AlibabaCloudClient()
logger.info(f"AuthManager: GITHUB_CLIENT_ID: {self.github_client_id}")
logger.info(f"AuthManager: GITHUB_CLIENT_SECRET: {self.github_client_secret}")
logger.info(f"AuthManager: GOOGLE_CLIENT_ID: {self.google_client_id}")
logger.info(f"AuthManager: GOOGLE_CLIENT_SECRET: {self.google_client_secret}")
logger.info(f"AuthManager: GOOGLE_REDIRECT_URI: {self.google_redirect_uri}")
logger.info(f"AuthManager: JWT_SECRET_KEY: {self.jwt_secret_key}")
logger.info(f"AuthManager: WORKER_SECRET_KEY: {self.worker_secret_key}")
def _create_token(self, data, expires_in_seconds, token_type, secret_key):
now = int(time.time())
payload = {
"user_id": data["user_id"],
"username": data["username"],
"email": data["email"],
"homepage": data["homepage"],
"token_type": token_type,
"iat": now,
"exp": now + expires_in_seconds,
"jti": str(uuid.uuid4()),
}
return jwt.encode(payload, secret_key, algorithm=self.jwt_algorithm)
def create_access_token(self, data):
return self._create_token(data, self.jwt_expiration_hours * 3600, "access", self.jwt_secret_key)
def create_refresh_token(self, data):
return self._create_token(data, self.refresh_token_expiration_days * 24 * 3600, "refresh", self.refresh_jwt_secret_key)
def create_tokens(self, data):
return self.create_access_token(data), self.create_refresh_token(data)
def create_jwt_token(self, data):
# Backwards compatibility for callers that still expect this name
return self.create_access_token(data)
async def auth_github(self, code):
try:
logger.info(f"GitHub OAuth code: {code}")
token_url = "https://github.com/login/oauth/access_token"
token_data = {"client_id": self.github_client_id, "client_secret": self.github_client_secret, "code": code}
headers = {"Accept": "application/json"}
proxy = os.getenv("auth_https_proxy", None)
if proxy:
logger.info(f"auth_github use proxy: {proxy}")
async with aiohttp.ClientSession() as session:
async with session.post(token_url, data=token_data, headers=headers, proxy=proxy) as response:
response.raise_for_status()
token_info = await response.json()
if "error" in token_info:
raise HTTPException(status_code=400, detail=f"GitHub OAuth error: {token_info['error']}")
access_token = token_info.get("access_token")
if not access_token:
raise HTTPException(status_code=400, detail="Failed to get access token")
user_url = "https://api.github.com/user"
user_headers = {"Authorization": f"token {access_token}", "Accept": "application/vnd.github.v3+json"}
async with aiohttp.ClientSession() as session:
async with session.get(user_url, headers=user_headers, proxy=proxy) as response:
response.raise_for_status()
user_info = await response.json()
return {
"source": "github",
"id": str(user_info["id"]),
"username": user_info["login"],
"email": user_info.get("email", ""),
"homepage": user_info.get("html_url", ""),
"avatar_url": user_info.get("avatar_url", ""),
}
except aiohttp.ClientError as e:
logger.error(f"GitHub API request failed: {e}")
raise HTTPException(status_code=500, detail="Failed to authenticate with GitHub")
except Exception as e:
logger.error(f"Authentication error: {e}")
raise HTTPException(status_code=500, detail="Authentication failed")
async def auth_google(self, code):
try:
logger.info(f"Google OAuth code: {code}")
token_url = "https://oauth2.googleapis.com/token"
token_data = {
"client_id": self.google_client_id,
"client_secret": self.google_client_secret,
"code": code,
"redirect_uri": self.google_redirect_uri,
"grant_type": "authorization_code",
}
headers = {"Content-Type": "application/x-www-form-urlencoded"}
proxy = os.getenv("auth_https_proxy", None)
if proxy:
logger.info(f"auth_google use proxy: {proxy}")
async with aiohttp.ClientSession() as session:
async with session.post(token_url, data=token_data, headers=headers, proxy=proxy) as response:
response.raise_for_status()
token_info = await response.json()
if "error" in token_info:
raise HTTPException(status_code=400, detail=f"Google OAuth error: {token_info['error']}")
access_token = token_info.get("access_token")
if not access_token:
raise HTTPException(status_code=400, detail="Failed to get access token")
# get user info
user_url = "https://www.googleapis.com/oauth2/v2/userinfo"
user_headers = {"Authorization": f"Bearer {access_token}"}
async with aiohttp.ClientSession() as session:
async with session.get(user_url, headers=user_headers, proxy=proxy) as response:
response.raise_for_status()
user_info = await response.json()
return {
"source": "google",
"id": str(user_info["id"]),
"username": user_info.get("name", user_info.get("email", "")),
"email": user_info.get("email", ""),
"homepage": user_info.get("link", ""),
"avatar_url": user_info.get("picture", ""),
}
except aiohttp.ClientError as e:
logger.error(f"Google API request failed: {e}")
raise HTTPException(status_code=500, detail="Failed to authenticate with Google")
except Exception as e:
logger.error(f"Google authentication error: {e}")
raise HTTPException(status_code=500, detail="Google authentication failed")
async def send_sms(self, phone_number):
return await self.aliyun_client.send_sms(phone_number)
async def check_sms(self, phone_number, verify_code):
ok = await self.aliyun_client.check_sms(phone_number, verify_code)
if not ok:
return None
return {
"source": "phone",
"id": phone_number,
"username": phone_number,
"email": "",
"homepage": "",
"avatar_url": "",
}
def _verify_token(self, token, expected_type, secret_key):
try:
payload = jwt.decode(token, secret_key, algorithms=[self.jwt_algorithm])
token_type = payload.get("token_type")
if token_type and token_type != expected_type:
raise HTTPException(status_code=401, detail="Token type mismatch")
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token has expired")
except Exception as e:
logger.error(f"verify_jwt_token error: {e}")
raise HTTPException(status_code=401, detail="Could not validate credentials")
def verify_jwt_token(self, token):
return self._verify_token(token, "access", self.jwt_secret_key)
def verify_refresh_token(self, token):
return self._verify_token(token, "refresh", self.refresh_jwt_secret_key)
def verify_worker_token(self, token):
return token == self.worker_secret_key
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>LightX2V — AI数字人视频生成平台</title>
<meta name="description" content="LightX2V:轻量、快速的AI数字人视频生成平台。在线体验、示例视频与使用说明。">
<meta name="keywords" content="AI数字人, 文生视频, 图生视频, LightX2V, Light AI, 视频生成, 文本生成视频, 数字人视频平台">
<meta name="robots" content="index,follow" />
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="theme-color" content="#0f1329">
<link rel="canonical" href="https://x2v.light-ai.top/" />
<link rel="alternate" hreflang="zh-CN" href="https://x2v.light-ai.top/" />
<link rel="alternate" hreflang="en" href="https://x2v.light-ai.top/" />
<link rel="alternate" hreflang="x-default" href="https://x2v.light-ai.top/" />
<!-- Open Graph for social platforms -->
<meta property="og:type" content="website">
<meta property="og:site_name" content="LightX2V">
<meta property="og:title" content="LightX2V — AI数字人视频生成平台">
<meta property="og:description" content="轻量、快速的AI数字人视频生成平台。在线体验、示例视频与使用说明。">
<meta property="og:url" content="https://x2v.light-ai.top/">
<meta property="og:image" content="/public/cover.png">
<!-- Twitter Card (for X/Twitter) -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="LightX2V — AI数字人视频生成平台">
<meta name="twitter:description" content="轻量、快速的AI数字人视频生成平台。由 Light AI 工具链驱动,支持文本/图像到视频的高效生成。">
<meta name="twitter:image" content="/public/cover.png">
<meta name="twitter:site" content="@LightX2V">
<!-- Favicon & App icons -->
<link rel="icon" href="/public/logo_black.png" sizes="32x32">
<link rel="icon" href="/public/logo_black.png" type="image/png">
<link rel="icon" href="/public/logo_black.png" type="image/x-icon">
<link rel="apple-touch-icon" sizes="180x180" href="/public/logo_black.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="preconnect" href="https://x2v.light-ai.top" crossorigin>
<link rel="preconnect" href="https://cdn.bootcdn.net" crossorigin>
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
<link rel="dns-prefetch" href="https://x2v.light-ai.top">
<link rel="dns-prefetch" href="https://cdn.bootcdn.net">
<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">
<link rel="preload" href="/src/style.css" as="style">
<link rel="preload" href="/src/main.js" as="script" type="module">
<link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/7.0.1/css/all.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css" rel="stylesheet" media="print" onload="this.media='all'">
<link rel='stylesheet' href='https://cdn-uicons.flaticon.com/3.0.0/uicons-solid-rounded/css/uicons-solid-rounded.css'>
<link rel='stylesheet' href='https://cdn-uicons.flaticon.com/3.0.0/uicons-bold-rounded/css/uicons-bold-rounded.css'>
<link rel='stylesheet' href='https://cdn-uicons.flaticon.com/3.0.0/uicons-bold-straight/css/uicons-bold-straight.css'>
<link rel='stylesheet' href='https://cdn-uicons.flaticon.com/3.0.0/uicons-solid-rounded/css/uicons-solid-rounded.css'>
<link rel='stylesheet' href='https://cdn-uicons.flaticon.com/3.0.0/uicons-regular-rounded/css/uicons-regular-rounded.css'>
<link rel='stylesheet' href='https://cdn-uicons.flaticon.com/3.0.0/uicons-thin-rounded/css/uicons-thin-rounded.css'>
<link rel='stylesheet' href='https://cdn-uicons.flaticon.com/3.0.0/uicons-solid-straight/css/uicons-solid-straight.css'>
<link rel='stylesheet' href='https://cdn-uicons.flaticon.com/3.0.0/uicons-solid-chubby/css/uicons-solid-chubby.css'>
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css">
<link href="/src/style.css" rel="stylesheet">
<style>
.seo-shell {
position: absolute !important;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
clip-path: inset(50%);
border: 0;
white-space: nowrap;
}
</style>
<script type="application/ld+json">
{
"@context":"https://schema.org",
"@type":"Organization",
"name":"Light AI",
"url":"https://www.light-ai.top/",
"logo":"https://x2v.light-ai.top/og-cover.jpg",
"sameAs":[
"https://github.com/ModelTC/LightX2V"
]
}
</script>
<script type="application/ld+json">
{
"@context":"https://schema.org",
"@type":"WebSite",
"name":"LightX2V",
"url":"https://x2v.light-ai.top/",
"publisher":{"@type":"Organization","name":"Light AI"},
"inLanguage":"zh-CN"
}
</script>
</head>
<body>
<main id="app">
<!-- 这是爬虫可读的首屏静态内容;JS 启动后可增强或替换 -->
<div class="seo-shell">
<header>
<h1>LightX2V</h1>
<p>免费、轻量、快速的AI数字人视频生成平台,由 Light AI 工具链提供端到端加速支持。</p>
<p>了解更多关于工具链与最新动态,请访问 <a href="https://www.light-ai.top/" rel="noopener" target="_blank">Light AI 官网</a><a href="https://github.com/ModelTC/LightX2V" rel="noopener" target="_blank">LightX2V GitHub</a></p>
</header>
<section>
<h2>功能亮点</h2>
<ul>
<li>电影级数字人视频生成</li>
<li>20倍生成提速</li>
<li>超低成本生成</li>
<li>精准口型对齐</li>
<li>分钟级视频时长</li>
<li>多场景应用</li>
<li>最新tts语音合成技术,支持多种语言,支持100+种音色,支持语音指令控制合成语音细节</li>
</ul>
</section>
<section>
<h2>快速开始</h2>
<ol>
<li>上传图片及音频,输入视频生成提示词,点击开始生成</li>
<li>生成并下载视频</li>
<li>应用模版,一键生成同款数字人视频</li>
</ol>
</section>
</div>
</main>
<script type="module" src="/src/main.js"></script>
</body>
</html>
{
"name": "frontend",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "frontend",
"version": "0.0.0",
"dependencies": {
"@flaticon/flaticon-uicons": "^3.3.1",
"@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.2.0",
"@tailwindcss/vite": "^4.1.13",
"pinia": "^3.0.3",
"tailwindcss": "^4.1.13",
"vue": "^3.5.21",
"vue-i18n": "^11.1.12",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"vite": "^7.1.7"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.4"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/types": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
"integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz",
"integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz",
"integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz",
"integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz",
"integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz",
"integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz",
"integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz",
"integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz",
"integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz",
"integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz",
"integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz",
"integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz",
"integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz",
"integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==",
"cpu": [
"mips64el"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz",
"integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz",
"integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz",
"integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz",
"integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz",
"integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz",
"integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz",
"integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz",
"integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz",
"integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz",
"integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz",
"integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz",
"integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz",
"integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@flaticon/flaticon-uicons": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@flaticon/flaticon-uicons/-/flaticon-uicons-3.3.1.tgz",
"integrity": "sha512-WN2zuECCdjuGBQrjzN0kpeSygzC5fgF8Q7pDR+FUuGtYWczSdIhIwoD+/fKBEfwqKfNIMZ1WouidevGQ4OJORg==",
"license": "SEE LICENSE IN LICENSE",
"optionalDependencies": {
"esbuild-linux-64": "^0.14.5"
}
},
"node_modules/@headlessui/vue": {
"version": "1.7.23",
"resolved": "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.23.tgz",
"integrity": "sha512-JzdCNqurrtuu0YW6QaDtR2PIYCKPUWq28csDyMvN4zmGccmE7lz40Is6hc3LA4HFeCI7sekZ/PQMTNmn9I/4Wg==",
"license": "MIT",
"dependencies": {
"@tanstack/vue-virtual": "^3.0.0-beta.60"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/@heroicons/vue": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@heroicons/vue/-/vue-2.2.0.tgz",
"integrity": "sha512-G3dbSxoeEKqbi/DFalhRxJU4mTXJn7GwZ7ae8NuEQzd1bqdd0jAbdaBZlHPcvPD2xI1iGzNVB4k20Un2AguYPw==",
"license": "MIT",
"peerDependencies": {
"vue": ">= 3"
}
},
"node_modules/@intlify/core-base": {
"version": "11.1.12",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.1.12.tgz",
"integrity": "sha512-whh0trqRsSqVLNEUCwU59pyJZYpU8AmSWl8M3Jz2Mv5ESPP6kFh4juas2NpZ1iCvy7GlNRffUD1xr84gceimjg==",
"license": "MIT",
"dependencies": {
"@intlify/message-compiler": "11.1.12",
"@intlify/shared": "11.1.12"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/message-compiler": {
"version": "11.1.12",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.1.12.tgz",
"integrity": "sha512-Fv9iQSJoJaXl4ZGkOCN1LDM3trzze0AS2zRz2EHLiwenwL6t0Ki9KySYlyr27yVOj5aVz0e55JePO+kELIvfdQ==",
"license": "MIT",
"dependencies": {
"@intlify/shared": "11.1.12",
"source-map-js": "^1.0.2"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/shared": {
"version": "11.1.12",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.1.12.tgz",
"integrity": "sha512-Om86EjuQtA69hdNj3GQec9ZC0L0vPSAnXzB3gP/gyJ7+mA7t06d9aOAiqMZ+xEOsumGP4eEBlfl8zF2LOTzf2A==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.29",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz",
"integrity": "sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
"integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz",
"integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz",
"integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz",
"integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz",
"integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz",
"integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz",
"integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz",
"integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz",
"integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz",
"integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz",
"integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz",
"integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz",
"integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz",
"integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz",
"integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz",
"integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz",
"integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz",
"integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"openharmony"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz",
"integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz",
"integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz",
"integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz",
"integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@tailwindcss/node": {
"version": "4.1.15",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.15.tgz",
"integrity": "sha512-HF4+7QxATZWY3Jr8OlZrBSXmwT3Watj0OogeDvdUY/ByXJHQ+LBtqA2brDb3sBxYslIFx6UP94BJ4X6a4L9Bmw==",
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
"enhanced-resolve": "^5.18.3",
"jiti": "^2.6.0",
"lightningcss": "1.30.2",
"magic-string": "^0.30.19",
"source-map-js": "^1.2.1",
"tailwindcss": "4.1.15"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.1.15",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.15.tgz",
"integrity": "sha512-krhX+UOOgnsUuks2SR7hFafXmLQrKxB4YyRTERuCE59JlYL+FawgaAlSkOYmDRJdf1Q+IFNDMl9iRnBW7QBDfQ==",
"license": "MIT",
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.1.15",
"@tailwindcss/oxide-darwin-arm64": "4.1.15",
"@tailwindcss/oxide-darwin-x64": "4.1.15",
"@tailwindcss/oxide-freebsd-x64": "4.1.15",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.15",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.15",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.15",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.15",
"@tailwindcss/oxide-linux-x64-musl": "4.1.15",
"@tailwindcss/oxide-wasm32-wasi": "4.1.15",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.15",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.15"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.1.15",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.15.tgz",
"integrity": "sha512-TkUkUgAw8At4cBjCeVCRMc/guVLKOU1D+sBPrHt5uVcGhlbVKxrCaCW9OKUIBv1oWkjh4GbunD/u/Mf0ql6kEA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.1.15",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.15.tgz",
"integrity": "sha512-xt5XEJpn2piMSfvd1UFN6jrWXyaKCwikP4Pidcf+yfHTSzSpYhG3dcMktjNkQO3JiLCp+0bG0HoWGvz97K162w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.1.15",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.15.tgz",
"integrity": "sha512-TnWaxP6Bx2CojZEXAV2M01Yl13nYPpp0EtGpUrY+LMciKfIXiLL2r/SiSRpagE5Fp2gX+rflp/Os1VJDAyqymg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.1.15",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.15.tgz",
"integrity": "sha512-quISQDWqiB6Cqhjc3iWptXVZHNVENsWoI77L1qgGEHNIdLDLFnw3/AfY7DidAiiCIkGX/MjIdB3bbBZR/G2aJg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.1.15",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.15.tgz",
"integrity": "sha512-ObG76+vPlab65xzVUQbExmDU9FIeYLQ5k2LrQdR2Ud6hboR+ZobXpDoKEYXf/uOezOfIYmy2Ta3w0ejkTg9yxg==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.1.15",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.15.tgz",
"integrity": "sha512-4WbBacRmk43pkb8/xts3wnOZMDKsPFyEH/oisCm2q3aLZND25ufvJKcDUpAu0cS+CBOL05dYa8D4U5OWECuH/Q==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.1.15",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.15.tgz",
"integrity": "sha512-AbvmEiteEj1nf42nE8skdHv73NoR+EwXVSgPY6l39X12Ex8pzOwwfi3Kc8GAmjsnsaDEbk+aj9NyL3UeyHcTLg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.1.15",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.15.tgz",
"integrity": "sha512-+rzMVlvVgrXtFiS+ES78yWgKqpThgV19ISKD58Ck+YO5pO5KjyxLt7AWKsWMbY0R9yBDC82w6QVGz837AKQcHg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.1.15",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.15.tgz",
"integrity": "sha512-fPdEy7a8eQN9qOIK3Em9D3TO1z41JScJn8yxl/76mp4sAXFDfV4YXxsiptJcOwy6bGR+70ZSwFIZhTXzQeqwQg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
"version": "4.1.15",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.15.tgz",
"integrity": "sha512-sJ4yd6iXXdlgIMfIBXuVGp/NvmviEoMVWMOAGxtxhzLPp9LOj5k0pMEMZdjeMCl4C6Up+RM8T3Zgk+BMQ0bGcQ==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
"@emnapi/runtime",
"@tybys/wasm-util",
"@emnapi/wasi-threads",
"tslib"
],
"cpu": [
"wasm32"
],
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.5.0",
"@emnapi/runtime": "^1.5.0",
"@emnapi/wasi-threads": "^1.1.0",
"@napi-rs/wasm-runtime": "^1.0.7",
"@tybys/wasm-util": "^0.10.1",
"tslib": "^2.4.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.15",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.15.tgz",
"integrity": "sha512-sJGE5faXnNQ1iXeqmRin7Ds/ru2fgCiaQZQQz3ZGIDtvbkeV85rAZ0QJFMDg0FrqsffZG96H1U9AQlNBRLsHVg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.1.15",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.15.tgz",
"integrity": "sha512-NLeHE7jUV6HcFKS504bpOohyi01zPXi2PXmjFfkzTph8xRxDdxkRsXm/xDO5uV5K3brrE1cCwbUYmFUSHR3u1w==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/vite": {
"version": "4.1.15",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.15.tgz",
"integrity": "sha512-B6s60MZRTUil+xKoZoGe6i0Iar5VuW+pmcGlda2FX+guDuQ1G1sjiIy1W0frneVpeL/ZjZ4KEgWZHNrIm++2qA==",
"license": "MIT",
"dependencies": {
"@tailwindcss/node": "4.1.15",
"@tailwindcss/oxide": "4.1.15",
"tailwindcss": "4.1.15"
},
"peerDependencies": {
"vite": "^5.2.0 || ^6 || ^7"
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/vue-virtual": {
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.12.tgz",
"integrity": "sha512-vhF7kEU9EXWXh+HdAwKJ2m3xaOnTTmgcdXcF2pim8g4GvI7eRrk2YRuV5nUlZnd/NbCIX4/Ja2OZu5EjJL06Ww==",
"license": "MIT",
"dependencies": {
"@tanstack/virtual-core": "3.13.12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"vue": "^2.7.0 || ^3.0.0"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
},
"node_modules/@vitejs/plugin-vue": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz",
"integrity": "sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rolldown/pluginutils": "1.0.0-beta.29"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"peerDependencies": {
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
"vue": "^3.2.25"
}
},
"node_modules/@vue/compiler-core": {
"version": "3.5.22",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.22.tgz",
"integrity": "sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.4",
"@vue/shared": "3.5.22",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.5.22",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.22.tgz",
"integrity": "sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==",
"license": "MIT",
"dependencies": {
"@vue/compiler-core": "3.5.22",
"@vue/shared": "3.5.22"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.5.22",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz",
"integrity": "sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.4",
"@vue/compiler-core": "3.5.22",
"@vue/compiler-dom": "3.5.22",
"@vue/compiler-ssr": "3.5.22",
"@vue/shared": "3.5.22",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.19",
"postcss": "^8.5.6",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.5.22",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.22.tgz",
"integrity": "sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.22",
"@vue/shared": "3.5.22"
}
},
"node_modules/@vue/devtools-api": {
"version": "7.7.7",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.7.tgz",
"integrity": "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==",
"license": "MIT",
"dependencies": {
"@vue/devtools-kit": "^7.7.7"
}
},
"node_modules/@vue/devtools-kit": {
"version": "7.7.7",
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz",
"integrity": "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==",
"license": "MIT",
"dependencies": {
"@vue/devtools-shared": "^7.7.7",
"birpc": "^2.3.0",
"hookable": "^5.5.3",
"mitt": "^3.0.1",
"perfect-debounce": "^1.0.0",
"speakingurl": "^14.0.1",
"superjson": "^2.2.2"
}
},
"node_modules/@vue/devtools-shared": {
"version": "7.7.7",
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz",
"integrity": "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==",
"license": "MIT",
"dependencies": {
"rfdc": "^1.4.1"
}
},
"node_modules/@vue/reactivity": {
"version": "3.5.22",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.22.tgz",
"integrity": "sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==",
"license": "MIT",
"dependencies": {
"@vue/shared": "3.5.22"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.5.22",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.22.tgz",
"integrity": "sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.22",
"@vue/shared": "3.5.22"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.5.22",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.22.tgz",
"integrity": "sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.22",
"@vue/runtime-core": "3.5.22",
"@vue/shared": "3.5.22",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.5.22",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.22.tgz",
"integrity": "sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==",
"license": "MIT",
"dependencies": {
"@vue/compiler-ssr": "3.5.22",
"@vue/shared": "3.5.22"
},
"peerDependencies": {
"vue": "3.5.22"
}
},
"node_modules/@vue/shared": {
"version": "3.5.22",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.22.tgz",
"integrity": "sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==",
"license": "MIT"
},
"node_modules/birpc": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.6.1.tgz",
"integrity": "sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/copy-anything": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
"integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
"license": "MIT",
"dependencies": {
"is-what": "^4.1.8"
},
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/enhanced-resolve": {
"version": "5.18.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/esbuild": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz",
"integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.11",
"@esbuild/android-arm": "0.25.11",
"@esbuild/android-arm64": "0.25.11",
"@esbuild/android-x64": "0.25.11",
"@esbuild/darwin-arm64": "0.25.11",
"@esbuild/darwin-x64": "0.25.11",
"@esbuild/freebsd-arm64": "0.25.11",
"@esbuild/freebsd-x64": "0.25.11",
"@esbuild/linux-arm": "0.25.11",
"@esbuild/linux-arm64": "0.25.11",
"@esbuild/linux-ia32": "0.25.11",
"@esbuild/linux-loong64": "0.25.11",
"@esbuild/linux-mips64el": "0.25.11",
"@esbuild/linux-ppc64": "0.25.11",
"@esbuild/linux-riscv64": "0.25.11",
"@esbuild/linux-s390x": "0.25.11",
"@esbuild/linux-x64": "0.25.11",
"@esbuild/netbsd-arm64": "0.25.11",
"@esbuild/netbsd-x64": "0.25.11",
"@esbuild/openbsd-arm64": "0.25.11",
"@esbuild/openbsd-x64": "0.25.11",
"@esbuild/openharmony-arm64": "0.25.11",
"@esbuild/sunos-x64": "0.25.11",
"@esbuild/win32-arm64": "0.25.11",
"@esbuild/win32-ia32": "0.25.11",
"@esbuild/win32-x64": "0.25.11"
}
},
"node_modules/esbuild-linux-64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz",
"integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT"
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"license": "MIT"
},
"node_modules/is-what": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
"integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
"license": "MIT",
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/jiti": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/lightningcss": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
},
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-android-arm64": "1.30.2",
"lightningcss-darwin-arm64": "1.30.2",
"lightningcss-darwin-x64": "1.30.2",
"lightningcss-freebsd-x64": "1.30.2",
"lightningcss-linux-arm-gnueabihf": "1.30.2",
"lightningcss-linux-arm64-gnu": "1.30.2",
"lightningcss-linux-arm64-musl": "1.30.2",
"lightningcss-linux-x64-gnu": "1.30.2",
"lightningcss-linux-x64-musl": "1.30.2",
"lightningcss-win32-arm64-msvc": "1.30.2",
"lightningcss-win32-x64-msvc": "1.30.2"
}
},
"node_modules/lightningcss-android-arm64": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
"integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
"integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-x64": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
"integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-freebsd-x64": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
"integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
"integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
"cpu": [
"arm"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
"integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-musl": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
"integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-gnu": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
"integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-musl": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
"integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
"integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
"integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/magic-string": {
"version": "0.30.19",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
"integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/perfect-debounce": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pinia": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.3.tgz",
"integrity": "sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^7.7.2"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"typescript": ">=4.4.4",
"vue": "^2.7.0 || ^3.5.11"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"license": "MIT"
},
"node_modules/rollup": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
"integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==",
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.8"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.52.5",
"@rollup/rollup-android-arm64": "4.52.5",
"@rollup/rollup-darwin-arm64": "4.52.5",
"@rollup/rollup-darwin-x64": "4.52.5",
"@rollup/rollup-freebsd-arm64": "4.52.5",
"@rollup/rollup-freebsd-x64": "4.52.5",
"@rollup/rollup-linux-arm-gnueabihf": "4.52.5",
"@rollup/rollup-linux-arm-musleabihf": "4.52.5",
"@rollup/rollup-linux-arm64-gnu": "4.52.5",
"@rollup/rollup-linux-arm64-musl": "4.52.5",
"@rollup/rollup-linux-loong64-gnu": "4.52.5",
"@rollup/rollup-linux-ppc64-gnu": "4.52.5",
"@rollup/rollup-linux-riscv64-gnu": "4.52.5",
"@rollup/rollup-linux-riscv64-musl": "4.52.5",
"@rollup/rollup-linux-s390x-gnu": "4.52.5",
"@rollup/rollup-linux-x64-gnu": "4.52.5",
"@rollup/rollup-linux-x64-musl": "4.52.5",
"@rollup/rollup-openharmony-arm64": "4.52.5",
"@rollup/rollup-win32-arm64-msvc": "4.52.5",
"@rollup/rollup-win32-ia32-msvc": "4.52.5",
"@rollup/rollup-win32-x64-gnu": "4.52.5",
"@rollup/rollup-win32-x64-msvc": "4.52.5",
"fsevents": "~2.3.2"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/speakingurl": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/superjson": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz",
"integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==",
"license": "MIT",
"dependencies": {
"copy-anything": "^3.0.2"
},
"engines": {
"node": ">=16"
}
},
"node_modules/tailwindcss": {
"version": "4.1.15",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.15.tgz",
"integrity": "sha512-k2WLnWkYFkdpRv+Oby3EBXIyQC8/s1HOFMBUViwtAh6Z5uAozeUSMQlIsn/c6Q2iJzqG6aJT3wdPaRNj70iYxQ==",
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/vite": {
"version": "7.1.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz",
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
"picomatch": "^4.0.3",
"postcss": "^8.5.6",
"rollup": "^4.43.0",
"tinyglobby": "^0.2.15"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
"lightningcss": "^1.21.0",
"sass": "^1.70.0",
"sass-embedded": "^1.70.0",
"stylus": ">=0.54.8",
"sugarss": "^5.0.0",
"terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"jiti": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
},
"tsx": {
"optional": true
},
"yaml": {
"optional": true
}
}
},
"node_modules/vue": {
"version": "3.5.22",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
"integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.22",
"@vue/compiler-sfc": "3.5.22",
"@vue/runtime-dom": "3.5.22",
"@vue/server-renderer": "3.5.22",
"@vue/shared": "3.5.22"
},
"peerDependencies": {
"typescript": "*"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/vue-i18n": {
"version": "11.1.12",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.1.12.tgz",
"integrity": "sha512-BnstPj3KLHLrsqbVU2UOrPmr0+Mv11bsUZG0PyCOzsawCivk8W00GMXHeVUWIDOgNaScCuZah47CZFE+Wnl8mw==",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "11.1.12",
"@intlify/shared": "11.1.12",
"@vue/devtools-api": "^6.5.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/vue-i18n/node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
"node_modules/vue-router": {
"version": "4.6.3",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.3.tgz",
"integrity": "sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^6.6.4"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/vue-router/node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
}
}
}
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@flaticon/flaticon-uicons": "^3.3.1",
"@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.2.0",
"@tailwindcss/vite": "^4.1.13",
"pinia": "^3.0.3",
"tailwindcss": "^4.1.13",
"vue": "^3.5.21",
"vue-i18n": "^11.1.12",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"vite": "^7.1.7"
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generator: visioncortex VTracer -->
<svg id="svg" version="1.1" xmlns="http://www.w3.org/2000/svg" style="display: block;" viewBox="0 0 512 512"><path d="M0 0 C1.19935684 -0.00151062 1.19935684 -0.00151062 2.42294312 -0.00305176 C36.44465308 0.02584755 66.83096651 4.81015715 92.3125 29 C104.73236108 41.91665553 117.15430318 62.02105437 119.125 80.25 C117.475 81.24 115.825 82.23 114.125 83.25 C114.18558594 83.83338135 114.24617188 84.4167627 114.30859375 85.01782227 C115.77649849 99.75191605 116.34355512 114.39107705 116.375 129.1875 C116.38249268 130.39589035 116.38249268 130.39589035 116.39013672 131.62869263 C116.46741408 146.815047 115.63994775 162.1240274 112.375 177 C112.20653564 177.77529053 112.03807129 178.55058105 111.86450195 179.34936523 C108.46241976 194.12910138 102.7471068 207.55542161 90.3359375 216.875 C85.92128643 219.62050738 82.04167592 220.55686325 76.875 220.9375 C75.81796875 221.02386719 74.7609375 221.11023438 73.671875 221.19921875 C69.47806155 221.25926233 65.29359015 220.71317668 61.125 220.25 C61.125 228.5 61.125 236.75 61.125 245.25 C63.105 245.58 65.085 245.91 67.125 246.25 C69.67184131 247.20255532 72.14073458 248.19267001 74.62890625 249.27734375 C75.34062988 249.58170853 76.05235352 249.8860733 76.78564453 250.19966125 C79.08961941 251.18802052 81.38883907 252.18684991 83.6875 253.1875 C84.47333466 253.52769165 85.25916931 253.8678833 86.06881714 254.21838379 C132.92921413 274.50836637 132.92921413 274.50836637 151.125 290.25 C151.74246094 290.73597656 152.35992187 291.22195313 152.99609375 291.72265625 C174.31395159 308.76744682 180.90242243 335.22291795 185.63330078 360.70043945 C185.93325063 362.25574048 186.27138299 363.8050734 186.67529297 365.33666992 C187.1015625 367.140625 187.1015625 367.140625 187.125 370.25 C185.33680454 372.47061363 183.98055469 373.77559316 181.75 375.4375 C181.16492676 375.89962891 180.57985352 376.36175781 179.97705078 376.83789062 C178.04850478 378.34039182 176.09169492 379.79786954 174.125 381.25 C173.47080078 381.74193848 172.81660156 382.23387695 172.14257812 382.74072266 C139.6626858 407.0372665 102.04329031 423.20945437 62.125 430.25 C61.29274902 430.41081055 60.46049805 430.57162109 59.60302734 430.73730469 C31.86455166 435.99758258 -0.19461137 435.61784631 -27.875 430.25 C-28.89158691 430.05664062 -29.90817383 429.86328125 -30.95556641 429.6640625 C-75.99758608 420.81274218 -115.84133693 400.55342297 -150.875 371.25 C-150.76430176 370.63705078 -150.65360352 370.02410156 -150.53955078 369.39257812 C-150.02201675 366.51248237 -149.51119632 363.63122719 -149 360.75 C-148.80929932 359.69643311 -148.61859863 358.64286621 -148.42211914 357.55737305 C-147.21352409 350.74422453 -147.21352409 350.74422453 -146.55078125 343.86328125 C-145.52866033 327.55961482 -135.90055941 312.92630913 -124.875 301.25 C-123.52728516 299.81914063 -123.52728516 299.81914063 -122.15234375 298.359375 C-118.22619793 294.28488102 -114.32811372 290.42482918 -109.625 287.25 C-95.80897049 277.29681956 -92.06583285 256.58023198 -89.26049805 240.86962891 C-88.22373646 233.82437759 -87.70945046 226.89752621 -87.671875 219.77734375 C-87.66162544 218.42767265 -87.66162544 218.42767265 -87.65116882 217.05073547 C-87.633997 214.09628552 -87.6278159 211.14199722 -87.625 208.1875 C-87.62299591 207.16289673 -87.62099182 206.13829346 -87.618927 205.0826416 C-87.61140747 189.17377232 -88.22574911 173.36969582 -89.3125 157.5 C-89.38089081 156.49746185 -89.44928162 155.49492371 -89.51974487 154.46200562 C-89.65670687 152.45848364 -89.79377538 150.45496895 -89.93095398 148.45146179 C-89.99756729 147.4774794 -90.0641806 146.50349701 -90.1328125 145.5 C-90.23146355 144.05999382 -90.23146355 144.05999382 -90.33210754 142.59089661 C-96.10825705 56.90928312 -96.10825705 56.90928312 -72.875 28.25 C-71.94880859 27.08017578 -71.94880859 27.08017578 -71.00390625 25.88671875 C-53.41588934 4.83064217 -25.98524653 0.02257652 0 0 Z " transform="translate(238.875,78.75)" style="fill: #FCA9A5;"/><path d="M0 0 C4.86620795 0.59753861 9.04243625 2.08244606 13.50390625 4.02734375 C14.5714917 4.48389091 14.5714917 4.48389091 15.66064453 4.94966125 C17.96461941 5.93802052 20.26383907 6.93684991 22.5625 7.9375 C23.34833466 8.27769165 24.13416931 8.6178833 24.94381714 8.96838379 C71.80421413 29.25836637 71.80421413 29.25836637 90 45 C90.61746094 45.48597656 91.23492187 45.97195313 91.87109375 46.47265625 C113.18895159 63.51744682 119.77742243 89.97291795 124.50830078 115.45043945 C124.80825063 117.00574048 125.14638299 118.5550734 125.55029297 120.08666992 C125.9765625 121.890625 125.9765625 121.890625 126 125 C124.21180454 127.22061363 122.85555469 128.52559316 120.625 130.1875 C120.03992676 130.64962891 119.45485352 131.11175781 118.85205078 131.58789062 C116.92350478 133.09039182 114.96669492 134.54786954 113 136 C112.34580078 136.49193848 111.69160156 136.98387695 111.01757812 137.49072266 C78.5376858 161.7872665 40.91829031 177.95945437 1 185 C0.16774902 185.16081055 -0.66450195 185.32162109 -1.52197266 185.48730469 C-29.26044834 190.74758258 -61.31961137 190.36784631 -89 185 C-90.01658691 184.80664062 -91.03317383 184.61328125 -92.08056641 184.4140625 C-137.12258608 175.56274218 -176.96633693 155.30342297 -212 126 C-211.88930176 125.38705078 -211.77860352 124.77410156 -211.66455078 124.14257812 C-211.14701675 121.26248237 -210.63619632 118.38122719 -210.125 115.5 C-209.93429932 114.44643311 -209.74359863 113.39286621 -209.54711914 112.30737305 C-208.33852409 105.49422453 -208.33852409 105.49422453 -207.67578125 98.61328125 C-206.65366033 82.30961482 -197.02555941 67.67630913 -186 56 C-185.10152344 55.04609375 -184.20304687 54.0921875 -183.27734375 53.109375 C-161.31499576 30.37069401 -130.9911306 17.22084328 -102 6 C-101.17870605 5.68063477 -100.35741211 5.36126953 -99.51123047 5.03222656 C-87.19453472 0.28502249 -87.19453472 0.28502249 -83 1 C-80.9296875 2.984375 -80.9296875 2.984375 -78.875 5.75 C-78.09382812 6.75289063 -77.31265625 7.75578125 -76.5078125 8.7890625 C-76.09821289 9.31790039 -75.68861328 9.84673828 -75.26660156 10.39160156 C-67.3437236 20.45249594 -57.5591985 31.0139067 -45 35 C-36.88601566 35.93389655 -30.03602049 35.14196532 -23.53125 30.078125 C-14.3903272 22.093584 -3.91302067 11.739062 0 0 Z " transform="translate(300,324)" style="fill: #F9E2DB;"/><path d="M0 0 C1.19935684 -0.00151062 1.19935684 -0.00151062 2.42294312 -0.00305176 C36.44465308 0.02584755 66.83096651 4.81015715 92.3125 29 C104.73236108 41.91665553 117.15430318 62.02105437 119.125 80.25 C114.375 83.25 114.375 83.25 112.125 83.25 C111.45345067 81.96086513 110.78791141 80.66859796 110.125 79.375 C109.75375 78.65570313 109.3825 77.93640625 109 77.1953125 C108.125 75.25 108.125 75.25 108.125 73.25 C107.17625 72.940625 106.2275 72.63125 105.25 72.3125 C102.125 71.25 102.125 71.25 100.125 70.25 C93.24838938 69.78360527 86.55659332 70.25442982 79.75 71.25 C59.16672064 74.15034797 39.11688215 73.60313835 21.51171875 61.4453125 C17.78377639 58.60207004 14.40438669 55.60258729 11.125 52.25 C10.99544922 54.1371875 10.99544922 54.1371875 10.86328125 56.0625 C8.49913303 86.32122116 8.49913303 86.32122116 -1.875 96.25 C-8.08295966 101.35800564 -14.06923587 101.91337939 -21.875 101.25 C-22.98875 100.755 -24.1025 100.26 -25.25 99.75 C-30.73221549 97.48149704 -36.02274567 97.57125916 -41.875 98.25 C-47.71572075 101.0166572 -51.94524998 105.86530245 -54.33203125 111.91796875 C-56.64519668 121.85293107 -55.16131952 132.0981286 -50.875 141.25 C-48.81313456 144.25271913 -48.81313456 144.25271913 -46.875 146.25 C-46.875 146.91 -46.875 147.57 -46.875 148.25 C-46.33875 148.518125 -45.8025 148.78625 -45.25 149.0625 C-42.875 150.25 -42.875 150.25 -40.375 151.8125 C-35.64698654 153.96010157 -35.64698654 153.96010157 -24.875 154.25 C-24.875 184.61 -24.875 214.97 -24.875 246.25 C-36.095 250.54 -47.315 254.83 -58.875 259.25 C-72.64124362 265.44480963 -85.47864753 272.02277441 -98.14550781 280.1484375 C-100.36231113 281.56038225 -102.6128939 282.91210067 -104.875 284.25 C-103.67922705 281.80326458 -102.41681605 279.41555903 -101.10546875 277.02734375 C-97.21493015 269.80613847 -94.09972877 262.69458778 -92 254.75 C-91.80946045 254.04093506 -91.6189209 253.33187012 -91.42260742 252.60131836 C-87.78564854 237.81167686 -87.51100974 222.88531887 -87.5625 207.75 C-87.56299347 206.74287964 -87.56348694 205.73575928 -87.56399536 204.69812012 C-87.5917681 185.88843476 -88.64542828 167.20923875 -89.93081665 148.45050049 C-89.99747528 147.47683533 -90.06413391 146.50317017 -90.1328125 145.5 C-90.2314711 144.06004669 -90.2314711 144.06004669 -90.3321228 142.59100342 C-96.107655 56.90854046 -96.107655 56.90854046 -72.875 28.25 C-71.94880859 27.08017578 -71.94880859 27.08017578 -71.00390625 25.88671875 C-53.41588934 4.83064217 -25.98524653 0.02257652 0 0 Z " transform="translate(238.875,78.75)" style="fill: #6684F4;"/><path d="M0 0 C8.806723 7.40681039 10.2382616 25.77511574 12.43798828 36.49902344 C12.93008359 38.68886893 13.52401231 40.78082951 14.21484375 42.9140625 C15.16094238 46.31676916 15.65335187 48.52513595 15 52 C11.86773417 56.02319654 8.22154187 58.7532968 4.02978516 61.60986328 C1.98252812 63.01196595 -0.00303248 64.47651258 -1.98828125 65.96484375 C-33.86159262 89.56624474 -71.00159311 105.12169782 -110 112 C-111.24837646 112.24121582 -111.24837646 112.24121582 -112.52197266 112.48730469 C-140.26044834 117.74758258 -172.31961137 117.36784631 -200 112 C-201.01658691 111.80664062 -202.03317383 111.61328125 -203.08056641 111.4140625 C-248.12258608 102.56274218 -287.96633693 82.30342297 -323 53 C-322.88930176 52.38705078 -322.77860352 51.77410156 -322.66455078 51.14257812 C-322.14701633 48.26248007 -321.63559663 45.38133604 -321.125 42.5 C-320.93719971 41.45150879 -320.74939941 40.40301758 -320.5559082 39.32275391 C-319.61987965 33.98906483 -318.82333729 28.71072442 -318.35839844 23.31640625 C-317.57832375 14.45475777 -314.73593722 8.3951533 -310 1 C-309.67 1 -309.34 1 -309 1 C-309.06960938 2.55847656 -309.06960938 2.55847656 -309.140625 4.1484375 C-309.48689466 15.1563765 -308.54113783 24.08761466 -300.71484375 32.53125 C-295.59458318 36.91671235 -289.11977609 39.36392608 -282.39453125 39.2890625 C-276.46511051 38.57327598 -269.98206712 36.61708115 -266 32 C-263.98492462 33.00528369 -261.97204278 34.01422008 -259.96484375 35.03515625 C-259.29582031 35.37417969 -258.62679687 35.71320313 -257.9375 36.0625 C-257.26589844 36.40410156 -256.59429687 36.74570312 -255.90234375 37.09765625 C-250.59727561 39.61402327 -245.15488335 40.0164058 -239.55078125 38.2109375 C-235.71467972 36.54968362 -232.17650818 34.76218103 -229 32 C-226.78537326 33.06630176 -224.70583833 34.18578632 -222.625 35.5 C-216.50626487 38.99642008 -210.53324033 40.03855171 -203.6640625 38.46484375 C-199.02772237 37.05545796 -195.59393796 35.36207099 -192 32 C-189.43365172 33.23184718 -187.00041934 34.5986838 -184.5625 36.0625 C-178.96710689 39.25817038 -173.06200709 39.94919753 -166.7265625 38.46875 C-162.06775007 37.04479404 -158.61562873 35.38236236 -155 32 C-152.42090014 33.23796793 -149.96031405 34.60747556 -147.5 36.0625 C-141.78419356 39.31313834 -136.11501685 39.93273715 -129.65625 38.46875 C-125.02635687 37.04891611 -121.59243611 35.36066604 -118 32 C-115.87438655 33.04091826 -113.79514987 34.11675396 -111.73828125 35.2890625 C-105.02748812 38.98486162 -100.1089451 40.17062572 -92.61328125 38.453125 C-87.98892407 37.07728319 -84.57423586 35.34364 -81 32 C-78.87438655 33.04091826 -76.79514987 34.11675396 -74.73828125 35.2890625 C-68.02748812 38.98486162 -63.1089451 40.17062572 -55.61328125 38.453125 C-50.98892407 37.07728319 -47.57423586 35.34364 -44 32 C-41.87438655 33.04091826 -39.79514987 34.11675396 -37.73828125 35.2890625 C-30.1998063 39.44068638 -24.65077861 40.19949958 -16.375 37.875 C-8.34480968 34.83919634 -4.49797194 29.92774747 -0.9453125 22.26953125 C0.6895025 18.34462416 1.13555799 15.35875434 1.125 11.125 C1.12886719 9.58199219 1.12886719 9.58199219 1.1328125 8.0078125 C1.00995061 5.225352 0.62944236 2.70760126 0 0 Z " transform="translate(411,397)" style="fill: #FE9FB6;"/><path d="M0 0 C0.33 0 0.66 0 1 0 C0.93039063 1.55847656 0.93039063 1.55847656 0.859375 3.1484375 C0.51310534 14.1563765 1.45886217 23.08761466 9.28515625 31.53125 C14.40541682 35.91671235 20.88022391 38.36392608 27.60546875 38.2890625 C33.53488949 37.57327598 40.01793288 35.61708115 44 31 C46.01507538 32.00528369 48.02795722 33.01422008 50.03515625 34.03515625 C50.70417969 34.37417969 51.37320313 34.71320313 52.0625 35.0625 C52.73410156 35.40410156 53.40570313 35.74570312 54.09765625 36.09765625 C59.40272439 38.61402327 64.84511665 39.0164058 70.44921875 37.2109375 C74.28532028 35.54968362 77.82349182 33.76218103 81 31 C83.21462674 32.06630176 85.29416167 33.18578632 87.375 34.5 C93.49373513 37.99642008 99.46675967 39.03855171 106.3359375 37.46484375 C110.97227763 36.05545796 114.40606204 34.36207099 118 31 C120.56634828 32.23184718 122.99958066 33.5986838 125.4375 35.0625 C131.03289311 38.25817038 136.93799291 38.94919753 143.2734375 37.46875 C147.93224993 36.04479404 151.38437127 34.38236236 155 31 C157.57909986 32.23796793 160.03968595 33.60747556 162.5 35.0625 C168.21580644 38.31313834 173.88498315 38.93273715 180.34375 37.46875 C184.97364313 36.04891611 188.40756389 34.36066604 192 31 C194.12561345 32.04091826 196.20485013 33.11675396 198.26171875 34.2890625 C204.97251188 37.98486162 209.8910549 39.17062572 217.38671875 37.453125 C222.01107593 36.07728319 225.42576414 34.34364 229 31 C231.12561345 32.04091826 233.20485013 33.11675396 235.26171875 34.2890625 C241.97251188 37.98486162 246.8910549 39.17062572 254.38671875 37.453125 C259.01107593 36.07728319 262.42576414 34.34364 266 31 C268.12561345 32.04091826 270.20485013 33.11675396 272.26171875 34.2890625 C279.8001937 38.44068638 285.34922139 39.19949958 293.625 36.875 C301.28923081 33.97754689 305.58769908 29.17588619 309 21.8125 C310.71283148 16.99516146 311.41709387 12.06345327 312 7 C316.42282212 12.59820225 317.48303904 18.42514189 317.3125 25.44921875 C316.42184355 32.71920201 312.43576393 39.18683458 307 44 C300.18437052 48.6481148 293.358911 51.07202671 285 50 C279.77381142 48.4124961 275.27605469 46.42280524 271 43 C268.74378837 44.08632412 266.62646456 45.23345965 264.5 46.5625 C257.90386846 50.35527563 248.7365094 50.06479669 241.48046875 48.43359375 C237.72351636 47.06155186 234.39955959 45.07460303 231 43 C230.32453125 43.42152344 229.6490625 43.84304688 228.953125 44.27734375 C227.61507812 45.09912109 227.61507812 45.09912109 226.25 45.9375 C224.92742187 46.75541016 224.92742187 46.75541016 223.578125 47.58984375 C218.58978504 50.31831454 213.59680481 50.64411128 208 50 C202.75303612 48.44818931 198.28371202 46.42108809 194 43 C191.74378837 44.08632412 189.62646456 45.23345965 187.5 46.5625 C180.86676011 50.37661294 171.70946688 50.0299739 164.41015625 48.43359375 C160.90878672 47.18822768 158.49953673 45.69587496 156 43 C154.02727284 43.96890582 152.09204968 44.94517041 150.203125 46.0703125 C142.91162858 50.00772056 135.03322731 50.12803593 127 49 C123.99737867 47.62866479 121.60389424 46.1534133 119.015625 44.1171875 C118.01789063 43.56417969 118.01789063 43.56417969 117 43 C114.93427543 43.63348429 114.93427543 43.63348429 113 45 C111.48330259 45.6965768 109.96234013 46.38393324 108.4375 47.0625 C107.67308594 47.40410156 106.90867187 47.74570312 106.12109375 48.09765625 C99.10775787 51.08123008 93.05356097 50.46432827 86.0546875 47.93359375 C83.3208714 46.69142255 81.18503514 45.04406513 79 43 C78.14921875 43.43119141 78.14921875 43.43119141 77.28125 43.87109375 C75.0387716 44.98081337 72.78130665 46.03513074 70.5 47.0625 C69.7471875 47.40410156 68.994375 47.74570312 68.21875 48.09765625 C61.51793375 50.82281215 55.51024492 50.80271486 48.8125 48.1875 C45.37385945 46.69865871 42.12548787 45.08365858 39 43 C38.46890625 43.34933594 37.9378125 43.69867188 37.390625 44.05859375 C36.33101562 44.74115234 36.33101562 44.74115234 35.25 45.4375 C34.55390625 45.88996094 33.8578125 46.34242188 33.140625 46.80859375 C26.56032672 50.47098604 16.65371465 50.19955999 9.484375 48.3515625 C1.59104242 44.71565734 -3.92908053 38.00750507 -7 30 C-9.73388966 20.69743586 -7.24860656 13.14601242 -2.84375 4.8125 C-1.92776092 3.18989077 -0.96881852 1.59163042 0 0 Z " transform="translate(101,398)" style="fill: #FEC2B1;"/><path d="M0 0 C4.07769501 1.49313367 7.64388825 3.47405492 11.375 5.6875 C23.42589254 12.67947997 35.68458979 17.91561013 49 22 C50.04414063 22.32097656 51.08828125 22.64195312 52.1640625 22.97265625 C54.43879821 23.66148467 56.71872023 24.33316438 59 25 C59 30.61 59 36.22 59 42 C39.84931373 39.84554779 21.30491692 29.99451625 9 15 C0 2.46315789 0 2.46315789 0 0 Z " transform="translate(241,273)" style="fill: #413578;"/></svg>
<svg width="766" height="664" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" overflow="hidden"><g transform="translate(-1750 -715)"><g><path d="M2243.72 715.11C2246.74 715.113 2246.74 715.113 2249.82 715.117 2252.12 715.114 2254.42 715.11 2256.79 715.107 2259.33 715.116 2261.88 715.124 2264.5 715.133 2268.5 715.133 2268.5 715.133 2272.57 715.132 2281.42 715.134 2290.27 715.153 2299.12 715.172 2305.24 715.176 2311.36 715.18 2317.48 715.182 2331.96 715.19 2346.45 715.209 2360.94 715.234 2377.43 715.261 2393.92 715.274 2410.41 715.286 2444.34 715.311 2478.26 715.354 2512.19 715.408 2508.04 724.065 2503.01 731.098 2497.09 738.687 2496.04 740.034 2495 741.382 2493.92 742.769 2491.6 745.746 2489.29 748.72 2486.97 751.692 2483 756.781 2479.05 761.885 2475.1 766.992 2460.44 785.934 2445.67 804.763 2430.67 823.436 2422.04 834.206 2413.58 845.099 2405.2 856.068 2397.85 865.678 2390.41 875.208 2382.93 884.711 2373.96 896.105 2365.04 907.535 2356.2 919.024 2354.25 921.553 2352.3 924.081 2350.3 926.687 2346.94 931.137 2343.73 935.699 2340.64 940.339 2395.66 940.339 2450.68 940.339 2507.36 940.339 2503.06 948.942 2500.32 952.707 2493.62 959.083 2485.57 966.934 2478.08 975.031 2470.75 983.552 2465.71 989.373 2460.57 995.09 2455.41 1000.8 2448.91 1008.01 2442.45 1015.25 2436.08 1022.57 2427.88 1031.99 2419.53 1041.26 2411.16 1050.54 2406.54 1055.69 2401.97 1060.88 2397.42 1066.11 2389.23 1075.53 2380.87 1084.8 2372.51 1094.07 2367.88 1099.23 2363.31 1104.42 2358.76 1109.64 2352.39 1116.96 2345.93 1124.2 2339.43 1131.41 2327.79 1144.34 2316.29 1157.39 2305 1170.64 2291.53 1186.46 2277.92 1202.08 2263.88 1217.4 2259.2 1222.54 2254.59 1227.74 2250.04 1232.99 2243.3 1240.75 2236.42 1248.37 2229.5 1255.97 2217.53 1269.12 2205.76 1282.39 2194.29 1295.98 2179.33 1313.68 2163.93 1330.84 2147.96 1347.65 2138.56 1357.55 2129.57 1367.67 2120.77 1378.11 2118.35 1373.27 2118.35 1373.27 2120.39 1367.04 2122.03 1363.06 2122.03 1363.06 2123.71 1359.01 2124.32 1357.55 2124.92 1356.08 2125.54 1354.57 2127.56 1349.66 2129.6 1344.76 2131.64 1339.86 2133.07 1336.41 2134.49 1332.96 2135.91 1329.51 2138.15 1324.04 2140.41 1318.58 2142.66 1313.11 2150.65 1293.77 2158.48 1274.36 2166.31 1254.95 2199.1 1173.69 2199.1 1173.69 2211.83 1146.37 2217.42 1134.28 2221.81 1121.88 2226.1 1109.27 2230.83 1096.33 2236.28 1083.66 2241.58 1070.94 2192.94 1070.94 2144.3 1070.94 2094.19 1070.94 2099.03 1056.4 2104.01 1042.11 2109.59 1027.86 2110.27 1026.13 2110.94 1024.39 2111.63 1022.6 2117.65 1007.17 2124.03 991.896 2130.43 976.618 2149.93 930.008 2169.27 883.341 2188.17 836.485 2197.34 813.764 2206.57 791.079 2216.17 768.533 2222.38 753.917 2228.24 739.174 2234.02 724.383 2237.71 715.502 2237.71 715.502 2243.72 715.11Z" fill="#85EEEE" fill-rule="evenodd" fill-opacity="1"/><path d="M1927.2 721.344C1930.12 721.342 1933.05 721.34 1936.06 721.338 1944.07 721.346 1952.07 721.407 1960.07 721.492 1968.44 721.569 1976.81 721.576 1985.19 721.59 2001.03 721.628 2016.87 721.729 2032.71 721.852 2050.76 721.989 2068.8 722.056 2086.84 722.117 2123.94 722.245 2161.04 722.463 2198.14 722.733 2193.36 735.655 2188.57 748.578 2183.64 761.891 2146.96 762.118 2110.28 762.293 2073.6 762.399 2056.57 762.449 2039.54 762.518 2022.5 762.63 2006.07 762.738 1989.64 762.795 1973.21 762.82 1966.94 762.838 1960.66 762.874 1954.39 762.926 1945.61 762.998 1936.84 763.007 1928.06 763.003 1925.46 763.038 1922.86 763.073 1920.17 763.109 1902.63 763.011 1902.63 763.011 1893.13 757.453 1886.75 749.869 1885.86 744.872 1886.21 734.97 1896.56 718.983 1910.1 721.071 1927.2 721.344Z" fill="#8CF1EA" fill-rule="evenodd" fill-opacity="1"/><path d="M1788.49 873.25C1793.21 873.192 1793.21 873.192 1798.03 873.133 1801.45 873.155 1804.86 873.18 1808.28 873.208 1811.8 873.2 1815.33 873.188 1818.85 873.173 1826.23 873.154 1833.6 873.181 1840.97 873.238 1850.41 873.308 1859.84 873.268 1869.28 873.194 1876.55 873.15 1883.82 873.164 1891.1 873.196 1894.58 873.204 1898.05 873.194 1901.53 873.165 1906.4 873.134 1911.27 873.19 1916.13 873.25 1918.9 873.26 1921.66 873.269 1924.51 873.28 1934.25 875.329 1937.93 879.24 1943.99 887.125 1944.29 895.878 1944.29 895.878 1941.58 904.324 1932.61 913.514 1926.51 915.397 1913.94 915.502 1910.84 915.548 1907.74 915.595 1904.55 915.643 1901.18 915.642 1897.82 915.638 1894.46 915.63 1890.99 915.649 1887.52 915.67 1884.05 915.694 1876.79 915.73 1869.53 915.729 1862.27 915.703 1852.98 915.675 1843.69 915.758 1834.4 915.872 1827.24 915.944 1820.08 915.949 1812.92 915.933 1809.49 915.936 1806.07 915.962 1802.64 916.011 1776 916.349 1776 916.349 1767.9 910.971 1763.19 905.453 1760.82 900.185 1759.12 893.114 1763.01 877.664 1774.07 873.301 1788.49 873.25Z" fill="#75DCE5" fill-rule="evenodd" fill-opacity="1"/><path d="M1932.29 1029.16C1935.39 1029.16 1938.5 1029.16 1941.7 1029.16 1945.08 1029.2 1948.46 1029.24 1951.84 1029.27 1955.3 1029.29 1958.77 1029.3 1962.23 1029.31 1971.34 1029.33 1980.45 1029.4 1989.55 1029.48 1998.85 1029.56 2008.15 1029.59 2017.45 1029.63 2035.68 1029.7 2053.91 1029.83 2072.15 1029.98 2068.1 1041.98 2064.06 1053.99 2060.02 1066 2041.53 1066.22 2023.04 1066.39 2004.56 1066.5 1995.97 1066.55 1987.39 1066.61 1978.8 1066.72 1968.93 1066.85 1959.06 1066.89 1949.18 1066.94 1946.1 1066.99 1943.02 1067.03 1939.85 1067.09 1935.55 1067.09 1935.55 1067.09 1931.17 1067.09 1928.65 1067.11 1926.13 1067.13 1923.53 1067.15 1915.24 1065.7 1912.41 1062.84 1907.24 1056.39 1905.36 1049.29 1906.27 1044.7 1907.24 1037.18 1914.88 1029.2 1921.51 1029.1 1932.29 1029.16Z" fill="#69D7E9" fill-rule="evenodd" fill-opacity="1"/><path d="M2026.67 873.654C2029.76 873.671 2032.86 873.688 2036.06 873.706 2044.27 873.752 2052.47 873.87 2060.68 874.004 2069.07 874.128 2077.46 874.183 2085.85 874.244 2102.29 874.377 2118.72 874.579 2135.16 874.833 2134.69 876.207 2134.22 877.58 2133.74 878.995 2129.88 890.425 2126.28 901.862 2123.06 913.488 2105.01 913.855 2086.96 914.109 2068.91 914.284 2062.77 914.357 2056.63 914.456 2050.5 914.581 2041.67 914.757 2032.85 914.84 2024.02 914.903 2021.27 914.978 2018.53 915.053 2015.7 915.13 1997.14 915.138 1997.14 915.138 1990.33 910.382 1985.56 904.899 1982.88 899.841 1981.16 892.802 1982.85 886.236 1985.24 882.088 1990.17 877.396 2000.97 871.389 2014.68 873.406 2026.67 873.654Z" fill="#7FE8E8" fill-rule="evenodd" fill-opacity="1"/><path d="M1913.53 1105.25C1916.22 1105.25 1918.92 1105.24 1921.7 1105.23 1927.38 1105.24 1933.07 1105.27 1938.76 1105.33 1947.47 1105.41 1956.18 1105.38 1964.9 1105.33 1970.43 1105.35 1975.95 1105.37 1981.48 1105.4 1984.09 1105.39 1986.7 1105.38 1989.39 1105.37 1993.03 1105.42 1993.03 1105.42 1996.74 1105.48 1999.94 1105.5 1999.94 1105.5 2003.2 1105.53 2010.81 1107.18 2013.75 1110.52 2018.62 1116.47 2019.99 1121.31 2019.99 1121.31 2019.98 1126.16 2020.03 1127.76 2020.09 1129.36 2020.14 1131.01 2017.62 1139.06 2013.73 1141.19 2006.54 1145.54 1997.49 1147.03 1988.52 1146.96 1979.36 1146.92 1976.72 1146.93 1974.09 1146.95 1971.37 1146.96 1965.81 1146.98 1960.24 1146.97 1954.68 1146.94 1946.17 1146.91 1937.66 1146.99 1929.15 1147.08 1923.74 1147.09 1918.33 1147.08 1912.92 1147.07 1910.37 1147.1 1907.83 1147.13 1905.21 1147.17 1887.61 1146.94 1887.61 1146.94 1878.09 1139.85 1873.65 1133.43 1873.65 1133.43 1872.14 1126.16 1876.78 1103.84 1894.84 1105.02 1913.53 1105.25Z" fill="#58C5E9" fill-rule="evenodd" fill-opacity="1"/><path d="M2097.24 1106.37C2099.9 1106.38 2102.56 1106.4 2105.29 1106.42 2113.76 1106.48 2122.23 1106.63 2130.7 1106.78 2136.45 1106.84 2142.19 1106.9 2147.94 1106.95 2162.02 1107.08 2176.09 1107.28 2190.17 1107.53 2185.96 1120.52 2181.36 1133.09 2175.72 1145.55 2160.84 1145.9 2145.97 1146.16 2131.09 1146.33 2126.03 1146.4 2120.97 1146.5 2115.91 1146.62 2108.64 1146.8 2101.36 1146.88 2094.08 1146.94 2091.83 1147.01 2089.57 1147.09 2087.24 1147.16 2071.91 1147.17 2071.91 1147.17 2065.59 1142.49 2060.81 1137.05 2057.9 1132.2 2056.17 1125.2 2061.58 1104.39 2079.4 1105.94 2097.24 1106.37Z" fill="#70D8E4" fill-rule="evenodd" fill-opacity="1"/><path d="M2011.84 951.436C2015.75 951.457 2015.75 951.457 2019.75 951.478 2028.07 951.532 2036.39 951.653 2044.71 951.776 2050.36 951.824 2056.01 951.869 2061.66 951.908 2075.49 952.015 2089.32 952.176 2103.16 952.38 2098.93 965.593 2094.32 978.379 2088.66 991.047 2073.7 991.286 2058.73 991.461 2043.77 991.578 2038.68 991.627 2033.58 991.693 2028.49 991.776 2021.18 991.893 2013.86 991.948 2006.54 991.991 2003.13 992.066 2003.13 992.066 1999.65 992.142 1983.84 992.148 1983.84 992.148 1976.08 986.457 1972.43 981.018 1971.3 978.21 1971.32 971.713 1971.26 970.118 1971.21 968.523 1971.16 966.88 1976.92 948.547 1996.36 951.13 2011.84 951.436Z" fill="#76DEE4" fill-rule="evenodd" fill-opacity="1"/><path d="M2091.61 796.255C2093.74 796.272 2095.88 796.289 2098.08 796.307 2104.87 796.373 2111.66 796.525 2118.45 796.678 2123.07 796.738 2127.68 796.793 2132.3 796.843 2143.59 796.975 2154.88 797.183 2166.17 797.431 2164.51 806.146 2162.27 813.891 2158.9 822.117 2157.62 825.265 2157.62 825.265 2156.32 828.477 2154.05 833.557 2154.05 833.557 2151.63 835.965 2145.45 836.298 2139.35 836.479 2133.17 836.539 2131.32 836.559 2129.46 836.578 2127.55 836.598 2123.61 836.632 2119.67 836.658 2115.73 836.676 2109.71 836.718 2103.7 836.823 2097.68 836.93 2093.86 836.954 2090.04 836.975 2086.21 836.991 2083.52 837.054 2083.52 837.054 2080.76 837.119 2072.38 837.084 2067.66 836.597 2060.82 831.602 2055.3 823.783 2055.62 818.791 2057.1 809.473 2067.1 795.805 2075.86 795.779 2091.61 796.255Z" fill="#88EEE8" fill-rule="evenodd" fill-opacity="1"/><path d="M1980.23 796.731C1983.33 796.745 1986.43 796.759 1989.63 796.774 2015.53 797.115 2015.53 797.115 2023.74 802.658 2029.39 811.148 2029.81 814.554 2028.59 824.543 2024.06 830.142 2020.45 833.487 2014.04 836.701 2004.08 837.382 1994.12 837.463 1984.15 837.552 1980.83 837.592 1977.52 837.659 1974.21 837.752 1969.42 837.885 1964.64 837.93 1959.85 837.974 1956.99 838.022 1954.12 838.07 1951.17 838.12 1943.69 836.701 1943.69 836.701 1937.91 830.998 1933.05 823.014 1932.26 819.146 1933.98 809.953 1944.67 792.74 1961.84 796.256 1980.23 796.731Z" fill="#87ECE8" fill-rule="evenodd" fill-opacity="1"/><path d="M1796.39 721.481C1801.44 721.501 1806.48 721.432 1811.53 721.356 1816.33 721.358 1816.33 721.358 1821.23 721.361 1824.15 721.356 1827.07 721.352 1830.08 721.347 1839.5 722.784 1843.54 725.399 1850.1 732.16 1851.47 740.567 1851.47 740.567 1850.1 748.974 1844.61 756.484 1841.67 758.381 1832.34 759.863 1827.63 759.903 1827.63 759.903 1822.83 759.943 1821.14 759.958 1819.46 759.972 1817.72 759.988 1814.17 760.005 1810.61 759.997 1807.06 759.967 1801.63 759.933 1796.21 760.015 1790.78 760.107 1787.32 760.109 1783.86 760.105 1780.4 760.093 1777.26 760.094 1774.12 760.096 1770.89 760.098 1762.82 758.582 1762.82 758.582 1756.88 753.027 1753.12 746.572 1753.12 746.572 1753.12 737.865 1759.23 717.439 1778.26 721.369 1796.39 721.481Z" fill="#89F0EB" fill-rule="evenodd" fill-opacity="1"/><path d="M2116.42 1184.19C2120.16 1184.23 2120.16 1184.23 2123.97 1184.26 2127.85 1184.36 2127.85 1184.36 2131.81 1184.45 2135.75 1184.5 2135.75 1184.5 2139.76 1184.55 2146.23 1184.63 2152.7 1184.75 2159.17 1184.9 2155.12 1196.94 2151.07 1208.98 2147.01 1221.02 2139.51 1221.38 2132.02 1221.59 2124.51 1221.78 2122.39 1221.88 2120.27 1221.98 2118.08 1222.08 2102.36 1222.38 2102.36 1222.38 2094.45 1215.5 2089.82 1206.58 2090.66 1201.63 2093.5 1192.12 2100.9 1184.93 2106.24 1184.03 2116.42 1184.19Z" fill="#5FCCE5" fill-rule="evenodd" fill-opacity="1"/></g></g></svg>
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment