├── .husky ├── .gitignore └── pre-commit ├── .dockerignore ├── .npmrc ├── src ├── components │ ├── AudioWave │ │ ├── _audio-wave.scss │ │ ├── index.js │ │ └── AudioWave.js │ ├── Header │ │ ├── index.js │ │ ├── _overrides.scss │ │ ├── Header.js │ │ └── _header.scss │ ├── Toast │ │ ├── index.js │ │ ├── _toast.scss │ │ └── Toast.js │ ├── TranscriptBox │ │ ├── index.js │ │ ├── utils.js │ │ ├── _transcript-box.scss │ │ └── TranscriptBox.js │ ├── KeywordTooltip │ │ ├── index.js │ │ ├── _keyword-tooltip.scss │ │ └── KeywordTooltip.js │ ├── OutputContainer │ │ ├── index.js │ │ ├── _output-container.scss │ │ └── OutputContainer.js │ ├── SubmitContainer │ │ ├── index.js │ │ ├── _submit-container.scss │ │ └── SubmitContainer.js │ ├── ControlContainer │ │ ├── index.js │ │ ├── _control-container.scss │ │ ├── _overrides.scss │ │ └── ControlContainer.js │ └── ServiceContainer │ │ ├── index.js │ │ ├── _service-container.scss │ │ ├── reducer.js │ │ ├── utils.js │ │ └── ServiceContainer.js ├── utils.js ├── styles │ ├── _index.scss │ └── main.scss ├── setupTests.js ├── hooks │ └── useScript.js ├── index.js ├── App.js ├── serviceWorker.js └── data │ └── models.json ├── public ├── robots.txt ├── favicon.ico ├── audio │ ├── ar-AR_Broadband-sample.wav │ ├── de-DE_Broadband-sample.wav │ ├── de-DE_Narrowband-sample.wav │ ├── en-GB_Broadband-sample.wav │ ├── en-GB_Narrowband-sample.wav │ ├── en-US_Broadband-sample.wav │ ├── en-US_Narrowband-sample.wav │ ├── es-ES_Broadband-sample.wav │ ├── es-ES_Narrowband-sample.wav │ ├── fr-FR_Broadband-sample.wav │ ├── fr-FR_Narrowband-sample.wav │ ├── ja-JP_Broadband-sample.wav │ ├── ja-JP_Narrowband-sample.wav │ ├── ko-KR_Broadband-sample.wav │ ├── ko-KR_Narrowband-sample.wav │ ├── nl-NL_Broadband-sample.wav │ ├── nl-NL_Narrowband-sample.wav │ ├── pt-BR_Broadband-sample.wav │ ├── pt-BR_Narrowband-sample.wav │ ├── zh-CN_Broadband-sample.wav │ ├── zh-CN_Narrowband-sample.wav │ └── en-US_ShortForm_Narrowband-sample.wav ├── manifest.json └── index.html ├── doc └── source │ ├── images │ ├── stt.png │ ├── cf_deploy.png │ ├── architecture.png │ ├── services_icon.png │ └── toolchain_pipeline.png │ ├── local.md │ └── openshift.md ├── test ├── jest.config.js ├── jest-puppeteer.config.js └── page.test.js ├── .gitignore ├── ACKNOWLEDGEMENTS.md ├── server.js ├── .github ├── dependabot.yml └── workflows │ ├── nodejs.yml │ └── stale.yml ├── Dockerfile-tools ├── TESTING.md ├── config ├── express.js ├── error-handler.js └── security.js ├── scripts ├── experience_test.sh ├── README.md └── experience_test.py ├── craco.config.js ├── .eslintrc.json ├── CONTRIBUTING.md ├── DEVELOPING.md ├── Dockerfile ├── .env.example ├── package.json ├── MAINTAINERS.md ├── app.js ├── README.md └── LICENSE /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules/ -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.com/ 2 | -------------------------------------------------------------------------------- /src/components/AudioWave/_audio-wave.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /src/components/Header/index.js: -------------------------------------------------------------------------------- 1 | import Header from './Header'; 2 | export default Header; 3 | -------------------------------------------------------------------------------- /src/components/Toast/index.js: -------------------------------------------------------------------------------- 1 | import Toast from './Toast'; 2 | 3 | export default Toast; 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged 5 | -------------------------------------------------------------------------------- /src/components/AudioWave/index.js: -------------------------------------------------------------------------------- 1 | import AudioWave from './AudioWave'; 2 | export default AudioWave; 3 | -------------------------------------------------------------------------------- /doc/source/images/stt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/doc/source/images/stt.png -------------------------------------------------------------------------------- /test/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'jest-puppeteer', 3 | testRegex: './*\\.test\\.js$', 4 | }; 5 | -------------------------------------------------------------------------------- /src/components/TranscriptBox/index.js: -------------------------------------------------------------------------------- 1 | import TranscriptBox from './TranscriptBox'; 2 | export default TranscriptBox; 3 | -------------------------------------------------------------------------------- /src/components/KeywordTooltip/index.js: -------------------------------------------------------------------------------- 1 | import KeywordTooltip from './KeywordTooltip'; 2 | export default KeywordTooltip; 3 | -------------------------------------------------------------------------------- /src/components/OutputContainer/index.js: -------------------------------------------------------------------------------- 1 | import OutputContainer from './OutputContainer'; 2 | export default OutputContainer; 3 | -------------------------------------------------------------------------------- /src/components/SubmitContainer/index.js: -------------------------------------------------------------------------------- 1 | import SubmitContainer from './SubmitContainer'; 2 | export default SubmitContainer; 3 | -------------------------------------------------------------------------------- /doc/source/images/cf_deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/doc/source/images/cf_deploy.png -------------------------------------------------------------------------------- /src/components/ControlContainer/index.js: -------------------------------------------------------------------------------- 1 | import ControlContainer from './ControlContainer'; 2 | export default ControlContainer; 3 | -------------------------------------------------------------------------------- /src/components/ServiceContainer/index.js: -------------------------------------------------------------------------------- 1 | import ServiceContainer from './ServiceContainer'; 2 | export default ServiceContainer; 3 | -------------------------------------------------------------------------------- /doc/source/images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/doc/source/images/architecture.png -------------------------------------------------------------------------------- /doc/source/images/services_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/doc/source/images/services_icon.png -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export const createError = (title, description) => { 2 | return { 3 | title, 4 | description, 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /doc/source/images/toolchain_pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/doc/source/images/toolchain_pipeline.png -------------------------------------------------------------------------------- /public/audio/ar-AR_Broadband-sample.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/public/audio/ar-AR_Broadband-sample.wav -------------------------------------------------------------------------------- /public/audio/de-DE_Broadband-sample.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/public/audio/de-DE_Broadband-sample.wav -------------------------------------------------------------------------------- /public/audio/de-DE_Narrowband-sample.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/public/audio/de-DE_Narrowband-sample.wav -------------------------------------------------------------------------------- /public/audio/en-GB_Broadband-sample.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/public/audio/en-GB_Broadband-sample.wav -------------------------------------------------------------------------------- /public/audio/en-GB_Narrowband-sample.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/public/audio/en-GB_Narrowband-sample.wav -------------------------------------------------------------------------------- /public/audio/en-US_Broadband-sample.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/public/audio/en-US_Broadband-sample.wav -------------------------------------------------------------------------------- /public/audio/en-US_Narrowband-sample.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/public/audio/en-US_Narrowband-sample.wav -------------------------------------------------------------------------------- /public/audio/es-ES_Broadband-sample.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/public/audio/es-ES_Broadband-sample.wav -------------------------------------------------------------------------------- /public/audio/es-ES_Narrowband-sample.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/public/audio/es-ES_Narrowband-sample.wav -------------------------------------------------------------------------------- /public/audio/fr-FR_Broadband-sample.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/public/audio/fr-FR_Broadband-sample.wav -------------------------------------------------------------------------------- /public/audio/fr-FR_Narrowband-sample.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/public/audio/fr-FR_Narrowband-sample.wav -------------------------------------------------------------------------------- /public/audio/ja-JP_Broadband-sample.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/public/audio/ja-JP_Broadband-sample.wav -------------------------------------------------------------------------------- /public/audio/ja-JP_Narrowband-sample.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/public/audio/ja-JP_Narrowband-sample.wav -------------------------------------------------------------------------------- /public/audio/ko-KR_Broadband-sample.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/public/audio/ko-KR_Broadband-sample.wav -------------------------------------------------------------------------------- /public/audio/ko-KR_Narrowband-sample.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/public/audio/ko-KR_Narrowband-sample.wav -------------------------------------------------------------------------------- /public/audio/nl-NL_Broadband-sample.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/public/audio/nl-NL_Broadband-sample.wav -------------------------------------------------------------------------------- /public/audio/nl-NL_Narrowband-sample.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/public/audio/nl-NL_Narrowband-sample.wav -------------------------------------------------------------------------------- /public/audio/pt-BR_Broadband-sample.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/public/audio/pt-BR_Broadband-sample.wav -------------------------------------------------------------------------------- /public/audio/pt-BR_Narrowband-sample.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/public/audio/pt-BR_Narrowband-sample.wav -------------------------------------------------------------------------------- /public/audio/zh-CN_Broadband-sample.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/public/audio/zh-CN_Broadband-sample.wav -------------------------------------------------------------------------------- /public/audio/zh-CN_Narrowband-sample.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/public/audio/zh-CN_Narrowband-sample.wav -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | .npm 3 | node_modules 4 | 5 | # Config 6 | .next 7 | 8 | # Misc 9 | .DS_Store 10 | *.env 11 | 12 | build 13 | -------------------------------------------------------------------------------- /public/audio/en-US_ShortForm_Narrowband-sample.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/speech-to-text-code-pattern/HEAD/public/audio/en-US_ShortForm_Narrowband-sample.wav -------------------------------------------------------------------------------- /test/jest-puppeteer.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | server: { 3 | command: 'npm run start', 4 | port: 5000, 5 | launchTimeout: 10000, 6 | debug: true, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /ACKNOWLEDGEMENTS.md: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | 3 | * This app was built by the Watson Developer Cloud SDK team. 4 | 5 | * This app was originally licensed under the [MIT License](https://opensource.org/licenses/MIT). 6 | -------------------------------------------------------------------------------- /src/styles/_index.scss: -------------------------------------------------------------------------------- 1 | @use '@carbon/react/scss/theme' as *; 2 | 3 | html { 4 | background-color: $background; 5 | } 6 | 7 | .app-container { 8 | display: flex; 9 | flex-direction: column; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/Header/_overrides.scss: -------------------------------------------------------------------------------- 1 | @use '@carbon/react/scss/colors' as *; 2 | 3 | .bx--link { 4 | &:focus { 5 | outline: none; 6 | } 7 | 8 | &:visited { 9 | color: $blue-40; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/KeywordTooltip/_keyword-tooltip.scss: -------------------------------------------------------------------------------- 1 | @use '@carbon/react/scss/spacing' as *; 2 | 3 | .keyword-tooltip { 4 | font-family: 'IBM Plex Mono'; 5 | 6 | p:first-child { 7 | padding-bottom: $spacing-02; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | require('dotenv').config({ silent: true }); 4 | 5 | const app = require('./app'); 6 | 7 | const port = process.env.PORT || 5000; 8 | const server = app.listen(port, () => { 9 | // eslint-disable-next-line no-console 10 | console.log('Server running on port: %d', port); 11 | }); 12 | 13 | module.exports = server; 14 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "STT Code Pattern", 3 | "name": "Speech to Text Code Pattern", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | target-branch: "development" 8 | labels: 9 | - "npm dependencies" 10 | - package-ecosystem: "docker" 11 | directory: "/" 12 | schedule: 13 | interval: "daily" 14 | target-branch: "development" 15 | labels: 16 | - "docker" 17 | -------------------------------------------------------------------------------- /src/hooks/useScript.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | const useScript = url => { 4 | useEffect(() => { 5 | const script = document.createElement('script'); 6 | 7 | script.src = url; 8 | script.async = true; 9 | 10 | document.body.appendChild(script); 11 | 12 | return () => { 13 | document.body.removeChild(script); 14 | }; 15 | }, [url]); 16 | }; 17 | 18 | export default useScript; 19 | -------------------------------------------------------------------------------- /Dockerfile-tools: -------------------------------------------------------------------------------- 1 | FROM registry.access.redhat.com/ubi8/nodejs-18-minimal:1 2 | 3 | ADD . /app 4 | 5 | ENV NODE_ENV production 6 | ENV PORT 5000 7 | 8 | EXPOSE 5000 9 | 10 | WORKDIR /app 11 | 12 | RUN npm install 13 | CMD ["/bin/bash"] 14 | 15 | ARG bx_dev_user=root 16 | ARG bx_dev_userid=1000 17 | 18 | RUN BX_DEV_USER=$bx_dev_user 19 | RUN BX_DEV_USERID=$bx_dev_userid 20 | RUN if [ "$bx_dev_user" != root ]; then useradd -ms /bin/bash -u $bx_dev_userid $bx_dev_user; fi -------------------------------------------------------------------------------- /src/components/OutputContainer/_output-container.scss: -------------------------------------------------------------------------------- 1 | @use '@carbon/react/scss/spacing' as *; 2 | 3 | .output-container { 4 | display: flex; 5 | flex-direction: column; 6 | height: 100%; 7 | padding: $spacing-08; 8 | padding-bottom: $spacing-07 * 2; 9 | width: 100%; 10 | 11 | @media (min-width: 1200px) { 12 | flex: 1 1 auto; 13 | margin-left: $spacing-09; 14 | } 15 | 16 | .container-title { 17 | padding-bottom: $spacing-09; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './styles/main.scss'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /src/components/ControlContainer/_control-container.scss: -------------------------------------------------------------------------------- 1 | @use '@carbon/react/scss/spacing' as *; 2 | @import './overrides.scss'; 3 | 4 | .control-container { 5 | display: flex; 6 | flex-direction: column; 7 | height: 100%; 8 | margin-bottom: $spacing-07; 9 | padding: $spacing-08; 10 | padding-bottom: $spacing-07 * 2; 11 | width: 100%; 12 | 13 | @media (min-width: 1200px) { 14 | flex: 1 1 auto; 15 | margin-bottom: 0; 16 | } 17 | 18 | .container-title { 19 | padding-bottom: $spacing-09; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/ServiceContainer/_service-container.scss: -------------------------------------------------------------------------------- 1 | @use '@carbon/react/scss/spacing' as *; 2 | @use '@carbon/react/scss/theme' as *; 3 | 4 | .service-container { 5 | background-color: $background; 6 | align-items: center; 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: unset; 10 | overflow-x: hidden; 11 | padding: $spacing-07; 12 | position: relative; 13 | 14 | @media (min-width: 1200px) { 15 | align-items: unset; 16 | flex-direction: row; 17 | justify-content: center; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /TESTING.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | ## Unit tests 4 | 5 | Run unit tests with: 6 | 7 | ```bash 8 | npm run test:components 9 | ``` 10 | 11 | ## Integration tests 12 | 13 | First you have to make sure your code is built: 14 | 15 | ```bash 16 | npm run build 17 | ``` 18 | 19 | Then run integration tests with: 20 | 21 | ```bash 22 | npm run test:integration 23 | ``` 24 | 25 | ## Experience tests with Selenium 26 | 27 | Additional UI tests are run with Selenium to meet the requirements of a starter kit. Refer to: [scripts/README.md](scripts/README.md). -------------------------------------------------------------------------------- /src/components/Toast/_toast.scss: -------------------------------------------------------------------------------- 1 | [class*='custom-toast-'] { 2 | left: 100%; 3 | position: absolute; 4 | top: 0; 5 | z-index: 10; 6 | 7 | &.enter { 8 | /* ToastNotification width is 18rem at this screen size. */ 9 | left: calc(100% - 19rem); 10 | transition-duration: 0.5s; 11 | transition-property: left; 12 | transition-timing-function: ease; 13 | 14 | @media (min-width: 1584px) { 15 | /* ToastNotification width is 22rem at this screen size. */ 16 | left: calc(100% - 23rem); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [16.x, 18.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm run build 22 | - run: npm run test 23 | env: 24 | CI: true 25 | -------------------------------------------------------------------------------- /src/components/KeywordTooltip/KeywordTooltip.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export const KeywordTooltip = ({ confidence, startTime, endTime }) => ( 5 |
6 |

Confidence: {confidence}

7 |

8 | {startTime}s - {endTime}s 9 |

10 |
11 | ); 12 | 13 | KeywordTooltip.propTypes = { 14 | confidence: PropTypes.number.isRequired, 15 | startTime: PropTypes.number.isRequired, 16 | endTime: PropTypes.number.isRequired, 17 | }; 18 | 19 | export default KeywordTooltip; 20 | -------------------------------------------------------------------------------- /src/components/TranscriptBox/utils.js: -------------------------------------------------------------------------------- 1 | export const createWordRegex = keywordInfo => { 2 | let allKeywords = []; 3 | keywordInfo.forEach(sectionKeywords => { 4 | allKeywords = [...allKeywords, ...Object.keys(sectionKeywords)]; 5 | }); 6 | const regexArray = allKeywords.map((word, index) => { 7 | if (index !== allKeywords.length - 1) { 8 | return `${word}|`; 9 | } 10 | return word; 11 | }); 12 | const regexWordSearch = regexArray.reduce((arr, str) => arr + str, ''); 13 | const regex = new RegExp(`(${regexWordSearch})(?!')`, 'gi'); 14 | return regex; 15 | }; 16 | -------------------------------------------------------------------------------- /config/express.js: -------------------------------------------------------------------------------- 1 | // Module dependencies. 2 | const express = require('express'); 3 | const bodyParser = require('body-parser'); 4 | const path = require('path'); 5 | const morgan = require('morgan'); 6 | const security = require('./security'); 7 | 8 | module.exports = function(app) { 9 | app.enable('trust proxy'); 10 | app.use(bodyParser.json({ limit: '1mb' })); 11 | 12 | // This is only loaded when running in Bluemix. 13 | if (process.env.VCAP_APPLICATION) { 14 | security(app); 15 | } 16 | 17 | app.use(express.static(path.join(__dirname, '..', 'build'))); 18 | app.use(morgan('dev')); 19 | }; 20 | -------------------------------------------------------------------------------- /scripts/experience_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Installing Chrome web driver..." 4 | source <(curl -sSL "${DEVX_SKIT_ASSETS_GIT_URL_RAW:-https://github.com/IBM/devex-skit-assets/raw/2.0.2}/scripts/install_chrome.sh") 5 | 6 | echo "Starting Chrome web driver..." 7 | source <(curl -sSL "${DEVX_SKIT_ASSETS_GIT_URL_RAW:-https://github.com/IBM/devex-skit-assets/raw/2.0.2}/scripts/start_chrome.sh") 8 | 9 | echo "Checking for pip" 10 | pip3 -V 11 | 12 | echo "Installing Selenium Python package..." 13 | pip3 install selenium 14 | 15 | echo "Running UI test using Selenium..." 16 | python3 experience_test.py 17 | -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | module.exports = { 4 | webpack: { 5 | alias: { 6 | "stream": "stream-browserify", 7 | }, 8 | fallback: { 9 | "buffer": require.resolve("buffer/"), 10 | "process": require.resolve("process/browser"), 11 | "stream": require.resolve("stream-browserify"), 12 | }, 13 | plugins: { 14 | add: [ 15 | new webpack.ProvidePlugin({ 16 | Buffer: ['buffer', 'Buffer'], 17 | process: 'process/browser', 18 | }), 19 | ], 20 | remove: [ "ModuleScopePlugin"], 21 | } 22 | } 23 | }; -------------------------------------------------------------------------------- /config/error-handler.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | // Catch 404 and forward to the error handler. 3 | app.use((req, res, next) => { 4 | const err = new Error('Not Found'); 5 | err.code = 404; 6 | err.message = 'Not Found'; 7 | next(err); 8 | }); 9 | 10 | // Error handler 11 | // eslint-disable-next-line no-unused-vars 12 | app.use((err, req, res, next) => { 13 | const error = { 14 | title: err.title || 'Internal Server Error', 15 | description: err.description || err.error || err.message, 16 | }; 17 | res.status(error.statusCode || 500).json(error); 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /src/styles/main.scss: -------------------------------------------------------------------------------- 1 | // Set theme. 2 | @use '@carbon/react/scss/themes'; 3 | @use '@carbon/react/scss/theme' with ( 4 | $theme: themes.$g10 5 | ); 6 | // vendors 7 | @use '@carbon/react'; 8 | 9 | @import './index'; 10 | 11 | // components 12 | @import '../components/AudioWave/audio-wave'; 13 | @import '../components/ControlContainer/control-container'; 14 | @import '../components/Header/header'; 15 | @import '../components/KeywordTooltip/keyword-tooltip'; 16 | @import '../components/OutputContainer/output-container'; 17 | @import '../components/ServiceContainer/service-container'; 18 | @import '../components/SubmitContainer/submit-container'; 19 | @import '../components/Toast/toast'; 20 | @import '../components/TranscriptBox/transcript-box'; 21 | -------------------------------------------------------------------------------- /src/components/SubmitContainer/_submit-container.scss: -------------------------------------------------------------------------------- 1 | @use '@carbon/react/scss/spacing' as *; 2 | @use '@carbon/react/scss/theme' as *; 3 | 4 | .submit-container { 5 | display: flex; 6 | flex-direction: column; 7 | 8 | @media (min-width: 480px) { 9 | flex-direction: row; 10 | max-height: $spacing-08 * 2; 11 | } 12 | 13 | .submit-button { 14 | max-width: 100%; 15 | min-height: $spacing-09; 16 | width: 100%; 17 | 18 | @media (min-width: 480px) { 19 | flex: 1 1 auto; 20 | max-width: unset; 21 | width: unset; 22 | } 23 | 24 | &:not(label) { 25 | margin-bottom: $spacing-03; 26 | 27 | @media (min-width: 480px) { 28 | margin-bottom: 0; 29 | margin-right: $spacing-03; 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/components/Header/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Tile } from '@carbon/react'; 4 | 5 | export const Header = ({ description, links, title }) => ( 6 | 7 |
8 |

{title}

9 |

{description}

10 |
11 |
12 |
{links.map(link => link)}
13 |
14 |
15 | ); 16 | 17 | Header.propTypes = { 18 | description: PropTypes.string, 19 | links: PropTypes.arrayOf(PropTypes.object), 20 | title: PropTypes.string, 21 | }; 22 | 23 | Header.defaultProps = { 24 | description: '', 25 | links: [], 26 | title: '', 27 | }; 28 | 29 | export default Header; 30 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true, 7 | "jest": true 8 | }, 9 | "extends": [ 10 | "eslint:recommended", 11 | "plugin:jest/recommended", 12 | "plugin:react/recommended" 13 | ], 14 | "globals": { 15 | "Atomics": "readonly", 16 | "SharedArrayBuffer": "readonly", 17 | "page": true, 18 | "browser": true, 19 | "context": true, 20 | "jestPuppeteer": true 21 | }, 22 | "parserOptions": { 23 | "ecmaFeatures": { 24 | "jsx": true 25 | }, 26 | "ecmaVersion": 2018, 27 | "sourceType": "module" 28 | }, 29 | "plugins": ["jest", "react", "react-hooks"], 30 | "rules": { 31 | "react-hooks/rules-of-hooks": "error", 32 | "react-hooks/exhaustive-deps": "warn" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | stale: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/stale@v1 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | stale-issue-message: ':wave: Hi! This issue has been marked stale due to inactivity. If no further activity occurs, it will automatically be closed.' 17 | stale-pr-message: ':wave: Hi! This pull request has been marked stale due to inactivity. If no further activity occurs, it will automatically be closed.' 18 | stale-issue-label: 'stale' 19 | exempt-issue-label: 'keep-open' 20 | stale-pr-label: 'stale' 21 | days-before-stale: 30 22 | days-before-close: 7 23 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This is an open source project, and we appreciate your help! 4 | 5 | We use the GitHub issue tracker to discuss new features and non-trivial bugs. 6 | 7 | In addition to the issue tracker, [#journeys on 8 | Slack](https://dwopen.slack.com) is the best way to get into contact with the 9 | project's maintainers. 10 | 11 | To contribute code, documentation, or tests, please submit a pull request to 12 | the GitHub repository. Generally, we expect two maintainers to review your pull 13 | request before it is approved for merging. For more details, see the 14 | [MAINTAINERS](MAINTAINERS.md) page. 15 | 16 | Contributions are subject to the [Developer Certificate of Origin, Version 1.1](https://developercertificate.org/) and the [Apache License, Version 2](https://www.apache.org/licenses/LICENSE-2.0.txt). 17 | -------------------------------------------------------------------------------- /src/components/ControlContainer/_overrides.scss: -------------------------------------------------------------------------------- 1 | @use '@carbon/react/scss/theme' as *; 2 | @use '@carbon/react/scss/spacing' as *; 3 | 4 | .cds--dropdown.cds--list-box { 5 | background-color: $layer-02; 6 | 7 | .cds--list-box__menu { 8 | background-color: $layer-02; 9 | } 10 | 11 | .cds--list-box__field { 12 | background-color: $layer-02; 13 | border-bottom: 1px solid $border-strong-01; 14 | } 15 | 16 | .cds--list-box__field:hover { 17 | background-color: $field-hover; 18 | } 19 | 20 | .cds--list-box__menu-item.cds--list-box__menu-item--highlighted { 21 | background-color: $layer-accent-01; 22 | } 23 | } 24 | 25 | .cds--fieldset { 26 | max-width: 100%; 27 | min-width: 100%; 28 | padding-bottom: $spacing-03; 29 | margin-bottom: $spacing-07; 30 | width: 100%; 31 | } 32 | 33 | .cds--text-area__wrapper { 34 | width: 100%; 35 | } 36 | -------------------------------------------------------------------------------- /DEVELOPING.md: -------------------------------------------------------------------------------- 1 | 2 | # Developing 3 | 4 | ## Directory structure 5 | 6 | ```none 7 | ├── app.js // Express routes 8 | ├── config // Express configuration 9 | ├── doc // Documentation 10 | ├── public // Static resources 11 | ├── scripts // Selenium tests 12 | ├── server.js // Node.js server entry point 13 | ├── src // React client 14 | │ └── index.js // React App entry point 15 | └── test // Puppet tests 16 | ``` 17 | 18 | ## Running locally for debugging 19 | 20 | 1. Install the dependencies 21 | 22 | ```bash 23 | npm install 24 | ``` 25 | 26 | 1. Build the application 27 | 28 | ```bash 29 | npm run build 30 | ``` 31 | 32 | 1. Run the application 33 | 34 | ```bash 35 | npm run dev 36 | ``` -------------------------------------------------------------------------------- /src/components/TranscriptBox/_transcript-box.scss: -------------------------------------------------------------------------------- 1 | @use '@carbon/react/scss/components/button/' as *; 2 | @use '@carbon/react/scss/theme' as *; 3 | @use '@carbon/react/scss/spacing' as *; 4 | 5 | .transcript-box { 6 | background-color: $layer-02; 7 | border-bottom: 1px solid $border-strong-01; 8 | min-height: 10rem; 9 | padding: $spacing-05; 10 | 11 | span { 12 | font-size: 0.875rem; 13 | font-weight: 400; 14 | line-height: 1.25rem; 15 | letter-spacing: 0.16px; 16 | 17 | &.speaker-label { 18 | &--0 { 19 | font-weight: 700; 20 | color: $button-primary; 21 | } 22 | 23 | &--1 { 24 | font-weight: 700; 25 | color: $button-secondary; 26 | } 27 | } 28 | } 29 | 30 | .keyword-info-trigger { 31 | font-size: 0.875rem; 32 | font-weight: 700; 33 | line-height: 1.25rem; 34 | letter-spacing: 0.16px; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /config/security.js: -------------------------------------------------------------------------------- 1 | // security.js 2 | const secure = require('express-secure-only'); 3 | const helmet = require('helmet'); 4 | const rateLimit = require('express-rate-limit'); 5 | 6 | module.exports = function secureApp(app) { 7 | app.use(secure()); 8 | app.use( 9 | helmet({ 10 | contentSecurityPolicy: { 11 | directives: { 12 | defaultSrc: ["'self'"], 13 | scriptSrc: ["'self'"], 14 | connectSrc: ["'self'", "wss://*.speech-to-text.watson.cloud.ibm.com"], 15 | fontSrc: ["'self'", "https://fonts.gstatic.com"], 16 | imageSrc: ["'self"], 17 | objectSrc: ["'none'"], 18 | upgradeInsecureRequests: [], 19 | } 20 | } 21 | }) 22 | ); 23 | 24 | const limiter = rateLimit({ 25 | windowMs: 30 * 1000, // seconds 26 | max: 5, 27 | message: JSON.stringify({ 28 | error: 'Too many requests, please try again in 30 seconds.', 29 | code: 429, 30 | }), 31 | }); 32 | app.use('/api/', limiter); 33 | }; 34 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.access.redhat.com/ubi8/nodejs-18-minimal:1 AS base 2 | 3 | USER 1001 4 | 5 | WORKDIR /opt/app-root/src 6 | 7 | FROM base as build 8 | COPY --chown=1001:1001 ./package*.js* /opt/app-root/src/ 9 | RUN chmod 777 /opt/app-root/src/package-lock.json 10 | 11 | RUN npm set progress=false && \ 12 | npm config set depth 0 && \ 13 | npm ci --only-production --ignore-scripts 14 | 15 | COPY ./config /opt/app-root/src/config 16 | COPY ./public /opt/app-root/src/public 17 | COPY ./src /opt/app-root/src/src 18 | COPY ./test /opt/app-root/src/test 19 | COPY ./*.js /opt/app-root/src/ 20 | 21 | RUN npm run build 22 | RUN npm run test:components 23 | 24 | FROM base as release 25 | 26 | USER 1001 27 | 28 | COPY --from=build /opt/app-root/src/build /opt/app-root/src/build 29 | COPY --from=build /opt/app-root/src/config /opt/app-root/src/config 30 | COPY --from=build /opt/app-root/src/*.js* /opt/app-root/src/ 31 | 32 | RUN npm install --only=prod 33 | 34 | ENV PORT 5000 35 | 36 | EXPOSE 5000 37 | CMD ["npm", "start"] 38 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | #### Experience Testing 2 | 3 | This application includes a UI test for the application experience written in Python, using Selenium, that you can extend as you further develop your application. 4 | 5 | To run the experience test, first you need to [install Python](https://www.python.org/downloads/) on your system. 6 | 7 | Then install selenium. 8 | ```bash 9 | pip install selenium 10 | ``` 11 | [Set up the Chrome WebDriver](https://chromedriver.chromium.org/getting-started) on your system, you will also need to install the [Chrome Browser](https://www.google.com/chrome/) if you do not have it. If you prefer, you can update the experience test experience_test.py to use the [Firefox WebDriver](https://developer.mozilla.org/en-US/docs/Web/WebDriver). Make sure that you have added the driver to your system PATH. 12 | 13 | With the application running locally (with `npm run start` or `ibmcloud dev run`), export the necessary environment variables, and run the Python web experience test in this directory. 14 | ```bash 15 | export APP_URL=https://localhost:5000 # default value for the local application 16 | python3 experience_test.py 17 | ``` 18 | -------------------------------------------------------------------------------- /scripts/experience_test.py: -------------------------------------------------------------------------------- 1 | import os, time, sys, datetime 2 | from selenium import webdriver 3 | from selenium.webdriver.chrome.options import Options 4 | from selenium.webdriver.common.by import By 5 | 6 | date = datetime.datetime.now().strftime("%Y-%m-%d-%H%M:%S:%f") 7 | 8 | # Do an action on the app's landing page 9 | options = Options() 10 | options.add_argument('--headless') 11 | options.add_argument('--no-sandbox') 12 | options.add_argument('--disable-dev-shm-usage') 13 | driver = webdriver.Chrome(options=options) 14 | success = False 15 | try: 16 | app_url = os.environ.get("APP_URL", "http://localhost:5000/") 17 | print("APP_URL: ", app_url) 18 | driver.get(app_url) # Open a browser to the app's landing page 19 | print("Title: ", driver.title) 20 | expected_title = "Speech to Text" 21 | if driver.title != expected_title: 22 | raise Exception("Title should be " + expected_title) 23 | 24 | # Find button and click it 25 | sample_button = driver.find_element(By.XPATH, "//button[contains(text(),'Play audio sample')]") # Locate the button 26 | sample_button.click() 27 | 28 | # Verify the action on the app's landing page 29 | time.sleep(30) 30 | transcript = driver.find_element(By.CLASS_NAME, 'transcript-box').text.splitlines() 31 | print("Transcript: ", transcript) 32 | expected = "So thank you very much for coming" # The beginning of the text... 33 | 34 | if expected not in transcript[0]: 35 | raise Exception("Did not get the expected transcript") 36 | 37 | success = True 38 | 39 | except Exception as e: 40 | print("Exception: ", e) 41 | raise 42 | 43 | finally: 44 | driver.quit() 45 | if success: 46 | print("Experience Test Successful") 47 | else: 48 | sys.exit("Experience Test Failed") 49 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 15 | 16 | 25 | Speech to Text 26 | 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/components/OutputContainer/OutputContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { FormGroup, Tile } from '@carbon/react'; 4 | import AudioWave from '../AudioWave'; 5 | import TranscriptBox from '../TranscriptBox'; 6 | 7 | export const OutputContainer = ({ 8 | audioAnalyzer, 9 | audioDataArray, 10 | audioDuration, 11 | audioSource, 12 | audioWaveContainerRef, 13 | isTranscribing, 14 | keywordInfo, 15 | transcriptArray, 16 | }) => ( 17 | 18 |

Output

19 | 20 | 28 | 29 | 30 | 34 | 35 |
36 | ); 37 | 38 | OutputContainer.propTypes = { 39 | audioAnalyzer: PropTypes.object.isRequired, 40 | audioDataArray: PropTypes.arrayOf(PropTypes.number), 41 | audioDuration: PropTypes.number, 42 | audioSource: PropTypes.string, 43 | audioWaveContainerRef: PropTypes.object.isRequired, 44 | isTranscribing: PropTypes.bool, 45 | keywordInfo: PropTypes.arrayOf(PropTypes.object), 46 | transcriptArray: PropTypes.arrayOf(PropTypes.object), 47 | }; 48 | 49 | OutputContainer.defaultProps = { 50 | audioDataArray: [], 51 | audioDuration: 0, 52 | audioSource: '', 53 | isTranscribing: false, 54 | keywordInfo: [], 55 | transcriptArray: [], 56 | }; 57 | 58 | export default OutputContainer; 59 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Copy this file to .env and replace the credentials with 2 | # your own before starting the app. 3 | 4 | #---------------------------------------------------------- 5 | # IBM Cloud 6 | # 7 | # If your services are running on IBM Cloud, 8 | # uncomment and configure these. 9 | # Remove or comment out the IBM Cloud Pak for Data sections. 10 | #---------------------------------------------------------- 11 | 12 | # SPEECH_TO_TEXT_AUTH_TYPE=iam 13 | # SPEECH_TO_TEXT_APIKEY= 14 | # SPEECH_TO_TEXT_URL= 15 | 16 | #---------------------------------------------------------- 17 | # IBM Cloud Pak for Data (username and password) 18 | # 19 | # If your services are running on IBM Cloud Pak for Data, 20 | # uncomment and configure these. 21 | # Remove or comment out the IBM Cloud section. 22 | #---------------------------------------------------------- 23 | 24 | # SPEECH_TO_TEXT_AUTH_TYPE=cp4d 25 | # SPEECH_TO_TEXT_URL=https://{cpd_cluster_host}{:port}/speech-to-text/{release}/instances/{instance_id}/api 26 | # SPEECH_TO_TEXT_AUTH_URL=https://{cpd_cluster_host}{:port} 27 | # SPEECH_TO_TEXT_USERNAME= 28 | # SPEECH_TO_TEXT_PASSWORD= 29 | # # If you use a self-signed certificate, you need to disable SSL verification. 30 | # # This is not secure and not recommended. 31 | # # SPEECH_TO_TEXT_AUTH_DISABLE_SSL=true 32 | 33 | # # Optional: Instead of the above IBM Cloud Pak for Data credentials... 34 | # # For testing and development, you can use the bearer token that's displayed 35 | # # in the IBM Cloud Pak for Data web client. To find this token, view the 36 | # # details for the provisioned service instance. The details also include the 37 | # # service endpoint URL. Only disable SSL if necessary (insecure). 38 | # # Don't use this token in production because it does not expire. 39 | # # 40 | # # SPEECH_TO_TEXT_AUTH_TYPE=bearertoken 41 | # # SPEECH_TO_TEXT_BEARER_TOKEN= 42 | # # SPEECH_TO_TEXT_URL= -------------------------------------------------------------------------------- /src/components/Header/_header.scss: -------------------------------------------------------------------------------- 1 | @use '@carbon/react/scss/colors' as *; 2 | @use '@carbon/react/scss/spacing' as *; 3 | @import './overrides'; 4 | 5 | .header { 6 | background-color: $gray-100; 7 | display: flex; 8 | min-height: unset; 9 | padding: $spacing-07; 10 | 11 | .link-container { 12 | align-items: flex-end; 13 | display: flex; 14 | flex-grow: 1; 15 | justify-content: flex-end; 16 | 17 | .link-wrapper { 18 | align-items: flex-end; 19 | display: flex; 20 | flex-direction: column; 21 | 22 | @media (min-width: 992px) { 23 | justify-content: flex-end; 24 | } 25 | 26 | @media (min-width: 1200px) { 27 | align-items: center; 28 | flex-direction: row; 29 | height: min-content; 30 | } 31 | 32 | .link { 33 | color: $blue-40; 34 | 35 | &.getting-started:hover { 36 | text-decoration: none; 37 | } 38 | 39 | &:not(:last-child) { 40 | padding-bottom: $spacing-03; 41 | 42 | @media (min-width: 1200px) { 43 | padding-bottom: 0; 44 | padding-right: $spacing-07; 45 | } 46 | } 47 | 48 | &-icon { 49 | display: block; 50 | fill: $white-0; 51 | 52 | @media (min-width: 992px) { 53 | display: none; 54 | } 55 | } 56 | 57 | &-text { 58 | display: none; 59 | text-align: end; 60 | 61 | @media (min-width: 992px) { 62 | display: block; 63 | } 64 | 65 | @media (min-width: 1200px) { 66 | text-align: unset; 67 | } 68 | } 69 | 70 | &-button { 71 | border-color: $white-0; 72 | color: $white-0; 73 | display: none; 74 | text-align: end; 75 | 76 | &:hover { 77 | background-color: $white-0; 78 | color: $gray-100; 79 | 80 | .bx--btn__icon path { 81 | fill: $gray-100; 82 | } 83 | } 84 | 85 | @media (min-width: 992px) { 86 | display: block; 87 | } 88 | 89 | @media (min-width: 1200px) { 90 | text-align: unset; 91 | } 92 | } 93 | } 94 | } 95 | } 96 | 97 | .title-container { 98 | color: $gray-10; 99 | display: flex; 100 | flex-direction: column; 101 | width: 70%; 102 | 103 | @media (min-width: 1200px) { 104 | width: 40%; 105 | } 106 | 107 | .header-title { 108 | padding-bottom: $spacing-06; 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /test/page.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | jest.setTimeout(10000); 3 | 4 | const TIMEOUT = { timeout: 3000 }; 5 | describe('Input methods', () => { 6 | beforeEach(async () => { 7 | await page.goto('http://localhost:5000'); 8 | }); 9 | 10 | it('Sample audio', async () => { 11 | 12 | await page.content(); 13 | 14 | // Select English 15 | await (await page.waitForXPath('//*[@id="language-model-dropdown"]', TIMEOUT)).click(); 16 | await (await page.waitForXPath('//*[text()="US English (8khz Narrowband)"]', TIMEOUT)).click(); 17 | 18 | // Add custom keywords. 19 | const TEST_KEYWORDS = ', course, I'; 20 | const keywords = await page.waitForXPath('//*[@class="cds--text-area cds--text-area--light"]', TIMEOUT); 21 | await keywords.type(TEST_KEYWORDS); 22 | const keywordsContent = await page.evaluate(el => el.textContent, keywords); 23 | expect(keywordsContent).toContain(TEST_KEYWORDS) 24 | 25 | // Choose to detect speakers. 26 | await (await page.waitForXPath('//*[@class="cds--toggle__switch"]', TIMEOUT)).click(); 27 | 28 | // Play sample audio. 29 | await (await page.waitForXPath('//*[text()="Play audio sample"]', TIMEOUT)).click(); 30 | 31 | // Wait for the audio to play for a bit. 32 | await page.waitForTimeout(5000); 33 | 34 | // Check transcript (CI checks box exists. Missing creds to check content.) 35 | expect(await page.waitForXPath('//*[@class="transcript-box"]', TIMEOUT)).toBeTruthy(); 36 | 37 | /* With proper creds, this logs content and tests first words... 38 | const transcriptElement = await page.waitForXPath('//*[@class="transcript-box"]/div/span', TIMEOUT); 39 | const transcript = await page.evaluate(el => el.textContent, transcriptElement); 40 | console.log("TRANSCRIPT: ", transcript); 41 | expect(transcript).toContain("Thank you"); 42 | */ 43 | }); 44 | 45 | it('File upload', async () => { 46 | 47 | await page.content(); 48 | 49 | // Select English 50 | await (await page.waitForXPath('//*[@id="language-model-dropdown"]', TIMEOUT)).click(); 51 | await (await page.waitForXPath('//*[text()="US English (16khz Broadband)"]', TIMEOUT)).click(); 52 | 53 | // Upload file. 54 | await (await page.waitForXPath('//*[@id="id1"]', TIMEOUT)).uploadFile('public/audio/en-US_Broadband-sample.wav'); 55 | 56 | // Wait for the audio to play for a bit. 57 | await page.waitForTimeout(5000); 58 | 59 | // Check transcript (CI checks box exists. Missing creds to check content.) 60 | expect(await page.waitForXPath('//*[@class="transcript-box"]', TIMEOUT)).toBeTruthy(); 61 | 62 | /* With proper creds, this logs content and tests first words... 63 | const transcript = await page.evaluate(el => el.textContent, await page.waitForXPath('//*[@class="transcript-box"]/div/span', TIMEOUT)); 64 | console.log("TRANSCRIPT: ", transcript); 65 | expect(transcript).toContain("So thank you very much for coming"); 66 | */ 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, Link } from '@carbon/react'; 3 | import { 4 | Api_1, 5 | Document, 6 | IbmCloud, 7 | Launch, 8 | LogoGithub 9 | } from '@carbon/react/icons'; 10 | import Header from './components/Header'; 11 | import ServiceContainer from './components/ServiceContainer'; 12 | import useScript from './hooks/useScript'; 13 | 14 | const HEADER_TITLE = 'Watson Speech to Text'; 15 | const HEADER_DESCRIPTION = 16 | 'IBM Watson Speech to Text is a cloud-native API that transforms voice into written text.'; 17 | const HEADER_LINKS = [ 18 | 25 |

API reference

26 | 27 | , 28 | 35 |

Documentation

36 | 37 | , 38 | 45 |

GitHub

46 | 47 | , 48 | 55 | 58 | 59 | , 60 | ]; 61 | 62 | export const App = () => { 63 | useScript( 64 | 'https://cdn.jsdelivr.net/gh/watson-developer-cloud/watson-developer-cloud.github.io@master/analytics.js', 65 | ); 66 | 67 | return ( 68 |
69 |
74 | 75 |
76 | ); 77 | }; 78 | 79 | export default App; 80 | -------------------------------------------------------------------------------- /src/components/Toast/Toast.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { ToastNotification } from '@carbon/react'; 4 | 5 | const NOTIFICATION_HAS_BEEN_SEEN = 'notificationHasBeenSeen'; 6 | 7 | export const Toast = ({ 8 | caption, 9 | children, 10 | className, 11 | hideAfterFirstDisplay, 12 | hideCloseButton, 13 | kind, 14 | lowContrast, 15 | onCloseButtonClick, 16 | role, 17 | subtitle, 18 | timeout, 19 | title, 20 | }) => { 21 | const [id, setId] = useState(); 22 | const [hideToast, setHideToast] = useState(false); 23 | 24 | useEffect(() => { 25 | setId( 26 | Math.random() 27 | .toString(36) 28 | .substring(2, 15) + 29 | Math.random() 30 | .toString(36) 31 | .substring(2, 15), 32 | ); 33 | }, []); 34 | 35 | useEffect(() => { 36 | const element = document.querySelector(`.custom-toast-${id}`); 37 | if (element) { 38 | element.className += 'enter'; 39 | } 40 | }, [id]); 41 | 42 | useEffect(() => { 43 | if ( 44 | hideAfterFirstDisplay && 45 | typeof window !== 'undefined' && 46 | typeof window.localStorage !== 'undefined' && 47 | window.localStorage.getItem(NOTIFICATION_HAS_BEEN_SEEN) === 'true' 48 | ) { 49 | setHideToast(true); 50 | } 51 | }, [hideAfterFirstDisplay]); 52 | 53 | return hideToast ? null : ( 54 | { 61 | if ( 62 | hideAfterFirstDisplay && 63 | typeof window !== 'undefined' && 64 | typeof window.localStorage !== 'undefined' 65 | ) { 66 | window.localStorage.setItem(NOTIFICATION_HAS_BEEN_SEEN, 'true'); 67 | } 68 | onCloseButtonClick(); 69 | }} 70 | role={role} 71 | subtitle={subtitle} 72 | timeout={timeout} 73 | title={title} 74 | > 75 | {children} 76 | 77 | ); 78 | }; 79 | 80 | Toast.propTypes = { 81 | caption: PropTypes.string, 82 | children: PropTypes.node, 83 | className: PropTypes.string, 84 | hideAfterFirstDisplay: PropTypes.bool, 85 | hideCloseButton: PropTypes.bool, 86 | kind: PropTypes.string, 87 | lowContrast: PropTypes.bool, 88 | onCloseButtonClick: PropTypes.func, 89 | role: PropTypes.string, 90 | subtitle: PropTypes.string, 91 | timeout: PropTypes.number, 92 | title: PropTypes.string, 93 | }; 94 | 95 | Toast.defaultProps = { 96 | caption: '', 97 | children: null, 98 | className: '', 99 | hideAfterFirstDisplay: true, 100 | hideCloseButton: false, 101 | kind: 'error', 102 | lowContrast: false, 103 | onCloseButtonClick: () => {}, 104 | role: 'alert', 105 | subtitle: '', 106 | timeout: 0, 107 | title: '', 108 | }; 109 | 110 | export default Toast; 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ibm-watson/speech-to-text-code-pattern", 3 | "version": "0.1.0", 4 | "proxy": "http://localhost:5000", 5 | "private": true, 6 | "dependencies": { 7 | "@craco/craco": "^7.0.0", 8 | "@types/react": "^18.0.12", 9 | "body-parser": "^1.20.1", 10 | "buffer": "^6.0.3", 11 | "concurrently": "^7.6.0", 12 | "cross-env": "^7.0.3", 13 | "dotenv": "^16.0.3", 14 | "es6-promise": "^4.2.8", 15 | "express": "^4.18.2", 16 | "express-rate-limit": "^6.7.0", 17 | "express-secure-only": "^0.2.1", 18 | "helmet": "^6.0.1", 19 | "husky": "^8.0.3", 20 | "ibm-watson": "^7.1.2", 21 | "isomorphic-fetch": "^3.0.0", 22 | "lint-staged": "^13.1.2", 23 | "morgan": "^1.10.0", 24 | "process": "^0.11.10", 25 | "stream-browserify": "^3.0.0", 26 | "vcap_services": "^0.7.1", 27 | "watson-speech": "^0.41.0" 28 | }, 29 | "scripts": { 30 | "dev": "concurrently \"npm:client\" \"npm:server\"", 31 | "client": "craco start", 32 | "server": "nodemon server.js", 33 | "start": "node server.js", 34 | "build": "INLINE_RUNTIME_CHUNK=false craco build", 35 | "test": "npm run test:components && npm run test:integration", 36 | "test:components": "cross-env CI=true craco test --env=jsdom --passWithNoTests", 37 | "test:integration": "JEST_PUPPETEER_CONFIG='test/jest-puppeteer.config.js' jest test -c test/jest.config.js", 38 | "prepare": "husky install" 39 | }, 40 | "eslintConfig": { 41 | "extends": "react-app" 42 | }, 43 | "engines": { 44 | "node": "^18.0.0" 45 | }, 46 | "browserslist": { 47 | "production": [ 48 | ">0.2%", 49 | "not dead", 50 | "not op_mini all" 51 | ], 52 | "development": [ 53 | "last 1 chrome version", 54 | "last 1 firefox version", 55 | "last 1 safari version" 56 | ] 57 | }, 58 | "lint-staged": { 59 | "./**/*.{js,scss,html,png,yaml,yml}": [ 60 | "npm run build" 61 | ] 62 | }, 63 | "devDependencies": { 64 | "@carbon/react": "^1.22.0", 65 | "@testing-library/jest-dom": "^5.16.5", 66 | "@testing-library/react": "^12.1.5", 67 | "@testing-library/user-event": "^14.4.3", 68 | "jest": "29.4.2", 69 | "jest-puppeteer": "^7.0.0", 70 | "nodemon": "^2.0.20", 71 | "prettier": "^2.8.4", 72 | "puppeteer": "^19", 73 | "react": "^17.0.2", 74 | "react-dom": "^17.0.2", 75 | "react-json-tree": "^0.18.0", 76 | "react-json-view": "^1.21.3", 77 | "react-scripts": "^5.0.1", 78 | "sass": "^1.58.0", 79 | "webpack": "^5.75.0" 80 | }, 81 | "overrides": { 82 | "@craco/craco": { 83 | "react-scripts": "5.0.1" 84 | } 85 | }, 86 | "prettier": { 87 | "trailingComma": "all", 88 | "singleQuote": true 89 | }, 90 | "nodemonConfig": { 91 | "watch": [ 92 | "app.js", 93 | "config/**/*.js", 94 | "server.js" 95 | ], 96 | "ext": "js", 97 | "ignore": [ 98 | ".git", 99 | "node_modules", 100 | "public", 101 | "src", 102 | "test" 103 | ], 104 | "delay": 500 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Maintainers Guide 2 | 3 | This guide is intended for maintainers - anybody with commit access to one or 4 | more Code Pattern repositories. 5 | 6 | ## Methodology 7 | 8 | This repository does not have a traditional release management cycle, but 9 | should instead be maintained as a useful, working, and polished reference at 10 | all times. While all work can therefore be focused on the master branch, the 11 | quality of this branch should never be compromised. 12 | 13 | The remainder of this document details how to merge pull requests to the 14 | repositories. 15 | 16 | ## Merge approval 17 | 18 | The project maintainers use LGTM (Looks Good To Me) in comments on the pull 19 | request to indicate acceptance prior to merging. A change requires LGTMs from 20 | two project maintainers. If the code is written by a maintainer, the change 21 | only requires one additional LGTM. 22 | 23 | ## Reviewing Pull Requests 24 | 25 | We recommend reviewing pull requests directly within GitHub. This allows a 26 | public commentary on changes, providing transparency for all users. When 27 | providing feedback be civil, courteous, and kind. Disagreement is fine, so long 28 | as the discourse is carried out politely. If we see a record of uncivil or 29 | abusive comments, we will revoke your commit privileges and invite you to leave 30 | the project. 31 | 32 | During your review, consider the following points: 33 | 34 | ### Does the change have positive impact? 35 | 36 | Some proposed changes may not represent a positive impact to the project. Ask 37 | whether or not the change will make understanding the code easier, or if it 38 | could simply be a personal preference on the part of the author (see 39 | [bikeshedding](https://en.wiktionary.org/wiki/bikeshedding)). 40 | 41 | Pull requests that do not have a clear positive impact should be closed without 42 | merging. 43 | 44 | ### Do the changes make sense? 45 | 46 | If you do not understand what the changes are or what they accomplish, ask the 47 | author for clarification. Ask the author to add comments and/or clarify test 48 | case names to make the intentions clear. 49 | 50 | At times, such clarification will reveal that the author may not be using the 51 | code correctly, or is unaware of features that accommodate their needs. If you 52 | feel this is the case, work up a code sample that would address the pull 53 | request for them, and feel free to close the pull request once they confirm. 54 | 55 | ### Does the change introduce a new feature? 56 | 57 | For any given pull request, ask yourself "is this a new feature?" If so, does 58 | the pull request (or associated issue) contain narrative indicating the need 59 | for the feature? If not, ask them to provide that information. 60 | 61 | Are new unit tests in place that test all new behaviors introduced? If not, do 62 | not merge the feature until they are! Is documentation in place for the new 63 | feature? (See the documentation guidelines). If not do not merge the feature 64 | until it is! Is the feature necessary for general use cases? Try and keep the 65 | scope of any given component narrow. If a proposed feature does not fit that 66 | scope, recommend to the user that they maintain the feature on their own, and 67 | close the request. You may also recommend that they see if the feature gains 68 | traction among other users, and suggest they re-submit when they can show such 69 | support. 70 | -------------------------------------------------------------------------------- /src/components/ServiceContainer/reducer.js: -------------------------------------------------------------------------------- 1 | export const actionTypes = { 2 | setAudioAnalyzer: 'SET_AUDIO_ANALYZER', 3 | setAudioContext: 'SET_AUDIO_CONTEXT', 4 | setAudioSource: 'SET_AUDIO_SOURCE', 5 | setAudioStream: 'SET_AUDIO_STREAM', 6 | setAudioVisualizationData: 'SET_AUDIO_VISUALIZATION_DATA', 7 | setError: 'SET_ERROR', 8 | setSpeakerLabels: 'SET_SPEAKER_LABELS', 9 | setIsRecording: 'SET_IS_RECORDING', 10 | setIsSamplePlaying: 'SET_IS_SAMPLE_PLAYING', 11 | setIsTranscribing: 'SET_IS_TRANSCRIBING', 12 | setIsUploadPlaying: 'SET_IS_UPLOAD_PLAYING', 13 | updateResults: 'UPDATE_RESULTS', 14 | }; 15 | 16 | export const initialState = { 17 | audioAnalyzer: {}, 18 | audioContext: null, 19 | audioDataArray: [], 20 | audioDurationInMs: 0, 21 | audioSource: '', 22 | audioStream: null, 23 | error: null, 24 | isRecording: false, 25 | isSamplePlaying: false, 26 | isTranscribing: false, 27 | isUploadPlaying: false, 28 | keywordInfo: [], 29 | speakerLabels: [], 30 | transcript: [], 31 | }; 32 | 33 | export const reducer = (state, action) => { 34 | switch (action.type) { 35 | case 'SET_AUDIO_ANALYZER': { 36 | return { 37 | ...state, 38 | audioAnalyzer: action.audioAnalyzer, 39 | }; 40 | } 41 | case 'SET_AUDIO_CONTEXT': { 42 | return { 43 | ...state, 44 | audioContext: action.audioContext, 45 | }; 46 | } 47 | case 'SET_AUDIO_SOURCE': { 48 | return { 49 | ...state, 50 | audioSource: action.audioSource, 51 | }; 52 | } 53 | case 'SET_AUDIO_STREAM': { 54 | return { 55 | ...state, 56 | audioStream: action.audioStream, 57 | }; 58 | } 59 | case 'SET_AUDIO_VISUALIZATION_DATA': { 60 | return { 61 | ...state, 62 | audioDataArray: action.audioDataArray, 63 | audioDurationInMs: action.audioDurationInMs, 64 | }; 65 | } 66 | case 'SET_ERROR': { 67 | return { 68 | ...state, 69 | error: action.error, 70 | }; 71 | } 72 | case 'SET_IS_RECORDING': { 73 | return { 74 | ...state, 75 | isRecording: action.isRecording, 76 | }; 77 | } 78 | case 'SET_IS_SAMPLE_PLAYING': { 79 | return { 80 | ...state, 81 | isSamplePlaying: action.isSamplePlaying, 82 | }; 83 | } 84 | case 'SET_IS_TRANSCRIBING': { 85 | return { 86 | ...state, 87 | isTranscribing: action.isTranscribing, 88 | }; 89 | } 90 | case 'SET_IS_UPLOAD_PLAYING': { 91 | return { 92 | ...state, 93 | isUploadPlaying: action.isUploadPlaying, 94 | }; 95 | } 96 | case 'SET_SPEAKER_LABELS': { 97 | return { 98 | ...state, 99 | speakerLabels: action.speakerLabels, 100 | }; 101 | } 102 | case 'UPDATE_RESULTS': { 103 | let updatedTranscript = [...state.transcript]; 104 | if (action.resultIndex === 0) { 105 | updatedTranscript = action.transcript; 106 | } else { 107 | updatedTranscript[action.resultIndex] = action.transcript[0]; 108 | } 109 | 110 | return { 111 | ...state, 112 | keywordInfo: action.keywordInfo, 113 | transcript: updatedTranscript, 114 | }; 115 | } 116 | default: { 117 | throw new Error(); 118 | } 119 | } 120 | }; 121 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const { Cp4dTokenManager, IamTokenManager } = require('ibm-watson/auth'); 2 | const path = require('path'); 3 | const express = require('express'); 4 | const vcapServices = require('vcap_services'); 5 | const app = express(); 6 | require('./config/express')(app); 7 | 8 | // For starter kit env. 9 | require('dotenv').config({ 10 | silent: true 11 | }); 12 | const skitJson = JSON.parse(process.env.service_watson_speech_to_text || "{}"); 13 | const vcapCredentials = vcapServices.getCredentials('speech_to_text'); 14 | 15 | // Look for credentials in all the possible places 16 | const apikey = process.env.SPEECH_TO_TEXT_APIKEY || process.env.SPEECHTOTEXT_APIKEY || vcapCredentials?.apikey || skitJson?.apikey; 17 | const url = process.env.SPEECH_TO_TEXT_URL || process.env.SPEECHTOTEXT_URL || vcapCredentials?.url || skitJson?.url; 18 | 19 | let bearerToken = process.env.SPEECH_TO_TEXT_BEARER_TOKEN; 20 | 21 | // Ensure we have a SPEECH_TO_TEXT_AUTH_TYPE so we can get a token for the UI. 22 | let sttAuthType = process.env.SPEECH_TO_TEXT_AUTH_TYPE; 23 | if (!sttAuthType) { 24 | sttAuthType = 'iam'; 25 | } else { 26 | sttAuthType = sttAuthType.toLowerCase(); 27 | } 28 | // Get a token manager for IAM or CP4D. 29 | let tokenManager = false; 30 | if (sttAuthType === 'cp4d') { 31 | tokenManager = new Cp4dTokenManager({ 32 | username: process.env.SPEECH_TO_TEXT_USERNAME, 33 | password: process.env.SPEECH_TO_TEXT_PASSWORD, 34 | url: process.env.SPEECH_TO_TEXT_AUTH_URL, 35 | disableSslVerification: process.env.SPEECH_TO_TEXT_AUTH_DISABLE_SSL || false 36 | }); 37 | } else if (sttAuthType === 'iam') { 38 | try { 39 | tokenManager = new IamTokenManager({ apikey }); 40 | } catch (err) { 41 | console.log("Error: ", err); 42 | } 43 | } else if (sttAuthType === 'bearertoken') { 44 | console.log('SPEECH_TO_TEXT_AUTH_TYPE=bearertoken is for dev use only.'); 45 | } else { 46 | console.log('SPEECH_TO_TEXT_AUTH_TYPE =', sttAuthType); 47 | console.log('SPEECH_TO_TEXT_AUTH_TYPE is not recognized.'); 48 | } 49 | 50 | const getToken = async () => { 51 | let tokenResponse = {}; 52 | 53 | try { 54 | if (tokenManager) { 55 | const token = await tokenManager.getToken(); 56 | tokenResponse = { 57 | ...tokenResponse, 58 | accessToken: token, 59 | url, 60 | }; 61 | } else if (bearerToken && url) { 62 | tokenResponse = { 63 | ...tokenResponse, 64 | accessToken: bearerToken, 65 | url, 66 | }; 67 | } else { 68 | tokenResponse = { 69 | ...tokenResponse, 70 | error: { 71 | title: 'No valid credentials found', 72 | description: 73 | 'Could not find valid credentials for the Speech to Text service.', 74 | statusCode: 401, 75 | }, 76 | }; 77 | } 78 | } catch (err) { 79 | console.log("Error: ", err); 80 | tokenResponse = { 81 | ...tokenResponse, 82 | error: { 83 | title: 'Authentication error', 84 | description: 85 | 'There was a problem authenticating with the Speech to Text service.', 86 | statusCode: 400, 87 | }, 88 | }; 89 | } 90 | 91 | return tokenResponse; 92 | }; 93 | 94 | app.get('/', (_, res) => { 95 | res.sendFile(path.join(__dirname, 'build', 'index.html')); 96 | }); 97 | 98 | app.get('/health', (_, res) => { 99 | res.json({ status: 'UP' }); 100 | }); 101 | 102 | app.get('/api/auth', async (_, res, next) => { 103 | const token = await getToken(); 104 | 105 | if (token.error) { 106 | console.error(token.error); 107 | next(token.error); 108 | } else { 109 | return res.json(token); 110 | } 111 | }); 112 | 113 | // error-handler settings for all other routes 114 | require('./config/error-handler')(app); 115 | 116 | module.exports = app; 117 | -------------------------------------------------------------------------------- /doc/source/local.md: -------------------------------------------------------------------------------- 1 | # Run locally 2 | 3 | This document shows how to run the `speech-to-text-code-pattern` application on your local machine. 4 | 5 | ## Steps 6 | 7 | 1. [Clone the repo](#clone-the-repo) 8 | 1. [Configure credentials](#configure-credentials) 9 | 1. [Start the server](#start-the-server) 10 | 11 | ### Clone the repo 12 | 13 | Clone `speech-to-text-code-pattern` repo locally. In a terminal, run: 14 | 15 | ```bash 16 | git clone https://github.com/IBM/speech-to-text-code-pattern 17 | cd speech-to-text-code-pattern 18 | ``` 19 | 20 | ### Configure credentials 21 | 22 | Copy the `.env.example` file to `.env`. 23 | 24 | ```bash 25 | cp .env.example .env 26 | ``` 27 | 28 | Edit the `.env` file to configure credentials before starting the Node.js server. 29 | The credentials to configure will depend on whether you are provisioning services using IBM Cloud Pak for Data or on IBM Cloud. 30 | 31 | Click to expand one: 32 | 33 |
IBM Cloud Pak for Data 34 |

