まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

タグ:api

このネタを見た時は「やられた!」と思いました:
手持ちの画像を「Excel」ファイルにしてしまうWebアプリ「Image to Excel」が爆誕

画像をアップロードして、そのピクセル情報を解析して(よりによって)エクセルファイル化する・・・ 素晴らしい変態アプリだと思いました(褒めてます)。 これをアイデアだけでなく実装して公開する行動力まで含めて素晴らしい姿勢だと感じました。自分もこうでありたい。。

で、その衝撃が強すぎたのか、「これ自分だったらどう作るだろうか??」と考え、特にセルの属性を変更したエクセルファイルとして出力する所は何をどうやれば実現できそうか・・ なども調べた上で、なんとか同じような機能を実装することができたと思っています。自分の場合はアプリケーションというよりは REST API として実装したので、この機能を外部アプリからも使えるような形で仕上げています(Swagger ドキュメントも用意したのでウェブブラウザの UI からも使えるはずです)。実装したものは先日このブログでも紹介した無料の PaaS 環境を使って公開しています:

ソースコード
 https://github.com/dotnsf/image2xlsx
公開した REST API(の Swagger ドキュメント)
 https://image2xlsx.140.238.50.190.sslip.io/ (使い方も後述します)


で、この機能を作る上で xlsx-js-style という npm ライブラリを使っています。このライブラリは Node.js やフロントエンド JavaScript からエクセルファイルを生成する機能を持ったライブラリです。もともとは SheetJS というライブラリ(無料)があったのですが、有料版の提供を境にして無料版のサポートが少なくなってしまったのか、更新が途絶えてしまいました。そこで SheetJS からフォークして様々な後継ライブラリが作られていったのですが、xlsx-js-style はその1つです。ただ詳しい使い方がウェブ上でもあまり見つけることができず、一部では「事実上、読み取り用のライブラリ」とも書かれていました。実際はそんなことはなくて、現に今回紹介するようなエクセルブックファイルを新規作成する上での最低限の機能を持っていました。一方で情報が少ないことも事実で、今回はソースコードの中身を含めて自分でも色々調べながらの作業となりました。その調べて分かった内容も含めて本ブログエントリにまとめておこうと思います。

なお以下で紹介する内容は 2023/12/08 時点で公開しているサービスと、そのソースコードについて紹介しています。今後の更新で多少実装内容が変わる可能性もあることをご了承ください。


【image2xlsx (の Swagger ドキュメント)の使い方】
何はともあれ公開している機能の使い方を紹介します。作ったのは REST API なんですが、Swagger ドキュメントも併せて公開しているので curl などを使わなくてもウェブブラウザで実際の挙動を確認できるように(つまり手持ちの画像をアップロードして画像化したエクセルファイルをダウンロードすることができるように)しています。ウェブブラウザでこの URL にアクセスしてみてください:
https://image2xlsx.140.238.50.190.sslip.io/

2023120701


↑上のような画面が表示されれば成功です。https スキーマを使ってアクセスしているはずなので、実際に使う前に Schemas と書かれた欄(上図の赤枠部分)を "HTTP" から "HTTPS" に変更しておいてください(このサイトのサービスを使う場合、あらかじめ HTTPS に変更しておかないと正しく実行できません)。

この Swagger ドキュメントで提供している REST API は1つだけで、"POST /image" です。緑色の "POST" と書かれた部分をクリックして展開してみましょう:
2023120702


省略されていた部分が展開されて上のような画面になったはずです。この画面で "POST /image" API にはいくつかのパラメータが指定できることがわかります。が、まずは実際に API を実行してみたいので "Try it out" と書かれたボタンをクリックします:
2023120703


パラメータ部分が編集可能状態に切り替わり、一番下には "Execute" と書かれた青いボタンが現れたはずです。これでパラメータを指定して "Execute" ボタンをクリックすると実際に REST API が動く状態になりました。パラメータは4つありますが、入力必須なのは一番下の file フィールドだけです。他はいったん無視して file フィールドのボタンをクリックして適当な画像ファイル(.jpg, .gif, .png など)を指定してください(2023/12/08 時点では 10MB 以下の画像ファイルを受け付けることができます。もっと大きなファイルを扱えるようにしたい場合は是非ソースコードをダウンロードして自分で改良してみてください)。この際に日本語名のファイルを指定するとダウンロードするエクセルファイル名が文字化けしてしまうので、(ファイル名の文字化け程度を許容できる場合はそのままでもいいですが)できれば半角英数字名だけで構成されたファイル名の画像ファイルを指定してください:
hachi
(↑hachi.jpg というこの画像ファイルを指定してみました)


ファイルを選択したら "Execute" ボタンをクリックします:
2023120704


すると指定したファイルがアップロードされ、エクセルファイル化が実行されます(実行中は "LOADING" と表示されます):
2023120705


正しく処理が完了すると "Responses" 欄が現れます。ここには実際に実行した REST API の内容(curl コマンドで実行した時のコマンドの内容)やその実行結果のレスポンスに関する情報が表示されます。処理が成功していると "Download file" と書かれたリンクが表示されていて、ここをクリックすると実行結果のエクセルファイルをダウンロードできます:
2023120706


実際に "Download file" をクリックします。ダウンロードは一瞬のはずです。ちなみにエクセルファイル名は元の画像ファイル名の拡張子部分が "xlsx" になったものです(hachi.jpg → hachi.xlsx):
2023120707


ダウンロードしたファイルを(ダブルクリックするなどして)エクセルやエクセル互換ツールで開いてみると期待通りのエクセルシートが表示されるはずです(シート名は元の画像ファイル名です)。元祖の Image to Excel と同じような結果になっているはずです:
2023120708
(↑シートに画像が貼られているのではなく、細かなセルの色を変えることで実現しています)


なお今回はアップロードする画像以外のパラメータを無視していました。これらのパラメータの意味とデフォルト値について簡単に説明しておきます:
2023120709


px パラメータは「エクセルファイル側で画像の1ピクセルを何ピクセルのセルで表現するか」という数値です。デフォルト値は「2」です。つまり何も指定しなければ画像の各ピクセルは「横 2px 縦 2px」のサイズのセルとして表示されます。この値を変えたい場合は px パラメータになんらかの数値(1とか3あたり)を入力してください。

width パラメータは「エクセルファイル側で画像の横幅を何個ぶんのセルを使って表現するか」の数です。デフォルト値は「100」です。つまり何も指定しなければアップロードされた画像の横幅サイズを 100 に縮小(または拡大)します。

同様に height パラメータは「エクセルファイル側で画像の縦幅を何個ぶんのセルを使って表現するか」の数です。デフォルト値はなく、何も指定されなかった場合は縦横比を保ったまま横サイズ(こちらのデフォルト値は 100)に合わせて縦幅が拡大縮小されます。

なお height パラメータのみ指定されて width パラメータが指定されていなかった場合は、縦横比を保ったまま縦サイズ(=height)に合わせて横幅が拡大縮小されます。

このような仕様となっていました。なので、これらを何も指定せずに実行した場合は、
・画像の1ピクセルは 2x2 px サイズのセルで表示される(そのピクセルの色がセルの色になる)
・横幅は 100px に拡大縮小され、縦幅は同じ比率で拡大縮小される
ということになります。何も指定しなければ横幅は 100 になるので、巨大な画像をアップロードしても、そこまで巨大なエクセルファイルにはならないはずです。よかったら(必要に応じてパラメータも変えたりしながら)使ってみてください。


【image2xlsx REST API の使い方】
ここからは Swagger ドキュメントの使い方というよりは REST API の使い方を説明します。改めて実行結果の Responses 部分の、"Curl" と書かれた部分を見てみます:
2023120701


file 以外のパラメータを指定せずに実行した場合はこんな感じに書かれているはずです:
curl -X POST "https://image2xlsx.140.238.50.190.sslip.io/image" -H "accept: application/vnd.ms-excel" -H "Content-Type: multipart/form-data" -F "file=@hachi.png;type=image/png"

ちなみにパラメータを指定した場合(例えば width パラメータを 200 に指定して実行した場合)はこのようになります(-F "width=200" が追加されています):
curl -X POST "https://image2xlsx.140.238.50.190.sslip.io/image" -H "accept: application/vnd.ms-excel" -H "Content-Type: multipart/form-data" -F "width=200" -F "file=@hachi.png;type=image/png"

これがこの画像ファイルをエクセルファイル化する時に実行された curl コマンドそのものでもあります。REST API としては以下のような仕様です:

・エンドポイント URL は "https://image2xlsx.140.238.50.190.sslip.io/image"
・HTTP メソッドは "POST"
・HTTP リクエストヘッダは "accept: application/vnd.ms-excel" と "Content-Type: multipart/form-data"
・HTTP リクエストボディは以下を FormData で送信
 - px パラメータでエクセルファイルのセル1つのサイズを何ピクセル平方にするか
 - width パラメータでエクセルファイルのセルを何列で表現するか
 - height パラメータでエクセルファイルのセルを何行で表現するか
 - file パラメータで画像ファイル本体(ここだけ必須)

・HTTP レスポンスはエクセルファイルのバイナリデータ

(需要があるとは思えないのですが)ここを理解した上で外部アプリからこのエンドポイントへファイルをアップロードすればエクセルファイル化してダウンロードすることもできるようになります。REST API 化したので外部連携も可能な形になっています(サーバーがそこまで潤沢な環境ではないので、お手柔らかに使ってください)。


【xslx-js-style をどのように使ったか】
ここまでの内容は「本家の Image to Excel をパクってみました」という内容でした。ここからがこのブログエントリの本体ともいえる部分だと思っていて、「これをどうやって作ったのか」という技術的な解説になります。本家のサービスがどのように作られているのかは分からないのですが、自分はこうやってみた、という紹介です。ちなみにプログラミング言語は Node.js で作っています。

改めて、私が作った image2xlsx のソースコードはこちらで公開しています。実際の実装ソースコードはここからも参照できるので興味ある方は是非どうぞ。

この "POST /image" メソッドは概ね以下のようなフローで作られています:
(1)一時フォルダにアップロードされた画像ファイルを受け取る
(2)画像ファイルを開いて、サイズ(横幅、縦幅)を調べる
(3)受け取ったパラメータなどから出力時のサイズ(横幅、縦幅)を求める
(4)Canvas ライブラリを使って仮想的なキャンバスを作り、画像を出力時のサイズで描画する
(5)エクセルワークブック(ファイル)を新規に作成する
(6)エクセルワークシート(タブ)を新規に作成する
(7)(3)で求めたサイズに合わせて画像の各ピクセルぶんのセルをワークシート内に用意する
(8)(4)の画像データを取り出し、1ピクセルごとに RGB 値を取得し、
(9)(8)の結果を各セルの背景色に指定する
(10)セルの色を指定したワークシートをワークブックに追加する
(11)ワークブックのデータを取り出し、HTTP レスポンスとして返す

以下、このフローの実装内容を順に説明していきます。まず(1)ですが、ここは multer というファイルアップロード機能を作る時の定番ライブラリがあるので multer を使って実装しています。

該当コードとしてはこのような感じです:
var express = require( 'express' ),
    bodyParser = require( 'body-parser' ),
    fs = require( 'fs' ),
    multer = require( 'multer' ),
    { createCanvas, Image } = require( '@napi-rs/canvas' ),
    XLSX = require( 'xlsx-js-style' ),
    app = express();

require( 'dotenv' ).config();
var default_px = 'DEFAULT_PX' in process.env ? parseInt( process.env.DEFAULT_PX ) : 2;
var default_width = 'DEFAULT_WIDTH' in process.env ? parseInt( process.env.DEFAULT_WIDTH ) : 100;

app.use( express.static( __dirname + '/public/_doc' ) );
app.use( bodyParser.urlencoded( { extended: true } ) );
app.use( bodyParser.json( { limit: '10mb' } ) );
app.use( express.Router() );
app.use( multer( { dest: './tmp/' } ).single( 'file' ) );

  :

app.post( '/image', function( req, res ){
  var imgpath = req.file.path;
  var outputfilename = '';
  try{
    var imgtype = req.file.mimetype;
    var filename = req.file.originalname;

    :


(2)以降で仮想キャンバス機能を使います。フロントエンド JavaScript では HTML5 の Canvas に相当する機能なのですが、node-Canvas というバックエンド用の互換ライブラリ(のプラットフォーム非互換性を解消した @napi-rs/canvas というライブラリ)を使っています。この canvas でアップロードされた画像ファイルを読み込み、横幅と縦幅を取り出します。

(3)は、画像ファイルと一緒に送られてきたサイズに関するパラメータを使い(送られていない場合はデフォルト設定値を使い)、必要に応じて元画像の縦横比に合わせる形で計算も行って、エクセルファイル内のセルの横幅数と縦幅数を求めておきます。

この(2)と(3)に相当する部分のコードはこのようになっています:
    :
    //. 画像解析
    var data = fs.readFileSync( imgpath );
    var img = new Image;
    img.src = data;

    var width = req.body.width ? parseInt( req.body.width ) : -1;
    var height = req.body.height ? parseInt( req.body.height ) : -1;
    if( width < 0 && height < 0 ){
      width = default_width;  //. サイズの指定が何もない場合、幅100とする
      height = width * img.height / img.width;
    }else if( width < 0 ){
      width = height * img.width / img.height;
    }else if( height < 0 ){
      height = width * img.height / img.width;
    }

    :


(4)では仮想的な Canvas を作って、Canvas 内に元画像を元のサイズから出力時のサイズに拡大・縮小したものを表示(仮想的な Canvas なので実際に表示されるわけではないですが)しています。Canvas に描画されたデータはピクセルごとの情報(今回の場合であれば RGB 値)を取り出して調べることができるようになります。

この部分のコードはこのようにしています:
    :
    //. リサイズして仮想キャンバスに描画
    var canvas = createCanvas( width, height );
    var ctx = canvas.getContext( '2d' );
    ctx.drawImage( img, 0, 0, img.width, img.height, 0, 0, width, height );

    //. 描画した画像のデータを取得
    var imagedata = ctx.getImageData( 0, 0, width, height );
    :

ここまでは(アップロードされた画像を拡大縮小して、各ピクセル毎の情報を取り出すための準備までは) xlsx-js-style ライブラリを使わずに実装しています。ここからが xlsx-js-style ライブラリの出番です。

(5)この後、各ピクセル毎の情報を調べていくのですが、その前にワークブックを新規に作成します。「ワークブック」=「エクセルファイル」と考えると理解しやすいと思いますが、ここでの「新規作成」の意味は「新しいエクセルファイルを作って、まだ保存していない状態にする」と思っていてください。なおこの時点ではワークブック内に1つもタブが存在していない状態で作成されています。

xlsx-js-style でワークブックを新規作成するには utils.book_new() というメソッドを実行します:
    :
    //. ワークブックを作成
    var wb = XLSX.utils.book_new();
    :

(6)ではワークブックに追加することになるワークシート(タブ)を新規に1つ作成します。

xlsx-js-style でワークシートを新規作成するには utils.aoa_to_sheet() というメソッドを実行します。引数にワークシート内の各列の情報を含めることができますが、この時点では特に指定せずに作成しています:
    :
    //. ワークシートを作成
    var row = [[]];
    var ws = XLSX.utils.aoa_to_sheet([row]);
    :


(7)はワークシート内で使うことになるセルの列数と行数(つまり画像の横幅と縦幅)の分だけセルのサイズを調整します。

ここでの「セルのサイズ」とは「REST API 実行時に px パラメータとして指定された値(指定されていない場合は default_px の値(=2)」です。また「セルの列数」とは「Canvas 上に表示されている画像の横幅(=imagedata.width)」で、「セルの行数」とは「Canvas 上に表示されている画像の縦幅(=imagedata.height)」のことです。したがって「セルの列数の分だけセルのサイズを調整する」コードは以下のようになります。具体的には各列の幅を { wpx: (セル幅のピクセル数)} というオブジェクトで定義する配列を用意してワークシートオブジェクトの '!cols' 値として設定し、各行の幅は [ hpx: (セル高さのピクセル数) } というオブジェクトで定義する配列を用意してワークシートオブジェクトの '!rows' 値として設定します。具体的には以下のようなコードになります:
    :
    //. 必要なセルの行数と列数ぶんだけ、幅と高さを指定する
    var px = req.body.px ? parseInt( req.body.px ) : default_px;
    var wscols = [];
    for( var x = 0; x < imagedata.width; x ++ ){
      wscols.push( { wpx: px } );
    }
    var wsrows = [];
    for( var y = 0; y < imagedata.height; y ++ ){
      wsrows.push( { hpx: px } );
    }

    ws['!cols'] = wscols;
    ws['!rows'] = wsrows;
    :

(8)では Canvas 内の各ピクセルにアクセスして RGB 値をそれぞれ取り出し、16進法の "RRGGBB" フォーマットに変換して、

(9)その "RRGGBB" 値をワークシート内の対応するセルの背景色として指定してゆきます。

この(8)と(9)はループ内で以下のように実現しています。ピクセルの RGB 値は Canvas のデータ内に4つごとに(R 値、G 値、B 値、輝値)格納されているのでそれぞれを取り出し、この値を "RRGGBB" というフォーマットに変えて、"A1", "A2", ... といったセルの色として指定しています。また cellname という関数を用意して縦位置、横位置からセル名に変換できるようにしています(この後の(10)でも使っています):
    :
    //. 1ピクセルずつ取り出して色を調べる
    for( var y = 0; y < imagedata.height; y ++ ){
      for( var x = 0; x < imagedata.width; x ++ ){
        var index = ( y * imagedata.width + x ) * 4;
        var rr = imagedata.data[index];    //. 0 <= xx < 256
        var gg = imagedata.data[index+1];
        var bb = imagedata.data[index+2];
        var alpha = imagedata.data[index+3];

        var rrggbb = rgb2hex( rr, gg, bb );
        var cname = cellname( x, y );
        ws[cname] = { v: "", f: undefined, t: 's', s: { fill: { fgColor: { rgb: rrggbb } } } };
      }
    }
    :
    :

function color2hex( c ){
  var x = c.toString( 16 );
  if( x.length == 1 ){ x = '0' + x; }
  return x;
}

function rgb2hex( r, g, b ){
  return color2hex( r ) + color2hex( g ) + color2hex( b );
}

function cellname( x, y ){
  //. String.fromCharCode( 65 ) = 'A';
  var c = '';
  var r = ( y + 1 );

  while( x >= 0 ){
    var m = x % 26;
    x = Math.floor( x / 26 );

    c = String.fromCharCode( 65 + m ) + c;
    if( x == 0 ){
      x = -1;
    }else{
      x --;
    }
  }

  return ( c + r );
}

(10)(7)、(8)、(9)の処理によって変更されたワークシートの内容をワークブックに反映します。これでレスポンスとして返すエクセルファイル(のデータ)が完成しました。

このコードでは実際に使ったセルの範囲を "A1:(最も右下にあるセルの名前)"  という形でワークシートの '!ref' 値に指定してから、作成したワークシートをワークブックに追加する、という処理を実行しています:
    :
    //. 値が適用される範囲を指定
    var cname = cellname( imagedata.width - 1, imagedata.height - 1 );
    ws['!ref'] = 'A1:' + cname;

    //. ワークシートをワークブックに追加
    XLSX.utils.book_append_sheet( wb, ws, filename );
    :


(11)最後に完成したエクセルファイルを HTTP リクエストに対する HTTP レスポンスとして返します。

ここでは、これまでの処理で作成したワークブックのバッファデータを取得して HTTP レスポンスヘッダと合わせる形で返す、ということになるのですが、xlsx-js-style のソースコードを調べてもワークブックのバッファデータを直接取得する関数は用意されていないようでした。というわけで、一旦ワークブックファイルとして書きだし、ワークブックファイルを読み込んだ上で返す(そしてワークブックファイルをアップロード画像と一緒に消す)、というアルゴリズムにしています:
    :
    //. xlsx化
    XLSX.writeFileSync( wb, 'tmp/' + outputfilename );

    res.contentType( 'application/vnd.ms-excel' );
    res.setHeader( 'Content-Disposition', 'attachment; filename="' + outputfilename + '"' );
    var bin = fs.readFileSync( 'tmp/' + outputfilename );
    res.write( bin );
    res.end();
    :

・・と、このような形で xlsx-js-style を使ってエクセルファイルをバックエンドで作成することができました。グラフとかまでは難しそうではありますが、セルのサイズや色を指定したワークシートを作るという程度であれば実現できないことはなさそうで、今後エクセル連携が必要になった場合でも使えそうなライブラリでした。





IBM の新しい AI プラットフォームである IBM watsonx.ai (「ワトソンエックス・エーアイ」)が公開され、IBM Cloud のライトプラン(無料)でもある程度使うことができそうなことがわかったので、まだあまりドキュメントが整備されていなさそうな REST API について調べてみました。そして調べた結果わかった情報を使って、実際に REST API を使って動くサンプルアプリを作り、公開してみました。
watsonx.ai



【事前準備】
このブログエントリに書かれた内容を全て試すためには、IBM Cloud のアカウントが必要です。まだ所有していない場合はこちらから登録してください。なお 2023 年7月時点では新規登録の際にクレジットカード情報の入力が必須です(このブログで紹介する内容は無料範囲でも可能ですが、カードの登録は必須です)。

また今回はプロンプトを使った質問を入力して、その内容に対する回答を取得する、という内容の REST API を実際に動かすのですが、そのためには Watson StudioWatson Machine Learning という2つのサービスを利用する必要があります(いずれも従量課金型のサービスですが、ライトプランを使うことである程度まで無料で使うことができます)。 IBM Cloud のアカウントでログイン後にこれら2つのサービスインスタンスを作っておいてください。


【環境構築】
では実際にプロンプトで指示を行うための環境を構築します。IBM Cloud にログインし、リソースリスト(左メニューの上から2番目)から「AI/機械学習」カテゴリを参照します。他の用途で既に IBM Cloud を使っている場合はここにいくつかのサービスインスタンスが並んでいるかもしれませんが、少なくとも上述の事前準備が完了していれば "Watson Machine Learning" と "Watson Studio" の2つのインスタンスは表示されているはずです。これら2つが表示されていることを確認した上で(実際はどっちでもいいんですが) "Watson Machine Learning" の方をクリックします:
2023071700


クリックした "Watson Machine Learning" インスタンスが表示されます。ここで「IBM Cloud Pak for Data で起動」と書かれた青いボタンをクリックします:
2023071701


初回はこのような画面が表示されます。少し待ちます:
2023071701


少し待つとこのような画面が表示されます。「ML モデルの作成と管理」というダイアログが表示されていますが、実はこの時点では watsonx.ai ではなく IBM Cloud Pak for Data という別プラットフォームの画面になっています。プラットフォームを切り替えるため、この画面では「キャンセル」をクリックします:
2023071702


プラットフォームの切り替えは画面右上のメニューから行うことができます。画面右上から「IBM watsonx.ai」を選択し直します。これでプラットフォームが watsonx.ai 用のものに切り替わります:
2023071703


はじめて watsonx.ai を使う時にはこのような画面が表示されます。利用規約にチェックを入れ、また初回はまだプロジェクトがないので(先に進むためにはプロジェクトが最低1つ必要なので)「サンドボックスプロジェクトの作成」ボタンをクリックします(既に利用済みで、自分のプロジェクトが存在している場合はそのプロジェクトを選択しても構いません):
2023071704


プロジェクトが選択されていると、このような画面(プロジェクトの初期画面)になります。プロンプト指示を試す場合は、一番左の「ファウンデーション・モデルを・・」と書かれたプロンプト・ラボを選択します:
2023071705


プロンプト・ラボを始めて実行するとこのようなダイアログが表示されます。内容を確認して全てチェックします。説明を確認する場合は「ツアーを開始」をクリックしてもよいのですが、飛ばす場合は「ツアーのスキップ」をクリックします。これで環境構築は一通り完了です:
2023071706



【プロンプト実行】
ここまで正しく実行すると下のような画面になり、プロンプト指示を実際に試すことができるようになります。なお特に今回は REST API を操作することを目的としているので、プロンプトを試す前に画面右の「コードの表示」メニューをクリックしておきます:
2023071704


すると画面右側に curl の実行コマンドが表示されます。この後プロンプトで入力をすることになるのですが、その入力処理を curl で実行した場合の実行内容(接続 URL やヘッダ、データの中身)を確認することができるので、この内容を参考に REST API のプログラミングができるようになります:
2023071705


参照ページの例を参考にプロンプトで指示を出してみます。すると実行結果が返ってくるだけでなく、その結果を得るために実行された curl コマンドの REST API パスが /ml/v1-beta/generation/text?version=2023-05-29 であったことや、日本語で指示している場合も特に日本語であることを明示するパラメータが送られていないことを含め、どのような REST API が実行されていたかがわかります:
2023071706


【アクセストークンの取得】
後はこれと同じことを自分のプログラミングの中で実装すればよい、、のですが、この REST API を実行する上で欠くことのできない2つのパラメータがあります:
2023071707


1つは "project_id" というパラメータです。これは名前の通り「プロジェクトのID」で、プロンプト実行前に指定したプロジェクトを一意に示す ID です。この値はプロンプト実行時にブラウザが参照している URL を見ると、URL パラメータの1つとして指定されていることがわかります(つまりブラウザの URL から取得することができます):
2023071708


問題はもう1つの "YOUR_ACCESS_TOKEN" (つまりアクセストークン値)です。これはこの文字列をそのまま使っても正しく実行できません。しかもこの値はウェブブラウザを参照するなどの方法では取得できず、IAM API キーと呼ばれる値とプログラミングによって動的に取得する必要があるものです。この取得方法については本ブログの趣旨とは異なるので詳しくは解説しませんが、詳しくはこちらのドキュメントを参照してください(後述のサンプルでもこの方法でアクセストークンを取得しています)。


【サンプル】
ここまでに記載した情報を使って、実際に動く Node.js のサンプルアプリケーションを作って公開しました:
https://github.com/dotnsf/watsonx


サンプルといっても実体は "POST /api/generate_text" というエンドポイントを1つだけ実装した Swagger ドキュメントベースの API アプリケーションです。起動時に IAM API キーやプロジェクト ID を環境変数に指定することもできますし、API 実行時にパラメータで指定することもできます。

中身を簡単に説明すると、実装はほぼこの app.js ファイル1つだけで、アクセストークンの取得は getAccessToken() 関数で、テキスト生成(プロンプトの実行)は generateText() 関数で実装しています。興味ある方はこれらの関数内の実装部分を参考にしてください(といっても私も上の方法で知った curl コマンドとそのパラメータ指定をそのまま Node.js 内で使ってるだけなんですけど)。

サンプルアプリケーションを実行するには Node.js インストール済みの環境でソースコードを "git clone" して、"npm install" して、.env ファイルに API キーとプロジェクト ID を保存後に "npm start" するとアプリケーションが 8080 番ポートで起動するので、"http://localhost:8080/_doc" にアクセスすると Swagger ドキュメントが開きます:
2023071701


唯一の API である "POST /generate_text" 部分をクリックして開いて "Try it" ボタンをクリックするとパラメータ設定ができる画面になります。API Key や Project ID 、Model ID は環境変数で指定してあればここでは空のままで構いません。必須入力項目といえるのは Input 値くらいで、ここにプロンプトの内容を記載します。最後に "Execute" ボタンで実行します:
2023071702


正しく実行されると、API の実行結果が下部に表示されます。ちなみにこの例では Input が「入力:\nAbout Watson Discovery\\nIBM Watson® Discovery is an intelligent document processing engine that helps you to gain insights from complex business documents.\n翻訳\n」で、その結果が「Watson Discoveryはビジネスドキュメントに関する意見を得るための知能型ドキュメント処理エンジンです。」でした。どこにも「日本語サポート」とも書かれていないし、「日本語で翻訳」とも指定していないのにここまでできるのはそこそこ日本語でもプロンプトに書かれた意図を理解する力があると思っています。なお、このアプリケーションから実行する場合、 REST API 実行時のパラメータで max_new_tokens の値を(デフォルトの 20 から)100 に変更しています。日本語の場合、20 程度だとまともなある程度長い文章を返せなくなってしまうようで、このようにしています:
2023071703


ちなみにこのサービスを無料のライトプランで使う場合、1か月で使えるトークン数は 25000 だそうです。自分がこれまでにどのくらいのトークンを消費しているかは、IBM Cloud のプロジェクト選択画面で「管理」タブから「リソース使用率」を選択した先の画面で確認することができます。ご利用は計画的に:
2023071704


なお REST API のパス(/ml/v1-beta/generation/text)をみても分かると思いますが、現在の API は v1 のベータ版であり、近い将来に仕様含めて変更する可能性が高いと思っています。その辺りもご注意の上で参照してください。


【参照】
IBM watsonx.ai がやってきた
IBM watsonx.ai を試してみた ( コピペ OK )
Documentation ( IBM watsonx.ai 用)



ChatGPT で話題になっている OpenAI からは API や SDK が提供されており、これらを使うことで、自作アプリケーションに ChatGPT のような機能を組み込むことができるようになります。API 自体は無料登録でも(バージョン 3.5 以下を)利用することができますが、有料版にすることで(精度が話題になっている)バージョン 4.0 の API を使うこともできるようになる、というものです。

・・・というものなのですが、実際に ChatGTP と同様に問い合わせをする API を実行してみるとかなりの確率で 429 という HTTP ステータスコードのエラーが返されます。この 429 は "Too many requests" を意味するエラーコードです。特に無料登録の場合は利用回数に制限があったり、API 実行時のリソース割り当て優先度が低く設定されてしまい、「(他に使われていて)混み合っている時は実行できない」ということが頻繁に発生するのでした。

この問題に関しては OpenAI の記事でも取り上げられていて、「(API 実行時に)"exponential backoff" で実行するようにしてほしい」と紹介されています:
https://help.openai.com/en/articles/5955604-how-can-i-solve-429-too-many-requests-errors


↑の記事では(exponential backoff 関数が標準で使える) Python での簡単なサンプルが紹介されています。個人的には JavaScript で実装することが多いので JavaScript のサンプルを探してみました。OpenAI API ではない例で exponential backoff を JavaScript で実装している例は見つけることができた(↓)のですが、OpenAI API のサンプルは軽く探した限りでは見つかりませんでした:
https://advancedweb.hu/how-to-implement-an-exponential-backoff-retry-strategy-in-javascript/


・・・というわけで自分で作ってみました、というブログエントリです。Node.js での実装サンプルはこちらの GitHub で公開しています:
https://github.com/dotnsf/openai-api


【そもそも exponential backoff とは?】
"exponential" は「指数的」という意味で、「指数的に増加する段階ウェイトをかけながら(何かを)繰り返し実行する」ことです。
2023060800


今回の例だと(ChatGPT の)API を実行して 429 エラーになった場合、1秒待って再実行し、また 429 エラーになったら1秒待って再実行し、・・・ という方法ではありません。これだとウェイトは増加することなく、常に1秒待って再実行を繰り返しているだけです。

「指数的」という名前の通り、ウェイトに指数を使います。例えば「2の〇乗」という指数を使った場合で計算すると、まず API を実行して 429 エラーになった場合、まず(2の0乗=)1秒待って再実行し、また 429 エラーになった場合、次は(2の1乗=)2秒待って再実行し、また 429 エラーになった場合は次は(2の2乗=)4秒待って再実行し、また 429 エラーになったら(2の3乗=)8秒待って再実行し、・・・ といった具合にウェイト時間が指数関数で増えていくようなループ処理を行うことになります(上の GitHub 公開サンプルの場合、秒ではなくミリ秒の単位で計算しています)。これが exponential backoff です。

特定条件下で 429 エラーが高い確率で返される可能性が高いとわかっているような API を使う時の、ベストプラクティス的な実装方法のようです。


【実装例】
Node.js(JavaScript)での実装例を紹介します。例えば exponential backoff 実装前はこのような形で実行していました:
var { Configuration, OpenAIApi } = require( 'openai' );
var configuration = new Configuration({ apiKey: "(API KEY)", organization: "" });
var openai = new OpenAIApi( configuration );
  :
  :

var option = {
    model: "gpt-3.5-turbo",
    prompt: "海をテーマにしたおススメ映画を教えてください",
    max_tokens: 4000
};

try{
  var result = await openai.createCompletion( option );
}catch( err ){
}

SDK を使って単純に createCompletion メソッドを実行しています。これだと 429 エラーが発生した時にそのまま 429 エラーを返すことになります。

ここを exponential backoff を実装して作り変えたものがこちらです(青字部分が変更箇所です):
var { Configuration, OpenAIApi } = require( 'openai' );
var configuration = new Configuration({ apiKey: "(API KEY)", organization: "" });
var openai = new OpenAIApi( configuration );
  :
  :

const wait = ( ms ) => new Promise( ( res ) => setTimeout( res, ms ) );
const progressingOperation = async ( option ) => {
  await wait( 10 );
  try{
    var result = await openai.createCompletion( option );
    return {
      status: true,
      result: result
    };
  }catch( e ){
    return {
      status: false,
      result: e
    };
  }
}
const callWithProgress = async ( fn, option, maxdepth = 7, depth = 0 ) => {
  const result = await fn( option );

  // check completion
  if( result.status ){
    // finished
    return result.result;
  }else{
    if( depth > maxdepth ){
      throw result;
    }
    await wait( Math.pow( 2, depth ) * 10 );
	
    return callWithProgress( fn, option, maxdepth, depth + 1 );
  }
}

var option = {
    model: "gpt-3.5-turbo",
    prompt: "海をテーマにしたおススメ映画を教えてください",
    max_tokens: 4000
};

try{
  //var result = await openai.createCompletion( option );
  var result = await callWithProgress( progressingOperation, option, 5 ); 
}catch( err ){
}


この実装ではデフォルトで最大7回まで再実行する想定で、失敗した後に(2 の試行回数乗) * 10 ミリ秒待ってから再実行する、という形で実装しています。


・・・ただ、OpenAI の無料枠の混雑具合は相当なものらしく、この方法で API を実行しても結局 429 エラーとなるだけっぽい気がしています。10 回くらい試してもいいのかな?

マーメイド記法を勉強している中で生じたアイデアを実装したら、なんとなくノーコード/ローコードっぽい仕上がりになり、自分で使っていても便利に感じたので公開して紹介することにしました。


【マーメイド記法とは】
最初に「マーメイド記法」について紹介します。マーメイド記法とは「特定のルールでテキストを記述することでフローチャート図を作る」ための記述ルールです。マークダウン記法の一種ですが、実際にマークダウンの中に含める形で使うこともできるようなサービスもあります。有名どころでは今年のバレンタインデーに GitHub 内のマークダウンファイル(.md)内にマーメイド記法で記述された部分があると、フロー図に変換して表示されるようになりました。

例えばこんな内容を含むマークダウンファイルを GitHub にコミットします(1行目はマークダウンそのものですが、3行目の ``` (バッククォート3つ)から ``` までの部分がマーメイド記法で書かれています。後述しますが、もう少し情報量を増やした書き方もあります):
2202110601

(↑なんとなくわかりますかね? AからB、AからC、BからD、CからD、という4つの流れが定義されているものです。先頭行の "graph DB;" は「フロー図を上から下(TopDown) に描く」という意味です)

そしてコミットされたファイルをブラウザで GitHub から見ると、マーメイド記法で書かれた部分がこのようにフロー図に変換されて表示されます:
2202110602



このマーメイド記法はウェブアプリケーション設計時の「画面遷移の定義」の表現に使えると思いました。例えばこんな感じのウェブアプリケーションの画面遷移を考えた時に・・・
 ・最初にログイン画面
 ・ログイン後にトップ画面
 ・トップ画面からA、B、Cの3つの画面に遷移できる

マーメイド記法だとこのように表現できると思ったのです:
```mermaid
  graph TD;
  Login --> Top;
  Top --> A;
  Top --> B;
  Top --> C;
```

また上でも少し触れたのですが、このマーメイド記法はかなり単純な最小限の情報しか含んでいませんが、もう少し情報量を増やしてこのような書き方もできます:
```mermaid
  graph TD;
  Login["ログイン"] --"/"--> Top["トップ"];
  Top --"/info"--> A["商品情報"];
  Top --"/download"--> B["ダウンロード"];
  Top --"/about"--> C["○○について"];
```


このマーメイド記法をフロー化するとこのようになります:
2022110503


A や B といったノードの名前そのままではなく、["~~"] で括られた部分がノードの表示用の名称として表示されています(一度 Top["トップ"] と表示名を定義しておくことで、2回目からはただ Top と書くだけでよくなります)。加えて矢印にも名前が付けることで、なんとなく UI のないワイヤーフレームっぽくなりました。どのページからどのページへ、なんという URL パスで進むようになるか、というのが視覚化できます(上の図の「ログイン」ページに相当する URL パスがないのでまだ不完全ですが、詳しくは後述します)。


マーメイド記法はフロー図だけでなく、色々な種類のグラフを描くことができます。ただマーメイド記法そのものの書き方についてはこのブログエントリの目的ではないので、他のサイトでの紹介を参照いただきたいです。私自身はこのページに多くお世話になりました:
https://zenn.dev/okazuki/articles/learning-mermaid


【アイデアと、実装して作ったツール】
このマーメイド記法を使って作ったファイルを使って、以下のような3つの機能を作れないだろうか、というアイデアを思いつきました:
 (1)ワイヤーフレームの全画面を表示できるようなウェブアプリ化
 (2)ワイヤーフレームの中で DB 定義が必要と思われる部分を抜き出して API 化
 (3)(おまけ)マーメイド記法で書かれた部分をフロー図化

(1)は上で示したようなフローを実現できるような最小限のウェブアプリを、マーメイド記述されたファイルから、各ページの URL パスをそのまま再現する形で自動生成する、というものです。 各ページの UI は最小限ですが、例えば上の例であれば「ログイン」ページを表示すると「トップ」ページに遷移できるようなリンクがあり、「トップ」ページを表示すると「商品情報」、「ダウンロード」、「○○について」という3つのリンクがあって、それぞれのページに繋がっている、というものです。フロー定義から画面実装までを自動化して、生成された HTML テンプレートをカスタマイズすることで見た目も定義できるようにする、というものです。

(1)についてはもう1つ、(2)にも関わるのですが例えばマーメイド記述された中に "/(複数形)""/(単数形)/:id" というフォーマットに合致する2つのパスが含まれていた場合は、前者を一覧ページ、後者を詳細ページとみなして、それに近い UI を用意する、という機能を持たせました。例えば、マーメイド記述が
```mermaid
  graph TD;
  Login["ログイン"] --"/"--> Top["トップ"];
  Top --"/users"--> A["ユーザー一覧"];
  A --"/user/:id"--> B["ユーザー"];
    :
```

のようになっていた場合、単に「ユーザー一覧」画面と「ユーザー」画面がリンクでつながっているのではなく、「「ユーザー一覧」は「複数のユーザーの一覧画面」」で「「ユーザー」画面は「ユーザー ID が :id となる単数のユーザーの詳細画面」」であることを識別できるようにしました。自動生成される画面も単なるリンクで繋げるのではなく、「ユーザー一覧」画面にはユーザーが複数表示され、それぞれのユーザーがクリックできるようになっていて、クリックするとクリックしたユーザーの詳細ページに遷移する、というものです。

そして(2)の機能は、この「ユーザー一覧」画面と「ユーザー詳細」画面があるということは「ユーザーのデータを扱う必要がある」と判断し、ユーザー情報を CRUD できるような API (と Swagger ドキュメントと DDL)を自動生成する機能です。具体的には、
 ・POST /user (一人のユーザーを新規作成する)
 ・POST /users (複数のユーザーをバルクインサートする)
 ・GET /user/:id (一人のユーザーの情報を ID を指定して取得する)
 ・GET /users (複数のユーザーの情報をまとめて取得する)
 ・PUT /user/:id (ID を指定したユーザーの情報を更新する)
 ・DELETE /user/:id (ID を指定したユーザーを削除する)
 ・DELETE /users (複数のユーザーをまとめて削除する)
という7つの CRUD API を作る、というものです。加えてこれらの API のデータ格納先となるテーブルの DDL 定義(id と name の2列だけですが・・)と、実際に動作確認できる Swagger API ドキュメントも作成します。


最後におまけの(3)、これはマーメイド記法で表記した内容を GitHub にコミットしなくても見ることができるよう、ローカルでマーメイドファイルを表示できるようにするツールもあると便利だったので、自分用に作ってみました。

こうしてできあがったのがこのツールで、現在は Node.js 向けソースコードを GitHub で公開しています:
https://github.com/dotnsf/web-template


【ツールの使い方】
このツールを使う場合は、まずは Node.js の環境を用意する必要があります。必要に応じてインストールしてください:
https://nodejs.org/ja/


次に git でソースコードを clone して、"npm install" で動作に必要なライブラリをインストールします:
$ git clone https://github.com/dotnsf/web-template
$ cd web-template
$ npm install

そしてアプリケーションや API サーバーのソースコードを生成するためのマーメイドファイルを用意します。ソースコード内に mermaid_sample.md というファイルが含まれていますので、これをそのまま使っても構いませんし、自分で用意しても構いません。

ただ自分でマークダウンファイルを用意する場合は次の点に注意してください:
  • 各ノード(ページ)の表示名を定義する場合は ["~~"] という形で記述する(マーメイドのルールでは ("~~") など、丸括弧で括っても正しく判断されますが、このツールを使う場合は ["~~"] という四角括弧で表示名を括ってください。
  • 全てのノート(ページ)を表示するための URL パスが定義できているようにマーメイドファイルを用意してください。

例えば上で紹介したこのマーメイドファイル例:
```mermaid
  graph TD;
  Login["ログイン"] --"/"--> Top["トップ"];
  Top --"/info"--> A["商品情報"];
  Top --"/download"--> B["ダウンロード"];
  Top --"/about"--> C["○○について"];
```

は一番上にある Login["ログイン"] ページを表示するための URL がどこにも定義されていません(ので、このまま実行してもエラーになります)。マーメイドファイルのどこかに(例えば以下の例のように)このログインページへの URL パスを追加する必要があります(mermaid_sample.md ファイルは初めからこのようになっています):
```mermaid
  graph TD;
  Login["ログイン"] --"/"--> Top["トップ"];
  Top --"/info"--> A["商品情報"];
  Top --"/download"--> B["ダウンロード"];
  Top --"/about"--> C["○○について"];
  Top --"/login"--> Login;
```

最後にトップページからログインページに遷移するためのパス /login を定義する行を加えました。これで全てのノード(ページ)の URL パスが定義されたことになります。もし実際のアプリケーションではトップページからログインページへのリンクが不要であれば、生成されたトップページからログインページへのリンクボタンを手動で削除してください(具体的には以下で説明します)。

なお最初の状態のマーメイドサンプルファイル(mermaid_sample.md)の内容は以下のようになっています:
# mermaid

```mermaid
  graph TD;
      Login["ログイン"] --"/"--> Top["トップ"];
      Top --"/items"--> Items["アイテム一覧"];
      Items --"/item/:id"--> Item["アイテム詳細"];
      Top --"/users"--> Users["ユーザー一覧"];
      Users --"/user/:id"--> User["ユーザー詳細"];
      Top --"/about"--> About["サイト解説"];
      Top --"/login"--> Login;
```

「アイテム一覧(/items)」と「アイテム詳細(/item/:id)」、「ユーザー一覧(/users)」と「ユーザー詳細(/user/:id)」という組み合わせが定義されていることを覚えておいてください。

また、このフローをマーメイド対応のビューワで表示すると、このようになります:
2022110504


マーメイドファイルが用意できたら実際に動かしてみましょう。まず(1)のワイヤーフレームの全画面を表示できるようなウェブアプリを作ってみます。この場合はソースコード内の web.js を使って、以下のように実行します:
$ MERMAID=mermaid_sample.md node web

実行時に MERMAID という環境変数を指定しています。この環境変数にマーメイドファイル名を指定します(上の例では mermaid_sample.md ファイルを指定しています)。

マーメイドファイルに問題もなく、正しく実行された場合は一瞬で実行が完了し、web/ というフォルダに実行結果が生成されています。実際に生成された結果を確認するには web/ フォルダで "npm start" を実行します:
$ cd web
$ npm install
$ npm start

成功すると 8080 番ポートでウェブアプリケーションが待ち受けているので実際にアクセスして動作を確認してみましょう。まずログインページにアクセスするには(マーメイドファイル内で)/login へアクセスすることでログインページが表示されるよう定義していたので、ウェブブラウザを起動して "http://localhost:8080/login" にアクセスしてみます:
2022110505


本来であれば「ログイン」ページには ID とパスワードを入力するフィールドがあって、、というものだと思いますが、残念ながらそこまでは再現できていません。ただこのページには「トップ」と書かれたボタンがあり、このボタンをクリックすることで(マーメイドファイルの中で定義されていたように)トップページへ遷移するようにできています:
2022110506


トップページは↑のようになります。マーメイドファイルではトップページから「アイテム一覧(/items)」、「ユーザー一覧(/users)」、「サイト解説(/about)」、「ログイン(/login)」という4つのページへのリンクが定義されていたので、それらがそのまま4つのボタンになっています。正しく再現できています。

試しに「アイテム一覧」ボタンをクリックすると以下のようになります:
2022110507


先ほどまでのページとは少し違う画面になりました。これはアイテム一覧のページ(/items) がアイテム詳細(/item/:id) のページと繋がっていて、これらの URL パスのルール性から「個別のページ」ではなく「item の一覧」のページであると判定された結果です。その結果、表形式で item の一覧(id と名前だけですけど)が表示されています。そしてこれらのいずれかをクリックすると(例えば id = 0 の一番上のデータをクリックすると)/item/0 へ遷移し、詳細画面が表示されます(トップ画面からユーザー一覧ボタンをクリックした場合も同様の画面遷移となります):
2022110508

※(注意)
この「一覧」と「詳細」の両ページの存在を確認するには、マーメイドファイル内の URL パスで
- /(複数形)
- /(単数形)/:id
という2つの URL パスが定義されていることが条件なのですが、この「複数形」の表現には注意が必要です。

"user" の複数形は "users" で、"item" の複数形は "items" で何も問題はないのですが、例えば "category" の複数形は注意が必要です。英単語として正しくは "categories" となりますが、本ツールでは単に "s" を付けるだけの "categorys" と定義する必要があります。"information" も(正しくは複数形が存在しない英単語ですが、もし information の一覧ページを作りたいのであれば)"/informations" という URL パスで一覧ページを定義する必要がある、という点に注意してください。
※(注意ここまで)


一方、トップ画面から「サイト解説」や「ログイン」ボタンをクリックした場合は、遷移先が一般的なページ画面であると認識され、表形式ではなく(リンクがある場合は)ボタンが表示される画面となります(下図は「サイト解説」ボタンを押した場合):
2022110509


・・・と、このように web.js はマーメイドファイルを指定するだけで、その内容からここまでのページ遷移を実現するウェブアプリケーションが自動生成できました。実際に詳細なワイヤーフレーム画面を作りたい場合は生成されたフォルダの views サブフォルダや public サブフォルダに HTML テンプレートや CSS, JavaScript ができているので、これらをカスタマイズすることでデフォルトの生成画面を(HTML や CSS の知識だけで)カスタマイズできる、というものです。


次に同じマーメイドファイルから(2)の API を作ってみます。(1)でも紹介しましたが、今回使用するマーメイドファイルには「ユーザー(user)」と「アイテム(item)」という2つのデータを対象に一覧表示したり、詳細表示するページが含まれていました。ということは user と item の2つはデータベースに格納して読み書き更新削除する可能性があるデータ、という推測ができることになります。

この推測通りに動くかどうかを実際に使って確認してみます。元の web-template フォルダで今度は api.js を以下のように実行します:
$ MERMAID=mermaid_sample.md node api

この実行結果は api/ フォルダに出力されます。今度も(1)と同様に api/ フォルダに生成されたコードを実行してみます:
$ cd api
$ npm install
$ npm start

成功すると 8081 番ポートで API アプリケーションが待ち受けており、また /_doc/ という URL パスで Swagger ドキュメントが稼働しています。実際に Swagger にアクセスして動作を確認してみましょう。"http://localhost:8081/_doc" にアクセスしてみます:
2022110510

2022110511


↑のように、item および user の情報を CRUD できる API と、その Swagger ドキュメントが用意されました。データはメモリに格納するので再起動するまでの間であれば実際にデータを保存したり、変更したり、削除したり、読み出したりすることができます。マーメイドファイルからデータ操作が必要な要素を見つけ出して API 化する(ためのソースコードを出力する)、という機能が実現できていることがわかります。 プロトタイピング目的であればこれだけでもかなり使えるところまで設計を可視化することができると思っていますが、(1)で紹介した web.js でマーメイドファイルからページ遷移機能と、(2)で紹介した api.js で API サーバーを両方作った上で、一覧ページと詳細ページの部分だけ API 連携するようカスタマイズすれば、細かな見た目以外のかなりの部分を実現できるプロトタイピングがほぼノーコード/ローコードで実現できると感じています。


最後におまけの(3)について紹介します。(3)はカスタマイズしたマーメイドファイルが、想定していた通りの正しいフローになっているかどうかを確認するためのマーメイド・プレビューワーとしてのアプリケーションです。(1)・(2)同様に環境変数 MERMAID にマーメイドファイルを指定して、以下のように実行します:
$ MERMAID=mermaid_sample.md node preview

成功すると 8000 番ポートでプレビューワーのアプリケーションが待ち受けています。実際にアクセスして動作を確認してみましょう。"http://localhost:8000" にアクセスしてみます:
2022110512


マーメイドファイルのプレビューができました。これで正しい(自分の想定通りの)ワイヤーフレームがマーメイドファイルとして実現できているかどうかを(GitHub などにコミットしなくても)事前に確認することができます。おまけツールではありますが、(1)や(2)を実行する前に何度か使うことになる利用頻度の高いツールであるとも思っています。


【まとめ】
もともとはウェブアプリのデザインとワイヤーフレームを効率よく作るためのプロトタイピングツールを作ろうとしていたのですが、マーメイドファイルとの相性がよく、データ API まで作れるようになったことで、ノーコード/ローコードツールともいえるかな、という形になりました。実際にはここで紹介していないパラメータを使うことで CRUD API を本物のデータベースと接続して作ったり、UI 部分も(デフォルトでは Bootstrap ベースで作るのですが)Carbon や Material デザインにしたり、ということもできるようには作っています。 興味あるかたは公開されているソースコードや README を見て試してみてください。

久しぶりにウェブサービスを作ってみたので、紹介含めてブログにまとめました。アイデアそのものは2週間ほど前に思いついて脳内設計し、1週間くらい前から作ったり改良を加えたりして、この三連休でとりあえず公開できるレベルになったかな、という感じです。

作ったのはいわゆる CMS (=Contents Management System「コンテンツ管理システム」)です。特徴的な要素として GitHub Issues を使っている、という点が挙げられると思っています。要は CMS のコンテンツそのものは専用 DB などに格納するのではなく、GitHub のリポジトリ内のプロジェクト/課題管理ツールである GitHub Issues を使っています。多くのケースでは GitHub Issues はプロジェクトの課題を記録したり、課題をカテゴリーに分類したり、課題の担当者や解決目標時期を割り当てたり、スレッド形式のコメントを追加したりしながら、プロジェクト全体の進捗を管理することに使われることが多いと思いっています。一方で、この GitHub Issues の仕組み自体はプロジェクト管理だけに使うのはもったいないほど色んな機能が無料で提供されているとも考えることができます。今回作成した GHaC(GitHub as CMS)は GitHub API を使って GitHub のログインや GitHub Issues の取得を行い、CMS 寄りの UI で表示することで CMS として使ってみたら面白いのでは? という興味から生まれたものです。

なお、2022/10/10 の現時点では PC 向けの UI のみ提供しています。


【GHaC の使い方】
GHaC を使う場合はまずウェブブラウザで以下にアクセスします:
  https://ghac.me/

2022101001


GHaC の紹介と簡単な使い方が説明されています。以下、このページに書かれた内容と重複する箇所がありますが、こちらでも使い方を説明します。

GHaC は GitHub のリポジトリを参照して(そのリポジトリの Issues や Comments を取り出して)コンテンツを表示します。例えばサンプルとして dotnsf/sample-issues という公開リポジトリを見る場合を想定します。なお、このリポジトリはソースコードとしては何もコミットされていないので、リポジトリを直接参照すると初期状態のまま表示されます:

https://github.com/dotnsf/sample-issues の画面)
2022101002


ソースコードは空ですが、GitHub Issues としての情報はいくつかの Issues と、各 Issue にコメントがついていたり、いなかったりします:

https://github.com/dotnsf/sample-issues/issues の画面。4つの Open な Issues と、表示されていない1つの Closed Issue が登録されています。また Comments が付与されている Issues は右側に吹き出しマークと一緒に Comments の数が表示されています)
2022101003


GHaC でこのリポジトリを参照する場合は https://ghac.me/ に続けて対象リポジトリを追加した URL にアクセスします。このリポジトリ(dotnsf/sample-issues)の場合であれば https://ghac.me/dotnsf/sample-issues にアクセスします。以下のような画面になります:
2022101004


最初は認証されていない状態でアクセスすることになるので上記のようなログインを促すメッセージだけが表示されています。画面右上の「ログイン」ボタンで GitHub の OAuth 認証ページに移動し、GitHub アカウントを指定してログインします。初回のみ以下のような画面になるので右下緑の "Authorize dotnsf" ボタンをクリックしてください:
2022101005


ログインが成功すると以下のような画面に切り替わります。なお、このリポジトリの例(dotnsf/sample-issues)は公開リポジトリなので誰がログインしてもこのように表示されますが、非公開リポジトリの場合はログインしたユーザーの参照権限の可否によって表示されたり、されなかったりします:
2022101006


画面最上部右にはログインしたユーザーの(GitHub の)プロフィールアイコンが表示されます。このアイコンをクリックすると GitHub からログアウトして GHaC のトップページに戻ります。またその右には GitHub API を実行できる回数※の目安が円グラフで表示されています。このグラフ部分にマウスのカーソルを重ねると、リセットまでの残り API 実行回数と、次回リセットの日付時刻が表示されます:
2022101007



GitHub API は1時間で 5000 回実行できます。1つのリポジトリの Issues や Comments を取得するために実行する API の回数はリポジトリによって異なります。↑の例ではこのページを表示するために既に4回実行していて、2022/10/10 19:50:06 までにあと 4996 回実行できる、という内容が表示されています(2022/10/10 19:50:06 になると API 実行はリセットされて、新たに 5000 回利用できるようになります)。

また画面上部には指定したリポジトリ内の open な issues がテーブル形式で表示されています:
2022101008

(参考 もとのリポジトリの Issues の同じ該当部分)
2022101003


画面下部には各 Issues と、その Issue に Comments がついている場合は全ての Comments がスレッド形式で表示されています。画面をスクロールすれば全て見ることができますが、特定の Issue の内容を確認したい場合は上部テーブルの該当 Issue をクリックすると、その Issue のスレッドに移動します。

例えば「ラベル付き issue」(Comments 数は2)と書かれたこの部分をクリックすると、
2022101008


該当 Issue が表示されている箇所までスクロールして表示することができます:
2022101009


Issue 本文や Comments 内容はマークダウン記法で記述されており、GHaC でも同様に表示されます(mermaid 記法には対応していません)。マークダウンによるリッチテキストを含む Issues や Comments はリッチテキストが再現されて表示されます:
2022101010

(2022/10/13 追記)
mermaid 記法と MathJax 記法に対応しました。


また現時点では GHaC は参照専用のツールです。GitHub リポジトリ内の Issues や Comments を編集するにはこのリポジトリを編集する権限をもったユーザーでログインした後に、画面内の GitHub アイコン(オクトキャットアイコン)をクリックすると・・
2022101009


対象リポジトリの対象 Issue の GitHub 画面が別タブで表示されます。必要に応じて、この画面から Edit ボタンをクリックして内容を編集したり、Comments を追加したり、New issue ボタンから新しい Issue を追加してください(保存後に GHaC 画面をリロードすればすぐに反映されているはずです)。余談ですが GitHub Issues の編集画面はマークダウンで記述できるだけでなく、OS からのコピー&ペーストで画像を貼りつけることができるので、リッチテキストの編集がとても楽です:
2022101011


なお、GHaC はデフォルトでは指定されたリポジトリの Open な Issues だけを対象にこのような画面を提供します。全ての Issues やクローズされた Issues だけを対象としたい場合は URL の最後にそれぞれ state=all (全ての Issues)や state=close (クローズされた Issues)というパラメータを付けてアクセスすることで目的の Issues を変更することができます(実はこれ以外にも URL パラメータで対象 Issues を絞り込むことができるようにしていますが、GitHub API でのオプションと同じ仕様にしているので興味ある人は自分で調べてみてください):
2022101012

2022101013


【まとめ】
このように GitHub Issues をウェブコンテンツのように使うことができるようになるのが GHaC の魅力です。GitHub Issues 本来の使い方とは異なるので少し慣れが必要かもしれませんが、ある意味で GitHub を無料のデータ管理ができるヘッドレス CMS のように使うことができると思っています。

なおこの GHaC を使うことで、公開されている全ての GitHub リポジトリを対象にこのような UI で Issues や Comments を表示することができるようになります。ただ例えば node-red/node-red のような Issues が多く登録されているようなリポジトリに対して実行すると1回の表示に必要な API 実行回数も多くなり、結果として表示されるまでにかかる時間も長くなってしまいます。その点に注意の上でリポジトリを指定して実行してください:
2022101014


(表示まで1分近くかかりました・・)
2022101015


まだまだ不具合も見つかっており、今後はその対処が必要になることに加え、現在は Oracle Cloud の無料インスタンス1つを使って運用していることもあって決して潤沢なサーバー環境ではありません。いろいろ不便があるかもしれませんが、ある程度理解した上で(主に情報提供用の)コンテンツ公開サービスと考えると、アクセス権管理も含めて結構使い道あるサービスなのではないかと思っています。


しばらく今の形で公開するつもりなので是非いろいろ使っていただき、ご意見やご要望などあれば伺って今後のサービス向上に役立てていきたいと思っています。


このページのトップヘ