2222import java .util .regex .Pattern ;
2323
2424/**
25- * 绘本
25+ * 绘本服务,负责处理绘本播放和文本同步
26+ * 使用JDK 21虚拟线程和结构化并发实现异步处理
2627 */
2728@ Service
2829public class HuiBenService {
@@ -55,8 +56,8 @@ public class HuiBenService {
5556 Runtime .getRuntime ().availableProcessors (),
5657 Thread .ofVirtual ().name ("huiBen-scheduler-" , 0 ).factory ());
5758
58- // 存储每个会话的当前歌词信息
59- private final Map <String , List <LyricLine >> sessionLyrics = new ConcurrentHashMap <>();
59+ // 存储每个会话的当前文本信息
60+ private final Map <String , List <TextLine >> sessionTexts = new ConcurrentHashMap <>();
6061
6162 // 存储每个会话的当前播放时间
6263 private final Map <String , AtomicLong > playTime = new ConcurrentHashMap <>();
@@ -68,16 +69,19 @@ public class HuiBenService {
6869 private final Map <String , ScheduledFuture <?>> scheduledTasks = new ConcurrentHashMap <>();
6970
7071 /**
71- * 歌词行数据结构 - 使用JDK 16+ Record类型
72+ * 文本行数据结构 - 使用JDK 16+ Record类型
7273 */
73- private record LyricLine (long timeMs , String text ) {
74+ private record TextLine (long timeMs , String text ) {
7475 }
7576
7677 /**
77- * 搜索并播放绘本
78+ * 播放绘本
7879 * 使用JDK 21虚拟线程和结构化并发实现异步处理
80+ *
81+ * @param session 会话
82+ * @param bookId 绘本ID
7983 */
80- public void playMusic (ChatSession session , Integer song ) {
84+ public void playHuiBen (ChatSession session , Integer bookId ) {
8185 String sessionId = session .getSessionId ();
8286
8387 // 使用虚拟线程处理异步任务
@@ -93,13 +97,13 @@ public void playMusic(ChatSession session, Integer song) {
9397 cleanupAudioFile (sessionId );
9498
9599 // 1. 获取绘本信息
96- Map <String , String > musicInfo = getHuiBenInfo (song );
97- if (musicInfo == null ) {
98- throw new RuntimeException ("无法找到歌曲 : " + song );
100+ Map <String , String > huiBenInfo = getHuiBenInfo (bookId );
101+ if (huiBenInfo == null ) {
102+ throw new RuntimeException ("无法找到绘本 : " + bookId );
99103 }
100104
101105 // 2. 下载音频文件到本地临时目录,使用随机文件名避免冲突
102- String audioUrl = musicInfo .get ("audioUrl" );
106+ String audioUrl = huiBenInfo .get ("audioUrl" );
103107 String randomName = "huiBen_" + sessionId + "_" + UUID .randomUUID () + ".mp3" ;
104108 String audioPath = downloadFile (audioUrl , randomName );
105109
@@ -113,7 +117,7 @@ public void playMusic(ChatSession session, Integer song) {
113117 // 发送绘本开始消息
114118 audioService .sendStart (session );
115119
116- // 发送音频和同步歌词
120+ // 发送音频和同步文本
117121 sendAudio (session , audioPath );
118122
119123 } catch (Exception e ) {
@@ -141,12 +145,12 @@ private void cleanupAudioFile(String sessionId) {
141145 logger .warn ("删除音频文件失败: {}" , audioPath , e );
142146 }
143147 }
144- // 清理会话的歌词数据
145- sessionLyrics .remove (sessionId );
148+ // 清理会话的文本数据
149+ sessionTexts .remove (sessionId );
146150 }
147151
148152 /**
149- * 发送音频和同步歌词
153+ * 发送音频和同步文本
150154 */
151155 private void sendAudio (ChatSession session , String audioPath ) {
152156 String sessionId = session .getSessionId ();
@@ -173,17 +177,17 @@ private void sendAudio(ChatSession session, String audioPath) {
173177 return ;
174178 }
175179
176- // 获取歌词
177- List <LyricLine > lyrics = sessionLyrics .getOrDefault (sessionId , Collections .emptyList ());
180+ // 获取文本
181+ List <TextLine > texts = sessionTexts .getOrDefault (sessionId , Collections .emptyList ());
178182 AtomicLong currPlayTime = playTime .computeIfAbsent (sessionId , k -> new AtomicLong (0 ));
179183
180- // 预处理歌词时间点 ,将毫秒时间转换为帧索引
181- Map <Integer , String > lyricFrameMap = new HashMap <>();
182- for (LyricLine line : lyrics ) {
183- // 计算歌词对应的帧索引
184+ // 预处理文本时间点 ,将毫秒时间转换为帧索引
185+ Map <Integer , String > textFrameMap = new HashMap <>();
186+ for (TextLine line : texts ) {
187+ // 计算文本对应的帧索引
184188 int frameIndex = (int ) (line .timeMs () / OPUS_FRAME_INTERVAL_MS );
185189 if (frameIndex < frames .size ()) {
186- lyricFrameMap .put (frameIndex , line .text ());
190+ textFrameMap .put (frameIndex , line .text ());
187191 }
188192 }
189193
@@ -204,10 +208,10 @@ private void sendAudio(ChatSession session, String audioPath) {
204208 // 更新当前播放时间
205209 currPlayTime .set (currentIndex * OPUS_FRAME_INTERVAL_MS );
206210
207- // 先检查是否有对应这一帧的歌词需要发送
208- String lyricText = lyricFrameMap .get (currentIndex );
209- if (lyricText != null ) {
210- audioService .sendSentenceStart (session , lyricText );
211+ // 先检查是否有对应这一帧的文本需要发送
212+ String textContent = textFrameMap .get (currentIndex );
213+ if (textContent != null ) {
214+ audioService .sendSentenceStart (session , textContent );
211215 }
212216
213217 // 发送当前帧
@@ -246,13 +250,14 @@ private void sendAudio(ChatSession session, String audioPath) {
246250 /**
247251 * 获取绘本信息(音频URL)
248252 */
249- private Map <String , String > getHuiBenInfo (Integer num ) {
253+ private Map <String , String > getHuiBenInfo (Integer bookId ) {
250254 try {
251255 // 构建URL
256+ String url = API_BASE_URL + bookId + ".html" ;
252257
253258 // 使用OkHttp3发送请求
254259 Request request = new Request .Builder ()
255- .url (API_BASE_URL + num + ".html" )
260+ .url (url )
256261 .get ()
257262 .build ();
258263
@@ -262,14 +267,14 @@ private Map<String, String> getHuiBenInfo(Integer num) {
262267 return null ;
263268 }
264269
265- // 解析JSON响应
270+ // 解析响应
266271 String responseBody = response .body () != null ? response .body ().string () : null ;
267272 if (responseBody == null ) {
268273 logger .error ("获取绘本信息失败,响应体为空" );
269274 return null ;
270275 }
271276
272- String audioUrl = extractAudioSrcByRegex (responseBody . toString () );
277+ String audioUrl = extractAudioSrcByRegex (responseBody );
273278 Map <String , String > result = new HashMap <>();
274279 result .put ("audioUrl" , audioUrl );
275280 return result ;
@@ -280,7 +285,9 @@ private Map<String, String> getHuiBenInfo(Integer num) {
280285 }
281286 }
282287
283-
288+ /**
289+ * 从HTML中提取音频源URL
290+ */
284291 public static String extractAudioSrcByRegex (String html ) {
285292 // 匹配 source 标签中的 src 属性
286293 Pattern pattern = Pattern .compile ("<source\\ s+[^>]*src\\ s*=\\ s*[\" ']([^\" ']+)[\" '][^>]*>" );
@@ -293,7 +300,6 @@ public static String extractAudioSrcByRegex(String html) {
293300 return null ;
294301 }
295302
296-
297303 /**
298304 * 下载文件到临时目录
299305 */
@@ -329,68 +335,12 @@ private String downloadFile(String fileUrl, String fileName) {
329335 }
330336 }
331337
332- /**
333- * 解析LRC格式歌词
334- */
335- private List <LyricLine > parseLyrics (String lyricUrl ) {
336- List <LyricLine > result = new ArrayList <>();
337-
338- if (lyricUrl == null || lyricUrl .isEmpty ()) {
339- logger .warn ("歌词URL为空,无法解析歌词" );
340- return result ;
341- }
342-
343- try {
344-
345- // 使用OkHttp3发送请求
346- Request request = new Request .Builder ()
347- .url (lyricUrl )
348- .get ()
349- .build ();
350-
351- try (Response response = okHttpClient .newCall (request ).execute ()) {
352- if (!response .isSuccessful () || response .body () == null ) {
353- logger .error ("获取歌词失败,响应码: {}" , response .code ());
354- return result ;
355- }
356-
357- String responseBody = response .body ().string ();
358-
359- // LRC时间标签正则表达式: [mm:ss.xx]
360- Pattern pattern = Pattern .compile ("\\ [(\\ d{2}):(\\ d{2})\\ .(\\ d{2})\\ ](.*)" );
361-
362- // 使用Stream API处理每一行
363- return responseBody .lines ()
364- .map (pattern ::matcher )
365- .filter (Matcher ::find )
366- .map (matcher -> {
367- int minutes = Integer .parseInt (matcher .group (1 ));
368- int seconds = Integer .parseInt (matcher .group (2 ));
369- int hundredths = Integer .parseInt (matcher .group (3 ));
370-
371- // 计算毫秒时间
372- long timeMs = (minutes * 60 * 1000 ) + (seconds * 1000 ) + (hundredths * 10 );
373- String text = matcher .group (4 ).trim ();
374-
375- return new LyricLine (timeMs , text );
376- })
377- .sorted (Comparator .comparingLong (LyricLine ::timeMs ))
378- .toList ();
379- }
380-
381- } catch (Exception e ) {
382- logger .error ("解析歌词时发生错误" , e );
383- }
384-
385- return result ;
386- }
387-
388338 /**
389339 * 停止播放绘本
390340 *
391341 * @param sessionId 会话ID
392342 */
393- public void stopMusic (String sessionId ) {
343+ public void stopHuiBen (String sessionId ) {
394344 Thread .startVirtualThread (() -> {
395345 try {
396346 ScheduledFuture <?> task = scheduledTasks .remove (sessionId );
0 commit comments