commit 80513a3258afaf54d45f08a683363140a97f1210 Author: bdim404 Date: Mon Jan 26 15:34:31 2026 +0800 init commit Co-Authored-By: Claude Sonnet 4.5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..47fb659 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +.idea/ +.vscode/ +venv/ +env/ +Qwen/ +qwen3-tts-frontend/node_modules/ +qwen3-tts-frontend/dist/ +qwen3-tts-frontend/.env +qwen3-tts-frontend/.env.local \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1f3ccd4 --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +# Qwen3-TTS WebUI + +A text-to-speech web application based on Qwen3-TTS, supporting custom voice, voice design, and voice cloning. + +[中文文档](./README.zh.md) + +## Features + +- Custom Voice: Predefined speaker voices +- Voice Design: Create voices from natural language descriptions +- Voice Cloning: Clone voices from uploaded audio +- JWT auth, async tasks, voice cache, dark mode + +## Tech Stack + +Backend: FastAPI + SQLAlchemy + PyTorch + JWT +Frontend: React 19 + TypeScript + Vite + Tailwind + Shadcn/ui + +## Quick Start + +### Backend + +```bash +cd qwen3-tts-backend +python -m venv venv +source venv/bin/activate +pip install -r requirements.txt +cp .env.example .env +# Edit .env to configure MODEL_BASE_PATH etc. +uvicorn main:app --host 0.0.0.0 --port 8000 --reload +``` + +### Frontend + +```bash +cd qwen3-tts-frontend +npm install +cp .env.example .env +# Edit .env to configure VITE_API_URL +npm run dev +``` + +Visit `http://localhost:5173` + +## Configuration + +Backend `.env` key settings: + +```env +SECRET_KEY=your-secret-key +MODEL_DEVICE=cuda:0 +MODEL_BASE_PATH=../Qwen +DATABASE_URL=sqlite:///./qwen_tts.db +``` + +Frontend `.env`: + +```env +VITE_API_URL=http://localhost:8000 +``` + +## API + +``` +POST /auth/register - Register +POST /auth/token - Login +POST /tts/custom-voice - Custom voice +POST /tts/voice-design - Voice design +POST /tts/voice-clone - Voice cloning +GET /jobs - Job list +GET /jobs/{id}/download - Download result +``` + +## License + +MIT diff --git a/README.zh.md b/README.zh.md new file mode 100644 index 0000000..bceca4f --- /dev/null +++ b/README.zh.md @@ -0,0 +1,76 @@ +# Qwen3-TTS WebUI + +基于 Qwen3-TTS 的文本转语音 Web 应用,支持自定义语音、语音设计和语音克隆。 + +[English Documentation](./README.md) + +## 功能特性 + +- 自定义语音:预定义说话人语音 +- 语音设计:自然语言描述创建语音 +- 语音克隆:上传音频克隆语音 +- JWT 认证、异步任务、语音缓存、暗黑模式 + +## 技术栈 + +后端:FastAPI + SQLAlchemy + PyTorch + JWT +前端:React 19 + TypeScript + Vite + Tailwind + Shadcn/ui + +## 快速开始 + +### 后端 + +```bash +cd qwen3-tts-backend +python -m venv venv +source venv/bin/activate +pip install -r requirements.txt +cp .env.example .env +# 编辑 .env 配置 MODEL_BASE_PATH 等 +uvicorn main:app --host 0.0.0.0 --port 8000 --reload +``` + +### 前端 + +```bash +cd qwen3-tts-frontend +npm install +cp .env.example .env +# 编辑 .env 配置 VITE_API_URL +npm run dev +``` + +访问 `http://localhost:5173` + +## 配置 + +后端 `.env` 关键配置: + +```env +SECRET_KEY=your-secret-key +MODEL_DEVICE=cuda:0 +MODEL_BASE_PATH=../Qwen +DATABASE_URL=sqlite:///./qwen_tts.db +``` + +前端 `.env`: + +```env +VITE_API_URL=http://localhost:8000 +``` + +## API + +``` +POST /auth/register - 注册 +POST /auth/token - 登录 +POST /tts/custom-voice - 自定义语音 +POST /tts/voice-design - 语音设计 +POST /tts/voice-clone - 语音克隆 +GET /jobs - 任务列表 +GET /jobs/{id}/download - 下载结果 +``` + +## 许可证 + +MIT diff --git a/qwen3-tts-backend/.env.example b/qwen3-tts-backend/.env.example new file mode 100644 index 0000000..7d4e8c2 --- /dev/null +++ b/qwen3-tts-backend/.env.example @@ -0,0 +1,22 @@ +SECRET_KEY=your-secret-key-change-this-in-production +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=30 +DATABASE_URL=sqlite:///./qwen_tts.db +CACHE_DIR=./voice_cache +OUTPUT_DIR=./outputs +MODEL_DEVICE=cuda:0 +MODEL_BASE_PATH=../Qwen +MAX_CACHE_ENTRIES=100 +CACHE_TTL_DAYS=7 +HOST=0.0.0.0 +PORT=8000 +WORKERS=1 +LOG_LEVEL=info +LOG_FILE=./app.log +RATE_LIMIT_PER_MINUTE=50 +RATE_LIMIT_PER_HOUR=1000 +MAX_QUEUE_SIZE=100 +BATCH_SIZE=4 +BATCH_WAIT_TIME=0.5 +MAX_TEXT_LENGTH=1000 +MAX_AUDIO_SIZE_MB=10 diff --git a/qwen3-tts-backend/.gitignore b/qwen3-tts-backend/.gitignore new file mode 100644 index 0000000..768405c --- /dev/null +++ b/qwen3-tts-backend/.gitignore @@ -0,0 +1,11 @@ +.env +*.pyc +__pycache__/ +*.log +qwen_tts.db +voice_cache/ +outputs/ +venv/ +.pytest_cache/ +.coverage +htmlcov/ diff --git a/qwen3-tts-backend/api/__init__.py b/qwen3-tts-backend/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/qwen3-tts-backend/api/auth.py b/qwen3-tts-backend/api/auth.py new file mode 100644 index 0000000..e239ec2 --- /dev/null +++ b/qwen3-tts-backend/api/auth.py @@ -0,0 +1,107 @@ +from datetime import timedelta +from typing import Annotated +from fastapi import APIRouter, Depends, HTTPException, status, Request +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from sqlalchemy.orm import Session +from slowapi import Limiter +from slowapi.util import get_remote_address + +from config import settings +from core.security import ( + get_password_hash, + verify_password, + create_access_token, + decode_access_token +) +from db.database import get_db +from db.crud import get_user_by_username, get_user_by_email, create_user +from schemas.user import User, UserCreate, Token + +router = APIRouter(prefix="/auth", tags=["authentication"]) + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token") + +limiter = Limiter(key_func=get_remote_address) + +async def get_current_user( + token: Annotated[str, Depends(oauth2_scheme)], + db: Session = Depends(get_db) +) -> User: + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + username = decode_access_token(token) + if username is None: + raise credentials_exception + + user = get_user_by_username(db, username=username) + if user is None: + raise credentials_exception + + return user + +@router.post("/register", response_model=User, status_code=status.HTTP_201_CREATED) +@limiter.limit("5/minute") +async def register(request: Request, user_data: UserCreate, db: Session = Depends(get_db)): + existing_user = get_user_by_username(db, username=user_data.username) + if existing_user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Username already registered" + ) + + existing_email = get_user_by_email(db, email=user_data.email) + if existing_email: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Email already registered" + ) + + hashed_password = get_password_hash(user_data.password) + user = create_user( + db, + username=user_data.username, + email=user_data.email, + hashed_password=hashed_password + ) + + return user + +@router.post("/token", response_model=Token) +@limiter.limit("5/minute") +async def login( + request: Request, + form_data: Annotated[OAuth2PasswordRequestForm, Depends()], + db: Session = Depends(get_db) +): + user = get_user_by_username(db, username=form_data.username) + if not user or not verify_password(form_data.password, user.hashed_password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + + if not user.is_active: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Inactive user" + ) + + access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username}, expires_delta=access_token_expires + ) + + return {"access_token": access_token, "token_type": "bearer"} + +@router.get("/me", response_model=User) +@limiter.limit("30/minute") +async def get_current_user_info( + request: Request, + current_user: Annotated[User, Depends(get_current_user)] +): + return current_user diff --git a/qwen3-tts-backend/api/cache.py b/qwen3-tts-backend/api/cache.py new file mode 100644 index 0000000..e669396 --- /dev/null +++ b/qwen3-tts-backend/api/cache.py @@ -0,0 +1,156 @@ +import logging +import json +from pathlib import Path +from typing import Optional +from fastapi import APIRouter, Depends, HTTPException, Request +from sqlalchemy.orm import Session +from slowapi import Limiter +from slowapi.util import get_remote_address + +from core.config import settings +from core.database import get_db +from core.cache_manager import VoiceCacheManager +from api.auth import get_current_user +from db.crud import list_cache_entries, delete_cache_entry +from db.models import VoiceCache, User +from utils.metrics import cache_metrics + +logger = logging.getLogger(__name__) +router = APIRouter(prefix="/cache", tags=["cache"]) + +limiter = Limiter(key_func=get_remote_address) + + +@router.get("/voices") +@limiter.limit("30/minute") +async def list_user_caches( + request: Request, + skip: int = 0, + limit: int = 100, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + caches = list_cache_entries(db, current_user.id, skip=skip, limit=limit) + + result = [] + for cache in caches: + meta_data = json.loads(cache.meta_data) if cache.meta_data else {} + cache_file = Path(cache.cache_path) + file_size_mb = cache_file.stat().st_size / (1024 * 1024) if cache_file.exists() else 0 + + result.append({ + 'id': cache.id, + 'ref_audio_hash': cache.ref_audio_hash, + 'created_at': cache.created_at.isoformat(), + 'last_accessed': cache.last_accessed.isoformat(), + 'access_count': cache.access_count, + 'metadata': meta_data, + 'size_mb': round(file_size_mb, 2) + }) + + return { + 'caches': result, + 'total': len(result) + } + + +@router.delete("/voices/{cache_id}") +@limiter.limit("30/minute") +async def delete_user_cache( + request: Request, + cache_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + cache = db.query(VoiceCache).filter( + VoiceCache.id == cache_id, + VoiceCache.user_id == current_user.id + ).first() + + if not cache: + raise HTTPException(status_code=404, detail="Cache not found") + + cache_file = Path(cache.cache_path) + if cache_file.exists(): + cache_file.unlink() + + success = delete_cache_entry(db, cache_id, current_user.id) + + if not success: + raise HTTPException(status_code=500, detail="Failed to delete cache") + + logger.info(f"Cache deleted: id={cache_id}, user={current_user.id}") + + return { + 'message': 'Cache deleted successfully', + 'cache_id': cache_id + } + + +@router.delete("/voices") +@limiter.limit("10/minute") +async def cleanup_expired_caches( + request: Request, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + cache_manager = await VoiceCacheManager.get_instance() + deleted_count = await cache_manager.cleanup_expired(db) + + logger.info(f"Expired cache cleanup: user={current_user.id}, deleted={deleted_count}") + + return { + 'message': 'Expired caches cleaned up', + 'deleted_count': deleted_count + } + + +@router.post("/voices/prune") +@limiter.limit("10/minute") +async def prune_caches( + request: Request, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + cache_manager = await VoiceCacheManager.get_instance() + deleted_count = await cache_manager.enforce_max_entries(current_user.id, db) + + logger.info(f"LRU prune: user={current_user.id}, deleted={deleted_count}") + + return { + 'message': 'LRU pruning completed', + 'deleted_count': deleted_count + } + + +@router.get("/stats") +@limiter.limit("30/minute") +async def get_cache_stats( + request: Request, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + stats = cache_metrics.get_stats(db, settings.CACHE_DIR) + + user_stats = None + for user_stat in stats['users']: + if user_stat['user_id'] == current_user.id: + user_stats = user_stat + break + + if user_stats is None: + user_cache_count = db.query(VoiceCache).filter( + VoiceCache.user_id == current_user.id + ).count() + user_stats = { + 'user_id': current_user.id, + 'hits': 0, + 'misses': 0, + 'hit_rate': 0.0, + 'cache_entries': user_cache_count + } + + return { + 'global': stats['global'], + 'user': user_stats + } diff --git a/qwen3-tts-backend/api/jobs.py b/qwen3-tts-backend/api/jobs.py new file mode 100644 index 0000000..34319e3 --- /dev/null +++ b/qwen3-tts-backend/api/jobs.py @@ -0,0 +1,176 @@ +import logging +from pathlib import Path +from typing import Optional +from fastapi import APIRouter, Depends, HTTPException, Query, Request +from fastapi.responses import FileResponse +from sqlalchemy.orm import Session +from slowapi import Limiter +from slowapi.util import get_remote_address + +from core.database import get_db +from core.config import settings +from db.models import Job, JobStatus, User +from api.auth import get_current_user + +logger = logging.getLogger(__name__) +router = APIRouter(prefix="/jobs", tags=["jobs"]) + +limiter = Limiter(key_func=get_remote_address) + + +@router.get("/{job_id}") +@limiter.limit("30/minute") +async def get_job( + request: Request, + job_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + job = db.query(Job).filter(Job.id == job_id).first() + + if not job: + raise HTTPException(status_code=404, detail="Job not found") + + if job.user_id != current_user.id: + raise HTTPException(status_code=403, detail="Access denied") + + download_url = None + if job.status == JobStatus.COMPLETED and job.output_path: + output_file = Path(job.output_path) + if output_file.exists(): + download_url = f"{settings.BASE_URL}/jobs/{job.id}/download" + + return { + "id": job.id, + "job_type": job.job_type, + "status": job.status, + "input_params": job.input_params, + "output_path": job.output_path, + "download_url": download_url, + "error_message": job.error_message, + "created_at": job.created_at.isoformat() + 'Z' if job.created_at else None, + "started_at": job.started_at.isoformat() + 'Z' if job.started_at else None, + "completed_at": job.completed_at.isoformat() + 'Z' if job.completed_at else None + } + + +@router.get("") +@limiter.limit("30/minute") +async def list_jobs( + request: Request, + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=100), + status: Optional[str] = Query(None), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + query = db.query(Job).filter(Job.user_id == current_user.id) + + if status: + try: + status_enum = JobStatus(status) + query = query.filter(Job.status == status_enum) + except ValueError: + raise HTTPException(status_code=400, detail=f"Invalid status: {status}") + + total = query.count() + jobs = query.order_by(Job.created_at.desc()).offset(skip).limit(limit).all() + + jobs_data = [] + for job in jobs: + download_url = None + if job.status == JobStatus.COMPLETED and job.output_path: + output_file = Path(job.output_path) + if output_file.exists(): + download_url = f"{settings.BASE_URL}/jobs/{job.id}/download" + + jobs_data.append({ + "id": job.id, + "job_type": job.job_type, + "status": job.status, + "input_params": job.input_params, + "output_path": job.output_path, + "download_url": download_url, + "error_message": job.error_message, + "created_at": job.created_at.isoformat() + 'Z' if job.created_at else None, + "completed_at": job.completed_at.isoformat() + 'Z' if job.completed_at else None + }) + + return { + "total": total, + "skip": skip, + "limit": limit, + "jobs": jobs_data + } + + +@router.delete("/{job_id}") +@limiter.limit("30/minute") +async def delete_job( + request: Request, + job_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + job = db.query(Job).filter(Job.id == job_id).first() + + if not job: + raise HTTPException(status_code=404, detail="Job not found") + + if job.user_id != current_user.id: + raise HTTPException(status_code=403, detail="Access denied") + + if job.output_path: + output_file = Path(job.output_path) + if output_file.exists(): + try: + output_file.unlink() + logger.info(f"Deleted output file: {output_file}") + except Exception as e: + logger.error(f"Failed to delete output file {output_file}: {e}") + + db.delete(job) + db.commit() + + return {"message": "Job deleted successfully"} + + +@router.get("/{job_id}/download") +@limiter.limit("30/minute") +async def download_job_output( + request: Request, + job_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + job = db.query(Job).filter(Job.id == job_id).first() + + if not job: + raise HTTPException(status_code=404, detail="Job not found") + + if job.user_id != current_user.id: + raise HTTPException(status_code=403, detail="Access denied") + + if job.status != JobStatus.COMPLETED: + raise HTTPException(status_code=400, detail="Job not completed yet") + + if not job.output_path: + raise HTTPException(status_code=404, detail="Output file not found") + + output_file = Path(job.output_path) + if not output_file.exists(): + raise HTTPException(status_code=404, detail="Output file does not exist") + + output_dir = Path(settings.OUTPUT_DIR).resolve() + if not output_file.resolve().is_relative_to(output_dir): + logger.warning(f"Path traversal attempt detected: {output_file}") + raise HTTPException(status_code=403, detail="Access denied") + + return FileResponse( + path=str(output_file), + media_type="audio/wav", + filename=output_file.name, + headers={ + "Content-Disposition": f'attachment; filename="{output_file.name}"' + } + ) diff --git a/qwen3-tts-backend/api/metrics.py b/qwen3-tts-backend/api/metrics.py new file mode 100644 index 0000000..728f270 --- /dev/null +++ b/qwen3-tts-backend/api/metrics.py @@ -0,0 +1,21 @@ +import logging +from fastapi import APIRouter + +from core.metrics import MetricsCollector + +logger = logging.getLogger(__name__) +router = APIRouter(prefix="/metrics", tags=["metrics"]) + + +@router.get("") +async def get_metrics(): + metrics = await MetricsCollector.get_instance() + data = await metrics.get_metrics() + return data + + +@router.post("/reset") +async def reset_metrics(): + metrics = await MetricsCollector.get_instance() + await metrics.reset() + return {"message": "Metrics reset successfully"} diff --git a/qwen3-tts-backend/api/tts.py b/qwen3-tts-backend/api/tts.py new file mode 100644 index 0000000..004f84b --- /dev/null +++ b/qwen3-tts-backend/api/tts.py @@ -0,0 +1,553 @@ +import logging +import tempfile +from datetime import datetime +from pathlib import Path +from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks, UploadFile, File, Form, Request +from sqlalchemy.orm import Session +from typing import Optional +from slowapi import Limiter +from slowapi.util import get_remote_address + +from core.config import settings +from core.database import get_db +from core.model_manager import ModelManager +from core.cache_manager import VoiceCacheManager +from db.models import Job, JobStatus, User +from schemas.tts import CustomVoiceRequest, VoiceDesignRequest +from api.auth import get_current_user +from utils.validation import ( + validate_language, + validate_speaker, + validate_text_length, + validate_generation_params, + get_supported_languages, + get_supported_speakers +) +from utils.audio import save_audio_file, validate_ref_audio, process_ref_audio, extract_audio_features +from utils.metrics import cache_metrics + +logger = logging.getLogger(__name__) +router = APIRouter(prefix="/tts", tags=["tts"]) + +limiter = Limiter(key_func=get_remote_address) + + +async def process_custom_voice_job( + job_id: int, + user_id: int, + request_data: dict, + db_url: str +): + from core.database import SessionLocal + + db = SessionLocal() + try: + job = db.query(Job).filter(Job.id == job_id).first() + if not job: + logger.error(f"Job {job_id} not found") + return + + job.status = JobStatus.PROCESSING + job.started_at = datetime.utcnow() + db.commit() + + logger.info(f"Processing custom-voice job {job_id}") + + model_manager = await ModelManager.get_instance() + await model_manager.load_model("custom-voice") + _, tts = await model_manager.get_current_model() + + if tts is None: + raise RuntimeError("Failed to load custom-voice model") + + result = tts.generate_custom_voice( + text=request_data['text'], + language=request_data['language'], + speaker=request_data['speaker'], + instruct=request_data.get('instruct', ''), + max_new_tokens=request_data['max_new_tokens'], + temperature=request_data['temperature'], + top_k=request_data['top_k'], + top_p=request_data['top_p'], + repetition_penalty=request_data['repetition_penalty'] + ) + + import numpy as np + if isinstance(result, tuple): + audio_data = result[0] + elif isinstance(result, list): + audio_data = np.array(result) + else: + audio_data = result + + from pathlib import Path + + timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S") + filename = f"{user_id}_{job_id}_{timestamp}.wav" + output_path = Path(settings.OUTPUT_DIR) / filename + + save_audio_file(audio_data, 24000, output_path) + + job.status = JobStatus.COMPLETED + job.output_path = str(output_path) + job.completed_at = datetime.utcnow() + db.commit() + + logger.info(f"Job {job_id} completed successfully") + + except Exception as e: + logger.error(f"Job {job_id} failed: {e}", exc_info=True) + job = db.query(Job).filter(Job.id == job_id).first() + if job: + job.status = JobStatus.FAILED + job.error_message = str(e) + job.completed_at = datetime.utcnow() + db.commit() + + finally: + db.close() + + +async def process_voice_design_job( + job_id: int, + user_id: int, + request_data: dict, + db_url: str +): + from core.database import SessionLocal + + db = SessionLocal() + try: + job = db.query(Job).filter(Job.id == job_id).first() + if not job: + logger.error(f"Job {job_id} not found") + return + + job.status = JobStatus.PROCESSING + job.started_at = datetime.utcnow() + db.commit() + + logger.info(f"Processing voice-design job {job_id}") + + model_manager = await ModelManager.get_instance() + await model_manager.load_model("voice-design") + _, tts = await model_manager.get_current_model() + + if tts is None: + raise RuntimeError("Failed to load voice-design model") + + result = tts.generate_voice_design( + text=request_data['text'], + language=request_data['language'], + instruct=request_data['instruct'], + max_new_tokens=request_data['max_new_tokens'], + temperature=request_data['temperature'], + top_k=request_data['top_k'], + top_p=request_data['top_p'], + repetition_penalty=request_data['repetition_penalty'] + ) + + import numpy as np + if isinstance(result, tuple): + audio_data = result[0] + elif isinstance(result, list): + audio_data = np.array(result) + else: + audio_data = result + + from pathlib import Path + + timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S") + filename = f"{user_id}_{job_id}_{timestamp}.wav" + output_path = Path(settings.OUTPUT_DIR) / filename + + save_audio_file(audio_data, 24000, output_path) + + job.status = JobStatus.COMPLETED + job.output_path = str(output_path) + job.completed_at = datetime.utcnow() + db.commit() + + logger.info(f"Job {job_id} completed successfully") + + except Exception as e: + logger.error(f"Job {job_id} failed: {e}", exc_info=True) + job = db.query(Job).filter(Job.id == job_id).first() + if job: + job.status = JobStatus.FAILED + job.error_message = str(e) + job.completed_at = datetime.utcnow() + db.commit() + + finally: + db.close() + + +async def process_voice_clone_job( + job_id: int, + user_id: int, + request_data: dict, + ref_audio_path: str, + db_url: str +): + from core.database import SessionLocal + import numpy as np + + db = SessionLocal() + try: + job = db.query(Job).filter(Job.id == job_id).first() + if not job: + logger.error(f"Job {job_id} not found") + return + + job.status = JobStatus.PROCESSING + job.started_at = datetime.utcnow() + db.commit() + + logger.info(f"Processing voice-clone job {job_id}") + + with open(ref_audio_path, 'rb') as f: + ref_audio_data = f.read() + + cache_manager = await VoiceCacheManager.get_instance() + ref_audio_hash = cache_manager.get_audio_hash(ref_audio_data) + + x_vector = None + cache_id = None + + if request_data.get('use_cache', True): + cached = await cache_manager.get_cache(user_id, ref_audio_hash, db) + if cached: + x_vector = cached['data'] + cache_id = cached['cache_id'] + cache_metrics.record_hit(user_id) + logger.info(f"Cache hit for job {job_id}, cache_id={cache_id}") + + if x_vector is None: + cache_metrics.record_miss(user_id) + logger.info(f"Cache miss for job {job_id}, creating voice clone prompt") + ref_audio_array, ref_sr = process_ref_audio(ref_audio_data) + + model_manager = await ModelManager.get_instance() + await model_manager.load_model("base") + _, tts = await model_manager.get_current_model() + + if tts is None: + raise RuntimeError("Failed to load base model") + + x_vector = tts.create_voice_clone_prompt( + ref_audio=(ref_audio_array, ref_sr), + ref_text=request_data.get('ref_text', ''), + x_vector_only_mode=request_data.get('x_vector_only_mode', False) + ) + + if request_data.get('use_cache', True): + features = extract_audio_features(ref_audio_array, ref_sr) + metadata = { + 'duration': features['duration'], + 'sample_rate': features['sample_rate'], + 'ref_text': request_data.get('ref_text', ''), + 'x_vector_only_mode': request_data.get('x_vector_only_mode', False) + } + cache_id = await cache_manager.set_cache( + user_id, ref_audio_hash, x_vector, metadata, db + ) + logger.info(f"Created cache for job {job_id}, cache_id={cache_id}") + + if request_data.get('x_vector_only_mode', False): + job.status = JobStatus.COMPLETED + job.output_path = f"x_vector_cached_{cache_id}" + job.completed_at = datetime.utcnow() + db.commit() + logger.info(f"Job {job_id} completed (x_vector_only_mode)") + return + + model_manager = await ModelManager.get_instance() + await model_manager.load_model("base") + _, tts = await model_manager.get_current_model() + + if tts is None: + raise RuntimeError("Failed to load base model") + + wavs, sample_rate = tts.generate_voice_clone( + text=request_data['text'], + language=request_data['language'], + voice_clone_prompt=x_vector, + max_new_tokens=request_data['max_new_tokens'], + temperature=request_data['temperature'], + top_k=request_data['top_k'], + top_p=request_data['top_p'], + repetition_penalty=request_data['repetition_penalty'] + ) + + audio_data = wavs[0] if isinstance(wavs, list) else wavs + + timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S") + filename = f"{user_id}_{job_id}_{timestamp}.wav" + output_path = Path(settings.OUTPUT_DIR) / filename + + save_audio_file(audio_data, sample_rate, output_path) + + job.status = JobStatus.COMPLETED + job.output_path = str(output_path) + job.completed_at = datetime.utcnow() + db.commit() + + logger.info(f"Job {job_id} completed successfully") + + except Exception as e: + logger.error(f"Job {job_id} failed: {e}", exc_info=True) + job = db.query(Job).filter(Job.id == job_id).first() + if job: + job.status = JobStatus.FAILED + job.error_message = str(e) + job.completed_at = datetime.utcnow() + db.commit() + + finally: + if Path(ref_audio_path).exists(): + Path(ref_audio_path).unlink() + db.close() + + +@router.post("/custom-voice") +@limiter.limit("10/minute") +async def create_custom_voice_job( + request: Request, + req_data: CustomVoiceRequest, + background_tasks: BackgroundTasks, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + try: + validate_text_length(req_data.text) + language = validate_language(req_data.language) + speaker = validate_speaker(req_data.speaker) + + params = validate_generation_params({ + 'max_new_tokens': req_data.max_new_tokens, + 'temperature': req_data.temperature, + 'top_k': req_data.top_k, + 'top_p': req_data.top_p, + 'repetition_penalty': req_data.repetition_penalty + }) + + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + job = Job( + user_id=current_user.id, + job_type="custom-voice", + status=JobStatus.PENDING, + input_data="", + input_params={ + "text": req_data.text, + "language": language, + "speaker": speaker, + "instruct": req_data.instruct or "", + **params + } + ) + db.add(job) + db.commit() + db.refresh(job) + + request_data = { + "text": req_data.text, + "language": language, + "speaker": speaker, + "instruct": req_data.instruct or "", + **params + } + + background_tasks.add_task( + process_custom_voice_job, + job.id, + current_user.id, + request_data, + str(settings.DATABASE_URL) + ) + + return { + "job_id": job.id, + "status": job.status, + "message": "Job created successfully" + } + + +@router.post("/voice-design") +@limiter.limit("10/minute") +async def create_voice_design_job( + request: Request, + req_data: VoiceDesignRequest, + background_tasks: BackgroundTasks, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + try: + validate_text_length(req_data.text) + language = validate_language(req_data.language) + + if not req_data.instruct or not req_data.instruct.strip(): + raise ValueError("Instruct parameter is required for voice design") + + params = validate_generation_params({ + 'max_new_tokens': req_data.max_new_tokens, + 'temperature': req_data.temperature, + 'top_k': req_data.top_k, + 'top_p': req_data.top_p, + 'repetition_penalty': req_data.repetition_penalty + }) + + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + job = Job( + user_id=current_user.id, + job_type="voice-design", + status=JobStatus.PENDING, + input_data="", + input_params={ + "text": req_data.text, + "language": language, + "instruct": req_data.instruct, + **params + } + ) + db.add(job) + db.commit() + db.refresh(job) + + request_data = { + "text": req_data.text, + "language": language, + "instruct": req_data.instruct, + **params + } + + background_tasks.add_task( + process_voice_design_job, + job.id, + current_user.id, + request_data, + str(settings.DATABASE_URL) + ) + + return { + "job_id": job.id, + "status": job.status, + "message": "Job created successfully" + } + + +@router.post("/voice-clone") +@limiter.limit("10/minute") +async def create_voice_clone_job( + request: Request, + text: str = Form(...), + language: str = Form(default="Auto"), + ref_audio: UploadFile = File(...), + ref_text: Optional[str] = Form(default=None), + use_cache: bool = Form(default=True), + x_vector_only_mode: bool = Form(default=False), + max_new_tokens: Optional[int] = Form(default=2048), + temperature: Optional[float] = Form(default=0.9), + top_k: Optional[int] = Form(default=50), + top_p: Optional[float] = Form(default=1.0), + repetition_penalty: Optional[float] = Form(default=1.05), + background_tasks: BackgroundTasks = None, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + try: + validate_text_length(text) + language = validate_language(language) + + params = validate_generation_params({ + 'max_new_tokens': max_new_tokens, + 'temperature': temperature, + 'top_k': top_k, + 'top_p': top_p, + 'repetition_penalty': repetition_penalty + }) + + ref_audio_data = await ref_audio.read() + + if not validate_ref_audio(ref_audio_data, max_size_mb=settings.MAX_AUDIO_SIZE_MB): + raise ValueError("Invalid reference audio: must be 1-30s duration and ≤10MB") + + cache_manager = await VoiceCacheManager.get_instance() + ref_audio_hash = cache_manager.get_audio_hash(ref_audio_data) + + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + job = Job( + user_id=current_user.id, + job_type="voice-clone", + status=JobStatus.PENDING, + input_data="", + input_params={ + "text": text, + "language": language, + "ref_text": ref_text or "", + "ref_audio_hash": ref_audio_hash, + "use_cache": use_cache, + "x_vector_only_mode": x_vector_only_mode, + **params + } + ) + db.add(job) + db.commit() + db.refresh(job) + + with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as tmp_file: + tmp_file.write(ref_audio_data) + tmp_audio_path = tmp_file.name + + request_data = { + "text": text, + "language": language, + "ref_text": ref_text or "", + "use_cache": use_cache, + "x_vector_only_mode": x_vector_only_mode, + **params + } + + background_tasks.add_task( + process_voice_clone_job, + job.id, + current_user.id, + request_data, + tmp_audio_path, + str(settings.DATABASE_URL) + ) + + existing_cache = await cache_manager.get_cache(current_user.id, ref_audio_hash, db) + cache_info = {"cache_id": existing_cache['cache_id']} if existing_cache else None + + return { + "job_id": job.id, + "status": job.status, + "message": "Job created successfully", + "cache_info": cache_info + } + + +@router.get("/models") +@limiter.limit("30/minute") +async def list_models(request: Request): + model_manager = await ModelManager.get_instance() + return model_manager.get_model_info() + + +@router.get("/speakers") +@limiter.limit("30/minute") +async def list_speakers(request: Request): + return get_supported_speakers() + + +@router.get("/languages") +@limiter.limit("30/minute") +async def list_languages(request: Request): + return get_supported_languages() diff --git a/qwen3-tts-backend/api/users.py b/qwen3-tts-backend/api/users.py new file mode 100644 index 0000000..2a5e074 --- /dev/null +++ b/qwen3-tts-backend/api/users.py @@ -0,0 +1,169 @@ +from typing import Annotated +from fastapi import APIRouter, Depends, HTTPException, status, Request +from sqlalchemy.orm import Session +from slowapi import Limiter +from slowapi.util import get_remote_address + +from api.auth import get_current_user +from config import settings +from core.security import get_password_hash +from db.database import get_db +from db.crud import ( + get_user_by_id, + get_user_by_username, + get_user_by_email, + list_users, + create_user_by_admin, + update_user, + delete_user +) +from schemas.user import User, UserCreateByAdmin, UserUpdate, UserListResponse + +router = APIRouter(prefix="/users", tags=["users"]) +limiter = Limiter(key_func=get_remote_address) + +async def require_superuser( + current_user: Annotated[User, Depends(get_current_user)] +) -> User: + if not current_user.is_superuser: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Superuser access required" + ) + return current_user + +@router.get("", response_model=UserListResponse) +@limiter.limit("30/minute") +async def get_users( + request: Request, + skip: int = 0, + limit: int = 100, + db: Session = Depends(get_db), + _: User = Depends(require_superuser) +): + users, total = list_users(db, skip=skip, limit=limit) + return UserListResponse(users=users, total=total, skip=skip, limit=limit) + +@router.post("", response_model=User, status_code=status.HTTP_201_CREATED) +@limiter.limit("10/minute") +async def create_user( + request: Request, + user_data: UserCreateByAdmin, + db: Session = Depends(get_db), + _: User = Depends(require_superuser) +): + existing_user = get_user_by_username(db, username=user_data.username) + if existing_user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Username already registered" + ) + + existing_email = get_user_by_email(db, email=user_data.email) + if existing_email: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Email already registered" + ) + + hashed_password = get_password_hash(user_data.password) + user = create_user_by_admin( + db, + username=user_data.username, + email=user_data.email, + hashed_password=hashed_password, + is_superuser=user_data.is_superuser + ) + + return user + +@router.get("/{user_id}", response_model=User) +@limiter.limit("30/minute") +async def get_user( + request: Request, + user_id: int, + db: Session = Depends(get_db), + _: User = Depends(require_superuser) +): + user = get_user_by_id(db, user_id=user_id) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + return user + +@router.put("/{user_id}", response_model=User) +@limiter.limit("10/minute") +async def update_user_info( + request: Request, + user_id: int, + user_data: UserUpdate, + db: Session = Depends(get_db), + current_user: User = Depends(require_superuser) +): + existing_user = get_user_by_id(db, user_id=user_id) + if not existing_user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + if user_data.username is not None: + username_exists = get_user_by_username(db, username=user_data.username) + if username_exists and username_exists.id != user_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Username already taken" + ) + + if user_data.email is not None: + email_exists = get_user_by_email(db, email=user_data.email) + if email_exists and email_exists.id != user_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Email already taken" + ) + + hashed_password = None + if user_data.password is not None: + hashed_password = get_password_hash(user_data.password) + + user = update_user( + db, + user_id=user_id, + username=user_data.username, + email=user_data.email, + hashed_password=hashed_password, + is_active=user_data.is_active, + is_superuser=user_data.is_superuser + ) + + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + return user + +@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT) +@limiter.limit("10/minute") +async def delete_user_by_id( + request: Request, + user_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(require_superuser) +): + if user_id == current_user.id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Cannot delete yourself" + ) + + success = delete_user(db, user_id=user_id) + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) diff --git a/qwen3-tts-backend/config.py b/qwen3-tts-backend/config.py new file mode 100644 index 0000000..24c2572 --- /dev/null +++ b/qwen3-tts-backend/config.py @@ -0,0 +1,66 @@ +import os +from pathlib import Path +from typing import Optional +from pydantic_settings import BaseSettings +from pydantic import Field, field_validator + +class Settings(BaseSettings): + SECRET_KEY: str = Field(default="your-secret-key-change-this-in-production") + ALGORITHM: str = Field(default="HS256") + ACCESS_TOKEN_EXPIRE_MINUTES: int = Field(default=30) + + DATABASE_URL: str = Field(default="sqlite:///./qwen_tts.db") + CACHE_DIR: str = Field(default="./voice_cache") + OUTPUT_DIR: str = Field(default="./outputs") + BASE_URL: str = Field(default="http://localhost:8000") + + MODEL_DEVICE: str = Field(default="cuda:0") + MODEL_BASE_PATH: str = Field(default="../Qwen") + + MAX_CACHE_ENTRIES: int = Field(default=100) + CACHE_TTL_DAYS: int = Field(default=7) + + HOST: str = Field(default="0.0.0.0") + PORT: int = Field(default=8000) + WORKERS: int = Field(default=1) + LOG_LEVEL: str = Field(default="info") + LOG_FILE: str = Field(default="./app.log") + + RATE_LIMIT_PER_MINUTE: int = Field(default=50) + RATE_LIMIT_PER_HOUR: int = Field(default=1000) + + MAX_QUEUE_SIZE: int = Field(default=100) + BATCH_SIZE: int = Field(default=4) + BATCH_WAIT_TIME: float = Field(default=0.5) + + MAX_TEXT_LENGTH: int = Field(default=1000) + MAX_AUDIO_SIZE_MB: int = Field(default=10) + + class Config: + env_file = ".env" + case_sensitive = True + + @field_validator('MODEL_BASE_PATH') + @classmethod + def validate_model_path(cls, v: str) -> str: + path = Path(v) + if not path.exists(): + raise ValueError(f"Model base path does not exist: {v}") + return v + + def validate(self): + if self.SECRET_KEY == "your-secret-key-change-this-in-production": + import warnings + warnings.warn("Using default SECRET_KEY! Change this in production!") + + Path(self.CACHE_DIR).mkdir(parents=True, exist_ok=True) + Path(self.OUTPUT_DIR).mkdir(parents=True, exist_ok=True) + + if self.WORKERS > 1: + import warnings + warnings.warn("WORKERS > 1 not recommended for GPU models. Setting to 1.") + self.WORKERS = 1 + + return True + +settings = Settings() diff --git a/qwen3-tts-backend/core/__init__.py b/qwen3-tts-backend/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/qwen3-tts-backend/core/batch_processor.py b/qwen3-tts-backend/core/batch_processor.py new file mode 100644 index 0000000..8465b91 --- /dev/null +++ b/qwen3-tts-backend/core/batch_processor.py @@ -0,0 +1,141 @@ +import asyncio +import logging +import time +from typing import Any, Callable, Dict, List, Optional, Tuple +from dataclasses import dataclass +from collections import deque + +from core.config import settings + +logger = logging.getLogger(__name__) + + +@dataclass +class BatchRequest: + request_id: str + data: Dict[str, Any] + future: asyncio.Future + timestamp: float + + +class BatchProcessor: + _instance: Optional['BatchProcessor'] = None + _lock = asyncio.Lock() + + def __init__(self, batch_size: int = None, batch_wait_time: float = None): + self.batch_size = batch_size or settings.BATCH_SIZE + self.batch_wait_time = batch_wait_time or settings.BATCH_WAIT_TIME + self.queue: deque = deque() + self.queue_lock = asyncio.Lock() + self.processing = False + self._processor_task: Optional[asyncio.Task] = None + logger.info(f"BatchProcessor initialized with batch_size={self.batch_size}, wait_time={self.batch_wait_time}s") + + @classmethod + async def get_instance(cls) -> 'BatchProcessor': + if cls._instance is None: + async with cls._lock: + if cls._instance is None: + cls._instance = cls() + cls._instance._start_processor() + return cls._instance + + def _start_processor(self): + if not self._processor_task or self._processor_task.done(): + self._processor_task = asyncio.create_task(self._process_batches()) + logger.info("Batch processor task started") + + async def _process_batches(self): + logger.info("Batch processing loop started") + while True: + try: + await asyncio.sleep(0.1) + + async with self.queue_lock: + if not self.queue: + continue + + current_time = time.time() + oldest_request = self.queue[0] + wait_duration = current_time - oldest_request.timestamp + + should_process = ( + len(self.queue) >= self.batch_size or + wait_duration >= self.batch_wait_time + ) + + if should_process: + batch = [] + for _ in range(min(self.batch_size, len(self.queue))): + if self.queue: + batch.append(self.queue.popleft()) + + if batch: + logger.info(f"Processing batch of {len(batch)} requests (queue_wait={wait_duration:.3f}s)") + asyncio.create_task(self._process_batch(batch)) + + except Exception as e: + logger.error(f"Error in batch processor loop: {e}", exc_info=True) + await asyncio.sleep(1) + + async def _process_batch(self, batch: List[BatchRequest]): + for request in batch: + try: + if not request.future.done(): + result = await self._execute_single_request(request.data) + request.future.set_result(result) + except Exception as e: + logger.error(f"Error processing request {request.request_id}: {e}", exc_info=True) + if not request.future.done(): + request.future.set_exception(e) + + async def _execute_single_request(self, data: Dict[str, Any]) -> Any: + raise NotImplementedError("Subclass must implement _execute_single_request") + + async def submit(self, request_id: str, data: Dict[str, Any], timeout: float = 300) -> Any: + future = asyncio.Future() + request = BatchRequest( + request_id=request_id, + data=data, + future=future, + timestamp=time.time() + ) + + async with self.queue_lock: + self.queue.append(request) + queue_size = len(self.queue) + + logger.debug(f"Request {request_id} queued (queue_size={queue_size})") + + try: + result = await asyncio.wait_for(future, timeout=timeout) + return result + except asyncio.TimeoutError: + logger.error(f"Request {request_id} timed out after {timeout}s") + async with self.queue_lock: + if request in self.queue: + self.queue.remove(request) + raise TimeoutError(f"Request timed out after {timeout}s") + + async def get_queue_length(self) -> int: + async with self.queue_lock: + return len(self.queue) + + async def get_stats(self) -> Dict[str, Any]: + queue_length = await self.get_queue_length() + return { + "queue_length": queue_length, + "batch_size": self.batch_size, + "batch_wait_time": self.batch_wait_time, + "processor_running": self._processor_task is not None and not self._processor_task.done() + } + + +class TTSBatchProcessor(BatchProcessor): + + def __init__(self, process_func: Callable, batch_size: int = None, batch_wait_time: float = None): + super().__init__(batch_size, batch_wait_time) + self.process_func = process_func + + async def _execute_single_request(self, data: Dict[str, Any]) -> Any: + return await self.process_func(**data) diff --git a/qwen3-tts-backend/core/cache_manager.py b/qwen3-tts-backend/core/cache_manager.py new file mode 100644 index 0000000..f9f6a01 --- /dev/null +++ b/qwen3-tts-backend/core/cache_manager.py @@ -0,0 +1,161 @@ +import hashlib +import pickle +import asyncio +from pathlib import Path +from typing import Optional, Dict, Any +from datetime import datetime, timedelta +import logging + +from sqlalchemy.orm import Session +from db.crud import ( + create_cache_entry, + get_cache_entry, + list_cache_entries, + delete_cache_entry +) +from db.models import VoiceCache +from core.config import settings + +logger = logging.getLogger(__name__) + + +class VoiceCacheManager: + _instance = None + _lock = asyncio.Lock() + + def __init__(self, cache_dir: str, max_entries: int, ttl_days: int): + self.cache_dir = Path(cache_dir) + self.max_entries = max_entries + self.ttl_days = ttl_days + self.cache_dir.mkdir(parents=True, exist_ok=True) + logger.info(f"VoiceCacheManager initialized: dir={cache_dir}, max={max_entries}, ttl={ttl_days}d") + + @classmethod + async def get_instance(cls) -> 'VoiceCacheManager': + if cls._instance is None: + async with cls._lock: + if cls._instance is None: + cls._instance = VoiceCacheManager( + cache_dir=settings.CACHE_DIR, + max_entries=settings.MAX_CACHE_ENTRIES, + ttl_days=settings.CACHE_TTL_DAYS + ) + return cls._instance + + def get_audio_hash(self, audio_data: bytes) -> str: + return hashlib.sha256(audio_data).hexdigest() + + async def get_cache(self, user_id: int, ref_audio_hash: str, db: Session) -> Optional[Dict[str, Any]]: + try: + cache_entry = get_cache_entry(db, user_id, ref_audio_hash) + if not cache_entry: + logger.debug(f"Cache miss: user={user_id}, hash={ref_audio_hash[:8]}...") + return None + + cache_file = Path(cache_entry.cache_path) + if not cache_file.exists(): + logger.warning(f"Cache file missing: {cache_file}") + delete_cache_entry(db, cache_entry.id, user_id) + return None + + with open(cache_file, 'rb') as f: + cache_data = pickle.load(f) + + logger.info(f"Cache hit: user={user_id}, hash={ref_audio_hash[:8]}..., access_count={cache_entry.access_count}") + return { + 'cache_id': cache_entry.id, + 'data': cache_data, + 'metadata': cache_entry.meta_data + } + + except Exception as e: + logger.error(f"Cache retrieval error: {e}", exc_info=True) + return None + + async def set_cache( + self, + user_id: int, + ref_audio_hash: str, + cache_data: Any, + metadata: Dict[str, Any], + db: Session + ) -> str: + async with self._lock: + try: + cache_filename = f"{user_id}_{ref_audio_hash}.pkl" + cache_path = self.cache_dir / cache_filename + + with open(cache_path, 'wb') as f: + pickle.dump(cache_data, f) + + cache_entry = create_cache_entry( + db=db, + user_id=user_id, + ref_audio_hash=ref_audio_hash, + cache_path=str(cache_path), + meta_data=metadata + ) + + await self.enforce_max_entries(user_id, db) + + logger.info(f"Cache created: user={user_id}, hash={ref_audio_hash[:8]}..., id={cache_entry.id}") + return cache_entry.id + + except Exception as e: + logger.error(f"Cache creation error: {e}", exc_info=True) + if cache_path.exists(): + cache_path.unlink() + raise + + async def enforce_max_entries(self, user_id: int, db: Session) -> int: + try: + all_caches = list_cache_entries(db, user_id, skip=0, limit=9999) + if len(all_caches) <= self.max_entries: + return 0 + + caches_to_delete = all_caches[self.max_entries:] + deleted_count = 0 + + for cache in caches_to_delete: + cache_file = Path(cache.cache_path) + if cache_file.exists(): + cache_file.unlink() + + delete_cache_entry(db, cache.id, user_id) + deleted_count += 1 + + if deleted_count > 0: + logger.info(f"LRU eviction: user={user_id}, deleted={deleted_count} entries") + + return deleted_count + + except Exception as e: + logger.error(f"LRU enforcement error: {e}", exc_info=True) + return 0 + + async def cleanup_expired(self, db: Session) -> int: + try: + cutoff_date = datetime.utcnow() - timedelta(days=self.ttl_days) + expired_caches = db.query(VoiceCache).filter( + VoiceCache.last_accessed < cutoff_date + ).all() + + deleted_count = 0 + for cache in expired_caches: + cache_file = Path(cache.cache_path) + if cache_file.exists(): + cache_file.unlink() + + db.delete(cache) + deleted_count += 1 + + if deleted_count > 0: + db.commit() + logger.info(f"Expired cache cleanup: deleted={deleted_count} entries") + + return deleted_count + + except Exception as e: + logger.error(f"Expired cache cleanup error: {e}", exc_info=True) + db.rollback() + return 0 diff --git a/qwen3-tts-backend/core/cleanup.py b/qwen3-tts-backend/core/cleanup.py new file mode 100644 index 0000000..8c917fc --- /dev/null +++ b/qwen3-tts-backend/core/cleanup.py @@ -0,0 +1,166 @@ +import logging +from datetime import datetime, timedelta +from pathlib import Path +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from core.config import settings +from core.cache_manager import VoiceCacheManager +from db.models import Job + +logger = logging.getLogger(__name__) + + +async def cleanup_expired_caches(db_url: str) -> dict: + try: + engine = create_engine(db_url) + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + db = SessionLocal() + + cache_manager = await VoiceCacheManager.get_instance() + deleted_count = await cache_manager.cleanup_expired(db) + + freed_space_mb = 0 + + db.close() + + logger.info(f"Cleanup: deleted {deleted_count} expired caches") + + return { + 'deleted_count': deleted_count, + 'freed_space_mb': freed_space_mb + } + + except Exception as e: + logger.error(f"Expired cache cleanup failed: {e}", exc_info=True) + return { + 'deleted_count': 0, + 'freed_space_mb': 0, + 'error': str(e) + } + + +async def cleanup_old_jobs(db_url: str, days: int = 7) -> dict: + try: + engine = create_engine(db_url) + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + db = SessionLocal() + + cutoff_date = datetime.utcnow() - timedelta(days=days) + + old_jobs = db.query(Job).filter( + Job.completed_at < cutoff_date, + Job.status.in_(['completed', 'failed']) + ).all() + + deleted_files = 0 + for job in old_jobs: + if job.output_path: + output_file = Path(job.output_path) + if output_file.exists(): + output_file.unlink() + deleted_files += 1 + + db.delete(job) + + db.commit() + deleted_jobs = len(old_jobs) + + db.close() + + logger.info(f"Cleanup: deleted {deleted_jobs} old jobs, {deleted_files} files") + + return { + 'deleted_jobs': deleted_jobs, + 'deleted_files': deleted_files + } + + except Exception as e: + logger.error(f"Old job cleanup failed: {e}", exc_info=True) + return { + 'deleted_jobs': 0, + 'deleted_files': 0, + 'error': str(e) + } + + +async def cleanup_orphaned_files(db_url: str) -> dict: + try: + engine = create_engine(db_url) + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + db = SessionLocal() + + output_dir = Path(settings.OUTPUT_DIR) + cache_dir = Path(settings.CACHE_DIR) + + output_files_in_db = {Path(job.output_path).name for job in db.query(Job.output_path).filter(Job.output_path.isnot(None)).all()} + + from db.models import VoiceCache + cache_files_in_db = {Path(cache.cache_path).name for cache in db.query(VoiceCache.cache_path).all()} + + deleted_orphans = 0 + freed_space_bytes = 0 + + if output_dir.exists(): + for output_file in output_dir.glob("*.wav"): + if output_file.name not in output_files_in_db: + size = output_file.stat().st_size + output_file.unlink() + deleted_orphans += 1 + freed_space_bytes += size + + if cache_dir.exists(): + for cache_file in cache_dir.glob("*.pkl"): + if cache_file.name not in cache_files_in_db: + size = cache_file.stat().st_size + cache_file.unlink() + deleted_orphans += 1 + freed_space_bytes += size + + freed_space_mb = freed_space_bytes / (1024 * 1024) + + db.close() + + logger.info(f"Cleanup: deleted {deleted_orphans} orphaned files, freed {freed_space_mb:.2f} MB") + + return { + 'deleted_orphans': deleted_orphans, + 'freed_space_mb': freed_space_mb + } + + except Exception as e: + logger.error(f"Orphaned file cleanup failed: {e}", exc_info=True) + return { + 'deleted_orphans': 0, + 'freed_space_mb': 0, + 'error': str(e) + } + + +async def run_scheduled_cleanup(db_url: str) -> dict: + logger.info("Starting scheduled cleanup task...") + + try: + cache_result = await cleanup_expired_caches(db_url) + job_result = await cleanup_old_jobs(db_url) + orphan_result = await cleanup_orphaned_files(db_url) + + result = { + 'timestamp': datetime.utcnow().isoformat(), + 'expired_caches': cache_result, + 'old_jobs': job_result, + 'orphaned_files': orphan_result, + 'status': 'completed' + } + + logger.info(f"Scheduled cleanup completed: {result}") + + return result + + except Exception as e: + logger.error(f"Scheduled cleanup failed: {e}", exc_info=True) + return { + 'timestamp': datetime.utcnow().isoformat(), + 'status': 'failed', + 'error': str(e) + } diff --git a/qwen3-tts-backend/core/config.py b/qwen3-tts-backend/core/config.py new file mode 100644 index 0000000..d7e8dcb --- /dev/null +++ b/qwen3-tts-backend/core/config.py @@ -0,0 +1,3 @@ +from config import settings, Settings + +__all__ = ['settings', 'Settings'] diff --git a/qwen3-tts-backend/core/database.py b/qwen3-tts-backend/core/database.py new file mode 100644 index 0000000..6dcaf2e --- /dev/null +++ b/qwen3-tts-backend/core/database.py @@ -0,0 +1,3 @@ +from db.database import Base, engine, SessionLocal, get_db, init_db + +__all__ = ['Base', 'engine', 'SessionLocal', 'get_db', 'init_db'] diff --git a/qwen3-tts-backend/core/metrics.py b/qwen3-tts-backend/core/metrics.py new file mode 100644 index 0000000..497e4ce --- /dev/null +++ b/qwen3-tts-backend/core/metrics.py @@ -0,0 +1,156 @@ +import time +import logging +from collections import deque, defaultdict +from typing import Dict, List, Optional, Any +from dataclasses import dataclass, field +import asyncio +import statistics + +logger = logging.getLogger(__name__) + + +@dataclass +class RequestMetric: + timestamp: float + endpoint: str + duration: float + status_code: int + queue_time: float = 0.0 + + +class MetricsCollector: + _instance: Optional['MetricsCollector'] = None + _lock = asyncio.Lock() + + def __init__(self, window_size: int = 1000): + self.window_size = window_size + self.requests: deque = deque(maxlen=window_size) + self.request_counts: Dict[str, int] = defaultdict(int) + self.error_counts: Dict[str, int] = defaultdict(int) + self.total_requests = 0 + self.start_time = time.time() + self.batch_stats = { + 'total_batches': 0, + 'total_requests_batched': 0, + 'avg_batch_size': 0.0 + } + self._lock_local = asyncio.Lock() + logger.info("MetricsCollector initialized") + + @classmethod + async def get_instance(cls) -> 'MetricsCollector': + if cls._instance is None: + async with cls._lock: + if cls._instance is None: + cls._instance = cls() + return cls._instance + + async def record_request( + self, + endpoint: str, + duration: float, + status_code: int, + queue_time: float = 0.0 + ): + async with self._lock_local: + metric = RequestMetric( + timestamp=time.time(), + endpoint=endpoint, + duration=duration, + status_code=status_code, + queue_time=queue_time + ) + self.requests.append(metric) + self.request_counts[endpoint] += 1 + self.total_requests += 1 + + if status_code >= 400: + self.error_counts[endpoint] += 1 + + async def record_batch(self, batch_size: int): + async with self._lock_local: + self.batch_stats['total_batches'] += 1 + self.batch_stats['total_requests_batched'] += batch_size + + total_batches = self.batch_stats['total_batches'] + total_requests = self.batch_stats['total_requests_batched'] + self.batch_stats['avg_batch_size'] = total_requests / total_batches if total_batches > 0 else 0.0 + + async def get_metrics(self) -> Dict[str, Any]: + async with self._lock_local: + current_time = time.time() + uptime = current_time - self.start_time + + recent_requests = [r for r in self.requests if current_time - r.timestamp < 60] + + durations = [r.duration for r in self.requests if r.duration > 0] + queue_times = [r.queue_time for r in self.requests if r.queue_time > 0] + + percentiles = {} + if durations: + sorted_durations = sorted(durations) + percentiles = { + 'p50': statistics.median(sorted_durations), + 'p95': sorted_durations[int(len(sorted_durations) * 0.95)] if len(sorted_durations) > 0 else 0, + 'p99': sorted_durations[int(len(sorted_durations) * 0.99)] if len(sorted_durations) > 0 else 0, + 'avg': statistics.mean(sorted_durations), + 'min': min(sorted_durations), + 'max': max(sorted_durations) + } + + queue_percentiles = {} + if queue_times: + sorted_queue_times = sorted(queue_times) + queue_percentiles = { + 'p50': statistics.median(sorted_queue_times), + 'p95': sorted_queue_times[int(len(sorted_queue_times) * 0.95)] if len(sorted_queue_times) > 0 else 0, + 'p99': sorted_queue_times[int(len(sorted_queue_times) * 0.99)] if len(sorted_queue_times) > 0 else 0, + 'avg': statistics.mean(sorted_queue_times) + } + + requests_per_second = len(recent_requests) / 60.0 if recent_requests else 0.0 + + import torch + gpu_stats = {} + if torch.cuda.is_available(): + gpu_stats = { + 'gpu_available': True, + 'gpu_memory_allocated_mb': torch.cuda.memory_allocated(0) / 1024**2, + 'gpu_memory_reserved_mb': torch.cuda.memory_reserved(0) / 1024**2, + 'gpu_memory_total_mb': torch.cuda.get_device_properties(0).total_memory / 1024**2 + } + else: + gpu_stats = {'gpu_available': False} + + from core.batch_processor import BatchProcessor + batch_processor = await BatchProcessor.get_instance() + batch_stats_current = await batch_processor.get_stats() + + return { + 'uptime_seconds': uptime, + 'total_requests': self.total_requests, + 'requests_per_second': requests_per_second, + 'request_counts_by_endpoint': dict(self.request_counts), + 'error_counts_by_endpoint': dict(self.error_counts), + 'latency': percentiles, + 'queue_time': queue_percentiles, + 'batch_processing': { + **self.batch_stats, + **batch_stats_current + }, + 'gpu': gpu_stats + } + + async def reset(self): + async with self._lock_local: + self.requests.clear() + self.request_counts.clear() + self.error_counts.clear() + self.total_requests = 0 + self.start_time = time.time() + self.batch_stats = { + 'total_batches': 0, + 'total_requests_batched': 0, + 'avg_batch_size': 0.0 + } + logger.info("Metrics reset") diff --git a/qwen3-tts-backend/core/model_manager.py b/qwen3-tts-backend/core/model_manager.py new file mode 100644 index 0000000..a0f524f --- /dev/null +++ b/qwen3-tts-backend/core/model_manager.py @@ -0,0 +1,123 @@ +import asyncio +import logging +from typing import Optional +import torch +from qwen_tts import Qwen3TTSModel +from core.config import settings + +logger = logging.getLogger(__name__) + + +class ModelManager: + _instance: Optional['ModelManager'] = None + _lock = asyncio.Lock() + + MODEL_PATHS = { + "custom-voice": "Qwen3-TTS-12Hz-1.7B-CustomVoice", + "voice-design": "Qwen3-TTS-12Hz-1.7B-VoiceDesign", + "base": "Qwen3-TTS-12Hz-1.7B-Base" + } + + def __init__(self): + if ModelManager._instance is not None: + raise RuntimeError("Use get_instance() to get ModelManager") + self.current_model_name: Optional[str] = None + self.tts: Optional[Qwen3TTSModel] = None + + @classmethod + async def get_instance(cls) -> 'ModelManager': + if cls._instance is None: + async with cls._lock: + if cls._instance is None: + cls._instance = cls() + return cls._instance + + async def load_model(self, model_name: str) -> None: + if model_name not in self.MODEL_PATHS: + raise ValueError( + f"Unknown model: {model_name}. " + f"Available models: {list(self.MODEL_PATHS.keys())}" + ) + + if self.current_model_name == model_name and self.tts is not None: + logger.info(f"Model {model_name} already loaded") + return + + async with self._lock: + logger.info(f"Loading model: {model_name}") + + if self.tts is not None: + logger.info(f"Unloading current model: {self.current_model_name}") + await self._unload_model_internal() + + from pathlib import Path + model_base_path = Path(settings.MODEL_BASE_PATH) + local_model_path = model_base_path / self.MODEL_PATHS[model_name] + + if local_model_path.exists(): + model_path = str(local_model_path) + logger.info(f"Using local model: {model_path}") + else: + model_path = f"Qwen/{self.MODEL_PATHS[model_name]}" + logger.info(f"Local path not found, using HuggingFace: {model_path}") + + try: + self.tts = Qwen3TTSModel.from_pretrained( + str(model_path), + device_map=settings.MODEL_DEVICE, + torch_dtype=torch.bfloat16 + ) + self.current_model_name = model_name + logger.info(f"Successfully loaded model: {model_name}") + + if torch.cuda.is_available(): + allocated = torch.cuda.memory_allocated(0) / 1024**3 + logger.info(f"GPU memory allocated: {allocated:.2f} GB") + + except Exception as e: + logger.error(f"Failed to load model {model_name}: {e}") + self.tts = None + self.current_model_name = None + raise + + async def get_current_model(self) -> tuple[Optional[str], Optional[Qwen3TTSModel]]: + return self.current_model_name, self.tts + + async def unload_model(self) -> None: + async with self._lock: + await self._unload_model_internal() + + async def _unload_model_internal(self) -> None: + if self.tts is not None: + logger.info(f"Unloading model: {self.current_model_name}") + del self.tts + self.tts = None + self.current_model_name = None + + if torch.cuda.is_available(): + torch.cuda.empty_cache() + logger.info("Cleared CUDA cache") + + async def get_memory_usage(self) -> dict: + memory_info = { + "gpu_available": torch.cuda.is_available(), + "current_model": self.current_model_name + } + + if torch.cuda.is_available(): + memory_info.update({ + "allocated_gb": torch.cuda.memory_allocated(0) / 1024**3, + "reserved_gb": torch.cuda.memory_reserved(0) / 1024**3, + "total_gb": torch.cuda.get_device_properties(0).total_memory / 1024**3 + }) + + return memory_info + + def get_model_info(self) -> dict: + return { + name: { + "path": path, + "loaded": name == self.current_model_name + } + for name, path in self.MODEL_PATHS.items() + } diff --git a/qwen3-tts-backend/core/security.py b/qwen3-tts-backend/core/security.py new file mode 100644 index 0000000..610018b --- /dev/null +++ b/qwen3-tts-backend/core/security.py @@ -0,0 +1,35 @@ +from datetime import datetime, timedelta +from typing import Optional +from jose import JWTError, jwt +from passlib.context import CryptContext + +from config import settings + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +def get_password_hash(password: str) -> str: + return pwd_context.hash(password) + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) + return encoded_jwt + +def decode_access_token(token: str) -> Optional[str]: + try: + payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) + username: str = payload.get("sub") + if username is None: + return None + return username + except JWTError: + return None diff --git a/qwen3-tts-backend/db/__init__.py b/qwen3-tts-backend/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/qwen3-tts-backend/db/crud.py b/qwen3-tts-backend/db/crud.py new file mode 100644 index 0000000..c4bbc9d --- /dev/null +++ b/qwen3-tts-backend/db/crud.py @@ -0,0 +1,198 @@ +import json +from typing import Optional, List, Dict, Any +from datetime import datetime +from sqlalchemy.orm import Session + +from db.models import User, Job, VoiceCache + +def get_user_by_username(db: Session, username: str) -> Optional[User]: + return db.query(User).filter(User.username == username).first() + +def get_user_by_email(db: Session, email: str) -> Optional[User]: + return db.query(User).filter(User.email == email).first() + +def create_user(db: Session, username: str, email: str, hashed_password: str) -> User: + user = User( + username=username, + email=email, + hashed_password=hashed_password + ) + db.add(user) + db.commit() + db.refresh(user) + return user + +def create_user_by_admin( + db: Session, + username: str, + email: str, + hashed_password: str, + is_superuser: bool = False +) -> User: + user = User( + username=username, + email=email, + hashed_password=hashed_password, + is_superuser=is_superuser + ) + db.add(user) + db.commit() + db.refresh(user) + return user + +def get_user_by_id(db: Session, user_id: int) -> Optional[User]: + return db.query(User).filter(User.id == user_id).first() + +def list_users(db: Session, skip: int = 0, limit: int = 100) -> tuple[List[User], int]: + total = db.query(User).count() + users = db.query(User).order_by(User.created_at.desc()).offset(skip).limit(limit).all() + return users, total + +def update_user( + db: Session, + user_id: int, + username: Optional[str] = None, + email: Optional[str] = None, + hashed_password: Optional[str] = None, + is_active: Optional[bool] = None, + is_superuser: Optional[bool] = None +) -> Optional[User]: + user = get_user_by_id(db, user_id) + if not user: + return None + + if username is not None: + user.username = username + if email is not None: + user.email = email + if hashed_password is not None: + user.hashed_password = hashed_password + if is_active is not None: + user.is_active = is_active + if is_superuser is not None: + user.is_superuser = is_superuser + + user.updated_at = datetime.utcnow() + db.commit() + db.refresh(user) + return user + +def delete_user(db: Session, user_id: int) -> bool: + user = get_user_by_id(db, user_id) + if not user: + return False + db.delete(user) + db.commit() + return True + +def create_job(db: Session, user_id: int, job_type: str, input_data: Dict[str, Any]) -> Job: + job = Job( + user_id=user_id, + job_type=job_type, + input_data=json.dumps(input_data), + status="pending" + ) + db.add(job) + db.commit() + db.refresh(job) + return job + +def get_job(db: Session, job_id: int, user_id: int) -> Optional[Job]: + return db.query(Job).filter(Job.id == job_id, Job.user_id == user_id).first() + +def list_jobs( + db: Session, + user_id: int, + skip: int = 0, + limit: int = 100, + status: Optional[str] = None +) -> List[Job]: + query = db.query(Job).filter(Job.user_id == user_id) + if status: + query = query.filter(Job.status == status) + return query.order_by(Job.created_at.desc()).offset(skip).limit(limit).all() + +def update_job_status( + db: Session, + job_id: int, + user_id: int, + status: str, + output_path: Optional[str] = None, + error_message: Optional[str] = None +) -> Optional[Job]: + job = get_job(db, job_id, user_id) + if not job: + return None + + job.status = status + if output_path: + job.output_path = output_path + if error_message: + job.error_message = error_message + if status in ["completed", "failed"]: + job.completed_at = datetime.utcnow() + + db.commit() + db.refresh(job) + return job + +def delete_job(db: Session, job_id: int, user_id: int) -> bool: + job = get_job(db, job_id, user_id) + if not job: + return False + db.delete(job) + db.commit() + return True + +def create_cache_entry( + db: Session, + user_id: int, + ref_audio_hash: str, + cache_path: str, + meta_data: Optional[Dict[str, Any]] = None +) -> VoiceCache: + cache = VoiceCache( + user_id=user_id, + ref_audio_hash=ref_audio_hash, + cache_path=cache_path, + meta_data=json.dumps(meta_data) if meta_data else None + ) + db.add(cache) + db.commit() + db.refresh(cache) + return cache + +def get_cache_entry(db: Session, user_id: int, ref_audio_hash: str) -> Optional[VoiceCache]: + cache = db.query(VoiceCache).filter( + VoiceCache.user_id == user_id, + VoiceCache.ref_audio_hash == ref_audio_hash + ).first() + + if cache: + cache.last_accessed = datetime.utcnow() + cache.access_count += 1 + db.commit() + db.refresh(cache) + + return cache + +def list_cache_entries( + db: Session, + user_id: int, + skip: int = 0, + limit: int = 100 +) -> List[VoiceCache]: + return db.query(VoiceCache).filter( + VoiceCache.user_id == user_id + ).order_by(VoiceCache.last_accessed.desc()).offset(skip).limit(limit).all() + +def delete_cache_entry(db: Session, cache_id: int, user_id: int) -> bool: + cache = db.query(VoiceCache).filter( + VoiceCache.id == cache_id, + VoiceCache.user_id == user_id + ).first() + if not cache: + return False + db.delete(cache) + db.commit() + return True diff --git a/qwen3-tts-backend/db/database.py b/qwen3-tts-backend/db/database.py new file mode 100644 index 0000000..0a5d48e --- /dev/null +++ b/qwen3-tts-backend/db/database.py @@ -0,0 +1,23 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, declarative_base + +from config import settings + +engine = create_engine( + settings.DATABASE_URL, + connect_args={"check_same_thread": False} if "sqlite" in settings.DATABASE_URL else {} +) + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +def init_db(): + Base.metadata.create_all(bind=engine) diff --git a/qwen3-tts-backend/db/models.py b/qwen3-tts-backend/db/models.py new file mode 100644 index 0000000..4e7e631 --- /dev/null +++ b/qwen3-tts-backend/db/models.py @@ -0,0 +1,67 @@ +from datetime import datetime +from enum import Enum +from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Index, JSON +from sqlalchemy.orm import relationship + +from db.database import Base + +class JobStatus(str, Enum): + PENDING = "pending" + PROCESSING = "processing" + COMPLETED = "completed" + FAILED = "failed" + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + username = Column(String(50), unique=True, nullable=False, index=True) + email = Column(String(255), unique=True, nullable=False, index=True) + hashed_password = Column(String(255), nullable=False) + is_active = Column(Boolean, default=True, nullable=False) + is_superuser = Column(Boolean, default=False, nullable=False) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + jobs = relationship("Job", back_populates="user", cascade="all, delete-orphan") + voice_caches = relationship("VoiceCache", back_populates="user", cascade="all, delete-orphan") + +class Job(Base): + __tablename__ = "jobs" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True) + job_type = Column(String(50), nullable=False) + status = Column(String(50), default="pending", nullable=False, index=True) + input_data = Column(Text, nullable=True) + input_params = Column(JSON, nullable=True) + output_path = Column(String(500), nullable=True) + error_message = Column(Text, nullable=True) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True) + started_at = Column(DateTime, nullable=True) + completed_at = Column(DateTime, nullable=True) + + user = relationship("User", back_populates="jobs") + + __table_args__ = ( + Index('idx_user_status', 'user_id', 'status'), + Index('idx_user_created', 'user_id', 'created_at'), + ) + +class VoiceCache(Base): + __tablename__ = "voice_caches" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True) + ref_audio_hash = Column(String(64), nullable=False, index=True) + cache_path = Column(String(500), nullable=False) + meta_data = Column(Text, nullable=True) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + last_accessed = Column(DateTime, default=datetime.utcnow, nullable=False, index=True) + access_count = Column(Integer, default=0, nullable=False) + + user = relationship("User", back_populates="voice_caches") + + __table_args__ = ( + Index('idx_user_hash', 'user_id', 'ref_audio_hash'), + ) diff --git a/qwen3-tts-backend/deploy/nginx.conf b/qwen3-tts-backend/deploy/nginx.conf new file mode 100644 index 0000000..98fd570 --- /dev/null +++ b/qwen3-tts-backend/deploy/nginx.conf @@ -0,0 +1,55 @@ +upstream qwen_tts_backend { + server 127.0.0.1:8000; +} + +server { + listen 80; + server_name your-domain.com; + + client_max_body_size 100M; + client_body_timeout 300s; + proxy_read_timeout 300s; + proxy_connect_timeout 300s; + proxy_send_timeout 300s; + + location / { + proxy_pass http://qwen_tts_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always; + + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS'; + add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type'; + add_header 'Content-Length' '0'; + add_header 'Content-Type' 'text/plain'; + return 204; + } + } + + location /outputs/ { + alias /opt/qwen3-tts-backend/outputs/; + autoindex off; + add_header Cache-Control "public, max-age=3600"; + add_header Content-Disposition "attachment"; + } + + location /health { + proxy_pass http://qwen_tts_backend/health; + proxy_set_header Host $host; + access_log off; + } + + location /metrics { + proxy_pass http://qwen_tts_backend/metrics; + proxy_set_header Host $host; + allow 127.0.0.1; + deny all; + } +} diff --git a/qwen3-tts-backend/deploy/qwen-tts.service b/qwen3-tts-backend/deploy/qwen-tts.service new file mode 100644 index 0000000..1996986 --- /dev/null +++ b/qwen3-tts-backend/deploy/qwen-tts.service @@ -0,0 +1,21 @@ +[Unit] +Description=Qwen3-TTS Backend API Service +After=network.target + +[Service] +Type=simple +User=qwen-tts +Group=qwen-tts +WorkingDirectory=/opt/qwen3-tts-backend +Environment="PATH=/opt/conda/envs/qwen3-tts/bin:/usr/local/bin:/usr/bin:/bin" +EnvironmentFile=/opt/qwen3-tts-backend/.env +ExecStart=/opt/conda/envs/qwen3-tts/bin/python main.py +Restart=on-failure +RestartSec=10s +StandardOutput=append:/var/log/qwen-tts/app.log +StandardError=append:/var/log/qwen-tts/error.log +TimeoutStopSec=30s +KillMode=mixed + +[Install] +WantedBy=multi-user.target diff --git a/qwen3-tts-backend/main.py b/qwen3-tts-backend/main.py new file mode 100644 index 0000000..c142ac6 --- /dev/null +++ b/qwen3-tts-backend/main.py @@ -0,0 +1,221 @@ +import logging +import sys +from contextlib import asynccontextmanager +from pathlib import Path + +import torch +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +from slowapi import Limiter, _rate_limit_exceeded_handler +from slowapi.util import get_remote_address +from slowapi.errors import RateLimitExceeded +from sqlalchemy import text + +from core.config import settings +from core.database import init_db +from core.model_manager import ModelManager +from core.cleanup import run_scheduled_cleanup +from api import auth, jobs, tts, cache, metrics, users +from apscheduler.schedulers.asyncio import AsyncIOScheduler + +logging.basicConfig( + level=getattr(logging, settings.LOG_LEVEL.upper()), + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(sys.stdout), + logging.FileHandler(settings.LOG_FILE) + ] +) + +logger = logging.getLogger(__name__) + +def get_user_identifier(request: Request) -> str: + from jose import jwt + from core.config import settings + + auth_header = request.headers.get("Authorization", "") + if auth_header.startswith("Bearer "): + token = auth_header.split(" ")[1] + try: + payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) + user_id = payload.get("sub") + if user_id: + return f"user:{user_id}" + except Exception: + pass + + return get_remote_address(request) + +limiter = Limiter(key_func=get_user_identifier) + +@asynccontextmanager +async def lifespan(app: FastAPI): + logger.info("Starting Qwen3-TTS Backend Service...") + logger.info(f"Model base path: {settings.MODEL_BASE_PATH}") + logger.info(f"Cache directory: {settings.CACHE_DIR}") + logger.info(f"Output directory: {settings.OUTPUT_DIR}") + logger.info(f"Device: {settings.MODEL_DEVICE}") + + try: + settings.validate() + logger.info("Configuration validated successfully") + except Exception as e: + logger.error(f"Configuration validation failed: {e}") + raise + + try: + init_db() + logger.info("Database initialized successfully") + except Exception as e: + logger.error(f"Database initialization failed: {e}") + raise + + try: + model_manager = await ModelManager.get_instance() + await model_manager.load_model("custom-voice") + logger.info("Preloaded custom-voice model") + except Exception as e: + logger.warning(f"Model preload failed: {e}") + + scheduler = AsyncIOScheduler() + scheduler.add_job( + run_scheduled_cleanup, + 'interval', + hours=6, + args=[str(settings.DATABASE_URL)], + id='cleanup_task' + ) + scheduler.start() + logger.info("Background cleanup scheduler started (runs every 6 hours)") + + yield + + logger.info("Shutting down Qwen3-TTS Backend Service...") + + scheduler.shutdown() + logger.info("Scheduler shutdown completed") + + try: + model_manager = await ModelManager.get_instance() + await model_manager.unload_model() + logger.info("Model cleanup completed") + except Exception as e: + logger.error(f"Model cleanup failed: {e}") + +app = FastAPI( + title="Qwen3-TTS-WebUI Backend API", + description="Backend service for Qwen3-TTS-WebUI text-to-speech system", + version="0.1.0", + lifespan=lifespan +) + +app.state.limiter = limiter +app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(auth.router) +app.include_router(jobs.router) +app.include_router(tts.router) +app.include_router(cache.router) +app.include_router(metrics.router) +app.include_router(users.router) + +@app.get("/health") +async def health_check(): + from core.batch_processor import BatchProcessor + from core.database import SessionLocal + + gpu_available = torch.cuda.is_available() + + gpu_memory_used_mb = 0 + gpu_memory_total_mb = 0 + if gpu_available: + gpu_memory_used_mb = torch.cuda.memory_allocated(0) / 1024**2 + gpu_memory_total_mb = torch.cuda.get_device_properties(0).total_memory / 1024**2 + + model_manager = await ModelManager.get_instance() + current_model, _ = await model_manager.get_current_model() + + batch_processor = await BatchProcessor.get_instance() + queue_length = await batch_processor.get_queue_length() + + database_connected = True + try: + db = SessionLocal() + db.execute(text("SELECT 1")) + db.close() + except Exception as e: + logger.error(f"Database health check failed: {e}") + database_connected = False + + cache_dir_writable = True + try: + test_file = Path(settings.CACHE_DIR) / ".health_check" + test_file.write_text("test") + test_file.unlink() + except Exception as e: + logger.error(f"Cache directory health check failed: {e}") + cache_dir_writable = False + + output_dir_writable = True + try: + test_file = Path(settings.OUTPUT_DIR) / ".health_check" + test_file.write_text("test") + test_file.unlink() + except Exception as e: + logger.error(f"Output directory health check failed: {e}") + output_dir_writable = False + + critical_issues = [] + if not database_connected: + critical_issues.append("database_disconnected") + if not cache_dir_writable: + critical_issues.append("cache_dir_not_writable") + if not output_dir_writable: + critical_issues.append("output_dir_not_writable") + + minor_issues = [] + if not gpu_available: + minor_issues.append("gpu_not_available") + if queue_length > 50: + minor_issues.append("queue_congested") + + if critical_issues: + status = "unhealthy" + elif minor_issues: + status = "degraded" + else: + status = "healthy" + + return { + "status": status, + "gpu_available": gpu_available, + "gpu_memory_used_mb": gpu_memory_used_mb, + "gpu_memory_total_mb": gpu_memory_total_mb, + "queue_length": queue_length, + "active_model": current_model, + "database_connected": database_connected, + "cache_dir_writable": cache_dir_writable, + "output_dir_writable": output_dir_writable, + "issues": { + "critical": critical_issues, + "minor": minor_issues + } + } + +if __name__ == "__main__": + import uvicorn + uvicorn.run( + "main:app", + host=settings.HOST, + port=settings.PORT, + workers=settings.WORKERS, + log_level=settings.LOG_LEVEL.lower() + ) diff --git a/qwen3-tts-backend/pytest.ini b/qwen3-tts-backend/pytest.ini new file mode 100644 index 0000000..cdce43f --- /dev/null +++ b/qwen3-tts-backend/pytest.ini @@ -0,0 +1,6 @@ +[pytest] +asyncio_mode = auto +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* diff --git a/qwen3-tts-backend/requirements.txt b/qwen3-tts-backend/requirements.txt new file mode 100644 index 0000000..335424a --- /dev/null +++ b/qwen3-tts-backend/requirements.txt @@ -0,0 +1,18 @@ +fastapi==0.115.0 +uvicorn[standard]==0.32.0 +pydantic==2.9.0 +python-multipart==0.0.12 +python-jose[cryptography]==3.3.0 +passlib==1.7.4 +bcrypt==3.2.2 +sqlalchemy==2.0.35 +aiosqlite==0.20.0 +soundfile==0.12.1 +scipy>=1.11.0 +apscheduler>=3.10.0 +slowapi==0.1.9 +locust==2.20.0 +pytest==8.3.0 +pytest-cov==4.1.0 +pytest-asyncio==0.23.0 +httpx==0.27.0 diff --git a/qwen3-tts-backend/schemas/__init__.py b/qwen3-tts-backend/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/qwen3-tts-backend/schemas/cache.py b/qwen3-tts-backend/schemas/cache.py new file mode 100644 index 0000000..5725e81 --- /dev/null +++ b/qwen3-tts-backend/schemas/cache.py @@ -0,0 +1,15 @@ +from datetime import datetime +from typing import Optional, Dict, Any +from pydantic import BaseModel, ConfigDict + +class CacheEntry(BaseModel): + id: int + user_id: int + ref_audio_hash: str + cache_path: str + meta_data: Optional[Dict[str, Any]] = None + created_at: datetime + last_accessed: datetime + access_count: int + + model_config = ConfigDict(from_attributes=True) diff --git a/qwen3-tts-backend/schemas/job.py b/qwen3-tts-backend/schemas/job.py new file mode 100644 index 0000000..d4e4eab --- /dev/null +++ b/qwen3-tts-backend/schemas/job.py @@ -0,0 +1,25 @@ +from datetime import datetime +from typing import Optional, Dict, Any, List +from pydantic import BaseModel, ConfigDict + +class JobBase(BaseModel): + job_type: str + +class JobCreate(JobBase): + input_data: Dict[str, Any] + +class Job(JobBase): + id: int + user_id: int + status: str + output_path: Optional[str] = None + download_url: Optional[str] = None + error_message: Optional[str] = None + created_at: datetime + completed_at: Optional[datetime] = None + + model_config = ConfigDict(from_attributes=True) + +class JobList(BaseModel): + total: int + jobs: List[Job] diff --git a/qwen3-tts-backend/schemas/tts.py b/qwen3-tts-backend/schemas/tts.py new file mode 100644 index 0000000..4da189a --- /dev/null +++ b/qwen3-tts-backend/schemas/tts.py @@ -0,0 +1,50 @@ +from typing import Optional, List +from pydantic import BaseModel, Field + +class TTSRequest(BaseModel): + text: str = Field(..., min_length=1, max_length=1000) + ref_audio: Optional[str] = None + ref_text: Optional[str] = None + language: str = Field(default="en") + speed: float = Field(default=1.0, ge=0.5, le=2.0) + +class TTSResponse(BaseModel): + job_id: int + status: str + audio_url: Optional[str] = None + + +class CustomVoiceRequest(BaseModel): + text: str = Field(..., min_length=1, max_length=1000) + language: str = Field(default="Auto") + speaker: str + instruct: Optional[str] = Field(default="") + max_new_tokens: Optional[int] = Field(default=2048, ge=128, le=4096) + temperature: Optional[float] = Field(default=0.9, ge=0.1, le=2.0) + top_k: Optional[int] = Field(default=50, ge=1, le=100) + top_p: Optional[float] = Field(default=1.0, ge=0.0, le=1.0) + repetition_penalty: Optional[float] = Field(default=1.05, ge=1.0, le=2.0) + + +class VoiceDesignRequest(BaseModel): + text: str = Field(..., min_length=1, max_length=1000) + language: str = Field(default="Auto") + instruct: str = Field(..., min_length=1) + max_new_tokens: Optional[int] = Field(default=2048, ge=128, le=4096) + temperature: Optional[float] = Field(default=0.9, ge=0.1, le=2.0) + top_k: Optional[int] = Field(default=50, ge=1, le=100) + top_p: Optional[float] = Field(default=1.0, ge=0.0, le=1.0) + repetition_penalty: Optional[float] = Field(default=1.05, ge=1.0, le=2.0) + + +class VoiceCloneRequest(BaseModel): + text: str = Field(..., min_length=1, max_length=1000) + language: str = Field(default="Auto") + ref_text: Optional[str] = Field(default=None, max_length=500) + use_cache: bool = Field(default=True) + x_vector_only_mode: bool = Field(default=False) + max_new_tokens: Optional[int] = Field(default=2048, ge=128, le=4096) + temperature: Optional[float] = Field(default=0.9, ge=0.1, le=2.0) + top_k: Optional[int] = Field(default=50, ge=1, le=100) + top_p: Optional[float] = Field(default=1.0, ge=0.0, le=1.0) + repetition_penalty: Optional[float] = Field(default=1.05, ge=1.0, le=2.0) diff --git a/qwen3-tts-backend/schemas/user.py b/qwen3-tts-backend/schemas/user.py new file mode 100644 index 0000000..055566d --- /dev/null +++ b/qwen3-tts-backend/schemas/user.py @@ -0,0 +1,91 @@ +from datetime import datetime +from typing import Optional +from pydantic import BaseModel, EmailStr, Field, field_validator, ConfigDict +import re + +class UserBase(BaseModel): + username: str = Field(..., min_length=3, max_length=50) + email: EmailStr + + @field_validator('username') + @classmethod + def validate_username(cls, v: str) -> str: + if not re.match(r'^[a-zA-Z0-9_-]+$', v): + raise ValueError('Username must contain only alphanumeric characters, underscores, and dashes') + return v + +class UserCreate(UserBase): + password: str = Field(..., min_length=8, max_length=128) + + @field_validator('password') + @classmethod + def validate_password_strength(cls, v: str) -> str: + if not re.search(r'[A-Z]', v): + raise ValueError('Password must contain at least one uppercase letter') + if not re.search(r'[a-z]', v): + raise ValueError('Password must contain at least one lowercase letter') + if not re.search(r'\d', v): + raise ValueError('Password must contain at least one digit') + return v + +class User(UserBase): + id: int + is_active: bool + is_superuser: bool + created_at: datetime + + model_config = ConfigDict(from_attributes=True) + +class UserCreateByAdmin(UserBase): + password: str = Field(..., min_length=8, max_length=128) + is_superuser: bool = False + + @field_validator('password') + @classmethod + def validate_password_strength(cls, v: str) -> str: + if not re.search(r'[A-Z]', v): + raise ValueError('Password must contain at least one uppercase letter') + if not re.search(r'[a-z]', v): + raise ValueError('Password must contain at least one lowercase letter') + if not re.search(r'\d', v): + raise ValueError('Password must contain at least one digit') + return v + +class UserUpdate(BaseModel): + username: Optional[str] = Field(None, min_length=3, max_length=50) + email: Optional[EmailStr] = None + password: Optional[str] = Field(None, min_length=8, max_length=128) + is_active: Optional[bool] = None + is_superuser: Optional[bool] = None + + @field_validator('username') + @classmethod + def validate_username(cls, v: Optional[str]) -> Optional[str]: + if v is not None and not re.match(r'^[a-zA-Z0-9_-]+$', v): + raise ValueError('Username must contain only alphanumeric characters, underscores, and dashes') + return v + + @field_validator('password') + @classmethod + def validate_password_strength(cls, v: Optional[str]) -> Optional[str]: + if v is not None: + if not re.search(r'[A-Z]', v): + raise ValueError('Password must contain at least one uppercase letter') + if not re.search(r'[a-z]', v): + raise ValueError('Password must contain at least one lowercase letter') + if not re.search(r'\d', v): + raise ValueError('Password must contain at least one digit') + return v + +class UserListResponse(BaseModel): + users: list[User] + total: int + skip: int + limit: int + +class Token(BaseModel): + access_token: str + token_type: str + +class TokenData(BaseModel): + username: Optional[str] = None diff --git a/qwen3-tts-backend/scripts/add_superuser_field.py b/qwen3-tts-backend/scripts/add_superuser_field.py new file mode 100644 index 0000000..12c9c28 --- /dev/null +++ b/qwen3-tts-backend/scripts/add_superuser_field.py @@ -0,0 +1,23 @@ +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from sqlalchemy import text +from core.database import engine + +def add_superuser_field(): + try: + with engine.connect() as conn: + conn.execute(text("ALTER TABLE users ADD COLUMN is_superuser BOOLEAN NOT NULL DEFAULT 0")) + conn.commit() + print("Successfully added is_superuser field to users table") + except Exception as e: + if "duplicate column name" in str(e).lower() or "already exists" in str(e).lower(): + print("is_superuser field already exists, skipping") + else: + print(f"Error adding is_superuser field: {e}") + raise + +if __name__ == "__main__": + add_superuser_field() diff --git a/qwen3-tts-backend/scripts/create_admin.py b/qwen3-tts-backend/scripts/create_admin.py new file mode 100644 index 0000000..4d34f75 --- /dev/null +++ b/qwen3-tts-backend/scripts/create_admin.py @@ -0,0 +1,40 @@ +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from core.database import SessionLocal +from core.security import get_password_hash +from db.crud import get_user_by_username, create_user_by_admin + +def create_admin(): + db = SessionLocal() + try: + existing_admin = get_user_by_username(db, username="admin") + if existing_admin: + print("Admin user already exists") + if not existing_admin.is_superuser: + existing_admin.is_superuser = True + db.commit() + print("Updated existing admin user to superuser") + return + + hashed_password = get_password_hash("admin123456") + admin_user = create_user_by_admin( + db, + username="admin", + email="admin@example.com", + hashed_password=hashed_password, + is_superuser=True + ) + print(f"Created admin user successfully: {admin_user.username}") + print("Username: admin") + print("Password: admin123456") + except Exception as e: + print(f"Error creating admin user: {e}") + raise + finally: + db.close() + +if __name__ == "__main__": + create_admin() diff --git a/qwen3-tts-backend/utils/__init__.py b/qwen3-tts-backend/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/qwen3-tts-backend/utils/audio.py b/qwen3-tts-backend/utils/audio.py new file mode 100644 index 0000000..767e6c1 --- /dev/null +++ b/qwen3-tts-backend/utils/audio.py @@ -0,0 +1,113 @@ +import base64 +import io +from pathlib import Path +import numpy as np +import soundfile as sf +from scipy import signal + + +def validate_ref_audio(audio_data: bytes, max_size_mb: int = 10) -> bool: + try: + size_mb = len(audio_data) / (1024 * 1024) + if size_mb > max_size_mb: + return False + + buffer = io.BytesIO(audio_data) + audio_array, sample_rate = sf.read(buffer) + + duration = len(audio_array) / sample_rate + if duration < 1.0 or duration > 30.0: + return False + + return True + except Exception: + return False + + +def process_ref_audio(audio_data: bytes) -> tuple[np.ndarray, int]: + buffer = io.BytesIO(audio_data) + audio_array, orig_sr = sf.read(buffer) + + if audio_array.ndim > 1: + audio_array = np.mean(audio_array, axis=1) + + target_sr = 24000 + if orig_sr != target_sr: + audio_array = resample_audio(audio_array, orig_sr, target_sr) + + audio_array = audio_array.astype(np.float32) + return audio_array, target_sr + + +def resample_audio(audio_array: np.ndarray, orig_sr: int, target_sr: int = 24000) -> np.ndarray: + if orig_sr == target_sr: + return audio_array + + num_samples = int(len(audio_array) * target_sr / orig_sr) + resampled = signal.resample(audio_array, num_samples) + return resampled.astype(np.float32) + + +def extract_audio_features(audio_array: np.ndarray, sample_rate: int) -> dict: + duration = len(audio_array) / sample_rate + rms_energy = np.sqrt(np.mean(audio_array ** 2)) + + return { + 'duration': float(duration), + 'sample_rate': int(sample_rate), + 'num_samples': int(len(audio_array)), + 'rms_energy': float(rms_energy) + } + + +def encode_audio_to_base64(audio_array: np.ndarray, sample_rate: int) -> str: + buffer = io.BytesIO() + sf.write(buffer, audio_array, sample_rate, format='WAV') + buffer.seek(0) + audio_bytes = buffer.read() + return base64.b64encode(audio_bytes).decode('utf-8') + + +def decode_base64_to_audio(base64_string: str) -> tuple[np.ndarray, int]: + audio_bytes = base64.b64decode(base64_string) + buffer = io.BytesIO(audio_bytes) + audio_array, sample_rate = sf.read(buffer) + return audio_array, sample_rate + + +def validate_audio_format(audio_data: bytes) -> bool: + try: + buffer = io.BytesIO(audio_data) + sf.read(buffer) + return True + except Exception: + return False + + +def get_audio_duration(audio_array: np.ndarray, sample_rate: int) -> float: + return len(audio_array) / sample_rate + + +def save_audio_file( + audio_array: np.ndarray, + sample_rate: int, + output_path: str | Path +) -> str: + output_path = Path(output_path) + output_path.parent.mkdir(parents=True, exist_ok=True) + + if not isinstance(audio_array, np.ndarray): + audio_array = np.array(audio_array, dtype=np.float32) + + if audio_array.ndim == 1: + pass + elif audio_array.ndim == 2: + if audio_array.shape[0] < audio_array.shape[1]: + audio_array = audio_array.T + else: + raise ValueError(f"Unexpected audio array shape: {audio_array.shape}") + + audio_array = audio_array.astype(np.float32) + + sf.write(str(output_path), audio_array, sample_rate, format='WAV', subtype='PCM_16') + return str(output_path) diff --git a/qwen3-tts-backend/utils/metrics.py b/qwen3-tts-backend/utils/metrics.py new file mode 100644 index 0000000..2fb8f28 --- /dev/null +++ b/qwen3-tts-backend/utils/metrics.py @@ -0,0 +1,80 @@ +import threading +from typing import Dict +from pathlib import Path +from sqlalchemy.orm import Session +from db.models import VoiceCache + + +class CacheMetrics: + def __init__(self): + self._lock = threading.Lock() + self.cache_hits = 0 + self.cache_misses = 0 + self._user_hits: Dict[int, int] = {} + self._user_misses: Dict[int, int] = {} + + def record_hit(self, user_id: int): + with self._lock: + self.cache_hits += 1 + self._user_hits[user_id] = self._user_hits.get(user_id, 0) + 1 + + def record_miss(self, user_id: int): + with self._lock: + self.cache_misses += 1 + self._user_misses[user_id] = self._user_misses.get(user_id, 0) + 1 + + def get_stats(self, db: Session, cache_dir: str) -> dict: + with self._lock: + total_requests = self.cache_hits + self.cache_misses + hit_rate = self.cache_hits / total_requests if total_requests > 0 else 0.0 + + total_entries = db.query(VoiceCache).count() + + total_size_bytes = 0 + cache_path = Path(cache_dir) + if cache_path.exists(): + for cache_file in cache_path.glob("*.pkl"): + total_size_bytes += cache_file.stat().st_size + + total_size_mb = total_size_bytes / (1024 * 1024) + + user_stats = [] + for user_id in set(list(self._user_hits.keys()) + list(self._user_misses.keys())): + hits = self._user_hits.get(user_id, 0) + misses = self._user_misses.get(user_id, 0) + total = hits + misses + user_hit_rate = hits / total if total > 0 else 0.0 + + user_cache_count = db.query(VoiceCache).filter( + VoiceCache.user_id == user_id + ).count() + + user_stats.append({ + 'user_id': user_id, + 'hits': hits, + 'misses': misses, + 'hit_rate': user_hit_rate, + 'cache_entries': user_cache_count + }) + + return { + 'global': { + 'total_requests': total_requests, + 'cache_hits': self.cache_hits, + 'cache_misses': self.cache_misses, + 'hit_rate': hit_rate, + 'total_entries': total_entries, + 'total_size_mb': total_size_mb + }, + 'users': user_stats + } + + def reset(self): + with self._lock: + self.cache_hits = 0 + self.cache_misses = 0 + self._user_hits.clear() + self._user_misses.clear() + + +cache_metrics = CacheMetrics() diff --git a/qwen3-tts-backend/utils/validation.py b/qwen3-tts-backend/utils/validation.py new file mode 100644 index 0000000..f78b495 --- /dev/null +++ b/qwen3-tts-backend/utils/validation.py @@ -0,0 +1,102 @@ +from typing import List, Dict + +SUPPORTED_LANGUAGES = [ + "Chinese", "English", "Japanese", "Korean", "German", + "French", "Russian", "Portuguese", "Spanish", "Italian", + "Auto", "Cantonese" +] + +SUPPORTED_SPEAKERS = [ + "Vivian", "Serena", "Uncle_Fu", "Dylan", "Eric", + "Ryan", "Aiden", "Ono_Anna", "Sohee" +] + +SPEAKER_DESCRIPTIONS = { + "Vivian": "Female, professional and clear", + "Serena": "Female, gentle and warm", + "Uncle_Fu": "Male, mature and authoritative", + "Dylan": "Male, young and energetic", + "Eric": "Male, calm and steady", + "Ryan": "Male, friendly and casual", + "Aiden": "Male, deep and resonant", + "Ono_Anna": "Female, cute and lively", + "Sohee": "Female, soft and melodious" +} + + +def validate_language(language: str) -> str: + normalized = language.strip() + + for supported in SUPPORTED_LANGUAGES: + if normalized.lower() == supported.lower(): + return supported + + raise ValueError( + f"Unsupported language: {language}. " + f"Supported languages: {', '.join(SUPPORTED_LANGUAGES)}" + ) + + +def validate_speaker(speaker: str) -> str: + normalized = speaker.strip() + + for supported in SUPPORTED_SPEAKERS: + if normalized.lower() == supported.lower(): + return supported + + raise ValueError( + f"Unsupported speaker: {speaker}. " + f"Supported speakers: {', '.join(SUPPORTED_SPEAKERS)}" + ) + + +def validate_text_length(text: str, max_length: int = 1000) -> str: + if not text or not text.strip(): + raise ValueError("Text cannot be empty") + + if len(text) > max_length: + raise ValueError( + f"Text length ({len(text)}) exceeds maximum ({max_length})" + ) + + return text.strip() + + +def validate_generation_params(params: dict) -> dict: + validated = {} + + validated['max_new_tokens'] = params.get('max_new_tokens', 2048) + if not 128 <= validated['max_new_tokens'] <= 4096: + raise ValueError("max_new_tokens must be between 128 and 4096") + + validated['temperature'] = params.get('temperature', 0.9) + if not 0.1 <= validated['temperature'] <= 2.0: + raise ValueError("temperature must be between 0.1 and 2.0") + + validated['top_k'] = params.get('top_k', 50) + if not 1 <= validated['top_k'] <= 100: + raise ValueError("top_k must be between 1 and 100") + + validated['top_p'] = params.get('top_p', 1.0) + if not 0.0 <= validated['top_p'] <= 1.0: + raise ValueError("top_p must be between 0.0 and 1.0") + + validated['repetition_penalty'] = params.get('repetition_penalty', 1.05) + if not 1.0 <= validated['repetition_penalty'] <= 2.0: + raise ValueError("repetition_penalty must be between 1.0 and 2.0") + + return validated + + +def get_supported_languages() -> List[str]: + return SUPPORTED_LANGUAGES.copy() + + +def get_supported_speakers() -> List[dict]: + return [ + { + "name": speaker, + "description": SPEAKER_DESCRIPTIONS.get(speaker, "") + } + for speaker in SUPPORTED_SPEAKERS + ] diff --git a/qwen3-tts-frontend/.env.example b/qwen3-tts-frontend/.env.example new file mode 100644 index 0000000..ec52b11 --- /dev/null +++ b/qwen3-tts-frontend/.env.example @@ -0,0 +1,2 @@ +VITE_API_URL=http://localhost:8000 +VITE_APP_NAME=Qwen3-TTS diff --git a/qwen3-tts-frontend/.env.production b/qwen3-tts-frontend/.env.production new file mode 100644 index 0000000..dab1cf7 --- /dev/null +++ b/qwen3-tts-frontend/.env.production @@ -0,0 +1,2 @@ +VITE_API_URL=https://api.example.com +VITE_APP_NAME=Qwen3-TTS diff --git a/qwen3-tts-frontend/.gitignore b/qwen3-tts-frontend/.gitignore new file mode 100644 index 0000000..4513a15 --- /dev/null +++ b/qwen3-tts-frontend/.gitignore @@ -0,0 +1,27 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +.env +.env.local +.env.*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/qwen3-tts-frontend/components.json b/qwen3-tts-frontend/components.json new file mode 100644 index 0000000..e2c49ef --- /dev/null +++ b/qwen3-tts-frontend/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} diff --git a/qwen3-tts-frontend/eslint.config.js b/qwen3-tts-frontend/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/qwen3-tts-frontend/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/qwen3-tts-frontend/fonts/noto-serif-regular.woff2 b/qwen3-tts-frontend/fonts/noto-serif-regular.woff2 new file mode 100644 index 0000000..c8f0c13 Binary files /dev/null and b/qwen3-tts-frontend/fonts/noto-serif-regular.woff2 differ diff --git a/qwen3-tts-frontend/fonts/noto-serif-sc-regular.woff2 b/qwen3-tts-frontend/fonts/noto-serif-sc-regular.woff2 new file mode 100644 index 0000000..fc14f99 Binary files /dev/null and b/qwen3-tts-frontend/fonts/noto-serif-sc-regular.woff2 differ diff --git a/qwen3-tts-frontend/index.html b/qwen3-tts-frontend/index.html new file mode 100644 index 0000000..c1f1c90 --- /dev/null +++ b/qwen3-tts-frontend/index.html @@ -0,0 +1,25 @@ + + + + + + + + Qwen3-TTS-WebUI + + + +
+ + + diff --git a/qwen3-tts-frontend/package-lock.json b/qwen3-tts-frontend/package-lock.json new file mode 100644 index 0000000..3989442 --- /dev/null +++ b/qwen3-tts-frontend/package-lock.json @@ -0,0 +1,6157 @@ +{ + "name": "qwen3-tts-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "qwen3-tts-frontend", + "version": "0.0.0", + "dependencies": { + "@hookform/resolvers": "^5.2.2", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slider": "^1.3.6", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", + "axios": "^1.13.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.563.0", + "next-themes": "^0.4.6", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-h5-audio-player": "^3.10.1", + "react-hook-form": "^7.71.1", + "react-router-dom": "^7.13.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.4.0", + "zod": "^4.3.6" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.9", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.23", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.19", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@hookform/resolvers": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", + "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@iconify/react": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@iconify/react/-/react-5.2.1.tgz", + "integrity": "sha512-37GDR3fYDZmnmUn9RagyaX+zca24jfVOMY8E1IXTqJuE8pxNtN51KWPQe3VODOWvuUurq7q9uUu3CFrpqj5Iqg==", + "license": "MIT", + "dependencies": { + "@iconify/types": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/cyberalien" + }, + "peerDependencies": { + "react": ">=16" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.8.tgz", + "integrity": "sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-context": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", + "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", + "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz", + "integrity": "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz", + "integrity": "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz", + "integrity": "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz", + "integrity": "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz", + "integrity": "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz", + "integrity": "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz", + "integrity": "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz", + "integrity": "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz", + "integrity": "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz", + "integrity": "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz", + "integrity": "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz", + "integrity": "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz", + "integrity": "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz", + "integrity": "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz", + "integrity": "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz", + "integrity": "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz", + "integrity": "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz", + "integrity": "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz", + "integrity": "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz", + "integrity": "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz", + "integrity": "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz", + "integrity": "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz", + "integrity": "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz", + "integrity": "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz", + "integrity": "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", + "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.9", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz", + "integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.1.tgz", + "integrity": "sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.53.1", + "@typescript-eslint/type-utils": "8.53.1", + "@typescript-eslint/utils": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.53.1", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.1.tgz", + "integrity": "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.53.1", + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/typescript-estree": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.1.tgz", + "integrity": "sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.53.1", + "@typescript-eslint/types": "^8.53.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.1.tgz", + "integrity": "sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.1.tgz", + "integrity": "sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.53.1.tgz", + "integrity": "sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/typescript-estree": "8.53.1", + "@typescript-eslint/utils": "8.53.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.1.tgz", + "integrity": "sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.1.tgz", + "integrity": "sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.53.1", + "@typescript-eslint/tsconfig-utils": "8.53.1", + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.1", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.1.tgz", + "integrity": "sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.53.1", + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/typescript-estree": "8.53.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.1.tgz", + "integrity": "sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.53", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.23", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", + "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001760", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.3.tgz", + "integrity": "sha512-ERT8kdX7DZjtUm7IitEyV7InTHAF42iJuMArIiDIV5YtPanJkgw4hw5Dyg9fh0mihdWNn1GKaeIWErfe56UQ1g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz", + "integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001766", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", + "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "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==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.278", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.278.tgz", + "integrity": "sha512-dQ0tM1svDRQOwxnXxm+twlGTjr9Upvt8UFWAgmLsxEzFQxhbti4VwxmMjsDxVC51Zo84swW7FVCXEV+VAkhuPw==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "license": "MPL-2.0", + "optional": true, + "peer": true, + "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" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "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" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "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" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "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" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "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" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "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" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "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" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "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" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "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" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "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" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "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" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.563.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz", + "integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "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/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "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/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/react-h5-audio-player": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/react-h5-audio-player/-/react-h5-audio-player-3.10.1.tgz", + "integrity": "sha512-r6fSj9WXR6af1kxH5qQ/tawwDK4KrMfayiVCUettLYGX/KZ3BH8OGuaZP4O5KD0AxwsKAXtBv4kVQCWFzaIrUA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.2", + "@iconify/react": "^5" + }, + "peerDependencies": { + "react": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.71.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz", + "integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz", + "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz", + "integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz", + "integrity": "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==", + "dev": true, + "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.56.0", + "@rollup/rollup-android-arm64": "4.56.0", + "@rollup/rollup-darwin-arm64": "4.56.0", + "@rollup/rollup-darwin-x64": "4.56.0", + "@rollup/rollup-freebsd-arm64": "4.56.0", + "@rollup/rollup-freebsd-x64": "4.56.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", + "@rollup/rollup-linux-arm-musleabihf": "4.56.0", + "@rollup/rollup-linux-arm64-gnu": "4.56.0", + "@rollup/rollup-linux-arm64-musl": "4.56.0", + "@rollup/rollup-linux-loong64-gnu": "4.56.0", + "@rollup/rollup-linux-loong64-musl": "4.56.0", + "@rollup/rollup-linux-ppc64-gnu": "4.56.0", + "@rollup/rollup-linux-ppc64-musl": "4.56.0", + "@rollup/rollup-linux-riscv64-gnu": "4.56.0", + "@rollup/rollup-linux-riscv64-musl": "4.56.0", + "@rollup/rollup-linux-s390x-gnu": "4.56.0", + "@rollup/rollup-linux-x64-gnu": "4.56.0", + "@rollup/rollup-linux-x64-musl": "4.56.0", + "@rollup/rollup-openbsd-x64": "4.56.0", + "@rollup/rollup-openharmony-arm64": "4.56.0", + "@rollup/rollup-win32-arm64-msvc": "4.56.0", + "@rollup/rollup-win32-ia32-msvc": "4.56.0", + "@rollup/rollup-win32-x64-gnu": "4.56.0", + "@rollup/rollup-win32-x64-msvc": "4.56.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "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/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.53.1.tgz", + "integrity": "sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.53.1", + "@typescript-eslint/parser": "8.53.1", + "@typescript-eslint/typescript-estree": "8.53.1", + "@typescript-eslint/utils": "8.53.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.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/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/qwen3-tts-frontend/package.json b/qwen3-tts-frontend/package.json new file mode 100644 index 0000000..6fbff31 --- /dev/null +++ b/qwen3-tts-frontend/package.json @@ -0,0 +1,58 @@ +{ + "name": "qwen3-tts-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@hookform/resolvers": "^5.2.2", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slider": "^1.3.6", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", + "axios": "^1.13.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.563.0", + "next-themes": "^0.4.6", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-h5-audio-player": "^3.10.1", + "react-hook-form": "^7.71.1", + "react-router-dom": "^7.13.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.4.0", + "zod": "^4.3.6" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.9", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.23", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.19", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } +} diff --git a/qwen3-tts-frontend/postcss.config.js b/qwen3-tts-frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/qwen3-tts-frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/qwen3-tts-frontend/public/vite.svg b/qwen3-tts-frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/qwen3-tts-frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/qwen3-tts-frontend/src/App.tsx b/qwen3-tts-frontend/src/App.tsx new file mode 100644 index 0000000..6e2a2b9 --- /dev/null +++ b/qwen3-tts-frontend/src/App.tsx @@ -0,0 +1,98 @@ +import { lazy, Suspense } from 'react' +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' +import { Toaster } from 'sonner' +import { ThemeProvider } from '@/contexts/ThemeContext' +import { AuthProvider, useAuth } from '@/contexts/AuthContext' +import { AppProvider } from '@/contexts/AppContext' +import { JobProvider } from '@/contexts/JobContext' +import ErrorBoundary from '@/components/ErrorBoundary' +import LoadingScreen from '@/components/LoadingScreen' +import { SuperAdminRoute } from '@/components/SuperAdminRoute' + +const Login = lazy(() => import('@/pages/Login')) +const Home = lazy(() => import('@/pages/Home')) +const UserManagement = lazy(() => import('@/pages/UserManagement')) + +function ProtectedRoute({ children }: { children: React.ReactNode }) { + const { isAuthenticated, isLoading } = useAuth() + + if (isLoading) { + return ( +
+
加载中...
+
+ ) + } + + if (!isAuthenticated) { + return + } + + return <>{children} +} + +function PublicRoute({ children }: { children: React.ReactNode }) { + const { isAuthenticated, isLoading } = useAuth() + + if (isLoading) { + return ( +
+
加载中...
+
+ ) + } + + if (isAuthenticated) { + return + } + + return <>{children} +} + +function App() { + return ( + + + + + + }> + + + + + } + /> + + + + + + + + } + /> + + + + } + /> + + + + + + + ) +} + +export default App diff --git a/qwen3-tts-frontend/src/assets/react.svg b/qwen3-tts-frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/qwen3-tts-frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/qwen3-tts-frontend/src/components/AudioInputSelector.tsx b/qwen3-tts-frontend/src/components/AudioInputSelector.tsx new file mode 100644 index 0000000..0cb22ec --- /dev/null +++ b/qwen3-tts-frontend/src/components/AudioInputSelector.tsx @@ -0,0 +1,44 @@ +import { useState } from 'react' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { Upload, Mic } from 'lucide-react' +import { FileUploader } from '@/components/FileUploader' +import { AudioRecorder } from '@/components/AudioRecorder' + +interface AudioInputSelectorProps { + value: File | null + onChange: (file: File | null) => void + error?: string +} + +export function AudioInputSelector({ value, onChange, error }: AudioInputSelectorProps) { + const [activeTab, setActiveTab] = useState('upload') + + const handleTabChange = (newTab: string) => { + onChange(null) + setActiveTab(newTab) + } + + return ( + + + + + 上传文件 + + + + 录制音频 + + + + + + + + + + {error &&

{error}

} +
+
+ ) +} diff --git a/qwen3-tts-frontend/src/components/AudioPlayer.module.css b/qwen3-tts-frontend/src/components/AudioPlayer.module.css new file mode 100644 index 0000000..34e5778 --- /dev/null +++ b/qwen3-tts-frontend/src/components/AudioPlayer.module.css @@ -0,0 +1,102 @@ +.audioPlayerWrapper { + display: flex; + align-items: center; + gap: 0.5rem; + border: 1px solid hsl(var(--border)); + border-radius: var(--radius); + padding: 0.75rem; + background: transparent; +} + +.audioPlayerWrapper :global(.rhap_container) { + flex: 1; + background-color: transparent; + box-shadow: none; + padding: 0; +} + +.audioPlayerWrapper :global(.rhap_main) { + --rhap_theme-color: hsl(var(--primary)); + --rhap_background-color: transparent; + --rhap_bar-color: hsl(var(--secondary)); + --rhap_time-color: hsl(var(--muted-foreground)); +} + +.audioPlayerWrapper :global(.rhap_progress-indicator), +.audioPlayerWrapper :global(.rhap_volume-indicator) { + background: hsl(var(--primary)); +} + +.audioPlayerWrapper :global(.rhap_progress-filled), +.audioPlayerWrapper :global(.rhap_volume-bar) { + background-color: hsl(var(--primary)); +} + +.audioPlayerWrapper :global(.rhap_progress-bar), +.audioPlayerWrapper :global(.rhap_volume-container) { + background-color: hsl(var(--secondary)); +} + +.audioPlayerWrapper :global(.rhap_progress-bar) { + height: 6px; + border-radius: 3px; + transition: height 0.15s ease; +} + +.audioPlayerWrapper :global(.rhap_progress-bar):hover { + height: 7px; +} + +.audioPlayerWrapper :global(.rhap_progress-filled) { + border-radius: 3px; +} + +.audioPlayerWrapper :global(.rhap_progress-indicator) { + width: 14px; + height: 14px; + top: -4px; + margin-left: -7px; + transition: transform 0.15s ease, box-shadow 0.15s ease; +} + +.audioPlayerWrapper :global(.rhap_progress-indicator):hover { + transform: scale(1.1); +} + +.audioPlayerWrapper :global(.rhap_progress-container) { + margin: 0 0.5rem; +} + +.audioPlayerWrapper :global(.rhap_horizontal .rhap_controls-section) { + margin-left: 0; +} + +.audioPlayerWrapper :global(.rhap_time) { + color: hsl(var(--muted-foreground)); + font-size: 0.875rem; + font-weight: 500; +} + +.audioPlayerWrapper :global(.rhap_button-clear) { + color: hsl(var(--foreground)); + font-size: 1.25rem; +} + +.audioPlayerWrapper :global(.rhap_button-clear):hover { + color: hsl(var(--primary)); +} + +.audioPlayerWrapper :global(.rhap_main-controls-button) { + width: 40px; + height: 40px; +} + +.audioPlayerWrapper :global(.rhap_main-controls-button svg) { + width: 22px; + height: 22px; +} + +.downloadButton { + min-height: 40px; + min-width: 40px; +} diff --git a/qwen3-tts-frontend/src/components/AudioPlayer.tsx b/qwen3-tts-frontend/src/components/AudioPlayer.tsx new file mode 100644 index 0000000..7ac8643 --- /dev/null +++ b/qwen3-tts-frontend/src/components/AudioPlayer.tsx @@ -0,0 +1,121 @@ +import { useRef, useState, useEffect, useCallback, memo } from 'react' +import AudioPlayerLib from 'react-h5-audio-player' +import 'react-h5-audio-player/lib/styles.css' +import { Button } from '@/components/ui/button' +import { Download } from 'lucide-react' +import apiClient from '@/lib/api' +import styles from './AudioPlayer.module.css' + +interface AudioPlayerProps { + audioUrl: string + jobId: number +} + +const AudioPlayer = memo(({ audioUrl, jobId }: AudioPlayerProps) => { + const [blobUrl, setBlobUrl] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [loadError, setLoadError] = useState(null) + const previousAudioUrlRef = useRef('') + const playerRef = useRef(null) + + useEffect(() => { + if (!audioUrl || audioUrl === previousAudioUrlRef.current) return + + let active = true + const prevBlobUrl = blobUrl + + const fetchAudio = async () => { + setIsLoading(true) + setLoadError(null) + + if (prevBlobUrl) { + URL.revokeObjectURL(prevBlobUrl) + } + + try { + const response = await apiClient.get(audioUrl, { responseType: 'blob' }) + if (active) { + const url = URL.createObjectURL(response.data) + setBlobUrl(url) + previousAudioUrlRef.current = audioUrl + } + } catch (error) { + console.error("Failed to load audio:", error) + if (active) { + setLoadError('Failed to load audio') + } + } finally { + if (active) { + setIsLoading(false) + } + } + } + + fetchAudio() + + return () => { + active = false + } + }, [audioUrl]) + + useEffect(() => { + return () => { + if (blobUrl) URL.revokeObjectURL(blobUrl) + } + }, []) + + const handleDownload = useCallback(() => { + const link = document.createElement('a') + link.href = blobUrl || audioUrl + link.download = `tts-${jobId}-${Date.now()}.wav` + link.click() + }, [blobUrl, audioUrl, jobId]) + + if (isLoading) { + return ( +
+ Loading... +
+ ) + } + + if (loadError) { + return ( +
+ {loadError} +
+ ) + } + + if (!blobUrl) { + return null + } + + return ( +
+ + + + ]} + customVolumeControls={[]} + showJumpControls={false} + volume={1} + /> +
+ ) +}) + +AudioPlayer.displayName = 'AudioPlayer' + +export { AudioPlayer } diff --git a/qwen3-tts-frontend/src/components/AudioRecorder.tsx b/qwen3-tts-frontend/src/components/AudioRecorder.tsx new file mode 100644 index 0000000..ea2aeb6 --- /dev/null +++ b/qwen3-tts-frontend/src/components/AudioRecorder.tsx @@ -0,0 +1,153 @@ +import { useEffect, useState } from 'react' +import { Button } from '@/components/ui/button' +import { Mic, Trash2, RotateCcw, FileAudio } from 'lucide-react' +import { toast } from 'sonner' +import { useAudioRecorder } from '@/hooks/useAudioRecorder' +import { useAudioValidation } from '@/hooks/useAudioValidation' + +interface AudioRecorderProps { + onChange: (file: File | null) => void +} + +export function AudioRecorder({ onChange }: AudioRecorderProps) { + const { + isRecording, + recordingDuration, + audioBlob, + error: recorderError, + isSupported, + startRecording, + stopRecording, + clearRecording, + } = useAudioRecorder() + + const { validateAudioFile } = useAudioValidation() + const [audioInfo, setAudioInfo] = useState<{ duration: number; size: number } | null>(null) + const [validationError, setValidationError] = useState(null) + + useEffect(() => { + if (recorderError) { + toast.error(recorderError) + } + }, [recorderError]) + + useEffect(() => { + if (audioBlob) { + handleValidateRecording(audioBlob) + } + }, [audioBlob]) + + const handleValidateRecording = async (blob: Blob) => { + const file = new File([blob], 'recording.wav', { type: 'audio/wav' }) + + const result = await validateAudioFile(file) + + if (result.valid && result.duration) { + onChange(file) + setAudioInfo({ duration: result.duration, size: file.size }) + setValidationError(null) + } else { + setValidationError(result.error || '录音验证失败') + clearRecording() + onChange(null) + } + } + + const handleMouseDown = () => { + if (!isRecording && !audioBlob) { + startRecording() + } + } + + const handleMouseUp = () => { + if (isRecording) { + stopRecording() + } + } + + const handleReset = () => { + clearRecording() + setAudioInfo(null) + setValidationError(null) + onChange(null) + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === ' ' && !isRecording && !audioBlob) { + e.preventDefault() + startRecording() + } + } + + const handleKeyUp = (e: React.KeyboardEvent) => { + if (e.key === ' ' && isRecording) { + e.preventDefault() + stopRecording() + } + } + + if (!isSupported) { + return ( +
+ 您的浏览器不支持录音功能 +
+ ) + } + + if (audioBlob && audioInfo) { + return ( +
+
+ +
+

录制完成

+

+ {(audioInfo.size / 1024 / 1024).toFixed(2)} MB · {audioInfo.duration.toFixed(1)} 秒 +

+
+ +
+
+ ) + } + + return ( +
+ + + {validationError && ( +
+

{validationError}

+ +
+ )} +
+ ) +} diff --git a/qwen3-tts-frontend/src/components/ErrorBoundary.tsx b/qwen3-tts-frontend/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..28ea5ac --- /dev/null +++ b/qwen3-tts-frontend/src/components/ErrorBoundary.tsx @@ -0,0 +1,73 @@ +import { Component, type ReactNode } from 'react'; + +interface Props { + children: ReactNode; +} + +interface State { + hasError: boolean; + error: Error | null; +} + +class ErrorBoundary extends Component { + constructor(props: Props) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error('ErrorBoundary caught error:', error, errorInfo); + } + + handleReset = () => { + this.setState({ hasError: false, error: null }); + }; + + render() { + if (this.state.hasError) { + return ( +
+
+
+

Something went wrong

+

+ An unexpected error occurred. Please try refreshing the page. +

+
+ + {this.state.error && ( +
+

+ {this.state.error.message} +

+
+ )} + +
+ + +
+
+
+ ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/qwen3-tts-frontend/src/components/FileUploader.tsx b/qwen3-tts-frontend/src/components/FileUploader.tsx new file mode 100644 index 0000000..a84ef84 --- /dev/null +++ b/qwen3-tts-frontend/src/components/FileUploader.tsx @@ -0,0 +1,89 @@ +import { useRef, useState, type ChangeEvent } from 'react' +import { Button } from '@/components/ui/button' +import { Upload, X, FileAudio } from 'lucide-react' +import { toast } from 'sonner' +import { useAudioValidation } from '@/hooks/useAudioValidation' + +interface AudioInfo { + duration: number + size: number +} + +interface FileUploaderProps { + value: File | null + onChange: (file: File | null) => void + error?: string +} + +export function FileUploader({ value, onChange, error }: FileUploaderProps) { + const inputRef = useRef(null) + const { validateAudioFile } = useAudioValidation() + const [isValidating, setIsValidating] = useState(false) + const [audioInfo, setAudioInfo] = useState(null) + + const handleFileSelect = async (e: ChangeEvent) => { + const file = e.target.files?.[0] + if (!file) return + + setIsValidating(true) + const result = await validateAudioFile(file) + setIsValidating(false) + + if (result.valid && result.duration) { + onChange(file) + setAudioInfo({ duration: result.duration, size: file.size }) + } else { + toast.error(result.error || '文件验证失败') + e.target.value = '' + } + } + + const handleRemove = () => { + onChange(null) + setAudioInfo(null) + if (inputRef.current) { + inputRef.current.value = '' + } + } + + return ( +
+ {!value ? ( + + ) : ( +
+ +
+

{value.name}

+ {audioInfo && ( +

+ {(audioInfo.size / 1024 / 1024).toFixed(2)} MB · {audioInfo.duration.toFixed(1)} 秒 +

+ )} +
+ +
+ )} + + + + {error &&

{error}

} +
+ ) +} diff --git a/qwen3-tts-frontend/src/components/FormSkeleton.tsx b/qwen3-tts-frontend/src/components/FormSkeleton.tsx new file mode 100644 index 0000000..30d1ea2 --- /dev/null +++ b/qwen3-tts-frontend/src/components/FormSkeleton.tsx @@ -0,0 +1,29 @@ +const FormSkeleton = () => { + return ( +
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+ ); +}; + +export default FormSkeleton; diff --git a/qwen3-tts-frontend/src/components/HistoryItem.tsx b/qwen3-tts-frontend/src/components/HistoryItem.tsx new file mode 100644 index 0000000..caee2ed --- /dev/null +++ b/qwen3-tts-frontend/src/components/HistoryItem.tsx @@ -0,0 +1,172 @@ +import { memo, useState } from 'react' +import type { Job } from '@/types/job' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog' +import { Trash2, AlertCircle, Loader2, Clock, Eye } from 'lucide-react' +import { getRelativeTime, cn } from '@/lib/utils' +import { JobDetailDialog } from '@/components/JobDetailDialog' + +interface HistoryItemProps { + job: Job + onDelete: (id: number) => void + onLoadParams: (job: Job) => void +} + +const jobTypeBadgeVariant = { + custom_voice: 'default' as const, + voice_design: 'secondary' as const, + voice_clone: 'outline' as const, +} + +const jobTypeLabel = { + custom_voice: '自定义音色', + voice_design: '音色设计', + voice_clone: '声音克隆', +} + +const HistoryItem = memo(({ job, onDelete, onLoadParams }: HistoryItemProps) => { + const [detailDialogOpen, setDetailDialogOpen] = useState(false) + + const getLanguageDisplay = (lang: string | undefined) => { + if (!lang || lang === 'Auto') return '自动检测' + return lang + } + + const handleCardClick = (e: React.MouseEvent) => { + if ((e.target as HTMLElement).closest('button')) return + setDetailDialogOpen(true) + } + + return ( +
+
+ + {jobTypeLabel[job.type]} + +
+ {getRelativeTime(job.created_at)} + +
+
+ +
+ {job.parameters?.text && ( +
+ 文本内容: + {job.parameters.text} +
+ )} + +
+ 语言: {getLanguageDisplay(job.parameters?.language)} +
+ + {job.type === 'custom_voice' && job.parameters?.speaker && ( +
+ 发音人: {job.parameters.speaker} +
+ )} + + {job.type === 'voice_design' && job.parameters?.instruct && ( +
+ 音色描述: + {job.parameters.instruct} +
+ )} + + {job.type === 'voice_clone' && job.parameters?.ref_text && ( +
+ 参考文本: + {job.parameters.ref_text} +
+ )} +
+ + {job.status === 'processing' && ( +
+ + 处理中... +
+ )} + + {job.status === 'pending' && ( +
+ + 等待处理... +
+ )} + + {job.status === 'failed' && job.error_message && ( +
+ + {job.error_message} +
+ )} + +
+ + + + + + + 确认删除 + + 确定要删除这条历史记录吗?此操作无法撤销。 + + + + 取消 + onDelete(job.id)} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + 删除 + + + + +
+ + +
+ ) +}, (prevProps, nextProps) => { + return ( + prevProps.job.id === nextProps.job.id && + prevProps.job.status === nextProps.job.status && + prevProps.job.updated_at === nextProps.job.updated_at && + prevProps.job.error_message === nextProps.job.error_message + ) +}) + +HistoryItem.displayName = 'HistoryItem' + +export { HistoryItem } diff --git a/qwen3-tts-frontend/src/components/HistorySidebar.tsx b/qwen3-tts-frontend/src/components/HistorySidebar.tsx new file mode 100644 index 0000000..cb1052c --- /dev/null +++ b/qwen3-tts-frontend/src/components/HistorySidebar.tsx @@ -0,0 +1,113 @@ +import { useRef, useEffect } from 'react' +import { useHistory } from '@/hooks/useHistory' +import { HistoryItem } from '@/components/HistoryItem' +import { ScrollArea } from '@/components/ui/scroll-area' +import { Sheet, SheetContent } from '@/components/ui/sheet' +import { Button } from '@/components/ui/button' +import { Loader2, FileAudio, RefreshCw } from 'lucide-react' +import type { JobType } from '@/types/job' +import { toast } from 'sonner' + +interface HistorySidebarProps { + open: boolean + onOpenChange: (open: boolean) => void + onLoadParams: (jobId: number, jobType: JobType) => Promise +} + +function HistorySidebarContent({ onLoadParams }: Pick) { + const { jobs, loading, loadingMore, hasMore, loadMore, deleteJob, error, retry } = useHistory() + const observerTarget = useRef(null) + + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasMore && !loadingMore) { + loadMore() + } + }, + { threshold: 0.5 } + ) + + if (observerTarget.current) { + observer.observe(observerTarget.current) + } + + return () => observer.disconnect() + }, [hasMore, loadingMore, loadMore]) + + const handleLoadParams = async (jobId: number, jobType: JobType) => { + try { + await onLoadParams(jobId, jobType) + } catch (error) { + toast.error('加载参数失败') + } + } + + return ( +
+
+

历史记录

+

共 {jobs.length} 条记录

+
+ + +
+ {loading ? ( +
+ +
+ ) : error ? ( +
+

{error}

+ +
+ ) : jobs.length === 0 ? ( +
+ +

暂无历史记录

+

+ 生成语音后,记录将会显示在这里 +

+
+ ) : ( + <> + {jobs.map((job) => ( + handleLoadParams(job.id, job.type)} + /> + ))} + + {hasMore && ( +
+ +
+ )} + + )} +
+
+
+ ) +} + +export function HistorySidebar({ open, onOpenChange, onLoadParams }: HistorySidebarProps) { + return ( + <> + + + + + + + + + ) +} diff --git a/qwen3-tts-frontend/src/components/JobDetailDialog.tsx b/qwen3-tts-frontend/src/components/JobDetailDialog.tsx new file mode 100644 index 0000000..39676c2 --- /dev/null +++ b/qwen3-tts-frontend/src/components/JobDetailDialog.tsx @@ -0,0 +1,230 @@ +import { memo } from 'react' +import type { Job } from '@/types/job' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Badge } from '@/components/ui/badge' +import { Separator } from '@/components/ui/separator' +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from '@/components/ui/collapsible' +import { ScrollArea } from '@/components/ui/scroll-area' +import { AudioPlayer } from '@/components/AudioPlayer' +import { ChevronDown, AlertCircle } from 'lucide-react' +import { jobApi } from '@/lib/api' + +interface JobDetailDialogProps { + job: Job | null + open: boolean + onOpenChange: (open: boolean) => void +} + +const jobTypeBadgeVariant = { + custom_voice: 'default' as const, + voice_design: 'secondary' as const, + voice_clone: 'outline' as const, +} + +const jobTypeLabel = { + custom_voice: '自定义音色', + voice_design: '音色设计', + voice_clone: '声音克隆', +} + +const formatTimestamp = (timestamp: string) => { + return new Date(timestamp).toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }) +} + +const getLanguageDisplay = (lang: string | undefined) => { + if (!lang || lang === 'Auto') return '自动检测' + return lang +} + +const formatBooleanDisplay = (value: boolean | undefined) => { + return value ? '是' : '否' +} + +const JobDetailDialog = memo(({ job, open, onOpenChange }: JobDetailDialogProps) => { + if (!job) return null + + const canPlay = job.status === 'completed' + const audioUrl = canPlay ? jobApi.getAudioUrl(job.id, job.audio_url) : '' + + return ( + + + +
+ + + {jobTypeLabel[job.type]} + + #{job.id} + + + {formatTimestamp(job.created_at)} + +
+
+ + +
+
+

基本信息

+
+ {job.type === 'custom_voice' && job.parameters?.speaker && ( +
+ 发音人: + {job.parameters.speaker} +
+ )} +
+ 语言: + {getLanguageDisplay(job.parameters?.language)} +
+ {job.type === 'voice_clone' && ( + <> +
+ 快速模式: + {formatBooleanDisplay(job.parameters?.x_vector_only_mode)} +
+
+ 使用缓存: + {formatBooleanDisplay(job.parameters?.use_cache)} +
+ + )} +
+
+ + + +
+

合成文本

+
+ {job.parameters?.text || 未设置} +
+
+ + {job.type === 'voice_design' && job.parameters?.instruct && ( + <> + +
+

音色描述

+
+ {job.parameters.instruct} +
+
+ + )} + + {job.type === 'custom_voice' && job.parameters?.instruct && ( + <> + +
+

情绪指导

+
+ {job.parameters.instruct} +
+
+ + )} + + {job.type === 'voice_clone' && ( + <> + +
+

参考文本

+
+ {job.parameters?.ref_text || 未提供} +
+
+ + )} + + + + + + 高级参数 + + + +
+ {job.parameters?.max_new_tokens !== undefined && ( +
+ 最大生成长度: + {job.parameters.max_new_tokens} +
+ )} + {job.parameters?.temperature !== undefined && ( +
+ 温度: + {job.parameters.temperature} +
+ )} + {job.parameters?.top_k !== undefined && ( +
+ Top K: + {job.parameters.top_k} +
+ )} + {job.parameters?.top_p !== undefined && ( +
+ Top P: + {job.parameters.top_p} +
+ )} + {job.parameters?.repetition_penalty !== undefined && ( +
+ 重复惩罚: + {job.parameters.repetition_penalty} +
+ )} +
+
+
+ + {job.status === 'failed' && job.error_message && ( + <> + +
+ +
+

错误信息

+

{job.error_message}

+
+
+ + )} + + {canPlay && ( + <> + +
+

音频播放

+ +
+ + )} +
+
+
+
+ ) +}) + +JobDetailDialog.displayName = 'JobDetailDialog' + +export { JobDetailDialog } diff --git a/qwen3-tts-frontend/src/components/LoadingScreen.tsx b/qwen3-tts-frontend/src/components/LoadingScreen.tsx new file mode 100644 index 0000000..3f73eac --- /dev/null +++ b/qwen3-tts-frontend/src/components/LoadingScreen.tsx @@ -0,0 +1,12 @@ +const LoadingScreen = () => { + return ( +
+
+
+

Loading...

+
+
+ ); +}; + +export default LoadingScreen; diff --git a/qwen3-tts-frontend/src/components/LoadingState.tsx b/qwen3-tts-frontend/src/components/LoadingState.tsx new file mode 100644 index 0000000..f1e0de3 --- /dev/null +++ b/qwen3-tts-frontend/src/components/LoadingState.tsx @@ -0,0 +1,24 @@ +import { memo } from 'react' + +interface LoadingStateProps { + elapsedTime: number +} + +const LoadingState = memo(({ elapsedTime }: LoadingStateProps) => { + const displayText = elapsedTime > 60 + ? '生成用时较长,请耐心等待...' + : '正在生成音频,请稍候...' + + return ( +
+

{displayText}

+

+ 已等待 {elapsedTime} 秒 +

+
+ ) +}) + +LoadingState.displayName = 'LoadingState' + +export { LoadingState } diff --git a/qwen3-tts-frontend/src/components/Navbar.tsx b/qwen3-tts-frontend/src/components/Navbar.tsx new file mode 100644 index 0000000..3583b33 --- /dev/null +++ b/qwen3-tts-frontend/src/components/Navbar.tsx @@ -0,0 +1,50 @@ +import { Menu, LogOut, Users } from 'lucide-react' +import { Link } from 'react-router-dom' +import { Button } from '@/components/ui/button' +import { ThemeToggle } from '@/components/ThemeToggle' +import { useAuth } from '@/contexts/AuthContext' + +interface NavbarProps { + onToggleSidebar?: () => void +} + +export function Navbar({ onToggleSidebar }: NavbarProps) { + const { logout, user } = useAuth() + + return ( + + ) +} diff --git a/qwen3-tts-frontend/src/components/ParamInput.tsx b/qwen3-tts-frontend/src/components/ParamInput.tsx new file mode 100644 index 0000000..4a37e36 --- /dev/null +++ b/qwen3-tts-frontend/src/components/ParamInput.tsx @@ -0,0 +1,55 @@ +import { Label } from '@/components/ui/label' +import { Input } from '@/components/ui/input' +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' +import { HelpCircle } from 'lucide-react' +import type { UseFormRegister, FieldValues, Path } from 'react-hook-form' + +interface ParamInputProps { + name: Path + label: string + description: string + tooltip: string + register: UseFormRegister + type?: 'number' + step?: number + min?: number + max?: number +} + +export function ParamInput({ + name, + label, + description, + tooltip, + register, + type = 'number', + step, + min, + max, +}: ParamInputProps) { + return ( +
+
+ + + + + + + +

