From 794c71088a74fbfe64e0b04cd0c45ad35739b660 Mon Sep 17 00:00:00 2001 From: Andrew Ward Date: Tue, 25 Feb 2025 14:25:33 +0000 Subject: [PATCH] refactor code into smaller subclasses --- .../api_key_manager.cpython-312.pyc | Bin 0 -> 5132 bytes .../hotkey_manager.cpython-312.pyc | Bin 0 -> 10047 bytes .../resource_utils.cpython-312.pyc | Bin 0 -> 1735 bytes utils/__pycache__/text_to_mic.cpython-312.pyc | Bin 76250 -> 68465 bytes utils/api_key_manager.py | 89 +++ utils/hotkey_manager.py | 133 +++++ utils/resource_utils.py | 30 + utils/text_to_mic.py | 542 +++++++----------- 8 files changed, 451 insertions(+), 343 deletions(-) create mode 100644 utils/__pycache__/api_key_manager.cpython-312.pyc create mode 100644 utils/__pycache__/hotkey_manager.cpython-312.pyc create mode 100644 utils/__pycache__/resource_utils.cpython-312.pyc create mode 100644 utils/api_key_manager.py create mode 100644 utils/hotkey_manager.py create mode 100644 utils/resource_utils.py diff --git a/utils/__pycache__/api_key_manager.cpython-312.pyc b/utils/__pycache__/api_key_manager.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..031861690c84427079fe6c94d75dfc4e46022688 GIT binary patch literal 5132 zcmbVQT}&HS7QQoMd&Xd}!NfMkz%--{PJkrsCIQkeC6J%~gwQ4Z31w?Jo&gO0>zyIQ zu4$@ORwdd>p)X6Ts49yf~w0&7=Y4)MgzSxBdGNrqfb|1EHhB4jjoWkwmO zvk^ARjdBdOaS>D0JZg?wMlE{X6yc-7sK5}GoFkI?29YeEvbhmQtx3kue}|PJKckt3 zgX*N8)vBVhq68=8EAeZZO$kR65m^ccBk>8asgA^hQb3BUa_nj-H%d-&`N~5I$|8{o zPHdEwm{CqPN$d^sY2IMe40W!gZh^X~q|QSfrdhBTpl+4;8%&|a3N1p3MOD(`uhMGz zho5^vPF@JcVD2>4H4q6Zim1lL$zV*1$RgYpr{$y=)eWL@Ocmn^nFiHxJf?J&Ee78> zF6iNuz8ICs&z(%=-<(m$t1+!Pl7^j0RTJ0`U2-Vrm^3qGF163;5-j?T+1Lq&q?r-o zx115V| zfp~{JvJgkZt>n$*@7|g>Z8o(o`Lj)jGhGAOrh&}aH?mD{WCYKq;MovbvO>#}x*}zS zmUZFOroA;Iv_6C(NT`4Ws94~yqvY?PED{xDu1sNTMKMrPiVz(ehIg0PPw^#{yee@L zcgIx3i)uA&(u`EV(Qz`zrr9aLU1gChX>yxL{72lU`85DL2>2_GnH1W{F`_WDEJH36 z27XE58h4q@GJb2yI}*Gq>(DeDDIPO`XbHt*!-kVW%_KXV04R)Mt}f=h-h@S?9%hVN-D2 zvfs3C2+dicc}ZHilo6WOh2E{2x`p-+2G(lCZNgZeTI%1l)!aIF^V~vwX=3T}@|8@( zv31*%I~?5GL4*4!b`={uZPn!JZ{t?m%>5mF8G`Ul=m>fi_GFP*Ns(zJ#@HtvC3_%B zVkPztSKK|ofk|f^RxR*Nnk~Zym*)1w*3ZC33EtQ{R%32}5VjIC@40fAX(Os0(_oF{ z0HGaoCbb52EK^7<6-BC&s#V-qy`)XXGj5ZQ*y1WL18V0?Z<~^~+;x_jC3%e2r2Id? zD9}H$X%3%95T4?AI3jn^eejeVQmG46>PF!~(Fg*=VRbIb<)CLiLus7udd>!+w}epz zv1P=RUh%BosuQRVW!i+}_)s86Xx1~=LUID*3`K9(tjc73R>lD}PEo1fOq;Q}DmTjj zP%mx4&aK$W$O>*JfZl+;trXTN?~g;?3;mQO5buy}OO3U0tFifm$=faS&u`jX8#Z6o z=3Bb7GMKUX)@?oCx*uCQn{^+WKfhJCZ((wYTdO+&K(9Hu%xpU97Ww!24~{G!Ty9+1 zpYa@DcbovA*PMJLkQ&d@`LAr9n@;ymHMHnmzJFvRj>ZRsg-7C(#H>wRa%VaQ|2pzT z-Q8FIaz1nD!n$o}%jx-k+YCm#N*7G>H%I#0$sb((K5mt70Cm-8?th$LeT+xF-Fmv2 zTRpb_w1@lLV+K70Jqo43#O=Bt{U1yWv3sv5ZLl^SQwy97QNG}Hz_$?J2g$FWg-DOg zTVUnF+=x^_vckN#Z*O)so-;q=NO@m`Y;JE(*BwN64UM5%Z8?+gGS7U6QDn%^m$JE@Oei|~wJJpoQdOA^tCN}; zKpsxO1yULnGL0g*O_6X+t{`grgxe7 z+Sj}=xan?My#D_6%)ygixw|)8+dfWxn7W~uRYNrQ@^MlX9M2Zl9*S$Q{2=x@Ot_JE>c#}E}mc5Xa} zp%t%?teDaS(%XF4!g1;A5Z(%b>1WK@RWfI1aco0N7Xl#wqz9K`HBK*l~qOd)=Qc-9QX&JtZI&Zo5d$cx>gd* zs#6z?qkxfbjnjBkga{U<@tA&mxf-P5;FSoZ*l;F@#6!V|9^5*xngHWO?kF*%$h3=g z!dx`VV35v+W4k<#35TVW_e@NcshEtLJxo#@;fv(u4ooW)nOT$kcb3T zIL<^hi;`4SIhx~=<2cbe43U9w447jkq@U0zVn?$Em1$&?*aQ`!P7k5bgQ|c%!Z8@{ zacp}6#X%6jH@FNv#8N+K<#bb>J2&VlWU8?|mX_Q)i5>q90!T(6&V7si_x%}P$69^o zJb%yb+OoTEUB7vK%`PsT|I*&Ei=^=YwKgMCXFf1ANLk0N(*7%1$s{v>Upm8go7nlV>ZC-AjA$zTI$u zmvi{0z0isW8C*Dh76r0v5sS1Rs(IXLJdBHnT8+=UMLGUzL;AFgQ%uTN4#D!f_yI_H z9)uq1oUI%6?b-VF<;Kh}E@obPW4%5AV63j+aAjT(5eD~Xk40!pl%BU-kb?W@^|Q?!$^`k&E#0fk~}$Q&H~^2u@;km6ko->1eM~yV^=SgPn6ai*!9*|L`w`-gU2k?wL(!m>fS$(d1?vbKT*X_v{iVt0}59Nfe^?jF-XFf ziV_plAT{9{bdhE|JxEi;0YY${Cj|N?WzHOQC&_^O0jl%{D8r?yrw#vvs3`oHI3k@5 znWN0ynnFA}3H2+4h!+~91aiRQdQehNi^;=$47MVtn!BUCqHwCjjq@=fDstnvo0HDA^k%i6n7@#Mi;c<3_0>s)=~eTu*^Zq7^YZ zD*Irzf>zqI!zIx337Z~{gv!{%ba3G?u6@ua~7gW*_M4F*%omt0Jn-Oix>)IfHQ zn6Fx%rr%@c88*kP(wS9r%<4zbL4-(@Tgd}od_kkodI7G;N2vM!Na0ml6Be&$mWZI) zMJa-Aq$t^QW=MfNPs~sSD`28tI-DY=scBN6RzkDjLitT=M`>b|1iE5uvLy@m`YcH9 z#~^wBOi13xATfoIlD;*BYO^HKLdd{+1H^k*r--wz0b+^__)|@NqRJ)W7WFFJ2(O3& zC&f4*GBq3<11ioV0nU%Exe9(Zf0L2IUg$tc&C{|#OcVAM*;m&`1gPF0$c>0_BT_hK zct*ofRg?py;SNa?Ba(tZaxx3K!Nz%65iMFzZC*-gOZcFzD=LNfsPal%;gsb7L;)dX z65*8{mv?;d`nAsMHJR1znW_$r?U0wDWxN(M85Ob!+#&=Om6uh>rFaPX^9fNQK7I=n zeo0;-fbtwJz^1Z3LPlHwC#$g-$O5m*;V}>*H%Pez`tgC!;RFA`bVd1z5rKat)%skA zwTB}@P|6_t(>X#LK9w7u)Q2at!)ML&Ekh>>nu`Ncf1EQ_S!cqqtrN+smYzqhgZc{| z1%t4UGH@a{a7G_ElO0gZqm;#?d@=~KR}JzBAuRnkXKAp`QUpBsBOVLgL>@zfCv$@{ z`ru4<@Mri~y#bHGfOZ*?cv&#~iP*R#oeuKRsIhFX3Y!>7sN#NEmSimEjnb&Z1Cd%R zYWR6Uuy`ck!X#xdcBhO)u!1=96lXG(jO0_Q?kVJjSWmeD84%L~P5UaoVXJd&qs}&F z*`@`CsIEnvl&cEpRe?;^#x(PgCViW~^ANs@98;$=b?+a#UVA&B2fDP~NA$qa%<5yA z{Uc`2L_&z;6Gt`n49~(-z-r9j;H?NywLoE+hxhH9YzNvMUAh8kDbtb`eNhaLIMf zBO-19RFrJa2bJx5!Ypsm6sQW6EgaFj0G(>=Hw6TT=n)u}Q^!RP(L9G~zRf6GWPl)u zPcc;s#)zmY?J=npb+ww~rufO;01e`eS9l;JOgwTUtZ4W#r7Kb*CM@!-MITKtAhl^J zeLS5%>S2b`3E4T~o07^)orpZN`nGII`#f8I@!;&i9NVI^EpzO;ukAXbCXL%U$L_)m zumn{IO&C?5LIG?gA_Cvy z;9f*15|(irqj`bcmRfG%Z%US8W39@#B&%(Jz1MG$hL2CEQV`(_aA~4&Bb>nS<)_Q5 zq5c>e8=x`OpslQiJpGW2z zon3QX)YvsywsXE|U3wqF@f_Q*;3n9Hr{HrZVCRYW%-C|AmrgS$8w!qM;jc5G%8JZ()X-rXmi+w}fP=r}-=3U*riSg4AV|4TZ{<^atDoA7%lLcfMB113r%PaG)#sU8HTG@H(9^UNwr1TCB-e|Qz!c!Zraf&erl!{~sP7w#cUvPm4&)>D0 zR}W4+tmZj(I&a1Z)aiX7jNNt=B1Q`CTOONJAb#=U5WRVbIC@Y7(e-CQbmbx97)cRC zx**2lK(BD~`54QXYu5J>`S+fm6Ep5<_X}Xb`$AZ-Ohkfkl7O%umm?+|?yX?Io@vhu z?AP_9?w5AluLD+Kza`fFdZ)cFuwUjy_B+d*B^`Oin*vS^Q9(-!5GcClnx@BG5EAnB zf()UNoq+|&10+Rm&@u`m%iB@*GGqn~LMAEigd&V~Px8@(D8CA|2MA=qC!>$yKE#iR zQG*_n!vY3iP!L2Wj~l)|@T6m7Z}YO@ii$DA|7IKoY7dLCM1VE?Q8djob$sB82XQdU zpeK2GGtRf*e5=7qcv^4*=51b5+p?s#)u;%`BCp!vt00nuG(c7$qRoe6!BLr?5DgzL zqhKZ7pt%N{S7{bjdyWZ?I`+KH3*4xL1x{_&6| zjB0f;Ej)3rF7{)AQ?#&ruTFU`a9^|aIkxE@+jQ-y&aRtdo93%)ueMyHwW_rmyA~Uy zsmfzG=E(J-m}7e%*z!ANWI)L)5aP>F17{8a;@yd@?o{~^a0j_VC?c>IMZtEOUOnB% zPl_R!mQP4OU_CeuxW{@h-{s^$&l0oneu69;C+Tq0nhRraO#B&Rur_$w7?4M<=U5G1 zbDp^)ml>ab#%d7tewrDq7~0u4Dz|gMAh>DjcV+|6~ur*NGZm0%fVUd|7XN zIn%fcLWqsLpNAal3SG zSBBfI?dj9{-_rNIo!c|0?-|VO8P-n9nxg6_6S-75r95`_b=fAGV^34PkRI9@>*0mg!-9&DIOIkLG!rPuo@K&W=!Ts5dJK+ z^g0sv@?W3{htULbzd`mIWS>Dt!-{ISWj+RP0!ZT-p4Sqp3J-Iv>5#8SZKb9`9-Iot z1W+Vo7-GDLPDa!iRuS?bf;@z>VU+PG8$lTo2w1m31eoJqHIwThB3IR-SG8oS)}@*IwW~fXdB5aZccykjx^$kYv4cvjS*8tF8+4{2 z%W&V+)Lt35JaBd9wvesamG;jwl^4rq%dVce#(!M-o9bUze=Pq-{k8f}$Nr`NxBZ`O z`MmR!POal$x-84|JoFIdU1|D$b*;AYV5Yj~e)Ib4sqn2^SVuH9Ys|_lQ~w`LTePhQzNpoX4e33@+KIT{b0*UyYfQse z4eeUT-n%=#=+V31){c+q-Q$^tu*TGV)!3nJ?!NoZ7h!$>JK8`*-+wyO7}c275d5xQ zab@S_omV5*C;ywEUnNU`hN{^#v*3rY_{GxMQf<}7+^TkcRr?+Cv*yoRK56-~@((2J5&$ijvX0f|M|!^!cXguL(rClkd$YmjoXpUG{4Ys0E8 zRo-WLliXWz0Zb+#wPip`$i<`KtsOv&h$6gCLJ5VrLLvBbz-S^GO}3!`7UD;v1~Z1e zL=d8*f-Fn5w?;tWdH*gYoz2VrX1R@w6%hbP!?eNP9aE)n-p%?p9^HScx zYZbB47@FA&+2f2c->^2#Tv`c_B=by3j%m`FrfVCr%tl;8kK;LJgU&$9Pwq37Hq*3e z9l{(VJ|4hAq=ISU4WNZ*Il1r_Yf;P$IZZk#xFrE8(h2{CPZfe|$pd=c5r#ck3DKZx zR|rQ38HI%`+KV7Zz9y)Z+0k)!Yexsa3hwwIilIncLJ>K0tl~#e zkBvC!v9ZJ7KOR;fY76$B!tsld+f69&65}8=J1L?r0D(Lr4h3|P##gv0)Sr|RTwDgS zhv5Ln+9N*(K1G|qaG{s|J)?LuFN=k%AOImg<^Tj%ED9!1h2bwE3DtgyS^|HEc@SAN zH@EbWiVH=~O&b0wnFr+=8&k*SQRqSc4FFf~ ztb70sb9YB2MU(~Hf&0xM6N$Tf1AeT4WyEyyIGVtWiP8k<-K6{ms?vw{zuu1khE8Ku zEXPr+R}cZIQqp31qp|?WwvuHF+LR@tz^Fwc!#2_iho#n;D7XG7)b^xtXcNkNkewqy zc{Fez4!u8gy(&}RsxhnPkr?1~hSQq1Wtr``x<+T#T7@Fo@=y< zHP<(NMt$!6#H;lm&ob}im=ij4LJNsnG^Q~pvW$eDX4#AWSwHw4*AHmB-!yHfg0T@8 zILDmE5~ep$ivHdc19K%9H2lHfgd`-QsLTd~XA*qW>hT7Hf)ok{WyH6pG;Km96#tZw zoo%o{;}H1+T#QSCj3j}KM5z2%kli5gVy0k4ats^4ZYU_oYB^7QMQ?gYQ||hQOqqKx z^|0CP?s~Y)?Oy+Ina{mrVL9Qc{FZ8Sze7H9L2&^W7fzA5+2bCyn^AG09Jf}xqh{;3 z6e^m_qm>~p=~YjMWAHB&la!E1#hO8GMkTzvzeXh#NX9#cKWdn_@{0U7(1zryvKlgY djwZ=J64n1stob8R_C5V7MMlYo1peUJ{|6V$%s>DD literal 0 HcmV?d00001 diff --git a/utils/__pycache__/resource_utils.cpython-312.pyc b/utils/__pycache__/resource_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c7e4a78a198ace7879d36e63eacd1b433de7e088 GIT binary patch literal 1735 zcmah~OH3q17_REaJb{7Phb+iiVM*MHf*wp33A+XYB8S}&(Zf#0bb7iA=%HsCt7~9q zGMJ6wfRDuNX~fmQQNkw6(M>cyu3lErsJ$`SL@(ahg+#;2zq+SqU@u!q|F8P0>aYJ% z{bPH30>Sv~=Wphzg3w<=2?yx@ICuqE9hu0)Hp&JZXEErKEfwXgjFE(BC^G^?KpSv$@G|IiM3LAyE15VeQ^l0l&`txLRY8X% zntG(7WK^&HP3k&jX3)2}WxLg3!7N@OhON6UnR6If&`YLmmF5X+AYB3$q6>$}5_pu? zjm04YAM+{-vJBtBI9Tf_j(Bsj5IWqnAhe2kBvcErP-^i)S4$418m^%*R;v;}71~Xy zhHgVFYtn7>g|gG|3Zt4di!LA@<^&BOHiOYq&P|59w)j(4w6~CPDd=8Y* z&ycU;>*(W;R?rRkI$FUQrK(L)PWS>Pdfs*HGN)uo=L>{8zA?q9jQ$}cM*Gb8aE?+lR~AK{3&W-P>d;a}xD#}Ne{!`~#G%;~LnkIh zeY)=fxrz28YW#KlcX#h*+eX{^^kEx1eYSq-)}{LBtVN>z@2@dS4abBR35$yx%#dqVLtQv*YLGANnxR6K$vh#BW0rggWBQ z7Uj(*EC|dK0~)J=4EZXq;eu%DCx(gdNFfHJiVTIC7H<^M3B(T`i8;%rLvO+RrRHtN zSiF`|m~e=RQ`s3HP&I(hNM6EUpos-uMA#^Eg_ne|(*lU z5rjS6_xU@&^$q{jH~dx4Uf;xC*W`NS_gKd#$q$mB4y-4CjXie&Pj31!*1C;m^UHDfyhsk3E#9#S8QiPU@ literal 0 HcmV?d00001 diff --git a/utils/__pycache__/text_to_mic.cpython-312.pyc b/utils/__pycache__/text_to_mic.cpython-312.pyc index 1e40b527323f1d0933dfccd64d8aa85e4fb8a86e..162b6c7a1bf92a38c9f7d876f6b760a3631815cb 100644 GIT binary patch delta 12484 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 diff --git a/utils/api_key_manager.py b/utils/api_key_manager.py new file mode 100644 index 0000000..86be346 --- /dev/null +++ b/utils/api_key_manager.py @@ -0,0 +1,89 @@ +import os +import platform +from pathlib import Path +from tkinter import messagebox, simpledialog +from dotenv import load_dotenv + +class APIKeyManager: + """Class to handle API key management operations.""" + + @staticmethod + def get_app_support_path_mac(): + """Get the application support path for macOS.""" + home = Path.home() + app_support_path = home / 'Library' / 'Application Support' / 'scorchsoft-text-to-mic' + app_support_path.mkdir(parents=True, exist_ok=True) # Ensure directory exists + return app_support_path + + @staticmethod + def save_api_key_mac(api_key): + """Save the API key on macOS.""" + env_path = APIKeyManager.get_app_support_path_mac() / 'config' / '.env' + env_path.parent.mkdir(parents=True, exist_ok=True) # Ensure config directory exists + with open(env_path, 'w') as f: + f.write(f"OPENAI_API_KEY={api_key}\n") + + @staticmethod + def save_api_key(api_key): + """Save the API key to the config/.env file.""" + try: + config_dir = Path("config") + config_dir.mkdir(parents=True, exist_ok=True) # Ensure directory exists + + env_path = config_dir / ".env" + with open(env_path, 'w') as f: + f.write(f"OPENAI_API_KEY={api_key}\n") + + # Reload environment to include the new API key + load_dotenv(dotenv_path=env_path) + return True + except Exception as e: + messagebox.showerror("Error", f"Failed to save API key: {str(e)}") + return False + + @staticmethod + def load_api_key_mac(): + """Load the API key on macOS.""" + env_path = APIKeyManager.get_app_support_path_mac() / 'config' / '.env' + if env_path.exists(): + with open(env_path, 'r') as f: + for line in f: + if line.startswith('OPENAI_API_KEY'): + return line.strip().split('=')[1] + return None + + @staticmethod + def get_api_key(parent=None): + """Get the API key from environment variables or local file, or prompt the user.""" + # First, try to load the API key from environment variables or local file + api_key = os.getenv("OPENAI_API_KEY") + if not api_key: # Check for macOS and use the macOS-specific method + if platform.system() == 'Darwin': # Darwin is the system name for macOS + api_key = APIKeyManager.load_api_key_mac() + + # If no API key is found, prompt the user + if not api_key and parent: + parent.show_instructions() # Show the "How to Use" modal after setting the key + api_key = simpledialog.askstring("API Key", "Enter your OpenAI API Key:", parent=parent) + if api_key: + try: + if platform.system() == 'Darwin': + APIKeyManager.save_api_key_mac(api_key) + else: + APIKeyManager.save_api_key(api_key) + messagebox.showinfo("API Key Set", "The OpenAI API Key has been updated successfully.") + except Exception as e: + messagebox.showerror("Error", f"Failed to save API key: {str(e)}") + + return api_key + + @staticmethod + def change_api_key(parent): + """Change the API key.""" + new_key = simpledialog.askstring("API Key", "Enter new OpenAI API Key:", parent=parent) + if new_key: + success = APIKeyManager.save_api_key(new_key) + if success: + messagebox.showinfo("API Key Updated", "The OpenAI API Key has been updated successfully.") + return new_key + return None \ No newline at end of file diff --git a/utils/hotkey_manager.py b/utils/hotkey_manager.py new file mode 100644 index 0000000..aad20be --- /dev/null +++ b/utils/hotkey_manager.py @@ -0,0 +1,133 @@ +import tkinter as tk +from tkinter import ttk, messagebox +import keyboard + +class HotkeyManager: + """Class to handle hotkey operations.""" + + def __init__(self, app): + self.app = app + self.setup_hotkeys() + + def setup_hotkeys(self): + """Set up hotkeys based on settings.""" + try: + # Attempt to clear existing hotkeys + keyboard.unhook_all() # This should clear all hotkeys in some versions of the library. + except AttributeError: + pass # Ignore if the method isn't supported + + settings = self.app.load_settings() + + def parse_hotkey(combo): + return '+'.join(filter(None, combo)) + + keyboard.add_hotkey(parse_hotkey(settings["hotkeys"]["record_start_stop"]), lambda: self.hotkey_record_trigger()) + keyboard.add_hotkey(parse_hotkey(settings["hotkeys"]["stop_recording"]), lambda: self.hotkey_stop_trigger()) + keyboard.add_hotkey(parse_hotkey(settings["hotkeys"]["play_last_audio"]), lambda: self.hotkey_play_last_audio_trigger()) + + def hotkey_play_last_audio_trigger(self): + """Trigger playing the last audio.""" + if hasattr(self.app, 'last_audio_file'): + self.app.play_last_audio() + else: + self.app.play_sound('assets/no-last-audio.wav') + + def hotkey_stop_trigger(self): + """Trigger stopping the recording.""" + self.app.play_sound('assets/wrong-short.wav') + if self.app.recording: + self.app.stop_recording(auto_play=False) + self.app.recording = False + + def hotkey_record_trigger(self): + """Trigger recording or stop recording and submit.""" + if self.app.recording: + self.app.play_sound('assets/pop.wav') + self.app.submit_text() + else: + if not self.app.recording: + self.app.start_recording(play_confirm_sound=True) + else: + self.app.stop_recording(auto_play=True) + + @staticmethod + def hotkey_settings_dialog(app): + """Show the hotkey settings dialog.""" + settings = app.load_settings() + hotkey_window = tk.Toplevel(app) + hotkey_window.title("Hotkey Settings") + hotkey_window.grab_set() # Grab the focus on this toplevel window + + main_frame = ttk.Frame(hotkey_window, padding="10") + main_frame.grid(column=0, row=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + + # Create dropdowns for each hotkey + keys = ["", "ctrl", "shift", "alt", "tab", "altgr"] + main_keys = list("abcdefghijklmnopqrstuvwxyz1234567890[];'#,./`") + \ + [f"f{i}" for i in range(1, 13)] # Add function keys F1 to F12 + + def create_hotkey_row(label_text, key_combo): + ttk.Label(main_frame, text=label_text).grid(row=create_hotkey_row.row, column=0, sticky=tk.W, pady=2) + + var1 = tk.StringVar(value=key_combo[0] if len(key_combo) > 0 else "") + var2 = tk.StringVar(value=key_combo[1] if len(key_combo) > 1 else "") + var3 = tk.StringVar(value=key_combo[2] if len(key_combo) > 2 else "") + + option_menu1 = ttk.OptionMenu(main_frame, var1, key_combo[0], *keys) + option_menu1.grid(row=create_hotkey_row.row, column=1, sticky=tk.W, pady=2) + + option_menu2 = ttk.OptionMenu(main_frame, var2, key_combo[1] if len(key_combo) > 1 else "", *keys) + option_menu2.grid(row=create_hotkey_row.row, column=2, sticky=tk.W, pady=2) + + option_menu3 = ttk.OptionMenu(main_frame, var3, key_combo[2] if len(key_combo) > 2 else "", *main_keys) + option_menu3.grid(row=create_hotkey_row.row, column=3, sticky=tk.W, pady=2) + + create_hotkey_row.row += 1 + return [var1, var2, var3] + + create_hotkey_row.row = 0 + + record_start_stop_vars = create_hotkey_row("Record Start/Stop:", settings["hotkeys"]["record_start_stop"]) + stop_recording_vars = create_hotkey_row("Stop Recording:", settings["hotkeys"]["stop_recording"]) + play_last_audio_vars = create_hotkey_row("Play Last Audio:", settings["hotkeys"]["play_last_audio"]) + + # Save Button + save_btn = ttk.Button(main_frame, text="Save", command=lambda: HotkeyManager.save_hotkey_settings(app, { + "record_start_stop": [record_start_stop_vars[0].get(), record_start_stop_vars[1].get(), record_start_stop_vars[2].get()], + "stop_recording": [stop_recording_vars[0].get(), stop_recording_vars[1].get(), stop_recording_vars[2].get()], + "play_last_audio": [play_last_audio_vars[0].get(), play_last_audio_vars[1].get(), play_last_audio_vars[2].get()] + })) + save_btn.grid(row=create_hotkey_row.row, column=1, sticky=tk.W + tk.E, pady=10) + + @staticmethod + def save_hotkey_settings(app, hotkeys): + """Save hotkey settings.""" + settings = app.load_settings() + settings["hotkeys"] = hotkeys + app.save_settings_to_JSON(settings) + app.hotkey_manager.setup_hotkeys() # Re-register the hotkeys with the new settings + messagebox.showinfo("Settings Updated", "Your hotkey settings have been saved successfully.") + + @staticmethod + def show_hotkey_instructions(app): + """Show hotkey instructions.""" + instruction_window = tk.Toplevel(app) + instruction_window.title("Hotkey Instructions") + instruction_window.geometry("400x300") # Width x Height + + instructions = """How to use Hotkeys +ctrl+shift+0 +This starts a recording, then converts to text and plays when you press this hotkey again. + +ctrl+shift+9 +If you are recording, you can press this hotkey to stop recording without playing + +ctrl+shift+8 +This replays the last audio clip played + + """ + tk.Label(instruction_window, text=instructions, justify=tk.LEFT, wraplength=380).pack(padx=10, pady=10) + + # Add a button to close the window + ttk.Button(instruction_window, text="Close", command=instruction_window.destroy).pack(pady=(10, 0)) \ No newline at end of file diff --git a/utils/resource_utils.py b/utils/resource_utils.py new file mode 100644 index 0000000..a068331 --- /dev/null +++ b/utils/resource_utils.py @@ -0,0 +1,30 @@ +import os +import sys +from audioplayer import AudioPlayer + +class ResourceUtils: + """Utility class for handling resources and audio playback.""" + + @staticmethod + def resource_path(relative_path): + """Get the absolute path to the resource, works for both development and PyInstaller environments.""" + try: + # When running in a PyInstaller bundle, use the '_MEIPASS' directory + base_path = sys._MEIPASS + except AttributeError: + # When running normally (not bundled), use the directory where the main script is located + base_path = os.path.dirname(os.path.abspath(sys.argv[0])) + + # Resolve the absolute path + abs_path = os.path.join(base_path, relative_path) + + # Debugging: Print the absolute path to check if it's correct + print(f"Resolved path for {relative_path}: {abs_path}") + + return abs_path + + @staticmethod + def play_sound(sound_file): + """Play a sound file.""" + player = AudioPlayer(ResourceUtils.resource_path(sound_file)) + player.play(block=True) \ No newline at end of file diff --git a/utils/text_to_mic.py b/utils/text_to_mic.py index 3257eab..93ed56a 100644 --- a/utils/text_to_mic.py +++ b/utils/text_to_mic.py @@ -6,7 +6,6 @@ import pyaudio import wave import webbrowser import json -import keyboard import sys from pystray import Icon as icon, MenuItem as item, Menu as menu @@ -16,8 +15,11 @@ from openai import OpenAI from dotenv import load_dotenv from pathlib import Path from pydub import AudioSegment -from audioplayer import AudioPlayer +# Import our refactored classes +from utils.api_key_manager import APIKeyManager +from utils.hotkey_manager import HotkeyManager +from utils.resource_utils import ResourceUtils # Modify the load environment variables to load from config/.env def load_env_file(): @@ -55,8 +57,8 @@ class TextToMic(tk.Tk): self.ensure_config_directory() load_env_file() - # Ensure API Key is loaded or prompted for before initializing GUI components - self.api_key = self.get_api_key() + # Get API key using APIKeyManager + self.api_key = APIKeyManager.get_api_key(self) if not self.api_key: messagebox.showinfo("API Key Needed", "Please provide your OpenAI API Key.") self.destroy() @@ -74,11 +76,10 @@ class TextToMic(tk.Tk): self.create_menu() self.initialize_gui() - self.setup_hotkeys() - - - + # Initialize our HotkeyManager + self.hotkey_manager = HotkeyManager(self) + def ensure_config_directory(self): """Ensure the config directory exists.""" config_dir = Path("config") @@ -105,14 +106,13 @@ class TextToMic(tk.Tk): self.menubar.add_cascade(label="Settings", menu=settings_menu) settings_menu.add_command(label="Change API Key", command=self.change_api_key) settings_menu.add_command(label="ChatGPT Manipulation", command=self.chat_gpt_settings) - settings_menu.add_command(label="Hotkey Settings", command=self.hotkey_settings) + settings_menu.add_command(label="Hotkey Settings", command=self.show_hotkey_settings) # Playback menu playback_menu = Menu(self.menubar, tearoff=0) self.menubar.add_cascade(label="Playback", menu=playback_menu) playback_menu.add_command(label="Play Last Audio", command=self.play_last_audio) - #playback_menu.add_command(label="Input Speech to Text", command=self.input_speech_to_text) #apply_ai input_menu = Menu(self.menubar, tearoff=0) @@ -127,136 +127,36 @@ class TextToMic(tk.Tk): help_menu.add_command(label="Terms of Use and Licence", command=self.show_terms_of_use) help_menu.add_command(label="Version", command=self.show_version) help_menu.add_command(label="Hotkey Instructions", command=self.show_hotkey_instructions) - + def show_hotkey_settings(self): + """Show the hotkey settings dialog.""" + HotkeyManager.hotkey_settings_dialog(self) - def hotkey_settings(self): - settings = self.load_settings() - hotkey_window = tk.Toplevel(self) - hotkey_window.title("Hotkey Settings") - hotkey_window.grab_set() # Grab the focus on this toplevel window + def show_hotkey_instructions(self): + """Show hotkey instructions.""" + HotkeyManager.show_hotkey_instructions(self) - main_frame = ttk.Frame(hotkey_window, padding="10") - main_frame.grid(column=0, row=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + def change_api_key(self): + """Change the API key using APIKeyManager.""" + new_key = APIKeyManager.change_api_key(self) + if new_key: + self.api_key = new_key + self.client = OpenAI(api_key=self.api_key) - # Create dropdowns for each hotkey - keys = ["", "ctrl", "shift", "alt", "tab", "altgr"] - main_keys = list("abcdefghijklmnopqrstuvwxyz1234567890[];'#,./`") + \ - [f"f{i}" for i in range(1, 13)] # Add function keys F1 to F12 - - def create_hotkey_row(label_text, key_combo): - ttk.Label(main_frame, text=label_text).grid(row=create_hotkey_row.row, column=0, sticky=tk.W, pady=2) - - var1 = tk.StringVar(value=key_combo[0] if len(key_combo) > 0 else "") - var2 = tk.StringVar(value=key_combo[1] if len(key_combo) > 1 else "") - var3 = tk.StringVar(value=key_combo[2] if len(key_combo) > 2 else "") - - option_menu1 = ttk.OptionMenu(main_frame, var1, key_combo[0], *keys) - option_menu1.grid(row=create_hotkey_row.row, column=1, sticky=tk.W, pady=2) - - option_menu2 = ttk.OptionMenu(main_frame, var2, key_combo[1] if len(key_combo) > 1 else "", *keys) - option_menu2.grid(row=create_hotkey_row.row, column=2, sticky=tk.W, pady=2) - - option_menu3 = ttk.OptionMenu(main_frame, var3, key_combo[2] if len(key_combo) > 2 else "", *main_keys) - option_menu3.grid(row=create_hotkey_row.row, column=3, sticky=tk.W, pady=2) - - create_hotkey_row.row += 1 - return [var1, var2, var3] - - create_hotkey_row.row = 0 - - record_start_stop_vars = create_hotkey_row("Record Start/Stop:", settings["hotkeys"]["record_start_stop"]) - stop_recording_vars = create_hotkey_row("Stop Recording:", settings["hotkeys"]["stop_recording"]) - play_last_audio_vars = create_hotkey_row("Play Last Audio:", settings["hotkeys"]["play_last_audio"]) - - # Save Button - save_btn = ttk.Button(main_frame, text="Save", command=lambda: self.save_hotkey_settings({ - "record_start_stop": [record_start_stop_vars[0].get(), record_start_stop_vars[1].get(), record_start_stop_vars[2].get()], - "stop_recording": [stop_recording_vars[0].get(), stop_recording_vars[1].get(), stop_recording_vars[2].get()], - "play_last_audio": [play_last_audio_vars[0].get(), play_last_audio_vars[1].get(), play_last_audio_vars[2].get()] - })) - save_btn.grid(row=create_hotkey_row.row, column=1, sticky=tk.W + tk.E, pady=10) - - - def save_hotkey_settings(self, hotkeys): - settings = self.load_settings() - settings["hotkeys"] = hotkeys - self.save_settings_to_JSON(settings) - self.setup_hotkeys() # Re-register the hotkeys with the new settings - messagebox.showinfo("Settings Updated", "Your hotkey settings have been saved successfully.") - - def setup_hotkeys(self): - try: - # Attempt to clear existing hotkeys - keyboard.unhook_all() # This should clear all hotkeys in some versions of the library. - except AttributeError: - pass # Ignore if the method isn't supported - - settings = self.load_settings() - - def parse_hotkey(combo): - return '+'.join(filter(None, combo)) - - keyboard.add_hotkey(parse_hotkey(settings["hotkeys"]["record_start_stop"]), lambda: self.hotkey_record_trigger()) - keyboard.add_hotkey(parse_hotkey(settings["hotkeys"]["stop_recording"]), lambda: self.hotkey_stop_trigger()) - keyboard.add_hotkey(parse_hotkey(settings["hotkeys"]["play_last_audio"]), lambda: self.hotkey_play_last_audio_trigger()) - - - def hotkey_play_last_audio_trigger(self): - if hasattr(self, 'last_audio_file'): - self.play_last_audio() + def get_audio_file_path(self, filename): + if platform.system() == 'Darwin': # Check if the OS is macOS + mac_path = APIKeyManager.get_app_support_path_mac() + return f"{mac_path}/{filename}" else: - self.play_sound('assets/no-last-audio.wav') - - - def hotkey_stop_trigger(self): - self.play_sound('assets/wrong-short.wav') - if self.recording: - self.stop_recording(auto_play=False) - self.recording=False - - # Sounds from https://mixkit.co/free-sound-effects/notification/ - def hotkey_record_trigger(self): - - if self.recording: - self.play_sound('assets/pop.wav') - self.submit_text() - else: - - if not self.recording: - self.start_recording(play_confirm_sound=True) - else: - self.stop_recording(auto_play=True) - - - - - + return Path(filename) # Default to current directory for non-macOS systems def play_sound(self, sound_file): - player = AudioPlayer(self.resource_path(sound_file)) - player.play(block=True) + """Play a sound file using ResourceUtils.""" + ResourceUtils.play_sound(sound_file) def resource_path(self, relative_path): - """Get the absolute path to the resource, works for both development and PyInstaller environments.""" - - try: - # When running in a PyInstaller bundle, use the '_MEIPASS' directory - base_path = sys._MEIPASS - except AttributeError: - # When running normally (not bundled), use the directory where the main script is located - base_path = os.path.dirname(os.path.abspath(sys.argv[0])) - - # Resolve the absolute path - abs_path = os.path.join(base_path, relative_path) - - # Debugging: Print the absolute path to check if it's correct - print(f"Resolved path for {relative_path}: {abs_path}") - - return abs_path - - - + """Get the resource path using ResourceUtils.""" + return ResourceUtils.resource_path(relative_path) def initialize_gui(self): @@ -449,27 +349,6 @@ class TextToMic(tk.Tk): - def show_hotkey_instructions(self): - instruction_window = tk.Toplevel(self) - instruction_window.title("Hotkey Instructions") - instruction_window.geometry("400x300") # Width x Height - - instructions = """How to use Hotkeys -ctrl+shift+0 -This starts a recording, then converts to text and plays when you press this hotkey again. - -ctrl+shift+9 -If you are recording, you can press this hotkey to stop recording without playing - -ctrl+shift+8 -This replays the last audio clip played - - """ - tk.Label(instruction_window, text=instructions, justify=tk.LEFT, wraplength=380).pack(padx=10, pady=10) - - # Add a button to close the window - ttk.Button(instruction_window, text="Close", command=instruction_window.destroy).pack(pady=(10, 0)) - def show_instructions(self): instruction_window = tk.Toplevel(self) instruction_window.title("How to Use") @@ -651,8 +530,7 @@ Please also make sure you read the Terms of use and licence statement before usi def get_audio_file_path(self, filename): if platform.system() == 'Darwin': # Check if the OS is macOS - mac_path = self.get_app_support_path_mac() - #return self.get_app_support_path_mac() / filename + mac_path = APIKeyManager.get_app_support_path_mac() return f"{mac_path}/{filename}" else: return Path(filename) # Default to current directory for non-macOS systems @@ -842,195 +720,6 @@ Please also make sure you read the Terms of use and licence statement before usi p.terminate() - def change_api_key(self): - new_key = simpledialog.askstring("API Key", "Enter new OpenAI API Key:", parent=self) - if new_key: - self.save_api_key(new_key) - self.api_key = new_key - self.client = OpenAI(api_key=self.api_key) - messagebox.showinfo("API Key Updated", "The OpenAI API Key has been updated successfully.") - - - def get_device_info(self, device_index): - p = pyaudio.PyAudio() - try: - device_info = p.get_device_info_by_index(device_index) - return device_info - finally: - p.terminate() - - def toggle_recording(self, auto_play=False): - if not self.recording: - self.start_recording() - else: - self.stop_recording(auto_play) - - def stop_recording_btn_change(self, btn_text): - self.record_button.config(text=btn_text) - - - def start_recording(self, play_confirm_sound=False): - - input_device_index = self.input_device_index.get() # Assuming input_device_index is a StringVar - input_device_id = self.available_input_devices.get(input_device_index) - - if input_device_id is None: - if play_confirm_sound: - self.play_sound('assets/please-select-input.wav') - else: - messagebox.showerror("Error", "Selected audio device is not available.") - return - - device_info = self.get_device_info(input_device_id) - sample_rate = int(device_info['defaultSampleRate']) - - print(f"Device info: {device_info}") - - if sample_rate is None: - sample_rate = 44100 - - #Record to GUI selected device ID - #device_id = None if self.input_device_index.get() == "Default" else input_devices[self.input_device_index.get()] - - if input_device_id is None: - messagebox.showerror("Error", "Selected audio device is not available.") - return - - try: - self.recording = True - self.record_button.config(text="Stop and Insert", style='Recording.TButton') - self.submit_button.config(text="Stop and Play", style='Recording.TButton') - - self.frames = [] - - self.p = pyaudio.PyAudio() - self.stream = self.p.open(format=pyaudio.paInt16, channels=1, rate=sample_rate, input=True, frames_per_buffer=1024, input_device_index=input_device_id) - - if play_confirm_sound: - self.play_sound('assets/pop.wav') - - def record(): - while self.recording: - data = self.stream.read(1024, exception_on_overflow=False) - self.frames.append(data) - - self.record_thread = threading.Thread(target=record) - self.record_thread.start() - - except Exception as e: - messagebox.showerror("Recording Error", f"Failed to record audio: {str(e)}") - self.stop_recording(True) - - def stop_recording(self, cancel_save=False, auto_play=False): - self.recording = False - if self.record_thread: - self.record_thread.join() - - if self.stream: - self.stream.stop_stream() - self.stream.close() - - if self.p: - self.p.terminate() - - if cancel_save==False: - self.save_recording(auto_play=auto_play) - - self.record_button.config(text="Record Mic", style='TButton') # Revert to default style - self.submit_button.config(text="Play", style='Green.TButton') # Revert to default style - - - def save_recording(self, auto_play = False): - file_path = "output.wav" - wf = wave.open(file_path, 'wb') - wf.setnchannels(1) - wf.setsampwidth(self.p.get_sample_size(pyaudio.paInt16)) - wf.setframerate(44100) - wf.writeframes(b''.join(self.frames)) - wf.close() - print("Recording saved.") - - self.after(0, self.transcribe_audio, file_path, auto_play) - - - - - def transcribe_audio(self, file_path, auto_play = False): - try: - with open(str(file_path), "rb") as audio_file: - transcription = self.client.audio.transcriptions.create( - file=audio_file, - model="whisper-1", - response_format="verbose_json" - ) - - settings = self.load_settings() - - if settings["chat_gpt_completion"] and settings["auto_apply_ai_to_recording"]: - auto_apply_ai = settings["auto_apply_ai_to_recording"] - else: - auto_apply_ai = False - - print(f"auto_apply_ai: {auto_apply_ai}") - - if auto_apply_ai: - print("applying ai") - play_text = self.apply_ai(transcription.text) - else: - print("outputting without ai") - #This prevents issues with trying to upload TK after thread operations - #whcih can cause crashes with no error displayed - self.text_input.delete("1.0", tk.END) # Clear existing text - self.text_input.insert("1.0", transcription.text) # Insert new text - play_text = transcription.text - - if auto_play: - #self.submit_text(play_text = playtext)# - print(f"Triggering auto play with: {play_text} ") - self.submit_text_helper(play_text = play_text) - # TODO: PLAY THE TEXT IMMEDIATELY - - print("Transcription Complete: The audio has been transcribed and the text has been placed in the input area.") - #messagebox.showinfo("Transcription Complete", "The audio has been transcribed and the text has been placed in the input area.") - - except Exception as e: - print(f"Transcription error: An error occurred during transcription: {str(e)}") - - - def load_settings(self): - settings_file = self.get_settings_file_path("settings.json") - try: - with open(settings_file, "r") as f: - settings = json.load(f) - except FileNotFoundError: - # Default settings - settings = { - "chat_gpt_completion": False, - "model": self.default_model, - "prompt": "", - "auto_apply_ai_to_recording": False, - "hotkeys": { - "record_start_stop": ["ctrl", "shift", "0"], - "stop_recording": ["ctrl", "shift", "9"], - "play_last_audio": ["ctrl", "shift", "8"] - } - } - self.save_settings_to_JSON(settings) - return settings - - def save_settings_to_JSON(self, settings): - settings_file = self.get_settings_file_path("settings.json") - - with open(settings_file, "w") as f: - json.dump(settings, f) - - def get_settings_file_path(self, filename): - if platform.system() == 'Darwin': # Check if the OS is macOS - mac_path = self.get_app_support_path_mac() - return f"{mac_path}/{filename}" - else: - return filename # Default to current directory for non-macOS systems - def chat_gpt_settings(self): settings = self.load_settings() settings_window = tk.Toplevel(self) @@ -1351,4 +1040,171 @@ Please also make sure you read the Terms of use and licence statement before usi self.save_presets() self.refresh_presets_display() + def get_device_info(self, device_index): + p = pyaudio.PyAudio() + try: + device_info = p.get_device_info_by_index(device_index) + return device_info + finally: + p.terminate() + + def toggle_recording(self, auto_play=False): + if not self.recording: + self.start_recording() + else: + self.stop_recording(auto_play) + + def stop_recording_btn_change(self, btn_text): + self.record_button.config(text=btn_text) + + def start_recording(self, play_confirm_sound=False): + input_device_index = self.input_device_index.get() # Assuming input_device_index is a StringVar + input_device_id = self.available_input_devices.get(input_device_index) + + if input_device_id is None: + if play_confirm_sound: + self.play_sound('assets/please-select-input.wav') + else: + messagebox.showerror("Error", "Selected audio device is not available.") + return + + device_info = self.get_device_info(input_device_id) + sample_rate = int(device_info['defaultSampleRate']) + + print(f"Device info: {device_info}") + + if sample_rate is None: + sample_rate = 44100 + + if input_device_id is None: + messagebox.showerror("Error", "Selected audio device is not available.") + return + + try: + self.recording = True + self.record_button.config(text="Stop and Insert", style='Recording.TButton') + self.submit_button.config(text="Stop and Play", style='Recording.TButton') + + self.frames = [] + + self.p = pyaudio.PyAudio() + self.stream = self.p.open(format=pyaudio.paInt16, channels=1, rate=sample_rate, input=True, frames_per_buffer=1024, input_device_index=input_device_id) + + if play_confirm_sound: + self.play_sound('assets/pop.wav') + + def record(): + while self.recording: + data = self.stream.read(1024, exception_on_overflow=False) + self.frames.append(data) + + self.record_thread = threading.Thread(target=record) + self.record_thread.start() + + except Exception as e: + messagebox.showerror("Recording Error", f"Failed to record audio: {str(e)}") + self.stop_recording(True) + + def stop_recording(self, cancel_save=False, auto_play=False): + self.recording = False + if hasattr(self, 'record_thread') and self.record_thread: + self.record_thread.join() + + if hasattr(self, 'stream') and self.stream: + self.stream.stop_stream() + self.stream.close() + + if hasattr(self, 'p') and self.p: + self.p.terminate() + + if cancel_save==False: + self.save_recording(auto_play=auto_play) + + self.record_button.config(text="Record Mic", style='TButton') # Revert to default style + self.submit_button.config(text="Play", style='Green.TButton') # Revert to default style + + def save_recording(self, auto_play = False): + file_path = "output.wav" + wf = wave.open(file_path, 'wb') + wf.setnchannels(1) + wf.setsampwidth(self.p.get_sample_size(pyaudio.paInt16)) + wf.setframerate(44100) + wf.writeframes(b''.join(self.frames)) + wf.close() + print("Recording saved.") + + self.after(0, self.transcribe_audio, file_path, auto_play) + + def transcribe_audio(self, file_path, auto_play = False): + try: + with open(str(file_path), "rb") as audio_file: + transcription = self.client.audio.transcriptions.create( + file=audio_file, + model="whisper-1", + response_format="verbose_json" + ) + + settings = self.load_settings() + + if settings["chat_gpt_completion"] and settings["auto_apply_ai_to_recording"]: + auto_apply_ai = settings["auto_apply_ai_to_recording"] + else: + auto_apply_ai = False + + print(f"auto_apply_ai: {auto_apply_ai}") + + if auto_apply_ai: + print("applying ai") + play_text = self.apply_ai(transcription.text) + else: + print("outputting without ai") + #This prevents issues with trying to upload TK after thread operations + #whcih can cause crashes with no error displayed + self.text_input.delete("1.0", tk.END) # Clear existing text + self.text_input.insert("1.0", transcription.text) # Insert new text + play_text = transcription.text + + if auto_play: + print(f"Triggering auto play with: {play_text} ") + self.submit_text_helper(play_text = play_text) + + print("Transcription Complete: The audio has been transcribed and the text has been placed in the input area.") + + except Exception as e: + print(f"Transcription error: An error occurred during transcription: {str(e)}") + + def load_settings(self): + settings_file = self.get_settings_file_path("settings.json") + try: + with open(settings_file, "r") as f: + settings = json.load(f) + except FileNotFoundError: + # Default settings + settings = { + "chat_gpt_completion": False, + "model": self.default_model, + "prompt": "", + "auto_apply_ai_to_recording": False, + "hotkeys": { + "record_start_stop": ["ctrl", "shift", "0"], + "stop_recording": ["ctrl", "shift", "9"], + "play_last_audio": ["ctrl", "shift", "8"] + } + } + self.save_settings_to_JSON(settings) + return settings + + def save_settings_to_JSON(self, settings): + settings_file = self.get_settings_file_path("settings.json") + + with open(settings_file, "w") as f: + json.dump(settings, f) + + def get_settings_file_path(self, filename): + if platform.system() == 'Darwin': # Check if the OS is macOS + mac_path = APIKeyManager.get_app_support_path_mac() + return f"{mac_path}/{filename}" + else: + return filename # Default to current directory for non-macOS systems +