AI,API,gemini,Google Apps Script,クラウド,中級,生成AI

このチュートリアルでは、Google Gemini APIに3択クイズを自動生成してもらい、正解を当てるアプリを作ります。難易度は「簡単,中級,難解」から選択できます。正解すると点数が加算されます。

Google Gemini APIの呼び出しにはGoogle Apps Script(GAS)を使います。

Google AI Studio で作るGemini APIは課金方法を登録しなくても以下の範囲ならば無料で使えます。APIキーは自分で取得してください。

  • 1 分あたりのリクエスト数: 5
  • 1 日あたりのリクエスト数: 250,000
  • 1 分あたりのトークン数(入力): 100

Google Apps Scriptの設定

API呼び出し処理を行うサーバー側のプログラムをGoogle Apps Scriptに設定します。

Google Apps Scriptの設定方法はGoogle公式ページのこちら、Google AI Studio でのAPIキーの取得方法はGoogle公式ページのこちらを参照ください。

Google Apps Script

Google Apps Scriptに設定するスクリプトはJavaScriptで書きます。http Getで呼び出せるようにdoGet()関数を書きます。ブラウザで簡単にテストできるのでdoGet()関数の方がdoPost()関数よりも便利です。

🔒 このページは以下の会員レベルで閲覧できます

残りのコンテンツを表示するにはログインする必要がありますどうぞ . 会員ではないですか ? 入会のご案内

AI,gemini,JavaScript,アプリ開発,チュートリアル,バイブコーディング,中級,生成AI

App Inventorでテトリスゲームを作ろうと思い立ち、いろいろと調べてみたところ、App Inventorだけで実装している記事を見つけました。しかし、その内容を見るとかなり手間がかかっており、大変そうでした。

本来、App Inventorは「簡単にスマホアプリを作る」ことを目的としたツールのはずです。それにもかかわらず、ゲームをひとつ作るのにここまで苦労するのは本末転倒だと感じました。そこで、複雑な処理はWebViewer内でJavaScriptに任せればよいことを思い出しました。

例えば、関数電卓アプリでは、複雑な計算部分をJavaScriptのライブラリで処理し、App Inventor側では結果の表示や操作部分だけを担当させることで、全体をずっと簡潔に構築できます。

そこで今回は、GoogleのGemini 2.5 Proを活用してテトリスゲームを作成します。なお、ここに掲載しているプロンプトと返答は、掲載用に改めて行った会話の内容であり、実際にゲームを作成した際のやり取りとは多少異なります。

実際に作ったテトリスゲームのチュートリアルはこちらです。

Gemini 2.5 Proとの対話

プロンプト

App Inventorでテトリスゲームを作りたい。ゲーム本体はウェブビュー内でJavaScriptで実行する。ゲームプレイ用のボタンなどはApp Inventor側で定義する。ウェブビューで使うhtmlファイルは1個にまとめる。

返答

App InventorとWebViewを組み合わせたテトリスゲームですね。承知いたしました。

WebView内で動作する、単一のHTMLファイルを作成します。このファイルには、ゲームのロジック(JavaScript)、描画(Canvas)、スタイル(CSS)がすべて含まれています。

App Inventor側では、このHTMLファイルをメディアとしてアップロードし、WebViewのHomeUrlに設定します。そして、App Inventorで作成したボタンから、WebViewのJavaScript関数を呼び出す(RunJavaScriptブロックを使用)ことでゲームを操作します。

こちらが、App InventorのWebViewで使用するための単一のHTMLファイルです。

tetris.html

🔒 このページは以下の会員レベルで閲覧できます

残りのコンテンツを表示するにはログインする必要がありますどうぞ . 会員ではないですか ? 入会のご案内

AI,API,gemini,Google Apps Script,クラウド,中級,生成AI

最近家族と公園に行ったのですが、見たことのないトンボが飛んでいました。なんという種類のとんぼなのか調べるのに生成AIが使えるんではないかと思い、カメラで撮影して調べるアプリを作ろうと思い立ちました。

そこで、このチュートリアルでは、Google Gemini APIを使って撮影した写真を解析し、写っているものを百科事典を引いてわかりやすく説明してくれるアプリを作ります。さらに説明の読み上げ機能もつけました。生徒の学習用に良いと思います。

Google Gemini APIの呼び出しにはGoogle Apps Script(GAS)を使います。GASのスクリプトはGoogle Gemini 2.5 Proで書いた生成AIお絵描きコーチアプリ用のスクリプトのプロンプトを書き換えています。プロンプトの書き換えにはもちろんGoogle Gemini 2.5 Proを使っています。

Google AI Studio で作るGemini APIは課金方法を登録しなくても以下の範囲ならば無料で使えます。

  • 1 分あたりのリクエスト数: 5
  • 1 日あたりのリクエスト数: 250,000
  • 1 分あたりのトークン数(入力): 100

Google Apps Scriptの設定

Gemini APIの設定からウェブアプリとしてのデプロイ方法、使い方などは生成AIお絵描きコーチアプリを参照してください。

Code.gs – 写真百科事典スクリプト

/**
 * App InventorからのPOSTリクエストを処理する関数
 * @param {Object} e - App Inventorから送信されるイベントオブジェクト
 * @return {ContentService.TextOutput} - Gemini APIからの結果をJSON形式で返す
 */
