JavaScript에서 텍스트를 음성으로 바꾸려면 SpeechSynthesisUtterance를 만들고 window.speechSynthesis.speak()에 전달하세요. 브라우저가 재생을 처리합니다. API 키, 서버, JavaScript 패키지가 필요하지 않습니다.
기기에서 제공하는 목소리로 충분한 프로토타입이나 소리 내어 읽기 기능에 적합합니다. 기기마다 같은 목소리가 필요하거나 전화 통화로 오디오를 스트리밍하는 음성 에이전트라면 호스팅 음성 합성 API를 사용하세요. 브라우저의 SpeechSynthesis와 Cartesia API는 서로 다른 음성 생성 방식입니다.
브라우저 음성 합성의 동작 원리
window.speechSynthesis는 Web Speech API에서 합성 음성을 제어합니다. utterance는 요청 하나의 텍스트와 설정을 담습니다. speak()를 호출하면 발화가 큐에 추가되며 오디오 파일을 반환하지 않습니다.1
브라우저는 getVoices()로 현재 기기의 목소리 목록을 제공합니다. 스크립트 실행 뒤에 목록이 로드될 수 있으므로 즉시 읽고 voiceschanged가 발생하면 갱신하세요.2 자신의 노트북에 있는 목소리 이름을 고정해 놓고 고객의 휴대전화에도 있을 것으로 기대하지 마세요.
1. 작동하는 음성 합성 데모 저장
다음 코드를 speech.html로 저장하고 브라우저에서 여세요. 짧은 문장을 입력하고 목소리 목록이 있다면 하나를 선택한 뒤 Speak를 누르세요. 일부 목소리는 원격 음성 서비스를 사용하므로 민감하지 않은 예시 텍스트를 쓰세요.3
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>JavaScript text-to-speech demo</title>
</head>
<body>
<h1>Read text aloud</h1>
<p>
<label for="text">Text to speak</label><br />
<textarea id="text" rows="4" cols="30" maxlength="500">
Hello from your browser.</textarea
>
</p>
<p>
<label for="voice">Voice</label>
<select id="voice"></select>
</p>
<button id="speak" type="button">Speak</button>
<button id="stop" type="button">Stop</button>
<p id="status" role="status" aria-live="polite">Ready.</p>
<script>
const text = document.querySelector("#text");
const voiceSelect = document.querySelector("#voice");
const speakButton = document.querySelector("#speak");
const stopButton = document.querySelector("#stop");
const status = document.querySelector("#status");
let voices = [];
let currentUtterance = null;
if (
!("speechSynthesis" in window) ||
!("SpeechSynthesisUtterance" in window)
) {
status.textContent = "This browser does not support speech synthesis.";
speakButton.disabled = true;
stopButton.disabled = true;
} else {
const synth = window.speechSynthesis;
function loadVoices() {
const previousVoice = voiceSelect.value;
voices = synth.getVoices();
voiceSelect.replaceChildren(new Option("Browser default", ""));
for (const voice of voices) {
voiceSelect.add(
new Option(`${voice.name} (${voice.lang})`, voice.voiceURI),
);
}
if (voices.some((voice) => voice.voiceURI === previousVoice)) {
voiceSelect.value = previousVoice;
}
}
loadVoices();
synth.addEventListener("voiceschanged", loadVoices);
speakButton.addEventListener("click", () => {
const transcript = text.value.trim();
if (!transcript) {
status.textContent = "Enter some text first.";
return;
}
// Ignore callbacks from the utterance being replaced.
currentUtterance = null;
synth.cancel();
const utterance = new SpeechSynthesisUtterance(transcript);
const voice = voices.find(
(item) => item.voiceURI === voiceSelect.value,
);
if (voice) {
utterance.voice = voice;
utterance.lang = voice.lang;
} else {
utterance.lang = document.documentElement.lang;
}
utterance.rate = 1;
currentUtterance = utterance;
status.textContent = "Starting speech...";
utterance.onstart = () => {
if (currentUtterance === utterance)
status.textContent = "Speaking...";
};
utterance.onend = () => {
if (currentUtterance !== utterance) return;
currentUtterance = null;
status.textContent = "Finished.";
};
utterance.onerror = (event) => {
if (currentUtterance !== utterance) return;
currentUtterance = null;
status.textContent = `Speech failed: ${event.error}`;
};
synth.speak(utterance);
});
stopButton.addEventListener("click", () => {
currentUtterance = null;
synth.cancel();
status.textContent = "Stopped.";
});
}
</script>
</body>
</html>
첫 테스트가 짧도록 데모의 입력을 500자로 제한했습니다. Web Speech API의 한도가 아니라 데모의 제한입니다. 지원하는 각 브라우저에서 긴 문단은 별도로 테스트하세요.
2. 목소리 선택과 재생 확인
선택 메뉴에는 각 목소리의 이름과 언어가 표시됩니다. 텍스트 언어에 맞는 목소리를 고르세요. utterance.lang을 설정해도 단어가 번역되지는 않습니다.
Speak는 페이지 로드 시점이 아니라 버튼을 눌렀을 때 실행됩니다. 청자가 제어할 수 있고 자동 재생에 의존하지 않습니다. Speak를 다시 누르면 반복 클릭을 큐에 쌓지 않고 현재 요청을 취소한 뒤 새 요청을 시작합니다. Stop은 cancel()을 호출하여 큐의 발화를 지우고 현재 음성을 멈춥니다.4
currentUtterance 검사는 취소한 요청의 늦은 콜백이 새 요청의 상태를 덮어쓰지 못하게 합니다. 이 코드를 컴포넌트로 바꾸면 언마운트할 때 voiceschanged 리스너를 제거하세요. 해당 컴포넌트가 페이지의 음성 큐를 소유한 경우에만 언마운트 시 재생을 취소하세요.
3. 소리가 없거나 동작이 일정하지 않을 때
| 증상 | 확인할 내용 |
|---|---|
| 목소리 선택에 Browser default만 표시됨 | voiceschanged를 기다리세요. 목록이 계속 비어 있으면 브라우저 지원과 설치된 시스템 목소리를 확인하세요. 데모는 여전히 기본 목소리를 요청할 수 있습니다. |
| Speak를 눌러도 소리가 없음 | 기기 음량과 오디오 출력을 확인하고, 맞는 언어의 짧은 문장으로 테스트한 뒤 상태 메시지에 합성 오류가 있는지 확인하세요. |
| 데스크톱에 있는 목소리가 모바일에는 없음 | 목소리 목록은 기기마다 다릅니다. 기본 옵션을 유지하고 실제 지원 운영체제에서 테스트하세요. |
| 반복 클릭으로 발화가 늦어짐 | speak()는 요청을 큐에 넣습니다. 새 텍스트로 바꾸려면 이전 요청을 먼저 취소하세요. |
| 인터넷 연결이 없으면 발화 실패 | 선택한 목소리가 원격 서비스를 사용할 수 있습니다. 로컬 합성이라고 가정하지 말고 voice.localService를 확인하고 오프라인에서 테스트하세요. |
소리 내어 읽기 기능에서는 원본 텍스트를 계속 보여 주고 키보드로 Stop에 접근할 수 있게 하세요. 음성을 자동으로 시작하거나 합성 음성을 화면 읽기 프로그램 지원의 대체물로 취급하지 마세요.
호스팅 TTS API를 사용할 때
청자에게 제공되는 목소리로 충분하고 해당 기기에서 재생만 하면 된다면 브라우저 음성을 선택하세요. 생성된 오디오 바이트, 제공업체가 관리하는 목소리 정체성, 브라우저 밖으로 전달하는 오디오가 필요하다면 호스팅 API를 선택하세요.
| 요구사항 | 브라우저 SpeechSynthesis | Cartesia 같은 호스팅 TTS API |
|---|---|---|
| 자격 증명 | 애플리케이션 API 키 불필요 | API 자격 증명과 사용 권한이 있는 계정 |
| 목소리 선택 | 브라우저와 운영체제의 목소리 목록 | 제공업체의 음성 라이브러리 또는 허가된 맞춤 목소리 |
| 오디오 전달 위치 | 브라우저 재생 | 애플리케이션이 오디오를 받아 재생, 저장, 전송 |
| 오프라인 동작 | 목소리와 기기에 따라 다름 | Cartesia API 요청에는 네트워크 연결 필요 |
| 텍스트 처리 | 목소리에 따라 로컬 또는 원격 | 제공업체로 텍스트 전송. 통합 전에 데이터 요구사항 검토 |
Cartesia 음성 합성은 백엔드에서 API를 호출하세요. API 키는 공개 HTML 파일이나 클라이언트 번들이 아닌 백엔드에 보관하세요. 브라우저 앱은 인증된 백엔드로 텍스트를 보내고, 백엔드는 입력 한도를 적용한 뒤 클라이언트에 오디오를 반환할 수 있습니다. 방문자가 API 크레딧을 무제한으로 쓰지 못하도록 해당 엔드포인트의 요청을 제한하세요.
Cartesia Python SDK로 WAV 파일을 생성하는 것부터 시작하세요. 해당 가이드는 호스팅 생성 경로를 테스트하며 스트리밍 브라우저 플레이어는 아닙니다. 대화형 앱에는 재생 버퍼링과 끼어들기 처리도 필요합니다. 전체 음성 에이전트 파이프라인을 직접 만들기 전에 그 작업을 Cartesia Managed Agents와 비교하세요.
관련 자료
각주
-
MDN, SpeechSynthesis. ↩
-
MDN, SpeechSynthesis.getVoices(). ↩
-
MDN, SpeechSynthesis.cancel(). ↩