見出し画像

AIが写真をモンスターに変換!「モンスター占い」Webアプリ【ソース有】

はじめに

以前、OpenAIのAPIを利用したWebアプリを非常に簡単に作成・実行・配布する方法を提案させていただきました。その記事を読まれていない方は、まず、こちらをご覧下さい。

せっかくなので、この仕組みを利用した何かしらのWebアプリを作ってみたいところです。可能であれば、簡単に作成出来、Webならではのコンテンツの方が面白そうですよね!

…ということで、今回はブラウザのWebカメラを利用したマルチモーダルなアプリケーションを作成してみたいと思います!


なぜ、このWebアプリなのか?

私が作成したコンテンツで、チャットモンスター…通称「チャトモン」というコンテンツがあります。

これは2枚のモンスター画像を用意して闘わせる…という遊びです。非常にシンプルで、大人も子供も簡単に楽しめるコンテンツなのですが…ひとつ問題があります。
それは…

「対戦させる画像を準備するのが大変!」

…というところです。
何回かチャトモンのイベントを行いましたが、結構な頻度で発生したのが…

「画像を生成するのに時間がかかるため、画像生成で行列が出来、対戦側に空き時間が出来てしまう!」

…という問題です。
チャトモンのイベントは親子連れが多く、親子がコミュニケーションとりながらお子さんが…「思い通りの絵が描けるまで、こだわりたい!」…とやっていると、どうしても時間がかかってしまいます。

そこで、プロンプトエンジニアリングの知識も不要で、時間をかけずに画像生成が出来、かつ、生成AIの面白さも体験出来るコンテンツがないかを考えてみました。
そのコンテンツの名前は…

「モンスター占い!」

…になります!

「モンスター占い」とは?

「モンスター占い」とはどのようなものでしょう?
簡単に説明すると…
ブラウザのWebカメラで貴方の顔の写真を撮影し、その顔からAIが人相学や想像力を駆使し、「どのようなモンスターのタイプか?」を占い「あなたの未来と過ごし方のアドバイス」が表示されるというコンテンツです!そして、その貴方をイメージしたモンスターの画像まで生成されます!

…つまり、占いで生成されたモンスターを自分の代理として「チャトモン」でお友達と対戦出来る!という事なのです!

対戦までの流れは下記のようになります。

顔写真撮影 → 占い結果の表示 → モンスター画像が作成 → モンスターの画像から能力表示 → お友達モンスターと対戦

こんな流れで、自分という素材を使って、いろいろな体験と発見が出来るような…そういう世界を目指す!という感じですね!

自分の写真が恥ずかしい…という場合は、自分の描いた絵を写真に撮っても同じ事が可能です!

モンスター占いの遊び方

このアプリを動かすと、このような画面になります。

画面イメージ

左上に「Webカメラ」、左下に「貴方をモンスターにした画像」、その間に「写真の人を占う」ボタンがあります。
右側に「モンスター占いの結果」が表示されています。

なお、左上のWebカメラの部分ですが…劇画調の絵が描かれていますが、実際はここにカメラで映された貴方の顔が表示される事になります。自分の顔が映るのは恥ずかしいので差し替えさせていただきましたw

これをベースに操作方法をご説明します。

① のステップ

まず、ブラウザを開いたらカメラの許可を求められるので許可をします。ノートPCの場合はオンライン会議などで自分の姿が映るようにカメラがついているはずです。そこのカメラから自分の姿が映されますので、すぐ下の「写真の人を占う」ボタンで写真を撮影しましょう!
この写真はAIで処理しますのでOpenAIに送信されます。情報漏洩は無いかと思いますが…節度を超えるような写真は送らない方が良いかと思います。なお、このWebアプリ自体では写真の保存はしません。

② のステップ

「写真の人を占う」ボタンが消え、Webカメラの下に撮影した写真が表示されます。そして…しばらく待つと右に占い結果が表示されます。占いの結果は表情にも左右されますので、いろいろ試してみると面白いかもしれません。人ではなくても占えます。
真剣に考えず、ネタとして楽しむぐらいでいきましょう!

