content-production-entry.service.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. import { Injectable } from '@angular/core';
  2. import { AssistantSuggestion } from './assistant.service';
  3. import { CreationBriefService } from './creation-brief.service';
  4. import { TopicPoolService } from './topic-pool.service';
  5. import { ViralAnalysisService } from './viral-analysis.service';
  6. import { CreationBrief } from '../models/creation-brief.model';
  7. import { TopicIdea, ViralAnalysis } from '../models/douyin-insight.model';
  8. import { ViralTopicBridgeService } from './viral-topic-bridge.service';
  9. export type ContentEntryTarget = 'topic-to-video' | 'digital-human';
  10. export interface ContentEntryNavigation {
  11. tab: ContentEntryTarget;
  12. brief: CreationBrief;
  13. message: string;
  14. }
  15. export interface SavedTopicResult {
  16. count: number;
  17. title: string;
  18. ids: string[];
  19. }
  20. @Injectable({ providedIn: 'root' })
  21. export class ContentProductionEntryService {
  22. constructor(
  23. private topicPool: TopicPoolService,
  24. private creationBriefs: CreationBriefService,
  25. private viralTopicBridge: ViralTopicBridgeService,
  26. private viralAnalysis: ViralAnalysisService,
  27. ) {}
  28. saveAssistantSuggestion(suggestion: AssistantSuggestion): TopicIdea {
  29. const payload = suggestion.payload || {};
  30. const title = String(payload['title'] || payload['topic'] || suggestion.label || '').trim();
  31. const angle = String(payload['angle'] || payload['description'] || payload['topic'] || title).trim();
  32. const tags = this.normalizeTags(payload['tags']);
  33. return this.topicPool.saveTopic({
  34. title: title || 'AI 助手选题',
  35. angle: angle || title || '来自 AI 助手的选题建议',
  36. sourceType: 'assistant',
  37. sourceVideoIds: [],
  38. sourceAuthorIds: [],
  39. tags: Array.from(new Set(['AI助手', ...tags])).slice(0, 12),
  40. audience: String(payload['audience'] || ''),
  41. hook: String(payload['hook'] || title || ''),
  42. outline: String(payload['outline'] || payload['scriptFrame'] || ''),
  43. status: 'idea',
  44. pipelineTarget: 'topic_video',
  45. confidence: 'medium',
  46. });
  47. }
  48. enterTopic(target: ContentEntryTarget, topic: TopicIdea): ContentEntryNavigation {
  49. const brief = this.creationBriefs.savePendingTopic(target, topic);
  50. return {
  51. tab: target,
  52. brief,
  53. message: `已带入选题:${topic.title}`,
  54. };
  55. }
  56. saveManualPrefill(target: ContentEntryTarget, payload: Record<string, unknown>): CreationBrief {
  57. return this.creationBriefs.savePendingManualPrefill(target, payload);
  58. }
  59. saveDailyReportTopics(topics: TopicIdea[]): SavedTopicResult {
  60. const saved = topics.map((topic) => this.topicPool.saveTopic({
  61. title: topic.title,
  62. angle: topic.angle,
  63. sourceType: 'daily_report',
  64. sourceVideoIds: topic.sourceVideoIds,
  65. sourceAuthorIds: topic.sourceAuthorIds,
  66. tags: topic.tags,
  67. audience: topic.audience,
  68. hook: topic.hook,
  69. outline: topic.outline,
  70. status: 'idea',
  71. pipelineTarget: topic.pipelineTarget,
  72. confidence: topic.confidence,
  73. }));
  74. return this.savedResult(saved);
  75. }
  76. enterDailyReportTopic(topic: TopicIdea): ContentEntryNavigation {
  77. return this.enterTopic('topic-to-video', topic);
  78. }
  79. saveViralAnalysisTopics(analysis: ViralAnalysis): { result: SavedTopicResult; analysis: ViralAnalysis } {
  80. const drafts = this.viralTopicBridge.buildTopicDrafts(analysis);
  81. const existing = this.topicPool.listTopics()
  82. .filter((topic) => topic.sourceType === 'viral_analysis' && topic.sourceVideoIds?.[0] === analysis.awemeId);
  83. const saved = drafts.map((draft) => {
  84. const matched = existing.find((topic) => topic.variantIndex === draft.variantIndex);
  85. return matched
  86. ? this.topicPool.updateTopic(matched.id, draft)
  87. : this.topicPool.saveTopic(draft);
  88. });
  89. const savedIds = new Set(saved.map((topic) => topic.id));
  90. existing
  91. .filter((topic) => !savedIds.has(topic.id) && topic.status !== 'archived')
  92. .forEach((topic) => this.topicPool.archiveTopic(topic.id));
  93. return {
  94. result: this.savedResult(saved),
  95. analysis: this.viralAnalysis.markTopicsSaved(analysis.id, saved.map((item) => item.id)),
  96. };
  97. }
  98. enterViralAnalysisTopicVideo(analysis: ViralAnalysis): ContentEntryNavigation {
  99. const brief = this.viralTopicBridge.buildTopicVideoBrief(analysis);
  100. this.creationBriefs.savePendingBrief('topic-to-video', brief);
  101. return {
  102. tab: 'topic-to-video',
  103. brief,
  104. message: `已整理选题:${brief.angle || brief.title}`,
  105. };
  106. }
  107. private normalizeTags(value: unknown): string[] {
  108. if (Array.isArray(value)) {
  109. return value.map((tag) => String(tag || '').trim()).filter(Boolean);
  110. }
  111. return String(value || '').split(/[,,\s]+/).map((tag) => tag.trim()).filter(Boolean);
  112. }
  113. private savedResult(topics: TopicIdea[]): SavedTopicResult {
  114. return {
  115. count: topics.length,
  116. title: topics[0]?.title || '',
  117. ids: topics.map((topic) => topic.id),
  118. };
  119. }
  120. }