diff --git a/.babelrc b/.babelrc deleted file mode 100644 index e3ab4eb..0000000 --- a/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": [ "es2015", "react" ] -} diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 05e8f09..0000000 --- a/.eslintrc +++ /dev/null @@ -1,27 +0,0 @@ -{ - "extends": "eslint-config-airbnb", - "env": { - "browser": true, - "node": true, - "mocha": true - }, - "rules": { - "no-redeclare": 0, - "react/jsx-uses-react": 2, - "react/jsx-uses-vars": 2, - "react/react-in-jsx-scope": 2, - "react/jsx-quotes": 0, - "react/sort-comp": 0, - "jsx-quotes": 2, - "comma-dangle": 0, - "indent": [2, 2, {"SwitchCase": 1}], - "no-console": 0, - "no-alert": 0, - "id-length": [2, {"min": 2, "max": 50, "properties": "never", "exceptions": ["$", "i"]}] - }, - "plugins": [ - "react" - ], - "globals": { - } -} diff --git a/.gitignore b/.gitignore index 30bc162..e43b0f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -/node_modules \ No newline at end of file +.DS_Store diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..35706e6 --- /dev/null +++ b/.npmignore @@ -0,0 +1,3 @@ +.git +.gitignore +.DS_Store diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..4b5afdb --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "arrowParens": "avoid", + "trailingComma": "none" +} diff --git a/LICENSE b/LICENSE index 762cd53..eebcb0e 100755 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -The MIT License (MIT) +MIT License -Copyright (c) 2014 Stephen J. Collings, Matthew Honnibal, Pieter Vanderwerff +Copyright (c) 2025 Andrew Golightly Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index e405ce7..380425e 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,84 @@ -# react-speech-recognition-input -A react speech recognition component for chrome browser +# Speech To Text -## install +A speech recognition module to convert speech into text. -$ npm install react-speech-recognition-input --save +## Install -## Usage +`npm install speech-to-text` -``` -import React from 'react'; -import ReactDOM from 'react-dom'; -import Input from 'react-speech-recognition-input'; +## Typical Usage -ReactDOM.render(( - console.log(value)} onEnd={(value) => console.log(value)} /> -), document.getElementById('chart')); +Here is the module being used in a React component -``` +```javascript +import React, { useState, useEffect } from 'react'; +import SpeechToText from 'speech-to-text'; + +const MyComponent = () => { + const [interimText, setInterimText] = useState(''); + const [finalisedText, setFinalisedText] = useState([]); + const [listening, setListening] = useState(false); + const [error, setError] = useState(null); + const [listener, setListener] = useState(null); + + useEffect(() => { + const onAnythingSaid = text => { + setInterimText(text); + }; -##Example + const onEndEvent = () => { + if (listening) { + listener?.startListening(); + } + }; -*run demo* + const onFinalised = text => { + setFinalisedText(prev => [text, ...prev]); + setInterimText(''); + }; + try { + const speechListener = new SpeechToText(onFinalised, onEndEvent, onAnythingSaid); + setListener(speechListener); + } catch (error) { + setError(error.message); + } + + // Cleanup function + return () => { + listener?.stopListening(); + }; + }, [listening, listener]); + + // Component JSX would go here... +} ``` -npm install -npm start + +## API + +### The constructor + +- onFinalised - a callback that will be passed the finalised transcription from the cloud. Slow, but accuate. +- onEndEvent - a callback that will be called when the end event is fired (speech recognition engine disconnects). +- onAnythingSaid - (optional) a callback that will be passed interim transcriptions. Fairly immediate, but less accurate than finalised text. +- language - (optional) the language to interpret against. Default is US English, and the supported languages are listed [here](https://cloud.google.com/speech-to-text/docs/languages). + +The constructor will throw an error if speech recognition is not supported by the browser. [Browser support](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition#Browser_compatibility) includes Chrome, Edge, and Safari. + +```javascript +if (!('webkitSpeechRecognition' in window)) { + throw new Error("This browser doesn't support speech recognition. Try Google Chrome."); +} ``` -open [http://localhost:8080/](http://localhost:8080/) -## props +### startListening + +Initiates listening to speech input. + +### stopListening -* `className`: the css class name of the wrapper. -* `onChange`: run when you start speaking, the value is what you say. -* `onEnd`: run when you stop speaking, the value is what you say. +Does just that. Stops listening. ## License -react-speech-recognition-input is released under the MIT license. +MIT diff --git a/example/app.js b/example/app.js deleted file mode 100644 index d58e175..0000000 --- a/example/app.js +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import Input from '../src'; - -ReactDOM.render(( -
-
点击录音按钮开始说话,说完之后再次点击按钮结束说话

