ラベル プログラミング の投稿を表示しています。 すべての投稿を表示
ラベル プログラミング の投稿を表示しています。 すべての投稿を表示

2025年7月11日金曜日

ChatGPTがいつも正しいとは限らない

久しぶりにRailsでアプリを作っている.しばらく遠ざかっていたら,いつの間にかRailsのバージョンも8に上がっていてびっくりである.RailsでWebアプリを作ることをゴールにしているオンライン授業があるが,そろそろ動画コンテンツを作り直さないとマズいなあ.

それはそれとして,いろいろと改良されていて戸惑いも多い.やはり,常日頃から最新の状況をキャッチアップしていないとあかんなあ,というところだろうか.

今回,ブラウザのタブに表示されるアイコン,いわゆるfaviconを設定しようとして少し右往左往してしまった.ChatGPTにでも聞けばよかったのかもしれないが,いつものようにググって新しめの記述を探すと,適切なディレクトリにfaviconをおいてfavicon_link_tagをapplication.html.erbのヘッダ部分に設定すればよい,とある.

しかし,何度やってもfaviconがうまく表示されない.faviconを置く場所が悪いのか,タグのオプション指定をしなければならないのか,いろいろと試行錯誤してみても,一向に表示されないのでへこたれる.どうやっても赤丸のアイコンしか表示されない……赤丸?

赤い丸がなぜ表示されるんだろう?faviconの設定がないページは,Chromeだと地球のマークが表示されるはず.はてさて?ふと思い立ってpublicフォルダを見てみると,そのなかにicon.pngとicon.svgというファイルがあり,それらは赤い丸の画像イメージであった.こいつらか?

それらを入れ替えてみたのが次の図である.なんということはない,これだけでfaviconが表示された.試してはいないが,iPadやiPhoneでWebサイトをアイコン化したときにもこれらのアイコンが使われるようである.

今回のこの顛末,終わってみればなんとも拍子抜けな展開であった.それで,ついでにChatGPTに聞いてみたわけだが,彼もまた古い情報をしたり顔で(?)教えてくれたのであった(次図).皆さんAIがなんでも正しい情報を教えてくれると思ったら大間違いですぞ?

2024年10月20日日曜日

ゲーム・プログラミングの妙

プログラミングは楽しい.少なくとも,自分のために実施するプログラミングはいくら時間があっても足りないくらい.というわけで,今回はゲーム・プログラミングである.

題材はThe Chain Triangle Chess Gameというゲームである. Triggle Gameという名前でも流通しているらしい.Amazonで販売されていたゲーム盤のページから商品画像を引用する.

みてのとおり,ルールはとてもシンプルである.6角形の盤面に,37本の柱が立っている.プレイヤーは,交代でその柱を使って区切りを指定する.区切りの指定方法は,輪ゴムを4本の柱に引っ掛けて直線の区切りを設定するというものである.三角形の小さな区画の3辺が全て区切られたら,区切った人の領土になる(それを示すコマを置ける).

YouTubeのショート動画で楽しそうに遊んでいるのをみて,プログラミングの題材に選んだという次第.

環境の選択

最近であればPythonで書くべき?ゲームならUnityだろうか.まあ,慣れているからということと将来的に分散オンラインゲーム化することを見込んで,シンプルにHTMLとJavaScriptを用いて書くことにした.

GUIはHTMLのCanvasを利用する,Canvas要素に直に描いていく方法である.最も単純なパターンである.余計なライブラリに依存しないぶん,安定してメンテナンスできそうという目論見もある.

プログラミング

というわけで,1日くらいかけて実装した.ソースコードはGitHubに置いておいたので,興味がある人は参照してみてほしい.アプリは下記に置いてある.URLのパラメータを指定して2人〜4人の対戦に対応できるようにしてある.

プログラムを大まかに解説すると,GUI部品としてはWidgetクラスを継承したPole, Wall, Patchというクラスがそれぞれ,柱,壁,三角形の領域を表す.さらに,Containerクラスを継承したPoles,Walls,Patchesというクラスが,柱,壁,三角領域をまとめて管理するという構成になっている.

さらに,それぞれのインスタンスの関係を配列の配列で定義して,適切な参照関係を事前に用意してからゲームを始めている点も重要な点であろう.

遊び方

遊び方は,説明するまでもないのではなかろうか.まずは,どこでもよいから柱をクリックしてみよう.クリックした柱は黄色くなり,そこから直線上に3つ離れた場所にある柱が水色で表示される(次図).

