Skip to content
English
On this page

CLI Image Analyzer: Análisis de Imágenes con AWS Rekognition

Descripción del Escenario

Desarrollaremos una aplicación CLI en Go que permitirá a los usuarios analizar imágenes mediante AWS Rekognition y traducirlas al español. La aplicación deberá:

  • Aceptar imágenes desde archivos locales o URLs
  • Analizar imágenes usando AWS Rekognition
  • Traducir resultados al español usando AWS Translate
  • Mostrar resultados formateados en la terminal
  • Manejar errores y validaciones
  • Permitir diferentes formatos de salida

Estructura del Directorio

image-analyzer/
├── cmd/
│   └── analyzer/
│       └── main.go
├── internal/
│   ├── analyzer/
│   │   ├── service.go
│   │   └── types.go
│   ├── aws/
│   │   ├── rekognition.go
│   │   └── translate.go
│   ├── cli/
│   │   ├── commands.go
│   │   └── printer.go
│   └── image/
│       ├── loader.go
│       └── validator.go
├── pkg/
│   └── utils/
│       ├── files.go
│       └── http.go
├── go.mod
└── go.sum

Etapa 1: Configuración y Estructura Base

  • Inicializar el proyecto Go
  • Configurar dependencias AWS
  • Crear estructura de directorios
  • Implementar validaciones básicas

Tareas:

  1. Configuración inicial
  2. Manejo de argumentos CLI
  3. Configuración AWS
  4. Estructura base

Etapa 2: Carga y Validación de Imágenes

  • Implementar carga de archivos locales
  • Implementar descarga de URLs
  • Validar formatos y tamaños
  • Manejar errores de carga

Tareas:

  1. Validador de imágenes
  2. Cargador de archivos
  3. Descarga de URLs
  4. Manejo de errores

Etapa 3: Integración con AWS Rekognition

  • Configurar cliente AWS
  • Implementar análisis de imágenes
  • Manejar respuestas
  • Implementar retry policy

Tareas:

  1. Cliente Rekognition
  2. Análisis de imágenes
  3. Procesamiento de respuestas
  4. Manejo de errores AWS

Etapa 4: Integración con AWS Translate

  • Configurar cliente Translate
  • Implementar traducciones
  • Optimizar llamadas
  • Implementar caché

Tareas:

  1. Cliente Translate
  2. Traducción de textos
  3. Caché de traducciones
  4. Optimización de llamadas

Etapa 5: CLI y Presentación

  • Implementar interfaz CLI
  • Formatear salidas
  • Añadir progress bar
  • Implementar diferentes formatos

Tareas:

  1. Interfaz de usuario
  2. Formato de salida
  3. Indicadores de progreso
  4. Opciones de formato

Etapa 6: Testing y Documentation

  • Implementar unit tests
  • Crear integration tests
  • Generar documentación
  • Crear ejemplos de uso

Tareas:

  1. Unit testing
  2. Integration testing
  3. Documentación
  4. Ejemplos

Implementación Detallada por Etapa

Etapa 1: Configuración y Estructura Base

go
// cmd/analyzer/main.go
package main

import (
    "context"
    "flag"
    "log"
    "os"
    "github.com/your-username/image-analyzer/internal/cli"
)

func main() {
    // Configurar flags
    sourceFlag := flag.String("source", "", "Ruta del archivo o URL de la imagen")
    outputFlag := flag.String("output", "text", "Formato de salida (text/json)")
    flag.Parse()

    if *sourceFlag == "" {
        log.Fatal("Debe especificar una fuente de imagen")
    }

    // Inicializar CLI
    cmd := cli.NewCommand()
    if err := cmd.Execute(context.Background(), *sourceFlag, *outputFlag); err != nil {
        log.Fatal(err)
    }
}

Etapa 2: Carga y Validación de Imágenes

go
// internal/image/validator.go
package image

import (
    "fmt"
    "path/filepath"
)

