summaryrefslogtreecommitdiff
path: root/internal/image/gemini.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/image/gemini.go')
-rw-r--r--internal/image/gemini.go329
1 files changed, 0 insertions, 329 deletions
diff --git a/internal/image/gemini.go b/internal/image/gemini.go
index e04b04e..05a2e0e 100644
--- a/internal/image/gemini.go
+++ b/internal/image/gemini.go
@@ -1,24 +1,15 @@
package image
import (
- "bytes"
"context"
- "crypto/md5"
- "encoding/base64"
- "encoding/hex"
"fmt"
- "image"
- _ "image/jpeg"
- "image/png"
"io"
"net/http"
"os"
"strings"
- "time"
"google.golang.org/genai"
- "codeberg.org/snonux/comicforge/internal/apicircuit"
"codeberg.org/snonux/comicforge/internal/httpctx"
)
@@ -303,326 +294,6 @@ func (c *GeminiProvider) ensureReady() error {
return nil
}
-func (c *GeminiProvider) resolveTranslation(_ context.Context, opts *SearchOptions, translation string) (string, error) {
- if translation != "" {
- fmt.Printf("Using provided translation: %s -> %s\n", opts.Query, translation)
- return translation, nil
- }
-
- return opts.Query, nil
-}
-
-func (c *GeminiProvider) resolvePrompt(ctx context.Context, opts *SearchOptions, translatedWord string) (string, error) {
- if customPrompt := strings.TrimSpace(opts.CustomPrompt); customPrompt != "" {
- if len(customPrompt) > maxCustomPrompt {
- customPrompt = customPrompt[:maxCustomPrompt-3] + "..."
- }
- fmt.Printf("Using custom prompt: %s\n", customPrompt)
- return customPrompt, nil
- }
-
- return c.createEducationalPrompt(ctx, opts.Query, translatedWord), nil
-}
-
-func (c *GeminiProvider) buildPrompt(ctx context.Context, opts *SearchOptions) (string, string, error) {
- if opts == nil {
- return "", "", &SearchError{
- Provider: geminiSource,
- Code: "INVALID_OPTIONS",
- Message: "search options are required",
- }
- }
-
- translation := strings.TrimSpace(opts.Translation)
- if customPrompt := strings.TrimSpace(opts.CustomPrompt); customPrompt != "" {
- if len(customPrompt) > maxCustomPrompt {
- customPrompt = customPrompt[:maxCustomPrompt-3] + "..."
- }
- fmt.Printf("Using custom prompt: %s\n", customPrompt)
- return customPrompt, translation, nil
- }
-
- translatedWord, err := c.resolveTranslation(ctx, opts, translation)
- if err != nil {
- return "", "", err
- }
-
- prompt, err := c.resolvePrompt(ctx, opts, translatedWord)
- if err != nil {
- return "", "", err
- }
-
- return prompt, translatedWord, nil
-}
-
-// createEducationalPrompt generates a prompt optimized for image generation.
-func (c *GeminiProvider) createEducationalPrompt(ctx context.Context, query, translation string) string {
- subject := promptSubject(translation, query)
-
- scene, err := c.generateSceneDescription(ctx, query, translation)
- if err != nil {
- fmt.Printf(" Failed to generate scene: %v, using basic prompt\n", err)
- scene = ""
- }
- if scene != "" {
- scene = sanitizeSceneDescription(scene)
- if !usableSceneDescription(scene) {
- fmt.Printf(" Scene response was too short or generic, using basic prompt\n")
- scene = ""
- }
- }
-
- selectedStyle := chooseArtisticStyle()
- if selectedStyle == defaultArtisticStyle {
- fmt.Printf(" No artistic styles available, using generic prompt\n")
- }
- fmt.Printf(" Using image style: %s\n", selectedStyle)
-
- return buildEducationalPrompt(selectedStyle, scene, subject)
-}
-
-func (c *GeminiProvider) generateSceneDescription(ctx context.Context, query, translation string) (string, error) {
- fmt.Printf("Gemini Scene Generation: Creating scene for %q (%s)\n", query, translation)
-
- scene, err := geminiGenerateText(
- ctx,
- c,
- c.textModelName(),
- "You are helping create educational flashcards for language learning. Generate a brief, vivid scene description that incorporates the given English word in a memorable, contextual way. The scene should be visually interesting and help with memory retention. Keep it to 1-2 sentences, focusing on visual elements that can be illustrated. The subject (the English word) should be the clear focal point of the image, prominent and centered.",
- fmt.Sprintf("Create a scene description for the English word %q that would make a memorable flashcard image. Make sure %q is the main focus and most prominent element in the scene.", translation, translation),
- 0.7,
- 100,
- )
- if err != nil {
- return "", fmt.Errorf("scene generation failed: %w", err)
- }
- scene = sanitizeSceneDescription(scene)
- if !usableSceneDescription(scene) {
- return "", fmt.Errorf("scene generation returned unusable content")
- }
-
- fmt.Printf("Generated scene: %s\n", scene)
- return scene, nil
-}
-
-func (c *GeminiProvider) generateText(ctx context.Context, model, systemPrompt, userPrompt string, temperature float32, maxOutputTokens int32) (string, error) {
- temp := temperature
- resp, err := apicircuit.Execute(nil, c.Name(), apicircuit.CapabilityText, func() (*genai.GenerateContentResponse, error) {
- return c.client.Models.GenerateContent(ctx, model, []*genai.Content{
- genai.NewContentFromText(userPrompt, genai.RoleUser),
- }, &genai.GenerateContentConfig{
- SystemInstruction: genai.NewContentFromText(systemPrompt, genai.RoleUser),
- Temperature: &temp,
- MaxOutputTokens: maxOutputTokens,
- })
- })
- if err != nil {
- return "", fmt.Errorf("gemini API error: %w", err)
- }
-
- text := strings.TrimSpace(resp.Text())
- if text == "" {
- return "", fmt.Errorf("no response received")
- }
-
- return text, nil
-}
-
-func (c *GeminiProvider) generateImage(ctx context.Context, prompt, aspectRatio string) ([]byte, string, error) {
- if aspectRatio == "" {
- aspectRatio = geminiAspectRatio
- }
-
- cfg := &genai.GenerateContentConfig{
- ResponseModalities: []string{string(genai.ModalityImage)},
- ImageConfig: &genai.ImageConfig{
- AspectRatio: aspectRatio,
- },
- }
-
- resp, err := apicircuit.Execute(nil, c.Name(), apicircuit.CapabilityImage, func() (*genai.GenerateContentResponse, error) {
- return c.client.Models.GenerateContent(ctx, c.modelName(), []*genai.Content{
- genai.NewContentFromText(prompt, genai.RoleUser),
- }, cfg)
- })
- if err != nil {
- return nil, "", &SearchError{
- Provider: geminiSource,
- Code: "API_ERROR",
- Message: fmt.Sprintf("failed to generate image: %v", err),
- }
- }
-
- imageBytes, mimeType, err := extractGeneratedImage(resp)
- if err != nil {
- return nil, "", &SearchError{
- Provider: geminiSource,
- Code: "NO_RESULTS",
- Message: err.Error(),
- }
- }
-
- return imageBytes, mimeType, nil
-}
-
-func (c *GeminiProvider) generateImageWithRefs(ctx context.Context, prompt, aspectRatio string, refs [][]byte) ([]byte, string, error) {
- if aspectRatio == "" {
- aspectRatio = geminiAspectRatio
- }
-
- cfg := &genai.GenerateContentConfig{
- ResponseModalities: []string{string(genai.ModalityImage)},
- ImageConfig: &genai.ImageConfig{AspectRatio: aspectRatio},
- }
-
- parts := make([]*genai.Part, 0, len(refs)+1)
- for _, ref := range refs {
- if len(ref) > 0 {
- parts = append(parts, &genai.Part{
- InlineData: &genai.Blob{MIMEType: "image/png", Data: ref},
- })
- }
- }
- refNote := fmt.Sprintf(
- "The %d reference image(s) above show the exact character appearance that must be preserved. "+
- "Every character, animal, or object must look identical in the new image. Now generate:\n\n",
- len(refs),
- )
- parts = append(parts, &genai.Part{Text: refNote + prompt})
-
- resp, err := apicircuit.Execute(nil, c.Name(), apicircuit.CapabilityImage, func() (*genai.GenerateContentResponse, error) {
- return c.client.Models.GenerateContent(ctx, c.modelName(), []*genai.Content{
- {
- Role: string(genai.RoleUser),
- Parts: parts,
- },
- }, cfg)
- })
- if err != nil {
- return nil, "", &SearchError{
- Provider: geminiSource,
- Code: "API_ERROR",
- Message: fmt.Sprintf("failed to generate image with refs: %v", err),
- }
- }
-
- imageBytes, mimeType, err := extractGeneratedImage(resp)
- if err != nil {
- return nil, "", &SearchError{
- Provider: geminiSource,
- Code: "NO_RESULTS",
- Message: err.Error(),
- }
- }
-
- return imageBytes, mimeType, nil
-}
-
-func extractGeneratedImage(response *genai.GenerateContentResponse) ([]byte, string, error) {
- if response == nil {
- return nil, "", fmt.Errorf("no response from Gemini")
- }
-
- for _, candidate := range response.Candidates {
- if candidate == nil || candidate.Content == nil {
- continue
- }
-
- for _, part := range candidate.Content.Parts {
- if part == nil || part.InlineData == nil || len(part.InlineData.Data) == 0 {
- continue
- }
-
- mimeType := part.InlineData.MIMEType
- if mimeType == "" {
- mimeType = "image/png"
- }
-
- return append([]byte(nil), part.InlineData.Data...), mimeType, nil
- }
- }
-
- return nil, "", fmt.Errorf("no image data returned from Gemini")
-}
-
-func encodeDataURL(imageBytes []byte, mimeType string) (string, error) {
- if len(imageBytes) == 0 {
- return "", fmt.Errorf("no image bytes returned")
- }
-
- normalizedBytes, err := normalizePNG(imageBytes, mimeType)
- if err != nil {
- return "", err
- }
-
- return geminiDataPrefix + base64.StdEncoding.EncodeToString(normalizedBytes), nil
-}
-
-func decodeDataURL(url string) (io.ReadCloser, error) {
- header, payload, ok := strings.Cut(url, ",")
- if !ok || !strings.HasPrefix(header, "data:") || !strings.Contains(header, ";base64") {
- return nil, fmt.Errorf("unsupported data URI: %s", url)
- }
-
- data, err := base64.StdEncoding.DecodeString(payload)
- if err != nil {
- return nil, fmt.Errorf("decode data URI: %w", err)
- }
-
- return io.NopCloser(bytes.NewReader(data)), nil
-}
-
-func normalizePNG(imageBytes []byte, mimeType string) ([]byte, error) {
- if strings.EqualFold(strings.TrimSpace(mimeType), "image/png") {
- return append([]byte(nil), imageBytes...), nil
- }
-
- img, _, err := image.Decode(bytes.NewReader(imageBytes))
- if err != nil {
- return nil, fmt.Errorf("decode generated image: %w", err)
- }
-
- var buffer bytes.Buffer
- if err := png.Encode(&buffer, img); err != nil {
- return nil, fmt.Errorf("encode generated image as png: %w", err)
- }
-
- return buffer.Bytes(), nil
-}
-
-func decodedImageDimensions(imageBytes []byte) (int, int, error) {
- cfg, _, err := image.DecodeConfig(bytes.NewReader(imageBytes))
- if err != nil {
- return 0, 0, fmt.Errorf("decode generated image dimensions: %w", err)
- }
-
- return cfg.Width, cfg.Height, nil
-}
-
-func (c *GeminiProvider) generateImageID(word string) string {
- hash := md5.Sum([]byte(word))
- return hex.EncodeToString(hash[:])[:8]
-}
-
-func (c *GeminiProvider) buildAttribution(result *SearchResult, prompt string) string {
- if result == nil {
- return ""
- }
-
- var attribution strings.Builder
- attribution.WriteString("Image generated by Google Gemini Nano Banana\n\n")
- fmt.Fprintf(&attribution, "Model: %s\n", c.modelName())
- fmt.Fprintf(&attribution, "Text model: %s\n", c.textModelName())
- fmt.Fprintf(&attribution, "Aspect ratio: %s\n", geminiAspectRatio)
- fmt.Fprintf(&attribution, "Size: %dx%d\n", result.Width, result.Height)
- if result.Description != "" {
- fmt.Fprintf(&attribution, "Result: %s\n", result.Description)
- }
- fmt.Fprintf(&attribution, "\nPrompt used:\n%s\n", prompt)
- fmt.Fprintf(&attribution, "\nGenerated at: %s\n", time.Now().Format("2006-01-02 15:04:05"))
- return attribution.String()
-}
-
func (c *GeminiProvider) modelName() string {
if c == nil || c.config == nil || strings.TrimSpace(c.config.Model) == "" {
return DefaultGeminiImageModel