【完全版】LLMで和歌をたしなむ？！

## 今の気持ちを伝えたい！
今回は、LLMを使ったアプリを考えます。

LLMは言語モデルということで、対象は言語。なので、言葉遊びから入りました。
言葉遊びといえば、詩歌、そしてその日本代表は和歌です。
百人一首など、身近にありますよね。

ということで、今回はLLMを使って和歌に親しむアプリを作りたいと思います。

## 完成イメージ
今の気分や好きな風景、動物、植物、地名、といったものを想像します。

それをアプリに伝えると、それになるべく近い和歌を返します。
また、その和歌にあった画像を生成し、和歌とともに画面に出してくれます。

出てくる画像は、こんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212141/61ccbe66-5740-ab21-a618-cec22a3de9ee.png)

この句には、深紅の紅葉が川を流れる意味があり、この情景のような情熱的な赤が似合います。
でもちょっと赤過ぎるか。

何かに高揚している気分を入力して、こんな絵が出てくると、なんかかっこいいですね。

## プロダクト
タイトル：　今の気持ちを伝えたい

- 今の気持ち、見たい風景、動物、植物、地名などを入力する。選択肢を表示しておく。

- LLMが入力に対して、適切な和歌をいくつか提示してくる。

- そのうちの一つを選ぶと、その和歌に応じた風景画が４つ描かれる。

- ４つのうち１つを選択すると、その画像と和歌が合成され、作品が完成する。

LLMアプリケーションでは、入力に対して和歌を選び出すロジックを実装します。

## ChatGPT 4oを使ってみた
LLMに和歌を作らせてみました。

```
高揚している、紅葉、川が流れる
というテーマで、和歌を作ってください。
```

というプロンプトで作ってみると、

> 秋の風 心踊らす 紅葉見て
流るる川に 色移りゆく

ほとんどそのままか言い換え。
紅葉なので秋、紅葉、流るる川はそのままで、高揚している気分は心躍らすと言い換え。

自分ではこのレベルでも無理ですが、もうちょっと何とかしたい。

何度か試してみても、やっぱり同レベルです。まあ五七五七七にまとめているだけでも凄いのかもしれません。GPT3.5だと全く和歌は作れないですから。

ということで、LLMに作ってもらうのはあきらめて、過去の作品から選び出すことにします。GPT-4oは情報としてかなりの歌の情報を持っているようなので、RAGを使うまでもなさそうです。

## Hugging Faceを使いたい

画像生成AIにOpenAIを使うという手もありますが、Stable Diffusionも使ってみたい。
そこで、色々なモデルが公開されているHugging Faceを利用することにしました。

画像はいくつか作ってみて、その中から選択するようにすれば、自分のイメージにより近づけられます。モデルを複数使ってそれぞれ表示するようにしたいです。

Hugging Faceにあるモデルを使うには、Inference APIを使うことになります。その場合は生成にかかる時間が結構必要で、タイムアウトすることもあります。
なので、ローカルでの画像生成も検討してみます。

## 完成？
苦労しました。APIを使って思うような出力がなかなか得られなかったです。今もほんとに得られているのかは謎です。

https://x.com/tadokuno/status/1818129334533812423

## 制作したもの
Webアプリとして制作しました。
実行環境はローカルのLinux環境(WSL)を使い、Webサーバーを立てる形を取りました。

<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212141/dd901363-9023-139c-1531-2983df74f7ee.png width=70%>

全体の構造はこんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212141/614ca777-9e78-d089-707d-309bf97e12d3.png)

外部とのやりとりは全てサーバー側(server.js)で実行します。
サーバー上の実行環境はNode.jsで作ります。

ファイル構造はこんな感じ
```
my-project/
├── .env
├── package.json
├── server.js
├── public/
│   ├── index.html
│   └── script.js
├── img/
│   ├── 生成された画像ファイル
```
.envにはAPI KEYなどを入れておきます。
```
API_KEY=MY_MIIBO_API_KEY
AGENT_ID=MY_MIIBO_AGENT_ID
UID=MY_MIIBO_UID
DEEPL_API_KEY=MY_DEEPL_API_KEY
HUGGINGFACE_API_KEY=MY_HUGGINGFACE_API_KEY
```
package.jsonは、Node.jsを初期化すると生成されますが、ESモジュールを使用するために、`"type": "module"`の設定を追加しています。

