ワインバーでフランス語学習をしたい。

[mediba Adventカレンダー](https://qiita.com/advent-calendar/2024/mediba) 16日目の記事です。


## 課題感

![IMG_0720.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147689/0906b6d2-6706-ba2c-215f-8e694a4407c0.jpeg)

この写真に映っているワインの名前、読めますでしょうか。
これで「アルボワ・ピュピヤン」となります。難しいですよね。

「Arbois（アルボワ）」は最後の「s」が発音されません。フランス語では語尾の子音が発音されないことが多いから、これがまずややこしいです。

仮に文字が読めて発音できたとしても、Arbois、Pupillinがどういう意味を持つのか事前知識がないとよく分かりません。

ワインボトルの裏側も見てみましょう。

![IMG_0721.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147689/1b17e759-0771-993d-01bc-39a4b1940235.jpeg)

「PLOUSSARD」で「プールサール」と読みます。これはブドウ品種の名前です。
（フランス語の発音としては「プルサー」に近いです）

私自身は今年、フランス語学力資格試験のDELF A1を取得してフランス語に触れる機会が増えたのですが、それでもワインボトルに書かれている単語は専門的で、知らない（＆発音できない）ものが多いと感じています。


## 今回作るもの

フランス語を知ることは、目の前のワインの背景を探るきっかけになるかもしれません。

そこで、生成AIを活用して、ワインのエチケット（ラベル）からフランス語学習に繋げる（ワインバーで暇をつぶす）アプリを作ってみます。

### 必要な機能
1. 複数枚の画像をアップロード
1. 画像に映っているワインのフランス語を文字起こし
2. 文字起こししたフランス語を日本語に翻訳
3. 文字起こししたフランス語の構文を解説
4. 文字起こししたフランス語を再生
5. 画像に映っているワインをネットで検索

### 利用するWebサービス

- [Vercel v0](https://v0.dev/)（UI作成）
- [ChatGPT](https://chatgpt.com/)（画像作成・その他）
- [heroku](https://jp.heroku.com/),[Express](https://expressjs.com/ja/)（ホスティング・バックエンド）
- [ChatGPT API](https://platform.openai.com/docs/api-reference)（画像認識、翻訳、構文解説）
- [TextToSpeech API](https://cloud.google.com/text-to-speech/docs/reference/rest)（テキストの音声合成）

画像認識はGoogleのCloud Vision APIを利用する予定でしたが、柔軟性が高くコンテクストを理解して文字抽出できるChatGPTを採用しました。


## その1) Vercel v0でUIを作成する

Vercel v0はテキストプロンプトでWebページを作成することができます。生成されるUIはshadcn/uiとTailwind CSSをベースとしているようです。

https://v0.dev/

### プロンプト 1回目

以下のようなプロンプトを用いてWebページのUIを作成してみます。

通常v0ではReactコンポーネントが出力されますが、今回はHTMLとして利用するため「tsxではなくHTMLとして出力する」と指示をしています。

```md
## 概要
- モバイル向けのウェブサイト（フランス語翻訳・解説） 
- フランスのワインの画像をアップロードし、そのワインに映っているフランス語を読み取る
- 画像を最大2枚まで添付可能
- 添付した画像はプレビューできる
- その画像にあるフランス語について、原文表示、日本語翻訳、構文解説、フランス語再生できる
- 各種テキストは英語で表示
- フランスをイメージした配色・デザイン
- 必要に応じてボタンにアイコンを設定
- 文字サイズは12px〜14px相当

## UI要件
- テキスト表示するエリアは3つあり、1つは「Original French Text」、2つめは「Translation」、3つ目は「Grammar Explanation」、4つ目は「Details about this wine」
- 1つめのテキスト領域には「Extract French from Images」ボタンを配置
- 2つめのテキスト領域には「Translate French to Japanese」ボタンを配置
- 3つめのテキスト領域には「Explanation of French Syntax」ボタンを配置
- 4つめのテキスト領域には「Research this wine on the web」ボタンを配置
- それぞれのテキスト領域のボタンを押すと、APIをコールして結果をその領域に表示

## その他
- tsxではなくHTMLとして出力する
```



### 生成されたUI

このような仕上がりになりました。指示通りにアイコンも反映されておりなかなかよさそうです。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147689/e268af97-7d05-b7e6-1603-a217dc9660d9.png)


### プロンプト 2回目

個々のボタンを押すと非同期処理が発生することになりますが、ローディング中であることを視覚的に分かりやすくするため、追加で以下のような指示を与えてUIに変更を加えてみます。

```
APIからレスポンスが返却されるまでの間、半透明でグレーアウトして処理中の表示をしたい
```


### 生成されたUI

先ほどのHTMLを修正する形で、ローディングアニメーションが施されたUIが生成されました。
setTimeoutの処理で擬似的にローディングを再現してくれています。賢いですね。

![hoge.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147689/55abbbc3-a2bf-f79c-5e9e-077a239bf6a9.gif)


この後、Vercelと対話をしながら細かくUIの調整を続けます。

## その2) ChatGPTでAPIの繋ぎ込みを試す

Vercelで作成したHTMLをChatGPTに投げてAPIの繋ぎ込みができるか試してみます。

結論としてはうまくいきませんでした。

### プロンプト

```md
以下のHTMLについて、指示の通り修正してください

## 指示
- Upload Imageで画像が設定されたら、その画像をChatGPT APIに問い合わせる。プロンプトは「次のフランス語の原文を返却してください」
- Translationボタンが押されたら、Original French Textの内容をChatGPTに問い合わせる。プロンプトは「次のフランス語を日本語に翻訳してください」
- Explanationボタンが押されたら、Original French Textの内容をChatGPTに問い合わせる。プロンプトは「次のフランス語を丁寧に解説してください」
- Audio Playbackボタンが押されたら、Original French Textの内容をGoogle CloudのTextToSpeechAPIへ渡す。返却結果は再生できるようにする

----

~~ Vercel v0で生成されたHTML ~~

```


例えばChatGPT APIの繋ぎ込みでは以下のようなコードが生成されました。

エンドポイントが微妙に間違っており、`text-davinci-003`もすでに利用できないモデルです。
リクエストボディにもmessagesがなく、そのまま使えそうにはありません。

```javascript
const response = await fetch("https://api.openai.com/v1/completions", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${CHATGPT_API_KEY}`
    },
    body: JSON.stringify({
        model: "text-davinci-003",
        prompt: prompt,
        max_tokens: 200,
        temperature: 0.7
    })
});
```


他のAPI箇所に関してもそうですが、ChatGPTはAPI仕様の正しい（最新の）知識を持ち合わせていないようでした。

## その3) APIとの繋ぎ込み

生成AIによる繋ぎ込みは諦めて、公式のドキュメントを眺めながら実装することにします。
ここではAPI Keyを秘匿化するためにNodeサーバ（Express）を中継する形にしています。

### フランス語の文字起こし
回答結果をそのままフロントエンドに渡してAPIとして利用します。

「承知しました。以下が〜〜〜です」のような余計な会話文を避けるため「文字起こしした文字列だけを返してください。」といった指示を与えています。

```javascript
const response = await openai.chat.completions.create({
    model: "gpt-4o-mini",
    max_tokens:1000,
    messages: [
        {
            role: "user",
            content: [
                { type: "text", text: "この写真に映っているフランス語の文字起こしをしてください。なお文字起こしした文字列だけを返してください。" },
                {
                    type: "image_url",
                    image_url: {
                        "url": `data:image/jpeg;base64,${base64Image}`,
                    }
                }
            ]
        }
    ]
});
```


### フランス語の翻訳

プロンプトで翻訳を指示します。

```javascript
const response = await openai.chat.completions.create({
    model: "gpt-4o-mini",
    max_tokens:1000,
    messages: [
        {
            role: "user",
            content: [
                { type: "text", text: "以下のフランス語を日本語に翻訳してください----" + req.body.input.text }
            ]
        }
    ]
});
```

### フランス語の構文解説

モデルに特定の人格を持たせるため、sysytemロールを使って「フランス語の先生」を設定しています。

```javascript

const response = await openai.chat.completions.create({
    model: "gpt-4o-mini",
    max_tokens:1000,
    messages: [
        {
            role :"system",
            content:"あなたはフランス語の先生です。"
        },
        {
            role: "user",
            content: [
                { type: "text", text: "以下のフランス語の構文解説を丁寧にしてください。----" + req.body.input.text },
            ]
        }
    ]
});

```

### フランス語の音声合成

GoogleのText-to-Speech APIを利用しました。

https://cloud.google.com/text-to-speech

```javascript
const response = await fetch(`https://texttospeech.googleapis.com/v1/text:synthesize?key=${process.env.GCP_API_KEY}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
        input: { text: req.body.input.text },
        voice: { languageCode: 'fr-FR', name: "fr-FR-Neural2-A" },
        audioConfig: { audioEncoding: 'MP3' }
    })
});
```

24年12月現在、フランス語には「fr-FR-Journey-D」「fr-FR-Journey-F」「fr-FR-Journey-O」という、従来のモデルよりも自然な発音のモデルが用意されていますが、まだプレビュー版のため細かいバグがあり、ここでは「fr-FR-Neural2-A」を選択しています。


https://cloud.google.com/text-to-speech/docs/voices?hl=ja

### ワインのエチケットのWeb検索

個々のワインに対する質問は学習量が低くハルシネーションを起こす可能性が高いため、Web検索を促すプロンプトを与えています。

```javascript
const response = await openai.chat.completions.create({
    model: "gpt-4o-mini",
    max_tokens:1000,
    messages: [
        {
            role :"system",
            content:"あなたはワインに精通したソムリエです。"
        },
        {
            role: "user",
            content: [
                { type: "text", text: "以下の写真に映っているワインをWeb検索してください。" },
                {
                    type: "image_url",
                    image_url: {
                        "url": `data:image/jpeg;base64,${base64Image}`,
                    }
                }
            ]
        }
    ]
});
```


## その4) サービス名を検討する

Vercel v0が生み出した「French Wine Translator」というサービス名もイマイチなので、サイト名もChatGPTに提案してもらいましょう。

```md
以下の内容でWebサービスを作りたい。最適なサービス名をいくつか英語で提案してください。

- フランス語のワインのエチケットの画像をアップロードできる
- そのエチケットのフランス語を抽出したり、日本語翻訳したり、あるいは構文解説ができる
- そのワインの情報をネットから検索できる
```

### 回答結果

> Certainly! Here are several English name suggestions for your web service, each reflecting its focus on French wine labels, translation, and information retrieval:
> 
> **VinoLingo**
> 
> Combines "Vino" (wine) with "Lingo" (language), highlighting the translation aspect.
> 
> **LabelVine**
> 
> Merges "Label" with "Vine," emphasizing wine labels and their analysis.
> 
> **VinTranslate**
> 
> Directly conveys the translation service for wine (Vin) labels.
> 
> **LabelLens**
> 
> Suggests a focused view on wine labels, including extraction and translation.
> 
> **WineScript**
> 
> Implies the service deals with the text (script) on wine labels.
> 
> **ÉtiquetteEase**
> 
> Uses the French word for label ("Étiquette") combined with "Ease," indicating simplicity in handling labels.
> 
> **VinSight**
> 
> A play on "insight," offering deep understanding of wine labels.
> 
> **LabelChateau**
> 
> Combines "Label" with "Chateau," evoking the French wine heritage.
> 
> **WineLex**
> 
> Suggests a comprehensive lexicon or dictionary for wine labels.
> 
> **LabelMosaic**
> 
> Implies a detailed and multifaceted analysis of wine labels.

VinSightが Vin（ワイン）とInsight（洞察）をかけているようにも見えるのでこれを採用します。

## その5) アプリアイコンを作る

サービス名が決まったので、アプリアイコンも生成AIで作りましょう。ここではDALL-Eを利用します。

https://openai.com/index/dall-e-3/

```
以下の内容でアイコンを生成してください

- 縦は512px、横は512px
- 立体感の無いシンプルなイラスト
- フランスのワインを翻訳するアプリ
- 色は#8b0000、#f8f1e9、#ffffffの3色
- サイトの名前は vinsightです。それに合いそうな雰囲気でお願いします。
```

### 回答結果

![DALL·E 2024-12-08 20.05.14 - A minimalistic flat icon design for a French wine translation app called 'Vin Sight'. The icon features a simple wine glass silhouette and a wine bott.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147689/8f328525-7aa4-cfb7-08ac-179cd6177488.png)



それっぽいアイコンが生成されたので、これを切り取って使うことにします。

![icon.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147689/0356b018-683d-1915-ca14-60aa1b0a57a5.png)


## その6) Add to Homescreen対応

ホーム画面からWebアプリとして利用するためにmanifest.jsonを追加します。
さきほどDALL-Eで作成したアイコンはiconsの`src`に指定しています。

```json
{
  "icons": [
    {
      "src": "/img/icon_192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "maskable"
    },
    {
      "src": "/img/icon_512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "maskable"
    }
  ],
  "name": "vinSight",
  "display": "fullscreen",
  "short_name": "vinSight",
  "start_url": "index.html",
  "theme_color": "#8b0000",
  "background_color": "#8b0000"
}
```

## できあがり

というわけで生成AIの助けを借りて、このようなUIのWebアプリが完成しました。

![french-app-a2b51d153ea2.herokuapp.com_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147689/51a63c27-b4bb-0256-72a4-ff26909b2d23.png)

## 使い方

最初に紹介したワインの写真を使って実際にWebアプリを動かしてみます。
（アニメGIFのため表示に時間がかかります）

### 1) 画像のアップロードとテキスト抽出


![66.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147689/f540471d-4478-493a-e525-63d77daf12b3.gif)

エチケットに記載されているフランス語を誤字なく読み取れているようです。

なお`Audio Playback`のリンクを押下することで、フランス語の音声として再生することができます。（GoogleのTextToSpeech APIをコールしています。）

### 2) 翻訳と構文解説


![77.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147689/cca13261-15b3-c94e-3616-30528bf9c2c2.gif)

翻訳もバッチリで構文解説も丁寧ですね。まさにこれを求めていました。

### 3) ワインの解説


![88.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147689/cb6e4095-ce7f-aa2d-befe-02b283e2f383.gif)

どうもWeb検索ができていないような挙動です。


作った後に気づいたのですが、2024年12月現在、[ChatGPT Search](https://openai.com/index/introducing-chatgpt-search/)（Bingで検索してくれる機能）はAPI経由では利用できないようです。

というわけでこの機能はChatGPT API側の対応を待つことにします。

## おわりに


生成AIの助けを借りることで「ワインバーで語学学習ができるWebアプリ」が作成できました。

これで店員さんとの会話のネタも増やせますね。