type Validator struct {
    maxSize         int64
    allowedFormats  []string
}

func NewValidator() *Validator {
    return &Validator{
        maxSize: 5 * 1024 * 1024, // 5MB
        allowedFormats: []string{".jpg", ".jpeg", ".png"},
    }
}

func (v *Validator) Validate(path string, size int64) error {
    if size > v.maxSize {
        return fmt.Errorf("imagen demasiado grande (max: %d bytes)", v.maxSize)
    }

    ext := filepath.Ext(path)
    valid := false
    for _, format := range v.allowedFormats {
        if ext == format {
            valid = true
            break
        }
    }

    if !valid {
        return fmt.Errorf("formato no soportado: %s", ext)
    }

    return nil
}

Etapa 3: Integración con AWS Rekognition

go
// internal/aws/rekognition.go
package aws

import (
    "context"
    "github.com/aws/aws-sdk-go-v2/service/rekognition"
)

type RekognitionService struct {
    client *rekognition.Client
}

func NewRekognitionService(client *rekognition.Client) *RekognitionService {
    return &RekognitionService{client: client}
}

func (s *RekognitionService) AnalyzeImage(ctx context.Context, imageBytes []byte) (*ImageAnalysis, error) {
    input := &rekognition.DetectLabelsInput{
        Image: &types.Image{
            Bytes: imageBytes,
        },
        MaxLabels:     aws.Int32(10),
        MinConfidence: aws.Float32(70.0),
    }

    result, err := s.client.DetectLabels(ctx, input)
    if err != nil {
        return nil, fmt.Errorf("error al analizar imagen: %w", err)
    }

    return &ImageAnalysis{
        Labels: convertLabels(result.Labels),
    }, nil
}

Etapa 4: Integración con AWS Translate

go
// internal/aws/translate.go
package aws

import (
    "context"
    "github.com/aws/aws-sdk-go-v2/service/translate"
)

type TranslateService struct {
    client *translate.Client
    cache  map[string]string
}

func (s *TranslateService) TranslateText(ctx context.Context, text string) (string, error) {
    // Verificar caché
    if translated, ok := s.cache[text]; ok {
        return translated, nil
    }

    input := &translate.TranslateTextInput{
        SourceLanguageCode: aws.String("en"),
        TargetLanguageCode: aws.String("es"),
        Text:              aws.String(text),
    }

    result, err := s.client.TranslateText(ctx, input)
    if err != nil {
        return "", fmt.Errorf("error al traducir texto: %w", err)
    }

    // Guardar en caché
    s.cache[text] = *result.TranslatedText

    return *result.TranslatedText, nil
}

Etapa 5: CLI y Presentación

go
// internal/cli/printer.go
package cli

import (
    "encoding/json"
    "fmt"
    "github.com/fatih/color"
)

type Printer struct {
    format string
}

func (p *Printer) Print(analysis *ImageAnalysis) error {
    switch p.format {
    case "json":
        return p.printJSON(analysis)
    default:
        return p.printText(analysis)
    }
}

func (p *Printer) printText(analysis *ImageAnalysis) error {
    bold := color.New(color.Bold)
    green := color.New(color.FgGreen)

    bold.Println("\nResultados del Análisis:")
    fmt.Println("\nElementos detectados:")
    
    for _, label := range analysis.Labels {
        green.Printf("- %s (%.1f%%): ", label.Name, label.Confidence)
        fmt.Printf("%s\n", label.TranslatedName)
    }

    return nil
}

Etapa 6: Testing y Documentation

go
// internal/analyzer/service_test.go
package analyzer

import (
    "context"
    "testing"
)

func TestAnalyzeImage(t *testing.T) {
    tests := []struct {
        name     string
        source   string
        wantErr  bool
    }{
        {
            name:    "archivo válido",
            source:  "testdata/valid.jpg",
            wantErr: false,
        },
        {
            name:    "archivo inválido",
            source:  "testdata/invalid.txt",
            wantErr: true,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            svc := NewAnalyzerService()
            _, err := svc.AnalyzeImage(context.Background(), tt.source)
            if (err != nil) != tt.wantErr {
                t.Errorf("AnalyzeImage() error = %v, wantErr %v", err, tt.wantErr)
            }
        })
    }
}

