Add speech-to-text endpoints for Chutes, Groq and Mistral

f7d9dbf5ce5ce6e152c801e338de5e083fdd5d8f

Cohee <18619528+Cohee1207@users.noreply.github.com>

1 files changed, +127 -48Showing whitespace changes
src/endpoints/openai.js+127 -48
@@ -246,54 +246,6 @@ router.post('/caption-image', async (request, response) => {
246 }246 }
247});247});
248248
249router.post('/transcribe-audio', async (request, response) => {
250 try {
251 const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI);
252
253 if (!key) {
254 console.warn('No OpenAI key found');
255 return response.sendStatus(400);
256 }
257
258 if (!request.file) {
259 console.warn('No audio file found');
260 return response.sendStatus(400);
261 }
262
263 const formData = new FormData();
264 console.info('Processing audio file', request.file.path);
265 formData.append('file', fs.createReadStream(request.file.path), { filename: 'audio.wav', contentType: 'audio/wav' });
266 formData.append('model', request.body.model);
267
268 if (request.body.language) {
269 formData.append('language', request.body.language);
270 }
271
272 const result = await fetch('https://api.openai.com/v1/audio/transcriptions', {
273 method: 'POST',
274 headers: {
275 'Authorization': `Bearer ${key}`,
276 ...formData.getHeaders(),
277 },
278 body: formData,
279 });
280
281 if (!result.ok) {
282 const text = await result.text();
283 console.warn('OpenAI request failed', result.statusText, text);
284 return response.status(500).send(text);
285 }
286
287 fs.unlinkSync(request.file.path);
288 const data = await result.json();
289 console.debug('OpenAI transcription response', data);
290 return response.json(data);
291 } catch (error) {
292 console.error('OpenAI transcription failed', error);
293 response.status(500).send('Internal server error');
294 }
295});
296
297router.post('/generate-voice', async (request, response) => {249router.post('/generate-voice', async (request, response) => {
298 try {250 try {
299 const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI);251 const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI);
@@ -696,3 +648,130 @@ custom.post('/generate-voice', async (request, response) => {
696});648});
697649
698router.use('/custom', custom);650router.use('/custom', custom);
651
652/**
653 * Creates a transcribe-audio endpoint handler for a given provider.
654 * @param {object} config - Provider configuration
655 * @param {string} config.secretKey - The SECRET_KEYS enum value for the provider
656 * @param {string} config.apiUrl - The transcription API endpoint URL
657 * @param {string} config.providerName - Display name for logging
658 * @returns {import('express').RequestHandler} Express request handler
659 */
660function createTranscribeHandler({ secretKey, apiUrl, providerName }) {
661 return async (request, response) => {
662 try {
663 const key = readSecret(request.user.directories, secretKey);
664
665 if (!key) {
666 console.warn(`No ${providerName} key found`);
667 return response.sendStatus(400);
668 }
669
670 if (!request.file) {
671 console.warn('No audio file found');
672 return response.sendStatus(400);
673 }
674
675 console.info(`Processing audio file with ${providerName}`, request.file.path);
676 const formData = new FormData();
677 formData.append('file', fs.createReadStream(request.file.path), { filename: 'audio.wav', contentType: 'audio/wav' });
678 formData.append('model', request.body.model);
679
680 if (request.body.language) {
681 formData.append('language', request.body.language);
682 }
683
684 const result = await fetch(apiUrl, {
685 method: 'POST',
686 headers: {
687 'Authorization': `Bearer ${key}`,
688 ...formData.getHeaders(),
689 },
690 body: formData,
691 });
692
693 if (!result.ok) {
694 const text = await result.text();
695 console.warn(`${providerName} request failed`, result.statusText, text);
696 return response.status(500).send(text);
697 }
698
699 fs.unlinkSync(request.file.path);
700 const data = await result.json();
701 console.debug(`${providerName} transcription response`, data);
702 return response.json(data);
703 } catch (error) {
704 console.error(`${providerName} transcription failed`, error);
705 response.status(500).send('Internal server error');
706 }
707 };
708}
709
710router.post('/transcribe-audio', createTranscribeHandler({
711 secretKey: SECRET_KEYS.OPENAI,
712 apiUrl: 'https://api.openai.com/v1/audio/transcriptions',
713 providerName: 'OpenAI',
714}));
715
716router.post('/groq/transcribe-audio', createTranscribeHandler({
717 secretKey: SECRET_KEYS.GROQ,
718 apiUrl: 'https://api.groq.com/openai/v1/audio/transcriptions',
719 providerName: 'Groq',
720}));
721
722router.post('/mistral/transcribe-audio', createTranscribeHandler({
723 secretKey: SECRET_KEYS.MISTRALAI,
724 apiUrl: 'https://api.mistral.ai/v1/audio/transcriptions',
725 providerName: 'MistralAI',
726}));
727
728router.post('/chutes/transcribe-audio', async (request, response) => {
729 try {
730 const key = readSecret(request.user.directories, SECRET_KEYS.CHUTES);
731
732 if (!key) {
733 console.warn('No Chutes key found');
734 return response.sendStatus(400);
735 }
736
737 if (!request.file) {
738 console.warn('No audio file found');
739 return response.sendStatus(400);
740 }
741
742 console.info('Processing audio file with Chutes', request.file.path);
743 const audioBase64 = fs.readFileSync(request.file.path).toString('base64');
744
745 const result = await fetch(`https://${request.body.model}.chutes.ai/transcribe`, {
746 method: 'POST',
747 headers: {
748 'Authorization': `Bearer ${key}`,
749 'Content-Type': 'application/json',
750 },
751 body: JSON.stringify({
752 audio_b64: audioBase64,
753 }),
754 });
755
756 if (!result.ok) {
757 const text = await result.text();
758 console.warn('Chutes request failed', result.statusText, text);
759 return response.status(500).send(text);
760 }
761
762 fs.unlinkSync(request.file.path);
763 const data = await result.json();
764 console.debug('Chutes transcription response', data);
765
766 if (!Array.isArray(data)) {
767 console.warn('Chutes transcription response invalid', data);
768 return response.sendStatus(500);
769 }
770
771 const fullText = data.map(chunk => chunk.text || '').join('').trim();
772 return response.json({ text: fullText });
773 } catch (error) {
774 console.error('Chutes transcription failed', error);
775 response.status(500).send('Internal server error');
776 }
777});