水色で表示された柱以外は選択できない.なお,黄色の柱を再度クリックすると,選択を取り消せる.水色のどれかをクリックすると,最初に選択した黄色の柱と次にクリックした柱の間に区画が作られる.

3個の辺に区画ができた三角形は,その回を担当したプレイヤーの持ち物となる.色で示されるので分かりやすいだろう.全ての区画が誰かのものと定まった時点でゲームは終了,ページの最下部に得点状況が表示される(次図).

今後の課題

一通り実装できたので,今日のところはいったん完成としよう.しかし,まだいろいろと考えるべきところはある.細かな話をするとすでに壁ができているところに同じ壁を作るのを許容するかどうか.いまの実装では,その操作は許されているが,三角領域の獲得には何の寄与もしない.いわば「パス」を許していることになるが,ゲームとしてそれはよいのかどうか.検討が必要であろう.

オンライン対戦化させるのも興味深い.旧来の中央集権的サーバを用意して管理するもよし,WebRTCのような分散型の通信方法を利用するもよし,いろいろと工夫の余地はあろう.卒研生の題材にするってのも面白いかもしれないな.

2022年11月10日木曜日

hogeだのfugaだの

昨日,プログラミング基礎の授業中にほげほげ言っていたら学生たちがクスクス笑うので「なに,ほげだのふがだの珍しいか?」とメタ構文変数について解説した.皆さんもメタ構文変数という名前こそ馴染みがないかもしれないけれど,hoge, fuga, piyo, あるいは foo, bar, baz など,ITの皆さんであれば日常的にお使いなのではなかろうか.

次の図は,2011年に出版した拙書[1]からの引用である.これらの単語は,固苦しい名前がついており,メタ構文変数と呼ばれる.

まあ,こういうことにも名前がついているんだとか,foo, bar, baz だけでなくてひたすらいろいろあるんだとか,そういうトリビアを知っているのも悪くないだろう.

ところで,話は変わるがプログラミングの初学者に次の事実を突きつけるとみんなびっくりする.「コンピュータは正確な計算ができない」ってことは多くの人に知っておいてほしい事実なんだよなあ.

参考文献

  1. 飯尾淳 (2011) C言語によるスーパーLinuxプログラミング Cライブラリの活用と実装・開発テクニック, ソフトバンク クリエイティブ

2022年10月21日金曜日

Jupyter Notebookをリモートから使う方法(2022年改訂版)

 Jupyter Notebook(以下,Jupyter NB)は,本体がバックエンドで動いており,Webブラウザからアクセスして使う.ならばその本体に他のコンピュータからアクセスして使えるはずだ.とはいえ基本的にはローカル内で完結して利用することが前提となっているので,設定をいじる必要がある……ということで,そのやり方のメモ

サーバ側の設定(事前準備:anyenvのインストール)

対象とするサーバは,さくらインターネットでお借りしたVPSのUbuntuサーバである.ポートは9999番を使うので,ダッシュボードの「パケットフィルタ設定」で9999番は開けておくこと.

リモートログインした端末で,次の手順を行う(「$」の部分はプロンプトなので入力しない).

uname -a

Linux ids 5.15.0-52-generic #58-Ubuntu SMP Thu Oct 13 08:03:55 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux

まず,anyenvの設定をする.

git clone https://github.com/anyenv/anyenv ~/.anyenv

(略)
echo 'export PATH="$HOME/.anyenv/bin:$PATH"' >> ~/.bashrc

echo 'eval "$(anyenv init -)"' >> ~/.bashrc

exec $SHELL -l

(略)

anyenv install --init

これで,anyenvがインストールされる.次の手順で,anyenvのバージョンを確認しておこう.

anyenv --version

anyenv 1.1.5-1-g5c58783


サーバ側の設定(事前準備:pyenvとpythonのインストール)

次に,pyenvをインストールして,最新版のpythonをインストールしよう.まずは,pyenvをインストールする.以下では,コマンドの出力は必要がある場合以外は省略する.さらに,[y/n]などでyes or noを尋ねられたときは,基本的にyを入力すればよい.

$ anyenv install pyenv

$ exec $SHELL -l

これで,pyenvがインストールされる.同様に,動作確認を兼ねてバージョンを確認しておこう.

pyenv --version

pyenv 2.3.5-2-g03a5d653

さて,次はpythonのインストールである.2022年10月20日の時点で安定版の最新版は3.10.8なので,それをインストールする.が,いろいろとライブラリなどを事前にインストールしておかないとpythonのインストールがうまくいかないので,以下のとおり,必要なライブラリをインストールしておこう.

