这是提交给Pinata挑战的内容
制作引人入胜、有吸引人叙事的视频可能会既耗时又复杂,甚至显得不专业。这你有没有试过将旁白或配音外包给别人?为此你可能需要准备一笔不小的费用,这笔费用可能相当高。有没有更简单的方法,比如用AI来简化这个过程,并且更便宜?
让我们来认识Memoire,这是一款由AI驱动的工具,能在几分钟内帮你制作叙述性视频。无论你是内容创作者、营销人员,还是热爱分享故事的人,Memoire都能轻松地将你的想法转化为引人注目的视频。
在这篇文章中,我会带您探索Memoire,展示其功能,开发过程中遇到的挑战以及它所拥有的各种可能性。
1/ 全面的身份验证功能:Memoire 使用 NextAuth 提供的身份验证系统,确保安全并提升用户体验。系统包括美观的电子邮件设计,用于账户验证和密码重置,从而增强功能性和用户参与度。
2/ 上传媒体并自动生成描述:您可以上传您的照片,Memoire 会自动生成这些照片的准确且吸引人的描述。如果描述缺少重要信息,您可以轻松添加更多细节并重新生成更合适的描述。
3/ 媒体转换:利用Memoire多种媒体转换功能,让您的视频叙事更上一层楼,提供多种选项,如“渐变”、“左滑”、“上滑”等。这些转换让您的视频看起来更专业,确保场景转换流畅且美观。
4/ Suanbu de Meiti Liebiao:批量上传照片时,顺序可能不固定。但您可以轻松拖放Memoire中的媒体,按照自己喜欢的顺序排列。
5/ AI脚本生成:Memoire 使用 Google 的 Gemini 1.5 Pro 模型为您生成视频脚本。这确保生成的脚本高质量且上下文相关,从而让您的视频叙事更加引人入胜。
6/ AI音频生成,可选声线:采用OpenAI的TTS-1模型,Memoire提供可自定义的声音供您使用。您可以从Echo、Alloy、Fable、Onyx、Nova和Shimmer这些选项中选择,以找到最适合您项目的完美声音。
7/ 项目设置:自定义您的项目,可以通过添加描述来实现,这有助于AI生成更好的脚本。您还可以调整项目的宽高比例和帧率以符合您的需求。
8/ 浏览器内生成预览:Memoire 使用 Remotion 在您的浏览器中直接生成视频预览。虽然预览与最终输出有些不同,改进工作正在进行中,以使预览更接近最终效果。
9/ AI作曲 :Memoire使用Meta的Music Gen模型为您的视频制作背景音乐。这个功能还在开发中,暂时还没有对外开放测试。
10/ AI 驱动的字幕生成功能:Memoire 可以利用 OpenAI 的 Whisper 模型为您生成视频字幕。此功能正在开发中,很快就能使用。
前端技术: TypeScript, Next.js, DND Kit
后端:Next.js API 路由端点,服务器端动作,Prisma
样式:Tailwind CSS,shadcn/ui 组件
文件存放 :Pinata(一种文件存储服务)
限速: Upstash
身份验证——Next Auth
AI 模型:Google 的 Gemini 1.5 Pro,OpenAI 的 文本转语音-1,Meta 的 Music Gen,OpenAI 的 Whisper
我在Pinata上尝试了几个东西,玩得很开心!具体如下:
1/ 多文件上传组件(带进度跟踪)(MediaPane.tsx):
利用Pinata的原生API接口开发了一个具有实时进度跟踪的多文件上传组件。相比使用SDK,用户体验更好,从而提升了用户满意度。
主要功能:
axios
直接上传到 Pinata 服务器这就是它的运作方式,
zh: a. 获取JWT用于认证:
const keyRequest = await fetch('/api/key'); const keyData = await keyRequest.json() as { JWT: 字符串 };
进入全屏模式 退出
zh: b. 准备并发送上传数据请求:
const UPLOAD_ENDPOINT = `https://uploads.pinata.cloud/v3/files`; const formData = new FormData(); formData.append('file', addedFileState.file); const { data: uploadResponse }: AxiosResponse<{ data: PinataUploadResponse }> = await axios.post(UPLOAD_ENDPOINT, formData, { headers: { Authorization: `Bearer ${keyData.JWT}` }, onUploadProgress: async (progressEvent) => { if (progressEvent.total) { const percentComplete = (progressEvent.loaded / progressEvent.total) * 100; // 更新文件进度 updateFileProgress(addedFileState.key, percentComplete); } } });
按Enter键全屏,按Esc退出全屏
c. 查看上传进度:
onUploadProgress: async (progressEvent) => { if (progressEvent.total) { // 计算上传进度的百分比 const percentComplete = (progressEvent.loaded / progressEvent.total) * 100; // 更新文件进度 updateFileProgress(addedFileState.key, percentComplete); } }
打开全屏,退出全屏
d. 处理上传响应并准备元数据:
await new Promise(resolve => setTimeout(resolve, 1000)); // 延迟1秒钟,然后更新文件进度为完成。 updateFileProgress(addedFileState.key, 'COMPLETE'); const data = addedFileState.type === 'PHOTO' ? await getPhotoDimensions(addedFileState.preview) : await getVideoDimensions(addedFileState.preview); // 根据文件类型,获取照片或视频的尺寸。 const metadata = { ...data, cid: uploadResponse.data.cid, type: addedFileState.type }; // 创建一个包含尺寸、上传响应CID和文件类型的元数据对象。
切换到全屏模式,退出全屏
这种实现允许无缝的上传体验并带有视觉反馈,让用户在上传媒体内容时有更好的互动体验,即使这个过程可能会比较耗时。
2/ 自定义图片组件 (PinataImage.tsx
):
创建了一个自定义的PinataImage组件,以高效地处理图像的获取、缓存和显示。这样可以减少不必要的网络请求,利用浏览器的本地存储来提升性能。
重要特点有:
这里列出它的主要功能
是否检查缓存图像:
const cachedImage = await db.images.where({ cid, width, height }).first(); if (cachedImage) { setImageUrl(URL.createObjectURL(cachedImage.blob)); return; }
以下代码检索数据库中存储的图像,并根据给定的cid、宽度和高度创建一个URL。如果找到缓存的图像,则设置图像URL并返回。
点全屏 关闭全屏
b. 生成安全的带签名URL:
const params = new URLSearchParams({ cid, width: width?.toString() || '', height: height?.toString() || '', expires }); // 获取包含签名URL所需的参数 const response = await fetch(`/api/getSignedUrl?${params}`); if (!response.ok) { // 如果请求失败,则抛出异常 throw new Error('获取签名URL失败'); } // 解析响应中的数据 const data = await response.json() as { url: string };
全屏模式, 退出全屏
c. 取缓图片:
const imageResponse = await fetch(`/api/getImage?url=${encodeURIComponent(data.url)}`); if (!imageResponse.ok) { throw new Error('获取图像失败'); } const blob = await imageResponse.blob(); const objectUrl = URL.createObjectURL(blob); setImageUrl(objectUrl); await db.images.put({ cid, width: Number(width), height: Number(height), blob });
全屏模式 退出全屏
d. 显示图像或一个临时的骨架图:
const renderedImage = useMemo(() => { if (imageUrl) { return ( <Image src={imageUrl} unoptimized={!!src} width={Number(width)} height={Number(height)} alt={alt} className={className} crossOrigin='匿名' {...props} /> ); } else { return ( <Skeleton className={className} /> ); } }, [imageUrl, width, height, src, alt, className, props]);
点击全屏 退出
这个组件确保了存储在Pinata上的图片的快速加载和显示,从而提升了Memoire的整体性能和用户体验。
3/ 媒体管理与预览 (VideoPreview.tsx
):
除了上传和显示图片之外,Pinata 还可用于存储和检索各种类型的媒体,包括音频和视频文件。这一点在 VideoPreview
组件中也很明显:
使用它们的 CIDs(内容ID)获取媒体文件
const getMediaUrl = useCallback(async (cid: string, projectId: string, type: 'media' | 'audio'): Promise<string> => { try { if (typeof window === 'undefined') { return ''; } const 表 = type === 'media' ? db.media : db.audio; let 数据项 = await 表.where({ cid }).first(); if (数据项) { return URL.createObjectURL(数据项.file); } const 响应 = await fetch(`/api/getFile?cid=${encodeURIComponent(cid)}`); if (!响应.ok) { throw new Error(`HTTP 错误!状态码:${响应.status}`); } const 文件 = await 响应.blob(); await 表.put({ cid, file: 文件, projectId }); return URL.createObjectURL(文件); } catch (e) { return '' } }, []);
全屏 退出全屏
b. 加载叙述音频文件
const loadAudio = useCallback(async () => { if (narration?.audioCid) { // 获取音频的 URL const audioUrl = await getMediaUrl(narration.audioCid, project.id, 'audio'); setLoadedAudioUrl(audioUrl); // 设置 narration 中的 audioUrl setNarration({ audioUrl }); } // eslint-disable-next-line react-hooks/exhaustive-deps // 禁用此行的 exhaustive-deps 检查 }, [narration?.audioCid, project.id, getMediaUrl]);
全屏、退出全屏
c. 加载并整理媒体文件
const loadMediaItems = useMemo(() => async () => { try { const loadedItems = await Promise.all( mediaItems.map(async (media) => ({ ...media, url: await getMediaUrl(media.cid, project.id, 'media') })) ); const sortedMediaItems = [...loadedItems].sort((first, next) => project.mediaOrder.indexOf(first.id) - project.mediaOrder.indexOf(next.id) ); // 比较排序后的媒体项与已加载的媒体项 const hasChanged = loadedMediaItems.length === 0 || sortedMediaItems.length !== loadedMediaItems.length || sortedMediaItems.some((item, index) => { const loadedItem = loadedMediaItems[index]; return !loadedItem || item.duration !== loadedItem.duration || item.transition !== loadedItem.transition; }); if (hasChanged) { setLoadedMediaItems(sortedMediaItems); } await loadAudio(); } catch (error) { console.error('Error loading media items :>>', error); } }, [mediaItems, loadedMediaItems, getMediaUrl, project.id, project.mediaOrder, loadAudio]);
进入全屏,退出全屏
这种全面的媒体管理方法可以高效地存储、检索和播放各种类型的媒体,在 Memoire 中。
1/ Pinata集成:与Pinata的合作经历非常有趣。他们提供的JavaScript SDK在上传文件时遇到了挑战,因为它没有内置的进度跟踪功能,这对于我的项目至关重要,需要为用户提供实时的上传进度反馈。为了找到解决办法,我仔细研究了他们的文档,并发现可以直接调用API来实现这一点。
此外,我没有采用传统的预取已签名URL的做法,而是选择了不同的途径。我直接从前端发起API调用并通过IndexedDB缓存响应。这种方法使得每次只需加载每个文件一次,大大减少了对Pinata的API调用,最终节省了信用。这是一次充满挑战的经历,它让我不得不动脑筋,高效地思考!
2/ AI整合:整合AI服务以进行叙述服务和脚本生成是一个重要的挑战。确保AI能产出高质量的结果需要大量的测试和微调。在进行大量测试时,我还遇到了速率限制的问题。
3/ 用户体验:创建一个直观且用户友好的用户界面至关重要,这一点非常重要。我花了相当多的时间设计和迭代用户界面,以确保它满足用户需求的同时也具有美观的外观。这对我来说比较棘手,因为我没时间找设计师合作;(。
链接地址: https://dub.sh/MemoireDemo
链接地址:https://git.new/MemoireRepo(点击链接查看)
1/ 视频音频不同步,画面与声音不匹配。
2/ 视频预览组件在首次加载时会闪烁,这是不必要的闪屏。
Memoire致力于简化视频创作的过程。借助人工智能的力量,我已使几分钟内就能制作出高质量的配音视频,成本极低。无论您是为社交媒体、营销活动还是个人项目制作内容,Memoire都能帮您实现。
我很期待看到你用Memoire能创造些什么。欢迎随时分享你的想法和建议,并告诉我有什么我可以改进的地方。敬请期待更多更新和新功能!