LoginSignup
6
6

More than 1 year has passed since last update.

最強(かもしれない)のしりとりAIを作ってみた

Last updated at Posted at 2020-03-26

おしらせ(次回予告?)

最近学んだNuxt.jsを生かして、作り直しています!

この記事&ソースコードは中1(2?)のときに書いた大変お見苦しいものになっています。
(ツッコミどころがたくさんあります)

忙しくなくなったら開発を進めてQiitaも書くはずなのでより最強に近づいた物を見たい方はお待ち下さい。
MediaWiki要約ファイルをDLして自作プログラムで読み仮名を抽出してDBにぶちこんだりしてます。

しりとりができるAIを作ってみた。

前回はGASを使ってLINE BOTを作りましたが、
今回はWebでJavascriptを使って作ってみます。

使ったAPI

gooラボ ひらがな化API
ウィキペディア MediaWikiAPI

あとjQueryとFont Awesomeのアイコンフォントを使います。
使用したアイコンはこれこれです

コード

index.html
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Wiki しりとり AI</title>
    <link rel="manifest" href="./manifest.json">
    <link rel="stylesheet" href="./font_icon/css/all.min.css">
    <link rel="stylesheet" href="./chat_UI.css">
    <link rel="icon" href="./icons/favicon.ico">
</head>

<body>
<script src="./jquery-3.4.1.min.js"></script>
<div id="chat-box">
    <div class="kaiwa">
        <figure class="kaiwa-img-left">
            <img src="./icons/Wikipedia-logo-v2-ja.png" alt="no-img2">
            <figcaption class="kaiwa-img-description">
                しりとり AI
            </figcaption>
        </figure>
        <div class="kaiwa-text-right">
            <p class="kaiwa-text">
                「Wiki しりとりAI」は、<br>
                gooラボの<a href="https://labs.goo.ne.jp/api/jp/hiragana-translation/">ひらがな化API</a>を使用しています。
            </p>
        </div>
    </div>
    <div class="kaiwa">

        <figure class="kaiwa-img-left">
            <img src="./icons/Wikipedia-logo-v2-ja.png" alt="no-img2">
            <figcaption class="kaiwa-img-description">
                しりとり AI
            </figcaption>
        </figure>
        <div class="kaiwa-text-right">
            <p class="kaiwa-text">
                Wikipediaから集めたデータを使ってしりとりができます。<br>
                漢字の読み間違いは許してください。
            </p>
        </div>
    </div>
    <div class="kaiwa">
        <figure class="kaiwa-img-left">
            <img src="./icons/Wikipedia-logo-v2-ja.png" alt="no-img2">
            <figcaption class="kaiwa-img-description">
                しりとり AI
            </figcaption>
        </figure>
        <div class="kaiwa-text-right">
            <p class="kaiwa-text">
                仕様
            <ul>
                <li>
                    強いので「ん」でも返します。
                </li>
                <li>
                    末尾の小文字(「ぁ」など)は大文字(「あ」)に変換されます。
                </li>
                <li>
                    発音できない文字が末尾にある場合は1番最後の発音出来る文字で判定します。
                </li>
            </ul>
            </p>
        </div>
    </div>
    <div class="kaiwa">
        <figure class="kaiwa-img-left">
            <img src="./icons/Wikipedia-logo-v2-ja.png" alt="no-img2">
            <figcaption class="kaiwa-img-description">
                しりとり AI
            </figcaption>
        </figure>
        <div class="kaiwa-text-right">
            <p class="kaiwa-text">
                「しりとり」
            </p>
        </div>
    </div>
</div>
<div id="form">
    <textarea id="input_text" cols="30" rows="3" placeholder="「り」から始まる言葉"></textarea>
    <div id="buttons">
        <button id="submit_btn"><i class="far fa-paper-plane"></i> <span id="submit_btn_text">送信</span></button>
        <button id="record_btn"><i class="fas fa-microphone"></i> <span id="record_btn_text">マイク</span></button>
        <input type="checkbox" id="check">CPU読み上げ</input>
        <label class="check" for="check">
            <div></div>
        </label>
    </div>
