perf: 新增录音转文字

This commit is contained in:
kuaifan 2025-03-04 23:16:10 +08:00
parent 4fa54381a6
commit 7b1d352c95
4 changed files with 281 additions and 7 deletions

View File

@ -1348,6 +1348,55 @@ class DialogController extends AbstractController
}
}
/**
* @api {post} api/dialog/msg/convertrecord 25. 录音转文字
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__convertrecord
*
* @apiParam {Number} dialog_id 对话ID
* @apiParam {String} base64 语音base64
* @apiParam {Number} duration 语音时长(毫秒)
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__convertrecord()
{
$user = User::auth();
$user->checkChatInformation();
//
$dialog_id = intval(Request::input('dialog_id'));
//
WebSocketDialog::checkDialog($dialog_id);
//
$path = "uploads/tmp/chat/" . date("Ym") . "/" . $dialog_id . "/";
$base64 = Request::input('base64');
$duration = intval(Request::input('duration'));
if ($duration < 600) {
return Base::retError('说话时间太短');
}
$data = Base::record64save([
"base64" => $base64,
"path" => $path,
]);
if (Base::isError($data)) {
return Base::retError($data['msg']);
}
$recordData = $data['data'];
$res = Extranet::openAItranscriptions($recordData['file']);
if (Base::isError($res)) {
return $res;
}
if (strlen($res['data']) < 1) {
return Base::retError('转文字失败');
}
return $res;
}
/**
* @api {post} api/dialog/msg/sendfile 26. 文件上传
*

View File

@ -188,15 +188,53 @@
v-transfer-dom
:data-transfer="true"
class="chat-input-record-transfer"
:class="{cancel: touchLimitY}"
:class="recordClassName"
:style="recordTransferStyle"
@click="stopRecord">
<div v-if="recordDuration > 0" class="record-duration">{{recordFormatDuration}}</div>
<div v-else class="record-loading"><Loading type="pure"/></div>
<div class="record-cancel" @click.stop="stopRecord(true)">{{$L(touchLimitY ? '松开取消' : '向上滑动取消')}}</div>
<div class="record-cancel" @click.stop="stopRecord(true)">{{$L(recordFormatTip)}}</div>
</div>
</transition>
<!-- 录音转文字 -->
<transition name="fade">
<div
v-if="recordConvertIng"
v-transfer-dom
:data-transfer="true"
class="chat-input-convert-transfer"
:style="recordTransferStyle">
<div class="convert-box">
<div class="convert-content">
<Input
type="textarea"
class="convert-result"
v-model="recordConvertResult"
:rows="1"
:autosize="{minRows: 1, maxRows: 5}"
:placeholder="recordConvertStatus === 0 ? '...' : ''"/>
</div>
<ul class="convert-footer">
<li @click="recordConvertIng=false">
<i class="taskfont">&#xe637;</i>
<span>{{$L('取消')}}</span>
</li>
<li @click="convertSend('voice')">
<i class="taskfont voice">&#xe793;</i>
<span>{{$L('发送原语音')}}</span>
</li>
<li @click="convertSend('result')">
<i v-if="recordConvertStatus === 0" class="send"><Loading/></i>
<i v-else-if="recordConvertStatus === 2" class="taskfont error">&#xe665;</i>
<i v-else class="taskfont send">&#xe684;</i>
</li>
</ul>
</div>
</div>
</transition>
<Modal
v-model="fullInput"
:mask-closable="false"
@ -335,6 +373,9 @@ export default {
recordInter: null,
recordState: "stop",
recordDuration: 0,
recordConvertIng: false,
recordConvertStatus: 0, // 0: 1: 2:
recordConvertResult: '',
touchStart: {},
touchFocus: false,
@ -453,6 +494,9 @@ export default {
if (this.recordRec) {
this.recordRec = null
}
if (this.recordConvertIng) {
this.recordConvertIng = false
}
if (this.recordInter) {
clearInterval(this.recordInter)
}
@ -563,6 +607,24 @@ export default {
return `${minute}:${seconds}${millisecond}`
},
recordClassName({touchLimitX, touchLimitY}) {
if (touchLimitY) {
return 'cancel'
} else if (touchLimitX) {
return 'convert'
}
return ''
},
recordFormatTip({touchLimitX, touchLimitY}) {
if (touchLimitY) {
return '松开取消'
} else if (touchLimitX) {
return '转文字'
}
return '向上滑动取消'
},
dialogData() {
return this.dialogId > 0 ? (this.cacheDialogs.find(({id}) => id == this.dialogId) || {}) : {};
},
@ -1140,7 +1202,7 @@ export default {
if (this.showMenu) {
return;
}
if (this.stopRecord(this.touchLimitY)) {
if (this.stopRecord(this.touchLimitY, this.touchLimitX)) {
return;
}
if (this.touchLimitY || this.touchLimitX) {
@ -1222,7 +1284,7 @@ export default {
}
},
stopRecord(isCancel) {
stopRecord(isCancel, isConvert = false) {
switch (this.recordState) {
case "ing":
this.recordState = "stop";
@ -1235,7 +1297,12 @@ export default {
$A.messageWarning("说话时间太短") // 600ms
} else {
this.recordBlob = blob;
this.uploadRecord(duration);
this.recordDuration = duration;
if (isConvert === true) {
this.convertRecord();
} else {
this.uploadRecord();
}
}
}, (msg) => {
this.recordRec.close();
@ -1270,7 +1337,54 @@ export default {
})
},
uploadRecord(duration) {
convertRecord() {
if (this.recordBlob === null) {
this.recordConvertIng = false
return;
}
this.recordConvertResult = ''
this.recordConvertStatus = 0
this.recordConvertIng = true
//
const reader = new FileReader();
reader.onloadend = () => {
this.$store.dispatch("call", {
url: 'dialog/msg/convertrecord',
data: {
dialog_id: this.dialogId,
base64: reader.result,
duration: this.recordDuration,
},
method: 'post',
}).then(({data}) => {
this.recordConvertStatus = data ? 1 : 2
this.recordConvertResult = data || this.$L('转文字失败')
}).catch(({msg}) => {
this.recordConvertStatus = 2
this.recordConvertResult = msg
});
};
reader.readAsDataURL(this.recordBlob);
},
convertSend(type) {
if (!this.recordConvertIng) {
return;
}
if (type === 'voice') {
this.uploadRecord();
this.recordConvertIng = false
} else {
if (this.recordConvertStatus === 1) {
this.$emit('on-send', this.recordConvertResult)
this.recordConvertIng = false
} else if (this.recordConvertStatus === 2) {
this.convertRecord()
}
}
},
uploadRecord() {
if (this.recordBlob === null) {
return;
}
@ -1279,7 +1393,7 @@ export default {
this.$emit('on-record', {
type: this.recordBlob.type,
base64: reader.result,
duration,
duration: this.recordDuration,
})
};
reader.readAsDataURL(this.recordBlob);

View File

@ -571,6 +571,7 @@ body.dark-mode-reverse {
}
.chat-input-record-transfer {
&.convert,
&.cancel {
color: #000000;
}

View File

@ -723,12 +723,122 @@
margin-top: 6px;
opacity: 0.6;
}
&.convert {
background-color: #2db7f5;
color: #ffffff;
}
&.cancel {
background-color: #ff6565;
color: #ffffff;
}
}
.chat-input-convert-transfer {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: flex-end;
flex-direction: column;
background-color: rgba(255, 255, 255, 0.8);
.convert-box {
width: 100%;
max-width: 500px;
height: 50%;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
.convert-content {
position: relative;
background-color: $primary-color;
color: #000000;
width: 88%;
padding: 18px;
border-radius: 14px;
transform: translateY(-50%);
&:before {
content: "";
position: absolute;
bottom: -15px;
right: 12%;
transform: translateX(-50%);
border-width: 8px;
border-style: solid;
border-color: $primary-color transparent transparent transparent;
}
.convert-result {
.ivu-input {
font-size: 18px;
border: 0;
box-shadow: none;
background: transparent;
color: #000000;
border-radius: 0;
outline: none;
resize: none;
&::placeholder {
color: rgba(0, 0, 0, 0.7);
}
}
}
}
.convert-footer {
width: 88%;
display: flex;
justify-content: space-around;
margin-bottom: 64px;
> li {
flex-grow: 0;
flex-shrink: 0;
width: 76px;
height: 76px;
list-style: none;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
> i {
font-size: 18px;
&.voice {
transform: rotate(180deg);
}
&.send,
&.error {
font-size: 22px;
width: 100%;
height: 100%;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
color: #0a7600;
background: #000000;
font-weight: 600;
.common-loading {
width: 26px;
height: 26px;
}
}
&.error {
color: #ff0000;
font-size: 30px;
font-weight: 500;
}
}
> span {
font-size: 12px;
margin-top: 4px;
}
}
}
}
}
.chat-input-full-input {
.ivu-modal {
.ivu-modal-content {