function doPost(e) {
  // エラーハンドリング
  try {
    // 1. App Inventorから送信されたJSONデータを解析
    const postData = JSON.parse(e.postData.contents);
    const base64Image = postData.image; // App Inventor側で "image" というキーに設定

    // 2. スクリプトプロパティからAPIキーを読み込む
    const API_KEY = PropertiesService.getScriptProperties().getProperty('API_KEY');
    if (!API_KEY) {
      return createErrorResponse('APIキーが設定されていません。');
    }

    // 3. Gemini APIのエンドポイントURL
    const API_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent?key=${API_KEY}`;
  
    // 4. Gemini APIに送信するリクエストボディを作成
    const payload = {
      "contents": [{
        "parts": [{
          "text": `# 命令
あなたは、あらゆる事象を網羅した百科事典の知識を持つ博士です。提供された画像を注意深く分析し、以下の指示に従って解説文を作成してください。

# 実行ステップ
1.  **被写体の特定:** 画像に写っている最も主要な対象物(物、生物、場所、人物など)を正確に特定します。
2.  **情報抽出:** 特定した対象物について、あなたの知識の中から「名称」「最も重要な特徴」「歴史的背景や興味深い事実」を抽出します。
3.  **文章構成:** 抽出した情報を基に、最も伝えたい核心部分から書き始め、読み手の興味を引くように自然な文章を構成します。

# 出力制約
*   文字数: 200文字以内
*   段落: 必ず一つの段落で完結させること。
*   形式: 箇条書きは絶対に使用しないこと。
*   文体: 専門的でありながらも、その分野に詳しくない人にも分かりやすい、簡潔で平易な言葉で記述すること。`
        }, {
          "inline_data": {
            "mime_type": "image/png", // App InventorのCanvasはPNG形式
            "data": base64Image
          }
        }]
      }]
    };

    // 5. Gemini APIにリクエストを送信するための設定
    const options = {
      'method': 'post',
      'contentType': 'application/json',
      'payload': JSON.stringify(payload),
      'muteHttpExceptions': true // エラーレスポンスを例外ではなくオブジェクトとして取得
    };

    // 6. APIにリクエストを送信
    const response = UrlFetchApp.fetch(API_URL, options);
    const responseCode = response.getResponseCode();
    const responseBody = response.getContentText();

    if (responseCode !== 200) {
       return createErrorResponse(`APIリクエストエラー: ${responseCode} ${responseBody}`);
    }
    
    // 7. APIからのレスポンスを解析
    const resultJson = JSON.parse(responseBody);
    
    // レスポンスの構造をチェックし、テキスト部分を取得
    let generatedText = "解析結果を取得できませんでした。";
    if (resultJson.candidates && resultJson.candidates.length > 0 &&
        resultJson.candidates[0].content && resultJson.candidates[0].content.parts &&
        resultJson.candidates[0].content.parts.length > 0) {
      generatedText = resultJson.candidates[0].content.parts[0].text;
    } else {
       // Geminiからのレスポンスが期待した形式でない場合
       return createErrorResponse(`予期しないAPIレスポンス形式です: ${responseBody}`);
    }

    // 8. App Inventorに返すJSONを作成
    const output = JSON.stringify({
      "status": "success",
      "result": generatedText
    });

    // 9. 結果をJSON形式で返す
    return ContentService.createTextOutput(output).setMimeType(ContentService.MimeType.JSON);

  } catch (error) {
    // スクリプト全体で発生したエラーをキャッチ
    return createErrorResponse(`スクリプトエラー: ${error.toString()}`);
  }
}

/**
 * エラーレスポンスを生成するヘルパー関数
 * @param {string} message - エラーメッセージ
 * @return {ContentService.TextOutput} - エラー情報を格納したJSONオブジェクト
 */
function createErrorResponse(message) {
  const errorOutput = JSON.stringify({
    "status": "error",
    "message": message
  });
  return ContentService.createTextOutput(errorOutput).setMimeType(ContentService.MimeType.JSON);
}

App Inventorアプリ

ページの最後にあるダウンロードセクションからソースコードをダウンロードできます。

[プロジェクト]メニューから[新規プロジェクトを始める]を選択し、"EncyclopediaCamera"と名前を付けます。

デザイン編集

🔒 このページは以下の会員レベルで閲覧できます

残りのコンテンツを表示するにはログインする必要がありますどうぞ . 会員ではないですか ? 入会のご案内

gemini,JavaScript,ゲーム,チュートリアル,中級

有名なゲームのテトリスを作ります

このチュートリアルでは、有名なゲームのテトリスを作ります。App Inventorだけで作った記事がありましたが随分大変そうなので、Google Gemini 2.5 Proに相談してウェブビュー内のJavaScriptでゲーム本体は作りました。App InventorからはこのJavaScriptをコントロールしています。

ページの最後にあるダウンロードセクションからソースコードをダウンロードできます。

[プロジェクト]メニューから[新規プロジェクトを始める]を選択し、"Tetris"と名前を付けます。

このチュートリアルで学べること

  • ウェブビューでJavaScriptを実行する方法
  • ウェブビューとの値の受け渡し方法

使用コンポーネント

ボタン、ラベル、ウェブビュー、水平配置、垂直配置

使用ブロック

特別なものは無し

デザイン編集

Screen1のタイトルを"テトリス"にします。

ページの最後にあるダウンロードセクションからtetris.htmlとover.mp3をダウンロードして、両方のファイルをファイルをアップロードボタンをクリックしてアップロードします。

レイアウト パレットから 垂直配置コンポーネント をビューアーに追加します。水平に整列するは"中央揃え"に、高さと横幅は"親要素に合わせる"に設定します。名前は垂直配置1になります。

ユーザーインターフェース パレットからウェブビュー垂直配置1に追加します。高さと横幅は"親要素に合わせる"に設定します。名前はウェブビュー1になります。

レイアウト パレットから 水平配置コンポーネント(水平配置1) を垂直配置コンポーネントに追加し、水平に整列するは"中央揃え"に、横幅は"親要素に合わせる"に設定します。水平配置1の中にユーザーインターフェース パレットからボタンを4個追加します。名前はボタン1ボタン2ボタン3ボタン4になります。

ユーザーインターフェース パレットからボタンラベル水平配置1の下に追加します。名前はそれぞれボタン5ラベル1になります。

以下のようにボタンラベルのプロパティを設定します。

  • ボタン1: テキスト="左″
  • ボタン2: テキスト="回転″
  • ボタン3: テキスト="右″
  • ボタン4: テキスト="下″
  • ボタン5: テキスト="スタート″
  • ラベル1: テキスト="″、横幅="親要素に合わせる"

最後にメディアパレットから、 サウンドコンポーネントをビューアーにドラッグアンドドロップします。ソースプロパティをover.mp3に指定します。

下図のようになります。

ブロック編集機能を使用したプログラミング

🔒 このページは以下の会員レベルで閲覧できます

残りのコンテンツを表示するにはログインする必要がありますどうぞ . 会員ではないですか ? 入会のご案内

AI,API,Google Apps Script,nano banana,クラウド,中級,生成AI

このチュートリアルでは、Google Gemini APIを使って、撮影した写真から有名な画家が描いたような絵を表示するアプリを作ります。

2025年8月にGoogleから公開されたNano Banana(Gemini 2.5 Flash Image)は写真を人物や被写体の特徴を保ったまま自然言語で自由に加工・変換できるAIです。生成AIコスプレカメラアプリでは撮影した顔写真からコスプレした全身写真を表示しましたが、別の面白い応用として顔写真を有名画家風に変更してみました。

こちらがGoogleによるNano Bananaの解説です。

https://note.com/google_gemini/n/n1afcbf4d5275?rt=email&sub_rt=daily_report_followee_notes

生成AI画家風カメラアプリのデモビデオをこちらからご覧ください。

ただ、Nano BananaはAPI経由では無料では使えませんので、課金方法の登録が必要です。Nano Bananaを使う場合はGoogle AI Studioから課金方法を登録してください。

Google Gemini APIの呼び出しにはGoogle Apps Script(GAS)を使います。GASのスクリプトはGoogle Gemini 2.5 Proで書いた生成AIお絵描きマジックアプリ用のスクリプトのプロンプトを書き換えています。スクリプトのプロンプトを書き換えるだけで簡単に機能が変更できるので、参考にしてください。

Google Apps Scriptの設定

Gemini APIの設定からウェブアプリとしてのデプロイ方法、使い方などは生成AIお絵描きコーチアプリを参照してください。

Code.gs – 絵画風カメラスクリプト

/**
 * App InventorからのPOSTリクエストを処理するメイン関数
 * @param {Object} e - App Inventorから送信されるイベントオブジェクト
 * @returns {ContentService.TextOutput} - 処理結果(新しい画像のBase64文字列 or エラーメッセージ)をJSON形式で返す
 */
function doPost(e) {
  // レスポンスの基本構造
  let response = {
    status: 'error',
    message: 'An unknown error occurred.',
    imageData: null
  };

  try {
    // POSTされたJSONデータをパース
    if (!e || !e.postData || !e.postData.contents) {
      throw new Error('Invalid request: No data received.');
    }
    const jsonData = JSON.parse(e.postData.contents);
    const base64Data = jsonData.imageData;
    const artist = jsonData.artist;
    const prompt = jsonData.prompt || `写真自体の特徴は一切変更しないで、${artist}風に描いて。言葉での返答は不要`;

    // 入力データの検証
    if (!base64Data || typeof base64Data !== 'string') {
      throw new Error('Invalid request: imageData is missing or not a string.');
    }

    // Base64データからヘッダーを削除
    const cleanedBase64 = cleanBase64String(base64Data);

    // スクリプトプロパティからAPIキーを取得
    const apiKey = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
    if (!apiKey) {
      throw new Error('Configuration error: API key is not set in Script Properties.');
    }

    // Gemini APIを呼び出して新しい画像を生成
    const generatedBase64 = callGeminiApi(apiKey, cleanedBase64, prompt);

    // 成功レスポンスを作成
    response = {
      status: 'success',
      message: 'Image generated successfully.',
      prompt: prompt,
      imageData: generatedBase64
    };

  } catch (error) {
    // エラーレスポンスを作成
    console.error(error.toString());
    response.message = error.message;
  }

  // 結果をJSON形式で返す
  return ContentService.createTextOutput(JSON.stringify(response))
    .setMimeType(ContentService.MimeType.JSON);
}

/**
 * Base64文字列からヘッダー部分(例: "data:image/png;base64,")を削除する
 * @param {string} base64String - 元のBase64文字列
 * @returns {string} - ヘッダーが削除されたBase64文字列
 */
function cleanBase64String(base64String) {
  // "data:[<mime-type>];base64," という形式のヘッダーを正規表現で探して削除
  return base64String.replace(/^data:image\/[a-z]+;base64,/, '');
}

/**
 * Gemini API (Image Generation) を呼び出して画像を生成する
 * @param {string} apiKey - Google AI StudioのAPIキー
 * @param {string} base64Image - 解析・改良の元となる画像のBase64文字列
 * @param {string} prompt - 画像をどのように改良するかを指定する指示文
 * @returns {string} - 生成された新しい画像のBase64文字列
 */
function callGeminiApi(apiKey, base64Image, prompt) {
  // Geminiの画像生成モデルのエンドポイント
  const model = 'gemini-2.5-flash-image-preview';
  const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;

  // APIに送信するペイロード(リクエストボディ)
  const payload = {
    contents: [{
      parts: [
        // 1. 指示テキスト
        { text: prompt },
        // 2. 入力画像
        {
          inlineData: {
            mimeType: 'image/png', // App InventorのCanvasはPNG形式
            data: base64Image
          }
        }
      ]
    }],
    generationConfig: {
      // レスポンスとして画像とテキストの両方を要求する
      responseModalities: ['IMAGE', 'TEXT'] 
    },
  };

  // APIリクエストのオプション
  const options = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload),
    muteHttpExceptions: true // HTTPエラー時に例外をスローさせず、レスポンスを直接受け取る
  };

  // APIにリクエストを送信
  const response = UrlFetchApp.fetch(url, options);
  const responseCode = response.getResponseCode();
  const responseBody = response.getContentText();

  // レスポンスを解析
  if (responseCode === 200) {
    const jsonResponse = JSON.parse(responseBody);
    
    // レスポンスの中から画像データ部分を探す
    const imagePart = jsonResponse?.candidates?.[0]?.content?.parts?.find(p => p.inlineData);
    
    if (imagePart && imagePart.inlineData.data) {
      // 画像のBase64データを返す
      return imagePart.inlineData.data;
    } else {
      // 画像が見つからない場合のエラー
      console.error('API response did not contain image data. Response: ' + responseBody);
      // テキスト部分があれば、それをエラーメッセージとして利用
      const textPart = jsonResponse?.candidates?.[0]?.content?.parts?.find(p => p.text);
      const errorMessage = textPart ? textPart.text : 'No image data in response.';
      throw new Error(`API Error: ${errorMessage}`);
    }
  } else {
    // APIがエラーを返した場合
    console.error('API Error. Code: ' + responseCode + '. Body: ' + responseBody);
    throw new Error(`API request failed with status code ${responseCode}. Details: ${responseBody}`);
  }
}

App Inventorアプリ

ページの最後にあるダウンロードセクションからソースコードをダウンロードできます。

[プロジェクト]メニューから[新規プロジェクトを始める]を選択し、"ArtCamera"と名前を付けます。

デザイン編集

🔒 このページは以下の会員レベルで閲覧できます

残りのコンテンツを表示するにはログインする必要がありますどうぞ . 会員ではないですか ? 入会のご案内

ChatGPT,アプリ開発,ノート,生成AI

考えたことメモ > App InventorからiPhoneアプリを作るには?

App InventorからPythonへどうやって移行するのか?」でChatGPT-4oがApp Inventorのaiaファイルを直接読み込み、アプリの要件定義書を書いてそのまま動作するPythonコードを書けました。

ここではChatGPT-5を使ってApp Inventorのaiaファイルを直接読み込み、アプリの要件定義書を書いてiPhone用のSwiftUIコードを書いてみます。

出発点のApp Inventorアプリは「オンデマンドアカデミー」で紹介されている カラフルお絵かきアプリです。このアプリは、基本的なUIの構成やイベント処理の理解に適しています。

aiaファイルの読み込みと解析

要件定義書作成

SwiftUIプログラミング

動作確認

サマリー

ChatGPT-5はApp Inventorのaiaファイルを直接読み込み、アプリの要件定義書を書いてそのまま動作するSwiftUIコードを書けました。

aiaファイルの読み込みと解析

プロンプト

あなたはApp Inventor 2の内部構造に精通したプロのソフトウェアエンジニアです。このaiaファイルを完全に解析してください。

返答

解析しました!まず要点をざっと👇

画面は Canvas(キャンバス)+3本の水平配置。上段に色(黒/青/グリーン/赤)、中段に太さ(細/中/※太の候補)、下段に「画面消去」「消しゴム」ボタン。

ブロックは 126ノード。主な構成は変数取得/設定、イベント、プロシージャ定義、Canvas 操作(DrawLine / Clear)。

定義プロシージャ:色ボタンリセット / 色ボタン選択 / 太さボタンリセット / 太さボタン選択 / 消しゴムボタン選択。

イベント:Screen1.Initialize, Canvas.Dragged, (ボタン)Click。
初期化で「選択中の色」「ペン太さ」を渡してプロシージャ呼び出し→UI状態と PaintColor / LineWidth を確定。
Dragged では prevX, prevY -> currentX, currentY へ DrawLine で描画。
「画面消去」は Canvas.Clear、消しゴムは白系で描く実装。

プロジェクト設定:sizing=Responsive、テーマ AppTheme.Light.DarkActionBar、位置情報未使用、拡張/アセットなし、バージョン 1.0 (code 1)。

フルの技術レポート(コンポーネント階層、ボタン属性表、イベント/メソッド一覧、推定挙動、改善提案まで)を用意しました。
# DoodlePlus(.aia)完全解析レポート

このレポートは MIT App Inventor 2 プロジェクトの .aia を展開し、画面(.scm)とブロック(.bky)、プロジェクト設定(project.properties)を静的解析した内容です。

## 1) プロジェクト設定(youngandroidproject/project.properties)

```
#
#Sat Apr 12 01:36:28 UTC 2025
sizing=Responsive
color.primary.dark=&HFF303F9F
color.primary=&HFF3F51B5
color.accent=&HFFFF4081
aname=DoodlePlus
defaultfilescope=App
main=appinventor.*****.DoodlePlus.Screen1
source=../src
actionbar=True
useslocation=False
assets=../assets
build=../build
name=DoodlePlus
showlistsasjson=True
theme=AppTheme.Light.DarkActionBar
versioncode=1
versionname=1.0
```

主な項目:

- アプリ名 (aname / name): **DoodlePlus / お絵かきアプリ+**
- メイン画面: **Screen1**
- テーマ: **showlistsasjson=True**
- デフォルトファイルスコープ: **main=appinventor.****.DoodlePlus.Screen1**
- ActionBar: **True**
- 位置情報使用 (useslocation): **False**

## 2) 画面レイアウト(Screen1.scm)

- 画面タイトル: **お絵かきアプリ+**
- コンポーネント数: **12**

### コンポーネント階層

- `キャンバス1` : Canvas (親: None)
- `水平配置1` : HorizontalArrangement (親: None)
  - `黒` : Button (親: 水平配置1)
  - `青` : Button (親: 水平配置1)
  - `グリーン` : Button (親: 水平配置1)
  - `赤` : Button (親: 水平配置1)
- `水平配置2` : HorizontalArrangement (親: None)
  - `細` : Button (親: 水平配置2)
  - `中` : Button (親: 水平配置2)
- `水平配置3` : HorizontalArrangement (親: None)
  - `画面消去` : Button (親: 水平配置3)
  - `消しゴム` : Button (親: 水平配置3)

### カラーボタン・太さボタン等の属性

| 親配置 | ボタン名 | 表示テキスト | 背景色 | フォントサイズ | 幅 |
|---|---|---|---|---|---|
| 水平配置1 | 黒 |  | &HFF000000 | 10 | -1025 |
| 水平配置1 | 青 |  | &HFF0000FF | 10 | -1025 |
| 水平配置1 | グリーン |  | &HFF00FF00 | 10 | -1025 |
| 水平配置1 | 赤 |  | &HFFFF0000 | 10 | -1025 |
| 水平配置2 | 細 | 細 |  |  |  |
| 水平配置2 | 中 | 中 |  |  |  |
| 水平配置3 | 画面消去 | 画面消去 |  |  |  |
| 水平配置3 | 消しゴム | 消しゴム |  |  |  |

## 3) ブロック(Screen1.bky)

- 総ブロック数(node): **126**
### ブロック種別の出現数(上位)
- lexical_variable_get: 34
- component_component_block: 14
- component_set_get: 14
- global_declaration: 7
- procedures_callnoreturn: 7
- procedures_defnoreturn: 5
- logic_false: 4
- controls_if: 4
- logic_compare: 4
- lexical_variable_set: 4
- lists_create_with: 3
- color_black: 3
- component_event: 3
- text: 2
- math_number: 2

### イベントハンドラ
- Screen1 : Form.Initialize
- (未指定) : Button.Click
- キャンバス1 : Canvas.Dragged

### 定義済みプロシージャ
- 色ボタンリセット
- 色ボタン選択
- 太さボタンリセット
- 太さボタン選択
- 消しゴムボタン選択

(推定機能)
- **色ボタンリセット / 色ボタン選択**: 色選択UIの状態管理(選択記号の付与・他色のリセット、および `Canvas.PaintColor` の更新)
- **太さボタンリセット / 太さボタン選択**: ペンの太さUIの状態管理(`Canvas.LineWidth` の更新)
- **消しゴムボタン選択**: 消しゴムモード(背景色=白 or 透明、`LineWidth` 固定化等)への切替

### 画面初期化時の呼び出し(Screen1.Initialize)
- Call 色ボタン選択 with args: [('var', 'global 選択色ボタン')]
- Call 太さボタン選択 with args: [('var', 'global ペン太さ')]

### メソッド利用(コンポーネント操作)

- キャンバス1 (Canvas).**Clear**
- キャンバス1 (Canvas).**DrawLine**

※ 少なくとも `Canvas.DrawLine`, `Canvas.Clear` を使用しており、ドラッグで線分を描画し、[画面消去] でキャンバスをクリアする仕様です。

## 4) 振る舞いの要約(静的解析からの推定)

- 画面上部に **Canvas**、下部に3行の **HorizontalArrangement**(色選択、太さ選択、ユーティリティ)が並ぶ構成。
- 色ボタン(`黒` / `青` / `グリーン` / `赤`)で `Canvas.PaintColor` を切替。選択中ボタンにはチェック記号(例: ✔︎)を付ける実装。
- 太さボタン(`細` / `中` / (恐らく `太`))で `Canvas.LineWidth` を切替。
- `画面消去` ボタンで `Canvas.Clear`。
- `消しゴム` ボタンで描画色を背景色(白)相当にして消す振る舞い。
- `Canvas.Dragged` では `prevX, prevY` → `currentX, currentY` に対して **DrawLine** する一般的なお絵かきロジック。

## 5) 拡張機能・アセット・権限

- 拡張(.aix): **未使用**(拡張の痕跡なし)
- アセット(/assets): **未検出**(aiaに含まれない)
- パーミッション: project.properties 上は **位置情報未使用**。その他の特別権限の記載なし。

## 6) 改善できるポイント(提案)

- **ボタンのアクセシビリティ**: 色名テキストの有無に揺れがあるため、全ボタンで `Text` を明示し `ContentDescription` を付けると支援技術に優しいです。
- **消しゴムモードの視覚化**: 現状のチェック記号流用だけでなく、トグル表示やボタン色の反転などで状態を明確にできます。
- **キャンバスサイズ**: `Height/Width=-2`(Fill Parent)使用だが、端末回転時の計測タイミングに注意。必要なら `sizing=Responsive` に合わせて初回レイアウト後に線幅を再設定。
- **Undo/Redo**: 線分の履歴リストを持ち、`Canvas.Clear` 前にバックアップすれば Undo が実装可能。
- **保存/共有**: `Canvas.SaveAs` による画像保存、`Sharing` コンポーネントによる共有の追加を検討。

要件定義書作成

プロンプト

このアプリの要件定義書をMarkdownで書いて

返答

線の太さに「太」が追加されていますが、よしとしましょう。

# DoodlePlus アプリ要件定義書

## 1. 概要

DoodlePlus は、Android スマートフォン上で動作するシンプルなお絵かきアプリである。ユーザは指でキャンバスに線を描画し、色や線の太さを切り替えたり、消しゴムで描いた線を消去したりできる。さらに、キャンバス全体を一括でクリアする機能を提供する。

## 2. 対象ユーザー

- 幼児から大人まで幅広い年齢層
- 落書きやメモを素早く行いたいユーザー
- 学習教材やプレゼンの補助に使いたいユーザー

## 3. 利用目的

- 指先で自由に絵や文字を描く
- 色や太さを変えた線で描画
- 消しゴム機能で部分修正
- 全消去で新しいキャンバスにリセット

## 4. 機能要件

### 4.1 キャンバス機能
- 指でドラッグした座標間に線を描画
- 線の色、太さを設定可能
- 全体を消去する機能(Canvas.Clear)

### 4.2 色選択機能
- 黒 / 青 / 緑 / 赤 の4色を選択可能
- 選択中の色を視覚的に強調表示(チェック記号)

### 4.3 線の太さ選択機能
- 細 / 中 / 太 の3段階から選択可能
- 選択中の太さを視覚的に強調表示

### 4.4 消しゴム機能
- 描画色を背景色(白)に変更して消去
- 消しゴム使用中は太さ固定(中程度)

### 4.5 全消去機能
- ボタン操作でキャンバス全体をクリア

## 5. 非機能要件

- **操作性**: タップ/ドラッグで直感的に利用可能
- **応答速度**: 線描画・消去操作はリアルタイムで反映
- **アクセシビリティ**: ボタンにテキストラベルを付与し、視認性を確保
- **互換性**: Android 5.0 以上で動作

## 6. UI要件

- 上部: 描画用キャンバス(画面の大部分を占有)
- 下部: 3列の水平配置
  - 1行目: 色選択ボタン(黒/青/緑/赤)
  - 2行目: 太さ選択ボタン(細/中/太)
  - 3行目: [消しゴム] ボタン, [画面消去] ボタン

## 7. 将来的な拡張要件(提案)

- 描画の Undo / Redo 機能
- 画像保存および共有機能
- 消しゴムモードを視覚的に分かりやすくするトグル表示
- 色や太さのカスタム選択(カラーピッカー/スライダー導入)

## 8. 制約条件

- MIT App Inventor 2 で開発
- 外部拡張(.aix)は使用しない
- インターネットや位置情報の権限は不要

## 9. 品質保証

- 動作確認: Android 実機でのテスト
- 正常系テスト: 色/太さ選択、描画、消去操作が正しく動作すること
- 異常系テスト: 高速なタッチ操作や多指操作でも動作が破綻しないこと

SwiftUIプログラミング

プロンプト

この要件定義書に基づいてSwiftUIのコードを書いて。初心者にも何をしているかわかるように、また、App Inventorアプリとの対応関係がわかるように、詳しいコメントをつけて。

返答

了解!要件定義書の機能に対応する SwiftUI 実装を、App Inventor(AI2)のコンポーネント/ブロックとの対応関係が分かるように、丁寧なコメント付きでまとめました。
以下の単一ファイル(ContentView.swift)を Xcode の SwiftUI プロジェクトに追加すれば、そのまま動作します(iOS 15+ を想定)。

//
//  ContentView.swift
//
//  DoodlePlus (SwiftUI port)
//  - MIT App Inventor 2 版の「Canvas + 色選択 + 太さ選択 + 消しゴム + 全消去」をSwiftUIで再現
//  - 初心者向けに、できるだけ丁寧にコメントしています。
//  - App Inventor のコンポーネント/ブロック対応をコード中に明記。
//  - 透明消去(アルファ抜き)ではなく「白で上書きする」方式の消しゴムで、AI2の挙動に近づけています。
//    (※本格的な透明消去をしたい場合は Core Graphics のブレンドモード destinationOut などの応用が必要)
//

import SwiftUI

// MARK: - モデル定義(AI2で言うところの「変数」や「状態」)
//
// App Inventor 対応:
// - PaintColor(Canvasの描画色)→ selectedColor
// - LineWidth   (線の太さ)    → selectedWidth
// - 消しゴムモード               → isEraser
// - 描いた線の履歴               → strokes(各ストロークは points の配列)
//

/// 線1本(ストローク)を表すモデル
struct Stroke: Identifiable {
    let id = UUID()
    var points: [CGPoint]     // 線を構成する点列(指でなぞった座標)
    var color: Color          // 線の色(消しゴムなら Color.white)
    var lineWidth: CGFloat    // 線の太さ
}

/// アプリ全体の状態
final class DrawingState: ObservableObject {
    // 現在の選択色(AI2: Canvas.PaintColor)
    @Published var selectedColor: Color = .black
    // 現在の線の太さ(AI2: Canvas.LineWidth)
    @Published var selectedWidth: CGFloat = 6
    // 消しゴムモード(AI2: 「消しゴムボタン選択」プロシージャで true/false を切替)
    @Published var isEraser: Bool = false

    // これまでに描いた全ストローク(AI2: DrawLine を積み重ねるイメージ)
    @Published var strokes: [Stroke] = []
    // ドラッグ中の「現在のストローク」(指を離したら strokes に確定追加)
    @Published var currentStroke: Stroke? = nil

    // 画面全消去(AI2: Canvas.Clear)
    func clear() {
        strokes.removeAll()
        currentStroke = nil
    }
}

// MARK: - SwiftUI 版 Canvas(AI2: Canvas コンポーネントに相当)
//
// - SwiftUI の Canvas/Path を使って、strokes と currentStroke を描画します。
// - App Inventor の Canvas.DrawLine(block) 相当の処理は、points を連結して Path にしています。
//
struct DrawingCanvas: View {
    @ObservedObject var state: DrawingState

    var body: some View {
        GeometryReader { geo in
            ZStack {
                // 背景は白(AI2: Canvas の BackgroundColor に相当)
                Color.white
                    .ignoresSafeArea()

                // これまでに描いたストロークを全部描画
                ForEach(state.strokes) { stroke in
                    Path { path in
                        guard let first = stroke.points.first else { return }
                        path.move(to: first)
                        for p in stroke.points.dropFirst() {
                            path.addLine(to: p)
                        }
                    }
                    .stroke(stroke.color, lineWidth: stroke.lineWidth)
                }

                // ドラッグ中のストロークもプレビュー表示
                if let stroke = state.currentStroke {
                    Path { path in
                        guard let first = stroke.points.first else { return }
                        path.move(to: first)
                        for p in stroke.points.dropFirst() {
                            path.addLine(to: p)
                        }
                    }
                    .stroke(stroke.color, lineWidth: stroke.lineWidth)
                }
            }
            // ジェスチャ:指でなぞる → 点を追加 → 指を離したら確定
            // App Inventor 対応:
            //   - Canvas.Dragged(prevX, prevY, currentX, currentY) で DrawLine しているのと同等
            .gesture(
                DragGesture(minimumDistance: 0)
                    .onChanged { value in
                        // ドラッグが始まったら currentStroke を作成(色/太さは現在の選択)
                        if state.currentStroke == nil {
                            let color = state.isEraser ? Color.white : state.selectedColor
                            state.currentStroke = Stroke(points: [value.location], color: color, lineWidth: state.selectedWidth)
                        } else {
                            // すでに作成済みなら座標を追加
                            state.currentStroke?.points.append(value.location)
                        }
                    }
                    .onEnded { value in
                        // 指を離したら確定(strokes に追加)し、currentStroke をクリア
                        if var stroke = state.currentStroke {
                            stroke.points.append(value.location)
                            state.strokes.append(stroke)
                        }
                        state.currentStroke = nil
                    }
            )
        }
    }
}

// MARK: - メインビュー(AI2: Screen1 に相当)
//
// 画面構成(要件/UI要件に基づく)
//  - 上部:描画用 Canvas
//  - 下部:3つの水平行(HStack)
//      1行目:色選択(黒/青/緑/赤)→ PaintColor 切替
//      2行目:太さ選択(細/中/太)→ LineWidth 切替
//      3行目:消しゴムトグル / 画面消去 → isEraser 切替 / Canvas.Clear
//
struct ContentView: View {
    @StateObject private var state = DrawingState()

    // 太さの候補(AI2: 「細/中/太」)
    private let widthOptions: [(label: String, value: CGFloat)] = [
        ("細", 4),
        ("中", 8),
        ("太", 14)
    ]

    // 色の候補(AI2: 「黒/青/緑/赤」)
    private let colorOptions: [(label: String, value: Color)] = [
        ("黒", .black),
        ("青", .blue),
        ("緑", .green),
        ("赤", .red)
    ]

    var body: some View {
        VStack(spacing: 0) {
            // 1) Canvas 領域(要件: 画面の大部分)
            DrawingCanvas(state: state)
                .overlay(alignment: .topLeading) {
                    // シンプルなタイトル(AI2: Screen1.Title)
                    Text("DoodlePlus")
                        .font(.headline)
                        .padding(8)
                        .background(.ultraThinMaterial)
                        .cornerRadius(8)
                        .padding(8)
                        .accessibilityHidden(true)
                }

            Divider()

            // 2) 下部パネル:3段構成
            VStack(spacing: 8) {
                // --- 行1: 色選択(AI2: 色ボタン群 + 「色ボタン選択/リセット」プロシージャに対応)---
                controlRow(title: "色", content: {
                    HStack {
                        ForEach(colorOptions, id: \.label) { opt in
                            Button {
                                // 色選択:PaintColor を更新、かつ消しゴムモードは OFF
                                state.selectedColor = opt.value
                                state.isEraser = false
                            } label: {
                                HStack(spacing: 6) {
                                    Circle()
                                        .fill(opt.value)
                                        .frame(width: 18, height: 18)
                                    Text(opt.label)
                                    // 選択中マーク(AI2: チェック記号の代替として表示)
                                    if !state.isEraser && state.selectedColor.description == opt.value.description {
                                        Image(systemName: "checkmark")
                                    }
                                }
                                .padding(.horizontal, 10)
                                .padding(.vertical, 8)
                                .background(
                                    // 選択中は控えめに強調
                                    (!state.isEraser && state.selectedColor.description == opt.value.description) ?
                                        Color.secondary.opacity(0.15) : Color.clear
                                )
                                .cornerRadius(10)
                            }
                            .accessibilityLabel(Text("色 \(opt.label)"))
                        }
                    }
                })

                // --- 行2: 太さ選択(AI2: 太さボタン群 + 「太さボタン選択/リセット」プロシージャに対応)---
                controlRow(title: "太さ", content: {
                    HStack {
                        ForEach(widthOptions, id: \.label) { opt in
                            Button {
                                state.selectedWidth = opt.value
                            } label: {
                                HStack(spacing: 8) {
                                    // 太さプレビュー
                                    Capsule()
                                        .frame(width: 28, height: max(4, opt.value / 1.6))
                                    Text(opt.label)
                                    if state.selectedWidth == opt.value {
                                        Image(systemName: "checkmark")
                                    }
                                }
                                .padding(.horizontal, 10)
                                .padding(.vertical, 8)
                                .background(
                                    (state.selectedWidth == opt.value) ?
                                        Color.secondary.opacity(0.15) : Color.clear
                                )
                                .cornerRadius(10)
                            }
                            .accessibilityLabel(Text("太さ \(opt.label)"))
                        }
                    }
                })

                // --- 行3: ユーティリティ(AI2: 消しゴム / 画面消去ボタン)---
                controlRow(title: "操作", content: {
                    HStack {
                        // 消しゴム(AI2: 消しゴムボタン選択 → isEraser = true、UI強調)
                        Button {
                            state.isEraser.toggle()
                        } label: {
                            Label("消しゴム", systemImage: state.isEraser ? "eraser.fill" : "eraser")
                                .padding(.horizontal, 12)
                                .padding(.vertical, 10)
                                .background(state.isEraser ? Color.secondary.opacity(0.2) : Color.clear)
                                .cornerRadius(10)
                        }
                        .accessibilityLabel(Text("消しゴム"))
                        .accessibilityHint(Text("オンにすると白い線で上書きして消せます"))

                        Spacer(minLength: 12)

                        // 画面消去(AI2: Canvas.Clear)
                        Button(role: .destructive) {
                            state.clear()
                        } label: {
                            Label("画面消去", systemImage: "trash")
                                .padding(.horizontal, 12)
                                .padding(.vertical, 10)
                                .background(Color.red.opacity(0.12))
                                .cornerRadius(10)
                        }
                        .accessibilityLabel(Text("画面消去"))
                        .accessibilityHint(Text("キャンバスをすべてクリアします"))
                    }
                })
            }
            .padding(12)
            .background(.ultraThinMaterial)
        }
        .preferredColorScheme(.light) // 背景白に合わせた見え方重視(任意)
    }

    // 下部各行のラッパー(タイトル+内容)
    private func controlRow<Content: View>(title: String, @ViewBuilder content: () -> Content) -> some View {
        VStack(alignment: .leading, spacing: 6) {
            Text(title)
                .font(.subheadline).bold()
                .accessibilityHidden(true)
            content()
        }
    }
}

// MARK: - プレビュー
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .previewDevice("iPhone 15")
    }
}
App Inventor(AI2)との対応表(抜粋)
App Inventor	SwiftUI 実装	補足
Screen1	ContentView	画面のメインビュー
Canvas	DrawingCanvas(GeometryReader + Path/ZStack)	背景白・ストローク配列を描画
Canvas.Dragged	DragGesture.onChanged / onEnded	ドラッグ座標を currentStroke.points に追加・確定
Canvas.DrawLine	Path.addLine(to:)	点列から線を生成
Canvas.Clear	state.clear()	strokes.removeAll()
色ボタン(黒/青/緑/赤)	colorOptions + ボタン群	押下で selectedColor セット&消しゴム解除
太さボタン(細/中/太)	widthOptions + ボタン群	押下で selectedWidth セット
消しゴム	state.isEraser.toggle()	消しゴムON時は Color.white で上書き
選択中のチェック表示	Image(systemName:"checkmark")	状態に応じて表示
使い方メモ(初心者向け)

ボタンで 色 と 太さ を選んで、上の白いキャンバスを指でなぞると線が描けます。

消しゴム を押すと白い線で上書きでき、描いた線を消す感覚になります(背景が白のため)。

画面消去 でキャンバスをまっさらな状態に戻せます。

透明消去(背景が透ける消しゴム)にしたい場合は、Canvas レイヤーを CALayer / Core Graphics で管理し、ブレンドモード(destinationOut 等)を使う設計に変更すれば実現できます。

必要なら、この SwiftUI 版に Undo/Redo や 画像保存/共有 を追加した発展版コードもすぐ出せます。どう拡張したいか教えてください!

動作確認

iPhoneアプリを作るにはMacで動くXcodeという開発環境が必要です。ここではmacOS 13.7.5上でXcode 15.2を使い、動作確認にはiOSシミュレータ(iOS 15.2)を使いました。

公開済みAI関連アプリチュートリアル:シルバー会員特典

ゴールド会員特典はAIが中心

初心者にはこちらも

AI,API,Google Apps Script,nano banana,クラウド,中級,生成AI

このチュートリアルでは、Google Gemini APIを使って、撮影した顔写真からコスプレした全身写真を表示するアプリを作ります。

2025年8月にGoogleから公開されたNano Banana(Gemini 2.5 Flash Image)は写真を人物や被写体の特徴を保ったまま自然言語で自由に加工・変換できるAIです。生成AI表情変更アプリでは撮影した顔写真の表情を変更しましたが、もっと面白い応用としてコスプレさせてみました。

こちらがGoogleによるNano Bananaの解説です。

https://note.com/google_gemini/n/n1afcbf4d5275?rt=email&sub_rt=daily_report_followee_notes

生成AIコスプレカメラアプリのデモビデオをこちらからご覧ください。

ただ、Nano BananaはAPI経由では無料では使えませんので、課金方法の登録が必要です。Nano Bananaを使う場合はGoogle AI Studioから課金方法を登録してください。

Google Gemini APIの呼び出しにはGoogle Apps Script(GAS)を使います。GASのスクリプトはGoogle Gemini 2.5 Proで書いた生成AIお絵描きマジックアプリ用のスクリプトのプロンプトを書き換えています。

Google Apps Scriptの設定

Gemini APIの設定からウェブアプリとしてのデプロイ方法、使い方などは生成AIお絵描きコーチアプリを参照してください。

Code.gs – コスプレカメラスクリプト

/**
 * App InventorからのPOSTリクエストを処理するメイン関数
 * @param {Object} e - App Inventorから送信されるイベントオブジェクト
 * @returns {ContentService.TextOutput} - 処理結果(新しい画像のBase64文字列 or エラーメッセージ)をJSON形式で返す
 */
function doPost(e) {
  // レスポンスの基本構造
  let response = {
    status: 'error',
    message: 'An unknown error occurred.',
    imageData: null
  };

  try {
    // POSTされたJSONデータをパース
    if (!e || !e.postData || !e.postData.contents) {
      throw new Error('Invalid request: No data received.');
    }
    const jsonData = JSON.parse(e.postData.contents);
    const base64Data = jsonData.imageData;
    const characer = jsonData.characer;
    const prompt = jsonData.prompt || `全身写真にしてから顔自体は一切変更しないで、${characer}でコスプレして。言葉での返答は不要`;

    // 入力データの検証
    if (!base64Data || typeof base64Data !== 'string') {
      throw new Error('Invalid request: imageData is missing or not a string.');
    }

    // Base64データからヘッダーを削除
    const cleanedBase64 = cleanBase64String(base64Data);

    // スクリプトプロパティからAPIキーを取得
    const apiKey = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
    if (!apiKey) {
      throw new Error('Configuration error: API key is not set in Script Properties.');
    }

    // Gemini APIを呼び出して新しい画像を生成
    const generatedBase64 = callGeminiApi(apiKey, cleanedBase64, prompt);

    // 成功レスポンスを作成
    response = {
      status: 'success',
      message: 'Image generated successfully.',
      prompt: prompt,
      imageData: generatedBase64
    };

  } catch (error) {
    // エラーレスポンスを作成
    console.error(error.toString());
    response.message = error.message;
  }

  // 結果をJSON形式で返す
  return ContentService.createTextOutput(JSON.stringify(response))
    .setMimeType(ContentService.MimeType.JSON);
}

/**
 * Base64文字列からヘッダー部分(例: "data:image/png;base64,")を削除する
 * @param {string} base64String - 元のBase64文字列
 * @returns {string} - ヘッダーが削除されたBase64文字列
 */
function cleanBase64String(base64String) {
  // "data:[<mime-type>];base64," という形式のヘッダーを正規表現で探して削除
  return base64String.replace(/^data:image\/[a-z]+;base64,/, '');
}

/**
 * Gemini API (Image Generation) を呼び出して画像を生成する
 * @param {string} apiKey - Google AI StudioのAPIキー
 * @param {string} base64Image - 解析・改良の元となる画像のBase64文字列
 * @param {string} prompt - 画像をどのように改良するかを指定する指示文
 * @returns {string} - 生成された新しい画像のBase64文字列
 */
function callGeminiApi(apiKey, base64Image, prompt) {
  // Geminiの画像生成モデルのエンドポイント
  const model = 'gemini-2.5-flash-image-preview';
  const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;

  // APIに送信するペイロード(リクエストボディ)
  const payload = {
    contents: [{
      parts: [
        // 1. 指示テキスト
        { text: prompt },
        // 2. 入力画像
        {
          inlineData: {
            mimeType: 'image/png', // App InventorのCanvasはPNG形式
            data: base64Image
          }
        }
      ]
    }],
    generationConfig: {
      // レスポンスとして画像とテキストの両方を要求する
      responseModalities: ['IMAGE', 'TEXT'] 
    },
  };

  // APIリクエストのオプション
  const options = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload),
    muteHttpExceptions: true // HTTPエラー時に例外をスローさせず、レスポンスを直接受け取る
  };

  // APIにリクエストを送信
  const response = UrlFetchApp.fetch(url, options);
  const responseCode = response.getResponseCode();
  const responseBody = response.getContentText();

  // レスポンスを解析
  if (responseCode === 200) {
    const jsonResponse = JSON.parse(responseBody);
    
    // レスポンスの中から画像データ部分を探す
    const imagePart = jsonResponse?.candidates?.[0]?.content?.parts?.find(p => p.inlineData);
    
    if (imagePart && imagePart.inlineData.data) {
      // 画像のBase64データを返す
      return imagePart.inlineData.data;
    } else {
      // 画像が見つからない場合のエラー
      console.error('API response did not contain image data. Response: ' + responseBody);
      // テキスト部分があれば、それをエラーメッセージとして利用
      const textPart = jsonResponse?.candidates?.[0]?.content?.parts?.find(p => p.text);
      const errorMessage = textPart ? textPart.text : 'No image data in response.';
      throw new Error(`API Error: ${errorMessage}`);
    }
  } else {
    // APIがエラーを返した場合
    console.error('API Error. Code: ' + responseCode + '. Body: ' + responseBody);
    throw new Error(`API request failed with status code ${responseCode}. Details: ${responseBody}`);
  }
}

App Inventorアプリ

ページの最後にあるダウンロードセクションからソースコードをダウンロードできます。

[プロジェクト]メニューから[新規プロジェクトを始める]を選択し、"CosplayCamera"と名前を付けます。

デザイン編集

🔒 このページは以下の会員レベルで閲覧できます

残りのコンテンツを表示するにはログインする必要がありますどうぞ . 会員ではないですか ? 入会のご案内

AI,API,Google Apps Script,nano banana,クラウド,中級,生成AI

このチュートリアルでは、Google Gemini APIを使って撮影した顔写真の表情を変更して表示するアプリを作ります。

2025年8月にGoogleから公開されたNano Banana(Gemini 2.5 Flash Image)は写真を人物や被写体の特徴を保ったまま自然言語で自由に加工・変換できるAIです。たとえば、従来の gemini-2.0-flash-preview-image-generation ではうまく顔の表情の変換はできませんでしたが、Nano Bananaだと上手に変換できます。こちらがGoogleの解説です。

https://note.com/google_gemini/n/n1afcbf4d5275?rt=email&sub_rt=daily_report_followee_notes

生成AI表情変更アプリのデモビデオをこちらからご覧ください。

ただ、Nano BananaはAPI経由では無料では使えませんので、課金方法の登録が必要です。Nano Bananaを使う場合はGoogle AI Studioから課金方法を登録してください。

Google Gemini APIの呼び出しにはGoogle Apps Script(GAS)を使います。GASのスクリプトはGoogle Gemini 2.5 Proで書いた生成AIお絵描きマジックアプリ用のスクリプトのプロンプトを書き換えています。

Google Apps Scriptの設定

Gemini APIの設定からウェブアプリとしてのデプロイ方法、使い方などは生成AIお絵描きコーチアプリを参照してください。

Code.gs – 表情変更スクリプト

/**
 * App InventorからのPOSTリクエストを処理するメイン関数
 * @param {Object} e - App Inventorから送信されるイベントオブジェクト
 * @returns {ContentService.TextOutput} - 処理結果(新しい画像のBase64文字列 or エラーメッセージ)をJSON形式で返す
 */
function doPost(e) {
  // レスポンスの基本構造
  let response = {
    status: 'error',
    message: 'An unknown error occurred.',
    imageData: null
  };

  try {
    // POSTされたJSONデータをパース
    if (!e || !e.postData || !e.postData.contents) {
      throw new Error('Invalid request: No data received.');
    }
    const jsonData = JSON.parse(e.postData.contents);
    const base64Data = jsonData.imageData;
// expression には表情が入っている。「笑った顔」など
    const expression = jsonData.expression;
    const prompt = jsonData.prompt || `この写真が顔写真ならば${expression}に変換して画像を戻して。言葉での返答は不要だから画像を戻して`;

    // 入力データの検証
    if (!base64Data || typeof base64Data !== 'string') {
      throw new Error('Invalid request: imageData is missing or not a string.');
    }

    // Base64データからヘッダーを削除
    const cleanedBase64 = cleanBase64String(base64Data);

    // スクリプトプロパティからAPIキーを取得
    const apiKey = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
    if (!apiKey) {
      throw new Error('Configuration error: API key is not set in Script Properties.');
    }

    // Gemini APIを呼び出して新しい画像を生成
    const generatedBase64 = callGeminiApi(apiKey, cleanedBase64, prompt);

    // 成功レスポンスを作成
    response = {
      status: 'success',
      message: 'Image generated successfully.',
      prompt: prompt,
      imageData: generatedBase64
    };

  } catch (error) {
    // エラーレスポンスを作成
    console.error(error.toString());
    response.message = error.message;
  }

  // 結果をJSON形式で返す
  return ContentService.createTextOutput(JSON.stringify(response))
    .setMimeType(ContentService.MimeType.JSON);
}

/**
 * Base64文字列からヘッダー部分(例: "data:image/png;base64,")を削除する
 * @param {string} base64String - 元のBase64文字列
 * @returns {string} - ヘッダーが削除されたBase64文字列
 */
function cleanBase64String(base64String) {
  // "data:[<mime-type>];base64," という形式のヘッダーを正規表現で探して削除
  return base64String.replace(/^data:image\/[a-z]+;base64,/, '');
}

/**
 * Gemini API (Image Generation) を呼び出して画像を生成する
 * @param {string} apiKey - Google AI StudioのAPIキー
 * @param {string} base64Image - 解析・改良の元となる画像のBase64文字列
 * @param {string} prompt - 画像をどのように改良するかを指定する指示文
 * @returns {string} - 生成された新しい画像のBase64文字列
 */
function callGeminiApi(apiKey, base64Image, prompt) {
  // Geminiの画像生成モデルのエンドポイント
  const model = 'gemini-2.5-flash-image-preview';
  const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;

  // APIに送信するペイロード(リクエストボディ)
  const payload = {
    contents: [{
      parts: [
        // 1. 指示テキスト
        { text: prompt },
        // 2. 入力画像
        {
          inlineData: {
            mimeType: 'image/png', // App InventorのCanvasはPNG形式
            data: base64Image
          }
        }
      ]
    }],
    generationConfig: {
      // レスポンスとして画像とテキストの両方を要求する
      responseModalities: ['IMAGE', 'TEXT'] 
    },
  };

  // APIリクエストのオプション
  const options = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload),
    muteHttpExceptions: true // HTTPエラー時に例外をスローさせず、レスポンスを直接受け取る
  };

  // APIにリクエストを送信
  const response = UrlFetchApp.fetch(url, options);
  const responseCode = response.getResponseCode();
  const responseBody = response.getContentText();

  // レスポンスを解析
  if (responseCode === 200) {
    const jsonResponse = JSON.parse(responseBody);
    
    // レスポンスの中から画像データ部分を探す
    const imagePart = jsonResponse?.candidates?.[0]?.content?.parts?.find(p => p.inlineData);
    
    if (imagePart && imagePart.inlineData.data) {
      // 画像のBase64データを返す
      return imagePart.inlineData.data;
    } else {
      // 画像が見つからない場合のエラー
      console.error('API response did not contain image data. Response: ' + responseBody);
      // テキスト部分があれば、それをエラーメッセージとして利用
      const textPart = jsonResponse?.candidates?.[0]?.content?.parts?.find(p => p.text);
      const errorMessage = textPart ? textPart.text : 'No image data in response.';
      throw new Error(`API Error: ${errorMessage}`);
    }
  } else {
    // APIがエラーを返した場合
    console.error('API Error. Code: ' + responseCode + '. Body: ' + responseBody);
    throw new Error(`API request failed with status code ${responseCode}. Details: ${responseBody}`);
  }
}

App Inventorアプリ

ページの最後にあるダウンロードセクションからソースコードをダウンロードできます。

[プロジェクト]メニューから[新規プロジェクトを始める]を選択し、"FaceChanger"と名前を付けます。

デザイン編集

🔒 このページは以下の会員レベルで閲覧できます

残りのコンテンツを表示するにはログインする必要がありますどうぞ . 会員ではないですか ? 入会のご案内

AI,API,gemini,Google Apps Script,クラウド,中級,生成AI

このチュートリアルでは、Google Gemini APIを使って撮影した写真の解説をしてくれるアプリを作ります。

Google Gemini APIの呼び出しにはGoogle Apps Script(GAS)を使います。GASのスクリプトはGoogle Gemini 2.5 Proで書いた生成AIお絵描きコーチアプリ用のスクリプトのプロンプトを書き換えています。

Google AI Studio で作るGemini APIは課金方法を登録しなくても以下の範囲ならば無料で使えます。

  • 1 分あたりのリクエスト数: 5
  • 1 日あたりのリクエスト数: 250,000
  • 1 分あたりのトークン数(入力): 100

Google Apps Scriptの設定

Gemini APIの設定からウェブアプリとしてのデプロイ方法、使い方などは生成AIお絵描きコーチアプリを参照してください。

Code.gs – 写真解説スクリプト

/**
 * App InventorからのPOSTリクエストを処理する関数
 * @param {Object} e - App Inventorから送信されるイベントオブジェクト
 * @return {ContentService.TextOutput} - Gemini APIからの結果をJSON形式で返す
 */
function doPost(e) {
  // エラーハンドリング
  try {
    // 1. App Inventorから送信されたJSONデータを解析
    const postData = JSON.parse(e.postData.contents);
    const base64Image = postData.image; // App Inventor側で "image" というキーに設定

    // 2. スクリプトプロパティからAPIキーを読み込む
    const API_KEY = PropertiesService.getScriptProperties().getProperty('API_KEY');
    if (!API_KEY) {
      return createErrorResponse('APIキーが設定されていません。');
    }

    // 3. Gemini APIのエンドポイントURL
    const API_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent?key=${API_KEY}`;
  
    // 4. Gemini APIに送信するリクエストボディを作成
    const payload = {
      "contents": [{
        "parts": [{
          "text": "画像を解析してください。  まず、ランドマークや観光地が写っている場合は正式な地名を含めてください。  そうでない場合は人物を確認し、有名人(俳優・女優・著名人)であればその人物名を含めてください。  最後に、写真全体の内容を200文字以内で自然な文章として解説してください。  出力は必ず一つの段落の文章で返答してください。箇条書きは禁止です。"
        }, {
          "inline_data": {
            "mime_type": "image/png", // App InventorのCanvasはPNG形式
            "data": base64Image
          }
        }]
      }]
    };

    // 5. Gemini APIにリクエストを送信するための設定
    const options = {
      'method': 'post',
      'contentType': 'application/json',
      'payload': JSON.stringify(payload),
      'muteHttpExceptions': true // エラーレスポンスを例外ではなくオブジェクトとして取得
    };

    // 6. APIにリクエストを送信
    const response = UrlFetchApp.fetch(API_URL, options);
    const responseCode = response.getResponseCode();
    const responseBody = response.getContentText();

    if (responseCode !== 200) {
       return createErrorResponse(`APIリクエストエラー: ${responseCode} ${responseBody}`);
    }
    
    // 7. APIからのレスポンスを解析
    const resultJson = JSON.parse(responseBody);
    
    // レスポンスの構造をチェックし、テキスト部分を取得
    let generatedText = "解析結果を取得できませんでした。";
    if (resultJson.candidates && resultJson.candidates.length > 0 &&
        resultJson.candidates[0].content && resultJson.candidates[0].content.parts &&
        resultJson.candidates[0].content.parts.length > 0) {
      generatedText = resultJson.candidates[0].content.parts[0].text;
    } else {
       // Geminiからのレスポンスが期待した形式でない場合
       return createErrorResponse(`予期しないAPIレスポンス形式です: ${responseBody}`);
    }

    // 8. App Inventorに返すJSONを作成
    const output = JSON.stringify({
      "status": "success",
      "result": generatedText
    });

    // 9. 結果をJSON形式で返す
    return ContentService.createTextOutput(output).setMimeType(ContentService.MimeType.JSON);

  } catch (error) {
    // スクリプト全体で発生したエラーをキャッチ
    return createErrorResponse(`スクリプトエラー: ${error.toString()}`);
  }
}

