246 lines
11 KiB
Python
246 lines
11 KiB
Python
import os
|
|
import glob
|
|
from moviepy.editor import ImageClip, AudioFileClip, concatenate_videoclips, TextClip, CompositeVideoClip
|
|
import moviepy.editor as mpe
|
|
from gtts import gTTS
|
|
from datetime import datetime
|
|
import tkinter as tk
|
|
from tkinter import ttk, filedialog, messagebox, simpledialog
|
|
from PIL import Image, ImageTk
|
|
import re
|
|
|
|
|
|
class VideoCreatorApp:
|
|
def __init__(self, master):
|
|
self.master = master
|
|
self.master.title('Video Creator')
|
|
self.sequence = []
|
|
self.image_thumbnails = {}
|
|
self.current_image = None
|
|
self.background_music = None
|
|
self.animation_after_id = None # 이 부분을 추가
|
|
self.init_ui()
|
|
|
|
|
|
def init_ui(self):
|
|
self.list_frame = ttk.Frame(self.master)
|
|
self.list_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
|
|
self.control_frame = ttk.Frame(self.master)
|
|
self.control_frame.pack(side=tk.RIGHT, fill=tk.Y)
|
|
|
|
self.sequence_listbox = tk.Listbox(self.list_frame, width=50, height=20)
|
|
self.sequence_listbox.pack(padx=10, pady=10)
|
|
self.sequence_listbox.bind('<<ListboxSelect>>', self.show_image_preview)
|
|
|
|
ttk.Button(self.control_frame, text="Insert Script", command=self.insert_script).pack(pady=5)
|
|
ttk.Button(self.control_frame, text="Insert Image", command=self.insert_image).pack(pady=5)
|
|
ttk.Button(self.control_frame, text="Insert Action", command=self.insert_action).pack(pady=5)
|
|
ttk.Button(self.control_frame, text="Set Background Music", command=self.set_background_music).pack(pady=5)
|
|
ttk.Button(self.control_frame, text="Create Video", command=self.create_video).pack(pady=5)
|
|
# 'Update' 버튼 추가
|
|
ttk.Button(self.control_frame, text="Update Item", command=self.update_item).pack(pady=5)
|
|
# 'Delete' 버튼 추가
|
|
ttk.Button(self.control_frame, text="Delete Item", command=self.delete_item).pack(pady=5)
|
|
|
|
self.preview_canvas = tk.Canvas(self.list_frame, width=100, height=100)
|
|
self.preview_canvas.pack(padx=10, pady=10)
|
|
|
|
def update_item(self):
|
|
selection = self.sequence_listbox.curselection()
|
|
if selection:
|
|
index = selection[0]
|
|
item = self.sequence[index]
|
|
|
|
# 스크립트나 이미지에 따라 다른 입력 요청
|
|
if item['type'] == 'script':
|
|
new_text = simpledialog.askstring("Update Script", "Enter new script:", initialvalue=item['content'])
|
|
if new_text is not None:
|
|
self.sequence[index]['content'] = new_text
|
|
elif item['type'] == 'image':
|
|
new_path = filedialog.askopenfilename(filetypes=(("Image files", "*.jpg;*.png;*.gif;*.webp"), ("All files", "*.*")), initialdir="/", title="Select file")
|
|
if new_path:
|
|
self.sequence[index]['content'] = new_path
|
|
|
|
self.update_sequence_listbox() # 리스트 박스 업데이트
|
|
|
|
def delete_item(self):
|
|
selection = self.sequence_listbox.curselection()
|
|
if selection:
|
|
index = selection[0]
|
|
del self.sequence[index] # 선택된 항목 삭제
|
|
self.update_sequence_listbox() # 리스트 박스 업데이트
|
|
|
|
def update_sequence_listbox(self):
|
|
self.sequence_listbox.delete(0, tk.END)
|
|
for item in self.sequence:
|
|
if item['type'] == 'script':
|
|
self.sequence_listbox.insert(tk.END, f"Script: {item['content'][:30]}")
|
|
elif item['type'] == 'image':
|
|
# 파일 이름만 표시
|
|
file_name = os.path.basename(item['content'])
|
|
self.sequence_listbox.insert(tk.END, f"Image: {file_name}")
|
|
|
|
|
|
def insert_script(self):
|
|
script_text = simpledialog.askstring("Input", "Enter your script:")
|
|
if script_text:
|
|
self.sequence.append({'type': 'script', 'content': script_text})
|
|
self.update_sequence_listbox()
|
|
|
|
def insert_image(self):
|
|
file_path = filedialog.askopenfilename(filetypes=(("Image files", "*.jpg;*.png;*.gif;*.webp"), ("All files", "*.*")))
|
|
if file_path:
|
|
self.sequence.append({'type': 'image', 'content': file_path})
|
|
self.update_sequence_listbox()
|
|
|
|
def insert_action(self):
|
|
action = simpledialog.askstring("Insert Action", "Enter action (clean or sleep):")
|
|
if action == "sleep":
|
|
duration = simpledialog.askinteger("Sleep Duration", "Enter duration in seconds:", parent=self.master)
|
|
if duration is not None:
|
|
self.sequence.append({'type': 'action', 'action': action, 'duration': duration})
|
|
elif action == "clean":
|
|
self.sequence.append({'type': 'action', 'action': action})
|
|
self.update_sequence_listbox()
|
|
|
|
|
|
def set_background_music(self):
|
|
file_path = filedialog.askopenfilename(filetypes=(("Audio files", "*.mp3;*.wav"), ("All files", "*.*")))
|
|
if file_path:
|
|
self.background_music = file_path
|
|
messagebox.showinfo("Background Music Set", f"Background music set to: {file_path}")
|
|
|
|
def create_tts_audio(self, text, filename):
|
|
# 자음만 있는 한글 문자열 패턴 확인 및 건너뜀
|
|
if re.fullmatch(r'[ㄱ-ㅎ]+', text):
|
|
print("자음만 있는 문자열은 TTS에서 제외됩니다:", text)
|
|
return False
|
|
|
|
# TTS 생성
|
|
try:
|
|
tts = gTTS(text=text, lang='ko', slow=False)
|
|
tts.save(filename)
|
|
return True
|
|
except Exception as e:
|
|
print(f"TTS 생성 중 오류 발생: {e}")
|
|
return False
|
|
|
|
def create_video(self):
|
|
clips = []
|
|
last_clip_end_time = 0 # 마지막 클립의 종료 시간을 추적합니다.
|
|
|
|
for index, item in enumerate(self.sequence):
|
|
if item['type'] == 'script':
|
|
tts_filename = f'temp_{datetime.now().strftime("%Y%m%d%H%M%S")}.mp3'
|
|
if self.create_tts_audio(item['content'], tts_filename):
|
|
audio_clip = AudioFileClip(tts_filename).set_start(last_clip_end_time)
|
|
txt_clip = TextClip(item['content'], fontsize=24, color='white', bg_color='black', align='South')\
|
|
.set_position(('center', 'bottom'))\
|
|
.set_duration(audio_clip.duration)\
|
|
.set_start(last_clip_end_time)
|
|
# 자막이 항상 최상위 레이어에 오도록 설정합니다.
|
|
video_clip = CompositeVideoClip([txt_clip], size=(1080,1920)).set_duration(audio_clip.duration).set_start(last_clip_end_time).set_audio(audio_clip)
|
|
clips.append(video_clip)
|
|
last_clip_end_time += audio_clip.duration
|
|
os.remove(tts_filename)
|
|
|
|
elif item['type'] == 'image':
|
|
if index > 0 and self.sequence[index - 1]['type'] == 'script':
|
|
# 이미지 클립의 지속 시간을 다음 스크립트 시작까지로 설정
|
|
next_script_start_time = last_clip_end_time
|
|
img_clip = ImageClip(item['content']).set_start(last_clip_end_time).set_duration(next_script_start_time - last_clip_end_time).resize(newsize=(1080, 1920))
|
|
clips.append(img_clip)
|
|
|
|
if clips:
|
|
final_clip = concatenate_videoclips(clips, method="chain", padding=-1)
|
|
output_path = self.generate_output_path()
|
|
final_clip.write_videofile(output_path, fps=24)
|
|
messagebox.showinfo("Video Creation", f"Video created successfully: {output_path}")
|
|
else:
|
|
messagebox.showerror("Video Creation", "No clips to create a video.")
|
|
|
|
|
|
|
|
def generate_output_path(self):
|
|
# 파일 경로 생성 로직 (변경 없음)
|
|
date_str = datetime.now().strftime("%Y-%m-%d")
|
|
output_dir = os.path.join("Result", date_str)
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
file_count = len(os.listdir(output_dir)) # 이미 있는 파일의 개수를 기준으로 파일 이름 결정
|
|
return os.path.join(output_dir, f"{date_str}_{file_count + 1:03d}.mp4")
|
|
|
|
def update_sequence_listbox(self):
|
|
self.sequence_listbox.delete(0, tk.END)
|
|
for item in self.sequence:
|
|
item_desc = f"{item['type']}: {item.get('action', '')} {item.get('content', '')[:50]}"
|
|
self.sequence_listbox.insert(tk.END, item_desc)
|
|
|
|
def show_image_preview(self, event=None):
|
|
selection = self.sequence_listbox.curselection()
|
|
if selection:
|
|
selected_index = selection[0]
|
|
selected_item = self.sequence[selected_index]
|
|
if selected_item['type'] == 'image':
|
|
self.display_image_thumbnail(selected_item['content'])
|
|
|
|
|
|
def display_image_thumbnail(self, image_path):
|
|
self.preview_canvas.delete("all") # 이전 이미지 삭제
|
|
if self.animation_after_id:
|
|
self.master.after_cancel(self.animation_after_id) # 기존 애니메이션 타이머 취소
|
|
self.animation_after_id = None # 타이머 ID 초기화
|
|
|
|
if image_path.lower().endswith('.gif'): # 애니메이션 GIF 처리
|
|
self.process_gif(image_path)
|
|
else: # 정적 이미지 처리
|
|
img = Image.open(image_path)
|
|
img.thumbnail((100, 100), getattr(Image, 'Resampling', Image).LANCZOS)
|
|
photo = ImageTk.PhotoImage(img)
|
|
self.preview_canvas.create_image(50, 50, image=photo, anchor=tk.CENTER)
|
|
self.image_thumbnails[image_path] = photo # 참조 유지
|
|
|
|
def process_gif(self, image_path):
|
|
gif = Image.open(image_path)
|
|
self.gif_frames = []
|
|
try:
|
|
while True:
|
|
self.gif_frames.append(ImageTk.PhotoImage(gif.copy()))
|
|
gif.seek(len(self.gif_frames)) # 다음 프레임으로 이동
|
|
except EOFError:
|
|
pass # 모든 프레임을 처리했음
|
|
|
|
self.current_frame = 0
|
|
self.update_gif_frame() # GIF 애니메이션 시작
|
|
|
|
def update_gif_frame(self):
|
|
if self.gif_frames:
|
|
frame = self.gif_frames[self.current_frame]
|
|
self.preview_canvas.create_image(50, 50, image=frame, anchor=tk.CENTER)
|
|
self.current_frame = (self.current_frame + 1) % len(self.gif_frames)
|
|
self.animation_after_id = self.master.after(100, self.update_gif_frame)
|
|
|
|
def preview_gif(self, img, image_path, frame_number=0):
|
|
try:
|
|
img.seek(frame_number)
|
|
frame = ImageTk.PhotoImage(img)
|
|
self.preview_canvas.delete("all")
|
|
self.preview_canvas.create_image(50, 50, image=frame, anchor=tk.CENTER)
|
|
self.image_thumbnails[image_path] = frame # 참조 유지
|
|
self.current_image = image_path # 현재 GIF 이미지 경로 저장
|
|
|
|
frame_number += 1
|
|
delay = img.info.get('duration', 100) # 다음 프레임까지의 지연 시간, 기본값 100ms
|
|
self.master.after(delay, self.preview_gif, img, image_path, frame_number)
|
|
except EOFError: # 마지막 프레임에 도달하면 다시 시작
|
|
if self.current_image == image_path: # 현재 이미지가 여전히 같은 GIF인 경우에만 재시작
|
|
self.preview_gif(img, image_path, 0)
|
|
|
|
def main():
|
|
root = tk.Tk()
|
|
app = VideoCreatorApp(root)
|
|
root.mainloop()
|
|
|
|
if __name__ == '__main__':
|
|
main()
|