テキストを音声に変換するサービス (Amazon Polly, Azure Cognitive Services)

英語の発表で合成音声を利用するというのはおもしろいなと思いました。

英語で動画コンテンツを作成するために、以前なら英語に堪能な人に依頼しようとするところでしたが、今ならテキストから音声に変換した結果のクオリティもよいだろうと考え、利用できそうなサービスを調べてみました。

Amazon

aws.amazon.com

Amazon PollyはAPIが用意されていて、そちらが本命の使い方なのだろうけど、Webインターフェースも用意されていて、簡単に利用することができました。また、音声のパターンも数種類から選ぶことができました。

f:id:iotaworks:20170831132631p:plain

Microsoft Azure

Cognitive ServicesのBing Speech APIでテキストから音声出力ができました。こちらもAmazonと同様にWeb APIが用意されています。

azure.microsoft.com

Amazon PollyのWebインターフェースを利用するためには、AWSのアカウントでログインする必要がありましたが、こちらはログインの必要なく利用できました。AWSアカウントを持っていない人に作業を依頼するなど、メンバーの構成によってはAmazonよりAzureのほうが手軽かもしれません。

Google

Google Text to Speech (TTS) というサービスがあるようですが、API等の情報を見つけることができませんでした。

Google翻訳は、翻訳結果を読み上げてくれますが、そのファイルは標準的な方法ではダウンロードできないようです。Sound of Textというサイトが内部的にGoogleのサービスを利用しているようで、入力したテキストに対応する音声をダウンロードできました。

scrap.php.xdomain.jp

まとめ

今回の私のケースだと、APIではなくWebインターフェースでサービスを利用したかったので、Amazon PollyかAzure Cognitive ServicesのBing Speech APIのどちらかを利用することになりそうです。プログラミングすることを少し覚悟していたので、Webインターフェースが用意されているのはうれしかったです。

Azure Database for PostgreSQLでpg_trgmを利用した日本語全文検索

AzureでPostgreSQLが利用できるようになりました。

セットアップ

Azureの管理画面からPostgreSQLデータベースを作成します。作成する際に、以下の情報を指定します。

  • サーバー名
  • サブスクリプション
  • リソース グループ
  • サーバー管理者ログイン名
  • パスワード
  • パスワードの確認
  • 場所
  • バージョン
  • 価格レベル

作成したデータベースにアクセスするためには、接続元のIPアドレスを登録する必要があります。Azureの管理画面の設定 > 接続のセキュリティから「自分のIPを追加」を押してから「保存」を押して、IPアドレスを登録します。

データベースへの接続

psqlコマンドでPostgreSQLデータベースに接続します。その際、ユーザー名にはusername@hostnameのフォーマットで指定します。ユーザー名のフォーマットが間違っていると、以下のようなエラーが出力されます。

psql: FATAL:  Invalid Username specified. Please check the Username and retry connection. The Username should be in <username@hostname> format.

psqlコマンドでユーザー名、ホスト名を指定します。接続先のデータベースには、最初はpostgresを指定します。

$ psql -U username@hostname -h hostname.postgres.database.azure.com -d postgres

データベースの作成

接続後、新規にデータベースを作成します。

hostname=> create database newdb;

接続先データベースを切り替えます。

hostname=> \c newdb
psql (9.6.4, server 9.6.2)
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-SHA384, bits: 256, compression: off)
You are now connected to database "newdb" as user "username@hostname".

全文検索のサポート状況

pg_bigm

pg_bigmを有効にするコマンドを実行すると、以下のようなエラーが出力されます。2017年8月17日現在ではpg_bigmはサポートされていないようです。

hostname=> create extension pg_bigm;
ERROR: extension "pg_bigm" is not supported by Azure Database for PostgreSQL
DETAIL: Installing the extension "pg_bigm" failed, because it is not on the list of extensions supported by Azure Database for PostgreSQL.
HINT: To see the full list of supported extensions run: SELECT * FROM pg_available_extensions;

pg_trgm

利用可能な機能を確認すると、pg_trgmは利用できるようです。

hostname=> select name, default_version from pg_available_extensions where name like '%pg_trgm%';
  name   | default_version 
---------+-----------------
 pg_trgm | 1.3
(1 row)

テーブルとインデックスの作成

テーブルを作成します。

hostname=> create table records (id serial, body text);

pg_trgmを有効にします。

hostname=> create extension pg_trgm;
CREATE EXTENSION

インデックスを作成します。

hostname=> create index records_body on records using gin (body gin_trgm_ops);
CREATE INDEX

サンプルデータの登録と検索

サンプルデータを登録します。今回はパフォーマンスを評価するわけではないので、1件だけ登録します。