$ sudo apt install build-essential

$ sudo apt install libbz2-dev libncurses-dev libffi-dev \

                   libssl-dev sqlite3 libsqlite3-dev liblzma-dev

これだけインストールしておけばOKのはず.そのうえで,pythonをインストールする.

$ pyenv install 3.10.8

pyenv local 3.10.8

とりあえずこの作業ディレクトリ以下(ローカル)で使えるようにしておこう.

サーバ側の設定(jupyterの設定と起動)

さて,ここまでお膳立てが整ったら,あとはjupyterの設定をするだけである.まず,インストールから行う.

$ pip install jupyter

簡単にインストールできるだろう.

$ jupyter-notebook --generate-config

これで,設定ファイルが作成される.

ホームディレクトリに.jupyter/jupyter_notebook_config.pyが作成されるはず.

続いて,ipythonを立ち上げる.次の手順でパスワードを設定する(In [*]: はipythonのプロンプトなのでコピペしないこと).Enter password: と Verify password: では好きなパスワードを入力する.エコーバックされないのでパスワードを間違えないように入力すること.また,ここで表示される'sha1:……'は,入力したパスワードに対するハッシュ値なのでこの値を正確にどこかにコピーしておく(あるいは,この端末を開きっぱなしにしておくこと).ここで入力するパスワードは,リモートから接続する際に利用するので忘れないこと.

$ ipython

Python 3.10.8 (main, Oct 19 2022, 17:28:48) [GCC 11.2.0]

Type 'copyright', 'credits' or 'license' for more information

IPython 8.5.0 -- An enhanced Interactive Python. Type '?' for help.


In [1]: from notebook.auth import passwd


In [2]: passwd()

Enter password: 

Verify password: 

Out[2]: 'sha1:******************** ... ***'


In [3]: exit

.jupyter/jupyter_notebook_config.pyファイルの末尾に以下を追記する.

c.IPKernelApp.pylab = 'inline'

c.NotebookApp.ip = '0.0.0.0'

c.NotebookApp.open_browser = False

c.NotebookApp.port = 9999

c.NotebookApp.password = u'sha1:******************** ... ***'

ここで,最後の行は先ほどのハッシュ値をコピペする.以上で準備は終わり.

リモートからの接続

サーバの端末に戻り,Jupyter NBを起動する.

$ jupyter-notebook

手元のマシンからWebブラウザで接続する.ポート番号を9999に変更したので,http://サーバ名:9999/としてアクセスすること.

パスワード画面が現れるので,先ほど設定したパスワードを入れてログインする.

フォルダを辿っていくと,作業用ディレクトリにアクセスできる

2022年9月25日日曜日

シンプルなチャットアプリ

WebSocket を使ってたいへんシンプルなチャットアプリを作りました.サーバのコードはこんなにシンプル(エラー処理と切断処理は省略しています).

#!/usr/bin/env python

import asyncio
import websockets