③ のステップ

これで、最後です。
さらに待つと、Webカメラで撮影した画像がモンスターの画像に差し変わります。その画像をチャトモンなどに利用したい場合は右クリックして画像を保存するようにして下さい。続けて①から操作できますが、今表示されているモンスター占いの結果やモンスター画像は消えてしまいますので、必要であれば手作業で保存しておいて下さい。

セキュリティ的な問題について

モンスター画像が生成されたので、これをワンクリックでダウンロードさせたり、その絵を元にカード化させたりしたくなると思います。しかし、これはラウザのセキュリティエラーに引っかかり、うまく動作しません。そのため、画像が欲しい場合は右クリックで保存する以外ありません。
これをどうしても何とかしたい場合は、こちらを参考にしてみて下さい。

こちらについては、私の方でもまた試してみたいと思います。

もうひとつの問題として、ブラウザのカメラ機能はHTTPSでの通信が必須なる事が挙げられます。このため、普通のホームページサービスでは対応していない可能性があります。多くの人に公開したい場合は注意して下さい。

ただし、今回のように、自分のPCで実行する(ローカルホストでの実行:http://localhost)ではHTTPでもカメラ機能を使うことができます。

このWebアプリのプログラムソース

このWebアプリは「HTMLファイルひとつでOpenAI-APIを使いこなす!」という過去の記事の通り…

「今回もHTMLファイルひとつで動きます!」

プログラムのソースファイルは下記の通りとなります。htmlフォルダに適当なサブフォルダを作成し、下記のソースをindex.htmlというファイル名で保存しましょう。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>モンスター占い</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      display: flex;
      align-items: center;
      height: 100vh;
      margin: 0;
      background-color: #f0f0f0;
      justify-content: center;
      flex-direction: column;
    }
    .header {
      width: 100%;
      background-color: #007BFF;
      color: #fff;
      text-align: center;
      padding: 5px 0;
      font-size: 32px;
    }
    .container {
      display: flex;
      flex-wrap: wrap;
      background: #fff;
      box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
      border-radius: 10px;
      overflow: auto;
      width: 90%;
      max-width: 1200px;
      padding: 10px;
      box-sizing: border-box;
      margin-top:10px;
      margin-bottom:10px;
    }
    .canvas-container {
      flex: 1 1 512px;
      display: flex;
      flex-direction: column;
      justify-content: flex-start;
      align-items: center;
    }
    .canvas-container canvas,
    .canvas-container video {
      max-width: 100%;
      height: auto;
    }
    .info-container {
      flex: 1 1 600px;
      padding: 10px;
      overflow-y: auto;
    }
    .info-container h2 {
      margin-top: 0;
      font-size: 26px;
      color: #333;
    }
    .info-container p {
      font-size: 18px;
      color: #666;
      line-height: 1.4;
      margin-bottom: 10px;
    }
    .button-container {
      width: 100%;
      display: flex;
      justify-content: center;
      margin-top:10px;
      margin-bottom:10px;
    }
    .button-container button {
      padding: 10px 20px;
      font-size: 14px;
      color: #fff;
      background-color: #007BFF;
      border: none;
      border-radius: 5px;
      cursor: pointer;
    }
    .button-container button:hover {
      background-color: #0056b3;
    }
  </style>
