📝 Originally published (in Japanese) at forge.workstyle.tech.
When You Collect 70 Voice Conversion Anchors, the Next Problem Emerges
Now that you’ve gathered 70 voice-conversion anchors, a new worry creeps in: Does this library truly cover everything? Are you overloaded with low male voices and missing high, bright female ones? Are you duplicating similar voices over and over?
You can guarantee the quality of each anchor with the selection filters and audits we wrote about in another post. But spotting systemic gaps—whether the collection is skewed as a whole—isn’t something you can see by looking at individual items. What you need is an overview, a single map of the entire voice library.
This post shows how we visualized an anchor set with two diagrams so you can check coverage and bias at a glance. No model required—just numpy and matplotlib.
Two Spaces You Want to Visualize
Voice anchors live in two different “spaces” with distinct properties.
Semantic-axis space
The app lets users adjust voices along eight axes (0–100 %): age feel, gender, pitch, body type, huskiness, clarity, warmth, roughness. Placing each anchor on these eight axes reveals the coverage of the design space.Speaker-embedding space
A 192-dimensional vector from campplus. Similar voices cluster together; dissimilar ones drift apart. This map shows the spread of raw voice quality.
The first space answers “Do we have enough choices along the axes the user can control?” The second answers “Where are the dense clusters and empty gaps in pure voice quality?” We decided to draw both on one page.
(A) Semantic-axis coverage: strip plots
For each of the eight axes we lay out every anchor’s value in a horizontal strip. One strip per axis, with tiny jitter added so points don’t overlap vertically, and a crimson vertical line marking the mean.
for i, key in enumerate(axes_keys):
vals = np.array([sv[n][i] for n in names])
y = np.full_like(vals, i, dtype=float) + (rng.random(len(vals)) - 0.5) * 0.5
axA.scatter(vals, y, s=18, alpha=0.55, color="#3a76b4")
axA.scatter(vals.mean(), i, s=90, marker="|", color="crimson", zorder=3) # mean
Reading the plot is straightforward: if points stretch from 0 % to 100 % on an axis, coverage is good; if they’re clustered on one side, there’s a gap. For example, a gender axis that reaches both ends tells you you can pick anything from masculine to feminine. A mean far off-center or a missing cluster immediately shows where anchors are missing.
Turning the vague feeling “I still need more ___ voices” into a concrete empty stretch on the axis is the real value of this diagram.
(B) Speaker-embedding scatter plot: PCA to 2-D
The second diagram compresses the 192-D speaker embeddings into two dimensions. We use a plain PCA—subtract the mean, run SVD, and take the first two principal components—no fancy libraries required.
X = np.asarray(bank.embeddings, float)
Xc = X - X.mean(axis=0)
U, S, Vt = np.linalg.svd(Xc, full_matrices=False)
Z = Xc @ Vt[:2].T # project to top 2 PCs
ev = (S[:2] ** 2) / (S ** 2).sum() * 100 # explained variance [%]
Each point (anchor) is labeled with the speaker name and colored by the gender-axis value (blue = masculine / red = feminine). We’re overlaying a human-interpretable axis on top of an abstract embedding space.
gi = list(axes_keys).index("gender")
gcol = np.array([sv[n][gi] for n in names])
axB.scatter(Z[:, 0], Z[:, 1], c=gcol, cmap="coolwarm", s=40, ...)
Here you see dense clumps (duplicate voices) and empty patches (gaps in voice quality). The coloring also shows how the embedding’s main axes relate to gender. Axis labels include the explained variance (PC1 (xx%)) so you know how much of the original variance this 2-D slice captures.
Quiet Implementation Details That Matter
Nothing flashy, but small touches that turn a visualization into a usable tool:
Explicit Japanese font selection.
Matplotlib defaults to tofu (â–ˇ) for Japanese. We tried Hiragino Sans and similar candidates, then placed the working font at the top of the script. Without readable labels, the map is half useless.Labels pulled from a lookup table.
Ifanchor_sources.jsonexists, we display the speaker’s real name (spk24); otherwise we fall back to the rawspkid. The labeling work from another post pays off here—you can talk about distributions in names, not numbers.Model-agnostic.
As long as you have the embeddings and slider values, you can draw these plots. No need to load the heavy Seed-VC model. Just numpy and matplotlib. Lightweight enough for CI or a quick local run, which quietly nudges the team to make visualization a habit.
Turning the Maps Into Decisions
A visualization isn’t the end goal; it’s a tool for deciding what to do next. From the two maps you can derive concrete actions:
- Axis with a one-sided gap (A) → prioritize collecting voices in that direction (e.g., more elderly voices, more husky voices).
- Dense clump (B) → duplicate voices; stop adding more in that region.
- Sparse region (B) → a hole in voice-quality coverage; hunt for material to fill it.
When “I feel like we’re missing something” becomes “this exact stretch on this axis is empty,” collecting anchors stops being guesswork. Treating the voice library as a design target is the real win of visualization.
Summary
We visualized an anchor set in two spaces:
(A) coverage along eight semantic axes,
(B) a PCA scatter plot of 192-D speaker embeddings.(A) Strip plots reveal empty stretches and mean shifts on each axis—quickly spotting missing voice types.
(B) A simple PCA (mean-centering + SVD) projects embeddings to 2-D; coloring by gender shows how the abstract space aligns with a human axis. Dense clusters and empty patches become visible.
Explicit Japanese fonts, label lookups, and model-agnostic code make the visualization actionable.
The purpose of the maps isn’t just to look—they drive decisions. Fill the empty axis stretches and sparse regions to guide your next anchor hunt.
Top comments (0)