├── .dockerignore ├── .npmrc ├── public ├── robots.txt ├── favicon.ico ├── manifest.json └── index.html ├── src ├── components │ ├── Header │ │ ├── index.js │ │ ├── _overrides.scss │ │ ├── Header.js │ │ └── _header.scss │ ├── Toast │ │ ├── index.js │ │ ├── _toast.scss │ │ └── Toast.js │ ├── OutputContainer │ │ ├── index.js │ │ ├── _output-container.scss │ │ └── OutputContainer.js │ ├── ControlContainer │ │ ├── index.js │ │ ├── _control-container.scss │ │ ├── _overrides.scss │ │ ├── utils.js │ │ └── ControlContainer.js │ └── ServiceContainer │ │ ├── index.js │ │ ├── _service-container.scss │ │ ├── utils.js │ │ └── ServiceContainer.js ├── utils.js ├── styles │ ├── _index.scss │ └── main.scss ├── setupTests.js ├── hooks │ └── useScript.js ├── index.js ├── App.js ├── serviceWorker.js └── data │ └── sampleText.js ├── .husky └── pre-commit ├── doc └── source │ ├── images │ ├── ui.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 ├── server.js ├── .github ├── dependabot.yml └── workflows │ ├── nodejs.yml │ └── stale.yml ├── Dockerfile-tools ├── config ├── express.js ├── security.js └── error-handler.js ├── TESTING.md ├── scripts ├── experience_test.sh ├── README.md └── experience_test.py ├── .eslintrc.json ├── CONTRIBUTING.md ├── DEVELOPING.md ├── Dockerfile ├── ACKNOWLEDGEMENTS.md ├── .env.example ├── package.json ├── app.js ├── MAINTAINERS.md ├── README.md └── LICENSE /.dockerignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules/ -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.com/ 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/text-to-speech-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 | -------------------------------------------------------------------------------- /doc/source/images/ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/text-to-speech-code-pattern/HEAD/doc/source/images/ui.png -------------------------------------------------------------------------------- /test/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'jest-puppeteer', 3 | testRegex: './*\\.test\\.js$', 4 | }; 5 | -------------------------------------------------------------------------------- /src/components/OutputContainer/index.js: -------------------------------------------------------------------------------- 1 | import OutputContainer from './OutputContainer'; 2 | export default OutputContainer; 3 | -------------------------------------------------------------------------------- /doc/source/images/cf_deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/text-to-speech-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/text-to-speech-code-pattern/HEAD/doc/source/images/architecture.png -------------------------------------------------------------------------------- /doc/source/images/services_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/text-to-speech-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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | .npm 3 | node_modules 4 | 5 | # Config 6 | .next 7 | 8 | # Misc 9 | .DS_Store 10 | .env 11 | /build 12 | -------------------------------------------------------------------------------- /doc/source/images/toolchain_pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/text-to-speech-code-pattern/HEAD/doc/source/images/toolchain_pipeline.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/styles/_index.scss: -------------------------------------------------------------------------------- 1 | @use '@carbon/react/scss/theme' as *; 2 | 3 | body { 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 | .cds--link { 4 | &:focus { 5 | outline: none; 6 | } 7 | 8 | &:visited { 9 | color: $blue-40; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /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": "TTS Code Pattern", 3 | "name": "Text to Speech 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 | -------------------------------------------------------------------------------- /src/components/ServiceContainer/_service-container.scss: -------------------------------------------------------------------------------- 1 | @use '@carbon/react/scss/spacing' as *; 2 | 3 | .service-container { 4 | align-items: center; 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: unset; 8 | overflow-x: hidden; 9 | padding: $spacing-07; 10 | position: relative; 11 | 12 | @media (min-width: 1200px) { 13 | align-items: unset; 14 | flex-direction: row; 15 | justify-content: center; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /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 21 | -------------------------------------------------------------------------------- /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/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 | 7 | // vendors 8 | @use '@carbon/react'; 9 | 10 | @import './index'; 11 | 12 | // components 13 | @import '../components/ControlContainer/control-container'; 14 | @import '../components/Header/header'; 15 | @import '../components/OutputContainer/output-container'; 16 | @import '../components/ServiceContainer/service-container'; 17 | @import '../components/Toast/toast'; 18 | -------------------------------------------------------------------------------- /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(morgan('dev')); 18 | }; 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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). 28 | -------------------------------------------------------------------------------- /.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/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 | min-height: unset; 8 | padding: $spacing-08; 9 | padding-bottom: $spacing-07 * 2; 10 | width: 100%; 11 | 12 | @media (min-width: 1200px) { 13 | flex: 1 1 auto; 14 | margin-left: $spacing-09; 15 | } 16 | 17 | .audio-output { 18 | width: 100%; 19 | } 20 | 21 | .container-title { 22 | padding-bottom: $spacing-09; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /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(helmet()); 9 | 10 | const limiter = rateLimit({ 11 | windowMs: 30 * 1000, // seconds 12 | max: 5, 13 | message: JSON.stringify({ 14 | error: 'Too many requests, please try again in 30 seconds.', 15 | code: 429, 16 | }), 17 | }); 18 | app.use('/api/', limiter); 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 | -------------------------------------------------------------------------------- /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/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 | min-height: unset; 10 | padding: $spacing-08; 11 | padding-bottom: $spacing-07 * 2; 12 | width: 100%; 13 | 14 | @media (min-width: 1200px) { 15 | flex: 1 1 auto; 16 | margin-bottom: 0; 17 | } 18 | 19 | .container-title { 20 | padding-bottom: $spacing-09; 21 | } 22 | 23 | .voice-info { 24 | padding-bottom: $spacing-07; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/components/OutputContainer/OutputContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { FormGroup, Tile } from '@carbon/react'; 4 | 5 | export const OutputContainer = ({ audioElementRef }) => { 6 | return ( 7 | 8 |

