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 页面结构

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()                        // 通知栏
      }
    }
  }
}

页面通过 $ 语法将 @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 的步骤索引
  • 自动终止 :当 failedcompleted 为 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 --- 作品持久化服务