Spaces:
Build error
Build error
| const express = require('express'); | |
| const fs = require('fs'); | |
| const fsp = fs.promises; | |
| const path = require('path'); | |
| const crypto = require('crypto'); | |
| const { spawn } = require('child_process'); | |
| const fetch = require('node-fetch'); | |
| const { v4: uuidv4 } = require('uuid'); | |
| const cors = require('cors'); | |
| const {generateImage} = require('./image.js') | |
| const app = express(); | |
| app.use(express.json()); // To parse JSON payloads | |
| app.use(cors()); // Enable CORS for all routes | |
| require('dotenv').config() | |
| const MEDIA_FOLDER = `public/media` | |
| const OPENAI_API_KEY = process.env.OPENAI_API_KEY | |
| // Ensure the MEDIA_FOLDER directory exists | |
| async function ensureDir(dir) { | |
| try { | |
| await fsp.mkdir(dir, { recursive: true }); | |
| } catch (err) { | |
| if (err.code !== 'EEXIST') throw err; | |
| } | |
| } | |
| (async () => { | |
| await ensureDir(MEDIA_FOLDER); | |
| })(); | |
| const audioCache = {}; // { [scriptHash]: audioFilePath } | |
| function parseScript(script) { | |
| const segments = script.trim().split('\n\n'); | |
| const parsedSegments = []; | |
| for (const segment of segments) { | |
| const [speaker_name, ...contentParts] = segment.split(': '); | |
| const content = contentParts.join(': '); | |
| parsedSegments.push({ speaker_name, content }); | |
| } | |
| return parsedSegments; | |
| } | |
| async function runOpenAITTS(text, audioFilename, voiceId, ttsModel='tts-1') { | |
| if (!OPENAI_API_KEY) { | |
| throw new Error('OPENAI_API_KEY is not set.'); | |
| } | |
| // Replace the URL below with the actual OpenAI TTS endpoint if available | |
| const response = await fetch('https://api.openai.com/v1/audio/speech', { | |
| method: 'POST', | |
| headers: { | |
| Authorization: `Bearer ${OPENAI_API_KEY}`, | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| model: ttsModel, | |
| voice: voiceId, | |
| input: text, | |
| }), | |
| }); | |
| if (!response.ok) { | |
| const errorText = await response.text(); | |
| throw new Error(`OpenAI TTS request failed: ${errorText}`); | |
| } | |
| const arrayBuffer = await response.arrayBuffer(); | |
| const buffer = Buffer.from(arrayBuffer); | |
| await fsp.writeFile(audioFilename, buffer); | |
| } | |
| async function generateAudio(speakerName, content) { | |
| const voiceLookupTable = { | |
| DEFAULT: 'alloy', | |
| ALICE: 'shimmer', | |
| BOB: 'echo', | |
| JENNIFER: 'nova', | |
| PROFESSOR: 'fable', | |
| MALE_GUEST: 'onyx', | |
| FEMALE_GUEST: 'alloy', | |
| }; | |
| const actualVoiceId = voiceLookupTable[speakerName] || voiceLookupTable['DEFAULT']; | |
| const fileName = path.join(MEDIA_FOLDER, `${uuidv4()}.mp3`); | |
| await runOpenAITTS(content, fileName, actualVoiceId, 'tts-1-hd'); | |
| return fileName; | |
| } | |
| function concatenateAudioFiles(audioFiles, outputFilePath) { | |
| return new Promise((resolve, reject) => { | |
| if (audioFiles.length === 1) { | |
| // If only one audio file, copy it directly | |
| fs.copyFileSync(audioFiles[0], outputFilePath); | |
| resolve(); | |
| return; | |
| } | |
| const listContent = audioFiles.join('|'); | |
| // Run FFmpeg with the concat protocol | |
| // ffmpeg -i "concat:file1.mp3|file2.mp3" -acodec copy output.mp3 | |
| const ffmpeg = spawn('ffmpeg', [ | |
| '-i', | |
| `concat:${listContent}`, | |
| '-acodec', | |
| 'copy', | |
| outputFilePath, | |
| ]); | |
| ffmpeg.stdout.on('data', (data) => { | |
| console.log(`stdout: ${data}`); | |
| }); | |
| ffmpeg.stderr.on('data', (data) => { | |
| console.error(`stderr: ${data}`); | |
| }); | |
| ffmpeg.on('close', (code) => { | |
| if (code === 0) { | |
| resolve(); | |
| } else { | |
| reject(new Error(`FFmpeg failed with exit code ${code}`)); | |
| } | |
| }); | |
| }); | |
| } | |
| // Existing GET endpoint (unchanged) | |
| app.get('/list-models', (req, res) => { | |
| // Placeholder for listing models, replace with actual implementation if needed | |
| res.json(['Model 1', 'Model 2', 'Model 3']); | |
| }); | |
| // Existing GET endpoint (unchanged) | |
| app.get('/generate/speech', async (req, res) => { | |
| try { | |
| const apiKey = req.query.api_key || 'their_api_key'; | |
| if (apiKey !== 'their_api_key') { | |
| // Replace "their_api_key" with your actual method of managing API keys | |
| res.status(401).send('Unauthorized'); | |
| return; | |
| } | |
| const script = req.query.payload; | |
| if (!script) { | |
| res.status(400).send('Bad Request: Missing payload'); | |
| return; | |
| } | |
| const hash = crypto.createHash('sha1'); | |
| hash.update(script); | |
| const scriptHash = hash.digest('hex'); | |
| if (audioCache[scriptHash]) { | |
| const filePath = audioCache[scriptHash]; | |
| res.sendFile(path.resolve(filePath), { headers: { 'Content-Type': 'audio/mpeg' } }); | |
| return; | |
| } | |
| const parsedSegments = parseScript(script); | |
| const audioSegments = []; | |
| for (const segment of parsedSegments) { | |
| const audioPath = await generateAudio(segment.speaker_name, segment.content); | |
| audioSegments.push(audioPath); | |
| } | |
| if (audioSegments.length === 0) { | |
| res.status(400).send('No audio generated'); | |
| return; | |
| } | |
| // Concatenate audio files into one using FFmpeg | |
| const combinedAudioPath = path.join(MEDIA_FOLDER, `combined_${uuidv4()}.mp3`); | |
| await concatenateAudioFiles(audioSegments, combinedAudioPath); | |
| audioCache[scriptHash] = combinedAudioPath; | |
| res.sendFile(path.resolve(combinedAudioPath), { headers: { 'Content-Type': 'audio/mpeg' } }); | |
| } catch (error) { | |
| console.error('Error generating speech:', error); | |
| res.status(500).send('Internal Server Error'); | |
| } | |
| }); | |
| // New POST endpoint with SSE support | |
| app.post('/api/generate/speech/stream', async (req, res) => { | |
| try { | |
| const apiKey = req.query.api_key || 'their_api_key'; | |
| if (apiKey !== 'their_api_key') { | |
| // Replace "their_api_key" with your actual method of managing API keys | |
| res.status(401).send('Unauthorized'); | |
| return; | |
| } | |
| const script = req.body.payload; | |
| if (!script) { | |
| res.status(400).send('Bad Request: Missing payload'); | |
| return; | |
| } | |
| // Set headers for SSE | |
| res.setHeader('Content-Type', 'text/event-stream'); | |
| res.setHeader('Cache-Control', 'no-cache'); | |
| res.setHeader('Connection', 'keep-alive'); | |
| const hash = crypto.createHash('sha1'); | |
| hash.update(script); | |
| const scriptHash = hash.digest('hex'); | |
| if (audioCache[scriptHash]) { | |
| // If audio is cached, send the final SSE with the combined audio URL | |
| const filePath = audioCache[scriptHash]; | |
| console.log(filePath) | |
| res.write(`event: audio_complete\ndata: ${req.protocol}://${req.get('host')}/${filePath}\n\n`); | |
| res.end(); | |
| return; | |
| } | |
| const parsedSegments = parseScript(script); | |
| const audioSegments = []; | |
| for (const segment of parsedSegments) { | |
| const audioPath = await generateAudio(segment.speaker_name, segment.content); | |
| audioSegments.push(audioPath); | |
| // Send SSE with the URL of the generated audio segment | |
| res.write(`event: audio_segment\ndata: ${req.protocol}://${req.get('host')}/${audioPath}\n\n`); | |
| } | |
| if (audioSegments.length === 0) { | |
| res.write(`event: error\ndata: No audio generated\n\n`); | |
| res.end(); | |
| return; | |
| } | |
| // Concatenate audio files into one using FFmpeg | |
| const combinedAudioPath = path.join(MEDIA_FOLDER, `combined_${uuidv4()}.mp3`); | |
| await concatenateAudioFiles(audioSegments, combinedAudioPath); | |
| audioCache[scriptHash] = combinedAudioPath; | |
| console.log(combinedAudioPath) | |
| // Send SSE with the URL of the combined audio | |
| res.write(`event: audio_complete\ndata: ${req.protocol}://${req.get('host')}/${combinedAudioPath}\n\n`); | |
| res.end(); | |
| } catch (error) { | |
| console.error('Error generating speech:', error); | |
| res.write(`event: error\ndata: Internal Server Error\n\n`); | |
| res.end(); | |
| } | |
| }); | |
| //for prompt-in-url: <img src="https://yourserver.com/generate/image?prompt=A%20large%20hamster&width=1024&height=1024" | |
| app.get('/api/generate/image', async (req, res) => { | |
| const responseFormat = req.query.response_format || 'image'; | |
| await generateImage(req.query, res, responseFormat) | |
| }); | |
| app.post("/api/generate/image", async(req, res)=> { | |
| const responseFormat = req.query.response_format || 'url'; | |
| await generateImage(req.body, res, responseFormat) | |
| }) | |
| // Client webpages and storage for generated content | |
| app.use('/cdn', express.static("public")); | |
| const port = 6666; | |
| app.listen(port, () => { | |
| console.log(`Listening on port ${port}`); | |
| }); | |