+ How it works: Upload multiple PDF files, specify how many pages (x)
+ should be combined onto a single sheet, and the app will create a merged PDF with N-up layout.
+ Default is 6 pages per sheet (2x3 grid).
+
How it works: Upload multiple PDF files, specify how many pages (x)
- should be combined onto a single sheet, and the app will create a merged PDF with N-up layout.
- Default is 6 pages per sheet (2x3 grid).
+ should be combined onto a single sheet, and the app will create a merged PDF with N-up layout
+ on standard A4 pages. Default is 6 pages per sheet (2x3 grid).
+ Aspect ratio is preserved with center-scaling.
@@ -437,6 +438,10 @@
PDF N-Up Merger
updateProgress(50);
+ // Standard A4 dimensions in points (210mm x 297mm)
+ const A4_WIDTH = 595.28;
+ const A4_HEIGHT = 841.89;
+
// Process pages in batches
const totalPages = allPages.length;
for (let batch = 0; batch < totalPages; batch += pagesPerSheet) {
@@ -466,10 +471,11 @@
PDF N-Up Merger
if (embeddedPages.length === 0) continue;
- // Get the size of the first page as reference
- const { width, height } = pageSizes[0];
+ // Use A4 dimensions for the output page
+ const width = A4_WIDTH;
+ const height = A4_HEIGHT;
- // Create a new page with the same dimensions
+ // Create a new A4 page
const newPage = mergedPdf.addPage([width, height]);
// Zero margins for seamless tiling
From da88ca745e0ebacd81ee8acfd0a1d5b9e124a12e Mon Sep 17 00:00:00 2001
From: deletefromuser <45934893+deletefromuser@users.noreply.github.com>
Date: Sat, 23 May 2026 11:15:50 +0800
Subject: [PATCH 12/13] reduce video size with h265
---
bat/batch_compress.md | 110 ++++++++++++++++++++
bat/batch_compress.ps1 | 193 +++++++++++++++++++++++++++++++++++
bat/batch_compress_cpu.ps1 | 200 +++++++++++++++++++++++++++++++++++++
3 files changed, 503 insertions(+)
create mode 100644 bat/batch_compress.md
create mode 100644 bat/batch_compress.ps1
create mode 100644 bat/batch_compress_cpu.ps1
diff --git a/bat/batch_compress.md b/bat/batch_compress.md
new file mode 100644
index 0000000..5e9e245
--- /dev/null
+++ b/bat/batch_compress.md
@@ -0,0 +1,110 @@
+# Batch Video Compression Scripts
+
+This repository contains two PowerShell scripts designed to recursively scan your directories, find large video files, and compress them into highly efficient H.265 (HEVC) formats.
+
+Choose the script that best matches your system hardware and compression goals:
+1. **`batch_compress.ps1` (GPU Accelerated)**: Best for raw speed and keeping CPU usage at 0%.
+2. **`batch_compress_cpu.ps1` (CPU Optimized)**: Best for achieving the absolute smallest file sizes and maximum storage savings using high-quality CRF encoding.
+
+---
+
+## 🚀 Key Features Comparison
+
+| Feature | `batch_compress.ps1` (GPU) | `batch_compress_cpu.ps1` (CPU) |
+| :--- | :--- | :--- |
+| **Engine** | NVIDIA NVENC (`hevc_nvenc`) | Software x265 (`libx265`) |
+| **Hardware Reqs** | NVIDIA Graphics Card | Modern Multi-core CPU |
+| **Encoding Speed** | **Extremely Fast** (Hardware matrix blocks) | **Slower** (Deep software calculations) |
+| **Compression Efficiency** | Great size reduction | **Max Space Savings** (20%-40% smaller than GPU) |
+| **Encoding Mode** | Adaptive Target Bitrate | Constant Rate Factor (CRF) Quality Engine |
+| **Resolution Downscale** | Hardware-native (`scale_cuda`) | Software-native (`scale`) |
+
+### Shared Intelligent Logic:
+* **Anti-Bloat Bitrate Clamping**: Both scripts evaluate the original file's true bitrate via `ffprobe`. If your target settings calculate a bitrate higher than the original file, the script **automatically clamps the bitrate down** to prevent up-sampling file bloating.
+* **Auto-Downscaling**: Both scripts automatically identify videos larger than 720p (like 1080p or 4K) and scale them down to 720p. Files already at 720p or lower are processed at their native resolution.
+* **Smart JSON Metadata Parsing**: Uses robust `ffprobe -of json` mappings to cleanly pull resolutions and codecs without failing on unusual file names or system language barriers.
+* **Session Auditing**: Tracks individual processing stopwatches per file alongside a running session runtime counter.
+
+---
+
+## 🛠️ Prerequisites
+
+* **Operating System**: Windows 10 or 11 with PowerShell.
+* **Dependencies**: `ffmpeg` and `ffprobe` must be installed on your machine and added to your system environment `PATH` variable.
+* **For GPU Script Only**: An NVIDIA graphics card supporting HEVC hardware encoding.
+* **PowerShell Execution Policy**: By default, Windows blocks script execution. **Before running the scripts**, you must allow script execution for your current session by running:
+```powershell
+ Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process
+```
+---
+
+## 💻 Configuration & Usage Guide
+
+### Script Parameters
+
+| Parameter | Position | Data Type | Default Value | Description |
+| :--- | :---: | :---: | :---: | :--- |
+| `MinSize` | 0 | String | `"2GB"` | File size threshold. Smaller files are skipped. (e.g., `"500M"`, `"1G"`, `"2GB"`) |
+| `MBPerMinute` | 1 | Integer | `12` | Target storage allowed per minute of video. Acts as the target bitrate for GPU, or the absolute maximum cap ceiling for CPU. |
+| `CRF` *(CPU Only)* | 2 | Integer | `26` | Constant Rate Factor. Lower = better quality/larger file. Standard range is `24`-`28`. |
+
+### Practical Examples
+
+Open **PowerShell**, navigate (`cd`) to your video library root path, and execute your chosen script format:
+
+#### Option A: Running the GPU Version (`batch_compress.ps1`)
+
+**Default Execution (Files > 2GB at 12MB/min target):**
+```powershell
+.\batch_compress.ps1
+
+```
+
+**Targeting smaller files with higher quality margins (Files > 1GB at 25MB/min):**
+
+```powershell
+.\batch_compress.ps1 -MinSize "1GB" -MBPerMinute 25
+
+```
+
+#### Option B: Running the CPU Version (`batch_compress_cpu.ps1`)
+
+**Default Execution (CRF 26 balanced profile, 12MB/min hard cap ceiling):**
+
+```powershell
+.\batch_compress_cpu.ps1
+
+```
+
+**Aggressive Compression Mode (CRF 28 for extremely tiny file sizes):**
+
+```powershell
+.\batch_compress_cpu.ps1 -MinSize "1GB" -MBPerMinute 10 -CRF 28
+
+```
+
+**High-Fidelity Archival Mode (CRF 23 for crisp details, lifting the cap ceiling to 20MB/min):**
+
+```powershell
+.\batch_compress_cpu.ps1 "2GB" 20 23
+
+```
+
+---
+
+## 📊 Technical Processing Pipeline
+
+When a file enters the compression pipeline, it undergoes the following automated stages:
+
+1. **Scan**: Discovers `.mp4`, `.mkv`, `.avi`, and `.ts` files inside the target tree matching your `MinSize`.
+2. **Deduplication Check**: Instantly skips any files with `_x265` in the title or items where the output file already exists.
+3. **Inspection**: Extracts stream tracks, height profiles, and container bitrates natively in clean JSON.
+4. **Safety Verification**: Compares target values against source bitrates and applies safety clamping if required.
+5. **Transcode Execution**: Spawns your chosen encoder context:
+* **GPU**: `Source File ──> CUDA Decode ──> scale_cuda ──> hevc_nvenc ──> Output`
+* **CPU**: `Source File ──> Software Decode ──> scale ──> libx265 (CRF) ──> Output`
+
+
+6. **Reporting**: Computes exact Megabytes saved, prints session timers, and cleans up the thread pipeline for the next file.
+
+
diff --git a/bat/batch_compress.ps1 b/bat/batch_compress.ps1
new file mode 100644
index 0000000..f2a3592
--- /dev/null
+++ b/bat/batch_compress.ps1
@@ -0,0 +1,193 @@
+param (
+ # Parameter 1: Threshold for original files (Integer only, e.g., 2GB, 2G, 500MB, 500M)
+ [Parameter(Mandatory=$false, Position=0)]
+ [string]$MinSize = "2GB",
+
+ # Parameter 2: Target MB per minute of video length (Default: 12MB/min)
+ [Parameter(Mandatory=$false, Position=1)]
+ [int]$MBPerMinute = 12
+)
+
+Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process
+
+# --- 1. Native Integer Size Parsing ---
+try {
+ $minSizeBytes = Invoke-Expression $MinSize
+ if ($minSizeBytes -isnot [long] -and $minSizeBytes -isnot [int]) { throw "Invalid MinSize" }
+} catch {
+ Write-Error "[Args Error] Cannot parse size format. Use integer syntax like: 2G, 500M, 2GB"
+ exit 1
+}
+
+# --- 2. Bitrate Calculations ---
+$totalKbps = [math]::Round(($MBPerMinute * 1048576 * 8) / 60 / 1000)
+$audioKbps = 96
+$videoKbps = $totalKbps - $audioKbps
+
+if ($videoKbps -lt 100) {
+ Write-Error "[Config Error] The requested MB/min ($MBPerMinute MB) is too low to sustain video and audio."
+ exit 1
+}
+
+$currentDir = Get-Location
+$displayMinSizeGB = [math]::Round($minSizeBytes / 1GB, 2)
+
+Write-Host "Scanning root directory: $currentDir" -ForegroundColor Cyan
+Write-Host " -> Processing files larger than: $MinSize ($displayMinSizeGB GB)" -ForegroundColor Gray
+Write-Host " -> Target Size Metric: $MBPerMinute MB per minute of video length (Calculated Target: ${videoKbps}k)" -ForegroundColor Yellow
+Write-Host " -> Auto-Downscale: Yes (If > 720P -> Downscale to 720P)" -ForegroundColor Magenta
+Write-Host "--------------------------------------------------------"
+
+$targetFiles = Get-ChildItem -Path $currentDir -Recurse -File -Include "*.mp4","*.mkv","*.avi","*.ts" | Where-Object {
+ $_.Length -gt $minSizeBytes -and $_.Name -notlike "*_x265*"
+}
+
+if ($targetFiles.Count -eq 0) {
+ Write-Host "No files found matching the filter criteria." -ForegroundColor Green
+ exit 0
+}
+
+# START TOTAL BATCH TIMER
+$totalScriptTimer = [System.Diagnostics.Stopwatch]::StartNew()
+
+foreach ($file in $targetFiles) {
+ $OutputFile = Join-Path -Path $file.DirectoryName -ChildPath "$($file.BaseName)_x265$($file.Extension)"
+
+ if (Test-Path -Path $OutputFile -PathType Leaf) {
+ Write-Host "`n[SKIP] Already processed: $($file.Name)" -ForegroundColor Yellow
+ continue
+ }
+
+ $currentSizeGB = [math]::Round($file.Length / 1GB, 2)
+ Write-Host "`n[Task] Processing: $($file.Name) ($currentSizeGB GB)" -ForegroundColor Cyan
+ Write-Host " -> Encoding started at: $(Get-Date -Format 'HH:mm:ss')" -ForegroundColor Gray
+
+ # --- 3. Robust Metadata Tracking via ffprobe (JSON Style) ---
+ $width = 0
+ $height = 0
+ $vCodec = "unknown"
+ $aCodec = "unknown"
+ $sourceBitrateKbps = 0
+ $ffprobeError = $null
+ try {
+ $ffprobeArgs = @("-v", "error", "-show_entries", "stream=codec_type,codec_name,width,height", "-show_entries", "format=bit_rate", "-of", "json", $file.FullName)
+ $ffprobeOut = & ffprobe $ffprobeArgs 2>&1
+
+ if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrEmpty($ffprobeOut)) {
+ $metadata = $ffprobeOut | ConvertFrom-Json
+
+ # Isolate primary video and audio streams respectively
+ $vStream = $metadata.streams | Where-Object { $_.codec_type -eq "video" } | Select-Object -First 1
+ $aStream = $metadata.streams | Where-Object { $_.codec_type -eq "audio" } | Select-Object -First 1
+
+ if ($vStream) {
+ $vCodec = if ($vStream.codec_name) { $vStream.codec_name } else { "unknown" }
+ $width = if ($vStream.width) { [int]$vStream.width } else { 0 }
+ $height = if ($vStream.height) { [int]$vStream.height } else { 0 }
+ }
+ if ($aStream) {
+ $aCodec = if ($aStream.codec_name) { $aStream.codec_name } else { "unknown" }
+ }
+
+ # FIXED: Handle string-enclosed json number format safely using explicit conversions
+ if ($metadata.format -and $metadata.format.bit_rate) {
+ $rawBitrate = $metadata.format.bit_rate.ToString().Trim()
+ if ($rawBitrate -match '^\d+$') {
+ $sourceBitrateKbps = [math]::Round(([long]$rawBitrate) / 1000)
+ }
+ }
+
+ $resolutionDisplay = if ($width -gt 0 -and $height -gt 0) { "${width}x${height}" } else { "unknown" }
+ $bitrateDisplay = if ($sourceBitrateKbps -gt 0) { "${sourceBitrateKbps}k" } else { "unknown" }
+
+ Write-Host " -> Source Properties: Resolution [$resolutionDisplay] | Video [$vCodec] | Audio [$aCodec] | Total Bitrate: [$bitrateDisplay]" -ForegroundColor Gray
+ } else {
+ $ffprobeError = $ffprobeOut
+ throw "ffprobe failed"
+ }
+ } catch {
+ Write-Host " -> [Warning] Failed to detect stream metadata automatically." -ForegroundColor Yellow
+ if ($ffprobeError) {
+ Write-Host " Reason: $ffprobeError" -ForegroundColor Gray
+ }
+ Write-Host " Defaulting to safe mode: Processing without forced downscaling or bitrate capping." -ForegroundColor Gray
+ }
+
+ # --- 4. Dynamic Bitrate Capping Logic ---
+ $activeVideoKbps = $videoKbps
+ if ($sourceBitrateKbps -gt 0) {
+ # Calculate original video portion estimate (Total original bitrate minus allocated transcode audio bitrate)
+ $sourceVideoKbps = $sourceBitrateKbps - $audioKbps
+ if ($sourceVideoKbps -lt 100) { $sourceVideoKbps = 100 }
+
+ if ($videoKbps -gt $sourceVideoKbps) {
+ $activeVideoKbps = $sourceVideoKbps
+ Write-Host " -> [Notice] Target bitrate (${videoKbps}k) exceeds original source video bitrate (${sourceVideoKbps}k)." -ForegroundColor Yellow
+ Write-Host " Capping encoding target to match source: ${activeVideoKbps}k" -ForegroundColor Yellow
+ }
+ }
+
+ $maxKbps = [math]::Round($activeVideoKbps * 1.35)
+ $bufKbps = $activeVideoKbps * 2
+
+ # --- 5. Dynamic Scale Arguments Construction ---
+ $vfParam = @()
+ if ($height -gt 720) {
+ Write-Host " -> Detected Resolution: ${height}P (> 720P). Adding downscale filter." -ForegroundColor Magenta
+ $vfParam = @("-vf", "scale_cuda=-2:720")
+ } elseif ($height -gt 0) {
+ Write-Host " -> Detected Resolution: ${height}P (<= 720P). Keeping original resolution." -ForegroundColor Gray
+ }
+
+ Write-Host " -> Encoding with NVIDIA GPU acceleration..." -ForegroundColor Green
+
+ # START INDIVIDUAL VIDEO TIMER
+ $videoTimer = [System.Diagnostics.Stopwatch]::StartNew()
+
+ # Execute Transcode Pipeline using final calculated active bitrates
+ ffmpeg -loglevel warning -hwaccel cuda -hwaccel_device 0 -hwaccel_output_format cuda -extra_hw_frames 8 -threads 1 -i $file.FullName $vfParam -c:v hevc_nvenc -b:v "${activeVideoKbps}k" -maxrate "${maxKbps}k" -bufsize "${bufKbps}k" -preset p5 -c:a aac -b:a "${audioKbps}k" -y $OutputFile
+
+ # STOP INDIVIDUAL VIDEO TIMER
+ $videoTimer.Stop()
+ $elapsedVideo = $videoTimer.Elapsed
+
+ if ($LASTEXITCODE -eq 0) {
+ $newSize = (Get-Item $OutputFile).Length
+ $savedBytes = $file.Length - $newSize
+ $savedMB = [math]::Round($savedBytes / 1MB, 2)
+
+ # Format the time nicely into mm:ss or hh:mm:ss
+ $timeString = "{0:00}m {1:00}s" -f $elapsedVideo.Minutes, $elapsedVideo.Seconds
+ if ($elapsedVideo.Hours -gt 0) { $timeString = "{0}h " -f $elapsedVideo.Hours + $timeString }
+
+ if ($savedBytes -gt 0) {
+ Write-Host "[SUCCESS] Done in $timeString! Reduced file size by ${savedMB} MB." -ForegroundColor Green
+ } else {
+ Write-Host "[NOTICE] Complete in $timeString, but file size didn't shrink." -ForegroundColor Yellow
+ }
+ } else {
+ Write-Host "[FAILED] FFmpeg execution crashed after processing for $($elapsedVideo.Minutes)m $($elapsedVideo.Seconds)s." -ForegroundColor Red
+ }
+
+ # Display running total of how long the whole script session has been active
+ $currentTotalElapsed = $totalScriptTimer.Elapsed
+
+ $hours = [math]::Truncate($currentTotalElapsed.TotalHours).ToString("00")
+ $minutes = $currentTotalElapsed.Minutes.ToString("00")
+ $seconds = $currentTotalElapsed.Seconds.ToString("00")
+
+ Write-Host " -> Total session run time so far: ${hours}h ${minutes}m ${seconds}s" -ForegroundColor Gray
+ Write-Host "--------------------------------------------------------"
+}
+
+# STOP TOTAL BATCH TIMER
+$totalScriptTimer.Stop()
+$finalTotalElapsed = $totalScriptTimer.Elapsed
+
+$fHours = [math]::Truncate($finalTotalElapsed.TotalHours).ToString("00")
+$fMinutes = $finalTotalElapsed.Minutes.ToString("00")
+$fSeconds = $finalTotalElapsed.Seconds.ToString("00")
+$finalTimeString = "${fHours}h ${fMinutes}m ${fSeconds}s"
+
+Write-Host "`nAll batch process targets complete!" -ForegroundColor Green
+Write-Host "Total Processing Duration: $finalTimeString" -ForegroundColor Cyan
\ No newline at end of file
diff --git a/bat/batch_compress_cpu.ps1 b/bat/batch_compress_cpu.ps1
new file mode 100644
index 0000000..40e7f49
--- /dev/null
+++ b/bat/batch_compress_cpu.ps1
@@ -0,0 +1,200 @@
+param (
+ # Parameter 1: Threshold for original files (Integer only, e.g., 2GB, 2G, 500MB, 500M)
+ [Parameter(Mandatory=$false, Position=0)]
+ [string]$MinSize = "2GB",
+
+ # Parameter 2: Target MAX MB per minute cap of video length (Default: 12MB/min)
+ [Parameter(Mandatory=$false, Position=1)]
+ [int]$MBPerMinute = 12,
+
+ # Parameter 3: CRF Quality Value (Lower = Better Quality / Larger Size. 28 is standard for x265, 24-26 is high-quality)
+ [Parameter(Mandatory=$false, Position=2)]
+ [int]$CRF = 26
+)
+
+Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process
+
+# --- 1. Native Integer Size Parsing ---
+try {
+ $minSizeBytes = Invoke-Expression $MinSize
+ if ($minSizeBytes -isnot [long] -and $minSizeBytes -isnot [int]) { throw "Invalid MinSize" }
+} catch {
+ Write-Error "[Args Error] Cannot parse size format. Use integer syntax like: 2G, 500M, 2GB"
+ exit 1
+}
+
+# --- 2. Bitrate Cap Calculations ---
+$totalKbps = [math]::Round(($MBPerMinute * 1048576 * 8) / 60 / 1000)
+$audioKbps = 96
+$videoCapKbps = $totalKbps - $audioKbps
+
+if ($videoCapKbps -lt 100) {
+ Write-Error "[Config Error] The requested MB/min ($MBPerMinute MB) is too low to sustain video and audio."
+ exit 1
+}
+
+$currentDir = Get-Location
+$displayMinSizeGB = [math]::Round($minSizeBytes / 1GB, 2)
+
+Write-Host "Scanning root directory: $currentDir" -ForegroundColor Cyan
+Write-Host " -> Processing files larger than: $MinSize ($displayMinSizeGB GB)" -ForegroundColor Gray
+Write-Host " -> Target Quality: CRF $CRF (Lower means better quality)" -ForegroundColor Yellow
+Write-Host " -> Target Size Limit: Max cap of $MBPerMinute MB per minute (${videoCapKbps}k max video)" -ForegroundColor Yellow
+Write-Host " -> Auto-Downscale: Yes (If > 720P -> Downscale to 720P via CPU)" -ForegroundColor Magenta
+Write-Host "--------------------------------------------------------"
+
+$targetFiles = Get-ChildItem -Path $currentDir -Recurse -File -Include "*.mp4","*.mkv","*.avi","*.ts" | Where-Object {
+ $_.Length -gt $minSizeBytes -and $_.Name -notlike "*_x265*"
+}
+
+if ($targetFiles.Count -eq 0) {
+ Write-Host "No files found matching the filter criteria." -ForegroundColor Green
+ exit 0
+}
+
+# START TOTAL BATCH TIMER
+$totalScriptTimer = [System.Diagnostics.Stopwatch]::StartNew()
+
+foreach ($file in $targetFiles) {
+ $OutputFile = Join-Path -Path $file.DirectoryName -ChildPath "$($file.BaseName)_x265$($file.Extension)"
+
+ if (Test-Path -Path $OutputFile -PathType Leaf) {
+ Write-Host "`n[SKIP] Already processed: $($file.Name)" -ForegroundColor Yellow
+ continue
+ }
+
+ $currentSizeGB = [math]::Round($file.Length / 1GB, 2)
+ Write-Host "`n[Task] Processing: $($file.Name) ($currentSizeGB GB)" -ForegroundColor Cyan
+ Write-Host " -> Encoding started at: $(Get-Date -Format 'HH:mm:ss')" -ForegroundColor Gray
+
+ # --- 3. Robust Metadata Tracking via ffprobe (JSON Style) ---
+ $width = 0
+ $height = 0
+ $vCodec = "unknown"
+ $aCodec = "unknown"
+ $sourceBitrateKbps = 0
+ $ffprobeError = $null
+ try {
+ $ffprobeArgs = @("-v", "error", "-show_entries", "stream=codec_type,codec_name,width,height", "-show_entries", "format=bit_rate", "-of", "json", $file.FullName)
+ $ffprobeOut = & ffprobe $ffprobeArgs 2>&1
+
+ if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrEmpty($ffprobeOut)) {
+ $metadata = $ffprobeOut | ConvertFrom-Json
+
+ $vStream = $metadata.streams | Where-Object { $_.codec_type -eq "video" } | Select-Object -First 1
+ $aStream = $metadata.streams | Where-Object { $_.codec_type -eq "audio" } | Select-Object -First 1
+
+ if ($vStream) {
+ $vCodec = if ($vStream.codec_name) { $vStream.codec_name } else { "unknown" }
+ $width = if ($vStream.width) { [int]$vStream.width } else { 0 }
+ $height = if ($vStream.height) { [int]$vStream.height } else { 0 }
+ }
+ if ($aStream) {
+ $aCodec = if ($aStream.codec_name) { $aStream.codec_name } else { "unknown" }
+ }
+
+ if ($metadata.format -and $metadata.format.bit_rate) {
+ $rawBitrate = $metadata.format.bit_rate.ToString().Trim()
+ if ($rawBitrate -match '^\d+$') {
+ $sourceBitrateKbps = [math]::Round(([long]$rawBitrate) / 1000)
+ }
+ }
+
+ $resolutionDisplay = if ($width -gt 0 -and $height -gt 0) { "${width}x${height}" } else { "unknown" }
+ $bitrateDisplay = if ($sourceBitrateKbps -gt 0) { "${sourceBitrateKbps}k" } else { "unknown" }
+
+ Write-Host " -> Source Properties: Resolution [$resolutionDisplay] | Video [$vCodec] | Audio [$aCodec] | Total Bitrate: [$bitrateDisplay]" -ForegroundColor Gray
+ } else {
+ $ffprobeError = $ffprobeOut
+ throw "ffprobe failed"
+ }
+ } catch {
+ Write-Host " -> [Warning] Failed to detect stream metadata automatically." -ForegroundColor Yellow
+ if ($ffprobeError) {
+ Write-Host " Reason: $ffprobeError" -ForegroundColor Gray
+ }
+ Write-Host " Defaulting to safe mode: Processing without forced downscaling or bitrate capping." -ForegroundColor Gray
+ }
+
+ # --- 4. Dynamic Bitrate Capping Logic ---
+ $activeMaxVideoKbps = $videoCapKbps
+ if ($sourceBitrateKbps -gt 0) {
+ $sourceVideoKbps = $sourceBitrateKbps - $audioKbps
+ if ($sourceVideoKbps -lt 100) { $sourceVideoKbps = 100 }
+
+ # If user target cap is bigger than original file bitrate, lower the cap to match original file
+ if ($videoCapKbps -gt $sourceVideoKbps) {
+ $activeMaxVideoKbps = $sourceVideoKbps
+ Write-Host " -> [Notice] Target cap (${videoCapKbps}k) exceeds original video bitrate (${sourceVideoKbps}k)." -ForegroundColor Yellow
+ Write-Host " Lowering maximum cap ceiling to match source: ${activeMaxVideoKbps}k" -ForegroundColor Yellow
+ }
+ }
+
+ # Calculate VBR buffer sizes relative to our active maximum cap
+ $bufKbps = $activeMaxVideoKbps * 2
+
+ # --- 5. CPU Scaling Filter Selection ---
+ $vfParam = @()
+ if ($height -gt 720) {
+ Write-Host " -> Detected Resolution: ${height}P (> 720P). Adding CPU software downscale filter." -ForegroundColor Magenta
+ # Using standard CPU scale filter since we aren't using hardware decode pipelines
+ $vfParam = @("-vf", "scale=-2:720")
+ } elseif ($height -gt 0) {
+ Write-Host " -> Detected Resolution: ${height}P (<= 720P). Keeping original resolution." -ForegroundColor Gray
+ }
+
+ Write-Host " -> Encoding with libx265 on CPU (CRF Mode)..." -ForegroundColor Green
+
+ # START INDIVIDUAL VIDEO TIMER
+ $videoTimer = [System.Diagnostics.Stopwatch]::StartNew()
+
+ # Execute CPU Transcode Pipeline
+ # -c:v libx265 : standard high-efficiency CPU encoder
+ # -crf $CRF : constant rate factor quality engine
+ # -preset fast : balance point between compression speed and maximum file savings on CPU
+ ffmpeg -loglevel warning -i $file.FullName $vfParam -c:v libx265 -crf $CRF -preset fast -maxrate "${activeMaxVideoKbps}k" -bufsize "${bufKbps}k" -c:a aac -b:a "${audioKbps}k" -y $OutputFile
+
+ # STOP INDIVIDUAL VIDEO TIMER
+ $videoTimer.Stop()
+ $elapsedVideo = $videoTimer.Elapsed
+
+ if ($LASTEXITCODE -eq 0) {
+ $newSize = (Get-Item $OutputFile).Length
+ $savedBytes = $file.Length - $newSize
+ $savedMB = [math]::Round($savedBytes / 1MB, 2)
+
+ # Format the time nicely into mm:ss or hh:mm:ss
+ $timeString = "{0:00}m {1:00}s" -f $elapsedVideo.Minutes, $elapsedVideo.Seconds
+ if ($elapsedVideo.Hours -gt 0) { $timeString = "{0}h " -f $elapsedVideo.Hours + $timeString }
+
+ if ($savedBytes -gt 0) {
+ Write-Host "[SUCCESS] Done in $timeString! Reduced file size by ${savedMB} MB." -ForegroundColor Green
+ } else {
+ Write-Host "[NOTICE] Complete in $timeString, but file size didn't shrink." -ForegroundColor Yellow
+ }
+ } else {
+ Write-Host "[FAILED] FFmpeg execution crashed after processing for $($elapsedVideo.Minutes)m $($elapsedVideo.Seconds)s." -ForegroundColor Red
+ }
+
+ # Display running total of how long the whole script session has been active
+ $currentTotalElapsed = $totalScriptTimer.Elapsed
+
+ $hours = [math]::Truncate($currentTotalElapsed.TotalHours).ToString("00")
+ $minutes = $currentTotalElapsed.Minutes.ToString("00")
+ $seconds = $currentTotalElapsed.Seconds.ToString("00")
+
+ Write-Host " -> Total session run time so far: ${hours}h ${minutes}m ${seconds}s" -ForegroundColor Gray
+ Write-Host "--------------------------------------------------------"
+}
+
+# STOP TOTAL BATCH TIMER
+$totalScriptTimer.Stop()
+$finalTotalElapsed = $totalScriptTimer.Elapsed
+
+$fHours = [math]::Truncate($finalTotalElapsed.TotalHours).ToString("00")
+$fMinutes = $finalTotalElapsed.Minutes.ToString("00")
+$fSeconds = $finalTotalElapsed.Seconds.ToString("00")
+$finalTimeString = "${fHours}h ${fMinutes}m ${fSeconds}s"
+
+Write-Host "`nAll batch process targets complete!" -ForegroundColor Green
+Write-Host "Total Processing Duration: $finalTimeString" -ForegroundColor Cyan
\ No newline at end of file
From d6796d6089a4cb14b7638f07a5abe166decab34d Mon Sep 17 00:00:00 2001
From: deletefromuser <45934893+deletefromuser@users.noreply.github.com>
Date: Sun, 9 Aug 2026 17:11:24 +0800
Subject: [PATCH 13/13] add price monitor script
---
jsscript/pricemonitor/AGENTS.md | 31 +++
jsscript/pricemonitor/README.md | 37 +++
jsscript/pricemonitor/goldfresh.js | 187 +++++++++++++++
jsscript/pricemonitor/goldfresh_v2.js | 330 ++++++++++++++++++++++++++
jsscript/pricemonitor/test.html | 133 +++++++++++
5 files changed, 718 insertions(+)
create mode 100644 jsscript/pricemonitor/AGENTS.md
create mode 100644 jsscript/pricemonitor/README.md
create mode 100644 jsscript/pricemonitor/goldfresh.js
create mode 100644 jsscript/pricemonitor/goldfresh_v2.js
create mode 100644 jsscript/pricemonitor/test.html
diff --git a/jsscript/pricemonitor/AGENTS.md b/jsscript/pricemonitor/AGENTS.md
new file mode 100644
index 0000000..39e1011
--- /dev/null
+++ b/jsscript/pricemonitor/AGENTS.md
@@ -0,0 +1,31 @@
+# Repository Guidelines
+
+## Project Structure & Module Organization
+
+This repository contains standalone browser automation scripts at the root. `goldfresh.js` is the original price monitor; `goldfresh_v2.js` is the active enhanced monitor with minimum/maximum thresholds, alerts, and panel controls. `test.html` is a local manual harness: it supplies `#now_price`, simulates prices, and loads `goldfresh_v2.js`. Keep related scripts at the root unless a descriptive subdirectory becomes necessary. There are no shared modules or automated test directories.
+
+## Build, Test, and Development Commands
+
+There is no package manifest or build system. Before committing, run:
+
+```powershell
+node --check goldfresh.js
+node --check goldfresh_v2.js
+git diff --check
+```
+
+The Node commands validate syntax; the Git command finds whitespace errors. For manual testing, open `test.html` in a browser. Its simulation crosses the default 900 and 945 limits. Start the monitor, optionally select an audio file, and use the page controls to test custom prices.
+
+## Coding Style & Naming Conventions
+
+Use four-space indentation, semicolons, double quotes, and lower camelCase names (`targetPrice`, `stopMonitoring`). Wrap browser scripts in an IIFE. Use descriptive `price_` DOM IDs such as `price_start`, `price_reset`, and `price_status_icon`.
+
+Check DOM queries before use. Pair every created interval, timeout, `Audio`, or `AudioContext` with cleanup. When adding a notification path, ensure Reset and manual Stop can cancel it without changing unrelated monitoring state.
+
+## Testing Guidelines
+
+For monitor changes, manually test missing or invalid prices, invalid thresholds, both threshold crossings, stop/restart behavior, and selected-file versus fallback audio. Confirm alerts automatically stop monitoring, the indicator and Start/Stop controls update, Reset silences audio only, and the default fallback chime repeats five times but can be cancelled. Document manual steps in the pull request. If automated tests are added, put them in `test/` and name them after the script (for example, `test/goldfresh_v2.test.js`).
+
+## Commit & Pull Request Guidelines
+
+Use short imperative commit subjects, consistent with history (for example, `add price alert reset`). In pull requests, name the affected script, target page selectors, testing performed, and any visible panel changes. Include a screenshot or short recording for control-panel or browser-behavior updates.
\ No newline at end of file
diff --git a/jsscript/pricemonitor/README.md b/jsscript/pricemonitor/README.md
new file mode 100644
index 0000000..e8d42c0
--- /dev/null
+++ b/jsscript/pricemonitor/README.md
@@ -0,0 +1,37 @@
+# 黄金价格监控脚本
+
+`goldfresh_v2.js` 是当前推荐的浏览器价格监控脚本。它读取页面中的 `#now_price`,当价格到达设置的最低或最高阈值时,播放提醒并自动停止监控。
+
+## 文件说明
+
+- `goldfresh.js`:原始价格监控版本。
+- `goldfresh_v2.js`:当前推荐版本,支持上下限提醒、状态指示和音频控制。
+- `test.html`:本地测试页,提供模拟价格和手动调价功能。
+
+## 快速开始
+
+1. 在浏览器中打开 `test.html`。
+2. 设置最低和最高价格(默认分别为 `900` 和 `945`)。
+3. 可选择本地音频文件,或使用默认提示音。
+4. 点击 **Start** 开始监控。
+
+测试页会自动以每秒 5 的幅度在 `880` 到 `965` 间往返变化,可覆盖默认阈值。停止自动模拟后,也可以输入手动价格测试。
+
+## 控制面板
+
+- **提醒声音**:可选本地音频文件;未选择时使用默认双音提示。
+- **最低价格提醒 / 最高价格提醒**:设置价格下限和上限。
+- **Start / Stop**:开始或手动停止监控;状态圆点显示当前是否正在监控。
+- **Reset**:仅停止提醒声音,不改变监控状态。
+
+价格达到任一阈值后,监控会自动停止。已选音频优先播放;未选择时默认双音提示连续播放五次。Reset 和手动 Stop 都可立即停止声音。
+
+## 验证
+
+```powershell
+node --check goldfresh.js
+node --check goldfresh_v2.js
+git diff --check
+```
+
+浏览器可能需要用户先点击页面或 Start 按钮才能播放音频。
\ No newline at end of file
diff --git a/jsscript/pricemonitor/goldfresh.js b/jsscript/pricemonitor/goldfresh.js
new file mode 100644
index 0000000..2751ef2
--- /dev/null
+++ b/jsscript/pricemonitor/goldfresh.js
@@ -0,0 +1,187 @@
+// setInterval(refreshNameAndCode, 1000);
+
+(function () {
+ let timer = null;
+ let targetPrice = 945;
+ let minPrice = 900;
+ let above = false;
+ let below = false;
+
+ // 声音
+ function beep(times = 5) {
+ let count = 0;
+
+ function playOnce() {
+ const ctx = new (window.AudioContext || window.webkitAudioContext)();
+
+ function playTone(freq, start, duration) {
+ const oscillator = ctx.createOscillator();
+ const gain = ctx.createGain();
+
+ oscillator.type = "sine";
+ oscillator.frequency.value = freq;
+
+ gain.gain.setValueAtTime(0, ctx.currentTime + start);
+ gain.gain.linearRampToValueAtTime(
+ 0.25,
+ ctx.currentTime + start + 0.05
+ );
+ gain.gain.exponentialRampToValueAtTime(
+ 0.001,
+ ctx.currentTime + start + duration
+ );
+
+ oscillator.connect(gain);
+ gain.connect(ctx.destination);
+
+ oscillator.start(ctx.currentTime + start);
+ oscillator.stop(ctx.currentTime + start + duration);
+ }
+
+ // 叮咚
+ // playTone(880, 0, 0.25);
+ // playTone(660, 0.25, 0.4);
+ playTone(587, 0, 0.25);
+ playTone(660, 0.25, 0.4);
+
+ setTimeout(() => ctx.close(), 1000);
+
+ count++;
+
+ if (count < times) {
+ setTimeout(playOnce, 1000);
+ }
+ }
+
+ playOnce();
+}
+
+
+ // 检查价格
+ function checkPrice() {
+ const el = document.getElementById("now_price");
+ if (!el) return;
+
+ const price = parseFloat(el.innerText);
+
+ if (isNaN(price)) return;
+
+ console.log("当前价格:", price, "最低:", minPrice, "最高:", targetPrice);
+
+ if (price >= targetPrice && !above) {
+ above = true;
+ beep(5);
+ console.log("🔔 达到目标价格:", price);
+ }
+
+ // 跌回去后重新允许提醒
+ if (price < targetPrice) {
+ above = false;
+ }
+
+ if (price <= minPrice && !below) {
+ below = true;
+ beep(5);
+ console.log("低于最低价格:", price);
+ }
+
+ // 涨回去后重新允许下限提醒
+ if (price > minPrice) {
+ below = false;
+ }
+ }
+
+
+ // 创建控制面板
+ const panel = document.createElement("div");
+
+ panel.style = `
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ z-index: 999999;
+ background: white;
+ border: 2px solid #333;
+ padding: 12px;
+ border-radius: 8px;
+ font-size: 14px;
+ box-shadow: 0 0 10px #999;
+ `;
+
+ panel.innerHTML = `
+