CASTAD-v0.1/services/sunoService.ts

118 lines
5.0 KiB
TypeScript

// Suno API 응답 인터페이스 (백엔드 응답용)
interface SunoBackendResponse {
audioUrl: string;
}
/**
* 가사 정제 로직 (Python의 _sanitize_lyrics 함수를 JavaScript로 포팅).
* Suno AI가 가사를 올바르게 해석하도록 특정 패턴을 [Verse 1]과 같은 태그로 변환합니다.
* @param {string} lyrics - 원본 가사 텍스트
* @returns {string} - Suno AI에 최적화된 정제된 가사 텍스트
*/
const sanitizeLyrics = (lyrics: string): string => {
// 섹션 헤더를 감지하는 정규식 (예: Verse, Chorus, Bridge, Hook, Intro, Outro)
const sectionPattern = /^(verse|chorus|bridge|hook|intro|outro)\s*(\d+)?\s*:?/i;
const sanitizedLines: string[] = []; // 정제된 가사 라인들을 저장할 배열
const lines = lyrics.split('\n'); // 가사를 줄 단위로 분리
for (const rawLine of lines) {
const line = rawLine.trim(); // 각 줄의 앞뒤 공백 제거
if (!line) {
sanitizedLines.push(""); // 빈 줄은 그대로 추가
continue;
}
const match = line.match(sectionPattern); // 섹션 패턴 매칭 시도
if (match) {
// 섹션 이름 첫 글자 대문자화
const name = match[1].charAt(0).toUpperCase() + match[1].slice(1).toLowerCase();
const number = match[2] || ""; // 섹션 번호 (있으면 사용, 없으면 빈 문자열)
// [Verse 1] 형태의 태그로 변환하여 추가
const label = `[${name}${number ? ' ' + number : ''}]`;
sanitizedLines.push(label);
} else {
sanitizedLines.push(line); // 패턴에 해당하지 않으면 원본 줄 추가
}
}
const result = sanitizedLines.join('\n').trim(); // 정제된 줄들을 다시 합치고 최종 공백 제거
if (!result) throw new Error("정제된 가사가 비어있습니다. Suno AI에 전달할 유효한 가사가 필요합니다.");
return result;
};
/**
* Suno AI를 사용하여 음악을 생성하는 함수입니다.
* 백엔드 프록시 서버(/api/suno/generate)를 통해 요청을 전송하여 CORS 문제 및 안정성을 개선했습니다.
*
* @param {string} rawLyrics - 사용자가 입력한 원본 가사
* @param {string} style - 음악 스타일 (예: Pop, Rock, Acoustic 등)
* @param {string} title - 노래 제목
* @param {boolean} isFullSong - 전체 길이 곡 생성 여부 (현재는 사용되지 않지만, API에 따라 달라질 수 있음)
* @returns {Promise<string>} - 생성된 음악 파일의 URL
*/
export const generateSunoMusic = async (
rawLyrics: string,
style: string,
title: string,
isFullSong: boolean, // 현재는 사용되지 않음
isInstrumental: boolean = false // 연주곡 여부
): Promise<string> => {
// 1. 가사 정제 (연주곡이 아닐 때만 수행)
let sanitizedLyrics = "";
if (!isInstrumental) {
sanitizedLyrics = sanitizeLyrics(rawLyrics);
} else {
// console.log("연주곡 모드: 가사 생성 생략"); // 제거
}
// 2. 요청 페이로드 구성 (Suno OpenAPI v1 Spec 준수)
const payload = {
// Custom Mode에서 instrumental이 false이면 prompt는 가사로 사용됨.
// instrumental이 true이면 prompt는 사용되지 않음 (하지만 예제에는 포함되어 있으므로 안전하게 유지).
prompt: isInstrumental ? "" : sanitizedLyrics,
style: style, // tags -> style 로 복구
title: title.substring(0, 80), // V4/V4_5ALL 기준 80자 제한 안전하게 적용
customMode: true,
instrumental: isInstrumental, // make_instrumental -> instrumental 로 복구
model: "V5", // 필수 필드: V5 사용
callBackUrl: "https://api.example.com/callback" // 필수 필드: 더미 URL이라도 보내야 함
};
try {
// console.log("백엔드를 통해 Suno 음악 생성 요청 시작..."); // 제거
// 백엔드 엔드포인트 호출 (프록시 사용 X, 로컬 서버 사용)
const response = await fetch('/api/suno/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}` // 인증 토큰 포함
},
body: JSON.stringify(payload) // JSON 페이로드 전송
});
if (!response.ok) {
const errorJson = await response.json().catch(() => ({ error: "Unknown Error" }));
console.error("Suno Backend Error:", errorJson);
throw new Error(`음악 생성 실패 (서버): ${errorJson.error || response.statusText}`);
}
const resData: SunoBackendResponse = await response.json();
if (!resData.audioUrl) {
throw new Error("서버에서 오디오 URL을 반환하지 않았습니다.");
}
console.log(`생성 완료! Audio URL: ${resData.audioUrl}`);
return resData.audioUrl;
} catch (e: any) {
console.error("Suno 서비스 오류 발생", e);
throw new Error(e.message || "음악 생성 중 알 수 없는 오류가 발생했습니다.");
}
};