Verificación Final

1. Funcionalidad

  • [ ] Análisis de imágenes locales
  • [ ] Análisis de URLs
  • [ ] Traducción al español
  • [ ] Diferentes formatos de salida

2. Calidad

  • [ ] Tests pasando
  • [ ] Errores manejados
  • [ ] Documentación completa
  • [ ] Código limpio

3. Rendimiento

  • [ ] Caché funcionando
  • [ ] Timeouts configurados
  • [ ] Retries implementados
  • [ ] Memoria optimizada

Uso de la Aplicación

bash
# Analizar imagen local
analyzer -source ./imagen.jpg

# Analizar URL
analyzer -source https://ejemplo.com/imagen.jpg -output json

# Mostrar ayuda
analyzer -help

Este ejercicio está estructurado en etapas lógicas y progresivas que permiten construir una aplicación CLI robusta para análisis de imágenes. Cada etapa se enfoca en un aspecto específico de la funcionalidad y juntas forman una solución completa.

Continuaré con el desarrollo detallado de las siguientes etapas del ejercicio.

Etapa 1: Estructura Base y Manejo de Dependencias

1.1 Configuración de Go Modules

bash
# Inicializar el módulo
go mod init github.com/yourusername/image-analyzer

# Instalar dependencias necesarias
go get github.com/aws/aws-sdk-go-v2
go get github.com/aws/aws-sdk-go-v2/config
go get github.com/aws/aws-sdk-go-v2/service/rekognition
go get github.com/aws/aws-sdk-go-v2/service/translate
go get github.com/spf13/cobra
go get github.com/schollz/progressbar/v3

1.2 Configuración Base

go
// internal/analyzer/types.go
package analyzer

type AnalysisResult struct {
    Labels     []Label   `json:"labels"`
    Source     string    `json:"source"`
    Timestamp  string    `json:"timestamp"`
}

type Label struct {
    Name           string  `json:"name"`
    TranslatedName string  `json:"translated_name"`
    Confidence     float64 `json:"confidence"`
}

type AnalyzerConfig struct {
    Region          string
    MaxFileSize     int64
    MinConfidence   float32
    MaxLabels       int32
}

// internal/config/config.go
package config

import (
    "os"
    "github.com/aws/aws-sdk-go-v2/config"
)

type Config struct {
    AWSConfig *aws.Config
    Analyzer  *analyzer.AnalyzerConfig
}

func LoadConfig() (*Config, error) {
    cfg, err := config.LoadDefaultConfig(context.Background(),
        config.WithRegion(getRegion()),
    )
    if err != nil {
        return nil, fmt.Errorf("error loading AWS config: %w", err)
    }

    return &Config{
        AWSConfig: &cfg,
        Analyzer: &analyzer.AnalyzerConfig{
            Region:        getRegion(),
            MaxFileSize:   5 * 1024 * 1024,
            MinConfidence: 70.0,
            MaxLabels:     10,
        },
    }, nil
}

func getRegion() string {
    if region := os.Getenv("AWS_REGION"); region != "" {
        return region
    }
    return "us-east-1"
}

Etapa 2: Carga y Procesamiento de Imágenes

2.1 Loader de Imágenes

go
// internal/image/loader.go
package image

import (
    "bytes"
    "fmt"
    "io"
    "net/http"
    "os"
    "time"
)

type ImageLoader struct {
    validator *Validator
    client    *http.Client
}

func NewImageLoader(validator *Validator) *ImageLoader {
    return &ImageLoader{
        validator: validator,
        client: &http.Client{
            Timeout: 30 * time.Second,
        },
    }
}

