はじめに
皆さんは、「映画を見ようと思ってNetflixを開いたのに、作品を選んでいるうちに疲れてしまい、結局Youtubeを見て寝てしまった」という経験ないでしょうか?私はあります。毎回あります。 「今の自分の気分」と「自分の性格(好み)」を理解して、 「これを見て、なぜなら君は今こういう状態だから」 という論理的に説得してくれる存在が欲しい。 そんな思いから、 MBTI × 感情分析シネマレコメンドアプリ「TYPECAST」 を開発しました。 技術的には Go (Echo) + Cloud Run をバックエンドに採用し、動的OGP生成まで作り込んだので、その知見を共有します。
Typecast
https://tycast.net/ 自分のMBTI(16タイプ診断)と現在のMood(気分・悩み)を入力すると、AIがその性格特性と心理状態を分析し、最適な映画をレコメンドしてくれるサービスです。
特徴
- 論理的なレコメンド 単なるキーワード検索ではなく、「INTJは論理的整合性を好むため、この脚本が刺さるはず」といった理由付けを行います。 :::message まだβ版なので、1日3回までのレコメンドで制限しています。 :::
- 感情分析チャート
1週間分の診断履歴をスコア化し、自分のメンタル推移をグラフで見れます。

- 動的OGP 診断結果をXでシェアすると、映画タイトルと診断スコアが入った画像が動的に生成されます。
技術スタック
個人開発なので好きな技術を使いたいので、以下を選定しました。
- Frontend: React (Vite, TypeScript), Tailwind CSS
- Backend: Go (Echo)
- Infrastructure: Google Cloud Run, Firebase (Auth, Firestore)
- AI API: Google Gemini API (gemini-2.0)
- Movie Data: TMDB API
- CI/CD: GitHub Actions

バックエンドにGoを選んだのは、軽量・高速な処理と、並行処理の強さ、そして何より 「書いていて楽しいから」 です。 また、AIモデルにはGemini 2.0 Flashを採用しました。無料枠が大きく、レスポンスも高速で、個人開発の強い味方です。
技術TIPS①:Geminiへの「人格」の埋め込み
単に映画をリストアップさせるだけなら簡単ですが、このアプリの肝は 「MBTIに基づいた納得感」 です。 そこで、システムプロンプト(Goのバックエンドから送信)でAIに「辛口な映画評論家」のようなペルソナを与え、JSON形式で厳格に出力させるようにしました。
// Goでのプロンプト構築例(抜粋)
prompt := fmt.Sprintf(`
あなたは心理学と映画理論に精通したコンシェルジュです。
ユーザーのMBTIタイプ: %s
現在のムード: %s
以下の制約を守り、最適な映画を3本推薦してください。
1. そのMBTIタイプが好むストーリー構造やテーマ性を分析すること。
2. ユーザーのムードに対して、同調(寄り添う)または覚醒(気分を変える)効果があるものを選ぶこと。
3. 出力は必ず指定のJSONフォーマットのみにすること。
`, mbti, mood)
これにより、「なんとなくおすすめ」ではなく「君の今の脳みそにはこれが効く」という処方箋のようなレコメンドを実現しています。
技術TIPS②:Goによる動的OGP生成の実装
今回一番こだわった(そして苦労した)のが、診断結果のシェア機能 です。
ReactなどのSPA(Single Page Application)において、動的なOGP(Twitterカード)を表示するのは鬼門です。TwitterのクローラーはJavaScriptを実行してくれないため、静的なindex.htmlのメタタグしか読み取ってくれないからです。 そこで、バックエンド(Go)側で「Bot判定」と「画像生成」を行うアプローチを取りました。
1. 処理の流れ
- ユーザーが「シェア」ボタンを押すと、診断結果IDを含んだURL(例: /s/abc1234)が発行される。
- このURLにアクセスがあった時、Goのハンドラが User-Agent をチェックする。
- Bot(Twitterbotなど)の場合: OGPタグを含んだHTMLを返す。画像URLは /api/ogp?title=… を指定。
- 人間(ブラウザ)の場合: Reactのフロントエンドへリダイレクトする。

