template.service.ts 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. import { Injectable } from '@angular/core';
  2. import { AuthCreditService } from './auth-credit.service';
  3. import { GenerationTemplate, GenerationTemplateInput } from '../models/template.model';
  4. import { PipelineRoute } from '../pipelines/pipeline-registry';
  5. const STORAGE_KEY = 'videoWorkflow.templates.user.v1';
  6. @Injectable({ providedIn: 'root' })
  7. export class TemplateService {
  8. private readonly systemTemplates: GenerationTemplate[] = this.buildSystemTemplates();
  9. constructor(private auth: AuthCreditService) {}
  10. listTemplates(): GenerationTemplate[] {
  11. const userId = this.auth.currentUser?.objectId || '';
  12. const userTemplates = this.readUserTemplates().filter((item) => item.userId === userId);
  13. return [...this.systemTemplates, ...userTemplates].sort((a, b) => {
  14. if (a.scope !== b.scope) return a.scope === 'system' ? -1 : 1;
  15. return Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
  16. });
  17. }
  18. listByPipeline(pipelineId: PipelineRoute): GenerationTemplate[] {
  19. return this.listTemplates().filter((item) => item.pipelineId === pipelineId);
  20. }
  21. createUserTemplate(input: GenerationTemplateInput): GenerationTemplate {
  22. const userId = this.requireUserId();
  23. const now = new Date().toISOString();
  24. const template: GenerationTemplate = {
  25. ...input,
  26. id: input.id || this.createId(),
  27. userId,
  28. scope: 'user',
  29. tags: this.uniqueTags(input.tags || []),
  30. config: this.cloneSerializable(input.config || {}),
  31. version: input.version || 1,
  32. usageCount: 0,
  33. createdAt: now,
  34. updatedAt: now,
  35. };
  36. const items = this.readUserTemplates();
  37. items.unshift(template);
  38. this.writeUserTemplates(items);
  39. return template;
  40. }
  41. cloneToUser(templateId: string): GenerationTemplate {
  42. const source = this.listTemplates().find((item) => item.id === templateId);
  43. if (!source) throw new Error('未找到模板');
  44. return this.createUserTemplate({
  45. pipelineId: source.pipelineId,
  46. name: `${source.name} 副本`,
  47. description: source.description || '',
  48. category: source.category,
  49. tags: source.tags || [],
  50. config: this.cloneSerializable(source.config || {}),
  51. preview: source.preview,
  52. version: source.version,
  53. });
  54. }
  55. updateUserTemplate(id: string, patch: Partial<GenerationTemplateInput>): GenerationTemplate {
  56. const userId = this.requireUserId();
  57. const items = this.readUserTemplates();
  58. const index = items.findIndex((item) => item.id === id && item.userId === userId);
  59. if (index < 0) throw new Error('未找到可编辑的模板');
  60. const updated: GenerationTemplate = {
  61. ...items[index],
  62. ...patch,
  63. id: items[index].id,
  64. userId,
  65. scope: 'user',
  66. tags: patch.tags ? this.uniqueTags(patch.tags) : items[index].tags,
  67. config: patch.config ? this.cloneSerializable(patch.config) : items[index].config,
  68. updatedAt: new Date().toISOString(),
  69. };
  70. items[index] = updated;
  71. this.writeUserTemplates(items);
  72. return updated;
  73. }
  74. deleteUserTemplate(id: string): void {
  75. const userId = this.requireUserId();
  76. this.writeUserTemplates(this.readUserTemplates().filter((item) => !(item.id === id && item.userId === userId)));
  77. }
  78. markUsed(id: string): void {
  79. const userId = this.auth.currentUser?.objectId || '';
  80. const items = this.readUserTemplates();
  81. const index = items.findIndex((item) => item.id === id && item.userId === userId);
  82. if (index < 0) return;
  83. items[index] = {
  84. ...items[index],
  85. usageCount: Number(items[index].usageCount || 0) + 1,
  86. updatedAt: new Date().toISOString(),
  87. };
  88. this.writeUserTemplates(items);
  89. }
  90. private requireUserId(): string {
  91. const userId = this.auth.currentUser?.objectId;
  92. if (!userId) {
  93. this.auth.requestLogin('模板库');
  94. throw new Error('请先登录后保存模板');
  95. }
  96. return userId;
  97. }
  98. private readUserTemplates(): GenerationTemplate[] {
  99. try {
  100. const raw = localStorage.getItem(STORAGE_KEY);
  101. const parsed = raw ? JSON.parse(raw) : [];
  102. return Array.isArray(parsed) ? parsed : [];
  103. } catch {
  104. return [];
  105. }
  106. }
  107. private writeUserTemplates(items: GenerationTemplate[]): void {
  108. localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
  109. }
  110. private createId(): string {
  111. return `tpl_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
  112. }
  113. private uniqueTags(tags: string[]): string[] {
  114. return tags.map((item) => String(item || '').trim()).filter((item, index, list) => !!item && list.indexOf(item) === index).slice(0, 12);
  115. }
  116. private cloneSerializable<T>(value: T): T {
  117. return JSON.parse(JSON.stringify(value || {}));
  118. }
  119. private buildSystemTemplates(): GenerationTemplate[] {
  120. const now = '2026-06-01T00:00:00.000Z';
  121. return [
  122. {
  123. id: 'sys_topic_knowledge_short',
  124. scope: 'system',
  125. pipelineId: 'topic-to-video',
  126. name: '知识口播短平快',
  127. description: '适合把一个观点拆成 3-5 个短分镜,节奏清晰,适合日更知识内容。',
  128. category: '知识科普',
  129. tags: ['口播', '知识', '日更'],
  130. config: {
  131. nScenes: 4,
  132. aspect: 'portrait',
  133. quickMode: true,
  134. mediaMode: 'image',
  135. clipFrames: 121,
  136. clipQuality: '720p',
  137. continuityEnabled: true,
  138. visualBible: {
  139. subject: '自信讲述者或干净的知识符号主体',
  140. product: '',
  141. location: '极简演播室或清爽信息图背景',
  142. palette: '白色、蓝色、高对比强调色',
  143. lighting: '柔和棚拍光',
  144. camera: '中近景,构图干净',
  145. styleNotes: '轻量信息图风格,大号关键词,层级清晰',
  146. constraints: '保持统一配色和讲述者风格',
  147. },
  148. scriptFrame: '开头提出反常识观点 -> 三点解释 -> 结尾抛出行动建议',
  149. },
  150. version: 1,
  151. usageCount: 0,
  152. createdAt: now,
  153. updatedAt: now,
  154. },
  155. {
  156. id: 'sys_topic_product_seed',
  157. scope: 'system',
  158. pipelineId: 'topic-to-video',
  159. name: '产品种草结构',
  160. description: '从用户痛点切入,突出使用场景、差异点和购买理由。',
  161. category: '产品种草',
  162. tags: ['种草', '产品', '转化'],
  163. config: {
  164. nScenes: 5,
  165. aspect: 'portrait',
  166. quickMode: true,
  167. mediaMode: 'video',
  168. clipFrames: 121,
  169. clipQuality: '720p',
  170. continuityEnabled: true,
  171. visualBible: {
  172. subject: '日常使用场景中的目标用户',
  173. product: '细节清晰的主推产品',
  174. location: '明亮的生活方式场景',
  175. palette: '温暖中性色,搭配清新的强调色',
  176. lighting: '自然日光',
  177. camera: '产品细节特写与平滑运镜',
  178. styleNotes: '真实产品广告质感,构图干净',
  179. constraints: '保持产品颜色和形状一致',
  180. },
  181. scriptFrame: '痛点场景 -> 产品出现 -> 三个卖点 -> 使用前后对比 -> 轻 CTA',
  182. },
  183. version: 1,
  184. usageCount: 0,
  185. createdAt: now,
  186. updatedAt: now,
  187. },
  188. {
  189. id: 'sys_image_cover_clean',
  190. scope: 'system',
  191. pipelineId: 'image-generation',
  192. name: '清爽封面图',
  193. description: '适合生成视频封面、知识卡片和素材库配图。',
  194. category: '其他',
  195. tags: ['封面', '信息图', '浅色'],
  196. config: {
  197. width: 1440,
  198. height: 1920,
  199. stylePrompt: '清爽浅色背景,主体明确,大标题留白,少量高对比强调色',
  200. },
  201. version: 1,
  202. usageCount: 0,
  203. createdAt: now,
  204. updatedAt: now,
  205. },
  206. {
  207. id: 'sys_dh_explainer',
  208. scope: 'system',
  209. pipelineId: 'digital-human',
  210. name: '数字人讲解',
  211. description: '适合课程介绍、产品说明、短口播讲解。',
  212. category: '口播',
  213. tags: ['数字人', '讲解', '口播'],
  214. config: {
  215. resolution: '1080p',
  216. scriptFrame: '问候 -> 背景问题 -> 核心说明 -> 适用人群 -> 行动建议',
  217. voiceMood: '自然、清晰、可信',
  218. },
  219. version: 1,
  220. usageCount: 0,
  221. createdAt: now,
  222. updatedAt: now,
  223. },
  224. ];
  225. }
  226. }