Spaces:
Running
Running
Create server.js
#2
by nirkyy - opened
server.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const express = require('express');
|
| 2 |
+
const WebSocket = require('ws');
|
| 3 |
+
const crypto = require('crypto');
|
| 4 |
+
const fs = require('fs');
|
| 5 |
+
const path = require('path');
|
| 6 |
+
const ffmpeg = require('fluent-ffmpeg');
|
| 7 |
+
|
| 8 |
+
const app = express();
|
| 9 |
+
const PORT = 7860;
|
| 10 |
+
|
| 11 |
+
// Konfigurasi TTS
|
| 12 |
+
const TRUSTED_CLIENT_TOKEN = "6A5AA1D4EAFF4E9FB37E23D68491D6F4";
|
| 13 |
+
const GEC_VERSION = "1-143.0.3650.75";
|
| 14 |
+
const WSS_URL = "wss://speech.text-to-speech.online/consumer/speech/synthesize/readaloud/edge/v1";
|
| 15 |
+
|
| 16 |
+
// Pastikan folder temp ada
|
| 17 |
+
const tempDir = path.join(__dirname, 'temp');
|
| 18 |
+
if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir);
|
| 19 |
+
|
| 20 |
+
/**
|
| 21 |
+
* RE Logics
|
| 22 |
+
*/
|
| 23 |
+
function generateMSHash() {
|
| 24 |
+
const WIN_EPOCH = 11644473600n;
|
| 25 |
+
const S_TO_NS = 1000000000n;
|
| 26 |
+
let ticks = BigInt(Math.floor(Date.now() / 1000));
|
| 27 |
+
ticks += WIN_EPOCH;
|
| 28 |
+
ticks -= (ticks % 300n);
|
| 29 |
+
ticks *= (S_TO_NS / 100n);
|
| 30 |
+
const strToHash = `${ticks}${TRUSTED_CLIENT_TOKEN}`;
|
| 31 |
+
return crypto.createHash('sha256').update(strToHash).digest('hex').toUpperCase();
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
function createGuid() {
|
| 35 |
+
return crypto.randomBytes(16).toString('hex').toUpperCase();
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
/**
|
| 39 |
+
* Core TTS Synthesis
|
| 40 |
+
*/
|
| 41 |
+
function synthesize(text, voice, speed) {
|
| 42 |
+
return new Promise((resolve, reject) => {
|
| 43 |
+
const requestId = createGuid();
|
| 44 |
+
const connectionId = createGuid();
|
| 45 |
+
const gec = generateMSHash();
|
| 46 |
+
const fullUrl = `${WSS_URL}?TrustedClientToken=${TRUSTED_CLIENT_TOKEN}&Sec-MS-GEC=${gec}&Sec-MS-GEC-Version=${GEC_VERSION}&Authorization=bearer%20undefined&ConnectionId=${connectionId}`;
|
| 47 |
+
|
| 48 |
+
const ws = new WebSocket(fullUrl, {
|
| 49 |
+
origin: 'https://www.text-to-speech.online',
|
| 50 |
+
headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36' }
|
| 51 |
+
});
|
| 52 |
+
|
| 53 |
+
let audioChunks = [];
|
| 54 |
+
const ttsRate = speed >= 1 ? `+${(speed - 1) * 100}%` : `-${(1 - speed) * 100}%`;
|
| 55 |
+
|
| 56 |
+
ws.on('open', () => {
|
| 57 |
+
ws.send(`Path: speech.config\r\nX-RequestId: ${requestId}\r\nX-Timestamp: ${new Date().toISOString()}\r\nContent-Type: application/json\r\n\r\n{"context":{"system":{"name":"SpeechSDK","version":"1.19.0","build":"JavaScript","lang":"JavaScript"},"os":{"platform":"Browser/Win32","name":"Chrome","version":"120.0.0.0"}}}`);
|
| 58 |
+
ws.send(`Path: synthesis.context\r\nX-RequestId: ${requestId}\r\nX-Timestamp: ${new Date().toISOString()}\r\nContent-Type: application/json\r\n\r\n{"synthesis":{"audio":{"metadataOptions":{"bookmarkEnabled":false,"sentenceBoundaryEnabled":false,"visemeEnabled":false,"wordBoundaryEnabled":false},"outputFormat":"audio-24khz-48kbitrate-mono-mp3"},"language":{"autoDetection":false}}}`);
|
| 59 |
+
ws.send(`Path: ssml\r\nX-RequestId: ${requestId}\r\nX-Timestamp: ${new Date().toISOString()}\r\nContent-Type: application/ssml+xml\r\n\r\n<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="en-US"><voice name="${voice}"><prosody rate="${ttsRate}" pitch="0%">${text}</prosody></voice></speak>`);
|
| 60 |
+
});
|
| 61 |
+
|
| 62 |
+
ws.on('message', (data, isBinary) => {
|
| 63 |
+
if (isBinary) {
|
| 64 |
+
const separator = Buffer.from('Path:audio\r\n');
|
| 65 |
+
const index = data.indexOf(separator);
|
| 66 |
+
if (index !== -1) audioChunks.push(data.slice(index + separator.length));
|
| 67 |
+
} else if (data.toString().includes('turn.end')) {
|
| 68 |
+
ws.close();
|
| 69 |
+
resolve(Buffer.concat(audioChunks));
|
| 70 |
+
}
|
| 71 |
+
});
|
| 72 |
+
ws.on('error', reject);
|
| 73 |
+
});
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
/**
|
| 77 |
+
* Express Routes
|
| 78 |
+
*/
|
| 79 |
+
|
| 80 |
+
// UI Sederhana
|
| 81 |
+
app.get('/', (req, res) => {
|
| 82 |
+
res.send(`
|
| 83 |
+
<html>
|
| 84 |
+
<head><title>Edge TTS Mixer</title></head>
|
| 85 |
+
<body style="font-family:sans-serif; max-width:500px; margin:50px auto;">
|
| 86 |
+
<h2>Edge TTS Web UI</h2>
|
| 87 |
+
<form action="/generate" method="GET">
|
| 88 |
+
<textarea name="text" style="width:100%" rows="4" placeholder="Masukkan teks..."></textarea><br><br>
|
| 89 |
+
Kecepatan: <input type="number" name="speed" value="1.0" step="0.1" min="0.5" max="2.0"><br><br>
|
| 90 |
+
Pakai Backsound: <input type="checkbox" name="use_bg" value="true" checked><br><br>
|
| 91 |
+
Volume Backsound (0.1 - 1.0): <input type="number" name="bg_vol" value="0.3" step="0.1"><br><br>
|
| 92 |
+
<button type="submit" style="padding:10px 20px">Generate & Download</button>
|
| 93 |
+
</form>
|
| 94 |
+
</body>
|
| 95 |
+
</html>
|
| 96 |
+
`);
|
| 97 |
+
});
|
| 98 |
+
|
| 99 |
+
// Endpoint Generate
|
| 100 |
+
app.get('/generate', async (req, res) => {
|
| 101 |
+
const text = req.query.text || "Halo selamat datang";
|
| 102 |
+
const speed = parseFloat(req.query.speed) || 1.0;
|
| 103 |
+
const useBg = req.query.use_bg === 'true';
|
| 104 |
+
const bgVol = parseFloat(req.query.bg_vol) || 0.3;
|
| 105 |
+
const voice = "id-ID-ArdiNeural";
|
| 106 |
+
|
| 107 |
+
const jobId = Date.now();
|
| 108 |
+
const vocalPath = path.join(tempDir, `vocal_${jobId}.mp3`);
|
| 109 |
+
const finalPath = path.join(tempDir, `final_${jobId}.mp3`);
|
| 110 |
+
const backsoundPath = path.join(__dirname, 'backsound.mp3');
|
| 111 |
+
|
| 112 |
+
try {
|
| 113 |
+
// 1. Ambil Vokal dari TTS
|
| 114 |
+
const audioBuffer = await synthesize(text, voice, speed);
|
| 115 |
+
fs.writeFileSync(vocalPath, audioBuffer);
|
| 116 |
+
|
| 117 |
+
if (useBg && fs.existsSync(backsoundPath)) {
|
| 118 |
+
// 2. Mix dengan Backsound
|
| 119 |
+
ffmpeg()
|
| 120 |
+
.input(vocalPath)
|
| 121 |
+
.input(backsoundPath)
|
| 122 |
+
.complexFilter([
|
| 123 |
+
{ filter: 'volume', options: { volume: bgVol }, inputs: '1:a', outputs: 'bg' },
|
| 124 |
+
{ filter: 'amix', options: { inputs: 2, duration: 'first' }, inputs: ['0:a', 'bg'], outputs: 'out' }
|
| 125 |
+
])
|
| 126 |
+
.map('out')
|
| 127 |
+
.audioCodec('libmp3lame')
|
| 128 |
+
.on('error', (err) => res.status(500).send("FFmpeg Error: " + err.message))
|
| 129 |
+
.on('end', () => {
|
| 130 |
+
res.download(finalPath, "audio_result.mp3", () => {
|
| 131 |
+
// Cleanup
|
| 132 |
+
if (fs.existsSync(vocalPath)) fs.unlinkSync(vocalPath);
|
| 133 |
+
if (fs.existsSync(finalPath)) fs.unlinkSync(finalPath);
|
| 134 |
+
});
|
| 135 |
+
})
|
| 136 |
+
.save(finalPath);
|
| 137 |
+
} else {
|
| 138 |
+
// 3. Tanpa Backsound
|
| 139 |
+
res.download(vocalPath, "audio_vocal.mp3", () => {
|
| 140 |
+
if (fs.existsSync(vocalPath)) fs.unlinkSync(vocalPath);
|
| 141 |
+
});
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
} catch (err) {
|
| 145 |
+
res.status(500).send("Error: " + err.message);
|
| 146 |
+
}
|
| 147 |
+
});
|
| 148 |
+
|
| 149 |
+
app.listen(PORT, () => {
|
| 150 |
+
console.log(`Server berjalan di http://localhost:${PORT}`);
|
| 151 |
+
});
|