AllIsHackedOff

Just a memo, just a progress

スマートスピーカーに青空文庫を朗読させるためにATOMのプラグインを作っている話

こんばんは。この記事はGunosy Advent Calendar 2017 - Qiitaの12/22の記事です。 前日の記事はGoogle Home と Android ThingsでLチカする - CrossBridge Labでした。 本末転倒?感のあるタイトルをつけてしまいましたが、 今回はGoogle HomeやらAmazon Echoに青空文庫を朗読させるために余暇で行っている下準備について記載します。

僕の頭のなかではAmazon Echoに青空文庫を朗読しながら眠りにつく毎日を送っているはずだったのですが。 Amazon Echo ... 申込んだのになかなか売ってもらえない....つらい。 実機がない状態でLambdaやPollyを触りつつ、所持しているGoogle Homeで粛々と準備をすすめている次第です。

目次

  • SSML(Speech Synthesis Markup Language)について
  • Amazon Polly
  • SSMLを気軽に作りたい -> Atomプラグイン作成

SSML(Speech Synthesis Markup Language)について

SSMLはSpeech Synthesis Markup Languageの略称です。その名の通り、音声合成マークアップするための言語で、仕様はSpeech Synthesis Markup Language (SSML) Version 1.1に記載されています。 SSMLを記述することにより会話中の

  • 休止(break / 秒数)
  • 強調(emphasis)
  • 声量/ピッチ/強弱/速さ (prosody)

をコントロールすることができます。

Alexaで利用できるタグに関しては下記の公式ドキュメントに非常に丁寧に記載されており、日本語の発話例も提示されています。 Speech Synthesis Markup Language (SSML) Reference | Custom Skills Speechcon Reference (Interjections): Japanese | Custom Skills

12/20に公開された下記の記事に具体例と共にGoogle Assistantの設定まで詳しく書かれており、非常に勉強になりました。 qiita.com

Amazon Polly

Amazon PollyはText-to-Speech用の音声を合成してくれるクラウドサービスです。 https://aws.amazon.com/jp/blogs/news/polly-text-to-speech-in-47-voices-and-24-languages/ AWS Consoleから気軽に試すことができ、無料利用枠も存在しています。

試しに青空文庫から宮沢賢治 狼森と笊森、盗森を取得してGUI上にコピペしてみると f:id:masashisalvador:20171222191624p:plain こんな感じになります。 soundcloud.com 青空文庫の文章を色々投げ込んでみて感じた所感としては、意外と発音は正しい、人名やコンテキストによって読みが変わる文字、ルビがないと人間にも読めないものはやはりうまく発音されません。 (上記の例だと、巨い(おおきい)など) そして何より、素の発音だと抑揚がないため、ぶっちゃけるとあまり面白くありません。

いい朗読をさせるためにはSSMLをうまく書いてやらないといけない。 ということになります。

しかしSSMLはただのXMLです。素のXMLを淡々と打ち込んでいくだけの精神力を、僕は持ち合わせていませんでした。

SSMLを気軽に作りたい -> Atomプラグイン作成

若干調べた感じ、フリーでいい感じにSSMLを編集させてくれるソフトは見当たらないようでした。 SSMLをもっと気軽にかけないと楽しい朗読までたどり着かない...ということで簡単なエディタを作ることにしました。

やりたいことはシンプル。タグを簡単に挿入していきたいのです。 また、使い方が簡単で、プログラマーでなくとも朗読用のSSMLを作れるようにしたい。

ということで、雑ですがイメージとしてはこんな感じです。 f:id:masashisalvador:20171222193402j:plain

しかしエディタを0から作るのはハードルが高い...ということで出来合いのものに乗っかることにします。 業務ではすっかりIntelliJの奴隷と化しているのですが、ここは一つATOM様に助けを求めたいとおもいます。

ATOMプラグイン作成自体は公式ドキュメント atom.io と幾つかの記事を参考にすると簡単に作ることができました。

以下、機能と書いたコードを少しだけ紹介します。

1. 休止の挿入

https://gyazo.com/7c4c3a8a92d6469b964be768d49adfd9

秒数を選択しておけば休止秒数が指定できるように、下記のようなコードを書いています。

  insertBreak() {
    let editor = atom.workspace.getActiveTextEditor();
    // 選択部分の取得
    let selectedText = editor.getSelectedText();
    var breakTag = "";

    if (selectedText.length <= 0) {
      breakTag = '<break strength="strong"/>';
    } else {
      breakTag = '<break time="' + selectedText + 's"/>'
    }

    editor.insertText(breakTag);
  },

2.強調の挿入

選択部分を強調します。選択部の冒頭にsr を付しておくと、強調タグ(emphasisタグ)のlevelが strongreduced になるようにしています。

https://gyazo.com/ad605e72ae354c9f5f7d41d32821452a

  insertEmphasis() {
    let editor = atom.workspace.getActiveTextEditor();
    let selectedText = editor.getSelectedText();
    if (selectedText.length <= 0) {
      return;
    }
  // 中略

    let tag = this.wrapTextWithEmphasisTagWithLevel(selectedText, level);
    editor.insertText(tag);
  },

まとめ

アドベントカレンダーまでには作り終わる!と意気込んでいたのですが、やっていくと色々と考慮しなければならない点が出てきたので未完の状態です。SSMLの各タグで指定できる属性が色々とあるので、属性をどうやってスムースに入力させるか...などを考えているとキリがなくなってきました。

ひとまず自分でひたすら使ってみて、駄目でも先に進むスタイルで年明けあたりに公開したいなと思っています。

それでは皆さんよいクリスマスを!

mattes / migrate のメモ

migrateの使い方

toolchain #golang

how to install? ref : https://github.com/mattes/migrate/tree/master/cli

# with homebrew
brew install migrate
# with curl
curl -L https://github.com/mattes/migrate/releases/download/v3.0.1/migrate.darwin-amd64.tar.gz
tar xvz migrate.darwin-amd64.tar.gz
mv migrate.darwin-amd64.tar.gz /usr/local/migrate

naming rule for migration file ref : https://github.com/mattes/migrate/blob/master/MIGRATIONS.md ここに書いてある形式でファイルを作成するらしい。 なんでupとdownを分けるの?についても