</div>
<script src="./siritori.js"></script>


</body>

</html>

次に見た目を作るCSSです。

chat_UI.css
/*——————–
 吹き出しを作る
——————–*/

/* 全体のスタイル */

.kaiwa {
  margin-bottom: 35px;
}

/* 左画像 */

.kaiwa-img-left {
  margin: 0;
  float: left;
  width: 60px;
  height: 60px;
  margin-right: -70px;
}

/* 右画像 */

.kaiwa-img-right {
  margin: 0;
  margin-right: 10px;
  float: right;
  width: 60px;
  height: 60px;
  margin-left: -70px;
}

.kaiwa figure img {
  width: 100%;
  height: 100%;
  border: 1px solid #aaa;
  border-radius: 50%;
  margin: 0;
}

/* 画像の下のテキスト */

.kaiwa-img-description {
  padding: 20px 0 0;
  font-size: 12px;
  text-align: center;
  position: relative;
  bottom: 15px;
}

/* 左からの吹き出しテキスト */

.kaiwa-text-right {
  position: relative;
  margin-left: 80px;
  padding: 10px;
  border-radius: 10px;
  background: #eee;
  margin-right: 20%;
  float: left;
}

/* 右からの吹き出しテキスト */

.kaiwa-text-left {
  position: relative;
  margin-right: 80px;
  padding: 10px;
  border-radius: 10px;
  background-color: #9cd6e7;
  margin-left: 20%;
  float: right;
}

p.kaiwa-text {
  margin: 0 0 20px;
}

p.kaiwa-text:last-child {
  margin-bottom: 0;
}

/* 左の三角形を作る */

.kaiwa-text-right:before {
  position: absolute;
  content: '';
  border: 10px solid transparent;
  top: 10px;
  left: -20px;
}

.kaiwa-text-right:after {
  position: absolute;
  content: '';
  border: 10px solid transparent;
  border-right: 10px solid #eee;
  top: 10px;
  left: -19px;
}

/* 右の三角形を作る */

.kaiwa-text-left:before {
  position: absolute;
  content: '';
  border: 10px solid transparent;
  top: 10px;
  right: -20px;
}

.kaiwa-text-left:after {
  position: absolute;
  content: '';
  border: 10px solid transparent;
  border-left: 10px solid #9cd6e7;
  top: 10px;
  right: -19px;
}

/* 回り込み解除 */

.kaiwa:after, .kaiwa:before {
  clear: both;
  content: "";
  display: block;
}

#input_text {
  resize: none;
  float: left;
  margin-right: 10px;
}

#chat-box {
  height: 500px;
  overflow-y: scroll;
}

#record_btn, #submit_btn {
  margin-left: 10px;
  margin-bottom: 50px;
}

#record_btn, #submit_btn {
  border-radius: 25px;
  position: relative;
  display: inline-block;
  font-weight: bold;
  padding: 0.25em 0.5em;
  text-decoration: none;
  color: #FFF;
  background: #00bcd4;
  transition: .4s;
  border-bottom: solid 4px #627295;
}

#submit_btn:hover {
  background: #00ff00;
}

#record_btn:hover {
  background: #ff0000;
}

#record_btn:active {
  /*ボタンを押したとき*/
  -webkit-transform: translateY(4px);
  transform: translateY(4px);
  /*下に動く*/
  border-bottom: none;
  /*線を消す*/
}

#submit_btn:active {
  /*ボタンを押したとき*/
  -webkit-transform: translateY(4px);
  transform: translateY(4px);
  /*下に動く*/
  border-bottom: none;
  /*線を消す*/
}

::placeholder {
  color: #ff0000;
  opacity: 0.5;
  font-size: 18px;
}

/*トグル*/

