🔍
Go × OpenRouter で作る“著名人判定 API”超入門 ― Vision-LLM を 20 分でデプロイ
完成イメージ
curl -F image=@obama.jpg http://localhost:8080/predict
# => {"isCelebrity":true,"name":"Barack Obama"}
0. 事前準備
ツール | 使ったバージョン | 備考 |
---|---|---|
Go | 1.24+ | brew install go |
Gin | v1.10+ | 自動で go get されます |
OpenRouter アカウント | - | API Key 発行 |
1. .env にキーとモデルをセット
OPENROUTER_API_KEY=sk-...
OPENROUTER_MODEL=meta-llama/llama-3.2-11b-vision-instruct:free
- モデルは Vision 対応 のものなら何でも OK
(mistralai/ や anthropic/ 系でも可)
2. ひな型を生成
go mod init celeb-detector
go get github.com/gin-gonic/gin github.com/joho/godotenv
3. main.go --- 完全版
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"io"
"log"
"net/http"
"os"
"regexp"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
)
/* ============ OpenAI-compatible 型 ============ */
type openaiReq struct {
Model string `json:"model"`
Messages []openaiMessage `json:"messages"`
}
type openaiMessage struct {
Role string `json:"role"`
Content []interface{} `json:"content"` // text と image_url を混在
}
type imagePart struct {
Type string `json:"type"` // "image_url"
ImageURL struct {
URL string `json:"url"`
} `json:"image_url"`
}
type textPart struct {
Type string `json:"type"` // "text"
Text string `json:"text"`
}
type openaiResp struct {
Choices []struct {
Message struct {
Content string `json:"content"`
} `json:"message"`
} `json:"choices"`
}
/* ============ メイン ============ */
func main() {
_ = godotenv.Load()
key := os.Getenv("OPENROUTER_API_KEY")
model := os.Getenv("OPENROUTER_MODEL")
if key == "" || model == "" {
log.Fatal("OPENROUTER_API_KEY / MODEL を .env に設定してください")
}
r := gin.Default()
r.POST("/predict", func(c *gin.Context) {
/* 1. 画像を受け取る */
file, _, err := c.Request.FormFile("image")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "image form-field required"})
return
}
defer file.Close()
buf, _ := io.ReadAll(file)
imgBase64 := "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(buf)
/* 2. OpenRouter に投げるリクエストを構築 */
reqBody := openaiReq{
Model: model,
Messages: []openaiMessage{{
Role: "user",
Content: []interface{}{
textPart{Type: "text", Text: `You are an AI assistant that tells whether the given face image contains a well-known public figure.
Return strictly JSON like: {"isCelebrity": true, "name": "Full Name"} or {"isCelebrity": false, "name": ""}.`},
imagePart{Type: "image_url", ImageURL: struct {
URL string `json:"url"`
}{URL: imgBase64}},
},
}},
}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST",
"https://openrouter.ai/api/v1/chat/completions",
bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+key)
/* 3. 呼び出し */
resp, err := http.DefaultClient.Do(req)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
rawResp, _ := io.ReadAll(resp.Body)
defer resp.Body.Close()
log.Printf("status=%d body=%s", resp.StatusCode, rawResp)
/* 4. OpenAI 互換レスポンスをパース */
var oaResp openaiResp
if err := json.Unmarshal(rawResp, &oaResp); err != nil {
c.JSON(502, gin.H{"error": "openrouter decode failed"})
return
}
if len(oaResp.Choices) == 0 {
c.JSON(502, gin.H{"error": "no choices returned", "detail": string(rawResp)})
return
}
/* 5. strict JSON が来ているか確認 */
raw := oaResp.Choices[0].Message.Content
var out struct {
IsCelebrity bool `json:"isCelebrity"`
Name string `json:"name"`
}
if err := json.Unmarshal([]byte(raw), &out); err != nil {
// => strict じゃなかったので {...} を抽出して再パース
re := regexp.MustCompile(`\{.*\}`)
jsonPart := re.FindString(raw)
if jsonPart == "" || json.Unmarshal([]byte(jsonPart), &out) != nil {
c.JSON(500, gin.H{"error": "LLM returned non-JSON", "raw": raw})
return
}
}
/* 6. 返却 */
c.JSON(200, out)
})
log.Println("listen :8080")
r.Run(":8080")
}
4. 動作チェック:追加 → 一覧 → 削除 …ならぬ「判定」!
# 起動
go run main.go
# => listen :8080
## 1) 有名人
curl -F image=@obama.jpg http://localhost:8080/predict
# {"isCelebrity":true,"name":"Barack Obama"}
## 2) 有名人でない人物
curl -F image=@myfriend.jpg http://localhost:8080/predict
# {"isCelebrity":false,"name":""}
## 3) モデルが文章を返しても OK(正規表現 fallback)
curl -F image=@abe.jpg http://localhost:8080/predict
# {"isCelebrity":true,"name":"Shinzo Abe"}
5. まとめ 🚀
- Vision-LLM + OpenRouter で顔写真→著名人判定をローカル API 化
- Gin のマルチパート受信 → Base64 → Data-URI → OpenRouter
- strict JSON を要求しつつ、正規表現 fallback で実用強度◎
Discussion