hostname=> insert into records (body) values ('吾輩は猫である。名前はまだ無い。');

SQLで検索します。

hostname=> select * from records where body like '%吾輩は猫%';
 id |               body               
----+----------------------------------
  1 | 吾輩は猫である。名前はまだ無い。
(1 row)

正常に検索できました。インデックスが利用されているか確認します。

hostname=> explain select * from records where body like '%吾輩は猫%';
                                 QUERY PLAN                                 
----------------------------------------------------------------------------
 Bitmap Heap Scan on records  (cost=16.00..20.01 rows=1 width=36)
   Recheck Cond: (body ~~ '%吾輩は猫%'::text)
   ->  Bitmap Index Scan on records_body  (cost=0.00..16.00 rows=1 width=0)
         Index Cond: (body ~~ '%吾輩は猫%'::text)
(4 rows)

検索する文字数が2文字以下だとインデックスが利用されず、シーケンシャルスキャンになります。

hostname=> explain select * from records where body like '%猫%';
                        QUERY PLAN                        
----------------------------------------------------------
 Seq Scan on records  (cost=0.00..25.88 rows=51 width=36)
   Filter: (body ~~ '%猫%'::text)

pg_trgmのメカニズムについては、以下のスライドにまとまっていたので、あとで読んでみたいと思います。

Rails 5.1へのAdminLTE導入メモ

Railsアプリケーションの管理画面がAdminLTEで構築しようとしています。最近のRailsのやり方でやってみようとしました。

dazooo.hatenablog.com

ただ、この方法だとAdminLTEのバージョンが2.3.8で、最新は2.4.0になっていました。そのため、AdminLTEはパッケージの仕組みではなく、assetsディレクトリーに保管するようにしました。

jQueryはyarnでセットアップしました。

$ brew install yarn
$ yarn add jquery

application.jsにjQueryを追加します。

//= require rails-ujs
//= require jquery
//= require_tree .

今回はBootstrapもassetsディレクトリ以下に保存しました。

OmniAuthでFitbitのOAuth2認証を利用

以前はFitbitではOAuth 1.0とOAuth 2.0の両方のインターフェースが用意されていましたが、OAuth 2.0のほうが利用できるデータも多いですし、Fitbitの公式ドキュメントもOAuth 2.0を前提とした記述になっています。

omniauth-fitbit-oauth2を利用します。

github.com

Gemfileに以下の記述を追加します。

gem 'devise'
gem 'omniauth-fitbit-oauth2'

bundleコマンドでインストールして、Deviseに関連するファイルを作成します。

$ bundle
$ rails g devise:install
$ rails g devise User
$ rails g devise:views

db/migrate以下にあるxxxxxxxxxxxxxx_devise_create_users.rbにカラムを追加します。

## Omniauthable
t.string :uid
t.string :provider

config/initializers/devise.rbにOAuth 2.0 Client IDとClient (Consumer) Secretを指定します。また、scopeに操作するデータを指定します。

config.omniauth :fitbit_oauth2, "CLIENT_ID", "CLIENT_SECRET", scope: 'heartrate'

これらの値は、Fitbitの場合は以下のサイトでアプリケーションを登録して取得することになります。CLIENT_IDはOAuth 1.0とOAuth 2.0で違ったものになります。

dev.fitbit.com

また、Callback URLを指定する必要があります。例えばRailsの開発環境で動作させるためには、以下のように指定します。

http://localhost:3000/users/auth/fitbit_oauth2/callback

app/models/user.rbにomniauthableオプションを追加します。

devise :omniauthable, omniauth_providers: [:fitbit_oauth2]

config/routes.rbにdeviseのルーティングを設定します。

devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }

OmniAuthのコールバックのためのコントローラーを作成します。

$ rails g controller users/omniauth_callbacks

app/controllers/users/omniauth_callbacks_controller.rbに以下の記述を追加します。

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def fitbit_oauth2
    # You need to implement the method below in your model (e.g. app/models/user.rb)
    @user = User.from_omniauth(request.env["omniauth.auth"])

    if @user.persisted?
      sign_in_and_redirect @user, :event => :authentication #this will throw if @user is not activated
      set_flash_message(:notice, :success, :kind => "Fitbit") if is_navigational_format?
    else
      session["devise.fitbit_data"] = request.env["omniauth.auth"].except("extra")
      redirect_to new_user_registration_url
    end
  end
end

request.env["omniauth.auth"].except("extra")としているのは、request.env["omniauth.auth"]のままだと、Cookieのサイズの問題でエラーが発生してしまうためです。

