From a5cc5d3bc015ee5f89bd381c521e596413cfe684 Mon Sep 17 00:00:00 2001 From: Morgan Date: Sun, 4 Feb 2024 17:42:22 +0900 Subject: [PATCH] Init. --- README.md | 58 ++++++ app.py | 535 +++++++++++++++++++++++++++++++++++++++++++++++++++++ convert.py | 55 ++++++ 3 files changed, 648 insertions(+) create mode 100644 README.md create mode 100644 app.py create mode 100644 convert.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..494554b --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +## VTT Maker. +Python GUI app for VTT subtitling. + +Just makes stacking subtitle. Nothing more, nothing advanced. + +### Usage +Load audio, load script, play audio, make subtitle. + +Script files are linebreaked text file, each line acts as single scene. +When from openai/whisper, you usually have to edit it manually. + +You can save your progress, and restore it. Script file also saved in savefile. + +You can edit, merge, delete each script lines. + +- **Mark(<;>)** + + Marks start or end of scene. + +- **Next(<'>)** + + Marks end, and starts next scene. + +- **Done(\)** + + Marks breaks scene, resets scene stack. + +
+ +### Stacking subtitle? + +It's subtitle that just stackes from previous scene until it gets too long, usually for lecture sutitles. + +``` +01:29.000 --> 01:38.000 +Welcome to our lecture on system programming, an essential field that serves as the backbone of computer operation and performance. + +01:39.000 --> 01:49.000 +Welcome to our lecture on system programming, an essential field that serves as the backbone of computer operation and performance. +Today, we're going to dive deep into the intricacies of system-level software, + +01:50.000 --> 01:54.000 +Welcome to our lecture on system programming, an essential field that serves as the backbone of computer operation and performance. +Today, we're going to dive deep into the intricacies of system-level software, +exploring how it interfaces directly with the hardware, providing a platform for all other application software to run. + +01:55.000 --> 01:57.000 +System programming is often characterized by its complexity and the need for precision, as it operates close to the machine. + +01:58.000 --> 02:00.000 +System programming is often characterized by its complexity and the need for precision, as it operates close to the machine. +We'll cover key concepts including memory management, process scheduling, and file systems, + +02:00.000 --> 02:10.000 +System programming is often characterized by its complexity and the need for precision, as it operates close to the machine. +We'll cover key concepts including memory management, process scheduling, and file systems, +which are critical for ensuring that our computers run efficiently and reliably. +``` diff --git a/app.py b/app.py new file mode 100644 index 0000000..38100f8 --- /dev/null +++ b/app.py @@ -0,0 +1,535 @@ +#!/usr/bin/env python3 + +import tkinter as tk +from tkinter import filedialog +from tkinter import simpledialog +from tkinter import messagebox +import vlc +import time, sys + +vlc_instance = vlc.Instance('--verbose=0') +player = vlc_instance.media_player_new() + +audio_started = False + +script_lines, line_index, current_subtitle = [], 0, {} + +subtitles = [] + +MediaTotalLength = 0 + +stdout_buf = [] + +class TextRedirector(object): + def __init__(self, buf, origin): + self.buffer = buf + self.origin = origin + + def write(self, string): + self.origin.write(string) + self.buffer.append(string) + + def flush(self): + self.origin.flush() + +def toggle_audio(event = None): + global audio_started + if not audio_started: + print("Audio Play.") + player.play() + audio_started = True + elif player.is_playing(): + print("Audio Pause.") + player.pause() + else: + print("Audio Play.") + player.play() + +def rewind_audio(event = None): + new_time = max(player.get_time() - 5000, 0) + player.set_time(new_time) + +def fastforward_audio(event = None): + new_time = player.get_time() + 5000 + player.set_time(new_time) + +def mark_start(): + timestamp = player.get_time() / 1000.0 + current_subtitle["start"] = timestamp + if line_index > len(script_lines) or not len(script_lines): + print("Please load first..") + return + current_subtitle["content"] = current_subtitle.get("content","") + script_lines[line_index] + update_display() + print(f"\n{timestamp} --> ", end="") + +def mark_end(): + if current_subtitle.get("content") == None: + print("Please load first..") + return + timestamp = player.get_time() / 1000.0 + current_subtitle["end"] = timestamp + subtitles.append(current_subtitle.copy()) + print(f"{current_subtitle["end"]}\n{current_subtitle["content"]}") + current_subtitle["content"] = current_subtitle["content"] + "\n" + current_subtitle["start"] = None + load_next_line() + update_display() + +def on_next_press(event = None): + if current_subtitle.get("start") is None: + mark_start() + else: + mark_end() + +def on_autonext_press(event = None): + if current_subtitle.get("start") is None: + mark_start() + else: + mark_end() + mark_start() + +def on_skip_press(event = None): + if line_index < 1: + return + if current_subtitle["start"] == None: + current_subtitle["content"] = "" + current_subtitle["start"] = None + load_next_line() + update_display() + return + timestamp = player.get_time() / 1000.0 + current_subtitle["end"] = timestamp + subtitles.append(current_subtitle.copy()) + print(f"{current_subtitle["end"]}\n{current_subtitle["content"]}") + current_subtitle["content"] = "" + current_subtitle["start"] = None + load_next_line() + update_display() + +def on_back(event = None): + global subtitles + if len(subtitles) == 0: + messagebox.showerror("Error", f"No subtitle to remove.") + return + if current_subtitle["start"]: + messagebox.showerror("Error", f"\nDeleting \"{current_subtitle.get("content")}\" \n You need to go back and mark start again.") + if len(subtitles) > 2: + current_subtitle["content"] = subtitles[-2]["content"] + "\n" + current_subtitle["content"] = "" + current_subtitle["start"] = None + else: + messagebox.showerror("Error", f"\nDeleting \"{subtitles[-1].get("content")}\" \n You need to go back and mark start again.") + del(subtitles[-1]) + print(f"\nCurrent: #{len(subtitles)+1} {current_subtitle}") + load_next_line(diff = -1) + +def update_timestamp(): + current_pos = max(player.get_time() / 1000, 0) + timestamp_label.config(text=f"{current_pos:.3f}s / {MediaTotalLength}s") + root.after(200, update_timestamp) + +def multiline(content): + return content.strip() + if content: + return "- " + content.strip().replace("\n", "\n- ") + +def to_time(seconds): + minutes = int(seconds // 60) + second = seconds % 60 + hours = int(minutes // 60) + minute = minutes % 60 + return f"{hours}:{minute:02}:{second:06.3f}" + +def update_display(): + script_listbox.delete(0, tk.END) + subtitle_text.config(state=tk.NORMAL) + subtitle_text.delete("1.0", tk.END) + + for n, line in enumerate(script_lines): + display_line = " " + line + script_listbox.insert(tk.END, display_line) + + if n < len(subtitles): + lstart = subtitles[n]["start"] + lend = subtitles[n]["end"] + subtitle_display = f"{n+1}\n{to_time(lstart)} --> {to_time(lend)}\n{multiline(subtitles[n]['content']).replace("\n", "↵\n")} \n\n" + subtitle_text.insert(tk.END, subtitle_display) + + elif n == len(subtitles) : + lstart = current_subtitle.get("start", "") + + if lstart: + subtitle_display = f"{n+1}\n{to_time(lstart)} -> \n{multiline(current_subtitle.get('content'))} \n\n" + subtitle_text.insert(tk.END, subtitle_display) + + if n == line_index: + script_listbox.itemconfig(n, {'bg': 'lightgrey'}) + + if player.is_playing(): + script_listbox.see(min(line_index + 5, len(script_lines))) + subtitle_text.see(tk.END) + + subtitle_text.config(state=tk.DISABLED) + +def load_next_line(diff = 1): + global line_index + if line_index < len(script_lines): + line_index += diff + update_display() + +def load_file(label): + file_path = filedialog.askopenfilename() + if file_path: + label.config(text=file_path.split("/")[-1]) + return file_path + return None + +def choose_audio(): + global player, MediaTotalLength + file_path = load_file(audio_label) + if file_path: + player.set_media(media := vlc.Media(file_path)) + media.parse_with_options(vlc.MediaParseFlag.fetch_network, 0) + while not media.is_parsed(): + time.sleep(0.1) + MediaTotalLength = (media.get_duration() // 1000) + +def choose_script(): + global script_lines, line_index, current_subtitle + file_path = load_file(script_label) + if file_path: + script_lines = open(file_path, 'r').read().splitlines() + subtitles.clear() + line_index, current_subtitle = 0, {} + update_display() + +def save_subtitles(): + vtt_path = filedialog.asksaveasfilename(defaultextension=".vtt", filetypes=[("VTT files", "*.vtt"), ("All files", "*.*")]) + if vtt_path: + with open(vtt_path, 'w') as f: + f.write('WEBVTT\n\n') + for subtitle in subtitles: + f.write(f"{to_time(subtitle['start'])} --> {to_time(subtitle['end'])}\n") + f.write(multiline(subtitle['content'])) + f.write("\n\n") + + print(f"Done saving {len(subtitles)} subtitles") + +def save_prog(): + import json + fpath = filedialog.asksaveasfilename(defaultextension=".json", filetypes=[("JSON files", "*.json"), ("All files", "*.*")]) + if fpath: + with open(fpath, 'w') as f: + f.write(json.dumps({"subtitles": subtitles, "script_lines": script_lines,"line_index": line_index,"current_subtitle": current_subtitle, "play": player.get_time()}, indent=2)) + +def load_prog(): + global subtitles, player, script_lines, line_index, current_subtitle + import json + fpath = filedialog.askopenfilename() + if fpath: + with open(fpath, 'r') as f: + back = json.loads(f.read()) + subtitles = back["subtitles"] + script_lines = back["script_lines"] + line_index = back["line_index"] + current_subtitle = back["current_subtitle"] + curpos = int(back["play"]) + messagebox.showinfo("Loaded", f"Loaded {len(subtitles)} subtitles.") + player.play() + player.set_time(curpos) + time.sleep(0.3) + player.pause() + update_display() + +def skip_to_time(): + try: + time_seconds = float(skip_time_entry.get()) + player.set_time(int(time_seconds * 1000)) + update_timestamp() + except ValueError: + messagebox.showerror("Error", "Please enter a valid number of seconds.") + + except Exception as e: + print(e) + +def on_list_right_click(event): + try: + context_menu.tk_popup(event.x_root, event.y_root) + finally: + context_menu.grab_release() + +def on_left_click(event): + context_menu.unpost() + +def on_list_left_click(event): + if player.is_playing(): + player.pause() + on_left_click(event) + +def merge_selected(event = None): + selected_indices = script_listbox.curselection() + if not selected_indices: + messagebox.showinfo("No selection", "Please select items to merge.") + return + + if len(selected_indices) == 1: + messagebox.showinfo("Not enough selection", "Please select multiple item to merge.") + return + + selected_lines = [script_lines[i] for i in selected_indices] + print("Merge", selected_lines) + merged_line = ' '.join(selected_lines) + + script_lines[selected_indices[0]] = merged_line + for i in selected_indices[1:]: + del(script_lines[i]) + + update_display() + +def edit_selected(event = None, selected_idx = None): + if not selected_idx: + selected_indices = script_listbox.curselection() + if not selected_indices: + messagebox.showinfo("No selection", "Please select item to split.") + return + + if len(selected_indices) > 1: + messagebox.showinfo("Too many selection", "Please select one item.") + return + + selected_idx = selected_indices[0] + + selected_line = script_lines[selected_idx] + + print("Edit", selected_line) + + top = tk.Toplevel(root) + top.title("Edit and Split Line") + text = tk.Text(top, height=5, width=50) + text.pack(padx=10, pady=10) + text.insert(tk.END, selected_line) + + editted_lines = [] + def edit_and_close(event = None): + global splitted_lines + edited_line = text.get('1.0', tk.END).strip() + editted_lines = edited_line.split('\n') + top.destroy() + + del(script_lines[selected_idx]) + print(editted_lines) + for i in reversed(editted_lines): + script_lines.insert(selected_idx, i) + + update_display() + + text.bind("", edit_and_close) + text.bind("", edit_and_close) + text.bind("", lambda e: top.destroy()) + + button = tk.Button(top, text="Done", command=lambda: edit_and_close()) + button.pack(pady=5) + + +def remove_selected(event = None): + selected_indices = script_listbox.curselection() + if not selected_indices: + messagebox.showinfo("No selection", "Please select items to remove.") + return + print("Delete", selected_lines) + for i in selected_indices[1:]: + del(script_lines[i]) + update_display() + +def on_list_double_click(event): + index = script_listbox.nearest(event.y) + if not script_listbox.selection_includes(index): + return + + text = script_listbox.get(index) + + entry = tk.Entry(root, bd=1, highlightthickness=1, ) + entry.insert(0, text) + entry.select_range(0, tk.END) + + def save_edit(event=None): + script_listbox.delete(index) + script_listbox.insert(index, entry.get()) + entry.destroy() + + bbox = script_listbox.bbox(index) + entry.place(x=bbox[0], y=bbox[1], width=bbox[2], height=bbox[3]) + + entry.bind("", save_edit) + entry.bind("", save_edit) + entry.bind("", lambda e: entry.destroy()) + + entry.focus_set() + +def on_list_double_click(event): + index = script_listbox.nearest(event.y) + if not script_listbox.selection_includes(index): + return + edit_selected(selected_idx = index) + +def update_stdout(): + stdoutext.config(state=tk.NORMAL) + stdoutext.delete("1.0", tk.END) + stdoutext.insert(tk.END, "".join(stdout_buf)) + stdoutext.config(state=tk.DISABLED) + debugwindow.after(100, update_stdout) + +def show_console_output_screen(): + global stdoutext, debugwindow + debugwindow = tk.Toplevel(root) + debugwindow.geometry("700x400") + stdoutext = tk.Text(debugwindow, state=tk.DISABLED) + stdoutext.pack(padx=15, pady=15, fill=tk.BOTH) + update_stdout() + + +root = tk.Tk() +root.title("Subtitle Timing Editor") +root.geometry('1000x800') +root.resizable(False, False) + +content_frame = tk.Frame(root) +content_frame.pack(side=tk.LEFT, padx=5, pady=5) + +subtitle_text = tk.Text(content_frame, width=100, height=20, borderwidth=1, relief="solid", state=tk.DISABLED) +subtitle_text.pack(side=tk.BOTTOM, fill='both', expand=True) + +script_listbox = tk.Listbox(content_frame, width=100, height=20, borderwidth=1, relief="solid", selectmode=tk.EXTENDED) +script_listbox.pack(side=tk.BOTTOM, fill='both', expand=True) + +context_menu = tk.Menu(root, tearoff=0) +context_menu.add_command(label="Merge (M)", command=merge_selected) +context_menu.add_command(label="Edit (E)", command=edit_selected) +context_menu.add_command(label="Remove (R)", command=remove_selected) +context_menu.add_separator() +context_menu.add_command(label="Exit", command=root.quit) + +script_listbox.bind("M", merge_selected) +script_listbox.bind("E", edit_selected) +script_listbox.bind("R", remove_selected) + +script_listbox.bind("", on_list_right_click) +script_listbox.bind("", on_list_double_click) + + +button_frame = tk.Frame(root) +button_frame.pack(side=tk.BOTTOM, pady=(0,15)) + +rewind_button = tk.Button(button_frame, text='-5s', width=2, command=rewind_audio) +rewind_button.pack(side=tk.LEFT, padx=5, pady=5) + +play_button = tk.Button(button_frame, text='P/P', width=2, command=toggle_audio) +play_button.pack(side=tk.LEFT, padx=5, pady=5) + +fastforward_button = tk.Button(button_frame, text='+5s', width=2, command=fastforward_audio) +fastforward_button.pack(side=tk.LEFT, padx=5, pady=5) + +timestamp_label = tk.Label(root, text="0.00s / 0.00s") +timestamp_label.pack(side=tk.BOTTOM, pady=5) + + + +btn_frame = tk.Frame(root, borderwidth=0, relief="solid") +btn_frame.pack(side=tk.BOTTOM, padx=5) + +next_button = tk.Button(btn_frame, text='Mark', width=2, command=on_next_press) +next_button.pack(side=tk.LEFT, padx=5, pady=5) + +autonext_button = tk.Button(btn_frame, text='Next', width=2, command=on_autonext_press) +autonext_button.pack(side=tk.LEFT, padx=5, pady=5) + +skip_button = tk.Button(btn_frame, text='Done', width=2, command=on_skip_press) +skip_button.pack(side=tk.LEFT, padx=5, pady=5) + + + +file_frame = tk.Frame(root, padx=10, pady=5, borderwidth=0, relief="solid") +file_frame.pack(side=tk.TOP, padx=(0,5), pady=5, fill=tk.BOTH) + +audio_button = tk.Button(file_frame, text='Choose Audio', command=choose_audio) +audio_button.pack(side=tk.TOP, fill=tk.X) + +audio_label = tk.Label(file_frame, text='No audio file selected') +audio_label.pack(side=tk.TOP, pady=5) + +script_button = tk.Button(file_frame, text='Choose Script', command=choose_script) +script_button.pack(side=tk.TOP, fill=tk.X) + +script_label = tk.Label(file_frame, text='No script file selected') +script_label.pack(side=tk.TOP, pady=5) + +saveprog_button = tk.Button(file_frame, text='Save progress', command=save_prog) +saveprog_button.pack(side=tk.TOP, pady=5, fill=tk.X) + +loadprog_button = tk.Button(file_frame, text='Load progress', command=load_prog) +loadprog_button.pack(side=tk.TOP, fill=tk.X) + +save_button = tk.Button(file_frame, text='Save Subtitles', command=save_subtitles) +save_button.pack(side=tk.TOP, pady=10, fill=tk.X) + +skip_time_frame = tk.Frame(file_frame) +skip_time_frame.pack(side=tk.TOP, fill=tk.X) + +skip_time_button = tk.Button(skip_time_frame, text='Skip To', command=skip_to_time) +skip_time_button.pack(side=tk.LEFT) + +skip_time_entry = tk.Entry(skip_time_frame) +skip_time_entry.pack(side=tk.LEFT, padx=5, fill=tk.X) +skip_time_entry.insert(0, "0") + +info_frame = tk.Frame(root, borderwidth=0, relief="solid", width=10, height=10, padx=10) +info_frame.pack(side=tk.TOP, expand=True, anchor="nw", padx=(5,15), pady=(10,15)) + +info_label = tk.Label(info_frame, text='VTT Maker by @morgan9e\n\nUsage:\n Mark <\'>\n Next <;>\n Done \n' + '\n- Creates \"stacked\" subtitles easily.\n- It stacks subtitle from previous scene.\n- You can save and load progress.' + '\n- Load audio before loading progress.\n- You can Edit, Merge, Delete script with left click.' + , font=("monospace", 8), wraplength=140, justify=tk.LEFT) + +info_label.pack(side=tk.TOP, anchor="nw") + +debug_button = tk.Button(root, text='Show stdout', command=show_console_output_screen, borderwidth=0) +debug_button.pack(side=tk.TOP, pady=(0,5)) + +def presskey(btn, func): + def wrapper(event): + btn.config(relief=tk.SUNKEN) + root.after(100, lambda: btn.config(relief=tk.RAISED)) + return func() + return wrapper + +root.bind('\'', presskey(next_button,on_next_press)) +root.bind(';', presskey(autonext_button,on_autonext_press)) +root.bind('', presskey(skip_button,on_skip_press)) +root.bind('', on_back) +root.bind('', presskey(play_button,toggle_audio)) + +root.bind('', presskey(rewind_button,rewind_audio)) +root.bind('', presskey(fastforward_button,fastforward_audio)) + +root.bind("", on_left_click) + +def on_closing(): + if messagebox.askokcancel("Quit", "Do you want to quit?"): + player.stop() + root.destroy() + +root.protocol("WM_DELETE_WINDOW", on_closing) + +update_timestamp() +update_display() + +stdout = sys.stdout +stderr = sys.stderr +sys.stdout = TextRedirector(stdout_buf, stdout) +sys.stderr = TextRedirector(stdout_buf, stderr) + +root.mainloop() + +sys.stdout = stdout +sys.stderr = stderr \ No newline at end of file diff --git a/convert.py b/convert.py new file mode 100644 index 0000000..e8b3bca --- /dev/null +++ b/convert.py @@ -0,0 +1,55 @@ +import json +import re + +def parse_vtt(vtt_filename): + with open(vtt_filename, 'r', encoding='utf-8') as file: + lines = file.readlines() + + time_pattern = re.compile(r'(\d+\.\d{3}) --> (\d+\.\d{3})') + + subtitles = [] + current_subtitle = {} + + for line in lines[1:]: + match = time_pattern.match(line) + if match: + current_subtitle['start'] = float(match.group(1)) + current_subtitle['end'] = float(match.group(2)) + current_subtitle['content'] = "" + elif line.strip() == '': + if current_subtitle: + if current_subtitle['content'][-1] == "\n": + current_subtitle['content'] = current_subtitle['content'][:-1] + subtitles.append(current_subtitle) + current_subtitle = {} + else: + current_subtitle['content'] += line.strip() + "\n" # Space to separate lines + + if current_subtitle: + if current_subtitle['content'][-1] == "\n": + current_subtitle['content'] = current_subtitle['content'][:-1] + subtitles.append(current_subtitle) + + return subtitles + +def subtitles_to_backup(subtitles): + + backup_data = { + "subtitles": subtitles, + "script_lines": [], + "line_index": len(subtitles), + "current_subtitle": {}, + "play": 0 + } + return backup_data + +def main(vtt_filename, output_filename): + subtitles = parse_vtt(vtt_filename) + backup_data = subtitles_to_backup(subtitles) + + with open(output_filename, 'w', encoding='utf-8') as json_file: + json.dump(backup_data, json_file, indent=2) + +vtt_filename = 'audio.vtt' +output_filename = 'backup2.json' +main(vtt_filename, output_filename)