- console.log(value)} onEnd={(value) => console.log(value)} /> -
-), document.getElementById('chart')); diff --git a/example/index.html b/example/index.html deleted file mode 100644 index 4dffb7a..0000000 --- a/example/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - React echarts example - - -
- - - \ No newline at end of file diff --git a/example/server.js b/example/server.js deleted file mode 100644 index 45250e3..0000000 --- a/example/server.js +++ /dev/null @@ -1,20 +0,0 @@ -var path = require('path'); -var webpack = require('webpack'); -var WebpackDevServer = require('webpack-dev-server'); -var config = require('./webpack.config'); - -new WebpackDevServer(webpack(config), { - contentBase: __dirname, - publicPath: config.output.publicPath, - hot: true, - historyApiFallback: true, - stats: { - colors: true - }, - headers: {"Access-Control-Allow-Origin": "*"} -}).listen(8080, 'localhost', function (err) { - if (err) { - console.log(err); - } - console.log('✅ Server is listening at http://localhost:8080'); -}); \ No newline at end of file diff --git a/example/webpack.config.js b/example/webpack.config.js deleted file mode 100644 index b9d259f..0000000 --- a/example/webpack.config.js +++ /dev/null @@ -1,16 +0,0 @@ -var path = require("path"); -module.exports = { - entry: { - app: path.join(__dirname, './app') - }, - output: { - path: path.join(__dirname, 'debug'), - publicPath: "/", - filename: "index.js" - }, - module: { - loaders: [ - {test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader'}, - ] - } -}; diff --git a/lib/index.css b/lib/index.css deleted file mode 100644 index df879b6..0000000 --- a/lib/index.css +++ /dev/null @@ -1,23 +0,0 @@ -.chatInputWrapper { - position: relative; - height: 40px; -} -.micImg { - position: absolute; - width: 38px; - height: 38px; - right: 5px; - top: 1px; -} -.chatMessageInput { - background-color: #ffffff; - padding: 0 10px; - margin: 0; - width: 100%; - border-radius: 2px; - border: 1px solid #e5e9ec; - transition: background 0.2s linear 0s, box-shadow 0.2s linear 0s; - outline: 0; - height: 40px; - box-sizing: border-box; -} \ No newline at end of file diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index 9cd58d3..0000000 --- a/lib/index.js +++ /dev/null @@ -1,127 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _react = require('react'); - -var _react2 = _interopRequireDefault(_react); - -var _index = require('style!css?modules!./index.css'); - -var _index2 = _interopRequireDefault(_index); - -var _mic = require('file!./mic.gif'); - -var _mic2 = _interopRequireDefault(_mic); - -var _micAnimate = require('file!./mic-animate.gif'); - -var _micAnimate2 = _interopRequireDefault(_micAnimate); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } - -var App = function (_Component) { - _inherits(App, _Component); - - function App(props) { - _classCallCheck(this, App); - - var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(App).call(this, props)); - - _this.state = { - inputValue: '', - supportVoice: 'webkitSpeechRecognition' in window - }; - return _this; - } - - _createClass(App, [{ - key: 'componentDidMount', - value: function componentDidMount() { - var _this2 = this; - - if (this.state.supportVoice) { - var WebkitSpeechRecognition = window.webkitSpeechRecognition; - this.recognition = new WebkitSpeechRecognition(); - this.recognition.continuous = true; - this.recognition.interimResults = true; - this.recognition.lang = 'cmn-Hans-CN'; - this.recognition.onresult = function (event) { - var interimTranscript = ''; - var finalTranscript = ''; - for (var i = event.resultIndex; i < event.results.length; ++i) { - if (event.results[i].isFinal) { - finalTranscript += event.results[i][0].transcript; - _this2.setState({ - inputValue: finalTranscript - }); - if (_this2.props.onChange) _this2.props.onChange(finalTranscript); - if (_this2.props.onEnd) _this2.props.onEnd(finalTranscript); - } else { - interimTranscript += event.results[i][0].transcript; - _this2.setState({ - inputValue: interimTranscript - }); - if (_this2.props.onChange) _this2.props.onChange(interimTranscript); - } - } - }; - } - } - }, { - key: 'changeValue', - value: function changeValue(event) { - this.setState({ - inputValue: event.target.value - }); - } - }, { - key: 'say', - value: function say() { - if (this.state.supportVoice) { - if (!this.state.speaking) { - // start listening - this.recognition.start(); - } else { - this.recognition.stop(); - var question = this.state.inputValue; - } - this.setState({ - speaking: !this.state.speaking, - inputValue: '' - }); - } - } - }, { - key: 'render', - value: function render() { - return _react2.default.createElement( - 'div', - { className: _index2.default.chatInputWrapper + " " + this.props.className }, - this.state.supportVoice && _react2.default.createElement('img', { - src: this.state.speaking ? _micAnimate2.default : _mic2.default, - className: _index2.default.micImg, - onClick: this.say.bind(this) }), - _react2.default.createElement('input', { - value: this.state.inputValue, - onChange: this.changeValue.bind(this), - className: _index2.default.chatMessageInput, - placeholder: 'Type your message' }) - ); - } - }]); - - return App; -}(_react.Component); - -exports.default = App; \ No newline at end of file diff --git a/lib/mic-animate.gif b/lib/mic-animate.gif deleted file mode 100644 index ac8de0d..0000000 Binary files a/lib/mic-animate.gif and /dev/null differ diff --git a/lib/mic.gif b/lib/mic.gif deleted file mode 100644 index 640d9fa..0000000 Binary files a/lib/mic.gif and /dev/null differ diff --git a/package.json b/package.json index d0ba468..9d95580 100644 --- a/package.json +++ b/package.json @@ -1,48 +1,29 @@ { - "name": "react-speech-recognition-input", - "version": "0.1.0", - "description": "A react speech recognition component for chrome browser", - "main": "index.js", - "scripts": { - "start": "node example/server.js", - "build": "rm -rf lib && cp -rf src lib && ./node_modules/.bin/babel src -d lib" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/somonus/react-speech-recognition-input.git" + "name": "speech-to-text", + "version": "3.0.2", + "description": "A speech to text module.", + "type": "module", + "main": "./src/index.js", + "exports": { + ".": "./src/index.js" }, "keywords": [ - "react", - "speeck", - "recognition" + "speech", + "recognition", + "text", + "browser", + "chrome", + "voice", + "transcription" ], - "author": "somonus", - "main": "lib/index.js", + "author": "Andrew Golightly (http://www.golightlyplus.com)", "license": "MIT", - "bugs": { - "url": "https://github.com/somonus/react-speech-recognition-input/issues" + "repository": { + "type": "git", + "url": "git+https://github.com/magician11/speech-to-text.git" }, - "homepage": "https://github.com/somonus/react-speech-recognition-input#readme", - "peerDependencies": { - "react": ">=0.14.2", - "react-dom": ">=0.14.2" + "bugs": { + "url": "https://github.com/magician11/speech-to-text/issues" }, - "devDependencies": { - "babel": "^6.5.2", - "babel-cli": "^6.10.1", - "babel-core": "^6.10.4", - "babel-loader": "^6.2.4", - "babel-plugin-react-transform": "^2.0.2", - "babel-preset-es2015": "^6.9.0", - "babel-preset-react": "^6.11.1", - "css-loader": "0.22.0", - "eslint": "1.6.0", - "eslint-config-airbnb": "0.1.0", - "eslint-loader": "1.0.0", - "eslint-plugin-react": "3.5.0", - "file-loader": "^0.9.0", - "style-loader": "0.13.0", - "webpack": "1.13.1", - "webpack-dev-server": "^1.14.1" - } + "homepage": "https://github.com/magician11/speech-to-text#readme" } diff --git a/src/index.css b/src/index.css deleted file mode 100644 index df879b6..0000000 --- a/src/index.css +++ /dev/null @@ -1,23 +0,0 @@ -.chatInputWrapper { - position: relative; - height: 40px; -} -.micImg { - position: absolute; - width: 38px; - height: 38px; - right: 5px; - top: 1px; -} -.chatMessageInput { - background-color: #ffffff; - padding: 0 10px; - margin: 0; - width: 100%; - border-radius: 2px; - border: 1px solid #e5e9ec; - transition: background 0.2s linear 0s, box-shadow 0.2s linear 0s; - outline: 0; - height: 40px; - box-sizing: border-box; -} \ No newline at end of file diff --git a/src/index.js b/src/index.js index b0c4a3e..efe261c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,85 +1,63 @@ -import React, { Component, PropTypes } from 'react'; -import styles from 'style!css?modules!./index.css'; -import mic from 'file!./mic.gif'; -import micAnimate from 'file!./mic-animate.gif' +export default class SpeechToText { + /* + This module is largely inspired by this article: + https://developers.google.com/web/updates/2013/01/Voice-Driven-Web-Apps-Introduction-to-the-Web-Speech-API -export default class App extends Component { - constructor(props) { - super(props); - this.state = { - inputValue: '', - supportVoice: 'webkitSpeechRecognition' in window, - }; - } + Arguments for the constructor: - componentDidMount() { - if (this.state.supportVoice) { - const WebkitSpeechRecognition = window.webkitSpeechRecognition; - this.recognition = new WebkitSpeechRecognition(); - this.recognition.continuous = true; - this.recognition.interimResults = true; - this.recognition.lang = 'cmn-Hans-CN'; - this.recognition.onresult = (event) => { - let interimTranscript = ''; - let finalTranscript = ''; - for (let i = event.resultIndex; i < event.results.length; ++i) { - if (event.results[i].isFinal) { - finalTranscript += event.results[i][0].transcript; - this.setState({ - inputValue: finalTranscript, - }); - if (this.props.onChange) this.props.onChange(finalTranscript); - if (this.props.onEnd) this.props.onEnd(finalTranscript); - } else { - interimTranscript += event.results[i][0].transcript; - this.setState({ - inputValue: interimTranscript, - }); - if (this.props.onChange) this.props.onChange(interimTranscript); - } - } - }; + - onFinalised - a callback that will be passed the finalised transcription from the cloud. Slow, but accurate. + - onEndEvent - a callback that will be called when the end event is fired (speech recognition engine disconnects). + - onAnythingSaid - a callback that will be passed interim transcriptions. Fairly immediate, but less accurate than finalised text. + - language - the language to interpret against. Default is US English. + + */ + constructor(onFinalised, onEndEvent, onAnythingSaid, language = 'en-US') { + // Check to see if this browser supports speech recognition + // https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition#Browser_compatibility + if (!('webkitSpeechRecognition' in window)) { + throw new Error( + "This browser doesn't support speech recognition. Try Google Chrome." + ); } - } - changeValue(event) { - this.setState({ - inputValue: event.target.value, - }); - } + const SpeechRecognition = window.webkitSpeechRecognition; + this.recognition = new SpeechRecognition(); + + // set interim results to be returned if a callback for it has been passed in + this.recognition.interimResults = !!onAnythingSaid; + this.recognition.lang = language; + + let finalTranscript = ''; + + // process both interim and finalised results + this.recognition.onresult = event => { + let interimTranscript = ''; - say() { - if (this.state.supportVoice) { - if (!this.state.speaking) { - // start listening - this.recognition.start(); - } else { - this.recognition.stop(); - const question = this.state.inputValue; + // concatenate all the transcribed pieces together (SpeechRecognitionResult) + for (let i = event.resultIndex; i < event.results.length; i += 1) { + const transcriptionPiece = event.results[i][0].transcript; + // check for a finalised transciption in the cloud + if (event.results[i].isFinal) { + finalTranscript += transcriptionPiece; + onFinalised(finalTranscript); + finalTranscript = ''; + } else if (this.recognition.interimResults) { + interimTranscript += transcriptionPiece; + onAnythingSaid(interimTranscript); + } } - this.setState({ - speaking: !this.state.speaking, - inputValue: '', - }); - } + }; + + this.recognition.onend = () => { + onEndEvent(); + }; } - render() { - return ( -
- { - this.state.supportVoice && - - } - -
- ); + startListening() { + this.recognition.start(); + } + + stopListening() { + this.recognition.stop(); } } diff --git a/src/mic-animate.gif b/src/mic-animate.gif deleted file mode 100644 index ac8de0d..0000000 Binary files a/src/mic-animate.gif and /dev/null differ diff --git a/src/mic.gif b/src/mic.gif deleted file mode 100644 index 640d9fa..0000000 Binary files a/src/mic.gif and /dev/null differ diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index 3bf6eff..0000000 --- a/webpack.config.js +++ /dev/null @@ -1,20 +0,0 @@ -var webpack = require("webpack"); - -module.exports = { - entry: { - app: './src/index.js' - }, - output: { - path: './bin', - filename: 'index.js', - }, - externals: { - react: 'react' - }, - module: { - loaders: [ - {test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader'}, - { test: /\.css$/, exclude: /node_modules/, loader: 'style-loader!css-loader?modules&importLoaders=1' } - ] - } -}