From 47cdaa76b687741c0f654bbb45a4a539cc4fa3e5 Mon Sep 17 00:00:00 2001 From: michalcourson Date: Tue, 24 Feb 2026 18:08:58 -0500 Subject: [PATCH] python service managment on client, port configuration --- audio-service/settings.json | 15 ++-- .../audio_recorder.cpython-313.pyc | Bin 7245 -> 6606 bytes .../src/__pycache__/settings.cpython-313.pyc | Bin 5027 -> 5214 bytes .../__pycache__/windows_audio.cpython-313.pyc | Bin 5764 -> 5611 bytes audio-service/src/audio_recorder.py | 15 ++-- audio-service/src/main.py | 21 ++--- .../__pycache__/metadata.cpython-313.pyc | Bin 6792 -> 6613 bytes .../__pycache__/recording.cpython-313.pyc | Bin 3339 -> 3064 bytes .../__pycache__/settings.cpython-313.pyc | Bin 1746 -> 2235 bytes audio-service/src/routes/metadata.py | 2 +- audio-service/src/routes/recording.py | 8 +- audio-service/src/routes/settings.py | 20 +++-- audio-service/src/settings.py | 12 ++- audio-service/src/windows_audio.py | 4 +- electron-ui/settings.json | 16 ++++ electron-ui/src/ipc/audio/channels.ts | 2 + electron-ui/src/ipc/audio/main.ts | 22 +++++ electron-ui/src/ipc/audio/types.ts | 19 +++++ electron-ui/src/main/main.ts | 5 ++ electron-ui/src/main/preload.ts | 9 +- electron-ui/src/main/service.ts | 79 ++++++++++++++++++ electron-ui/src/renderer/App.tsx | 2 +- electron-ui/src/renderer/Settings.tsx | 38 +++++++-- electron-ui/src/renderer/api.ts | 16 ++-- .../src/renderer/components/ClipList.tsx | 2 +- electron-ui/src/renderer/preload.d.ts | 4 +- 26 files changed, 244 insertions(+), 67 deletions(-) create mode 100644 electron-ui/settings.json diff --git a/audio-service/settings.json b/audio-service/settings.json index 3e9bf0b..ed12104 100644 --- a/audio-service/settings.json +++ b/audio-service/settings.json @@ -1,16 +1,17 @@ { "input_device": { - "index": 49, - "name": "Microphone (Logi C615 HD WebCam)", - "max_input_channels": 1, - "default_samplerate": 48000.0 + "channels": 2, + "default_samplerate": 48000, + "index": 55, + "name": "VM Mic mix (VB-Audio Voicemeeter VAIO)" }, "save_path": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings", "recording_length": 30, "output_device": { + "default_samplerate": 48000, "index": 40, - "name": "Speakers (Realtek(R) Audio)", "max_output_channels": 2, - "default_samplerate": 48000.0 - } + "name": "Speakers (Realtek(R) Audio)" + }, + "http_port": 5010 } \ No newline at end of file diff --git a/audio-service/src/__pycache__/audio_recorder.cpython-313.pyc b/audio-service/src/__pycache__/audio_recorder.cpython-313.pyc index e036ec8402ccf7cbe0a28a3a6c3443b0bc59bc9e..d61d721195d0e40955656c6caccc3ca3a1c0da57 100644 GIT binary patch delta 1428 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 delta 1109 zcmZuw&rcIU6rS1L*>1PH+p+}-KMD(iSR>FB3Q9x>Q9wn|wi`_<7@!48q;0kZm5Zo} z|G*1gJeqjMaPnZH2ja;D4oK1%5)%@y1`db^<4ns21t-~W-+tfw-g`ScyM6X`leVX- zGNS9s$g5EmM97^GsX6s=OV7^o{aR?VF+R_OlLhVuY|IXOP z+w~$sgtQ3EX4j0B*-_{)NO2|?E(bb?TeKUcc$))_$k0V(BGGAecq@A09+9!m@>Pk@ z8gnG#JgJ>_7Bn$RCj1D+D(tQ%X~z+{63t2`54Xjt8aGv|Y&GJF5)HL+_8bQMBn@`6 zCYlu$i{+qOY&~YxqT;{Slz3=`fJo>jgnjT^tpS(ACmY>-Et;AT+=W{1L zes`WrXY)d4JU5dUX4A=3UYt(kCLhM(g)=OYjFn8~V9WVjdPzU?!4le8&B_Y)d{s); zOWRk>m4S8r(x%c4&s^2{Hq~%vu zq1*Mbr+OgBe+tTj7|N7UHAuNUPrLg$s}r6my|@oPC=(Ln{g_hc)IQt~bLts4Pbqfo z^-@?yu&vf&o^4>6#SRlKz)V&Li+SpdYJ;w8^dVbGVF|6SX^pt8m8CQ@2vEoi@%gz- z!N>YD`T~V4`=JcO(izbKK2JTqx!UV_gK-ahFS%OJ67!Okm`q#ogk@c17B-n-dxl_G zZ^U89>5s+2P|dKXUoY!n_4*j}GhmZiLlE|khFXblcLsB$c~`9kyo6V=>+3sHRd^Tp-f5K3=GliAW0AiWin-iu>3ir z*(d*DmYuvL>w3nM zlNYfCvVGuTun%FJ%*AfU`h|l*PGNE!yE*qk8y;tQmP0&j&hncVus>mB)S2AMsm-Gd zG~gCXHqgi-_04-YZJ2DefFeZ_AVLg8XoCnHAfd^EP1H}5sYo28L=i+7Y&PYo02#fG z*HBy@B%%P9lLN9enTwP+f8$-os1G)gxkw5m4Uz%t2bu+PPmvXnCIGSc3=)5aA9Yd^Wp_vNAHpO!gHs2LP{i BXqNy0 delta 723 zcmZvY&1(}u7{+I2KeC(bZqs};vKX`4NV6nSYSGZxMx(*n()!Uf2!>M7t`dw_Acof6>7Zkkbkb~r654lJ^sV7g)yD_zbbNKDN&-3mx@62BMSyum{>wd)7 zTKQG2*U@7Fi4eAIB-xiSKU)(T`Kk|WSY@d~3^YSd<6&)~3b_?PsHzTY#OslFRjsOQ zSBSG+Ey|-5W{P1fvlHU+ygceYknF3VRc)mI{r-Del|2&UWHrb-S~#Rgysd)j`)40? z6Y3w=yBg%(#Z6+NCTWOwknLyR#hbZy>_)L%ytZhSoL23&V=Xo|oh_%CxB5~IwrzFO zGq^$tkN67LwgYTKIxI^MX|3^?wWSYukEP}7v7dgV8r|SjH#Og#Ds+u=J>~r8LYY04 z3ljrHf1z$C-(+59wvTj;R8O%7@dW!M=K?!^tt5#%rd*O-pKpM1mVHywT9TjC7w_>g zXv&>b=g54TEA$Xx0+0b@Ic)KN>J^*N7_=M%OuMfGTM!-9r(+foMMEIWx~Il|VVr!M9Vb>1lnVfOdU_Ec07@Kvv&@3y zrX_hveXaGN{^Y*1QFmHSeK_bPXkB2>!<~>f^CZ-81sl^eTZ$~1GoYLS%mHQrmjNs8 N`-p_`I{O&S{RSy%isk?S diff --git a/audio-service/src/audio_recorder.py b/audio-service/src/audio_recorder.py index 243271b..d66ed94 100644 --- a/audio-service/src/audio_recorder.py +++ b/audio-service/src/audio_recorder.py @@ -10,7 +10,7 @@ class AudioRecorder: def __new__(cls, *args, **kwargs): if cls._instance is None: - print("Creating new AudioRecorder instance") + # print("Creating new AudioRecorder instance") cls._instance = super().__new__(cls) cls._instance.init() return cls._instance @@ -22,7 +22,7 @@ class AudioRecorder: :param sample_rate: Audio sample rate (if None, use default device sample rate) :param channels: Number of audio channels """ - print(f"Initializing AudioRecorder") + # print(f"Initializing AudioRecorder") self.duration = 30 self.sample_rate = 44100 self.channels = 2 @@ -42,7 +42,7 @@ class AudioRecorder: self.stream.stop() self.buffer = np.zeros((int(self.duration * self.sample_rate), self.channels), dtype=np.float32) - print(f"AudioRecorder initialized with duration={self.duration}s, sample_rate={self.sample_rate}Hz, channels={self.channels}") + # print(f"AudioRecorder initialized with duration={self.duration}s, sample_rate={self.sample_rate}Hz, channels={self.channels}") self.stream = sd.InputStream( callback=self.record_callback ) @@ -63,7 +63,8 @@ class AudioRecorder: :param status: Recording status """ if status: - print(f"Recording status: {status}") + # print(f"Recording status: {status}") + pass # Circular buffer implementation self.buffer = np.roll(self.buffer, -frames, axis=0) @@ -128,9 +129,9 @@ class AudioRecorder: Start continuous audio recording with circular buffer. """ if(self.stream.active): - print("Already recording") + # print("Already recording") return - print('number of channels', self.channels) + # print('number of channels', self.channels) self.stream.start() @@ -139,7 +140,7 @@ class AudioRecorder: Stop continuous audio recording with circular buffer. """ if(not self.stream.active): - print("Already stopped") + # print("Already stopped") return self.stream.stop() diff --git a/audio-service/src/main.py b/audio-service/src/main.py index 7774405..adc432d 100644 --- a/audio-service/src/main.py +++ b/audio-service/src/main.py @@ -46,23 +46,20 @@ def main(): app.register_blueprint(device_bp) app.register_blueprint(metadata_bp) app.register_blueprint(settings_bp) - app.run(host='127.0.0.1', port=args.osc_port, debug=False, use_reloader=True) + app.run(host='127.0.0.1', port=settings.get_settings('http_port'), debug=False, use_reloader=True) # socketio.run(app, host='127.0.0.1', port=args.osc_port, debug=False, use_reloader=True) # Run the OSC server - try: - print(f"Starting OSC Recording Server on port {args.osc_port}") - - - - # osc_server.run_server() - except KeyboardInterrupt: - print("\nServer stopped by user.") - except Exception as e: - print(f"Error starting server: {e}") - sys.exit(1) + # try: + # print(f"Starting OSC Recording Server on port {args.osc_port}") + # # osc_server.run_server() + # except KeyboardInterrupt: + # print("\nServer stopped by user.") + # except Exception as e: + # print(f"Error starting server: {e}") + # sys.exit(1) if __name__ == "__main__": diff --git a/audio-service/src/routes/__pycache__/metadata.cpython-313.pyc b/audio-service/src/routes/__pycache__/metadata.cpython-313.pyc index 6fa02b04a610d61a38a76d75c0ac85e9b14fc72d..9bf59f755553e78a0552c3a54c1476b850fd84bc 100644 GIT binary patch delta 181 zcmeA$y=u(+nU|M~0SNxc&ddC~k#`a=z3zgb5HA_e|~;H=XP$$i2CT z?-e7X!Q^s*0!HJ>eH^lErXYo;o4Ey7G6BU;aT!m3A#B2J4idEh5yq3%Mda8lK}^fd z-XdI#n~OO87@3OnCr=QNnrths%V;z?TU?P5Yz5N?mdUrpBUp=cf$ArRNn~(?-2t}4 Ka`Q=v{fq!BH7?o! delta 460 zcmca=++oW5nU|M~0SHzw&&>R|k#`dBO<1zq>7J&$p$z0;rlf4AFHuvzo zVr0~xTrN<+Xf(NxLzc}1q{d`3x8O=9p!g{+@X5LScFNJ5Kw&TlW)5ZKV+dvmW#VHnW-(=ih$=A1O+LpjK3Rc_pOI&> z0hg#PKU6(KFl#7tFq<9|OsNWkB15zQOqQJ?QJH}uk0lzRo;_N4aw4xVF9$+TFz4h( zUO7iiF25p4pw1$FAW@|ol$xBHS(cijP?TC&np#||BJB6N^3c>F&tv!ax!K< jBp~6W$9zbG5yaMG1(K5&i)V0yBL^J%7MsN+_A>$iivecT diff --git a/audio-service/src/routes/__pycache__/recording.cpython-313.pyc b/audio-service/src/routes/__pycache__/recording.cpython-313.pyc index d45df46c484825143ab986c2cc412148b1817ab3..7ed3f26152d5a45fa6d4b3725215d65981bc7c12 100644 GIT binary patch delta 566 zcmeB{`XSEynU|M~0SGwd=4EPaMDv_Ovu#eypSqSWO4qLj?MbcN!?veXn!rXp^jp|@DlGILTjK~{INhVf~j zTK$dHPc8|+)fH^&){Oo@3-cJG5l&zZWkm5Ii(e5t$Ts%klKg^#)D*DYAX_J~d-3U^ zSh_ipy`8DP4s5F&Qi!65nj%9q6HpNturnkoGXTRH5&pr<2-k!%z=9%}1raR4tR_sw zO!5rDKz)u3Do{F&F^$2NSzeJLjZu@$5A4h$1)v>8iXcJ>M5q7>P3FmmxU4yxfy@Sm n4{Ve9xE;hrfSe{CeujQ78|Il3=FA%zZJ4*S+HB6^UdRXlu#i}< delta 575 zcmew%-Yv!ZnU|M~0SIQe&CXQX$lK05`72|&Sbjl4W?s5NVrfcdzCuxIa(+5zl0H7ISfZAhUtt3BOEteS^mx7S0P?T31-KZ%p=Kab?Vyyq{HaGCz}v zeI8>pJID?Y2xbap3})72GG&DDRTwl`{HkO;LP7$p6oM1Wuvz zwHjAgG;d5UVD)3HfLU9w0kMH0kqKn2KhRnfhp>b)8ZaU`gVnD}9^wq=jMU_8kVD`; zS12w?EGaE60(s*WdvOUcd{R@2xPd%PSa^VBcruIQ;d&t~ zZ0$@OQ-G3F8KM~{3o?nY$T37SPmW}lmS+Kqf&n{2qA~+R9#9-663i0KJ~@$7c=9rK zvB_PWVvg*7RkF~aD@iOWK?GhwPGV(JVsbXvjYSGT1Bw(ugc68Q0TMuW^8l&IdpWHI zeSi$0BSF!pd4)ym1Jfif2N_AAU@wmp!z?aGcIG7#j!evZ7=f&VOstOVo5Q&lG6Dd4 CXNlPW diff --git a/audio-service/src/routes/__pycache__/settings.cpython-313.pyc b/audio-service/src/routes/__pycache__/settings.cpython-313.pyc index bbb536edf1a7abc9a189cb48a8d511a86d701dee..8953471255dc7e63315efb9b70356e456b7561ab 100644 GIT binary patch delta 1083 zcmZ8gO-vI}5PompZdMd$>nka zBH^y*$?xp8`%{Zk=6j%Q8)9Uk84S=I5zH^TLhkLKpW#dW9o(|HY0RxpGGXY^HRdfF zLfEBuqh9FJAE0nB#Nav#@e!pmL`f3%5@Yg}<~?o=GaL*!T5gnF1A%Yc~O%E?R=xIrH?04os*M8P~M9IH7+Aeoi9 zOQ#6g*3Wc+R)U-5Elkx~bJu~%%K)Uv1Z%ZtUb9D`8)RfhlyL*OrjM&W8*^DjtP-=L z6{*^LwF8tCA`+PD1%M~^Osmc)kojt>9iR=wC`)CrgLpdtD=3yZGh2+ZU6v@an)!cP zl?ecu{BaC*oY(|RfDse`2^bQlF(otIJgdwr$;ZlqWbH$$?BlA0>nWgKt}IWLGI6X( zSb3C6Vr2n0kYw^Hp+8+4r3)ncwMt0Av@IxUt5VQ}K}^}k0#P9+W{EM`hwAEziO6A? z+!{`$R?8-K zZtM>12KT&1?X`VjxG1y~g!A8o&b-jM(|;gbIp)B*u3t9b@n=JOjd}lgL5vuV$nPRw zUjmTSEx-C^ey{CyBJUq5h@&6+kRe9CIwF6M5C!~E17who%$RrF2Xa1V1fpDP?hPq|s EKM9}ey#N3J delta 590 zcmdljc!`(qGcPX}0}x!BIU(~D^F+SG(j38D`i#MBmQ2CiQVgabIUp$FPUq3&o%rSq zqvqtDjGb(}ATi#_9!%Pd{F6(Vwz8~bF5;Q2%WTBRGdY1-TYV){kuXS!2#_dd0ul-e zMFK$T7I%7TNqli?Nl9j2dXd!RP0Zm+{2+0b0RP~SWJVwp2H1cMW+48&g<-NSi(@@w zC}T3vh-h}0f?%d#=3o{*CR3=cU{(_*6^3X|m^{qwLEJDA1RcR>3UU9-BGG!F+A6`Y#GKMph0J1wqSV6D%%aqkA~B$N<^-S}UB zwjvfFb&DmdI6v1o(gKq!Ec$o2g=S=K5V', methods=['POST']) -def set_setting(name): - value = request.json.get('value') - if value is None: - return jsonify({'status': 'error', 'message': 'Value is required'}), 400 - SettingsManager().set_settings(name, value) - return jsonify({'status': 'success', 'name': name, 'value': value}) \ No newline at end of file +@settings_bp.route('/settings/update', methods=['POST']) +def set_all_settings(): + settings = request.json.get('settings') + print (f"Received settings update: {settings}") + if settings is None: + return jsonify({'status': 'error', 'message': 'Settings are required'}), 400 + try: + for name, value in settings.items(): + print(f"Updating setting '{name}' to '{value}'") + SettingsManager().set_settings(name, value) + return jsonify({'status': 'success', 'settings': settings}) + except ValueError as e: + return jsonify({'status': 'error', 'message': str(e)}), 400 \ No newline at end of file diff --git a/audio-service/src/settings.py b/audio-service/src/settings.py index 56672e4..f385c9d 100644 --- a/audio-service/src/settings.py +++ b/audio-service/src/settings.py @@ -13,6 +13,7 @@ class SettingsManager: return cls._instance def init(self): # read settings file from executing directory + print("Initializing SettingsManager", os.getcwd()) self.settings_file = os.path.join(os.getcwd(), "settings.json") if os.path.exists(self.settings_file): with open(self.settings_file, "r") as f: @@ -46,18 +47,23 @@ class SettingsManager: #see if input device is in "devices", if not set to the first index if input is not None and any(d['name'] == input["name"] for d in input_devices): - print(f"Using saved input device index: {input}") + # print(f"Using saved input device index: {input}") + pass else: input = input_devices[0] if input_devices else None self.settings["input_device"] = input #see if output device is in "devices", if not set to the first index if output is not None and any(d['name'] == output["name"] for d in output_devices): - print(f"Using saved output device index: {output}") + # print(f"Using saved output device index: {output}") + pass else: output = output_devices[0] if output_devices else None self.settings["output_device"] = output + if not "http_port" in self.settings: + self.settings["http_port"] = 5010 + self.save_settings() @@ -71,6 +77,8 @@ class SettingsManager: return self.settings def set_settings(self, name, value): + if(name not in self.settings): + raise ValueError(f"Setting '{name}' not found.") self.settings[name] = value self.save_settings() diff --git a/audio-service/src/windows_audio.py b/audio-service/src/windows_audio.py index e9a574a..652d779 100644 --- a/audio-service/src/windows_audio.py +++ b/audio-service/src/windows_audio.py @@ -25,11 +25,11 @@ class WindowsAudioManager: wasapi_device_indexes = api['devices'] break # print(f"Host APIs: {host_apis}") - print(f"WASAPI Device Indexes: {wasapi_device_indexes}") + # print(f"WASAPI Device Indexes: {wasapi_device_indexes}") wasapi_device_indexes = set(wasapi_device_indexes) if wasapi_device_indexes is not None else set() self.devices = [dev for dev in sd.query_devices() if dev['index'] in wasapi_device_indexes] # self.devices = sd.query_devices() - print(f"devices: {self.devices}") + # print(f"devices: {self.devices}") self.default_input = sd.default.device[0] self.default_output = sd.default.device[1] diff --git a/electron-ui/settings.json b/electron-ui/settings.json new file mode 100644 index 0000000..17e6c51 --- /dev/null +++ b/electron-ui/settings.json @@ -0,0 +1,16 @@ +{ + "input_device": { + "index": 49, + "name": "Microphone (Logi C615 HD WebCam)", + "channels": 1, + "default_samplerate": 48000.0 + }, + "output_device": { + "index": 40, + "name": "Speakers (Realtek(R) Audio)", + "channels": 2, + "default_samplerate": 48000.0 + }, + "save_path": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\electron-ui\\recordings", + "recording_length": 15 +} \ No newline at end of file diff --git a/electron-ui/src/ipc/audio/channels.ts b/electron-ui/src/ipc/audio/channels.ts index ba3dda9..cceef3b 100644 --- a/electron-ui/src/ipc/audio/channels.ts +++ b/electron-ui/src/ipc/audio/channels.ts @@ -1,5 +1,7 @@ const AudioChannels = { LOAD_AUDIO_BUFFER: 'audio:loadAudioBuffer', + GET_PORT: 'audio:getPort', + RESTART_SERVICE: 'audio:restartService', } as const; export default AudioChannels; diff --git a/electron-ui/src/ipc/audio/main.ts b/electron-ui/src/ipc/audio/main.ts index caecc76..5def87a 100644 --- a/electron-ui/src/ipc/audio/main.ts +++ b/electron-ui/src/ipc/audio/main.ts @@ -2,6 +2,7 @@ import { ipcMain } from 'electron'; import fs from 'fs'; import AudioChannels from './channels'; import { LoadAudioBufferArgs, LoadAudioBufferResult } from './types'; +import PythonSubprocessManager from '../../main/service'; export default function registerAudioIpcHandlers() { ipcMain.handle( @@ -15,4 +16,25 @@ export default function registerAudioIpcHandlers() { } }, ); + + ipcMain.handle(AudioChannels.GET_PORT, async () => { + try { + if (PythonSubprocessManager.instance?.portNumber) { + return { port: PythonSubprocessManager.instance.portNumber }; + } + + return { error: 'Port number not available yet.' }; + } catch (err: any) { + return { error: err.message }; + } + }); + + ipcMain.handle(AudioChannels.RESTART_SERVICE, async () => { + try { + PythonSubprocessManager.instance?.restart(); + return { success: true }; + } catch (err: any) { + return { success: false, error: err.message }; + } + }); } diff --git a/electron-ui/src/ipc/audio/types.ts b/electron-ui/src/ipc/audio/types.ts index c0cdd0a..49443dd 100644 --- a/electron-ui/src/ipc/audio/types.ts +++ b/electron-ui/src/ipc/audio/types.ts @@ -6,3 +6,22 @@ export interface LoadAudioBufferResult { buffer?: Buffer; error?: string; } + +export interface GetPortResult { + port?: number; + error?: string; +} + +export interface SetPortArgs { + port: number; +} + +export interface SetPortResult { + success: boolean; + error?: string; +} + +export interface RestartServiceResult { + success: boolean; + error?: string; +} diff --git a/electron-ui/src/main/main.ts b/electron-ui/src/main/main.ts index badfdee..caf9382 100644 --- a/electron-ui/src/main/main.ts +++ b/electron-ui/src/main/main.ts @@ -16,6 +16,7 @@ import log from 'electron-log'; import MenuBuilder from './menu'; import { resolveHtmlPath } from './util'; import registerFileIpcHandlers from '../ipc/audio/main'; +import PythonSubprocessManager from './service'; class AppUpdater { constructor() { @@ -110,6 +111,10 @@ const createWindow = async () => { }); registerFileIpcHandlers(); + + const pythonManager = new PythonSubprocessManager('src/main.py'); + + pythonManager.start(); // Remove this if your app does not use auto updates // eslint-disable-next-line new AppUpdater(); diff --git a/electron-ui/src/main/preload.ts b/electron-ui/src/main/preload.ts index 0b15825..1487c23 100644 --- a/electron-ui/src/main/preload.ts +++ b/electron-ui/src/main/preload.ts @@ -1,8 +1,7 @@ // Disable no-unused-vars, broken for spread args /* eslint no-unused-vars: off */ import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'; -import FileChannels from '../ipc/audio/channels'; -import { LoadAudioBufferArgs, ReadTextArgs } from '../ipc/audio/types'; +import { LoadAudioBufferArgs } from '../ipc/audio/types'; import AudioChannels from '../ipc/audio/channels'; // import '../ipc/file/preload'; // Import file API preload to ensure it runs and exposes the API @@ -41,10 +40,8 @@ const audioHandler = { filePath, } satisfies LoadAudioBufferArgs), - readText: (filePath: string) => - ipcRenderer.invoke(AudioChannels.READ_TEXT, { - filePath, - } satisfies ReadTextArgs), + getPort: () => ipcRenderer.invoke(AudioChannels.GET_PORT), + restartService: () => ipcRenderer.invoke(AudioChannels.RESTART_SERVICE), }; contextBridge.exposeInMainWorld('audio', audioHandler); diff --git a/electron-ui/src/main/service.ts b/electron-ui/src/main/service.ts index e69de29..5013be9 100644 --- a/electron-ui/src/main/service.ts +++ b/electron-ui/src/main/service.ts @@ -0,0 +1,79 @@ +import { spawn, ChildProcessWithoutNullStreams } from 'child_process'; +import path from 'path'; + +export default class PythonSubprocessManager { + // eslint-disable-next-line no-use-before-define + public static instance: PythonSubprocessManager | null = null; + + private process: ChildProcessWithoutNullStreams | null = null; + + private scriptPath: string; + + private working_dir: string = path.join( + __dirname, + '..', + '..', + '..', + 'audio-service', + ); + + public portNumber: number | null = null; + + constructor(scriptPath: string) { + this.scriptPath = scriptPath; + PythonSubprocessManager.instance = this; + } + + start(args: string[] = []): void { + if (this.process) { + throw new Error('Process already running.'); + } + console.log(`Using Python working directory at: ${this.working_dir}`); + console.log(`Starting Python subprocess with script: ${this.scriptPath}`); + this.process = spawn( + 'venv/Scripts/python.exe', + [this.scriptPath, ...args], + { + cwd: this.working_dir, + detached: false, + stdio: 'pipe', + }, + ); + this.process.stdout.on('data', (data: Buffer) => { + console.log(`Python stdout: ${data.toString()}`); + }); + this.process.stderr.on('data', (data: Buffer) => { + console.error(`Python stderr: ${data.toString()}`); + const lines = data.toString().split('\n'); + // eslint-disable-next-line no-restricted-syntax + for (const line of lines) { + const match = line.match(/Running on .*:(\d+)/); + if (match) { + const port = parseInt(match[1], 10); + console.log(`Detected port: ${port}`); + this.portNumber = port; + } + } + }); + this.process.on('exit', () => { + console.log('Python subprocess exited.'); + this.process = null; + }); + } + + stop(): void { + if (this.process) { + this.process.kill(); + this.process = null; + } + } + + restart(args: string[] = []): void { + this.stop(); + this.start(args); + } + + isHealthy(): boolean { + return !!this.process && !this.process.killed; + } +} diff --git a/electron-ui/src/renderer/App.tsx b/electron-ui/src/renderer/App.tsx index e3d8501..2106d9d 100644 --- a/electron-ui/src/renderer/App.tsx +++ b/electron-ui/src/renderer/App.tsx @@ -14,7 +14,7 @@ import { useAppDispatch, useAppSelector } from './hooks'; import { store } from '../redux/main'; import { useNavigate } from 'react-router-dom'; import SettingsPage from './Settings'; -import { apiFetch } from './api'; +import apiFetch from './api'; function MainPage() { const dispatch = useAppDispatch(); diff --git a/electron-ui/src/renderer/Settings.tsx b/electron-ui/src/renderer/Settings.tsx index 4f2cfec..748f60b 100644 --- a/electron-ui/src/renderer/Settings.tsx +++ b/electron-ui/src/renderer/Settings.tsx @@ -5,7 +5,7 @@ import './App.css'; import TextField from '@mui/material/TextField'; import Select from '@mui/material/Select'; import MenuItem from '@mui/material/MenuItem'; -import { apiFetch } from './api'; +import apiFetch from './api'; type AudioDevice = { index: number; @@ -57,10 +57,26 @@ async function fetchSettings(): Promise { }); } -const sendSettingsToBackend = (settings: Settings) => { +const sendSettingsToBackend = async (settings: Settings) => { // Replace with actual backend call // Example: window.api.updateSettings(settings); console.log('Settings updated:', settings); + await apiFetch('settings/update', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ settings }), + }) + .then((res) => res.json()) + .then((data) => { + console.log('Settings update response:', data); + if (data.status === 'success') { + window.audio.restartService(); + } + return data; + }) + .catch((error) => { + console.error('Error updating settings:', error); + }); }; export default function SettingsPage() { @@ -95,11 +111,10 @@ export default function SettingsPage() { }); }, []); - useEffect(() => { - sendSettingsToBackend(settings); - }, [settings]); + useEffect(() => {}, [settings]); const handleChange = () => { + sendSettingsToBackend(settings); // const { name, value } = e.target; // setSettings((prev) => ({ // ...prev, @@ -142,7 +157,7 @@ export default function SettingsPage() { type="text" name="httpPort" value={settings.http_port} - onBlur={() => console.log('port blur')} + onBlur={() => handleChange()} onChange={(e) => { if (!Number.isNaN(Number(e.target.value))) { setSettings((prev) => ({ @@ -168,8 +183,12 @@ export default function SettingsPage() { if (newDevice) { setSettings((prev) => ({ ...prev, - inputDevice: newDevice, + input_device: newDevice, })); + sendSettingsToBackend({ + ...settings, + input_device: newDevice, + }); } }} className="ml-2 w-64" @@ -196,6 +215,10 @@ export default function SettingsPage() { ...prev, output_device: newDevice, })); + sendSettingsToBackend({ + ...settings, + output_device: newDevice, + }); } }} className="ml-2 w-64" @@ -222,6 +245,7 @@ export default function SettingsPage() { })); } }} + onBlur={() => handleChange()} className="ml-2 w-[150px]" /> diff --git a/electron-ui/src/renderer/api.ts b/electron-ui/src/renderer/api.ts index a98a033..3b87142 100644 --- a/electron-ui/src/renderer/api.ts +++ b/electron-ui/src/renderer/api.ts @@ -1,13 +1,13 @@ -const getBaseUrl = () => { +const getBaseUrl = async () => { + const port = await window.audio.getPort(); + if (port.error || !port.port) { + return `http://localhost:5010`; + } // You can store the base URL in localStorage, a config file, or state - return localStorage.getItem('baseUrl') || 'http://localhost:5010'; + return `http://localhost:${port.port}`; }; -export function apiFetch(endpoint: string, options = {}) { - const url = `${getBaseUrl()}/${endpoint}`; +export default async function apiFetch(endpoint: string, options = {}) { + const url = `${await getBaseUrl()}/${endpoint}`; return fetch(url, options); } - -export function setBaseUrl(baseUrl: string) { - localStorage.setItem('baseUrl', baseUrl); -} diff --git a/electron-ui/src/renderer/components/ClipList.tsx b/electron-ui/src/renderer/components/ClipList.tsx index 7e450fd..80f1f0d 100644 --- a/electron-ui/src/renderer/components/ClipList.tsx +++ b/electron-ui/src/renderer/components/ClipList.tsx @@ -15,7 +15,7 @@ import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; import AudioTrimmer from './AudioTrimer'; import { ClipMetadata } from '../../redux/types'; import { useAppDispatch, useAppSelector } from '../hooks'; -import { apiFetch } from '../api'; +import apiFetch from '../api'; export interface ClipListProps { collection: string; diff --git a/electron-ui/src/renderer/preload.d.ts b/electron-ui/src/renderer/preload.d.ts index 792d060..2f6ee95 100644 --- a/electron-ui/src/renderer/preload.d.ts +++ b/electron-ui/src/renderer/preload.d.ts @@ -1,10 +1,10 @@ -import { ElectronHandler, FileHandler } from '../main/preload'; +import { ElectronHandler, AudioHandler } from '../main/preload'; declare global { // eslint-disable-next-line no-unused-vars interface Window { electron: ElectronHandler; - audio: FileHandler; + audio: AudioHandler; } }