
第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和noticeTextRecognitionWaitingPage编排中枢接收图片数据驱动 AI 生成流程管理进度动画与状态机保存作品RecognitionResultPage编排终点展示生成结果视频或静态识别信息提供返回作品集的入口这种三页面编排模式是鸿蒙应用中的经典实践——将复杂流程拆解为独立的页面单元每个页面通过 Router API 传递参数、切换页面既保证了代码的内聚性又降低了单个页面的复杂度。二、Page 1PhotoRecognitionPage——编排入口PhotoRecognitionPage是整个流程的入口页面它本身并不参与 AI 编排逻辑而是作为采集容器将拍照/选图的能力委托给子组件PhotoRecognitionComponent。2.1 页面结构EntryComponentstruct PhotoRecognitionPage{StateprivategenerationProgress:number0;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 2RecognitionWaitingPage——编排中枢RecognitionWaitingPage是整个流程的核心承载着状态机管理、并行任务调度、进度通知和作品保存四大职责。这是全项目中最具编排色彩的页面。3.1 状态机设计页面通过 5 个核心State变量构建了一个完整的等待流程状态机Stateprivateprogress:number12;// 进度值 0-100StateprivateactiveStep:number0;// 当前步骤索引 0-3Stateprivatefailed:booleanfalse;// 是否失败Stateprivatecompleted:booleanfalse;// 是否完成StateprivatestatusText:string正在准备生成任务;// 状态文本StateprivatevideoUri:string;// 生成后的视频 URIStateprivateworkId:string;// 保存后的作品 ID这些状态变量定义了四种互斥的页面状态状态progressfailedcompletedUI 表现加载中12~97falsefalse进度条动画、等待提示、灰色按钮已完成100falsetrue进度条满、查看视频结果按钮亮起已失败任意值truefalse红色错误文本、重试生成按钮重试中重置为 12重置重置回到加载中状态3.2 双轨并行定时器动画 异步生成页面启动时aboutToAppear同时触发两条并行的执行路径aboutToAppear(){// 1. 读取路由参数constparamsthis.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.timerIdsetInterval((){if(this.failed||this.completed){clearInterval(this.timerId);// 状态终止时停止return;}this.animationFrame(this.animationFrame1)%4;// 气泡动画帧this.waitingTipWAITING_TIPS[this.animationFrame];// 轮换提示if(this.progress92){this.progressMath.min(92,this.progress3);// 快速增长阶段}else{this.progressMath.min(97,this.progress1);// 慢速增长阶段}this.activeStepMath.min(3,Math.floor(this.progress/28));// 步骤索引},1200);}关键设计点分阶段增速92% 之前每次 392% 之后每次 1模拟先快后慢的真实生成体验步骤映射通过Math.floor(progress / 28)将进度值映射到 0-3 的步骤索引自动终止当failed或completed为 true 时清理定时器避免资源泄漏生命周期对称在aboutToDisappear中清理定时器3.4 进度通知机制onStatus 回调后台生成任务通过onStatus回调函数将内部状态实时同步给页面constgeneratedVideo:GeneratedVideoawaitAIGenerationService.generateVideo(this.imageUri,this.prompt,(message:string){this.statusTextmessage;// 实时更新状态文本});AIGenerationService.generateVideo内部在各个关键节点调用onStatus阶段回调消息准备阶段正在压缩图片压缩完成图片已压缩正在上传任务提交任务已提交正在生成动画轮询中正在等待动画生成第 N 次检查网络波动网络有点慢继续等待动画完成下载阶段动画已生成正在保存到本地最终完成视频已生成并保存到作品这种回调通知模式实现了非阻塞的进度反馈——生成任务在后台异步执行UI 层通过回调被动接收状态更新两者完全解耦。3.5 后台生成完整链路startGeneration方法的执行链路如下startGeneration() │ ├─ prepareUploadBase64(imageUri, onStatus) │ ├─ readImageAsArrayBuffer → 读取图片为二进制 │ ├─ compressImageBuffer → 压缩至 ~900KB78% 质量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生成成功后页面立即将作品持久化constworkWorkRepository.createWork(this.workSource,// 来源photo | doodle | ai-chatgeneratedVideo.prompt,// 使用的 PromptfinalCoverUri,// 封面图generatedVideo.videoUri// 视频地址);WorkRepository.save(work);this.workIdwork.id;作品保存后即使应用重启用户也能在我的作品中看到生成的动画。workId也会作为路由参数传递给结果页面用于显示和后续操作。3.7 错误处理与重试机制当生成过程中任意环节抛出异常时catch块捕获错误并更新 UItry{// ... 整个生成流程}catch(error){this.failedtrue;this.statusText生成失败this.getErrorMessage(errorasError);}用户点击重试生成按钮时执行完整的重置操作if(this.failed){// 重置所有状态到初始值this.progress12;this.activeStep0;this.animationFrame0;this.waitingTipWAITING_TIPS[0];// 重新启动双轨流程this.startWaitingTimer();this.startGeneration();}重置操作恢复了 5 个状态变量到初始值然后重新启动定时器和生成任务相当于一次完整的重来。四、WAITING_STEPS四步进度指示器页面底部使用WAITING_STEPS数组渲染了一个四步进度指示器constWAITING_STEPS:string[][看看画里有什么,// Step 0想一想怎么动,// Step 1画出动画片段,// Step 2保存到我的作品// Step 3];每一步通过StepRowBuilder 渲染activeStep控制其视觉状态BuilderprivateStepRow(step:string,index:number){Row(){Text((index1).toString())// 步骤编号.fontColor(this.activeStepindex?#FFFFFF:#8A8FA4).backgroundColor(this.activeStepindex?this.mint:#ECECF6)Text(step)// 步骤描述.fontColor(this.activeStepindex?this.ink:#8A8FA4)Text(this.activeStepindex?完成:// 状态标签(this.activeStepindex?进行中:等待)).fontColor(this.activeStepindex?this.mint:#9AA0B5)}}每个步骤有三种视觉状态状态条件步骤编号文字颜色标签已完成activeStep index白色底绿色字深色“完成”进行中activeStep index绿色底白字深色“进行中”未开始activeStep index灰色底白字灰色“等待”四个步骤的进度映射关系为activeStep Math.min(3, Math.floor(progress / 28))进度范围activeStep处于进行中的步骤0~270看看画里有什么28~551想一想怎么动56~832画出动画片段84~1003保存到我的作品五、Page 3RecognitionResultPage——结果展示生成完成后用户跳转到RecognitionResultPage查看结果。该页面通过路由参数接收所有上游数据根据videoUri的有无在两种展示模式间切换。5.1 参数接收与反序列化aboutToAppear(){constparamsthis.getUIContext().getRouter().getParams()asResultParams;// 逐个字段赋值...if(paramsparams.recognitionResult){try{this.recognitionResultJSON.parse(params.recognitionResult)asDrawingRecognitionResult;}catch(error){this.recognitionResultImageRecognitionService.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.isPlayingtrue;}).onPause((){this.isPlayingfalse;}).onFinish((){this.isPlayingfalse;})视频上方叠加了自定义的播放/暂停按钮和已保存到作品标签营造更友好的交互体验。模式二RecognitionResult——展示静态图片和识别详情页面展示识别服务返回的结构化数据包括主角、场景、情绪和动画建议四个维度以及对应的置信度百分比主角小恐龙 96% 场景森林 91% 情绪开心 88% 动画建议跳跃动作 86%5.3 页面终点跳转回作品集两种模式下底部的按钮最终都导航到首页的作品 Tabthis.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 (tabworks) │ ▼ Index (作品集)这种设计体现了典型的管道模式——中间页面在透传上游参数的基础上不断追加自己产生的数据最终下游页面接收完整的上下文。七、完整数据流与状态转换图┌──────────────────────────────────────────────────────────────────┐ │ 状态转换总图 │ └──────────────────────────────────────────────────────────────────┘ [PhotoRecognitionPage] [RecognitionWaitingPage] [RecognitionResultPage] ┌─────────────┐ │ aboutToAppear │ │ 读取路由参数 │ └──────┬──────┘ │ ┌────────────┴────────────┐ │ │ ▼ ▼ ┌─────────────────┐ ┌─────────────────────┐ │ startWaitingTimer│ │ startGeneration │ │ (前台动画) │ │ (后台生成) │ │ │ │ │ │ progress: 12→97 │ │ prepareUploadBase64 │ │ activeStep: 0→3 │ │ │ │ │ animationFrame │ │ createImg2VideoTask │ │ 循环 0~3 │ │ │ │ └────────┬────────┘ │ pollImg2VideoTask │ │ │ (每5s轮询,最多6min) │ │ │ │ │ │ │ downloadVideo │ │ │ │ │ │ │ WorkRepository.save │ │ └────────┬────────────┘ │ │ │ ┌────────┴────────┐ │ │ completedtrue │ │ │ videoUrixxx │ │ │ workIdxxx │ │ └────────┬────────┘ │ │ │ (按钮触发 pushUrl) │ │ │ ▼ │ ┌─────────────────┐ │ │ RecognitionResult│ │ │ videoUri 存在 │ │ │ ├─是→ VideoResult│ │ │ └─否→ RecogResult│ │ │ │ │ │ │ Button → Index │ │ └─────────────────┘ │ (异常发生时) │ ▼ ┌──────────────┐ │ failedtrue │ │ 显示错误信息 │ └──────┬───────┘ │ 用户点击重试 │ ▼ ┌──────────────┐ │ 重置状态机 │ │ progress12 │ │ activeStep0 │ │ 重新执行 │ └──────────────┘八、服务调用时序图从页面层面往下看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保存到 preferences9.2 状态机设计价值单一数据源所有 UI 状态由State变量驱动不存在多个状态副本可预测转换failed/completed互斥不会出现同时为 true 的非法状态灵活重置重试操作只需重置状态机的初始值重新执行生成函数UI 自动同步状态变化通过声明式绑定自动反映到界面9.3 错误处理策略错误类型处理方式图片读取/压缩失败降级使用原图通过onStatus通知用户任务提交失败透传错误信息设failedtrueUI 显示重试按钮轮询中超时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— 作品持久化服务