From b80e97799fbd6fb7a67d04a3f281e5f56fcaa7c1 Mon Sep 17 00:00:00 2001 From: 1iaan Date: Thu, 9 Apr 2026 08:47:37 +0000 Subject: [PATCH] initialize --- .gitignore | 13 + README.md | 259 +++++ backend/app/__init__.py | 1 + backend/app/config.py | 12 + backend/app/main.py | 71 ++ backend/app/schemas.py | 36 + backend/app/services/media.py | 104 ++ backend/app/services/storage.py | 23 + backend/app/services/tasks.py | 124 +++ backend/requirements.txt | 3 + frontend/index.html | 12 + frontend/package-lock.json | 1677 +++++++++++++++++++++++++++++++ frontend/package.json | 19 + frontend/src/App.jsx | 258 +++++ frontend/src/main.jsx | 10 + frontend/src/styles.css | 264 +++++ frontend/vite.config.js | 9 + prompt.md | 116 +++ storage/covers/.gitkeep | 1 + storage/hls/.gitkeep | 1 + storage/processed/.gitkeep | 1 + storage/uploads/.gitkeep | 1 + 22 files changed, 3015 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 backend/app/__init__.py create mode 100644 backend/app/config.py create mode 100644 backend/app/main.py create mode 100644 backend/app/schemas.py create mode 100644 backend/app/services/media.py create mode 100644 backend/app/services/storage.py create mode 100644 backend/app/services/tasks.py create mode 100644 backend/requirements.txt create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/src/App.jsx create mode 100644 frontend/src/main.jsx create mode 100644 frontend/src/styles.css create mode 100644 frontend/vite.config.js create mode 100644 prompt.md create mode 100644 storage/covers/.gitkeep create mode 100644 storage/hls/.gitkeep create mode 100644 storage/processed/.gitkeep create mode 100644 storage/uploads/.gitkeep diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac124d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +__pycache__/ +.pytest_cache/ +backend/.venv/ +frontend/node_modules/ +frontend/dist/ +storage/uploads/* +storage/processed/* +storage/covers/* +storage/hls/* +!storage/uploads/.gitkeep +!storage/processed/.gitkeep +!storage/covers/.gitkeep +!storage/hls/.gitkeep diff --git a/README.md b/README.md new file mode 100644 index 0000000..4b5b7aa --- /dev/null +++ b/README.md @@ -0,0 +1,259 @@ +# VPlatform + +一个面向“通信服务后端 / 音视频处理 / 视频编辑服务 / AIGC 流程”求职方向设计的最小可用项目。它实现了一个完整的音视频处理服务闭环:上传视频、创建异步处理任务、由内存队列中的 worker 调用 FFmpeg 执行处理、返回任务状态与结果,并在前端页面中展示处理产物。 + +## Step 1 - 功能解读 + +### 这个项目解决什么问题 + +很多视频处理场景都不是同步接口能优雅承载的:上传文件后,转码、裁剪、封面抽帧、HLS 切片通常都要几秒到几十秒。如果全部放在请求线程内,会导致接口超时、资源竞争和糟糕的用户体验。 + +这个项目把问题缩成一个 1 天内能交付的最小闭环: + +- 用户上传视频 +- 用户提交处理任务 +- 后端把任务放入内存队列 +- worker 异步执行 FFmpeg 任务 +- 前端轮询查看状态和结果 + +### 为什么贴合 JD + +- 体现了通信服务后端常见的“任务异步化”和“状态可观测” +- 体现了音视频工程能力,真实调用 FFmpeg 而不是伪代码 +- 体现了视频编辑服务常见能力:裁剪、转码、封面、HLS +- 体现了 AIGC/媒体流水线常见的 pipeline 思维 +- 体现了调度系统最小形态:队列、worker、状态、日志 + +### 为什么一天内能完成 + +- 不引入 Redis、MySQL、对象存储等重基础设施 +- 任务队列使用内存 `Queue` +- 文件存储使用本地文件系统 +- 后端选 FastAPI,前端选 React + Vite,开发速度快 +- 核心亮点集中在“处理链路真实可跑通” + +## Step 2 - 技术选型 + +### Frontend + +- `React + Vite` +- 原因:启动快、代码量小、适合快速搭建可展示页面 + +### Backend + +- `Python FastAPI` +- 原因:接口编写快,文件上传和 JSON API 处理顺手,和 FFmpeg 命令行集成成本低 + +### Task Model + +- `内存队列 + 后台 worker 线程` +- 原因:足够展示异步处理模型,不需要为了 demo 引入 Redis / Celery + +### Media Processing + +- `FFmpeg` +- 原因:真实工业标准工具,能直接体现音视频处理能力 + +### Storage + +- `本地文件系统` +- 原因:最容易跑通,便于 GitHub demo 和本地展示 + +## Step 3 - 架构设计 + +### 模块划分 + +- 前端页面:上传文件、创建任务、轮询状态、展示结果 +- FastAPI 接口层:上传接口、创建任务接口、任务查询接口 +- TaskManager:任务元数据管理、状态维护、队列分发 +- Worker:从队列消费任务,调用 FFmpeg 执行媒体处理 +- 本地存储:保存原始文件、转码结果、封面图、HLS 切片 + +### 请求流转 + +1. 前端上传视频到 `/api/upload` +2. 后端保存文件并返回 `file_id` +3. 前端使用 `file_id` 调用 `/api/tasks` +4. 后端将任务入队并返回 `task_id` +5. 前端轮询 `/api/tasks` 或 `/api/tasks/{task_id}` + +### 异步任务流转 + +1. API 创建 `queued` 状态任务 +2. worker 从队列取出任务,更新为 `processing` +3. FFmpeg 依次执行转码、封面、HLS +4. 成功则更新为 `completed` +5. 失败则更新为 `failed` 并记录错误 + +### 存储结构 + +- `storage/uploads/` 原始上传文件 +- `storage/processed/` 处理后的 MP4 +- `storage/covers/` 封面图 +- `storage/hls//` HLS m3u8 与 ts 分片 + +### Mermaid 架构图 + +```mermaid +flowchart LR + A[React Frontend] -->|上传视频| B[FastAPI Upload API] + A -->|创建任务| C[FastAPI Task API] + C --> D[In-Memory Queue] + D --> E[Worker Thread] + E --> F[FFmpeg Pipeline] + F --> G[Local Storage] + A -->|轮询状态| C + C -->|返回状态/结果| A + G -->|静态文件访问| A +``` + +## Step 4 - 分阶段计划 + +- 第 1 小时:设计接口、目录结构、任务模型 +- 第 2 小时:完成 FastAPI 文件上传和任务查询接口 +- 第 3 小时:实现内存队列和 worker +- 第 4 小时:接入 FFmpeg 转码、封面、HLS +- 第 5 小时:搭 React 页面,打通上传和轮询 +- 第 6 小时:联调、异常处理、README 收尾 + +## Step 5 - 项目目录结构 + +```text +vplatform/ +├── backend/ +│ ├── app/ +│ │ ├── services/ +│ │ │ ├── media.py +│ │ │ ├── storage.py +│ │ │ └── tasks.py +│ │ ├── __init__.py +│ │ ├── config.py +│ │ ├── main.py +│ │ └── schemas.py +│ └── requirements.txt +├── frontend/ +│ ├── src/ +│ │ ├── App.jsx +│ │ ├── main.jsx +│ │ └── styles.css +│ ├── index.html +│ ├── package.json +│ └── vite.config.js +├── storage/ +│ ├── covers/ +│ ├── hls/ +│ ├── processed/ +│ └── uploads/ +├── .gitignore +├── README.md +└── prompt.md +``` + +## 功能列表 + +- 上传视频文件 +- 创建异步音视频处理任务 +- 任务状态查询:`queued / processing / completed / failed` +- 视频裁剪前 N 秒 +- MP4 转码并叠加简单文字水印 +- 封面图抽帧 +- HLS 切片输出 +- 前端任务看板和结果展示 + +## 快速启动 + +### 1. 启动后端 + +```bash +cd backend +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +uvicorn app.main:app --reload --port 8000 +``` + +如果本机没有 `python3-venv`,也可以直接: + +```bash +cd backend +python3 -m pip install --user -r requirements.txt +python3 -m uvicorn app.main:app --reload --port 8000 +``` + +### 2. 启动前端 + +```bash +cd frontend +npm install +npm run dev +``` + +### 3. 打开页面 + +- 前端:http://localhost:5173 +- 后端:http://localhost:8000 +- 健康检查:http://localhost:8000/api/health + +## API 说明 + +### `POST /api/upload` + +上传视频文件,返回上传后的 `file_id`。 + +响应示例: + +```json +{ + "file_id": "f8d25e4c0e8b4b2b9b60f1a4d0e6a1df.mp4", + "original_name": "demo.mp4", + "file_url": "/media/uploads/f8d25e4c0e8b4b2b9b60f1a4d0e6a1df.mp4" +} +``` + +### `POST /api/tasks` + +创建异步处理任务。 + +请求示例: + +```json +{ + "file_id": "f8d25e4c0e8b4b2b9b60f1a4d0e6a1df.mp4", + "clip_seconds": 8, + "transcode_mp4": true, + "generate_cover": true, + "generate_hls": true, + "watermark_text": "VPlatform Demo" +} +``` + +### `GET /api/tasks` + +获取全部任务列表,用于前端轮询。 + +### `GET /api/tasks/{task_id}` + +获取单个任务详情。 + +## 页面预览说明 + +- 顶部 Hero 区展示项目定位和技术栈 +- 左侧面板上传视频 +- 右侧面板配置处理参数并提交异步任务 +- 底部任务看板实时展示状态、日志、转码视频、封面图和 HLS 链接 + +## 项目亮点 + +- 用最小成本演示了“上传 -> 入队 -> worker -> FFmpeg -> 结果回查”的完整链路 +- 没有停留在概念层,能真实跑出 MP4、封面图和 HLS 文件 +- 结构足够清晰,后续可自然扩展到 Redis、Celery、对象存储和告警系统 +- 非常适合写进简历,能覆盖后端、音视频、异步任务和前后端联调能力 + +## 后续优化方向 + +- 使用 Redis + Celery / RQ 替换内存队列 +- 接入 MySQL / PostgreSQL 持久化任务状态 +- 上传到对象存储而不是本地磁盘 +- 增加任务超时、重试和告警 +- 接入 WebSocket 推送任务状态,替代前端轮询 +- 支持更多处理能力,例如字幕叠加、GIF 生成、拼接和音轨替换 diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/app/config.py b/backend/app/config.py new file mode 100644 index 0000000..e3a66b5 --- /dev/null +++ b/backend/app/config.py @@ -0,0 +1,12 @@ +from pathlib import Path + + +ROOT_DIR = Path(__file__).resolve().parents[2] +STORAGE_DIR = ROOT_DIR / "storage" +UPLOAD_DIR = STORAGE_DIR / "uploads" +PROCESSED_DIR = STORAGE_DIR / "processed" +HLS_DIR = STORAGE_DIR / "hls" +COVER_DIR = STORAGE_DIR / "covers" + +for directory in (STORAGE_DIR, UPLOAD_DIR, PROCESSED_DIR, HLS_DIR, COVER_DIR): + directory.mkdir(parents=True, exist_ok=True) diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..a225de9 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,71 @@ +from typing import Dict, List + +from fastapi import FastAPI, File, HTTPException, UploadFile +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles + +from app.config import COVER_DIR, HLS_DIR, PROCESSED_DIR, STORAGE_DIR, UPLOAD_DIR +from app.schemas import TaskCreateRequest, TaskResponse, UploadResponse +from app.services.storage import get_upload_path, save_upload +from app.services.tasks import task_manager + + +app = FastAPI(title="VPlatform API", version="0.1.0") +app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:5173"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.mount("/media/uploads", StaticFiles(directory=UPLOAD_DIR), name="uploads") +app.mount("/media/processed", StaticFiles(directory=PROCESSED_DIR), name="processed") +app.mount("/media/hls", StaticFiles(directory=HLS_DIR), name="hls") +app.mount("/media/covers", StaticFiles(directory=COVER_DIR), name="covers") +app.mount("/media", StaticFiles(directory=STORAGE_DIR), name="media") + + +@app.on_event("startup") +def start_worker() -> None: + task_manager.start() + + +@app.get("/api/health") +def health() -> Dict[str, str]: + return {"status": "ok"} + + +@app.post("/api/upload", response_model=UploadResponse) +async def upload_video(file: UploadFile = File(...)) -> UploadResponse: + if not file.filename: + raise HTTPException(status_code=400, detail="missing filename") + + file_id, original_name = save_upload(file) + return UploadResponse( + file_id=file_id, + original_name=original_name, + file_url=f"/media/uploads/{file_id}", + ) + + +@app.post("/api/tasks", response_model=TaskResponse) +def create_task(payload: TaskCreateRequest) -> TaskResponse: + if not get_upload_path(payload.file_id).exists(): + raise HTTPException(status_code=404, detail="uploaded file not found") + + record = task_manager.create_task(payload) + return TaskResponse(**record.__dict__) + + +@app.get("/api/tasks", response_model=List[TaskResponse]) +def list_tasks() -> List[TaskResponse]: + return [TaskResponse(**task.__dict__) for task in task_manager.list_tasks()] + + +@app.get("/api/tasks/{task_id}", response_model=TaskResponse) +def get_task(task_id: str) -> TaskResponse: + task = task_manager.get_task(task_id) + if task is None: + raise HTTPException(status_code=404, detail="task not found") + return TaskResponse(**task.__dict__) diff --git a/backend/app/schemas.py b/backend/app/schemas.py new file mode 100644 index 0000000..1360839 --- /dev/null +++ b/backend/app/schemas.py @@ -0,0 +1,36 @@ +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class UploadResponse(BaseModel): + file_id: str + original_name: str + file_url: str + + +class TaskCreateRequest(BaseModel): + file_id: str = Field(..., description="Uploaded file identifier") + clip_seconds: Optional[int] = Field(default=8, ge=1, le=60) + transcode_mp4: bool = True + generate_cover: bool = True + generate_hls: bool = True + watermark_text: Optional[str] = Field(default="VPlatform Demo", max_length=32) + + +class TaskResult(BaseModel): + mp4_url: Optional[str] = None + cover_url: Optional[str] = None + hls_url: Optional[str] = None + log: List[str] = Field(default_factory=list) + + +class TaskResponse(BaseModel): + task_id: str + status: str + file_id: str + created_at: str + updated_at: str + options: TaskCreateRequest + result: TaskResult + error: Optional[str] = None diff --git a/backend/app/services/media.py b/backend/app/services/media.py new file mode 100644 index 0000000..1d54cfa --- /dev/null +++ b/backend/app/services/media.py @@ -0,0 +1,104 @@ +import subprocess +from pathlib import Path +from typing import List, Optional + +from app.config import COVER_DIR, HLS_DIR, PROCESSED_DIR + + +class FFmpegError(RuntimeError): + pass + + +def run_ffmpeg(command: List[str]) -> None: + completed = subprocess.run( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=False, + ) + if completed.returncode != 0: + raise FFmpegError(completed.stderr.strip() or "ffmpeg command failed") + + +def build_filter(watermark_text: Optional[str]) -> Optional[str]: + if not watermark_text: + return None + escaped = watermark_text.replace(":", r"\:").replace("'", r"\\'") + return ( + "drawtext=text='" + f"{escaped}" + "':fontcolor=white:fontsize=28:box=1:boxcolor=black@0.45:boxborderw=6:x=w-tw-24:y=h-th-24" + ) + + +def transcode_to_mp4( + source: Path, task_id: str, clip_seconds: Optional[int], watermark_text: Optional[str] +) -> Path: + output = PROCESSED_DIR / f"{task_id}.mp4" + command = ["ffmpeg", "-y", "-i", str(source)] + if clip_seconds: + command.extend(["-t", str(clip_seconds)]) + + video_filter = build_filter(watermark_text) + if video_filter: + command.extend(["-vf", video_filter]) + + command.extend( + [ + "-c:v", + "libx264", + "-preset", + "veryfast", + "-c:a", + "aac", + "-movflags", + "+faststart", + str(output), + ] + ) + run_ffmpeg(command) + return output + + +def generate_cover(source: Path, task_id: str) -> Path: + output = COVER_DIR / f"{task_id}.jpg" + command = [ + "ffmpeg", + "-y", + "-ss", + "00:00:01", + "-i", + str(source), + "-frames:v", + "1", + str(output), + ] + run_ffmpeg(command) + return output + + +def generate_hls(source_mp4: Path, task_id: str) -> Path: + task_dir = HLS_DIR / task_id + task_dir.mkdir(parents=True, exist_ok=True) + playlist = task_dir / "index.m3u8" + segment_pattern = task_dir / "segment_%03d.ts" + command = [ + "ffmpeg", + "-y", + "-i", + str(source_mp4), + "-codec:v", + "libx264", + "-codec:a", + "aac", + "-hls_time", + "4", + "-hls_playlist_type", + "vod", + "-hls_segment_filename", + str(segment_pattern), + str(playlist), + ] + run_ffmpeg(command) + return playlist diff --git a/backend/app/services/storage.py b/backend/app/services/storage.py new file mode 100644 index 0000000..0f417ff --- /dev/null +++ b/backend/app/services/storage.py @@ -0,0 +1,23 @@ +import shutil +import uuid +from pathlib import Path +from typing import Tuple + +from fastapi import UploadFile + +from app.config import UPLOAD_DIR + + +def save_upload(file: UploadFile) -> Tuple[str, str]: + suffix = Path(file.filename or "").suffix.lower() or ".mp4" + file_id = f"{uuid.uuid4().hex}{suffix}" + destination = UPLOAD_DIR / file_id + + with destination.open("wb") as buffer: + shutil.copyfileobj(file.file, buffer) + + return file_id, file.filename or file_id + + +def get_upload_path(file_id: str) -> Path: + return UPLOAD_DIR / file_id diff --git a/backend/app/services/tasks.py b/backend/app/services/tasks.py new file mode 100644 index 0000000..8a0fbf4 --- /dev/null +++ b/backend/app/services/tasks.py @@ -0,0 +1,124 @@ +import threading +import uuid +from dataclasses import dataclass, field +from datetime import datetime +from pathlib import Path +from queue import Queue +from typing import Dict, List, Optional + +from app.schemas import TaskCreateRequest, TaskResult +from app.services.media import generate_cover, generate_hls, transcode_to_mp4 +from app.services.storage import get_upload_path + + +def now_iso() -> str: + return datetime.utcnow().replace(microsecond=0).isoformat() + "Z" + + +@dataclass +class TaskRecord: + task_id: str + file_id: str + options: TaskCreateRequest + status: str = "queued" + result: TaskResult = field(default_factory=TaskResult) + error: Optional[str] = None + created_at: str = field(default_factory=now_iso) + updated_at: str = field(default_factory=now_iso) + + def touch(self) -> None: + self.updated_at = now_iso() + + +class TaskManager: + def __init__(self) -> None: + self.tasks: Dict[str, TaskRecord] = {} + self.queue: Queue = Queue() + self.lock = threading.Lock() + self.worker = threading.Thread(target=self._worker_loop, daemon=True) + self.worker_started = False + + def start(self) -> None: + if not self.worker_started: + self.worker.start() + self.worker_started = True + + def create_task(self, payload: TaskCreateRequest) -> TaskRecord: + task_id = uuid.uuid4().hex[:12] + record = TaskRecord(task_id=task_id, file_id=payload.file_id, options=payload) + with self.lock: + self.tasks[task_id] = record + self.queue.put(task_id) + return record + + def get_task(self, task_id: str) -> Optional[TaskRecord]: + return self.tasks.get(task_id) + + def list_tasks(self) -> List[TaskRecord]: + return sorted(self.tasks.values(), key=lambda task: task.created_at, reverse=True) + + def _update_status(self, task: TaskRecord, status: str) -> None: + with self.lock: + task.status = status + task.touch() + + def _append_log(self, task: TaskRecord, message: str) -> None: + with self.lock: + task.result.log.append(message) + task.touch() + + def _fail_task(self, task: TaskRecord, error: str) -> None: + with self.lock: + task.status = "failed" + task.error = error + task.touch() + + def _worker_loop(self) -> None: + while True: + task_id = self.queue.get() + task = self.tasks.get(task_id) + if task is None: + self.queue.task_done() + continue + + try: + self._process_task(task) + except Exception as exc: # noqa: BLE001 + self._fail_task(task, str(exc)) + finally: + self.queue.task_done() + + def _process_task(self, task: TaskRecord) -> None: + source_path = get_upload_path(task.file_id) + if not source_path.exists(): + raise FileNotFoundError(f"uploaded file not found: {task.file_id}") + + self._update_status(task, "processing") + working_source: Path = source_path + + if task.options.transcode_mp4: + self._append_log(task, "Transcoding to MP4 with optional clipping and watermark") + mp4_path = transcode_to_mp4( + source=source_path, + task_id=task.task_id, + clip_seconds=task.options.clip_seconds, + watermark_text=task.options.watermark_text, + ) + task.result.mp4_url = f"/media/processed/{mp4_path.name}" + working_source = mp4_path + + if task.options.generate_cover: + self._append_log(task, "Generating cover image") + cover_path = generate_cover(working_source, task.task_id) + task.result.cover_url = f"/media/covers/{cover_path.name}" + + if task.options.generate_hls: + self._append_log(task, "Generating HLS playlist") + playlist = generate_hls(working_source, task.task_id) + task.result.hls_url = f"/media/hls/{task.task_id}/{playlist.name}" + + self._append_log(task, "Task completed") + self._update_status(task, "completed") + + +task_manager = TaskManager() diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..ec09873 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,3 @@ +fastapi==0.115.0 +uvicorn[standard]==0.30.6 +python-multipart==0.0.9 diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..292dd9e --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + VPlatform + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..f649e7f --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1677 @@ +{ + "name": "vplatform-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vplatform-frontend", + "version": "0.1.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.1", + "vite": "^5.4.8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "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.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@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.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@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.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "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/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.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "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.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "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/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "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/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz", + "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "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.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001787", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", + "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", + "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/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/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/electron-to-chromium": { + "version": "1.5.334", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz", + "integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "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/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/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/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "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/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/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "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/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/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/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "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/postcss": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "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/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "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.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "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/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/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/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "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" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..9ae8ad4 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,19 @@ +{ + "name": "vplatform-frontend", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.1", + "vite": "^5.4.8" + } +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..4115006 --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,258 @@ +import { useEffect, useState } from 'react' + +const API_BASE = 'http://localhost:8000' + +const defaultForm = { + clip_seconds: 8, + transcode_mp4: true, + generate_cover: true, + generate_hls: true, + watermark_text: 'VPlatform Demo', +} + +function formatTime(iso) { + return new Date(iso).toLocaleString('zh-CN', { hour12: false }) +} + +function App() { + const [fileInfo, setFileInfo] = useState(null) + const [form, setForm] = useState(defaultForm) + const [tasks, setTasks] = useState([]) + const [uploading, setUploading] = useState(false) + const [creating, setCreating] = useState(false) + const [error, setError] = useState('') + + async function fetchTasks() { + try { + const response = await fetch(`${API_BASE}/api/tasks`) + if (!response.ok) { + throw new Error('获取任务列表失败') + } + const data = await response.json() + setTasks(data) + } catch (err) { + setError(err.message) + } + } + + useEffect(() => { + fetchTasks() + const timer = setInterval(fetchTasks, 2500) + return () => clearInterval(timer) + }, []) + + async function handleUpload(event) { + const selected = event.target.files?.[0] + if (!selected) { + return + } + + setUploading(true) + setError('') + const formData = new FormData() + formData.append('file', selected) + + try { + const response = await fetch(`${API_BASE}/api/upload`, { + method: 'POST', + body: formData, + }) + if (!response.ok) { + throw new Error('上传失败') + } + const data = await response.json() + setFileInfo(data) + } catch (err) { + setError(err.message) + } finally { + setUploading(false) + } + } + + async function handleCreateTask() { + if (!fileInfo) { + setError('请先上传视频') + return + } + + setCreating(true) + setError('') + try { + const response = await fetch(`${API_BASE}/api/tasks`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + file_id: fileInfo.file_id, + ...form, + }), + }) + if (!response.ok) { + throw new Error('创建任务失败') + } + const task = await response.json() + setTasks((current) => [task, ...current]) + } catch (err) { + setError(err.message) + } finally { + setCreating(false) + } + } + + function updateField(key, value) { + setForm((current) => ({ ...current, [key]: value })) + } + + return ( +
+
+
+

Audio / Video Processing Demo

+

VPlatform

+

+ 一个 1 天内可交付的音视频处理服务最小闭环:上传、排队、异步转码、生成封面与 HLS,并在前端实时查看结果。 +

+
+
+ 后端 + FastAPI + In-Memory Worker + 处理链路 + FFmpeg +
+
+ +
+
+

1. 上传视频

+ + {fileInfo && ( +
+

已上传:{fileInfo.original_name}

+ + 查看原文件 + +
+ )} +
+ +
+

2. 创建任务

+
+ + +
+
+ + + +
+ + {error &&

{error}

} +
+ +
+
+

3. 任务看板

+ +
+ +
+ {tasks.length === 0 &&

暂无任务,先上传一个视频并提交处理。

} + {tasks.map((task) => ( +
+
+
+

Task #{task.task_id}

+

{task.status}

+
+

{formatTime(task.created_at)}

+
+ +

+ clip {task.options.clip_seconds}s / mp4 {String(task.options.transcode_mp4)} / cover{' '} + {String(task.options.generate_cover)} / hls {String(task.options.generate_hls)} +

+ + {task.error &&

{task.error}

} + +
+ {task.result.mp4_url && ( +
+

处理后视频

+
+ )} + {task.result.cover_url && ( +
+

封面图

+ cover +
+ )} + {task.result.hls_url && ( +
+

HLS 播放清单

+ + 打开 m3u8 + +
+ )} +
+ +
+ {task.result.log.map((item, index) => ( +

{item}

+ ))} +
+
+ ))} +
+
+
+
+ ) +} + +export default App diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..2e5947f --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './styles.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +) diff --git a/frontend/src/styles.css b/frontend/src/styles.css new file mode 100644 index 0000000..5c71537 --- /dev/null +++ b/frontend/src/styles.css @@ -0,0 +1,264 @@ +:root { + color: #f5f1e8; + background: + radial-gradient(circle at top left, rgba(237, 123, 72, 0.22), transparent 28%), + radial-gradient(circle at top right, rgba(90, 153, 212, 0.22), transparent 32%), + linear-gradient(135deg, #111827, #1f2937 40%, #0f172a 100%); + font-family: "IBM Plex Sans", "Segoe UI", sans-serif; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; +} + +a { + color: #ffd79f; +} + +button, +input { + font: inherit; +} + +.page-shell { + width: min(1180px, calc(100% - 32px)); + margin: 0 auto; + padding: 32px 0 48px; +} + +.hero { + display: grid; + grid-template-columns: minmax(0, 1.3fr) minmax(260px, 0.7fr); + gap: 20px; + align-items: stretch; + margin-bottom: 24px; +} + +.eyebrow { + margin: 0 0 12px; + font-size: 12px; + letter-spacing: 0.24em; + text-transform: uppercase; + color: #fca46b; +} + +.hero h1 { + margin: 0; + font-size: clamp(40px, 8vw, 72px); + line-height: 0.95; +} + +.hero-copy { + max-width: 680px; + color: #d8dee9; +} + +.hero-card, +.panel, +.task-card { + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(17, 24, 39, 0.72); + backdrop-filter: blur(14px); + border-radius: 20px; + box-shadow: 0 18px 60px rgba(0, 0, 0, 0.25); +} + +.hero-card { + display: grid; + gap: 8px; + padding: 24px; + align-content: center; +} + +.hero-card span { + color: #9fb3c8; + font-size: 14px; +} + +.hero-card strong { + font-size: 22px; +} + +.grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 20px; +} + +.panel { + padding: 24px; +} + +.full-width { + grid-column: 1 / -1; +} + +.upload-box { + display: grid; + place-items: center; + min-height: 180px; + border: 1px dashed rgba(255, 255, 255, 0.3); + border-radius: 18px; + cursor: pointer; + background: rgba(255, 255, 255, 0.03); +} + +.upload-box input { + display: none; +} + +.info-card, +.log-box, +.hls-box { + padding: 14px 16px; + border-radius: 14px; + background: rgba(255, 255, 255, 0.04); +} + +.form-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; +} + +.form-grid label, +.checkboxes label { + display: grid; + gap: 8px; + font-size: 14px; + color: #d8dee9; +} + +.form-grid input[type='number'], +.form-grid input[type='text'] { + border: 0; + border-radius: 12px; + padding: 12px 14px; + color: #f5f1e8; + background: rgba(255, 255, 255, 0.08); +} + +.checkboxes { + display: flex; + gap: 18px; + flex-wrap: wrap; + margin: 18px 0; +} + +.primary-btn, +.ghost-btn { + border: 0; + border-radius: 999px; + cursor: pointer; + transition: transform 160ms ease, opacity 160ms ease; +} + +.primary-btn { + padding: 12px 20px; + color: #111827; + background: linear-gradient(90deg, #ffd79f, #fca46b); + font-weight: 700; +} + +.ghost-btn { + padding: 10px 16px; + color: #f5f1e8; + background: rgba(255, 255, 255, 0.08); +} + +.primary-btn:hover, +.ghost-btn:hover { + transform: translateY(-1px); +} + +.panel-title, +.task-meta { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; +} + +.task-list { + display: grid; + gap: 16px; +} + +.task-card { + padding: 20px; +} + +.task-id { + margin: 0; + font-size: 18px; +} + +.status-pill { + display: inline-flex; + margin-top: 8px; + padding: 6px 10px; + border-radius: 999px; + font-size: 12px; + text-transform: uppercase; +} + +.status-pill.queued { + background: rgba(250, 204, 21, 0.16); + color: #fde68a; +} + +.status-pill.processing { + background: rgba(59, 130, 246, 0.18); + color: #bfdbfe; +} + +.status-pill.completed { + background: rgba(74, 222, 128, 0.18); + color: #bbf7d0; +} + +.status-pill.failed { + background: rgba(248, 113, 113, 0.18); + color: #fecaca; +} + +.result-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 16px; + margin: 16px 0; +} + +.result-grid video, +.result-grid img { + width: 100%; + border-radius: 14px; + background: #000; +} + +.log-box p, +.muted-text, +.error-text { + margin: 6px 0; +} + +.muted-text { + color: #9fb3c8; +} + +.error-text { + color: #fca5a5; +} + +@media (max-width: 900px) { + .hero, + .grid, + .form-grid { + grid-template-columns: 1fr; + } +} diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..abccf69 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + }, +}) diff --git a/prompt.md b/prompt.md new file mode 100644 index 0000000..09b5fb8 --- /dev/null +++ b/prompt.md @@ -0,0 +1,116 @@ +# Role +你是一名资深全栈工程师 + 后端多媒体工程师 + 技术项目负责人。 +你的目标不是泛泛而谈,而是直接交付一个“1天内可完成、可运行、可展示、可写进简历”的最小可用项目。 + +# Project Goal +请围绕“音视频处理服务最小闭环”设计并实现一个前后端项目,项目需要贴合以下求职方向: +- 通信服务后端开发实习生 +- 音视频技术 +- 视频编辑服务 +- AIGC处理流程 +- 调度系统 / 任务队列 / 监控告警 + +项目必须满足: +1. 一天内可快速开发完成 +2. 必须有前端页面展示 +3. 必须有后端 API +4. 必须体现异步任务处理思想 +5. 必须能运行一个真实的音视频处理流程 +6. 必须适合发布到 GitHub +7. 最终输出必须包含 README.md 和完整文件内容 + +# Core Requirement +请基于“最小可行但足够有技术亮点”的原则,完成以下工作: + +1. 先做需求功能解读 +2. 再做技术选型 +3. 再做系统架构设计 +4. 再做分阶段开发计划 +5. 再输出完整项目目录结构 +6. 再逐文件给出可运行代码 +7. 最后生成符合 GitHub 风格的 README.md + +# Business Scope +项目建议方向: +一个极简音视频处理平台,至少包含以下能力: +- 上传视频 +- 创建处理任务 +- 异步执行任务 +- 查询任务状态 +- 返回处理结果 +- 前端可查看结果 + +处理能力至少实现 2~3 个: +- 视频转码 MP4 +- 截取前 N 秒 +- 生成封面图 +- 生成 HLS +- 添加简单水印 + +# Preferred Tech Stack +默认优先选择“开发速度快、展示效果好、一天能做完”的技术栈。 +如果没有特别冲突,优先: +- Frontend: React + Vite +- Backend: Python FastAPI 或 Go Gin(二选一,并说明原因) +- Task model: 内存队列 + worker +- Media processing: FFmpeg +- Storage: 本地文件系统 +- API style: RESTful JSON + +如果你认为其他技术栈更合适,也可以替换,但必须说明为什么更适合“一天内交付”。 + +# Hard Constraints +你必须严格遵守以下要求: + +1. 不要做成“大而全”的复杂平台 +2. 只做最小可用版本,但必须完整闭环 +3. 不要只给概念,必须给真实代码 +4. 不要只给代码片段,必须给完整文件内容 +5. 不要只写伪代码 +6. 不要省略关键文件 +7. 代码要能直接复制到本地创建项目 +8. 优先保证“能跑通”而不是“功能很多” +9. 前端页面可以朴素,但流程必须完整 +10. 后端必须体现任务队列 / 异步处理思想 +11. README.md 必须像真实 GitHub 开源项目 +12. README 中必须包含: + - 项目介绍 + - 技术栈 + - 功能列表 + - 架构图(Mermaid) + - 快速启动 + - API 说明 + - 页面预览说明 + - 项目亮点 + - 后续优化方向 + +# Execution Order +请严格按以下顺序输出,不要跳步: + +## Step 1 - 功能解读 +把这个项目要解决的问题、为什么贴合 JD、为什么一天内能完成,讲清楚。 + +## Step 2 - 技术选型 +给出前端、后端、任务队列、音视频处理方案、存储方案,并说明取舍。 + +## Step 3 - 架构设计 +输出: +- 模块划分 +- 请求流转 +- 异步任务流转 +- 存储结构 +- Mermaid 架构图 + +## Step 4 - 分阶段计划 +给出按小时拆分的一天开发计划,例如: +- 第 1 阶段:搭后端 +- 第 2 阶段:接 FFmpeg +- 第 3 阶段:写前端 +- 第 4 阶段:联调 +- 第 5 阶段:README + +## Step 5 - 项目目录结构 +给出完整目录树。 + +## step 6 - 编写代码 +按照分阶段计划编写代码,每个阶段末尾停滞一次申请用户来检查代码。 \ No newline at end of file diff --git a/storage/covers/.gitkeep b/storage/covers/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/storage/covers/.gitkeep @@ -0,0 +1 @@ + diff --git a/storage/hls/.gitkeep b/storage/hls/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/storage/hls/.gitkeep @@ -0,0 +1 @@ + diff --git a/storage/processed/.gitkeep b/storage/processed/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/storage/processed/.gitkeep @@ -0,0 +1 @@ + diff --git a/storage/uploads/.gitkeep b/storage/uploads/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/storage/uploads/.gitkeep @@ -0,0 +1 @@ +