DEV Community

orca_forge
orca_forge

Posted on Originally published at forge.workstyle.tech

Four Registration Paths, Zero Management Screens — How Audio Asset Catalogs Became Unmanageable

📝 Originally published (in Japanese) at forge.workstyle.tech.

When asked, "Which screen manages this voice?" I couldn’t answer.

The question was about changing the settings for a reference voice assigned to a character. It’s listed in the catalog, and it can be assigned to a character. Yet, there’s no screen anywhere to edit or delete that entry.

After investigating, I found that the catalog has four registration paths, and three of them had no management screens.

Inside the Catalog

Voice assets are consolidated into a single table called voice_catalog. The design is correct. Assigning voices to characters only requires referencing the ID from this table.

Here’s the breakdown of the 76 active entries:

kind provider Count Management Screen
sbv2_finetuned Style-Bert-VITS2 26 ✅ Yes
sovits_reference GPT-SoVITS 22 ❌ No
cosyvoice_reference CosyVoice2 22 ❌ No
voicevox_preset VOICEVOX 6 ❌ No

Only 26 entries have a management screen. The remaining 50 entries are listed in the catalog and usable, but there’s no way to list or edit them.

Why Did This Happen?

The issue arose because each registration path writes to a different destination.

[Training Screen]         → voice_models table → Derived registration in voice_catalog
[Emotional Voice Pipeline] → Directly to voice_catalog
[Reference Voice Registration]     → Directly to voice_catalog
[Initial Deployment Script]  → Directly to voice_catalog
                              ↓
                    [Character Voice Selection UI] ← The only exit (read-only + assignment)
Enter fullscreen mode Exit fullscreen mode

The management screen lists the voice_models table. Models created from the training screen appear here because they’re added to this table.

Meanwhile, the other three paths write directly to voice_catalog. Since there’s no corresponding entry in voice_models, these entries never appear in the management screen by design.

And the only screen that lists voice_catalog is the character voice selection UI. But this is assignment-only, allowing only listening and selection. There’s no way to edit or delete catalog entries themselves.

The backend had an API for editing, but there was no screen to use it.

The Root Cause of "Conflicting Lists"

This structure manifested as another symptom. There were reports of conflicting voice lists across three screens.

Training Screen List     → voice_models (26 entries)
Voice List Screen       → Subset of voice_models
Character Selection UI → voice_catalog (76 entries)
Enter fullscreen mode Exit fullscreen mode

They were looking at different tables, so inconsistencies were inevitable. One table is a model management ledger, while the other is a catalog of voices assignable to characters. Both were presented as "voice lists," despite serving different purposes.

At one point, we thought it was a synchronization issue and tried adding sync processes, but that was the wrong direction. Instead of syncing, we should have clarified what each list represented.

Creating an Inventory API

To understand the current state, I created an API that returns all catalog entries with their details.

@router.get("/inventory")
async def catalog_inventory(db: AsyncDB, token_data: dict = Depends(verify_token)):
    """Catalog inventory (for management screen).

    An entry's existence doesn't mean it's a usable model. Models that have lost styles on the inference server
    may remain in the catalog, mixing into character assignment options.
    Returns speaking style, style count, and reference character count in one call for screen-side identification.
    """
    rows = await fetch_catalog(db, owner_ids)

    # Speaking style requires tracing back to the manufacturing job
    jobs = {str(vm_id): {"conv_style": cs, "seed": seed}
            for vm_id, cs, seed in await db.execute(sqltext(
                "SELECT vm.id, j.progress->'params'->>'conv_style', j.seed "
                "FROM voice_design_jobs j JOIN voice_models vm ON vm.id = j.voice_model_id"))}

    # Who is using it?
    assigned = {cid: n for cid, n in await db.execute(sqltext(
        "SELECT profile->>'voice_catalog_id', count(*) FROM characters "
        "WHERE profile->>'voice_catalog_id' IS NOT NULL GROUP BY 1"))}

    # Does it exist on the inference server? (Empty style list = unusable)
    styles = await fetch_styles_parallel(rows)

    return {"items": [{
        "id": str(v.id), "name": v.name, "kind": v.kind, "provider": v.provider,
        "conv_style": jobs.get(str(v.source_voice_model_id), {}).get("conv_style"),
        "style_count": len(styles[v.inference_model_id]) if ... else None,
        "alive": None if st is None else len(st) > 0,
        "has_scream": "Scream" in (st or []),        # Presence of extreme acting styles
        "assigned_characters": assigned.get(str(v.id), 0),
        ...
    } for v in rows]}
Enter fullscreen mode Exit fullscreen mode

The API returns four types of information:

Speaking style (conv_style). This indicates the purpose for which the voice was created. However, this isn’t in voice_catalog; it requires tracing back to the manufacturing job via voice_models.

Style count and liveness. Queries the inference server to confirm if the voice exists. An empty style list means "registered but unusable."

Presence of extreme acting styles. In this environment, the "Scream" style triggers intense reactions, which isn’t suitable for professional voices (since speaking styles can’t be changed after training). This is used for warning against misselection.

