@@ -252,6 +252,34 @@ impl InferenceEngine {
252252 self . backend . unload ( ) ;
253253 }
254254
255+ /// Update engine configuration. If backend or backend model settings changed,
256+ /// re-creates the backend and returns `true` (meaning the caller should reload).
257+ pub fn update_config ( & mut self , new_config : Arc < AppConfig > ) -> bool {
258+ let backend_changed = self . config . engine . backend != new_config. engine . backend
259+ || ( new_config. engine . backend == BackendChoice :: WhisperCpp
260+ && self . config . engine . whisper_cpp != new_config. engine . whisper_cpp )
261+ || ( new_config. engine . backend == BackendChoice :: Moonshine
262+ && self . config . engine . moonshine != new_config. engine . moonshine )
263+ || ( new_config. engine . backend == BackendChoice :: Parakeet
264+ && self . config . engine . parakeet != new_config. engine . parakeet )
265+ || ( new_config. engine . backend == BackendChoice :: RemoteOpenAi
266+ && self . config . engine . remote_openai != new_config. engine . remote_openai ) ;
267+
268+ self . config = new_config. clone ( ) ;
269+
270+ if backend_changed {
271+ info ! (
272+ "Inference backend configuration changed, switching backend to {:?}" ,
273+ new_config. engine. backend
274+ ) ;
275+ self . backend . unload ( ) ;
276+ self . backend = build_backend ( & new_config) ;
277+ true
278+ } else {
279+ false
280+ }
281+ }
282+
255283 /// Transcribe and post-process. Returns the final text.
256284 pub fn process ( & self , req : InferenceRequest ) -> Result < InferenceOutput > {
257285 if req. audio . is_empty ( ) {
@@ -533,6 +561,19 @@ pub fn run_worker(
533561 config : Arc < AppConfig > ,
534562 rx : Receiver < InferenceRequest > ,
535563 tx : Sender < InferenceOutput > ,
564+ ) {
565+ let ( _dummy_tx, dummy_rx) = crossbeam_channel:: unbounded ( ) ;
566+ run_worker_with_config ( config, rx, tx, dummy_rx) ;
567+ }
568+
569+ /// Run the inference engine on a dedicated OS thread with dynamic config reloading.
570+ /// Receives `InferenceRequest` from `rx`, sends `InferenceOutput` to `tx`,
571+ /// and updates/reloads the backend whenever `config_rx` receives a new `AppConfig`.
572+ pub fn run_worker_with_config (
573+ config : Arc < AppConfig > ,
574+ rx : Receiver < InferenceRequest > ,
575+ tx : Sender < InferenceOutput > ,
576+ config_rx : Receiver < Arc < AppConfig > > ,
536577) {
537578 std:: thread:: Builder :: new ( )
538579 . name ( "voxctrl-inference" . into ( ) )
@@ -554,42 +595,70 @@ pub fn run_worker(
554595 }
555596 } ;
556597
557- while let Ok ( req) = rx. recv ( ) {
558- if !loaded {
559- match engine. load ( ) {
560- Ok ( ( ) ) => {
561- info ! ( "Inference engine ready (loaded on demand)" ) ;
562- loaded = true ;
563- }
564- Err ( e) => {
565- error ! ( "Inference backend still not loadable: {e:#}" ) ;
566- let _ = tx. send ( InferenceOutput {
567- text : String :: new ( ) ,
568- target_id : req. target_id ,
569- raw_text : String :: new ( ) ,
570- inference_ms : 0 ,
571- language : String :: new ( ) ,
572- error : Some ( format ! ( "{e:#}" ) ) ,
573- } ) ;
574- continue ;
598+ loop {
599+ crossbeam_channel:: select! {
600+ recv( rx) -> req_res => {
601+ let req = match req_res {
602+ Ok ( r) => r,
603+ Err ( _) => break ,
604+ } ;
605+
606+ if !loaded {
607+ match engine. load( ) {
608+ Ok ( ( ) ) => {
609+ info!( "Inference engine ready (loaded on demand)" ) ;
610+ loaded = true ;
611+ }
612+ Err ( e) => {
613+ error!( "Inference backend still not loadable: {e:#}" ) ;
614+ let _ = tx. send( InferenceOutput {
615+ text: String :: new( ) ,
616+ target_id: req. target_id,
617+ raw_text: String :: new( ) ,
618+ inference_ms: 0 ,
619+ language: String :: new( ) ,
620+ error: Some ( format!( "{e:#}" ) ) ,
621+ } ) ;
622+ continue ;
623+ }
624+ }
575625 }
576- }
577- }
578626
579- match engine. process ( req) {
580- Ok ( output) => {
581- let _ = tx. send ( output) ;
627+ match engine. process( req) {
628+ Ok ( output) => {
629+ let _ = tx. send( output) ;
630+ }
631+ Err ( e) => {
632+ error!( "Inference error: {:?}" , e) ;
633+ let _ = tx. send( InferenceOutput {
634+ text: "" . to_string( ) ,
635+ target_id: "" . to_string( ) ,
636+ raw_text: "" . to_string( ) ,
637+ inference_ms: 0 ,
638+ language: "" . to_string( ) ,
639+ error: Some ( format!( "{e:#}" ) ) ,
640+ } ) ;
641+ }
642+ }
582643 }
583- Err ( e) => {
584- error ! ( "Inference error: {:?}" , e) ;
585- let _ = tx. send ( InferenceOutput {
586- text : "" . to_string ( ) ,
587- target_id : "" . to_string ( ) ,
588- raw_text : "" . to_string ( ) ,
589- inference_ms : 0 ,
590- language : "" . to_string ( ) ,
591- error : Some ( format ! ( "{e:#}" ) ) ,
592- } ) ;
644+ recv( config_rx) -> new_cfg_res => {
645+ let new_cfg = match new_cfg_res {
646+ Ok ( c) => c,
647+ Err ( _) => break ,
648+ } ;
649+ let needs_reload = engine. update_config( new_cfg) ;
650+ if needs_reload {
651+ loaded = match engine. load( ) {
652+ Ok ( ( ) ) => {
653+ info!( "Inference engine ready with new backend" ) ;
654+ true
655+ }
656+ Err ( e) => {
657+ error!( "Failed to load new inference backend: {e:#}" ) ;
658+ false
659+ }
660+ } ;
661+ }
593662 }
594663 }
595664 }
@@ -649,4 +718,25 @@ mod tests {
649718 assert_eq ! ( backend. name( ) , "remote-openai" ) ;
650719 assert ! ( backend. is_loaded( ) ) ;
651720 }
721+
722+ #[ test]
723+ fn test_engine_update_config_switches_backend ( ) {
724+ let cfg = AppConfig :: default ( ) ;
725+ let mut engine = InferenceEngine :: new ( Arc :: new ( cfg. clone ( ) ) ) ;
726+ assert_eq ! ( engine. backend. name( ) , "whisper-cpp" ) ;
727+
728+ let mut new_cfg = cfg. clone ( ) ;
729+ new_cfg. engine . backend = BackendChoice :: RemoteOpenAi ;
730+ new_cfg. engine . remote_openai . endpoint = "http://localhost:5000/v1" . to_string ( ) ;
731+ let reloaded = engine. update_config ( Arc :: new ( new_cfg) ) ;
732+ assert ! ( reloaded) ;
733+ assert_eq ! ( engine. backend. name( ) , "remote-openai" ) ;
734+
735+ // Non-backend config change should not trigger backend reload
736+ let mut features_cfg = engine. config . as_ref ( ) . clone ( ) ;
737+ features_cfg. features . remove_fillers = !features_cfg. features . remove_fillers ;
738+ let reloaded_features = engine. update_config ( Arc :: new ( features_cfg) ) ;
739+ assert ! ( !reloaded_features) ;
740+ assert_eq ! ( engine. backend. name( ) , "remote-openai" ) ;
741+ }
652742}
0 commit comments