Hard Negative Generation¶
Understanding how WakeBuilder generates phonetically similar words to reduce false positives.
Why Hard Negatives Matter¶
Hard negatives are words that sound similar to your wake word but should NOT trigger detection. Without them, a model might:
- Trigger on partial matches ("jar" for "jarvis")
- Confuse similar-sounding words ("jarvy" for "jarvis")
- Have high false positive rates in real-world usage
graph LR
subgraph "Without Hard Negatives"
A1[Model hears 'jarv']
A1 --> B1{Similar enough?}
B1 -->|Yes| C1[FALSE TRIGGER]
end
subgraph "With Hard Negatives"
A2[Model hears 'jarv']
A2 --> B2{Learned: jarv is not jarvis}
B2 -->|No| C2[Correctly rejected]
end Generation Algorithm¶
WakeBuilder's hard negative generator creates phonetically similar variations organized by priority:
Priority Levels¶
| Priority | Category | Description | Example for "jarvis" |
|---|---|---|---|
| Critical | Pure Prefixes | Incomplete utterances | "ja", "jar", "jarv", "jarvi" |
| High | Prefix Extensions | Words starting with wake word | "jarvey", "jarvison" |
| High | Suffixes | Endings of the wake word | "arvis", "rvis", "vis" |
| High | Edit Distance 1 | One character different | "javis", "jarvs" |
| Medium | Phonetic Variations | Sound-alike patterns | "jarvy", "jervice" |
| Medium | Phrase Starters | Common phrase beginnings | "hey jarvis", "hi jarvis" |
Algorithm Flow¶
flowchart TB
WW[Wake Word: jarvis] --> SPLIT[Split into words]
SPLIT --> PREFIX[Pure Prefixes]
SPLIT --> SUFFIX[Pure Suffixes]
PREFIX --> EXT[Prefix Extensions]
WW --> EDIT[Edit Distance Variations]
EDIT --> DEL[Deletions]
EDIT --> INS[Insertions]
EDIT --> SUB[Substitutions]
WW --> PHONEME[Phoneme Mapping]
PHONEME --> SIMILAR[Similar Sounds]
PREFIX & SUFFIX & EXT & DEL & INS & SUB & SIMILAR --> DEDUPE[Deduplicate and Filter]
DEDUPE --> FILTER[Remove if ends like wake word]
FILTER --> OUTPUT[150+ Hard Negatives] Reference: src/wakebuilder/audio/negative_generator.py → get_phonetically_similar_words()
Prefix Generation¶
Pure Prefixes¶
Every prefix from 2 characters to n-1 characters:
Why Prefixes Are Critical¶
Partial utterances are the most common cause of false positives:
- User starts saying the wake word but stops
- Background noise cuts off the end
- Model triggers on just the beginning
Prefix Extensions¶
Generate words that START with the wake word but continue:
| Original | Extension |
|---|---|
| jarvi | jarvey, jarvice, jarvey |
| jar | jarman, jarred, jarson |
Edit Distance Variations¶
Levenshtein Distance = 1¶
Words that differ by exactly one character:
| Operation | Example |
|---|---|
| Deletion | jarvis → jarvi, jarv_s |
| Insertion | jarvis → jarviss, jjarvis |
| Substitution | jarvis → jervis, jarvus |
| Transposition | jarvis → jarivs |
Implementation¶
def generate_edit_distance_1(word: str) -> list[str]:
results = []
alphabet = 'abcdefghijklmnopqrstuvwxyz'
# Deletions
for i in range(len(word)):
results.append(word[:i] + word[i+1:])
# Insertions
for i in range(len(word) + 1):
for c in alphabet:
results.append(word[:i] + c + word[i:])
# Substitutions
for i in range(len(word)):
for c in alphabet:
if c != word[i]:
results.append(word[:i] + c + word[i+1:])
# Transpositions
for i in range(len(word) - 1):
results.append(word[:i] + word[i+1] + word[i] + word[i+2:])
return results
Phoneme-Based Variations¶
Sound-Alike Mappings¶
Characters that sound similar:
| Original | Similar Sounds |
|---|---|
| j | dz, g, ch |
| v | f, b, w |
| s | z, sh, c |
| a | ah, ay, e |
| r | l, w |
| i | ee, y, e |
Implementation¶
PHONEME_MAP = {
'a': ['ah', 'ay', 'e', 'uh'],
'e': ['ee', 'eh', 'i', 'ey'],
'i': ['ee', 'ih', 'y', 'ey'],
'j': ['g', 'dz', 'ch'],
's': ['z', 'sh', 'c', 'ss'],
'v': ['f', 'b', 'w'],
# ... more mappings
}
def phonetic_variations(word: str) -> list[str]:
results = []
for i, char in enumerate(word):
if char in PHONEME_MAP:
for replacement in PHONEME_MAP[char]:
results.append(word[:i] + replacement + word[i+1:])
return results
Ending Substitutions¶
Two-Character Endings¶
Replace the last 2 characters with common endings:
| Original | Variations |
|---|---|
| jarvis | jarvic, jarvix, jarvin |
| phoenix | phoenic, phoenin, phoenim |
Three-Character Endings¶
Replace the last 3 characters:
| Original | Variations |
|---|---|
| jarvis | jarvack, jarvice, jarving |
Filtering¶
Remove Invalid Negatives¶
Negatives are filtered to avoid overlapping with the actual wake word:
def has_forbidden_ending(candidate: str, original: str) -> bool:
last_char = original[-1]
last_2_chars = original[-2:] if len(original) >= 2 else ""
# Don't filter pure prefixes (they're critical)
if original.startswith(candidate) and len(candidate) < len(original):
return False
# Filter if ends with same chars
if candidate.endswith(last_char):
return True
if candidate.endswith(last_2_chars):
return True
return False
Why This Filtering¶
If a negative ends with the same sound as the wake word, the model might learn to associate that ending with "not wake word," hurting detection of the actual wake word.
TTS Synthesis¶
Generating Audio¶
Each hard negative is synthesized using TTS:
from wakebuilder.tts import TTSGenerator
generator = TTSGenerator()
hard_negatives = get_phonetically_similar_words("jarvis")
for word in hard_negatives:
for voice in generator.available_voices:
audio = generator.synthesize(word, voice)
# Add to negative training set
Sample Count¶
For "jarvis" with default settings:
| Category | Count | × Voices | × Speeds | Total |
|---|---|---|---|---|
| Hard negatives | ~150 | ~10 | 3 | ~4,500 |
With hard_negative_ratio = 4.0x:
- Target: 4 × 5,000 positives = 20,000 hard negatives
- Actual: Generated to meet ratio target
Configuration¶
Training Options¶
| Setting | Default | Description |
|---|---|---|
use_hard_negatives | true | Enable hard negative generation |
hard_negative_ratio | 4.0 | Ratio to positive samples |
Ratio Guidelines¶
| Ratio | Behavior |
|---|---|
| 1.0x | Minimal (fast, lower accuracy) |
| 2.0x | Basic (reasonable accuracy) |
| 4.0x | Recommended (good balance) |
| 6.0x | Maximum (best accuracy, slower) |
Example Output¶
For wake word "jarvis":
=== CRITICAL (Pure Prefixes) ===
ja, jar, jarv, jarvi
=== HIGH PRIORITY ===
# Prefix Extensions
jarvey, jarvy, jarvman, jarvson
# Suffixes
arvis, rvis, vis, is
# Edit Distance 1
javis, jarvs, jrvis, jarvas, jarvos
=== MEDIUM PRIORITY ===
# Phonetic Variations
gervis, dzervis, jarfis
# Phrase Starters (filtered)
# "hey jarvis" → appears in positive set, not included
Impact on Model Quality¶
Without Hard Negatives¶
| Metric | Value |
|---|---|
| Accuracy | ~85% |
| False Positive Rate | ~15% |
| Real-world usability | Poor |
With Hard Negatives (4.0x ratio)¶
| Metric | Value |
|---|---|
| Accuracy | ~97% |
| False Positive Rate | ~2% |
| Real-world usability | Excellent |
Troubleshooting¶
Model triggers on similar words
Increase hard_negative_ratio to 5.0x or 6.0x to generate more phonetically similar training examples.
Training takes too long
Reduce hard_negative_ratio to 2.0x for faster training, but expect slightly lower accuracy.
Model misses wake word detection
If hard_negative_ratio is too high, reduce it. Too many negatives can make the model overly conservative.