BETA

「くまねこ - 短歌ジェネレーター」を支える技術

投稿日:2019-08-16
最終更新:2019-09-06

くまねこについて

宮沢みちひを改修したものです。Nuxt.js + Express on Herokuのプロダクトです。

宮沢みちひは、入力されたフレーズを使って短歌っぽい文字列を生成するWebアプリに、ヴァーチャル歌人という名目で名前をつけたものです。ただ、短歌自動生成というのはほぼ釣りみたいなもので、必ずしも57577のきれいな短歌に見える文字列が生成できるわけではありません。

Nuxt.js周りの小ワザ

jQueryプラグインの使用

ややレガシーなjQueryプラグインをバンバン使っています。

module.exports = {  
    /*  
    ** Plugins to load before mounting the App  
    */  
    plugins: [  
        { src: "@/plugins/clientOnly.js", ssr: false }  
    ],  
    /*  
    ** Build configuration  
    */  
    build: {  
        plugins: [  
            new webpack.ProvidePlugin({  
                "$": "jquery",  
                "jQuery": "jquery",  
                "window.jQuery": "jquery",  
                "_": "underscore"  
            })  
        ],  
        extractCSS: true  
    }  
}  

plugins配下に適当に作成したファイル内で読み込みます。

if (process.client) {  

    // bootstrap  
    require("./bootstrap/bootstrap.min.js")  

    // bootgrid  
    require("./jquery.bootgrid/jquery.bootgrid.min.js")  
    require("./jquery.bootgrid/jquery.bootgrid.fa.min.js")  

    // flexdatalist  
    require("./jquery-flexdatalist/jquery.flexdatalist.min")  

    // jssocial  
    require("./jssocials/jssocials.min.js")  

}  

表示の高速化

このあたりのモジュールを使っておきます。

Heroku周りの小ワザ

クラッシュ時に自動で復帰させる

nodemonとforeverを併用するとよいとどこかで読みました。package.jsonを以下のような感じで書きます。

{  
  "scripts": {  
    "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server",  
    "build": "cross-env NODE_ENV=production nuxt build",  
    "start": "cross-env NODE_ENV=production forever --killSignal=SIGTERM -c 'nodemon --exitcrash' server/index.js",  
    "heroku-postbuild": "yarn build",  
    "generate-docs": "apidoc -i server/routes/ -o docs/"  
  }  
}  

nodeコマンドにオプションを渡したいので、nodemon.jsonをプロジェクトのルートに作成して以下のような感じで書きます。

{  
    "verbose": true,  
    "ignore": ["node_modules/*", "tmp/*"],  
    "execMap": {  
        "js": "node --optimize_for_size --max_old_space_size=480 --gc_interval=100"  
    }  
}  

短歌を生成する方法

DB設計

くまねこは与えられたフレーズからマルコフ連鎖で文字列を生成します。実態としてはあらかじめ音数を数えて格納しておいたDBを検索しつつ音数を足し合わせる処理をしています。

collection

CREATE TABLE "collection" ( `prefix` TEXT, `suffix` TEXT, `pyomi` TEXT, `syomi` TEXT, `plength` INTEGER, `slength` INTEGER )  

prefixとその共起語であるsuffixのリストです。

prefix suffix pyomi syomi plengh slength
八月 ハチガツ 4 1
夜明け ヨアケ 1 3
夜明け ヨアケ 3 1

chunks

CREATE TABLE "chunks" ( `index` TEXT, `chunk` TEXT, `yomi` TEXT, `clength` INTEGER )  

prefixがcollection中に見つからない場合はこの中からランダムに文節を返します。

index chunk yomi clength
八月の ハチガツノ 5
夜明けが ヨアケガ 4
明ける 明ける アケル 3

collection_prefix

CREATE INDEX `collection_prefix` ON `collection` (`prefix` )  

collectionのprefixのインデックスです。

API

/api/poemというエンドポイントを叩いて文字列を生成します。与えられたフレーズを詠み込む位置をふつうに場合分けで調整したうえで、axiosで実際にDBを開いて文字列を生成する自身のエンドポイントを叩いています。

