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

f7d9dbf5ce5ce6e152c801e338de5e083fdd5d8f

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

1 files changed, +127 -48Ignore whitespace
src/endpoints/openai.js+127 -48
@@ -246,54 +246,6 @@ router.post('/caption-image', async (request, response) => {
246246 }
247247});
248248
249-router.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-
297249router.post('/generate-voice', async (request, response) => {
298250 try {
299251 const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI);
@@ -696,3 +648,130 @@ custom.post('/generate-voice', async (request, response) => {
696648});
697649
698650router.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+ */
660+function 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+
710+router.post('/transcribe-audio', createTranscribeHandler({
711+ secretKey: SECRET_KEYS.OPENAI,
712+ apiUrl: 'https://api.openai.com/v1/audio/transcriptions',
713+ providerName: 'OpenAI',
714+}));
715+
716+router.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+
722+router.post('/mistral/transcribe-audio', createTranscribeHandler({
723+ secretKey: SECRET_KEYS.MISTRALAI,
724+ apiUrl: 'https://api.mistral.ai/v1/audio/transcriptions',
725+ providerName: 'MistralAI',
726+}));
727+
728+router.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+});