158 lines
		
	
	
		
			3.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			158 lines
		
	
	
		
			3.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| require("dotenv").config()
 | |
| 
 | |
| const path = require("node:path")
 | |
| const fs = require("node:fs")
 | |
| const axios = require("axios")
 | |
| const { aws4Interceptor } = require("aws4-axios")
 | |
| 
 | |
| const express = require("express")
 | |
| const cors = require("cors")
 | |
| 
 | |
| const getPhrases = require("../../node_lib")
 | |
| 
 | |
| let app = null
 | |
| 
 | |
| const { LISTENING_PORT } = process.env
 | |
| const PORT = LISTENING_PORT || 3000
 | |
| 
 | |
| const cachePath = path.join(process.cwd(), ".cache")
 | |
| const audiosPath = path.join(cachePath, "audio")
 | |
| 
 | |
| const PollyBaseURL = "https://polly.us-east-1.amazonaws.com"
 | |
| 
 | |
| const PollyDefaultConfig = {
 | |
| 	Engine: "generative",
 | |
| 	VoiceId: "Lucia",
 | |
| 	OutputFormat: "mp3",
 | |
| 	LanguageCode: "es-ES",
 | |
| }
 | |
| 
 | |
| const interceptor = aws4Interceptor({
 | |
| 	options: {
 | |
| 		region: "us-east-1",
 | |
| 		service: "polly",
 | |
| 	},
 | |
| 	credentials: {
 | |
| 		accessKeyId: process.env.AWS_API_KEY,
 | |
| 		secretAccessKey: process.env.AWS_API_SECRET,
 | |
| 	},
 | |
| })
 | |
| 
 | |
| axios.interceptors.request.use(interceptor)
 | |
| 
 | |
| async function synthesizePollyVoice(phrase, phraseId) {
 | |
| 	if (!fs.existsSync(audiosPath)) {
 | |
| 		fs.mkdirSync(audiosPath, { recursive: true })
 | |
| 	}
 | |
| 
 | |
| 	// call to api and stream result to a file
 | |
| 	const voiceResultPath = path.resolve(audiosPath, phraseId + ".mp3")
 | |
| 
 | |
| 	if (fs.existsSync(voiceResultPath)) {
 | |
| 		fs.unlinkSync(voiceResultPath)
 | |
| 	}
 | |
| 
 | |
| 	const voiceResultFile = fs.createWriteStream(voiceResultPath)
 | |
| 
 | |
| 	console.log(`Catching TTS file for id [${phraseId}]`)
 | |
| 
 | |
| 	const { data: stream } = await axios({
 | |
| 		url: `${PollyBaseURL}/v1/speech`,
 | |
| 		method: "POST",
 | |
| 		data: {
 | |
| 			...PollyDefaultConfig,
 | |
| 			Text: phrase,
 | |
| 		},
 | |
| 		responseType: "stream",
 | |
| 	})
 | |
| 
 | |
| 	stream.pipe(voiceResultFile)
 | |
| 
 | |
| 	return new Promise((resolve, reject) => {
 | |
| 		stream.on("end", () => resolve(voiceResultPath))
 | |
| 		stream.on("error", (error) => reject(error))
 | |
| 	})
 | |
| }
 | |
| 
 | |
| async function fetchTTSAudioURL(req, phrase, phraseId) {
 | |
| 	const filePath = path.join(audiosPath, `${phraseId}.mp3`)
 | |
| 
 | |
| 	if (!fs.existsSync(filePath)) {
 | |
| 		await synthesizePollyVoice(phrase, phraseId)
 | |
| 	}
 | |
| 
 | |
| 	return `${process.env.NODE_ENV === "production" ? "https" : req.protocol}://${req.get("host")}${req.path}/audio/${phraseId}.mp3`
 | |
| }
 | |
| 
 | |
| async function handleApiRequest(req, res) {
 | |
| 	let { random } = req.query
 | |
| 
 | |
| 	// try to parse random, can be a number or a boolean
 | |
| 	if (random) {
 | |
| 		if (random === "true") {
 | |
| 			random = true
 | |
| 		} else if (Number(random)) {
 | |
| 			random = Number(random)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	const result = await getPhrases({ random })
 | |
| 
 | |
| 	if (random) {
 | |
| 		const phraseId = result
 | |
| 			.trim()
 | |
| 			.toLowerCase()
 | |
| 			.replace(/\s+/g, "_")
 | |
| 			.replace(/[^\w\s]/gi, "")
 | |
| 
 | |
| 		return res.json({
 | |
| 			id: phraseId,
 | |
| 			phrase: result.trim(),
 | |
| 			tts_file: await fetchTTSAudioURL(req, result, phraseId),
 | |
| 		})
 | |
| 	}
 | |
| 
 | |
| 	return res.json(result)
 | |
| }
 | |
| 
 | |
| const useLogger = (req, res, next) => {
 | |
| 	const startHrTime = process.hrtime()
 | |
| 
 | |
| 	res.on("finish", () => {
 | |
| 		let url = req.url
 | |
| 		const elapsedHrTime = process.hrtime(startHrTime)
 | |
| 		const elapsedTimeInMs = elapsedHrTime[0] * 1000 + elapsedHrTime[1] / 1e6
 | |
| 
 | |
| 		res._responseTimeMs = elapsedTimeInMs
 | |
| 
 | |
| 		// cut req.url if is too long
 | |
| 		if (url.length > 100) {
 | |
| 			url = url.substring(0, 100) + "..."
 | |
| 		}
 | |
| 
 | |
| 		console.log(
 | |
| 			`${req.method} ${res._status_code ?? res.statusCode ?? 200} ${url} ${elapsedTimeInMs}ms`,
 | |
| 		)
 | |
| 	})
 | |
| 
 | |
| 	next()
 | |
| }
 | |
| 
 | |
| async function main() {
 | |
| 	app = express()
 | |
| 
 | |
| 	app.use(cors())
 | |
| 	app.use(express.json())
 | |
| 	app.use(useLogger)
 | |
| 
 | |
| 	app.get("/api", handleApiRequest)
 | |
| 	app.use("/api/audio", express.static(audiosPath))
 | |
| 	app.use(express.static(path.join(__dirname, "..", "web", "dist")))
 | |
| 
 | |
| 	app.listen(PORT)
 | |
| 
 | |
| 	console.log(`Listening on port ${PORT}`)
 | |
| }
 | |
| 
 | |
| main().catch(console.error)
 |