feat: [sound-logo] Phase 2 AI 어시스트 모드 추가

- Gemini API 연동: 프롬프트 → 음표 시퀀스 JSON 자동 생성
- AI 탭 UI: 프롬프트 입력, 카테고리/길이 선택, 빠른 프롬프트 10종
- AI 결과 미리보기: 음표 시각화, 미리듣기, 시퀀서 로드
- POST /rd/sound-logo/generate 엔드포인트 추가
This commit is contained in:
김보곤
2026-03-08 12:34:42 +09:00
parent 92e2bddf50
commit 85304bdfbc
3 changed files with 309 additions and 0 deletions

View File

@@ -9,6 +9,8 @@
use App\Services\Rd\AiQuotationService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
class RdController extends Controller
@@ -337,4 +339,117 @@ public function soundLogo(Request $request): View|\Illuminate\Http\Response
return view('rd.sound-logo.index');
}
/**
* 사운드 로고 AI 생성 (Gemini API)
*/
public function soundLogoGenerate(Request $request): JsonResponse
{
$request->validate([
'prompt' => 'required|string|max:500',
'category' => 'nullable|string',
'duration' => 'nullable|numeric|min:0.3|max:5',
]);
$apiKey = config('services.gemini.api_key');
$baseUrl = config('services.gemini.base_url', 'https://generativelanguage.googleapis.com/v1beta');
$model = config('services.gemini.model', 'gemini-2.5-flash');
if (! $apiKey) {
return response()->json(['success' => false, 'error' => 'Gemini API 키가 설정되지 않았습니다.'], 500);
}
$category = $request->category ?? '기업 시그널';
$duration = $request->duration ?? 1.5;
$prompt = <<<PROMPT
당신은 사운드 디자인 전문가입니다. 사용자의 요청에 맞는 사운드 로고(짧은 시그니처 사운드)를 Web Audio API 음표 시퀀스로 설계해주세요.
## 사용자 요청
- 설명: {$request->prompt}
- 카테고리: {$category}
- 목표 길이: {$duration}초
## 사용 가능한 음표
C3, C#3, D3, D#3, E3, F3, F#3, G3, G#3, A3, A#3, B3,
C4, C#4, D4, D#4, E4, F4, F#4, G4, G#4, A4, A#4, B4,
C5, C#5, D5, D#5, E5, F5, F#5, G5, G#5, A5, A#5, B5, C6
## 음표 타입
- note: 단일 음 (note 필드 필수)
- chord: 화음 (chord 배열 필수, 2~4개 음)
- rest: 쉼표 (duration만 필요)
## 신스 타입
- sine: 부드러움 (기업 로고, 알림에 적합)
- triangle: 따뜻함 (성공, 게임에 적합)
- square: 8bit/디지털 (게임, UI에 적합)
- sawtooth: 날카로움 (록, 긴급 알림에 적합)
## 반드시 아래 JSON 형식으로만 응답하세요
{
"name": "사운드 이름",
"desc": "사운드 설명 (한줄)",
"synth": "sine",
"adsr": { "attack": 10, "decay": 80, "sustain": 0.6, "release": 400 },
"volume": 0.8,
"reverb": 0.3,
"notes": [
{ "type": "note", "note": "C5", "duration": 0.20, "velocity": 0.8 },
{ "type": "rest", "duration": 0.10 },
{ "type": "chord", "chord": ["C4", "E4", "G4"], "duration": 0.50, "velocity": 1.0 }
]
}
## 설계 원칙
- 음표의 duration 합계가 목표 길이({$duration}초)에 근접하도록 설계
- velocity: 0.3~1.0 (음의 강약으로 표현력 추가)
- ADSR: attack(1~500ms), decay(10~1000ms), sustain(0~1.0), release(10~3000ms)
- 카테고리 특성에 맞는 synth와 ADSR 선택
- 음악적으로 조화롭고 기억에 남는 멜로디 설계
- 최소 2개, 최대 12개 음표 사용
PROMPT;
try {
$response = Http::timeout(30)->post(
"{$baseUrl}/models/{$model}:generateContent?key={$apiKey}",
[
'contents' => [
['parts' => [['text' => $prompt]]],
],
'generationConfig' => [
'temperature' => 0.9,
'maxOutputTokens' => 2048,
'responseMimeType' => 'application/json',
],
]
);
} catch (\Exception $e) {
Log::error('SoundLogo AI 생성 실패', ['error' => $e->getMessage()]);
return response()->json(['success' => false, 'error' => 'AI 서버 연결 실패'], 500);
}
if (! $response->successful()) {
Log::error('SoundLogo AI API 오류', ['status' => $response->status(), 'body' => $response->body()]);
return response()->json(['success' => false, 'error' => 'AI 생성 실패: '.$response->status()], 500);
}
$data = $response->json();
$text = $data['candidates'][0]['content']['parts'][0]['text'] ?? '';
// JSON 파싱 (코드블록 제거)
$text = preg_replace('/^```(?:json)?\s*/m', '', $text);
$text = preg_replace('/```\s*$/m', '', $text);
$result = json_decode(trim($text), true);
if (! $result || ! isset($result['notes'])) {
Log::warning('SoundLogo AI 응답 파싱 실패', ['text' => substr($text, 0, 500)]);
return response()->json(['success' => false, 'error' => 'AI 응답을 파싱할 수 없습니다.'], 500);
}
return response()->json(['success' => true, 'data' => $result]);
}
}