Compare commits

..

No commits in common. "main" and "BlackCherryCat-main" have entirely different histories.

7 changed files with 117 additions and 273 deletions

View File

@ -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"]

View File

@ -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>```

View File

@ -126,7 +126,6 @@ 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
@ -149,4 +148,3 @@ Libetalmente
Litebalmente Litebalmente
Mojoner Mojoner
Quicha Quicha
Chuminoso

View File

@ -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)

View File

@ -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"

View File

@ -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;
} }

View File

@ -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>,
)