From 47ce4cafdfe5d543eb3b028fb0c1121cb7d6a074 Mon Sep 17 00:00:00 2001 From: James Pine Date: Fri, 13 Mar 2026 04:18:34 -0700 Subject: [PATCH 1/5] fix: patch VoiceEncoder.forward to cast float64 mels to float32 The previous approach of patching librosa.load didn't work because melspectrogram itself performs float64 math (numpy dot, signal.lfilter) regardless of input dtype. The actual mismatch happens when pack() creates a float64 tensor from the mel arrays and passes it into the float32 LSTM weights in VoiceEncoder.forward(). Fix by monkey-patching VoiceEncoder.forward() to call mels.float() before the LSTM, ensuring the input always matches the model dtype. --- backend/backends/chatterbox_backend.py | 15 +++++++++++++++ backend/backends/chatterbox_turbo_backend.py | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/backend/backends/chatterbox_backend.py b/backend/backends/chatterbox_backend.py index 88f87d4e..826d7c05 100644 --- a/backend/backends/chatterbox_backend.py +++ b/backend/backends/chatterbox_backend.py @@ -178,6 +178,21 @@ def _patched_load(*args, **kwargs): progress_manager.mark_complete(model_name) task_manager.complete_download(model_name) + # Monkey-patch VoiceEncoder.forward to cast input to float32. + # The upstream melspectrogram returns float64 numpy arrays when + # hp.normalized_mels is False (the default). pack() preserves + # the dtype, so double tensors hit float32 LSTM weights → + # "expected m1 and m2 to have the same dtype: float != double". + _ve = self.model.ve + _orig_ve_forward = _ve.forward.__func__ if hasattr(_ve.forward, '__func__') else _ve.forward + + import types + + def _f32_forward(self_ve, mels): + return _orig_ve_forward(self_ve, mels.float()) + + _ve.forward = types.MethodType(_f32_forward, _ve) + logger.info("Chatterbox Multilingual TTS loaded successfully") except ImportError as e: diff --git a/backend/backends/chatterbox_turbo_backend.py b/backend/backends/chatterbox_turbo_backend.py index 16bb5d70..6c5bf842 100644 --- a/backend/backends/chatterbox_turbo_backend.py +++ b/backend/backends/chatterbox_turbo_backend.py @@ -178,6 +178,21 @@ def _patched_load(*args, **kwargs): progress_manager.mark_complete(model_name) task_manager.complete_download(model_name) + # Monkey-patch VoiceEncoder.forward to cast input to float32. + # The upstream melspectrogram returns float64 numpy arrays when + # hp.normalized_mels is False (the default). pack() preserves + # the dtype, so double tensors hit float32 LSTM weights → + # "expected m1 and m2 to have the same dtype: float != double". + _ve = self.model.ve + _orig_ve_forward = _ve.forward.__func__ if hasattr(_ve.forward, '__func__') else _ve.forward + + import types + + def _f32_forward(self_ve, mels): + return _orig_ve_forward(self_ve, mels.float()) + + _ve.forward = types.MethodType(_f32_forward, _ve) + logger.info("Chatterbox Turbo TTS loaded successfully") except ImportError as e: From cac80f6af0fa034310414f54f152735d25b16579 Mon Sep 17 00:00:00 2001 From: James Pine Date: Fri, 13 Mar 2026 04:44:13 -0700 Subject: [PATCH 2/5] feat: add per-model unload endpoint and UI button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - POST /models/{model_name}/unload — unloads a specific model from memory without deleting from disk, supports all engine types - Frontend: Unload button in model detail dialog when model is loaded - Delete button remains disabled while loaded (unload first) --- .../ServerSettings/ModelManagement.tsx | 82 ++++++++++++++----- app/src/lib/api/client.ts | 6 ++ backend/main.py | 64 ++++++++++++++- 3 files changed, 131 insertions(+), 21 deletions(-) diff --git a/app/src/components/ServerSettings/ModelManagement.tsx b/app/src/components/ServerSettings/ModelManagement.tsx index bcc944b2..9811f643 100644 --- a/app/src/components/ServerSettings/ModelManagement.tsx +++ b/app/src/components/ServerSettings/ModelManagement.tsx @@ -13,6 +13,7 @@ import { RotateCcw, Scale, Trash2, + Unplug, X, } from 'lucide-react'; import { useCallback, useMemo, useState } from 'react'; @@ -300,6 +301,27 @@ export function ModelManagement() { }, }); + const unloadMutation = useMutation({ + mutationFn: async (modelName: string) => { + return await apiClient.unloadModel(modelName); + }, + onSuccess: async (_data, modelName) => { + toast({ + title: 'Model unloaded', + description: `${modelName} has been unloaded from memory.`, + }); + await queryClient.invalidateQueries({ queryKey: ['modelStatus'], refetchType: 'all' }); + await queryClient.refetchQueries({ queryKey: ['modelStatus'] }); + }, + onError: (error: Error) => { + toast({ + title: 'Unload failed', + description: error.message, + variant: 'destructive', + }); + }, + }); + const formatSize = (sizeMb?: number): string => { if (!sizeMb) return 'Unknown size'; if (sizeMb < 1024) return `${sizeMb.toFixed(1)} MB`; @@ -697,26 +719,46 @@ export function ModelManagement() { ) : freshSelectedModel.downloaded ? ( - +
+ {freshSelectedModel.loaded && ( + + )} + +
) : (