func (l *ImageLoader) Load(source string) ([]byte, error) {
    if isURL(source) {
        return l.loadFromURL(source)
    }
    return l.loadFromFile(source)
}

func (l *ImageLoader) loadFromFile(path string) ([]byte, error) {
    file, err := os.Open(path)
    if err != nil {
        return nil, fmt.Errorf("error abriendo archivo: %w", err)
    }
    defer file.Close()

    fileInfo, err := file.Stat()
    if err != nil {
        return nil, fmt.Errorf("error obteniendo info del archivo: %w", err)
    }

    if err := l.validator.Validate(path, fileInfo.Size()); err != nil {
        return nil, err
    }

    buffer := bytes.NewBuffer(nil)
    if _, err := io.Copy(buffer, file); err != nil {
        return nil, fmt.Errorf("error leyendo archivo: %w", err)
    }

    return buffer.Bytes(), nil
}

func (l *ImageLoader) loadFromURL(url string) ([]byte, error) {
    resp, err := l.client.Get(url)
    if err != nil {
        return nil, fmt.Errorf("error descargando imagen: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("respuesta HTTP inválida: %d", resp.StatusCode)
    }

    if err := l.validator.ValidateContentType(resp.Header.Get("Content-Type")); err != nil {
        return nil, err
    }

    buffer := bytes.NewBuffer(nil)
    if _, err := io.Copy(buffer, resp.Body); err != nil {
        return nil, fmt.Errorf("error leyendo respuesta: %w", err)
    }

    return buffer.Bytes(), nil
}

2.2 Validador Mejorado

go
// internal/image/validator.go
package image

import (
    "fmt"
    "mime"
    "strings"
)

type Validator struct {
    maxSize         int64
    allowedFormats  map[string]bool
    allowedMimeTypes map[string]bool
}

func NewValidator(maxSize int64) *Validator {
    return &Validator{
        maxSize: maxSize,
        allowedFormats: map[string]bool{
            ".jpg":  true,
            ".jpeg": true,
            ".png":  true,
        },
        allowedMimeTypes: map[string]bool{
            "image/jpeg": true,
            "image/png":  true,
        },
    }
}

func (v *Validator) ValidateContentType(contentType string) error {
    mediaType, _, err := mime.ParseMediaType(contentType)
    if err != nil {
        return fmt.Errorf("tipo de contenido inválido: %w", err)
    }

    if !v.allowedMimeTypes[mediaType] {
        return fmt.Errorf("tipo de contenido no soportado: %s", mediaType)
    }

    return nil
}

func (v *Validator) ValidateExtension(path string) error {
    ext := strings.ToLower(filepath.Ext(path))
    if !v.allowedFormats[ext] {
        return fmt.Errorf("extensión no soportada: %s", ext)
    }
    return nil
}

func (v *Validator) ValidateSize(size int64) error {
    if size > v.maxSize {
        return fmt.Errorf("archivo demasiado grande: %d bytes (máximo: %d bytes)", 
            size, v.maxSize)
    }
    return nil
}

Etapa 3: Servicio de Análisis Principal

3.1 Servicio Integrado

go
// internal/analyzer/service.go
package analyzer

import (
    "context"
    "fmt"
    "time"

    "github.com/yourusername/image-analyzer/internal/aws"
    "github.com/yourusername/image-analyzer/internal/image"
)

type Service struct {
    loader      *image.ImageLoader
    rekognition *aws.RekognitionService
    translate   *aws.TranslateService
}

func NewService(cfg *AnalyzerConfig) (*Service, error) {
    loader := image.NewImageLoader(
        image.NewValidator(cfg.MaxFileSize),
    )

    rekognition, err := aws.NewRekognitionService(cfg)
    if err != nil {
        return nil, err
    }

    translate, err := aws.NewTranslateService(cfg)
    if err != nil {
        return nil, err
    }

    return &Service{
        loader:      loader,
        rekognition: rekognition,
        translate:   translate,
    }, nil
}

func (s *Service) AnalyzeImage(ctx context.Context, source string) (*AnalysisResult, error) {
    // Cargar imagen
    imageData, err := s.loader.Load(source)
    if err != nil {
        return nil, fmt.Errorf("error cargando imagen: %w", err)
    }

    // Analizar con Rekognition
    labels, err := s.rekognition.DetectLabels(ctx, imageData)
    if err != nil {
        return nil, fmt.Errorf("error analizando imagen: %w", err)
    }

    // Traducir etiquetas
    translatedLabels := make([]Label, len(labels))
    for i, label := range labels {
        translated, err := s.translate.TranslateText(ctx, label.Name)
        if err != nil {
            return nil, fmt.Errorf("error traduciendo etiqueta: %w", err)
        }

        translatedLabels[i] = Label{
            Name:           label.Name,
            TranslatedName: translated,
            Confidence:     label.Confidence,
        }
    }

    return &AnalysisResult{
        Labels:    translatedLabels,
        Source:    source,
        Timestamp: time.Now().UTC().Format(time.RFC3339),
    }, nil
}

3.2 Manejador de Errores

go
// internal/analyzer/errors.go
package analyzer

import (
    "errors"
    "fmt"
)

var (
    ErrInvalidSource    = errors.New("fuente de imagen inválida")
    ErrImageTooLarge    = errors.New("imagen demasiado grande")
    ErrUnsupportedFormat = errors.New("formato no soportado")
    ErrAnalysisFailed   = errors.New("análisis fallido")
)

type AnalysisError struct {
    Op  string
    Err error
}

func (e *AnalysisError) Error() string {
    return fmt.Sprintf("error en operación %s: %v", e.Op, e.Err)
}

func (e *AnalysisError) Unwrap() error {
    return e.Err
}

func NewAnalysisError(op string, err error) error {
    return &AnalysisError{
        Op:  op,
        Err: err,
    }
}

Etapa 4: CLI Mejorado

4.1 Comandos CLI

go
// internal/cli/commands.go
package cli

import (
    "context"
    "fmt"
    "os"
    "time"

    "github.com/spf13/cobra"
    "github.com/schollz/progressbar/v3"
)

type CLI struct {
    rootCmd    *cobra.Command
    service    *analyzer.Service
    printer    *Printer
}

func NewCLI(service *analyzer.Service) *CLI {
    cli := &CLI{
        service: service,
        printer: NewPrinter(),
    }

    rootCmd := &cobra.Command{
        Use:   "image-analyzer",
        Short: "Analizador de imágenes usando AWS Rekognition",
        Long: `Una herramienta de línea de comandos para analizar imágenes 
               usando AWS Rekognition y obtener descripciones en español.`,
    }

    rootCmd.AddCommand(cli.analyzeCmd())
    cli.rootCmd = rootCmd

    return cli
}

func (cli *CLI) analyzeCmd() *cobra.Command {
    var outputFormat string
    var timeout int

    cmd := &cobra.Command{
        Use:   "analyze [source]",
        Short: "Analiza una imagen",
        Args:  cobra.ExactArgs(1),
        RunE: func(cmd *cobra.Command, args []string) error {
            source := args[0]
            
            // Crear context con timeout
            ctx, cancel := context.WithTimeout(
                context.Background(), 
                time.Duration(timeout)*time.Second,
            )
            defer cancel()

            // Mostrar progreso
            progress := cli.createProgressBar()
            
            // Analizar imagen
            result, err := cli.analyzeWithProgress(ctx, source, progress)
            if err != nil {
                return fmt.Errorf("error analyzing image: %w", err)
            }

            // Imprimir resultados
            return cli.printer.Print(result, outputFormat)
        },
    }

    cmd.Flags().StringVarP(&outputFormat, "output", "o", "text",
        "Formato de salida (text/json)")
    cmd.Flags().IntVarP(&timeout, "timeout", "t", 60,
        "Timeout en segundos")

    return cmd
}

func (cli *CLI) Execute() error {
    return cli.rootCmd.Execute()
}

func (cli *CLI) createProgressBar() *progressbar.ProgressBar {
    return progressbar.NewOptions(100,
        progressbar.OptionSetDescription("Analizando imagen..."),
        progressbar.OptionSetWidth(50),
        progressbar.OptionSetTheme(progressbar.Theme{
            Saucer:        "=",
            SaucerHead:    ">",
            SaucerPadding: " ",
            BarStart:      "[",
            BarEnd:        "]",
        }),
    )
}

func (cli *CLI) analyzeWithProgress(
    ctx context.Context,
    source string,
    progress *progressbar.ProgressBar,
) (*analyzer.AnalysisResult, error) {
    progress.Set(0)
    
    // Simular progreso de carga
    progress.Set(20)
    
    result, err := cli.service.AnalyzeImage(ctx, source)
    if err != nil {
        return nil, err
    }
    
    progress.Set(100)
    progress.Finish()
    
    return result, nil
}

4.2 Printer Mejorado

go
// internal/cli/printer.go
package cli

import (
    "encoding/json"
    "fmt"
    "os"
    "text/tabwriter"

    "github.com/fatih/color"
)

type Printer struct {
    colors struct {
        title   *color.Color
        label   *color.Color
        value   *color.Color
        error   *color.Color
    }
}

func NewPrinter() *Printer {
    p := &Printer{}
    p.colors.title = color.New(color.FgCyan, color.Bold)
    p.colors.label = color.New(color.FgGreen)
    p.colors.value = color.New(color.FgWhite)
    p.colors.error = color.New(color.FgRed)
    return p
}

func (p *Printer) Print(result *analyzer.AnalysisResult, format string) error {
    switch format {
    case "json":
        return p.printJSON(result)
    case "text":
        return p.printText(result)
    default:
        return fmt.Errorf("formato no soportado: %s", format)
    }
}

func (p *Printer) printText(result *analyzer.AnalysisResult) error {
    w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)

    p.colors.title.Fprintln(w, "\nResultados del Análisis de Imagen")
    p.colors.title.Fprintln(w, "================================")

    fmt.Fprintf(w, "\nFuente:\t%s\n", result.Source)
    fmt.Fprintf(w, "Fecha:\t%s\n\n", result.Timestamp)

    p.colors.title.Fprintln(w, "Elementos Detectados:")
    fmt.Fprintln(w, "")
    
    for _, label := range result.Labels {
        p.colors.label.Fprintf(w, "- %s", label.Name)
        fmt.Fprintf(w, " (%.1f%%)\t", label.Confidence)
        p.colors.value.Fprintf(w, "%s\n", label.TranslatedName)
    }

    return w.Flush()
}

