Spaces:
Paused
Paused
| const express = require('express'); | |
| const WebSocket = require('ws'); | |
| const crypto = require('crypto'); | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const ffmpeg = require('fluent-ffmpeg'); | |
| const app = express(); | |
| const PORT = 7860; | |
| // Konfigurasi TTS | |
| const TRUSTED_CLIENT_TOKEN = "6A5AA1D4EAFF4E9FB37E23D68491D6F4"; | |
| const GEC_VERSION = "1-143.0.3650.75"; | |
| const WSS_URL = "wss://speech.text-to-speech.online/consumer/speech/synthesize/readaloud/edge/v1"; | |
| // Pastikan folder temp ada | |
| const tempDir = path.join(__dirname, 'temp'); | |
| if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir); | |
| /** | |
| * RE Logics | |
| */ | |
| function generateMSHash() { | |
| const WIN_EPOCH = 11644473600n; | |
| const S_TO_NS = 1000000000n; | |
| let ticks = BigInt(Math.floor(Date.now() / 1000)); | |
| ticks += WIN_EPOCH; | |
| ticks -= (ticks % 300n); | |
| ticks *= (S_TO_NS / 100n); | |
| const strToHash = `${ticks}${TRUSTED_CLIENT_TOKEN}`; | |
| return crypto.createHash('sha256').update(strToHash).digest('hex').toUpperCase(); | |
| } | |
| function createGuid() { | |
| return crypto.randomBytes(16).toString('hex').toUpperCase(); | |
| } | |
| /** | |
| * Core TTS Synthesis | |
| */ | |
| function synthesize(text, voice, speed) { | |
| return new Promise((resolve, reject) => { | |
| const requestId = createGuid(); | |
| const connectionId = createGuid(); | |
| const gec = generateMSHash(); | |
| const fullUrl = `${WSS_URL}?TrustedClientToken=${TRUSTED_CLIENT_TOKEN}&Sec-MS-GEC=${gec}&Sec-MS-GEC-Version=${GEC_VERSION}&Authorization=bearer%20undefined&ConnectionId=${connectionId}`; | |
| const ws = new WebSocket(fullUrl, { | |
| origin: 'https://www.text-to-speech.online', | |
| headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36' } | |
| }); | |
| let audioChunks = []; | |
| const ttsRate = speed >= 1 ? `+${(speed - 1) * 100}%` : `-${(1 - speed) * 100}%`; | |
| ws.on('open', () => { | |
| 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"}}}`); | |
| 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}}}`); | |
| 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>`); | |
| }); | |
| ws.on('message', (data, isBinary) => { | |
| if (isBinary) { | |
| const separator = Buffer.from('Path:audio\r\n'); | |
| const index = data.indexOf(separator); | |
| if (index !== -1) audioChunks.push(data.slice(index + separator.length)); | |
| } else if (data.toString().includes('turn.end')) { | |
| ws.close(); | |
| resolve(Buffer.concat(audioChunks)); | |
| } | |
| }); | |
| ws.on('error', reject); | |
| }); | |
| } | |
| /** | |
| * Express Routes | |
| */ | |
| // UI Sederhana | |
| app.get('/', (req, res) => { | |
| res.send(` | |
| <html> | |
| <head><title>Edge TTS Mixer</title></head> | |
| <body style="font-family:sans-serif; max-width:500px; margin:50px auto;"> | |
| <h2>Edge TTS Web UI</h2> | |
| <form action="/generate" method="GET"> | |
| <textarea name="text" style="width:100%" rows="4" placeholder="Masukkan teks..."></textarea><br><br> | |
| Kecepatan: <input type="number" name="speed" value="1.0" step="0.1" min="0.5" max="2.0"><br><br> | |
| Pakai Backsound: <input type="checkbox" name="use_bg" value="true" checked><br><br> | |
| Volume Backsound (0.1 - 1.0): <input type="number" name="bg_vol" value="0.3" step="0.1"><br><br> | |
| <button type="submit" style="padding:10px 20px">Generate & Download</button> | |
| </form> | |
| </body> | |
| </html> | |
| `); | |
| }); | |
| // Endpoint Generate | |
| app.get('/generate', async (req, res) => { | |
| const text = req.query.text || "Halo selamat datang"; | |
| const speed = parseFloat(req.query.speed) || 1.0; | |
| const useBg = req.query.use_bg === 'true'; | |
| const bgVol = parseFloat(req.query.bg_vol) || 0.3; | |
| const voice = "id-ID-ArdiNeural"; | |
| const jobId = Date.now(); | |
| const vocalPath = path.join(tempDir, `vocal_${jobId}.mp3`); | |
| const finalPath = path.join(tempDir, `final_${jobId}.mp3`); | |
| const backsoundPath = path.join(__dirname, 'backsound.mp3'); | |
| try { | |
| // 1. Ambil Vokal dari TTS | |
| const audioBuffer = await synthesize(text, voice, speed); | |
| fs.writeFileSync(vocalPath, audioBuffer); | |
| if (useBg && fs.existsSync(backsoundPath)) { | |
| // 2. Mix dengan Backsound | |
| ffmpeg() | |
| .input(vocalPath) | |
| .input(backsoundPath) | |
| .complexFilter([ | |
| { filter: 'volume', options: { volume: bgVol }, inputs: '1:a', outputs: 'bg' }, | |
| { filter: 'amix', options: { inputs: 2, duration: 'first' }, inputs: ['0:a', 'bg'], outputs: 'out' } | |
| ]) | |
| .map('out') | |
| .audioCodec('libmp3lame') | |
| .on('error', (err) => res.status(500).send("FFmpeg Error: " + err.message)) | |
| .on('end', () => { | |
| res.download(finalPath, "audio_result.mp3", () => { | |
| // Cleanup | |
| if (fs.existsSync(vocalPath)) fs.unlinkSync(vocalPath); | |
| if (fs.existsSync(finalPath)) fs.unlinkSync(finalPath); | |
| }); | |
| }) | |
| .save(finalPath); | |
| } else { | |
| // 3. Tanpa Backsound | |
| res.download(vocalPath, "audio_vocal.mp3", () => { | |
| if (fs.existsSync(vocalPath)) fs.unlinkSync(vocalPath); | |
| }); | |
| } | |
| } catch (err) { | |
| res.status(500).send("Error: " + err.message); | |
| } | |
| }); | |
| app.listen(PORT, () => { | |
| console.log(`Server berjalan di http://localhost:${PORT}`); | |
| }); |