| 696 | 648 | }); |
| 697 | 649 | |
| 698 | 650 | router.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 | +}); |