Build voice AI training platforms with Hume EVI, Twilio, and tRPC. Use when creating roleplay systems, voice call integrations, phone-based training apps, or conversational AI platforms with emotional intelligence.
Build production-ready voice AI training platforms with Hume EVI (Empathic Voice Interface), Twilio phone integration, and modern web stack (React + tRPC + Drizzle ORM).
Frontend (React + tRPC)
↓
Backend (Express + tRPC + Drizzle)
↓
Voice AI (Hume EVI) + Phone (Twilio)
↓
Database (MySQL/TiDB) - Session & conversation tracking
1. Add Hume AI Credentials
Use webdev_request_secrets to add:
HUME_API_KEY - API key from Hume dashboardHUME_SECRET_KEY - Secret key for access token generationHUME_FEMALE_CONFIG_ID - Female voice configuration UUIDHUME_MALE_CONFIG_ID - Male voice configuration UUIDHUME_WEBHOOK_URL - Webhook endpoint for call events (optional)2. Add Twilio Credentials (if phone integration needed)
TWILIO_ACCOUNT_SID - Account SID from Twilio consoleTWILIO_AUTH_TOKEN - Auth token from Twilio consoleTWILIO_PHONE_NUMBER - Purchased phone number in E.164 format (+15551234567)3. Update server environment config
Add to server/_core/env.ts:
export const ENV = {
// ... existing vars
humeApiKey: process.env.HUME_API_KEY!,
humeSecretKey: process.env.HUME_SECRET_KEY!,
humeFemaleConfigId: process.env.HUME_FEMALE_CONFIG_ID!,
humeMaleConfigId: process.env.HUME_MALE_CONFIG_ID!,
humeWebhookUrl: process.env.HUME_WEBHOOK_URL,
twilioAccountSid: process.env.TWILIO_ACCOUNT_SID,
twilioAuthToken: process.env.TWILIO_AUTH_TOKEN,
twilioPhoneNumber: process.env.TWILIO_PHONE_NUMBER,
};
Core tables needed:
Sessions table - Practice/training sessions
Conversation turns table - Message-by-message tracking
User progress table - Aggregate stats
Scripts table (if using predefined content)
Example schema (Drizzle ORM):
export const practiceSessions = mysqlTable('practice_sessions', {
id: int('id').primaryKey().autoincrement(),
userId: int('user_id').notNull(),
userCode: varchar('user_code', { length: 50 }),
archetype: varchar('archetype', { length: 50 }).notNull(),
difficulty: varchar('difficulty', { length: 50 }).notNull(),
practiceMode: varchar('practice_mode', { length: 50 }).notNull(),
voiceGender: varchar('voice_gender', { length: 10 }).notNull(),
roleplayMode: varchar('roleplay_mode', { length: 20 }).default('standard'),
callType: varchar('call_type', { length: 20 }).default('web'),
userPhoneNumber: varchar('user_phone_number', { length: 20 }),
twilioCallSid: varchar('twilio_call_sid', { length: 100 }),
status: varchar('status', { length: 20 }).notNull(),
score: int('score'),
feedback: text('feedback'),
duration: int('duration'),
createdAt: timestamp('created_at').defaultNow(),
});
Install Hume SDK:
pnpm add hume
Create server/hume-service.ts:
Key functions:
getHumeAccessToken() - Generate access token for clientgenerateSystemPrompt() - Dynamic prompt based on scenario/difficulty/modeprepareHumeSession() - Return configId + systemPromptgetConfigId() - Select male/female voice configSystem prompt pattern:
function generateSystemPrompt(params: {
archetype: string;
difficulty: string;
practiceMode: string;
roleplayMode: 'standard' | 'reverse';
scriptContent?: string;
}): string {
if (params.roleplayMode === 'reverse') {
// AI plays expert sales rep, user plays homeowner
return `You are an expert door-to-door fiber sales representative...`;
}
// Standard: AI plays homeowner, user plays sales rep
return `You are a ${params.archetype} homeowner. ${getPersonalityTraits(params.archetype)}...`;
}
Install Twilio SDK:
pnpm add twilio
Create server/twilio-service.ts:
Key functions:
getTwilioPhoneNumber() - Return configured phone numberinitiateOutboundCall() - Call user's phone number with Hume configOutbound call pattern:
export async function initiateOutboundCall(params: {
toNumber: string;
configId: string;
apiKey: string;
}): Promise<{ success: boolean; callSid?: string; error?: string }> {
const client = twilio(ENV.twilioAccountSid, ENV.twilioAuthToken);
const call = await client.calls.create({
to: params.toNumber,
from: ENV.twilioPhoneNumber,
twiml: `<Response><Connect><Stream url="wss://api.hume.ai/v0/evi/chat?config_id=${params.configId}&api_key=${params.apiKey}" /></Connect></Response>`,
});
return { success: true, callSid: call.sid };
}
Create server/practice-router.ts (or similar):
Essential endpoints:
startSession - Create session, return sessionIdgetHumeToken - Get access token for web voice callsprepareHumeSession - Get configId + systemPromptgetTwilioPhoneNumber - Return phone number for call-ininitiatePhoneCall - Trigger outbound callupdateSession - Update score/feedback/durationgetMySessions - List user's sessionsgetSession - Get single session detailsgetConversation - Get message-by-message transcriptKey pattern - session creation:
startSession: protectedProcedure
.input(z.object({
scriptId: z.number().optional(),
userCode: z.string().optional(),
archetype: z.enum(['type1', 'type2', 'type3']),
difficulty: z.enum(['easy', 'medium', 'hard']),
practiceMode: z.enum(['mode1', 'mode2']),
voiceGender: z.enum(['male', 'female']),
}))
.mutation(async ({ ctx, input }) => {
const result = await createPracticeSession({
userId: ctx.user.id,
...input,
status: 'in_progress',
});
return { sessionId: result.insertId };
})
Create client/src/hooks/useHumeVoice.ts:
Manages WebSocket connection to Hume EVI:
Pattern:
export function useHumeVoice(sessionId: number | null) {
const [status, setStatus] = useState<'idle' | 'connecting' | 'connected' | 'disconnected'>('idle');
const [messages, setMessages] = useState<Message[]>([]);
const connect = async () => {
const token = await trpc.practice.getHumeToken.query();
const config = await trpc.practice.prepareHumeSession.query({ sessionId });
// WebSocket connection to Hume EVI
const ws = new WebSocket(`wss://api.hume.ai/v0/evi/chat?access_token=${token}&config_id=${config.configId}`);
// ... handle audio streaming
};
return { status, messages, connect, disconnect };
}
Create client/src/components/VoiceCall.tsx:
Features:
Integration with useHumeVoice hook:
export function VoiceCall({ sessionId }: { sessionId: number }) {
const { status, messages, connect, disconnect } = useHumeVoice(sessionId);
return (
<Card>
<CardHeader>
<CardTitle>Practice Session</CardTitle>
<Badge>{status}</Badge>
</CardHeader>
<CardContent>
<div className="space-y-4">
{messages.map((msg, i) => (
<div key={i} className={msg.speaker === 'user' ? 'text-right' : 'text-left'}>
<p>{msg.content}</p>
</div>
))}
</div>
<Button onClick={status === 'connected' ? disconnect : connect}>
{status === 'connected' ? 'End Call' : 'Start Call'}
</Button>
</CardContent>
</Card>
);
}
Create client/src/pages/Practice.tsx:
Configuration sections:
Phone integration UI:
// Call method selection
<RadioGroup value={callMethod} onValueChange={setCallMethod}>
<RadioGroupItem value="web">Web Browser</RadioGroupItem>
<RadioGroupItem value="phone_inbound">
Call In: {twilioPhone?.phoneNumber}
</RadioGroupItem>
<RadioGroupItem value="phone_outbound">
Receive Call
</RadioGroupItem>
</RadioGroup>
// Phone number input (for outbound)
{callMethod === 'phone_outbound' && (
<Input
placeholder="+15551234567"
value={userPhoneNumber}
onChange={(e) => setUserPhoneNumber(e.target.value)}
/>
)}
Create client/src/pages/History.tsx:
Display:
Create client/src/pages/Analytics.tsx:
Aggregate stats:
1. Credential validation tests:
// server/hume.credentials.test.ts
test('Hume credentials are valid', async () => {
const token = await getHumeAccessToken();
expect(token).toBeTruthy();
});
2. Integration tests:
// server/phone-integration.test.ts
test('System prompt includes roleplay mode', () => {
const prompt = generateSystemPrompt({ roleplayMode: 'reverse', ... });
expect(prompt).toContain('expert sales representative');
});
3. End-to-end flow:
Allow users to practice by playing the "customer" role while AI demonstrates expert responses:
if (roleplayMode === 'reverse') {
systemPrompt = `You are an expert ${profession}. The user will play the role of ${customerType} and present objections or scenarios. Demonstrate how to handle them professionally...`;
} else {
systemPrompt = `You are a ${customerType}. The user is a ${profession} trying to ${goal}...`;
}
Enable managers to track individual users without requiring login:
// Auto-fill from profile or manual entry
const repCode = user?.repCode || manualRepCode || undefined;
// Store with session
await createPracticeSession({
userId: ctx.user.id,
userCode: repCode,
...otherParams
});
// Filter in manager dashboard
const sessions = await getUserSessions({ userCode: 'REP-1234' });
Adjust AI behavior based on difficulty:
const difficultyModifiers = {
rookie: 'Be friendly and receptive. Show clear buying signals.',
intermediate: 'Be neutral. Require some persuasion.',
expert: 'Be skeptical. Present common objections.',
nightmare: 'Be hostile and dismissive. Maximum difficulty.',
};
systemPrompt += ` ${difficultyModifiers[difficulty]}`;
webdev_request_secretspnpm db:push)Hume 401 errors:
HUME_API_KEY and HUME_SECRET_KEY are correctTwilio call failures:
TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKENWebSocket connection issues:
No audio in calls:
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer