四零语境前端代码仓库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

280 lines
8.2 KiB

  1. /**
  2. * 音频相关工具方法
  3. * AudioControls.vue 抽离的通用工具方法
  4. */
  5. /**
  6. * 智能分割文本按句号和逗号分割中英文文本
  7. * @param {string} text - 要分割的文本
  8. * @returns {Array} 分割后的文本段落数组
  9. */
  10. export function splitTextIntelligently(text) {
  11. if (!text || typeof text !== 'string') {
  12. return [text];
  13. }
  14. // 判断是否为中文文本(包含中文字符)
  15. const isChinese = /[\u4e00-\u9fa5]/.test(text);
  16. const maxLength = isChinese ? 100 : 200;
  17. // 如果文本长度不超过限制,直接返回
  18. if (text.length <= maxLength) {
  19. return [{
  20. text: text,
  21. startIndex: 0,
  22. endIndex: text.length - 1
  23. }];
  24. }
  25. const segments = [];
  26. let currentText = text;
  27. let globalStartIndex = 0;
  28. while (currentText.length > 0) {
  29. if (currentText.length <= maxLength) {
  30. // 剩余文本不超过限制,直接添加
  31. segments.push({
  32. text: currentText,
  33. startIndex: globalStartIndex,
  34. endIndex: globalStartIndex + currentText.length - 1
  35. });
  36. break;
  37. }
  38. // 在限制长度内寻找最佳分割点
  39. let splitIndex = maxLength;
  40. let bestSplitIndex = -1;
  41. // 优先寻找句号
  42. for (let i = Math.min(maxLength, currentText.length - 1); i >= Math.max(0, maxLength - 50); i--) {
  43. const char = currentText[i];
  44. if (char === '。' || char === '.') {
  45. bestSplitIndex = i + 1; // 包含句号
  46. break;
  47. }
  48. }
  49. // 如果没找到句号,寻找逗号
  50. if (bestSplitIndex === -1) {
  51. for (let i = Math.min(maxLength, currentText.length - 1); i >= Math.max(0, maxLength - 50); i--) {
  52. const char = currentText[i];
  53. if (char === ',' || char === ',' || char === ';' || char === ';') {
  54. bestSplitIndex = i + 1; // 包含标点符号
  55. break;
  56. }
  57. }
  58. }
  59. // 如果还是没找到合适的分割点,使用默认长度
  60. if (bestSplitIndex === -1) {
  61. bestSplitIndex = maxLength;
  62. }
  63. // 提取当前段落
  64. const segment = currentText.substring(0, bestSplitIndex).trim();
  65. if (segment.length > 0) {
  66. segments.push({
  67. text: segment,
  68. startIndex: globalStartIndex,
  69. endIndex: globalStartIndex + segment.length - 1
  70. });
  71. }
  72. // 更新剩余文本和全局索引
  73. currentText = currentText.substring(bestSplitIndex).trim();
  74. globalStartIndex += bestSplitIndex;
  75. }
  76. return segments;
  77. }
  78. /**
  79. * 格式化时间显示
  80. * @param {number} seconds - 秒数
  81. * @returns {string} 格式化后的时间字符串 (mm:ss)
  82. */
  83. export function formatTime(seconds) {
  84. const mins = Math.floor(seconds / 60);
  85. const secs = Math.floor(seconds % 60);
  86. return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
  87. }
  88. /**
  89. * 生成音频缓存键
  90. * @param {string} courseId - 课程ID
  91. * @param {number} pageNumber - 页面号码
  92. * @param {string} voiceId - 音色ID
  93. * @returns {string} 缓存键
  94. */
  95. export function generateCacheKey(courseId, pageNumber, voiceId) {
  96. return `${courseId}_${pageNumber}_${voiceId}`;
  97. }
  98. /**
  99. * 验证缓存数据的有效性
  100. * @param {Object} cachedData - 缓存的音频数据
  101. * @param {number} expectedPage - 期望的页面号码
  102. * @returns {boolean} 缓存数据是否有效
  103. */
  104. export function validateCacheData(cachedData, expectedPage) {
  105. if (!cachedData || !cachedData.audios || cachedData.audios.length === 0) {
  106. return false;
  107. }
  108. // 检查页面号码匹配
  109. if (expectedPage !== null && cachedData.pageNumber && cachedData.pageNumber !== expectedPage) {
  110. return false;
  111. }
  112. // 检查音频URL有效性
  113. const firstAudio = cachedData.audios[0];
  114. if (!firstAudio || !firstAudio.url) {
  115. return false;
  116. }
  117. return true;
  118. }
  119. /**
  120. * 查找第一个非导语音频的索引
  121. * @param {Array} audioArray - 音频数组
  122. * @returns {number} 第一个非导语音频的索引如果没有找到返回-1
  123. */
  124. export function findFirstNonLeadAudio(audioArray) {
  125. if (!audioArray || audioArray.length === 0) {
  126. return -1;
  127. }
  128. // 从第一个音频开始查找非导语音频
  129. for (let i = 0; i < audioArray.length; i++) {
  130. const audioData = audioArray[i];
  131. if (audioData && !audioData.isLead) {
  132. return i;
  133. }
  134. }
  135. // 如果所有音频都是导语,返回 -1 表示不播放
  136. return -1;
  137. }
  138. /**
  139. * 限制缓存大小保留最近访问的页面
  140. * @param {Object} audioCache - 音频缓存对象
  141. * @param {number} maxSize - 最大缓存数量默认10
  142. * @returns {Object} 清理后的缓存对象
  143. */
  144. export function limitCacheSize(audioCache, maxSize = 10) {
  145. const cacheKeys = Object.keys(audioCache);
  146. if (cacheKeys.length > maxSize) {
  147. // 删除最旧的缓存项
  148. const keysToDelete = cacheKeys.slice(0, cacheKeys.length - maxSize);
  149. keysToDelete.forEach(key => {
  150. delete audioCache[key];
  151. });
  152. }
  153. return audioCache;
  154. }
  155. /**
  156. * 清理音频缓存
  157. * @param {Object} audioCache - 音频缓存对象
  158. * @returns {Object} 清空的缓存对象
  159. */
  160. export function clearAudioCache(audioCache) {
  161. return {};
  162. }
  163. /**
  164. * 音频状态重置工具
  165. * @param {Object} audioState - 音频状态对象
  166. * @param {boolean} clearHighlight - 是否清除高亮索引默认true
  167. * @returns {Object} 重置后的状态对象
  168. */
  169. export function resetAudioState(audioState, clearHighlight = true) {
  170. const resetState = {
  171. ...audioState,
  172. isPlaying: false,
  173. currentTime: 0,
  174. sliderValue: 0
  175. };
  176. if (clearHighlight) {
  177. resetState.currentHighlightIndex = -1;
  178. }
  179. return resetState;
  180. }
  181. /**
  182. * 检查音频数据是否属于当前页面
  183. * @param {Object} audioData - 音频数据
  184. * @param {string} expectedCacheKey - 期望的缓存键
  185. * @returns {boolean} 是否属于当前页面
  186. */
  187. export function isAudioDataForCurrentPage(audioData, expectedCacheKey) {
  188. if (!audioData || !audioData.cacheKey) {
  189. return false;
  190. }
  191. return audioData.cacheKey === expectedCacheKey;
  192. }
  193. /**
  194. * 将字级别的字幕数据转换为句子级别
  195. * @param {Array} wordData - 字级别的字幕数据数组
  196. * @returns {Array} 句子级别的字幕数据数组
  197. */
  198. export function convertToSentences(wordData) {
  199. if (!wordData || wordData.length === 0) return []
  200. const sentences = []
  201. let currentSentence = {
  202. text: '',
  203. beginTime: null,
  204. endTime: null,
  205. words: []
  206. }
  207. // 句子结束标点符号(中英文)
  208. const sentenceEndMarks = /[。!?;.!?;…]/
  209. for (let i = 0; i < wordData.length; i++) {
  210. const word = wordData[i]
  211. // 设置句子开始时间
  212. if (currentSentence.beginTime === null) {
  213. currentSentence.beginTime = word.BeginTime
  214. }
  215. // 添加文字到当前句子
  216. currentSentence.text += word.Text
  217. currentSentence.words.push(word)
  218. currentSentence.endTime = word.EndTime
  219. // 检查是否是句子结束
  220. const isLastWord = i === wordData.length - 1
  221. const isEndOfSentence = sentenceEndMarks.test(word.Text)
  222. // 如果遇到句子结束标点或者是最后一个字,结束当前句子
  223. if (isEndOfSentence || isLastWord) {
  224. // 只有当句子有内容时才添加
  225. if (currentSentence.text) {
  226. sentences.push({
  227. text: currentSentence.text,
  228. beginTime: currentSentence.beginTime,
  229. endTime: currentSentence.endTime,
  230. duration: currentSentence.endTime - currentSentence.beginTime,
  231. wordCount: currentSentence.words.length
  232. })
  233. }
  234. // 重置当前句子
  235. currentSentence = {
  236. text: '',
  237. beginTime: null,
  238. endTime: null,
  239. words: []
  240. }
  241. }
  242. }
  243. return sentences
  244. }