35 | 36 | For the **Speech to Text** service, the following settings are needed: 37 | 38 | * Set SPEECH_TO_TEXT_AUTH_TYPE to cp4d 39 | * Provide the SPEECH_TO_TEXT_URL, SPEECH_TO_TEXT_USERNAME and SPEECH_TO_TEXT_PASSWORD collected in the previous step. 40 | * For the SPEECH_TO_TEXT_AUTH_URL use the base fragment of your URL including the host and port. I.e. https://{cpd_cluster_host}{:port}. 41 | * If your CPD installation is using a self-signed certificate, you need to disable SSL verification with SPEECH_TO_TEXT_AUTH_DISABLE_SSL set to true. You might also need to use browser-specific steps to ignore certificate errors (try browsing to the AUTH_URL). Disable SSL only if absolutely necessary, and take steps to enable SSL as soon as possible. 42 | * Make sure the examples for IBM Cloud and bearer token auth are commented out (or removed). 43 | 44 | ```bash 45 | #---------------------------------------------------------- 46 | # IBM Cloud Pak for Data (username and password) 47 | # 48 | # If your services are running on IBM Cloud Pak for Data, 49 | # uncomment and configure these. 50 | # Remove or comment out the IBM Cloud section. 51 | #---------------------------------------------------------- 52 | 53 | SPEECH_TO_TEXT_AUTH_TYPE=cp4d 54 | SPEECH_TO_TEXT_URL=https://{cpd_cluster_host}{:port}/speech-to-text/{release}/instances/{instance_id}/api 55 | SPEECH_TO_TEXT_AUTH_URL=https://{cpd_cluster_host}{:port} 56 | SPEECH_TO_TEXT_USERNAME= 57 | SPEECH_TO_TEXT_PASSWORD= 58 | # If you use a self-signed certificate, you need to disable SSL verification. 59 | # This is not secure and not recommended. 60 | # SPEECH_TO_TEXT_AUTH_DISABLE_SSL=true 61 | ``` 62 | 63 |

