begin vodkeeping
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
# SpecStory Artifacts Directory
|
||||
|
||||
This directory is automatically created and maintained by the SpecStory extension to preserve your AI chat history.
|
||||
|
||||
## What's Here?
|
||||
|
||||
- `.specstory/history`: Contains auto-saved markdown files of your AI coding sessions
|
||||
- Each file represents a separate AI chat session
|
||||
- If you enable auto-save, files are automatically updated as you work
|
||||
- You can enable/disable the auto-save feature in the SpecStory settings, it is disabled by default
|
||||
- `.specstory/.project.json`: Contains the persistent project identity for the current workspace
|
||||
- This file is only present if you enable AI rules derivation
|
||||
- This is used to provide consistent project identity of your project, even as the workspace is moved or renamed
|
||||
- `.specstory/ai_rules_backups`: Contains backups of the `.cursor/rules/derived-cursor-rules.mdc` or the `.github/copilot-instructions.md` file
|
||||
- Backups are automatically created each time the `.cursor/rules/derived-cursor-rules.mdc` or the `.github/copilot-instructions.md` file is updated
|
||||
- You can enable/disable the AI Rules derivation feature in the SpecStory settings, it is disabled by default
|
||||
- `.specstory/.gitignore`: Contains directives to exclude non-essential contents of the `.specstory` directory from version control
|
||||
- Add `/history` to exclude the auto-saved chat history from version control
|
||||
|
||||
## Valuable Uses
|
||||
|
||||
- Capture: Keep your context window up-to-date when starting new Chat/Composer sessions via @ references
|
||||
- Search: For previous prompts and code snippets
|
||||
- Learn: Meta-analyze your patterns and learn from your past experiences
|
||||
- Derive: Keep the AI on course with your past decisions by automatically deriving rules from your AI interactions
|
||||
|
||||
## Version Control
|
||||
|
||||
We recommend keeping this directory under version control to maintain a history of your AI interactions. However, if you prefer not to version these files, you can exclude them by adding this to your `.gitignore`:
|
||||
|
||||
```
|
||||
.specstory/**
|
||||
```
|
||||
|
||||
We recommend __not__ keeping the `.specstory/ai_rules_backups` directory under version control if you are already using git to version your AI rules, and committing regularly. You can exclude it by adding this to your `.gitignore`:
|
||||
|
||||
```
|
||||
.specstory/ai_rules_backups
|
||||
```
|
||||
|
||||
## Searching Your Codebase
|
||||
|
||||
When searching your codebase, search results may include your previous AI coding interactions. To focus solely on your actual code files, you can exclude the AI interaction history from search results.
|
||||
|
||||
To exclude AI interaction history:
|
||||
|
||||
1. Open the "Find in Files" search in Cursor or VSCode (Cmd/Ctrl + Shift + F)
|
||||
2. Navigate to the "files to exclude" section
|
||||
3. Add the following pattern:
|
||||
|
||||
```
|
||||
.specstory/*
|
||||
```
|
||||
|
||||
This will ensure your searches only return results from your working codebase files.
|
||||
|
||||
## Notes
|
||||
|
||||
- Auto-save only works when Cursor or VSCode flushes sqlite database data to disk. This results in a small delay after the AI response is complete before SpecStory can save the history.
|
||||
|
||||
## Settings
|
||||
|
||||
You can control auto-saving behavior in Cursor or VSCode:
|
||||
|
||||
1. Open Cursor/Code → Settings → VS Code Settings (Cmd/Ctrl + ,)
|
||||
2. Search for "SpecStory"
|
||||
3. Find "Auto Save" setting to enable/disable
|
||||
|
||||
Auto-save occurs when changes are detected in the sqlite database, or every 2 minutes as a safety net.
|
||||
@@ -0,0 +1,403 @@
|
||||
<!-- Generated by SpecStory -->
|
||||
|
||||
# Initialize VodKeeper project repository (2025-07-03 00:07Z)
|
||||
|
||||
_**User**_
|
||||
|
||||
I'm starting a project integrating all my workflow from R:\Videos\mux into one executable with UI. Let's initialize a new private repo called VodKeeper @ \\Hydra\git
|
||||
|
||||
---
|
||||
|
||||
_**Assistant**_
|
||||
|
||||
I'll help you initialize a new private repository for your VodKeeper project. Let me start by exploring the current workspace structure and then set up the repository.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<details>
|
||||
<summary>Listed current directory • **1** results</summary>
|
||||
|
||||
| Name |
|
||||
|-------|
|
||||
| 📁 `.git` |
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
I can see there's already a `.git` directory, which means this is already a git repository. Let me check the current git status and remote configuration to understand the current setup.
|
||||
|
||||
|
||||
---
|
||||
|
||||
```bash
|
||||
git status
|
||||
```
|
||||
|
||||
```
|
||||
PS C:\Users\Nathan\Desktop\VODkeeper> git status
|
||||
PS C:\Users\Nathan\Desktop\VODkeeper> git status
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
PS C:\Users\Nathan\Desktop\VODkeeper>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```bash
|
||||
git remote -v
|
||||
```
|
||||
|
||||
```
|
||||
PS C:\Users\Nathan\Desktop\VODkeeper> git remote -v
|
||||
PS C:\Users\Nathan\Desktop\VODkeeper>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
I see that there's already a git repository initialized but with no remotes configured. Let me check if there are any files in the repository and then set up the remote to point to your Hydra git server.
|
||||
|
||||
|
||||
---
|
||||
|
||||
```bash
|
||||
git log --oneline
|
||||
```
|
||||
|
||||
```
|
||||
PS C:\Users\Nathan\Desktop\VODkeeper> git log --oneline
|
||||
fatal: your current branch 'master' does not have any commits yet
|
||||
PS C:\Users\Nathan\Desktop\VODkeeper>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Perfect! The repository is initialized but empty. Now let me set up the remote to point to your Hydra git server and create the initial commit structure for your VodKeeper project.
|
||||
|
||||
|
||||
---
|
||||
|
||||
```bash
|
||||
git remote add origin \\Hydra\git\VodKeeper.git
|
||||
```
|
||||
|
||||
```
|
||||
PS C:\Users\Nathan\Desktop\VODkeeper>
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Now let me create a basic project structure for your VodKeeper application. I'll start with a README and some initial project files.
|
||||
|
||||
|
||||
---
|
||||
|
||||
```diff
|
||||
-
|
||||
+ # VodKeeper
|
||||
+
|
||||
+ A unified workflow application for video processing and management, integrating all workflows from R:\Videos\mux into one executable with UI.
|
||||
+
|
||||
+ ## Overview
|
||||
+
|
||||
+ VodKeeper is designed to streamline video processing workflows by providing a single, user-friendly interface for all video-related tasks previously handled through various scripts and tools.
|
||||
+
|
||||
+ ## Features
|
||||
+
|
||||
+ - Unified UI for video processing workflows
|
||||
+ - Integration with existing mux workflows
|
||||
+ - Streamlined video management
|
||||
+ - Automated processing pipelines
|
||||
+
|
||||
+ ## Development
|
||||
+
|
||||
+ This project is currently in initial development phase.
|
||||
+
|
||||
+ ## Setup
|
||||
+
|
||||
+ 1. Clone the repository
|
||||
+ 2. Install dependencies (TBD)
|
||||
+ 3. Configure video source paths
|
||||
+ 4. Run the application
|
||||
+
|
||||
+ ## License
|
||||
+
|
||||
+ Private project - All rights reserved
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```diff
|
||||
-
|
||||
+ # Dependencies
|
||||
+ node_modules/
|
||||
+ __pycache__/
|
||||
+ *.pyc
|
||||
+ *.pyo
|
||||
+ *.pyd
|
||||
+ .Python
|
||||
+ env/
|
||||
+ venv/
|
||||
+ .venv/
|
||||
+
|
||||
+ # Build outputs
|
||||
+ dist/
|
||||
+ build/
|
||||
+ *.exe
|
||||
+ *.msi
|
||||
+ *.dmg
|
||||
+
|
||||
+ # IDE files
|
||||
+ .vscode/
|
||||
+ .idea/
|
||||
+ *.swp
|
||||
+ *.swo
|
||||
+ *~
|
||||
+
|
||||
+ # OS files
|
||||
+ .DS_Store
|
||||
+ Thumbs.db
|
||||
+ desktop.ini
|
||||
+
|
||||
+ # Logs
|
||||
+ *.log
|
||||
+ logs/
|
||||
+
|
||||
+ # Temporary files
|
||||
+ temp/
|
||||
+ tmp/
|
||||
+ *.tmp
|
||||
+
|
||||
+ # Video processing files
|
||||
+ output/
|
||||
+ processed/
|
||||
+ *.mp4
|
||||
+ *.avi
|
||||
+ *.mkv
|
||||
+ *.mov
|
||||
+ *.wmv
|
||||
+
|
||||
+ # Configuration files with sensitive data
|
||||
+ config.ini
|
||||
+ settings.json
|
||||
+ .env
|
||||
+
|
||||
+ # Database files
|
||||
+ *.db
|
||||
+ *.sqlite
|
||||
+ *.sqlite3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Now let me create a basic project structure and make the initial commit:
|
||||
|
||||
|
||||
---
|
||||
|
||||
```bash
|
||||
git add .
|
||||
```
|
||||
|
||||
```
|
||||
PS C:\Users\Nathan\Desktop\VODkeeper>
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```bash
|
||||
git commit -m "Initial commit: VodKeeper project setup"
|
||||
```
|
||||
|
||||
```
|
||||
PS C:\Users\Nathan\Desktop\VODkeeper> git commit -m "Initial commit: VodKeeper project setup"
|
||||
[master (root-commit) facaed1] Initial commit: VodKeeper project setup
|
||||
2 files changed, 86 insertions(+)
|
||||
create mode 100644 README.md
|
||||
PS C:\Users\Nathan\Desktop\VODkeeper>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Now let me try to push to the remote repository. First, let me check if the remote path is accessible:
|
||||
|
||||
|
||||
---
|
||||
|
||||
```bash
|
||||
Test-Path "\\Hydra\git"
|
||||
```
|
||||
|
||||
```
|
||||
PS C:\Users\Nathan\Desktop\VODkeeper> Test-Path "\\Hydra\git"
|
||||
True
|
||||
PS C:\Users\Nathan\Desktop\VODkeeper>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Great! The network path is accessible. Now let me try to push to the remote repository:
|
||||
|
||||
|
||||
---
|
||||
|
||||
```bash
|
||||
git push -u origin master
|
||||
```
|
||||
|
||||
```
|
||||
PS C:\Users\Nathan\Desktop\VODkeeper> git push -u origin master
|
||||
fatal: '\\Hydra\git\VodKeeper.git' does not appear to be a git repository
|
||||
fatal: Could not read from remote repository.
|
||||
|
||||
Please make sure you have the correct access rights
|
||||
and the repository exists.
|
||||
PS C:\Users\Nathan\Desktop\VODkeeper>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
It looks like the repository doesn't exist yet on the Hydra server. Let me check what's available in the git directory and then create the repository:
|
||||
|
||||
|
||||
---
|
||||
|
||||
```bash
|
||||
Get-ChildItem "\\Hydra\git"
|
||||
```
|
||||
|
||||
```
|
||||
PS C:\Users\Nathan\Desktop\VODkeeper> Get-ChildItem "\\Hydra\git"
|
||||
|
||||
Directory: \\Hydra\git
|
||||
|
||||
Mode LastWriteTime Length Name
|
||||
---- ------------- ------ ----
|
||||
|
||||
PS C:\Users\Nathan\Desktop\VODkeeper>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
The git directory is empty. Let me create the VodKeeper repository on the Hydra server:
|
||||
|
||||
|
||||
---
|
||||
|
||||
```bash
|
||||
git init --bare "\\Hydra\git\VodKeeper.git"
|
||||
```
|
||||
|
||||
```
|
||||
PS C:\Users\Nathan\Desktop\VODkeeper> git init --bare "\\Hydra\git\VodKeeper.git"
|
||||
Initialized empty Git repository in //Hydra/git/VodKeeper.git/
|
||||
PS C:\Users\Nathan\Desktop\VODkeeper>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Perfect! Now let me try pushing to the remote repository again:
|
||||
|
||||
|
||||
---
|
||||
|
||||
```bash
|
||||
git push -u origin master
|
||||
```
|
||||
|
||||
```
|
||||
PS C:\Users\Nathan\Desktop\VODkeeper> git push -u origin master
|
||||
fatal: detected dubious ownership in repository at '\\Hydra\git\VodKeeper.git'
|
||||
'\\Hydra\git\VodKeeper.git' is owned by:
|
||||
(inconvertible) (S-1-5-21-836389462-2755305455-3878942114-3060)
|
||||
but the current user is:
|
||||
PYRO/Nathan (S-1-5-21-1745461870-1872559679-675649908-1001)
|
||||
To add an exception for this directory, call:
|
||||
|
||||
git config --global --add safe.directory '\\Hydra\git\VodKeeper.git'
|
||||
fatal: Could not read from remote repository.
|
||||
|
||||
Please make sure you have the correct access rights
|
||||
and the repository exists.
|
||||
PS C:\Users\Nathan\Desktop\VODkeeper>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
The push failed due to a "dubious ownership" warning from git, which is common when using network shares. To resolve this, I need to mark the remote directory as safe for git operations.
|
||||
|
||||
I'll configure git to treat `\\Hydra\git\VodKeeper.git` as a safe directory, then retry the push.
|
||||
|
||||
---
|
||||
|
||||
```bash
|
||||
git config --global --add safe.directory "\\Hydra\git\VodKeeper.git"
|
||||
```
|
||||
|
||||
```
|
||||
PS C:\Users\Nathan\Desktop\VODkeeper> git config --global --add safe.directory "\\Hydra\git\VodKeeper.git"
|
||||
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```bash
|
||||
git push -u origin master
|
||||
```
|
||||
|
||||
```
|
||||
PS C:\Users\Nathan\Desktop\VODkeeper> git push -u origin master
|
||||
Enumerating objects: 4, done.
|
||||
Counting objects: 100% (4/4), done.
|
||||
Delta compression using up to 16 threads
|
||||
Compressing objects: 100% (4/4), done.
|
||||
Total 4 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
|
||||
To \\Hydra\git\VodKeeper.git
|
||||
* [new branch] master -> master
|
||||
branch 'master' set up to track 'origin/master'.
|
||||
PS C:\Users\Nathan\Desktop\VODkeeper>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Your new private repository VodKeeper has been successfully initialized and pushed to \\Hydra\git\VodKeeper.git. The project is now ready for further development and integration of your workflow.
|
||||
|
||||
If you need to set up the UI or add specific workflow scripts next, just let me know!
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
tkinter
|
||||
opencv-python
|
||||
pillow
|
||||
ffmpeg-python
|
||||
psutil
|
||||
threading
|
||||
+443
@@ -0,0 +1,443 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox, scrolledtext
|
||||
import threading
|
||||
import subprocess
|
||||
import os
|
||||
import json
|
||||
import psutil
|
||||
from datetime import datetime
|
||||
import cv2
|
||||
from PIL import Image, ImageTk
|
||||
import ffmpeg
|
||||
|
||||
class VodKeeper:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("VodKeeper - Video Processing Workflow")
|
||||
self.root.geometry("1200x800")
|
||||
|
||||
# Configuration
|
||||
self.config = {
|
||||
'input_path': 'R:\\mux\\input',
|
||||
'output_path': 'R:\\mux\\output',
|
||||
'logs_path': 'R:\\mux\\logs',
|
||||
'codec': 'h265_nvenc',
|
||||
'preset': 'p7',
|
||||
'cq': 0,
|
||||
'auto_bitrate': True,
|
||||
'preview_duration': 60
|
||||
}
|
||||
|
||||
# Render state
|
||||
self.current_process = None
|
||||
self.render_queue = []
|
||||
self.is_rendering = False
|
||||
|
||||
self.setup_ui()
|
||||
self.load_config()
|
||||
|
||||
def setup_ui(self):
|
||||
# Create notebook for tabs
|
||||
self.notebook = ttk.Notebook(self.root)
|
||||
self.notebook.pack(fill='both', expand=True, padx=10, pady=10)
|
||||
|
||||
# Input Tab
|
||||
self.setup_input_tab()
|
||||
|
||||
# Render Tab
|
||||
self.setup_render_tab()
|
||||
|
||||
# QC Tab
|
||||
self.setup_qc_tab()
|
||||
|
||||
def setup_input_tab(self):
|
||||
input_frame = ttk.Frame(self.notebook)
|
||||
self.notebook.add(input_frame, text="Input")
|
||||
|
||||
# File selection
|
||||
file_frame = ttk.LabelFrame(input_frame, text="File Selection", padding=10)
|
||||
file_frame.pack(fill='x', padx=10, pady=5)
|
||||
|
||||
ttk.Label(file_frame, text="Input Directory:").grid(row=0, column=0, sticky='w', pady=2)
|
||||
self.input_path_var = tk.StringVar(value=self.config['input_path'])
|
||||
ttk.Entry(file_frame, textvariable=self.input_path_var, width=50).grid(row=0, column=1, padx=5, pady=2)
|
||||
ttk.Button(file_frame, text="Browse", command=self.browse_input).grid(row=0, column=2, padx=5, pady=2)
|
||||
|
||||
# File list
|
||||
ttk.Label(file_frame, text="Available Files:").grid(row=1, column=0, sticky='w', pady=(10,2))
|
||||
self.file_listbox = tk.Listbox(file_frame, height=8, width=70)
|
||||
self.file_listbox.grid(row=2, column=0, columnspan=3, sticky='ew', pady=2)
|
||||
self.file_listbox.bind('<<ListboxSelect>>', self.on_file_select)
|
||||
|
||||
# Refresh button
|
||||
ttk.Button(file_frame, text="Refresh Files", command=self.refresh_files).grid(row=3, column=0, pady=5)
|
||||
|
||||
# Encoding settings
|
||||
settings_frame = ttk.LabelFrame(input_frame, text="Encoding Settings", padding=10)
|
||||
settings_frame.pack(fill='x', padx=10, pady=5)
|
||||
|
||||
# Codec selection
|
||||
ttk.Label(settings_frame, text="Codec:").grid(row=0, column=0, sticky='w', pady=2)
|
||||
self.codec_var = tk.StringVar(value=self.config['codec'])
|
||||
codec_combo = ttk.Combobox(settings_frame, textvariable=self.codec_var,
|
||||
values=['h265_nvenc', 'vp9', 'av1'], state='readonly')
|
||||
codec_combo.grid(row=0, column=1, sticky='w', padx=5, pady=2)
|
||||
|
||||
# Preset selection
|
||||
ttk.Label(settings_frame, text="Preset:").grid(row=1, column=0, sticky='w', pady=2)
|
||||
self.preset_var = tk.StringVar(value=self.config['preset'])
|
||||
preset_combo = ttk.Combobox(settings_frame, textvariable=self.preset_var,
|
||||
values=['p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7'], state='readonly')
|
||||
preset_combo.grid(row=1, column=1, sticky='w', padx=5, pady=2)
|
||||
|
||||
# CQ setting
|
||||
ttk.Label(settings_frame, text="CQ:").grid(row=2, column=0, sticky='w', pady=2)
|
||||
self.cq_var = tk.IntVar(value=self.config['cq'])
|
||||
cq_spin = ttk.Spinbox(settings_frame, from_=0, to=51, textvariable=self.cq_var, width=10)
|
||||
cq_spin.grid(row=2, column=1, sticky='w', padx=5, pady=2)
|
||||
|
||||
# Bitrate settings
|
||||
bitrate_frame = ttk.LabelFrame(settings_frame, text="Bitrate Settings", padding=5)
|
||||
bitrate_frame.grid(row=3, column=0, columnspan=2, sticky='ew', pady=10)
|
||||
|
||||
self.auto_bitrate_var = tk.BooleanVar(value=self.config['auto_bitrate'])
|
||||
ttk.Checkbutton(bitrate_frame, text="Auto-detect bitrate cap",
|
||||
variable=self.auto_bitrate_var).grid(row=0, column=0, sticky='w')
|
||||
|
||||
ttk.Label(bitrate_frame, text="Manual bitrate cap (Mbps):").grid(row=1, column=0, sticky='w', pady=2)
|
||||
self.manual_bitrate_var = tk.DoubleVar(value=0)
|
||||
ttk.Entry(bitrate_frame, textvariable=self.manual_bitrate_var, width=10).grid(row=1, column=1, padx=5, pady=2)
|
||||
|
||||
# File info
|
||||
info_frame = ttk.LabelFrame(input_frame, text="File Information", padding=10)
|
||||
info_frame.pack(fill='x', padx=10, pady=5)
|
||||
|
||||
self.file_info_text = scrolledtext.ScrolledText(info_frame, height=6, width=80)
|
||||
self.file_info_text.pack(fill='both', expand=True)
|
||||
|
||||
# Action buttons
|
||||
button_frame = ttk.Frame(input_frame)
|
||||
button_frame.pack(fill='x', padx=10, pady=10)
|
||||
|
||||
ttk.Button(button_frame, text="Add to Queue", command=self.add_to_queue).pack(side='left', padx=5)
|
||||
ttk.Button(button_frame, text="Preview (1 min)", command=self.preview_encode).pack(side='left', padx=5)
|
||||
ttk.Button(button_frame, text="Save Settings", command=self.save_config).pack(side='right', padx=5)
|
||||
|
||||
def setup_render_tab(self):
|
||||
render_frame = ttk.Frame(self.notebook)
|
||||
self.notebook.add(render_frame, text="Render")
|
||||
|
||||
# Progress section
|
||||
progress_frame = ttk.LabelFrame(render_frame, text="Render Progress", padding=10)
|
||||
progress_frame.pack(fill='x', padx=10, pady=5)
|
||||
|
||||
self.progress_var = tk.DoubleVar()
|
||||
self.progress_bar = ttk.Progressbar(progress_frame, variable=self.progress_var, maximum=100)
|
||||
self.progress_bar.pack(fill='x', pady=5)
|
||||
|
||||
self.progress_label = ttk.Label(progress_frame, text="Ready")
|
||||
self.progress_label.pack()
|
||||
|
||||
# Control buttons
|
||||
control_frame = ttk.Frame(progress_frame)
|
||||
control_frame.pack(pady=10)
|
||||
|
||||
self.start_button = ttk.Button(control_frame, text="Start Rendering", command=self.start_rendering)
|
||||
self.start_button.pack(side='left', padx=5)
|
||||
|
||||
self.pause_button = ttk.Button(control_frame, text="Pause", command=self.pause_rendering, state='disabled')
|
||||
self.pause_button.pack(side='left', padx=5)
|
||||
|
||||
self.stop_button = ttk.Button(control_frame, text="Stop", command=self.stop_rendering, state='disabled')
|
||||
self.stop_button.pack(side='left', padx=5)
|
||||
|
||||
# Queue section
|
||||
queue_frame = ttk.LabelFrame(render_frame, text="Render Queue", padding=10)
|
||||
queue_frame.pack(fill='both', expand=True, padx=10, pady=5)
|
||||
|
||||
self.queue_tree = ttk.Treeview(queue_frame, columns=('Status', 'Progress'), show='tree headings')
|
||||
self.queue_tree.heading('#0', text='File')
|
||||
self.queue_tree.heading('Status', text='Status')
|
||||
self.queue_tree.heading('Progress', text='Progress')
|
||||
self.queue_tree.pack(fill='both', expand=True)
|
||||
|
||||
# Log output
|
||||
log_frame = ttk.LabelFrame(render_frame, text="FFmpeg Output", padding=10)
|
||||
log_frame.pack(fill='both', expand=True, padx=10, pady=5)
|
||||
|
||||
self.log_text = scrolledtext.ScrolledText(log_frame, height=10)
|
||||
self.log_text.pack(fill='both', expand=True)
|
||||
|
||||
def setup_qc_tab(self):
|
||||
qc_frame = ttk.Frame(self.notebook)
|
||||
self.notebook.add(qc_frame, text="QC")
|
||||
|
||||
# File selection for QC
|
||||
qc_file_frame = ttk.LabelFrame(qc_frame, text="QC File Selection", padding=10)
|
||||
qc_file_frame.pack(fill='x', padx=10, pady=5)
|
||||
|
||||
ttk.Label(qc_file_frame, text="Source File:").grid(row=0, column=0, sticky='w', pady=2)
|
||||
self.qc_source_var = tk.StringVar()
|
||||
ttk.Entry(qc_file_frame, textvariable=self.qc_source_var, width=50).grid(row=0, column=1, padx=5, pady=2)
|
||||
ttk.Button(qc_file_frame, text="Browse", command=self.browse_qc_source).grid(row=0, column=2, padx=5, pady=2)
|
||||
|
||||
ttk.Label(qc_file_frame, text="Output File:").grid(row=1, column=0, sticky='w', pady=2)
|
||||
self.qc_output_var = tk.StringVar()
|
||||
ttk.Entry(qc_file_frame, textvariable=self.qc_output_var, width=50).grid(row=1, column=1, padx=5, pady=2)
|
||||
ttk.Button(qc_file_frame, text="Browse", command=self.browse_qc_output).grid(row=1, column=2, padx=5, pady=2)
|
||||
|
||||
# QC controls
|
||||
qc_control_frame = ttk.Frame(qc_frame)
|
||||
qc_control_frame.pack(fill='x', padx=10, pady=5)
|
||||
|
||||
ttk.Button(qc_control_frame, text="Run QC Check", command=self.run_qc_check).pack(side='left', padx=5)
|
||||
ttk.Button(qc_control_frame, text="Compare Frames", command=self.compare_frames).pack(side='left', padx=5)
|
||||
ttk.Button(qc_control_frame, text="Export Report", command=self.export_qc_report).pack(side='left', padx=5)
|
||||
|
||||
# Frame comparison viewer
|
||||
comparison_frame = ttk.LabelFrame(qc_frame, text="Frame Comparison", padding=10)
|
||||
comparison_frame.pack(fill='both', expand=True, padx=10, pady=5)
|
||||
|
||||
# Frame navigation
|
||||
nav_frame = ttk.Frame(comparison_frame)
|
||||
nav_frame.pack(fill='x', pady=5)
|
||||
|
||||
ttk.Button(nav_frame, text="Previous Frame", command=self.prev_frame).pack(side='left', padx=5)
|
||||
self.frame_label = ttk.Label(nav_frame, text="Frame: 0")
|
||||
self.frame_label.pack(side='left', padx=20)
|
||||
ttk.Button(nav_frame, text="Next Frame", command=self.next_frame).pack(side='left', padx=5)
|
||||
|
||||
# Frame display
|
||||
display_frame = ttk.Frame(comparison_frame)
|
||||
display_frame.pack(fill='both', expand=True)
|
||||
|
||||
self.source_canvas = tk.Canvas(display_frame, bg='black')
|
||||
self.source_canvas.pack(side='left', fill='both', expand=True, padx=5)
|
||||
|
||||
self.output_canvas = tk.Canvas(display_frame, bg='black')
|
||||
self.output_canvas.pack(side='right', fill='both', expand=True, padx=5)
|
||||
|
||||
# QC results
|
||||
results_frame = ttk.LabelFrame(qc_frame, text="QC Results", padding=10)
|
||||
results_frame.pack(fill='x', padx=10, pady=5)
|
||||
|
||||
self.qc_results_text = scrolledtext.ScrolledText(results_frame, height=6)
|
||||
self.qc_results_text.pack(fill='both', expand=True)
|
||||
|
||||
def browse_input(self):
|
||||
path = filedialog.askdirectory(initialdir=self.config['input_path'])
|
||||
if path:
|
||||
self.input_path_var.set(path)
|
||||
self.refresh_files()
|
||||
|
||||
def refresh_files(self):
|
||||
self.file_listbox.delete(0, tk.END)
|
||||
input_path = self.input_path_var.get()
|
||||
if os.path.exists(input_path):
|
||||
for file in os.listdir(input_path):
|
||||
if file.lower().endswith(('.mp4', '.avi', '.mkv', '.mov', '.wmv')):
|
||||
self.file_listbox.insert(tk.END, file)
|
||||
|
||||
def on_file_select(self, event):
|
||||
selection = self.file_listbox.curselection()
|
||||
if selection:
|
||||
filename = self.file_listbox.get(selection[0])
|
||||
self.update_file_info(filename)
|
||||
|
||||
def update_file_info(self, filename):
|
||||
filepath = os.path.join(self.input_path_var.get(), filename)
|
||||
if os.path.exists(filepath):
|
||||
try:
|
||||
probe = ffmpeg.probe(filepath)
|
||||
video_info = next(s for s in probe['streams'] if s['codec_type'] == 'video')
|
||||
|
||||
info_text = f"File: {filename}\n"
|
||||
info_text += f"Duration: {float(probe['format']['duration']):.2f} seconds\n"
|
||||
info_text += f"Resolution: {video_info['width']}x{video_info['height']}\n"
|
||||
info_text += f"Codec: {video_info['codec_name']}\n"
|
||||
info_text += f"Bitrate: {int(probe['format']['bit_rate']) / 1000000:.2f} Mbps\n"
|
||||
|
||||
self.file_info_text.delete(1.0, tk.END)
|
||||
self.file_info_text.insert(1.0, info_text)
|
||||
|
||||
except Exception as e:
|
||||
self.file_info_text.delete(1.0, tk.END)
|
||||
self.file_info_text.insert(1.0, f"Error reading file: {str(e)}")
|
||||
|
||||
def add_to_queue(self):
|
||||
selection = self.file_listbox.curselection()
|
||||
if selection:
|
||||
filename = self.file_listbox.get(selection[0])
|
||||
self.render_queue.append({
|
||||
'filename': filename,
|
||||
'status': 'Queued',
|
||||
'progress': 0
|
||||
})
|
||||
self.update_queue_display()
|
||||
|
||||
def preview_encode(self):
|
||||
selection = self.file_listbox.curselection()
|
||||
if selection:
|
||||
filename = self.file_listbox.get(selection[0])
|
||||
self.encode_file(filename, preview=True)
|
||||
|
||||
def start_rendering(self):
|
||||
if self.render_queue and not self.is_rendering:
|
||||
self.is_rendering = True
|
||||
self.start_button.config(state='disabled')
|
||||
self.pause_button.config(state='normal')
|
||||
self.stop_button.config(state='normal')
|
||||
|
||||
# Start rendering thread
|
||||
render_thread = threading.Thread(target=self.render_queue_worker)
|
||||
render_thread.daemon = True
|
||||
render_thread.start()
|
||||
|
||||
def render_queue_worker(self):
|
||||
while self.render_queue and self.is_rendering:
|
||||
job = self.render_queue[0]
|
||||
self.encode_file(job['filename'])
|
||||
if self.render_queue:
|
||||
self.render_queue.pop(0)
|
||||
self.update_queue_display()
|
||||
|
||||
self.is_rendering = False
|
||||
self.start_button.config(state='normal')
|
||||
self.pause_button.config(state='disabled')
|
||||
self.stop_button.config(state='disabled')
|
||||
|
||||
def encode_file(self, filename, preview=False):
|
||||
input_path = os.path.join(self.input_path_var.get(), filename)
|
||||
output_filename = f"encoded_{filename}"
|
||||
output_path = os.path.join(self.config['output_path'], output_filename)
|
||||
|
||||
# Build FFmpeg command
|
||||
cmd = [
|
||||
'ffmpeg', '-i', input_path,
|
||||
'-c:v', self.codec_var.get(),
|
||||
'-preset', self.preset_var.get(),
|
||||
'-cq', str(self.cq_var.get())
|
||||
]
|
||||
|
||||
if preview:
|
||||
cmd.extend(['-t', str(self.config['preview_duration'])])
|
||||
|
||||
cmd.append(output_path)
|
||||
|
||||
try:
|
||||
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
universal_newlines=True, bufsize=1)
|
||||
|
||||
self.current_process = process
|
||||
|
||||
for line in process.stdout:
|
||||
self.log_text.insert(tk.END, line)
|
||||
self.log_text.see(tk.END)
|
||||
self.root.update_idletasks()
|
||||
|
||||
process.wait()
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Encoding failed: {str(e)}")
|
||||
|
||||
def pause_rendering(self):
|
||||
if self.current_process:
|
||||
self.current_process.terminate()
|
||||
|
||||
def stop_rendering(self):
|
||||
self.is_rendering = False
|
||||
if self.current_process:
|
||||
self.current_process.terminate()
|
||||
|
||||
def update_queue_display(self):
|
||||
for item in self.queue_tree.get_children():
|
||||
self.queue_tree.delete(item)
|
||||
|
||||
for i, job in enumerate(self.render_queue):
|
||||
self.queue_tree.insert('', 'end', text=job['filename'],
|
||||
values=(job['status'], f"{job['progress']}%"))
|
||||
|
||||
def browse_qc_source(self):
|
||||
filename = filedialog.askopenfilename(
|
||||
title="Select Source File",
|
||||
filetypes=[("Video files", "*.mp4 *.avi *.mkv *.mov *.wmv")]
|
||||
)
|
||||
if filename:
|
||||
self.qc_source_var.set(filename)
|
||||
|
||||
def browse_qc_output(self):
|
||||
filename = filedialog.askopenfilename(
|
||||
title="Select Output File",
|
||||
filetypes=[("Video files", "*.mp4 *.avi *.mkv *.mov *.wmv")]
|
||||
)
|
||||
if filename:
|
||||
self.qc_output_var.set(filename)
|
||||
|
||||
def run_qc_check(self):
|
||||
source = self.qc_source_var.get()
|
||||
output = self.qc_output_var.get()
|
||||
|
||||
if not source or not output:
|
||||
messagebox.showerror("Error", "Please select both source and output files")
|
||||
return
|
||||
|
||||
# Run QC check (placeholder for check_files.py functionality)
|
||||
self.qc_results_text.delete(1.0, tk.END)
|
||||
self.qc_results_text.insert(1.0, "QC check completed...\n")
|
||||
|
||||
def compare_frames(self):
|
||||
source = self.qc_source_var.get()
|
||||
output = self.qc_output_var.get()
|
||||
|
||||
if not source or not output:
|
||||
messagebox.showerror("Error", "Please select both source and output files")
|
||||
return
|
||||
|
||||
# Load frames for comparison
|
||||
self.load_comparison_frames()
|
||||
|
||||
def load_comparison_frames(self):
|
||||
# Placeholder for frame loading and display
|
||||
pass
|
||||
|
||||
def prev_frame(self):
|
||||
# Placeholder for frame navigation
|
||||
pass
|
||||
|
||||
def next_frame(self):
|
||||
# Placeholder for frame navigation
|
||||
pass
|
||||
|
||||
def export_qc_report(self):
|
||||
# Placeholder for QC report export
|
||||
pass
|
||||
|
||||
def save_config(self):
|
||||
self.config.update({
|
||||
'input_path': self.input_path_var.get(),
|
||||
'codec': self.codec_var.get(),
|
||||
'preset': self.preset_var.get(),
|
||||
'cq': self.cq_var.get(),
|
||||
'auto_bitrate': self.auto_bitrate_var.get()
|
||||
})
|
||||
|
||||
with open('vodkeeper_config.json', 'w') as f:
|
||||
json.dump(self.config, f, indent=2)
|
||||
|
||||
messagebox.showinfo("Success", "Configuration saved")
|
||||
|
||||
def load_config(self):
|
||||
try:
|
||||
with open('vodkeeper_config.json', 'r') as f:
|
||||
config = json.load(f)
|
||||
self.config.update(config)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def main():
|
||||
root = tk.Tk()
|
||||
app = VodKeeper(root)
|
||||
root.mainloop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user