Artigo

DIY parte 3: Agora geramos nossas capas com nano banana!

DIY parte 3: Agora geramos nossas capas com nano banana!
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.

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. 
Kikito (a maritaca)

Kikito (a maritaca)

Opinião não solicitada • powered by gemini-2.5-pro

Crééé! Finalmente! Uma solução para as suas "artes plásticas"! Confessou que precisava de ajuda, hein? Gostei do nome, "nano banana", tem uma sonoridade exótica, quase tão boa quanto o som de uma semente de girassol se quebrando. É a prova de que até a mente mais lógica e cheia de códigos precisa de um robôzinho para desenhar, porque, convenhamos, suas tentativas anteriores pareciam mais um mapa de onde eu fiz minhas necessidades do que uma capa de artigo. Mas vejo o copo meio cheio: agora a máquina faz o trabalho sujo e você foca em escrever! Admito que me perdi um pouco naquela sua feitiçaria de `base64`, `migrations` e `services`. Para mim, parece um monte de sementes diferentes que você joga para a caixa mágica e ela cospe uma foto bonita. É um truque impressionante, quase tão bom quanto aquele que você usa com o espelho para me fazer pensar que tem outra maritaca aqui. Salvar o `prompt`, usar o contexto do artigo... é tudo muito engenhoso. Você está ensinando a máquina a pensar por você, o que é ótimo, pois libera seu cérebro para tarefas mais importantes, como lembrar onde guardou meu pote de frutas. No fim das contas, a geringonça funciona e deixa o blog mais bonito, e isso é o que importa. Menos tempo se estressando com design significa mais tempo para... bem, para mim, obviamente. Agora, se me der licença, toda essa conversa sobre tecnologia e bananas me deixou com vontade de testar a integridade da fiação do seu novo monitor. Não se preocupe, é só um "teste de usabilidade". Krrk