稀土掘金技术社区

H5录音、图文视频IndexDB储存最佳实践:用AI生成语音备忘录

Image

引言

早在大学的时候,我就期望做一款属于自己的 APP,可惜那时不懂技术。现在有了技术,但却没有时间。好在 AI 的快速发展终于让我完成了这个愿望。于是,我用半天的时间,用 AI 生成了一个纯前端的 H5 程序:「随心记」。

Image
❝

「在线体验地址:」https://aicoding.juejin.cn/aicoding/work/7519454663779811366[1]

❞

什么是随心记

随心记是一个由 AI 生成的网页备忘录,它支持「语音录入」(可下载)、「图文视频」记录。最重要的是,它支持「离线使用」,所有数据都储存在浏览器中,不依赖后端,刷新页面数据也不会丢失!

❝

此外,它还做了语音兼容处理,支持在 iOS 和安卓设备中通过 WebView 嵌套使用。

❞

功能介绍

  • 「欢迎页」
Image

  • 「信息录入页」

支持图文

Image

支持语音录入、播放、下载

Image

数据离线缓存,刷新页面不丢失

Image
❝

仅点击退出会清空数据

❞

技术栈

本项目通过 「Trae+Claude-3.7」 生成,纯科技,不夹杂人工代码,项目最终通过掘金 MCP 一键部署发布。

❝

作品发布 & 体验地址:aicoding.juejin.cn/aicoding/wo…[2]

❞
Image

AI 的代码技术栈如下:

  • 星空背景:「Cnavas」 实现
  • 录音功能:纯原生实现,后为了兼容改用 「record-core」
  • 图片 / 视频功能:「Vant-UI」
  • 数据离线储存:采用 「IndexDB」

AI 实践过程

开发工具准备

要使用 AI 实现这样一个小程序,首先要选取合适的 AI 编程工具。比如字节的 Trae 或国外的 Cursor 都行,「由于要使用掘金的 MCP 一键部署,还是建议使用 Trae」。

不同的 AI 模型可能影响代码生成效果,因此,建议使用 「Claude,编码能力更强。」

产品及 prompt 设计

要使用 AI 生成你想要的项目,首先一定要设计好你的产品,比如它包含哪些模块,有哪些功能,交互逻辑如何等等。因为这些内容,最终都将作为 prompt(AI 模型提示词) 投喂给 AI,而 prompt 的详细程度,往往直接决定了你生成的产品质量。

当然,如果你实在不知道怎么设计产品,也可以请教 ChatGPT,比如这样问:

我想通过 AI 编译器生成一个记事本 APP,帮我设计一个详细的prompt

Image

设置自定义智能体

为了让 AI 开发代码更准确,符合我们的开发、编码习惯,我们可以创建一个智能体,让 AI 通过我们的预设信息来进行代码生成。

Image

通过 AI 提问开发项目

一切都准备好后,我们就可以使用 AI 生成项目了。首先,我们需要让 AI 帮我们搭建项目框架。

❝

我要创建一个名为 “随心记” 的 APP,使用 Vue3+typeScript+less 帮我搭建项目框架,不要写入任何内容,等待我的指示

❞

搭建项目的过程中,我们只需要按照提示,运行相应的命令即可:

Image

项目搭建好,我们就可以按照模块,让 AI 帮我们一步步完善代码。我们以生成星空背景 + 手机框架为例。

❝

promot:帮我生成星空背景,样式要炫酷,屏幕中间显示一个 Ihone 的手机框架。

❞

我们可以用过上下文协议,选中某个文件,将代码生成在指定位置:

Image

代码生成后,我们先预览效果,如果效果不达标,我们可以通过不断提问去增加细节,如给背景的星星增加闪烁效果,给背景增加流星之类的。通过不断优化promot,效果会越来越接近我们想要的效果。

给出我通过 AI 生成的星空背景代码:

Image
<template>
  <div class="media-wrap">
  <canvas class="star-canvas"></canvas>
  </div>
  </template>

  <script setup>
  import { onMounted } from 'vue'
