From 4e1a4fb7245356c0f1a0189665b41a81d12b6602 Mon Sep 17 00:00:00 2001 From: MohammedShalan Date: Tue, 26 Jul 2022 06:48:09 +0200 Subject: [PATCH 01/19] create gui design --- application.py | 128 +++++++++++++++++++++++++++++++++++++++++++++++++ constants.py | 5 ++ 2 files changed, 133 insertions(+) create mode 100644 application.py create mode 100644 constants.py diff --git a/application.py b/application.py new file mode 100644 index 0000000..a585857 --- /dev/null +++ b/application.py @@ -0,0 +1,128 @@ +import tkinter as tk +from tkinter import StringVar, filedialog +import constants +from pathlib import Path +import wit_transcriber +import asyncio +import sys +import tkinter.font as tkFont + + +class IORedirector(object): + #https://stackoverflow.com/a/3333386 + '''A general class for redirecting I/O to this Text widget.''' + def __init__(self,text_area): + self.text_area = text_area + +class StdoutRedirector(IORedirector): + '''A class for redirecting stdout to this Text widget.''' + def write(self,string): + self.text_area.insert('end', string) + self.text_area.see('end') + + +""" +Idea for connecting asyncio loop_event with tkinter main_loop from: +https://www.loekvandenouweland.com/content/python-asyncio-and-tkinter.html +""" + +class GUI(tk.Tk): + + def __init__(self,loop): + self.loop = loop + self.parent = tk.Tk() + self.parent.title("أداة التفريغ الصوتي") + self.output_path = StringVar() + self.input_path = StringVar() + self.init_settings() + + self.default_font = tkFont.nametofont("TkDefaultFont") + self.default_font.configure(family='Tajawal',size=10) + + + self.label = tk.Label(self.parent, text="wit.ai أداة للتفريغ الصوتي باستخدام ") + self.label.grid(row=0, column=0, pady=10,sticky='w,e') + + self.intput_entry = tk.Entry(self.parent,textvariable = self.input_path,width=60) + self.intput_entry.grid(row=1, column=0, pady=10,padx=10) + + self.output_entry = tk.Entry(self.parent,textvariable = self.output_path,width=60) + self.output_entry.grid(row=3, column=0, pady=10,padx=10) + + + tk.Button(self.parent,text=constants.INPUT_BUTTON_TITLE,command=self.askForInputPath).grid(row=1, column=1, pady=10,padx=10) + + tk.Button(self.parent,text=constants.OUTPUT_BUTTON_TITLE,command=self.askForOutputPath).grid(row=3, column=1, pady=10,padx=10) + + + self.startTranscribe = tk.Button(self.parent,text=constants.SUBMIT_BUTTON,command=lambda: self.loop.create_task(self.getTranscribe())) + self.startTranscribe.grid(row=4, column=0, pady=10,padx=10,columnspan=2) + + self.scrollbar = tk.Scrollbar(self.parent,orient=tk.VERTICAL) + self.output_area = tk.Text(self.parent, height = 5,width = 25, bg = "light gray",yscrollcommand=self.scrollbar.set) + self.scrollbar.config(command=self.output_area.yview) + self.output_area.grid(row=5,column=0,sticky="wes",padx=10,pady=10) + self.scrollbar.grid(row=5,column=0,sticky="nse",padx=10,pady=10) + + sys.stdout = StdoutRedirector( self.output_area ) + + + + def init_settings(self): + self.output_path.set(Path().absolute()) + + # [Improvment] edit to handle onClosing and stop asyncio loop + async def show(self): + while True: + self.parent.update() + await asyncio.sleep(.1) + + def askForOutputPath(self): + output_path = filedialog.askdirectory() + self.output_path.set(output_path) + + def askForInputPath(self): + input_path=filedialog.askopenfilename(initialdir = "/",title = constants.INPUT_DIALOG_TITLE,filetypes = (("Audio files","*.mp3 *.wav *.m4a *.ogg"),("all files","*.*"))) + self.input_path.set(input_path) + + + async def getTranscribe(self): + if not self.preference.checkIfArKeyExists(): + self.on_error_occurs(constants.ERROR_API_KEY) + + self.disableEntries() + self.output_area.insert(tk.INSERT,"Please wait....") + file_path = Path(self.input_path.get()) + output_path = Path(self.output_path.get()+ f"\\{file_path.stem}.txt") + config_path = Path(self.preference.getConfigFile()) + try: + await wit_transcriber.transcribe( + file_path=file_path, + output=output_path, + semaphore = 5, + config_file=config_path, + verbose= True&self.verbose_checkbox_var.get(), + lang="ar") + except: + self.output_area.insert(tk.INSERT,"Error occurs! Please try again!") + self.enableEntries() + self.enableEntries() + + def disableEntries(self): + self.intput_entry.config(state= "disabled") + self.output_entry.config(state= "disabled") + self.startTranscribe['state'] = tk.DISABLED + + def enableEntries(self): + self.intput_entry.config(state= "normal") + self.output_entry.config(state= "normal") + self.startTranscribe['state'] = tk.NORMAL + +class App: + async def exec(self): + self.window = GUI(asyncio.get_event_loop()) + await self.window.show() + + +if __name__ == "__main__": + asyncio.run(App().exec()) diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..b11bb92 --- /dev/null +++ b/constants.py @@ -0,0 +1,5 @@ +OUTPUT_BUTTON_TITLE = "تغيير مجلد الإخراح" +INPUT_BUTTON_TITLE = "اختيار الملف" +INPUT_DIALOG_TITLE = "اختيار الملف الصوتي" +SUBMIT_BUTTON =" بدءالتحويل" + From 5fca83da798b1612154f1060113a21a6c465e12f Mon Sep 17 00:00:00 2001 From: MohammedShalan Date: Tue, 26 Jul 2022 06:51:06 +0200 Subject: [PATCH 02/19] create settings window and preference file --- Preferences.py | 47 ++++++++++++++++++++++++++ settings.py | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 Preferences.py create mode 100644 settings.py diff --git a/Preferences.py b/Preferences.py new file mode 100644 index 0000000..463cba8 --- /dev/null +++ b/Preferences.py @@ -0,0 +1,47 @@ +import json +import os +from pathlib import Path +class Preferences(): + + def __init__(self,path) -> None: + self.filename = path + "/config.json" + self.settings_object = dict() + if not os.path.exists(self.filename): + self.createPrefFile() + else: + self.loadPrefFile() + + def loadPrefFile(self): + with open(self.filename, 'r') as file: + self.settings_object = json.load(file) + + def createPrefFile(self): + with open(self.filename,'w') as file: + self.settings_object = { + 'ar':'', + } + file.write(json.dumps(self.settings_object,indent=4)) + + def updatePrefFile(self): + with open(self.filename,'w') as file: + file.truncate(0) + file.write(json.dumps(self.settings_object,indent=4)) + + def put(self,key,value): + self.settings_object[key]=value + self.updatePrefFile() + + + def get(self,key): + self.loadPrefFile() + return self.settings_object[key] + + def getJson(self): + return self.settings_object + + def checkIfArKeyExists(self): + return self.get('ar') + + def getConfigFile(self): + return Path(self.filename) + diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..1e9645c --- /dev/null +++ b/settings.py @@ -0,0 +1,90 @@ +import tkinter as tk +import tkinter.font as tkFont + +class Setting_Window(): + def __init__(self,parent,preference) : + self.parent = parent + self.preference = preference + + self.window= tk.Toplevel(self.parent) + #window.geometry("400x250") + #setting window size + width=400 + height=300 + screenwidth = self.window.winfo_screenwidth() + screenheight = self.window.winfo_screenheight() + alignstr = '%dx%d+%d+%d' % (width, height, (screenwidth - width) / 2, (screenheight - height) / 2) + self.window.geometry(alignstr) + self.window.resizable(width=False, height=False) + self.window.title("Settings") + ft = tkFont.Font(family='Tajawal',size=10) + + setting_main_title=tk.Label(self.window) + setting_main_title["font"] = ft + setting_main_title["fg"] = "#333333" + setting_main_title["justify"] = "center" + setting_main_title["text"] = "اعدادات البرنامج" + setting_main_title.place(x=140,y=20,width=100,height=25) + + self.ar_lang_entry_strvar = tk.StringVar() + self.ar_lang_entry_strvar.set("ar") + ar_lang_entry=tk.Entry(self.window,textvariable=self.ar_lang_entry_strvar) + ar_lang_entry["borderwidth"] = "1px" + ar_lang_entry["font"] = ft + ar_lang_entry["justify"] = "left" + ar_lang_entry["state"] = "disabled" + ar_lang_entry.place(x=30,y=80,width=109,height=32) + + self.ar_apiKey_entry_strvar = tk.StringVar() + self.ar_apiKey_entry_strvar.set(self.preference.get("ar")) + ar_apiKey_entry=tk.Entry(self.window,textvariable=self.ar_apiKey_entry_strvar) + ar_apiKey_entry["borderwidth"] = "1px" + ar_apiKey_entry["font"] = ft + ar_apiKey_entry["justify"] = "left" + ar_apiKey_entry.place(x=170,y=80,width=213,height=30) + + + ar_lang_label=tk.Label(self.window) + ar_lang_label["font"] = ft + ar_lang_label["fg"] = "#333333" + ar_lang_label["justify"] = "left" + ar_lang_label["text"] = "اللغة العربية" + ar_lang_label.place(x=30,y=50,width=70,height=25) + + ar_apiKey_label=tk.Label(self.window) + ar_apiKey_label["font"] = ft + ar_apiKey_label["fg"] = "#333333" + ar_apiKey_label["justify"] = "center" + ar_apiKey_label["text"] = "مفتاح التفعيل" + ar_apiKey_label.place(x=170,y=50,width=100,height=25) + + save_btn=tk.Button(self.window) + save_btn["bg"] = "#f0f0f0" + save_btn["font"] = ft + save_btn["fg"] = "#000000" + save_btn["justify"] = "center" + save_btn["text"] = "حفظ" + save_btn.place(x=160,y=250,width=70,height=25) + save_btn["command"] = self.save_settings + + + def save_settings(self): + self.preference.put("ar",self.ar_apiKey_entry_strvar.get()) + self.show_info("تم حفظ الاعدادات بنجاح!") + + def show_info(self,msg): + tk.messagebox.showinfo('اعدادات',msg) + + def load_preference_settings(self): + self.ar_apiKey_entry_strvar.set(self.preference.get("ar")) + + + + + + + + + + + From 2e41bf54c49948e10beea9bdc7b8a7c067dbd8ca Mon Sep 17 00:00:00 2001 From: MohammedShalan Date: Tue, 26 Jul 2022 07:07:37 +0200 Subject: [PATCH 03/19] add menu bar & fix msg error --- application.py | 32 ++++++++++++++++++++++++++++++-- constants.py | 7 +++++++ settings.py | 3 +++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/application.py b/application.py index a585857..11dfe3f 100644 --- a/application.py +++ b/application.py @@ -1,7 +1,10 @@ import tkinter as tk +from tkinter import messagebox from tkinter import StringVar, filedialog +from Preferences import Preferences import constants from pathlib import Path +from settings import Setting_Window import wit_transcriber import asyncio import sys @@ -35,10 +38,21 @@ def __init__(self,loop): self.output_path = StringVar() self.input_path = StringVar() self.init_settings() - + self.preference = Preferences(str(Path().absolute())) self.default_font = tkFont.nametofont("TkDefaultFont") self.default_font.configure(family='Tajawal',size=10) + self.menu = tk.Menu(self.parent) + filemenu = tk.Menu(self.menu, tearoff=0) + filemenu.add_command(label=constants.MENU_BAR_FILE_NEW, command=self.askForInputPath) + filemenu.add_command(label=constants.MENU_BAR_FILE_SETTINGS, command=self.open_win) + filemenu.add_separator() + filemenu.add_command(label=constants.MENU_BAR_FILE_EXIT, command=self.parent.destroy) + helpmenu = tk.Menu(self.menu, tearoff=0) + helpmenu.add_command(label=constants.MENU_BAR_ABOUT, command='') + self.menu.add_cascade(label=constants.MENU_BAR_FILE, menu=filemenu) + self.menu.add_cascade(label=constants.MENU_BAR_HELP, menu=helpmenu) + self.parent.config(menu=self.menu) self.label = tk.Label(self.parent, text="wit.ai أداة للتفريغ الصوتي باستخدام ") self.label.grid(row=0, column=0, pady=10,sticky='w,e') @@ -66,6 +80,15 @@ def __init__(self,loop): sys.stdout = StdoutRedirector( self.output_area ) + self.verbose_checkbox_var = tk.IntVar() + # print(self.verbose_checkbox_var) + verbose_checkbox=tk.Checkbutton(self.parent,variable=self.verbose_checkbox_var) + verbose_checkbox["justify"] = "center" + verbose_checkbox["text"] = "اظهار النتائج" + verbose_checkbox.grid(row=6,column=0,sticky="w",padx=10,pady=10) + verbose_checkbox["offvalue"] = 0 + verbose_checkbox["onvalue"] = 1 + def init_settings(self): @@ -85,7 +108,12 @@ def askForInputPath(self): input_path=filedialog.askopenfilename(initialdir = "/",title = constants.INPUT_DIALOG_TITLE,filetypes = (("Audio files","*.mp3 *.wav *.m4a *.ogg"),("all files","*.*"))) self.input_path.set(input_path) - + def on_error_occurs(self,error_msg): + messagebox.showerror('خطا',error_msg) + + def open_win(self): + Setting_Window(self.parent,self.preference) + async def getTranscribe(self): if not self.preference.checkIfArKeyExists(): self.on_error_occurs(constants.ERROR_API_KEY) diff --git a/constants.py b/constants.py index b11bb92..c6f6e83 100644 --- a/constants.py +++ b/constants.py @@ -2,4 +2,11 @@ INPUT_BUTTON_TITLE = "اختيار الملف" INPUT_DIALOG_TITLE = "اختيار الملف الصوتي" SUBMIT_BUTTON =" بدءالتحويل" +MENU_BAR_FILE = "ملف" +MENU_BAR_FILE_SETTINGS = "الاعدادات" +MENU_BAR_FILE_NEW = "ملف جديد" +MENU_BAR_FILE_EXIT ="خروج" +MENU_BAR_HELP = "المساعدة" +MENU_BAR_ABOUT= "عن البرنامج" +ERROR_API_KEY = "برجاء اضافة المفتاح الخاص بموقع wit.ai واعادة المحاولة" diff --git a/settings.py b/settings.py index 1e9645c..98d77d2 100644 --- a/settings.py +++ b/settings.py @@ -2,6 +2,9 @@ import tkinter.font as tkFont class Setting_Window(): + """ + Window is designed by using https://visualtk.com/ + """ def __init__(self,parent,preference) : self.parent = parent self.preference = preference From 277d62b1ad82a275312d29670b698ddf90254c70 Mon Sep 17 00:00:00 2001 From: MohammedShalan Date: Tue, 26 Jul 2022 07:09:11 +0200 Subject: [PATCH 04/19] fix main loop run forever after closing --- application.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/application.py b/application.py index 11dfe3f..d5a21b5 100644 --- a/application.py +++ b/application.py @@ -34,6 +34,7 @@ class GUI(tk.Tk): def __init__(self,loop): self.loop = loop self.parent = tk.Tk() + self.parent.protocol("WM_DELETE_WINDOW", self.on_closing) self.parent.title("أداة التفريغ الصوتي") self.output_path = StringVar() self.input_path = StringVar() @@ -47,7 +48,7 @@ def __init__(self,loop): filemenu.add_command(label=constants.MENU_BAR_FILE_NEW, command=self.askForInputPath) filemenu.add_command(label=constants.MENU_BAR_FILE_SETTINGS, command=self.open_win) filemenu.add_separator() - filemenu.add_command(label=constants.MENU_BAR_FILE_EXIT, command=self.parent.destroy) + filemenu.add_command(label=constants.MENU_BAR_FILE_EXIT, command=self.on_closing) helpmenu = tk.Menu(self.menu, tearoff=0) helpmenu.add_command(label=constants.MENU_BAR_ABOUT, command='') self.menu.add_cascade(label=constants.MENU_BAR_FILE, menu=filemenu) @@ -114,6 +115,10 @@ def on_error_occurs(self,error_msg): def open_win(self): Setting_Window(self.parent,self.preference) + def on_closing(self): + self.parent.destroy() + asyncio.get_event_loop().stop() + async def getTranscribe(self): if not self.preference.checkIfArKeyExists(): self.on_error_occurs(constants.ERROR_API_KEY) From 34f1745c4cb4dd96a5db78470317904d0b20f7e9 Mon Sep 17 00:00:00 2001 From: MohammedShalan Date: Tue, 26 Jul 2022 07:24:25 +0200 Subject: [PATCH 05/19] handle about menu to open github repo --- application.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/application.py b/application.py index d5a21b5..6176a23 100644 --- a/application.py +++ b/application.py @@ -2,6 +2,7 @@ from tkinter import messagebox from tkinter import StringVar, filedialog from Preferences import Preferences +from about import About_Window import constants from pathlib import Path from settings import Setting_Window @@ -9,6 +10,7 @@ import asyncio import sys import tkinter.font as tkFont +from webbrowser import open_new_tab class IORedirector(object): @@ -50,7 +52,7 @@ def __init__(self,loop): filemenu.add_separator() filemenu.add_command(label=constants.MENU_BAR_FILE_EXIT, command=self.on_closing) helpmenu = tk.Menu(self.menu, tearoff=0) - helpmenu.add_command(label=constants.MENU_BAR_ABOUT, command='') + helpmenu.add_command(label=constants.MENU_BAR_ABOUT, command=lambda:open_new_tab("https://github.com/yshalsager/wit_transcriber")) self.menu.add_cascade(label=constants.MENU_BAR_FILE, menu=filemenu) self.menu.add_cascade(label=constants.MENU_BAR_HELP, menu=helpmenu) self.parent.config(menu=self.menu) From ee7e373bdf76d3bbc41be0ba6b8adf45877bf91b Mon Sep 17 00:00:00 2001 From: MohammedShalan Date: Tue, 26 Jul 2022 07:29:29 +0200 Subject: [PATCH 06/19] fix about window and add icon --- application.py | 1 - main.ico | Bin 0 -> 4286 bytes 2 files changed, 1 deletion(-) create mode 100644 main.ico diff --git a/application.py b/application.py index 6176a23..dc5eca2 100644 --- a/application.py +++ b/application.py @@ -2,7 +2,6 @@ from tkinter import messagebox from tkinter import StringVar, filedialog from Preferences import Preferences -from about import About_Window import constants from pathlib import Path from settings import Setting_Window diff --git a/main.ico b/main.ico new file mode 100644 index 0000000000000000000000000000000000000000..b782eefd9852552af817bf92d91da08b253418dd GIT binary patch literal 4286 zcmcgwTWnNS6upQs{`mMZ(Zqm%M&?lwECwmbThfS}c@ON~io-hnDSHXYQGvduK**V3=n0+}U^E zwf0_T-`<&8)*}2YU25^Swfk1fy3Mkzy8(o(#{tIG3+GyIP9q)2{#{jHVga`U9|A7| zHvrj*MTk(x#^+cCNCNADn*b|RJt}%GT=@?)mMbp6{XEj{YpUV>otbh z3OlDLCD`2V{w0ky6NsC^^w z2`~z9-iQ=nYPyd(6FRq$m0HxK_Vd6Dpfsu*N<>4)u5{IWTn#R2QhTG01?x(rV!uze z?ea?5Pd?#$eXh`z9ONP=xm}#p{z`N2*yEFLN|(vjogUe~+w0CPs21vxi=5=9hSsyS zzvO%7ZQkyYoqK)CemiTd9dY1TXym%MWr>!|*oKKVz=>H1pJPu+A$z17Az(;st4Ge;IvvGdaP_ zti`3_&^PMJ@S3qut!r(yy++S8AgxRc|%X2X!81W4W9>T`PUR zZ&E!DkNzk_BNeLu;gL!?GZ~lZnd6v?msCvmP7eA+PIBwGy*l*iaisM*89&>i`r}^L z{qmI3t8aN4-uFnX<2AeIwa7UlO^MZNr3ZhU+$)18%9MASi+)W_A61;>X56U3p-<0w zU$sKxAo%EUXZOcSuj9xEGI_phVb7Z$RWmR;7RGmcbVw>jC~9Ye=nCV&7tQXR9`ib2UMTk{a;`f z*GY1wUm7v9J&$wruWRs2ZR1LrIM*ihk$cTqF#qPUUuxGzAGYIu2{%6}$qT>XevGT@ z&3vP8UB|!Z^O@w6zQN5hKcAA3u>h{gnqS)w=e-s0d{fQkfAr289Qu4N7|#a9-O~Pw z{GCcl*RfA+E$vVKmAb)t$DFHv-TbCL&y`kqWmlEiOZt8|dVFJ+_c^@Op5|T2O%0>R zO#h5++UAjRd`1aHoX?`Pk7ix@d(TxDpE=|tH#KtU^SaVh18YE=W&=68@gfVF_$k7BRXqNX*wUS#$$k>nEK7GSZ+2L8+7 I8oa3U4~!!^ZU6uP literal 0 HcmV?d00001 From 8feb9db299747ae6fc0477b2ec364667ba3acc2e Mon Sep 17 00:00:00 2001 From: MohammedShalan Date: Tue, 26 Jul 2022 07:53:22 +0200 Subject: [PATCH 07/19] add cx_freeze setup config file --- setup.py | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 setup.py diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..66d4fc0 --- /dev/null +++ b/setup.py @@ -0,0 +1,49 @@ +import os +import shutil +import sys +import cx_Freeze + +os.environ['TCL_LIBRARY'] = r'PATH_TO_PYTHON\\tcl\\tcl8.6' +os.environ['TK_LIBRARY'] = r'PATH_TO_PYTHON\\tcl\\tk8.6' + +__version__ = '1.0.0' +base = None +if sys.platform == 'win32': + base = 'Win32GUI' + +include_files = ['ffmpeg.exe','main.ico'] +includes = ['tkinter'] +excludes = ['matplotlib', 'sqlite3'] +packages = ['httpx','http','anyio', 'traceback', 'pydub','asyncio','traceback','json','re','typing','pathlib','ratelimiter','distutils'] + +bdist_msi_options = { + 'upgrade_code': '{66620F3A-DC3A-11E2-B341-002219E9B01E}', + 'add_to_path': False, + 'initial_target_dir': r'[ProgramFilesFolder]\%s' % ('TranscribeArabic'), + } + +cx_Freeze.setup( + name='Transcribe Arabic', + description='تحويل الملفات الصوتية الي نصوص', + version=__version__, + executables=[cx_Freeze.Executable('application.py', base=base,icon='main.ico',shortcutName="Transcribe Arabic", + shortcutDir="DesktopFolder",)], + options = { + 'build_exe': { + 'packages': packages, + 'includes': includes, + 'include_files': include_files, + 'include_msvcr': True, + 'excludes': excludes, + }, + 'bdist_msi':{ + 'upgrade_code': '{00EF338F-794D-3AB8-8CD6-2B0AB7541021}', + 'add_to_path': False, + 'initial_target_dir': r'[ProgramFilesFolder]\%s' % ('TranscribeArabic'), + }}, +) + +path = os.path.abspath(os.path.join(os.path.realpath(__file__), os.pardir)) +build_path = os.path.join(path, 'build', 'exe.win32-3.7') +shutil.copy(r'PATH_TO_PYTHON\\DLLs\\tcl86t.dll', build_path) +shutil.copy(r'PATH_TO_PYTHON\\DLLs\\tk86t.dll', build_path) From 87ba0515c1e3024d757166a9ce39dbecafe10910 Mon Sep 17 00:00:00 2001 From: MohammedShalan Date: Tue, 26 Jul 2022 07:53:55 +0200 Subject: [PATCH 08/19] add screenshots --- screenshots/1.png | Bin 0 -> 13654 bytes screenshots/2.png | Bin 0 -> 5154 bytes screenshots/3.png | Bin 0 -> 13857 bytes screenshots/4.png | Bin 0 -> 16440 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 screenshots/1.png create mode 100644 screenshots/2.png create mode 100644 screenshots/3.png create mode 100644 screenshots/4.png diff --git a/screenshots/1.png b/screenshots/1.png new file mode 100644 index 0000000000000000000000000000000000000000..f3660198801b0217868bde5336ce1612bda282d8 GIT binary patch literal 13654 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

