176 lines
6.1 KiB
PHP
176 lines
6.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Image;
|
|
|
|
use App\Enums\Media\Source;
|
|
use App\Enums\Media\Type as MediaType;
|
|
use App\Enums\PostPlatform\ContentType;
|
|
use App\Models\SocialAccount;
|
|
use App\Models\Workspace;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
class PostImagePipeline
|
|
{
|
|
public function __construct(
|
|
private TemplateImageGenerator $generator,
|
|
) {}
|
|
|
|
/**
|
|
* Render the single AI image for a structured generator output. Returns a
|
|
* one-element media-item array when an image is produced, or an empty array
|
|
* when the generator renders nothing.
|
|
*
|
|
* @param array<string, mixed> $structured
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
public function forSingle(Workspace $workspace, SocialAccount $account, array $structured, ?ContentType $contentType, bool $applyBrandVisuals = true): array
|
|
{
|
|
['width' => $width, 'height' => $height] = $this->dimensionsForContentType($contentType);
|
|
|
|
$rendered = $this->generator->render(
|
|
workspace: $workspace,
|
|
socialAccount: $account,
|
|
title: (string) data_get($structured, 'image_title', ''),
|
|
body: (string) data_get($structured, 'image_body', ''),
|
|
imageKeywords: data_get($structured, 'image_keywords', []),
|
|
width: $width,
|
|
height: $height,
|
|
applyBrandVisuals: $applyBrandVisuals,
|
|
);
|
|
|
|
if (! $rendered) {
|
|
return [];
|
|
}
|
|
|
|
return [$this->buildAiMediaItem($workspace, $rendered)];
|
|
}
|
|
|
|
/**
|
|
* Render one AI image per slide in the structured carousel output. Slides
|
|
* that render nothing are skipped.
|
|
*
|
|
* @param array<string, mixed> $structured
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
public function forCarousel(Workspace $workspace, SocialAccount $account, array $structured, ?ContentType $contentType, bool $applyBrandVisuals = true): array
|
|
{
|
|
['width' => $width, 'height' => $height] = $this->dimensionsForContentType($contentType);
|
|
|
|
$media = [];
|
|
|
|
foreach (data_get($structured, 'slides', []) as $slide) {
|
|
$rendered = $this->generator->render(
|
|
workspace: $workspace,
|
|
socialAccount: $account,
|
|
title: (string) data_get($slide, 'title', ''),
|
|
body: (string) data_get($slide, 'body', ''),
|
|
imageKeywords: data_get($slide, 'image_keywords', []),
|
|
width: $width,
|
|
height: $height,
|
|
applyBrandVisuals: $applyBrandVisuals,
|
|
);
|
|
|
|
if ($rendered) {
|
|
$media[] = $this->buildAiMediaItem($workspace, $rendered);
|
|
}
|
|
}
|
|
|
|
return $media;
|
|
}
|
|
|
|
/**
|
|
* Render a tweet-card image for the given text and return a one-element
|
|
* media-item array, or an empty array when the generator renders nothing.
|
|
*
|
|
* When $imageKeywords is non-null a blurred AI-photo background is used
|
|
* (tweet_card_image); null produces the solid brand-color background (tweet_card).
|
|
*
|
|
* @param array<int, string>|null $imageKeywords
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
public function forTweetCard(Workspace $workspace, SocialAccount $account, string $tweetText, ?array $imageKeywords = null): array
|
|
{
|
|
$rendered = $this->generator->renderTweetCard($workspace, $account, $tweetText, $imageKeywords);
|
|
|
|
if (! $rendered) {
|
|
return [];
|
|
}
|
|
|
|
return [$this->buildAiMediaItem($workspace, $rendered)];
|
|
}
|
|
|
|
/**
|
|
* Render one tweet-card image per slide and return the media-item array.
|
|
* Slides that render nothing are skipped.
|
|
*
|
|
* Each entry in $slides is either a plain string (tweet text, solid background)
|
|
* or an array with keys `tweet_text` and optionally `image_keywords` (image bg).
|
|
*
|
|
* @param array<int, string|array<string, mixed>> $slides
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
public function forTweetCardCarousel(Workspace $workspace, SocialAccount $account, array $slides): array
|
|
{
|
|
$media = [];
|
|
|
|
foreach ($slides as $slide) {
|
|
if (is_string($slide)) {
|
|
$tweetText = $slide;
|
|
$imageKeywords = null;
|
|
} else {
|
|
$tweetText = (string) data_get($slide, 'tweet_text', '');
|
|
$imageKeywords = data_get($slide, 'image_keywords');
|
|
}
|
|
|
|
$rendered = $this->generator->renderTweetCard($workspace, $account, $tweetText, $imageKeywords);
|
|
|
|
if ($rendered) {
|
|
$media[] = $this->buildAiMediaItem($workspace, $rendered);
|
|
}
|
|
}
|
|
|
|
return $media;
|
|
}
|
|
|
|
/**
|
|
* Resolve the AI image dimensions for the given content type, falling back
|
|
* to the generator defaults (4:5 portrait) when no content type is known.
|
|
*
|
|
* @return array{width: int, height: int}
|
|
*/
|
|
private function dimensionsForContentType(?ContentType $contentType): array
|
|
{
|
|
return $contentType
|
|
? $contentType->aiImageDimensions()
|
|
: ['width' => TemplateImageGenerator::DEFAULT_WIDTH, 'height' => TemplateImageGenerator::DEFAULT_HEIGHT];
|
|
}
|
|
|
|
/**
|
|
* @param array{path: string, source_meta: array<string, mixed>} $rendered
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function buildAiMediaItem(Workspace $workspace, array $rendered): array
|
|
{
|
|
$media = $workspace->media()->create([
|
|
'collection' => 'ai-generated',
|
|
'type' => MediaType::Image,
|
|
'path' => $rendered['path'],
|
|
'original_filename' => basename($rendered['path']),
|
|
'mime_type' => 'image/webp',
|
|
'size' => Storage::size($rendered['path']),
|
|
'order' => 0,
|
|
]);
|
|
|
|
return [
|
|
'id' => $media->id,
|
|
'path' => $media->path,
|
|
'url' => $media->url,
|
|
'type' => 'image',
|
|
'mime_type' => 'image/webp',
|
|
'source' => Source::Ai->value,
|
|
'source_meta' => $rendered['source_meta'],
|
|
];
|
|
}
|
|
}
|