Files
jenna-tools/xfetch.ts
bipproduction 822b68c10f tambahannya
2025-12-07 09:00:54 +08:00

210 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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));
})();