app/models/user.rbに以下のメソッドを追加します。その際、emailについてはuidを利用したものを仮想的に入力するように指定します。これは、DeviseではUserのemailがユニークな必要があるためです。

def self.from_omniauth(auth)
  where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
    if auth.provider == 'fitbit_oauth2'
      user.email = "#{auth.uid}@fitbit.com"
    else
      user.email = auth.info.email
    end
    user.password = Devise.friendly_token[0,20]
    # user.name = auth.info.name   # assuming the user model has a name
    # user.image = auth.info.image # assuming the user model has an image
  end
end

DeviseとOmniAuth関係の基本的な設定はこれで完了です。

$ rails g controller welcome index

app/controllers/welcome_controller.rbを以下のように編集します。

class WelcomeController < ApplicationController
  before_action :authenticate_user!

  def index
  end
end

app/views/welcome/index.html.erbを以下のように編集します。

<h1>Welcome#index</h1>
<p>Find me in app/views/welcome/index.html.erb</p>

<% if user_signed_in? %>
  Signed in as <%= current_user.email %>.
  <%= link_to "Sign out", destroy_user_session_path, :method => :delete %>
<% else %>
  <%= link_to "Sign up", new_user_registration_path %> or
  <%= link_to "Sign in", new_user_session_path %>
<% end %>

config/routes.rbにrootを指定します。

root 'welcome#index'

これで、OAuth 2.0を利用してFitbitアカウントとDeviseのomniauthableオプションを利用した認証が動作するようになりました。

Devise+OmniAuthで直接Googleのログイン画面を表示する方法

DeviseとOmniAuthでGoogleアカウントと連携するアプリケーションを開発しています。Deviseでは認証が要求される場合には、Deviseのログイン画面が開きます。しかし、Googleのアカウントのみと連携するアプリケーションであれば、直接Googleのログイン画面を表示させたほうがシンプルになります。

以下の情報を参考にして設定することができました。

stackoverflow.com

qiita.com

  • config/initializer/devise.rb
# ==> Warden configuration
# If you want to use other strategies, that are not supported by Devise, or
# change the failure app, you can configure them inside the config.warden block.
#
# config.warden do |manager|
#   manager.intercept_401 = false
#   manager.default_strategies(scope: :user).unshift :some_external_strategy
# end
config.warden do |manager|
  manager.failure_app = CustomAuthenticationFailure
end

リダイレクト先にGoogleのログインページにリダイレクトするパスを指定します。

  • lib/custom_authentication_failure.rb
class CustomAuthenticationFailure < Devise::FailureApp
  protected
  def redirect_url
    user_google_omniauth_authorize_path
  end
end

libディレクトリ以下のファイルをロードするように設定します。

  • config/application.rb
config.autoload_paths += %W(#{config.root}/lib)

これらの設定で、これまでDeviseのログイン画面を表示していた処理が、直接Googleのログイン画面を表示するようになりました。

Material Design LiteでAdsenseを2回ロードさせない方法

Material Design LiteでAdsenseのコードを貼り付けると、2回ロードされてしまうことがあります。

github.com

mdl-componentupgradedイベントで処理する方法が示されていて、日本語の解説記事もありました。

b.0218.jp

ただ、私がこの方法でやってみて、最初はうまくいきませんでした。Javascriptのコードは記事に示されていた通り、以下のように記述していました。

document.addEventListener('mdl-componentupgraded', function() {
  if (!document.getElementById('adsbygooglejs')) {
    var head = document.getElementsByTagName('head')[0];
    var script = document.createElement('script');
    script.id = 'adsbygooglejs';
    script.src = "//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js";
    head.appendChild(script);
  }\
});

原因は私のAdsenseを呼び出すHTMLが間違っていました。私の場合、以下のように記述していました。

<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<ins class="adsbygoogle" style="display:block" data-ad-client="ca-pub-xxxxxxxxxxxxxxxx" data-ad-slot="xxxxxxxxxx" data-ad-format="auto"></ins>
<script>(adsbygoogle = window.adsbygoogle || []).push({});</script>

さきほどのJavascriptはAdsenseのJavascriptをmdl-componentupgradedイベントで読み込むようにしていたので、それ以外の場所でAdsenseのJavascriptを読み込むようにしてはいけないことに気づきました。さきほどのHTMLの最初の行を削除して、以下のようにして正常に動作させることができました。

<ins class="adsbygoogle" style="display:block" data-ad-client="ca-pub-xxxxxxxxxxxxxxxx" data-ad-slot="xxxxxxxxxx" data-ad-format="auto"></ins>
<script>(adsbygoogle = window.adsbygoogle || []).push({});</script>