onMounted(() => {
  const canvas = document.querySelector('.star-canvas')
  const ctx = canvas.getContext('2d')

  function resize() {
    canvas.width = canvas.offsetWidth
    canvas.height = canvas.offsetHeight
  }

  resize()
  window.addEventListener('resize', resize)

  // ⭐ 星星类(星星变大)
  class Star {
    constructor() {
      this.reset()
    }

    reset() {
      this.x = Math.random() * canvas.width
      this.y = Math.random() * canvas.height
      this.radius = Math.random() * 2.5 + 0.5  // ⭐ 更大:0.5 ~ 3px
      this.alpha = Math.random()
      this.fade = Math.random() * 0.02
      this.speed = 0.1 + Math.random() * 0.3
    }

    update() {
      this.y += this.speed
      this.alpha += this.fade
      if (this.alpha <= 0 || this.alpha >= 1) this.fade = -this.fade
      if (this.y > canvas.height) this.reset()
    }

    draw() {
      ctx.beginPath()
      ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2)
      ctx.fillStyle = `rgba(255,255,255,${this.alpha})`
      ctx.fill()
    }
  }

  // ☄️ 流星类(左下 → 右上)
  class Meteor {
    constructor() {
      this.reset()
    }

    reset() {
      this.x = Math.random() * canvas.width * 0.5         // 左侧
      this.y = canvas.height - Math.random() * canvas.height * 0.5  // 下方
      this.len = Math.random() * 80 + 100
      this.speed = Math.random() * 4 + 6
      this.angle = Math.PI / 4  // ↗ 从左下到右上
      this.alpha = 1
    }

    update() {
      this.x += this.speed * Math.cos(this.angle)
      this.y -= this.speed * Math.sin(this.angle)
      this.alpha -= 0.01
      if (this.alpha <= 0) this.reset()
    }

    draw() {
      const x = this.x
      const y = this.y
      const len = this.len
      ctx.save()
      ctx.beginPath()
      ctx.moveTo(x, y)
      ctx.lineTo(
        x + len * Math.cos(this.angle),
        y - len * Math.sin(this.angle)
      )
      ctx.strokeStyle = `rgba(255,255,255,${this.alpha})`
      ctx.lineWidth = 2
      ctx.shadowColor = '#fff'
      ctx.shadowBlur = 10
      ctx.stroke()
      ctx.restore()
    }
  }

  const stars = Array.from({ length: 200 }, () => new Star())
  const meteors = Array.from({ length: 3 }, () => new Meteor())

  function animate() {
    ctx.clearRect(0, 0, canvas.width, canvas.height)

    stars.forEach(star => {
      star.update()
      star.draw()
    })

    meteors.forEach(meteor => {
      meteor.update()
      meteor.draw()
    })

    requestAnimationFrame(animate)
  }

    animate()
})
</script>




