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 へアクセスすると,次のようなアプリを利用できる.

0 件のコメント:

コメントを投稿