feat(app): added downloading files

This commit is contained in:
Thijs Houben 2024-05-30 16:37:44 +02:00
parent 935b6f47bc
commit e9a2b8902b
4 changed files with 219 additions and 185 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
.apikeys.env
kernel/.coverage
app/.pnpm-store

View file

@ -67,6 +67,7 @@
"tailwind-merge": "^2.3.0",
"tailwind-variants": "^0.2.1",
"wavesurfer.js": "^7.7.14",
"webm-to-mp4": "^1.0.0",
"zod": "^3.23.8"
},
"packageManager": "pnpm@9.1.1+sha512.14e915759c11f77eac07faba4d019c193ec8637229e62ec99eefb7cf3c3b75c64447882b7c485142451ee3a6b408059cdfb7b7fa0341b975f12d0f7629c71195"

16
app/pnpm-lock.yaml generated
View file

@ -71,6 +71,9 @@ importers:
wavesurfer.js:
specifier: ^7.7.14
version: 7.7.14
webm-to-mp4:
specifier: ^1.0.0
version: 1.0.0
zod:
specifier: ^3.23.8
version: 3.23.8
@ -2094,6 +2097,9 @@ packages:
fastq@1.17.1:
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
ffmpeg.js@4.2.9003:
resolution: {integrity: sha512-l1JBr8HwnnJEaSwg5p8K3Ifbom8O2IDHsZp7UVyr6MzQ7gc32tt/2apoOuQAr/j76c+uDOjla799VSsBnRvSTg==}
file-entry-cache@6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0}
@ -3482,6 +3488,10 @@ packages:
wavesurfer.js@7.7.14:
resolution: {integrity: sha512-sbd48yHnOVDEbZwsnD3dWzBj4SF2q2rsPysmPE8spoR2XwKVkU/POtZg/L0wPi6ypqXb7brQLftSeOovJNToPQ==}
webm-to-mp4@1.0.0:
resolution: {integrity: sha512-XCqDsNF9QoUxhsDI2bL+IyAjU+O/u80G8pojnpzMqKVgqIj9+++G8jSzNwMqlTRWP1nRnuVhMIiu3Kszs+PAbQ==}
engines: {node: '>=10'}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
@ -5381,6 +5391,8 @@ snapshots:
dependencies:
reusify: 1.0.4
ffmpeg.js@4.2.9003: {}
file-entry-cache@6.0.1:
dependencies:
flat-cache: 3.2.0
@ -6736,6 +6748,10 @@ snapshots:
wavesurfer.js@7.7.14: {}
webm-to-mp4@1.0.0:
dependencies:
ffmpeg.js: 4.2.9003
which@2.0.2:
dependencies:
isexe: 2.0.0

View file