input[type="checkbox"] {
    display: none;
}
input[type="checkbox"] + label.check {
    position: relative;
    cursor: pointer;
    display: inline-block;
    width: 60px;
    height: 28px;
    color: #969696;
    border: 1px solid #a3a3a3;
    border-radius: 3px;
    background-color: #ffffff;
}
input[type="checkbox"]:checked + label.check {
    border: 1px solid #4db4e4;
    background-color: #4db4e4;
}
input[type="checkbox"] + label.check::before {
    content: "OFF";
    position: absolute;
    top: 4px;
    left: auto;
    right: 6px;
}
input[type="checkbox"]:checked + label.check::before {
    content: "ON";
    position: absolute;
    left: 6px;
    right: auto;
    color: #ffffff;
}
input[type="checkbox"] + label.check > div {
    position: absolute;
    top: 2px;
    left: 2px;
    width: 12px;
    height: 22px;
    border: 1px solid #a3a3a3;
    border-radius: 3px;
    background-color: #ffffff;
    transition: 0.2s;
}
input[type="checkbox"]:checked + label.check > div {
    border: 1px solid transparent;
    left: 44px;
}

次はしりとりの処理のJavascriptのコードです。

siritoti.js(450行)
siritori.js
"use strict";
//PWA Start
if ("serviceWorker" in navigator) {
    navigator.serviceWorker.register("./serviceWorker.js")
        .then(
            function (registration) {
                if (typeof registration.update == "function") {
                    registration.update();
                }
            })
        .catch(function (error) {
            console.log("Error Log: " + error);
        });
}

if (navigator.onLine) {
    //オンライン
} else {
    //オフライン
    const reload = confirm("インターネットに接続してください\n再読み込みしますか?");
    if (reload) {
        location.reload(true);
    } else {
        window.addEventListener("online", (e) => {
            location.reload(true)
        })
        alert("インターネットにつながないとしりとりはできません。");
    }
}
window.addEventListener("offline", (e) => {
    //オフライン
    const reload = confirm("インターネットに接続してください\n再読み込みしますか?");
    if (reload) {
        location.reload(true);
    } else {
        window.addEventListener("online", (e) => {
            location.reload(true)
        })
        alert("インターネットにつながないとしりとりはできません。");
    }
})
//PWA End

//音声認識/合成の準備
const obj = document.getElementById("chat-box");
const SpeechRecognition = window.SpeechRecognition || webkitSpeechRecognition;
let speech;
const msg = new SpeechSynthesisUtterance();
msg.lang = "ja-JP"; //言語

let words = [];
let links = [];
let Word_history = ["しりとり"];
let cpu_word = "";
let next_word = "";
let IsWork = false;
const switchButton = $("#check");
const recordButton = $("#record_btn");
const recordButtonText = $("#record_btn_text");
const submitButton = $("#submit_btn");
const submitButtonText = $("#submit_btn_text");
const inputText = $("#input_text");
const chatBox = $("#chat-box");

if (SpeechRecognition !== undefined) {
    // ユーザのブラウザは音声認識に対応しています。
    speech = new SpeechRecognition();
    speech.lang = "ja-JP";
    speech.interimResults = true;
    recordButton.click(function () {
        // 音声認識をスタート
        if (!IsWork) {
            IsWork = true;
            recordButton.prop("disabled", true);
            submitButton.prop("disabled", true);
            recordButtonText.text("マイクで録音中");
            recordButton.css("background-color", "#ff0000");
            speech.start();
        }
    });
    speech.onnomatch = function () {
        console.log("認識できませんでした");
        say("認識できませんでした", chatBox)
        ResetUI();
        IsWork = false;
        inputText.attr("readonly", false);
    };
    speech.onerror = function () {
        console.log("認識できませんでした");
        say("認識できませんでした", chatBox)
        ResetUI();
        IsWork = false;
        inputText.attr("readonly", false);
    };
    //音声自動文字起こし機能
    speech.onresult = function (e) {
        if (!e.results[0].isFinal) {
            var speechtext = e.results[0][0].transcript
            console.log(speechtext)
            inputText.attr("readonly", true);
            inputText.val(speechtext);
            return;
        }

        recordButtonText.text("処理中");
        submitButtonText.text("処理中");
        submitButton.css("background-color", "#999999");
        recordButton.css("background-color", "#999999");
        console.log("リザルト")
        speech.stop();

        if (e.results[0].isFinal) {
            console.log("聞き取り成功!")
            var autotext = e.results[0][0].transcript
            console.log(e);
            console.log(autotext);//autotextが結果
            inputText.val(autotext);
            Submit(autotext)
        }
    }
} else {
    recordButton.click(function () {
        alert("このブラウザは音声認識に対応していません")
    })
    recordButton.prop("disabled", true);
    recordButtonText.text("非対応")
}

