├── .nvmrc ├── .eslintignore ├── .storybook ├── styles │ ├── fonts.scss │ └── main.scss ├── addons.js ├── config.js └── webpack.config.js ├── packages ├── stt-adapters │ ├── README.md │ ├── autoEdit2 │ │ ├── example-usage.js │ │ ├── index.test.js │ │ └── index.js │ ├── ibm │ │ ├── example-usage.js │ │ └── index.test.js │ ├── google-stt │ │ ├── example-usage.js │ │ └── index.test.js │ ├── generate-entities-ranges │ │ ├── package.json │ │ ├── index.js │ │ └── index.test.js │ ├── amazon-transcribe │ │ ├── example-usage.js │ │ ├── sample │ │ │ └── todayinfocuswords.sample.json │ │ ├── group-words-by-speakers.js │ │ └── group-words-by-speakers.test.js │ ├── bbc-kaldi │ │ ├── example-usage.js │ │ ├── index.test.js │ │ ├── groups-words-by-speakers.test.js │ │ └── index.js │ ├── digital-paper-edit │ │ ├── example-usage.js │ │ ├── index.test.js │ │ ├── group-words-by-speakers.test.js │ │ └── index.js │ ├── speechmatics │ │ ├── example-usage.js │ │ └── index.test.js │ ├── create-entity-map │ │ └── index.js │ └── index.js ├── export-adapters │ ├── README.md │ ├── subtitles-generator │ │ ├── compose-subtitles │ │ │ ├── util │ │ │ │ ├── format-seconds.js │ │ │ │ ├── escape-text.js │ │ │ │ └── tc-format.js │ │ │ ├── vtt.js │ │ │ ├── srt.js │ │ │ ├── ttml.js │ │ │ ├── csv.js │ │ │ ├── premiere.js │ │ │ └── itt.js │ │ ├── presegment-text │ │ │ ├── line-break-between-sentences │ │ │ │ ├── index.js │ │ │ │ ├── index.test.js │ │ │ │ └── README.md │ │ │ ├── util │ │ │ │ ├── remove-space-after-carriage-return.js │ │ │ │ └── remove-space-at-beginning-of-line.js │ │ │ ├── index.test.js │ │ │ ├── text-segmentation │ │ │ │ ├── HONORIFICS.txt │ │ │ │ ├── index.js │ │ │ │ ├── index.test.js │ │ │ │ └── README.md │ │ │ ├── divide-into-two-lines │ │ │ │ ├── index.js │ │ │ │ ├── index.test.js │ │ │ │ └── README.md │ │ │ ├── fold │ │ │ │ ├── index.test.js │ │ │ │ ├── README.md │ │ │ │ └── index.js │ │ │ ├── index.js │ │ │ └── README.md │ │ └── example-usage.js │ ├── draftjs-to-digital-paper-edit │ │ ├── index.test.js │ │ └── index.js │ ├── txt-speakers-timecodes │ │ └── index.js │ ├── txt │ │ └── index.js │ ├── docx │ │ └── index.js │ └── index.js ├── components │ ├── video-player │ │ ├── index.module.css │ │ └── index.js │ ├── settings │ │ ├── TimecodeOffset │ │ │ ├── index.module.css │ │ │ └── index.js │ │ ├── Toggle │ │ │ ├── index.js │ │ │ └── index.module.css │ │ ├── index.module.css │ │ └── stories │ │ │ └── index.stories.js │ ├── keyboard-shortcuts │ │ ├── stories │ │ │ └── index.stories.js │ │ ├── index.module.css │ │ ├── index.js │ │ └── hot-keys.js │ ├── media-player │ │ ├── src │ │ │ ├── config │ │ │ │ └── playbackRates.js │ │ │ ├── Select.js │ │ │ ├── PlayerControls │ │ │ │ ├── TimeBox.js │ │ │ │ └── index.module.scss │ │ │ ├── PlaybackRate.js │ │ │ ├── ProgressBar.js │ │ │ ├── Select.module.scss │ │ │ └── ProgressBar.module.scss │ │ ├── index.module.scss │ │ ├── index.test.js │ │ └── stories │ │ │ └── index.stories.js │ ├── timed-text-editor │ │ ├── index.module.css │ │ ├── SpeakerLabel.js │ │ ├── UpdateTimestamps │ │ │ ├── example-usage.js │ │ │ └── index.js │ │ ├── Word.js │ │ ├── CustomEditor.js │ │ ├── stories │ │ │ └── index.stories.js │ │ └── WrapperBlock.module.css │ └── transcript-editor │ │ └── src │ │ ├── Header.js │ │ ├── index.module.css │ │ ├── HowDoesThisWork.js │ │ └── ExportOptions.js ├── config │ └── style-guide │ │ └── colours.scss ├── util │ └── timecode-converter │ │ ├── src │ │ ├── timecodeToSeconds.test.js │ │ ├── secondsToTimecode.test.js │ │ ├── timecodeToSeconds.js │ │ ├── padTimeToTimecode.js │ │ ├── padTimeToTimecode.test.js │ │ └── secondsToTimecode.js │ │ └── index.js └── index.js ├── .npmignore ├── docs ├── qa │ ├── images │ │ ├── player-controls.png │ │ ├── picture-in-picture.png │ │ ├── player-controls-preview.png │ │ └── timed-text-editor-double-click-on-word.gif │ ├── 4-keyboard-shortcuts.md │ ├── 2-timed-text-editor.md │ ├── 5-analytics.md │ ├── README.md │ ├── 0-component-interface.md │ └── 1-player-controls.md ├── notes │ ├── 2018-10-06-babel-jest.md │ ├── 2018-11-28-travis-ci.md │ ├── draftjs │ │ ├── 2018-10-05-draftjs-paragraph-block-formatting.md │ │ ├── 2018-12-11-multi-user-collaboration.md │ │ ├── 2018-10-08-draftjs-5-saving-current.md │ │ ├── 2018-10-20-draftjs-6-local-storage.md │ │ └── 2018-10-01-draftjs-1-basics.md │ ├── 2018-10-12-replace-timestamps-for-tests.md │ ├── 2018-10-06-automated-testing.md │ ├── 2018-10-16-locastorage.md │ ├── 2019-07-31-npm-tags.md │ ├── 2018-11-22-cra-eslint.md │ ├── 2018-10-05-babel-setup.md │ ├── 2019-01-03-babel-7-cli-ignore-syntax.md │ ├── 2018-11-29-scroll-sync.md │ ├── 2018-11-29-git-cheat-sheet.md │ ├── 2018-10-20-local-storage-draft-js.md │ ├── 2018-11-29-auto-pause-while-typing.md │ ├── 2018-10-07-component-will-receive-props-deprecated.md │ └── 2018-10-06-refactoring-reduce-to-for.md ├── adr │ ├── 2018-11-20-save-to-server.md │ ├── 2018-12-06-analytics.md │ ├── 2018-11-20-local-storage-save.md │ ├── ADR-Template.md │ ├── 2018-10-02-component-fonts.md │ ├── 2018-10-02-css-setup.md │ └── 2018-10-05-components-comunication.md └── guides │ └── analytics.md ├── demo ├── stories │ └── index.stories.js ├── local-storage.js ├── select-export-format.js ├── select-stt-json-type.js └── index.module.scss ├── __mocks__ └── styleMock.js ├── .github ├── ISSUE_TEMPLATE │ ├── question.md │ ├── feature_request.md │ └── bug_report.md └── PULL_REQUEST_TEMPLATE.md ├── .travis.yml ├── babel.config.js ├── .stylelintrc ├── LICENCE.md ├── .gitignore └── .eslintrc /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/dubnium 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | docs 3 | /dist 4 | /build 5 | **/**.json 6 | **/sample 7 | -------------------------------------------------------------------------------- /.storybook/styles/fonts.scss: -------------------------------------------------------------------------------- 1 | @import url('https://gel.files.bbci.co.uk/r2.302/bbc-reith.css'); 2 | -------------------------------------------------------------------------------- /packages/stt-adapters/README.md: -------------------------------------------------------------------------------- 1 | # Adaprters 2 | 3 | To convert STT json transcript into draft.js code block. -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | demo 3 | lib 4 | packages 5 | build 6 | .babelrc 7 | .babel.config.js 8 | webpack.config.js -------------------------------------------------------------------------------- /packages/export-adapters/README.md: -------------------------------------------------------------------------------- 1 | # Export Adapters 2 | 3 | To convert draft.js code block into downloadable files. 4 | -------------------------------------------------------------------------------- /docs/qa/images/player-controls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbc/react-transcript-editor/HEAD/docs/qa/images/player-controls.png -------------------------------------------------------------------------------- /docs/qa/images/picture-in-picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbc/react-transcript-editor/HEAD/docs/qa/images/picture-in-picture.png -------------------------------------------------------------------------------- /docs/qa/images/player-controls-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbc/react-transcript-editor/HEAD/docs/qa/images/player-controls-preview.png -------------------------------------------------------------------------------- /docs/qa/images/timed-text-editor-double-click-on-word.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbc/react-transcript-editor/HEAD/docs/qa/images/timed-text-editor-double-click-on-word.gif -------------------------------------------------------------------------------- /demo/stories/index.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { storiesOf } from '@storybook/react'; 4 | 5 | import App from '../app'; 6 | 7 | storiesOf('Demo', module).add('default', () => ); 8 | -------------------------------------------------------------------------------- /packages/export-adapters/subtitles-generator/compose-subtitles/util/format-seconds.js: -------------------------------------------------------------------------------- 1 | const formatSeconds = seconds => new Date(seconds.toFixed(3) * 1000).toISOString().substr(11, 12); 2 | 3 | export default formatSeconds; -------------------------------------------------------------------------------- /docs/notes/2018-10-06-babel-jest.md: -------------------------------------------------------------------------------- 1 | # Using babel with jest 2 | 3 | [see docs](https://github.com/facebook/jest#using-babel) 4 | 5 | ``` 6 | npm install -save-dev babel-jest babel-core@^7.0.0-bridge.0 @babel/core regenerator-runtime 7 | ``` -------------------------------------------------------------------------------- /docs/notes/2018-11-28-travis-ci.md: -------------------------------------------------------------------------------- 1 | # Travis CI notes 2 | 3 | - [Learning resources](https://github.com/dwyl/learn-travis) 4 | - [autoEdit - travis setup](https://autoedit.gitbook.io/documentation/overview/deploymentbuild/travis-ci-continuous-build) -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-knobs/register'; 2 | import '@storybook/addon-viewport/register'; 3 | import '@storybook/addon-actions/register'; 4 | import '@storybook/addon-links/register'; 5 | import '@storybook/addon-a11y/register'; 6 | -------------------------------------------------------------------------------- /__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | // __mocks__/styleMock.js 2 | // mocking css files and other media for jest when used with react import 3 | // see also package.json under jest -> moduleNameMapper 4 | // https://jestjs.io/docs/en/webpack.html 5 | module.exports = {}; -------------------------------------------------------------------------------- /docs/notes/draftjs/2018-10-05-draftjs-paragraph-block-formatting.md: -------------------------------------------------------------------------------- 1 | # Paragraph block formatting 2 | 3 | spacing draft.js paragraphs, with css 4 | 5 | ```css 6 | /* "paragraphs" */ 7 | div[data-block] + div[data-block] { 8 | margin-top: 1em; 9 | } 10 | ``` -------------------------------------------------------------------------------- /packages/export-adapters/subtitles-generator/presegment-text/line-break-between-sentences/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function addLineBreakBetweenSentences(text) { 4 | return text.replace(/\n/g, '\n\n'); 5 | } 6 | 7 | export default addLineBreakBetweenSentences; -------------------------------------------------------------------------------- /packages/stt-adapters/autoEdit2/example-usage.js: -------------------------------------------------------------------------------- 1 | const autoEdit2ToDraft = require('./index'); 2 | const autoEdit2TedTalkTranscript = require('./sample/autoEdit2TedTalkTranscript.sample.json'); 3 | 4 | console.log(autoEdit2ToDraft(autoEdit2TedTalkTranscript)); 5 | -------------------------------------------------------------------------------- /packages/stt-adapters/ibm/example-usage.js: -------------------------------------------------------------------------------- 1 | import ibmToDraft from './index.js'; 2 | import ibmTedTalkTranscript from './sample/ibmTedTalkTranscript.sample.json'; 3 | 4 | const result = ibmToDraft(ibmTedTalkTranscript); 5 | 6 | console.log(JSON.stringify(result, null, 2)); -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question about this project 4 | title: '' 5 | labels: bug 6 | assignees: pietrop 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.storybook/styles/main.scss: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: ReithSerif, Fallback, sans-serif; 3 | 4 | } 5 | 6 | p, span { 7 | font-family: ReithSans, Helvetica, sans-serif; 8 | 9 | } 10 | 11 | h1, h2, h3, h4, h5, h6, label * { 12 | font-family: ReithSerif; 13 | } 14 | -------------------------------------------------------------------------------- /packages/stt-adapters/google-stt/example-usage.js: -------------------------------------------------------------------------------- 1 | import gcpSttToDraft from './index'; 2 | import gcpSttTedTalkTranscript from './sample/gcpSttPunctuation.sample.json'; 3 | 4 | console.log('Starting'); 5 | console.log(JSON.stringify(gcpSttToDraft(gcpSttTedTalkTranscript), null, 2)); 6 | -------------------------------------------------------------------------------- /packages/export-adapters/subtitles-generator/compose-subtitles/util/escape-text.js: -------------------------------------------------------------------------------- 1 | const AMP_REGEX = /&/g; 2 | const LT_REGEX = //g; 4 | const escapeText = str => str.replace(AMP_REGEX, '&').replace(LT_REGEX, '<').replace(GT_REGEX, '>'); 5 | 6 | export default escapeText; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Travis CI configuration to run automated test when pushing to github 2 | language: node_js 3 | node_js: 4 | - "10" 5 | 6 | install: 7 | - node --version 8 | - npm i -g npm@5 9 | - npm --version 10 | - npm install 11 | 12 | script: 13 | - npm run test:ci 14 | # TODO: could also run lit -------------------------------------------------------------------------------- /docs/notes/draftjs/2018-12-11-multi-user-collaboration.md: -------------------------------------------------------------------------------- 1 | via Laurian 2 | 3 | some notes on collabroative editing with draftJs 4 | 5 | https://medium.com/@david.roegiers/building-a-real-time-collaborative-text-editor-for-the-web-draftjs-sharedb-1dd8e8826295 6 | 7 | 8 | https://github.com/facebook/draft-js/issues/93 -------------------------------------------------------------------------------- /packages/stt-adapters/generate-entities-ranges/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bbc/react-transcript-editor-util-generate-entities-ranges", 3 | "version": "1.0.0", 4 | "description": "Helper function to generate Draft.js entities", 5 | "main": "src/index.js", 6 | "license": "MIT", 7 | "private": true 8 | } 9 | -------------------------------------------------------------------------------- /packages/stt-adapters/amazon-transcribe/example-usage.js: -------------------------------------------------------------------------------- 1 | import amazonTranscribeToDraft from './index'; 2 | import amazonTranscribeTedTalkTranscript from './sample/amazonTranscribe.sample.json'; 3 | 4 | console.log('Starting'); 5 | console.log(JSON.stringify(amazonTranscribeToDraft(amazonTranscribeTedTalkTranscript), null, 2)); 6 | -------------------------------------------------------------------------------- /docs/notes/2018-10-12-replace-timestamps-for-tests.md: -------------------------------------------------------------------------------- 1 | # Replace time stamps in json 2 | For draft js tests, repalcing auto generated keys 3 | 4 | In Visual code use this regex with `*` regex find 5 | 6 | ```js 7 | "key": "([a-zA-Z0-9]*)" 8 | ``` 9 | And replace with 10 | ```js 11 | "key": expect.any(String)//"ss8pm4p" 12 | ``` -------------------------------------------------------------------------------- /packages/stt-adapters/bbc-kaldi/example-usage.js: -------------------------------------------------------------------------------- 1 | const bbcKaldiToDraft = require('./index'); 2 | // using require, because of testing outside of React app 3 | const kaldiTedTalkTranscript = require('./sample/kaldiTedTalkTranscript.sample.json'); 4 | 5 | const result = bbcKaldiToDraft(kaldiTedTalkTranscript); 6 | 7 | console.log(result); -------------------------------------------------------------------------------- /packages/components/video-player/index.module.css: -------------------------------------------------------------------------------- 1 | .videoEl { 2 | cursor: pointer; 3 | } 4 | 5 | /* Desktop size */ 6 | @media (min-width: 768px) { 7 | 8 | .videoEl { 9 | max-width: 100%; 10 | } 11 | } 12 | 13 | /* Mobile devices - excluding ipad*/ 14 | @media (max-width: 767px) { 15 | 16 | .videoEl { 17 | max-width: 100%; 18 | } 19 | } -------------------------------------------------------------------------------- /packages/config/style-guide/colours.scss: -------------------------------------------------------------------------------- 1 | $color-labs-red: #a0372d !default; 2 | $color-darkest-grey: #282828 !default; 3 | $color-dark-grey: #4a4a4a !default; 4 | $color-mid-grey: #696969 !default; 5 | $color-light-grey: #767676 !default; 6 | $color-lightest-grey: #f2f2f2 !default; 7 | $color-subt-green: #69e3c2 !default; 8 | $color-light-shilo: #E2A9A2 !default; 9 | -------------------------------------------------------------------------------- /packages/export-adapters/subtitles-generator/presegment-text/util/remove-space-after-carriage-return.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper function to remove space after carriage return \n in lines 3 | * @param {string} text 4 | */ 5 | function removeSpaceAfterCarriageReturn(text) { 6 | return text.replace(/\n /g, '\n'); 7 | } 8 | 9 | export default removeSpaceAfterCarriageReturn; -------------------------------------------------------------------------------- /packages/export-adapters/subtitles-generator/compose-subtitles/util/tc-format.js: -------------------------------------------------------------------------------- 1 | // for itt 2 | import TC from 'smpte-timecode'; 3 | 4 | const tcFormat = (frames, FPS) => { 5 | const tc = TC(Math.round(frames), FPS, false); 6 | 7 | return tc.toString().replace(/^00/, '01'); // FIXME this breaks on videos longer than 1h! 8 | }; 9 | 10 | export default tcFormat; -------------------------------------------------------------------------------- /packages/stt-adapters/digital-paper-edit/example-usage.js: -------------------------------------------------------------------------------- 1 | import digitalPaperEditToDraft from './index.js'; 2 | // using require, because of testing outside of React app 3 | import kaldiTedTalkTranscript from './sample/digitalPaperEdit.sample.json'; 4 | 5 | const result = digitalPaperEditToDraft(kaldiTedTalkTranscript); 6 | 7 | console.log(JSON.stringify(result, null, 2)); -------------------------------------------------------------------------------- /packages/stt-adapters/speechmatics/example-usage.js: -------------------------------------------------------------------------------- 1 | const speechmaticsToDraft = require('./index'); 2 | // using require, because of testing outside of React app 3 | const speechmaticsTedTalkTranscript = require('./sample/speechmaticsTedTalkTranscript.sample.json'); 4 | 5 | const result = speechmaticsToDraft(speechmaticsTedTalkTranscript); 6 | 7 | console.log(result); 8 | -------------------------------------------------------------------------------- /docs/notes/2018-10-06-automated-testing.md: -------------------------------------------------------------------------------- 1 | # automated testing 2 | 3 | Using 4 | 5 | ``` 6 | npm install react-testing-library -save-dev 7 | ``` 8 | 9 | ``` 10 | npm install enzyme -save-dev 11 | ``` 12 | 13 | [Husky](https://github.com/typicode/husky) for pre-push hooks. 14 | ``` 15 | "prepush": "npm install && npm run-script lint && npm run-script test-ci" 16 | 17 | ``` 18 | 19 | 20 | -------------------------------------------------------------------------------- /packages/components/settings/TimecodeOffset/index.module.css: -------------------------------------------------------------------------------- 1 | .offsetContainer { 2 | display: inline-block; 3 | } 4 | 5 | .inputBox { 6 | height: 2em; 7 | width: 96px; 8 | box-sizing: border-box; 9 | border: none; 10 | text-align: center; 11 | font-weight: bold; 12 | margin-right: 16px; 13 | vertical-align: middle; 14 | } 15 | 16 | .button { 17 | font-weight: lighter; 18 | font-size: 0.8em; 19 | cursor: pointer; 20 | } 21 | -------------------------------------------------------------------------------- /packages/export-adapters/subtitles-generator/compose-subtitles/vtt.js: -------------------------------------------------------------------------------- 1 | import formatSeconds from './util/format-seconds.js'; 2 | 3 | const vttGenerator = (vttJSON) => { 4 | let vttOut = 'WEBVTT\n\n'; 5 | vttJSON.forEach((v, i) => { 6 | vttOut += `${ i + 1 }\n${ formatSeconds(parseFloat(v.start)) } --> ${ formatSeconds(parseFloat(v.end)) }\n${ v.text }\n\n`; 7 | }); 8 | 9 | return vttOut; 10 | }; 11 | 12 | export default vttGenerator; -------------------------------------------------------------------------------- /packages/export-adapters/subtitles-generator/compose-subtitles/srt.js: -------------------------------------------------------------------------------- 1 | import formatSeconds from './util/format-seconds.js'; 2 | const srtGenerator = (vttJSON) => { 3 | let srtOut = ''; 4 | vttJSON.forEach((v, i) => { 5 | srtOut += `${ i + 1 }\n${ formatSeconds(parseFloat(v.start)).replace('.', ',') } --> ${ formatSeconds(parseFloat(v.end)).replace('.', ',') }\n${ v.text.trim() }\n\n`; 6 | }); 7 | 8 | return srtOut; 9 | }; 10 | 11 | export default srtGenerator; -------------------------------------------------------------------------------- /packages/export-adapters/subtitles-generator/presegment-text/util/remove-space-at-beginning-of-line.js: -------------------------------------------------------------------------------- 1 | // Remove preceding empty space a beginning of line 2 | // without removing carriage returns 3 | // https://stackoverflow.com/questions/24282158/javascript-how-to-remove-the-white-space-at-the-start-of-the-string 4 | 5 | function removeSpaceAtBeginningOfLine(text) { 6 | return text.map((r) => {return r.replace(/^\s+/g, '');}); 7 | } 8 | 9 | export default removeSpaceAtBeginningOfLine; -------------------------------------------------------------------------------- /packages/components/keyboard-shortcuts/stories/index.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { storiesOf } from '@storybook/react'; 4 | import { action } from '@storybook/addon-actions'; 5 | 6 | import KeyboardShortcuts from '../index.js'; 7 | 8 | const fixtureProps = { 9 | handleShortcutsToggle: action('Shortcuts toggle') 10 | }; 11 | 12 | storiesOf('KeyboardShortcuts', module).add('default', () => ( 13 | 14 | )); 15 | -------------------------------------------------------------------------------- /docs/notes/2018-10-16-locastorage.md: -------------------------------------------------------------------------------- 1 | # local storage 2 | 3 | using `mediaUrl` in `TranscriptEditor` passed to `TimedTextEditor` to set id in local storage. 4 | 5 | use case 6 | You load the transcript editor for one transcript, and save 7 | 8 | Load transcript editor for another different transcript, you want it to restore from second url, otherwise can only handle one transcript at a time in local storage. 9 | 10 | 11 | 12 | [How to store Draft.js content](https://codepulse.blog/2018/04/04/how-to-store-draft-js-content/) -------------------------------------------------------------------------------- /packages/stt-adapters/ibm/index.test.js: -------------------------------------------------------------------------------- 1 | import ibmToDraft from './index'; 2 | 3 | import draftTranscriptExample from './sample/ibmToDraft.sample.js'; 4 | import ibmTedTalkTranscript from './sample/ibmTedTalkTranscript.sample.json'; 5 | 6 | describe('bbcKaldiToDraft', () => { 7 | const result = ibmToDraft(ibmTedTalkTranscript); 8 | it('Should be defined', ( ) => { 9 | expect(result).toBeDefined(); 10 | }); 11 | 12 | it('Should be equal to expected value', ( ) => { 13 | expect(result).toEqual(draftTranscriptExample); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/components/media-player/src/config/playbackRates.js: -------------------------------------------------------------------------------- 1 | const PLAYBACK_RATES = [ 2 | { value: 0.2, label: '0.2' }, 3 | { value: 0.25, label: '0.25' }, 4 | { value: 0.5, label: '0.5' }, 5 | { value: 0.75, label: '0.75' }, 6 | { value: 1, label: '1' }, 7 | { value: 1.25, label: '1.25' }, 8 | { value: 1.5, label: '1.5' }, 9 | { value: 1.75, label: '1.75' }, 10 | { value: 2, label: '2' }, 11 | { value: 2.5, label: '2.5' }, 12 | { value: 3, label: '3' }, 13 | { value: 3.5, label: '3.5' } 14 | ]; 15 | 16 | export default PLAYBACK_RATES; -------------------------------------------------------------------------------- /packages/components/timed-text-editor/index.module.css: -------------------------------------------------------------------------------- 1 | @import '../../config/style-guide/colours.scss'; 2 | 3 | .DraftEditor-root { 4 | background: #f9f9f9; 5 | } 6 | 7 | /* 8 | Giving the editor a oveflow 9 | https://github.com/facebook/draft-js/issues/528 10 | */ 11 | 12 | .editor :global(.public-DraftEditor-content) { 13 | max-height: 75vh; 14 | overflow: auto; 15 | padding: 8px 16px; 16 | background-color: white; 17 | } 18 | 19 | /* Mobile devices */ 20 | @media (max-width: 768px) { 21 | 22 | .editor :global(.public-DraftEditor-content) { 23 | margin: 0 auto; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/export-adapters/draftjs-to-digital-paper-edit/index.test.js: -------------------------------------------------------------------------------- 1 | import draftToDigitalPaperEdit from './index.js'; 2 | 3 | import digitalPaperEditTranscript from './digitalPaperEdit.sample.json'; 4 | import draftTranscriptExample from './draftjs.sample.json'; 5 | 6 | describe('Draft Js to Digital Paper Edit', () => { 7 | const result = draftToDigitalPaperEdit(draftTranscriptExample); 8 | it('Should be defined', ( ) => { 9 | expect(result).toBeDefined(); 10 | }); 11 | 12 | it('Should be equal to expected value', ( ) => { 13 | expect(result).toEqual(digitalPaperEditTranscript); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/export-adapters/txt-speakers-timecodes/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert DraftJS to plain text with timecodes and speaker names 3 | * 4 | * Example: 5 | ``` 6 | F_S12 [00:00:13] There is a day. About ten years ago when I asked a friend to hold a baby dinosaur robot upside down. It was a toy called plea. All 7 | ``` 8 | * 9 | */ 10 | import { shortTimecode } from '../../util/timecode-converter/index.js'; 11 | 12 | export default (blockData) => { 13 | const lines = blockData.blocks.map((block) => { 14 | return `${ block.data.speaker } \t [${ shortTimecode(block.data.start) }] \t ${ block.text }`; 15 | }); 16 | 17 | return lines.join('\n\n'); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/util/timecode-converter/src/timecodeToSeconds.test.js: -------------------------------------------------------------------------------- 1 | import timecodeToSecondsHelper from './timecodeToSeconds'; 2 | 3 | describe('Timecode conversion TC- convertToSeconds', () => { 4 | it('Should be defined', ( ) => { 5 | const demoTcValue = '00:10:00:00'; 6 | const result = timecodeToSecondsHelper(demoTcValue); 7 | expect(result).toBeDefined(); 8 | }); 9 | 10 | it('Should be able to convert from: hh:mm:ss:ff ', ( ) => { 11 | const demoTcValue = '00:10:00:00'; 12 | const demoExpectedResultInSeconds = 600; 13 | const result = timecodeToSecondsHelper(demoTcValue); 14 | expect(result).toEqual(demoExpectedResultInSeconds); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/util/timecode-converter/src/secondsToTimecode.test.js: -------------------------------------------------------------------------------- 1 | import secondsToTimecode from './secondsToTimecode'; 2 | 3 | describe('Timecode conversion TC- convertToSeconds', () => { 4 | it('Should be defined', ( ) => { 5 | const dmoSecondsValue = 600; 6 | // const demoExpectedTc = '00:10:00:00'; 7 | const result = secondsToTimecode(dmoSecondsValue); 8 | expect(result).toBeDefined(); 9 | }); 10 | 11 | it('Should be able to convert to: hh:mm:ss:ff ', ( ) => { 12 | const dmoSecondsValue = 600; 13 | const demoExpectedTc = '00:10:00:00'; 14 | const result = secondsToTimecode(dmoSecondsValue); 15 | expect(result).toEqual(demoExpectedTc); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/stt-adapters/autoEdit2/index.test.js: -------------------------------------------------------------------------------- 1 | import autoEdit2ToDraft from './index.js'; 2 | // TODO: could make this test run faster by shortning the two sample to one or two paragraphs? 3 | import draftTranscriptExample from './sample/autoEdit2ToDraft.sample.js'; 4 | import autoEdit2TedTalkTranscript from './sample/autoEdit2TedTalkTranscript.sample.json'; 5 | 6 | describe('bbcKaldiToDraft', () => { 7 | const result = autoEdit2ToDraft(autoEdit2TedTalkTranscript, 'text'); 8 | it('Should be defined', ( ) => { 9 | expect(result).toBeDefined(); 10 | }); 11 | 12 | it('Should be equal to expected value', ( ) => { 13 | expect(result).toEqual(draftTranscriptExample); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/export-adapters/subtitles-generator/compose-subtitles/ttml.js: -------------------------------------------------------------------------------- 1 | import escapeText from './util/escape-text.js'; 2 | import formatSeconds from './util/format-seconds.js'; 3 | 4 | const ttmlGenerator = (vttJSON) => { 5 | let ttmlOut = ` 6 | 7 | 8 | 9 |
`; 10 | vttJSON.forEach((v) => { 11 | ttmlOut += `

