孙振宇 commited on
Commit
060fbda
·
0 Parent(s):

Initial HF Spaces deployment

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +258 -0
  2. .python-version +1 -0
  3. .streamlit/config.toml +3 -0
  4. Dockerfile +80 -0
  5. LICENSE +201 -0
  6. README.md +339 -0
  7. app.py +387 -0
  8. benchmark/batch_bench_record.md +16 -0
  9. benchmark/benchmark_batch.py +67 -0
  10. cli.py +430 -0
  11. datasets/make_yolo_images.py +64 -0
  12. docker-compose.yaml +26 -0
  13. docker-entrypoint.sh +13 -0
  14. example.py +22 -0
  15. ffmpeg/README.md +69 -0
  16. frontend/README.md +38 -0
  17. frontend/bun.lock +409 -0
  18. frontend/index.html +13 -0
  19. frontend/jsconfig.json +8 -0
  20. frontend/package.json +25 -0
  21. frontend/public/favicon.ico +0 -0
  22. frontend/src/App.vue +640 -0
  23. frontend/src/assets/base.css +86 -0
  24. frontend/src/assets/logo.svg +1 -0
  25. frontend/src/assets/main.css +35 -0
  26. frontend/src/components/HelloWorld.vue +44 -0
  27. frontend/src/components/TheWelcome.vue +95 -0
  28. frontend/src/components/WelcomeItem.vue +87 -0
  29. frontend/src/components/icons/IconCommunity.vue +7 -0
  30. frontend/src/components/icons/IconDocumentation.vue +7 -0
  31. frontend/src/components/icons/IconEcosystem.vue +7 -0
  32. frontend/src/components/icons/IconSupport.vue +7 -0
  33. frontend/src/components/icons/IconTooling.vue +19 -0
  34. frontend/src/main.js +23 -0
  35. frontend/src/views/Upload.vue +13 -0
  36. frontend/vite.config.js +26 -0
  37. hf_spaces_README.md +27 -0
  38. mds/reward.md +1 -0
  39. model_version.json +1 -0
  40. notebooks/imputation.ipynb +0 -0
  41. one-click-portable.md +26 -0
  42. profile/profile_clean.sh +16 -0
  43. profile/profile_process_chunk.sh +16 -0
  44. profile/profile_process_chunk_async.sh +16 -0
  45. profile/profile_whole_infer.sh +16 -0
  46. profile/run_clean.py +128 -0
  47. profile/run_process_chunk.py +368 -0
  48. profile/run_process_chunk_async.py +513 -0
  49. profile/run_whole.py +273 -0
  50. 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
+ ![image](assests/frontend.png)
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
+ ![](../assests/wechat_reward.jpg)
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*"]