| 696 | }); | 648 | }); |
| 697 | | 649 | |
| 698 | router.use('/custom', custom); | 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 | }); |