第3.9篇:AI 编排流程——从拍照到动画的完整链路
难度:⭐⭐⭐ 高级
前置知识:第 2.1 ~ 2.9 篇、第 3.1 ~ 3.8 篇
涉及源文件:products/default/src/main/ets/pages/PhotoRecognitionPage.ets→RecognitionWaitingPage.ets→RecognitionResultPage.ets
概述
"画伴梦工厂"的核心功能,是将儿童的画作通过 AI 转化为动画。支撑这一功能的并非单个页面或单个服务,而是一条完整的编排链路:从拍照采集画作开始,经过图片压缩上传、图生视频任务提交与轮询、视频下载保存,再到最终的结果展示,跨越三个页面、调用多个 AI 服务,整个流程高度耦合且涉及大量异步状态管理。
本文将从架构视角,完整拆解这条"拍照 → 识别 → 生成 → 等待 → 展示"的全链路,重点分析多页面编排、状态机设计、并行任务调度以及跨页面数据传输等核心话题。
一、三页面的编排架构
整个链路由三个独立页面顺序衔接而成:
PhotoRecognitionPage ──router.push──→ RecognitionWaitingPage ──router.push──→ RecognitionResultPage (拍照采集) (AI 生成等待) (结果展示)每个页面职责清晰、边界分明:
| 页面 | 角色 | 核心职责 |
|---|---|---|
PhotoRecognitionPage | 编排入口 | 采集画作(拍照/相册),初始化generationProgress和noticeText |
RecognitionWaitingPage | 编排中枢 | 接收图片数据,驱动 AI 生成流程,管理进度动画与状态机,保存作品 |
RecognitionResultPage | 编排终点 | 展示生成结果(视频或静态识别信息),提供返回作品集的入口 |
这种"三页面编排"模式是鸿蒙应用中的经典实践——将复杂流程拆解为独立的页面单元,每个页面通过 Router API 传递参数、切换页面,既保证了代码的内聚性,又降低了单个页面的复杂度。
二、Page 1:PhotoRecognitionPage——编排入口
PhotoRecognitionPage是整个流程的入口页面,它本身并不参与 AI 编排逻辑,而是作为采集容器,将拍照/选图的能力委托给子组件PhotoRecognitionComponent。
2.1 页面结构
@Entry@Componentstruct PhotoRecognitionPage{@StateprivategenerationProgress:number=0;@StateprivatenoticeText:string='';build(){Scroll(){Column(){this.Header()// 标题栏 "拍画变动画"Progress({value:this.generationProgress,total:100})// 进度条PhotoRecognitionComponent({// 子组件generationProgress:$generationProgress,noticeText:$noticeText})this.NoticeBar()// 通知栏}}}}2.2 @Link 状态同步
页面通过$语法将@State generationProgress和@State noticeText以双向绑定方式传递给子组件:
PhotoRecognitionComponent({generationProgress:$generationProgress,// 双向绑定noticeText:$noticeText})子组件PhotoRecognitionComponent内部通过@Link接收:
@Componentexportstruct PhotoRecognitionComponent{@LinkgenerationProgress:number;@LinknoticeText:string;// ...}当用户在子组件中完成了拍照或选图操作后,子组件会更新generationProgress(例如设为 35)和noticeText(例如"已采集画作,可以直接生成动画"),这些变化会立即反映到父页面的 UI 上。
2.3 页面跳转时机
拍照/选图完成后,子组件内部通过 Router API 跳转到等待页面:
this.getUIContext().getRouter().pushUrl({url:'pages/RecognitionWaitingPage',params:{source:'画作识别',workSource:'photo',prompt:'...',imageUri:'...',coverUri:'...',recognitionResult:'...'}});三、Page 2:RecognitionWaitingPage——编排中枢
RecognitionWaitingPage是整个流程的核心,承载着状态机管理、并行任务调度、进度通知和作品保存四大职责。这是全项目中最具编排色彩的页面。
3.1 状态机设计
页面通过 5 个核心@State变量构建了一个完整的等待流程状态机:
@Stateprivateprogress:number=12;// 进度值 0-100@StateprivateactiveStep:number=0;// 当前步骤索引 0-3@Stateprivatefailed:boolean=false;// 是否失败@Stateprivatecompleted:boolean=false;// 是否完成@StateprivatestatusText:string='正在准备生成任务';// 状态文本@StateprivatevideoUri:string='';// 生成后的视频 URI@StateprivateworkId:string='';// 保存后的作品 ID这些状态变量定义了四种互斥的页面状态:
| 状态 | progress | failed | completed | UI 表现 |
|---|---|---|---|---|
| 加载中 | 12~97 | false | false | 进度条动画、等待提示、灰色按钮 |
| 已完成 | 100 | false | true | 进度条满、"查看视频结果"按钮亮起 |
| 已失败 | 任意值 | true | false | 红色错误文本、"重试生成"按钮 |
| 重试中 | 重置为 12 | 重置 | 重置 | 回到加载中状态 |
3.2 双轨并行:定时器动画 + 异步生成
页面启动时(aboutToAppear),同时触发两条并行的执行路径:
aboutToAppear(){// 1. 读取路由参数constparams=this.getUIContext().getRouter().getParams()asWaitingParams;// ... 逐个字段赋值// 2. 启动前台动画(定时器驱动)this.startWaitingTimer();// 3. 启动后台实际生成(异步 AI 调用)this.startGeneration();}这两条路径相互独立又彼此协作:
时间轴 ──────────────────────────────────────────────→ 前台定时器 (setInterval,每 1200ms) ├── 更新 animationFrame → 气泡动画 ├── 更新 waitingTip → 轮换提示文字 └── 更新 progress → 进度条增长(上限 97%) 后台生成 (async/await) ├── prepareUploadBase64 → 压缩图片 ├── createImg2VideoTask → 提交任务 ├── pollImg2VideoTask → 轮询结果(最长 6min) └── downloadVideo → 下载到本地 └── WorkRepository.save → 持久化 └── completed = true设计亮点:进度条由前台定时器驱动(从 12% 递增到 97%),而非等待后台任务返回真实进度。这样做的好处是——即使用户的图片处理时间较长,UI 也始终保持动效,不会出现"卡住"的感觉。最终的后 3%(97→100)由后台任务完成时一次性推进。
3.3 定时器动画机制
privatestartWaitingTimer(){this.timerId=setInterval(()=>{if(this.failed||this.completed){clearInterval(this.timerId);// 状态终止时停止return;}this.animationFrame=(this.animationFrame+1)%4;// 气泡动画帧this.waitingTip=WAITING_TIPS[this.animationFrame];// 轮换提示if(this.progress<92){this.progress=Math.min(92,this.progress+3);// 快速增长阶段}else{this.progress=Math.min(97,this.progress+1);// 慢速增长阶段}this.activeStep=Math.min(3,Math.floor(this.progress/28));// 步骤索引},1200);}关键设计点:
- 分阶段增速:92% 之前每次 +3,92% 之后每次 +1,模拟"先快后慢"的真实生成体验
- 步骤映射:通过
Math.floor(progress / 28)将进度值映射到 0-3 的步骤索引 - 自动终止:当
failed或completed为 true 时清理定时器,避免资源泄漏 - 生命周期对称:在
aboutToDisappear中清理定时器
3.4 进度通知机制(onStatus 回调)
后台生成任务通过onStatus回调函数将内部状态实时同步给页面:
constgeneratedVideo:GeneratedVideo=awaitAIGenerationService.generateVideo(this.imageUri,this.prompt,(message:string)=>{this.statusText=message;// 实时更新状态文本});AIGenerationService.generateVideo内部在各个关键节点调用onStatus:
| 阶段 | 回调消息 |
|---|---|
| 准备阶段 | '正在压缩图片' |
| 压缩完成 | '图片已压缩,正在上传' |
| 任务提交 | '任务已提交,正在生成动画' |
| 轮询中 | '正在等待动画生成,第 N 次检查' |
| 网络波动 | '网络有点慢,继续等待动画完成' |
| 下载阶段 | '动画已生成,正在保存到本地' |
| 最终完成 | '视频已生成并保存到作品' |
这种"回调通知"模式实现了非阻塞的进度反馈——生成任务在后台异步执行,UI 层通过回调被动接收状态更新,两者完全解耦。
3.5 后台生成完整链路
startGeneration方法的执行链路如下:
startGeneration() │ ├─ prepareUploadBase64(imageUri, onStatus) │ ├─ readImageAsArrayBuffer → 读取图片为二进制 │ ├─ compressImageBuffer → 压缩至 ~900KB(78% 质量,1280px 边缘) │ └─ arrayBufferToBase64 → 编码为 Base64 字符串 │ ├─ createImg2VideoTask(base64) │ ├─ POST /img2video/volcengine/img2video │ └─ 返回 taskId(任务唯一标识) │ ├─ pollImg2VideoTask(taskId, onStatus) │ ├─ 每 5 秒查询一次 /img2video/volcengine/img2videoStatus │ ├─ 最长等待 6 分钟(MAX_SEEDANCE_WAIT_MS = 360000ms) │ ├─ 检查 status === 1 且 videoUrl 不为空 │ └─ 超时或状态码异常则抛错 │ ├─ downloadVideo(remoteUrl, taskId) │ ├─ GET 请求下载视频 ArrayBuffer │ └─ 写入 filesDir/seedance_{taskId}.mp4 │ └─ 返回 GeneratedVideo { prompt, videoUri, taskId, remoteVideoUrl } │ ▼ (回到 startGeneration 方法) WorkRepository.createWork(workSource, prompt, coverUri, videoUri) WorkRepository.save(work) workId = work.id completed = true3.6 作品保存(WorkRepository)
生成成功后,页面立即将作品持久化:
constwork=WorkRepository.createWork(this.workSource,// 来源:'photo' | 'doodle' | 'ai-chat'generatedVideo.prompt,// 使用的 PromptfinalCoverUri,// 封面图generatedVideo.videoUri// 视频地址);WorkRepository.save(work);this.workId=work.id;作品保存后,即使应用重启,用户也能在"我的作品"中看到生成的动画。workId也会作为路由参数传递给结果页面,用于显示和后续操作。
3.7 错误处理与重试机制
当生成过程中任意环节抛出异常时,catch块捕获错误并更新 UI:
try{// ... 整个生成流程}catch(error){this.failed=true;this.statusText='生成失败:'+this.getErrorMessage(errorasError);}用户点击"重试生成"按钮时,执行完整的重置操作:
if(this.failed){// 重置所有状态到初始值this.progress=12;this.activeStep=0;this.animationFrame=0;this.waitingTip=WAITING_TIPS[0];// 重新启动双轨流程this.startWaitingTimer();this.startGeneration();}重置操作恢复了 5 个状态变量到初始值,然后重新启动定时器和生成任务,相当于一次完整的"重来"。
四、WAITING_STEPS:四步进度指示器
页面底部使用WAITING_STEPS数组渲染了一个四步进度指示器:
constWAITING_STEPS:string[]=['看看画里有什么',// Step 0'想一想怎么动',// Step 1'画出动画片段',// Step 2'保存到我的作品'// Step 3];每一步通过StepRow@Builder 渲染,activeStep控制其视觉状态:
@BuilderprivateStepRow(step:string,index:number){Row(){Text((index+1).toString())// 步骤编号.fontColor(this.activeStep>=index?'#FFFFFF':'#8A8FA4').backgroundColor(this.activeStep>=index?this.mint:'#ECECF6')Text(step)// 步骤描述.fontColor(this.activeStep>=index?this.ink:'#8A8FA4')Text(this.activeStep>index?'完成':// 状态标签(this.activeStep===index?'进行中':'等待')).fontColor(this.activeStep>=index?this.mint:'#9AA0B5')}}每个步骤有三种视觉状态:
| 状态 | 条件 | 步骤编号 | 文字颜色 | 标签 |
|---|---|---|---|---|
| 已完成 | activeStep > index | 白色底绿色字 | 深色 | “完成” |
| 进行中 | activeStep === index | 绿色底白字 | 深色 | “进行中” |
| 未开始 | activeStep < index | 灰色底白字 | 灰色 | “等待” |
四个步骤的进度映射关系为activeStep = Math.min(3, Math.floor(progress / 28)):
| 进度范围 | activeStep | 处于"进行中"的步骤 |
|---|---|---|
| 0~27 | 0 | 看看画里有什么 |
| 28~55 | 1 | 想一想怎么动 |
| 56~83 | 2 | 画出动画片段 |
| 84~100 | 3 | 保存到我的作品 |
五、Page 3:RecognitionResultPage——结果展示
生成完成后,用户跳转到RecognitionResultPage查看结果。该页面通过路由参数接收所有上游数据,根据videoUri的有无,在两种展示模式间切换。
5.1 参数接收与反序列化
aboutToAppear(){constparams=this.getUIContext().getRouter().getParams()asResultParams;// 逐个字段赋值...if(params&¶ms.recognitionResult){try{this.recognitionResult=JSON.parse(params.recognitionResult)asDrawingRecognitionResult;}catch(error){this.recognitionResult=ImageRecognitionService.getFallbackResult();}}}注意recognitionResult是以 JSON 字符串形式传递的(Router API 不支持传递复杂对象),所以在接收端需要通过JSON.parse反序列化,并用 try-catch 做容错处理。
5.2 双模式展示
页面根据videoUri判断展示哪种结果:
if(this.videoUri!==''){this.VideoResult()// 模式一:视频结果}else{this.RecognitionResult()// 模式二:静态识别结果}模式一:VideoResult——渲染完整的视频播放器:
Video({src:this.videoUri,previewUri:this.getPreviewUri(),controller:this.videoController}).controls(false)// 隐藏默认控件.autoPlay(true)// 自动播放.onStart(()=>{this.isPlaying=true;}).onPause(()=>{this.isPlaying=false;}).onFinish(()=>{this.isPlaying=false;})视频上方叠加了自定义的播放/暂停按钮和"已保存到作品"标签,营造更友好的交互体验。
模式二:RecognitionResult——展示静态图片和识别详情:
页面展示识别服务返回的结构化数据,包括主角、场景、情绪和动画建议四个维度,以及对应的置信度百分比:
主角:小恐龙 96% 场景:森林 91% 情绪:开心 88% 动画建议:跳跃动作 86%5.3 页面终点:跳转回作品集
两种模式下,底部的按钮最终都导航到首页的作品 Tab:
this.getUIContext().getRouter().replaceUrl({url:'pages/Index',params:{tab:'works'}});使用replaceUrl而非pushUrl,用户从结果页回到作品集后,后退按钮不会回到结果页,避免形成无效的导航循环。
六、跨页面数据传输全景
三个页面之间的数据传输通过 Router API 的params对象完成。整个链路中传输的完整数据如下:
PhotoRecognitionPage │ │ pushUrl → RecognitionWaitingPage │ params: │ source: string ← 来源标签(默认'画作识别') │ workSource: string ← 作品来源('photo'|'doodle'|'ai-chat') │ prompt: string ← AI 生成 Prompt │ imageUri: string ← 原始图片 URI │ coverUri: string ← 封面图片 URI │ recognitionResult: string ← JSON 序列化的识别结果 │ ▼ RecognitionWaitingPage │ │ (内部生成 videoUri 和 workId) │ │ pushUrl → RecognitionResultPage │ params: │ source: string ← 透传 │ workSource: string ← 透传 │ prompt: string ← 透传 │ imageUri: string ← 透传 │ coverUri: string ← 透传 │ recognitionResult: string ← 透传 │ videoUri: string ← 新增!生成的视频地址 │ workId: string ← 新增!保存后的作品 ID │ ▼ RecognitionResultPage │ │ replaceUrl → Index (tab=works) │ ▼ Index (作品集)这种设计体现了典型的管道模式——中间页面在透传上游参数的基础上,不断追加自己产生的数据,最终下游页面接收完整的上下文。
七、完整数据流与状态转换图
┌──────────────────────────────────────────────────────────────────┐ │ 状态转换总图 │ └──────────────────────────────────────────────────────────────────┘ [PhotoRecognitionPage] [RecognitionWaitingPage] [RecognitionResultPage] ┌─────────────┐ │ aboutToAppear │ │ 读取路由参数 │ └──────┬──────┘ │ ┌────────────┴────────────┐ │ │ ▼ ▼ ┌─────────────────┐ ┌─────────────────────┐ │ startWaitingTimer│ │ startGeneration │ │ (前台动画) │ │ (后台生成) │ │ │ │ │ │ progress: 12→97 │ │ prepareUploadBase64 │ │ activeStep: 0→3 │ │ │ │ │ animationFrame │ │ createImg2VideoTask │ │ 循环 0~3 │ │ │ │ └────────┬────────┘ │ pollImg2VideoTask │ │ │ (每5s轮询,最多6min) │ │ │ │ │ │ │ downloadVideo │ │ │ │ │ │ │ WorkRepository.save │ │ └────────┬────────────┘ │ │ │ ┌────────┴────────┐ │ │ completed=true │ │ │ videoUri=xxx │ │ │ workId=xxx │ │ └────────┬────────┘ │ │ │ (按钮触发 pushUrl) │ │ │ ▼ │ ┌─────────────────┐ │ │ RecognitionResult│ │ │ videoUri 存在? │ │ │ ├─是→ VideoResult│ │ │ └─否→ RecogResult│ │ │ │ │ │ │ Button → Index │ │ └─────────────────┘ │ (异常发生时) │ ▼ ┌──────────────┐ │ failed=true │ │ 显示错误信息 │ └──────┬───────┘ │ 用户点击"重试" │ ▼ ┌──────────────┐ │ 重置状态机 │ │ progress=12 │ │ activeStep=0 │ │ 重新执行 │ └──────────────┘八、服务调用时序图
从页面层面往下看,AIGenerationService.generateVideo内部包含四次网络请求和一次文件写入:
RecognitionWaitingPage AIGenerationService AI 后端服务 │ │ │ │──startGeneration()──────────────→│ │ │ │──prepareUploadBase64()───→ │ │ │ (读取图片 → 压缩 → Base64) │ │ │ │ │ │──createImg2VideoTask()────→ │ │ │ POST /img2video │ │ │←────── { taskId } ──────────│ │ │ │ │ │──pollImg2VideoTask()────────→│ │ │ POST /img2videoStatus │ │ │ (每 5 秒轮询) │ │ │ ←── { status:1, url } ────│ │ │ │ │ │──downloadVideo()───────────→ │ │ │ GET /{videoUrl} │ │ │←──── ArrayBuffer ───────────│ │ │ (写入 filesDir) │ │ │ │ │←──{ videoUri, prompt, taskId }──│ │ │ │ │ │──WorkRepository.save(work)─────→│ │ │ (持久化到 preferences) │ │ │ │ │ │ completed = true │ │九、架构设计要点总结
9.1 编排模式
| 要点 | 实现方式 |
|---|---|
| 页面拆分 | 三页面职责分离,各司其职 |
| 参数传递 | Router APIparams+ JSON 序列化 |
| 状态驱动 | 5 个核心@State变量构成状态机 |
| 并行调度 | setInterval(前台动画)+async/await(后台生成) |
| 进度通知 | onStatus回调函数模式 |
| 持久化 | WorkRepository保存到 preferences |
9.2 状态机设计价值
- 单一数据源:所有 UI 状态由
@State变量驱动,不存在多个状态副本 - 可预测转换:
failed/completed互斥,不会出现同时为 true 的非法状态 - 灵活重置:重试操作只需重置状态机的初始值,重新执行生成函数
- UI 自动同步:状态变化通过声明式绑定自动反映到界面
9.3 错误处理策略
| 错误类型 | 处理方式 |
|---|---|
| 图片读取/压缩失败 | 降级使用原图,通过onStatus通知用户 |
| 任务提交失败 | 透传错误信息,设failed=true,UI 显示"重试"按钮 |
| 轮询中超时 | 6 分钟后抛出"视频生成超时"错误 |
| 网络波动(轮询中) | 自动重试,继续等待 |
| JSON 解析失败 | try-catch 兜底,使用getFallbackResult() |
9.4 文件依赖关系
PhotoRecognitionPage.ets └── PhotoRecognitionComponent (components/CreationComponents.ets) RecognitionWaitingPage.ets ├── AIGenerationService (services/AIGenerationService.ets) │ ├── prepareUploadBase64 │ ├── createImg2VideoTask │ ├── pollImg2VideoTask │ └── downloadVideo └── WorkRepository (services/WorkRepository.ets) RecognitionResultPage.ets └── ImageRecognitionService (services/ImageRecognitionService.ets)总结
本文从架构视角完整剖析了"画伴梦工厂"最核心的 AI 编排链路。通过三个职责清晰的页面(采集 → 等待 → 展示)、一个精巧的状态机设计、一套并行调度策略(前台动画 + 后台生成),以及完整的数据流和错误处理机制,构建了从拍照到动画转换的完整闭环。
这条链路的架构设计体现了几个关键原则:职责分离(每个页面只做一件事)、非阻塞(动画不依赖后台真实进度)、可恢复(失败后可完整重试)、数据管道化(上游数据逐层透传并扩充)。
下一节我们将进入第 4 篇的系统能力篇,了解如何通过canIUseAPI 检测设备能力,实现多设备的按需适配。
参考源码
本文所有代码均来自项目文件:
products/default/src/main/ets/pages/PhotoRecognitionPage.ets— 采集入口页,展示 @Link 父子组件通信products/default/src/main/ets/pages/RecognitionWaitingPage.ets— AI 编排中枢,状态机 + 双轨并行 + 进度通知products/default/src/main/ets/pages/RecognitionResultPage.ets— 结果展示页,视频/静态双模式products/default/src/main/ets/services/AIGenerationService.ets— 图生视频服务,四步调用链products/default/src/main/ets/services/WorkRepository.ets— 作品持久化服务