Spaces:
Sleeping
Sleeping
孙振宇 commited on
Commit ·
060fbda
0
Parent(s):
Initial HF Spaces deployment
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitignore +258 -0
- .python-version +1 -0
- .streamlit/config.toml +3 -0
- Dockerfile +80 -0
- LICENSE +201 -0
- README.md +339 -0
- app.py +387 -0
- benchmark/batch_bench_record.md +16 -0
- benchmark/benchmark_batch.py +67 -0
- cli.py +430 -0
- datasets/make_yolo_images.py +64 -0
- docker-compose.yaml +26 -0
- docker-entrypoint.sh +13 -0
- example.py +22 -0
- ffmpeg/README.md +69 -0
- frontend/README.md +38 -0
- frontend/bun.lock +409 -0
- frontend/index.html +13 -0
- frontend/jsconfig.json +8 -0
- frontend/package.json +25 -0
- frontend/public/favicon.ico +0 -0
- frontend/src/App.vue +640 -0
- frontend/src/assets/base.css +86 -0
- frontend/src/assets/logo.svg +1 -0
- frontend/src/assets/main.css +35 -0
- frontend/src/components/HelloWorld.vue +44 -0
- frontend/src/components/TheWelcome.vue +95 -0
- frontend/src/components/WelcomeItem.vue +87 -0
- frontend/src/components/icons/IconCommunity.vue +7 -0
- frontend/src/components/icons/IconDocumentation.vue +7 -0
- frontend/src/components/icons/IconEcosystem.vue +7 -0
- frontend/src/components/icons/IconSupport.vue +7 -0
- frontend/src/components/icons/IconTooling.vue +19 -0
- frontend/src/main.js +23 -0
- frontend/src/views/Upload.vue +13 -0
- frontend/vite.config.js +26 -0
- hf_spaces_README.md +27 -0
- mds/reward.md +1 -0
- model_version.json +1 -0
- notebooks/imputation.ipynb +0 -0
- one-click-portable.md +26 -0
- profile/profile_clean.sh +16 -0
- profile/profile_process_chunk.sh +16 -0
- profile/profile_process_chunk_async.sh +16 -0
- profile/profile_whole_infer.sh +16 -0
- profile/run_clean.py +128 -0
- profile/run_process_chunk.py +368 -0
- profile/run_process_chunk_async.py +513 -0
- profile/run_whole.py +273 -0
- pyproject.toml +64 -0
.gitignore
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Byte-compiled / optimized / DLL files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[codz]
|
| 4 |
+
*$py.class
|
| 5 |
+
|
| 6 |
+
# C extensions
|
| 7 |
+
*.so
|
| 8 |
+
|
| 9 |
+
# Distribution / packaging
|
| 10 |
+
.Python
|
| 11 |
+
build/
|
| 12 |
+
develop-eggs/
|
| 13 |
+
dist/
|
| 14 |
+
downloads/
|
| 15 |
+
eggs/
|
| 16 |
+
.eggs/
|
| 17 |
+
lib/
|
| 18 |
+
lib64/
|
| 19 |
+
parts/
|
| 20 |
+
sdist/
|
| 21 |
+
var/
|
| 22 |
+
wheels/
|
| 23 |
+
share/python-wheels/
|
| 24 |
+
*.egg-info/
|
| 25 |
+
.installed.cfg
|
| 26 |
+
*.egg
|
| 27 |
+
MANIFEST
|
| 28 |
+
|
| 29 |
+
# PyInstaller
|
| 30 |
+
# Usually these files are written by a python script from a template
|
| 31 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 32 |
+
*.manifest
|
| 33 |
+
*.spec
|
| 34 |
+
|
| 35 |
+
# Installer logs
|
| 36 |
+
pip-log.txt
|
| 37 |
+
pip-delete-this-directory.txt
|
| 38 |
+
|
| 39 |
+
# Unit test / coverage reports
|
| 40 |
+
htmlcov/
|
| 41 |
+
.tox/
|
| 42 |
+
.nox/
|
| 43 |
+
.coverage
|
| 44 |
+
.coverage.*
|
| 45 |
+
.cache
|
| 46 |
+
nosetests.xml
|
| 47 |
+
coverage.xml
|
| 48 |
+
*.cover
|
| 49 |
+
*.py.cover
|
| 50 |
+
.hypothesis/
|
| 51 |
+
.pytest_cache/
|
| 52 |
+
cover/
|
| 53 |
+
|
| 54 |
+
# Translations
|
| 55 |
+
*.mo
|
| 56 |
+
*.pot
|
| 57 |
+
|
| 58 |
+
# Django stuff:
|
| 59 |
+
*.log
|
| 60 |
+
local_settings.py
|
| 61 |
+
db.sqlite3
|
| 62 |
+
db.sqlite3-journal
|
| 63 |
+
|
| 64 |
+
# Flask stuff:
|
| 65 |
+
instance/
|
| 66 |
+
.webassets-cache
|
| 67 |
+
|
| 68 |
+
# Scrapy stuff:
|
| 69 |
+
.scrapy
|
| 70 |
+
|
| 71 |
+
# Sphinx documentation
|
| 72 |
+
docs/_build/
|
| 73 |
+
|
| 74 |
+
# PyBuilder
|
| 75 |
+
.pybuilder/
|
| 76 |
+
target/
|
| 77 |
+
|
| 78 |
+
# Jupyter Notebook
|
| 79 |
+
.ipynb_checkpoints
|
| 80 |
+
|
| 81 |
+
# IPython
|
| 82 |
+
profile_default/
|
| 83 |
+
ipython_config.py
|
| 84 |
+
|
| 85 |
+
# pyenv
|
| 86 |
+
# For a library or package, you might want to ignore these files since the code is
|
| 87 |
+
# intended to run in multiple environments; otherwise, check them in:
|
| 88 |
+
# .python-version
|
| 89 |
+
|
| 90 |
+
# pipenv
|
| 91 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 92 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 93 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
| 94 |
+
# install all needed dependencies.
|
| 95 |
+
#Pipfile.lock
|
| 96 |
+
|
| 97 |
+
# UV
|
| 98 |
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
| 99 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 100 |
+
# commonly ignored for libraries.
|
| 101 |
+
#uv.lock
|
| 102 |
+
|
| 103 |
+
# poetry
|
| 104 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
| 105 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 106 |
+
# commonly ignored for libraries.
|
| 107 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
| 108 |
+
#poetry.lock
|
| 109 |
+
#poetry.toml
|
| 110 |
+
|
| 111 |
+
# pdm
|
| 112 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
| 113 |
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
| 114 |
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
| 115 |
+
#pdm.lock
|
| 116 |
+
#pdm.toml
|
| 117 |
+
.pdm-python
|
| 118 |
+
.pdm-build/
|
| 119 |
+
|
| 120 |
+
# pixi
|
| 121 |
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
| 122 |
+
#pixi.lock
|
| 123 |
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
| 124 |
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
| 125 |
+
.pixi
|
| 126 |
+
|
| 127 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
| 128 |
+
__pypackages__/
|
| 129 |
+
|
| 130 |
+
# Celery stuff
|
| 131 |
+
celerybeat-schedule
|
| 132 |
+
celerybeat.pid
|
| 133 |
+
|
| 134 |
+
# SageMath parsed files
|
| 135 |
+
*.sage.py
|
| 136 |
+
|
| 137 |
+
# Environments
|
| 138 |
+
.env
|
| 139 |
+
.envrc
|
| 140 |
+
.venv
|
| 141 |
+
env/
|
| 142 |
+
venv/
|
| 143 |
+
ENV/
|
| 144 |
+
env.bak/
|
| 145 |
+
venv.bak/
|
| 146 |
+
|
| 147 |
+
# Spyder project settings
|
| 148 |
+
.spyderproject
|
| 149 |
+
.spyproject
|
| 150 |
+
|
| 151 |
+
# Rope project settings
|
| 152 |
+
.ropeproject
|
| 153 |
+
|
| 154 |
+
# mkdocs documentation
|
| 155 |
+
/site
|
| 156 |
+
|
| 157 |
+
# mypy
|
| 158 |
+
.mypy_cache/
|
| 159 |
+
.dmypy.json
|
| 160 |
+
dmypy.json
|
| 161 |
+
|
| 162 |
+
# Pyre type checker
|
| 163 |
+
.pyre/
|
| 164 |
+
|
| 165 |
+
# pytype static type analyzer
|
| 166 |
+
.pytype/
|
| 167 |
+
|
| 168 |
+
# Cython debug symbols
|
| 169 |
+
cython_debug/
|
| 170 |
+
|
| 171 |
+
# PyCharm
|
| 172 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
| 173 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
| 174 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
| 175 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
| 176 |
+
#.idea/
|
| 177 |
+
|
| 178 |
+
# Abstra
|
| 179 |
+
# Abstra is an AI-powered process automation framework.
|
| 180 |
+
# Ignore directories containing user credentials, local state, and settings.
|
| 181 |
+
# Learn more at https://abstra.io/docs
|
| 182 |
+
.abstra/
|
| 183 |
+
|
| 184 |
+
# Visual Studio Code
|
| 185 |
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
| 186 |
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
| 187 |
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
| 188 |
+
# you could uncomment the following to ignore the entire vscode folder
|
| 189 |
+
# .vscode/
|
| 190 |
+
|
| 191 |
+
# Ruff stuff:
|
| 192 |
+
.ruff_cache/
|
| 193 |
+
|
| 194 |
+
# PyPI configuration file
|
| 195 |
+
.pypirc
|
| 196 |
+
|
| 197 |
+
# Cursor
|
| 198 |
+
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
| 199 |
+
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
| 200 |
+
# refer to https://docs.cursor.com/context/ignore-files
|
| 201 |
+
.cursorignore
|
| 202 |
+
.cursorindexingignore
|
| 203 |
+
|
| 204 |
+
# Marimo
|
| 205 |
+
marimo/_static/
|
| 206 |
+
marimo/_lsp/
|
| 207 |
+
__marimo__/
|
| 208 |
+
output
|
| 209 |
+
|
| 210 |
+
videos
|
| 211 |
+
|
| 212 |
+
datasets/images
|
| 213 |
+
datasets/labels
|
| 214 |
+
datasets/coco8
|
| 215 |
+
.DS_store
|
| 216 |
+
outputs
|
| 217 |
+
yolo11n.pt
|
| 218 |
+
yolo11s.pt
|
| 219 |
+
best.pt
|
| 220 |
+
**/best.pt
|
| 221 |
+
|
| 222 |
+
.claude
|
| 223 |
+
|
| 224 |
+
runs
|
| 225 |
+
.idea
|
| 226 |
+
working_dir
|
| 227 |
+
data
|
| 228 |
+
upload_to_huggingface.py
|
| 229 |
+
resources/best.pt
|
| 230 |
+
resources/model_version.json
|
| 231 |
+
.web
|
| 232 |
+
examples
|
| 233 |
+
resources/checkpoint
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
frontend/node_modules
|
| 237 |
+
|
| 238 |
+
*.nsys-rep
|
| 239 |
+
*.qdstrm
|
| 240 |
+
# profile/profile_e2fgvi_hq.nsys-rep
|
| 241 |
+
*.npy
|
| 242 |
+
profiling/
|
| 243 |
+
|
| 244 |
+
profile/*.qdrep
|
| 245 |
+
profile/*.nsys-rep
|
| 246 |
+
profile/*.qdstrm
|
| 247 |
+
profile/*.sqlite
|
| 248 |
+
profile/*.trace
|
| 249 |
+
profile/*.trace.json
|
| 250 |
+
profile/*.trace.json.gz
|
| 251 |
+
profile/*.trace.json.gz.part
|
| 252 |
+
profile/*.trace.json.gz.part.1
|
| 253 |
+
profile/*.trace.json.gz.part.2
|
| 254 |
+
profile/*.trace.json.gz.part.3
|
| 255 |
+
profile/*.trace.json.gz.part.4
|
| 256 |
+
profile/*.trace.json.gz.part.5assests/*.mp4
|
| 257 |
+
resources/*.mp4
|
| 258 |
+
resources/*.png
|
.python-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
3.12
|
.streamlit/config.toml
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[server]
|
| 2 |
+
maxUploadSize=4096
|
| 3 |
+
# 4GB as maximum
|
Dockerfile
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
|
| 3 |
+
# System dependencies
|
| 4 |
+
RUN apt-get update && apt-get install -y \
|
| 5 |
+
ffmpeg \
|
| 6 |
+
git \
|
| 7 |
+
curl \
|
| 8 |
+
build-essential \
|
| 9 |
+
libgl1-mesa-glx \
|
| 10 |
+
libglib2.0-0 \
|
| 11 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 12 |
+
|
| 13 |
+
# Install uv
|
| 14 |
+
RUN pip install uv
|
| 15 |
+
|
| 16 |
+
WORKDIR /app
|
| 17 |
+
|
| 18 |
+
# Copy project files
|
| 19 |
+
COPY pyproject.toml .
|
| 20 |
+
COPY sorawm/ sorawm/
|
| 21 |
+
COPY app.py .
|
| 22 |
+
COPY start_server.py .
|
| 23 |
+
COPY .streamlit/ .streamlit/
|
| 24 |
+
COPY model_version.json .
|
| 25 |
+
|
| 26 |
+
# Create required directories
|
| 27 |
+
RUN mkdir -p resources/checkpoint output working_dir logs data frontend/dist/assets
|
| 28 |
+
|
| 29 |
+
# Install Python dependencies (skip mmcv-full which needs special build)
|
| 30 |
+
RUN uv pip install --system --no-cache \
|
| 31 |
+
aiofiles \
|
| 32 |
+
aiosqlite \
|
| 33 |
+
diffusers \
|
| 34 |
+
einops \
|
| 35 |
+
"fastapi==0.108.0" \
|
| 36 |
+
ffmpeg-python \
|
| 37 |
+
fire \
|
| 38 |
+
greenlet \
|
| 39 |
+
httpx \
|
| 40 |
+
huggingface-hub \
|
| 41 |
+
loguru \
|
| 42 |
+
omegaconf \
|
| 43 |
+
opencv-python-headless \
|
| 44 |
+
pandas \
|
| 45 |
+
pydantic \
|
| 46 |
+
python-multipart \
|
| 47 |
+
requests \
|
| 48 |
+
rich \
|
| 49 |
+
ruptures \
|
| 50 |
+
scikit-learn \
|
| 51 |
+
sqlalchemy \
|
| 52 |
+
streamlit \
|
| 53 |
+
"torch>=2.5.0" \
|
| 54 |
+
"torchvision>=0.20.0" \
|
| 55 |
+
tqdm \
|
| 56 |
+
transformers \
|
| 57 |
+
ultralytics \
|
| 58 |
+
uvicorn
|
| 59 |
+
|
| 60 |
+
# Download YOLO weights and E2FGVI checkpoint at build time
|
| 61 |
+
RUN python -c "\
|
| 62 |
+
import os, requests; \
|
| 63 |
+
os.makedirs('resources/checkpoint', exist_ok=True); \
|
| 64 |
+
print('Downloading YOLO weights...'); \
|
| 65 |
+
import json; \
|
| 66 |
+
mv = json.load(open('model_version.json')); \
|
| 67 |
+
url = mv.get('url', 'https://github.com/linkedlist771/SoraWatermarkCleaner/releases/download/V0.0.1/best.pt'); \
|
| 68 |
+
r = requests.get(url, stream=True); \
|
| 69 |
+
open('resources/best.pt', 'wb').write(r.content); \
|
| 70 |
+
print('YOLO weights downloaded.') \
|
| 71 |
+
" || echo "YOLO download skipped, will download at runtime"
|
| 72 |
+
|
| 73 |
+
# Expose ports: 8501 (Streamlit UI), 5344 (FastAPI)
|
| 74 |
+
EXPOSE 8501 5344
|
| 75 |
+
|
| 76 |
+
# Start both Streamlit and FastAPI
|
| 77 |
+
COPY docker-entrypoint.sh .
|
| 78 |
+
RUN chmod +x docker-entrypoint.sh
|
| 79 |
+
|
| 80 |
+
CMD ["./docker-entrypoint.sh"]
|
LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Apache License
|
| 2 |
+
Version 2.0, January 2004
|
| 3 |
+
http://www.apache.org/licenses/
|
| 4 |
+
|
| 5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 6 |
+
|
| 7 |
+
1. Definitions.
|
| 8 |
+
|
| 9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 11 |
+
|
| 12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 13 |
+
the copyright owner that is granting the License.
|
| 14 |
+
|
| 15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 16 |
+
other entities that control, are controlled by, or are under common
|
| 17 |
+
control with that entity. For the purposes of this definition,
|
| 18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 19 |
+
direction or management of such entity, whether by contract or
|
| 20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 22 |
+
|
| 23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 24 |
+
exercising permissions granted by this License.
|
| 25 |
+
|
| 26 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 27 |
+
including but not limited to software source code, documentation
|
| 28 |
+
source, and configuration files.
|
| 29 |
+
|
| 30 |
+
"Object" form shall mean any form resulting from mechanical
|
| 31 |
+
transformation or translation of a Source form, including but
|
| 32 |
+
not limited to compiled object code, generated documentation,
|
| 33 |
+
and conversions to other media types.
|
| 34 |
+
|
| 35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 36 |
+
Object form, made available under the License, as indicated by a
|
| 37 |
+
copyright notice that is included in or attached to the work
|
| 38 |
+
(an example is provided in the Appendix below).
|
| 39 |
+
|
| 40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 41 |
+
form, that is based on (or derived from) the Work and for which the
|
| 42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 44 |
+
of this License, Derivative Works shall not include works that remain
|
| 45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 46 |
+
the Work and Derivative Works thereof.
|
| 47 |
+
|
| 48 |
+
"Contribution" shall mean any work of authorship, including
|
| 49 |
+
the original version of the Work and any modifications or additions
|
| 50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 54 |
+
means any form of electronic, verbal, or written communication sent
|
| 55 |
+
to the Licensor or its representatives, including but not limited to
|
| 56 |
+
communication on electronic mailing lists, source code control systems,
|
| 57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 59 |
+
excluding communication that is conspicuously marked or otherwise
|
| 60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 61 |
+
|
| 62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 64 |
+
subsequently incorporated within the Work.
|
| 65 |
+
|
| 66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 71 |
+
Work and such Derivative Works in Source or Object form.
|
| 72 |
+
|
| 73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 76 |
+
(except as stated in this section) patent license to make, have made,
|
| 77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 78 |
+
where such license applies only to those patent claims licensable
|
| 79 |
+
by such Contributor that are necessarily infringed by their
|
| 80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 82 |
+
institute patent litigation against any entity (including a
|
| 83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 84 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 85 |
+
or contributory patent infringement, then any patent licenses
|
| 86 |
+
granted to You under this License for that Work shall terminate
|
| 87 |
+
as of the date such litigation is filed.
|
| 88 |
+
|
| 89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 90 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 91 |
+
modifications, and in Source or Object form, provided that You
|
| 92 |
+
meet the following conditions:
|
| 93 |
+
|
| 94 |
+
(a) You must give any other recipients of the Work or
|
| 95 |
+
Derivative Works a copy of this License; and
|
| 96 |
+
|
| 97 |
+
(b) You must cause any modified files to carry prominent notices
|
| 98 |
+
stating that You changed the files; and
|
| 99 |
+
|
| 100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 101 |
+
that You distribute, all copyright, patent, trademark, and
|
| 102 |
+
attribution notices from the Source form of the Work,
|
| 103 |
+
excluding those notices that do not pertain to any part of
|
| 104 |
+
the Derivative Works; and
|
| 105 |
+
|
| 106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 107 |
+
distribution, then any Derivative Works that You distribute must
|
| 108 |
+
include a readable copy of the attribution notices contained
|
| 109 |
+
within such NOTICE file, excluding those notices that do not
|
| 110 |
+
pertain to any part of the Derivative Works, in at least one
|
| 111 |
+
of the following places: within a NOTICE text file distributed
|
| 112 |
+
as part of the Derivative Works; within the Source form or
|
| 113 |
+
documentation, if provided along with the Derivative Works; or,
|
| 114 |
+
within a display generated by the Derivative Works, if and
|
| 115 |
+
wherever such third-party notices normally appear. The contents
|
| 116 |
+
of the NOTICE file are for informational purposes only and
|
| 117 |
+
do not modify the License. You may add Your own attribution
|
| 118 |
+
notices within Derivative Works that You distribute, alongside
|
| 119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 120 |
+
that such additional attribution notices cannot be construed
|
| 121 |
+
as modifying the License.
|
| 122 |
+
|
| 123 |
+
You may add Your own copyright statement to Your modifications and
|
| 124 |
+
may provide additional or different license terms and conditions
|
| 125 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 126 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 127 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 128 |
+
the conditions stated in this License.
|
| 129 |
+
|
| 130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 132 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 133 |
+
this License, without any additional terms or conditions.
|
| 134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 135 |
+
the terms of any separate license agreement you may have executed
|
| 136 |
+
with Licensor regarding such Contributions.
|
| 137 |
+
|
| 138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 140 |
+
except as required for reasonable and customary use in describing the
|
| 141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 142 |
+
|
| 143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 144 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 147 |
+
implied, including, without limitation, any warranties or conditions
|
| 148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 150 |
+
appropriateness of using or redistributing the Work and assume any
|
| 151 |
+
risks associated with Your exercise of permissions under this License.
|
| 152 |
+
|
| 153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 154 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 155 |
+
unless required by applicable law (such as deliberate and grossly
|
| 156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 157 |
+
liable to You for damages, including any direct, indirect, special,
|
| 158 |
+
incidental, or consequential damages of any character arising as a
|
| 159 |
+
result of this License or out of the use or inability to use the
|
| 160 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 161 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 162 |
+
other commercial damages or losses), even if such Contributor
|
| 163 |
+
has been advised of the possibility of such damages.
|
| 164 |
+
|
| 165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 168 |
+
or other liability obligations and/or rights consistent with this
|
| 169 |
+
License. However, in accepting such obligations, You may act only
|
| 170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 171 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 172 |
+
defend, and hold each Contributor harmless for any liability
|
| 173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 174 |
+
of your accepting any such warranty or additional liability.
|
| 175 |
+
|
| 176 |
+
END OF TERMS AND CONDITIONS
|
| 177 |
+
|
| 178 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 179 |
+
|
| 180 |
+
To apply the Apache License to your work, attach the following
|
| 181 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 182 |
+
replaced with your own identifying information. (Don't include
|
| 183 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 184 |
+
comment syntax for the file format. We also recommend that a
|
| 185 |
+
file or class name and description of purpose be included on the
|
| 186 |
+
same "printed page" as the copyright notice for easier
|
| 187 |
+
identification within third-party archives.
|
| 188 |
+
|
| 189 |
+
Copyright [yyyy] [name of copyright owner]
|
| 190 |
+
|
| 191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 192 |
+
you may not use this file except in compliance with the License.
|
| 193 |
+
You may obtain a copy of the License at
|
| 194 |
+
|
| 195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 196 |
+
|
| 197 |
+
Unless required by applicable law or agreed to in writing, software
|
| 198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 200 |
+
See the License for the specific language governing permissions and
|
| 201 |
+
limitations under the License.
|
README.md
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Sora Watermark Cleaner
|
| 3 |
+
emoji: 🎬
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
app_port: 5344
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# SoraWatermarkCleaner
|
| 12 |
+
|
| 13 |
+
## IMPORTANT
|
| 14 |
+
**This project is being archived.** OpenAI has discontinued the Sora video generation model, so this project will no longer be maintained. However, check out [DeMark-World](https://github.com/linkedlist771/DeMark-World) — it provides a universal method to remove watermarks from videos generated by other models such as Veo, Runway, and more.
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
> This project provides an elegant way to remove the sora watermark in the sora2 generated videos.
|
| 18 |
+
|
| 19 |
+
<table>
|
| 20 |
+
<tr>
|
| 21 |
+
<td width="20%">
|
| 22 |
+
<strong>Case1(25s)</strong>
|
| 23 |
+
</td>
|
| 24 |
+
<td width="80%">
|
| 25 |
+
<video src="https://github.com/user-attachments/assets/55f4e822-a356-4fab-a372-8910e4cb3c28"
|
| 26 |
+
width="100%" controls></video>
|
| 27 |
+
</td>
|
| 28 |
+
</tr>
|
| 29 |
+
<tr>
|
| 30 |
+
<td>
|
| 31 |
+
<strong>Case2(10s)</strong>
|
| 32 |
+
</td>
|
| 33 |
+
<td>
|
| 34 |
+
<video src="https://github.com/user-attachments/assets/2773df41-62dc-4876-bd2f-4dd3ccac4b9e"
|
| 35 |
+
width="100%" controls></video>
|
| 36 |
+
</td>
|
| 37 |
+
</tr>
|
| 38 |
+
<tr>
|
| 39 |
+
<td>
|
| 40 |
+
<strong>Case3(10s)</strong>
|
| 41 |
+
</td>
|
| 42 |
+
<td>
|
| 43 |
+
<video src="https://github.com/user-attachments/assets/2bdba310-6379-48f2-a93c-6de857c4df3d"
|
| 44 |
+
width="100%" controls></video>
|
| 45 |
+
</td>
|
| 46 |
+
</tr>
|
| 47 |
+
</table>
|
| 48 |
+
|
| 49 |
+
**Commercial Hosted Service & Sponsorship**
|
| 50 |
+
|
| 51 |
+
> If you prefer a one-click online service instead of running everything locally, you can use the hosted Sora watermark remover here:
|
| 52 |
+
>
|
| 53 |
+
> 👉 **https://www.sorawatermarkremover.ai/**
|
| 54 |
+
>
|
| 55 |
+
> SoraWatermarkRemover runs **SoraWatermarkCleaner** under the hood and provides GPU-backed processing, credits-based pricing and an easy web UI. This service financially supports the ongoing development and maintenance of **SoraWatermarkCleaner**.
|
| 56 |
+
|
| 57 |
+
⭐️:
|
| 58 |
+
|
| 59 |
+
- **I'm excited to release [DeMark-World](https://github.com/linkedlist771/DeMark-World) – to the best of my knowledge, the first model capable of removing any watermark from AI-generated videos.**
|
| 60 |
+
|
| 61 |
+
- **We have provided another model which could preserve time consistency without flicker!**
|
| 62 |
+
|
| 63 |
+
- **We support batch processing now.**
|
| 64 |
+
|
| 65 |
+
- **For the new watermark with username, the Yolo weights has been updated, try the new version watermark detect model, it should work better.**
|
| 66 |
+
|
| 67 |
+
- **We have uploaded the labelled datasets into huggingface, check this [dataset](https://huggingface.co/datasets/LLinked/sora-watermark-dataset) out. Free free to train your custom detector model or improve our model!**
|
| 68 |
+
|
| 69 |
+
- **One-click portable build is available** — [Download here](#3-one-click-portable-version) for Windows users! No installation required.
|
| 70 |
+
|
| 71 |
+
- **Docker Compose deployment is now supported** — [Get started](#6-docker-compose-deployment) with a single command. Note: the image requires CUDA and is large (~20 GB) due to NVIDIA libraries and PyTorch.
|
| 72 |
+
|
| 73 |
+
---
|
| 74 |
+
|
| 75 |
+
💝 If you find this project helpful, please consider [buying me a coffee](mds/reward.md) to support the development!
|
| 76 |
+
|
| 77 |
+
## 1. Method
|
| 78 |
+
|
| 79 |
+
The SoraWatermarkCleaner(we call it `SoraWm` later) is composed of two parsts:
|
| 80 |
+
|
| 81 |
+
- SoraWaterMarkDetector: We trained a yolov11s version to detect the sora watermark. (Thank you yolo!)
|
| 82 |
+
|
| 83 |
+
- WaterMarkCleaner: We refer iopaint's implementation for watermark removal using the lama model.
|
| 84 |
+
|
| 85 |
+
(This codebase is from https://github.com/Sanster/IOPaint#, thanks for their amazing work!)
|
| 86 |
+
|
| 87 |
+
Our SoraWm is purely deeplearning driven and yields good results in many generated videos.
|
| 88 |
+
|
| 89 |
+
## 2. Installation
|
| 90 |
+
|
| 91 |
+
[FFmpeg](https://ffmpeg.org/) is needed for video processing, please install it first. We highly recommend using the `uv` to install the environments:
|
| 92 |
+
|
| 93 |
+
1. installation:
|
| 94 |
+
|
| 95 |
+
```bash
|
| 96 |
+
uv sync
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
> now the envs will be installed at the `.venv`, you can activate the env using:
|
| 100 |
+
>
|
| 101 |
+
> ```bash
|
| 102 |
+
> source .venv/bin/activate
|
| 103 |
+
> ```
|
| 104 |
+
|
| 105 |
+
2. Downloaded the pretrained models:
|
| 106 |
+
|
| 107 |
+
The trained yolo weights will be stored in the `resources` dir as the `best.pt`. And it will be automatically download from https://github.com/linkedlist771/SoraWatermarkCleaner/releases/download/V0.0.1/best.pt . The `Lama` model is downloaded from https://github.com/Sanster/models/releases/download/add_big_lama/big-lama.pt, and will be stored in the torch cache dir. Both downloads are automatic, if you fail, please check your internet status.
|
| 108 |
+
|
| 109 |
+
3. Batch processing
|
| 110 |
+
Use the cli.py for batch processing
|
| 111 |
+
|
| 112 |
+
```
|
| 113 |
+
python cli.py [-h] -i INPUT -o OUTPUT [-p PATTERN] [-m MODEL] [--quiet]
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
examples:
|
| 117 |
+
|
| 118 |
+
```
|
| 119 |
+
# Process all .mp4 files in input folder
|
| 120 |
+
python cli.py -i /path/to/input -o /path/to/output
|
| 121 |
+
# Process all .mov files
|
| 122 |
+
python cli.py -i /path/to/input -o /path/to/output --pattern "*.mov"
|
| 123 |
+
# Process all video files (mp4, mov, avi)
|
| 124 |
+
python cli.py -i /path/to/input -o /path/to/output --pattern "*.{mp4,mov,avi}"
|
| 125 |
+
# Use e2fgvi_hq model for time-consistent results (slower, requires CUDA)
|
| 126 |
+
python cli.py -i /path/to/input -o /path/to/output --model e2fgvi_hq
|
| 127 |
+
# Without displaying the Tqdm bar inside sorawm procrssing.
|
| 128 |
+
python cli.py -i /path/to/input -o /path/to/output --quiet
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
## 3. One-Click Portable Version
|
| 132 |
+
|
| 133 |
+
For users who prefer a ready-to-use solution without manual installation, we provide a **one-click portable distribution** that includes all dependencies pre-configured.
|
| 134 |
+
|
| 135 |
+
### Download Links
|
| 136 |
+
|
| 137 |
+
**Google Drive:**
|
| 138 |
+
|
| 139 |
+
- [Download from Google Drive](https://drive.google.com/file/d/1ujH28aHaCXGgB146g6kyfz3Qxd-wHR1c/view?usp=share_link)
|
| 140 |
+
|
| 141 |
+
**Baidu Pan (百度网盘) - For users in China:**
|
| 142 |
+
|
| 143 |
+
- Link: https://pan.baidu.com/s/1onMom81mvw2c6PFkCuYzdg?pwd=jusu
|
| 144 |
+
- Extract Code (提取码): `jusu`
|
| 145 |
+
|
| 146 |
+
### Features
|
| 147 |
+
|
| 148 |
+
- ✅ No installation required
|
| 149 |
+
- ✅ All dependencies included
|
| 150 |
+
- ✅ Pre-configured environment
|
| 151 |
+
- ✅ Ready to use out of the box
|
| 152 |
+
|
| 153 |
+
Simply download, extract, and run!
|
| 154 |
+
|
| 155 |
+
## 4. Performance Optimization
|
| 156 |
+
|
| 157 |
+
We provide several options to speed up processing:
|
| 158 |
+
|
| 159 |
+
| Detector | Batch | Cleaner | TorchCompile | Bf16 | Time (s) | Speedup |
|
| 160 |
+
|:--------:|:-----:|:-------:|:------------:|:----:|:--------:|:-------:|
|
| 161 |
+
| YOLO | × | LAMA | × | × | 44.33 | - |
|
| 162 |
+
| YOLO | × | E2FGVI | × | × | 142.42 | 1.00× |
|
| 163 |
+
| YOLO | × | E2FGVI | ✓ | × | 117.19 | 1.22× |
|
| 164 |
+
| YOLO | 4 | E2FGVI | ✓ | × | 82.63 | 1.72× |
|
| 165 |
+
| YOLO | 4 | E2FGVI | ✓ | ✓ | 58.60 | 2.43× |
|
| 166 |
+
|
| 167 |
+
> Speedup is calculated relative to the E2FGVI baseline. LAMA uses a different cleaning approach and is not directly comparable.
|
| 168 |
+
|
| 169 |
+
- **YOLO Batch Detection**: Default batch size is 4 (`detect_batch_size=4`), enables batch inference for watermark detection, provides ~40% speedup
|
| 170 |
+
- **TorchCompile** (E2FGVI only): Enabled by default (`enable_torch_compile=True`), provides ~22% speedup
|
| 171 |
+
- **Bf16 Inference** (E2FGVI only): Enable with `use_bf16=True`(Default False), provides up to **2.43× speedup**. Note: quality may slightly decrease, and the first inference will be slow (~90s) due to compilation overhead; subsequent runs will be much faster (~58s) as artifacts are cached.
|
| 172 |
+
|
| 173 |
+
You can customize these settings when initializing `SoraWM`:
|
| 174 |
+
|
| 175 |
+
```python
|
| 176 |
+
from sorawm.core import SoraWM
|
| 177 |
+
from sorawm.schemas import CleanerType
|
| 178 |
+
|
| 179 |
+
# LAMA with batch detection (fast)
|
| 180 |
+
sora_wm = SoraWM(
|
| 181 |
+
cleaner_type=CleanerType.LAMA,
|
| 182 |
+
detect_batch_size=4 # default: 4
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
# E2FGVI_HQ with all optimizations (time-consistent)
|
| 186 |
+
sora_wm = SoraWM(
|
| 187 |
+
cleaner_type=CleanerType.E2FGVI_HQ,
|
| 188 |
+
enable_torch_compile=True, # default: True
|
| 189 |
+
detect_batch_size=8 # custom batch size
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
# E2FGVI_HQ with bf16 for maximum speed (may have slight quality loss)
|
| 193 |
+
sora_wm = SoraWM(
|
| 194 |
+
cleaner_type=CleanerType.E2FGVI_HQ,
|
| 195 |
+
enable_torch_compile=True,
|
| 196 |
+
detect_batch_size=4,
|
| 197 |
+
use_bf16=True # enables bfloat16 inference
|
| 198 |
+
)
|
| 199 |
+
```
|
| 200 |
+
|
| 201 |
+
## 5. Demo
|
| 202 |
+
|
| 203 |
+
To have a basic usage, just try the `example.py`:
|
| 204 |
+
|
| 205 |
+
> We provide two models to remove watermark. LAMA is fast but may have flicker on the cleaned area, which E2FGVI_HQ compromise this only requires cuda otherwise very slow on CPU or MPS.
|
| 206 |
+
|
| 207 |
+
```python
|
| 208 |
+
from pathlib import Path
|
| 209 |
+
|
| 210 |
+
from sorawm.core import SoraWM
|
| 211 |
+
from sorawm.schemas import CleanerType
|
| 212 |
+
|
| 213 |
+
if __name__ == "__main__":
|
| 214 |
+
input_video_path = Path("resources/dog_vs_sam.mp4")
|
| 215 |
+
output_video_path = Path("outputs/sora_watermark_removed")
|
| 216 |
+
|
| 217 |
+
# 1. LAMA is fast and good quality, but not time consistent.
|
| 218 |
+
sora_wm = SoraWM(cleaner_type=CleanerType.LAMA)
|
| 219 |
+
sora_wm.run(input_video_path, Path(f"{output_video_path}_lama.mp4"))
|
| 220 |
+
|
| 221 |
+
# 2. E2FGVI_HQ ensures time consistency, but will be very slow on no-cuda device.
|
| 222 |
+
sora_wm = SoraWM(cleaner_type=CleanerType.E2FGVI_HQ)
|
| 223 |
+
sora_wm.run(input_video_path, Path(f"{output_video_path}_e2fgvi_hq.mp4"))
|
| 224 |
+
```
|
| 225 |
+
|
| 226 |
+
We also provide you with a `streamlit` based interactive web page, try it with:
|
| 227 |
+
|
| 228 |
+
> We also provide the switch here.
|
| 229 |
+
|
| 230 |
+
```bash
|
| 231 |
+
streamlit run app.py
|
| 232 |
+
```
|
| 233 |
+
|
| 234 |
+
<img src="assests/model_switch.png" style="zoom: 25%;" />
|
| 235 |
+
|
| 236 |
+
Batch processing is also supported, now you can drag a folder or select multiple files to process.
|
| 237 |
+
<img src="assests/streamlit_batch.png" style="zoom: 50%;" />
|
| 238 |
+
|
| 239 |
+
## 6. Docker Compose Deployment
|
| 240 |
+
|
| 241 |
+
The easiest way to deploy SoraWatermarkCleaner is via Docker Compose.
|
| 242 |
+
|
| 243 |
+
> **Note:** The Docker image (`llinkedlist/sorawm:latest`) requires CUDA and includes NVIDIA libraries and PyTorch, making it quite large (~20 GB). The initial pull may take a significant amount of time depending on your network speed.
|
| 244 |
+
|
| 245 |
+
**Prerequisites:**
|
| 246 |
+
|
| 247 |
+
- [Docker](https://docs.docker.com/get-docker/) with [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html) installed
|
| 248 |
+
- A CUDA-capable GPU
|
| 249 |
+
|
| 250 |
+
**Start the service:**
|
| 251 |
+
|
| 252 |
+
```bash
|
| 253 |
+
docker compose up -d
|
| 254 |
+
```
|
| 255 |
+
|
| 256 |
+
This will:
|
| 257 |
+
|
| 258 |
+
- Pull the image from Docker Hub (first time only — be patient, ~20 GB)
|
| 259 |
+
- Mount the current directory to `/workspace` inside the container
|
| 260 |
+
- Cache model weights in `./.cache` to avoid re-downloading on restart
|
| 261 |
+
- Expose the Streamlit UI on port **8501**
|
| 262 |
+
|
| 263 |
+
Access the Streamlit UI at `http://localhost:8501`.
|
| 264 |
+
|
| 265 |
+
## 7. WebServer
|
| 266 |
+
|
| 267 |
+
Here, we provide a **FastAPI-based web server** that can quickly turn this watermark remover into a service.
|
| 268 |
+
|
| 269 |
+
We also have a frontUI for the webserver, to try this:
|
| 270 |
+
|
| 271 |
+
```bash
|
| 272 |
+
cd frontend && bun install && bun run build
|
| 273 |
+
```
|
| 274 |
+
|
| 275 |
+
And then start the server, the frontend UI will be just ready in root route:
|
| 276 |
+
|
| 277 |
+
> The task statuses are recoreded and can resume when server is down.
|
| 278 |
+
|
| 279 |
+

|
| 280 |
+
|
| 281 |
+
Simply run:
|
| 282 |
+
|
| 283 |
+
```
|
| 284 |
+
python start_server.py
|
| 285 |
+
```
|
| 286 |
+
|
| 287 |
+
The web server will start on port **5344**.
|
| 288 |
+
|
| 289 |
+
You can view the FastAPI [documentation](http://localhost:5344/docs) for more details.
|
| 290 |
+
|
| 291 |
+
There are three routes available:
|
| 292 |
+
|
| 293 |
+
1. **submit_remove_task**
|
| 294 |
+
|
| 295 |
+
> After uploading a video, a task ID will be returned, and the video will begin processing immediately.
|
| 296 |
+
|
| 297 |
+
<img src="resources/53abf3fd-11a9-4dd7-a348-34920775f8ad.png" alt="image" style="zoom: 25%;" />
|
| 298 |
+
|
| 299 |
+
2. **get_results**
|
| 300 |
+
|
| 301 |
+
You can use the task ID obtained above to check the task status.
|
| 302 |
+
|
| 303 |
+
It will display the percentage of video processing completed.
|
| 304 |
+
|
| 305 |
+
Once finished, the returned data will include a **download URL**.
|
| 306 |
+
|
| 307 |
+
3. **download**
|
| 308 |
+
|
| 309 |
+
You can use the **download URL** from step 2 to retrieve the cleaned video.
|
| 310 |
+
|
| 311 |
+
## 8. Datasets
|
| 312 |
+
|
| 313 |
+
We have uploaded the labelled datasets into huggingface, check this out https://huggingface.co/datasets/LLinked/sora-watermark-dataset. Free free to train your custom detector model or improve our model!
|
| 314 |
+
|
| 315 |
+
## 9. API
|
| 316 |
+
|
| 317 |
+
Packaged as a Cog and [published to Replicate](https://replicate.com/uglyrobot/sora2-watermark-remover) for simple API based usage.
|
| 318 |
+
|
| 319 |
+
## 10. License
|
| 320 |
+
|
| 321 |
+
Apache License
|
| 322 |
+
|
| 323 |
+
## 11. Citation
|
| 324 |
+
|
| 325 |
+
If you use this project, please cite:
|
| 326 |
+
|
| 327 |
+
```bibtex
|
| 328 |
+
@misc{sorawatermarkcleaner2025,
|
| 329 |
+
author = {linkedlist771},
|
| 330 |
+
title = {SoraWatermarkCleaner},
|
| 331 |
+
year = {2025},
|
| 332 |
+
url = {https://github.com/linkedlist771/SoraWatermarkCleaner}
|
| 333 |
+
}
|
| 334 |
+
```
|
| 335 |
+
|
| 336 |
+
## 12. Acknowledgments
|
| 337 |
+
|
| 338 |
+
- [IOPaint](https://github.com/Sanster/IOPaint) for the LAMA implementation
|
| 339 |
+
- [Ultralytics YOLO](https://github.com/ultralytics/ultralytics) for object detection
|
app.py
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import shutil
|
| 2 |
+
import tempfile
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
import streamlit as st
|
| 6 |
+
|
| 7 |
+
from sorawm.core import SoraWM
|
| 8 |
+
from sorawm.schemas import CleanerType
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def main():
|
| 12 |
+
st.set_page_config(
|
| 13 |
+
page_title="Sora Watermark Cleaner", page_icon="🎬", layout="centered"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
# Header section with improved layout
|
| 17 |
+
st.markdown(
|
| 18 |
+
"""
|
| 19 |
+
<div style='text-align: center; padding: 1rem 0;'>
|
| 20 |
+
<h1 style='margin-bottom: 0.5rem;'>
|
| 21 |
+
🎬 Sora Watermark Cleaner
|
| 22 |
+
</h1>
|
| 23 |
+
<p style='font-size: 1.2rem; color: #666; margin-bottom: 1rem;'>
|
| 24 |
+
Remove watermarks from Sora-generated videos with AI-powered precision
|
| 25 |
+
</p>
|
| 26 |
+
</div>
|
| 27 |
+
""",
|
| 28 |
+
unsafe_allow_html=True,
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
# # Feature badges
|
| 32 |
+
# col1, col2, col3 = st.columns(3)
|
| 33 |
+
# with col1:
|
| 34 |
+
# st.markdown(
|
| 35 |
+
# """
|
| 36 |
+
# <div style='text-align: center; padding: 0.8rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 37 |
+
# border-radius: 10px; color: white;'>
|
| 38 |
+
# <div style='font-size: 1.5rem;'>⚡</div>
|
| 39 |
+
# <div style='font-weight: bold;'>Fast Processing</div>
|
| 40 |
+
# <div style='font-size: 0.85rem; opacity: 0.9;'>GPU Accelerated</div>
|
| 41 |
+
# </div>
|
| 42 |
+
# """,
|
| 43 |
+
# unsafe_allow_html=True,
|
| 44 |
+
# )
|
| 45 |
+
# with col2:
|
| 46 |
+
# st.markdown(
|
| 47 |
+
# """
|
| 48 |
+
# <div style='text-align: center; padding: 0.8rem; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
| 49 |
+
# border-radius: 10px; color: white;'>
|
| 50 |
+
# <div style='font-size: 1.5rem;'>🎯</div>
|
| 51 |
+
# <div style='font-weight: bold;'>High Precision</div>
|
| 52 |
+
# <div style='font-size: 0.85rem; opacity: 0.9;'>AI-Powered</div>
|
| 53 |
+
# </div>
|
| 54 |
+
# """,
|
| 55 |
+
# unsafe_allow_html=True,
|
| 56 |
+
# )
|
| 57 |
+
# with col3:
|
| 58 |
+
# st.markdown(
|
| 59 |
+
# """
|
| 60 |
+
# <div style='text-align: center; padding: 0.8rem; background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
| 61 |
+
# border-radius: 10px; color: white;'>
|
| 62 |
+
# <div style='font-size: 1.5rem;'>📦</div>
|
| 63 |
+
# <div style='font-weight: bold;'>Batch Support</div>
|
| 64 |
+
# <div style='font-size: 0.85rem; opacity: 0.9;'>Process Multiple</div>
|
| 65 |
+
# </div>
|
| 66 |
+
# """,
|
| 67 |
+
# unsafe_allow_html=True,
|
| 68 |
+
# )
|
| 69 |
+
|
| 70 |
+
# Footer info
|
| 71 |
+
st.markdown(
|
| 72 |
+
"""
|
| 73 |
+
<div style='text-align: center; padding: 1rem 0; margin-top: 1rem;'>
|
| 74 |
+
<p style='color: #888; font-size: 0.9rem;'>
|
| 75 |
+
Built with ❤️ using Streamlit and AI |
|
| 76 |
+
<a href='https://github.com/linkedlist771/SoraWatermarkCleaner'
|
| 77 |
+
target='_blank' style='color: #667eea; text-decoration: none;'>
|
| 78 |
+
⭐ Star on GitHub
|
| 79 |
+
</a>
|
| 80 |
+
</p>
|
| 81 |
+
</div>
|
| 82 |
+
""",
|
| 83 |
+
unsafe_allow_html=True,
|
| 84 |
+
)
|
| 85 |
+
st.markdown("---")
|
| 86 |
+
|
| 87 |
+
# Model selection
|
| 88 |
+
st.markdown("### ⚙️ Model Settings")
|
| 89 |
+
|
| 90 |
+
col1, col2 = st.columns([2, 3])
|
| 91 |
+
with col1:
|
| 92 |
+
model_type = st.selectbox(
|
| 93 |
+
"Select Cleaner Model:",
|
| 94 |
+
options=[CleanerType.LAMA, CleanerType.E2FGVI_HQ],
|
| 95 |
+
format_func=lambda x: {
|
| 96 |
+
CleanerType.LAMA: "🚀 LAMA (Fast, Good Quality)",
|
| 97 |
+
CleanerType.E2FGVI_HQ: "💎 E2FGVI-HQ (Slower when not on GPU, Best Quality with time consistency)",
|
| 98 |
+
}[x],
|
| 99 |
+
help="LAMA: Fast processing with good quality. E2FGVI-HQ: Slower when not on GPU but highest quality results.",
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
with col2:
|
| 103 |
+
model_info = {
|
| 104 |
+
CleanerType.LAMA: "⚡ **Fast processing** - Recommended for most videos. Uses LaMa (Large Mask Inpainting) for quick watermark removal.",
|
| 105 |
+
CleanerType.E2FGVI_HQ: "🎯 **Highest quality** - Uses temporal flow-based video inpainting. Best for professional results. Slower when not on GPU. Time consistency is guaranteed.",
|
| 106 |
+
}
|
| 107 |
+
st.info(model_info[model_type])
|
| 108 |
+
|
| 109 |
+
# Initialize or reinitialize SoraWM if model changed
|
| 110 |
+
if (
|
| 111 |
+
"sora_wm" not in st.session_state
|
| 112 |
+
or st.session_state.get("current_model") != model_type
|
| 113 |
+
):
|
| 114 |
+
with st.spinner(f"Loading {model_type.value.upper()} model..."):
|
| 115 |
+
st.session_state.sora_wm = SoraWM(cleaner_type=model_type)
|
| 116 |
+
st.session_state.current_model = model_type
|
| 117 |
+
st.success(f"✅ {model_type.value.upper()} model loaded!")
|
| 118 |
+
|
| 119 |
+
st.markdown("---")
|
| 120 |
+
|
| 121 |
+
# Mode selection
|
| 122 |
+
mode = st.radio(
|
| 123 |
+
"Select input mode:",
|
| 124 |
+
["📁 Upload Video File", "🗂️ Process Folder"],
|
| 125 |
+
horizontal=True,
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
if mode == "📁 Upload Video File":
|
| 129 |
+
# File uploader
|
| 130 |
+
uploaded_file = st.file_uploader(
|
| 131 |
+
"Upload your video",
|
| 132 |
+
type=["mp4", "avi", "mov", "mkv"],
|
| 133 |
+
accept_multiple_files=False,
|
| 134 |
+
help="Select a video file to remove watermark",
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
if uploaded_file:
|
| 138 |
+
# Clear previous processed video if a new file is uploaded
|
| 139 |
+
if (
|
| 140 |
+
"current_file_name" not in st.session_state
|
| 141 |
+
or st.session_state.current_file_name != uploaded_file.name
|
| 142 |
+
):
|
| 143 |
+
st.session_state.current_file_name = uploaded_file.name
|
| 144 |
+
if "processed_video_data" in st.session_state:
|
| 145 |
+
del st.session_state.processed_video_data
|
| 146 |
+
if "processed_video_path" in st.session_state:
|
| 147 |
+
del st.session_state.processed_video_path
|
| 148 |
+
if "processed_video_name" in st.session_state:
|
| 149 |
+
del st.session_state.processed_video_name
|
| 150 |
+
|
| 151 |
+
# Display video info
|
| 152 |
+
st.success(f"✅ Uploaded: {uploaded_file.name}")
|
| 153 |
+
|
| 154 |
+
# Create two columns for before/after comparison
|
| 155 |
+
col_left, col_right = st.columns(2)
|
| 156 |
+
|
| 157 |
+
with col_left:
|
| 158 |
+
st.markdown("### 📥 Original Video")
|
| 159 |
+
st.video(uploaded_file)
|
| 160 |
+
|
| 161 |
+
with col_right:
|
| 162 |
+
st.markdown("### 🎬 Processed Video")
|
| 163 |
+
# Placeholder for processed video
|
| 164 |
+
if "processed_video_data" not in st.session_state:
|
| 165 |
+
st.info("Click 'Remove Watermark' to process the video")
|
| 166 |
+
else:
|
| 167 |
+
st.video(st.session_state.processed_video_data)
|
| 168 |
+
|
| 169 |
+
# Process button
|
| 170 |
+
if st.button(
|
| 171 |
+
"🚀 Remove Watermark", type="primary", use_container_width=True
|
| 172 |
+
):
|
| 173 |
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
| 174 |
+
tmp_path = Path(tmp_dir)
|
| 175 |
+
|
| 176 |
+
try:
|
| 177 |
+
# Create progress bar and status text
|
| 178 |
+
progress_bar = st.progress(0)
|
| 179 |
+
status_text = st.empty()
|
| 180 |
+
|
| 181 |
+
def update_progress(progress: int):
|
| 182 |
+
progress_bar.progress(progress / 100)
|
| 183 |
+
if progress < 50:
|
| 184 |
+
status_text.text(
|
| 185 |
+
f"🔍 Detecting watermarks... {progress}%"
|
| 186 |
+
)
|
| 187 |
+
elif progress < 95:
|
| 188 |
+
status_text.text(
|
| 189 |
+
f"🧹 Removing watermarks... {progress}%"
|
| 190 |
+
)
|
| 191 |
+
else:
|
| 192 |
+
status_text.text(f"🎵 Merging audio... {progress}%")
|
| 193 |
+
|
| 194 |
+
# Single file processing
|
| 195 |
+
input_path = tmp_path / uploaded_file.name
|
| 196 |
+
with open(input_path, "wb") as f:
|
| 197 |
+
f.write(uploaded_file.read())
|
| 198 |
+
|
| 199 |
+
output_path = tmp_path / f"cleaned_{uploaded_file.name}"
|
| 200 |
+
|
| 201 |
+
st.session_state.sora_wm.run(
|
| 202 |
+
input_path, output_path, progress_callback=update_progress
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
progress_bar.progress(100)
|
| 206 |
+
status_text.text("✅ Processing complete!")
|
| 207 |
+
st.success("✅ Watermark removed successfully!")
|
| 208 |
+
|
| 209 |
+
# Store processed video path and read video data
|
| 210 |
+
with open(output_path, "rb") as f:
|
| 211 |
+
video_data = f.read()
|
| 212 |
+
|
| 213 |
+
st.session_state.processed_video_path = output_path
|
| 214 |
+
st.session_state.processed_video_data = video_data
|
| 215 |
+
st.session_state.processed_video_name = (
|
| 216 |
+
f"cleaned_{uploaded_file.name}"
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
# Rerun to show the video in the right column
|
| 220 |
+
st.rerun()
|
| 221 |
+
|
| 222 |
+
except Exception as e:
|
| 223 |
+
st.error(f"❌ Error processing video: {str(e)}")
|
| 224 |
+
|
| 225 |
+
# Download button (show only if video is processed)
|
| 226 |
+
if "processed_video_data" in st.session_state:
|
| 227 |
+
st.download_button(
|
| 228 |
+
label="⬇️ Download Cleaned Video",
|
| 229 |
+
data=st.session_state.processed_video_data,
|
| 230 |
+
file_name=st.session_state.processed_video_name,
|
| 231 |
+
mime="video/mp4",
|
| 232 |
+
use_container_width=True,
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
else: # Folder mode
|
| 236 |
+
st.info(
|
| 237 |
+
"💡 Drag and drop your video folder here, or click to browse and select multiple video files"
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
# File uploader for multiple files (supports folder drag & drop)
|
| 241 |
+
uploaded_files = st.file_uploader(
|
| 242 |
+
"Upload videos from folder",
|
| 243 |
+
type=["mp4", "avi", "mov", "mkv"],
|
| 244 |
+
accept_multiple_files=True,
|
| 245 |
+
help="You can drag & drop an entire folder here, or select multiple video files",
|
| 246 |
+
key="folder_uploader",
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
if uploaded_files:
|
| 250 |
+
# Display uploaded files info
|
| 251 |
+
video_count = len(uploaded_files)
|
| 252 |
+
st.success(f"✅ {video_count} video file(s) uploaded")
|
| 253 |
+
|
| 254 |
+
# Show file list in an expander
|
| 255 |
+
with st.expander("📋 View uploaded files", expanded=False):
|
| 256 |
+
for i, file in enumerate(uploaded_files, 1):
|
| 257 |
+
file_size_mb = file.size / (1024 * 1024)
|
| 258 |
+
st.text(f"{i}. {file.name} ({file_size_mb:.2f} MB)")
|
| 259 |
+
|
| 260 |
+
# Process button
|
| 261 |
+
if st.button(
|
| 262 |
+
"🚀 Process All Videos", type="primary", use_container_width=True
|
| 263 |
+
):
|
| 264 |
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
| 265 |
+
tmp_path = Path(tmp_dir)
|
| 266 |
+
input_folder = tmp_path / "input"
|
| 267 |
+
output_folder = tmp_path / "output"
|
| 268 |
+
input_folder.mkdir(exist_ok=True)
|
| 269 |
+
output_folder.mkdir(exist_ok=True)
|
| 270 |
+
|
| 271 |
+
try:
|
| 272 |
+
# Save all uploaded files to temp folder
|
| 273 |
+
status_text = st.empty()
|
| 274 |
+
status_text.text("📥 Saving uploaded files...")
|
| 275 |
+
|
| 276 |
+
for uploaded_file in uploaded_files:
|
| 277 |
+
# Preserve folder structure if file.name contains subdirectories
|
| 278 |
+
file_path = input_folder / uploaded_file.name
|
| 279 |
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
| 280 |
+
with open(file_path, "wb") as f:
|
| 281 |
+
f.write(uploaded_file.read())
|
| 282 |
+
|
| 283 |
+
# Create progress tracking
|
| 284 |
+
progress_bar = st.progress(0)
|
| 285 |
+
current_file_text = st.empty()
|
| 286 |
+
processed_count = 0
|
| 287 |
+
|
| 288 |
+
def update_progress(progress: int):
|
| 289 |
+
# Calculate overall progress
|
| 290 |
+
overall_progress = (
|
| 291 |
+
(processed_count * 100 + progress) / video_count / 100
|
| 292 |
+
)
|
| 293 |
+
progress_bar.progress(overall_progress)
|
| 294 |
+
|
| 295 |
+
if progress < 50:
|
| 296 |
+
current_file_text.text(
|
| 297 |
+
f"🔍 Processing file {processed_count + 1}/{video_count}: Detecting watermarks... {progress}%"
|
| 298 |
+
)
|
| 299 |
+
elif progress < 95:
|
| 300 |
+
current_file_text.text(
|
| 301 |
+
f"🧹 Processing file {processed_count + 1}/{video_count}: Removing watermarks... {progress}%"
|
| 302 |
+
)
|
| 303 |
+
else:
|
| 304 |
+
current_file_text.text(
|
| 305 |
+
f"🎵 Processing file {processed_count + 1}/{video_count}: Merging audio... {progress}%"
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
# Process each video file
|
| 309 |
+
for video_file in input_folder.rglob("*"):
|
| 310 |
+
if video_file.is_file() and video_file.suffix.lower() in [
|
| 311 |
+
".mp4",
|
| 312 |
+
".avi",
|
| 313 |
+
".mov",
|
| 314 |
+
".mkv",
|
| 315 |
+
]:
|
| 316 |
+
# Determine output path maintaining folder structure
|
| 317 |
+
rel_path = video_file.relative_to(input_folder)
|
| 318 |
+
output_path = (
|
| 319 |
+
output_folder
|
| 320 |
+
/ rel_path.parent
|
| 321 |
+
/ f"cleaned_{rel_path.name}"
|
| 322 |
+
)
|
| 323 |
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 324 |
+
|
| 325 |
+
# Process the video
|
| 326 |
+
st.session_state.sora_wm.run(
|
| 327 |
+
video_file,
|
| 328 |
+
output_path,
|
| 329 |
+
progress_callback=update_progress,
|
| 330 |
+
)
|
| 331 |
+
processed_count += 1
|
| 332 |
+
|
| 333 |
+
progress_bar.progress(100)
|
| 334 |
+
current_file_text.text("✅ All videos processed!")
|
| 335 |
+
st.success(f"✅ {video_count} video(s) processed successfully!")
|
| 336 |
+
|
| 337 |
+
# Create download option for processed videos
|
| 338 |
+
st.markdown("### 📦 Download Processed Videos")
|
| 339 |
+
|
| 340 |
+
# Store processed files info in session state
|
| 341 |
+
if "batch_processed_files" not in st.session_state:
|
| 342 |
+
st.session_state.batch_processed_files = []
|
| 343 |
+
|
| 344 |
+
st.session_state.batch_processed_files.clear()
|
| 345 |
+
|
| 346 |
+
for processed_file in output_folder.rglob("*"):
|
| 347 |
+
if processed_file.is_file():
|
| 348 |
+
with open(processed_file, "rb") as f:
|
| 349 |
+
video_data = f.read()
|
| 350 |
+
rel_path = processed_file.relative_to(output_folder)
|
| 351 |
+
st.session_state.batch_processed_files.append(
|
| 352 |
+
{"name": str(rel_path), "data": video_data}
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
st.rerun()
|
| 356 |
+
|
| 357 |
+
except Exception as e:
|
| 358 |
+
st.error(f"❌ Error processing videos: {str(e)}")
|
| 359 |
+
import traceback
|
| 360 |
+
|
| 361 |
+
st.error(f"Details: {traceback.format_exc()}")
|
| 362 |
+
|
| 363 |
+
# Show download buttons for processed files
|
| 364 |
+
if (
|
| 365 |
+
"batch_processed_files" in st.session_state
|
| 366 |
+
and st.session_state.batch_processed_files
|
| 367 |
+
):
|
| 368 |
+
st.markdown("---")
|
| 369 |
+
st.markdown("### ⬇️ Download Processed Videos")
|
| 370 |
+
|
| 371 |
+
for file_info in st.session_state.batch_processed_files:
|
| 372 |
+
col1, col2 = st.columns([3, 1])
|
| 373 |
+
with col1:
|
| 374 |
+
st.text(f"📹 {file_info['name']}")
|
| 375 |
+
with col2:
|
| 376 |
+
st.download_button(
|
| 377 |
+
label="⬇️ Download",
|
| 378 |
+
data=file_info["data"],
|
| 379 |
+
file_name=file_info["name"],
|
| 380 |
+
mime="video/mp4",
|
| 381 |
+
key=f"download_{file_info['name']}",
|
| 382 |
+
use_container_width=True,
|
| 383 |
+
)
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
if __name__ == "__main__":
|
| 387 |
+
main()
|
benchmark/batch_bench_record.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Batch Processing Benchmark Results
|
| 2 |
+
|
| 3 |
+
## Ablation Study
|
| 4 |
+
|
| 5 |
+
| Detector | Batch | Cleaner | TorchCompile | Bf16 | Time (s) | Speedup |
|
| 6 |
+
|:--------:|:-----:|:-------:|:------------:|:----:|:--------:|:-------:|
|
| 7 |
+
| YOLO | × | LAMA | × | × | 44.33 | - |
|
| 8 |
+
| YOLO | × | E2FGVI | × | × | 142.42 | 1.00× |
|
| 9 |
+
| YOLO | × | E2FGVI | ✓ | × | 117.19 | 1.22× |
|
| 10 |
+
| YOLO | 4 | E2FGVI | ✓ | × | 82.63 | 1.72× |
|
| 11 |
+
| YOLO | 4 | E2FGVI | ✓ | ✓ | 58.60 | 2.43× |
|
| 12 |
+
|
| 13 |
+
> **Note**:
|
| 14 |
+
> - Speedup is calculated relative to the E2FGVI baseline (142.42s).
|
| 15 |
+
> - LAMA is a different cleaner and not directly comparable.
|
| 16 |
+
> - When enabling both bf16 and torch.compile, the first inference may be very slow (~90s) due to compilation overhead. Subsequent inferences will be significantly faster (~58s) as the compiled artifacts are cached.
|
benchmark/benchmark_batch.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from contextlib import contextmanager
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from time import perf_counter
|
| 4 |
+
|
| 5 |
+
from sorawm.core import SoraWM
|
| 6 |
+
from sorawm.schemas import CleanerType
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@contextmanager
|
| 10 |
+
def timer(name: str):
|
| 11 |
+
start = perf_counter()
|
| 12 |
+
yield
|
| 13 |
+
elapsed = perf_counter() - start
|
| 14 |
+
print(f"[{name}] Time elapsed: {elapsed:.2f}s")
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
if __name__ == "__main__":
|
| 18 |
+
input_video_path = Path("resources/dog_vs_sam.mp4")
|
| 19 |
+
output_video_path = Path("outputs/sora_watermark_removed")
|
| 20 |
+
|
| 21 |
+
# # 1. LAMA is fast and good quality, but not time consistent.
|
| 22 |
+
# sora_wm = SoraWM(cleaner_type=CleanerType.LAMA)
|
| 23 |
+
# with timer("LAMA"):
|
| 24 |
+
# sora_wm.run(input_video_path, Path(f"{output_video_path}_lama.mp4"))
|
| 25 |
+
|
| 26 |
+
# # 2. E2FGVI_HQ ensures time consistency, but will be very slow on no-cuda device.
|
| 27 |
+
# sora_wm = SoraWM(cleaner_type=CleanerType.E2FGVI_HQ, enable_torch_compile=False)
|
| 28 |
+
# with timer("E2FGVI_HQ"):
|
| 29 |
+
# sora_wm.run(input_video_path, Path(f"{output_video_path}_e2fgvi_hq.mp4"))
|
| 30 |
+
|
| 31 |
+
# 3. E2FGVI_HQ with torch compile is fast and good quality, but not time consistent.
|
| 32 |
+
sora_wm = SoraWM(cleaner_type=CleanerType.E2FGVI_HQ, enable_torch_compile=True)
|
| 33 |
+
with timer("E2FGVI_HQ + torch.compile"):
|
| 34 |
+
sora_wm.run(
|
| 35 |
+
input_video_path, Path(f"{output_video_path}_e2fgvi_hq_torch_compile.mp4")
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
# 4. Enable batch detection
|
| 39 |
+
batch_size = 4
|
| 40 |
+
sora_wm = SoraWM(
|
| 41 |
+
cleaner_type=CleanerType.E2FGVI_HQ,
|
| 42 |
+
enable_torch_compile=True,
|
| 43 |
+
detect_batch_size=4,
|
| 44 |
+
)
|
| 45 |
+
with timer("E2FGVI_HQ + torch.compile + batch"):
|
| 46 |
+
sora_wm.run(
|
| 47 |
+
input_video_path,
|
| 48 |
+
Path(f"{output_video_path}_e2fgvi_hq_torch_compile_batch.mp4"),
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
# 5. Enable bf16 inference
|
| 52 |
+
sora_wm = SoraWM(
|
| 53 |
+
cleaner_type=CleanerType.E2FGVI_HQ,
|
| 54 |
+
enable_torch_compile=True,
|
| 55 |
+
detect_batch_size=4,
|
| 56 |
+
use_bf16=True,
|
| 57 |
+
)
|
| 58 |
+
with timer("E2FGVI_HQ + torch.compile + batch + bf16"):
|
| 59 |
+
sora_wm.run(
|
| 60 |
+
input_video_path,
|
| 61 |
+
Path(f"{output_video_path}_e2fgvi_hq_torch_compile_batch_bf16.mp4"),
|
| 62 |
+
)
|
| 63 |
+
with timer("E2FGVI_HQ + torch.compile + batch + bf16"):
|
| 64 |
+
sora_wm.run(
|
| 65 |
+
input_video_path,
|
| 66 |
+
Path(f"{output_video_path}_e2fgvi_hq_torch_compile_batch_bf16.mp4"),
|
| 67 |
+
)
|
cli.py
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import sys
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Dict, List
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def validate_args_and_show_help():
|
| 9 |
+
"""
|
| 10 |
+
Parse CLI arguments, validate the input folder, and return resolved paths and parsed args.
|
| 11 |
+
|
| 12 |
+
Parses command-line options for input, output, pattern, quiet, and model; converts input and output to resolved Path objects and validates that the input path exists and is a directory. Exits the process with code 1 if the input path is missing or not a directory.
|
| 13 |
+
|
| 14 |
+
Returns:
|
| 15 |
+
(input_folder, output_folder, args):
|
| 16 |
+
input_folder (Path): Resolved Path to the input directory.
|
| 17 |
+
output_folder (Path): Resolved Path to the output directory.
|
| 18 |
+
args (argparse.Namespace): Parsed command-line arguments.
|
| 19 |
+
"""
|
| 20 |
+
parser = argparse.ArgumentParser(
|
| 21 |
+
description="🎬 Batch process videos to remove Sora watermarks",
|
| 22 |
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 23 |
+
epilog="""
|
| 24 |
+
Examples:
|
| 25 |
+
# Process all .mp4 files in input folder
|
| 26 |
+
python batch_process.py -i /path/to/input -o /path/to/output
|
| 27 |
+
# Process all .mov files
|
| 28 |
+
python batch_process.py -i /path/to/input -o /path/to/output --pattern "*.mov"
|
| 29 |
+
# Process all video files (mp4, mov, avi)
|
| 30 |
+
python batch_process.py -i /path/to/input -o /path/to/output --pattern "*.{mp4,mov,avi}"
|
| 31 |
+
# Without displaying the Tqdm bar inside sorawm procrssing.
|
| 32 |
+
python batch_process.py -i /path/to/input -o /path/to/output --quiet
|
| 33 |
+
""",
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
parser.add_argument(
|
| 37 |
+
"-i",
|
| 38 |
+
"--input",
|
| 39 |
+
type=str,
|
| 40 |
+
required=True,
|
| 41 |
+
help="📁 Input folder containing video files",
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
parser.add_argument(
|
| 45 |
+
"-o",
|
| 46 |
+
"--output",
|
| 47 |
+
type=str,
|
| 48 |
+
required=True,
|
| 49 |
+
help="📁 Output folder for cleaned videos",
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
parser.add_argument(
|
| 53 |
+
"-p",
|
| 54 |
+
"--pattern",
|
| 55 |
+
type=str,
|
| 56 |
+
default="*.mp4",
|
| 57 |
+
help="🔍 File pattern to match (default: *.mp4)",
|
| 58 |
+
)
|
| 59 |
+
parser.add_argument(
|
| 60 |
+
"--quiet",
|
| 61 |
+
action="store_true",
|
| 62 |
+
default=False,
|
| 63 |
+
help="Run in quiet mode (suppress tqdm and most logs).",
|
| 64 |
+
)
|
| 65 |
+
parser.add_argument(
|
| 66 |
+
"-m",
|
| 67 |
+
"--model",
|
| 68 |
+
type=str,
|
| 69 |
+
default="lama",
|
| 70 |
+
choices=["lama", "e2fgvi_hq"],
|
| 71 |
+
help="🔧 Model to use for watermark removal (default: lama). Options: lama (fast, may flicker), e2fgvi_hq (time consistent, slower)",
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
args = parser.parse_args()
|
| 75 |
+
|
| 76 |
+
# Convert to Path objects
|
| 77 |
+
input_folder = Path(args.input).expanduser().resolve()
|
| 78 |
+
output_folder = Path(args.output).expanduser().resolve()
|
| 79 |
+
|
| 80 |
+
# Validate input folder
|
| 81 |
+
if not input_folder.exists():
|
| 82 |
+
print(f"❌ Error: Input folder does not exist: {input_folder}", file=sys.stderr)
|
| 83 |
+
sys.exit(1)
|
| 84 |
+
|
| 85 |
+
if not input_folder.is_dir():
|
| 86 |
+
print(
|
| 87 |
+
f"❌ Error: Input path is not a directory: {input_folder}", file=sys.stderr
|
| 88 |
+
)
|
| 89 |
+
sys.exit(1)
|
| 90 |
+
|
| 91 |
+
return input_folder, output_folder, args
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
# Classes are now defined inside main() after imports
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def main():
|
| 98 |
+
# Validate arguments BEFORE loading heavy dependencies (ffmpeg, torch, etc.)
|
| 99 |
+
"""
|
| 100 |
+
Orchestrate CLI argument validation, lazy-load heavy dependencies, and run the batch video processing workflow.
|
| 101 |
+
|
| 102 |
+
Validates and processes command-line arguments, imports runtime-only dependencies, selects the watermark removal model, constructs and runs the batch processor, and handles termination: exits with code 130 on user interrupt and with code 1 on other fatal errors.
|
| 103 |
+
"""
|
| 104 |
+
input_folder, output_folder, args = validate_args_and_show_help()
|
| 105 |
+
|
| 106 |
+
pattern = args.pattern
|
| 107 |
+
|
| 108 |
+
# Only NOW import heavy dependencies after validation passed
|
| 109 |
+
from rich import box
|
| 110 |
+
from rich.console import Console
|
| 111 |
+
from rich.panel import Panel
|
| 112 |
+
from rich.progress import (
|
| 113 |
+
BarColumn,
|
| 114 |
+
MofNCompleteColumn,
|
| 115 |
+
Progress,
|
| 116 |
+
ProgressColumn,
|
| 117 |
+
SpinnerColumn,
|
| 118 |
+
TaskProgressColumn,
|
| 119 |
+
TextColumn,
|
| 120 |
+
TimeElapsedColumn,
|
| 121 |
+
TimeRemainingColumn,
|
| 122 |
+
)
|
| 123 |
+
from rich.table import Table
|
| 124 |
+
from rich.text import Text
|
| 125 |
+
from rich.text import Text as RichText
|
| 126 |
+
|
| 127 |
+
from sorawm.core import SoraWM
|
| 128 |
+
from sorawm.schemas import CleanerType
|
| 129 |
+
|
| 130 |
+
# Initialize console after importing rich
|
| 131 |
+
console = Console()
|
| 132 |
+
|
| 133 |
+
# Make SpeedColumn a proper ProgressColumn subclass now that we've imported it
|
| 134 |
+
global SpeedColumn
|
| 135 |
+
|
| 136 |
+
class SpeedColumnImpl(ProgressColumn):
|
| 137 |
+
"""Custom column to display processing speed in it/s format (only for video processing)"""
|
| 138 |
+
|
| 139 |
+
def render(self, task):
|
| 140 |
+
"""Render the speed in it/s format, but only for video processing tasks"""
|
| 141 |
+
# Only show speed for video processing, not for overall batch progress
|
| 142 |
+
if "Overall Progress" in task.description:
|
| 143 |
+
return RichText("", style="")
|
| 144 |
+
|
| 145 |
+
speed = task.finished_speed or task.speed
|
| 146 |
+
if speed is None:
|
| 147 |
+
return RichText("-- it/s", style="progress.data.speed")
|
| 148 |
+
return RichText(f"{speed:.2f} it/s", style="cyan")
|
| 149 |
+
|
| 150 |
+
SpeedColumn = SpeedColumnImpl
|
| 151 |
+
|
| 152 |
+
# Define BatchProcessor here to have access to all imports
|
| 153 |
+
class BatchProcessorImpl:
|
| 154 |
+
"""Batch video processor with progress tracking"""
|
| 155 |
+
|
| 156 |
+
def __init__(
|
| 157 |
+
self,
|
| 158 |
+
input_folder: Path,
|
| 159 |
+
output_folder: Path,
|
| 160 |
+
pattern: str = "*.mp4",
|
| 161 |
+
cleaner_type: CleanerType = CleanerType.LAMA,
|
| 162 |
+
):
|
| 163 |
+
"""
|
| 164 |
+
Initialize the batch processor with paths, file-matching pattern, and watermark cleaner selection.
|
| 165 |
+
|
| 166 |
+
Parameters:
|
| 167 |
+
input_folder (Path): Directory containing videos to process.
|
| 168 |
+
output_folder (Path): Directory where cleaned videos will be written.
|
| 169 |
+
pattern (str): Glob pattern used to find video files in the input folder (default: "*.mp4").
|
| 170 |
+
cleaner_type (CleanerType): Cleaner model to use for watermark removal (e.g., CleanerType.LAMA or CleanerType.E2FGVI_HQ).
|
| 171 |
+
"""
|
| 172 |
+
self.input_folder = input_folder
|
| 173 |
+
self.output_folder = output_folder
|
| 174 |
+
self.pattern = pattern
|
| 175 |
+
self.sora_wm = SoraWM(cleaner_type=cleaner_type)
|
| 176 |
+
self.console = console
|
| 177 |
+
|
| 178 |
+
# Statistics
|
| 179 |
+
self.successful: List[str] = []
|
| 180 |
+
self.failed: Dict[str, str] = {}
|
| 181 |
+
|
| 182 |
+
def show_banner(self):
|
| 183 |
+
"""Display a colorful welcome banner"""
|
| 184 |
+
banner_text = Text()
|
| 185 |
+
banner_text.append("🎬 ", style="bold yellow")
|
| 186 |
+
banner_text.append("Sora Watermark Remover", style="bold cyan")
|
| 187 |
+
banner_text.append(" - Batch Processor", style="bold magenta")
|
| 188 |
+
|
| 189 |
+
panel = Panel(
|
| 190 |
+
banner_text,
|
| 191 |
+
box=box.DOUBLE,
|
| 192 |
+
border_style="bright_blue",
|
| 193 |
+
padding=(1, 2),
|
| 194 |
+
)
|
| 195 |
+
console.print(panel)
|
| 196 |
+
console.print()
|
| 197 |
+
|
| 198 |
+
def find_videos(self) -> List[Path]:
|
| 199 |
+
"""Find all video files matching the pattern"""
|
| 200 |
+
video_files = list(self.input_folder.glob(self.pattern))
|
| 201 |
+
return sorted(video_files)
|
| 202 |
+
|
| 203 |
+
def process_batch(self):
|
| 204 |
+
"""Process all videos in the batch with progress tracking"""
|
| 205 |
+
# Show banner
|
| 206 |
+
self.show_banner()
|
| 207 |
+
|
| 208 |
+
# Find all videos
|
| 209 |
+
video_files = self.find_videos()
|
| 210 |
+
|
| 211 |
+
if not video_files:
|
| 212 |
+
console.print(
|
| 213 |
+
f"[bold red]❌ No files matching '{self.pattern}' found in {self.input_folder}[/bold red]"
|
| 214 |
+
)
|
| 215 |
+
return
|
| 216 |
+
|
| 217 |
+
# Display configuration
|
| 218 |
+
config_table = Table(show_header=False, box=box.SIMPLE, padding=(0, 1))
|
| 219 |
+
config_table.add_row(
|
| 220 |
+
"📁 Input folder:", f"[cyan]{self.input_folder}[/cyan]"
|
| 221 |
+
)
|
| 222 |
+
config_table.add_row(
|
| 223 |
+
"📁 Output folder:", f"[green]{self.output_folder}[/green]"
|
| 224 |
+
)
|
| 225 |
+
config_table.add_row("🔍 Pattern:", f"[yellow]{self.pattern}[/yellow]")
|
| 226 |
+
config_table.add_row(
|
| 227 |
+
"🎬 Videos found:", f"[bold magenta]{len(video_files)}[/bold magenta]"
|
| 228 |
+
)
|
| 229 |
+
console.print(config_table)
|
| 230 |
+
console.print()
|
| 231 |
+
|
| 232 |
+
# Create output folder
|
| 233 |
+
self.output_folder.mkdir(parents=True, exist_ok=True)
|
| 234 |
+
|
| 235 |
+
# Process each video with batch-level progress bar
|
| 236 |
+
start_time = datetime.now()
|
| 237 |
+
|
| 238 |
+
# Create rich progress display
|
| 239 |
+
with Progress(
|
| 240 |
+
SpinnerColumn(),
|
| 241 |
+
TextColumn("[progress.description]{task.description}"),
|
| 242 |
+
BarColumn(bar_width=40),
|
| 243 |
+
TaskProgressColumn(),
|
| 244 |
+
MofNCompleteColumn(),
|
| 245 |
+
SpeedColumn(),
|
| 246 |
+
TimeElapsedColumn(),
|
| 247 |
+
TimeRemainingColumn(),
|
| 248 |
+
console=console,
|
| 249 |
+
) as progress:
|
| 250 |
+
# Batch progress task
|
| 251 |
+
batch_task = progress.add_task(
|
| 252 |
+
"[cyan]Overall Progress", total=len(video_files)
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
for idx, input_path in enumerate(video_files, 1):
|
| 256 |
+
output_path = self.output_folder / f"cleaned_{input_path.name}"
|
| 257 |
+
|
| 258 |
+
# Update batch task description
|
| 259 |
+
progress.update(
|
| 260 |
+
batch_task,
|
| 261 |
+
description=f"[cyan]Overall Progress ({idx}/{len(video_files)})",
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
# Show current file being processed
|
| 265 |
+
console.print(
|
| 266 |
+
f"\n[bold blue]📹 [{idx}/{len(video_files)}][/bold blue] "
|
| 267 |
+
f"[yellow]{input_path.name}[/yellow]"
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
try:
|
| 271 |
+
# Video processing task
|
| 272 |
+
video_task = progress.add_task(
|
| 273 |
+
f" [green]Processing video", total=100
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
last_progress = [0]
|
| 277 |
+
|
| 278 |
+
def progress_callback(prog: int):
|
| 279 |
+
"""Update the video progress bar"""
|
| 280 |
+
if prog > last_progress[0]:
|
| 281 |
+
progress.update(
|
| 282 |
+
video_task, advance=prog - last_progress[0]
|
| 283 |
+
)
|
| 284 |
+
last_progress[0] = prog
|
| 285 |
+
|
| 286 |
+
# Process the video (quiet=True suppresses internal tqdm bars if enabled)
|
| 287 |
+
self.sora_wm.run(
|
| 288 |
+
input_path, output_path, progress_callback, quiet=args.quiet
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
# Ensure video progress reaches 100%
|
| 292 |
+
if last_progress[0] < 100:
|
| 293 |
+
progress.update(video_task, advance=100 - last_progress[0])
|
| 294 |
+
|
| 295 |
+
progress.remove_task(video_task)
|
| 296 |
+
|
| 297 |
+
self.successful.append(input_path.name)
|
| 298 |
+
console.print(
|
| 299 |
+
f" [bold green]✅ Completed:[/bold green] {output_path.name}"
|
| 300 |
+
)
|
| 301 |
+
|
| 302 |
+
except Exception as e:
|
| 303 |
+
progress.remove_task(video_task)
|
| 304 |
+
self.failed[input_path.name] = str(e)
|
| 305 |
+
console.print(f" [bold red]❌ Error:[/bold red] {e}")
|
| 306 |
+
|
| 307 |
+
# Update batch progress
|
| 308 |
+
progress.update(batch_task, advance=1)
|
| 309 |
+
|
| 310 |
+
# Print summary
|
| 311 |
+
self._print_summary(start_time)
|
| 312 |
+
|
| 313 |
+
def _print_summary(self, start_time: datetime):
|
| 314 |
+
"""Print processing summary with rich formatting"""
|
| 315 |
+
end_time = datetime.now()
|
| 316 |
+
duration = end_time - start_time
|
| 317 |
+
|
| 318 |
+
console.print()
|
| 319 |
+
|
| 320 |
+
# Create summary statistics table
|
| 321 |
+
summary_table = Table(
|
| 322 |
+
show_header=False, box=box.ROUNDED, border_style="cyan"
|
| 323 |
+
)
|
| 324 |
+
summary_table.add_column("Metric", style="bold")
|
| 325 |
+
summary_table.add_column("Value")
|
| 326 |
+
|
| 327 |
+
summary_table.add_row("⏱️ Total Time", f"[yellow]{duration}[/yellow]")
|
| 328 |
+
summary_table.add_row(
|
| 329 |
+
"✅ Successful", f"[bold green]{len(self.successful)}[/bold green]"
|
| 330 |
+
)
|
| 331 |
+
summary_table.add_row(
|
| 332 |
+
"❌ Failed", f"[bold red]{len(self.failed)}[/bold red]"
|
| 333 |
+
)
|
| 334 |
+
summary_table.add_row(
|
| 335 |
+
"📊 Total",
|
| 336 |
+
f"[bold magenta]{len(self.successful) + len(self.failed)}[/bold magenta]",
|
| 337 |
+
)
|
| 338 |
+
|
| 339 |
+
# Success rate
|
| 340 |
+
total = len(self.successful) + len(self.failed)
|
| 341 |
+
success_rate = (len(self.successful) / total * 100) if total > 0 else 0
|
| 342 |
+
summary_table.add_row(
|
| 343 |
+
"📈 Success Rate", f"[bold cyan]{success_rate:.1f}%[/bold cyan]"
|
| 344 |
+
)
|
| 345 |
+
|
| 346 |
+
# Wrap in a panel
|
| 347 |
+
summary_panel = Panel(
|
| 348 |
+
summary_table,
|
| 349 |
+
title="[bold white]📋 BATCH PROCESSING SUMMARY[/bold white]",
|
| 350 |
+
border_style="bright_cyan",
|
| 351 |
+
box=box.DOUBLE,
|
| 352 |
+
)
|
| 353 |
+
console.print(summary_panel)
|
| 354 |
+
|
| 355 |
+
# Successful files
|
| 356 |
+
if self.successful:
|
| 357 |
+
console.print()
|
| 358 |
+
success_table = Table(
|
| 359 |
+
title="[bold green]✅ Successfully Processed[/bold green]",
|
| 360 |
+
box=box.SIMPLE,
|
| 361 |
+
show_header=True,
|
| 362 |
+
header_style="bold green",
|
| 363 |
+
)
|
| 364 |
+
success_table.add_column("#", style="dim", width=4)
|
| 365 |
+
success_table.add_column("Filename", style="green")
|
| 366 |
+
|
| 367 |
+
for idx, filename in enumerate(self.successful, 1):
|
| 368 |
+
success_table.add_row(str(idx), filename)
|
| 369 |
+
|
| 370 |
+
console.print(success_table)
|
| 371 |
+
|
| 372 |
+
# Failed files
|
| 373 |
+
if self.failed:
|
| 374 |
+
console.print()
|
| 375 |
+
failed_table = Table(
|
| 376 |
+
title="[bold red]❌ Failed to Process[/bold red]",
|
| 377 |
+
box=box.SIMPLE,
|
| 378 |
+
show_header=True,
|
| 379 |
+
header_style="bold red",
|
| 380 |
+
)
|
| 381 |
+
failed_table.add_column("#", style="dim", width=4)
|
| 382 |
+
failed_table.add_column("Filename", style="red")
|
| 383 |
+
failed_table.add_column("Error", style="dim")
|
| 384 |
+
|
| 385 |
+
for idx, (filename, error) in enumerate(self.failed.items(), 1):
|
| 386 |
+
# Truncate long error messages
|
| 387 |
+
error_msg = error if len(error) < 60 else error[:57] + "..."
|
| 388 |
+
failed_table.add_row(str(idx), filename, error_msg)
|
| 389 |
+
|
| 390 |
+
console.print(failed_table)
|
| 391 |
+
|
| 392 |
+
# Final message
|
| 393 |
+
console.print()
|
| 394 |
+
if len(self.failed) == 0:
|
| 395 |
+
console.print(
|
| 396 |
+
"[bold green]🎉 All videos processed successfully![/bold green]",
|
| 397 |
+
justify="center",
|
| 398 |
+
)
|
| 399 |
+
else:
|
| 400 |
+
console.print(
|
| 401 |
+
"[bold yellow]⚠️ Some videos failed to process. Check errors above.[/bold yellow]",
|
| 402 |
+
justify="center",
|
| 403 |
+
)
|
| 404 |
+
console.print()
|
| 405 |
+
|
| 406 |
+
# Create processor and run
|
| 407 |
+
try:
|
| 408 |
+
cleaner_type = (
|
| 409 |
+
CleanerType.LAMA if args.model == "lama" else CleanerType.E2FGVI_HQ
|
| 410 |
+
)
|
| 411 |
+
processor = BatchProcessorImpl(
|
| 412 |
+
input_folder, output_folder, pattern, cleaner_type
|
| 413 |
+
)
|
| 414 |
+
processor.process_batch()
|
| 415 |
+
except KeyboardInterrupt:
|
| 416 |
+
console.print()
|
| 417 |
+
console.print(
|
| 418 |
+
"[bold yellow]⚠️ Processing interrupted by user[/bold yellow]",
|
| 419 |
+
justify="center",
|
| 420 |
+
)
|
| 421 |
+
sys.exit(130)
|
| 422 |
+
except Exception as e:
|
| 423 |
+
console.print()
|
| 424 |
+
console.print(f"[bold red]❌ Fatal error:[/bold red] {e}")
|
| 425 |
+
sys.exit(1)
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
if __name__ == "__main__":
|
| 429 |
+
main()
|
| 430 |
+
1
|
datasets/make_yolo_images.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
|
| 3 |
+
import cv2
|
| 4 |
+
from tqdm import tqdm
|
| 5 |
+
|
| 6 |
+
from sorawm.configs import ROOT
|
| 7 |
+
from sorawm.watermark_detector import SoraWaterMarkDetector
|
| 8 |
+
|
| 9 |
+
videos_dir = ROOT / "videos"
|
| 10 |
+
datasets_dir = ROOT / "datasets"
|
| 11 |
+
images_dir = datasets_dir / "images"
|
| 12 |
+
images_dir.mkdir(exist_ok=True, parents=True)
|
| 13 |
+
detector = SoraWaterMarkDetector()
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
if __name__ == "__main__":
|
| 17 |
+
fps_save_interval = 1 # Save every 5th frame
|
| 18 |
+
|
| 19 |
+
video_idx = 0
|
| 20 |
+
image_idx = 0 # 全局图片索引
|
| 21 |
+
total_failed = 0 # 检测失败的总数
|
| 22 |
+
|
| 23 |
+
for video_path in tqdm(list(videos_dir.rglob("*.mp4"))):
|
| 24 |
+
# Open the video file
|
| 25 |
+
cap = cv2.VideoCapture(str(video_path))
|
| 26 |
+
video_name = video_path.name
|
| 27 |
+
if not cap.isOpened():
|
| 28 |
+
print(f"Error opening video: {video_path}")
|
| 29 |
+
continue
|
| 30 |
+
|
| 31 |
+
frame_count = 0
|
| 32 |
+
|
| 33 |
+
try:
|
| 34 |
+
while True:
|
| 35 |
+
ret, frame = cap.read()
|
| 36 |
+
|
| 37 |
+
# Break if no more frames
|
| 38 |
+
if not ret:
|
| 39 |
+
break
|
| 40 |
+
|
| 41 |
+
# Save frame at the specified interval
|
| 42 |
+
if frame_count % fps_save_interval == 0:
|
| 43 |
+
if not detector.detect(frame)["detected"]:
|
| 44 |
+
# Create filename: image_idx_framecount.jpg
|
| 45 |
+
image_filename = (
|
| 46 |
+
f"{video_name}_failed_image_frame_{frame_count:06d}.jpg"
|
| 47 |
+
)
|
| 48 |
+
image_path = images_dir / image_filename
|
| 49 |
+
# Save the frame
|
| 50 |
+
cv2.imwrite(str(image_path), frame)
|
| 51 |
+
image_idx += 1
|
| 52 |
+
total_failed += 1
|
| 53 |
+
|
| 54 |
+
frame_count += 1
|
| 55 |
+
|
| 56 |
+
finally:
|
| 57 |
+
# Release the video capture object
|
| 58 |
+
cap.release()
|
| 59 |
+
|
| 60 |
+
video_idx += 1
|
| 61 |
+
|
| 62 |
+
print(
|
| 63 |
+
f"Processed {video_idx} videos, extracted {total_failed} failed detection frames to {images_dir}"
|
| 64 |
+
)
|
docker-compose.yaml
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3'
|
| 2 |
+
services:
|
| 3 |
+
cog-video:
|
| 4 |
+
image: llinkedlist/sorawm:latest #nvidia/cuda:12.1.1-cudnn8-devel-ubuntu22.04
|
| 5 |
+
container_name: sorawm_container
|
| 6 |
+
ports:
|
| 7 |
+
- 5344:5344
|
| 8 |
+
- 8501:8501
|
| 9 |
+
command: streamlit run app.py --server.port 8501 --server.address 0.0.0.0
|
| 10 |
+
volumes:
|
| 11 |
+
- ./:/workspace
|
| 12 |
+
- ./.cache:/root/.cache
|
| 13 |
+
deploy:
|
| 14 |
+
resources:
|
| 15 |
+
reservations:
|
| 16 |
+
devices:
|
| 17 |
+
- driver: nvidia
|
| 18 |
+
count: all
|
| 19 |
+
capabilities: [gpu]
|
| 20 |
+
tty: true
|
| 21 |
+
stdin_open: true
|
| 22 |
+
working_dir: /workspace
|
| 23 |
+
shm_size: '16gb'
|
| 24 |
+
|
| 25 |
+
volumes:
|
| 26 |
+
huggingface_cache:
|
docker-entrypoint.sh
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
set -e
|
| 3 |
+
|
| 4 |
+
# Start FastAPI server in background
|
| 5 |
+
python start_server.py --host 0.0.0.0 --port 5344 &
|
| 6 |
+
|
| 7 |
+
# Start Streamlit (primary app, HF Spaces routes to app_port 8501)
|
| 8 |
+
streamlit run app.py \
|
| 9 |
+
--server.port 8501 \
|
| 10 |
+
--server.address 0.0.0.0 \
|
| 11 |
+
--server.headless true \
|
| 12 |
+
--server.enableCORS false \
|
| 13 |
+
--server.enableXsrfProtection false
|
example.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
|
| 3 |
+
from sorawm.core import SoraWM
|
| 4 |
+
from sorawm.schemas import CleanerType
|
| 5 |
+
|
| 6 |
+
if __name__ == "__main__":
|
| 7 |
+
input_video_path = Path("resources/dog_vs_sam.mp4")
|
| 8 |
+
output_video_path = Path("outputs/sora_watermark_removed")
|
| 9 |
+
|
| 10 |
+
# 1. LAMA is fast and good quality, but not time consistent.
|
| 11 |
+
sora_wm = SoraWM(cleaner_type=CleanerType.LAMA)
|
| 12 |
+
sora_wm.run(input_video_path, Path(f"{output_video_path}_lama.mp4"))
|
| 13 |
+
|
| 14 |
+
# 2. E2FGVI_HQ ensures time consistency, but will be very slow on no-cuda device.
|
| 15 |
+
sora_wm = SoraWM(cleaner_type=CleanerType.E2FGVI_HQ, enable_torch_compile=False)
|
| 16 |
+
sora_wm.run(input_video_path, Path(f"{output_video_path}_e2fgvi_hq.mp4"))
|
| 17 |
+
|
| 18 |
+
# 3. E2FGVI_HQ with torch compile is fast and good quality, but not time consistent.
|
| 19 |
+
sora_wm = SoraWM(cleaner_type=CleanerType.E2FGVI_HQ, enable_torch_compile=True)
|
| 20 |
+
sora_wm.run(
|
| 21 |
+
input_video_path, Path(f"{output_video_path}_e2fgvi_hq_torch_compile.mp4")
|
| 22 |
+
)
|
ffmpeg/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# FFmpeg 可执行文件目录
|
| 2 |
+
|
| 3 |
+
## 用途
|
| 4 |
+
|
| 5 |
+
这个目录用于存放 FFmpeg 可执行文件,使项目成为真正的便携版(无需系统安装 FFmpeg)。
|
| 6 |
+
|
| 7 |
+
## Windows 用户配置步骤
|
| 8 |
+
|
| 9 |
+
### 1. 下载 FFmpeg
|
| 10 |
+
|
| 11 |
+
访问 [FFmpeg-Builds Release](https://github.com/BtbN/FFmpeg-Builds/releases) 页面:
|
| 12 |
+
|
| 13 |
+
- 下载最新的 `ffmpeg-master-latest-win64-gpl.zip`(约 120MB)
|
| 14 |
+
- 或者下载特定版本,如 `ffmpeg-n6.1-latest-win64-gpl-6.1.zip`
|
| 15 |
+
|
| 16 |
+
### 2. 解压并复制文件
|
| 17 |
+
|
| 18 |
+
1. 解压下载的 zip 文件
|
| 19 |
+
2. 在解压后的文件夹中找到 `bin` 目录
|
| 20 |
+
3. 将以下两个文件复制到**当前目录**(`ffmpeg/`):
|
| 21 |
+
- `ffmpeg.exe` - FFmpeg 主程序
|
| 22 |
+
- `ffprobe.exe` - FFmpeg 媒体信息探测工具
|
| 23 |
+
|
| 24 |
+
### 3. 验证配置
|
| 25 |
+
|
| 26 |
+
完成后,此目录应包含:
|
| 27 |
+
|
| 28 |
+
```
|
| 29 |
+
ffmpeg/
|
| 30 |
+
├── .gitkeep
|
| 31 |
+
├── README.md
|
| 32 |
+
├── ffmpeg.exe ← 你复制的文件
|
| 33 |
+
└── ffprobe.exe ← 你复制的文件
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
### 4. 测试
|
| 37 |
+
|
| 38 |
+
运行项目中的测试脚本验证配置:
|
| 39 |
+
|
| 40 |
+
```bash
|
| 41 |
+
python test_ffmpeg_setup.py
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
如果配置正确,你将看到:`✓ 测试通过!FFmpeg已正确配置并可以使用`
|
| 45 |
+
|
| 46 |
+
## macOS/Linux 用户
|
| 47 |
+
|
| 48 |
+
如果需要便携版,请:
|
| 49 |
+
|
| 50 |
+
1. 下载对应平台的 FFmpeg 二进制文件
|
| 51 |
+
2. 将 `ffmpeg` 和 `ffprobe` 可执行文件放到此目录
|
| 52 |
+
3. 确保文件有执行权限:`chmod +x ffmpeg ffprobe`
|
| 53 |
+
|
| 54 |
+
## 注意事项
|
| 55 |
+
|
| 56 |
+
- 这些可执行文件不会被 git 提交(已在 `.gitignore` 中配置)
|
| 57 |
+
- 程序会自动检测并使用此目录下的 FFmpeg
|
| 58 |
+
- 如果此目录没有 FFmpeg,程序会尝试使用系统安装的版本
|
| 59 |
+
|
| 60 |
+
## 下载链接汇总
|
| 61 |
+
|
| 62 |
+
- **Windows**: https://github.com/BtbN/FFmpeg-Builds/releases
|
| 63 |
+
- **官方网站**: https://ffmpeg.org/download.html
|
| 64 |
+
- **镜像站点**: https://www.gyan.dev/ffmpeg/builds/ (Windows)
|
| 65 |
+
|
| 66 |
+
## 许可证
|
| 67 |
+
|
| 68 |
+
FFmpeg 使用 GPL 许可证,请遵守相关条款。
|
| 69 |
+
|
frontend/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# sorawm
|
| 2 |
+
|
| 3 |
+
This template should help get you started developing with Vue 3 in Vite.
|
| 4 |
+
|
| 5 |
+
## Recommended IDE Setup
|
| 6 |
+
|
| 7 |
+
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
| 8 |
+
|
| 9 |
+
## Recommended Browser Setup
|
| 10 |
+
|
| 11 |
+
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
|
| 12 |
+
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
|
| 13 |
+
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
|
| 14 |
+
- Firefox:
|
| 15 |
+
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
|
| 16 |
+
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
|
| 17 |
+
|
| 18 |
+
## Customize configuration
|
| 19 |
+
|
| 20 |
+
See [Vite Configuration Reference](https://vite.dev/config/).
|
| 21 |
+
|
| 22 |
+
## Project Setup
|
| 23 |
+
|
| 24 |
+
```sh
|
| 25 |
+
npm install
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
### Compile and Hot-Reload for Development
|
| 29 |
+
|
| 30 |
+
```sh
|
| 31 |
+
npm run dev
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
### Compile and Minify for Production
|
| 35 |
+
|
| 36 |
+
```sh
|
| 37 |
+
npm run build
|
| 38 |
+
```
|
frontend/bun.lock
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"lockfileVersion": 1,
|
| 3 |
+
"workspaces": {
|
| 4 |
+
"": {
|
| 5 |
+
"name": "sorawm",
|
| 6 |
+
"dependencies": {
|
| 7 |
+
"element-plus": "^2.11.8",
|
| 8 |
+
"pinia": "^3.0.4",
|
| 9 |
+
"vue": "^3.5.22",
|
| 10 |
+
},
|
| 11 |
+
"devDependencies": {
|
| 12 |
+
"@vitejs/plugin-vue": "^6.0.1",
|
| 13 |
+
"vite": "^7.1.11",
|
| 14 |
+
"vite-plugin-vue-devtools": "^8.0.3",
|
| 15 |
+
},
|
| 16 |
+
},
|
| 17 |
+
},
|
| 18 |
+
"packages": {
|
| 19 |
+
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
| 20 |
+
|
| 21 |
+
"@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
|
| 22 |
+
|
| 23 |
+
"@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@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" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
|
| 24 |
+
|
| 25 |
+
"@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
| 26 |
+
|
| 27 |
+
"@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="],
|
| 28 |
+
|
| 29 |
+
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
| 30 |
+
|
| 31 |
+
"@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="],
|
| 32 |
+
|
| 33 |
+
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
| 34 |
+
|
| 35 |
+
"@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="],
|
| 36 |
+
|
| 37 |
+
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
| 38 |
+
|
| 39 |
+
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
|
| 40 |
+
|
| 41 |
+
"@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="],
|
| 42 |
+
|
| 43 |
+
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
|
| 44 |
+
|
| 45 |
+
"@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="],
|
| 46 |
+
|
| 47 |
+
"@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="],
|
| 48 |
+
|
| 49 |
+
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
| 50 |
+
|
| 51 |
+
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
| 52 |
+
|
| 53 |
+
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
| 54 |
+
|
| 55 |
+
"@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
| 56 |
+
|
| 57 |
+
"@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
| 58 |
+
|
| 59 |
+
"@babel/plugin-proposal-decorators": ["@babel/plugin-proposal-decorators@7.28.0", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-decorators": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg=="],
|
| 60 |
+
|
| 61 |
+
"@babel/plugin-syntax-decorators": ["@babel/plugin-syntax-decorators@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A=="],
|
| 62 |
+
|
| 63 |
+
"@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww=="],
|
| 64 |
+
|
| 65 |
+
"@babel/plugin-syntax-import-meta": ["@babel/plugin-syntax-import-meta@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g=="],
|
| 66 |
+
|
| 67 |
+
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w=="],
|
| 68 |
+
|
| 69 |
+
"@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ=="],
|
| 70 |
+
|
| 71 |
+
"@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA=="],
|
| 72 |
+
|
| 73 |
+
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
| 74 |
+
|
| 75 |
+
"@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
| 76 |
+
|
| 77 |
+
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
| 78 |
+
|
| 79 |
+
"@ctrl/tinycolor": ["@ctrl/tinycolor@3.6.1", "", {}, "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA=="],
|
| 80 |
+
|
| 81 |
+
"@element-plus/icons-vue": ["@element-plus/icons-vue@2.3.2", "", { "peerDependencies": { "vue": "^3.2.0" } }, "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A=="],
|
| 82 |
+
|
| 83 |
+
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
| 84 |
+
|
| 85 |
+
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
|
| 86 |
+
|
| 87 |
+
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
|
| 88 |
+
|
| 89 |
+
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
|
| 90 |
+
|
| 91 |
+
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
|
| 92 |
+
|
| 93 |
+
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
|
| 94 |
+
|
| 95 |
+
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
|
| 96 |
+
|
| 97 |
+
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
|
| 98 |
+
|
| 99 |
+
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
|
| 100 |
+
|
| 101 |
+
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
|
| 102 |
+
|
| 103 |
+
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
|
| 104 |
+
|
| 105 |
+
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
|
| 106 |
+
|
| 107 |
+
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
|
| 108 |
+
|
| 109 |
+
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
|
| 110 |
+
|
| 111 |
+
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
|
| 112 |
+
|
| 113 |
+
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
|
| 114 |
+
|
| 115 |
+
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
|
| 116 |
+
|
| 117 |
+
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
|
| 118 |
+
|
| 119 |
+
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
|
| 120 |
+
|
| 121 |
+
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
|
| 122 |
+
|
| 123 |
+
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
|
| 124 |
+
|
| 125 |
+
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
|
| 126 |
+
|
| 127 |
+
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
|
| 128 |
+
|
| 129 |
+
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
|
| 130 |
+
|
| 131 |
+
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
|
| 132 |
+
|
| 133 |
+
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
| 134 |
+
|
| 135 |
+
"@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
|
| 136 |
+
|
| 137 |
+
"@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="],
|
| 138 |
+
|
| 139 |
+
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
|
| 140 |
+
|
| 141 |
+
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
| 142 |
+
|
| 143 |
+
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
| 144 |
+
|
| 145 |
+
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
| 146 |
+
|
| 147 |
+
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
| 148 |
+
|
| 149 |
+
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
| 150 |
+
|
| 151 |
+
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
| 152 |
+
|
| 153 |
+
"@popperjs/core": ["@sxzz/popperjs-es@2.11.7", "", {}, "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ=="],
|
| 154 |
+
|
| 155 |
+
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.50", "", {}, "sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA=="],
|
| 156 |
+
|
| 157 |
+
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.3", "", { "os": "android", "cpu": "arm" }, "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w=="],
|
| 158 |
+
|
| 159 |
+
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.53.3", "", { "os": "android", "cpu": "arm64" }, "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w=="],
|
| 160 |
+
|
| 161 |
+
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.53.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA=="],
|
| 162 |
+
|
| 163 |
+
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.53.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ=="],
|
| 164 |
+
|
| 165 |
+
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.53.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w=="],
|
| 166 |
+
|
| 167 |
+
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.53.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q=="],
|
| 168 |
+
|
| 169 |
+
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.53.3", "", { "os": "linux", "cpu": "arm" }, "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw=="],
|
| 170 |
+
|
| 171 |
+
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.53.3", "", { "os": "linux", "cpu": "arm" }, "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg=="],
|
| 172 |
+
|
| 173 |
+
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.53.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w=="],
|
| 174 |
+
|
| 175 |
+
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.53.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A=="],
|
| 176 |
+
|
| 177 |
+
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g=="],
|
| 178 |
+
|
| 179 |
+
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.53.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw=="],
|
| 180 |
+
|
| 181 |
+
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g=="],
|
| 182 |
+
|
| 183 |
+
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A=="],
|
| 184 |
+
|
| 185 |
+
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.53.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg=="],
|
| 186 |
+
|
| 187 |
+
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.53.3", "", { "os": "linux", "cpu": "x64" }, "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w=="],
|
| 188 |
+
|
| 189 |
+
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.53.3", "", { "os": "linux", "cpu": "x64" }, "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q=="],
|
| 190 |
+
|
| 191 |
+
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.53.3", "", { "os": "none", "cpu": "arm64" }, "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw=="],
|
| 192 |
+
|
| 193 |
+
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.53.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw=="],
|
| 194 |
+
|
| 195 |
+
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.53.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA=="],
|
| 196 |
+
|
| 197 |
+
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.53.3", "", { "os": "win32", "cpu": "x64" }, "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg=="],
|
| 198 |
+
|
| 199 |
+
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.53.3", "", { "os": "win32", "cpu": "x64" }, "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ=="],
|
| 200 |
+
|
| 201 |
+
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
| 202 |
+
|
| 203 |
+
"@types/lodash": ["@types/lodash@4.17.21", "", {}, "sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ=="],
|
| 204 |
+
|
| 205 |
+
"@types/lodash-es": ["@types/lodash-es@4.17.12", "", { "dependencies": { "@types/lodash": "*" } }, "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ=="],
|
| 206 |
+
|
| 207 |
+
"@types/web-bluetooth": ["@types/web-bluetooth@0.0.16", "", {}, "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ=="],
|
| 208 |
+
|
| 209 |
+
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.2", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-beta.50" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", "vue": "^3.2.25" } }, "sha512-iHmwV3QcVGGvSC1BG5bZ4z6iwa1SOpAPWmnjOErd4Ske+lZua5K9TtAVdx0gMBClJ28DViCbSmZitjWZsWO3LA=="],
|
| 210 |
+
|
| 211 |
+
"@vue/babel-helper-vue-transform-on": ["@vue/babel-helper-vue-transform-on@1.5.0", "", {}, "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA=="],
|
| 212 |
+
|
| 213 |
+
"@vue/babel-plugin-jsx": ["@vue/babel-plugin-jsx@1.5.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.2", "@vue/babel-helper-vue-transform-on": "1.5.0", "@vue/babel-plugin-resolve-type": "1.5.0", "@vue/shared": "^3.5.18" }, "peerDependencies": { "@babel/core": "^7.0.0-0" }, "optionalPeers": ["@babel/core"] }, "sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw=="],
|
| 214 |
+
|
| 215 |
+
"@vue/babel-plugin-resolve-type": ["@vue/babel-plugin-resolve-type@1.5.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/parser": "^7.28.0", "@vue/compiler-sfc": "^3.5.18" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w=="],
|
| 216 |
+
|
| 217 |
+
"@vue/compiler-core": ["@vue/compiler-core@3.5.24", "", { "dependencies": { "@babel/parser": "^7.28.5", "@vue/shared": "3.5.24", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig=="],
|
| 218 |
+
|
| 219 |
+
"@vue/compiler-dom": ["@vue/compiler-dom@3.5.24", "", { "dependencies": { "@vue/compiler-core": "3.5.24", "@vue/shared": "3.5.24" } }, "sha512-1QHGAvs53gXkWdd3ZMGYuvQFXHW4ksKWPG8HP8/2BscrbZ0brw183q2oNWjMrSWImYLHxHrx1ItBQr50I/q2zw=="],
|
| 220 |
+
|
| 221 |
+
"@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.24", "", { "dependencies": { "@babel/parser": "^7.28.5", "@vue/compiler-core": "3.5.24", "@vue/compiler-dom": "3.5.24", "@vue/compiler-ssr": "3.5.24", "@vue/shared": "3.5.24", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-8EG5YPRgmTB+YxYBM3VXy8zHD9SWHUJLIGPhDovo3Z8VOgvP+O7UP5vl0J4BBPWYD9vxtBabzW1EuEZ+Cqs14g=="],
|
| 222 |
+
|
| 223 |
+
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.24", "", { "dependencies": { "@vue/compiler-dom": "3.5.24", "@vue/shared": "3.5.24" } }, "sha512-trOvMWNBMQ/odMRHW7Ae1CdfYx+7MuiQu62Jtu36gMLXcaoqKvAyh+P73sYG9ll+6jLB6QPovqoKGGZROzkFFg=="],
|
| 224 |
+
|
| 225 |
+
"@vue/devtools-api": ["@vue/devtools-api@7.7.9", "", { "dependencies": { "@vue/devtools-kit": "^7.7.9" } }, "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g=="],
|
| 226 |
+
|
| 227 |
+
"@vue/devtools-core": ["@vue/devtools-core@8.0.5", "", { "dependencies": { "@vue/devtools-kit": "^8.0.5", "@vue/devtools-shared": "^8.0.5", "mitt": "^3.0.1", "nanoid": "^5.1.5", "pathe": "^2.0.3", "vite-hot-client": "^2.1.0" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-dpCw8nl0GDBuiL9SaY0mtDxoGIEmU38w+TQiYEPOLhW03VDC0lfNMYXS/qhl4I0YlysGp04NLY4UNn6xgD0VIQ=="],
|
| 228 |
+
|
| 229 |
+
"@vue/devtools-kit": ["@vue/devtools-kit@8.0.5", "", { "dependencies": { "@vue/devtools-shared": "^8.0.5", "birpc": "^2.6.1", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^2.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg=="],
|
| 230 |
+
|
| 231 |
+
"@vue/devtools-shared": ["@vue/devtools-shared@8.0.5", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg=="],
|
| 232 |
+
|
| 233 |
+
"@vue/reactivity": ["@vue/reactivity@3.5.24", "", { "dependencies": { "@vue/shared": "3.5.24" } }, "sha512-BM8kBhtlkkbnyl4q+HiF5R5BL0ycDPfihowulm02q3WYp2vxgPcJuZO866qa/0u3idbMntKEtVNuAUp5bw4teg=="],
|
| 234 |
+
|
| 235 |
+
"@vue/runtime-core": ["@vue/runtime-core@3.5.24", "", { "dependencies": { "@vue/reactivity": "3.5.24", "@vue/shared": "3.5.24" } }, "sha512-RYP/byyKDgNIqfX/gNb2PB55dJmM97jc9wyF3jK7QUInYKypK2exmZMNwnjueWwGceEkP6NChd3D2ZVEp9undQ=="],
|
| 236 |
+
|
| 237 |
+
"@vue/runtime-dom": ["@vue/runtime-dom@3.5.24", "", { "dependencies": { "@vue/reactivity": "3.5.24", "@vue/runtime-core": "3.5.24", "@vue/shared": "3.5.24", "csstype": "^3.1.3" } }, "sha512-Z8ANhr/i0XIluonHVjbUkjvn+CyrxbXRIxR7wn7+X7xlcb7dJsfITZbkVOeJZdP8VZwfrWRsWdShH6pngMxRjw=="],
|
| 238 |
+
|
| 239 |
+
"@vue/server-renderer": ["@vue/server-renderer@3.5.24", "", { "dependencies": { "@vue/compiler-ssr": "3.5.24", "@vue/shared": "3.5.24" }, "peerDependencies": { "vue": "3.5.24" } }, "sha512-Yh2j2Y4G/0/4z/xJ1Bad4mxaAk++C2v4kaa8oSYTMJBJ00/ndPuxCnWeot0/7/qafQFLh5pr6xeV6SdMcE/G1w=="],
|
| 240 |
+
|
| 241 |
+
"@vue/shared": ["@vue/shared@3.5.24", "", {}, "sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A=="],
|
| 242 |
+
|
| 243 |
+
"@vueuse/core": ["@vueuse/core@9.13.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.16", "@vueuse/metadata": "9.13.0", "@vueuse/shared": "9.13.0", "vue-demi": "*" } }, "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw=="],
|
| 244 |
+
|
| 245 |
+
"@vueuse/metadata": ["@vueuse/metadata@9.13.0", "", {}, "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ=="],
|
| 246 |
+
|
| 247 |
+
"@vueuse/shared": ["@vueuse/shared@9.13.0", "", { "dependencies": { "vue-demi": "*" } }, "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw=="],
|
| 248 |
+
|
| 249 |
+
"ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="],
|
| 250 |
+
|
| 251 |
+
"async-validator": ["async-validator@4.2.5", "", {}, "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg=="],
|
| 252 |
+
|
| 253 |
+
"baseline-browser-mapping": ["baseline-browser-mapping@2.8.31", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw=="],
|
| 254 |
+
|
| 255 |
+
"birpc": ["birpc@2.8.0", "", {}, "sha512-Bz2a4qD/5GRhiHSwj30c/8kC8QGj12nNDwz3D4ErQ4Xhy35dsSDvF+RA/tWpjyU0pdGtSDiEk6B5fBGE1qNVhw=="],
|
| 256 |
+
|
| 257 |
+
"browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="],
|
| 258 |
+
|
| 259 |
+
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
| 260 |
+
|
| 261 |
+
"caniuse-lite": ["caniuse-lite@1.0.30001756", "", {}, "sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A=="],
|
| 262 |
+
|
| 263 |
+
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
| 264 |
+
|
| 265 |
+
"copy-anything": ["copy-anything@4.0.5", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA=="],
|
| 266 |
+
|
| 267 |
+
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
| 268 |
+
|
| 269 |
+
"dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="],
|
| 270 |
+
|
| 271 |
+
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
| 272 |
+
|
| 273 |
+
"default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="],
|
| 274 |
+
|
| 275 |
+
"default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
|
| 276 |
+
|
| 277 |
+
"define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
|
| 278 |
+
|
| 279 |
+
"electron-to-chromium": ["electron-to-chromium@1.5.259", "", {}, "sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ=="],
|
| 280 |
+
|
| 281 |
+
"element-plus": ["element-plus@2.11.8", "", { "dependencies": { "@ctrl/tinycolor": "^3.4.1", "@element-plus/icons-vue": "^2.3.2", "@floating-ui/dom": "^1.0.1", "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", "@types/lodash": "^4.17.20", "@types/lodash-es": "^4.17.12", "@vueuse/core": "^9.1.0", "async-validator": "^4.2.5", "dayjs": "^1.11.18", "lodash": "^4.17.21", "lodash-es": "^4.17.21", "lodash-unified": "^1.0.3", "memoize-one": "^6.0.0", "normalize-wheel-es": "^1.2.0" }, "peerDependencies": { "vue": "^3.2.0" } }, "sha512-2wzSj2uubFU1f0t/gHkkE1d09mUgV18fSZX5excw3Ar6hyWcxph4E57U8dgYLDt7HwkKYv1BiqPyBdy0WqWlOA=="],
|
| 282 |
+
|
| 283 |
+
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
| 284 |
+
|
| 285 |
+
"error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="],
|
| 286 |
+
|
| 287 |
+
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
| 288 |
+
|
| 289 |
+
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
| 290 |
+
|
| 291 |
+
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
| 292 |
+
|
| 293 |
+
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
| 294 |
+
|
| 295 |
+
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
| 296 |
+
|
| 297 |
+
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
| 298 |
+
|
| 299 |
+
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
| 300 |
+
|
| 301 |
+
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
|
| 302 |
+
|
| 303 |
+
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
|
| 304 |
+
|
| 305 |
+
"is-what": ["is-what@5.5.0", "", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="],
|
| 306 |
+
|
| 307 |
+
"is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
|
| 308 |
+
|
| 309 |
+
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
| 310 |
+
|
| 311 |
+
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
| 312 |
+
|
| 313 |
+
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
| 314 |
+
|
| 315 |
+
"kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="],
|
| 316 |
+
|
| 317 |
+
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
| 318 |
+
|
| 319 |
+
"lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="],
|
| 320 |
+
|
| 321 |
+
"lodash-unified": ["lodash-unified@1.0.3", "", { "peerDependencies": { "@types/lodash-es": "*", "lodash": "*", "lodash-es": "*" } }, "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ=="],
|
| 322 |
+
|
| 323 |
+
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
| 324 |
+
|
| 325 |
+
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
| 326 |
+
|
| 327 |
+
"memoize-one": ["memoize-one@6.0.0", "", {}, "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="],
|
| 328 |
+
|
| 329 |
+
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
| 330 |
+
|
| 331 |
+
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
| 332 |
+
|
| 333 |
+
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
| 334 |
+
|
| 335 |
+
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
| 336 |
+
|
| 337 |
+
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
| 338 |
+
|
| 339 |
+
"normalize-wheel-es": ["normalize-wheel-es@1.2.0", "", {}, "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw=="],
|
| 340 |
+
|
| 341 |
+
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
| 342 |
+
|
| 343 |
+
"open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
|
| 344 |
+
|
| 345 |
+
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
| 346 |
+
|
| 347 |
+
"perfect-debounce": ["perfect-debounce@2.0.0", "", {}, "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow=="],
|
| 348 |
+
|
| 349 |
+
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
| 350 |
+
|
| 351 |
+
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
| 352 |
+
|
| 353 |
+
"pinia": ["pinia@3.0.4", "", { "dependencies": { "@vue/devtools-api": "^7.7.7" }, "peerDependencies": { "typescript": ">=4.5.0", "vue": "^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw=="],
|
| 354 |
+
|
| 355 |
+
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
| 356 |
+
|
| 357 |
+
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
|
| 358 |
+
|
| 359 |
+
"rollup": ["rollup@4.53.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.3", "@rollup/rollup-android-arm64": "4.53.3", "@rollup/rollup-darwin-arm64": "4.53.3", "@rollup/rollup-darwin-x64": "4.53.3", "@rollup/rollup-freebsd-arm64": "4.53.3", "@rollup/rollup-freebsd-x64": "4.53.3", "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", "@rollup/rollup-linux-arm-musleabihf": "4.53.3", "@rollup/rollup-linux-arm64-gnu": "4.53.3", "@rollup/rollup-linux-arm64-musl": "4.53.3", "@rollup/rollup-linux-loong64-gnu": "4.53.3", "@rollup/rollup-linux-ppc64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-musl": "4.53.3", "@rollup/rollup-linux-s390x-gnu": "4.53.3", "@rollup/rollup-linux-x64-gnu": "4.53.3", "@rollup/rollup-linux-x64-musl": "4.53.3", "@rollup/rollup-openharmony-arm64": "4.53.3", "@rollup/rollup-win32-arm64-msvc": "4.53.3", "@rollup/rollup-win32-ia32-msvc": "4.53.3", "@rollup/rollup-win32-x64-gnu": "4.53.3", "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA=="],
|
| 360 |
+
|
| 361 |
+
"run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
|
| 362 |
+
|
| 363 |
+
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
| 364 |
+
|
| 365 |
+
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
|
| 366 |
+
|
| 367 |
+
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
| 368 |
+
|
| 369 |
+
"speakingurl": ["speakingurl@14.0.1", "", {}, "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="],
|
| 370 |
+
|
| 371 |
+
"superjson": ["superjson@2.2.5", "", { "dependencies": { "copy-anything": "^4" } }, "sha512-zWPTX96LVsA/eVYnqOM2+ofcdPqdS1dAF1LN4TS2/MWuUpfitd9ctTa87wt4xrYnZnkLtS69xpBdSxVBP5Rm6w=="],
|
| 372 |
+
|
| 373 |
+
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
| 374 |
+
|
| 375 |
+
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
| 376 |
+
|
| 377 |
+
"unplugin-utils": ["unplugin-utils@0.3.1", "", { "dependencies": { "pathe": "^2.0.3", "picomatch": "^4.0.3" } }, "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog=="],
|
| 378 |
+
|
| 379 |
+
"update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="],
|
| 380 |
+
|
| 381 |
+
"vite": ["vite@7.2.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w=="],
|
| 382 |
+
|
| 383 |
+
"vite-dev-rpc": ["vite-dev-rpc@1.1.0", "", { "dependencies": { "birpc": "^2.4.0", "vite-hot-client": "^2.1.0" }, "peerDependencies": { "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0" } }, "sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A=="],
|
| 384 |
+
|
| 385 |
+
"vite-hot-client": ["vite-hot-client@2.1.0", "", { "peerDependencies": { "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" } }, "sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ=="],
|
| 386 |
+
|
| 387 |
+
"vite-plugin-inspect": ["vite-plugin-inspect@11.3.3", "", { "dependencies": { "ansis": "^4.1.0", "debug": "^4.4.1", "error-stack-parser-es": "^1.0.5", "ohash": "^2.0.11", "open": "^10.2.0", "perfect-debounce": "^2.0.0", "sirv": "^3.0.1", "unplugin-utils": "^0.3.0", "vite-dev-rpc": "^1.1.0" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0-0" } }, "sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA=="],
|
| 388 |
+
|
| 389 |
+
"vite-plugin-vue-devtools": ["vite-plugin-vue-devtools@8.0.5", "", { "dependencies": { "@vue/devtools-core": "^8.0.5", "@vue/devtools-kit": "^8.0.5", "@vue/devtools-shared": "^8.0.5", "sirv": "^3.0.2", "vite-plugin-inspect": "^11.3.3", "vite-plugin-vue-inspector": "^5.3.2" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0-0" } }, "sha512-p619BlKFOqQXJ6uDWS1vUPQzuJOD6xJTfftj57JXBGoBD/yeQCowR7pnWcr/FEX4/HVkFbreI6w2uuGBmQOh6A=="],
|
| 390 |
+
|
| 391 |
+
"vite-plugin-vue-inspector": ["vite-plugin-vue-inspector@5.3.2", "", { "dependencies": { "@babel/core": "^7.23.0", "@babel/plugin-proposal-decorators": "^7.23.0", "@babel/plugin-syntax-import-attributes": "^7.22.5", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-transform-typescript": "^7.22.15", "@vue/babel-plugin-jsx": "^1.1.5", "@vue/compiler-dom": "^3.3.4", "kolorist": "^1.8.0", "magic-string": "^0.30.4" }, "peerDependencies": { "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" } }, "sha512-YvEKooQcSiBTAs0DoYLfefNja9bLgkFM7NI2b07bE2SruuvX0MEa9cMaxjKVMkeCp5Nz9FRIdcN1rOdFVBeL6Q=="],
|
| 392 |
+
|
| 393 |
+
"vue": ["vue@3.5.24", "", { "dependencies": { "@vue/compiler-dom": "3.5.24", "@vue/compiler-sfc": "3.5.24", "@vue/runtime-dom": "3.5.24", "@vue/server-renderer": "3.5.24", "@vue/shared": "3.5.24" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-uTHDOpVQTMjcGgrqFPSb8iO2m1DUvo+WbGqoXQz8Y1CeBYQ0FXf2z1gLRaBtHjlRz7zZUBHxjVB5VTLzYkvftg=="],
|
| 394 |
+
|
| 395 |
+
"vue-demi": ["vue-demi@0.14.10", "", { "peerDependencies": { "@vue/composition-api": "^1.0.0-rc.1", "vue": "^3.0.0-0 || ^2.6.0" }, "optionalPeers": ["@vue/composition-api"], "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-switch": "bin/vue-demi-switch.js" } }, "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg=="],
|
| 396 |
+
|
| 397 |
+
"wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
|
| 398 |
+
|
| 399 |
+
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
| 400 |
+
|
| 401 |
+
"@vue/devtools-api/@vue/devtools-kit": ["@vue/devtools-kit@7.7.9", "", { "dependencies": { "@vue/devtools-shared": "^7.7.9", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA=="],
|
| 402 |
+
|
| 403 |
+
"@vue/devtools-core/nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
|
| 404 |
+
|
| 405 |
+
"@vue/devtools-api/@vue/devtools-kit/@vue/devtools-shared": ["@vue/devtools-shared@7.7.9", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA=="],
|
| 406 |
+
|
| 407 |
+
"@vue/devtools-api/@vue/devtools-kit/perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
|
| 408 |
+
}
|
| 409 |
+
}
|
frontend/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<link rel="icon" href="/favicon.ico">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Vite App</title>
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div id="app"></div>
|
| 11 |
+
<script type="module" src="/src/main.js"></script>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
frontend/jsconfig.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"paths": {
|
| 4 |
+
"@/*": ["./src/*"]
|
| 5 |
+
}
|
| 6 |
+
},
|
| 7 |
+
"exclude": ["node_modules", "dist"]
|
| 8 |
+
}
|
frontend/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "sorawm",
|
| 3 |
+
"version": "0.0.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"type": "module",
|
| 6 |
+
"engines": {
|
| 7 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 8 |
+
},
|
| 9 |
+
"scripts": {
|
| 10 |
+
"dev": "vite",
|
| 11 |
+
"build": "vite build",
|
| 12 |
+
"preview": "vite preview"
|
| 13 |
+
},
|
| 14 |
+
"dependencies": {
|
| 15 |
+
"axios": "^1.13.2",
|
| 16 |
+
"element-plus": "^2.11.8",
|
| 17 |
+
"pinia": "^3.0.4",
|
| 18 |
+
"vue": "^3.5.22"
|
| 19 |
+
},
|
| 20 |
+
"devDependencies": {
|
| 21 |
+
"@vitejs/plugin-vue": "^6.0.1",
|
| 22 |
+
"vite": "^7.1.11",
|
| 23 |
+
"vite-plugin-vue-devtools": "^8.0.3"
|
| 24 |
+
}
|
| 25 |
+
}
|
frontend/public/favicon.ico
ADDED
|
|
frontend/src/App.vue
ADDED
|
@@ -0,0 +1,640 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script setup>
|
| 2 |
+
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
| 3 |
+
import { ElMessage } from 'element-plus'
|
| 4 |
+
import { UploadFilled, VideoPlay, Download, RefreshRight, Plus, Loading, Check, Warning, Setting } from '@element-plus/icons-vue'
|
| 5 |
+
import dayjs from 'dayjs'
|
| 6 |
+
import axios from 'axios'
|
| 7 |
+
|
| 8 |
+
// --- Configuration ---
|
| 9 |
+
const API_BASE_URL = '/api/v1'
|
| 10 |
+
const POLL_INTERVAL = 2000
|
| 11 |
+
|
| 12 |
+
// --- State ---
|
| 13 |
+
const isUploading = ref(false)
|
| 14 |
+
const showUploader = ref(true)
|
| 15 |
+
const timer = ref(null)
|
| 16 |
+
const uploadRef = ref(null) // 用于引用 upload 组件以清理文件
|
| 17 |
+
const selectedModel = ref('lama') // 新增:当前选择的模型
|
| 18 |
+
|
| 19 |
+
// Data State aligned with Backend Models
|
| 20 |
+
const queueSummary = ref({
|
| 21 |
+
is_busy: false,
|
| 22 |
+
queue_length: 0,
|
| 23 |
+
total_active: 0
|
| 24 |
+
})
|
| 25 |
+
|
| 26 |
+
const currentTaskId = ref(null)
|
| 27 |
+
const currentTaskResult = ref(null)
|
| 28 |
+
const waitingQueue = ref([])
|
| 29 |
+
|
| 30 |
+
// --- API Interactions ---
|
| 31 |
+
|
| 32 |
+
// 1. 获取队列状态
|
| 33 |
+
const fetchQueueStatus = async () => {
|
| 34 |
+
try {
|
| 35 |
+
const { data } = await axios.get(`${API_BASE_URL}/get_queue_status`)
|
| 36 |
+
queueSummary.value = data.summary
|
| 37 |
+
waitingQueue.value = data.waiting_queue
|
| 38 |
+
const newCurrentTaskId = data.current_task_id
|
| 39 |
+
currentTaskId.value = newCurrentTaskId
|
| 40 |
+
|
| 41 |
+
if (newCurrentTaskId) {
|
| 42 |
+
fetchCurrentTaskResult(newCurrentTaskId)
|
| 43 |
+
} else {
|
| 44 |
+
currentTaskResult.value = null
|
| 45 |
+
}
|
| 46 |
+
} catch (error) {
|
| 47 |
+
console.error('Failed to fetch queue status:', error)
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// 2. 获取特定任务结果
|
| 52 |
+
const fetchCurrentTaskResult = async (taskId) => {
|
| 53 |
+
try {
|
| 54 |
+
const { data } = await axios.get(`${API_BASE_URL}/get_results`, {
|
| 55 |
+
params: { remove_task_id: taskId }
|
| 56 |
+
})
|
| 57 |
+
currentTaskResult.value = {
|
| 58 |
+
id: taskId,
|
| 59 |
+
status: data.status,
|
| 60 |
+
percentage: data.percentage,
|
| 61 |
+
video_path: 'Processing...',
|
| 62 |
+
created_at: null,
|
| 63 |
+
download_url: data.download_url
|
| 64 |
+
}
|
| 65 |
+
} catch (error) {
|
| 66 |
+
console.error('Failed to fetch task result:', error)
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// 3. 提交任务
|
| 71 |
+
const handleUploadChange = async (file) => {
|
| 72 |
+
isUploading.value = true
|
| 73 |
+
const formData = new FormData()
|
| 74 |
+
formData.append('video', file.raw)
|
| 75 |
+
|
| 76 |
+
try {
|
| 77 |
+
// 修改:使用 selectedModel 的值
|
| 78 |
+
await axios.post(`${API_BASE_URL}/submit_remove_task`, formData, {
|
| 79 |
+
params: { cleaner_type: selectedModel.value },
|
| 80 |
+
headers: { 'Content-Type': 'multipart/form-data' }
|
| 81 |
+
})
|
| 82 |
+
|
| 83 |
+
ElMessage.success({ message: `Task submitted: ${file.name}`, plain: true })
|
| 84 |
+
|
| 85 |
+
// 修改:不再隐藏上传框,而是刷新队列并清理当前文件,允许继续上传
|
| 86 |
+
// showUploader.value = false
|
| 87 |
+
if (uploadRef.value) {
|
| 88 |
+
uploadRef.value.clearFiles()
|
| 89 |
+
}
|
| 90 |
+
fetchQueueStatus()
|
| 91 |
+
} catch (error) {
|
| 92 |
+
ElMessage.error({ message: `Upload failed: ${error.message}`, plain: true })
|
| 93 |
+
} finally {
|
| 94 |
+
isUploading.value = false
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
// --- Computed Logic ---
|
| 99 |
+
const tableData = computed(() => {
|
| 100 |
+
const list = []
|
| 101 |
+
if (currentTaskId.value && currentTaskResult.value) {
|
| 102 |
+
list.push({
|
| 103 |
+
id: currentTaskId.value,
|
| 104 |
+
status: currentTaskResult.value.status,
|
| 105 |
+
percentage: currentTaskResult.value.percentage,
|
| 106 |
+
video_path: currentTaskResult.value.video_path,
|
| 107 |
+
created_at: null,
|
| 108 |
+
is_current: true
|
| 109 |
+
})
|
| 110 |
+
}
|
| 111 |
+
if (waitingQueue.value && waitingQueue.value.length > 0) {
|
| 112 |
+
waitingQueue.value.forEach(task => {
|
| 113 |
+
list.push({
|
| 114 |
+
id: task.id,
|
| 115 |
+
status: task.status,
|
| 116 |
+
percentage: task.percentage,
|
| 117 |
+
video_path: task.video_path,
|
| 118 |
+
created_at: task.created_at,
|
| 119 |
+
is_current: false
|
| 120 |
+
})
|
| 121 |
+
})
|
| 122 |
+
}
|
| 123 |
+
return list
|
| 124 |
+
})
|
| 125 |
+
|
| 126 |
+
const stats = computed(() => {
|
| 127 |
+
return {
|
| 128 |
+
totalActive: queueSummary.value.total_active,
|
| 129 |
+
queueLength: queueSummary.value.queue_length,
|
| 130 |
+
isBusy: queueSummary.value.is_busy ? 1 : 0
|
| 131 |
+
}
|
| 132 |
+
})
|
| 133 |
+
|
| 134 |
+
// --- Helpers ---
|
| 135 |
+
const formatDate = (dateStr) => {
|
| 136 |
+
if (!dateStr) return '-'
|
| 137 |
+
return dayjs(dateStr).format('MMM D, HH:mm')
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
const getDownloadUrl = (taskId) => {
|
| 141 |
+
return `${API_BASE_URL}/download/${taskId}`
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
const getStatusConfig = (status) => {
|
| 145 |
+
const map = {
|
| 146 |
+
'FINISHED': { type: 'success', label: 'Ready', icon: Check, bg: 'pill-green' },
|
| 147 |
+
'PROCESSING': { type: 'primary', label: 'Processing', icon: Loading, bg: 'pill-blue' },
|
| 148 |
+
'QUEUED': { type: 'info', label: 'Queued', icon: null, bg: 'pill-gray' },
|
| 149 |
+
'UPLOADING': { type: 'warning', label: 'Uploading', icon: Loading, bg: 'pill-gray' },
|
| 150 |
+
'ERROR': { type: 'danger', label: 'Failed', icon: Warning, bg: 'pill-red' }
|
| 151 |
+
}
|
| 152 |
+
return map[status] || { type: 'info', label: status, bg: 'pill-gray' }
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
// --- Lifecycle ---
|
| 156 |
+
onMounted(() => {
|
| 157 |
+
fetchQueueStatus()
|
| 158 |
+
timer.value = setInterval(fetchQueueStatus, POLL_INTERVAL)
|
| 159 |
+
})
|
| 160 |
+
|
| 161 |
+
onUnmounted(() => {
|
| 162 |
+
if (timer.value) clearInterval(timer.value)
|
| 163 |
+
})
|
| 164 |
+
</script>
|
| 165 |
+
|
| 166 |
+
<template>
|
| 167 |
+
<div class="oa-page">
|
| 168 |
+
<header class="oa-header">
|
| 169 |
+
<div class="oa-header-inner">
|
| 170 |
+
<div class="oa-brand">
|
| 171 |
+
<div class="oa-dot" :class="{ 'oa-dot-busy': queueSummary.is_busy }" />
|
| 172 |
+
<span class="oa-title">Video Tasks</span>
|
| 173 |
+
</div>
|
| 174 |
+
<div class="oa-actions">
|
| 175 |
+
<el-button
|
| 176 |
+
class="oa-primary-btn"
|
| 177 |
+
:icon="Plus"
|
| 178 |
+
@click="showUploader = !showUploader"
|
| 179 |
+
>
|
| 180 |
+
{{ showUploader ? 'Hide upload' : 'New task' }}
|
| 181 |
+
</el-button>
|
| 182 |
+
</div>
|
| 183 |
+
</div>
|
| 184 |
+
</header>
|
| 185 |
+
|
| 186 |
+
<main class="oa-container">
|
| 187 |
+
<section class="oa-stats">
|
| 188 |
+
<div class="oa-stat-card">
|
| 189 |
+
<div class="oa-stat-label">System Status</div>
|
| 190 |
+
<div class="oa-stat-value">
|
| 191 |
+
{{ queueSummary.is_busy ? 'Busy' : 'Idle' }}
|
| 192 |
+
</div>
|
| 193 |
+
</div>
|
| 194 |
+
|
| 195 |
+
<div class="oa-stat-card">
|
| 196 |
+
<div class="oa-stat-label">Queue Length</div>
|
| 197 |
+
<div class="oa-stat-value">{{ stats.queueLength }}</div>
|
| 198 |
+
</div>
|
| 199 |
+
|
| 200 |
+
<div class="oa-stat-card">
|
| 201 |
+
<div class="oa-stat-label">Total Active</div>
|
| 202 |
+
<div class="oa-stat-value">{{ stats.totalActive }}</div>
|
| 203 |
+
</div>
|
| 204 |
+
</section>
|
| 205 |
+
|
| 206 |
+
<transition name="el-fade-in-linear">
|
| 207 |
+
<section v-if="showUploader" class="oa-upload-section">
|
| 208 |
+
<div class="oa-controls">
|
| 209 |
+
<span class="oa-control-label">Model:</span>
|
| 210 |
+
<el-radio-group v-model="selectedModel" size="small" class="oa-radio-group">
|
| 211 |
+
<el-radio-button label="lama">Lama (Fast)</el-radio-button>
|
| 212 |
+
<el-radio-button label="e2fgvi_hq">E2FGVI (High Quality)</el-radio-button>
|
| 213 |
+
</el-radio-group>
|
| 214 |
+
</div>
|
| 215 |
+
|
| 216 |
+
<el-upload
|
| 217 |
+
ref="uploadRef"
|
| 218 |
+
class="oa-uploader"
|
| 219 |
+
drag
|
| 220 |
+
action="#"
|
| 221 |
+
:auto-upload="false"
|
| 222 |
+
:on-change="handleUploadChange"
|
| 223 |
+
:show-file-list="false"
|
| 224 |
+
:disabled="isUploading"
|
| 225 |
+
>
|
| 226 |
+
<div class="oa-upload-inner">
|
| 227 |
+
<el-icon class="oa-upload-icon" v-if="!isUploading"><UploadFilled /></el-icon>
|
| 228 |
+
<el-icon class="oa-upload-icon is-loading" v-else><Loading /></el-icon>
|
| 229 |
+
|
| 230 |
+
<div class="oa-upload-text">
|
| 231 |
+
<span v-if="!isUploading">
|
| 232 |
+
<span class="oa-upload-strong">Click to upload</span>
|
| 233 |
+
or drag video
|
| 234 |
+
</span>
|
| 235 |
+
<span v-else>Uploading to server...</span>
|
| 236 |
+
</div>
|
| 237 |
+
<div class="oa-upload-hint">MP4, MOV, AVI · Max 500MB</div>
|
| 238 |
+
</div>
|
| 239 |
+
</el-upload>
|
| 240 |
+
</section>
|
| 241 |
+
</transition>
|
| 242 |
+
|
| 243 |
+
<section class="oa-table">
|
| 244 |
+
<div class="oa-table-head">
|
| 245 |
+
<h3 class="oa-section-title">Current & Queue</h3>
|
| 246 |
+
<el-button
|
| 247 |
+
:icon="RefreshRight"
|
| 248 |
+
text
|
| 249 |
+
size="small"
|
| 250 |
+
class="oa-refresh"
|
| 251 |
+
@click="fetchQueueStatus"
|
| 252 |
+
>
|
| 253 |
+
Refresh
|
| 254 |
+
</el-button>
|
| 255 |
+
</div>
|
| 256 |
+
|
| 257 |
+
<el-table
|
| 258 |
+
:data="tableData"
|
| 259 |
+
class="oa-el-table"
|
| 260 |
+
:row-style="{ height: '68px' }"
|
| 261 |
+
empty-text="No active tasks"
|
| 262 |
+
>
|
| 263 |
+
<el-table-column label="Video Info" min-width="320">
|
| 264 |
+
<template #default="{ row }">
|
| 265 |
+
<div class="oa-file-cell">
|
| 266 |
+
<div class="oa-file-icon">
|
| 267 |
+
<el-icon><VideoPlay /></el-icon>
|
| 268 |
+
</div>
|
| 269 |
+
<div class="oa-file-info">
|
| 270 |
+
<div class="oa-file-name">{{ row.video_path || `Task: ${row.id.substring(0,8)}...` }}</div>
|
| 271 |
+
<div class="oa-file-meta">{{ row.id }}</div>
|
| 272 |
+
</div>
|
| 273 |
+
</div>
|
| 274 |
+
</template>
|
| 275 |
+
</el-table-column>
|
| 276 |
+
|
| 277 |
+
<el-table-column label="Status" width="150">
|
| 278 |
+
<template #default="{ row }">
|
| 279 |
+
<div class="oa-status-pill" :class="getStatusConfig(row.status).bg">
|
| 280 |
+
<span class="oa-status-dot"></span>
|
| 281 |
+
{{ getStatusConfig(row.status).label }}
|
| 282 |
+
</div>
|
| 283 |
+
</template>
|
| 284 |
+
</el-table-column>
|
| 285 |
+
|
| 286 |
+
<el-table-column label="Progress" width="220">
|
| 287 |
+
<template #default="{ row }">
|
| 288 |
+
<div class="oa-progress">
|
| 289 |
+
<el-progress
|
| 290 |
+
:percentage="row.percentage"
|
| 291 |
+
:show-text="false"
|
| 292 |
+
:stroke-width="4"
|
| 293 |
+
:color="row.status === 'ERROR' ? '#ef4444' : '#10a37f'"
|
| 294 |
+
:indeterminate="row.status === 'PROCESSING' && row.percentage === 0"
|
| 295 |
+
class="oa-progress-bar"
|
| 296 |
+
/>
|
| 297 |
+
<span class="oa-progress-text">
|
| 298 |
+
{{ row.percentage }}%
|
| 299 |
+
</span>
|
| 300 |
+
</div>
|
| 301 |
+
</template>
|
| 302 |
+
</el-table-column>
|
| 303 |
+
|
| 304 |
+
<el-table-column label="Created At" width="170" align="right">
|
| 305 |
+
<template #default="{ row }">
|
| 306 |
+
<span class="oa-date">{{ formatDate(row.created_at) }}</span>
|
| 307 |
+
</template>
|
| 308 |
+
</el-table-column>
|
| 309 |
+
|
| 310 |
+
<el-table-column width="64" align="center">
|
| 311 |
+
<template #default="{ row }">
|
| 312 |
+
<a
|
| 313 |
+
v-if="row.status === 'FINISHED'"
|
| 314 |
+
:href="getDownloadUrl(row.id)"
|
| 315 |
+
target="_blank"
|
| 316 |
+
class="oa-download-link"
|
| 317 |
+
>
|
| 318 |
+
<el-button link class="oa-download">
|
| 319 |
+
<el-icon><Download /></el-icon>
|
| 320 |
+
</el-button>
|
| 321 |
+
</a>
|
| 322 |
+
</template>
|
| 323 |
+
</el-table-column>
|
| 324 |
+
</el-table>
|
| 325 |
+
</section>
|
| 326 |
+
</main>
|
| 327 |
+
</div>
|
| 328 |
+
</template>
|
| 329 |
+
|
| 330 |
+
<style scoped>
|
| 331 |
+
/* ---------------------------
|
| 332 |
+
OpenAI-like Design Tokens
|
| 333 |
+
---------------------------- */
|
| 334 |
+
:root {
|
| 335 |
+
--oa-bg: #ffffff;
|
| 336 |
+
--oa-surface: #f7f7f8;
|
| 337 |
+
--oa-surface-2: #fbfbfc;
|
| 338 |
+
--oa-border: #e6e6e9;
|
| 339 |
+
--oa-text: #0b0c0e;
|
| 340 |
+
--oa-text-2: #5f6368;
|
| 341 |
+
--oa-text-3: #8a8f98;
|
| 342 |
+
--oa-green: #10a37f;
|
| 343 |
+
--oa-black: #0b0c0e;
|
| 344 |
+
--oa-radius-lg: 14px;
|
| 345 |
+
--oa-radius-md: 10px;
|
| 346 |
+
--oa-shadow-sm: 0 1px 2px rgba(0,0,0,0.04);
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
/* Page + container */
|
| 350 |
+
.oa-page {
|
| 351 |
+
min-height: 100vh;
|
| 352 |
+
background: var(--oa-bg);
|
| 353 |
+
color: var(--oa-text);
|
| 354 |
+
font-family: system-ui, -apple-system, Segoe UI, Roboto, Inter, sans-serif;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
.oa-container {
|
| 358 |
+
max-width: 1040px;
|
| 359 |
+
margin: 0 auto;
|
| 360 |
+
padding: 28px 24px 56px;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
/* Header */
|
| 364 |
+
.oa-header {
|
| 365 |
+
position: sticky;
|
| 366 |
+
top: 0;
|
| 367 |
+
z-index: 5;
|
| 368 |
+
background: rgba(255,255,255,0.9);
|
| 369 |
+
backdrop-filter: blur(8px);
|
| 370 |
+
border-bottom: 1px solid var(--oa-border);
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
.oa-header-inner {
|
| 374 |
+
max-width: 1040px;
|
| 375 |
+
margin: 0 auto;
|
| 376 |
+
padding: 14px 24px;
|
| 377 |
+
display: flex;
|
| 378 |
+
align-items: center;
|
| 379 |
+
justify-content: space-between;
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
.oa-brand {
|
| 383 |
+
display: flex;
|
| 384 |
+
align-items: center;
|
| 385 |
+
gap: 10px;
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
.oa-dot {
|
| 389 |
+
width: 10px;
|
| 390 |
+
height: 10px;
|
| 391 |
+
background: #ccc;
|
| 392 |
+
border-radius: 999px;
|
| 393 |
+
transition: background 0.3s ease;
|
| 394 |
+
}
|
| 395 |
+
.oa-dot-busy {
|
| 396 |
+
background: var(--oa-green);
|
| 397 |
+
box-shadow: 0 0 8px rgba(16, 163, 127, 0.4);
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
.oa-title {
|
| 401 |
+
font-size: 15px;
|
| 402 |
+
font-weight: 600;
|
| 403 |
+
letter-spacing: 0.1px;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
/* Primary button */
|
| 407 |
+
.oa-primary-btn {
|
| 408 |
+
background: var(--oa-black) !important;
|
| 409 |
+
color: #fff !important;
|
| 410 |
+
border: none !important;
|
| 411 |
+
border-radius: 999px !important;
|
| 412 |
+
padding: 8px 14px !important;
|
| 413 |
+
font-weight: 600;
|
| 414 |
+
box-shadow: var(--oa-shadow-sm);
|
| 415 |
+
}
|
| 416 |
+
.oa-primary-btn:hover { background: #000 !important; }
|
| 417 |
+
|
| 418 |
+
/* Stats */
|
| 419 |
+
.oa-stats {
|
| 420 |
+
display: grid;
|
| 421 |
+
grid-template-columns: repeat(3, 1fr);
|
| 422 |
+
gap: 14px;
|
| 423 |
+
margin-top: 20px;
|
| 424 |
+
margin-bottom: 22px;
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
.oa-stat-card {
|
| 428 |
+
background: var(--oa-surface);
|
| 429 |
+
border: 1px solid var(--oa-border);
|
| 430 |
+
border-radius: var(--oa-radius-lg);
|
| 431 |
+
padding: 18px;
|
| 432 |
+
box-shadow: var(--oa-shadow-sm);
|
| 433 |
+
display: flex;
|
| 434 |
+
flex-direction: column;
|
| 435 |
+
gap: 8px;
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
.oa-stat-label {
|
| 439 |
+
font-size: 12px;
|
| 440 |
+
color: var(--oa-text-2);
|
| 441 |
+
font-weight: 600;
|
| 442 |
+
text-transform: uppercase;
|
| 443 |
+
letter-spacing: 0.06em;
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
.oa-stat-value {
|
| 447 |
+
font-size: 26px;
|
| 448 |
+
font-weight: 700;
|
| 449 |
+
letter-spacing: -0.02em;
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
/* Upload & Controls */
|
| 453 |
+
.oa-upload-section {
|
| 454 |
+
margin-top: 8px;
|
| 455 |
+
margin-bottom: 26px;
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
.oa-controls {
|
| 459 |
+
display: flex;
|
| 460 |
+
align-items: center;
|
| 461 |
+
gap: 12px;
|
| 462 |
+
margin-bottom: 12px;
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
.oa-control-label {
|
| 466 |
+
font-size: 13px;
|
| 467 |
+
font-weight: 600;
|
| 468 |
+
color: var(--oa-text-2);
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
/* Customizing Radio Button to look cleaner */
|
| 472 |
+
.oa-radio-group :deep(.el-radio-button__inner) {
|
| 473 |
+
border-radius: 6px !important;
|
| 474 |
+
border: 1px solid var(--oa-border);
|
| 475 |
+
box-shadow: none !important;
|
| 476 |
+
margin-right: 8px;
|
| 477 |
+
padding: 8px 16px;
|
| 478 |
+
font-weight: 500;
|
| 479 |
+
background: var(--oa-surface);
|
| 480 |
+
color: var(--oa-text);
|
| 481 |
+
}
|
| 482 |
+
.oa-radio-group :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
|
| 483 |
+
background-color: var(--oa-black);
|
| 484 |
+
border-color: var(--oa-black);
|
| 485 |
+
color: #fff;
|
| 486 |
+
box-shadow: none;
|
| 487 |
+
}
|
| 488 |
+
.oa-radio-group :deep(.el-radio-button:first-child .el-radio-button__inner) {
|
| 489 |
+
border-left: 1px solid var(--oa-border);
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
.oa-uploader :deep(.el-upload-dragger) {
|
| 493 |
+
height: 128px;
|
| 494 |
+
border: 1.5px dashed var(--oa-border);
|
| 495 |
+
background: var(--oa-surface-2);
|
| 496 |
+
border-radius: var(--oa-radius-lg);
|
| 497 |
+
transition: all 0.2s ease;
|
| 498 |
+
}
|
| 499 |
+
.oa-uploader :deep(.el-upload-dragger:hover) {
|
| 500 |
+
border-color: var(--oa-green);
|
| 501 |
+
background: #f3fbf8;
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
.oa-upload-inner {
|
| 505 |
+
height: 100%;
|
| 506 |
+
display: grid;
|
| 507 |
+
place-content: center;
|
| 508 |
+
gap: 6px;
|
| 509 |
+
text-align: center;
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
.oa-upload-icon { font-size: 22px; color: var(--oa-text-3); }
|
| 513 |
+
.oa-upload-text { font-size: 14px; color: var(--oa-text-2); }
|
| 514 |
+
.oa-upload-strong { color: var(--oa-green); font-weight: 700; }
|
| 515 |
+
.oa-upload-hint { font-size: 12px; color: var(--oa-text-3); }
|
| 516 |
+
.is-loading { animation: rotating 2s linear infinite; }
|
| 517 |
+
|
| 518 |
+
/* Table Section */
|
| 519 |
+
.oa-table {
|
| 520 |
+
background: var(--oa-bg);
|
| 521 |
+
border: 1px solid var(--oa-border);
|
| 522 |
+
border-radius: var(--oa-radius-lg);
|
| 523 |
+
padding: 14px 12px 6px;
|
| 524 |
+
box-shadow: var(--oa-shadow-sm);
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
.oa-table-head {
|
| 528 |
+
display: flex;
|
| 529 |
+
align-items: center;
|
| 530 |
+
justify-content: space-between;
|
| 531 |
+
padding: 6px 8px 12px;
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
.oa-section-title {
|
| 535 |
+
margin: 0;
|
| 536 |
+
font-size: 16px;
|
| 537 |
+
font-weight: 700;
|
| 538 |
+
letter-spacing: -0.01em;
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
.oa-refresh { color: var(--oa-text-2) !important; }
|
| 542 |
+
|
| 543 |
+
/* Element Plus table overrides */
|
| 544 |
+
.oa-el-table {
|
| 545 |
+
--el-table-border-color: transparent;
|
| 546 |
+
--el-table-header-bg-color: transparent;
|
| 547 |
+
--el-table-row-hover-bg-color: #fafafa;
|
| 548 |
+
}
|
| 549 |
+
.oa-el-table :deep(th.el-table__cell) {
|
| 550 |
+
font-size: 11px;
|
| 551 |
+
text-transform: uppercase;
|
| 552 |
+
letter-spacing: 0.08em;
|
| 553 |
+
color: var(--oa-text-3);
|
| 554 |
+
font-weight: 700;
|
| 555 |
+
border-bottom: 1px solid var(--oa-border) !important;
|
| 556 |
+
padding: 10px 8px 12px;
|
| 557 |
+
}
|
| 558 |
+
.oa-el-table :deep(td.el-table__cell) {
|
| 559 |
+
border-bottom: 1px solid var(--oa-border);
|
| 560 |
+
padding: 12px 8px;
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
/* File cell */
|
| 564 |
+
.oa-file-cell {
|
| 565 |
+
display: flex;
|
| 566 |
+
align-items: center;
|
| 567 |
+
gap: 12px;
|
| 568 |
+
}
|
| 569 |
+
.oa-file-icon {
|
| 570 |
+
width: 40px;
|
| 571 |
+
height: 40px;
|
| 572 |
+
border-radius: 10px;
|
| 573 |
+
background: var(--oa-surface);
|
| 574 |
+
border: 1px solid var(--oa-border);
|
| 575 |
+
display: grid;
|
| 576 |
+
place-content: center;
|
| 577 |
+
color: var(--oa-text);
|
| 578 |
+
}
|
| 579 |
+
.oa-file-name { font-size: 14px; font-weight: 600; }
|
| 580 |
+
.oa-file-meta { font-size: 12px; color: var(--oa-text-2); }
|
| 581 |
+
|
| 582 |
+
/* Status pills */
|
| 583 |
+
.oa-status-pill {
|
| 584 |
+
display: inline-flex;
|
| 585 |
+
align-items: center;
|
| 586 |
+
gap: 8px;
|
| 587 |
+
padding: 5px 10px;
|
| 588 |
+
border-radius: 999px;
|
| 589 |
+
font-size: 12px;
|
| 590 |
+
font-weight: 700;
|
| 591 |
+
letter-spacing: 0.02em;
|
| 592 |
+
}
|
| 593 |
+
.oa-status-dot {
|
| 594 |
+
width: 6px;
|
| 595 |
+
height: 6px;
|
| 596 |
+
border-radius: 50%;
|
| 597 |
+
background: currentColor;
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
/* Softer OpenAI-like pastels */
|
| 601 |
+
.pill-green { background: #e9f9f3; color: #0f7a5a; }
|
| 602 |
+
.pill-blue { background: #eef3ff; color: #2a5bd7; }
|
| 603 |
+
.pill-gray { background: #f1f2f4; color: #5f6368; }
|
| 604 |
+
.pill-red { background: #fdecec; color: #b42318; }
|
| 605 |
+
|
| 606 |
+
/* Progress */
|
| 607 |
+
.oa-progress {
|
| 608 |
+
display: flex;
|
| 609 |
+
align-items: center;
|
| 610 |
+
gap: 10px;
|
| 611 |
+
}
|
| 612 |
+
.oa-progress-bar { flex: 1; }
|
| 613 |
+
.oa-progress-text {
|
| 614 |
+
font-size: 12px;
|
| 615 |
+
color: var(--oa-text-2);
|
| 616 |
+
width: 34px;
|
| 617 |
+
text-align: right;
|
| 618 |
+
font-variant-numeric: tabular-nums;
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
/* Date + download */
|
| 622 |
+
.oa-date {
|
| 623 |
+
font-size: 13px;
|
| 624 |
+
color: var(--oa-text-2);
|
| 625 |
+
font-variant-numeric: tabular-nums;
|
| 626 |
+
}
|
| 627 |
+
.oa-download-link { text-decoration: none; }
|
| 628 |
+
.oa-download { color: var(--oa-text-2) !important; }
|
| 629 |
+
.oa-download:hover { color: var(--oa-green) !important; }
|
| 630 |
+
|
| 631 |
+
@keyframes rotating {
|
| 632 |
+
from { transform: rotate(0deg); }
|
| 633 |
+
to { transform: rotate(360deg); }
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
/* Responsive */
|
| 637 |
+
@media (max-width: 900px) {
|
| 638 |
+
.oa-stats { grid-template-columns: 1fr; }
|
| 639 |
+
}
|
| 640 |
+
</style>
|
frontend/src/assets/base.css
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* color palette from <https://github.com/vuejs/theme> */
|
| 2 |
+
:root {
|
| 3 |
+
--vt-c-white: #ffffff;
|
| 4 |
+
--vt-c-white-soft: #f8f8f8;
|
| 5 |
+
--vt-c-white-mute: #f2f2f2;
|
| 6 |
+
|
| 7 |
+
--vt-c-black: #181818;
|
| 8 |
+
--vt-c-black-soft: #222222;
|
| 9 |
+
--vt-c-black-mute: #282828;
|
| 10 |
+
|
| 11 |
+
--vt-c-indigo: #2c3e50;
|
| 12 |
+
|
| 13 |
+
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
| 14 |
+
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
| 15 |
+
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
| 16 |
+
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
| 17 |
+
|
| 18 |
+
--vt-c-text-light-1: var(--vt-c-indigo);
|
| 19 |
+
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
| 20 |
+
--vt-c-text-dark-1: var(--vt-c-white);
|
| 21 |
+
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
/* semantic color variables for this project */
|
| 25 |
+
:root {
|
| 26 |
+
--color-background: var(--vt-c-white);
|
| 27 |
+
--color-background-soft: var(--vt-c-white-soft);
|
| 28 |
+
--color-background-mute: var(--vt-c-white-mute);
|
| 29 |
+
|
| 30 |
+
--color-border: var(--vt-c-divider-light-2);
|
| 31 |
+
--color-border-hover: var(--vt-c-divider-light-1);
|
| 32 |
+
|
| 33 |
+
--color-heading: var(--vt-c-text-light-1);
|
| 34 |
+
--color-text: var(--vt-c-text-light-1);
|
| 35 |
+
|
| 36 |
+
--section-gap: 160px;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
@media (prefers-color-scheme: dark) {
|
| 40 |
+
:root {
|
| 41 |
+
--color-background: var(--vt-c-black);
|
| 42 |
+
--color-background-soft: var(--vt-c-black-soft);
|
| 43 |
+
--color-background-mute: var(--vt-c-black-mute);
|
| 44 |
+
|
| 45 |
+
--color-border: var(--vt-c-divider-dark-2);
|
| 46 |
+
--color-border-hover: var(--vt-c-divider-dark-1);
|
| 47 |
+
|
| 48 |
+
--color-heading: var(--vt-c-text-dark-1);
|
| 49 |
+
--color-text: var(--vt-c-text-dark-2);
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
*,
|
| 54 |
+
*::before,
|
| 55 |
+
*::after {
|
| 56 |
+
box-sizing: border-box;
|
| 57 |
+
margin: 0;
|
| 58 |
+
font-weight: normal;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
body {
|
| 62 |
+
min-height: 100vh;
|
| 63 |
+
color: var(--color-text);
|
| 64 |
+
background: var(--color-background);
|
| 65 |
+
transition:
|
| 66 |
+
color 0.5s,
|
| 67 |
+
background-color 0.5s;
|
| 68 |
+
line-height: 1.6;
|
| 69 |
+
font-family:
|
| 70 |
+
Inter,
|
| 71 |
+
-apple-system,
|
| 72 |
+
BlinkMacSystemFont,
|
| 73 |
+
'Segoe UI',
|
| 74 |
+
Roboto,
|
| 75 |
+
Oxygen,
|
| 76 |
+
Ubuntu,
|
| 77 |
+
Cantarell,
|
| 78 |
+
'Fira Sans',
|
| 79 |
+
'Droid Sans',
|
| 80 |
+
'Helvetica Neue',
|
| 81 |
+
sans-serif;
|
| 82 |
+
font-size: 15px;
|
| 83 |
+
text-rendering: optimizeLegibility;
|
| 84 |
+
-webkit-font-smoothing: antialiased;
|
| 85 |
+
-moz-osx-font-smoothing: grayscale;
|
| 86 |
+
}
|
frontend/src/assets/logo.svg
ADDED
|
|
frontend/src/assets/main.css
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import './base.css';
|
| 2 |
+
|
| 3 |
+
#app {
|
| 4 |
+
max-width: 1280px;
|
| 5 |
+
margin: 0 auto;
|
| 6 |
+
padding: 2rem;
|
| 7 |
+
font-weight: normal;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
a,
|
| 11 |
+
.green {
|
| 12 |
+
text-decoration: none;
|
| 13 |
+
color: hsla(160, 100%, 37%, 1);
|
| 14 |
+
transition: 0.4s;
|
| 15 |
+
padding: 3px;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
@media (hover: hover) {
|
| 19 |
+
a:hover {
|
| 20 |
+
background-color: hsla(160, 100%, 37%, 0.2);
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
@media (min-width: 1024px) {
|
| 25 |
+
body {
|
| 26 |
+
display: flex;
|
| 27 |
+
place-items: center;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
#app {
|
| 31 |
+
display: grid;
|
| 32 |
+
grid-template-columns: 1fr 1fr;
|
| 33 |
+
padding: 0 2rem;
|
| 34 |
+
}
|
| 35 |
+
}
|
frontend/src/components/HelloWorld.vue
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script setup>
|
| 2 |
+
defineProps({
|
| 3 |
+
msg: {
|
| 4 |
+
type: String,
|
| 5 |
+
required: true,
|
| 6 |
+
},
|
| 7 |
+
})
|
| 8 |
+
</script>
|
| 9 |
+
|
| 10 |
+
<template>
|
| 11 |
+
<div class="greetings">
|
| 12 |
+
<h1 class="green">{{ msg }}</h1>
|
| 13 |
+
<h3>
|
| 14 |
+
You’ve successfully created a project with
|
| 15 |
+
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
|
| 16 |
+
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
|
| 17 |
+
</h3>
|
| 18 |
+
</div>
|
| 19 |
+
</template>
|
| 20 |
+
|
| 21 |
+
<style scoped>
|
| 22 |
+
h1 {
|
| 23 |
+
font-weight: 500;
|
| 24 |
+
font-size: 2.6rem;
|
| 25 |
+
position: relative;
|
| 26 |
+
top: -10px;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
h3 {
|
| 30 |
+
font-size: 1.2rem;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
.greetings h1,
|
| 34 |
+
.greetings h3 {
|
| 35 |
+
text-align: center;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
@media (min-width: 1024px) {
|
| 39 |
+
.greetings h1,
|
| 40 |
+
.greetings h3 {
|
| 41 |
+
text-align: left;
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
</style>
|
frontend/src/components/TheWelcome.vue
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script setup>
|
| 2 |
+
import WelcomeItem from './WelcomeItem.vue'
|
| 3 |
+
import DocumentationIcon from './icons/IconDocumentation.vue'
|
| 4 |
+
import ToolingIcon from './icons/IconTooling.vue'
|
| 5 |
+
import EcosystemIcon from './icons/IconEcosystem.vue'
|
| 6 |
+
import CommunityIcon from './icons/IconCommunity.vue'
|
| 7 |
+
import SupportIcon from './icons/IconSupport.vue'
|
| 8 |
+
|
| 9 |
+
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
|
| 10 |
+
</script>
|
| 11 |
+
|
| 12 |
+
<template>
|
| 13 |
+
<WelcomeItem>
|
| 14 |
+
<template #icon>
|
| 15 |
+
<DocumentationIcon />
|
| 16 |
+
</template>
|
| 17 |
+
<template #heading>Documentation</template>
|
| 18 |
+
|
| 19 |
+
Vue’s
|
| 20 |
+
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
| 21 |
+
provides you with all information you need to get started.
|
| 22 |
+
</WelcomeItem>
|
| 23 |
+
|
| 24 |
+
<WelcomeItem>
|
| 25 |
+
<template #icon>
|
| 26 |
+
<ToolingIcon />
|
| 27 |
+
</template>
|
| 28 |
+
<template #heading>Tooling</template>
|
| 29 |
+
|
| 30 |
+
This project is served and bundled with
|
| 31 |
+
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
| 32 |
+
recommended IDE setup is
|
| 33 |
+
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
|
| 34 |
+
+
|
| 35 |
+
<a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener"
|
| 36 |
+
>Vue - Official</a
|
| 37 |
+
>. If you need to test your components and web pages, check out
|
| 38 |
+
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
|
| 39 |
+
and
|
| 40 |
+
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
|
| 41 |
+
/
|
| 42 |
+
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
|
| 43 |
+
|
| 44 |
+
<br />
|
| 45 |
+
|
| 46 |
+
More instructions are available in
|
| 47 |
+
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
|
| 48 |
+
>.
|
| 49 |
+
</WelcomeItem>
|
| 50 |
+
|
| 51 |
+
<WelcomeItem>
|
| 52 |
+
<template #icon>
|
| 53 |
+
<EcosystemIcon />
|
| 54 |
+
</template>
|
| 55 |
+
<template #heading>Ecosystem</template>
|
| 56 |
+
|
| 57 |
+
Get official tools and libraries for your project:
|
| 58 |
+
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
| 59 |
+
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
| 60 |
+
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
| 61 |
+
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
| 62 |
+
you need more resources, we suggest paying
|
| 63 |
+
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
| 64 |
+
a visit.
|
| 65 |
+
</WelcomeItem>
|
| 66 |
+
|
| 67 |
+
<WelcomeItem>
|
| 68 |
+
<template #icon>
|
| 69 |
+
<CommunityIcon />
|
| 70 |
+
</template>
|
| 71 |
+
<template #heading>Community</template>
|
| 72 |
+
|
| 73 |
+
Got stuck? Ask your question on
|
| 74 |
+
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
|
| 75 |
+
(our official Discord server), or
|
| 76 |
+
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
| 77 |
+
>StackOverflow</a
|
| 78 |
+
>. You should also follow the official
|
| 79 |
+
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
|
| 80 |
+
Bluesky account or the
|
| 81 |
+
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
| 82 |
+
X account for latest news in the Vue world.
|
| 83 |
+
</WelcomeItem>
|
| 84 |
+
|
| 85 |
+
<WelcomeItem>
|
| 86 |
+
<template #icon>
|
| 87 |
+
<SupportIcon />
|
| 88 |
+
</template>
|
| 89 |
+
<template #heading>Support Vue</template>
|
| 90 |
+
|
| 91 |
+
As an independent project, Vue relies on community backing for its sustainability. You can help
|
| 92 |
+
us by
|
| 93 |
+
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
| 94 |
+
</WelcomeItem>
|
| 95 |
+
</template>
|
frontend/src/components/WelcomeItem.vue
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<template>
|
| 2 |
+
<div class="item">
|
| 3 |
+
<i>
|
| 4 |
+
<slot name="icon"></slot>
|
| 5 |
+
</i>
|
| 6 |
+
<div class="details">
|
| 7 |
+
<h3>
|
| 8 |
+
<slot name="heading"></slot>
|
| 9 |
+
</h3>
|
| 10 |
+
<slot></slot>
|
| 11 |
+
</div>
|
| 12 |
+
</div>
|
| 13 |
+
</template>
|
| 14 |
+
|
| 15 |
+
<style scoped>
|
| 16 |
+
.item {
|
| 17 |
+
margin-top: 2rem;
|
| 18 |
+
display: flex;
|
| 19 |
+
position: relative;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
.details {
|
| 23 |
+
flex: 1;
|
| 24 |
+
margin-left: 1rem;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
i {
|
| 28 |
+
display: flex;
|
| 29 |
+
place-items: center;
|
| 30 |
+
place-content: center;
|
| 31 |
+
width: 32px;
|
| 32 |
+
height: 32px;
|
| 33 |
+
|
| 34 |
+
color: var(--color-text);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
h3 {
|
| 38 |
+
font-size: 1.2rem;
|
| 39 |
+
font-weight: 500;
|
| 40 |
+
margin-bottom: 0.4rem;
|
| 41 |
+
color: var(--color-heading);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
@media (min-width: 1024px) {
|
| 45 |
+
.item {
|
| 46 |
+
margin-top: 0;
|
| 47 |
+
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
i {
|
| 51 |
+
top: calc(50% - 25px);
|
| 52 |
+
left: -26px;
|
| 53 |
+
position: absolute;
|
| 54 |
+
border: 1px solid var(--color-border);
|
| 55 |
+
background: var(--color-background);
|
| 56 |
+
border-radius: 8px;
|
| 57 |
+
width: 50px;
|
| 58 |
+
height: 50px;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.item:before {
|
| 62 |
+
content: ' ';
|
| 63 |
+
border-left: 1px solid var(--color-border);
|
| 64 |
+
position: absolute;
|
| 65 |
+
left: 0;
|
| 66 |
+
bottom: calc(50% + 25px);
|
| 67 |
+
height: calc(50% - 25px);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.item:after {
|
| 71 |
+
content: ' ';
|
| 72 |
+
border-left: 1px solid var(--color-border);
|
| 73 |
+
position: absolute;
|
| 74 |
+
left: 0;
|
| 75 |
+
top: calc(50% + 25px);
|
| 76 |
+
height: calc(50% - 25px);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.item:first-of-type:before {
|
| 80 |
+
display: none;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.item:last-of-type:after {
|
| 84 |
+
display: none;
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
</style>
|
frontend/src/components/icons/IconCommunity.vue
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<template>
|
| 2 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
| 3 |
+
<path
|
| 4 |
+
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
| 5 |
+
/>
|
| 6 |
+
</svg>
|
| 7 |
+
</template>
|
frontend/src/components/icons/IconDocumentation.vue
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<template>
|
| 2 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
| 3 |
+
<path
|
| 4 |
+
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
| 5 |
+
/>
|
| 6 |
+
</svg>
|
| 7 |
+
</template>
|
frontend/src/components/icons/IconEcosystem.vue
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<template>
|
| 2 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
| 3 |
+
<path
|
| 4 |
+
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
| 5 |
+
/>
|
| 6 |
+
</svg>
|
| 7 |
+
</template>
|
frontend/src/components/icons/IconSupport.vue
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<template>
|
| 2 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
| 3 |
+
<path
|
| 4 |
+
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
| 5 |
+
/>
|
| 6 |
+
</svg>
|
| 7 |
+
</template>
|
frontend/src/components/icons/IconTooling.vue
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
| 2 |
+
<template>
|
| 3 |
+
<svg
|
| 4 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 5 |
+
xmlns:xlink="http://www.w3.org/1999/xlink"
|
| 6 |
+
aria-hidden="true"
|
| 7 |
+
role="img"
|
| 8 |
+
class="iconify iconify--mdi"
|
| 9 |
+
width="24"
|
| 10 |
+
height="24"
|
| 11 |
+
preserveAspectRatio="xMidYMid meet"
|
| 12 |
+
viewBox="0 0 24 24"
|
| 13 |
+
>
|
| 14 |
+
<path
|
| 15 |
+
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
| 16 |
+
fill="currentColor"
|
| 17 |
+
></path>
|
| 18 |
+
</svg>
|
| 19 |
+
</template>
|
frontend/src/main.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import './assets/main.css'
|
| 2 |
+
|
| 3 |
+
import { createApp } from 'vue'
|
| 4 |
+
import App from './App.vue'
|
| 5 |
+
import ElementPlus from 'element-plus'
|
| 6 |
+
import 'element-plus/dist/index.css'
|
| 7 |
+
import zhCn from 'element-plus/dist/locale/zh-cn'
|
| 8 |
+
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
| 9 |
+
// import router from './router'
|
| 10 |
+
import { createPinia } from 'pinia'
|
| 11 |
+
|
| 12 |
+
const app = createApp(App)
|
| 13 |
+
const pinia = createPinia()
|
| 14 |
+
|
| 15 |
+
app.use(pinia)
|
| 16 |
+
app.use(ElementPlus, {
|
| 17 |
+
locale: zhCn,
|
| 18 |
+
})
|
| 19 |
+
// app.use(router)
|
| 20 |
+
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
| 21 |
+
app.component(key, component)
|
| 22 |
+
}
|
| 23 |
+
app.mount('#app')
|
frontend/src/views/Upload.vue
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!-- Upload.vue -->
|
| 2 |
+
<script setup>
|
| 3 |
+
|
| 4 |
+
</script>
|
| 5 |
+
|
| 6 |
+
<template>
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
</template>
|
| 10 |
+
|
| 11 |
+
<style scoped>
|
| 12 |
+
|
| 13 |
+
</style>
|
frontend/vite.config.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { fileURLToPath, URL } from 'node:url'
|
| 2 |
+
|
| 3 |
+
import { defineConfig } from 'vite'
|
| 4 |
+
import vue from '@vitejs/plugin-vue'
|
| 5 |
+
import vueDevTools from 'vite-plugin-vue-devtools'
|
| 6 |
+
|
| 7 |
+
// https://vite.dev/config/
|
| 8 |
+
export default defineConfig({
|
| 9 |
+
plugins: [
|
| 10 |
+
vue(),
|
| 11 |
+
vueDevTools(),
|
| 12 |
+
],
|
| 13 |
+
resolve: {
|
| 14 |
+
alias: {
|
| 15 |
+
'@': fileURLToPath(new URL('./src', import.meta.url))
|
| 16 |
+
},
|
| 17 |
+
},
|
| 18 |
+
server: {
|
| 19 |
+
proxy: {
|
| 20 |
+
"/api": {
|
| 21 |
+
target: "http://localhost:5344",
|
| 22 |
+
changeOrigin: true,
|
| 23 |
+
},
|
| 24 |
+
},
|
| 25 |
+
},
|
| 26 |
+
})
|
hf_spaces_README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Sora Watermark Cleaner
|
| 3 |
+
emoji: 🎬
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
app_port: 8501
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# Sora Watermark Cleaner
|
| 12 |
+
|
| 13 |
+
Remove watermarks from Sora-generated videos using AI-powered inpainting.
|
| 14 |
+
|
| 15 |
+
## Models
|
| 16 |
+
|
| 17 |
+
- **LAMA** — Fast, good quality
|
| 18 |
+
- **E2FGVI-HQ** — Slower, best quality with temporal consistency
|
| 19 |
+
|
| 20 |
+
## API Endpoints
|
| 21 |
+
|
| 22 |
+
The FastAPI server runs on port 5344:
|
| 23 |
+
|
| 24 |
+
- `POST /api/v1/submit_remove_task` — Upload video, returns `task_id`
|
| 25 |
+
- `GET /api/v1/get_results?remove_task_id={id}` — Poll task status
|
| 26 |
+
- `GET /api/v1/download/{task_id}` — Download processed video
|
| 27 |
+
- `GET /api/v1/get_queue_status` — Queue metrics
|
mds/reward.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+

|
model_version.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"sha256": "79b44170111bd206d4964966b3b35adef1b3b15e7acf6427a95d35a2c715f987"}
|
notebooks/imputation.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
one-click-portable.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# One-Click Portable Version | 一键便携版
|
| 2 |
+
|
| 3 |
+
For **Windows** users - No installation required!
|
| 4 |
+
|
| 5 |
+
适用于 **Windows** 用户 - 无需安装!
|
| 6 |
+
|
| 7 |
+
## Download | 下载
|
| 8 |
+
|
| 9 |
+
**Google Drive:**
|
| 10 |
+
- https://drive.google.com/file/d/1ujH28aHaCXGgB146g6kyfz3Qxd-wHR1c/view?usp=share_link
|
| 11 |
+
|
| 12 |
+
**Baidu Pan | 百度网盘:**
|
| 13 |
+
- Link | 链接: https://pan.baidu.com/s/1onMom81mvw2c6PFkCuYzdg?pwd=jusu
|
| 14 |
+
- Extract Code | 提取码: `jusu`
|
| 15 |
+
|
| 16 |
+
## Usage | 使用方法
|
| 17 |
+
|
| 18 |
+
1. Download and extract the zip file | 下载并解压 zip 文件
|
| 19 |
+
2. Double-click `run.bat` | 双击 `run.bat` 文件
|
| 20 |
+
3. The web service will start automatically! | 网页服务将自动启动!
|
| 21 |
+
|
| 22 |
+
## Features | 特点
|
| 23 |
+
|
| 24 |
+
- ✅ Zero installation | 无需安装
|
| 25 |
+
- ✅ All dependencies included | 包含所有依赖
|
| 26 |
+
- ✅ Ready to use | 开箱即用
|
profile/profile_clean.sh
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
CUDA_VISIBLE_DEVICES=0 nsys profile \
|
| 4 |
+
--trace=cuda,cublas,nvtx,osrt,cudnn \
|
| 5 |
+
--force-overwrite=true \
|
| 6 |
+
-o profile/profile_clean \
|
| 7 |
+
python profile/run_clean.py
|
| 8 |
+
|
| 9 |
+
# 仅追踪 cuda 和 nvtx,不追踪 cudnn/cublas,也不追踪系统调用(osrt)
|
| 10 |
+
# nsys profile \
|
| 11 |
+
# --trace=cuda,nvtx \
|
| 12 |
+
# --sample=none \
|
| 13 |
+
# --cpuctxsw=none \
|
| 14 |
+
# --force-overwrite=true \
|
| 15 |
+
# -o profile/profile_lite \
|
| 16 |
+
# python profile/run.py
|
profile/profile_process_chunk.sh
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
CUDA_VISIBLE_DEVICES=0 nsys profile \
|
| 4 |
+
--trace=cuda,cublas,nvtx,osrt,cudnn \
|
| 5 |
+
--force-overwrite=true \
|
| 6 |
+
-o profiling/profile_process_chunk \
|
| 7 |
+
python profile/run_process_chunk.py
|
| 8 |
+
|
| 9 |
+
# 仅追踪 cuda 和 nvtx,不追踪 cudnn/cublas,也不追踪系统调用(osrt)
|
| 10 |
+
# nsys profile \
|
| 11 |
+
# --trace=cuda,nvtx \
|
| 12 |
+
# --sample=none \
|
| 13 |
+
# --cpuctxsw=none \
|
| 14 |
+
# --force-overwrite=true \
|
| 15 |
+
# -o profile/profile_lite \
|
| 16 |
+
# python profile/run.py
|
profile/profile_process_chunk_async.sh
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
CUDA_VISIBLE_DEVICES=0 nsys profile \
|
| 4 |
+
--trace=cuda,cublas,nvtx,osrt,cudnn \
|
| 5 |
+
--force-overwrite=true \
|
| 6 |
+
-o profiling/profile_process_chunk_async \
|
| 7 |
+
python profile/run_process_chunk_async.py
|
| 8 |
+
|
| 9 |
+
# 仅追踪 cuda 和 nvtx,不追踪 cudnn/cublas,也不追踪系统调用(osrt)
|
| 10 |
+
# nsys profile \
|
| 11 |
+
# --trace=cuda,nvtx \
|
| 12 |
+
# --sample=none \
|
| 13 |
+
# --cpuctxsw=none \
|
| 14 |
+
# --force-overwrite=true \
|
| 15 |
+
# -o profile/profile_lite \
|
| 16 |
+
# python profile/run.py
|
profile/profile_whole_infer.sh
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
CUDA_VISIBLE_DEVICES=0 nsys profile \
|
| 4 |
+
--trace=cuda,cublas,nvtx,osrt,cudnn \
|
| 5 |
+
--force-overwrite=true \
|
| 6 |
+
-o profile/profile_e2fgvi_hq \
|
| 7 |
+
python profile/run_whole.py
|
| 8 |
+
|
| 9 |
+
# 仅追踪 cuda 和 nvtx,不追踪 cudnn/cublas,也不追踪系统调用(osrt)
|
| 10 |
+
# nsys profile \
|
| 11 |
+
# --trace=cuda,nvtx \
|
| 12 |
+
# --sample=none \
|
| 13 |
+
# --cpuctxsw=none \
|
| 14 |
+
# --force-overwrite=true \
|
| 15 |
+
# -o profile/profile_lite \
|
| 16 |
+
# python profile/run.py
|
profile/run_clean.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from contextlib import contextmanager
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from typing import List
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
import torch
|
| 7 |
+
from loguru import logger
|
| 8 |
+
from torch.cuda.nvtx import range_pop, range_push
|
| 9 |
+
from tqdm import tqdm
|
| 10 |
+
|
| 11 |
+
from sorawm.cleaner.e2fgvi_hq_cleaner import *
|
| 12 |
+
from sorawm.utils.video_utils import merge_frames_with_overlap
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@contextmanager
|
| 16 |
+
def nvtx(msg: str):
|
| 17 |
+
range_push(msg)
|
| 18 |
+
try:
|
| 19 |
+
yield
|
| 20 |
+
finally:
|
| 21 |
+
range_pop()
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class ProfileE2FGVIHDCleaner(E2FGVIHDCleaner):
|
| 25 |
+
def clean(self, frames: np.ndarray, masks: np.ndarray) -> List[np.ndarray]:
|
| 26 |
+
"""
|
| 27 |
+
Process frames and masks in overlapping temporal chunks, run per-chunk inpainting/propagation, and merge the chunk results into a final list of cleaned frames.
|
| 28 |
+
|
| 29 |
+
Parameters:
|
| 30 |
+
frames (np.ndarray): Input video frames with time as the first dimension, e.g. shape (T, H, W, C) or a sequence where frames[0].shape == (H, W, C).
|
| 31 |
+
masks (np.ndarray): Corresponding masks with time as the first dimension, e.g. shape (T, H, W) or (T, H, W, 1). Nonzero pixels indicate regions to be processed.
|
| 32 |
+
|
| 33 |
+
Returns:
|
| 34 |
+
List[np.ndarray]: A list of length T containing the cleaned/composted frames as numpy arrays with shape (H, W, C).
|
| 35 |
+
"""
|
| 36 |
+
with nvtx("ProfileE2FGVIHDCleaner.clean_total"):
|
| 37 |
+
with nvtx("setup_basic_params"):
|
| 38 |
+
video_length = len(frames)
|
| 39 |
+
chunk_size = int(self.config.chunk_size_ratio * video_length)
|
| 40 |
+
overlap_size = int(self.config.overlap_ratio * video_length)
|
| 41 |
+
num_chunks = int(np.ceil(video_length / (chunk_size - overlap_size)))
|
| 42 |
+
h, w = frames[0].shape[:2]
|
| 43 |
+
|
| 44 |
+
# Convert to tensors
|
| 45 |
+
with nvtx("numpy_to_tensor"):
|
| 46 |
+
imgs_all, masks_all = numpy_to_tensor(frames, masks)
|
| 47 |
+
|
| 48 |
+
# Prepare binary masks for compositing
|
| 49 |
+
with nvtx("prepare_binary_masks"):
|
| 50 |
+
binary_masks = np.expand_dims(masks > 0, axis=-1).astype(
|
| 51 |
+
np.uint8
|
| 52 |
+
) # (T, H, W, 1)
|
| 53 |
+
|
| 54 |
+
comp_frames = [None] * video_length
|
| 55 |
+
logger.debug(
|
| 56 |
+
f"Processing {video_length} frames in {num_chunks} chunks "
|
| 57 |
+
f"(chunk_size={chunk_size}, overlap={overlap_size})"
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
for chunk_idx in tqdm(
|
| 61 |
+
range(num_chunks), desc="Chunk", position=0, leave=True
|
| 62 |
+
):
|
| 63 |
+
with nvtx(f"chunk_{chunk_idx:03d}_total"):
|
| 64 |
+
with nvtx("chunk_compute_indices"):
|
| 65 |
+
start_idx = chunk_idx * (chunk_size - overlap_size)
|
| 66 |
+
end_idx = min(start_idx + chunk_size, video_length)
|
| 67 |
+
actual_chunk_size = end_idx - start_idx
|
| 68 |
+
|
| 69 |
+
# Extract chunk data
|
| 70 |
+
with nvtx("chunk_extract_and_to_device"):
|
| 71 |
+
imgs_chunk = imgs_all[:, start_idx:end_idx, :, :, :].to(device)
|
| 72 |
+
masks_chunk = masks_all[:, start_idx:end_idx, :, :, :].to(
|
| 73 |
+
device
|
| 74 |
+
)
|
| 75 |
+
frames_np_chunk = frames[start_idx:end_idx]
|
| 76 |
+
binary_masks_chunk = binary_masks[start_idx:end_idx]
|
| 77 |
+
|
| 78 |
+
# Core inpainting / propagation
|
| 79 |
+
with nvtx("process_frames_chunk"):
|
| 80 |
+
comp_frames_chunk = self.process_frames_chunk(
|
| 81 |
+
actual_chunk_size,
|
| 82 |
+
self.config.neighbor_stride,
|
| 83 |
+
imgs_chunk,
|
| 84 |
+
masks_chunk,
|
| 85 |
+
binary_masks_chunk,
|
| 86 |
+
frames_np_chunk,
|
| 87 |
+
h,
|
| 88 |
+
w,
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
# Merge results with blending in overlap region
|
| 92 |
+
with nvtx("merge_frames_with_overlap"):
|
| 93 |
+
comp_frames = merge_frames_with_overlap(
|
| 94 |
+
result_frames=comp_frames,
|
| 95 |
+
chunk_frames=comp_frames_chunk,
|
| 96 |
+
start_idx=start_idx,
|
| 97 |
+
overlap_size=overlap_size,
|
| 98 |
+
is_first_chunk=(chunk_idx == 0),
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
# Clear GPU memory
|
| 102 |
+
with nvtx("chunk_cleanup"):
|
| 103 |
+
del imgs_chunk, masks_chunk, comp_frames_chunk
|
| 104 |
+
try:
|
| 105 |
+
torch.cuda.empty_cache()
|
| 106 |
+
except Exception:
|
| 107 |
+
pass
|
| 108 |
+
|
| 109 |
+
return comp_frames
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
if __name__ == "__main__":
|
| 113 |
+
CMD = Path.cwd() / "profile"
|
| 114 |
+
|
| 115 |
+
masks_npy_path = CMD / "masks.npy"
|
| 116 |
+
frames_npy_path = CMD / "frames.npy"
|
| 117 |
+
|
| 118 |
+
with nvtx("load_numpy_inputs"):
|
| 119 |
+
masks = np.load(masks_npy_path)
|
| 120 |
+
frames = np.load(frames_npy_path)
|
| 121 |
+
|
| 122 |
+
with nvtx("init_cleaner"):
|
| 123 |
+
cleaner = ProfileE2FGVIHDCleaner()
|
| 124 |
+
|
| 125 |
+
with nvtx("run_cleaner"):
|
| 126 |
+
cleaned_frames = cleaner.clean(frames, masks)
|
| 127 |
+
|
| 128 |
+
# np.save(CMD / "cleaned_frames.npy", cleaned_frames)
|
profile/run_process_chunk.py
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from contextlib import contextmanager
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from typing import List
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn.functional as F
|
| 8 |
+
from loguru import logger
|
| 9 |
+
from torch.cuda.nvtx import range_pop, range_push
|
| 10 |
+
from tqdm import tqdm
|
| 11 |
+
|
| 12 |
+
from sorawm.cleaner.e2fgvi_hq_cleaner import *
|
| 13 |
+
from sorawm.models.model.e2fgvi_hq import InpaintGenerator
|
| 14 |
+
from sorawm.utils.video_utils import merge_frames_with_overlap
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@contextmanager
|
| 18 |
+
def nvtx(msg: str):
|
| 19 |
+
range_push(msg)
|
| 20 |
+
try:
|
| 21 |
+
yield
|
| 22 |
+
finally:
|
| 23 |
+
range_pop()
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class ProfileInpaintGenerator(InpaintGenerator):
|
| 27 |
+
def forward_bidirect_flow(self, masked_local_frames):
|
| 28 |
+
"""
|
| 29 |
+
Estimate bidirectional optical flows between consecutive frames in a local masked sequence.
|
| 30 |
+
|
| 31 |
+
Parameters:
|
| 32 |
+
masked_local_frames (torch.Tensor): Input tensor of masked local frames with shape
|
| 33 |
+
(batch, time, channels, height, width).
|
| 34 |
+
|
| 35 |
+
Returns:
|
| 36 |
+
tuple: A pair (pred_flows_forward, pred_flows_backward) where each is a torch.Tensor
|
| 37 |
+
of shape (batch, time - 1, 2, height // 4, width // 4). Each tensor contains 2D
|
| 38 |
+
optical flow vectors: `pred_flows_forward` maps each frame to the next (i -> i+1),
|
| 39 |
+
and `pred_flows_backward` maps each frame to the previous (i+1 -> i).
|
| 40 |
+
"""
|
| 41 |
+
with nvtx("InpaintGenerator.forward_bidirect_flow_total"):
|
| 42 |
+
b, l_t, c, h, w = masked_local_frames.size()
|
| 43 |
+
|
| 44 |
+
with nvtx("flow_downsample_interpolate"):
|
| 45 |
+
masked_local_frames = F.interpolate(
|
| 46 |
+
masked_local_frames.view(-1, c, h, w),
|
| 47 |
+
scale_factor=1 / 4,
|
| 48 |
+
mode="bilinear",
|
| 49 |
+
align_corners=True,
|
| 50 |
+
recompute_scale_factor=True,
|
| 51 |
+
)
|
| 52 |
+
masked_local_frames = masked_local_frames.view(
|
| 53 |
+
b, l_t, c, h // 4, w // 4
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
with nvtx("flow_prepare_pairs"):
|
| 57 |
+
mlf_1 = masked_local_frames[:, :-1, :, :, :].reshape(
|
| 58 |
+
-1, c, h // 4, w // 4
|
| 59 |
+
)
|
| 60 |
+
mlf_2 = masked_local_frames[:, 1:, :, :, :].reshape(
|
| 61 |
+
-1, c, h // 4, w // 4
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
with nvtx("spynet_forward"):
|
| 65 |
+
pred_flows_forward = self.update_spynet(mlf_1, mlf_2)
|
| 66 |
+
|
| 67 |
+
with nvtx("spynet_backward"):
|
| 68 |
+
pred_flows_backward = self.update_spynet(mlf_2, mlf_1)
|
| 69 |
+
|
| 70 |
+
with nvtx("flow_reshape"):
|
| 71 |
+
pred_flows_forward = pred_flows_forward.view(
|
| 72 |
+
b, l_t - 1, 2, h // 4, w // 4
|
| 73 |
+
)
|
| 74 |
+
pred_flows_backward = pred_flows_backward.view(
|
| 75 |
+
b, l_t - 1, 2, h // 4, w // 4
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
return pred_flows_forward, pred_flows_backward
|
| 79 |
+
|
| 80 |
+
def forward(self, masked_frames, num_local_frames):
|
| 81 |
+
"""
|
| 82 |
+
Run inpainting generator on a sequence of masked frames, producing reconstructed frames and bidirectional flow estimates.
|
| 83 |
+
|
| 84 |
+
Parameters:
|
| 85 |
+
masked_frames (torch.Tensor): Tensor of shape (batch, time, channels, height, width) containing masked input frames (expected normalized to model range).
|
| 86 |
+
num_local_frames (int): Number of initial frames in each sequence treated as local (used for flow estimation and local feature propagation).
|
| 87 |
+
|
| 88 |
+
Returns:
|
| 89 |
+
output (torch.Tensor): Reconstructed frames tensor of shape (batch * time, channels_out, height_out, width_out) with values in [-1, 1].
|
| 90 |
+
pred_flows (tuple): A pair (pred_flows_forward, pred_flows_backward) of tensors holding predicted optical flows for forward and backward directions; each has shape (batch, time-1, 2, h_flow, w_flow).
|
| 91 |
+
"""
|
| 92 |
+
with nvtx("InpaintGenerator.forward_total"):
|
| 93 |
+
l_t = num_local_frames
|
| 94 |
+
b, t, ori_c, ori_h, ori_w = masked_frames.size()
|
| 95 |
+
|
| 96 |
+
with nvtx("forward_normalize_local_frames"):
|
| 97 |
+
masked_local_frames = (masked_frames[:, :l_t, ...] + 1) / 2
|
| 98 |
+
|
| 99 |
+
with nvtx("forward_bidirect_flow_call"):
|
| 100 |
+
pred_flows = self.forward_bidirect_flow(masked_local_frames)
|
| 101 |
+
|
| 102 |
+
with nvtx("encoder_all_frames"):
|
| 103 |
+
enc_feat = self.encoder(masked_frames.view(b * t, ori_c, ori_h, ori_w))
|
| 104 |
+
|
| 105 |
+
with nvtx("split_local_ref_feat"):
|
| 106 |
+
_, c, h, w = enc_feat.size()
|
| 107 |
+
fold_output_size = (h, w)
|
| 108 |
+
local_feat = enc_feat.view(b, t, c, h, w)[:, :l_t, ...]
|
| 109 |
+
ref_feat = enc_feat.view(b, t, c, h, w)[:, l_t:, ...]
|
| 110 |
+
|
| 111 |
+
with nvtx("feat_prop_module"):
|
| 112 |
+
local_feat = self.feat_prop_module(
|
| 113 |
+
local_feat, pred_flows[0], pred_flows[1]
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
with nvtx("concat_local_ref"):
|
| 117 |
+
enc_feat = torch.cat((local_feat, ref_feat), dim=1)
|
| 118 |
+
|
| 119 |
+
with nvtx("temporal_focal_transformers_ss"):
|
| 120 |
+
trans_feat = self.ss(enc_feat.view(-1, c, h, w), b, fold_output_size)
|
| 121 |
+
|
| 122 |
+
with nvtx("temporal_transformer_blocks"):
|
| 123 |
+
trans_feat = self.transformer([trans_feat, fold_output_size])
|
| 124 |
+
|
| 125 |
+
with nvtx("sc_fuse"):
|
| 126 |
+
trans_feat = self.sc(trans_feat[0], t, fold_output_size)
|
| 127 |
+
trans_feat = trans_feat.view(b, t, -1, h, w)
|
| 128 |
+
|
| 129 |
+
with nvtx("residual_add"):
|
| 130 |
+
enc_feat = enc_feat + trans_feat
|
| 131 |
+
|
| 132 |
+
with nvtx("decoder"):
|
| 133 |
+
output = self.decoder(enc_feat.view(b * t, c, h, w))
|
| 134 |
+
output = torch.tanh(output)
|
| 135 |
+
|
| 136 |
+
return output, pred_flows
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
class ProfileE2FGVIHDCleaner(E2FGVIHDCleaner):
|
| 140 |
+
def __init__(
|
| 141 |
+
self,
|
| 142 |
+
ckpt_path: Path = E2FGVI_HQ_CHECKPOINT_PATH,
|
| 143 |
+
config: E2FGVIHDConfig = E2FGVIHDConfig(),
|
| 144 |
+
):
|
| 145 |
+
with nvtx("cleaner_init_total"):
|
| 146 |
+
with nvtx("ensure_model_downloaded"):
|
| 147 |
+
ensure_model_downloaded(ckpt_path, E2FGVI_HQ_CHECKPOINT_REMOTE_URL)
|
| 148 |
+
|
| 149 |
+
with nvtx("init_model"):
|
| 150 |
+
self.model = ProfileInpaintGenerator().to(device)
|
| 151 |
+
|
| 152 |
+
with nvtx("load_ckpt"):
|
| 153 |
+
state = torch.load(ckpt_path, map_location=device)
|
| 154 |
+
self.model.load_state_dict(state)
|
| 155 |
+
|
| 156 |
+
with nvtx("model_eval_mode"):
|
| 157 |
+
self.model.eval()
|
| 158 |
+
|
| 159 |
+
self.model = torch.compile(self.model)
|
| 160 |
+
|
| 161 |
+
self.config = config
|
| 162 |
+
|
| 163 |
+
def clean(self, frames: np.ndarray, masks: np.ndarray) -> List[np.ndarray]:
|
| 164 |
+
"""
|
| 165 |
+
Run the full cleaning pipeline on a video using chunked, overlapping processing and return reconstructed frames.
|
| 166 |
+
|
| 167 |
+
Processes the input frames and masks in configurable chunks with overlap: converts inputs to tensors, runs per-chunk inpainting and fusion, merges chunk outputs handling overlaps, and returns the final list of cleaned frames in original order.
|
| 168 |
+
|
| 169 |
+
Parameters:
|
| 170 |
+
frames (np.ndarray): Sequence of input RGB frames as a numpy array of shape (T, H, W, C) with values in [0, 255] or [0,1].
|
| 171 |
+
masks (np.ndarray): Corresponding mask array of shape (T, H, W) where nonzero values indicate regions to inpaint.
|
| 172 |
+
|
| 173 |
+
Returns:
|
| 174 |
+
List[np.ndarray]: List of T reconstructed RGB frames as numpy arrays (H, W, C), in the same order as the input.
|
| 175 |
+
"""
|
| 176 |
+
with nvtx("ProfileE2FGVIHDCleaner.clean_total"):
|
| 177 |
+
with nvtx("setup_basic_params"):
|
| 178 |
+
video_length = len(frames)
|
| 179 |
+
chunk_size = int(self.config.chunk_size_ratio * video_length)
|
| 180 |
+
overlap_size = int(self.config.overlap_ratio * video_length)
|
| 181 |
+
num_chunks = int(np.ceil(video_length / (chunk_size - overlap_size)))
|
| 182 |
+
h, w = frames[0].shape[:2]
|
| 183 |
+
|
| 184 |
+
with nvtx("numpy_to_tensor"):
|
| 185 |
+
imgs_all, masks_all = numpy_to_tensor(frames, masks)
|
| 186 |
+
|
| 187 |
+
with nvtx("prepare_binary_masks"):
|
| 188 |
+
binary_masks = np.expand_dims(masks > 0, axis=-1).astype(np.uint8)
|
| 189 |
+
|
| 190 |
+
comp_frames = [None] * video_length
|
| 191 |
+
logger.debug(
|
| 192 |
+
f"Processing {video_length} frames in {num_chunks} chunks "
|
| 193 |
+
f"(chunk_size={chunk_size}, overlap={overlap_size})"
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
for chunk_idx in tqdm(
|
| 197 |
+
range(num_chunks), desc="Chunk", position=0, leave=True
|
| 198 |
+
):
|
| 199 |
+
with nvtx(f"chunk_{chunk_idx:03d}_total"):
|
| 200 |
+
with nvtx("chunk_compute_indices"):
|
| 201 |
+
start_idx = chunk_idx * (chunk_size - overlap_size)
|
| 202 |
+
end_idx = min(start_idx + chunk_size, video_length)
|
| 203 |
+
actual_chunk_size = end_idx - start_idx
|
| 204 |
+
|
| 205 |
+
with nvtx("chunk_extract_and_to_device"):
|
| 206 |
+
imgs_chunk = imgs_all[:, start_idx:end_idx, :, :, :].to(device)
|
| 207 |
+
masks_chunk = masks_all[:, start_idx:end_idx, :, :, :].to(
|
| 208 |
+
device
|
| 209 |
+
)
|
| 210 |
+
frames_np_chunk = frames[start_idx:end_idx]
|
| 211 |
+
binary_masks_chunk = binary_masks[start_idx:end_idx]
|
| 212 |
+
|
| 213 |
+
with nvtx("chunk_process_frames_chunk"):
|
| 214 |
+
comp_frames_chunk = self.process_frames_chunk(
|
| 215 |
+
actual_chunk_size,
|
| 216 |
+
self.config.neighbor_stride,
|
| 217 |
+
imgs_chunk,
|
| 218 |
+
masks_chunk,
|
| 219 |
+
binary_masks_chunk,
|
| 220 |
+
frames_np_chunk,
|
| 221 |
+
h,
|
| 222 |
+
w,
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
with nvtx("merge_frames_with_overlap"):
|
| 226 |
+
comp_frames = merge_frames_with_overlap(
|
| 227 |
+
result_frames=comp_frames,
|
| 228 |
+
chunk_frames=comp_frames_chunk,
|
| 229 |
+
start_idx=start_idx,
|
| 230 |
+
overlap_size=overlap_size,
|
| 231 |
+
is_first_chunk=(chunk_idx == 0),
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
with nvtx("chunk_cleanup"):
|
| 235 |
+
del imgs_chunk, masks_chunk, comp_frames_chunk
|
| 236 |
+
try:
|
| 237 |
+
torch.cuda.empty_cache()
|
| 238 |
+
except Exception:
|
| 239 |
+
pass
|
| 240 |
+
|
| 241 |
+
return comp_frames
|
| 242 |
+
|
| 243 |
+
def process_frames_chunk(
|
| 244 |
+
self,
|
| 245 |
+
chunk_length: int,
|
| 246 |
+
neighbor_stride: int,
|
| 247 |
+
imgs_chunk: torch.Tensor,
|
| 248 |
+
masks_chunk: torch.Tensor,
|
| 249 |
+
binary_masks_chunk: np.ndarray,
|
| 250 |
+
frames_np_chunk: np.ndarray,
|
| 251 |
+
h: int,
|
| 252 |
+
w: int,
|
| 253 |
+
) -> List[np.ndarray]:
|
| 254 |
+
"""
|
| 255 |
+
Compose inpainted frames for a chunk by running the model on sliding windows, blending predictions back into original frames.
|
| 256 |
+
|
| 257 |
+
Parameters:
|
| 258 |
+
chunk_length (int): Number of frames in the current chunk.
|
| 259 |
+
neighbor_stride (int): Half-window radius (in frames) used to select neighboring frames around each reference; determines step between processed reference frames.
|
| 260 |
+
imgs_chunk (torch.Tensor): Tensor of shape (1, T, C, H, W) containing chunk frames normalized for model input.
|
| 261 |
+
masks_chunk (torch.Tensor): Tensor of shape (1, T, 1, H, W) containing corresponding masks where masked regions are 1.
|
| 262 |
+
binary_masks_chunk (np.ndarray): Array of per-frame binary masks (H, W) or (H, W, 1) used for compositing predictions onto original frames (values 0/1).
|
| 263 |
+
frames_np_chunk (np.ndarray): Original chunk frames as uint8 numpy arrays in shape (T, H, W, C).
|
| 264 |
+
h (int): Original frame height.
|
| 265 |
+
w (int): Original frame width.
|
| 266 |
+
|
| 267 |
+
Returns:
|
| 268 |
+
List[np.ndarray]: A list of length `chunk_length` where each entry is the reconstructed uint8 RGB frame with model predictions composited into unmasked regions; overlapping predictions are averaged.
|
| 269 |
+
|
| 270 |
+
Raises:
|
| 271 |
+
RuntimeError: Intentionally raises RuntimeError("Stop here") to terminate profiling at the profiling breakpoint.
|
| 272 |
+
"""
|
| 273 |
+
comp_frames_chunk = [None] * chunk_length
|
| 274 |
+
|
| 275 |
+
for f in tqdm(
|
| 276 |
+
range(0, chunk_length, neighbor_stride),
|
| 277 |
+
desc=f" Frame progress",
|
| 278 |
+
position=1,
|
| 279 |
+
leave=False,
|
| 280 |
+
):
|
| 281 |
+
with nvtx(f"window_f_{f:05d}_total"):
|
| 282 |
+
with nvtx("window_neighbor_ref_ids"):
|
| 283 |
+
neighbor_ids = [
|
| 284 |
+
i
|
| 285 |
+
for i in range(
|
| 286 |
+
max(0, f - neighbor_stride),
|
| 287 |
+
min(chunk_length, f + neighbor_stride + 1),
|
| 288 |
+
)
|
| 289 |
+
]
|
| 290 |
+
ref_ids = get_ref_index(
|
| 291 |
+
f,
|
| 292 |
+
neighbor_ids,
|
| 293 |
+
chunk_length,
|
| 294 |
+
self.config.ref_length,
|
| 295 |
+
self.config.num_ref,
|
| 296 |
+
)
|
| 297 |
+
|
| 298 |
+
with nvtx("window_select_tensors"):
|
| 299 |
+
selected_imgs = imgs_chunk[:1, neighbor_ids + ref_ids, :, :, :]
|
| 300 |
+
selected_masks = masks_chunk[:1, neighbor_ids + ref_ids, :, :, :]
|
| 301 |
+
|
| 302 |
+
with torch.no_grad():
|
| 303 |
+
with nvtx("window_apply_mask"):
|
| 304 |
+
masked_imgs = selected_imgs * (1 - selected_masks)
|
| 305 |
+
|
| 306 |
+
with nvtx("window_pad_flip_concat"):
|
| 307 |
+
mod_size_h = 60
|
| 308 |
+
mod_size_w = 108
|
| 309 |
+
h_pad = (mod_size_h - h % mod_size_h) % mod_size_h
|
| 310 |
+
w_pad = (mod_size_w - w % mod_size_w) % mod_size_w
|
| 311 |
+
|
| 312 |
+
masked_imgs = torch.cat(
|
| 313 |
+
[masked_imgs, torch.flip(masked_imgs, [3])], 3
|
| 314 |
+
)[:, :, :, : h + h_pad, :]
|
| 315 |
+
|
| 316 |
+
masked_imgs = torch.cat(
|
| 317 |
+
[masked_imgs, torch.flip(masked_imgs, [4])], 4
|
| 318 |
+
)[:, :, :, :, : w + w_pad]
|
| 319 |
+
|
| 320 |
+
with nvtx("window_model_infer"):
|
| 321 |
+
# GPU ops
|
| 322 |
+
pred_imgs, _ = self.model(masked_imgs, len(neighbor_ids))
|
| 323 |
+
pred_imgs = pred_imgs[:, :, :h, :w]
|
| 324 |
+
pred_imgs = (pred_imgs + 1) / 2
|
| 325 |
+
with nvtx("D2H"):
|
| 326 |
+
# IO ops
|
| 327 |
+
pred_imgs = pred_imgs.cpu().permute(0, 2, 3, 1).numpy() * 255
|
| 328 |
+
|
| 329 |
+
with nvtx("window_composite_back_to_frames"):
|
| 330 |
+
for i in range(len(neighbor_ids)):
|
| 331 |
+
idx = neighbor_ids[i]
|
| 332 |
+
img = np.array(pred_imgs[i]).astype(
|
| 333 |
+
np.uint8
|
| 334 |
+
) * binary_masks_chunk[idx] + frames_np_chunk[idx] * (
|
| 335 |
+
1 - binary_masks_chunk[idx]
|
| 336 |
+
)
|
| 337 |
+
|
| 338 |
+
if comp_frames_chunk[idx] is None:
|
| 339 |
+
comp_frames_chunk[idx] = img
|
| 340 |
+
else:
|
| 341 |
+
comp_frames_chunk[idx] = (
|
| 342 |
+
comp_frames_chunk[idx].astype(np.float32) * 0.5
|
| 343 |
+
+ img.astype(np.float32) * 0.5
|
| 344 |
+
)
|
| 345 |
+
|
| 346 |
+
# 你用来中断 profiling 的断点,保留
|
| 347 |
+
raise RuntimeError("Stop here")
|
| 348 |
+
|
| 349 |
+
return comp_frames_chunk
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
if __name__ == "__main__":
|
| 353 |
+
CMD = Path.cwd() / "profiling"
|
| 354 |
+
|
| 355 |
+
masks_npy_path = CMD / "masks.npy"
|
| 356 |
+
frames_npy_path = CMD / "frames.npy"
|
| 357 |
+
|
| 358 |
+
with nvtx("load_numpy_inputs"):
|
| 359 |
+
masks = np.load(masks_npy_path)
|
| 360 |
+
frames = np.load(frames_npy_path)
|
| 361 |
+
|
| 362 |
+
with nvtx("init_cleaner"):
|
| 363 |
+
cleaner = ProfileE2FGVIHDCleaner()
|
| 364 |
+
|
| 365 |
+
with nvtx("run_cleaner"):
|
| 366 |
+
cleaned_frames = cleaner.clean(frames, masks)
|
| 367 |
+
|
| 368 |
+
# np.save(CMD / "cleaned_frames.npy", cleaned_frames)
|
profile/run_process_chunk_async.py
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from contextlib import contextmanager
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from typing import List
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn.functional as F
|
| 8 |
+
from loguru import logger
|
| 9 |
+
from torch.cuda.nvtx import range_pop, range_push
|
| 10 |
+
from tqdm import tqdm
|
| 11 |
+
|
| 12 |
+
from sorawm.cleaner.e2fgvi_hq_cleaner import *
|
| 13 |
+
from sorawm.models.model.e2fgvi_hq import InpaintGenerator
|
| 14 |
+
from sorawm.utils.video_utils import merge_frames_with_overlap
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@contextmanager
|
| 18 |
+
def nvtx(msg: str):
|
| 19 |
+
range_push(msg)
|
| 20 |
+
try:
|
| 21 |
+
yield
|
| 22 |
+
finally:
|
| 23 |
+
range_pop()
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class ProfileInpaintGenerator(InpaintGenerator):
|
| 27 |
+
def forward_bidirect_flow(self, masked_local_frames):
|
| 28 |
+
"""
|
| 29 |
+
Estimate bidirectional optical flows between consecutive frames in a local masked sequence.
|
| 30 |
+
|
| 31 |
+
Parameters:
|
| 32 |
+
masked_local_frames (torch.Tensor): Input tensor of masked local frames with shape
|
| 33 |
+
(batch, time, channels, height, width).
|
| 34 |
+
|
| 35 |
+
Returns:
|
| 36 |
+
tuple: A pair (pred_flows_forward, pred_flows_backward) where each is a torch.Tensor
|
| 37 |
+
of shape (batch, time - 1, 2, height // 4, width // 4). Each tensor contains 2D
|
| 38 |
+
optical flow vectors: `pred_flows_forward` maps each frame to the next (i -> i+1),
|
| 39 |
+
and `pred_flows_backward` maps each frame to the previous (i+1 -> i).
|
| 40 |
+
"""
|
| 41 |
+
with nvtx("InpaintGenerator.forward_bidirect_flow_total"):
|
| 42 |
+
b, l_t, c, h, w = masked_local_frames.size()
|
| 43 |
+
|
| 44 |
+
with nvtx("flow_downsample_interpolate"):
|
| 45 |
+
masked_local_frames = F.interpolate(
|
| 46 |
+
masked_local_frames.view(-1, c, h, w),
|
| 47 |
+
scale_factor=1 / 4,
|
| 48 |
+
mode="bilinear",
|
| 49 |
+
align_corners=True,
|
| 50 |
+
recompute_scale_factor=True,
|
| 51 |
+
)
|
| 52 |
+
masked_local_frames = masked_local_frames.view(
|
| 53 |
+
b, l_t, c, h // 4, w // 4
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
with nvtx("flow_prepare_pairs"):
|
| 57 |
+
mlf_1 = masked_local_frames[:, :-1, :, :, :].reshape(
|
| 58 |
+
-1, c, h // 4, w // 4
|
| 59 |
+
)
|
| 60 |
+
mlf_2 = masked_local_frames[:, 1:, :, :, :].reshape(
|
| 61 |
+
-1, c, h // 4, w // 4
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
with nvtx("spynet_forward"):
|
| 65 |
+
pred_flows_forward = self.update_spynet(mlf_1, mlf_2)
|
| 66 |
+
|
| 67 |
+
with nvtx("spynet_backward"):
|
| 68 |
+
pred_flows_backward = self.update_spynet(mlf_2, mlf_1)
|
| 69 |
+
|
| 70 |
+
with nvtx("flow_reshape"):
|
| 71 |
+
pred_flows_forward = pred_flows_forward.view(
|
| 72 |
+
b, l_t - 1, 2, h // 4, w // 4
|
| 73 |
+
)
|
| 74 |
+
pred_flows_backward = pred_flows_backward.view(
|
| 75 |
+
b, l_t - 1, 2, h // 4, w // 4
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
return pred_flows_forward, pred_flows_backward
|
| 79 |
+
|
| 80 |
+
def forward(self, masked_frames, num_local_frames):
|
| 81 |
+
"""
|
| 82 |
+
Run inpainting generator on a sequence of masked frames, producing reconstructed frames and bidirectional flow estimates.
|
| 83 |
+
|
| 84 |
+
Parameters:
|
| 85 |
+
masked_frames (torch.Tensor): Tensor of shape (batch, time, channels, height, width) containing masked input frames (expected normalized to model range).
|
| 86 |
+
num_local_frames (int): Number of initial frames in each sequence treated as local (used for flow estimation and local feature propagation).
|
| 87 |
+
|
| 88 |
+
Returns:
|
| 89 |
+
output (torch.Tensor): Reconstructed frames tensor of shape (batch * time, channels_out, height_out, width_out) with values in [-1, 1].
|
| 90 |
+
pred_flows (tuple): A pair (pred_flows_forward, pred_flows_backward) of tensors holding predicted optical flows for forward and backward directions; each has shape (batch, time-1, 2, h_flow, w_flow).
|
| 91 |
+
"""
|
| 92 |
+
with nvtx("InpaintGenerator.forward_total"):
|
| 93 |
+
l_t = num_local_frames
|
| 94 |
+
b, t, ori_c, ori_h, ori_w = masked_frames.size()
|
| 95 |
+
|
| 96 |
+
with nvtx("forward_normalize_local_frames"):
|
| 97 |
+
masked_local_frames = (masked_frames[:, :l_t, ...] + 1) / 2
|
| 98 |
+
|
| 99 |
+
with nvtx("forward_bidirect_flow_call"):
|
| 100 |
+
pred_flows = self.forward_bidirect_flow(masked_local_frames)
|
| 101 |
+
|
| 102 |
+
with nvtx("encoder_all_frames"):
|
| 103 |
+
enc_feat = self.encoder(masked_frames.view(b * t, ori_c, ori_h, ori_w))
|
| 104 |
+
|
| 105 |
+
with nvtx("split_local_ref_feat"):
|
| 106 |
+
_, c, h, w = enc_feat.size()
|
| 107 |
+
fold_output_size = (h, w)
|
| 108 |
+
local_feat = enc_feat.view(b, t, c, h, w)[:, :l_t, ...]
|
| 109 |
+
ref_feat = enc_feat.view(b, t, c, h, w)[:, l_t:, ...]
|
| 110 |
+
|
| 111 |
+
with nvtx("feat_prop_module"):
|
| 112 |
+
local_feat = self.feat_prop_module(
|
| 113 |
+
local_feat, pred_flows[0], pred_flows[1]
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
with nvtx("concat_local_ref"):
|
| 117 |
+
enc_feat = torch.cat((local_feat, ref_feat), dim=1)
|
| 118 |
+
|
| 119 |
+
with nvtx("temporal_focal_transformers_ss"):
|
| 120 |
+
trans_feat = self.ss(enc_feat.view(-1, c, h, w), b, fold_output_size)
|
| 121 |
+
|
| 122 |
+
with nvtx("temporal_transformer_blocks"):
|
| 123 |
+
trans_feat = self.transformer([trans_feat, fold_output_size])
|
| 124 |
+
|
| 125 |
+
with nvtx("sc_fuse"):
|
| 126 |
+
trans_feat = self.sc(trans_feat[0], t, fold_output_size)
|
| 127 |
+
trans_feat = trans_feat.view(b, t, -1, h, w)
|
| 128 |
+
|
| 129 |
+
with nvtx("residual_add"):
|
| 130 |
+
enc_feat = enc_feat + trans_feat
|
| 131 |
+
|
| 132 |
+
with nvtx("decoder"):
|
| 133 |
+
output = self.decoder(enc_feat.view(b * t, c, h, w))
|
| 134 |
+
output = torch.tanh(output)
|
| 135 |
+
|
| 136 |
+
return output, pred_flows
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
class ProfileE2FGVIHDCleaner(E2FGVIHDCleaner):
|
| 140 |
+
def __init__(
|
| 141 |
+
self,
|
| 142 |
+
ckpt_path: Path = E2FGVI_HQ_CHECKPOINT_PATH,
|
| 143 |
+
config: E2FGVIHDConfig = E2FGVIHDConfig(),
|
| 144 |
+
):
|
| 145 |
+
with nvtx("cleaner_init_total"):
|
| 146 |
+
with nvtx("ensure_model_downloaded"):
|
| 147 |
+
ensure_model_downloaded(ckpt_path, E2FGVI_HQ_CHECKPOINT_REMOTE_URL)
|
| 148 |
+
|
| 149 |
+
with nvtx("init_model"):
|
| 150 |
+
self.model = ProfileInpaintGenerator().to(device)
|
| 151 |
+
|
| 152 |
+
with nvtx("load_ckpt"):
|
| 153 |
+
state = torch.load(ckpt_path, map_location=device)
|
| 154 |
+
self.model.load_state_dict(state)
|
| 155 |
+
|
| 156 |
+
with nvtx("model_eval_mode"):
|
| 157 |
+
self.model.eval()
|
| 158 |
+
|
| 159 |
+
self.config = config
|
| 160 |
+
|
| 161 |
+
def clean(self, frames: np.ndarray, masks: np.ndarray) -> List[np.ndarray]:
|
| 162 |
+
"""
|
| 163 |
+
Run the full cleaning pipeline on a video using chunked, overlapping processing and return reconstructed frames.
|
| 164 |
+
|
| 165 |
+
Processes the input frames and masks in configurable chunks with overlap: converts inputs to tensors, runs per-chunk inpainting and fusion, merges chunk outputs handling overlaps, and returns the final list of cleaned frames in original order.
|
| 166 |
+
|
| 167 |
+
Parameters:
|
| 168 |
+
frames (np.ndarray): Sequence of input RGB frames as a numpy array of shape (T, H, W, C) with values in [0, 255] or [0,1].
|
| 169 |
+
masks (np.ndarray): Corresponding mask array of shape (T, H, W) where nonzero values indicate regions to inpaint.
|
| 170 |
+
|
| 171 |
+
Returns:
|
| 172 |
+
List[np.ndarray]: List of T reconstructed RGB frames as numpy arrays (H, W, C), in the same order as the input.
|
| 173 |
+
"""
|
| 174 |
+
with nvtx("ProfileE2FGVIHDCleaner.clean_total"):
|
| 175 |
+
with nvtx("setup_basic_params"):
|
| 176 |
+
video_length = len(frames)
|
| 177 |
+
chunk_size = int(self.config.chunk_size_ratio * video_length)
|
| 178 |
+
overlap_size = int(self.config.overlap_ratio * video_length)
|
| 179 |
+
num_chunks = int(np.ceil(video_length / (chunk_size - overlap_size)))
|
| 180 |
+
h, w = frames[0].shape[:2]
|
| 181 |
+
|
| 182 |
+
with nvtx("numpy_to_tensor"):
|
| 183 |
+
imgs_all, masks_all = numpy_to_tensor(frames, masks)
|
| 184 |
+
|
| 185 |
+
with nvtx("prepare_binary_masks"):
|
| 186 |
+
binary_masks = np.expand_dims(masks > 0, axis=-1).astype(np.uint8)
|
| 187 |
+
|
| 188 |
+
comp_frames = [None] * video_length
|
| 189 |
+
logger.debug(
|
| 190 |
+
f"Processing {video_length} frames in {num_chunks} chunks "
|
| 191 |
+
f"(chunk_size={chunk_size}, overlap={overlap_size})"
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
for chunk_idx in tqdm(
|
| 195 |
+
range(num_chunks), desc="Chunk", position=0, leave=True
|
| 196 |
+
):
|
| 197 |
+
with nvtx(f"chunk_{chunk_idx:03d}_total"):
|
| 198 |
+
with nvtx("chunk_compute_indices"):
|
| 199 |
+
start_idx = chunk_idx * (chunk_size - overlap_size)
|
| 200 |
+
end_idx = min(start_idx + chunk_size, video_length)
|
| 201 |
+
actual_chunk_size = end_idx - start_idx
|
| 202 |
+
|
| 203 |
+
with nvtx("chunk_extract_and_to_device"):
|
| 204 |
+
imgs_chunk = imgs_all[:, start_idx:end_idx, :, :, :].to(device)
|
| 205 |
+
masks_chunk = masks_all[:, start_idx:end_idx, :, :, :].to(
|
| 206 |
+
device
|
| 207 |
+
)
|
| 208 |
+
frames_np_chunk = frames[start_idx:end_idx]
|
| 209 |
+
binary_masks_chunk = binary_masks[start_idx:end_idx]
|
| 210 |
+
|
| 211 |
+
with nvtx("chunk_process_frames_chunk"):
|
| 212 |
+
comp_frames_chunk = self.process_frames_chunk(
|
| 213 |
+
actual_chunk_size,
|
| 214 |
+
self.config.neighbor_stride,
|
| 215 |
+
imgs_chunk,
|
| 216 |
+
masks_chunk,
|
| 217 |
+
binary_masks_chunk,
|
| 218 |
+
frames_np_chunk,
|
| 219 |
+
h,
|
| 220 |
+
w,
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
with nvtx("merge_frames_with_overlap"):
|
| 224 |
+
comp_frames = merge_frames_with_overlap(
|
| 225 |
+
result_frames=comp_frames,
|
| 226 |
+
chunk_frames=comp_frames_chunk,
|
| 227 |
+
start_idx=start_idx,
|
| 228 |
+
overlap_size=overlap_size,
|
| 229 |
+
is_first_chunk=(chunk_idx == 0),
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
with nvtx("chunk_cleanup"):
|
| 233 |
+
del imgs_chunk, masks_chunk, comp_frames_chunk
|
| 234 |
+
try:
|
| 235 |
+
torch.cuda.empty_cache()
|
| 236 |
+
except Exception:
|
| 237 |
+
pass
|
| 238 |
+
|
| 239 |
+
return comp_frames
|
| 240 |
+
|
| 241 |
+
# def process_frames_chunk(
|
| 242 |
+
# self,
|
| 243 |
+
# chunk_length: int,
|
| 244 |
+
# neighbor_stride: int,
|
| 245 |
+
# imgs_chunk: torch.Tensor,
|
| 246 |
+
# masks_chunk: torch.Tensor,
|
| 247 |
+
# binary_masks_chunk: np.ndarray,
|
| 248 |
+
# frames_np_chunk: np.ndarray,
|
| 249 |
+
# h: int,
|
| 250 |
+
# w: int,
|
| 251 |
+
# ) -> List[np.ndarray]:
|
| 252 |
+
# """
|
| 253 |
+
# Compose inpainted frames for a chunk by running the model on sliding windows, blending predictions back into original frames.
|
| 254 |
+
|
| 255 |
+
# Parameters:
|
| 256 |
+
# chunk_length (int): Number of frames in the current chunk.
|
| 257 |
+
# neighbor_stride (int): Half-window radius (in frames) used to select neighboring frames around each reference; determines step between processed reference frames.
|
| 258 |
+
# imgs_chunk (torch.Tensor): Tensor of shape (1, T, C, H, W) containing chunk frames normalized for model input.
|
| 259 |
+
# masks_chunk (torch.Tensor): Tensor of shape (1, T, 1, H, W) containing corresponding masks where masked regions are 1.
|
| 260 |
+
# binary_masks_chunk (np.ndarray): Array of per-frame binary masks (H, W) or (H, W, 1) used for compositing predictions onto original frames (values 0/1).
|
| 261 |
+
# frames_np_chunk (np.ndarray): Original chunk frames as uint8 numpy arrays in shape (T, H, W, C).
|
| 262 |
+
# h (int): Original frame height.
|
| 263 |
+
# w (int): Original frame width.
|
| 264 |
+
|
| 265 |
+
# Returns:
|
| 266 |
+
# List[np.ndarray]: A list of length `chunk_length` where each entry is the reconstructed uint8 RGB frame with model predictions composited into unmasked regions; overlapping predictions are averaged.
|
| 267 |
+
|
| 268 |
+
# Raises:
|
| 269 |
+
# RuntimeError: Intentionally raises RuntimeError("Stop here") to terminate profiling at the profiling breakpoint.
|
| 270 |
+
# """
|
| 271 |
+
# comp_frames_chunk = [None] * chunk_length
|
| 272 |
+
|
| 273 |
+
# for f in tqdm(
|
| 274 |
+
# range(0, chunk_length, neighbor_stride),
|
| 275 |
+
# desc=f" Frame progress",
|
| 276 |
+
# position=1,
|
| 277 |
+
# leave=False,
|
| 278 |
+
# ):
|
| 279 |
+
# with nvtx(f"window_f_{f:05d}_total"):
|
| 280 |
+
# with nvtx("window_neighbor_ref_ids"):
|
| 281 |
+
# neighbor_ids = [
|
| 282 |
+
# i
|
| 283 |
+
# for i in range(
|
| 284 |
+
# max(0, f - neighbor_stride),
|
| 285 |
+
# min(chunk_length, f + neighbor_stride + 1),
|
| 286 |
+
# )
|
| 287 |
+
# ]
|
| 288 |
+
# ref_ids = get_ref_index(
|
| 289 |
+
# f,
|
| 290 |
+
# neighbor_ids,
|
| 291 |
+
# chunk_length,
|
| 292 |
+
# self.config.ref_length,
|
| 293 |
+
# self.config.num_ref,
|
| 294 |
+
# )
|
| 295 |
+
|
| 296 |
+
# with nvtx("window_select_tensors"):
|
| 297 |
+
# selected_imgs = imgs_chunk[:1, neighbor_ids + ref_ids, :, :, :]
|
| 298 |
+
# selected_masks = masks_chunk[:1, neighbor_ids + ref_ids, :, :, :]
|
| 299 |
+
|
| 300 |
+
# with torch.no_grad():
|
| 301 |
+
# with nvtx("window_apply_mask"):
|
| 302 |
+
# masked_imgs = selected_imgs * (1 - selected_masks)
|
| 303 |
+
|
| 304 |
+
# with nvtx("window_pad_flip_concat"):
|
| 305 |
+
# mod_size_h = 60
|
| 306 |
+
# mod_size_w = 108
|
| 307 |
+
# h_pad = (mod_size_h - h % mod_size_h) % mod_size_h
|
| 308 |
+
# w_pad = (mod_size_w - w % mod_size_w) % mod_size_w
|
| 309 |
+
|
| 310 |
+
# masked_imgs = torch.cat(
|
| 311 |
+
# [masked_imgs, torch.flip(masked_imgs, [3])], 3
|
| 312 |
+
# )[:, :, :, : h + h_pad, :]
|
| 313 |
+
|
| 314 |
+
# masked_imgs = torch.cat(
|
| 315 |
+
# [masked_imgs, torch.flip(masked_imgs, [4])], 4
|
| 316 |
+
# )[:, :, :, :, : w + w_pad]
|
| 317 |
+
|
| 318 |
+
# with nvtx("window_model_infer"):
|
| 319 |
+
# pred_imgs, _ = self.model(masked_imgs, len(neighbor_ids))
|
| 320 |
+
|
| 321 |
+
# with nvtx("window_crop_postprocess"):
|
| 322 |
+
# pred_imgs = pred_imgs[:, :, :h, :w]
|
| 323 |
+
# pred_imgs = (pred_imgs + 1) / 2
|
| 324 |
+
# pred_imgs = pred_imgs.cpu().permute(0, 2, 3, 1).numpy() * 255
|
| 325 |
+
|
| 326 |
+
# with nvtx("window_composite_back_to_frames"):
|
| 327 |
+
# for i in range(len(neighbor_ids)):
|
| 328 |
+
# idx = neighbor_ids[i]
|
| 329 |
+
# img = np.array(pred_imgs[i]).astype(
|
| 330 |
+
# np.uint8
|
| 331 |
+
# ) * binary_masks_chunk[idx] + frames_np_chunk[idx] * (
|
| 332 |
+
# 1 - binary_masks_chunk[idx]
|
| 333 |
+
# )
|
| 334 |
+
|
| 335 |
+
# if comp_frames_chunk[idx] is None:
|
| 336 |
+
# comp_frames_chunk[idx] = img
|
| 337 |
+
# else:
|
| 338 |
+
# comp_frames_chunk[idx] = (
|
| 339 |
+
# comp_frames_chunk[idx].astype(np.float32) * 0.5
|
| 340 |
+
# + img.astype(np.float32) * 0.5
|
| 341 |
+
# )
|
| 342 |
+
|
| 343 |
+
# # 你用来中断 profiling 的断点,保留
|
| 344 |
+
# # raise RuntimeError("Stop here")
|
| 345 |
+
# return comp_frames_chunk
|
| 346 |
+
|
| 347 |
+
def process_frames_chunk(
|
| 348 |
+
self,
|
| 349 |
+
chunk_length: int,
|
| 350 |
+
neighbor_stride: int,
|
| 351 |
+
imgs_chunk: torch.Tensor,
|
| 352 |
+
masks_chunk: torch.Tensor,
|
| 353 |
+
binary_masks_chunk: np.ndarray,
|
| 354 |
+
frames_np_chunk: np.ndarray,
|
| 355 |
+
h: int,
|
| 356 |
+
w: int,
|
| 357 |
+
) -> List[np.ndarray]:
|
| 358 |
+
comp_frames_chunk = [None] * chunk_length
|
| 359 |
+
|
| 360 |
+
# 创建用于数据传输的 stream
|
| 361 |
+
transfer_stream = torch.cuda.Stream()
|
| 362 |
+
|
| 363 |
+
# 用于存储上一轮的结果(异步传输中)
|
| 364 |
+
prev_pred_imgs_cpu = None
|
| 365 |
+
prev_neighbor_ids = None
|
| 366 |
+
|
| 367 |
+
all_windows = list(range(0, chunk_length, neighbor_stride))
|
| 368 |
+
|
| 369 |
+
for window_idx, f in enumerate(
|
| 370 |
+
tqdm(
|
| 371 |
+
all_windows,
|
| 372 |
+
desc=f" Frame progress",
|
| 373 |
+
position=1,
|
| 374 |
+
leave=False,
|
| 375 |
+
)
|
| 376 |
+
):
|
| 377 |
+
with nvtx(f"window_f_{f:05d}_total"):
|
| 378 |
+
# ============ 准备当前窗口数据 ============
|
| 379 |
+
with nvtx("window_neighbor_ref_ids"):
|
| 380 |
+
neighbor_ids = [
|
| 381 |
+
i
|
| 382 |
+
for i in range(
|
| 383 |
+
max(0, f - neighbor_stride),
|
| 384 |
+
min(chunk_length, f + neighbor_stride + 1),
|
| 385 |
+
)
|
| 386 |
+
]
|
| 387 |
+
ref_ids = get_ref_index(
|
| 388 |
+
f,
|
| 389 |
+
neighbor_ids,
|
| 390 |
+
chunk_length,
|
| 391 |
+
self.config.ref_length,
|
| 392 |
+
self.config.num_ref,
|
| 393 |
+
)
|
| 394 |
+
|
| 395 |
+
with nvtx("window_select_tensors"):
|
| 396 |
+
selected_imgs = imgs_chunk[:1, neighbor_ids + ref_ids, :, :, :]
|
| 397 |
+
selected_masks = masks_chunk[:1, neighbor_ids + ref_ids, :, :, :]
|
| 398 |
+
|
| 399 |
+
with torch.no_grad():
|
| 400 |
+
with nvtx("window_apply_mask"):
|
| 401 |
+
masked_imgs = selected_imgs * (1 - selected_masks)
|
| 402 |
+
|
| 403 |
+
with nvtx("window_pad_flip_concat"):
|
| 404 |
+
mod_size_h = 60
|
| 405 |
+
mod_size_w = 108
|
| 406 |
+
h_pad = (mod_size_h - h % mod_size_h) % mod_size_h
|
| 407 |
+
w_pad = (mod_size_w - w % mod_size_w) % mod_size_w
|
| 408 |
+
|
| 409 |
+
masked_imgs = torch.cat(
|
| 410 |
+
[masked_imgs, torch.flip(masked_imgs, [3])], 3
|
| 411 |
+
)[:, :, :, : h + h_pad, :]
|
| 412 |
+
|
| 413 |
+
masked_imgs = torch.cat(
|
| 414 |
+
[masked_imgs, torch.flip(masked_imgs, [4])], 4
|
| 415 |
+
)[:, :, :, :, : w + w_pad]
|
| 416 |
+
|
| 417 |
+
# ============ 模型推理 (默认 stream) ============
|
| 418 |
+
with nvtx("window_model_infer"):
|
| 419 |
+
pred_imgs, _ = self.model(masked_imgs, len(neighbor_ids))
|
| 420 |
+
|
| 421 |
+
# ============ GPU 上的后处理 ============
|
| 422 |
+
with nvtx("window_crop_postprocess_gpu"):
|
| 423 |
+
pred_imgs = pred_imgs[:, :, :h, :w]
|
| 424 |
+
pred_imgs = (pred_imgs + 1) / 2
|
| 425 |
+
pred_imgs = pred_imgs.permute(0, 2, 3, 1) * 255
|
| 426 |
+
|
| 427 |
+
# 记录当前计算完成的事件
|
| 428 |
+
compute_done = torch.cuda.Event()
|
| 429 |
+
compute_done.record()
|
| 430 |
+
|
| 431 |
+
# ============ 处理上一轮的结果 (如果有) ============
|
| 432 |
+
if prev_pred_imgs_cpu is not None:
|
| 433 |
+
with nvtx("window_composite_prev"):
|
| 434 |
+
# 等待上一轮传输完成
|
| 435 |
+
transfer_stream.synchronize()
|
| 436 |
+
|
| 437 |
+
# 在 CPU 上合成上一轮的帧
|
| 438 |
+
self._composite_frames(
|
| 439 |
+
prev_pred_imgs_cpu,
|
| 440 |
+
prev_neighbor_ids,
|
| 441 |
+
binary_masks_chunk,
|
| 442 |
+
frames_np_chunk,
|
| 443 |
+
comp_frames_chunk,
|
| 444 |
+
)
|
| 445 |
+
|
| 446 |
+
# ============ 异步传输当前结果到 CPU ============
|
| 447 |
+
with nvtx("window_async_transfer"):
|
| 448 |
+
# 确保计算完成后再传输
|
| 449 |
+
transfer_stream.wait_event(compute_done)
|
| 450 |
+
|
| 451 |
+
with torch.cuda.stream(transfer_stream):
|
| 452 |
+
# 使用 non_blocking=True 异步传输
|
| 453 |
+
# 先转到 pinned memory 的 tensor
|
| 454 |
+
pred_imgs_cpu = pred_imgs.cpu().numpy()
|
| 455 |
+
|
| 456 |
+
# 保存给下一轮处理
|
| 457 |
+
prev_pred_imgs_cpu = pred_imgs_cpu
|
| 458 |
+
prev_neighbor_ids = neighbor_ids.copy()
|
| 459 |
+
|
| 460 |
+
# ============ 处理最后一轮的结果 ============
|
| 461 |
+
if prev_pred_imgs_cpu is not None:
|
| 462 |
+
transfer_stream.synchronize()
|
| 463 |
+
self._composite_frames(
|
| 464 |
+
prev_pred_imgs_cpu,
|
| 465 |
+
prev_neighbor_ids,
|
| 466 |
+
binary_masks_chunk,
|
| 467 |
+
frames_np_chunk,
|
| 468 |
+
comp_frames_chunk,
|
| 469 |
+
)
|
| 470 |
+
|
| 471 |
+
return comp_frames_chunk
|
| 472 |
+
|
| 473 |
+
def _composite_frames(
|
| 474 |
+
self,
|
| 475 |
+
pred_imgs_np: np.ndarray,
|
| 476 |
+
neighbor_ids: List[int],
|
| 477 |
+
binary_masks_chunk: np.ndarray,
|
| 478 |
+
frames_np_chunk: np.ndarray,
|
| 479 |
+
comp_frames_chunk: List[np.ndarray],
|
| 480 |
+
):
|
| 481 |
+
"""将预测结果合成到原始帧上"""
|
| 482 |
+
for i in range(len(neighbor_ids)):
|
| 483 |
+
idx = neighbor_ids[i]
|
| 484 |
+
img = np.array(pred_imgs_np[i]).astype(np.uint8) * binary_masks_chunk[
|
| 485 |
+
idx
|
| 486 |
+
] + frames_np_chunk[idx] * (1 - binary_masks_chunk[idx])
|
| 487 |
+
|
| 488 |
+
if comp_frames_chunk[idx] is None:
|
| 489 |
+
comp_frames_chunk[idx] = img
|
| 490 |
+
else:
|
| 491 |
+
comp_frames_chunk[idx] = (
|
| 492 |
+
comp_frames_chunk[idx].astype(np.float32) * 0.5
|
| 493 |
+
+ img.astype(np.float32) * 0.5
|
| 494 |
+
)
|
| 495 |
+
|
| 496 |
+
|
| 497 |
+
if __name__ == "__main__":
|
| 498 |
+
CMD = Path.cwd() / "profiling"
|
| 499 |
+
|
| 500 |
+
masks_npy_path = CMD / "masks.npy"
|
| 501 |
+
frames_npy_path = CMD / "frames.npy"
|
| 502 |
+
|
| 503 |
+
with nvtx("load_numpy_inputs"):
|
| 504 |
+
masks = np.load(masks_npy_path)
|
| 505 |
+
frames = np.load(frames_npy_path)
|
| 506 |
+
|
| 507 |
+
with nvtx("init_cleaner"):
|
| 508 |
+
cleaner = ProfileE2FGVIHDCleaner()
|
| 509 |
+
|
| 510 |
+
with nvtx("run_cleaner"):
|
| 511 |
+
cleaned_frames = cleaner.clean(frames, masks)
|
| 512 |
+
|
| 513 |
+
# np.save(CMD / "cleaned_frames.npy", cleaned_frames)
|
profile/run_whole.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from contextlib import contextmanager
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from typing import Callable
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
from loguru import logger
|
| 7 |
+
from torch.cuda.nvtx import range_pop, range_push
|
| 8 |
+
from tqdm import tqdm
|
| 9 |
+
|
| 10 |
+
import ffmpeg
|
| 11 |
+
from sorawm.core import SoraWM
|
| 12 |
+
from sorawm.schemas import CleanerType
|
| 13 |
+
from sorawm.utils.imputation_utils import (
|
| 14 |
+
find_2d_data_bkps,
|
| 15 |
+
find_idxs_interval,
|
| 16 |
+
get_interval_average_bbox,
|
| 17 |
+
)
|
| 18 |
+
from sorawm.utils.video_utils import VideoLoader, merge_frames_with_overlap
|
| 19 |
+
from sorawm.watermark_cleaner import WaterMarkCleaner
|
| 20 |
+
from sorawm.watermark_detector import SoraWaterMarkDetector
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@contextmanager
|
| 24 |
+
def nvtx(msg: str):
|
| 25 |
+
range_push(msg)
|
| 26 |
+
try:
|
| 27 |
+
yield
|
| 28 |
+
finally:
|
| 29 |
+
range_pop()
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class ProfileSoraWM(SoraWM):
|
| 33 |
+
def run(
|
| 34 |
+
self,
|
| 35 |
+
input_video_path: Path,
|
| 36 |
+
output_video_path: Path,
|
| 37 |
+
progress_callback: Callable[[int], None] | None = None,
|
| 38 |
+
quiet: bool = False,
|
| 39 |
+
):
|
| 40 |
+
"""
|
| 41 |
+
Run the watermark detection and removal pipeline on an input video and write the processed video (with audio merged) to the given output path.
|
| 42 |
+
|
| 43 |
+
Detects watermark bounding boxes per frame, fills missing detections by interval averaging or neighboring frames, processes the video in breakpoint-based segments with overlap using the configured cleaner, encodes the cleaned frames to an intermediate video file, then merges the original audio into the final output.
|
| 44 |
+
|
| 45 |
+
Parameters:
|
| 46 |
+
input_video_path (Path): Path to the source video to process.
|
| 47 |
+
output_video_path (Path): Path where the final video with merged audio will be written.
|
| 48 |
+
progress_callback (Callable[[int], None] | None): Optional callback invoked periodically with an integer progress percentage (0–100). Progress values generally advance through detection and cleaning phases and report a final near-completion value before audio merge.
|
| 49 |
+
quiet (bool): If True, suppresses progress bar and most debug logging.
|
| 50 |
+
"""
|
| 51 |
+
with nvtx("ProfileSoraWM.run"):
|
| 52 |
+
with nvtx("init video loader"):
|
| 53 |
+
input_video_loader = VideoLoader(input_video_path)
|
| 54 |
+
width = input_video_loader.width
|
| 55 |
+
height = input_video_loader.height
|
| 56 |
+
fps = input_video_loader.fps
|
| 57 |
+
total_frames = input_video_loader.total_frames
|
| 58 |
+
|
| 59 |
+
temp_output_path = (
|
| 60 |
+
output_video_path.parent / f"temp_{output_video_path.name}"
|
| 61 |
+
)
|
| 62 |
+
output_options = {
|
| 63 |
+
"pix_fmt": "yuv420p",
|
| 64 |
+
"vcodec": "libx264",
|
| 65 |
+
"preset": "slow",
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
if input_video_loader.original_bitrate:
|
| 69 |
+
output_options["video_bitrate"] = str(
|
| 70 |
+
int(int(input_video_loader.original_bitrate) * 1.2)
|
| 71 |
+
)
|
| 72 |
+
else:
|
| 73 |
+
output_options["crf"] = "18"
|
| 74 |
+
|
| 75 |
+
process_out = (
|
| 76 |
+
ffmpeg.input(
|
| 77 |
+
"pipe:",
|
| 78 |
+
format="rawvideo",
|
| 79 |
+
pix_fmt="bgr24",
|
| 80 |
+
s=f"{width}x{height}",
|
| 81 |
+
r=fps,
|
| 82 |
+
)
|
| 83 |
+
.output(str(temp_output_path), **output_options)
|
| 84 |
+
.overwrite_output()
|
| 85 |
+
.global_args("-loglevel", "error")
|
| 86 |
+
.run_async(pipe_stdin=True)
|
| 87 |
+
)
|
| 88 |
+
range_push("detect watermarks")
|
| 89 |
+
frame_bboxes = {}
|
| 90 |
+
detect_missed = []
|
| 91 |
+
bbox_centers = []
|
| 92 |
+
bboxes = []
|
| 93 |
+
|
| 94 |
+
if not quiet:
|
| 95 |
+
logger.debug(
|
| 96 |
+
f"total frames: {total_frames}, fps: {fps}, width: {width}, height: {height}"
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
for idx, frame in enumerate(
|
| 100 |
+
tqdm(
|
| 101 |
+
input_video_loader,
|
| 102 |
+
total=total_frames,
|
| 103 |
+
desc="Detect watermarks",
|
| 104 |
+
disable=quiet,
|
| 105 |
+
)
|
| 106 |
+
):
|
| 107 |
+
detection_result = self.detector.detect(frame)
|
| 108 |
+
if detection_result["detected"]:
|
| 109 |
+
frame_bboxes[idx] = {"bbox": detection_result["bbox"]}
|
| 110 |
+
x1, y1, x2, y2 = detection_result["bbox"]
|
| 111 |
+
bbox_centers.append((int((x1 + x2) / 2), int((y1 + y2) / 2)))
|
| 112 |
+
bboxes.append((x1, y1, x2, y2))
|
| 113 |
+
else:
|
| 114 |
+
frame_bboxes[idx] = {"bbox": None}
|
| 115 |
+
detect_missed.append(idx)
|
| 116 |
+
bbox_centers.append(None)
|
| 117 |
+
bboxes.append(None)
|
| 118 |
+
|
| 119 |
+
if progress_callback and idx % 10 == 0:
|
| 120 |
+
progress = 10 + int((idx / total_frames) * 40)
|
| 121 |
+
progress_callback(progress)
|
| 122 |
+
|
| 123 |
+
if not quiet:
|
| 124 |
+
logger.debug(f"detect missed frames: {detect_missed}")
|
| 125 |
+
|
| 126 |
+
range_pop()
|
| 127 |
+
range_push("find bkps")
|
| 128 |
+
bkps_full = [0, total_frames]
|
| 129 |
+
if detect_missed:
|
| 130 |
+
bkps = find_2d_data_bkps(bbox_centers)
|
| 131 |
+
bkps_full = [0] + bkps + [total_frames]
|
| 132 |
+
|
| 133 |
+
interval_bboxes = get_interval_average_bbox(bboxes, bkps_full)
|
| 134 |
+
missed_intervals = find_idxs_interval(detect_missed, bkps_full)
|
| 135 |
+
|
| 136 |
+
for missed_idx, interval_idx in zip(detect_missed, missed_intervals):
|
| 137 |
+
if (
|
| 138 |
+
interval_idx < len(interval_bboxes)
|
| 139 |
+
and interval_bboxes[interval_idx] is not None
|
| 140 |
+
):
|
| 141 |
+
frame_bboxes[missed_idx]["bbox"] = interval_bboxes[interval_idx]
|
| 142 |
+
if not quiet:
|
| 143 |
+
logger.debug(
|
| 144 |
+
f"Filled missed frame {missed_idx} with bbox:\n"
|
| 145 |
+
f" {interval_bboxes[interval_idx]}"
|
| 146 |
+
)
|
| 147 |
+
else:
|
| 148 |
+
before = max(missed_idx - 1, 0)
|
| 149 |
+
after = min(missed_idx + 1, total_frames - 1)
|
| 150 |
+
before_box = frame_bboxes[before]["bbox"]
|
| 151 |
+
after_box = frame_bboxes[after]["bbox"]
|
| 152 |
+
if before_box:
|
| 153 |
+
frame_bboxes[missed_idx]["bbox"] = before_box
|
| 154 |
+
elif after_box:
|
| 155 |
+
frame_bboxes[missed_idx]["bbox"] = after_box
|
| 156 |
+
else:
|
| 157 |
+
del bboxes, bbox_centers, detect_missed
|
| 158 |
+
range_pop()
|
| 159 |
+
range_push("remove watermarks")
|
| 160 |
+
|
| 161 |
+
if self.cleaner_type == CleanerType.LAMA:
|
| 162 |
+
raise NotImplementedError("Lama cleaner is not implemented yet.")
|
| 163 |
+
|
| 164 |
+
elif self.cleaner_type == CleanerType.E2FGVI_HQ:
|
| 165 |
+
input_video_loader = VideoLoader(input_video_path)
|
| 166 |
+
frame_counter = 0
|
| 167 |
+
overlap_ratio = self.cleaner.config.overlap_ratio
|
| 168 |
+
all_cleaned_frames = None
|
| 169 |
+
num_segments = len(bkps_full) - 1
|
| 170 |
+
|
| 171 |
+
for segment_idx in range(num_segments):
|
| 172 |
+
# with nvtx(f"process segment {segment_idx}"):
|
| 173 |
+
range_push(f"process segment {segment_idx}")
|
| 174 |
+
seg_start = bkps_full[segment_idx]
|
| 175 |
+
seg_end = bkps_full[segment_idx + 1]
|
| 176 |
+
seg_length = seg_end - seg_start
|
| 177 |
+
segment_overlap = max(1, int(overlap_ratio * seg_length))
|
| 178 |
+
start = seg_start
|
| 179 |
+
end = seg_end
|
| 180 |
+
|
| 181 |
+
if segment_idx > 0:
|
| 182 |
+
start = max(
|
| 183 |
+
seg_start - segment_overlap,
|
| 184 |
+
bkps_full[segment_idx - 1],
|
| 185 |
+
)
|
| 186 |
+
if segment_idx < num_segments - 1:
|
| 187 |
+
end = min(
|
| 188 |
+
seg_end + segment_overlap,
|
| 189 |
+
bkps_full[segment_idx + 2],
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
if not quiet:
|
| 193 |
+
logger.debug(
|
| 194 |
+
f"Segment {segment_idx}: original=[{seg_start}, {seg_end}), "
|
| 195 |
+
f"with_overlap=[{start}, {end}), overlap={segment_overlap}"
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
frames = np.array(input_video_loader.get_slice(start, end))
|
| 199 |
+
frames = frames[:, :, :, ::-1].copy()
|
| 200 |
+
|
| 201 |
+
masks = np.zeros((len(frames), height, width), dtype=np.uint8)
|
| 202 |
+
for idx in range(start, end):
|
| 203 |
+
bbox = frame_bboxes[idx]["bbox"]
|
| 204 |
+
if bbox is not None:
|
| 205 |
+
x1, y1, x2, y2 = bbox
|
| 206 |
+
idx_offset = idx - start
|
| 207 |
+
masks[idx_offset][y1:y2, x1:x2] = 255
|
| 208 |
+
|
| 209 |
+
# with nvtx(f"clean frames [{start},{end})"):
|
| 210 |
+
range_push(f"clean frames [{start},{end})")
|
| 211 |
+
# masks_npy_path = Path("masks.npy")
|
| 212 |
+
# frames_np_path = Path("frames.npy")
|
| 213 |
+
# np.save(masks_npy_path, masks)
|
| 214 |
+
# np.save(frames_np_path, frames)
|
| 215 |
+
# raise Exception("Stop here")
|
| 216 |
+
cleaned_frames = self.cleaner.clean(frames, masks)
|
| 217 |
+
range_pop()
|
| 218 |
+
# with nvtx("merge frames"):
|
| 219 |
+
range_push("merge frames")
|
| 220 |
+
all_cleaned_frames = merge_frames_with_overlap(
|
| 221 |
+
result_frames=all_cleaned_frames,
|
| 222 |
+
chunk_frames=cleaned_frames,
|
| 223 |
+
start_idx=start,
|
| 224 |
+
overlap_size=segment_overlap,
|
| 225 |
+
is_first_chunk=(segment_idx == 0),
|
| 226 |
+
)
|
| 227 |
+
range_pop()
|
| 228 |
+
|
| 229 |
+
# with nvtx("write frames"):
|
| 230 |
+
range_push("write frames")
|
| 231 |
+
write_start = seg_start
|
| 232 |
+
write_end = seg_end
|
| 233 |
+
for write_idx in range(write_start, write_end):
|
| 234 |
+
if (
|
| 235 |
+
write_idx < len(all_cleaned_frames)
|
| 236 |
+
and all_cleaned_frames[write_idx] is not None
|
| 237 |
+
):
|
| 238 |
+
cleaned_frame = all_cleaned_frames[write_idx]
|
| 239 |
+
cleaned_frame_bgr = cleaned_frame[:, :, ::-1]
|
| 240 |
+
process_out.stdin.write(
|
| 241 |
+
cleaned_frame_bgr.astype(np.uint8).tobytes()
|
| 242 |
+
)
|
| 243 |
+
frame_counter += 1
|
| 244 |
+
if progress_callback and frame_counter % 10 == 0:
|
| 245 |
+
progress = 50 + int((frame_counter / total_frames) * 45)
|
| 246 |
+
progress_callback(progress)
|
| 247 |
+
|
| 248 |
+
range_pop()
|
| 249 |
+
range_pop()
|
| 250 |
+
range_pop()
|
| 251 |
+
range_push("finalize ffmpeg")
|
| 252 |
+
process_out.stdin.close()
|
| 253 |
+
process_out.wait()
|
| 254 |
+
|
| 255 |
+
if progress_callback:
|
| 256 |
+
progress_callback(95)
|
| 257 |
+
|
| 258 |
+
range_pop()
|
| 259 |
+
range_push("merge audio track")
|
| 260 |
+
self.merge_audio_track(
|
| 261 |
+
input_video_path, temp_output_path, output_video_path
|
| 262 |
+
)
|
| 263 |
+
range_pop()
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
if __name__ == "__main__":
|
| 267 |
+
input_video_path = Path("resources/dog_vs_sam.mp4")
|
| 268 |
+
output_stem = Path("outputs/sora_watermark_removed")
|
| 269 |
+
|
| 270 |
+
sora_wm = ProfileSoraWM(cleaner_type=CleanerType.E2FGVI_HQ)
|
| 271 |
+
sora_wm.run(
|
| 272 |
+
input_video_path, output_stem.parent / (output_stem.name + "_e2fgvi_hq.mp4")
|
| 273 |
+
)
|
pyproject.toml
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "sorawatermarkcleaner"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Add your description here"
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
requires-python = ">=3.12"
|
| 7 |
+
dependencies = [
|
| 8 |
+
"aiofiles>=24.1.0",
|
| 9 |
+
"aiosqlite>=0.21.0",
|
| 10 |
+
"diffusers>=0.35.1",
|
| 11 |
+
"einops>=0.8.1",
|
| 12 |
+
"fastapi==0.108.0",
|
| 13 |
+
"ffmpeg-python>=0.2.0",
|
| 14 |
+
"fire>=0.7.1",
|
| 15 |
+
"greenlet>=3.2.4",
|
| 16 |
+
"httpx>=0.28.1",
|
| 17 |
+
"huggingface-hub>=0.35.3",
|
| 18 |
+
"jupyter>=1.1.1",
|
| 19 |
+
"loguru>=0.7.3",
|
| 20 |
+
"matplotlib>=3.10.6",
|
| 21 |
+
"mmcv-full>=1.7.2",
|
| 22 |
+
"notebook>=7.4.7",
|
| 23 |
+
"omegaconf>=2.3.0",
|
| 24 |
+
"opencv-python>=4.12.0.88",
|
| 25 |
+
"pandas>=2.3.3",
|
| 26 |
+
"pydantic>=2.11.10",
|
| 27 |
+
"python-multipart>=0.0.20",
|
| 28 |
+
"requests>=2.32.5",
|
| 29 |
+
"rich>=14.2.0",
|
| 30 |
+
"ruptures>=1.1.10",
|
| 31 |
+
"scikit-learn>=1.7.2",
|
| 32 |
+
"sqlalchemy>=2.0.43",
|
| 33 |
+
"streamlit>=1.50.0",
|
| 34 |
+
"torch>=2.5.0",
|
| 35 |
+
"torchvision>=0.20.0",
|
| 36 |
+
"tqdm>=4.67.1",
|
| 37 |
+
"transformers>=4.57.0",
|
| 38 |
+
"ultralytics>=8.3.204",
|
| 39 |
+
"uuid>=1.30",
|
| 40 |
+
"uvicorn>=0.35.0",
|
| 41 |
+
]
|
| 42 |
+
|
| 43 |
+
[tool.pytest.ini_options]
|
| 44 |
+
testpaths = ["tests"]
|
| 45 |
+
python_files = "test_*.py"
|
| 46 |
+
python_classes = "Test*"
|
| 47 |
+
python_functions = "test_*"
|
| 48 |
+
addopts = "-v --tb=short --strict-markers"
|
| 49 |
+
markers = [
|
| 50 |
+
"unit: Unit tests",
|
| 51 |
+
"integration: Integration tests",
|
| 52 |
+
"slow: Slow running tests",
|
| 53 |
+
"gpu: Tests requiring GPU",
|
| 54 |
+
]
|
| 55 |
+
filterwarnings = [
|
| 56 |
+
"ignore::DeprecationWarning",
|
| 57 |
+
"ignore::PendingDeprecationWarning",
|
| 58 |
+
]
|
| 59 |
+
|
| 60 |
+
[tool.uv.extra-build-dependencies]
|
| 61 |
+
mmcv-full = ["setuptools<81", "wheel", "packaging"]
|
| 62 |
+
|
| 63 |
+
[tool.setuptools.packages.find]
|
| 64 |
+
include = ["sorawm*"]
|