Spaces:
Running
Running
File size: 6,988 Bytes
f2f99a3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | #!/usr/bin/env node
/**
* LTMarx CLI — Video watermark embedding and detection
*
* Usage:
* ltmarx embed -i input.mp4 -o output.mp4 --key SECRET --preset moderate
* ltmarx detect -i video.mp4 --key SECRET --preset moderate
*/
import { parseArgs } from 'node:util';
import { probeVideo, readYuvFrames, createEncoder } from './ffmpeg-io.js';
import { embedWatermark } from '../core/embedder.js';
import { detectWatermark, detectWatermarkMultiFrame } from '../core/detector.js';
import { getPreset, PRESET_DESCRIPTIONS } from '../core/presets.js';
import type { PresetName } from '../core/types.js';
function usage() {
console.log(`
LTMarx — Video Watermarking System
Commands:
embed Embed a watermark into a video
detect Detect and extract a watermark from a video
presets List available presets
Usage:
ltmarx embed -i input.mp4 -o output.mp4 --key SECRET --preset moderate --payload DEADBEEF
ltmarx detect -i video.mp4 --key SECRET --preset moderate [--frames N]
ltmarx presets
Options:
-i, --input Input video file
-o, --output Output video file (embed only)
--key Secret key for watermark
--preset Preset name: light, moderate, strong, fortress
--payload 32-bit payload as hex string (embed only, default: DEADBEEF)
--frames Number of frames to analyze (detect only, default: 10)
--crf Output CRF quality (embed only, default: 18)
`);
process.exit(1);
}
async function main() {
const args = process.argv.slice(2);
const command = args[0];
if (!command || command === '--help' || command === '-h') usage();
if (command === 'presets') {
console.log('\nAvailable presets:\n');
for (const [name, desc] of Object.entries(PRESET_DESCRIPTIONS)) {
console.log(` ${name.padEnd(12)} ${desc}`);
}
console.log();
process.exit(0);
}
const { values } = parseArgs({
args: args.slice(1),
options: {
input: { type: 'string', short: 'i' },
output: { type: 'string', short: 'o' },
key: { type: 'string' },
preset: { type: 'string' },
payload: { type: 'string' },
frames: { type: 'string' },
crf: { type: 'string' },
},
});
const input = values.input;
const key = values.key;
const presetName = (values.preset || 'moderate') as PresetName;
if (!input) { console.error('Error: --input is required'); process.exit(1); }
if (!key) { console.error('Error: --key is required'); process.exit(1); }
const config = getPreset(presetName);
if (command === 'embed') {
const output = values.output;
if (!output) { console.error('Error: --output is required for embed'); process.exit(1); }
const payloadHex = values.payload || 'DEADBEEF';
const payload = hexToBytes(payloadHex);
const crf = parseInt(values.crf || '18', 10);
console.log(`Embedding watermark...`);
console.log(` Input: ${input}`);
console.log(` Output: ${output}`);
console.log(` Preset: ${presetName}`);
console.log(` Payload: ${payloadHex}`);
console.log(` CRF: ${crf}`);
const info = await probeVideo(input);
console.log(` Video: ${info.width}x${info.height} @ ${info.fps.toFixed(2)} fps, ${info.totalFrames} frames`);
const encoder = createEncoder(output, info.width, info.height, info.fps, crf);
let frameCount = 0;
let totalPsnr = 0;
const ySize = info.width * info.height;
const uvSize = (info.width / 2) * (info.height / 2);
for await (const frame of readYuvFrames(input, info.width, info.height)) {
const result = embedWatermark(frame.y, info.width, info.height, payload, key, config);
totalPsnr += result.psnr;
// Write YUV420p frame: watermarked Y + original U + V
const yuvFrame = Buffer.alloc(ySize + 2 * uvSize);
yuvFrame.set(result.yPlane, 0);
yuvFrame.set(frame.u, ySize);
yuvFrame.set(frame.v, ySize + uvSize);
encoder.stdin.write(yuvFrame);
frameCount++;
if (frameCount % 30 === 0) {
process.stdout.write(`\r Progress: ${frameCount} frames (PSNR: ${(totalPsnr / frameCount).toFixed(1)} dB)`);
}
}
encoder.stdin.end();
await new Promise<void>((resolve) => encoder.process.on('close', () => resolve()));
console.log(`\r Complete: ${frameCount} frames, avg PSNR: ${(totalPsnr / frameCount).toFixed(1)} dB`);
console.log(` Output saved to: ${output}`);
} else if (command === 'detect') {
const maxFrames = parseInt(values.frames || '10', 10);
console.log(`Detecting watermark...`);
console.log(` Input: ${input}`);
console.log(` Preset: ${presetName}`);
console.log(` Frames: ${maxFrames}`);
const info = await probeVideo(input);
console.log(` Video: ${info.width}x${info.height} @ ${info.fps.toFixed(2)} fps`);
const yPlanes: Uint8Array[] = [];
let frameCount = 0;
for await (const frame of readYuvFrames(input, info.width, info.height)) {
yPlanes.push(new Uint8Array(frame.y));
frameCount++;
if (frameCount >= maxFrames) break;
process.stdout.write(`\r Reading frame ${frameCount}/${maxFrames}...`);
}
console.log(`\r Analyzing ${yPlanes.length} frames...`);
// Try multi-frame detection first
if (yPlanes.length > 1) {
const result = detectWatermarkMultiFrame(yPlanes, info.width, info.height, key, config);
if (result.detected) {
console.log(`\n WATERMARK DETECTED (multi-frame)`);
console.log(` Payload: ${bytesToHex(result.payload!)}`);
console.log(` Confidence: ${(result.confidence * 100).toFixed(1)}%`);
console.log(` Tiles: ${result.tilesDecoded}/${result.tilesTotal}`);
process.exit(0);
}
}
// Try single-frame detection
for (let i = 0; i < yPlanes.length; i++) {
const result = detectWatermark(yPlanes[i], info.width, info.height, key, config);
if (result.detected) {
console.log(`\n WATERMARK DETECTED (frame ${i + 1})`);
console.log(` Payload: ${bytesToHex(result.payload!)}`);
console.log(` Confidence: ${(result.confidence * 100).toFixed(1)}%`);
console.log(` Tiles: ${result.tilesDecoded}/${result.tilesTotal}`);
process.exit(0);
}
}
console.log(`\n No watermark detected.`);
process.exit(1);
} else {
console.error(`Unknown command: ${command}`);
usage();
}
}
function hexToBytes(hex: string): Uint8Array {
const clean = hex.replace(/^0x/, '');
const padded = clean.length % 2 ? '0' + clean : clean;
const bytes = new Uint8Array(padded.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(padded.slice(i * 2, i * 2 + 2), 16);
}
return bytes;
}
function bytesToHex(bytes: Uint8Array): string {
return Array.from(bytes).map((b) => b.toString(16).padStart(2, '0')).join('').toUpperCase();
}
main().catch((e) => {
console.error('Error:', e.message || e);
process.exit(1);
});
|