Skip to content

Commit 04814f0

Browse files
committed
update: 更新绘本播放代码
1 parent a3909c0 commit 04814f0

File tree

2 files changed

+45
-95
lines changed

2 files changed

+45
-95
lines changed

src/main/java/com/xiaozhi/dialogue/llm/tool/function/PlayHuiBenFunction.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,22 +33,22 @@ public class PlayHuiBenFunction implements ToolsGlobalRegistry.GlobalFunction {
3333
if (num == null || num < 5 || num > 1100) {
3434
num = RandomUtil.randomInt(5, 1100);
3535
}
36-
huiBenService.playMusic(chatSession, num);
36+
huiBenService.playHuiBen(chatSession, num);
3737
return "尝试播放绘本《" + num + "》";
3838

3939
} catch (Exception e) {
40-
logger.error("device 绘本播放异常,song name: {}", num, e);
40+
logger.error("播放绘本异常,绘本编号: {}", num, e);
4141
return "绘本播放失败";
4242
}
4343
})
4444
.toolMetadata(ToolMetadata.builder().returnDirect(true).build())
45-
.description("绘本播放助手,需要用户提供绘本数字编号")
45+
.description("绘本播放助手需要用户提供绘本数字编号")
4646
.inputSchema("""
4747
{
4848
"type": "object",
4949
"properties": {
50-
"songName": {
51-
"type": "string",
50+
"num": {
51+
"type": "integer",
5252
"description": "要播放的绘本数字编号"
5353
}
5454
},
@@ -63,4 +63,4 @@ public class PlayHuiBenFunction implements ToolsGlobalRegistry.GlobalFunction {
6363
public ToolCallback getFunctionCallTool(ChatSession chatSession) {
6464
return toolCallback;
6565
}
66-
}
66+
}

src/main/java/com/xiaozhi/dialogue/service/HuiBenService.java

Lines changed: 39 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
import java.util.regex.Pattern;
2323

2424
/**
25-
* 绘本
25+
* 绘本服务,负责处理绘本播放和文本同步
26+
* 使用JDK 21虚拟线程和结构化并发实现异步处理
2627
*/
2728
@Service
2829
public 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

Comments
 (0)