feat:사용자 매뉴얼 영상 자동 생성 기능 구현

- TutorialVideo 모델 (상태 관리, TenantScope)
- GeminiScriptService에 callGeminiWithParts() 멀티모달 지원 추가
- ScreenAnalysisService: Gemini Vision 스크린샷 AI 분석
- SlideAnnotationService: PHP GD 이미지 어노테이션 (마커, 캡션)
- TutorialAssemblyService: FFmpeg 이미지→영상 합성 (crossfade)
- TutorialVideoJob: 분석→슬라이드→TTS→BGM→합성 파이프라인
- TutorialVideoController: 업로드/분석/생성/상태/다운로드/이력 API
- React-in-Blade UI: 3단계 (업로드→분석확인→생성모니터링) + 이력

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-15 15:56:39 +09:00
parent 6e6608ab0c
commit 768bc30a6d
9 changed files with 1992 additions and 11 deletions

View File

@@ -331,9 +331,19 @@ public function generateScenario(string $title, string $keyword = ''): array
}
/**
* Gemini API 호출 (AiConfig 기반 - API Key / Vertex AI 자동 분기)
* Gemini API 호출 (텍스트 전용 - 기존 호환)
*/
private function callGemini(string $prompt): ?string
{
return $this->callGeminiWithParts([['text' => $prompt]]);
}
/**
* Gemini API 호출 (멀티모달 지원 - 텍스트 + 이미지)
*
* @param array $parts [['text' => '...'], ['inlineData' => ['mimeType' => '...', 'data' => '...']]]
*/
public function callGeminiWithParts(array $parts, float $temperature = 0.9, int $maxTokens = 4096): ?string
{
if (! $this->config) {
Log::error('GeminiScriptService: 활성화된 Gemini 설정이 없습니다. (시스템 > AI 설정 확인)');
@@ -346,20 +356,17 @@ private function callGemini(string $prompt): ?string
$body = [
'contents' => [
[
'parts' => [
['text' => $prompt],
],
'parts' => $parts,
],
],
'generationConfig' => [
'temperature' => 0.9,
'maxOutputTokens' => 4096,
'temperature' => $temperature,
'maxOutputTokens' => $maxTokens,
'responseMimeType' => 'application/json',
],
];
if ($this->config->isVertexAi()) {
// Vertex AI 방식 (서비스 계정 OAuth)
$accessToken = $this->googleCloud->getAccessToken();
if (! $accessToken) {
Log::error('GeminiScriptService: Vertex AI 액세스 토큰 획득 실패');
@@ -371,19 +378,17 @@ private function callGemini(string $prompt): ?string
$region = $this->config->getRegion();
$url = "https://{$region}-aiplatform.googleapis.com/v1/projects/{$projectId}/locations/{$region}/publishers/google/models/{$model}:generateContent";
// Vertex AI에서는 role 필수
$body['contents'][0]['role'] = 'user';
$response = Http::withToken($accessToken)
->timeout(60)
->timeout(120)
->post($url, $body);
} else {
// API Key 방식
$apiKey = $this->config->api_key;
$baseUrl = $this->config->base_url ?? 'https://generativelanguage.googleapis.com/v1beta';
$url = "{$baseUrl}/models/{$model}:generateContent?key={$apiKey}";
$response = Http::timeout(60)->post($url, $body);
$response = Http::timeout(120)->post($url, $body);
}
if (! $response->successful()) {
@@ -407,6 +412,14 @@ private function callGemini(string $prompt): ?string
}
}
/**
* JSON 응답 파싱 (public - 외부 서비스에서도 사용)
*/
public function parseJson(string $text): ?array
{
return $this->parseJsonResponse($text);
}
/**
* JSON 응답 파싱 (코드블록 제거 포함)
*/