function Submit(text) {
    recordButton.prop("disabled", true);
    recordButtonText.text("処理中");
    submitButton.prop("disabled", true);
    submitButtonText.text("処理中");
    console.log("リザルト")
    console.log(text);//textが結果
    //ここから返答処理
    chatBox.html(chatBox.html() + "<div class=\"kaiwa\"><!–右からの吹き出し–><figure class=\"kaiwa-img-right\"><img src=\"./icons/human_icon.png\" alt=\"no-img2\"><figcaption class=\"kaiwa-img-description\">あなた</figcaption></figure><div class=\"kaiwa-text-left\"><p class=\"kaiwa-text\">「" + text + "」</p></div></div><!–右からの吹き出し 終了–>");
    obj.scrollTop = obj.scrollHeight;
    //処理が終わったら考え中の文字を削除し、結果を入れる
    if (next_word !== str_change(text, 1)[0]) {
        say("" + next_word + "」から言葉を始めてね!", chatBox);
        ResetUI();

    } else if (Word_history.indexOf(text) !== -1) {
        say("" + text + "」は、もう使われた言葉だよ!", chatBox);
        ResetUI();

    } else {
        Word_history.push(text);
        siritori(text).then(function (values) {
            let value = values[0];
            let link = values[1];
            // 非同期処理成功
            console.log(value);
            inputText.attr("placeholder", "" + str_change(value, -1)[0] + "」から始まる言葉");
            next_word = str_change(value, -1)[0]
            say("" + value + "", chatBox, link)
            Word_history.push(value);
            obj.scrollTop = obj.scrollHeight;

            if (switchButton.checked) {
                msg.text = value;
                speechSynthesis.speak(msg);
            }

            console.log("処理終了");
            ResetUI();
        }).catch(function (error) {
            // 非同期処理失敗。呼ばれない
            alert("error:Wikipedia api\n" + error)
            console.log(error);
            say("エラーが起きました", chatBox);
            ResetUI();
        }).finally(function () {
            IsWork = false;
        });
    }
}

function SubmitButtonClick() {
    submitButton.css("background-color", "#999999");
    recordButton.css("background-color", "#999999");
    var text = inputText.val();
    if (text === "") {
        ResetUI();
        return; //何もないなら関数を終了させる
    }
    Submit(text);

}

function ResetUI() {
    inputText.val("");
    recordButton.prop("disabled", false);
    submitButton.prop("disabled", false);
    recordButtonText.text("マイク");
    submitButtonText.text("送信");
    recordButton.css("background-color", "#00bcd4");
    submitButton.css("background-color", "#00bcd4");
}

submitButton.click(SubmitButtonClick)

function siritori(user_msg) {
    return new Promise(function (resolve, reject) {
        words = [];
        links = [];
        var changes = str_change(user_msg, -1)
        var taskA = new Promise(function (resolve) {
            WikipediaAPI(changes[0], resolve);
        });
        var taskB = new Promise(function (resolve) {
            WikipediaAPI(changes[1], resolve);
        });
        Promise.all([taskA, taskB]).then(function () {
            console.log(words);
            if (words === []) {
                say("負けました", chatBox);
                console.error("強すぎException")
                return;
            }
            let random = Math.floor(Math.random() * words.length);
            cpu_word = words[random];
            let wikiLink = links[random];
            if (str_change(cpu_word, -1)[0] === "") {
                do {
                    words.splice(words.indexOf(cpu_word), words.indexOf(cpu_word))
                    random = Math.floor(Math.random() * words.length);
                    cpu_word = words[random];
                    wikiLink = links[random];
                } while (str_change(cpu_word, -1)[0] === "")
                resolve([cpu_word, wikiLink]);

            } else {
                resolve([cpu_word, wikiLink]);
            }


        })
    });
}

