bitrate input for a vidarchv per-VOD basis

This commit is contained in:
2026-03-08 16:28:27 -06:00
parent 02d901e57e
commit baf17ec369
3 changed files with 567 additions and 0 deletions
@@ -0,0 +1,268 @@
<!-- Generated by SpecStory, Markdown v2.1.0 -->
<!-- cursor Session f004580b-70e3-4556-bee1-79aab7fd2613 (2026-03-08 21:40Z) -->
# Bitrate input for encode_VOD scripts (2026-03-08 21:40Z)
_**User**_
Create a new version of @encode_VOD.py and @encode_VOD.bat that asks the user to input a bitrate, and automatically populates b:v, maxrate, and bufsize from this number.
---
_**Agent (model kimi-k2.5, mode Agent)**_
<think><details><summary>Thought Process</summary>
The user wants me to create new versions of two files:
1. `encode_VOD.py` - a Python script
2. `encode_VOD.bat` - a batch file
The new versions should:
- Ask the user to input a bitrate
- Automatically populate `b:v`, `maxrate`, and `bufsize` from this number
Let me first read the existing files to understand their structure and content.</details></think>
---
I'll first read the existing files to understand their current structure, then create new versions with the bitrate input functionality.
---
<tool-use data-tool-type="read" data-tool-name="read_file_v2">
<details><summary>Tool use: **read_file_v2** • Read file: R:\Videos\mux\encode_VOD.py</summary>
</details>
</tool-use>
---
<tool-use data-tool-type="generic" data-tool-name="glob_file_search">
<details>
<summary>Tool use: **glob_file_search** • Searched codebase "undefined" • **1** result in **1** directory</summary>
Directory: **undefined** (1 file)
| File |
|------|
| `encode_VOD.bat` |
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
Let me read the batch file too.</details></think>
---
<tool-use data-tool-type="read" data-tool-name="read_file_v2">
<details><summary>Tool use: **read_file_v2** • Read file: R:\Videos\mux\encode_VOD.bat</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
Now I understand the structure of both files. Let me create new versions that:
1. Ask the user for a bitrate input
2. Calculate `b:v`, `maxrate`, and `bufsize` from that number
Looking at the current code:
- `b:v` is set to `10000k` (video bitrate)
- `maxrate` is set to `10000k` (maximum bitrate)
- `bufsize` is set to `20000k` (buffer size, which is 2x the bitrate)
The relationship seems to be:
- `b:v` = user input
- `maxrate` = user input (same as b:v)
- `bufsize` = 2x user input
I'll create new versions of both files with this functionality.</details></think>
---
Now I'll create the new versions with bitrate input functionality. The new files will ask for a bitrate and automatically set `b:v`, `maxrate` (same as bitrate), and `bufsize` (2x bitrate).
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: R:\Videos\mux\encode_VOD.py</summary>
</details>
</tool-use>
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: R:\Videos\mux\encode_VOD.bat</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
Both files have been written. Let me summarize the changes made.</details></think>
---
Done! I've updated both files with bitrate input functionality.
**Changes made:**
1. **`encode_VOD.py`** - Added a `get_bitrate()` function that prompts the user to enter a bitrate in kbps. The function:
- Accepts numeric input (e.g., 10000 for 10 Mbps)
- Validates that the input is a positive number
- Passes the bitrate to `encode_dvr()` which automatically calculates:
- `b:v` = bitrate + 'k' (e.g., "10000k")
- `maxrate` = bitrate + 'k' (same as b:v)
- `bufsize` = (bitrate * 2) + 'k' (double the bitrate)
2. **`encode_VOD.bat`** - Remains unchanged (just runs the Python script and pauses)
The script now prompts for both GPU selection and bitrate before processing files, and displays the calculated bitrate settings for each file being encoded.
---
_**User**_
I said create a new version, not edit the existing scripts.
---
_**Agent (model kimi-k2.5, mode Agent)**_
<think><details><summary>Thought Process</summary>
The user is upset because I edited the existing files instead of creating new versions as they requested. I need to:
1. Restore the original files from git
2. Create new versions with different names
Let me first check what the original files looked like by restoring them, then create new versions with the bitrate functionality.</details></think>
---
You're right, I apologize for the confusion. Let me restore the original files and create new versions with different names.
---
<tool-use data-tool-type="shell" data-tool-name="run_terminal_command_v2">
<details><summary>Tool use: **run_terminal_command_v2** • Run command: git checkout encode_VOD.py encode_VOD.bat</summary>
```bash
git checkout encode_VOD.py encode_VOD.bat
```
```
Updated 2 paths from the index
```
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
Good, the original files have been restored. Now I need to create new versions with different names. I'll create:
- `encode_VOD_bitrate.py` and `encode_VOD_bitrate.bat`
Or maybe:
- `encode_VOD_v2.py` and `encode_VOD_v2.bat`
Let me create the new versions with "bitrate" in the name to make it clear what they do.</details></think>
---
Now I'll create the new versions with different names.
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: R:\Videos\mux\encode_VOD_bitrate.py</summary>
</details>
</tool-use>
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: R:\Videos\mux\encode_VOD_bitrate.bat</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
I've created the new versions with the bitrate functionality. The original files are restored and new versions have been created with "_bitrate" suffix.</details></think>
---
Done! I've restored the original files and created new versions:
- `encode_VOD_bitrate.py` - New version that asks for bitrate input
- `encode_VOD_bitrate.bat` - Batch file to run the new version
The original `encode_VOD.py` and `encode_VOD.bat` files remain unchanged.
**How the new version works:**
1. Asks for GPU selection (same as original)
2. Asks for bitrate in kbps (e.g., enter `10000` for 10 Mbps)
3. Automatically calculates:
- `b:v` = 10000k
- `maxrate` = 10000k (same as bitrate)
- `bufsize` = 20000k (2x bitrate)
To use the new version, run `encode_VOD_bitrate.bat` instead.
---
_**User**_
It's not reporting the bitrate, time, or time elapsed anymore?
Progress: frame= 41 fps= 40 q=25.0 size= 0KiB time=N/A bitrate=N/A speed=N/A elapsed=0:00:01.01
Progress: frame= 121 fps= 60 q=28.0 size= 0KiB time=N/A bitrate=N/A speed=N/A elapsed=0:00:02.02
Progress: frame= 205 fps= 67 q=29.0 size= 0KiB time=N/A bitrate=N/A speed=N/A elapsed=0:00:03.05
Progress: frame= 283 fps= 70 q=30.0 size= 0KiB time=N/A bitrate=N/A speed=N/A elapsed=0:00:04.06
Progress: frame= 363 fps= 71 q=32.0 size= 0KiB time=N/A bitrate=N/A speed=N/A elapsed=0:00:05.07
Progress: frame= 443 fps= 73 q=30.0 size= 0KiB time=N/A bitrate=N/A speed=N/A elapsed=0:00:06.09
Progress: frame= 525 fps= 74 q=32.0 size= 0KiB time=N/A bitrate=N/A speed=N/A elapsed=0:00:07.10
Progress: frame= 605 fps= 74 q=14.0 size= 1792KiB time=N/A bitrate=N/A dup=0 drop=2 speed=N/A elapsed=0:00:08.12
Progress: frame= 685 fps= 75 q=26.0 size= 3072KiB time=N/A bitrate=N/A dup=0 drop=2 speed=N/A elapsed=0:00:09.14
Progress: frame= 767 fps= 76 q=30.0 size= 4608KiB time=N/A bitrate=N/A dup=0 drop=2 speed=N/A elapsed=0:00:10.15
Progress: frame= 847 fps= 76 q=29.0 size= 6144KiB time=N/A bitrate=N/A dup=0 drop=2 speed=N/A elapsed=0:00:11.18
Progress: frame= 927 fps= 76 q=29.0 size= 7680KiB time=N/A bitrate=N/A dup=0 drop=2 speed=N/A elapsed=0:00:12.18
Progress: frame= 1005 fps= 76 q=28.0 size= 8960KiB time=N/A bitrate=N/A dup=0 drop=2 speed=N/A elapsed=0:00:13.19
Progress: frame= 1089 fps= 77 q=29.0 size= 10752KiB time=N/A bitrate=N/A dup=0 drop=2 speed=N/A elapsed=0:00:14.22
Progress: frame= 1165 fps= 76 q=29.0 size= 12032KiB time=N/A bitrate=N/A dup=0 drop=2 speed=N/A elapsed=0:00:15.23
Progress: frame= 1247 fps= 77 q=23.0 size= 13568KiB time=N/A bitrate=N/A dup=0 drop=2 speed=N/A elapsed=0:00:16.24
Progress: frame= 1327 fps= 77 q=29.0 size= 15104KiB time=N/A bitrate=N/A dup=0 drop=2 speed=N/A elapsed=0:00:17.26
Progress: frame= 1407 fps= 77 q=29.0 size= 16384KiB time=N/A bitrate=N/A dup=0 drop=2 speed=N/A elapsed=0:00:18.27
Progress: frame= 1490 fps= 77 q=29.0 size= 17920KiB time=N/A bitrate=N/A dup=0 drop=2 speed=N/A elapsed=0:00:19.30
Progress: frame= 1569 fps= 77 q=32.0 size= 19456KiB time=N/A bitrate=N/A dup=0 drop=2 speed=N/A elapsed=0:00:20.31
Progress: frame= 1649 fps= 77 q=31.0 size= 20992KiB time=N/A bitrate=N/A dup=0 drop=2 speed=N/A elapsed=0:00:21.32
Progress: frame= 1729 fps= 77 q=31.0 size= 22528KiB time=N/A bitrate=N/A dup=0 drop=2 speed=N/A elapsed=0:00:22.35
Progress: frame= 1811 fps= 78 q=15.0 size= 24064KiB time=N/A bitrate=N/A dup=0 drop=2 speed=N/A elapsed=0:00:23.36
---
_**Agent (model kimi-k2.5, mode Agent)**_
<think><details><summary>Thought Process</summary>
The user is showing the progress output from ffmpeg. Looking at the output, the user</details></think>
---
+3
View File
@@ -0,0 +1,3 @@
@echo off
python encode_VOD_bitrate.py
pause
+296
View File
@@ -0,0 +1,296 @@
import os
import subprocess
from pathlib import Path
import json
import logging
from datetime import datetime
import shutil
import time
# ANSI color codes
class Colors:
PURPLE = '\033[95m'
BLUE = '\033[94m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
RED = '\033[91m'
ENDC = '\033[0m'
def get_gpu_selection():
while True:
print(f"\n{Colors.BLUE}Select GPU slot:{Colors.ENDC}")
print("0 - First GPU")
print("1 - Second GPU")
print("2 - Third GPU")
gpu = input(f"{Colors.YELLOW}Enter GPU number (0-2):{Colors.ENDC} ").strip()
if gpu in ['0', '1', '2']:
return gpu
print(f"{Colors.RED}Invalid selection. Please try again.{Colors.ENDC}")
def get_bitrate():
while True:
print(f"\n{Colors.BLUE}Select video bitrate:{Colors.ENDC}")
print("Common values: 5000k (5 Mbps), 8000k (8 Mbps), 10000k (10 Mbps), 15000k (15 Mbps)")
bitrate = input(f"{Colors.YELLOW}Enter bitrate in kbps (e.g., 10000 for 10 Mbps):{Colors.ENDC} ").strip()
try:
bitrate_val = int(bitrate)
if bitrate_val > 0:
return bitrate_val
print(f"{Colors.RED}Bitrate must be positive. Please try again.{Colors.ENDC}")
except ValueError:
print(f"{Colors.RED}Invalid input. Please enter a number.{Colors.ENDC}")
# Custom file handler that silently handles I/O errors (for network shares)
class SafeFileHandler(logging.FileHandler):
"""File handler that silently handles I/O errors during flush"""
def flush(self):
"""Override flush to silently handle I/O errors"""
try:
super().flush()
except (OSError, IOError):
pass
except Exception:
pass
def emit(self, record):
"""Override emit to handle errors gracefully"""
try:
super().emit(record)
except (OSError, IOError):
self.handleError(record)
except Exception:
self.handleError(record)
def handleError(self, record):
"""Override to prevent error messages from being printed"""
pass
# Set up logging
log_dir = "logs"
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, f"encode_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log")
handler = SafeFileHandler(log_file, mode='w', encoding='utf-8')
handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger = logging.getLogger()
logger.setLevel(logging.INFO)
logger.addHandler(handler)
logger.handlers = [handler]
def get_file_info(input_file):
cmd = [
'ffprobe',
'-v', 'error',
'-show_entries', 'format=duration,size:stream=codec_name,width,height,r_frame_rate,channels,channel_layout',
'-of', 'json',
input_file
]
result = subprocess.run(cmd, capture_output=True, text=True)
return json.loads(result.stdout)
def get_audio_labels(input_file):
cmd = [
'ffprobe',
'-v', 'error',
'-select_streams', 'a',
'-show_entries', 'stream=index:stream_tags=title',
'-of', 'json',
input_file
]
result = subprocess.run(cmd, capture_output=True, text=True)
info = json.loads(result.stdout)
labels = []
for stream in info.get('streams', []):
title = stream.get('tags', {}).get('title', None)
labels.append(title)
return labels
def format_size(size_bytes):
for unit in ['B', 'KB', 'MB', 'GB']:
if size_bytes < 1024:
return f"{size_bytes:.2f} {unit}"
size_bytes /= 1024
return f"{size_bytes:.2f} TB"
def safe_log_info(message, print_msg=None):
try:
logging.info(message)
except (OSError, IOError) as e:
if print_msg is None:
print(f"{Colors.YELLOW}[Logging failed: {e}] {message}{Colors.ENDC}")
else:
print(print_msg)
print(f"{Colors.YELLOW}[Logging failed: {e}]{Colors.ENDC}")
return
except Exception as e:
if print_msg is None:
print(f"{Colors.YELLOW}[Logging error: {e}] {message}{Colors.ENDC}")
else:
print(print_msg)
print(f"{Colors.YELLOW}[Logging error: {e}]{Colors.ENDC}")
return
if print_msg is not None:
print(print_msg)
def safe_log_error(message, print_msg=None):
try:
logging.error(message)
except (OSError, IOError) as e:
if print_msg is None:
print(f"{Colors.RED}[Logging failed: {e}] {message}{Colors.ENDC}")
else:
print(print_msg)
print(f"{Colors.RED}[Logging failed: {e}]{Colors.ENDC}")
return
except Exception as e:
if print_msg is None:
print(f"{Colors.RED}[Logging error: {e}] {message}{Colors.ENDC}")
else:
print(print_msg)
print(f"{Colors.RED}[Logging error: {e}]{Colors.ENDC}")
return
if print_msg is not None:
print(print_msg)
else:
print(f"{Colors.RED}{message}{Colors.ENDC}")
def encode_dvr(input_file, output_dir, gpu, bitrate):
input_path = Path(input_file)
output_path = Path(output_dir) / f"{input_path.stem}{input_path.suffix}"
b_v = f"{bitrate}k"
maxrate = f"{bitrate}k"
bufsize = f"{bitrate * 2}k"
file_info = get_file_info(str(input_path))
input_size = int(file_info['format']['size'])
duration = float(file_info['format']['duration'])
safe_log_info(f"Processing file: {input_path}")
safe_log_info(f"Input size: {format_size(input_size)}")
safe_log_info(f"Duration: {duration:.2f} seconds")
safe_log_info(f"Bitrate settings - b:v: {b_v}, maxrate: {maxrate}, bufsize: {bufsize}")
print(f"\n{Colors.BLUE}Processing file: {input_path}{Colors.ENDC}")
print(f"Input size: {format_size(input_size)}")
print(f"Duration: {duration:.2f} seconds")
print(f"Bitrate settings - b:v: {b_v}, maxrate: {maxrate}, bufsize: {bufsize}")
for i, stream in enumerate(file_info.get('streams', [])):
stream_type = 'Video' if stream.get('codec_name', '').startswith('h') else 'Audio'
safe_log_info(f"Stream {i} ({stream_type}):")
for key, value in stream.items():
safe_log_info(f" {key}: {value}")
if output_path.exists():
output_size = output_path.stat().st_size
safe_log_info(f"Skipping {input_path} - output already exists: {output_path}")
safe_log_info(f"Output size: {format_size(output_size)}")
print(f"{Colors.YELLOW}Skipping {input_path} - output already exists{Colors.ENDC}")
return
audio_labels = get_audio_labels(str(input_path))
safe_log_info(f"Audio labels: {audio_labels}")
cmd = [
'ffmpeg',
'-v', 'info',
'-stats',
'-stats_period', '1.0',
'-i', str(input_path),
'-c:v', 'hevc_nvenc',
'-gpu', gpu,
'-preset', 'p7',
'-tune', 'hq',
'-rc', 'vbr',
'-rc-lookahead', '32',
'-spatial-aq', '1',
'-aq-strength', '15',
'-cq', '0',
'-b:v', b_v,
'-maxrate', maxrate,
'-bufsize', bufsize,
'-c:a', 'copy',
'-map', '0',
]
for idx, label in enumerate(audio_labels):
if label:
cmd += [f'-metadata:s:a:{idx}', f'title={label}']
cmd.append(str(output_path))
try:
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
bufsize=1
)
for line in iter(process.stdout.readline, ''):
if not line:
break
text = line.strip()
try:
if text.startswith('frame=') or ' fps=' in text:
safe_log_info(f"Progress: {text}", f"{Colors.PURPLE}Progress: {text}{Colors.ENDC}")
else:
safe_log_info(f"FFmpeg: {text}", f"{Colors.GREEN}FFmpeg: {text}{Colors.ENDC}")
except (OSError, IOError) as e:
safe_log_error(f"I/O error reading FFmpeg output: {e}")
break
except Exception as e:
safe_log_error(f"Unexpected error processing FFmpeg output: {e}")
process.wait()
if process.returncode == 0:
output_info = get_file_info(str(output_path))
output_size = int(output_info['format']['size'])
compression_ratio = input_size / output_size if output_size > 0 else 0
safe_log_info(f"Successfully encoded: {output_path}", f"{Colors.GREEN}Successfully encoded: {output_path}{Colors.ENDC}")
safe_log_info(f"Output size: {format_size(output_size)}")
safe_log_info(f"Compression ratio: {compression_ratio:.2f}x", f"Compression ratio: {compression_ratio:.2f}x")
else:
return_code = process.returncode
if return_code > 2147483647:
return_code = return_code - 4294967296
safe_log_error(f"FFmpeg process failed with return code {return_code}",
f"{Colors.RED}FFmpeg process failed with return code {return_code}{Colors.ENDC}")
except subprocess.CalledProcessError as e:
safe_log_error(f"Error encoding {input_path}: {e}", f"{Colors.RED}Error encoding {input_path}: {e}{Colors.ENDC}")
except Exception as e:
safe_log_error(f"Unexpected error encoding {input_path}: {type(e).__name__}: {e}",
f"{Colors.RED}Unexpected error encoding {input_path}: {e}{Colors.ENDC}")
if __name__ == "__main__":
gpu = get_gpu_selection()
safe_log_info(f"Selected GPU: {gpu}")
bitrate = get_bitrate()
safe_log_info(f"Selected bitrate: {bitrate}k")
input_dir = "input"
output_dir = "output"
os.makedirs(output_dir, exist_ok=True)
files = [f for f in os.listdir(input_dir) if f.endswith(('.mp4', '.DVR.mp4'))]
total_files = len(files)
if total_files == 0:
safe_log_info("No files to process in input directory", f"{Colors.YELLOW}No files to process in input directory{Colors.ENDC}")
else:
safe_log_info(f"Found {total_files} files to process", f"{Colors.BLUE}Found {total_files} files to process{Colors.ENDC}")
for i, file in enumerate(files, 1):
input_file = os.path.join(input_dir, file)
safe_log_info(f"Processing file {i}/{total_files}: {file}")
print(f"\n{Colors.BLUE}Processing file {i}/{total_files}: {file}{Colors.ENDC}")
encode_dvr(input_file, output_dir, gpu, bitrate)