2. Goでの画像描画
画像生成には fogleman/gg というライブラリを使用しました。CanvasライクにGoで絵が描けます。
// 画像生成ロジックの一部
func GenerateImage(title string, score int) ([]byte, error) {
dc := gg.NewContext(1200, 630)
// 背景色塗りつぶし
dc.SetColor(color.RGBA{15, 23, 42, 255})
dc.Clear()
// フォント読み込み (ここがハマりポイント!)
if err := dc.LoadFontFace("assets/fonts/NotoSansJP-Bold.ttf", 80); err != nil {
return nil, err
}
// タイトル描画
dc.SetColor(color.White)
dc.DrawStringAnchored(title, 600, 315, 0.5, 0.5)
// ...
}
ここでdc.LoadFontFaceが指定しているのは、あくまで「実行環境のファイルパス」です。Go言語はシングルバイナリにコンパイルされるため、つい「バイナリさえあれば動く」と錯覚して、画像やフォントなどの静的アセットはバイナリに含まれないことに気付かず進めてしまいました。 そのため、ローカル(go run)では動くのに、Cloud Runにデプロイした途端に以下のようなエラーで落ちていました。
panic: open assets/fonts/NotoSansJP-Bold.ttf: no such file or directory
解決策として、Dockerfileで明示的にフォントファイルを本番イメージにコピーする方法を取りました。
# ビルドステージからバイナリをコピー
COPY --from=builder /app/main .
# ★重要:フォントなどの静的ファイルも忘れずにコピーする!
COPY --from=builder /app/assets ./assets
このCOPYを1行足すことで、本番環境でも正しくフォントを読み込めました。
公開できない変数などの設定について
インフラはCloud Runを使用していますが、デプロイはGitHub Actionsで自動化しています。 今回、APIキー(Gemini,TMDB)やFirebaseの認証情報など、機密情報はすべてGitHub Secretsで管理し、デプロイ時に —set-env-vars で注入するようにしました。
途中、フロントエンドとバックエンドの環境変数の渡し方で混乱し、本番環境のシェアURLがlocalhostになってしまうミスもありましたが、最終的にはYAMLファイルで環境変数を明示的にマッピングすることで解決しました。
今後の展望について
MVP(実証実験版)としてリリースしたTYPECASTですが、やりたいことはまだまだあります。特に以下の3点を重点的に開発していく予定です。
-
ユーザーフィードバックによる精度向上 (RLHF的なアプローチ) 現在は「一方的なレコメンド」ですが、ユーザーが「この映画は見たことある」「これは好みじゃない」といったフィードバックを返せるようにしたいです。そのデータを蓄積し、次回のGeminiへのプロンプトに「過去にユーザーがNGを出した傾向」を含めることで、使えば使うほど 「自分専属のコンシェルジュ」 に育つ仕組みを実装予定です。
-
RAG (Retrieval-Augmented Generation) の導入 現在はGeminiの学習データのみに頼って映画を選定していますが、これには「最新の映画を知らない」「マニアックすぎる映画が出にくい」という弱点があります。そこで、TMDBのデータをベクトル化してDB(Pineconeやpgvectorなど)に格納し、「ムードに近い作品をベクトル検索 → その候補をGeminiに渡して解説させる」 というRAG構成に移行したいと考えています。これにより、AIの幻覚(ハルシネーション)を防ぎつつ、より確実なレコメンドにしたいです。
-
「どこで観れる?」の解決(VOD連携) 「見たい映画は見つかったけど、結局NetflixにもAmazon Primeにもなかった」という体験は最悪です。TMDB APIのWatch Providers情報を活用し、「自分が加入しているサブスクで見れる映画だけをレコメンドする」というフィルタリング機能を実装し、診断から視聴までの導線をスムーズにする予定です。
まとめ
初めての個人開発でβ版ですが、リリースしたものになります。勢いで作り始めましたが、GoとCloud Runを使ったモダンな構成で、自分が欲しかったアプリを形にすることができました。特に「バックエンドでOGP画像を動的に作る」という処理は、Goのパフォーマンスの良さも相まって非常に高速で、実装していて楽しかったです。
もしよかったら、年末年始の映画選びに使ってみてください! フィードバックお待ちしています。