${ escapeText(v.text).replace(/\n/g, '
') }

\n`; 12 | }); 13 | ttmlOut += '
\n\n
'; 14 | 15 | return ttmlOut; 16 | }; 17 | 18 | export default ttmlGenerator; -------------------------------------------------------------------------------- /packages/stt-adapters/bbc-kaldi/index.test.js: -------------------------------------------------------------------------------- 1 | import bbcKaldiToDraft from './index'; 2 | 3 | import draftTranscriptExample from './sample/bbcKaldiToDraft.sample.js'; 4 | import kaldiTedTalkTranscript from './sample/kaldiTedTalkTranscript.sample.json'; 5 | 6 | // TODO: figure out why the second of these two tests hang 7 | // might need to review the draftJS data structure output 8 | describe('bbcKaldiToDraft', () => { 9 | const result = bbcKaldiToDraft(kaldiTedTalkTranscript); 10 | it('Should be defined', ( ) => { 11 | expect(result).toBeDefined(); 12 | }); 13 | 14 | it('Should be equal to expected value', ( ) => { 15 | expect(result).toEqual(draftTranscriptExample); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // following babel 7, system wide config at root level 2 | // https://babeljs.io/docs/en/next/config-files#project-wide-configuration 3 | // https://stackoverflow.com/questions/52387820/babel7-jest-unexpected-token-export/52388264 4 | module.exports = { 5 | presets: [ 6 | [ 7 | '@babel/preset-env', { 8 | targets: { node: 'current' } 9 | } 10 | ], 11 | '@babel/preset-react', 12 | [ 13 | 'minify', 14 | { 15 | builtIns: false, 16 | evaluate: false, 17 | mangle: false 18 | } 19 | ] 20 | ], 21 | plugins: [ 22 | '@babel/plugin-proposal-object-rest-spread', 23 | '@babel/plugin-proposal-class-properties' 24 | ] 25 | }; 26 | -------------------------------------------------------------------------------- /docs/notes/draftjs/2018-10-08-draftjs-5-saving-current.md: -------------------------------------------------------------------------------- 1 | # Saving DraftJs current data 2 | 3 | > also, if you want to save the edited data, to get that JSON back of blocks, entityMap, etc. is just `const data = convertToRaw(editorState.getCurrentContent());` 4 | 5 | --- 6 | > Import convertToRaw the same way you import convertFromRaw then you can iterate that data do to various things I have somewhere code to do bbc transcript model out of that it is a monster like https://github.com/bbc/subtitalizer/blob/d102c233236b782011a4a94a21e6de13491abb45/src/components/TranscriptEditor.js#L105-L241but basically, you have to take each block and split the text on space, then find what entity range covers each word then get the timing data. 7 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Is your Pull Request request related to another [issue](https://github.com/bbc/react-transcript-editor/issues) in this repository ?** 2 | 3 | 4 | **Describe what the PR does** 5 | 6 | 7 | 8 | **State whether the PR is ready for review or whether it needs extra work** 9 | 10 | 11 | **Additional context** 12 | 13 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "rules": { 4 | "indentation": 2, 5 | "string-quotes": "single", 6 | "color-named": "always-where-possible", 7 | "selector-combinator-space-after": "always", 8 | "selector-attribute-quotes": "always", 9 | "selector-attribute-operator-space-before": "always", 10 | "selector-attribute-operator-space-after": "always", 11 | "declaration-colon-space-before": "never", 12 | "declaration-colon-space-after": "always", 13 | "rule-empty-line-before": "always", 14 | "media-feature-range-operator-space-before": "always", 15 | "media-feature-range-operator-space-after": "always", 16 | "media-feature-colon-space-after": "always" 17 | } 18 | } -------------------------------------------------------------------------------- /packages/stt-adapters/speechmatics/index.test.js: -------------------------------------------------------------------------------- 1 | import speechmaticsToDraft from './index'; 2 | 3 | import draftTranscriptExample from './sample/speechmaticsToDraft.sample.js'; 4 | import speechmaticsTedTalkTranscript from './sample/speechmaticsTedTalkTranscript.sample.json'; 5 | 6 | // TODO: figure out why the second of these two tests hang 7 | // might need to review the draftJS data structure output 8 | describe('speechmaticsToDraft', () => { 9 | const result = speechmaticsToDraft(speechmaticsTedTalkTranscript); 10 | it('Should be defined', ( ) => { 11 | expect(result).toBeDefined(); 12 | }); 13 | 14 | it('Should be equal to expected value', ( ) => { 15 | expect(result).toEqual(draftTranscriptExample); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: Enhancement 6 | assignees: pietrop 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | 12 | 13 | **Describe the solution you'd like** 14 | 15 | 16 | **Describe alternatives you've considered** 17 | 18 | 19 | **Additional context** 20 | 21 | -------------------------------------------------------------------------------- /docs/notes/2019-07-31-npm-tags.md: -------------------------------------------------------------------------------- 1 | # Npm tags 2 | 3 | First make sure you have done a commit of latest changes then 4 | 5 | >You can run `npm version 1.0.4-alpha.1` to update `package.json` and create a git tag in one go (see https://docs.npmjs.com/cli/version). 6 | 7 | - [Publishing a beta or alpha version to NPM](https://medium.com/@kevinkreuzer/publishing-a-beta-or-alpha-version-to-npm-46035b630dd7) 8 | 9 | this changes `package.json` version to be 10 | 11 | ```json 12 | "version": "1.0.4-alpha.0", 13 | ``` 14 | 15 | then you can run `npm run publish:public` which under the hood preps the files and folder and runs `npm publish dist --access public`. 16 | 17 | To install in another repo 18 | 19 | ``` 20 | npm install @bbc/react-transcript-editor@alpha 21 | ``` -------------------------------------------------------------------------------- /packages/stt-adapters/digital-paper-edit/index.test.js: -------------------------------------------------------------------------------- 1 | import digitalPaperEditToDraft from './index'; 2 | 3 | // TODO .js coz you need to change the keys for draftJs entities to in jest format 4 | // import draftTranscriptExample from './sample/ibmToDraft.sample.js'; 5 | import digitalPaperEditTranscript from './sample/digitalPaperEdit.sample.json'; 6 | import draftTranscriptSample from './sample/digitalPaperEditToDraftJs.sample.js'; 7 | 8 | describe('Digital Paper Edit to Draft', () => { 9 | const result = digitalPaperEditToDraft(digitalPaperEditTranscript); 10 | it('Should be defined', ( ) => { 11 | expect(result).toBeDefined(); 12 | }); 13 | 14 | it.skip('Should be equal to expected value', ( ) => { 15 | expect(result).toEqual(draftTranscriptSample); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/components/settings/Toggle/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import style from './index.module.css'; 5 | 6 | class Toggle extends React.Component { 7 | render() { 8 | return ( 9 |
10 | 17 |
18 | ); 19 | } 20 | } 21 | 22 | Toggle.propTypes = { 23 | handleToggle: PropTypes.func, 24 | label: PropTypes.string, 25 | defaultValue: PropTypes.bool 26 | }; 27 | 28 | export default Toggle; 29 | -------------------------------------------------------------------------------- /packages/components/media-player/index.module.scss: -------------------------------------------------------------------------------- 1 | .topSection { 2 | background: black; 3 | } 4 | 5 | .playerSection { 6 | display: inline-flex; 7 | align-items: flex-start; 8 | width: 100%; 9 | } 10 | 11 | .controlsSection { 12 | text-align: center; 13 | width: 100%; 14 | margin: auto; 15 | padding: 1em; 16 | position: relative; 17 | // overflow-x: auto; 18 | // overflow-y: auto; 19 | } 20 | 21 | .title { 22 | color: white; 23 | line-height: 1.2em; 24 | width: 60%; 25 | margin-top: 0; 26 | margin-bottom: 0.5em; 27 | font-size: 1.2em; 28 | overflow: hidden; 29 | display: inline-block; 30 | white-space: nowrap; 31 | text-overflow: ellipsis; 32 | } 33 | 34 | @media (max-width: 768px) { 35 | 36 | .title { 37 | text-align: center; 38 | padding-top: 1em; 39 | white-space: normal; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/export-adapters/subtitles-generator/presegment-text/index.test.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import preSegmentText from './index.js'; 3 | // requrie on js and json is relative to current file path 4 | import { words as sampleWords } from '../sample/words-list.sample.json'; 5 | // fs path is relative to where the node process start 6 | const sampleSegmentedOutput = fs.readFileSync('./packages/export-adapters/subtitles-generator/sample/test-presegment.sample.txt').toString(); 7 | 8 | const numberOfCharPerLine35 = 35; 9 | 10 | test('presegment text ', () => { 11 | const result = preSegmentText(sampleWords); 12 | expect(result).toEqual(sampleSegmentedOutput); 13 | }); 14 | 15 | test('presegment text - 35', () => { 16 | const result = preSegmentText(sampleWords, numberOfCharPerLine35); 17 | expect(result).toEqual(sampleSegmentedOutput); 18 | }); -------------------------------------------------------------------------------- /packages/export-adapters/subtitles-generator/presegment-text/text-segmentation/HONORIFICS.txt: -------------------------------------------------------------------------------- 1 | A. 2 | Adj. 3 | Adm. 4 | Adv. 5 | Asst. 6 | B. 7 | Bart. 8 | Bldg. 9 | Brig. 10 | Bros. 11 | C. 12 | Capt. 13 | Cmdr. 14 | Col. 15 | Comdr. 16 | Con. 17 | Cpl. 18 | D. 19 | DR. 20 | Dr. 21 | E. 22 | Ens. 23 | F. 24 | Fr. 25 | G. 26 | Gen. 27 | Gov. 28 | H. 29 | Hon. 30 | Hosp. 31 | I. 32 | Insp. 33 | J. 34 | K. 35 | L. 36 | Lt. 37 | M. 38 | M. 39 | MM. 40 | MR. 41 | MRS. 42 | MS. 43 | Maj. 44 | Messrs. 45 | Mlle. 46 | Mme. 47 | Mr. 48 | Mrs. 49 | Ms. 50 | Msgr. 51 | N. 52 | O. 53 | Op. 54 | Ord. 55 | P. 56 | Pfc. 57 | Ph. 58 | Prof. 59 | Pvt. 60 | Q. 61 | R. 62 | Rep. 63 | Reps. 64 | Res. 65 | Rev. 66 | Rt. 67 | S. 68 | Sen. 69 | Sens. 70 | Sfc. 71 | Sgt. 72 | Sr. 73 | St. 74 | Supt. 75 | Surg. 76 | T. 77 | U. 78 | V. 79 | W. 80 | X. 81 | Y. 82 | Z. 83 | v. 84 | vs. -------------------------------------------------------------------------------- /docs/adr/2018-11-20-save-to-server.md: -------------------------------------------------------------------------------- 1 | # Save to server 2 | 3 | * Status: accepted 4 | * Deciders: Pietro, James 5 | * Date: 2018-11-20 6 | 7 | ## Context and Problem Statement 8 | 9 | How should the system handle saving the data to a server API end point? 10 | should it be a responsibility fo the component or not? 11 | 12 | ## Decision Drivers 13 | 14 | * easy to reason around 15 | * clean interface 16 | * flexible to integrate and use component on variety of settings 17 | * un-opinionated in regards to the API end point and how to make that request 18 | 19 | ## Considered Options 20 | 21 | * Inside component 22 | * Outside component 23 | 24 | ## Decision Outcome 25 | 26 | - component returns content in default draftJS format or in variety of supported adapters/converters 27 | - saving to server is done outside of the component to allow more flexible integration within other contexts 28 | -------------------------------------------------------------------------------- /packages/export-adapters/subtitles-generator/compose-subtitles/csv.js: -------------------------------------------------------------------------------- 1 | function csvGenerator(srtJsonContent) { 2 | let lines = 'N, In, Out, Text\n'; 3 | srtJsonContent.forEach((srtLineO, index) => { 4 | lines += `${ index + 1 },`; 5 | //need to surround timecodes with "\"" escaped " to escape the , for the milliseconds 6 | lines += `\"${ srtLineO.start }\",\"${ srtLineO.end }\",`; 7 | // removing line breaks and and removing " as they break the csv. 8 | // wrapping text in escaped " to escape any , for the csv. 9 | // adding carriage return \n to signal end of line in csv 10 | // Preserving line break within srt lines to allow round trip from csv back to srt file in same format. 11 | // by replacing \n with \r\n. 12 | lines += `\"${ srtLineO.text.replace(/\n/g, '\r\n') }\"\n`; 13 | }); 14 | 15 | return lines; 16 | } 17 | 18 | export default csvGenerator; -------------------------------------------------------------------------------- /packages/stt-adapters/create-entity-map/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper function to generate draft.js entityMap from draftJS blocks, 3 | */ 4 | 5 | /** 6 | * helper function to flatten a list. 7 | * converts nested arrays into one dimensional array 8 | * @param {array} list 9 | */ 10 | const flatten = list => list.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []); 11 | 12 | /** 13 | * helper function to create createEntityMap 14 | * @param {*} blocks - draftJs blocks 15 | */ 16 | const createEntityMap = (blocks) => { 17 | const entityRanges = blocks.map(block => block.entityRanges); 18 | const flatEntityRanges = flatten(entityRanges); 19 | 20 | const entityMap = {}; 21 | 22 | flatEntityRanges.forEach((data) => { 23 | entityMap[data.key] = { 24 | type: 'WORD', 25 | mutability: 'MUTABLE', 26 | data, 27 | }; 28 | }); 29 | 30 | return entityMap; 31 | }; 32 | 33 | export default createEntityMap; -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure, addDecorator, addParameters } from '@storybook/react'; 2 | import { withA11y } from '@storybook/addon-a11y'; 3 | 4 | // automatically import all files ending in *.stories.js 5 | // https://webpack.js.org/guides/dependency-management/ 6 | const components = require.context('../packages/components/', true, /.stories.js$/); 7 | const demo = require.context('../demo/', true, /.stories.js$/); 8 | const styles = require.context('./styles', true, /\.scss$/); 9 | 10 | function loadStories() { 11 | demo.keys().forEach((filename) => demo(filename)); 12 | components.keys().forEach((filename) => components(filename)); 13 | styles.keys().forEach((filename) => styles(filename)); 14 | } 15 | 16 | addDecorator(withA11y); 17 | 18 | addParameters({ 19 | options: { 20 | // showPanel: false, 21 | panelPosition: 'bottom', 22 | sidebarAnimations: true 23 | }, 24 | }); 25 | 26 | configure(loadStories, module); 27 | -------------------------------------------------------------------------------- /packages/export-adapters/subtitles-generator/presegment-text/text-segmentation/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import tokenizer from 'sbd'; 3 | 4 | function textSegmentation(text, honorifics) { 5 | var optionalHonorifics = null; 6 | 7 | if (honorifics !== undefined) { 8 | optionalHonorifics = honorifics; 9 | } 10 | 11 | var options = { 12 | 'newline_boundaries': true, 13 | 'html_boundaries': false, 14 | 'sanitize': false, 15 | 'allowed_tags': false, 16 | //TODO: Here could open HONORIFICS file and pass them in here I think 17 | //abbreviations: list of abbreviations to override the original ones for use with other languages. Don't put dots in abbreviations. 18 | 'abbreviations': optionalHonorifics 19 | }; 20 | 21 | var sentences = tokenizer.sentences(text, options); 22 | var sentencesWithLineSpaces = sentences.join('\n'); 23 | 24 | return sentencesWithLineSpaces; 25 | } 26 | 27 | export default textSegmentation; 28 | -------------------------------------------------------------------------------- /packages/components/media-player/src/Select.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import style from './Select.module.scss'; 5 | 6 | class Select extends React.Component { 7 | 8 | render() { 9 | const options = this.props.options.map((option, index) => { 10 | // eslint-disable-next-line react/no-array-index-key 11 | return ; 12 | }); 13 | 14 | return ( 15 | 23 | ); 24 | } 25 | } 26 | 27 | Select.propTypes = { 28 | options: PropTypes.array, 29 | name: PropTypes.string, 30 | currentValue: PropTypes.string, 31 | handleChange: PropTypes.func 32 | }; 33 | 34 | export default Select; 35 | -------------------------------------------------------------------------------- /packages/components/media-player/src/PlayerControls/TimeBox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import isEqual from 'react-fast-compare'; 3 | 4 | import style from './index.module.scss'; 5 | 6 | class TimeBox extends React.Component { 7 | shouldComponentUpdate = (nextProps) => { 8 | return !isEqual(this.props, nextProps); 9 | } 10 | 11 | handleClick = (e) => { 12 | this.props.promptSetCurrentTime(e); 13 | } 14 | 15 | render() { 16 | return ( 17 |
18 | 22 | { this.props.currentTime } 23 | | 24 | 27 | {this.props.duration} 28 |
29 | ); 30 | } 31 | } 32 | 33 | export default TimeBox; 34 | -------------------------------------------------------------------------------- /packages/util/timecode-converter/src/timecodeToSeconds.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Helperf unction 3 | * @param {*} tc 4 | * @param {*} fps 5 | */ 6 | const timecodeToFrames = function (tc, fps) { 7 | // TODO make 29.97 fps drop-frame aware - works for 25 only. 8 | 9 | const s = tc.split(':'); 10 | let frames = parseInt(s[3]); 11 | frames += parseInt(s[2]) * fps; 12 | frames += parseInt(s[1]) * (fps * 60); 13 | frames += parseInt(s[0]) * (fps * 60 * 60); 14 | 15 | return frames; 16 | }; 17 | 18 | /** 19 | * Convert broadcast timecodes to seconds 20 | * @param {*} tc - `hh:mm:ss:ff` 21 | * @param {*} framePerSeconds - defaults to 25 if not provided 22 | */ 23 | const timecodeToSecondsHelper = function (tc, framePerSeconds) { 24 | let fps = 25; 25 | if (framePerSeconds !== undefined) { 26 | fps = framePerSeconds; 27 | } 28 | const frames = timecodeToFrames(tc, fps); 29 | 30 | return Number(Number(frames / fps).toFixed(2)); 31 | }; 32 | 33 | export default timecodeToSecondsHelper; 34 | -------------------------------------------------------------------------------- /docs/notes/2018-11-22-cra-eslint.md: -------------------------------------------------------------------------------- 1 | # setting up Create React App with ESLint 2 | 3 | Two main reasons 4 | - being able to lint on save with editor of choice 5 | - being able to have a `npm run lint` command to for automated linting check of contributors eg 6 | - git pre-commit and pre-push hooks 7 | - use in continuos integration/deployment settings, eg for PR 8 | 9 | 10 | install dev dependencies in `package.json` 11 | 12 | ```json 13 | "devDependencies": { 14 | ... 15 | "eslint": "^5.4.0", 16 | "eslint-config-airbnb": "^17.1.0", 17 | "eslint-plugin-import": "^2.14.0", 18 | "eslint-plugin-jsx-a11y": "^6.1.1", 19 | "eslint-plugin-react": "^7.11.1", 20 | ... 21 | ``` 22 | 23 | Then in npm scripts add 24 | 25 | ```json 26 | "scripts": { 27 | ... 28 | "lint": "eslint --ignore-path .gitignore .", 29 | "lint-fix": "eslint --ignore-path .gitignore . --fix", 30 | ... 31 | ``` 32 | 33 | optionally you can add a `.eslintignore` if there's extra files/folder to exclude that are not in the `.gitignore`. -------------------------------------------------------------------------------- /packages/components/timed-text-editor/SpeakerLabel.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 5 | import { faUserEdit } from '@fortawesome/free-solid-svg-icons'; 6 | 7 | import style from './WrapperBlock.module.css'; 8 | 9 | class SpeakerLabel extends PureComponent { 10 | render() { 11 | return ( 12 | 15 | 16 | 17 | 18 | {this.props.name} 19 | 20 | ); 21 | } 22 | } 23 | 24 | SpeakerLabel.propTypes = { 25 | name: PropTypes.string, 26 | handleOnClickEdit: PropTypes.func 27 | }; 28 | 29 | export default SpeakerLabel; 30 | -------------------------------------------------------------------------------- /packages/export-adapters/subtitles-generator/presegment-text/divide-into-two-lines/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import removeSpaceAtBeginningOfLine from '../util/remove-space-at-beginning-of-line.js'; 3 | 4 | function divideIntoTwoLines(text) { 5 | var lines = text.split('\n'); 6 | 7 | var counter = 0; 8 | 9 | var result = lines.map((l) => { 10 | if (l === '') { 11 | return l; 12 | } else { 13 | if (counter === 0) { 14 | counter += 1; 15 | if (l[l.length - 1][0] === '.') { 16 | return l + '\n\n'; 17 | } 18 | 19 | return l + '\n'; 20 | } else if (counter === 1) { 21 | counter = 0; 22 | 23 | return l + '\n\n'; 24 | } 25 | } 26 | }); 27 | 28 | result = removeSpaceAtBeginningOfLine(result); 29 | // remove empty lines from list to avoid unwanted space a beginning of line 30 | result = result.filter(line => line.length !== 0); 31 | 32 | result = result.join('').trim(); 33 | 34 | return result; 35 | } 36 | 37 | export default divideIntoTwoLines; -------------------------------------------------------------------------------- /packages/stt-adapters/digital-paper-edit/group-words-by-speakers.test.js: -------------------------------------------------------------------------------- 1 | import groupWordsInParagraphsBySpeakers from './group-words-by-speakers'; 2 | 3 | import digitalPaperEditTranscript from './sample/digitalPaperEdit.sample.json'; 4 | 5 | const segmentation = digitalPaperEditTranscript.paragraphs; 6 | const words = digitalPaperEditTranscript.words; 7 | 8 | describe('groupWordsInParagraphsBySpeakers', () => { 9 | /** 10 | * Hard to test if the segmentation algo it's working properly 11 | * But one basic test for now is to test there is the same number of words 12 | * In the result. 13 | */ 14 | it('Expect same word count in results', ( ) => { 15 | 16 | const wordsByParagraphs = groupWordsInParagraphsBySpeakers(words, segmentation); 17 | 18 | const resultWordCount = wordsByParagraphs.reduce(reduceFunction, 0); 19 | 20 | function reduceFunction(total, currentParagraph) { 21 | return total + currentParagraph.words.length; 22 | }; 23 | 24 | expect(resultWordCount).toBe(words.length); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: pietrop 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | 22 | 23 | **Screenshots** 24 | 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | 39 | -------------------------------------------------------------------------------- /packages/stt-adapters/bbc-kaldi/groups-words-by-speakers.test.js: -------------------------------------------------------------------------------- 1 | import groupWordsInParagraphsBySpeakers from './group-words-by-speakers'; 2 | 3 | import kaldiTedTalkTranscript from './sample/bbcKaldiTranscriptWithSpeakerSegments.sample.json'; 4 | 5 | const segmentation = kaldiTedTalkTranscript.retval.segmentation; 6 | const words = kaldiTedTalkTranscript.retval.words; 7 | 8 | describe('groupWordsInParagraphsBySpeakers', () => { 9 | /** 10 | * Hard to test if the segmentation algo it's working properly 11 | * But one basic test for now is to test there is the same number of words 12 | * In the result. 13 | */ 14 | it('Expect same word count in results', ( ) => { 15 | 16 | const wordsByParagraphs = groupWordsInParagraphsBySpeakers(words, segmentation); 17 | 18 | const resultWordCount = wordsByParagraphs.reduce(reduceFunction, 0); 19 | function reduceFunction(total, currentParagraph) { 20 | return total + currentParagraph.words.length; 21 | }; 22 | 23 | expect(resultWordCount).toBe(words.length); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /docs/notes/2018-10-05-babel-setup.md: -------------------------------------------------------------------------------- 1 | # Babel Setup 2 | 3 | While setting up `create-react-app` as a way to build a React component as [described in adr notes](../adr/2018-10-05-component-development-setup.md) I've run into a [transpiling issue as described here](https://github.com/aakashns/create-component-lib/issues/1). So here's a few notes on the fix. 4 | 5 | To get setup with babel for transpiling component you need 6 | 7 | ## CLI + Core 8 | 9 | To transpile you need to add [`@babel/core` `@babel/cli`](https://babeljs.io/docs/en/babel-cli) see docs for more info. 10 | 11 | ``` 12 | npm install @babel/core @babel/cli --save-dev 13 | ``` 14 | 15 | ## Presets 16 | For the React presents you can use the official once by adding [@babel/preset-react](https://babeljs.io/docs/en/next/babel-preset-react.html), see docs for more info. 17 | 18 | ``` 19 | npm install @babel/preset-react --save-dev 20 | ``` 21 | 22 | And make sure `.babeblrc` preset is setup as follows 23 | 24 | ```json 25 | { 26 | "presets": [ 27 | "@babel/preset-react" 28 | ] 29 | } 30 | ``` 31 | 32 | 33 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | // https://github.com/storybooks/storybook/issues/270#issuecomment-318101549 2 | // this config augments the storybook one with support for css modules 3 | // storybook does not have support for css modules out of the box 4 | // if CRA were to be present, storybook webpack augment those configs 5 | module.exports = { 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.module.(sa|sc|c)ss$/, 10 | use: [ 11 | "style-loader", 12 | { 13 | loader: "css-loader", 14 | options: { modules: true } 15 | }, 16 | { 17 | loader: "sass-loader", 18 | options: { sourcemap: true } 19 | } 20 | ] 21 | }, 22 | { 23 | test: /\.s(a|c)ss$/, 24 | exclude: /\.module.(s(a|c)ss)$/, 25 | use: [ 26 | "style-loader", 27 | "css-loader", 28 | { 29 | loader: "sass-loader", 30 | options: { sourcemap: true } 31 | } 32 | ] 33 | } 34 | ] 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /packages/components/media-player/src/PlaybackRate.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import isEqual from 'react-fast-compare'; 4 | 5 | import Select from './Select'; 6 | 7 | import style from './PlayerControls/index.module.scss'; 8 | 9 | class PlaybackRate extends React.Component { 10 | 11 | shouldComponentUpdate = (nextProps) => { 12 | return !isEqual(this.props, nextProps); 13 | } 14 | 15 | render() { 16 | return ( 17 | 19 | 27 | 28 | ); 29 | } 30 | } 31 | 32 | ProgressBar.propTypes = { 33 | value: PropTypes.string, 34 | max: PropTypes.string, 35 | buttonClick: PropTypes.func 36 | }; 37 | 38 | ProgressBar.defaultProps = { 39 | value: '0', 40 | max: '0', 41 | }; 42 | 43 | export default ProgressBar; 44 | -------------------------------------------------------------------------------- /packages/components/timed-text-editor/UpdateTimestamps/example-usage.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import alignWords from './stt-align-node.js'; 3 | import { words, paragraphs } from './sample/NathanielGleicher-aws-dpe.sample.json'; 4 | import text from './sample/The Facebook Dilemma - Nathaniel Gleicher-F0ykdaOck_M.en.txt.sample.js'; 5 | 6 | function splitWords(text) { 7 | return text 8 | .trim() 9 | .replace(/\n /g, '') 10 | .replace(/\n/g, ' ') 11 | .split(' '); 12 | } 13 | 14 | const transcriptWords = splitWords(text); 15 | 16 | const sttWords = words.map((word) => { 17 | word.word = word.text; 18 | 19 | return word; 20 | }); 21 | 22 | let result = alignWords(sttWords, transcriptWords); 23 | const wordsResults = result.map((word, index) => { 24 | word.id = index; 25 | word.text = word.word; 26 | delete word.word; 27 | 28 | return word; 29 | }); 30 | 31 | result = { words: wordsResults, paragraphs }; 32 | 33 | // console.log(result); 34 | fs.writeFileSync('./packages/components/timed-text-editor/UpdateTimestamps/sample/alignement-result.sample.json', JSON.stringify(result, null, 2)); -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Pietro Passarelli, James Dooley CC0 BBC 2018 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/components/keyboard-shortcuts/index.module.css: -------------------------------------------------------------------------------- 1 | @import '../../config/style-guide/colours.scss'; 2 | 3 | .shortcuts { 4 | font-size: 0.85em; 5 | padding: 1em; 6 | margin: 0 auto; 7 | height: auto; 8 | vertical-align: middle; 9 | color: black; 10 | background: $color-lightest-grey; 11 | z-index: 2; 12 | position: absolute; 13 | top: 0; 14 | right: 0; 15 | } 16 | 17 | .header { 18 | margin-top: 0; 19 | margin-bottom: 1em; 20 | text-align: center; 21 | } 22 | 23 | .closeButton { 24 | position: absolute; 25 | top: 0; 26 | right: 0; 27 | padding: 1em; 28 | cursor: pointer; 29 | z-index: 3; 30 | } 31 | 32 | .list { 33 | list-style: none; 34 | padding: 1em; 35 | margin: 0 auto; 36 | } 37 | 38 | .listItem { 39 | margin: 0 auto; 40 | margin-bottom: 0.5em; 41 | } 42 | 43 | .shortcut { 44 | display: inline-block; 45 | width: 6em; 46 | background: $color-labs-red; 47 | font-weight: lighter; 48 | color: white; 49 | padding: 0 0; 50 | border-radius: 1em; 51 | text-align: center; 52 | margin-right: 1em; 53 | } 54 | 55 | .shortcutLabel { 56 | display: inline-block; 57 | text-align: left; 58 | } 59 | -------------------------------------------------------------------------------- /packages/components/media-player/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, cleanup } from 'react-testing-library'; 3 | import { shallow } from 'enzyme'; 4 | 5 | import MediaPlayer from './index.js'; 6 | 7 | afterEach(cleanup); 8 | 9 | const fakeVideoUrl = 'https://storage.googleapis.com/coverr-main/mp4/Pigeon-Impossible.mp4'; 10 | 11 | xtest('GIVEN a chapter title I expect that WHEN the Video component is rendered THEN the correct title is displayed', () => { 12 | const { container } = render(); 13 | 14 | expect(container.innerHTML).toContain('videoSection'); 15 | }); 16 | 17 | xtest("GIVEN a video as a chapter with src video url THEN the video is rendered with it's source url", () => { 18 | const { getByTestId } = render(); 19 | 20 | expect(getByTestId('media-player-id').attributes.src.value).toBe(fakeVideoUrl); 21 | }); 22 | xtest('WHEN the Video component is rendered THEN a video element is displayed', () => { 23 | const wrapper = shallow(); 24 | expect(wrapper.find('video').type()).toBe('video'); 25 | }); 26 | -------------------------------------------------------------------------------- /docs/notes/2019-01-03-babel-7-cli-ignore-syntax.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Thought it might be useful to note here for future reference, when the blog post was written it showed how to ignore files using babel 6. However with babel 7 the syntax for ignoring files from the CLI has changed slightly, 4 | - still uses the glob format 5 | - it needs to be in between brakets `'` 6 | - comma separated for multiple instances, but without spaces 7 | - avoid using `--copy-files` as this includes non transpiled files. 8 | 9 | See example below of the npm script I use in `package.json` - `npm run build:component` 10 | ```sh 11 | "build:component": "rimraf dist && NODE_ENV=production babel src/lib --out-dir dist --ignore '**/*__tests__,**/*.spec.js,**/*.test.js,**/*__snapshots__,**/*.sample.js,**/*.sample.json,/**/example-usage.js'" , 12 | ``` 13 | 14 | [rimraf](https://github.com/isaacs/rimraf) 15 | 16 | problem is that there is a bug in babel 7, that if you add `--copy-files` then ignore flag doesn't work. 17 | This means that either you ignore the files like test etc.. or you skip adding non js files such as css or json. 18 | 19 | there might be plugin loaders workaround to add css and json but tbc -------------------------------------------------------------------------------- /packages/stt-adapters/amazon-transcribe/sample/todayinfocuswords.sample.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "start_time": "8.65", 3 | "end_time": "8.98", 4 | "alternatives": [ 5 | { 6 | "confidence": "0.9999", 7 | "content": "one" 8 | } 9 | ], 10 | "type": "pronunciation", 11 | "speaker_label": "spk_0" 12 | }, 13 | { 14 | "start_time": "8.98", 15 | "end_time": "9.05", 16 | "alternatives": [ 17 | { 18 | "confidence": "0.9999", 19 | "content": "of" 20 | } 21 | ], 22 | "type": "pronunciation", 23 | "speaker_label": "spk_1" 24 | }, 25 | { 26 | "start_time": "9.05", 27 | "end_time": "9.15", 28 | "alternatives": [ 29 | { 30 | "confidence": "1.0", 31 | "content": "the" 32 | } 33 | ], 34 | "type": "pronunciation", 35 | "speaker_label": "spk_1" 36 | }, 37 | { 38 | "start_time": "9.15", 39 | "end_time": "9.76", 40 | "alternatives": [ 41 | { 42 | "confidence": "1.0", 43 | "content": "worst" 44 | } 45 | ], 46 | "type": "pronunciation", 47 | "speaker_label": "spk_0" 48 | }] -------------------------------------------------------------------------------- /packages/stt-adapters/generate-entities-ranges/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper function to generate draft.js entities, 3 | * see unit test for example data structure 4 | * it adds offset and length to recognise word in draftjs 5 | */ 6 | 7 | /** 8 | * @param {json} words - List of words 9 | * @param {string} wordAttributeName - eg 'punct' or 'text' or etc. 10 | * attribute for the word object containing the text. eg word ={ punct:'helo', ... } 11 | * or eg word ={ text:'helo', ... } 12 | */ 13 | const generateEntitiesRanges = (words, wordAttributeName) => { 14 | let position = 0; 15 | 16 | return words.map((word) => { 17 | const result = { 18 | start: word.start, 19 | end: word.end, 20 | confidence: word.confidence, 21 | text: word[wordAttributeName], 22 | offset: position, 23 | length: word[wordAttributeName].length, 24 | key: Math.random() 25 | .toString(36) 26 | .substring(6), 27 | }; 28 | // increase position counter - to determine word offset in paragraph 29 | position = position + word[wordAttributeName].length + 1; 30 | 31 | return result; 32 | }); 33 | }; 34 | 35 | export default generateEntitiesRanges; 36 | -------------------------------------------------------------------------------- /docs/notes/2018-11-29-scroll-sync.md: -------------------------------------------------------------------------------- 1 | # Scroll sync feature notes 2 | 3 | Things considered 4 | - [`window.scrollTo(x-coord, y-coord)`](https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollTo) 5 | - [`element.scrollIntoView();`](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView) 6 | 7 | `window.scrollTo` doesn't seem to work inside an element that has `overflow: auto` which is the text element, to allow the text to scroll internally. 8 | 9 | while `element.scrollIntoView` seems like a better fit, eg: 10 | 11 | ```js 12 | // get the word element - eg 13 | let el = document.querySelector("[data-start='339.86']") 14 | 15 | // passing the `centre` value it should avoid jarring movement of the whole interface and 16 | // just move the text to the centre 17 | el.scrollIntoView({block: "center", inline: "center"}) 18 | ``` 19 | 20 | 21 | [You can try it in the demo](https://bbc.github.io/react-transcript-editor/) 22 | 23 | 24 | ## Todo 25 | _possible way to implement this_ 26 | 27 | - [ ] Add Toggle 28 | - [ ] add inside `TimedTextEditor` 29 | - [ ] inside `onTimeUpdate` 30 | - [ ] if scroll sync toggle on 31 | - [ ] after very block(?) scroll it back to centre -------------------------------------------------------------------------------- /packages/export-adapters/subtitles-generator/compose-subtitles/premiere.js: -------------------------------------------------------------------------------- 1 | import escapeText from './util/escape-text.js'; 2 | import formatSeconds from './util/format-seconds.js'; 3 | 4 | const ttmlGeneratorPremiere = (vttJSON) => { 5 | let ttmlOut = ` 6 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
`; 20 | vttJSON.forEach((v) => { 21 | ttmlOut += `

${ escapeText(v.text).replace(/\n/g, '
') }

\n`; 22 | }); 23 | ttmlOut += '
\n\n
'; 24 | 25 | return ttmlOut; 26 | }; 27 | 28 | export default ttmlGeneratorPremiere; -------------------------------------------------------------------------------- /packages/index.js: -------------------------------------------------------------------------------- 1 | import TranscriptEditor from './components/transcript-editor/index.js'; 2 | import TimedTextEditor from './components/timed-text-editor/index.js'; 3 | import Settings from './components/settings/index.js'; 4 | import KeyboardShortcuts from './components/keyboard-shortcuts/index.js'; 5 | import VideoPlayer from './components/video-player/index.js'; 6 | import MediaPlayer from './components/media-player/index.js'; 7 | import PlayerControls from './components/media-player/src/PlayerControls/index.js'; 8 | import groupWordsInParagraphsBySpeakersDPE from './stt-adapters/digital-paper-edit/group-words-by-speakers.js'; 9 | 10 | import { 11 | secondsToTimecode, 12 | timecodeToSeconds, 13 | shortTimecode 14 | } from './util/timecode-converter/index.js'; 15 | 16 | import exportAdapter from './export-adapters/index.js'; 17 | import sttJsonAdapter from './stt-adapters/index.js'; 18 | 19 | export default TranscriptEditor; 20 | 21 | export { 22 | TranscriptEditor, 23 | TimedTextEditor, 24 | VideoPlayer, 25 | MediaPlayer, 26 | PlayerControls, 27 | Settings, 28 | KeyboardShortcuts, 29 | secondsToTimecode, 30 | timecodeToSeconds, 31 | shortTimecode, 32 | exportAdapter, 33 | sttJsonAdapter, 34 | groupWordsInParagraphsBySpeakersDPE 35 | }; 36 | -------------------------------------------------------------------------------- /packages/util/timecode-converter/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Wrapping around "time stamps" and timecode conversion modules 3 | * To provide more support for variety of formats. 4 | */ 5 | import secondsToTimecode from './src/secondsToTimecode'; 6 | import timecodeToSecondsHelper from './src/timecodeToSeconds'; 7 | import padTimeToTimecode from './src/padTimeToTimecode'; 8 | 9 | /** 10 | * @param {*} time 11 | * Can take as input timecodes in the following formats 12 | * - hh:mm:ss:ff 13 | * - mm:ss 14 | * - m:ss 15 | * - ss - seconds --> if it's already in seconds then it just returns seconds 16 | * - hh:mm:ff 17 | * @todo could be refactored with some helper functions for clarity 18 | */ 19 | const timecodeToSeconds = (time) => { 20 | if (typeof time === 'string') { 21 | const resultPadded = padTimeToTimecode(time); 22 | const resultConverted = timecodeToSecondsHelper(resultPadded); 23 | 24 | return resultConverted; 25 | } 26 | 27 | // assuming it receive timecode as seconds as string '600' 28 | return parseFloat(time); 29 | }; 30 | 31 | const shortTimecode = (time) => { 32 | const timecode = secondsToTimecode(time); 33 | 34 | return timecode.slice(0, -3); 35 | }; 36 | 37 | export { secondsToTimecode, timecodeToSeconds, shortTimecode }; 38 | -------------------------------------------------------------------------------- /demo/local-storage.js: -------------------------------------------------------------------------------- 1 | const localSave = (mediaUrl, fileName, data) => { 2 | let mediaUrlName = mediaUrl; 3 | // if using local media instead of using random blob name 4 | // that makes it impossible to retrieve from on page refresh 5 | // use file name 6 | if (mediaUrlName.includes('blob')) { 7 | mediaUrlName = fileName; 8 | } 9 | 10 | localStorage.setItem(`draftJs-${ mediaUrlName }`, JSON.stringify(data)); 11 | }; 12 | 13 | // eslint-disable-next-line class-methods-use-this 14 | const isPresentInLocalStorage = (mediaUrl, fileName) => { 15 | if (mediaUrl !== null) { 16 | let mediaUrlName = mediaUrl; 17 | if (mediaUrl.includes('blob')) { 18 | mediaUrlName = fileName; 19 | } 20 | 21 | const data = localStorage.getItem(`draftJs-${ mediaUrlName }`); 22 | if (data !== null) { 23 | return true; 24 | } 25 | 26 | return false; 27 | } 28 | 29 | return false; 30 | }; 31 | 32 | const loadLocalSavedData = (mediaUrl, fileName) => { 33 | let mediaUrlName = mediaUrl; 34 | if (mediaUrl.includes('blob')) { 35 | mediaUrlName = fileName; 36 | } 37 | const data = JSON.parse(localStorage.getItem(`draftJs-${ mediaUrlName }`)); 38 | return data; 39 | }; 40 | 41 | export { loadLocalSavedData, isPresentInLocalStorage, localSave }; 42 | -------------------------------------------------------------------------------- /packages/components/settings/index.module.css: -------------------------------------------------------------------------------- 1 | .settings { 2 | position: absolute; 3 | left: 0; 4 | right: 0; 5 | margin: 0 auto; 6 | width: 30%; 7 | min-width: 300px; 8 | min-height: 300px; 9 | text-align: center; 10 | vertical-align: middle; 11 | color: white; 12 | background: #4a4a4a; 13 | padding: 1em; 14 | font-weight: lighter; 15 | z-index: 2; 16 | } 17 | 18 | .header { 19 | margin-top: 0; 20 | margin-bottom: 1em; 21 | } 22 | 23 | .closeButton { 24 | position: absolute; 25 | top: 0; 26 | right: 0; 27 | padding: 1em; 28 | cursor: pointer; 29 | } 30 | 31 | .controlsContainer { 32 | display: flex; 33 | flex-direction: column; 34 | align-content: flex-start; 35 | align-items: center; 36 | margin: 0 auto; 37 | } 38 | 39 | .settingElement { 40 | text-align: left; 41 | align-self: auto; 42 | margin-bottom: 0.5em; 43 | } 44 | 45 | .label { 46 | display: inline-block; 47 | min-width: 200px; 48 | width: 200px; 49 | } 50 | 51 | .rollbackValue { 52 | height: 2em; 53 | width: 48px; 54 | box-sizing: border-box; 55 | border: none; 56 | text-align: center; 57 | font-weight: bold; 58 | margin-right: 16px; 59 | vertical-align: middle; 60 | } 61 | 62 | .timecodeLabel { 63 | display: block; 64 | text-align: center; 65 | } 66 | -------------------------------------------------------------------------------- /packages/components/settings/Toggle/index.module.css: -------------------------------------------------------------------------------- 1 | @import '../../../config/style-guide/colours.scss'; 2 | 3 | .switchContainer { 4 | display: inline-block; 5 | position: relative; 6 | width: 48px; 7 | display: inline-block; 8 | vertical-align: middle; 9 | margin-right: 1em; 10 | } 11 | 12 | .switch { 13 | position: relative; 14 | display: inline-block; 15 | width: 48px; 16 | height: 28px; 17 | } 18 | 19 | .switch input { 20 | opacity: 0; 21 | width: 0; 22 | height: 0; 23 | } 24 | 25 | .slider { 26 | position: absolute; 27 | cursor: pointer; 28 | top: 0; 29 | left: 0; 30 | right: 0; 31 | bottom: 0; 32 | background-color: #ccc; 33 | -webkit-transition: 0.4s; 34 | transition: 0.4s; 35 | } 36 | 37 | .slider:before { 38 | position: absolute; 39 | content: ''; 40 | height: 20px; 41 | width: 20px; 42 | left: 4px; 43 | bottom: 4px; 44 | background-color: white; 45 | -webkit-transition: 0.4s; 46 | transition: 0.4s; 47 | } 48 | 49 | input:checked + .slider { 50 | background-color: $color-labs-red; 51 | } 52 | 53 | input:focus + .slider { 54 | box-shadow: 0 0 1px $color-labs-red; 55 | } 56 | 57 | input:checked + .slider:before { 58 | -webkit-transform: translateX(20px); 59 | -ms-transform: translateX(20px); 60 | transform: translateX(20px); 61 | } 62 | -------------------------------------------------------------------------------- /packages/export-adapters/txt/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert DraftJS to plain text without timecodes or speaker names 3 | * Text+speaker+timecode 4 | * TODO: have a separate one or some logic to get text without timecodes? 5 | * 6 | * Export looks like 7 | ``` 8 | 00:00:13 F_S12 9 | There is a day. About ten years ago when I asked a friend to hold a baby dinosaur called plea. All 10 | 11 | 00:00:24 F_S1 12 | that 13 | 14 | 00:00:24 F_S12 15 | he'd ordered and I was really excited about it because I've always loved about this one has really caught technical features. It had more orders and touch sensors. It had an infra red camera and one of the things that had was a tilt sensor so it. Knew what direction. It was facing. If and when you held it upside down. 16 | 17 | 00:00:46 U_UKN 18 | I thought. 19 | ``` 20 | */ 21 | // import { shortTimecode } from '../../util/timecode-converter/'; 22 | 23 | export default (blockData) => { 24 | // TODO: to export text without speaker or timecodes use line below 25 | // const lines = blockData.blocks.map(paragraph => paragraph.text); 26 | const lines = blockData.blocks.map(paragraph => { 27 | // return `${ shortTimecode(paragraph.data.words[0].start) }\t${ paragraph.data.speaker }\n${ paragraph.text }`; 28 | return `${ paragraph.text }`; 29 | }); 30 | 31 | return lines.join('\n\n'); 32 | }; 33 | -------------------------------------------------------------------------------- /demo/select-export-format.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const ExportFormatSelect = props => { 5 | return ; 22 | }; 23 | 24 | ExportFormatSelect.propTypes = { 25 | className: PropTypes.string, 26 | name: PropTypes.string, 27 | value: PropTypes.string, 28 | handleChange: PropTypes.func 29 | }; 30 | 31 | export default ExportFormatSelect; 32 | -------------------------------------------------------------------------------- /packages/export-adapters/subtitles-generator/presegment-text/line-break-between-sentences/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import addLineBreakBetweenSentences from './index.js'; 3 | 4 | var sampleText = `Hi there, my name is Ian police - are recording this video to talk about mercury for the folks at a tech daily conference in New York. 5 | Sorry, I can't be there in person, so we are building a prototype funded in part by Google DNI of a web-based computer, assisted transcription and translation tool with some video editing features. 6 | It does speech to text and then automated consistent translation and then text to speech generate synthetic voices at time codes that line up with the original original audio.`; 7 | 8 | var expectedOutput = `Hi there, my name is Ian police - are recording this video to talk about mercury for the folks at a tech daily conference in New York. 9 | 10 | Sorry, I can't be there in person, so we are building a prototype funded in part by Google DNI of a web-based computer, assisted transcription and translation tool with some video editing features. 11 | 12 | It does speech to text and then automated consistent translation and then text to speech generate synthetic voices at time codes that line up with the original original audio.`; 13 | 14 | test('add line break between sentences', () => { 15 | var result = addLineBreakBetweenSentences(sampleText); 16 | expect(result).toBe(expectedOutput); 17 | }); 18 | -------------------------------------------------------------------------------- /docs/adr/2018-12-06-analytics.md: -------------------------------------------------------------------------------- 1 | # Analytics 2 | 3 | * Status: being evaluated 4 | * Deciders: Pietro, Luke, James 5 | * Date: 2018-12-06 6 | 7 | ## Context and Problem Statement 8 | 9 | It be great to be able to track some component level analytics in a way that is agnostic to the tracking provider (eg piwik/[matomo](https://developer.matomo.org/api-reference/tracking-javascript#using-the-tracker-object), google analytics etc..) 10 | 11 | ## Decision Drivers 12 | 13 | * easy to reason around 14 | * clean interface 15 | * flexible to integrate with variety of analytics providers 16 | * un-opinionated in regards to the API end point and how to make that request 17 | 18 | ## Considered Options 19 | 20 | 1. [npm analytics](https://www.npmjs.com/package/analytics) module. 21 | > This is a pluggable event driven analytics library designed to work with any third party analytics tool. 22 | 23 | 2. Making one from scratch - a class that takes in an option and handles the logic to call the end points 24 | 25 | 3. just raise events to the top parent component - see [notes here](../notes/2018-12-06-analytics-raise-events.md) then parent component can decide how to handle depending on the library in use for analytics. 26 | 27 | ## Decision Outcome 28 | 29 | 30 | Option 3 - as suggested by Luke, raise event to parent component. 31 | 32 | 33 | ## Other 34 | 35 | - [Working With Events in React](https://css-tricks.com/working-with-events-in-react/) -------------------------------------------------------------------------------- /demo/select-stt-json-type.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const SttTypeSelect = props => { 5 | return ; 23 | }; 24 | 25 | SttTypeSelect.propTypes = { 26 | className: PropTypes.string, 27 | name: PropTypes.string, 28 | value: PropTypes.string, 29 | handleChange: PropTypes.func 30 | }; 31 | 32 | export default SttTypeSelect; 33 | -------------------------------------------------------------------------------- /packages/components/media-player/src/Select.module.scss: -------------------------------------------------------------------------------- 1 | .selectPlayerControl[name='playbackRate'] { 2 | font-size: 1em; 3 | -webkit-appearance: none; 4 | -moz-appearance: none; 5 | appearance: none; 6 | height: 48px; 7 | width: 97px; 8 | border-radius: 0; 9 | background-image: url(data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTguMS4xLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDEyNS4zMDQgMTI1LjMwNCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTI1LjMwNCAxMjUuMzA0OyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgd2lkdGg9IjE2cHgiIGhlaWdodD0iMTZweCI+CjxnPgoJPGc+CgkJPHBvbHlnb24gcG9pbnRzPSI2Mi42NTIsMTAzLjg5NSAwLDIxLjQwOSAxMjUuMzA0LDIxLjQwOSAgICIgZmlsbD0iI0ZGRkZGRiIvPgoJPC9nPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+Cjwvc3ZnPgo=); 10 | background-repeat: no-repeat; 11 | background-position: 85% center; 12 | color: white; 13 | border-color: transparent; 14 | padding-left: 32px; 15 | } 16 | 17 | @media (max-width: 768px) { 18 | 19 | .selectPlayerControl[name='playbackRate'] { 20 | width: 100%; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/export-adapters/subtitles-generator/presegment-text/fold/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import foldWords from './index.js'; 3 | 4 | const sampleText = `Hi there, my name is Ian police - are recording this video to talk about mercury for the folks at a tech daily conference in New York. 5 | 6 | Sorry, I can't be there in person, so we are building a prototype funded in part by Google DNI of a web-based computer, assisted transcription and translation tool with some video editing features. 7 | 8 | It does speech to text and then automated consistent translation and then text to speech generate synthetic voices at time codes that line up with the original original audio.`; 9 | 10 | const expectedOutput = `Hi there, my name is Ian police - 11 | are recording this video to talk 12 | about mercury for the folks at a 13 | tech daily conference in New York. 14 | 15 | Sorry, I can't be there in person, 16 | so we are building a prototype 17 | funded in part by Google DNI of a 18 | web-based computer, assisted 19 | transcription and translation tool 20 | with some video editing features. 21 | 22 | It does speech to text and then 23 | automated consistent translation 24 | and then text to speech generate 25 | synthetic voices at time codes 26 | that line up with the original 27 | original audio.`; 28 | 29 | test('fold words at 35 char', () => { 30 | const result = foldWords(sampleText, 35); 31 | expect(result).toBe(expectedOutput); 32 | }); 33 | -------------------------------------------------------------------------------- /packages/util/timecode-converter/src/padTimeToTimecode.js: -------------------------------------------------------------------------------- 1 | const countColon = timecode => timecode.split(':').length; 2 | 3 | const includesFullStop = timecode => timecode.includes('.'); 4 | 5 | const isOneDigit = str => str.length === 1; 6 | 7 | const padTimeToTimecode = (time) => { 8 | if (typeof time === 'string') { 9 | switch (countColon(time)) { 10 | case 4: 11 | // is already in timecode format 12 | // hh:mm:ss:ff 13 | return time; 14 | case 2: 15 | // m:ss 16 | if (isOneDigit(time.split(':')[0])) { 17 | return `00:0${ time }:00`; 18 | } 19 | 20 | return `00:${ time }:00`; 21 | case 3: 22 | // hh:mm:ss 23 | return `${ time }:00`; 24 | default: 25 | // mm.ss 26 | if (includesFullStop(time)) { 27 | // m.ss 28 | if (isOneDigit(time.split('.')[0])) { 29 | return `00:0${ time.split('.')[0] }:${ time.split('.')[1] }:00`; 30 | } 31 | 32 | return `00:${ time.replace('.', ':') }:00`; 33 | } 34 | 35 | // if just int, then it's seconds 36 | // s 37 | if (isOneDigit(time)) { 38 | return `00:00:0${ time }:00`; 39 | } 40 | 41 | return `00:00:${ time }:00`; 42 | } 43 | // edge case if it's number return a number coz cannot refactor 44 | // TODO: might need to refactor and move this elsewhere 45 | } else { 46 | return time; 47 | } 48 | }; 49 | 50 | export default padTimeToTimecode; 51 | -------------------------------------------------------------------------------- /packages/stt-adapters/amazon-transcribe/group-words-by-speakers.js: -------------------------------------------------------------------------------- 1 | export const groupWordsBySpeakerLabel = (words) => { 2 | const groupedWords = []; 3 | let currentSpeaker = ''; 4 | words.forEach((word) => { 5 | if (word.speaker_label === currentSpeaker) { 6 | groupedWords[groupedWords.length - 1].words.push(word); 7 | } else { 8 | currentSpeaker = word.speaker_label; 9 | // start new speaker block 10 | groupedWords.push({ 11 | speaker: word.speaker_label, 12 | words: [ word ] }); 13 | } 14 | }); 15 | 16 | return groupedWords; 17 | }; 18 | 19 | export const findSpeakerForWord = (word, segments) => { 20 | const startTime = parseFloat(word.start_time); 21 | const endTime = parseFloat(word.end_time); 22 | const firstMatchingSegment = segments.find((seg) => { 23 | return startTime >= parseFloat(seg.start_time) && endTime <= parseFloat(seg.end_time); 24 | }); 25 | if (firstMatchingSegment === undefined) { 26 | return 'UKN'; 27 | } else { 28 | return firstMatchingSegment.speaker_label.replace('spk_', ''); 29 | } 30 | }; 31 | 32 | const addSpeakerLabelToWords = (words, segments) => { 33 | return words.map(w => Object.assign(w, { 'speaker_label': findSpeakerForWord(w, segments) })); 34 | }; 35 | 36 | export const groupWordsBySpeaker = (words, speakerLabels) => { 37 | const wordsWithSpeakers = addSpeakerLabelToWords(words, speakerLabels.segments); 38 | 39 | return groupWordsBySpeakerLabel(wordsWithSpeakers); 40 | }; -------------------------------------------------------------------------------- /packages/components/keyboard-shortcuts/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { faWindowClose } from '@fortawesome/free-solid-svg-icons'; 4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 5 | 6 | import returnHotKeys from './hot-keys'; 7 | 8 | import style from './index.module.css'; 9 | 10 | export const getHotKeys = returnHotKeys; 11 | 12 | class KeyboardShortcuts extends React.Component { 13 | render() { 14 | const hotKeys = returnHotKeys(this); 15 | 16 | const hotKeysCheatsheet = Object.keys(hotKeys).map(key => { 17 | const shortcut = hotKeys[key]; 18 | 19 | return ( 20 |
  • 21 |
    {shortcut.displayKeyCombination}
    22 |
    {shortcut.label}
    23 |
  • 24 | ); 25 | }); 26 | 27 | return ( 28 |
    29 |

    Shortcuts

    30 |
    33 | 34 |
    35 |
      {hotKeysCheatsheet}
    36 |
    37 | ); 38 | } 39 | } 40 | 41 | KeyboardShortcuts.propTypes = { 42 | handleShortcutsToggle: PropTypes.func 43 | }; 44 | 45 | export default KeyboardShortcuts; 46 | -------------------------------------------------------------------------------- /docs/qa/4-keyboard-shortcuts.md: -------------------------------------------------------------------------------- 1 | ### Item to test #4: Keyboard Shortcuts 2 | 3 | #### Item to test #4.1:Keyboard Shortcuts - show 4 | 5 | ##### Steps: 6 | - click on keyboard shortcut, icon top left 7 | ##### Expected Results: 8 | - [ ] Expect keyboard shortcut panel to come up 9 | 10 | #### Item to test #4.2: Keyboard Shortcuts - hide 11 | 12 | ##### Steps: 13 | - click on keyboard shortcut panel cross, top right 14 | ##### Expected Results: 15 | - [ ] Expect keyboard shortcut panel to hide 16 | 17 | #### Item to test #4.3: Keyboard Shortcuts 18 | 19 | | n | Functionality | Steps | Expected Results | 20 | |---|--- |--- |--- | 21 | | 1 |Play Media | click keyboard shortcut combination | to play/pause media 22 | | 2 | set current time | click keyboard shortcut combination | to prompt change current time dialog 23 | | 3 | Fast Forward | click keyboard shortcut combination | To fast forward media 24 | | 4 | Rewind | click keyboard shortcut combination | to rewind media 25 | | 5 | Increase playback speed | click keyboard shortcut combination | to Increase playback speed of the media, and show current playback speed in playback speed btn 26 | | 6 | Decrease playback speed | click keyboard shortcut combination | to Decrease playback speed of the media, and show current playback speed in playback speed btn 27 | | 6 | Roll Back | click keyboard shortcut combination | to RollBack playhead default amount of 15 sec, or custom amount set in settings 28 | -------------------------------------------------------------------------------- /packages/export-adapters/subtitles-generator/presegment-text/index.js: -------------------------------------------------------------------------------- 1 | import textSegmentation from './text-segmentation/index.js'; 2 | import addLineBreakBetweenSentences from './line-break-between-sentences/index.js'; 3 | import foldWords from './fold/index.js'; 4 | import divideIntoTwoLines from './divide-into-two-lines/index.js'; 5 | 6 | /** 7 | * Takes in array of word object, 8 | * and returns string containing all the text 9 | * @param {array} words - Words 10 | */ 11 | function getTextFromWordsList(words) { 12 | return words.map((word) => {return word.text;}).join(' '); 13 | } 14 | 15 | /** 16 | * 17 | * @param {*} textInput - can be either plain text string or an array of word objects 18 | */ 19 | function preSegmentText(textInput, tmpNumberOfCharPerLine = 35) { 20 | let text = textInput; 21 | if (typeof textInput === 'object') { 22 | text = getTextFromWordsList(textInput); 23 | } 24 | const segmentedText = textSegmentation(text); 25 | // - 2.Line brek between stentences 26 | const textWithLineBreakBetweenSentences = addLineBreakBetweenSentences(segmentedText); 27 | // - 3.Fold char limit per line 28 | const foldedText = foldWords(textWithLineBreakBetweenSentences, tmpNumberOfCharPerLine); 29 | // - 4.Divide into two lines 30 | // console.log(foldedText) 31 | const textDividedIntoTwoLines = divideIntoTwoLines(foldedText); 32 | 33 | return textDividedIntoTwoLines; 34 | } 35 | 36 | export { 37 | preSegmentText, 38 | getTextFromWordsList 39 | }; 40 | 41 | export default preSegmentText; -------------------------------------------------------------------------------- /docs/notes/2018-11-29-git-cheat-sheet.md: -------------------------------------------------------------------------------- 1 | # Git Cheat Sheet 2 | 3 | ## git create branch from un-staged changes 4 | ``` 5 | git checkout -b new_branch_name 6 | ``` 7 | 8 | ## checkout pull request locally 9 | ``` 10 | git fetch origin pull/ID/head:BRANCHNAME 11 | ``` 12 | 13 | ## git pull from remote to override local changes 14 | 15 | ``` 16 | git fetch origin 17 | ``` 18 | ``` 19 | git reset --hard origin/master 20 | ``` 21 | 22 | ## Git pull remote branch locally 23 | ``` 24 | git fetch origin 25 | ``` 26 | ``` 27 | git checkout --track origin/ 28 | ``` 29 | 30 | ## git push local branch to remote 31 | ``` 32 | git checkout -b 33 | ``` 34 | ``` 35 | git push -u origin 36 | ``` 37 | 38 | ## Git merge master to update branch 39 | ``` 40 | git checkout 41 | ``` 42 | ``` 43 | git merge master 44 | ``` 45 | 46 | or [merging vs rebasing](https://www.atlassian.com/git/tutorials/merging-vs-rebasing ) 47 | 48 | 49 | ## to view previous commits 50 | 51 | ``` 52 | git log 53 | ``` 54 | ## to add to previous commit 55 | 56 | ### If have not pushed 57 | 58 | ``` 59 | git commit --amend 60 | ``` 61 | - [Changing a commit message](https://help.github.com/articles/changing-a-commit-message/) 62 | - [rewriting history blog post](https://robots.thoughtbot.com/git-interactive-rebase-squash-amend-rewriting-history 63 | ) 64 | 65 | 66 | ### if you have pushed 67 | if you have pushed already same as above but you can force a push to amend remote as well 68 | 69 | ``` 70 | git push -f 71 | ``` 72 | -------------------------------------------------------------------------------- /packages/export-adapters/subtitles-generator/presegment-text/divide-into-two-lines/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import divideIntoTwoLines from './index.js'; 3 | 4 | var sampleText = `Hi there, my name is Ian police - 5 | are recording this video to talk 6 | about mercury for the folks at a 7 | tech daily conference in New York. 8 | 9 | Sorry, I can’t be there in person, 10 | so we are building a prototype 11 | funded in part by Google DNI of a 12 | web-based computer, assisted 13 | transcription and translation tool 14 | with some video editing features. 15 | 16 | It does speech to text and then 17 | automated consistent translation 18 | and then text to speech generate 19 | synthetic voices at time codes that 20 | line up with the original original 21 | audio.`; 22 | 23 | var expectedOutput = `Hi there, my name is Ian police - 24 | are recording this video to talk 25 | 26 | about mercury for the folks at a 27 | tech daily conference in New York. 28 | 29 | Sorry, I can’t be there in person, 30 | so we are building a prototype 31 | 32 | funded in part by Google DNI of a 33 | web-based computer, assisted 34 | 35 | transcription and translation tool 36 | with some video editing features. 37 | 38 | It does speech to text and then 39 | automated consistent translation 40 | 41 | and then text to speech generate 42 | synthetic voices at time codes that 43 | 44 | line up with the original original 45 | audio.`; 46 | 47 | test('divide into two lines', () => { 48 | var result = divideIntoTwoLines(sampleText); 49 | expect(result).toBe(expectedOutput); 50 | }); 51 | -------------------------------------------------------------------------------- /packages/util/timecode-converter/src/padTimeToTimecode.test.js: -------------------------------------------------------------------------------- 1 | import padTimeToTimecode from './padTimeToTimecode'; 2 | 3 | describe('Timecode conversion TC- convertToSeconds', () => { 4 | it('Should be defined', ( ) => { 5 | const demoTimecode = '12:34:56:78'; 6 | const result = padTimeToTimecode(demoTimecode); 7 | expect(result).toBeDefined(); 8 | }); 9 | 10 | it('hh:mm:ss:ff --> hh:mm:ss:ff ', ( ) => { 11 | const demoTimecode = '12:34:56:78'; 12 | const result = padTimeToTimecode(demoTimecode); 13 | expect(result).toEqual(demoTimecode); 14 | }); 15 | 16 | it('mm:ss --> convert to hh:mm:ss:ms', ( ) => { 17 | const demoTime = '34:56'; 18 | const expectedTimecode = '00:34:56:00'; 19 | const result = padTimeToTimecode(demoTime); 20 | expect(result).toEqual(expectedTimecode); 21 | }); 22 | 23 | xit('hh:mm:ss --> convert to hh:mm:ss:ms', ( ) => { 24 | const demoTime = '34:56:78'; 25 | const expectedTimecode = '00:34:56:78'; 26 | const result = padTimeToTimecode(demoTime); 27 | expect(result).toEqual(expectedTimecode); 28 | }); 29 | 30 | it('mm.ss--> convert to hh:mm:ss:ms', ( ) => { 31 | const demoTime = '34.56'; 32 | const expectedTimecode = '00:34:56:00'; 33 | const result = padTimeToTimecode(demoTime); 34 | expect(result).toEqual(expectedTimecode); 35 | }); 36 | 37 | it('120 sec --> 120', ( ) => { 38 | const demoTime = 120; 39 | const expectedTimecode = 120; 40 | const result = padTimeToTimecode(demoTime); 41 | expect(result).toEqual(expectedTimecode); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /packages/components/media-player/stories/index.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { storiesOf } from '@storybook/react'; 4 | import { action } from '@storybook/addon-actions'; 5 | import { withKnobs, text, number } from '@storybook/addon-knobs'; 6 | 7 | import MediaPlayer from '../index.js'; 8 | 9 | storiesOf('MediaPlayer', module) 10 | .addDecorator(withKnobs) 11 | .add('default', () => { 12 | const videoRef = React.createRef(); 13 | 14 | const mediaUrl = 'https://download.ted.com/talks/KateDarling_2018S-950k.mp4'; 15 | 16 | const fixtureProps = { 17 | videoRef: videoRef, 18 | title: text('title', 'Ted Talk'), 19 | hookSeek: action('hookSeek'), 20 | hookPlayMedia: action('hookPlayMedia'), 21 | hookIsPlaying: action('hookIsPlaying'), 22 | mediaUrl: text('mediaUrl', mediaUrl), 23 | hookOnTimeUpdate: action('hookOnTimeUpdate'), 24 | rollBackValueInSeconds: number('rollBackValueInSeconds', 10), 25 | timecodeOffset: number('timecodeOffset', 0), 26 | handleAnalyticsEvents: action('handleAnalyticsEvents'), 27 | mediaDuration: text('mediaDuration', '01:00:00:00'), 28 | handleSaveTranscript: action('handleSaveTranscript') 29 | }; 30 | 31 | return ( 32 | 33 | 34 |
    35 | 41 |
    42 | ); 43 | }); 44 | -------------------------------------------------------------------------------- /docs/qa/2-timed-text-editor.md: -------------------------------------------------------------------------------- 1 | ### Item to test #2: Timed Text Editor 2 | 3 | #### Item to test #2.1: Timed Text Editor - double click on words 4 | 5 | ![timed-text-editor-double-click-on-word](./images/timed-text-editor-double-click-on-word.gif) 6 | 7 | ##### Steps: 8 | - Double click on a word 9 | ##### Expected Results: 10 | - [ ] Expect play head and current time it to jump to the corresponding point in the media 11 | - [ ] Expect the text before the word to be darker color 12 | - [ ] Expect current word to be highlighted 13 | - [ ] Expect media to start playing and highlight and current word to continue 14 | 15 | #### Item to test #2.2: Timed Text Editor - click on timecodes 16 | 17 | ##### Steps: 18 | - Click on timecode next to the text of the paragraph 19 | ##### Expected Results: 20 | - [ ] Expect play head and current time it to jump to the corresponding point in the media beginning of paragraph 21 | 27 | 28 | #### Item to test #2.3: Timed Text Editor - Edit speakers labels 29 | 30 | #### Steps: 31 | - click on speaker icon next to default speaker names 32 | #### Expected Results: 33 | - [ ] Expect a prompt to come up, 34 | - [ ] on add text and click on expect name to change in Timed Text Editor next to speaker label icon clicked. 35 | - [ ] if instead of ok click cancel, expect nothing to happen 36 | 37 | --- 38 | -------------------------------------------------------------------------------- /docs/notes/draftjs/2018-10-20-draftjs-6-local-storage.md: -------------------------------------------------------------------------------- 1 | # saving to local storage on keystroke 2 | 3 | Getting data 4 | 5 | ```js 6 | const data = convertToRaw(editorState.getCurrentContent()); 7 | ``` 8 | 9 | 10 | Import `convertToRaw` the same way you import `convertFromRaw` then you can iterate that data do to various things 11 | 12 | eg in bbc transcript model https://github.com/bbc/subtitalizer/blob/d102c233236b782011a4a94a21e6de13491abb45/src/components/TranscriptEditor.js#L105-L241 13 | 14 | so, you need to get JSON of that content state, store that in localstorage, and use that to load into draft back. 15 | 16 | So, basically 17 | 18 | https://github.com/bbc/subtitalizer/blob/d102c233236b782011a4a94a21e6de13491abb45/src/components/TranscriptEditor.js#L105-L241 19 | 20 | without the Immutable things 21 | 22 | 23 | 24 | ## how often to save? saving on every keystroke? 25 | 26 | it will make it slow, can do debounce to improve perfroamce 27 | 28 | 29 | ```js 30 | this.debouncedRebuildTranscript = debounce(this.rebuildTranscript, 100); 31 | ``` 32 | 33 | 34 | https://github.com/bbc/subtitalizer/blob/d102c233236b782011a4a94a21e6de13491abb45/src/components/TranscriptEditor.js#L266 35 | 36 | ```js 37 | import debounce from 'lodash.debounce'; 38 | ``` 39 | 40 | [https://lodash.com/docs/4.17.11](https://lodash.com/docs/4.17.11) - [debounce](https://lodash.com/docs#debounce) 41 | 42 | This as you type, it postpones that until you do a 100ms pause in this example. 43 | 44 | Laurian 45 | > Do it without, then test and see if you need it, but there is a lot of computation to do on a keystroke, and JS is single threaded -------------------------------------------------------------------------------- /packages/export-adapters/subtitles-generator/presegment-text/text-segmentation/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import textSegmentation from './index.js'; 3 | 4 | var sampleText = 'Hi there, my name is Mr. Ian police - are recording this video to talk about mercury for the folks at a tech daily conference in New York. Sorry, I can\'t be there in person, so we are building a prototype funded in part by Google DNI of a web-based computer, assisted transcription and translation tool with some video editing features. It does speech to text and then automated consistent translation and then text to speech generate synthetic voices at time codes that line up with the original original audio.'; 5 | 6 | var expectedOutput = `Hi there, my name is Mr. Ian police - are recording this video to talk about mercury for the folks at a tech daily conference in New York. 7 | Sorry, I can't be there in person, so we are building a prototype funded in part by Google DNI of a web-based computer, assisted transcription and translation tool with some video editing features. 8 | It does speech to text and then automated consistent translation and then text to speech generate synthetic voices at time codes that line up with the original original audio.`; 9 | 10 | var optionalHonorificsSample = 'Mr'; 11 | 12 | test('add line break between sentences', () => { 13 | var result = textSegmentation(sampleText); 14 | expect(result).toBe(expectedOutput); 15 | }); 16 | 17 | test('add line break between sentences,with optional honorifics', () => { 18 | var result = textSegmentation(sampleText, optionalHonorificsSample); 19 | expect(result).toBe(expectedOutput); 20 | }); 21 | -------------------------------------------------------------------------------- /docs/qa/5-analytics.md: -------------------------------------------------------------------------------- 1 | ### Item to test #5: Analytics 2 | 3 | Analytics in[the demo app](https://bbc.github.io/react-transcript-editor/) are logged in a text area below the component for demonstration purposes. 4 | This can also be used to test tha they are fulling working. 5 | 6 | Every new event return an object into an array in the text area. 7 | Inspecting the array for new elements gives a good indication of whether all expected events are being tracked. eg if an event is not in the array list then the code might not be working as expected. 8 | 9 | Here's a list of events grouped by functionality. It might be easier to test one at a time, or if already done the test for above items, can just review the array generated so far to see if any of these are missing. 10 | 11 | ##### info 12 | - [ ] duration of media 13 | - [ ] number of words in transcript 14 | 15 | _these we'd expect to be triggered first_ 16 | 17 | ##### actions 18 | - [ ] click on progress bar 19 | - [ ] double click on word // now also triggered when clicking on time-codes 20 | - [ ] click on time-codes, at paragraph level 21 | - [ ] Set current time, Jump to time, click on current time display 22 | - [ ] play/pause, click on media preview // but triggered by other events as well 23 | - [ ] playback speed change 24 | - [ ] use of keyboard shortcuts // see keyboard shortcut cheat sheet and test each individually 25 | - [ ] edit speaker label 26 | - [ ] ~skip forward~ 27 | - [ ] ~skip backward~ 28 | 29 | ##### settings 30 | - [ ] set timecode offset 31 | - [ ] pause while typing 32 | - [ ] scroll into view 33 | 34 | - [ ] rollback 35 | - [ ] Toggles speaker names - show/hide 36 | -------------------------------------------------------------------------------- /docs/notes/2018-10-20-local-storage-draft-js.md: -------------------------------------------------------------------------------- 1 | 2018-11-20-local-saving-to-local-storage-on-keystroke.md 2 | 3 | # saving to local storage on keystroke 4 | 5 | Getting data 6 | 7 | ```js 8 | const data = convertToRaw(editorState.getCurrentContent()); 9 | ``` 10 | 11 | 12 | Import `convertToRaw` the same way you import `convertFromRaw` then you can iterate that data do to various things 13 | 14 | eg in bbc transcript model https://github.com/bbc/subtitalizer/blob/d102c233236b782011a4a94a21e6de13491abb45/src/components/TranscriptEditor.js#L105-L241 15 | 16 | so, you need to get JSON of that content state, store that in localstorage, and use that to load into draft back. 17 | 18 | So, basically 19 | 20 | https://github.com/bbc/subtitalizer/blob/d102c233236b782011a4a94a21e6de13491abb45/src/components/TranscriptEditor.js#L105-L241 21 | 22 | without the Immutable things 23 | 24 | 25 | 26 | ## how often to save? saving on every keystroke? 27 | 28 | it will make it slow, can do debounce to improve perfroamce 29 | 30 | 31 | ```js 32 | this.debouncedRebuildTranscript = debounce(this.rebuildTranscript, 100); 33 | ``` 34 | 35 | 36 | https://github.com/bbc/subtitalizer/blob/d102c233236b782011a4a94a21e6de13491abb45/src/components/TranscriptEditor.js#L266 37 | 38 | ```js 39 | import debounce from 'lodash.debounce'; 40 | ``` 41 | 42 | [https://lodash.com/docs/4.17.11](https://lodash.com/docs/4.17.11) - [debounce](https://lodash.com/docs#debounce) 43 | 44 | This as you type, it postpones that until you do a 100ms pause in this example. 45 | 46 | Laurian 47 | > Do it without, then test and see if you need it, but there is a lot of computation to do on a keystroke, and JS is single threaded -------------------------------------------------------------------------------- /packages/stt-adapters/google-stt/index.test.js: -------------------------------------------------------------------------------- 1 | import gcpSttToDraft, { 2 | getBestAlternativeSentence, 3 | trimLeadingAndTailingWhiteSpace 4 | } from './index'; 5 | import draftTranscriptSample from './sample/googleSttToDraftJs.sample.js'; 6 | import gcpSttTedTalkTranscript from './sample/gcpSttPunctuation.sample.json'; 7 | 8 | describe('gcpSttToDraft', () => { 9 | const result = gcpSttToDraft(gcpSttTedTalkTranscript); 10 | it('Should be defined', () => { 11 | expect(result).toBeDefined(); 12 | }); 13 | 14 | it('Should be equal to expected value', () => { 15 | expect(result).toEqual(draftTranscriptSample); 16 | }); 17 | }); 18 | 19 | describe('leading and tailing white space should be removed from text block', () => { 20 | const sentence = ' this is a sentence '; 21 | const expected = 'this is a sentence'; 22 | 23 | const result = trimLeadingAndTailingWhiteSpace(sentence); 24 | it('should be equal to expected value', () => { 25 | expect(result).toEqual(expected); 26 | }); 27 | }); 28 | 29 | describe('Best alternative sentence should be returned', () => { 30 | const sentences = { 31 | alternatives: [ 32 | { 33 | 'transcript': 'this is the first sentence', 34 | 'confidence': 0.95, 35 | }, 36 | { 37 | 'transcript': 'this is the first sentence alternative', 38 | 'confidence': 0.80, 39 | } 40 | ] 41 | }; 42 | const expected = { 43 | 'transcript': 'this is the first sentence', 44 | 'confidence': 0.95 45 | }; 46 | 47 | it('Should be equal to expected value', () => { 48 | 49 | const result = getBestAlternativeSentence(sentences); 50 | expect(result).toEqual(expected); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/export-adapters/subtitles-generator/presegment-text/line-break-between-sentences/README.md: -------------------------------------------------------------------------------- 1 | # Line break between sentences 2 | 3 | 4 | separates each line (a sentence) with an empty line. 5 | 6 | 7 | #### Input 8 | 9 | Text where each sentence that ends with full stop is on a new line. `\n`. 10 | 11 | ``` 12 | Hi there, my name is Ian police - are recording this video to talk about mercury for the folks at a tech daily conference in New York. 13 | Sorry, I can't be there in person, so we are building a prototype funded in part by Google DNI of a web-based computer, assisted transcription and translation tool with some video editing features. 14 | It does speech to text and then automated consistent translation and then text to speech generate synthetic voices at time codes that line up with the original original audio. 15 | ``` 16 | 17 | #### Output 18 | 19 | ``` 20 | Hi there, my name is Ian police - are recording this video to talk about mercury for the folks at a tech daily conference in New York. 21 | 22 | Sorry, I can't be there in person, so we are building a prototype funded in part by Google DNI of a web-based computer, assisted transcription and translation tool with some video editing features. 23 | 24 | It does speech to text and then automated consistent translation and then text to speech generate synthetic voices at time codes that line up with the original original audio. 25 | ``` 26 | 27 | #### algo 28 | 29 | ```bash 30 | # Add blank line after every new line 31 | sed -e 'G' test.txt > test2.txt 32 | ``` 33 | 34 | Equivalent to 35 | 36 | ```js 37 | test.replace(/\n/g,"\n\n") 38 | ``` 39 | -------------------------------------------------------------------------------- /packages/stt-adapters/amazon-transcribe/group-words-by-speakers.test.js: -------------------------------------------------------------------------------- 1 | import amazonTodayInFocusTranscript from './sample/todayinfocus.sample.json'; 2 | import wordsWithSpeakers from './sample/todayinfocuswords.sample.json'; 3 | 4 | import { groupWordsBySpeakerLabel, findSpeakerForWord, groupWordsBySpeaker } from './group-words-by-speakers'; 5 | 6 | const words = amazonTodayInFocusTranscript.results.items; 7 | const speakerLabels = amazonTodayInFocusTranscript.results.speaker_labels; 8 | 9 | describe('groupWordsBySpeakerLabel', () => { 10 | 11 | it('Should group speakers correctly', ( ) => { 12 | 13 | const groups = groupWordsBySpeakerLabel(wordsWithSpeakers); 14 | expect(groups[0].speaker).toBe('spk_0'); 15 | expect(groups[0].words.length).toBe(1); 16 | expect(groups[1].speaker).toBe('spk_1'); 17 | expect(groups[1].words.length).toBe(2); 18 | }); 19 | }); 20 | 21 | describe('findSpeakerForWord', () => { 22 | 23 | it('Should find correct speaker', ( ) => { 24 | 25 | const speaker = findSpeakerForWord({ 26 | 'start_time': '8.65', 27 | 'end_time': '8.98', 28 | 'alternatives': [ 29 | { 30 | 'confidence': '0.9999', 31 | 'content': 'one' 32 | } 33 | ], 34 | 'type': 'pronunciation' 35 | }, speakerLabels.segments); 36 | 37 | expect(speaker).toBe('0'); 38 | }); 39 | }); 40 | 41 | describe('groupWordsBySpeaker', () => { 42 | /** Hopefully the other unit tests suffice. 43 | * this is a rather lazy one to check the full results 44 | */ 45 | it('Should return expected number of groups', ( ) => { 46 | 47 | const groups = groupWordsBySpeaker(words, speakerLabels); 48 | expect(groups.length).toBe(173); 49 | }); 50 | }); -------------------------------------------------------------------------------- /packages/export-adapters/draftjs-to-digital-paper-edit/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert DraftJS to Digital Paper Edit format 3 | * More details see 4 | * https://github.com/bbc/digital-paper-edit 5 | */ 6 | export default (blockData) => { 7 | const result = { words: [], paragraphs: [] }; 8 | 9 | blockData.blocks.forEach((block, index) => { 10 | if (block.data.words !== undefined) { 11 | // TODO: make sure that when restoring timecodes text attribute in block word data 12 | // should be updated as well 13 | const tmpParagraph = { 14 | id: index, 15 | start: block.data.words[0].start, //block.data.start, 16 | end: block.data.words[block.data.words.length - 1].end, 17 | speaker: block.data.speaker 18 | }; 19 | result.paragraphs.push(tmpParagraph); 20 | // using data within a block to get words info 21 | const tmpWords = block.data.words.map((word) => { 22 | const tmpWord = { 23 | id: word.index, 24 | start: word.start, 25 | end: word.end, 26 | text: null 27 | }; 28 | // TODO: need to normalise various stt adapters 29 | // so that when they create draftJs json, word text attribute 30 | // has got consistent naming. `text` and not `punct` or `word`. 31 | if (word.text) { 32 | tmpWord.text = word.text; 33 | } 34 | else if (word.punct) { 35 | tmpWord.text = word.punct; 36 | } 37 | else if (word.word) { 38 | tmpWord.text = word.punct; 39 | } 40 | 41 | return tmpWord; 42 | }); 43 | // flattening the list of words 44 | result.words = result.words.concat(tmpWords); 45 | } 46 | }); 47 | 48 | return result; 49 | }; 50 | -------------------------------------------------------------------------------- /packages/components/settings/stories/index.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { storiesOf } from '@storybook/react'; 4 | import { action } from '@storybook/addon-actions'; 5 | import { withKnobs, number, boolean } from '@storybook/addon-knobs'; 6 | 7 | import Settings from '../index.js'; 8 | 9 | storiesOf('Settings', module) 10 | .addDecorator(withKnobs) 11 | .add('default', () => { 12 | 13 | const fixtureProps = { 14 | handleSettingsToggle: action('Toggle settings'), 15 | showTimecodes: boolean('showTimecodes', true), 16 | showSpeakers: boolean('showSpeakers', true), 17 | timecodeOffset: number('timecodeOffset', 3600), 18 | defaultValueScrollSync: boolean('defaultValueScrollSync', true), 19 | defaultValuePauseWhileTyping: boolean('defaultValuePauseWhileTyping', true), 20 | defaultRollBackValueInSeconds: number('defaultRollBackValueInSeconds', 10), 21 | previewIsDisplayed: boolean('previewIsDisplayed', true), 22 | handleShowTimecodes: action('handleShowTimecodes'), 23 | handleShowSpeakers: action('handleShowSpeakers'), 24 | handleSetTimecodeOffset: action('handleSetTimecodeOffset'), 25 | handleSettingsToggle: action('handleSettingsToggle'), 26 | handlePauseWhileTyping: action('handlePauseWhileTyping'), 27 | handleIsScrollIntoViewChange: action('handleIsScrollIntoViewChange'), 28 | handleRollBackValueInSeconds: action('handleRollBackValueInSeconds'), 29 | handlePreviewIsDisplayed: action('handlePreviewIsDisplayed'), 30 | handleChangePreviewViewWidth: action('handleChangePreviewViewWidth'), 31 | handleAnalyticsEvents: action('handleAnalyticsEvents') 32 | }; 33 | 34 | return ( 35 | 36 | );}); 37 | -------------------------------------------------------------------------------- /packages/util/timecode-converter/src/secondsToTimecode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Raised in this comment https://github.com/bbc/react-transcript-editor/pull/9 3 | * abstracted from https://github.com/bbc/newslabs-cdn/blob/master/js/20-bbcnpf.utils.js 4 | * In broadcast VIDEO, timecode is NOT hh:mm:ss:ms, it's hh:mm:ss:ff where ff is frames, 5 | * dependent on the framerate of the media concerned. 6 | * `hh:mm:ss:ff` 7 | */ 8 | 9 | /** 10 | * Helper function 11 | * Rounds to the 14milliseconds boundaries 12 | * Time in video can only "exist in" 14milliseconds boundaries. 13 | * This makes it possible for the HTML5 player to be frame accurate. 14 | * @param {*} seconds 15 | * @param {*} fps 16 | */ 17 | const normalisePlayerTime = function (seconds, fps) { 18 | return Number((1.0 / fps * Math.floor(Number((fps * seconds).toPrecision(12)))).toFixed(2)); 19 | }; 20 | 21 | /* 22 | * @param {*} seconds 23 | * @param {*} fps 24 | */ 25 | const secondsToTimecode = function (seconds, framePerSeconds) { 26 | // written for PAL non-drop timecode 27 | let fps = 25; 28 | if (framePerSeconds !== undefined) { 29 | fps = framePerSeconds; 30 | } 31 | 32 | const normalisedSeconds = normalisePlayerTime(seconds, fps); 33 | 34 | const wholeSeconds = Math.floor(normalisedSeconds); 35 | const frames = ((normalisedSeconds - wholeSeconds) * fps).toFixed(2); 36 | 37 | // prepends zero - example pads 3 to 03 38 | function _padZero(n) { 39 | if (n < 10) return `0${ parseInt(n) }`; 40 | 41 | return parseInt(n); 42 | } 43 | 44 | return `${ _padZero((wholeSeconds / 60 / 60) % 60) 45 | }:${ 46 | _padZero((wholeSeconds / 60) % 60) 47 | }:${ 48 | _padZero(wholeSeconds % 60) 49 | }:${ 50 | _padZero(frames) }`; 51 | }; 52 | 53 | export default secondsToTimecode; 54 | -------------------------------------------------------------------------------- /docs/adr/2018-11-20-local-storage-save.md: -------------------------------------------------------------------------------- 1 | # Local storage save 2 | 3 | * Status: being evaluated 4 | * Deciders: Pietro, James 5 | * Date: 2018-11-20 6 | 7 | 8 | ## Context and Problem Statement 9 | 10 | It be good to be able to save in local storage as the user is editing the text. 11 | Some things to define 12 | - Is this done inside the component? or does the component return the data and the parent component handles saving to local storage? 13 | - how to be able to save multiple transcript for the same editor? eg with a unique id? 14 | - perhaps adding time stamps to the saving for versioning? 15 | - When loading transcript from server, if it's in local storage should we resume from that one instead? 16 | - ignoring multi users scenarios for now. 17 | 18 | How often to save 19 | - save button? 20 | - save on each new char? 21 | - save on time interval? 22 | 23 | ## Decision Drivers 24 | 25 | * simple to reason around 26 | * [driver 2, e.g., a force, facing concern, …] 27 | * … 28 | 29 | ## Considered Options 30 | 31 | * inside component TimedTextEditor 32 | * outside React TranscriptEditor component 33 | * Debouncing on save (Lodash helper function) 34 | 35 | ## Decision Outcome 36 | 37 | - Save inside component TimedTextEditor 38 | - Save button for local storage 39 | - Save on each new char | switch to timer if it effects performance for users 40 | - Save on char / multiple of char inputs 41 | - Can use "save-on-char" together with time intervals 42 | - using url of media as id to save to local storage 43 | - add time stamps 44 | - When loading transcript from server, if it's in local storage should resume from that one instead 45 | 46 | 47 | [See separate ADR for saving to server.](./2018-11-20-save-to-server.md) 48 | -------------------------------------------------------------------------------- /packages/components/timed-text-editor/Word.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class Word extends Component { 5 | 6 | shouldComponentUpdate(nextProps) { 7 | if ( nextProps.decoratedText !== this.props.decoratedText) { 8 | return true; 9 | } 10 | 11 | return false; 12 | } 13 | 14 | generateConfidence = (data) => { 15 | // handling edge case where confidence score not present 16 | if (data.confidence) { 17 | return data.confidence > 0.6 ? 'high' : 'low'; 18 | } 19 | 20 | return 'high'; 21 | } 22 | 23 | generatePreviousTimes = (data) => { 24 | let prevTimes = ''; 25 | 26 | for (let i = 0; i < data.start; i++) { 27 | prevTimes += `${ i } `; 28 | } 29 | 30 | if (data.start % 1 > 0) { 31 | // Find the closest quarter-second to the current time, for more dynamic results 32 | const dec = Math.floor((data.start % 1) * 4.0) / 4.0; 33 | prevTimes += ` ${ Math.floor(data.start) + dec }`; 34 | } 35 | 36 | return prevTimes; 37 | } 38 | 39 | render() { 40 | const data = this.props.entityKey 41 | ? this.props.contentState.getEntity(this.props.entityKey).getData() 42 | : {}; 43 | 44 | return ( 45 | 52 | {this.props.children} 53 | 54 | ); 55 | } 56 | } 57 | 58 | Word.propTypes = { 59 | contentState: PropTypes.object, 60 | entityKey: PropTypes.string, 61 | children: PropTypes.array 62 | }; 63 | 64 | export default Word; 65 | -------------------------------------------------------------------------------- /packages/components/video-player/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styles from './index.module.css'; 4 | 5 | class VideoPlayer extends React.Component { 6 | 7 | // to avoid unnecessary re-renders 8 | shouldComponentUpdate(nextProps) { 9 | if (nextProps.previewIsDisplayed !== this.props.previewIsDisplayed) { 10 | return true; 11 | } 12 | 13 | if (nextProps.mediaUrl !== this.props.mediaUrl) { 14 | return true; 15 | } 16 | 17 | return false; 18 | } 19 | 20 | handlePlayMedia = () => { 21 | if (this.props.videoRef.current !== null) { 22 | return this.props.videoRef.current.paused 23 | ? this.props.videoRef.current.play() 24 | : this.props.videoRef.current.pause(); 25 | } 26 | }; 27 | render() { 28 | const isDisplayed = this.props.previewIsDisplayed ? 'inline' : 'none'; 29 | 30 | return ( 31 |