前提条件

构建内容

您可以利用面板小程序开发构建出一个基于 Ray 框架的 AI 录音转写总结设备面板。

所需条件

详见 面板小程序 > 搭建环境

首先需要创建一个产品,定义产品有哪些功能点,然后再在面板中一一实现这些功能点。

注册登录 涂鸦开发者平台,并在平台创建产品:

  1. 单击页面左侧 产品 > 产品开发,在 产品开发 页面单击 创建产品
  2. 标准类目 下选择产品类目,在右侧选择想要创建的产品。选择智能化方式和产品方案,并完善产品信息。
  3. 单击 创建产品 按钮,完成产品创建。
  4. 产品创建完成后,进入到 添加标准功能 页面,根据实际需求选择对应的开关功能点,可以直接使用默认 DP 配置,然后单击 确定
  5. 在产品详情页复制产品对应的 PID,联系您的项目经理,提供该 PID 以配置开启 产品 AI 能力
  6. 确认已开启产品 AI 能力后,可以在 01 功能定义 下看到 产品 AI 能力 页签。 在 已选产品 AI 能力 下可以看到当前产品使用的 AI 能力,也可以单击 添加产品 AI 能力 来添加其他 AI 功能。
  7. 已选 AI Agent > 面板端智能体 下,单击 选择智能体
  8. 添加面板端智能体 页面,选择智能体的实现方式为 方式 2:基于模板创建
  9. AI 转录 下,勾选 选择此模板创建 Agent,并单击 确定
  10. 在已选择的智能体下,单击 查看详情
  11. 复制智能体对应的 ID,保存备用。

开发者平台创建面板小程序

面板小程序的开发在 小程序开发者 平台上进行操作,首先请前往 小程序开发者平台 完成平台的注册登录。

详细操作步骤可以参考 面板小程序 > 创建面板小程序

IDE 基于模板创建项目工程

打开 IDE 创建一个基于 AI录音转写总结模版 的面板小程序项目,需要在 Tuya MiniApp IDE 上进行操作。

详细操作步骤可以参考 面板小程序 > 初始化项目工程

在项目内配置您在前面 创建产品 > 步骤 11 中复制的智能体 ID,模版代码 src/pages/Home/index

// 智能体 ID,修改为产品配置的智能体 ID
const AGENT_ID = "xxx";

同时,需要提前配置音频的语言 Code 参数,或者实现语言 Code 选择的交互,模版代码 src/pages/Home/index;语言 Code 格式如: en-US、zh-CN、fr-FR、ja-JP 等。

// App 启动回调
startAIAudioTranscriptionTask({
  devId,
  businessCode: AGENT_ID,
  key: storageKey.current,
  language: "zh-CN", // 录音音频语言
  duration: Math.floor(duration / 1000),
  template: "default", // 当前仅支持 default,不可更改
});

这样就完成了 AI 录音转写总结面板的基础配置。

录音通过 音频 API-InnerAudioContext 实现。


// 音频文件格式 固定mp3
const AUDIO_FILE_SUFFIX = "mp3";
const recorderManagerRef = useRef(null);
const [isRecording, setIsRecording] = useState(false);
const [tempAudioFilePath, setTempAudioFilePath] = useState("");
const [duration, setDuration] = useState(0); // 录音文件时长,毫秒
const tempPathRef = useRef(null); // 录音文件临时路径

useEffect(() => {
  recorderManagerRef.current = ty.getRecorderManager();
}, []);

// 开始录音
const handleStart = () => {
  try {
    recorderManagerRef.current.start({
      frameSize: undefined,
      sampleRate: 16000,
      numberOfChannels: 1,
      format: AUDIO_FILE_SUFFIX,
      success: (d) => {
        tempPathRef.current = d.tempFilePath;
        setIsRecording(true);
      },
      fail: (err) => {},
    });
  } catch (error) {}
};