```json:package.json
{
  "name": "waka",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "type": "module",
  "scripts": {
    "start": "node server.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "axios": "^1.7.2",
    "dotenv": "^16.4.5",
    "express": "^4.19.2",
    "node-fetch": "^3.3.2"
  }
}
```

以下、ソースコードです。
<details>
<summary>クリックしてコードを表示</summary>

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>和歌画像生成アプリ</title>
  <style>
    .selected {
      border: 5px solid red;
    }
    .generated-image {
      max-width: 200px;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <h1>和歌画像生成アプリ</h1>
  <input type="text" id="utterance" placeholder="キーワードを入力">
  <button id="getWakaButton" onclick="getWaka()">和歌を取得</button>
  <div id="wakaDisplay"></div>
  <div id="translation"></div>
  <div id="images"></div>
  <button id="generateImageButton" style="display:none;" onclick="generateImages()">画像生成</button>
  <button id="combineButton" style="display:none;" onclick="combineImageAndWaka()">画像と和歌を合成</button>
  <div id="status"></div>
  <div id="result"></div>
  <script src="script.js"></script>
</body>
</html>

```
```javascript:script.js
async function getWaka() {
  const utterance = document.getElementById('utterance').value;
  const response = await fetch('/get-waka', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ utterance })
  });

  const data = await response.json();
  displayWaka(data);
}

function displayWaka(data) {
  const wakaDisplay = document.getElementById('wakaDisplay');
  wakaDisplay.innerHTML = ''; // Clear previous waka display
  const wakaOptions = data.bestResponse.utterance.split('\n').filter(waka => waka.trim());
  wakaOptions.forEach((waka, index) => {
    const radio = document.createElement('input');
    radio.type = 'radio';
    radio.name = 'waka';
    radio.value = waka;
    radio.id = `waka${index}`;

    const label = document.createElement('label');
    label.htmlFor = `waka${index}`;
    label.textContent = waka;

    wakaDisplay.appendChild(radio);
    wakaDisplay.appendChild(label);
    wakaDisplay.appendChild(document.createElement('br'));
  });
  document.getElementById('generateImageButton').style.display = 'inline';
}

async function generateImages() {
  const selectedWaka = document.querySelector('input[name="waka"]:checked').value;
  const translation = await translateWaka(selectedWaka);
  displayTranslation(translation);

  const models = [
    'stabilityai/stable-diffusion-xl-base-1.0',
    'stabilityai/stable-diffusion-2-1-base',
    'stabilityai/stable-diffusion-3-medium-diffusers',
    'sd-community/sdxl-flash'
  ];

  const images = [];
  const statusDiv = document.getElementById('status');

  for (const model of models) {
    try {
      statusDiv.innerHTML = `Generating image with model: ${model}`;
      const imagePath = await generateImage(model, translation);
      images.push(imagePath);
    } catch (error) {
      console.error(`Error generating image with model ${model}:`, error);
      statusDiv.innerHTML = `Error generating image with model: ${model}. Continuing with next model.`;
    }
  }

  statusDiv.innerHTML = 'All images generated!';
  displayImages(images);
}

async function translateWaka(waka) {
  const response = await fetch('/translate', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ text: waka })
  });

  const data = await response.json();
  return data.translations[0].text;
}

function displayTranslation(translation) {
  const translationDiv = document.getElementById('translation');
  translationDiv.innerHTML = `<p>Translation: ${translation}</p>`;
}

async function generateImage(model, prompt) {
  const response = await fetch('/generate-image', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ model, prompt })
  });

  const data = await response.json();
  if (data.imagePath) {
    return data.imagePath;
  } else {
    throw new Error('Image generation failed');
  }
}

function displayImages(images) {
  const imagesDiv = document.getElementById('images');
  imagesDiv.innerHTML = ''; // Clear previous images
  images.forEach((imagePath, index) => {
    const img = document.createElement('img');
    img.src = imagePath;
    img.id = `image${index}`;
    img.classList.add('generated-image');
    imagesDiv.appendChild(img);
  });
  document.getElementById('combineButton').style.display = 'inline';
}

function combineImageAndWaka() {
  const selectedWaka = document.querySelector('input[name="waka"]:checked').value;
  const formattedWaka = formatWaka(selectedWaka);
  const selectedImage = document.querySelector('img.selected').src;

  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  const img = new Image();
  img.src = selectedImage;

  img.onload = () => {
    canvas.width = img.width;
    canvas.height = img.height;
    ctx.drawImage(img, 0, 0);

    drawVerticalText(ctx, formattedWaka, canvas.width - 50, 50);

    const result = canvas.toDataURL('image/png');
    displayResult(result);
  }
}

