HarmonyOS APP《画伴梦工厂》开发第24篇:AI 编排流程——从拍照到动画的完整链路

📅 2026/7/3 17:48:35 👁️ 阅读次数 📝 编程学习
HarmonyOS APP《画伴梦工厂》开发第24篇:AI 编排流程——从拍照到动画的完整链路

第3.9篇:AI 编排流程——从拍照到动画的完整链路

难度:⭐⭐⭐ 高级
前置知识:第 2.1 ~ 2.9 篇、第 3.1 ~ 3.8 篇
涉及源文件products/default/src/main/ets/pages/PhotoRecognitionPage.etsRecognitionWaitingPage.etsRecognitionResultPage.ets


概述

"画伴梦工厂"的核心功能,是将儿童的画作通过 AI 转化为动画。支撑这一功能的并非单个页面或单个服务,而是一条完整的编排链路:从拍照采集画作开始,经过图片压缩上传、图生视频任务提交与轮询、视频下载保存,再到最终的结果展示,跨越三个页面、调用多个 AI 服务,整个流程高度耦合且涉及大量异步状态管理。

本文将从架构视角,完整拆解这条"拍照 → 识别 → 生成 → 等待 → 展示"的全链路,重点分析多页面编排状态机设计并行任务调度以及跨页面数据传输等核心话题。


一、三页面的编排架构

整个链路由三个独立页面顺序衔接而成:

PhotoRecognitionPage ──router.push──→ RecognitionWaitingPage ──router.push──→ RecognitionResultPage (拍照采集) (AI 生成等待) (结果展示)

每个页面职责清晰、边界分明:

页面角色核心职责
PhotoRecognitionPage编排入口采集画作(拍照/相册),初始化generationProgressnoticeText
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

这些状态变量定义了四种互斥的页面状态

状态progressfailedcompletedUI 表现
加载中12~97falsefalse进度条动画、等待提示、灰色按钮
已完成100falsetrue进度条满、"查看视频结果"按钮亮起
已失败任意值truefalse红色错误文本、"重试生成"按钮
重试中重置为 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 的步骤索引
  • 自动终止:当failedcompleted为 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 = true

3.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~270看看画里有什么
28~551想一想怎么动
56~832画出动画片段
84~1003保存到我的作品

五、Page 3:RecognitionResultPage——结果展示

生成完成后,用户跳转到RecognitionResultPage查看结果。该页面通过路由参数接收所有上游数据,根据videoUri的有无,在两种展示模式间切换。

5.1 参数接收与反序列化

aboutToAppear(){constparams=this.getUIContext().getRouter().getParams()asResultParams;// 逐个字段赋值...if(params&&params.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— 作品持久化服务