当前位置: 首页 > news >正文

String boot 接入 azure云TTS

1.引入依赖

<!--微软的tts服务--><dependency><groupId>com.microsoft.cognitiveservices.speech</groupId><artifactId>client-sdk</artifactId><version>1.44.0</version></dependency><dependency><groupId>com.azure</groupId><artifactId>azure-identity</artifactId><version>1.13.1</version></dependency>

2.英文情绪到中文的映射表

package com.ruoyi.image.utils;import java.util.HashMap;
import java.util.Map;public class AzureTTSStyleTranslator {// 英文风格到中文的映射表public static final Map<String, String> STYLE_TRANSLATIONS = new HashMap<String, String>() {{put("assistant", "助理");put("chat", "聊天");put("customerservice", "客户服务");put("newscast", "新闻");put("affectionate", "撒娇");put("angry", "愤怒");put("calm", "平静");put("cheerful", "愉悦");put("disgruntled", "不满");put("fearful", "害怕");put("gentle", "温柔");put("lyrical", "抒情");put("sad", "悲伤");put("serious", "严厉");put("poetry-reading", "诗歌朗诵");put("friendly", "友好");put("chat-casual", "聊天 - 休闲");put("whispering", "低语");put("sorry", "抱歉");put("excited", "兴奋");put("narration-relaxed", "旁白-放松");put("embarrassed", "尴尬");put("depressed", "沮丧");put("sports-commentary", "体育解说");put("sports-commentary-excited", "体育解说-兴奋");put("documentary-narration", "纪录片-旁白");put("narration-professional", "旁白 - 专业");put("newscast-casual", "新闻 - 休闲");put("livecommercial", "实时广告");put("envious", "羡慕");put("empathetic", "同理心");put("story", "故事");put("advertisement-upbeat", "广告-欢快");}};
}

3.工具类

