?? 记忆:只需几分钟!使用AI创建解说视频!

2024/10/15 21:04:02

本文主要是介绍?? 记忆:只需几分钟!使用AI创建解说视频!,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

这是提交给Pinata挑战的内容

✒️ 简介

制作引人入胜、有吸引人叙事的视频可能会既耗时又复杂,甚至显得不专业。这你有没有试过将旁白或配音外包给别人?为此你可能需要准备一笔不小的费用,这笔费用可能相当高。有没有更简单的方法,比如用AI来简化这个过程,并且更便宜?

让我们来认识Memoire,这是一款由AI驱动的工具,能在几分钟内帮你制作叙述性视频。无论你是内容创作者、营销人员,还是热爱分享故事的人,Memoire都能轻松地将你的想法转化为引人注目的视频。

在这篇文章中,我会带您探索Memoire,展示其功能,开发过程中遇到的挑战以及它所拥有的各种可能性。

Memoire 落地页

🔐 关键特性

1/ 全面的身份验证功能:Memoire 使用 NextAuth 提供的身份验证系统,确保安全并提升用户体验。系统包括美观的电子邮件设计,用于账户验证和密码重置,从而增强功能性和用户参与度。

Memoire Feature Screenshot, #1

2/ 上传媒体并自动生成描述:您可以上传您的照片,Memoire 会自动生成这些照片的准确且吸引人的描述。如果描述缺少重要信息,您可以轻松添加更多细节并重新生成更合适的描述。

记忆功能截图,#2

3/ 媒体转换:利用Memoire多种媒体转换功能,让您的视频叙事更上一层楼,提供多种选项,如“渐变”、“左滑”、“上滑”等。这些转换让您的视频看起来更专业,确保场景转换流畅且美观。

如下功能截屏,#3

4/ Suanbu de Meiti Liebiao:批量上传照片时,顺序可能不固定。但您可以轻松拖放Memoire中的媒体,按照自己喜欢的顺序排列。

Memoire 功能截图,#4

5/ AI脚本生成:Memoire 使用 Google 的 Gemini 1.5 Pro 模型为您生成视频脚本。这确保生成的脚本高质量且上下文相关,从而让您的视频叙事更加引人入胜。

Memoire 功能截图,#5

6/ AI音频生成,可选声线:采用OpenAI的TTS-1模型,Memoire提供可自定义的声音供您使用。您可以从Echo、Alloy、Fable、Onyx、Nova和Shimmer这些选项中选择,以找到最适合您项目的完美声音。

Memoire 功能截图,#6

7/ 项目设置:自定义您的项目,可以通过添加描述来实现,这有助于AI生成更好的脚本。您还可以调整项目的宽高比例和帧率以符合您的需求。

Memoire 特性截图7

8/ 浏览器内生成预览:Memoire 使用 Remotion 在您的浏览器中直接生成视频预览。虽然预览与最终输出有些不同,改进工作正在进行中,以使预览更接近最终效果。

记忆功能截图 #8

9/ AI作曲 :Memoire使用Meta的Music Gen模型为您的视频制作背景音乐。这个功能还在开发中,暂时还没有对外开放测试。

10/ AI 驱动的字幕生成功能:Memoire 可以利用 OpenAI 的 Whisper 模型为您生成视频字幕。此功能正在开发中,很快就能使用。

🛠️ 技术栈(Tech Stack)

  • 前端技术: 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

  • 浏览器内预览功能 Remotion

🦄 我用了Pinata的哪些功能(彩蛋)

我在Pinata上尝试了几个东西,玩得很开心!具体如下:

1/ 多文件上传组件(带进度跟踪)(MediaPane.tsx):
利用Pinata的原生API接口开发了一个具有实时进度跟踪的多文件上传组件。相比使用SDK,用户体验更好,从而提升了用户满意度。

主要功能

  • 使用 axios 直接上传到 Pinata 服务器
  • 基于 JWT 的认证以实现安全上传
  • 实时跟踪上传进度

这就是它的运作方式,

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组件,以高效地处理图像的获取、缓存和显示。这样可以减少不必要的网络请求,利用浏览器的本地存储来提升性能。

重要特点有:

  • 利用IndexedDB实现本地缓存
  • 生成签名URL以确保安全访问
  • 懒加载及骨架占位符

这里列出它的主要功能

是否检查缓存图像:

    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 中。

&#128170; 遇到的挑战

1/ Pinata集成:与Pinata的合作经历非常有趣。他们提供的JavaScript SDK在上传文件时遇到了挑战,因为它没有内置的进度跟踪功能,这对于我的项目至关重要,需要为用户提供实时的上传进度反馈。为了找到解决办法,我仔细研究了他们的文档,并发现可以直接调用API来实现这一点。

此外,我没有采用传统的预取已签名URL的做法,而是选择了不同的途径。我直接从前端发起API调用并通过IndexedDB缓存响应。这种方法使得每次只需加载每个文件一次,大大减少了对Pinata的API调用,最终节省了信用。这是一次充满挑战的经历,它让我不得不动脑筋,高效地思考!

2/ AI整合:整合AI服务以进行叙述服务和脚本生成是一个重要的挑战。确保AI能产出高质量的结果需要大量的测试和微调。在进行大量测试时,我还遇到了速率限制的问题。

3/ 用户体验:创建一个直观且用户友好的用户界面至关重要,这一点非常重要。我花了相当多的时间设计和迭代用户界面,以确保它满足用户需求的同时也具有美观的外观。这对我来说比较棘手,因为我没时间找设计师合作;(。

&#128248; 屏幕截图

Memoire Screenshots

&#128279; 项目网址

链接地址: https://dub.sh/MemoireDemo

&#128187; 代码库

链接地址:https://git.new/MemoireRepo(点击链接查看)

注意:已知问题

1/ 视频音频不同步,画面与声音不匹配。
2/ 视频预览组件在首次加载时会闪烁,这是不必要的闪屏。

&#127881; 结尾

Memoire致力于简化视频创作的过程。借助人工智能的力量,我已使几分钟内就能制作出高质量的配音视频,成本极低。无论您是为社交媒体、营销活动还是个人项目制作内容,Memoire都能帮您实现。

我很期待看到你用Memoire能创造些什么。欢迎随时分享你的想法和建议,并告诉我有什么我可以改进的地方。敬请期待更多更新和新功能!



这篇关于?? 记忆:只需几分钟!使用AI创建解说视频!的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程