mirror of
https://github.com/D-Ogi/WatermarkRemover-AI.git
synced 2026-10-18 21:00:48 -05:00
Fix Windows GUI, model preparation and portable EXE builds (#90)
Use a local Qt desktop interface on Windows, verify and prepare model downloads with progress and retry, and build versioned portable executables with release checks. Preserve configured model mirrors and validate application-only refreshes. Closes #51.
This commit is contained in:
1 parent
8af9726666
commit
f4ffa1c2b6
72 files changed
+3547
-189
No files matched your search
@@ -26,3 +26,6 @@
|
||||
*.pth binary
|
||||
*.ckpt binary
|
||||
*.safetensors binary
|
||||
|
||||
# Preserve pinned vendor bytes, including upstream license formatting.
|
||||
ui/vendor/** -text -whitespace
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
- name: Check Python correctness
|
||||
run: python -m ruff check .
|
||||
- name: Compile application and tests without importing models
|
||||
run: python -m compileall -q remwm.py remwmgui.py utils.py lama_inpaint tests
|
||||
run: python -m compileall -q remwm.py remwmgui.py desktop_main.py desktop_runtime.py desktop_models.py model_assets.py utils.py lama_inpaint scripts tests
|
||||
- name: Run CPU tests
|
||||
run: python -m pytest
|
||||
- name: Check Bash syntax
|
||||
@@ -99,7 +99,30 @@ jobs:
|
||||
sudo apt-get install -y libegl1 libgl1 libxkbcommon-x11-0 libxcb-cursor0 libxcb-icccm4 libxcb-keysyms1 libxcb-shape0 libxcb-xinerama0 libxcb-randr0 libnss3 libasound2t64
|
||||
- name: Check installed desktop backend (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: xvfb-run -a python scripts/check_webview.py
|
||||
run: xvfb-run -a python scripts/check_desktop.py
|
||||
- name: Check installed desktop backend (Windows/macOS)
|
||||
if: runner.os != 'Linux'
|
||||
run: python scripts/check_webview.py
|
||||
run: python scripts/check_desktop.py
|
||||
|
||||
windows-portable:
|
||||
name: Windows portable executable
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Build independent embedded runtime and executable
|
||||
run: python scripts/build_windows.py --output "${{ runner.temp }}/portable" --cache "${{ runner.temp }}/downloads" --backend cpu --skip-archive
|
||||
- name: Exercise the executable and real offline desktop page
|
||||
shell: pwsh
|
||||
run: |
|
||||
$version = (Get-Content VERSION -Raw).Trim()
|
||||
$bundle = Join-Path '${{ runner.temp }}/portable' "WatermarkRemover-AI-$version-windows-x64-cpu"
|
||||
$process = Start-Process -FilePath (Join-Path $bundle 'WatermarkRemover-AI.exe') -ArgumentList '--check' -WindowStyle Hidden -PassThru
|
||||
$handle = $process.Handle
|
||||
if (-not $process.WaitForExit(120000)) { $process.Kill(); throw 'Portable check timed out' }
|
||||
if ($process.ExitCode -ne 0) { throw "Portable check failed: $($process.ExitCode)" }
|
||||
@@ -0,0 +1,55 @@
|
||||
name: Windows portable packages
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags: ["v*"]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Portable (${{ matrix.backend }})
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 40
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
backend: [cpu, cu126]
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Validate release tag
|
||||
shell: pwsh
|
||||
run: |
|
||||
$version = (Get-Content VERSION -Raw).Trim()
|
||||
if ($env:GITHUB_REF_TYPE -eq 'tag' -and $env:GITHUB_REF_NAME -ne "v$version") { throw 'Tag must match VERSION' }
|
||||
- name: Build folder and archive
|
||||
run: python scripts/build_windows.py --output dist --cache "${{ runner.temp }}/downloads" --backend ${{ matrix.backend }}
|
||||
- name: Verify executable and local GUI
|
||||
shell: pwsh
|
||||
run: |
|
||||
$version = (Get-Content VERSION -Raw).Trim()
|
||||
$name = "WatermarkRemover-AI-$version-windows-x64-${{ matrix.backend }}"
|
||||
$destination = Join-Path $env:RUNNER_TEMP 'extracted portable'
|
||||
python -m zipfile -e "dist/$name.zip" "$destination"
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Archive extraction failed' }
|
||||
$bundle = Join-Path $destination $name
|
||||
$process = Start-Process -FilePath (Join-Path $bundle 'WatermarkRemover-AI.exe') -ArgumentList '--check' -WindowStyle Hidden -PassThru
|
||||
$handle = $process.Handle
|
||||
if (-not $process.WaitForExit(120000)) { $process.Kill(); throw 'Portable check timed out' }
|
||||
if ($process.ExitCode -ne 0) { throw "Portable check failed: $($process.ExitCode)" }
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: windows-${{ matrix.backend }}
|
||||
path: |
|
||||
dist/*.zip
|
||||
dist/*.sha256
|
||||
compression-level: 0
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
@@ -58,3 +58,6 @@ huggingface/
|
||||
.venv-test/
|
||||
.pytest_cache/
|
||||
.ruff_cache/
|
||||
|
||||
node_modules/
|
||||
build-downloads/
|
||||
+2
-2
@@ -22,7 +22,7 @@ CI runs these checks on Windows, Linux, and macOS. It also parses the PowerShell
|
||||
|
||||
These lightweight checks use CPU fixtures. They do not install the full application, launch its GUI, download model weights, or prove CUDA/MPS compatibility. The test dependencies are separate from the application dependencies. A successful `pip check` in the test environment only validates that environment.
|
||||
|
||||
A separate application CI job installs `requirements.txt` on all three platforms with the normal resolver, runs `pip check` in that environment, verifies application imports and CLI help, and tests the adapter using a generated TorchScript fixture. It also checks image and short-video routing without downloading detection models. This job does not prove a GUI interaction or real-model/GPU inference. See [LaMA runtime validation](docs/lama-runtime.md#validation).
|
||||
A separate application CI job installs `requirements.txt` on all three platforms with the normal resolver, runs `pip check` in that environment, verifies application imports and CLI help, and tests the adapter using a generated TorchScript fixture. It also checks image and short-video routing without downloading detection models. This job also opens the real local desktop page and exercises its JavaScript/Python bridge. It does not prove real-model/GPU inference. See [LaMA runtime validation](docs/lama-runtime.md#validation).
|
||||
|
||||
## Evidence required by change type
|
||||
|
||||
@@ -47,7 +47,7 @@ GPU/model integration tests require a separate trusted environment and must not
|
||||
|
||||
Automated review provides suggestions; maintainers verify findings against the code and test evidence. A bot's approval does not replace application testing. Resolve applicable review findings or explain why they do not apply. Maintainers decide when to merge.
|
||||
|
||||
Once the new CI jobs have passed reliably, maintainers can require their status checks through repository rules. Adding this workflow alone does not enable branch protection. Full-manifest installation checks run in the application job; installer-script execution and GUI interaction still require separate validation on affected platforms.
|
||||
Once the new CI jobs have passed reliably, maintainers can require their status checks through repository rules. Adding this workflow alone does not enable branch protection. Full-manifest installation checks run in the application job; installer-script execution and GUI interactions beyond the automated desktop check still require separate validation on affected platforms.
|
||||
|
||||
## Dependency updates
|
||||
|
||||
|
||||
@@ -41,6 +41,12 @@ https://github.com/user-attachments/assets/505be2a8-8eda-4def-90b6-5a4ceefee456
|
||||
|
||||
---
|
||||
|
||||
## Windows portable release
|
||||
|
||||
The versioned Windows package includes an `.exe` launcher, Python and the desktop
|
||||
runtime. See [portable setup, model downloads and troubleshooting](docs/windows-portable.md).
|
||||
The first launch prepares verified models with visible progress and a retry button.
|
||||
|
||||
## Installation
|
||||
|
||||
### Windows
|
||||
|
||||
@@ -26,3 +26,22 @@ asset digest. The runtime uses the pinned SHA-256, not MD5 or an environment
|
||||
variable supplied checksum. This is an integrity pin, not an upstream signature.
|
||||
For model provenance and terms, see [LaMA](https://github.com/advimman/lama)
|
||||
and the artifact publisher. Code licensing does not replace model/data terms.
|
||||
|
||||
## Offline desktop assets
|
||||
|
||||
- Alpine.js 3.13.3: MIT, https://github.com/alpinejs/alpine/tree/v3.13.3 ; license in `ui/vendor/ALPINE-LICENSE.md`.
|
||||
- Tailwind CSS 3.4.17: MIT, https://github.com/tailwindlabs/tailwindcss/tree/v3.4.17 ; generated CSS and license in `ui/vendor`. Rebuild with `npm ci --ignore-scripts` and `npm run build:ui`.
|
||||
- Space Mono, Rubik Glitch, Inter, Cinzel, Dancing Script, Oswald, Playfair Display and Mali: SIL Open Font License 1.1. Font files were obtained from Google Fonts; the corresponding OFL files are included in `ui/vendor`. See https://github.com/google/fonts .
|
||||
- `ui/vendor/manifest.json` records the bundled asset SHA-256 digests.
|
||||
|
||||
The Windows portable package includes CPython under the PSF license and PySide6/Qt
|
||||
under their distributed licenses, including LGPL components. Their license files
|
||||
remain in the Python distribution and installed package metadata. Libraries remain
|
||||
as separate replaceable files. See https://doc.qt.io/qtforpython-6/licenses.html
|
||||
for licensing and corresponding source references. The package also preserves
|
||||
PyTorch and other dependencies' license files in their installed distributions.
|
||||
|
||||
Florence-2 assets are pinned to revision `4271c66b88cdbc05735372ec13b2360108de5317`
|
||||
of `florence-community/Florence-2-large`. `models/florence.json` records sizes and
|
||||
SHA-256 digests checked against the repository's Git/LFS metadata. Model files are
|
||||
downloaded separately, with byte verification before use.
|
||||
@@ -0,0 +1,8 @@
|
||||
"""Windowed entry point for source installs and the portable executable."""
|
||||
from desktop_runtime import configure_runtime
|
||||
|
||||
configure_runtime()
|
||||
|
||||
if __name__ == "__main__":
|
||||
from remwmgui import main
|
||||
main()
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Keep model verification/downloads off the desktop bridge and UI threads."""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
from desktop_runtime import APP_ROOT, python_executable, worker_options
|
||||
|
||||
|
||||
class ModelPreparation:
|
||||
"""One preparation worker per window, with pollable progress and explicit retry."""
|
||||
|
||||
def __init__(self):
|
||||
"""Start idle; callers decide when to check or download model assets."""
|
||||
self._lock = threading.Lock()
|
||||
self._process = None
|
||||
self._running = False
|
||||
self._closed = False
|
||||
self._state = dict(status="unchecked", message="Checking model files...")
|
||||
|
||||
def status(self):
|
||||
"""Return a copy so bridge serialization never races with worker updates."""
|
||||
with self._lock:
|
||||
return dict(self._state, busy=self._running)
|
||||
|
||||
def start(self, *, check=False):
|
||||
"""Start checking or downloading; completed/failed jobs can be retried."""
|
||||
with self._lock:
|
||||
if self._closed:
|
||||
return {"error": "Window is closing"}
|
||||
if self._running:
|
||||
return {"error": "Model preparation is already running"}
|
||||
self._running = True
|
||||
self._state = dict(status="checking", message="Checking model files...")
|
||||
threading.Thread(target=self._run, args=(check,), daemon=True).start()
|
||||
return {"status": "started"}
|
||||
|
||||
def _run(self, check):
|
||||
"""Consume structured progress while keeping errors visible and retryable."""
|
||||
try:
|
||||
cmd = [python_executable(), str(APP_ROOT / "model_assets.py")]
|
||||
if check:
|
||||
cmd.append("--check")
|
||||
with self._lock:
|
||||
if self._closed:
|
||||
return
|
||||
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
**worker_options())
|
||||
self._process = process
|
||||
for line in process.stdout:
|
||||
try:
|
||||
event = json.loads(line)
|
||||
except ValueError:
|
||||
continue
|
||||
if isinstance(event, dict) and "status" in event:
|
||||
with self._lock:
|
||||
self._state = event
|
||||
process.stdout.close()
|
||||
code = process.wait()
|
||||
with self._lock:
|
||||
if code or self._state.get("status") != "ready":
|
||||
if self._state.get("status") not in {"missing", "error"}:
|
||||
self._state = dict(status="error", message=f"Model preparation stopped (exit {code}). Please retry.")
|
||||
except Exception as exc:
|
||||
with self._lock:
|
||||
self._state = dict(status="error", message=str(exc))
|
||||
finally:
|
||||
with self._lock:
|
||||
self._process = None
|
||||
self._running = False
|
||||
|
||||
def close(self):
|
||||
"""Terminate only this window's worker; downloads remain safe to retry."""
|
||||
with self._lock:
|
||||
self._closed = True
|
||||
process = self._process
|
||||
if process and process.poll() is None:
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=3)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
process.wait(timeout=3)
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Paths and subprocess settings shared by source and portable desktop launches."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
APP_ROOT = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def data_dir():
|
||||
"""Keep writable user state outside application files, or beside a portable build."""
|
||||
override = os.environ.get("WMR_DATA_DIR")
|
||||
if override:
|
||||
return Path(override).expanduser().resolve()
|
||||
if (APP_ROOT / "portable.flag").exists():
|
||||
return APP_ROOT / "data"
|
||||
if sys.platform == "win32":
|
||||
return Path(os.environ.get("LOCALAPPDATA", str(Path.home()))) / "WatermarkRemover-AI"
|
||||
return Path(os.environ.get("XDG_DATA_HOME", str(Path.home() / ".local/share"))) / "WatermarkRemover-AI"
|
||||
|
||||
|
||||
def configure_runtime():
|
||||
"""Prepare logs and portable caches before importing the graphical backend."""
|
||||
root = data_dir()
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
if (APP_ROOT / "portable.flag").exists():
|
||||
os.environ.setdefault("TORCH_HOME", str(root / "models/torch"))
|
||||
os.environ.setdefault("HF_HOME", str(root / "models/huggingface"))
|
||||
for name in ("stdout", "stderr"):
|
||||
if getattr(sys, name) is None:
|
||||
setattr(sys, name, (root / "desktop.log").open("a", encoding="utf-8", buffering=1))
|
||||
logging.basicConfig(filename=root / "desktop.log", level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)s %(message)s", encoding="utf-8")
|
||||
|
||||
|
||||
def python_executable():
|
||||
"""Workers need python.exe with real pipes even when the GUI uses pythonw.exe."""
|
||||
executable = Path(sys.executable)
|
||||
if executable.name.lower() == "pythonw.exe":
|
||||
return str(executable.with_name("python.exe"))
|
||||
return str(executable)
|
||||
|
||||
|
||||
def worker_options(*, offline=False):
|
||||
"""Launch bounded local workers without console windows or shell interpretation."""
|
||||
env = os.environ.copy()
|
||||
env.update(PYTHONUNBUFFERED="1", PYTHONIOENCODING="utf-8")
|
||||
if offline:
|
||||
env.update(HF_HUB_OFFLINE="1", TRANSFORMERS_OFFLINE="1")
|
||||
return dict(cwd=str(APP_ROOT), env=env, text=True, encoding="utf-8", errors="replace",
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0)
|
||||
@@ -0,0 +1,102 @@
|
||||
# Windows portable application
|
||||
|
||||
The portable package contains a versioned `WatermarkRemover-AI.exe`, CPython
|
||||
3.13.15, the application dependencies, Qt WebEngine, and the complete offline
|
||||
interface. No system Python, Git, administrator access or WebView2 installation
|
||||
is needed. Extract the whole archive to a writable directory before opening the
|
||||
EXE; do not launch it from inside the archive.
|
||||
|
||||
## First launch
|
||||
|
||||
1. Choose the CPU package for broad hardware compatibility, or the CUDA 12.6
|
||||
package for a compatible NVIDIA GPU/driver. See [device guidance](dependency-migration.md).
|
||||
2. Extract the package and open `WatermarkRemover-AI.exe`.
|
||||
3. Open **Models**, then choose **Download / Retry**. The application checks its
|
||||
pinned LaMA and Florence-2 files, downloads missing/damaged files and shows
|
||||
progress. Model data is about 1.8 GB; allow additional disk space for the
|
||||
application and temporary transfers.
|
||||
4. After **ready** appears, choose input and output paths and process a file.
|
||||
Detection Preview uses the same prepared models.
|
||||
|
||||
A connection failure is shown in the Models panel. Fix the connection or free disk
|
||||
space and use **Download / Retry** again. Completed verified files are reused.
|
||||
Partial Florence-2 transfers resume when the server supports byte ranges; LaMA
|
||||
restarts an interrupted transfer. No downloaded file is accepted before its pinned
|
||||
size and SHA-256 match. Neither application updates nor arbitrary scripts are
|
||||
fetched through this model downloader.
|
||||
|
||||
The UI, translations, themes and fonts work without internet. Processing uses
|
||||
prepared local models. First model preparation requires access to GitHub and
|
||||
Hugging Face, or a preseeded cache. See [offline cache instructions](lama-runtime.md).
|
||||
The Florence-2 manifest and pinned snapshot revision are in `models/florence.json`.
|
||||
Source installers remember the selected Florence mirror for later desktop retries.
|
||||
An explicit `HF_ENDPOINT` takes precedence over the saved `model-endpoint.json` in
|
||||
the application data directory; only HTTPS base URLs without credentials, query
|
||||
strings or fragments are accepted. A mirror never changes the pinned checksums.
|
||||
LaMA still uses its separate GitHub source. To persist a different Florence endpoint:
|
||||
|
||||
```sh
|
||||
python -m model_assets --florence-only --endpoint https://huggingface.co
|
||||
```
|
||||
|
||||
FFmpeg is still optional and must be available on PATH to preserve video audio.
|
||||
The UI reports its availability. The package does not silently install a system
|
||||
codec or driver. CUDA packages do not certify every NVIDIA card or older driver;
|
||||
MPS is not a Windows feature and is outside the current LaMA device contract.
|
||||
|
||||
## Portable state and upgrades
|
||||
|
||||
`data/` beside the executable stores settings, logs and default model caches.
|
||||
Explicit `TORCH_HOME`, `HF_HOME` or Hugging Face cache variables still take
|
||||
precedence. Keep the `data` directory when moving the portable application.
|
||||
For an upgrade, extract into a new directory, close the old application, then copy
|
||||
`data/` into the new directory. Keep the previous version until the new one works.
|
||||
Do not mix dependency folders from different versions or CPU/CUDA packages.
|
||||
|
||||
Source installations use the user's application data directory for new settings
|
||||
and logs, and read an existing project `ui.yml` when no new settings file exists.
|
||||
Existing theme and language choices are retained.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
- `data/desktop.log` records windowed-launch errors. Inspect it before sharing;
|
||||
logs may contain local paths and processing details.
|
||||
- `build-info.json` identifies version, backend, Python artifact and source commit.
|
||||
- `installed-packages.txt` records the exact package versions included in the build.
|
||||
- Launch `WatermarkRemover-AI.exe --check` to exercise the real local page and
|
||||
JavaScript/Python bridge without downloading production models. This is a UI
|
||||
installation check, not proof of GPU or model inference.
|
||||
- For CLI usage, run `python/python.exe remwm.py INPUT OUTPUT` from the extracted
|
||||
directory. Keep cache variables consistent with your GUI cache; in a shell set
|
||||
`TORCH_HOME` to `data/models/torch` and `HF_HOME` to `data/models/huggingface`.
|
||||
|
||||
## Building and release checks
|
||||
|
||||
On Windows, from a clean checkout with a working build Python/pip:
|
||||
|
||||
```powershell
|
||||
python scripts/build_windows.py --output dist --cache build-downloads --backend cpu
|
||||
python scripts/build_windows.py --output dist --cache build-downloads --backend cu126
|
||||
```
|
||||
|
||||
The builder verifies the embedded Python archive, uses the explicit Torch index,
|
||||
checks dependency resolution and imports, compiles the native launcher and emits
|
||||
versioned ZIP/SHA-256 files. Runtime DLLs and dependencies' license files remain
|
||||
separate in the package. Python entry-point wrappers containing build-machine
|
||||
paths are excluded; use `python.exe -m pip` for intentional package maintenance.
|
||||
Never run installation commands into a release candidate after recording its
|
||||
validation evidence. Build diagnostics are not a promise of byte-for-byte
|
||||
reproducible archives: preserve the emitted package inventory and checksum.
|
||||
|
||||
For application-only rebuilds, `--refresh-app` requires matching Python, backend,
|
||||
Torch, torchvision, and pip versions and dependency manifests from the previous build. It replaces
|
||||
application directories so removed files cannot remain. Runtime changes or old
|
||||
build metadata require a fresh output directory. User data and the embedded runtime
|
||||
are preserved during an application-only refresh.
|
||||
|
||||
CI builds the CPU executable and exercises the actual offline desktop page.
|
||||
The Windows portable workflow builds both CPU/CUDA artifacts on a version tag or
|
||||
manual dispatch, without automatically publishing a release. Before publishing,
|
||||
validate the extracted archive from another directory, first model download,
|
||||
connection failure/retry, preview, processing, cancellation and error reporting.
|
||||
Real model/GPU runs stay in a trusted environment outside pull-request CI.
|
||||
@@ -56,7 +56,7 @@ def _is_valid(path):
|
||||
return False
|
||||
|
||||
|
||||
def ensure_model(cache_dir=None, *, download=True):
|
||||
def ensure_model(cache_dir=None, *, download=True, progress=None):
|
||||
"""Return verified weights, downloading atomically when allowed.
|
||||
|
||||
Downloads use a unique temporary file and a per-artifact cross-process lock.
|
||||
@@ -65,6 +65,8 @@ def ensure_model(cache_dir=None, *, download=True):
|
||||
"""
|
||||
cache = Path(cache_dir) if cache_dir is not None else default_cache_dir()
|
||||
target = cache / MODEL_NAME
|
||||
if progress:
|
||||
progress(dict(status="checking", model="LaMA", current=0, total=MODEL_SIZE))
|
||||
if not download:
|
||||
if not _is_valid(target):
|
||||
raise ModelError(
|
||||
@@ -76,13 +78,13 @@ def ensure_model(cache_dir=None, *, download=True):
|
||||
with FileLock(str(target) + ".lock", timeout=600):
|
||||
if _is_valid(target):
|
||||
return target
|
||||
_download(target)
|
||||
_download(target, progress=progress)
|
||||
except Timeout as exc:
|
||||
raise ModelError("Timed out waiting for another LaMA model download.") from exc
|
||||
return target
|
||||
|
||||
|
||||
def _download(target):
|
||||
def _download(target, progress=None):
|
||||
"""Publish only a complete verified transfer; retain the old file on failure."""
|
||||
temporary = None
|
||||
try:
|
||||
@@ -100,6 +102,8 @@ def _download(target):
|
||||
"LaMA download exceeded its size or time limit."
|
||||
)
|
||||
output.write(chunk)
|
||||
if progress:
|
||||
progress(dict(status="downloading", model="LaMA", current=size, total=MODEL_SIZE))
|
||||
output.flush()
|
||||
os.fsync(output.fileno())
|
||||
with temporary.open("rb") as stream:
|
||||
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
"""Pinned Florence assets with verified cache reuse and resumable downloads."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import time
|
||||
import tempfile
|
||||
from urllib.parse import urlsplit
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
from filelock import FileLock
|
||||
|
||||
from desktop_runtime import data_dir
|
||||
|
||||
MANIFEST = json.loads((Path(__file__).parent / "models/florence.json").read_text())
|
||||
FLORENCE_REPO = MANIFEST["repo"]
|
||||
FLORENCE_REVISION = MANIFEST["revision"]
|
||||
|
||||
|
||||
def validate_endpoint(value):
|
||||
"""Accept HTTPS base URLs without credentials, query strings or fragments."""
|
||||
parsed = urlsplit(value)
|
||||
if (parsed.scheme != "https" or not parsed.hostname or parsed.username is not None
|
||||
or parsed.password is not None or parsed.query or parsed.fragment
|
||||
or any(character.isspace() for character in value) or "\\" in value):
|
||||
raise ValueError("The model endpoint must be an HTTPS base URL without credentials, query or fragment.")
|
||||
# Validate explicit port syntax/range even if no download is needed yet.
|
||||
parsed.port
|
||||
return value.rstrip("/")
|
||||
|
||||
|
||||
def save_endpoint(value):
|
||||
"""Persist the installer's selection before downloading, including failed attempts."""
|
||||
endpoint = validate_endpoint(value)
|
||||
root = data_dir()
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
temporary = None
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=root,
|
||||
prefix="model-endpoint-", delete=False) as output:
|
||||
temporary = Path(output.name)
|
||||
json.dump({"endpoint": endpoint}, output)
|
||||
output.flush()
|
||||
os.fsync(output.fileno())
|
||||
os.replace(temporary, root / "model-endpoint.json")
|
||||
finally:
|
||||
if temporary is not None:
|
||||
temporary.unlink(missing_ok=True)
|
||||
return endpoint
|
||||
|
||||
|
||||
def model_endpoint():
|
||||
"""Use an explicit environment override, then the saved selection, then origin."""
|
||||
endpoint = os.environ.get("HF_ENDPOINT")
|
||||
if not endpoint:
|
||||
settings = data_dir() / "model-endpoint.json"
|
||||
endpoint = (json.loads(settings.read_text(encoding="utf-8"))["endpoint"]
|
||||
if settings.exists() else "https://huggingface.co")
|
||||
return validate_endpoint(endpoint)
|
||||
|
||||
|
||||
def florence_snapshot():
|
||||
"""Reuse the standard Hugging Face snapshot directory for the pinned revision."""
|
||||
hf_home = Path(os.environ.get("HF_HOME", str(Path(os.environ.get("XDG_CACHE_HOME", "~/.cache")).expanduser() / "huggingface"))).expanduser()
|
||||
hub = Path(os.environ.get("HF_HUB_CACHE", os.environ.get("HUGGINGFACE_HUB_CACHE", str(hf_home / "hub")))).expanduser()
|
||||
return hub / ("models--" + FLORENCE_REPO.replace("/", "--")) / "snapshots" / FLORENCE_REVISION
|
||||
|
||||
|
||||
def valid_file(path, artifact):
|
||||
"""Validate bytes before publishing or loading an artifact, including cached files."""
|
||||
try:
|
||||
if path.stat().st_size != artifact["size"]:
|
||||
return False
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as source:
|
||||
while chunk := source.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest() == artifact["sha256"]
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
|
||||
def download_file(url, target, artifact, report):
|
||||
"""Resume only a matching byte range; publish verified bytes atomically.
|
||||
|
||||
Network interruptions retain partial bytes for retry. A checksum failure
|
||||
removes the partial file so retry cannot repeatedly reuse corrupt bytes.
|
||||
Callers hold the per-model file lock throughout validation and download.
|
||||
"""
|
||||
partial = target.with_name(target.name + ".part")
|
||||
offset = partial.stat().st_size if partial.exists() else 0
|
||||
if offset >= artifact["size"]:
|
||||
if valid_file(partial, artifact):
|
||||
os.replace(partial, target)
|
||||
return
|
||||
partial.unlink()
|
||||
offset = 0
|
||||
headers = {"Range": f"bytes={offset}-", "Accept-Encoding": "identity"} if offset else {"Accept-Encoding": "identity"}
|
||||
deadline = time.monotonic() + 3600
|
||||
with urlopen(Request(url, headers=headers), timeout=30) as response:
|
||||
if response.status == 206:
|
||||
content_range = response.headers.get("Content-Range", "")
|
||||
expected = f"bytes {offset}-{artifact['size'] - 1}/{artifact['size']}"
|
||||
if content_range != expected:
|
||||
raise RuntimeError("Server returned an unexpected download range; retry the download.")
|
||||
elif response.status == 200:
|
||||
offset = 0 # Server ignored Range: restart instead of appending duplicate bytes.
|
||||
else:
|
||||
raise RuntimeError(f"Unexpected download response: {response.status}")
|
||||
report(offset, artifact["size"])
|
||||
with partial.open("ab" if offset else "wb") as output:
|
||||
while chunk := response.read(1024 * 1024):
|
||||
offset += len(chunk)
|
||||
if offset > artifact["size"] or time.monotonic() > deadline:
|
||||
raise RuntimeError("Model download exceeded its expected size or time limit.")
|
||||
output.write(chunk)
|
||||
report(offset, artifact["size"])
|
||||
output.flush()
|
||||
os.fsync(output.fileno())
|
||||
if partial.stat().st_size < artifact["size"]:
|
||||
raise RuntimeError("Model transfer ended early. Please retry to resume the download.")
|
||||
if not valid_file(partial, artifact):
|
||||
partial.unlink(missing_ok=True)
|
||||
raise RuntimeError("Model download failed its SHA-256 check. Please retry.")
|
||||
os.replace(partial, target)
|
||||
|
||||
|
||||
def ensure_florence(*, download=True, progress=None):
|
||||
"""Return a complete verified pinned snapshot; offline checks never open the network."""
|
||||
download = download and os.environ.get("HF_HUB_OFFLINE", "").upper() not in {"1", "TRUE", "YES"}
|
||||
root = florence_snapshot()
|
||||
if download:
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
def report(status, filename="", current=0, total=0):
|
||||
"""Forward plain progress data to the caller without coupling to a GUI."""
|
||||
if progress:
|
||||
progress(dict(status=status, model="Florence-2", file=filename, current=current, total=total))
|
||||
def prepare():
|
||||
"""Verify every required file and fetch only missing or damaged artifacts."""
|
||||
for artifact in MANIFEST["files"]:
|
||||
name = artifact["name"]
|
||||
target = root / name
|
||||
report("checking", name)
|
||||
if valid_file(target, artifact):
|
||||
continue
|
||||
if not download:
|
||||
raise RuntimeError(f"Florence-2 needs a download or repair: {name}")
|
||||
report("downloading", name, 0, artifact["size"])
|
||||
url = f"{model_endpoint()}/{FLORENCE_REPO}/resolve/{FLORENCE_REVISION}/{name}"
|
||||
download_file(url, target, artifact,
|
||||
lambda current, total: report("downloading", name, current, total))
|
||||
if download:
|
||||
report("waiting")
|
||||
with FileLock(str(root / ".prepare.lock"), timeout=600):
|
||||
prepare()
|
||||
else:
|
||||
prepare()
|
||||
return root
|
||||
|
||||
|
||||
def prepare_models(*, download=True, progress=None):
|
||||
"""Verify and prepare both models before allowing desktop processing."""
|
||||
from lama_inpaint.model_store import ensure_model
|
||||
ensure_model(download=download, progress=progress)
|
||||
ensure_florence(download=download, progress=progress)
|
||||
|
||||
|
||||
def main():
|
||||
"""Emit progress as JSON lines for the isolated desktop preparation worker."""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--check", action="store_true")
|
||||
parser.add_argument("--florence-only", action="store_true")
|
||||
parser.add_argument("--endpoint", help="Persist an HTTPS Florence endpoint for future desktop retries")
|
||||
args = parser.parse_args()
|
||||
def emit(event):
|
||||
"""Flush each event so the GUI can report progress while the worker runs."""
|
||||
print(json.dumps(event), flush=True)
|
||||
try:
|
||||
if args.endpoint is not None:
|
||||
save_endpoint(args.endpoint)
|
||||
os.environ["HF_ENDPOINT"] = args.endpoint
|
||||
prepare = ensure_florence if args.florence_only else prepare_models
|
||||
prepare(download=not args.check, progress=emit)
|
||||
emit(dict(status="ready", message="Models are verified and ready."))
|
||||
except Exception as exc:
|
||||
emit(dict(status="missing" if args.check else "error",
|
||||
message="Model files are missing or need repair. Choose Download / Retry." if args.check else str(exc),
|
||||
detail=str(exc)))
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"repo": "florence-community/Florence-2-large",
|
||||
"revision": "4271c66b88cdbc05735372ec13b2360108de5317",
|
||||
"files": [
|
||||
{
|
||||
"name": "added_tokens.json",
|
||||
"size": 22430,
|
||||
"sha256": "1d75deda84dfa81fb6c09301f3fed00f9695059568bbb1403a6bf299cd84fc37"
|
||||
},
|
||||
{
|
||||
"name": "config.json",
|
||||
"size": 2396,
|
||||
"sha256": "8412483f687f2f71587328a38c6fa70a68d9488f28e90607be3c182641f60f2c"
|
||||
},
|
||||
{
|
||||
"name": "generation_config.json",
|
||||
"size": 292,
|
||||
"sha256": "0251459c49cc358ac033b5d4b8569e61ac22bde5d76be404b97a44cfb33fb12e"
|
||||
},
|
||||
{
|
||||
"name": "merges.txt",
|
||||
"size": 456318,
|
||||
"sha256": "1ce1664773c50f3e0cc8842619a93edc4624525b728b188a9e0be33b7726adc5"
|
||||
},
|
||||
{
|
||||
"name": "model.safetensors",
|
||||
"size": 1553541016,
|
||||
"sha256": "7715423d6549bf1e71188bdd84f4ac960cc0597886af24a5ef7b66f128660685"
|
||||
},
|
||||
{
|
||||
"name": "preprocessor_config.json",
|
||||
"size": 603,
|
||||
"sha256": "1396ec5a0a7adfe1c04fb777b09e8ba753be6dbb5868212ab3c3ef39d91fe031"
|
||||
},
|
||||
{
|
||||
"name": "processor_config.json",
|
||||
"size": 2264,
|
||||
"sha256": "cd0e3bf41a39b1276503fbd273bc03b9afc70d7a15ea681a92a1b4b77f858ee6"
|
||||
},
|
||||
{
|
||||
"name": "special_tokens_map.json",
|
||||
"size": 146627,
|
||||
"sha256": "72ff172dc769bc1551b1b4211628ce3271643bc60379e4da45d85a9be9332c39"
|
||||
},
|
||||
{
|
||||
"name": "tokenizer.json",
|
||||
"size": 3748144,
|
||||
"sha256": "3ad7001f773409abe6bba33eac92662611a73d72f459bda2f00d2a221dd31ce4"
|
||||
},
|
||||
{
|
||||
"name": "tokenizer_config.json",
|
||||
"size": 197922,
|
||||
"sha256": "cb5f80bd9afa767bb1bb798ee5f0a79eae45239b8e72506166b4218175a16723"
|
||||
},
|
||||
{
|
||||
"name": "vocab.json",
|
||||
"size": 798293,
|
||||
"sha256": "ed19656ea1707df69134c4af35c8ceda2cc9860bf2c3495026153a133670ab5e"
|
||||
}
|
||||
]
|
||||
}
|
||||
Generated
+1021
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build:ui": "tailwindcss -i ui/tailwind-input.css -o ui/vendor/tailwind.css --minify"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tailwindcss": "3.4.17"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Windows.Forms;
|
||||
|
||||
[assembly: AssemblyTitle("WatermarkRemover-AI")]
|
||||
[assembly: AssemblyProduct("WatermarkRemover-AI")]
|
||||
[assembly: AssemblyCompany("D-Ogi")]
|
||||
[assembly: AssemblyVersion("__VERSION__.0")]
|
||||
[assembly: AssemblyFileVersion("__VERSION__.0")]
|
||||
|
||||
internal static class Launcher
|
||||
{
|
||||
[STAThread]
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
bool check = args.Length == 1 && args[0] == "--check";
|
||||
string root = AppDomain.CurrentDomain.BaseDirectory;
|
||||
string interpreter = Path.Combine(root, "python", check ? "python.exe" : "pythonw.exe");
|
||||
string script = Path.Combine(root, check ? "scripts/check_desktop.py" : "desktop_main.py");
|
||||
try
|
||||
{
|
||||
if (args.Length > 0 && !check) throw new ArgumentException("Supported argument: --check");
|
||||
if (!File.Exists(interpreter) || !File.Exists(script))
|
||||
throw new FileNotFoundException("Extract the complete portable archive before launching. Keep the EXE beside its python and ui folders.");
|
||||
ProcessStartInfo start = new ProcessStartInfo(interpreter, "\"" + script + "\"");
|
||||
start.WorkingDirectory = root;
|
||||
start.UseShellExecute = false;
|
||||
start.CreateNoWindow = true;
|
||||
start.EnvironmentVariables["PYTHONIOENCODING"] = "utf-8";
|
||||
start.EnvironmentVariables.Remove("PYTHONHOME");
|
||||
start.EnvironmentVariables.Remove("PYTHONPATH");
|
||||
using (Process child = Process.Start(start))
|
||||
{
|
||||
child.WaitForExit();
|
||||
if (child.ExitCode != 0 && !check)
|
||||
MessageBox.Show("The application could not start. Check data\\desktop.log in the extracted folder. Make sure the folder is writable.", "WatermarkRemover-AI", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return child.ExitCode;
|
||||
}
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
if (!check) MessageBox.Show(error.Message, "WatermarkRemover-AI", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": "3.13.15",
|
||||
"url": "https://www.python.org/ftp/python/3.13.15/python-3.13.15-embed-amd64.zip",
|
||||
"sha256": "d1f04d990aee1253d8569e8e5104e30fa9f5fa830899f14843448872d936a2cf"
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
from model_assets import ensure_florence
|
||||
import sys
|
||||
import click
|
||||
from pathlib import Path
|
||||
@@ -550,10 +551,11 @@ def main(input_path: str, output_path: str, preview: bool, overwrite: bool, tran
|
||||
# Apply float32 for CPU (compatibility)
|
||||
model_dtype = torch.float32 if device == "cpu" else None
|
||||
|
||||
florence_path = str(ensure_florence())
|
||||
florence_model = Florence2ForConditionalGeneration.from_pretrained(
|
||||
"florence-community/Florence-2-large",
|
||||
florence_path,
|
||||
torch_dtype=model_dtype).to(device).eval()
|
||||
florence_processor = AutoProcessor.from_pretrained("florence-community/Florence-2-large")
|
||||
florence_processor = AutoProcessor.from_pretrained(florence_path)
|
||||
|
||||
# Get sample image from input
|
||||
if input_path.is_dir():
|
||||
@@ -628,10 +630,11 @@ def main(input_path: str, output_path: str, preview: bool, overwrite: bool, tran
|
||||
# Apply float32 for CPU (compatibility)
|
||||
model_dtype = torch.float32 if device == "cpu" else None
|
||||
|
||||
florence_path = str(ensure_florence())
|
||||
florence_model = Florence2ForConditionalGeneration.from_pretrained(
|
||||
"florence-community/Florence-2-large",
|
||||
florence_path,
|
||||
torch_dtype=model_dtype).to(device).eval()
|
||||
florence_processor = AutoProcessor.from_pretrained("florence-community/Florence-2-large")
|
||||
florence_processor = AutoProcessor.from_pretrained(florence_path)
|
||||
logger.info("Florence-2 Model loaded")
|
||||
|
||||
if not transparent:
|
||||
|
||||
+120
-91
@@ -5,19 +5,6 @@ PyWebview frontend with brainrot HTML UI
|
||||
|
||||
import logging
|
||||
|
||||
# Suppress noisy pywebview WebView2 COM warnings (thread safety noise, doesn't affect functionality)
|
||||
class PyWebviewFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
msg = record.getMessage()
|
||||
# Filter out WebView2 COM interface errors that spam the console
|
||||
if 'Error while processing window.native' in msg:
|
||||
return False
|
||||
if 'CoreWebView2 members can only be accessed' in msg:
|
||||
return False
|
||||
return True
|
||||
|
||||
logging.getLogger('pywebview').addFilter(PyWebviewFilter())
|
||||
|
||||
import webview
|
||||
import threading
|
||||
import subprocess
|
||||
@@ -27,6 +14,8 @@ import json
|
||||
import yaml
|
||||
import base64
|
||||
from pathlib import Path
|
||||
from desktop_models import ModelPreparation
|
||||
from desktop_runtime import APP_ROOT, configure_runtime, data_dir, python_executable, worker_options
|
||||
|
||||
# Only psutil for system info (lightweight)
|
||||
try:
|
||||
@@ -36,7 +25,7 @@ except ImportError:
|
||||
PSUTIL_AVAILABLE = False
|
||||
|
||||
|
||||
CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ui.yml")
|
||||
CONFIG_FILE = str(data_dir() / "ui.yml")
|
||||
|
||||
|
||||
class Api:
|
||||
@@ -45,14 +34,32 @@ class Api:
|
||||
def __init__(self):
|
||||
self.window = None
|
||||
self.process = None
|
||||
self._preview_process = None
|
||||
self._preview_running = False
|
||||
self._closed = False
|
||||
self.is_running = False
|
||||
print(f"[DEBUG] CONFIG_FILE path: {CONFIG_FILE}")
|
||||
print(f"[DEBUG] File exists: {os.path.exists(CONFIG_FILE)}")
|
||||
if os.path.exists(CONFIG_FILE):
|
||||
with open(CONFIG_FILE, 'r') as f:
|
||||
print(f"[DEBUG] Raw file contents:\n{f.read()}")
|
||||
self._process_lock = threading.Lock()
|
||||
self._stop_requested = threading.Event()
|
||||
self.config = self._load_config()
|
||||
print(f"[DEBUG] Config loaded at startup: {self.config}")
|
||||
self._models = ModelPreparation()
|
||||
self._models.start(check=True)
|
||||
|
||||
def get_model_status(self):
|
||||
"""Report verified readiness, byte progress, or an actionable download error."""
|
||||
return self._models.status()
|
||||
|
||||
def download_models(self):
|
||||
"""Download missing assets or retry a failed transfer using verified caches."""
|
||||
if self.is_running or self._preview_running:
|
||||
return {"error": "Wait until processing or preview finishes"}
|
||||
return self._models.start()
|
||||
|
||||
def _close(self):
|
||||
"""Reap only child processes created by this application window."""
|
||||
with self._process_lock:
|
||||
self._closed = True
|
||||
self.stop_processing()
|
||||
self._models.close()
|
||||
|
||||
def set_window(self, window):
|
||||
"""Set the webview window reference"""
|
||||
@@ -60,10 +67,14 @@ class Api:
|
||||
|
||||
def _load_config(self):
|
||||
"""Load saved configuration from YAML file"""
|
||||
if os.path.exists(CONFIG_FILE):
|
||||
source = Path(CONFIG_FILE)
|
||||
if not source.exists():
|
||||
source = APP_ROOT / "ui.yml"
|
||||
if source.exists():
|
||||
try:
|
||||
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
|
||||
return yaml.safe_load(f) or {}
|
||||
with source.open('r', encoding='utf-8') as f:
|
||||
config = yaml.safe_load(f)
|
||||
return config if isinstance(config, dict) else {}
|
||||
except Exception:
|
||||
pass
|
||||
return {}
|
||||
@@ -71,6 +82,7 @@ class Api:
|
||||
def _save_config(self, config):
|
||||
"""Save configuration to YAML file"""
|
||||
try:
|
||||
Path(CONFIG_FILE).parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(config, f, default_flow_style=False)
|
||||
except Exception as e:
|
||||
@@ -82,7 +94,6 @@ class Api:
|
||||
|
||||
def get_config(self):
|
||||
"""Return saved configuration to frontend"""
|
||||
print(f"[DEBUG] get_config called, returning: {self.config}")
|
||||
return self.config
|
||||
|
||||
def save_config(self, config):
|
||||
@@ -190,8 +201,8 @@ class Api:
|
||||
# Check CUDA via subprocess (avoid importing torch in GUI)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, '-c', 'import torch; print("CUDA:" + str(torch.cuda.is_available()) + ":" + (torch.cuda.get_device_name(0) if torch.cuda.is_available() else ""))'],
|
||||
capture_output=True, text=True, timeout=10, creationflags=creationflags
|
||||
[python_executable(), '-c', 'import torch; print("CUDA:" + str(torch.cuda.is_available()) + ":" + (torch.cuda.get_device_name(0) if torch.cuda.is_available() else ""))'],
|
||||
capture_output=True, text=True, timeout=30, creationflags=creationflags
|
||||
)
|
||||
if result.returncode == 0 and 'CUDA:' in result.stdout:
|
||||
parts = result.stdout.strip().split(':')
|
||||
@@ -203,7 +214,7 @@ class Api:
|
||||
|
||||
# Check FFmpeg
|
||||
try:
|
||||
subprocess.run(['ffmpeg', '-version'], capture_output=True, check=True, creationflags=creationflags)
|
||||
subprocess.run(['ffmpeg', '-version'], capture_output=True, check=True, timeout=5, creationflags=creationflags)
|
||||
info['ffmpeg'] = True
|
||||
except (subprocess.SubprocessError, FileNotFoundError):
|
||||
info['ffmpeg'] = False
|
||||
@@ -231,6 +242,8 @@ class Api:
|
||||
if self.is_running:
|
||||
return {'error': 'Already running'}
|
||||
|
||||
if self._models.status().get('status') != 'ready':
|
||||
return {'error': 'Prepare the AI models before processing. Open Models and choose Download / Retry.'}
|
||||
input_path = settings.get('input', '')
|
||||
output_path = settings.get('output', '')
|
||||
|
||||
@@ -283,7 +296,7 @@ class Api:
|
||||
})
|
||||
|
||||
# Build command
|
||||
cmd = [sys.executable, 'remwm.py', input_path, output_path]
|
||||
cmd = [python_executable(), str(APP_ROOT / 'remwm.py'), input_path, output_path]
|
||||
|
||||
if settings.get('overwrite'):
|
||||
cmd.append('--overwrite')
|
||||
@@ -311,7 +324,13 @@ class Api:
|
||||
cmd.append(f'--fade-out={float(fade_out)}')
|
||||
|
||||
# Start processing in background thread
|
||||
self.is_running = True
|
||||
with self._process_lock:
|
||||
if self._closed:
|
||||
return {"error": "Window is closing"}
|
||||
if self.is_running or self._preview_running:
|
||||
return {"error": "Processing or preview is already running"}
|
||||
self.is_running = True
|
||||
self._stop_requested.clear()
|
||||
threading.Thread(target=self._run_process, args=(cmd,), daemon=True).start()
|
||||
return {'status': 'started'}
|
||||
|
||||
@@ -324,30 +343,30 @@ class Api:
|
||||
cli_display = cli_display.replace(' --', ' \\\n --')
|
||||
self._call_js(f'addLog("$ {json.dumps(cli_display)[1:-1]}", "text-neon-cyan")')
|
||||
|
||||
env = os.environ.copy()
|
||||
env['PYTHONUNBUFFERED'] = '1'
|
||||
|
||||
working_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
script_path = os.path.join(working_dir, 'remwm.py')
|
||||
|
||||
# Verify script exists
|
||||
if not os.path.exists(script_path):
|
||||
self._call_js(f'addLog("ERROR: remwm.py not found at {json.dumps(script_path)}", "text-error")')
|
||||
self._call_js('processingComplete()')
|
||||
self._call_js(f'addLog({json.dumps("ERROR: remwm.py not found at " + script_path)}, "text-error")')
|
||||
self._call_js('processingComplete({success: false})')
|
||||
return
|
||||
|
||||
self.process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
env=env,
|
||||
cwd=working_dir
|
||||
)
|
||||
with self._process_lock:
|
||||
if self._stop_requested.is_set():
|
||||
self._call_js('processingComplete({success: false, cancelled: true})')
|
||||
return
|
||||
self.process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
bufsize=1,
|
||||
**worker_options(offline=True)
|
||||
)
|
||||
|
||||
for line in iter(self.process.stdout.readline, ''):
|
||||
if not self.is_running:
|
||||
if self._stop_requested.is_set():
|
||||
break
|
||||
|
||||
line = line.strip()
|
||||
@@ -377,8 +396,8 @@ class Api:
|
||||
|
||||
self._call_js(f'addLog({escaped}, "{color}")')
|
||||
|
||||
self.process.wait()
|
||||
self._call_js('processingComplete()')
|
||||
code = self.process.wait()
|
||||
self._call_js(f'processingComplete({json.dumps({"success": code == 0 and not self._stop_requested.is_set(), "cancelled": self._stop_requested.is_set(), "exit_code": code})})')
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
@@ -387,11 +406,14 @@ class Api:
|
||||
# Log full traceback for debugging
|
||||
tb = json.dumps(traceback.format_exc())
|
||||
self._call_js(f'addLog({tb}, "text-gray-500")')
|
||||
self._call_js('processingComplete()')
|
||||
self._call_js('processingComplete({success: false})')
|
||||
|
||||
finally:
|
||||
self.is_running = False
|
||||
self.process = None
|
||||
with self._process_lock:
|
||||
if self.process and self.process.stdout:
|
||||
self.process.stdout.close()
|
||||
self.is_running = False
|
||||
self.process = None
|
||||
|
||||
def _call_js(self, js_code):
|
||||
"""Safely call JavaScript in the frontend"""
|
||||
@@ -403,69 +425,73 @@ class Api:
|
||||
|
||||
def stop_processing(self):
|
||||
"""Stop the current processing"""
|
||||
self.is_running = False
|
||||
self._stop_requested.set()
|
||||
with self._process_lock:
|
||||
processes = (self.process, self._preview_process)
|
||||
|
||||
if self.process:
|
||||
for process in processes:
|
||||
if process is None:
|
||||
continue
|
||||
try:
|
||||
self.process.terminate()
|
||||
process.terminate()
|
||||
try:
|
||||
self.process.wait(timeout=0.5)
|
||||
process.wait(timeout=0.5)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.process.kill()
|
||||
process.kill()
|
||||
process.wait(timeout=3)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {'status': 'stopped'}
|
||||
|
||||
def preview_detection(self, settings):
|
||||
"""
|
||||
Preview watermark detection via CLI subprocess.
|
||||
Returns image with bounding boxes drawn as base64.
|
||||
"""
|
||||
"""Run one owned preview worker and return detections or a visible error."""
|
||||
if self._models.status().get('status') != 'ready':
|
||||
return {'error': 'Prepare the AI models before processing. Open Models and choose Download / Retry.'}
|
||||
input_path = settings.get('input', '')
|
||||
detection_prompt = settings.get('detection_prompt', 'watermark')
|
||||
max_bbox = settings.get('max_bbox', 15)
|
||||
|
||||
if not input_path:
|
||||
return {'error': 'No input path specified'}
|
||||
|
||||
with self._process_lock:
|
||||
if self._closed:
|
||||
return {'error': 'Window is closing'}
|
||||
if self.is_running or self._preview_running:
|
||||
return {'error': 'Processing or preview is already running'}
|
||||
self._preview_running = True
|
||||
process = None
|
||||
try:
|
||||
# Call CLI with --preview flag
|
||||
cmd = [
|
||||
sys.executable, 'remwm.py',
|
||||
input_path, '--preview',
|
||||
'--max-bbox-percent', str(int(max_bbox)),
|
||||
'--detection-prompt', detection_prompt
|
||||
]
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
cwd=os.path.dirname(os.path.abspath(__file__))
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
return {'error': result.stderr or 'Preview failed'}
|
||||
|
||||
# Parse JSON output from CLI
|
||||
output = result.stdout.strip()
|
||||
# Find JSON in output (may have log lines before it)
|
||||
for line in output.split('\n'):
|
||||
cmd = [python_executable(), str(APP_ROOT / 'remwm.py'), input_path, '--preview',
|
||||
'--max-bbox-percent', str(int(settings.get('max_bbox', 15))),
|
||||
'--detection-prompt', settings.get('detection_prompt', 'watermark')]
|
||||
with self._process_lock:
|
||||
if self._closed:
|
||||
return {'error': 'Window is closing'}
|
||||
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
**worker_options(offline=True))
|
||||
self._preview_process = process
|
||||
output, errors = process.communicate(timeout=120)
|
||||
if process.returncode != 0:
|
||||
return {'error': errors or 'Preview failed or was cancelled'}
|
||||
for line in output.strip().splitlines():
|
||||
if line.startswith('{'):
|
||||
return json.loads(line)
|
||||
|
||||
return {'error': 'No preview data returned'}
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return {'error': 'Preview timed out'}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
except Exception as error:
|
||||
return {'error': str(error)}
|
||||
finally:
|
||||
if process is not None:
|
||||
if process.poll() is None:
|
||||
process.kill()
|
||||
process.communicate()
|
||||
with self._process_lock:
|
||||
self._preview_process = None
|
||||
self._preview_running = False
|
||||
|
||||
|
||||
def main():
|
||||
def main(startup=None, *, hidden=False):
|
||||
"""Main entry point"""
|
||||
configure_runtime()
|
||||
api = Api()
|
||||
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -478,11 +504,14 @@ def main():
|
||||
width=950,
|
||||
height=860,
|
||||
min_size=(800, 600),
|
||||
background_color='#050505'
|
||||
background_color='#050505',
|
||||
hidden=hidden
|
||||
)
|
||||
|
||||
api.set_window(window)
|
||||
webview.start()
|
||||
window.events.closed += api._close
|
||||
webview.start(startup, args=(window, api) if startup else None,
|
||||
http_server=True, gui="qt" if sys.platform == "win32" else None)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
-r requirements-core.txt
|
||||
|
||||
# Desktop UI. Linux needs a concrete backend; Qt wheels avoid a GTK source build.
|
||||
pywebview>=6.2.1; sys_platform != "linux"
|
||||
pywebview>=6.2.1; sys_platform == "darwin"
|
||||
pywebview[pyside6]>=6.2.1; sys_platform == "win32"
|
||||
pywebview[qt]>=6.2.1; sys_platform == "linux"
|
||||
psutil
|
||||
pyyaml
|
||||
@@ -1,3 +1,3 @@
|
||||
@echo off
|
||||
cd /d "%~dp0"
|
||||
python\pythonw.exe remwmgui.py
|
||||
start "" "%~dp0python\pythonw.exe" "%~dp0desktop_main.py"
|
||||
@@ -0,0 +1,149 @@
|
||||
"""Build a versioned portable Windows folder and ZIP with an executable launcher."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from urllib.request import urlopen
|
||||
import zipfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
APP_FILES = ["VERSION", "LICENSE", "THIRD_PARTY_NOTICES.md", "README.md", "remwm.py",
|
||||
"remwmgui.py", "utils.py", "desktop_main.py", "desktop_runtime.py",
|
||||
"desktop_models.py", "model_assets.py", "requirements.txt", "requirements-core.txt"]
|
||||
APP_DIRS = ["lama_inpaint", "ui", "models", "licenses", "docs"]
|
||||
PIP_VERSION = "26.2.1"
|
||||
TORCH_VERSION = "2.14.0"
|
||||
VISION_VERSION = "0.29.0"
|
||||
|
||||
|
||||
def run(*args):
|
||||
"""Stop packaging at the first failed command rather than publishing a broken build."""
|
||||
subprocess.run([str(arg) for arg in args], check=True, cwd=ROOT)
|
||||
|
||||
|
||||
def sha256(path):
|
||||
"""Hash large archives without holding them in memory."""
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as source:
|
||||
while block := source.read(1024*1024):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def runtime_inputs(backend, runtime):
|
||||
"""Identify every declared input used to install the embedded runtime."""
|
||||
return dict(backend=backend, python=runtime, pip=PIP_VERSION,
|
||||
torch=TORCH_VERSION, torchvision=VISION_VERSION,
|
||||
requirements={name: hashlib.sha256((ROOT/name).read_text(encoding="utf-8").encode("utf-8")).hexdigest()
|
||||
for name in ("requirements.txt", "requirements-core.txt")})
|
||||
|
||||
|
||||
def validate_refresh(target, expected):
|
||||
"""Reject stale or legacy runtime metadata before changing any packaged file."""
|
||||
marker = target / "build-info.json"
|
||||
existing = json.loads(marker.read_text(encoding="utf-8")) if marker.exists() else {}
|
||||
if (existing.get("runtime_inputs") != expected
|
||||
or existing.get("backend") != expected["backend"]
|
||||
or existing.get("python") != expected["python"]):
|
||||
raise SystemExit("Runtime inputs changed or are missing; create a fresh build instead of --refresh-app")
|
||||
|
||||
|
||||
def copy_app(target):
|
||||
"""Copy an explicit public file list, excluding caches, private settings and models."""
|
||||
target = target.resolve()
|
||||
directories = [target/name for name in [*APP_DIRS, "scripts"]]
|
||||
# Check every absolute deletion target before replacing any application directory.
|
||||
for destination in directories:
|
||||
resolved = destination.resolve()
|
||||
if destination.is_symlink() or resolved == target or not resolved.is_relative_to(target):
|
||||
raise ValueError(f"Application directory escapes the package: {destination.name}")
|
||||
for destination in directories:
|
||||
if destination.exists():
|
||||
shutil.rmtree(destination)
|
||||
for name in APP_FILES:
|
||||
shutil.copy2(ROOT/name, target/name)
|
||||
for name in APP_DIRS:
|
||||
shutil.copytree(ROOT/name, target/name,
|
||||
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
|
||||
(target/"scripts").mkdir()
|
||||
shutil.copy2(ROOT/"scripts/check_desktop.py", target/"scripts/check_desktop.py")
|
||||
(target/"portable.flag").write_text("Store settings, logs and model caches in data/.\n")
|
||||
|
||||
|
||||
def main():
|
||||
"""Install a private runtime, compile a small native launcher and archive tested files."""
|
||||
parser=argparse.ArgumentParser()
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--cache", type=Path, required=True)
|
||||
parser.add_argument("--backend", choices=["cpu", "cu126"], default="cpu")
|
||||
parser.add_argument("--refresh-app", action="store_true")
|
||||
parser.add_argument("--skip-archive", action="store_true")
|
||||
args=parser.parse_args()
|
||||
if sys.platform != "win32":
|
||||
raise SystemExit("Windows packaging must run on Windows")
|
||||
version=(ROOT/"VERSION").read_text().strip()
|
||||
if json.loads((ROOT/"ui/config.json").read_text(encoding="utf-8"))["version"] != version:
|
||||
raise SystemExit("VERSION and ui/config.json must agree")
|
||||
name=f"WatermarkRemover-AI-{version}-windows-x64-{args.backend}"
|
||||
target=args.output.resolve()/name
|
||||
runtime=json.loads((ROOT/"packaging/python-runtime.json").read_text())
|
||||
args.cache.mkdir(parents=True,exist_ok=True)
|
||||
if args.refresh_app:
|
||||
validate_refresh(target, runtime_inputs(args.backend, runtime))
|
||||
else:
|
||||
target.mkdir(parents=True,exist_ok=False)
|
||||
archive=args.cache/Path(runtime["url"]).name
|
||||
if not archive.exists():
|
||||
with urlopen(runtime["url"],timeout=30) as source, archive.open("wb") as output:
|
||||
shutil.copyfileobj(source,output)
|
||||
if sha256(archive) != runtime["sha256"]:
|
||||
raise SystemExit("Embedded Python checksum mismatch; remove the incomplete cached archive and retry")
|
||||
python_dir=target/"python";python_dir.mkdir()
|
||||
with zipfile.ZipFile(archive) as source:
|
||||
for member in source.namelist():
|
||||
if not (python_dir/member).resolve().is_relative_to(python_dir):
|
||||
raise SystemExit("Unsafe archive member")
|
||||
source.extractall(python_dir)
|
||||
major_minor="".join(runtime["version"].split(".")[:2])
|
||||
(python_dir/f"python{major_minor}._pth").write_text(f"python{major_minor}.zip\n.\n..\nLib/site-packages\nimport site\n")
|
||||
(python_dir/"Lib/site-packages").mkdir(parents=True)
|
||||
executable=python_dir/"python.exe"
|
||||
run(sys.executable,"-m","pip","--python",executable,"install",f"pip=={PIP_VERSION}")
|
||||
run(executable,"-m","pip","--isolated","install",f"torch=={TORCH_VERSION}",f"torchvision=={VISION_VERSION}",
|
||||
"--index-url",f"https://download.pytorch.org/whl/{args.backend}")
|
||||
run(executable,"-m","pip","--isolated","install","-r",ROOT/"requirements.txt")
|
||||
copy_app(target)
|
||||
executable=target/"python/python.exe"
|
||||
run(executable,"-m","pip","check")
|
||||
run(executable,"-c","import remwm, webview; from PySide6 import QtWebEngineWidgets")
|
||||
source=target/"Launcher.cs"
|
||||
source.write_text((ROOT/"packaging/Launcher.cs").read_text().replace("__VERSION__",version))
|
||||
compiler=Path(os.environ["WINDIR"])/"Microsoft.NET/Framework64/v4.0.30319/csc.exe"
|
||||
run(compiler,"/nologo","/target:winexe","/platform:x64","/optimize+","/debug-",
|
||||
"/reference:System.Windows.Forms.dll",f"/out:{target/'WatermarkRemover-AI.exe'}",source)
|
||||
source.unlink()
|
||||
freeze=subprocess.check_output([str(executable),"-m","pip","freeze","--all"],text=True)
|
||||
(target/"installed-packages.txt").write_text(freeze,encoding="utf-8")
|
||||
commit=subprocess.check_output(["git","rev-parse","HEAD"],cwd=ROOT,text=True).strip()
|
||||
(target/"build-info.json").write_text(json.dumps(dict(version=version,backend=args.backend,
|
||||
python=runtime,runtime_inputs=runtime_inputs(args.backend, runtime),source_commit=commit,
|
||||
source_dirty=bool(subprocess.check_output(["git","diff","--ignore-space-at-eol","HEAD"],cwd=ROOT))),indent=2)+"\n")
|
||||
if not args.skip_archive:
|
||||
bundle=args.output.resolve()/(name+".zip")
|
||||
with zipfile.ZipFile(bundle,"w",compression=zipfile.ZIP_DEFLATED,compresslevel=6) as output:
|
||||
for path in sorted(target.rglob("*")):
|
||||
relative=path.relative_to(target)
|
||||
if path.is_file() and relative.parts[0]!="data" and "__pycache__" not in relative.parts and relative.parts[:2] != ("python", "Scripts"):
|
||||
output.write(path,Path(name)/relative)
|
||||
digest=sha256(bundle)
|
||||
bundle.with_suffix(".zip.sha256").write_text(f"{digest} {bundle.name}\n")
|
||||
print("Built",bundle,"SHA256",digest,flush=True)
|
||||
print("Portable folder",target,flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Exercise the actual local desktop page, assets, configuration and API bridge."""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
from desktop_runtime import configure_runtime
|
||||
configure_runtime()
|
||||
logging.getLogger().addHandler(logging.StreamHandler())
|
||||
import remwmgui
|
||||
|
||||
finished = threading.Event()
|
||||
failures = []
|
||||
|
||||
|
||||
def exercise(window, api):
|
||||
"""Read real Alpine state and execute actions through JavaScript API promises."""
|
||||
original_config = dict(api.get_config())
|
||||
try:
|
||||
assert window.events.loaded.wait(60), "Desktop page did not finish loading"
|
||||
deadline = time.monotonic() + 20
|
||||
state = None
|
||||
while time.monotonic() < deadline:
|
||||
state = window.evaluate_js("""JSON.stringify({
|
||||
ready: !!window.appInstance && !window.appInstance.isLoading,
|
||||
api: !!window.pywebview?.api,
|
||||
version: window.appInstance?.appVersion,
|
||||
translations: Object.keys(window.appInstance?.t || {}).length,
|
||||
ram: window.appInstance?.systemInfo?.ram,
|
||||
models: window.appInstance?.models?.status,
|
||||
background: getComputedStyle(document.body).backgroundColor,
|
||||
origin: location.protocol,
|
||||
resources: performance.getEntriesByType('resource').map(x => x.name)
|
||||
})""")
|
||||
state = json.loads(state) if isinstance(state, str) else state
|
||||
if state and state.get('ready') and state.get('translations') and state.get('ram', 0) > 0:
|
||||
break
|
||||
time.sleep(.2)
|
||||
assert state and state['ready'] and state['api'], state
|
||||
assert state['origin'] == 'http:', state
|
||||
assert state['translations'] >= 10 and state['version'] != '0.0', state
|
||||
assert all(url.startswith(('http://127.0.0.1:', 'http://localhost:')) for url in state['resources']), state
|
||||
window.evaluate_js("document.querySelector('#models-button').click()")
|
||||
assert window.evaluate_js('window.appInstance.modelsOpen')
|
||||
window.evaluate_js("document.querySelector('#close-models').click()")
|
||||
assert not window.evaluate_js('window.appInstance.modelsOpen')
|
||||
window.evaluate_js("window.savedProbe = null; window.pywebview.api.save_config({theme:'korpo',lang:'en'}).then(() => window.pywebview.api.get_config()).then(x => window.savedProbe = x)")
|
||||
deadline = time.monotonic() + 10
|
||||
while time.monotonic() < deadline:
|
||||
if window.evaluate_js("window.savedProbe?.theme") == 'korpo':
|
||||
break
|
||||
time.sleep(.1)
|
||||
assert window.evaluate_js("window.savedProbe?.lang") == 'en'
|
||||
print('DESKTOP PASS', json.dumps(state), flush=True)
|
||||
except BaseException as exc:
|
||||
failures.append(repr(exc))
|
||||
finally:
|
||||
api.save_config(original_config)
|
||||
window.destroy()
|
||||
|
||||
|
||||
def watchdog():
|
||||
"""Bound native window startup and shutdown as well as the page checks."""
|
||||
if not finished.wait(90):
|
||||
print('Desktop check timed out', flush=True)
|
||||
os._exit(1)
|
||||
|
||||
|
||||
threading.Thread(target=watchdog, daemon=True).start()
|
||||
# WebKit can defer network page loading for an entirely hidden native window.
|
||||
# Exercise its normal visible startup on the macOS CI desktop.
|
||||
remwmgui.main(exercise, hidden=sys.platform != "darwin")
|
||||
finished.set()
|
||||
if failures:
|
||||
raise SystemExit('; '.join(failures))
|
||||
@@ -17,7 +17,7 @@ set PYTHON_EXE=%PYTHON_DIR%\python.exe
|
||||
set CHINA_MODE=0
|
||||
set PIP_MIRROR=
|
||||
set PIP_TRUSTED_HOST=
|
||||
set HF_ENDPOINT=
|
||||
set HF_ENDPOINT=https://huggingface.co
|
||||
|
||||
:: Check if user is in China (for mirror selection)
|
||||
echo [?] Are you in China? (y/n)
|
||||
@@ -143,13 +143,11 @@ echo.
|
||||
echo [*] Downloading Florence-2 model (~1.5GB)...
|
||||
if "%CHINA_MODE%"=="1" (
|
||||
echo Using HF-Mirror for faster download in China
|
||||
"%PYTHON_EXE%" -c "import os; os.environ['HF_ENDPOINT']='%HF_ENDPOINT%'; from huggingface_hub import snapshot_download; snapshot_download('florence-community/Florence-2-large', local_dir_use_symlinks=False)"
|
||||
) else (
|
||||
"%PYTHON_EXE%" -c "from huggingface_hub import snapshot_download; snapshot_download('florence-community/Florence-2-large', local_dir_use_symlinks=False)"
|
||||
)
|
||||
"%PYTHON_EXE%" -m model_assets --florence-only --endpoint "%HF_ENDPOINT%"
|
||||
if errorlevel 1 (
|
||||
echo [!] Warning: Could not download Florence-2 model
|
||||
echo It will be downloaded on first use
|
||||
echo Open Models in the application and choose Download / Retry
|
||||
) else (
|
||||
echo [OK] Florence-2 model ready
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ $PYTHON_EXE = "$PYTHON_DIR\python.exe"
|
||||
$CHINA_MODE = $false
|
||||
$PIP_INDEX_URL = ""
|
||||
$PIP_EXTRA_ARGS = @()
|
||||
$HF_ENDPOINT = ""
|
||||
$HF_ENDPOINT = "https://huggingface.co"
|
||||
|
||||
# Fun facts and tips to show during installation
|
||||
$tips = @(
|
||||
@@ -194,25 +194,9 @@ Write-Host " Did you know?" -ForegroundColor DarkGray
|
||||
Write-Host ""
|
||||
|
||||
if ($CHINA_MODE) {
|
||||
# Set HF_ENDPOINT environment variable for China mirror
|
||||
$env:HF_ENDPOINT = $HF_ENDPOINT
|
||||
Write-Host " Using HF-Mirror for faster download in China" -ForegroundColor DarkGray
|
||||
$florenceScript = @"
|
||||
import os
|
||||
os.environ['HF_ENDPOINT'] = '$HF_ENDPOINT'
|
||||
from huggingface_hub import snapshot_download
|
||||
snapshot_download('florence-community/Florence-2-large', local_dir_use_symlinks=False)
|
||||
print('FLORENCE_OK')
|
||||
"@
|
||||
} else {
|
||||
$florenceScript = @"
|
||||
from huggingface_hub import snapshot_download
|
||||
snapshot_download('florence-community/Florence-2-large', local_dir_use_symlinks=False)
|
||||
print('FLORENCE_OK')
|
||||
"@
|
||||
}
|
||||
|
||||
$florenceProcess = Start-Process -FilePath $PYTHON_EXE -ArgumentList "-c", "`"$florenceScript`"" -NoNewWindow -PassThru
|
||||
$florenceProcess = Start-Process -FilePath $PYTHON_EXE -ArgumentList "-m", "model_assets", "--florence-only", "--endpoint", $HF_ENDPOINT -NoNewWindow -PassThru
|
||||
$florenceHandle = $florenceProcess.Handle
|
||||
|
||||
$lastTipTime = Get-Date
|
||||
@@ -235,7 +219,7 @@ Write-Host "`r
|
||||
$florenceProcess.WaitForExit()
|
||||
if ($florenceProcess.ExitCode -ne 0) {
|
||||
Write-Host " [!] Warning: Could not download Florence-2 model" -ForegroundColor Yellow
|
||||
Write-Host " It will be downloaded on first use" -ForegroundColor Yellow
|
||||
Write-Host " Open Models in the application and choose Download / Retry" -ForegroundColor Yellow
|
||||
}
|
||||
else {
|
||||
Write-Host " [OK] Florence-2 model ready" -ForegroundColor Green
|
||||
|
||||
@@ -11,7 +11,7 @@ echo ""
|
||||
# China mirror configuration
|
||||
CHINA_MODE=0
|
||||
PIP_MIRROR=""
|
||||
HF_ENDPOINT=""
|
||||
HF_ENDPOINT="https://huggingface.co"
|
||||
|
||||
# Check if user is in China (for mirror selection)
|
||||
echo " [?] Are you in China? (y/n)"
|
||||
@@ -120,10 +120,8 @@ python -m lama_inpaint download
|
||||
echo " [*] Downloading Florence-2 model (~1.5GB)..."
|
||||
if [ "$CHINA_MODE" == "1" ]; then
|
||||
echo " Using HF-Mirror for faster download in China"
|
||||
HF_ENDPOINT="$HF_ENDPOINT" python -c "import os; os.environ['HF_ENDPOINT']='$HF_ENDPOINT'; from huggingface_hub import snapshot_download; snapshot_download('florence-community/Florence-2-large', local_dir_use_symlinks=False)" || echo " [!] Florence-2 download failed, will retry on first use"
|
||||
else
|
||||
python -c "from huggingface_hub import snapshot_download; snapshot_download('florence-community/Florence-2-large', local_dir_use_symlinks=False)" || echo " [!] Florence-2 download failed, will retry on first use"
|
||||
fi
|
||||
python -m model_assets --florence-only --endpoint "$HF_ENDPOINT" || echo " [!] Florence-2 download failed, open Models in the application and choose Download / Retry"
|
||||
|
||||
echo ""
|
||||
echo " ============================================="
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
module.exports = {
|
||||
content: ['./ui/**/*.html', './ui/**/*.js', './remwmgui.py'],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
mono: ['"Space Mono"', 'monospace'],
|
||||
glitch: ['"Rubik Glitch"', 'cursive'],
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
},
|
||||
colors: {
|
||||
'void': '#050505',
|
||||
'card': '#111116',
|
||||
'neon-green': '#00ff88',
|
||||
'neon-pink': '#ff006e',
|
||||
'neon-cyan': '#00f3ff',
|
||||
'error': '#ff3333',
|
||||
},
|
||||
animation: {
|
||||
'pulse-fast': 'pulse 0.5s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||
'glitch': 'glitch 1s linear infinite',
|
||||
'marquee': 'marquee 10s linear infinite',
|
||||
},
|
||||
keyframes: {
|
||||
glitch: {
|
||||
'2%, 64%': { transform: 'translate(2px,0) skew(0deg)' },
|
||||
'4%, 60%': { transform: 'translate(-2px,0) skew(0deg)' },
|
||||
'62%': { transform: 'translate(0,0) skew(5deg)' },
|
||||
},
|
||||
marquee: {
|
||||
'0%': { transform: 'translateX(0)' },
|
||||
'100%': { transform: 'translateX(-50%)' },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Exercise actual preparation subprocess failures and retries without network/models."""
|
||||
import time
|
||||
|
||||
from desktop_models import ModelPreparation
|
||||
import desktop_models
|
||||
|
||||
|
||||
def wait(controller):
|
||||
"""Bound worker completion so a broken process lifecycle fails the test."""
|
||||
deadline=time.monotonic()+10
|
||||
while controller.status()['busy'] and time.monotonic()<deadline:
|
||||
time.sleep(.02)
|
||||
assert not controller.status()['busy']
|
||||
return controller.status()
|
||||
|
||||
|
||||
def test_retry_after_worker_failure(tmp_path, monkeypatch):
|
||||
"""A failed worker exposes its message and a retry can reach verified readiness."""
|
||||
(tmp_path/'model_assets.py').write_text("import json, pathlib, sys, time\np=pathlib.Path('attempt')\ntime.sleep(.1)\nif not p.exists():\n p.touch(); print(json.dumps({'status':'error','message':'interrupted'})); sys.exit(1)\nprint(json.dumps({'status':'ready'}))\n")
|
||||
monkeypatch.setattr(desktop_models, 'APP_ROOT', tmp_path)
|
||||
original=desktop_models.worker_options
|
||||
monkeypatch.setattr(desktop_models,'worker_options',lambda:dict(original(),cwd=str(tmp_path)))
|
||||
controller=ModelPreparation()
|
||||
try:
|
||||
assert controller.start()['status']=='started'
|
||||
assert 'error' in controller.start()
|
||||
assert wait(controller)['message']=='interrupted'
|
||||
assert controller.start()['status']=='started'
|
||||
assert wait(controller)['status']=='ready'
|
||||
finally:
|
||||
controller.close()
|
||||
assert 'error' in controller.start()
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Regression checks for desktop worker completion and cancellation reports."""
|
||||
import json
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("webview")
|
||||
import remwmgui
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def api(tmp_path, monkeypatch):
|
||||
"""Capture bridge events while using real child processes and no model downloads."""
|
||||
class Prepared:
|
||||
def start(self, **kwargs):
|
||||
return {"status": "started"}
|
||||
def status(self):
|
||||
return {"status": "ready"}
|
||||
def close(self):
|
||||
pass
|
||||
monkeypatch.setattr(remwmgui, "ModelPreparation", Prepared)
|
||||
monkeypatch.setattr(remwmgui, "CONFIG_FILE", str(tmp_path / "ui.yml"))
|
||||
instance = remwmgui.Api()
|
||||
instance.events = []
|
||||
instance._call_js = instance.events.append
|
||||
yield instance
|
||||
instance._close()
|
||||
|
||||
|
||||
def completion(api):
|
||||
"""Decode the externally visible completion payload sent to the page."""
|
||||
event = next(x for x in reversed(api.events) if x.startswith("processingComplete("))
|
||||
return json.loads(event.removeprefix("processingComplete(").removesuffix(")"))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("code", [0, 7])
|
||||
def test_exit_status_is_not_always_success(api, code):
|
||||
api.is_running = True
|
||||
api._run_process([sys.executable, "-c", f"raise SystemExit({code})"])
|
||||
result = completion(api)
|
||||
assert result == {"success": code == 0, "cancelled": False, "exit_code": code}
|
||||
assert not api.is_running and api.process is None
|
||||
|
||||
|
||||
def test_cancel_running_worker_reaps_child(api):
|
||||
api.is_running = True
|
||||
thread = threading.Thread(target=api._run_process,
|
||||
args=([sys.executable, "-c", "import time; print('started', flush=True); time.sleep(60)"],))
|
||||
thread.start()
|
||||
try:
|
||||
deadline = time.monotonic() + 10
|
||||
while api.process is None and time.monotonic() < deadline:
|
||||
time.sleep(.01)
|
||||
process = api.process
|
||||
assert process is not None
|
||||
api.stop_processing()
|
||||
thread.join(10)
|
||||
assert not thread.is_alive() and process.poll() is not None
|
||||
result = completion(api)
|
||||
assert result["cancelled"] and not result["success"]
|
||||
assert not api.is_running and api.process is None
|
||||
finally:
|
||||
api.stop_processing()
|
||||
thread.join(10)
|
||||
|
||||
|
||||
def test_cancel_before_spawn_still_completes_page(api, monkeypatch):
|
||||
def forbidden(*args, **kwargs):
|
||||
pytest.fail("Cancelled job spawned a worker")
|
||||
monkeypatch.setattr(remwmgui.subprocess, "Popen", forbidden)
|
||||
api.is_running = True
|
||||
api.stop_processing()
|
||||
api._run_process([sys.executable, "-c", "pass"])
|
||||
assert 'processingComplete({success: false, cancelled: true})' in api.events
|
||||
assert not api.is_running and api.process is None
|
||||
|
||||
|
||||
def test_close_reaps_preview_and_rejects_new_work(api, tmp_path, monkeypatch):
|
||||
"""Closing the desktop must not leave its preview model process alive."""
|
||||
(tmp_path / 'remwm.py').write_text("import time; time.sleep(60)\n")
|
||||
monkeypatch.setattr(remwmgui, 'APP_ROOT', tmp_path)
|
||||
result = []
|
||||
thread = threading.Thread(target=lambda: result.append(api.preview_detection({'input': 'fixture.png'})))
|
||||
thread.start()
|
||||
try:
|
||||
deadline = time.monotonic() + 10
|
||||
while api._preview_process is None and time.monotonic() < deadline:
|
||||
time.sleep(.01)
|
||||
process = api._preview_process
|
||||
assert process is not None
|
||||
assert 'already running' in api.preview_detection({'input': 'fixture.png'})['error']
|
||||
api._close()
|
||||
thread.join(10)
|
||||
assert not thread.is_alive() and process.poll() is not None
|
||||
assert 'error' in result[0]
|
||||
assert 'closing' in api.preview_detection({'input': 'fixture.png'})['error']
|
||||
finally:
|
||||
api._close()
|
||||
thread.join(10)
|
||||
@@ -0,0 +1,168 @@
|
||||
"""Verify progress, interruption, safe resumption and pinned model integrity."""
|
||||
import hashlib
|
||||
import io
|
||||
|
||||
import pytest
|
||||
import model_assets as assets
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def artifact():
|
||||
"""Use tiny fixed bytes instead of real model downloads."""
|
||||
data = b"verified model fixture"
|
||||
return data, dict(name="model.bin", size=len(data), sha256=hashlib.sha256(data).hexdigest())
|
||||
|
||||
|
||||
class Response(io.BytesIO):
|
||||
"""Minimal HTTP stream with explicit status and range headers."""
|
||||
status = 200
|
||||
headers = {}
|
||||
|
||||
|
||||
def test_interruption_and_resume_publish_only_verified_file(tmp_path, monkeypatch, artifact):
|
||||
"""Keep a partial transfer after a network error and resume at its exact offset."""
|
||||
data, info = artifact
|
||||
target = tmp_path / info['name']
|
||||
class Interrupted(Response):
|
||||
def read(self, size=-1):
|
||||
"""Return one chunk before simulating a lost connection."""
|
||||
if self.tell():
|
||||
raise OSError('connection lost')
|
||||
return super().read(5)
|
||||
monkeypatch.setattr(assets, 'urlopen', lambda *a, **kw: Interrupted(data))
|
||||
with pytest.raises(OSError):
|
||||
assets.download_file('https://example.invalid/model', target, info, lambda *a: None)
|
||||
assert not target.exists()
|
||||
assert target.with_suffix('.bin.part').read_bytes() == data[:5]
|
||||
def resume(request, **kwargs):
|
||||
"""Honor the caller's byte range with the remaining fixture content."""
|
||||
assert request.get_header('Range') == 'bytes=5-'
|
||||
response = Response(data[5:]); response.status = 206
|
||||
response.headers = {'Content-Range': f'bytes 5-{len(data)-1}/{len(data)}'}
|
||||
return response
|
||||
monkeypatch.setattr(assets, 'urlopen', resume)
|
||||
events = []
|
||||
assets.download_file('https://example.invalid/model', target, info, lambda n, total: events.append((n,total)))
|
||||
assert target.read_bytes() == data
|
||||
assert events[0] == (5,len(data)) and events[-1] == (len(data),len(data))
|
||||
assert not target.with_suffix('.bin.part').exists()
|
||||
|
||||
|
||||
def test_server_ignoring_range_restarts_safely(tmp_path, monkeypatch, artifact):
|
||||
"""A full 200 response must replace partial bytes rather than duplicate them."""
|
||||
data, info = artifact
|
||||
target = tmp_path / info['name']; target.with_suffix('.bin.part').write_bytes(data[:5])
|
||||
monkeypatch.setattr(assets, 'urlopen', lambda *a, **kw: Response(data))
|
||||
assets.download_file('https://example.invalid/model', target, info, lambda *a: None)
|
||||
assert target.read_bytes() == data
|
||||
|
||||
|
||||
def test_wrong_range_is_rejected_without_publication(tmp_path, monkeypatch, artifact):
|
||||
"""A mismatched 206 response must never be appended or accepted."""
|
||||
data, info = artifact
|
||||
target = tmp_path / info['name']; partial = target.with_suffix('.bin.part');partial.write_bytes(data[:5])
|
||||
response = Response(data); response.status=206; response.headers={'Content-Range': f'bytes 0-{len(data)-1}/{len(data)}'}
|
||||
monkeypatch.setattr(assets, 'urlopen', lambda *a, **kw: response)
|
||||
with pytest.raises(RuntimeError, match='range'):
|
||||
assets.download_file('https://example.invalid/model', target, info, lambda *a: None)
|
||||
assert not target.exists() and partial.read_bytes()==data[:5]
|
||||
|
||||
|
||||
def test_bad_digest_keeps_previous_file_and_allows_clean_retry(tmp_path, monkeypatch, artifact):
|
||||
"""A same-size tampered download cannot replace the previous cached artifact."""
|
||||
data, info = artifact
|
||||
target = tmp_path / info['name']; target.write_bytes(b'previous')
|
||||
monkeypatch.setattr(assets, 'urlopen', lambda *a, **kw: Response(b'x'*len(data)))
|
||||
with pytest.raises(RuntimeError, match='SHA-256'):
|
||||
assets.download_file('https://example.invalid/model', target, info, lambda *a: None)
|
||||
assert target.read_bytes()==b'previous' and not target.with_suffix('.bin.part').exists()
|
||||
monkeypatch.setattr(assets, 'urlopen', lambda *a, **kw: Response(data))
|
||||
assets.download_file('https://example.invalid/model', target, info, lambda *a: None)
|
||||
assert target.read_bytes()==data
|
||||
|
||||
|
||||
def test_offline_verification_never_connects(tmp_path, monkeypatch, artifact):
|
||||
"""Require all pinned assets even when the snapshot directory already exists."""
|
||||
data, info=artifact
|
||||
monkeypatch.setattr(assets, 'MANIFEST', {'files':[info]})
|
||||
monkeypatch.setattr(assets, 'florence_snapshot', lambda: tmp_path)
|
||||
def forbidden(*a, **kw):
|
||||
"""Fail any network access during an offline verification."""
|
||||
pytest.fail('offline verification opened the network')
|
||||
monkeypatch.setattr(assets, 'urlopen', forbidden)
|
||||
with pytest.raises(RuntimeError, match='repair'):
|
||||
assets.ensure_florence(download=False)
|
||||
(tmp_path/info['name']).write_bytes(data)
|
||||
assert assets.ensure_florence(download=False)==tmp_path
|
||||
(tmp_path/info['name']).write_bytes(b'x'*len(data))
|
||||
with pytest.raises(RuntimeError, match='repair'):
|
||||
assets.ensure_florence(download=False)
|
||||
|
||||
|
||||
def test_truncated_response_retains_bytes_for_retry(tmp_path, monkeypatch, artifact):
|
||||
"""A short HTTP body is resumable even when the server closes without an error."""
|
||||
data, info = artifact
|
||||
target = tmp_path / info['name']
|
||||
monkeypatch.setattr(assets, 'urlopen', lambda *a, **kw: Response(data[:5]))
|
||||
with pytest.raises(RuntimeError, match='ended early'):
|
||||
assets.download_file('https://example.invalid/model', target, info, lambda *a: None)
|
||||
assert not target.exists()
|
||||
assert target.with_suffix('.bin.part').read_bytes() == data[:5]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("endpoint", ["http://mirror.example", "file:///tmp/model",
|
||||
"https://user:pass@mirror.example", "https://mirror.example?token=x",
|
||||
"https://mirror.example/#fragment", "https://", "https://mirror.example:bad",
|
||||
"https://mirror.example/with space"])
|
||||
def test_invalid_endpoint_is_not_persisted(tmp_path, monkeypatch, endpoint):
|
||||
"""Reject insecure or ambiguous configuration before modifying saved settings."""
|
||||
monkeypatch.setenv("WMR_DATA_DIR", str(tmp_path))
|
||||
with pytest.raises(ValueError):
|
||||
assets.save_endpoint(endpoint)
|
||||
assert not (tmp_path / "model-endpoint.json").exists()
|
||||
|
||||
|
||||
def test_mirror_survives_process_restart_and_environment_can_override(tmp_path, monkeypatch):
|
||||
"""Desktop workers launched later must reuse the installer's saved endpoint."""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
monkeypatch.setenv("WMR_DATA_DIR", str(tmp_path))
|
||||
monkeypatch.delenv("HF_ENDPOINT", raising=False)
|
||||
assert assets.model_endpoint() == "https://huggingface.co"
|
||||
assets.save_endpoint("https://mirror.example/models/")
|
||||
result = subprocess.run([sys.executable, "-c",
|
||||
"from model_assets import model_endpoint; print(model_endpoint())"],
|
||||
cwd=assets.Path(assets.__file__).parent, env=os.environ.copy(),
|
||||
capture_output=True, text=True, check=True)
|
||||
assert result.stdout.strip() == "https://mirror.example/models"
|
||||
monkeypatch.setenv("HF_ENDPOINT", "https://override.example")
|
||||
assert assets.model_endpoint() == "https://override.example"
|
||||
monkeypatch.delenv("HF_ENDPOINT")
|
||||
assets.save_endpoint("https://huggingface.co")
|
||||
assert assets.model_endpoint() == "https://huggingface.co"
|
||||
|
||||
|
||||
def test_mirror_download_still_rejects_tampering_and_retry_uses_same_endpoint(tmp_path, monkeypatch, artifact):
|
||||
"""A saved mirror changes routing, never the pinned revision or checksum policy."""
|
||||
data, info = artifact
|
||||
monkeypatch.setenv("WMR_DATA_DIR", str(tmp_path / "settings"))
|
||||
monkeypatch.delenv("HF_ENDPOINT", raising=False)
|
||||
monkeypatch.delenv("HF_HUB_OFFLINE", raising=False)
|
||||
assets.save_endpoint("https://mirror.example")
|
||||
monkeypatch.setattr(assets, "MANIFEST", {"files": [info]})
|
||||
root = tmp_path / "snapshot"
|
||||
monkeypatch.setattr(assets, "florence_snapshot", lambda: root)
|
||||
replies = iter([b"x" * len(data), data])
|
||||
urls = []
|
||||
def open_response(request, **kwargs):
|
||||
"""Return tampered bytes first, then valid bytes on explicit retry."""
|
||||
urls.append(request.full_url)
|
||||
return Response(next(replies))
|
||||
monkeypatch.setattr(assets, "urlopen", open_response)
|
||||
with pytest.raises(RuntimeError, match="SHA-256"):
|
||||
assets.ensure_florence()
|
||||
assert not (root / info["name"]).exists()
|
||||
assert assets.ensure_florence() == root
|
||||
assert (root / info["name"]).read_bytes() == data
|
||||
assert urls == [f"https://mirror.example/{assets.FLORENCE_REPO}/resolve/{assets.FLORENCE_REVISION}/{info['name']}"] * 2
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Keep the desktop shell self-contained and verify its vendored asset inventory."""
|
||||
import hashlib
|
||||
from html.parser import HTMLParser
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class Assets(HTMLParser):
|
||||
"""Collect executable scripts and stylesheets from the real page."""
|
||||
def __init__(self):
|
||||
"""Start an empty inventory before parsing the application HTML."""
|
||||
super().__init__();self.paths=[]
|
||||
def handle_starttag(self,tag,attrs):
|
||||
"""Record only resources loaded automatically by the browser."""
|
||||
attrs=dict(attrs)
|
||||
if tag=='script' and 'src' in attrs:self.paths.append(attrs['src'])
|
||||
if tag=='link' and attrs.get('rel')=='stylesheet':self.paths.append(attrs['href'])
|
||||
|
||||
|
||||
def test_desktop_scripts_and_styles_are_local():
|
||||
"""A first launch must not depend on CDNs for its script or stylesheet assets."""
|
||||
parser=Assets();parser.feed((ROOT/'ui/index.html').read_text(encoding='utf-8'))
|
||||
assert parser.paths
|
||||
for name in parser.paths:
|
||||
assert not urlparse(name).scheme and not name.startswith('//')
|
||||
assert (ROOT/'ui'/name).is_file()
|
||||
|
||||
|
||||
def test_vendored_assets_match_manifest():
|
||||
"""Catch missing or accidentally changed vendor files, including fonts/licenses."""
|
||||
vendor=ROOT/'ui/vendor';manifest=json.loads((vendor/'manifest.json').read_text())
|
||||
for name,digest in manifest.items():
|
||||
assert hashlib.sha256((vendor/name).read_bytes()).hexdigest()==digest,name
|
||||
|
||||
|
||||
def test_ui_and_package_version_agree():
|
||||
"""Use one release version for the launcher and displayed interface."""
|
||||
assert (ROOT/'VERSION').read_text().strip()==json.loads((ROOT/'ui/config.json').read_text(encoding='utf-8'))['version']
|
||||
|
||||
|
||||
def test_theme_and_font_resources_are_local():
|
||||
"""Every theme and font must remain usable without an external image/font server."""
|
||||
for stylesheet in [ROOT/'ui/themes.css', ROOT/'ui/vendor/fonts.css']:
|
||||
for value in re.findall(r"url\(([^)]+)\)", stylesheet.read_text(encoding='utf-8')):
|
||||
value=value.strip("\"'")
|
||||
assert not urlparse(value).scheme and not value.startswith('//'),value
|
||||
assert (stylesheet.parent/value).is_file(),value
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Prevent refreshed portable packages from misreporting or retaining old content."""
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from scripts import build_windows as builder
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def package(tmp_path, monkeypatch):
|
||||
"""Create a tiny app/runtime layout without downloading or installing anything."""
|
||||
source = tmp_path / "source"
|
||||
source.mkdir()
|
||||
monkeypatch.setattr(builder, "ROOT", source)
|
||||
for name in builder.APP_FILES:
|
||||
(source/name).write_text("current", encoding="utf-8")
|
||||
for name in [*builder.APP_DIRS, "scripts"]:
|
||||
(source/name).mkdir()
|
||||
(source/name/"current.txt").write_text("current", encoding="utf-8")
|
||||
(source/"scripts/check_desktop.py").write_text("pass", encoding="utf-8")
|
||||
target = tmp_path / "package"
|
||||
target.mkdir()
|
||||
runtime = {"version": "3.13.15", "url": "https://example.invalid/python.zip", "sha256": "abc"}
|
||||
expected = builder.runtime_inputs("cpu", runtime)
|
||||
(target/"build-info.json").write_text(json.dumps({"backend": "cpu", "python": runtime,
|
||||
"runtime_inputs": expected}), encoding="utf-8")
|
||||
return source, target, runtime, expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("change", ["python", "backend", "requirements", "torch", "missing"])
|
||||
def test_refresh_rejects_changed_runtime_before_mutation(package, monkeypatch, change):
|
||||
"""Changing interpreter, dependency inputs or legacy metadata requires a fresh build."""
|
||||
source, target, runtime, expected = package
|
||||
before = (target/"build-info.json").read_bytes()
|
||||
if change == "python":
|
||||
runtime = dict(runtime, sha256="different")
|
||||
elif change == "requirements":
|
||||
(source/"requirements-core.txt").write_text("changed dependency", encoding="utf-8")
|
||||
elif change == "torch":
|
||||
monkeypatch.setattr(builder, "TORCH_VERSION", "99.0.0")
|
||||
elif change == "missing":
|
||||
(target/"build-info.json").unlink()
|
||||
backend = "cu126" if change == "backend" else "cpu"
|
||||
with pytest.raises(SystemExit, match="fresh build"):
|
||||
builder.validate_refresh(target, builder.runtime_inputs(backend, runtime))
|
||||
if change != "missing":
|
||||
assert (target/"build-info.json").read_bytes() == before
|
||||
|
||||
|
||||
def test_refresh_removes_obsolete_app_files_preserving_runtime_and_user_data(package):
|
||||
"""Deleted UI/module files must disappear while the private runtime and data survive."""
|
||||
source, target, runtime, expected = package
|
||||
for name in [*builder.APP_DIRS, "scripts", "python", "data"]:
|
||||
(target/name).mkdir()
|
||||
(target/name/"old.txt").write_text("old", encoding="utf-8")
|
||||
builder.validate_refresh(target, expected)
|
||||
builder.copy_app(target)
|
||||
for name in builder.APP_DIRS:
|
||||
assert not (target/name/"old.txt").exists()
|
||||
assert (target/name/"current.txt").read_text(encoding="utf-8") == "current"
|
||||
assert sorted(path.name for path in (target/"scripts").iterdir()) == ["check_desktop.py"]
|
||||
for name in ("python", "data"):
|
||||
assert (target/name/"old.txt").read_text(encoding="utf-8") == "old"
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 1000"><defs><linearGradient id="sky" x2="0" y2="1"><stop stop-color="#127dc5"/><stop offset="1" stop-color="#a7e6f4"/></linearGradient><linearGradient id="grass" x2="0" y2="1"><stop stop-color="#79b928"/><stop offset="1" stop-color="#245b14"/></linearGradient></defs><path fill="url(#sky)" d="M0 0h1600v1000H0z"/><g fill="#fff" opacity=".65"><ellipse cx="1100" cy="200" rx="240" ry="35"/><ellipse cx="440" cy="120" rx="130" ry="25"/></g><path fill="#5d9b38" d="M0 770Q350 410 850 710T1600 570v430H0z"/><path fill="url(#grass)" d="M0 600Q540 540 900 840T1600 780v220H0z"/></svg>
|
||||
|
After Width: | Height: | Size: 635 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><g fill="none" stroke="#fff" stroke-opacity=".22" stroke-width="1"><circle cx="16" cy="16" r="12"/><circle cx="16" cy="16" r="7"/><path d="M0 16Q16 0 32 16Q16 32 0 16M16 0Q32 16 16 32Q0 16 16 0"/></g></svg>
|
||||
|
After Width: | Height: | Size: 289 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="m12 1 3 7 8 1-6 6 2 8-7-4-7 4 2-8-6-6 8-1z" fill="#ffb5db" stroke="#7f315c"/><circle cx="10" cy="12" r="1"/><circle cx="15" cy="12" r="1"/></svg>
|
||||
|
After Width: | Height: | Size: 237 B |
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "0.67",
|
||||
"version": "0.68.0",
|
||||
"themes": [
|
||||
{ "id": "brainrot", "name": "Brainrot" },
|
||||
{ "id": "slay", "name": "Slay Queen" },
|
||||
|
||||
+68
-51
@@ -6,51 +6,14 @@
|
||||
<title>WM REMOVER - OHIO EDITION</title>
|
||||
|
||||
<!-- Tailwind + Alpine + Fonts -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js"></script>
|
||||
<!-- Base fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,400&family=Rubik+Glitch&family=Inter:wght@400;800;900&display=swap" rel="stylesheet">
|
||||
<!-- Theme fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&family=Dancing+Script:wght@700&family=Oswald:wght@400;700&family=Playfair+Display:ital,wght@0,400;1,400&family=Mali:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="vendor/tailwind.css">
|
||||
<script defer src="vendor/alpine-3.13.3.min.js"></script>
|
||||
<!-- Local base and theme fonts -->
|
||||
<link rel="stylesheet" href="vendor/fonts.css">
|
||||
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
mono: ['"Space Mono"', 'monospace'],
|
||||
glitch: ['"Rubik Glitch"', 'cursive'],
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
},
|
||||
colors: {
|
||||
'void': '#050505',
|
||||
'card': '#111116',
|
||||
'neon-green': '#00ff88',
|
||||
'neon-pink': '#ff006e',
|
||||
'neon-cyan': '#00f3ff',
|
||||
'error': '#ff3333',
|
||||
},
|
||||
animation: {
|
||||
'pulse-fast': 'pulse 0.5s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||
'glitch': 'glitch 1s linear infinite',
|
||||
'marquee': 'marquee 10s linear infinite',
|
||||
},
|
||||
keyframes: {
|
||||
glitch: {
|
||||
'2%, 64%': { transform: 'translate(2px,0) skew(0deg)' },
|
||||
'4%, 60%': { transform: 'translate(-2px,0) skew(0deg)' },
|
||||
'62%': { transform: 'translate(0,0) skew(5deg)' },
|
||||
},
|
||||
marquee: {
|
||||
'0%': { transform: 'translateX(0)' },
|
||||
'100%': { transform: 'translateX(-50%)' },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
[x-cloak] { display: none !important; }
|
||||
.model-dialog { color: #e5e7eb; }
|
||||
body { background-color: #050505; color: white; }
|
||||
|
||||
/* CRT Scanline Effect */
|
||||
@@ -237,7 +200,7 @@
|
||||
</div>
|
||||
|
||||
<!-- PREVIEW BUTTON -->
|
||||
<button @click="runPreview()" :disabled="!inputPath || previewLoading"
|
||||
<button id="preview-processing" @click="runPreview()" :disabled="!inputPath || previewLoading"
|
||||
:data-tooltip="t.buttons?.preview_tooltip || ''"
|
||||
class="w-full py-3 bg-neon-cyan/20 hover:bg-neon-cyan/40 text-neon-cyan border border-neon-cyan/50 rounded-xl font-bold transition-all disabled:opacity-30 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
:class="previewLoading ? 'animate-pulse' : ''">
|
||||
@@ -300,7 +263,7 @@
|
||||
</div>
|
||||
|
||||
<!-- START BUTTON -->
|
||||
<button @click="startProcessing()" :disabled="processing || !inputPath"
|
||||
<button id="start-processing" @click="startProcessing()" :disabled="processing || !inputPath"
|
||||
class="relative group w-full py-5 bg-neon-green rounded-xl font-black text-black text-xl overflow-hidden hover:scale-[1.02] active:scale-95 transition-transform disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<span class="relative z-10 flex items-center justify-center gap-2" x-text="processing ? (t.buttons?.start_processing || 'Processing...') : (t.buttons?.start || 'Start')">
|
||||
</span>
|
||||
@@ -500,6 +463,30 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="fixed bottom-3 right-3 z-50">
|
||||
<button id="models-button" @click="modelsOpen = true" class="glass-panel px-3 py-2 text-xs text-neon-cyan rounded">
|
||||
Models: <span x-text="models.status"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div x-show="bridgeError" class="fixed top-2 left-2 right-2 z-50 bg-card border border-error p-3 text-sm" x-text="bridgeError"></div>
|
||||
<div x-show="modelsOpen" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-6">
|
||||
<section class="glass-panel model-dialog w-full max-w-xl p-6 rounded-xl" role="dialog" aria-label="AI models">
|
||||
<h2 class="text-xl font-bold text-neon-cyan">AI models</h2>
|
||||
<p class="text-sm text-gray-400 mt-3">Download once, then process locally. About 1.8 GB is needed for Florence-2 and LaMA.</p>
|
||||
<p class="mt-4" x-text="[models.model, models.file].filter(Boolean).join(' — ')"></p>
|
||||
<p class="text-sm mt-2 break-words" x-text="models.message || models.status"></p>
|
||||
<div x-show="models.status === 'downloading'" class="mt-4">
|
||||
<progress class="w-full" max="100" :value="modelProgress()"></progress>
|
||||
<p class="text-xs mt-1" x-text="`${modelProgress()}% — ${(models.current / 1048576).toFixed(1)} / ${(models.total / 1048576).toFixed(1)} MB`"></p>
|
||||
</div>
|
||||
<p x-show="models.status === 'error'" class="text-sm text-gray-400 mt-3">Check your connection and free disk space, then retry. Completed files are reused; partial Florence-2 downloads resume when supported.</p>
|
||||
<div class="flex gap-3 mt-5">
|
||||
<button id="download-models" @click="downloadModels()" :disabled="models.busy || processing" class="bg-neon-cyan text-black px-4 py-2 rounded disabled:opacity-50">Download / Retry</button>
|
||||
<button id="close-models" @click="modelsOpen = false" class="border border-gray-500 px-4 py-2 rounded">Close</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Store reference to Alpine app instance for Python callbacks (Alpine 3.x compatible)
|
||||
window.appInstance = null;
|
||||
@@ -517,8 +504,8 @@
|
||||
}));
|
||||
}
|
||||
|
||||
function processingComplete() {
|
||||
window.dispatchEvent(new CustomEvent('pywebview-complete'));
|
||||
function processingComplete(result = {success: false}) {
|
||||
window.dispatchEvent(new CustomEvent('pywebview-complete', {detail: result}));
|
||||
}
|
||||
|
||||
// Theme switcher function
|
||||
@@ -594,6 +581,8 @@
|
||||
|
||||
|
||||
async init() {
|
||||
if (this._initialized) return;
|
||||
this._initialized = true;
|
||||
// Store reference for global Python callbacks
|
||||
window.appInstance = this;
|
||||
|
||||
@@ -641,6 +630,7 @@
|
||||
switchTheme(this.theme);
|
||||
await this.switchLang(this.lang);
|
||||
|
||||
this.bridgeError = '';
|
||||
this._configLoaded = true; // Now allow saving
|
||||
this.isLoading = false; // Hide loading screen
|
||||
} catch (e) {
|
||||
@@ -662,7 +652,7 @@
|
||||
// Fallback timeout in case pywebview event never fires
|
||||
setTimeout(() => {
|
||||
if (this.isLoading) {
|
||||
console.log('Pywebview timeout, using random config');
|
||||
this.bridgeError = "Connecting to the desktop engine is taking longer than expected. If controls remain unavailable, restart the app and check desktop.log.";
|
||||
this.theme = pickRandom(randomizableThemes);
|
||||
this.lang = pickRandom(randomizableLangs);
|
||||
switchTheme(this.theme);
|
||||
@@ -692,15 +682,20 @@
|
||||
if (window.__pywebviewCompleteHandler) {
|
||||
window.removeEventListener('pywebview-complete', window.__pywebviewCompleteHandler);
|
||||
}
|
||||
window.__pywebviewCompleteHandler = () => {
|
||||
window.__pywebviewCompleteHandler = (event) => {
|
||||
this.processing = false;
|
||||
this.progress = 100;
|
||||
this.addLogEntry(this.t.logs?.done || "Done! File processed successfully.", "text-neon-pink font-bold");
|
||||
if (event.detail?.success) {
|
||||
this.progress = 100;
|
||||
this.addLogEntry(this.t.logs?.done || "Done! File processed successfully.", "text-neon-pink font-bold");
|
||||
} else {
|
||||
this.addLogEntry(event.detail?.cancelled ? "Processing stopped." : "Processing failed. See the error above and retry.", "text-error");
|
||||
}
|
||||
};
|
||||
window.addEventListener('pywebview-complete', window.__pywebviewCompleteHandler);
|
||||
|
||||
// Get static info once (CUDA, FFmpeg, GPU)
|
||||
this.loadStaticInfo();
|
||||
this.pollModels();
|
||||
// Poll dynamic info (RAM, CPU) every 3 seconds
|
||||
setInterval(() => this.updateDynamicInfo(), 3000);
|
||||
},
|
||||
@@ -792,7 +787,29 @@
|
||||
}
|
||||
},
|
||||
|
||||
modelsOpen: false,
|
||||
models: {status: 'unchecked', busy: true},
|
||||
bridgeError: '',
|
||||
async pollModels() {
|
||||
try {
|
||||
if (window.pywebview?.api) {
|
||||
const previousStatus = this.models.status;
|
||||
this.models = await pywebview.api.get_model_status();
|
||||
if (previousStatus !== this.models.status && (this.models.status === 'missing' || this.models.status === 'error')) this.modelsOpen = true;
|
||||
}
|
||||
} catch (error) { this.bridgeError = error.message; }
|
||||
setTimeout(() => this.pollModels(), 500);
|
||||
},
|
||||
async downloadModels() {
|
||||
const result = await pywebview.api.download_models();
|
||||
if (result.error) this.bridgeError = result.error;
|
||||
else { this.bridgeError = ''; this.models = {status: 'checking', busy: true}; }
|
||||
},
|
||||
modelProgress() {
|
||||
return this.models.total ? Math.min(100, Math.round(100 * this.models.current / this.models.total)) : 0;
|
||||
},
|
||||
async startProcessing() {
|
||||
if (this.models.status !== 'ready') { this.modelsOpen = true; return; }
|
||||
if (!this.inputPath) {
|
||||
this.addLogEntry(this.t.logs?.no_input || "No input selected. Please select a file first.", "text-error");
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
+4
-4
@@ -349,7 +349,7 @@ body.theme-coquette {
|
||||
}
|
||||
|
||||
body.theme-coquette .crt::before {
|
||||
background: url('https://www.transparenttextures.com/patterns/lace.png');
|
||||
background: url('assets/lace.svg');
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
@@ -402,7 +402,7 @@ body.theme-coquette input::placeholder {
|
||||
========================================= */
|
||||
body.theme-xp {
|
||||
background-color: #3a6ea5 !important;
|
||||
background-image: url('https://i.imgur.com/Hl4Nppq.jpg') !important;
|
||||
background-image: url('assets/hills.svg') !important;
|
||||
background-size: cover;
|
||||
background-position: center bottom;
|
||||
background-attachment: fixed;
|
||||
@@ -694,14 +694,14 @@ body.theme-anime {
|
||||
background-size: 40px 40px;
|
||||
font-family: 'Mali', 'Comic Sans MS', cursive !important;
|
||||
color: #ff69b4;
|
||||
cursor: url('https://cur.cursors-4u.net/anime/ani-12/ani1146.gif'), auto !important;
|
||||
cursor: url('assets/star-cursor.svg'), auto !important;
|
||||
}
|
||||
|
||||
body.theme-anime button,
|
||||
body.theme-anime a,
|
||||
body.theme-anime select,
|
||||
body.theme-anime input {
|
||||
cursor: url('https://cur.cursors-4u.net/anime/ani-12/ani1146.gif'), pointer !important;
|
||||
cursor: url('assets/star-cursor.svg'), pointer !important;
|
||||
}
|
||||
|
||||
body.theme-anime .crt::before { display: none !important; }
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
# MIT License
|
||||
|
||||
Copyright © 2019-2021 Caleb Porzio and contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Tailwind Labs, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
(()=>{var tt=!1,rt=!1,V=[],nt=-1;function Vt(e){Sn(e)}function Sn(e){V.includes(e)||V.push(e),An()}function Ee(e){let t=V.indexOf(e);t!==-1&&t>nt&&V.splice(t,1)}function An(){!rt&&!tt&&(tt=!0,queueMicrotask(On))}function On(){tt=!1,rt=!0;for(let e=0;e<V.length;e++)V[e](),nt=e;V.length=0,nt=-1,rt=!1}var T,k,$,ot,it=!0;function qt(e){it=!1,e(),it=!0}function Ut(e){T=e.reactive,$=e.release,k=t=>e.effect(t,{scheduler:r=>{it?Vt(r):r()}}),ot=e.raw}function st(e){k=e}function Wt(e){let t=()=>{};return[n=>{let i=k(n);return e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(o=>o())}),e._x_effects.add(i),t=()=>{i!==void 0&&(e._x_effects.delete(i),$(i))},i},()=>{t()}]}function q(e,t,r={}){e.dispatchEvent(new CustomEvent(t,{detail:r,bubbles:!0,composed:!0,cancelable:!0}))}function O(e,t){if(typeof ShadowRoot=="function"&&e instanceof ShadowRoot){Array.from(e.children).forEach(i=>O(i,t));return}let r=!1;if(t(e,()=>r=!0),r)return;let n=e.firstElementChild;for(;n;)O(n,t,!1),n=n.nextElementSibling}function v(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}var Gt=!1;function Jt(){Gt&&v("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),Gt=!0,document.body||v("Unable to initialize. Trying to load Alpine before `<body>` is available. Did you forget to add `defer` in Alpine's `<script>` tag?"),q(document,"alpine:init"),q(document,"alpine:initializing"),ce(),rr(t=>S(t,O)),Q(t=>ae(t)),Ae((t,r)=>{le(t,r).forEach(n=>n())});let e=t=>!U(t.parentElement,!0);Array.from(document.querySelectorAll(Zt().join(","))).filter(e).forEach(t=>{S(t)}),q(document,"alpine:initialized")}var at=[],Yt=[];function Xt(){return at.map(e=>e())}function Zt(){return at.concat(Yt).map(e=>e())}function ve(e){at.push(e)}function Se(e){Yt.push(e)}function U(e,t=!1){return Z(e,r=>{if((t?Zt():Xt()).some(i=>r.matches(i)))return!0})}function Z(e,t){if(e){if(t(e))return e;if(e._x_teleportBack&&(e=e._x_teleportBack),!!e.parentElement)return Z(e.parentElement,t)}}function Qt(e){return Xt().some(t=>e.matches(t))}var er=[];function tr(e){er.push(e)}function S(e,t=O,r=()=>{}){ir(()=>{t(e,(n,i)=>{r(n,i),er.forEach(o=>o(n,i)),le(n,n.attributes).forEach(o=>o()),n._x_ignore&&i()})})}function ae(e){O(e,t=>{ct(t),nr(t)})}var or=[],sr=[],ar=[];function rr(e){ar.push(e)}function Q(e,t){typeof t=="function"?(e._x_cleanups||(e._x_cleanups=[]),e._x_cleanups.push(t)):(t=e,sr.push(t))}function Ae(e){or.push(e)}function Ce(e,t,r){e._x_attributeCleanups||(e._x_attributeCleanups={}),e._x_attributeCleanups[t]||(e._x_attributeCleanups[t]=[]),e._x_attributeCleanups[t].push(r)}function ct(e,t){e._x_attributeCleanups&&Object.entries(e._x_attributeCleanups).forEach(([r,n])=>{(t===void 0||t.includes(r))&&(n.forEach(i=>i()),delete e._x_attributeCleanups[r])})}function nr(e){if(e._x_cleanups)for(;e._x_cleanups.length;)e._x_cleanups.pop()()}var ut=new MutationObserver(mt),ft=!1;function ce(){ut.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),ft=!0}function dt(){Cn(),ut.disconnect(),ft=!1}var ue=[],lt=!1;function Cn(){ue=ue.concat(ut.takeRecords()),ue.length&&!lt&&(lt=!0,queueMicrotask(()=>{Tn(),lt=!1}))}function Tn(){mt(ue),ue.length=0}function h(e){if(!ft)return e();dt();let t=e();return ce(),t}var pt=!1,Oe=[];function cr(){pt=!0}function lr(){pt=!1,mt(Oe),Oe=[]}function mt(e){if(pt){Oe=Oe.concat(e);return}let t=[],r=[],n=new Map,i=new Map;for(let o=0;o<e.length;o++)if(!e[o].target._x_ignoreMutationObserver&&(e[o].type==="childList"&&(e[o].addedNodes.forEach(s=>s.nodeType===1&&t.push(s)),e[o].removedNodes.forEach(s=>s.nodeType===1&&r.push(s))),e[o].type==="attributes")){let s=e[o].target,a=e[o].attributeName,c=e[o].oldValue,l=()=>{n.has(s)||n.set(s,[]),n.get(s).push({name:a,value:s.getAttribute(a)})},u=()=>{i.has(s)||i.set(s,[]),i.get(s).push(a)};s.hasAttribute(a)&&c===null?l():s.hasAttribute(a)?(u(),l()):u()}i.forEach((o,s)=>{ct(s,o)}),n.forEach((o,s)=>{or.forEach(a=>a(s,o))});for(let o of r)t.includes(o)||(sr.forEach(s=>s(o)),ae(o));t.forEach(o=>{o._x_ignoreSelf=!0,o._x_ignore=!0});for(let o of t)r.includes(o)||o.isConnected&&(delete o._x_ignoreSelf,delete o._x_ignore,ar.forEach(s=>s(o)),o._x_ignore=!0,o._x_ignoreSelf=!0);t.forEach(o=>{delete o._x_ignoreSelf,delete o._x_ignore}),t=null,r=null,n=null,i=null}function Te(e){return F(j(e))}function N(e,t,r){return e._x_dataStack=[t,...j(r||e)],()=>{e._x_dataStack=e._x_dataStack.filter(n=>n!==t)}}function j(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot=="function"&&e instanceof ShadowRoot?j(e.host):e.parentNode?j(e.parentNode):[]}function F(e){return new Proxy({objects:e},Rn)}var Rn={ownKeys({objects:e}){return Array.from(new Set(e.flatMap(t=>Object.keys(t))))},has({objects:e},t){return t==Symbol.unscopables?!1:e.some(r=>Object.prototype.hasOwnProperty.call(r,t))},get({objects:e},t,r){return t=="toJSON"?Mn:Reflect.get(e.find(n=>Object.prototype.hasOwnProperty.call(n,t))||{},t,r)},set({objects:e},t,r,n){let i=e.find(sLine truncated
|
||||
|
||||
${r?'Expression: "'+r+`"
|
||||
|
||||
`:""}`,t),setTimeout(()=>{throw e},0)}var Ne=!0;function Ie(e){let t=Ne;Ne=!1;let r=e();return Ne=t,r}function R(e,t,r={}){let n;return x(e,t)(i=>n=i,r),n}function x(...e){return dr(...e)}var dr=xt;function pr(e){dr=e}function xt(e,t){let r={};fe(r,e);let n=[r,...j(e)],i=typeof t=="function"?Pn(n,t):Dn(n,t,e);return fr.bind(null,e,t,i)}function Pn(e,t){return(r=()=>{},{scope:n={},params:i=[]}={})=>{let o=t.apply(F([n,...e]),i);Pe(r,o)}}var gt={};function In(e,t){if(gt[e])return gt[e];let r=Object.getPrototypeOf(async function(){}).constructor,n=/^[\n\s]*if.*\(.*\)/.test(e.trim())||/^(let|const)\s/.test(e.trim())?`(async()=>{ ${e} })()`:e,o=(()=>{try{let s=new r(["__self","scope"],`with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`);return Object.defineProperty(s,"name",{value:`[Alpine] ${e}`}),s}catch(s){return ee(s,t,e),Promise.resolve()}})();return gt[e]=o,o}function Dn(e,t,r){let n=In(t,r);return(i=()=>{},{scope:o={},params:s=[]}={})=>{n.result=void 0,n.finished=!1;let a=F([o,...e]);if(typeof n=="function"){let c=n(n,a).catch(l=>ee(l,r,t));n.finished?(Pe(i,n.result,a,s,r),n.result=void 0):c.then(l=>{Pe(i,l,a,s,r)}).catch(l=>ee(l,r,t)).finally(()=>n.result=void 0)}}}function Pe(e,t,r,n,i){if(Ne&&typeof t=="function"){let o=t.apply(r,n);o instanceof Promise?o.then(s=>Pe(e,s,r,n)).catch(s=>ee(s,i,t)):e(o)}else typeof t=="object"&&t instanceof Promise?t.then(o=>e(o)):e(t)}var Et="x-";function C(e=""){return Et+e}function mr(e){Et=e}var yt={};function d(e,t){return yt[e]=t,{before(r){if(!yt[r]){console.warn("Cannot find directive `${directive}`. `${name}` will use the default order of execution");return}let n=W.indexOf(r);W.splice(n>=0?n:W.indexOf("DEFAULT"),0,e)}}}function le(e,t,r){if(t=Array.from(t),e._x_virtualDirectives){let o=Object.entries(e._x_virtualDirectives).map(([a,c])=>({name:a,value:c})),s=vt(o);o=o.map(a=>s.find(c=>c.name===a.name)?{name:`x-bind:${a.name}`,value:`"${a.value}"`}:a),t=t.concat(o)}let n={};return t.map(_r((o,s)=>n[o]=s)).filter(xr).map(Ln(n,r)).sort($n).map(o=>kn(e,o))}function vt(e){return Array.from(e).map(_r()).filter(t=>!xr(t))}var bt=!1,de=new Map,hr=Symbol();function ir(e){bt=!0;let t=Symbol();hr=t,de.set(t,[]);let r=()=>{for(;de.get(t).length;)de.get(t).shift()();de.delete(t)},n=()=>{bt=!1,r()};e(r),n()}function _t(e){let t=[],r=a=>t.push(a),[n,i]=Wt(e);return t.push(i),[{Alpine:B,effect:n,cleanup:r,evaluateLater:x.bind(x,e),evaluate:R.bind(R,e)},()=>t.forEach(a=>a())]}function kn(e,t){let r=()=>{},n=yt[t.type]||r,[i,o]=_t(e);Ce(e,t.original,o);let s=()=>{e._x_ignore||e._x_ignoreSelf||(n.inline&&n.inline(e,t,i),n=n.bind(n,e,t,i),bt?de.get(hr).push(n):n())};return s.runCleanups=o,s}var De=(e,t)=>({name:r,value:n})=>(r.startsWith(e)&&(r=r.replace(e,t)),{name:r,value:n}),ke=e=>e;function _r(e=()=>{}){return({name:t,value:r})=>{let{name:n,value:i}=gr.reduce((o,s)=>s(o),{name:t,value:r});return n!==t&&e(n,t),{name:n,value:i}}}var gr=[];function te(e){gr.push(e)}function xr({name:e}){return yr().test(e)}var yr=()=>new RegExp(`^${Et}([^:^.]+)\\b`);function Ln(e,t){return({name:r,value:n})=>{let i=r.match(yr()),o=r.match(/:([a-zA-Z0-9\-_:]+)/),s=r.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],a=t||e[r]||r;return{type:i?i[1]:null,value:o?o[1]:null,modifiers:s.map(c=>c.replace(".","")),expression:n,original:a}}}var wt="DEFAULT",W=["ignore","ref","data","id","anchor","bind","init","for","model","modelable","transition","show","if",wt,"teleport"];function $n(e,t){let r=W.indexOf(e.type)===-1?wt:e.type,n=W.indexOf(t.type)===-1?wt:t.type;return W.indexOf(r)-W.indexOf(n)}var St=[],At=!1;function re(e=()=>{}){return queueMicrotask(()=>{At||setTimeout(()=>{Le()})}),new Promise(t=>{St.push(()=>{e(),t()})})}function Le(){for(At=!1;St.length;)St.shift()()}function br(){At=!0}function pe(e,t){return Array.isArray(t)?wr(e,t.join(" ")):typeof t=="object"&&t!==null?jn(e,t):typeof t=="function"?pe(e,t()):wr(e,t)}function wr(e,t){let r=o=>o.split(" ").filter(Boolean),n=o=>o.split(" ").filter(s=>!e.classList.contains(s)).filter(Boolean),i=o=>(e.classList.add(...o),()=>{e.classList.remove(...o)});return t=t===!0?t="":t||"",i(n(t))}function jn(e,t){let r=a=>a.split(" ").filter(Boolean),n=Object.entries(t).flatMap(([a,c])=>c?r(a):!1).filter(Boolean),i=Object.entries(t).flatMap(([a,c])=>c?!1:r(a)).filter(Boolean),o=[],s=[];return i.forEach(a=>{e.classList.contains(a)&&(e.classList.remove(a),s.push(a))}),n.forEach(a=>{e.classList.contains(a)||(e.classList.add(a),o.push(a))}),()=>{s.forEach(a=>e.classList.add(a)),o.forEach(a=>e.classList.remove(a))}}function G(e,t){return typeof t=="object"&&t!==null?Fn(e,t):Bn(e,t)}function Fn(e,t){let r={};return Object.entries(t).forEach(([n,i])=>{r[n]=e.style[n],n.startsWith("--")||(n=Kn(n)),e.style.setProperty(n,i)}),setTimeout(()=>{e.style.length===0&&e.removeAttribute("style")}),()=>{G(e,r)}}function Bn(e,t){let r=e.getAttribute("style",t);return e.setAttribute("style",t),()=>{e.setAttribute("style",r||"")}}function Kn(e){return e.replace(/Line truncated
|
||||
Vendored
+93
@@ -0,0 +1,93 @@
|
||||
Copyright 2020 The Cinzel Project Authors (https://github.com/NDISCOVER/Cinzel)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Vendored
+93
@@ -0,0 +1,93 @@
|
||||
Copyright 2016 The Dancing Script Project Authors (https://github.com/googlefonts/DancingScript), with Reserved Font Name 'Dancing Script'.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Vendored
+121
@@ -0,0 +1,121 @@
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfMZg.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
font-display: swap;
|
||||
src: url(UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuDyYMZg.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url(UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuBWYMZg.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Rubik Glitch';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(qkBSXv8b_srFRYQVYrDKh9ZvmA.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(i7dNIFZifjKcF5UAWdDRYER8QA.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(i7dPIFZifjKcF5UAWdDRUEY.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(i7dMIFZifjKcF5UAWdDRaPpZYFI.ttf) format('truetype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Cinzel';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(8vIU7ww63mVu7gtR-kwKxNvkNOjw-tbnTYo.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Cinzel';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(8vIU7ww63mVu7gtR-kwKxNvkNOjw-jHgTYo.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Dancing Script';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(If2cXTr6YS-zF4S-kcSWSVi_sxjsohD9F50Ruu7B1i0HTQ.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Mali';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(N0bU2SRONuN4SCjECg.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Mali';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(N0ba2SRONuN4eCo.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Mali';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(N0bV2SRONuN4QJbhKlQ.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Oswald';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(TK3_WkUHHAIjg75cFRf3bXL8LICs1_FvgUE.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Oswald';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(TK3_WkUHHAIjg75cFRf3bXL8LICs1xZogUE.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Playfair Display';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(nuFRD-vYSZviVYUb_rj3ij__anPXDTnCjmHKM4nYO7KN_qiTbtY.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Playfair Display';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(nuFvD-vYSZviVYUb_rj3ij__anPXJzDwcbmjWBN2PKdFvUDQ.ttf) format('truetype');
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
+93
@@ -0,0 +1,93 @@
|
||||
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Vendored
+93
@@ -0,0 +1,93 @@
|
||||
Copyright 2018 The Mali Project Authors (https://github.com/cadsondemak/Mali)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"8vIU7ww63mVu7gtR-kwKxNvkNOjw-jHgTYo.ttf": "2c6b1ff0d368efb73636700b69dbef707fa5602a06d085a4e91e5125082651f0",
|
||||
"8vIU7ww63mVu7gtR-kwKxNvkNOjw-tbnTYo.ttf": "a0242f6698ae0d3220a29986a09274f9d1eddb4a24da4e50faf387fd1804eb7b",
|
||||
"alpine-3.13.3.min.js": "c8fa8ff457abdcd212f37a07ef2f292c999011dffabcaa577fb1e1e0076ca658",
|
||||
"ALPINE-LICENSE.md": "5f05c4925dd3edf049a70b4b403ac0dd43b20f72501d9359911ddd25d5ac657d",
|
||||
"cinzel-OFL.txt": "f2b3029aba64c378bf0963b62945eee15e564fe4330b934c8f2eb058282b5e83",
|
||||
"dancingscript-OFL.txt": "6f090277c00af96651ce6dbcc38ff1591047a3bffef486e80b6a32e8276a8201",
|
||||
"fonts.css": "1d5f79c43b263a0c40011c992b0b270b68e19d29faef51ea9c605bb3950e3872",
|
||||
"i7dMIFZifjKcF5UAWdDRaPpZYFI.ttf": "5be2b984cdb8befc8ad1871d10f1aa8337f7d2bae3c6bf0ce720d0745ac1a8ec",
|
||||
"i7dNIFZifjKcF5UAWdDRYER8QA.ttf": "71fc772fd07392d445baf8b3c374f71896909c6152b91d4f1d750ee4212d5c4a",
|
||||
"i7dPIFZifjKcF5UAWdDRUEY.ttf": "829aad32ab9525358de2a7ebc718d05a6b335e67421aad5e1e09cd1fa796af4e",
|
||||
"If2cXTr6YS-zF4S-kcSWSVi_sxjsohD9F50Ruu7B1i0HTQ.ttf": "d2b3a6ce8d91be38a76aa1a1696a9e184140ac5fb5c3aa6a1b8f03dabd9560ee",
|
||||
"inter-OFL.txt": "5b9321a4298cfeb6b34354164a1c3afc3db114569984c502b9b35d988fd58c57",
|
||||
"mali-OFL.txt": "bc1175730d609233043cde47e48f69831f012e85537c5ec1a2cf4836757c1540",
|
||||
"N0ba2SRONuN4eCo.ttf": "65c0bb03aebbd47f9708a71ec588dde3754dbee055c1f64cbb41b4390fe2dfe3",
|
||||
"N0bU2SRONuN4SCjECg.ttf": "7267ed0bc4603fa3d78f65a12ec38ac0f13a405eb2e655bbd91ee54a9cf92541",
|
||||
"N0bV2SRONuN4QJbhKlQ.ttf": "db7eeb7fdf5f16e2c0c2466d6bf88af41ab0547207b392de2a764f1f89c8fda2",
|
||||
"nuFRD-vYSZviVYUb_rj3ij__anPXDTnCjmHKM4nYO7KN_qiTbtY.ttf": "6021680663c58b344d8404c289c755aadc662a02189ffe4f5ca79378f9e97840",
|
||||
"nuFvD-vYSZviVYUb_rj3ij__anPXJzDwcbmjWBN2PKdFvUDQ.ttf": "1609a54fa3ad1abc8d0037132f73700333241ea0b65672079998e352f151dea7",
|
||||
"oswald-OFL.txt": "0fd731a904b729a4e02eaf5e8ebd06783edd9abe400e8882760160230675b652",
|
||||
"playfairdisplay-OFL.txt": "566be814f8e96e93dfa16101331557eb6b5467e9e03f627c0910fe93ca12300e",
|
||||
"qkBSXv8b_srFRYQVYrDKh9ZvmA.ttf": "b89ee9ff618965971fe59abcf0798b887ed870944044eda7dc2ea02269bc98e2",
|
||||
"rubikglitch-OFL.txt": "0b6ed65cab9d0dd41ca65913ec505d8a7fc7797056aa14a9ab36b0fda1daa691",
|
||||
"spacemono-OFL.txt": "8e4ee42b2553e1e01504e61cb0d46d148cd8c9e5eacaa3622a7df2d4f2955b9f",
|
||||
"TAILWIND-LICENSE": "60e0b68c0f35c078eef3a5d29419d0b03ff84ec1df9c3f9d6e39a519a5ae7985",
|
||||
"tailwind.css": "f8ad0f81e7e5b9070c7f84d4ec2e191b8ed6d1bbbcc364cbd6f651420d87e242",
|
||||
"TK3_WkUHHAIjg75cFRf3bXL8LICs1xZogUE.ttf": "a6ee8b91ec269a6f48631ba4ba1a3ab7e5039e5482dda8dc9672ee4620d7bd68",
|
||||
"TK3_WkUHHAIjg75cFRf3bXL8LICs1_FvgUE.ttf": "bd6ff5a83bcdc92d7aac6dbb16e4dd2b0b0320bec6402bb81f5dd9735b500e46",
|
||||
"UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuBWYMZg.ttf": "5b717029a2c4cfca2167e97be1157edb24151591e5ec96eb484745c392817a7a",
|
||||
"UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuDyYMZg.ttf": "eec66af7f2337bd34fe6e801cf92ededcb57a20c0d7bc40a61d4eefcbe3dd40c",
|
||||
"UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfMZg.ttf": "1b08e7fc267a5c7e1d614100f604b83e7e8a0be241f0f288faa2b3ac93a683ba"
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Vendored
+93
@@ -0,0 +1,93 @@
|
||||
Copyright 2016 The Oswald Project Authors (https://github.com/googlefonts/OswaldFont)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Vendored
+93
@@ -0,0 +1,93 @@
|
||||
Copyright 2017 The Playfair Display Project Authors (https://github.com/clauseggers/Playfair-Display), with Reserved Font Name "Playfair Display"
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
BIN
Binary file not shown.
Vendored
+93
@@ -0,0 +1,93 @@
|
||||
Copyright 2020 The Rubik Filtered Project Authors (https://github.com/NaN-xyz/Rubik-Filtered)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Vendored
+93
@@ -0,0 +1,93 @@
|
||||
Copyright 2016 The Space Mono Project Authors (https://github.com/googlefonts/spacemono)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Inter,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:Space Mono,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.collapse{visibility:collapse}.staLine truncated
|
||||
Reference in new issue
Block a user