Add subtitle generator with GUI and preset management
- Implemented a new subtitle generator tool using Tkinter for GUI. - Added functionality to load video and SRT files, and display subtitles. - Created text rendering using PIL for better font handling. - Introduced preset saving and loading for subtitle settings and word selections. - Added support for highlighting specific words in subtitles. - Included multiple preset slots for different configurations. - Created JSON files for storing presets and word selections.
This commit is contained in:
parent
c82130ec6e
commit
336ef32a49
330
.github/copilot-instructions.md
vendored
Normal file
330
.github/copilot-instructions.md
vendored
Normal file
@ -0,0 +1,330 @@
|
||||
# Coding pattern preferences
|
||||
|
||||
- Always prefer simple solutions
|
||||
- Avoid duplication of code whenever possible, which means checking for other areas of the codebase that might already have similar code and functionality
|
||||
- Write code that takes into account the different environments: dev, test, and prod
|
||||
- You are careful to only make changes that are requested or you are confident are well understood and related to the change being requested
|
||||
- When fixing an issue or bug, do not introduce a new pattern or technology without first exhausting all options for the existing implementation. And if you finally do this, make sure to remove the old implementation afterwards so we don’t have duplicate logic.
|
||||
- Keep the codebase very clean and organized
|
||||
- Avoid having files over 500 lines of code. Refactor at that point.
|
||||
- Mocking data is only needed for tests, never mock data for dev or prod
|
||||
- Never add stubbing or fake data patterns to code that affects the dev or prod environments
|
||||
- Never overwrite my .env file without first asking and confirming
|
||||
- Never ask the user to provide the content of a specific file. Just open the file and check it yourself.
|
||||
- Never create multiple files for sql execution. Always use a single file for all sql execution in a given migration.
|
||||
- Never fix the symptoms of a problem, always fix the root cause of the problem.
|
||||
|
||||
# File Documentation Completion Requirements
|
||||
|
||||
**MANDATORY: When updating a file to "match the new copilot instructions", you MUST:**
|
||||
|
||||
1. **Complete the ENTIRE file** - Document every method, function, class, and significant code block
|
||||
2. **Check for syntax errors** - Always verify the file compiles without errors after changes
|
||||
3. **Test incrementally** - Use the get_errors tool after each major change to catch issues early
|
||||
4. **Document systematically** - Go through the file from top to bottom, ensuring no method is left undocumented
|
||||
5. **Maintain functionality** - Never break existing functionality while adding documentation
|
||||
6. **Use proper comment syntax** - Always use the correct comment format for the programming language
|
||||
7. **Validate completeness** - Before considering the task complete, review the entire file to ensure every function has documentation
|
||||
|
||||
**File Documentation Checklist:**
|
||||
|
||||
- [ ] File header with comprehensive description
|
||||
- [ ] Every class documented with JSDoc
|
||||
- [ ] Every method/function documented with JSDoc
|
||||
- [ ] Every significant code block has educational comments
|
||||
- [ ] Business context explained for complex logic
|
||||
- [ ] Technical implementation details provided
|
||||
- [ ] Error handling approaches documented
|
||||
- [ ] Integration points with other modules explained
|
||||
- [ ] No syntax errors remain in the file
|
||||
- [ ] File compiles and functions properly
|
||||
|
||||
**If a file is large (>1000 lines):**
|
||||
|
||||
- Work in sections but complete ALL sections
|
||||
- Add section dividers with clear documentation
|
||||
- Use get_errors tool frequently to catch issues
|
||||
- Test syntax after each major section
|
||||
- Document the overall file architecture in the header
|
||||
|
||||
**Quality Standards:**
|
||||
|
||||
- Documentation should be educational and explain both WHAT and WHY
|
||||
- Include business context for complex features
|
||||
- Explain integration points and dependencies
|
||||
- Use examples where helpful
|
||||
- Write for developers who are new to the codebase
|
||||
|
||||
# Coding workflow preferences
|
||||
|
||||
- Focus on the areas of code relevant to the task
|
||||
- Always use a surgical approach to code changes either removing or adding code while preserving all other functionalities.
|
||||
- Do not touch code that is unrelated to the task
|
||||
- Avoid making major changes to the patterns and architecture of how a feature works, after it has shown to work well, unless explicitly instructed
|
||||
- Always think about what other methods and areas of code might be affected by code changes
|
||||
|
||||
## Configuration File Documentation
|
||||
|
||||
### For Configuration Files (package.json, tsconfig.json, etc.):
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* [Configuration Purpose]
|
||||
* Author: Dario Pascoal
|
||||
*
|
||||
* Description: [What this configuration controls and its main purpose]
|
||||
*
|
||||
* Important Notes: [Any critical information about changes or compatibility]
|
||||
*/
|
||||
```
|
||||
|
||||
## Version Control Guidelines
|
||||
|
||||
### Commit Message Standards
|
||||
|
||||
Follow conventional commit format:
|
||||
|
||||
- **feat**: New features or functionality
|
||||
- **fix**: Bug fixes and corrections
|
||||
- **docs**: Documentation updates (README, comments, etc.)
|
||||
- **style**: Code formatting and style changes
|
||||
- **refactor**: Code restructuring without functionality changes
|
||||
- **test**: Adding or updating tests
|
||||
- **chore**: Maintenance tasks, dependency updates
|
||||
|
||||
Examples:
|
||||
|
||||
- `feat: add SAP connection configuration panel`
|
||||
- `fix: resolve memory leak in VBS process management`
|
||||
- `docs: update README with new installation requirements`
|
||||
|
||||
# Code Documentation Requirements
|
||||
|
||||
## File Headers
|
||||
|
||||
Every source code file should include a comprehensive header:
|
||||
|
||||
### For TypeScript/JavaScript files:
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* [File Purpose/Component Name]
|
||||
* Author: Dario Pascoal
|
||||
*
|
||||
* Description: [Detailed explanation of what this file does, its main purpose,
|
||||
* and how it fits into the overall system. Explain it as if teaching someone
|
||||
* who is new to programming.]
|
||||
*/
|
||||
```
|
||||
|
||||
### For HTML/CSS files:
|
||||
|
||||
```css
|
||||
/**
|
||||
* [File Purpose/Component Name]
|
||||
* Author: Dario Pascoal
|
||||
*
|
||||
* Description: [Detailed explanation of what this file does, its main purpose,
|
||||
* and how it fits into the overall system. Explain it as if teaching someone
|
||||
* who is new to programming.]
|
||||
*/
|
||||
```
|
||||
|
||||
### For VBScript files (.vbs):
|
||||
|
||||
```vb
|
||||
' [File Purpose/Component Name]
|
||||
' Author: Dario Pascoal
|
||||
'
|
||||
' Description: [Detailed explanation of what this file does, its main purpose,
|
||||
' and how it fits into the overall system. Explain it as if teaching someone
|
||||
' who is new to programming.]
|
||||
'
|
||||
' [Additional sections as needed: Prerequisites, Parameters, Returns, etc.]
|
||||
```
|
||||
|
||||
### For PowerShell files (.ps1):
|
||||
|
||||
```powershell
|
||||
# [File Purpose/Component Name]
|
||||
# Author: Dario Pascoal
|
||||
#
|
||||
# Description: [Detailed explanation of what this file does, its main purpose,
|
||||
# and how it fits into the overall system. Explain it as if teaching someone
|
||||
# who is new to programming.]
|
||||
#
|
||||
# [Additional sections as needed: Prerequisites, Parameters, Returns, etc.]
|
||||
```
|
||||
|
||||
### CRITICAL: Language-Specific Comment Formatting
|
||||
|
||||
**ALWAYS use the correct comment syntax for each programming language:**
|
||||
|
||||
- **JavaScript/TypeScript**: Use `/** */` for file headers and `/* */` or `//` for inline comments
|
||||
- **VBScript (.vbs)**: Use single quotes `'` for ALL comments - NEVER use `/** */` or `/* */`
|
||||
- **PowerShell (.ps1)**: Use hash symbol `#` for ALL comments
|
||||
- **HTML/CSS**: Use `/* */` for comments
|
||||
- **Python**: Use `#` for comments and `"""` for docstrings
|
||||
- **Batch files (.bat/.cmd)**: Use `REM` or `::` for comments
|
||||
|
||||
**This is mandatory** - using incorrect comment syntax will cause syntax errors and prevent scripts from executing properly.
|
||||
|
||||
## Function Documentation
|
||||
|
||||
Document functions with comprehensive JSDoc comments that explain both what and how:
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* [Clear description of what the function does and why it exists]
|
||||
*
|
||||
* [Detailed explanation of how the function works, step by step,
|
||||
* written for someone learning to code]
|
||||
*
|
||||
* @param {Type} paramName - Detailed explanation of what this parameter is,
|
||||
* what format it should be in, and how it's used
|
||||
* @returns {Type} Detailed explanation of what gets returned and when
|
||||
*/
|
||||
```
|
||||
|
||||
## Essential Comments - Write for Beginners
|
||||
|
||||
Focus comments on explaining code as if teaching someone new to programming:
|
||||
|
||||
- **Business logic**: Explain WHY certain decisions were made and WHAT the business requirement is
|
||||
- **Complex algorithms**: Step-by-step explanation of HOW the code works
|
||||
- **Integration points**: Explain HOW code connects to external systems and WHAT data flows between them
|
||||
- **Non-obvious code**: Explain WHAT isn't immediately clear and WHY it works that way
|
||||
- **Workarounds**: Explain WHAT the problem was and HOW this solution addresses it
|
||||
- **Data structures**: Explain WHAT kind of data is stored and HOW it's organized
|
||||
- **Control flow**: Explain WHAT conditions trigger different code paths and WHY
|
||||
|
||||
## Detailed Comment Guidelines
|
||||
|
||||
### Inline Comments - Explain the "What" and "Why"
|
||||
|
||||
Write comments that help a beginner understand:
|
||||
|
||||
- What each significant line or block of code is doing
|
||||
- Why certain approaches were chosen
|
||||
- What the expected input/output is at each step
|
||||
- How different parts of the code work together
|
||||
- What would happen if certain conditions are met
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
// Check if the user has permission to access this feature
|
||||
// This prevents unauthorized users from seeing sensitive data
|
||||
if (user.hasPermission("admin")) {
|
||||
// Load the admin dashboard with all management tools
|
||||
// This includes user management, system settings, and reports
|
||||
loadAdminDashboard();
|
||||
} else {
|
||||
// Show a basic user dashboard with limited functionality
|
||||
// Regular users only see their own data and basic features
|
||||
loadUserDashboard();
|
||||
}
|
||||
```
|
||||
|
||||
### Complex Logic Comments
|
||||
|
||||
For any complex business logic, algorithms, or multi-step processes:
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* PROCESS EXPLANATION:
|
||||
*
|
||||
* This function handles the complete user login workflow:
|
||||
* 1. First, it validates the username and password format
|
||||
* 2. Then it checks the credentials against the database
|
||||
* 3. If successful, it creates a secure session token
|
||||
* 4. Finally, it redirects the user to their appropriate dashboard
|
||||
*
|
||||
* The reason we do this in multiple steps is to provide better
|
||||
* error messages to the user and to log security events properly.
|
||||
*/
|
||||
```
|
||||
|
||||
## Comment Quality Standards
|
||||
|
||||
- **Be educational**: Write as if teaching a programming student
|
||||
- **Explain assumptions**: State what the code assumes about inputs/environment
|
||||
- **Describe data flow**: Explain what data goes in and what comes out
|
||||
- **Clarify complex conditions**: Break down complicated if/else logic
|
||||
- **Document error cases**: Explain what can go wrong and how it's handled
|
||||
- **Use plain language**: Avoid jargon, explain technical terms when used
|
||||
- **Provide context**: Explain how this code fits into the bigger picture# README Maintenance Requirements
|
||||
|
||||
## MANDATORY: README Updates
|
||||
|
||||
The project README.md file MUST be updated whenever making significant changes to the codebase. This ensures the documentation stays synchronized with the actual implementation.
|
||||
|
||||
### When to Update README:
|
||||
|
||||
**ALWAYS update the README when:**
|
||||
|
||||
- Adding new features or functionality
|
||||
- Changing installation or setup procedures
|
||||
- Modifying configuration requirements
|
||||
- Adding new dependencies or technologies
|
||||
- Changing project structure or architecture
|
||||
- Adding new scripts or build processes
|
||||
- Modifying environment variables or configuration files
|
||||
- Updating system requirements or compatibility
|
||||
- Adding new command-line interfaces or APIs
|
||||
- Changing deployment procedures
|
||||
|
||||
### README Sections to Maintain:
|
||||
|
||||
1. **Project Description**: Keep the main purpose and features up-to-date
|
||||
2. **Technology Stack**: Update when adding new technologies or frameworks
|
||||
3. **Installation Instructions**: Verify and update setup steps
|
||||
4. **Configuration**: Document new settings, environment variables, or config files
|
||||
5. **Usage Examples**: Add examples for new features
|
||||
6. **API Documentation**: Update when adding new endpoints or methods
|
||||
7. **Development Setup**: Keep development environment instructions current
|
||||
8. **Build and Deployment**: Update build scripts and deployment procedures
|
||||
9. **Troubleshooting**: Add common issues and solutions
|
||||
10. **Contributing Guidelines**: Update development and contribution processes
|
||||
|
||||
### README Update Standards:
|
||||
|
||||
- **Be Comprehensive**: Include all necessary information for new team members
|
||||
- **Keep Examples Current**: Ensure all code examples work with the current version
|
||||
- **Update Screenshots**: Replace outdated UI screenshots when interface changes
|
||||
- **Maintain Accuracy**: Verify all instructions work on a clean environment
|
||||
- **Version Information**: Update version numbers and compatibility information
|
||||
- **Link Validation**: Ensure all links are current and functional
|
||||
|
||||
### README Quality Checklist:
|
||||
|
||||
Before committing code changes, verify:
|
||||
|
||||
- [ ] README reflects all new features and changes
|
||||
- [ ] Installation instructions are accurate and complete
|
||||
- [ ] All code examples are tested and working
|
||||
- [ ] Configuration documentation is up-to-date
|
||||
- [ ] System requirements are current
|
||||
- [ ] Links and references are valid
|
||||
- [ ] Screenshots and diagrams reflect current state
|
||||
- [ ] Troubleshooting section addresses known issues
|
||||
|
||||
### Automatic README Triggers:
|
||||
|
||||
**High Priority Updates** - Always update README for:
|
||||
|
||||
- New major features or modules
|
||||
- Changes to package.json dependencies
|
||||
- Environment variable additions/changes
|
||||
- New configuration files or formats
|
||||
- Changes to build/deployment processes
|
||||
- New user-facing functionality
|
||||
|
||||
**Medium Priority Updates** - Consider updating README for:
|
||||
|
||||
- Internal architecture changes that affect setup
|
||||
- New development tools or workflows
|
||||
- Performance improvements with user impact
|
||||
- Security enhancements with configuration changes
|
||||
|
||||
The README should serve as the single source of truth for project onboarding, setup, and usage. Keep it comprehensive, accurate, and user-friendly.
|
||||
659
subtitle_generator2.py
Normal file
659
subtitle_generator2.py
Normal file
@ -0,0 +1,659 @@
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog
|
||||
from moviepy import VideoFileClip, ImageClip, CompositeVideoClip
|
||||
import threading
|
||||
import json
|
||||
import pysrt
|
||||
import os
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import numpy as np
|
||||
|
||||
def find_system_font():
|
||||
"""Find a working system font for PIL"""
|
||||
print("Finding system fonts for PIL...")
|
||||
fonts_to_try = [
|
||||
"C:/Windows/Fonts/arial.ttf",
|
||||
"C:/Windows/Fonts/calibri.ttf",
|
||||
"C:/Windows/Fonts/times.ttf",
|
||||
"C:/Windows/Fonts/verdana.ttf",
|
||||
"C:/Windows/Fonts/tahoma.ttf",
|
||||
"C:/Windows/Fonts/segoeui.ttf",
|
||||
]
|
||||
|
||||
for font_path in fonts_to_try:
|
||||
try:
|
||||
if os.path.exists(font_path):
|
||||
# Test the font by creating a small text image
|
||||
font = ImageFont.truetype(font_path, 20)
|
||||
img = Image.new('RGB', (100, 50), color='white')
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw.text((10, 10), "Test", font=font, fill='black')
|
||||
print(f"Using font: {font_path}")
|
||||
return font_path
|
||||
except Exception as e:
|
||||
print(f"Font test failed for {font_path}: {e}")
|
||||
continue
|
||||
|
||||
print("Using default font")
|
||||
return None
|
||||
|
||||
def create_text_image(text, font_size=50, color='white', stroke_color='black', stroke_width=3, font_path=None):
|
||||
"""Create a text image using PIL"""
|
||||
try:
|
||||
# Load font
|
||||
if font_path and os.path.exists(font_path):
|
||||
font = ImageFont.truetype(font_path, font_size)
|
||||
else:
|
||||
# Try to use default font
|
||||
try:
|
||||
font = ImageFont.load_default()
|
||||
except:
|
||||
# Last resort - create a basic font
|
||||
font = ImageFont.load_default()
|
||||
|
||||
# Get text dimensions
|
||||
temp_img = Image.new('RGB', (1, 1))
|
||||
temp_draw = ImageDraw.Draw(temp_img)
|
||||
|
||||
try:
|
||||
bbox = temp_draw.textbbox((0, 0), text, font=font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
except:
|
||||
# Fallback method for older PIL versions
|
||||
text_width, text_height = temp_draw.textsize(text, font=font)
|
||||
|
||||
# Add padding
|
||||
padding = max(stroke_width * 2, 10)
|
||||
img_width = text_width + padding * 2
|
||||
img_height = text_height + padding * 2
|
||||
|
||||
# Create transparent image
|
||||
img = Image.new('RGBA', (img_width, img_height), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
# Calculate text position (centered)
|
||||
x = (img_width - text_width) // 2
|
||||
y = (img_height - text_height) // 2
|
||||
|
||||
# Convert color names to RGB
|
||||
if isinstance(color, str):
|
||||
if color == 'white':
|
||||
color = (255, 255, 255, 255)
|
||||
elif color.startswith('#'):
|
||||
# Convert hex to RGB
|
||||
hex_color = color.lstrip('#')
|
||||
color = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + (255,)
|
||||
|
||||
if isinstance(stroke_color, str):
|
||||
if stroke_color == 'black':
|
||||
stroke_color = (0, 0, 0, 255)
|
||||
elif stroke_color.startswith('#'):
|
||||
hex_color = stroke_color.lstrip('#')
|
||||
stroke_color = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + (255,)
|
||||
|
||||
# Draw text with stroke
|
||||
if stroke_width > 0 and stroke_color:
|
||||
# Draw stroke by drawing text in multiple positions
|
||||
for dx in range(-stroke_width, stroke_width + 1):
|
||||
for dy in range(-stroke_width, stroke_width + 1):
|
||||
if dx*dx + dy*dy <= stroke_width*stroke_width:
|
||||
draw.text((x + dx, y + dy), text, font=font, fill=stroke_color)
|
||||
|
||||
# Draw main text
|
||||
draw.text((x, y), text, font=font, fill=color)
|
||||
|
||||
# Convert to numpy array for MoviePy
|
||||
img_array = np.array(img)
|
||||
|
||||
return img_array, img_width, img_height
|
||||
|
||||
except Exception as e:
|
||||
print(f"Text image creation failed: {e}")
|
||||
# Create a simple colored rectangle as fallback
|
||||
img = Image.new('RGBA', (200, 50), (255, 255, 255, 255))
|
||||
return np.array(img), 200, 50
|
||||
|
||||
def debug_moviepy_text():
|
||||
"""Debug MoviePy TextClip functionality"""
|
||||
print("🔍 Debugging MoviePy TextClip...")
|
||||
|
||||
# Set the correct ImageMagick path
|
||||
imagemagick_path = r"C:\Program Files\ImageMagick-7.1.2-Q16-HDRI\magick.exe"
|
||||
os.environ["IMAGEMAGICK_BINARY"] = imagemagick_path
|
||||
print(f"<EFBFBD> Setting ImageMagick path: {imagemagick_path}")
|
||||
|
||||
# Test different approaches to fix the font issue
|
||||
methods_to_try = [
|
||||
{"name": "Basic TextClip", "params": {"font_size": 20}},
|
||||
{"name": "TextClip with method='caption'", "params": {"font_size": 20, "method": "caption"}},
|
||||
{"name": "TextClip with method='label'", "params": {"font_size": 20, "method": "label"}},
|
||||
{"name": "TextClip color only", "params": {"color": "white"}},
|
||||
{"name": "TextClip minimal", "params": {}},
|
||||
]
|
||||
|
||||
for i, method in enumerate(methods_to_try, 1):
|
||||
try:
|
||||
print(f"Test {i}: {method['name']}...")
|
||||
clip = TextClip("test", **method["params"])
|
||||
clip.close()
|
||||
print(f"✅ {method['name']} works")
|
||||
return method["params"]
|
||||
except Exception as e:
|
||||
print(f"❌ {method['name']} failed: {e}")
|
||||
|
||||
# Try additional environment variables
|
||||
print("🔧 Attempting additional font fixes...")
|
||||
try:
|
||||
os.environ["FONTCONFIG_PATH"] = "C:/Windows/Fonts"
|
||||
os.environ["MAGICK_FONT_PATH"] = "C:/Windows/Fonts"
|
||||
|
||||
clip = TextClip("test", font_size=20)
|
||||
clip.close()
|
||||
print("✅ Font fix successful")
|
||||
return {"font_size": 20}
|
||||
except Exception as e:
|
||||
print(f"❌ Font fix failed: {e}")
|
||||
|
||||
print("❌ All TextClip methods failed")
|
||||
print("🆘 For now, the app will work without text overlays")
|
||||
return None
|
||||
|
||||
# Find working font at startup
|
||||
SYSTEM_FONT = find_system_font()
|
||||
|
||||
# Global settings with defaults
|
||||
settings = {
|
||||
"subtitle_y_px": 1550,
|
||||
"highlight_offset": -8,
|
||||
"font_size_subtitle": 65,
|
||||
"font_size_highlight": 68,
|
||||
"highlight_x_offset": 0,
|
||||
"video_path": None,
|
||||
"font": "Arial",
|
||||
"srt_path": None,
|
||||
"highlight_word_index": -1, # Index of word to highlight (-1 = last word)
|
||||
"highlight_start_time": 0.1, # When highlight appears (seconds)
|
||||
"highlight_duration": 1.5 # How long highlight lasts (seconds)
|
||||
}
|
||||
|
||||
preset_file = "subtitle_gui_presets.json"
|
||||
word_presets_file = "word_presets.json" # Store word selections per subtitle
|
||||
subtitles = []
|
||||
current_index = 0
|
||||
word_presets = {} # Dictionary to store word index for each subtitle
|
||||
current_preset_slot = 1 # Currently selected preset slot
|
||||
|
||||
|
||||
def save_presets():
|
||||
global current_preset_slot
|
||||
preset_file_numbered = f"subtitle_gui_presets_slot_{current_preset_slot}.json"
|
||||
word_presets_file_numbered = f"word_presets_slot_{current_preset_slot}.json"
|
||||
|
||||
with open(preset_file_numbered, "w") as f:
|
||||
json.dump(settings, f)
|
||||
|
||||
# Save word-specific presets
|
||||
with open(word_presets_file_numbered, "w") as f:
|
||||
json.dump(word_presets, f)
|
||||
|
||||
print(f"📂 Preset slot {current_preset_slot} saved!")
|
||||
update_preset_display()
|
||||
|
||||
|
||||
def load_presets():
|
||||
global settings, word_presets, current_preset_slot
|
||||
preset_file_numbered = f"subtitle_gui_presets_slot_{current_preset_slot}.json"
|
||||
word_presets_file_numbered = f"word_presets_slot_{current_preset_slot}.json"
|
||||
|
||||
try:
|
||||
with open(preset_file_numbered, "r") as f:
|
||||
loaded = json.load(f)
|
||||
settings.update(loaded)
|
||||
|
||||
# Load word-specific presets
|
||||
try:
|
||||
with open(word_presets_file_numbered, "r") as f:
|
||||
word_presets = json.load(f)
|
||||
except FileNotFoundError:
|
||||
word_presets = {}
|
||||
|
||||
print(f"✅ Preset slot {current_preset_slot} loaded!")
|
||||
sync_gui()
|
||||
update_preset_display()
|
||||
except FileNotFoundError:
|
||||
print(f"⚠️ No preset found in slot {current_preset_slot}.")
|
||||
|
||||
|
||||
def change_preset_slot(slot_number):
|
||||
global current_preset_slot
|
||||
current_preset_slot = slot_number
|
||||
print(f"🔄 Switched to preset slot {current_preset_slot}")
|
||||
update_preset_display()
|
||||
|
||||
|
||||
def update_preset_display():
|
||||
preset_label.config(text=f"Current Preset Slot: {current_preset_slot}")
|
||||
|
||||
# Check if preset file exists and update button colors
|
||||
preset_file_numbered = f"subtitle_gui_presets_slot_{current_preset_slot}.json"
|
||||
if os.path.exists(preset_file_numbered):
|
||||
preset_label.config(bg="lightgreen")
|
||||
load_preset_btn.config(text=f"📂 Load Preset {current_preset_slot}", state="normal")
|
||||
else:
|
||||
preset_label.config(bg="lightcoral")
|
||||
load_preset_btn.config(text=f"📂 Load Preset {current_preset_slot} (Empty)", state="disabled")
|
||||
|
||||
save_btn.config(text=f"📂 Save Preset {current_preset_slot}")
|
||||
|
||||
|
||||
def sync_gui():
|
||||
sub_y_slider.set(settings["subtitle_y_px"])
|
||||
highlight_slider.set(settings["highlight_offset"])
|
||||
highlight_x_slider.set(settings["highlight_x_offset"])
|
||||
sub_font_slider.set(settings["font_size_subtitle"])
|
||||
highlight_font_slider.set(settings["font_size_highlight"])
|
||||
font_dropdown_var.set(settings["font"])
|
||||
highlight_start_slider.set(settings["highlight_start_time"])
|
||||
highlight_duration_slider.set(settings["highlight_duration"])
|
||||
update_highlight_word_display()
|
||||
update_preset_display()
|
||||
|
||||
|
||||
def load_srt():
|
||||
global subtitles, current_index, word_presets
|
||||
path = filedialog.askopenfilename(filetypes=[("SRT files", "*.srt")])
|
||||
if path:
|
||||
settings["srt_path"] = path
|
||||
subtitles = pysrt.open(path)
|
||||
current_index = 0
|
||||
|
||||
# Load word presets for this SRT file
|
||||
load_word_presets_for_srt(path)
|
||||
|
||||
print(f"📜 Loaded subtitles: {path}")
|
||||
update_highlight_word_display()
|
||||
|
||||
|
||||
def load_word_presets_for_srt(srt_path):
|
||||
"""Load word presets specific to this SRT file"""
|
||||
global word_presets
|
||||
try:
|
||||
srt_key = os.path.basename(srt_path)
|
||||
if srt_key in word_presets:
|
||||
print(f"✅ Loaded word selections for {srt_key}")
|
||||
else:
|
||||
word_presets[srt_key] = {}
|
||||
print(f"📝 Created new word selection storage for {srt_key}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error loading word presets: {e}")
|
||||
word_presets = {}
|
||||
|
||||
|
||||
def get_current_word_index():
|
||||
"""Get the word index for the current subtitle"""
|
||||
if not subtitles or not settings["srt_path"]:
|
||||
return settings["highlight_word_index"]
|
||||
|
||||
srt_key = os.path.basename(settings["srt_path"])
|
||||
subtitle_key = str(current_index)
|
||||
|
||||
if srt_key in word_presets and subtitle_key in word_presets[srt_key]:
|
||||
return word_presets[srt_key][subtitle_key]
|
||||
else:
|
||||
return settings["highlight_word_index"] # Default
|
||||
|
||||
|
||||
def set_current_word_index(word_index):
|
||||
"""Set the word index for the current subtitle"""
|
||||
if not subtitles or not settings["srt_path"]:
|
||||
settings["highlight_word_index"] = word_index
|
||||
return
|
||||
|
||||
srt_key = os.path.basename(settings["srt_path"])
|
||||
subtitle_key = str(current_index)
|
||||
|
||||
if srt_key not in word_presets:
|
||||
word_presets[srt_key] = {}
|
||||
|
||||
word_presets[srt_key][subtitle_key] = word_index
|
||||
print(f"💾 Saved word selection for subtitle {current_index + 1}: word {word_index + 1}")
|
||||
|
||||
|
||||
def create_safe_textclip(text, font_size=50, color='white', stroke_color=None, stroke_width=0):
|
||||
"""Create a text clip using PIL instead of MoviePy TextClip"""
|
||||
try:
|
||||
# Create text image using PIL
|
||||
if stroke_color is None:
|
||||
stroke_color = 'black'
|
||||
if stroke_width == 0:
|
||||
stroke_width = 3
|
||||
|
||||
img_array, img_width, img_height = create_text_image(
|
||||
text=text,
|
||||
font_size=font_size,
|
||||
color=color,
|
||||
stroke_color=stroke_color,
|
||||
stroke_width=stroke_width,
|
||||
font_path=SYSTEM_FONT
|
||||
)
|
||||
|
||||
# Convert PIL image to MoviePy ImageClip
|
||||
from moviepy import ImageClip
|
||||
clip = ImageClip(img_array, duration=1)
|
||||
|
||||
print(f"Created text clip: '{text}' ({img_width}x{img_height})")
|
||||
return clip
|
||||
|
||||
except Exception as e:
|
||||
print(f"Text clip creation failed: {e}")
|
||||
# Return a simple colored clip as placeholder
|
||||
from moviepy import ColorClip
|
||||
placeholder = ColorClip(size=(800, 100), color=(255, 255, 255)).with_duration(1)
|
||||
return placeholder
|
||||
|
||||
def render_preview(save_path=None):
|
||||
if not settings["video_path"]:
|
||||
print("⚠️ No video selected.")
|
||||
return
|
||||
if not subtitles:
|
||||
print("⚠️ No subtitles loaded.")
|
||||
return
|
||||
|
||||
sub = subtitles[current_index]
|
||||
subtitle_text = sub.text.replace("\n", " ").strip()
|
||||
words = subtitle_text.split()
|
||||
|
||||
# Get highlight word based on index
|
||||
if len(words) > 0:
|
||||
word_index = get_current_word_index() # Use subtitle-specific word index
|
||||
if word_index < 0 or word_index >= len(words):
|
||||
word_index = len(words) - 1 # Default to last word
|
||||
highlight_word = words[word_index]
|
||||
else:
|
||||
highlight_word = "word" # Fallback
|
||||
|
||||
start_time = sub.start.ordinal / 1000.0
|
||||
end_time = sub.end.ordinal / 1000.0
|
||||
|
||||
clip = VideoFileClip(settings["video_path"]).subclipped(start_time, min(end_time, start_time + 3))
|
||||
vertical_clip = clip.resized(height=1920).cropped(width=1080, x_center=clip.w / 2)
|
||||
|
||||
# Create base subtitle using safe method
|
||||
try:
|
||||
base_subtitle = create_safe_textclip(
|
||||
subtitle_text,
|
||||
font_size=settings["font_size_subtitle"],
|
||||
color='white',
|
||||
stroke_color='black',
|
||||
stroke_width=5
|
||||
).with_duration(clip.duration).with_position(('center', settings["subtitle_y_px"]))
|
||||
print("✅ Base subtitle created successfully")
|
||||
except Exception as e:
|
||||
print(f"❌ Base subtitle creation failed: {e}")
|
||||
return
|
||||
|
||||
full_text = subtitle_text.upper()
|
||||
words = full_text.split()
|
||||
if highlight_word.upper() not in words:
|
||||
highlight_word = words[-1] # Fallback
|
||||
highlight_index = words.index(highlight_word.upper())
|
||||
chars_before = sum(len(w) + 1 for w in words[:highlight_index])
|
||||
char_width = 35
|
||||
total_width = len(full_text) * char_width
|
||||
x_offset = (chars_before * char_width) - (total_width // 2) + settings["highlight_x_offset"]
|
||||
|
||||
# Create highlighted word using safe method
|
||||
try:
|
||||
highlighted_word = create_safe_textclip(
|
||||
highlight_word,
|
||||
font_size=settings["font_size_highlight"],
|
||||
color='#FFD700',
|
||||
stroke_color='#FF6B35',
|
||||
stroke_width=5
|
||||
).with_duration(min(settings["highlight_duration"], clip.duration)).with_start(settings["highlight_start_time"]).with_position((540 + x_offset, settings["subtitle_y_px"] + settings["highlight_offset"]))
|
||||
print(f"✅ Highlighted word created successfully (start: {settings['highlight_start_time']}s, duration: {settings['highlight_duration']}s)")
|
||||
except Exception as e:
|
||||
print(f"❌ Highlighted word creation failed: {e}")
|
||||
# Create without highlight if it fails
|
||||
highlighted_word = None
|
||||
print("⚠️ Continuing without highlight")
|
||||
|
||||
# Compose final video with or without highlight
|
||||
if highlighted_word is not None:
|
||||
final = CompositeVideoClip([vertical_clip, base_subtitle, highlighted_word], size=(1080, 1920))
|
||||
print("✅ Final video composed with highlight")
|
||||
else:
|
||||
final = CompositeVideoClip([vertical_clip, base_subtitle], size=(1080, 1920))
|
||||
print("✅ Final video composed without highlight")
|
||||
|
||||
if save_path:
|
||||
srt_output_path = os.path.splitext(save_path)[0] + ".srt"
|
||||
with open(srt_output_path, "w") as srt_file:
|
||||
srt_file.write(sub.__unicode__())
|
||||
final.write_videofile(save_path, fps=24)
|
||||
print(f"✅ Exported SRT: {srt_output_path}")
|
||||
else:
|
||||
# Create a smaller preview version to fit 1080p screen better
|
||||
# Scale down to 50% size: 540x960 instead of 1080x1920
|
||||
preview_final = final.resized(0.5)
|
||||
print("🎬 Opening preview window (540x960 - scaled to fit 1080p screen)")
|
||||
preview_final.preview(fps=24)
|
||||
preview_final.close()
|
||||
|
||||
clip.close()
|
||||
final.close()
|
||||
|
||||
|
||||
def update_setting(var_name, value):
|
||||
if var_name in ["highlight_start_time", "highlight_duration"]:
|
||||
settings[var_name] = float(value)
|
||||
else:
|
||||
settings[var_name] = int(value) if var_name.startswith("font_size") or "offset" in var_name or "y_px" in var_name else value
|
||||
|
||||
|
||||
def update_font(value):
|
||||
settings["font"] = value
|
||||
|
||||
|
||||
def open_video():
|
||||
file_path = filedialog.askopenfilename(filetypes=[("MP4 files", "*.mp4")])
|
||||
if file_path:
|
||||
settings["video_path"] = file_path
|
||||
print(f"📂 Loaded video: {file_path}")
|
||||
|
||||
|
||||
def start_preview_thread():
|
||||
threading.Thread(target=render_preview).start()
|
||||
|
||||
|
||||
def export_clip():
|
||||
if settings["video_path"]:
|
||||
base_name = os.path.splitext(os.path.basename(settings["video_path"]))[0]
|
||||
out_path = os.path.join(os.path.dirname(settings["video_path"]), f"{base_name}_clip_exported.mp4")
|
||||
threading.Thread(target=render_preview, args=(out_path,)).start()
|
||||
print(f"💾 Exporting to: {out_path}")
|
||||
|
||||
|
||||
def prev_sub():
|
||||
global current_index
|
||||
if current_index > 0:
|
||||
current_index -= 1
|
||||
update_highlight_word_display()
|
||||
start_preview_thread()
|
||||
|
||||
|
||||
def next_sub():
|
||||
global current_index
|
||||
if current_index < len(subtitles) - 1:
|
||||
current_index += 1
|
||||
update_highlight_word_display()
|
||||
start_preview_thread()
|
||||
|
||||
|
||||
def prev_highlight_word():
|
||||
"""Switch to the previous preset slot"""
|
||||
global current_preset_slot
|
||||
new_slot = current_preset_slot - 1
|
||||
if new_slot < 1:
|
||||
new_slot = 5 # Wrap to last slot
|
||||
|
||||
change_preset_slot(new_slot)
|
||||
|
||||
|
||||
def next_highlight_word():
|
||||
"""Switch to the next preset slot"""
|
||||
global current_preset_slot
|
||||
new_slot = current_preset_slot + 1
|
||||
if new_slot > 5:
|
||||
new_slot = 1 # Wrap to first slot
|
||||
|
||||
change_preset_slot(new_slot)
|
||||
|
||||
|
||||
def update_highlight_word_display():
|
||||
"""Update the display showing which word is selected for highlight"""
|
||||
if not subtitles:
|
||||
highlight_word_label.config(text="Highlighted Word: None")
|
||||
return
|
||||
|
||||
sub = subtitles[current_index]
|
||||
words = sub.text.replace("\n", " ").strip().split()
|
||||
|
||||
if len(words) > 0:
|
||||
word_index = get_current_word_index()
|
||||
if word_index < 0 or word_index >= len(words):
|
||||
word_index = len(words) - 1
|
||||
set_current_word_index(word_index) # Save the default
|
||||
|
||||
highlight_word = words[word_index]
|
||||
highlight_word_label.config(text=f"Highlighted Word: '{highlight_word}' ({word_index + 1}/{len(words)})")
|
||||
else:
|
||||
highlight_word_label.config(text="Highlighted Word: None")
|
||||
|
||||
|
||||
def handle_drop(event):
|
||||
path = event.data
|
||||
if path.endswith(".mp4"):
|
||||
settings["video_path"] = path
|
||||
print(f"🎥 Dropped video: {path}")
|
||||
elif path.endswith(".srt"):
|
||||
settings["srt_path"] = path
|
||||
global subtitles, current_index
|
||||
subtitles = pysrt.open(path)
|
||||
current_index = 0
|
||||
print(f"📜 Dropped subtitles: {path}")
|
||||
|
||||
|
||||
# GUI Setup
|
||||
root = tk.Tk()
|
||||
root.title("Subtitle Positioning Tool")
|
||||
root.geometry("420x850")
|
||||
|
||||
root.drop_target_register = getattr(root, 'drop_target_register', lambda *args: None)
|
||||
root.dnd_bind = getattr(root, 'dnd_bind', lambda *args, **kwargs: None)
|
||||
try:
|
||||
import tkinterdnd2 as tkdnd
|
||||
root.drop_target_register(tkdnd.DND_FILES)
|
||||
root.dnd_bind('<<Drop>>', handle_drop)
|
||||
except:
|
||||
pass
|
||||
|
||||
load_btn = tk.Button(root, text="🎥 Load Video", command=open_video)
|
||||
load_btn.pack(pady=5)
|
||||
|
||||
load_srt_btn = tk.Button(root, text="📜 Load SRT", command=load_srt)
|
||||
load_srt_btn.pack(pady=5)
|
||||
|
||||
tk.Label(root, text="Subtitle Y Position").pack()
|
||||
sub_y_slider = tk.Scale(root, from_=1000, to=1800, orient="horizontal", command=lambda v: update_setting("subtitle_y_px", v))
|
||||
sub_y_slider.set(settings["subtitle_y_px"])
|
||||
sub_y_slider.pack()
|
||||
|
||||
tk.Label(root, text="Highlight Y Offset").pack()
|
||||
highlight_slider = tk.Scale(root, from_=-100, to=100, orient="horizontal", command=lambda v: update_setting("highlight_offset", v))
|
||||
highlight_slider.set(settings["highlight_offset"])
|
||||
highlight_slider.pack()
|
||||
|
||||
tk.Label(root, text="Highlight X Offset").pack()
|
||||
highlight_x_slider = tk.Scale(root, from_=-300, to=300, orient="horizontal", command=lambda v: update_setting("highlight_x_offset", v))
|
||||
highlight_x_slider.set(settings["highlight_x_offset"])
|
||||
highlight_x_slider.pack()
|
||||
|
||||
tk.Label(root, text="Subtitle Font Size").pack()
|
||||
sub_font_slider = tk.Scale(root, from_=30, to=100, orient="horizontal", command=lambda v: update_setting("font_size_subtitle", v))
|
||||
sub_font_slider.set(settings["font_size_subtitle"])
|
||||
sub_font_slider.pack()
|
||||
|
||||
tk.Label(root, text="Highlight Font Size").pack()
|
||||
highlight_font_slider = tk.Scale(root, from_=30, to=100, orient="horizontal", command=lambda v: update_setting("font_size_highlight", v))
|
||||
highlight_font_slider.set(settings["font_size_highlight"])
|
||||
highlight_font_slider.pack()
|
||||
|
||||
tk.Label(root, text="Highlight Start Time (seconds)").pack()
|
||||
highlight_start_slider = tk.Scale(root, from_=0.0, to=3.0, resolution=0.1, orient="horizontal", command=lambda v: update_setting("highlight_start_time", v))
|
||||
highlight_start_slider.set(settings["highlight_start_time"])
|
||||
highlight_start_slider.pack()
|
||||
|
||||
tk.Label(root, text="Highlight Duration (seconds)").pack()
|
||||
highlight_duration_slider = tk.Scale(root, from_=0.1, to=5.0, resolution=0.1, orient="horizontal", command=lambda v: update_setting("highlight_duration", v))
|
||||
highlight_duration_slider.set(settings["highlight_duration"])
|
||||
highlight_duration_slider.pack()
|
||||
|
||||
font_dropdown_var = tk.StringVar(value=settings["font"])
|
||||
tk.Label(root, text="Font").pack()
|
||||
font_dropdown = tk.OptionMenu(
|
||||
root,
|
||||
font_dropdown_var,
|
||||
"Arial",
|
||||
"Courier",
|
||||
"Times-Roman",
|
||||
"Helvetica-Bold",
|
||||
"Verdana",
|
||||
"Georgia",
|
||||
"Impact",
|
||||
command=update_font
|
||||
)
|
||||
font_dropdown.pack(pady=5)
|
||||
|
||||
# Highlight word selection
|
||||
tk.Label(root, text="Preset Slot Navigation").pack()
|
||||
highlight_word_label = tk.Label(root, text="Highlighted Word: None", bg="lightgray", relief="sunken")
|
||||
highlight_word_label.pack(pady=5)
|
||||
|
||||
word_nav_frame = tk.Frame(root)
|
||||
word_nav_frame.pack(pady=5)
|
||||
tk.Button(word_nav_frame, text="⏮️ Prev Slot", command=prev_highlight_word).pack(side="left", padx=2)
|
||||
tk.Button(word_nav_frame, text="⏭️ Next Slot", command=next_highlight_word).pack(side="left", padx=2)
|
||||
|
||||
preview_btn = tk.Button(root, text="▶️ Preview Clip", command=start_preview_thread)
|
||||
preview_btn.pack(pady=10)
|
||||
|
||||
export_btn = tk.Button(root, text="💾 Export Clip", command=export_clip)
|
||||
export_btn.pack(pady=5)
|
||||
|
||||
nav_frame = tk.Frame(root)
|
||||
nav_frame.pack(pady=5)
|
||||
tk.Button(nav_frame, text="⏮️ Prev", command=prev_sub).pack(side="left", padx=5)
|
||||
tk.Button(nav_frame, text="⏭️ Next", command=next_sub).pack(side="left", padx=5)
|
||||
|
||||
# Preset slot selection
|
||||
tk.Label(root, text="Preset Slots").pack()
|
||||
preset_label = tk.Label(root, text="Current Preset Slot: 1", bg="lightcoral", relief="sunken")
|
||||
preset_label.pack(pady=2)
|
||||
|
||||
preset_slot_frame = tk.Frame(root)
|
||||
preset_slot_frame.pack(pady=2)
|
||||
tk.Button(preset_slot_frame, text="Slot 1", command=lambda: change_preset_slot(1)).pack(side="left", padx=1)
|
||||
tk.Button(preset_slot_frame, text="Slot 2", command=lambda: change_preset_slot(2)).pack(side="left", padx=1)
|
||||
tk.Button(preset_slot_frame, text="Slot 3", command=lambda: change_preset_slot(3)).pack(side="left", padx=1)
|
||||
tk.Button(preset_slot_frame, text="Slot 4", command=lambda: change_preset_slot(4)).pack(side="left", padx=1)
|
||||
tk.Button(preset_slot_frame, text="Slot 5", command=lambda: change_preset_slot(5)).pack(side="left", padx=1)
|
||||
|
||||
save_btn = tk.Button(root, text="📂 Save Preset 1", command=save_presets)
|
||||
save_btn.pack(pady=5)
|
||||
|
||||
load_preset_btn = tk.Button(root, text="📂 Load Preset 1", command=load_presets)
|
||||
load_preset_btn.pack(pady=5)
|
||||
|
||||
root.mainloop()
|
||||
600
subtitle_generator2_fixed.py
Normal file
600
subtitle_generator2_fixed.py
Normal file
@ -0,0 +1,600 @@
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog
|
||||
from moviepy import VideoFileClip, ImageClip, CompositeVideoClip
|
||||
import threading
|
||||
import json
|
||||
import pysrt
|
||||
import os
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import numpy as np
|
||||
|
||||
def find_system_font():
|
||||
"""Find a working system font for PIL"""
|
||||
print("Finding system fonts for PIL...")
|
||||
fonts_to_try = [
|
||||
"C:/Windows/Fonts/arial.ttf",
|
||||
"C:/Windows/Fonts/calibri.ttf",
|
||||
"C:/Windows/Fonts/times.ttf",
|
||||
"C:/Windows/Fonts/verdana.ttf",
|
||||
"C:/Windows/Fonts/tahoma.ttf",
|
||||
"C:/Windows/Fonts/segoeui.ttf",
|
||||
]
|
||||
|
||||
for font_path in fonts_to_try:
|
||||
try:
|
||||
if os.path.exists(font_path):
|
||||
# Test the font by creating a small text image
|
||||
font = ImageFont.truetype(font_path, 20)
|
||||
img = Image.new('RGB', (100, 50), color='white')
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw.text((10, 10), "Test", font=font, fill='black')
|
||||
print(f"Using font: {font_path}")
|
||||
return font_path
|
||||
except Exception as e:
|
||||
print(f"Font test failed for {font_path}: {e}")
|
||||
continue
|
||||
|
||||
print("Using default font")
|
||||
return None
|
||||
|
||||
def create_text_image(text, font_size=50, color='white', stroke_color='black', stroke_width=3, font_path=None):
|
||||
"""Create a text image using PIL"""
|
||||
try:
|
||||
# Load font
|
||||
if font_path and os.path.exists(font_path):
|
||||
font = ImageFont.truetype(font_path, font_size)
|
||||
else:
|
||||
# Try to use default font
|
||||
try:
|
||||
font = ImageFont.load_default()
|
||||
except:
|
||||
# Last resort - create a basic font
|
||||
font = ImageFont.load_default()
|
||||
|
||||
# Get text dimensions
|
||||
temp_img = Image.new('RGB', (1, 1))
|
||||
temp_draw = ImageDraw.Draw(temp_img)
|
||||
|
||||
try:
|
||||
bbox = temp_draw.textbbox((0, 0), text, font=font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
except:
|
||||
# Fallback method for older PIL versions
|
||||
text_width, text_height = temp_draw.textsize(text, font=font)
|
||||
|
||||
# Add padding
|
||||
padding = max(stroke_width * 2, 10)
|
||||
img_width = text_width + padding * 2
|
||||
img_height = text_height + padding * 2
|
||||
|
||||
# Create transparent image
|
||||
img = Image.new('RGBA', (img_width, img_height), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
# Calculate text position (centered)
|
||||
x = (img_width - text_width) // 2
|
||||
y = (img_height - text_height) // 2
|
||||
|
||||
# Convert color names to RGB
|
||||
if isinstance(color, str):
|
||||
if color == 'white':
|
||||
color = (255, 255, 255, 255)
|
||||
elif color.startswith('#'):
|
||||
# Convert hex to RGB
|
||||
hex_color = color.lstrip('#')
|
||||
color = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + (255,)
|
||||
|
||||
if isinstance(stroke_color, str):
|
||||
if stroke_color == 'black':
|
||||
stroke_color = (0, 0, 0, 255)
|
||||
elif stroke_color.startswith('#'):
|
||||
hex_color = stroke_color.lstrip('#')
|
||||
stroke_color = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + (255,)
|
||||
|
||||
# Draw text with stroke
|
||||
if stroke_width > 0 and stroke_color:
|
||||
# Draw stroke by drawing text in multiple positions
|
||||
for dx in range(-stroke_width, stroke_width + 1):
|
||||
for dy in range(-stroke_width, stroke_width + 1):
|
||||
if dx*dx + dy*dy <= stroke_width*stroke_width:
|
||||
draw.text((x + dx, y + dy), text, font=font, fill=stroke_color)
|
||||
|
||||
# Draw main text
|
||||
draw.text((x, y), text, font=font, fill=color)
|
||||
|
||||
# Convert to numpy array for MoviePy
|
||||
img_array = np.array(img)
|
||||
|
||||
return img_array, img_width, img_height
|
||||
|
||||
except Exception as e:
|
||||
print(f"Text image creation failed: {e}")
|
||||
# Create a simple colored rectangle as fallback
|
||||
img = Image.new('RGBA', (200, 50), (255, 255, 255, 255))
|
||||
return np.array(img), 200, 50
|
||||
|
||||
# Find working font at startup
|
||||
SYSTEM_FONT = find_system_font()
|
||||
|
||||
# Global settings with defaults (no highlight_word_index here!)
|
||||
settings = {
|
||||
"subtitle_y_px": 1550,
|
||||
"highlight_offset": -8,
|
||||
"font_size_subtitle": 65,
|
||||
"font_size_highlight": 68,
|
||||
"highlight_x_offset": 0,
|
||||
"video_path": None,
|
||||
"font": "Arial",
|
||||
"srt_path": None,
|
||||
"highlight_start_time": 0.1, # When highlight appears (seconds)
|
||||
"highlight_duration": 1.5 # How long highlight lasts (seconds)
|
||||
}
|
||||
|
||||
preset_file = "subtitle_gui_presets.json"
|
||||
word_presets_file = "word_presets.json" # Store word selections per subtitle
|
||||
subtitles = []
|
||||
current_index = 0
|
||||
word_presets = {} # Dictionary to store word index for each subtitle
|
||||
|
||||
|
||||
def save_presets():
|
||||
with open(preset_file, "w") as f:
|
||||
json.dump(settings, f)
|
||||
|
||||
# Save word-specific presets
|
||||
with open(word_presets_file, "w") as f:
|
||||
json.dump(word_presets, f)
|
||||
|
||||
print("📂 Presets and word selections saved!")
|
||||
|
||||
|
||||
def load_presets():
|
||||
global settings, word_presets
|
||||
try:
|
||||
with open(preset_file, "r") as f:
|
||||
loaded = json.load(f)
|
||||
settings.update(loaded)
|
||||
|
||||
# Load word-specific presets
|
||||
try:
|
||||
with open(word_presets_file, "r") as f:
|
||||
word_presets = json.load(f)
|
||||
except FileNotFoundError:
|
||||
word_presets = {}
|
||||
|
||||
print("✅ Presets and word selections loaded!")
|
||||
sync_gui()
|
||||
except FileNotFoundError:
|
||||
print("⚠️ No presets found.")
|
||||
|
||||
|
||||
def sync_gui():
|
||||
sub_y_slider.set(settings["subtitle_y_px"])
|
||||
highlight_slider.set(settings["highlight_offset"])
|
||||
highlight_x_slider.set(settings["highlight_x_offset"])
|
||||
sub_font_slider.set(settings["font_size_subtitle"])
|
||||
highlight_font_slider.set(settings["font_size_highlight"])
|
||||
font_dropdown_var.set(settings["font"])
|
||||
highlight_start_slider.set(settings["highlight_start_time"])
|
||||
highlight_duration_slider.set(settings["highlight_duration"])
|
||||
update_highlight_word_display()
|
||||
|
||||
|
||||
def load_srt():
|
||||
global subtitles, current_index, word_presets
|
||||
path = filedialog.askopenfilename(filetypes=[("SRT files", "*.srt")])
|
||||
if path:
|
||||
settings["srt_path"] = path
|
||||
subtitles = pysrt.open(path)
|
||||
current_index = 0
|
||||
|
||||
# Load word presets for this SRT file
|
||||
load_word_presets_for_srt(path)
|
||||
|
||||
print(f"📜 Loaded subtitles: {path}")
|
||||
update_highlight_word_display()
|
||||
|
||||
|
||||
def load_word_presets_for_srt(srt_path):
|
||||
"""Load word presets specific to this SRT file"""
|
||||
global word_presets
|
||||
try:
|
||||
srt_key = os.path.basename(srt_path)
|
||||
if srt_key in word_presets:
|
||||
print(f"✅ Loaded word selections for {srt_key}")
|
||||
else:
|
||||
word_presets[srt_key] = {}
|
||||
print(f"📝 Created new word selection storage for {srt_key}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error loading word presets: {e}")
|
||||
word_presets = {}
|
||||
|
||||
|
||||
def get_current_word_index():
|
||||
"""Get the word index for the current subtitle"""
|
||||
if not subtitles or not settings["srt_path"]:
|
||||
return -1 # Default to last word
|
||||
|
||||
srt_key = os.path.basename(settings["srt_path"])
|
||||
subtitle_key = str(current_index)
|
||||
|
||||
if srt_key in word_presets and subtitle_key in word_presets[srt_key]:
|
||||
saved_index = word_presets[srt_key][subtitle_key]
|
||||
print(f"🔄 Loading saved word index {saved_index} for subtitle {current_index + 1}")
|
||||
return saved_index
|
||||
else:
|
||||
print(f"🆕 No saved word for subtitle {current_index + 1}, using default (last word)")
|
||||
return -1 # Default to last word
|
||||
|
||||
|
||||
def set_current_word_index(word_index):
|
||||
"""Set the word index for the current subtitle"""
|
||||
if not subtitles or not settings["srt_path"]:
|
||||
print("⚠️ Cannot save word index - no subtitles loaded")
|
||||
return
|
||||
|
||||
srt_key = os.path.basename(settings["srt_path"])
|
||||
subtitle_key = str(current_index)
|
||||
|
||||
if srt_key not in word_presets:
|
||||
word_presets[srt_key] = {}
|
||||
|
||||
word_presets[srt_key][subtitle_key] = word_index
|
||||
print(f"💾 Saved word selection for subtitle {current_index + 1}: word {word_index + 1}")
|
||||
|
||||
|
||||
def create_safe_textclip(text, font_size=50, color='white', stroke_color=None, stroke_width=0):
|
||||
"""Create a text clip using PIL instead of MoviePy TextClip"""
|
||||
try:
|
||||
# Create text image using PIL
|
||||
if stroke_color is None:
|
||||
stroke_color = 'black'
|
||||
if stroke_width == 0:
|
||||
stroke_width = 3
|
||||
|
||||
img_array, img_width, img_height = create_text_image(
|
||||
text=text,
|
||||
font_size=font_size,
|
||||
color=color,
|
||||
stroke_color=stroke_color,
|
||||
stroke_width=stroke_width,
|
||||
font_path=SYSTEM_FONT
|
||||
)
|
||||
|
||||
# Convert PIL image to MoviePy ImageClip
|
||||
from moviepy import ImageClip
|
||||
clip = ImageClip(img_array, duration=1)
|
||||
|
||||
print(f"Created text clip: '{text}' ({img_width}x{img_height})")
|
||||
return clip
|
||||
|
||||
except Exception as e:
|
||||
print(f"Text clip creation failed: {e}")
|
||||
# Return a simple colored clip as placeholder
|
||||
from moviepy import ColorClip
|
||||
placeholder = ColorClip(size=(800, 100), color=(255, 255, 255)).with_duration(1)
|
||||
return placeholder
|
||||
|
||||
def render_preview(save_path=None):
|
||||
if not settings["video_path"]:
|
||||
print("⚠️ No video selected.")
|
||||
return
|
||||
if not subtitles:
|
||||
print("⚠️ No subtitles loaded.")
|
||||
return
|
||||
|
||||
sub = subtitles[current_index]
|
||||
subtitle_text = sub.text.replace("\n", " ").strip()
|
||||
words = subtitle_text.split()
|
||||
|
||||
# Get highlight word based on INDIVIDUAL subtitle word index
|
||||
if len(words) > 0:
|
||||
word_index = get_current_word_index() # Get subtitle-specific word index
|
||||
if word_index < 0 or word_index >= len(words):
|
||||
word_index = len(words) - 1 # Default to last word
|
||||
set_current_word_index(word_index) # Save the default
|
||||
highlight_word = words[word_index]
|
||||
print(f"🎯 Using word '{highlight_word}' (index {word_index}) for subtitle {current_index + 1}")
|
||||
else:
|
||||
highlight_word = "word" # Fallback
|
||||
|
||||
start_time = sub.start.ordinal / 1000.0
|
||||
end_time = sub.end.ordinal / 1000.0
|
||||
|
||||
clip = VideoFileClip(settings["video_path"]).subclipped(start_time, min(end_time, start_time + 3))
|
||||
vertical_clip = clip.resized(height=1920).cropped(width=1080, x_center=clip.w / 2)
|
||||
|
||||
# Create base subtitle using safe method
|
||||
try:
|
||||
base_subtitle = create_safe_textclip(
|
||||
subtitle_text,
|
||||
font_size=settings["font_size_subtitle"],
|
||||
color='white',
|
||||
stroke_color='black',
|
||||
stroke_width=5
|
||||
).with_duration(clip.duration).with_position(('center', settings["subtitle_y_px"]))
|
||||
print("✅ Base subtitle created successfully")
|
||||
except Exception as e:
|
||||
print(f"❌ Base subtitle creation failed: {e}")
|
||||
return
|
||||
|
||||
full_text = subtitle_text.upper()
|
||||
words = full_text.split()
|
||||
if highlight_word.upper() not in words:
|
||||
highlight_word = words[-1] # Fallback
|
||||
highlight_index = words.index(highlight_word.upper())
|
||||
chars_before = sum(len(w) + 1 for w in words[:highlight_index])
|
||||
char_width = 35
|
||||
total_width = len(full_text) * char_width
|
||||
x_offset = (chars_before * char_width) - (total_width // 2) + settings["highlight_x_offset"]
|
||||
|
||||
# Create highlighted word using safe method
|
||||
try:
|
||||
highlighted_word = create_safe_textclip(
|
||||
highlight_word,
|
||||
font_size=settings["font_size_highlight"],
|
||||
color='#FFD700',
|
||||
stroke_color='#FF6B35',
|
||||
stroke_width=5
|
||||
).with_duration(min(settings["highlight_duration"], clip.duration)).with_start(settings["highlight_start_time"]).with_position((540 + x_offset, settings["subtitle_y_px"] + settings["highlight_offset"]))
|
||||
print(f"✅ Highlighted word created successfully (start: {settings['highlight_start_time']}s, duration: {settings['highlight_duration']}s)")
|
||||
except Exception as e:
|
||||
print(f"❌ Highlighted word creation failed: {e}")
|
||||
# Create without highlight if it fails
|
||||
highlighted_word = None
|
||||
print("⚠️ Continuing without highlight")
|
||||
|
||||
# Compose final video with or without highlight
|
||||
if highlighted_word is not None:
|
||||
final = CompositeVideoClip([vertical_clip, base_subtitle, highlighted_word], size=(1080, 1920))
|
||||
print("✅ Final video composed with highlight")
|
||||
else:
|
||||
final = CompositeVideoClip([vertical_clip, base_subtitle], size=(1080, 1920))
|
||||
print("✅ Final video composed without highlight")
|
||||
|
||||
if save_path:
|
||||
srt_output_path = os.path.splitext(save_path)[0] + ".srt"
|
||||
with open(srt_output_path, "w") as srt_file:
|
||||
srt_file.write(sub.__unicode__())
|
||||
final.write_videofile(save_path, fps=24)
|
||||
print(f"✅ Exported SRT: {srt_output_path}")
|
||||
else:
|
||||
# Create a smaller preview version to fit 1080p screen better
|
||||
# Scale down to 50% size: 540x960 instead of 1080x1920
|
||||
preview_final = final.resized(0.5)
|
||||
print("🎬 Opening preview window (540x960 - scaled to fit 1080p screen)")
|
||||
preview_final.preview(fps=24)
|
||||
preview_final.close()
|
||||
|
||||
clip.close()
|
||||
final.close()
|
||||
|
||||
|
||||
def update_setting(var_name, value):
|
||||
if var_name in ["highlight_start_time", "highlight_duration"]:
|
||||
settings[var_name] = float(value)
|
||||
else:
|
||||
settings[var_name] = int(value) if var_name.startswith("font_size") or "offset" in var_name or "y_px" in var_name else value
|
||||
|
||||
|
||||
def update_font(value):
|
||||
settings["font"] = value
|
||||
|
||||
|
||||
def open_video():
|
||||
file_path = filedialog.askopenfilename(filetypes=[("MP4 files", "*.mp4")])
|
||||
if file_path:
|
||||
settings["video_path"] = file_path
|
||||
print(f"📂 Loaded video: {file_path}")
|
||||
|
||||
|
||||
def start_preview_thread():
|
||||
threading.Thread(target=render_preview).start()
|
||||
|
||||
|
||||
def export_clip():
|
||||
if settings["video_path"]:
|
||||
base_name = os.path.splitext(os.path.basename(settings["video_path"]))[0]
|
||||
out_path = os.path.join(os.path.dirname(settings["video_path"]), f"{base_name}_clip_exported.mp4")
|
||||
threading.Thread(target=render_preview, args=(out_path,)).start()
|
||||
print(f"💾 Exporting to: {out_path}")
|
||||
|
||||
|
||||
def prev_sub():
|
||||
global current_index
|
||||
if current_index > 0:
|
||||
current_index -= 1
|
||||
print(f"📍 Switched to subtitle {current_index + 1}")
|
||||
update_highlight_word_display()
|
||||
start_preview_thread()
|
||||
|
||||
|
||||
def next_sub():
|
||||
global current_index
|
||||
if current_index < len(subtitles) - 1:
|
||||
current_index += 1
|
||||
print(f"📍 Switched to subtitle {current_index + 1}")
|
||||
update_highlight_word_display()
|
||||
start_preview_thread()
|
||||
|
||||
|
||||
def prev_highlight_word():
|
||||
"""Select the previous word to highlight"""
|
||||
if not subtitles:
|
||||
return
|
||||
|
||||
sub = subtitles[current_index]
|
||||
words = sub.text.replace("\n", " ").strip().split()
|
||||
if len(words) <= 1:
|
||||
return
|
||||
|
||||
current_word_index = get_current_word_index()
|
||||
if current_word_index < 0:
|
||||
current_word_index = len(words) - 1
|
||||
|
||||
new_index = current_word_index - 1
|
||||
if new_index < 0:
|
||||
new_index = len(words) - 1 # Wrap to last word
|
||||
|
||||
set_current_word_index(new_index)
|
||||
update_highlight_word_display()
|
||||
start_preview_thread()
|
||||
|
||||
|
||||
def next_highlight_word():
|
||||
"""Select the next word to highlight"""
|
||||
if not subtitles:
|
||||
return
|
||||
|
||||
sub = subtitles[current_index]
|
||||
words = sub.text.replace("\n", " ").strip().split()
|
||||
if len(words) <= 1:
|
||||
return
|
||||
|
||||
current_word_index = get_current_word_index()
|
||||
if current_word_index < 0:
|
||||
current_word_index = len(words) - 1
|
||||
|
||||
new_index = (current_word_index + 1) % len(words)
|
||||
set_current_word_index(new_index)
|
||||
update_highlight_word_display()
|
||||
start_preview_thread()
|
||||
|
||||
|
||||
def update_highlight_word_display():
|
||||
"""Update the display showing which word is selected for highlight"""
|
||||
if not subtitles:
|
||||
highlight_word_label.config(text="Highlighted Word: None")
|
||||
return
|
||||
|
||||
sub = subtitles[current_index]
|
||||
words = sub.text.replace("\n", " ").strip().split()
|
||||
|
||||
if len(words) > 0:
|
||||
word_index = get_current_word_index()
|
||||
if word_index < 0 or word_index >= len(words):
|
||||
word_index = len(words) - 1
|
||||
set_current_word_index(word_index) # Save the default
|
||||
|
||||
highlight_word = words[word_index]
|
||||
highlight_word_label.config(text=f"Highlighted Word: '{highlight_word}' ({word_index + 1}/{len(words)})")
|
||||
print(f"🔄 Display updated: subtitle {current_index + 1}, word '{highlight_word}' ({word_index + 1}/{len(words)})")
|
||||
else:
|
||||
highlight_word_label.config(text="Highlighted Word: None")
|
||||
|
||||
|
||||
def handle_drop(event):
|
||||
path = event.data
|
||||
if path.endswith(".mp4"):
|
||||
settings["video_path"] = path
|
||||
print(f"🎥 Dropped video: {path}")
|
||||
elif path.endswith(".srt"):
|
||||
settings["srt_path"] = path
|
||||
global subtitles, current_index
|
||||
subtitles = pysrt.open(path)
|
||||
current_index = 0
|
||||
load_word_presets_for_srt(path)
|
||||
print(f"📜 Dropped subtitles: {path}")
|
||||
update_highlight_word_display()
|
||||
|
||||
|
||||
# GUI Setup
|
||||
root = tk.Tk()
|
||||
root.title("Subtitle Positioning Tool - Fixed Version")
|
||||
root.geometry("420x800")
|
||||
|
||||
root.drop_target_register = getattr(root, 'drop_target_register', lambda *args: None)
|
||||
root.dnd_bind = getattr(root, 'dnd_bind', lambda *args, **kwargs: None)
|
||||
try:
|
||||
import tkinterdnd2 as tkdnd
|
||||
root.drop_target_register(tkdnd.DND_FILES)
|
||||
root.dnd_bind('<<Drop>>', handle_drop)
|
||||
except:
|
||||
pass
|
||||
|
||||
load_btn = tk.Button(root, text="🎥 Load Video", command=open_video)
|
||||
load_btn.pack(pady=5)
|
||||
|
||||
load_srt_btn = tk.Button(root, text="📜 Load SRT", command=load_srt)
|
||||
load_srt_btn.pack(pady=5)
|
||||
|
||||
tk.Label(root, text="Subtitle Y Position").pack()
|
||||
sub_y_slider = tk.Scale(root, from_=1000, to=1800, orient="horizontal", command=lambda v: update_setting("subtitle_y_px", v))
|
||||
sub_y_slider.set(settings["subtitle_y_px"])
|
||||
sub_y_slider.pack()
|
||||
|
||||
tk.Label(root, text="Highlight Y Offset").pack()
|
||||
highlight_slider = tk.Scale(root, from_=-100, to=100, orient="horizontal", command=lambda v: update_setting("highlight_offset", v))
|
||||
highlight_slider.set(settings["highlight_offset"])
|
||||
highlight_slider.pack()
|
||||
|
||||
tk.Label(root, text="Highlight X Offset").pack()
|
||||
highlight_x_slider = tk.Scale(root, from_=-300, to=300, orient="horizontal", command=lambda v: update_setting("highlight_x_offset", v))
|
||||
highlight_x_slider.set(settings["highlight_x_offset"])
|
||||
highlight_x_slider.pack()
|
||||
|
||||
tk.Label(root, text="Subtitle Font Size").pack()
|
||||
sub_font_slider = tk.Scale(root, from_=30, to=100, orient="horizontal", command=lambda v: update_setting("font_size_subtitle", v))
|
||||
sub_font_slider.set(settings["font_size_subtitle"])
|
||||
sub_font_slider.pack()
|
||||
|
||||
tk.Label(root, text="Highlight Font Size").pack()
|
||||
highlight_font_slider = tk.Scale(root, from_=30, to=100, orient="horizontal", command=lambda v: update_setting("font_size_highlight", v))
|
||||
highlight_font_slider.set(settings["font_size_highlight"])
|
||||
highlight_font_slider.pack()
|
||||
|
||||
tk.Label(root, text="Highlight Start Time (seconds)").pack()
|
||||
highlight_start_slider = tk.Scale(root, from_=0.0, to=3.0, resolution=0.1, orient="horizontal", command=lambda v: update_setting("highlight_start_time", v))
|
||||
highlight_start_slider.set(settings["highlight_start_time"])
|
||||
highlight_start_slider.pack()
|
||||
|
||||
tk.Label(root, text="Highlight Duration (seconds)").pack()
|
||||
highlight_duration_slider = tk.Scale(root, from_=0.1, to=5.0, resolution=0.1, orient="horizontal", command=lambda v: update_setting("highlight_duration", v))
|
||||
highlight_duration_slider.set(settings["highlight_duration"])
|
||||
highlight_duration_slider.pack()
|
||||
|
||||
font_dropdown_var = tk.StringVar(value=settings["font"])
|
||||
tk.Label(root, text="Font").pack()
|
||||
font_dropdown = tk.OptionMenu(
|
||||
root,
|
||||
font_dropdown_var,
|
||||
"Arial",
|
||||
"Courier",
|
||||
"Times-Roman",
|
||||
"Helvetica-Bold",
|
||||
"Verdana",
|
||||
"Georgia",
|
||||
"Impact",
|
||||
command=update_font
|
||||
)
|
||||
font_dropdown.pack(pady=5)
|
||||
|
||||
# Highlight word selection
|
||||
tk.Label(root, text="Select Highlight Word").pack()
|
||||
highlight_word_label = tk.Label(root, text="Highlighted Word: None", bg="lightgray", relief="sunken")
|
||||
highlight_word_label.pack(pady=5)
|
||||
|
||||
word_nav_frame = tk.Frame(root)
|
||||
word_nav_frame.pack(pady=5)
|
||||
tk.Button(word_nav_frame, text="⏮️ Prev Word", command=prev_highlight_word).pack(side="left", padx=2)
|
||||
tk.Button(word_nav_frame, text="⏭️ Next Word", command=next_highlight_word).pack(side="left", padx=2)
|
||||
|
||||
preview_btn = tk.Button(root, text="▶️ Preview Clip", command=start_preview_thread)
|
||||
preview_btn.pack(pady=10)
|
||||
|
||||
export_btn = tk.Button(root, text="💾 Export Clip", command=export_clip)
|
||||
export_btn.pack(pady=5)
|
||||
|
||||
nav_frame = tk.Frame(root)
|
||||
nav_frame.pack(pady=5)
|
||||
tk.Button(nav_frame, text="⏮️ Prev", command=prev_sub).pack(side="left", padx=5)
|
||||
tk.Button(nav_frame, text="⏭️ Next", command=next_sub).pack(side="left", padx=5)
|
||||
|
||||
save_btn = tk.Button(root, text="📂 Save Preset", command=save_presets)
|
||||
save_btn.pack(pady=5)
|
||||
|
||||
load_preset_btn = tk.Button(root, text="📂 Load Preset", command=load_presets)
|
||||
load_preset_btn.pack(pady=5)
|
||||
|
||||
root.mainloop()
|
||||
@ -1 +1 @@
|
||||
{"subtitle_y_px": 1550, "highlight_offset": 0, "font_size_subtitle": 65, "font_size_highlight": 65, "highlight_x_offset": -53, "video_path": "C:/Users/braul/Desktop/shorts_project/shorts/short_1.mp4"}
|
||||
{"subtitle_y_px": 1550, "highlight_offset": 0, "font_size_subtitle": 65, "font_size_highlight": 65, "highlight_x_offset": 26, "video_path": "C:/Users/braul/Desktop/shorts_project/shorts/short_1.mp4", "font": "Arial", "srt_path": "C:/Users/braul/Desktop/shorts_project/subtitles.srt", "highlight_word_index": -1, "highlight_start_time": 0.1, "highlight_duration": 1.5}
|
||||
1
subtitle_gui_presets_slot_1.json
Normal file
1
subtitle_gui_presets_slot_1.json
Normal file
@ -0,0 +1 @@
|
||||
{"subtitle_y_px": 1550, "highlight_offset": 3, "font_size_subtitle": 65, "font_size_highlight": 65, "highlight_x_offset": 26, "video_path": "C:/Users/braul/Desktop/shorts_project/shorts/short_1.mp4", "font": "Arial", "srt_path": "C:/Users/braul/Desktop/shorts_project/subtitles.srt", "highlight_word_index": -1, "highlight_start_time": 0.1, "highlight_duration": 1.5}
|
||||
1
subtitle_gui_presets_slot_2.json
Normal file
1
subtitle_gui_presets_slot_2.json
Normal file
@ -0,0 +1 @@
|
||||
{"subtitle_y_px": 1550, "highlight_offset": 0, "font_size_subtitle": 65, "font_size_highlight": 65, "highlight_x_offset": -18, "video_path": "C:/Users/braul/Desktop/shorts_project/shorts/short_1.mp4", "font": "Arial", "srt_path": "C:/Users/braul/Desktop/shorts_project/subtitles.srt", "highlight_word_index": -1, "highlight_start_time": 0.1, "highlight_duration": 1.5}
|
||||
1
word_presets.json
Normal file
1
word_presets.json
Normal file
@ -0,0 +1 @@
|
||||
{"subtitles.srt": {"0": 0}}
|
||||
1
word_presets_slot_1.json
Normal file
1
word_presets_slot_1.json
Normal file
@ -0,0 +1 @@
|
||||
{"subtitles.srt": {"0": 0}}
|
||||
1
word_presets_slot_2.json
Normal file
1
word_presets_slot_2.json
Normal file
@ -0,0 +1 @@
|
||||
{"subtitles.srt": {"0": 1}}
|
||||
Loading…
Reference in New Issue
Block a user