</head>
<body>
  <div class="header">
    モンスター占い
  </div>
  <div class="container">
    <div class="canvas-container">
      <video id="video" width="384" height="288" autoplay></video>
      <div class="button-container">
        <button id="telling">写真の人物を占う</button>
      </div>
      <canvas id="monsterCanvas" width="384" height="384" style="background-color: #CCCCCC;"></canvas>
      <canvas id="videoCanvas" width="384" height="288" style="background-color: #CCCCCC;margin-bottom:96px;display:none;"></canvas>
    </div>
    <div class="info-container">
      <p><strong>種類:</strong> <span id="type"></span></p>
      <p><strong>特性:</strong> <span id="traits"></span></p>
      <p><strong>弱点:</strong> <span id="weakness"></span></p>
      <p><strong>居場所:</strong> <span id="habitat"></span></p>
      <p><strong>仲間:</strong> <span id="companions"></span></p>
      <p><strong>強さ:</strong> <span id="strength"></span></p>
      <p><strong>好きなもの:</strong> <span id="likes"></span></p>
      <p><strong>恐れるもの:</strong> <span id="fears"></span></p>
      <p><strong>未来:</strong> <span id="future"></span></p>
      <p><strong>アドバイス:</strong> <span id="advice"></span></p>
    </div>
  </div>
  <script>

    let chatGptApiKey = window.prompt("Please enter your OpenAI API key:");
    const endPoint = "https://api.openai.com/v1/chat/completions";
    const endPointImg = "https://api.openai.com/v1/images/generations";
    const modelName_GPT4V = "gpt-4o";

	// **** カメラの設定 ****
	navigator.mediaDevices.getUserMedia({ video: true })
	  .then(function(stream) {
	    var video = document.getElementById('video');
	    video.srcObject = stream;
	    video.onloadedmetadata = function(e) {
	      video.play();
	    };
	  })
	  .catch(function(err) {
	    console.log("An error occurred: " + err);
	  });
	  

	// **** 占いを行う ****
	async function telling(base64ImageData) {
	
    	const prmpt_sys = "あなたは世界一の占い師であり心理学者かつモンスター博士です。優れた人相学を駆使し、非常に想像力豊かな発想で、非常にユニークな回答をしてください。";
    	const prmpt_usr =  `
			次の特性に基づいて、ユニークなモンスターの詳細な説明を日本語で生成してください: 種類、特性、弱点、居場所、仲間、強さ、好きなもの、恐れるもの、未来、アドバイス。この説明は生き生きとした想像力豊かなものであり、写真の背景を無視し、分析した人物の特徴を反映するものである必要があります。出力はJSONのみとし、次の形式のJSONで行ってください:

			{
			  "type": "モンスターの種類",
			  "traits": "モンスターの主要な特性や能力",
			  "weakness": "弱点や脆弱性",
			  "habitat": "モンスターが住んでいる環境や場所",
			  "companions": "モンスターの友人や仲間",
			  "strength": "モンスターの強さや能力",
			  "likes": "モンスターが好むものや趣味",
			  "fears": "モンスターが恐れるものや苦手なもの",
			  "future": "モンスターの未来の予測や展望",
			  "advice": "モンスターからのアドバイスやメッセージ"
			  "prompt": "モンスターのちびキャラ画像を生成するためのdalle3へのプロンプト"
			}
		`;

        let response = await callChatMon(base64ImageData,prmpt_sys,prmpt_usr);
        
        const cleanedTextData = response.replace(/```json/g, '').replace(/```/g, '').trim();
        const monsterData = JSON.parse(cleanedTextData);
        
	    document.getElementById('type').innerText = monsterData.type;
	    document.getElementById('traits').innerText = monsterData.traits;
	    document.getElementById('weakness').innerText = monsterData.weakness;
	    document.getElementById('habitat').innerText = monsterData.habitat;
	    document.getElementById('companions').innerText = monsterData.companions;
	    document.getElementById('strength').innerText = monsterData.strength;
	    document.getElementById('likes').innerText = monsterData.likes;
	    document.getElementById('fears').innerText = monsterData.fears;
	    document.getElementById('future').innerText = monsterData.future;
	    document.getElementById('advice').innerText = monsterData.advice;
        
	    displayImage("one chibi monster,Without Text,Remove Text,Without the instructions,Without color palette,Detailed and realistic light novel illustration style," + monsterData.prompt, chatGptApiKey, document.getElementById('monsterCanvas'));

	}


	// **** 画像を元に占い結果を取得 ****
	function callChatMon(base64Image,prmpt_sys,prmpt_usr) {
	  return new Promise((resolve, reject) => {
	    const messages = [
	      {
	        role: "system",
	        content: `${prmpt_sys}`,
	      },
	      {
	        role: "user",
	        content: [
	          {
	            type: "text",
	            text: `${prmpt_usr}`,
	          },
	          {
	            type: "image_url",
	            image_url: {
	              url: `data:image/jpeg;base64,${base64Image}`,
	              detail: "low",
	            },
	          },
	        ],
	      },
	    ];

	    const requestOptions = {
	      method: "POST",
	      headers: {
	        "Content-Type": "application/json",
	        Authorization: `Bearer ${chatGptApiKey}`,
	      },
	      body: JSON.stringify({
	        model: modelName_GPT4V,
	        messages: messages,
	        max_tokens: 1000,
	      }),
	    };

	    const myRequest = new Request(endPoint, requestOptions); 

	    fetch(myRequest)
	      .then((res) => res.json())
	      .then((json) => {
	        resolve(json.choices[0].message.content);
	      })
	      .catch((err) => {
	        reject("エラーが発生しました。再度やり直してください。");
	        try{
	        	console.error("Error:", err);
	        }catch(e){
	        }
	      });
	  });
	}

    // **** 画像の生成 ****
	async function generateImage(prompt, apiKey) {
	    try {
	        const response = await fetch(endPointImg, {
	            method: 'POST',
	            headers: {
	                'Authorization': `Bearer ${apiKey}`,
	                'Content-Type': 'application/json',
	            },
	            body: JSON.stringify({
			      'model' : 'dall-e-3',
			      'quality' : 'standard',
			      'style' : 'vivid',
	                prompt: prompt,
	                n: 1,
	                size: "1024x1024",
	            }),
	        });
	        const data = await response.json();
	        const image = data.data[0].url;
	        return image;
	    } catch (error) {
	        throw new Error(error);
	    }
	}

    // **** 画像を表示する ****
	async function displayImage(prompt, apiKey, canvasElement) {
	    try {
	        const imageUrl = await generateImage(prompt, apiKey);
	        const imgElement = document.createElement('img');
	        imgElement.src = imageUrl;
	        imgElement.onload = function() {
	            const context = canvasElement.getContext('2d');
	            context.drawImage(imgElement, 0, 0, canvasElement.width, canvasElement.height);
	            document.getElementById('monsterCanvas').style.display = 'block';
                document.getElementById('videoCanvas').style.display = 'none';
      			document.getElementById('telling').style.visibility = 'visible';
	        }
	    } catch (error) {
	        console.error('Error:', error);
	    }
	}


	// **** スナップショットを撮る ****
	document.getElementById("telling").addEventListener("click", function() {
	  var canvas = document.getElementById('videoCanvas');
	  var context = canvas.getContext('2d');
	  var video = document.getElementById('video');

	  document.querySelectorAll('.info-container span').forEach(span => span.innerText = '');
      document.getElementById('monsterCanvas').style.display = 'none';
      document.getElementById('videoCanvas').style.display = 'block';
      document.getElementById('telling').style.visibility = 'hidden';
      
      context.drawImage(video, 0, 0, 384, 288);

	  // 画像をBASE64の文字列に変換して表示する
	  var base64ImageData = canvas.toDataURL().split(',')[1]; // カンマ以降のBASE64部分を取得

	  // 占いを行う
	  telling(base64ImageData)

	});

  </script>
</body>
</html>

ソースファイルです。zipでも提供いたします。

html/MonsterUranaiというフォルダーにindex.htmlを保存した場合は…

http://localhost/MonsterUranai

というURLを呼び出せば実行できます!
このソースファイルは「Webカメラでの撮影」「画像をAIで診断」「プロンプトを元にした画像生成」が含まれますので、これらを利用していろいろなものが作れそうですね!

ぜひ、皆様、いろいろ試して、自分のオリジナルのWebアプリを作成してみて下さい!!

この記事が気に入ったらサポートをしてみませんか?