{tooltip}

+
+
+
+
+ +

{description}

+
+ ) +} diff --git a/qwen3-tts-frontend/src/components/PresetSelector.tsx b/qwen3-tts-frontend/src/components/PresetSelector.tsx new file mode 100644 index 0000000..1f23775 --- /dev/null +++ b/qwen3-tts-frontend/src/components/PresetSelector.tsx @@ -0,0 +1,37 @@ +import { memo, useMemo } from 'react' +import { Button } from '@/components/ui/button' + +interface Preset { + label: string + [key: string]: any +} + +interface PresetSelectorProps { + presets: readonly T[] + onSelect: (preset: T) => void +} + +const PresetSelectorInner = ({ presets, onSelect }: PresetSelectorProps) => { + const presetButtons = useMemo(() => { + return presets.map((preset, index) => ( + + )) + }, [presets, onSelect]) + + return ( +
+ {presetButtons} +
+ ) +} + +export const PresetSelector = memo(PresetSelectorInner) as typeof PresetSelectorInner diff --git a/qwen3-tts-frontend/src/components/SuperAdminRoute.tsx b/qwen3-tts-frontend/src/components/SuperAdminRoute.tsx new file mode 100644 index 0000000..1ed6d33 --- /dev/null +++ b/qwen3-tts-frontend/src/components/SuperAdminRoute.tsx @@ -0,0 +1,21 @@ +import { Navigate } from 'react-router-dom' +import { useAuth } from '@/contexts/AuthContext' +import LoadingScreen from '@/components/LoadingScreen' + +export function SuperAdminRoute({ children }: { children: React.ReactNode }) { + const { isAuthenticated, isLoading, user } = useAuth() + + if (isLoading) { + return + } + + if (!isAuthenticated) { + return + } + + if (!user?.is_superuser) { + return + } + + return <>{children} +} diff --git a/qwen3-tts-frontend/src/components/ThemeToggle.tsx b/qwen3-tts-frontend/src/components/ThemeToggle.tsx new file mode 100644 index 0000000..ca766f1 --- /dev/null +++ b/qwen3-tts-frontend/src/components/ThemeToggle.tsx @@ -0,0 +1,17 @@ +import { Sun, Moon } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { useTheme } from '@/contexts/ThemeContext' + +export function ThemeToggle() { + const { theme, toggleTheme } = useTheme() + + return ( + + ) +} diff --git a/qwen3-tts-frontend/src/components/tts/CustomVoiceForm.tsx b/qwen3-tts-frontend/src/components/tts/CustomVoiceForm.tsx new file mode 100644 index 0000000..1ba81ac --- /dev/null +++ b/qwen3-tts-frontend/src/components/tts/CustomVoiceForm.tsx @@ -0,0 +1,276 @@ +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import * as z from 'zod' +import { useEffect, useState, forwardRef, useImperativeHandle, useMemo } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Textarea } from '@/components/ui/textarea' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible' +import { Label } from '@/components/ui/label' +import { ChevronDown } from 'lucide-react' +import { toast } from 'sonner' +import { ttsApi, jobApi } from '@/lib/api' +import { useJobPolling } from '@/hooks/useJobPolling' +import { LoadingState } from '@/components/LoadingState' +import { AudioPlayer } from '@/components/AudioPlayer' +import { PresetSelector } from '@/components/PresetSelector' +import { ParamInput } from '@/components/ParamInput' +import { PRESET_INSTRUCTS, ADVANCED_PARAMS_INFO } from '@/lib/constants' +import type { Language, Speaker } from '@/types/tts' + +const formSchema = z.object({ + text: z.string().min(1, '请输入要合成的文本').max(5000, '文本长度不能超过 5000 字符'), + language: z.string().min(1, '请选择语言'), + speaker: z.string().min(1, '请选择发音人'), + instruct: z.string().optional(), + max_new_tokens: z.number().min(1).max(10000).optional(), + temperature: z.number().min(0).max(2).optional(), + top_k: z.number().min(1).max(100).optional(), + top_p: z.number().min(0).max(1).optional(), + repetition_penalty: z.number().min(0).max(2).optional(), +}) + +type FormData = z.infer + +export interface CustomVoiceFormHandle { + loadParams: (params: any) => void +} + +const CustomVoiceForm = forwardRef((_props, ref) => { + const [languages, setLanguages] = useState([]) + const [speakers, setSpeakers] = useState([]) + const [isLoading, setIsLoading] = useState(false) + const [advancedOpen, setAdvancedOpen] = useState(false) + + const { currentJob, isPolling, isCompleted, startPolling, elapsedTime } = useJobPolling() + + const { + register, + handleSubmit, + setValue, + watch, + formState: { errors }, + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + text: '', + language: 'Auto', + speaker: '', + instruct: '', + max_new_tokens: 2048, + temperature: 0.3, + top_k: 20, + top_p: 0.7, + repetition_penalty: 1.05, + }, + }) + + useImperativeHandle(ref, () => ({ + loadParams: (params: any) => { + setValue('text', params.text || '') + setValue('language', params.language || 'Auto') + setValue('speaker', params.speaker || '') + setValue('instruct', params.instruct || '') + setValue('max_new_tokens', params.max_new_tokens || 2048) + setValue('temperature', params.temperature || 0.3) + setValue('top_k', params.top_k || 20) + setValue('top_p', params.top_p || 0.7) + setValue('repetition_penalty', params.repetition_penalty || 1.05) + } + })) + + useEffect(() => { + const fetchData = async () => { + try { + const [langs, spks] = await Promise.all([ + ttsApi.getLanguages(), + ttsApi.getSpeakers(), + ]) + setLanguages(langs) + setSpeakers(spks) + } catch (error) { + toast.error('加载数据失败') + } + } + fetchData() + }, []) + + + const onSubmit = async (data: FormData) => { + setIsLoading(true) + try { + const result = await ttsApi.createCustomVoiceJob(data) + toast.success('任务已创建') + startPolling(result.job_id) + } catch (error) { + toast.error('创建任务失败') + } finally { + setIsLoading(false) + } + } + + const memoizedAudioUrl = useMemo(() => { + if (!currentJob) return '' + return jobApi.getAudioUrl(currentJob.id, currentJob.audio_url) + }, [currentJob?.id, currentJob?.audio_url]) + + return ( +
+
+ + + {errors.language && ( +

{errors.language.message}

+ )} +
+ +
+ + + {errors.speaker && ( +

{errors.speaker.message}

+ )} +
+ +
+ +