ShortGenerator/video_editor_pyqt6.py

1598 lines
60 KiB
Python

"""
Professional Video Editor for Generated Shorts - PyQt6 Version
Author: Dario Pascoal
Description: This is a comprehensive video editing application designed specifically for editing
short-form video content, fully migrated to PyQt6 for enhanced performance and professional features.
The application provides a professional timeline-based interface similar to industry-standard
video editing software, with features including:
- Multi-track timeline with visual track roads for professional editing workflow
- Hardware-accelerated video preview with frame-accurate scrubbing
- Professional editing tools: trim, speed adjustment, volume control, fade effects
- Real-time effects system with ripple, fade, and text overlays
- Text overlay capabilities with customizable styling
- Export functionality with multiple format support
- Tabbed interface organizing tools into logical categories
- Dark theme optimized for video editing work
- Support for multiple video formats (MP4, AVI, MOV, MKV, etc.)
- Full keyboard control system (Space, arrows, F11, etc.)
- Professional fullscreen mode with all effects
PyQt6 Advantages:
- Hardware-accelerated video playback with QMediaPlayer
- OpenGL timeline rendering for smooth performance
- Professional UI components and styling
- Better threading and signal/slot architecture
- Native multimedia framework integration
- GPU-accelerated effects pipeline
- Professional dock system for panels
- Modern styling with CSS-like stylesheets
Technical Architecture:
- Uses QMediaPlayer for hardware-accelerated video playback
- Implements QGraphicsView-based timeline with precise time calculations
- Signal/slot pattern for clean event handling
- QThread-based background processing
- Hardware-accelerated effects pipeline ready for expansion
- Maintains professional video editing workflow patterns
"""
import sys
import os
import threading
import time
from datetime import datetime
import cv2
import numpy as np
from PIL import Image
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
QSlider, QPushButton, QLabel, QFrame, QTabWidget, QScrollArea, QSplitter,
QFileDialog, QMessageBox, QSpacerItem, QSizePolicy, QLineEdit, QSpinBox,
QDoubleSpinBox, QComboBox, QCheckBox, QProgressBar, QTextEdit, QGroupBox,
QToolButton, QButtonGroup, QDockWidget, QStyleFactory, QStackedWidget, QStackedLayout
)
from PyQt6.QtCore import (
Qt, QUrl, QTimer, pyqtSignal, QPropertyAnimation, QEasingCurve, QRect,
QThread, pyqtSlot, QObject, QRunnable, QThreadPool, QMutex, QSize, QEvent
)
from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput
from PyQt6.QtMultimediaWidgets import QVideoWidget
from PyQt6.QtGui import (
QPalette, QColor, QFont, QIcon, QKeySequence, QShortcut, QPixmap, QPainter,
QBrush, QPen, QPolygon, QAction, QCursor
)
# Try to import MoviePy, handle if not available
try:
from moviepy.editor import VideoFileClip, TextClip, CompositeVideoClip
from moviepy.video.fx import FadeIn, FadeOut, Resize
from moviepy.audio.fx import MultiplyVolume
MOVIEPY_AVAILABLE = True
except ImportError:
print("⚠️ MoviePy not available - using OpenCV backend for video processing")
MOVIEPY_AVAILABLE = False
class ProfessionalSlider(QSlider):
"""Custom slider with professional video editing styling"""
def __init__(self, orientation=Qt.Orientation.Horizontal):
super().__init__(orientation)
self.setStyleSheet("""
QSlider::groove:horizontal {
background: #2d2d2d;
height: 6px;
border-radius: 3px;
border: 1px solid #404040;
}
QSlider::handle:horizontal {
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
stop:0 #00aaff, stop:1 #0088cc);
border: 2px solid #ffffff;
width: 16px;
height: 16px;
margin: -6px 0;
border-radius: 8px;
}
QSlider::handle:horizontal:hover {
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
stop:0 #00ccff, stop:1 #00aadd);
border: 2px solid #ffffff;
}
QSlider::handle:horizontal:pressed {
background: #ffffff;
border: 2px solid #00aaff;
}
QSlider::sub-page:horizontal {
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
stop:0 #00aaff, stop:1 #0077bb);
border-radius: 3px;
}
QSlider::add-page:horizontal {
background: #404040;
border-radius: 3px;
}
""")
class TimelineWidget(QWidget):
"""Professional timeline widget for video editing"""
positionChanged = pyqtSignal(float) # Emit time position changes
def __init__(self):
super().__init__()
self.duration = 0.0
self.current_time = 0.0
self.zoom_level = 1.0
self.track_height = 70 # Adjusted for 5 tracks to fit properly
self.ruler_height = 30 # Increased ruler height
self.setup_ui()
def setup_ui(self):
"""Setup timeline interface"""
self.setMinimumHeight(400) # Much larger for proper track visibility
self.setStyleSheet("""
TimelineWidget {
background-color: #1e1e1e;
border: 1px solid #404040;
}
""")
def paintEvent(self, event):
"""Custom paint event for timeline rendering"""
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
# Draw timeline background
painter.fillRect(self.rect(), QColor("#1e1e1e"))
# Draw time ruler first
self.draw_time_ruler(painter)
# Draw track areas with visual representations like original Tkinter version
track_count = 5 # Video 1, Video 2, Video 3, Audio 1, Audio 2
track_names = ["🎬 Video 1", "🎬 Video 2", "🎬 Video 3", "🎵 Audio 1", "🎵 Audio 2"]
track_colors = ["#3498db", "#2ecc71", "#9b59b6", "#e74c3c", "#f39c12"]
# Start tracks below the ruler
ruler_bottom = self.ruler_height
for i in range(track_count):
y = ruler_bottom + (i * self.track_height)
# Draw track background
track_rect = QRect(0, y, self.width(), self.track_height)
painter.fillRect(track_rect, QColor("#2d2d2d"))
# Draw track border
painter.setPen(QPen(QColor("#404040"), 1))
painter.drawRect(track_rect)
# Draw track content area (like original design)
content_rect = QRect(120, y + 10, self.width() - 130, self.track_height - 20)
painter.fillRect(content_rect, QColor("#1a1a1a"))
painter.setPen(QPen(QColor(track_colors[i]), 2))
painter.drawRect(content_rect)
# Draw sample content blocks (visual representation)
if self.duration > 0:
content_width = content_rect.width()
# Draw a content block spanning part of the timeline
block_width = min(200, content_width // 2)
block_rect = QRect(content_rect.x() + 10, content_rect.y() + 5,
block_width, content_rect.height() - 10)
# Fill with track color
painter.fillRect(block_rect, QColor(track_colors[i]))
painter.setPen(QPen(QColor("#ffffff"), 1))
painter.drawRect(block_rect)
# Draw track name on the content block
painter.setFont(QFont("Arial", 10, QFont.Weight.Bold))
painter.setPen(QPen(QColor("#ffffff"), 1))
text_rect = QRect(block_rect.x() + 5, block_rect.y(),
block_rect.width() - 10, block_rect.height())
painter.drawText(text_rect, Qt.AlignmentFlag.AlignCenter, track_names[i])
# Draw track labels on the left
label_rect = QRect(10, y, 100, self.track_height)
painter.setFont(QFont("Arial", 11, QFont.Weight.Bold))
painter.setPen(QPen(QColor("#ffffff"), 1))
painter.drawText(label_rect, Qt.AlignmentFlag.AlignCenter, track_names[i])
# Draw separation line between ruler and tracks
painter.setPen(QPen(QColor("#666666"), 2))
painter.drawLine(0, ruler_bottom, self.width(), ruler_bottom)
# Draw playhead on top of everything
if self.duration > 0:
playhead_x = 120 + ((self.current_time / self.duration) * (self.width() - 130))
painter.setPen(QPen(QColor("#ff4444"), 3))
painter.drawLine(int(playhead_x), ruler_bottom, int(playhead_x), self.height())
# Draw playhead handle
handle_rect = QRect(int(playhead_x) - 8, ruler_bottom - 15, 16, 15)
painter.fillRect(handle_rect, QColor("#ff4444"))
painter.setPen(QPen(QColor("#ffffff"), 1))
painter.drawRect(handle_rect)
def draw_time_ruler(self, painter):
"""Draw time ruler at the top"""
painter.fillRect(0, 0, self.width(), self.ruler_height, QColor("#2d2d2d"))
if self.duration > 0:
# Calculate time intervals
content_area_start = 120
content_area_width = self.width() - 130
pixels_per_second = content_area_width / self.duration
painter.setPen(QPen(QColor("#cccccc"), 1))
painter.setFont(QFont("Arial", 9))
# Draw second markers
for second in range(int(self.duration) + 1):
x = content_area_start + (second * pixels_per_second)
if x <= self.width() - 10:
painter.drawLine(int(x), self.ruler_height - 8, int(x), self.ruler_height)
# Draw time labels every 5 seconds
if second % 5 == 0:
painter.drawText(int(x) + 2, self.ruler_height - 12, f"{second}s")
# Draw ruler border
painter.setPen(QPen(QColor("#404040"), 1))
painter.drawRect(0, 0, self.width(), self.ruler_height)
def mousePressEvent(self, event):
"""Handle timeline clicking for seeking"""
if event.button() == Qt.MouseButton.LeftButton and self.duration > 0:
# Only allow seeking in the content area (not on track labels)
content_area_start = 120
if event.position().x() >= content_area_start:
# Calculate time from click position relative to content area
content_width = self.width() - 130
click_x = event.position().x() - content_area_start
click_time = (click_x / content_width) * self.duration
self.current_time = max(0, min(click_time, self.duration))
self.positionChanged.emit(self.current_time)
self.update()
def set_duration(self, duration):
"""Set timeline duration"""
self.duration = duration
self.update()
def set_position(self, position):
"""Set current playhead position"""
self.current_time = position
self.update()
class EffectsPanel(QWidget):
"""Professional effects panel with time-based controls"""
effectApplied = pyqtSignal(str, dict) # effect_type, parameters
def __init__(self):
super().__init__()
self.setup_ui()
def setup_ui(self):
"""Setup effects panel interface"""
layout = QVBoxLayout(self)
# Ripple Effect
ripple_group = QGroupBox("🌊 Ripple Effect")
ripple_group.setStyleSheet("""
QGroupBox {
font-weight: bold;
border: 2px solid #404040;
border-radius: 5px;
margin-top: 10px;
color: #ffffff;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
}
""")
ripple_layout = QVBoxLayout(ripple_group)
# Time controls
time_layout = QHBoxLayout()
time_layout.addWidget(QLabel("Start:"))
self.ripple_start = QDoubleSpinBox()
self.ripple_start.setRange(0.0, 999.0)
self.ripple_start.setSingleStep(0.1)
self.ripple_start.setSuffix("s")
time_layout.addWidget(self.ripple_start)
time_layout.addWidget(QLabel("End:"))
self.ripple_end = QDoubleSpinBox()
self.ripple_end.setRange(0.0, 999.0)
self.ripple_end.setSingleStep(0.1)
self.ripple_end.setValue(2.0)
self.ripple_end.setSuffix("s")
time_layout.addWidget(self.ripple_end)
ripple_layout.addLayout(time_layout)
# Intensity control
intensity_layout = QHBoxLayout()
intensity_layout.addWidget(QLabel("Intensity:"))
self.ripple_intensity = QSpinBox()
self.ripple_intensity.setRange(1, 50)
self.ripple_intensity.setValue(10)
intensity_layout.addWidget(self.ripple_intensity)
ripple_layout.addLayout(intensity_layout)
# Apply button
apply_ripple_btn = QPushButton("Apply Ripple Effect")
apply_ripple_btn.clicked.connect(self.apply_ripple_effect)
apply_ripple_btn.setStyleSheet("""
QPushButton {
background-color: #0066cc;
color: white;
border: none;
padding: 8px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #0088ff;
}
""")
ripple_layout.addWidget(apply_ripple_btn)
layout.addWidget(ripple_group)
# Fade Effect
fade_group = QGroupBox("🌅 Fade Effect")
fade_group.setStyleSheet(ripple_group.styleSheet())
fade_layout = QVBoxLayout(fade_group)
# Fade time controls
fade_time_layout = QHBoxLayout()
fade_time_layout.addWidget(QLabel("Start:"))
self.fade_start = QDoubleSpinBox()
self.fade_start.setRange(0.0, 999.0)
self.fade_start.setSingleStep(0.1)
self.fade_start.setSuffix("s")
fade_time_layout.addWidget(self.fade_start)
fade_time_layout.addWidget(QLabel("End:"))
self.fade_end = QDoubleSpinBox()
self.fade_end.setRange(0.0, 999.0)
self.fade_end.setSingleStep(0.1)
self.fade_end.setValue(3.0)
self.fade_end.setSuffix("s")
fade_time_layout.addWidget(self.fade_end)
fade_layout.addLayout(fade_time_layout)
# Apply button
apply_fade_btn = QPushButton("Apply Fade Effect")
apply_fade_btn.clicked.connect(self.apply_fade_effect)
apply_fade_btn.setStyleSheet(apply_ripple_btn.styleSheet())
fade_layout.addWidget(apply_fade_btn)
layout.addWidget(fade_group)
# Text Effect
text_group = QGroupBox("📝 Text Overlay")
text_group.setStyleSheet(ripple_group.styleSheet())
text_layout = QVBoxLayout(text_group)
# Text input
self.text_input = QLineEdit()
self.text_input.setPlaceholderText("Enter text to overlay...")
self.text_input.setText("Sample Text")
text_layout.addWidget(self.text_input)
# Text time controls
text_time_layout = QHBoxLayout()
text_time_layout.addWidget(QLabel("Start:"))
self.text_start = QDoubleSpinBox()
self.text_start.setRange(0.0, 999.0)
self.text_start.setSingleStep(0.1)
self.text_start.setSuffix("s")
text_time_layout.addWidget(self.text_start)
text_time_layout.addWidget(QLabel("End:"))
self.text_end = QDoubleSpinBox()
self.text_end.setRange(0.0, 999.0)
self.text_end.setSingleStep(0.1)
self.text_end.setValue(4.0)
self.text_end.setSuffix("s")
text_time_layout.addWidget(self.text_end)
text_layout.addLayout(text_time_layout)
# Apply button
apply_text_btn = QPushButton("Apply Text Overlay")
apply_text_btn.clicked.connect(self.apply_text_effect)
apply_text_btn.setStyleSheet(apply_ripple_btn.styleSheet())
text_layout.addWidget(apply_text_btn)
layout.addWidget(text_group)
# Add stretch to push everything to top
layout.addStretch()
def apply_ripple_effect(self):
"""Apply ripple effect with current parameters"""
params = {
'start_time': self.ripple_start.value(),
'end_time': self.ripple_end.value(),
'intensity': self.ripple_intensity.value()
}
self.effectApplied.emit('ripple', params)
def apply_fade_effect(self):
"""Apply fade effect with current parameters"""
params = {
'start_time': self.fade_start.value(),
'end_time': self.fade_end.value()
}
self.effectApplied.emit('fade', params)
def apply_text_effect(self):
"""Apply text overlay with current parameters"""
params = {
'start_time': self.text_start.value(),
'end_time': self.text_end.value(),
'text': self.text_input.text()
}
self.effectApplied.emit('text', params)
class MediaBin(QWidget):
"""Professional media bin for file management"""
mediaSelected = pyqtSignal(str) # file_path
def __init__(self):
super().__init__()
self.media_files = []
self.setup_ui()
def setup_ui(self):
"""Setup media bin interface"""
layout = QVBoxLayout(self)
# Header
header = QLabel("📁 Media Bin")
header.setStyleSheet("""
QLabel {
color: #ffffff;
font-size: 14px;
font-weight: bold;
padding: 10px;
background-color: #2d2d2d;
border-radius: 4px;
}
""")
layout.addWidget(header)
# Add media button
add_btn = QPushButton("+ Add Media")
add_btn.clicked.connect(self.add_media_file)
add_btn.setStyleSheet("""
QPushButton {
background-color: #006600;
color: white;
border: none;
padding: 8px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #008800;
}
""")
layout.addWidget(add_btn)
# Media list
self.media_list = QWidget()
self.media_list_layout = QVBoxLayout(self.media_list)
# Minimize spacing between media items for ultra-compact layout
self.media_list_layout.setSpacing(1)
self.media_list_layout.setContentsMargins(2, 2, 2, 2)
# Add stretch at the end to push items to the top
self.media_list_layout.addStretch()
scroll_area = QScrollArea()
scroll_area.setWidget(self.media_list)
scroll_area.setWidgetResizable(True)
scroll_area.setStyleSheet("""
QScrollArea {
border: 1px solid #404040;
border-radius: 4px;
background-color: #1e1e1e;
padding: 2px;
}
QScrollBar:vertical {
width: 12px;
background-color: #2d2d2d;
}
""")
layout.addWidget(scroll_area, 1)
# Auto-load existing media
self.auto_load_media()
def auto_load_media(self):
"""Auto-load media files from current directory"""
video_extensions = ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv']
for file in os.listdir('.'):
if any(file.lower().endswith(ext) for ext in video_extensions):
self.add_media_to_list(file)
def add_media_file(self):
"""Add media file through file dialog"""
file_path, _ = QFileDialog.getOpenFileName(
self,
"Select Media File",
"",
"Video Files (*.mp4 *.avi *.mov *.mkv *.wmv *.flv *.webm);;All Files (*)"
)
if file_path:
self.add_media_to_list(file_path)
def add_media_to_list(self, file_path):
"""Add media file to the list"""
if file_path not in self.media_files:
self.media_files.append(file_path)
# Create media item widget
media_item = QPushButton(f"🎬 {os.path.basename(file_path)}")
media_item.setStyleSheet("""
QPushButton {
text-align: left;
padding: 4px 6px;
margin: 0px;
border: 1px solid #404040;
border-radius: 3px;
background-color: #2d2d2d;
color: #ffffff;
min-height: 18px;
max-height: 26px;
font-size: 12px;
}
QPushButton:hover {
background-color: #404040;
border: 1px solid #00aaff;
}
""")
media_item.clicked.connect(lambda: self.select_media(file_path))
# Insert before the stretch to keep items at the top
self.media_list_layout.insertWidget(self.media_list_layout.count() - 1, media_item)
print(f"📁 Added to media bin: {os.path.basename(file_path)}")
def select_media(self, file_path):
"""Select media file"""
self.mediaSelected.emit(file_path)
print(f"🎬 Selected: {os.path.basename(file_path)}")
class ProfessionalVideoEditor(QMainWindow):
"""Professional video editor with PyQt6"""
def __init__(self):
super().__init__()
# Video state
self.current_video = None
self.current_time = 0.0
self.video_duration = 0.0
self.is_playing = False
self.current_frame = None
# Effects state
self.effects_enabled = {
'ripple': False,
'fade': False,
'text': False
}
self.effect_times = {
'ripple': {'start': 0.0, 'end': 2.0},
'fade': {'start': 0.0, 'end': 3.0},
'text': {'start': 0.0, 'end': 4.0}
}
self.effect_params = {}
# Fullscreen state
self.is_fullscreen = False
self.fullscreen_window = None
self.setup_ui()
self.setup_media_player()
self.setup_shortcuts()
self.setup_styling()
def setup_ui(self):
"""Setup the main professional video editor interface"""
self.setWindowTitle("Professional Video Editor - PyQt6")
self.setGeometry(100, 100, 1400, 900)
self.setMinimumSize(1000, 700)
# Central widget with splitter layout
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QHBoxLayout(central_widget)
main_layout.setContentsMargins(5, 5, 5, 5)
# Main splitter
main_splitter = QSplitter(Qt.Orientation.Horizontal)
main_layout.addWidget(main_splitter)
# Left panel (Media Bin)
left_panel = QWidget()
left_panel.setFixedWidth(250)
left_layout = QVBoxLayout(left_panel)
self.media_bin = MediaBin()
self.media_bin.mediaSelected.connect(self.load_video_file)
left_layout.addWidget(self.media_bin)
main_splitter.addWidget(left_panel)
# Center panel (Video Player + Timeline)
center_panel = QWidget()
center_layout = QVBoxLayout(center_panel)
# Video player area
video_frame = QFrame()
video_frame.setMinimumHeight(480) # 400 for video + 60 for controls + some padding
video_frame.setStyleSheet("""
QFrame {
background-color: #2d2d2d;
border: 2px solid #404040;
border-radius: 8px;
}
""")
video_layout = QVBoxLayout(video_frame)
video_layout.setContentsMargins(5, 5, 5, 5) # Add some padding
# Adaptive video container that matches video aspect ratio
video_container = QWidget()
video_container.setStyleSheet("""
QWidget {
background-color: #2d2d2d;
border: 2px solid #555555;
border-radius: 8px;
}
""")
video_container_layout = QVBoxLayout(video_container)
video_container_layout.setContentsMargins(0, 0, 0, 0)
video_container_layout.setSpacing(0)
# Adaptive video widget with intelligent sizing
self.video_widget = QVideoWidget()
# Set default size for 16:9 aspect ratio (1080p scaled down)
default_width = 640 # Standard width for video player
default_height = int(default_width * 9 / 16) # 16:9 aspect ratio = 360
self.video_widget.setMinimumSize(default_width, default_height)
self.video_widget.setMaximumHeight(600) # Limit maximum height
self.video_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
# Keep aspect ratio to prevent distortion, but adapt container size
self.video_widget.setAspectRatioMode(Qt.AspectRatioMode.KeepAspectRatio)
self.video_widget.setStyleSheet("""
QVideoWidget {
border: none;
border-radius: 6px;
background-color: #2d2d2d;
}
""")
# Add video widget with top alignment to push it up
video_container_layout.addWidget(self.video_widget, 0, Qt.AlignmentFlag.AlignTop)
# Add stretch to push video to top and leave space at bottom
video_container_layout.addStretch()
# Store references for dynamic sizing
self.video_container = video_container
print("📺 Clean video widget initialized (no overlay)")
# Add placeholder label for when no video is loaded
self.video_placeholder = QLabel("🎬 Select a video from Media Bin to start editing")
self.video_placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.video_placeholder.setStyleSheet("""
QLabel {
color: #888888;
font-size: 16px;
font-weight: bold;
background-color: transparent;
padding: 20px;
}
""")
# Create a stacked widget to switch between placeholder and video container
self.video_stack = QStackedWidget()
self.video_stack.addWidget(self.video_placeholder) # Index 0
self.video_stack.addWidget(self.video_container) # Index 1 - use the stored container
self.video_stack.setCurrentIndex(0) # Start with placeholder
video_layout.addWidget(self.video_stack)
# Set object name for easy finding later
video_frame.setObjectName("video_frame")
center_layout.addWidget(video_frame, 1)
# Timeline and playback controls
timeline_frame = QFrame()
timeline_frame.setFixedHeight(500) # Much larger to accommodate all tracks properly
timeline_frame.setStyleSheet("""
QFrame {
background-color: #1e1e1e;
border: 1px solid #404040;
border-radius: 4px;
}
""")
timeline_layout = QVBoxLayout(timeline_frame)
# Playback controls
playback_controls = QHBoxLayout()
# Time display
self.time_label = QLabel("00:00 / 00:00")
self.time_label.setStyleSheet("color: #ffffff; font-weight: bold;")
playback_controls.addWidget(self.time_label)
playback_controls.addStretch()
# Play/Pause button
self.play_pause_btn = QPushButton("")
self.play_pause_btn.clicked.connect(self.toggle_playback)
self.play_pause_btn.setStyleSheet("""
QPushButton {
background-color: #00aa00;
color: white;
border: none;
padding: 10px 15px;
border-radius: 6px;
font-size: 16px;
font-weight: bold;
min-width: 50px;
}
QPushButton:hover {
background-color: #00cc00;
}
""")
playback_controls.addWidget(self.play_pause_btn)
# Add some spacing between buttons
playback_controls.addSpacing(10)
# Fullscreen button - Add next to the play button in timeline
self.timeline_fullscreen_btn = QPushButton("⛶ Fullscreen")
self.timeline_fullscreen_btn.setToolTip("Fullscreen (F11)")
self.timeline_fullscreen_btn.clicked.connect(self.toggle_fullscreen)
self.timeline_fullscreen_btn.setStyleSheet("""
QPushButton {
background-color: #0066cc;
color: white;
border: none;
padding: 10px 15px;
border-radius: 6px;
font-size: 14px;
font-weight: bold;
min-width: 100px;
}
QPushButton:hover {
background-color: #0088ff;
}
QPushButton:pressed {
background-color: #004499;
}
""")
playback_controls.addWidget(self.timeline_fullscreen_btn)
print("🎮 Added fullscreen button to timeline controls")
playback_controls.addStretch()
timeline_layout.addLayout(playback_controls)
# Timeline slider
timeline_slider_layout = QHBoxLayout()
self.timeline_slider = ProfessionalSlider()
self.timeline_slider.setRange(0, 1000)
self.timeline_slider.sliderPressed.connect(self.on_timeline_pressed)
self.timeline_slider.sliderReleased.connect(self.on_timeline_released)
self.timeline_slider.valueChanged.connect(self.on_timeline_changed)
timeline_slider_layout.addWidget(self.timeline_slider, 1)
timeline_layout.addLayout(timeline_slider_layout)
# Professional timeline widget
self.timeline_widget = TimelineWidget()
self.timeline_widget.positionChanged.connect(self.seek_to_time)
timeline_layout.addWidget(self.timeline_widget, 1)
center_layout.addWidget(timeline_frame)
main_splitter.addWidget(center_panel)
# Right panel (Tabbed Tools)
right_panel = QWidget()
right_panel.setFixedWidth(300)
right_layout = QVBoxLayout(right_panel)
# Create tabbed interface for tools
self.tool_tabs = QTabWidget()
self.tool_tabs.setStyleSheet("""
QTabWidget::pane {
border: 1px solid #404040;
background-color: #2d2d2d;
}
QTabWidget::tab-bar {
alignment: center;
}
QTabBar::tab {
background-color: #404040;
color: #ffffff;
padding: 8px 12px;
margin: 2px;
border-radius: 4px;
}
QTabBar::tab:selected {
background-color: #00aaff;
color: #ffffff;
font-weight: bold;
}
QTabBar::tab:hover {
background-color: #606060;
}
""")
# Effects tab
self.effects_panel = EffectsPanel()
self.effects_panel.effectApplied.connect(self.apply_effect)
self.tool_tabs.addTab(self.effects_panel, "🎨 Effects")
# Basic editing tab
basic_edit_panel = self.create_basic_edit_panel()
self.tool_tabs.addTab(basic_edit_panel, "✂️ Edit")
# Export tab
export_panel = self.create_export_panel()
self.tool_tabs.addTab(export_panel, "📤 Export")
right_layout.addWidget(self.tool_tabs)
main_splitter.addWidget(right_panel)
# Set splitter proportions
main_splitter.setSizes([250, 850, 300])
# Status bar
self.statusBar().showMessage("Ready - Select a video from the media bin to start")
def setup_media_player(self):
"""Setup PyQt6 media player"""
self.media_player = QMediaPlayer()
self.audio_output = QAudioOutput()
self.media_player.setVideoOutput(self.video_widget)
self.media_player.setAudioOutput(self.audio_output)
# Connect signals
self.media_player.positionChanged.connect(self.update_position)
self.media_player.durationChanged.connect(self.update_duration)
self.media_player.playbackStateChanged.connect(self.update_playback_state)
self.media_player.errorOccurred.connect(self.handle_media_error)
# Connect video output changed to get video dimensions
self.media_player.metaDataChanged.connect(self.update_video_info)
# Position update timer
self.position_timer = QTimer()
self.position_timer.timeout.connect(self.update_timeline_position)
self.position_timer.start(50) # 20 FPS updates
self.timeline_dragging = False
def setup_shortcuts(self):
"""Setup keyboard shortcuts"""
# Spacebar - Play/Pause
space_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Space), self)
space_shortcut.activated.connect(self.toggle_playback)
# Left/Right arrows
left_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Left), self)
left_shortcut.activated.connect(self.frame_backward)
right_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Right), self)
right_shortcut.activated.connect(self.frame_forward)
# F11 - Fullscreen
f11_shortcut = QShortcut(QKeySequence(Qt.Key.Key_F11), self)
f11_shortcut.activated.connect(self.toggle_fullscreen)
# ESC - Exit fullscreen
esc_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Escape), self)
esc_shortcut.activated.connect(self.exit_fullscreen)
print("⌨️ Keyboard shortcuts enabled:")
print(" SPACE: Play/Pause")
print(" ←/→: Frame navigation")
print(" F11: Toggle fullscreen")
print(" ESC: Exit fullscreen")
def resizeEvent(self, event):
"""Handle window resize"""
super().resizeEvent(event)
def setup_styling(self):
"""Apply professional dark theme"""
self.setStyleSheet("""
QMainWindow {
background-color: #1e1e1e;
color: #ffffff;
}
QWidget {
background-color: #1e1e1e;
color: #ffffff;
}
QLabel {
color: #ffffff;
}
QGroupBox {
background-color: #2d2d2d;
}
QLineEdit, QSpinBox, QDoubleSpinBox {
background-color: #404040;
border: 1px solid #666666;
border-radius: 3px;
padding: 5px;
color: #ffffff;
}
QLineEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus {
border: 2px solid #00aaff;
}
""")
def create_basic_edit_panel(self):
"""Create basic editing tools panel"""
panel = QWidget()
layout = QVBoxLayout(panel)
# Trim section
trim_group = QGroupBox("✂️ Trim Video")
trim_group.setStyleSheet("""
QGroupBox {
font-weight: bold;
border: 2px solid #404040;
border-radius: 5px;
margin-top: 10px;
color: #ffffff;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
}
""")
trim_layout = QVBoxLayout(trim_group)
# Trim controls
trim_controls = QHBoxLayout()
trim_controls.addWidget(QLabel("Start:"))
self.trim_start = QDoubleSpinBox()
self.trim_start.setRange(0.0, 999.0)
self.trim_start.setSingleStep(0.1)
self.trim_start.setSuffix("s")
trim_controls.addWidget(self.trim_start)
trim_controls.addWidget(QLabel("End:"))
self.trim_end = QDoubleSpinBox()
self.trim_end.setRange(0.0, 999.0)
self.trim_end.setSingleStep(0.1)
self.trim_end.setValue(10.0)
self.trim_end.setSuffix("s")
trim_controls.addWidget(self.trim_end)
trim_layout.addLayout(trim_controls)
trim_btn = QPushButton("Apply Trim")
trim_btn.clicked.connect(self.apply_trim)
trim_btn.setStyleSheet("""
QPushButton {
background-color: #cc6600;
color: white;
border: none;
padding: 8px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #ff8800;
}
""")
trim_layout.addWidget(trim_btn)
layout.addWidget(trim_group)
# Speed section
speed_group = QGroupBox("⚡ Speed Control")
speed_group.setStyleSheet(trim_group.styleSheet())
speed_layout = QVBoxLayout(speed_group)
speed_controls = QHBoxLayout()
speed_controls.addWidget(QLabel("Speed:"))
self.speed_factor = QDoubleSpinBox()
self.speed_factor.setRange(0.1, 10.0)
self.speed_factor.setSingleStep(0.1)
self.speed_factor.setValue(1.0)
self.speed_factor.setSuffix("x")
speed_controls.addWidget(self.speed_factor)
speed_layout.addLayout(speed_controls)
speed_btn = QPushButton("Apply Speed")
speed_btn.clicked.connect(self.apply_speed)
speed_btn.setStyleSheet(trim_btn.styleSheet())
speed_layout.addWidget(speed_btn)
layout.addWidget(speed_group)
# Volume section
volume_group = QGroupBox("🔊 Volume Control")
volume_group.setStyleSheet(trim_group.styleSheet())
volume_layout = QVBoxLayout(volume_group)
volume_controls = QHBoxLayout()
volume_controls.addWidget(QLabel("Volume:"))
self.volume_factor = QDoubleSpinBox()
self.volume_factor.setRange(0.0, 5.0)
self.volume_factor.setSingleStep(0.1)
self.volume_factor.setValue(1.0)
self.volume_factor.setSuffix("x")
volume_controls.addWidget(self.volume_factor)
volume_layout.addLayout(volume_controls)
volume_btn = QPushButton("Apply Volume")
volume_btn.clicked.connect(self.apply_volume)
volume_btn.setStyleSheet(trim_btn.styleSheet())
volume_layout.addWidget(volume_btn)
layout.addWidget(volume_group)
layout.addStretch()
return panel
def create_export_panel(self):
"""Create export tools panel"""
panel = QWidget()
layout = QVBoxLayout(panel)
# Export settings
export_group = QGroupBox("📤 Export Settings")
export_group.setStyleSheet("""
QGroupBox {
font-weight: bold;
border: 2px solid #404040;
border-radius: 5px;
margin-top: 10px;
color: #ffffff;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
}
""")
export_layout = QVBoxLayout(export_group)
# Output filename
filename_layout = QHBoxLayout()
filename_layout.addWidget(QLabel("Filename:"))
self.export_filename = QLineEdit()
self.export_filename.setText("edited_video.mp4")
filename_layout.addWidget(self.export_filename)
export_layout.addLayout(filename_layout)
# Quality settings
quality_layout = QHBoxLayout()
quality_layout.addWidget(QLabel("Quality:"))
self.export_quality = QComboBox()
self.export_quality.addItems(["High", "Medium", "Low"])
quality_layout.addWidget(self.export_quality)
export_layout.addLayout(quality_layout)
# Export button
export_btn = QPushButton("🎬 Export Video")
export_btn.clicked.connect(self.export_video)
export_btn.setStyleSheet("""
QPushButton {
background-color: #006600;
color: white;
border: none;
padding: 12px;
border-radius: 6px;
font-weight: bold;
font-size: 14px;
}
QPushButton:hover {
background-color: #008800;
}
""")
export_layout.addWidget(export_btn)
layout.addWidget(export_group)
# Export progress
self.export_progress = QProgressBar()
self.export_progress.setVisible(False)
layout.addWidget(self.export_progress)
layout.addStretch()
return panel
def apply_trim(self):
"""Apply trim effect"""
QMessageBox.information(self, "Trim Applied",
f"Trim applied: {self.trim_start.value():.1f}s to {self.trim_end.value():.1f}s")
def apply_speed(self):
"""Apply speed effect"""
QMessageBox.information(self, "Speed Applied",
f"Speed changed to {self.speed_factor.value():.1f}x")
def apply_volume(self):
"""Apply volume effect"""
QMessageBox.information(self, "Volume Applied",
f"Volume changed to {self.volume_factor.value():.1f}x")
def export_video(self):
"""Export the edited video"""
if not self.current_video:
QMessageBox.warning(self, "No Video", "Please load a video first.")
return
filename = self.export_filename.text()
if not filename.endswith('.mp4'):
filename += '.mp4'
QMessageBox.information(self, "Export Started",
f"Video export started: {filename}\n"
f"Quality: {self.export_quality.currentText()}")
def setup_styling(self):
"""Apply professional dark theme"""
self.setStyleSheet("""
QMainWindow {
background-color: #1e1e1e;
color: #ffffff;
}
QWidget {
background-color: #1e1e1e;
color: #ffffff;
}
QLabel {
color: #ffffff;
}
QGroupBox {
background-color: #2d2d2d;
}
QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox {
background-color: #404040;
border: 1px solid #666666;
border-radius: 3px;
padding: 5px;
color: #ffffff;
}
QLineEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus, QComboBox:focus {
border: 2px solid #00aaff;
}
QComboBox::drop-down {
border: none;
}
QComboBox::down-arrow {
image: none;
border: none;
}
QProgressBar {
border: 1px solid #404040;
border-radius: 3px;
text-align: center;
background-color: #2d2d2d;
}
QProgressBar::chunk {
background-color: #00aaff;
border-radius: 2px;
}
""")
def update_video_info(self):
"""Update video player size based on loaded video dimensions"""
try:
# Get video metadata
if self.media_player.metaData():
video_size = self.media_player.metaData().get('Resolution')
if video_size and hasattr(video_size, 'width') and hasattr(video_size, 'height'):
self.adapt_video_player_size(video_size.width(), video_size.height())
return
# Fallback: Try to get size from video widget after a short delay
QTimer.singleShot(1000, self.try_get_video_size_from_widget)
except Exception as e:
print(f"⚠️ Could not get video dimensions: {e}")
# Use default 16:9 sizing
self.adapt_video_player_size(1920, 1080)
def try_get_video_size_from_widget(self):
"""Fallback method to get video size"""
try:
# Try to get size from video widget
widget_size = self.video_widget.videoSize()
if widget_size and widget_size.width() > 0 and widget_size.height() > 0:
self.adapt_video_player_size(widget_size.width(), widget_size.height())
print(f"📐 Video size detected from widget: {widget_size.width()}x{widget_size.height()}")
else:
# Default to 16:9 if we can't detect
self.adapt_video_player_size(1920, 1080)
print("📐 Using default 16:9 aspect ratio")
except Exception as e:
print(f"⚠️ Fallback size detection failed: {e}")
self.adapt_video_player_size(1920, 1080)
def adapt_video_player_size(self, video_width, video_height):
"""Use consistent size matching the placeholder - no dynamic scaling"""
try:
# Use the same size as the default video widget for consistency
# This matches the size set in the initialization (640x360)
default_width = 795 # Same as initial video widget size
default_height = 420 # Same as initial video widget size (16:9 ratio)
# Always use the same size regardless of video dimensions
# This ensures consistent UI layout and eliminates black bar issues
display_width = default_width
display_height = default_height
# Update video widget size to match default
self.video_widget.setMinimumSize(display_width, display_height)
self.video_widget.setMaximumSize(display_width, display_height) # Fixed size
# Force layout update
self.video_container.updateGeometry()
self.video_stack.updateGeometry()
print(f"📐 Using consistent video player size: {display_width}x{display_height} (original: {video_width}x{video_height})")
except Exception as e:
print(f"⚠️ Error setting video player size: {e}")
def load_video_file(self, file_path):
"""Load video file"""
try:
self.current_video = file_path
url = QUrl.fromLocalFile(file_path)
self.media_player.setSource(url)
# Switch from placeholder to video widget
self.video_stack.setCurrentIndex(1)
# Enable controls
self.timeline_slider.setEnabled(True)
self.play_pause_btn.setEnabled(True)
self.timeline_fullscreen_btn.setEnabled(True)
self.statusBar().showMessage(f"✅ Loaded: {os.path.basename(file_path)}")
print(f"✅ Loaded video: {os.path.basename(file_path)}")
except Exception as e:
self.statusBar().showMessage(f"❌ Error: {e}")
QMessageBox.critical(self, "Error", f"Failed to load video:\n{e}")
def toggle_playback(self):
"""Toggle play/pause"""
if self.media_player.playbackState() == QMediaPlayer.PlaybackState.PlayingState:
self.media_player.pause()
# Update timeline play button
self.play_pause_btn.setText("")
# Update integrated button text if it exists
if hasattr(self, 'integrated_play_btn'):
self.integrated_play_btn.setText("▶ Play")
else:
self.media_player.play()
# Update timeline play button
self.play_pause_btn.setText("")
# Update integrated button text if it exists
if hasattr(self, 'integrated_play_btn'):
self.integrated_play_btn.setText("⏸ Pause")
def frame_backward(self):
"""Go back one frame"""
current_pos = self.media_player.position()
frame_duration = 1000 // 30 # ~33ms for 30fps
new_pos = max(0, current_pos - frame_duration)
self.media_player.setPosition(new_pos)
def frame_forward(self):
"""Go forward one frame"""
current_pos = self.media_player.position()
frame_duration = 1000 // 30 # ~33ms for 30fps
new_pos = min(self.media_player.duration(), current_pos + frame_duration)
self.media_player.setPosition(new_pos)
def seek_to_time(self, time_seconds):
"""Seek to specific time"""
position_ms = int(time_seconds * 1000)
self.media_player.setPosition(position_ms)
def toggle_fullscreen(self):
"""Toggle fullscreen mode"""
if self.is_fullscreen:
self.exit_fullscreen()
else:
self.enter_fullscreen()
def enter_fullscreen(self):
"""Enter fullscreen mode with improved widget handling"""
try:
self.is_fullscreen = True
# Create fullscreen window
self.fullscreen_window = QWidget()
self.fullscreen_window.setWindowTitle("Video Fullscreen")
self.fullscreen_window.setStyleSheet("background-color: black;")
self.fullscreen_window.showFullScreen()
# Create a new video widget for fullscreen (don't move the original)
self.fullscreen_video_widget = QVideoWidget()
self.media_player.setVideoOutput(self.fullscreen_video_widget)
fullscreen_layout = QVBoxLayout(self.fullscreen_window)
fullscreen_layout.setContentsMargins(0, 0, 0, 0)
fullscreen_layout.addWidget(self.fullscreen_video_widget)
# Add exit button overlay
exit_btn = QPushButton("✕ Exit Fullscreen (ESC)")
exit_btn.clicked.connect(self.exit_fullscreen)
exit_btn.setStyleSheet("""
QPushButton {
background-color: rgba(51, 51, 51, 200);
color: white;
border: none;
padding: 15px 20px;
border-radius: 8px;
font-weight: bold;
font-size: 14px;
}
QPushButton:hover {
background-color: rgba(77, 77, 77, 220);
}
""")
exit_btn.setFixedSize(200, 50)
exit_btn.move(30, 30)
exit_btn.setParent(self.fullscreen_window)
exit_btn.show()
self.fullscreen_window.setFocus()
self.timeline_fullscreen_btn.setText("🗗 Exit FS")
# Setup fullscreen shortcuts
esc_fullscreen = QShortcut(QKeySequence(Qt.Key.Key_Escape), self.fullscreen_window)
esc_fullscreen.activated.connect(self.exit_fullscreen)
space_fullscreen = QShortcut(QKeySequence(Qt.Key.Key_Space), self.fullscreen_window)
space_fullscreen.activated.connect(self.toggle_playback)
print("🔳 Entered fullscreen mode successfully")
except Exception as e:
print(f"⚠️ Error entering fullscreen: {e}")
self.is_fullscreen = False
def exit_fullscreen(self):
"""Exit fullscreen mode with improved widget handling"""
if self.is_fullscreen and self.fullscreen_window:
try:
self.is_fullscreen = False
# Restore video output to original widget
self.media_player.setVideoOutput(self.video_widget)
# Clean up fullscreen window and video widget
if hasattr(self, 'fullscreen_video_widget'):
self.fullscreen_video_widget.setParent(None)
self.fullscreen_video_widget = None
self.fullscreen_window.close()
self.fullscreen_window = None
self.timeline_fullscreen_btn.setText("⛶ Fullscreen")
print("🔲 Exited fullscreen mode successfully")
except Exception as e:
print(f"⚠️ Error exiting fullscreen: {e}")
self.is_fullscreen = False
self.fullscreen_window = None
def restore_video_widget(self):
"""Safely restore video widget to original location"""
try:
# Find the video frame container
for widget in self.centralWidget().findChildren(QFrame):
if widget.objectName() == "video_frame":
video_layout = widget.layout()
if video_layout:
video_layout.insertWidget(0, self.video_widget)
print("📺 Video widget restored to original position")
return
# Fallback: recreate the video layout structure
self.setup_video_container()
except Exception as e:
print(f"⚠️ Error restoring video widget: {e}")
def setup_video_container(self):
"""Setup or recreate video container structure"""
try:
# Find the center panel and recreate video structure if needed
center_widget = None
for widget in self.centralWidget().findChildren(QWidget):
if hasattr(widget, 'layout') and widget.layout():
layout = widget.layout()
if layout.count() > 0:
center_widget = widget
break
if center_widget:
# Create new video frame
video_frame = QFrame()
video_frame.setObjectName("video_frame")
video_frame.setStyleSheet("""
QFrame {
background-color: #000000;
border: 2px solid #404040;
border-radius: 8px;
}
""")
video_layout = QVBoxLayout(video_frame)
video_layout.addWidget(self.video_widget)
# Add back to center layout
center_layout = center_widget.layout()
center_layout.insertWidget(0, video_frame)
print("🔧 Video container recreated successfully")
except Exception as e:
print(f"⚠️ Error setting up video container: {e}")
def apply_effect(self, effect_type, params):
"""Apply effect with parameters"""
self.effects_enabled[effect_type] = True
self.effect_times[effect_type] = {
'start': params['start_time'],
'end': params['end_time']
}
self.effect_params[effect_type] = params
QMessageBox.information(
self,
"Effect Applied",
f"{effect_type.title()} effect applied from {params['start_time']:.1f}s to {params['end_time']:.1f}s"
)
print(f"🎨 Applied {effect_type} effect: {params}")
def on_timeline_pressed(self):
"""Timeline pressed"""
self.timeline_dragging = True
def on_timeline_released(self):
"""Timeline released"""
self.timeline_dragging = False
if self.media_player.duration() > 0:
position = (self.timeline_slider.value() / 1000.0) * self.media_player.duration()
self.media_player.setPosition(int(position))
def on_timeline_changed(self, value):
"""Timeline value changed"""
if self.timeline_dragging and self.media_player.duration() > 0:
position = (value / 1000.0) * self.media_player.duration()
self.current_time = position / 1000.0
self.update_time_display()
def update_position(self, position):
"""Update position from media player"""
self.current_time = position / 1000.0
# Update timeline slider
if not self.timeline_dragging and self.media_player.duration() > 0:
value = (position / self.media_player.duration()) * 1000
self.timeline_slider.setValue(int(value))
# Update timeline widget
self.timeline_widget.set_position(self.current_time)
# Update time display
self.update_time_display()
# Debug output for time tracking
if int(self.current_time) % 5 == 0: # Every 5 seconds
print(f"⏱️ Time: {self.format_time(self.current_time)} / {self.format_time(self.video_duration)}")
def update_duration(self, duration):
"""Update duration"""
self.video_duration = duration / 1000.0
self.timeline_widget.set_duration(self.video_duration)
self.update_time_display()
print(f"📏 Video duration set: {self.format_time(self.video_duration)}")
def update_timeline_position(self):
"""High-frequency timeline updates"""
if not self.timeline_dragging and self.media_player.duration() > 0:
position = self.media_player.position()
# Only update if position actually changed
if abs(position - (self.current_time * 1000)) > 100: # 100ms threshold
self.update_position(position)
def update_time_display(self):
"""Update time display"""
current = self.format_time(self.current_time)
total = self.format_time(self.video_duration)
self.time_label.setText(f"{current} / {total}")
def update_playback_state(self, state):
"""Update play/pause button"""
if state == QMediaPlayer.PlaybackState.PlayingState:
self.play_pause_btn.setText("")
self.is_playing = True
else:
self.play_pause_btn.setText("")
self.is_playing = False
def handle_media_error(self, error):
"""Handle media errors"""
error_msg = f"Media error: {error}"
self.statusBar().showMessage(f"{error_msg}")
QMessageBox.critical(self, "Media Error", error_msg)
def format_time(self, seconds):
"""Format time as mm:ss"""
minutes = int(seconds // 60)
seconds = int(seconds % 60)
return f"{minutes:02d}:{seconds:02d}"
def main():
"""Run the PyQt6 professional video editor"""
app = QApplication(sys.argv)
# Set application properties
app.setApplicationName("Professional Video Editor - PyQt6")
app.setApplicationVersion("2.0")
# Apply fusion style for modern look
app.setStyle(QStyleFactory.create('Fusion'))
# Create and show editor
editor = ProfessionalVideoEditor()
editor.show()
print("🎬 Professional Video Editor (PyQt6) Started!")
print("🚀 PyQt6 Features:")
print(" • Hardware-accelerated video playback")
print(" • Professional timeline with visual tracks")
print(" • Real-time effects system")
print(" • Modern docking interface")
print(" • GPU-accelerated rendering")
print(" • Professional keyboard shortcuts")
sys.exit(app.exec())
if __name__ == "__main__":
main()