function WikipediaAPI(query, end) {
    //API呼び出し
    console.log(query);
    $.ajax({
        type: "GET",
        timeout: 10000,
        dataType: "jsonp",
        url: "https://ja.wikipedia.org/w/api.php?format=json&action=query&list=prefixsearch&pssearch=" + query + "&pslimit=200&psnamespace=0",
        async: false,
        success: function (json) {
            console.log(json)
            json.query.prefixsearch.forEach(function (value) {
                if (value.title !== query) {
                    var word = value.title;
                    word = word.replace(/ *\([^)]*\) */g, "");
                    if (NG_word.indexOf(word.slice(-1)) === -1 && Word_history.indexOf(word) === -1) {
                        words.push(word);
                        links.push("http://ja.wikipedia.org/?curid=" + value.pageid);
                    }
                }
            });
            end();
        }
    });

}

//正規表現
const regex = /(?!\p{Lm})\p{L}|\p{N}/u;

/**
 * 引数ran
 1 先頭切り出し
 -1 末尾切り出し
 **/
function str_change(str, ran) {
    var range = ran
    if (range === 1) {
        range = [0, 1]
    } else {
        range = [-1, undefined]
    }
    const hiragana = ["", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "",
        "", "", "",
        "", "", "", "", "",
        "",
        "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
    ]
    const katakana = ["", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "",
        "", "", "",
        "", "", "", "", "",
        "",
        "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",]
    let r = [];
    let word = str;

    //しりとり処理除外対象が二回連続でも対応

    //const del_str =func_str.slice(range[0], range[1]) != "ー" && func_str.slice(range[0], range[1]) != "-" && func_str.slice(range[0], range[1]) != "!" && func_str.slice(range[0], range[1]) != "?" && func_str.slice(range[0], range[1]) != "!" && func_str.slice(range[0], range[1]) != "?" && func_str.slice(range[0], range[1]) != "〜" && func_str.slice(range[0], range[1]) != "、"&&func_str.slice(range[0], range[1]) != "。"&&func_str.slice(range[0], range[1]) != "."


    if (hiragana.indexOf(word.slice(range[0], range[1])) !== -1) {//ひらがな
        r.push(word.slice(range[0], range[1]));
        r.push(katakana[hiragana.indexOf(word.slice(range[0], range[1]))]);
        console.log(r)
    } else if (katakana.indexOf(str.slice(range[0], range[1])) !== -1) {//カタカナ
        r.push(hiragana[katakana.indexOf(word.slice(range[0], range[1]))]);
        r.push(word.slice(range[0], range[1]));
        console.log(r)
    } else {//漢字
        $.ajax({
            type: "POST",
            timeout: 10000,
            url: "https://labs.goo.ne.jp/api/hiragana",
            async: false,
            "headers": {
                "Content-Type": "application/json",
            },
            data: JSON.stringify({
                "app_id": "xxxxxxxxxxxxxx",
                "sentence": word,
                "output_type": "hiragana"
            }),
        }).done(function (data) {
            word = data.converted;
            if (range[0] === -1) {
                if (!regex.test(word.slice(-1))) {
                    do {
                        word = word.slice(0, word.length - 1)
                    } while (!regex.test(word.slice(-1)))
                    word = word.slice(-1)
                } else {
                    word = word.slice(-1);
                }
            }

            r.push(word.slice(range[0], range[1]));// ひらがなを rに追加
            r.push(katakana[hiragana.indexOf(word.slice(range[0], range[1]))]);//カタカナをr に追加
            console.log(r)
        });
    }
    switch (r[0]) {//小文字変換 ひらがな もっといい方法あるかな?
        case "":
            r[0] = "";
            break;
        case "":
            r[0] = "";
            break;
        case "":
            r[0] = "";
            break;
        case "":
            r[0] = "";
            break;
        case "":
            r[0] = "";
            break;
        case "":
            r[0] = "";
            break;
        case "":
            r[0] = "";
            break;
        case "":
            r[0] = "";
            break;
        case "":
            r[0] = "";
            break;
        case "":
            r[0] = "";
            break;
        case "":
            r[0] = "";
    }
    switch (r[1]) {//小文字変換 カタカナ
        case "":
            r[1] = "";
            break;
        case "":
            r[1] = "";
            break;
        case "":
            r[1] = "";
            break;
        case "":
            r[1] = "";
            break;
        case "":
            r[1] = "";
            break;
        case "":
            r[1] = "";
            break;
        case "":
            r[1] = "";
            break;
        case "":
            r[1] = "";
            break;
        case "":
            r[1] = "";
            break;
        case "":
            r[1] = "";
            break;
        case "":
            r[1] = "";
            break;
        case "":
            r[1] = "";
            break;
        case "":
            r[1] = "";
    }
    console.log(r);
    return r;
}
function say(text, element, link) {// Sayする関数
    if (link === undefined) {
        element.html(`${element.html()}<div class="kaiwa"><figure class="kaiwa-img-left"><img src="./icons/Wikipedia-logo-v2-ja.png" alt="no-img2"><figcaption class="kaiwa-img-description">しりとり AI</figcaption></figure><div class="kaiwa-text-right"><p class="kaiwa-text">${text}</p></div></div>`)
    } else {
        element.html(`${element.html()}<div class="kaiwa"><figure class="kaiwa-img-left"><img src="./icons/Wikipedia-logo-v2-ja.png" alt="no-img2"><figcaption class="kaiwa-img-description">しりとり AI</figcaption></figure><div class="kaiwa-text-right"><p class="kaiwa-text"><a href="${link}">${text}</a></p></div></div>`)
    }
    obj.scrollTop = obj.scrollHeight;
}

