opt:语音控制功能-第一次联调测试修改

This commit is contained in:
zhaoyf
2026-04-22 10:47:24 +08:00
parent 5d83cceca5
commit bced456945
8 changed files with 178 additions and 130 deletions

View File

@@ -35,6 +35,9 @@
<if test="pcsn != null and pcsn != ''"> <if test="pcsn != null and pcsn != ''">
AND gro.pcsn = #{pcsn} AND gro.pcsn = #{pcsn}
</if> </if>
<if test="struct_code != null and struct_code != ''">
AND ivt.struct_code = #{struct_code}
</if>
<if test="stor_code != null and stor_code != ''"> <if test="stor_code != null and stor_code != ''">
AND ivt.stor_code = #{stor_code} AND ivt.stor_code = #{stor_code}
</if> </if>

View File

@@ -16,7 +16,7 @@ import java.util.Map;
*/ */
@Slf4j @Slf4j
@RestController @RestController
@RequestMapping("/speech") @RequestMapping("/api/speech")
public class SysSpeechController { public class SysSpeechController {
@Resource @Resource
@@ -56,7 +56,7 @@ public class SysSpeechController {
*/ */
@PostMapping("/confirm") @PostMapping("/confirm")
public ResponseEntity<Object> confirm(@RequestBody Map<String, Object> body){ public ResponseEntity<Object> confirm(@RequestBody Map<String, Object> body){
sysSpeechService.confirm(body.getOrDefault("text","").toString()); Map<String, Object> res = sysSpeechService.confirm(body.getOrDefault("text", "").toString());
return new ResponseEntity<>(HttpStatus.OK); return ResponseEntity.ok(res);
} }
} }

View File

@@ -56,46 +56,42 @@ public class SysSpeechWebSocketEndpoint {
if ("start".equals(type)) { if ("start".equals(type)) {
//TODO 健康检查 //TODO 健康检查
asrTtsClient.health().thenAccept(result -> { asrTtsClient.health().thenAccept(result -> {
log.info("健康检查结果: {}", result);
Integer resCode = JSON.parseObject(result.toString()).getInteger("code"); Integer resCode = JSON.parseObject(result.toString()).getInteger("code");
if (resCode != 0) { if (resCode != 0) {
sendError("语音控制服务异常"); sendTextBySys("语音控制服务异常");
} }
}).exceptionally(ex -> { }).exceptionally(ex -> {
log.error("健康检查异常: {}", session.getId(), ex); log.error("健康检查异常: {}", session.getId(), ex);
sendError("语音控制服务异常"); sendTextBySys("语音控制服务异常");
return null; return null;
}); });
} else if ("end".equals(type)) { } else if ("end".equals(type)) {
// 录音结束,将 PCM 数据发送给第三方 ASR // 录音结束,将 PCM 数据发送给第三方 ASR
byte[] pcmData = ctx.audioBuffer.toByteArray(); byte[] pcmData = ctx.audioBuffer.toByteArray();
//测试
// String resp = JSONObject.toJSONString(MapOf.of("type", "transcription", "text", "识别结果"));
// String resp1 = JSONObject.toJSONString(MapOf.of("type", "response", "text", "系统回复消息"));
// session.getBasicRemote().sendText(resp);
// session.getBasicRemote().sendText(resp1);
//测试结束
// 异步调用第三方服务,避免阻塞 WebSocket 线程 // 异步调用第三方服务,避免阻塞 WebSocket 线程
asrTtsClient.recognizeAsync(pcmData, ctx.sampleRate, ctx.channels, ctx.bitDepth) asrTtsClient.recognizeAsync(pcmData, ctx.sampleRate, ctx.channels, ctx.bitDepth)
.thenAccept(result -> { .thenAccept(result -> {
JSONObject jsonObject = JSON.parseObject(result.toString(), JSONObject.class); JSONObject jsonObject = JSON.parseObject(result.toString(), JSONObject.class);
log.info("ASR 输出参数为:-------------------" + jsonObject.toString());
Integer code = jsonObject.getInteger("code"); Integer code = jsonObject.getInteger("code");
if (code == null || code != 0) { if (code == null || code != 0) {
String msg = jsonObject.getString("msg"); String msg = jsonObject.getString("msg");
sendError(msg != null ? msg : "未知错误"); sendTextBySys(msg != null ? msg : "未知错误");
return; return;
} }
JSONObject data = jsonObject.getJSONObject("data"); JSONObject data = jsonObject.getJSONObject("data");
if (data != null) { if (data != null) {
String text = data.getString("text"); String text = data.getString("text");
sendText(text); sendTextByUser(text);
} else { } else {
sendError("语音转文字发生错误"); sendTextBySys("语音转文字发生错误");
} }
}) })
.exceptionally(ex -> { .exceptionally(ex -> {
log.error("ASR 异常: {}", session.getId(), ex); log.error("ASR 异常: {}", session.getId(), ex);
sendError("ASR 异常"); sendTextByUser("ASR 异常");
return null; return null;
}); });
ctx.audioBuffer.reset(); ctx.audioBuffer.reset();
@@ -144,11 +140,11 @@ public class SysSpeechWebSocketEndpoint {
} }
} }
private void sendText(String message) { private void sendTextByUser(String message) {
try { try {
Map<String, Object> map = new HashMap<>(); Map<String, Object> map = new HashMap<>();
map.put("type", "result"); map.put("type", "user");
map.put("text", message); map.put("message", message);
// 结果通过 WebSocket 返回给前端 // 结果通过 WebSocket 返回给前端
String response = objectMapper.writeValueAsString(map); String response = objectMapper.writeValueAsString(map);
session.getBasicRemote().sendText(response); session.getBasicRemote().sendText(response);
@@ -157,10 +153,10 @@ public class SysSpeechWebSocketEndpoint {
} }
} }
private void sendError(String message) { private void sendTextBySys(String message) {
try { try {
Map<String, String> map = new HashMap<>(); Map<String, String> map = new HashMap<>();
map.put("type", "error"); map.put("type", "system");
map.put("message", message); map.put("message", message);
session.getBasicRemote().sendText( session.getBasicRemote().sendText(
objectMapper.writeValueAsString(map)); objectMapper.writeValueAsString(map));

View File

@@ -85,8 +85,10 @@ public class VoiceCallbackService {
@Transactional @Transactional
public JSONObject handleInboundTask(InboundTaskRequest inboundTaskRequest){ public JSONObject handleInboundTask(InboundTaskRequest inboundTaskRequest){
// SchBasePoint one = iSchBasePointService.getOne(new LambdaQueryWrapper<SchBasePoint>()
// .eq(SchBasePoint::getPoint_code, inboundTaskRequest.getLocationFrom()));
SchBasePoint one = iSchBasePointService.getOne(new LambdaQueryWrapper<SchBasePoint>() SchBasePoint one = iSchBasePointService.getOne(new LambdaQueryWrapper<SchBasePoint>()
.eq(SchBasePoint::getPoint_code, inboundTaskRequest.getLocationFrom())); .eq(SchBasePoint::getPoint_code, "RKD01"));
//分配仓位 //分配仓位
MdMeMaterialbase meMaterialbase = iMdMeMaterialbaseService.getByCode(inboundTaskRequest.getMaterial()); MdMeMaterialbase meMaterialbase = iMdMeMaterialbaseService.getByCode(inboundTaskRequest.getMaterial());
@@ -114,8 +116,8 @@ public class VoiceCallbackService {
List<Structattr> structattrs = iStructattrService.inBoundSectDiv( List<Structattr> structattrs = iStructattrService.inBoundSectDiv(
StrategyStructParam.builder() StrategyStructParam.builder()
.ioType(StatusEnum.STRATEGY_TYPE.code("入库")) .ioType(StatusEnum.STRATEGY_TYPE.code("入库"))
.sect_code("BG01") .sect_code("YL01")
.stor_code("BGK") .stor_code("YL")
.storagevehicle_code(one.getVehicle_code()) .storagevehicle_code(one.getVehicle_code())
.strategyMaters(materss) .strategyMaters(materss)
.build()); .build());
@@ -124,23 +126,28 @@ public class VoiceCallbackService {
} }
Structattr attrDao = structattrs.get(0); Structattr attrDao = structattrs.get(0);
//确定起点 //确定起点
SchBasePoint schBasePoint = iSchBasePointService.getOne(new LambdaQueryWrapper<SchBasePoint>() // SchBasePoint schBasePoint = iSchBasePointService.getOne(new LambdaQueryWrapper<SchBasePoint>()
.eq(SchBasePoint::getPoint_code, inboundTaskRequest.getLocationFrom())); // .eq(SchBasePoint::getPoint_code, inboundTaskRequest.getLocationFrom()));
if (ObjectUtil.isEmpty(schBasePoint)) { // if (ObjectUtil.isEmpty(schBasePoint)) {
throw new BadRequestException("未找到载具所在的点位信息,请检查"); // throw new BadRequestException("未找到载具所在的点位信息,请检查");
} // }
JSONObject whereJson = new JSONObject(); JSONObject whereJson = new JSONObject();
whereJson.put("point_code1", schBasePoint.getPoint_code()); // whereJson.put("point_code1", "RKD01");
// whereJson.put("point_code1", schBasePoint.getPoint_code());
//确定终点 //确定终点
whereJson.put("point_code2", attrDao.getStruct_code()); // whereJson.put("point_code2", attrDao.getStruct_code());
whereJson.put("vehicle_code", one.getVehicle_code()); whereJson.put("vehicle_code", one.getVehicle_code());
whereJson.put("task_type", "STInTask");
whereJson.put("TaskCode", CodeUtil.getNewCode("TASK_CODE"));
whereJson.put("PickingLocation", "RKD01");
whereJson.put("PlacedLocation", attrDao.getStruct_code());
//创建任务 //创建任务
String taskId = applyTaskMap.get("VehicleInTask").create(whereJson); String taskId = applyTaskMap.get("STInTask").create(whereJson);
// 更新起点绑定id // 更新起点绑定id
iSchBasePointService.update( iSchBasePointService.update(
new UpdateWrapper<SchBasePoint>().lambda() new UpdateWrapper<SchBasePoint>().lambda()
.set(SchBasePoint::getIos_id, null) .set(SchBasePoint::getIos_id, null)
.eq(SchBasePoint::getPoint_code, schBasePoint.getPoint_code()) .eq(SchBasePoint::getPoint_code, "RKD01")
); );
// 更新终点锁定状态 // 更新终点锁定状态
JSONObject lock_map = new JSONObject(); JSONObject lock_map = new JSONObject();
@@ -159,8 +166,8 @@ public class VoiceCallbackService {
JSONObject whereJson = new JSONObject(); JSONObject whereJson = new JSONObject();
whereJson.put("material_id", meMaterialbase.getMaterial_id()); whereJson.put("material_id", meMaterialbase.getMaterial_id());
whereJson.put("material_code", meMaterialbase.getMaterial_code()); whereJson.put("material_code", meMaterialbase.getMaterial_code());
whereJson.put("stor_code", "BGK"); whereJson.put("stor_code", "YL");
whereJson.put("sect_code", "BG01"); whereJson.put("sect_code", "YL01");
whereJson.put("siteCode", "CKD01"); whereJson.put("siteCode", "CKD01");
StrategyMater mater = new StrategyMater(); StrategyMater mater = new StrategyMater();
mater.setQty(BigDecimal.valueOf(outboundTaskRequest.getQty())); mater.setQty(BigDecimal.valueOf(outboundTaskRequest.getQty()));
@@ -214,19 +221,33 @@ public class VoiceCallbackService {
return null; return null;
}; };
public JSONObject handleMoveTask(MoveTaskRequest moveTaskRequest){ public JSONObject handleMoveTask(MoveTaskRequest moveTaskRequest){
String taskId = applyTaskMap.get("StructMoveTask").create((JSONObject) JSONObject.toJSON(moveTaskRequest)); Structattr attrDao = iStructattrService.findByCode(moveTaskRequest.getLocationFrom());
JSONObject taskForm = new JSONObject();
taskForm.put("point_code1", moveTaskRequest.getLocationFrom());
taskForm.put("point_code", moveTaskRequest.getLocationTo());
taskForm.put("config_code", IOSConstant.MOVE_CONFIG_TASK);
taskForm.put("vehicle_code", attrDao.getStoragevehicle_code());
String taskId = applyTaskMap.get("MoveTask").create((JSONObject) JSONObject.toJSON(taskForm));
return null; return null;
}; };
public JSONObject handleQueryTask(QueryBoundRequest queryBoundRequest){ public JSONObject handleQueryTask(QueryBoundRequest queryBoundRequest){
List<StructattrVechielDto> structCode = iStructattrService.collectVechicle(MapOf.of("struct_code", queryBoundRequest.getLocation(), "search", queryBoundRequest.getMaterial())); List<StructattrVechielDto> structCode = iStructattrService.collectVechicle(MapOf.of("struct_code", queryBoundRequest.getLocation(), "search", queryBoundRequest.getMaterial()));
Map<String, BigDecimal> collect = structCode.stream()
.collect(Collectors.groupingBy(StructattrVechielDto::getMaterial_code,
Collectors.reducing(BigDecimal.ZERO, StructattrVechielDto::getQty, BigDecimal::add)));
StringBuffer stringBuffer = new StringBuffer(); StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("当前仓库物料有物料"); stringBuffer.append("当前仓库物料有物料");
for (StructattrVechielDto vechielDtos : structCode) { collect.entrySet().forEach(r -> {
stringBuffer.append(vechielDtos.getMaterial_name()).append("共计") stringBuffer.append(r.getKey()).append("共计")
.append(vechielDtos.getQty()) .append(r.getValue())
.append(""); .append("");
} });
// for (StructattrVechielDto vechielDtos : structCode) {
// stringBuffer.append(vechielDtos.getMaterial_name()).append("共计")
// .append(vechielDtos.getQty())
// .append("个");
// }
String text = stringBuffer.toString(); String text = stringBuffer.toString();
try { try {
// voiceService.speechTts(text); // voiceService.speechTts(text);

View File

@@ -18,6 +18,7 @@ public class AsrTtsClient {
@Value("${vc.base-url:http://127.0.0.1:5000}") @Value("${vc.base-url:http://127.0.0.1:5000}")
private String vcBaseUrl; private String vcBaseUrl;
/** /**
* 健康检查 * 健康检查
*/ */
@@ -29,12 +30,14 @@ public class AsrTtsClient {
}); });
} }
/** /**
* 语音转文字 * 语音转文字
*/ */
public static void asr() { public static void asr() {
} }
/** /**
* 文字转语音 * 文字转语音
*/ */
@@ -55,11 +58,11 @@ public class AsrTtsClient {
}); });
} }
/** /**
* 用户确认文字 * 用户确认文字
*/ */
public CompletableFuture<Object> confirm(String text, boolean confirmed) { public String confirm(String text, boolean confirmed) {
return CompletableFuture.supplyAsync(() -> {
Map<String, Object> params = new HashMap<>(); Map<String, Object> params = new HashMap<>();
params.put("text", text); params.put("text", text);
params.put("confirmed", confirmed); params.put("confirmed", confirmed);
@@ -68,7 +71,6 @@ public class AsrTtsClient {
.contentType("application/json") .contentType("application/json")
.execute() .execute()
.body(); .body();
});
} }
@@ -76,7 +78,8 @@ public class AsrTtsClient {
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.supplyAsync(() -> {
return HttpRequest.post(vcBaseUrl + "/api/speech/asr") return HttpRequest.post(vcBaseUrl + "/api/speech/asr")
.form("audio", pcmData) .form("audio", pcmData, "audio.pcm")
// .body(pcmData)
.contentType("multipart/form-data") .contentType("multipart/form-data")
.execute() .execute()
.body(); .body();

View File

@@ -1,5 +1,6 @@
package org.nl.wms.system_manage.service.speech.impl; package org.nl.wms.system_manage.service.speech.impl;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.nl.common.utils.MapOf; import org.nl.common.utils.MapOf;
import org.nl.wms.system_manage.service.speech.SysSpeechService; import org.nl.wms.system_manage.service.speech.SysSpeechService;
@@ -39,18 +40,20 @@ public class SysSpeechServiceImpl implements SysSpeechService {
@Override @Override
public Map<String, Object> confirm(String text) { public Map<String, Object> confirm(String text) {
if ("".equals( text)){ if ("".equals(text)) {
throw new RuntimeException("确认文本为空"); throw new RuntimeException("确认文本为空");
} }
asrTtsClient.confirm(text, true) String resJson = asrTtsClient.confirm(text, true);
.thenAccept(result -> { log.info("语音控制:用户确认结果: {}", resJson);
log.info("语音控制:用户确认结果: {}", result); //TODO
}).exceptionally(ex -> { JSONObject jsonObject = JSONObject.parseObject(resJson);
log.error("语音控制:用户确认异常: {}", ex); JSONObject data = jsonObject.getJSONObject("data");
return null; if (data != null) {
}); String resText = data.getString("text");
return MapOf.of("text", resText);
}
byte[] audio = asrTtsClient.tts(text).join(); byte[] audio = asrTtsClient.tts(text).join();
//TODO 音频返回前端播放处理 //TODO 音频返回前端播放处理
return Collections.emptyMap(); return MapOf.of("text", text);
} }
} }

View File

@@ -9,10 +9,11 @@ spring:
druid: druid:
db-type: com.alibaba.druid.pool.DruidDataSource db-type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${DB_HOST:192.168.81.251}:${DB_PORT:3306}/${DB_NAME:wms_standardv2}?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false&useOldAliasMetadataBehavior=true&allowPublicKeyRetrieval=true&useSSL=false # url: jdbc:mysql://${DB_HOST:192.168.81.251}:${DB_PORT:3306}/${DB_NAME:wms_standardv2}?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false&useOldAliasMetadataBehavior=true&allowPublicKeyRetrieval=true&useSSL=false
# url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:wms_oulun}?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false&useOldAliasMetadataBehavior=true&allowPublicKeyRetrieval=true&useSSL=false url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:wms_standardv2}?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false&useOldAliasMetadataBehavior=true&allowPublicKeyRetrieval=true&useSSL=false
username: ${DB_USER:root} username: ${DB_USER:root}
password: ${DB_PWD:P@ssw0rd.} password: ${DB_PWD:123456}
# password: ${DB_PWD:P@ssw0rd.}
# 初始连接数 # 初始连接数
initial-size: 15 initial-size: 15
# 最小连接数 # 最小连接数
@@ -137,3 +138,5 @@ sa-token:
lucene: lucene:
index: index:
path: D:\lms\lucene\index path: D:\lms\lucene\index
vc:
base-url: http://192.168.20.156:5000

View File

@@ -133,10 +133,18 @@
<span>正在录音... {{ recordingDuration }}"</span> <span>正在录音... {{ recordingDuration }}"</span>
</div> </div>
<!-- 识别中提示 -->
<div v-if="isTranscribing" class="transcribing-hint">
<div class="transcribing-spinner">
<i class="el-icon-loading" />
</div>
<span>正在识别录音内容...</span>
</div>
<!-- 录音按钮 --> <!-- 录音按钮 -->
<div <div
class="record-button" class="record-button"
:class="{ 'recording': isRecording, 'disabled': !isConnected || isProcessing }" :class="{ 'recording': isRecording, 'disabled': !isConnected || isProcessing || isTranscribing }"
@mousedown="startRecording" @mousedown="startRecording"
@mouseup="stopRecording" @mouseup="stopRecording"
@mouseleave="isRecording && stopRecording()" @mouseleave="isRecording && stopRecording()"
@@ -240,7 +248,7 @@ export default {
this.isConnected = false this.isConnected = false
}, },
// 处理WebSocket消息 // 处理WebSocket消息 - 只处理user和system两种类型
handleWebSocketMessage(data) { handleWebSocketMessage(data) {
// 判断数据类型 // 判断数据类型
if (data instanceof ArrayBuffer || data instanceof Blob) { if (data instanceof ArrayBuffer || data instanceof Blob) {
@@ -252,14 +260,21 @@ export default {
const message = JSON.parse(data) const message = JSON.parse(data)
console.log('收到WebSocket消息:', message) console.log('收到WebSocket消息:', message)
if (message.type === 'error') { // 只处理user和system两种类型
this.handleError(message.message || '识别失败') if (message.type === 'user') {
} else if (message.type === 'transcription') { // 用户识别结果消息
// 语音识别结果 this.handleUserMessage(message)
this.handleTranscriptionResult(message) } else if (message.type === 'system') {
} else if (message.type === 'response') { // 系统回复消息
// 系统文本回复 this.handleSystemMessage(message)
this.handleSystemTextResponse(message) } else if (message.type === 'error') {
// 错误消息作为系统消息显示
this.addMessage({
role: 'system',
type: 'text',
content: message.message || '处理失败',
timestamp: Date.now()
})
} }
} catch (e) { } catch (e) {
console.error('解析WebSocket消息失败:', e) console.error('解析WebSocket消息失败:', e)
@@ -267,9 +282,9 @@ export default {
} }
}, },
// 处理语音识别结果 // 处理用户消息(语音识别结果
handleTranscriptionResult(message) { handleUserMessage(message) {
const text = message.text || message.transcript || '' const text = message.message
if (!text) return if (!text) return
this.isTranscribing = false this.isTranscribing = false
@@ -284,6 +299,19 @@ export default {
}) })
}, },
// 处理系统消息
handleSystemMessage(message) {
this.isProcessing = false
// 添加系统消息
this.addMessage({
role: 'system',
type: 'text',
content: message.message,
timestamp: Date.now()
})
},
// 确认消息 - 用户点击确认按钮后执行 // 确认消息 - 用户点击确认按钮后执行
async confirmMessage(message) { async confirmMessage(message) {
if (!message || !message.content) return if (!message || !message.content) return
@@ -294,14 +322,16 @@ export default {
// 执行confirm函数 // 执行confirm函数
try { try {
const res = await confirm(message.content) const res = await confirm({
text: message.content
})
this.isProcessing = false this.isProcessing = false
// 添加系统回复消息 // 添加系统回复消息
this.addMessage({ this.addMessage({
role: 'system', role: 'system',
type: 'text', type: 'text',
content: res.data || '收到您的消息', content: res.text,
timestamp: Date.now() timestamp: Date.now()
}) })
} catch (error) { } catch (error) {
@@ -328,18 +358,7 @@ export default {
this.scrollToBottom() this.scrollToBottom()
}, },
// 处理系统文本回复 // 处理系统语音回复(二进制数据)
handleSystemTextResponse(message) {
this.isProcessing = false
this.addMessage({
role: 'system',
type: 'text',
content: message.text || message.content || '收到您的消息',
timestamp: Date.now()
})
},
// 处理系统语音回复
async handleSystemVoiceResponse(audioData) { async handleSystemVoiceResponse(audioData) {
try { try {
// 创建音频URL // 创建音频URL
@@ -360,7 +379,7 @@ export default {
role: 'system', role: 'system',
type: 'voice', type: 'voice',
audioUrl: audioUrl, audioUrl: audioUrl,
duration: '3"', // 这里应该根据实际音频时长计算 duration: '3"',
timestamp: Date.now() timestamp: Date.now()
} }
this.addMessage(message) this.addMessage(message)
@@ -372,27 +391,7 @@ export default {
} }
}, },
// 处理错误
handleError(message) {
this.isProcessing = false
this.isTranscribing = false
this.addMessage({
role: 'system',
type: 'text',
content: message || '服务出现错误',
timestamp: Date.now()
})
// 更新最后一条用户消息状态
// const lastUserMessage = this.messages.filter(m => m.role === 'user').pop()
// if (lastUserMessage) {
// lastUserMessage.isTranscribing = false
// lastUserMessage.content = message
// lastUserMessage.type = 'text'
// }
//
// this.$message.error(message)
},
// 添加消息 // 添加消息
addMessage(message) { addMessage(message) {
@@ -1030,6 +1029,26 @@ export default {
} }
} }
.transcribing-hint {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10px;
color: #409EFF;
font-size: 14px;
.transcribing-spinner {
display: flex;
align-items: center;
margin-right: 8px;
i {
font-size: 18px;
animation: rotating 1s linear infinite;
}
}
}
.record-button { .record-button {
display: flex; display: flex;
align-items: center; align-items: center;