卒業制作プロダクト(↓)のフロントのイメージを掴むために、GitHubで見つけた「Yoube Search」をベースに改造してみます。
完成したサイトはこちらです。(音声からしか検索できないバグあり。。。後述します(´・ω・`))
Youtube APIを取得する
動作にはAPIが必要なので、以下のURLを参考にして取得しました。
改造元の挙動を確認する
こんな感じで検索単語を入力して、「Published Date」にプルダウンで変更して「Submit」を押すと古い順に出力できました。
音声入力を可能にする
今週学んだ「Web Speech API」を組み込んで音声で検索単語を入力できるようにしてみます。
授業で学んだVue.jsを移植しようとしましたが大本がjQueryで書かれており、うまく移植できなかったので仕切り直し。
以下URLを参考に「start」と「stop」を組み込んでみました。
音声入力した値で検索してくれるようになりました。
あとはデフォルトのソートが「None」だったので「Published Date」をデフォルトに変更しています。
課題
・文字入力欄(「Enter Channel, playlist or video」と書かれてるところ)に音声で入力したテキストを逆輸入したかったのですが時間切れでした。
・改造する前からなのですが、Submitを2回押さないとちゃんと「Published Date」順に表示されません。。
デプロイ
Netlifyへデプロイします。
使ってもらってフィードバックをもらう
嫁(非IT業界)に使ってもらいました。
●こんな反応を期待しているという期待値
・スマホで文字入力するのは面倒なので音声入力は助かる。
●実際に触ってもらってどうだったか
・音声入力するのに「start」ぼたんを押さないといけないんだね
・英語なのは何か意図があるの?
・このプルダウンの「10」ってなあに?
・入力したのと違う結果が出る気がする
・入力内容を変更して「Submit」押しても更新されない
・音声入力がご認識されたときの訂正、削除方法がわからない
・音声入力の際に、STOPもおさないといけないのはめんどい……。
・英語わからないから困る
・音声入力だと検索できたけど、文字入力欄があるにも関わらず、文字入力で検索ができないとか困る
・っていうか公式で音声検索できるよ?
・言葉を音声認識してテキスト入力欄に入るから、文字の修正もできるし、上書きもできるよ
・……あれ? っていうかこの検索メニューは何のために作ったんです??
・音声入力させた際、「Stop」押さなくても「Submit」できる上に、音声認識は生きてるままだから、音声入力画面が大変なことになるよ!?
・音声認識の削除訂正ができないから、再検索もできない気がします(`・ω・´)
フィードバックに基づいたアプリの改善案
・「テキスト検索枠」に入力した値を「音声入力枠」で上書きしてしまうので、発話していないと**「音声入力枠(NULL)」で検索されて、「テキスト検索枠」が意味をなしていない**(ので変な結果が出た)。 → 設計不足とテスト不足。短時間で組むならテキスト入力欄はいっそ消してしまったほうが良かった。時間があればYoutube公式は「音声入力」で「テキスト検索枠」に文字列が入るのでそちらを参考にしたい。
・「stop」ボタンで音声入力を止めるのは面倒 → 発話が止まったら入力終了とするオプションを使う。
・一度音声入力するとサイトをリロードしないと初期化できない → 「start」を押したときに追記型になっているので、一度値を消して再度入力するようにする。
・表示件数が「10」である、といった説明が不足している。 → 追加する。
・英語なのでわかりにくい。 → 日本語にする。
まとめ
自分だったらKKD(勘と経験と度胸)で適当になんとなくで使うところが軒並み指摘されましたw
でもこれが一般人の反応なんだろうな、と思います(´・ω・`)
あとは「Submit」「Published Date」くらいいいかとそのままにしてましたが、いっそ英語が何もないレベルで日本語に変えたほうが一般向けには良さそうです。
このあたりは卒業制作に活かせそうです。
自分の期待値との差ですが、「音声入力より手打ち入力が慣れている」人の場合、”手が離せない”シチュエーションでの操作がメインで想定されるわけでもない限り音声入力のメリットはあまりないのかな、と思いました。
開発中の記事
コード
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1" name="viewport">
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js">
</script>
<link href="assets/css/styles.css" rel="stylesheet">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js">
</script>
<link href="assets/images/favicon.ico" rel="icon" sizes="16x16" type="image/png">
<title>Yoube Search</title>
</head>
<body>
<div class="jumbotron">
<div class="container">
<h1><code>Yoube Search</code></h1><br>
音声入力 <button id="start-btn">start</button> <button id="stop-btn">stop</button>
<div id="result-div"></div>
<form id="searchForm" name="searchForm">
<div class="container" id="search">
<div class="row">
<div class="col-sm-5">
<div class="input-group input-group-lg queryArea">
<input autocomplete="on" class="center-block form-control" id="query" placeholder="Enter Channel, playlist or video" type="text">
</div>
</div>
<div class="col-sm-1 qCount">
<div class="form-group">
<select class="form-control" id="resp">
<option>
10
</option>
<option>
20
</option>
<option>
30
</option>
<option>
40
</option>
<option>
50
</option>
</select>
</div>
</div>
<div class="col-sm-2 qCount">
<div class="form-group">
<select class="form-control" id="sort">
<option value="publishedAt">
Published Date
</option>
<option value="title">
Title
</option>
<option value="null">
None
</option>
</select>
</div>
</div>
<div class="col-sm-2" id="submit">
<input class="btn btn-primary" id="submit" type="submit" value="Submit">
</div>
</div><br>
</div>
</form>
</div>
</div>
<div class="container">
<img id="load" src="assets/images/ripple.gif">
<ul id="results"></ul>
</div>
<script src="assets/js/scripts.js">
</script>
</body>
</html>
var API_KEY = '自分のAPIキー'
$('#search-form').submit(function(e) {
e.preventDefault();
});
$('#submit').on('click', (e) => {
e.preventDefault();
return search();
});
function search() {
$('#results').html('');
$('#buttons').html('')
query = $('#query').val();
count = $('#resp').val();
sort = $('#sort').val();
query = finalTranscript;
if (this.q == 'undefined') {
this.q = query;
this.s = sort;
}
if (this.q != query || this.c != count) {
$('#load').show();
results = [];
this.q = query;
this.c = count;
$.get('https://www.googleapis.com/youtube/v3/search', {
part: 'snippet, id',
q: query,
maxResults: count,
key: API_KEY
}, function(response) {
$('#load').hide();
$.each(response.items, (i, item) => {
let output = getJson(item);
results.push(output);
});
displayResults(results);
});
} else if (sort != "null") {
this.results = sortResults(this.results, sort);
displayResults(this.results);
} else if (sort == "null") {
displayResults(this.results);
}
}
function sortResults(results, attr) {
sortedResults = []
for (i = results.length - 1; i >= 0; --i) {
for (j = 0; j < i; j++) {
if (attr == 'title') {
if (results[j].title.localeCompare(results[j + 1].title) > 0) {
temp = results[j];
results[j] = results[j + 1];
results[j + 1] = temp;
}
} else if (attr == 'publishedAt') {
if (results[j].publishedAt.localeCompare(results[j + 1].publishedAt) > 0) {
temp = results[j];
results[j] = results[j + 1];
results[j + 1] = temp;
}
}
}
}
return results
}
function displayResults(results) {
$('#buttons').html('')
for (each = 0; each < results.length; each++) {
$('#results').append(getString(results[each]));
}
}
function getString(output) {
return `<li>
<div class ="list-left">
<a href="${output.href}"><img src="${output.thumb}"></a>
</div>
<div class="list-right">
<a href="${output.href}"><h3>${output.title}</h3></a>
<p>By <span>${output.channelTitle}</span> on ${output.publishedAt}</p>
<p>${output.description}</p>
</div>
`
}
function getJson(item) {
var object = new Object();
object["channelTitle"] = item.snippet.channelTitle;
object["href"] = "https://youtube.com/"
if (item.id.kind == "youtube#channel") {
object["href"] = object["href"] + "channel/" + item.id.channelId
} else if (item.id.kind == "youtube#video") {
object["href"] = object["href"] + "watch?v=" + item.id.videoId
} else if (item.id.kind == "youtube#playlist") {
tumb_id = item.snippet.thumbnails.default.url.split('/')[4]
object["href"] = object["href"] + "watch?v=" + tumb_id + "&list=" + item.id.playlistId
}
object["title"] = item.snippet.title;
object["thumb"] = item.snippet.thumbnails.high.url;
object["publishedAt"] = item.snippet.publishedAt;
object["description"] = item.snippet.description;
return object
}
//以下WebSpeechAPI用
const startBtn = document.querySelector('#start-btn');
const stopBtn = document.querySelector('#stop-btn');
const resultDiv = document.querySelector('#result-div');
SpeechRecognition = webkitSpeechRecognition || SpeechRecognition;
let recognition = new SpeechRecognition();
recognition.lang = 'ja-JP';
recognition.interimResults = true;
recognition.continuous = true;
let finalTranscript = ''; // 確定した(黒の)認識結果
recognition.onresult = (event) => {
let interimTranscript = ''; // 暫定(灰色)の認識結果
for (let i = event.resultIndex; i < event.results.length; i++) {
let transcript = event.results[i][0].transcript;
if (event.results[i].isFinal) {
finalTranscript += transcript;
} else {
interimTranscript = transcript;
}
}
resultDiv.innerHTML = finalTranscript + '<i style="color:#ddd;">' + interimTranscript + '</i>';
}
startBtn.onclick = () => {
recognition.start();
}
stopBtn.onclick = () => {
recognition.stop();
}