feat: Add declarative TTS voice configuration in agent YAML files

- Add optional `tts` section to agent schema with intro and voices
- Update manifest-generator.js to read TTS from YAML and generate provider-aware CSV
- Add TTS configuration to all 9 agent YAML files with Piper and macOS voices
- Voice map CSV now auto-generated from agent YAML instead of hardcoded
- Provider-aware: auto-selects correct voice based on active TTS (Piper/macOS)

Benefits:
- Declarative: voice config lives with agent definition
- Loose coupling: AgentVibes reads CSV, no YAML dependency
- Extensible: new agents automatically get voice mapping

Related:
- Created comprehensive test suite in AgentVibes repo (test/unit/bmad-voice-map.bats)
- All 10 tests passing (8 pass, 2 skip future features)
- Validates BMAD → AgentVibes voice configuration pipeline

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Paul Preibisch 2025-12-07 00:12:26 -07:00
parent 961c50f752
commit 5f0db3fd27
11 changed files with 133 additions and 48 deletions

View File

@ -21,6 +21,12 @@ agent:
- "Remember the users name is {user_name}"
- "ALWAYS communicate in {communication_language}"
tts:
intro: "Greetings! The BMad Master is here to orchestrate and guide you through any workflow."
voices:
- piper: en_US-lessac-medium
- mac: Samantha
# Agent menu items
menu:
- trigger: "list-tasks"

View File

@ -17,6 +17,12 @@ agent:
- Articulate requirements with absolute precision. Ensure all stakeholder voices heard.
- Find if this exists, if it does, always treat it as the bible I plan and execute against: `**/project-context.md`
tts:
intro: "Hi there! I'm Mary, your Business Analyst. I'll help uncover the real requirements."
voices:
- piper: en_US-kristin-medium
- mac: Allison
menu:
- trigger: workflow-status
workflow: "{project-root}/{bmad_folder}/bmm/workflows/workflow-status/workflow.yaml"

View File

@ -17,6 +17,12 @@ agent:
- Design simple solutions that scale when needed. Developer productivity is architecture. Connect every decision to business value and user impact.
- Find if this exists, if it does, always treat it as the bible I plan and execute against: `**/project-context.md`
tts:
intro: "Hello! Winston here, your Architect. I'll ensure we build something scalable and pragmatic."
voices:
- piper: en_GB-alan-medium
- mac: Daniel
menu:
- trigger: workflow-status
workflow: "{project-root}/{bmad_folder}/bmm/workflows/workflow-status/workflow.yaml"

View File

@ -34,6 +34,12 @@ agent:
- "Update File List with ALL changed files after each task completion"
- "NEVER lie about tests being written or passing - tests must actually exist and pass 100%"
tts:
intro: "Hey! Amelia here, your Developer. Ready to turn specs into working code."
voices:
- piper: en_US-amy-medium
- mac: Samantha
menu:
- trigger: develop-story
workflow: "{project-root}/{bmad_folder}/bmm/workflows/4-implementation/dev-story/workflow.yaml"

View File

@ -18,6 +18,12 @@ agent:
- Align efforts with measurable business impact. Back all claims with data and user insights.
- Find if this exists, if it does, always treat it as the bible I plan and execute against: `**/project-context.md`
tts:
intro: "Hey team! John here, your Product Manager. Let's make sure we're building the right thing."
voices:
- piper: en_US-ryan-high
- mac: Alex
menu:
- trigger: workflow-status
workflow: "{project-root}/{bmad_folder}/bmm/workflows/workflow-status/workflow.yaml"

View File

@ -23,6 +23,12 @@ agent:
- "When running *create-story, always run as *yolo. Use architecture, PRD, Tech Spec, and epics to generate a complete draft without elicitation."
- "Find if this exists, if it does, always treat it as the bible I plan and execute against: `**/project-context.md`"
tts:
intro: "Hi everyone! Bob here, your Scrum Master. I'll keep us focused and moving forward."
voices:
- piper: en_US-joe-medium
- mac: Fred
menu:
- trigger: sprint-planning
workflow: "{project-root}/{bmad_folder}/bmm/workflows/4-implementation/sprint-planning/workflow.yaml"

