diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..7f62ddf --- /dev/null +++ b/.github/copilot-instructions.md @@ -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. diff --git a/subtitle_generator2.py b/subtitle_generator2.py new file mode 100644 index 0000000..bae56c6 --- /dev/null +++ b/subtitle_generator2.py @@ -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"๏ฟฝ 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('<>', 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() diff --git a/subtitle_generator2_fixed.py b/subtitle_generator2_fixed.py new file mode 100644 index 0000000..60491ab --- /dev/null +++ b/subtitle_generator2_fixed.py @@ -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('<>', 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() diff --git a/subtitle_gui_presets.json b/subtitle_gui_presets.json index 8858791..960aa0f 100644 --- a/subtitle_gui_presets.json +++ b/subtitle_gui_presets.json @@ -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"} \ No newline at end of file +{"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} \ No newline at end of file diff --git a/subtitle_gui_presets_slot_1.json b/subtitle_gui_presets_slot_1.json new file mode 100644 index 0000000..c92e906 --- /dev/null +++ b/subtitle_gui_presets_slot_1.json @@ -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} \ No newline at end of file diff --git a/subtitle_gui_presets_slot_2.json b/subtitle_gui_presets_slot_2.json new file mode 100644 index 0000000..3cc7f69 --- /dev/null +++ b/subtitle_gui_presets_slot_2.json @@ -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} \ No newline at end of file diff --git a/word_presets.json b/word_presets.json new file mode 100644 index 0000000..7a49fe8 --- /dev/null +++ b/word_presets.json @@ -0,0 +1 @@ +{"subtitles.srt": {"0": 0}} \ No newline at end of file diff --git a/word_presets_slot_1.json b/word_presets_slot_1.json new file mode 100644 index 0000000..7a49fe8 --- /dev/null +++ b/word_presets_slot_1.json @@ -0,0 +1 @@ +{"subtitles.srt": {"0": 0}} \ No newline at end of file diff --git a/word_presets_slot_2.json b/word_presets_slot_2.json new file mode 100644 index 0000000..ccdc475 --- /dev/null +++ b/word_presets_slot_2.json @@ -0,0 +1 @@ +{"subtitles.srt": {"0": 1}} \ No newline at end of file