feat: added local separator model demucs_4s support

This commit is contained in:
Xiaohan-Tian
2026-05-27 20:17:17 -07:00
parent e2a7d1c285
commit 2140e944f2
18 changed files with 723 additions and 149 deletions
@@ -29,6 +29,7 @@ const configState = new Map<string, unknown>([
['general.local_browser.context_length', 65536],
['general.local_browser.model_url', 'https://huggingface.co/notabilia/gemma-4-E4B-it-litert-lm/resolve/main/gemma-4-E4B-it-web.task'],
['general.uvr5_web_runtime.mdx_net_model_url', 'https://huggingface.co/notabilia/uvr5-models/resolve/main/UVR-MDX-NET-Inst_HQ_3.onnx'],
['general.uvr5_web_runtime.htdemucs_4s_model_url', 'https://huggingface.co/notabilia/uvr5-models/resolve/main/htdemucs_embedded.onnx'],
['general.soundfont.base_url', 'https://cdn.jsdelivr.net/npm/soundfont-for-samplers/FluidR3_GM/'],
['general.kgone.enabled', false],
['general.kgone.base_url', 'http://127.0.0.1:8000'],
@@ -140,6 +141,7 @@ describe('GeneralSettings', () => {
expect(await screen.findByDisplayValue('https://huggingface.co/notabilia/gemma-4-E4B-it-litert-lm/resolve/main/gemma-4-E4B-it-web.task')).toBeTruthy();
expect(screen.getByDisplayValue('https://huggingface.co/notabilia/uvr5-models/resolve/main/UVR-MDX-NET-Inst_HQ_3.onnx')).toBeTruthy();
expect(screen.getByDisplayValue('https://huggingface.co/notabilia/uvr5-models/resolve/main/htdemucs_embedded.onnx')).toBeTruthy();
const inputs = screen.getAllByRole('textbox');
const gemmaUrlInput = inputs.find(input =>
@@ -148,22 +150,30 @@ describe('GeneralSettings', () => {
const uvr5UrlInput = inputs.find(input =>
(input as HTMLInputElement).value.includes('UVR-MDX-NET-Inst_HQ_3.onnx'),
) as HTMLInputElement | undefined;
const htdemucsUrlInput = inputs.find(input =>
(input as HTMLInputElement).value.includes('htdemucs_embedded.onnx'),
) as HTMLInputElement | undefined;
expect(gemmaUrlInput).toBeTruthy();
expect(uvr5UrlInput).toBeTruthy();
expect(htdemucsUrlInput).toBeTruthy();
fireEvent.change(gemmaUrlInput!, { target: { value: 'https://example.com/gemma.task' } });
fireEvent.change(uvr5UrlInput!, { target: { value: 'https://example.com/uvr5.onnx' } });
fireEvent.change(htdemucsUrlInput!, { target: { value: 'https://example.com/htdemucs.onnx' } });
await waitFor(() => {
expect(configManagerMock.set).toHaveBeenCalledWith('general.local_browser.model_url', 'https://example.com/gemma.task');
expect(configManagerMock.set).toHaveBeenCalledWith('general.uvr5_web_runtime.mdx_net_model_url', 'https://example.com/uvr5.onnx');
expect(configManagerMock.set).toHaveBeenCalledWith('general.uvr5_web_runtime.htdemucs_4s_model_url', 'https://example.com/htdemucs.onnx');
});
});
it('restores default download URLs and deletes the UVR5 model cache', async () => {
localSeparatorModelCacheMock.exists
.mockResolvedValueOnce(true)
.mockResolvedValueOnce(true)
.mockResolvedValueOnce(false)
.mockResolvedValueOnce(false);
render(<GeneralSettings />);
@@ -173,6 +183,7 @@ describe('GeneralSettings', () => {
const restoreLinks = screen.getAllByText('Restore default');
fireEvent.click(restoreLinks[0]);
fireEvent.click(restoreLinks[1]);
fireEvent.click(restoreLinks[2]);
const uvr5DeleteButton = screen.getAllByRole('button', { name: 'Delete Cached Model' })[1];
expect(uvr5DeleteButton).not.toBeDisabled();
fireEvent.click(uvr5DeleteButton);
@@ -186,6 +197,10 @@ describe('GeneralSettings', () => {
'general.uvr5_web_runtime.mdx_net_model_url',
'https://huggingface.co/notabilia/uvr5-models/resolve/main/UVR-MDX-NET-Inst_HQ_3.onnx',
);
expect(configManagerMock.set).toHaveBeenCalledWith(
'general.uvr5_web_runtime.htdemucs_4s_model_url',
'https://huggingface.co/notabilia/uvr5-models/resolve/main/htdemucs_embedded.onnx',
);
expect(localSeparatorModelCacheMock.delete).toHaveBeenCalled();
});
@@ -12,7 +12,10 @@ import {
normalizeLocalLLMContextLength,
type LocalLLMContextLength,
} from '../../../util/localLLMConfig';
import { LOCAL_SEPARATOR_DEFAULT_MODEL_URL } from '../../../util/local-separator/config';
import {
LOCAL_SEPARATOR_MODEL_CONFIGS,
LOCAL_SEPARATOR_MODEL_IDS,
} from '../../../util/local-separator/config';
const GeneralSettings: React.FC = () => {
const [llmProvider, setLlmProvider] = useState<string>(LOCAL_LLM_PROVIDER_KEY);
@@ -39,9 +42,12 @@ const GeneralSettings: React.FC = () => {
const [localModelState, setLocalModelState] = useState<LocalLLMModelState>(LocalLLMModelManager.getState());
const [localModelUrl, setLocalModelUrl] = useState<string>('');
const [uvr5ModelUrl, setUvr5ModelUrl] = useState<string>('');
const [htdemucsModelUrl, setHtdemucsModelUrl] = useState<string>('');
const [isUvr5ModelCached, setIsUvr5ModelCached] = useState<boolean>(false);
const [isCheckingUvr5ModelCache, setIsCheckingUvr5ModelCache] = useState<boolean>(false);
const [isDeletingUvr5Model, setIsDeletingUvr5Model] = useState<boolean>(false);
const [isHtdemucsModelCached, setIsHtdemucsModelCached] = useState<boolean>(false);
const [isDeletingHtdemucsModel, setIsDeletingHtdemucsModel] = useState<boolean>(false);
const configManager = ConfigManager.instance();
@@ -62,10 +68,16 @@ const GeneralSettings: React.FC = () => {
const refreshUvr5ModelCacheState = useCallback(async () => {
setIsCheckingUvr5ModelCache(true);
try {
setIsUvr5ModelCached(await LocalSeparatorModelCache.exists());
const [mdxCached, demucsCached] = await Promise.all([
LocalSeparatorModelCache.exists(LOCAL_SEPARATOR_MODEL_CONFIGS[LOCAL_SEPARATOR_MODEL_IDS.mdxMedium]),
LocalSeparatorModelCache.exists(LOCAL_SEPARATOR_MODEL_CONFIGS[LOCAL_SEPARATOR_MODEL_IDS.htdemucs4s]),
]);
setIsUvr5ModelCached(mdxCached);
setIsHtdemucsModelCached(demucsCached);
} catch (error) {
console.error('Failed to check UVR5 cached model state:', error);
setIsUvr5ModelCached(false);
setIsHtdemucsModelCached(false);
} finally {
setIsCheckingUvr5ModelCache(false);
}
@@ -95,7 +107,14 @@ const GeneralSettings: React.FC = () => {
setCompatibleModel((configManager.get('general.openai_compatible.model') as string) || '');
setLocalContextLength(normalizeLocalLLMContextLength(configManager.get('general.local_browser.context_length')));
setLocalModelUrl((configManager.get('general.local_browser.model_url') as string) || LOCAL_LLM_DEFAULT_MODEL_URL);
setUvr5ModelUrl((configManager.get('general.uvr5_web_runtime.mdx_net_model_url') as string) || LOCAL_SEPARATOR_DEFAULT_MODEL_URL);
setUvr5ModelUrl(
(configManager.get('general.uvr5_web_runtime.mdx_net_model_url') as string)
|| LOCAL_SEPARATOR_MODEL_CONFIGS[LOCAL_SEPARATOR_MODEL_IDS.mdxMedium].download.defaultUrl,
);
setHtdemucsModelUrl(
(configManager.get('general.uvr5_web_runtime.htdemucs_4s_model_url') as string)
|| LOCAL_SEPARATOR_MODEL_CONFIGS[LOCAL_SEPARATOR_MODEL_IDS.htdemucs4s].download.defaultUrl,
);
setSoundfontBaseUrl((configManager.get('general.soundfont.base_url') as string) || '');
setKgoneEnabled((configManager.get('general.kgone.enabled') as boolean) ?? false);
setKgoneBaseUrl((configManager.get('general.kgone.base_url') as string) || '');
@@ -245,6 +264,11 @@ const GeneralSettings: React.FC = () => {
debouncedSave('general.uvr5_web_runtime.mdx_net_model_url', value);
};
const handleHtdemucsModelUrlChange = (value: string) => {
setHtdemucsModelUrl(value);
debouncedSave('general.uvr5_web_runtime.htdemucs_4s_model_url', value);
};
const handleDeleteLocalModel = async () => {
try {
await LocalLLMModelManager.deleteCachedModel();
@@ -256,7 +280,7 @@ const GeneralSettings: React.FC = () => {
const handleDeleteUvr5Model = async () => {
setIsDeletingUvr5Model(true);
try {
await LocalSeparatorModelCache.delete();
await LocalSeparatorModelCache.delete(LOCAL_SEPARATOR_MODEL_CONFIGS[LOCAL_SEPARATOR_MODEL_IDS.mdxMedium]);
setIsUvr5ModelCached(false);
} catch (error) {
console.error('Failed to delete UVR5 cached model:', error);
@@ -266,6 +290,19 @@ const GeneralSettings: React.FC = () => {
}
};
const handleDeleteHtdemucsModel = async () => {
setIsDeletingHtdemucsModel(true);
try {
await LocalSeparatorModelCache.delete(LOCAL_SEPARATOR_MODEL_CONFIGS[LOCAL_SEPARATOR_MODEL_IDS.htdemucs4s]);
setIsHtdemucsModelCached(false);
} catch (error) {
console.error('Failed to delete HTDemucs cached model:', error);
} finally {
setIsDeletingHtdemucsModel(false);
await refreshUvr5ModelCacheState();
}
};
const handleLocalContextLengthChange = async (value: string) => {
const parsed = Number(value);
const normalized = normalizeLocalLLMContextLength(parsed);
@@ -448,7 +485,7 @@ const GeneralSettings: React.FC = () => {
<input
type="text"
className="settings-input"
placeholder={`e.g. ${LOCAL_SEPARATOR_DEFAULT_MODEL_URL}`}
placeholder={`e.g. ${LOCAL_SEPARATOR_MODEL_CONFIGS[LOCAL_SEPARATOR_MODEL_IDS.mdxMedium].download.defaultUrl}`}
value={uvr5ModelUrl}
onChange={(e) => handleUvr5ModelUrlChange(e.target.value)}
/>
@@ -458,7 +495,9 @@ const GeneralSettings: React.FC = () => {
href="#"
onClick={(e) => {
e.preventDefault();
handleUvr5ModelUrlChange(LOCAL_SEPARATOR_DEFAULT_MODEL_URL);
handleUvr5ModelUrlChange(
LOCAL_SEPARATOR_MODEL_CONFIGS[LOCAL_SEPARATOR_MODEL_IDS.mdxMedium].download.defaultUrl,
);
}}
style={{ color: '#5a9fd4', textDecoration: 'underline', cursor: 'pointer' }}
>
@@ -477,6 +516,45 @@ const GeneralSettings: React.FC = () => {
{isDeletingUvr5Model ? 'Deleting...' : 'Delete Cached Model'}
</button>
</div>
<div className="settings-item">
<label className="settings-label">
htdemucs_4s Download URL
</label>
<input
type="text"
className="settings-input"
placeholder={`e.g. ${LOCAL_SEPARATOR_MODEL_CONFIGS[LOCAL_SEPARATOR_MODEL_IDS.htdemucs4s].download.defaultUrl}`}
value={htdemucsModelUrl}
onChange={(e) => handleHtdemucsModelUrlChange(e.target.value)}
/>
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
Changing this URL may break downloads or point to an incompatible model file.{' '}
<a
href="#"
onClick={(e) => {
e.preventDefault();
handleHtdemucsModelUrlChange(
LOCAL_SEPARATOR_MODEL_CONFIGS[LOCAL_SEPARATOR_MODEL_IDS.htdemucs4s].download.defaultUrl,
);
}}
style={{ color: '#5a9fd4', textDecoration: 'underline', cursor: 'pointer' }}
>
Restore default
</a>
</div>
</div>
<div className="settings-item" style={{ marginTop: '12px' }}>
<button
type="button"
className="settings-btn settings-btn-danger"
onClick={() => void handleDeleteHtdemucsModel()}
disabled={isCheckingUvr5ModelCache || isDeletingHtdemucsModel || !isHtdemucsModelCached}
>
{isDeletingHtdemucsModel ? 'Deleting...' : 'Delete Cached Model'}
</button>
</div>
</div>
<div className="settings-group">