@ -1,199 +1,215 @@
<script lang="ts">
import JSZip from 'jszip';
import JSZip from 'jszip';
import webmToMp4 from 'webm-to-mp4';
let mediaStream: MediaStream;
let mediaRecorder: MediaRecorder;
let recordedChunks: Blob[] = [];
let videoURL: string = '';
let isRecording: boolean = false;
let recorded: boolean = false;
let prompts: string[]|null = null;
let promptIds: string[]|null = null
let recordings: (string|null)[]|null = null
let promptIndex = 0;
let mediaStream: MediaStream;
let mediaRecorder: MediaRecorder;
let recordedChunks: Blob[] = [];
let videoURL: string = '';
let isRecording: boolean = false;
let recorded: boolean = false;
let prompts: string[] | null = null;
let promptIds: string[] | null = null;
let recordings: (string | null)[] | null = null;
let promptIndex = 0;
let fileName: string = '';
// Function to handle the file input change event
function handleFileUpload(event: Event): void {
const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
const file = input.files[0];
if (file.type === 'text/plain') {
const reader = new FileReader();
reader.onload = () => {
let fileContent = reader.result as string;
promptIndex = 0;
prompts = [];
promptIds = [];
recordings = [];
console.log(fileContent)
for(let line of fileContent.split(/\r?\n/)) {
console.log(line)
let indexSplit = line.indexOf(" ");
promptIds.push(line.substring(0,indexSplit));
prompts.push(line.substring(indexSplit));
recordings.push(null)
}
};
reader.readAsText(file);
} else {
alert('Please upload a valid .txt file.');
}
}
}
// Function to handle the file input change event
function handleFileUpload(event: Event): void {
const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
const file = input.files[0];
fileName = file.name;
if (file.type === 'text/plain') {
const reader = new FileReader();
reader.onload = () => {
let fileContent = reader.result as string;
promptIndex = 0;
prompts = [];
promptIds = [];
recordings = [];
console.log(fileContent);
for (let line of fileContent.split(/\r?\n/)) {
console.log(line);
let indexSplit = line.indexOf(' ');
promptIds.push(line.substring(0, indexSplit));
prompts.push(line.substring(indexSplit));
recordings.push(null);
}
};
reader.readAsText(file);
} else {
alert('Please upload a valid .txt file.');
}
}
}
// Start the video and audio stream and recorder
async function startRecording(): Promise<void> {
try {
mediaStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
const videoPreview = document.querySelector<HTMLVideoElement>('#video-preview');
if (videoPreview) {
videoPreview.srcObject = mediaStream;
videoPreview.muted = true; // Mute the audio during recording
}
mediaRecorder = new MediaRecorder(mediaStream, { mimeType: 'video/webm' });
// Start the video and audio stream and recorder
async function startRecording(): Promise<void> {
try {
mediaStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
const videoPreview = document.querySelector<HTMLVideoElement>('#video-preview');
if (videoPreview) {
videoPreview.srcObject = mediaStream;
videoPreview.muted = true; // Mute the audio during recording
}
mediaRecorder = new MediaRecorder(mediaStream, { mimeType: 'video/webm' });
mediaRecorder.ondataavailable = (event: BlobEvent) => {
if (event.data.size > 0) {
recordedChunks.push(event.data);
}
};
mediaRecorder.ondataavailable = (event: BlobEvent) => {
if (event.data.size > 0) {
recordedChunks.push(event.data);
}
};
mediaRecorder.onstop = () => {
const blob = new Blob(recordedChunks, { type: 'video/webm' });
videoURL = URL.createObjectURL(blob);
if(recordings!==null){
recordings[promptIndex] = videoURL;
}
recordedChunks = [];
recorded = true;
};
mediaRecorder.onstop = () => {
const blob = new Blob(recordedChunks, { type: 'video/webm' });
videoURL = URL.createObjectURL(blob);
if (recordings !== null) {
recordings[promptIndex] = videoURL;
}
recordedChunks = [];
recorded = true;
};
mediaRecorder.start();
isRecording = true;
recorded = false; // Reset the recorded state
} catch (err) {
console.error('Error accessing media devices.', err);
}
}
mediaRecorder.start();
isRecording = true;
recorded = false; // Reset the recorded state
} catch (err) {
console.error('Error accessing media devices.', err);
}
}
// Stop the video and audio recording
function stopRecording(): void {
if (mediaRecorder && isRecording) {
mediaRecorder.stop();
mediaStream.getTracks().forEach(track => track.stop());
isRecording = false;
}
}
// Stop the video and audio recording
function stopRecording(): void {
if (mediaRecorder && isRecording) {
mediaRecorder.stop();
mediaStream.getTracks().forEach((track) => track.stop());
isRecording = false;
}
}
// Restart recording by resetting recordedChunks and starting a new recording
function restartRecording(): void {
recordedChunks = [];
videoURL = '';
startRecording();
}
// Restart recording by resetting recordedChunks and starting a new recording
function restartRecording(): void {
recordedChunks = [];
videoURL = '';
startRecording();
}
function increaseIndex(): void {
promptIndex++;
}
function increaseIndex(): void {
promptIndex++;
}
function decreaseIndex(): void {
promptIndex--;
}
function decreaseIndex(): void {
promptIndex--;
}
async function downloadAllRecordings() {
if (recordings !== null) {
const zip = new JSZip();
for (let i = 0; i < recordings.length; i++) {
const cur = recordings[i]
if (cur !== null) {
const response = await fetch(cur, {
method: 'GET',
mode: 'cors', // You might need to adjust the mode based on your server's configuration
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/octet-stream'
},
redirect: 'follow',
referrerPolicy: 'no-referrer'
});
const blob = await response.blob();
zip.file(`recording_${i + 1}.webm`, blob);
}
}
zip.generateAsync({ type: 'blob' }).then(function(content:Blob) {
const url = URL.createObjectURL(content);
const a = document.createElement('a');
a.href = url;
a.download = 'all_recordings.zip';
a.click();
URL.revokeObjectURL(url);
});
}
}
</script>
async function downloadAllRecordings() {
if (recordings !== null && promptIds !== null && prompts !== null) {
const zip = new JSZip();
for (let i = 0; i < recordings.length; i++) {
const cur = recordings[i];
if (cur !== null) {
const response = await fetch(cur, {
method: 'GET',
mode: 'cors', // You might need to adjust the mode based on your server's configuration
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/octet-stream'
},
redirect: 'follow',
referrerPolicy: 'no-referrer'
});
const blob = new Blob([await webmToMp4(new Uint8Array(await response.arrayBuffer()))], {
type: 'video/mp4'
});
zip.file(`${promptIds[i]}.mp4`, blob);
zip.file(`${promptIds[i]}.txt`, prompts[i]);
}
}
zip.generateAsync({ type: 'blob' }).then(function (content: Blob) {
const url = URL.createObjectURL(content);
const a = document.createElement('a');
a.href = url;
a.download = fileName + '.zip';
a.click();
URL.revokeObjectURL(url);
});
}
}
</script>
<style>
video {
max-width: 100%;
max-height: 100%;
}
button {
margin-right: 5px;
}
.hidden {
display: none;
}
</style>
<div class="rounded w-full h-full">
<div class="h-full border-8 flex flex-col">
<div id="top-part" class="w-full flex bg-gray-100 h-4/5">
<div class="bg-gray-500 w-full h-full flex justify-center items-center">
{#if recordings === null}
<p class="text-6xl" class:hidden={recorded||isRecording}>First upload prompts</p>
{:else}
<p class="text-6xl" class:hidden={recordings[promptIndex]!==null||isRecording}>Start a recording</p>
<video class="m-4" id="video-preview" autoplay playsinline class:hidden={!isRecording}></video>
<video class="m-4" id="video-playback" src={recordings[promptIndex]} controls class:hidden={recordings[promptIndex]===null || isRecording}></video>
{/if}
</div>
<div class="w-1/3">
<div class="m-4 bg-gray-200 rounded">
{#if prompts === null}
<p class="border-2 text-3xl text-red-600">
Prompts still have to be uploaded
</p>
{:else}
<p class="border-2 text-3xl">
{prompts[promptIndex]}
</p>
{/if}
</div>
</div>
</div>
<div id="top-part" class="h-1/5">
{#if isRecording}
<button on:click={stopRecording}>Stop Recording</button>
{:else if recordings !== null && recordings[promptIndex]===null}
<button on:click={startRecording}>Start Recording</button>
{:else}
<button on:click={restartRecording}>Restart Recording</button>
{/if}
<button disabled={recordings===null||promptIndex===0} on:click={decreaseIndex}>
Previous
</button>
<button disabled={recordings===null||promptIndex===recordings.length-1} on:click={increaseIndex}>
Next
</button>
<div>
<input type="file" accept=".txt" on:change={handleFileUpload} class="file-upload" />
</div>
{#if recordings !== null}
<button on:click={downloadAllRecordings}>Download All Recordings</button>
{/if}
</div>
</div>
</div>
<div class="h-full w-full rounded">
<div class="flex h-full flex-col border-8">
<div id="top-part" class="flex h-4/5 w-full bg-gray-100">
<div class="flex h-full w-full items-center justify-center bg-gray-500">
{#if recordings === null}
<p class="text-6xl" class:hidden={recorded || isRecording}>First upload prompts</p>
{:else}
<p class="text-6xl" class:hidden={recordings[promptIndex] !== null || isRecording}>
Start a recording
</p>
<!-- svelte-ignore a11y_media_has_caption -->
<video class="m-4" id="video-preview" autoplay playsinline class:hidden={!isRecording}
></video>
<!-- svelte-ignore a11y_media_has_caption -->
<video
class="m-4"
id="video-playback"
src={recordings[promptIndex]}
controls
class:hidden={recordings[promptIndex] === null || isRecording}
></video>
{/if}
</div>
<div class="w-1/3">
<div class="m-4 rounded bg-gray-200">
{#if prompts === null}
<p class="border-2 text-3xl text-red-600">Prompts still have to be uploaded</p>
{:else}
<p class="border-2 text-3xl">
{prompts[promptIndex]}
</p>
{/if}
</div>
</div>
</div>
<div id="top-part" class="h-1/5">
{#if isRecording}
<button on:click={stopRecording}>Stop Recording</button>
{:else if recordings !== null && recordings[promptIndex] === null}
<button on:click={startRecording}>Start Recording</button>
{:else}
<button on:click={restartRecording}>Restart Recording</button>
{/if}
<button disabled={recordings === null || promptIndex === 0} on:click={decreaseIndex}>
Previous
</button>
<button
disabled={recordings === null || promptIndex === recordings.length - 1}
on:click={increaseIndex}
>
Next
</button>
<div>
<input type="file" accept=".txt" on:change={handleFileUpload} class="file-upload" />
</div>
{#if recordings !== null}
<button on:click={downloadAllRecordings}>Download All Recordings</button>
{/if}
</div>
</div>
</div>
<style>
video {
max-width: 100%;
max-height: 100%;
}
button {
margin-right: 5px;
}
.hidden {
display: none;
}
</style>