Output

9 | 10 | 13 | 14 |
15 | ); 16 | }; 17 | 18 | OutputContainer.propTypes = { 19 | audioElementRef: PropTypes.shape({ current: PropTypes.instanceOf(Element) }), 20 | }; 21 | 22 | OutputContainer.defaultProps = { 23 | audioElementRef: null, 24 | }; 25 | 26 | export default OutputContainer; 27 | -------------------------------------------------------------------------------- /src/components/ControlContainer/utils.js: -------------------------------------------------------------------------------- 1 | // This function removes V2 models from selection and does a little extra formatting to the labels. 2 | export const mapVoicesToDropdownItems = voices => 3 | voices 4 | .filter(voice => voice.name.indexOf('V2') === -1) 5 | .sort((voiceA, voiceB) => 6 | voiceA.description.localeCompare(voiceB.description), 7 | ) 8 | .map(voice => { 9 | const isV3 = voice.name.indexOf('V3') > -1; 10 | const colonIndex = voice.description.indexOf(':'); 11 | const voicePersonName = voice.description.substring(0, colonIndex); 12 | const restOfDescription = voice.description.substring( 13 | colonIndex + 1, 14 | voice.description.indexOf('.'), 15 | ); 16 | const label = `${voicePersonName}${ 17 | isV3 ? ' (V3)' : '' 18 | }:${restOfDescription}`; 19 | 20 | return { 21 | id: voice.name, 22 | label, 23 | }; 24 | }); 25 | -------------------------------------------------------------------------------- /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 | ``` 37 | 38 | 1. View the application in a browser at `localhost:3000` 39 | 40 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/components/ServiceContainer/utils.js: -------------------------------------------------------------------------------- 1 | export const canPlayAudioFormat = (mimeType, audioElement) => { 2 | if (!audioElement) { 3 | audioElement = document.createElement('audio'); 4 | } 5 | 6 | if (audioElement) { 7 | return ( 8 | typeof audioElement.canPlayType === 'function' && 9 | audioElement.canPlayType(mimeType) !== '' 10 | ); 11 | } 12 | return false; 13 | }; 14 | 15 | /** 16 | * @return {Function} A polyfill for URLSearchParams 17 | */ 18 | export const getSearchParams = () => { 19 | if (typeof URLSearchParams === 'function') { 20 | return new URLSearchParams(); 21 | } 22 | 23 | // Simple polyfill for URLSearchparams 24 | const SearchParams = function SearchParams() {}; 25 | 26 | SearchParams.prototype.set = function set(key, value) { 27 | this[key] = value; 28 | }; 29 | 30 | SearchParams.prototype.toString = function toString() { 31 | return Object.keys(this) 32 | .map(v => `${encodeURI(v)}=${encodeURI(this[v])}`) 33 | .join('&'); 34 | }; 35 | 36 | return new SearchParams(); 37 | }; 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 | -------------------------------------------------------------------------------- /test/page.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | jest.setTimeout(10000); 3 | 4 | describe('App functionality', () => { 5 | beforeEach(async () => { 6 | await page.goto('http://localhost:5000'); 7 | }); 8 | 9 | it('Text synthesis', async () => { 10 | await page.waitForSelector('div.cds--dropdown', { 11 | timeout: 0, 12 | }); 13 | 14 | // Choose voice model. 15 | await expect(page).toClick('div.cds--dropdown'); 16 | await expect(page).toClick('div.cds--list-box__menu-item__option', { 17 | text: 'Allison (V3): American English female voice', 18 | }); 19 | 20 | // Add some text. 21 | await expect(page).toFill( 22 | 'textarea.cds--text-area', 23 | 'Good news, good news, good news, thats all they wanna hear.', 24 | ); 25 | 26 | // Synthesize text. 27 | await expect(page).toClick('button', { 28 | text: 'Synthesize', 29 | }); 30 | 31 | // Wait for the audio to play for a bit. 32 | await page.waitForTimeout(1000); 33 | 34 | // Assert that the audio element now has a source. 35 | const audioElement = await page.$('audio.audio-output'); 36 | const audioSrc = await audioElement.getProperty('src'); 37 | expect(audioSrc).toBeTruthy(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /ACKNOWLEDGEMENTS.md: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | 3 | * Original development by the Watson Developer Cloud team (https://watson-developer-cloud.github.io/). 4 | 5 | * This app was originally licensed under the [MIT License](https://opensource.org/licenses/MIT). 6 | 7 | * Most of the example text uses a statement that is © European Union, 2018. Those rights are retained and any reuse should follow their policy: 8 | 9 | 1. Reuse of data published on this website for commercial or non-commercial purposes is authorised provided the source is acknowledged. The reuse policy of the European Commission is implemented by a Decision of 12 December 2011. 10 | 2. The following rules apply: 11 | * Downloading and reproduction are authorised provided the source is acknowledged. 12 | * Translations of publications into languages other than the language editions published on this website are subject to the conclusion of a free-of-charge license agreement. 13 | * Where the content of EU publications is incorporated in works that are sold (regardless of their medium), the natural or legal person publishing the works must inform buyers, both before they pay any subscription or fee and each time they access the works, that the information taken from EU publications may be obtained, in electronic format, free of charge through this website. 14 | 15 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 15 | 16 | 25 | Text to Speech 26 | 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /.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 | # TEXT_TO_SPEECH_AUTH_TYPE=iam 13 | # TEXT_TO_SPEECH_APIKEY= 14 | # TEXT_TO_SPEECH_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 | # TEXT_TO_SPEECH_AUTH_TYPE=cp4d 25 | # TEXT_TO_SPEECH_URL=https://{cpd_cluster_host}{:port}/text-to-speech/{release}/instances/{instance_id}/api 26 | # TEXT_TO_SPEECH_AUTH_URL=https://{cpd_cluster_host}{:port} 27 | # TEXT_TO_SPEECH_USERNAME= 28 | # TEXT_TO_SPEECH_PASSWORD= 29 | # # If you use a self-signed certificate, you need to disable SSL verification. 30 | # # This is not secure and not recommended. 31 | # TEXT_TO_SPEECH_DISABLE_SSL=true 32 | # TEXT_TO_SPEECH_AUTH_DISABLE_SSL=true 33 | 34 | 35 | # # Optional: Instead of the above IBM Cloud Pak for Data credentials... 36 | # # For testing and development, you can use the bearer token that's displayed 37 | # # in the IBM Cloud Pak for Data web client. To find this token, view the 38 | # # details for the provisioned service instance. The details also include the 39 | # # service endpoint URL. Only disable SSL if necessary (insecure). 40 | # # Don't use this token in production because it does not expire. 41 | # # 42 | # TEXT_TO_SPEECH_AUTH_TYPE=bearertoken 43 | # TEXT_TO_SPEECH_BEARER_TOKEN= 44 | # TEXT_TO_SPEECH_URL= 45 | # # TEXT_TO_SPEECH_DISABLE_SSL=true -------------------------------------------------------------------------------- /src/components/ServiceContainer/ServiceContainer.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import ControlContainer from '../ControlContainer'; 3 | import OutputContainer from '../OutputContainer'; 4 | import Toast from '../Toast'; 5 | import { createError } from '../../utils'; 6 | import { canPlayAudioFormat, getSearchParams } from './utils'; 7 | 8 | const SYNTHESIZE_ERROR_TITLE = 'Text synthesis error'; 9 | const GDPR_DISCLAIMER = 10 | 'This system is for demonstration purposes only and is not intended to process Personal Data. No Personal Data is to be entered into this system as it may not have the necessary controls in place to meet the requirements of the General Data Protection Regulation (EU) 2016/679.'; 11 | 12 | export const ServiceContainer = () => { 13 | const [error, setError] = useState(); 14 | let audioElementRef = useRef(null); 15 | 16 | const getSynthesizeUrl = (text, voice) => { 17 | const params = getSearchParams(); 18 | 19 | params.set('text', text); 20 | params.set('voice', voice.id); 21 | 22 | let accept; 23 | if (canPlayAudioFormat('audio/mp3', audioElementRef.current)) { 24 | accept = 'audio/mp3'; 25 | } else if ( 26 | canPlayAudioFormat('audio/ogg;codec=opus', audioElementRef.current) 27 | ) { 28 | accept = 'audio/ogg;codec=opus'; 29 | } else if (canPlayAudioFormat('audio/wav', audioElementRef.current)) { 30 | accept = 'audio/wav'; 31 | } 32 | if (accept) { 33 | params.set('accept', accept); 34 | } 35 | 36 | return `/api/synthesize?${params.toString()}`; 37 | }; 38 | 39 | const onSynthesize = async (text, voice) => { 40 | try { 41 | audioElementRef.current.setAttribute( 42 | 'src', 43 | getSynthesizeUrl(text, voice), 44 | ); 45 | await audioElementRef.current.play(); 46 | } catch (err) { 47 | setError(createError(SYNTHESIZE_ERROR_TITLE, err.message)); 48 | } 49 | }; 50 | 51 | return ( 52 |
53 | 54 | {error && ( 55 | { 62 | setError(null); 63 | }} 64 | /> 65 | )} 66 | 67 | 68 |
69 | ); 70 | }; 71 | 72 | export default ServiceContainer; 73 | -------------------------------------------------------------------------------- /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 Text to Speech'; 15 | const HEADER_DESCRIPTION = 16 | 'The Watson Text to Speech service understands text and natural language to generate synthesized audio output complete with appropriate cadence and intonation.'; 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ibm-watson/text-to-speech-code-pattern", 3 | "version": "0.1.0", 4 | "proxy": "http://localhost:5000", 5 | "private": true, 6 | "dependencies": { 7 | "@carbon/react": "^1.22.0", 8 | "axios": "^0.27.2", 9 | "body-parser": "^1.20.1", 10 | "concurrently": "^7.6.0", 11 | "cross-env": "^7.0.3", 12 | "dotenv": "^16.0.3", 13 | "express": "^4.18.2", 14 | "express-rate-limit": "^6.7.0", 15 | "express-secure-only": "^0.2.1", 16 | "helmet": "^6.0.1", 17 | "husky": "^8.0.3", 18 | "ibm-watson": "^7.1.2", 19 | "lint-staged": "^13.1.1", 20 | "morgan": "^1.10.0", 21 | "sass": "^1.58.0" 22 | }, 23 | "scripts": { 24 | "dev": "concurrently \"npm:client\" \"npm:server\"", 25 | "client": "react-scripts start", 26 | "server": "nodemon server.js", 27 | "start": "node server.js", 28 | "build": "INLINE_RUNTIME_CHUNK=false react-scripts build", 29 | "test": "npm run test:components && npm run test:integration", 30 | "test:components": "cross-env CI=true react-scripts test --env=jsdom --passWithNoTests", 31 | "test:integration": "JEST_PUPPETEER_CONFIG='test/jest-puppeteer.config.js' jest test -c test/jest.config.js", 32 | "prepare": "husky install" 33 | }, 34 | "eslintConfig": { 35 | "extends": "react-app" 36 | }, 37 | "engines": { 38 | "node": "^18.0.0" 39 | }, 40 | "browserslist": { 41 | "production": [ 42 | ">0.2%", 43 | "not dead", 44 | "not op_mini all" 45 | ], 46 | "development": [ 47 | "last 1 chrome version", 48 | "last 1 firefox version", 49 | "last 1 safari version" 50 | ] 51 | }, 52 | "lint-staged": { 53 | "./**/*.{js,scss,html,png,yaml,yml}": [ 54 | "npm run build" 55 | ] 56 | }, 57 | "devDependencies": { 58 | "@testing-library/jest-dom": "^5.16.5", 59 | "@testing-library/react": "^12.1.5", 60 | "@testing-library/user-event": "^14.4.3", 61 | "jest-puppeteer": "^6.2.0", 62 | "nodemon": "^2.0.20", 63 | "prettier": "^2.8.4", 64 | "puppeteer": "^18.0.3", 65 | "react": "^17.0.2", 66 | "react-dom": "^17.0.2", 67 | "react-json-tree": "^0.18.0", 68 | "react-json-view": "^1.21.3", 69 | "react-scripts": "5.0.1" 70 | }, 71 | "prettier": { 72 | "trailingComma": "all", 73 | "singleQuote": true 74 | }, 75 | "nodemonConfig": { 76 | "watch": [ 77 | "app.js", 78 | "config/**/*.js", 79 | "server.js" 80 | ], 81 | "ext": "js", 82 | "ignore": [ 83 | ".git", 84 | "node_modules", 85 | "public", 86 | "src", 87 | "test" 88 | ], 89 | "delay": 500 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/components/Header/_header.scss: -------------------------------------------------------------------------------- 1 | @use '@carbon/react/scss/spacing' as *; 2 | @use '@carbon/react/scss/colors' 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 | .cds--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 | -------------------------------------------------------------------------------- /scripts/experience_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import sys 4 | from selenium import webdriver 5 | from selenium.webdriver.chrome.options import Options 6 | from selenium.webdriver.common.by import By 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 | 15 | success = False 16 | 17 | try: 18 | app_url = os.environ.get("APP_URL", "http://localhost:5000/") 19 | print("APP_URL: ", app_url) 20 | driver.get(app_url) # Open a browser to the app's landing page 21 | 22 | time.sleep(3) # Init time needed? 23 | 24 | print("Title: ", driver.title) 25 | expected_title = "Text to Speech" 26 | if driver.title != expected_title: 27 | raise Exception("Title should be " + expected_title) 28 | 29 | audio_button = driver.find_element(By.CLASS_NAME, 'audio-output') 30 | src = audio_button.get_attribute("src") 31 | print("AUDIO SOURCE: ", src) 32 | 33 | # Find and select Allison V3 34 | drop_down = driver.find_element(By.XPATH, "//button[@id='downshift-0-toggle-button']") 35 | drop_down.click() 36 | drop_down_element = driver.find_element(By.XPATH, "//div[contains(text(),'Allison (V3)')]") 37 | drop_down_element.click() 38 | 39 | # Find button and click it 40 | synthesize_button = driver.find_element(By.XPATH, "//button[contains(text(),'Synthesize')]") 41 | synthesize_button.click() 42 | 43 | time.sleep(20) 44 | 45 | # Verify the action on the app's landing page 46 | # Input text 47 | text_to_say = driver.find_element(By.ID, "text-input").text 48 | print("SAY: ", text_to_say) 49 | 50 | expected = "Conscious of its spiritual and moral heritage" 51 | if expected not in text_to_say: 52 | raise Exception("Did not get the expected text to say") 53 | else: 54 | print("First Test Successful") 55 | 56 | # Test that we got some audio 57 | audio_button = driver.find_element(By.CLASS_NAME, 'audio-output') 58 | src = audio_button.get_attribute("src") 59 | print("AUDIO SOURCE: ", src) 60 | 61 | expected = "api/synthesize?text=Conscious+of+its+spiritual+and+moral+heritage" 62 | if expected not in src: 63 | raise Exception("Did not get the expected audio src") 64 | else: 65 | print("Second Test Successful") 66 | 67 | success = True 68 | 69 | except Exception as e: 70 | print("Exception: ", e) 71 | raise 72 | 73 | finally: 74 | driver.quit() 75 | if success: 76 | print("Experience Test Successful") 77 | else: 78 | sys.exit("Experience Test Failed") 79 | -------------------------------------------------------------------------------- /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().toString(36).substring(2, 15) + 27 | Math.random().toString(36).substring(2, 15), 28 | ); 29 | }, []); 30 | 31 | useEffect(() => { 32 | const element = document.querySelector(`.custom-toast-${id}`); 33 | if (element) { 34 | element.className += 'enter'; 35 | } 36 | }, [id]); 37 | 38 | useEffect(() => { 39 | if ( 40 | hideAfterFirstDisplay && 41 | typeof window !== undefined && 42 | typeof window.localStorage !== undefined && 43 | window.localStorage.getItem(NOTIFICATION_HAS_BEEN_SEEN) === 'true' 44 | ) { 45 | setHideToast(true); 46 | } 47 | }, [hideAfterFirstDisplay]); 48 | 49 | return hideToast ? null : ( 50 | { 57 | if ( 58 | hideAfterFirstDisplay && 59 | typeof window !== undefined && 60 | typeof window.localStorage !== undefined 61 | ) { 62 | window.localStorage.setItem(NOTIFICATION_HAS_BEEN_SEEN, 'true'); 63 | } 64 | onCloseButtonClick(); 65 | }} 66 | role={role} 67 | subtitle={subtitle} 68 | timeout={timeout} 69 | title={title} 70 | > 71 | {children} 72 | 73 | ); 74 | }; 75 | 76 | Toast.propTypes = { 77 | caption: PropTypes.string, 78 | children: PropTypes.node, 79 | className: PropTypes.string, 80 | hideAfterFirstDisplay: PropTypes.bool, 81 | hideCloseButton: PropTypes.bool, 82 | kind: PropTypes.string, 83 | lowContrast: PropTypes.bool, 84 | onCloseButtonClick: PropTypes.func, 85 | role: PropTypes.string, 86 | subtitle: PropTypes.string, 87 | timeout: PropTypes.number, 88 | title: PropTypes.string, 89 | }; 90 | 91 | Toast.defaultProps = { 92 | caption: '', 93 | children: null, 94 | className: '', 95 | hideAfterFirstDisplay: true, 96 | hideCloseButton: false, 97 | kind: 'error', 98 | lowContrast: false, 99 | onCloseButtonClick: () => {}, 100 | role: 'alert', 101 | subtitle: '', 102 | timeout: 0, 103 | title: '', 104 | }; 105 | 106 | export default Toast; 107 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const TextToSpeechV1 = require('ibm-watson/text-to-speech/v1.js'); 2 | const path = require('path'); 3 | const express = require('express'); 4 | const app = express(); 5 | require('./config/express')(app); 6 | 7 | // For starter kit env. 8 | require('dotenv').config({ 9 | silent: true 10 | }); 11 | const pEnv = process.env; 12 | const skitJson = JSON.parse(process.env.service_watson_text_to_speech || "{}"); 13 | 14 | // Look for credentials in all the places 15 | const apikey = process.env.TEXT_TO_SPEECH_APIKEY || process.env.TEXTTOSPEECH_APIKEY || skitJson?.apikey; 16 | const url = process.env.TEXT_TO_SPEECH_URL || 17 | process.env.TEXTTOSPEECH_URL || skitJson?.url; 18 | 19 | // A null/undefined service env var would actually cause 20 | // the core SDK to throw an error in integration tests 21 | // and fail the test, but if the env var is left unset 22 | // it won't 23 | if (apikey) { 24 | process.env.TEXT_TO_SPEECH_APIKEY = apikey; 25 | } 26 | 27 | if (url) { 28 | process.env.TEXT_TO_SPEECH_URL = url; 29 | } 30 | 31 | // Create Text to Speech client. 32 | let client; 33 | try { 34 | client = new TextToSpeechV1({ version: '2020-06-02' }); 35 | } catch (err) { 36 | console.error('Error creating service client: ', err); 37 | } 38 | 39 | // Caching issues are causing alerts. Avoid cache. 40 | var options = { 41 | etag: false, 42 | maxAge: '0', 43 | setHeaders: function (res, _path, _stat) { 44 | res.set('Cache-Control', 'private, no-cache, no-store, must-revalidate'); 45 | res.header('Pragma', 'no-cache'); 46 | res.header('Expires', '0'); 47 | } 48 | } 49 | 50 | app.use(express.static(path.join(__dirname, 'build'), options )); 51 | 52 | app.get('/health', (_, res) => { 53 | res.json({ status: 'UP' }); 54 | }); 55 | 56 | app.get('/api/voices', async (_, res, next) => { 57 | try { 58 | if (client) { 59 | const { result } = await client.listVoices(); 60 | return res.json(result); 61 | } else { 62 | // Return Allison for testing and user still gets creds pop-up. 63 | return res.json( 64 | { voices: [ 65 | { name: 'en-US_AllisonV3Voice', 66 | description: 'Allison: American English female voice. Dnn technology.', 67 | }] 68 | }); 69 | } 70 | } catch (err) { 71 | console.error(err); 72 | if (!client) { 73 | err.statusCode = 401; 74 | err.description = 75 | 'Could not find valid credentials for the Text to Speech service.'; 76 | err.title = 'Invalid credentials'; 77 | } 78 | next(err); 79 | } 80 | }); 81 | 82 | app.get('/api/synthesize', async (req, res, next) => { 83 | try { 84 | const { result } = await client.synthesize(req.query); 85 | result.pipe(res); 86 | } catch (err) { 87 | console.error(err); 88 | if (!client) { 89 | err.statusCode = 401; 90 | err.description = 91 | 'Could not find valid credentials for the Text to Speech service.'; 92 | err.title = 'Invalid credentials'; 93 | } 94 | next(err); 95 | } 96 | }); 97 | 98 | // error-handler settings for all other routes 99 | require('./config/error-handler')(app); 100 | 101 | module.exports = app; 102 | -------------------------------------------------------------------------------- /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/ControlContainer/ControlContainer.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { VolumeUpFilled } from '@carbon/react/icons'; 4 | import { 5 | Button, 6 | Dropdown, 7 | DropdownSkeleton, 8 | FormGroup, 9 | TextArea, 10 | Tile, 11 | } from '@carbon/react'; 12 | import axios from 'axios'; 13 | import { sampleText } from '../../data/sampleText'; 14 | import { mapVoicesToDropdownItems } from './utils'; 15 | 16 | const VOICES_ENDPOINT = '/api/voices'; 17 | 18 | export const ControlContainer = ({ onSynthesize }) => { 19 | const [voices, setVoices] = useState([]); 20 | const [selectedVoice, setSelectedVoice] = useState(); 21 | const [text, setText] = useState(''); 22 | const [isLoading, setIsLoading] = useState(true); 23 | const [isError, setIsError] = useState(false); 24 | 25 | // Get voices data 26 | useEffect(() => { 27 | axios(VOICES_ENDPOINT) 28 | .then(({ data }) => setVoices(data.voices)) 29 | .catch(err => { 30 | console.log(err); 31 | setIsError(true); 32 | }) 33 | .finally(setIsLoading(false)); 34 | }, []); 35 | 36 | // Default to initial voice once all voices are loaded. 37 | useEffect(() => { 38 | if (voices[1]) { 39 | onSelectVoice(mapVoicesToDropdownItems(voices)[1]); 40 | } 41 | }, [voices]); 42 | 43 | const onSelectVoice = voice => { 44 | setSelectedVoice(voice); 45 | 46 | const text = sampleText[voice.id]; 47 | setText(text); 48 | }; 49 | 50 | return ( 51 | 52 |

Input

53 |

54 | For optimal naturalness, select the (V3) voices, which are built using 55 | deep neural networks. 56 |

57 | 58 | {isLoading || (voices.length === 0 && !isError) ? ( 59 | 60 | ) : ( 61 | { 65 | onSelectVoice(newModel.selectedItem); 66 | }} 67 | items={mapVoicesToDropdownItems(voices)} 68 | selectedItem={selectedVoice && selectedVoice.label} 69 | ariaLabel="Voice model selection dropdown" 70 | light 71 | /> 72 | )} 73 | 74 | 75 |