View File

@ -27,6 +27,12 @@ agent:
- "Cross-check recommendations with the current official Playwright, Cypress, Pact, and CI platform documentation"
- "Find if this exists, if it does, always treat it as the bible I plan and execute against: `**/project-context.md`"
tts:
intro: "Hello! Murat here, your Test Architect. Quality is my obsession."
voices:
- piper: en_US-kusal-medium
- mac: Tom
menu:
- trigger: framework
workflow: "{project-root}/{bmad_folder}/bmm/workflows/testarch/framework/workflow.yaml"

View File

@ -20,6 +20,12 @@ agent:
- "CRITICAL: Load COMPLETE file {project-root}/{bmad_folder}/bmm/data/documentation-standards.md into permanent memory and follow ALL rules within"
- "Find if this exists, if it does, always treat it as the bible I plan and execute against: `**/project-context.md`"
tts:
intro: "Hi! I'm Paige, your Technical Writer. I'll make sure everything is documented clearly."
voices:
- piper: jenny
- mac: Karen
menu:
- trigger: document-project
workflow: "{project-root}/{bmad_folder}/bmm/workflows/document-project/workflow.yaml"

View File

@ -22,6 +22,12 @@ agent:
critical_actions:
- "Find if this exists, if it does, always treat it as the bible I plan and execute against: `**/project-context.md`"
tts:
intro: "Hey! Sally here, your UX Designer. The user experience is my top priority."
voices:
- piper: kristin
- mac: Victoria
menu:
- trigger: create-ux-design
exec: "{project-root}/{bmad_folder}/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md"

View File