<style lang="less" scoped>
.media-wrap {
    width: 100%;
    height: 100vh;
    overflow: hidden;
    background: linear-gradient(to bottom, #1b1e3f, #2c3e50, #3b2c59); // 深蓝+紫色
    // ✨ 动态渐变星云背景
    background: linear-gradient(-45deg, #1b1e3f, #2c3e50, #3b2c59, #1b1e3f);
    background-size: 400% 400%;
    animation: galaxyMove 20s ease infinite;

    .star-canvas {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        z-index: 0;
    }

    .iphone {
        width: 380px;
        height: 85%;
        border-radius: 30px;
        position: relative;
        box-shadow: 0 0 0 10px black;
        margin: 40px auto;
        z-index: 1;

        .screen {
            width: 100%;
            height: 100%;
            border-radius: 30px;
            margin-top: 5%;
            overflow: hidden;
        }

        .notch {
            position: absolute;
            top: 0;
            left: 50%;
            transform: translateX(-50%);
            width: 130px;
            height: 30px;
            background: #000;
            border-bottom-left-radius: 20px;
            border-bottom-right-radius: 20px;
            z-index: 2000;
        }

        .side-button {
            position: absolute;
            width: 4px;
            background: #666;
            border-radius: 2px;

            &.power {
                height: 40px;
                right: -6px;
                top: 120px;
            }

            &.volume-up {
                height: 30px;
                left: -6px;
                top: 100px;
            }

            &.volume-down {
                height: 30px;
                left: -6px;
                top: 140px;
            }
        }
    }
}

.title {
    position: relative;
    z-index: 2;
    text-align: left;
    margin-top: 20px;
    font-weight: 600;
    letter-spacing: 1px;
    text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);

    .gradient-text {
        font-size: 28px;
        background: linear-gradient(45deg, #f9d423, #ff4e50, #7b4397, #00c6ff);
        background-size: 300% 300%;
        -webkit-background-clip: text;
        -webkit-text-fill-color: transparent;
        animation: gradient-shift 8s ease infinite, pulse 2s infinite alternate;
    }
}

.info {
    color: #fff;
    position: fixed;
    bottom: 10px;
    right: 10px;
    cursor: pointer;
    font-size: 16px;
}

@keyframes galaxyMove {
    0% {
        background-position: 0% 50%;
    }

    50% {
        background-position: 100% 50%;
    }

    100% {
        background-position: 0% 50%;
    }
}

@keyframes gradient-shift {
    0% {
        background-position: 0% 50%;
    }

    50% {
        background-position: 100% 50%;
    }

    100% {
        background-position: 0% 50%;
    }
}

@keyframes pulse {
    0% {
        transform: scale(1);
    }

    100% {
        transform: scale(1.05);
    }
}
</style>


在我用 AI 生成的这个项目中,最复杂的部分莫过于录音功能了,通过大约半小时的反复提问与细节优化,终于通过代码实现了语音录入、自定义语音样式、语音播放的功能。

Image

AI 的核心实现原理是通过原生录音事件完成的

// 创建音频上下文
const AudioContext = window.AudioContext || (window as any).webkitAudioContext
audioContext = new AudioContext()

// 创建分析器
analyser = audioContext.createAnalyser()
analyser.fftSize = 256 // 必须是2的幂次方
const bufferLength = analyser.frequencyBinCount // 通常是fftSize的一半
dataArray = new Uint8Array(bufferLength)

// ...

// 请求录音权限
navigator.mediaDevices
  .getUserMedia({ audio: true })
  .then((stream) => {
    resolve(stream)
  })
  .catch((err) => {
    console.error('获取录音权限失败:', err)
    Toast('获取录音权限失败,请检查权限设置')
    reject(err)
  })

// 创建MediaRecorder实例
recorder = new MediaRecorder(stream as MediaStream)


但经过实际测试,「在 IOS 的 webview 中」,这种方案兼容性不好,在低版本的 IOS 中,录音播放异常,这与原生录音方法生成的录音格式有关。

于是,通过搜索,最终使用了record-core实现了录音功能,并让 AI 帮我重构了代码。

对于数据的离线储存,通过提问 AI 后,选用了 「indexDB 的储存方案」。但在实测中,播放语音时会报错:

Image

于是,通过提问 AI 继续修复与优化。

Image

在经过不断地测试与 AI 修复后,功能终于是没有任何问题了。在整体项目生成后,就是利用 AI 提问不断完善一些小功能

Image
Image

用 AI 生成项目的过程需要一步步的提问,让 AI 优化细节,这个过程其实是比较漫长的,但对于不懂技术或者不想自己写代码的同学而言,这都不是什么问题。

项目开发完毕,就可以部署在线上体验了。

项目部署

要发布线上,目前最方便的方式就是在 Trae 中配置 MCP,一键部署。

❝
Image
❞

部署前,我们需要先配置掘金的 MCP, 在 Trae 或 IDE 的 MCP 配置中,添加以下 JSON 配置:

{
  "mcpServers": {
    "juejin-deploy-mcp": {
      "command": "npx",
      "args": [
        "--registry=https://registry.npmjs.org",
        "-y",
        "@juejin-team/mcp-server@latest"
      ],
      "env": {
        "JUEJIN_TOKEN": ""
      }
    }
  }
}
Image

注意,配置项中的JUEJIN_TOKEN需要填写你自己的掘金 Token,获取地址如下:

❝

www.zhihu.com/[3]

❞
Image

点击添加后,你就可以在你的内置智能体中看到到

Image

现在,你只需要切换到这个智能体,给出对应的部署提示即可。

Image

部署成功后,会在对话框看到部署链接,点击后就可以看到效果。

Image

总结

通过本文可以看出,借助 AI 从零开发一个完整的程序已不再是梦想,这让越来越多的人有机会成为创作者。而要用好 AI 开发产品,提示词(prompt)的质量至关重要。令人惊喜的是,优秀的提示词本身也可以由 AI 生成。用 AI 生成 prompt,再用这个 prompt 指导 AI 开发产品,这种 “AI 调用 AI” 的方式,既魔幻又充满趣味。

AI编程资讯AI Coding专区指南:https://aicoding.juejin.cn/aicoding

图片
点击"阅读原文"了解详情~