CASTAD-v0.1/services/geminiService.ts

327 lines
12 KiB
TypeScript

import { BusinessInfo, TTSConfig, AspectRatio, Language } from '../types';
import { decodeBase64, decodeAudioData, bufferToWaveBlob } from './audioUtils';
const SUPPORTED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/heic', 'image/heif'];
// Helper to convert File object to base64 Data URL (MIME type 포함)
export const fileToBase64 = (file: File): Promise<{ base64: string; mimeType: string }> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
const result = reader.result as string;
const [header, base64] = result.split(',');
const mimeType = header.split(':')[1].split(';')[0];
resolve({ base64, mimeType });
};
reader.onerror = error => reject(error);
});
};
/**
* 비즈니스 정보 검색 (Gemini 2.5 Flash + Google Maps Tool) - 백엔드 프록시 이용
* @param {string} query - 검색어 (업체명 또는 주소)
* @returns {Promise<{name: string, description: string, mapLink?: string}>}
*/
export const searchBusinessInfo = async (
query: string
): Promise<{ name: string; description: string; mapLink?: string }> => {
try {
const response = await fetch('/api/gemini/search-business', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}` // 인증 토큰 포함
},
body: JSON.stringify({ query })
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `Server error: ${response.statusText}`);
}
const data = await response.json();
return data;
} catch (e: any) {
console.error("Search Business (Frontend) Failed:", e);
throw new Error(e.message || "업체 정보 검색에 실패했습니다.");
}
};
/**
* 창의적 콘텐츠 생성 (광고 카피 & 가사/스크립트) - 백엔드 프록시 이용
* @param {BusinessInfo} info - 비즈니스 정보 객체
* @returns {Promise<{adCopy: string[], lyrics: string}>}
*/
export const generateCreativeContent = async (
info: BusinessInfo
): Promise<{ adCopy: string[]; lyrics: string }> => {
try {
// 이미지 File 객체를 Base64로 변환하여 백엔드로 전달
const imagesForBackend = await Promise.all(
info.images.map(async (file) => await fileToBase64(file))
);
const payload = { ...info, images: imagesForBackend };
const response = await fetch('/api/gemini/creative-content', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `Server error: ${response.statusText}`);
}
const data = await response.json();
return data;
} catch (e: any) {
console.error("Generate Creative Content (Frontend) Failed:", e);
throw new Error(e.message || "창의적 콘텐츠 생성에 실패했습니다.");
}
};
// 사용자 설정(성별/톤)을 Gemini 미리 정의된 목소리 이름으로 매핑 (이 함수는 백엔드에서 사용)
// const getVoiceName = (config: TTSConfig): string => { ... };
/**
* 고급 음성 합성 (TTS) - 백엔드 프록시 이용
* @param {string} text - 음성으로 변환할 텍스트
* @param {TTSConfig} config - TTS 설정
* @returns {Promise<string>} - Base64 인코딩된 오디오 데이터 URL
*/
export const generateAdvancedSpeech = async (
text: string,
config: TTSConfig
): Promise<string> => {
try {
const response = await fetch('/api/gemini/speech', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ text, config })
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `Server error: ${response.statusText}`);
}
const data = await response.json();
// Base64 오디오 데이터를 Blob URL로 변환하여 반환
const audioContext = new ((window as any).AudioContext || (window as any).webkitAudioContext)({ sampleRate: 24000 });
const audioBytes = decodeBase64(data.base64Audio);
const audioBuffer = await decodeAudioData(audioBytes, audioContext, 24000, 1);
const wavBlob = bufferToWaveBlob(audioBuffer, audioBuffer.length);
return URL.createObjectURL(wavBlob);
} catch (e: any) {
console.error("Generate Advanced Speech (Frontend) Failed:", e);
throw new Error(e.message || "성우 음성 생성에 실패했습니다.");
}
};
/**
* 광고 포스터 생성 - 백엔드 프록시 이용
* @param {BusinessInfo} info - 비즈니스 정보 객체
* @returns {Promise<{ blobUrl: string; base64: string; mimeType: string }>}
*/
export const generateAdPoster = async (
info: BusinessInfo
): Promise<{ blobUrl: string; base64: string; mimeType: string }> => {
try {
const imagesForBackend = await Promise.all(
info.images.map(async (file) => await fileToBase64(file))
);
const payload = { ...info, images: imagesForBackend };
const response = await fetch('/api/gemini/ad-poster', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ info: payload })
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `Server error: ${response.statusText}`);
}
const data = await response.json();
// Base64를 Blob URL로 변환
const byteCharacters = atob(data.base64);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: data.mimeType });
const blobUrl = URL.createObjectURL(blob);
return { blobUrl, base64: data.base64, mimeType: data.mimeType };
} catch (e: any) {
console.error("Generate Ad Poster (Frontend) Failed:", e);
throw new Error(e.message || "광고 포스터 생성에 실패했습니다.");
}
};
/**
* 다수의 비즈니스 관련 이미지 생성 (갤러리/슬라이드쇼용) - 백엔드 프록시 이용
* @param {BusinessInfo} info - 비즈니스 정보 객체
* @param {number} count - 생성할 이미지 개수
* @returns {Promise<string[]>} - Base64 Data URL 배열
*/
export const generateImageGallery = async (
info: BusinessInfo,
count: number
): Promise<string[]> => {
try {
const response = await fetch('/api/gemini/image-gallery', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ info, count })
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `Server error: ${response.statusText}`);
}
const data = await response.json();
return data.images;
} catch (e: any) {
console.error("Generate Image Gallery (Frontend) Failed:", e);
throw new Error(e.message || "이미지 갤러리 생성에 실패했습니다.");
}
};
/**
* 비디오 배경 생성 - 백엔드 프록시 이용
* @param {string} posterBase64 - Base64 인코딩된 포스터 이미지 데이터
* @param {string} posterMimeType - 포스터 이미지 MIME 타입
* @param {AspectRatio} aspectRatio - 비디오 화면 비율
* @returns {Promise<string>} - 생성된 비디오의 원격 URL
*/
export const generateVideoBackground = async (
posterBase64: string,
posterMimeType: string,
aspectRatio: AspectRatio = '16:9'
): Promise<string> => {
try {
const response = await fetch('/api/gemini/video-background', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ posterBase64, posterMimeType, aspectRatio })
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `Server error: ${response.statusText}`);
}
const data = await response.json();
// 서버에서 받은 비디오 URL에 API 키를 붙이지 않고 그대로 반환
return data.videoUrl;
} catch (e: any) {
console.error("Generate Video Background (Frontend) Failed:", e);
throw new Error(e.message || "비디오 배경 생성에 실패했습니다.");
}
};
/**
* AI 이미지 검수 (Gemini Vision) - 백엔드 프록시 이용
* @param {Array<object>} imagesData - Base64 인코딩된 이미지 데이터 배열 ({ mimeType, base64 })
* @returns {Promise<Array<object>>} - 선별된 Base64 이미지 데이터 배열
*/
export const filterBestImages = async (
imagesData: { base64: string; mimeType: string }[]
): Promise<{ base64: string; mimeType: string }[]> => {
try {
const response = await fetch('/api/gemini/filter-images', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ imagesData })
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `Server error: ${response.statusText}`);
}
const data = await response.json();
return data.filteredImages;
} catch (e: any) {
console.error("Filter Best Images (Frontend) Failed:", e);
throw new Error(e.message || "이미지 검수에 실패했습니다.");
}
};
/**
* 리뷰 기반 마케팅 설명 생성 - 백엔드 프록시 이용
* @param {string} name - 업체명
* @param {string} rawDescription - 기본 설명
* @param {string[]} reviews - 고객 리뷰 배열
* @param {number} rating - 평균 별점
* @returns {Promise<string>} - 생성된 설명
*/
export const enrichDescriptionWithReviews = async (
name: string,
rawDescription: string,
reviews: string[],
rating: number
): Promise<string> => {
try {
const response = await fetch('/api/gemini/enrich-description', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ name, rawDescription, reviews, rating })
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `Server error: ${response.statusText}`);
}
const data = await response.json();
return data.enrichedDescription;
} catch (e: any) {
console.error("Enrich Description (Frontend) Failed:", e);
throw new Error(e.message || "마케팅 설명 생성에 실패했습니다.");
}
};
/**
* 이미지에서 텍스트 스타일(CSS) 추출 - 백엔드 프록시 이용
* @param {File} imageFile - 이미지 File 객체
* @returns {Promise<string>} - 생성된 CSS 코드
*/
export const extractTextEffectFromImage = async (
imageFile: File
): Promise<string> => {
try {
const imageForBackend = await fileToBase64(imageFile);
const response = await fetch('/api/gemini/text-effect', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ imageFile: imageForBackend })
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `Server error: ${response.statusText}`);
}
const data = await response.json();
return data.cssCode;
} catch (e: any) {
console.error("Extract Text Effect (Frontend) Failed:", e);
throw new Error(e.message || "텍스트 스타일 분석에 실패했습니다.");
}
};