This commit is contained in:
stirelshka8_BigARM 2025-11-26 16:06:40 +03:00
parent 3f06e4f797
commit 41beee6283
7 changed files with 459 additions and 0 deletions

35
core/logger.py Normal file
View File

@ -0,0 +1,35 @@
import logging
import os
from datetime import datetime
class Logger:
def __init__(self):
self.log_messages = []
def log(self, level, message):
"""Добавление сообщения в лог"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_entry = f"[{timestamp}] [{level}] {message}"
self.log_messages.append(log_entry)
print(log_entry) # Также выводим в консоль
def save_log(self, file_path):
"""Сохранение лога в файл"""
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write("VIDEO EDITOR PROCESSING LOG\n")
f.write("=" * 50 + "\n")
f.write(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write("=" * 50 + "\n\n")
for log_entry in self.log_messages:
f.write(log_entry + "\n")
self.log("INFO", f"Лог сохранен: {file_path}")
except Exception as e:
self.log("ERROR", f"Ошибка сохранения лога: {str(e)}")
def clear_log(self):
"""Очистка лога"""
self.log_messages.clear()

111
core/video_processor.py Normal file
View File

@ -0,0 +1,111 @@
import os
from moviepy.editor import VideoFileClip, concatenate_videoclips
import tempfile
from datetime import datetime
from core.logger import Logger
class VideoProcessor:
def __init__(self):
self.logger = Logger()
def process_videos(self, video_data, output_dir, output_format="mp4",
quality="high", progress_callback=None):
"""
Основной метод обработки видео
"""
self.logger.log("INFO", f"Начало обработки {len(video_data)} видео файлов")
clips = []
total_steps = sum(len(data['time_ranges']) for data in video_data) + 2
current_step = 0
try:
# Обработка каждого видео файла
for i, data in enumerate(video_data):
video_path = data['path']
time_ranges = data['time_ranges']
self.logger.log("INFO", f"Обработка файла: {os.path.basename(video_path)}")
with VideoFileClip(video_path) as video:
# Вырезаем указанные диапазоны
for start, end in time_ranges:
if progress_callback:
progress = current_step / total_steps
progress_callback(progress,
f"Вырезание фрагмента {i + 1}/{len(video_data)}")
# Обрезаем видео по таймкодам
clip = video.subclip(start, end)
clips.append(clip)
current_step += 1
self.logger.log("INFO",
f"Вырезан фрагмент: {self.format_time(start)} - {self.format_time(end)}")
if not clips:
raise ValueError("Нет видео фрагментов для объединения")
# Объединение клипов
if progress_callback:
progress_callback(current_step / total_steps, "Объединение видео фрагментов")
self.logger.log("INFO", f"Объединение {len(clips)} фрагментов")
final_clip = concatenate_videoclips(clips)
# Настройка качества
bitrate = self.get_bitrate(quality)
# Генерация имени выходного файла
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_filename = f"edited_video_{timestamp}.{output_format}"
output_path = os.path.join(output_dir, output_filename)
# Сохранение результата
if progress_callback:
progress_callback((total_steps - 1) / total_steps, "Сохранение итогового файла")
self.logger.log("INFO", f"Сохранение файла: {output_path}")
final_clip.write_videofile(
output_path,
codec='libx264',
bitrate=bitrate,
audio_codec='aac',
verbose=False,
logger=None
)
# Закрытие клипов
final_clip.close()
for clip in clips:
clip.close()
self.logger.log("SUCCESS", f"Обработка завершена успешно: {output_path}")
return output_path
except Exception as e:
self.logger.log("ERROR", f"Ошибка обработки: {str(e)}")
# Закрытие клипов в случае ошибки
for clip in clips:
try:
clip.close()
except:
pass
raise
def get_bitrate(self, quality):
"""Определение битрейта в зависимости от качества"""
quality_settings = {
'low': '1000k',
'medium': '3000k',
'high': '8000k'
}
return quality_settings.get(quality, '3000k')
def format_time(self, seconds):
"""Форматирование времени в читаемый вид"""
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
seconds = int(seconds % 60)
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"

167
gui/main_window.py Normal file
View File

@ -0,0 +1,167 @@
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import os
from gui.video_item import VideoItem
from core.video_processor import VideoProcessor
from core.logger import Logger
class MainWindow:
def __init__(self, root):
self.root = root
self.root.title("Video Editor Pro")
self.root.geometry("900x700")
self.video_items = []
self.setup_ui()
self.logger = Logger()
def setup_ui(self):
# Main frame
main_frame = ttk.Frame(self.root, padding="10")
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# Add files section
files_frame = ttk.LabelFrame(main_frame, text="Видео файлы", padding="5")
files_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
ttk.Button(files_frame, text="Добавить файлы",
command=self.add_video_files).pack(side=tk.LEFT, padx=(0, 10))
ttk.Button(files_frame, text="Очистить все",
command=self.clear_all).pack(side=tk.LEFT)
# Scrollable frame for video items
canvas = tk.Canvas(main_frame)
scrollbar = ttk.Scrollbar(main_frame, orient="vertical", command=canvas.yview)
self.scrollable_frame = ttk.Frame(canvas)
self.scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
canvas.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
scrollbar.grid(row=1, column=1, sticky=(tk.N, tk.S))
# Output settings
output_frame = ttk.LabelFrame(main_frame, text="Настройки вывода", padding="5")
output_frame.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(10, 0))
ttk.Label(output_frame, text="Формат:").grid(row=0, column=0, padx=(0, 5))
self.format_var = tk.StringVar(value="mp4")
format_combo = ttk.Combobox(output_frame, textvariable=self.format_var,
values=["mp4", "avi", "mov", "mkv"], state="readonly")
format_combo.grid(row=0, column=1, padx=(0, 20))
ttk.Label(output_frame, text="Качество:").grid(row=0, column=2, padx=(0, 5))
self.quality_var = tk.StringVar(value="high")
quality_combo = ttk.Combobox(output_frame, textvariable=self.quality_var,
values=["low", "medium", "high"], state="readonly")
quality_combo.grid(row=0, column=3, padx=(0, 20))
ttk.Button(output_frame, text="Выбрать папку для сохранения",
command=self.select_output_dir).grid(row=0, column=4, padx=(0, 10))
self.output_dir_var = tk.StringVar(value=os.getcwd())
ttk.Label(output_frame, textvariable=self.output_dir_var).grid(row=0, column=5)
# Process button
ttk.Button(main_frame, text="Выполнить",
command=self.process_videos, style="Accent.TButton").grid(row=3, column=0, pady=20)
# Progress bar
self.progress_var = tk.DoubleVar()
self.progress_bar = ttk.Progressbar(main_frame, variable=self.progress_var, maximum=100)
self.progress_bar.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E))
# Status label
self.status_var = tk.StringVar(value="Готов к работе")
ttk.Label(main_frame, textvariable=self.status_var).grid(row=5, column=0, columnspan=2)
# Configure grid weights
main_frame.columnconfigure(0, weight=1)
main_frame.rowconfigure(1, weight=1)
def add_video_files(self):
files = filedialog.askopenfilenames(
title="Выберите видео файлы",
filetypes=[
("Видео файлы", "*.mp4 *.avi *.mov *.mkv *.wmv *.flv *.webm"),
("Все файлы", "*.*")
]
)
for file_path in files:
video_item = VideoItem(self.scrollable_frame, file_path)
video_item.pack(fill=tk.X, padx=5, pady=2)
self.video_items.append(video_item)
self.update_status(f"Добавлено файлов: {len(files)}")
def clear_all(self):
for item in self.video_items:
item.destroy()
self.video_items.clear()
self.update_status("Все файлы удалены")
def select_output_dir(self):
directory = filedialog.askdirectory(title="Выберите папку для сохранения")
if directory:
self.output_dir_var.set(directory)
def update_status(self, message):
self.status_var.set(message)
self.root.update()
def process_videos(self):
if not self.video_items:
messagebox.showerror("Ошибка", "Добавьте хотя бы один видео файл")
return
# Collect video data
video_data = []
for item in self.video_items:
time_ranges = item.get_time_ranges()
if not time_ranges: # Use entire video if no ranges specified
time_ranges = [(0, item.get_duration())]
video_data.append({
'path': item.file_path,
'time_ranges': time_ranges
})
output_format = self.format_var.get()
output_quality = self.quality_var.get()
output_dir = self.output_dir_var.get()
try:
processor = VideoProcessor()
def progress_callback(progress, message):
self.progress_var.set(progress * 100)
self.update_status(message)
output_path = processor.process_videos(
video_data=video_data,
output_dir=output_dir,
output_format=output_format,
quality=output_quality,
progress_callback=progress_callback
)
# Save log
log_path = output_path.replace(f".{output_format}", "_log.txt")
self.logger.save_log(log_path)
self.update_status(f"Готово! Файл сохранен: {output_path}")
messagebox.showinfo("Успех", f"Обработка завершена!\nФайл: {output_path}")
except Exception as e:
error_msg = f"Ошибка обработки: {str(e)}"
self.update_status(error_msg)
self.logger.log("ERROR", error_msg)
messagebox.showerror("Ошибка", error_msg)
finally:
self.progress_var.set(0)

108
gui/video_item.py Normal file
View File

@ -0,0 +1,108 @@
import tkinter as tk
from tkinter import ttk
import os
from moviepy.editor import VideoFileClip
class VideoItem(ttk.Frame):
def __init__(self, parent, file_path):
super().__init__(parent)
self.file_path = file_path
self.time_ranges = []
self.setup_ui()
def setup_ui(self):
# File info
file_name = os.path.basename(self.file_path)
ttk.Label(self, text=file_name, font=('Arial', 9, 'bold')).grid(row=0, column=0, sticky=tk.W)
# Duration info
try:
with VideoFileClip(self.file_path) as clip:
duration = clip.duration
duration_str = self.format_time(duration)
ttk.Label(self, text=f"Длительность: {duration_str}").grid(row=1, column=0, sticky=tk.W)
except:
duration_str = "Неизвестно"
ttk.Label(self, text="Ошибка чтения файла").grid(row=1, column=0, sticky=tk.W)
# Time ranges frame
ranges_frame = ttk.LabelFrame(self, text="Временные диапазоны")
ranges_frame.grid(row=2, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
self.ranges_container = ttk.Frame(ranges_frame)
self.ranges_container.pack(fill=tk.X, padx=5, pady=5)
# Add range button
ttk.Button(ranges_frame, text="+ Добавить диапазон",
command=self.add_time_range).pack(pady=5)
# Buttons
ttk.Button(self, text="Удалить",
command=self.destroy).grid(row=0, column=2, rowspan=2, padx=5)
self.columnconfigure(0, weight=1)
def format_time(self, seconds):
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
seconds = int(seconds % 60)
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
def add_time_range(self, start_time=0, end_time=0):
range_frame = ttk.Frame(self.ranges_container)
range_frame.pack(fill=tk.X, pady=2)
ttk.Label(range_frame, text="От:").pack(side=tk.LEFT)
start_var = tk.StringVar(value=self.format_time(start_time))
start_entry = ttk.Entry(range_frame, textvariable=start_var, width=8)
start_entry.pack(side=tk.LEFT, padx=(0, 10))
ttk.Label(range_frame, text="До:").pack(side=tk.LEFT)
end_var = tk.StringVar(value=self.format_time(end_time))
end_entry = ttk.Entry(range_frame, textvariable=end_var, width=8)
end_entry.pack(side=tk.LEFT, padx=(0, 10))
def remove_range():
range_frame.destroy()
self.time_ranges = [r for r in self.time_ranges if r['frame'] != range_frame]
ttk.Button(range_frame, text="×", width=3,
command=remove_range).pack(side=tk.LEFT)
self.time_ranges.append({
'frame': range_frame,
'start_var': start_var,
'end_var': end_var
})
def get_time_ranges(self):
ranges = []
for range_data in self.time_ranges:
try:
start_time = self.parse_time(range_data['start_var'].get())
end_time = self.parse_time(range_data['end_var'].get())
if start_time < end_time:
ranges.append((start_time, end_time))
except:
continue
return ranges
def parse_time(self, time_str):
parts = time_str.split(':')
if len(parts) == 3: # HH:MM:SS
hours, minutes, seconds = map(int, parts)
return hours * 3600 + minutes * 60 + seconds
elif len(parts) == 2: # MM:SS
minutes, seconds = map(int, parts)
return minutes * 60 + seconds
else: # SS
return int(time_str)
def get_duration(self):
try:
with VideoFileClip(self.file_path) as clip:
return clip.duration
except:
return 0

10
main.py Normal file
View File

@ -0,0 +1,10 @@
import tkinter as tk
from gui.main_window import MainWindow
def main():
root = tk.Tk()
app = MainWindow(root)
root.mainloop()
if __name__ == "__main__":
main()

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
moviepy==1.0.3
Pillow==10.0.0

26
utils/file_utils.py Normal file
View File

@ -0,0 +1,26 @@
import os
import shutil
def get_file_size(file_path):
"""Получение размера файла в читаемом формате"""
size_bytes = os.path.getsize(file_path)
for unit in ['B', 'KB', 'MB', 'GB']:
if size_bytes < 1024.0:
return f"{size_bytes:.2f} {unit}"
size_bytes /= 1024.0
return f"{size_bytes:.2f} TB"
def ensure_directory(directory):
"""Создание директории если не существует"""
if not os.path.exists(directory):
os.makedirs(directory)
def get_valid_filename(filename):
"""Очистка имени файла от недопустимых символов"""
invalid_chars = '<>:"/\\|?*'
for char in invalid_chars:
filename = filename.replace(char, '_')
return filename