#!/usr/bin/env node /** * Real-time Streaming TTS Audio Player */ import { spawn, ChildProcess } from "child_process"; import { Supertone } from "../src/index.js"; import * as models from "../src/models/index.js"; import type { PronunciationDictionaryEntry } from "../src/lib/custom_utils/index.js"; import * as dotenv from "dotenv"; import * as path from "path"; import { fileURLToPath } from "url"; // Load environment variables const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); dotenv.config({ path: path.join(__dirname, ".env") }); const API_KEY = process.env.SUPERTONE_API_KEY || ""; interface PlaybackStats { streamingStartTime: number; apiCallStartTime: number; apiCallEndTime: number; firstChunkTime: number; playbackStartTime: number; totalBytes: number; totalChunks: number; estimatedDuration: number; sampleRate?: number; channels?: number; } class SimpleMpvPlayer { private mpvProcess: ChildProcess | null = null; private isPlaying = false; private isPlaybackActive = false; private initialBuffer = new Uint8Array(0); private bufferThreshold = 16384; // 16KB private stats: PlaybackStats = { streamingStartTime: 0, apiCallStartTime: 0, apiCallEndTime: 0, firstChunkTime: 0, playbackStartTime: 0, totalBytes: 0, totalChunks: 0, estimatedDuration: 0, }; /** * Start mpv player */ async startPlayer(): Promise { this.mpvProcess = spawn("mpv", ["--no-video", "fd://0"], { stdio: ["pipe", "pipe", "ignore"], }); // Monitor playback status this.mpvProcess.stdout?.on("data", (data) => { const output = data.toString(); if ( (output.includes("AO:") || output.includes("Playing")) && !this.isPlaybackActive ) { this.isPlaybackActive = true; this.stats.playbackStartTime = Date.now(); console.log("๐ŸŽต Playback started"); } if (output.includes("Exiting") || output.includes("EOF")) { this.isPlaybackActive = false; } }); this.mpvProcess.on("exit", () => { this.isPlaybackActive = false; }); this.mpvProcess.on("error", () => { this.isPlaybackActive = false; }); await new Promise((resolve) => setTimeout(resolve, 300)); this.isPlaying = true; } /** * Mark streaming start time */ startStreaming(): void { this.stats.streamingStartTime = Date.now(); } /** * Mark API call start time */ markApiCallStart(): void { this.stats.apiCallStartTime = Date.now(); } /** * Mark API call end time */ markApiCallEnd(): void { this.stats.apiCallEndTime = Date.now(); } /** * Add audio chunk to playback buffer */ addAudioChunk(chunkData: Uint8Array): void { if (!this.isPlaying || !this.mpvProcess?.stdin) return; // Record first chunk time if (this.stats.totalChunks === 0) { this.stats.firstChunkTime = Date.now(); this.parseWavHeader(chunkData); } this.stats.totalChunks++; this.stats.totalBytes += chunkData.length; // Initial buffering if (this.initialBuffer.length < this.bufferThreshold) { const newBuffer = new Uint8Array( this.initialBuffer.length + chunkData.length ); newBuffer.set(this.initialBuffer); newBuffer.set(chunkData, this.initialBuffer.length); this.initialBuffer = newBuffer; if (this.initialBuffer.length >= this.bufferThreshold) { this.writeToMpv(this.initialBuffer); this.initialBuffer = new Uint8Array(0); } return; } this.writeToMpv(chunkData); } /** * Extract audio information from WAV header */ private parseWavHeader(chunkData: Uint8Array): void { try { if (chunkData.length >= 44) { const riff = new TextDecoder().decode(chunkData.slice(0, 4)); const wave = new TextDecoder().decode(chunkData.slice(8, 12)); if (riff === "RIFF" && wave === "WAVE") { const sampleRate = new DataView( chunkData.buffer, chunkData.byteOffset + 24, 4 ).getUint32(0, true); const channels = new DataView( chunkData.buffer, chunkData.byteOffset + 22, 2 ).getUint16(0, true); this.stats.sampleRate = sampleRate; this.stats.channels = channels; } } } catch (error) { // Ignore parsing errors } } /** * Write data to mpv safely */ private writeToMpv(data: Uint8Array): void { try { if (this.mpvProcess?.stdin?.writable) { this.mpvProcess.stdin.write(Buffer.from(data)); } } catch (error) { // Ignore errors } } /** * Finish streaming and flush remaining buffers */ finishStreaming(): void { // Calculate estimated playback duration if (this.stats.sampleRate && this.stats.channels) { const bytesPerSecond = this.stats.sampleRate * this.stats.channels * 2; // 16bit this.stats.estimatedDuration = this.stats.totalBytes / bytesPerSecond; } if (this.initialBuffer.length > 0) { this.writeToMpv(this.initialBuffer); this.initialBuffer = new Uint8Array(0); } try { this.mpvProcess?.stdin?.end(); } catch (error) { // Ignore errors } } /** * Wait for playback completion (improved version) */ async waitForPlaybackComplete(): Promise { // Wait for playback to start let waitCount = 0; while (!this.isPlaybackActive && waitCount < 50) { await new Promise((resolve) => setTimeout(resolve, 100)); waitCount++; } if (!this.isPlaybackActive) { console.log("โš ๏ธ Playback did not start"); return; } // Ensure minimum wait based on estimated playback duration const minimumWaitTime = Math.max( this.stats.estimatedDuration * 1000, // Estimated playback duration 5000 // Minimum 5 seconds ); console.log( `โณ Waiting minimum ${(minimumWaitTime / 1000).toFixed( 1 )}s (based on estimated duration)` ); // Step 1: Wait for estimated playback duration await new Promise((resolve) => setTimeout(resolve, minimumWaitTime)); // Step 2: Wait for completion signal (additional max 10s) console.log("โณ Waiting for completion signal..."); waitCount = 0; while (this.isPlaybackActive && waitCount < 100) { await new Promise((resolve) => setTimeout(resolve, 100)); waitCount++; } console.log("โœ… Playback wait completed"); } /** * Stop the player */ async stopPlayer(): Promise { this.isPlaying = false; if (this.mpvProcess) { setTimeout(() => { if (this.mpvProcess && !this.mpvProcess.killed) { this.mpvProcess.kill(); } }, 1000); } } /** * Get playback statistics */ getStats(): PlaybackStats { return this.stats; } } /** * Streaming TTS + Playback */ async function simpleStreamingTts( voiceId: string, text: string, language: models.APIConvertTextToSpeechUsingCharacterRequestLanguage = models .APIConvertTextToSpeechUsingCharacterRequestLanguage.Ko, pronunciationDictionary?: PronunciationDictionaryEntry[] ): Promise { console.log(`๐Ÿ“ "${text.slice(0, 50)}${text.length > 50 ? "..." : ""}"`); console.log(`๐Ÿ“ Text length: ${text.length} characters`); console.log(`๐ŸŒ Language: ${language}`); if (pronunciationDictionary && pronunciationDictionary.length > 0) { console.log(`๐Ÿ“– Pronunciation dictionary: ${pronunciationDictionary.length} entries`); } const player = new SimpleMpvPlayer(); try { await player.startPlayer(); player.startStreaming(); const client = new Supertone({ apiKey: API_KEY }); // Mark API call start player.markApiCallStart(); console.log(" โฑ๏ธ API call started..."); const response = await client.textToSpeech.streamSpeech( { voiceId: voiceId, apiConvertTextToSpeechUsingCharacterRequest: { text: text, language: language, outputFormat: models.APIConvertTextToSpeechUsingCharacterRequestOutputFormat.Wav, style: "neutral", model: "sona_speech_1", }, }, pronunciationDictionary ? { pronunciationDictionary } : undefined ); // Mark API call end (response received) player.markApiCallEnd(); const apiCallTime = Date.now() - player.getStats().apiCallStartTime; console.log(` โฑ๏ธ API response received: ${apiCallTime}ms`); if (response?.result) { // Check if result is a ReadableStream if ( typeof response.result === "object" && "getReader" in response.result ) { const reader = ( response.result as ReadableStream ).getReader(); try { while (true) { const { done, value } = await reader.read(); if (done) break; if (value) { player.addAudioChunk(value); await new Promise((resolve) => setTimeout(resolve, 10)); } } } finally { reader.releaseLock(); } } else { console.log("โŒ Response is not a ReadableStream"); return false; } player.finishStreaming(); await player.waitForPlaybackComplete(); // Print statistics const stats = player.getStats(); const totalTime = Date.now() - stats.streamingStartTime; const apiResponseTime = stats.apiCallEndTime - stats.apiCallStartTime; const timeToFirstChunk = stats.firstChunkTime - stats.streamingStartTime; const firstChunkAfterResponse = stats.firstChunkTime - stats.apiCallEndTime; const timeToPlayback = stats.playbackStartTime - stats.streamingStartTime; console.log("๐Ÿ“Š Playback Statistics:"); console.log( ` ๐ŸŽค Total audio duration: ${stats.estimatedDuration.toFixed(1)}s` ); console.log( ` ๐Ÿ“ก API response time: ${apiResponseTime}ms (HTTP round-trip + init)` ); console.log( ` ๐Ÿ“ฆ Time to first chunk: ${timeToFirstChunk}ms (total), ${firstChunkAfterResponse}ms (after response)` ); console.log(` ๐ŸŽต Time to playback: ${timeToPlayback}ms`); console.log( ` ๐Ÿ“Š Total data: ${(stats.totalBytes / 1024).toFixed(1)}KB (${ stats.totalChunks } chunks)` ); return true; } else { console.log("โŒ No response"); return false; } } catch (error) { console.error("โŒ Error:", error); return false; } finally { await player.stopPlayer(); } } /** * Demo scenarios with various text lengths */ async function simpleDemo(): Promise { const voiceId = "91992bbd4758bdcf9c9b01"; const scenarios: string[] = [ "์•ˆ๋…•ํ•˜์„ธ์š”! ์‹ฌํ”Œํ•œ ํ…Œ์ŠคํŠธ์ž…๋‹ˆ๋‹ค.", "์‹ค์‹œ๊ฐ„ ํ…์ŠคํŠธ ์Œ์„ฑ ๋ณ€ํ™˜ ๊ธฐ์ˆ ์€ ์ •๋ง ๋†€๋ž์Šต๋‹ˆ๋‹ค. ์ด ๊ธฐ์ˆ ์„ ํ†ตํ•ด ๊ธด ํ…์ŠคํŠธ๋„ ์ฆ‰์‹œ ์Œ์„ฑ์œผ๋กœ ๋“ค์„ ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", "๋จธ์‹  ๋Ÿฌ๋‹๊ณผ ๋”ฅ ๋Ÿฌ๋‹์˜ ๋ฐœ์ „์€ ์ธ๊ณต์ง€๋Šฅ ๋ถ„์•ผ์— ํ˜์‹ ์ ์ธ ๋ณ€ํ™”๋ฅผ ๊ฐ€์ ธ์™”์Šต๋‹ˆ๋‹ค. ํŠนํžˆ ์ž์—ฐ์–ด ์ฒ˜๋ฆฌ ๊ธฐ์ˆ ์˜ ๋ฐœ๋‹ฌ๋กœ ์ธํ•ด ํ…์ŠคํŠธ ์Œ์„ฑ ๋ณ€ํ™˜ ํ’ˆ์งˆ์ด ๋น„์•ฝ์ ์œผ๋กœ ํ–ฅ์ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ตœ์‹  ์‹ ๊ฒฝ๋ง ๊ธฐ๋ฐ˜ TTS ๋ชจ๋ธ๋“ค์€ ์ธ๊ฐ„์˜ ๋ฐœ์Œ๊ณผ ์–ต์–‘์„ ๊ฑฐ์˜ ๊ตฌ๋ถ„ํ•  ์ˆ˜ ์—†์„ ์ •๋„๋กœ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋ชจ๋ฐฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", // Scenario 300+ characters (~380 chars) "ํ˜„๋Œ€ ์‚ฌํšŒ์—์„œ ์ธ๊ณต์ง€๋Šฅ ๊ธฐ์ˆ ์€ ์šฐ๋ฆฌ ์ผ์ƒ์ƒํ™œ์˜ ๋ชจ๋“  ์˜์—ญ์— ์Šค๋ฉฐ๋“ค๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ํŠนํžˆ ์Œ์„ฑ ํ•ฉ์„ฑ ๊ธฐ์ˆ ์€ ์‹œ๊ฐ ์žฅ์• ์ธ์„ ์œ„ํ•œ ์ ‘๊ทผ์„ฑ ๋„๊ตฌ์—์„œ๋ถ€ํ„ฐ ์—”ํ„ฐํ…Œ์ธ๋จผํŠธ ์‚ฐ์—…์˜ ์ฝ˜ํ…์ธ  ์ œ์ž‘๊นŒ์ง€ ๊ด‘๋ฒ”์œ„ํ•˜๊ฒŒ ํ™œ์šฉ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์‹ค์‹œ๊ฐ„ ์ŠคํŠธ๋ฆฌ๋ฐ ๊ธฐ์ˆ ๊ณผ ๊ฒฐํ•ฉ๋œ ํ…์ŠคํŠธ ์Œ์„ฑ ๋ณ€ํ™˜ ์‹œ์Šคํ…œ์€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํ˜์‹ ์ ์œผ๋กœ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” ์ „์ฒด ํ…์ŠคํŠธ์˜ ์Œ์„ฑ ๋ณ€ํ™˜์ด ์™„๋ฃŒ๋˜๊ธฐ๋ฅผ ๊ธฐ๋‹ค๋ฆด ํ•„์š” ์—†์ด, ์ฒซ ๋ฒˆ์งธ ์ฒญํฌ๊ฐ€ ์ฒ˜๋ฆฌ๋˜๋Š” ์ฆ‰์‹œ ์˜ค๋””์˜ค๋ฅผ ๋“ค์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ž๋™ ์ฒญํ‚น ์•Œ๊ณ ๋ฆฌ์ฆ˜์€ ํ…์ŠคํŠธ๋ฅผ ๋ฌธ๋งฅ๊ณผ ๋ฌธ์žฅ ๊ตฌ์กฐ๋ฅผ ๊ณ ๋ คํ•˜์—ฌ ์ ์ ˆํ•œ ํฌ๊ธฐ๋กœ ๋ถ„ํ• ํ•˜๋ฉฐ, ๊ฐ ์ฒญํฌ๋Š” ๋ณ‘๋ ฌ๋กœ ์ฒ˜๋ฆฌ๋˜์–ด ์ „์ฒด ์‘๋‹ต ์‹œ๊ฐ„์„ ๋Œ€ํญ ๋‹จ์ถ•์‹œํ‚ต๋‹ˆ๋‹ค.", // Scenario 500+ characters (~580 chars) "ํด๋ผ์šฐ๋“œ ์ปดํ“จํŒ… ํ™˜๊ฒฝ์—์„œ์˜ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜๋Š” ํ˜„๋Œ€ ์†Œํ”„ํŠธ์›จ์–ด ๊ฐœ๋ฐœ์˜ ํ•ต์‹ฌ ํŒจ๋Ÿฌ๋‹ค์ž„์œผ๋กœ ์ž๋ฆฌ์žก์•˜์Šต๋‹ˆ๋‹ค. ๋ชจ๋†€๋ฆฌ์‹ ์•„ํ‚คํ…์ฒ˜์™€ ๋‹ฌ๋ฆฌ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๋Š” ๊ฐ๊ฐ์˜ ๋…๋ฆฝ์ ์ธ ์„œ๋น„์Šค๊ฐ€ ํŠน์ • ๋น„์ฆˆ๋‹ˆ์Šค ๊ธฐ๋Šฅ์„ ๋‹ด๋‹นํ•˜๋ฉฐ, ์ด๋“ค์ด ๋„คํŠธ์›Œํฌ๋ฅผ ํ†ตํ•ด ํ†ต์‹ ํ•˜๋ฉด์„œ ์ „์ฒด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ์•„ํ‚คํ…์ฒ˜์˜ ๊ฐ€์žฅ ํฐ ์žฅ์ ์€ ํ™•์žฅ์„ฑ๊ณผ ์œ ์—ฐ์„ฑ์ž…๋‹ˆ๋‹ค. ๊ฐ ์„œ๋น„์Šค๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ๋ฐฐํฌ๋˜๊ณ  ํ™•์žฅ๋  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ํ•˜๋‚˜์˜ ์„œ๋น„์Šค์— ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•ด๋„ ์ „์ฒด ์‹œ์Šคํ…œ์— ๋ฏธ์น˜๋Š” ์˜ํ–ฅ์„ ์ตœ์†Œํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ ์„œ๋กœ ๋‹ค๋ฅธ ๊ธฐ์ˆ  ์Šคํƒ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด ๊ฐ ์„œ๋น„์Šค์˜ ์š”๊ตฌ์‚ฌํ•ญ์— ๊ฐ€์žฅ ์ ํ•ฉํ•œ ๊ธฐ์ˆ ์„ ์„ ํƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ปจํ…Œ์ด๋„ˆ ๊ธฐ์ˆ ์˜ ๋ฐœ์ „, ํŠนํžˆ ๋„์ปค์™€ ์ฟ ๋ฒ„๋„คํ‹ฐ์Šค์˜ ๋“ฑ์žฅ์€ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๋ฐฐํฌ์™€ ๊ด€๋ฆฌ๋ฅผ ๋”์šฑ ํšจ์œจ์ ์œผ๋กœ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. ์„œ๋น„์Šค ๋ฉ”์‹œ์™€ API ๊ฒŒ์ดํŠธ์›จ์ด ๊ฐ™์€ ๊ธฐ์ˆ ๋“ค์€ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๊ฐ„์˜ ํ†ต์‹ ์„ ๋”์šฑ ์•ˆ์ „ํ•˜๊ณ  ํšจ์œจ์ ์œผ๋กœ ๋งŒ๋“ค์–ด์ค๋‹ˆ๋‹ค.", // Scenario 800+ characters (~850 chars) "์˜›๋‚  ํ•œ ์ž‘์€ ๋งˆ์„์— ์ฒœ์žฌ์ ์ธ ์žฌ๋Šฅ์„ ๊ฐ€์ง„ ์ Š์€ ๊ฐœ๋ฐœ์ž๊ฐ€ ์‚ด๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ์˜ ์ด๋ฆ„์€ ๋ฏผ์ค€์ด์˜€๊ณ , ์–ด๋ฆด ๋•Œ๋ถ€ํ„ฐ ์ปดํ“จํ„ฐ์™€ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์— ๋‚จ๋‹ค๋ฅธ ๊ด€์‹ฌ์„ ๋ณด์˜€์Šต๋‹ˆ๋‹ค. ๋Œ€ํ•™์—์„œ ์ปดํ“จํ„ฐ ๊ณผํ•™์„ ์ „๊ณตํ•œ ๋ฏผ์ค€์€ ์กธ์—… ํ›„ ์Šคํƒ€ํŠธ์—…์— ์ž…์‚ฌํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๊ณณ์—์„œ ๊ทธ๋Š” ์ธ๊ณต์ง€๋Šฅ๊ณผ ์Œ์„ฑ ๊ธฐ์ˆ ์— ๋Œ€ํ•œ ๊นŠ์€ ์ง€์‹์„ ์Œ“๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์–ด๋А ๋‚ , ๋ฏผ์ค€์€ ์‹œ๊ฐ ์žฅ์• ๊ฐ€ ์žˆ๋Š” ์นœ๊ตฌ ์„œ์—ฐ์„ ๋งŒ๋‚ฌ์Šต๋‹ˆ๋‹ค. ์„œ์—ฐ์€ ์ธํ„ฐ๋„ท์˜ ์ˆ˜๋งŽ์€ ์ •๋ณด๋ฅผ ํ…์ŠคํŠธ๋กœ๋งŒ ์ ‘ํ•  ์ˆ˜ ์žˆ์–ด ๋งŽ์€ ๋ถˆํŽธํ•จ์„ ๊ฒช๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๋‹น์‹œ์˜ ์Œ์„ฑ ํ•ฉ์„ฑ ๊ธฐ์ˆ ์€ ๋กœ๋ด‡ ๊ฐ™์€ ๋ชฉ์†Œ๋ฆฌ๋ฅผ ๋‚ด๋ฉฐ, ๊ธด ํ…์ŠคํŠธ๋ฅผ ์ฝ์–ด์ฃผ๋ ค๋ฉด ๋ชจ๋“  ์ฒ˜๋ฆฌ๊ฐ€ ๋๋‚  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ ค์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ๋ณธ ๋ฏผ์ค€์€ ๋” ์ž์—ฐ์Šค๋Ÿฝ๊ณ  ๋น ๋ฅธ ์Œ์„ฑ ํ•ฉ์„ฑ ๊ธฐ์ˆ ์„ ๋งŒ๋“ค๊ธฐ๋กœ ๊ฒฐ์‹ฌํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ฐค๋‚ฎ์—†์ด ์—ฐ๊ตฌ์— ๋งค์ง„ํ•œ ๋ฏผ์ค€์€ ํ˜์‹ ์ ์ธ ์•„์ด๋””์–ด๋ฅผ ๋– ์˜ฌ๋ ธ์Šต๋‹ˆ๋‹ค. ๊ธด ํ…์ŠคํŠธ๋ฅผ ์ž‘์€ ๋‹จ์œ„๋กœ ๋‚˜๋ˆ„์–ด ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ณ , ์ฒซ ๋ฒˆ์งธ ๋ถ€๋ถ„์ด ์™„์„ฑ๋˜๋Š” ์ฆ‰์‹œ ์žฌ์ƒ์„ ์‹œ์ž‘ํ•˜๋Š” ์ŠคํŠธ๋ฆฌ๋ฐ ๋ฐฉ์‹์ด์—ˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ธฐ์ˆ ์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด ๊ทธ๋Š” ์ตœ์‹  ๋”ฅ๋Ÿฌ๋‹ ๋ชจ๋ธ๊ณผ ์‹ ๊ฒฝ๋ง ์•„ํ‚คํ…์ฒ˜๋ฅผ ์—ฐ๊ตฌํ–ˆ์Šต๋‹ˆ๋‹ค. ์ˆ˜๋งŽ์€ ์‹œํ–‰์ฐฉ์˜ค๋ฅผ ๊ฑฐ์ณ ๋งˆ์นจ๋‚ด ์ž์—ฐ์Šค๋Ÿฌ์šด ์Œ์„ฑ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋Š” ์‹œ์Šคํ…œ์„ ์™„์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ์˜ ๊ธฐ์ˆ ์€ ๋ฌธ์žฅ์˜ ๋ฌธ๋งฅ๊ณผ ๊ฐ์ •๊นŒ์ง€ ์ดํ•ดํ•˜์—ฌ ์ ์ ˆํ•œ ์–ต์–‘๊ณผ ์†๋„๋กœ ์ฝ์–ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.", ]; // Additional test scenarios for word-based and character-based chunking const additionalScenarios: Array<{ text: string; label: string; category: string; language: models.APIConvertTextToSpeechUsingCharacterRequestLanguage; pronunciationDictionary?: PronunciationDictionaryEntry[]; }> = [ { // Korean text WITHOUT punctuation to test word-based chunking // Text length: ~450 characters (exceeds 300 char limit) text: "์ด๊ฒƒ์€ ๊ตฌ๋‘์  ์—†์ด ๋งค์šฐ ๊ธด ๋ฌธ์žฅ์„ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ์œผ๋กœ ์‚ผ๋ฐฑ ๊ธ€์ž๋ฅผ ์ดˆ๊ณผํ•˜๋Š” ํ…์ŠคํŠธ์—์„œ ๋‹จ์–ด ๊ธฐ๋ฐ˜ ๋ถ„ํ• ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค ์ด๋Ÿฌํ•œ ๊ฒฝ์šฐ SDK๋Š” ๋ฌธ์žฅ ๊ฒฝ๊ณ„ ๋Œ€์‹  ๋‹จ์–ด ๊ฒฝ๊ณ„๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ…์ŠคํŠธ๋ฅผ ์ ์ ˆํ•œ ํฌ๊ธฐ๋กœ ๋‚˜๋ˆ„์–ด์•ผ ํ•˜๋ฉฐ ์ด๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์ƒ์„ฑํ•œ ์ฝ˜ํ…์ธ ์—์„œ ํ”ํžˆ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์ƒํ™ฉ์ž…๋‹ˆ๋‹ค ์˜ˆ๋ฅผ ๋“ค์–ด ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€๋‚˜ ๋น„๊ณต์‹์ ์ธ ํ…์ŠคํŠธ ์ž…๋ ฅ์—์„œ๋Š” ์˜ฌ๋ฐ”๋ฅธ ๋ฌธ๋ฒ•๊ณผ ๊ตฌ๋‘์ ์ด ํ•ญ์ƒ ๋ณด์žฅ๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค ๋˜ํ•œ ์‹ค์‹œ๊ฐ„ ์ŠคํŠธ๋ฆฌ๋ฐ ํ™˜๊ฒฝ์—์„œ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๋น ๋ฅด๊ฒŒ ์ž…๋ ฅํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„์„œ ๊ตฌ๋‘์ ์„ ์ƒ๋žตํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋นˆ๋ฒˆํ•˜๊ฒŒ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค ์ด๋Ÿฌํ•œ ์ƒํ™ฉ์—์„œ๋„ SDK๋Š” ์•ˆ์ •์ ์œผ๋กœ ํ…์ŠคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  ์ž์—ฐ์Šค๋Ÿฌ์šด ์Œ์„ฑ์„ ์ƒ์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค ๋”ฐ๋ผ์„œ ๋‹จ์–ด ๊ธฐ๋ฐ˜ ๋ถ„ํ•  ๊ธฐ๋Šฅ์€ ๋งค์šฐ ์ค‘์š”ํ•œ ์—ญํ• ์„ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค", label: "Long sentence without punctuation (Word-based chunking, 450+ chars)", category: "Word-based Chunking Test", language: models.APIConvertTextToSpeechUsingCharacterRequestLanguage.Ko, }, { // Japanese text WITHOUT punctuation marks (ใ€‚๏ผ๏ผŸetc) to test pure character-based chunking // Text length: ~450 characters (exceeds 300 char limit) text: "ๆ—ฅๆœฌ่ชžใฎใƒ†ใ‚ญใ‚นใƒˆใฏ้€šๅธธใ‚นใƒšใƒผใ‚นใ‚’ๅซใพใชใ„ใŸใ‚็‰นๅˆฅใชๅ‡ฆ็†ใŒๅฟ…่ฆใงใ™ใ“ใฎใƒ†ใ‚นใƒˆใฏไธ‰็™พๆ–‡ๅญ—ใ‚’่ถ…ใˆใ‚‹้•ทใ„ๆ—ฅๆœฌ่ชžใƒ†ใ‚ญใ‚นใƒˆใŒๆญฃใ—ใๅ‡ฆ็†ใ•ใ‚Œใ‚‹ใ“ใจใ‚’็ขบ่ชใ—ใพใ™่‡ช็„ถ่จ€่ชžๅ‡ฆ็†ๆŠ€่ก“ใฎ็™บๅฑ•ใซใ‚ˆใ‚Š้Ÿณๅฃฐๅˆๆˆใฎๅ“่ณชใฏๅคงๅน…ใซๅ‘ไธŠใ—ใพใ—ใŸ็‰นใซใƒ‡ใ‚ฃใƒผใƒ—ใƒฉใƒผใƒ‹ใƒณใ‚ฐใ‚’ๆดป็”จใ—ใŸๆœ€ๆ–ฐใฎใƒ†ใ‚ญใ‚นใƒˆ้Ÿณๅฃฐๅค‰ๆ›ใ‚ทใ‚นใƒ†ใƒ ใฏไบบ้–“ใฎ็™บ่ฉฑใซ้žๅธธใซ่ฟ‘ใ„่‡ช็„ถใช้Ÿณๅฃฐใ‚’็”Ÿๆˆใงใใพใ™ใ‚นใƒšใƒผใ‚นใŒใชใ„่จ€่ชžใงใฏๆ–‡ๅญ—ๅ˜ไฝใงใฎๅˆ†ๅ‰ฒใŒๅฟ…่ฆใงใ‚ใ‚Šใ“ใฎSDKใฏใใฎใ‚ˆใ†ใช็Šถๆณใ‚’่‡ชๅ‹•็š„ใซๆคœๅ‡บใ—ใฆ้ฉๅˆ‡ใซๅ‡ฆ็†ใ—ใพใ™ใ“ใ‚Œใซใ‚ˆใ‚Šๆ—ฅๆœฌ่ชžไธญๅ›ฝ่ชž้Ÿ“ๅ›ฝ่ชžใชใฉใฎใ‚ขใ‚ธใ‚ข่จ€่ชžใงใ‚‚ๅ•้กŒใชใ้•ทใ„ใƒ†ใ‚ญใ‚นใƒˆใ‚’้Ÿณๅฃฐใซๅค‰ๆ›ใ™ใ‚‹ใ“ใจใŒใงใใพใ™้ŸณๅฃฐๅˆๆˆๆŠ€่ก“ใฏ่ฆ–่ฆš้šœๅฎณ่€…ใฎใŸใ‚ใฎใ‚ขใ‚ฏใ‚ปใ‚ทใƒ“ใƒชใƒ†ใ‚ฃใƒ„ใƒผใƒซใ‹ใ‚‰ๅฏพ่ฉฑๅž‹AIใ‚ขใ‚ทใ‚นใ‚ฟใƒณใƒˆใพใงๅน…ๅบƒใ„็”จ้€”ใงๆดป็”จใ•ใ‚Œใฆใ„ใพใ™ใ•ใ‚‰ใซใƒชใ‚ขใƒซใ‚ฟใ‚คใƒ ใ‚นใƒˆใƒชใƒผใƒŸใƒณใ‚ฐๆŠ€่ก“ใจ็ต„ใฟๅˆใ‚ใ›ใ‚‹ใ“ใจใงๅพ…ใกๆ™‚้–“ใ‚’ๅคงๅน…ใซ็Ÿญ็ธฎใ—ๅ„ชใ‚ŒใŸใƒฆใƒผใ‚ถใƒผไฝ“้จ“ใ‚’ๆไพ›ใ™ใ‚‹ใ“ใจใŒใงใใพใ™ๆœ€ๆ–ฐใฎ้ŸณๅฃฐๅˆๆˆๆŠ€่ก“ใฏๆ„Ÿๆƒ…ใ‚„ๆŠ‘ๆšใ‚‚่‡ช็„ถใซ่กจ็พใงใใ‚‹ใ‚ˆใ†ใซใชใ‚Šใพใ—ใŸ", label: "Japanese text without spaces AND punctuation (Character-based chunking, 450+ chars)", category: "Character-based Chunking Test", language: models.APIConvertTextToSpeechUsingCharacterRequestLanguage.Ja, }, { // English text with ellipsis punctuation (โ€ฆ โ€ฅ) - tests fix/text_utils multilingual punctuation // Text length: ~380 characters (exceeds 300 char limit) text: "Sometimes we need to pause and thinkโ€ฆ The ellipsis character is used to indicate a trailing thought or a pause in speechโ€ฆ This test verifies that the text chunking system correctly handles Unicode ellipsis charactersโ€ฅ There are multiple types of ellipsis in Unicodeโ€ฆ The horizontal ellipsis and the two dot leader are both supportedโ€ฅ When processing long texts the SDK should split at these punctuation marksโ€ฆ This ensures natural pauses in the generated speech outputโ€ฅ Let us verify everything works correctlyโ€ฆ", label: "Ellipsis punctuation test (โ€ฆ โ€ฅ) - 380+ chars", category: "Multilingual Punctuation Test", language: models.APIConvertTextToSpeechUsingCharacterRequestLanguage.En, }, { // Korean text with ellipsis (โ€ฆ) - tests Korean with Unicode ellipsis // Text length: ~350 characters (exceeds 300 char limit) text: "ํ•œ๊ตญ์–ด ํ…์ŠคํŠธ์—์„œ ๋ง์ค„์ž„ํ‘œ๋Š” ์ƒ๊ฐ์˜ ํ๋ฆ„์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹คโ€ฆ ์ด ํ…Œ์ŠคํŠธ๋Š” ์œ ๋‹ˆ์ฝ”๋“œ ๋ง์ค„์ž„ํ‘œ ๋ฌธ์ž๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ฒ˜๋ฆฌ๋˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹คโ€ฆ ์ธ๊ณต์ง€๋Šฅ ๊ธฐ์ˆ ์ด ๋ฐœ์ „ํ•˜๋ฉด์„œ ์Œ์„ฑ ํ•ฉ์„ฑ์˜ ํ’ˆ์งˆ๋„ ํฌ๊ฒŒ ํ–ฅ์ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹คโ€ฆ ํŠนํžˆ ๋”ฅ๋Ÿฌ๋‹์„ ํ™œ์šฉํ•œ ์ตœ์‹  ์‹œ์Šคํ…œ์€ ๋งค์šฐ ์ž์—ฐ์Šค๋Ÿฌ์šด ์Œ์„ฑ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹คโ€ฆ ๊ธด ํ…์ŠคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•  ๋•Œ SDK๋Š” ์ด๋Ÿฌํ•œ ๊ตฌ๋‘์ ์—์„œ ์ ์ ˆํžˆ ๋ถ„ํ• ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹คโ€ฆ ์ด๋ฅผ ํ†ตํ•ด ์ž์—ฐ์Šค๋Ÿฌ์šด ์Œ์„ฑ ์ถœ๋ ฅ์ด ๊ฐ€๋Šฅํ•ด์ง‘๋‹ˆ๋‹คโ€ฆ ์‹ค์‹œ๊ฐ„ ์ŠคํŠธ๋ฆฌ๋ฐ ๊ธฐ์ˆ ๊ณผ ๊ฒฐํ•ฉํ•˜๋ฉด ๋”์šฑ ๋น ๋ฅธ ์‘๋‹ต์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹คโ€ฆ ์Œ์„ฑ ํ•ฉ์„ฑ ๊ธฐ์ˆ ์€ ์ ‘๊ทผ์„ฑ ๋„๊ตฌ๋ถ€ํ„ฐ AI ์–ด์‹œ์Šคํ„ดํŠธ๊นŒ์ง€ ๋‹ค์–‘ํ•˜๊ฒŒ ํ™œ์šฉ๋ฉ๋‹ˆ๋‹คโ€ฆ ๋ชจ๋“  ๊ฒƒ์ด ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•ด ๋ด…์‹œ๋‹คโ€ฆ", label: "Korean ellipsis punctuation test (โ€ฆ) - 350+ chars", category: "Multilingual Punctuation Test", language: models.APIConvertTextToSpeechUsingCharacterRequestLanguage.Ko, }, { // Japanese text WITH CJK punctuation (ใ€‚๏ผ๏ผŸ) - tests CJK punctuation splitting // Text length: ~320 characters (exceeds 300 char limit) text: "ๆ—ฅๆœฌ่ชžใฎใƒ†ใ‚ญใ‚นใƒˆใฏ้€šๅธธใ‚นใƒšใƒผใ‚นใ‚’ๅซใพใชใ„ใŸใ‚็‰นๅˆฅใชๅ‡ฆ็†ใŒๅฟ…่ฆใงใ™ใ€‚ใ“ใฎใƒ†ใ‚นใƒˆใฏๆ—ฅๆœฌ่ชžใฎๅฅ่ชญ็‚นใงๆญฃใ—ใๅˆ†ๅ‰ฒใ•ใ‚Œใ‚‹ใ“ใจใ‚’็ขบ่ชใ—ใพใ™ใ€‚่‡ช็„ถ่จ€่ชžๅ‡ฆ็†ๆŠ€่ก“ใฎ็™บๅฑ•ใซใ‚ˆใ‚Š้Ÿณๅฃฐๅˆๆˆใฎๅ“่ณชใฏๅคงๅน…ใซๅ‘ไธŠใ—ใพใ—ใŸใ€‚็‰นใซใƒ‡ใ‚ฃใƒผใƒ—ใƒฉใƒผใƒ‹ใƒณใ‚ฐใ‚’ๆดป็”จใ—ใŸๆœ€ๆ–ฐใฎใƒ†ใ‚ญใ‚นใƒˆ้Ÿณๅฃฐๅค‰ๆ›ใ‚ทใ‚นใƒ†ใƒ ใฏไบบ้–“ใฎ็™บ่ฉฑใซ้žๅธธใซ่ฟ‘ใ„่‡ช็„ถใช้Ÿณๅฃฐใ‚’็”Ÿๆˆใงใใพใ™ใ€‚ใ‚นใƒšใƒผใ‚นใŒใชใ„่จ€่ชžใงใฏๅฅ่ชญ็‚นใงใฎๅˆ†ๅ‰ฒใŒ้‡่ฆใงใ™ใ€‚ใ“ใฎSDKใฏใใฎใ‚ˆใ†ใช็Šถๆณใ‚’่‡ชๅ‹•็š„ใซๆคœๅ‡บใ—ใฆ้ฉๅˆ‡ใซๅ‡ฆ็†ใ—ใพใ™ใ€‚ใƒชใ‚ขใƒซใ‚ฟใ‚คใƒ ใ‚นใƒˆใƒชใƒผใƒŸใƒณใ‚ฐๆŠ€่ก“ใจ็ต„ใฟๅˆใ‚ใ›ใ‚‹ใ“ใจใงๅพ…ใกๆ™‚้–“ใ‚’ๅคงๅน…ใซ็Ÿญ็ธฎใงใใพใ™ใ€‚ใ“ใ‚Œใซใ‚ˆใ‚Šๆ—ฅๆœฌ่ชžใงใ‚‚ๅ•้กŒใชใ้•ทใ„ใƒ†ใ‚ญใ‚นใƒˆใ‚’้Ÿณๅฃฐใซๅค‰ๆ›ใ™ใ‚‹ใ“ใจใŒใงใใพใ™ใ€‚้ŸณๅฃฐๅˆๆˆๆŠ€่ก“ใฎๆœชๆฅใฏใจใฆใ‚‚ๆ˜Žใ‚‹ใ„ใงใ™ใ€‚", label: "Japanese CJK punctuation test (ใ€‚) - 320+ chars", category: "Multilingual Punctuation Test", language: models.APIConvertTextToSpeechUsingCharacterRequestLanguage.Ja, }, // Pronunciation Dictionary Tests { // Basic pronunciation dictionary test with partial_match=true/false text: "The CEO of OpenAI announced that GPT models are improving. Dr. Smith from MIT said AI research is accelerating.", label: "Pronunciation dictionary (partial_match=true/false)", category: "Pronunciation Dictionary Test", language: models.APIConvertTextToSpeechUsingCharacterRequestLanguage.En, pronunciationDictionary: [ // partial_match=false: exact word boundary match { text: "CEO", pronunciation: "Chief Executive Officer", partial_match: false }, { text: "MIT", pronunciation: "Massachusetts Institute of Technology", partial_match: false }, { text: "AI", pronunciation: "Artificial Intelligence", partial_match: false }, // partial_match=true: substring match { text: "GPT", pronunciation: "Generative Pre-trained Transformer", partial_match: true }, { text: "Dr.", pronunciation: "Doctor", partial_match: true }, ], }, { // Pronunciation dictionary causing text expansion to exceed 300 chars (triggers chunking) // Original text: ~190 chars, After expansion: 400+ chars text: "AI and ML are revolutionizing tech. The CEO discussed GPT advancements. Dr. Kim from MIT explained how NLP and CV work together. AWS and GCP provide cloud AI services.", label: "Pronunciation dictionary + Long text chunking (~190 chars -> 400+ chars)", category: "Pronunciation Dictionary + Chunking Test", language: models.APIConvertTextToSpeechUsingCharacterRequestLanguage.En, pronunciationDictionary: [ // partial_match=false: exact word boundary matches { text: "AI", pronunciation: "Artificial Intelligence", partial_match: false }, { text: "ML", pronunciation: "Machine Learning", partial_match: false }, { text: "CEO", pronunciation: "Chief Executive Officer", partial_match: false }, { text: "MIT", pronunciation: "Massachusetts Institute of Technology", partial_match: false }, { text: "NLP", pronunciation: "Natural Language Processing", partial_match: false }, { text: "CV", pronunciation: "Computer Vision", partial_match: false }, { text: "AWS", pronunciation: "Amazon Web Services", partial_match: false }, { text: "GCP", pronunciation: "Google Cloud Platform", partial_match: false }, // partial_match=true: substring matches { text: "GPT", pronunciation: "Generative Pre-trained Transformer", partial_match: true }, { text: "Dr.", pronunciation: "Doctor", partial_match: true }, { text: "tech", pronunciation: "technology", partial_match: true }, ], }, { // Korean pronunciation dictionary test text: "SK์™€ LG์˜ CEO๊ฐ€ AI์™€ ML ๊ธฐ์ˆ ์— ๋Œ€ํ•ด ๋ฐœํ‘œํ–ˆ์Šต๋‹ˆ๋‹ค. Dr. ๊น€ ๋ฐ•์‚ฌ๊ฐ€ MIT์—์„œ NLP ์—ฐ๊ตฌ ์„ฑ๊ณผ๋ฅผ ๊ณต๊ฐœํ–ˆ์Šต๋‹ˆ๋‹ค.", label: "Korean pronunciation dictionary test", category: "Pronunciation Dictionary Test", language: models.APIConvertTextToSpeechUsingCharacterRequestLanguage.Ko, pronunciationDictionary: [ { text: "SK", pronunciation: "์—์Šค์ผ€์ด", partial_match: false }, { text: "LG", pronunciation: "์—˜์ง€", partial_match: false }, { text: "CEO", pronunciation: "์ตœ๊ณ ๊ฒฝ์˜์ž", partial_match: false }, { text: "AI", pronunciation: "์ธ๊ณต์ง€๋Šฅ", partial_match: false }, { text: "ML", pronunciation: "๋จธ์‹ ๋Ÿฌ๋‹", partial_match: false }, { text: "MIT", pronunciation: "๋งค์‚ฌ์ถ”์„ธ์ธ  ๊ณต๊ณผ๋Œ€ํ•™๊ต", partial_match: false }, { text: "NLP", pronunciation: "์ž์—ฐ์–ด์ฒ˜๋ฆฌ", partial_match: false }, { text: "Dr.", pronunciation: "๋‹ฅํ„ฐ", partial_match: true }, ], }, ]; for (let i = 0; i < scenarios.length; i++) { console.log(`\n๐Ÿ”ฅ Scenario ${i + 1}/${scenarios.length}`); // Display category based on text length const textLength = scenarios[i].length; let category = ""; if (textLength < 100) category = "Short Text"; else if (textLength < 300) category = "Medium Text"; else if (textLength < 500) category = "Long Text (300+ chars)"; else if (textLength < 800) category = "Very Long Text (500+ chars)"; else category = "Extra Long Text (800+ chars)"; console.log(`๐Ÿ“‚ Category: ${category}`); console.log("โ”€".repeat(50)); const success = await simpleStreamingTts(voiceId, scenarios[i]); if (!success) { console.log(`โŒ Scenario ${i + 1} failed`); break; } if (i < scenarios.length - 1) { console.log("\nโณ Waiting..."); await new Promise((resolve) => setTimeout(resolve, 2000)); } } // Run additional scenarios for chunking tests console.log("\n" + "=".repeat(60)); console.log("๐Ÿ”ง Additional Chunking Test Scenarios"); console.log("=".repeat(60)); for (let i = 0; i < additionalScenarios.length; i++) { const scenario = additionalScenarios[i]; console.log( `\n๐Ÿ”ฌ Additional Scenario ${i + 1}/${additionalScenarios.length}` ); console.log(`๐Ÿ“‚ Category: ${scenario.category}`); console.log(`๐Ÿ“ ${scenario.label}`); console.log("โ”€".repeat(50)); const success = await simpleStreamingTts( voiceId, scenario.text, scenario.language, scenario.pronunciationDictionary ); if (!success) { console.log(`โŒ Additional scenario ${i + 1} failed`); } if (i < additionalScenarios.length - 1) { console.log("\nโณ Waiting..."); await new Promise((resolve) => setTimeout(resolve, 2000)); } } console.log("\n๐ŸŽ‰ Demo completed!"); console.log("\n๐Ÿ“Š Tested text length ranges:"); console.log(" โ€ข Short text: ~100 chars"); console.log(" โ€ข Medium text: 100~300 chars"); console.log(" โ€ข Long text: 300~500 chars"); console.log(" โ€ข Very long text: 500~800 chars"); console.log(" โ€ข Extra long text: 800+ chars"); console.log("\n๐Ÿ”ง Chunking strategy tests:"); console.log(" โ€ข Word-based chunking: Long sentences without punctuation"); console.log( " โ€ข Character-based chunking: Japanese/Chinese text without spaces" ); console.log("\n๐ŸŒ Multilingual punctuation tests:"); console.log(" โ€ข Ellipsis: English (โ€ฆ โ€ฅ)"); console.log(" โ€ข Korean ellipsis: Korean (โ€ฆ)"); console.log(" โ€ข CJK punctuation: Japanese (ใ€‚)"); console.log("\n๐Ÿ“– Pronunciation dictionary tests:"); console.log(" โ€ข partial_match=false: Word boundary matching (CEO, MIT, AI)"); console.log(" โ€ข partial_match=true: Substring matching (GPT, Dr., tech)"); console.log(" โ€ข Long text chunking: Text expansion exceeding 300 chars"); console.log(" โ€ข Korean pronunciation: SK, LG, CEO, AI, ML, MIT, NLP"); } /** * Check if mpv is installed */ async function checkMpv(): Promise { try { const test = spawn("mpv", ["--version"], { stdio: "ignore" }); return new Promise((resolve) => { test.on("exit", (code) => resolve(code === 0)); test.on("error", () => resolve(false)); }); } catch { return false; } } /** * Main entry point */ async function main(): Promise { console.log("๐ŸŽต Real-time TTS Player"); console.log("=".repeat(30)); // Check API key if (!API_KEY) { console.error("โŒ API key is not configured."); console.error(" Please set SUPERTONE_API_KEY in the .env file."); process.exit(1); } if (!(await checkMpv())) { console.error("โŒ mpv is required: brew install mpv"); process.exit(1); } console.log("โœ… Ready"); try { await simpleDemo(); } catch (error) { console.error("โŒ Error:", error); } } // Execute if (import.meta.url === `file://${process.argv[1]}`) { main().catch(console.error); } export { SimpleMpvPlayer, simpleStreamingTts };