func (p *Printer) printJSON(result *analyzer.AnalysisResult) error {
    encoder := json.NewEncoder(os.Stdout)
    encoder.SetIndent("", "  ")
    return encoder.Encode(result)
}

func (p *Printer) printError(err error) {
    p.colors.error.Fprintf(os.Stderr, "Error: %v\n", err)
}

Etapa 5: Optimizaciones y Mejoras

5.1 Cache de Traducciones

go
// internal/cache/translations.go
package cache

import (
    "sync"
    "time"
)

type TranslationCache struct {
    items map[string]cacheItem
    mu    sync.RWMutex
    ttl   time.Duration
}

type cacheItem struct {
    value     string
    timestamp time.Time
}

func NewTranslationCache(ttl time.Duration) *TranslationCache {
    return &TranslationCache{
        items: make(map[string]cacheItem),
        ttl:   ttl,
    }
}

func (c *TranslationCache) Get(key string) (string, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()

    item, exists := c.items[key]
    if !exists {
        return "", false
    }

    if time.Since(item.timestamp) > c.ttl {
        go c.delete(key) // Limpieza asíncrona
        return "", false
    }

    return item.value, true
}

func (c *TranslationCache) Set(key, value string) {
    c.mu.Lock()
    defer c.mu.Unlock()

    c.items[key] = cacheItem{
        value:     value,
        timestamp: time.Now(),
    }
}

