Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions components/notice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from "react";
import Snackbar from "@material-ui/core/Snackbar";
import MuiAlert from "@material-ui/lab/Alert";
import { bool, node, string, func } from "prop-types";

const Notice = props => (
<Snackbar open={props.open} autoHideDuration={6000} onClose={props.onClose}>
<MuiAlert
elevation={6}
variant="filled"
onClose={props.onClose}
severity={props.severity}
>
{props.children}
</MuiAlert>
</Snackbar>
);

Notice.propTypes = {
open: bool,
severity: string,
onClose: func,
children: node
};

export default Notice;
45 changes: 45 additions & 0 deletions components/tagField.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from "react";
import Chip from "@material-ui/core/Chip";
import Autocomplete from "@material-ui/lab/Autocomplete";
import TextField from "@material-ui/core/TextField";
import { bool, func, array, string } from "prop-types";

const TagField = props => (
<Autocomplete
disabled={props.disabled}
multiple
id="tags-filled"
options={props.options}
freeSolo
defaultValue={props.defaultValue}
onChange={(event, values) => {
props.onTagChange(values);
}}
renderTags={(value, getTagProps) =>
value.map((option, index) => (
<Chip variant="outlined" label={option} {...getTagProps({ index })} />
))
}
renderInput={params => {
return (
<TextField
{...params}
variant="outlined"
label={props.label}
placeholder={props.placeholder}
/>
);
}}
/>
);

TagField.propTypes = {
options: array,
defaultValue: array,
disabled: bool,
onTagChange: func,
label: string,
placeholder: string
};

export default TagField;
19 changes: 19 additions & 0 deletions components/transcriptField.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from "react";
import { bool, string } from "prop-types";

const TranscriptField = props => (
<p>
{props.finalText}
<span style={{ color: props.isMatch ? "#f00" : "#aaa" }}>
{props.transcript}
</span>
</p>
);

TranscriptField.propTypes = {
isMatch: bool,
finalText: string,
transcript: string
};

export default TranscriptField;
45 changes: 45 additions & 0 deletions components/uploadButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from "react";
import IconButton from "@material-ui/core/IconButton";
import LibraryMusicIcon from "@material-ui/icons/LibraryMusic";
import { bool, func, string } from "prop-types";

const UploadButton = props => (
<div>
<input
accept={`${props.fileType}/*`}
id="file-input"
multiple
type="file"
style={{ display: "none" }}
onChange={event => {
const file = event.target.files[0];
if (!(file instanceof File)) return;
if (file.type.indexOf(props.fileType) === -1) {
props.onInvalidFileError();
return;
}
props.onFileChange(file);
}}
/>
<label htmlFor="file-input">
<IconButton
color="primary"
size="large"
disabled={props.disabled}
aria-label="upload audio"
component="span"
>
<LibraryMusicIcon />
</IconButton>
</label>
</div>
);

UploadButton.propTypes = {
onFileChange: func,
onInvalidFileError: func,
fileType: string,
disabled: bool
};

export default UploadButton;
156 changes: 52 additions & 104 deletions pages/index.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,41 @@
import React, { useState, useEffect, useRef } from "react";
import Link from "next/link";
import Head from "../components/head";
import Container from "@material-ui/core/Container";
import Chip from "@material-ui/core/Chip";
import Autocomplete from "@material-ui/lab/Autocomplete";
import TextField from "@material-ui/core/TextField";
import Snackbar from "@material-ui/core/Snackbar";
import MuiAlert from "@material-ui/lab/Alert";
import Box from "@material-ui/core/Box";
import Grid from "@material-ui/core/Grid";
import Button from "@material-ui/core/Button";
import IconButton from "@material-ui/core/IconButton";
import LibraryMusicIcon from "@material-ui/icons/LibraryMusic";
import Notice from "../components/notice";
import UploadButton from "../components/uploadButton";
import TagField from "../components/tagField";
import TranscriptField from "../components/transcriptField";

