UNPKG

cloudonix-js

Version:

A JavaScript library for building and serving Cloudonix Voice Application XML (CXML) documents with support for AI voice agents, media streaming, and advanced telephony features

429 lines (377 loc) 12.5 kB
/** * @file Example demonstrating a complex flow with nested Record, Play, and Gather verbs * @copyright 2025 Nir Simionovich, nirs@cloudonix.io * @license MIT */ 'use strict'; const { CXMLBuilder, CXMLServer } = require('../'); /** * Creates a record-and-confirm flow with nested verbs * @returns {CXMLBuilder} - CXML builder with the recording flow */ function createRecordAndConfirmFlow() { return new CXMLBuilder() .createResponse() // Introduction .addSay("Welcome to our voice message system.", { voice: "Google:en-US-Neural2-F", language: "en-US" }) .addPause(1) // Instruction before recording .addSay("Please record your message after the beep. Press pound when finished.", { voice: "Google:en-US-Neural2-F" }) // Record the message .addRecord({ action: "/recording-handler", method: "POST", maxLength: 60, finishOnKey: "#", playBeep: true, timeout: 5, transcribe: true, transcribeCallback: "/transcription-handler", recordingStatusCallback: "/recording-status", recordingStatusCallbackMethod: "POST", trim: "trim-silence" }); } /** * Creates a playback and confirmation flow * @param {string} recordingUrl - URL of the recording to play * @returns {CXMLBuilder} - CXML builder with the playback flow */ function createPlaybackAndConfirmFlow(recordingUrl = "http://example.com/recordings/sample.mp3") { return new CXMLBuilder() .createResponse() // Transition message .addSay("Thank you. Here's a playback of your recording:", { voice: "Google:en-US-Neural2-F" }) .addPause(1) // Play back the recording .addPlay(recordingUrl, { loop: 1 }) // Gather confirmation .addGather({ input: "dtmf speech", timeout: 5, numDigits: 1, action: "/confirmation-handler", method: "POST", speechTimeout: "auto", speechEngine: "google", language: "en-US", hints: "yes,no,correct,incorrect,save,discard" }) .addSay("Is this recording correct?", { voice: "Google:en-US-Neural2-F" }) .addPause(0.5) .addSay("Press 1 or say 'yes' to save the recording.", { voice: "Google:en-US-Neural2-F" }) .addPause(0.3) .addSay("Press 2 or say 'no' to discard and try again.", { voice: "Google:en-US-Neural2-F" }) .done() // Default action if no input is received .addRedirect("/save-recording"); } /** * Creates a message saved confirmation * @returns {CXMLBuilder} - CXML builder with the save confirmation */ function createSaveConfirmation() { return new CXMLBuilder() .createResponse() .addSay("Your message has been saved successfully.", { voice: "Google:en-US-Neural2-F" }) .addPause(0.5) .addSay("Thank you for using our service. Goodbye!", { voice: "Google:en-US-Neural2-F" }) .addHangup(); } /** * Creates a discard and retry flow * @returns {CXMLBuilder} - CXML builder with the retry flow */ function createRetryFlow() { return new CXMLBuilder() .createResponse() .addSay("Your message has been discarded.", { voice: "Google:en-US-Neural2-F" }) .addPause(0.5) .addSay("Let's try again.", { voice: "Google:en-US-Neural2-F" }) .addRedirect("/record-message"); } // Run as standalone script if invoked directly if (require.main === module) { // If running as a script, just output the XML const fullFlow = new CXMLBuilder() .createResponse() // Introduction .addSay("Welcome to our voice message system.", { voice: "Google:en-US-Neural2-F", language: "en-US" }) .addPause(1) // Instruction before recording .addSay("Please record your message after the beep. Press pound when finished.", { voice: "Google:en-US-Neural2-F" }) // Record the message .addRecord({ action: "/recording-handler", method: "POST", maxLength: 60, finishOnKey: "#", playBeep: true, timeout: 5, transcribe: true, transcribeCallback: "/transcription-handler", recordingStatusCallback: "/recording-status", recordingStatusCallbackMethod: "POST", trim: "trim-silence" }) // Transition message .addSay("Thank you. Here's a playback of your recording:", { voice: "Google:en-US-Neural2-F" }) .addPause(1) // Play back the recording .addPlay("http://example.com/recordings/sample.mp3", { loop: 1 }) // Gather confirmation .addGather({ input: "dtmf speech", timeout: 5, numDigits: 1, action: "/confirmation-handler", method: "POST", speechTimeout: "auto", speechEngine: "google", language: "en-US", hints: "yes,no,correct,incorrect,save,discard" }) .addSay("Is this recording correct?", { voice: "Google:en-US-Neural2-F" }) .addPause(0.5) .addSay("Press 1 or say 'yes' to save the recording.", { voice: "Google:en-US-Neural2-F" }) .addPause(0.3) .addSay("Press 2 or say 'no' to discard and try again.", { voice: "Google:en-US-Neural2-F" }) .done() // Default action if no input is received .addRedirect("/save-recording") .build(); console.log(fullFlow); // Run the server if the --server flag is passed if (process.argv.includes('--server')) { runServer(); } } else { // Export the functions for use in other modules module.exports = { createRecordAndConfirmFlow, createPlaybackAndConfirmFlow, createSaveConfirmation, createRetryFlow }; } /** * Runs a CXML server with the nested Record, Play, Gather example */ function runServer() { const PORT = process.env.PORT || 3006; // Create a new CXML server const server = new CXMLServer({ port: PORT, verbose: true }); // Mock recording URL - in a real app, this would be dynamic let recordingUrl = "http://example.com/recordings/sample.mp3"; // Start the flow server.route('/record-message', async (context) => { return createRecordAndConfirmFlow(); }); // Handle recording completion server.route('/recording-handler', async (context) => { console.log('Recording completed:'); if (context.body && typeof context.body === 'object') { console.log(JSON.stringify(context.body, null, 2)); // In a real app, we would get the recording URL from the request if (context.body.RecordingUrl) { recordingUrl = context.body.RecordingUrl; } } else { console.log(context.body); } // Continue to playback and confirmation return createPlaybackAndConfirmFlow(recordingUrl); }); // Handle recording status updates server.route('/recording-status', async (context) => { console.log('Recording status update:'); if (context.body && typeof context.body === 'object') { console.log(JSON.stringify(context.body, null, 2)); } else { console.log(context.body); } return new CXMLBuilder().createResponse(); }); // Handle transcription callback server.route('/transcription-handler', async (context) => { console.log('Transcription received:'); if (context.body && typeof context.body === 'object') { console.log(JSON.stringify(context.body, null, 2)); } else { console.log(context.body); } return new CXMLBuilder().createResponse(); }); // Handle confirmation input server.route('/confirmation-handler', async (context) => { console.log('Confirmation input received:'); if (context.body && typeof context.body === 'object') { console.log(JSON.stringify(context.body, null, 2)); } else { console.log(context.body); } let choice = ''; // Extract input from the request body if (typeof context.body === 'object') { if (context.body.Digits) { choice = context.body.Digits; } else if (context.body.SpeechResult || context.body.speech_result) { // For speech, extract yes/no const speech = (context.body.SpeechResult || context.body.speech_result || '').toLowerCase(); if (speech.includes('yes') || speech.includes('correct') || speech.includes('save')) { choice = '1'; } else if (speech.includes('no') || speech.includes('incorrect') || speech.includes('discard')) { choice = '2'; } } } else if (typeof context.body === 'string') { // Try to parse form data or JSON if (context.body.includes('Digits=')) { const match = context.body.match(/Digits=(\d+)/); if (match) choice = match[1]; } } // Route based on the confirmation choice if (choice === '1') { return new CXMLBuilder() .createResponse() .addRedirect('/save-recording'); } else if (choice === '2') { return new CXMLBuilder() .createResponse() .addRedirect('/discard-recording'); } else { return new CXMLBuilder() .createResponse() .addSay("I didn't understand your choice. Let's try again.", { voice: "Google:en-US-Neural2-F" }) .addRedirect('/recording-handler'); } }); // Handle save recording server.route('/save-recording', async (context) => { return createSaveConfirmation(); }); // Handle discard and retry server.route('/discard-recording', async (context) => { return createRetryFlow(); }); // Home page with explanation server.route('/', async (context) => { // Return a simple HTML page for browser navigation context.setHeader('Content-Type', 'text/html; charset=utf-8'); return ` <!DOCTYPE html> <html> <head> <title>Nested Record, Play, Gather Example</title> <style> body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; } h1 { color: #333; } a { display: block; margin: 10px 0; padding: 10px; background: #f4f4f4; text-decoration: none; color: #333; border-radius: 4px; } a:hover { background: #e0e0e0; } pre { background: #f8f8f8; padding: 10px; border-radius: 4px; overflow-x: auto; } .section { margin-bottom: 30px; } .flow { margin: 20px 0; padding: 15px; background: #f5f5f5; border-radius: 5px; } .step { margin: 10px 0 10px 20px; } </style> </head> <body> <h1>Nested Record, Play, Gather Example</h1> <p>This example demonstrates a complex voice application flow that:</p> <div class="flow"> <div class="step">1. Records a message from the caller</div> <div class="step">2. Plays back the recording</div> <div class="step">3. Gathers confirmation from the user</div> <div class="step">4. Redirects to different handlers based on input</div> </div> <p>Click the link below to try the flow:</p> <a href="/record-message">Start Recording Flow</a> <h2>Flow Details</h2> <p>This example shows how to chain multiple CXML verbs together to create a complete voice application flow.</p> <h3>Code Example:</h3> <pre> .addRecord({ action: "/recording-handler", method: "POST", maxLength: 60, finishOnKey: "#", playBeep: true, timeout: 5, transcribe: true }) // Later, after receiving the recording... .addPlay(recordingUrl) // Then get confirmation .addGather({ input: "dtmf speech", numDigits: 1, action: "/confirmation-handler" }) .addSay("Is this recording correct?") .done() </pre> <p>In a real telephony application, this flow would allow callers to record messages, review them, and confirm or re-record as needed.</p> </body> </html> `; }); // Start the server async function startServer() { try { await server.start(); console.log(`Nested Record Example server running at http://localhost:${PORT}/`); console.log('Open the above URL in your browser to see the flow'); } catch (error) { console.error('Failed to start server:', error); process.exit(1); } } startServer(); } // For running with --server flag from command line if (process.argv.includes('--server')) { runServer(); }