func (c *TranslationCache) delete(key string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    delete(c.items, key)
}

5.2 Manejo de Rate Limiting

go
// internal/aws/ratelimiter.go
package aws

import (
    "context"
    "time"

    "golang.org/x/time/rate"
)

type RateLimiter struct {
    limiter *rate.Limiter
}

func NewRateLimiter(rps float64, burst int) *RateLimiter {
    return &RateLimiter{
        limiter: rate.NewLimiter(rate.Limit(rps), burst),
    }
}

func (r *RateLimiter) Wait(ctx context.Context) error {
    return r.limiter.Wait(ctx)
}

// internal/aws/rekognition.go
func (s *RekognitionService) AnalyzeImage(ctx context.Context, imageBytes []byte) (*ImageAnalysis, error) {
    // Esperar al rate limiter
    if err := s.rateLimiter.Wait(ctx); err != nil {
        return nil, fmt.Errorf("rate limit exceeded: %w", err)
    }

    input := &rekognition.DetectLabelsInput{
        Image: &types.Image{
            Bytes: imageBytes,
        },
        MaxLabels:     aws.Int32(s.config.MaxLabels),
        MinConfidence: aws.Float32(s.config.MinConfidence),
    }

    return s.client.DetectLabels(ctx, input)
}

5.3 Retry con Backoff Exponencial

