From baf17ec369a2e7752b7d209bfa4fcb30a50f98da Mon Sep 17 00:00:00 2001 From: Raincloud Date: Sun, 8 Mar 2026 16:28:27 -0600 Subject: [PATCH] bitrate input for a vidarchv per-VOD basis --- ...0Z-bitrate-input-for-encode-vod-scripts.md | 268 ++++++++++++++++ encode_VOD_bitrate.bat | 3 + encode_VOD_bitrate.py | 296 ++++++++++++++++++ 3 files changed, 567 insertions(+) create mode 100644 .specstory/history/2026-03-08_21-40Z-bitrate-input-for-encode-vod-scripts.md create mode 100644 encode_VOD_bitrate.bat create mode 100644 encode_VOD_bitrate.py diff --git a/.specstory/history/2026-03-08_21-40Z-bitrate-input-for-encode-vod-scripts.md b/.specstory/history/2026-03-08_21-40Z-bitrate-input-for-encode-vod-scripts.md new file mode 100644 index 0000000..380606d --- /dev/null +++ b/.specstory/history/2026-03-08_21-40Z-bitrate-input-for-encode-vod-scripts.md @@ -0,0 +1,268 @@ + + + + +# 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)**_ + +
Thought Process +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.
+ +--- + +I'll first read the existing files to understand their current structure, then create new versions with the bitrate input functionality. + +--- + + +
Tool use: **read_file_v2** • Read file: R:\Videos\mux\encode_VOD.py + +
+
+ +--- + + +
+Tool use: **glob_file_search** • Searched codebase "undefined" • **1** result in **1** directory + +Directory: **undefined** (1 file) + +| File | +|------| +| `encode_VOD.bat` | + +
+
+ +--- + +
Thought Process +Let me read the batch file too.
+ +--- + + +
Tool use: **read_file_v2** • Read file: R:\Videos\mux\encode_VOD.bat + +
+
+ +--- + +
Thought Process +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.
+ +--- + +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: **edit_file_v2** • Edit file: R:\Videos\mux\encode_VOD.py + +
+ +
+ +--- + + + +
Tool use: **edit_file_v2** • Edit file: R:\Videos\mux\encode_VOD.bat + +
+ +
+ +--- + +
Thought Process +Both files have been written. Let me summarize the changes made.
+ +--- + +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)**_ + +
Thought Process +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.
+ +--- + +You're right, I apologize for the confusion. Let me restore the original files and create new versions with different names. + +--- + + +
Tool use: **run_terminal_command_v2** • Run command: git checkout encode_VOD.py encode_VOD.bat + +```bash +git checkout encode_VOD.py encode_VOD.bat +``` + +``` +Updated 2 paths from the index + +``` +
+
+ +--- + +
Thought Process +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.
+ +--- + +Now I'll create the new versions with different names. + +--- + + + +
Tool use: **edit_file_v2** • Edit file: R:\Videos\mux\encode_VOD_bitrate.py + +
+ +
+ +--- + + + +
Tool use: **edit_file_v2** • Edit file: R:\Videos\mux\encode_VOD_bitrate.bat + +
+ +
+ +--- + +
Thought Process +I've created the new versions with the bitrate functionality. The original files are restored and new versions have been created with "_bitrate" suffix.
+ +--- + +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)**_ + +
Thought Process +The user is showing the progress output from ffmpeg. Looking at the output, the user
+ +--- + diff --git a/encode_VOD_bitrate.bat b/encode_VOD_bitrate.bat new file mode 100644 index 0000000..573b4f3 --- /dev/null +++ b/encode_VOD_bitrate.bat @@ -0,0 +1,3 @@ +@echo off +python encode_VOD_bitrate.py +pause diff --git a/encode_VOD_bitrate.py b/encode_VOD_bitrate.py new file mode 100644 index 0000000..34aa12a --- /dev/null +++ b/encode_VOD_bitrate.py @@ -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)