@@ -3,6 +3,13 @@ const twilio = require('twilio');
33const { v4 : uuidv4 } = require ( 'uuid' ) ;
44const path = require ( 'path' ) ;
55const dotenv = require ( 'dotenv' ) ;
6+ const {
7+ DEFAULT_TEXT_MODEL : BASE_TEXT_MODEL ,
8+ DEFAULT_TTS_MODEL : BASE_TTS_MODEL ,
9+ createTtsUrl,
10+ buildOpenAiUrl,
11+ createOpenAiPayload
12+ } = require ( './pollinations-utils' ) ;
613
714dotenv . config ( { path : path . resolve ( __dirname , '.env' ) } ) ;
815
@@ -17,6 +24,8 @@ const TWILIO_ACCOUNT_SID = process.env.TWILIO_ACCOUNT_SID;
1724const TWILIO_AUTH_TOKEN = process . env . TWILIO_AUTH_TOKEN ;
1825const TWILIO_PHONE_NUMBER = process . env . TWILIO_PHONE_NUMBER ;
1926const DEFAULT_VOICE = process . env . POLLINATIONS_VOICE || 'nova' ;
27+ const DEFAULT_TEXT_MODEL = process . env . POLLINATIONS_TEXT_MODEL || BASE_TEXT_MODEL ;
28+ const DEFAULT_TTS_MODEL = process . env . POLLINATIONS_TTS_MODEL || BASE_TTS_MODEL ;
2029
2130const hasTwilioCredentials =
2231 Boolean ( TWILIO_ACCOUNT_SID && TWILIO_AUTH_TOKEN && TWILIO_PHONE_NUMBER ) ;
@@ -44,39 +53,15 @@ const client = hasTwilioCredentials
4453 ? twilio ( TWILIO_ACCOUNT_SID , TWILIO_AUTH_TOKEN )
4554 : null ;
4655
47- function sanitizeForTts ( text ) {
48- if ( ! text ) return '' ;
49- const compact = text . replace ( / \s + / g, ' ' ) . trim ( ) ;
50- if ( compact . length <= 380 ) return compact ;
51- return `${ compact . slice ( 0 , 377 ) } ...` ;
52- }
53-
54- function createTtsUrl ( text , voice = DEFAULT_VOICE ) {
55- const sanitized = sanitizeForTts ( text ) ;
56- const encoded = encodeURIComponent ( sanitized ) ;
57- const url = new URL ( `https://text.pollinations.ai/${ encoded } ` ) ;
58- url . searchParams . set ( 'model' , 'openai-audio' ) ;
59- url . searchParams . set ( 'voice' , voice ) ;
60- return url . toString ( ) ;
61- }
62-
6356async function fetchPollinationsResponse ( session , userMessage ) {
6457 if ( userMessage && userMessage . trim ( ) ) {
6558 session . messages . push ( { role : 'user' , content : userMessage . trim ( ) } ) ;
6659 }
6760
68- const payload = {
69- model : 'openai' ,
70- messages : session . messages ,
71- temperature : 0.8 ,
72- max_output_tokens : 300 ,
73- top_p : 0.95 ,
74- presence_penalty : 0 ,
75- frequency_penalty : 0 ,
76- stream : false
77- } ;
61+ const sessionModel = session . model || DEFAULT_TEXT_MODEL ;
62+ const payload = createOpenAiPayload ( session . messages , { model : sessionModel } ) ;
7863
79- const response = await fetchImpl ( 'https://text.pollinations.ai/openai' , {
64+ const response = await fetchImpl ( buildOpenAiUrl ( sessionModel ) , {
8065 method : 'POST' ,
8166 headers : { 'Content-Type' : 'application/json' } ,
8267 body : JSON . stringify ( payload )
@@ -98,12 +83,13 @@ async function fetchPollinationsResponse(session, userMessage) {
9883 return assistantMessage ;
9984}
10085
101- function createSession ( phoneNumber , initialVoice = DEFAULT_VOICE ) {
86+ function createSession ( phoneNumber , initialVoice = DEFAULT_VOICE , initialModel = DEFAULT_TEXT_MODEL ) {
10287 const id = uuidv4 ( ) ;
10388 const session = {
10489 id,
10590 phoneNumber,
10691 voice : initialVoice ,
92+ model : initialModel ,
10793 messages : [ { role : 'system' , content : SYSTEM_PROMPT } ] ,
10894 lastAssistant : null
10995 } ;
@@ -123,7 +109,7 @@ function buildVoiceResponse(session, twiml, promptMessage, gatherPrompt) {
123109 return twiml ;
124110 }
125111
126- const audioUrl = createTtsUrl ( responseMessage , session . voice ) ;
112+ const audioUrl = createTtsUrl ( responseMessage , session . voice , { model : DEFAULT_TTS_MODEL } ) ;
127113 twiml . play ( audioUrl ) ;
128114
129115 const gather = twiml . gather ( {
@@ -167,7 +153,7 @@ async function startPhoneCall(session) {
167153
168154app . post ( '/api/start-call' , async ( req , res ) => {
169155 try {
170- const { phoneNumber, initialPrompt, voice } = req . body || { } ;
156+ const { phoneNumber, initialPrompt, voice, model } = req . body || { } ;
171157 if ( ! phoneNumber || typeof phoneNumber !== 'string' ) {
172158 return res . status ( 400 ) . json ( { error : 'A destination phoneNumber is required.' } ) ;
173159 }
@@ -178,7 +164,7 @@ app.post('/api/start-call', async (req, res) => {
178164 return res . status ( 500 ) . json ( { error : 'PUBLIC_SERVER_URL is not configured on the server.' } ) ;
179165 }
180166
181- const session = createSession ( phoneNumber . trim ( ) , voice || DEFAULT_VOICE ) ;
167+ const session = createSession ( phoneNumber . trim ( ) , voice || DEFAULT_VOICE , model || DEFAULT_TEXT_MODEL ) ;
182168 const gatherPrompt = 'After the message, speak your reply and stay on the line for the assistant to respond.' ;
183169
184170 if ( initialPrompt && initialPrompt . trim ( ) ) {
@@ -269,6 +255,31 @@ app.use((err, req, res, next) => {
269255 res . status ( 500 ) . json ( { error : 'Internal server error.' } ) ;
270256} ) ;
271257
272- app . listen ( PORT , ( ) => {
273- console . log ( `Twilio voice bridge listening on port ${ PORT } ` ) ;
274- } ) ;
258+ function startServer ( port = PORT ) {
259+ return app . listen ( port , ( ) => {
260+ console . log ( `Twilio voice bridge listening on port ${ port } ` ) ;
261+ } ) ;
262+ }
263+
264+ if ( require . main === module ) {
265+ startServer ( ) ;
266+ }
267+
268+ module . exports = {
269+ app,
270+ startServer,
271+ fetchPollinationsResponse,
272+ createSession,
273+ buildGatherAction,
274+ buildVoiceResponse,
275+ startPhoneCall,
276+ getSession,
277+ sessions,
278+ hasTwilioCredentials,
279+ DEFAULT_VOICE ,
280+ DEFAULT_TEXT_MODEL ,
281+ DEFAULT_TTS_MODEL ,
282+ buildOpenAiUrl,
283+ createOpenAiPayload,
284+ createTtsUrl
285+ } ;
0 commit comments