├── .nvmrc ├── .nojekyll ├── docs ├── .keep ├── QA │ ├── .keep │ ├── apple-script-testing.md │ └── README.md ├── guides │ ├── .keep │ ├── README.md │ ├── npm-tags.md │ ├── dpe-transcript-format.md │ └── features-list.md ├── notes │ ├── .keep │ ├── README.md │ ├── pause-while-typing.md │ ├── insert-text-at-selection.md │ ├── web-workers.md │ ├── insert-slate-functions.md │ ├── pause-while-typing-timer.md │ ├── OHMS.md │ ├── debounce.md │ ├── css-injection-karaoke.md │ ├── set-selection.md │ ├── deconstructing word timing computation.md │ ├── verbose-generate-previous-timings-up-to-current-func.md │ ├── draftjs-vs-slatejs.md │ ├── notes.md │ └── alternative-alignment-approaches.md ├── adr │ ├── README.md │ └── adr-template.md ├── README.md └── SUMMARY.md ├── .gitbook.yaml ├── .prettierignore ├── .prettierrc ├── src ├── components │ ├── slate-helpers │ │ ├── handle-split-paragraph │ │ │ ├── is-same-block.js │ │ │ ├── is-selection-collapsed.js │ │ │ ├── is-beginning-of-the-block.js │ │ │ ├── split-text-at-offset.js │ │ │ ├── split-words-list-at-offset.js │ │ │ ├── is-end-of-the-block.js │ │ │ ├── is-text-same-as-words-list.js │ │ │ └── index.js │ │ ├── break-paragraph │ │ │ └── index.js │ │ ├── insert-text │ │ │ └── index.js │ │ ├── README.md │ │ ├── set-node │ │ │ └── index.js │ │ ├── set-selection │ │ │ └── index.js │ │ ├── collapse-selection-to-a-single-point │ │ │ └── index.js │ │ ├── get-node-by-path │ │ │ └── index.js │ │ ├── merge-nodes │ │ │ └── index.js │ │ ├── remove-nodes │ │ │ └── index.js │ │ ├── get-closest-block │ │ │ └── index.js │ │ ├── split-nodes │ │ │ └── index.js │ │ ├── create-new-paragraph-block │ │ │ └── index.js │ │ ├── index.js │ │ ├── insert-nodes-at-selection │ │ │ └── index.js │ │ ├── get-selection-nodes │ │ │ └── index.js │ │ └── handle-delete-in-paragraph │ │ │ └── index.js │ ├── 3-SlateSimpleEditor.stories.js │ ├── 2-Longer.stories.js │ ├── 5-Saving.stories.js │ ├── 4-Live.stories.js │ ├── 6-CustomTheme.stories.js │ └── 1-SlateTranscriptEditor.stories.js ├── util │ ├── export-adapters │ │ ├── subtitles-generator │ │ │ ├── compose-subtitles │ │ │ │ ├── util │ │ │ │ │ ├── format-seconds.js │ │ │ │ │ ├── escape-text.js │ │ │ │ │ └── tc-format.js │ │ │ │ ├── srt.js │ │ │ │ ├── vtt.js │ │ │ │ ├── ttml.js │ │ │ │ ├── premiere.js │ │ │ │ ├── csv │ │ │ │ │ ├── index.test.js │ │ │ │ │ └── index.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 │ │ │ │ ├── text-segmentation │ │ │ │ │ ├── HONORIFICS.txt │ │ │ │ │ ├── index.js │ │ │ │ │ ├── index.test.js │ │ │ │ │ └── README.md │ │ │ │ ├── index.test.js │ │ │ │ ├── divide-into-two-lines │ │ │ │ │ ├── index.js │ │ │ │ │ ├── index.test.js │ │ │ │ │ └── README.md │ │ │ │ ├── fold │ │ │ │ │ ├── index.test.js │ │ │ │ │ ├── README.md │ │ │ │ │ └── index.js │ │ │ │ ├── index.js │ │ │ │ ├── README.md │ │ │ │ └── steps.md │ │ │ ├── list.js │ │ │ ├── example-usage.js │ │ │ ├── index.js │ │ │ └── sample │ │ │ │ └── test.sample.txt │ │ ├── txt │ │ │ └── index.js │ │ ├── index.js │ │ ├── slate-to-dpe │ │ │ ├── update-timestamps │ │ │ │ ├── plain-text-align-to-slate.js │ │ │ │ └── update-bloocks-timestamps.js │ │ │ └── index.js │ │ └── docx │ │ │ └── index.js │ ├── is-empty │ │ └── index.js │ ├── pluk │ │ ├── index.js │ │ └── README.md │ ├── convert-words-to-text │ │ └── index.js │ ├── get-media-type │ │ └── index.js │ ├── get-words-for-paragraph │ │ └── index.js │ ├── timecode-converter │ │ ├── src │ │ │ ├── timecodeToSeconds.test.js │ │ │ ├── secondsToTimecode.test.js │ │ │ ├── timecodeToSeconds.js │ │ │ ├── padTimeToTimecode.js │ │ │ ├── padTimeToTimecode.test.js │ │ │ └── secondsToTimecode.js │ │ ├── index.js │ │ └── index.test.js │ ├── downlaod │ │ └── index.js │ ├── count-words │ │ └── index.js │ ├── dpe-to-slate │ │ ├── generate-previous-timings-up-to-current │ │ │ └── index.js │ │ └── index.js │ └── insert-timecodes-in-line-in-words-list │ │ └── index.js ├── index.js └── sample-data │ └── segmented-transcript.js ├── .babelrc ├── .storybook └── main.js ├── .github ├── ISSUE_TEMPLATE │ ├── question.md │ ├── feature_request.md │ ├── bug_report.md │ ├── qa_individual_issue_report.md │ └── qa_report.md └── PULL_REQUEST_TEMPLATE.md ├── .npmignore ├── LICENCE.md ├── .gitignore ├── package.json ├── CONTRIBUTING.md └── CODE_OF_CONDUCT.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 12 -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/QA/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/guides/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/notes/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitbook.yaml: -------------------------------------------------------------------------------- 1 | root: ./docs/ -------------------------------------------------------------------------------- /docs/adr/README.md: -------------------------------------------------------------------------------- 1 | # ADR 2 | 3 | -------------------------------------------------------------------------------- /docs/notes/README.md: -------------------------------------------------------------------------------- 1 | # notes 2 | 3 | -------------------------------------------------------------------------------- /docs/guides/README.md: -------------------------------------------------------------------------------- 1 | # Guides 2 | 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | *.json 3 | .out/ 4 | dist/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "printWidth": 150, 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": false 8 | } -------------------------------------------------------------------------------- /docs/notes/pause-while-typing.md: -------------------------------------------------------------------------------- 1 | # pause-while-typing 2 | 3 | [Wait for User to Stop Typing, in JavaScript](https://schier.co/blog/wait-for-user-to-stop-typing-using-javascript) 4 | 5 | -------------------------------------------------------------------------------- /src/components/slate-helpers/handle-split-paragraph/is-same-block.js: -------------------------------------------------------------------------------- 1 | function isSameBlock(anchorPath, focusPath) { 2 | return anchorPath[0] === focusPath[0]; 3 | } 4 | export default isSameBlock; 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["transform-react-jsx"], 3 | "ignore": [ 4 | "src/components/*.storiesjs", 5 | "src/components/sample-data/**" 6 | ], 7 | "presets":["@babel/preset-react"] 8 | } -------------------------------------------------------------------------------- /src/components/slate-helpers/break-paragraph/index.js: -------------------------------------------------------------------------------- 1 | import { Editor } from 'slate'; 2 | 3 | const breakParagraph = (editor) => { 4 | Editor.insertBreak(editor); 5 | }; 6 | 7 | export default breakParagraph; 8 | -------------------------------------------------------------------------------- /src/util/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; 4 | -------------------------------------------------------------------------------- /src/components/slate-helpers/handle-split-paragraph/is-selection-collapsed.js: -------------------------------------------------------------------------------- 1 | function isSelectionCollapsed(anchorOffset, focusOffset) { 2 | return anchorOffset === focusOffset; 3 | } 4 | export default isSelectionCollapsed; 5 | -------------------------------------------------------------------------------- /src/util/is-empty/index.js: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/questions/679915/how-do-i-test-for-an-empty-javascript-object 2 | function isEmpty(obj) { 3 | return Object.keys(obj).length === 0; 4 | } 5 | 6 | export default isEmpty; 7 | -------------------------------------------------------------------------------- /src/components/slate-helpers/insert-text/index.js: -------------------------------------------------------------------------------- 1 | import { Transforms } from 'slate'; 2 | const insertText = ({ editor, text = '[INAUDIBLE]' }) => { 3 | Transforms.insertText(editor, text); 4 | }; 5 | 6 | export default insertText; 7 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../src/components/**/*.stories.js'], 3 | addons: ['@storybook/addon-actions', '@storybook/addon-links', '@storybook/addon-knobs/register', '@storybook/addon-storysource'], 4 | }; 5 | -------------------------------------------------------------------------------- /docs/notes/insert-text-at-selection.md: -------------------------------------------------------------------------------- 1 | # insert-text-at-selection 2 | 3 | ```javascript 4 | Transforms.insertText(editor, 'res'); 5 | ``` 6 | 7 | [https://github.com/pietrop/slate-snippets](https://github.com/pietrop/slate-snippets) 8 | 9 | -------------------------------------------------------------------------------- /src/components/slate-helpers/handle-split-paragraph/is-beginning-of-the-block.js: -------------------------------------------------------------------------------- 1 | function isBeginningOftheBlock(anchorOffset, focusOffset) { 2 | return anchorOffset === 0 && focusOffset === 0; 3 | } 4 | 5 | export default isBeginningOftheBlock; 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question about this project 4 | title: '' 5 | labels: bug 6 | assignees: 7 | 8 | --- 9 | 10 | -------------------------------------------------------------------------------- /src/util/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; 8 | -------------------------------------------------------------------------------- /src/util/pluk/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Pluck Unique Values from Array of Javascript Objects 3 | * https://gist.github.com/JamieMason/bed71c73576ba8d70a4671ea91b6178e 4 | */ 5 | const pluck = key => array => Array.from(new Set(array.map(obj => obj[key]))); 6 | 7 | export default pluck; 8 | -------------------------------------------------------------------------------- /docs/notes/web-workers.md: -------------------------------------------------------------------------------- 1 | # web workers 2 | 3 | * [Parallel programming in JavaScript using Web Workers](https://itnext.io/achieving-parallelism-in-javascript-using-web-workers-8f921f2d26db) 4 | * [Electron Documentation - Multithreading - Web Workers](https://www.electronjs.org/docs/tutorial/multithreading) 5 | 6 | -------------------------------------------------------------------------------- /docs/notes/insert-slate-functions.md: -------------------------------------------------------------------------------- 1 | ```js 2 | const breakParagraph = () => { 3 | Editor.insertBreak(editor); 4 | }; 5 | const insertTextInaudible = () => { 6 | Transforms.insertText(editor, '[INAUDIBLE]'); 7 | }; 8 | 9 | const handleInsertMusicNote = () => { 10 | Transforms.insertText(editor, '♫'); // or ♪ 11 | }; 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/notes/pause-while-typing-timer.md: -------------------------------------------------------------------------------- 1 | ```js 2 | if (saveTimer !== null) { 3 | clearTimeout(saveTimer); 4 | } 5 | const tmpSaveTimer = setTimeout(() => { 6 | if (mediaRef && mediaRef.current) { 7 | mediaRef.current.play(); 8 | } 9 | }, PAUSE_WHILTE_TYPING_TIMEOUT_MILLISECONDS); 10 | setSaveTimer(tmpSaveTimer); 11 | ``` 12 | -------------------------------------------------------------------------------- /src/util/export-adapters/subtitles-generator/compose-subtitles/util/escape-text.js: -------------------------------------------------------------------------------- 1 | const AMP_REGEX = /&/g; 2 | const LT_REGEX = //g; 4 | const escapeText = str => 5 | str 6 | .replace(AMP_REGEX, '&') 7 | .replace(LT_REGEX, '<') 8 | .replace(GT_REGEX, '>'); 9 | 10 | export default escapeText; 11 | -------------------------------------------------------------------------------- /src/util/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; 11 | -------------------------------------------------------------------------------- /src/components/slate-helpers/README.md: -------------------------------------------------------------------------------- 1 | Helpers are modules specifically for slateJs. 2 | 3 | sSome are simple wrapper around slateJs utilities, to keep some flexibility if the slateJS interface where to change in future versions. 4 | 5 | Others are slightly more complex operations. Specific of the timed text domain. 6 | 7 | More generic modules live under `src/utils` folder. 8 | -------------------------------------------------------------------------------- /src/util/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; 10 | -------------------------------------------------------------------------------- /src/components/slate-helpers/set-node/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | https://github.com/dylans/slate-snippets#set-node 3 | 4 | Transforms.setNodes(editor, { type: 'paragraph' }, { at: path }); 5 | */ 6 | 7 | import { Transforms } from 'slate'; 8 | 9 | function setNode({ editor, block, path }) { 10 | Transforms.setNodes(editor, block, { at: path }); 11 | } 12 | 13 | export default setNode; 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | demo 3 | lib 4 | packages 5 | build 6 | .babelrc 7 | .babel.config.js 8 | webpack.config.js 9 | 10 | CONTRIBUTING.md 11 | CODE_OF_CONDUCT.md 12 | .github/ 13 | docs/ 14 | src/sample-data/ 15 | .out/ 16 | .gitbook.yaml 17 | .nvmrc 18 | .nojekyll 19 | .storybook/main.js 20 | *.sample.json 21 | *.sample.xml 22 | *.sample.txt 23 | *.stories.js 24 | *.test.js -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Slate Transcript Editor - Docs 2 | 3 | _work in progress_ 4 | 5 | Docs for [pietrop/slate-transcript-editor](https://github.com/pietrop/slate-transcript-editor) 6 | 7 | - [github repo](https://github.com/pietrop/slate-transcript-editor) 8 | - [storybook](https://pietropassarelli.com/slate-transcript-editor) 9 | - [gitbook](https://autoedit.gitbook.io/slate-transcript-editor-docs/) 10 | -------------------------------------------------------------------------------- /src/util/convert-words-to-text/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper function 3 | * @param {array} words - dpe word objeect, with at list text attribute to be able to convert to string of text 4 | */ 5 | function convertWordsToText(words) { 6 | return words 7 | .map((word) => { 8 | return word.text ? word.text.trim() : ''; 9 | }) 10 | .join(' '); 11 | } 12 | 13 | export default convertWordsToText; 14 | -------------------------------------------------------------------------------- /src/components/slate-helpers/set-selection/index.js: -------------------------------------------------------------------------------- 1 | // https://docs.slatejs.org/api/transforms#transforms-setselection-editor-editor-props-partial-less-than-range-greater-than 2 | // Set new properties on the selection. 3 | import { Transforms } from 'slate'; 4 | function setSelection({ editor, nextPoint }) { 5 | Transforms.setSelection(editor, { anchor: nextPoint, focus: nextPoint }); 6 | } 7 | export default setSelection; 8 | -------------------------------------------------------------------------------- /src/util/get-media-type/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | const getMediaType = (mediaUrl) => { 4 | const clipExt = path.extname(mediaUrl); 5 | let tmpMediaType = 'video'; 6 | if (clipExt === '.wav' || clipExt === '.mp3' || clipExt === '.m4a' || clipExt === '.flac' || clipExt === '.aiff') { 7 | tmpMediaType = 'audio'; 8 | } 9 | return tmpMediaType; 10 | }; 11 | 12 | export default getMediaType; 13 | -------------------------------------------------------------------------------- /src/components/slate-helpers/handle-split-paragraph/split-text-at-offset.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param {string} text -text string 4 | * @param {number} offset - offset char number position/index 5 | */ 6 | function splitTextAtOffset(text, offset) { 7 | const textBefore = text.slice(0, offset); 8 | const textAfter = text.slice(offset); 9 | return [textBefore, textAfter]; 10 | } 11 | 12 | export default splitTextAtOffset; 13 | -------------------------------------------------------------------------------- /src/components/slate-helpers/collapse-selection-to-a-single-point/index.js: -------------------------------------------------------------------------------- 1 | // https://docs.slatejs.org/api/transforms#transforms-collapse-editor-editor-options 2 | // Collapse the selection to a single point. 3 | // Options: {edge?: 'anchor' | 'focus' | 'start' | 'end'} 4 | 5 | import { Transforms } from 'slate'; 6 | function collapseSelectionToAsinglePoint(editor) { 7 | Transforms.collapse(editor, { edge: 'start' }); 8 | } 9 | 10 | export default collapseSelectionToAsinglePoint; 11 | -------------------------------------------------------------------------------- /src/components/slate-helpers/handle-split-paragraph/split-words-list-at-offset.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param {string} text -text string 4 | * @param {number} offset - offset char number position/index 5 | */ 6 | function splitWordsListAtOffset(words, offset) { 7 | const tmpWords = JSON.parse(JSON.stringify(words)); 8 | const wordsAfter = tmpWords.splice(offset); 9 | const wordsBefore = tmpWords; 10 | return [wordsBefore, wordsAfter]; 11 | } 12 | 13 | export default splitWordsListAtOffset; 14 | -------------------------------------------------------------------------------- /src/util/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( 6 | '.', 7 | ',' 8 | )}\n${v.text.trim()}\n\n`; 9 | }); 10 | 11 | return srtOut; 12 | }; 13 | 14 | export default srtGenerator; 15 | -------------------------------------------------------------------------------- /src/components/slate-helpers/get-node-by-path/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get node by path 3 | * https://github.com/dylans/slate-snippets#get-node-by-path 4 | * Get the descendant node referred to by a specific path. If the path is an empty array, get the root node itself. 5 | * https://docs.slatejs.org/api/nodes 6 | */ 7 | import { Node } from 'slate'; 8 | function getNodebyPath({ editor, path }) { 9 | const node = Node.get(editor, path); 10 | return node; 11 | } 12 | 13 | export default getNodebyPath; 14 | -------------------------------------------------------------------------------- /src/util/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 => { 7 | return r.replace(/^\s+/g, ''); 8 | }); 9 | } 10 | 11 | export default removeSpaceAtBeginningOfLine; 12 | -------------------------------------------------------------------------------- /src/util/export-adapters/subtitles-generator/compose-subtitles/vtt.js: -------------------------------------------------------------------------------- 1 | import formatSeconds from './util/format-seconds.js'; 2 | 3 | const vttGenerator = (vttJSON, speakers = false) => { 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${speakers ? `` : ``}${ 7 | v.text 8 | }\n\n`; 9 | }); 10 | 11 | return vttOut; 12 | }; 13 | 14 | export default vttGenerator; 15 | -------------------------------------------------------------------------------- /src/components/slate-helpers/merge-nodes/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | https://docs.slatejs.org/api/transforms#transforms-mergenodes-editor-editor-options 3 | Merge a node at the specified location with the previous node at the same depth. If no location is specified, use the selection. Resulting empty container nodes are removed. 4 | Options supported: NodeOptions & {hanging?: boolean} 5 | */ 6 | import { Transforms } from 'slate'; 7 | 8 | function mergeNodes({ editor, options = {} }) { 9 | Transforms.mergeNodes(editor, options); 10 | } 11 | export default mergeNodes; 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import SlateTranscriptEditor from './components/index.js'; 2 | import { secondsToTimecode, timecodeToSeconds, shortTimecode } from './util/timecode-converter/index.js'; 3 | import convertDpeToSlate from './util/dpe-to-slate/index.js'; 4 | import converSlateToDpe from './util/export-adapters/slate-to-dpe/index.js'; 5 | import slateToText from './util/export-adapters/txt'; 6 | 7 | export default SlateTranscriptEditor; 8 | 9 | export { SlateTranscriptEditor, secondsToTimecode, timecodeToSeconds, shortTimecode, convertDpeToSlate, converSlateToDpe, slateToText }; 10 | -------------------------------------------------------------------------------- /src/components/slate-helpers/remove-nodes/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | https://docs.slatejs.org/api/transforms#transforms-removenodes-editor-editor-options 3 | 4 | Transforms.removeNodes(editor: Editor, options?) 5 | Remove nodes at the specified location in the document. If no location is specified, remove the nodes in the selection. 6 | Options supported: NodeOptions & {hanging?: boolean} 7 | 8 | */ 9 | import { Transforms } from 'slate'; 10 | 11 | function removeNodes({ editor, options = {} }) { 12 | Transforms.removeNodes(editor, options); 13 | } 14 | export default removeNodes; 15 | -------------------------------------------------------------------------------- /src/util/get-words-for-paragraph/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param {*} currentParagraph a dpe paragraph object, with start, and end attribute eg in seconds 4 | * @param {*} words a list of word objects with start and end attributes 5 | * @returns a lsit of words obejcts that are included in the given paragraphs 6 | */ 7 | const getWordsForParagraph = (currentParagraph, words) => { 8 | const { start, end } = currentParagraph; 9 | return words.filter((word) => { 10 | return word.start >= start && word.end <= end; 11 | }); 12 | }; 13 | 14 | export default getWordsForParagraph; 15 | -------------------------------------------------------------------------------- /docs/notes/OHMS.md: -------------------------------------------------------------------------------- 1 | OHMS 2 | 3 | OHMS is an open source indexing tool created by the University of Kentucky, which is used by a number of cultural heritage institutions 4 | 5 | https://www.oralhistoryonline.org/ 6 | 7 | Example 8 | 9 | https://kentuckyoralhistory.org/ark:/16417/xt71d837kj8dp 10 | you have to toggle to “Play Interview” 11 | 12 | it uses xml for the the index and a Word doc for the transcript (if a transcript exists) with timecodes at 30 second or 60 second intervals written in-line in the format of [hh:mm:ss] 13 | 14 | `slate-transcript-editor` OHMS export option exports the word part. 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Is your Pull Request request related to another issue 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 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/components/slate-helpers/get-closest-block/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | from ~https://github.com/dylans/slate-snippets#get-closest-block~ 3 | from https://github.com/ianstormtaylor/slate/blob/228f4fa94f61f42ca41feae2b3029ebb570e0480/packages/slate/src/transforms/text.ts#L108-L112 4 | const startBlock = Editor.above(editor, { 5 | match: (n) => Editor.isBlock(editor, n), 6 | at: start, 7 | voids, 8 | }); 9 | return startBlock; 10 | */ 11 | import { Editor } from 'slate'; 12 | 13 | function getClosestBlock(editor) { 14 | const [blockNode, path] = Editor.above(editor, { match: (n) => Editor.isBlock(editor, n) }); 15 | return [blockNode, path]; 16 | } 17 | export default getClosestBlock; 18 | -------------------------------------------------------------------------------- /src/components/slate-helpers/handle-split-paragraph/is-end-of-the-block.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This helper function checks if the cursor/caret is at the end of a line 3 | * by comparing the anchros offset with the focus offset and seeing if they are equal to the total number 4 | * of chars in that block 5 | * 6 | * There seems to be an alternative way of doing this that could also be exploreed 7 | * https://github.com/udecode/slate-plugins/blob/master/packages/slate-plugins/src/common/queries/isSelectionAtBlockEnd.ts 8 | */ 9 | function isEndOftheBlock({ anchorOffset, focusOffset, totlaChar }) { 10 | return anchorOffset === focusOffset && anchorOffset === totlaChar; 11 | } 12 | 13 | export default isEndOftheBlock; 14 | -------------------------------------------------------------------------------- /docs/QA/apple-script-testing.md: -------------------------------------------------------------------------------- 1 | # Apple script testing 2 | 3 | Script to use with [apple script](https://en.wikipedia.org/wiki/AppleScript) to test and simulate correcting the text in the editor over extended period of time. 4 | 5 | ```js 6 | delay 2 7 | repeat 3000 times 8 | repeat 30 times 9 | tell application "System Events" to keystroke "SOME TEXT " 10 | delay 3 11 | end repeat 12 | delay 6 13 | tell application "System Events" to keystroke (ASCII character 31) --down arrow 14 | tell application "System Events" to keystroke (ASCII character 31) --down arrow 15 | tell application "System Events" to keystroke (ASCII character 31) --down arrow 16 | end repeat 17 | ``` 18 | 19 | -------------------------------------------------------------------------------- /src/util/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( 12 | /\n/g, 13 | '
' 14 | )}