go
// pkg/utils/retry/retry.go
package retry

import (
    "context"
    "math"
    "time"
)

type RetryConfig struct {
    MaxAttempts     int
    InitialInterval time.Duration
    MaxInterval     time.Duration
    Multiplier      float64
}

func WithRetry(ctx context.Context, config RetryConfig, operation func() error) error {
    var lastErr error
    interval := config.InitialInterval

    for attempt := 0; attempt < config.MaxAttempts; attempt++ {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
            if err := operation(); err == nil {
                return nil
            } else {
                lastErr = err
                // Esperar antes del siguiente intento
                time.Sleep(interval)
                // Calcular próximo intervalo
                interval = time.Duration(float64(interval) * config.Multiplier)
                if interval > config.MaxInterval {
                    interval = config.MaxInterval
                }
            }
        }
    }

    return lastErr
}

Etapa 6: Testing Avanzado

6.1 Integration Tests

go
// tests/integration/analyzer_test.go
package integration

import (
    "context"
    "os"
    "testing"
    "time"

    "github.com/yourusername/image-analyzer/internal/analyzer"
    "github.com/yourusername/image-analyzer/internal/config"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestImageAnalysis(t *testing.T) {
    if testing.Short() {
        t.Skip("Skipping integration test in short mode")
    }

    // Configurar test
    cfg, err := config.LoadConfig()
    require.NoError(t, err)

    svc, err := analyzer.NewService(cfg.Analyzer)
    require.NoError(t, err)

    tests := []struct {
        name     string
        source   string
        wantErr  bool
        validate func(*testing.T, *analyzer.AnalysisResult)
    }{
        {
            name:    "analizar imagen local válida",
            source:  "testdata/test_image.jpg",
            wantErr: false,
            validate: func(t *testing.T, result *analyzer.AnalysisResult) {
                assert.NotEmpty(t, result.Labels)
                assert.NotEmpty(t, result.Labels[0].TranslatedName)
                assert.Greater(t, result.Labels[0].Confidence, float64(50))
            },
        },
        {
            name:    "analizar URL válida",
            source:  "https://example.com/test.jpg",
            wantErr: false,
            validate: func(t *testing.T, result *analyzer.AnalysisResult) {
                assert.NotEmpty(t, result.Labels)
            },
        },
        {
            name:    "error en imagen inválida",
            source:  "testdata/invalid.txt",
            wantErr: true,
            validate: func(t *testing.T, result *analyzer.AnalysisResult) {
                assert.Nil(t, result)
            },
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
            defer cancel()

            result, err := svc.AnalyzeImage(ctx, tt.source)
            if tt.wantErr {
                assert.Error(t, err)
            } else {
                assert.NoError(t, err)
            }

            tt.validate(t, result)
        })
    }
}

