Index: in/trunk/model-interfaces-dev/atea/macron-restoration/src/components/TranscriptionItem.vue
===================================================================
--- /main/trunk/model-interfaces-dev/atea/macron-restoration/src/components/TranscriptionItem.vue	(revision 35716)
+++ 	(revision )
@@ -1,209 +1,0 @@
-<template>
-    <div class="card">
-        <!-- Header containing info and actions for the transcription -->
-        <div class="transcription__header">
-            <button class="btn-fab" v-on:click="toggleAudio" type="button" :title="translations.get('TranscriptionItem_PlayButtonTooltip')">
-                <span class="material-icons mdi-l play-button" v-if="!isPlaying">play_arrow</span>
-                <span class="material-icons mdi-l play-button" v-if="isPlaying">pause</span>
-            </button>
-
-            <span>{{ translations.get("TranscriptionItem_FileName") }}: {{ transcription.fileName }}</span>
-
-            <div style="position: relative;">
-                <button class="btn-primary" @mouseover="showDownloadOptions = true" @mouseout="showDownloadOptions = false" type="button">
-                    <span class="material-icons">download</span>
-                    <span>{{ translations.get("TranscriptionItem_Download") }}</span>
-                </button>
-
-                <div class="download-popup card" :class="{ 'download-popup-show': showDownloadOptions }"
-                     @mouseover="showDownloadOptions = true" @mouseout="showDownloadOptions = false">
-                    <button @click="downloadAsText" type="button" class="btn-primary theme-flat">
-                        <span class="material-icons">text_snippet</span>
-                        <span>{{ translations.get("TranscriptionItem_DownloadAsText") }}</span>
-                    </button>
-
-                    <button @click="downloadAsJson" type="button" class="btn-primary theme-flat">
-                        <span class="material-icons">integration_instructions</span>
-                        <span>{{ translations.get("TranscriptionItem_DownloadAsJson") }}</span>
-                    </button>
-
-                    <button @click="downloadAsWebvtt" type="button" class="btn-primary theme-flat">
-                        <span class="material-icons">subtitles</span>
-                        <span>{{ translations.get("TranscriptionItem_DownloadAsWebvtt") }}</span>
-                    </button>
-                </div>
-            </div>
-
-            <button class="btn-primary theme-error" @click="remove" type="button">
-                <span class="material-icons">delete</span>
-                <span>{{ translations.get("TranscriptionItem_Remove") }}</span>
-            </button>
-        </div>
-
-        <div class="editor-controls">
-            <audio-time-bar v-model.number="currentPlaybackTime" :audio-length="audioLength" :isDisabled="playbackState.id != transcription.id" />
-        </div>
-
-        <hr />
-
-        <div class="editor-controls">
-            <TranscriptionItemEditor ref="editor" :transcription="transcription" style="margin-bottom: 1em" :enableEditing="enableEditing" />
-            <toggle-button v-model="enableEditing" :title="translations.get('TranscriptionItemEditor_ToggleEditTooltip')">
-                <span class="material-icons">edit</span>
-            </toggle-button>
-        </div>
-    </div>
-</template>
-
-<style scoped lang="scss">
-.transcription__header {
-    display: grid;
-    gap: 0.5em 0.5em;
-    grid-template-columns: auto 1fr auto auto;
-    align-items: center;
-}
-
-.download-popup {
-    display: flex;
-    flex-direction: column;
-    align-items: stretch;
-
-    position: absolute;
-    top: 97%;
-    z-index: 2;
-
-    padding: 2px;
-    margin: 0;
-    width: 14em;
-
-    transition-duration: var(--transition-duration);
-    visibility: hidden;
-    opacity: 0;
-}
-
-.download-popup-show {
-    visibility: visible;
-    opacity: 1;
-}
-
-.editor-controls {
-    display: grid;
-    align-items: flex-start;
-    grid-template-columns: 1fr auto;
-    margin: 0.5em 0 0.3em 0;
-    gap: 1em;
-}
-
-.rotate-180 {
-    transform: rotate(180deg);
-}
-</style>
-
-<script>
-import { mapState } from "vuex";
-import { saveAs } from "file-saver"
-import { TranscriptionViewModel } from "../main";
-import AudioPlayback from "../js/AudioPlaybackModule"
-import Util from "../js/Util"
-import AudioTimeBar from "./AudioTimeBar.vue"
-import TranscriptionItemEditor from "./TranscriptionItemEditor.vue"
-
-export default {
-    name: "TranscriptionItem",
-    components: {
-        AudioTimeBar,
-        TranscriptionItemEditor
-    },
-    props: {
-        transcription: TranscriptionViewModel
-    },
-    data() {
-        return {
-            enableEditing: false,
-            showDownloadOptions: false
-        }
-    },
-    computed: {
-        currentPlaybackTime: {
-            get() {
-                return this.$store.getters.transcriptionPlaybackTime(this.transcription.id);
-            },
-            set(value) {
-                this.$store.commit("playbackStateSetTime", { id: this.transcription.id, time: value });
-            }
-        },
-        audioLength() {
-            return this.$store.getters.transcriptionPlaybackLength(this.transcription.id);
-        },
-        isPlaying() {
-            return this.playbackState.isPlaying && this.playbackState.id === this.transcription.id;
-        },
-        ...mapState({
-            translations: state => state.translations,
-            playbackState: state => state.playbackState
-        })
-    },
-    methods: {
-        async toggleAudio() {
-            this.isPlaying ? AudioPlayback.pause() : await AudioPlayback.play(this.transcription.id, -1);
-        },
-        remove() {
-            this.$store.commit("rawTranscriptionRemove", this.transcription.id);
-        },
-        downloadAsText() {
-            const fileName = buildDownloadableFileName(this.transcription.fileName, "txt");
-
-            const blob = new Blob([ this.$refs.editor.words.map(w => w.word).join(" ") ], { type: "text/plain;charset=utf-8" });
-            saveAs(blob, fileName);
-        },
-        downloadAsJson() {
-            const fileName = buildDownloadableFileName(this.transcription.fileName, "json");
-            const toDownload = (({ fileName, transcription }) => ({ fileName, transcription }))(this.transcription);
-            toDownload.words = this.$refs.editor.words.map(w => (({ word, startTime, endTime }) => ({ word, startTime, endTime }))(w));
-
-            const blob = new Blob([ JSON.stringify(toDownload, null, 4) ], { type: "application/json;charset=utf-8" });
-            saveAs(blob, fileName);
-        },
-        downloadAsWebvtt() {
-            const fileName = buildDownloadableFileName(this.transcription.fileName, "vtt");
-            const toDownload = buildWebvttFileContents(this.transcription, this.$refs.editor);
-
-            const blob = new Blob([ toDownload ], { type: "text/vtt;charset=utf-8" });
-            saveAs(blob, fileName);
-        }
-    }
-}
-
-/**
- * Builds a file name for a download.
- * @param {String} transcriptionFileName The name of the transcription that will be downloaded.
- * @param {String} extension The file extension of the download. Do not include a period.
- * @returns {String} The file name.
- */
-function buildDownloadableFileName(transcriptionFileName, extension) {
-    const extensionIndex = transcriptionFileName.lastIndexOf(".");
-    let fileName = transcriptionFileName.slice(0, extensionIndex);
-    fileName += "_transcription." + extension;
-
-    return fileName;
-}
-
-/**
- * Builds a WebVTT file of the given transcription
- * @param {TranscriptionViewModel} transcription The transcription.
- * @returns {String} The WebVTT content.
- */
-function buildWebvttFileContents(transcription, editor) {
-    let contents = "WEBVTT Transcription of " + transcription.fileName + "\n\n";
-
-    for (const word of editor.words) {
-        const startTime = Util.formatSecondsTimeString(word.startTime, true);
-        const endTime = Util.formatSecondsTimeString(word.endTime, true);
-
-        contents += startTime + " --> " + endTime + "\n";
-        contents += "- " + word.word + "\n\n";
-    }
-
-    return contents;
-}
-</script>
Index: in/trunk/model-interfaces-dev/atea/macron-restoration/src/components/TranscriptionItemEditor.vue
===================================================================
--- /main/trunk/model-interfaces-dev/atea/macron-restoration/src/components/TranscriptionItemEditor.vue	(revision 35716)
+++ 	(revision )
@@ -1,477 +1,0 @@
-<template>
-<div>
-    <div class="text-container words-container" :hidden="enableEditing">
-        <ul class="list-view" v-if="!enableEditing">
-            <li v-for="word in words" :key="word.id" class="word-container" @click="playAudio(word.startTime)">
-                <span class="word-highlight word" :class="{ 'word-highlight-applied': word.shouldHighlight }">
-                    {{ word.word }}
-                </span>
-            </li>
-        </ul>
-
-        <ul class="list-view" v-else>
-            <li v-for="(word, index) in words" :key="word.id" class="word-container">
-                <input :size="word.word.length === 0 ? 1 : word.word.length" :ref="word.id" @click="loadAudio(word.startTime)"
-                    class="editor-word" v-model="word.word" type="text" :class="{ 'word-highlight-applied': word.shouldHighlight }"
-                    @beforeinput="onEditorBeforeInput($event, index)" @focusout="onEditorFocusOut(index)" @focus="onEditorFocus(index)" />
-                <span>&nbsp;</span>
-            </li>
-        </ul>
-    </div>
-
-    <word-timing-selector class="word-timing-selector" @wordUpdated="onWordUpdated" v-if="enableEditing"
-        :surroundingWords="surroundingWords" :wordIndex="surroundingWordPrincipleIndex" :transcriptionID="transcription.id" />
-</div>
-</template>
-
-<style scoped lang="scss">
-.word {
-    padding: 0.25em;
-}
-
-.words-container {
-    transition-duration: var(--transition-duration);
-
-    &[hidden] {
-        background-color: inherit;
-    }
-}
-
-.word-container {
-    font: var(--monospace-font);
-    display: inline-block;
-    line-height: 2em;
-}
-
-.word-highlight-applied {
-    background-color: var(--highlighted-word-bg);
-}
-
-.word-highlight {
-    &:hover {
-        @extend .word-highlight-applied;
-    }
-}
-
-.editor-word {
-    border: 1px solid black;
-    border-radius: 2px;
-    padding: 2px;
-    font-family: inherit;
-    font-size: 1rem;
-}
-
-.word-timing-selector {
-    margin: 0.5em auto;
-}
-</style>
-
-<script>
-import { mapState } from "vuex";
-import { TranscriptionViewModel } from "../main";
-import AudioPlayback from "../js/AudioPlaybackModule"
-import WordTimingSelector from "./WordTimingSelector.vue"
-import Util from "../js/Util";
-
-export class Word {
-    /**
-     * Initialises a new instance of the Word class.
-     * @param {String} word The word.
-     * @param {Number} startTime The time within the audio that this word starts being spoken.
-     * @param {Number} endTime The time within the audio that this word finishes being spoken.
-     */
-    constructor(word, startTime, endTime, id = null, shouldHighlight = false, deletionAttempts = 0) {
-        /** @type {String} The ID of this word. */
-        this.id = id ?? Util.generateUuid();
-
-        /** @type {String} The word. */
-        this.word = word;
-
-        /** @type {Number} The time within the audio that this word starts being spoken. */
-        this.startTime = startTime;
-
-        /** @type {Number} The time within the audio that this word finishes being spoken. */
-        this.endTime = endTime;
-
-        /** @type {Boolean} A value indicating whether this word should be highlighted. */
-        this.shouldHighlight = shouldHighlight;
-
-        /** @type {Number} The number of times the user has tried to delete this word. */
-        this.deletionAttempts = deletionAttempts;
-    }
-}
-
-export default {
-    name: "TranscriptionItemEditor",
-    components: {
-        WordTimingSelector
-    },
-    props: {
-        transcription: TranscriptionViewModel,
-        enableEditing: Boolean
-    },
-    data() {
-        return {
-            words: [],
-            lastHighlightedWordIndex: 0,
-            selectedIndex: 0,
-            surroundingWordPrincipleIndex: 0
-        }
-    },
-    computed: {
-        currentPlaybackTime: {
-            get() {
-                return this.$store.getters.transcriptionPlaybackTime(this.transcription.id);
-            },
-            set(value) {
-                this.$store.commit("playbackStateSetTime", { id: this.transcription.id, time: value });
-            }
-        },
-        surroundingWords() {
-            return this.getSurroundingWords(this.selectedIndex);
-        },
-        ...mapState({
-            playbackState: state => state.playbackState,
-            translations: state => state.translations
-        })
-    },
-    methods: {
-
-        /**
-         * Gets the words in a transcription.
-         * @param {TranscriptionViewModel} transcription The transcription.
-         * @returns {Word[]}
-         */
-        getWords() {
-            const metadata = this.transcription.metadata;
-
-            if (metadata.length === 0) {
-                return;
-            }
-
-            /** @type {Word[]} */
-            const words = [];
-
-            let currentWord = "";
-            let currStartTime = metadata[0].start_time;
-
-            for (let i = 0; i < metadata.length; i++) {
-                const mChar = metadata[i];
-
-                if (mChar.char === " ") {
-                    words.push(new Word(currentWord, currStartTime, mChar.start_time));
-                    currentWord = "";
-
-                    if (i + 1 < metadata.length) {
-                        currStartTime = metadata[i + 1].start_time;
-                    }
-                    else {
-                        break;
-                    }
-                }
-                else {
-                    currentWord += mChar.char;
-                }
-            }
-
-            // Push the last word, as most transcriptions will not end in a space (hence breaking the above algorithm)
-            if (currentWord.length > 0) {
-                const newWord = new Word(currentWord, currStartTime, metadata[metadata.length - 1].start_time)
-                words.push(newWord);
-            }
-
-            return words;
-        },
-
-        async playAudio(startTime) {
-            await AudioPlayback.play(this.transcription.id, startTime);
-        },
-
-        async loadAudio(startTime) {
-            await AudioPlayback.load(this.transcription.id, startTime);
-        },
-
-        /**
-         * Gets the minimum viable index that surrounding words can start at.
-         * @param {Number} principleIndex The index of the primary surrounding word.
-         * @param {Number} buffer The maximum number of words to take on each side of the principle word.
-         */
-        getSurroundingWordMinIndex(principleIndex, buffer) {
-            let min = principleIndex;
-            for (let i = principleIndex; i > principleIndex - buffer && i > 0; i--) {
-                min--;
-            }
-
-            return min;
-        },
-
-        getSurroundingWords(index) {
-            const BUFFER = 2; // The number of words to take on each side TODO: Global constant
-
-            const min = this.getSurroundingWordMinIndex(index, BUFFER);
-
-            let max = index + 1;
-            for (let i = index; i < index + BUFFER && i < this.words.length; i++) {
-                max++;
-            }
-
-            this.surroundingWordPrincipleIndex = index - min;
-            return this.words.slice(min, max);
-        },
-
-        onWordUpdated(relativeIndex, word) {
-            const BUFFER = 2; // The number of words to take on each side
-
-            const min = this.getSurroundingWordMinIndex(this.selectedIndex, BUFFER);
-
-            this.words[min + relativeIndex] = word;
-        },
-
-        /**
-         * Invoked when the value of a word changes.
-         * @param {InputEvent} event The input event.
-         * @param {Number} index The index of the word that has changed.
-         */
-        onEditorBeforeInput(event, index) {
-            // https://rawgit.com/w3c/input-events/v1/index.html#interface-InputEvent-Attributes
-            const deletionEvents = [
-                "deleteWordForward", "deleteWordBackward",
-                "deleteSoftLineBackward", "deleteSoftLineForward", "deleteEntireSoftLine",
-                "deleteHardLineBackward", "deleteHardLineForward",
-                "deleteByDrag", "deleteByCut",
-                "deleteContent", "deleteContentBackward", "deleteContentForward"
-            ];
-
-            const backwardDeletionEvents = [
-                "deleteWordBackward", "deleteSoftLineBackward", "deleteHardLineBackward", "deleteContentBackward"
-            ]
-
-            const forwardDeletionEvents = [
-                "deleteWordForward", "deleteSoftLineForward", "deleteHardLineForward", "deleteContentForward"
-            ]
-
-            const insertionEvents = [
-                "insertText", "insertReplacementText",
-                "insertLineBreak", "insertParagraph",
-                "insertOrderedList", "insertUnorderedList", "insertHorizontalRule",
-                "insertFromYank", "insertFromDrop", "insertFromPaste", "insertFromPasteAsQuotation",
-                "insertTranspose", "insertCompositionText", "insertLink"
-            ];
-
-            if (event.inputType === "historyUndo" || event.inputType === "historyRedo") {
-                event.preventDefault();
-                return;
-            }
-
-            /** @type {Word} */
-            const word = this.words[index];
-            const inputIndex = this.$refs[word.id].selectionStart;
-            const inputEndIndex = this.$refs[word.id].selectionEnd;
-
-            if (deletionEvents.includes(event.inputType)) {
-                if (word.word.length === 0) { // Remove the word
-                    if (index === 0) {
-                        this.mergeWordForward(word, index);
-                    }
-                    else if (index === this.words.length - 1) {
-                        this.mergeWordBackward(word, index);
-                    }
-                    else if (backwardDeletionEvents.includes(event.inputType)) {
-                        this.mergeWordBackward(word, index);
-                    }
-                    else if (forwardDeletionEvents.includes(event.inputType)) {
-                        this.mergeWordForward(word, index);
-                    }
-
-                    event.preventDefault();
-                }
-                else if (inputIndex === 0 && backwardDeletionEvents.includes(event.inputType) && !inputEndIndex) { // Join with last word
-                    if (index === 0) {
-                        return;
-                    }
-                    else {
-                        this.mergeWordBackward(word, index);
-                    }
-
-                    event.preventDefault();
-                }
-                else if (inputIndex === word.word.length && forwardDeletionEvents.includes(event.inputType)) { // Join with next word
-                    if (index === this.words.length - 1) {
-                        return;
-                    }
-                    else {
-                        this.mergeWordForward(word, index);
-                    }
-
-                    event.preventDefault();
-                }
-            }
-
-            if (insertionEvents.includes(event.inputType)) {
-                if (event.data === " ") { // ONLY whitespace entered. Split or add new word
-                    let newWord;
-
-                    if (inputIndex === 0) { // Insert on left side
-                        const newEndTime = word.startTime + (word.endTime - word.startTime) / 2;
-                        newWord = new Word("", word.startTime, newEndTime);
-
-                        this.words.splice(index, 0, newWord);
-                        word.startTime = newEndTime;
-                    }
-                    else if (inputIndex === word.word.length) { // Insert on right side
-                        const newStartTime = word.startTime + ((word.endTime - word.startTime) / 2);
-                        newWord = new Word("", newStartTime, word.endTime);
-
-                        this.words.splice(index + 1, 0, newWord);
-                        word.endTime = newStartTime;
-                    }
-                    else { // Split down the middle
-                        const metadataIndex = this.transcription.metadata.findIndex(m => m.start_time === word.startTime) + inputIndex;
-                        const newStartTime = this.transcription.metadata[metadataIndex].start_time;
-                        newWord = new Word(word.word.slice(inputIndex), newStartTime, word.endTime);
-
-                        this.words.splice(index + 1, 0, newWord);
-                        word.endTime = newStartTime;
-                        word.word = word.word.slice(0, inputIndex);
-                    }
-
-                    // Prevent the event from being propagated to the newly focused input
-                    event.preventDefault();
-
-                    // Give the element time to be mounted before focusing
-                    setTimeout(
-                        () => {
-                            this.$refs[newWord.id].focus();
-                            this.$refs[newWord.id].selectionStart = 0;
-                            this.$refs[newWord.id].selectionEnd = null;
-                        },
-                        50
-                    );
-                }
-                else if (event.data.includes(" ")) {
-                    let wordsToAdd = event.data.split(" ");
-                    wordsToAdd = wordsToAdd.filter(w => w.trim().length > 0).map(w => w.trim());
-                    const sharedTime = (word.endTime - word.startTime) / wordsToAdd.length;
-
-                    if (wordsToAdd.length === 0) {
-                        return;
-                    }
-
-                    word.endTime = word.startTime + sharedTime;
-                    let newStartTime = word.endTime;
-
-                    for (let i = 0; i < wordsToAdd.length; i++) {
-                        const newWord = new Word(wordsToAdd[i], newStartTime, newStartTime + sharedTime);
-                        newStartTime += sharedTime;
-
-                        this.words.splice(index + i + 1, 0, newWord);
-                    }
-
-                    event.preventDefault();
-                }
-            }
-        },
-
-        mergeWordForward(word, index) {
-            if (this.words.length < 2) {
-                return;
-            }
-
-            this.words[index + 1].startTime = word.startTime;
-            this.words[index + 1].word = word.word + this.words[index + 1].word;
-
-            word.word = "";
-            this.setFocus(index, false);
-        },
-
-        mergeWordBackward(word, index) {
-            if (this.words.length < 2) {
-                return;
-            }
-
-            this.words[index - 1].endTime = word.endTime;
-            this.words[index - 1].word += word.word;
-
-            word.word = "";
-            this.setFocus(index, true);
-        },
-
-        onEditorFocus(index) {
-            if (this.currentPlaybackTime < this.words[index].startTime || this.currentPlaybackTime > this.words[index].endTime) {
-                const wordStartTime = this.words[index].startTime;
-                this.$store.commit("playbackStateSetTime", { id: this.transcription.id, time: wordStartTime });
-            }
-
-            this.selectedIndex = index;
-        },
-
-        onEditorFocusOut(index) {
-            // Remove empty words when they lose focus
-            if (this.words[index].word === "") {
-                this.words.splice(index, 1);
-            }
-        },
-
-        /**
-         * Gets the index of an adjacent word to focus on.
-         * @param {Number} index The index of the currently focused word.
-         * @param {Boolean} focusLeft Whether to focus on the word to the left or right of the current index.
-         */
-        getFocusIndex(index, focusLeft) {
-            let focusIndex = 0;
-
-            if (focusLeft) {
-                focusIndex = index === 0 ? 0 : index - 1;
-            }
-            else {
-                focusIndex = index === this.words.length - 1 ? index : index + 1;
-            }
-
-            return focusIndex;
-        },
-
-        /**
-         * Sets the focus to an adjacent word.
-         * @param {Number} index The index of the currently focused word.
-         * @param {Boolean} focusLeft Whether to focus on the word to the left or right of the current index.
-         */
-        setFocus(index, focusLeft) {
-            const focusIndex = this.getFocusIndex(index, focusLeft);
-            const focusId = this.words[focusIndex].id;
-            this.$refs[focusId].focus();
-
-            if (!focusLeft) {
-                this.$refs[focusId].setSelectionRange(0, 0);
-            }
-        }
-
-    },
-    watch: {
-        currentPlaybackTime(newValue) {
-            this.words[this.lastHighlightedWordIndex].shouldHighlight = false;
-
-            if (this.playbackState.id !== this.transcription.id) {
-                return;
-            }
-
-            for (let i = 0; i < this.words.length; i++) {
-                const word = this.words[i];
-
-                if (word.startTime <= newValue && word.endTime > newValue) {
-                    word.shouldHighlight = true;
-
-                    if (this.$refs[word.id]) {
-                        this.$refs[word.id].focus();
-                    }
-
-                    this.lastHighlightedWordIndex = i;
-                    break;
-                }
-            }
-        }
-    },
-    beforeMount() {
-        this.words = this.getWords();
-    }
-}
-</script>
Index: in/trunk/model-interfaces-dev/atea/macron-restoration/src/js/TranscribeModule.js
===================================================================
--- /main/trunk/model-interfaces-dev/atea/macron-restoration/src/js/TranscribeModule.js	(revision 35716)
+++ 	(revision )
@@ -1,192 +1,0 @@
-/**
- * @file Defines components that are used to interact with the transcription proxy servlet.
- * @author Carl Stephens
- * @module
- */
-
-import { log } from "./Util"
-
-/* eslint-disable camelcase */
-
-export class TranscriptionMetadata {
-    /**
-     * A metadata object held by a {@link TranscriptionModel}.
-     *
-     * @param {String} char The character.
-     * @param {Number} confidence The confidence that the character is correct. Smaller values are more confident.
-     * @param {Number} start_time The timestamp in the audio at which this character is spoken.
-     */
-    constructor(char, confidence, start_time) {
-        this.char = char;
-        this.confidence = confidence;
-        this.start_time = start_time;
-    }
-}
-
-export class TranscriptionModel {
-    /**
-     * The transcription object returned from our transcription endpoint.
-     *
-     * @param {String} file_name The name of the file that was transcribed.
-     * @param {String} log A note of how the transcription was processed.
-     * @param {TranscriptionMetadata[]} metadata The character metadata.
-     * @param {boolean} success A value indicating if the transcription was successful or not.
-     * @param {String} transcription The transcription.
-     */
-    constructor(file_name, log, metadata, success, transcription) {
-        this.file_name = file_name;
-        this.log = log;
-        this.metadata = metadata;
-        this.success = success;
-        this.transcription = transcription;
-    }
-}
-
-/* eslint-enable camelcase */
-
-export class TranscriptionError extends Error {
-    /**
-     * Initialises a new instance of the {@link TranscriptionError} object.
-     *
-     * @param {Number | undefined} statusCode The HTTP status code of the error.
-     * @param {String | undefined} message The status message.
-     * @param {String | null} fileName The file on which the transcription failed.
-     */
-    constructor(message = undefined, fileName = null, statusCode = -1) {
-        super(message);
-
-        /** @type {String | null} The name of the file that the transcription error occured on. */
-        this.fileName = fileName;
-
-        /** @type {Number | undefined} The status code returned by the API when the erro was generated. */
-        this.statusCode = statusCode;
-    }
-}
-
-/**
- * A service that uploads audio files in a multipart request to the Korero Maori API Interface servlet, and interprets the result.
- */
-export default class TranscribeService {
-    constructor() {
-        /* eslint-disable quotes */
-        this.TEST_JSON = '{"file_name":"akl_mi_pk_0002.wav","log":"Took 0.6s to transcribe 8.7s audio file /tmp/transcribe-aw7ujpu8-clean.wav","metadata":[{"char":"k","prob":7.0E-6,"start_time":0.24},{"char":"o","prob":8.3E-5,"start_time":0.26},{"char":"t","prob":0.995578,"start_time":0.42},{"char":"a","prob":0.997089,"start_time":0.44},{"char":"h","prob":3.0E-6,"start_time":0.54},{"char":"i","prob":8.0E-6,"start_time":0.56},{"char":" ","prob":2.2E-5,"start_time":0.6},{"char":"a","prob":0.982987,"start_time":0.64},{"char":"n","prob":5.0E-6,"start_time":0.8},{"char":"Å","prob":1.82E-4,"start_time":0.84},{"char":" ","prob":0.996944,"start_time":1.08},{"char":"t","prob":1.23E-4,"start_time":1.14},{"char":"e","prob":6.3E-5,"start_time":1.16},{"char":" ","prob":0.587595,"start_time":1.28},{"char":"t","prob":4.91E-4,"start_time":1.34},{"char":"u","prob":0.010654,"start_time":1.36},{"char":"p","prob":5.9E-5,"start_time":1.52},{"char":"u","prob":1.26E-4,"start_time":1.54},{"char":"n","prob":0.0,"start_time":1.7},{"char":"a","prob":0.99997,"start_time":1.72},{"char":" ","prob":0.993724,"start_time":2.14},{"char":"o","prob":4.3E-5,"start_time":2.22},{"char":" ","prob":0.99696,"start_time":2.32},{"char":"t","prob":1.4E-5,"start_time":2.36},{"char":"e","prob":3.8E-5,"start_time":2.38},{"char":" ","prob":0.996606,"start_time":2.48},{"char":"t","prob":0.0,"start_time":2.52},{"char":"a","prob":0.994873,"start_time":2.54},{"char":"Å","prob":2.9E-5,"start_time":2.64},{"char":"a","prob":0.994639,"start_time":2.66},{"char":"t","prob":3.0E-6,"start_time":2.8},{"char":"a","prob":0.99995,"start_time":2.82},{"char":" ","prob":0.0,"start_time":2.9},{"char":"m","prob":0.999606,"start_time":2.98},{"char":"Ä","prob":0.010628,"start_time":3.0},{"char":"o","prob":0.996415,"start_time":3.2},{"char":"r","prob":0.0,"start_time":3.28},{"char":"i","prob":0.999698,"start_time":3.32},{"char":" ","prob":0.997927,"start_time":4.5},{"char":"k","prob":0.0,"start_time":4.58},{"char":"o","prob":3.2E-5,"start_time":4.6},{"char":" ","prob":0.956949,"start_time":4.7},{"char":"r","prob":2.5E-5,"start_time":4.74},{"char":"a","prob":0.997649,"start_time":4.76},{"char":"Å","prob":0.0,"start_time":4.9},{"char":"i","prob":1.63E-4,"start_time":4.92},{"char":"n","prob":1.3E-5,"start_time":5.28},{"char":"u","prob":4.0E-6,"start_time":5.3},{"char":"i","prob":0.99997,"start_time":5.42},{"char":" ","prob":0.994305,"start_time":5.78},{"char":"e","prob":3.1E-5,"start_time":5.84},{"char":" ","prob":0.966097,"start_time":6.04},{"char":"t","prob":2.0E-6,"start_time":6.1},{"char":"Å«","prob":5.4E-5,"start_time":6.12},{"char":" ","prob":0.837368,"start_time":6.34},{"char":"n","prob":0.0,"start_time":6.38},{"char":"e","prob":0.004238,"start_time":6.4},{"char":"i","prob":0.99966,"start_time":6.5},{"char":" ","prob":0.99728,"start_time":7.28},{"char":"o","prob":6.28E-4,"start_time":7.36},{"char":" ","prob":0.961049,"start_time":7.48},{"char":"p","prob":5.0E-6,"start_time":7.52},{"char":"a","prob":0.984707,"start_time":7.54},{"char":"p","prob":9.0E-6,"start_time":7.66},{"char":"a","prob":0.998754,"start_time":7.68},{"char":"t","prob":0.002356,"start_time":7.88},{"char":"Å«","prob":1.24E-4,"start_time":7.9},{"char":"Ä","prob":0.883958,"start_time":8.0},{"char":"n","prob":0.985871,"start_time":8.24},{"char":"u","prob":0.907369,"start_time":8.26},{"char":"k","prob":0.999045,"start_time":8.38},{"char":"u","prob":0.995632,"start_time":8.4}],"success":true,"transcription":"kotahi anÅ te tupuna o te tangata mÄori ko ranginui e tÅ« nei o papatÅ«Änuku"}';
-        /* eslint-enable quotes */
-
-        /** @type {String} The URL to which query POST requests should be made. */
-        if (process.env.NODE_ENV !== "production") {
-            this.queryUrl = "//localhost:8383/gs3-koreromaori/transcribe";
-        }
-        else {
-            this.queryUrl = "/gs3-koreromaori/transcribe";
-        }
-
-        /** @type {Number} The maximum number of files which can be transcribed in one request to the API. */
-        this.MAX_BATCH_COUNT = 3;
-
-        /** @type {Number} The soft upper limit on how many bytes can be submitted in one request. */
-        this.BATCH_BYTE_LIMIT = 5242880; // 5 MiB
-    }
-
-    /**
-     * Performs chunked queries to transcribe the given audio files, returning the data in iterations.
-     * Data is chunked according to which ever occurs first:
-     * A maximum of three files per request, or;
-     * A maximum of 5 MiB before chunking.
-     *
-     * @param {FileList | File[]} files The files to upload
-     * @returns {AsyncGenerator<TranscriptionModel[]>} The transcribed audio files.
-     * @throws {TranscriptionError} When the transcription request fails to complete.
-     */
-    async* batchTranscribeFiles(files) {
-        let filesToSubmit = [];
-        let fileCounter = 0;
-        let byteCounter = 0;
-
-        for (const file of files) {
-            if (fileCounter === this.MAX_BATCH_COUNT || byteCounter > this.BATCH_BYTE_LIMIT) { // 5 MiB
-                yield await this.transcribeFiles(filesToSubmit);
-                filesToSubmit = [];
-                byteCounter = 0;
-                fileCounter = 0;
-            }
-
-            filesToSubmit[fileCounter++] = file;
-            byteCounter += file.size;
-        }
-
-        if (filesToSubmit.length > 0) {
-            yield await this.transcribeFiles(filesToSubmit);
-        }
-    }
-
-    /**
-     * Performs a query to transcribe the given audio files.
-     *
-     * @param {File[]} files The files to upload.
-     * @returns {Promise<TranscriptionModel[]>} The transcribed audio file.
-     * @throws {TranscriptionError} When the transcription request fails to complete.
-     */
-    async transcribeFiles(files) {
-        if (process.env.NODE_ENV !== "production" || files.some(file => file.name === "akl_mi_pk_0002_offline.wav")) {
-            return await this.transcribeFilesTest(files);
-        }
-        else {
-            return await this.transcribeFilesActual(files);
-        }
-    }
-
-    async transcribeFilesTest(files) {
-        const objects = [];
-
-        for (const file of files) {
-            const object = JSON.parse(this.TEST_JSON);
-            object.file_name = file.name;
-            objects.push(object);
-        }
-
-        return objects;
-    }
-
-    /**
-     * Performs a query to transcribe the given audio files.
-     *
-     * @param {FileList | File[]} files The files to upload.
-     * @returns {Promise<TranscriptionModel[]>} The transcribed audio file.
-     * @throws {TranscriptionError} When the transcription request fails to complete.
-     */
-    async transcribeFilesActual(files) {
-        const that = this;
-        const formData = new FormData();
-
-        let audioFileKeys = "";
-        for (let i = 0; i < files.length; i++) {
-            const f = files[i];
-            const key = "audioFile" + i;
-
-            formData.append(key, f, f.name);
-            audioFileKeys += key + "|";
-        }
-        formData.append("audioFileKeys", audioFileKeys);
-
-        try {
-            const response = await fetch(
-                that.queryUrl,
-                {
-                    method: "POST",
-                    body: formData
-                }
-            );
-
-            if (!response.ok) {
-                log(`Transcription API failed with status ${response.status} and message ${response.statusText}`, "error")
-                throw new TranscriptionError(response.statusText, undefined, response.status);
-            }
-
-            return await response.json();
-        }
-        catch (e) {
-            log(`Transcription failed with reason ${e}`, "error");
-            throw new TranscriptionError(undefined, "Unknown");
-        }
-    }
-}