clients = {}
async def echo(soc):
  async for msg in soc:
    if not (soc in clients):
      clients[soc] = msg
      print(f'{msg} is registered')
      for s in clients:
        await s.send(f’{clients[soc]} が参加しました')
    else:
      print(f'RECV: {msg}')
      for s in clients:
        await s.send(f'{clients[soc]}: {msg}')

async def main():
  async with websockets.serve(echo, port=8765, ping_timeout=None):
    await asyncio.Future()

asyncio.run(main())

クライアントのコードもこんなに簡単!まずは,HTMLのコード.

<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>A Simple BBS</title>
  <meta name="description" content="WebSockets TEST">
  <meta name="author" content="Jun IIO">
  <script src="jquery-3.6.1.min.js"></script>
  <script src="scripts.js"></script>
</head>
<body>
  <h1>シンプル・チャット・システム</h1>
  <div>
    名前:
    <input id="regname" type="text">
    <button id="register">登録</button>
  </div>
  <div>
    <textarea id="msgarea" cols=80 rows=10
              readonly disabled="disabled"></textarea>
  </div>
  <div>
    <input id="chatmsg" type="text" size=80 disabled="disabled">
    <button id="send" disabled="disabled">送信</button>
  </div>
</body>
</html>
JavaScriptのコードも,とってもシンプル.
$(function(){
  let socket = new WebSocket("ws://localhost:8765");

  $("#register").on("click", function(event) {
    name = $("#regname").val();
    if (name != "") {
      socket.send(name);
      $("#register").prop("disabled", true);
      $("#regname").prop("disabled", true);
      $("#msgarea").prop("disabled", false);
      $("#chatmsg").prop("disabled", false);
      $("#send").prop("disabled", false);
    };
  });

  socket.onmessage = function(event) {
    text = $("#msgarea").val();
    $("#msgarea").val(text + event.data + "\n");
  };

  $("#send").on("click", function(event) {
    msg = $("#chatmsg").val();
    if (msg != "") {
      socket.send(msg);
      $("#chatmsg").val("");
    };
  });
});

2022年7月7日木曜日

プログラミング本の難しさ

演習の準備をしていたら,教科書として使っている本に掲載されているプログラムのバグを発見してしまった.それがこちら.プログラムのとおり実行すると,こんなグラフが表示される.

おやおや?これ何さ.期待されている図は次のようなもの.インフォグラフィクスの事例である.

これ,扱うデータの問題で,書籍の説明が若干不十分だったことによる.筆者は「たまたまうまく動作するデータ」で検証してしまったのであろう.上の「なんだかよくわからない図」を,下の「まともな図」に直すにはどうすればよいか.実はコードの一行,一箇所だけ,手を加えればよいのだが,それも演習の課題にしてみよう.

環境を揃える難しさ

プログラミング指南本を執筆する際に,このような「環境を整える難しさ」を避けられない.とくに,バージョンが変わると仕様がコロコロ変わるような環境は要注意である.

私が過去執筆した某書籍,順を追ってアプリケーションを構築していくという建て付けになっており,当時の院生だったNさんも検証してくれて「これでエラーもなく完璧だ」と自身を持ってリリースした.ところが,すぐにプラットフォームのバージョンがアップデートされてしまい,また,ライブラリのバージョンもいろいろと変わって,書籍の説明どおりにやってもうまくいかないという状況になってしまった.

2022年6月24日金曜日

オンライン・プログラミング演習のその後

これまで何度か,オンラインでプログラミング演習を実施する方法について紹介してきた(末尾のリンクを参照されたい).学生の作業でトラブルが生じていないかどうか,教室では机間巡回して確認できるが,オンラインではそれができない.オンラインで学生の作業状況チェックを実現する方法として,OBSを使う方法や既存のサービスを使うもの,あるいは,Google Colaboratory(以下,Colab)のノートを共有して一覧表示させるために開発したMultiViewなど,いくつかのパターンを紹介した.

「オンライン・プログラミング演習がいよいよスタート」で紹介したように,Colabのノートを共有してMultiViewで閲覧しつつ,それぞれのプログラミングを指導するという方法でオンライン演習を10回ほど実施した.履修者でアンケートに応えてくれたのが11名と,サンプルサイズは小さいものの,一定の傾向は見られたので報告したい.

参加学生の評価

まず,Colabを用いたプログラミング演習について.「とても取り組みにくい」から「とても取り組みやすい」まで,5段階で評価してもらった.結果は次のグラフに示すとおりで,おおむね,取り組みやすいという感想であった.

続いて,MultiViewで皆の作業状況を共有することについて.こちらも「全く好ましくない」から「とても好ましい」までの5段階評価である.ひとり「全く好ましくない」に印をつけた学生がいたが,それ以外は好ましく感じてもらえているようである(次図).


そして,教室でやったほうがよいか,オンライン演習のほうがよいか.これは,なかなか難しい結果となった.ほぼ半数が「どちらでもよい」と答えており,2名が教室,4名がオンラインを希望という結果になった.キャンパスの立地条件と全学ゼミという性質から,どうしてもオンライン参加とせざるを得ないとの制約もあるため,やむなくオンライン参加という面もある.しかし,オンラインで気軽に参加できる良さと,教室で丁寧に学びたいというせめぎ合いもあるようだ.

ともあれ,手探りで始めたオンライン・プログラミング演習ではあれども,なんとか無事に離陸できたと安堵しているところである.

おまけ

本件は,2022年8月25日に開催される私情協の「ICT利用による教育改善研究発表会」で報告します.より詳しく知りたいかたは,そちらもどうぞ.

(参考)これまでの経緯

オンライン・プログラミング演習がいよいよスタート

オンライン演習までの(厳しい)道のり

2022年6月22日水曜日

我が愛しの習作たち

MAlibだのPirika'rだの真面目なプロダクトは仕事でいくつか作ってきた.今でもTWtrendsやOLiVES, Dialogbookなど,研究用途ではあるがそれなりの実用的なシステムを作っては提供している.ただし,それ以上に細々した「ちょっとしたアプリ」もちょこちょこと作ってきたので紹介したい.学生諸君は,学生時代にこういうのを作って少しでもバズれば,就職活動でも「あー,あれ作った人ね!」とアピールできるかもよ?

成分チェッカー

残念ながらこのアプリはいま動作していない.動かしていたサーバが撤去されてしまったからである.ジョークアプリではあるけれど,当時,それなりにバズった記憶がある.原理と詳細はTake IT Easy のコラム「メタ集合知に訊け!」で紹介しているのでそちらを参照されたい.


猫ちゃんの主張ジェネレータ,呪怨izer

文学部時代に手遊びとして作ったもの.いまでも社情実習室に置いてあるサーバで稼働しているが,そのうち撤去されるはずなので,どこかのタイミングでソースコードをサルベージにいかないといけないな.

どちらもしょーもない小品だが,まだ遊べるようなので,お暇ならどうぞ.

猫ちゃんの主張ジェネレータは下記からどうぞ.

http://sil.tamacc.chuo-u.ac.jp/iio/wp/voice_of_nuko

呪怨izer はこちらから.

http://sil.tamacc.chuo-u.ac.jp/iio/wp/juonizer

Make over a 100! Puzzle

ちょっとしたパズルゲームである.2018年1月に「『100を作れ』パズル」という記事で紹介されている.当時のサーバはもう既になく,記事中のリンクは切れているが,新たに下記においておいたので,そこにアクセスすれば遊べる.Have fun!

https://www.iiojun.com/Puzzle100/index_ja.html


Rubik's Cubeシミュレータ

そういえば最近はルービックキューブのシミュレータを作ったんだった.これは既に「ルービックキューブ・シミュレータ」で紹介済みだ.

2022年5月22日日曜日

Herokuアプリ,データのバックアップ方法

備忘録として,稼働中のHerokuアプリからローカルの開発版へデータを反映させる方法をメモしておく.

Heroku Postgres データベースからデータをエクスポートする.新しいバックアップを作成してダウンロードするという方法でできる.その手順は次のとおり.

$ heroku pg:backups:capture
$ heroku pg:backups:download

この操作をすると,ローカルに lateset.dump というダンプファイルができる.

このダンプファイルから,データをローカルの Postgres データベースにリストアする.その方法は次のとおり.

$ pg_restore --verbose --clean --no-acl --no-owner -h localhost -U myuser -d mydb latest.dump

ただし,ここで myuser はローカルで利用しているデータベースのユーザ名,mydb は,利用しているデータベース名である.Rails の開発環境ならば,application名_development というデータベース名のはず.

これで,稼働中のデータが開発環境に反映される.

(Heroku Dev Center どおりにやれば,問題なくできる)



2022年5月18日水曜日

ルービックキューブ・シミュレータ

ことの発端は,ねとらぼの記事だった.

ルービックキューブは「2次元」にすると分かりやすい……? 画期的な動画が「天才的発想」「すごいことをしていることだけ分かった」と話題

記事で紹介されているTwitterに投稿されたGIFアニメがとてもよくできている.ルービックキューブを操作する3次元の動きを2次元上の同心円にマップしたら,分かりやすいんじゃないか,というアイデアをアニメーションで具現化したものである.

たしかにとても分かりやすい.ん?分かりやすい?気がする.たしかにくるくる回す操作を円上の移動で表現すれば,何が行われているのか一目で把握することができる.

しかし,「すごいことをしていることだけが分かった」という感想は,しごくもっともなものである.たしかに,これを見ていると分かった気になるのだが,いかんせん,アニメーションの動画が速すぎて,何が行われているのかさっぱり分からない.

そこで,これ,自分で動かせれば分かりやすくなるんじゃね?と,思いたって作ったのが,この,ルービックキューブ・シミュレータである.https://www.iiojun.com/RubiksCube/ にアクセスすると遊べるので,ぜひ,試してみて.

右側の矢印ボタンで,ルービックキューブを操作できる.青のボタンが上の同心円,緑のボタンが左の同心円,赤のボタンが右の同心円に対応する.それぞれ,外側,真ん中,内側の円上のコマを操作できる.

右矢印で時計回り,左矢印で反時計回りである.回す方向を間違えないように.クイックハックで作ったので,アニメーションはしないが,注意深くみればきちんと操作できていることが分かるだろう(アニメーションさせるようにするのはさほど難しくなさそうなので,そのうち時間ができたら改造してみようかな).

さて,これで動作を確認すれば,ルービックキューブの仕組みが分かるだろう……と思ったものの,実際にやってみたら,これがなかなか難しい.ルービックキューブのパズルって,本当によく考えられたものだと改めて感心した.

こんな状態までぐちゃぐちゃになったら,とてもじゃないが自分では元に戻せない……

ちなみに,マジもんなシミュレータはこちらにある.私が作ったおもちゃのプログラムなんて泣いて逃げ出すレベルの凄さです.

2022年5月8日日曜日

感性評価システムの実装

学生が「映画ポスターの感性評価をしたい」というので,簡単な感性評価システムを作った.バックエンドは以下の手順で簡単に作成できる.

RailsアプリをAPIモードで作成

以下の手順でRailsアプリを作成する.なお,下記の赤字部分,生データを用意する手順は本記事では省略する.

$ bundle init

$ bundle config set path vendor/bundle

$ sed -i -e 's/# rails/rails/' Gemfile

$ bundle install

bundle exec rails new . -f --api

bin/rails g model poster title:string url:string

bin/rails g controller posters_controller index

bin/rails g model evaluation poster:references arousal:integer valence:integer

bin/rails g controller evaluations_controller create

bin/rails db:create

bin/rails db:migrate

(db/seeds.rb に映画データのシードを用意しておく)

$ bin/rails db:seed

次のように,コントローラはごく簡単でよい.APIモードのアプリなのでビューは作らない.なお,今回の例は同時に3つのポスターを評価するという前提である.

app/controllers/posters_controller.rb を次のように記述する.

class PostersController < ApplicationController

  def index

    @posters = Poster.all.sample(3)

    render json: @posters

  end

end

実に単純で,Posterテーブルに格納された全てのデータから3つのデータをランダムサンプリングして提示するだけである.APIモードで作っているので,クライアントにはJSON形式でレスポンスが返る.

続いて app/controllers/evaluations_controller.rb である.今回は実験用の単純なアプリなので,ストロングパラメータの取扱いなどは全て省略している.もう少し上手な書き方がありそうだが,とりあえずのquick hackである.

class EvaluationsController < ApplicationController

  def create

    arousal = [ params['arousal_0'], params['arousal_1'], params['arousal_2'] ]

    valence = [ params['valence_0'], params['valence_1'], params['valence_2'] ]

    posters = [ params['posters_0'], params['posters_1'], params['posters_2'] ]

    i = 0

    while i < arousal.length do

      e = Evaluation.create(arousal: arousal[i], valence: valence[i])

      p = Poster.find(posters[i].to_i)

      p.evaluations << e

      i += 1

    end

    redirect_to root_path

  end

end

app/models/poster.rb にも一行(has_manyの行)追加する.

class Poster < ApplicationRecord

  has_many :evaluations

end

ルーティング設定(config/routes.rb)は以下の通りとする.

Rails.application.routes.draw do

  root "posters#index"

  resources :evaluations, only: [ :create ]

end

以上でバックエンドの基本的な準備は終了である.これを,SSL対応で動かすとか,developmentモードのままで運用する際に手配しなければならない手順などについては,後述する.

フロント側のコード

フロントエンドは単純なHTML/CSS/JavaScriptによるシンプルなSPA(Single Page Application)である.バックエンドとはAJAXで通信する.

JQuery,JQuery-UI,Bootstrapを使用している.必要なライブラリは全て今回デプロイしたサーバにダウンロードして使っている.それぞれを利用時にCDNから直接ダウンロードするようにしてもよい.どちらでも構わない.

まずHTML(rp.html)である.

<!DOCTYPE html>

<html lang="ja" xml:lang="ja" xmlns="http://www.w3.org/1999/xhtml">

<head>

  <meta charset="utf-8" />

  <meta name="viewport" content="width=device-width, initial-scale=1" />

  <meta name="keywords" content="" />

  <meta name="description" content="" />

  <title>RandomPoster</title>

  <!-- for Bootstrap 5 -->

  <link rel="stylesheet" 

        href="bootstrap-5.0.2-dist/css/bootstrap.min.css">

  <script src="bootstrap-5.0.2-dist/js/bootstrap.bundle.min.js"></script>

  <!-- for jQuery & jQuery-UI -->

  <link rel="stylesheet" href="jquery-ui-1.13.1/jquery-ui.css">

  <script src="jquery-3.6.0.min.js"></script>

  <script src="jquery-ui-1.13.1/jquery-ui.js"></script>

  <!-- for this application -->

  <script src="rp.js"></script>

  <link rel="stylesheet" href="rp.css">

</head>

<body>

  <div class="container">

    <h1>Emotional Analysis on Movie Posters</h1>

    <div class="row mb-3">

      <div class="col-4">

        <img class="col-12" id="image-0" src="poster.gif">

        <div class="row">

          <div class="col-4 text-left my-2">High ←</div>

          <div class="col-4 text-center my-2">覚醒度</div>

          <div class="col-4 text-end my-2">→ Low</div>

        </div>

        <div id="slider-a0"></div>

        <div class="row">

          <div class="col-4 text-left my-2">Positive ←</div>

          <div class="col-4 text-center my-2">感情価</div>

          <div class="col-4 text-end my-2">→ Negative</div>

        </div>

        <div id="slider-v0"></div>

      </div>


      <div class="col-4">

        <img class="col-12" id="image-1" src="poster.gif">

        <div class="row">

          <div class="col-4 text-left my-2">High ←</div>

          <div class="col-4 text-center my-2">覚醒度</div>

          <div class="col-4 text-end my-2">→ Low</div>

        </div>

        <div id="slider-a1"></div>

        <div class="row">

          <div class="col-4 text-left my-2">Positive ←</div>

          <div class="col-4 text-center my-2">感情価</div>

          <div class="col-4 text-end my-2">→ Negative</div>

        </div>

        <div id="slider-v1"></div>

      </div>

      <div class="col-4">

        <img class="col-12" id="image-2" src="poster.gif">

        <div class="row">

          <div class="col-4 text-left my-2">High ←</div>

          <div class="col-4 text-center my-2">覚醒度</div>

          <div class="col-4 text-end my-2">→ Low</div>

        </div>

        <div id="slider-a2"></div>

        <div class="row">

          <div class="col-4 text-left my-2">Positive ←</div>

          <div class="col-4 text-center my-2">感情価</div>

          <div class="col-4 text-end my-2">→ Negative</div>

        </div>

        <div id="slider-v2"></div>

      </div>

    </div>


    <button id="submit" class="btn btn-primary">送信</button>

  </div>

</body>

</html>

idの命名規則を少し工夫した.<img>タグによるポスターの表示,その下に覚醒度(arousal)と感情価(valence)を操作するスライダーUIが表示される<div>タグ部分,それぞれを,image-{#}, slider-a{#}, slider-v{#} と番号付きで設定している箇所がポイントである.このようにすることによって,JavaScript側からそれぞれをモジュールとして統一的に扱えるようにしている.詳しくはJavaScriptのプログラムと付き合わせてみてほしい.

<img>タグ部分は,AJAXでサーバ側から提供されるランダム提示の映画ポスターが配置される.非同期で通信が行われるため,通信待ちの間,表示されるプレースホルダとして"poster.gif"というアニメーション画像を用意した.このアニメーション画像で「Now loading……」というアニメーションを表示する.

JavaScriptのコード(rp.js)は次のとおりである.コード内の「(サーバ名)」部分は,運用するサーバのドメイン名を入れる.また,サーバとはSSLでAJAX通信をする前提であり,ポート番号は3443にしているが,これも事情に合わせて変更して構わない.

$( function() { // this function runs when the page is loaded


  // evaluation records

  var ers = [];


  // definition of the initialization function

  var init = function(data) {

    ers.length = 0; // ers <- [];

    data.forEach( (item, i) => {

      ers.push({ posters: item["id"], arousal: 5, valence: 5 });

      $( "#image-"+i ).attr("src", item["url"]);

      $( "#slider-a"+i ).slider({ min: 1, max: 9, step: 1, value: 5,

        slide: function(event, ui) { ers[i].arousal = ui.value; } });

      $( "#slider-v"+i ).slider({ min: 1, max: 9, step: 1, value: 5,

        slide: function(event, ui) { ers[i].valence = ui.value; } });

    });

  }


  // function definition callbacked when the submit button pressed

  $("#submit").click(function() {

    //convert 'ers' to 'params' for the server

    var params = {};

    ers.forEach( (item, i) => {

      params["posters_"+i] = item.posters;

      params["arousal_"+i] = item.arousal;

      params["valence_"+i] = item.valence;


      // set loading images

      $( "#image-"+i ).attr("src", "poster.gif");

    });


    // post the user's evaluation data to the server

    $.ajax({

      url: "http://(サーバ名):3443/evaluations",

      type: "POST", dataType: "json", data: params

    }).done(function(data) {

      // re-initialization must be performed after submit an evaluation

      init(data);

    }).fail(function(XMLHttpRequest, textStatus, errorThrown) {});

  });


  // initialization must be conducted at the end of resource loading...

  $.ajax("http://(サーバ名):3443", { dataType: "json" })

   .done(function(data) {

     init(data);

  });

});

このプログラムは,初期化する関数 init() と,送信ボタンをクリックしてコールバックする無名関数による実装から構成されている.

スライダーを操作した値は,配列 ers に格納される.ers の中身は,[ { posters: X0, arousal: Y0, valence: Z0 }, { posters: X1, arousal: Y1, valence: Z1 }, { posters: X2, arousal: Y2, valence: Z2 } ]というように,対象となるポスターのid番号,arousal と valence の値が格納される.

init() では,AJAXでサーバにポスターの情報をリクエストする.サーバからデータを受信すると,ers.push({ posters: item["id"], arousal: 5, valence: 5 })というように,ポスターのid番号が設定され,arousal と valence は標準の5に指定される(min: 1, max: 9, step: 1なので,中央の値は5である).続いて,<img id="image-{#}">と<div id="slider-[av]{#}"> がそれぞれ設定される.これでサーバから受信したデータに基づいて画面が構築される,という具合である.ここで,先ほど述べたidの工夫が生きていることがわかるだろう.

送信ボタンのクリックで呼ばれるコールバック関数も,さほど複雑なことをしているわけではない.ers がオブジェクトの配列であること,パラメータの表記が若干異なることを吸収するために,データ形式を若干,変換してからPOSTしているだけである.なお,その際,POSTする前にプレースホルダを "poster.gif" に置き換えているので,ここで通信の遅延が生じても,ローディングイメージが表示されるのでユーザフレンドリーなインタフェースを提供できている.

データがサーバに無事ポストされれば,それで1回の評価は終了である.すぐに次の評価に移れるように,init(data)をし直して初期状態に戻る(映画ポスターはランダムに選ばれるので,画面は変化する).POSTした後のサーバ側で,redirect_to root_path している点もミソである.この記述をしているので,POSTのレスポンスとして新たなランダムデータがJSON形式で送り返される.それを再描画すれば,新しい画面になるという寸法である.

おまけのようなCSS(rp.css)は次のとおり.

#slider-a0 .ui-slider-handle { background:#0088ff; border:2px solid #000088; }

#slider-v0 .ui-slider-handle { background:#0088ff; border:2px solid #000088; }

#slider-a1 .ui-slider-handle { background:#0088ff; border:2px solid #000088; }

#slider-v1 .ui-slider-handle { background:#0088ff; border:2px solid #000088; }

#slider-a2 .ui-slider-handle { background:#0088ff; border:2px solid #000088; }

#slider-v2 .ui-slider-handle { background:#0088ff; border:2px solid #000088; }

ui-slider-handle が標準だとはっきりしないので,そこに色をつけるだけのものである.まとめて設定できるはずだが,うまくいかないので個別に指定している..ui-slider-handle というクラス指定子だけでまとめて設定できないのは何故だろう?(要確認事項)

サーバへの配備

フロントのコードはまとめてApacheウェブサーバのドキュメントルート以下に配備.Rails側は3443ポートで動作させる.ただし,CORS対策の設定やBlocked hostの対応など,少し,準備が必要である.

CORS対策として,まず,Gemfileに次を追加し,bundle install する.

gem "rack-cors"

config/application.rb に以下を追加する.これでCORSへの対策はOK.

    config.middleware.insert_before 0, Rack::Cors do

      allow do

        origins "*"

        resource "*",

          headers: :any,

          methods: [:get, :post, :options, :head]

      end

    end

  end

Blocked host の対策として,config/environments/development.rb に以下を追加する.

  config.hosts << "(サーバ名)"

また,サーバをSSLで通信するように起動する方法として,config/puma.rb にその設定を記述するという説明がネットで散見されるが,どうしてもうまくいかなかったので,次の方法でサーバを起動することで対応した.キーと証明書は,Let's Encrypt で取得したものをコピーして使用している.このあたりも,きちんと対応する必要があろう.とりあえずはこれでOKかもしれないが.

$ bin/rails s -b "ssl://0.0.0.0:3443?key=(キー)&cert=(証明書)"

以上で準備OKである.rp.html へアクセスすると,次のようなアプリを利用できる.