/**  
 * @api {post} /api/poem  
 * @apiGroup API/POEM  
 * @apiName PoemCreation  
 * @apiParam {String} keyphrase.phraselast Last term of keyphrase.  
 * @apiParam {string} keyphrase.phrase Phrase body.  
 * @apiParam {Number} keyphrase.mora Mora count of keyphrase  
 * @apiSuccess {String} info Poem body.  
 */  
router.post("/poem", (req, res) => {  

    /**  
     * PoemGenerator  
     * @param {String} prefix - Prefix phrase.  
     * @param {Number} expectation - Length of phrase coming out.  
     * @return {String} Poem body.  
     */  
    function generatePhrases (prefix, expectation) {  
        if (expectation <= 0) { return {data: ""}; }  
        if (!_.isEmpty(prefix)) {  
            return axios.put("/api/find", {  
                 pref: prefix, expec: expectation  
            }).then(response => {  
                // console.log(response.data);  
                return response.data.body;  
            });  
        } else {  
            return axios.put("/api/random", {  
                expec: expectation  
            }).then(response => {  
                // console.log(response.data);  
                return response.data.body;  
            });  
        }  
    };  

    /**  
     * ConcatPhrases  
     * @param {String[]} arr - Array of phrases.  
     * @return {String} Phrase body.  
     */  
    function concatPhrases (arr) {  
        const tmp = _.reduce(arr, (memo, str) => { return memo.concat(str) });  
        return tmp;  
    }  

    (async (keyphrase) => { // console.info(keyphrase);  

        try {  
            const mora = Number(keyphrase.mora);  

            if (mora <= 6) { // 6以下なら3句  

                const one =  await generatePhrases("", 5);  
                const two = await generatePhrases("", 7);  
                const four = await generatePhrases(keyphrase.phraselast, 7);  
                const five = await generatePhrases("", 7);  
                const text = concatPhrases([one, two, keyphrase.phrase, four, five]);  
                res.status(200).json({ info: text });  

            } else if (mora >= 7 && mora <= 8) { // 7なら2句  

                const one = await generatePhrases("", 5);  
                const three = await generatePhrases(keyphrase.phraselast, 5);  
                const four = await generatePhrases("", 7);  
                const five = await generatePhrases("", 7);  
                const text = concatPhrases([one, keyphrase.phrase, three, four, five]);  
                res.status(200).json({ info: text });  

            } else if (mora >= 9 && mora <= 13) { // 2句3句  

                const one = await generatePhrases("", 5);  
                const four = await generatePhrases(keyphrase.phraselast, 7);  
                const five = await generatePhrases("", 7);  
                const text = concatPhrases([one, keyphrase.phrase, four, five]);  
                res.status(200).json({ info: text });  

            } else if (mora >= 14 && mora <= 16) { // 14以上16以下なら下の句  

                const one = await generatePhrases("", 5);  
                const twothree = await generatePhrases("", 12);  
                const text = concatPhrases([one, twothree, keyphrase.phrase]);  
                res.status(200).json({ info: text });  

            } else if (mora >= 17 && mora <= 18) { // 上の句  

                const four = await generatePhrases(keyphrase.phraselast, 7);  
                const five = await generatePhrases("", 7);  
                const text = concatPhrases([keyphrase.phrase, four, five]);  
                res.status(200).json({ info: text });  

            } else if (mora === 19) { // 19なら先頭5を3句にして下の句  

                const onetwo = await generatePhrases("", 12);  
                const text = concatPhrases([onetwo, keyphrase.phrase]);  
                res.status(200).json({ info: text });  

            } else {  

                const onetwo = generatePhrases("", 31 - mora);  
                const text = concatPhrases([onetwo, keyphrase.phrase]);  
                res.status(200).json({ info: text });  

            }  
        } catch (err) {  
            console.error(err);  
            const text = "うーん、いいのが思いつかないや……ごめんね。";  
            res.status(200).json({ info: text });  
        }  
    })(req.body);  
});  
技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる
技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
or 外部アカウントではじめる
10秒で技術ブログが作れます!