[Go] 画像生成サイトをGoで構築してみた

### はじめに

この記事では、ホラーテイストの画像を生成するサイトをGo言語とHTMLで構築する方法を紹介します。
ユーザーがテキストを入力すると、その内容に基づいてホラー映画ポスター風の画像を生成し、レスポンスとして返します。

[[Swift] ホラー画像生成アプリ]() のGo言語版です。

### 要件

- バックエンドはGo言語
- OpenAI APIを使う
- プロンプトを入力すると、ホラー画像を生成する

### 準備

#### 環境構築

Go言語の開発環境は、公式サイトをご確認ください。

- [Go言語公式サイト](https://go.dev/)

#### ライブラリのインストール

以下のライブラリをインストールします。

- `gin-gonic/gin`: Webフレームワーク
- `nfnt/resize`: 画像リサイズ用ライブラリ

※実行時にエラーが出たら、該当するライブラリをインストールしてください。

### ディレクトリ構成

```
horror-image-generator/
├── main.go
└── index.html
```

### 実装

- **`main.go`**:  APIの処理を実装します
```go:main.go
package main

import (
	"encoding/json"
	"fmt"
	"image"
	"image/jpeg"
	"image/png"
	"net/http"
	"os"

	"bytes"

	"github.com/gin-gonic/gin"
	"github.com/nfnt/resize"
)

type ImageResponse struct {
	Data []struct {
		URL string `json:"url"`
	} `json:"data"`
}

func main() {
	r := gin.Default()

	// 画像生成APIエンドポイント
	r.POST("/generate", func(c *gin.Context) {
		var req struct {
			Text string `json:"text"`
		}
		if err := c.BindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

        // OPENAI_API_KEY は環境変数に登録しておく
		apiKey := os.Getenv("OPENAI_API_KEY")
		if apiKey == "" {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "OPENAI_API_KEY environment variable is not set."})
			return
		}

		// プロンプトをホラーテイストに調整
		horrorPrompt := fmt.Sprintf("An eerie and unsettling scene with %s, in the style of a horror movie poster, dark and gruesome, high quality, digital art.", req.Text)

		url := "https://api.openai.com/v1/images/generations"

		// OpenAI APIへのリクエストを作成
		body, err := json.Marshal(map[string]interface{}{
			"model":  "dall-e-3",
			"prompt": horrorPrompt,
			"n":      1,
			"size":   "1024x1024",
		})
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}

		// HTTPリクエストを作成
		httpRequest, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}

		// ヘッダーを設定
		httpRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
		httpRequest.Header.Set("Content-Type", "application/json")

		// OpenAI APIからレスポンスを取得
		client := &http.Client{}
		resp, err := client.Do(httpRequest)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		defer resp.Body.Close()

		// レスポンスをデコード
		var imageResponse ImageResponse
		if err := json.NewDecoder(resp.Body).Decode(&imageResponse); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}

		// 画像URLから画像を取得
		imageUrl := imageResponse.Data[0].URL
		response, err := http.Get(imageUrl)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		defer response.Body.Close()

		// レスポンスヘッダーからコンテンツタイプを取得
		contentType := response.Header.Get("Content-Type")

		// 画像をリサイズ
		var resizedImage image.Image
		var decodeErr error

		// コンテンツタイプに基づいてデコード
		switch contentType {
		case "image/jpeg":
			resizedImage, decodeErr = jpeg.Decode(response.Body)
		case "image/png":
			resizedImage, decodeErr = png.Decode(response.Body)
		default:
			c.JSON(http.StatusInternalServerError, gin.H{"error": "Unsupported image format"})
			return
		}
		if decodeErr != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": decodeErr.Error()})
			return
		}

		// 画像をリサイズ
		resizedImage = resize.Resize(300, 0, resizedImage, resize.Lanczos3)

		// レスポンスとして画像を返す
		c.Writer.Header().Set("Content-Type", contentType)

		// コンテンツタイプに基づいてエンコード
		switch contentType {
		case "image/jpeg":
			jpeg.Encode(c.Writer, resizedImage, nil)
		case "image/png":
			png.Encode(c.Writer, resizedImage)
		}
	})

	// HTMLファイルの配信
	r.Static("/", "./")

	r.Run(":8080")
}
```

- **`index.html`**:  フロントエンドのHTMLファイルです
```html:index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Horror Image Generator</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css">
  <style>
    body {
      background-color: #181818; /* Dark Background */
      font-family: 'Arial', sans-serif;
      color: #fff;
    }

    .container {
      max-width: 600px;
      margin: 50px auto;
      padding: 30px;
      background-color: #282c34; /* Darker Grey */
      border-radius: 10px;
      box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
    }

    .title {
      text-align: center;
      margin-bottom: 30px;
      font-size: 2.5rem;
      text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
    }

    .form-group label {
      font-weight: bold;
    }

    .btn-generate {
      background-color: #dc3545; /* Red */
      border-color: #dc3545; /* Red */
      font-weight: bold;
    }

    .result-image {
      margin-top: 30px;
      text-align: center;
    }

    .loading {
      display: flex;
      justify-content: center;
      align-items: center;
      height: 300px;
    }

    .spinner {
      width: 50px;
      height: 50px;
      border: 4px solid #dc3545; /* Red */
      border-radius: 50%;
      border-top-color: transparent;
      animation: spin 1s linear infinite;
    }

    @keyframes spin {
      0% { transform: rotate(0deg); }
      100% { transform: rotate(360deg); }
    }
  </style>
</head>
<body>
  <div class="container">
    <h1 class="title mb-4">Horror Image Generator</h1>

    <form id="generate-form">
      <div class="form-group">
        <label for="text-input">Prompt:</label>
        <input type="text" class="form-control" id="text-input" placeholder="Enter image description...">
      </div>

      <button type="submit" class="btn btn-generate btn-block">Generate</button>

      <div id="loading" class="loading d-none">
        <div class="spinner"></div>
      </div>

      <div id="result-image" class="result-image d-none">
        <img src="" id="generated-image" class="img-fluid">
      </div>
    </form>
  </div>

  <script>
    const form = document.getElementById('generate-form');
    const loading = document.getElementById('loading');
    const resultImage = document.getElementById('result-image');
    const generatedImage = document.getElementById('generated-image');

    form.addEventListener('submit', async (event) => {
      event.preventDefault();

      // ローディング表示
      loading.classList.remove('d-none');
      resultImage.classList.add('d-none');

      // 入力値取得
      const textInput = document.getElementById('text-input').value;

      // 画像生成APIへのリクエスト
      try {
        const response = await fetch('/generate', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({ text: textInput })
        });

        if (!response.ok) {
          throw new Error('Failed to generate image.');
        }

        // 画像データの取得
        const blob = await response.blob();

        // 画像表示
        generatedImage.src = URL.createObjectURL(blob);
        resultImage.classList.remove('d-none');
      } catch (error) {
        alert(error);
      } finally {
        // ローディング非表示
        loading.classList.add('d-none');
      }
    });
  </script>

  <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.min.js"></script>
</body>
</html>
```

### 動作確認

ターミナルで以下のコマンドを実行してAPIを起動します。

```bash
go run main.go
```
localhost:8080 でアクセスすると、このような画面に。
ちょっとだけホラーテイストにしてあります。
それでは、プロンプトを入力して画像を生成してみましょう！

![スクリーンショット 2024-06-20 1.20.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407770/68bdaa09-b4f9-c1f4-80bd-6fdbbf305cd7.png)

プロンプトはチューリップにしました。雰囲気でてますね！

![スクリーンショット 2024-06-20 1.21.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407770/6ec34094-ac41-c108-2213-8063cbae2e02.png)


### まとめ

この記事では、Go言語とOpenAIのDALL-E 3 APIを使って、ホラーテイストの画像を生成するAPIを構築しました。このAPIは、ユーザーのテキスト入力に基づいて、ホラー映画ポスター風の画像を生成します。

生成AIの発達によって、様々なことが可能になってきました。
根気があればアイデアが実現するので、可能性は無限大。

みなさんも一緒にがんばりましょう！