6.2 Benchmark Tests

go
// tests/benchmark/analyzer_bench_test.go
package benchmark

import (
    "context"
    "testing"
    "time"

    "github.com/yourusername/image-analyzer/internal/analyzer"
    "github.com/yourusername/image-analyzer/internal/config"
)

func BenchmarkImageAnalysis(b *testing.B) {
    cfg, err := config.LoadConfig()
    if err != nil {
        b.Fatal(err)
    }

    svc, err := analyzer.NewService(cfg.Analyzer)
    if err != nil {
        b.Fatal(err)
    }

    ctx := context.Background()
    source := "testdata/bench_image.jpg"

    b.ResetTimer()
    
    for i := 0; i < b.N; i++ {
        _, err := svc.AnalyzeImage(ctx, source)
        if err != nil {
            b.Fatal(err)
        }
    }
}

func BenchmarkImageAnalysisParallel(b *testing.B) {
    cfg, err := config.LoadConfig()
    if err != nil {
        b.Fatal(err)
    }

    svc, err := analyzer.NewService(cfg.Analyzer)
    if err != nil {
        b.Fatal(err)
    }

    source := "testdata/bench_image.jpg"

    b.ResetTimer()
    
    b.RunParallel(func(pb *testing.PB) {
        ctx := context.Background()
        for pb.Next() {
            _, err := svc.AnalyzeImage(ctx, source)
            if err != nil {
                b.Fatal(err)
            }
        }
    })
}

6.3 Documentación y Ejemplos

go
// examples/basic_usage/main.go
package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/yourusername/image-analyzer/internal/analyzer"
    "github.com/yourusername/image-analyzer/internal/config"
)

func main() {
    // Cargar configuración
    cfg, err := config.LoadConfig()
    if err != nil {
        log.Fatal(err)
    }

    // Crear servicio
    svc, err := analyzer.NewService(cfg.Analyzer)
    if err != nil {
        log.Fatal(err)
    }

    // Analizar imagen
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    result, err := svc.AnalyzeImage(ctx, "examples/images/sample.jpg")
    if err != nil {
        log.Fatal(err)
    }

    // Imprimir resultados
    fmt.Println("Resultados del análisis:")
    for _, label := range result.Labels {
        fmt.Printf("- %s (%.1f%%): %s\n",
            label.Name,
            label.Confidence,
            label.TranslatedName)
    }
}

Uso y Ejemplos

bash
# Analizar imagen local
image-analyzer analyze photo.jpg

# Analizar URL con salida JSON
image-analyzer analyze https://example.com/photo.jpg -o json

# Analizar con timeout personalizado
image-analyzer analyze photo.jpg -t 120

# Obtener ayuda
image-analyzer --help

Verificación Final

Funcionalidad

  • [ ] Análisis de imágenes locales funcionando
  • [ ] Análisis de URLs funcionando
  • [ ] Traducciones correctas
  • [ ] Diferentes formatos de salida
  • [ ] Progress bar funcional
  • [ ] Manejo de errores robusto

Performance

  • [ ] Cache de traducciones funcionando
  • [ ] Rate limiting implementado
  • [ ] Retry policy funcionando
  • [ ] Tiempos de respuesta optimizados

Calidad

  • [ ] Tests unitarios completos
  • [ ] Tests de integración funcionando
  • [ ] Benchmarks implementados
  • [ ] Documentación generada
  • [ ] Ejemplos funcionales

Próximos Pasos

  1. Añadir soporte para más formatos de imagen
  2. Implementar análisis batch
  3. Añadir más opciones de formato de salida
  4. Mejorar el manejo de errores
  5. Optimizar el uso de memoria