第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 页面结构
typescript
@Entry
@Component
struct PhotoRecognitionPage {
@State private generationProgress: number = 0;
@State private noticeText: 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 以双向绑定方式传递给子组件:
typescript
PhotoRecognitionComponent({
generationProgress: $generationProgress, // 双向绑定
noticeText: $noticeText
})
子组件 PhotoRecognitionComponent 内部通过 @Link 接收:
typescript
@Component
export struct PhotoRecognitionComponent {
@Link generationProgress: number;
@Link noticeText: string;
// ...
}
当用户在子组件中完成了拍照或选图操作后,子组件会更新 generationProgress(例如设为 35)和 noticeText(例如"已采集画作,可以直接生成动画"),这些变化会立即反映到父页面的 UI 上。
2.3 页面跳转时机
拍照/选图完成后,子组件内部通过 Router API 跳转到等待页面:
typescript
this.getUIContext().getRouter().pushUrl({
url: 'pages/RecognitionWaitingPage',
params: {
source: '画作识别',
workSource: 'photo',
prompt: '...',
imageUri: '...',
coverUri: '...',
recognitionResult: '...'
}
});
三、Page 2:RecognitionWaitingPage------编排中枢
RecognitionWaitingPage 是整个流程的核心,承载着状态机管理 、并行任务调度 、进度通知 和作品保存四大职责。这是全项目中最具编排色彩的页面。
3.1 状态机设计
页面通过 5 个核心 @State 变量构建了一个完整的等待流程状态机:
typescript
@State private progress: number = 12; // 进度值 0-100
@State private activeStep: number = 0; // 当前步骤索引 0-3
@State private failed: boolean = false; // 是否失败
@State private completed: boolean = false; // 是否完成
@State private statusText: string = '正在准备生成任务'; // 状态文本
@State private videoUri: string = ''; // 生成后的视频 URI
@State private workId: string = ''; // 保存后的作品 ID
这些状态变量定义了四种互斥的页面状态:
| 状态 | progress | failed | completed | UI 表现 |
|---|---|---|---|---|
| 加载中 | 12~97 | false | false | 进度条动画、等待提示、灰色按钮 |
| 已完成 | 100 | false | true | 进度条满、"查看视频结果"按钮亮起 |
| 已失败 | 任意值 | true | false | 红色错误文本、"重试生成"按钮 |
| 重试中 | 重置为 12 | 重置 | 重置 | 回到加载中状态 |
3.2 双轨并行:定时器动画 + 异步生成
页面启动时(aboutToAppear),同时触发两条并行的执行路径:
typescript
aboutToAppear() {
// 1. 读取路由参数
const params = this.getUIContext().getRouter().getParams() as WaitingParams;
// ... 逐个字段赋值
// 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 定时器动画机制
typescript
private startWaitingTimer() {
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 回调函数将内部状态实时同步给页面:
typescript
const generatedVideo: GeneratedVideo = await AIGenerationService.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)
生成成功后,页面立即将作品持久化:
typescript
const work = WorkRepository.createWork(
this.workSource, // 来源:'photo' | 'doodle' | 'ai-chat'
generatedVideo.prompt, // 使用的 Prompt
finalCoverUri, // 封面图
generatedVideo.videoUri // 视频地址
);
WorkRepository.save(work);
this.workId = work.id;
作品保存后,即使应用重启,用户也能在"我的作品"中看到生成的动画。workId 也会作为路由参数传递给结果页面,用于显示和后续操作。
3.7 错误处理与重试机制
当生成过程中任意环节抛出异常时,catch 块捕获错误并更新 UI:
typescript
try {
// ... 整个生成流程
} catch (error) {
this.failed = true;
this.statusText = '生成失败:' + this.getErrorMessage(error as Error);
}
用户点击"重试生成"按钮时,执行完整的重置操作:
typescript
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 数组渲染了一个四步进度指示器:
typescript
const WAITING_STEPS: string[] = [
'看看画里有什么', // Step 0
'想一想怎么动', // Step 1
'画出动画片段', // Step 2
'保存到我的作品' // Step 3
];
每一步通过 StepRow @Builder 渲染,activeStep 控制其视觉状态:
typescript
@Builder
private StepRow(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 参数接收与反序列化
typescript
aboutToAppear() {
const params = this.getUIContext().getRouter().getParams() as ResultParams;
// 逐个字段赋值...
if (params && params.recognitionResult) {
try {
this.recognitionResult = JSON.parse(params.recognitionResult) as DrawingRecognitionResult;
} catch (error) {
this.recognitionResult = ImageRecognitionService.getFallbackResult();
}
}
}
注意 recognitionResult 是以 JSON 字符串形式传递的(Router API 不支持传递复杂对象),所以在接收端需要通过 JSON.parse 反序列化,并用 try-catch 做容错处理。
5.2 双模式展示
页面根据 videoUri 判断展示哪种结果:
typescript
if (this.videoUri !== '') {
this.VideoResult() // 模式一:视频结果
} else {
this.RecognitionResult() // 模式二:静态识别结果
}
模式一:VideoResult------渲染完整的视频播放器:
typescript
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:
typescript
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 API params + 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 篇的系统能力篇,了解如何通过 canIUse API 检测设备能力,实现多设备的按需适配。
参考源码
本文所有代码均来自项目文件:
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--- 作品持久化服务