// 结束录音
const handleStop = () => {
  try {
    recorderManagerRef.current.stop({
      success: (d) => {
        setTimeout(() => {
          if (tempPathRef.current) {
            setTempAudioFilePath(tempPathRef.current);
          }
          ty.getAudioFileDuration({
            path: tempPathRef.current,
            success: (res) => {
              if (res.duration) {
                setDuration(res.duration);
              }
            },
            fail: (err) => {},
          });
          setIsRecording(false);
        }, 1000);
      },
    });
  } catch (error) {}
};

录音结束,获取录音临时文件路径后,进行文件上传并发起转录。

import {
  getAIAudioTranscriptionStorageConfig,
  startAIAudioTranscriptionTask,
} from "@ray-js/ray";

// 音频文件格式 固定为 MP3
const AUDIO_FILE_SUFFIX = "mp3";
// 0-未开始 1-文件上传中 2-文件上传成功 3-转录中 4-转录完成
const [transferStatus, setTransferStatus] = useState(0);
// 音频文件Key
const storageKey = useRef();

const handleStartTransfer = async () => {
  try {
    ty.showLoading({ title: "" });
    setTransferStatus(1);
    // 请求云存储授权 Token
    const storageConfig = await getAIAudioTranscriptionStorageConfig({
      devId,
      name: `audio_${Math.round(Math.random() * 100)}_${new Date().getTime()}`,
      businessCode: AGENT_ID,
      suffix: AUDIO_FILE_SUFFIX,
    });
    const { headers, key, url } = storageConfig as any;
    storageKey.current = key;
    // 上传文件
    const task = ty.uploadFile({
      url,
      filePath: tempAudioFilePath,
      name: key,
      header: headers,
      success: (res) => {
        setTransferStatus(2);
        startAIAudioTranscriptionTask({
          devId,
          businessCode: AGENT_ID,
          key: storageKey.current,
          language: "zh-CN", // 录音语言
          duration: Math.floor(duration / 1000),
          template: "default",
        })
          .then(() => {
            setTransferStatus(3);
          })
          .catch((e) => {
            ty.showToast({ title: "文件转录失败" });
          });
      },
      fail: (err) => {
        console.log("Upload fail", err);
        ty.showToast({ title: "文件上传失败" });
        setTransferStatus(0);
      },
    });
    task.onProgressUpdate((res) => {
      console.log("Upload progress", res.progress);
    });
    task.onHeadersReceived((res) => {
      console.log("Upload headers", res);
    });
    ty.hideLoading();
  } catch (error) {
    ty.hideLoading();
  }
};

发起转录任务且接口返回成功后,需要轮询转录状态接口查询任务进行状态

import { getAIAudioTranscriptionStatus } from "@ray-js/ray";

const intervalId = useRef(null);
const getTransferProcessStatus = useCallback(async () => {
  try {
    // 对已上传且转录中的录音进行轮询
    const transferStatusList = await getAIAudioTranscriptionStatus({
      devId,
      keys: storageKey.current,
      businessCode: AGENT_ID,
    });
    // 1 - 已上传
    // 2 - 转录中
    // 9 - 已完成
    // 100 - 错误
    if (transferStatusList?.[0].status === 9) {
      clearInterval(intervalId.current);
      setTransferStatus(4);
    } else if (transferStatusList?.[0].status === 100) {
      clearInterval(intervalId.current);
      setTransferStatus(0);
      ty.showToast({ title: "转录失败 code:100" });
    }
  } catch (error) {
    console.log(error);
    clearInterval(intervalId.current);
  }
}, []);

当轮询转录状态接口返回结果 status=9 时,表示转录、总结完成,此时可以调用接口分别查询转录、总结结果。

import { getAIAudioTranscriptionSttText, getAIAudioTranscriptionSummary } from "@ray-js/ray";

getAIAudioTranscriptionSttText({
  devId,
  key: storageKey.current,
  businessCode: AGENT_ID,
}).then((d: any) => {
  if (d?.length) {
    setSttList(d);
  }
});
getAIAudioTranscriptionSummary({
  devId,
  key: storageKey.current,
  businessCode: AGENT_ID,
}).then((d: any) => {
  if (d?.summary) {
    setSummary(d?.summary);
  }
});