11import React , { useState , useEffect , useRef } from "react" ;
2- import Link from "next/link" ;
32import Head from "../components/head" ;
43import Container from "@material-ui/core/Container" ;
5- import Chip from "@material-ui/core/Chip" ;
6- import Autocomplete from "@material-ui/lab/Autocomplete" ;
7- import TextField from "@material-ui/core/TextField" ;
8- import Snackbar from "@material-ui/core/Snackbar" ;
9- import MuiAlert from "@material-ui/lab/Alert" ;
104import Box from "@material-ui/core/Box" ;
115import Grid from "@material-ui/core/Grid" ;
126import Button from "@material-ui/core/Button" ;
13- import IconButton from "@material-ui/core/IconButton" ;
14- import LibraryMusicIcon from "@material-ui/icons/LibraryMusic" ;
7+ import Notice from "../components/notice" ;
8+ import UploadButton from "../components/uploadButton" ;
9+ import TagField from "../components/tagField" ;
10+ import TranscriptField from "../components/transcriptField" ;
1511
1612const Home = ( ) => {
13+ // 音声認識インスタンス
1714 const recognizerRef = useRef ( ) ;
18- const inputRef = useRef ( ) ;
19- const [ finalText , setFinalText ] = useState ( "" ) ;
20- const [ transcript , setTranscript ] = useState ( "ボタンを押して検知開始" ) ;
21- const initialTagValues = [ "年収" ] ;
22- const [ tagValues , setTagValues ] = useState ( initialTagValues ) ;
23- const [ detecting , setDetecting ] = useState ( false ) ;
24- const candidates = [ "年収" , "自由" , "成功" ] ;
25- const [ alertOpen , setAlertOpen ] = useState ( false ) ;
26- const [ fileLoaded , setFileLoaded ] = useState ( false ) ;
27- const [ userMusic , setUserMusic ] = useState ( null ) ;
28- const [ userMusicName , setUserMusicName ] = useState ( "" ) ;
15+ // スナックバー表示
16+ const [ alertOpen , setAlertOpen ] = useState ( false ) ; // 自慢検知アラート
17+ const [ fileLoaded , setFileLoaded ] = useState ( false ) ; // ファイル読み込み完了
18+ // 音声認識
19+ const [ detecting , setDetecting ] = useState ( false ) ; // 音声認識ステータス
20+ const [ finalText , setFinalText ] = useState ( "" ) ; // 確定された文章
21+ const [ transcript , setTranscript ] = useState ( "ボタンを押して検知開始" ) ; // 認識中の文章
22+ // 単語検知
23+ const initialTagValues = [ "年収" ] ; // デフォルト検知単語
24+ const candidates = [ "年収" , "自由" , "成功" ] ; // 検知単語候補
25+ const [ tagValues , setTagValues ] = useState ( initialTagValues ) ; // 検知単語一覧
26+ // 効果音
27+ const [ userMusic , setUserMusic ] = useState ( null ) ; // ユーザー追加音
28+ const [ userMusicName , setUserMusicName ] = useState ( "" ) ; // ファイル名
2929
3030 useEffect ( ( ) => {
31- const music = new Audio ( "/static/warning01.mp3" ) ;
31+ const music = new Audio ( "/static/warning01.mp3" ) ; // デフォルト音
32+ // NOTE: Web Speech APIが使えるブラウザか判定
33+ // https://developer.mozilla.org/ja/docs/Web/API/Web_Speech_API
3234 if ( ! window . SpeechRecognition && ! window . webkitSpeechRecognition ) {
3335 alert ( "お使いのブラウザには未対応です" ) ;
3436 return ;
3537 }
38+ // NOTE: 将来的にwebkit prefixが取れる可能性があるため
3639 const SpeechRecognition =
3740 window . SpeechRecognition || window . webkitSpeechRecognition ;
3841 recognizerRef . current = new SpeechRecognition ( ) ;
@@ -49,12 +52,15 @@ const Home = () => {
4952 [ ...event . results ] . slice ( event . resultIndex ) . forEach ( result => {
5053 const transcript = result [ 0 ] . transcript ;
5154 if ( result . isFinal ) {
55+ // 音声認識が完了して文章が確定
5256 setFinalText ( prevState => {
5357 return prevState + transcript ;
5458 } ) ;
5559 setTranscript ( "" ) ;
5660 } else {
61+ // 音声認識の途中経過
5762 if ( tagValues . some ( value => transcript . includes ( value ) ) ) {
63+ // NOTE: ユーザーが効果音を追加しなければデフォルトを鳴らす
5864 ( userMusic || music ) . play ( ) ;
5965 setAlertOpen ( true ) ;
6066 }
@@ -66,124 +72,66 @@ const Home = () => {
6672
6773 return (
6874 < div >
69- < Head title = "Home " />
70- < Snackbar
75+ < Head title = "自慢ディテクター " />
76+ < Notice
7177 open = { alertOpen }
72- autoHideDuration = { 6000 }
78+ severity = "error"
7379 onClose = { ( ) => {
7480 setAlertOpen ( false ) ;
7581 } }
7682 >
77- < MuiAlert
78- elevation = { 6 }
79- variant = "filled"
80- onClose = { ( ) => {
81- setAlertOpen ( false ) ;
82- } }
83- severity = "error"
84- >
85- 自慢を検知しました
86- </ MuiAlert >
87- </ Snackbar >
88- < Snackbar
83+ 自慢を検知しました
84+ </ Notice >
85+ < Notice
8986 open = { fileLoaded }
90- autoHideDuration = { 6000 }
87+ severity = "success"
9188 onClose = { ( ) => {
9289 setFileLoaded ( false ) ;
9390 } }
9491 >
95- < MuiAlert
96- elevation = { 6 }
97- variant = "filled"
98- onClose = { ( ) => {
99- setFileLoaded ( false ) ;
100- } }
101- severity = "success"
102- >
103- { userMusicName } を読み込みました
104- </ MuiAlert >
105- </ Snackbar >
92+ { userMusicName } を読み込みました
93+ </ Notice >
10694 < Container >
10795 < Grid container alignItems = "center" justify = "center" >
10896 < Grid item >
109- < img src = "/static/logo.png" height = "200px" />
97+ < img src = "/static/logo.png" height = "200px" alt = "自慢ディテクター" />
11098 </ Grid >
11199 </ Grid >
112100 < Box fontSize = { 25 } >
113- < p >
114- { finalText }
115- < span style = { { color : alertOpen ? "#f00" : "#aaa" } } >
116- { transcript }
117- </ span >
118- </ p >
119- < div id = "result-div" > </ div >
101+ < TranscriptField
102+ finalText = { finalText }
103+ transcript = { transcript }
104+ isMatch = { alertOpen }
105+ />
120106 </ Box >
121107 < Grid container spacing = { 2 } >
122108 < Grid item xs = { 11 } >
123- < Autocomplete
109+ < TagField
124110 disabled = { detecting }
125- multiple
126- id = "tags-filled"
127111 options = { candidates }
128- freeSolo
129112 defaultValue = { initialTagValues }
130- onChange = { ( event , values ) => {
113+ label = "反応する単語"
114+ placeholder = "単語を追加 +"
115+ onTagChange = { values => {
131116 setTagValues ( values ) ;
132117 } }
133- renderTags = { ( value , getTagProps ) =>
134- value . map ( ( option , index ) => (
135- < Chip
136- variant = "outlined"
137- label = { option }
138- { ...getTagProps ( { index } ) }
139- />
140- ) )
141- }
142- renderInput = { params => {
143- return (
144- < TextField
145- { ...params }
146- variant = "outlined"
147- label = "反応する単語"
148- placeholder = "単語を追加 +"
149- />
150- ) ;
151- } }
152118 />
153119 </ Grid >
154120 < Grid item >
155- < input
156- ref = { inputRef }
157- accept = "audio/*"
158- id = "file-input"
159- multiple
160- type = "file"
161- style = { { display : "none" } }
162- onChange = { event => {
163- const file = event . target . files [ 0 ] ;
164- if ( ! ( file instanceof File ) ) return ;
165- if ( file . type . indexOf ( "audio" ) === - 1 ) {
166- alert ( "オーディオファイルを選択してください" ) ;
167- return ;
168- }
121+ < UploadButton
122+ disabled = { detecting }
123+ fileType = "audio"
124+ onFileChange = { file => {
169125 const src = window . URL . createObjectURL ( file ) ;
170126 const audio = new Audio ( src ) ;
171127 setUserMusic ( audio ) ;
172128 setUserMusicName ( file . name ) ;
173129 setFileLoaded ( true ) ;
174130 } }
131+ onInvalidFileError = { ( ) => {
132+ alert ( "オーディオファイルを選択してください" ) ;
133+ } }
175134 />
176- < label htmlFor = "file-input" >
177- < IconButton
178- color = "primary"
179- size = "large"
180- disabled = { detecting }
181- aria-label = "upload audio"
182- component = "span"
183- >
184- < LibraryMusicIcon />
185- </ IconButton >
186- </ label >
187135 </ Grid >
188136 </ Grid >
189137 < Box m = { 2 } >
0 commit comments