활용 방법
타입캐스트 API와 Google Sheets를 사용하면 다음을 할 수 있습니다:
- 배치 TTS 생성 - 한 번의 클릭으로 여러 텍스트를 음성으로 변환
- 워크플로우 자동화 - 데이터를 처리하고 자동으로 오디오 생성
- 코딩 불필요 - 간단한 사용자 정의 메뉴 인터페이스 사용
- 팀 협업 - 팀원들과 시트를 공유하여 협업 오디오 제작
- Drive에 자동 저장 - 생성된 오디오 파일이 Google Drive에 자동 저장
사전 준비 사항
시작하기 전에 다음을 준비하세요:
- Google 계정 - Google Sheets 액세스
- 타입캐스트 API 키 - 여기서 받기
설정 가이드
단계 1: 스프레드시트 만들기
- Google Sheets 열기
- 새 빈 스프레드시트 만들기
- 첫 번째 행에 헤더로 열 설정:
- A열:
Text- 음성으로 변환할 텍스트 - B열:
Voice- Voice ID (예:tc_66aca22c7d31e45ff05ff418) 또는 음성 라이브러리의 음성 이름 - C열:
Language- 언어 코드 (eng,kor,jpn,cmn등) - D열:
Audio URL- 비워두기 (자동으로 채워짐)
- A열:
- 두 번째 행부터 데이터 추가 (1행은 헤더용)
단계 2: Apps Script 편집기 열기
- 메뉴 바에서 확장 프로그램 클릭
- Apps Script 선택
- Apps Script 편집기가 있는 새 탭이 열립니다
단계 3: 타입캐스트 연동 코드 추가
- 기본
myFunction()코드 삭제 - 다음 코드를 복사하여 붙여넣기:
📋 전체 Apps Script 코드 보기 클릭
// 타입캐스트 API 구성
const TYPECAST_API_KEY = "YOUR_API_KEY_HERE"; // 실제 API 키로 교체
const TYPECAST_API_URL = "https://api.typecast.ai/v1/text-to-speech";
/**
* 스프레드시트가 열릴 때 사용자 정의 메뉴 생성
*/
function onOpen() {
const ui = SpreadsheetApp.getUi();
ui.createMenu("🎙️ Typecast TTS")
.addItem("모든 오디오 생성", "generateAllAudio")
.addItem("선택한 행만 생성", "generateSelectedAudio")
.addSeparator()
.addItem("오디오 URL 지우기", "clearAudioUrls")
.addToUi();
}
/**
* 텍스트가 있는 모든 행에 대해 오디오 생성
*/
function generateAllAudio() {
const sheet = SpreadsheetApp.getActiveSheet();
const lastRow = sheet.getLastRow();
if (lastRow < 2) {
SpreadsheetApp.getUi().alert("처리할 데이터가 없습니다!");
return;
}
// 더 나은 성능을 위해 모든 데이터를 한 번에 가져오기
const dataRange = sheet.getRange(2, 1, lastRow - 1, 4);
const data = dataRange.getValues();
let successCount = 0;
let errorCount = 0;
// 각 행 처리
data.forEach((row, index) => {
const text = row[0];
const voiceNameOrId = row[1];
const language = row[2] || "kor"; // 지정되지 않으면 기본값 한국어
const rowNumber = index + 2;
// 텍스트 또는 음성 이름/ID가 비어있으면 건너뛰기
if (!text || !voiceNameOrId) {
return;
}
// 오디오 URL이 이미 있으면 건너뛰기
if (row[3]) {
return;
}
try {
const audioUrl = callTypecastAPI(text, voiceNameOrId, language);
sheet.getRange(rowNumber, 4).setValue(audioUrl);
successCount++;
// 요청 제한을 피하기 위해 약간의 지연 추가
Utilities.sleep(500);
} catch (error) {
sheet.getRange(rowNumber, 4).setValue("오류: " + error.message);
errorCount++;
}
});
SpreadsheetApp.getUi().alert(
`생성 완료!\n\n성공: ${successCount}\n오류: ${errorCount}`,
);
}
/**
* 선택한 행에 대해서만 오디오 생성
*/
function generateSelectedAudio() {
const sheet = SpreadsheetApp.getActiveSheet();
const selection = sheet.getActiveRange();
const startRow = selection.getRow();
const numRows = selection.getNumRows();
if (startRow === 1) {
SpreadsheetApp.getUi().alert("데이터 행을 선택하세요 (헤더 제외)");
return;
}
let successCount = 0;
let errorCount = 0;
for (let i = 0; i < numRows; i++) {
const rowNumber = startRow + i;
const text = sheet.getRange(rowNumber, 1).getValue();
const voiceNameOrId = sheet.getRange(rowNumber, 2).getValue();
const language = sheet.getRange(rowNumber, 3).getValue() || "kor";
if (!text || !voiceNameOrId) {
continue;
}
try {
const audioUrl = callTypecastAPI(text, voiceNameOrId, language);
sheet.getRange(rowNumber, 4).setValue(audioUrl);
successCount++;
Utilities.sleep(500);
} catch (error) {
sheet.getRange(rowNumber, 4).setValue("오류: " + error.message);
errorCount++;
}
}
SpreadsheetApp.getUi().alert(
`생성 완료!\n\n성공: ${successCount}\n오류: ${errorCount}`,
);
}
/**
* D열의 모든 오디오 URL 지우기
*/
function clearAudioUrls() {
const sheet = SpreadsheetApp.getActiveSheet();
const lastRow = sheet.getLastRow();
if (lastRow < 2) {
return;
}
const response = SpreadsheetApp.getUi().alert(
"오디오 URL 지우기",
"모든 오디오 URL을 지우시겠습니까?",
SpreadsheetApp.getUi().ButtonSet.YES_NO,
);
if (response === SpreadsheetApp.getUi().Button.YES) {
sheet.getRange(2, 4, lastRow - 1, 1).clearContent();
SpreadsheetApp.getUi().alert("오디오 URL이 지워졌습니다!");
}
}
/**
* Voices API를 호출하여 음성 이름에서 음성 ID 가져오기
* @param {string} voiceName - 조회할 음성 이름
* @returns {string} 음성 ID (예: tc_66aca22c7d31e45ff05ff418)
*/
function getVoiceIdByName(voiceName) {
const url = "https://api.typecast.ai/v2/voices";
const options = {
method: "get",
headers: {
"X-API-KEY": TYPECAST_API_KEY,
},
muteHttpExceptions: true,
};
const response = UrlFetchApp.fetch(url, options);
const responseCode = response.getResponseCode();
if (responseCode !== 200) {
throw new Error(`음성 가져오기 실패: ${responseCode}`);
}
const voices = JSON.parse(response.getContentText());
// 이름으로 음성 검색 (대소문자 구분 없음)
const searchName = voiceName.toLowerCase().trim();
const voice = voices.find((v) => v.voice_name.toLowerCase() === searchName);
if (!voice) {
throw new Error(
`"${voiceName}" 음성을 찾을 수 없습니다. 음성 이름을 확인하거나 음성 ID를 대신 사용하세요.`,
);
}
return voice.voice_id;
}
/**
* 타입캐스트 API를 호출하여 음성 생성
* @param {string} text - 음성으로 변환할 텍스트
* @param {string} voiceNameOrId - 음성 이름 (예: "Emily") 또는 음성 ID (예: "tc_66aca22c7d31e45ff05ff418")
* @param {string} language - 언어 코드 (eng, kor, jpn, cmn 등)
* @returns {string} 생성된 오디오 파일의 URL
*/
function callTypecastAPI(text, voiceNameOrId, language) {
// 입력값 정리 및 검증
text = String(text).trim();
voiceNameOrId = String(voiceNameOrId).trim();
language = String(language || "kor").trim();
if (!text || !voiceNameOrId) {
throw new Error("텍스트와 음성 이름/ID가 필요합니다");
}
// 입력이 음성 ID인지 이름인지 결정
// 음성 ID는 "tc_"로 시작
let voiceId;
if (voiceNameOrId.startsWith("tc_")) {
voiceId = voiceNameOrId;
Logger.log("음성 ID 사용: " + voiceId);
} else {
// 음성 이름이므로 ID 조회
Logger.log("음성 이름 조회 중: " + voiceNameOrId);
voiceId = getVoiceIdByName(voiceNameOrId);
Logger.log("음성 ID 찾음: " + voiceId);
}
// 디버깅용 로그
Logger.log("API 호출 중, 텍스트: " + text);
Logger.log("언어: " + language);
const payload = {
voice_id: voiceId,
text: text,
model: "ssfm-v30",
language: language,
prompt: {
emotion_type: "smart", // 자연스러운 음성을 위한 스마트 이모션 활성화
},
output: {
audio_format: "mp3",
},
};
// 디버깅용 페이로드 로그
Logger.log("페이로드: " + JSON.stringify(payload));
const options = {
method: "post",
contentType: "application/json",
headers: {
"X-API-KEY": TYPECAST_API_KEY,
},
payload: JSON.stringify(payload),
muteHttpExceptions: true,
};
const response = UrlFetchApp.fetch(TYPECAST_API_URL, options);
const responseCode = response.getResponseCode();
if (responseCode !== 200) {
const errorBody = response.getContentText();
Logger.log("오류 응답: " + errorBody);
throw new Error(`API 오류: ${responseCode} - ${errorBody}`);
}
// 오디오 blob 가져오기
const audioBlob = response.getBlob();
// Google Drive에 업로드
const folder = DriveApp.getRootFolder(); // 또는 특정 폴더 지정
const fileName = `typecast_${new Date().getTime()}.mp3`;
const file = folder.createFile(audioBlob.setName(fileName));
// 링크로 접근 가능하도록 파일 설정
file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
// 파일 URL 반환
return file.getUrl();
}
- 저장 아이콘 (💾)을 클릭하거나
⌘+S(Mac) /Ctrl+S(Windows)를 누르세요 - 프로젝트에 이름을 지정하세요 (예: "타입캐스트 TTS 통합")
단계 4: 권한 승인
스크립트를 처음 실행할 때 Google이 권한을 요청합니다:
- Apps Script 탭을 닫고 스프레드시트로 돌아가기
- 페이지 새로고침 (F5 또는
⌘+R) - 메뉴 바에 새 메뉴 🎙️ 타입캐스트 TTS가 나타나야 합니다
- 🎙️ 타입캐스트 TTS → 모든 오디오 생성 클릭
- Google이 스크립트 승인을 요청합니다:
- 계속 클릭
- Google 계정 선택
- 고급 → 프로젝트 이름(으)로 이동 (안전하지 않음) 클릭
- 허용 클릭
사용법
모든 행에 대해 음성 생성
- 🎙️ 타입캐스트 TTS → 모든 오디오 생성 클릭
- 스크립트가 텍스트와 음성 ID가 있는 모든 행을 처리합니다
- 완료되면 성공 메시지가 표시됩니다
- D열에 오디오 URL이 나타납니다
- 생성된 오디오 파일이 Google Drive에 저장됩니다
선택한 행에 대해 음성 생성
- 처리할 행 선택 (행 번호를 클릭하고 드래그)
- 🎙️ 타입캐스트 TTS → 선택한 행만 생성 클릭
- 선택한 행만 처리됩니다
음성 URL 지우기
모든 생성된 URL을 제거하려면 (Drive에서 오디오 파일은 삭제되지 않음):
- 🎙️ 타입캐스트 TTS → 오디오 URL 지우기 클릭
- 작업 확인
- D열의 모든 URL이 지워집니다
고급 설정
스마트 이모션 (기본값)
스크립트는 기본적으로 스마트 이모션을 사용하며, 텍스트를 자동으로 분석하여 적절한 감정을 적용합니다:
prompt: {
emotion_type: "smart"; // 텍스트에 가장 적합한 감정을 자동으로 감지
}
감정 프리셋 사용
감정을 수동으로 제어하려면 emotion_type을 preset으로 변경하세요:
function callTypecastAPI(text, voiceId, language) {
const payload = {
voice_id: voiceId,
text: text,
model: "ssfm-v30",
language: language,
prompt: {
emotion_type: "preset",
emotion_preset: "happy", // 옵션: happy, sad, angry, whisper, normal, toneup, 또는 tonedown.
emotion_intensity: 1.0, // 0.0 ~ 1.0
},
output: {
audio_format: "mp3",
audio_tempo: 1.0, // 속도: 0.5 (느림) ~ 2.0 (빠름)
audio_pitch: 0, // 피치: -12 ~ +12 반음
volume: 100, // 볼륨: 0 ~ 200
},
};
// ... 나머지 코드
}
지원 언어
스크립트는 ssfm-v30 모델(기본 사용)로 37개 언어를 지원합니다:
| 코드 | 언어 | 코드 | 언어 | 코드 | 언어 |
|---|---|---|---|---|---|
ara | 아랍어 | ind | 인도네시아어 | por | 포르투갈어 |
ben | 벵골어 | ita | 이탈리아어 | ron | 루마니아어 |
bul | 불가리아어 | jpn | 일본어 | rus | 러시아어 |
ces | 체코어 | kor | 한국어 | slk | 슬로바키아어 |
dan | 덴마크어 | msa | 말레이어 | spa | 스페인어 |
deu | 독일어 | nan | 민난어 | swe | 스웨덴어 |
ell | 그리스어 | nld | 네덜란드어 | tam | 타밀어 |
eng | 영어 | nor | 노르웨이어 | tgl | 타갈로그어 |
fin | 핀란드어 | pan | 펀자브어 | tha | 태국어 |
fra | 프랑스어 | pol | 폴란드어 | tur | 터키어 |
hin | 힌디어 | ukr | 우크라이나어 | vie | 베트남어 |
hrv | 크로아티아어 | yue | 광둥어 | zho | 중국어 |
hun | 헝가리어 |
다른 언어를 사용하려면 C열의 값을 변경하면 됩니다! 언어 코드는 대소문자를 구분하지 않습니다 (ENG와 eng 모두 작동).
특정 Drive 폴더에 저장
오디오 파일을 루트 대신 특정 폴더에 저장하려면:
// 이 줄을:
const folder = DriveApp.getRootFolder();
// 이것으로 교체 (폴더 ID 사용):
const folder = DriveApp.getFolderById("YOUR_FOLDER_ID_HERE");
// 또는 새 폴더 생성:
const folder = DriveApp.createFolder("Typecast Audio Files");
추가 열 설정
더 세밀한 제어를 위해 추가 열로 스프레드시트를 확장할 수 있습니다:
- E열: 감정 프리셋 (happy, sad, angry, normal)
- F열: 오디오 템포 (0.5 ~ 2.0)
- G열: 오디오 피치 (-12 ~ +12)
- H열: 상태 (Processing, Done, Error)
그런 다음 스크립트를 수정하여 시트에서 이 값들을 읽도록 합니다.
Google Sheets와 타입캐스트 API를 사용해야 하는 이유
간단한 복사-붙여넣기 설정. 개발자가 아니어도 단 5분 만에 전문적인 보이스오버를 쉽게 생성할 수 있습니다.
한 번 설정하면 영구적으로 사용 가능. 반복적인 TTS 작업과 매크로 스타일 워크플로우에 이상적입니다.
한 번의 클릭으로 수백 개의 오디오 파일을 생성합니다. 전체 콘텐츠 캘린더를 한 번에 처리할 수 있습니다.
팀과 스프레드시트를 공유하세요. 모두가 텍스트를 관리하고 함께 오디오를 생성할 수 있습니다.
활용 사례
교육 콘텐츠 제작
수업 스크립트에서 강의 내레이션을 만드세요. A열에 텍스트를 추가하고 오디오를 생성한 다음 동영상에 사용할 수 있습니다.
팟캐스트 제작
공유 Google Sheet에서 인트로/아웃트로 세그먼트, 광고 읽기, 안내 멘트를 생성하세요.
상품 설명
상품 설명을 접근성 향상이나 마케팅 동영상을 위한 오디오로 변환하세요.
SNS 콘텐츠
콘텐츠 캘린더에서 Instagram Reels, TikTok, YouTube Shorts용 보이스오버를 일괄 생성하세요.
다국어 콘텐츠
시트에서 텍스트를 번역하고 글로벌 사용자를 위해 여러 언어로 오디오를 생성하세요.
문제 해결
새로고침 후 메뉴가 나타나지 않음
- Apps Script 편집기에서 스크립트를 저장했는지 확인 -
onOpen함수를 수동으로 실행해 보기: 1. Apps Script 편집기로 돌아가기 2. 함수 드롭다운에서onOpen선택 3. 실행 버튼 (▶️) 클릭 - 브라우저 콘솔에서 오류 확인 (F12)
권한 오류
- 프롬프트가 나타났을 때 허용을 클릭했는지 확인 - 권한을 지우고 다시 권한 승인 시도: 1. Apps Script 편집기 → 실행 → 권한 지우기 2. 저장하고 닫기 3. 스프레드시트 새로고침 4. 메뉴 다시 시도
API 오류: 401 - 잘못된 API 키
YOUR_API_KEY_HERE를 실제 API 키로 교체했는지 확인 - 타입캐스트 API 콘솔에서 API 키 확인 - 키 주변에 여분의 공백이 없는지 확인
API 오류: 402 - 크레딧 부족
- 타입캐스트 API 콘솔에서 크레딧 잔액 확인 - 크레딧은 글자 수에 기반하여 차감 - 충분한지 확인
대용량 데이터셋에서 스크립트 시간 초과
- "선택한 행만 생성"을 사용하여 더 작은 배치로 처리 - Apps Script는 6분 실행 제한이 있음 - 매우 큰 데이터셋(500+ 행)은 여러 시트로 분할
Drive에 오디오 파일이 나타나지 않음
- Google Drive 루트 폴더 확인 - 오디오 파일 이름은
typecast_[timestamp].mp3입니다 - 스크립트에 Drive 권한이 있는지 확인 (올바르게 승인됨)