feat(app): added downloading files
This commit is contained in:
parent
935b6f47bc
commit
e9a2b8902b
4 changed files with 219 additions and 185 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
|||
.apikeys.env
|
||||
kernel/.coverage
|
||||
app/.pnpm-store
|
||||
|
|
|
@ -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
16
app/pnpm-lock.yaml
generated
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue