125 lines
4.6 KiB
PHP
125 lines
4.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Ai\Tools;
|
|
|
|
use App\Enums\Ai\Orientation;
|
|
use App\Enums\Ai\UsageType;
|
|
use App\Features\AiImagesLimit;
|
|
use App\Models\AiUsageLog;
|
|
use App\Models\Post;
|
|
use App\Models\Workspace;
|
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
use Laravel\Ai\Contracts\Tool;
|
|
use Laravel\Ai\Image;
|
|
use Laravel\Ai\Tools\Request;
|
|
use Laravel\Pennant\Feature;
|
|
use Stringable;
|
|
|
|
class GenerateImage implements Tool
|
|
{
|
|
public function __construct(
|
|
public Workspace $workspace,
|
|
public ?Post $post = null,
|
|
public ?string $userId = null,
|
|
public ?AttachmentCollector $collector = null,
|
|
) {
|
|
$this->collector ??= app(AttachmentCollector::class);
|
|
}
|
|
|
|
public function description(): Stringable|string
|
|
{
|
|
return <<<'TXT'
|
|
Generate an AI image and attach it to the current post.
|
|
|
|
Call this when the user asks for an image, photo, carousel slide, or any visual content.
|
|
Pass a detailed visual prompt describing what to generate, and an orientation:
|
|
- "square" (1:1) for LinkedIn, Facebook, or when user wants square
|
|
- "portrait" (4:5) for Instagram Feed, Threads
|
|
- "vertical" (9:16) for Instagram Reel/Story, Pinterest Pin, TikTok
|
|
- "horizontal" (16:9) for X/Twitter, YouTube thumbnail
|
|
|
|
Choose the orientation that best matches the target platform.
|
|
|
|
The image is generated, stored, registered in the workspace's media library,
|
|
logged in monthly usage tracking, and attached to the assistant's response message.
|
|
TXT;
|
|
}
|
|
|
|
public function handle(Request $request): Stringable|string
|
|
{
|
|
$limit = (int) Feature::for($this->workspace->account)->value(AiImagesLimit::class);
|
|
$used = AiUsageLog::monthlyCount($this->workspace->account_id, UsageType::Image);
|
|
|
|
if ($used >= $limit) {
|
|
return "Image quota exhausted this month ({$used} of {$limit} used). Ask the user to upgrade their plan or wait until next month.";
|
|
}
|
|
|
|
$prompt = (string) data_get($request, 'prompt', '');
|
|
$orientationString = (string) data_get($request, 'orientation', 'vertical');
|
|
$orientationEnum = Orientation::tryFrom($orientationString) ?? Orientation::Portrait;
|
|
$aspectRatio = $orientationEnum->aspectRatio();
|
|
|
|
$renderedPrompt = view('prompts.assistant.image', [
|
|
'prompt' => $prompt,
|
|
'brand_name' => $this->workspace->name ?? '',
|
|
'tone' => $this->workspace->brand_tone ?? 'professional',
|
|
'aspect_ratio' => $aspectRatio,
|
|
'content_language' => $this->workspace->content_language ?? 'en',
|
|
])->render();
|
|
|
|
$response = Image::of($renderedPrompt)
|
|
->size($aspectRatio)
|
|
->quality('high')
|
|
->generate();
|
|
|
|
$storedPath = $response->store('medias');
|
|
|
|
$media = $this->workspace->media()->create([
|
|
'group_id' => Str::uuid()->toString(),
|
|
'collection' => 'assets',
|
|
'type' => 'image',
|
|
'path' => $storedPath,
|
|
'original_filename' => 'ai-generated.png',
|
|
'mime_type' => 'image/png',
|
|
'size' => Storage::size($storedPath),
|
|
'order' => 0,
|
|
'meta' => ['ai_generated' => true, 'prompt' => Str::limit($prompt, 200)],
|
|
]);
|
|
|
|
AiUsageLog::create([
|
|
'account_id' => $this->workspace->account_id,
|
|
'workspace_id' => $this->workspace->id,
|
|
'user_id' => $this->userId,
|
|
'post_id' => $this->post?->id,
|
|
'type' => UsageType::Image,
|
|
'provider' => 'gemini',
|
|
]);
|
|
|
|
$this->collector->push([
|
|
'id' => $media->id,
|
|
'path' => $media->path,
|
|
'url' => $media->url,
|
|
'mime_type' => 'image/png',
|
|
'type' => 'image',
|
|
]);
|
|
|
|
return "Generated a {$orientationString} image (id: {$media->id}) and attached it to the post.";
|
|
}
|
|
|
|
public function schema(JsonSchema $schema): array
|
|
{
|
|
return [
|
|
'prompt' => $schema->string()
|
|
->description('A detailed visual description of the image to generate. Include subject, style, composition, mood, and any text that should appear in the image.')
|
|
->required(),
|
|
'orientation' => $schema->string()
|
|
->enum(['square', 'portrait', 'vertical', 'horizontal'])
|
|
->description('"square" (1:1) for LinkedIn, Facebook. "portrait" (4:5) for Instagram Feed, Threads. "vertical" (9:16) for Instagram Reel/Story, Pinterest, TikTok. "horizontal" (16:9) for X/Twitter.')
|
|
->required(),
|
|
];
|
|
}
|
|
}
|