Spaces:
Running
Running
Upload 37 files
Browse files- Dockerfile +40 -0
- README.md +166 -5
- core/bch.ts +395 -0
- core/crc.ts +72 -0
- core/dct.ts +156 -0
- core/detector.ts +340 -0
- core/dmqim.ts +69 -0
- core/dwt.ts +221 -0
- core/embedder.ts +187 -0
- core/keygen.ts +129 -0
- core/masking.ts +54 -0
- core/presets.ts +77 -0
- core/tiling.ts +178 -0
- core/types.ts +81 -0
- package-lock.json +2826 -0
- package.json +34 -0
- server/api.ts +218 -0
- server/cli.ts +203 -0
- server/ffmpeg-io.ts +163 -0
- tsconfig.json +27 -0
- tsconfig.server.json +24 -0
- vite.config.ts +31 -0
- web/.DS_Store +0 -0
- web/index.html +13 -0
- web/src/.DS_Store +0 -0
- web/src/App.tsx +88 -0
- web/src/components/ApiDocs.tsx +277 -0
- web/src/components/ComparisonView.tsx +197 -0
- web/src/components/DetectPanel.tsx +155 -0
- web/src/components/EmbedPanel.tsx +279 -0
- web/src/components/ResultCard.tsx +102 -0
- web/src/components/RobustnessTest.tsx +167 -0
- web/src/components/StrengthSlider.tsx +91 -0
- web/src/index.css +1 -0
- web/src/lib/video-io.ts +408 -0
- web/src/main.tsx +10 -0
- web/src/workers/watermark.worker.ts +62 -0
Dockerfile
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:22-slim AS builder
|
| 2 |
+
|
| 3 |
+
# Install FFmpeg for server-side video processing
|
| 4 |
+
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
|
| 5 |
+
|
| 6 |
+
WORKDIR /app
|
| 7 |
+
|
| 8 |
+
# Copy package files and install dependencies
|
| 9 |
+
COPY package.json package-lock.json* ./
|
| 10 |
+
RUN npm ci --ignore-scripts
|
| 11 |
+
|
| 12 |
+
# Copy source code
|
| 13 |
+
COPY . .
|
| 14 |
+
|
| 15 |
+
# Build web UI and check types
|
| 16 |
+
RUN npx vite build --config vite.config.ts
|
| 17 |
+
|
| 18 |
+
# Production stage
|
| 19 |
+
FROM node:22-slim
|
| 20 |
+
|
| 21 |
+
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
|
| 22 |
+
|
| 23 |
+
WORKDIR /app
|
| 24 |
+
|
| 25 |
+
COPY package.json package-lock.json* ./
|
| 26 |
+
RUN npm ci --ignore-scripts --omit=dev 2>/dev/null || npm ci --ignore-scripts
|
| 27 |
+
|
| 28 |
+
# Copy built web assets and source (for tsx runtime)
|
| 29 |
+
COPY --from=builder /app/dist/web ./dist/web
|
| 30 |
+
COPY core/ ./core/
|
| 31 |
+
COPY server/ ./server/
|
| 32 |
+
|
| 33 |
+
# HuggingFace Spaces expects port 7860
|
| 34 |
+
ENV PORT=7860
|
| 35 |
+
ENV STATIC_DIR=/app/dist/web
|
| 36 |
+
EXPOSE 7860
|
| 37 |
+
|
| 38 |
+
# Run the API server using tsx for TypeScript execution
|
| 39 |
+
RUN npm install -g tsx
|
| 40 |
+
CMD ["tsx", "server/api.ts"]
|
README.md
CHANGED
|
@@ -1,10 +1,171 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: LTMarX
|
| 3 |
+
emoji: 🎬
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# LTMarX — Video Watermarking
|
| 12 |
+
|
| 13 |
+
Imperceptible 32-bit watermarking for video. Embeds a payload into the luminance channel using DWT/DCT transform-domain quantization (DM-QIM) with BCH error correction. Survives re-encoding, rescaling, brightness/contrast/saturation changes.
|
| 14 |
+
|
| 15 |
+
All processing runs in the browser — no server round-trips needed.
|
| 16 |
+
|
| 17 |
+
## Quick Start
|
| 18 |
+
|
| 19 |
+
```bash
|
| 20 |
+
npm install
|
| 21 |
+
npm run dev # Web UI at localhost:5173
|
| 22 |
+
npm test # Run test suite
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
## CLI
|
| 26 |
+
|
| 27 |
+
```bash
|
| 28 |
+
npx tsx server/cli.ts embed -i input.mp4 -o output.mp4 --key SECRET --preset moderate --payload DEADBEEF
|
| 29 |
+
npx tsx server/cli.ts detect -i output.mp4 --key SECRET
|
| 30 |
+
npx tsx server/cli.ts presets
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
## Docker
|
| 34 |
+
|
| 35 |
+
```bash
|
| 36 |
+
docker build -t ltmarx .
|
| 37 |
+
docker run -p 7860:7860 ltmarx
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
## Architecture
|
| 41 |
+
|
| 42 |
+
```
|
| 43 |
+
core/ Pure TypeScript watermark engine (isomorphic, zero platform deps)
|
| 44 |
+
├── dwt.ts Haar DWT (forward/inverse, multi-level)
|
| 45 |
+
├── dct.ts 8×8 DCT with zigzag scan
|
| 46 |
+
├── dmqim.ts Dither-Modulated QIM (embed/extract with soft decisions)
|
| 47 |
+
├── bch.ts BCH codec (GF(2^m), Berlekamp-Massey decoding)
|
| 48 |
+
├── crc.ts CRC-4 / CRC-8 / CRC-16
|
| 49 |
+
├── tiling.ts Periodic tile layout for redundant embedding
|
| 50 |
+
├── masking.ts Perceptual masking (variance-adaptive delta)
|
| 51 |
+
├── keygen.ts Seeded PRNG for dithers and permutations
|
| 52 |
+
├── embedder.ts Y-plane → watermarked Y-plane
|
| 53 |
+
├── detector.ts Y-plane → payload + confidence
|
| 54 |
+
├── presets.ts Named configurations (light → fortress)
|
| 55 |
+
└── types.ts Shared types
|
| 56 |
+
|
| 57 |
+
web/ Frontend (Vite + React + Tailwind)
|
| 58 |
+
├── src/
|
| 59 |
+
│ ├── App.tsx
|
| 60 |
+
│ ├── components/
|
| 61 |
+
│ │ ├── EmbedPanel.tsx Upload, configure, embed
|
| 62 |
+
│ │ ├── DetectPanel.tsx Upload, detect, display results
|
| 63 |
+
│ │ ├── ApiDocs.tsx Inline API reference
|
| 64 |
+
│ │ ├── ComparisonView.tsx Side-by-side / difference viewer
|
| 65 |
+
│ │ ├── RobustnessTest.tsx Automated attack testing
|
| 66 |
+
│ │ ├── StrengthSlider.tsx Preset selector with snap points
|
| 67 |
+
│ │ └── ResultCard.tsx Detection result display
|
| 68 |
+
│ ├── lib/
|
| 69 |
+
│ │ └── video-io.ts Frame extraction, encoding, attack utilities
|
| 70 |
+
│ └── workers/
|
| 71 |
+
│ └── watermark.worker.ts
|
| 72 |
+
└── index.html
|
| 73 |
+
|
| 74 |
+
server/ Node.js CLI + HTTP API
|
| 75 |
+
├── cli.ts CLI for embed/detect
|
| 76 |
+
├── api.ts HTTP server (serves web UI + REST endpoints)
|
| 77 |
+
└── ffmpeg-io.ts FFmpeg subprocess for YUV420p I/O
|
| 78 |
+
|
| 79 |
+
tests/ Vitest test suite
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
**Design principle:** `core/` has zero platform dependencies — it operates on raw `Uint8Array` Y-plane buffers. The same code runs in the browser (via Canvas + ffmpeg.wasm) and on the server (via Node.js + FFmpeg).
|
| 83 |
+
|
| 84 |
+
## Watermarking Pipeline
|
| 85 |
+
|
| 86 |
+
### Embedding
|
| 87 |
+
|
| 88 |
+
```
|
| 89 |
+
Y plane → 2-level Haar DWT → HL subband → tile grid →
|
| 90 |
+
per tile: 8×8 DCT blocks → select mid-freq coefficients →
|
| 91 |
+
DM-QIM embed coded bits → inverse DCT → inverse DWT → modified Y plane
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
### Payload Encoding
|
| 95 |
+
|
| 96 |
+
```
|
| 97 |
+
32-bit payload → CRC append → BCH encode → keyed interleave → map to coefficients
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
### Detection
|
| 101 |
+
|
| 102 |
+
```
|
| 103 |
+
Y plane → DWT → HL subband → tile grid →
|
| 104 |
+
per tile: DCT → DM-QIM soft extract →
|
| 105 |
+
soft-combine across tiles and frames → BCH decode → CRC verify → payload
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
## Presets
|
| 109 |
+
|
| 110 |
+
| Preset | Delta | Tile Period | BCH Code | Masking | Use Case |
|
| 111 |
+
|--------|-------|-------------|----------|---------|----------|
|
| 112 |
+
| **Light** | 50 | 256px | (63,36,5) | No | Near-invisible, mild compression |
|
| 113 |
+
| **Moderate** | 80 | 232px | (63,36,5) | Yes | Balanced with perceptual masking |
|
| 114 |
+
| **Strong** | 110 | 208px | (63,36,5) | Yes | More frequencies, handles rescaling |
|
| 115 |
+
| **Fortress** | 150 | 192px | (63,36,5) | Yes | Maximum robustness |
|
| 116 |
+
|
| 117 |
+
## API
|
| 118 |
+
|
| 119 |
+
### Embedding
|
| 120 |
+
|
| 121 |
+
```typescript
|
| 122 |
+
import { embedWatermark } from './core/embedder';
|
| 123 |
+
import { getPreset } from './core/presets';
|
| 124 |
+
|
| 125 |
+
const config = getPreset('moderate');
|
| 126 |
+
const result = embedWatermark(yPlane, width, height, payload, key, config);
|
| 127 |
+
// result.yPlane: watermarked Y plane
|
| 128 |
+
// result.psnr: quality metric (dB)
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
### Detection
|
| 132 |
+
|
| 133 |
+
```typescript
|
| 134 |
+
import { detectWatermarkMultiFrame } from './core/detector';
|
| 135 |
+
import { getPreset } from './core/presets';
|
| 136 |
+
|
| 137 |
+
const result = detectWatermarkMultiFrame(yPlanes, width, height, key, config);
|
| 138 |
+
// result.detected: boolean
|
| 139 |
+
// result.payload: Uint8Array | null
|
| 140 |
+
// result.confidence: 0–1
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
### Auto-Detection (tries all presets)
|
| 144 |
+
|
| 145 |
+
```typescript
|
| 146 |
+
import { autoDetectMultiFrame } from './core/detector';
|
| 147 |
+
|
| 148 |
+
const result = autoDetectMultiFrame(yPlanes, width, height, key);
|
| 149 |
+
// result.presetUsed: which preset matched
|
| 150 |
+
```
|
| 151 |
+
|
| 152 |
+
## HTTP API
|
| 153 |
+
|
| 154 |
+
```
|
| 155 |
+
POST /api/embed { videoBase64, key, preset, payload }
|
| 156 |
+
POST /api/detect { videoBase64, key, preset?, frames? }
|
| 157 |
+
GET /api/health → { status: "ok" }
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
## Testing
|
| 161 |
+
|
| 162 |
+
```bash
|
| 163 |
+
npm test # Run all tests
|
| 164 |
+
npm run test:watch # Watch mode
|
| 165 |
+
```
|
| 166 |
+
|
| 167 |
+
Tests cover: DWT round-trip, DCT round-trip, DM-QIM embed/extract, BCH encode/decode with error correction, CRC append/verify, full embed→detect pipeline across presets, false positive rejection, wrong key rejection.
|
| 168 |
+
|
| 169 |
+
## Browser Encoding
|
| 170 |
+
|
| 171 |
+
The web UI encodes watermarked video using ffmpeg.wasm (x264 in WebAssembly). To avoid memory pressure, frames are encoded in chunks of 100 and concatenated at the end. Peak memory stays at ~chunk size rather than scaling with video length.
|
core/bch.ts
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* BCH (Bose-Chaudhuri-Hocquenghem) Error-Correcting Code
|
| 3 |
+
*
|
| 4 |
+
* Supports configurable BCH(n, k, t) codes over GF(2^m).
|
| 5 |
+
* Uses Berlekamp-Massey decoding with Chien search.
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import type { BchParams } from './types.js';
|
| 9 |
+
|
| 10 |
+
// Primitive polynomials for GF(2^m)
|
| 11 |
+
const PRIMITIVE_POLYS: Record<number, number> = {
|
| 12 |
+
6: 0x43, // x^6 + x + 1
|
| 13 |
+
7: 0x89, // x^7 + x^3 + 1
|
| 14 |
+
8: 0x11D, // x^8 + x^4 + x^3 + x^2 + 1
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
/**
|
| 18 |
+
* Galois Field GF(2^m) arithmetic
|
| 19 |
+
*/
|
| 20 |
+
export class GaloisField {
|
| 21 |
+
readonly m: number;
|
| 22 |
+
readonly size: number; // 2^m
|
| 23 |
+
readonly expTable: Int32Array; // alpha^i → element
|
| 24 |
+
readonly logTable: Int32Array; // element → i (log_alpha)
|
| 25 |
+
|
| 26 |
+
constructor(m: number) {
|
| 27 |
+
this.m = m;
|
| 28 |
+
this.size = 1 << m;
|
| 29 |
+
const poly = PRIMITIVE_POLYS[m];
|
| 30 |
+
if (!poly) throw new Error(`No primitive polynomial for GF(2^${m})`);
|
| 31 |
+
|
| 32 |
+
this.expTable = new Int32Array(this.size * 2);
|
| 33 |
+
this.logTable = new Int32Array(this.size);
|
| 34 |
+
this.logTable[0] = -1; // log(0) undefined
|
| 35 |
+
|
| 36 |
+
let val = 1;
|
| 37 |
+
for (let i = 0; i < this.size - 1; i++) {
|
| 38 |
+
this.expTable[i] = val;
|
| 39 |
+
this.logTable[val] = i;
|
| 40 |
+
val <<= 1;
|
| 41 |
+
if (val >= this.size) {
|
| 42 |
+
val ^= poly;
|
| 43 |
+
val &= this.size - 1;
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
// Extend exp table for easier modular arithmetic
|
| 47 |
+
for (let i = this.size - 1; i < this.size * 2; i++) {
|
| 48 |
+
this.expTable[i] = this.expTable[i - (this.size - 1)];
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
mul(a: number, b: number): number {
|
| 53 |
+
if (a === 0 || b === 0) return 0;
|
| 54 |
+
return this.expTable[this.logTable[a] + this.logTable[b]];
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
div(a: number, b: number): number {
|
| 58 |
+
if (b === 0) throw new Error('Division by zero in GF');
|
| 59 |
+
if (a === 0) return 0;
|
| 60 |
+
let exp = this.logTable[a] - this.logTable[b];
|
| 61 |
+
if (exp < 0) exp += this.size - 1;
|
| 62 |
+
return this.expTable[exp];
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
pow(a: number, e: number): number {
|
| 66 |
+
if (a === 0) return e === 0 ? 1 : 0;
|
| 67 |
+
let log = (this.logTable[a] * e) % (this.size - 1);
|
| 68 |
+
if (log < 0) log += this.size - 1;
|
| 69 |
+
return this.expTable[log];
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
inv(a: number): number {
|
| 73 |
+
if (a === 0) throw new Error('Inverse of zero');
|
| 74 |
+
return this.expTable[this.size - 1 - this.logTable[a]];
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
/**
|
| 79 |
+
* Compute the generator polynomial for a binary BCH code.
|
| 80 |
+
* The generator is the LCM of minimal polynomials of alpha^1 through alpha^(2t).
|
| 81 |
+
* All minimal polynomials over GF(2) have binary (0/1) coefficients.
|
| 82 |
+
*/
|
| 83 |
+
function computeGeneratorPoly(gf: GaloisField, t: number): Uint8Array {
|
| 84 |
+
// Start with g(x) = 1 as a binary polynomial
|
| 85 |
+
let gen = new Uint8Array([1]);
|
| 86 |
+
const order = gf.size - 1;
|
| 87 |
+
const processed = new Set<number>();
|
| 88 |
+
|
| 89 |
+
for (let i = 1; i <= 2 * t; i++) {
|
| 90 |
+
// Normalize i modulo the field order
|
| 91 |
+
const norm = i % order;
|
| 92 |
+
if (processed.has(norm)) continue;
|
| 93 |
+
|
| 94 |
+
// Find conjugate class of alpha^i: {i, 2i, 4i, ...} mod order
|
| 95 |
+
const conjugates: number[] = [];
|
| 96 |
+
let r = norm;
|
| 97 |
+
for (let j = 0; j < gf.m; j++) {
|
| 98 |
+
const rNorm = r % order;
|
| 99 |
+
if (conjugates.includes(rNorm)) break;
|
| 100 |
+
conjugates.push(rNorm);
|
| 101 |
+
processed.add(rNorm);
|
| 102 |
+
r = (r * 2) % order;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
// Compute minimal polynomial: product of (x + alpha^c) for c in conjugate class
|
| 106 |
+
// Start with [1] (= 1), then multiply by each (x + alpha^c)
|
| 107 |
+
// Coefficients are in GF(2^m), but we reduce to binary at the end
|
| 108 |
+
let minPoly = [1]; // GF(2^m) coefficients
|
| 109 |
+
for (const c of conjugates) {
|
| 110 |
+
const root = gf.expTable[c];
|
| 111 |
+
const newPoly = new Array(minPoly.length + 1).fill(0);
|
| 112 |
+
for (let k = 0; k < minPoly.length; k++) {
|
| 113 |
+
newPoly[k + 1] ^= minPoly[k]; // x * term
|
| 114 |
+
newPoly[k] ^= gf.mul(minPoly[k], root); // root * term
|
| 115 |
+
}
|
| 116 |
+
minPoly = newPoly;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
// Convert to binary (all coefficients should be 0 or 1 for minimal polys over GF(2))
|
| 120 |
+
const binaryMinPoly = new Uint8Array(minPoly.length);
|
| 121 |
+
for (let j = 0; j < minPoly.length; j++) {
|
| 122 |
+
binaryMinPoly[j] = minPoly[j] === 0 ? 0 : 1;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
// Multiply gen by binaryMinPoly (binary polynomial multiplication)
|
| 126 |
+
const newGen = new Uint8Array(gen.length + binaryMinPoly.length - 1);
|
| 127 |
+
for (let a = 0; a < gen.length; a++) {
|
| 128 |
+
if (!gen[a]) continue;
|
| 129 |
+
for (let b = 0; b < binaryMinPoly.length; b++) {
|
| 130 |
+
if (binaryMinPoly[b]) {
|
| 131 |
+
newGen[a + b] ^= 1;
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
+
gen = newGen;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
return gen;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
/**
|
| 142 |
+
* BCH Encoder/Decoder
|
| 143 |
+
*/
|
| 144 |
+
export class BchCodec {
|
| 145 |
+
readonly params: BchParams;
|
| 146 |
+
readonly gf: GaloisField;
|
| 147 |
+
private readonly genPoly: Uint8Array;
|
| 148 |
+
private readonly genPolyDeg: number;
|
| 149 |
+
|
| 150 |
+
constructor(params: BchParams) {
|
| 151 |
+
this.params = params;
|
| 152 |
+
this.gf = new GaloisField(params.m);
|
| 153 |
+
this.genPoly = computeGeneratorPoly(this.gf, params.t);
|
| 154 |
+
this.genPolyDeg = this.genPoly.length - 1;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
/**
|
| 158 |
+
* Encode message bits (length k) into codeword bits (length n)
|
| 159 |
+
* Systematic encoding: message bits appear at the start
|
| 160 |
+
*/
|
| 161 |
+
encode(message: Uint8Array): Uint8Array {
|
| 162 |
+
const { n, k } = this.params;
|
| 163 |
+
if (message.length !== k) {
|
| 164 |
+
throw new Error(`Message length ${message.length} !== k=${k}`);
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
const nk = n - k;
|
| 168 |
+
const codeword = new Uint8Array(n);
|
| 169 |
+
codeword.set(message);
|
| 170 |
+
|
| 171 |
+
// Compute remainder: m(x) * x^(n-k) mod g(x) using binary polynomial long division
|
| 172 |
+
const dividend = new Uint8Array(n);
|
| 173 |
+
// Place message coefficients at positions nk..n-1 (high degree)
|
| 174 |
+
// In our polynomial representation, index i = coefficient of x^i
|
| 175 |
+
// For systematic encoding: codeword(x) = m(x) * x^(n-k) + remainder
|
| 176 |
+
for (let i = 0; i < k; i++) {
|
| 177 |
+
dividend[nk + i] = message[i];
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
// Long division
|
| 181 |
+
const rem = new Uint8Array(dividend);
|
| 182 |
+
for (let i = n - 1; i >= nk; i--) {
|
| 183 |
+
if (rem[i]) {
|
| 184 |
+
for (let j = 0; j <= this.genPolyDeg; j++) {
|
| 185 |
+
rem[i - this.genPolyDeg + j] ^= this.genPoly[j];
|
| 186 |
+
}
|
| 187 |
+
}
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
// Codeword = message (high positions) + remainder (low positions)
|
| 191 |
+
for (let i = 0; i < nk; i++) {
|
| 192 |
+
codeword[k + i] = message[i]; // Will be overwritten below
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
// Actually: codeword[i] for i=0..nk-1 is remainder, for i=nk..n-1 is message
|
| 196 |
+
// Let's use standard ordering: codeword = [message | parity]
|
| 197 |
+
// where c(x) = m(x) * x^(n-k) + (m(x) * x^(n-k) mod g(x))
|
| 198 |
+
for (let i = 0; i < k; i++) {
|
| 199 |
+
codeword[i] = message[i];
|
| 200 |
+
}
|
| 201 |
+
for (let i = 0; i < nk; i++) {
|
| 202 |
+
codeword[k + i] = rem[i];
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
return codeword;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
/**
|
| 209 |
+
* Decode a received codeword. Returns corrected message bits or null if uncorrectable.
|
| 210 |
+
*/
|
| 211 |
+
decode(received: Uint8Array): Uint8Array | null {
|
| 212 |
+
const { n, k, t } = this.params;
|
| 213 |
+
if (received.length !== n) {
|
| 214 |
+
throw new Error(`Received length ${received.length} !== n=${n}`);
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
// Convert to polynomial form matching our encoding
|
| 218 |
+
// received[0..k-1] = message bits, received[k..n-1] = parity bits
|
| 219 |
+
// As polynomial: r(x) = sum r[i] * x^(n-k+i) for i=0..k-1, + sum r[k+i] * x^i for i=0..n-k-1
|
| 220 |
+
const nk = n - k;
|
| 221 |
+
const rPoly = new Uint8Array(n);
|
| 222 |
+
for (let i = 0; i < k; i++) {
|
| 223 |
+
rPoly[nk + i] = received[i];
|
| 224 |
+
}
|
| 225 |
+
for (let i = 0; i < nk; i++) {
|
| 226 |
+
rPoly[i] = received[k + i];
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
// Compute syndromes S_1 ... S_{2t}
|
| 230 |
+
// S_j = r(alpha^j) for j = 1..2t
|
| 231 |
+
const syndromes = new Array<number>(2 * t);
|
| 232 |
+
let hasErrors = false;
|
| 233 |
+
|
| 234 |
+
for (let j = 0; j < 2 * t; j++) {
|
| 235 |
+
let s = 0;
|
| 236 |
+
let alphaPow = 1; // alpha^((j+1)*0)
|
| 237 |
+
const alphaJ = this.gf.expTable[j + 1]; // alpha^(j+1)
|
| 238 |
+
for (let i = 0; i < n; i++) {
|
| 239 |
+
if (rPoly[i]) {
|
| 240 |
+
s ^= alphaPow;
|
| 241 |
+
}
|
| 242 |
+
alphaPow = this.gf.mul(alphaPow, alphaJ);
|
| 243 |
+
}
|
| 244 |
+
syndromes[j] = s;
|
| 245 |
+
if (s !== 0) hasErrors = true;
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
if (!hasErrors) {
|
| 249 |
+
return new Uint8Array(received.subarray(0, k));
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
// Berlekamp-Massey to find error locator polynomial
|
| 253 |
+
const sigma = this.berlekampMassey(syndromes, t);
|
| 254 |
+
if (!sigma) return null;
|
| 255 |
+
|
| 256 |
+
// Chien search to find error positions in polynomial representation
|
| 257 |
+
const errorPolyPositions = this.chienSearch(sigma, n);
|
| 258 |
+
if (!errorPolyPositions || errorPolyPositions.length !== sigma.length - 1) {
|
| 259 |
+
return null;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
// Correct errors in polynomial representation
|
| 263 |
+
const correctedPoly = new Uint8Array(rPoly);
|
| 264 |
+
for (const pos of errorPolyPositions) {
|
| 265 |
+
if (pos >= n) return null;
|
| 266 |
+
correctedPoly[pos] ^= 1;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
// Verify: recompute syndromes should all be zero
|
| 270 |
+
for (let j = 0; j < 2 * t; j++) {
|
| 271 |
+
let s = 0;
|
| 272 |
+
let alphaPow = 1;
|
| 273 |
+
const alphaJ = this.gf.expTable[j + 1];
|
| 274 |
+
for (let i = 0; i < n; i++) {
|
| 275 |
+
if (correctedPoly[i]) s ^= alphaPow;
|
| 276 |
+
alphaPow = this.gf.mul(alphaPow, alphaJ);
|
| 277 |
+
}
|
| 278 |
+
if (s !== 0) return null;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
// Extract message bits from corrected polynomial
|
| 282 |
+
const message = new Uint8Array(k);
|
| 283 |
+
for (let i = 0; i < k; i++) {
|
| 284 |
+
message[i] = correctedPoly[nk + i];
|
| 285 |
+
}
|
| 286 |
+
return message;
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
/**
|
| 290 |
+
* Berlekamp-Massey algorithm
|
| 291 |
+
*/
|
| 292 |
+
private berlekampMassey(syndromes: number[], t: number): number[] | null {
|
| 293 |
+
const gf = this.gf;
|
| 294 |
+
const twoT = 2 * t;
|
| 295 |
+
|
| 296 |
+
let C = [1];
|
| 297 |
+
let B = [1];
|
| 298 |
+
let L = 0;
|
| 299 |
+
let m = 1;
|
| 300 |
+
let b = 1;
|
| 301 |
+
|
| 302 |
+
for (let n = 0; n < twoT; n++) {
|
| 303 |
+
let d = syndromes[n];
|
| 304 |
+
for (let i = 1; i <= L; i++) {
|
| 305 |
+
if (i < C.length && (n - i) >= 0) {
|
| 306 |
+
d ^= gf.mul(C[i], syndromes[n - i]);
|
| 307 |
+
}
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
if (d === 0) {
|
| 311 |
+
m++;
|
| 312 |
+
} else if (2 * L <= n) {
|
| 313 |
+
const T = [...C];
|
| 314 |
+
const factor = gf.div(d, b);
|
| 315 |
+
while (C.length < B.length + m) C.push(0);
|
| 316 |
+
for (let i = 0; i < B.length; i++) {
|
| 317 |
+
C[i + m] ^= gf.mul(factor, B[i]);
|
| 318 |
+
}
|
| 319 |
+
L = n + 1 - L;
|
| 320 |
+
B = T;
|
| 321 |
+
b = d;
|
| 322 |
+
m = 1;
|
| 323 |
+
} else {
|
| 324 |
+
const factor = gf.div(d, b);
|
| 325 |
+
while (C.length < B.length + m) C.push(0);
|
| 326 |
+
for (let i = 0; i < B.length; i++) {
|
| 327 |
+
C[i + m] ^= gf.mul(factor, B[i]);
|
| 328 |
+
}
|
| 329 |
+
m++;
|
| 330 |
+
}
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
if (L > t) return null;
|
| 334 |
+
return C;
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
/**
|
| 338 |
+
* Chien search: find roots of the error locator polynomial.
|
| 339 |
+
*
|
| 340 |
+
* sigma(x) = prod(1 - alpha^(e_j) * x), so roots are at x = alpha^(-e_j).
|
| 341 |
+
* We test x = alpha^(-i) for i = 0..n-1. If sigma(alpha^(-i)) = 0,
|
| 342 |
+
* then error position is i (in polynomial index).
|
| 343 |
+
*/
|
| 344 |
+
private chienSearch(sigma: number[], n: number): number[] | null {
|
| 345 |
+
const gf = this.gf;
|
| 346 |
+
const positions: number[] = [];
|
| 347 |
+
const degree = sigma.length - 1;
|
| 348 |
+
const order = gf.size - 1;
|
| 349 |
+
|
| 350 |
+
for (let i = 0; i < n; i++) {
|
| 351 |
+
// Evaluate sigma at alpha^(-i)
|
| 352 |
+
let val = sigma[0]; // = 1
|
| 353 |
+
for (let j = 1; j < sigma.length; j++) {
|
| 354 |
+
// alpha^(-i*j) = alpha^(order - i*j mod order)
|
| 355 |
+
let exp = (order - ((i * j) % order)) % order;
|
| 356 |
+
val ^= gf.mul(sigma[j], gf.expTable[exp]);
|
| 357 |
+
}
|
| 358 |
+
if (val === 0) {
|
| 359 |
+
positions.push(i);
|
| 360 |
+
}
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
if (positions.length !== degree) return null;
|
| 364 |
+
return positions;
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
/**
|
| 368 |
+
* Soft-decode: use soft input values to attempt decoding
|
| 369 |
+
*/
|
| 370 |
+
decodeSoft(softBits: Float64Array): { message: Uint8Array | null; reliable: boolean } {
|
| 371 |
+
const hard = new Uint8Array(softBits.length);
|
| 372 |
+
for (let i = 0; i < softBits.length; i++) {
|
| 373 |
+
hard[i] = softBits[i] > 0 ? 1 : 0;
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
const decoded = this.decode(hard);
|
| 377 |
+
if (decoded) {
|
| 378 |
+
let reliabilitySum = 0;
|
| 379 |
+
for (let i = 0; i < softBits.length; i++) {
|
| 380 |
+
reliabilitySum += Math.abs(softBits[i]);
|
| 381 |
+
}
|
| 382 |
+
const avgReliability = reliabilitySum / softBits.length;
|
| 383 |
+
return { message: decoded, reliable: avgReliability > 0.15 };
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
return { message: null, reliable: false };
|
| 387 |
+
}
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
/**
|
| 391 |
+
* Create BCH codec from standard parameters
|
| 392 |
+
*/
|
| 393 |
+
export function createBchCodec(params: BchParams): BchCodec {
|
| 394 |
+
return new BchCodec(params);
|
| 395 |
+
}
|
core/crc.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* CRC computation for payload integrity verification
|
| 3 |
+
* Supports CRC-4, CRC-8, and CRC-16
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
// CRC-4 polynomial: x^4 + x + 1 (0x13, used bits: 0x03)
|
| 7 |
+
const CRC4_POLY = 0x03;
|
| 8 |
+
// CRC-8 polynomial: x^8 + x^2 + x + 1 (CRC-8-CCITT)
|
| 9 |
+
const CRC8_POLY = 0x07;
|
| 10 |
+
// CRC-16 polynomial: x^16 + x^15 + x^2 + 1 (CRC-16-IBM)
|
| 11 |
+
const CRC16_POLY = 0x8005;
|
| 12 |
+
|
| 13 |
+
/**
|
| 14 |
+
* Compute CRC over a bit array
|
| 15 |
+
* Uses non-zero initial value to prevent all-zeros from having CRC=0
|
| 16 |
+
*/
|
| 17 |
+
function computeCrc(bits: Uint8Array, poly: number, crcBits: number): number {
|
| 18 |
+
const mask = (1 << crcBits) - 1;
|
| 19 |
+
let crc = mask; // Non-zero init: all 1s (0xF for CRC-4, 0xFF for CRC-8, 0xFFFF for CRC-16)
|
| 20 |
+
|
| 21 |
+
for (let i = 0; i < bits.length; i++) {
|
| 22 |
+
crc ^= (bits[i] & 1) << (crcBits - 1);
|
| 23 |
+
if (crc & (1 << (crcBits - 1))) {
|
| 24 |
+
crc = ((crc << 1) ^ poly) & mask;
|
| 25 |
+
} else {
|
| 26 |
+
crc = (crc << 1) & mask;
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
return crc;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
/**
|
| 34 |
+
* Compute CRC and append to bit array
|
| 35 |
+
*/
|
| 36 |
+
export function crcAppend(bits: Uint8Array, crcBits: 4 | 8 | 16): Uint8Array {
|
| 37 |
+
const poly = crcBits === 4 ? CRC4_POLY : crcBits === 8 ? CRC8_POLY : CRC16_POLY;
|
| 38 |
+
const crc = computeCrc(bits, poly, crcBits);
|
| 39 |
+
|
| 40 |
+
const result = new Uint8Array(bits.length + crcBits);
|
| 41 |
+
result.set(bits);
|
| 42 |
+
|
| 43 |
+
// Append CRC bits (MSB first)
|
| 44 |
+
for (let i = 0; i < crcBits; i++) {
|
| 45 |
+
result[bits.length + i] = (crc >> (crcBits - 1 - i)) & 1;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
return result;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/**
|
| 52 |
+
* Verify CRC on a bit array (last crcBits bits are the CRC)
|
| 53 |
+
* Returns the payload bits (without CRC) if valid, null otherwise
|
| 54 |
+
*/
|
| 55 |
+
export function crcVerify(bits: Uint8Array, crcBits: 4 | 8 | 16): Uint8Array | null {
|
| 56 |
+
if (bits.length <= crcBits) return null;
|
| 57 |
+
|
| 58 |
+
const payload = bits.subarray(0, bits.length - crcBits);
|
| 59 |
+
const poly = crcBits === 4 ? CRC4_POLY : crcBits === 8 ? CRC8_POLY : CRC16_POLY;
|
| 60 |
+
const computed = computeCrc(payload, poly, crcBits);
|
| 61 |
+
|
| 62 |
+
// Extract received CRC
|
| 63 |
+
let received = 0;
|
| 64 |
+
for (let i = 0; i < crcBits; i++) {
|
| 65 |
+
received |= (bits[bits.length - crcBits + i] & 1) << (crcBits - 1 - i);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
if (computed === received) {
|
| 69 |
+
return new Uint8Array(payload);
|
| 70 |
+
}
|
| 71 |
+
return null;
|
| 72 |
+
}
|
core/dct.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* 8x8 DCT forward and inverse transforms
|
| 3 |
+
* Separable implementation: rows then columns — O(N^3) instead of O(N^4)
|
| 4 |
+
* Reuses temp buffers to avoid per-call allocations
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
const N = 8;
|
| 8 |
+
|
| 9 |
+
// Precompute cosine table: cos((2i+1)*j*PI / 16) for i,j in [0,8)
|
| 10 |
+
const COS_TABLE = new Float64Array(N * N);
|
| 11 |
+
for (let i = 0; i < N; i++) {
|
| 12 |
+
for (let j = 0; j < N; j++) {
|
| 13 |
+
COS_TABLE[i * N + j] = Math.cos(((2 * i + 1) * j * Math.PI) / (2 * N));
|
| 14 |
+
}
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
// Precompute alpha normalization table: alpha(u) * alpha(v) for all (u,v)
|
| 18 |
+
const ALPHA_0 = 1 / Math.sqrt(N);
|
| 19 |
+
const ALPHA_K = Math.sqrt(2 / N);
|
| 20 |
+
const ALPHA_UV = new Float64Array(N * N);
|
| 21 |
+
const ALPHA = new Float64Array(N);
|
| 22 |
+
for (let k = 0; k < N; k++) ALPHA[k] = k === 0 ? ALPHA_0 : ALPHA_K;
|
| 23 |
+
for (let u = 0; u < N; u++) {
|
| 24 |
+
for (let v = 0; v < N; v++) {
|
| 25 |
+
ALPHA_UV[u * N + v] = ALPHA[u] * ALPHA[v];
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
// Precompute scaled cosine: ALPHA[k] * COS_TABLE[i*N+k] for forward transform
|
| 30 |
+
const SCALED_COS = new Float64Array(N * N);
|
| 31 |
+
for (let i = 0; i < N; i++) {
|
| 32 |
+
for (let k = 0; k < N; k++) {
|
| 33 |
+
SCALED_COS[i * N + k] = ALPHA[k] * COS_TABLE[i * N + k];
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// Shared temp buffers (avoids allocation per call)
|
| 38 |
+
const _temp64 = new Float64Array(64);
|
| 39 |
+
const _temp8 = new Float64Array(8);
|
| 40 |
+
|
| 41 |
+
/**
|
| 42 |
+
* Forward 8x8 DCT on a block (in-place)
|
| 43 |
+
* Separable: transform rows, then columns — O(8^3) = 512 ops vs O(8^4) = 4096
|
| 44 |
+
*/
|
| 45 |
+
export function dctForward8x8(block: Float64Array): void {
|
| 46 |
+
// Step 1: Transform each row (spatial x → frequency v)
|
| 47 |
+
for (let x = 0; x < N; x++) {
|
| 48 |
+
const rowOff = x * N;
|
| 49 |
+
for (let v = 0; v < N; v++) {
|
| 50 |
+
let sum = 0;
|
| 51 |
+
for (let y = 0; y < N; y++) {
|
| 52 |
+
sum += block[rowOff + y] * COS_TABLE[y * N + v];
|
| 53 |
+
}
|
| 54 |
+
_temp64[rowOff + v] = ALPHA[v] * sum;
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
// Step 2: Transform each column (spatial x → frequency u)
|
| 59 |
+
for (let v = 0; v < N; v++) {
|
| 60 |
+
for (let u = 0; u < N; u++) {
|
| 61 |
+
let sum = 0;
|
| 62 |
+
for (let x = 0; x < N; x++) {
|
| 63 |
+
sum += _temp64[x * N + v] * COS_TABLE[x * N + u];
|
| 64 |
+
}
|
| 65 |
+
block[u * N + v] = ALPHA[u] * sum;
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
/**
|
| 71 |
+
* Inverse 8x8 DCT on a block (in-place)
|
| 72 |
+
* Separable: inverse columns, then inverse rows
|
| 73 |
+
*/
|
| 74 |
+
export function dctInverse8x8(block: Float64Array): void {
|
| 75 |
+
// Step 1: Inverse transform columns (frequency u → spatial x)
|
| 76 |
+
for (let v = 0; v < N; v++) {
|
| 77 |
+
for (let x = 0; x < N; x++) {
|
| 78 |
+
let sum = 0;
|
| 79 |
+
for (let u = 0; u < N; u++) {
|
| 80 |
+
sum += ALPHA[u] * block[u * N + v] * COS_TABLE[x * N + u];
|
| 81 |
+
}
|
| 82 |
+
_temp64[x * N + v] = sum;
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
// Step 2: Inverse transform rows (frequency v → spatial y)
|
| 87 |
+
for (let x = 0; x < N; x++) {
|
| 88 |
+
const rowOff = x * N;
|
| 89 |
+
for (let y = 0; y < N; y++) {
|
| 90 |
+
let sum = 0;
|
| 91 |
+
for (let v = 0; v < N; v++) {
|
| 92 |
+
sum += ALPHA[v] * _temp64[rowOff + v] * COS_TABLE[y * N + v];
|
| 93 |
+
}
|
| 94 |
+
block[rowOff + y] = sum;
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
/**
|
| 100 |
+
* Zigzag scan order for 8x8 block
|
| 101 |
+
* Maps zigzag index → [row, col]
|
| 102 |
+
*/
|
| 103 |
+
export const ZIGZAG_ORDER: [number, number][] = [
|
| 104 |
+
[0,0],[0,1],[1,0],[2,0],[1,1],[0,2],[0,3],[1,2],
|
| 105 |
+
[2,1],[3,0],[4,0],[3,1],[2,2],[1,3],[0,4],[0,5],
|
| 106 |
+
[1,4],[2,3],[3,2],[4,1],[5,0],[6,0],[5,1],[4,2],
|
| 107 |
+
[3,3],[2,4],[1,5],[0,6],[0,7],[1,6],[2,5],[3,4],
|
| 108 |
+
[4,3],[5,2],[6,1],[7,0],[7,1],[6,2],[5,3],[4,4],
|
| 109 |
+
[3,5],[2,6],[1,7],[2,7],[3,6],[4,5],[5,4],[6,3],
|
| 110 |
+
[7,2],[7,3],[6,4],[5,5],[4,6],[3,7],[4,7],[5,6],
|
| 111 |
+
[6,5],[7,4],[7,5],[6,6],[5,7],[6,7],[7,6],[7,7],
|
| 112 |
+
];
|
| 113 |
+
|
| 114 |
+
/**
|
| 115 |
+
* Extract an 8x8 block from a subband into a reusable buffer
|
| 116 |
+
*/
|
| 117 |
+
export function extractBlock(
|
| 118 |
+
data: Float64Array,
|
| 119 |
+
width: number,
|
| 120 |
+
blockRow: number,
|
| 121 |
+
blockCol: number,
|
| 122 |
+
out?: Float64Array
|
| 123 |
+
): Float64Array {
|
| 124 |
+
const block = out || new Float64Array(64);
|
| 125 |
+
const startY = blockRow * 8;
|
| 126 |
+
const startX = blockCol * 8;
|
| 127 |
+
for (let r = 0; r < 8; r++) {
|
| 128 |
+
const srcOff = (startY + r) * width + startX;
|
| 129 |
+
const dstOff = r * 8;
|
| 130 |
+
for (let c = 0; c < 8; c++) {
|
| 131 |
+
block[dstOff + c] = data[srcOff + c];
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
return block;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
/**
|
| 138 |
+
* Write an 8x8 block back into a subband
|
| 139 |
+
*/
|
| 140 |
+
export function writeBlock(
|
| 141 |
+
data: Float64Array,
|
| 142 |
+
width: number,
|
| 143 |
+
blockRow: number,
|
| 144 |
+
blockCol: number,
|
| 145 |
+
block: Float64Array
|
| 146 |
+
): void {
|
| 147 |
+
const startY = blockRow * 8;
|
| 148 |
+
const startX = blockCol * 8;
|
| 149 |
+
for (let r = 0; r < 8; r++) {
|
| 150 |
+
const dstOff = (startY + r) * width + startX;
|
| 151 |
+
const srcOff = r * 8;
|
| 152 |
+
for (let c = 0; c < 8; c++) {
|
| 153 |
+
data[dstOff + c] = block[srcOff + c];
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
}
|
core/detector.ts
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* High-level watermark detector
|
| 3 |
+
*
|
| 4 |
+
* Takes a Y plane + key + config → returns detection result
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
import type { WatermarkConfig, DetectionResult, Buffer2D } from './types.js';
|
| 8 |
+
import { yPlaneToBuffer, dwtForward, extractSubband } from './dwt.js';
|
| 9 |
+
import { dctForward8x8, extractBlock, ZIGZAG_ORDER } from './dct.js';
|
| 10 |
+
import { dmqimExtractSoft } from './dmqim.js';
|
| 11 |
+
import { crcVerify } from './crc.js';
|
| 12 |
+
import { BchCodec } from './bch.js';
|
| 13 |
+
import { generateDithers, generatePermutation } from './keygen.js';
|
| 14 |
+
import { computeTileGrid, getTileOrigin, getTileBlocks, type TileGrid } from './tiling.js';
|
| 15 |
+
import { blockAcEnergy, computeMaskingFactors } from './masking.js';
|
| 16 |
+
import { bitsToPayload } from './embedder.js';
|
| 17 |
+
import { PRESETS } from './presets.js';
|
| 18 |
+
import type { PresetName } from './types.js';
|
| 19 |
+
|
| 20 |
+
/**
|
| 21 |
+
* Detect and extract watermark from a single Y plane
|
| 22 |
+
*/
|
| 23 |
+
export function detectWatermark(
|
| 24 |
+
yPlane: Uint8Array,
|
| 25 |
+
width: number,
|
| 26 |
+
height: number,
|
| 27 |
+
key: string,
|
| 28 |
+
config: WatermarkConfig
|
| 29 |
+
): DetectionResult {
|
| 30 |
+
return detectWatermarkMultiFrame([yPlane], width, height, key, config);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
/**
|
| 34 |
+
* Extract per-tile soft decisions from a single Y plane.
|
| 35 |
+
* Returns an array of soft-bit vectors, one per tile.
|
| 36 |
+
*/
|
| 37 |
+
/** Precomputed DWT subband + tile grid info for a frame */
|
| 38 |
+
interface FrameDWT {
|
| 39 |
+
hlSubband: Buffer2D;
|
| 40 |
+
subbandTilePeriod: number;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
function computeFrameDWT(
|
| 44 |
+
yPlane: Uint8Array,
|
| 45 |
+
width: number,
|
| 46 |
+
height: number,
|
| 47 |
+
config: WatermarkConfig
|
| 48 |
+
): FrameDWT {
|
| 49 |
+
const buf = yPlaneToBuffer(yPlane, width, height);
|
| 50 |
+
const { buf: dwtBuf, dims } = dwtForward(buf, config.dwtLevels);
|
| 51 |
+
const hlSubband = extractSubband(dwtBuf, dims[dims.length - 1].w, dims[dims.length - 1].h, 'HL');
|
| 52 |
+
const subbandTilePeriod = Math.floor(config.tilePeriod / (1 << config.dwtLevels));
|
| 53 |
+
return { hlSubband, subbandTilePeriod };
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
function extractSoftBitsFromSubband(
|
| 57 |
+
hlSubband: Buffer2D,
|
| 58 |
+
tileGrid: TileGrid,
|
| 59 |
+
key: string,
|
| 60 |
+
config: WatermarkConfig
|
| 61 |
+
): { tileSoftBits: Float64Array[]; totalTiles: number } | null {
|
| 62 |
+
if (tileGrid.totalTiles === 0) return null;
|
| 63 |
+
|
| 64 |
+
const codedLength = config.bch.n;
|
| 65 |
+
const maxCoeffsPerTile = 1024;
|
| 66 |
+
const dithers = generateDithers(key, maxCoeffsPerTile, config.delta);
|
| 67 |
+
|
| 68 |
+
const tileSoftBits: Float64Array[] = [];
|
| 69 |
+
const blockBuf = new Float64Array(64);
|
| 70 |
+
|
| 71 |
+
// Precompute zigzag → coefficient index mapping
|
| 72 |
+
const zigCoeffIdx = new Int32Array(config.zigzagPositions.length);
|
| 73 |
+
for (let z = 0; z < config.zigzagPositions.length; z++) {
|
| 74 |
+
const [r, c] = ZIGZAG_ORDER[config.zigzagPositions[z]];
|
| 75 |
+
zigCoeffIdx[z] = r * 8 + c;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
for (let tileIdx = 0; tileIdx < tileGrid.totalTiles; tileIdx++) {
|
| 79 |
+
let ditherIdx = 0; // Reset per tile — matches embedder
|
| 80 |
+
const origin = getTileOrigin(tileGrid, tileIdx);
|
| 81 |
+
const blocks = getTileBlocks(origin.x, origin.y, tileGrid.tilePeriod, hlSubband.width, hlSubband.height);
|
| 82 |
+
|
| 83 |
+
const softBits = new Float64Array(codedLength);
|
| 84 |
+
const bitCounts = new Float64Array(codedLength);
|
| 85 |
+
|
| 86 |
+
let maskingFactors: Float64Array | null = null;
|
| 87 |
+
if (config.perceptualMasking && blocks.length > 0) {
|
| 88 |
+
const energies = new Float64Array(blocks.length);
|
| 89 |
+
for (let bi = 0; bi < blocks.length; bi++) {
|
| 90 |
+
extractBlock(hlSubband.data, hlSubband.width, blocks[bi].row, blocks[bi].col, blockBuf);
|
| 91 |
+
dctForward8x8(blockBuf);
|
| 92 |
+
energies[bi] = blockAcEnergy(blockBuf);
|
| 93 |
+
}
|
| 94 |
+
maskingFactors = computeMaskingFactors(energies);
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
let bitIdx = 0;
|
| 98 |
+
for (let bi = 0; bi < blocks.length; bi++) {
|
| 99 |
+
const { row, col } = blocks[bi];
|
| 100 |
+
extractBlock(hlSubband.data, hlSubband.width, row, col, blockBuf);
|
| 101 |
+
dctForward8x8(blockBuf);
|
| 102 |
+
|
| 103 |
+
const maskFactor = maskingFactors ? maskingFactors[bi] : 1.0;
|
| 104 |
+
const effectiveDelta = config.delta * maskFactor;
|
| 105 |
+
|
| 106 |
+
for (let z = 0; z < zigCoeffIdx.length; z++) {
|
| 107 |
+
if (bitIdx >= codedLength) bitIdx = 0;
|
| 108 |
+
|
| 109 |
+
const coeffIdx = zigCoeffIdx[z];
|
| 110 |
+
const dither = dithers[ditherIdx++];
|
| 111 |
+
|
| 112 |
+
const soft = dmqimExtractSoft(blockBuf[coeffIdx], effectiveDelta, dither);
|
| 113 |
+
softBits[bitIdx] += soft;
|
| 114 |
+
bitCounts[bitIdx]++;
|
| 115 |
+
|
| 116 |
+
bitIdx++;
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
for (let i = 0; i < codedLength; i++) {
|
| 121 |
+
if (bitCounts[i] > 0) softBits[i] /= bitCounts[i];
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
tileSoftBits.push(softBits);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
return { tileSoftBits, totalTiles: tileGrid.totalTiles };
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
/**
|
| 131 |
+
* Detect watermark from multiple Y planes.
|
| 132 |
+
* Extracts soft decisions from each frame independently, then combines
|
| 133 |
+
* across frames and tiles (never averages raw pixels).
|
| 134 |
+
*/
|
| 135 |
+
export function detectWatermarkMultiFrame(
|
| 136 |
+
yPlanes: Uint8Array[],
|
| 137 |
+
width: number,
|
| 138 |
+
height: number,
|
| 139 |
+
key: string,
|
| 140 |
+
config: WatermarkConfig,
|
| 141 |
+
): DetectionResult {
|
| 142 |
+
const noResult: DetectionResult = {
|
| 143 |
+
detected: false,
|
| 144 |
+
payload: null,
|
| 145 |
+
confidence: 0,
|
| 146 |
+
tilesDecoded: 0,
|
| 147 |
+
tilesTotal: 0,
|
| 148 |
+
};
|
| 149 |
+
|
| 150 |
+
if (yPlanes.length === 0) return noResult;
|
| 151 |
+
|
| 152 |
+
const codedLength = config.bch.n;
|
| 153 |
+
const bch = new BchCodec(config.bch);
|
| 154 |
+
const perm = generatePermutation(key, codedLength);
|
| 155 |
+
|
| 156 |
+
// Helper: try to detect with given frames and explicit tile grid
|
| 157 |
+
const tryWithGrid = (
|
| 158 |
+
frames: FrameDWT[],
|
| 159 |
+
makeGrid: (hlSubband: Buffer2D, stp: number) => TileGrid,
|
| 160 |
+
): DetectionResult | null => {
|
| 161 |
+
const softBits: Float64Array[] = [];
|
| 162 |
+
for (const { hlSubband, subbandTilePeriod } of frames) {
|
| 163 |
+
const tileGrid = makeGrid(hlSubband, subbandTilePeriod);
|
| 164 |
+
const frameResult = extractSoftBitsFromSubband(hlSubband, tileGrid, key, config);
|
| 165 |
+
if (frameResult) softBits.push(...frameResult.tileSoftBits);
|
| 166 |
+
}
|
| 167 |
+
if (softBits.length === 0) return null;
|
| 168 |
+
return decodeFromSoftBits(softBits, codedLength, perm, bch, config);
|
| 169 |
+
};
|
| 170 |
+
|
| 171 |
+
// Fast path: zero-phase grid (uncropped frames)
|
| 172 |
+
const frameDWTs = yPlanes.map((yp) => computeFrameDWT(yp, width, height, config));
|
| 173 |
+
const fast = tryWithGrid(frameDWTs, (hl, stp) =>
|
| 174 |
+
computeTileGrid(hl.width, hl.height, stp));
|
| 175 |
+
if (fast) return fast;
|
| 176 |
+
|
| 177 |
+
return noResult;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
/**
|
| 181 |
+
* Combine soft bits from all tiles, decode, and compute confidence.
|
| 182 |
+
* Returns null if decoding fails or confidence is too low.
|
| 183 |
+
*/
|
| 184 |
+
function decodeFromSoftBits(
|
| 185 |
+
allTileSoftBits: Float64Array[],
|
| 186 |
+
codedLength: number,
|
| 187 |
+
perm: Uint32Array,
|
| 188 |
+
bch: BchCodec,
|
| 189 |
+
config: WatermarkConfig
|
| 190 |
+
): DetectionResult | null {
|
| 191 |
+
const combined = new Float64Array(codedLength);
|
| 192 |
+
for (const tileSoft of allTileSoftBits) {
|
| 193 |
+
for (let i = 0; i < codedLength; i++) {
|
| 194 |
+
combined[i] += tileSoft[i];
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
for (let i = 0; i < codedLength; i++) {
|
| 198 |
+
combined[i] /= allTileSoftBits.length;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
const decoded = tryDecode(combined, perm, bch, config);
|
| 202 |
+
if (!decoded) return null;
|
| 203 |
+
|
| 204 |
+
// Cross-validate — count how many individual tiles agree with the combined decode
|
| 205 |
+
const reEncoded = bch.encode(decoded.rawMessage);
|
| 206 |
+
let agreeTiles = 0;
|
| 207 |
+
|
| 208 |
+
for (const tileSoft of allTileSoftBits) {
|
| 209 |
+
const deinterleaved = new Float64Array(codedLength);
|
| 210 |
+
for (let i = 0; i < codedLength; i++) {
|
| 211 |
+
deinterleaved[i] = tileSoft[perm[i]];
|
| 212 |
+
}
|
| 213 |
+
let matching = 0;
|
| 214 |
+
for (let i = 0; i < codedLength; i++) {
|
| 215 |
+
const hardBit = deinterleaved[i] > 0 ? 1 : 0;
|
| 216 |
+
if (hardBit === reEncoded[i]) matching++;
|
| 217 |
+
}
|
| 218 |
+
if (matching / codedLength > 0.65) agreeTiles++;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
const totalTileCount = allTileSoftBits.length;
|
| 222 |
+
const zSingle = 0.3 * Math.sqrt(codedLength);
|
| 223 |
+
const pChance = Math.max(1e-10, 0.5 * Math.exp(-0.5 * zSingle * zSingle));
|
| 224 |
+
|
| 225 |
+
const expected = totalTileCount * pChance;
|
| 226 |
+
const stddev = Math.sqrt(totalTileCount * pChance * (1 - pChance));
|
| 227 |
+
const z = stddev > 0 ? (agreeTiles - expected) / stddev : agreeTiles > expected ? 100 : 0;
|
| 228 |
+
const statsConfidence = Math.max(0, Math.min(1.0, 1 - Math.exp(-z * 0.5)));
|
| 229 |
+
|
| 230 |
+
const confidence = Math.max(statsConfidence, decoded.softConfidence);
|
| 231 |
+
|
| 232 |
+
if (confidence < MIN_CONFIDENCE) return null;
|
| 233 |
+
|
| 234 |
+
return {
|
| 235 |
+
detected: true,
|
| 236 |
+
payload: decoded.payload,
|
| 237 |
+
confidence,
|
| 238 |
+
tilesDecoded: agreeTiles,
|
| 239 |
+
tilesTotal: allTileSoftBits.length,
|
| 240 |
+
};
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
/** Minimum confidence to report a detection (low threshold is fine —
|
| 244 |
+
* the statistical model already ensures noise scores near 0%) */
|
| 245 |
+
const MIN_CONFIDENCE = 0.10;
|
| 246 |
+
|
| 247 |
+
/**
|
| 248 |
+
* Try to decode soft bits into a payload
|
| 249 |
+
*/
|
| 250 |
+
function tryDecode(
|
| 251 |
+
softBits: Float64Array,
|
| 252 |
+
perm: Uint32Array,
|
| 253 |
+
bch: BchCodec,
|
| 254 |
+
config: WatermarkConfig
|
| 255 |
+
): { payload: Uint8Array; rawMessage: Uint8Array; softConfidence: number } | null {
|
| 256 |
+
const codedLength = config.bch.n;
|
| 257 |
+
|
| 258 |
+
// De-interleave
|
| 259 |
+
const deinterleaved = new Float64Array(codedLength);
|
| 260 |
+
for (let i = 0; i < codedLength; i++) {
|
| 261 |
+
deinterleaved[i] = softBits[perm[i]];
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
// BCH soft decode
|
| 265 |
+
const { message, reliable } = bch.decodeSoft(deinterleaved);
|
| 266 |
+
if (!message) return null;
|
| 267 |
+
|
| 268 |
+
// Extract the CRC-protected portion (first 32 + crc_bits of the BCH message)
|
| 269 |
+
const PAYLOAD_BITS = 32;
|
| 270 |
+
const crcProtectedLen = PAYLOAD_BITS + config.crc.bits;
|
| 271 |
+
const crcProtected = message.subarray(0, crcProtectedLen);
|
| 272 |
+
|
| 273 |
+
// CRC verify the 32-bit payload
|
| 274 |
+
const verified = crcVerify(crcProtected, config.crc.bits);
|
| 275 |
+
if (!verified) return null;
|
| 276 |
+
|
| 277 |
+
// Convert 32 payload bits to bytes
|
| 278 |
+
const payload = bitsToPayload(verified);
|
| 279 |
+
|
| 280 |
+
// Soft confidence = correlation between soft decisions and decoded codeword.
|
| 281 |
+
// Real signal: soft values are large AND agree with the codeword → high correlation.
|
| 282 |
+
// Noise that BCH happened to decode: soft values are small/random → low correlation.
|
| 283 |
+
const reEncoded = bch.encode(message);
|
| 284 |
+
let correlation = 0;
|
| 285 |
+
for (let i = 0; i < codedLength; i++) {
|
| 286 |
+
const sign = reEncoded[i] === 1 ? 1 : -1;
|
| 287 |
+
correlation += sign * deinterleaved[i];
|
| 288 |
+
}
|
| 289 |
+
correlation /= codedLength;
|
| 290 |
+
const softConfidence = Math.max(0, Math.min(1.0, correlation * 2));
|
| 291 |
+
|
| 292 |
+
return { payload, rawMessage: message, softConfidence };
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
/** Extended detection result that includes which preset matched */
|
| 296 |
+
export interface AutoDetectResult extends DetectionResult {
|
| 297 |
+
/** The preset that produced the detection (null if not detected) */
|
| 298 |
+
presetUsed: PresetName | null;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
/**
|
| 302 |
+
* Auto-detect: try all presets and return the best result.
|
| 303 |
+
* No need for the user to know which preset was used during embedding.
|
| 304 |
+
*/
|
| 305 |
+
export function autoDetect(
|
| 306 |
+
yPlane: Uint8Array,
|
| 307 |
+
width: number,
|
| 308 |
+
height: number,
|
| 309 |
+
key: string,
|
| 310 |
+
): AutoDetectResult {
|
| 311 |
+
return autoDetectMultiFrame([yPlane], width, height, key);
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
/**
|
| 315 |
+
* Auto-detect with multiple frames: try all presets, return the best result.
|
| 316 |
+
*/
|
| 317 |
+
export function autoDetectMultiFrame(
|
| 318 |
+
yPlanes: Uint8Array[],
|
| 319 |
+
width: number,
|
| 320 |
+
height: number,
|
| 321 |
+
key: string,
|
| 322 |
+
): AutoDetectResult {
|
| 323 |
+
let best: AutoDetectResult = {
|
| 324 |
+
detected: false,
|
| 325 |
+
payload: null,
|
| 326 |
+
confidence: 0,
|
| 327 |
+
tilesDecoded: 0,
|
| 328 |
+
tilesTotal: 0,
|
| 329 |
+
presetUsed: null,
|
| 330 |
+
};
|
| 331 |
+
|
| 332 |
+
for (const [name, config] of Object.entries(PRESETS)) {
|
| 333 |
+
const result = detectWatermarkMultiFrame(yPlanes, width, height, key, config);
|
| 334 |
+
if (result.detected && result.confidence > best.confidence) {
|
| 335 |
+
best = { ...result, presetUsed: name as PresetName };
|
| 336 |
+
}
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
return best;
|
| 340 |
+
}
|
core/dmqim.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Dither-Modulated Quantization Index Modulation (DM-QIM)
|
| 3 |
+
*
|
| 4 |
+
* Embeds a single bit into a coefficient using quantization.
|
| 5 |
+
* The dither value provides key-dependent randomization.
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* Embed a single bit into a coefficient using DM-QIM
|
| 10 |
+
*
|
| 11 |
+
* @param coeff - Original coefficient value
|
| 12 |
+
* @param bit - Bit to embed (0 or 1)
|
| 13 |
+
* @param delta - Quantization step size
|
| 14 |
+
* @param dither - Key-dependent dither value
|
| 15 |
+
* @returns Watermarked coefficient
|
| 16 |
+
*/
|
| 17 |
+
export function dmqimEmbed(coeff: number, bit: number, delta: number, dither: number): number {
|
| 18 |
+
// Dither modulation: shift coefficient by dither before quantizing
|
| 19 |
+
const shifted = coeff - dither;
|
| 20 |
+
|
| 21 |
+
// Quantize to the nearest lattice point for the given bit
|
| 22 |
+
// Bit 0 → even quantization levels: ..., -2Δ, 0, 2Δ, ...
|
| 23 |
+
// Bit 1 → odd quantization levels: ..., -Δ, Δ, 3Δ, ...
|
| 24 |
+
const halfDelta = delta / 2;
|
| 25 |
+
|
| 26 |
+
if (bit === 0) {
|
| 27 |
+
// Quantize to nearest even multiple of delta
|
| 28 |
+
const quantized = Math.round(shifted / delta) * delta;
|
| 29 |
+
return quantized + dither;
|
| 30 |
+
} else {
|
| 31 |
+
// Quantize to nearest odd multiple of delta (offset by delta/2)
|
| 32 |
+
const quantized = Math.round((shifted - halfDelta) / delta) * delta + halfDelta;
|
| 33 |
+
return quantized + dither;
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/**
|
| 38 |
+
* Extract a soft decision from a coefficient using DM-QIM
|
| 39 |
+
*
|
| 40 |
+
* Returns a signed float:
|
| 41 |
+
* Positive → likely bit 1
|
| 42 |
+
* Negative → likely bit 0
|
| 43 |
+
* Magnitude → confidence (closer to ±delta/4 = maximum confidence)
|
| 44 |
+
*
|
| 45 |
+
* @param coeff - Possibly watermarked coefficient
|
| 46 |
+
* @param delta - Quantization step size
|
| 47 |
+
* @param dither - Key-dependent dither value (must match embed)
|
| 48 |
+
* @returns Soft decision value in [-delta/4, +delta/4]
|
| 49 |
+
*/
|
| 50 |
+
export function dmqimExtractSoft(coeff: number, delta: number, dither: number): number {
|
| 51 |
+
const shifted = coeff - dither;
|
| 52 |
+
const halfDelta = delta / 2;
|
| 53 |
+
|
| 54 |
+
// Distance to nearest even lattice point (bit 0)
|
| 55 |
+
const d0 = Math.abs(shifted - Math.round(shifted / delta) * delta);
|
| 56 |
+
// Distance to nearest odd lattice point (bit 1)
|
| 57 |
+
const d1 = Math.abs(shifted - halfDelta - Math.round((shifted - halfDelta) / delta) * delta);
|
| 58 |
+
|
| 59 |
+
// Soft output: positive for bit 1, negative for bit 0
|
| 60 |
+
// Magnitude reflects confidence
|
| 61 |
+
return (d0 - d1) / delta;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
/**
|
| 65 |
+
* Extract a hard decision (0 or 1) from a coefficient
|
| 66 |
+
*/
|
| 67 |
+
export function dmqimExtractHard(coeff: number, delta: number, dither: number): number {
|
| 68 |
+
return dmqimExtractSoft(coeff, delta, dither) > 0 ? 1 : 0;
|
| 69 |
+
}
|
core/dwt.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Buffer2D, DwtResult } from './types.js';
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Create a 2D buffer
|
| 5 |
+
*/
|
| 6 |
+
export function createBuffer2D(width: number, height: number): Buffer2D {
|
| 7 |
+
return { data: new Float64Array(width * height), width, height };
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
/**
|
| 11 |
+
* Copy a Buffer2D
|
| 12 |
+
*/
|
| 13 |
+
export function copyBuffer2D(buf: Buffer2D): Buffer2D {
|
| 14 |
+
return { data: new Float64Array(buf.data), width: buf.width, height: buf.height };
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
/**
|
| 18 |
+
* Get value from 2D buffer
|
| 19 |
+
*/
|
| 20 |
+
function get(buf: Buffer2D, x: number, y: number): number {
|
| 21 |
+
return buf.data[y * buf.width + x];
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
/**
|
| 25 |
+
* Set value in 2D buffer
|
| 26 |
+
*/
|
| 27 |
+
function set(buf: Buffer2D, x: number, y: number, val: number): void {
|
| 28 |
+
buf.data[y * buf.width + x] = val;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// Reusable temp buffer for Haar transforms — grown as needed
|
| 32 |
+
let _haarTmp: Float64Array | null = null;
|
| 33 |
+
|
| 34 |
+
function getHaarTmp(len: number): Float64Array {
|
| 35 |
+
if (!_haarTmp || _haarTmp.length < len) {
|
| 36 |
+
_haarTmp = new Float64Array(len);
|
| 37 |
+
}
|
| 38 |
+
return _haarTmp;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
/**
|
| 42 |
+
* 1D Haar forward transform (in-place on a row/column slice)
|
| 43 |
+
*/
|
| 44 |
+
function haarForward1D(input: Float64Array, length: number): void {
|
| 45 |
+
const half = length >> 1;
|
| 46 |
+
const tmp = getHaarTmp(length);
|
| 47 |
+
const s = Math.SQRT1_2; // 1/sqrt(2)
|
| 48 |
+
for (let i = 0; i < half; i++) {
|
| 49 |
+
const a = input[2 * i];
|
| 50 |
+
const b = input[2 * i + 1];
|
| 51 |
+
tmp[i] = (a + b) * s; // approximation (low)
|
| 52 |
+
tmp[half + i] = (a - b) * s; // detail (high)
|
| 53 |
+
}
|
| 54 |
+
for (let i = 0; i < length; i++) input[i] = tmp[i];
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
/**
|
| 58 |
+
* 1D Haar inverse transform (in-place on a row/column slice)
|
| 59 |
+
*/
|
| 60 |
+
function haarInverse1D(input: Float64Array, length: number): void {
|
| 61 |
+
const half = length >> 1;
|
| 62 |
+
const tmp = getHaarTmp(length);
|
| 63 |
+
const s = Math.SQRT1_2;
|
| 64 |
+
for (let i = 0; i < half; i++) {
|
| 65 |
+
const low = input[i];
|
| 66 |
+
const high = input[half + i];
|
| 67 |
+
tmp[2 * i] = (low + high) * s;
|
| 68 |
+
tmp[2 * i + 1] = (low - high) * s;
|
| 69 |
+
}
|
| 70 |
+
for (let i = 0; i < length; i++) input[i] = tmp[i];
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/**
|
| 74 |
+
* Forward 2D Haar DWT (one level)
|
| 75 |
+
* Transforms the top-left (w x h) region of the buffer
|
| 76 |
+
*/
|
| 77 |
+
function dwtForward2DLevel(buf: Buffer2D, w: number, h: number): void {
|
| 78 |
+
// Transform rows
|
| 79 |
+
const rowBuf = new Float64Array(w);
|
| 80 |
+
for (let y = 0; y < h; y++) {
|
| 81 |
+
const off = y * buf.width;
|
| 82 |
+
for (let x = 0; x < w; x++) rowBuf[x] = buf.data[off + x];
|
| 83 |
+
haarForward1D(rowBuf, w);
|
| 84 |
+
for (let x = 0; x < w; x++) buf.data[off + x] = rowBuf[x];
|
| 85 |
+
}
|
| 86 |
+
// Transform columns
|
| 87 |
+
const colBuf = new Float64Array(h);
|
| 88 |
+
for (let x = 0; x < w; x++) {
|
| 89 |
+
for (let y = 0; y < h; y++) colBuf[y] = buf.data[y * buf.width + x];
|
| 90 |
+
haarForward1D(colBuf, h);
|
| 91 |
+
for (let y = 0; y < h; y++) buf.data[y * buf.width + x] = colBuf[y];
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
/**
|
| 96 |
+
* Inverse 2D Haar DWT (one level)
|
| 97 |
+
*/
|
| 98 |
+
function dwtInverse2DLevel(buf: Buffer2D, w: number, h: number): void {
|
| 99 |
+
// Inverse columns
|
| 100 |
+
const colBuf = new Float64Array(h);
|
| 101 |
+
for (let x = 0; x < w; x++) {
|
| 102 |
+
for (let y = 0; y < h; y++) colBuf[y] = buf.data[y * buf.width + x];
|
| 103 |
+
haarInverse1D(colBuf, h);
|
| 104 |
+
for (let y = 0; y < h; y++) buf.data[y * buf.width + x] = colBuf[y];
|
| 105 |
+
}
|
| 106 |
+
// Inverse rows
|
| 107 |
+
const rowBuf = new Float64Array(w);
|
| 108 |
+
for (let y = 0; y < h; y++) {
|
| 109 |
+
const off = y * buf.width;
|
| 110 |
+
for (let x = 0; x < w; x++) rowBuf[x] = buf.data[off + x];
|
| 111 |
+
haarInverse1D(rowBuf, w);
|
| 112 |
+
for (let x = 0; x < w; x++) buf.data[off + x] = rowBuf[x];
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
/**
|
| 117 |
+
* Extract a subband from the DWT buffer
|
| 118 |
+
* After one level of DWT on a (w x h) region:
|
| 119 |
+
* LL = top-left (w/2 x h/2)
|
| 120 |
+
* LH = top-right (w/2 x h/2) — vertical detail
|
| 121 |
+
* HL = bottom-left (w/2 x h/2) — horizontal detail
|
| 122 |
+
* HH = bottom-right (w/2 x h/2) — diagonal detail
|
| 123 |
+
*/
|
| 124 |
+
export function extractSubband(
|
| 125 |
+
buf: Buffer2D,
|
| 126 |
+
w: number,
|
| 127 |
+
h: number,
|
| 128 |
+
band: 'LL' | 'LH' | 'HL' | 'HH'
|
| 129 |
+
): Buffer2D {
|
| 130 |
+
const hw = w >> 1;
|
| 131 |
+
const hh = h >> 1;
|
| 132 |
+
const offX = band === 'LH' || band === 'HH' ? hw : 0;
|
| 133 |
+
const offY = band === 'HL' || band === 'HH' ? hh : 0;
|
| 134 |
+
const out = createBuffer2D(hw, hh);
|
| 135 |
+
for (let y = 0; y < hh; y++) {
|
| 136 |
+
const srcOff = (offY + y) * buf.width + offX;
|
| 137 |
+
const dstOff = y * hw;
|
| 138 |
+
for (let x = 0; x < hw; x++) {
|
| 139 |
+
out.data[dstOff + x] = buf.data[srcOff + x];
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
return out;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
/**
|
| 146 |
+
* Write a subband back into the DWT buffer
|
| 147 |
+
*/
|
| 148 |
+
export function writeSubband(
|
| 149 |
+
buf: Buffer2D,
|
| 150 |
+
w: number,
|
| 151 |
+
h: number,
|
| 152 |
+
band: 'LL' | 'LH' | 'HL' | 'HH',
|
| 153 |
+
subband: Buffer2D
|
| 154 |
+
): void {
|
| 155 |
+
const hw = w >> 1;
|
| 156 |
+
const hh = h >> 1;
|
| 157 |
+
const offX = band === 'LH' || band === 'HH' ? hw : 0;
|
| 158 |
+
const offY = band === 'HL' || band === 'HH' ? hh : 0;
|
| 159 |
+
for (let y = 0; y < hh; y++) {
|
| 160 |
+
const dstOff = (offY + y) * buf.width + offX;
|
| 161 |
+
const srcOff = y * hw;
|
| 162 |
+
for (let x = 0; x < hw; x++) {
|
| 163 |
+
buf.data[dstOff + x] = subband.data[srcOff + x];
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
/**
|
| 169 |
+
* Multi-level forward DWT
|
| 170 |
+
* Returns the modified buffer and subband info for reconstruction
|
| 171 |
+
*/
|
| 172 |
+
export function dwtForward(input: Buffer2D, levels: number): { buf: Buffer2D; dims: Array<{ w: number; h: number }> } {
|
| 173 |
+
const buf = copyBuffer2D(input);
|
| 174 |
+
const dims: Array<{ w: number; h: number }> = [];
|
| 175 |
+
let w = buf.width;
|
| 176 |
+
let h = buf.height;
|
| 177 |
+
|
| 178 |
+
for (let l = 0; l < levels; l++) {
|
| 179 |
+
// Ensure even dimensions
|
| 180 |
+
w = w & ~1;
|
| 181 |
+
h = h & ~1;
|
| 182 |
+
dims.push({ w, h });
|
| 183 |
+
dwtForward2DLevel(buf, w, h);
|
| 184 |
+
w >>= 1;
|
| 185 |
+
h >>= 1;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
return { buf, dims };
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
/**
|
| 192 |
+
* Multi-level inverse DWT
|
| 193 |
+
*/
|
| 194 |
+
export function dwtInverse(buf: Buffer2D, dims: Array<{ w: number; h: number }>): void {
|
| 195 |
+
for (let l = dims.length - 1; l >= 0; l--) {
|
| 196 |
+
const { w, h } = dims[l];
|
| 197 |
+
dwtInverse2DLevel(buf, w, h);
|
| 198 |
+
}
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
/**
|
| 202 |
+
* Convert a Uint8Array Y plane to a Float64 Buffer2D
|
| 203 |
+
*/
|
| 204 |
+
export function yPlaneToBuffer(yPlane: Uint8Array, width: number, height: number): Buffer2D {
|
| 205 |
+
const buf = createBuffer2D(width, height);
|
| 206 |
+
for (let i = 0; i < width * height; i++) {
|
| 207 |
+
buf.data[i] = yPlane[i];
|
| 208 |
+
}
|
| 209 |
+
return buf;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
/**
|
| 213 |
+
* Convert a Float64 Buffer2D back to a Uint8Array Y plane (clamped to 0–255)
|
| 214 |
+
*/
|
| 215 |
+
export function bufferToYPlane(buf: Buffer2D): Uint8Array {
|
| 216 |
+
const out = new Uint8Array(buf.width * buf.height);
|
| 217 |
+
for (let i = 0; i < out.length; i++) {
|
| 218 |
+
out[i] = Math.max(0, Math.min(255, Math.round(buf.data[i])));
|
| 219 |
+
}
|
| 220 |
+
return out;
|
| 221 |
+
}
|
core/embedder.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* High-level watermark embedder
|
| 3 |
+
*
|
| 4 |
+
* Takes a Y plane + payload + key + config → returns watermarked Y plane
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
import type { WatermarkConfig, EmbedResult, Buffer2D } from './types.js';
|
| 8 |
+
import { yPlaneToBuffer, bufferToYPlane, dwtForward, dwtInverse, extractSubband, writeSubband } from './dwt.js';
|
| 9 |
+
import { dctForward8x8, dctInverse8x8, extractBlock, writeBlock, ZIGZAG_ORDER } from './dct.js';
|
| 10 |
+
import { dmqimEmbed } from './dmqim.js';
|
| 11 |
+
import { crcAppend } from './crc.js';
|
| 12 |
+
import { BchCodec } from './bch.js';
|
| 13 |
+
import { generateDithers, generatePermutation } from './keygen.js';
|
| 14 |
+
import { computeTileGrid, getTileOrigin, getTileBlocks } from './tiling.js';
|
| 15 |
+
import { blockAcEnergy, computeMaskingFactors } from './masking.js';
|
| 16 |
+
|
| 17 |
+
/**
|
| 18 |
+
* Convert a 32-bit payload (4 bytes) to a bit array
|
| 19 |
+
*/
|
| 20 |
+
export function payloadToBits(payload: Uint8Array): Uint8Array {
|
| 21 |
+
const bits = new Uint8Array(payload.length * 8);
|
| 22 |
+
for (let i = 0; i < payload.length; i++) {
|
| 23 |
+
for (let b = 0; b < 8; b++) {
|
| 24 |
+
bits[i * 8 + b] = (payload[i] >> (7 - b)) & 1;
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
return bits;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/**
|
| 31 |
+
* Convert a bit array back to bytes
|
| 32 |
+
*/
|
| 33 |
+
export function bitsToPayload(bits: Uint8Array): Uint8Array {
|
| 34 |
+
const bytes = new Uint8Array(Math.ceil(bits.length / 8));
|
| 35 |
+
for (let i = 0; i < bits.length; i++) {
|
| 36 |
+
if (bits[i]) {
|
| 37 |
+
bytes[Math.floor(i / 8)] |= 1 << (7 - (i % 8));
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
return bytes;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/**
|
| 44 |
+
* Embed a watermark into a Y plane
|
| 45 |
+
*/
|
| 46 |
+
export function embedWatermark(
|
| 47 |
+
yPlane: Uint8Array,
|
| 48 |
+
width: number,
|
| 49 |
+
height: number,
|
| 50 |
+
payload: Uint8Array,
|
| 51 |
+
key: string,
|
| 52 |
+
config: WatermarkConfig
|
| 53 |
+
): EmbedResult {
|
| 54 |
+
// Step 1: Encode payload
|
| 55 |
+
// Always use exactly 32 bits of payload, zero-pad the BCH message to fill k
|
| 56 |
+
const PAYLOAD_BITS = 32;
|
| 57 |
+
let payloadBits = payloadToBits(payload);
|
| 58 |
+
// Truncate or pad to exactly 32 bits
|
| 59 |
+
if (payloadBits.length < PAYLOAD_BITS) {
|
| 60 |
+
const padded = new Uint8Array(PAYLOAD_BITS);
|
| 61 |
+
padded.set(payloadBits);
|
| 62 |
+
payloadBits = padded;
|
| 63 |
+
} else if (payloadBits.length > PAYLOAD_BITS) {
|
| 64 |
+
payloadBits = payloadBits.subarray(0, PAYLOAD_BITS);
|
| 65 |
+
}
|
| 66 |
+
// CRC protects the 32-bit payload
|
| 67 |
+
const withCrc = crcAppend(payloadBits, config.crc.bits);
|
| 68 |
+
// Pad to fill BCH message length k
|
| 69 |
+
const bchMessage = new Uint8Array(config.bch.k);
|
| 70 |
+
bchMessage.set(withCrc);
|
| 71 |
+
const bch = new BchCodec(config.bch);
|
| 72 |
+
const encoded = bch.encode(bchMessage);
|
| 73 |
+
const codedLength = encoded.length;
|
| 74 |
+
|
| 75 |
+
// Generate interleaving permutation
|
| 76 |
+
const perm = generatePermutation(key, codedLength);
|
| 77 |
+
const interleaved = new Uint8Array(codedLength);
|
| 78 |
+
for (let i = 0; i < codedLength; i++) {
|
| 79 |
+
interleaved[perm[i]] = encoded[i];
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
// Step 2: Forward DWT
|
| 83 |
+
const buf = yPlaneToBuffer(yPlane, width, height);
|
| 84 |
+
const { buf: dwtBuf, dims } = dwtForward(buf, config.dwtLevels);
|
| 85 |
+
|
| 86 |
+
// Step 3: Extract HL subband at deepest level
|
| 87 |
+
// After N levels, the HL subband is in the bottom-left of the level-N region
|
| 88 |
+
let w = dims[dims.length - 1].w;
|
| 89 |
+
let h = dims[dims.length - 1].h;
|
| 90 |
+
for (let l = 0; l < config.dwtLevels - 1; l++) {
|
| 91 |
+
w >>= 1;
|
| 92 |
+
h >>= 1;
|
| 93 |
+
}
|
| 94 |
+
const hlSubband = extractSubband(dwtBuf, dims[dims.length - 1].w, dims[dims.length - 1].h, 'HL');
|
| 95 |
+
|
| 96 |
+
// Step 4: Set up tile grid
|
| 97 |
+
// tilePeriod is in spatial pixels; in subband it's tilePeriod / (2^levels)
|
| 98 |
+
const subbandTilePeriod = Math.floor(config.tilePeriod / (1 << config.dwtLevels));
|
| 99 |
+
const tileGrid = computeTileGrid(hlSubband.width, hlSubband.height, subbandTilePeriod);
|
| 100 |
+
|
| 101 |
+
if (tileGrid.totalTiles === 0) {
|
| 102 |
+
// Frame too small for any tiles — return unchanged
|
| 103 |
+
return { yPlane: new Uint8Array(yPlane), psnr: Infinity };
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
// Step 5: Generate dithers — same sequence reused per tile so each tile
|
| 107 |
+
// is independently decodable (required for crop robustness)
|
| 108 |
+
const coeffsPerBlock = config.zigzagPositions.length;
|
| 109 |
+
const maxCoeffsPerTile = 1024; // Upper bound
|
| 110 |
+
const dithers = generateDithers(key, maxCoeffsPerTile, config.delta);
|
| 111 |
+
|
| 112 |
+
// Step 6: For each tile, embed the coded bits
|
| 113 |
+
// Reusable block buffer to avoid allocation per block
|
| 114 |
+
const blockBuf = new Float64Array(64);
|
| 115 |
+
|
| 116 |
+
// Precompute zigzag → coefficient index mapping
|
| 117 |
+
const zigCoeffIdx = new Int32Array(config.zigzagPositions.length);
|
| 118 |
+
for (let z = 0; z < config.zigzagPositions.length; z++) {
|
| 119 |
+
const [r, c] = ZIGZAG_ORDER[config.zigzagPositions[z]];
|
| 120 |
+
zigCoeffIdx[z] = r * 8 + c;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
for (let tileIdx = 0; tileIdx < tileGrid.totalTiles; tileIdx++) {
|
| 124 |
+
let ditherIdx = 0; // Reset per tile — each tile uses same dither sequence
|
| 125 |
+
const origin = getTileOrigin(tileGrid, tileIdx);
|
| 126 |
+
const blocks = getTileBlocks(origin.x, origin.y, subbandTilePeriod, hlSubband.width, hlSubband.height);
|
| 127 |
+
|
| 128 |
+
// Compute masking factors if enabled
|
| 129 |
+
let maskingFactors: Float64Array | null = null;
|
| 130 |
+
if (config.perceptualMasking && blocks.length > 0) {
|
| 131 |
+
const energies = new Float64Array(blocks.length);
|
| 132 |
+
for (let bi = 0; bi < blocks.length; bi++) {
|
| 133 |
+
extractBlock(hlSubband.data, hlSubband.width, blocks[bi].row, blocks[bi].col, blockBuf);
|
| 134 |
+
dctForward8x8(blockBuf);
|
| 135 |
+
energies[bi] = blockAcEnergy(blockBuf);
|
| 136 |
+
}
|
| 137 |
+
maskingFactors = computeMaskingFactors(energies);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
let bitIdx = 0;
|
| 141 |
+
for (let bi = 0; bi < blocks.length; bi++) {
|
| 142 |
+
const { row, col } = blocks[bi];
|
| 143 |
+
extractBlock(hlSubband.data, hlSubband.width, row, col, blockBuf);
|
| 144 |
+
|
| 145 |
+
// Forward DCT
|
| 146 |
+
dctForward8x8(blockBuf);
|
| 147 |
+
|
| 148 |
+
const maskFactor = maskingFactors ? maskingFactors[bi] : 1.0;
|
| 149 |
+
const effectiveDelta = config.delta * maskFactor;
|
| 150 |
+
|
| 151 |
+
// Embed bits into selected zigzag positions
|
| 152 |
+
for (let z = 0; z < zigCoeffIdx.length; z++) {
|
| 153 |
+
if (bitIdx >= codedLength) bitIdx = 0; // Repeat pattern
|
| 154 |
+
|
| 155 |
+
const coeffIdx = zigCoeffIdx[z];
|
| 156 |
+
const bit = interleaved[bitIdx];
|
| 157 |
+
const dither = dithers[ditherIdx++];
|
| 158 |
+
|
| 159 |
+
blockBuf[coeffIdx] = dmqimEmbed(blockBuf[coeffIdx], bit, effectiveDelta, dither);
|
| 160 |
+
|
| 161 |
+
bitIdx++;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
// Inverse DCT and write back
|
| 165 |
+
dctInverse8x8(blockBuf);
|
| 166 |
+
writeBlock(hlSubband.data, hlSubband.width, row, col, blockBuf);
|
| 167 |
+
}
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
// Step 7: Write modified HL subband back and inverse DWT
|
| 171 |
+
writeSubband(dwtBuf, dims[dims.length - 1].w, dims[dims.length - 1].h, 'HL', hlSubband);
|
| 172 |
+
dwtInverse(dwtBuf, dims);
|
| 173 |
+
|
| 174 |
+
// Convert back to Y plane
|
| 175 |
+
const watermarkedY = bufferToYPlane(dwtBuf);
|
| 176 |
+
|
| 177 |
+
// Compute PSNR
|
| 178 |
+
let mse = 0;
|
| 179 |
+
for (let i = 0; i < yPlane.length; i++) {
|
| 180 |
+
const diff = yPlane[i] - watermarkedY[i];
|
| 181 |
+
mse += diff * diff;
|
| 182 |
+
}
|
| 183 |
+
mse /= yPlane.length;
|
| 184 |
+
const psnr = mse > 0 ? 10 * Math.log10(255 * 255 / mse) : Infinity;
|
| 185 |
+
|
| 186 |
+
return { yPlane: watermarkedY, psnr };
|
| 187 |
+
}
|
core/keygen.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Key derivation and PRNG for watermark embedding
|
| 3 |
+
*
|
| 4 |
+
* Uses a simple but effective approach: derive deterministic
|
| 5 |
+
* pseudo-random sequences from a secret key using a seeded PRNG.
|
| 6 |
+
* For browser compatibility, we use a pure-JS implementation.
|
| 7 |
+
*/
|
| 8 |
+
|
| 9 |
+
/**
|
| 10 |
+
* Simple hash function (djb2 variant) for string to 32-bit seed
|
| 11 |
+
*/
|
| 12 |
+
function hashString(str: string): number {
|
| 13 |
+
let hash = 5381;
|
| 14 |
+
for (let i = 0; i < str.length; i++) {
|
| 15 |
+
hash = ((hash << 5) + hash + str.charCodeAt(i)) | 0;
|
| 16 |
+
}
|
| 17 |
+
return hash >>> 0;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
/**
|
| 21 |
+
* Mix two 32-bit values (MurmurHash3 finalizer)
|
| 22 |
+
*/
|
| 23 |
+
function mix(h: number): number {
|
| 24 |
+
h = ((h >>> 16) ^ h) * 0x45d9f3b | 0;
|
| 25 |
+
h = ((h >>> 16) ^ h) * 0x45d9f3b | 0;
|
| 26 |
+
h = (h >>> 16) ^ h;
|
| 27 |
+
return h >>> 0;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/**
|
| 31 |
+
* Seeded PRNG (xorshift128)
|
| 32 |
+
* Produces deterministic sequences from a seed
|
| 33 |
+
*/
|
| 34 |
+
export class SeededPRNG {
|
| 35 |
+
private s0: number;
|
| 36 |
+
private s1: number;
|
| 37 |
+
private s2: number;
|
| 38 |
+
private s3: number;
|
| 39 |
+
|
| 40 |
+
constructor(seed: number) {
|
| 41 |
+
// Initialize state from seed using splitmix32
|
| 42 |
+
this.s0 = seed | 0;
|
| 43 |
+
this.s1 = mix(seed + 1);
|
| 44 |
+
this.s2 = mix(seed + 2);
|
| 45 |
+
this.s3 = mix(seed + 3);
|
| 46 |
+
|
| 47 |
+
// Warm up
|
| 48 |
+
for (let i = 0; i < 20; i++) this.next();
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/** Get next random 32-bit unsigned integer */
|
| 52 |
+
next(): number {
|
| 53 |
+
let t = this.s3;
|
| 54 |
+
const s = this.s0;
|
| 55 |
+
this.s3 = this.s2;
|
| 56 |
+
this.s2 = this.s1;
|
| 57 |
+
this.s1 = s;
|
| 58 |
+
t ^= t << 11;
|
| 59 |
+
t ^= t >>> 8;
|
| 60 |
+
this.s0 = t ^ s ^ (s >>> 19);
|
| 61 |
+
return this.s0 >>> 0;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
/** Get random float in [0, 1) */
|
| 65 |
+
nextFloat(): number {
|
| 66 |
+
return this.next() / 0x100000000;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
/** Get random float in [min, max) */
|
| 70 |
+
nextRange(min: number, max: number): number {
|
| 71 |
+
return min + this.nextFloat() * (max - min);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
/** Get random integer in [0, max) */
|
| 75 |
+
nextInt(max: number): number {
|
| 76 |
+
return (this.next() % max) >>> 0;
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
/**
|
| 81 |
+
* Derive a seed from key + purpose string
|
| 82 |
+
*/
|
| 83 |
+
export function deriveSeed(key: string, purpose: string): number {
|
| 84 |
+
const combined = key + ':' + purpose;
|
| 85 |
+
return mix(hashString(combined));
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
/**
|
| 89 |
+
* Generate dither values for DM-QIM embedding
|
| 90 |
+
* Returns array of dither values in [-delta/2, +delta/2]
|
| 91 |
+
*/
|
| 92 |
+
export function generateDithers(key: string, count: number, delta: number): Float64Array {
|
| 93 |
+
const prng = new SeededPRNG(deriveSeed(key, 'dither'));
|
| 94 |
+
const dithers = new Float64Array(count);
|
| 95 |
+
for (let i = 0; i < count; i++) {
|
| 96 |
+
dithers[i] = prng.nextRange(-delta / 2, delta / 2);
|
| 97 |
+
}
|
| 98 |
+
return dithers;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
/**
|
| 102 |
+
* Generate a permutation (Fisher-Yates) for bit interleaving
|
| 103 |
+
*/
|
| 104 |
+
export function generatePermutation(key: string, length: number): Uint32Array {
|
| 105 |
+
const prng = new SeededPRNG(deriveSeed(key, 'permutation'));
|
| 106 |
+
const perm = new Uint32Array(length);
|
| 107 |
+
for (let i = 0; i < length; i++) perm[i] = i;
|
| 108 |
+
|
| 109 |
+
for (let i = length - 1; i > 0; i--) {
|
| 110 |
+
const j = prng.nextInt(i + 1);
|
| 111 |
+
const tmp = perm[i];
|
| 112 |
+
perm[i] = perm[j];
|
| 113 |
+
perm[j] = tmp;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
return perm;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
/**
|
| 120 |
+
* Generate inverse permutation
|
| 121 |
+
*/
|
| 122 |
+
export function generateInversePermutation(key: string, length: number): Uint32Array {
|
| 123 |
+
const perm = generatePermutation(key, length);
|
| 124 |
+
const inv = new Uint32Array(length);
|
| 125 |
+
for (let i = 0; i < length; i++) {
|
| 126 |
+
inv[perm[i]] = i;
|
| 127 |
+
}
|
| 128 |
+
return inv;
|
| 129 |
+
}
|
core/masking.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Perceptual masking — adapt watermark strength based on local image content
|
| 3 |
+
*
|
| 4 |
+
* High-energy (textured) areas can tolerate stronger watermarks,
|
| 5 |
+
* while smooth areas need weaker embedding to remain imperceptible.
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* Compute AC energy of an 8x8 DCT block (sum of squared AC coefficients)
|
| 10 |
+
* Assumes the block is already in DCT domain.
|
| 11 |
+
*/
|
| 12 |
+
export function blockAcEnergy(dctBlock: Float64Array): number {
|
| 13 |
+
let energy = 0;
|
| 14 |
+
for (let i = 1; i < 64; i++) { // Skip DC (index 0)
|
| 15 |
+
energy += dctBlock[i] * dctBlock[i];
|
| 16 |
+
}
|
| 17 |
+
return energy;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
/**
|
| 21 |
+
* Compute perceptual masking factors for a set of DCT blocks
|
| 22 |
+
*
|
| 23 |
+
* Returns per-block multiplier for delta:
|
| 24 |
+
* Δ_effective = Δ_base × masking_factor
|
| 25 |
+
*
|
| 26 |
+
* Factors are in [0.5, 2.0]:
|
| 27 |
+
* - Smooth blocks → factor < 1 (reduce strength)
|
| 28 |
+
* - Textured blocks → factor > 1 (can increase strength)
|
| 29 |
+
*
|
| 30 |
+
* @param blockEnergies - AC energy for each block
|
| 31 |
+
* @returns Array of masking factors, same length as input
|
| 32 |
+
*/
|
| 33 |
+
export function computeMaskingFactors(blockEnergies: Float64Array): Float64Array {
|
| 34 |
+
const n = blockEnergies.length;
|
| 35 |
+
if (n === 0) return new Float64Array(0);
|
| 36 |
+
|
| 37 |
+
// Compute median energy
|
| 38 |
+
const sorted = new Float64Array(blockEnergies).sort();
|
| 39 |
+
const median = n % 2 === 0
|
| 40 |
+
? (sorted[n / 2 - 1] + sorted[n / 2]) / 2
|
| 41 |
+
: sorted[Math.floor(n / 2)];
|
| 42 |
+
|
| 43 |
+
// Avoid division by zero
|
| 44 |
+
const safeMedian = Math.max(median, 1e-6);
|
| 45 |
+
|
| 46 |
+
const factors = new Float64Array(n);
|
| 47 |
+
for (let i = 0; i < n; i++) {
|
| 48 |
+
const ratio = blockEnergies[i] / safeMedian;
|
| 49 |
+
// Clamp to [0.5, 2.0]
|
| 50 |
+
factors[i] = Math.max(0.5, Math.min(2.0, ratio));
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
return factors;
|
| 54 |
+
}
|
core/presets.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { PresetName, WatermarkConfig } from './types.js';
|
| 2 |
+
|
| 3 |
+
/** Mid-frequency DCT coefficients (zigzag positions 3–14, 12 coeffs) */
|
| 4 |
+
const MID_FREQ_ZIGZAG = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
|
| 5 |
+
|
| 6 |
+
/** Low+mid frequency coefficients (zigzag 1–20, 20 coeffs) — survive heavy compression */
|
| 7 |
+
const LOW_MID_FREQ_ZIGZAG = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
|
| 8 |
+
|
| 9 |
+
/** Low frequency coefficients (zigzag 1–10, 10 coeffs) — survive extreme compression */
|
| 10 |
+
const LOW_FREQ_ZIGZAG = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
| 11 |
+
|
| 12 |
+
export const PRESETS: Record<PresetName, WatermarkConfig> = {
|
| 13 |
+
light: {
|
| 14 |
+
alpha: 0.10,
|
| 15 |
+
delta: 50,
|
| 16 |
+
tilePeriod: 256,
|
| 17 |
+
bch: { n: 63, k: 36, t: 5, m: 6 },
|
| 18 |
+
crc: { bits: 4 },
|
| 19 |
+
dwtLevels: 2,
|
| 20 |
+
zigzagPositions: MID_FREQ_ZIGZAG,
|
| 21 |
+
perceptualMasking: false,
|
| 22 |
+
temporalFrames: 1,
|
| 23 |
+
},
|
| 24 |
+
|
| 25 |
+
moderate: {
|
| 26 |
+
alpha: 0.18,
|
| 27 |
+
delta: 80,
|
| 28 |
+
tilePeriod: 232,
|
| 29 |
+
bch: { n: 63, k: 36, t: 5, m: 6 },
|
| 30 |
+
crc: { bits: 4 },
|
| 31 |
+
dwtLevels: 2,
|
| 32 |
+
zigzagPositions: MID_FREQ_ZIGZAG,
|
| 33 |
+
perceptualMasking: true,
|
| 34 |
+
temporalFrames: 1,
|
| 35 |
+
},
|
| 36 |
+
|
| 37 |
+
strong: {
|
| 38 |
+
alpha: 0.26,
|
| 39 |
+
delta: 110,
|
| 40 |
+
tilePeriod: 208,
|
| 41 |
+
bch: { n: 63, k: 36, t: 5, m: 6 },
|
| 42 |
+
crc: { bits: 4 },
|
| 43 |
+
dwtLevels: 2,
|
| 44 |
+
zigzagPositions: LOW_MID_FREQ_ZIGZAG,
|
| 45 |
+
perceptualMasking: true,
|
| 46 |
+
temporalFrames: 1,
|
| 47 |
+
},
|
| 48 |
+
|
| 49 |
+
fortress: {
|
| 50 |
+
alpha: 0.35,
|
| 51 |
+
delta: 150,
|
| 52 |
+
tilePeriod: 192,
|
| 53 |
+
bch: { n: 63, k: 36, t: 5, m: 6 },
|
| 54 |
+
crc: { bits: 4 },
|
| 55 |
+
dwtLevels: 2,
|
| 56 |
+
zigzagPositions: LOW_MID_FREQ_ZIGZAG,
|
| 57 |
+
perceptualMasking: true,
|
| 58 |
+
temporalFrames: 1,
|
| 59 |
+
},
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
/**
|
| 63 |
+
* Get a preset configuration by name
|
| 64 |
+
*/
|
| 65 |
+
export function getPreset(name: PresetName): WatermarkConfig {
|
| 66 |
+
return { ...PRESETS[name] };
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
/**
|
| 70 |
+
* Preset descriptions for the UI
|
| 71 |
+
*/
|
| 72 |
+
export const PRESET_DESCRIPTIONS: Record<PresetName, string> = {
|
| 73 |
+
light: '🌤️ Lightest touch. Near-invisible, survives mild compression.',
|
| 74 |
+
moderate: '⚡ Balanced embedding with perceptual masking. Good compression resilience.',
|
| 75 |
+
strong: '🛡️ Stronger embedding across more frequencies. Handles rescaling well.',
|
| 76 |
+
fortress: '🏰 Maximum robustness. Survives heavy compression and color adjustments.',
|
| 77 |
+
};
|
core/tiling.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Tile layout and autocorrelation-based sync recovery
|
| 3 |
+
*
|
| 4 |
+
* The watermark is embedded as a periodic pattern of tiles.
|
| 5 |
+
* Each tile contains one complete copy of the coded payload.
|
| 6 |
+
* During detection, autocorrelation recovers the tile grid
|
| 7 |
+
* even after cropping.
|
| 8 |
+
*/
|
| 9 |
+
|
| 10 |
+
import type { Buffer2D } from './types.js';
|
| 11 |
+
|
| 12 |
+
/** Tile grid description */
|
| 13 |
+
export interface TileGrid {
|
| 14 |
+
/** Tile period in subband pixels */
|
| 15 |
+
tilePeriod: number;
|
| 16 |
+
/** Phase offset X (for cropped frames) */
|
| 17 |
+
phaseX: number;
|
| 18 |
+
/** Phase offset Y (for cropped frames) */
|
| 19 |
+
phaseY: number;
|
| 20 |
+
/** Number of complete tiles in X direction */
|
| 21 |
+
tilesX: number;
|
| 22 |
+
/** Number of complete tiles in Y direction */
|
| 23 |
+
tilesY: number;
|
| 24 |
+
/** Total number of tiles */
|
| 25 |
+
totalTiles: number;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
/**
|
| 29 |
+
* Compute the tile grid for a given subband size and tile period
|
| 30 |
+
* During embedding, phase is always (0, 0)
|
| 31 |
+
*/
|
| 32 |
+
export function computeTileGrid(
|
| 33 |
+
subbandWidth: number,
|
| 34 |
+
subbandHeight: number,
|
| 35 |
+
tilePeriod: number
|
| 36 |
+
): TileGrid {
|
| 37 |
+
const tilesX = Math.floor(subbandWidth / tilePeriod);
|
| 38 |
+
const tilesY = Math.floor(subbandHeight / tilePeriod);
|
| 39 |
+
return {
|
| 40 |
+
tilePeriod,
|
| 41 |
+
phaseX: 0,
|
| 42 |
+
phaseY: 0,
|
| 43 |
+
tilesX,
|
| 44 |
+
tilesY,
|
| 45 |
+
totalTiles: tilesX * tilesY,
|
| 46 |
+
};
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
/**
|
| 50 |
+
* Get the subband region for a specific tile
|
| 51 |
+
* Returns [startX, startY] in subband coordinates
|
| 52 |
+
*/
|
| 53 |
+
export function getTileOrigin(grid: TileGrid, tileIdx: number): { x: number; y: number } {
|
| 54 |
+
const tileCol = tileIdx % grid.tilesX;
|
| 55 |
+
const tileRow = Math.floor(tileIdx / grid.tilesX);
|
| 56 |
+
return {
|
| 57 |
+
x: grid.phaseX + tileCol * grid.tilePeriod,
|
| 58 |
+
y: grid.phaseY + tileRow * grid.tilePeriod,
|
| 59 |
+
};
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
/**
|
| 63 |
+
* Compute 8x8 DCT block positions within a tile
|
| 64 |
+
* Returns array of [blockRow, blockCol] in subband coordinates
|
| 65 |
+
*/
|
| 66 |
+
export function getTileBlocks(
|
| 67 |
+
tileOriginX: number,
|
| 68 |
+
tileOriginY: number,
|
| 69 |
+
tilePeriod: number,
|
| 70 |
+
subbandWidth: number,
|
| 71 |
+
subbandHeight: number
|
| 72 |
+
): Array<{ row: number; col: number }> {
|
| 73 |
+
const blocks: Array<{ row: number; col: number }> = [];
|
| 74 |
+
const blocksPerTileSide = Math.floor(tilePeriod / 8);
|
| 75 |
+
|
| 76 |
+
for (let br = 0; br < blocksPerTileSide; br++) {
|
| 77 |
+
for (let bc = 0; bc < blocksPerTileSide; bc++) {
|
| 78 |
+
const absRow = Math.floor(tileOriginY / 8) + br;
|
| 79 |
+
const absCol = Math.floor(tileOriginX / 8) + bc;
|
| 80 |
+
// Ensure the block fits within the subband
|
| 81 |
+
if ((absRow + 1) * 8 <= subbandHeight && (absCol + 1) * 8 <= subbandWidth) {
|
| 82 |
+
blocks.push({ row: absRow, col: absCol });
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
return blocks;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
/**
|
| 91 |
+
* Autocorrelation-based tile period and phase recovery
|
| 92 |
+
*
|
| 93 |
+
* Computes 2D autocorrelation of the subband energy pattern
|
| 94 |
+
* to find the periodic tile structure.
|
| 95 |
+
*/
|
| 96 |
+
export function recoverTileGrid(
|
| 97 |
+
subband: Buffer2D,
|
| 98 |
+
expectedTilePeriod: number,
|
| 99 |
+
searchRange: number = 4
|
| 100 |
+
): TileGrid {
|
| 101 |
+
const { data, width, height } = subband;
|
| 102 |
+
|
| 103 |
+
// Compute block energy map (8x8 blocks)
|
| 104 |
+
const bw = Math.floor(width / 8);
|
| 105 |
+
const bh = Math.floor(height / 8);
|
| 106 |
+
const energy = new Float64Array(bw * bh);
|
| 107 |
+
|
| 108 |
+
for (let by = 0; by < bh; by++) {
|
| 109 |
+
for (let bx = 0; bx < bw; bx++) {
|
| 110 |
+
let e = 0;
|
| 111 |
+
for (let r = 0; r < 8; r++) {
|
| 112 |
+
for (let c = 0; c < 8; c++) {
|
| 113 |
+
const v = data[(by * 8 + r) * width + (bx * 8 + c)];
|
| 114 |
+
e += v * v;
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
energy[by * bw + bx] = e;
|
| 118 |
+
}
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
// Expected tile period in blocks
|
| 122 |
+
const expectedBlockPeriod = Math.floor(expectedTilePeriod / 8);
|
| 123 |
+
|
| 124 |
+
// Search for the best period near the expected value
|
| 125 |
+
let bestPeriod = expectedBlockPeriod;
|
| 126 |
+
let bestCorr = -Infinity;
|
| 127 |
+
let bestPhaseX = 0;
|
| 128 |
+
let bestPhaseY = 0;
|
| 129 |
+
|
| 130 |
+
for (let p = expectedBlockPeriod - searchRange; p <= expectedBlockPeriod + searchRange; p++) {
|
| 131 |
+
if (p < 2) continue;
|
| 132 |
+
|
| 133 |
+
// For each candidate phase offset
|
| 134 |
+
for (let py = 0; py < p; py++) {
|
| 135 |
+
for (let px = 0; px < p; px++) {
|
| 136 |
+
let corr = 0;
|
| 137 |
+
let count = 0;
|
| 138 |
+
|
| 139 |
+
// Compute autocorrelation at this period and phase
|
| 140 |
+
for (let by = py; by + p < bh; by += p) {
|
| 141 |
+
for (let bx = px; bx + p < bw; bx += p) {
|
| 142 |
+
const e1 = energy[by * bw + bx];
|
| 143 |
+
const e2 = energy[(by + p) * bw + bx];
|
| 144 |
+
const e3 = energy[by * bw + (bx + p)];
|
| 145 |
+
corr += e1 * e2 + e1 * e3;
|
| 146 |
+
count++;
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
if (count > 0) {
|
| 151 |
+
corr /= count;
|
| 152 |
+
if (corr > bestCorr) {
|
| 153 |
+
bestCorr = corr;
|
| 154 |
+
bestPeriod = p;
|
| 155 |
+
bestPhaseX = px;
|
| 156 |
+
bestPhaseY = py;
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
// Convert from block coordinates back to subband pixels
|
| 164 |
+
const tilePeriod = bestPeriod * 8;
|
| 165 |
+
const phaseX = bestPhaseX * 8;
|
| 166 |
+
const phaseY = bestPhaseY * 8;
|
| 167 |
+
const tilesX = Math.floor((width - phaseX) / tilePeriod);
|
| 168 |
+
const tilesY = Math.floor((height - phaseY) / tilePeriod);
|
| 169 |
+
|
| 170 |
+
return {
|
| 171 |
+
tilePeriod,
|
| 172 |
+
phaseX,
|
| 173 |
+
phaseY,
|
| 174 |
+
tilesX,
|
| 175 |
+
tilesY,
|
| 176 |
+
totalTiles: tilesX * tilesY,
|
| 177 |
+
};
|
| 178 |
+
}
|
core/types.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** Watermark preset names */
|
| 2 |
+
export type PresetName = 'light' | 'moderate' | 'strong' | 'fortress';
|
| 3 |
+
|
| 4 |
+
/** BCH code parameters */
|
| 5 |
+
export interface BchParams {
|
| 6 |
+
/** Codeword length */
|
| 7 |
+
n: number;
|
| 8 |
+
/** Message length */
|
| 9 |
+
k: number;
|
| 10 |
+
/** Error correction capability */
|
| 11 |
+
t: number;
|
| 12 |
+
/** Galois field order m (GF(2^m)) */
|
| 13 |
+
m: number;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
/** CRC configuration */
|
| 17 |
+
export interface CrcConfig {
|
| 18 |
+
/** CRC bit width (4, 8, or 16) */
|
| 19 |
+
bits: 4 | 8 | 16;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
/** Full watermark configuration */
|
| 23 |
+
export interface WatermarkConfig {
|
| 24 |
+
/** Embedding strength multiplier (0–1) */
|
| 25 |
+
alpha: number;
|
| 26 |
+
/** QIM quantization step */
|
| 27 |
+
delta: number;
|
| 28 |
+
/** Tile period in pixels (spatial domain) */
|
| 29 |
+
tilePeriod: number;
|
| 30 |
+
/** BCH code parameters */
|
| 31 |
+
bch: BchParams;
|
| 32 |
+
/** CRC configuration */
|
| 33 |
+
crc: CrcConfig;
|
| 34 |
+
/** DWT decomposition levels */
|
| 35 |
+
dwtLevels: number;
|
| 36 |
+
/** Zigzag coefficient positions to use within 8x8 DCT blocks */
|
| 37 |
+
zigzagPositions: number[];
|
| 38 |
+
/** Enable perceptual masking */
|
| 39 |
+
perceptualMasking: boolean;
|
| 40 |
+
/** Number of frames to average for temporal detection (1 = single frame) */
|
| 41 |
+
temporalFrames: number;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
/** Detection result */
|
| 45 |
+
export interface DetectionResult {
|
| 46 |
+
/** Whether a watermark was detected */
|
| 47 |
+
detected: boolean;
|
| 48 |
+
/** Decoded payload (null if not detected) */
|
| 49 |
+
payload: Uint8Array | null;
|
| 50 |
+
/** Detection confidence (0–1) */
|
| 51 |
+
confidence: number;
|
| 52 |
+
/** Number of tiles successfully decoded */
|
| 53 |
+
tilesDecoded: number;
|
| 54 |
+
/** Total tiles found */
|
| 55 |
+
tilesTotal: number;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
/** Embedding result */
|
| 59 |
+
export interface EmbedResult {
|
| 60 |
+
/** Watermarked Y plane */
|
| 61 |
+
yPlane: Uint8Array;
|
| 62 |
+
/** PSNR of the watermarked frame relative to original (dB) */
|
| 63 |
+
psnr: number;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/** 2D buffer for signal processing (row-major Float64Array) */
|
| 67 |
+
export interface Buffer2D {
|
| 68 |
+
data: Float64Array;
|
| 69 |
+
width: number;
|
| 70 |
+
height: number;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/** DWT decomposition result */
|
| 74 |
+
export interface DwtResult {
|
| 75 |
+
/** All subbands at each level: [level][LL, LH, HL, HH] */
|
| 76 |
+
subbands: Buffer2D[][];
|
| 77 |
+
/** Original dimensions at each level */
|
| 78 |
+
dimensions: Array<{ width: number; height: number }>;
|
| 79 |
+
/** Number of decomposition levels */
|
| 80 |
+
levels: number;
|
| 81 |
+
}
|
package-lock.json
ADDED
|
@@ -0,0 +1,2826 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "ltmarx",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"lockfileVersion": 3,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"packages": {
|
| 7 |
+
"": {
|
| 8 |
+
"name": "ltmarx",
|
| 9 |
+
"version": "1.0.0",
|
| 10 |
+
"license": "MIT",
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"@ffmpeg/ffmpeg": "^0.12.15",
|
| 13 |
+
"@ffmpeg/util": "^0.12.2",
|
| 14 |
+
"react": "^19.2.4",
|
| 15 |
+
"react-dom": "^19.2.4"
|
| 16 |
+
},
|
| 17 |
+
"bin": {
|
| 18 |
+
"ltmarx": "dist/server/cli.js"
|
| 19 |
+
},
|
| 20 |
+
"devDependencies": {
|
| 21 |
+
"@tailwindcss/vite": "^4.2.1",
|
| 22 |
+
"@types/node": "^25.3.0",
|
| 23 |
+
"@types/react": "^19.2.14",
|
| 24 |
+
"@types/react-dom": "^19.2.3",
|
| 25 |
+
"@vitejs/plugin-react": "^5.1.4",
|
| 26 |
+
"tailwindcss": "^4.2.1",
|
| 27 |
+
"typescript": "^5.9.3",
|
| 28 |
+
"vite": "^7.3.1",
|
| 29 |
+
"vitest": "^4.0.18"
|
| 30 |
+
}
|
| 31 |
+
},
|
| 32 |
+
"node_modules/@babel/code-frame": {
|
| 33 |
+
"version": "7.29.0",
|
| 34 |
+
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
| 35 |
+
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
|
| 36 |
+
"dev": true,
|
| 37 |
+
"license": "MIT",
|
| 38 |
+
"dependencies": {
|
| 39 |
+
"@babel/helper-validator-identifier": "^7.28.5",
|
| 40 |
+
"js-tokens": "^4.0.0",
|
| 41 |
+
"picocolors": "^1.1.1"
|
| 42 |
+
},
|
| 43 |
+
"engines": {
|
| 44 |
+
"node": ">=6.9.0"
|
| 45 |
+
}
|
| 46 |
+
},
|
| 47 |
+
"node_modules/@babel/compat-data": {
|
| 48 |
+
"version": "7.29.0",
|
| 49 |
+
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
|
| 50 |
+
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
|
| 51 |
+
"dev": true,
|
| 52 |
+
"license": "MIT",
|
| 53 |
+
"engines": {
|
| 54 |
+
"node": ">=6.9.0"
|
| 55 |
+
}
|
| 56 |
+
},
|
| 57 |
+
"node_modules/@babel/core": {
|
| 58 |
+
"version": "7.29.0",
|
| 59 |
+
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
| 60 |
+
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
| 61 |
+
"dev": true,
|
| 62 |
+
"license": "MIT",
|
| 63 |
+
"dependencies": {
|
| 64 |
+
"@babel/code-frame": "^7.29.0",
|
| 65 |
+
"@babel/generator": "^7.29.0",
|
| 66 |
+
"@babel/helper-compilation-targets": "^7.28.6",
|
| 67 |
+
"@babel/helper-module-transforms": "^7.28.6",
|
| 68 |
+
"@babel/helpers": "^7.28.6",
|
| 69 |
+
"@babel/parser": "^7.29.0",
|
| 70 |
+
"@babel/template": "^7.28.6",
|
| 71 |
+
"@babel/traverse": "^7.29.0",
|
| 72 |
+
"@babel/types": "^7.29.0",
|
| 73 |
+
"@jridgewell/remapping": "^2.3.5",
|
| 74 |
+
"convert-source-map": "^2.0.0",
|
| 75 |
+
"debug": "^4.1.0",
|
| 76 |
+
"gensync": "^1.0.0-beta.2",
|
| 77 |
+
"json5": "^2.2.3",
|
| 78 |
+
"semver": "^6.3.1"
|
| 79 |
+
},
|
| 80 |
+
"engines": {
|
| 81 |
+
"node": ">=6.9.0"
|
| 82 |
+
},
|
| 83 |
+
"funding": {
|
| 84 |
+
"type": "opencollective",
|
| 85 |
+
"url": "https://opencollective.com/babel"
|
| 86 |
+
}
|
| 87 |
+
},
|
| 88 |
+
"node_modules/@babel/generator": {
|
| 89 |
+
"version": "7.29.1",
|
| 90 |
+
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
|
| 91 |
+
"integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
|
| 92 |
+
"dev": true,
|
| 93 |
+
"license": "MIT",
|
| 94 |
+
"dependencies": {
|
| 95 |
+
"@babel/parser": "^7.29.0",
|
| 96 |
+
"@babel/types": "^7.29.0",
|
| 97 |
+
"@jridgewell/gen-mapping": "^0.3.12",
|
| 98 |
+
"@jridgewell/trace-mapping": "^0.3.28",
|
| 99 |
+
"jsesc": "^3.0.2"
|
| 100 |
+
},
|
| 101 |
+
"engines": {
|
| 102 |
+
"node": ">=6.9.0"
|
| 103 |
+
}
|
| 104 |
+
},
|
| 105 |
+
"node_modules/@babel/helper-compilation-targets": {
|
| 106 |
+
"version": "7.28.6",
|
| 107 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
|
| 108 |
+
"integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
|
| 109 |
+
"dev": true,
|
| 110 |
+
"license": "MIT",
|
| 111 |
+
"dependencies": {
|
| 112 |
+
"@babel/compat-data": "^7.28.6",
|
| 113 |
+
"@babel/helper-validator-option": "^7.27.1",
|
| 114 |
+
"browserslist": "^4.24.0",
|
| 115 |
+
"lru-cache": "^5.1.1",
|
| 116 |
+
"semver": "^6.3.1"
|
| 117 |
+
},
|
| 118 |
+
"engines": {
|
| 119 |
+
"node": ">=6.9.0"
|
| 120 |
+
}
|
| 121 |
+
},
|
| 122 |
+
"node_modules/@babel/helper-globals": {
|
| 123 |
+
"version": "7.28.0",
|
| 124 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
|
| 125 |
+
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
|
| 126 |
+
"dev": true,
|
| 127 |
+
"license": "MIT",
|
| 128 |
+
"engines": {
|
| 129 |
+
"node": ">=6.9.0"
|
| 130 |
+
}
|
| 131 |
+
},
|
| 132 |
+
"node_modules/@babel/helper-module-imports": {
|
| 133 |
+
"version": "7.28.6",
|
| 134 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
|
| 135 |
+
"integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
|
| 136 |
+
"dev": true,
|
| 137 |
+
"license": "MIT",
|
| 138 |
+
"dependencies": {
|
| 139 |
+
"@babel/traverse": "^7.28.6",
|
| 140 |
+
"@babel/types": "^7.28.6"
|
| 141 |
+
},
|
| 142 |
+
"engines": {
|
| 143 |
+
"node": ">=6.9.0"
|
| 144 |
+
}
|
| 145 |
+
},
|
| 146 |
+
"node_modules/@babel/helper-module-transforms": {
|
| 147 |
+
"version": "7.28.6",
|
| 148 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
|
| 149 |
+
"integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
|
| 150 |
+
"dev": true,
|
| 151 |
+
"license": "MIT",
|
| 152 |
+
"dependencies": {
|
| 153 |
+
"@babel/helper-module-imports": "^7.28.6",
|
| 154 |
+
"@babel/helper-validator-identifier": "^7.28.5",
|
| 155 |
+
"@babel/traverse": "^7.28.6"
|
| 156 |
+
},
|
| 157 |
+
"engines": {
|
| 158 |
+
"node": ">=6.9.0"
|
| 159 |
+
},
|
| 160 |
+
"peerDependencies": {
|
| 161 |
+
"@babel/core": "^7.0.0"
|
| 162 |
+
}
|
| 163 |
+
},
|
| 164 |
+
"node_modules/@babel/helper-plugin-utils": {
|
| 165 |
+
"version": "7.28.6",
|
| 166 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
|
| 167 |
+
"integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
|
| 168 |
+
"dev": true,
|
| 169 |
+
"license": "MIT",
|
| 170 |
+
"engines": {
|
| 171 |
+
"node": ">=6.9.0"
|
| 172 |
+
}
|
| 173 |
+
},
|
| 174 |
+
"node_modules/@babel/helper-string-parser": {
|
| 175 |
+
"version": "7.27.1",
|
| 176 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
| 177 |
+
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
| 178 |
+
"dev": true,
|
| 179 |
+
"license": "MIT",
|
| 180 |
+
"engines": {
|
| 181 |
+
"node": ">=6.9.0"
|
| 182 |
+
}
|
| 183 |
+
},
|
| 184 |
+
"node_modules/@babel/helper-validator-identifier": {
|
| 185 |
+
"version": "7.28.5",
|
| 186 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
| 187 |
+
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
| 188 |
+
"dev": true,
|
| 189 |
+
"license": "MIT",
|
| 190 |
+
"engines": {
|
| 191 |
+
"node": ">=6.9.0"
|
| 192 |
+
}
|
| 193 |
+
},
|
| 194 |
+
"node_modules/@babel/helper-validator-option": {
|
| 195 |
+
"version": "7.27.1",
|
| 196 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
|
| 197 |
+
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
|
| 198 |
+
"dev": true,
|
| 199 |
+
"license": "MIT",
|
| 200 |
+
"engines": {
|
| 201 |
+
"node": ">=6.9.0"
|
| 202 |
+
}
|
| 203 |
+
},
|
| 204 |
+
"node_modules/@babel/helpers": {
|
| 205 |
+
"version": "7.28.6",
|
| 206 |
+
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
|
| 207 |
+
"integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
|
| 208 |
+
"dev": true,
|
| 209 |
+
"license": "MIT",
|
| 210 |
+
"dependencies": {
|
| 211 |
+
"@babel/template": "^7.28.6",
|
| 212 |
+
"@babel/types": "^7.28.6"
|
| 213 |
+
},
|
| 214 |
+
"engines": {
|
| 215 |
+
"node": ">=6.9.0"
|
| 216 |
+
}
|
| 217 |
+
},
|
| 218 |
+
"node_modules/@babel/parser": {
|
| 219 |
+
"version": "7.29.0",
|
| 220 |
+
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
|
| 221 |
+
"integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
|
| 222 |
+
"dev": true,
|
| 223 |
+
"license": "MIT",
|
| 224 |
+
"dependencies": {
|
| 225 |
+
"@babel/types": "^7.29.0"
|
| 226 |
+
},
|
| 227 |
+
"bin": {
|
| 228 |
+
"parser": "bin/babel-parser.js"
|
| 229 |
+
},
|
| 230 |
+
"engines": {
|
| 231 |
+
"node": ">=6.0.0"
|
| 232 |
+
}
|
| 233 |
+
},
|
| 234 |
+
"node_modules/@babel/plugin-transform-react-jsx-self": {
|
| 235 |
+
"version": "7.27.1",
|
| 236 |
+
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
|
| 237 |
+
"integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
|
| 238 |
+
"dev": true,
|
| 239 |
+
"license": "MIT",
|
| 240 |
+
"dependencies": {
|
| 241 |
+
"@babel/helper-plugin-utils": "^7.27.1"
|
| 242 |
+
},
|
| 243 |
+
"engines": {
|
| 244 |
+
"node": ">=6.9.0"
|
| 245 |
+
},
|
| 246 |
+
"peerDependencies": {
|
| 247 |
+
"@babel/core": "^7.0.0-0"
|
| 248 |
+
}
|
| 249 |
+
},
|
| 250 |
+
"node_modules/@babel/plugin-transform-react-jsx-source": {
|
| 251 |
+
"version": "7.27.1",
|
| 252 |
+
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
|
| 253 |
+
"integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
|
| 254 |
+
"dev": true,
|
| 255 |
+
"license": "MIT",
|
| 256 |
+
"dependencies": {
|
| 257 |
+
"@babel/helper-plugin-utils": "^7.27.1"
|
| 258 |
+
},
|
| 259 |
+
"engines": {
|
| 260 |
+
"node": ">=6.9.0"
|
| 261 |
+
},
|
| 262 |
+
"peerDependencies": {
|
| 263 |
+
"@babel/core": "^7.0.0-0"
|
| 264 |
+
}
|
| 265 |
+
},
|
| 266 |
+
"node_modules/@babel/template": {
|
| 267 |
+
"version": "7.28.6",
|
| 268 |
+
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
| 269 |
+
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
|
| 270 |
+
"dev": true,
|
| 271 |
+
"license": "MIT",
|
| 272 |
+
"dependencies": {
|
| 273 |
+
"@babel/code-frame": "^7.28.6",
|
| 274 |
+
"@babel/parser": "^7.28.6",
|
| 275 |
+
"@babel/types": "^7.28.6"
|
| 276 |
+
},
|
| 277 |
+
"engines": {
|
| 278 |
+
"node": ">=6.9.0"
|
| 279 |
+
}
|
| 280 |
+
},
|
| 281 |
+
"node_modules/@babel/traverse": {
|
| 282 |
+
"version": "7.29.0",
|
| 283 |
+
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
|
| 284 |
+
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
|
| 285 |
+
"dev": true,
|
| 286 |
+
"license": "MIT",
|
| 287 |
+
"dependencies": {
|
| 288 |
+
"@babel/code-frame": "^7.29.0",
|
| 289 |
+
"@babel/generator": "^7.29.0",
|
| 290 |
+
"@babel/helper-globals": "^7.28.0",
|
| 291 |
+
"@babel/parser": "^7.29.0",
|
| 292 |
+
"@babel/template": "^7.28.6",
|
| 293 |
+
"@babel/types": "^7.29.0",
|
| 294 |
+
"debug": "^4.3.1"
|
| 295 |
+
},
|
| 296 |
+
"engines": {
|
| 297 |
+
"node": ">=6.9.0"
|
| 298 |
+
}
|
| 299 |
+
},
|
| 300 |
+
"node_modules/@babel/types": {
|
| 301 |
+
"version": "7.29.0",
|
| 302 |
+
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
|
| 303 |
+
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
| 304 |
+
"dev": true,
|
| 305 |
+
"license": "MIT",
|
| 306 |
+
"dependencies": {
|
| 307 |
+
"@babel/helper-string-parser": "^7.27.1",
|
| 308 |
+
"@babel/helper-validator-identifier": "^7.28.5"
|
| 309 |
+
},
|
| 310 |
+
"engines": {
|
| 311 |
+
"node": ">=6.9.0"
|
| 312 |
+
}
|
| 313 |
+
},
|
| 314 |
+
"node_modules/@esbuild/aix-ppc64": {
|
| 315 |
+
"version": "0.27.3",
|
| 316 |
+
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
|
| 317 |
+
"integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
|
| 318 |
+
"cpu": [
|
| 319 |
+
"ppc64"
|
| 320 |
+
],
|
| 321 |
+
"dev": true,
|
| 322 |
+
"license": "MIT",
|
| 323 |
+
"optional": true,
|
| 324 |
+
"os": [
|
| 325 |
+
"aix"
|
| 326 |
+
],
|
| 327 |
+
"engines": {
|
| 328 |
+
"node": ">=18"
|
| 329 |
+
}
|
| 330 |
+
},
|
| 331 |
+
"node_modules/@esbuild/android-arm": {
|
| 332 |
+
"version": "0.27.3",
|
| 333 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
|
| 334 |
+
"integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
|
| 335 |
+
"cpu": [
|
| 336 |
+
"arm"
|
| 337 |
+
],
|
| 338 |
+
"dev": true,
|
| 339 |
+
"license": "MIT",
|
| 340 |
+
"optional": true,
|
| 341 |
+
"os": [
|
| 342 |
+
"android"
|
| 343 |
+
],
|
| 344 |
+
"engines": {
|
| 345 |
+
"node": ">=18"
|
| 346 |
+
}
|
| 347 |
+
},
|
| 348 |
+
"node_modules/@esbuild/android-arm64": {
|
| 349 |
+
"version": "0.27.3",
|
| 350 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
|
| 351 |
+
"integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
|
| 352 |
+
"cpu": [
|
| 353 |
+
"arm64"
|
| 354 |
+
],
|
| 355 |
+
"dev": true,
|
| 356 |
+
"license": "MIT",
|
| 357 |
+
"optional": true,
|
| 358 |
+
"os": [
|
| 359 |
+
"android"
|
| 360 |
+
],
|
| 361 |
+
"engines": {
|
| 362 |
+
"node": ">=18"
|
| 363 |
+
}
|
| 364 |
+
},
|
| 365 |
+
"node_modules/@esbuild/android-x64": {
|
| 366 |
+
"version": "0.27.3",
|
| 367 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
|
| 368 |
+
"integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
|
| 369 |
+
"cpu": [
|
| 370 |
+
"x64"
|
| 371 |
+
],
|
| 372 |
+
"dev": true,
|
| 373 |
+
"license": "MIT",
|
| 374 |
+
"optional": true,
|
| 375 |
+
"os": [
|
| 376 |
+
"android"
|
| 377 |
+
],
|
| 378 |
+
"engines": {
|
| 379 |
+
"node": ">=18"
|
| 380 |
+
}
|
| 381 |
+
},
|
| 382 |
+
"node_modules/@esbuild/darwin-arm64": {
|
| 383 |
+
"version": "0.27.3",
|
| 384 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
|
| 385 |
+
"integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
|
| 386 |
+
"cpu": [
|
| 387 |
+
"arm64"
|
| 388 |
+
],
|
| 389 |
+
"dev": true,
|
| 390 |
+
"license": "MIT",
|
| 391 |
+
"optional": true,
|
| 392 |
+
"os": [
|
| 393 |
+
"darwin"
|
| 394 |
+
],
|
| 395 |
+
"engines": {
|
| 396 |
+
"node": ">=18"
|
| 397 |
+
}
|
| 398 |
+
},
|
| 399 |
+
"node_modules/@esbuild/darwin-x64": {
|
| 400 |
+
"version": "0.27.3",
|
| 401 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
|
| 402 |
+
"integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
|
| 403 |
+
"cpu": [
|
| 404 |
+
"x64"
|
| 405 |
+
],
|
| 406 |
+
"dev": true,
|
| 407 |
+
"license": "MIT",
|
| 408 |
+
"optional": true,
|
| 409 |
+
"os": [
|
| 410 |
+
"darwin"
|
| 411 |
+
],
|
| 412 |
+
"engines": {
|
| 413 |
+
"node": ">=18"
|
| 414 |
+
}
|
| 415 |
+
},
|
| 416 |
+
"node_modules/@esbuild/freebsd-arm64": {
|
| 417 |
+
"version": "0.27.3",
|
| 418 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
|
| 419 |
+
"integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
|
| 420 |
+
"cpu": [
|
| 421 |
+
"arm64"
|
| 422 |
+
],
|
| 423 |
+
"dev": true,
|
| 424 |
+
"license": "MIT",
|
| 425 |
+
"optional": true,
|
| 426 |
+
"os": [
|
| 427 |
+
"freebsd"
|
| 428 |
+
],
|
| 429 |
+
"engines": {
|
| 430 |
+
"node": ">=18"
|
| 431 |
+
}
|
| 432 |
+
},
|
| 433 |
+
"node_modules/@esbuild/freebsd-x64": {
|
| 434 |
+
"version": "0.27.3",
|
| 435 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
|
| 436 |
+
"integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
|
| 437 |
+
"cpu": [
|
| 438 |
+
"x64"
|
| 439 |
+
],
|
| 440 |
+
"dev": true,
|
| 441 |
+
"license": "MIT",
|
| 442 |
+
"optional": true,
|
| 443 |
+
"os": [
|
| 444 |
+
"freebsd"
|
| 445 |
+
],
|
| 446 |
+
"engines": {
|
| 447 |
+
"node": ">=18"
|
| 448 |
+
}
|
| 449 |
+
},
|
| 450 |
+
"node_modules/@esbuild/linux-arm": {
|
| 451 |
+
"version": "0.27.3",
|
| 452 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
|
| 453 |
+
"integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
|
| 454 |
+
"cpu": [
|
| 455 |
+
"arm"
|
| 456 |
+
],
|
| 457 |
+
"dev": true,
|
| 458 |
+
"license": "MIT",
|
| 459 |
+
"optional": true,
|
| 460 |
+
"os": [
|
| 461 |
+
"linux"
|
| 462 |
+
],
|
| 463 |
+
"engines": {
|
| 464 |
+
"node": ">=18"
|
| 465 |
+
}
|
| 466 |
+
},
|
| 467 |
+
"node_modules/@esbuild/linux-arm64": {
|
| 468 |
+
"version": "0.27.3",
|
| 469 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
|
| 470 |
+
"integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
|
| 471 |
+
"cpu": [
|
| 472 |
+
"arm64"
|
| 473 |
+
],
|
| 474 |
+
"dev": true,
|
| 475 |
+
"license": "MIT",
|
| 476 |
+
"optional": true,
|
| 477 |
+
"os": [
|
| 478 |
+
"linux"
|
| 479 |
+
],
|
| 480 |
+
"engines": {
|
| 481 |
+
"node": ">=18"
|
| 482 |
+
}
|
| 483 |
+
},
|
| 484 |
+
"node_modules/@esbuild/linux-ia32": {
|
| 485 |
+
"version": "0.27.3",
|
| 486 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
|
| 487 |
+
"integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
|
| 488 |
+
"cpu": [
|
| 489 |
+
"ia32"
|
| 490 |
+
],
|
| 491 |
+
"dev": true,
|
| 492 |
+
"license": "MIT",
|
| 493 |
+
"optional": true,
|
| 494 |
+
"os": [
|
| 495 |
+
"linux"
|
| 496 |
+
],
|
| 497 |
+
"engines": {
|
| 498 |
+
"node": ">=18"
|
| 499 |
+
}
|
| 500 |
+
},
|
| 501 |
+
"node_modules/@esbuild/linux-loong64": {
|
| 502 |
+
"version": "0.27.3",
|
| 503 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
|
| 504 |
+
"integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
|
| 505 |
+
"cpu": [
|
| 506 |
+
"loong64"
|
| 507 |
+
],
|
| 508 |
+
"dev": true,
|
| 509 |
+
"license": "MIT",
|
| 510 |
+
"optional": true,
|
| 511 |
+
"os": [
|
| 512 |
+
"linux"
|
| 513 |
+
],
|
| 514 |
+
"engines": {
|
| 515 |
+
"node": ">=18"
|
| 516 |
+
}
|
| 517 |
+
},
|
| 518 |
+
"node_modules/@esbuild/linux-mips64el": {
|
| 519 |
+
"version": "0.27.3",
|
| 520 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
|
| 521 |
+
"integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
|
| 522 |
+
"cpu": [
|
| 523 |
+
"mips64el"
|
| 524 |
+
],
|
| 525 |
+
"dev": true,
|
| 526 |
+
"license": "MIT",
|
| 527 |
+
"optional": true,
|
| 528 |
+
"os": [
|
| 529 |
+
"linux"
|
| 530 |
+
],
|
| 531 |
+
"engines": {
|
| 532 |
+
"node": ">=18"
|
| 533 |
+
}
|
| 534 |
+
},
|
| 535 |
+
"node_modules/@esbuild/linux-ppc64": {
|
| 536 |
+
"version": "0.27.3",
|
| 537 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
|
| 538 |
+
"integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
|
| 539 |
+
"cpu": [
|
| 540 |
+
"ppc64"
|
| 541 |
+
],
|
| 542 |
+
"dev": true,
|
| 543 |
+
"license": "MIT",
|
| 544 |
+
"optional": true,
|
| 545 |
+
"os": [
|
| 546 |
+
"linux"
|
| 547 |
+
],
|
| 548 |
+
"engines": {
|
| 549 |
+
"node": ">=18"
|
| 550 |
+
}
|
| 551 |
+
},
|
| 552 |
+
"node_modules/@esbuild/linux-riscv64": {
|
| 553 |
+
"version": "0.27.3",
|
| 554 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
|
| 555 |
+
"integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
|
| 556 |
+
"cpu": [
|
| 557 |
+
"riscv64"
|
| 558 |
+
],
|
| 559 |
+
"dev": true,
|
| 560 |
+
"license": "MIT",
|
| 561 |
+
"optional": true,
|
| 562 |
+
"os": [
|
| 563 |
+
"linux"
|
| 564 |
+
],
|
| 565 |
+
"engines": {
|
| 566 |
+
"node": ">=18"
|
| 567 |
+
}
|
| 568 |
+
},
|
| 569 |
+
"node_modules/@esbuild/linux-s390x": {
|
| 570 |
+
"version": "0.27.3",
|
| 571 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
|
| 572 |
+
"integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
|
| 573 |
+
"cpu": [
|
| 574 |
+
"s390x"
|
| 575 |
+
],
|
| 576 |
+
"dev": true,
|
| 577 |
+
"license": "MIT",
|
| 578 |
+
"optional": true,
|
| 579 |
+
"os": [
|
| 580 |
+
"linux"
|
| 581 |
+
],
|
| 582 |
+
"engines": {
|
| 583 |
+
"node": ">=18"
|
| 584 |
+
}
|
| 585 |
+
},
|
| 586 |
+
"node_modules/@esbuild/linux-x64": {
|
| 587 |
+
"version": "0.27.3",
|
| 588 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
|
| 589 |
+
"integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
|
| 590 |
+
"cpu": [
|
| 591 |
+
"x64"
|
| 592 |
+
],
|
| 593 |
+
"dev": true,
|
| 594 |
+
"license": "MIT",
|
| 595 |
+
"optional": true,
|
| 596 |
+
"os": [
|
| 597 |
+
"linux"
|
| 598 |
+
],
|
| 599 |
+
"engines": {
|
| 600 |
+
"node": ">=18"
|
| 601 |
+
}
|
| 602 |
+
},
|
| 603 |
+
"node_modules/@esbuild/netbsd-arm64": {
|
| 604 |
+
"version": "0.27.3",
|
| 605 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
|
| 606 |
+
"integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
|
| 607 |
+
"cpu": [
|
| 608 |
+
"arm64"
|
| 609 |
+
],
|
| 610 |
+
"dev": true,
|
| 611 |
+
"license": "MIT",
|
| 612 |
+
"optional": true,
|
| 613 |
+
"os": [
|
| 614 |
+
"netbsd"
|
| 615 |
+
],
|
| 616 |
+
"engines": {
|
| 617 |
+
"node": ">=18"
|
| 618 |
+
}
|
| 619 |
+
},
|
| 620 |
+
"node_modules/@esbuild/netbsd-x64": {
|
| 621 |
+
"version": "0.27.3",
|
| 622 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
|
| 623 |
+
"integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
|
| 624 |
+
"cpu": [
|
| 625 |
+
"x64"
|
| 626 |
+
],
|
| 627 |
+
"dev": true,
|
| 628 |
+
"license": "MIT",
|
| 629 |
+
"optional": true,
|
| 630 |
+
"os": [
|
| 631 |
+
"netbsd"
|
| 632 |
+
],
|
| 633 |
+
"engines": {
|
| 634 |
+
"node": ">=18"
|
| 635 |
+
}
|
| 636 |
+
},
|
| 637 |
+
"node_modules/@esbuild/openbsd-arm64": {
|
| 638 |
+
"version": "0.27.3",
|
| 639 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
|
| 640 |
+
"integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
|
| 641 |
+
"cpu": [
|
| 642 |
+
"arm64"
|
| 643 |
+
],
|
| 644 |
+
"dev": true,
|
| 645 |
+
"license": "MIT",
|
| 646 |
+
"optional": true,
|
| 647 |
+
"os": [
|
| 648 |
+
"openbsd"
|
| 649 |
+
],
|
| 650 |
+
"engines": {
|
| 651 |
+
"node": ">=18"
|
| 652 |
+
}
|
| 653 |
+
},
|
| 654 |
+
"node_modules/@esbuild/openbsd-x64": {
|
| 655 |
+
"version": "0.27.3",
|
| 656 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
|
| 657 |
+
"integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
|
| 658 |
+
"cpu": [
|
| 659 |
+
"x64"
|
| 660 |
+
],
|
| 661 |
+
"dev": true,
|
| 662 |
+
"license": "MIT",
|
| 663 |
+
"optional": true,
|
| 664 |
+
"os": [
|
| 665 |
+
"openbsd"
|
| 666 |
+
],
|
| 667 |
+
"engines": {
|
| 668 |
+
"node": ">=18"
|
| 669 |
+
}
|
| 670 |
+
},
|
| 671 |
+
"node_modules/@esbuild/openharmony-arm64": {
|
| 672 |
+
"version": "0.27.3",
|
| 673 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
|
| 674 |
+
"integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
|
| 675 |
+
"cpu": [
|
| 676 |
+
"arm64"
|
| 677 |
+
],
|
| 678 |
+
"dev": true,
|
| 679 |
+
"license": "MIT",
|
| 680 |
+
"optional": true,
|
| 681 |
+
"os": [
|
| 682 |
+
"openharmony"
|
| 683 |
+
],
|
| 684 |
+
"engines": {
|
| 685 |
+
"node": ">=18"
|
| 686 |
+
}
|
| 687 |
+
},
|
| 688 |
+
"node_modules/@esbuild/sunos-x64": {
|
| 689 |
+
"version": "0.27.3",
|
| 690 |
+
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
|
| 691 |
+
"integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
|
| 692 |
+
"cpu": [
|
| 693 |
+
"x64"
|
| 694 |
+
],
|
| 695 |
+
"dev": true,
|
| 696 |
+
"license": "MIT",
|
| 697 |
+
"optional": true,
|
| 698 |
+
"os": [
|
| 699 |
+
"sunos"
|
| 700 |
+
],
|
| 701 |
+
"engines": {
|
| 702 |
+
"node": ">=18"
|
| 703 |
+
}
|
| 704 |
+
},
|
| 705 |
+
"node_modules/@esbuild/win32-arm64": {
|
| 706 |
+
"version": "0.27.3",
|
| 707 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
|
| 708 |
+
"integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
|
| 709 |
+
"cpu": [
|
| 710 |
+
"arm64"
|
| 711 |
+
],
|
| 712 |
+
"dev": true,
|
| 713 |
+
"license": "MIT",
|
| 714 |
+
"optional": true,
|
| 715 |
+
"os": [
|
| 716 |
+
"win32"
|
| 717 |
+
],
|
| 718 |
+
"engines": {
|
| 719 |
+
"node": ">=18"
|
| 720 |
+
}
|
| 721 |
+
},
|
| 722 |
+
"node_modules/@esbuild/win32-ia32": {
|
| 723 |
+
"version": "0.27.3",
|
| 724 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
|
| 725 |
+
"integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
|
| 726 |
+
"cpu": [
|
| 727 |
+
"ia32"
|
| 728 |
+
],
|
| 729 |
+
"dev": true,
|
| 730 |
+
"license": "MIT",
|
| 731 |
+
"optional": true,
|
| 732 |
+
"os": [
|
| 733 |
+
"win32"
|
| 734 |
+
],
|
| 735 |
+
"engines": {
|
| 736 |
+
"node": ">=18"
|
| 737 |
+
}
|
| 738 |
+
},
|
| 739 |
+
"node_modules/@esbuild/win32-x64": {
|
| 740 |
+
"version": "0.27.3",
|
| 741 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
|
| 742 |
+
"integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
|
| 743 |
+
"cpu": [
|
| 744 |
+
"x64"
|
| 745 |
+
],
|
| 746 |
+
"dev": true,
|
| 747 |
+
"license": "MIT",
|
| 748 |
+
"optional": true,
|
| 749 |
+
"os": [
|
| 750 |
+
"win32"
|
| 751 |
+
],
|
| 752 |
+
"engines": {
|
| 753 |
+
"node": ">=18"
|
| 754 |
+
}
|
| 755 |
+
},
|
| 756 |
+
"node_modules/@ffmpeg/ffmpeg": {
|
| 757 |
+
"version": "0.12.15",
|
| 758 |
+
"resolved": "https://registry.npmjs.org/@ffmpeg/ffmpeg/-/ffmpeg-0.12.15.tgz",
|
| 759 |
+
"integrity": "sha512-1C8Obr4GsN3xw+/1Ww6PFM84wSQAGsdoTuTWPOj2OizsRDLT4CXTaVjPhkw6ARyDus1B9X/L2LiXHqYYsGnRFw==",
|
| 760 |
+
"license": "MIT",
|
| 761 |
+
"dependencies": {
|
| 762 |
+
"@ffmpeg/types": "^0.12.4"
|
| 763 |
+
},
|
| 764 |
+
"engines": {
|
| 765 |
+
"node": ">=18.x"
|
| 766 |
+
}
|
| 767 |
+
},
|
| 768 |
+
"node_modules/@ffmpeg/types": {
|
| 769 |
+
"version": "0.12.4",
|
| 770 |
+
"resolved": "https://registry.npmjs.org/@ffmpeg/types/-/types-0.12.4.tgz",
|
| 771 |
+
"integrity": "sha512-k9vJQNBGTxE5AhYDtOYR5rO5fKsspbg51gbcwtbkw2lCdoIILzklulcjJfIDwrtn7XhDeF2M+THwJ2FGrLeV6A==",
|
| 772 |
+
"license": "MIT",
|
| 773 |
+
"engines": {
|
| 774 |
+
"node": ">=16.x"
|
| 775 |
+
}
|
| 776 |
+
},
|
| 777 |
+
"node_modules/@ffmpeg/util": {
|
| 778 |
+
"version": "0.12.2",
|
| 779 |
+
"resolved": "https://registry.npmjs.org/@ffmpeg/util/-/util-0.12.2.tgz",
|
| 780 |
+
"integrity": "sha512-ouyoW+4JB7WxjeZ2y6KpRvB+dLp7Cp4ro8z0HIVpZVCM7AwFlHa0c4R8Y/a4M3wMqATpYKhC7lSFHQ0T11MEDw==",
|
| 781 |
+
"license": "MIT",
|
| 782 |
+
"engines": {
|
| 783 |
+
"node": ">=18.x"
|
| 784 |
+
}
|
| 785 |
+
},
|
| 786 |
+
"node_modules/@jridgewell/gen-mapping": {
|
| 787 |
+
"version": "0.3.13",
|
| 788 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
| 789 |
+
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
| 790 |
+
"dev": true,
|
| 791 |
+
"license": "MIT",
|
| 792 |
+
"dependencies": {
|
| 793 |
+
"@jridgewell/sourcemap-codec": "^1.5.0",
|
| 794 |
+
"@jridgewell/trace-mapping": "^0.3.24"
|
| 795 |
+
}
|
| 796 |
+
},
|
| 797 |
+
"node_modules/@jridgewell/remapping": {
|
| 798 |
+
"version": "2.3.5",
|
| 799 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
| 800 |
+
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
| 801 |
+
"dev": true,
|
| 802 |
+
"license": "MIT",
|
| 803 |
+
"dependencies": {
|
| 804 |
+
"@jridgewell/gen-mapping": "^0.3.5",
|
| 805 |
+
"@jridgewell/trace-mapping": "^0.3.24"
|
| 806 |
+
}
|
| 807 |
+
},
|
| 808 |
+
"node_modules/@jridgewell/resolve-uri": {
|
| 809 |
+
"version": "3.1.2",
|
| 810 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
| 811 |
+
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
| 812 |
+
"dev": true,
|
| 813 |
+
"license": "MIT",
|
| 814 |
+
"engines": {
|
| 815 |
+
"node": ">=6.0.0"
|
| 816 |
+
}
|
| 817 |
+
},
|
| 818 |
+
"node_modules/@jridgewell/sourcemap-codec": {
|
| 819 |
+
"version": "1.5.5",
|
| 820 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
| 821 |
+
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
| 822 |
+
"dev": true,
|
| 823 |
+
"license": "MIT"
|
| 824 |
+
},
|
| 825 |
+
"node_modules/@jridgewell/trace-mapping": {
|
| 826 |
+
"version": "0.3.31",
|
| 827 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
| 828 |
+
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
| 829 |
+
"dev": true,
|
| 830 |
+
"license": "MIT",
|
| 831 |
+
"dependencies": {
|
| 832 |
+
"@jridgewell/resolve-uri": "^3.1.0",
|
| 833 |
+
"@jridgewell/sourcemap-codec": "^1.4.14"
|
| 834 |
+
}
|
| 835 |
+
},
|
| 836 |
+
"node_modules/@rolldown/pluginutils": {
|
| 837 |
+
"version": "1.0.0-rc.3",
|
| 838 |
+
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
|
| 839 |
+
"integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==",
|
| 840 |
+
"dev": true,
|
| 841 |
+
"license": "MIT"
|
| 842 |
+
},
|
| 843 |
+
"node_modules/@rollup/rollup-android-arm-eabi": {
|
| 844 |
+
"version": "4.59.0",
|
| 845 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
|
| 846 |
+
"integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
|
| 847 |
+
"cpu": [
|
| 848 |
+
"arm"
|
| 849 |
+
],
|
| 850 |
+
"dev": true,
|
| 851 |
+
"license": "MIT",
|
| 852 |
+
"optional": true,
|
| 853 |
+
"os": [
|
| 854 |
+
"android"
|
| 855 |
+
]
|
| 856 |
+
},
|
| 857 |
+
"node_modules/@rollup/rollup-android-arm64": {
|
| 858 |
+
"version": "4.59.0",
|
| 859 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
|
| 860 |
+
"integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
|
| 861 |
+
"cpu": [
|
| 862 |
+
"arm64"
|
| 863 |
+
],
|
| 864 |
+
"dev": true,
|
| 865 |
+
"license": "MIT",
|
| 866 |
+
"optional": true,
|
| 867 |
+
"os": [
|
| 868 |
+
"android"
|
| 869 |
+
]
|
| 870 |
+
},
|
| 871 |
+
"node_modules/@rollup/rollup-darwin-arm64": {
|
| 872 |
+
"version": "4.59.0",
|
| 873 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
|
| 874 |
+
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
|
| 875 |
+
"cpu": [
|
| 876 |
+
"arm64"
|
| 877 |
+
],
|
| 878 |
+
"dev": true,
|
| 879 |
+
"license": "MIT",
|
| 880 |
+
"optional": true,
|
| 881 |
+
"os": [
|
| 882 |
+
"darwin"
|
| 883 |
+
]
|
| 884 |
+
},
|
| 885 |
+
"node_modules/@rollup/rollup-darwin-x64": {
|
| 886 |
+
"version": "4.59.0",
|
| 887 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
|
| 888 |
+
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
|
| 889 |
+
"cpu": [
|
| 890 |
+
"x64"
|
| 891 |
+
],
|
| 892 |
+
"dev": true,
|
| 893 |
+
"license": "MIT",
|
| 894 |
+
"optional": true,
|
| 895 |
+
"os": [
|
| 896 |
+
"darwin"
|
| 897 |
+
]
|
| 898 |
+
},
|
| 899 |
+
"node_modules/@rollup/rollup-freebsd-arm64": {
|
| 900 |
+
"version": "4.59.0",
|
| 901 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
|
| 902 |
+
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
|
| 903 |
+
"cpu": [
|
| 904 |
+
"arm64"
|
| 905 |
+
],
|
| 906 |
+
"dev": true,
|
| 907 |
+
"license": "MIT",
|
| 908 |
+
"optional": true,
|
| 909 |
+
"os": [
|
| 910 |
+
"freebsd"
|
| 911 |
+
]
|
| 912 |
+
},
|
| 913 |
+
"node_modules/@rollup/rollup-freebsd-x64": {
|
| 914 |
+
"version": "4.59.0",
|
| 915 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
|
| 916 |
+
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
|
| 917 |
+
"cpu": [
|
| 918 |
+
"x64"
|
| 919 |
+
],
|
| 920 |
+
"dev": true,
|
| 921 |
+
"license": "MIT",
|
| 922 |
+
"optional": true,
|
| 923 |
+
"os": [
|
| 924 |
+
"freebsd"
|
| 925 |
+
]
|
| 926 |
+
},
|
| 927 |
+
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
| 928 |
+
"version": "4.59.0",
|
| 929 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
|
| 930 |
+
"integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
|
| 931 |
+
"cpu": [
|
| 932 |
+
"arm"
|
| 933 |
+
],
|
| 934 |
+
"dev": true,
|
| 935 |
+
"license": "MIT",
|
| 936 |
+
"optional": true,
|
| 937 |
+
"os": [
|
| 938 |
+
"linux"
|
| 939 |
+
]
|
| 940 |
+
},
|
| 941 |
+
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
| 942 |
+
"version": "4.59.0",
|
| 943 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
|
| 944 |
+
"integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
|
| 945 |
+
"cpu": [
|
| 946 |
+
"arm"
|
| 947 |
+
],
|
| 948 |
+
"dev": true,
|
| 949 |
+
"license": "MIT",
|
| 950 |
+
"optional": true,
|
| 951 |
+
"os": [
|
| 952 |
+
"linux"
|
| 953 |
+
]
|
| 954 |
+
},
|
| 955 |
+
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
| 956 |
+
"version": "4.59.0",
|
| 957 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
|
| 958 |
+
"integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
|
| 959 |
+
"cpu": [
|
| 960 |
+
"arm64"
|
| 961 |
+
],
|
| 962 |
+
"dev": true,
|
| 963 |
+
"license": "MIT",
|
| 964 |
+
"optional": true,
|
| 965 |
+
"os": [
|
| 966 |
+
"linux"
|
| 967 |
+
]
|
| 968 |
+
},
|
| 969 |
+
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
| 970 |
+
"version": "4.59.0",
|
| 971 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
|
| 972 |
+
"integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
|
| 973 |
+
"cpu": [
|
| 974 |
+
"arm64"
|
| 975 |
+
],
|
| 976 |
+
"dev": true,
|
| 977 |
+
"license": "MIT",
|
| 978 |
+
"optional": true,
|
| 979 |
+
"os": [
|
| 980 |
+
"linux"
|
| 981 |
+
]
|
| 982 |
+
},
|
| 983 |
+
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
| 984 |
+
"version": "4.59.0",
|
| 985 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
|
| 986 |
+
"integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
|
| 987 |
+
"cpu": [
|
| 988 |
+
"loong64"
|
| 989 |
+
],
|
| 990 |
+
"dev": true,
|
| 991 |
+
"license": "MIT",
|
| 992 |
+
"optional": true,
|
| 993 |
+
"os": [
|
| 994 |
+
"linux"
|
| 995 |
+
]
|
| 996 |
+
},
|
| 997 |
+
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
| 998 |
+
"version": "4.59.0",
|
| 999 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
|
| 1000 |
+
"integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
|
| 1001 |
+
"cpu": [
|
| 1002 |
+
"loong64"
|
| 1003 |
+
],
|
| 1004 |
+
"dev": true,
|
| 1005 |
+
"license": "MIT",
|
| 1006 |
+
"optional": true,
|
| 1007 |
+
"os": [
|
| 1008 |
+
"linux"
|
| 1009 |
+
]
|
| 1010 |
+
},
|
| 1011 |
+
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
| 1012 |
+
"version": "4.59.0",
|
| 1013 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
|
| 1014 |
+
"integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
|
| 1015 |
+
"cpu": [
|
| 1016 |
+
"ppc64"
|
| 1017 |
+
],
|
| 1018 |
+
"dev": true,
|
| 1019 |
+
"license": "MIT",
|
| 1020 |
+
"optional": true,
|
| 1021 |
+
"os": [
|
| 1022 |
+
"linux"
|
| 1023 |
+
]
|
| 1024 |
+
},
|
| 1025 |
+
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
| 1026 |
+
"version": "4.59.0",
|
| 1027 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
|
| 1028 |
+
"integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
|
| 1029 |
+
"cpu": [
|
| 1030 |
+
"ppc64"
|
| 1031 |
+
],
|
| 1032 |
+
"dev": true,
|
| 1033 |
+
"license": "MIT",
|
| 1034 |
+
"optional": true,
|
| 1035 |
+
"os": [
|
| 1036 |
+
"linux"
|
| 1037 |
+
]
|
| 1038 |
+
},
|
| 1039 |
+
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
| 1040 |
+
"version": "4.59.0",
|
| 1041 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
|
| 1042 |
+
"integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
|
| 1043 |
+
"cpu": [
|
| 1044 |
+
"riscv64"
|
| 1045 |
+
],
|
| 1046 |
+
"dev": true,
|
| 1047 |
+
"license": "MIT",
|
| 1048 |
+
"optional": true,
|
| 1049 |
+
"os": [
|
| 1050 |
+
"linux"
|
| 1051 |
+
]
|
| 1052 |
+
},
|
| 1053 |
+
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
| 1054 |
+
"version": "4.59.0",
|
| 1055 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
|
| 1056 |
+
"integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
|
| 1057 |
+
"cpu": [
|
| 1058 |
+
"riscv64"
|
| 1059 |
+
],
|
| 1060 |
+
"dev": true,
|
| 1061 |
+
"license": "MIT",
|
| 1062 |
+
"optional": true,
|
| 1063 |
+
"os": [
|
| 1064 |
+
"linux"
|
| 1065 |
+
]
|
| 1066 |
+
},
|
| 1067 |
+
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
| 1068 |
+
"version": "4.59.0",
|
| 1069 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
|
| 1070 |
+
"integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
|
| 1071 |
+
"cpu": [
|
| 1072 |
+
"s390x"
|
| 1073 |
+
],
|
| 1074 |
+
"dev": true,
|
| 1075 |
+
"license": "MIT",
|
| 1076 |
+
"optional": true,
|
| 1077 |
+
"os": [
|
| 1078 |
+
"linux"
|
| 1079 |
+
]
|
| 1080 |
+
},
|
| 1081 |
+
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
| 1082 |
+
"version": "4.59.0",
|
| 1083 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
|
| 1084 |
+
"integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
|
| 1085 |
+
"cpu": [
|
| 1086 |
+
"x64"
|
| 1087 |
+
],
|
| 1088 |
+
"dev": true,
|
| 1089 |
+
"license": "MIT",
|
| 1090 |
+
"optional": true,
|
| 1091 |
+
"os": [
|
| 1092 |
+
"linux"
|
| 1093 |
+
]
|
| 1094 |
+
},
|
| 1095 |
+
"node_modules/@rollup/rollup-linux-x64-musl": {
|
| 1096 |
+
"version": "4.59.0",
|
| 1097 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
|
| 1098 |
+
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
|
| 1099 |
+
"cpu": [
|
| 1100 |
+
"x64"
|
| 1101 |
+
],
|
| 1102 |
+
"dev": true,
|
| 1103 |
+
"license": "MIT",
|
| 1104 |
+
"optional": true,
|
| 1105 |
+
"os": [
|
| 1106 |
+
"linux"
|
| 1107 |
+
]
|
| 1108 |
+
},
|
| 1109 |
+
"node_modules/@rollup/rollup-openbsd-x64": {
|
| 1110 |
+
"version": "4.59.0",
|
| 1111 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
|
| 1112 |
+
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
|
| 1113 |
+
"cpu": [
|
| 1114 |
+
"x64"
|
| 1115 |
+
],
|
| 1116 |
+
"dev": true,
|
| 1117 |
+
"license": "MIT",
|
| 1118 |
+
"optional": true,
|
| 1119 |
+
"os": [
|
| 1120 |
+
"openbsd"
|
| 1121 |
+
]
|
| 1122 |
+
},
|
| 1123 |
+
"node_modules/@rollup/rollup-openharmony-arm64": {
|
| 1124 |
+
"version": "4.59.0",
|
| 1125 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
|
| 1126 |
+
"integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
|
| 1127 |
+
"cpu": [
|
| 1128 |
+
"arm64"
|
| 1129 |
+
],
|
| 1130 |
+
"dev": true,
|
| 1131 |
+
"license": "MIT",
|
| 1132 |
+
"optional": true,
|
| 1133 |
+
"os": [
|
| 1134 |
+
"openharmony"
|
| 1135 |
+
]
|
| 1136 |
+
},
|
| 1137 |
+
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
| 1138 |
+
"version": "4.59.0",
|
| 1139 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
|
| 1140 |
+
"integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
|
| 1141 |
+
"cpu": [
|
| 1142 |
+
"arm64"
|
| 1143 |
+
],
|
| 1144 |
+
"dev": true,
|
| 1145 |
+
"license": "MIT",
|
| 1146 |
+
"optional": true,
|
| 1147 |
+
"os": [
|
| 1148 |
+
"win32"
|
| 1149 |
+
]
|
| 1150 |
+
},
|
| 1151 |
+
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
| 1152 |
+
"version": "4.59.0",
|
| 1153 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
|
| 1154 |
+
"integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
|
| 1155 |
+
"cpu": [
|
| 1156 |
+
"ia32"
|
| 1157 |
+
],
|
| 1158 |
+
"dev": true,
|
| 1159 |
+
"license": "MIT",
|
| 1160 |
+
"optional": true,
|
| 1161 |
+
"os": [
|
| 1162 |
+
"win32"
|
| 1163 |
+
]
|
| 1164 |
+
},
|
| 1165 |
+
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
| 1166 |
+
"version": "4.59.0",
|
| 1167 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
|
| 1168 |
+
"integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
|
| 1169 |
+
"cpu": [
|
| 1170 |
+
"x64"
|
| 1171 |
+
],
|
| 1172 |
+
"dev": true,
|
| 1173 |
+
"license": "MIT",
|
| 1174 |
+
"optional": true,
|
| 1175 |
+
"os": [
|
| 1176 |
+
"win32"
|
| 1177 |
+
]
|
| 1178 |
+
},
|
| 1179 |
+
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
| 1180 |
+
"version": "4.59.0",
|
| 1181 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
|
| 1182 |
+
"integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
|
| 1183 |
+
"cpu": [
|
| 1184 |
+
"x64"
|
| 1185 |
+
],
|
| 1186 |
+
"dev": true,
|
| 1187 |
+
"license": "MIT",
|
| 1188 |
+
"optional": true,
|
| 1189 |
+
"os": [
|
| 1190 |
+
"win32"
|
| 1191 |
+
]
|
| 1192 |
+
},
|
| 1193 |
+
"node_modules/@standard-schema/spec": {
|
| 1194 |
+
"version": "1.1.0",
|
| 1195 |
+
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
| 1196 |
+
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
| 1197 |
+
"dev": true,
|
| 1198 |
+
"license": "MIT"
|
| 1199 |
+
},
|
| 1200 |
+
"node_modules/@tailwindcss/node": {
|
| 1201 |
+
"version": "4.2.1",
|
| 1202 |
+
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz",
|
| 1203 |
+
"integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==",
|
| 1204 |
+
"dev": true,
|
| 1205 |
+
"license": "MIT",
|
| 1206 |
+
"dependencies": {
|
| 1207 |
+
"@jridgewell/remapping": "^2.3.5",
|
| 1208 |
+
"enhanced-resolve": "^5.19.0",
|
| 1209 |
+
"jiti": "^2.6.1",
|
| 1210 |
+
"lightningcss": "1.31.1",
|
| 1211 |
+
"magic-string": "^0.30.21",
|
| 1212 |
+
"source-map-js": "^1.2.1",
|
| 1213 |
+
"tailwindcss": "4.2.1"
|
| 1214 |
+
}
|
| 1215 |
+
},
|
| 1216 |
+
"node_modules/@tailwindcss/oxide": {
|
| 1217 |
+
"version": "4.2.1",
|
| 1218 |
+
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz",
|
| 1219 |
+
"integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==",
|
| 1220 |
+
"dev": true,
|
| 1221 |
+
"license": "MIT",
|
| 1222 |
+
"engines": {
|
| 1223 |
+
"node": ">= 20"
|
| 1224 |
+
},
|
| 1225 |
+
"optionalDependencies": {
|
| 1226 |
+
"@tailwindcss/oxide-android-arm64": "4.2.1",
|
| 1227 |
+
"@tailwindcss/oxide-darwin-arm64": "4.2.1",
|
| 1228 |
+
"@tailwindcss/oxide-darwin-x64": "4.2.1",
|
| 1229 |
+
"@tailwindcss/oxide-freebsd-x64": "4.2.1",
|
| 1230 |
+
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1",
|
| 1231 |
+
"@tailwindcss/oxide-linux-arm64-gnu": "4.2.1",
|
| 1232 |
+
"@tailwindcss/oxide-linux-arm64-musl": "4.2.1",
|
| 1233 |
+
"@tailwindcss/oxide-linux-x64-gnu": "4.2.1",
|
| 1234 |
+
"@tailwindcss/oxide-linux-x64-musl": "4.2.1",
|
| 1235 |
+
"@tailwindcss/oxide-wasm32-wasi": "4.2.1",
|
| 1236 |
+
"@tailwindcss/oxide-win32-arm64-msvc": "4.2.1",
|
| 1237 |
+
"@tailwindcss/oxide-win32-x64-msvc": "4.2.1"
|
| 1238 |
+
}
|
| 1239 |
+
},
|
| 1240 |
+
"node_modules/@tailwindcss/oxide-android-arm64": {
|
| 1241 |
+
"version": "4.2.1",
|
| 1242 |
+
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz",
|
| 1243 |
+
"integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==",
|
| 1244 |
+
"cpu": [
|
| 1245 |
+
"arm64"
|
| 1246 |
+
],
|
| 1247 |
+
"dev": true,
|
| 1248 |
+
"license": "MIT",
|
| 1249 |
+
"optional": true,
|
| 1250 |
+
"os": [
|
| 1251 |
+
"android"
|
| 1252 |
+
],
|
| 1253 |
+
"engines": {
|
| 1254 |
+
"node": ">= 20"
|
| 1255 |
+
}
|
| 1256 |
+
},
|
| 1257 |
+
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
| 1258 |
+
"version": "4.2.1",
|
| 1259 |
+
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz",
|
| 1260 |
+
"integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==",
|
| 1261 |
+
"cpu": [
|
| 1262 |
+
"arm64"
|
| 1263 |
+
],
|
| 1264 |
+
"dev": true,
|
| 1265 |
+
"license": "MIT",
|
| 1266 |
+
"optional": true,
|
| 1267 |
+
"os": [
|
| 1268 |
+
"darwin"
|
| 1269 |
+
],
|
| 1270 |
+
"engines": {
|
| 1271 |
+
"node": ">= 20"
|
| 1272 |
+
}
|
| 1273 |
+
},
|
| 1274 |
+
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
| 1275 |
+
"version": "4.2.1",
|
| 1276 |
+
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz",
|
| 1277 |
+
"integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==",
|
| 1278 |
+
"cpu": [
|
| 1279 |
+
"x64"
|
| 1280 |
+
],
|
| 1281 |
+
"dev": true,
|
| 1282 |
+
"license": "MIT",
|
| 1283 |
+
"optional": true,
|
| 1284 |
+
"os": [
|
| 1285 |
+
"darwin"
|
| 1286 |
+
],
|
| 1287 |
+
"engines": {
|
| 1288 |
+
"node": ">= 20"
|
| 1289 |
+
}
|
| 1290 |
+
},
|
| 1291 |
+
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
| 1292 |
+
"version": "4.2.1",
|
| 1293 |
+
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz",
|
| 1294 |
+
"integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==",
|
| 1295 |
+
"cpu": [
|
| 1296 |
+
"x64"
|
| 1297 |
+
],
|
| 1298 |
+
"dev": true,
|
| 1299 |
+
"license": "MIT",
|
| 1300 |
+
"optional": true,
|
| 1301 |
+
"os": [
|
| 1302 |
+
"freebsd"
|
| 1303 |
+
],
|
| 1304 |
+
"engines": {
|
| 1305 |
+
"node": ">= 20"
|
| 1306 |
+
}
|
| 1307 |
+
},
|
| 1308 |
+
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
| 1309 |
+
"version": "4.2.1",
|
| 1310 |
+
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz",
|
| 1311 |
+
"integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==",
|
| 1312 |
+
"cpu": [
|
| 1313 |
+
"arm"
|
| 1314 |
+
],
|
| 1315 |
+
"dev": true,
|
| 1316 |
+
"license": "MIT",
|
| 1317 |
+
"optional": true,
|
| 1318 |
+
"os": [
|
| 1319 |
+
"linux"
|
| 1320 |
+
],
|
| 1321 |
+
"engines": {
|
| 1322 |
+
"node": ">= 20"
|
| 1323 |
+
}
|
| 1324 |
+
},
|
| 1325 |
+
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
| 1326 |
+
"version": "4.2.1",
|
| 1327 |
+
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz",
|
| 1328 |
+
"integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==",
|
| 1329 |
+
"cpu": [
|
| 1330 |
+
"arm64"
|
| 1331 |
+
],
|
| 1332 |
+
"dev": true,
|
| 1333 |
+
"license": "MIT",
|
| 1334 |
+
"optional": true,
|
| 1335 |
+
"os": [
|
| 1336 |
+
"linux"
|
| 1337 |
+
],
|
| 1338 |
+
"engines": {
|
| 1339 |
+
"node": ">= 20"
|
| 1340 |
+
}
|
| 1341 |
+
},
|
| 1342 |
+
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
| 1343 |
+
"version": "4.2.1",
|
| 1344 |
+
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz",
|
| 1345 |
+
"integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==",
|
| 1346 |
+
"cpu": [
|
| 1347 |
+
"arm64"
|
| 1348 |
+
],
|
| 1349 |
+
"dev": true,
|
| 1350 |
+
"license": "MIT",
|
| 1351 |
+
"optional": true,
|
| 1352 |
+
"os": [
|
| 1353 |
+
"linux"
|
| 1354 |
+
],
|
| 1355 |
+
"engines": {
|
| 1356 |
+
"node": ">= 20"
|
| 1357 |
+
}
|
| 1358 |
+
},
|
| 1359 |
+
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
| 1360 |
+
"version": "4.2.1",
|
| 1361 |
+
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz",
|
| 1362 |
+
"integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==",
|
| 1363 |
+
"cpu": [
|
| 1364 |
+
"x64"
|
| 1365 |
+
],
|
| 1366 |
+
"dev": true,
|
| 1367 |
+
"license": "MIT",
|
| 1368 |
+
"optional": true,
|
| 1369 |
+
"os": [
|
| 1370 |
+
"linux"
|
| 1371 |
+
],
|
| 1372 |
+
"engines": {
|
| 1373 |
+
"node": ">= 20"
|
| 1374 |
+
}
|
| 1375 |
+
},
|
| 1376 |
+
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
| 1377 |
+
"version": "4.2.1",
|
| 1378 |
+
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz",
|
| 1379 |
+
"integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==",
|
| 1380 |
+
"cpu": [
|
| 1381 |
+
"x64"
|
| 1382 |
+
],
|
| 1383 |
+
"dev": true,
|
| 1384 |
+
"license": "MIT",
|
| 1385 |
+
"optional": true,
|
| 1386 |
+
"os": [
|
| 1387 |
+
"linux"
|
| 1388 |
+
],
|
| 1389 |
+
"engines": {
|
| 1390 |
+
"node": ">= 20"
|
| 1391 |
+
}
|
| 1392 |
+
},
|
| 1393 |
+
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
| 1394 |
+
"version": "4.2.1",
|
| 1395 |
+
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz",
|
| 1396 |
+
"integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==",
|
| 1397 |
+
"bundleDependencies": [
|
| 1398 |
+
"@napi-rs/wasm-runtime",
|
| 1399 |
+
"@emnapi/core",
|
| 1400 |
+
"@emnapi/runtime",
|
| 1401 |
+
"@tybys/wasm-util",
|
| 1402 |
+
"@emnapi/wasi-threads",
|
| 1403 |
+
"tslib"
|
| 1404 |
+
],
|
| 1405 |
+
"cpu": [
|
| 1406 |
+
"wasm32"
|
| 1407 |
+
],
|
| 1408 |
+
"dev": true,
|
| 1409 |
+
"license": "MIT",
|
| 1410 |
+
"optional": true,
|
| 1411 |
+
"dependencies": {
|
| 1412 |
+
"@emnapi/core": "^1.8.1",
|
| 1413 |
+
"@emnapi/runtime": "^1.8.1",
|
| 1414 |
+
"@emnapi/wasi-threads": "^1.1.0",
|
| 1415 |
+
"@napi-rs/wasm-runtime": "^1.1.1",
|
| 1416 |
+
"@tybys/wasm-util": "^0.10.1",
|
| 1417 |
+
"tslib": "^2.8.1"
|
| 1418 |
+
},
|
| 1419 |
+
"engines": {
|
| 1420 |
+
"node": ">=14.0.0"
|
| 1421 |
+
}
|
| 1422 |
+
},
|
| 1423 |
+
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
| 1424 |
+
"version": "4.2.1",
|
| 1425 |
+
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz",
|
| 1426 |
+
"integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==",
|
| 1427 |
+
"cpu": [
|
| 1428 |
+
"arm64"
|
| 1429 |
+
],
|
| 1430 |
+
"dev": true,
|
| 1431 |
+
"license": "MIT",
|
| 1432 |
+
"optional": true,
|
| 1433 |
+
"os": [
|
| 1434 |
+
"win32"
|
| 1435 |
+
],
|
| 1436 |
+
"engines": {
|
| 1437 |
+
"node": ">= 20"
|
| 1438 |
+
}
|
| 1439 |
+
},
|
| 1440 |
+
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
| 1441 |
+
"version": "4.2.1",
|
| 1442 |
+
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz",
|
| 1443 |
+
"integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==",
|
| 1444 |
+
"cpu": [
|
| 1445 |
+
"x64"
|
| 1446 |
+
],
|
| 1447 |
+
"dev": true,
|
| 1448 |
+
"license": "MIT",
|
| 1449 |
+
"optional": true,
|
| 1450 |
+
"os": [
|
| 1451 |
+
"win32"
|
| 1452 |
+
],
|
| 1453 |
+
"engines": {
|
| 1454 |
+
"node": ">= 20"
|
| 1455 |
+
}
|
| 1456 |
+
},
|
| 1457 |
+
"node_modules/@tailwindcss/vite": {
|
| 1458 |
+
"version": "4.2.1",
|
| 1459 |
+
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz",
|
| 1460 |
+
"integrity": "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==",
|
| 1461 |
+
"dev": true,
|
| 1462 |
+
"license": "MIT",
|
| 1463 |
+
"dependencies": {
|
| 1464 |
+
"@tailwindcss/node": "4.2.1",
|
| 1465 |
+
"@tailwindcss/oxide": "4.2.1",
|
| 1466 |
+
"tailwindcss": "4.2.1"
|
| 1467 |
+
},
|
| 1468 |
+
"peerDependencies": {
|
| 1469 |
+
"vite": "^5.2.0 || ^6 || ^7"
|
| 1470 |
+
}
|
| 1471 |
+
},
|
| 1472 |
+
"node_modules/@types/babel__core": {
|
| 1473 |
+
"version": "7.20.5",
|
| 1474 |
+
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
| 1475 |
+
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
|
| 1476 |
+
"dev": true,
|
| 1477 |
+
"license": "MIT",
|
| 1478 |
+
"dependencies": {
|
| 1479 |
+
"@babel/parser": "^7.20.7",
|
| 1480 |
+
"@babel/types": "^7.20.7",
|
| 1481 |
+
"@types/babel__generator": "*",
|
| 1482 |
+
"@types/babel__template": "*",
|
| 1483 |
+
"@types/babel__traverse": "*"
|
| 1484 |
+
}
|
| 1485 |
+
},
|
| 1486 |
+
"node_modules/@types/babel__generator": {
|
| 1487 |
+
"version": "7.27.0",
|
| 1488 |
+
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
|
| 1489 |
+
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
|
| 1490 |
+
"dev": true,
|
| 1491 |
+
"license": "MIT",
|
| 1492 |
+
"dependencies": {
|
| 1493 |
+
"@babel/types": "^7.0.0"
|
| 1494 |
+
}
|
| 1495 |
+
},
|
| 1496 |
+
"node_modules/@types/babel__template": {
|
| 1497 |
+
"version": "7.4.4",
|
| 1498 |
+
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
|
| 1499 |
+
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
|
| 1500 |
+
"dev": true,
|
| 1501 |
+
"license": "MIT",
|
| 1502 |
+
"dependencies": {
|
| 1503 |
+
"@babel/parser": "^7.1.0",
|
| 1504 |
+
"@babel/types": "^7.0.0"
|
| 1505 |
+
}
|
| 1506 |
+
},
|
| 1507 |
+
"node_modules/@types/babel__traverse": {
|
| 1508 |
+
"version": "7.28.0",
|
| 1509 |
+
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
|
| 1510 |
+
"integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
|
| 1511 |
+
"dev": true,
|
| 1512 |
+
"license": "MIT",
|
| 1513 |
+
"dependencies": {
|
| 1514 |
+
"@babel/types": "^7.28.2"
|
| 1515 |
+
}
|
| 1516 |
+
},
|
| 1517 |
+
"node_modules/@types/chai": {
|
| 1518 |
+
"version": "5.2.3",
|
| 1519 |
+
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
| 1520 |
+
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
|
| 1521 |
+
"dev": true,
|
| 1522 |
+
"license": "MIT",
|
| 1523 |
+
"dependencies": {
|
| 1524 |
+
"@types/deep-eql": "*",
|
| 1525 |
+
"assertion-error": "^2.0.1"
|
| 1526 |
+
}
|
| 1527 |
+
},
|
| 1528 |
+
"node_modules/@types/deep-eql": {
|
| 1529 |
+
"version": "4.0.2",
|
| 1530 |
+
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
| 1531 |
+
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
| 1532 |
+
"dev": true,
|
| 1533 |
+
"license": "MIT"
|
| 1534 |
+
},
|
| 1535 |
+
"node_modules/@types/estree": {
|
| 1536 |
+
"version": "1.0.8",
|
| 1537 |
+
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
| 1538 |
+
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
| 1539 |
+
"dev": true,
|
| 1540 |
+
"license": "MIT"
|
| 1541 |
+
},
|
| 1542 |
+
"node_modules/@types/node": {
|
| 1543 |
+
"version": "25.3.0",
|
| 1544 |
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz",
|
| 1545 |
+
"integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==",
|
| 1546 |
+
"dev": true,
|
| 1547 |
+
"license": "MIT",
|
| 1548 |
+
"dependencies": {
|
| 1549 |
+
"undici-types": "~7.18.0"
|
| 1550 |
+
}
|
| 1551 |
+
},
|
| 1552 |
+
"node_modules/@types/react": {
|
| 1553 |
+
"version": "19.2.14",
|
| 1554 |
+
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
| 1555 |
+
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
| 1556 |
+
"dev": true,
|
| 1557 |
+
"license": "MIT",
|
| 1558 |
+
"dependencies": {
|
| 1559 |
+
"csstype": "^3.2.2"
|
| 1560 |
+
}
|
| 1561 |
+
},
|
| 1562 |
+
"node_modules/@types/react-dom": {
|
| 1563 |
+
"version": "19.2.3",
|
| 1564 |
+
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
| 1565 |
+
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
| 1566 |
+
"dev": true,
|
| 1567 |
+
"license": "MIT",
|
| 1568 |
+
"peerDependencies": {
|
| 1569 |
+
"@types/react": "^19.2.0"
|
| 1570 |
+
}
|
| 1571 |
+
},
|
| 1572 |
+
"node_modules/@vitejs/plugin-react": {
|
| 1573 |
+
"version": "5.1.4",
|
| 1574 |
+
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz",
|
| 1575 |
+
"integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==",
|
| 1576 |
+
"dev": true,
|
| 1577 |
+
"license": "MIT",
|
| 1578 |
+
"dependencies": {
|
| 1579 |
+
"@babel/core": "^7.29.0",
|
| 1580 |
+
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
|
| 1581 |
+
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
|
| 1582 |
+
"@rolldown/pluginutils": "1.0.0-rc.3",
|
| 1583 |
+
"@types/babel__core": "^7.20.5",
|
| 1584 |
+
"react-refresh": "^0.18.0"
|
| 1585 |
+
},
|
| 1586 |
+
"engines": {
|
| 1587 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 1588 |
+
},
|
| 1589 |
+
"peerDependencies": {
|
| 1590 |
+
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
| 1591 |
+
}
|
| 1592 |
+
},
|
| 1593 |
+
"node_modules/@vitest/expect": {
|
| 1594 |
+
"version": "4.0.18",
|
| 1595 |
+
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
|
| 1596 |
+
"integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==",
|
| 1597 |
+
"dev": true,
|
| 1598 |
+
"license": "MIT",
|
| 1599 |
+
"dependencies": {
|
| 1600 |
+
"@standard-schema/spec": "^1.0.0",
|
| 1601 |
+
"@types/chai": "^5.2.2",
|
| 1602 |
+
"@vitest/spy": "4.0.18",
|
| 1603 |
+
"@vitest/utils": "4.0.18",
|
| 1604 |
+
"chai": "^6.2.1",
|
| 1605 |
+
"tinyrainbow": "^3.0.3"
|
| 1606 |
+
},
|
| 1607 |
+
"funding": {
|
| 1608 |
+
"url": "https://opencollective.com/vitest"
|
| 1609 |
+
}
|
| 1610 |
+
},
|
| 1611 |
+
"node_modules/@vitest/mocker": {
|
| 1612 |
+
"version": "4.0.18",
|
| 1613 |
+
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz",
|
| 1614 |
+
"integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==",
|
| 1615 |
+
"dev": true,
|
| 1616 |
+
"license": "MIT",
|
| 1617 |
+
"dependencies": {
|
| 1618 |
+
"@vitest/spy": "4.0.18",
|
| 1619 |
+
"estree-walker": "^3.0.3",
|
| 1620 |
+
"magic-string": "^0.30.21"
|
| 1621 |
+
},
|
| 1622 |
+
"funding": {
|
| 1623 |
+
"url": "https://opencollective.com/vitest"
|
| 1624 |
+
},
|
| 1625 |
+
"peerDependencies": {
|
| 1626 |
+
"msw": "^2.4.9",
|
| 1627 |
+
"vite": "^6.0.0 || ^7.0.0-0"
|
| 1628 |
+
},
|
| 1629 |
+
"peerDependenciesMeta": {
|
| 1630 |
+
"msw": {
|
| 1631 |
+
"optional": true
|
| 1632 |
+
},
|
| 1633 |
+
"vite": {
|
| 1634 |
+
"optional": true
|
| 1635 |
+
}
|
| 1636 |
+
}
|
| 1637 |
+
},
|
| 1638 |
+
"node_modules/@vitest/pretty-format": {
|
| 1639 |
+
"version": "4.0.18",
|
| 1640 |
+
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz",
|
| 1641 |
+
"integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==",
|
| 1642 |
+
"dev": true,
|
| 1643 |
+
"license": "MIT",
|
| 1644 |
+
"dependencies": {
|
| 1645 |
+
"tinyrainbow": "^3.0.3"
|
| 1646 |
+
},
|
| 1647 |
+
"funding": {
|
| 1648 |
+
"url": "https://opencollective.com/vitest"
|
| 1649 |
+
}
|
| 1650 |
+
},
|
| 1651 |
+
"node_modules/@vitest/runner": {
|
| 1652 |
+
"version": "4.0.18",
|
| 1653 |
+
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz",
|
| 1654 |
+
"integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==",
|
| 1655 |
+
"dev": true,
|
| 1656 |
+
"license": "MIT",
|
| 1657 |
+
"dependencies": {
|
| 1658 |
+
"@vitest/utils": "4.0.18",
|
| 1659 |
+
"pathe": "^2.0.3"
|
| 1660 |
+
},
|
| 1661 |
+
"funding": {
|
| 1662 |
+
"url": "https://opencollective.com/vitest"
|
| 1663 |
+
}
|
| 1664 |
+
},
|
| 1665 |
+
"node_modules/@vitest/snapshot": {
|
| 1666 |
+
"version": "4.0.18",
|
| 1667 |
+
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz",
|
| 1668 |
+
"integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==",
|
| 1669 |
+
"dev": true,
|
| 1670 |
+
"license": "MIT",
|
| 1671 |
+
"dependencies": {
|
| 1672 |
+
"@vitest/pretty-format": "4.0.18",
|
| 1673 |
+
"magic-string": "^0.30.21",
|
| 1674 |
+
"pathe": "^2.0.3"
|
| 1675 |
+
},
|
| 1676 |
+
"funding": {
|
| 1677 |
+
"url": "https://opencollective.com/vitest"
|
| 1678 |
+
}
|
| 1679 |
+
},
|
| 1680 |
+
"node_modules/@vitest/spy": {
|
| 1681 |
+
"version": "4.0.18",
|
| 1682 |
+
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz",
|
| 1683 |
+
"integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==",
|
| 1684 |
+
"dev": true,
|
| 1685 |
+
"license": "MIT",
|
| 1686 |
+
"funding": {
|
| 1687 |
+
"url": "https://opencollective.com/vitest"
|
| 1688 |
+
}
|
| 1689 |
+
},
|
| 1690 |
+
"node_modules/@vitest/utils": {
|
| 1691 |
+
"version": "4.0.18",
|
| 1692 |
+
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz",
|
| 1693 |
+
"integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==",
|
| 1694 |
+
"dev": true,
|
| 1695 |
+
"license": "MIT",
|
| 1696 |
+
"dependencies": {
|
| 1697 |
+
"@vitest/pretty-format": "4.0.18",
|
| 1698 |
+
"tinyrainbow": "^3.0.3"
|
| 1699 |
+
},
|
| 1700 |
+
"funding": {
|
| 1701 |
+
"url": "https://opencollective.com/vitest"
|
| 1702 |
+
}
|
| 1703 |
+
},
|
| 1704 |
+
"node_modules/assertion-error": {
|
| 1705 |
+
"version": "2.0.1",
|
| 1706 |
+
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
| 1707 |
+
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
| 1708 |
+
"dev": true,
|
| 1709 |
+
"license": "MIT",
|
| 1710 |
+
"engines": {
|
| 1711 |
+
"node": ">=12"
|
| 1712 |
+
}
|
| 1713 |
+
},
|
| 1714 |
+
"node_modules/baseline-browser-mapping": {
|
| 1715 |
+
"version": "2.10.0",
|
| 1716 |
+
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
|
| 1717 |
+
"integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==",
|
| 1718 |
+
"dev": true,
|
| 1719 |
+
"license": "Apache-2.0",
|
| 1720 |
+
"bin": {
|
| 1721 |
+
"baseline-browser-mapping": "dist/cli.cjs"
|
| 1722 |
+
},
|
| 1723 |
+
"engines": {
|
| 1724 |
+
"node": ">=6.0.0"
|
| 1725 |
+
}
|
| 1726 |
+
},
|
| 1727 |
+
"node_modules/browserslist": {
|
| 1728 |
+
"version": "4.28.1",
|
| 1729 |
+
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
|
| 1730 |
+
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
|
| 1731 |
+
"dev": true,
|
| 1732 |
+
"funding": [
|
| 1733 |
+
{
|
| 1734 |
+
"type": "opencollective",
|
| 1735 |
+
"url": "https://opencollective.com/browserslist"
|
| 1736 |
+
},
|
| 1737 |
+
{
|
| 1738 |
+
"type": "tidelift",
|
| 1739 |
+
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
| 1740 |
+
},
|
| 1741 |
+
{
|
| 1742 |
+
"type": "github",
|
| 1743 |
+
"url": "https://github.com/sponsors/ai"
|
| 1744 |
+
}
|
| 1745 |
+
],
|
| 1746 |
+
"license": "MIT",
|
| 1747 |
+
"dependencies": {
|
| 1748 |
+
"baseline-browser-mapping": "^2.9.0",
|
| 1749 |
+
"caniuse-lite": "^1.0.30001759",
|
| 1750 |
+
"electron-to-chromium": "^1.5.263",
|
| 1751 |
+
"node-releases": "^2.0.27",
|
| 1752 |
+
"update-browserslist-db": "^1.2.0"
|
| 1753 |
+
},
|
| 1754 |
+
"bin": {
|
| 1755 |
+
"browserslist": "cli.js"
|
| 1756 |
+
},
|
| 1757 |
+
"engines": {
|
| 1758 |
+
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
| 1759 |
+
}
|
| 1760 |
+
},
|
| 1761 |
+
"node_modules/caniuse-lite": {
|
| 1762 |
+
"version": "1.0.30001774",
|
| 1763 |
+
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz",
|
| 1764 |
+
"integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==",
|
| 1765 |
+
"dev": true,
|
| 1766 |
+
"funding": [
|
| 1767 |
+
{
|
| 1768 |
+
"type": "opencollective",
|
| 1769 |
+
"url": "https://opencollective.com/browserslist"
|
| 1770 |
+
},
|
| 1771 |
+
{
|
| 1772 |
+
"type": "tidelift",
|
| 1773 |
+
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
| 1774 |
+
},
|
| 1775 |
+
{
|
| 1776 |
+
"type": "github",
|
| 1777 |
+
"url": "https://github.com/sponsors/ai"
|
| 1778 |
+
}
|
| 1779 |
+
],
|
| 1780 |
+
"license": "CC-BY-4.0"
|
| 1781 |
+
},
|
| 1782 |
+
"node_modules/chai": {
|
| 1783 |
+
"version": "6.2.2",
|
| 1784 |
+
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
| 1785 |
+
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
|
| 1786 |
+
"dev": true,
|
| 1787 |
+
"license": "MIT",
|
| 1788 |
+
"engines": {
|
| 1789 |
+
"node": ">=18"
|
| 1790 |
+
}
|
| 1791 |
+
},
|
| 1792 |
+
"node_modules/convert-source-map": {
|
| 1793 |
+
"version": "2.0.0",
|
| 1794 |
+
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
| 1795 |
+
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
| 1796 |
+
"dev": true,
|
| 1797 |
+
"license": "MIT"
|
| 1798 |
+
},
|
| 1799 |
+
"node_modules/csstype": {
|
| 1800 |
+
"version": "3.2.3",
|
| 1801 |
+
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
| 1802 |
+
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
| 1803 |
+
"dev": true,
|
| 1804 |
+
"license": "MIT"
|
| 1805 |
+
},
|
| 1806 |
+
"node_modules/debug": {
|
| 1807 |
+
"version": "4.4.3",
|
| 1808 |
+
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
| 1809 |
+
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
| 1810 |
+
"dev": true,
|
| 1811 |
+
"license": "MIT",
|
| 1812 |
+
"dependencies": {
|
| 1813 |
+
"ms": "^2.1.3"
|
| 1814 |
+
},
|
| 1815 |
+
"engines": {
|
| 1816 |
+
"node": ">=6.0"
|
| 1817 |
+
},
|
| 1818 |
+
"peerDependenciesMeta": {
|
| 1819 |
+
"supports-color": {
|
| 1820 |
+
"optional": true
|
| 1821 |
+
}
|
| 1822 |
+
}
|
| 1823 |
+
},
|
| 1824 |
+
"node_modules/detect-libc": {
|
| 1825 |
+
"version": "2.1.2",
|
| 1826 |
+
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
| 1827 |
+
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
| 1828 |
+
"dev": true,
|
| 1829 |
+
"license": "Apache-2.0",
|
| 1830 |
+
"engines": {
|
| 1831 |
+
"node": ">=8"
|
| 1832 |
+
}
|
| 1833 |
+
},
|
| 1834 |
+
"node_modules/electron-to-chromium": {
|
| 1835 |
+
"version": "1.5.302",
|
| 1836 |
+
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz",
|
| 1837 |
+
"integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==",
|
| 1838 |
+
"dev": true,
|
| 1839 |
+
"license": "ISC"
|
| 1840 |
+
},
|
| 1841 |
+
"node_modules/enhanced-resolve": {
|
| 1842 |
+
"version": "5.19.0",
|
| 1843 |
+
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz",
|
| 1844 |
+
"integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==",
|
| 1845 |
+
"dev": true,
|
| 1846 |
+
"license": "MIT",
|
| 1847 |
+
"dependencies": {
|
| 1848 |
+
"graceful-fs": "^4.2.4",
|
| 1849 |
+
"tapable": "^2.3.0"
|
| 1850 |
+
},
|
| 1851 |
+
"engines": {
|
| 1852 |
+
"node": ">=10.13.0"
|
| 1853 |
+
}
|
| 1854 |
+
},
|
| 1855 |
+
"node_modules/es-module-lexer": {
|
| 1856 |
+
"version": "1.7.0",
|
| 1857 |
+
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
| 1858 |
+
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
|
| 1859 |
+
"dev": true,
|
| 1860 |
+
"license": "MIT"
|
| 1861 |
+
},
|
| 1862 |
+
"node_modules/esbuild": {
|
| 1863 |
+
"version": "0.27.3",
|
| 1864 |
+
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
| 1865 |
+
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
|
| 1866 |
+
"dev": true,
|
| 1867 |
+
"hasInstallScript": true,
|
| 1868 |
+
"license": "MIT",
|
| 1869 |
+
"bin": {
|
| 1870 |
+
"esbuild": "bin/esbuild"
|
| 1871 |
+
},
|
| 1872 |
+
"engines": {
|
| 1873 |
+
"node": ">=18"
|
| 1874 |
+
},
|
| 1875 |
+
"optionalDependencies": {
|
| 1876 |
+
"@esbuild/aix-ppc64": "0.27.3",
|
| 1877 |
+
"@esbuild/android-arm": "0.27.3",
|
| 1878 |
+
"@esbuild/android-arm64": "0.27.3",
|
| 1879 |
+
"@esbuild/android-x64": "0.27.3",
|
| 1880 |
+
"@esbuild/darwin-arm64": "0.27.3",
|
| 1881 |
+
"@esbuild/darwin-x64": "0.27.3",
|
| 1882 |
+
"@esbuild/freebsd-arm64": "0.27.3",
|
| 1883 |
+
"@esbuild/freebsd-x64": "0.27.3",
|
| 1884 |
+
"@esbuild/linux-arm": "0.27.3",
|
| 1885 |
+
"@esbuild/linux-arm64": "0.27.3",
|
| 1886 |
+
"@esbuild/linux-ia32": "0.27.3",
|
| 1887 |
+
"@esbuild/linux-loong64": "0.27.3",
|
| 1888 |
+
"@esbuild/linux-mips64el": "0.27.3",
|
| 1889 |
+
"@esbuild/linux-ppc64": "0.27.3",
|
| 1890 |
+
"@esbuild/linux-riscv64": "0.27.3",
|
| 1891 |
+
"@esbuild/linux-s390x": "0.27.3",
|
| 1892 |
+
"@esbuild/linux-x64": "0.27.3",
|
| 1893 |
+
"@esbuild/netbsd-arm64": "0.27.3",
|
| 1894 |
+
"@esbuild/netbsd-x64": "0.27.3",
|
| 1895 |
+
"@esbuild/openbsd-arm64": "0.27.3",
|
| 1896 |
+
"@esbuild/openbsd-x64": "0.27.3",
|
| 1897 |
+
"@esbuild/openharmony-arm64": "0.27.3",
|
| 1898 |
+
"@esbuild/sunos-x64": "0.27.3",
|
| 1899 |
+
"@esbuild/win32-arm64": "0.27.3",
|
| 1900 |
+
"@esbuild/win32-ia32": "0.27.3",
|
| 1901 |
+
"@esbuild/win32-x64": "0.27.3"
|
| 1902 |
+
}
|
| 1903 |
+
},
|
| 1904 |
+
"node_modules/escalade": {
|
| 1905 |
+
"version": "3.2.0",
|
| 1906 |
+
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
| 1907 |
+
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
| 1908 |
+
"dev": true,
|
| 1909 |
+
"license": "MIT",
|
| 1910 |
+
"engines": {
|
| 1911 |
+
"node": ">=6"
|
| 1912 |
+
}
|
| 1913 |
+
},
|
| 1914 |
+
"node_modules/estree-walker": {
|
| 1915 |
+
"version": "3.0.3",
|
| 1916 |
+
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
| 1917 |
+
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
| 1918 |
+
"dev": true,
|
| 1919 |
+
"license": "MIT",
|
| 1920 |
+
"dependencies": {
|
| 1921 |
+
"@types/estree": "^1.0.0"
|
| 1922 |
+
}
|
| 1923 |
+
},
|
| 1924 |
+
"node_modules/expect-type": {
|
| 1925 |
+
"version": "1.3.0",
|
| 1926 |
+
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
|
| 1927 |
+
"integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
|
| 1928 |
+
"dev": true,
|
| 1929 |
+
"license": "Apache-2.0",
|
| 1930 |
+
"engines": {
|
| 1931 |
+
"node": ">=12.0.0"
|
| 1932 |
+
}
|
| 1933 |
+
},
|
| 1934 |
+
"node_modules/fdir": {
|
| 1935 |
+
"version": "6.5.0",
|
| 1936 |
+
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
| 1937 |
+
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
| 1938 |
+
"dev": true,
|
| 1939 |
+
"license": "MIT",
|
| 1940 |
+
"engines": {
|
| 1941 |
+
"node": ">=12.0.0"
|
| 1942 |
+
},
|
| 1943 |
+
"peerDependencies": {
|
| 1944 |
+
"picomatch": "^3 || ^4"
|
| 1945 |
+
},
|
| 1946 |
+
"peerDependenciesMeta": {
|
| 1947 |
+
"picomatch": {
|
| 1948 |
+
"optional": true
|
| 1949 |
+
}
|
| 1950 |
+
}
|
| 1951 |
+
},
|
| 1952 |
+
"node_modules/fsevents": {
|
| 1953 |
+
"version": "2.3.3",
|
| 1954 |
+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
| 1955 |
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
| 1956 |
+
"dev": true,
|
| 1957 |
+
"hasInstallScript": true,
|
| 1958 |
+
"license": "MIT",
|
| 1959 |
+
"optional": true,
|
| 1960 |
+
"os": [
|
| 1961 |
+
"darwin"
|
| 1962 |
+
],
|
| 1963 |
+
"engines": {
|
| 1964 |
+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 1965 |
+
}
|
| 1966 |
+
},
|
| 1967 |
+
"node_modules/gensync": {
|
| 1968 |
+
"version": "1.0.0-beta.2",
|
| 1969 |
+
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
| 1970 |
+
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
| 1971 |
+
"dev": true,
|
| 1972 |
+
"license": "MIT",
|
| 1973 |
+
"engines": {
|
| 1974 |
+
"node": ">=6.9.0"
|
| 1975 |
+
}
|
| 1976 |
+
},
|
| 1977 |
+
"node_modules/graceful-fs": {
|
| 1978 |
+
"version": "4.2.11",
|
| 1979 |
+
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
| 1980 |
+
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
| 1981 |
+
"dev": true,
|
| 1982 |
+
"license": "ISC"
|
| 1983 |
+
},
|
| 1984 |
+
"node_modules/jiti": {
|
| 1985 |
+
"version": "2.6.1",
|
| 1986 |
+
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
| 1987 |
+
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
| 1988 |
+
"dev": true,
|
| 1989 |
+
"license": "MIT",
|
| 1990 |
+
"bin": {
|
| 1991 |
+
"jiti": "lib/jiti-cli.mjs"
|
| 1992 |
+
}
|
| 1993 |
+
},
|
| 1994 |
+
"node_modules/js-tokens": {
|
| 1995 |
+
"version": "4.0.0",
|
| 1996 |
+
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
| 1997 |
+
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
| 1998 |
+
"dev": true,
|
| 1999 |
+
"license": "MIT"
|
| 2000 |
+
},
|
| 2001 |
+
"node_modules/jsesc": {
|
| 2002 |
+
"version": "3.1.0",
|
| 2003 |
+
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
| 2004 |
+
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
| 2005 |
+
"dev": true,
|
| 2006 |
+
"license": "MIT",
|
| 2007 |
+
"bin": {
|
| 2008 |
+
"jsesc": "bin/jsesc"
|
| 2009 |
+
},
|
| 2010 |
+
"engines": {
|
| 2011 |
+
"node": ">=6"
|
| 2012 |
+
}
|
| 2013 |
+
},
|
| 2014 |
+
"node_modules/json5": {
|
| 2015 |
+
"version": "2.2.3",
|
| 2016 |
+
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
| 2017 |
+
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
| 2018 |
+
"dev": true,
|
| 2019 |
+
"license": "MIT",
|
| 2020 |
+
"bin": {
|
| 2021 |
+
"json5": "lib/cli.js"
|
| 2022 |
+
},
|
| 2023 |
+
"engines": {
|
| 2024 |
+
"node": ">=6"
|
| 2025 |
+
}
|
| 2026 |
+
},
|
| 2027 |
+
"node_modules/lightningcss": {
|
| 2028 |
+
"version": "1.31.1",
|
| 2029 |
+
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz",
|
| 2030 |
+
"integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==",
|
| 2031 |
+
"dev": true,
|
| 2032 |
+
"license": "MPL-2.0",
|
| 2033 |
+
"dependencies": {
|
| 2034 |
+
"detect-libc": "^2.0.3"
|
| 2035 |
+
},
|
| 2036 |
+
"engines": {
|
| 2037 |
+
"node": ">= 12.0.0"
|
| 2038 |
+
},
|
| 2039 |
+
"funding": {
|
| 2040 |
+
"type": "opencollective",
|
| 2041 |
+
"url": "https://opencollective.com/parcel"
|
| 2042 |
+
},
|
| 2043 |
+
"optionalDependencies": {
|
| 2044 |
+
"lightningcss-android-arm64": "1.31.1",
|
| 2045 |
+
"lightningcss-darwin-arm64": "1.31.1",
|
| 2046 |
+
"lightningcss-darwin-x64": "1.31.1",
|
| 2047 |
+
"lightningcss-freebsd-x64": "1.31.1",
|
| 2048 |
+
"lightningcss-linux-arm-gnueabihf": "1.31.1",
|
| 2049 |
+
"lightningcss-linux-arm64-gnu": "1.31.1",
|
| 2050 |
+
"lightningcss-linux-arm64-musl": "1.31.1",
|
| 2051 |
+
"lightningcss-linux-x64-gnu": "1.31.1",
|
| 2052 |
+
"lightningcss-linux-x64-musl": "1.31.1",
|
| 2053 |
+
"lightningcss-win32-arm64-msvc": "1.31.1",
|
| 2054 |
+
"lightningcss-win32-x64-msvc": "1.31.1"
|
| 2055 |
+
}
|
| 2056 |
+
},
|
| 2057 |
+
"node_modules/lightningcss-android-arm64": {
|
| 2058 |
+
"version": "1.31.1",
|
| 2059 |
+
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz",
|
| 2060 |
+
"integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==",
|
| 2061 |
+
"cpu": [
|
| 2062 |
+
"arm64"
|
| 2063 |
+
],
|
| 2064 |
+
"dev": true,
|
| 2065 |
+
"license": "MPL-2.0",
|
| 2066 |
+
"optional": true,
|
| 2067 |
+
"os": [
|
| 2068 |
+
"android"
|
| 2069 |
+
],
|
| 2070 |
+
"engines": {
|
| 2071 |
+
"node": ">= 12.0.0"
|
| 2072 |
+
},
|
| 2073 |
+
"funding": {
|
| 2074 |
+
"type": "opencollective",
|
| 2075 |
+
"url": "https://opencollective.com/parcel"
|
| 2076 |
+
}
|
| 2077 |
+
},
|
| 2078 |
+
"node_modules/lightningcss-darwin-arm64": {
|
| 2079 |
+
"version": "1.31.1",
|
| 2080 |
+
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz",
|
| 2081 |
+
"integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==",
|
| 2082 |
+
"cpu": [
|
| 2083 |
+
"arm64"
|
| 2084 |
+
],
|
| 2085 |
+
"dev": true,
|
| 2086 |
+
"license": "MPL-2.0",
|
| 2087 |
+
"optional": true,
|
| 2088 |
+
"os": [
|
| 2089 |
+
"darwin"
|
| 2090 |
+
],
|
| 2091 |
+
"engines": {
|
| 2092 |
+
"node": ">= 12.0.0"
|
| 2093 |
+
},
|
| 2094 |
+
"funding": {
|
| 2095 |
+
"type": "opencollective",
|
| 2096 |
+
"url": "https://opencollective.com/parcel"
|
| 2097 |
+
}
|
| 2098 |
+
},
|
| 2099 |
+
"node_modules/lightningcss-darwin-x64": {
|
| 2100 |
+
"version": "1.31.1",
|
| 2101 |
+
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz",
|
| 2102 |
+
"integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==",
|
| 2103 |
+
"cpu": [
|
| 2104 |
+
"x64"
|
| 2105 |
+
],
|
| 2106 |
+
"dev": true,
|
| 2107 |
+
"license": "MPL-2.0",
|
| 2108 |
+
"optional": true,
|
| 2109 |
+
"os": [
|
| 2110 |
+
"darwin"
|
| 2111 |
+
],
|
| 2112 |
+
"engines": {
|
| 2113 |
+
"node": ">= 12.0.0"
|
| 2114 |
+
},
|
| 2115 |
+
"funding": {
|
| 2116 |
+
"type": "opencollective",
|
| 2117 |
+
"url": "https://opencollective.com/parcel"
|
| 2118 |
+
}
|
| 2119 |
+
},
|
| 2120 |
+
"node_modules/lightningcss-freebsd-x64": {
|
| 2121 |
+
"version": "1.31.1",
|
| 2122 |
+
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz",
|
| 2123 |
+
"integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==",
|
| 2124 |
+
"cpu": [
|
| 2125 |
+
"x64"
|
| 2126 |
+
],
|
| 2127 |
+
"dev": true,
|
| 2128 |
+
"license": "MPL-2.0",
|
| 2129 |
+
"optional": true,
|
| 2130 |
+
"os": [
|
| 2131 |
+
"freebsd"
|
| 2132 |
+
],
|
| 2133 |
+
"engines": {
|
| 2134 |
+
"node": ">= 12.0.0"
|
| 2135 |
+
},
|
| 2136 |
+
"funding": {
|
| 2137 |
+
"type": "opencollective",
|
| 2138 |
+
"url": "https://opencollective.com/parcel"
|
| 2139 |
+
}
|
| 2140 |
+
},
|
| 2141 |
+
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
| 2142 |
+
"version": "1.31.1",
|
| 2143 |
+
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz",
|
| 2144 |
+
"integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==",
|
| 2145 |
+
"cpu": [
|
| 2146 |
+
"arm"
|
| 2147 |
+
],
|
| 2148 |
+
"dev": true,
|
| 2149 |
+
"license": "MPL-2.0",
|
| 2150 |
+
"optional": true,
|
| 2151 |
+
"os": [
|
| 2152 |
+
"linux"
|
| 2153 |
+
],
|
| 2154 |
+
"engines": {
|
| 2155 |
+
"node": ">= 12.0.0"
|
| 2156 |
+
},
|
| 2157 |
+
"funding": {
|
| 2158 |
+
"type": "opencollective",
|
| 2159 |
+
"url": "https://opencollective.com/parcel"
|
| 2160 |
+
}
|
| 2161 |
+
},
|
| 2162 |
+
"node_modules/lightningcss-linux-arm64-gnu": {
|
| 2163 |
+
"version": "1.31.1",
|
| 2164 |
+
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz",
|
| 2165 |
+
"integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==",
|
| 2166 |
+
"cpu": [
|
| 2167 |
+
"arm64"
|
| 2168 |
+
],
|
| 2169 |
+
"dev": true,
|
| 2170 |
+
"license": "MPL-2.0",
|
| 2171 |
+
"optional": true,
|
| 2172 |
+
"os": [
|
| 2173 |
+
"linux"
|
| 2174 |
+
],
|
| 2175 |
+
"engines": {
|
| 2176 |
+
"node": ">= 12.0.0"
|
| 2177 |
+
},
|
| 2178 |
+
"funding": {
|
| 2179 |
+
"type": "opencollective",
|
| 2180 |
+
"url": "https://opencollective.com/parcel"
|
| 2181 |
+
}
|
| 2182 |
+
},
|
| 2183 |
+
"node_modules/lightningcss-linux-arm64-musl": {
|
| 2184 |
+
"version": "1.31.1",
|
| 2185 |
+
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz",
|
| 2186 |
+
"integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==",
|
| 2187 |
+
"cpu": [
|
| 2188 |
+
"arm64"
|
| 2189 |
+
],
|
| 2190 |
+
"dev": true,
|
| 2191 |
+
"license": "MPL-2.0",
|
| 2192 |
+
"optional": true,
|
| 2193 |
+
"os": [
|
| 2194 |
+
"linux"
|
| 2195 |
+
],
|
| 2196 |
+
"engines": {
|
| 2197 |
+
"node": ">= 12.0.0"
|
| 2198 |
+
},
|
| 2199 |
+
"funding": {
|
| 2200 |
+
"type": "opencollective",
|
| 2201 |
+
"url": "https://opencollective.com/parcel"
|
| 2202 |
+
}
|
| 2203 |
+
},
|
| 2204 |
+
"node_modules/lightningcss-linux-x64-gnu": {
|
| 2205 |
+
"version": "1.31.1",
|
| 2206 |
+
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz",
|
| 2207 |
+
"integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==",
|
| 2208 |
+
"cpu": [
|
| 2209 |
+
"x64"
|
| 2210 |
+
],
|
| 2211 |
+
"dev": true,
|
| 2212 |
+
"license": "MPL-2.0",
|
| 2213 |
+
"optional": true,
|
| 2214 |
+
"os": [
|
| 2215 |
+
"linux"
|
| 2216 |
+
],
|
| 2217 |
+
"engines": {
|
| 2218 |
+
"node": ">= 12.0.0"
|
| 2219 |
+
},
|
| 2220 |
+
"funding": {
|
| 2221 |
+
"type": "opencollective",
|
| 2222 |
+
"url": "https://opencollective.com/parcel"
|
| 2223 |
+
}
|
| 2224 |
+
},
|
| 2225 |
+
"node_modules/lightningcss-linux-x64-musl": {
|
| 2226 |
+
"version": "1.31.1",
|
| 2227 |
+
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz",
|
| 2228 |
+
"integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==",
|
| 2229 |
+
"cpu": [
|
| 2230 |
+
"x64"
|
| 2231 |
+
],
|
| 2232 |
+
"dev": true,
|
| 2233 |
+
"license": "MPL-2.0",
|
| 2234 |
+
"optional": true,
|
| 2235 |
+
"os": [
|
| 2236 |
+
"linux"
|
| 2237 |
+
],
|
| 2238 |
+
"engines": {
|
| 2239 |
+
"node": ">= 12.0.0"
|
| 2240 |
+
},
|
| 2241 |
+
"funding": {
|
| 2242 |
+
"type": "opencollective",
|
| 2243 |
+
"url": "https://opencollective.com/parcel"
|
| 2244 |
+
}
|
| 2245 |
+
},
|
| 2246 |
+
"node_modules/lightningcss-win32-arm64-msvc": {
|
| 2247 |
+
"version": "1.31.1",
|
| 2248 |
+
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz",
|
| 2249 |
+
"integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==",
|
| 2250 |
+
"cpu": [
|
| 2251 |
+
"arm64"
|
| 2252 |
+
],
|
| 2253 |
+
"dev": true,
|
| 2254 |
+
"license": "MPL-2.0",
|
| 2255 |
+
"optional": true,
|
| 2256 |
+
"os": [
|
| 2257 |
+
"win32"
|
| 2258 |
+
],
|
| 2259 |
+
"engines": {
|
| 2260 |
+
"node": ">= 12.0.0"
|
| 2261 |
+
},
|
| 2262 |
+
"funding": {
|
| 2263 |
+
"type": "opencollective",
|
| 2264 |
+
"url": "https://opencollective.com/parcel"
|
| 2265 |
+
}
|
| 2266 |
+
},
|
| 2267 |
+
"node_modules/lightningcss-win32-x64-msvc": {
|
| 2268 |
+
"version": "1.31.1",
|
| 2269 |
+
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz",
|
| 2270 |
+
"integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==",
|
| 2271 |
+
"cpu": [
|
| 2272 |
+
"x64"
|
| 2273 |
+
],
|
| 2274 |
+
"dev": true,
|
| 2275 |
+
"license": "MPL-2.0",
|
| 2276 |
+
"optional": true,
|
| 2277 |
+
"os": [
|
| 2278 |
+
"win32"
|
| 2279 |
+
],
|
| 2280 |
+
"engines": {
|
| 2281 |
+
"node": ">= 12.0.0"
|
| 2282 |
+
},
|
| 2283 |
+
"funding": {
|
| 2284 |
+
"type": "opencollective",
|
| 2285 |
+
"url": "https://opencollective.com/parcel"
|
| 2286 |
+
}
|
| 2287 |
+
},
|
| 2288 |
+
"node_modules/lru-cache": {
|
| 2289 |
+
"version": "5.1.1",
|
| 2290 |
+
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
| 2291 |
+
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
| 2292 |
+
"dev": true,
|
| 2293 |
+
"license": "ISC",
|
| 2294 |
+
"dependencies": {
|
| 2295 |
+
"yallist": "^3.0.2"
|
| 2296 |
+
}
|
| 2297 |
+
},
|
| 2298 |
+
"node_modules/magic-string": {
|
| 2299 |
+
"version": "0.30.21",
|
| 2300 |
+
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
| 2301 |
+
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
| 2302 |
+
"dev": true,
|
| 2303 |
+
"license": "MIT",
|
| 2304 |
+
"dependencies": {
|
| 2305 |
+
"@jridgewell/sourcemap-codec": "^1.5.5"
|
| 2306 |
+
}
|
| 2307 |
+
},
|
| 2308 |
+
"node_modules/ms": {
|
| 2309 |
+
"version": "2.1.3",
|
| 2310 |
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
| 2311 |
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
| 2312 |
+
"dev": true,
|
| 2313 |
+
"license": "MIT"
|
| 2314 |
+
},
|
| 2315 |
+
"node_modules/nanoid": {
|
| 2316 |
+
"version": "3.3.11",
|
| 2317 |
+
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
| 2318 |
+
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
| 2319 |
+
"dev": true,
|
| 2320 |
+
"funding": [
|
| 2321 |
+
{
|
| 2322 |
+
"type": "github",
|
| 2323 |
+
"url": "https://github.com/sponsors/ai"
|
| 2324 |
+
}
|
| 2325 |
+
],
|
| 2326 |
+
"license": "MIT",
|
| 2327 |
+
"bin": {
|
| 2328 |
+
"nanoid": "bin/nanoid.cjs"
|
| 2329 |
+
},
|
| 2330 |
+
"engines": {
|
| 2331 |
+
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
| 2332 |
+
}
|
| 2333 |
+
},
|
| 2334 |
+
"node_modules/node-releases": {
|
| 2335 |
+
"version": "2.0.27",
|
| 2336 |
+
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
|
| 2337 |
+
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
|
| 2338 |
+
"dev": true,
|
| 2339 |
+
"license": "MIT"
|
| 2340 |
+
},
|
| 2341 |
+
"node_modules/obug": {
|
| 2342 |
+
"version": "2.1.1",
|
| 2343 |
+
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
|
| 2344 |
+
"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
|
| 2345 |
+
"dev": true,
|
| 2346 |
+
"funding": [
|
| 2347 |
+
"https://github.com/sponsors/sxzz",
|
| 2348 |
+
"https://opencollective.com/debug"
|
| 2349 |
+
],
|
| 2350 |
+
"license": "MIT"
|
| 2351 |
+
},
|
| 2352 |
+
"node_modules/pathe": {
|
| 2353 |
+
"version": "2.0.3",
|
| 2354 |
+
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
| 2355 |
+
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
| 2356 |
+
"dev": true,
|
| 2357 |
+
"license": "MIT"
|
| 2358 |
+
},
|
| 2359 |
+
"node_modules/picocolors": {
|
| 2360 |
+
"version": "1.1.1",
|
| 2361 |
+
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
| 2362 |
+
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
| 2363 |
+
"dev": true,
|
| 2364 |
+
"license": "ISC"
|
| 2365 |
+
},
|
| 2366 |
+
"node_modules/picomatch": {
|
| 2367 |
+
"version": "4.0.3",
|
| 2368 |
+
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
| 2369 |
+
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
| 2370 |
+
"dev": true,
|
| 2371 |
+
"license": "MIT",
|
| 2372 |
+
"engines": {
|
| 2373 |
+
"node": ">=12"
|
| 2374 |
+
},
|
| 2375 |
+
"funding": {
|
| 2376 |
+
"url": "https://github.com/sponsors/jonschlinkert"
|
| 2377 |
+
}
|
| 2378 |
+
},
|
| 2379 |
+
"node_modules/postcss": {
|
| 2380 |
+
"version": "8.5.6",
|
| 2381 |
+
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
| 2382 |
+
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
| 2383 |
+
"dev": true,
|
| 2384 |
+
"funding": [
|
| 2385 |
+
{
|
| 2386 |
+
"type": "opencollective",
|
| 2387 |
+
"url": "https://opencollective.com/postcss/"
|
| 2388 |
+
},
|
| 2389 |
+
{
|
| 2390 |
+
"type": "tidelift",
|
| 2391 |
+
"url": "https://tidelift.com/funding/github/npm/postcss"
|
| 2392 |
+
},
|
| 2393 |
+
{
|
| 2394 |
+
"type": "github",
|
| 2395 |
+
"url": "https://github.com/sponsors/ai"
|
| 2396 |
+
}
|
| 2397 |
+
],
|
| 2398 |
+
"license": "MIT",
|
| 2399 |
+
"dependencies": {
|
| 2400 |
+
"nanoid": "^3.3.11",
|
| 2401 |
+
"picocolors": "^1.1.1",
|
| 2402 |
+
"source-map-js": "^1.2.1"
|
| 2403 |
+
},
|
| 2404 |
+
"engines": {
|
| 2405 |
+
"node": "^10 || ^12 || >=14"
|
| 2406 |
+
}
|
| 2407 |
+
},
|
| 2408 |
+
"node_modules/react": {
|
| 2409 |
+
"version": "19.2.4",
|
| 2410 |
+
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
| 2411 |
+
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
| 2412 |
+
"license": "MIT",
|
| 2413 |
+
"engines": {
|
| 2414 |
+
"node": ">=0.10.0"
|
| 2415 |
+
}
|
| 2416 |
+
},
|
| 2417 |
+
"node_modules/react-dom": {
|
| 2418 |
+
"version": "19.2.4",
|
| 2419 |
+
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
| 2420 |
+
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
| 2421 |
+
"license": "MIT",
|
| 2422 |
+
"dependencies": {
|
| 2423 |
+
"scheduler": "^0.27.0"
|
| 2424 |
+
},
|
| 2425 |
+
"peerDependencies": {
|
| 2426 |
+
"react": "^19.2.4"
|
| 2427 |
+
}
|
| 2428 |
+
},
|
| 2429 |
+
"node_modules/react-refresh": {
|
| 2430 |
+
"version": "0.18.0",
|
| 2431 |
+
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
|
| 2432 |
+
"integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
|
| 2433 |
+
"dev": true,
|
| 2434 |
+
"license": "MIT",
|
| 2435 |
+
"engines": {
|
| 2436 |
+
"node": ">=0.10.0"
|
| 2437 |
+
}
|
| 2438 |
+
},
|
| 2439 |
+
"node_modules/rollup": {
|
| 2440 |
+
"version": "4.59.0",
|
| 2441 |
+
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
|
| 2442 |
+
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
|
| 2443 |
+
"dev": true,
|
| 2444 |
+
"license": "MIT",
|
| 2445 |
+
"dependencies": {
|
| 2446 |
+
"@types/estree": "1.0.8"
|
| 2447 |
+
},
|
| 2448 |
+
"bin": {
|
| 2449 |
+
"rollup": "dist/bin/rollup"
|
| 2450 |
+
},
|
| 2451 |
+
"engines": {
|
| 2452 |
+
"node": ">=18.0.0",
|
| 2453 |
+
"npm": ">=8.0.0"
|
| 2454 |
+
},
|
| 2455 |
+
"optionalDependencies": {
|
| 2456 |
+
"@rollup/rollup-android-arm-eabi": "4.59.0",
|
| 2457 |
+
"@rollup/rollup-android-arm64": "4.59.0",
|
| 2458 |
+
"@rollup/rollup-darwin-arm64": "4.59.0",
|
| 2459 |
+
"@rollup/rollup-darwin-x64": "4.59.0",
|
| 2460 |
+
"@rollup/rollup-freebsd-arm64": "4.59.0",
|
| 2461 |
+
"@rollup/rollup-freebsd-x64": "4.59.0",
|
| 2462 |
+
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
|
| 2463 |
+
"@rollup/rollup-linux-arm-musleabihf": "4.59.0",
|
| 2464 |
+
"@rollup/rollup-linux-arm64-gnu": "4.59.0",
|
| 2465 |
+
"@rollup/rollup-linux-arm64-musl": "4.59.0",
|
| 2466 |
+
"@rollup/rollup-linux-loong64-gnu": "4.59.0",
|
| 2467 |
+
"@rollup/rollup-linux-loong64-musl": "4.59.0",
|
| 2468 |
+
"@rollup/rollup-linux-ppc64-gnu": "4.59.0",
|
| 2469 |
+
"@rollup/rollup-linux-ppc64-musl": "4.59.0",
|
| 2470 |
+
"@rollup/rollup-linux-riscv64-gnu": "4.59.0",
|
| 2471 |
+
"@rollup/rollup-linux-riscv64-musl": "4.59.0",
|
| 2472 |
+
"@rollup/rollup-linux-s390x-gnu": "4.59.0",
|
| 2473 |
+
"@rollup/rollup-linux-x64-gnu": "4.59.0",
|
| 2474 |
+
"@rollup/rollup-linux-x64-musl": "4.59.0",
|
| 2475 |
+
"@rollup/rollup-openbsd-x64": "4.59.0",
|
| 2476 |
+
"@rollup/rollup-openharmony-arm64": "4.59.0",
|
| 2477 |
+
"@rollup/rollup-win32-arm64-msvc": "4.59.0",
|
| 2478 |
+
"@rollup/rollup-win32-ia32-msvc": "4.59.0",
|
| 2479 |
+
"@rollup/rollup-win32-x64-gnu": "4.59.0",
|
| 2480 |
+
"@rollup/rollup-win32-x64-msvc": "4.59.0",
|
| 2481 |
+
"fsevents": "~2.3.2"
|
| 2482 |
+
}
|
| 2483 |
+
},
|
| 2484 |
+
"node_modules/scheduler": {
|
| 2485 |
+
"version": "0.27.0",
|
| 2486 |
+
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
| 2487 |
+
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
| 2488 |
+
"license": "MIT"
|
| 2489 |
+
},
|
| 2490 |
+
"node_modules/semver": {
|
| 2491 |
+
"version": "6.3.1",
|
| 2492 |
+
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
| 2493 |
+
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
| 2494 |
+
"dev": true,
|
| 2495 |
+
"license": "ISC",
|
| 2496 |
+
"bin": {
|
| 2497 |
+
"semver": "bin/semver.js"
|
| 2498 |
+
}
|
| 2499 |
+
},
|
| 2500 |
+
"node_modules/siginfo": {
|
| 2501 |
+
"version": "2.0.0",
|
| 2502 |
+
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
| 2503 |
+
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
| 2504 |
+
"dev": true,
|
| 2505 |
+
"license": "ISC"
|
| 2506 |
+
},
|
| 2507 |
+
"node_modules/source-map-js": {
|
| 2508 |
+
"version": "1.2.1",
|
| 2509 |
+
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
| 2510 |
+
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
| 2511 |
+
"dev": true,
|
| 2512 |
+
"license": "BSD-3-Clause",
|
| 2513 |
+
"engines": {
|
| 2514 |
+
"node": ">=0.10.0"
|
| 2515 |
+
}
|
| 2516 |
+
},
|
| 2517 |
+
"node_modules/stackback": {
|
| 2518 |
+
"version": "0.0.2",
|
| 2519 |
+
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
| 2520 |
+
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
| 2521 |
+
"dev": true,
|
| 2522 |
+
"license": "MIT"
|
| 2523 |
+
},
|
| 2524 |
+
"node_modules/std-env": {
|
| 2525 |
+
"version": "3.10.0",
|
| 2526 |
+
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
| 2527 |
+
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
|
| 2528 |
+
"dev": true,
|
| 2529 |
+
"license": "MIT"
|
| 2530 |
+
},
|
| 2531 |
+
"node_modules/tailwindcss": {
|
| 2532 |
+
"version": "4.2.1",
|
| 2533 |
+
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz",
|
| 2534 |
+
"integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==",
|
| 2535 |
+
"dev": true,
|
| 2536 |
+
"license": "MIT"
|
| 2537 |
+
},
|
| 2538 |
+
"node_modules/tapable": {
|
| 2539 |
+
"version": "2.3.0",
|
| 2540 |
+
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
|
| 2541 |
+
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
|
| 2542 |
+
"dev": true,
|
| 2543 |
+
"license": "MIT",
|
| 2544 |
+
"engines": {
|
| 2545 |
+
"node": ">=6"
|
| 2546 |
+
},
|
| 2547 |
+
"funding": {
|
| 2548 |
+
"type": "opencollective",
|
| 2549 |
+
"url": "https://opencollective.com/webpack"
|
| 2550 |
+
}
|
| 2551 |
+
},
|
| 2552 |
+
"node_modules/tinybench": {
|
| 2553 |
+
"version": "2.9.0",
|
| 2554 |
+
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
| 2555 |
+
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
| 2556 |
+
"dev": true,
|
| 2557 |
+
"license": "MIT"
|
| 2558 |
+
},
|
| 2559 |
+
"node_modules/tinyexec": {
|
| 2560 |
+
"version": "1.0.2",
|
| 2561 |
+
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
|
| 2562 |
+
"integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
|
| 2563 |
+
"dev": true,
|
| 2564 |
+
"license": "MIT",
|
| 2565 |
+
"engines": {
|
| 2566 |
+
"node": ">=18"
|
| 2567 |
+
}
|
| 2568 |
+
},
|
| 2569 |
+
"node_modules/tinyglobby": {
|
| 2570 |
+
"version": "0.2.15",
|
| 2571 |
+
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
| 2572 |
+
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
| 2573 |
+
"dev": true,
|
| 2574 |
+
"license": "MIT",
|
| 2575 |
+
"dependencies": {
|
| 2576 |
+
"fdir": "^6.5.0",
|
| 2577 |
+
"picomatch": "^4.0.3"
|
| 2578 |
+
},
|
| 2579 |
+
"engines": {
|
| 2580 |
+
"node": ">=12.0.0"
|
| 2581 |
+
},
|
| 2582 |
+
"funding": {
|
| 2583 |
+
"url": "https://github.com/sponsors/SuperchupuDev"
|
| 2584 |
+
}
|
| 2585 |
+
},
|
| 2586 |
+
"node_modules/tinyrainbow": {
|
| 2587 |
+
"version": "3.0.3",
|
| 2588 |
+
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz",
|
| 2589 |
+
"integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==",
|
| 2590 |
+
"dev": true,
|
| 2591 |
+
"license": "MIT",
|
| 2592 |
+
"engines": {
|
| 2593 |
+
"node": ">=14.0.0"
|
| 2594 |
+
}
|
| 2595 |
+
},
|
| 2596 |
+
"node_modules/typescript": {
|
| 2597 |
+
"version": "5.9.3",
|
| 2598 |
+
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
| 2599 |
+
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
| 2600 |
+
"dev": true,
|
| 2601 |
+
"license": "Apache-2.0",
|
| 2602 |
+
"bin": {
|
| 2603 |
+
"tsc": "bin/tsc",
|
| 2604 |
+
"tsserver": "bin/tsserver"
|
| 2605 |
+
},
|
| 2606 |
+
"engines": {
|
| 2607 |
+
"node": ">=14.17"
|
| 2608 |
+
}
|
| 2609 |
+
},
|
| 2610 |
+
"node_modules/undici-types": {
|
| 2611 |
+
"version": "7.18.2",
|
| 2612 |
+
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
| 2613 |
+
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
| 2614 |
+
"dev": true,
|
| 2615 |
+
"license": "MIT"
|
| 2616 |
+
},
|
| 2617 |
+
"node_modules/update-browserslist-db": {
|
| 2618 |
+
"version": "1.2.3",
|
| 2619 |
+
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
| 2620 |
+
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
|
| 2621 |
+
"dev": true,
|
| 2622 |
+
"funding": [
|
| 2623 |
+
{
|
| 2624 |
+
"type": "opencollective",
|
| 2625 |
+
"url": "https://opencollective.com/browserslist"
|
| 2626 |
+
},
|
| 2627 |
+
{
|
| 2628 |
+
"type": "tidelift",
|
| 2629 |
+
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
| 2630 |
+
},
|
| 2631 |
+
{
|
| 2632 |
+
"type": "github",
|
| 2633 |
+
"url": "https://github.com/sponsors/ai"
|
| 2634 |
+
}
|
| 2635 |
+
],
|
| 2636 |
+
"license": "MIT",
|
| 2637 |
+
"dependencies": {
|
| 2638 |
+
"escalade": "^3.2.0",
|
| 2639 |
+
"picocolors": "^1.1.1"
|
| 2640 |
+
},
|
| 2641 |
+
"bin": {
|
| 2642 |
+
"update-browserslist-db": "cli.js"
|
| 2643 |
+
},
|
| 2644 |
+
"peerDependencies": {
|
| 2645 |
+
"browserslist": ">= 4.21.0"
|
| 2646 |
+
}
|
| 2647 |
+
},
|
| 2648 |
+
"node_modules/vite": {
|
| 2649 |
+
"version": "7.3.1",
|
| 2650 |
+
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
| 2651 |
+
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
| 2652 |
+
"dev": true,
|
| 2653 |
+
"license": "MIT",
|
| 2654 |
+
"dependencies": {
|
| 2655 |
+
"esbuild": "^0.27.0",
|
| 2656 |
+
"fdir": "^6.5.0",
|
| 2657 |
+
"picomatch": "^4.0.3",
|
| 2658 |
+
"postcss": "^8.5.6",
|
| 2659 |
+
"rollup": "^4.43.0",
|
| 2660 |
+
"tinyglobby": "^0.2.15"
|
| 2661 |
+
},
|
| 2662 |
+
"bin": {
|
| 2663 |
+
"vite": "bin/vite.js"
|
| 2664 |
+
},
|
| 2665 |
+
"engines": {
|
| 2666 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 2667 |
+
},
|
| 2668 |
+
"funding": {
|
| 2669 |
+
"url": "https://github.com/vitejs/vite?sponsor=1"
|
| 2670 |
+
},
|
| 2671 |
+
"optionalDependencies": {
|
| 2672 |
+
"fsevents": "~2.3.3"
|
| 2673 |
+
},
|
| 2674 |
+
"peerDependencies": {
|
| 2675 |
+
"@types/node": "^20.19.0 || >=22.12.0",
|
| 2676 |
+
"jiti": ">=1.21.0",
|
| 2677 |
+
"less": "^4.0.0",
|
| 2678 |
+
"lightningcss": "^1.21.0",
|
| 2679 |
+
"sass": "^1.70.0",
|
| 2680 |
+
"sass-embedded": "^1.70.0",
|
| 2681 |
+
"stylus": ">=0.54.8",
|
| 2682 |
+
"sugarss": "^5.0.0",
|
| 2683 |
+
"terser": "^5.16.0",
|
| 2684 |
+
"tsx": "^4.8.1",
|
| 2685 |
+
"yaml": "^2.4.2"
|
| 2686 |
+
},
|
| 2687 |
+
"peerDependenciesMeta": {
|
| 2688 |
+
"@types/node": {
|
| 2689 |
+
"optional": true
|
| 2690 |
+
},
|
| 2691 |
+
"jiti": {
|
| 2692 |
+
"optional": true
|
| 2693 |
+
},
|
| 2694 |
+
"less": {
|
| 2695 |
+
"optional": true
|
| 2696 |
+
},
|
| 2697 |
+
"lightningcss": {
|
| 2698 |
+
"optional": true
|
| 2699 |
+
},
|
| 2700 |
+
"sass": {
|
| 2701 |
+
"optional": true
|
| 2702 |
+
},
|
| 2703 |
+
"sass-embedded": {
|
| 2704 |
+
"optional": true
|
| 2705 |
+
},
|
| 2706 |
+
"stylus": {
|
| 2707 |
+
"optional": true
|
| 2708 |
+
},
|
| 2709 |
+
"sugarss": {
|
| 2710 |
+
"optional": true
|
| 2711 |
+
},
|
| 2712 |
+
"terser": {
|
| 2713 |
+
"optional": true
|
| 2714 |
+
},
|
| 2715 |
+
"tsx": {
|
| 2716 |
+
"optional": true
|
| 2717 |
+
},
|
| 2718 |
+
"yaml": {
|
| 2719 |
+
"optional": true
|
| 2720 |
+
}
|
| 2721 |
+
}
|
| 2722 |
+
},
|
| 2723 |
+
"node_modules/vitest": {
|
| 2724 |
+
"version": "4.0.18",
|
| 2725 |
+
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz",
|
| 2726 |
+
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
|
| 2727 |
+
"dev": true,
|
| 2728 |
+
"license": "MIT",
|
| 2729 |
+
"dependencies": {
|
| 2730 |
+
"@vitest/expect": "4.0.18",
|
| 2731 |
+
"@vitest/mocker": "4.0.18",
|
| 2732 |
+
"@vitest/pretty-format": "4.0.18",
|
| 2733 |
+
"@vitest/runner": "4.0.18",
|
| 2734 |
+
"@vitest/snapshot": "4.0.18",
|
| 2735 |
+
"@vitest/spy": "4.0.18",
|
| 2736 |
+
"@vitest/utils": "4.0.18",
|
| 2737 |
+
"es-module-lexer": "^1.7.0",
|
| 2738 |
+
"expect-type": "^1.2.2",
|
| 2739 |
+
"magic-string": "^0.30.21",
|
| 2740 |
+
"obug": "^2.1.1",
|
| 2741 |
+
"pathe": "^2.0.3",
|
| 2742 |
+
"picomatch": "^4.0.3",
|
| 2743 |
+
"std-env": "^3.10.0",
|
| 2744 |
+
"tinybench": "^2.9.0",
|
| 2745 |
+
"tinyexec": "^1.0.2",
|
| 2746 |
+
"tinyglobby": "^0.2.15",
|
| 2747 |
+
"tinyrainbow": "^3.0.3",
|
| 2748 |
+
"vite": "^6.0.0 || ^7.0.0",
|
| 2749 |
+
"why-is-node-running": "^2.3.0"
|
| 2750 |
+
},
|
| 2751 |
+
"bin": {
|
| 2752 |
+
"vitest": "vitest.mjs"
|
| 2753 |
+
},
|
| 2754 |
+
"engines": {
|
| 2755 |
+
"node": "^20.0.0 || ^22.0.0 || >=24.0.0"
|
| 2756 |
+
},
|
| 2757 |
+
"funding": {
|
| 2758 |
+
"url": "https://opencollective.com/vitest"
|
| 2759 |
+
},
|
| 2760 |
+
"peerDependencies": {
|
| 2761 |
+
"@edge-runtime/vm": "*",
|
| 2762 |
+
"@opentelemetry/api": "^1.9.0",
|
| 2763 |
+
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
| 2764 |
+
"@vitest/browser-playwright": "4.0.18",
|
| 2765 |
+
"@vitest/browser-preview": "4.0.18",
|
| 2766 |
+
"@vitest/browser-webdriverio": "4.0.18",
|
| 2767 |
+
"@vitest/ui": "4.0.18",
|
| 2768 |
+
"happy-dom": "*",
|
| 2769 |
+
"jsdom": "*"
|
| 2770 |
+
},
|
| 2771 |
+
"peerDependenciesMeta": {
|
| 2772 |
+
"@edge-runtime/vm": {
|
| 2773 |
+
"optional": true
|
| 2774 |
+
},
|
| 2775 |
+
"@opentelemetry/api": {
|
| 2776 |
+
"optional": true
|
| 2777 |
+
},
|
| 2778 |
+
"@types/node": {
|
| 2779 |
+
"optional": true
|
| 2780 |
+
},
|
| 2781 |
+
"@vitest/browser-playwright": {
|
| 2782 |
+
"optional": true
|
| 2783 |
+
},
|
| 2784 |
+
"@vitest/browser-preview": {
|
| 2785 |
+
"optional": true
|
| 2786 |
+
},
|
| 2787 |
+
"@vitest/browser-webdriverio": {
|
| 2788 |
+
"optional": true
|
| 2789 |
+
},
|
| 2790 |
+
"@vitest/ui": {
|
| 2791 |
+
"optional": true
|
| 2792 |
+
},
|
| 2793 |
+
"happy-dom": {
|
| 2794 |
+
"optional": true
|
| 2795 |
+
},
|
| 2796 |
+
"jsdom": {
|
| 2797 |
+
"optional": true
|
| 2798 |
+
}
|
| 2799 |
+
}
|
| 2800 |
+
},
|
| 2801 |
+
"node_modules/why-is-node-running": {
|
| 2802 |
+
"version": "2.3.0",
|
| 2803 |
+
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
| 2804 |
+
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
| 2805 |
+
"dev": true,
|
| 2806 |
+
"license": "MIT",
|
| 2807 |
+
"dependencies": {
|
| 2808 |
+
"siginfo": "^2.0.0",
|
| 2809 |
+
"stackback": "0.0.2"
|
| 2810 |
+
},
|
| 2811 |
+
"bin": {
|
| 2812 |
+
"why-is-node-running": "cli.js"
|
| 2813 |
+
},
|
| 2814 |
+
"engines": {
|
| 2815 |
+
"node": ">=8"
|
| 2816 |
+
}
|
| 2817 |
+
},
|
| 2818 |
+
"node_modules/yallist": {
|
| 2819 |
+
"version": "3.1.1",
|
| 2820 |
+
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
| 2821 |
+
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
| 2822 |
+
"dev": true,
|
| 2823 |
+
"license": "ISC"
|
| 2824 |
+
}
|
| 2825 |
+
}
|
| 2826 |
+
}
|
package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "ltmarx",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "Video watermarking system with imperceptible 32-bit payload embedding",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"bin": {
|
| 7 |
+
"ltmarx": "./dist/server/cli.js"
|
| 8 |
+
},
|
| 9 |
+
"scripts": {
|
| 10 |
+
"dev": "vite",
|
| 11 |
+
"build": "tsc && vite build",
|
| 12 |
+
"build:cli": "tsc -p tsconfig.server.json",
|
| 13 |
+
"test": "vitest run",
|
| 14 |
+
"test:watch": "vitest"
|
| 15 |
+
},
|
| 16 |
+
"license": "MIT",
|
| 17 |
+
"devDependencies": {
|
| 18 |
+
"@tailwindcss/vite": "^4.2.1",
|
| 19 |
+
"@types/node": "^25.3.0",
|
| 20 |
+
"@types/react": "^19.2.14",
|
| 21 |
+
"@types/react-dom": "^19.2.3",
|
| 22 |
+
"@vitejs/plugin-react": "^5.1.4",
|
| 23 |
+
"tailwindcss": "^4.2.1",
|
| 24 |
+
"typescript": "^5.9.3",
|
| 25 |
+
"vite": "^7.3.1",
|
| 26 |
+
"vitest": "^4.0.18"
|
| 27 |
+
},
|
| 28 |
+
"dependencies": {
|
| 29 |
+
"@ffmpeg/ffmpeg": "^0.12.15",
|
| 30 |
+
"@ffmpeg/util": "^0.12.2",
|
| 31 |
+
"react": "^19.2.4",
|
| 32 |
+
"react-dom": "^19.2.4"
|
| 33 |
+
}
|
| 34 |
+
}
|
server/api.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Optional HTTP API for watermark embedding/detection
|
| 3 |
+
*
|
| 4 |
+
* Serves the web UI and provides API endpoints for server-side processing.
|
| 5 |
+
* Used for HuggingFace Space deployment.
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { createServer } from 'node:http';
|
| 9 |
+
import { readFile, stat } from 'node:fs/promises';
|
| 10 |
+
import { join, extname } from 'node:path';
|
| 11 |
+
import { tmpdir } from 'node:os';
|
| 12 |
+
import { randomUUID } from 'node:crypto';
|
| 13 |
+
import { writeFile, unlink } from 'node:fs/promises';
|
| 14 |
+
import { probeVideo, readYuvFrames, createEncoder } from './ffmpeg-io.js';
|
| 15 |
+
import { embedWatermark } from '../core/embedder.js';
|
| 16 |
+
import { detectWatermark, detectWatermarkMultiFrame } from '../core/detector.js';
|
| 17 |
+
import { getPreset } from '../core/presets.js';
|
| 18 |
+
import type { PresetName } from '../core/types.js';
|
| 19 |
+
|
| 20 |
+
const MIME_TYPES: Record<string, string> = {
|
| 21 |
+
'.html': 'text/html',
|
| 22 |
+
'.js': 'application/javascript',
|
| 23 |
+
'.css': 'text/css',
|
| 24 |
+
'.json': 'application/json',
|
| 25 |
+
'.png': 'image/png',
|
| 26 |
+
'.svg': 'image/svg+xml',
|
| 27 |
+
'.woff2': 'font/woff2',
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
const PORT = parseInt(process.env.PORT || '7860', 10);
|
| 31 |
+
const STATIC_DIR = process.env.STATIC_DIR || join(import.meta.dirname || '.', '../dist/web');
|
| 32 |
+
|
| 33 |
+
async function serveStatic(url: string): Promise<{ data: Buffer; contentType: string } | null> {
|
| 34 |
+
const safePath = url.replace(/\.\./g, '').replace(/\/+/g, '/');
|
| 35 |
+
const filePath = join(STATIC_DIR, safePath === '/' ? 'index.html' : safePath);
|
| 36 |
+
|
| 37 |
+
try {
|
| 38 |
+
const s = await stat(filePath);
|
| 39 |
+
if (!s.isFile()) return null;
|
| 40 |
+
const data = await readFile(filePath);
|
| 41 |
+
const ext = extname(filePath);
|
| 42 |
+
return { data, contentType: MIME_TYPES[ext] || 'application/octet-stream' };
|
| 43 |
+
} catch {
|
| 44 |
+
return null;
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
const server = createServer(async (req, res) => {
|
| 49 |
+
const url = new URL(req.url || '/', `http://localhost:${PORT}`);
|
| 50 |
+
|
| 51 |
+
// CORS + SharedArrayBuffer headers (required for ffmpeg.wasm)
|
| 52 |
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
| 53 |
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
| 54 |
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
| 55 |
+
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
| 56 |
+
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
|
| 57 |
+
|
| 58 |
+
if (req.method === 'OPTIONS') {
|
| 59 |
+
res.writeHead(200);
|
| 60 |
+
res.end();
|
| 61 |
+
return;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
// API: Health check
|
| 65 |
+
if (url.pathname === '/api/health') {
|
| 66 |
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
| 67 |
+
res.end(JSON.stringify({ status: 'ok' }));
|
| 68 |
+
return;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
// API: Embed
|
| 72 |
+
if (url.pathname === '/api/embed' && req.method === 'POST') {
|
| 73 |
+
try {
|
| 74 |
+
const chunks: Buffer[] = [];
|
| 75 |
+
for await (const chunk of req) chunks.push(chunk as Buffer);
|
| 76 |
+
const body = Buffer.concat(chunks);
|
| 77 |
+
|
| 78 |
+
// Parse multipart or raw JSON
|
| 79 |
+
const contentType = req.headers['content-type'] || '';
|
| 80 |
+
|
| 81 |
+
if (contentType.includes('application/json')) {
|
| 82 |
+
const { videoBase64, key, preset, payload } = JSON.parse(body.toString());
|
| 83 |
+
|
| 84 |
+
const videoBuffer = Buffer.from(videoBase64, 'base64');
|
| 85 |
+
const inputPath = join(tmpdir(), `ltmarx-in-${randomUUID()}.mp4`);
|
| 86 |
+
const outputPath = join(tmpdir(), `ltmarx-out-${randomUUID()}.mp4`);
|
| 87 |
+
|
| 88 |
+
await writeFile(inputPath, videoBuffer);
|
| 89 |
+
|
| 90 |
+
const config = getPreset((preset || 'moderate') as PresetName);
|
| 91 |
+
const payloadBytes = hexToBytes(payload || 'DEADBEEF');
|
| 92 |
+
const info = await probeVideo(inputPath);
|
| 93 |
+
|
| 94 |
+
const encoder = createEncoder(outputPath, info.width, info.height, info.fps);
|
| 95 |
+
const ySize = info.width * info.height;
|
| 96 |
+
const uvSize = (info.width / 2) * (info.height / 2);
|
| 97 |
+
let totalPsnr = 0;
|
| 98 |
+
let frameCount = 0;
|
| 99 |
+
|
| 100 |
+
for await (const frame of readYuvFrames(inputPath, info.width, info.height)) {
|
| 101 |
+
const result = embedWatermark(frame.y, info.width, info.height, payloadBytes, key, config);
|
| 102 |
+
totalPsnr += result.psnr;
|
| 103 |
+
|
| 104 |
+
const yuvFrame = Buffer.alloc(ySize + 2 * uvSize);
|
| 105 |
+
yuvFrame.set(result.yPlane, 0);
|
| 106 |
+
yuvFrame.set(frame.u, ySize);
|
| 107 |
+
yuvFrame.set(frame.v, ySize + uvSize);
|
| 108 |
+
encoder.stdin.write(yuvFrame);
|
| 109 |
+
frameCount++;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
encoder.stdin.end();
|
| 113 |
+
await new Promise<void>((resolve) => encoder.process.on('close', () => resolve()));
|
| 114 |
+
|
| 115 |
+
const outputBuffer = await readFile(outputPath);
|
| 116 |
+
|
| 117 |
+
// Cleanup temp files
|
| 118 |
+
await unlink(inputPath).catch(() => {});
|
| 119 |
+
await unlink(outputPath).catch(() => {});
|
| 120 |
+
|
| 121 |
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
| 122 |
+
res.end(JSON.stringify({
|
| 123 |
+
videoBase64: outputBuffer.toString('base64'),
|
| 124 |
+
frames: frameCount,
|
| 125 |
+
avgPsnr: totalPsnr / frameCount,
|
| 126 |
+
}));
|
| 127 |
+
} else {
|
| 128 |
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
| 129 |
+
res.end(JSON.stringify({ error: 'Expected application/json content type' }));
|
| 130 |
+
}
|
| 131 |
+
} catch (e) {
|
| 132 |
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
| 133 |
+
res.end(JSON.stringify({ error: String(e) }));
|
| 134 |
+
}
|
| 135 |
+
return;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
// API: Detect
|
| 139 |
+
if (url.pathname === '/api/detect' && req.method === 'POST') {
|
| 140 |
+
try {
|
| 141 |
+
const chunks: Buffer[] = [];
|
| 142 |
+
for await (const chunk of req) chunks.push(chunk as Buffer);
|
| 143 |
+
const body = JSON.parse(Buffer.concat(chunks).toString());
|
| 144 |
+
|
| 145 |
+
const { videoBase64, key, preset, frames: maxFrames } = body;
|
| 146 |
+
const videoBuffer = Buffer.from(videoBase64, 'base64');
|
| 147 |
+
const inputPath = join(tmpdir(), `ltmarx-det-${randomUUID()}.mp4`);
|
| 148 |
+
|
| 149 |
+
await writeFile(inputPath, videoBuffer);
|
| 150 |
+
|
| 151 |
+
const config = getPreset((preset || 'moderate') as PresetName);
|
| 152 |
+
const info = await probeVideo(inputPath);
|
| 153 |
+
const framesToRead = Math.min(maxFrames || 10, info.totalFrames);
|
| 154 |
+
|
| 155 |
+
const yPlanes: Uint8Array[] = [];
|
| 156 |
+
let count = 0;
|
| 157 |
+
for await (const frame of readYuvFrames(inputPath, info.width, info.height)) {
|
| 158 |
+
yPlanes.push(new Uint8Array(frame.y));
|
| 159 |
+
count++;
|
| 160 |
+
if (count >= framesToRead) break;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
await unlink(inputPath).catch(() => {});
|
| 164 |
+
|
| 165 |
+
const result = detectWatermarkMultiFrame(yPlanes, info.width, info.height, key, config);
|
| 166 |
+
|
| 167 |
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
| 168 |
+
res.end(JSON.stringify({
|
| 169 |
+
detected: result.detected,
|
| 170 |
+
payload: result.payload ? bytesToHex(result.payload) : null,
|
| 171 |
+
confidence: result.confidence,
|
| 172 |
+
tilesDecoded: result.tilesDecoded,
|
| 173 |
+
tilesTotal: result.tilesTotal,
|
| 174 |
+
}));
|
| 175 |
+
} catch (e) {
|
| 176 |
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
| 177 |
+
res.end(JSON.stringify({ error: String(e) }));
|
| 178 |
+
}
|
| 179 |
+
return;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
// Static file serving
|
| 183 |
+
const staticResult = await serveStatic(url.pathname);
|
| 184 |
+
if (staticResult) {
|
| 185 |
+
res.writeHead(200, { 'Content-Type': staticResult.contentType });
|
| 186 |
+
res.end(staticResult.data);
|
| 187 |
+
return;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
// SPA fallback
|
| 191 |
+
const indexResult = await serveStatic('/');
|
| 192 |
+
if (indexResult) {
|
| 193 |
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
| 194 |
+
res.end(indexResult.data);
|
| 195 |
+
return;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
| 199 |
+
res.end('Not Found');
|
| 200 |
+
});
|
| 201 |
+
|
| 202 |
+
server.listen(PORT, () => {
|
| 203 |
+
console.log(`LTMarx server listening on http://localhost:${PORT}`);
|
| 204 |
+
});
|
| 205 |
+
|
| 206 |
+
function hexToBytes(hex: string): Uint8Array {
|
| 207 |
+
const clean = hex.replace(/^0x/, '');
|
| 208 |
+
const padded = clean.length % 2 ? '0' + clean : clean;
|
| 209 |
+
const bytes = new Uint8Array(padded.length / 2);
|
| 210 |
+
for (let i = 0; i < bytes.length; i++) {
|
| 211 |
+
bytes[i] = parseInt(padded.slice(i * 2, i * 2 + 2), 16);
|
| 212 |
+
}
|
| 213 |
+
return bytes;
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
function bytesToHex(bytes: Uint8Array): string {
|
| 217 |
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, '0')).join('').toUpperCase();
|
| 218 |
+
}
|
server/cli.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* LTMarx CLI — Video watermark embedding and detection
|
| 5 |
+
*
|
| 6 |
+
* Usage:
|
| 7 |
+
* ltmarx embed -i input.mp4 -o output.mp4 --key SECRET --preset moderate
|
| 8 |
+
* ltmarx detect -i video.mp4 --key SECRET --preset moderate
|
| 9 |
+
*/
|
| 10 |
+
|
| 11 |
+
import { parseArgs } from 'node:util';
|
| 12 |
+
import { probeVideo, readYuvFrames, createEncoder } from './ffmpeg-io.js';
|
| 13 |
+
import { embedWatermark } from '../core/embedder.js';
|
| 14 |
+
import { detectWatermark, detectWatermarkMultiFrame } from '../core/detector.js';
|
| 15 |
+
import { getPreset, PRESET_DESCRIPTIONS } from '../core/presets.js';
|
| 16 |
+
import type { PresetName } from '../core/types.js';
|
| 17 |
+
|
| 18 |
+
function usage() {
|
| 19 |
+
console.log(`
|
| 20 |
+
LTMarx — Video Watermarking System
|
| 21 |
+
|
| 22 |
+
Commands:
|
| 23 |
+
embed Embed a watermark into a video
|
| 24 |
+
detect Detect and extract a watermark from a video
|
| 25 |
+
presets List available presets
|
| 26 |
+
|
| 27 |
+
Usage:
|
| 28 |
+
ltmarx embed -i input.mp4 -o output.mp4 --key SECRET --preset moderate --payload DEADBEEF
|
| 29 |
+
ltmarx detect -i video.mp4 --key SECRET --preset moderate [--frames N]
|
| 30 |
+
ltmarx presets
|
| 31 |
+
|
| 32 |
+
Options:
|
| 33 |
+
-i, --input Input video file
|
| 34 |
+
-o, --output Output video file (embed only)
|
| 35 |
+
--key Secret key for watermark
|
| 36 |
+
--preset Preset name: light, moderate, strong, fortress
|
| 37 |
+
--payload 32-bit payload as hex string (embed only, default: DEADBEEF)
|
| 38 |
+
--frames Number of frames to analyze (detect only, default: 10)
|
| 39 |
+
--crf Output CRF quality (embed only, default: 18)
|
| 40 |
+
`);
|
| 41 |
+
process.exit(1);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
async function main() {
|
| 45 |
+
const args = process.argv.slice(2);
|
| 46 |
+
const command = args[0];
|
| 47 |
+
|
| 48 |
+
if (!command || command === '--help' || command === '-h') usage();
|
| 49 |
+
|
| 50 |
+
if (command === 'presets') {
|
| 51 |
+
console.log('\nAvailable presets:\n');
|
| 52 |
+
for (const [name, desc] of Object.entries(PRESET_DESCRIPTIONS)) {
|
| 53 |
+
console.log(` ${name.padEnd(12)} ${desc}`);
|
| 54 |
+
}
|
| 55 |
+
console.log();
|
| 56 |
+
process.exit(0);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
const { values } = parseArgs({
|
| 60 |
+
args: args.slice(1),
|
| 61 |
+
options: {
|
| 62 |
+
input: { type: 'string', short: 'i' },
|
| 63 |
+
output: { type: 'string', short: 'o' },
|
| 64 |
+
key: { type: 'string' },
|
| 65 |
+
preset: { type: 'string' },
|
| 66 |
+
payload: { type: 'string' },
|
| 67 |
+
frames: { type: 'string' },
|
| 68 |
+
crf: { type: 'string' },
|
| 69 |
+
},
|
| 70 |
+
});
|
| 71 |
+
|
| 72 |
+
const input = values.input;
|
| 73 |
+
const key = values.key;
|
| 74 |
+
const presetName = (values.preset || 'moderate') as PresetName;
|
| 75 |
+
|
| 76 |
+
if (!input) { console.error('Error: --input is required'); process.exit(1); }
|
| 77 |
+
if (!key) { console.error('Error: --key is required'); process.exit(1); }
|
| 78 |
+
|
| 79 |
+
const config = getPreset(presetName);
|
| 80 |
+
|
| 81 |
+
if (command === 'embed') {
|
| 82 |
+
const output = values.output;
|
| 83 |
+
if (!output) { console.error('Error: --output is required for embed'); process.exit(1); }
|
| 84 |
+
|
| 85 |
+
const payloadHex = values.payload || 'DEADBEEF';
|
| 86 |
+
const payload = hexToBytes(payloadHex);
|
| 87 |
+
const crf = parseInt(values.crf || '18', 10);
|
| 88 |
+
|
| 89 |
+
console.log(`Embedding watermark...`);
|
| 90 |
+
console.log(` Input: ${input}`);
|
| 91 |
+
console.log(` Output: ${output}`);
|
| 92 |
+
console.log(` Preset: ${presetName}`);
|
| 93 |
+
console.log(` Payload: ${payloadHex}`);
|
| 94 |
+
console.log(` CRF: ${crf}`);
|
| 95 |
+
|
| 96 |
+
const info = await probeVideo(input);
|
| 97 |
+
console.log(` Video: ${info.width}x${info.height} @ ${info.fps.toFixed(2)} fps, ${info.totalFrames} frames`);
|
| 98 |
+
|
| 99 |
+
const encoder = createEncoder(output, info.width, info.height, info.fps, crf);
|
| 100 |
+
let frameCount = 0;
|
| 101 |
+
let totalPsnr = 0;
|
| 102 |
+
|
| 103 |
+
const ySize = info.width * info.height;
|
| 104 |
+
const uvSize = (info.width / 2) * (info.height / 2);
|
| 105 |
+
|
| 106 |
+
for await (const frame of readYuvFrames(input, info.width, info.height)) {
|
| 107 |
+
const result = embedWatermark(frame.y, info.width, info.height, payload, key, config);
|
| 108 |
+
totalPsnr += result.psnr;
|
| 109 |
+
|
| 110 |
+
// Write YUV420p frame: watermarked Y + original U + V
|
| 111 |
+
const yuvFrame = Buffer.alloc(ySize + 2 * uvSize);
|
| 112 |
+
yuvFrame.set(result.yPlane, 0);
|
| 113 |
+
yuvFrame.set(frame.u, ySize);
|
| 114 |
+
yuvFrame.set(frame.v, ySize + uvSize);
|
| 115 |
+
|
| 116 |
+
encoder.stdin.write(yuvFrame);
|
| 117 |
+
frameCount++;
|
| 118 |
+
|
| 119 |
+
if (frameCount % 30 === 0) {
|
| 120 |
+
process.stdout.write(`\r Progress: ${frameCount} frames (PSNR: ${(totalPsnr / frameCount).toFixed(1)} dB)`);
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
encoder.stdin.end();
|
| 125 |
+
await new Promise<void>((resolve) => encoder.process.on('close', () => resolve()));
|
| 126 |
+
|
| 127 |
+
console.log(`\r Complete: ${frameCount} frames, avg PSNR: ${(totalPsnr / frameCount).toFixed(1)} dB`);
|
| 128 |
+
console.log(` Output saved to: ${output}`);
|
| 129 |
+
|
| 130 |
+
} else if (command === 'detect') {
|
| 131 |
+
const maxFrames = parseInt(values.frames || '10', 10);
|
| 132 |
+
|
| 133 |
+
console.log(`Detecting watermark...`);
|
| 134 |
+
console.log(` Input: ${input}`);
|
| 135 |
+
console.log(` Preset: ${presetName}`);
|
| 136 |
+
console.log(` Frames: ${maxFrames}`);
|
| 137 |
+
|
| 138 |
+
const info = await probeVideo(input);
|
| 139 |
+
console.log(` Video: ${info.width}x${info.height} @ ${info.fps.toFixed(2)} fps`);
|
| 140 |
+
|
| 141 |
+
const yPlanes: Uint8Array[] = [];
|
| 142 |
+
let frameCount = 0;
|
| 143 |
+
|
| 144 |
+
for await (const frame of readYuvFrames(input, info.width, info.height)) {
|
| 145 |
+
yPlanes.push(new Uint8Array(frame.y));
|
| 146 |
+
frameCount++;
|
| 147 |
+
if (frameCount >= maxFrames) break;
|
| 148 |
+
process.stdout.write(`\r Reading frame ${frameCount}/${maxFrames}...`);
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
console.log(`\r Analyzing ${yPlanes.length} frames...`);
|
| 152 |
+
|
| 153 |
+
// Try multi-frame detection first
|
| 154 |
+
if (yPlanes.length > 1) {
|
| 155 |
+
const result = detectWatermarkMultiFrame(yPlanes, info.width, info.height, key, config);
|
| 156 |
+
if (result.detected) {
|
| 157 |
+
console.log(`\n WATERMARK DETECTED (multi-frame)`);
|
| 158 |
+
console.log(` Payload: ${bytesToHex(result.payload!)}`);
|
| 159 |
+
console.log(` Confidence: ${(result.confidence * 100).toFixed(1)}%`);
|
| 160 |
+
console.log(` Tiles: ${result.tilesDecoded}/${result.tilesTotal}`);
|
| 161 |
+
process.exit(0);
|
| 162 |
+
}
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
// Try single-frame detection
|
| 166 |
+
for (let i = 0; i < yPlanes.length; i++) {
|
| 167 |
+
const result = detectWatermark(yPlanes[i], info.width, info.height, key, config);
|
| 168 |
+
if (result.detected) {
|
| 169 |
+
console.log(`\n WATERMARK DETECTED (frame ${i + 1})`);
|
| 170 |
+
console.log(` Payload: ${bytesToHex(result.payload!)}`);
|
| 171 |
+
console.log(` Confidence: ${(result.confidence * 100).toFixed(1)}%`);
|
| 172 |
+
console.log(` Tiles: ${result.tilesDecoded}/${result.tilesTotal}`);
|
| 173 |
+
process.exit(0);
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
console.log(`\n No watermark detected.`);
|
| 178 |
+
process.exit(1);
|
| 179 |
+
|
| 180 |
+
} else {
|
| 181 |
+
console.error(`Unknown command: ${command}`);
|
| 182 |
+
usage();
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
function hexToBytes(hex: string): Uint8Array {
|
| 187 |
+
const clean = hex.replace(/^0x/, '');
|
| 188 |
+
const padded = clean.length % 2 ? '0' + clean : clean;
|
| 189 |
+
const bytes = new Uint8Array(padded.length / 2);
|
| 190 |
+
for (let i = 0; i < bytes.length; i++) {
|
| 191 |
+
bytes[i] = parseInt(padded.slice(i * 2, i * 2 + 2), 16);
|
| 192 |
+
}
|
| 193 |
+
return bytes;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
function bytesToHex(bytes: Uint8Array): string {
|
| 197 |
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, '0')).join('').toUpperCase();
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
main().catch((e) => {
|
| 201 |
+
console.error('Error:', e.message || e);
|
| 202 |
+
process.exit(1);
|
| 203 |
+
});
|
server/ffmpeg-io.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* FFmpeg-based video I/O for server-side watermark processing
|
| 3 |
+
*
|
| 4 |
+
* Spawns FFmpeg subprocesses to decode/encode raw YUV420p frames.
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
import { spawn, type ChildProcess } from 'node:child_process';
|
| 8 |
+
import { Readable, Writable } from 'node:stream';
|
| 9 |
+
|
| 10 |
+
/** Video metadata */
|
| 11 |
+
export interface VideoInfo {
|
| 12 |
+
width: number;
|
| 13 |
+
height: number;
|
| 14 |
+
fps: number;
|
| 15 |
+
duration: number;
|
| 16 |
+
totalFrames: number;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
/**
|
| 20 |
+
* Probe video file for metadata using ffprobe
|
| 21 |
+
*/
|
| 22 |
+
export async function probeVideo(inputPath: string): Promise<VideoInfo> {
|
| 23 |
+
return new Promise((resolve, reject) => {
|
| 24 |
+
const proc = spawn('ffprobe', [
|
| 25 |
+
'-v', 'quiet',
|
| 26 |
+
'-print_format', 'json',
|
| 27 |
+
'-show_streams',
|
| 28 |
+
'-show_format',
|
| 29 |
+
inputPath,
|
| 30 |
+
]);
|
| 31 |
+
|
| 32 |
+
let stdout = '';
|
| 33 |
+
let stderr = '';
|
| 34 |
+
proc.stdout.on('data', (d: Buffer) => (stdout += d.toString()));
|
| 35 |
+
proc.stderr.on('data', (d: Buffer) => (stderr += d.toString()));
|
| 36 |
+
|
| 37 |
+
proc.on('close', (code) => {
|
| 38 |
+
if (code !== 0) {
|
| 39 |
+
reject(new Error(`ffprobe failed (${code}): ${stderr}`));
|
| 40 |
+
return;
|
| 41 |
+
}
|
| 42 |
+
try {
|
| 43 |
+
const info = JSON.parse(stdout);
|
| 44 |
+
const videoStream = info.streams?.find((s: { codec_type: string }) => s.codec_type === 'video');
|
| 45 |
+
if (!videoStream) throw new Error('No video stream found');
|
| 46 |
+
|
| 47 |
+
const [num, den] = (videoStream.r_frame_rate || '30/1').split('/').map(Number);
|
| 48 |
+
const fps = den ? num / den : 30;
|
| 49 |
+
const duration = parseFloat(info.format?.duration || videoStream.duration || '0');
|
| 50 |
+
const totalFrames = Math.ceil(fps * duration);
|
| 51 |
+
|
| 52 |
+
resolve({
|
| 53 |
+
width: videoStream.width,
|
| 54 |
+
height: videoStream.height,
|
| 55 |
+
fps,
|
| 56 |
+
duration,
|
| 57 |
+
totalFrames,
|
| 58 |
+
});
|
| 59 |
+
} catch (e) {
|
| 60 |
+
reject(new Error(`Failed to parse ffprobe output: ${e}`));
|
| 61 |
+
}
|
| 62 |
+
});
|
| 63 |
+
});
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/**
|
| 67 |
+
* Frame reader: decodes a video file to raw Y planes
|
| 68 |
+
* Yields one Y plane (Uint8Array of width*height) per frame
|
| 69 |
+
*/
|
| 70 |
+
export async function* readYPlanes(
|
| 71 |
+
inputPath: string,
|
| 72 |
+
width: number,
|
| 73 |
+
height: number
|
| 74 |
+
): AsyncGenerator<Uint8Array> {
|
| 75 |
+
const frameSize = width * height; // Y plane only
|
| 76 |
+
const yuvFrameSize = frameSize * 3 / 2; // YUV420p: Y + U/4 + V/4
|
| 77 |
+
|
| 78 |
+
const proc = spawn('ffmpeg', [
|
| 79 |
+
'-i', inputPath,
|
| 80 |
+
'-f', 'rawvideo',
|
| 81 |
+
'-pix_fmt', 'yuv420p',
|
| 82 |
+
'-v', 'error',
|
| 83 |
+
'pipe:1',
|
| 84 |
+
], { stdio: ['ignore', 'pipe', 'pipe'] });
|
| 85 |
+
|
| 86 |
+
let buffer = Buffer.alloc(0);
|
| 87 |
+
|
| 88 |
+
for await (const chunk of proc.stdout as AsyncIterable<Buffer>) {
|
| 89 |
+
buffer = Buffer.concat([buffer, chunk]);
|
| 90 |
+
|
| 91 |
+
while (buffer.length >= yuvFrameSize) {
|
| 92 |
+
// Extract Y plane (first width*height bytes of YUV420p frame)
|
| 93 |
+
const yPlane = new Uint8Array(buffer.subarray(0, frameSize));
|
| 94 |
+
yield yPlane;
|
| 95 |
+
buffer = buffer.subarray(yuvFrameSize);
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
/**
|
| 101 |
+
* Create a write pipe to FFmpeg for encoding watermarked frames
|
| 102 |
+
* Returns a writable stream that accepts YUV420p frame data
|
| 103 |
+
*/
|
| 104 |
+
export function createEncoder(
|
| 105 |
+
outputPath: string,
|
| 106 |
+
width: number,
|
| 107 |
+
height: number,
|
| 108 |
+
fps: number,
|
| 109 |
+
crf: number = 18
|
| 110 |
+
): { stdin: Writable; process: ChildProcess } {
|
| 111 |
+
const proc = spawn('ffmpeg', [
|
| 112 |
+
'-y',
|
| 113 |
+
'-f', 'rawvideo',
|
| 114 |
+
'-pix_fmt', 'yuv420p',
|
| 115 |
+
'-s', `${width}x${height}`,
|
| 116 |
+
'-r', String(fps),
|
| 117 |
+
'-i', 'pipe:0',
|
| 118 |
+
'-c:v', 'libx264',
|
| 119 |
+
'-crf', String(crf),
|
| 120 |
+
'-preset', 'medium',
|
| 121 |
+
'-pix_fmt', 'yuv420p',
|
| 122 |
+
'-v', 'error',
|
| 123 |
+
outputPath,
|
| 124 |
+
], { stdio: ['pipe', 'ignore', 'pipe'] });
|
| 125 |
+
|
| 126 |
+
return { stdin: proc.stdin, process: proc };
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
/**
|
| 130 |
+
* Read all YUV420p frames from video and provide full frame buffers
|
| 131 |
+
* (Y, U, V planes) for pass-through of chroma channels
|
| 132 |
+
*/
|
| 133 |
+
export async function* readYuvFrames(
|
| 134 |
+
inputPath: string,
|
| 135 |
+
width: number,
|
| 136 |
+
height: number
|
| 137 |
+
): AsyncGenerator<{ y: Uint8Array; u: Uint8Array; v: Uint8Array }> {
|
| 138 |
+
const ySize = width * height;
|
| 139 |
+
const uvSize = (width / 2) * (height / 2);
|
| 140 |
+
const frameSize = ySize + 2 * uvSize;
|
| 141 |
+
|
| 142 |
+
const proc = spawn('ffmpeg', [
|
| 143 |
+
'-i', inputPath,
|
| 144 |
+
'-f', 'rawvideo',
|
| 145 |
+
'-pix_fmt', 'yuv420p',
|
| 146 |
+
'-v', 'error',
|
| 147 |
+
'pipe:1',
|
| 148 |
+
], { stdio: ['ignore', 'pipe', 'pipe'] });
|
| 149 |
+
|
| 150 |
+
let buffer = Buffer.alloc(0);
|
| 151 |
+
|
| 152 |
+
for await (const chunk of proc.stdout as AsyncIterable<Buffer>) {
|
| 153 |
+
buffer = Buffer.concat([buffer, chunk]);
|
| 154 |
+
|
| 155 |
+
while (buffer.length >= frameSize) {
|
| 156 |
+
const y = new Uint8Array(buffer.subarray(0, ySize));
|
| 157 |
+
const u = new Uint8Array(buffer.subarray(ySize, ySize + uvSize));
|
| 158 |
+
const v = new Uint8Array(buffer.subarray(ySize + uvSize, frameSize));
|
| 159 |
+
yield { y, u, v };
|
| 160 |
+
buffer = buffer.subarray(frameSize);
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
}
|
tsconfig.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2022",
|
| 4 |
+
"module": "ESNext",
|
| 5 |
+
"moduleResolution": "bundler",
|
| 6 |
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
| 7 |
+
"jsx": "react-jsx",
|
| 8 |
+
"strict": true,
|
| 9 |
+
"esModuleInterop": true,
|
| 10 |
+
"skipLibCheck": true,
|
| 11 |
+
"forceConsistentCasingInFileNames": true,
|
| 12 |
+
"resolveJsonModule": true,
|
| 13 |
+
"isolatedModules": true,
|
| 14 |
+
"noEmit": true,
|
| 15 |
+
"declaration": true,
|
| 16 |
+
"declarationMap": true,
|
| 17 |
+
"sourceMap": true,
|
| 18 |
+
"outDir": "./dist",
|
| 19 |
+
"rootDir": ".",
|
| 20 |
+
"baseUrl": ".",
|
| 21 |
+
"paths": {
|
| 22 |
+
"@core/*": ["./core/*"]
|
| 23 |
+
}
|
| 24 |
+
},
|
| 25 |
+
"include": ["core/**/*.ts", "web/src/**/*.ts", "web/src/**/*.tsx", "server/**/*.ts", "tests/**/*.ts"],
|
| 26 |
+
"exclude": ["node_modules", "dist"]
|
| 27 |
+
}
|
tsconfig.server.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2022",
|
| 4 |
+
"module": "ESNext",
|
| 5 |
+
"moduleResolution": "bundler",
|
| 6 |
+
"lib": ["ES2022"],
|
| 7 |
+
"strict": true,
|
| 8 |
+
"esModuleInterop": true,
|
| 9 |
+
"skipLibCheck": true,
|
| 10 |
+
"forceConsistentCasingInFileNames": true,
|
| 11 |
+
"resolveJsonModule": true,
|
| 12 |
+
"isolatedModules": true,
|
| 13 |
+
"declaration": true,
|
| 14 |
+
"sourceMap": true,
|
| 15 |
+
"outDir": "./dist",
|
| 16 |
+
"rootDir": ".",
|
| 17 |
+
"baseUrl": ".",
|
| 18 |
+
"paths": {
|
| 19 |
+
"@core/*": ["./core/*"]
|
| 20 |
+
}
|
| 21 |
+
},
|
| 22 |
+
"include": ["core/**/*.ts", "server/**/*.ts"],
|
| 23 |
+
"exclude": ["node_modules", "dist"]
|
| 24 |
+
}
|
vite.config.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite';
|
| 2 |
+
import react from '@vitejs/plugin-react';
|
| 3 |
+
import tailwindcss from '@tailwindcss/vite';
|
| 4 |
+
import { resolve } from 'path';
|
| 5 |
+
|
| 6 |
+
export default defineConfig({
|
| 7 |
+
plugins: [react(), tailwindcss()],
|
| 8 |
+
root: 'web',
|
| 9 |
+
resolve: {
|
| 10 |
+
alias: {
|
| 11 |
+
'@core': resolve(__dirname, 'core'),
|
| 12 |
+
},
|
| 13 |
+
},
|
| 14 |
+
build: {
|
| 15 |
+
outDir: '../dist/web',
|
| 16 |
+
emptyOutDir: true,
|
| 17 |
+
},
|
| 18 |
+
worker: {
|
| 19 |
+
format: 'es',
|
| 20 |
+
},
|
| 21 |
+
server: {
|
| 22 |
+
headers: {
|
| 23 |
+
// Required for ffmpeg.wasm SharedArrayBuffer support
|
| 24 |
+
'Cross-Origin-Opener-Policy': 'same-origin',
|
| 25 |
+
'Cross-Origin-Embedder-Policy': 'require-corp',
|
| 26 |
+
},
|
| 27 |
+
},
|
| 28 |
+
optimizeDeps: {
|
| 29 |
+
exclude: ['@ffmpeg/ffmpeg', '@ffmpeg/util'],
|
| 30 |
+
},
|
| 31 |
+
});
|
web/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
web/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" class="dark">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>LTMarX — Video Watermarking</title>
|
| 7 |
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎬</text></svg>" />
|
| 8 |
+
</head>
|
| 9 |
+
<body class="bg-zinc-950 text-zinc-100 antialiased">
|
| 10 |
+
<div id="root"></div>
|
| 11 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
web/src/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
web/src/App.tsx
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react';
|
| 2 |
+
import EmbedPanel from './components/EmbedPanel.js';
|
| 3 |
+
import DetectPanel from './components/DetectPanel.js';
|
| 4 |
+
import ApiDocs from './components/ApiDocs.js';
|
| 5 |
+
|
| 6 |
+
export default function App() {
|
| 7 |
+
const [tab, setTab] = useState<'embed' | 'detect' | 'api'>('embed');
|
| 8 |
+
|
| 9 |
+
return (
|
| 10 |
+
<div className="min-h-screen bg-zinc-950">
|
| 11 |
+
<header className="sticky top-0 z-10 border-b border-zinc-800/50 bg-zinc-950/80 px-6 py-4 backdrop-blur-xl">
|
| 12 |
+
<div className="mx-auto flex max-w-2xl items-center justify-between">
|
| 13 |
+
<div className="flex items-center gap-3">
|
| 14 |
+
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-blue-600 to-violet-600 shadow-lg shadow-blue-600/20">
|
| 15 |
+
<svg className="h-4 w-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
| 16 |
+
<path strokeLinecap="round" strokeLinejoin="round" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
| 17 |
+
</svg>
|
| 18 |
+
</div>
|
| 19 |
+
<div>
|
| 20 |
+
<h1 className="text-lg font-bold tracking-tight text-zinc-100">
|
| 21 |
+
LTMar<span className="text-blue-400">X</span>
|
| 22 |
+
</h1>
|
| 23 |
+
<p className="text-[10px] text-zinc-600">The watermark bits your generated video didn't even know it needs</p>
|
| 24 |
+
</div>
|
| 25 |
+
</div>
|
| 26 |
+
<nav className="flex gap-1 rounded-xl bg-zinc-900/80 p-1 ring-1 ring-zinc-800/50">
|
| 27 |
+
<button
|
| 28 |
+
onClick={() => setTab('embed')}
|
| 29 |
+
className={`rounded-lg px-4 py-1.5 text-sm font-medium transition-all ${
|
| 30 |
+
tab === 'embed'
|
| 31 |
+
? 'bg-zinc-800 text-zinc-100 shadow-sm'
|
| 32 |
+
: 'text-zinc-400 hover:text-zinc-200'
|
| 33 |
+
}`}
|
| 34 |
+
>
|
| 35 |
+
🎨 Embed
|
| 36 |
+
</button>
|
| 37 |
+
<button
|
| 38 |
+
onClick={() => setTab('detect')}
|
| 39 |
+
className={`rounded-lg px-4 py-1.5 text-sm font-medium transition-all ${
|
| 40 |
+
tab === 'detect'
|
| 41 |
+
? 'bg-zinc-800 text-zinc-100 shadow-sm'
|
| 42 |
+
: 'text-zinc-400 hover:text-zinc-200'
|
| 43 |
+
}`}
|
| 44 |
+
>
|
| 45 |
+
🔍 Detect
|
| 46 |
+
</button>
|
| 47 |
+
<button
|
| 48 |
+
onClick={() => setTab('api')}
|
| 49 |
+
className={`rounded-lg px-4 py-1.5 text-sm font-medium transition-all ${
|
| 50 |
+
tab === 'api'
|
| 51 |
+
? 'bg-zinc-800 text-zinc-100 shadow-sm'
|
| 52 |
+
: 'text-zinc-400 hover:text-zinc-200'
|
| 53 |
+
}`}
|
| 54 |
+
>
|
| 55 |
+
{'{}'} API
|
| 56 |
+
</button>
|
| 57 |
+
</nav>
|
| 58 |
+
</div>
|
| 59 |
+
</header>
|
| 60 |
+
|
| 61 |
+
<main className="mx-auto max-w-2xl px-6 py-10">
|
| 62 |
+
<div className="mb-8">
|
| 63 |
+
<h2 className="text-2xl font-bold tracking-tight text-zinc-100">
|
| 64 |
+
{tab === 'embed' ? '🎬 Embed Watermark' : tab === 'detect' ? '🔎 Detect Watermark' : '📖 API Reference'}
|
| 65 |
+
</h2>
|
| 66 |
+
<p className="mt-1 text-sm text-zinc-500">
|
| 67 |
+
{tab === 'embed'
|
| 68 |
+
? 'Embed an imperceptible 32-bit payload into your video.'
|
| 69 |
+
: tab === 'detect'
|
| 70 |
+
? 'Analyze a video to detect and extract embedded watermarks.'
|
| 71 |
+
: 'Integrate LTMarX into your application.'}
|
| 72 |
+
</p>
|
| 73 |
+
</div>
|
| 74 |
+
|
| 75 |
+
{tab === 'embed' ? <EmbedPanel /> : tab === 'detect' ? <DetectPanel /> : <ApiDocs />}
|
| 76 |
+
</main>
|
| 77 |
+
|
| 78 |
+
<footer className="border-t border-zinc-800/30 px-6 py-6">
|
| 79 |
+
<div className="mx-auto max-w-2xl">
|
| 80 |
+
<p className="text-center text-[11px] text-zinc-700">
|
| 81 |
+
LTMar<span className="text-zinc-600">X</span> — DWT/DCT watermarking with DM-QIM and BCH error correction.
|
| 82 |
+
All processing happens in your browser.
|
| 83 |
+
</p>
|
| 84 |
+
</div>
|
| 85 |
+
</footer>
|
| 86 |
+
</div>
|
| 87 |
+
);
|
| 88 |
+
}
|
web/src/components/ApiDocs.tsx
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react';
|
| 2 |
+
|
| 3 |
+
interface CodeBlockProps {
|
| 4 |
+
lang: string;
|
| 5 |
+
children: string;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
function CodeBlock({ lang, children }: CodeBlockProps) {
|
| 9 |
+
const [copied, setCopied] = useState(false);
|
| 10 |
+
const copy = () => {
|
| 11 |
+
navigator.clipboard.writeText(children.trim());
|
| 12 |
+
setCopied(true);
|
| 13 |
+
setTimeout(() => setCopied(false), 1500);
|
| 14 |
+
};
|
| 15 |
+
return (
|
| 16 |
+
<div className="group relative">
|
| 17 |
+
<div className="flex items-center justify-between rounded-t-lg bg-zinc-800/80 px-3 py-1.5">
|
| 18 |
+
<span className="text-[10px] font-medium uppercase tracking-wider text-zinc-500">{lang}</span>
|
| 19 |
+
<button
|
| 20 |
+
onClick={copy}
|
| 21 |
+
className="text-[10px] text-zinc-500 transition-colors hover:text-zinc-300"
|
| 22 |
+
>
|
| 23 |
+
{copied ? 'Copied!' : 'Copy'}
|
| 24 |
+
</button>
|
| 25 |
+
</div>
|
| 26 |
+
<pre className="overflow-x-auto rounded-b-lg bg-zinc-900/80 p-3 text-xs leading-relaxed text-zinc-300 ring-1 ring-zinc-800/50">
|
| 27 |
+
<code>{children.trim()}</code>
|
| 28 |
+
</pre>
|
| 29 |
+
</div>
|
| 30 |
+
);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
| 34 |
+
return (
|
| 35 |
+
<div className="space-y-3">
|
| 36 |
+
<h3 className="text-sm font-semibold text-zinc-200">{title}</h3>
|
| 37 |
+
{children}
|
| 38 |
+
</div>
|
| 39 |
+
);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
function Endpoint({ method, path, desc }: { method: string; path: string; desc: string }) {
|
| 43 |
+
const color = method === 'POST' ? 'text-amber-400 bg-amber-400/10' : 'text-emerald-400 bg-emerald-400/10';
|
| 44 |
+
return (
|
| 45 |
+
<div className="flex items-start gap-2">
|
| 46 |
+
<span className={`mt-0.5 shrink-0 rounded px-1.5 py-0.5 text-[10px] font-bold ${color}`}>{method}</span>
|
| 47 |
+
<div>
|
| 48 |
+
<code className="text-xs font-medium text-zinc-200">{path}</code>
|
| 49 |
+
<p className="mt-0.5 text-xs text-zinc-500">{desc}</p>
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
| 52 |
+
);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
function ParamTable({ params }: { params: { name: string; type: string; desc: string; required?: boolean }[] }) {
|
| 56 |
+
return (
|
| 57 |
+
<div className="overflow-hidden rounded-lg ring-1 ring-zinc-800/50">
|
| 58 |
+
<table className="w-full text-xs">
|
| 59 |
+
<thead>
|
| 60 |
+
<tr className="border-b border-zinc-800/50 bg-zinc-900/50">
|
| 61 |
+
<th className="px-3 py-1.5 text-left font-medium text-zinc-400">Parameter</th>
|
| 62 |
+
<th className="px-3 py-1.5 text-left font-medium text-zinc-400">Type</th>
|
| 63 |
+
<th className="px-3 py-1.5 text-left font-medium text-zinc-400">Description</th>
|
| 64 |
+
</tr>
|
| 65 |
+
</thead>
|
| 66 |
+
<tbody>
|
| 67 |
+
{params.map((p) => (
|
| 68 |
+
<tr key={p.name} className="border-b border-zinc-800/30 last:border-0">
|
| 69 |
+
<td className="px-3 py-1.5">
|
| 70 |
+
<code className="text-zinc-200">{p.name}</code>
|
| 71 |
+
{p.required && <span className="ml-1 text-[9px] text-red-400">*</span>}
|
| 72 |
+
</td>
|
| 73 |
+
<td className="px-3 py-1.5 text-zinc-500">{p.type}</td>
|
| 74 |
+
<td className="px-3 py-1.5 text-zinc-400">{p.desc}</td>
|
| 75 |
+
</tr>
|
| 76 |
+
))}
|
| 77 |
+
</tbody>
|
| 78 |
+
</table>
|
| 79 |
+
</div>
|
| 80 |
+
);
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
export default function ApiDocs() {
|
| 84 |
+
return (
|
| 85 |
+
<div className="space-y-8">
|
| 86 |
+
{/* Core Library */}
|
| 87 |
+
<Section title="Core Library (TypeScript)">
|
| 88 |
+
<p className="text-xs text-zinc-400">
|
| 89 |
+
The core engine is isomorphic — same code runs in the browser and on Node.js.
|
| 90 |
+
It operates on raw Y-plane (luminance) buffers with no platform dependencies.
|
| 91 |
+
</p>
|
| 92 |
+
|
| 93 |
+
<div className="space-y-4">
|
| 94 |
+
<div className="space-y-2">
|
| 95 |
+
<h4 className="text-xs font-medium text-zinc-300">Embed a watermark</h4>
|
| 96 |
+
<CodeBlock lang="typescript">{`
|
| 97 |
+
import { embedWatermark } from '@core/embedder';
|
| 98 |
+
import { getPreset } from '@core/presets';
|
| 99 |
+
|
| 100 |
+
const config = getPreset('moderate');
|
| 101 |
+
const payload = new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF]);
|
| 102 |
+
|
| 103 |
+
const result = embedWatermark(yPlane, width, height, payload, secretKey, config);
|
| 104 |
+
// result.yPlane → Uint8Array (watermarked luminance)
|
| 105 |
+
// result.psnr → number (quality in dB, higher = less visible)
|
| 106 |
+
`}</CodeBlock>
|
| 107 |
+
</div>
|
| 108 |
+
|
| 109 |
+
<div className="space-y-2">
|
| 110 |
+
<h4 className="text-xs font-medium text-zinc-300">Detect a watermark</h4>
|
| 111 |
+
<CodeBlock lang="typescript">{`
|
| 112 |
+
import { detectWatermarkMultiFrame } from '@core/detector';
|
| 113 |
+
import { getPreset } from '@core/presets';
|
| 114 |
+
|
| 115 |
+
const config = getPreset('moderate');
|
| 116 |
+
const result = detectWatermarkMultiFrame(yPlanes, width, height, secretKey, config);
|
| 117 |
+
// result.detected → boolean
|
| 118 |
+
// result.payload → Uint8Array | null (the 4-byte payload)
|
| 119 |
+
// result.confidence → number (0–1)
|
| 120 |
+
// result.tilesDecoded → number
|
| 121 |
+
`}</CodeBlock>
|
| 122 |
+
</div>
|
| 123 |
+
|
| 124 |
+
<div className="space-y-2">
|
| 125 |
+
<h4 className="text-xs font-medium text-zinc-300">Auto-detect (tries all presets)</h4>
|
| 126 |
+
<CodeBlock lang="typescript">{`
|
| 127 |
+
import { autoDetectMultiFrame } from '@core/detector';
|
| 128 |
+
|
| 129 |
+
const result = autoDetectMultiFrame(yPlanes, width, height, secretKey);
|
| 130 |
+
// result.presetUsed → 'light' | 'moderate' | 'strong' | 'fortress' | null
|
| 131 |
+
`}</CodeBlock>
|
| 132 |
+
</div>
|
| 133 |
+
</div>
|
| 134 |
+
</Section>
|
| 135 |
+
|
| 136 |
+
{/* HTTP API */}
|
| 137 |
+
<Section title="HTTP API">
|
| 138 |
+
<p className="text-xs text-zinc-400">
|
| 139 |
+
Available when running the server via <code className="text-zinc-300">tsx server/api.ts</code> or Docker.
|
| 140 |
+
</p>
|
| 141 |
+
|
| 142 |
+
<div className="space-y-4">
|
| 143 |
+
<div className="space-y-2">
|
| 144 |
+
<Endpoint method="POST" path="/api/embed" desc="Embed a watermark into a video" />
|
| 145 |
+
<ParamTable params={[
|
| 146 |
+
{ name: 'videoBase64', type: 'string', desc: 'Base64-encoded video file', required: true },
|
| 147 |
+
{ name: 'key', type: 'string', desc: 'Secret key for embedding', required: true },
|
| 148 |
+
{ name: 'preset', type: 'string', desc: 'light | moderate | strong | fortress', required: true },
|
| 149 |
+
{ name: 'payload', type: 'string', desc: 'Hex string, up to 8 chars (32 bits)', required: true },
|
| 150 |
+
]} />
|
| 151 |
+
<CodeBlock lang="bash">{`
|
| 152 |
+
curl -X POST http://localhost:7860/api/embed \\
|
| 153 |
+
-H "Content-Type: application/json" \\
|
| 154 |
+
-d '{
|
| 155 |
+
"videoBase64": "'$(base64 -i input.mp4)'",
|
| 156 |
+
"key": "my-secret",
|
| 157 |
+
"preset": "moderate",
|
| 158 |
+
"payload": "DEADBEEF"
|
| 159 |
+
}'
|
| 160 |
+
`}</CodeBlock>
|
| 161 |
+
</div>
|
| 162 |
+
|
| 163 |
+
<div className="space-y-2">
|
| 164 |
+
<Endpoint method="POST" path="/api/detect" desc="Detect and extract a watermark from a video" />
|
| 165 |
+
<ParamTable params={[
|
| 166 |
+
{ name: 'videoBase64', type: 'string', desc: 'Base64-encoded video file', required: true },
|
| 167 |
+
{ name: 'key', type: 'string', desc: 'Secret key used during embedding', required: true },
|
| 168 |
+
{ name: 'preset', type: 'string', desc: 'Preset to try (omit to try all)', required: false },
|
| 169 |
+
{ name: 'frames', type: 'number', desc: 'Max frames to analyze (default: 10)', required: false },
|
| 170 |
+
]} />
|
| 171 |
+
<CodeBlock lang="json">{`
|
| 172 |
+
{
|
| 173 |
+
"detected": true,
|
| 174 |
+
"payload": "DEADBEEF",
|
| 175 |
+
"confidence": 0.97,
|
| 176 |
+
"preset": "moderate",
|
| 177 |
+
"tilesDecoded": 12,
|
| 178 |
+
"tilesTotal": 16
|
| 179 |
+
}
|
| 180 |
+
`}</CodeBlock>
|
| 181 |
+
</div>
|
| 182 |
+
|
| 183 |
+
<Endpoint method="GET" path="/api/health" desc="Health check — returns { status: 'ok' }" />
|
| 184 |
+
</div>
|
| 185 |
+
</Section>
|
| 186 |
+
|
| 187 |
+
{/* CLI */}
|
| 188 |
+
<Section title="CLI">
|
| 189 |
+
<p className="text-xs text-zinc-400">
|
| 190 |
+
Requires Node.js and FFmpeg installed locally.
|
| 191 |
+
</p>
|
| 192 |
+
|
| 193 |
+
<div className="space-y-2">
|
| 194 |
+
<CodeBlock lang="bash">{`
|
| 195 |
+
# Embed a watermark
|
| 196 |
+
npx tsx server/cli.ts embed \\
|
| 197 |
+
-i input.mp4 -o output.mp4 \\
|
| 198 |
+
--key SECRET --preset moderate --payload DEADBEEF
|
| 199 |
+
|
| 200 |
+
# Detect a watermark (auto-tries all presets)
|
| 201 |
+
npx tsx server/cli.ts detect -i video.mp4 --key SECRET
|
| 202 |
+
|
| 203 |
+
# List available presets
|
| 204 |
+
npx tsx server/cli.ts presets
|
| 205 |
+
`}</CodeBlock>
|
| 206 |
+
</div>
|
| 207 |
+
</Section>
|
| 208 |
+
|
| 209 |
+
{/* Presets reference */}
|
| 210 |
+
<Section title="Presets">
|
| 211 |
+
<div className="overflow-hidden rounded-lg ring-1 ring-zinc-800/50">
|
| 212 |
+
<table className="w-full text-xs">
|
| 213 |
+
<thead>
|
| 214 |
+
<tr className="border-b border-zinc-800/50 bg-zinc-900/50">
|
| 215 |
+
<th className="px-3 py-1.5 text-left font-medium text-zinc-400">Preset</th>
|
| 216 |
+
<th className="px-3 py-1.5 text-left font-medium text-zinc-400">Delta</th>
|
| 217 |
+
<th className="px-3 py-1.5 text-left font-medium text-zinc-400">BCH</th>
|
| 218 |
+
<th className="px-3 py-1.5 text-left font-medium text-zinc-400">Masking</th>
|
| 219 |
+
<th className="px-3 py-1.5 text-left font-medium text-zinc-400">Use case</th>
|
| 220 |
+
</tr>
|
| 221 |
+
</thead>
|
| 222 |
+
<tbody>
|
| 223 |
+
{[
|
| 224 |
+
{ name: 'Light', delta: 50, bch: '(63,36,5)', mask: 'No', use: 'Near-invisible, mild compression' },
|
| 225 |
+
{ name: 'Moderate', delta: 80, bch: '(63,36,5)', mask: 'Yes', use: 'Balanced with perceptual masking' },
|
| 226 |
+
{ name: 'Strong', delta: 110, bch: '(63,36,5)', mask: 'Yes', use: 'More frequencies, handles rescaling' },
|
| 227 |
+
{ name: 'Fortress', delta: 150, bch: '(63,36,5)', mask: 'Yes', use: 'Maximum robustness' },
|
| 228 |
+
].map((p) => (
|
| 229 |
+
<tr key={p.name} className="border-b border-zinc-800/30 last:border-0">
|
| 230 |
+
<td className="px-3 py-1.5 font-medium text-zinc-200">{p.name}</td>
|
| 231 |
+
<td className="px-3 py-1.5 tabular-nums text-zinc-400">{p.delta}</td>
|
| 232 |
+
<td className="px-3 py-1.5 font-mono text-zinc-400">{p.bch}</td>
|
| 233 |
+
<td className="px-3 py-1.5 text-zinc-400">{p.mask}</td>
|
| 234 |
+
<td className="px-3 py-1.5 text-zinc-400">{p.use}</td>
|
| 235 |
+
</tr>
|
| 236 |
+
))}
|
| 237 |
+
</tbody>
|
| 238 |
+
</table>
|
| 239 |
+
</div>
|
| 240 |
+
</Section>
|
| 241 |
+
|
| 242 |
+
{/* How it works */}
|
| 243 |
+
<Section title="How It Works">
|
| 244 |
+
<div className="space-y-3 text-xs text-zinc-400">
|
| 245 |
+
<div className="rounded-lg bg-zinc-900/50 p-3 ring-1 ring-zinc-800/50">
|
| 246 |
+
<p className="mb-2 font-medium text-zinc-300">Embedding pipeline</p>
|
| 247 |
+
<code className="block leading-relaxed text-zinc-400">
|
| 248 |
+
Y plane → 2-level Haar DWT → HL subband → tile grid →<br />
|
| 249 |
+
per tile: 8x8 DCT → select mid-freq coefficients →<br />
|
| 250 |
+
DM-QIM embed coded bits → inverse DCT → inverse DWT
|
| 251 |
+
</code>
|
| 252 |
+
</div>
|
| 253 |
+
<div className="rounded-lg bg-zinc-900/50 p-3 ring-1 ring-zinc-800/50">
|
| 254 |
+
<p className="mb-2 font-medium text-zinc-300">Payload encoding</p>
|
| 255 |
+
<code className="block leading-relaxed text-zinc-400">
|
| 256 |
+
32-bit payload → CRC append → BCH encode → keyed interleave → map to coefficients
|
| 257 |
+
</code>
|
| 258 |
+
</div>
|
| 259 |
+
<div className="rounded-lg bg-zinc-900/50 p-3 ring-1 ring-zinc-800/50">
|
| 260 |
+
<p className="mb-2 font-medium text-zinc-300">Detection</p>
|
| 261 |
+
<code className="block leading-relaxed text-zinc-400">
|
| 262 |
+
Y plane → DWT → HL subband → tile grid →<br />
|
| 263 |
+
per tile: DCT → DM-QIM soft extract →<br />
|
| 264 |
+
soft-combine across tiles + frames → BCH decode → CRC verify
|
| 265 |
+
</code>
|
| 266 |
+
</div>
|
| 267 |
+
<p>
|
| 268 |
+
The watermark is embedded exclusively in the <strong className="text-zinc-300">luminance (Y) channel</strong> within
|
| 269 |
+
the DWT-domain DCT coefficients. This makes it invisible to the human eye while surviving
|
| 270 |
+
lossy compression, rescaling, and color adjustments. Each tile carries a complete copy of
|
| 271 |
+
the payload for redundancy — more tiles means better detection under degradation.
|
| 272 |
+
</p>
|
| 273 |
+
</div>
|
| 274 |
+
</Section>
|
| 275 |
+
</div>
|
| 276 |
+
);
|
| 277 |
+
}
|
web/src/components/ComparisonView.tsx
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useRef, useEffect, useState } from 'react';
|
| 2 |
+
|
| 3 |
+
interface ComparisonViewProps {
|
| 4 |
+
originalFrames: ImageData[];
|
| 5 |
+
watermarkedFrames: ImageData[];
|
| 6 |
+
width: number;
|
| 7 |
+
height: number;
|
| 8 |
+
fps: number;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export default function ComparisonView({
|
| 12 |
+
originalFrames,
|
| 13 |
+
watermarkedFrames,
|
| 14 |
+
width,
|
| 15 |
+
height,
|
| 16 |
+
fps,
|
| 17 |
+
}: ComparisonViewProps) {
|
| 18 |
+
const [mode, setMode] = useState<'side-by-side' | 'difference'>('side-by-side');
|
| 19 |
+
const [amplify, setAmplify] = useState(1);
|
| 20 |
+
const [playing, setPlaying] = useState(false);
|
| 21 |
+
const [currentFrame, setCurrentFrame] = useState(0);
|
| 22 |
+
const diffCanvasRef = useRef<HTMLCanvasElement>(null);
|
| 23 |
+
const origCanvasRef = useRef<HTMLCanvasElement>(null);
|
| 24 |
+
const wmCanvasRef = useRef<HTMLCanvasElement>(null);
|
| 25 |
+
const animRef = useRef<number>(0);
|
| 26 |
+
const lastFrameTime = useRef(0);
|
| 27 |
+
|
| 28 |
+
const totalFrames = originalFrames.length;
|
| 29 |
+
|
| 30 |
+
// Render side-by-side canvases for current frame
|
| 31 |
+
useEffect(() => {
|
| 32 |
+
if (mode !== 'side-by-side') return;
|
| 33 |
+
const origCtx = origCanvasRef.current?.getContext('2d');
|
| 34 |
+
const wmCtx = wmCanvasRef.current?.getContext('2d');
|
| 35 |
+
if (origCtx && originalFrames[currentFrame]) {
|
| 36 |
+
origCtx.putImageData(originalFrames[currentFrame], 0, 0);
|
| 37 |
+
}
|
| 38 |
+
if (wmCtx && watermarkedFrames[currentFrame]) {
|
| 39 |
+
wmCtx.putImageData(watermarkedFrames[currentFrame], 0, 0);
|
| 40 |
+
}
|
| 41 |
+
}, [originalFrames, watermarkedFrames, mode, currentFrame]);
|
| 42 |
+
|
| 43 |
+
// Render diff frame
|
| 44 |
+
useEffect(() => {
|
| 45 |
+
if (mode !== 'difference') return;
|
| 46 |
+
const ctx = diffCanvasRef.current?.getContext('2d');
|
| 47 |
+
if (!ctx || !originalFrames[currentFrame] || !watermarkedFrames[currentFrame]) return;
|
| 48 |
+
|
| 49 |
+
const orig = originalFrames[currentFrame];
|
| 50 |
+
const wm = watermarkedFrames[currentFrame];
|
| 51 |
+
const diff = new ImageData(width, height);
|
| 52 |
+
|
| 53 |
+
for (let i = 0; i < orig.data.length; i += 4) {
|
| 54 |
+
const dr = Math.abs(orig.data[i] - wm.data[i]);
|
| 55 |
+
const dg = Math.abs(orig.data[i + 1] - wm.data[i + 1]);
|
| 56 |
+
const db = Math.abs(orig.data[i + 2] - wm.data[i + 2]);
|
| 57 |
+
const d = Math.max(dr, dg, db);
|
| 58 |
+
const amplified = Math.min(255, d * amplify);
|
| 59 |
+
diff.data[i] = amplified;
|
| 60 |
+
diff.data[i + 1] = 0;
|
| 61 |
+
diff.data[i + 2] = 255 - amplified;
|
| 62 |
+
diff.data[i + 3] = 255;
|
| 63 |
+
}
|
| 64 |
+
ctx.putImageData(diff, 0, 0);
|
| 65 |
+
}, [originalFrames, watermarkedFrames, mode, amplify, currentFrame, width, height]);
|
| 66 |
+
|
| 67 |
+
// Playback loop
|
| 68 |
+
useEffect(() => {
|
| 69 |
+
if (!playing) return;
|
| 70 |
+
const interval = 1000 / Math.min(fps, 5);
|
| 71 |
+
|
| 72 |
+
const step = (time: number) => {
|
| 73 |
+
if (time - lastFrameTime.current >= interval) {
|
| 74 |
+
setCurrentFrame((f) => (f + 1) % totalFrames);
|
| 75 |
+
lastFrameTime.current = time;
|
| 76 |
+
}
|
| 77 |
+
animRef.current = requestAnimationFrame(step);
|
| 78 |
+
};
|
| 79 |
+
animRef.current = requestAnimationFrame(step);
|
| 80 |
+
|
| 81 |
+
return () => cancelAnimationFrame(animRef.current);
|
| 82 |
+
}, [playing, fps, totalFrames]);
|
| 83 |
+
|
| 84 |
+
const aspect = `${width} / ${height}`;
|
| 85 |
+
|
| 86 |
+
return (
|
| 87 |
+
<div className="space-y-3">
|
| 88 |
+
<div className="flex items-center justify-between">
|
| 89 |
+
<div className="flex items-center gap-2">
|
| 90 |
+
<div className="flex gap-1 rounded-md bg-zinc-800/50 p-0.5">
|
| 91 |
+
<button
|
| 92 |
+
onClick={() => setMode('side-by-side')}
|
| 93 |
+
className={`rounded-md px-3 py-1 text-xs font-medium transition-colors ${
|
| 94 |
+
mode === 'side-by-side' ? 'bg-zinc-700 text-zinc-100' : 'text-zinc-400 hover:text-zinc-200'
|
| 95 |
+
}`}
|
| 96 |
+
>
|
| 97 |
+
Side by Side
|
| 98 |
+
</button>
|
| 99 |
+
<button
|
| 100 |
+
onClick={() => setMode('difference')}
|
| 101 |
+
className={`rounded-md px-3 py-1 text-xs font-medium transition-colors ${
|
| 102 |
+
mode === 'difference' ? 'bg-zinc-700 text-zinc-100' : 'text-zinc-400 hover:text-zinc-200'
|
| 103 |
+
}`}
|
| 104 |
+
>
|
| 105 |
+
Difference
|
| 106 |
+
</button>
|
| 107 |
+
</div>
|
| 108 |
+
|
| 109 |
+
<button
|
| 110 |
+
onClick={() => setPlaying(!playing)}
|
| 111 |
+
className="rounded-md bg-zinc-800/50 px-2.5 py-1 text-xs text-zinc-400 hover:text-zinc-200 transition-colors"
|
| 112 |
+
>
|
| 113 |
+
{playing ? 'Pause' : 'Play'}
|
| 114 |
+
</button>
|
| 115 |
+
</div>
|
| 116 |
+
|
| 117 |
+
<div className="flex items-center gap-3">
|
| 118 |
+
{mode === 'difference' && (
|
| 119 |
+
<div className="flex items-center gap-2">
|
| 120 |
+
<span className="text-[10px] text-zinc-500">Amplify</span>
|
| 121 |
+
<input
|
| 122 |
+
type="range"
|
| 123 |
+
min={1}
|
| 124 |
+
max={10}
|
| 125 |
+
value={amplify}
|
| 126 |
+
onChange={(e) => setAmplify(parseInt(e.target.value))}
|
| 127 |
+
className="h-1 w-20 appearance-none rounded-full bg-zinc-700 accent-violet-500
|
| 128 |
+
[&::-webkit-slider-thumb]:appearance-none
|
| 129 |
+
[&::-webkit-slider-thumb]:w-3
|
| 130 |
+
[&::-webkit-slider-thumb]:h-3
|
| 131 |
+
[&::-webkit-slider-thumb]:rounded-full
|
| 132 |
+
[&::-webkit-slider-thumb]:bg-violet-500"
|
| 133 |
+
/>
|
| 134 |
+
<span className="text-[10px] tabular-nums text-zinc-500">{amplify}x</span>
|
| 135 |
+
</div>
|
| 136 |
+
)}
|
| 137 |
+
<span className="text-[10px] tabular-nums text-zinc-600">
|
| 138 |
+
{currentFrame + 1}/{totalFrames}
|
| 139 |
+
</span>
|
| 140 |
+
</div>
|
| 141 |
+
</div>
|
| 142 |
+
|
| 143 |
+
{mode === 'side-by-side' ? (
|
| 144 |
+
<div className="grid grid-cols-2 gap-2">
|
| 145 |
+
<div className="space-y-1">
|
| 146 |
+
<p className="text-[10px] uppercase tracking-wider text-zinc-600">Original</p>
|
| 147 |
+
<canvas
|
| 148 |
+
ref={origCanvasRef}
|
| 149 |
+
width={width}
|
| 150 |
+
height={height}
|
| 151 |
+
className="w-full rounded-lg border border-zinc-800"
|
| 152 |
+
style={{ aspectRatio: aspect }}
|
| 153 |
+
/>
|
| 154 |
+
</div>
|
| 155 |
+
<div className="space-y-1">
|
| 156 |
+
<p className="text-[10px] uppercase tracking-wider text-zinc-600">Watermarked</p>
|
| 157 |
+
<canvas
|
| 158 |
+
ref={wmCanvasRef}
|
| 159 |
+
width={width}
|
| 160 |
+
height={height}
|
| 161 |
+
className="w-full rounded-lg border border-zinc-800"
|
| 162 |
+
style={{ aspectRatio: aspect }}
|
| 163 |
+
/>
|
| 164 |
+
</div>
|
| 165 |
+
</div>
|
| 166 |
+
) : (
|
| 167 |
+
<div className="space-y-1">
|
| 168 |
+
<p className="text-[10px] uppercase tracking-wider text-zinc-600">
|
| 169 |
+
Pixel Difference (amplified {amplify}x)
|
| 170 |
+
</p>
|
| 171 |
+
<canvas
|
| 172 |
+
ref={diffCanvasRef}
|
| 173 |
+
width={width}
|
| 174 |
+
height={height}
|
| 175 |
+
className="w-full rounded-lg border border-zinc-800"
|
| 176 |
+
style={{ aspectRatio: aspect }}
|
| 177 |
+
/>
|
| 178 |
+
</div>
|
| 179 |
+
)}
|
| 180 |
+
|
| 181 |
+
{/* Frame scrubber */}
|
| 182 |
+
<input
|
| 183 |
+
type="range"
|
| 184 |
+
min={0}
|
| 185 |
+
max={totalFrames - 1}
|
| 186 |
+
value={currentFrame}
|
| 187 |
+
onChange={(e) => { setCurrentFrame(parseInt(e.target.value)); setPlaying(false); }}
|
| 188 |
+
className="w-full h-1 appearance-none rounded-full bg-zinc-700 accent-blue-500
|
| 189 |
+
[&::-webkit-slider-thumb]:appearance-none
|
| 190 |
+
[&::-webkit-slider-thumb]:w-3
|
| 191 |
+
[&::-webkit-slider-thumb]:h-3
|
| 192 |
+
[&::-webkit-slider-thumb]:rounded-full
|
| 193 |
+
[&::-webkit-slider-thumb]:bg-blue-500"
|
| 194 |
+
/>
|
| 195 |
+
</div>
|
| 196 |
+
);
|
| 197 |
+
}
|
web/src/components/DetectPanel.tsx
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useRef, useCallback } from 'react';
|
| 2 |
+
import type { DetectionResult } from '@core/types.js';
|
| 3 |
+
import { autoDetectMultiFrame, type AutoDetectResult } from '@core/detector.js';
|
| 4 |
+
import { extractFrames, rgbaToY } from '../lib/video-io.js';
|
| 5 |
+
import ResultCard from './ResultCard.js';
|
| 6 |
+
|
| 7 |
+
export default function DetectPanel() {
|
| 8 |
+
const [videoUrl, setVideoUrl] = useState<string | null>(null);
|
| 9 |
+
const [videoName, setVideoName] = useState('');
|
| 10 |
+
const [key, setKey] = useState('');
|
| 11 |
+
const [maxFrames, setMaxFrames] = useState(10);
|
| 12 |
+
const [processing, setProcessing] = useState(false);
|
| 13 |
+
const [progress, setProgress] = useState({ phase: '', current: 0, total: 0 });
|
| 14 |
+
const [result, setResult] = useState<AutoDetectResult | null>(null);
|
| 15 |
+
const fileRef = useRef<HTMLInputElement>(null);
|
| 16 |
+
|
| 17 |
+
const handleFile = useCallback((file: File) => {
|
| 18 |
+
const url = URL.createObjectURL(file);
|
| 19 |
+
setVideoUrl(url);
|
| 20 |
+
setVideoName(file.name);
|
| 21 |
+
setResult(null);
|
| 22 |
+
}, []);
|
| 23 |
+
|
| 24 |
+
const handleDrop = useCallback(
|
| 25 |
+
(e: React.DragEvent) => {
|
| 26 |
+
e.preventDefault();
|
| 27 |
+
const file = e.dataTransfer.files[0];
|
| 28 |
+
if (file?.type.startsWith('video/')) handleFile(file);
|
| 29 |
+
},
|
| 30 |
+
[handleFile]
|
| 31 |
+
);
|
| 32 |
+
|
| 33 |
+
const handleDetect = async () => {
|
| 34 |
+
if (!videoUrl || !key) return;
|
| 35 |
+
setProcessing(true);
|
| 36 |
+
setResult(null);
|
| 37 |
+
|
| 38 |
+
try {
|
| 39 |
+
setProgress({ phase: 'Extracting frames', current: 0, total: 0 });
|
| 40 |
+
const { frames, width, height } = await extractFrames(videoUrl, maxFrames, (c, t) =>
|
| 41 |
+
setProgress({ phase: 'Extracting frames', current: c, total: t })
|
| 42 |
+
);
|
| 43 |
+
|
| 44 |
+
setProgress({ phase: 'Converting frames', current: 0, total: frames.length });
|
| 45 |
+
const yPlanes = frames.map((frame, i) => {
|
| 46 |
+
setProgress({ phase: 'Converting frames', current: i + 1, total: frames.length });
|
| 47 |
+
return rgbaToY(frame);
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
setProgress({ phase: 'Trying all presets', current: 0, total: 0 });
|
| 51 |
+
const detection = autoDetectMultiFrame(yPlanes, width, height, key);
|
| 52 |
+
|
| 53 |
+
setResult(detection);
|
| 54 |
+
} catch (e) {
|
| 55 |
+
console.error('Detection error:', e);
|
| 56 |
+
alert(`Error: ${e}`);
|
| 57 |
+
} finally {
|
| 58 |
+
setProcessing(false);
|
| 59 |
+
}
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
return (
|
| 63 |
+
<div className="space-y-8">
|
| 64 |
+
{/* Upload area */}
|
| 65 |
+
<div
|
| 66 |
+
onDrop={handleDrop}
|
| 67 |
+
onDragOver={(e) => e.preventDefault()}
|
| 68 |
+
onClick={() => fileRef.current?.click()}
|
| 69 |
+
className={`group cursor-pointer rounded-xl border-2 border-dashed p-10 text-center transition-colors
|
| 70 |
+
${videoUrl
|
| 71 |
+
? 'border-zinc-700 bg-zinc-900/30'
|
| 72 |
+
: 'border-zinc-800 bg-zinc-900/20 hover:border-zinc-600 hover:bg-zinc-900/40'
|
| 73 |
+
}`}
|
| 74 |
+
>
|
| 75 |
+
<input
|
| 76 |
+
ref={fileRef}
|
| 77 |
+
type="file"
|
| 78 |
+
accept="video/*"
|
| 79 |
+
className="hidden"
|
| 80 |
+
onChange={(e) => {
|
| 81 |
+
const file = e.target.files?.[0];
|
| 82 |
+
if (file) handleFile(file);
|
| 83 |
+
}}
|
| 84 |
+
/>
|
| 85 |
+
{videoUrl ? (
|
| 86 |
+
<div className="space-y-2">
|
| 87 |
+
<p className="text-sm font-medium text-zinc-300">{videoName}</p>
|
| 88 |
+
<p className="text-xs text-zinc-500">Click or drop to replace</p>
|
| 89 |
+
</div>
|
| 90 |
+
) : (
|
| 91 |
+
<div className="space-y-2">
|
| 92 |
+
<svg className="mx-auto h-8 w-8 text-zinc-600 transition-colors group-hover:text-zinc-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
| 93 |
+
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
|
| 94 |
+
</svg>
|
| 95 |
+
<p className="text-sm text-zinc-400">Drop a video file to analyze</p>
|
| 96 |
+
<p className="text-xs text-zinc-600">Upload a potentially watermarked video</p>
|
| 97 |
+
</div>
|
| 98 |
+
)}
|
| 99 |
+
</div>
|
| 100 |
+
|
| 101 |
+
{/* Configuration — just key and frame count */}
|
| 102 |
+
<div className="grid gap-6 sm:grid-cols-2">
|
| 103 |
+
<div className="space-y-1.5">
|
| 104 |
+
<label className="text-sm font-medium text-zinc-300">Secret Key</label>
|
| 105 |
+
<input
|
| 106 |
+
type="text"
|
| 107 |
+
value={key}
|
| 108 |
+
onChange={(e) => setKey(e.target.value)}
|
| 109 |
+
placeholder="Enter the secret key used for embedding..."
|
| 110 |
+
className="w-full rounded-lg border border-zinc-800 bg-zinc-900/50 px-3 py-2 text-sm text-zinc-100
|
| 111 |
+
placeholder:text-zinc-600 focus:border-blue-600 focus:outline-none focus:ring-1 focus:ring-blue-600"
|
| 112 |
+
/>
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
+
<div className="space-y-1.5">
|
| 116 |
+
<label className="text-sm font-medium text-zinc-300">Frames to analyze</label>
|
| 117 |
+
<input
|
| 118 |
+
type="number"
|
| 119 |
+
value={maxFrames}
|
| 120 |
+
onChange={(e) => setMaxFrames(Math.max(1, Math.min(100, parseInt(e.target.value) || 10)))}
|
| 121 |
+
min={1}
|
| 122 |
+
max={100}
|
| 123 |
+
className="w-full rounded-lg border border-zinc-800 bg-zinc-900/50 px-3 py-2 text-sm text-zinc-100
|
| 124 |
+
focus:border-blue-600 focus:outline-none focus:ring-1 focus:ring-blue-600"
|
| 125 |
+
/>
|
| 126 |
+
<p className="text-[10px] text-zinc-600">More frames = better detection, slower processing</p>
|
| 127 |
+
</div>
|
| 128 |
+
</div>
|
| 129 |
+
|
| 130 |
+
<p className="text-xs text-zinc-500">
|
| 131 |
+
All presets will be tried automatically. No need to know which preset was used during embedding.
|
| 132 |
+
</p>
|
| 133 |
+
|
| 134 |
+
{/* Detect button */}
|
| 135 |
+
<button
|
| 136 |
+
onClick={handleDetect}
|
| 137 |
+
disabled={!videoUrl || !key || processing}
|
| 138 |
+
className="w-full rounded-lg bg-violet-600 px-4 py-2.5 text-sm font-medium text-white
|
| 139 |
+
transition-colors hover:bg-violet-500 disabled:cursor-not-allowed disabled:bg-zinc-800 disabled:text-zinc-500"
|
| 140 |
+
>
|
| 141 |
+
{processing ? (
|
| 142 |
+
<span className="flex items-center justify-center gap-2">
|
| 143 |
+
<span className="h-4 w-4 animate-spin rounded-full border-2 border-zinc-400 border-t-white" />
|
| 144 |
+
{progress.phase} {progress.total > 0 ? `${progress.current}/${progress.total}` : ''}
|
| 145 |
+
</span>
|
| 146 |
+
) : (
|
| 147 |
+
'Detect Watermark'
|
| 148 |
+
)}
|
| 149 |
+
</button>
|
| 150 |
+
|
| 151 |
+
{/* Results */}
|
| 152 |
+
<ResultCard result={result} presetUsed={result?.presetUsed ?? null} loading={processing} />
|
| 153 |
+
</div>
|
| 154 |
+
);
|
| 155 |
+
}
|
web/src/components/EmbedPanel.tsx
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
| 2 |
+
import type { PresetName } from '@core/types.js';
|
| 3 |
+
import { getPreset, PRESET_DESCRIPTIONS } from '@core/presets.js';
|
| 4 |
+
import { streamExtractAndEmbed } from '../lib/video-io.js';
|
| 5 |
+
import StrengthSlider from './StrengthSlider.js';
|
| 6 |
+
import ComparisonView from './ComparisonView.js';
|
| 7 |
+
import RobustnessTest from './RobustnessTest.js';
|
| 8 |
+
|
| 9 |
+
const PLEASE_HOLD = [
|
| 10 |
+
{ art: ' (•_•)\n ( •_•)>⌐■-■\n (⌐■_■)', caption: 'Putting on invisibility shades...' },
|
| 11 |
+
{ art: ' ┌(▀Ĺ̯▀)┐\n ┌(▀Ĺ̯▀)┘\n └(▀Ĺ̯▀)┐', caption: 'Robot dance while we wait...' },
|
| 12 |
+
{ art: ' ╔══╗\n ║▓▓║ ☕\n ╚══╝', caption: 'Brewing the perfect watermark...' },
|
| 13 |
+
{ art: ' 🎬 → 🔬 → 💎', caption: 'Turning pixels into secrets...' },
|
| 14 |
+
{ art: ' [▓▓▓░░░░░░░]\n [▓▓▓▓▓░░░░░]\n [▓▓▓▓▓▓▓▓░░]', caption: 'Hiding bits in plain sight...' },
|
| 15 |
+
{ art: ' /\\_/\\\n ( o.o )\n > ^ <', caption: 'Even the cat can\'t see the watermark...' },
|
| 16 |
+
{ art: ' ┌─────────┐\n │ 01101001 │\n └─────────┘', caption: 'Whispering bits into wavelets...' },
|
| 17 |
+
{ art: ' ~~ 🌊 ~~\n ~🏄~ ~~ \n ~~~~~~~~', caption: 'Surfing the frequency domain...' },
|
| 18 |
+
{ art: ' 📼 ➜ 🧬 ➜ 📼', caption: 'Splicing invisible DNA into frames...' },
|
| 19 |
+
{ art: ' ¯\\_(ツ)_/¯', caption: 'Trust us, the watermark is there...' },
|
| 20 |
+
];
|
| 21 |
+
|
| 22 |
+
/** Max frames to keep in memory for the comparison view */
|
| 23 |
+
const COMPARISON_SAMPLE = 30;
|
| 24 |
+
|
| 25 |
+
export default function EmbedPanel() {
|
| 26 |
+
const [videoUrl, setVideoUrl] = useState<string | null>(null);
|
| 27 |
+
const [videoName, setVideoName] = useState('');
|
| 28 |
+
const [key, setKey] = useState('');
|
| 29 |
+
const [payload, setPayload] = useState('DEADBEEF');
|
| 30 |
+
const [preset, setPreset] = useState<PresetName>('moderate');
|
| 31 |
+
const [alpha, setAlpha] = useState(0.33);
|
| 32 |
+
const [processing, setProcessing] = useState(false);
|
| 33 |
+
const [progress, setProgress] = useState({ phase: '', current: 0, total: 0 });
|
| 34 |
+
const [jokeIndex, setJokeIndex] = useState(0);
|
| 35 |
+
const [result, setResult] = useState<{
|
| 36 |
+
blob: Blob;
|
| 37 |
+
psnr: number;
|
| 38 |
+
frames: number;
|
| 39 |
+
originalFrames: ImageData[];
|
| 40 |
+
watermarkedFrames: ImageData[];
|
| 41 |
+
width: number;
|
| 42 |
+
height: number;
|
| 43 |
+
fps: number;
|
| 44 |
+
} | null>(null);
|
| 45 |
+
const fileRef = useRef<HTMLInputElement>(null);
|
| 46 |
+
|
| 47 |
+
// Rotate jokes every 4s while processing
|
| 48 |
+
useEffect(() => {
|
| 49 |
+
if (!processing) return;
|
| 50 |
+
setJokeIndex(Math.floor(Math.random() * PLEASE_HOLD.length));
|
| 51 |
+
const id = setInterval(() => {
|
| 52 |
+
setJokeIndex((i) => (i + 1) % PLEASE_HOLD.length);
|
| 53 |
+
}, 4000);
|
| 54 |
+
return () => clearInterval(id);
|
| 55 |
+
}, [processing]);
|
| 56 |
+
|
| 57 |
+
const handleFile = useCallback((file: File) => {
|
| 58 |
+
const url = URL.createObjectURL(file);
|
| 59 |
+
setVideoUrl(url);
|
| 60 |
+
setVideoName(file.name);
|
| 61 |
+
setResult(null);
|
| 62 |
+
}, [result]);
|
| 63 |
+
|
| 64 |
+
const handleDrop = useCallback(
|
| 65 |
+
(e: React.DragEvent) => {
|
| 66 |
+
e.preventDefault();
|
| 67 |
+
const file = e.dataTransfer.files[0];
|
| 68 |
+
if (file?.type.startsWith('video/')) handleFile(file);
|
| 69 |
+
},
|
| 70 |
+
[handleFile]
|
| 71 |
+
);
|
| 72 |
+
|
| 73 |
+
const handleEmbed = async () => {
|
| 74 |
+
if (!videoUrl || !key) return;
|
| 75 |
+
setProcessing(true);
|
| 76 |
+
setResult(null);
|
| 77 |
+
|
| 78 |
+
try {
|
| 79 |
+
// Parse payload
|
| 80 |
+
const payloadHex = payload.replace(/^0x/, '');
|
| 81 |
+
const payloadBytes = new Uint8Array(
|
| 82 |
+
(payloadHex.length % 2 ? '0' + payloadHex : payloadHex)
|
| 83 |
+
.match(/.{2}/g)!
|
| 84 |
+
.map((b) => parseInt(b, 16))
|
| 85 |
+
);
|
| 86 |
+
|
| 87 |
+
const config = getPreset(preset);
|
| 88 |
+
|
| 89 |
+
// Stream: extract -> watermark -> encode in chunks
|
| 90 |
+
const embedResult = await streamExtractAndEmbed(
|
| 91 |
+
videoUrl,
|
| 92 |
+
payloadBytes,
|
| 93 |
+
key,
|
| 94 |
+
config,
|
| 95 |
+
COMPARISON_SAMPLE,
|
| 96 |
+
(phase, current, total) => setProgress({ phase, current, total })
|
| 97 |
+
);
|
| 98 |
+
|
| 99 |
+
setResult({
|
| 100 |
+
blob: embedResult.blob,
|
| 101 |
+
psnr: embedResult.avgPsnr,
|
| 102 |
+
frames: embedResult.totalFrames,
|
| 103 |
+
originalFrames: embedResult.sampleOriginal,
|
| 104 |
+
watermarkedFrames: embedResult.sampleWatermarked,
|
| 105 |
+
width: embedResult.width,
|
| 106 |
+
height: embedResult.height,
|
| 107 |
+
fps: embedResult.fps,
|
| 108 |
+
});
|
| 109 |
+
} catch (e) {
|
| 110 |
+
console.error('Embed error:', e);
|
| 111 |
+
alert(`Error: ${e}`);
|
| 112 |
+
} finally {
|
| 113 |
+
setProcessing(false);
|
| 114 |
+
}
|
| 115 |
+
};
|
| 116 |
+
|
| 117 |
+
const handleDownload = () => {
|
| 118 |
+
if (!result) return;
|
| 119 |
+
const url = URL.createObjectURL(result.blob);
|
| 120 |
+
const a = document.createElement('a');
|
| 121 |
+
a.href = url;
|
| 122 |
+
a.download = videoName.replace(/\.[^.]+$/, '') + '_watermarked.mp4';
|
| 123 |
+
a.click();
|
| 124 |
+
URL.revokeObjectURL(url);
|
| 125 |
+
};
|
| 126 |
+
|
| 127 |
+
const maxPayloadHexChars = 8; // Always 32 bits = 8 hex chars
|
| 128 |
+
|
| 129 |
+
return (
|
| 130 |
+
<div className="space-y-8">
|
| 131 |
+
{/* Upload area */}
|
| 132 |
+
<div
|
| 133 |
+
onDrop={handleDrop}
|
| 134 |
+
onDragOver={(e) => e.preventDefault()}
|
| 135 |
+
onClick={() => fileRef.current?.click()}
|
| 136 |
+
className={`group cursor-pointer rounded-2xl border-2 border-dashed p-10 text-center transition-all duration-200
|
| 137 |
+
${videoUrl
|
| 138 |
+
? 'border-zinc-700 bg-zinc-900/30'
|
| 139 |
+
: 'border-zinc-800 bg-zinc-900/20 hover:border-blue-600/40 hover:bg-zinc-900/40 hover:shadow-lg hover:shadow-blue-600/5'
|
| 140 |
+
}`}
|
| 141 |
+
>
|
| 142 |
+
<input
|
| 143 |
+
ref={fileRef}
|
| 144 |
+
type="file"
|
| 145 |
+
accept="video/*"
|
| 146 |
+
className="hidden"
|
| 147 |
+
onChange={(e) => {
|
| 148 |
+
const file = e.target.files?.[0];
|
| 149 |
+
if (file) handleFile(file);
|
| 150 |
+
}}
|
| 151 |
+
/>
|
| 152 |
+
{videoUrl ? (
|
| 153 |
+
<div className="space-y-2">
|
| 154 |
+
<p className="text-sm font-medium text-zinc-300">🎬 {videoName}</p>
|
| 155 |
+
<p className="text-xs text-zinc-500">Click or drop to replace</p>
|
| 156 |
+
</div>
|
| 157 |
+
) : (
|
| 158 |
+
<div className="space-y-3">
|
| 159 |
+
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-xl bg-zinc-800/80 transition-colors group-hover:bg-blue-600/10">
|
| 160 |
+
<svg className="h-6 w-6 text-zinc-500 transition-colors group-hover:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
| 161 |
+
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
| 162 |
+
</svg>
|
| 163 |
+
</div>
|
| 164 |
+
<p className="text-sm text-zinc-400">Drop a video file or click to browse</p>
|
| 165 |
+
<p className="text-xs text-zinc-600">MP4, WebM, MOV supported</p>
|
| 166 |
+
</div>
|
| 167 |
+
)}
|
| 168 |
+
</div>
|
| 169 |
+
|
| 170 |
+
{/* Configuration */}
|
| 171 |
+
<div className="grid gap-6 sm:grid-cols-2">
|
| 172 |
+
<div className="space-y-1.5">
|
| 173 |
+
<label className="text-sm font-medium text-zinc-300">🔑 Secret Key</label>
|
| 174 |
+
<input
|
| 175 |
+
type="text"
|
| 176 |
+
value={key}
|
| 177 |
+
onChange={(e) => setKey(e.target.value)}
|
| 178 |
+
placeholder="Enter a secret key..."
|
| 179 |
+
className="w-full rounded-lg border border-zinc-800 bg-zinc-900/50 px-3 py-2 text-sm text-zinc-100
|
| 180 |
+
placeholder:text-zinc-600 focus:border-blue-600 focus:outline-none focus:ring-1 focus:ring-blue-600
|
| 181 |
+
transition-colors"
|
| 182 |
+
/>
|
| 183 |
+
</div>
|
| 184 |
+
|
| 185 |
+
<div className="space-y-1.5">
|
| 186 |
+
<label className="text-sm font-medium text-zinc-300">📦 Payload (hex)</label>
|
| 187 |
+
<input
|
| 188 |
+
type="text"
|
| 189 |
+
value={payload}
|
| 190 |
+
onChange={(e) => setPayload(e.target.value.replace(/[^0-9a-fA-F]/g, '').slice(0, maxPayloadHexChars))}
|
| 191 |
+
placeholder="DEADBEEF"
|
| 192 |
+
className="w-full rounded-lg border border-zinc-800 bg-zinc-900/50 px-3 py-2 font-mono text-sm text-zinc-100
|
| 193 |
+
placeholder:text-zinc-600 focus:border-blue-600 focus:outline-none focus:ring-1 focus:ring-blue-600
|
| 194 |
+
transition-colors"
|
| 195 |
+
/>
|
| 196 |
+
<p className="text-[10px] text-zinc-600">32-bit payload, 4 bytes hex</p>
|
| 197 |
+
</div>
|
| 198 |
+
</div>
|
| 199 |
+
|
| 200 |
+
<StrengthSlider
|
| 201 |
+
value={alpha}
|
| 202 |
+
onChange={(v, p) => {
|
| 203 |
+
setAlpha(v);
|
| 204 |
+
setPreset(p);
|
| 205 |
+
}}
|
| 206 |
+
disabled={processing}
|
| 207 |
+
/>
|
| 208 |
+
|
| 209 |
+
<p className="text-xs text-zinc-500">
|
| 210 |
+
{PRESET_DESCRIPTIONS[preset]}
|
| 211 |
+
</p>
|
| 212 |
+
|
| 213 |
+
{/* Embed button */}
|
| 214 |
+
<button
|
| 215 |
+
onClick={handleEmbed}
|
| 216 |
+
disabled={!videoUrl || !key || processing}
|
| 217 |
+
className="w-full rounded-xl bg-gradient-to-r from-blue-600 to-blue-500 px-4 py-3 text-sm font-semibold text-white
|
| 218 |
+
shadow-lg shadow-blue-600/20 transition-all hover:from-blue-500 hover:to-blue-400 hover:shadow-blue-500/30
|
| 219 |
+
disabled:cursor-not-allowed disabled:from-zinc-800 disabled:to-zinc-800 disabled:text-zinc-500 disabled:shadow-none"
|
| 220 |
+
>
|
| 221 |
+
{processing ? (
|
| 222 |
+
<span className="flex items-center justify-center gap-2">
|
| 223 |
+
<span className="h-4 w-4 animate-spin rounded-full border-2 border-zinc-400 border-t-white" />
|
| 224 |
+
{progress.phase} {progress.total > 0 ? `${progress.current}/${progress.total}` : ''}
|
| 225 |
+
</span>
|
| 226 |
+
) : (
|
| 227 |
+
'✨ Embed Watermark'
|
| 228 |
+
)}
|
| 229 |
+
</button>
|
| 230 |
+
|
| 231 |
+
{/* Please-hold jokes while processing */}
|
| 232 |
+
{processing && (
|
| 233 |
+
<div className="flex flex-col items-center gap-2 rounded-xl bg-zinc-900/50 py-6 text-center">
|
| 234 |
+
<pre className="font-mono text-sm leading-tight text-zinc-400">
|
| 235 |
+
{PLEASE_HOLD[jokeIndex].art}
|
| 236 |
+
</pre>
|
| 237 |
+
<p className="text-xs text-zinc-500">{PLEASE_HOLD[jokeIndex].caption}</p>
|
| 238 |
+
</div>
|
| 239 |
+
)}
|
| 240 |
+
|
| 241 |
+
{/* Results */}
|
| 242 |
+
{result && (
|
| 243 |
+
<div className="space-y-6 rounded-2xl border border-emerald-800/30 bg-emerald-950/10 p-6">
|
| 244 |
+
<div className="flex items-center justify-between">
|
| 245 |
+
<div>
|
| 246 |
+
<h3 className="text-sm font-semibold text-emerald-400">✅ Embedding Complete</h3>
|
| 247 |
+
<p className="mt-1 text-xs text-zinc-500">
|
| 248 |
+
{result.frames} frames processed �� Average PSNR: {result.psnr.toFixed(1)} dB
|
| 249 |
+
</p>
|
| 250 |
+
</div>
|
| 251 |
+
<button
|
| 252 |
+
onClick={handleDownload}
|
| 253 |
+
className="rounded-lg bg-emerald-600/20 px-4 py-2 text-sm font-medium text-emerald-400
|
| 254 |
+
ring-1 ring-emerald-600/30 transition-all hover:bg-emerald-600/30 hover:ring-emerald-500/40"
|
| 255 |
+
>
|
| 256 |
+
⬇️ Download
|
| 257 |
+
</button>
|
| 258 |
+
</div>
|
| 259 |
+
|
| 260 |
+
<ComparisonView
|
| 261 |
+
originalFrames={result.originalFrames}
|
| 262 |
+
watermarkedFrames={result.watermarkedFrames}
|
| 263 |
+
width={result.width}
|
| 264 |
+
height={result.height}
|
| 265 |
+
fps={result.fps}
|
| 266 |
+
/>
|
| 267 |
+
|
| 268 |
+
<RobustnessTest
|
| 269 |
+
blob={result.blob}
|
| 270 |
+
width={result.width}
|
| 271 |
+
height={result.height}
|
| 272 |
+
payload={payload}
|
| 273 |
+
secretKey={key}
|
| 274 |
+
/>
|
| 275 |
+
</div>
|
| 276 |
+
)}
|
| 277 |
+
</div>
|
| 278 |
+
);
|
| 279 |
+
}
|
web/src/components/ResultCard.tsx
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { DetectionResult, PresetName } from '@core/types.js';
|
| 2 |
+
|
| 3 |
+
interface ResultCardProps {
|
| 4 |
+
result: DetectionResult | null;
|
| 5 |
+
presetUsed?: PresetName | null;
|
| 6 |
+
loading?: boolean;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export default function ResultCard({ result, presetUsed, loading }: ResultCardProps) {
|
| 10 |
+
if (loading) {
|
| 11 |
+
return (
|
| 12 |
+
<div className="rounded-xl border border-zinc-800/50 bg-zinc-900/50 p-6">
|
| 13 |
+
<div className="flex items-center gap-3">
|
| 14 |
+
<div className="h-5 w-5 animate-spin rounded-full border-2 border-zinc-600 border-t-blue-500" />
|
| 15 |
+
<span className="text-sm text-zinc-400">Analyzing frames...</span>
|
| 16 |
+
</div>
|
| 17 |
+
</div>
|
| 18 |
+
);
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
if (!result) return null;
|
| 22 |
+
|
| 23 |
+
const payloadHex = result.payload
|
| 24 |
+
? Array.from(result.payload).map((b) => b.toString(16).padStart(2, '0')).join('').toUpperCase()
|
| 25 |
+
: '—';
|
| 26 |
+
|
| 27 |
+
return (
|
| 28 |
+
<div
|
| 29 |
+
className={`rounded-xl border p-6 transition-colors ${
|
| 30 |
+
result.detected
|
| 31 |
+
? 'border-emerald-800/50 bg-emerald-950/30'
|
| 32 |
+
: 'border-amber-800/50 bg-amber-950/20'
|
| 33 |
+
}`}
|
| 34 |
+
>
|
| 35 |
+
<div className="flex items-start justify-between">
|
| 36 |
+
<div>
|
| 37 |
+
<h3 className={`text-sm font-semibold ${result.detected ? 'text-emerald-400' : 'text-amber-400'}`}>
|
| 38 |
+
{result.detected ? 'Watermark Detected' : 'No Watermark Found'}
|
| 39 |
+
</h3>
|
| 40 |
+
|
| 41 |
+
{result.detected && (
|
| 42 |
+
<div className="mt-4 space-y-3">
|
| 43 |
+
<div>
|
| 44 |
+
<p className="text-[10px] uppercase tracking-wider text-zinc-500">Payload</p>
|
| 45 |
+
<p className="mt-0.5 font-mono text-lg tracking-wide text-zinc-100">{payloadHex}</p>
|
| 46 |
+
</div>
|
| 47 |
+
|
| 48 |
+
<div className="flex gap-6">
|
| 49 |
+
<div>
|
| 50 |
+
<p className="text-[10px] uppercase tracking-wider text-zinc-500">Confidence</p>
|
| 51 |
+
<div className="mt-1 flex items-center gap-2">
|
| 52 |
+
<div className="h-1.5 w-24 overflow-hidden rounded-full bg-zinc-800">
|
| 53 |
+
<div
|
| 54 |
+
className="h-full rounded-full bg-emerald-500 transition-all"
|
| 55 |
+
style={{ width: `${result.confidence * 100}%` }}
|
| 56 |
+
/>
|
| 57 |
+
</div>
|
| 58 |
+
<span className="text-xs tabular-nums text-zinc-400">
|
| 59 |
+
{(result.confidence * 100).toFixed(1)}%
|
| 60 |
+
</span>
|
| 61 |
+
</div>
|
| 62 |
+
</div>
|
| 63 |
+
|
| 64 |
+
<div>
|
| 65 |
+
<p className="text-[10px] uppercase tracking-wider text-zinc-500">Tiles used</p>
|
| 66 |
+
<p className="mt-0.5 text-sm tabular-nums text-zinc-300">
|
| 67 |
+
{result.tilesDecoded} of {result.tilesTotal} available
|
| 68 |
+
</p>
|
| 69 |
+
</div>
|
| 70 |
+
|
| 71 |
+
{presetUsed && (
|
| 72 |
+
<div>
|
| 73 |
+
<p className="text-[10px] uppercase tracking-wider text-zinc-500">Preset</p>
|
| 74 |
+
<p className="mt-0.5 text-sm capitalize text-zinc-300">
|
| 75 |
+
{presetUsed}
|
| 76 |
+
</p>
|
| 77 |
+
</div>
|
| 78 |
+
)}
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
)}
|
| 82 |
+
</div>
|
| 83 |
+
|
| 84 |
+
<div
|
| 85 |
+
className={`flex h-10 w-10 items-center justify-center rounded-full ${
|
| 86 |
+
result.detected ? 'bg-emerald-500/10' : 'bg-amber-500/10'
|
| 87 |
+
}`}
|
| 88 |
+
>
|
| 89 |
+
{result.detected ? (
|
| 90 |
+
<svg className="h-5 w-5 text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
| 91 |
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
| 92 |
+
</svg>
|
| 93 |
+
) : (
|
| 94 |
+
<svg className="h-5 w-5 text-amber-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
| 95 |
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4.5c-.77-.833-2.694-.833-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
| 96 |
+
</svg>
|
| 97 |
+
)}
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
</div>
|
| 101 |
+
);
|
| 102 |
+
}
|
web/src/components/RobustnessTest.tsx
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useCallback } from 'react';
|
| 2 |
+
import { autoDetectMultiFrame } from '@core/detector.js';
|
| 3 |
+
import { attackReencode, attackDownscale, attackBrightness, attackContrast, attackSaturation } from '../lib/video-io.js';
|
| 4 |
+
|
| 5 |
+
type TestStatus = 'idle' | 'running' | 'pass' | 'fail' | 'error';
|
| 6 |
+
|
| 7 |
+
interface TestResult {
|
| 8 |
+
status: TestStatus;
|
| 9 |
+
confidence?: number;
|
| 10 |
+
payloadMatch?: boolean;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
interface RobustnessTestProps {
|
| 14 |
+
blob: Blob;
|
| 15 |
+
width: number;
|
| 16 |
+
height: number;
|
| 17 |
+
payload: string;
|
| 18 |
+
secretKey: string;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
interface TestDef {
|
| 22 |
+
label: string;
|
| 23 |
+
category: string;
|
| 24 |
+
run: (blob: Blob, w: number, h: number) => Promise<{ yPlanes: Uint8Array[]; width: number; height: number }>;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
const TESTS: TestDef[] = [
|
| 28 |
+
// CRF re-encoding
|
| 29 |
+
{ label: 'CRF 23', category: 'Re-encode', run: (b, w, h) => attackReencode(b, 23, w, h) },
|
| 30 |
+
{ label: 'CRF 28', category: 'Re-encode', run: (b, w, h) => attackReencode(b, 28, w, h) },
|
| 31 |
+
{ label: 'CRF 33', category: 'Re-encode', run: (b, w, h) => attackReencode(b, 33, w, h) },
|
| 32 |
+
{ label: 'CRF 38', category: 'Re-encode', run: (b, w, h) => attackReencode(b, 38, w, h) },
|
| 33 |
+
{ label: 'CRF 43', category: 'Re-encode', run: (b, w, h) => attackReencode(b, 43, w, h) },
|
| 34 |
+
// Downscale
|
| 35 |
+
{ label: '50%', category: 'Downscale', run: (b, w, h) => attackDownscale(b, 50, w, h) },
|
| 36 |
+
{ label: '75%', category: 'Downscale', run: (b, w, h) => attackDownscale(b, 75, w, h) },
|
| 37 |
+
{ label: '90%', category: 'Downscale', run: (b, w, h) => attackDownscale(b, 90, w, h) },
|
| 38 |
+
// Brightness
|
| 39 |
+
{ label: '-0.2', category: 'Brightness', run: (b, w, h) => attackBrightness(b, -0.2, w, h) },
|
| 40 |
+
{ label: '+0.2', category: 'Brightness', run: (b, w, h) => attackBrightness(b, 0.2, w, h) },
|
| 41 |
+
{ label: '+0.4', category: 'Brightness', run: (b, w, h) => attackBrightness(b, 0.4, w, h) },
|
| 42 |
+
// Contrast
|
| 43 |
+
{ label: '0.5x', category: 'Contrast', run: (b, w, h) => attackContrast(b, 0.5, w, h) },
|
| 44 |
+
{ label: '1.5x', category: 'Contrast', run: (b, w, h) => attackContrast(b, 1.5, w, h) },
|
| 45 |
+
{ label: '2.0x', category: 'Contrast', run: (b, w, h) => attackContrast(b, 2.0, w, h) },
|
| 46 |
+
// Saturation
|
| 47 |
+
{ label: '0x', category: 'Saturation', run: (b, w, h) => attackSaturation(b, 0, w, h) },
|
| 48 |
+
{ label: '0.5x', category: 'Saturation', run: (b, w, h) => attackSaturation(b, 0.5, w, h) },
|
| 49 |
+
{ label: '2.0x', category: 'Saturation', run: (b, w, h) => attackSaturation(b, 2.0, w, h) },
|
| 50 |
+
];
|
| 51 |
+
|
| 52 |
+
function payloadToHex(payload: Uint8Array): string {
|
| 53 |
+
return Array.from(payload).map((b) => b.toString(16).padStart(2, '0')).join('').toUpperCase();
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
export default function RobustnessTest({ blob, width, height, payload, secretKey }: RobustnessTestProps) {
|
| 57 |
+
const [results, setResults] = useState<Record<number, TestResult>>({});
|
| 58 |
+
const [runningAll, setRunningAll] = useState(false);
|
| 59 |
+
|
| 60 |
+
const expectedHex = payload.replace(/^0x/, '').toUpperCase();
|
| 61 |
+
|
| 62 |
+
const runTest = useCallback(async (idx: number) => {
|
| 63 |
+
setResults((prev) => ({ ...prev, [idx]: { status: 'running' } }));
|
| 64 |
+
try {
|
| 65 |
+
const test = TESTS[idx];
|
| 66 |
+
const attacked = await test.run(blob, width, height);
|
| 67 |
+
|
| 68 |
+
// Pipeline already caps at 30 frames; use up to 10 evenly spaced
|
| 69 |
+
const step = Math.max(1, Math.floor(attacked.yPlanes.length / 10));
|
| 70 |
+
const sampled = attacked.yPlanes.filter((_, i) => i % step === 0).slice(0, 10);
|
| 71 |
+
|
| 72 |
+
const detection = autoDetectMultiFrame(sampled, attacked.width, attacked.height, secretKey);
|
| 73 |
+
|
| 74 |
+
const detectedHex = detection.payload ? payloadToHex(detection.payload) : '';
|
| 75 |
+
const match = detection.detected && detectedHex === expectedHex;
|
| 76 |
+
|
| 77 |
+
setResults((prev) => ({
|
| 78 |
+
...prev,
|
| 79 |
+
[idx]: {
|
| 80 |
+
status: match ? 'pass' : 'fail',
|
| 81 |
+
confidence: detection.confidence,
|
| 82 |
+
payloadMatch: match,
|
| 83 |
+
},
|
| 84 |
+
}));
|
| 85 |
+
} catch (e) {
|
| 86 |
+
console.error(`Robustness test ${idx} error:`, e);
|
| 87 |
+
setResults((prev) => ({ ...prev, [idx]: { status: 'error' } }));
|
| 88 |
+
}
|
| 89 |
+
}, [blob, width, height, secretKey, expectedHex]);
|
| 90 |
+
|
| 91 |
+
const runAll = useCallback(async () => {
|
| 92 |
+
setRunningAll(true);
|
| 93 |
+
for (let i = 0; i < TESTS.length; i++) {
|
| 94 |
+
await runTest(i);
|
| 95 |
+
}
|
| 96 |
+
setRunningAll(false);
|
| 97 |
+
}, [runTest]);
|
| 98 |
+
|
| 99 |
+
const categories = ['Re-encode', 'Downscale', 'Brightness', 'Contrast', 'Saturation'];
|
| 100 |
+
|
| 101 |
+
return (
|
| 102 |
+
<div className="space-y-4">
|
| 103 |
+
<div className="flex items-center justify-between">
|
| 104 |
+
<h4 className="text-sm font-semibold text-zinc-300">Robustness Testing</h4>
|
| 105 |
+
<button
|
| 106 |
+
onClick={runAll}
|
| 107 |
+
disabled={runningAll}
|
| 108 |
+
className="rounded-lg bg-zinc-800 px-3 py-1.5 text-xs font-medium text-zinc-300
|
| 109 |
+
ring-1 ring-zinc-700 transition-all hover:bg-zinc-700 hover:text-zinc-100
|
| 110 |
+
disabled:cursor-not-allowed disabled:opacity-50"
|
| 111 |
+
>
|
| 112 |
+
{runningAll ? (
|
| 113 |
+
<span className="flex items-center gap-1.5">
|
| 114 |
+
<span className="h-3 w-3 animate-spin rounded-full border-[1.5px] border-zinc-500 border-t-zinc-200" />
|
| 115 |
+
Running...
|
| 116 |
+
</span>
|
| 117 |
+
) : (
|
| 118 |
+
'Run All Tests'
|
| 119 |
+
)}
|
| 120 |
+
</button>
|
| 121 |
+
</div>
|
| 122 |
+
|
| 123 |
+
<div className="space-y-3">
|
| 124 |
+
{categories.map((cat) => {
|
| 125 |
+
const catTests = TESTS.map((t, i) => ({ ...t, idx: i })).filter((t) => t.category === cat);
|
| 126 |
+
return (
|
| 127 |
+
<div key={cat} className="flex items-center gap-2">
|
| 128 |
+
<span className="w-24 shrink-0 text-xs text-zinc-500">{cat}</span>
|
| 129 |
+
<div className="flex flex-wrap gap-1.5">
|
| 130 |
+
{catTests.map(({ label, idx }) => {
|
| 131 |
+
const r = results[idx];
|
| 132 |
+
const status = r?.status ?? 'idle';
|
| 133 |
+
return (
|
| 134 |
+
<button
|
| 135 |
+
key={idx}
|
| 136 |
+
onClick={() => runTest(idx)}
|
| 137 |
+
disabled={status === 'running' || runningAll}
|
| 138 |
+
className={`inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-medium
|
| 139 |
+
ring-1 transition-all disabled:cursor-not-allowed
|
| 140 |
+
${status === 'idle' ? 'bg-zinc-800/80 text-zinc-400 ring-zinc-700 hover:bg-zinc-700 hover:text-zinc-200' : ''}
|
| 141 |
+
${status === 'running' ? 'bg-zinc-800 text-zinc-400 ring-zinc-700' : ''}
|
| 142 |
+
${status === 'pass' ? 'bg-emerald-950/40 text-emerald-400 ring-emerald-800/50' : ''}
|
| 143 |
+
${status === 'fail' ? 'bg-red-950/40 text-red-400 ring-red-800/50' : ''}
|
| 144 |
+
${status === 'error' ? 'bg-amber-950/40 text-amber-400 ring-amber-800/50' : ''}
|
| 145 |
+
`}
|
| 146 |
+
>
|
| 147 |
+
{status === 'running' && (
|
| 148 |
+
<span className="h-3 w-3 animate-spin rounded-full border-[1.5px] border-zinc-500 border-t-zinc-200" />
|
| 149 |
+
)}
|
| 150 |
+
{status === 'pass' && <span>✅</span>}
|
| 151 |
+
{status === 'fail' && <span>❌</span>}
|
| 152 |
+
{status === 'error' && <span>⚠️</span>}
|
| 153 |
+
{label}
|
| 154 |
+
{r?.confidence !== undefined && (
|
| 155 |
+
<span className="ml-0.5 opacity-70">{(r.confidence * 100).toFixed(0)}%</span>
|
| 156 |
+
)}
|
| 157 |
+
</button>
|
| 158 |
+
);
|
| 159 |
+
})}
|
| 160 |
+
</div>
|
| 161 |
+
</div>
|
| 162 |
+
);
|
| 163 |
+
})}
|
| 164 |
+
</div>
|
| 165 |
+
</div>
|
| 166 |
+
);
|
| 167 |
+
}
|
web/src/components/StrengthSlider.tsx
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { PresetName } from '@core/types.js';
|
| 2 |
+
|
| 3 |
+
const PRESET_MARKS: Array<{ value: number; label: string; emoji: string; preset: PresetName }> = [
|
| 4 |
+
{ value: 0.00, label: 'Light', emoji: '🌤️', preset: 'light' },
|
| 5 |
+
{ value: 0.33, label: 'Moderate', emoji: '⚡', preset: 'moderate' },
|
| 6 |
+
{ value: 0.67, label: 'Strong', emoji: '🛡️', preset: 'strong' },
|
| 7 |
+
{ value: 1.00, label: 'Fortress', emoji: '🏰', preset: 'fortress' },
|
| 8 |
+
];
|
| 9 |
+
|
| 10 |
+
interface StrengthSliderProps {
|
| 11 |
+
value: number;
|
| 12 |
+
onChange: (value: number, preset: PresetName) => void;
|
| 13 |
+
disabled?: boolean;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export default function StrengthSlider({ value, onChange, disabled }: StrengthSliderProps) {
|
| 17 |
+
const nearestPreset = PRESET_MARKS.reduce((best, mark) =>
|
| 18 |
+
Math.abs(mark.value - value) < Math.abs(best.value - value) ? mark : best
|
| 19 |
+
);
|
| 20 |
+
|
| 21 |
+
return (
|
| 22 |
+
<div className="space-y-3">
|
| 23 |
+
<div className="flex items-center justify-between">
|
| 24 |
+
<label className="text-sm font-medium text-zinc-300">Strength</label>
|
| 25 |
+
<span className="text-xs tabular-nums text-zinc-500">
|
| 26 |
+
{nearestPreset.emoji} {nearestPreset.label}
|
| 27 |
+
</span>
|
| 28 |
+
</div>
|
| 29 |
+
|
| 30 |
+
<div className="relative">
|
| 31 |
+
<input
|
| 32 |
+
type="range"
|
| 33 |
+
min="0"
|
| 34 |
+
max="1"
|
| 35 |
+
step="0.01"
|
| 36 |
+
value={value}
|
| 37 |
+
disabled={disabled}
|
| 38 |
+
onChange={(e) => {
|
| 39 |
+
const v = parseFloat(e.target.value);
|
| 40 |
+
const snap = PRESET_MARKS.find((m) => Math.abs(m.value - v) < 0.04);
|
| 41 |
+
if (snap) {
|
| 42 |
+
onChange(snap.value, snap.preset);
|
| 43 |
+
} else {
|
| 44 |
+
// Snap to nearest preset (no custom mode)
|
| 45 |
+
const nearest = PRESET_MARKS.reduce((best, mark) =>
|
| 46 |
+
Math.abs(mark.value - v) < Math.abs(best.value - v) ? mark : best
|
| 47 |
+
);
|
| 48 |
+
onChange(nearest.value, nearest.preset);
|
| 49 |
+
}
|
| 50 |
+
}}
|
| 51 |
+
className="w-full h-1.5 rounded-full appearance-none cursor-pointer
|
| 52 |
+
bg-zinc-700 accent-blue-500
|
| 53 |
+
disabled:opacity-50 disabled:cursor-not-allowed
|
| 54 |
+
[&::-webkit-slider-thumb]:appearance-none
|
| 55 |
+
[&::-webkit-slider-thumb]:w-4
|
| 56 |
+
[&::-webkit-slider-thumb]:h-4
|
| 57 |
+
[&::-webkit-slider-thumb]:rounded-full
|
| 58 |
+
[&::-webkit-slider-thumb]:bg-blue-500
|
| 59 |
+
[&::-webkit-slider-thumb]:shadow-lg
|
| 60 |
+
[&::-webkit-slider-thumb]:shadow-blue-500/20
|
| 61 |
+
[&::-webkit-slider-thumb]:transition-transform
|
| 62 |
+
[&::-webkit-slider-thumb]:hover:scale-110"
|
| 63 |
+
/>
|
| 64 |
+
|
| 65 |
+
<div className="relative mt-2 h-5">
|
| 66 |
+
{PRESET_MARKS.map((mark, i) => {
|
| 67 |
+
const pct = mark.value * 100;
|
| 68 |
+
const isFirst = i === 0;
|
| 69 |
+
const isLast = i === PRESET_MARKS.length - 1;
|
| 70 |
+
const align = isFirst ? 'left-0' : isLast ? 'right-0' : '-translate-x-1/2';
|
| 71 |
+
return (
|
| 72 |
+
<button
|
| 73 |
+
key={mark.preset}
|
| 74 |
+
onClick={() => onChange(mark.value, mark.preset)}
|
| 75 |
+
disabled={disabled}
|
| 76 |
+
style={isFirst || isLast ? undefined : { left: `${pct}%` }}
|
| 77 |
+
className={`absolute whitespace-nowrap text-[10px] transition-colors ${align} ${
|
| 78 |
+
nearestPreset.preset === mark.preset
|
| 79 |
+
? 'text-blue-400 font-medium'
|
| 80 |
+
: 'text-zinc-600 hover:text-zinc-400'
|
| 81 |
+
} disabled:opacity-50`}
|
| 82 |
+
>
|
| 83 |
+
{mark.emoji} {mark.label}
|
| 84 |
+
</button>
|
| 85 |
+
);
|
| 86 |
+
})}
|
| 87 |
+
</div>
|
| 88 |
+
</div>
|
| 89 |
+
</div>
|
| 90 |
+
);
|
| 91 |
+
}
|
web/src/index.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
@import "tailwindcss";
|
web/src/lib/video-io.ts
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Browser-based video frame extraction and re-encoding
|
| 3 |
+
* Uses ffmpeg.wasm for H.264 MP4 output
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import { FFmpeg } from '@ffmpeg/ffmpeg';
|
| 7 |
+
import { toBlobURL } from '@ffmpeg/util';
|
| 8 |
+
import type { WatermarkConfig } from '@core/types.js';
|
| 9 |
+
import { embedWatermark } from '@core/embedder.js';
|
| 10 |
+
|
| 11 |
+
let ffmpegInstance: FFmpeg | null = null;
|
| 12 |
+
let ffmpegLoaded = false;
|
| 13 |
+
|
| 14 |
+
/** Get or initialize the shared FFmpeg instance */
|
| 15 |
+
async function getFFmpeg(onLog?: (msg: string) => void): Promise<FFmpeg> {
|
| 16 |
+
if (ffmpegInstance && ffmpegLoaded) return ffmpegInstance;
|
| 17 |
+
|
| 18 |
+
ffmpegInstance = new FFmpeg();
|
| 19 |
+
if (onLog) {
|
| 20 |
+
ffmpegInstance.on('log', ({ message }) => onLog(message));
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
// Load ffmpeg.wasm from CDN
|
| 24 |
+
const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm';
|
| 25 |
+
await ffmpegInstance.load({
|
| 26 |
+
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
|
| 27 |
+
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
|
| 28 |
+
});
|
| 29 |
+
|
| 30 |
+
ffmpegLoaded = true;
|
| 31 |
+
return ffmpegInstance;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
/** Extract Y plane from an RGBA ImageData */
|
| 35 |
+
export function rgbaToY(imageData: ImageData): Uint8Array {
|
| 36 |
+
const { data, width, height } = imageData;
|
| 37 |
+
const y = new Uint8Array(width * height);
|
| 38 |
+
for (let i = 0; i < width * height; i++) {
|
| 39 |
+
const r = data[i * 4];
|
| 40 |
+
const g = data[i * 4 + 1];
|
| 41 |
+
const b = data[i * 4 + 2];
|
| 42 |
+
y[i] = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
|
| 43 |
+
}
|
| 44 |
+
return y;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
/** Apply Y plane delta to RGBA ImageData (modifies in place) */
|
| 48 |
+
export function applyYDelta(imageData: ImageData, originalY: Uint8Array, watermarkedY: Uint8Array): void {
|
| 49 |
+
const { data } = imageData;
|
| 50 |
+
for (let i = 0; i < originalY.length; i++) {
|
| 51 |
+
const delta = watermarkedY[i] - originalY[i];
|
| 52 |
+
data[i * 4] = Math.max(0, Math.min(255, data[i * 4] + delta));
|
| 53 |
+
data[i * 4 + 1] = Math.max(0, Math.min(255, data[i * 4 + 1] + delta));
|
| 54 |
+
data[i * 4 + 2] = Math.max(0, Math.min(255, data[i * 4 + 2] + delta));
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
/** Result of the streaming extract+embed pipeline */
|
| 59 |
+
export interface StreamEmbedResult {
|
| 60 |
+
blob: Blob;
|
| 61 |
+
width: number;
|
| 62 |
+
height: number;
|
| 63 |
+
fps: number;
|
| 64 |
+
totalFrames: number;
|
| 65 |
+
avgPsnr: number;
|
| 66 |
+
sampleOriginal: ImageData[];
|
| 67 |
+
sampleWatermarked: ImageData[];
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
/** Number of frames to accumulate before flushing to H.264 encoder */
|
| 71 |
+
const CHUNK_SIZE = 100;
|
| 72 |
+
|
| 73 |
+
/**
|
| 74 |
+
* Streaming extract → watermark → encode pipeline.
|
| 75 |
+
* Processes frames in chunks of CHUNK_SIZE, encoding each chunk to a
|
| 76 |
+
* temporary MP4 segment and freeing the raw buffer. This keeps peak
|
| 77 |
+
* memory at ~(CHUNK_SIZE * frameSize) instead of (totalFrames * frameSize).
|
| 78 |
+
* At the end, all segments are concatenated into the final MP4.
|
| 79 |
+
*/
|
| 80 |
+
export async function streamExtractAndEmbed(
|
| 81 |
+
videoUrl: string,
|
| 82 |
+
payload: Uint8Array,
|
| 83 |
+
key: string,
|
| 84 |
+
config: WatermarkConfig,
|
| 85 |
+
comparisonSample: number,
|
| 86 |
+
onProgress?: (phase: string, current: number, total: number) => void
|
| 87 |
+
): Promise<StreamEmbedResult> {
|
| 88 |
+
const video = document.createElement('video');
|
| 89 |
+
video.src = videoUrl;
|
| 90 |
+
video.muted = true;
|
| 91 |
+
video.preload = 'auto';
|
| 92 |
+
|
| 93 |
+
await new Promise<void>((resolve, reject) => {
|
| 94 |
+
video.onloadedmetadata = () => resolve();
|
| 95 |
+
video.onerror = () => reject(new Error('Failed to load video'));
|
| 96 |
+
});
|
| 97 |
+
|
| 98 |
+
const { videoWidth: rawW, videoHeight: rawH, duration } = video;
|
| 99 |
+
// x264 with yuv420p requires even dimensions
|
| 100 |
+
const width = rawW % 2 === 0 ? rawW : rawW - 1;
|
| 101 |
+
const height = rawH % 2 === 0 ? rawH : rawH - 1;
|
| 102 |
+
const totalFrames = Math.ceil(duration * 30);
|
| 103 |
+
const interval = duration / totalFrames;
|
| 104 |
+
const fps = totalFrames / duration;
|
| 105 |
+
|
| 106 |
+
const canvas = document.createElement('canvas');
|
| 107 |
+
canvas.width = width;
|
| 108 |
+
canvas.height = height;
|
| 109 |
+
const ctx = canvas.getContext('2d')!;
|
| 110 |
+
|
| 111 |
+
const frameSize = width * height * 4;
|
| 112 |
+
|
| 113 |
+
// Determine which frame indices to sample for comparison
|
| 114 |
+
const sampleIndices = new Set<number>();
|
| 115 |
+
const sampleStep = Math.max(1, Math.floor(totalFrames / comparisonSample));
|
| 116 |
+
for (let i = 0; i < totalFrames && sampleIndices.size < comparisonSample; i += sampleStep) {
|
| 117 |
+
sampleIndices.add(i);
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
const sampleOriginal: ImageData[] = [];
|
| 121 |
+
const sampleWatermarked: ImageData[] = [];
|
| 122 |
+
let totalPsnr = 0;
|
| 123 |
+
|
| 124 |
+
const ffmpeg = await getFFmpeg();
|
| 125 |
+
const segments: string[] = [];
|
| 126 |
+
|
| 127 |
+
// Chunk buffer — reused across chunks
|
| 128 |
+
let chunkBuffer = new Uint8Array(Math.min(CHUNK_SIZE, totalFrames) * frameSize);
|
| 129 |
+
let chunkOffset = 0;
|
| 130 |
+
let framesInChunk = 0;
|
| 131 |
+
|
| 132 |
+
const flushChunk = async () => {
|
| 133 |
+
if (framesInChunk === 0) return;
|
| 134 |
+
|
| 135 |
+
const segName = `seg_${segments.length}.mp4`;
|
| 136 |
+
const usedBytes = chunkBuffer.slice(0, framesInChunk * frameSize);
|
| 137 |
+
await ffmpeg.writeFile('chunk.raw', usedBytes);
|
| 138 |
+
|
| 139 |
+
await ffmpeg.exec([
|
| 140 |
+
'-f', 'rawvideo',
|
| 141 |
+
'-pix_fmt', 'rgba',
|
| 142 |
+
'-s', `${width}x${height}`,
|
| 143 |
+
'-r', String(fps),
|
| 144 |
+
'-i', 'chunk.raw',
|
| 145 |
+
'-c:v', 'libx264',
|
| 146 |
+
'-pix_fmt', 'yuv420p',
|
| 147 |
+
'-crf', '20',
|
| 148 |
+
'-preset', 'ultrafast',
|
| 149 |
+
'-an', '-y',
|
| 150 |
+
segName,
|
| 151 |
+
]);
|
| 152 |
+
|
| 153 |
+
await ffmpeg.deleteFile('chunk.raw');
|
| 154 |
+
segments.push(segName);
|
| 155 |
+
chunkOffset = 0;
|
| 156 |
+
framesInChunk = 0;
|
| 157 |
+
};
|
| 158 |
+
|
| 159 |
+
for (let i = 0; i < totalFrames; i++) {
|
| 160 |
+
onProgress?.('Embedding', i + 1, totalFrames);
|
| 161 |
+
|
| 162 |
+
// Seek and extract frame
|
| 163 |
+
video.currentTime = i * interval;
|
| 164 |
+
await new Promise<void>((resolve) => {
|
| 165 |
+
video.onseeked = () => resolve();
|
| 166 |
+
});
|
| 167 |
+
ctx.drawImage(video, 0, 0, width, height);
|
| 168 |
+
const frameData = ctx.getImageData(0, 0, width, height);
|
| 169 |
+
|
| 170 |
+
// Watermark
|
| 171 |
+
const y = rgbaToY(frameData);
|
| 172 |
+
const result = embedWatermark(y, width, height, payload, key, config);
|
| 173 |
+
totalPsnr += result.psnr;
|
| 174 |
+
|
| 175 |
+
// Apply watermark delta to RGBA
|
| 176 |
+
applyYDelta(frameData, y, result.yPlane);
|
| 177 |
+
|
| 178 |
+
// Append to chunk buffer
|
| 179 |
+
chunkBuffer.set(frameData.data, chunkOffset);
|
| 180 |
+
chunkOffset += frameSize;
|
| 181 |
+
framesInChunk++;
|
| 182 |
+
|
| 183 |
+
// Keep sample frames for comparison
|
| 184 |
+
if (sampleIndices.has(i)) {
|
| 185 |
+
ctx.drawImage(video, 0, 0, width, height);
|
| 186 |
+
sampleOriginal.push(ctx.getImageData(0, 0, width, height));
|
| 187 |
+
sampleWatermarked.push(frameData);
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
// Flush chunk when full
|
| 191 |
+
if (framesInChunk >= CHUNK_SIZE) {
|
| 192 |
+
onProgress?.('Encoding chunk', segments.length + 1, Math.ceil(totalFrames / CHUNK_SIZE));
|
| 193 |
+
await flushChunk();
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
// Flush remaining frames
|
| 198 |
+
if (framesInChunk > 0) {
|
| 199 |
+
onProgress?.('Encoding chunk', segments.length + 1, Math.ceil(totalFrames / CHUNK_SIZE));
|
| 200 |
+
await flushChunk();
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
// Free chunk buffer
|
| 204 |
+
chunkBuffer = null!;
|
| 205 |
+
|
| 206 |
+
// Concatenate all segments
|
| 207 |
+
let blob: Blob;
|
| 208 |
+
if (segments.length === 1) {
|
| 209 |
+
// Single segment — just use it directly
|
| 210 |
+
const data = await ffmpeg.readFile(segments[0]);
|
| 211 |
+
await ffmpeg.deleteFile(segments[0]);
|
| 212 |
+
if (typeof data === 'string') throw new Error('Unexpected string output from ffmpeg');
|
| 213 |
+
blob = new Blob([(data as unknown as { buffer: ArrayBuffer }).buffer], { type: 'video/mp4' });
|
| 214 |
+
} else {
|
| 215 |
+
onProgress?.('Joining segments', 0, 0);
|
| 216 |
+
// Write concat file list
|
| 217 |
+
const concatList = segments.map((s) => `file '${s}'`).join('\n');
|
| 218 |
+
await ffmpeg.writeFile('concat.txt', concatList);
|
| 219 |
+
|
| 220 |
+
await ffmpeg.exec([
|
| 221 |
+
'-f', 'concat',
|
| 222 |
+
'-safe', '0',
|
| 223 |
+
'-i', 'concat.txt',
|
| 224 |
+
'-c', 'copy',
|
| 225 |
+
'-movflags', '+faststart',
|
| 226 |
+
'-y',
|
| 227 |
+
'final.mp4',
|
| 228 |
+
]);
|
| 229 |
+
|
| 230 |
+
await ffmpeg.deleteFile('concat.txt');
|
| 231 |
+
for (const seg of segments) {
|
| 232 |
+
await ffmpeg.deleteFile(seg);
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
const data = await ffmpeg.readFile('final.mp4');
|
| 236 |
+
await ffmpeg.deleteFile('final.mp4');
|
| 237 |
+
if (typeof data === 'string') throw new Error('Unexpected string output from ffmpeg');
|
| 238 |
+
blob = new Blob([(data as unknown as { buffer: ArrayBuffer }).buffer], { type: 'video/mp4' });
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
return {
|
| 242 |
+
blob,
|
| 243 |
+
width,
|
| 244 |
+
height,
|
| 245 |
+
fps,
|
| 246 |
+
totalFrames,
|
| 247 |
+
avgPsnr: totalPsnr / totalFrames,
|
| 248 |
+
sampleOriginal,
|
| 249 |
+
sampleWatermarked,
|
| 250 |
+
};
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
/**
|
| 254 |
+
* Extract frames from a video (used by detect panel).
|
| 255 |
+
* Spreads frames evenly across the full duration.
|
| 256 |
+
*/
|
| 257 |
+
export async function extractFrames(
|
| 258 |
+
videoUrl: string,
|
| 259 |
+
maxFrames: number = 30,
|
| 260 |
+
onProgress?: (frame: number, total: number) => void
|
| 261 |
+
): Promise<{ frames: ImageData[]; width: number; height: number; fps: number; duration: number }> {
|
| 262 |
+
const video = document.createElement('video');
|
| 263 |
+
video.src = videoUrl;
|
| 264 |
+
video.muted = true;
|
| 265 |
+
video.preload = 'auto';
|
| 266 |
+
|
| 267 |
+
await new Promise<void>((resolve, reject) => {
|
| 268 |
+
video.onloadedmetadata = () => resolve();
|
| 269 |
+
video.onerror = () => reject(new Error('Failed to load video'));
|
| 270 |
+
});
|
| 271 |
+
|
| 272 |
+
const { videoWidth: rawW, videoHeight: rawH, duration } = video;
|
| 273 |
+
const width = rawW % 2 === 0 ? rawW : rawW - 1;
|
| 274 |
+
const height = rawH % 2 === 0 ? rawH : rawH - 1;
|
| 275 |
+
const nativeFrameCount = Math.ceil(duration * 30);
|
| 276 |
+
const totalFrames = Math.min(maxFrames, nativeFrameCount);
|
| 277 |
+
const interval = duration / totalFrames;
|
| 278 |
+
|
| 279 |
+
const canvas = document.createElement('canvas');
|
| 280 |
+
canvas.width = width;
|
| 281 |
+
canvas.height = height;
|
| 282 |
+
const ctx = canvas.getContext('2d')!;
|
| 283 |
+
|
| 284 |
+
const frames: ImageData[] = [];
|
| 285 |
+
|
| 286 |
+
for (let i = 0; i < totalFrames; i++) {
|
| 287 |
+
video.currentTime = i * interval;
|
| 288 |
+
await new Promise<void>((resolve) => {
|
| 289 |
+
video.onseeked = () => resolve();
|
| 290 |
+
});
|
| 291 |
+
|
| 292 |
+
ctx.drawImage(video, 0, 0, width, height);
|
| 293 |
+
frames.push(ctx.getImageData(0, 0, width, height));
|
| 294 |
+
onProgress?.(i + 1, totalFrames);
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
const fps = totalFrames / duration;
|
| 298 |
+
|
| 299 |
+
return { frames, width, height, fps, duration };
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
// ---------------------------------------------------------------------------
|
| 303 |
+
// Attack utilities — apply degradation to watermarked MP4, return Y planes
|
| 304 |
+
// ---------------------------------------------------------------------------
|
| 305 |
+
|
| 306 |
+
/** Result of decoding an attacked video back to Y planes */
|
| 307 |
+
export interface AttackYPlanes {
|
| 308 |
+
yPlanes: Uint8Array[];
|
| 309 |
+
width: number;
|
| 310 |
+
height: number;
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
/**
|
| 314 |
+
* Write blob to ffmpeg FS, run attack filter, decode output to Y planes.
|
| 315 |
+
* Only processes `maxFrames` frames to keep WASM encoding fast.
|
| 316 |
+
*/
|
| 317 |
+
async function runAttackPipeline(
|
| 318 |
+
blob: Blob,
|
| 319 |
+
filterArgs: string[],
|
| 320 |
+
outputWidth: number,
|
| 321 |
+
outputHeight: number,
|
| 322 |
+
maxFrames: number = 30,
|
| 323 |
+
): Promise<AttackYPlanes> {
|
| 324 |
+
const ffmpeg = await getFFmpeg();
|
| 325 |
+
|
| 326 |
+
const inputData = new Uint8Array(await blob.arrayBuffer());
|
| 327 |
+
await ffmpeg.writeFile('attack_input.mp4', inputData);
|
| 328 |
+
|
| 329 |
+
await ffmpeg.exec([
|
| 330 |
+
'-i', 'attack_input.mp4',
|
| 331 |
+
...filterArgs,
|
| 332 |
+
'-frames:v', String(maxFrames),
|
| 333 |
+
'-c:v', 'libx264',
|
| 334 |
+
'-pix_fmt', 'yuv420p',
|
| 335 |
+
'-preset', 'ultrafast',
|
| 336 |
+
'-an',
|
| 337 |
+
'-y',
|
| 338 |
+
'attack_output.mp4',
|
| 339 |
+
]);
|
| 340 |
+
|
| 341 |
+
// Decode attacked output to raw grayscale
|
| 342 |
+
await ffmpeg.exec([
|
| 343 |
+
'-i', 'attack_output.mp4',
|
| 344 |
+
'-f', 'rawvideo',
|
| 345 |
+
'-pix_fmt', 'gray',
|
| 346 |
+
'-y',
|
| 347 |
+
'attack_raw.raw',
|
| 348 |
+
]);
|
| 349 |
+
|
| 350 |
+
const rawData = await ffmpeg.readFile('attack_raw.raw');
|
| 351 |
+
|
| 352 |
+
await ffmpeg.deleteFile('attack_input.mp4');
|
| 353 |
+
await ffmpeg.deleteFile('attack_output.mp4');
|
| 354 |
+
await ffmpeg.deleteFile('attack_raw.raw');
|
| 355 |
+
|
| 356 |
+
if (typeof rawData === 'string') throw new Error('Unexpected string output from ffmpeg');
|
| 357 |
+
const raw = new Uint8Array((rawData as unknown as { buffer: ArrayBuffer }).buffer);
|
| 358 |
+
|
| 359 |
+
const frameSize = outputWidth * outputHeight;
|
| 360 |
+
const frameCount = Math.floor(raw.length / frameSize);
|
| 361 |
+
const yPlanes: Uint8Array[] = [];
|
| 362 |
+
for (let i = 0; i < frameCount; i++) {
|
| 363 |
+
yPlanes.push(raw.slice(i * frameSize, (i + 1) * frameSize));
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
return { yPlanes, width: outputWidth, height: outputHeight };
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
/** Attack: re-encode at a given CRF quality level */
|
| 370 |
+
export async function attackReencode(blob: Blob, crf: number, width: number, height: number): Promise<AttackYPlanes> {
|
| 371 |
+
return runAttackPipeline(blob, ['-crf', String(crf)], width, height);
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
/** Attack: downscale to scalePct%, then scale back up to original size */
|
| 375 |
+
export async function attackDownscale(
|
| 376 |
+
blob: Blob,
|
| 377 |
+
scalePct: number,
|
| 378 |
+
width: number,
|
| 379 |
+
height: number,
|
| 380 |
+
): Promise<AttackYPlanes> {
|
| 381 |
+
const scaledW = Math.round(width * scalePct / 100);
|
| 382 |
+
const scaledH = Math.round(height * scalePct / 100);
|
| 383 |
+
// Ensure even dimensions for libx264
|
| 384 |
+
const sW = scaledW % 2 === 0 ? scaledW : scaledW + 1;
|
| 385 |
+
const sH = scaledH % 2 === 0 ? scaledH : scaledH + 1;
|
| 386 |
+
return runAttackPipeline(
|
| 387 |
+
blob,
|
| 388 |
+
['-vf', `scale=${sW}:${sH},scale=${width}:${height}`, '-crf', '18'],
|
| 389 |
+
width,
|
| 390 |
+
height,
|
| 391 |
+
);
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
/** Attack: adjust brightness by delta (-1.0 to 1.0) */
|
| 395 |
+
export async function attackBrightness(blob: Blob, delta: number, width: number, height: number): Promise<AttackYPlanes> {
|
| 396 |
+
return runAttackPipeline(blob, ['-vf', `eq=brightness=${delta}`, '-crf', '18'], width, height);
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
/** Attack: adjust contrast (1.0 = unchanged, <1 = less, >1 = more) */
|
| 400 |
+
export async function attackContrast(blob: Blob, factor: number, width: number, height: number): Promise<AttackYPlanes> {
|
| 401 |
+
return runAttackPipeline(blob, ['-vf', `eq=contrast=${factor}`, '-crf', '18'], width, height);
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
/** Attack: adjust saturation (1.0 = unchanged, 0 = grayscale, >1 = boosted) */
|
| 405 |
+
export async function attackSaturation(blob: Blob, factor: number, width: number, height: number): Promise<AttackYPlanes> {
|
| 406 |
+
return runAttackPipeline(blob, ['-vf', `eq=saturation=${factor}`, '-crf', '18'], width, height);
|
| 407 |
+
}
|
| 408 |
+
|
web/src/main.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { StrictMode } from 'react';
|
| 2 |
+
import { createRoot } from 'react-dom/client';
|
| 3 |
+
import App from './App';
|
| 4 |
+
import './index.css';
|
| 5 |
+
|
| 6 |
+
createRoot(document.getElementById('root')!).render(
|
| 7 |
+
<StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</StrictMode>
|
| 10 |
+
);
|
web/src/workers/watermark.worker.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Web Worker for non-blocking watermark embedding/detection
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
import { embedWatermark } from '@core/embedder.js';
|
| 6 |
+
import { detectWatermark } from '@core/detector.js';
|
| 7 |
+
import { getPreset } from '@core/presets.js';
|
| 8 |
+
import type { PresetName, WatermarkConfig } from '@core/types.js';
|
| 9 |
+
|
| 10 |
+
export type WorkerMessage =
|
| 11 |
+
| {
|
| 12 |
+
type: 'embed';
|
| 13 |
+
id: number;
|
| 14 |
+
yPlane: Uint8Array;
|
| 15 |
+
width: number;
|
| 16 |
+
height: number;
|
| 17 |
+
payload: Uint8Array;
|
| 18 |
+
key: string;
|
| 19 |
+
preset: PresetName;
|
| 20 |
+
customConfig?: Partial<WatermarkConfig>;
|
| 21 |
+
}
|
| 22 |
+
| {
|
| 23 |
+
type: 'detect';
|
| 24 |
+
id: number;
|
| 25 |
+
yPlane: Uint8Array;
|
| 26 |
+
width: number;
|
| 27 |
+
height: number;
|
| 28 |
+
key: string;
|
| 29 |
+
preset: PresetName;
|
| 30 |
+
customConfig?: Partial<WatermarkConfig>;
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
self.onmessage = (e: MessageEvent<WorkerMessage>) => {
|
| 34 |
+
const msg = e.data;
|
| 35 |
+
|
| 36 |
+
try {
|
| 37 |
+
const config = getPreset(msg.preset);
|
| 38 |
+
|
| 39 |
+
if (msg.type === 'embed') {
|
| 40 |
+
const result = embedWatermark(msg.yPlane, msg.width, msg.height, msg.payload, msg.key, config);
|
| 41 |
+
self.postMessage({
|
| 42 |
+
type: 'embed-result',
|
| 43 |
+
id: msg.id,
|
| 44 |
+
yPlane: result.yPlane,
|
| 45 |
+
psnr: result.psnr,
|
| 46 |
+
});
|
| 47 |
+
} else if (msg.type === 'detect') {
|
| 48 |
+
const result = detectWatermark(msg.yPlane, msg.width, msg.height, msg.key, config);
|
| 49 |
+
self.postMessage({
|
| 50 |
+
type: 'detect-result',
|
| 51 |
+
id: msg.id,
|
| 52 |
+
...result,
|
| 53 |
+
});
|
| 54 |
+
}
|
| 55 |
+
} catch (err) {
|
| 56 |
+
self.postMessage({
|
| 57 |
+
type: 'error',
|
| 58 |
+
id: msg.id,
|
| 59 |
+
error: String(err),
|
| 60 |
+
});
|
| 61 |
+
}
|
| 62 |
+
};
|