はじめに
記事投稿キャンペーンの賞品のポータブル電源が欲しい…!!!
と、いうことでついに追加されたMarkdown AIのサーバーAI機能を使って、昔話風の物語を作ってくれる&朗読してくれるWebアプリを公開したので紹介します!
気になったらここからアクセス!
↓↓↓
どんな技術を使ってるの?
Markdown AIにJavaScriptを組み合わせ、ブラウザ標準のWeb Speech APIを利用することで実現しました
Markdown AIでは、サーバーAIを利用する際にJavaScriptを使っており、この部分に独自のコードを入れ込みました
Web Speech APIはW3Cで策定されているWeb標準APIで、近年のモダンなブラウザならばほとんどの端末で利用でき、読み上げだけでなく音声認識も行うことができます
特徴・こだわった点
Markdownで書いたときと同じUI
Markdown AIで挿入できる「サーバーAIを利用するためのスクリプト」(前項画像参照)では、LLMが生成したテキストを、div要素の中に直接入れて表示するようになっています
つまりLLMにMarkdownで出力するように指示していると、タイトルが「#」から始まるようなテキストがただ表示されるだけのWebアプリになってしまいます
このWebアプリでは、最低限のMarkdown記法をMarkdown AIがhtmlでの表示に利用しているタグに変換してあげることで、デザイン性を確保しました
変換スクリプト
function markdownToHtml(markdown) {
// HTML出力を格納する変数
let html = "";
// Markdownテキストを行ごとに分割
const lines = markdown.split("\n");
lines.forEach((line) => {
// 見出し1 (# ) -> <h1 class="is-size-1">
if (/^# (.+)/.test(line)) {
const content = line.match(/^# (.+)/)[1];
html += `<h1 class="is-size-1">${content}</h1>`;
}
// 見出し2 (## ) -> <h2 class="is-size-2">
else if (/^## (.+)/.test(line)) {
const content = line.match(/^## (.+)/)[1];
html += `<h2 class="is-size-2">${content}</h2>`;
}
// 段落 -> <p>
else if (/^(.+)$/.test(line) && !/^#/.test(line)) {
const content = line.match(/^(.+)$/)[1];
html += `<p>${content}</p>`;
}
// 空行 -> <p style="margin-bottom: 1.35rem"></p>
else if (/^\s*$/.test(line)) {
html += `<p style="margin-bottom: 1.35rem"></p>`;
}
// 水平線 (---) -> <hr class="horizontal-rule">
else if (/^---$/.test(line)) {
html += `<hr class="horizontal-rule">`;
}
});
return html;
}
文章の読み上げ
昔話と言えば朗読だろう、ということで、Web Speech APIを利用した読み上げ機能を実装しました
導入に外部ライブラリ等を必要としないので、Markdown AIでも利用することができました
Markdown AIの良かったところ
公開しているページのリンクが変わらない
一度公開したページは、内容を編集してもリンクが変わらないので、ページを共有する際に使い勝手が良いなと思いました
即時更新
編集画面で保存ボタンをクリックすると、すぐに公開中のページの内容が切り替わるので、更新待ちの時間がなくて便利でした
Markdown AIの改善できそうなところ
エディタ画面
エディタがhtmlのtextarea要素そのままなので、段落やTabなどの表示が凄く見にくいです
Qiitaのようなリッチなエディタになると使いやすくなるかなと思います
作成したMarkdown(JavaScript)
折りたためます
# 昔話生成Bot
##### あなたの好きなテーマ・登場人物で昔話風の物語を作ろう!
---
<div style="display: inline-block; margin-bottom: 30px;">
<h5>テーマを入力</h5>
<input type="text" id="text-1732073162"
style="width: 200px; height: 30px; border: 0.1svw solid #000; border-radius: 0.5svw;" value="">
<button type="button" id="button-1732073162"
style="text-align: center; width: 75px; height: 30px; background-color: white; border: 0.1svw solid #000; border-radius: 0.5svw;">生成開始</button>
<button type="button" id="startstopSpeech"
style="text-align: center; width: 75px; height: 30px; background-color: white; border: 0.1svw solid #000; border-radius: 0.5svw;">朗読開始</button>
<button type="button" id="resetSpeech"
style="text-align: center; width: 95px; height: 30px; background-color: white; border: 0.1svw solid #000; border-radius: 0.5svw;">朗読リセット</button>
</div>
<div id="answer-1732073162"></div>
<script>
(() => {
function markdownToHtml(markdown) {
// HTML出力を格納する変数
let html = "";
// Markdownテキストを行ごとに分割
const lines = markdown.split("\n");
lines.forEach((line) => {
// 見出し1 (# ) -> <h1 class="is-size-1">
if (/^# (.+)/.test(line)) {
const content = line.match(/^# (.+)/)[1];
html += `<h1 class="is-size-1">${content}</h1>`;
}
// 見出し2 (## ) -> <h2 class="is-size-2">
else if (/^## (.+)/.test(line)) {
const content = line.match(/^## (.+)/)[1];
html += `<h2 class="is-size-2">${content}</h2>`;
}
// 段落 -> <p>
else if (/^(.+)$/.test(line) && !/^#/.test(line)) {
const content = line.match(/^(.+)$/)[1];
html += `<p>${content}</p>`;
}
// 空行 -> <p style="margin-bottom: 1.35rem"></p>
else if (/^\s*$/.test(line)) {
html += `<p style="margin-bottom: 1.35rem"></p>`;
}
// 水平線 (---) -> <hr class="horizontal-rule">
else if (/^---$/.test(line)) {
html += `<hr class="horizontal-rule">`;
}
});
return html;
}
var answer = "";
const button = document.getElementById('button-1732073162');
var isSpeechingNow = false;
var isSpeechPause = false;
const speech = document.getElementById('startstopSpeech');
const reset = document.getElementById('resetSpeech');
button.addEventListener('click', async event => {
button.disabled = true;
const serverAi = new ServerAI();
const message = document.getElementById('text-1732073162').value;
answer = await serverAi.getAnswerText('hxyNtBuxZrDopufEacW8Vu', '', message);
document.getElementById('answer-1732073162').innerHTML = markdownToHtml(answer);
button.disabled = false;
speechSynthesis.cancel();
isSpeechingNow = false;
isSpeechPause = false;
speech.innerHTML = "朗読開始";
});
speech.addEventListener('click', async event => {
if (!'speechSynthesis' in window) {
alert("このブラウザは音声合成に対応していません")
}
if (!isSpeechingNow && !isSpeechPause) {
isSpeechingNow = true;
const uttr = new SpeechSynthesisUtterance(answer.replaceAll("#", ""));
speechSynthesis.speak(uttr);
speech.innerHTML = "一時停止";
uttr.onend = () => {
speech.innerHTML = "朗読開始";
isSpeechingNow = false;
isSpeechPause = false;
};
} else if (isSpeechingNow && !isSpeechPause) {
speechSynthesis.pause();
isSpeechingNow = false;
isSpeechPause = true;
speech.innerHTML = "再生再開";
} else if (!isSpeechingNow && isSpeechPause) {
speechSynthesis.resume();
isSpeechingNow = true;
isSpeechPause = false;
speech.innerHTML = "一時停止";
}
});
reset.addEventListener('click', async event => {
if (isSpeechingNow || isSpeechPause) {
speechSynthesis.cancel();
isSpeechingNow = false;
isSpeechPause = false;
speech.innerHTML = "朗読開始";
}
});
/* タブが非アクティブになったら再生を停止するスクリプト */
/*document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
speechSynthesis.pause();
isSpeechingNow = false;
isSpeechPause = true;
speech.innerHTML = "再生再開";
}
});*/
})();
</script>
サーバーAIに指定したプロンプト
あなたは日本の昔話研究家 兼 脚本家です。
いま、プロンプトが与えられます。
クライアントがどのような物語を期待しているかを想定し、各パートごとにタイトルをつけながら、Markdownで出力してください。
今後の展望・おわりに
Markdown AIのサーバーAIでは、「Gemini」・「Claude」・「GPT-4o mini」・「GPT-3.5 Turbo」・「GPT-4」の5種類のLLMを利用できますが、今回はGPT-4o miniで実装しました(理由は特になし)
それぞれのモデルに特徴・癖があるので、モデルを切り替えられるようにして、同じお題を出した時にどんな物語を生成するのかを比較できるようにすると面白いかなと思っています
最後までお読みいただきありがとうございました!
いいね・コメント頂けると大変喜びます!
ついでに「昔話生成Bot」にアクセス!
参考文献
Web Speech APIについて