package com.ruoyi.image.utils;import cn.hutool.core.io.FileUtil;
import com.google.gson.Gson;
import com.microsoft.cognitiveservices.speech.*;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.utils.uuid.UUID;
import org.bouncycastle.tsp.TSPUtil;
import org.springframework.stereotype.Service;import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.stream.Collectors;@Service
public class AzureTtsUtil {//加微信号 JIkyDy 试用Azure云Tts服务,或者搜索 创云,全云 联系Azure云中国代理private static final String speechSubscriptionKey = "4BJpOHyvlhiN";private static final String endpointUrl = "https://germanywestcentral.api.cognitive.microsoft.com/";/*** 测试合成语音* @param text*/public static void tts(String text){try (SpeechConfig config = SpeechConfig.fromEndpoint(new java.net.URI(endpointUrl), speechSubscriptionKey)) {// Set the voice name, refer to https://aka.ms/speech/voices/neural for full// list.//config.setSpeechSynthesisVoiceName("zh-CN-XiaoxiaoMultilingualNeural");config.setSpeechSynthesisVoiceName("zh-CN-YunjianNeural");config.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Audio16Khz128KBitRateMonoMp3);try (SpeechSynthesizer synth = new SpeechSynthesizer(config)) {assert (config != null);assert (synth != null);Future<SpeechSynthesisResult> task = synth.SpeakTextAsync(text);assert (task != null);SpeechSynthesisResult result = task.get();assert (result != null);if (result.getReason() == ResultReason.SynthesizingAudioCompleted) {System.out.println("Speech synthesized to speaker for text [" + text + "]");byte[] audioData = result.getAudioData();File file = new File("F:\\微软语音合成\\test.mp3");// 使用 Hutool 的 FileUtil.writeBytes 方法将 byte[] 写入文件FileUtil.writeBytes(audioData, file);} else if (result.getReason() == ResultReason.Canceled) {SpeechSynthesisCancellationDetails cancellation = SpeechSynthesisCancellationDetails.fromResult(result);System.out.println("CANCELED: Reason=" + cancellation.getReason());if (cancellation.getReason() == CancellationReason.Error) {System.out.println("CANCELED: ErrorCode=" + cancellation.getErrorCode());System.out.println("CANCELED: ErrorDetails=" + cancellation.getErrorDetails());System.out.println("CANCELED: Did you update the subscription info?");}}}} catch (Exception ex) {System.out.println("Unexpected exception: " + ex.getMessage());}}/*** 获得朗读人列表*/public static Map<String,Object> getVoices(){try {//三个儿童数字人 晓双 晓悠 云夏List<String> list1 = Arrays.asList("zh-CN-XiaoshuangNeural", "zh-CN-XiaoyouNeural", "zh-CN-YunxiaNeural");//有些角色的localName是英文,在页面上显示效果不好,需要过滤掉 比如:Yunxiao MultilingualList<String> list = Arrays.asList("Yunxiao Multilingual");SpeechConfig speechConfig = SpeechConfig.fromEndpoint(new URI(endpointUrl), speechSubscriptionKey);SpeechSynthesizer synthesizer = new SpeechSynthesizer(speechConfig);SynthesisVoicesResult voicesResult = synthesizer.getVoicesAsync("zh-CN").get();Map<String,Object> rmap=new HashMap<>();List<VoiceInfo> voices = voicesResult.getVoices();//儿童List<VoiceInfo> collect = voices.stream().filter(i -> !list.contains(i.getLocalName())).filter(i -> list1.contains(i.getShortName())).collect(Collectors.toList());List<Map<String,Object>> childList=new ArrayList<>();for(VoiceInfo info:collect){Map<String,Object> infoMap=new HashMap<>();infoMap.put("locale",info.getLocale());infoMap.put("shortName",info.getShortName());infoMap.put("gender",info.getGender().name());infoMap.put("localName",info.getLocalName());List<Map<String,String>> list12=new ArrayList<>();List<String> styleList = info.getStyleList();for(String style:styleList){Map<String,String> styleMap=new HashMap<>();styleMap.put("style",style);styleMap.put("styleName",AzureTTSStyleTranslator.STYLE_TRANSLATIONS.get(style));list12.add(styleMap);}infoMap.put("styleList",list12);childList.add(infoMap);}rmap.put("child",childList);//男人List<VoiceInfo> collect1 = voices.stream().filter(i -> !list.contains(i.getLocalName())).filter(i -> !list1.contains(i.getShortName()) && StringUtils.equals(i.getGender().name(),"Male")).collect(Collectors.toList());List<Map<String,Object>> maleList=new ArrayList<>();for(VoiceInfo info:collect1){Map<String,Object> infoMap=new HashMap<>();infoMap.put("locale",info.getLocale());infoMap.put("shortName",info.getShortName());infoMap.put("gender",info.getGender().name());infoMap.put("localName",info.getLocalName());List<Map<String,String>> list12=new ArrayList<>();List<String> styleList = info.getStyleList();for(String style:styleList){Map<String,String> styleMap=new HashMap<>();styleMap.put("style",style);styleMap.put("styleName",AzureTTSStyleTranslator.STYLE_TRANSLATIONS.get(style));list12.add(styleMap);}infoMap.put("styleList",list12);maleList.add(infoMap);}rmap.put("male",maleList);//女人List<VoiceInfo> collect2 = voices.stream().filter(i -> !list.contains(i.getLocalName())).filter(i -> !list1.contains(i.getShortName()) && StringUtils.equals(i.getGender().name(),"Female")).collect(Collectors.toList());List<Map<String,Object>> femaleList=new ArrayList<>();for(VoiceInfo info:collect2){Map<String,Object> infoMap=new HashMap<>();infoMap.put("locale",info.getLocale());infoMap.put("shortName",info.getShortName());infoMap.put("gender",info.getGender().name());infoMap.put("localName",info.getLocalName());List<Map<String,String>> list12=new ArrayList<>();List<String> styleList = info.getStyleList();for(String style:styleList){Map<String,String> styleMap=new HashMap<>();styleMap.put("style",style);styleMap.put("styleName",AzureTTSStyleTranslator.STYLE_TRANSLATIONS.get(style));list12.add(styleMap);}infoMap.put("styleList",list12);femaleList.add(infoMap);}rmap.put("female",femaleList);/* Gson gson= new Gson();String json = gson.toJson(rmap);System.out.println(json);*/return rmap;} catch (URISyntaxException e) {throw new RuntimeException(e);} catch (ExecutionException e) {throw new RuntimeException(e);} catch (InterruptedException e) {throw new RuntimeException(e);}}/*** 对制定文本,指定朗读人,指定风格进行朗读* @param text* @param voice* @param style*/public static File ttsVoiceStyle(String text,String voice,String style){try (SpeechConfig config = SpeechConfig.fromEndpoint(new java.net.URI(endpointUrl), speechSubscriptionKey)) {// Set the voice name, refer to https://aka.ms/speech/voices/neural for full// list.//config.setSpeechSynthesisVoiceName("zh-CN-XiaoxiaoMultilingualNeural");//config.setSpeechSynthesisVoiceName("zh-CN-YunjianNeural");config.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Audio16Khz128KBitRateMonoMp3);try (SpeechSynthesizer synth = new SpeechSynthesizer(config)) {assert (config != null);assert (synth != null);// styledegree:强度 (0.01-2)/*** rate:语速 ("x-slow"~"x-fast" 或百分比)  slow:慢 ,不支持百分比,但支持小数* 值	描述	等效百分比(近似)* x-slow	极慢速	~50%* slow	慢速	~75%* medium	中速(默认值)	100%* fast	快速	~150%* x-fast	极快速	~200%** pitch:音调 ("x-low"~"x-high" 或 +/-n%)** volume:音量 ("silent"~"x-loud" 或 +/-ndB)*/// 4. 使用 SSML 设置语音风格String ssml = "<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xmlns:mstts='https://www.w3.org/2001/mstts' xml:lang='zh-CN'>"+ "<voice name='"+voice+"'>"+ "<mstts:express-as style='"+style+"' styledegree='1.5' >"+  "<prosody rate='0.8'>"+  text+ "</prosody>"+ "</mstts:express-as>"+ "</voice>"+ "</speak>";Future<SpeechSynthesisResult> task = synth.SpeakSsmlAsync(ssml);assert (task != null);SpeechSynthesisResult result = task.get();assert (result != null);if (result.getReason() == ResultReason.SynthesizingAudioCompleted) {System.out.println("Speech synthesized to speaker for text [" + text + "]");byte[] audioData = result.getAudioData();com.ruoyi.common.core.utils.uuid.UUID uuid = UUID.randomUUID();String uuidString = uuid.toString().replace("-", "");//输出文件绝对路径String tempDir = System.getProperty("java.io.tmpdir");if (org.apache.commons.lang3.StringUtils.isNotBlank(tempDir) && !tempDir.endsWith(File.separator)) {tempDir += File.separator;}String out=tempDir+uuidString+".mp3";File file = new File(out);// 使用 Hutool 的 FileUtil.writeBytes 方法将 byte[] 写入文件FileUtil.writeBytes(audioData, file);return file;} else if (result.getReason() == ResultReason.Canceled) {SpeechSynthesisCancellationDetails cancellation = SpeechSynthesisCancellationDetails.fromResult(result);System.out.println("CANCELED: Reason=" + cancellation.getReason());if (cancellation.getReason() == CancellationReason.Error) {System.out.println("CANCELED: ErrorCode=" + cancellation.getErrorCode());System.out.println("CANCELED: ErrorDetails=" + cancellation.getErrorDetails());System.out.println("CANCELED: Did you update the subscription info?");}}}} catch (Exception ex) {System.out.println("Unexpected exception: " + ex.getMessage());}return null;}public static void main(String[] args) {ttsVoiceStyle("沃伦·巴菲特1930年8月30日出生于美国内布拉斯加州奥马哈,是全球投资界传奇人物,被誉为“奥马哈的先知”,也是杰出投资者、企业家和慈善家。他出生于普通中产家庭,父亲是股票经纪人和共和党国会议员。巴菲特从小对数字和商业感兴趣,11岁开始投资生涯,购买了三股城市服务优先股。","zh-CN-YunzeNeural","angry");//getVoices();}
}

