Compare commits

...

9 Commits

Author SHA1 Message Date
srgooglo dd85e90250 added console log 2025-02-22 06:28:57 +01:00
srgooglo b9b14fa785 fix node_env 2025-02-22 06:24:04 +01:00
srgooglo bc78c82616 use node_env 2025-02-22 06:21:31 +01:00
srgooglo 39165b3ca3 log 2025-02-22 06:11:15 +01:00
srgooglo 0e61f78dbb support tts 2025-02-22 06:08:58 +01:00
srgooglo 0777666877 Actualizar README.md 2025-02-22 04:05:45 +00:00
srgooglo 0d229cd63b Merge pull request 'update monstercanker' (#4) from BlackCherryCat/monstercanker:main into main
Reviewed-on: #4
2024-02-26 15:40:26 +00:00
BlackCherryCat b8b2303f57 update monstercanker 2024-02-26 16:10:15 +01:00
SrGooglo 41139d5944 Merge branch 'BlackCherryCat-main' 2024-02-13 15:48:42 +01:00
7 changed files with 276 additions and 120 deletions

View File

@ -18,4 +18,6 @@ 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"]

View File

@ -1,8 +1,8 @@
# [monstercanker](https://monstercanker.mcommunity.fun/) # [monstercanker](https://msk.ragestudio.net/)
Hechate un gapo la irgen Hechate un gapo la irgen
## API Usage ## API Usage
```https://monstercanker.mcommunity.fun/api?random=<number|bool>``` ```https://msk.ragestudio.net/api?random=<number|bool>```

View File

@ -126,6 +126,7 @@ La republica bananera del congo
Low pollution Low pollution
Mas viejo que el frio Mas viejo que el frio
Mas viejo que andar palante Mas viejo que andar palante
Mas viejo que las piedras
Zambombazo Zambombazo
Petardazo de cuetara Petardazo de cuetara
Monsterkiker Monsterkiker
@ -148,3 +149,4 @@ Libetalmente
Litebalmente Litebalmente
Mojoner Mojoner
Quicha Quicha
Chuminoso

View File

@ -1,48 +1,157 @@
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", async (req, res) => { app.get("/api", handleApiRequest)
let { random } = req.query app.use("/api/audio", express.static(audiosPath))
app.use(express.static(path.join(__dirname, "..", "web", "dist")))
// try to parse random, can be a number or a boolean app.listen(PORT)
if (random) {
if (random === "true") {
random = true
} else if (Number(random)) {
random = Number(random)
}
}
const phrases = await mlib({ random }) console.log(`Listening on port ${PORT}`)
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)

View File

@ -4,6 +4,8 @@
"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"

View File

@ -1,86 +1,99 @@
: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;
} }

View File

@ -5,48 +5,76 @@ import axios from "axios"
import "./index.css" import "./index.css"
const API_ENDPOINT = import.meta.env.PROD ? "/api" : `http://${window.location.hostname}:3000/api` const API_ENDPOINT = import.meta.env.PROD
? "/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({ async function loadRandom({ random = true } = {}) {
random = true, setLoading(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)
} }
React.useEffect(() => { async function playbackCurrentWord() {
loadRandom() if (!randomWord || !randomWord.tts_file) return
}, [])
return <div className="app"> const audio = new Audio()
{
loading ? <p>Loading...</p> : <h1>{randomWord}</h1>
}
<footer> audio.src = randomWord.tts_file
<a href="https://git.ragestudio.net/srgooglo/monstercanker">GitHub</a> audio.volume = 0.5
<a href={API_ENDPOINT}>API</a>
</footer> audio.play()
</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( ReactDOM.createRoot(document.getElementById("root")).render(<App />)
<React.StrictMode>
<App />
</React.StrictMode>,
)