/**
 * エラーレスポンスを生成するヘルパー関数
 * @param {string} message - エラーメッセージ
 * @return {ContentService.TextOutput} - エラー情報を格納したJSONオブジェクト
 */
function createErrorResponse(message) {
  const errorOutput = JSON.stringify({
    "status": "error",
    "message": message
  });
  return ContentService.createTextOutput(errorOutput).setMimeType(ContentService.MimeType.JSON);
}

App Inventorアプリ

ページの最後にあるダウンロードセクションからソースコードをダウンロードできます。

[プロジェクト]メニューから[新規プロジェクトを始める]を選択し、"KnowledgeCamera"と名前を付けます。

デザイン編集

🔒 このページは以下の会員レベルで閲覧できます

残りのコンテンツを表示するにはログインする必要がありますどうぞ . 会員ではないですか ? 入会のご案内

アプリ開発,ノート

考えたことメモ > App Inventor nb202bでiPhoneアプリを作ってみた

2025年8月25日にMITで公開されたApp Inventor nb202bでベータではありますが、iOSアプリのビルド機能が追加されたのでうまく動くか実験してみました。

日本語化プロジェクトのサーバーではまだiOSアプリのビルド機能はサポートしていません。

https://appinventor.mit.edu/ai2/ReleaseNotes

試したアプリは以下の通りです。

自分のApp InventorアプリをiPhoneでビルドしてみたい人は会員専用コミュニティフォーラム:ご意見・ご要望コーナーから連絡ください。

準備

App InventorでiOSアプリをビルドするにはプロビジョニングプロファイルファイルを作る必要があり、そのためには年額$99のApple Developer Programへの加入が必要です。私たちはApple Developer Programに加入済みなので、https://ai2.appinventor.mit.edu/reference/other/build-ios-apps.html に従ってプロビジョニングプロファイルファイルを作りました。

四則演算電卓アプリ

動作状況

  • ビルド自体は正常終了。
  • デザインがまるで違う。
  • ボタンが動作しない。

公開済みAI関連アプリチュートリアル:シルバー会員特典

ゴールド会員特典はAIが中心

初心者にはこちらも