function formatWaka(waka) {
  // 番号を削除し、「」を削除し、半角スペースで改行、作者の上にスペース
  let formattedWaka = waka.match(/「(.*)」/);
  formattedWaka[1] = formattedWaka[1].replace(/\s+/g, '\n');
  const authorMatch = waka.match(/- (.*)/);
  const author = authorMatch ? authorMatch[1] : '';
  return `${formattedWaka[1]}\n  ${author}`;
}

function drawVerticalText(ctx, text, x, y) {
  const lines = text.split('\n');
  ctx.font = '30px Arial';
  ctx.fillStyle = 'white';
  ctx.textAlign = 'center';

  lines.forEach((line, i) => {
    const chars = line.split('');
    chars.forEach((char, j) => {
      ctx.fillText(char, x - (i * 30), y + (j * 30));
    });
  });
}

function displayResult(result) {
  const resultDiv = document.getElementById('result');
  resultDiv.innerHTML = ''; // Clear previous result
  const img = document.createElement('img');
  img.src = result;
  resultDiv.appendChild(img);
}

document.addEventListener('click', (event) => {
  if (event.target.tagName === 'IMG') {
    document.querySelectorAll('img').forEach(img => img.classList.remove('selected'));
    event.target.classList.add('selected');
  }
});

```
```javascript:server.js
import 'dotenv/config';
import fs from 'fs';
import path from 'path';
import express from 'express';
import fetch from 'node-fetch';
import { fileURLToPath } from 'url';

const app = express();
const PORT = process.env.PORT || 3000;

// ESモジュール環境で__dirnameを使用するための設定
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

app.use(express.static('public'));
app.use(express.json());

// imgディレクトリを静的ファイルとして提供
app.use('/img', express.static(path.join(__dirname, 'img')));

app.post('/get-waka', async (req, res) => {
  const { utterance } = req.body;
  try {
    const response = await fetch('https://api-mebo.dev/api', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        api_key: process.env.API_KEY,
        agent_id: process.env.AGENT_ID,
        utterance: utterance,
        uid: process.env.UID
      })
    });

    const data = await response.json();
    res.json(data);
  } catch (error) {
    res.status(500).json({ error: 'Failed to fetch waka.' });
  }
});

app.post('/translate', async (req, res) => {
  const { text } = req.body;
  const params = new URLSearchParams();
  params.append('text', text);
  params.append('target_lang', 'EN');

  try {
    const response = await fetch('https://api-free.deepl.com/v2/translate', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Authorization': `DeepL-Auth-Key ${process.env.DEEPL_API_KEY}`
      },
      body: params.toString()
    });

    const data = await response.json();
    console.log(data);
    res.json(data);
  } catch (error) {
    console.error('Error:', error);
    res.status(500).json({ error: 'Translation failed' });
  }
});

async function generateImage(model, prompt) {
  try {
    const response = await fetch(`https://api-inference.huggingface.co/models/${model}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${process.env.HUGGINGFACE_API_KEY}`
      },
      body: JSON.stringify({
        inputs: prompt,
        options: {
          wait_for_model: true
        }
      }),
    });

    if (response.ok) {
      const buffer = await response.arrayBuffer();
      const imagePath = path.join(__dirname, 'img', `${model.replace('/', '-')}.png`);
      fs.writeFileSync(imagePath, Buffer.from(buffer));
      return `/img/${model.replace('/', '-')}.png`;
    } else {
      throw new Error('Failed to generate image.');
    }
  } catch (error) {
    throw new Error('Failed to return.');
  }
}