\n`; 15 | }); 16 | ttmlOut += '
\n\n
'; 17 | 18 | return ttmlOut; 19 | }; 20 | 21 | export default ttmlGenerator; 22 | -------------------------------------------------------------------------------- /docs/QA/README.md: -------------------------------------------------------------------------------- 1 | # QA Report 2 | 3 | To run QA, raise a QA issue to document the process as a [QA Report issue](https://github.com/pietrop/slate-transcript-editor/issues/new?assignees=&labels=QA%20Report&template=qa_report.md&title=[QA]%20Main%20check%20list), it will have the checklist below and you can run through it and check each item as you follow the steps. 4 | 5 | If you run into issues with any of the individual items, raise a separate issue for each as a [QA Report - individual issue](https://github.com/pietrop/slate-transcript-editor/issues/new?assignees=&labels=QA%20Issue&template=qa_individual_issue_report.md&title=[QA]%20Issue%20#1.1%20Can%20edit%20the%0text). Write a note of the item numnber, and "title" in the issue title and description 6 | -------------------------------------------------------------------------------- /.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: 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 | -------------------------------------------------------------------------------- /docs/guides/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 0.1.2-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 slate-transcript-editor@alpha 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/notes/debounce.md: -------------------------------------------------------------------------------- 1 | # notes on debounce 2 | 3 | This worked, to do auto/align when the user stops typing. It only calls it once. 4 | 5 | Outside of the component 6 | 7 | ```js 8 | import pDebounce from 'p-debounce'; 9 | ... 10 | const debouncedSave = pDebounce(updateBloocksTimestamps, 3000); 11 | ``` 12 | 13 | inside the component keydown 14 | 15 | ```js 16 | const handleOnKeyDown = async (event) => { 17 | ... 18 | // value is the content of slateJS 19 | const alignedSlateData = await debouncedSave(value); 20 | setValue(alignedSlateData); 21 | setIsContentIsModified(false); 22 | ``` 23 | 24 | seems like having it inside the component was being effected by the components re-renders. 25 | 26 | This could be used for pause while typing as well. 27 | -------------------------------------------------------------------------------- /src/util/downlaod/index.js: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/questions/2897619/using-html5-javascript-to-generate-and-save-a-file 2 | const download = (content, filename, contentType) => { 3 | const type = contentType || 'application/octet-stream'; 4 | const link = document.createElement('a'); 5 | const blob = new Blob([content], { type: type }); 6 | 7 | link.href = window.URL.createObjectURL(blob); 8 | link.download = filename; 9 | // Firefox fix - cannot do link.click() if it's not attached to DOM in firefox 10 | // https://stackoverflow.com/questions/32225904/programmatical-click-on-a-tag-not-working-in-firefox 11 | document.body.appendChild(link); 12 | link.click(); 13 | document.body.removeChild(link); 14 | }; 15 | 16 | export default download; 17 | -------------------------------------------------------------------------------- /src/util/export-adapters/subtitles-generator/list.js: -------------------------------------------------------------------------------- 1 | const subtitlesExportOptionsList = [ 2 | { type: 'srt', label: 'Srt', ext: 'srt' }, 3 | { type: 'vtt', label: 'VTT', ext: 'vtt' }, 4 | { type: 'vtt_speakers', label: 'VTT with speakers', ext: 'vtt' }, 5 | { type: 'vtt_speakers_paragraphs', label: 'VTT with speakers and paragraphs', ext: 'vtt' }, 6 | { type: 'itt', label: 'iTT', ext: 'itt' }, 7 | { type: 'ttml', label: 'TTML', ext: 'ttml' }, 8 | { type: 'premiereTTML', label: 'TTML for Adobe Premiere', ext: 'ttml' }, 9 | { type: 'csv', label: 'CSV', ext: 'csv' }, 10 | { type: 'pre-segment-txt', label: 'Pre segmented txt', ext: 'txt' }, 11 | { type: 'json', label: 'Json', ext: 'json' }, 12 | ]; 13 | 14 | export default subtitlesExportOptionsList; 15 | -------------------------------------------------------------------------------- /src/util/count-words/index.js: -------------------------------------------------------------------------------- 1 | export const removeExtraWhiteSpaces = (text) => { 2 | return text.trim().replace(/\s\s+/g, ' '); 3 | }; 4 | 5 | export const splitOnWhiteSpaces = (text) => { 6 | return removeExtraWhiteSpaces(text).split(' '); 7 | }; 8 | 9 | export const countChar = (text) => { 10 | // remove white spaces and count chat 11 | return splitOnWhiteSpaces(text).join('').length; 12 | }; 13 | 14 | const countWords = (text) => { 15 | // return text.trim().replace(/\n /g, '').replace(/\n/g, ' ').split(' ').length; 16 | // Don't count multiple spaces as multiple words 17 | // https://www.w3schools.com/jsref/jsref_regexp_whitespace.asp 18 | // Do a global search for whitespace characters in a string 19 | return splitOnWhiteSpaces(text).length; 20 | }; 21 | 22 | export default countWords; 23 | -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | 3 | * [Slate Transcript Editor - Docs](README.md) 4 | * [notes](notes/README.md) 5 | * [Apple script testing](notes/apple-script-testing.md) 6 | * [css-injection-karaoke](notes/css-injection-karaoke.md) 7 | * [insert-text-at-selection](notes/insert-text-at-selection.md) 8 | * [set-selection](notes/set-selection.md) 9 | * [notes](notes/notes.md) 10 | * [web workers](notes/web-workers.md) 11 | * [Takeaways form draftJs vs Slate](notes/draftjs-vs-slatejs.md) 12 | * [pause-while-typing](notes/pause-while-typing.md) 13 | * [verbose-generate-previous-timings-up-to-current-func](notes/verbose-generate-previous-timings-up-to-current-func.md) 14 | * [ADR](adr/README.md) 15 | * [\[short title of solved problem and solution\] - ADR Template](adr/adr-template.md) 16 | 17 | -------------------------------------------------------------------------------- /docs/notes/css-injection-karaoke.md: -------------------------------------------------------------------------------- 1 | # css-injection-karaoke 2 | 3 | ```jsx 4 | 25 | ``` 26 | 27 | -------------------------------------------------------------------------------- /src/util/pluk/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Pluck Unique Values from Array of Javascript Objects 3 | from [JamieMason/pluck-unique-values-from-array-of-javascript-objects.md](https://gist.github.com/JamieMason/bed71c73576ba8d70a4671ea91b6178e) 4 | ## Implementation 5 | 6 | ```js 7 | const pluck = key => array => Array.from(new Set(array.map(obj => obj[key]))); 8 | ``` 9 | 10 | ## Usage 11 | 12 | ```js 13 | const cars = [ 14 | { brand: 'Audi', color: 'black' }, 15 | { brand: 'Audi', color: 'white' }, 16 | { brand: 'Ferarri', color: 'red' }, 17 | { brand: 'Ford', color: 'white' }, 18 | { brand: 'Peugot', color: 'white' } 19 | ]; 20 | 21 | const getBrands = pluck('brand'); 22 | 23 | console.log(getBrands(cars)); 24 | ``` 25 | 26 | ### Output 27 | 28 | ```json 29 | [ 30 | "Audi", 31 | "Ferarri", 32 | "Ford", 33 | "Peugot" 34 | ] 35 | ``` -------------------------------------------------------------------------------- /src/util/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/notes/set-selection.md: -------------------------------------------------------------------------------- 1 | # set-selection 2 | 3 | [https://docs.slatejs.org/concepts/05-operations](https://docs.slatejs.org/concepts/05-operations) 4 | 5 | ```javascript 6 | editor.apply({ 7 | type: 'set_selection', 8 | properties: { 9 | anchor: { path: [0, 0], offset: 0 }, 10 | }, 11 | newProperties: { 12 | anchor: { path: [0, 0], offset: 15 }, 13 | }, 14 | }) 15 | ``` 16 | 17 | break on selection 18 | 19 | ```javascript 20 | Editor.insertBreak(editor) 21 | ``` 22 | 23 | select whole editor for range, from [Slate slack](https://slate-js.slack.com/archives/C1RH7AXSS/p1581298796206700?thread_ts=1581290922.206500&cid=C1RH7AXSS) 24 | 25 | ```javascript 26 | Editor.range(editor, []) 27 | ``` 28 | 29 | ```js 30 | const wholeTranscriptSelection = Editor.range(editor, []); 31 | Transforms.setSelection(editor, wholeTranscriptSelection) 32 | ``` 33 | -------------------------------------------------------------------------------- /src/util/dpe-to-slate/generate-previous-timings-up-to-current/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * See explanation in `src/utils/dpe-to-slate/index.js` for how this function works with css injection 3 | * to provide current paragaph's highlight. 4 | */ 5 | 6 | /** 7 | * Generate a list of times, each rounded up to int. 8 | * from zero to the provided `time`. 9 | * eg if `time` is 6, the list would be [0, 1, 2, 3, 4, 5] 10 | * @param {Number} time - float or int, time in seconds 11 | */ 12 | 13 | function generatePreviousTimingsUpToCurrent(start) { 14 | const startTimeInt = parseInt(start); 15 | if (start === 0) { 16 | return ''; 17 | } 18 | if (start === 1) { 19 | return '0 1'; 20 | } 21 | return new Array(startTimeInt) 22 | .fill(1) 23 | .map((_, i) => i + 1) 24 | .join(' '); 25 | } 26 | 27 | export default generatePreviousTimingsUpToCurrent; 28 | -------------------------------------------------------------------------------- /src/components/slate-helpers/split-nodes/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | https://github.com/ianstormtaylor/slate/blob/b5859b7e2ef97cdc5d5aaa675b807c4783b2e83c/packages/slate/src/transforms/node.ts#L584-L595 4 | 5 | const splitMode = mode === 'lowest' ? 'lowest' : 'highest' 6 | 7 | Transforms.splitNodes(editor, { 8 | at: end, 9 | match, 10 | mode: splitMode, 11 | voids, 12 | }) 13 | 14 | https://docs.slatejs.org/api/transforms#transforms-splitnodes-editor-editor-options 15 | 16 | Split nodes at the specified location. If no location is specified, split the selection. 17 | Options supported: NodeOptions & {height?: number, always?: boolean} 18 | 19 | Transforms.splitNodes(editor: Editor, options?) 20 | */ 21 | 22 | import { Transforms } from 'slate'; 23 | function splitNotdes(editor, options = {}) { 24 | Transforms.splitNodes(editor, options); 25 | } 26 | 27 | export default splitNotdes; 28 | -------------------------------------------------------------------------------- /src/util/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 | -------------------------------------------------------------------------------- /src/sample-data/segmented-transcript.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const DEMO_SOLEIO = require('../sample-data/soleio-dpe.json'); 3 | /** 4 | * 5 | * helper funciton to simulate data structure for live 6 | */ 7 | function findWordsRangeForQuoteInTranscript({ paragraph, words }) { 8 | const paragraphStart = paragraph.start; 9 | const paragraphEnd = paragraph.end; 10 | const wordResults = words.filter(word => { 11 | return word.start >= paragraphStart && word.end <= paragraphEnd; 12 | }); 13 | return wordResults; 14 | } 15 | 16 | function segmentedTranscript(transcript) { 17 | return transcript.paragraphs.map(paragraph => { 18 | const wordsResult = findWordsRangeForQuoteInTranscript({ paragraph, words: transcript.words }); 19 | return { words: wordsResult, paragraphs: [paragraph] }; 20 | }); 21 | } 22 | 23 | const result = segmentedTranscript(DEMO_SOLEIO); 24 | fs.writeFileSync('./src/sample-data/segmented-transcript-soleio-dpe.json', JSON.stringify(result, null, 2)); 25 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/components/3-SlateSimpleEditor.stories.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useMemo } from 'react'; 2 | import { createEditor } from 'slate'; 3 | // https://docs.slatejs.org/walkthroughs/01-installing-slate 4 | // Import the Slate components and React plugin. 5 | import { Slate, Editable, withReact } from 'slate-react'; 6 | 7 | export default { 8 | title: 'SlateSimpleEditor', 9 | component: SlateSimpleEditor, 10 | }; 11 | 12 | const SlateSimpleEditor = () => { 13 | const editor = useMemo(() => withReact(createEditor()), []); 14 | // Add the initial value when setting up our state. 15 | const [value, setValue] = useState([ 16 | { 17 | type: 'paragraph', 18 | children: [{ text: 'A line of text in a paragraph.' }], 19 | }, 20 | ]); 21 | 22 | return ( 23 | setValue(value)}> 24 | 25 | 26 | ); 27 | }; 28 | 29 | export const SlateSimpleDemo = () => ; 30 | -------------------------------------------------------------------------------- /src/util/export-adapters/subtitles-generator/presegment-text/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import fs from 'fs'; 3 | import preSegmentText from './index.js'; 4 | // requrie on js and json is relative to current file path 5 | import { words as sampleWords } from '../sample/words-list.sample.json'; 6 | // fs path is relative to where the node process start 7 | const sampleSegmentedOutput = fs.readFileSync('./packages/export-adapters/subtitles-generator/sample/test-presegment.sample.txt').toString(); 8 | 9 | const numberOfCharPerLine35 = 35; 10 | // TODO: not sure why Jest is having issues running this test 11 | describe.skip('presegment text', () => { 12 | test('presegment text ', () => { 13 | const result = preSegmentText(sampleWords); 14 | expect(result).toEqual(sampleSegmentedOutput); 15 | }); 16 | 17 | test('presegment text - 35', () => { 18 | const result = preSegmentText(sampleWords, numberOfCharPerLine35); 19 | expect(result).toEqual(sampleSegmentedOutput); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/components/slate-helpers/create-new-paragraph-block/index.js: -------------------------------------------------------------------------------- 1 | import { shortTimecode } from '../../../util/timecode-converter'; //'../../../timecode-converter'; 2 | import generatePreviousTimingsUpToCurrent from '../../../util/dpe-to-slate/generate-previous-timings-up-to-current'; 3 | 4 | function createNewParagraphBlock({ speaker, start, text = '', words = [], previousTimings, startTimecode }) { 5 | let newPreviousTimings = previousTimings; 6 | if (!newPreviousTimings) { 7 | newPreviousTimings = generatePreviousTimingsUpToCurrent(start); 8 | } 9 | let newStartTimecode = startTimecode; 10 | if (!newStartTimecode) { 11 | newStartTimecode = shortTimecode(start); 12 | } 13 | return { 14 | speaker, 15 | start, 16 | previousTimings: newPreviousTimings, 17 | startTimecode: newStartTimecode, 18 | type: 'timedText', 19 | children: [ 20 | { 21 | text, 22 | words, 23 | }, 24 | ], 25 | }; 26 | } 27 | 28 | export default createNewParagraphBlock; 29 | -------------------------------------------------------------------------------- /src/util/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; 38 | -------------------------------------------------------------------------------- /.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: 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 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Pietro Passarelli 2020 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /docs/notes/deconstructing word timing computation.md: -------------------------------------------------------------------------------- 1 | ```js 2 | (startTime * (nodeWords.length - idx) + endTime * idx) / nodeWords.length, 3 | ``` 4 | 5 | ## first word 6 | 7 | ``` 8 | start = 1.2 9 | endTime = 3.5 10 | nodeWords.length = 10 11 | idx = 0 12 | ``` 13 | 14 | ```js 15 | ( 1.2 * ( 10 - 0 ) + 3.5 * 0 ) / 10 16 | (startTime * (nodeWords.length - idx) + endTime * idx) / nodeWords.length, 17 | ``` 18 | 19 | ``` 20 | (1.2*(10-0)+3.5*0)/10 = 1.2 21 | ``` 22 | 23 | ## second word 24 | 25 | ``` 26 | start = 1.2 27 | endTime = 3.5 28 | nodeWords.length = 10 29 | idx = 1 30 | ``` 31 | 32 | ```js 33 | ( 1.2 * ( 10 - 1 ) + 3.5 * 1 ) / 10 34 | (startTime * (nodeWords.length - idx) + endTime * idx) / nodeWords.length, 35 | ``` 36 | 37 | ## third word 38 | 39 | ``` 40 | start = 1.2 41 | endTime = 3.5 42 | nodeWords.length = 10 43 | idx = 2 44 | ``` 45 | 46 | ```js 47 | ( 1.2 * ( 10 - 2 ) + 3.5 * 2 ) / 10 48 | (startTime * (nodeWords.length - idx) + endTime * idx) / nodeWords.length, 49 | ``` 50 | 51 | ``` 52 | (1.2*(10-2)+3.5*2)/10 53 | (1.2*(8)+3.5*2)/10 54 | (1.2*(8)+7)/10 55 | (9.6+7)/10 56 | 16.6/10 57 | 1.6600000000000001 58 | ``` 59 | -------------------------------------------------------------------------------- /src/util/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 | 21 | vttJSON.forEach((v) => { 22 | ttmlOut += `

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

\n`; 26 | }); 27 | ttmlOut += '
\n\n
'; 28 | 29 | return `${ttmlOut}`; 30 | }; 31 | 32 | export default ttmlGeneratorPremiere; 33 | -------------------------------------------------------------------------------- /src/components/slate-helpers/handle-split-paragraph/is-text-same-as-words-list.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper function to tell if caret/cursor is in the middle of a word 3 | * helper function for handle split paragraph 4 | * @param {string} textBefore - string text 5 | * @param {array} wordsBefore - list of words object 6 | */ 7 | // import SlateTextEditor from '../index'; 8 | 9 | function isTextSameAsWordsList(textBefore, wordsBefore) { 10 | // convert them to the same format, for comparison 11 | // convert the text list string to an array strings (words text) 12 | const textBeforeList = textBefore.trim().replace(/\s\s+/g, ' ').split(' '); 13 | // convert the array of words object, to an array of strings (words text) 14 | const wordsBeforeList = wordsBefore.map((w) => { 15 | return w.text; 16 | }); 17 | // get last word from text list 18 | const lastTextWord = textBeforeList[textBeforeList.length - 1]; 19 | // get last word from word list 20 | const lastWord = wordsBeforeList[wordsBeforeList.length - 1]; 21 | // if they are not the same then the cursor is in the middle of a word 22 | // because `lastTextWord` would be chopped 23 | 24 | const result = !(lastTextWord.trim() === lastWord.trim()); 25 | 26 | return result; 27 | } 28 | 29 | export default isTextSameAsWordsList; 30 | -------------------------------------------------------------------------------- /src/components/slate-helpers/index.js: -------------------------------------------------------------------------------- 1 | import getClosestBlock from './get-closest-block'; 2 | import getSelectionNodes from './get-selection-nodes'; 3 | import insertNodesAtSelection from './insert-nodes-at-selection'; 4 | import insertText from './insert-text'; 5 | import mergeNodes from './merge-nodes'; 6 | import removeNodes from './remove-nodes'; 7 | import setNode from './set-node'; 8 | import splitNodes from './split-nodes'; 9 | import breakParagraph from './break-paragraph'; 10 | import collapseSelectionToAsinglePoint from './collapse-selection-to-a-single-point'; 11 | import handleSplitParagraph from './handle-split-paragraph'; 12 | import createNewParagraphBlock from './create-new-paragraph-block'; 13 | import handleDeleteInParagraph from './handle-delete-in-paragraph'; 14 | import setSelection from './set-selection'; 15 | import getNodebyPath from './get-node-by-path'; 16 | const SlateHelpers = { 17 | getClosestBlock, 18 | getSelectionNodes, 19 | insertNodesAtSelection, 20 | mergeNodes, 21 | removeNodes, 22 | setNode, 23 | splitNodes, 24 | breakParagraph, 25 | insertText, 26 | collapseSelectionToAsinglePoint, 27 | handleSplitParagraph, 28 | createNewParagraphBlock, 29 | handleDeleteInParagraph, 30 | setSelection, 31 | getNodebyPath, 32 | }; 33 | 34 | export default SlateHelpers; 35 | -------------------------------------------------------------------------------- /src/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 | // handle edge case if it's zero, then just return shorter timecode 33 | if (time === 0) { 34 | return '00:00:00'; 35 | } else { 36 | const timecode = secondsToTimecode(time); 37 | return timecode.slice(0, -3); 38 | } 39 | }; 40 | 41 | export { secondsToTimecode, timecodeToSeconds, shortTimecode }; 42 | -------------------------------------------------------------------------------- /src/util/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 | -------------------------------------------------------------------------------- /src/util/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 | -------------------------------------------------------------------------------- /docs/notes/verbose-generate-previous-timings-up-to-current-func.md: -------------------------------------------------------------------------------- 1 | # verbose-generate-previous-timings-up-to-current-func 2 | 3 | ```javascript 4 | /** 5 | * See explanation in `src/utils/dpe-to-slate/index.js` for how this function works with css injection 6 | * to provide current paragaph's highlight. 7 | * @param {Number} currentTime - float in seconds 8 | */ 9 | const generatePreviousTimingsUpToCurrent = (currentTime) => { 10 | const lastWordStartTime = props.transcriptData.words[props.transcriptData.words.length - 1].start; 11 | const lastWordStartTimeInt = parseInt(lastWordStartTime); 12 | const emptyListOfTimes = Array(lastWordStartTimeInt); 13 | const listOfTimesInt = [...emptyListOfTimes.keys()]; 14 | const listOfTimesUpToCurrentTimeInt = listOfTimesInt.splice(0, currentTime, 0); 15 | const stringlistOfTimesUpToCurrentTimeInt = listOfTimesUpToCurrentTimeInt.join(' '); 16 | return stringlistOfTimesUpToCurrentTimeInt; 17 | }; 18 | ``` 19 | 20 | One line 21 | 22 | ```javascript 23 | const generatePreviousTimingsUpToCurrent = (currentTime) => { 24 | return [...Array(parseInt(props.transcriptData.words[props.transcriptData.words.length - 1].start)).keys()].splice(0, currentTime, 0).join(' '); 25 | }; 26 | ``` 27 | 28 | simplified without using words 29 | 30 | ```js 31 | function generatePreviousTimingsUpToCurrent(start) { 32 | return new Array(parseInt(start)) 33 | .fill(1) 34 | .map((_, i) => i + 1) 35 | .join(' '); 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/components/slate-helpers/insert-nodes-at-selection/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | https://github.com/dylans/slate-snippets#insert-node-at-beginning-of-document 3 | // Insert nodes at selection 4 | 5 | // https://docs.slatejs.org/api/transforms#transforms-insertnodes-editor-editor-nodes-node-or-node-options 6 | 7 | Transforms.insertNodes(editor, [ 8 | { type: 'inline_type', children: [{ text: 'some text', marks: [] }] }, 9 | { text: ' and some text after the inline', marks: [] }, 10 | ]); 11 | 12 | https://github.com/dylans/slate-snippets#insert-inline--text--navigate-to-text 13 | // Insert inline + text & navigate to text 14 | 15 | Transforms.insertNodes(editor, [ 16 | { type: 'link', url:'x', children: [{ text:'mja', marks:[] }] }, 17 | { text: '', marks:[] }, 18 | ]); 19 | const nextPoint = Editor.after(editor, editor.selection.anchor); 20 | Editor.setSelection(editor, {anchor:nextPoint, focus:nextPoint}) 21 | */ 22 | import { Transforms, Editor } from 'slate'; 23 | 24 | /** 25 | * 26 | * @param {*} editor 27 | * @param {array} - list of slateJS blocks objects 28 | */ 29 | function insertNodesAtSelection({ editor, blocks, moveSelection = false, options = {} }) { 30 | Transforms.insertNodes(editor, [...blocks], options); 31 | // move selection to that point 32 | if (moveSelection) { 33 | const nextPoint = Editor.after(editor, editor.selection.anchor); 34 | Transforms.setSelection(editor, { anchor: nextPoint, focus: nextPoint }); 35 | } 36 | } 37 | 38 | export default insertNodesAtSelection; 39 | -------------------------------------------------------------------------------- /src/util/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 13 | .map((word) => { 14 | return word.text; 15 | }) 16 | .join(' '); 17 | } 18 | 19 | /** 20 | * 21 | * @param {*} textInput - can be either plain text string or an array of word objects 22 | */ 23 | function preSegmentText(textInput, tmpNumberOfCharPerLine = 35) { 24 | let text = textInput; 25 | if (typeof textInput === 'object') { 26 | text = getTextFromWordsList(textInput); 27 | } 28 | const segmentedText = textSegmentation(text); 29 | // - 2.Line brek between stentences 30 | const textWithLineBreakBetweenSentences = addLineBreakBetweenSentences(segmentedText); 31 | // - 3.Fold char limit per line 32 | const foldedText = foldWords(textWithLineBreakBetweenSentences, tmpNumberOfCharPerLine); 33 | // - 4.Divide into two lines 34 | const textDividedIntoTwoLines = divideIntoTwoLines(foldedText); 35 | 36 | return textDividedIntoTwoLines; 37 | } 38 | 39 | export { preSegmentText, getTextFromWordsList }; 40 | 41 | export default preSegmentText; 42 | -------------------------------------------------------------------------------- /src/util/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 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/qa_individual_issue_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: QA Report - individual issue 3 | about: Raise an individual QA issue 4 | title: '[QA] Issue #2.1 - Create a project' 5 | labels: QA Issue 6 | assignees: 7 | --- 8 | 9 | 10 | 11 | **QA Item and step's title** 12 | 13 | 14 | 15 | 16 | 17 | **Describe the bug** 18 | 19 | 20 | 21 | **To Reproduce** 22 | Steps to reproduce the behavior: 23 | 24 | 1. Go to '...' 25 | 2. Click on '....' 26 | 3. Scroll down to '....' 27 | 4. See error 28 | 29 | **Expected behavior** 30 | 31 | 32 | 33 | **Screenshots** 34 | 35 | 36 | 37 | **Desktop (please complete the following information):** 38 | 39 | - OS: [e.g. iOS] 40 | - Browser [e.g. chrome, safari] 41 | - Version [e.g. 22] 42 | 43 | **Smartphone (please complete the following information):** 44 | 45 | - Device: [e.g. iPhone6] 46 | - OS: [e.g. iOS8.1] 47 | - Browser [e.g. stock browser, safari] 48 | - Version [e.g. 22] 49 | 50 | **Additional context** 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/util/export-adapters/subtitles-generator/presegment-text/text-segmentation/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import textSegmentation from './index.js'; 3 | 4 | var sampleText = 5 | "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."; 6 | 7 | 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. 8 | 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. 9 | 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.`; 10 | 11 | var optionalHonorificsSample = 'Mr'; 12 | 13 | test('add line break between sentences', () => { 14 | var result = textSegmentation(sampleText); 15 | expect(result).toBe(expectedOutput); 16 | }); 17 | 18 | test('add line break between sentences,with optional honorifics', () => { 19 | var result = textSegmentation(sampleText, optionalHonorificsSample); 20 | expect(result).toBe(expectedOutput); 21 | }); 22 | -------------------------------------------------------------------------------- /src/util/export-adapters/subtitles-generator/compose-subtitles/csv/index.test.js: -------------------------------------------------------------------------------- 1 | import csvGenerator from './index.js'; 2 | const SAMPLE_SRT_JSON_CONTENT = [ 3 | { 4 | text: "=cmd|' /C calc'!'A1' So tell me, let’s start at the beginning.", 5 | start: 1.41, 6 | end: 3.28, 7 | speaker: 'James Jacoby', 8 | }, 9 | { 10 | text: 'How’d you get to Facebook in the beginning?', 11 | start: 3.28, 12 | end: 6.1, 13 | speaker: 'James Jacoby', 14 | }, 15 | { 16 | text: 'So I joined the company in the late summer of 2005.', 17 | start: 6.1, 18 | end: 9.49, 19 | speaker: 'James Jacoby', 20 | }, 21 | { 22 | text: 'At the time, I was an independent designer and developer working in', 23 | start: 9.49, 24 | end: 12.67, 25 | speaker: 'Soleio Cuervo', 26 | }, 27 | { 28 | text: 'San Francisco.', 29 | start: 12.67, 30 | end: 13.29, 31 | speaker: 'Soleio Cuervo', 32 | }, 33 | ]; 34 | 35 | const CSV_SAMPLE_OUTPUT = `N,In,Out,Speaker,Text 36 | 1,"00:00:01,410","00:00:03,280","'James Jacoby","'=cmd|' /C calc'!'A1' So tell me, let’s start at the beginning." 37 | 2,"00:00:03,280","00:00:06,100","'James Jacoby","'How’d you get to Facebook in the beginning?" 38 | 3,"00:00:06,100","00:00:09,490","'James Jacoby","'So I joined the company in the late summer of 2005." 39 | 4,"00:00:09,490","00:00:12,670","'Soleio Cuervo","'At the time, I was an independent designer and developer working in" 40 | 5,"00:00:12,670","00:00:13,290","'Soleio Cuervo","'San Francisco."`; 41 | 42 | test('CSV generator', () => { 43 | const result = csvGenerator(SAMPLE_SRT_JSON_CONTENT); 44 | expect(result).toEqual(CSV_SAMPLE_OUTPUT); 45 | }); 46 | -------------------------------------------------------------------------------- /src/util/export-adapters/txt/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert Slate editor contnet 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 | 22 | import { shortTimecode } from '../../timecode-converter/index.js'; 23 | import { Node } from 'slate'; 24 | const slateToText = ({ value, speakers, timecodes, atlasFormat }) => { 25 | return ( 26 | value 27 | // Return the string content of each paragraph in the value's children. 28 | .map((n) => { 29 | if (atlasFormat) { 30 | return `${timecodes ? `${speakers ? n.speaker : ''}\t[${shortTimecode(n.start)}]\t` : ''}\t${Node.string(n)}`; 31 | } else { 32 | return `${timecodes ? `${shortTimecode(n.start)}\t` : ''}${speakers ? n.speaker.toUpperCase() : ''}${ 33 | speakers || timecodes ? '\n' : '' 34 | }${Node.string(n)}`; 35 | } 36 | }) 37 | // Join them all with line breaks denoting paragraphs. 38 | .join('\n\n') 39 | ); 40 | }; 41 | 42 | export default slateToText; 43 | -------------------------------------------------------------------------------- /src/util/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 | -------------------------------------------------------------------------------- /src/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 | // handle edge case, trying to convert zero seconds 27 | if (seconds === 0) { 28 | return '00:00:00:00'; 29 | } 30 | // written for PAL non-drop timecode 31 | let fps = 25; 32 | if (framePerSeconds !== undefined) { 33 | fps = framePerSeconds; 34 | } 35 | 36 | const normalisedSeconds = normalisePlayerTime(seconds, fps); 37 | 38 | const wholeSeconds = Math.floor(normalisedSeconds); 39 | const frames = ((normalisedSeconds - wholeSeconds) * fps).toFixed(2); 40 | 41 | // prepends zero - example pads 3 to 03 42 | function _padZero(n) { 43 | if (n < 10) return `0${parseInt(n)}`; 44 | 45 | return parseInt(n); 46 | } 47 | 48 | return `${_padZero((wholeSeconds / 60 / 60) % 60)}:${_padZero((wholeSeconds / 60) % 60)}:${_padZero(wholeSeconds % 60)}:${_padZero(frames)}`; 49 | }; 50 | 51 | export default secondsToTimecode; 52 | -------------------------------------------------------------------------------- /src/util/export-adapters/subtitles-generator/compose-subtitles/csv/index.js: -------------------------------------------------------------------------------- 1 | import formatSeconds from '../util/format-seconds.js'; 2 | 3 | /** 4 | * from issue https://github.com/newscorp-ghfb/dj-tools-transcribe/issues/70 5 | * > prepend each cell field with a single quote, so that their content will be read as text by the spreadsheet editor. 6 | * @param {string} text 7 | * @returns {string} 8 | */ 9 | function escapeStringForCSV(text) { 10 | return `'${text}`; 11 | } 12 | 13 | // seconds to HH:MM:SS,000 14 | function formatTimecodesInSrtFormat(seconds) { 15 | return formatSeconds(parseFloat(seconds)).replace('.', ','); 16 | } 17 | 18 | function csvGenerator(srtJsonContent) { 19 | const csvHeader = 'N,In,Out,Speaker,Text'; 20 | 21 | const csvBody = srtJsonContent 22 | .map(({ start, end, speaker, text }, index) => { 23 | const lineIndex = `${index + 1}`; 24 | const startTimecode = `\"${formatTimecodesInSrtFormat(start)}\"`; 25 | const endTimecode = `\"${formatTimecodesInSrtFormat(end)}\"`; 26 | // removing line breaks and and removing " as they break the csv. 27 | // wrapping text in escaped " to escape any , for the csv. 28 | // adding carriage return \n to signal end of line in csv 29 | // Preserving line break within srt lines to allow round trip from csv back to srt file in same format. 30 | // by replacing \n with \r\n. 31 | const speakerLabel = `\"${escapeStringForCSV(speaker.replace(/\n/g, ' '))}\"`; 32 | const textField = `\"${escapeStringForCSV(text.replace(/\n/g, ' '))}\"`; 33 | const csvLine = `${lineIndex},${startTimecode},${endTimecode},${speakerLabel},${textField}`; 34 | return csvLine; 35 | }) 36 | .join('\n'); 37 | 38 | const csvContent = `${csvHeader}\n${csvBody}`; 39 | return csvContent; 40 | } 41 | 42 | export default csvGenerator; 43 | -------------------------------------------------------------------------------- /src/components/slate-helpers/get-selection-nodes/index.js: -------------------------------------------------------------------------------- 1 | const getSelectionNodes = (editor, selection) => { 2 | try { 3 | const orderedSelection = [selection.anchor, selection.focus].sort((a, b) => { 4 | return a.path[0] - b.path[0]; 5 | }); 6 | const selectionStart = orderedSelection[0]; 7 | const selectionEnd = orderedSelection[1]; 8 | let counterAnchor = 0; 9 | let goalAnchor = selectionStart.offset; 10 | let targetWordIndexAnchor = null; 11 | let selectedLeafWordsAnchor = editor.children[selectionStart.path[0]].children[0].words; 12 | // let pathValue = selectionStart.path; 13 | // let selectedLeafWordsAnchor2 = editor.children[selectionStart.path].children[0].words; 14 | 15 | selectedLeafWordsAnchor.forEach((word, wordIndex) => { 16 | const wordLength = (word.text + ' ').length; 17 | 18 | counterAnchor = counterAnchor + wordLength; 19 | if (counterAnchor <= goalAnchor) { 20 | targetWordIndexAnchor = wordIndex; 21 | } 22 | }); 23 | 24 | const startWord = selectedLeafWordsAnchor[targetWordIndexAnchor + 1]; 25 | 26 | let counter = 0; 27 | let goal = selectionEnd.offset; 28 | let targetWordIndex = null; 29 | let selectedLeafWords = editor.children[selectionEnd.path[0]].children[0].words; 30 | selectedLeafWords.forEach((word, wordIndex) => { 31 | const wordLength = (word.text + ' ').length; 32 | 33 | counter = counter + wordLength; 34 | if (counter <= goal) { 35 | targetWordIndex = wordIndex; 36 | } 37 | }); 38 | 39 | const endWord = selectedLeafWords[targetWordIndex + 1]; 40 | // return { startSec: startWord.start, endSec: endWord.end }; 41 | return { startWord, endWord }; 42 | } catch (error) { 43 | console.error('error finding times from selection:: ', error); 44 | } 45 | }; 46 | 47 | export default getSelectionNodes; 48 | -------------------------------------------------------------------------------- /src/util/export-adapters/subtitles-generator/presegment-text/README.md: -------------------------------------------------------------------------------- 1 | # Pre segmentation 2 | 3 | ## Input 4 | - either an array list of words objects 5 | example 6 | ```json 7 | [ 8 | { 9 | "id": 0, 10 | "start": 13.02, 11 | "end": 13.17, 12 | "text": "There" 13 | }, 14 | { 15 | "id": 1, 16 | "start": 13.17, 17 | "end": 13.38, 18 | "text": "is" 19 | }, 20 | { 21 | "id": 2, 22 | "start": 13.38, 23 | "end": 13.44, 24 | "text": "a" 25 | }, 26 | { 27 | "id": 3, 28 | "start": 13.44, 29 | "end": 13.86, 30 | "text": "day." 31 | }, 32 | { 33 | "id": 4, 34 | "start": 13.86, 35 | "end": 14.13, 36 | "text": "About" 37 | }, 38 | { 39 | "id": 5, 40 | "start": 14.13, 41 | "end": 14.38, 42 | "text": "ten" 43 | }, 44 | { 45 | "id": 6, 46 | "start": 14.38, 47 | "end": 14.61, 48 | "text": "years" 49 | }, 50 | { 51 | "id": 7, 52 | "start": 14.61, 53 | "end": 15.15, 54 | "text": "ago" 55 | }, 56 | ``` 57 | - or a string of text 58 | Example 59 | ``` 60 | There is a day. About ten years ago 61 | ``` 62 | 63 | ## Output: 64 | - segmented plain text 65 | 66 | example 67 | 68 | ``` 69 | There is a day. 70 | 71 | About ten years ago when I asked a 72 | 73 | friend to hold a baby dinosaur 74 | robot upside down. 75 | 76 | It was a toy called plea. 77 | 78 | All It's a super courts are 79 | 80 | showing off to my friend and I 81 | said to hold it, but he'll see 82 | 83 | ... 84 | ``` 85 | 86 | 87 | This allows for flexibility in giving the input either to aeneas forced aligner to produce subtitles or to another algorithm to restore timecodes from STT word timings output if available. -------------------------------------------------------------------------------- /src/util/export-adapters/subtitles-generator/compose-subtitles/itt.js: -------------------------------------------------------------------------------- 1 | import tcFormat from './util/tc-format.js'; 2 | import escapeText from './util/escape-text.js'; 3 | 4 | const ittGenerator = (vttJSON, lang = 'en-GB', FPS = 25) => { 5 | let ittOut = ` 6 | 18 | 19 | 20 |