const Home = () => {
// 音声認識インスタンス
const recognizerRef = useRef();
const inputRef = useRef();
const [finalText, setFinalText] = useState("");
const [transcript, setTranscript] = useState("ボタンを押して検知開始");
const initialTagValues = ["年収"];
const [tagValues, setTagValues] = useState(initialTagValues);
const [detecting, setDetecting] = useState(false);
const candidates = ["年収", "自由", "成功"];
const [alertOpen, setAlertOpen] = useState(false);
const [fileLoaded, setFileLoaded] = useState(false);
const [userMusic, setUserMusic] = useState(null);
const [userMusicName, setUserMusicName] = useState("");
// スナックバー表示
const [alertOpen, setAlertOpen] = useState(false); // 自慢検知アラート
const [fileLoaded, setFileLoaded] = useState(false); // ファイル読み込み完了
// 音声認識
const [detecting, setDetecting] = useState(false); // 音声認識ステータス
const [finalText, setFinalText] = useState(""); // 確定された文章
const [transcript, setTranscript] = useState("ボタンを押して検知開始"); // 認識中の文章
// 単語検知
const initialTagValues = ["年収"]; // デフォルト検知単語
const candidates = ["年収", "自由", "成功"]; // 検知単語候補
const [tagValues, setTagValues] = useState(initialTagValues); // 検知単語一覧
// 効果音
const [userMusic, setUserMusic] = useState(null); // ユーザー追加音
const [userMusicName, setUserMusicName] = useState(""); // ファイル名

useEffect(() => {
const music = new Audio("/static/warning01.mp3");
const music = new Audio("/static/warning01.mp3"); // デフォルト音
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

デプロイ時にAudioインスタンスでエラーが出たから、useEffect内に含めて必ずクライアントのみで実行するようにした。

// NOTE: Web Speech APIが使えるブラウザか判定
// https://developer.mozilla.org/ja/docs/Web/API/Web_Speech_API
if (!window.SpeechRecognition && !window.webkitSpeechRecognition) {
alert("お使いのブラウザには未対応です");
return;
}
// NOTE: 将来的にwebkit prefixが取れる可能性があるため
const SpeechRecognition =
window.SpeechRecognition || window.webkitSpeechRecognition;
recognizerRef.current = new SpeechRecognition();
Expand All @@ -49,12 +52,15 @@ const Home = () => {
[...event.results].slice(event.resultIndex).forEach(result => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

event.resultsは俗に言う連想配列みたいな形をしてたから配列に変換して反復処理している。

const transcript = result[0].transcript;
if (result.isFinal) {
// 音声認識が完了して文章が確定
setFinalText(prevState => {
return prevState + transcript;
});
setTranscript("");
} else {
// 音声認識の途中経過
if (tagValues.some(value => transcript.includes(value))) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

認識した文章に検知する単語が含まれていれば

// NOTE: ユーザーが効果音を追加しなければデフォルトを鳴らす
(userMusic || music).play();
setAlertOpen(true);
}
Expand All @@ -66,124 +72,66 @@ const Home = () => {

return (
<div>
<Head title="Home" />
<Snackbar
<Head title="自慢ディテクター" />
<Notice
open={alertOpen}
autoHideDuration={6000}
severity="error"
onClose={() => {
setAlertOpen(false);
}}
>
<MuiAlert
elevation={6}
variant="filled"
onClose={() => {
setAlertOpen(false);
}}
severity="error"
>
自慢を検知しました
</MuiAlert>
</Snackbar>
<Snackbar
自慢を検知しました
</Notice>
<Notice
open={fileLoaded}
autoHideDuration={6000}
severity="success"
onClose={() => {
setFileLoaded(false);
}}
>
<MuiAlert
elevation={6}
variant="filled"
onClose={() => {
setFileLoaded(false);
}}
severity="success"
>
{userMusicName}を読み込みました
</MuiAlert>
</Snackbar>
{userMusicName}を読み込みました
</Notice>
<Container>
<Grid container alignItems="center" justify="center">
<Grid item>
<img src="/static/logo.png" height="200px" />
<img src="/static/logo.png" height="200px" alt="自慢ディテクター" />
</Grid>
</Grid>
<Box fontSize={25}>
<p>
{finalText}
<span style={{ color: alertOpen ? "#f00" : "#aaa" }}>
{transcript}
</span>
</p>
<div id="result-div"></div>
<TranscriptField
finalText={finalText}
transcript={transcript}
isMatch={alertOpen}
/>
</Box>
<Grid container spacing={2}>
<Grid item xs={11}>
<Autocomplete
<TagField
disabled={detecting}
multiple
id="tags-filled"
options={candidates}
freeSolo
defaultValue={initialTagValues}
onChange={(event, values) => {
label="反応する単語"
placeholder="単語を追加 +"
onTagChange={values => {
setTagValues(values);
}}
renderTags={(value, getTagProps) =>
value.map((option, index) => (
<Chip
variant="outlined"
label={option}
{...getTagProps({ index })}
/>
))
}
renderInput={params => {
return (
<TextField
{...params}
variant="outlined"
label="反応する単語"
placeholder="単語を追加 +"
/>
);
}}
/>
</Grid>
<Grid item>
<input
ref={inputRef}
accept="audio/*"
id="file-input"
multiple
type="file"
style={{ display: "none" }}
onChange={event => {
const file = event.target.files[0];
if (!(file instanceof File)) return;
if (file.type.indexOf("audio") === -1) {
alert("オーディオファイルを選択してください");
return;
}
<UploadButton
disabled={detecting}
fileType="audio"
onFileChange={file => {
const src = window.URL.createObjectURL(file);
const audio = new Audio(src);
setUserMusic(audio);
setUserMusicName(file.name);
setFileLoaded(true);
}}
onInvalidFileError={() => {
alert("オーディオファイルを選択してください");
}}
/>
<label htmlFor="file-input">
<IconButton
color="primary"
size="large"
disabled={detecting}
aria-label="upload audio"
component="span"
>
<LibraryMusicIcon />
</IconButton>
</label>
</Grid>
</Grid>
<Box m={2}>
Expand Down