gnT1y#Y|9Sz&h?1Q-> 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 literal 0 HcmV?d00001 diff --git a/screenshots/2.png b/screenshots/2.png new file mode 100644 index 0000000000000000000000000000000000000000..6ff53a0ad7a3be1299d4fdff35edf4389f75765d GIT binary patch literal 5154 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 Vua0^oI@beSH?cIXFueEpp8(iw$7KKj literal 0 HcmV?d00001 diff --git a/screenshots/3.png b/screenshots/3.png new file mode 100644 index 0000000000000000000000000000000000000000..c71b2a040ab2478d8ecf5240469ba86ad0c46e66 GIT binary patch literal 13857 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 Date: Tue, 26 Jul 2022 08:01:29 +0200 Subject: [PATCH 09/19] updatee readme with GUI screenshots --- README.md | 96 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 84 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index d5ee0a3..7f0a447 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,143 @@ # wit_transcriber + + A mini command line tool to transcribe media files using [wit.ai](https://wit.ai) + + [![GitHub release](https://img.shields.io/github/release/yshalsager/wit_transcriber.svg)](https://github.com/yshalsager/wit_transcriber/releases/) + + [![Open Source Love](https://badges.frapsoft.com/os/v1/open-source.png?v=103)](https://github.com/ellerbrock/open-source-badges/) + [![made-with-python](https://img.shields.io/badge/Made%20with-Python-1f425f.svg)](https://www.python.org/) + + [![PayPal](https://img.shields.io/badge/PayPal-Donate-00457C?style=flat&labelColor=00457C&logo=PayPal&logoColor=white&link=https://www.paypal.me/yshalsager)](https://www.paypal.me/yshalsager) + [![Patreon](https://img.shields.io/badge/Patreon-Support-F96854?style=flat&labelColor=F96854&logo=Patreon&logoColor=white&link=https://www.patreon.com/XiaomiFirmwareUpdater)](https://www.patreon.com/XiaomiFirmwareUpdater) + [![Liberapay](https://img.shields.io/badge/Liberapay-Support-F6C915?style=flat&labelColor=F6C915&logo=Liberapay&logoColor=white&link=https://liberapay.com/yshalsager)](https://liberapay.com/yshalsager) + + ## Configuring Wit.ai + + - Open [wit.ai](https://wit.ai/) and sign with Facebook account. + - Go to [wit.ai/apps](https://wit.ai/apps) and click on New App. + - Choose a name and select a language, set the app visibility to private, then press Create. + - Go to Management > Settings (`https://wit.ai/apps//settings`). + - Under the Client Access Token section click on the token to copy it, this is the API key. + + ## Usage + + **Note**: ffmpeg must be installed! + + Copy config example file to `config.json` and add required languages API keys you got from the previous step. + + ```bash + usage: wit_transcriber.py [-h] -i INPUT [-o OUTPUT] [-c CONFIG] [-x CONNECTIONS] [-l LANG] [-v] + + options: - -h, --help show this help message and exit - -i INPUT, --input INPUT - Path of media file to be transcribed. - -o OUTPUT, --output OUTPUT - Path of output file. - -c CONFIG, --config CONFIG - Path of config file. - -x CONNECTIONS, --connections CONNECTIONS - Number of API connections limit. - -l LANG, --lang LANG Language to use. - -v, --verbose Print API responses. + +-h, --help show this help message and exit + +-i INPUT, --input INPUT + +Path of media file to be transcribed. + +-o OUTPUT, --output OUTPUT + +Path of output file. + +-c CONFIG, --config CONFIG + +Path of config file. + +-x CONNECTIONS, --connections CONNECTIONS + +Number of API connections limit. + +-l LANG, --lang LANG Language to use. + +-v, --verbose Print API responses. + ``` + + --- + + # أداة التفريغ النصي بواسطة wit.ai + + أداة صغيرة لتفريغ الصوتيات عبر [wit.ai](https://wit.ai). + + ## إعداد Wit.ai + + - افتح [wit.ai](https://wit.ai/) وسجل الدخول بحساب فيس بوك. + - افتح [wit.ai/apps](https://wit.ai/apps) واضغط على New App لإنشاء تطبيق جديد. + - اختر اسمًا للتطبيق واختر لغةَ ثم عدل إعدادات ظهور التطبيق إلى خاص واضغط إنشاء. + - افتح قسم اﻹدارة ثم اﻹعدادات Management > Settings (`https://wit.ai/apps//settings`). + - أسفل قسم Client Access Token section ستجد مفتاح استخدام الواجهة البرمجية، انسخه لتستخدمه في الخطوة التالية. + + ## الاستخدام + + انسخ ملف config example إلى ملف باسم `config.json` ثم أضف مفاتيح استخدام الواجهة البرمجية الخاصة باللغات المطلوبة. + + ### تشغيل اﻷداة على ويندوز + #### من خلال الواجهة الرسومية + يمكن تحميل الاداة بواجهة رسومية [من هنا](/release/TranscribeArabic_v1.0.0.zip) + ![الواجهة الرئيسية ](/![screenshots](/screenshots/2.png)/1.png) + ثم اضافة مفتاح التشغيل الخاص بموقع wit.at من خلال شاشة الاعدادات كالتالي: + +#### من خلال اوامر التشغيل CMD + - حمل أحدث نسخة من الملف التنفيذي للأداة من [هنا](https://github.com/yshalsager/wit_transcriber/releases/latest). + - حمل ملفات ffmpeg إذا لم يكن مثبتا عندك من [هنا](https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.7z) وفك الضغط عن الملف ثم انقل `ffmpeg.exe` و `ffprobe.exe` إلى نفس المجلد الذي به الأداة. -- شغل الملف التنفيذي عبر سطر/موجه اﻷوامر مع استبدال كلمة filename باسم الملف المراد تفريغه. +- شغل الملف التنفيذي عبر سطر/موجه اﻷوامر مع استبدال كلمة filename باسم الملف المراد تفريغه. ```powershell + ./wit_transcriber.exe -i filename + ``` From 42b38535e83acc7f372312cac7bc848524bb2d69 Mon Sep 17 00:00:00 2001 From: MohammedShalan Date: Tue, 26 Jul 2022 08:11:19 +0200 Subject: [PATCH 10/19] fix screenshots --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7f0a447..d78c375 100644 --- a/README.md +++ b/README.md @@ -124,10 +124,12 @@ Number of API connections limit. ### تشغيل اﻷداة على ويندوز - #### من خلال الواجهة الرسومية +#### من خلال الواجهة الرسومية يمكن تحميل الاداة بواجهة رسومية [من هنا](/release/TranscribeArabic_v1.0.0.zip) - ![الواجهة الرئيسية ](/![screenshots](/screenshots/2.png)/1.png) +![main_window](/screenshots/1.png) ثم اضافة مفتاح التشغيل الخاص بموقع wit.at من خلال شاشة الاعدادات كالتالي: + ![settings_window](/screenshots/2.png) + #### من خلال اوامر التشغيل CMD From 6f34d638eb97320df796a17fd9ec22df001fb8ee Mon Sep 17 00:00:00 2001 From: yshalsager Date: Fri, 5 Aug 2022 20:24:02 +0200 Subject: [PATCH 11/19] Add RTL support using AwesomeTkinter and refactor code * Type-hint the whole codebase * Format with black * Use pathlib instead of os module Signed-off-by: yshalsager --- Preferences.py | 47 ------ application.py | 284 +++++++++++++++++++++-------------- constants.py | 15 +- poetry.lock | 107 ++++++++++++- preferences.py | 42 ++++++ pyinstaller/requirements.txt | 21 +-- pyproject.toml | 2 + settings.py | 177 +++++++++++----------- setup.py | 87 ++++++----- 9 files changed, 484 insertions(+), 298 deletions(-) delete mode 100644 Preferences.py create mode 100644 preferences.py diff --git a/Preferences.py b/Preferences.py deleted file mode 100644 index 463cba8..0000000 --- a/Preferences.py +++ /dev/null @@ -1,47 +0,0 @@ -import json -import os -from pathlib import Path -class Preferences(): - - def __init__(self,path) -> None: - self.filename = path + "/config.json" - self.settings_object = dict() - if not os.path.exists(self.filename): - self.createPrefFile() - else: - self.loadPrefFile() - - def loadPrefFile(self): - with open(self.filename, 'r') as file: - self.settings_object = json.load(file) - - def createPrefFile(self): - with open(self.filename,'w') as file: - self.settings_object = { - 'ar':'', - } - file.write(json.dumps(self.settings_object,indent=4)) - - def updatePrefFile(self): - with open(self.filename,'w') as file: - file.truncate(0) - file.write(json.dumps(self.settings_object,indent=4)) - - def put(self,key,value): - self.settings_object[key]=value - self.updatePrefFile() - - - def get(self,key): - self.loadPrefFile() - return self.settings_object[key] - - def getJson(self): - return self.settings_object - - def checkIfArKeyExists(self): - return self.get('ar') - - def getConfigFile(self): - return Path(self.filename) - diff --git a/application.py b/application.py index dc5eca2..7e7e973 100644 --- a/application.py +++ b/application.py @@ -1,162 +1,220 @@ -import tkinter as tk -from tkinter import messagebox -from tkinter import StringVar, filedialog -from Preferences import Preferences -import constants -from pathlib import Path -from settings import Setting_Window -import wit_transcriber import asyncio -import sys +import sys +import tkinter as tk import tkinter.font as tkFont +from pathlib import Path +from tkinter import StringVar, filedialog, messagebox from webbrowser import open_new_tab +from awesometkinter.bidirender import render_text + +import constants +import wit_transcriber +from preferences import PreferencesManager +from settings import SettingWindow + -class IORedirector(object): - #https://stackoverflow.com/a/3333386 - '''A general class for redirecting I/O to this Text widget.''' - def __init__(self,text_area): +class IORedirector: + # https://stackoverflow.com/a/3333386 + """A general class for redirecting I/O to this Text widget.""" + + def __init__(self, text_area: tk.Text) -> None: self.text_area = text_area + class StdoutRedirector(IORedirector): - '''A class for redirecting stdout to this Text widget.''' - def write(self,string): - self.text_area.insert('end', string) - self.text_area.see('end') + """A class for redirecting stdout to this Text widget.""" + + def __init__(self, text_area: tk.Text): + super().__init__(text_area) + + def write(self, string: str) -> None: + self.text_area.insert("end", render_text(string)) + self.text_area.see("end") + + def flush(self) -> None: + pass + - """ Idea for connecting asyncio loop_event with tkinter main_loop from: https://www.loekvandenouweland.com/content/python-asyncio-and-tkinter.html """ -class GUI(tk.Tk): - - def __init__(self,loop): - self.loop = loop - self.parent = tk.Tk() + +class App: + def __init__(self) -> None: + self.parent: tk.Tk = tk.Tk() self.parent.protocol("WM_DELETE_WINDOW", self.on_closing) self.parent.title("أداة التفريغ الصوتي") self.output_path = StringVar() self.input_path = StringVar() self.init_settings() - self.preference = Preferences(str(Path().absolute())) + self.preference = PreferencesManager(Path().absolute()) self.default_font = tkFont.nametofont("TkDefaultFont") - self.default_font.configure(family='Tajawal',size=10) - + self.default_font.configure(family="Tajawal", size=10) + self.menu = tk.Menu(self.parent) - filemenu = tk.Menu(self.menu, tearoff=0) - filemenu.add_command(label=constants.MENU_BAR_FILE_NEW, command=self.askForInputPath) - filemenu.add_command(label=constants.MENU_BAR_FILE_SETTINGS, command=self.open_win) - filemenu.add_separator() - filemenu.add_command(label=constants.MENU_BAR_FILE_EXIT, command=self.on_closing) - helpmenu = tk.Menu(self.menu, tearoff=0) - helpmenu.add_command(label=constants.MENU_BAR_ABOUT, command=lambda:open_new_tab("https://github.com/yshalsager/wit_transcriber")) - self.menu.add_cascade(label=constants.MENU_BAR_FILE, menu=filemenu) - self.menu.add_cascade(label=constants.MENU_BAR_HELP, menu=helpmenu) + file_menu = tk.Menu(self.menu, tearoff=0) + file_menu.add_command( + label=render_text(constants.MENU_BAR_FILE_NEW), + command=self.ask_for_input_path, + ) + file_menu.add_command( + label=render_text(constants.MENU_BAR_FILE_SETTINGS), command=self.open_win + ) + file_menu.add_separator() + file_menu.add_command( + label=render_text(constants.MENU_BAR_FILE_EXIT), command=self.on_closing + ) + help_menu = tk.Menu(self.menu, tearoff=0) + help_menu.add_command( + label=render_text(constants.MENU_BAR_ABOUT), + command=lambda: open_new_tab( + "https://github.com/yshalsager/wit_transcriber" + ), + ) + self.menu.add_cascade( + label=render_text(constants.MENU_BAR_FILE), menu=file_menu + ) + self.menu.add_cascade( + label=render_text(constants.MENU_BAR_HELP), menu=help_menu + ) self.parent.config(menu=self.menu) - self.label = tk.Label(self.parent, text="wit.ai أداة للتفريغ الصوتي باستخدام ") - self.label.grid(row=0, column=0, pady=10,sticky='w,e') - - self.intput_entry = tk.Entry(self.parent,textvariable = self.input_path,width=60) - self.intput_entry.grid(row=1, column=0, pady=10,padx=10) - - self.output_entry = tk.Entry(self.parent,textvariable = self.output_path,width=60) - self.output_entry.grid(row=3, column=0, pady=10,padx=10) - - - tk.Button(self.parent,text=constants.INPUT_BUTTON_TITLE,command=self.askForInputPath).grid(row=1, column=1, pady=10,padx=10) - - tk.Button(self.parent,text=constants.OUTPUT_BUTTON_TITLE,command=self.askForOutputPath).grid(row=3, column=1, pady=10,padx=10) - - - self.startTranscribe = tk.Button(self.parent,text=constants.SUBMIT_BUTTON,command=lambda: self.loop.create_task(self.getTranscribe())) - self.startTranscribe.grid(row=4, column=0, pady=10,padx=10,columnspan=2) - - self.scrollbar = tk.Scrollbar(self.parent,orient=tk.VERTICAL) - self.output_area = tk.Text(self.parent, height = 5,width = 25, bg = "light gray",yscrollcommand=self.scrollbar.set) + self.label = tk.Label( + self.parent, text=render_text("wit.ai أداة للتفريغ الصوتي باستخدام") + ) + self.label.grid(row=0, column=0, pady=10, sticky="w,e") + + self.input_entry = tk.Entry(self.parent, textvariable=self.input_path, width=60) + self.input_entry.grid(row=1, column=0, pady=10, padx=10) + + self.output_entry = tk.Entry( + self.parent, textvariable=self.output_path, width=60 + ) + self.output_entry.grid(row=3, column=0, pady=10, padx=10) + + tk.Button( + self.parent, + text=render_text(constants.INPUT_BUTTON_TITLE), + command=self.ask_for_input_path, + ).grid(row=1, column=1, pady=10, padx=10) + + tk.Button( + self.parent, + text=render_text(constants.OUTPUT_BUTTON_TITLE), + command=self.ask_for_output_path, + ).grid(row=3, column=1, pady=10, padx=10) + + self.startTranscribe = tk.Button( + self.parent, + text=render_text(constants.SUBMIT_BUTTON), + command=lambda: asyncio.create_task(self.get_transcribe()), + ) + self.startTranscribe.grid(row=4, column=0, pady=10, padx=10, columnspan=2) + + self.scrollbar = tk.Scrollbar(self.parent, orient=tk.VERTICAL) + self.output_area = tk.Text( + self.parent, + height=5, + width=25, + bg="light gray", + yscrollcommand=self.scrollbar.set, + ) self.scrollbar.config(command=self.output_area.yview) - self.output_area.grid(row=5,column=0,sticky="wes",padx=10,pady=10) - self.scrollbar.grid(row=5,column=0,sticky="nse",padx=10,pady=10) + self.output_area.grid(row=5, column=0, sticky="wes", padx=10, pady=10) + self.scrollbar.grid(row=5, column=0, sticky="nse", padx=10, pady=10) - sys.stdout = StdoutRedirector( self.output_area ) + sys.stdout = StdoutRedirector(self.output_area) # type: ignore self.verbose_checkbox_var = tk.IntVar() # print(self.verbose_checkbox_var) - verbose_checkbox=tk.Checkbutton(self.parent,variable=self.verbose_checkbox_var) - verbose_checkbox["justify"] = "center" - verbose_checkbox["text"] = "اظهار النتائج" - verbose_checkbox.grid(row=6,column=0,sticky="w",padx=10,pady=10) - verbose_checkbox["offvalue"] = 0 - verbose_checkbox["onvalue"] = 1 - - - - def init_settings(self): - self.output_path.set(Path().absolute()) - - # [Improvment] edit to handle onClosing and stop asyncio loop - async def show(self): + verbose_checkbox = tk.Checkbutton( + self.parent, + variable=self.verbose_checkbox_var, + justify="center", + text=render_text("إظهار النتائج"), + offvalue=0, + onvalue=1, + ) + verbose_checkbox.grid(row=6, column=0, sticky="w", padx=10, pady=10) + + def init_settings(self) -> None: + self.output_path.set(str(Path().absolute())) + + # TODO [Improvement] edit to handle onClosing and stop asyncio loop + async def show(self) -> None: while True: self.parent.update() - await asyncio.sleep(.1) + await asyncio.sleep(0.1) - def askForOutputPath(self): + def ask_for_output_path(self) -> None: output_path = filedialog.askdirectory() self.output_path.set(output_path) - - def askForInputPath(self): - input_path=filedialog.askopenfilename(initialdir = "/",title = constants.INPUT_DIALOG_TITLE,filetypes = (("Audio files","*.mp3 *.wav *.m4a *.ogg"),("all files","*.*"))) + + def ask_for_input_path(self) -> None: + input_path = filedialog.askopenfilename( + initialdir="./", + title=render_text(constants.INPUT_DIALOG_TITLE), + filetypes=( + ("Audio files", "*.mp3 *.wav *.m4a *.ogg"), + ("all files", "*.*"), + ), + ) self.input_path.set(input_path) - def on_error_occurs(self,error_msg): - messagebox.showerror('خطا',error_msg) + @staticmethod + def on_error_occurs(error_msg: str) -> None: + messagebox.showerror("خطأ", error_msg) - def open_win(self): - Setting_Window(self.parent,self.preference) + def open_win(self) -> None: + SettingWindow(self.parent, self.preference) - def on_closing(self): + def on_closing(self) -> None: self.parent.destroy() asyncio.get_event_loop().stop() - async def getTranscribe(self): - if not self.preference.checkIfArKeyExists(): - self.on_error_occurs(constants.ERROR_API_KEY) + async def get_transcribe(self) -> None: + if not self.preference.check_if_ar_key_exists(): + self.on_error_occurs(render_text(constants.ERROR_API_KEY)) - self.disableEntries() - self.output_area.insert(tk.INSERT,"Please wait....") + self.disable_entries() + self.output_area.insert(tk.INSERT, "Please wait....\n") file_path = Path(self.input_path.get()) - output_path = Path(self.output_path.get()+ f"\\{file_path.stem}.txt") - config_path = Path(self.preference.getConfigFile()) + output_path = Path(self.output_path.get() + f"/{file_path.stem}.txt") + config_path = Path(self.preference.get_config_file()) try: await wit_transcriber.transcribe( - file_path=file_path, - output=output_path, - semaphore = 5, - config_file=config_path, - verbose= True&self.verbose_checkbox_var.get(), - lang="ar") + file_path=file_path, + output=output_path, + semaphore=5, + config_file=config_path, + verbose=True and bool(self.verbose_checkbox_var.get()), + lang="ar", + ) except: - self.output_area.insert(tk.INSERT,"Error occurs! Please try again!") - self.enableEntries() - self.enableEntries() - - def disableEntries(self): - self.intput_entry.config(state= "disabled") - self.output_entry.config(state= "disabled") - self.startTranscribe['state'] = tk.DISABLED - - def enableEntries(self): - self.intput_entry.config(state= "normal") - self.output_entry.config(state= "normal") - self.startTranscribe['state'] = tk.NORMAL + self.output_area.insert(tk.INSERT, "Error occurs! Please try again!") + self.enable_entries() + self.enable_entries() -class App: - async def exec(self): - self.window = GUI(asyncio.get_event_loop()) - await self.window.show() + def disable_entries(self) -> None: + self.input_entry.config(state="disabled") + self.output_entry.config(state="disabled") + self.startTranscribe["state"] = tk.DISABLED + def enable_entries(self) -> None: + self.input_entry.config(state="normal") + self.output_entry.config(state="normal") + self.startTranscribe["state"] = tk.NORMAL -if __name__ == "__main__": + async def exec(self) -> None: + await self.show() + + +def main() -> None: asyncio.run(App().exec()) + + +if __name__ == "__main__": + main() diff --git a/constants.py b/constants.py index c6f6e83..b7c91b5 100644 --- a/constants.py +++ b/constants.py @@ -1,12 +1,11 @@ -OUTPUT_BUTTON_TITLE = "تغيير مجلد الإخراح" +OUTPUT_BUTTON_TITLE = "تغيير مجلد الحفظ" INPUT_BUTTON_TITLE = "اختيار الملف" INPUT_DIALOG_TITLE = "اختيار الملف الصوتي" -SUBMIT_BUTTON =" بدءالتحويل" +SUBMIT_BUTTON = " بدء التحويل" MENU_BAR_FILE = "ملف" -MENU_BAR_FILE_SETTINGS = "الاعدادات" +MENU_BAR_FILE_SETTINGS = "الإعدادات" MENU_BAR_FILE_NEW = "ملف جديد" -MENU_BAR_FILE_EXIT ="خروج" -MENU_BAR_HELP = "المساعدة" -MENU_BAR_ABOUT= "عن البرنامج" -ERROR_API_KEY = "برجاء اضافة المفتاح الخاص بموقع wit.ai واعادة المحاولة" - +MENU_BAR_FILE_EXIT = "خروج" +MENU_BAR_HELP = "مساعدة" +MENU_BAR_ABOUT = "حول البرنامج" +ERROR_API_KEY = "برجاء إضافة المفتاح الخاص بموقع wit.ai وإعادة المحاولة" diff --git a/poetry.lock b/poetry.lock index 1009363..5a040e7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -23,6 +23,18 @@ doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] trio = ["trio (>=0.16)"] +[[package]] +name = "awesometkinter" +version = "2021.11.8" +description = "Pretty tkinter widgets" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pillow = ">=6.0.0" +python-bidi = "*" + [[package]] name = "black" version = "22.6.0" @@ -271,6 +283,18 @@ python-versions = ">=3.6.0" [package.dependencies] future = "*" +[[package]] +name = "pillow" +version = "9.2.0" +description = "Python Imaging Library (Fork)" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] + [[package]] name = "platformdirs" version = "2.5.1" @@ -350,6 +374,17 @@ category = "dev" optional = false python-versions = ">=3.7" +[[package]] +name = "python-bidi" +version = "0.4.2" +description = "Pure python implementation of the BiDi layout algorithm" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = "*" + [[package]] name = "pywin32-ctypes" version = "0.2.0" @@ -395,7 +430,7 @@ idna2008 = ["idna"] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" @@ -452,7 +487,7 @@ testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", [metadata] lock-version = "1.1" python-versions = ">=3.10,<3.11" -content-hash = "64bd0aa5c22aed8a9ab73ff1c6b21bb2bb1441e1edee50f91bb449006c2d7104" +content-hash = "d199c511810a53ce8b697e3375960f9e06065b9114c8958a5104c0eae6cc2298" [metadata.files] altgraph = [ @@ -463,6 +498,10 @@ anyio = [ {file = "anyio-3.6.1-py3-none-any.whl", hash = "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be"}, {file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"}, ] +awesometkinter = [ + {file = "AwesomeTkinter-2021.11.8-py3-none-any.whl", hash = "sha256:a10aa984d7d60e90e3c76e96698d3f0377aeb14461d83d824d8096b5e0c55950"}, + {file = "AwesomeTkinter-2021.11.8.tar.gz", hash = "sha256:2944e11468a437d51859f596b755c64f0df557b92b7641626fe3cfb222e4c180"}, +] black = [ {file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"}, {file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"}, @@ -591,6 +630,66 @@ pathspec = [ pefile = [ {file = "pefile-2022.5.30.tar.gz", hash = "sha256:a5488a3dd1fd021ce33f969780b88fe0f7eebb76eb20996d7318f307612a045b"}, ] +pillow = [ + {file = "Pillow-9.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a9c9bc489f8ab30906d7a85afac4b4944a572a7432e00698a7239f44a44e6efb"}, + {file = "Pillow-9.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:510cef4a3f401c246cfd8227b300828715dd055463cdca6176c2e4036df8bd4f"}, + {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7888310f6214f19ab2b6df90f3f06afa3df7ef7355fc025e78a3044737fab1f5"}, + {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:831e648102c82f152e14c1a0938689dbb22480c548c8d4b8b248b3e50967b88c"}, + {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cc1d2451e8a3b4bfdb9caf745b58e6c7a77d2e469159b0d527a4554d73694d1"}, + {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:136659638f61a251e8ed3b331fc6ccd124590eeff539de57c5f80ef3a9594e58"}, + {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6e8c66f70fb539301e064f6478d7453e820d8a2c631da948a23384865cd95544"}, + {file = "Pillow-9.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:37ff6b522a26d0538b753f0b4e8e164fdada12db6c6f00f62145d732d8a3152e"}, + {file = "Pillow-9.2.0-cp310-cp310-win32.whl", hash = "sha256:c79698d4cd9318d9481d89a77e2d3fcaeff5486be641e60a4b49f3d2ecca4e28"}, + {file = "Pillow-9.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:254164c57bab4b459f14c64e93df11eff5ded575192c294a0c49270f22c5d93d"}, + {file = "Pillow-9.2.0-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:408673ed75594933714482501fe97e055a42996087eeca7e5d06e33218d05aa8"}, + {file = "Pillow-9.2.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:727dd1389bc5cb9827cbd1f9d40d2c2a1a0c9b32dd2261db522d22a604a6eec9"}, + {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50dff9cc21826d2977ef2d2a205504034e3a4563ca6f5db739b0d1026658e004"}, + {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb6259196a589123d755380b65127ddc60f4c64b21fc3bb46ce3a6ea663659b0"}, + {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b0554af24df2bf96618dac71ddada02420f946be943b181108cac55a7a2dcd4"}, + {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:15928f824870535c85dbf949c09d6ae7d3d6ac2d6efec80f3227f73eefba741c"}, + {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:bdd0de2d64688ecae88dd8935012c4a72681e5df632af903a1dca8c5e7aa871a"}, + {file = "Pillow-9.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5b87da55a08acb586bad5c3aa3b86505f559b84f39035b233d5bf844b0834b1"}, + {file = "Pillow-9.2.0-cp311-cp311-win32.whl", hash = "sha256:b6d5e92df2b77665e07ddb2e4dbd6d644b78e4c0d2e9272a852627cdba0d75cf"}, + {file = "Pillow-9.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6bf088c1ce160f50ea40764f825ec9b72ed9da25346216b91361eef8ad1b8f8c"}, + {file = "Pillow-9.2.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:2c58b24e3a63efd22554c676d81b0e57f80e0a7d3a5874a7e14ce90ec40d3069"}, + {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eef7592281f7c174d3d6cbfbb7ee5984a671fcd77e3fc78e973d492e9bf0eb3f"}, + {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dcd7b9c7139dc8258d164b55696ecd16c04607f1cc33ba7af86613881ffe4ac8"}, + {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a138441e95562b3c078746a22f8fca8ff1c22c014f856278bdbdd89ca36cff1b"}, + {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:93689632949aff41199090eff5474f3990b6823404e45d66a5d44304e9cdc467"}, + {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:f3fac744f9b540148fa7715a435d2283b71f68bfb6d4aae24482a890aed18b59"}, + {file = "Pillow-9.2.0-cp37-cp37m-win32.whl", hash = "sha256:fa768eff5f9f958270b081bb33581b4b569faabf8774726b283edb06617101dc"}, + {file = "Pillow-9.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:69bd1a15d7ba3694631e00df8de65a8cb031911ca11f44929c97fe05eb9b6c1d"}, + {file = "Pillow-9.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:030e3460861488e249731c3e7ab59b07c7853838ff3b8e16aac9561bb345da14"}, + {file = "Pillow-9.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:74a04183e6e64930b667d321524e3c5361094bb4af9083db5c301db64cd341f3"}, + {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d33a11f601213dcd5718109c09a52c2a1c893e7461f0be2d6febc2879ec2402"}, + {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fd6f5e3c0e4697fa7eb45b6e93996299f3feee73a3175fa451f49a74d092b9f"}, + {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a647c0d4478b995c5e54615a2e5360ccedd2f85e70ab57fbe817ca613d5e63b8"}, + {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:4134d3f1ba5f15027ff5c04296f13328fecd46921424084516bdb1b2548e66ff"}, + {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:bc431b065722a5ad1dfb4df354fb9333b7a582a5ee39a90e6ffff688d72f27a1"}, + {file = "Pillow-9.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1536ad017a9f789430fb6b8be8bf99d2f214c76502becc196c6f2d9a75b01b76"}, + {file = "Pillow-9.2.0-cp38-cp38-win32.whl", hash = "sha256:2ad0d4df0f5ef2247e27fc790d5c9b5a0af8ade9ba340db4a73bb1a4a3e5fb4f"}, + {file = "Pillow-9.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:ec52c351b35ca269cb1f8069d610fc45c5bd38c3e91f9ab4cbbf0aebc136d9c8"}, + {file = "Pillow-9.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ed2c4ef2451de908c90436d6e8092e13a43992f1860275b4d8082667fbb2ffc"}, + {file = "Pillow-9.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ad2f835e0ad81d1689f1b7e3fbac7b01bb8777d5a985c8962bedee0cc6d43da"}, + {file = "Pillow-9.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea98f633d45f7e815db648fd7ff0f19e328302ac36427343e4432c84432e7ff4"}, + {file = "Pillow-9.2.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7761afe0126d046974a01e030ae7529ed0ca6a196de3ec6937c11df0df1bc91c"}, + {file = "Pillow-9.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a54614049a18a2d6fe156e68e188da02a046a4a93cf24f373bffd977e943421"}, + {file = "Pillow-9.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:5aed7dde98403cd91d86a1115c78d8145c83078e864c1de1064f52e6feb61b20"}, + {file = "Pillow-9.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:13b725463f32df1bfeacbf3dd197fb358ae8ebcd8c5548faa75126ea425ccb60"}, + {file = "Pillow-9.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:808add66ea764ed97d44dda1ac4f2cfec4c1867d9efb16a33d158be79f32b8a4"}, + {file = "Pillow-9.2.0-cp39-cp39-win32.whl", hash = "sha256:337a74fd2f291c607d220c793a8135273c4c2ab001b03e601c36766005f36885"}, + {file = "Pillow-9.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:fac2d65901fb0fdf20363fbd345c01958a742f2dc62a8dd4495af66e3ff502a4"}, + {file = "Pillow-9.2.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ad2277b185ebce47a63f4dc6302e30f05762b688f8dc3de55dbae4651872cdf3"}, + {file = "Pillow-9.2.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c7b502bc34f6e32ba022b4a209638f9e097d7a9098104ae420eb8186217ebbb"}, + {file = "Pillow-9.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d1f14f5f691f55e1b47f824ca4fdcb4b19b4323fe43cc7bb105988cad7496be"}, + {file = "Pillow-9.2.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:dfe4c1fedfde4e2fbc009d5ad420647f7730d719786388b7de0999bf32c0d9fd"}, + {file = "Pillow-9.2.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:f07f1f00e22b231dd3d9b9208692042e29792d6bd4f6639415d2f23158a80013"}, + {file = "Pillow-9.2.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1802f34298f5ba11d55e5bb09c31997dc0c6aed919658dfdf0198a2fe75d5490"}, + {file = "Pillow-9.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17d4cafe22f050b46d983b71c707162d63d796a1235cdf8b9d7a112e97b15bac"}, + {file = "Pillow-9.2.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96b5e6874431df16aee0c1ba237574cb6dff1dcb173798faa6a9d8b399a05d0e"}, + {file = "Pillow-9.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:0030fdbd926fb85844b8b92e2f9449ba89607231d3dd597a21ae72dc7fe26927"}, + {file = "Pillow-9.2.0.tar.gz", hash = "sha256:75e636fd3e0fb872693f23ccb8a5ff2cd578801251f3a4f6854c6a5d437d3c04"}, +] platformdirs = [ {file = "platformdirs-2.5.1-py3-none-any.whl", hash = "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"}, {file = "platformdirs-2.5.1.tar.gz", hash = "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d"}, @@ -628,6 +727,10 @@ pyinstaller-hooks-contrib = [ {file = "pyinstaller-hooks-contrib-2022.2.tar.gz", hash = "sha256:ab1d14fe053016fff7b0c6aea51d980bac6d02114b04063b46ef7dac70c70e1e"}, {file = "pyinstaller_hooks_contrib-2022.2-py2.py3-none-any.whl", hash = "sha256:7605e440ccb55904cb2a87d72e83642ef176fb7030c77e52ac4d9679bb3d1537"}, ] +python-bidi = [ + {file = "python-bidi-0.4.2.tar.gz", hash = "sha256:5347f71e82b3e9976dc657f09ded2bfe39ba8d6777ca81a5b2c56c30121c496e"}, + {file = "python_bidi-0.4.2-py2.py3-none-any.whl", hash = "sha256:50eef6f6a0bbdd685f9e8c207f3c9050f5b578d0a46e37c76a9c4baea2cc2e13"}, +] pywin32-ctypes = [ {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, diff --git a/preferences.py b/preferences.py new file mode 100644 index 0000000..2e815bc --- /dev/null +++ b/preferences.py @@ -0,0 +1,42 @@ +import json +from pathlib import Path +from typing import Dict + + +class PreferencesManager: + def __init__(self, path: Path) -> None: + self.filename: Path = path / "config.json" + self.settings_object: Dict[str, str] = {} + if not self.filename.exists(): + self.create_preferences_file() + else: + self.load_preferences_file() + + def load_preferences_file(self) -> None: + self.settings_object = json.loads(self.filename.read_text()) + + def create_preferences_file(self) -> None: + self.settings_object = { + "ar": "", + } + self.update_preferences_file() + + def update_preferences_file(self) -> None: + self.filename.write_text(json.dumps(self.settings_object, indent=4)) + + def put(self, key: str, value: str) -> None: + self.settings_object[key] = value + self.update_preferences_file() + + def get(self, key: str) -> str: + self.load_preferences_file() + return self.settings_object[key] + + def get_json(self) -> Dict[str, str]: + return self.settings_object + + def check_if_ar_key_exists(self) -> str: + return self.get("ar") + + def get_config_file(self) -> Path: + return Path(self.filename) diff --git a/pyinstaller/requirements.txt b/pyinstaller/requirements.txt index 713c004..8923ad1 100644 --- a/pyinstaller/requirements.txt +++ b/pyinstaller/requirements.txt @@ -1,11 +1,14 @@ -anyio==3.6.1; python_full_version >= "3.6.2" and python_version >= "3.6" -certifi==2022.6.15; python_version >= "3.6" -charset-normalizer==2.1.0; python_full_version >= "3.5.0" and python_version >= "3.6" -h11==0.12.0; python_version >= "3.6" -httpcore==0.15.0; python_version >= "3.6" -httpx==0.23.0; python_version >= "3.6" -idna==3.3; python_version >= "3.6" and python_full_version >= "3.6.2" +anyio==3.6.1; python_full_version >= "3.6.2" and python_version >= "3.7" +awesometkinter==2021.11.8; python_version >= "3.6" +certifi==2022.6.15; python_version >= "3.7" +h11==0.12.0; python_version >= "3.7" +httpcore==0.15.0; python_version >= "3.7" +httpx==0.23.0; python_version >= "3.7" +idna==3.3; python_version >= "3.7" and python_full_version >= "3.6.2" +pillow==9.2.0; python_version >= "3.7" pydub==0.25.1 +python-bidi==0.4.2; python_version >= "3.6" ratelimiter==1.2.0.post0 -rfc3986==1.5.0; python_version >= "3.6" -sniffio==1.2.0; python_full_version >= "3.6.2" and python_version >= "3.6" +rfc3986==1.5.0; python_version >= "3.7" +six==1.16.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" +sniffio==1.2.0; python_full_version >= "3.6.2" and python_version >= "3.7" diff --git a/pyproject.toml b/pyproject.toml index fc0d7a9..97cee41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ python = ">=3.10,<3.11" httpx = "^0.23.0" pydub = "^0.25.1" ratelimiter = "^1.2.0" +AwesomeTkinter = {version = "^2021.11.8", extras = ["gui"]} [tool.poetry.dev-dependencies] black = "^22.6" @@ -24,6 +25,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry.scripts] wit_transcriber = 'wit_transcriber:main' +wit_transcriber_gui = 'application:main' [tool.black] diff --git a/settings.py b/settings.py index 98d77d2..1389255 100644 --- a/settings.py +++ b/settings.py @@ -1,93 +1,102 @@ import tkinter as tk import tkinter.font as tkFont +from tkinter import messagebox -class Setting_Window(): +from awesometkinter.bidirender import render_text + +from preferences import PreferencesManager + + +class SettingWindow: """ - Window is designed by using https://visualtk.com/ + Window is designed by using https://visualtk.com/ """ - def __init__(self,parent,preference) : - self.parent = parent - self.preference = preference - self.window= tk.Toplevel(self.parent) - #window.geometry("400x250") - #setting window size - width=400 - height=300 + def __init__(self, parent: tk.Tk, preferences: PreferencesManager) -> None: + self.parent: tk.Tk = parent + self.preferences: PreferencesManager = preferences + + self.window = tk.Toplevel(self.parent) + # window.geometry("400x250") + # setting window size + width = 400 + height = 300 screenwidth = self.window.winfo_screenwidth() screenheight = self.window.winfo_screenheight() - alignstr = '%dx%d+%d+%d' % (width, height, (screenwidth - width) / 2, (screenheight - height) / 2) - self.window.geometry(alignstr) + align_str = f"{width:.0f}x{height:.0f}+{(screenwidth - width) / 2:.0f}+{(screenheight - height) / 2:.0f}" + self.window.geometry(align_str) self.window.resizable(width=False, height=False) - self.window.title("Settings") - ft = tkFont.Font(family='Tajawal',size=10) - - setting_main_title=tk.Label(self.window) - setting_main_title["font"] = ft - setting_main_title["fg"] = "#333333" - setting_main_title["justify"] = "center" - setting_main_title["text"] = "اعدادات البرنامج" - setting_main_title.place(x=140,y=20,width=100,height=25) - - self.ar_lang_entry_strvar = tk.StringVar() - self.ar_lang_entry_strvar.set("ar") - ar_lang_entry=tk.Entry(self.window,textvariable=self.ar_lang_entry_strvar) - ar_lang_entry["borderwidth"] = "1px" - ar_lang_entry["font"] = ft - ar_lang_entry["justify"] = "left" - ar_lang_entry["state"] = "disabled" - ar_lang_entry.place(x=30,y=80,width=109,height=32) - - self.ar_apiKey_entry_strvar = tk.StringVar() - self.ar_apiKey_entry_strvar.set(self.preference.get("ar")) - ar_apiKey_entry=tk.Entry(self.window,textvariable=self.ar_apiKey_entry_strvar) - ar_apiKey_entry["borderwidth"] = "1px" - ar_apiKey_entry["font"] = ft - ar_apiKey_entry["justify"] = "left" - ar_apiKey_entry.place(x=170,y=80,width=213,height=30) - - - ar_lang_label=tk.Label(self.window) - ar_lang_label["font"] = ft - ar_lang_label["fg"] = "#333333" - ar_lang_label["justify"] = "left" - ar_lang_label["text"] = "اللغة العربية" - ar_lang_label.place(x=30,y=50,width=70,height=25) - - ar_apiKey_label=tk.Label(self.window) - ar_apiKey_label["font"] = ft - ar_apiKey_label["fg"] = "#333333" - ar_apiKey_label["justify"] = "center" - ar_apiKey_label["text"] = "مفتاح التفعيل" - ar_apiKey_label.place(x=170,y=50,width=100,height=25) - - save_btn=tk.Button(self.window) - save_btn["bg"] = "#f0f0f0" - save_btn["font"] = ft - save_btn["fg"] = "#000000" - save_btn["justify"] = "center" - save_btn["text"] = "حفظ" - save_btn.place(x=160,y=250,width=70,height=25) - save_btn["command"] = self.save_settings - - - def save_settings(self): - self.preference.put("ar",self.ar_apiKey_entry_strvar.get()) - self.show_info("تم حفظ الاعدادات بنجاح!") - - def show_info(self,msg): - tk.messagebox.showinfo('اعدادات',msg) - - def load_preference_settings(self): - self.ar_apiKey_entry_strvar.set(self.preference.get("ar")) - - - - - - - - - - - + self.window.title("الإعدادات") + ft = tkFont.Font(family="Tajawal", size=10) + + setting_main_title = tk.Label( + self.window, + font=ft, + fg="#333333", + justify="center", + text=render_text("إعدادات البرنامج"), + ) + setting_main_title.place(x=140, y=20, width=200, height=25) + + self.ar_lang_entry_str_var = tk.StringVar() + self.ar_lang_entry_str_var.set("ar") + ar_lang_entry = tk.Entry( + self.window, + textvariable=self.ar_lang_entry_str_var, + borderwidth="1px", + font=ft, + justify="left", + state="disabled", + ) + ar_lang_entry.place(x=30, y=80, width=109, height=32) + + self.ar_api_key_entry_str_var = tk.StringVar() + self.ar_api_key_entry_str_var.set(self.preferences.get("ar")) + ar_api_key_entry = tk.Entry( + self.window, + textvariable=self.ar_api_key_entry_str_var, + borderwidth="1px", + font=ft, + justify="left", + ) + ar_api_key_entry.place(x=170, y=80, width=213, height=30) + + ar_lang_label = tk.Label( + self.window, + font=ft, + fg="#333333", + justify="left", + text=render_text("اللغة العربية"), + ) + ar_lang_label.place(x=30, y=50, width=70, height=25) + + ar_api_key_label = tk.Label( + self.window, + font=ft, + fg="#333333", + justify="center", + text=render_text("مفتاح التفعيل"), + ) + ar_api_key_label.place(x=170, y=50, width=100, height=25) + + save_btn = tk.Button( + self.window, + bg="#f0f0f0", + fg="#000000", + font=ft, + justify="center", + text=render_text("حفظ"), + command=self.save_settings, + ) + save_btn.place(x=160, y=250, width=70, height=25) + + def save_settings(self) -> None: + self.preferences.put("ar", self.ar_api_key_entry_str_var.get()) + self.show_info(render_text("تم حفظ الإعدادات بنجاح!")) + + @staticmethod + def show_info(msg: str) -> None: + messagebox.showinfo(render_text("إعدادات"), msg) + + def load_preference_settings(self) -> None: + self.ar_api_key_entry_str_var.set(self.preferences.get("ar")) diff --git a/setup.py b/setup.py index 66d4fc0..570288b 100644 --- a/setup.py +++ b/setup.py @@ -1,49 +1,66 @@ import os import shutil import sys -import cx_Freeze -os.environ['TCL_LIBRARY'] = r'PATH_TO_PYTHON\\tcl\\tcl8.6' -os.environ['TK_LIBRARY'] = r'PATH_TO_PYTHON\\tcl\\tk8.6' +import cx_Freeze -__version__ = '1.0.0' -base = None -if sys.platform == 'win32': - base = 'Win32GUI' +os.environ["TCL_LIBRARY"] = r"PATH_TO_PYTHON\\tcl\\tcl8.6" +os.environ["TK_LIBRARY"] = r"PATH_TO_PYTHON\\tcl\\tk8.6" -include_files = ['ffmpeg.exe','main.ico'] -includes = ['tkinter'] -excludes = ['matplotlib', 'sqlite3'] -packages = ['httpx','http','anyio', 'traceback', 'pydub','asyncio','traceback','json','re','typing','pathlib','ratelimiter','distutils'] +__version__ = "1.0.0" +base = None +if sys.platform == "win32": + base = "Win32GUI" -bdist_msi_options = { - 'upgrade_code': '{66620F3A-DC3A-11E2-B341-002219E9B01E}', - 'add_to_path': False, - 'initial_target_dir': r'[ProgramFilesFolder]\%s' % ('TranscribeArabic'), - } +include_files = ["ffmpeg.exe", "main.ico"] +includes = ["tkinter"] +excludes = ["matplotlib", "sqlite3"] +packages = [ + "httpx", + "http", + "anyio", + "traceback", + "pydub", + "asyncio", + "traceback", + "json", + "re", + "typing", + "pathlib", + "ratelimiter", + "distutils", +] cx_Freeze.setup( - name='Transcribe Arabic', - description='تحويل الملفات الصوتية الي نصوص', + name="Transcribe Arabic", + description="تحويل الملفات الصوتية الي نصوص", version=__version__, - executables=[cx_Freeze.Executable('application.py', base=base,icon='main.ico',shortcutName="Transcribe Arabic", - shortcutDir="DesktopFolder",)], - options = { - 'build_exe': { - 'packages': packages, - 'includes': includes, - 'include_files': include_files, - 'include_msvcr': True, - 'excludes': excludes, + executables=[ + cx_Freeze.Executable( + "application.py", + base=base, + icon="main.ico", + shortcutName="Transcribe Arabic", + shortcutDir="DesktopFolder", + ) + ], + options={ + "build_exe": { + "packages": packages, + "includes": includes, + "include_files": include_files, + "include_msvcr": True, + "excludes": excludes, + }, + "bdist_msi": { + "upgrade_code": "{00EF338F-794D-3AB8-8CD6-2B0AB7541021}", + "add_to_path": False, + "initial_target_dir": r"[ProgramFilesFolder]\%s" % ("TranscribeArabic"), }, - 'bdist_msi':{ - 'upgrade_code': '{00EF338F-794D-3AB8-8CD6-2B0AB7541021}', - 'add_to_path': False, - 'initial_target_dir': r'[ProgramFilesFolder]\%s' % ('TranscribeArabic'), - }}, + }, ) path = os.path.abspath(os.path.join(os.path.realpath(__file__), os.pardir)) -build_path = os.path.join(path, 'build', 'exe.win32-3.7') -shutil.copy(r'PATH_TO_PYTHON\\DLLs\\tcl86t.dll', build_path) -shutil.copy(r'PATH_TO_PYTHON\\DLLs\\tk86t.dll', build_path) +build_path = os.path.join(path, "build", "exe.win32-3.7") +shutil.copy(r"PATH_TO_PYTHON\\DLLs\\tcl86t.dll", build_path) +shutil.copy(r"PATH_TO_PYTHON\\DLLs\\tk86t.dll", build_path) From 585001e4b7012ab927d3ee1d9bf9941a21e8f003 Mon Sep 17 00:00:00 2001 From: yshalsager Date: Fri, 5 Aug 2022 21:26:40 +0200 Subject: [PATCH 12/19] refactor: organize code into a proper package Signed-off-by: yshalsager --- .gitignore | 3 +- poetry.lock | 16 ++-- pyinstaller/requirements.txt | 3 + pyproject.toml | 10 ++- setup.py | 2 +- wit_transcriber/__init__.py | 43 ++++++++++ wit_transcriber/__main__.py | 20 +++++ wit_transcriber/api_client/__init__.py | 0 .../api_client/client.py | 80 +------------------ wit_transcriber/cli/__init__.py | 0 wit_transcriber/cli/app.py | 74 +++++++++++++++++ wit_transcriber/gui/__init__.py | 0 wit_transcriber/gui/app.py | 20 +++++ .../gui/constants.py | 0 .../gui/main_window.py | 37 ++++----- .../gui/preferences.py | 0 .../gui/settings.py | 2 +- 17 files changed, 195 insertions(+), 115 deletions(-) create mode 100644 wit_transcriber/__init__.py create mode 100644 wit_transcriber/__main__.py create mode 100644 wit_transcriber/api_client/__init__.py rename wit_transcriber.py => wit_transcriber/api_client/client.py (71%) create mode 100644 wit_transcriber/cli/__init__.py create mode 100644 wit_transcriber/cli/app.py create mode 100644 wit_transcriber/gui/__init__.py create mode 100644 wit_transcriber/gui/app.py rename constants.py => wit_transcriber/gui/constants.py (100%) rename application.py => wit_transcriber/gui/main_window.py (89%) rename preferences.py => wit_transcriber/gui/preferences.py (100%) rename settings.py => wit_transcriber/gui/settings.py (98%) diff --git a/.gitignore b/.gitignore index 0d2287d..84f3398 100644 --- a/.gitignore +++ b/.gitignore @@ -59,4 +59,5 @@ config.json *.txt *.mp* *.m4a -*.exe \ No newline at end of file +*.exe +last_run.log* \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 5a040e7..03f68ce 100644 --- a/poetry.lock +++ b/poetry.lock @@ -74,11 +74,11 @@ python-versions = ">=3.6.1" [[package]] name = "click" -version = "8.0.4" +version = "8.1.3" description = "Composable command line interface toolkit" -category = "dev" +category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} @@ -87,7 +87,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.4" description = "Cross-platform colored terminal text." -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -446,7 +446,7 @@ python-versions = ">=3.5" name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" @@ -487,7 +487,7 @@ testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", [metadata] lock-version = "1.1" python-versions = ">=3.10,<3.11" -content-hash = "d199c511810a53ce8b697e3375960f9e06065b9114c8958a5104c0eae6cc2298" +content-hash = "d98c2349403beaa533b5a0a1dfc58cd58671f78536c616779f85f230cc61afa3" [metadata.files] altgraph = [ @@ -536,8 +536,8 @@ cfgv = [ {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] click = [ - {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, - {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, diff --git a/pyinstaller/requirements.txt b/pyinstaller/requirements.txt index 8923ad1..145d39f 100644 --- a/pyinstaller/requirements.txt +++ b/pyinstaller/requirements.txt @@ -1,6 +1,8 @@ anyio==3.6.1; python_full_version >= "3.6.2" and python_version >= "3.7" awesometkinter==2021.11.8; python_version >= "3.6" certifi==2022.6.15; python_version >= "3.7" +click==8.1.3; python_version >= "3.7" +colorama==0.4.4; python_version >= "3.7" and python_full_version < "3.0.0" and platform_system == "Windows" or platform_system == "Windows" and python_version >= "3.7" and python_full_version >= "3.5.0" h11==0.12.0; python_version >= "3.7" httpcore==0.15.0; python_version >= "3.7" httpx==0.23.0; python_version >= "3.7" @@ -12,3 +14,4 @@ ratelimiter==1.2.0.post0 rfc3986==1.5.0; python_version >= "3.7" six==1.16.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" sniffio==1.2.0; python_full_version >= "3.6.2" and python_version >= "3.7" +toml==0.10.2; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") diff --git a/pyproject.toml b/pyproject.toml index 97cee41..b591c76 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,8 @@ httpx = "^0.23.0" pydub = "^0.25.1" ratelimiter = "^1.2.0" AwesomeTkinter = {version = "^2021.11.8", extras = ["gui"]} +click = "^8.1.3" +toml = "^0.10.2" [tool.poetry.dev-dependencies] black = "^22.6" @@ -24,12 +26,12 @@ requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.poetry.scripts] -wit_transcriber = 'wit_transcriber:main' -wit_transcriber_gui = 'application:main' +wit_transcriber = 'wit_transcriber.cli.app:transcribe' +wit_transcriber_gui = 'wit_transcriber.gui.app:gui' [tool.black] -include = '(droos_bot\/.*$|\.pyi?$)' +include = '(wit_transcriber\/.*$|\.pyi?$)' exclude = ''' /( \.git @@ -42,7 +44,7 @@ exclude = ''' profile = "black" [tool.mypy] -files = ["droos_bot"] +files = ["wit_transcriber"] ignore_missing_imports = true disallow_untyped_defs = true #disallow_any_unimported = true diff --git a/setup.py b/setup.py index 570288b..3e6ff9e 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ version=__version__, executables=[ cx_Freeze.Executable( - "application.py", + "wit_transcriber/gui/main_window.py", base=base, icon="main.ico", shortcutName="Transcribe Arabic", diff --git a/wit_transcriber/__init__.py b/wit_transcriber/__init__.py new file mode 100644 index 0000000..03ca00b --- /dev/null +++ b/wit_transcriber/__init__.py @@ -0,0 +1,43 @@ +"""Module initialization""" +import logging +from importlib import metadata +from logging.handlers import TimedRotatingFileHandler +from pathlib import Path +from sys import stderr, stdout + +import toml + +# Use __file__ so PyInstaller bundle can access files too +PKG_DIR = Path(__file__).absolute().parent +PARENT_DIR = PKG_DIR.parent + +# Set package version dynamically +try: + # In production use package metadata + __version__ = metadata.version(__package__) +except metadata.PackageNotFoundError: + # otherwise, read version from pyproject + + __version__ = toml.loads((PARENT_DIR / "pyproject.toml").read_text())["tool"][ + "poetry" + ]["version"] + + +CONFIG_FILE = PARENT_DIR / "config.json" +# Logging +LOG_FILE = PARENT_DIR / "last_run.log" +LOG_FORMAT = "%(asctime)s [%(levelname)s] %(name)s [%(module)s.%(funcName)s:%(lineno)d]: %(message)s" +FORMATTER: logging.Formatter = logging.Formatter(LOG_FORMAT) +handler = TimedRotatingFileHandler(LOG_FILE, when="d", interval=1, backupCount=3) +logging.basicConfig(filename=str(LOG_FILE), filemode="w", format=LOG_FORMAT) +OUT = logging.StreamHandler(stdout) +ERR = logging.StreamHandler(stderr) +OUT.setFormatter(FORMATTER) +ERR.setFormatter(FORMATTER) +OUT.setLevel(logging.INFO) +ERR.setLevel(logging.WARNING) +LOGGER = logging.getLogger() +LOGGER.addHandler(OUT) +LOGGER.addHandler(ERR) +LOGGER.addHandler(handler) +LOGGER.setLevel(logging.INFO) diff --git a/wit_transcriber/__main__.py b/wit_transcriber/__main__.py new file mode 100644 index 0000000..bf4c623 --- /dev/null +++ b/wit_transcriber/__main__.py @@ -0,0 +1,20 @@ +"""Entry Point.""" +import click + +from wit_transcriber.cli.app import transcribe +from wit_transcriber.gui.app import gui + + +@click.group() +def click_cli() -> None: + pass + + +def main() -> None: + click_cli.add_command(transcribe) + click_cli.add_command(gui) + click_cli() + + +if __name__ == "__main__": + main() diff --git a/wit_transcriber/api_client/__init__.py b/wit_transcriber/api_client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wit_transcriber.py b/wit_transcriber/api_client/client.py similarity index 71% rename from wit_transcriber.py rename to wit_transcriber/api_client/client.py index 4e5e32c..9f9d0e2 100644 --- a/wit_transcriber.py +++ b/wit_transcriber/api_client/client.py @@ -1,8 +1,7 @@ import json import re import traceback -from argparse import ArgumentParser -from asyncio import BoundedSemaphore, ensure_future, gather, run +from asyncio import BoundedSemaphore, ensure_future, gather from pathlib import Path from typing import List, Tuple @@ -46,7 +45,7 @@ def text(self) -> str: ) # strip extra white spaces from lines text = text.replace(".", ".\n").replace("\n ", "\n") - text = re.sub("[ ]{2,}", " ", text, re.M) + text = re.sub(" {2,}", " ", text, re.M) return text def has_api_key(self) -> bool: @@ -117,7 +116,7 @@ async def __preprocess_audio(audio: AudioSegment) -> AudioSegment: return audio.set_sample_width(2).set_channels(1).set_frame_rate(8000) async def __bound_fetch(self, chunk: AudioSegment, idx: int) -> Tuple[str, str]: - # Getter function with semaphore. + # Getter functions with semaphore. async with self._sem: return await self.__transcribe_chunk(chunk, idx) @@ -146,76 +145,3 @@ async def transcribe(self, path: Path) -> None: raise Exception( "`Error decoding the audio file.\nEnsure that the provided audio is a valid audio file!`" ) - - -async def transcribe( - file_path: Path, - output: Path, - semaphore: int, - config_file: Path, - verbose: bool = False, - lang: str = "ar", -) -> None: - """Speech to text using Wit.ai""" - api = WitAiAPI(lang, semaphore, config_file, verbose=verbose) - if not api.has_api_key(): - raise RuntimeError("Language API key was not found! Exitting!") - await api.transcribe(file_path) - Path(output).write_text(api.text, encoding="utf-8") - - -def main() -> None: - parser = ArgumentParser() - parser.add_argument( - "-i", - "--input", - help="Path of media file to be transcribed.", - required=True, - type=Path, - ) - parser.add_argument("-o", "--output", help="Path of output file.", type=Path) - parser.add_argument( - "-c", - "--config", - help="Path of config file.", - type=Path, - default=Path("config.json"), - ) - parser.add_argument( - "-x", - "--connections", - help="Number of API connections limit.", - type=int, - default=5, - ) - parser.add_argument( - "-l", - "--lang", - help="Language to use.", - type=str, - default="ar", - ) - parser.add_argument( - "-v", "--verbose", action="store_true", help="Print API responses." - ) - args = parser.parse_args() - if not args.input.exists(): - raise RuntimeError("Input file doesn't exist! Exitting!") - if not args.config.exists(): - raise RuntimeError("Config was not found! Exitting!") - - output_file = args.output if args.output else Path(f"{args.input.stem}.txt") - run( - transcribe( - args.input, - output_file, - args.connections, - args.config, - verbose=args.verbose, - lang=args.lang, - ) - ) - - -if __name__ == "__main__": - main() diff --git a/wit_transcriber/cli/__init__.py b/wit_transcriber/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wit_transcriber/cli/app.py b/wit_transcriber/cli/app.py new file mode 100644 index 0000000..85e71be --- /dev/null +++ b/wit_transcriber/cli/app.py @@ -0,0 +1,74 @@ +from asyncio import run +from pathlib import Path + +import click + +from wit_transcriber import CONFIG_FILE +from wit_transcriber.api_client.client import WitAiAPI + + +@click.command() +@click.option( + "-i", + "--input", + "file_path", + type=Path, + help="Path of media file to be transcribed.", + required=True, +) +@click.option("-o", "--output", type=Path, help="Path of output file.") +@click.option( + "-c", + "--config", + "config_file", + type=Path, + help="Path of config file.", + default=CONFIG_FILE, +) +@click.option( + "-x", "--connections", type=int, help="Number of API connections limit.", default=5 +) +@click.option("-l", "--lang", type=str, help="Language to use.", default="ar") +@click.option("-v", "--verbose", type=bool, help="Print API responses.", default=False) +def transcribe( + file_path: Path, + output: Path, + connections: int, + config_file: Path, + verbose: bool = False, + lang: str = "ar", +) -> None: + run(run_transcribe(file_path, output, connections, config_file, verbose, lang)) + + +async def run_transcribe( + file_path: Path, + output: Path, + connections: int, + config_file: Path, + verbose: bool, + lang: str, +) -> None: + """Speech to text using Wit.ai""" + if not file_path.exists(): + raise RuntimeError("Input file doesn't exist! Exiting!") + if not config_file.exists(): + raise RuntimeError("Config was not found! Exiting!") + output_file = output if output else Path(f"{file_path.stem}.txt") + + api_client = WitAiAPI(lang, connections, config_file, verbose=verbose) + if not api_client.has_api_key(): + raise RuntimeError("Language API key was not found! Exiting!") + await api_client.transcribe(file_path) + Path(output_file).write_text(api_client.text, encoding="utf-8") + print("Done Successfully!") + + +if __name__ == "__main__": + + @click.group() + def cli() -> None: + pass + + cli.add_command(transcribe) + cli() diff --git a/wit_transcriber/gui/__init__.py b/wit_transcriber/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wit_transcriber/gui/app.py b/wit_transcriber/gui/app.py new file mode 100644 index 0000000..4e9ba38 --- /dev/null +++ b/wit_transcriber/gui/app.py @@ -0,0 +1,20 @@ +from asyncio import run + +import click + +from wit_transcriber.gui.main_window import App + + +@click.command() +def gui() -> None: + run(App().exec()) + + +if __name__ == "__main__": + + @click.group() + def cli() -> None: + pass + + cli.add_command(gui) + cli() diff --git a/constants.py b/wit_transcriber/gui/constants.py similarity index 100% rename from constants.py rename to wit_transcriber/gui/constants.py diff --git a/application.py b/wit_transcriber/gui/main_window.py similarity index 89% rename from application.py rename to wit_transcriber/gui/main_window.py index 7e7e973..28bdd81 100644 --- a/application.py +++ b/wit_transcriber/gui/main_window.py @@ -1,17 +1,18 @@ -import asyncio import sys import tkinter as tk import tkinter.font as tkFont +from asyncio import create_task, get_event_loop, sleep from pathlib import Path from tkinter import StringVar, filedialog, messagebox from webbrowser import open_new_tab from awesometkinter.bidirender import render_text -import constants -import wit_transcriber -from preferences import PreferencesManager -from settings import SettingWindow +from wit_transcriber import PARENT_DIR +from wit_transcriber.api_client.client import WitAiAPI +from wit_transcriber.gui import constants +from wit_transcriber.gui.preferences import PreferencesManager +from wit_transcriber.gui.settings import SettingWindow class IORedirector: @@ -47,6 +48,7 @@ def __init__(self) -> None: self.parent: tk.Tk = tk.Tk() self.parent.protocol("WM_DELETE_WINDOW", self.on_closing) self.parent.title("أداة التفريغ الصوتي") + # self.parent.iconphoto(True, tk.PhotoImage(file=str(PARENT_DIR / "chat-centered-text-duotone.png"))) self.output_path = StringVar() self.input_path = StringVar() self.init_settings() @@ -110,7 +112,7 @@ def __init__(self) -> None: self.startTranscribe = tk.Button( self.parent, text=render_text(constants.SUBMIT_BUTTON), - command=lambda: asyncio.create_task(self.get_transcribe()), + command=lambda: create_task(self.get_transcribe()), ) self.startTranscribe.grid(row=4, column=0, pady=10, padx=10, columnspan=2) @@ -147,7 +149,7 @@ def init_settings(self) -> None: async def show(self) -> None: while True: self.parent.update() - await asyncio.sleep(0.1) + await sleep(0.1) def ask_for_output_path(self) -> None: output_path = filedialog.askdirectory() @@ -173,7 +175,7 @@ def open_win(self) -> None: def on_closing(self) -> None: self.parent.destroy() - asyncio.get_event_loop().stop() + get_event_loop().stop() async def get_transcribe(self) -> None: if not self.preference.check_if_ar_key_exists(): @@ -185,14 +187,11 @@ async def get_transcribe(self) -> None: output_path = Path(self.output_path.get() + f"/{file_path.stem}.txt") config_path = Path(self.preference.get_config_file()) try: - await wit_transcriber.transcribe( - file_path=file_path, - output=output_path, - semaphore=5, - config_file=config_path, - verbose=True and bool(self.verbose_checkbox_var.get()), - lang="ar", + api_client = WitAiAPI( + "ar", 5, config_path, bool(self.verbose_checkbox_var.get()) ) + await api_client.transcribe(file_path) + Path(output_path).write_text(api_client.text, encoding="utf-8") except: self.output_area.insert(tk.INSERT, "Error occurs! Please try again!") self.enable_entries() @@ -210,11 +209,3 @@ def enable_entries(self) -> None: async def exec(self) -> None: await self.show() - - -def main() -> None: - asyncio.run(App().exec()) - - -if __name__ == "__main__": - main() diff --git a/preferences.py b/wit_transcriber/gui/preferences.py similarity index 100% rename from preferences.py rename to wit_transcriber/gui/preferences.py diff --git a/settings.py b/wit_transcriber/gui/settings.py similarity index 98% rename from settings.py rename to wit_transcriber/gui/settings.py index 1389255..3f3665d 100644 --- a/settings.py +++ b/wit_transcriber/gui/settings.py @@ -4,7 +4,7 @@ from awesometkinter.bidirender import render_text -from preferences import PreferencesManager +from wit_transcriber.gui.preferences import PreferencesManager class SettingWindow: From d823b16cc2a9bd0690999fbcdfd7869a4ea82769 Mon Sep 17 00:00:00 2001 From: yshalsager Date: Fri, 5 Aug 2022 21:46:01 +0200 Subject: [PATCH 13/19] chore: update pyinstaller configuration Signed-off-by: yshalsager --- .github/workflows/ci.yml | 63 ++++++++++++++----- .pre-commit-config.yaml | 2 +- Makefile | 11 ++++ .../requirements.txt => requirements.txt | 0 ...t_transcriber.spec => wit_transcriber.spec | 6 +- wit_transcriber/cli/app.py | 2 +- wit_transcriber/gui/app.py | 2 +- wit_transcriber_gui.spec | 44 +++++++++++++ 8 files changed, 110 insertions(+), 20 deletions(-) create mode 100644 Makefile rename pyinstaller/requirements.txt => requirements.txt (100%) rename pyinstaller/wit_transcriber.spec => wit_transcriber.spec (89%) create mode 100644 wit_transcriber_gui.spec diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa9e878..0766b99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,26 +2,33 @@ name: Package Application with Pyinstaller on: push: - branches: [master] + branches: [ master ] + tags: [ v* ] pull_request: - branches: [master] + branches: [ master ] jobs: build: runs-on: ubuntu-latest if: ${{ !contains(github.event.head_commit.message, '(deps') }} - steps: - uses: actions/checkout@v2 - uses: hmarr/debug-action@v2 - - name: Package Application for Windows + - name: Package CLI Application for Windows + uses: yshalsager/pyinstaller-action-windows@main + with: + path: . + spec: wit_transcriber.spec + requirements: requirements.txt + + - name: Package GUI Application for Windows uses: yshalsager/pyinstaller-action-windows@main with: path: . - spec: pyinstaller/wit_transcriber.spec - requirements: pyinstaller/requirements.txt + spec: wit_transcriber_gui.spec + requirements: requirements.txt - uses: actions/upload-artifact@v2 if: github.actor != 'dependabot[bot]' @@ -29,12 +36,25 @@ jobs: name: wit_transcriber.exe path: dist/windows - - name: Package Application for Linux + - uses: actions/upload-artifact@v2 + if: github.actor != 'dependabot[bot]' + with: + name: wit_transcriber_gui.exe + path: dist/windows + + - name: Package CLI Application for Linux uses: yshalsager/pyinstaller-action-linux@main with: path: . - spec: pyinstaller/wit_transcriber.spec - requirements: pyinstaller/requirements.txt + spec: wit_transcriber.spec + requirements: requirements.txt + + - name: Package GUI Application for Windows + uses: yshalsager/pyinstaller-action-linux@main + with: + path: . + spec: wit_transcriber_gui.spec + requirements: requirements.txt - uses: actions/upload-artifact@v2 if: github.actor != 'dependabot[bot]' @@ -42,16 +62,31 @@ jobs: name: wit_transcriber path: dist/linux - - name: Get datetime - id: datetime - run: echo ::set-output name=datetime::$(date +'%Y%m%d-%H%M%S') + - uses: actions/upload-artifact@v2 + if: github.actor != 'dependabot[bot]' + with: + name: wit_transcriber_gui + path: dist/linux + + - name: Set release name to tag name or datetime + id: release + run: | + echo ${{ github.ref }} + ref='refs/tags/v' + if [[ ${{ github.ref }} == *${ref}* ]]; then + echo ::set-output name=version::${GITHUB_REF/refs\/tags\//} + else + echo ::set-output name=version::$(date +'%Y%m%d-%H%M%S') + fi - name: Release if: github.actor != 'dependabot[bot]' uses: softprops/action-gh-release@v1 with: - tag_name: ${{ steps.datetime.outputs.datetime }} - name: ${{ steps.datetime.outputs.datetime }} + tag_name: ${{ steps.release.outputs.version }} + name: ${{ steps.release.outputs.version }} files: | dist/windows/wit_transcriber.exe dist/linux/wit_transcriber + dist/windows/wit_transcriber_gui.exe + dist/linux/wit_transcriber_gui diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d84568b..8f45f44 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: hooks: - id: system name: Requirements - entry: poetry export --format=requirements.txt --without-hashes --output=pyinstaller/requirements.txt + entry: poetry export --format=requirements.txt --without-hashes --output=requirements.txt pass_filenames: false language: system diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..488078d --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +pyinstaller-cli: + pyinstaller -F wit_transcriber/cli/app.py \ + --clean -y \ + --add-data="pyproject.toml:." \ + -n wit_transcriber +pyinstaller-gui: + pyinstaller -F wit_transcriber/gui/app.py \ + --clean -y \ + --add-data="pyproject.toml:." \ + --windowed \ + -n wit_transcriber_gui diff --git a/pyinstaller/requirements.txt b/requirements.txt similarity index 100% rename from pyinstaller/requirements.txt rename to requirements.txt diff --git a/pyinstaller/wit_transcriber.spec b/wit_transcriber.spec similarity index 89% rename from pyinstaller/wit_transcriber.spec rename to wit_transcriber.spec index 852ae73..5a9a451 100644 --- a/pyinstaller/wit_transcriber.spec +++ b/wit_transcriber.spec @@ -5,13 +5,13 @@ block_cipher = None a = Analysis( - ['../wit_transcriber.py'], + ['wit_transcriber/cli/app.py'], pathex=[], binaries=[], - datas=[], + datas=[('pyproject.toml', '.')], hiddenimports=[], hookspath=[], - hooksconfig=None, + hooksconfig={}, runtime_hooks=[], excludes=[], win_no_prefer_redirects=False, diff --git a/wit_transcriber/cli/app.py b/wit_transcriber/cli/app.py index 85e71be..f11fea4 100644 --- a/wit_transcriber/cli/app.py +++ b/wit_transcriber/cli/app.py @@ -71,4 +71,4 @@ def cli() -> None: pass cli.add_command(transcribe) - cli() + transcribe() diff --git a/wit_transcriber/gui/app.py b/wit_transcriber/gui/app.py index 4e9ba38..b458093 100644 --- a/wit_transcriber/gui/app.py +++ b/wit_transcriber/gui/app.py @@ -17,4 +17,4 @@ def cli() -> None: pass cli.add_command(gui) - cli() + gui() diff --git a/wit_transcriber_gui.spec b/wit_transcriber_gui.spec new file mode 100644 index 0000000..d51f3f1 --- /dev/null +++ b/wit_transcriber_gui.spec @@ -0,0 +1,44 @@ +# -*- mode: python ; coding: utf-8 -*- + + +block_cipher = None + + +a = Analysis( + ['wit_transcriber/gui/app.py'], + pathex=[], + binaries=[], + datas=[('pyproject.toml', '.')], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='wit_transcriber_gui', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) From 7238b55c21f33b745b568db3b7beb593db777fdd Mon Sep 17 00:00:00 2001 From: yshalsager Date: Fri, 5 Aug 2022 21:47:43 +0200 Subject: [PATCH 14/19] fix(gui): correct input path initial dir Signed-off-by: yshalsager --- wit_transcriber/gui/main_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wit_transcriber/gui/main_window.py b/wit_transcriber/gui/main_window.py index 28bdd81..beff164 100644 --- a/wit_transcriber/gui/main_window.py +++ b/wit_transcriber/gui/main_window.py @@ -157,7 +157,7 @@ def ask_for_output_path(self) -> None: def ask_for_input_path(self) -> None: input_path = filedialog.askopenfilename( - initialdir="./", + initialdir="/", title=render_text(constants.INPUT_DIALOG_TITLE), filetypes=( ("Audio files", "*.mp3 *.wav *.m4a *.ogg"), From cae6119860c28f3fd53f1e5a7e3aff5b422e46a1 Mon Sep 17 00:00:00 2001 From: yshalsager Date: Fri, 5 Aug 2022 21:51:08 +0200 Subject: [PATCH 15/19] chore: update dependencies and bump version Signed-off-by: yshalsager --- poetry.lock | 115 +++++++++++++++++++++++------------------------ pyproject.toml | 4 +- requirements.txt | 2 +- 3 files changed, 60 insertions(+), 61 deletions(-) diff --git a/poetry.lock b/poetry.lock index 03f68ce..1f51ccc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -85,7 +85,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "colorama" -version = "0.4.4" +version = "0.4.5" description = "Cross-platform colored terminal text." category = "main" optional = false @@ -93,7 +93,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "distlib" -version = "0.3.4" +version = "0.3.5" description = "Distribution utilities" category = "dev" optional = false @@ -101,7 +101,7 @@ python-versions = "*" [[package]] name = "filelock" -version = "3.6.0" +version = "3.7.1" description = "A platform independent file lock." category = "dev" optional = false @@ -180,7 +180,7 @@ socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "identify" -version = "2.4.11" +version = "2.5.3" description = "File identification library for Python" category = "dev" optional = false @@ -213,7 +213,7 @@ plugins = ["setuptools"] [[package]] name = "macholib" -version = "1.15.2" +version = "1.16" description = "Mach-O header analysis and editing" category = "dev" optional = false @@ -258,11 +258,11 @@ python-versions = "*" [[package]] name = "nodeenv" -version = "1.6.0" +version = "1.7.0" description = "Node.js virtual environment builder" category = "dev" optional = false -python-versions = "*" +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" [[package]] name = "pathspec" @@ -297,15 +297,15 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa [[package]] name = "platformdirs" -version = "2.5.1" +version = "2.5.2" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] -test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] +test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] [[package]] name = "pre-commit" @@ -349,7 +349,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pyinstaller" -version = "5.2" +version = "5.3" description = "PyInstaller bundles a Python application and all its dependencies into a single package." category = "dev" optional = false @@ -368,7 +368,7 @@ hook_testing = ["pytest (>=2.7.3)", "execnet (>=1.5.0)", "psutil"] [[package]] name = "pyinstaller-hooks-contrib" -version = "2022.2" +version = "2022.8" description = "Community maintained hooks for PyInstaller" category = "dev" optional = false @@ -452,37 +452,36 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tomli" -version = "1.2.3" +version = "2.0.1" description = "A lil' TOML parser" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "typing-extensions" -version = "4.1.1" -description = "Backported and Experimental Type Hints for Python 3.6+" +version = "4.3.0" +description = "Backported and Experimental Type Hints for Python 3.7+" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "virtualenv" -version = "20.13.2" +version = "20.16.3" description = "Virtual Python Environment builder" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.6" [package.dependencies] -distlib = ">=0.3.1,<1" -filelock = ">=3.2,<4" -platformdirs = ">=2,<3" -six = ">=1.9.0,<2" +distlib = ">=0.3.5,<1" +filelock = ">=3.4.1,<4" +platformdirs = ">=2.4,<3" [package.extras] -docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] -testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] +docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"] +testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] [metadata] lock-version = "1.1" @@ -540,16 +539,16 @@ click = [ {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] colorama = [ - {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, + {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, + {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, ] distlib = [ - {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, - {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, + {file = "distlib-0.3.5-py2.py3-none-any.whl", hash = "sha256:b710088c59f06338ca514800ad795a132da19fda270e3ce4affc74abf955a26c"}, + {file = "distlib-0.3.5.tar.gz", hash = "sha256:a7f75737c70be3b25e2bee06288cec4e4c221de18455b2dd037fe2a795cab2fe"}, ] filelock = [ - {file = "filelock-3.6.0-py3-none-any.whl", hash = "sha256:f8314284bfffbdcfa0ff3d7992b023d4c628ced6feb957351d4c48d059f56bc0"}, - {file = "filelock-3.6.0.tar.gz", hash = "sha256:9cd540a9352e432c7246a48fe4e8712b10acb1df2ad1f30e8c070b82ae1fed85"}, + {file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"}, + {file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"}, ] flake8 = [ {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, @@ -571,8 +570,8 @@ httpx = [ {file = "httpx-0.23.0.tar.gz", hash = "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"}, ] identify = [ - {file = "identify-2.4.11-py2.py3-none-any.whl", hash = "sha256:fd906823ed1db23c7a48f9b176a1d71cb8abede1e21ebe614bac7bdd688d9213"}, - {file = "identify-2.4.11.tar.gz", hash = "sha256:2986942d3974c8f2e5019a190523b0b0e2a07cb8e89bf236727fb4b26f27f8fd"}, + {file = "identify-2.5.3-py2.py3-none-any.whl", hash = "sha256:25851c8c1370effb22aaa3c987b30449e9ff0cece408f810ae6ce408fdd20893"}, + {file = "identify-2.5.3.tar.gz", hash = "sha256:887e7b91a1be152b0d46bbf072130235a8117392b9f1828446079a816a05ef44"}, ] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, @@ -583,8 +582,8 @@ isort = [ {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] macholib = [ - {file = "macholib-1.15.2-py2.py3-none-any.whl", hash = "sha256:885613dd02d3e26dbd2b541eb4cc4ce611b841f827c0958ab98656e478b9e6f6"}, - {file = "macholib-1.15.2.tar.gz", hash = "sha256:1542c41da3600509f91c165cb897e7e54c0e74008bd8da5da7ebbee519d593d2"}, + {file = "macholib-1.16-py2.py3-none-any.whl", hash = "sha256:5a0742b587e6e57bfade1ab90651d4877185bf66fd4a176a488116de36878229"}, + {file = "macholib-1.16.tar.gz", hash = "sha256:001bf281279b986a66d7821790d734e61150d52f40c080899df8fefae056e9f7"}, ] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, @@ -620,8 +619,8 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] nodeenv = [ - {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, - {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, + {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, + {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, ] pathspec = [ {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, @@ -691,8 +690,8 @@ pillow = [ {file = "Pillow-9.2.0.tar.gz", hash = "sha256:75e636fd3e0fb872693f23ccb8a5ff2cd578801251f3a4f6854c6a5d437d3c04"}, ] platformdirs = [ - {file = "platformdirs-2.5.1-py3-none-any.whl", hash = "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"}, - {file = "platformdirs-2.5.1.tar.gz", hash = "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d"}, + {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, + {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, ] pre-commit = [ {file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"}, @@ -711,21 +710,21 @@ pyflakes = [ {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, ] pyinstaller = [ - {file = "pyinstaller-5.2-py3-none-macosx_10_13_universal2.whl", hash = "sha256:f10b19ad4f66ccad16574ff1979cc15e1ea010f8577292500125dd45abcd8303"}, - {file = "pyinstaller-5.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:1061f7a6de055007949ec9ad1c6a080b93e102b2a288c8ff88f65e5d7716d4aa"}, - {file = "pyinstaller-5.2-py3-none-manylinux2014_i686.whl", hash = "sha256:6ecce857491bc4f477fcbde1b430d63b957d99bc511fa7e79136c07f831fc505"}, - {file = "pyinstaller-5.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:dda3a4787fa4498bb9e688f81bed918f061bd583c8ff0e47881a5422a4b2093b"}, - {file = "pyinstaller-5.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:9d44f331f96fa0ef556cf5304f8b906ca20f55503ddd7aa2a914e87bc58e2cc9"}, - {file = "pyinstaller-5.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:03984eed0baa252ea9854eb0785a1c40ac033c5c28d3abdae7d820da734aed5a"}, - {file = "pyinstaller-5.2-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:a21c07dd026bc127684e7a451320bf59cac2c85bea4cb412f7193876ad741d58"}, - {file = "pyinstaller-5.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:60c53ba54651f22f75dce2bcf49f0a1bcd03c729ced15f2efabad28e0ea0c938"}, - {file = "pyinstaller-5.2-py3-none-win32.whl", hash = "sha256:4def5b6433b4233b2b53ef92ac351788edb11e4d1e08123b9c90e21a7b310a41"}, - {file = "pyinstaller-5.2-py3-none-win_amd64.whl", hash = "sha256:5d170f7c4c402c820b4c5cf7fde61dd9741bf0456342b19b207e29041d75aa30"}, - {file = "pyinstaller-5.2.tar.gz", hash = "sha256:5efc1b3ffb13fe50a51305fe57fb9e6e7bce00d009c16dd3cb76ea4d702a04ab"}, + {file = "pyinstaller-5.3-py3-none-macosx_10_13_universal2.whl", hash = "sha256:7591a9e1e2a481f99eb99036d6786e20717bc10f8f0a8ef519958cb3172fac7a"}, + {file = "pyinstaller-5.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:d4123992556951ed24e11cf2eec9a4e18e94ee8bd63ca49d9b7fc37387097eb9"}, + {file = "pyinstaller-5.3-py3-none-manylinux2014_i686.whl", hash = "sha256:066b83a0eae89ad418749e9e29429c152f1ff096230df11a093bbded8344ade0"}, + {file = "pyinstaller-5.3-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:4c658a762cbbee5c5997c364578804d4c1e91d688de8ed018710c2705bf1474b"}, + {file = "pyinstaller-5.3-py3-none-manylinux2014_s390x.whl", hash = "sha256:a0e7a80fe04204add3f743101958a3cf62b79e7ccda838388784b1a35bb5b27f"}, + {file = "pyinstaller-5.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:aa9d1b8639d2402438c179ae1c8acfd41b65366c803a5a6484a5bb7586e88647"}, + {file = "pyinstaller-5.3-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:794e8e143ae73d1acdd2cbc52f02dd34cdfbd954ede34c7067ce68a268d8b7c2"}, + {file = "pyinstaller-5.3-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:9efbad718fe29d425336f289871c67bfc6a1876013037fee2ef1f7613fd675a2"}, + {file = "pyinstaller-5.3-py3-none-win32.whl", hash = "sha256:cae43e01e04f37185d23202aba8cf2837fa24ec3d0aa5ebc42e26f404e6eba95"}, + {file = "pyinstaller-5.3-py3-none-win_amd64.whl", hash = "sha256:b38505b445cdd64279f04650e0ddfe5ac6cef61996b14f06e3c99da8aac3cfbe"}, + {file = "pyinstaller-5.3.tar.gz", hash = "sha256:de71d4669806e4d54b23b477cc077e2e8fe9c4d57e79ed32d22b7585137fd7b7"}, ] pyinstaller-hooks-contrib = [ - {file = "pyinstaller-hooks-contrib-2022.2.tar.gz", hash = "sha256:ab1d14fe053016fff7b0c6aea51d980bac6d02114b04063b46ef7dac70c70e1e"}, - {file = "pyinstaller_hooks_contrib-2022.2-py2.py3-none-any.whl", hash = "sha256:7605e440ccb55904cb2a87d72e83642ef176fb7030c77e52ac4d9679bb3d1537"}, + {file = "pyinstaller-hooks-contrib-2022.8.tar.gz", hash = "sha256:c4210fc50282c9c6a918e485e0bfae9405592390508e3be9fde19acc2213da56"}, + {file = "pyinstaller_hooks_contrib-2022.8-py2.py3-none-any.whl", hash = "sha256:e46f099934dd4577fb1ddcf37a99fa04027c92f8f5291c8802f326345988d001"}, ] python-bidi = [ {file = "python-bidi-0.4.2.tar.gz", hash = "sha256:5347f71e82b3e9976dc657f09ded2bfe39ba8d6777ca81a5b2c56c30121c496e"}, @@ -791,14 +790,14 @@ toml = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] tomli = [ - {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, - {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] typing-extensions = [ - {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, - {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, + {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, + {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, ] virtualenv = [ - {file = "virtualenv-20.13.2-py2.py3-none-any.whl", hash = "sha256:e7b34c9474e6476ee208c43a4d9ac1510b041c68347eabfe9a9ea0c86aa0a46b"}, - {file = "virtualenv-20.13.2.tar.gz", hash = "sha256:01f5f80744d24a3743ce61858123488e91cb2dd1d3bdf92adaf1bba39ffdedf0"}, + {file = "virtualenv-20.16.3-py2.py3-none-any.whl", hash = "sha256:4193b7bc8a6cd23e4eb251ac64f29b4398ab2c233531e66e40b19a6b7b0d30c1"}, + {file = "virtualenv-20.16.3.tar.gz", hash = "sha256:d86ea0bb50e06252d79e6c241507cb904fcd66090c3271381372d6221a3970f9"}, ] diff --git a/pyproject.toml b/pyproject.toml index b591c76..d0999cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "wit_transcriber" -version = "0.1.0" -description = "A mini command line tool to transcribe media files using wit.ai" +version = "0.2.0" +description = "A tool to transcribe media files using wit.ai" authors = ["yshalsager "] [tool.poetry.dependencies] diff --git a/requirements.txt b/requirements.txt index 145d39f..ec33aad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ anyio==3.6.1; python_full_version >= "3.6.2" and python_version >= "3.7" awesometkinter==2021.11.8; python_version >= "3.6" certifi==2022.6.15; python_version >= "3.7" click==8.1.3; python_version >= "3.7" -colorama==0.4.4; python_version >= "3.7" and python_full_version < "3.0.0" and platform_system == "Windows" or platform_system == "Windows" and python_version >= "3.7" and python_full_version >= "3.5.0" +colorama==0.4.5; python_version >= "3.7" and python_full_version < "3.0.0" and platform_system == "Windows" or platform_system == "Windows" and python_version >= "3.7" and python_full_version >= "3.5.0" h11==0.12.0; python_version >= "3.7" httpcore==0.15.0; python_version >= "3.7" httpx==0.23.0; python_version >= "3.7" From 9a8769178fd7a1b7d17a2765253b16dfe97951eb Mon Sep 17 00:00:00 2001 From: yshalsager Date: Fri, 5 Aug 2022 22:00:33 +0200 Subject: [PATCH 16/19] fix(ci): Don't release on pull requests Signed-off-by: yshalsager --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0766b99..15aa74d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,7 +80,7 @@ jobs: fi - name: Release - if: github.actor != 'dependabot[bot]' + if: github.actor != 'dependabot[bot]' && github.event_name != 'pull_request' uses: softprops/action-gh-release@v1 with: tag_name: ${{ steps.release.outputs.version }} From d6ae0aa09a78e0548b743621951e6a793dc2fcfa Mon Sep 17 00:00:00 2001 From: yshalsager Date: Fri, 5 Aug 2022 22:28:27 +0200 Subject: [PATCH 17/19] refactor(gui): move ui init to separate function Signed-off-by: yshalsager --- wit_transcriber/gui/main_window.py | 53 +++++++++++++++--------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/wit_transcriber/gui/main_window.py b/wit_transcriber/gui/main_window.py index beff164..dfffbdc 100644 --- a/wit_transcriber/gui/main_window.py +++ b/wit_transcriber/gui/main_window.py @@ -57,6 +57,33 @@ def __init__(self) -> None: self.default_font.configure(family="Tajawal", size=10) self.menu = tk.Menu(self.parent) + self.label = tk.Label( + self.parent, text=render_text("wit.ai أداة للتفريغ الصوتي باستخدام") + ) + self.input_entry = tk.Entry(self.parent, textvariable=self.input_path, width=60) + self.output_entry = tk.Entry( + self.parent, textvariable=self.output_path, width=60 + ) + self.startTranscribe = tk.Button( + self.parent, + text=render_text(constants.SUBMIT_BUTTON), + command=lambda: create_task(self.get_transcribe()), + ) + self.scrollbar = tk.Scrollbar(self.parent, orient=tk.VERTICAL) + self.output_area = tk.Text( + self.parent, + height=5, + width=25, + bg="light gray", + yscrollcommand=self.scrollbar.set, + ) + self.verbose_checkbox_var = tk.IntVar() + self.init_ui() + + def init_settings(self) -> None: + self.output_path.set(str(Path().absolute())) + + def init_ui(self) -> None: file_menu = tk.Menu(self.menu, tearoff=0) file_menu.add_command( label=render_text(constants.MENU_BAR_FILE_NEW), @@ -84,17 +111,8 @@ def __init__(self) -> None: ) self.parent.config(menu=self.menu) - self.label = tk.Label( - self.parent, text=render_text("wit.ai أداة للتفريغ الصوتي باستخدام") - ) self.label.grid(row=0, column=0, pady=10, sticky="w,e") - - self.input_entry = tk.Entry(self.parent, textvariable=self.input_path, width=60) self.input_entry.grid(row=1, column=0, pady=10, padx=10) - - self.output_entry = tk.Entry( - self.parent, textvariable=self.output_path, width=60 - ) self.output_entry.grid(row=3, column=0, pady=10, padx=10) tk.Button( @@ -109,28 +127,14 @@ def __init__(self) -> None: command=self.ask_for_output_path, ).grid(row=3, column=1, pady=10, padx=10) - self.startTranscribe = tk.Button( - self.parent, - text=render_text(constants.SUBMIT_BUTTON), - command=lambda: create_task(self.get_transcribe()), - ) self.startTranscribe.grid(row=4, column=0, pady=10, padx=10, columnspan=2) - self.scrollbar = tk.Scrollbar(self.parent, orient=tk.VERTICAL) - self.output_area = tk.Text( - self.parent, - height=5, - width=25, - bg="light gray", - yscrollcommand=self.scrollbar.set, - ) self.scrollbar.config(command=self.output_area.yview) self.output_area.grid(row=5, column=0, sticky="wes", padx=10, pady=10) self.scrollbar.grid(row=5, column=0, sticky="nse", padx=10, pady=10) sys.stdout = StdoutRedirector(self.output_area) # type: ignore - self.verbose_checkbox_var = tk.IntVar() # print(self.verbose_checkbox_var) verbose_checkbox = tk.Checkbutton( self.parent, @@ -142,9 +146,6 @@ def __init__(self) -> None: ) verbose_checkbox.grid(row=6, column=0, sticky="w", padx=10, pady=10) - def init_settings(self) -> None: - self.output_path.set(str(Path().absolute())) - # TODO [Improvement] edit to handle onClosing and stop asyncio loop async def show(self) -> None: while True: From 71cb77de3e123de98fdd88d1c654b39633a7a57f Mon Sep 17 00:00:00 2001 From: yshalsager Date: Fri, 5 Aug 2022 22:46:27 +0200 Subject: [PATCH 18/19] fix(rtl): render text in correct direction based on OS Signed-off-by: yshalsager --- wit_transcriber/gui/main_window.py | 45 +++++++++++++++++------------- wit_transcriber/gui/settings.py | 24 ++++++++-------- wit_transcriber/gui/utils.py | 6 ++++ 3 files changed, 43 insertions(+), 32 deletions(-) create mode 100644 wit_transcriber/gui/utils.py diff --git a/wit_transcriber/gui/main_window.py b/wit_transcriber/gui/main_window.py index dfffbdc..37b5169 100644 --- a/wit_transcriber/gui/main_window.py +++ b/wit_transcriber/gui/main_window.py @@ -3,16 +3,16 @@ import tkinter.font as tkFont from asyncio import create_task, get_event_loop, sleep from pathlib import Path +from platform import system from tkinter import StringVar, filedialog, messagebox from webbrowser import open_new_tab -from awesometkinter.bidirender import render_text - from wit_transcriber import PARENT_DIR from wit_transcriber.api_client.client import WitAiAPI from wit_transcriber.gui import constants from wit_transcriber.gui.preferences import PreferencesManager from wit_transcriber.gui.settings import SettingWindow +from wit_transcriber.gui.utils import _text class IORedirector: @@ -30,7 +30,7 @@ def __init__(self, text_area: tk.Text): super().__init__(text_area) def write(self, string: str) -> None: - self.text_area.insert("end", render_text(string)) + self.text_area.insert("end", _text(system(), string)) self.text_area.see("end") def flush(self) -> None: @@ -45,6 +45,7 @@ def flush(self) -> None: class App: def __init__(self) -> None: + self._platform = system() self.parent: tk.Tk = tk.Tk() self.parent.protocol("WM_DELETE_WINDOW", self.on_closing) self.parent.title("أداة التفريغ الصوتي") @@ -58,7 +59,7 @@ def __init__(self) -> None: self.menu = tk.Menu(self.parent) self.label = tk.Label( - self.parent, text=render_text("wit.ai أداة للتفريغ الصوتي باستخدام") + self.parent, text=self.render_text("wit.ai أداة للتفريغ الصوتي باستخدام") ) self.input_entry = tk.Entry(self.parent, textvariable=self.input_path, width=60) self.output_entry = tk.Entry( @@ -66,7 +67,7 @@ def __init__(self) -> None: ) self.startTranscribe = tk.Button( self.parent, - text=render_text(constants.SUBMIT_BUTTON), + text=self.render_text(constants.SUBMIT_BUTTON), command=lambda: create_task(self.get_transcribe()), ) self.scrollbar = tk.Scrollbar(self.parent, orient=tk.VERTICAL) @@ -86,28 +87,30 @@ def init_settings(self) -> None: def init_ui(self) -> None: file_menu = tk.Menu(self.menu, tearoff=0) file_menu.add_command( - label=render_text(constants.MENU_BAR_FILE_NEW), + label=self.render_text(constants.MENU_BAR_FILE_NEW), command=self.ask_for_input_path, ) file_menu.add_command( - label=render_text(constants.MENU_BAR_FILE_SETTINGS), command=self.open_win + label=self.render_text(constants.MENU_BAR_FILE_SETTINGS), + command=self.open_win, ) file_menu.add_separator() file_menu.add_command( - label=render_text(constants.MENU_BAR_FILE_EXIT), command=self.on_closing + label=self.render_text(constants.MENU_BAR_FILE_EXIT), + command=self.on_closing, ) help_menu = tk.Menu(self.menu, tearoff=0) help_menu.add_command( - label=render_text(constants.MENU_BAR_ABOUT), + label=self.render_text(constants.MENU_BAR_ABOUT), command=lambda: open_new_tab( "https://github.com/yshalsager/wit_transcriber" ), ) self.menu.add_cascade( - label=render_text(constants.MENU_BAR_FILE), menu=file_menu + label=self.render_text(constants.MENU_BAR_FILE), menu=file_menu ) self.menu.add_cascade( - label=render_text(constants.MENU_BAR_HELP), menu=help_menu + label=self.render_text(constants.MENU_BAR_HELP), menu=help_menu ) self.parent.config(menu=self.menu) @@ -117,13 +120,13 @@ def init_ui(self) -> None: tk.Button( self.parent, - text=render_text(constants.INPUT_BUTTON_TITLE), + text=self.render_text(constants.INPUT_BUTTON_TITLE), command=self.ask_for_input_path, ).grid(row=1, column=1, pady=10, padx=10) tk.Button( self.parent, - text=render_text(constants.OUTPUT_BUTTON_TITLE), + text=self.render_text(constants.OUTPUT_BUTTON_TITLE), command=self.ask_for_output_path, ).grid(row=3, column=1, pady=10, padx=10) @@ -140,7 +143,7 @@ def init_ui(self) -> None: self.parent, variable=self.verbose_checkbox_var, justify="center", - text=render_text("إظهار النتائج"), + text=self.render_text("إظهار النتائج"), offvalue=0, onvalue=1, ) @@ -159,7 +162,7 @@ def ask_for_output_path(self) -> None: def ask_for_input_path(self) -> None: input_path = filedialog.askopenfilename( initialdir="/", - title=render_text(constants.INPUT_DIALOG_TITLE), + title=self.render_text(constants.INPUT_DIALOG_TITLE), filetypes=( ("Audio files", "*.mp3 *.wav *.m4a *.ogg"), ("all files", "*.*"), @@ -167,12 +170,11 @@ def ask_for_input_path(self) -> None: ) self.input_path.set(input_path) - @staticmethod - def on_error_occurs(error_msg: str) -> None: - messagebox.showerror("خطأ", error_msg) + def on_error_occurs(self, error_msg: str) -> None: + messagebox.showerror(self.render_text("خطأ"), error_msg) def open_win(self) -> None: - SettingWindow(self.parent, self.preference) + SettingWindow(self.parent, self.preference, self.render_text) def on_closing(self) -> None: self.parent.destroy() @@ -180,7 +182,7 @@ def on_closing(self) -> None: async def get_transcribe(self) -> None: if not self.preference.check_if_ar_key_exists(): - self.on_error_occurs(render_text(constants.ERROR_API_KEY)) + self.on_error_occurs(self.render_text(constants.ERROR_API_KEY)) self.disable_entries() self.output_area.insert(tk.INSERT, "Please wait....\n") @@ -210,3 +212,6 @@ def enable_entries(self) -> None: async def exec(self) -> None: await self.show() + + def render_text(self, text: str) -> str: + return _text(self._platform, text) diff --git a/wit_transcriber/gui/settings.py b/wit_transcriber/gui/settings.py index 3f3665d..5d7b840 100644 --- a/wit_transcriber/gui/settings.py +++ b/wit_transcriber/gui/settings.py @@ -1,8 +1,7 @@ import tkinter as tk import tkinter.font as tkFont from tkinter import messagebox - -from awesometkinter.bidirender import render_text +from typing import Callable from wit_transcriber.gui.preferences import PreferencesManager @@ -12,10 +11,12 @@ class SettingWindow: Window is designed by using https://visualtk.com/ """ - def __init__(self, parent: tk.Tk, preferences: PreferencesManager) -> None: + def __init__( + self, parent: tk.Tk, preferences: PreferencesManager, render_text: Callable + ) -> None: self.parent: tk.Tk = parent self.preferences: PreferencesManager = preferences - + self.render_text = render_text self.window = tk.Toplevel(self.parent) # window.geometry("400x250") # setting window size @@ -34,7 +35,7 @@ def __init__(self, parent: tk.Tk, preferences: PreferencesManager) -> None: font=ft, fg="#333333", justify="center", - text=render_text("إعدادات البرنامج"), + text=self.render_text("إعدادات البرنامج"), ) setting_main_title.place(x=140, y=20, width=200, height=25) @@ -66,7 +67,7 @@ def __init__(self, parent: tk.Tk, preferences: PreferencesManager) -> None: font=ft, fg="#333333", justify="left", - text=render_text("اللغة العربية"), + text=self.render_text("اللغة العربية"), ) ar_lang_label.place(x=30, y=50, width=70, height=25) @@ -75,7 +76,7 @@ def __init__(self, parent: tk.Tk, preferences: PreferencesManager) -> None: font=ft, fg="#333333", justify="center", - text=render_text("مفتاح التفعيل"), + text=self.render_text("مفتاح التفعيل"), ) ar_api_key_label.place(x=170, y=50, width=100, height=25) @@ -85,18 +86,17 @@ def __init__(self, parent: tk.Tk, preferences: PreferencesManager) -> None: fg="#000000", font=ft, justify="center", - text=render_text("حفظ"), + text=self.render_text("حفظ"), command=self.save_settings, ) save_btn.place(x=160, y=250, width=70, height=25) def save_settings(self) -> None: self.preferences.put("ar", self.ar_api_key_entry_str_var.get()) - self.show_info(render_text("تم حفظ الإعدادات بنجاح!")) + self.show_info(self.render_text("تم حفظ الإعدادات بنجاح!")) - @staticmethod - def show_info(msg: str) -> None: - messagebox.showinfo(render_text("إعدادات"), msg) + def show_info(self, msg: str) -> None: + messagebox.showinfo(self.render_text("إعدادات"), msg) def load_preference_settings(self) -> None: self.ar_api_key_entry_str_var.set(self.preferences.get("ar")) diff --git a/wit_transcriber/gui/utils.py b/wit_transcriber/gui/utils.py new file mode 100644 index 0000000..1149f94 --- /dev/null +++ b/wit_transcriber/gui/utils.py @@ -0,0 +1,6 @@ +from awesometkinter.bidirender import render_text + + +def _text(platform: str, text: str) -> str: + """A helper function to get text in correct direction based on OS.""" + return render_text(text) if platform != "Windows" else text From c9538295480182a7a388c744bcf019c6df7d6a7c Mon Sep 17 00:00:00 2001 From: yshalsager Date: Fri, 5 Aug 2022 22:52:56 +0200 Subject: [PATCH 19/19] fix(ci): Use tkinter branch for linux's gui step action. Signed-off-by: yshalsager --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15aa74d..6d0e120 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,12 +49,13 @@ jobs: spec: wit_transcriber.spec requirements: requirements.txt - - name: Package GUI Application for Windows - uses: yshalsager/pyinstaller-action-linux@main + - name: Package GUI Application for Linux + uses: yshalsager/pyinstaller-action-linux@tkinter with: path: . spec: wit_transcriber_gui.spec requirements: requirements.txt + tkinter: true - uses: actions/upload-artifact@v2 if: github.actor != 'dependabot[bot]'