feat: add Chatterbox model support for Cloud asset browser (#8418)

## Summary

Adds support for creating Chatterbox TTS nodes when clicking Chatterbox
models in the Cloud asset browser.

## Changes

### modelToNodeStore.ts
- Add `findProvidersWithFallback()` helper for hierarchical model type
lookups (e.g., `parent/child` falls back to `parent`)
- Register 4 Chatterbox model directories with empty widget keys:
  - `chatterbox/chatterbox` → `FL_ChatterboxTTS`
  - `chatterbox/chatterbox_turbo` → `FL_ChatterboxTurboTTS`
- `chatterbox/chatterbox_multilingual` → `FL_ChatterboxMultilingualTTS`
  - `chatterbox/chatterbox_vc` → `FL_ChatterboxVC`

### createModelNodeFromAsset.ts
- Skip widget assignment when `provider.key` is empty (for nodes that
auto-load models without a widget selector)

### Tests
- Add tests for hierarchical fallback behavior
- Add tests for empty widget key (auto-load nodes)
- Add Chatterbox node types to mock data

## Notes

- Empty `key` convention: Chatterbox nodes auto-load their models and
don't have a model selector widget, so we register them with `key: ''`
and skip the widget assignment step
- Hierarchical fallback only goes one level deep (`a/b/c` → `a`, not
`a/b`)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8418-feat-add-Chatterbox-model-support-for-Cloud-asset-browser-2f76d73d365081be822bc369b155f099)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
Christian Byrne
2026-01-29 16:26:51 -08:00
committed by GitHub
parent dce6fd1040
commit 80ccc13659
4 changed files with 148 additions and 24 deletions

View File

@@ -23,7 +23,11 @@ const EXPECTED_DEFAULT_TYPES = [
'audio_encoders',
'model_patches',
'animatediff_models',
'animatediff_motion_lora'
'animatediff_motion_lora',
'chatterbox/chatterbox',
'chatterbox/chatterbox_turbo',
'chatterbox/chatterbox_multilingual',
'chatterbox/chatterbox_vc'
] as const
type NodeDefStoreType = ReturnType<typeof useNodeDefStore>
@@ -61,7 +65,11 @@ const MOCK_NODE_NAMES = [
'AudioEncoderLoader',
'ModelPatchLoader',
'ADE_LoadAnimateDiffModel',
'ADE_AnimateDiffLoRALoader'
'ADE_AnimateDiffLoRALoader',
'FL_ChatterboxTTS',
'FL_ChatterboxTurboTTS',
'FL_ChatterboxMultilingualTTS',
'FL_ChatterboxVC'
] as const
const mockNodeDefsByName = Object.fromEntries(
@@ -135,6 +143,36 @@ describe('useModelToNodeStore', () => {
const provider = modelToNodeStore.getNodeProvider('checkpoints')
expect(provider).toBeDefined()
})
it('should fallback to top-level folder for hierarchical model types', () => {
const modelToNodeStore = useModelToNodeStore()
modelToNodeStore.registerDefaults()
const provider = modelToNodeStore.getNodeProvider('checkpoints/subfolder')
expect(provider).toBeDefined()
expect(provider?.nodeDef?.name).toBe('CheckpointLoaderSimple')
})
it('should return undefined for hierarchical type with unregistered top-level', () => {
const modelToNodeStore = useModelToNodeStore()
modelToNodeStore.registerDefaults()
expect(
modelToNodeStore.getNodeProvider('UnknownType/subfolder')
).toBeUndefined()
})
it('should return provider for chatterbox nodes with empty key', () => {
const modelToNodeStore = useModelToNodeStore()
modelToNodeStore.registerDefaults()
const provider = modelToNodeStore.getNodeProvider(
'chatterbox/chatterbox_vc'
)
expect(provider).toBeDefined()
expect(provider?.nodeDef?.name).toBe('FL_ChatterboxVC')
expect(provider?.key).toBe('')
})
})
describe('getAllNodeProviders', () => {
@@ -184,6 +222,17 @@ describe('useModelToNodeStore', () => {
const providers = modelToNodeStore.getAllNodeProviders('checkpoints')
expect(providers.length).toBeGreaterThan(0)
})
it('should fallback to top-level folder for hierarchical model types', () => {
const modelToNodeStore = useModelToNodeStore()
modelToNodeStore.registerDefaults()
const providers = modelToNodeStore.getAllNodeProviders(
'checkpoints/subfolder'
)
expect(providers).toHaveLength(2)
expect(providers[0].nodeDef.name).toBe('CheckpointLoaderSimple')
})
})
describe('registerNodeProvider', () => {
@@ -491,5 +540,16 @@ describe('useModelToNodeStore', () => {
expect(modelToNodeStore.getNodeProvider('')).toBeUndefined()
expect(modelToNodeStore.getAllNodeProviders('')).toEqual([])
})
it('should handle invalid input types gracefully', () => {
const modelToNodeStore = useModelToNodeStore()
modelToNodeStore.registerDefaults()
expect(modelToNodeStore.getNodeProvider(null as any)).toBeUndefined()
expect(modelToNodeStore.getNodeProvider(undefined as any)).toBeUndefined()
expect(modelToNodeStore.getNodeProvider(123 as any)).toBeUndefined()
expect(modelToNodeStore.getAllNodeProviders(null as any)).toEqual([])
expect(modelToNodeStore.getAllNodeProviders(undefined as any)).toEqual([])
})
})
})