| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- import { Injectable } from '@angular/core';
- import { AuthCreditService } from './auth-credit.service';
- import { GenerationTemplate, GenerationTemplateInput } from '../models/template.model';
- import { PipelineRoute } from '../pipelines/pipeline-registry';
- const STORAGE_KEY = 'videoWorkflow.templates.user.v1';
- @Injectable({ providedIn: 'root' })
- export class TemplateService {
- private readonly systemTemplates: GenerationTemplate[] = this.buildSystemTemplates();
- constructor(private auth: AuthCreditService) {}
- listTemplates(): GenerationTemplate[] {
- const userId = this.auth.currentUser?.objectId || '';
- const userTemplates = this.readUserTemplates().filter((item) => item.userId === userId);
- return [...this.systemTemplates, ...userTemplates].sort((a, b) => {
- if (a.scope !== b.scope) return a.scope === 'system' ? -1 : 1;
- return Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
- });
- }
- listByPipeline(pipelineId: PipelineRoute): GenerationTemplate[] {
- return this.listTemplates().filter((item) => item.pipelineId === pipelineId);
- }
- createUserTemplate(input: GenerationTemplateInput): GenerationTemplate {
- const userId = this.requireUserId();
- const now = new Date().toISOString();
- const template: GenerationTemplate = {
- ...input,
- id: input.id || this.createId(),
- userId,
- scope: 'user',
- tags: this.uniqueTags(input.tags || []),
- config: this.cloneSerializable(input.config || {}),
- version: input.version || 1,
- usageCount: 0,
- createdAt: now,
- updatedAt: now,
- };
- const items = this.readUserTemplates();
- items.unshift(template);
- this.writeUserTemplates(items);
- return template;
- }
- cloneToUser(templateId: string): GenerationTemplate {
- const source = this.listTemplates().find((item) => item.id === templateId);
- if (!source) throw new Error('未找到模板');
- return this.createUserTemplate({
- pipelineId: source.pipelineId,
- name: `${source.name} 副本`,
- description: source.description || '',
- category: source.category,
- tags: source.tags || [],
- config: this.cloneSerializable(source.config || {}),
- preview: source.preview,
- version: source.version,
- });
- }
- updateUserTemplate(id: string, patch: Partial<GenerationTemplateInput>): GenerationTemplate {
- const userId = this.requireUserId();
- const items = this.readUserTemplates();
- const index = items.findIndex((item) => item.id === id && item.userId === userId);
- if (index < 0) throw new Error('未找到可编辑的模板');
- const updated: GenerationTemplate = {
- ...items[index],
- ...patch,
- id: items[index].id,
- userId,
- scope: 'user',
- tags: patch.tags ? this.uniqueTags(patch.tags) : items[index].tags,
- config: patch.config ? this.cloneSerializable(patch.config) : items[index].config,
- updatedAt: new Date().toISOString(),
- };
- items[index] = updated;
- this.writeUserTemplates(items);
- return updated;
- }
- deleteUserTemplate(id: string): void {
- const userId = this.requireUserId();
- this.writeUserTemplates(this.readUserTemplates().filter((item) => !(item.id === id && item.userId === userId)));
- }
- markUsed(id: string): void {
- const userId = this.auth.currentUser?.objectId || '';
- const items = this.readUserTemplates();
- const index = items.findIndex((item) => item.id === id && item.userId === userId);
- if (index < 0) return;
- items[index] = {
- ...items[index],
- usageCount: Number(items[index].usageCount || 0) + 1,
- updatedAt: new Date().toISOString(),
- };
- this.writeUserTemplates(items);
- }
- private requireUserId(): string {
- const userId = this.auth.currentUser?.objectId;
- if (!userId) {
- this.auth.requestLogin('模板库');
- throw new Error('请先登录后保存模板');
- }
- return userId;
- }
- private readUserTemplates(): GenerationTemplate[] {
- try {
- const raw = localStorage.getItem(STORAGE_KEY);
- const parsed = raw ? JSON.parse(raw) : [];
- return Array.isArray(parsed) ? parsed : [];
- } catch {
- return [];
- }
- }
- private writeUserTemplates(items: GenerationTemplate[]): void {
- localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
- }
- private createId(): string {
- return `tpl_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
- }
- private uniqueTags(tags: string[]): string[] {
- return tags.map((item) => String(item || '').trim()).filter((item, index, list) => !!item && list.indexOf(item) === index).slice(0, 12);
- }
- private cloneSerializable<T>(value: T): T {
- return JSON.parse(JSON.stringify(value || {}));
- }
- private buildSystemTemplates(): GenerationTemplate[] {
- const now = '2026-06-01T00:00:00.000Z';
- return [
- {
- id: 'sys_topic_knowledge_short',
- scope: 'system',
- pipelineId: 'topic-to-video',
- name: '知识口播短平快',
- description: '适合把一个观点拆成 3-5 个短分镜,节奏清晰,适合日更知识内容。',
- category: '知识科普',
- tags: ['口播', '知识', '日更'],
- config: {
- nScenes: 4,
- aspect: 'portrait',
- quickMode: true,
- mediaMode: 'image',
- clipFrames: 121,
- clipQuality: '720p',
- continuityEnabled: true,
- visualBible: {
- subject: '自信讲述者或干净的知识符号主体',
- product: '',
- location: '极简演播室或清爽信息图背景',
- palette: '白色、蓝色、高对比强调色',
- lighting: '柔和棚拍光',
- camera: '中近景,构图干净',
- styleNotes: '轻量信息图风格,大号关键词,层级清晰',
- constraints: '保持统一配色和讲述者风格',
- },
- scriptFrame: '开头提出反常识观点 -> 三点解释 -> 结尾抛出行动建议',
- },
- version: 1,
- usageCount: 0,
- createdAt: now,
- updatedAt: now,
- },
- {
- id: 'sys_topic_product_seed',
- scope: 'system',
- pipelineId: 'topic-to-video',
- name: '产品种草结构',
- description: '从用户痛点切入,突出使用场景、差异点和购买理由。',
- category: '产品种草',
- tags: ['种草', '产品', '转化'],
- config: {
- nScenes: 5,
- aspect: 'portrait',
- quickMode: true,
- mediaMode: 'video',
- clipFrames: 121,
- clipQuality: '720p',
- continuityEnabled: true,
- visualBible: {
- subject: '日常使用场景中的目标用户',
- product: '细节清晰的主推产品',
- location: '明亮的生活方式场景',
- palette: '温暖中性色,搭配清新的强调色',
- lighting: '自然日光',
- camera: '产品细节特写与平滑运镜',
- styleNotes: '真实产品广告质感,构图干净',
- constraints: '保持产品颜色和形状一致',
- },
- scriptFrame: '痛点场景 -> 产品出现 -> 三个卖点 -> 使用前后对比 -> 轻 CTA',
- },
- version: 1,
- usageCount: 0,
- createdAt: now,
- updatedAt: now,
- },
- {
- id: 'sys_image_cover_clean',
- scope: 'system',
- pipelineId: 'image-generation',
- name: '清爽封面图',
- description: '适合生成视频封面、知识卡片和素材库配图。',
- category: '其他',
- tags: ['封面', '信息图', '浅色'],
- config: {
- width: 1440,
- height: 1920,
- stylePrompt: '清爽浅色背景,主体明确,大标题留白,少量高对比强调色',
- },
- version: 1,
- usageCount: 0,
- createdAt: now,
- updatedAt: now,
- },
- {
- id: 'sys_dh_explainer',
- scope: 'system',
- pipelineId: 'digital-human',
- name: '数字人讲解',
- description: '适合课程介绍、产品说明、短口播讲解。',
- category: '口播',
- tags: ['数字人', '讲解', '口播'],
- config: {
- resolution: '1080p',
- scriptFrame: '问候 -> 背景问题 -> 核心说明 -> 适用人群 -> 行动建议',
- voiceMood: '自然、清晰、可信',
- },
- version: 1,
- usageCount: 0,
- createdAt: now,
- updatedAt: now,
- },
- ];
- }
- }
|