Number of referencing characters. Identifies unused entries.

Result: Only 19 Entries Have Speaking Styles

The inventory revealed a clear picture:

Active 76 entries
  ├ With speaking style 19 … From voice-design pipeline
  └ Without speaking style 57 … Old recipes / Emotional pipeline / Reference voices / Presets
Enter fullscreen mode Exit fullscreen mode

Three-quarters lack recorded speaking styles. Even if we create a system to automatically select voices based on purpose, these 57 entries won’t be considered.

However, the breakdown shows the issue is less severe than it seems.

kind Count With Style Referenced by Characters
sbv2_finetuned 26 19 16
sovits_reference 22 0 6
cosyvoice_reference 22 0 0
voicevox_preset 6 0 0

Of the 57 entries without styles, 28 have never been referenced by characters. They’re essentially dead stock.

Among the 22 actively used entries, 15 (68%) have retrievable speaking styles. The "three-quarters unretrievable" ratio applies to the entire inventory, not the active assets.

I almost made design decisions based on inventory statistics, but filtering by actual usage changes the picture. The inventory should capture both total count and usage status.

Deciding Not to Retroactively Add Labels

The question arose: "Can we retroactively add speaking styles to the 57 entries without them?" We decided not to.

conv_style isn’t a label for how the model behaves; it’s a record of which script corpus it was trained on. Adding narration to an old recipe model doesn’t mean it was trained on a narration corpus. It also hasn’t passed current quality gates.

Adding labels that diverge from reality would corrupt the database. If an automated selection system relied on this, it could lead to hard-to-trace failures.

Instead, we explicitly mark them as "speaking style unknown" and display "purpose unrecorded" in the UI. We treat this as a fact, not a missing value.

We’ve encountered similar issues in this environment. One boolean column served both "gallery publication" and "guest path authorization," leading to connection errors when disabling publication. Another column named role had the same value across all rows, representing technology type, not job role.

When one column has two meanings, disabling one can break the other. Retroactively adding labels would create this problematic structure.

Creating the Management Screen

With the inventory API in place, I added a screen to display it:

Name | Type | Speaking Style | Seed | Style Count | Status | Characters Using | Actions
Enter fullscreen mode Exit fullscreen mode

Actions are limited to renaming and toggling capability flags.

Assignments remain in character management. Handling assignments here would mix catalog management and assignment responsibilities.

Training and retraining remain in the training screen. Adding more paths would recreate the same problem.

Deletion isn’t included. Removing an entry referenced by characters would break things. A disable flag would be safer, but it wasn’t needed yet, so I didn’t implement it.

Entries without entities are highlighted in red. Catalog entries without inference server entities won’t work when selected. This makes them identifiable in the list.

Summary

  • If there are multiple registration paths, each needs its own management path. Ensure one screen isn’t trying to handle everything.
  • "Conflicting lists" aren’t always a sync issue. They might just be looking at different tables. Before adding sync, confirm what each list represents.
  • Inventory should capture both total count and usage status. Inventory ratios and active asset ratios tell different stories.
  • An entry’s existence doesn’t mean it’s usable. Separately confirm the entity’s liveness.
  • Don’t add labels that can’t be retroactively applied. Records diverging from reality harm automated decision-making.
  • Avoid giving one column two meanings. Disabling one can break the other.

Series: Mass-Producing Practical Voices from Diffusion TTS

This series documents designing voices from single captions, generating training corpora, and mass-producing role-specific practical voices. This article is Part 4: Operations.

← Previous: "ja" vs. "JP": Creating a Babbling Model

→ Next: Deployments Overwriting Each Other’s Work

All 18 Articles in the Series

  1. High-Quality TTS Was Too Slow for Conversations
  2. Voice Gacha: Deterministic Voice Selection
  3. Machine-Selecting "Narrator-Like" Voices from 24 Candidates
  4. Strict Quality Gates Favor Monotonous Voices
  5. Speaking Styles Can’t Be Changed After Training
  6. TTS That Changes "Recording Room" Every Generation
  7. One Rough Clip Can Ruin an Entire Style
  8. Where Did AI Learn to Draw Out "Hello~"?
  9. "Shomo" Instead of "Shoyō" — Character Allowlist Broke Japanese TTS
  10. Hallucination Guard Code Never Triggered for Hallucinations
  11. Three Characters Allowed by Quality Gates Became a Verbal Tic
  12. Rejecting Candidates for Fixable Defects
  13. Defects Invisible to Transcription
  14. 70 Minutes of Training Data Lost to a Network Blink
  15. "ja" vs. "JP": Creating a Babbling Model 16. Four Registration Paths, Zero Management Screens ← You are here
  16. Deployments Overwriting Each Other’s Work
  17. Thresholds for Unmeasured Metrics Always Fail

The insights in this series are compiled in the notes on Mass-Producing Practical Voices from Diffusion TTS Manufacturing Pipeline.

Top comments (0)