64 |
65 | 66 |
IBM Cloud 67 |

68 | 69 | For the Speech to Text service, the following settings are needed: 70 | 71 | * Set SPEECH_TO_TEXT_AUTH_TYPE to iam 72 | * Provide the SPEECH_TO_TEXT_URL and SPEECH_TO_TEXT_APIKEY collected in the previous step. 73 | * Make sure the examples for IBM Cloud Pak for Data and bearer token auth are commented out (or removed). 74 |

75 | 76 | ```bash 77 | #---------------------------------------------------------- 78 | # IBM Cloud 79 | # 80 | # If your services are running on IBM Cloud, 81 | # uncomment and configure these. 82 | # Remove or comment out the IBM Cloud Pak for Data sections. 83 | #---------------------------------------------------------- 84 | 85 | SPEECH_TO_TEXT_AUTH_TYPE=iam 86 | SPEECH_TO_TEXT_APIKEY= 87 | SPEECH_TO_TEXT_URL= 88 | ``` 89 | 90 |

91 |
92 | 93 | > Need more information? See the [authentication wiki](https://github.com/IBM/node-sdk-core/blob/master/AUTHENTICATION.md). 94 | 95 | ### Start the server 96 | 97 | ```bash 98 | npm install 99 | npm start 100 | ``` 101 | 102 | The application will be available in your browser at http://localhost:5000. Return to the README.md for instructions on how to use the app. 103 | 104 | [![return](https://raw.githubusercontent.com/IBM/pattern-utils/master/deploy-buttons/return.png)](../../README.md#3-use-the-app) 105 | -------------------------------------------------------------------------------- /src/components/ServiceContainer/utils.js: -------------------------------------------------------------------------------- 1 | const AUDIO_VISUALIZATION_DIMENSIONS = { 2 | DATA_POINT_WIDTH: 1, 3 | DATA_POINT_HEIGHT: 50, 4 | DATA_POINT_MARGIN: 2, 5 | DATA_POINT_X_OFFSET: 25, 6 | DATA_POINT_Y_OFFSET: 50, 7 | }; 8 | 9 | const readFileToArrayBuffer = fileData => { 10 | const fileReader = new FileReader(); 11 | 12 | return new Promise((resolve, reject) => { 13 | fileReader.onload = () => { 14 | const arrayBuffer = fileReader.result; 15 | resolve(arrayBuffer); 16 | }; 17 | 18 | fileReader.onerror = () => { 19 | fileReader.abort(); 20 | reject(new Error('failed to process file')); 21 | }; 22 | 23 | // Initiate the conversion. 24 | fileReader.readAsArrayBuffer(fileData); 25 | }); 26 | }; 27 | 28 | export const formatStreamData = data => { 29 | const { results, result_index: resultIndex } = data; 30 | 31 | let finalKeywords = []; 32 | const finalTranscript = []; 33 | let isFinal = false; 34 | 35 | results.forEach(result => { 36 | const { final } = result; 37 | let alternatives = null; 38 | let speaker = null; 39 | let keywords_result = null; 40 | 41 | if (final) { 42 | ({ alternatives, speaker, keywords_result } = result); 43 | } else { 44 | ({ alternatives, speaker } = result); 45 | } 46 | 47 | // Extract the main alternative to get keywords. 48 | const [mainAlternative] = alternatives; 49 | const { transcript } = mainAlternative; 50 | 51 | if (speaker === undefined) { 52 | speaker = null; 53 | } 54 | 55 | // Push object to final transcript. 56 | finalTranscript.push({ 57 | final, 58 | speaker, 59 | text: transcript, 60 | }); 61 | 62 | isFinal = final; 63 | 64 | // Push keywords to final keyword list. 65 | if (keywords_result) { 66 | finalKeywords.push(keywords_result); 67 | } 68 | }); 69 | 70 | return { 71 | transcript: finalTranscript, 72 | keywordInfo: finalKeywords, 73 | resultIndex, 74 | final: isFinal, 75 | }; 76 | }; 77 | 78 | export const convertAudioBlobToVisualizationData = async ( 79 | audioBlob, 80 | audioCtx, 81 | audioWaveContainerWidth, 82 | ) => { 83 | const audioArrayBuffer = await readFileToArrayBuffer(audioBlob); 84 | const audioUint8Array = new Uint8Array(audioArrayBuffer.slice(0)); 85 | 86 | // NOTE: BaseAudioContext.decodeAudioData has a promise syntax 87 | // which we are unable to use in order to be compatible with Safari. 88 | // Therefore, we wrap the callback syntax in a promise to give us the same 89 | // effect while ensuring compatibility 90 | // see more: https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext/decodeAudioData#Browser_compatibility 91 | return new Promise((resolve, reject) => { 92 | audioCtx.decodeAudioData( 93 | audioArrayBuffer, 94 | audioDataBuffer => { 95 | const { duration } = audioDataBuffer; 96 | 97 | const { DATA_POINT_MARGIN } = AUDIO_VISUALIZATION_DIMENSIONS; 98 | const validContainerWidth = 99 | audioWaveContainerWidth - DATA_POINT_MARGIN * 2; 100 | const numberOfChunks = Math.floor(validContainerWidth / 2); 101 | const chunkSize = audioUint8Array.length / numberOfChunks; 102 | 103 | const chunkedAudioDataArray = []; 104 | for (let i = 1; i < numberOfChunks; i += 1) { 105 | let previousIndex = i - 1; 106 | if (previousIndex < 0) { 107 | previousIndex = 0; 108 | } 109 | 110 | chunkedAudioDataArray.push( 111 | audioUint8Array.slice(previousIndex * chunkSize, i * chunkSize), 112 | ); 113 | } 114 | 115 | const reducedFloatArray = chunkedAudioDataArray.map(chunk => { 116 | const totalValue = chunk.reduce( 117 | (prevValue, currentValue) => prevValue + currentValue, 118 | ); 119 | const floatValue = totalValue / (chunkSize * 255); 120 | return floatValue; 121 | }); 122 | 123 | resolve({ 124 | duration, 125 | reducedFloatArray, 126 | }); 127 | }, 128 | () => { 129 | reject(new Error('failed to chunk audio')); 130 | }, 131 | ); 132 | }); 133 | }; 134 | -------------------------------------------------------------------------------- /src/components/TranscriptBox/TranscriptBox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { DefinitionTooltip } from '@carbon/react'; 4 | import KeywordTooltip from '../KeywordTooltip'; 5 | import { createWordRegex } from './utils'; 6 | 7 | const mapTranscriptTextToElements = (text, keywordInfo, totalIndex) => { 8 | let finalSentenceArray = []; 9 | let matches = []; 10 | 11 | if (keywordInfo.length > 0) { 12 | const regex = createWordRegex(keywordInfo); 13 | matches = text.split(regex); 14 | } 15 | 16 | // If we don't have words to find yet, just return the interim text. 17 | if (matches.length === 0) { 18 | return [ 19 | { 20 | text, 21 | type: 'normal', 22 | }, 23 | ]; 24 | } 25 | 26 | const wordOccurences = {}; 27 | finalSentenceArray = matches.map((sentenceFragment, index) => { 28 | // Use lowercased version when searching through keyword map. 29 | const fragmentToSearch = sentenceFragment.toLowerCase(); 30 | 31 | if (index % 2 === 0) { 32 | return { 33 | text: sentenceFragment, 34 | type: 'normal', 35 | }; 36 | } 37 | 38 | // Find keyword info object to use based on text from sentenceFragment and 39 | // current index in wordOccurences. 40 | const keywordInfoMatch = 41 | keywordInfo[totalIndex] && keywordInfo[totalIndex][fragmentToSearch]; 42 | let keywordOccurenceIndex = 0; 43 | if (wordOccurences[fragmentToSearch]) { 44 | keywordOccurenceIndex = wordOccurences[fragmentToSearch]; 45 | wordOccurences[fragmentToSearch] += 1; 46 | } else { 47 | wordOccurences[fragmentToSearch] = 1; 48 | } 49 | const infoForOccurence = 50 | keywordInfoMatch && keywordInfoMatch[keywordOccurenceIndex]; 51 | 52 | // Bail in case we can't get the keyword info for whatever reason. 53 | if (!infoForOccurence) { 54 | return {}; 55 | } 56 | 57 | return { 58 | text: sentenceFragment, 59 | type: 'keyword', 60 | startTime: infoForOccurence.start_time, 61 | endTime: infoForOccurence.end_time, 62 | confidence: infoForOccurence.confidence, 63 | }; 64 | }); 65 | 66 | return finalSentenceArray; 67 | }; 68 | 69 | export const TranscriptBox = ({ keywordInfo, transcriptArray }) => { 70 | return ( 71 |
72 | {transcriptArray.map((transcriptItem, overallIndex) => { 73 | const { speaker, text } = transcriptItem; 74 | const parsedTextElements = mapTranscriptTextToElements( 75 | text, 76 | keywordInfo, 77 | overallIndex, 78 | ); 79 | 80 | return ( 81 |
82 | {speaker !== null && ( 83 | 84 | {`Speaker ${speaker}: `} 85 | 86 | )} 87 | {parsedTextElements.map((element, elementIndex) => { 88 | if (!element) { 89 | return null; 90 | } 91 | 92 | if (element.type === 'normal') { 93 | return ( 94 | {`${element.text}`} 97 | ); 98 | } else if (element.type === 'keyword') { 99 | return ( 100 | 109 | } 110 | triggerClassName="keyword-info-trigger" 111 | > 112 | {element.text} 113 | 114 | ); 115 | } 116 | 117 | return null; 118 | })} 119 |
120 | ); 121 | })} 122 |
123 | ); 124 | }; 125 | 126 | TranscriptBox.propTypes = { 127 | keywordInfo: PropTypes.arrayOf(PropTypes.object), 128 | transcriptArray: PropTypes.arrayOf(PropTypes.object), 129 | }; 130 | 131 | TranscriptBox.defaultProps = { 132 | keywordInfo: [], 133 | transcriptArray: [], 134 | }; 135 | 136 | export default TranscriptBox; 137 | -------------------------------------------------------------------------------- /src/components/ControlContainer/ControlContainer.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { 4 | Dropdown, 5 | FormGroup, 6 | TextArea, 7 | Tile, 8 | Toggle, 9 | } from '@carbon/react'; 10 | import SubmitContainer from '../SubmitContainer'; 11 | import models from '../../data/models.json'; 12 | 13 | export const ControlContainer = ({ 14 | isRecording, 15 | isSamplePlaying, 16 | isUploadPlaying, 17 | onError, 18 | onSelectNewModel, 19 | onStartPlayingFileUpload, 20 | onStopPlayingFileUpload, 21 | onStartPlayingSample, 22 | onStopPlayingSample, 23 | onStartRecording, 24 | onStopRecording, 25 | }) => { 26 | const dropdownChoices = models.map(model => ({ 27 | id: model.name, 28 | label: model.description, 29 | supportsSpeakerLabels: model.supported_features.speaker_labels, 30 | })); 31 | 32 | const [model, selectModel] = useState(dropdownChoices[0]); 33 | const [keywordText, setKeywordText] = useState(models[0].keywords); 34 | const [useSpeakerLabels, setUseSpeakerLabels] = useState(false); 35 | 36 | const onChangeLanguageModel = newModel => { 37 | selectModel(newModel.selectedItem); 38 | 39 | const newKeywordText = models.find( 40 | model => model.name === newModel.selectedItem.id, 41 | ).keywords; 42 | setKeywordText(newKeywordText); 43 | 44 | if (useSpeakerLabels && !newModel.selectedItem.supportsSpeakerLabels) { 45 | setUseSpeakerLabels(false); 46 | } 47 | 48 | onSelectNewModel(); 49 | }; 50 | 51 | return ( 52 | 53 |

Input

54 | 55 | 64 | 65 | 66 |