Java springbot项目qq机器人AI生成原神角色语音发送到QQ群
效果如下
一、前言
1.环境配置
jdk 11 + Python 3.7 (无需敲python代码,我也不太会,只需跑起项目即可)
2.关联工具
Vist 项目 :
链接:https://pan.baidu.com/s/1hHHyAbYCnsZ8teuKn92Wmg
提取码:1234
音频转换工具:
链接:https://pan.baidu.com/s/1-bwq7lSTJiYdYM9WHAkdKQ
提取码:1234
3.Vits项目启动
vits文件夹内,start.bat启动项目,端口8023,初次启动肯定会报错很正常(解决比较麻烦可以看下面,搭建问题有空我会描述更清晰点)
4.Vist 项目环境搭建问题
环境:python 3.7
vist目录执行 cd monotonic_align python setup.py build_ext
–inplace
错误解决麻烦请看 https://gitee.com/sumght/vits-yunzai-plugin
项目启动可能需要比较麻烦,看人
最主要的可能是torch无法找到对应的版本系统安装不了, 我也忘记当初怎么慢慢的调好了
启动碰到需要引用的包报错直接 python i 插件名
二、Java 项目结构
注意lib需要放那些东西
三、代码编写
1、maven配置
java-mirai-qrcode-0.1.jar 请看我的前篇文章,Java mirai 扫码登录**
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.bot</groupId>
<artifactId>bot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>JavaBot</name>
<description>Bot Spring Boot</description>
<properties>
<java.version>11</java.version>
<simbot.version>2.3.4</simbot.version>
<kotlin.version>1.7.10</kotlin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web-services</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.6</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.4</version>
</dependency>
<dependency>
<groupId>net.mamoe</groupId>
<artifactId>mirai-core-jvm</artifactId>
<version>2.15.0-M1</version>
</dependency>
<dependency>
<groupId>java-mirai-qrcode</groupId>
<artifactId>lame</artifactId>
<version>0.1</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/java-mirai-qrcode-0.1.jar</systemPath>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.yml</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.yml</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
<resource>
<directory>lib</directory>
<targetPath>/BOOT-INF/lib/</targetPath>
<includes>
<include>**/*.jar</include>
</includes>
</resource>
</resources>
</build>
</project>
2、启动类
注意启动类不是常规的SpringApplication.run(BotApplication.class);启动
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
@SpringBootApplication
public class BotApplication {
public static void main(String[] args) {
SpringApplicationBuilder builder = new SpringApplicationBuilder(BotApplication.class);
builder.headless(false).web(WebApplicationType.NONE).run(args); } }
3、Application.yml的配置
server: port: 2101 bot: # 你的qq号,建议用小号
account: 123456948978 vits: # 生成语音文件地址
file: 'C:\file\audio'
# 生成角色语音本地API api: 'http://127.0.0.1:8023/create'
# vits工程文件地址(未启动项目时备用)
path: 'D:\vits\run_new.py'
logging: level: learning: debug
file: name: log/app-user.log
4、工具类
AudioUtils 工具类,处理音频文件转换
package com.bot.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Component public class AudioUtils { static Logger logger = LoggerFactory.getLogger(AudioUtils.class); public static String path = "lib\\silk_converter\\";
public static String toSilk(String filePath, boolean isSource){
Integer index = filePath.lastIndexOf("\\") + 1;
return toSilk(filePath.substring(0, index),
filePath.substring(index, filePath.length()), isSource); }
public static String toSilk(String path, String name, boolean isSource) {
try {
String suffix = name.split("\\.")[1];
if (!suffix.toLowerCase().equals("mp3") && !suffix.toLowerCase().equals("wav")) {
throw new Exception("文件格式必须是mp3/wav");
}
String filePath = path + name; File file = new File(filePath);
if (!file.exists()) { throw new Exception("文件不存在!"); }
SimpleDateFormat ttime = new SimpleDateFormat("yyyyMMddhhMMSS");
String time = ttime.format(new Date());
String pcmPath = path + "PCM_" + time + ".pcm";
toPcm(filePath, pcmPath);
String silkPath = path + "SILK_" + time + ".silk";
pcmToSilk(pcmPath, silkPath);
File pcmFile = new File(pcmPath);
if (pcmFile.exists()) { pcmFile.delete(); }
if (isSource) { File audioFile = new File(filePath);
if (audioFile.exists()) { audioFile.delete(); } }
return silkPath; }
catch (Exception e) { e.printStackTrace(); } return null; }
public static void wavToPcm (String wavPath, String target) {
toPcm(wavPath, target); }
public static void mp3ToPcm(String mp3Path, String target) {
toPcm(mp3Path, target); }
private static void toPcm(String fpath, String target) {
List<String> commend = new ArrayList<String>();
commend.add(path + "ffmpeg.exe"); commend.add("-y");
commend.add("-i"); commend.add(fpath); commend.add("-f");
commend.add("s16le"); commend.add("-ar"); commend.add("24000");
commend.add("-ac"); commend.add("-2");
commend.add(target); Process p = null;
try {
ProcessBuilder builder = new ProcessBuilder();
builder.command(commend);
p = builder.start(); p.waitFor(); }
catch (Exception e) { e.printStackTrace(); }
finally { if (p != null) { p.destroy(); } } }
public static void pcmToSilk(String pcmPath, String target) {
Process process = null;
try { process = Runtime.getRuntime().exec("cmd /c start " + path + "silk_v3_encoder.exe "
+ pcmPath + " " + target + " -tencent"); process.waitFor(); Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace(); }
finally {
try { if (process != null) {
process.destroy();
} }
catch (Exception e) { e.printStackTrace(); } } }
public static void mp3ToAmr(String mp3Path, String target) {
File source = new File(path);
try { if (!source.exists()) { throw new Exception("文件不存在!");
}
List<String> commend = new ArrayList<String>();
commend.add(path + "ffmpeg.exe");
commend.add("-y");
commend.add("-i");
commend.add(mp3Path);
commend.add("-ac");
commend.add("1");
commend.add("-ar");
commend.add("8000");
commend.add(target);
try { ProcessBuilder builder = new ProcessBuilder();
builder.command(commend);
Process p = builder.start(); p.waitFor(); }
catch (Exception e) { e.printStackTrace(); } }
catch (Exception e) { logger.error("mp3转amr异常-{}", e); } }
public static byte[] byteAudio(String filePath) {
try { InputStream inStream = new FileInputStream(filePath);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = inStream.read(buffer)) > 0) {
baos.write(buffer, 0, bytesRead);
}
inStream.close();
baos.close();
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
BotGlobalUtils 储存全局变量Bot
import net.mamoe.mirai.Bot;
public class BotGlobalUtils {
private static Bot bot;
public static void setBot(Bot bot) {
BotGlobalUtils.bot = bot;
}
public static Bot getBot() {
return bot; } }
MessageUtils 处理qq群信息发送,其他信息的处理我没有加,比如发送图片,@,或者各种发送的混合的方法如需要可以在评论下留言
package com.bot.util;
import net.mamoe.mirai.contact.Group;
import net.mamoe.mirai.message.data.Audio;
import net.mamoe.mirai.utils.ExternalResource;
public class MessageUtils {
public static void sendGroupAudio (Long code, byte [] bytes) {
Audio audio; ExternalResource resource = ExternalResource.create(bytes);
Group group = BotGlobalUtils.getBot().getGroup(code);
try { audio = group.uploadAudio(resource); group.sendMessage(audio);
} catch (Exception e) { e.printStackTrace();
} finally { try { if (resource != null) { resource.close();
} } catch (Exception c) { c.printStackTrace();
} } } public static void sendGroupMsg(Long groupCode, String msg) { Group group = BotGlobalUtils.getBot().getGroup(groupCode);
if (group != null) { group.sendMessage(msg); } } }
5、Vist 原神音频生成处理及发送
先创建config包,在下面创建RestTemplateConfig类用于实例化RestTemplate,方便我们调用接口
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration public class RestTemplateConfig {
@Bean public RestTemplate restTemplate(){
return new RestTemplate();
}
}
创建vist包,把常量、配置类和pojo请求类先建好
VitsConstant 常量类
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class VitsConstant {
public static final List<String> CHARACTER = Arrays.stream(new String[]{
"派蒙", "凯亚", "安柏", "丽莎", "琴", "香菱", "枫原万叶", "迪卢克", "温迪", "可莉", "早柚", "托马", "芭芭拉", "优菈", "云堇",
"钟离", "魈", "凝光", "雷电将军", "北斗", "甘雨", "七七", "刻晴", "神里绫华", "戴因斯雷布", "雷泽", "神里绫人",
"罗莎莉亚", "阿贝多", "八重神子", "宵宫", "荒泷一斗", "九条裟罗", "夜兰", "珊瑚宫心海", "五郎", "散兵", "女士", "达达利亚",
"莫娜", "班尼特", "申鹤", "行秋", "烟绯", "久岐忍", "辛焱", "砂糖", "胡桃", "重云", "菲谢尔", "诺艾尔", "迪奥娜",
"鹿野院平藏"}).collect(Collectors.toList());
public static final List<String> CHINESE_NUM = Arrays.stream((new String[]{
"零", "一", "二", "三", "四", "五", "六", "七", "八", "九"})).collect(Collectors.toList());
}
VitsPojo vist项目请求参数组装
package com.bot.vits.pojo; public class VitsPojo {
private Integer character; private String path; private String fileName; private Float noise_scale; private Float noise_scale_w;
private Float length_scale;
private String text;
public VitsPojo() { }
public VitsPojo(VitsPojo pojo, String text) {
this.character = pojo.getCharacter();
this.path = pojo.getPath(); this.fileName = pojo.getFileName();
this.noise_scale = pojo.getNoise_scale();
this.noise_scale_w = pojo.getNoise_scale_w(); this.text = text;
}
public VitsPojo(Integer character, String text) {
this.character = character; this.text = text;
}
public String getText() { return text; }
public void setText(String text) { this.text = text; }
public Integer getCharacter() { return character; }
public void setCharacter(Integer character) { this.character = character; }
public String getPath() { return path; }
public void setPath(String path) { this.path = path; }
public String getFileName() { return fileName; }
public void setFileName(String fileName) { this.fileName = fileName; }
public Float getNoise_scale() { return noise_scale; }
public void setNoise_scale(Float noise_scale) { this.noise_scale = noise_scale; }
public Float getNoise_scale_w() { return noise_scale_w; }
public void setNoise_scale_w(Float noise_scale_w) { this.noise_scale_w = noise_scale_w; }
public Float getLength_scale() { return length_scale; }
public void setLength_scale(Float length_scale) { this.length_scale = length_scale; } }
VitsProperties 配置类
package com.bot.vits.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "vits")
public class VitsProperties {
private String file;
private String api;
private String path;
public String getFile() { return file; }
public void setFile(String file) { this.file = file; }
public String getApi() { return api; }
public void setApi(String api) { this.api = api; }
public String getPath() { return path; }
public void setPath(String path) { this.path = path; } }
新建service.VistService类,用于语音生成处理
package com.bot.vits.service;
import com.bot.enums.InsEnums;
import com.bot.param.GroupParam;
import com.bot.util.AudioUtils;
import com.bot.util.MessageUtils;
import com.bot.vits.constant.VitsConstant;
import com.bot.vits.pojo.VitsPojo;
import com.bot.vits.properties.VitsProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service
public class VitsService {
SimpleDateFormat ttime = new SimpleDateFormat("yyyyMMddhhMMSS");
static Logger logger = LoggerFactory.getLogger(VitsService.class);
@Autowired
private VitsProperties vitsProperties;
@Autowired private RestTemplate restTemplate;
public static Integer characterIndex (String name) {
for (int i = 0; i < VitsConstant.CHARACTER.size(); i++) {
if (VitsConstant.CHARACTER.get(i).equals(name)) {
return i; } }
return null; }
public static String chineseNumber (String text)
{ String speak = ""; for (int i = 0; i < text.length(); i++) {
String c = String.valueOf(text.charAt(i));
if (c.matches("\\d+")) {
c = VitsConstant.CHINESE_NUM.get(Integer.parseInt(c));
} speak = speak + c; }
return speak;
}
public void imitate (GroupParam groupParam) {
Pattern pattern = Pattern.compile(InsEnums.IMITATE.getRegex());
Matcher matcher = pattern.matcher(groupParam.getSerializeMessage());
if (matcher.find()) {
Integer character = characterIndex(matcher.group(1));
if (character != null) {
String message = chineseNumber(matcher.group(2));
String audioUrl = reqVits(new VitsPojo(character, message));
if (StringUtils.isEmpty(audioUrl)) {
MessageUtils.sendGroupMsg(groupParam.getGroupCode(), "生成语音异常"); return; }
String silkPath = AudioUtils.toSilk(audioUrl, true);
if (silkPath == null) { MessageUtils.sendGroupMsg(groupParam.getGroupCode(), "生成语音异常"); return; }
MessageUtils.sendGroupAudio(groupParam.getGroupCode(), AudioUtils.byteAudio(silkPath));
File silkFile = new File(silkPath);
if (silkFile.exists()) { silkFile.delete(); } }
else
{ MessageUtils.sendGroupMsg(groupParam.getGroupCode(), "模仿角色不存在"); } } }
public String reqVits (VitsPojo vitsPojo) {
vitsPojo.setNoise_scale(0.667F);
vitsPojo.setNoise_scale_w(0.8F);
vitsPojo.setLength_scale(1F);
vitsPojo.setFileName(ttime.format(new Date()) + ".wav");
vitsPojo.setPath(vitsProperties.getFile());
try {
File file = new File(vitsProperties.getFile());
if (!file.exists()) { file.createNewFile(); }
restTemplate.getForObject(vitsProperties.getApi() + buildParam(vitsPojo, true), String.class);
}
catch (Exception e)
{ logger.error("API生成原神角色语音异常", e);
Process process = null;
try { if (vitsProperties.getPath() == null) {
throw new Exception("没有配置vits工程文件地址-指令生成Ai语音失败"); }
String path = vitsProperties.getPath().substring(0, vitsProperties.getPath().lastIndexOf("\\") + 1);
String pyFile =vitsProperties.getPath().substring(vitsProperties.getPath().lastIndexOf("\\") + 1,
vitsProperties.getPath().length());
process = Runtime.getRuntime().exec("python " + pyFile + buildParam(vitsPojo, false),null, new File(path));
String cmd;
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
while ((cmd = bufferedReader.readLine()) != null) {
System.out.println(cmd); }
process.waitFor(10, TimeUnit.SECONDS); }
catch (Exception i) {
logger.error("指令生成原神角色语音异常", e); }
finally {
if (process != null) { process.destroy(); } } }
return vitsProperties.getFile() + "\\" + vitsPojo.getFileName(); }
public static String buildParam (VitsPojo vitsPojo, boolean isUrl) throws IllegalAccessException {
String request = "";
for (Field field : vitsPojo.getClass().getDeclaredFields()) {
field.setAccessible(true);
if (!StringUtils.isEmpty(field.get(vitsPojo))) {
if (isUrl) { String param = field.getName() + "=" + field.get(vitsPojo);
request = request + ( ("".equals(request))? "?" + param : "&" + param); }
else { request = request + " --" + field.getName() + "=" + field.get(vitsPojo); } } }
System.out.println("原神语音生成参数打印:" + request); return request; } }
6、QQ群指令校验和发送
创建指令处理的枚举类,enums.InsEnums
import java.util.regex.Pattern;
public enum InsEnums {
IMITATE(1, "^模仿(.*)说(.*)"), ALL_ROLE(2, "#角色大全") ;
private Integer code;
private String regex;
public boolean validate (String serializeMessage) {
Pattern pattern = Pattern.compile(regex);
if (pattern.matcher(serializeMessage).matches()) { return true; } return false; }
InsEnums (Integer code, String regex) { this.code = code; this.regex = regex; }
public Integer getCode() { return code; }
public void setCode(Integer code) { this.code = code; }
public String getRegex() { return regex; }
public void setRegex(String regex) { this.regex = regex; } }
我们再创建adapter,在下面创建群聊处理类GroupAdapter
import com.bot.enums.InsEnums;
import com.bot.param.GroupParam;
import com.bot.util.MessageUtils;
import com.bot.vits.constant.VitsConstant;
import com.bot.vits.service.VitsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class GroupAdapter { @Autowired private VitsService vitsService;
public void handle (GroupParam groupParam) {
if (InsEnums.IMITATE.validate(groupParam.getSerializeMessage())) {
vitsService.imitate(groupParam); }
else
if (InsEnums.ALL_ROLE.validate(groupParam.getSerializeMessage())) {
MessageUtils.sendGroupMsg(groupParam.getGroupCode(),
String.join(",", VitsConstant.CHARACTER)); } } }
7、QQ登录及自动回复信息(调用GroupAdapter 类)
创建properties包,在properties包下创建BotProperties
@ConfigurationProperties(prefix = “bot”) 的bot指的是application.yml下的bot
@ConfigurationProperties(prefix = "bot")
public class BotProperties {
private Long account;
public Long getAccount() { return account; }
public void setAccount(Long account) { this.account = account; } }
创建param包,在param包下创建GroupParam类,用于保存QQ群发送的信息或群信息
import net.mamoe.mirai.contact.Group;
import net.mamoe.mirai.contact.Member;
import net.mamoe.mirai.message.data.MessageChain;
public class GroupParam {
private Long groupCode;
private String groupName;
private String serializeMessage;
private Long personCode;
private String personName;
public GroupParam () {}
public GroupParam (MessageChain message, Group group, Member sender) {
this.groupCode = group.getId();
this.groupName = group.getName();
this.serializeMessage = message.serializeToMiraiCode();
this.personCode = sender.getId();
this.personName = sender.getNick(); }
public Long getGroupCode() { return groupCode; }
public void setGroupCode(Long groupCode) { this.groupCode = groupCode; }
public String getGroupName() { return groupName; }
public void setGroupName(String groupName) { this.groupName = groupName; }
public String getSerializeMessage() { return serializeMessage; }
public void setSerializeMessage(String serializeMessage) { this.serializeMessage = serializeMessage; }
public Long getPersonCode() { return personCode; }
public void setPersonCode(Long personCode) { this.personCode = personCode; } public String getPersonName() { return personName; }
public void setPersonName(String personName) { this.personName = personName; } }
创建config包,在config包下创建BotAutoLogin类
GroupAdapter 就是处理群消息的方法了
import com.bot.adapter.GroupAdapter;
import com.bot.param.GroupParam;
import com.bot.properties.BotProperties;
import com.bot.util.BotGlobalUtils;
import com.bot.vits.properties.VitsProperties;
import com.qrcode.QRCodeBot;
import kotlin.coroutines.EmptyCoroutineContext;
import net.mamoe.mirai.Bot;
import net.mamoe.mirai.event.ConcurrencyKind;
import net.mamoe.mirai.event.EventPriority;
import net.mamoe.mirai.event.events.GroupMessageEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;x
@Configuration
@EnableConfigurationProperties({BotProperties.class, VitsProperties.class})
public class BotAutoLogin {
@Autowired
private BotProperties botProperties;
@Autowired
private GroupAdapter groupAdapter;
@Bean public void login () {
Bot bot = QRCodeBot.getQRCodeBot(botProperties.getAccount()); bot.login();
b
ot.getEventChannel().subscribeAlways(GroupMessageEvent.class, EmptyCoroutineContext.INSTANCE,
ConcurrencyKind.CONCURRENT, EventPriority.NORMAL, event -> {
GroupParam param = new GroupParam(event.getMessage(), event.getGroup(), event.getSender()); groupAdapter.handle(param); });
BotGlobalUtils.setBot(bot); } }
到此结束,启动项目扫码登录,把机器人拉到群里就可以开始操作了~