ai-summary.service.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. import axios from 'axios';
  2. import { withAiLog } from '../../services/ai-call-logger';
  3. /**
  4. * AI 文本摘要服务
  5. * 使用通义千问或讯飞星火 API 生成文本摘要
  6. */
  7. export class AISummaryService {
  8. private apiKey: string;
  9. private apiEndpoint: string;
  10. constructor() {
  11. // 这里可以使用多个 AI 服务,暂时使用 Mock 实现
  12. this.apiKey = process.env.AI_API_KEY || '';
  13. this.apiEndpoint = process.env.AI_API_ENDPOINT || '';
  14. }
  15. /**
  16. * 生成文本摘要
  17. * @param text 原始文本
  18. * @param maxLength 最大长度(默认 200 字)
  19. */
  20. async generateSummary(text: string, maxLength: number = 200): Promise<string> {
  21. try {
  22. // 如果配置了真实的 AI API,调用真实服务
  23. if (this.apiKey && this.apiEndpoint) {
  24. return await this.callAIAPI(text, maxLength);
  25. }
  26. // 否则使用 Mock 实现(提取前 200 字)
  27. return this.mockSummary(text, maxLength);
  28. } catch (error) {
  29. console.error('生成摘要失败:', error);
  30. // 降级处理:返回前 200 字
  31. return this.mockSummary(text, maxLength);
  32. }
  33. }
  34. /**
  35. * 调用 AI API 生成摘要
  36. */
  37. private async callAIAPI(text: string, maxLength: number): Promise<string> {
  38. const prompt = `请为以下文本生成一个${maxLength}字以内的摘要,要求简洁明了,突出核心内容:\n\n${text.slice(0, 1000)}`;
  39. const response = await withAiLog(
  40. () => axios.post(
  41. this.apiEndpoint,
  42. {
  43. prompt,
  44. max_tokens: maxLength,
  45. temperature: 0.7,
  46. },
  47. {
  48. headers: {
  49. Authorization: `Bearer ${this.apiKey}`,
  50. 'Content-Type': 'application/json',
  51. },
  52. }
  53. ),
  54. { callType: 'llm_chat', provider: 'ai-summary', model: 'summary-model', textLen: prompt.length }
  55. );
  56. return response.data.result || response.data.choices?.[0]?.text || '';
  57. }
  58. /**
  59. * Mock 摘要生成(简单截取)
  60. */
  61. private mockSummary(text: string, maxLength: number): string {
  62. // 去除多余空格和换行
  63. const cleaned = text.replace(/\s+/g, ' ').trim();
  64. if (cleaned.length <= maxLength) {
  65. return cleaned;
  66. }
  67. // 截取到最后一个完整的句子
  68. let summary = cleaned.substring(0, maxLength);
  69. const lastPeriod = summary.lastIndexOf('。');
  70. const lastQuestion = summary.lastIndexOf('?');
  71. const lastExclamation = summary.lastIndexOf('!');
  72. const lastPunctuation = Math.max(lastPeriod, lastQuestion, lastExclamation);
  73. if (lastPunctuation > 0) {
  74. summary = summary.substring(0, lastPunctuation + 1);
  75. }
  76. return summary + '...';
  77. }
  78. /**
  79. * 生成标题
  80. * @param text 原始文本
  81. */
  82. async generateTitle(text: string): Promise<string> {
  83. try {
  84. // 尝试从文本中提取关键信息作为标题
  85. const lines = text.split('\n').filter(line => line.trim().length > 0);
  86. // 如果第一行比较短,可能是标题
  87. if (lines.length > 0 && lines[0].length < 50) {
  88. return lines[0].trim();
  89. }
  90. // 否则使用 AI 生成
  91. if (this.apiKey && this.apiEndpoint) {
  92. return await this.callAITitle(text);
  93. }
  94. // Mock 实现:提取前 30 个字
  95. return text.replace(/\s+/g, ' ').substring(0, 30).trim() + '...';
  96. } catch (error) {
  97. console.error('生成标题失败:', error);
  98. return 'AI 生成的音频内容';
  99. }
  100. }
  101. /**
  102. * 调用 AI 生成标题
  103. */
  104. private async callAITitle(text: string): Promise<string> {
  105. const prompt = `请为以下文本生成一个吸引人的标题,不超过 30 个字:\n\n${text.slice(0, 500)}`;
  106. const response = await withAiLog(
  107. () => axios.post(
  108. this.apiEndpoint,
  109. {
  110. prompt,
  111. max_tokens: 30,
  112. temperature: 0.8,
  113. },
  114. {
  115. headers: {
  116. Authorization: `Bearer ${this.apiKey}`,
  117. 'Content-Type': 'application/json',
  118. },
  119. }
  120. ),
  121. { callType: 'llm_chat', provider: 'ai-summary', model: 'title-model', textLen: prompt.length }
  122. );
  123. return response.data.result || response.data.choices?.[0]?.text || 'AI 生成的音频内容';
  124. }
  125. /**
  126. * 提取关键词标签
  127. * @param text 原始文本
  128. */
  129. async extractTags(text: string): Promise<string[]> {
  130. try {
  131. // 简单的关键词提取(基于词频)
  132. const words = text.split(/[\s,,.。!??!]/).filter(word => word.length > 1);
  133. const wordFreq = new Map<string, number>();
  134. words.forEach(word => {
  135. wordFreq.set(word, (wordFreq.get(word) || 0) + 1);
  136. });
  137. // 按频率排序,取前 5 个
  138. const sorted = Array.from(wordFreq.entries())
  139. .sort((a, b) => b[1] - a[1])
  140. .slice(0, 5)
  141. .map(entry => entry[0]);
  142. return sorted.filter(tag => tag.length <= 10); // 过滤掉太长的词
  143. } catch (error) {
  144. console.error('提取标签失败:', error);
  145. return ['AI 生成', '有声书'];
  146. }
  147. }
  148. }
  149. // 导出单例
  150. export const aiSummaryService = new AISummaryService();