fix: harden input validation, file safety, and error handling#227
fix: harden input validation, file safety, and error handling#227ankit1999 wants to merge 11 commits intojamiepine:mainfrom
Conversation
- New custom_models.py module for CRUD management of user-defined HF TTS models - New /custom-models API endpoints (list, add, get, delete) - Updated MLX and PyTorch backends to resolve custom model paths (custom:slug format) - Added Custom Models section to ModelManagement UI with add/remove dialogs - Updated GenerationForm and FloatingGenerateBox with grouped model selectors - Added CustomModelCreate/Response types and API client methods - Added instruct field to GenerationRequest type - Graceful actool fallback in build.rs for non-Xcode environments - Added custom_models hidden import for PyInstaller bundling Author: AJ - Kamyab (Ankit Jain)
- ModelManagement.tsx: validate model_name format before removal, guard duplicate clicks - client.ts: URL-encode modelId in removeCustomModel path - useGenerationForm.ts: constrain modelSize with regex pattern - useModelStatus.ts: extract shared hook from GenerationForm/FloatingGenerateBox - custom_models.py: atomic writes, threading lock, corrupt config backup, strict hf_repo_id regex - models.py: add regex pattern to model_size Field - config.py: platform-appropriate default data dir for PyInstaller bundles - main.py: re-raise HTTPException before broad except to preserve 202 responses - custom_models.json: remove pre-seeded entry for clean installs - requirements.txt: add platformdirs dependency - backend/README.md: document --data-dir CLI flag
📝 WalkthroughWalkthroughThis PR adds end-to-end support for registering and managing user-defined custom HuggingFace TTS models. It introduces a persistent Changes
Sequence Diagram(s)sequenceDiagram
participant User as User
participant UI as ModelManagement UI
participant API as API Client
participant Backend as Backend Server
participant Storage as custom_models.json
participant HF as HuggingFace
User->>UI: Click "Add Custom Model"
UI->>UI: Open dialog, enter repo ID & display name
User->>UI: Submit form
UI->>API: POST /custom-models {hf_repo_id, display_name}
API->>Backend: Custom model request
Backend->>Backend: Validate repo ID format
Backend->>Backend: Generate slug from repo ID
Backend->>Backend: Check for duplicates
Backend->>Storage: Append new custom model (atomic write)
Storage-->>Backend: Persisted
Backend-->>API: CustomModelResponse {id, hf_repo_id, display_name, added_at}
API-->>UI: Success
UI->>UI: Invalidate modelStatus query, close dialog
UI->>Backend: GET /models/status (refetch)
Backend->>Storage: list_custom_models()
Storage-->>Backend: Custom models list
Backend->>HF: Check HF cache for each custom model
HF-->>Backend: Cache metadata
Backend-->>UI: ModelStatus with custom entries
UI->>User: Display updated custom models section
sequenceDiagram
participant User as User
participant UI as Generation UI
participant Hook as useGenerationForm
participant API as API Client
participant Backend as Backend Server
participant MLX as MLX Backend
participant Cache as HF Cache
User->>UI: Select custom model from dropdown
UI->>Hook: modelSize = "custom:my-model"
Hook->>Hook: Validate against regex<br/>(1.7B|0.6B|1B|3B|custom:[a-z0-9]...)
Hook->>Hook: Extract slug "my-model"
Hook->>API: getModelStatus() [refetch]
API->>Backend: GET /models/status
Backend->>Backend: Fetch custom model by id
Backend-->>API: ModelStatus including custom entry
API-->>Hook: modelStatus updated
Hook->>Hook: Set displayName from model metadata
User->>UI: Generate audio
UI->>Hook: handleSubmit with modelSize="custom:my-model"
Hook->>Hook: Build GenerationRequest<br/>model_size="custom:my-model"
API->>Backend: POST /generation {model_size="custom:my-model", ...}
Backend->>MLX: _get_model_path("custom:my-model")
MLX->>MLX: Detect custom: prefix
MLX->>Backend: get_hf_repo_id_for_custom_model("my-model")
Backend-->>MLX: "owner/repo"
MLX->>Cache: Load model from HF cache<br/>models--owner--repo
Cache-->>MLX: Model files
MLX-->>Backend: model_path
Backend->>Backend: Generate audio with custom model
Backend-->>API: Audio response
API-->>UI: Display result
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/src/lib/hooks/useGenerationForm.ts (1)
120-132:⚠️ Potential issue | 🟠 MajorHandle download-pending responses before using
resultasGenerationResponse.This block assumes
result.durationexists. When backend returns 202 download-pending payload, this can crash the success path.✅ Safer flow: early-return on undownloaded model
try { const modelStatus = await apiClient.getModelStatus(); const model = modelStatus.models.find((m) => m.model_name === modelName); if (model) { displayName = model.display_name; if (!model.downloaded) { // Not yet downloaded — enable progress tracking UI setDownloadingModelName(modelName); setDownloadingDisplayName(displayName); + if (!model.downloading) { + await apiClient.triggerModelDownload(modelName); + } + toast({ + title: 'Model download in progress', + description: `${displayName} is downloading. Try generating again when ready.`, + }); + return; } } } catch (error) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/lib/hooks/useGenerationForm.ts` around lines 120 - 132, The success handler assumes result is a fully available GenerationResponse and accesses result.duration, which will crash if the backend returned a 202 download-pending payload; update the flow in useGenerationForm where generation.mutateAsync is awaited (the block that constructs toast with result.duration) to first detect the download-pending response (e.g., check a status field or absence of duration/download URL on the returned object) and early-return or show a "download pending" toast instead of accessing result.duration; ensure subsequent logic only runs when result.duration exists (guard on result.duration or result.status === 'ready') so you never dereference duration on a pending response.
🧹 Nitpick comments (5)
backend/config.py (1)
50-57: Consider ensuring the data directory exists inget_data_dir().Unlike
set_data_dir()and the subdirectory getters (e.g.,get_profiles_dir()),get_data_dir()does not callmkdir(). If called beforeset_data_dir()and the directory doesn't exist, callers may encounter errors when writing files.🔧 Proposed fix
def get_data_dir() -> Path: """ Get the data directory path. Returns: Path to the data directory """ + _data_dir.mkdir(parents=True, exist_ok=True) return _data_dir🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/config.py` around lines 50 - 57, get_data_dir() should ensure the underlying directory exists like set_data_dir() and get_profiles_dir() do: before returning the module-level _data_dir Path, call _data_dir.mkdir(parents=True, exist_ok=True) (or equivalent) to create the directory if missing so callers won’t fail when writing files; update the get_data_dir() function to perform this mkdir step and then return _data_dir.backend/backends/mlx_backend.py (1)
40-65: Consider extracting shared custom model resolution logic.The custom model resolution code (lines 40-49) is nearly identical to
pytorch_backend.py(lines 65-73). While this duplication is minimal and acceptable, you could extract it to a shared utility if this pattern expands to more backends.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/backends/mlx_backend.py` around lines 40 - 65, The duplicate custom-model resolution block (checking model_size.startswith("custom:"), extracting custom_id, calling get_hf_repo_id_for_custom_model and raising ValueError) should be extracted into a shared helper so both mlx_backend and pytorch_backend reuse it; create a utility function (e.g., resolve_custom_model_hf_repo or get_hf_repo_id_from_model_size) that accepts model_size, imports/calls get_hf_repo_id_for_custom_model, returns the hf_repo_id or raises the ValueError, and replace the inline code in mlx_backend (the model_size.startswith("custom:") branch) and the corresponding code in pytorch_backend with calls to this new helper; keep existing behavior/logging (print) and preserve mlx_model_map/other logic.backend/custom_models.py (1)
47-62: Uselogger.exceptionto include traceback in error logs.Within exception handlers,
logger.exceptionautomatically includes the traceback, which is more useful for debugging thanlogger.error.♻️ Proposed fix
except json.JSONDecodeError as exc: # Back up the corrupt file so we don't lose data backup = path.with_suffix( f".json.corrupt.{datetime.utcnow().strftime('%Y%m%dT%H%M%S')}" ) try: path.rename(backup) - logger.error( + logger.exception( "Corrupt custom_models.json backed up to %s: %s", backup, exc ) except OSError as rename_err: - logger.error( + logger.exception( "Failed to back up corrupt config %s: %s (original error: %s)", path, rename_err, exc, ) raise🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/custom_models.py` around lines 47 - 62, Replace the logger.error calls inside the JSON decode and nested OSError handlers with logger.exception so the traceback is recorded: in the except json.JSONDecodeError as exc block, change the logger.error("Corrupt custom_models.json backed up to %s: %s", backup, exc) to logger.exception(...) and in the inner except OSError as rename_err block change logger.error("Failed to back up corrupt config %s: %s (original error: %s)", path, rename_err, exc) to logger.exception(...) (preserving the descriptive messages and context) so the full tracebacks for the JSONDecodeError and the rename error are included.backend/main.py (1)
1546-1552: Don’t silently swallow cache-probe exceptions in model status paths.Current
except Exception: passbehavior hides actionable failures. Log at debug level so support can diagnose cache-state inconsistencies.🧭 Minimal observability patch
+import logging +logger = logging.getLogger(__name__) @@ - except Exception: - pass + except Exception: + logger.debug("Custom model cache probe failed (incomplete check): %s", hf_repo_id, exc_info=True) @@ - except Exception: - pass + except Exception: + logger.debug("Custom model size computation failed: %s", hf_repo_id, exc_info=True) @@ - except Exception: - pass - except Exception: - pass + except Exception: + logger.debug("Custom model fallback size computation failed: %s", hf_repo_id, exc_info=True) + except Exception: + logger.debug("Custom model fallback cache probe failed: %s", hf_repo_id, exc_info=True) @@ - except Exception: - pass + except Exception: + logger.debug("Custom model loaded-state check failed: %s", model_name, exc_info=True) @@ - except Exception: + except Exception: + logger.debug("Custom model status assembly failed: %s", model_name, exc_info=True) is_downloading = model_name in active_download_namesAlso applies to: 1557-1560, 1584-1593, 1598-1601, 1618-1628
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/main.py` around lines 1546 - 1552, Replace silent except blocks that currently do "except Exception: pass" in the model status cache probes (the blocks that compute cache_dir_path, blobs_dir and has_incomplete using hf_constants.HF_HUB_CACHE and hf_repo_id) with exception handlers that call the module logger at debug level including a descriptive message and the exception details (e.g., "failed probing HF cache for repo {hf_repo_id}, cache_dir={cache_dir_path}") so failures aren't swallowed; make the same change for the other similar blocks flagged (the ones around lines computing blobs_dir/has_incomplete in the same function) to ensure consistent debug-level observability while preserving existing control flow.app/src/components/Generation/GenerationForm.tsx (1)
146-174: Extract shared model-option rendering/mapping to avoid drift.The built-in/custom select rendering and
model_name→sizeValuemapping is duplicated inFloatingGenerateBox.tsx; fallback text has already started diverging. Consider a shared helper/component for this block.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/components/Generation/GenerationForm.tsx` around lines 146 - 174, The built-in/custom model option rendering and the model_name→sizeValue mapping in GenerationForm (see builtInModels, customModels, SelectGroup/SelectItem usage and the sizeValue = model.model_name.replace('qwen-tts-', '')) are duplicated in FloatingGenerateBox; extract a shared helper or small presentational component (e.g., renderModelOptions or ModelSelectGroup component and a mapModelNameToSize utility) that returns the same SelectGroup/SelectItem structure and performs the model_name→sizeValue transformation, then replace the duplicated loops in both GenerationForm.tsx and FloatingGenerateBox.tsx to call the new shared function/component so fallback text and mapping remain consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/src/components/Generation/GenerationForm.tsx`:
- Line 37: The hook useModelStatus currently classifies builtInModels using
model_name.startsWith('qwen-tts'), which incorrectly flags repos like
"qwen-tts/my-voice" as built-in; update the predicate in useModelStatus (the
filter that builds builtInModels/customModels) to only treat true built-ins such
as the core qwen-tts identifier (e.g., model_name === 'qwen-tts' or match a
pattern that disallows a following '/'), for example replace
startsWith('qwen-tts') with a stricter check (exact equality or a regex like
/^qwen-tts($|[:@])/) so repo-qualified names with a slash go to customModels.
In `@app/src/components/ServerSettings/ModelManagement.tsx`:
- Around line 617-623: The Remove button currently disables only when
model.loaded or isUnregistering is true; update the disable condition for the
Button (the component with onClick={onRemove}) to also check the showDownloading
flag so removal is prevented while a download is active. Locate the Button that
uses onRemove and the disabled prop, and add showDownloading (or its local/state
variable) into the combined disabled expression alongside model.loaded and
isUnregistering.
In `@backend/custom_models.py`:
- Line 10: Remove the unused fcntl import from the top of custom_models.py
(delete the `import fcntl` statement), verify there are no remaining references
to fcntl in the file (the module already uses threading.Lock for
synchronization), and run tests/linting to confirm no regressions.
In `@backend/voicebox-server.spec`:
- Around line 16-19: The spec unconditionally calls collect_all('mlx') and
collect_all('mlx_audio') which fails when MLX isn't installed; wrap those
collect_all calls in a runtime presence check (e.g., try/except ImportError or
importlib.util.find_spec) before invoking collect_all so that if 'mlx' or
'mlx_audio' is missing you skip updating tmp_ret/datas/binaries/hiddenimports.
Specifically, guard the collect_all('mlx') and collect_all('mlx_audio') blocks
(the places that assign tmp_ret and then do datas += tmp_ret[0]; binaries +=
tmp_ret[1]; hiddenimports += tmp_ret[2]) so they only run when the package is
importable.
---
Outside diff comments:
In `@app/src/lib/hooks/useGenerationForm.ts`:
- Around line 120-132: The success handler assumes result is a fully available
GenerationResponse and accesses result.duration, which will crash if the backend
returned a 202 download-pending payload; update the flow in useGenerationForm
where generation.mutateAsync is awaited (the block that constructs toast with
result.duration) to first detect the download-pending response (e.g., check a
status field or absence of duration/download URL on the returned object) and
early-return or show a "download pending" toast instead of accessing
result.duration; ensure subsequent logic only runs when result.duration exists
(guard on result.duration or result.status === 'ready') so you never dereference
duration on a pending response.
---
Nitpick comments:
In `@app/src/components/Generation/GenerationForm.tsx`:
- Around line 146-174: The built-in/custom model option rendering and the
model_name→sizeValue mapping in GenerationForm (see builtInModels, customModels,
SelectGroup/SelectItem usage and the sizeValue =
model.model_name.replace('qwen-tts-', '')) are duplicated in
FloatingGenerateBox; extract a shared helper or small presentational component
(e.g., renderModelOptions or ModelSelectGroup component and a mapModelNameToSize
utility) that returns the same SelectGroup/SelectItem structure and performs the
model_name→sizeValue transformation, then replace the duplicated loops in both
GenerationForm.tsx and FloatingGenerateBox.tsx to call the new shared
function/component so fallback text and mapping remain consistent.
In `@backend/backends/mlx_backend.py`:
- Around line 40-65: The duplicate custom-model resolution block (checking
model_size.startswith("custom:"), extracting custom_id, calling
get_hf_repo_id_for_custom_model and raising ValueError) should be extracted into
a shared helper so both mlx_backend and pytorch_backend reuse it; create a
utility function (e.g., resolve_custom_model_hf_repo or
get_hf_repo_id_from_model_size) that accepts model_size, imports/calls
get_hf_repo_id_for_custom_model, returns the hf_repo_id or raises the
ValueError, and replace the inline code in mlx_backend (the
model_size.startswith("custom:") branch) and the corresponding code in
pytorch_backend with calls to this new helper; keep existing behavior/logging
(print) and preserve mlx_model_map/other logic.
In `@backend/config.py`:
- Around line 50-57: get_data_dir() should ensure the underlying directory
exists like set_data_dir() and get_profiles_dir() do: before returning the
module-level _data_dir Path, call _data_dir.mkdir(parents=True, exist_ok=True)
(or equivalent) to create the directory if missing so callers won’t fail when
writing files; update the get_data_dir() function to perform this mkdir step and
then return _data_dir.
In `@backend/custom_models.py`:
- Around line 47-62: Replace the logger.error calls inside the JSON decode and
nested OSError handlers with logger.exception so the traceback is recorded: in
the except json.JSONDecodeError as exc block, change the logger.error("Corrupt
custom_models.json backed up to %s: %s", backup, exc) to logger.exception(...)
and in the inner except OSError as rename_err block change logger.error("Failed
to back up corrupt config %s: %s (original error: %s)", path, rename_err, exc)
to logger.exception(...) (preserving the descriptive messages and context) so
the full tracebacks for the JSONDecodeError and the rename error are included.
In `@backend/main.py`:
- Around line 1546-1552: Replace silent except blocks that currently do "except
Exception: pass" in the model status cache probes (the blocks that compute
cache_dir_path, blobs_dir and has_incomplete using hf_constants.HF_HUB_CACHE and
hf_repo_id) with exception handlers that call the module logger at debug level
including a descriptive message and the exception details (e.g., "failed probing
HF cache for repo {hf_repo_id}, cache_dir={cache_dir_path}") so failures aren't
swallowed; make the same change for the other similar blocks flagged (the ones
around lines computing blobs_dir/has_incomplete in the same function) to ensure
consistent debug-level observability while preserving existing control flow.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
bun.lockis excluded by!**/*.locktauri/src-tauri/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (19)
app/src/components/Generation/FloatingGenerateBox.tsxapp/src/components/Generation/GenerationForm.tsxapp/src/components/ServerSettings/ModelManagement.tsxapp/src/lib/api/client.tsapp/src/lib/api/types.tsapp/src/lib/hooks/useGenerationForm.tsapp/src/lib/hooks/useModelStatus.tsbackend/README.mdbackend/backends/mlx_backend.pybackend/backends/pytorch_backend.pybackend/build_binary.pybackend/config.pybackend/custom_models.pybackend/main.pybackend/models.pybackend/requirements.txtbackend/voicebox-server.specdata/custom_models.jsontauri/src-tauri/build.rs
| const { form, handleSubmit, isPending } = useGenerationForm(); | ||
|
|
||
| // Use shared hook for model status fetching and grouping | ||
| const { builtInModels, customModels } = useModelStatus(); |
There was a problem hiding this comment.
Prevent built-in/custom option overlap from hook classification.
builtInModels from useModelStatus is based on model_name.startsWith('qwen-tts'). A custom repo like qwen-tts/my-voice can appear in both groups and be misinterpreted in built-in mapping.
🔧 Proposed fix (in app/src/lib/hooks/useModelStatus.ts)
-const builtInModels =
- modelStatus?.models.filter((m) => m.model_name.startsWith('qwen-tts')) || [];
+const builtInModels =
+ modelStatus?.models.filter((m) => !m.is_custom && m.model_name.startsWith('qwen-tts-')) || [];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/src/components/Generation/GenerationForm.tsx` at line 37, The hook
useModelStatus currently classifies builtInModels using
model_name.startsWith('qwen-tts'), which incorrectly flags repos like
"qwen-tts/my-voice" as built-in; update the predicate in useModelStatus (the
filter that builds builtInModels/customModels) to only treat true built-ins such
as the core qwen-tts identifier (e.g., model_name === 'qwen-tts' or match a
pattern that disallows a following '/'), for example replace
startsWith('qwen-tts') with a stricter check (exact equality or a regex like
/^qwen-tts($|[:@])/) so repo-qualified names with a slash go to customModels.
| <Button | ||
| size="sm" | ||
| onClick={onRemove} | ||
| variant="ghost" | ||
| title="Remove custom model from list" | ||
| disabled={model.loaded || isUnregistering} | ||
| > |
There was a problem hiding this comment.
Disable “remove custom model” while download is active.
Unregistering during showDownloading can create inconsistent UX/state around in-flight downloads.
🛠️ Small UI guard improvement
<Button
size="sm"
onClick={onRemove}
variant="ghost"
title="Remove custom model from list"
- disabled={model.loaded || isUnregistering}
+ disabled={model.loaded || isUnregistering || showDownloading}
>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/src/components/ServerSettings/ModelManagement.tsx` around lines 617 -
623, The Remove button currently disables only when model.loaded or
isUnregistering is true; update the disable condition for the Button (the
component with onClick={onRemove}) to also check the showDownloading flag so
removal is prevented while a download is active. Locate the Button that uses
onRemove and the disabled prop, and add showDownloading (or its local/state
variable) into the combined disabled expression alongside model.loaded and
isUnregistering.
| tmp_ret = collect_all('mlx') | ||
| datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] | ||
| tmp_ret = collect_all('mlx_audio') | ||
| datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
git ls-files | head -20Repository: jamiepine/voicebox
Length of output: 446
🏁 Script executed:
find . -name "*.spec" -o -name "*requirements*.txt" -o -name "pyproject.toml" -o -name "setup.cfg" | grep -v ".git" | head -20Repository: jamiepine/voicebox
Length of output: 169
🏁 Script executed:
ls -la backend/ 2>/dev/null || echo "backend dir not found"Repository: jamiepine/voicebox
Length of output: 1843
🏁 Script executed:
# Check if the spec file exists and examine it
if [ -f "backend/voicebox-server.spec" ]; then
echo "=== backend/voicebox-server.spec ==="
head -30 backend/voicebox-server.spec
fiRepository: jamiepine/voicebox
Length of output: 1802
🏁 Script executed:
echo "=== backend/requirements.txt ===" && cat backend/requirements.txt && echo && \
echo "=== backend/requirements-mlx.txt ===" && cat backend/requirements-mlx.txtRepository: jamiepine/voicebox
Length of output: 672
🏁 Script executed:
echo "=== Build workflows ===" && \
for f in .github/workflows/*.yml; do
echo "--- $f ---"
cat "$f" | head -50
doneRepository: jamiepine/voicebox
Length of output: 2778
🏁 Script executed:
echo "=== Check Makefile for build commands ===" && \
cat Makefile 2>/dev/null | head -80Repository: jamiepine/voicebox
Length of output: 3156
🏁 Script executed:
# Verify PyInstaller collect_all behavior and check if error handling exists in build_binary.py
echo "=== backend/build_binary.py ===" && \
head -50 backend/build_binary.py && \
echo "..." && \
tail -30 backend/build_binary.pyRepository: jamiepine/voicebox
Length of output: 2784
🏁 Script executed:
# Check if there's any error handling around collect_all in the repo
rg -n "collect_all|try.*collect" backend/Repository: jamiepine/voicebox
Length of output: 268
Guard MLX collect_all calls for environments where MLX is absent.
The spec file unconditionally calls collect_all('mlx') and collect_all('mlx_audio') (lines 16–19), but MLX is declared as Apple Silicon-only in requirements-mlx.txt and conditionally installed only on aarch64-apple-darwin. Windows and Intel Mac CI builds skip MLX dependencies, causing PyInstaller to fail when the spec file is executed on these platforms. Add package-presence guards before collecting.
Suggested fix
+from importlib.util import find_spec
+
tmp_ret = collect_all('mlx')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('mlx_audio')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
+if find_spec("mlx") is not None:
+ _d, _b, _h = collect_all("mlx")
+ datas += _d
+ binaries += _b
+ hiddenimports += _h
+
+if find_spec("mlx_audio") is not None:
+ _d, _b, _h = collect_all("mlx_audio")
+ datas += _d
+ binaries += _b
+ hiddenimports += _h📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| tmp_ret = collect_all('mlx') | |
| datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] | |
| tmp_ret = collect_all('mlx_audio') | |
| datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] | |
| from importlib.util import find_spec | |
| if find_spec("mlx") is not None: | |
| _d, _b, _h = collect_all("mlx") | |
| datas += _d | |
| binaries += _b | |
| hiddenimports += _h | |
| if find_spec("mlx_audio") is not None: | |
| _d, _b, _h = collect_all("mlx_audio") | |
| datas += _d | |
| binaries += _b | |
| hiddenimports += _h |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/voicebox-server.spec` around lines 16 - 19, The spec unconditionally
calls collect_all('mlx') and collect_all('mlx_audio') which fails when MLX isn't
installed; wrap those collect_all calls in a runtime presence check (e.g.,
try/except ImportError or importlib.util.find_spec) before invoking collect_all
so that if 'mlx' or 'mlx_audio' is missing you skip updating
tmp_ret/datas/binaries/hiddenimports. Specifically, guard the collect_all('mlx')
and collect_all('mlx_audio') blocks (the places that assign tmp_ret and then do
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]) so
they only run when the package is importable.
Resolved 16 merge conflicts to integrate custom model management into the new refactored architecture: Frontend: - Removed inline modelSize selectors in FloatingGenerateBox and GenerationForm in favor of the unified EngineModelSelector - Merged model_size typing (string for custom: slugs) with origin/main's GenerationRequest properties (engine, effects_chain, etc.) - Preserved custom model CRUD in ModelManagement with new modal UI - Updated useGenerationForm to handle custom model identifiers within the new multi-engine architecture Backend: - Accepted origin/main's modular app.py + routes architecture - Injected custom model status into routes/models.py get_model_status - Added custom model CRUD endpoints (/custom-models) to the models router - Merged platformdirs fallback into config.py path resolution - Extended model_size regex to accept 1B, 3B sizes alongside custom: slugs - Combined hidden imports in build_binary.py and voicebox-server.spec
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
app/src/lib/api/client.ts (1)
3-22:⚠️ Potential issue | 🟠 MajorDuplicate
ActiveTasksResponseimport.
ActiveTasksResponseis imported on both line 4 and line 19 (Biome flags this as a parse error). Drop the second occurrence; the other added types (CustomModelCreate,CustomModelResponse,CustomModelListResponse) should stay, ideally merged alphabetically into the existing sorted import list.🛠️ Proposed fix
import type { - ActiveTasksResponse, ApplyEffectsRequest, AvailableEffectsResponse, + ActiveTasksResponse, + CustomModelCreate, + CustomModelListResponse, + CustomModelResponse, CudaStatus, EffectConfig, EffectPresetCreate, EffectPresetResponse, GenerationRequest, GenerationResponse, GenerationVersionResponse, HealthResponse, HistoryListResponse, HistoryQuery, HistoryResponse, ModelDownloadRequest, - ActiveTasksResponse, - CustomModelCreate, - CustomModelResponse, - CustomModelListResponse, ModelStatusListResponse,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/lib/api/client.ts` around lines 3 - 22, The import list contains a duplicate ActiveTasksResponse symbol; remove the second occurrence and merge the new types (CustomModelCreate, CustomModelResponse, CustomModelListResponse) into the existing sorted import block so the list remains alphabetized and deduplicated (update the top-level import that currently declares ActiveTasksResponse, ApplyEffectsRequest, ..., GenerationResponse, etc., to include the three CustomModel* types in the correct alphabetical position and omit the duplicate ActiveTasksResponse).app/src/components/ServerSettings/ModelManagement.tsx (1)
1-1186:⚠️ Potential issue | 🔴 CriticalThis file is structurally broken and will not compile.
Biome is already flagging five parse errors, but there are far more blocking issues — this file as posted cannot type-check or run. Key problems:
Imports are corrupted (lines 3-64).
- Line 3 concatenates two imports on one line (
...} from 'react';import {) and the secondimport {never gets a matching source before intervening imports appear.- Lines 19-25 (
DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog';plusInput/Label/Progress) have no openingimport {— the dialog named imports (Dialog,DialogContent,DialogDescription) are missing entirely, yet they're used at lines 714-715, 1147-1148, etc.- Line 64 (
import { usePlatform } from '@/platform/PlatformContext';) and line 65 (useServerStore) are placed inside theModelManagementfunction body opened at line 39. Imports cannot appear inside a function.
ModelManagementnever closes cleanly. Top-level declarations likefetchHuggingFaceModelInfo,MODEL_DESCRIPTIONS,formatDownloads,formatLicense,formatPipelineTag,formatBytes(lines 67-123) are nested insideModelManagement()because the function opened at line 39 has no matching close before them. Corresponding closing}then never balances.Multiple statements on one line where newlines were lost.
- Line 318: two
await queryClient.*calls on a single line.- Line 343:
title: 'Unload failed', description: error.message,— missing newline between object properties.- Line 410:
.replace('license:', ''); return (— return statement merged with the previous assignment.- Line 1030:
{pendingMigrateDir} </div>— closing tag merged with text.Undefined references.
modelStatus,isLoading(used at lines 231, 377-378, 387, 390, 496, 500, 502) —useModelStatus()is never imported or called, and nouseQuery(['modelStatus'])exists in this file either.addCustomModelMutation(lines 357, 1177, 1178) andremoveCustomModelMutation(lines 683, 694, 697, 938, 940, 948) are used but never declared.CustomModelItem(line 669) is used but neither imported nor defined.DialogContent,DialogDescription,Dialog(lines 714-715, 720, 1147-1148, 1151) are not imported.JSX mismatched. Lines 710-711 close with
</div> ) : null} </CardContent>but no<Card>/<CardContent>ever opens in this file. The<Dialog open={addDialogOpen}>block at lines 1147-1183 is emitted afterModelManagementcloses at line 1145, making it orphaned top-level JSX followed by another stray</div>,), and}at 1184-1186.Duplicated "Custom Models" UI. The sections loop at lines 502-586 already renders a "Custom Models" subsection (with
section.isCustomAdd button at 508-512), and then lines 651-707 render another full "Custom Models" section, seemingly copy-pasted from an earlier version. One of the two needs to go.This looks like the result of a messy merge/patch —
tsc --noEmitwould not pass on this file despite what the PR description claims. Please re-apply the intended hunks cleanly (reconstruct imports, ensure the function body/JSX balances, wire upuseModelStatus, declare the missing mutations, and either define or importCustomModelItem) and re-runtsc --noEmitbefore merging.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/components/ServerSettings/ModelManagement.tsx` around lines 1 - 1186, The file is syntactically broken due to mangled imports, misplaced top-level declarations inside the ModelManagement function, missing hook/mutation declarations, duplicate UI blocks, and several merged lines — fix by: 1) move and reconstruct all import statements to the top (include Dialog, DialogContent, DialogDescription, DialogFooter/DialogHeader/DialogTitle, CustomModelItem, useModelStatus/usePlatform/useServerStore), removing the stray concatenated import fragments; 2) ensure ModelManagement() opens and closes correctly so helper functions (fetchHuggingFaceModelInfo, MODEL_DESCRIPTIONS, formatDownloads/formatLicense/formatPipelineTag/formatBytes) are declared outside the component or moved above the component and not nested inside it; 3) declare or create the missing React Query mutations/variables (addCustomModelMutation, removeCustomModelMutation, modelStatus/isLoading via useModelStatus or a useQuery for ['modelStatus']), and wire up CustomModelItem prop types; 4) remove the duplicated "Custom Models" UI block (keep only the sections-based rendering) and fix merged statements/newlines (split combined await/queryClient calls and object properties like onError descriptions) so JSX tags (CardContent/Dialog/DialogContent/etc.) are balanced; 5) run tsc --noEmit and eslint/formatter to catch any remaining parse/type errors before committing.
🧹 Nitpick comments (4)
backend/backends/pytorch_backend.py (1)
54-63: Custom model resolution looks correct.Logic correctly handles the
custom:prefix before falling through to the built-in map, and raisesValueErrorconsistent with the unknown-size error path below, so_load_model_sync/_is_model_cachedcallers see a uniform failure mode. The lazy import of..custom_modelsavoids potential circular-import risk at module load.One minor nit: the
#@ModifiedAJ - Kamyab (Ankit Jain)attribution comment on line 55 is better tracked via VCS (git blame) than inline; consider dropping it to keep the source clean.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/backends/pytorch_backend.py` around lines 54 - 63, Remove the inline attribution comment "# `@modified` AJ - Kamyab (Ankit Jain) — Added custom model path resolution" to keep the source clean and rely on VCS history; leave the surrounding logic intact (the model_size.startswith("custom:") branch that computes custom_id, calls get_hf_repo_id_for_custom_model(custom_id), and raises ValueError when not found) so callers like _load_model_sync and _is_model_cached continue to see the same failure behavior.app/src/lib/api/client.ts (1)
459-463: Minor: indentation insideremoveCustomModelbody.Line 460 is indented at 6 spaces (new code) but lines 461-463 retain the surrounding 4-space indentation, making the closing brace misalign with the return. Not a correctness issue, but worth normalizing before merge.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/lib/api/client.ts` around lines 459 - 463, The indentation inside the removeCustomModel method is inconsistent: adjust the indentation so the return statement and the closing brace align with the surrounding 4-space indentation used in this file; locate the async removeCustomModel(modelId: string) method and change the line(s) currently indented with 6 spaces to use 4 spaces so the return and the closing brace line up with other methods.app/src/lib/hooks/useModelStatus.ts (1)
22-29: Consider a defensive fallback forcustomModels.Filtering
customModelsstrictly onm.is_customcouples this hook tightly to the backend always populating the flag. As noted on the backend change, the current/models/statusimplementation forgets to setis_custom=Trueon injected custom entries, so this hook will currently return an emptycustomModelslist. Once that's fixed server-side this works, but a combined predicate (e.g.m.is_custom || m.model_name.startsWith('custom:')) would make the hook resilient to future drift and match the convention used elsewhere (ModelManagement.tsx).🛠️ Proposed fix
const customModels = useMemo( - () => modelStatus?.models.filter((m) => m.is_custom) ?? [], + () => modelStatus?.models.filter((m) => m.is_custom || m.model_name.startsWith('custom:')) ?? [], [modelStatus], );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/lib/hooks/useModelStatus.ts` around lines 22 - 29, The customModels filter in useModelStatus currently only checks m.is_custom which can be missing; update the useMemo for customModels in useModelStatus to defensively include models whose model_name follows the custom convention (e.g., m.model_name.startsWith('custom:')) in addition to m.is_custom (i.e., filter by m.is_custom || m.model_name?.startsWith('custom:')), preserving the null-safety fallback (?? []) so the hook remains resilient to backend omissions and matches the convention used in ModelManagement.tsx.backend/routes/models.py (1)
389-428: Duplicate custom-model scanning logic.This block re-implements cache directory presence / weights detection / size computation that already exists in the built-in loop at lines 312-345. Since custom entries differ only in where their config comes from, consider extracting the cache-probe into a helper (e.g.
_probe_repo_cache(repo_id) -> (downloaded, size_mb)) and reusing it for both paths. Not a blocker, but given the PR explicitly defersprobe_model_cache_statusrefactoring, this is the natural spot to consolidate.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/routes/models.py` around lines 389 - 428, The custom-model block duplicates the repo cache probing/size logic already used in the built-in model loop (the code around the existing probe_model_cache_status logic at lines ~312-345); extract that logic into a small helper (e.g. _probe_repo_cache(repo_id) -> (downloaded: bool, size_mb: Optional[float])) and call it from both the custom entries loop and the built-in loop so the code in the custom_models.list_custom_models() handling only maps config fields and uses _probe_repo_cache(hf_repo) to set downloaded and size_mb; preserve use of models.ModelStatus, hf_repo in active_download_repos for downloading flag, and keep exception handling around the per-entry probe.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/src/components/Generation/FloatingGenerateBox.tsx`:
- Around line 11-13: The import list in FloatingGenerateBox.tsx includes unused
symbols SelectGroup and SelectLabel; remove these two from the named imports
(the import that currently lists SelectGroup, SelectItem, SelectLabel) so only
used symbols like SelectItem remain, then run a quick type-check/ESLint to
ensure no other unused imports remain.
In `@app/src/components/Generation/GenerationForm.tsx`:
- Around line 18-20: The imports SelectGroup and SelectLabel in the
GenerationForm component are unused; remove them from the import list that
currently includes SelectGroup, SelectItem, SelectLabel so only the needed
symbols (e.g., SelectItem) remain, updating the import statement at the top of
GenerationForm.tsx (the component/function named GenerationForm) to eliminate
SelectGroup and SelectLabel.
In `@backend/routes/models.py`:
- Around line 495-509: Add a short docstring or inline comment at the start of
the custom model cache-delete branch (the code that checks
model_name.startswith("custom:"), extracts custom_id and deletes the HF repo
cache_dir) stating that this endpoint only removes the HF cache directory and
does not unregister or unload the model (it intentionally does not call
remove_custom_model(custom_id) or perform any unload), and that callers must
also call the DELETE /custom-models/{model_id} endpoint to remove the registry
entry and fully delete the model; reference the variables model_name, custom_id
and the helper remove_custom_model in the comment to make the divergence
explicit for future maintainers.
- Around line 415-425: The appended ModelStatus entries for injected custom
models are missing the is_custom flag; update the models.ModelStatus
construction inside the statuses.append call that builds entries for
cm_name/display/hf_repo so it includes is_custom=True for these injected custom
entries (leave other ModelStatus constructions unchanged); this ensures the
useModelStatus hook (and consumers like GenerationForm) can detect custom models
via the is_custom field.
---
Outside diff comments:
In `@app/src/components/ServerSettings/ModelManagement.tsx`:
- Around line 1-1186: The file is syntactically broken due to mangled imports,
misplaced top-level declarations inside the ModelManagement function, missing
hook/mutation declarations, duplicate UI blocks, and several merged lines — fix
by: 1) move and reconstruct all import statements to the top (include Dialog,
DialogContent, DialogDescription, DialogFooter/DialogHeader/DialogTitle,
CustomModelItem, useModelStatus/usePlatform/useServerStore), removing the stray
concatenated import fragments; 2) ensure ModelManagement() opens and closes
correctly so helper functions (fetchHuggingFaceModelInfo, MODEL_DESCRIPTIONS,
formatDownloads/formatLicense/formatPipelineTag/formatBytes) are declared
outside the component or moved above the component and not nested inside it; 3)
declare or create the missing React Query mutations/variables
(addCustomModelMutation, removeCustomModelMutation, modelStatus/isLoading via
useModelStatus or a useQuery for ['modelStatus']), and wire up CustomModelItem
prop types; 4) remove the duplicated "Custom Models" UI block (keep only the
sections-based rendering) and fix merged statements/newlines (split combined
await/queryClient calls and object properties like onError descriptions) so JSX
tags (CardContent/Dialog/DialogContent/etc.) are balanced; 5) run tsc --noEmit
and eslint/formatter to catch any remaining parse/type errors before committing.
In `@app/src/lib/api/client.ts`:
- Around line 3-22: The import list contains a duplicate ActiveTasksResponse
symbol; remove the second occurrence and merge the new types (CustomModelCreate,
CustomModelResponse, CustomModelListResponse) into the existing sorted import
block so the list remains alphabetized and deduplicated (update the top-level
import that currently declares ActiveTasksResponse, ApplyEffectsRequest, ...,
GenerationResponse, etc., to include the three CustomModel* types in the correct
alphabetical position and omit the duplicate ActiveTasksResponse).
---
Nitpick comments:
In `@app/src/lib/api/client.ts`:
- Around line 459-463: The indentation inside the removeCustomModel method is
inconsistent: adjust the indentation so the return statement and the closing
brace align with the surrounding 4-space indentation used in this file; locate
the async removeCustomModel(modelId: string) method and change the line(s)
currently indented with 6 spaces to use 4 spaces so the return and the closing
brace line up with other methods.
In `@app/src/lib/hooks/useModelStatus.ts`:
- Around line 22-29: The customModels filter in useModelStatus currently only
checks m.is_custom which can be missing; update the useMemo for customModels in
useModelStatus to defensively include models whose model_name follows the custom
convention (e.g., m.model_name.startsWith('custom:')) in addition to m.is_custom
(i.e., filter by m.is_custom || m.model_name?.startsWith('custom:')), preserving
the null-safety fallback (?? []) so the hook remains resilient to backend
omissions and matches the convention used in ModelManagement.tsx.
In `@backend/backends/pytorch_backend.py`:
- Around line 54-63: Remove the inline attribution comment "# `@modified` AJ -
Kamyab (Ankit Jain) — Added custom model path resolution" to keep the source
clean and rely on VCS history; leave the surrounding logic intact (the
model_size.startswith("custom:") branch that computes custom_id, calls
get_hf_repo_id_for_custom_model(custom_id), and raises ValueError when not
found) so callers like _load_model_sync and _is_model_cached continue to see the
same failure behavior.
In `@backend/routes/models.py`:
- Around line 389-428: The custom-model block duplicates the repo cache
probing/size logic already used in the built-in model loop (the code around the
existing probe_model_cache_status logic at lines ~312-345); extract that logic
into a small helper (e.g. _probe_repo_cache(repo_id) -> (downloaded: bool,
size_mb: Optional[float])) and call it from both the custom entries loop and the
built-in loop so the code in the custom_models.list_custom_models() handling
only maps config fields and uses _probe_repo_cache(hf_repo) to set downloaded
and size_mb; preserve use of models.ModelStatus, hf_repo in
active_download_repos for downloading flag, and keep exception handling around
the per-entry probe.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 7947d167-e950-4785-8b1b-10f1289f738a
📒 Files selected for processing (17)
.github/workflows/release.ymlapp/src/components/Generation/FloatingGenerateBox.tsxapp/src/components/Generation/GenerationForm.tsxapp/src/components/ServerSettings/ModelManagement.tsxapp/src/lib/api/client.tsapp/src/lib/api/types.tsapp/src/lib/hooks/useGenerationForm.tsapp/src/lib/hooks/useModelStatus.tsbackend/backends/mlx_backend.pybackend/backends/pytorch_backend.pybackend/build_binary.pybackend/config.pybackend/main.pybackend/models.pybackend/requirements.txtbackend/routes/models.pybackend/voicebox-server.spec
💤 Files with no reviewable changes (1)
- .github/workflows/release.yml
✅ Files skipped from review due to trivial changes (3)
- backend/requirements.txt
- backend/voicebox-server.spec
- backend/main.py
🚧 Files skipped from review as they are similar to previous changes (4)
- backend/build_binary.py
- backend/backends/mlx_backend.py
- app/src/lib/hooks/useGenerationForm.ts
- backend/models.py
| # Handle custom models | ||
| if model_name.startswith("custom:"): | ||
| custom_id = model_name[len("custom:"):] | ||
| cm = custom_models.get_custom_model(custom_id) | ||
| if not cm: | ||
| raise HTTPException(status_code=404, detail=f"Custom model '{custom_id}' not found") | ||
| hf_repo_id = cm.get("hf_repo_id", "") | ||
| try: | ||
| cache_dir = hf_constants.HF_HUB_CACHE | ||
| repo_cache_dir = Path(cache_dir) / ("models--" + hf_repo_id.replace("/", "--")) | ||
| if repo_cache_dir.exists(): | ||
| shutil.rmtree(repo_cache_dir) | ||
| return {"message": f"Custom model {model_name} cache deleted successfully"} | ||
| except Exception as e: | ||
| raise HTTPException(status_code=500, detail=f"Failed to delete custom model cache: {str(e)}") from e |
There was a problem hiding this comment.
Custom-model cache delete leaves registry intact and skips unload — intentional, but document the divergence.
This branch deletes the HF cache directory but does not call remove_custom_model(custom_id) or attempt to unload the model. The separate DELETE /custom-models/{model_id} endpoint handles unregistration. That's a reasonable split, but the frontend has to call both endpoints to fully remove a custom model; otherwise the entry reappears as "not downloaded" after cache clear. Consider a short docstring note so callers don't assume this endpoint removes the registration too.
🧰 Tools
🪛 Ruff (0.15.10)
[warning] 509-509: Use explicit conversion flag
Replace with conversion flag
(RUF010)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/routes/models.py` around lines 495 - 509, Add a short docstring or
inline comment at the start of the custom model cache-delete branch (the code
that checks model_name.startswith("custom:"), extracts custom_id and deletes the
HF repo cache_dir) stating that this endpoint only removes the HF cache
directory and does not unregister or unload the model (it intentionally does not
call remove_custom_model(custom_id) or perform any unload), and that callers
must also call the DELETE /custom-models/{model_id} endpoint to remove the
registry entry and fully delete the model; reference the variables model_name,
custom_id and the helper remove_custom_model in the comment to make the
divergence explicit for future maintainers.
- Add exception chaining (raise ... from e) on all unchained raises in delete_model - Rename loop variable 'config' to 'model_cfg' in get_model_status to avoid shadowing
- Rebuild ModelManagement.tsx from upstream base with clean custom model additions (fixes broken imports, missing query/mutations, duplicate JSX, code outside function) - Add Plus icon, DialogFooter, Input, Label imports for custom model dialog - Add addCustomModelMutation and removeCustomModelMutation definitions - Add Custom Models section to sections array with Add button - Add Unregister button in detail modal for custom models - Add Add Custom Model dialog (repo ID + display name) - Remove unused SelectGroup/SelectLabel imports in FloatingGenerateBox and GenerationForm - Remove duplicate ActiveTasksResponse import in client.ts - Remove unused fcntl import in backend/custom_models.py (cross-platform safety) - Set is_custom=True when injecting custom model status entries in routes/models.py
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/src/components/ServerSettings/ModelManagement.tsx (1)
898-970:⚠️ Potential issue | 🟠 MajorAllow unregistering custom models before download.
The
Unregisteraction is nested underfreshSelectedModel.downloaded, so a newly added custom model that has not been downloaded can only be downloaded, not removed. Render the custom unregister action in the non-downloaded branch too, or lift it outside the downloaded/not-downloaded split.🛠️ One way to keep unregister available
- ) : ( - <Button - size="sm" - onClick={() => handleDownload(freshSelectedModel.model_name)} - className="flex-1" - > - <Download className="h-4 w-4 mr-2" /> - Download - </Button> + ) : ( + <div className="flex gap-2 flex-1"> + <Button + size="sm" + onClick={() => handleDownload(freshSelectedModel.model_name)} + className="flex-1" + > + <Download className="h-4 w-4 mr-2" /> + Download + </Button> + {freshSelectedModel.model_name.startsWith('custom:') && ( + <Button + size="sm" + onClick={() => { + if (removeCustomModelMutation.isPending) return; + const customId = freshSelectedModel.model_name.slice('custom:'.length); + removeCustomModelMutation.mutate(customId, { + onSuccess: () => { + setDetailOpen(false); + setSelectedModel(null); + }, + }); + }} + variant="destructive" + disabled={removeCustomModelMutation.isPending} + className="flex-1" + title="Unregister model from custom list" + > + <X className="h-4 w-4 mr-2" /> + Unregister + </Button> + )} + </div> )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/components/ServerSettings/ModelManagement.tsx` around lines 898 - 970, The Unregister button for custom models is only rendered inside the downloaded branch so newly added custom models that aren't downloaded can't be unregistered; move or duplicate the custom unregister UI so it appears regardless of freshSelectedModel.downloaded (i.e., lift the conditional freshSelectedModel.model_name.startsWith('custom:') block out of the downloaded/not-downloaded ternary or render an equivalent button in the non-downloaded branch), keeping the same click handler that checks removeCustomModelMutation.isPending, slices the custom id, calls removeCustomModelMutation.mutate(customId, { onSuccess: () => { setDetailOpen(false); setSelectedModel(null); } }), and preserves disabling logic (use removeCustomModelMutation.isPending and/or freshSelectedModel.loaded as appropriate).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/custom_models.py`:
- Around line 40-63: The loader currently treats any IOError as a missing config
and returns {"models": []}, which can mask permission/transient errors and cause
overwrites; change the final except to only treat a missing file as empty:
import errno and either catch FileNotFoundError or use "except OSError as e:"
then if e.errno == errno.ENOENT return {"models": []} else re-raise the
exception so permission and other I/O failures surface; ensure this loader (the
function around the shown snippet used by add_custom_model) uses that behavior.
- Around line 98-166: Replace the loose regex validation in add_custom_model
with Hugging Face’s official validator: import validate_repo_id (from
huggingface_hub.utils or huggingface_hub) and call validate_repo_id(hf_repo_id,
repo_type="model") after trimming hf_repo_id; catch the ValueError it raises and
re-raise or convert to your ValueError with a clear message so invalid IDs are
rejected early. Update or remove the _HF_REPO_RE usage to avoid allowing repo
IDs that the Hub would reject (refer to add_custom_model and _HF_REPO_RE to
locate places to change).
In `@backend/routes/models.py`:
- Around line 389-425: The custom model status detection misses active custom
downloads and some file types; update the loop in
custom_models.list_custom_models handling (symbols:
custom_models.list_custom_models, active_download_repos, models.ModelStatus,
hf_constants.HF_HUB_CACHE, repo_cache) to mirror built-in logic by: (1) marking
downloading=true if either hf_repo OR the custom model name (cm_name like
"custom:<id>") is present in active_download_repos; (2) expand on-disk detection
to include .npz, .pt, .pth (in addition to .safetensors and .bin) and treat
partial/incomplete download artifacts (e.g., *.part, *.partial, temp/lock files,
or known incomplete blob markers) as "downloading" rather than fully downloaded;
and (3) compute size_mb only when files are fully present. Ensure these checks
replace the existing repo_cache/snapshots checks and propagate the corrected
downloaded/downloading flags into the models.ModelStatus(...) call.
- Around line 415-425: The custom-model entries are being forced to
loaded=False; instead detect loaded state by resolving the backend and comparing
its _current_model_size to the custom model name (cm_name). Locate the code
creating models.ModelStatus (where cm_name, hf_repo, cm_size_mb are available)
and replace the naive loaded flag with a check that: resolve the backend
instance for this model (same logic used for load_model_async), then set loaded
= (backend is not None and getattr(backend, "_current_model_size", None) ==
cm_name). Keep other fields (downloaded, downloading, is_custom) unchanged and
do not call get_model_config or check_model_loaded for custom models.
---
Outside diff comments:
In `@app/src/components/ServerSettings/ModelManagement.tsx`:
- Around line 898-970: The Unregister button for custom models is only rendered
inside the downloaded branch so newly added custom models that aren't downloaded
can't be unregistered; move or duplicate the custom unregister UI so it appears
regardless of freshSelectedModel.downloaded (i.e., lift the conditional
freshSelectedModel.model_name.startsWith('custom:') block out of the
downloaded/not-downloaded ternary or render an equivalent button in the
non-downloaded branch), keeping the same click handler that checks
removeCustomModelMutation.isPending, slices the custom id, calls
removeCustomModelMutation.mutate(customId, { onSuccess: () => {
setDetailOpen(false); setSelectedModel(null); } }), and preserves disabling
logic (use removeCustomModelMutation.isPending and/or freshSelectedModel.loaded
as appropriate).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6de2f34a-90f6-49f4-ae54-dfb90794a3e0
📒 Files selected for processing (7)
app/src/components/Generation/FloatingGenerateBox.tsxapp/src/components/ServerSettings/ModelManagement.tsxapp/src/lib/api/client.tsbackend/backends/mlx_backend.pybackend/backends/pytorch_backend.pybackend/custom_models.pybackend/routes/models.py
✅ Files skipped from review due to trivial changes (1)
- app/src/components/Generation/FloatingGenerateBox.tsx
| path = _get_config_path() | ||
| if not path.exists(): | ||
| return {"models": []} | ||
| try: | ||
| with open(path, "r") as f: | ||
| return json.load(f) | ||
| except json.JSONDecodeError as exc: | ||
| # Back up the corrupt file so we don't lose data | ||
| backup = path.with_suffix( | ||
| f".json.corrupt.{datetime.utcnow().strftime('%Y%m%dT%H%M%S')}" | ||
| ) | ||
| try: | ||
| path.rename(backup) | ||
| logger.error( | ||
| "Corrupt custom_models.json backed up to %s: %s", backup, exc | ||
| ) | ||
| except OSError as rename_err: | ||
| logger.error( | ||
| "Failed to back up corrupt config %s: %s (original error: %s)", | ||
| path, rename_err, exc, | ||
| ) | ||
| raise | ||
| except IOError: | ||
| return {"models": []} |
There was a problem hiding this comment.
Don’t treat every read failure as a missing config.
Line 62 also catches permission errors and transient I/O failures after the file exists. In add_custom_model, that can turn a failed read into {"models": []} and then overwrite the existing registry on save. Only missing files should return an empty config; other OSErrors should surface.
🛠️ Proposed safer load path
- if not path.exists():
- return {"models": []}
try:
- with open(path, "r") as f:
+ with open(path) as f:
return json.load(f)
+ except FileNotFoundError:
+ return {"models": []}
@@
- except IOError:
- return {"models": []}
+ except OSError:
+ logger.exception("Failed to read custom models config: %s", path)
+ raise🧰 Tools
🪛 Ruff (0.15.10)
[warning] 44-44: Unnecessary mode argument
Remove mode argument
(UP015)
[warning] 62-62: Replace aliased errors with OSError
Replace IOError with builtin OSError
(UP024)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/custom_models.py` around lines 40 - 63, The loader currently treats
any IOError as a missing config and returns {"models": []}, which can mask
permission/transient errors and cause overwrites; change the final except to
only treat a missing file as empty: import errno and either catch
FileNotFoundError or use "except OSError as e:" then if e.errno == errno.ENOENT
return {"models": []} else re-raise the exception so permission and other I/O
failures surface; ensure this loader (the function around the shown snippet used
by add_custom_model) uses that behavior.
| # Regex for valid HuggingFace repo IDs: owner/repo where each segment is | ||
| # non-empty and contains only alphanumeric characters, dots, underscores, | ||
| # and hyphens. | ||
| _HF_REPO_RE = re.compile(r"^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$") | ||
|
|
||
|
|
||
| def _generate_id(hf_repo_id: str) -> str: | ||
| """Generate a slug ID from a HuggingFace repo ID. | ||
|
|
||
| Example: 'AryanNsc/IND-QWENTTS-V1' -> 'aryansc-ind-qwentts-v1' | ||
| """ | ||
| slug = hf_repo_id.lower().replace("/", "-") | ||
| slug = re.sub(r"[^a-z0-9-]", "-", slug) | ||
| slug = re.sub(r"-+", "-", slug).strip("-") | ||
| return slug | ||
|
|
||
|
|
||
| def list_custom_models() -> List[dict]: | ||
| """List all custom models. | ||
|
|
||
| Returns: | ||
| List of custom model dicts | ||
| """ | ||
| with _config_lock: | ||
| data = _load_config() | ||
| return data.get("models", []) | ||
|
|
||
|
|
||
| def get_custom_model(model_id: str) -> Optional[dict]: | ||
| """Get a single custom model by ID. | ||
|
|
||
| Args: | ||
| model_id: Custom model ID (slug) | ||
|
|
||
| Returns: | ||
| Model dict or None if not found | ||
| """ | ||
| models = list_custom_models() | ||
| for model in models: | ||
| if model["id"] == model_id: | ||
| return model | ||
| return None | ||
|
|
||
|
|
||
| def add_custom_model(hf_repo_id: str, display_name: str) -> dict: | ||
| """Add a new custom model. | ||
|
|
||
| Args: | ||
| hf_repo_id: HuggingFace repo ID (e.g. 'AryanNsc/IND-QWENTTS-V1') | ||
| display_name: User-friendly display name | ||
|
|
||
| Returns: | ||
| Created model dict | ||
|
|
||
| Raises: | ||
| ValueError: If model already exists or inputs are invalid | ||
| """ | ||
| hf_repo_id = hf_repo_id.strip() | ||
| display_name = display_name.strip() | ||
|
|
||
| if not hf_repo_id: | ||
| raise ValueError("HuggingFace repo ID is required") | ||
| if not display_name: | ||
| raise ValueError("Display name is required") | ||
| if not _HF_REPO_RE.match(hf_repo_id): | ||
| raise ValueError( | ||
| "HuggingFace repo ID must be in format 'owner/model-name' " | ||
| "(alphanumeric, dots, underscores, and hyphens only, no leading/trailing slashes)" | ||
| ) |
There was a problem hiding this comment.
Use HuggingFace’s repo validator instead of the loose regex.
Line 101 accepts values like owner/foo..bar, owner/-repo, or owner/repo--x; HuggingFace Hub rejects those, so registration can succeed but downloads fail later. The HuggingFace docs for validate_repo_id list the stricter constraints, including --/.. rejection and length/name rules: https://huggingface.co/docs/huggingface_hub/package_reference/utilities
🛠️ Proposed validation alignment
+from huggingface_hub.utils import validate_repo_id
+
@@
-_HF_REPO_RE = re.compile(r"^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$")
+_HF_REPO_RE = re.compile(r"^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$")
@@
- if not _HF_REPO_RE.match(hf_repo_id):
+ if hf_repo_id.count("/") != 1:
raise ValueError(
"HuggingFace repo ID must be in format 'owner/model-name' "
"(alphanumeric, dots, underscores, and hyphens only, no leading/trailing slashes)"
)
+ try:
+ validate_repo_id(hf_repo_id)
+ except ValueError as exc:
+ raise ValueError(f"Invalid HuggingFace repo ID: {hf_repo_id}") from excHugging Face huggingface_hub validate_repo_id rules owner repo -- .. leading trailing hyphen dot max length
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/custom_models.py` around lines 98 - 166, Replace the loose regex
validation in add_custom_model with Hugging Face’s official validator: import
validate_repo_id (from huggingface_hub.utils or huggingface_hub) and call
validate_repo_id(hf_repo_id, repo_type="model") after trimming hf_repo_id; catch
the ValueError it raises and re-raise or convert to your ValueError with a clear
message so invalid IDs are rejected early. Update or remove the _HF_REPO_RE
usage to avoid allowing repo IDs that the Hub would reject (refer to
add_custom_model and _HF_REPO_RE to locate places to change).
| # ── Inject custom model entries ────────────────────────────────────── | ||
| try: | ||
| from huggingface_hub import constants as hf_constants | ||
|
|
||
| custom_entries = custom_models.list_custom_models() | ||
| for entry in custom_entries: | ||
| cm_name = f"custom:{entry['id']}" | ||
| hf_repo = entry.get("hf_repo_id", "") | ||
| display = entry.get("display_name", cm_name) | ||
| downloaded = False | ||
| cm_size_mb = None | ||
| try: | ||
| cache_dir = hf_constants.HF_HUB_CACHE | ||
| repo_cache = Path(cache_dir) / ("models--" + hf_repo.replace("/", "--")) | ||
| if repo_cache.exists(): | ||
| snapshots = repo_cache / "snapshots" | ||
| if snapshots.exists() and ( | ||
| any(snapshots.rglob("*.safetensors")) | ||
| or any(snapshots.rglob("*.bin")) | ||
| ): | ||
| downloaded = True | ||
| total = sum(f.stat().st_size for f in repo_cache.rglob("*") if f.is_file()) | ||
| cm_size_mb = total / (1024 * 1024) | ||
| except Exception: | ||
| pass | ||
|
|
||
| statuses.append( | ||
| models.ModelStatus( | ||
| model_name=cm_name, | ||
| display_name=display, | ||
| hf_repo_id=hf_repo, | ||
| downloaded=downloaded, | ||
| downloading=hf_repo in active_download_repos, | ||
| size_mb=cm_size_mb, | ||
| loaded=False, | ||
| is_custom=True, | ||
| ) |
There was a problem hiding this comment.
Mirror built-in download/cache detection for custom entries.
Custom downloads are tracked by model_name (custom:<slug>), but Line 421 checks only hf_repo in active_download_repos, which may miss active custom downloads. This block also ignores .npz/.pt/.pth and incomplete blobs, so MLX custom models or partial downloads can show the wrong status.
🛠️ Proposed status fixes
try:
cache_dir = hf_constants.HF_HUB_CACHE
repo_cache = Path(cache_dir) / ("models--" + hf_repo.replace("/", "--"))
if repo_cache.exists():
+ blobs = repo_cache / "blobs"
+ has_incomplete = blobs.exists() and any(blobs.glob("*.incomplete"))
snapshots = repo_cache / "snapshots"
- if snapshots.exists() and (
- any(snapshots.rglob("*.safetensors"))
- or any(snapshots.rglob("*.bin"))
- ):
+ if not has_incomplete and snapshots.exists() and any(
+ snapshots.rglob(pattern)
+ for pattern in ("*.safetensors", "*.bin", "*.pt", "*.pth", "*.npz")
+ ):
downloaded = True
- total = sum(f.stat().st_size for f in repo_cache.rglob("*") if f.is_file())
+ total = sum(
+ f.stat().st_size
+ for f in repo_cache.rglob("*")
+ if f.is_file() and not f.name.endswith(".incomplete")
+ )
cm_size_mb = total / (1024 * 1024)
except Exception:
pass
+ is_downloading = cm_name in active_download_names or hf_repo in active_download_repos
+ if is_downloading:
+ downloaded = False
+ cm_size_mb = None
+
statuses.append(
models.ModelStatus(
@@
- downloading=hf_repo in active_download_repos,
+ downloading=is_downloading,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/routes/models.py` around lines 389 - 425, The custom model status
detection misses active custom downloads and some file types; update the loop in
custom_models.list_custom_models handling (symbols:
custom_models.list_custom_models, active_download_repos, models.ModelStatus,
hf_constants.HF_HUB_CACHE, repo_cache) to mirror built-in logic by: (1) marking
downloading=true if either hf_repo OR the custom model name (cm_name like
"custom:<id>") is present in active_download_repos; (2) expand on-disk detection
to include .npz, .pt, .pth (in addition to .safetensors and .bin) and treat
partial/incomplete download artifacts (e.g., *.part, *.partial, temp/lock files,
or known incomplete blob markers) as "downloading" rather than fully downloaded;
and (3) compute size_mb only when files are fully present. Ensure these checks
replace the existing repo_cache/snapshots checks and propagate the corrected
downloaded/downloading flags into the models.ModelStatus(...) call.
| statuses.append( | ||
| models.ModelStatus( | ||
| model_name=cm_name, | ||
| display_name=display, | ||
| hf_repo_id=hf_repo, | ||
| downloaded=downloaded, | ||
| downloading=hf_repo in active_download_repos, | ||
| size_mb=cm_size_mb, | ||
| loaded=False, | ||
| is_custom=True, | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Inspect backend model-config resolution for custom model IDs.
rg -nP -C4 '\bdef\s+get_model_config\b|custom:' backendRepository: jamiepine/voicebox
Length of output: 6486
🏁 Script executed:
# Find get_all_model_configs definition
rg -n "def get_all_model_configs" backendRepository: jamiepine/voicebox
Length of output: 144
🏁 Script executed:
# Find check_model_loaded function
rg -n "def check_model_loaded" backendRepository: jamiepine/voicebox
Length of output: 147
🏁 Script executed:
# Examine custom_models.py to see how custom model configs are handled
head -50 backend/custom_models.pyRepository: jamiepine/voicebox
Length of output: 1435
🏁 Script executed:
# Check the current custom model injection code at lines 391-425 in models.py
sed -n '385,430p' backend/routes/models.pyRepository: jamiepine/voicebox
Length of output: 1886
🏁 Script executed:
# Get the full implementation of get_all_model_configs
sed -n '361,380p' backend/backends/__init__.pyRepository: jamiepine/voicebox
Length of output: 794
🏁 Script executed:
# Get the full implementation of check_model_loaded
sed -n '469,485p' backend/backends/__init__.pyRepository: jamiepine/voicebox
Length of output: 860
🏁 Script executed:
# Get the full implementation of get_model_config
sed -n '374,390p' backend/backends/__init__.pyRepository: jamiepine/voicebox
Length of output: 592
🏁 Script executed:
# Check how custom models are structured - do they have ModelConfig representation?
rg -n "class ModelConfig|def get_custom_model" backendRepository: jamiepine/voicebox
Length of output: 196
🏁 Script executed:
# Search for how custom models are used/loaded in the request handling
rg -n "cm_name|custom_model|model_name.*custom:" backend/routes/models.py -A 3 -B 1 | head -80Repository: jamiepine/voicebox
Length of output: 2280
🏁 Script executed:
# Check if custom models go through the same TTS backend engine loading
rg -n "get_tts_model\|is_loaded\|engine.*custom" backend -iRepository: jamiepine/voicebox
Length of output: 44
🏁 Script executed:
# Look at the delete/unregister flow to understand the real impact
rg -n "def.*delete.*model|def.*unregister" backend/routes/models.py -A 10Repository: jamiepine/voicebox
Length of output: 624
🏁 Script executed:
# Check how custom models are used in TTS inference - are they loaded like built-in models?
rg -n "model_name.*custom|custom.*model_name|custom:" backend/services -A 3 -B 1Repository: jamiepine/voicebox
Length of output: 44
🏁 Script executed:
# Check the TTS model loading/inference logic to see if it handles custom model names
rg -n "def.*inference|def.*synthesize|_current_model_size|model_size" backend/services/tts.py -A 2 | head -100Repository: jamiepine/voicebox
Length of output: 44
🏁 Script executed:
# Look at the frontend side to see what `loaded` status actually controls
fd -e tsx -e ts frontend | xargs rg -l "loaded|delete.*model" | head -3Repository: jamiepine/voicebox
Length of output: 143
🏁 Script executed:
# Check the delete/unregister endpoint's actual guards
sed -n '491,540p' backend/routes/models.pyRepository: jamiepine/voicebox
Length of output: 2141
🏁 Script executed:
# Search for how TTS model loads and uses model names/sizes
rg -n "model_size|_current_model_size|engine" backend/services/tts.py | head -40Repository: jamiepine/voicebox
Length of output: 44
🏁 Script executed:
# Check if there's any custom model tracking in the service layer
rg -n "custom:|cm_name" backend/servicesRepository: jamiepine/voicebox
Length of output: 44
🏁 Script executed:
# Check if custom models have an engine or model size in their stored config
rg -n "add_custom_model|custom_model.*schema|CustomModel" backend/custom_models.py -A 5 | head -60Repository: jamiepine/voicebox
Length of output: 319
🏁 Script executed:
# Look for where custom models are actually used in inference
rg -n "custom:" backend/main.py backend/services -B 2 -A 2Repository: jamiepine/voicebox
Length of output: 44
🏁 Script executed:
# Check if the TTS model can accept custom model names
rg -n "def.*synthesize|def.*__init__|model_size.*custom|custom.*engine" backend/services/tts.py -B 2 -A 5Repository: jamiepine/voicebox
Length of output: 44
🏁 Script executed:
# Check main.py to see how custom models are handled in the TTS request
rg -n "model_size|custom" backend/main.py -B 3 -A 3Repository: jamiepine/voicebox
Length of output: 44
🏁 Script executed:
# Look at TTS service to understand what "loaded" means
head -100 backend/services/tts.pyRepository: jamiepine/voicebox
Length of output: 828
🏁 Script executed:
# Check if custom models have any special handling in backend initialization
rg -n "custom|ModelConfig" backend/backends/__init__.py | head -40Repository: jamiepine/voicebox
Length of output: 1865
🏁 Script executed:
# Check the frontend to see how it uses the 'loaded' status for custom models
find frontend -name "*.tsx" -o -name "*.ts" | xargs grep -l "loaded\|delete\|custom" 2>/dev/null | head -5Repository: jamiepine/voicebox
Length of output: 105
🏁 Script executed:
# Look at how custom models are actually used when making TTS requests
rg -n "model_size\|SynthesisRequest\|synthesize" backend/routes -B 2 -A 5 | head -80Repository: jamiepine/voicebox
Length of output: 44
🏁 Script executed:
# Check if custom models actually support different "sizes" or engines
sed -n '142,180p' backend/custom_models.pyRepository: jamiepine/voicebox
Length of output: 1385
🏁 Script executed:
# Understand the relationship: when a custom model is used, what engine handles it?
rg -n "hf_repo_id\|model_size\|engine" backend/routes/models.py -B 2 -A 2 | grep -A 10 "custom_entries"Repository: jamiepine/voicebox
Length of output: 44
🏁 Script executed:
# Check how synthesis requests work - do they specify custom model IDs or do they use engine+repo?
rg -n "SynthesisRequest\|model_size.*=\|class.*Request" backend/models.py -A 10 | head -60Repository: jamiepine/voicebox
Length of output: 44
🏁 Script executed:
# Verify: are custom models used with a specific engine or as full model replacements?
sed -n '76,90p' backend/models.pyRepository: jamiepine/voicebox
Length of output: 1015
🏁 Script executed:
# Check if custom models can be loaded/tracked like built-in models
rg -n "is_loaded|_loaded|_current_model" backend/backends -A 2 | head -50Repository: jamiepine/voicebox
Length of output: 3198
🏁 Script executed:
# Verify the actual inference path: how does the backend use a custom model spec?
rg -n "synthesize|load.*model|custom:" backend/backends/pytorch_backend.py -B 2 -A 5 | head -80Repository: jamiepine/voicebox
Length of output: 3103
🏁 Script executed:
# Check if there's tracking of which custom model is currently loaded
rg -n "_current.*model\|loaded.*custom\|custom.*loaded" backend -rRepository: jamiepine/voicebox
Length of output: 475
🏁 Script executed:
# Verify: can custom models actually be loaded and tracked like built-in models?
sed -n '78,100p' backend/backends/pytorch_backend.pyRepository: jamiepine/voicebox
Length of output: 882
🏁 Script executed:
# Check the full flow of model loading - does it handle custom model IDs correctly?
sed -n '102,140p' backend/backends/pytorch_backend.pyRepository: jamiepine/voicebox
Length of output: 1784
🏁 Script executed:
# Understand what "loaded=False" prevents in the frontend - is it just UI or backend validation?
rg -n "loaded.*False\|disable.*delete\|unregister" backend/routes -B 2 -A 3Repository: jamiepine/voicebox
Length of output: 44
🏁 Script executed:
# Verify: after loading a custom model, what is _current_model_size set to?
# Also check if custom models use a different backend or the same pytorch/qwen backend
rg -n "qwen_custom_voice\|pytorch.*custom\|custom.*engine" backend/backends -B 2 -A 5 | head -100Repository: jamiepine/voicebox
Length of output: 44
🏁 Script executed:
# Check the synthesis endpoint to see what happens when custom model is requested
rg -n "def.*synthesize\|model_size.*custom" backend/routes -B 3 -A 8Repository: jamiepine/voicebox
Length of output: 44
🏁 Script executed:
# Search for any synthesis endpoint or handler that uses model_size
rg -n "def.*synth\|async def\|model_size" backend/routes/synthesis.py -B 2 -A 8 2>/dev/null || rg -n "def.*synth\|async def" backend/routes -B 2 -A 8Repository: jamiepine/voicebox
Length of output: 44
🏁 Script executed:
# Check if there's any test or example showing custom model usage
fd -e py backend | xargs grep -l "custom:" | head -3Repository: jamiepine/voicebox
Length of output: 129
🏁 Script executed:
# Look for where load_model is actually called with custom model IDs
rg -n "load_model\|synthesize" backend/routes -B 3 -A 10 | head -80Repository: jamiepine/voicebox
Length of output: 44
🏁 Script executed:
# Check what the backend's _current_model_size tracking looks like
sed -n '34,42p' backend/backends/pytorch_backend.pyRepository: jamiepine/voicebox
Length of output: 362
🏁 Script executed:
# Understand: when a custom model is loaded, is _current_model_size set to "custom:xyz"?
sed -n '56,62p' backend/backends/pytorch_backend.pyRepository: jamiepine/voicebox
Length of output: 433
🏁 Script executed:
# Look at how the Qwen backend handles model_size in load path
sed -n '45,80p' backend/backends/pytorch_backend.pyRepository: jamiepine/voicebox
Length of output: 1359
🏁 Script executed:
# Key question: when a custom model is loaded, what does _current_model_size actually contain?
# Check the complete _load_model_sync to see what's stored
sed -n '102,146p' backend/backends/pytorch_backend.pyRepository: jamiepine/voicebox
Length of output: 1968
🏁 Script executed:
# Now check: does check_model_loaded compare against _current_model_size?
sed -n '469,510p' backend/backends/__init__.pyRepository: jamiepine/voicebox
Length of output: 1826
🏁 Script executed:
# Critical verification: when a custom model is loaded, what gets stored in _current_model_size?
# If custom model "custom:my-repo" is loaded, is _current_model_size set to "custom:my-repo"?
sed -n '136,138p' backend/backends/pytorch_backend.pyRepository: jamiepine/voicebox
Length of output: 212
🏁 Script executed:
# Confirmation: are custom models able to actually be loaded via the standard load_model path?
sed -n '78,98p' backend/backends/pytorch_backend.pyRepository: jamiepine/voicebox
Length of output: 818
🏁 Script executed:
# Final check: is there a way to get the currently loaded HF repo ID for comparison?
rg -n "hf_repo_id\|get_hf_repo_id_for_custom_model" backend/backendsRepository: jamiepine/voicebox
Length of output: 44
Custom models need actual loaded state checking, but the suggested approach won't work.
The concern is valid: custom models are hard-coded with loaded=False even though they can be loaded and tracked like built-in models. However, the suggested fix is incorrect.
get_model_config("custom:<slug>") will always return None because get_all_model_configs() only includes built-in models, and check_model_loaded() requires a ModelConfig object. Custom models are stored separately and don't have ModelConfig representations.
Instead, check if the custom model is currently loaded by comparing against the backend's _current_model_size. When a custom model is loaded (e.g., load_model_async("custom:xyz")), _current_model_size is set to that exact string. Resolve the custom entry, get the backend instance, and check if backend._current_model_size == cm_name:
+ custom_loaded = False
+ try:
+ backend = get_tts_backend()
+ custom_loaded = backend._current_model_size == cm_name if backend.is_loaded() else False
+ except Exception:
+ custom_loaded = False
+
statuses.append(
models.ModelStatus(
model_name=cm_name,
display_name=display,
hf_repo_id=hf_repo,
downloaded=downloaded,
downloading=hf_repo in active_download_repos,
size_mb=cm_size_mb,
- loaded=False,
+ loaded=custom_loaded,
is_custom=True,
)
)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/routes/models.py` around lines 415 - 425, The custom-model entries
are being forced to loaded=False; instead detect loaded state by resolving the
backend and comparing its _current_model_size to the custom model name
(cm_name). Locate the code creating models.ModelStatus (where cm_name, hf_repo,
cm_size_mb are available) and replace the naive loaded flag with a check that:
resolve the backend instance for this model (same logic used for
load_model_async), then set loaded = (backend is not None and getattr(backend,
"_current_model_size", None) == cm_name). Keep other fields (downloaded,
downloading, is_custom) unchanged and do not call get_model_config or
check_model_loaded for custom models.
Summary
Addresses code review findings across frontend and backend — input validation, file integrity, error handling, and code deduplication.
Changes
Frontend
model_nameformat before removal; disable Remove button during pending mutationmodelIdinremoveCustomModelpathmodelSizewith regex (built-in sizes + custom slugs)GenerationForm/FloatingGenerateBoxBackend
owner/reporegex forhf_repo_idmodel_sizeField--onefilebundles (viaplatformdirs)HTTPExceptionbefore broadexcept Exceptionso 202 download-in-progress responses aren't swallowed as 500splatformdirs>=4.0.0--data-dirCLI flagSkipped
main.pyprobe_model_cache_status refactoring (pure refactoring, deferred to reduce regression risk)Verification
tsc --noEmit— zero errorspy_compile— all modified files passSummary by CodeRabbit
Release Notes
New Features
Improvements