-database        Run migrations against this database (driver://url)
# DSNを指定すればいいっぽい?
# http://go-database-sql.org/accessing.html

up

[👾  @masashi.salvador.mitsuzawa] ~/dev/sample/mattes_migrate
% migrate -database 'mysql://root@tcp(127.0.0.1:3306)/hogehoge' -path ./ up
1/u create_hoge (15.876738ms)
[👾  @masashi.salvador.mitsuzawa] ~/dev/sample/mattes_migrate
% migrate -database 'mysql://root@tcp(127.0.0.1:3306)/hogehoge' -path ./ up
no change

down

[👾  @masashi.salvador.mitsuzawa] ~/dev/sample/mattes_migrate
% migrate -database 'mysql://root@tcp(127.0.0.1:3306)/hogehoge' -path ./ down
no change

良さそう。

実行すると schema_migrations テーブルが作成されます。

Firebase Authから返ってくるJWTをゴニョゴニョする (2)

前回の記事 allishackedoff.hatenablog.com の続きです。引き続きゴニョゴニョします。

JWTをDecodeする

golangのJWTのライブラリは github.com が最も使われているようです。 otiai10.hatenablog.com christina04.hatenablog.com この辺りでも利用されているようで、今回トライする際に参考にさせて頂きました。

Firebase Authから返ってくるJWTを検証する

検証するにはヘッダーをdecodeしないといけません。 headerに含まれる kid に対応するCERTIFICATEを使ってtokenが正しいかどうかを検証します。 「文字列としてcertificateを渡せばよしなにやってくれるやろ」とか甘いことを考えていましたが、しっかりrsa.PublicKey型のkeyを抜き出してやらないと verifyできません。

検証の流れは下記です。

  • JWTのヘッダーをDecode
  • https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com からcertificateのリストを取得
  • 取得したcertificateのリストから、 ヘッダーの kid に対応するkeyを見つける
    • 見つからない場合はexpireしている
  • 見つけたkeyをx509 パッケージを利用して rsa.PublicKey 型の変数にする
  • Tokenをdecode
  • decodeで生成されたjwt.Token Objectの Validを見る
    • expireしている / publickeyがうまく取得できていない場合は token.Valid == false になる

ソースコード (in Golang)

package main

import (
    "encoding/json"
    "encoding/pem"
    "errors"
    "fmt"
    "log"
    "net/http"
    "strings"

    "crypto/x509"

    "crypto/rsa"

    jwt "github.com/dgrijalva/jwt-go"
)

type header struct {
    Alg string `json:"alg"`
    Kid string `json:"kid"`
}

type payload struct {
    jwt.StandardClaims
}

func main() {
    const (
        publicKeySrcURL = "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com"
    )
    rawToken := "<PASTE YOUR Firebase Auth JWT"
    segs := strings.Split(rawToken, ".")

    bHeader, err := jwt.DecodeSegment(segs[0])
    if err != nil {
        log.Fatalln("error in decoding segment header")
    }

    var h header
    json.Unmarshal(bHeader, &h)

    resp, err := http.Get(publicKeySrcURL)
    if err != nil {
        log.Fatalln("error in getting public key")
    }
    defer resp.Body.Close()
        
       // 一応Streamで扱っておく
    decoder := json.NewDecoder(resp.Body)
    pubkeyMap := make(map[string]string)
    err = decoder.Decode(&pubkeyMap)
    if err != nil {
        fmt.Printf("%v", err)
        log.Fatalln("error on decoding http response body")
    }

    rawPubkey, ok := pubkeyMap[h.Kid]
    if !ok {
        log.Fatalln("public key corresponding to XXX is not found")
    }
    pk, err := parseKey([]byte(rawPubkey))
    if err != nil {
        panic(err)
    }

    token, err := jwt.Parse(rawToken, func(token *jwt.Token) (interface{}, error) {
        if _, ok = token.Method.(*jwt.SigningMethodRSA); !ok {
            return nil, errors.New("signing methods other than RSA are not expected")
        }
        if token.Method != jwt.SigningMethodRS256 {
            return nil, errors.New("Signing methods other than RS256 are not expected")
        }

        return pk, nil
    })

    //spew.Dump(token)
    if !token.Valid {
        fmt.Println("token is invalid")
    }
}

// --CETIFICATE -> rsa.PublicKeyへの変換
func parseKey(rawPubkey []byte) (interface{}, error) {
    block, _ := pem.Decode(rawPubkey)
    cert, _ := x509.ParseCertificate(block.Bytes)
    pk := cert.PublicKey.(*rsa.PublicKey)

    fmt.Println(pk)
    return pk, nil
}

string -> []byteへのキャストをやっているのがイケてないですが、とりあえずこれでdecodeできます。

NOTE

Certificate -> rsa.PublicKeyへの変換が所見だったのでこのあたりの知識を補強しておく

Firebase Authから返ってくるJWTをゴニョゴニョする(1)

同じモチベーションの人がいたら幸いです。

モチベーション

  • Rails にしろGoにしろ自前でAuth周りを書くのがだるい

    • ログイン機能は必要だけど、さっさと作って使われるか検証したい
    • 認証なしでやればいいじゃん?って話もあるけど、なんだかんだMyhogehogeみたいな機能を実装して試したい
  • Rails -> devisev? => なにか起こる度にRack::MiddleWareとかにログを指して折ってくのつらい..

    • 2010年くらい(@Masashisalvadorが学生だったころ)から同じような感じ..
  • Go

    • コードは読みに行けるけど..自前で実装すんのかよ => 本質的な機能の部分をさっさと実装したい (=認証は外付けにしたい)
  • FirebaseにAuthあるじゃん

    • 公式SDKはNodeとかか…
    • Googleがやってくれるならメンテしてくれるだろうし、無茶苦茶なことにはならないだろうからいいな
    • ID/Passwordログインも、各種サービスのOAuthに対応してる => 勝てる?
    • AuthはRest API経由だとできないのか => JWTを使えばまあ認証サーバの切り出し(=Firebaseにおまかせ)はできそうだな。
    • 3rd partyでホゲる際のことも公式ドキュメントに書いてあるし、いきなりAuthだけつかうんじゃねえ!とはならなそう。
    • ついでにPushとかもFirebase経由で送れるのは便利だ。

最終的にやりたいこと

  • Firebaseから飛んでくるJWTをよしなにパースして認証したい。
    • Rails or Goで(Goは技術調査も兼ねた自前プロジェクトで使う)
    • RailsだったらGem入れる or コードコピーしてrequireしてゴニョるくらいで使いたい = 使い回しを容易にしたい

とりあえず認証をザクっと書いてJWTの中身を覗いてみる

コードはクソ雑ですが…

onClickSignInWithFacbook() {
    firebase.auth().signInWithPopup(provider).then((result) => {
      // This gives you a Facebook Access Token. You can use it to access the Facebook API.
      global.rrr = result;
      let token = result.credential.accessToken;
      // The signed-in user info.
      let user = result.user;
      // ...3
      this.setState({ user });
      console.log(token);
      console.log(user);
    }).catch((error) => {
      console.log(error);
      console.log("Error on facebook auth");
      // Handle Errors here.
      let errorCode = error.code;
      let errorMessage = error.message;
      // The email of the user's account used.
      let email = error.email;
      // The firebase.auth.AuthCredential type that was used.
      let credential = error.credential;
      // ...
    });
  }
  onClickSignInWithGoogle() {
    firebase.auth().signInWithPopup(gProvider).then((result) => {
      // This gives you a Google Access Token. You can use it to access the Google API.
      global.rrr = result;
      let token = result.credential.accessToken;
      // The signed-in user info.
      let user = result.user;
      this.setState({ user });
  
      console.log(token);
      console.log(user);
      // ...
    }).catch((error) => {
      console.log("error on google auth");
      console.log(error);
      // Handle Errors here.
      let errorCode = error.code;
      let errorMessage = error.message;
      // The email of the user's account used.
      let email = error.email;
      // The firebase.auth.AuthCredential type that was used.
      let credential = error.credential;
      // ...
    });    
  }

Google Authの場合

rrr.credential.idToken # .でくぎられたJWTが入ってる
rrr.credential.idToken.split(".") # header / claims / signatureに別れてる

1つめのセグメント = splitした配列の先頭はただのヘッダのはず

echo -n "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdhOWY5Yzc5ZGYzMDU5MzhlMjY4ODk3Y2JkNDc1ZDQ3MjY4MWI0ZWEifQ" | base64 -D                                                                                                                 
{"alg":"RS256","kid":"7a9f9c79df305938e268897cbd475d472681b4ea"%

JWTのヘッダが入っている。閉じカッコがないのが気になるがこれは? kidは公開鍵を探しに行く時に使う。

echo -n "2つ目のセグメント" | base64 -D | jq .
{
  "azp": "xxxxx.apps.googleusercontent.com",
  "aud": "xxxxx.apps.googleusercontent.com",
  "sub": "1111111111111111111",
  "email": "hoge@example.com",
  "email_verified": true,
  "at_hash": "xxxxxxx",
  "iss": "accounts.google.com",
  "iat": 1495084945,
  "exp": 1495088545
}

JWTのStandardぽいものに、認証に使ったemailなどが入っている。

Facebook Authの場合

上記コードの result.user.Yd というキーで JWTが入っていた。google Auth の場合も同様でした。(googleの場合はcredentialsにも入っている)

rrr.user.Yd.split(".")
{
  "iss": "https://securetoken.google.com/xxxx",
  "name": "Masashi Salvador Mitsuzawa",
  "picture": "https://scontent.xx.fbcdn.net/v/t1.0-1/p100x100/16142336_1426152514084193_2027921832216883945_n.jpg?oh=8d6238d7d50034e2fd7208c8a4208fc1&oe=59745742",
  "aud": "xxxx",
  "auth_time": 1495086800,
  "user_id": "xxxx",
  "sub": "xxxx",
  "iat": 1495086800,
  "exp": 1495090400,
  "email": "xxxxx@yahoo.co.jp",
  "email_verified": false,
  "firebase": {
    "identities": {
      "facebook.com": [
        "1562766520422791"
      ],
      "email": [
        "m_m_moonty@yahoo.co.jp"
      ]
    },
    "sign_in_provider": "facebook.com"
  }
}

こんな感じでClaimsがDecodeできる。

} (closing bracket)が消える問題

echo -n "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdhOWY5Yzc5ZGYzMDU5MzhlMjY4ODk3Y2JkNDc1ZDQ3MjY4MWI0ZWEifQ" | base64 -D                                                                                                                 
{"alg":"RS256","kid":"7a9f9c79df305938e268897cbd475d472681b4ea"% # }が消えてる

同じ文字列を Golang で書いた適当なコードでdecodeしてやると

(string) (len=64) "{\"alg\":\"RS256\",\"kid\":\"e62eaba0e06fe1463746c69efa974c1152234634\"}"

問題なく閉じカッコが出現する

}エンコードすると

echo -n "}" | base64
fQ==

となるので、たしかに文字列としてはたりなさそうに見える。 Base64自体はRFCかなにかで規定されている(はず)なので、挙動差異は変なのだが…何なのだろう?

Rails5のAPIModeつかってるけどRailsAdminもheroku上でサクッと使いたいよねというお話(失敗談)

API Mode

Rails 5 をAPI modeで動かしている。 * RailsAdminは使いたいよな(管理画面のフロントをSPAで書くのは工数の無駄) * herokuは使いたい(今更deploy周りとかサーバのセットアップとかだるい)

ということで Client -> rack app (routing) -> API @ port 3000 -> Admin (RailsAdmin) @ port 19998

構成

Gemfile
Gemfile.lock
app.rb      
config.ru   
vendor      
web1        
web2
ADMIN_PATH = '/admin'
WEB_SERVER_PORT = 3000
ADMIN_SERVER_PORT = 19899

class SimpleApp
  def call(env)
    request = Rack::Request.new(env)
    request_path = env['REQUEST_PATH']

    if request_path.start_with?(ADMIN_PATH)
      [ 301, {'Location' => "http://localhost:#{ADMIN_SERVER_PORT}#{request_path}" }, self ]
    else
      [ 301, {'Location' => "http://localhost:#{WEB_SERVER_PORT}#{request_path}" }, self ]
    end
  end
  def each(&block)
  end
end

こんな感じでheroku上で動かそうとしたが、そもそもlocalhostヘリダイレクトはクライアント側のローカルホストにリダイレクトされてしまうのでだめですよね。という当然の結論にheroku上で立ち上がっているappサーバにうまく通信しに行く方法はないんだろうか

2017年にもなってHerokuでハマるクラスタ (heroku MySQL)

初期データ投入のため

heroku run rake db:seed_fu
ActiveRecord::StatementInvalid: Mysql2::Error: User 'ba2df6a9fed0b0' has exceeded the 'max_questions' resource (current value: 3600): INSERT INTO `item_resources` (`item_id`, `resource_id`, `created_at`, `updated_at`) VALUES (1741, 2, '2017-05-01 04:53:46', '2017-05-01 04:53:46')

max_questionsてなんやねんと思ったら、MySQL側でユーザごとのクエリ数とかを制限する仕組みらしい。 そういった制限のあるところでMySQLを使ったことがなかったので初めて遭遇するエラーである。 Heroku上にはQPHの制限について書いてなかったので、ググっていると下記のサイトを見つけた。

getsatisfaction.com なるほど、関連テーブルもあるからクエリ数が足りなすぎる問題。どうしたものか。

そもそも放置気味になっているN+1問題を引き起こすエンドポイントとかあるので…直さないと一瞬で食いつぶされてしまう…orz

MacのxargsのJ option (replacerを使う記法)

command  | xargs -i cp {} target_dir

的なことがやりたかったがどうもMacのxargsだとだめらしい(これだからMacは…)

manを開くと下記が記載されていたので事なきを得た。

     -J replstr
             If this option is specified, xargs will use the data read from standard input to replace the first occurrence of replstr instead of appending that data after all other arguments.  This option will not
             affect how many arguments will be read from input (-n), or the size of the command(s) xargs will generate (-s).  The option just moves where those arguments will be placed in the command(s) that are
             executed.  The replstr must show up as a distinct argument to xargs.  It will not be recognized if, for instance, it is in the middle of a quoted string.  Furthermore, only the first occurrence of the
             replstr will be replaced.  For example, the following command will copy the list of files and directories which start with an uppercase letter in the current directory to destdir:

                   /bin/ls -1d [A-Z]* | xargs -J % cp -rp % destdir