INIT
This commit is contained in:
parent
3f06e4f797
commit
41beee6283
35
core/logger.py
Normal file
35
core/logger.py
Normal 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
111
core/video_processor.py
Normal 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
167
gui/main_window.py
Normal 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
108
gui/video_item.py
Normal 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
10
main.py
Normal 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
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
moviepy==1.0.3
|
||||||
|
Pillow==10.0.0
|
||||||
26
utils/file_utils.py
Normal file
26
utils/file_utils.py
Normal 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
|
||||||
Loading…
Reference in New Issue
Block a user