Sempre tive problemas para fazer arte (plásticas) cagada sempre foi meio fácil de fazer.
Neste post vou mostrar como integrei o Google Imagen 3 (via API do Gemini) ao blog para gerar capas automaticamente, usando a mesma chave de API que já estava configurada para o comentarista de IA.
Neste post vou mostrar como integrei o Google Imagen 3 (via API do Gemini) ao blog para gerar capas automaticamente, usando a mesma chave de API que já estava configurada para o comentarista de IA.
1. O que foi construído
O fluxo final ficou assim: no painel de edição (e criação) de artigos, o autor descreve a imagem que quer, marca opcionalmente se quer incluir o conteúdo do artigo e/ou sua bio como contexto, clica em Gerar capa com IA e em alguns segundos a imagem aparece como capa do post, comprimida em WebP, salva em storage/covers/.
2. A migration
Precisei de três colunas novas na tabela posts:
Schema::table(\'posts\', function (Blueprint $table) {
$table->text(\'cover_image_prompt\')->nullable()->after(\'cover_image\');
$table->boolean(\'cover_image_use_content\')->default(false)->after(\'cover_image_prompt\');
$table->boolean(\'cover_image_use_bio\')->default(false)->after(\'cover_image_use_content\');
});O cover_image_prompt persiste o prompt do artigo, assim o autor pode regenerar a capa futuramente sem redigitar tudo. Os dois booleanos guardam a preferência de contexto.
3. Salvando a imagem retornada pela API
O Google Imagen devolve a imagem como uma string base64. Adicionei um método storeFromBase64() no ImageService existente para lidar com isso:
public function storeFromBase64(
string $base64,
string $directory,
int $maxWidth = 1920,
int $maxHeight = 1080,
int $quality = 82
): string {
$imageData = base64_decode($base64);
$tmpPath = sys_get_temp_dir() . \'/\' . Str::uuid() . \'.png\';
file_put_contents($tmpPath, $imageData);
try {
$image = $this->manager->read($tmpPath);
$image->scaleDown($maxWidth, $maxHeight);
$filename = Str::uuid() . \'.webp\';
$path = $directory . \'/\' . $filename;
$encoded = $image->toWebp($quality);
Storage::disk(\'public\')->put($path, (string) $encoded);
} finally {
@unlink($tmpPath);
}
return $path;
}A lógica é simples: decodifica o base64 em um arquivo temporário, usa o Intervention Image para redimensionar e converter para WebP, salva no disco público e apaga o temporário. O bloco finally garante que o arquivo temporário seja removido mesmo em caso de erro.
4. O ImagenService
Criei um serviço dedicado para a geração:
class ImagenService
{
private const MODEL = \'gemini-3.1-flash-image-preview\';
public function generateCoverImage(Post $post, User $user): string
{
$apiKey = $user->gemini_api_key;
$prompt = $this->buildPrompt($post, $user);
$response = Http::withoutVerifying()
->timeout(120)
->post(
"https://generativelanguage.googleapis.com/v1beta/models/"
. self::MODEL . ":predict?key={$apiKey}",
[
\'instances\' => [[\'prompt\' => $prompt]],
\'parameters\' => [
\'sampleCount\' => 1,
\'aspectRatio\' => \'16:9\',
],
]
);
$response->throw();
$base64 = $response->json(\'predictions.0.bytesBase64Encoded\')
?? throw new \RuntimeException(\'Resposta inesperada da API do Google Imagen.\');
return app(ImageService::class)->storeFromBase64($base64, \'covers\');
}
private function buildPrompt(Post $post, User $user): string
{
$parts = [trim($post->cover_image_prompt)];
if ($post->cover_image_use_content) {
$content = Str::limit(strip_tags($post->content ?? \'\'), 500);
if ($content) {
$parts[] = "Contexto do artigo \"{->title}\": {$content}";
}
}
if ($post->cover_image_use_bio && $user->about_me) {
$parts[] = "Sobre o autor do blog: {$user->about_me}";
}
$parts[] = \'Estilo: fotografia profissional, alta qualidade, adequada para blog, formato 16:9.\';
return implode("\n\n", $parts);
}
}Alguns pontos importantes:
- O modelo gemini-3.1-flash-image-preview é a versão rápida do Google 3, para qualidade máxima use gemini-3-pro-image-preview .
- A chave de API é a mesma já armazenada no campo gemini_api_key (criptografado) do usuário, sem nenhuma configuração extra.
- O parâmetro aspectRatio: 16:9 garante que a imagem já venha no formato ideal para capa de blog.
- O Http::withoutVerifying() é necessário no ambiente local com Docker onde o certificado SSL pode não estar configurado.
5. Integração no Livewire Volt
A adição na view de edição segue o mesmo padrão do botão de comentário IA já existente:
public function generateAiCover(): void
{
$this->validate([\'cover_image_prompt\' => \'required|string|max:2000\']);
$this->generatingCover = true;
$this->coverStatus = null;
try {
$this->post->update([
\'cover_image_prompt\' => $this->cover_image_prompt,
\'cover_image_use_content\' => $this->cover_image_use_content,
\'cover_image_use_bio\' => $this->cover_image_use_bio,
]);
$this->post->refresh();
if ($this->existing_cover_image) {
Storage::disk(\'public\')->delete($this->existing_cover_image);
}
$path = app(ImagenService::class)->generateCoverImage($this->post, auth()->user());
$this->post->update([\'cover_image\' => $path]);
$this->existing_cover_image = $path;
$this->coverStatus = \'success\';
} catch (\Throwable $e) {
$this->coverStatus = \'error:\' . $e->getMessage();
} finally {
$this->generatingCover = false;
}
}Na view de criação (onde o post ainda não existe no banco), o truque foi criar uma instância temporária do modelo para passar ao serviço sem persistir:
$tempPost = new \App\Models\Post([
\'title\' => $this->title ?: \'Artigo sem título\',
\'content\' => $this->content,
\'cover_image_prompt\' => $this->cover_image_prompt,
\'cover_image_use_content\' => $this->cover_image_use_content,
\'cover_image_use_bio\' => $this->cover_image_use_bio,
]);
$path = app(ImagenService::class)->generateCoverImage($tempPost, auth()->user());
$this->ai_generated_cover_path = $path;A imagem fica guardada no storage e o caminho em $ai_generated_cover_path. Quando o artigo for salvo, ela é vinculada ao post normalmente.
6. O resultado
A seção de geração aparece logo abaixo do campo de upload manual de capa, dentro de um card com borda índigo. O autor escreve o prompt, marca o que quer incluir e clica no botão, a imagem aparece em poucos segundos, já no formato 16:9 e convertida para WebP.
O prompt e as preferências são salvos junto com o artigo, então regenerar a capa no futuro é só clicar no botão novamente.
7. Custo e limites
O Google Imagen 3 via API do Gemini tem uma cota gratuita generosa para desenvolvimento. Para um blog pessoal, dificilmente você vai chegar perto do limite. Consulte a página de preços do Google AI para detalhes atualizados.
Isso tudo ta la no GitHub pra quem quiser usar ou copiar ou tanto faz.
Isso tudo ta la no GitHub pra quem quiser usar ou copiar ou tanto faz.