app.post('/generate-image', async (req, res) => {
  const { model, prompt } = req.body;

  try {
    const imagePath = await generateImage(model, prompt);
    res.json({ imagePath });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});
```
</details>

## 制作過程
今の気持ちからMiiboを使って和歌を選び出すAPIを実装し、テストするところからスタートしました。

### 課題の整理
- 今の気持ちから和歌を選び出す
- 選んだ和歌から画像生成を行う部分を実装する
- 画像に和歌を合成する
- UIを作って、全体を繋げる
- ローカルの画像生成AIを使ってみる
- デモ動画の撮影

### 和歌を生成する部分を実装
LLMを利用するツールとして、miiboを使います。
httpでAPIとして呼び出しをして、和歌を取得できるところまでの動作確認をします。

#### miiboのエージェントを新規作成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212141/cfec627e-2c78-d4b1-951d-2ef7ce34a38e.png)

エージェントの設定から、AIによる応答の設定で、投入するプロンプトの設定を行います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212141/428fa7ae-6171-0f6c-fa29-08dd5527b4be.png)

最初はAIにプロンプトを考えてもらいました。
```
あなたは和歌選定AIです。ユーザーが入力したキーワードに基づいて、関連する和歌を表示してください。

# 役割
和歌選定AIとして、ユーザーが入力したキーワードに関連する和歌を表示します。

# 行動方針
1. ユーザーがキーワードを入力します。
2. 入力されたキーワードに関連する和歌を検索し、表示します。
3. ユーザーが満足するまでこのプロセスを繰り返します。

# プロセス
1. ユーザーにキーワードの入力を促します。
2. キーワードに関連する和歌を検索し、表示します。
3. ユーザーが満足するまで繰り返します。

# ユーザーの状態
下記はユーザーが入力したキーワードです。
キーワード: #{キーワード}

# 和歌の表示
ユーザーが入力したキーワードに関連する和歌を表示してください。

# クイックリプライ
ユーザーが和歌を確認した後、次のアクションを促すクイックリプライを表示してください。
クイックリプライの数は3つです。クイックリプライはユーザー視点での発話候補です。ユーザーが次に話したいと思う言葉を予測して出力しましょう。

# ステートの記録
会話の進行と共に、ユーザーが入力したキーワードをステートに記録してください。
キー名: キーワード
```

このプロンプトを使ってみましたが、どうもイメージとは違うものが出てきます。

`プロンプトは小細工せず、シンプルな方が最新のLLMではよい結果が得られる`とどこかで読んだ気がするので、思い切って単純なものにしました。

```
あなたは和歌選定AIです。ユーザーが入力したキーワードに基づいて、
関連するものを過去の和歌から３つ選択してください。
```
結果、こちらの方がよかったように思えます。

### 動作テスト
miiboのエージェントを外部公開しました。
**公開設定**から、**一般公開**にすると、URLから呼び出すことができるようになります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212141/abf6e186-9c43-3cca-c605-b236070ae601.png" width="50%">

さらに、メニューから

　**外部サービス連携**
 　ー**APIを利用して会話やデータの入稿、エージェントの管理を行う**

を選択して、エージェントのAPIを利用するを選び、APIを有効にします。

すると、API KEYとエージェントIDが生成され、https://api-mebo.dev/api をエンドポイントとして、このエージェントを呼び出すことが出来るようになります。

テストとして、curlサンプルがあるので、これを実行してみます。

```shell
curl -H "Content-Type: application/json" -X POST -d '{"api_key":"MY_API_KEY","agent_id":"MY_AGENT_ID","utterance":"高揚している、紅葉、川が流れる","uid":"MY_UID"}' https://api-mebo.dev/api
```
ここでは3つのIDがありますが、UIDが実際のAPIコールに必要かどうかがよく分かりません。セッションIDとして使うのでしょうか。とりあえず、コードには含めておきます。

応答はこんな感じで、JSONで戻ってきます。（実際には改行は入らずに一行で戻ってきますが、読みにくいので改行を入れました）

```json
{"utterance":"高揚している、紅葉、川が流れる",
 "bestResponse":{
   "utterance":"以下の和歌をご紹介します：\n\n
       1. 山川 に 風のかけたる しがらみは 流れもあへぬ 紅葉なりけり（藤原定家）\n
       2. 紅葉散る 滝の白糸 たえだえに しきてぞまさる 水の玉川（藤原公任）\n
       3. たま川の 水のかさふり まどひつつ 紅葉流れて うきたつを見る（藤原俊成）",
   "score":1100,
   "options":["他にも紅葉に関する和歌を教えてください","春に関する和歌も知りたいです","恋の和歌を紹介してください"],  
   "topic":"",
   "imageUrl":"",
   "url":"",
   "isAutoResponse":true,
   "extensions":null,
   "shouldSelectOption":false,
   "state":"",
   "embededHtml":""
  },
 "avatarIconUrl":"https://firebasestorage.googleapis.com/v0/b/mabo-f1cc7.appspot.com/o/images%2FjrKpyim7GmZ8WW8NVXzECeSA0im1%2F81f54908-6f12-41cd-a388-03dcc962ddf9190b9fb1c6d2bd%2Fwaka.jpg?alt=media&token=9f5205ab-6785-4673-a886-a31dd9e410f0",
 "userState":{},
 "isError":false}
```

注文通り、３つの句が戻ってきています。
このうち一つを選んで画像を生成することになります。




### 和歌から画像生成を行う部分を実装
和歌をプロンプトとして、Hugging Face Inference APIを使って、4種類の画像を生成します。このモデルを決める所から始めました。
それぞれ違うモデルを使うのですが、それを選択するのに時間が掛かりました。
というのは、多くのモデルがAPIを使ってアクセスできないし、出来ると書いていても、やってみると上手くいかない、というものが多いからです。
また、動作確認をしても、時々意味不明のエラーが出るため、エラーが出ることを前提に作る必要があります。

そういったテストを繰り返して、４つを選択しました。
```
'stabilityai/stable-diffusion-xl-base-1.0',
'stabilityai/stable-diffusion-2-1-base',
'stabilityai/stable-diffusion-3-medium-diffusers',
'sd-community/sdxl-flash'
```
結局、３つはStable Diffusionのバージョン違いということになりました。

### 一気に作る
課題には個別に書いていましたが、全体を繋げる所まで一気にプロンプトを作ってChatGPTに作ってもらいました。

プロンプト：
```
以下、API_KEYなどは全て、.envに設定する。
Webサーバーにhttp-serverを使う。

1.和歌を取得する。
呼び出し（サンプル）
curl -H "Content-Type: application/json" -X POST -d '{"api_key":"MY_API_KEY","agent_id":"MY_AGENT_ID","utterance":"高揚している、紅葉、川が流れる","uid":"MY_UID"}' https://api-mebo.dev/api
戻り値（サンプル）
{"utterance":"高揚している、紅葉、川が流れる",
 "bestResponse":{
   "utterance":"以下の和歌をご紹介します：\n\n
       1. 山川 に 風のかけたる しがらみは 流れもあへぬ 紅葉なりけり（藤原定家）\n
       2. 紅葉散る 滝の白糸 たえだえに しきてぞまさる 水の玉川（藤原公任）\n
       3. たま川の 水のかさふり まどひつつ 紅葉流れて うきたつを見る（藤原俊成）",
   "score":1100,
   "options":["他にも紅葉に関する和歌を教えてください","春に関する和歌も知りたいです","恋の和歌を紹介してください"],  
   "topic":"",
   "imageUrl":"",
   "url":"",
   "isAutoResponse":true,
   "extensions":null,
   "shouldSelectOption":false,
   "state":"",
   "embededHtml":""
  },
 "avatarIconUrl":"https://firebasestorage.googleapis.com/v0/b/mabo-f1cc7.appspot.com/o/images%2FjrKpyim7GmZ8WW8NVXzECeSA0im1%2F81f54908-6f12-41cd-a388-03dcc962ddf9190b9fb1c6d2bd%2Fwaka.jpg?alt=media&token=9f5205ab-6785-4673-a886-a31dd9e410f0",
 "userState":{},
 "isError":false}

実装はJavaScriptをクライアントサイドで動かす。


2.和歌を選択して、画像生成する。
　1.の戻り値 utterance の３つの和歌からWebUIにて1つ選択する。
　その和歌をDeepLを使って英語に翻訳する。
　動画生成ボタンを押すと、翻訳した和歌をプロンプトとして、Hugging Face Inference APIを使って、4つのモデルを使ってそれぞれ1つづつ画像を生成する。
　モデルは、
stabilityai/stable-diffusion-xl-base-1.0
stabilityai/stable-diffusion-2-1-base
stabilityai/stable-diffusion-3-medium-diffusers
sd-community/sdxl-flash
　生成した画像をWebUIに表示して、選択することができる。

3.画像と和歌を合成する。
　次にボタンを押すと、生成した画像に、日本語の和歌を合成して、目的の画像を生成して、表示する。
```

出力されたコードは、概ね問題なさそうだったのですが、APIの呼び出しでエラーが出るので、そのあたりを順番に修正していきます。

### DeepLでの問題

まず、エンドポイントが間違っていました。
ChatGPTは、`https://api.deepl.com/v2/translate` を指定してきましたが、自分はフリーなので、
`https://api-free.deepl.com/v2/translate`に変更しました。

この問題は、なかなか分からなかったのですが、curlでテストをして戻り値にこのURLに変更せよとあったので、分かりました。`APIは必ずcurlでテストして、戻り値等を確認する`のがよさそうです。

次に、Content-Typeはjsonではなく、x-www-form-urlencodedに設定する必要がある、ということでした。これはGETで引数を渡すフォーマットと同じなのですが、POSTでBodyにxxxx=yyyy? のように引数を記述する必要があります。

この2点が修正箇所になります。

### Hugging Faceの画像生成での問題

戻り値での画像データの取得方法など、細かい問題があって、その修正は行ないました。
それよりも、本質的な部分は以下の点になります。
- 同時に１つないし２つ程度しか実行できない。
- 処理に時間が掛かり、いつ戻ってくるか分からない。
- 同じ処理をしても、戻ってこない場合やエラーが返ってくる場合が頻発する。

この点に対処しておく必要があります。

最初にChatGPTが出してきたコードは、4つのAPIコールを同時に行って並列処理させるようなものでした。この方が勿論効率はいいのですが、API側がそれを受け付けない仕様なので、順次結果が出てから次の呼び出しを行うようにしました。
呼び出しの結果エラーになったりタイムアウトになった場合は、それを無視して次のモデルに移る、という形にしました。

エラーやタイムアウトの際、リトライするようにしたかったのですが、ChatGPTはその答えを出してくれず、自分で考えるしかなさそうだったので、次回の課題とします。

### テストを繰り返すうちに別の問題が発生

テストを行っていると、ずっと問題なかったはずの和歌の生成がおかしなものになってきました。
こんな和歌あったかな、というようなものになっており、明らかにAIが作ったような拙い和歌が出るようになってきました。

また、DeepLの翻訳も、おかしなものになってきました。
例えば、
>「春過ぎて 夏来にけらし 白妙の 衣ほすてふ 天の香具山」 - 持統天皇

の翻訳が、
> 2. "Spring has passed and summer has come.

春が過ぎて夏が来た、だけじゃなくて、もう少し頑張れ、と思いました。
中には、”May,May,May,May..."　とだけ繰り返すような翻訳になったり。どうも、短時間に呼び出しを繰り返すと、手抜きをし始めるような気がします。
フリーだからでしょうか。

そうはいっても、ちゃんと翻訳してくれることが多いので、この問題には目をつぶります。

和歌の問題は放っては置けないので、プロンプトを変更することにしました。

```
あなたは和歌選定AIです。ユーザーが入力したキーワードに基づいて、関連するものを過去の和歌から３つ選択してください。
作者不詳は禁止する。勝手に作るのも禁止、必ず実在の和歌にすること。
```
2行目を追加しました。

これで、一旦は問題解決しました。

さらに、いつの間にか和歌の戻り値のフォーマットが変わっていました。具体的には、和歌が（春過ぎて 夏来にけらし 白妙の 衣ほすてふ 天の香具山）で括弧に括られて帰ってきたものが、「春過ぎて 夏来にけらし 白妙の 衣ほすてふ 天の香具山」と鍵括弧に括られているのです。
このあたりも揺らぎがあるようなので、どちらにも対応する必要があるのかも知れませんし、フォーマットをきっちりと指定した方がよいのかも知れません。
とりあえず、鍵括弧に対応する形としました。

### とりあえず、完成形

ということで、最初の動画を撮影して完成としました。
このキーワードで、この和歌で良いのか、という所は目をつぶります。

## 反省
残った課題として、
- ローカルの画像生成AIを使ってみる

というのがあります。

動作環境をローカルにしたのは、じつはこの課題を想定していたものでしたが、ちょっと時間が掛かり過ぎてタイムアップとなってしましました。

新しい課題としては、
- Hugging Faceの呼び出しで、エラーの場合にリトライをする
- 和歌の戻り値のフォーマットのゆらぎを無くす
- AIが馬鹿になっていく原因を突き止める

があります。

１回の試行では問題は無くとも、繰り返していくと馬鹿になっていくものだと実用には堪えないので、この辺りがAIを使って実用的なアプリを作る上での重要は課題のように思えます。

あと、***見た目***がAIが作ったデフォルトなので、もうちょっと見栄えのするものにしたかったです。
この辺りは、CSSファイルを作らせる、UIを絵で描いて指定する、とか工夫の余地は大きかったです。
まずは見た目から入る、という作り方の方がよかったかも知れません。

自分の興味として、見た目よりもロジックの方に目が行ってしまうので、そのあたりは要改善ですね。
皆さんに見てもらうには、まずは目につかないと。

以上