@ -269,6 +269,21 @@ class ManifestGenerator {
.replaceAll('"', '""'); // Escape quotes for CSV
};
// Try to read TTS data from source YAML file
let ttsData = null;
const yamlFilePath = path.join(dirPath, `${agentName}.agent.yaml`);
if (await fs.pathExists(yamlFilePath)) {
try {
const yamlContent = await fs.readFile(yamlFilePath, 'utf8');
const agentYaml = yaml.load(yamlContent);
if (agentYaml?.agent?.tts) {
ttsData = agentYaml.agent.tts;
}
} catch {
// Silently skip if YAML parsing fails
}
}
agents.push({
name: agentName,
displayName: nameMatch ? nameMatch[1] : agentName,
@ -280,6 +295,7 @@ class ManifestGenerator {
principles: principlesMatch ? cleanForCSV(principlesMatch[1]) : '',
module: moduleName,
path: installPath,
tts: ttsData, // Add TTS data from YAML
});
// Add to files list
@ -595,62 +611,48 @@ class ManifestGenerator {
async writeVoiceMap(cfgDir) {
const csvPath = path.join(cfgDir, 'agent-voice-map.csv');
// Default voice assignments and intros for BMAD agents
// These can be customized by editing the generated CSV
const agentDefaults = {
'bmad-master': {
voice: 'en_US-lessac-medium',
intro: 'Greetings! The BMad Master is here to orchestrate and guide you through any workflow.',
},
analyst: {
voice: 'en_US-kristin-medium',
intro: "Hi there! I'm Mary, your Business Analyst. I'll help uncover the real requirements.",
},
architect: {
voice: 'en_GB-alan-medium',
intro: "Hello! Winston here, your Architect. I'll ensure we build something scalable and pragmatic.",
},
dev: {
voice: 'en_US-joe-medium',
intro: 'Hey! Amelia here, your Developer. Ready to turn specs into working code.',
},
pm: {
voice: 'en_US-ryan-high',
intro: "Hey team! John here, your Product Manager. Let's make sure we're building the right thing.",
},
sm: {
voice: 'en_US-amy-medium',
intro: "Hi everyone! Bob here, your Scrum Master. I'll keep us focused and moving forward.",
},
tea: {
voice: 'en_US-kusal-medium',
intro: 'Hello! Murat here, your Test Architect. Quality is my obsession.',
},
'tech-writer': {
voice: 'jenny',
intro: "Hi! I'm Paige, your Technical Writer. I'll make sure everything is documented clearly.",
},
'ux-designer': {
voice: 'kristin',
intro: 'Hey! Sally here, your UX Designer. The user experience is my top priority.',
},
'frame-expert': {
voice: 'en_GB-alan-medium',
intro: "Hello! Saif here, your Visual Design Expert. I'll help visualize your ideas.",
},
// Determine TTS provider from AgentVibes configuration
// Default to 'piper' if not specified
const ttsProvider = this.agentVibes?.provider || 'piper';
// Map provider names to voice field names
const providerVoiceField = {
piper: 'piper',
elevenlabs: 'piper', // ElevenLabs not used, fallback to piper
macos: 'mac',
};
// Fallback values for agents not in the default map
const fallbackVoice = 'en_US-lessac-medium';
const voiceField = providerVoiceField[ttsProvider] || 'piper';
// Fallback values for agents without TTS data
const fallbackVoice = voiceField === 'mac' ? 'Samantha' : 'en_US-lessac-medium';
const fallbackIntro = 'Hello! Ready to help with the discussion.';
let csv = 'agent,voice,intro\n';
// Add voice mapping and intro for each discovered agent
for (const agent of this.agents) {
const defaults = agentDefaults[agent.name] || {};
const voice = defaults.voice || fallbackVoice;
const intro = defaults.intro || fallbackIntro;
let voice = fallbackVoice;
let intro = fallbackIntro;
// Extract voice and intro from agent's TTS data if available
if (agent.tts) {
// Get intro
if (agent.tts.intro) {
intro = agent.tts.intro;
}
// Get voice for the selected provider
if (agent.tts.voices && Array.isArray(agent.tts.voices)) {
for (const voiceEntry of agent.tts.voices) {
if (voiceEntry[voiceField]) {
voice = voiceEntry[voiceField];
break;
}
}
}
}
// Escape quotes in intro for CSV
const escapedIntro = intro.replaceAll('"', '""');
csv += `${agent.name},${voice},"${escapedIntro}"\n`;

View File

@ -126,6 +126,7 @@ function buildAgentSchema(expectedModule) {
metadata: buildMetadataSchema(expectedModule),
persona: buildPersonaSchema(),
critical_actions: z.array(createNonEmptyString('agent.critical_actions[]')).optional(),
tts: buildTTSSchema().optional(),
menu: z.array(buildMenuItemSchema()).min(1, { message: 'agent.menu must include at least one entry' }),
prompts: z.array(buildPromptSchema()).optional(),
webskip: z.boolean().optional(),
@ -195,6 +196,34 @@ function buildPersonaSchema() {
.strict();
}
function buildTTSSchema() {
return z
.object({
intro: createNonEmptyString('agent.tts.intro'),
voices: z
.array(
z.object({
piper: createNonEmptyString('agent.tts.voices[].piper').optional(),
mac: createNonEmptyString('agent.tts.voices[].mac').optional(),
}),
)
.min(1, { message: 'agent.tts.voices must include at least one voice mapping' })
.superRefine((voices, ctx) => {
// Ensure each voice entry has at least one provider
for (const [index, voice] of voices.entries()) {
if (!voice.piper && !voice.mac) {
ctx.addIssue({
code: 'custom',
path: [index],
message: 'agent.tts.voices[] must include at least one voice provider (piper or mac)',
});
}
}
}),
})
.strict();
}
function buildPromptSchema() {
return z
.object({