详见 面板小程序 > 搭建环境。
IPC 精彩时刻功能,是涂鸦云开发者平台为所有带摄像头的智能硬件,专门打造的"AI 视频增值服务",具体运转机制为:通过 AI 大模型自动识别预设的目标或自定义事件后,就能抓取摄像头中的精彩片段(如宠物卖萌互动、宝宝第一次走路、日出日落等),并一键生成带特效的专属 Vlog。能够满足婴儿/宠物看护、生活娱乐、风景旅行等全场景需求,帮助用户轻松记录生活中的高光时刻!
面板小程序的开发在 小程序开发者 平台上进行操作,首先请前往 小程序开发者平台 完成平台的注册登录。
详细操作步骤,请参考 创建面板小程序。
打开物料广场基于 IPC 精彩时刻通用模版 物料,创建一个小程序项目,需要在 Tuya MiniApp IDE 上操作。
详细操作步骤,请参考 初始化项目工程。
完成以上步骤后,一个面板小程序的开发模板初始化完成。以下为工程目录的介绍:
├── src
│ ├── api // 面板所有云端 API 请求聚合文件
│ ├── components
│ │ ├── GlobalToast // 全局轻弹窗
│ │ ├── IconFont // svg 图标容器组件
│ │ ├── MusicSelectModal // 背景音乐选择弹窗
│ │ ├── TopBar // 顶部信息栏
│ │ ├── TouchableOpacity // 点击按钮组件
│ │ ├── Video // 视频容器组件
│ │ ├── VideoSeeker // 视频滑动条组件
│ ├── constant
│ │ ├── index.ts // 存放所有的常量配置
│ ├── devices // 设备模型
│ ├── features // 业务函数集
│ ├── hooks // hooks
│ ├── i18n // 多语言
│ ├── iconfont // svg图标
│ ├── pages
│ │ ├── ai-all // 精彩时刻全量相册
│ │ ├── ai-video // On-App AI 精彩时刻 AI 编辑
│ │ ├── ai-vision-setting // 精彩时刻服务详情编辑
│ │ ├── bind-device // 服务关联相关设备
│ │ ├── device-service-setting // 精彩时刻智能识别时间设置
│ │ ├── player // 视频播放器
│ │ ├── preview-image // 图片预览组件
│ │ ├── select-lang // 多语言设置
│ ├── redux // redux
│ ├── res // 图片资源
│ ├── share // 公共资源集
│ ├── utils // 通用函数集
│ ├── styles // 全局样式
│ ├── types // 全局类型定义
│ ├── utils // 业务常用工具方法
│ ├── app.less
│ ├── app.tsx
│ ├── global.config.ts
│ ├── mixins.less // less mixins
│ ├── routes.config.ts // 配置路由
│ ├── variables.less // less variables
精彩时刻服务详情主要包含以下 5 个板块:
相关代码段
import {
showLoading,
hideLoading,
showToast,
albumSettingEdit,
getAlbumSetting,
albumSettingSave,
} from "@ray-js/ray";
const {
homeId,
stockId,
visionBoxId,
serviceDetails,
statusChange,
settingRef,
onSaved,
} = props;
import { unstable_batchedUpdates as batchedUpdates } from "@ray-core/ray";
const { data: albumSetting, runAsync: getSetting } = useRequest(
getAlbumSetting,
{
manual: true,
}
);
useEffect(() => {
if (visionBoxId && homeId && stockId) {
showLoading({ title: "" });
getSetting({
gid: homeId,
stockId,
})
.then((res) => {
if (!res) return;
batchedUpdates(() => {
pauseChangeStatus.current = true;
form.setValue("albumName", res.albumName);
form.setValue("enableStatus", !!res.enableStatus);
form.setValue("timedCaptureConfig", res.timedCaptureConfig);
form.setValue("smartCaptureConfig", res.smartCaptureConfig);
pauseChangeStatus.current = false;
}, null);
})
.finally(() => hideLoading());
}
}, [visionBoxId, homeId, stockId]);
相关代码段
import { useForm, useWatch } from "react-hook-form";
const form = useForm({ mode: "onChange" });
const [timedCaptureConfig, smartCaptureConfig] = useWatch({
control: form.control,
name: ["timedCaptureConfig", "smartCaptureConfig"],
});
const handSave = async (): Promise<boolean> => {
return new Promise((resolve) => {
if (!serviceDetails?.cloudService?.bindDeviceNum) {
showToast({
icon: "none",
title: I18n.t("aiVisual.requiredDevice.tip"),
});
resolve(false);
return;
}
if (loadingRef.current) return;
form.trigger().then(async (res) => {
if (res) {
loadingRef.current = true;
const values = form.getValues();
const params: Parameters<typeof albumSettingEdit>[0] = {
gid: homeId,
stockId,
albumId: visionBoxId,
albumName: values.albumName,
enableStatus: values.enableStatus ? 1 : 0,
timedCaptureConfig: values.timedCaptureConfig || [],
smartCaptureConfig: values.smartCaptureConfig || [],
};
showLoading({ title: "" });
let ret;
if (visionBoxId) {
ret = await albumSettingEdit(params);
} else {
ret = await albumSettingSave(params);
}
hideLoading();
if (ret) {
hideLoading();
const txt = I18n.t("common.save.success");
showToast({
title: txt,
icon: "success",
});
changeSettingStatus("saved");
onSaved && onSaved(visionBoxId || ret);
resolve(true);
loadingRef.current = false;
} else {
showToast({
title: I18n.t("common.save.failed"),
icon: "error",
});
resolve(false);
loadingRef.current = false;
}
}
});
});
};
精彩时刻精彩时刻智能相册主要包含以下 4 块内容:
相关代码段
import {
Text,
View,
showToast,
hideLoading,
showLoading,
albumVideoDateCount,
} from "@ray-js/ray";
const [aiDatas, setAiDatas] = useState<IAlbumVideoDateCountRes[]>([])
const [todayData, setTodayData] = useState<IAlbumVideoDateCountRes>()
const [loading, setLoading] = useState(true);
const init = () => {
if (isNaN(Number(timeAlbumId)) || isNaN(Number(stockId))) {
showToast({ title: "request.param.error" });
setLoading(false);
return;
}
const param: IAlbumVideoDateCountReq = {
stockId: Number(stockId), // 库存id
gid: homeId, // 家庭id
timeAlbumId: Number(timeAlbumId), // 相册id
};
showLoading({ title: "" });
albumVideoDateCount(param).then((result: IAlbumVideoDateCountRes[]) => {
hideLoading();
setLoading(false);
if (!result || !result.length) return;
const res = result.map((item) => {
const fileUrl = item.coverInfo?.fileUrl || "";
const fileName = fileUrl.split("?")[0].split("/").pop();
return {
...item,
coverInfo: { ...item.coverInfo, ...{ fileName } },
};
});
if (res.length > 0) {
if (res[0].type === "today") {
setTodayData(res[0]);
}
const d = res.filter((item) => item.type !== "today");
setAiDatas(d);
}
});
};
精彩时刻全量相册主要包含以下内容:
相关代码段
import {
Button,
View,
showToast,
showModal,
albumFileDelete,
albumVideoFileList,
} from "@ray-js/ray";
import { useBatchDecryptImage } from "@/features";
import { deleteImages } from "@ray-js/ray-ipc-decrypt-image";
const { timeAlbumId, stockId, secret, onShow, homeId } = props;
const batchLoadInstance = useBatchDecryptImage(true, 400);
const getPageData = (params) => {
return new Promise((resolve, reject) => {
if (isNaN(Number(timeAlbumId)) || isNaN(Number(stockId))) {
showToast({ title: I18n.t("request.param.error") });
return;
}
const param: IAlbumVideoFileListReq = {
stockId: Number(stockId), // 库存id
gid: homeId, // 家庭id
timeAlbumId: Number(timeAlbumId), // 相册id
pageNum: params.page,
// 为了避免
endTime: endTimeRef.current,
// endTime: 1740386449,
pageSize: params.pageSize,
};
albumVideoFileList(param).then((result: IAlbumVideoFileListRes) => {
if (result && result.list && result.list.length > 0) {
const res = result.list.map((item) => {
const fileUrl = item.coverInfo?.fileUrl || "";
const fileName = fileUrl.split("?")[0].split("/").pop();
batchLoadInstance.addOriginData({
fileUrl,
decryptKey: secret,
});
return {
...item,
coverInfo: { ...item.coverInfo, ...{ fileName } },
};
});
batchLoadInstance.loadAllOriginData();
resolve({ data: res, total: result.total });
return;
}
resolve({ data: [] });
});
});
};
相关代码段
import {
Button,
View,
showToast,
showModal,
albumFileDelete,
albumVideoFileList,
} from '@ray-js/ray'
import {
cancelVideoDownload,
composeVideos,
getAuthorize,
imageDownload,
videoDownload,
} from '@/features/downloadImage'
import { jumpToAiVideoEdit, jumpToPlayer } from '@/features/jump-to-page'
const [selectedIds, setSelectedIds] = useImmer([])
const [selectIdIndexMap, setSelectIdIndexMap] = useState<Map<number, number>>(new Map())
const getSelectDatas = () => {
const maxCount = selectedIds.length
let currentCount = 0
const result = []
for (let i = 0; i < cloudData.length; i++) {
if (currentCount >= maxCount) break
const item = cloudData[i]
const index = selectedIds.indexOf(item.id)
if (index >= 0) {
result[index] = item
currentCount++
}
}
return result
}
const onOperateCompose = async (e, type) => {
if (type === EOperate.download) {
isCloseDownload.current = false
if (selectedIds.length > MAX_DOWNLOAD_SIZE) {
showToast({
title: I18n.format('download.count.limit.desc', { time: MAX_DOWNLOAD_SIZE }),
icon: 'error',
})
return
}
const selectDatas = getSelectDatas()
const bl = await getAuthorize()
if (bl) {
onShow({ show: true, title: I18n.t('downloading.video'), type: ELoadingType.loading })
const indexErrs = []
// 下载一个成功后,再去下载另一个
for (let i = 0; i < selectDatas.length; i++) {
if (isCloseDownload.current) {
break
}
const ele = selectDatas[i]
if (ele.type === 1 || ele.type === 2) {
const dl = await videoDownload(deviceId, ele.mediaInfo?.fileUrl, secret)
if (dl === -1) {
indexErrs.push(i)
}
} else if (ele.type === 0) {
// 视频不存在,就下载图片
const dl = await imageDownload(deviceId, ele.coverInfo?.fileUrl, secret)
if (dl === -1) {
indexErrs.push(i)
}
} else {
indexErrs.push(i) // 说明云端返回类型不对
}
}
onShow({ show: false, title: I18n.t('download.success'), type: ELoadingType.success })
if (indexErrs.length > 0) {
const str = indexErrs.join(',')
showModal({
title: '',
content: str + I18n.t('download.clip.error'),
showCancel: false,
confirmText: I18n.t('common.confirm'),
})
} else {
setSelectedIds([])
setSelectIdIndexMap(null)
}
}
} else if (type === EOperate.compose) {
if (selectedIds.length > MAX_COMPOSE_SIZE) {
showToast({
title: I18n.format('compose.count.limit.desc', { time: MAX_COMPOSE_SIZE }),
icon: 'error',
})
return
}
const selectDatas = getSelectDatas()
const isVideos = isExistSingleImage(selectDatas)
if (isVideos) {
showToast({ title: I18n.t('compose.only.media'), icon: 'error' })
return
}
const bl = await getAuthorize()
if (bl) {
const mediaInfos = selectDatas.map((item) => {
return {
fileUrl: item.mediaInfo?.fileUrl || '',
key: secret,
}
})
const json = JSON.stringify({ fileInfo: mediaInfos })
onShow({ show: true, title: I18n.t('composing.video'), type: ELoadingType.loading })
const result = await composeVideos(deviceId, json)
if (result === -1) {
onShow({ show: false, title: I18n.t('compose.fail'), type: ELoadingType.error })
} else {
onShow({ show: false, title: I18n.t('compose.success'), type: ELoadingType.success })
setSelectedIds([])
setSelectIdIndexMap(null)
const { path = '', thingfilePath = '' } = result as Record<string, any>
setTimeout(() => {
jumpToAiVideoEdit({ videoPath: thingfilePath, videoOriginPath: path })
}, 0)
}
}
} else if (type === EOperate.delete) {
if (isNaN(Number(stockId))) {
showToast({ title: I18n.t('request.param.error') })
return
}
if (selectedIds.length > MAX_DELETE_SIZE) {
showToast({
title: I18n.format('delete.count.limit.desc', { time: MAX_DELETE_SIZE }),
icon: 'error',
})
return
}
showModal({
title: '',
content: I18n.t('module.delete.desc'),
cancelText: I18n.t('common.cancel'),
confirmText: I18n.t('aiVisual.delete'),
success: async ({ confirm, cancel }) => {
console.log('res===1', confirm, cancel)
if (confirm) {
const param: IAlbumFileDeleteReq = {
stockId: Number(stockId),
gid: homeId,
timeAlbumId: Number(timeAlbumId),
albumRecordIds: selectedIds.join(','),
}
const result = await albumFileDelete(param)
if (result) {
showToast({ title: I18n.t('aiVisual.delete.success'), icon: 'success' })
const curCloudDatas = getCurCloudDatas()
const selectDatas = curCloudDatas.filter((item) => selectedIds.includes(item.id))
const fileNames = selectDatas.map((item) => item.coverInfo.fileName)
deleteImages(deviceId, fileNames)
setDeletedIds([...new Set([...deletedIds, ...selectedIds])])
setSelectedIds([])
setSelectIdIndexMap(null)
} else {
showToast({ title: I18n.t('aiVisual.delete.fail'), icon: 'error' })
}
}
},
})
}
}
精彩时刻视频 AI 编辑主要包含以下内容:
视频主体突出 AI 编辑相关开发信息,可参考 AI 视频流主体突出功能模版。
相关代码段
import { ai } from "@ray-js/ray";
import { createTempVideoRoot } from "@/utils";
const {
objectDetectCreate,
objectDetectDestroy,
objectDetectForVideo,
objectDetectForVideoCancel,
offVideoObjectDetectProgress,
onVideoObjectDetectProgress,
} = ai;
// 当前 AI 视频的编辑状态
const [handleState, setHandleState] = useState("idle");
// 当前 AI 视频流处理进度
const [progressState, setProgressState] = useState(0);
// 注册 App AI 实例
useEffect(() => {
objectDetectCreate();
return () => {
// 页面销毁时同时销毁 App AI 实例
objectDetectDestroy();
audioManager.destroy({
success: (res) => {
console.log("==destroy2==success", res);
},
fail: (error) => {
console.log("==destroy2==fail", error);
},
});
};
}, []);
// AI 视频流处理功能
const handleVideoByAI = (
detectType: number,
imageEditType: number,
musicPath = ""
) => {
const tempVideoPath = createTempVideoRoot();
onVideoObjectDetectProgress(handleListenerProgress);
privacyProtectDetectForVideo({
inputVideoPath: videoOriginSrc,
outputVideoPath: tempVideoPath,
detectType,
musicPath,
originAudioVolume: volumeObj.video / 100,
overlayAudioVolume: volumeObj.music / 100,
imageEditType,
audioEditType: 3,
success: ({ path }) => {
console.log("===path", path, tempVideoPath);
offVideoObjectDetectProgress(handleListenerProgress);
setProgressState(0);
fetchVideoThumbnails({
filePath: path,
startTime: 0,
endTime: 10,
thumbnailCount: 1,
thumbnailWidth: 375,
thumbnailHeight: 212,
success: (res) => {
console.log("===fetchVideoThumbnails==res", res);
setHandleState("success");
setVideoSrc(path);
setVideoOriginSrc(path);
setPosterSrc(res?.thumbnailsPath[0]);
showToast({
title: I18n.t("dsc_ai_generates_success"),
icon: "success",
});
},
fail: ({ errorMsg }) => {
console.log("==fetchVideoThumbnails==fail==", errorMsg);
},
});
},
fail: ({ errorMsg }) => {
console.log("==objectDetectForVideo==fail==", errorMsg);
offVideoObjectDetectProgress(handleListenerProgress);
setProgressState(0);
setHandleState("fail");
setHandleState("selectSkill");
setIsShowAISkills(true);
showToast({
title: I18n.t("dsc_ai_generates_fail"),
icon: "error",
});
},
});
};
// 打断 AI 隐私保护视频流生成
const handleCancelAIProcess = () => {
objectDetectForVideoCancel({
success: () => {
setHandleState("select");
},
});
};
具体 AI 技术方案介绍,详见:视频解决方案—IPC 精彩时刻通用方案
具体 API 相关介绍,详见:开发者文档—AI 基础包