第57篇美文生成让照片详情页长出可分享的文字第 57 篇看详情页里的“写回忆文字”。这不是另一个孤立的 AI 功能而是把照片记录、远程生成、本地兜底和详情展示串起来让用户能把照片变成更适合分享的文字。generateAiPoem的实现很有训练营价值它可以调用远程模型也可以在失败时生成本地四行文案最终结果写回aiPoem字段由详情页展示。这一篇继续围绕 21 天「智能相机开发实战」训练营展开。阅读时可以先看界面效果再顺着函数名回到 DevEco Studio 定位实现最后把成功态、取消态和失败态串成一个可复现闭环。本篇目标理解详情页按钮如何触发智能文案生成。掌握远程生成失败时的本地兜底策略。把生成结果写回aiPoem并持久化。让智能解读和美文生成互不抢状态。对应源码位置entry/src/main/ets/pages/Index.etsentry/src/main/ets/services/VolcengineArkService.ets一、详情页是 AI 文案最自然的落点用户查看某张照片时才最容易产生“帮我写一段”的需求。详情页按钮不需要讲太多功能说明只要提供“智能解读”和“写回忆文字”两个动作。页面会根据aiInsightBusy和aiPoemBusy控制按钮可用状态避免两个生成任务同时改同一条记录。照片详情页展示智能摘要和回忆文字二、按钮层只负责触发动作和展示状态详情页中“智能解读”要求已配置 Key“写回忆文字”则允许远程失败后走本地兜底。按钮层只调用对应函数不直接处理模型请求。这让 UI 保持轻量真正的生成逻辑放到页面方法和服务层里。详情页同时提供智能解读和写回忆文字入口Button(this.aiInsightBusy ? 整理中... : 智能解读) .height(38) .layoutWeight(1) .fontSize(12) .fontWeight(FontWeight.Medium) .fontColor(this.getWarmActionTextColor()) .backgroundColor(this.getWarmActionBackgroundColor()) .borderRadius(15) .enabled(!this.aiInsightBusy !this.aiPoemBusy this.arkApiKey.trim().length 0) .onClick(() { void this.generateRemoteInsight(record.id); }) Button(this.aiPoemBusy ? 书写中... : 写回忆文字) .height(38) .layoutWeight(1) .fontSize(12) .fontWeight(FontWeight.Medium) .fontColor(this.getMutedActionTextColor()) .backgroundColor(this.getMutedActionBackgroundColor()) .borderRadius(15) .enabled(!this.aiInsightBusy !this.aiPoemBusy) .onClick(() { void this.generateAiPoem(record.id); }) } .width(100%) } if (this.aiSynthesisEntryVisible this.getRecordSmartCaption(record).length 0) { Text(this.getRecordSmartCaption(record)) .fontSize(13) .lineHeight(20) .fontColor($r(app.color.ml_on_surface)) .maxLines(3) .textOverflow({ overflow: TextOverflow.Ellipsis }) } if (this.aiSynthesisEntryVisible this.getRecordAiPoem(record).length 0) { Text(this.getRecordAiPoem(record)) .fontSize(13) .lineHeight(21)按钮状态用enabled控制文案用 busy 状态切换用户能看到当前任务正在进行。三、远程失败时不让用户空手而归generateAiPoem先尝试远程生成失败后调用buildLocalPoem。这比简单弹出“生成失败”更适合相册产品因为用户仍然得到一段可以分享的文字。生成结果最终写回aiPoem同时刷新选中 ID 和本地持久化记录。generateAiPoem 使用远程生成和本地兜底private async generateAiPoem(recordId: string): Promisevoid { if (this.aiPoemBusy) { return; } const targetRecord this.galleryRecords.find((record: GalleryMoment) record.id recordId); if (!targetRecord) { this.galleryNoticeText ; return; } if (!await this.ensureHuaweiIdentityForAiSynthesis(gallery)) { return; } this.aiPoemBusy true; this.galleryNoticeText 正在为 ${targetRecord.place} 写一段文字...; let poemText ; try { const poem await VolcengineArkService.generatePoem( this.getAbilityContext(), targetRecord, this.arkApiKey.trim().length 0 ? this.arkApiKey.trim() : undefined ); poemText poem.poem; } catch (error) { poemText this.buildLocalPoem(targetRecord); const message error instanceof Error ? error.message : JSON.stringify(error); console.error(Smart poem generation failed, using local fallback: ${message}); } try { const nextRecords this.galleryRecords.map((record: GalleryMoment) { if (record.id ! recordId) { return record; } const nextRecord this.cloneGalleryRecord(record); nextRecord.aiPoem poemText; return nextRecord; }); this.galleryRecords nextRecords; this.gallerySelectedId recordId; this.galleryNoticeText ; await this.persistGalleryRecords(nextRecords); } finally { this.aiPoemBusy false; } } private isVideoRecordSelected(recordId: string): boolean { return this.selectedVideoRecordIds.some((selectedId: string) selectedId recordId); } private toggleVideoRecordSelection(recordId: string): void { if (!this.videoTaskBusy) { this.movieSelectionStatusText ;这里的兜底文案不是假装远程成功而是明确用本地规则生成一段可用文字。产品体验不会被网络失败完全打断。四、服务层提示词约束输出风格generatePoem要求模型写四行现代中文温暖、具体不编造地标。它同样会输入后摄图和可选前摄图让文案尽量贴近照片。服务层只返回poem和rawText页面层决定怎么保存和展示。服务层生成四行现代中文回忆文字static async generatePoem( context: common.UIAbilityContext, record: GalleryMoment, apiKey?: string ): PromiseVolcenginePoemResult { const config await VolcengineArkService.resolveConfig(context, apiKey); const textContent: ArkResponseTextContent { type: input_text, text: [ You are writing for a HarmonyOS travel memory album., Look at the rear-camera scene photo and front-camera selfie if present., Write one short modern Chinese poem, 4 lines only, warm and concrete., Mention visible scenery or mood, but do not invent landmarks., Known place hint: ${record.place}. ].join( ) }; const backImageContent: ArkResponseImageContent { type: input_image, image_url: VolcengineArkService.toDataUrl(record.backPath), detail: high }; const contentItems: ArrayArkResponseContentItem [textContent, backImageContent]; if (record.frontPath.length 0 record.frontPath ! record.backPath) { const frontImageContent: ArkResponseImageContent { type: input_image, image_url: VolcengineArkService.toDataUrl(record.frontPath), detail: high }; contentItems.push(frontImageContent); } const inputItem: ArkResponseInputItem { role: user, content: contentItems }; const requestBody: ArkResponseRequestBody { model: config.responseModel, input: [inputItem] }; const response await VolcengineArkService.requestJsonArkResponseApiResult( ${VolcengineArkService.BASE_URL}/responses, http.RequestMethod.POST, config.apiKey, JSON.stringify(requestBody) ); const rawText VolcengineArkService.extractResponseText(response.data, response.rawText); return { poem: VolcengineArkService.normalizePoem(rawText, record),如果后续要做“朋友圈文案”“旅行日记”“短片旁白”可以复用这个模式服务层约束风格页面层写回对应字段。五、写回字段要和详情展示保持同一条记录美文生成最怕“生成出来了但页面刷新后丢了”。项目没有把生成文字只放在临时变量里而是写回GalleryMoment.aiPoem。这样用户从详情页切到相册再回到同一张照片时文字仍然跟着记录走。这一点和第 55 篇的智能解读是同一个思路AI 结果必须进入记录模型才算真正成为产品能力。详情页只是展示入口记录模型才是后续分享、重启恢复和云同步的承载点。AI 结果进入 GalleryMoment 后详情页可以稳定展示和复用const nextRecords this.galleryRecords.map((record: GalleryMoment) { if (record.id ! recordId) { return record; } const nextRecord this.cloneGalleryRecord(record); nextRecord.aiPoem poemText; return nextRecord; }); this.galleryRecords nextRecords; this.gallerySelectedId recordId; this.galleryNoticeText ; await this.persistGalleryRecords(nextRecords);这段代码的重点是cloneGalleryRecord。它会保留地点、路径、可见性、云同步字段和用户备注再只替换aiPoem。这样生成美文不会破坏照片记录的其他信息也不会让保险箱和普通相册的字段边界混乱。工程检查清单详情页按钮不直接写请求逻辑。智能解读和美文生成有独立 busy 状态。远程失败后提供本地兜底文案。生成结果写入记录字段并持久化。提示词要求不编造照片外事实。今日练习给一张照片生成回忆文字重启应用后确认aiPoem仍然存在。断网后触发写回忆文字观察本地兜底文案。设计一个“短片旁白”字段说明它应该复用哪些服务层逻辑。训练营后面的文章会继续按“真实页面效果 - 源码定位 - 状态闭环 - 可验证结果”的节奏推进。每一篇都尽量让你能拿着代码直接回到项目里复现而不是只停留在概念说明。