Compare commits
No commits in common. "main" and "main" have entirely different histories.
|
@ -18,6 +18,4 @@ RUN cd /home/node/app/node_api/api && npm install -D --force
|
||||||
RUN cd /home/node/app/node_api/web && npm install -D --force
|
RUN cd /home/node/app/node_api/web && npm install -D --force
|
||||||
RUN cd /home/node/app/node_api/web && npm run build
|
RUN cd /home/node/app/node_api/web && npm run build
|
||||||
|
|
||||||
# set to production
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
CMD ["node", "/home/node/app/node_api/api/index.js"]
|
CMD ["node", "/home/node/app/node_api/api/index.js"]
|
|
@ -1,8 +1,8 @@
|
||||||
# [monstercanker](https://msk.ragestudio.net/)
|
# [monstercanker](https://monstercanker.mcommunity.fun/)
|
||||||
|
|
||||||
Hechate un gapo la irgen
|
Hechate un gapo la irgen
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## API Usage
|
## API Usage
|
||||||
```https://msk.ragestudio.net/api?random=<number|bool>```
|
```https://monstercanker.mcommunity.fun/api?random=<number|bool>```
|
|
@ -1,157 +1,48 @@
|
||||||
require("dotenv").config()
|
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 express = require("express")
|
||||||
|
const path = require("path")
|
||||||
const cors = require("cors")
|
const cors = require("cors")
|
||||||
|
const mlib = require("../../node_lib")
|
||||||
const getPhrases = require("../../node_lib")
|
|
||||||
|
|
||||||
let app = null
|
let app = null
|
||||||
|
|
||||||
const { LISTENING_PORT } = process.env
|
const { LISTENING_PORT } = process.env
|
||||||
const PORT = LISTENING_PORT || 3000
|
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() {
|
async function main() {
|
||||||
app = express()
|
app = express()
|
||||||
|
|
||||||
app.use(cors())
|
app.use(cors())
|
||||||
app.use(express.json())
|
app.use(express.json())
|
||||||
app.use(useLogger)
|
|
||||||
|
|
||||||
app.get("/api", handleApiRequest)
|
app.get("/api", async (req, res) => {
|
||||||
app.use("/api/audio", express.static(audiosPath))
|
let { random } = req.query
|
||||||
app.use(express.static(path.join(__dirname, "..", "web", "dist")))
|
|
||||||
|
|
||||||
app.listen(PORT)
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`Listening on port ${PORT}`)
|
const phrases = await mlib({ random })
|
||||||
|
|
||||||
|
res.json(phrases)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use(express.static(path.join(__dirname, "..", "web", "dist",)))
|
||||||
|
|
||||||
|
// serve static react build
|
||||||
|
app.get("*", (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, "..", "web", "dist", "index.html"))
|
||||||
|
})
|
||||||
|
|
||||||
|
app.listen(PORT)
|
||||||
|
|
||||||
|
console.log(`Listening on port ${PORT}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch(console.error)
|
main().catch(console.error)
|
|
@ -4,8 +4,6 @@
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"aws4-axios": "^3.3.15",
|
|
||||||
"axios": "^1.7.9",
|
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.1",
|
"dotenv": "^16.4.1",
|
||||||
"express": "^4.18.2"
|
"express": "^4.18.2"
|
||||||
|
|
|
@ -1,99 +1,86 @@
|
||||||
:root {
|
:root {
|
||||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
color-scheme: light dark;
|
color-scheme : light dark;
|
||||||
color: rgba(255, 255, 255, 0.87);
|
color : rgba(255, 255, 255, 0.87);
|
||||||
background-color: #242424;
|
background-color: #242424;
|
||||||
|
|
||||||
font-synthesis: none;
|
font-synthesis : none;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering : optimizeLegibility;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing : antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
font-weight: 500;
|
font-weight : 500;
|
||||||
color: #646cff;
|
color : #646cff;
|
||||||
text-decoration: inherit;
|
text-decoration: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
color: #535bf2;
|
color: #535bf2;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
display: flex;
|
display : flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
margin: 0;
|
margin : 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 3.2em;
|
font-size : 3.2em;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
border-radius: 8px;
|
border-radius : 8px;
|
||||||
border: 1px solid transparent;
|
border : 1px solid transparent;
|
||||||
padding: 0.6em 1.2em;
|
padding : 0.6em 1.2em;
|
||||||
font-size: 1em;
|
font-size : 1em;
|
||||||
font-weight: 500;
|
font-weight : 500;
|
||||||
font-family: inherit;
|
font-family : inherit;
|
||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
cursor: pointer;
|
cursor : pointer;
|
||||||
transition: border-color 0.25s;
|
transition : border-color 0.25s;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
border-color: #646cff;
|
border-color: #646cff;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:focus,
|
button:focus,
|
||||||
button:focus-visible {
|
button:focus-visible {
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app {
|
.app {
|
||||||
display: flex;
|
display : flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
align-items: center;
|
align-items : center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
width: 100%;
|
width : 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
|
||||||
|
|
||||||
.result {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.playback_audio {
|
|
||||||
font-size: 24px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
||||||
bottom: 10vh;
|
bottom: 10vh;
|
||||||
left: 0;
|
left : 0;
|
||||||
|
|
||||||
width: 100%;
|
width : 100%;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
align-items: center;
|
align-items : center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
|
@ -5,76 +5,48 @@ import axios from "axios"
|
||||||
|
|
||||||
import "./index.css"
|
import "./index.css"
|
||||||
|
|
||||||
const API_ENDPOINT = import.meta.env.PROD
|
const API_ENDPOINT = import.meta.env.PROD ? "/api" : `http://${window.location.hostname}:3000/api`
|
||||||
? "/api"
|
|
||||||
: `http://${window.location.hostname}:3000/api`
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const [loading, setLoading] = React.useState(true)
|
const [loading, setLoading] = React.useState(true)
|
||||||
const [randomWord, setRandomWord] = React.useState(null)
|
const [randomWord, setRandomWord] = React.useState(null)
|
||||||
|
|
||||||
async function loadRandom({ random = true } = {}) {
|
async function loadRandom({
|
||||||
setLoading(true)
|
random = true,
|
||||||
|
} = {}) {
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
let { data } = await axios({
|
let { data } = await axios({
|
||||||
url: API_ENDPOINT,
|
url: API_ENDPOINT,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
params: {
|
params: {
|
||||||
random,
|
random,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|
||||||
setRandomWord(data)
|
setRandomWord(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function playbackCurrentWord() {
|
React.useEffect(() => {
|
||||||
if (!randomWord || !randomWord.tts_file) return
|
loadRandom()
|
||||||
|
}, [])
|
||||||
|
|
||||||
const audio = new Audio()
|
return <div className="app">
|
||||||
|
{
|
||||||
|
loading ? <p>Loading...</p> : <h1>{randomWord}</h1>
|
||||||
|
}
|
||||||
|
|
||||||
audio.src = randomWord.tts_file
|
<footer>
|
||||||
audio.volume = 0.5
|
<a href="https://git.ragestudio.net/srgooglo/monstercanker">GitHub</a>
|
||||||
|
<a href={API_ENDPOINT}>API</a>
|
||||||
audio.play()
|
</footer>
|
||||||
}
|
</div>
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
loadRandom()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="app">
|
|
||||||
<div className="result">
|
|
||||||
{loading ? <p>Loading...</p> : <h1>{randomWord.phrase}</h1>}
|
|
||||||
|
|
||||||
<div className="playback_audio" onClick={playbackCurrentWord}>
|
|
||||||
<svg
|
|
||||||
stroke="currentColor"
|
|
||||||
fill="none"
|
|
||||||
strokeWidth="2"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
height="1em"
|
|
||||||
width="1em"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
|
|
||||||
<path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<a href="https://git.ragestudio.net/srgooglo/monstercanker">
|
|
||||||
GitHub
|
|
||||||
</a>
|
|
||||||
<a href={API_ENDPOINT}>API</a>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")).render(<App />)
|
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue