24.6.3 【補完テクニック】生成AIとの付き合い方

 生成AIが気軽に利用できるようになってきました。Buddyでアプリ作りをする際に、生成AIがどのように活用できるか、ヒントとなることや注意点をいくつか書いてみます。生成AIの技術はどんどん進歩しているので、あくまで現時点での話としてお読みください。

 無料で利用できる生成AIはたくさんありますが、ここでは登録などが不要ですぐに使えるものとしてマイクロソフトのCopilotというサービスを例に説明します。CopilotはWindowsに標準装備されているマイクロソフトのWebブラウザEdgeに搭載されています。Edgeの画面の右上にCopilotの青いアイコンがあり、これをクリックするとブラウザ画面の右側にCopilotの縦長のウィンドウが開きます。その下部に「何でも尋ねてください」とあるテキストボックスがありますので、ここに質問を入力することで、生成AIが答えを返してくれます。

○プログラミングの知識やサンプルを得る

 例えばJavascriptによるスクリプトで日付に関する処理をしたいとします。ある年月日から60日後の年月日を得たいとするとどう書けばいいでしょうか。
 生成AIがなかったときには、Googleなどの検索でそういった情報のあるWebページを探し回ることになりますが、生成AIは過去の多くのWebページを学習していて、そこからそれらしい答えを生成して返してくれます。Copilotに「Javascriptで、ある年月日から60日後の年月日を得たい」と聞いてみます。すると例えば次のような答えが返ってきます。

const date = new Date();
date.setDate(date.getDate() + 60);
console.log(`${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`);

このコードを実行すると、現在日付から60日後の日付が表示されます。例えば、今日が2024年5月16日(木)の場合、60日後は2024年7月15日(月)になります。

もし特定の日付から60日後を計算したい場合は、その日付をnew Date()で作成したDateオブジェクトにセットして同様の方法で計算できます。

 この答えでは現在日付から計算する例になっています。そこでさらに「特定の日付からの計算方法」と聞いてみます。(Copilotは対話型なので、さっきの質問を覚えていて、その続きだということをちゃんと認識します。この辺が生成AIの便利なところです。)すると、例えば次のような答えが返ってきます。

特定の日付から60日後を計算するには、その日付をnew Date()で作成したDateオブジェクトにセットして同様の方法で計算します。以下のサンプルコードを参考にしてください。

// 特定の日付を設定
const specificDate = new Date('2024-05-23'); // 例: 2024年5月23日
// 60日後の日付を計算
specificDate.setDate(specificDate.getDate() + 60);
// 結果を表示
console.log(`${specificDate.getFullYear()}年${specificDate.getMonth() + 1}月${specificDate.getDate()}日`);

このコードでは、specificDateに特定の日付を設定し、setDateメソッドを使って60日後の日付を計算しています。例えば、特定の日付が2024年5月23日の場合、60日後は2024年7月22日になります。

ご希望の日付を設定して、同様の方法で計算してみてくださいね!

 このようにして、サンプルコードとその解説も含めて、なかなか親切な回答をしてくれます。どんな質問でもそれなりに答える賢さがありますが、的外れな場合もあります。その時は上の例のように追加の質問をすることで求める答えに近づけていく辛抱強さは必要です。

 プログラミングでわからないことがあるときなかなか便利ですが、注意点もあります。
・サンプルコードが正しいとは限りません。そもそももとになっているWeb上の情報にも誤ったものもありますし、生成AIが推測によって結果をまとめる際にも誤った内容が生成される場合もあります。プログラムコードが正しく機能するかを検証するような機能は生成AIにはありません。経験豊富な人に尋ねて参考にするが、鵜呑みにはしない、という感じで付き合う必要があります。
・生成AIはBuddyのことはほとんど知りません。例えばBuddyの日時入力モジュールの使い方について質問しても、まともな答えは返ってこないでしょう。残念ながら現状ではBuddyアプリのスクリプトは世の中にほとんど流布していないので、生成AIの学習対象にもなっていません。使えるのはあくまでJavascript一般についての質問に限られます。

○アイコンを作る

 別の使い方として、アプリで必要となるアイコン画像づくりを試して見ましょう。Copilotに、「指定の日付からの指定日数後の日付を得るボタンのためのアイコンを作って下さい」と質問します。すると、文章だけで「カレンダーのアイコンをベースにします。カレンダーのページをめくるようなデザインが良いでしょう。」といったアイデアだけの答えが返ってきました。そこで次は、「実際のアイコンの画像を作って下さい」と質問します。すると、画像生成AIが使われて、生成された4つのサンプル画像が表示されました。ボタンに使うにはちょっとイメージが違ったので、さらに、「もっとシンプルなもの」と質問します。今度はボタンにも使えそうな画像が生成されました。


※生成された画像の例です。

 画像生成AIはそれ専門のサービスもいろいろありますが、上記のようにCopilotからでもある程度の画像生成が可能です。質問を重ねてもなかなか思うような画像は生成されなかったりもしますので、あまり過大な期待は禁物ですが、手軽に利用できます。

 このように、うまく使うと便利な生成AIを活用して、Buddyでのアプリ作りを加速してみてください。

(2024/06/03 中島)

23.6.14 【新機能】フレームワーク2

Buddyではアプリの構成要素のうちBuddy側で用意しているもの(例えばスクリーンモジュールなど)を総称してフレームワークと呼んでいます。フレームワークにユーザーが作成したデータベースやスクリーンなどの設計情報が組み合わされて、実際に動作するアプリとなります。2023年6月のアップデートで、フレームワークの新しいバージョンをリリースしました。従来のフレームワークをフレームワーク1、新しいフレームワークをフレームワーク2と呼んでいます。

フレームワーク2はフレームワーク1との互換性は考慮せずに一から設計し直して、より使いやすくなるようにしました。その基本的な考え方は、従来のフレームワーク1ではどうしても一定量のJavascriptのスクリプトを書く必要があった機能について、その必要をなくしたり、簡潔でわかりやすい書き方にすることです。具体的には次のような特徴があります。

・スクリーンスクリプトの書き方がシンプルになりました。Buddyが提供する様々な機能は全てbuddyというオブジェクトから利用できます。
・データベース操作などの非同期処理が最近の主流であるプロミスを使用した方式になり、簡潔でわかりやすい書き方ができるようになりました。
・データベースとモジュールの結びつきを扱うDataLinkerという仕組みが導入され、非常に簡潔に書けるようになりました。
・スクリーンテンプレートが一新され、作成後のスクリーンにモジュールの追加・削除を行った際にも、スクリプトはできるだけそのままで動作するようになりました。
・新しいスクリーンテンプレート「一覧」「閲覧・入力」が追加され、よりシンプルな画面構成が可能となりました。これを利用したアプリをExcelデータから簡単な操作で作成できるようになりました。
・スクリーンモジュールはすべてフレームワーク2用に一新されました。使用頻度の高い基本的なモジュールのみが標準モジュールとなり、それ以外はプラグインとして必要な時にリポジトリから取り込んで使用するようになりました。今後、特別な機能を持ったモジュールもプラグインとして提供し、作成できるアプリの幅を広げていく予定です。

詳しくは開発ガイドやプログラミングガイドなどのマニュアルに記載していますが、この記事ではプロミスによる非同期処理、DataLinker、プラグインについて簡単に紹介します。

○プロミスによる非同期処理

プロミスは非同期処理を扱うための比較的新しい仕組みです。従来はJavascriptでの非同期処理はコールバック方式で、フレームワーク1でもコールバック方式でした。フレームワーク2ではプロミス方式に変更されています。

例えばフレームワーク1でデータベーステーブルtable1からID1のレコードを読み出してnameカラムの値を表示するには次のようにします。(エラー処理については省略しています。)

const table = this.tables["table1"];
table.readData({where: {ID: 1}}, (function(error, data) {
	this.items.TEXTBOX1.setvalue(data[0].name);
}).bind(this));

フレームワーク2では次のようになります。

const table = buddy.app.findModel("table1");
const reulst = await table.select().where({ID: 1});
buddyscreen.items.TEXTBOX1.value = result.rows[0].name;

フレームワーク1ではreadData()のコールバック関数の中で、読み出した結果を得て、それを表示する処理をしています。それに対してフレームワーク2ではselect()はプロミスを返し、プロミスの処理終了を待ってその結果を得るキーワード「await」を利用することで、「result = await …」と結果を得ています。

上記の例のように単独の処理であればどちらの書き方でも大差ないように見えますが、いくつかの非同期処理を順次行いたい場合には大きな違いがあります。例えばtable1のnameカラムに「サンプルA」「サンプルB」「サンプルC」という値をこの順に入れるようにレコード追加したいとします。フレームワーク1では次のようになります。

const table = this.tables["table1"];
table.insertRecord({data: {name: "サンプルA"}}, (function(error, result){
	table.insertRecord({data: {name: "サンプルB"}}, (function(error, result){
		table.insertRecord({data: {name: "サンプルC"}}, (function(error, result){
		}).bind(this));
	}).bind(this));
}).bind(this));

コールバック関数の中で次のinsertRecord()を実行する必要があるので、このようにコールバックの入れ子になります。省略しているエラー処理もおこなうようにすると、とても複雑なスクリプトになります。

フレームワーク2では次のようになります。

const table = buddy.app.findModel("table1");
await table.insert({name: "サンプルA"});
await table.insert({name: "サンプルB"});
await table.insert({name: "サンプルC"});

awaitで処理終了まで待つことができるので、上記のように単純に書き並べることで順次処理ができ、非常にわかりやすいスクリプトになります。

○DataLinker

DataLinkerはスクリーンモジュールとデータベースとの仲立ちをしてくれる仕組みです。スクリーンに配置したテキストボックスなどのモジュールの「データリンク」という属性で、そのモジュールとどのデターベーステーブルやビューとそのカラムが対応するかを指定しておきます。すると、そのテキストボックスなどの値をデータベースから読み出した値にセットしたり、逆に入力された値をデータベースに保存したりする処理は、DataLinkerが面倒を見てくれます。

例えばデータベーステーブルtable1のID1のレコードを読み出して、その値をスクリーンモジュールの「データリンク」でtable1が指定されているものにセットするには、次のようにします。

const tableName = "table1";
const table = buddy.app.findModel(tableName);
const data = await table.select().where({ID: 1});
const linker = new buddy.lib.DataLinker(buddy, tableName);
linker.set(data);

逆にテキストボックスなどの入力用のスクリーンモジュールに入力された値を、データベーステーブルにレコードとして追加するには、次のようにします。

const tableName = "table1";
const linker = new buddy.lib.DataLinker(buddy, tableName);
await linker.insert();

「linker.insert()」だけで、スクリーンモジュールからの値の収集と、それをデータベーステーブルにinsert()する処理をまとめて行ってくれます。

○プラグイン

フレームワーク1では、テキストボックスやボタンなどの基本的なものから、地図やカレンダーなどの複合的なものまで、様々なスクリーンモジュールが用意されています。Buddyの応用範囲も広がるにつれて、Buddyのアップデートによって少しずつ新たなモジュールが加わったり、モジュールの機能が強化されたりしてきました。これにより、次第にモジュールが肥大化し、アプリが重くなる要因となる恐れが出てきました。

そこでフレームワーク2では、標準で用意するモジュールは基本的なものに絞り、使用頻度が低いと思われるものはプラグインとして必要なときにアプリに取り込む仕組みとしました。フレームワーク1にあったモジュールがプラグインに移ったものもありますが、新しく作られたプラグインモジュールもあります。今後さらに充実させて、Buddyでのアプリ制作の幅を広げていく予定です。

以上、かいつまんでフレームワーク2をご紹介しました。現時点ではベータバージョンであり、未実装の機能もありますが、今後完成度を高めていきます。ぜひ試していただければと思います。

(2023/6/14 中島)

22.9.13 【補完テクニック】画像をクライアント側で処理する

扱うデータに画像を含むようなアプリを作る場合、クライアント側にある画像ファイルあるいはスマホなどであればその場で撮影した画像をサーバーにアップロードして保存することになります。最近のスマホのカメラは大変解像度が高くなり、そのままだとサイズが大きすぎるので、縮小処理したい場合も多いと思います。

Buddyではサーバーに保存された画像ファイルを縮小処理する方法も用意されていますが、クライアント側で縮小してからサーバーにアップロードすることができれば、通信量が減るのでよりよい方法となります。

これを実現するには、
1)選択した画像の内容を読み取る。
2)画像を縮小する。
3)縮小した画像をアップロードする。
という処理をJavascriptでおこなう方法を知る必要があります。
具体絵的には、1)はFileReaderクラスの使い方、2)はcanvasの機能、3)は画像データをアップロードに必要なFileオブジェクトにする方法、そして共通するテクニックとしてデータURLの扱い、ということになります。これらをすべて学んで適切に組み立てるのは結構面倒です。そこで、必要な機能をまとめたライブラリファイルとしてimagefiletool.jsを作成しました。

imagefiletool.js です。← このファイル名部分をクリックしてダウンロードしてください。

imagefiletool.jsを、アプリのfiles/javascriptsフォルダに入れることによって、スクリーンのスクリプトでImageFileToolというクラスと、ImageUtilというオブジェクトが使えるようになります。これらには次の機能があります。

new ImageFileTool() で得たオブジェクトで次のメソッドを使う
  readFile() 画像ファイルのFileオブジェクトから読み取り、縮小して内部のキャンバスに描画
  getSrc() その画像を表示するsrc,width,heightを得る
  fileForUpload() その画像をアップロードするFileオブジェクトを得る
次のユーティリティもある。これはImageFileToolとは独立して使える。
  ImageUtil.getSize() 画像の幅と高さを得てコールバックで返す
  ImageUtil.calcShrinkSize() 幅と高さを指定の最大長辺値内になるように計算する

スクリーンで、ファイル選択モジュールFILE1、画像モジュールIMAGE1、ボタンBUTTON1、が配置されているとして、FILE1で画像を選択したら縮小処理するとともに画像をIMAGE1に表示し、BUTTON1をクリックしたらアップロードする、というスクリプトは次のようになります。

	// 画像ファイル選択時
	FILE1_onChange: function(evt){
		var files = this.items.FILE1.getFiles();
		if(files.length != 1) return;
		this.ift.readFile(files[0], (function(err){
			if(err) return console.log(err);
			var src = this.ift.getSrc();
			this.items.IMAGE1.setSrc(src.src);
			this.items.IMAGE1.setStyle({width: src.width, height: src.height});
		}).bind(this));
	},
	// ボタンクリック時
	BUTTON1_onClick: function(evt){
		var file = this.ift.fileForUpload();
		var dir = 'files/…';  // アップロード先のフォルダ
		api.request.upload(api.constants.appName, dir, [file], (function(error, result){
			// アップロード結果にもとづく処理
			…
		}).bind(this));
	},
	// スクリーンロード時
	onLoad: function(){
		// maxはアップロードする縮小画像の長辺サイズ、showmaxは表示する縮小画像の長辺サイズ
		this.ift = new ImageFileTool({max: 800, showmax: 150});
	},

このように、縮小処理とアップロードを簡単に行うことができます。

なお、カメラで撮影した画像を読み取った場合、右や左に回転した状態の画像となる場合があります。imagefiletool.jsではこれを補正する処理も行っています。興味のある方はソースコードをご覧ください。

(2022/9/13 中島)