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:
D-Ogi authored and GitHub committed 2026-09-16 13:24:57 +02:00
1 parent 8af9726666
commit f4ffa1c2b6
72 files changed
+3547 -189

No files matched your search

+3
View File
@@ -26,3 +26,6 @@
*.pth binary
*.ckpt binary
*.safetensors binary
# Preserve pinned vendor bytes, including upstream license formatting.
ui/vendor/** -text -whitespace
+26 -3
View File
@@ -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)" }
+55
View File
@@ -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
+3
View File
@@ -58,3 +58,6 @@ huggingface/
.venv-test/
.pytest_cache/
.ruff_cache/
node_modules/
build-downloads/
+2 -2
View File
@@ -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
+6
View File
@@ -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
+19
View File
@@ -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.
+1
View File
@@ -0,0 +1 @@
0.68.0
+8
View File
@@ -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()
+83
View File
@@ -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)
+53
View File
@@ -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)
+102
View File
@@ -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.
+7 -3
View File
@@ -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
View File
@@ -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()
+61
View File
@@ -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"
}
]
}
+1021
View File
File diff suppressed because it is too large. Load diff
+9
View File
@@ -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"
}
}
+48
View File
@@ -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;
}
}
}
+5
View File
@@ -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"
}
+7 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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 -1
View File
@@ -1,3 +1,3 @@
@echo off
cd /d "%~dp0"
python\pythonw.exe remwmgui.py
start "" "%~dp0python\pythonw.exe" "%~dp0desktop_main.py"
+149
View File
@@ -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()
+79
View File
@@ -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))
+3 -5
View File
@@ -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
)
+3 -19
View File
@@ -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
+2 -4
View File
@@ -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 " ============================================="
+36
View File
@@ -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%)' },
}
}
}
}
};
+32
View File
@@ -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()
+101
View File
@@ -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)
+168
View File
@@ -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
+51
View File
@@ -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
+62
View File
@@ -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"
+1
View File
@@ -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

+1
View File
@@ -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

+1
View File
@@ -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
View File
@@ -1,5 +1,5 @@
{
"version": "0.67",
"version": "0.68.0",
"themes": [
{ "id": "brainrot", "name": "Brainrot" },
{ "id": "slay", "name": "Slay Queen" },
+68 -51
View File
@@ -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;
+3
View File
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+4 -4
View File
@@ -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.
+21
View File
@@ -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.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+21
View File
@@ -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.
+5
View File
@@ -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
+93
View File
@@ -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.
+93
View File
@@ -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.
+121
View File
@@ -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');
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
+93
View File
@@ -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.
+93
View File
@@ -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.
+32
View File
@@ -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.
+93
View File
@@ -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.
+93
View File
@@ -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.
Binary file not shown.
+93
View File
@@ -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.
+93
View File
@@ -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.
+1
View File
@@ -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