Files
sam-manage/app/Services/Video/VeoVideoService.php
김보곤 6ab93aedd2 feat:YouTube Shorts AI 자동 생성 시스템 구현 (Veo 3.1 + Gemini)
- GeminiScriptService: 트렌딩 제목/시나리오 생성
- VeoVideoService: Veo 3.1 영상 클립 생성
- TtsService: Google TTS 나레이션 생성
- BgmService: 분위기별 BGM 선택
- VideoAssemblyService: FFmpeg 영상 합성
- VideoGenerationJob: 백그라운드 처리
- Veo3Controller: API 엔드포인트
- React 프론트엔드 (5단계 위저드)
- GoogleCloudService.getAccessToken() public 변경

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 08:46:28 +09:00

183 lines
5.8 KiB
PHP

<?php
namespace App\Services\Video;
use App\Services\GoogleCloudService;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class VeoVideoService
{
private GoogleCloudService $googleCloud;
private string $projectId;
private string $location;
public function __construct(GoogleCloudService $googleCloud)
{
$this->googleCloud = $googleCloud;
$this->projectId = config('services.vertex_ai.project_id', 'codebridge-chatbot');
$this->location = config('services.vertex_ai.location', 'us-central1');
}
/**
* 프롬프트 → 영상 클립 생성 요청 (비동기)
*
* @return array|null ['operationName' => '...'] or null
*/
public function generateClip(string $prompt, int $duration = 8): ?array
{
$token = $this->googleCloud->getAccessToken();
if (! $token) {
Log::error('VeoVideoService: 액세스 토큰 획득 실패');
return null;
}
try {
$url = "https://{$this->location}-aiplatform.googleapis.com/v1/projects/{$this->projectId}/locations/{$this->location}/publishers/google/models/veo-3.1-generate-preview:predictLongRunning";
$response = Http::withToken($token)
->timeout(60)
->post($url, [
'instances' => [
[
'prompt' => $prompt,
],
],
'parameters' => [
'aspectRatio' => '9:16',
'duration' => "{$duration}s",
'resolution' => '720p',
'personGeneration' => 'allow_adult',
'generateAudio' => false,
'sampleCount' => 1,
],
]);
if (! $response->successful()) {
Log::error('VeoVideoService: 영상 생성 요청 실패', [
'status' => $response->status(),
'body' => $response->body(),
]);
return null;
}
$data = $response->json();
$operationName = $data['name'] ?? null;
if (! $operationName) {
Log::error('VeoVideoService: Operation name 없음', ['response' => $data]);
return null;
}
Log::info('VeoVideoService: 영상 생성 요청 성공', [
'operationName' => $operationName,
'prompt' => substr($prompt, 0, 100),
]);
return ['operationName' => $operationName];
} catch (\Exception $e) {
Log::error('VeoVideoService: 영상 생성 예외', ['error' => $e->getMessage()]);
return null;
}
}
/**
* 비동기 작업 상태 확인
*
* @return array ['done' => bool, 'video' => base64|null, 'error' => string|null]
*/
public function checkOperation(string $operationName): array
{
$token = $this->googleCloud->getAccessToken();
if (! $token) {
return ['done' => false, 'video' => null, 'error' => '토큰 획득 실패'];
}
try {
$url = "https://{$this->location}-aiplatform.googleapis.com/v1/{$operationName}";
$response = Http::withToken($token)->timeout(30)->get($url);
if (! $response->successful()) {
return ['done' => false, 'video' => null, 'error' => 'HTTP ' . $response->status()];
}
$data = $response->json();
if (! ($data['done'] ?? false)) {
return ['done' => false, 'video' => null, 'error' => null];
}
if (isset($data['error'])) {
return ['done' => true, 'video' => null, 'error' => $data['error']['message'] ?? 'Unknown error'];
}
// 영상 데이터 추출
$predictions = $data['response']['predictions'] ?? [];
if (empty($predictions)) {
return ['done' => true, 'video' => null, 'error' => '영상 데이터 없음'];
}
$videoBase64 = $predictions[0]['bytesBase64Encoded'] ?? null;
return ['done' => true, 'video' => $videoBase64, 'error' => null];
} catch (\Exception $e) {
Log::error('VeoVideoService: 상태 확인 예외', ['error' => $e->getMessage()]);
return ['done' => false, 'video' => null, 'error' => $e->getMessage()];
}
}
/**
* 영상 생성 완료까지 폴링 대기
*
* @return string|null 저장된 파일 경로 또는 null
*/
public function waitAndSave(string $operationName, string $savePath, int $maxAttempts = 120): ?string
{
for ($i = 0; $i < $maxAttempts; $i++) {
sleep(10);
$result = $this->checkOperation($operationName);
if ($result['error'] && $result['done']) {
Log::error('VeoVideoService: 영상 생성 실패', ['error' => $result['error']]);
return null;
}
if ($result['done'] && $result['video']) {
$videoData = base64_decode($result['video']);
$dir = dirname($savePath);
if (! is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($savePath, $videoData);
Log::info('VeoVideoService: 영상 저장 완료', [
'path' => $savePath,
'size' => strlen($videoData),
]);
return $savePath;
}
}
Log::error('VeoVideoService: 영상 생성 타임아웃', [
'operationName' => $operationName,
'attempts' => $maxAttempts,
]);
return null;
}
}