CLASSROOMIO-T-95: Upload to S3 and Muse AI

This commit is contained in:
Rotimi Best
2023-08-10 12:46:14 +01:00
parent ec807a1db9
commit 0ff76e4aa8
21 changed files with 460 additions and 206 deletions
+3 -2
View File
@@ -13,6 +13,7 @@
"prepare": "svelte-kit sync"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.386.0",
"@sentry/browser": "^7.61.0",
"@sentry/integrations": "^7.61.0",
"@sentry/tracing": "^7.61.0",
@@ -25,10 +26,12 @@
"@types/pluralize": "0.0.30",
"ai": "^2.1.31",
"autoprefixer": "^10.4.14",
"axios": "^1.4.0",
"body-parser": "^1.20.2",
"canvas-confetti": "^1.6.0",
"cookie-parser": "^1.4.6",
"copy-to-clipboard": "^3.3.3",
"form-data": "^4.0.0",
"hotkeys-js": "^3.11.2",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
@@ -47,8 +50,6 @@
"@babel/plugin-transform-runtime": "^7.22.9",
"@babel/preset-env": "^7.22.9",
"@babel/runtime": "^7.22.6",
"@fortawesome/free-brands-svg-icons": "^6.4.0",
"@fortawesome/free-regular-svg-icons": "^6.4.0",
"@sveltejs/kit": "^1.22.4",
"@tailwindcss/typography": "^0.5.9",
"@types/lodash": "^4.14.196",
@@ -0,0 +1,25 @@
<script>
import { Accordion, AccordionItem } from 'carbon-components-svelte';
</script>
<Accordion>
<AccordionItem title="Natural Language Classifier">
<p>
Natural Language Classifier uses advanced natural language processing and machine learning
techniques to create custom classification models. Users train their data and the service
predicts the appropriate category for the inputted text.
</p>
</AccordionItem>
<AccordionItem title="Natural Language Understanding">
<p>
Analyze text to extract meta-data from content such as concepts, entities, emotion, relations,
sentiment and more.
</p>
</AccordionItem>
<AccordionItem title="Language Translator">
<p>
Translate text, documents, and websites from one language to another. Create industry or
region-specific translations via the service's customization capability.
</p>
</AccordionItem>
</Accordion>
@@ -3,6 +3,9 @@
import LocalVideo from './LocalVideo.svelte';
import * as CONSTANTS from './constants';
export let lessonId = '';
export let saveLesson = () => {};
let tabs = CONSTANTS.videoTabs;
let currentTab = tabs[0].value;
@@ -34,7 +37,7 @@
{#if currentTab === 1}
<YoutubeVideo />
{:else}
<LocalVideo />
<LocalVideo {lessonId} {saveLesson} />
{/if}
</main>
</section>
@@ -1,45 +1,95 @@
<script>
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { deleteLessonVideo } from '$lib/components/Course/components/Lesson/store/lessons';
import {
lesson,
deleteLessonVideo,
uploadCourseVideoStore
} from '$lib/components/Course/components/Lesson/store/lessons';
import { enhance } from '$app/forms';
import { Moon } from 'svelte-loading-spinners';
export let lessonId = '';
export let saveLesson = () => {};
let isWrongFormat = false;
let isUploaded = false;
let isLoaded = false;
let isBig = false;
let fileInput;
let submit;
let uploadedFileUrl = '';
let isLoading = false;
const uploadVideo = () => {
// if the file is too big or not the correct format
isWrongFormat = true;
function isVideoAdded(link) {
return $lesson.materials?.videos?.find((v) => v.link === link);
}
// when the upload has been sucessfully completed
// here the you set the video_url to something and that would be displayed on the page
isUploaded = false;
async function isDoneUploading(response) {
uploadedFileUrl = response?.success && response?.url;
// closes the modal
// $uploadCourseVideo.isModalOpen = false;
};
if (uploadedFileUrl && !isVideoAdded(uploadedFileUrl)) {
$lesson.materials.videos = [
...$lesson.materials.videos,
{
type: 'muse',
link: uploadedFileUrl,
metadata: {}
}
];
saveLesson();
}
}
const onUpload = () => {
// PROBABLY GET THE SNAPSHOT OR SOMETHING
isLoaded = true;
};
$: isDoneUploading($uploadCourseVideoStore.formRes);
</script>
{#if isLoaded == false}
{#if isLoaded === false}
<button
on:click={onUpload}
class="h-full w-full flex flex-col items-center justify-center border border-blue-300 border-dashed rounded-xl"
type="button"
on:click={() => (fileInput && !isLoading ? fileInput.click() : null)}
class="w-full h-full"
disabled={isLoading}
>
<img src="/upload-video.svg" alt="upload" />
<span class="pt-3">
<h3 class="text-center text-xl font-normal dark:text-white">Upload video</h3>
<p class=" text-center text-sm font-normal">
Select file( Mp4, MOV, AVI) to upload or drag and drop video here.<br /> Size must be less than
20 MB.
</p>
</span>
<form
class="h-full w-full flex flex-col items-center justify-center border border-blue-300 border-dashed rounded-xl"
method="POST"
action="?/create"
enctype="multipart/form-data"
use:enhance={() => {
isLoading = true;
return async ({ update }) => {
await update();
isLoading = false;
isLoaded = true;
};
}}
>
{#if isLoading}
<Moon size="40" color="#1d4ed8" unit="px" duration="1s" />
<p class="mt-5">Uploading Video</p>
{:else}
<img src="/upload-video.svg" alt="upload" />
<span class="pt-3">
<h3 class="text-center text-xl font-normal dark:text-white">Upload video</h3>
<p class=" text-center text-sm font-normal">
Select file( Mp4, MOV, AVI) to upload to your lesson.
</p>
</span>
{/if}
<input
style="display:none;"
type="file"
accept="video/*"
name="file"
on:change={() => submit.click()}
bind:this={fileInput}
/>
<input type="text" name="lessonId" value={lessonId} style="display: none;" />
<input style="display:none;" type="submit" bind:this={submit} />
</form>
</button>
{:else if isBig && isUploaded == false}
{:else if isBig && isUploaded === false}
<div class="h-full w-full flex flex-col items-center justify-center rounded-xl">
<img src="/video-upload-error.svg" alt="upload error" />
<span class="pt-3 pb-2">
@@ -52,7 +102,7 @@
</span>
<PrimaryButton label="Try again" />
</div>
{:else if isWrongFormat && isUploaded == false}
{:else if isWrongFormat && isUploaded === false}
<div class="h-full w-full flex flex-col items-center justify-center rounded-xl">
<img src="/video-upload-error.svg" alt="upload error" />
<span class="pt-3 pb-2">
@@ -72,17 +122,19 @@
<div class="flex items-center justify-between">
<span class="flex items-center gap-2">
<div class="rounded-sm overflow-hidden">
<img src="/course-video.png" alt="coursevideo" class="object-cover" />
<video class="w-[200px]">
<source src={uploadedFileUrl} type="video/mp4" />
<!-- <source src="/path/to/video.webm" type="video/webm" /> -->
<!-- Captions are optional -->
<track kind="captions" />
</video>
</div>
<p>Video.png</p>
<p>{fileInput?.files?.[0]?.name || uploadedFileUrl}</p>
</span>
<button on:click={deleteLessonVideo}>
<img src="/delete-video.svg" alt="deletevideo" class="dark:invert" />
</button>
</div>
</div>
<div class="ml-auto">
<PrimaryButton label="upload" onClick={uploadVideo} />
</div>
</div>
{/if}
@@ -1,47 +1,49 @@
<script>
import { CopyButton, Tag } from 'carbon-components-svelte';
import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte';
import IconButton from '$lib/components/IconButton/index.svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import { lesson, isLessonDirty } from '$lib/components/Course/components/Lesson/store/lessons';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import Chip from '$lib/components/Chip/Text.svelte';
import IconButton from '$lib/components/IconButton/index.svelte';
let youtubeLinks = '';
let error = '';
function getVideoUrls(urls = '') {
return (urls || '').split(',').filter((url) => !!url.trim());
}
function addVideo() {
const links = youtubeLinks.split(',').map((link) => link.trim());
const links = getVideoUrls(youtubeLinks);
const validLinks = links.filter(isValidYouTubeLink);
if (validLinks.length === 0) {
error = 'Invalid YouTube link(s)';
} else {
const existingLinks = $lesson.materials.video_url
.split(',')
.map((link) => link.trim())
.filter(Boolean);
const updatedLinks = [
const existingLinks = $lesson?.materials?.videos || [];
$lesson.materials.videos = [
...existingLinks,
...validLinks.filter((link) => !existingLinks.includes(link))
...validLinks.map((link = '') => ({
type: 'youtube',
link,
metadata: {}
}))
];
$lesson.materials.video_url = updatedLinks.join(',').trim();
youtubeLinks = '';
error = '';
}
}
function isValidYouTubeLink(link) {
function isValidYouTubeLink(link = '') {
const youtubeRegex =
/^(https?:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
return youtubeRegex.test(link.trim());
}
function removeVideo(index) {
const links = $lesson.materials.video_url.split(',').map((link) => link.trim());
links.splice(index, 1);
$lesson.materials.video_url = links.join(',').trim();
function removeVideo(index = 0) {
$lesson.materials.videos = $lesson.materials.videos.filter((v, i) => i !== index);
}
</script>
@@ -58,22 +60,23 @@
<PrimaryButton label="Add Video" className="rounded-md" onClick={addVideo} />
</div>
<p class="mt-4 pl-2 text-sm">
Added : <strong
>{($lesson?.materials?.video_url && $lesson.materials.video_url.trim().split(',').length) || 0} Videos</strong
>
Videos added: <strong>
{$lesson?.materials?.videos.filter((v) => v.type === 'youtube' && isValidYouTubeLink(v.link))
.length || 0}
</strong>
</p>
<div class="">
{#each $lesson?.materials?.video_url && $lesson.materials.video_url
.trim()
.split(',') as videoLink, index}
<div class="flex items-center gap-1">
<Tag type="blue">
{videoLink}
</Tag>
<CopyButton text={videoLink} feedback="Copied to clipboard" />
<IconButton value="delete-video" onClick={() => removeVideo(index)}>
<TrashCanIcon size={16} class="carbon-icon dark:text-white" />
</IconButton>
</div>
{#each $lesson?.materials?.videos as video, index}
{#if video.type === 'youtube'}
<div class="flex items-center gap-1">
<Tag type="blue">
{video.link}
</Tag>
<CopyButton text={video.link} feedback="Copied to clipboard" />
<IconButton value="delete-video" onClick={() => removeVideo(index)}>
<TrashCanIcon size={16} class="carbon-icon dark:text-white" />
</IconButton>
</div>
{/if}
{/each}
</div>
@@ -1,11 +1,13 @@
<script lang="ts">
import { useCompletion } from 'ai/svelte';
import merge from 'lodash/merge';
import MODES from '$lib/utils/constants/mode.js';
import { getEmbedId } from '$lib/utils/functions/formatYoutubeVideo';
import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte';
import IconButton from '$lib/components/IconButton/index.svelte';
import { formatYoutubeVideo } from '$lib/utils/functions/formatYoutubeVideo';
import Modal from '$lib/components/Modal/index.svelte';
import { Popover } from 'carbon-components-svelte';
import AlignBoxTopLeftIcon from 'carbon-icons-svelte/lib/AlignBoxTopLeft.svelte';
import TrashCan from 'carbon-icons-svelte/lib/TrashCan.svelte';
import ListIcon from 'carbon-icons-svelte/lib/List.svelte';
import IbmWatsonKnowledgeStudioIcon from 'carbon-icons-svelte/lib/IbmWatsonKnowledgeStudio.svelte';
import MachineLearningModel from 'carbon-icons-svelte/lib/MachineLearningModel.svelte';
@@ -27,7 +29,8 @@
import VideoUploader from '$lib/components/Course/components/Lesson/Materials/Video/Index.svelte';
import { course } from '$lib/components/Course/store';
import * as CONSTANTS from './constants';
import { browser } from '$app/environment';
import { fetchLesson } from '$lib/utils/services/courses';
import type { LessonPage } from '$lib/utils/types';
export let mode = MODES.view;
export let prevMode = '';
@@ -36,11 +39,9 @@
let tabs = CONSTANTS.tabs;
let currentTab = tabs[0].value;
let errors = {};
let player = {};
let textareaRef = {};
let aiButtonRef = {};
let openPopover = false;
let videoUrls: Array<string> = [];
let aiButtonClass =
'flex items-center px-5 py-2 border border-gray-300 hover:bg-gray-200 rounded-md w-full mb-2';
@@ -49,26 +50,26 @@
() =>
(currentTab = tab);
function initPlyr(_player, videoUrl = '') {
if (!browser) return;
// problem: after we save an uploaded video,
async function saveLesson() {
const { data } = await fetchLesson(lessonId);
const materials = merge(data, $lesson.materials);
const players = Array.from(document.querySelectorAll('.plyr-video-trigger')).map((p) => {
// @ts-ignore
return new Plyr(p);
});
// @ts-ignore
window.players = players;
lesson.update((l) => ({
...l,
materials
}));
handleUpdateLessonMaterials($lesson, lessonId);
}
function handleSave(prevMode) {
function handleSave(prevMode: string) {
if (prevMode === MODES.edit) {
handleUpdateLessonMaterials($lesson, lessonId);
saveLesson();
}
}
function addBadgeValueToTab(materials) {
const { slide_url, video_url, note } = materials;
const { slide_url, videos, note } = materials;
tabs = tabs.map((tab) => {
let badgeValue;
@@ -78,7 +79,7 @@
} else if (tab.value === 2) {
badgeValue = !!note ? 1 : 0;
} else {
badgeValue = typeof video_url === 'string' && !!video_url ? video_url.split(',').length : 0;
badgeValue = Array.isArray(videos) ? videos.length : 0;
}
tab.badgeValue = badgeValue;
@@ -114,19 +115,16 @@
}
const onClose = () => {
handleUpdateLessonMaterials($lesson, lessonId);
saveLesson();
$uploadCourseVideoStore.isModalOpen = false;
$uploadCourseVideoStore.formRes = null;
};
$: handleSave(prevMode);
$: initPlyr(player, $lesson.materials.video_url);
$: addBadgeValueToTab($lesson.materials);
$: updateNoteByCompletion($completion);
$: videoUrls = ($lesson?.materials?.video_url || '').split(',').filter((url) => !!url.trim());
</script>
<Modal
@@ -135,7 +133,7 @@
width="w-4/5 h-[566px]"
modalHeading="Add a Video"
>
<VideoUploader {onClose} />
<VideoUploader {lessonId} {saveLesson} />
</Modal>
<Tabs {tabs} {currentTab} {onChange}>
@@ -236,34 +234,45 @@
</TabContent>
<TabContent value={tabs[2].value} index={currentTab}>
{#if mode === MODES.edit && videoUrls.length}
<PrimaryButton label="Add/Edit Video" onClick={openAddVideoModal} className="mb-2" />
{#if mode === MODES.edit && $lesson.materials.videos.length}
<PrimaryButton label="Add/Edit Video(s)" onClick={openAddVideoModal} className="mb-2" />
{/if}
<!-- Line 230 would run if its in edit mode or the video is truthy but then in line 235 we are mapping for an empty string -->
{#if videoUrls.length}
{#if $lesson.materials.videos.length}
<div class="flex flex-col items-start w-full h-full">
{#each videoUrls as url, index}
{#each $lesson.materials.videos as video, index}
{#if mode === MODES.edit}
<div class="ml-auto">
<IconButton
value="delete-video"
contained={true}
onClick={() => deleteLessonVideo(index)}
>
<TrashCanIcon size={20} class="carbon-icon dark:text-white" />
</IconButton>
</div>
{/if}
<div class="w-full h-full flex flex-col gap-2 overflow-hidden">
{#if url.includes('youtube')}
{#key video.link}
<div class="mb-5">
<div
bind:this={player}
class="plyr-video-trigger"
data-plyr-provider="youtube"
data-plyr-embed-id={getEmbedId(url)}
/>
{#if video.type === 'youtube'}
<iframe
width="100%"
height="569"
class="iframe"
src={formatYoutubeVideo(video.link, errors)}
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
/>
{:else}
<video class="plyr-video-trigger" playsinline controls>
<source src={video.link} type="video/mp4" />
<track kind="captions" />
</video>
{/if}
</div>
{:else}
<video bind:this={player} class="plyr-video-trigger" playsinline controls>
<source
src="https://pub-bd38ff7251aa402a93762181a947f7ea.r2.dev/generate-lesson-draft.mp4"
type="video/mp4"
/>
<!-- <source src="/path/to/video.webm" type="video/webm" /> -->
<!-- Captions are optional -->
<track kind="captions" />
</video>
{/if}
{/key}
</div>
{/each}
</div>
@@ -1,21 +1,23 @@
import { writable } from 'svelte/store';
import type { Writable, Updater } from 'svelte/store';
import { createLesson, updateLesson, deleteLesson } from '$lib/utils/services/courses';
import type { Lesson, Course } from '$lib/utils/types';
import type { Lesson, Course, LessonPage } from '$lib/utils/types';
export const uploadCourseVideoStore = writable({
isModalOpen: false
isModalOpen: false,
formRes: null
});
export const lessons: Writable<Lesson[]> = writable([]);
export const lesson = writable({
export const lesson = writable<LessonPage>({
id: null,
totalExercises: 0,
is_complete: false,
materials: {
note: '',
slide_url: '',
video_url: ''
videos: []
},
exercises: []
});
@@ -103,14 +105,12 @@ export async function handleUpdateLessonMaterials(lesson: any, lessonId: Lesson[
export const deleteLessonVideo = (index: any) => {
lesson.update((currentLesson) => {
const links = currentLesson.materials.video_url.split(',').map((link) => link.trim());
links.splice(index, 1);
const updatedVideoUrl = links.join(',').trim();
const videos = currentLesson.materials.videos || [];
return {
...currentLesson,
materials: {
...currentLesson.materials,
video_url: updatedVideoUrl
videos: videos?.filter((video, i) => i !== index)
}
};
});
@@ -1,5 +1,5 @@
<script>
import CaretSortDownIcon from 'carbon-icons-svelte/lib/CaretSortDown.svelte';
import { slide } from 'svelte/transition';
import ChevronDownIcon from 'carbon-icons-svelte/lib/ChevronDown.svelte';
import ChevronUpIcon from 'carbon-icons-svelte/lib/ChevronUp.svelte';
import IconButton from '$lib/components/IconButton/index.svelte';
@@ -48,7 +48,7 @@
{/if}
</button>
{#if isExpanded}
<div class="flex flex-col">
<div in:slide out:slide class="flex flex-col">
<slot />
</div>
{/if}
@@ -249,10 +249,13 @@
{/if}
</div>
<div class="flex items-center">
{#if lesson.video_url}
{#if lesson.videos}
<span class="text-sm font-light flex w-2/4"
><Video size={16} class="mr-1" />1 video</span
>
><Video size={16} class="mr-1" />{lesson.videos.length} video{lesson.videos
.length > 1
? 's'
: ''}
</span>
{/if}
{#if get(lesson, 'totalExercises[0].count')}
<span class="text-sm font-light flex w-2/4"
-58
View File
@@ -1,58 +0,0 @@
<script>
import Icon from 'svelte-awesome/components/Icon.svelte';
import { faTelegram } from '@fortawesome/free-brands-svg-icons';
import { faEnvelope } from '@fortawesome/free-regular-svg-icons';
export let avatarUrl =
'https://tailwindcss.com/_next/static/media/sarah-dayan.a8ff3f1095a58085a82e3bb6aab12eb2.jpg';
export let name = 'Sarah Dayan';
export let title = 'Staff Engineer, Algolia';
export let mailLink;
export let telegramLink;
</script>
<figure class="w-52 bg-gray-100 dark:bg-gray-700 px-4 py-7 mr-4 mb-4">
<img
class="w-32 h-32 rounded-full mx-auto"
src={avatarUrl}
alt=""
width="384"
height="512"
/>
<div class="pt-6 text-center space-y-4">
<div class="description font-medium flex flex-col justify-around">
<div class="text-cyan-600">{name}</div>
<div class="text-gray-500 dark:text-white">{title}</div>
</div>
</div>
{#if telegramLink && mailLink}
<div class="flex justify-center">
<a href={telegramLink} target="_blank">
<Icon
data={faTelegram}
scale="1"
class="social-icon"
style="color:#4dabf7;margin:0 5px;"
/>
</a>
<a href={mailLink} target="_blank">
<Icon
data={faEnvelope}
scale="1"
class="social-icon"
style="color:#d44c3d;margin:0 5px;"
/>
</a>
</div>
{/if}
</figure>
<style>
img {
width: 104px;
height: 104px;
}
.description {
min-height: 70px;
}
</style>
+1
View File
@@ -0,0 +1 @@
export default () => Date.now().toString(36) + Math.random().toString(36).substr(2);
+1 -1
View File
@@ -108,7 +108,7 @@ async function cloneLessons(lessons: Lesson[], courseId: Course['id']) {
public: lesson.public,
slide_url: lesson.slide_url,
title: lesson.title,
video_url: lesson.video_url
videos: lesson.videos
}));
const { data } = await supabase.from('lesson').insert(clonedLessons).select();
+2 -2
View File
@@ -34,7 +34,7 @@ export async function fetchCourse(courseId?: Course['id'], slug?: Course['slug']
certificate_theme,
lessons:lesson(
id, title,public, lesson_at, call_url, is_unlocked, is_complete, order, created_at,
note, video_url, slide_url, call_url, totalExercises:exercise(count),
note, videos, slide_url, call_url, totalExercises:exercise(count),
profile:teacher_id(id, avatar_url, fullname)
),
attendance:group_attendance(*)
@@ -150,7 +150,7 @@ export function deleteGroupMember(groupMemberId: Groupmember['id']) {
export function fetchLesson(lessonId: Lesson['id']) {
return supabase
.from('lesson')
.select(`id, note, video_url, slide_url, call_url, totalExercises:exercise(count)`)
.select(`id, note, videos, slide_url, call_url, totalExercises:exercise(count)`)
.eq('id', lessonId)
.single();
}
+21 -1
View File
@@ -136,9 +136,29 @@ export interface Group_attendance {
groupmember?: Groupmember;
}
enum VideoType {
youtube,
muse
}
export interface LessonPage {
id?: string | null;
totalExercises: Number;
is_complete: Boolean;
materials: {
note: string;
slide_url: string;
videos: Array<{
type: string;
link: string;
metadata?: {};
}>;
};
exercises: [];
}
export interface Lesson {
note?: any; // type unknown;
video_url?: any; // type unknown;
videos?: []; // type unknown;
slide_url?: any; // type unknown;
course_id: string /* foreign key to course.id */;
id?: string /* primary key */;
+2 -2
View File
@@ -39,7 +39,7 @@
import '../app.postcss';
let supabase = getSupabase();
let path = $page.url.pathname.replace('/', '');
let path = $page.url?.pathname?.replace('/', '');
let theme = 'white';
let skipAuth = false;
@@ -261,7 +261,7 @@
};
});
$: path = $page.url.pathname.replace('/', '');
$: path = $page.url?.pathname?.replace('/', '');
$: theme = $appStore.isDark ? 'g100' : 'white';
$: setOrgBasedOnUrl($page.url.host);
</script>
+42
View File
@@ -0,0 +1,42 @@
import { json } from '@sveltejs/kit';
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import {
CLOUDFLARE_ACCESS_KEY,
CLOUDFLARE_SECRET_ACCESS_KEY,
CLOUDFLARE_ACCOUNT_ID,
CLOUDFLARE_PUBLIC_ACCOUNT_ID,
MUSE_API_KEY
} from '$env/static/private';
const S3 = new S3Client({
region: 'auto',
endpoint: `https://${CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: CLOUDFLARE_ACCESS_KEY,
secretAccessKey: CLOUDFLARE_SECRET_ACCESS_KEY
}
});
const downloadTranscript = async () => {};
const uploadTranscriptToS3 = async () => {};
export async function GET({ url }) {
const museFid = url.searchParams.get('museFid');
console.log('museFid');
// check the transcript table and see if we find a record
// if we find one
// check if downloaded = true
// if true
// return the transcript
// if false
// download the transcript from muse
// upload it to S3
// return the transcript
// if we dont
return json({ success: true, museFid });
}
+2 -1
View File
@@ -3,6 +3,7 @@
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import { setCourse } from '$lib/components/Course/store';
import { fetchCourse } from '$lib/utils/services/courses';
import { browser } from '$app/environment';
export let data;
const { courseId } = data;
@@ -10,7 +11,7 @@
let prevCourseId = '';
async function onCourseIdChange(courseId = '') {
if (!courseId || prevCourseId === courseId) return;
if (!courseId || prevCourseId === courseId || !browser) return;
const { data: _data } = await fetchCourse(courseId);
@@ -1,13 +0,0 @@
export const load = ({ params = { id: '' } }) => {
const { id: courseId, lessonParams = [] } = params;
const splitparams = lessonParams.split('/');
const [lessonId, exerciseRouteName, exerciseId] = splitparams;
return {
courseId,
lessonId,
exerciseRouteName,
exerciseId,
isMaterialsTabActive: !exerciseRouteName
};
};
@@ -0,0 +1,157 @@
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import FormData from 'form-data';
import axios from 'axios';
import { fail } from '@sveltejs/kit';
import genUniqueId from '$lib/utils/functions/genUniqueId.js';
import { getSupabase } from '$lib/utils/functions/supabase';
import {
CLOUDFLARE_ACCESS_KEY,
CLOUDFLARE_SECRET_ACCESS_KEY,
CLOUDFLARE_ACCOUNT_ID,
CLOUDFLARE_PUBLIC_ACCOUNT_ID,
MUSE_API_KEY
} from '$env/static/private';
// Set the runtime to edge for best performance
export const config = {
runtime: 'edge'
};
const S3 = new S3Client({
region: 'auto',
endpoint: `https://${CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: CLOUDFLARE_ACCESS_KEY,
secretAccessKey: CLOUDFLARE_SECRET_ACCESS_KEY
}
});
const supabase = getSupabase();
const uploadToMuse = (s3Data, fileName, lessonId, fileUrl) => {
const form = new FormData();
form.append('file', s3Data, fileName);
axios({
method: 'POST',
url: 'https://api.muse.ai/api/files/upload',
data: form,
maxContentLength: Infinity,
maxBodyLength: Infinity,
headers: {
...form.getHeaders(),
Key: MUSE_API_KEY
}
})
.then(async function (response) {
console.log('Upload to muse complete', response.data);
const { data: lesson } = await supabase
.from('lesson')
.select(`*`)
.eq('id', lessonId)
.single();
if (lesson) {
// Add Muse response to metadata
const videos = lesson.videos.map((v) => {
if (v.link === fileUrl) {
console.log('matches');
v.metadata = JSON.parse(JSON.stringify(response.data));
}
return v;
});
const { error, data } = await supabase
.from('lesson')
.update({ videos })
.eq('id', lessonId)
.select();
if (error) {
console.log('Error updating supabase configs');
}
console.log('update error', error);
console.log('update data', data, data[0].videos);
}
})
.catch(function (error) {
console.log('Upload error', error);
});
};
export const load = ({ params = { id: '' } }) => {
const { id: courseId, lessonParams = [] } = params;
const splitparams = lessonParams.split('/');
const [lessonId, exerciseRouteName, exerciseId] = splitparams;
return {
courseId,
lessonId,
exerciseRouteName,
exerciseId,
isMaterialsTabActive: !exerciseRouteName
};
};
export const actions = {
create: async ({ request }) => {
const formData = Object.fromEntries(await request.formData());
console.log('formData', formData);
if (!formData.file?.name || formData.file?.name === 'undefined') {
return fail(400, {
success: false,
message: 'You must provide a file to upload'
});
}
const { file, lessonId } = formData;
console.log('file', file);
console.log('lessonId', lessonId);
const stream = await file.arrayBuffer();
const fileName = `${genUniqueId()}-${file.name}`;
const params = {
Bucket: 'videos',
Key: fileName,
Body: stream,
ContentLength: stream.length
};
console.log('params', params);
let upload;
const fileUrl = `https://pub-${CLOUDFLARE_PUBLIC_ACCOUNT_ID}.r2.dev/${fileName}`;
try {
upload = await S3.send(new PutObjectCommand(params));
// process data.
} catch (error) {
// error handling.
const { requestId, cfId, extendedRequestId } = error.$$metadata;
console.error('Error uploading file');
console.log({ requestId, cfId, extendedRequestId });
} finally {
// finally.
console.log('File upload complete', upload);
console.log('\n\nInitilizing Muse Request');
const data = await S3.send(
new GetObjectCommand({
Bucket: 'videos',
Key: fileName
})
);
// console.log('GetObjectCommand', data);
uploadToMuse(data.Body, file.name, lessonId, fileUrl);
}
return {
success: true,
url: fileUrl,
message: 'Uploaded successfully'
};
}
};
@@ -12,11 +12,17 @@
import Exercises from '$lib/components/Course/components/Lesson/Exercises/index.svelte';
import MODES from '$lib/utils/constants/mode.js';
import { setCourse, course } from '$lib/components/Course/store';
import { lesson, handleSaveLesson } from '$lib/components/Course/components/Lesson/store/lessons';
import {
lesson,
handleSaveLesson,
uploadCourseVideoStore
} from '$lib/components/Course/components/Lesson/store/lessons';
import { browser } from '$app/environment';
export let data;
export let form;
let path = '';
let mode = MODES.view;
let prevMode = '';
@@ -68,12 +74,14 @@
mode = mode === MODES.edit ? MODES.view : MODES.edit;
}
$: path = $page.url.pathname.replace(/\/exercises[\/ 0-9 a-z -]*/, '');
$: path = $page.url?.pathname?.replace(/\/exercises[\/ 0-9 a-z -]*/, '');
$: if (data.courseId && browser) {
mode = MODES.view;
fetchReqData(data.lessonId);
}
$: $uploadCourseVideoStore.formRes = form;
</script>
<CourseContainer {isFetching} {path} isExercisePage={!data.isMaterialsTabActive && data.exerciseId}>
@@ -293,7 +293,7 @@
</a>
<a
class="flex items-center no-underline hover:underline text-black my-2"
href="{$page.url.pathname.replace('submissions', 'lessons')}/{item.lesson
href="{$page.url?.pathname?.replace('submissions', 'lessons')}/{item.lesson
.id}/exercises/{item.exercise.id}"
>
<p class="dark:text-white text-grey text-sm">