trypost/app/Services/Ai/VideoGenerationService.php

149 lines
4.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Ai;
use App\Enums\Ai\Orientation;
use App\Enums\Ai\UsageType;
use App\Models\AiUsageLog;
use App\Models\Workspace;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class VideoGenerationService
{
private string $baseUrl = 'https://generativelanguage.googleapis.com/v1beta';
private string $model = 'veo-3.1-generate-preview';
private int $maxPollAttempts = 60;
private int $pollIntervalSeconds = 10;
private string $apiKey;
public function __construct()
{
$this->apiKey = config('services.gemini.api_key') ?? '';
}
/**
* @return array{id: string, path: string, url: string, mime_type: string, type: string}
*/
public function generate(string $prompt, Workspace $workspace, ?string $userId = null, ?string $postId = null, Orientation $orientation = Orientation::Vertical): array
{
if (empty($this->apiKey)) {
throw new \RuntimeException('Gemini API key is not configured. Please set GEMINI_API_KEY in your .env file.');
}
$aspectRatio = $orientation->aspectRatio();
$fullPrompt = view('prompts.assistant.video', [
'prompt' => $prompt,
'brand_name' => $workspace->name ?? '',
'tone' => $workspace->brand_tone ?? 'professional',
'content_language' => $workspace->content_language ?? 'en',
])->render();
$operationName = $this->startGeneration($fullPrompt, $aspectRatio);
$videoData = $this->pollForCompletion($operationName);
$decoded = base64_decode($videoData);
return DB::transaction(function () use ($decoded, $prompt, $workspace, $userId, $postId) {
$filename = Str::uuid().'.mp4';
$path = 'medias/'.$filename;
Storage::put($path, $decoded);
$media = $workspace->media()->create([
'group_id' => Str::uuid()->toString(),
'collection' => 'assets',
'type' => 'video',
'path' => $path,
'original_filename' => 'ai-generated.mp4',
'mime_type' => 'video/mp4',
'size' => strlen($decoded),
'order' => 0,
'meta' => ['ai_generated' => true, 'prompt' => Str::limit($prompt, 200)],
]);
AiUsageLog::create([
'account_id' => $workspace->account_id,
'workspace_id' => $workspace->id,
'user_id' => $userId,
'post_id' => $postId,
'type' => UsageType::Video,
'provider' => 'veo',
]);
return [
'id' => $media->id,
'path' => $media->path,
'url' => $media->url,
'mime_type' => 'video/mp4',
'type' => 'video',
];
});
}
private function startGeneration(string $prompt, string $aspectRatio = '9:16'): string
{
$response = Http::timeout(30)
->withHeaders(['x-goog-api-key' => $this->apiKey])
->post("{$this->baseUrl}/models/{$this->model}:predictLongRunning", [
'instances' => [['prompt' => $prompt]],
'parameters' => [
'aspectRatio' => $aspectRatio,
'durationSeconds' => 8,
'resolution' => '720p',
'personGeneration' => 'allow_all',
],
]);
if ($response->failed()) {
Log::error('VideoGenerationService start failed', ['body' => $response->body()]);
throw new \RuntimeException('Failed to start video generation. Please try again.');
}
$operationName = data_get($response->json(), 'name');
if (! $operationName) {
throw new \RuntimeException('No operation returned from video generation API.');
}
return $operationName;
}
private function pollForCompletion(string $operationName): string
{
for ($i = 0; $i < $this->maxPollAttempts; $i++) {
sleep($this->pollIntervalSeconds);
$response = Http::timeout(30)
->withHeaders(['x-goog-api-key' => $this->apiKey])
->get("{$this->baseUrl}/{$operationName}");
if ($response->failed()) {
continue;
}
$status = $response->json();
if (data_get($status, 'done')) {
$videoData = data_get($status, 'response.predictions.0.bytesBase64Encoded');
if ($videoData) {
return $videoData;
}
}
}
throw new \RuntimeException('Video generation timed out. Please try again.');
}
}