210 lines
6.0 KiB
TypeScript
210 lines
6.0 KiB
TypeScript
import fs from "fs";
|
||
|
||
const HOST = "http://85.31.224.193:4000/";
|
||
const TEMP_DIR = "./temp-tts";
|
||
|
||
// Pastikan folder temp ada
|
||
if (!fs.existsSync(TEMP_DIR)) {
|
||
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
||
}
|
||
|
||
/* ============================================================
|
||
CLEAN TEXT (AMAN UNTUK UNICODE)
|
||
============================================================ */
|
||
|
||
function convertDateToText(text: string): string {
|
||
const months = [
|
||
"Januari", "Februari", "Maret", "April", "Mei", "Juni",
|
||
"Juli", "Agustus", "September", "Oktober", "November", "Desember"
|
||
];
|
||
|
||
return text.replace(/\(?(\d{1,2})\/(\d{1,2})\/(\d{4})\)?/g, (_, dd, mm, yyyy) => {
|
||
const monthIndex = parseInt(mm, 10) - 1;
|
||
if (monthIndex < 0 || monthIndex > 11) return _;
|
||
return `${parseInt(dd, 10)} ${months[monthIndex]} ${yyyy}`;
|
||
});
|
||
}
|
||
|
||
function cleanText(text: string): string {
|
||
// Ubah tanggal dulu biar lebih mudah dibaca TTS
|
||
text = convertDateToText(text);
|
||
|
||
return text
|
||
// izinkan: huruf, angka, spasi, dan . , ! ? ;
|
||
.replace(/[^\p{L}\p{N} .,!?;]/gu, "")
|
||
// normalkan banyak spasi menjadi 1 spasi
|
||
.replace(/\s+/g, " ")
|
||
// trim spasi kiri/kanan
|
||
.trim();
|
||
}
|
||
|
||
|
||
|
||
|
||
/* ============================================================
|
||
SPLIT TEXT (MAKSIMAL 200 CHAR + CARI TITIK/KOMA/!?)
|
||
============================================================ */
|
||
|
||
function splitText(text: string, max = 200): string[] {
|
||
const chunks: string[] = [];
|
||
text = text.trim();
|
||
|
||
const isDecimal = (str: string, idx: number) => {
|
||
// kasus angka.desimal → contoh: 1000.25 atau 1,234.56
|
||
const before = str[idx - 1];
|
||
const after = str[idx + 1];
|
||
return /\d/.test(before || '') && /\d/.test(after || '');
|
||
};
|
||
|
||
const isThousandsSeparator = (str: string, idx: number) => {
|
||
// angka ribuan 1.234 atau 2,500 dll
|
||
const before = str[idx - 1];
|
||
const after = str[idx + 1];
|
||
return /\d/.test(before || '') && /\d/.test(after || '');
|
||
};
|
||
|
||
while (text.length > 0) {
|
||
if (text.length <= max) {
|
||
chunks.push(text);
|
||
break;
|
||
}
|
||
|
||
const slice = text.slice(0, max);
|
||
|
||
let cutIndex = -1;
|
||
|
||
// Cari tanda baca yang aman untuk split
|
||
for (let i = slice.length - 1; i >= 0; i--) {
|
||
const ch = slice[i];
|
||
|
||
if (".,!?;".includes(ch || '')) {
|
||
// Abaikan jika itu bagian dari angka
|
||
if (isDecimal(slice, i)) continue;
|
||
if (isThousandsSeparator(slice, i)) continue;
|
||
|
||
cutIndex = i + 1;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Jika tidak ada tanda baca yang valid
|
||
if (cutIndex === -1) cutIndex = max;
|
||
|
||
const part = text.slice(0, cutIndex).trim();
|
||
if (part) chunks.push(part);
|
||
|
||
text = text.slice(cutIndex).trim();
|
||
}
|
||
|
||
return chunks;
|
||
}
|
||
|
||
|
||
/* ============================================================
|
||
FETCH WITH RETRY
|
||
============================================================ */
|
||
|
||
async function fetchRetry(url: string, options: any = {}, retries = 3) {
|
||
for (let i = 0; i < retries; i++) {
|
||
try {
|
||
return await fetch(url, options);
|
||
} catch (err) {
|
||
console.warn(`Fetch attempt ${i + 1} failed:`, err);
|
||
if (i === retries - 1) throw err;
|
||
await new Promise((r) => setTimeout(r, 1000 * (i + 1)));
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
TTS ASYNC REQUEST
|
||
============================================================ */
|
||
|
||
async function generate(text: string) {
|
||
const res = await fetchRetry(`${HOST}/tts-async`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||
body: new URLSearchParams({ text, prompt: "dayu" }),
|
||
});
|
||
|
||
if (!res) {
|
||
throw new Error("Failed to fetch result");
|
||
}
|
||
|
||
let data;
|
||
try {
|
||
data = await res.json();
|
||
} catch {
|
||
throw new Error("Invalid JSON response from server");
|
||
}
|
||
|
||
if (!data.job_id) {
|
||
console.error("❌ Response tidak mengandung job_id:", data);
|
||
throw new Error("job_id missing in generate response");
|
||
}
|
||
|
||
return data;
|
||
}
|
||
|
||
|
||
/* ============================================================
|
||
MAIN
|
||
============================================================ */
|
||
|
||
(async () => {
|
||
console.log("🎙️ TTS Async Client Starting...\n");
|
||
|
||
let text = `
|
||
Kalau soal **minimal durasi contoh suara untuk clone suara (voice cloning)** di model seperti Chatterbox TTS atau umumnya model voice cloning, biasanya durasi minimalnya tergantung dari kualitas dan metode cloning-nya.
|
||
|
||
Berikut gambaran umumnya:
|
||
|
||
* **Minimal durasi ideal untuk voice cloning berkualitas cukup baik:**
|
||
**sekitar 30 detik – 1 menit** rekaman suara bersih, jernih, tanpa noise, dan dengan intonasi alami.
|
||
|
||
* **Jika kurang dari 30 detik:**
|
||
Model bisa saja clone, tapi hasilnya biasanya kurang natural dan suaranya bisa terdengar “robotik” atau kurang variatif.
|
||
|
||
* **Durasi lebih dari 1 menit:**
|
||
Biasanya semakin bagus hasil cloning karena model punya banyak data untuk belajar karakter suara, intonasi, ekspresi, dan variasi.
|
||
|
||
---
|
||
|
||
### Catatan:
|
||
|
||
* Kualitas rekaman sangat penting: noise rendah, microphone bagus, format lossless (WAV) lebih disarankan.
|
||
* Banyak model TTS/voice cloning modern bisa pakai data pendek, tapi kualitas output terbatas.
|
||
* Kalau kamu pakai Chatterbox TTS khusus, coba cek dokumentasi mereka apakah ada rekomendasi durasi input.
|
||
|
||
---
|
||
|
||
Kalau kamu butuh, aku bisa bantu cek dokumentasi spesifik Chatterbox TTS atau rekomendasi untuk voice cloning dari model lain juga. Mau?
|
||
|
||
`;
|
||
|
||
text = cleanText(text);
|
||
|
||
const parts = splitText(text, 360);
|
||
|
||
console.log(`📝 Total chunks: ${parts.length}\n`);
|
||
|
||
const jobs: string[] = [];
|
||
|
||
// SEND JOBS
|
||
console.log("📤 Sending jobs to server...");
|
||
for (let i = 0; i < parts.length; i++) {
|
||
try {
|
||
const res = await generate(parts[i] as string);
|
||
jobs.push(res.job_id);
|
||
console.log(` ✓ Job ${i + 1}/${parts.length}: ${res.job_id}`);
|
||
} catch (error) {
|
||
console.error(`❌ Failed to generate job for chunk ${i + 1}:`, error);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
console.log(`\n✅ All ${jobs.length} jobs submitted\n`);
|
||
fs.writeFileSync(`${TEMP_DIR}/jobs.json`, JSON.stringify(jobs));
|
||
|
||
})();
|