4.接口调用工具类

package com.ruoyi.image.apicontroller;import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.common.core.constant.Constants;
import com.ruoyi.common.core.constant.SecurityConstants;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.enums.TempFileModuleEnum;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.utils.file.FileUtils;
import com.ruoyi.common.core.utils.hwcloud.obs.ObsUtil;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.image.apicontroller.req.CopywritingToSpeechByAzureTTSReq;
import com.ruoyi.image.apicontroller.req.MingmenGenerateVideoReq;
import com.ruoyi.image.apicontroller.vo.GenerateAudioVo;
import com.ruoyi.image.utils.AzureTtsUtil;
import com.ruoyi.image.utils.TextKeyGenerator;
import com.ruoyi.image.utils.alitts.AliTtsRes;
import com.ruoyi.image.utils.ffmpeg.FfmpegUtil;
import com.ruoyi.user.api.RemoteTempFileService;
import com.ruoyi.user.api.model.MmTempFile;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;import static com.ruoyi.image.utils.ffmpeg.FfmpegUtil.getAudioDuration;@Slf4j
@RestController
@RequestMapping("/api/azureTts")
public class AzureTtsController {public static final String SPLIT = ":";public static final String AUDIO_KEY_PRE = "MMWZ:MMWZ_IMAGE:GENERATE_VIDEO:";public static final String EXPIRED = "_expired";@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate RemoteTempFileService remoteTempFileService;/*** 获取所有朗读人* @return*/@GetMapping("/getVoices")public AjaxResult getVoices(){Map<String, Object> voices = AzureTtsUtil.getVoices();return AjaxResult.success("获取成功",voices);}/*** 微软朗读* @param* @return*/@PostMapping("/copywritingToSpeechByAzureTTS")public AjaxResult copywritingToSpeechByAzureTTS(@RequestBody CopywritingToSpeechByAzureTTSReq req) {if (StringUtils.isBlank(req.getCopywriting())) {return AjaxResult.error("请写下文本");}try {// redis keyString encrypt = this.encryptCopywriting(req.getCopywriting(), req.getVoice(), req.getEmotion());String key = AUDIO_KEY_PRE + encrypt;// redis里面有  直接取出String value = null;if (stringRedisTemplate.hasKey(key + EXPIRED)) {value = stringRedisTemplate.opsForValue().get(key);}if (StringUtils.isNotBlank(value)) {return AjaxResult.success(JSON.parseObject(value, GenerateAudioVo.class));}// 生成语音Object[] objects = this.textToSpeech(req.getCopywriting(), req.getVoice(), req.getEmotion());if(objects == null || objects.length != 2){return AjaxResult.error("生成失败");}Map<String,Object> map=new HashMap<>();map.put("url",objects[0]);map.put("audioDuration",objects[1]);// 放redis中// 带有过期时间的stringRedisTemplate.opsForValue().set(key + EXPIRED, "1", 1, TimeUnit.DAYS);// 真正存储数据stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(map));//加入临时文件,定期删除MmTempFile mmTempFile = new MmTempFile();mmTempFile.setUrl(String.valueOf(objects[0]));mmTempFile.setFileName(FileUtil.getName(String.valueOf(objects[0])));mmTempFile.setFileType(ObsUtil.FILE_TYPE_AUDIO);mmTempFile.setModule(TempFileModuleEnum.TUWEN_VOICE_LISTENING.getModule());mmTempFile.setModuleDesc(com.ruoyi.common.core.utils.StringUtils.join(Arrays.asList(req.getVoice(), req.getEmotion()),"-"));mmTempFile.setExpireTime(DateUtil.offsetDay(DateUtil.date(), 2));//两天之后删除R<MmTempFile> booleanR = remoteTempFileService.addTempFile(mmTempFile, SecurityConstants.INNER);return AjaxResult.success(map);} catch (Exception e) {return AjaxResult.error("文案转语音失败: " + e.getMessage());}}/*** 冒号拼接 并特殊编码* 用于判断是否相同**/private String encryptCopywriting(String copywriting, String voice, String emotion, String... extension) {StringBuilder content = new StringBuilder(voice + SPLIT + emotion);if (extension != null) {for (String s : extension) {content.append(SPLIT).append(s);}}content.append(SPLIT).append(copywriting);return TextKeyGenerator.generateKey(content.toString());}private Object[] textToSpeech(String copywriting,String voice,String style) throws IOException {// 校验文案是否为空if (StringUtils.isBlank(copywriting)) {throw new ServiceException("文案内容不能为空", Constants.FAIL);}List<String> textList = this.split4000(copywriting);// 保存阿里云返回的音频文件List<File> aliTtsResList = new ArrayList<>();// 1、 调用阿里云 语音合成接口 生成语音文件for (String text : textList) {// TTSFile audioFile = AzureTtsUtil.ttsVoiceStyle(text, voice,style);if (audioFile == null || !audioFile.exists()) {throw new ServiceException("语音生成失败", Constants.FAIL);}aliTtsResList.add(audioFile);}// 把aliTtsResList中的多个音频文件合成一个File audioFile = FfmpegUtil.mergeAudioFiles(aliTtsResList.stream().collect(Collectors.toList()));if (audioFile == null || !audioFile.exists()) {throw new ServiceException("语音生成失败", Constants.FAIL);}/*** 主要限制* 标准限制:** 单次请求最大字符数:10,000 个字符(包括 SSML 标签)** 中文文本限制:大约 3,000-5,000 个汉字(因SSML标签占用部分字符)** 实际可用字符:** 纯中文文本(不含SSML标签)通常可处理约 4,500-5,000 个汉字** 含复杂SSML标签的文本可能只能处理 3,000-4,000 个汉字*///File audioFile = AzureTtsUtil.ttsVoiceStyle(audioVo.getCopywriting(), audioVo.getVoice(), audioVo.getEmotion());// 2. 获取音频时长int audioDuration = getAudioDuration(audioFile);//audioVo.setCopywritingAudioDuration(String.valueOf(audioDuration));// 3. 将语音文件转换为MultipartFileMultipartFile multipartFile = FileUtils.convertToMultipartFile(audioFile);// 4. 上传语音文件到OBSMap<String, String> uploadResult = ObsUtil.upload(multipartFile);if (!uploadResult.containsKey("url")) {throw new ServiceException("语音上传失败", Constants.FAIL);}return new Object[]{uploadResult.get("url"),audioDuration};}/*** 截取4000以内字符* 截取时按照标点符号截,保证句子完整。**/private List<String> split4000(String text) {List<String> textList = new ArrayList<>();if (text.length() >= 4000) {String copywriting = text;// 定义句子结束的标点符号String[] sentenceEndings = {"。", "!", "?", ";", ".", "!", "?", ";", ",", ",", "...", " "};int startIndex = 0;while (startIndex < copywriting.length()) {// 计算当前段的结束位置(不超过300字)int endIndex = Math.min(startIndex + 4000, copywriting.length());// 如果不是最后一段,需要找到合适的句子结束位置if (endIndex < copywriting.length()) {// 从300字位置向前查找最近的句子结束符boolean foundEnding = false;for (int i = endIndex; i > startIndex; i--) {String currentChar = String.valueOf(copywriting.charAt(i - 1));for (String ending : sentenceEndings) {if (ending.equals(currentChar)) {endIndex = i;foundEnding = true;break;}}if (foundEnding) {break;}}// 如果没找到句子结束符,就按300字截取if (!foundEnding) {endIndex = startIndex + 4000;}}// 截取当前段并添加到列表String segment = copywriting.substring(startIndex, endIndex);textList.add(segment);// 更新起始位置startIndex = endIndex;}// 打印分段结果,方便调试log.info("文案分段结果:");for (int i = 0; i < textList.size(); i++) {log.info("第{}段:{}", i + 1, textList.get(i));}} else {textList.add(text);}return textList;}
}

5.如果需要Docker部署,需要构建镜像时加入依赖

#FROM ibm-semeru-runtimes:open-17-jre
#FROM ibm-semeru-runtimes:open-8-jre
FROM openjdk:8-jdkVOLUME /tmp
WORKDIR /opt/appARG JAR_FILE=*.jar
COPY ${JAR_FILE} app.jar
COPY bootstrap.yml bootstrap.yml#RUN mkdir /opt/arthas
#COPY /opt/arthas/arthas-boot.jar /opt/arthas/arthas-boot.jar#安装ffmpeg
RUN mkdir /opt/ffmpeg
COPY ffmpeg/ /opt/ffmpeg/
RUN chmod +x /opt/ffmpeg/bin/*ENV FFMPEG_PATH=/opt/ffmpeg/bin/ffmpeg#复制字体(一键成片字幕字体)
COPY font/SIMKAI.TTF /usr/share/fonts/
RUN chmod 644 /usr/share/fonts/SIMKAI.TTF#微软tts依赖
# 安装必要依赖
RUN apt-get update && \apt-get install -y \libasound2 \libpulse-dev \libssl-dev \ca-certificates && \rm -rf /var/lib/apt/lists/*# 设置环境变量(关键!)
ENV LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu:$LD_LIBRARY_PATHENV JAVA_OPTS=""
EXPOSE 9205
#ENTRYPOINT java ${JAVA_OPTS} --add-opens=java.base/java.lang=ALL-UNNAMED -Djava.security.egd=file:/dev/./urandom -jar /opt/app/app.jar
ENTRYPOINT java ${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -jar /opt/app/app.jar

6.补充工具方法

/*** 合并多个音频文件* @param fileList 音频文件列表* @return 合并后的音频文件*/public static File mergeAudioFiles(List<File> fileList) throws IOException {if (CollectionUtils.isEmpty(fileList)) {return null;}// 如果只有一个文件,直接返回if (fileList.size() == 1) {return fileList.get(0);}// 创建临时文件列表List<String> tempFileList = new ArrayList<>();for (File file : fileList) {tempFileList.add(file.getAbsolutePath());}// 创建合并后的临时文件String mergedAudioPath = fileList.get(0).getAbsolutePath().replace(".mp3", "_merged.mp3");File mergedFile = new File(mergedAudioPath);try {// 构建FFmpeg命令List<String> command = new ArrayList<>();command.add(ffmpegPath);command.add("-y"); // 覆盖已存在的文件command.add("-i");command.add("concat:" + String.join("|", tempFileList));command.add("-c:a");command.add("copy");command.add(mergedAudioPath);// 执行FFmpeg命令ProcessBuilder processBuilder = new ProcessBuilder(command);Process process = processBuilder.start();int exitCode = process.waitFor();if (exitCode != 0) {log.error("音频合并失败,退出码:{}", exitCode);return null;}// 删除原始临时文件for (File file : fileList) {if (file != null && file.exists()) {file.delete();}}return mergedFile;} catch (Exception e) {log.error("音频合并失败", e);// 清理临时文件if (mergedFile.exists()) {mergedFile.delete();}return null;}}

http://www.lryc.cn/news/607824.html

相关文章:

  • Java试题-选择题(4)
  • 防火墙相关技术内容
  • JVM 调优中JVM的参数如何起到调优动作?具体案例,G1GC垃圾收集器参数调整建议
  • JVM学习日记(十四)Day14——性能监控与调优(一)
  • 基于ELK Stack的实时日志分析与智能告警实践指南
  • SpringBoot 信用卡检测、OpenAI gym、OCR结合、DICOM图形处理、知识图谱、农业害虫识别实战
  • JVM 01 运行区域
  • Qwen3 Embedding:新一代文本表征与排序模型
  • Hyper-V + Centos stream 9 搭建K8s集群(一)
  • 手动开发一个TCP客户端调试工具(三):工具界面设计
  • 【人工智能agent】--服务器部署PaddleX 的 印章文本识别模型
  • Design Compiler:Milkyway库的创建与使用
  • 分布式微服务--Nacos作为配置中心(补)关于bosststrap.yml与@RefreshScope
  • 集成电路学习:什么是CMSIS微控制器软件接口标准
  • [创业之路-528]:技术成熟度曲线如何指导创业与投资?
  • UNet改进(28):KD Attention增强UNet的知识蒸馏方法详解
  • 深入解析 <component :is> 在 Vue3 组合式中的使用与局限
  • 【推荐100个unity插件】快速实现汽车控制器——PROMETEO: Car Controller插件
  • 除数博弈(动态规划)
  • [硬件电路-124]:模拟电路 - 信号处理电路 - 测量系统的前端电路详解
  • python匿名函数lambda
  • 【LeetCode刷题指南】--二叉树的前序遍历,二叉树的中序遍历
  • 2025熵密杯 -- 初始谜题 -- Reproducibility
  • 进阶向:自动化天气查询工具(API调用)
  • stm32是如何实现电源控制的?
  • 【7.5 Unity AssetPostprocessor】
  • 2-5 Dify案例实践—利用RAG技术构建企业私有知识库
  • 【最新区块链论文录用资讯】CCF A--WWW 2025 23篇
  • 第三章 用户和权限
  • 【C++】第二十一节—一文详解 | 红黑树实现(规则+效率+结构+插入+查找+验证)