jfinal-4.2 通过jocab 调用windows的sapi进行语音合成
[产生背景]
1.因为其他厂商语音合成的内网部署都太贵了
2.众所周知,widows系统本身就有语音合成的功能
3.能不能将windows的语音合成对提供成HTTP API服务.让其他程序调用,这样在内网0费用就可以使用语音合成了
搭建工程
添加依赖
<dependency> <groupId>com.jfinal</groupId> <artifactId>jfinal-undertow</artifactId> <version>1.6</version> </dependency> <dependency> <groupId>com.jfinal</groupId> <artifactId>jfinal</artifactId> <version>4.2</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <!--桥接器:告诉slf4j使用Log4j2 --> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.1</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.1</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.1</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.25</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.10</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.hynnet</groupId> <artifactId>jacob</artifactId> <version>1.18</version> </dependency>
创建在工程目录下添加lib目录将jacob-1.18-x64.dll添加lib目录
LibraryUtil 将当前工程下的lib目录添加到library path
package com.litong.utils.dll; import java.io.File; import java.io.IOException; import java.lang.reflect.Field; public class LibraryUtil { public static void addLibary() { File file = new File("lib"); String absolutePath = file.getAbsolutePath(); System.out.println("absolutePath:" + absolutePath); if (!file.exists()) { file.mkdirs(); } try { addLibDir(absolutePath); } catch (IOException e1) { e1.printStackTrace(); } } public static void addLibDir(String s) throws IOException { try { Field field = java.lang.ClassLoader.class.getDeclaredField("usr_paths"); field.setAccessible(true); String[] paths = (String[]) field.get(null); for (int i = 0; i < paths.length; i++) { if (s.equals(paths[i])) { return; } } String[] tmp = new String[paths.length + 1]; System.arraycopy(paths, 0, tmp, 0, paths.length); tmp[paths.length] = s; field.set(null, tmp); } catch (IllegalAccessException e) { throw new IOException("Failed to get permissions to set library path"); } catch (NoSuchFieldException e) { throw new IOException("Failed to get field handle to set library path"); } } }
启动类Application的main方法中调用LibraryUtil.addLibary(); 加载
package com.litong.jfinal; import com.jfinal.config.Constants; import com.jfinal.config.Handlers; import com.jfinal.config.Interceptors; import com.jfinal.config.JFinalConfig; import com.jfinal.config.Plugins; import com.jfinal.config.Routes; import com.jfinal.server.undertow.UndertowConfig; import com.jfinal.server.undertow.UndertowServer; import com.jfinal.template.Engine; import com.litong.jfinal.route.AdminRoutes; import com.litong.jfinal.route.ApiRoutes; import com.litong.jfinal.route.FrontRoutes; import com.litong.jfinal.route.SystemRoutes; import com.litong.jfinal.utils.PropKitUtil; import com.litong.jfinal.utils.UndertowUtil; import com.litong.utils.dll.LibraryUtil; import com.litong.utils.ip.IPUtil; public class Application extends JFinalConfig { private static String configFileName = PropKitUtil.configFileName; public static void main(String[] args) { long start = System.currentTimeMillis(); LibraryUtil.addLibary(); UndertowUtil.server = UndertowServer.create(Application.class, configFileName); // 启动Server全局共享 UndertowUtil.server.addSystemClassPrefix("com.litong.jfinal.utils.UndertowUtil"); UndertowUtil.server.start(); UndertowConfig undertowConfig = UndertowUtil.server.getUndertowConfig(); int port = undertowConfig.getPort(); String contextPath = undertowConfig.getContextPath(); long end = System.currentTimeMillis(); System.out.println(IPUtil.getThisUrl(port, contextPath)); System.out.println("启动完成,共使用了" + (end - start) + "ms"); } public void configConstant(Constants me) { me.setInjectDependency(true); me.setInjectSuperClass(true); } public void configRoute(Routes me) { me.setMappingSuperClass(true); me.add(new FrontRoutes()); // 前端路由 me.add(new AdminRoutes()); // 后端路由 me.add(new SystemRoutes()); // 系统路由 me.add(new ApiRoutes()); // API路由 } @Override public void configEngine(Engine me) { } @Override public void configPlugin(Plugins me) { } @Override public void configInterceptor(Interceptors me) { } @Override public void configHandler(Handlers me) { } }
配置路由
ApiRoutes
package com.litong.jfinal.route; import com.jfinal.config.Routes; import com.litong.modules.map.controller.MapServiceController; import com.litong.modules.tts.controller.TTSController; /** * @author bill robot * @date 2020年8月16日_下午5:08:54 * @version 1.0 * @desc */ public class ApiRoutes extends Routes { @Override public void config() { add("tts", TTSController.class); } }
TTSController
package com.litong.modules.tts.controller; import java.io.File; import com.jfinal.core.Controller; import com.jfinal.core.paragetter.Para; import com.litong.modules.tts.service.MsTtsUtil; import com.litong.modules.tts.vo.TTSVo; import lombok.extern.slf4j.Slf4j; /** * @author bill robot * @date 2020年8月16日_下午11:43:15 * @version 1.0 * @desc */ @Slf4j public class TTSController extends Controller { public void test(@Para("") TTSVo ttsVo) { renderJson(ttsVo); } public void index(@Para("") TTSVo ttsVo) { // 设置翻发音人需要修改注册表,我放弃修改了; String filePath = MsTtsUtil.stringToFile(ttsVo); if (filePath == null) { renderText("tts fail"); log.error("tts fail"); return; } File file = new File(filePath); if (!file.exists()) { String msg = "file is not exists:" + file.getAbsolutePath(); renderText(msg); log.error(msg); } else { renderFile(file); } return; } }
MsTtsUtil语音合成工具类
package com.litong.modules.tts.service; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import com.jacob.activeX.ActiveXComponent; import com.jacob.com.Dispatch; import com.jacob.com.Variant; import com.jfinal.kit.HashKit; import com.litong.modules.tts.vo.TTSVo; import com.litong.utils.dll.LibraryUtil; import lombok.extern.slf4j.Slf4j; /** * MS 语音合成工具类 */ @Slf4j public class MsTtsUtil { public static void main(String[] args) { LibraryUtil.addLibary(); TTSVo ttsVo = new TTSVo(); ttsVo.setType(6); ttsVo.setVolume(100); ttsVo.setRate(0); ttsVo.setText("今天猪肉又涨价了,一下子涨了两块钱"); stringToFile(ttsVo); ttsVo.setText("今天羊肉羊肉也涨价了"); stringToFile(ttsVo); // String path = "text\\过秦论.txt"; // textToFile(path); } /** * 语音转文字音频 * * @param txt */ public static String stringToFile(TTSVo ttsVo) { ActiveXComponent ax = new ActiveXComponent("Sapi.SpVoice"); String filePath = getFilePath(ax, ttsVo); try { // 运行时输出语音内容 Dispatch spVoice = ax.getObject(); // 下面是构建文件流到成语音文件 ax = new ActiveXComponent("Sapi.SpFileStream"); Dispatch spFileStream = ax.getObject(); ax = new ActiveXComponent("Sapi.SpAudioFormat"); Dispatch spAudioFormat = ax.getObject(); // 设置音频流格式 Dispatch.put(spAudioFormat, "Type", ttsVo.getType()); // 设置文件输出流格式 Dispatch.putRef(spFileStream, "Format", spAudioFormat); // 调用输出 文件流打开方法,创建一个.wav文件 Dispatch.call(spFileStream, "Open", new Variant(filePath), new Variant(3), new Variant(true)); // 设置声音对象的音频输出流为输出文件对象 Dispatch.putRef(spVoice, "AudioOutputStream", spFileStream); // 设置音量 0到100 Dispatch.put(spVoice, "Volume", new Variant(ttsVo.getVolume())); // 设置朗读速度 Dispatch.put(spVoice, "Rate", new Variant(ttsVo.getRate())); // 开始朗读,实际上是合成得到文件 Dispatch.call(spVoice, "Speak", new Variant(ttsVo.getText())); // 关闭输出文件 Dispatch.call(spFileStream, "Close"); Dispatch.putRef(spVoice, "AudioOutputStream", null); spAudioFormat.safeRelease(); spFileStream.safeRelease(); spVoice.safeRelease(); } catch (Exception e) { e.printStackTrace(); } finally { ax.safeRelease(); } return filePath; } public static String getFilePath(ActiveXComponent ax, TTSVo ttsVo) { File file = new File("tts"); if (!file.exists()) { file.mkdirs(); log.info("mkdir:{}", file.getAbsoluteFile()); } // 文件名规则 // voice_type_volume_rate_md5 String voice = getCurrentVoice(ax); String filename = append("_", ttsVo.getType(), ttsVo.getVolume(), ttsVo.getRate()); // 添加文件名称 filename += "_" + HashKit.md5(ttsVo.getText()); // 添加文件后缀 if (ttsVo.getType() == 6) { filename += ".wav"; } // 发音人和加目录 filename = "tts/" + voice + "_" + filename; return filename; } private static String append(String string, int... ints) { String retval = new String(); for (int i : ints) { retval += "_" + i; } return retval; } private static String getCurrentVoice(ActiveXComponent ax) { // 获取Sapi.SpVoice对象的voice属性值,voice的属性值是一个voice对象,对象使用Dispatch包装 Dispatch voiceDispatche = ax.getProperty("voice").toDispatch(); // 执行voice对象的GetDescription方法,返回值可能使用String,使用Variant包装 Variant descripton = Dispatch.call(voiceDispatche, "GetDescription"); return descripton.toString(); } public static String textToFile(String path,TTSVo ttsVo) { ActiveXComponent ax = new ActiveXComponent("Sapi.SpVoice"); String filePath = getFilePath(ax, ttsVo); try { // 运行时输出语音内容 Dispatch spVoice = ax.getObject(); // 下面是构建文件流到成语音文件 ax = new ActiveXComponent("Sapi.SpFileStream"); Dispatch spFileStream = ax.getObject(); ax = new ActiveXComponent("Sapi.SpAudioFormat"); Dispatch spAudioFormat = ax.getObject(); // 设置音频流格式 Dispatch.put(spAudioFormat, "Type", new Variant(ttsVo.getType())); // 设置文件输出流格式 Dispatch.putRef(spFileStream, "Format", spAudioFormat); // 调用输出 文件流打开方法,创建一个.wav文件 Dispatch.call(spFileStream, "Open", new Variant(filePath), new Variant(3), new Variant(true)); // 设置声音对象的音频输出流为输出文件对象 Dispatch.putRef(spVoice, "AudioOutputStream", spFileStream); // 设置音量 0到100 Dispatch.put(spVoice, "Volume", new Variant(ttsVo.getVolume())); // 设置朗读速度 Dispatch.put(spVoice, "Rate", new Variant(ttsVo.getRate())); // 输入文件 File srcFile = new File(path); // 使用包装字符流读取文件 BufferedReader br = new BufferedReader(new FileReader(srcFile)); String content = br.readLine(); // 开始朗读,实际上是合成得到文件 while (content != null) { Dispatch.call(spVoice, "Speak", new Variant(content)); content = br.readLine(); } br.close(); // 关闭输出文件 Dispatch.call(spFileStream, "Close"); Dispatch.putRef(spVoice, "AudioOutputStream", null); spAudioFormat.safeRelease(); spFileStream.safeRelease(); spVoice.safeRelease(); } catch (Exception e) { e.printStackTrace(); } finally { ax.safeRelease(); } return filePath; } }
添加完成后启动访问浏览器测试
http://192.168.0.10:11023/litongjava-windows-tts/tts?type=6&volume=100&rate=0&text=秦孝公据崤函之固,拥雍州之地,君臣固守以窥周室,有席卷天下,包举宇内,囊括四海之意,并吞八荒之心。当是时也,商君佐之,内立法度,务耕织,修守战之具,外连衡而斗诸侯。于是秦人拱手而取西河之外。 孝公既没,惠文、武、昭襄蒙故业,因遗策,南取汉中,西举巴、蜀,东割膏腴之地,北收要害之郡。诸侯恐惧,会盟而谋弱秦,不爱珍器重宝肥饶之地,以致天下之士,合从缔交,相与为一。当此之时,齐有孟尝,赵有平原,楚有春申,魏有信陵。此四君者,皆明智而忠信,宽厚而爱人,尊贤而重士,约从离衡,兼韩、魏、燕、楚、齐、赵、宋、卫、中山之众。于是六国之士,有宁越、徐尚、苏秦、杜赫之属为之谋,齐明、周最、陈轸、召滑、楼缓、翟景、苏厉、乐毅之徒通其意,吴 把jacob-1.18-x64.dll下载 https://files.cnblogs.com/files/w1441639547/jacob-1.18-x64.rar