とても長くなりました!(普段JSを触らないので基準はわかりませんが)

解説

このしりとりAIはなんと音声認識ができます!
しかし、SpeechRecognitionはまだブラウザが完全に対応してないのでバグが多いです。
(Windows版Chrome 80.0.3987.149 では結構しっかり動いた)
なのでtextareaから入力することをお勧めします。

siritori.jsではtextareaまたは音声認識で入力が来たら、まず前の言葉の最後の文字と頭文字が同じかチェックし、そのあとその言葉が今までに出たかをチェックします。

if (next_word != str_change(text, 1)[0]) {
//      ・・・略・・・
} else if (Word_history.indexOf(text) != -1) {
//      ・・・略・・・
}

その二つの条件をクリアしたら
関数siritori()を呼び、その中でWikipediaのAPIからしりとりの条件にある言葉を取得して返答しています。
関数WikipediaAPI()の中の配列NG_wordには「ん」で読み仮名が終わる漢字など返答の候補に入れる際、除外してほしい文字が入っています。
関数str_change()は言葉をひらがなや、カタカナに変換し、最後や最初の一文字を返します。
「ー」や「!」などが最後の一文字の場合はその一つ前の文字を、
「ぁ」や「ゃ」などの小さい文字は「あ」や「や」に変換してくれます。

ひらがな化APIの部分にはgooラボAPI利用登録からgithubで登録してappidを取得して、str_change()のapp_idを書き換えてください。

function str_change(str, ran) {
    //  省略
     else {//漢字
        $.ajax({
            type: 'POST',
            timeout: 10000,
            url: "https://labs.goo.ne.jp/api/hiragana",
            async: false,
            'headers': {
                'Content-Type': "application/json",
            },
            data: JSON.stringify({
                'app_id': 'xxxxxxxxxxxxxx',// <=ここを書き換える
                'sentence': func_str,
                'output_type': 'hiragana'
            }),
        })
//   省略

こんなコードを書くとWikipediaの頭脳を持ったしりとりAIが完成します。

なんと!このAIは最後の文字を「ん」にしても言葉を返してきます!

遊んでみてください!

しりとり AI
バグがあったら教えてくれると助かります!

GitHubリポジトリ

6
6
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6