Initial commit

This commit is contained in:
pluja
2023-08-26 10:36:05 +02:00
commit a3543ee13a
76 changed files with 6165 additions and 0 deletions

View File

@@ -0,0 +1,282 @@
<script>
import { onMount, onDestroy } from 'svelte';
import { writable } from 'svelte/store';
import toast from 'svelte-french-toast';
import { editorSettings, currentTranscription, editorHistory } from '$lib/stores';
import EditorSettings from './EditorSettings.svelte';
import EditorSegment from './EditorSegment.svelte';
import { CLIENT_API_HOST } from '$lib/utils';
let language = writable('original');
// Segments lazy loading
let segmentsToShow = 20;
function loadMore() {
segmentsToShow += 10;
}
let loadMoreButton;
async function textFromSegments() {
let text = '';
if ($language == 'original') {
text = $currentTranscription.result.segments
.map((segment) => segment.text)
.join(' ')
.replace(/(\r\n|\n|\r)/gm, ' ');
} else {
text = $currentTranscription.translations
.filter((translation) => translation.targetLanguage == $language)[0]
.result.segments.map((segment) => segment.text)
.join(' ')
.replace(/(\r\n|\n|\r)/gm, ' ');
}
console.log(text);
return text;
}
async function saveChanges() {
var url = `${CLIENT_API_HOST}/api/transcriptions`; // replace with your actual endpoint
console.log($language)
// Update text to match segments
if ($language == 'original') {
$currentTranscription.result.text = await textFromSegments();
} else {
$currentTranscription.translations.forEach(async (translation) => {
if (translation.targetLanguage == $language) translation.result.text = await textFromSegments();
});
}
try {
const response = await fetch(url, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify($currentTranscription)
});
if (!response.ok) {
if (response.status === 304) {
if (!$editorSettings.autoSave) {
toast('No changes were made!', {
icon: '👐'
});
}
return;
} else {
toast.error("Couldn't save!");
throw new Error(`HTTP error! status: ${response.status}`);
}
}
if ($editorSettings.autoSave) {
toast('Autosaving...', { icon: 'ℹ️' });
} else {
toast.success('Saved!');
}
} catch (error) {
toast.error("Couldn't save!");
console.error('Error:', error);
}
}
let handleKeyDown;
let observer;
onMount(() => {
// Lazy loading
observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
loadMore();
}
});
});
observer.observe(loadMoreButton);
// Function to handle Ctrl+Z and Ctrl+S shortcuts
editorHistory.set([JSON.parse(JSON.stringify($currentTranscription))]);
let isUndoing = false;
handleKeyDown = function (e) {
// Undo (CTRL+Z)
if (e.ctrlKey && e.key === 'z' && !isUndoing) {
isUndoing = true;
let previousTranscription = null;
editorHistory.update((history) => {
if (history.length > 1) {
history = history.slice(0, -1);
previousTranscription = { ...history[history.length - 1] };
}
return history;
});
if (previousTranscription) {
$currentTranscription = { ...previousTranscription };
}
isUndoing = false;
}
let isSaving = false;
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
if (!isSaving) {
isSaving = true;
saveChanges();
isSaving = false;
}
}
};
if (!$editorSettings.autoSave) {
toast('Autosave is disabled.', {
icon: '👋'
});
}
// Listen to keydown event
document.addEventListener('keydown', handleKeyDown);
});
// Autosave
let autosaveInterval;
let autoSaveAux = $editorSettings.autoSave;
$: if ($editorSettings.autoSave) {
toast.success('Autosave enabled.');
autoSaveAux = true;
autosaveInterval = setInterval(() => {
saveChanges();
}, $editorSettings.autosaveInterval);
} else {
if (autoSaveAux == true) {
toast('Autosave is disabled.', {
icon: '👋'
});
autoSaveAux = false;
}
clearInterval(autosaveInterval);
}
</script>
{#if $currentTranscription.status != 2}
<div class="flex items-center justify-center">
<span class="loading loading-spinner loading-lg"></span>
<p class="text-center">Waiting for task to finish {$currentTranscription.status == 3 ? "translating" : "transcribing"}...</p>
</div>
{:else}
<div class="flex flex-col items-center break-words">
<h1 class="text-center text-2xl mt-8 break-words">
{$currentTranscription.fileName.split('_WHSHPR_')[1]}
</h1>
<!-- Menu -->
<ul class="menu menu-horizontal bg-base-200 rounded-box mt-6">
<li>
<a href="/" class="tooltip" data-tip="Home">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
/></svg
>
</a>
</li>
<li>
<button on:click={saveChanges} class="tooltip" data-tip="Save (Ctrl+S)">
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-device-floppy"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M6 4h10l4 4v10a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2" />
<path d="M12 14m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path d="M14 4l0 4l-6 0l0 -4" />
</svg>
</button>
</li>
</ul>
<!-- End Menu -->
{#if $currentTranscription.translations.length > 0}
<div class="form-control max-w-xs my-4">
<label for="language" class="label">
<span class="label-text">Subtitles language</span>
</label>
<select
bind:value={$language}
name="language"
class="select select-sm select-bordered uppercase"
>
<option value="original">✅ {$currentTranscription.result.language}</option>
{#each $currentTranscription.translations as translation}
<option value={translation.targetLanguage}>🤖 {translation.targetLanguage}</option>
{/each}
</select>
</div>
{/if}
</div>
<div class="mt-4">
<!-- Editor configuration -->
<EditorSettings />
<!-- End Editor configuration -->
<div class="overflow-x-auto">
<!-- Segments table -->
<table class="table px-4">
<thead>
<tr>
<th />
<th>Start</th>
<th>End</th>
<th>Text</th>
<th>Info</th>
<th />
</tr>
</thead>
<tbody>
{#if $language == 'original'}
{#each $currentTranscription.result.segments.slice(0, segmentsToShow) as segment, index (segment.id)}
<EditorSegment {segment} {index} translationIndex={-1} />
{/each}
{:else}
{#each $currentTranscription.translations as translation, translationIndex}
{#if translation.targetLanguage == $language}
{#each translation.result.segments.slice(0, segmentsToShow) as segment, index (segment.id)}
<EditorSegment {segment} {index} {translationIndex} />
{/each}
{/if}
{/each}
{/if}
</tbody>
</table>
<button bind:this={loadMoreButton}>
{#if $language == 'original'}
{#if segmentsToShow >= $currentTranscription.result.segments.length}
No more segments to load
{:else}
Loading more...
{/if}
{:else if segmentsToShow >= $currentTranscription.translations.filter((translation) => translation.targetLanguage == $language)[0].result.segments.length}
No more segments to load
{:else}
Loading more...
{/if}
</button>
<!-- End Segments table -->
</div>
</div>
{/if}

View File

@@ -0,0 +1,354 @@
<script>
import { editorSettings } from '$lib/stores';
import { currentVideoPlayerTime, currentTranscription, editorHistory } from '$lib/stores';
export let index;
export let segment;
export let translationIndex;
let isActive = false;
function getCps(s) {
// Find duration
let duration = s.end - s.start;
// Count characters
let charCount = s.text.length;
// Calculate CPS
let cps = charCount / duration;
// Round to 2 decimals
cps = Math.round(cps * 100) / 100;
// Truncate to integer
return Math.trunc(cps);
}
function deleteSegment(index, callback) {
console.log($currentTranscription)
const source =
translationIndex == -1
? $currentTranscription.result.segments
: $currentTranscription.translations[translationIndex].result.segments;
source.splice(index, 1);
// Update index
$currentTranscription = { ...$currentTranscription }; // deep copy
callback();
}
// This function takes a segment and splits it into two segments at the given index
function splitSegment(index, callback) {
// Choose the correct source based on translationIndex
const source =
translationIndex == -1
? $currentTranscription.result.segments
: $currentTranscription.translations[translationIndex].result.segments;
const segment = source[index];
const words = segment.text.split(' ');
const half = Math.ceil(words.length / 2);
const firstHalf = words.slice(0, half).join(' ');
const secondHalf = words.slice(half).join(' ');
const duration = segment.end - segment.start;
const midTime = segment.start + duration / 2;
// Update current segment
segment.text = firstHalf;
segment.end = midTime;
// Create and insert new segment
const newSegment = {
id: JSON.stringify(Date.now()),
start: midTime,
end: segment.end + duration / 2,
text: secondHalf,
words: []
};
source.splice(index + 1, 0, newSegment);
$currentTranscription = { ...$currentTranscription };
callback();
}
// Text changes only save after 6 keystrokes
let keystrokes = 0;
function handleKeystrokes() {
keystrokes++;
if (keystrokes > 6) {
handleHistory();
keystrokes = 0;
}
}
// Save history on editing
function handleHistory() {
let currentT = JSON.parse(JSON.stringify($currentTranscription));
editorHistory.update((value) => {
return [...value, currentT];
});
}
function insertSegmentAbove(index, callback) {
const source =
translationIndex == -1
? $currentTranscription.result.segments
: $currentTranscription.translations[translationIndex].result.segments;
source.splice(index, 0, {
id: JSON.stringify(Date.now()),
start: 0,
end: 0,
text: '',
words: []
});
$currentTranscription = { ...$currentTranscription }; // deep copy
callback();
}
function insertSegmentBelow(index, callback) {
const source =
translationIndex == -1
? $currentTranscription.result.segments
: $currentTranscription.translations[translationIndex].result.segments;
source.splice(index + 1, 0, {
id: JSON.stringify(Date.now()),
start: 0,
end: 0,
text: '',
words: []
});
$currentTranscription = { ...$currentTranscription }; // deep copy
callback();
}
$: if (segment.start <= $currentVideoPlayerTime && $currentVideoPlayerTime <= segment.end) {
isActive = true;
} else {
isActive = false;
}
</script>
<tr class:bg-warning={isActive} class:bg-opacity-30={isActive} data-start={segment.start}>
<th>{index}</th>
<td class="space-x-2">
<!-- Start input -->
<input
class="w-20 input input-sm input-bordered"
type="number"
step="0.01"
bind:value={segment.start}
on:input={(e) => ($currentVideoPlayerTime.set(e.target.value))}
on:input={handleHistory}
on:click={(e) => {
if ($editorSettings.seekOnClick) $currentVideoPlayerTime = e.target.value;
}}
/>
<span class="tooltip" data-tip="Set to current time">
<button
on:click={() => {
segment.start = $currentVideoPlayerTime;
handleHistory();
}}
class="btn btn-xs btn-primary"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-4 h-4 stroke-white"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 7v5l2 2" />
<path d="M17 22l5 -3l-5 -3z" />
<path d="M13.017 20.943a9 9 0 1 1 7.831 -7.292" />
</svg>
</button>
</span>
</td>
<td class="space-x-2">
<!-- End input -->
<input
class="w-20 input input-sm input-bordered"
type="number"
step="0.01"
bind:value={segment.end}
on:input={(e) => ($currentVideoPlayerTime = e.target.value)}
on:input={handleHistory}
on:click={(e) => {
if ($editorSettings.seekOnClick) $currentVideoPlayerTime = e.target.value;
}}
/>
<span class="tooltip" data-tip="Set to current time">
<button
on:click={() => {
segment.end = $currentVideoPlayerTime;
handleHistory();
}}
class="btn btn-xs btn-primary"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-4 h-4 stroke-white"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 7v5l2 2" />
<path d="M17 22l5 -3l-5 -3z" />
<path d="M13.017 20.943a9 9 0 1 1 7.831 -7.292" />
</svg>
</button>
</span>
</td>
<td>
<!-- Text input -->
<div
bind:textContent={segment.text}
on:input={handleKeystrokes}
class="border-2 font-mono font-bold rounded-lg p-3 max-w-md bg-base-100"
class:border-error={getCps(segment) > 16}
contenteditable="true"
/>
</td>
<td>
<div>
<span class="flex flex-col text-xs flex-grow">
<span class:text-error={getCps(segment) > 16}>
<span class="whitespace-nowrap font-mono font-bold">
CPS: {getCps(segment)}
</span>
</span>
<span>
<span class="whitespace-nowrap font-mono font-bold">
Duration: {Math.round((segment.end - segment.start) * 100) / 100}s
</span>
</span>
</span>
</div>
</td>
<td
class="space-x-1 flex flex-col items-center justify-center space-y-2 md:space-y-0 align-middle lg:flex-row"
>
<!-- Add above -->
<span class="tooltip" data-tip="Insert Above">
<button
on:click={() => insertSegmentAbove(index, handleHistory)}
class="btn btn-primary btn-xs md:btn-sm btn-square"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-5 h-5"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M20 6v4a1 1 0 0 1 -1 1h-14a1 1 0 0 1 -1 -1v-4a1 1 0 0 1 1 -1h14a1 1 0 0 1 1 1z"
/>
<path d="M12 15l0 4" />
<path d="M14 17l-4 0" />
</svg>
</button>
</span>
<!-- Add below -->
<span class="tooltip" data-tip="Insert Below">
<button
on:click={() => insertSegmentBelow(index, handleHistory)}
class="btn btn-primary btn-xs md:btn-sm btn-square"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-5 h-5"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M4 18v-4a1 1 0 0 1 1 -1h14a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-14a1 1 0 0 1 -1 -1z"
/>
<path d="M12 9v-4" />
<path d="M10 7l4 0" />
</svg>
</button>
</span>
<!-- Split -->
<span class="tooltip" data-tip="Split segment">
<button
on:click={() => splitSegment(index, handleHistory)}
class="btn btn-primary btn-xs md:btn-sm btn-square"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-arrows-split"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M21 17h-8l-3.5 -5h-6.5" />
<path d="M21 7h-8l-3.495 5" />
<path d="M18 10l3 -3l-3 -3" />
<path d="M18 20l3 -3l-3 -3" />
</svg>
</button>
</span>
<!-- Delete -->
<button
on:click={deleteSegment(index, handleHistory)}
class="btn btn-error btn-xs md:btn-sm btn-square"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-5 h-5"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M20 6a1 1 0 0 1 .117 1.993l-.117 .007h-.081l-.919 11a3 3 0 0 1 -2.824 2.995l-.176 .005h-8c-1.598 0 -2.904 -1.249 -2.992 -2.75l-.005 -.167l-.923 -11.083h-.08a1 1 0 0 1 -.117 -1.993l.117 -.007h16zm-9.489 5.14a1 1 0 0 0 -1.218 1.567l1.292 1.293l-1.292 1.293l-.083 .094a1 1 0 0 0 1.497 1.32l1.293 -1.292l1.293 1.292l.094 .083a1 1 0 0 0 1.32 -1.497l-1.292 -1.293l1.292 -1.293l.083 -.094a1 1 0 0 0 -1.497 -1.32l-1.293 1.292l-1.293 -1.292l-.094 -.083z"
stroke-width="0"
fill="currentColor"
/>
<path
d="M14 2a2 2 0 0 1 2 2a1 1 0 0 1 -1.993 .117l-.007 -.117h-4l-.007 .117a1 1 0 0 1 -1.993 -.117a2 2 0 0 1 1.85 -1.995l.15 -.005h4z"
stroke-width="0"
fill="currentColor"
/>
</svg>
</button>
</td>
</tr>

View File

@@ -0,0 +1,22 @@
<script>
import { editorSettings } from '$lib/stores';
</script>
<div class="flex flex-row items-center justify-center p-3 space-x-4 m-2">
<div
class="form-control tooltip"
data-tip="When clicking a timestamp input, seek the video to that position."
>
<label class="label cursor-pointer space-x-2">
<span class="label-text">Seek on click</span>
<input bind:checked={$editorSettings.seekOnClick} type="checkbox" class="checkbox checkbox-sm" />
</label>
</div>
<div class="form-control tooltip" data-tip="Automatically save changes every 30s.">
<label class="label cursor-pointer space-x-2">
<span class="label-text">Autosave</span>
<input bind:checked={$editorSettings.autoSave} type="checkbox" class="checkbox checkbox-sm" />
</label>
</div>
</div>

View File

@@ -0,0 +1,16 @@
<script>
import {deleteTranscription} from "$lib/utils.js";
export let tr;
</script>
<div class="alert alert-error p-3">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<span>
<p class="font-bold text-info-content text-md">id-{tr.id}</p>
<p class="font-mono text-info-content text-sm opacity-70">Failed: Could not transcribe.</p>
</span>
<div>
<button on:click={deleteTranscription(tr.id)} class="btn btn-sm">Delete</button>
</div>
</div>

View File

@@ -0,0 +1,161 @@
<script>
import {downloadSRT, downloadTXT, downloadJSON, downloadVTT, CLIENT_API_HOST} from '$lib/utils';
import toast from 'svelte-french-toast';
export let tr;
let subtitleFormat = "srt";
let language = "original";
function downloadSubtitle() {
console.log("download subtitle");
let segments = [];
let text = "";
console.log(language)
if (language == "original") {
segments = tr.result.segments;
text = tr.result.text;
} else {
for (const translation of tr.translations) {
if (translation.targetLanguage == language) {
segments = translation.result.segments;
text = translation.result.text;
break;
}
}
}
if (segments.length == 0 || text == "") {
toast.error("No data available for download");
return;
}
if (subtitleFormat == "srt") {
downloadSRT(segments);
} else if (subtitleFormat == "vtt") {
downloadVTT(segments);
} else if (subtitleFormat == "json") {
downloadJSON(tr.result);
} else if (subtitleFormat == "txt") {
downloadTXT(text);
}
}
function downloadMedia() {
let link = document.createElement('a');
link.href = `${CLIENT_API_HOST}/api/video/${tr.fileName}`;
link.target = "_blank";
link.download = tr.fileName.split('_WHSHPR_')[1]+".mp4";
link.click();
}
async function copyText() {
console.log("copy text");
let text = "";
if (language == "original") {
text = tr.result.text;
} else {
for (const translation of tr.translations) {
console.log(translation.targetLanguage == language)
if (translation.targetLanguage == language) {
text = translation.result.text;
break;
}
}
}
// Copy tr.result.text to clipboard
try {
await navigator.clipboard.writeText(text);
console.log('Text copied to clipboard');
toast.success('Text copied to clipboard');
} catch (err) {
console.error('Error in copying text: ', err);
}
}
</script>
<dialog id="modalDownloadOptions" class="modal">
<form method="dialog" class="modal-box flex flex-col items-center justify-center">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
{#if tr}
<h1 class="text-center font-bold mt-2 pb-2">Download Options</h1>
<div class="flex flex-row space-x-4">
<div class="form-control">
<label for="format" class="label">
<span class="label-text font-bold">File Format</span>
</label>
<select bind:value={subtitleFormat} name="format" class="select select-bordered w-full max-w-xs">
<option value="srt">SRT</option>
<option value="vtt">VTT</option>
<option value="json">JSON</option>
<option value="txt">TXT</option>
</select>
</div>
<div class="form-control">
<label for="language" class="label">
<span class="label-text font-bold">Text Language</span>
</label>
<select bind:value={language} name="language" class="select select-bordered w-full max-w-xs uppercase">
<option value="original">✅ {tr.result.language}</option>
{#each tr.translations as translation}
<option value="{translation.targetLanguage}">🤖 {translation.targetLanguage}</option>
{/each}
</select>
</div>
</div>
<div class="space-x-2 mt-8">
<span class="tooltip" data-tip="Download {subtitleFormat} file in {language}.">
<button on:click={downloadSubtitle} class="btn btn-sm btn-success">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-download" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2"></path>
<path d="M7 11l5 5l5 -5"></path>
<path d="M12 4l0 12"></path>
</svg>
<span>
File
</span>
</button>
</span>
<span class="tooltip" data-tip="Copy raw text in '{language}' language.">
<button on:click={copyText} class="btn btn-sm btn-info">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-copy" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path>
<path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path>
</svg>
<span>
Copy
</span>
</button>
</span>
<span class="tooltip" data-tip="Download source media">
<button on:click={downloadMedia} class="btn btn-sm btn-error">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-download" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"></path>
<path d="M12 17v-6"></path>
<path d="M9.5 14.5l2.5 2.5l2.5 -2.5"></path>
</svg>
<span>
Media
</span>
</button>
</span>
</div>
{:else}
<div class="flex items-center justify-center w-screen h-screen">
<h1>
<span class="loading loading-bars loading-lg" />
</h1>
</div>
{/if}
</form>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>

View File

@@ -0,0 +1,183 @@
<script>
import { validateURL, CLIENT_API_HOST } from '$lib/utils.js';
import toast from 'svelte-french-toast';
let errorMessage = '';
let disableSubmit = true;
let modelSize = 'small';
let language = 'auto';
let sourceUrl = '';
let fileInput;
let languages = [
'auto',
'ar',
'be',
'bg',
'bn',
'ca',
'cs',
'cy',
'da',
'de',
'el',
'en',
'es',
'fr',
'it',
'jp',
'nl',
'pl',
'pt',
'ru',
'sk',
'sl',
'sv',
'tk',
'tr',
'zh'
];
let models = [
'tiny',
'tiny.en',
'base',
'base.en',
'small',
'small.en',
'medium',
'medium.en',
'large-v2'
];
// Sort the languages
languages.sort((a, b) => {
if (a == 'auto') return -1;
if (b == 'auto') return 1;
return a.localeCompare(b);
});
// Function that sends the data as a form to the backend
async function sendForm() {
if (sourceUrl && !validateURL(sourceUrl)) {
toast.error('You must enter a valid URL.');
return;
}
if (!sourceUrl && !fileInput) {
toast.error('No file or URL.');
return;
}
let formData = new FormData();
formData.append('language', language);
formData.append('modelSize', modelSize);
formData.append('sourceUrl', sourceUrl);
if (sourceUrl == '') {
formData.append('file', fileInput.files[0]);
}
const res = await fetch(`${CLIENT_API_HOST}/api/transcriptions`, {
method: 'POST',
body: formData
});
// Set file and sourceUrl to empty
sourceUrl = '';
fileInput.value = '';
toast.success('Success!');
}
// Reactive statement
$: if (sourceUrl && !validateURL(sourceUrl)) {
errorMessage = 'Enter a valid URL';
disableSubmit = true;
} else {
errorMessage = '';
disableSubmit = false;
}
</script>
<dialog id="modalNewTranscription" class="modal">
<form method="dialog" class="modal-box">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
{#if errorMessage != ''}
<div class="alert alert-error">
<svg
xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/></svg
>
<span>{errorMessage}</span>
</div>
{/if}
<div class="space-y-2 mt-0">
<div class="form-control w-full max-w-xs">
<label for="file" class="label">
<span class="label-text">Pick a file</span>
</label>
<input
name="file"
bind:this={fileInput}
type="file"
class="file-input file-input-sm file-input-bordered file-input-primary w-full max-w-xs"
/>
</div>
<div class="form-control w-full max-w-xs">
<label for="sourceUrl" class="label">
<span class="label-text">Or a source URL</span>
</label>
<input
name="sourceUrl"
bind:value={sourceUrl}
type="text"
placeholder="https://youtube.com/watch?v=Hd33fCdW"
class="input input-sm input-bordered input-primary w-full max-w-xs"
/>
</div>
</div>
<div class="divider mb-0" />
<!-- Whisper Configuration -->
<div class="flex space-x-4">
<div class="form-control w-full max-w-xs">
<label for="modelSize" class="label">
<span class="label-text">Whisper model</span>
</label>
<select name="modelSize" bind:value={modelSize} class="select select-bordered">
{#each models as m}
<option value={m}>{m}</option>
{/each}
</select>
</div>
<div class="form-control w-full max-w-xs">
<label for="language" class="label">
<span class="label-text">Language</span>
</label>
<select name="language" bind:value={language} class="select select-bordered">
{#each languages as l}
<option value={l}>{l}</option>
{/each}
</select>
</div>
</div>
<div class="divider mb-0" />
<!--Actions-->
<button class="btn btn-wide btn-primary" on:click={sendForm} disabled={disableSubmit}
>Start</button
>
</form>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>

View File

@@ -0,0 +1,85 @@
<script>
import { onMount } from 'svelte';
import toast from 'svelte-french-toast';
import { CLIENT_API_HOST } from '$lib/utils';
import { PUBLIC_TRANSLATION_API_HOST } from '$env/static/public';
export let tr;
let targetLanguage = null;
let availableLanguages = [];
const getAvailableLangs = () => {
const fetchLanguages = () => {
fetch(`${PUBLIC_TRANSLATION_API_HOST}/languages`)
.then(res => res.json())
.then(data => {
if (data) {
availableLanguages = data;
console.log(availableLanguages)
// Languages fetched successfully, stop trying
clearInterval(fetchLanguagesInterval);
}
});
};
// Fetch languages repeatedly until successful
const fetchLanguagesInterval = setInterval(fetchLanguages, 5000);
fetchLanguages();
};
const handleTranslate = (id) => {
if(targetLanguage) {
const url = `${CLIENT_API_HOST}/api/translate/${id}/${targetLanguage}`;
fetch(url)
.then(() => toast.success('Translation started!'))
.catch(error => {
console.error(error);
toast.error('Error translating text!')
});
}
}
onMount(async () => {
await getAvailableLangs();
});
</script>
<dialog id="modalTranslation" class="modal">
<form method="dialog" class="modal-box flex flex-col items-center justify-center">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
{#if tr}
<h1 class="text-center font-bold mt-2 pb-2">
Translate
</h1>
<div>
<!-- Language picker -->
<div class="form-control w-full max-w-xs">
<label for="target-lan" class="label">
<span class="label-text">Target language</span>
</label>
<select bind:value={targetLanguage} name="target-lan" class="select select-bordered">
<option disabled selected>Pick one</option>
<!-- Iterate all available languages -->
{#each availableLanguages as lan}
<!-- When we find the source language -->
{#if lan.code == tr.result.language}
<!-- Iterate all possible target languages -->
{#each lan.targets as t}
{#if t != tr.result.language}
<option value="{t}">{t}</option>
{/if}
{/each}
{/if}
{/each}
</select>
</div>
<!-- End language picker-->
<button on:click={handleTranslate(tr.id)} class="btn btn-active btn-primary mt-5">Translate</button>
</div>
{/if}
</form>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>

View File

@@ -0,0 +1,11 @@
<script>
export let tr;
</script>
<div class="alert alert-info p-3">
<span class="loading loading-ring loading-md"></span>
<span>
<p class="font-bold text-info-content text-md">{tr.fileName != "" ? tr.fileName.split("_WHSHPR_")[1] : tr.id}</p>
<p class="font-mono text-info-content text-sm opacity-60">Waiting for transcription...</p>
</span>
</div>

View File

@@ -0,0 +1,25 @@
<script>
export let tr;
</script>
<div class="alert alert-info p-3">
<span class="loading loading-dots loading-md"></span>
<span>
<p class="font-bold text-info-content text-md flex items-center justify-center space-x-2">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-language-hiragana" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4 5h7"></path>
<path d="M7 4c0 4.846 0 7 .5 8"></path>
<path d="M10 8.5c0 2.286 -2 4.5 -3.5 4.5s-2.5 -1.135 -2.5 -2c0 -2 1 -3 3 -3s5 .57 5 2.857c0 1.524 -.667 2.571 -2 3.143"></path>
<path d="M12 20l4 -9l4 9"></path>
<path d="M19.1 18h-6.2"></path>
</svg>
<span>{tr.fileName != "" ? tr.fileName.split("_WHSHPR_")[1] : tr.id}</span>
</p>
<p class="opacity-60">
<span class="font-mono text-info-content text-sm ">
Waiting for translation...
</span>
</p>
</span>
</div>

View File

@@ -0,0 +1,75 @@
<!-- SuccessTranscription.svelte -->
<script>
import {createEventDispatcher} from 'svelte';
import {deleteTranscription} from "$lib/utils.js";
export let tr;
const dispatch = createEventDispatcher();
let download = () => {
dispatch('download', tr); // emit a custom event with the transcription as detail
}
let translate = () => {
dispatch('translate', tr); // emit a custom event with the transcription as detail
}
</script>
<div class="alert alert-success p-3">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<span>
<p class="font-bold text-info-content text-md">{tr.fileName.split("_WHSHPR_")[1]}</p>
<p class="font-mono text-info-content text-sm opacity-60 flex space-x-2 md:space-x-4 lg:space-x-8">
<span class="space-x-1">
<span class="font-bold text-xs">~{Math.round(tr.result.duration)}s long</span>
</span>
<span class="space-x-1">
<span class="font-bold text-xs">{tr.translations.length} translations</span>
</span>
<span class="space-x-1">
<span class="font-bold text-xs">{tr.result.text.split(" ").length} words</span>
</span>
</p>
</span>
<div class="flex items-center justify-center flex-wrap space-x-2">
<a href="/editor/{tr.id}" class="btn btn-xs md:btn-sm">
<span class="tooltip flex items-center justify-center" data-tip="Edit">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 15l8.385 -8.415a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3z"></path>
<path d="M16 5l3 3"></path>
<path d="M9 7.07a7 7 0 0 0 1 13.93a7 7 0 0 0 6.929 -6"></path>
</svg>
</span>
</a>
<button on:click={download} onclick="modalDownloadOptions.showModal()" class="btn btn-xs md:btn-sm">
<span class="tooltip flex items-center justify-center" data-tip="Download">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2"></path>
<path d="M7 11l5 5l5 -5"></path>
<path d="M12 4l0 12"></path>
</svg>
</span>
</button>
<button on:click={translate} class="btn btn-xs md:btn-sm btn-primary">
<span class="tooltip flex items-center justify-center" data-tip="Translate">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4 5h7"></path>
<path d="M7 4c0 4.846 0 7 .5 8"></path>
<path d="M10 8.5c0 2.286 -2 4.5 -3.5 4.5s-2.5 -1.135 -2.5 -2c0 -2 1 -3 3 -3s5 .57 5 2.857c0 1.524 -.667 2.571 -2 3.143"></path>
<path d="M12 20l4 -9l4 9"></path>
<path d="M19.1 18h-6.2"></path>
</svg>
</span>
</button>
<button on:click={deleteTranscription(tr.id)} class="btn btn-xs md:btn-sm btn-error">
<span class="tooltip flex items-center justify-center" data-tip="Delete">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M20 6a1 1 0 0 1 .117 1.993l-.117 .007h-.081l-.919 11a3 3 0 0 1 -2.824 2.995l-.176 .005h-8c-1.598 0 -2.904 -1.249 -2.992 -2.75l-.005 -.167l-.923 -11.083h-.08a1 1 0 0 1 -.117 -1.993l.117 -.007h16zm-9.489 5.14a1 1 0 0 0 -1.218 1.567l1.292 1.293l-1.292 1.293l-.083 .094a1 1 0 0 0 1.497 1.32l1.293 -1.292l1.293 1.292l.094 .083a1 1 0 0 0 1.32 -1.497l-1.292 -1.293l1.292 -1.293l.083 -.094a1 1 0 0 0 -1.497 -1.32l-1.293 1.292l-1.293 -1.292l-.094 -.083z" stroke-width="0" fill="currentColor"></path>
<path d="M14 2a2 2 0 0 1 2 2a1 1 0 0 1 -1.993 .117l-.007 -.117h-4l-.007 .117a1 1 0 0 1 -1.993 -.117a2 2 0 0 1 1.85 -1.995l.15 -.005h4z" stroke-width="0" fill="currentColor"></path>
</svg>
</span>
</button>
</div>
</div>