perf: 语音消息转文字

This commit is contained in:
kuaifan 2024-05-10 13:15:27 +09:00
parent bf46a00937
commit 426fa63288
13 changed files with 193 additions and 12 deletions

View File

@ -11,6 +11,7 @@ use Carbon\Carbon;
use App\Models\File;
use App\Models\User;
use App\Module\Base;
use App\Module\Extranet;
use App\Module\TimeRange;
use App\Models\FileContent;
use App\Models\AbstractModel;
@ -1476,6 +1477,50 @@ class DialogController extends AbstractController
return Base::retSuccess("success");
}
/**
* @api {get} api/dialog/msg/voice2text 29. 语音消息转文字
*
* @apiDescription 将语音消息转文字需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__voice2text
*
* @apiParam {Number} msg_id 消息ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__voice2text()
{
User::auth();
//
$msg_id = intval(Request::input("msg_id"));
$msg = WebSocketDialogMsg::whereId($msg_id)->first();
if (empty($msg)) {
return Base::retError("消息不存在或已被删除");
}
if ($msg->type !== 'record') {
return Base::retError("仅支持语音消息");
}
if ($msg['msg']['text']) {
return Base::retSuccess("success", $msg);
}
WebSocketDialog::checkDialog($msg->dialog_id);
//
$msgData = Base::json2array($msg->getRawOriginal('msg'));
$res = Extranet::openAItranscriptions(public_path($msgData['path']));
if (Base::isError($res)) {
return $res;
}
//
$msg->updateInstance([
'msg' => array_merge($msgData, ['text' => $res['data']]),
]);
$msg->save();
return Base::retSuccess("success", $msg);
}
/**
* @api {get} api/dialog/msg/mark 30. 消息标记操作
*

View File

@ -40,7 +40,7 @@ class SystemController extends AbstractController
* @apiParam {String} type
* - get: 获取(默认)
* - all: 获取所有(需要管理员权限)
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'temp_account_alias', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'e2e_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'all_group_mute', 'all_group_autoin', 'user_private_chat_mute', 'user_group_chat_mute', 'image_compress', 'image_save_local', 'start_home']
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'temp_account_alias', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'voice2text', 'e2e_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'all_group_mute', 'all_group_autoin', 'user_private_chat_mute', 'user_group_chat_mute', 'image_compress', 'image_save_local', 'start_home']
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@ -66,6 +66,7 @@ class SystemController extends AbstractController
'project_invite',
'chat_information',
'anon_message',
'voice2text',
'e2e_message',
'auto_archived',
'archived_day',
@ -93,6 +94,9 @@ class SystemController extends AbstractController
return Base::retError('自动归档时间不可大于100天');
}
}
if ($all['voice2text'] == 'open' && empty(Base::settingFind('aibotSetting', 'openai_key'))) {
return Base::retError('开启语音转文字功能需要在应用中开启 ChatGPT AI 机器人。');
}
$setting = Base::setting('system', Base::newTrim($all));
} else {
$setting = Base::setting('system');
@ -113,6 +117,7 @@ class SystemController extends AbstractController
$setting['project_invite'] = $setting['project_invite'] ?: 'open';
$setting['chat_information'] = $setting['chat_information'] ?: 'optional';
$setting['anon_message'] = $setting['anon_message'] ?: 'open';
$setting['voice2text'] = $setting['voice2text'] ?: 'close';
$setting['e2e_message'] = $setting['e2e_message'] ?: 'close';
$setting['auto_archived'] = $setting['auto_archived'] ?: 'close';
$setting['archived_day'] = floatval($setting['archived_day']) ?: 7;

View File

@ -12,6 +12,49 @@ use Illuminate\Support\Facades\Config;
*/
class Extranet
{
/**
* 通过 openAI 语音转文字
* @param string $filePath
* @return array
*/
public static function openAItranscriptions($filePath)
{
if (!file_exists($filePath)) {
return Base::retError("语音文件不存在");
}
$systemSetting = Base::setting('system');
$aibotSetting = Base::setting('aibotSetting');
if ($systemSetting['voice2text'] !== 'open' || empty($aibotSetting['openai_key'])) {
return Base::retError("语音转文字功能未开启");
}
//
$extra = [
'Content-Type' => 'multipart/form-data',
'Authorization' => 'Bearer ' . $aibotSetting['openai_key'],
];
if ($aibotSetting['openai_agency']) {
$extra['CURLOPT_PROXY'] = $aibotSetting['openai_agency'];
if (str_contains($aibotSetting['openai_agency'], 'socks')) {
$extra['CURLOPT_PROXYTYPE'] = CURLPROXY_SOCKS5;
} else {
$extra['CURLOPT_PROXYTYPE'] = CURLPROXY_HTTP;
}
}
$res = Ihttp::ihttp_request('https://api.openai.com/v1/audio/transcriptions', [
'file' => new \CURLFile($filePath),
'model' => 'whisper-1'
], $extra, 15);
if (Base::isError($res)) {
return Base::retError("语音转文字失败", $res);
}
$resData = Base::json2array($res['data']);
if (empty($resData['text'])) {
return Base::retError("语音转文字失败", $resData);
}
//
return Base::retSuccess("success", $resData['text']);
}
/**
* 获取IP地址经纬度
* @param string $ip

View File

@ -35,8 +35,12 @@ class Ihttp
if($post) {
if (is_array($post)) {
$filepost = false;
foreach ($post as $name => $value) {
if (is_string($value) && substr($value, 0, 1) == '@') {
foreach ($post as $value) {
if (is_string($value) && str_starts_with($value, '@')) {
$filepost = true;
break;
}
if ($value instanceof \CURLFile) {
$filepost = true;
break;
}

View File

@ -497,3 +497,10 @@ Api接口文档
请选择举报类型
请填写举报原因
开启语音转文字功能需要在应用中开启 ChatGPT AI 机器人。
语音转文字功能未开启
语音文件不存在
语音转文字失败
仅支持语音消息

View File

@ -1594,3 +1594,9 @@ License Key
举报原因
举报图
举报投诉
转文字
语音转文字
长按语音消息可转换成文字。
需要在应用中开启 ChatGPT AI 机器人
关闭语音转文字功能。

View File

@ -52,6 +52,9 @@
<div class="record-time">{{recordDuration(msgData.msg.duration)}}</div>
<div class="record-icon taskfont"></div>
</div>
<div v-if="msgData.msg.text" class="dialog-record-text">
{{msgData.msg.text}}
</div>
</div>
<!--会议-->
<div v-else-if="msgData.type === 'meeting'" class="content-meeting no-dark-content">

View File

@ -297,6 +297,10 @@
<i class="taskfont">&#xe779;</i>
<span>{{ $L('编辑') }}</span>
</li>
<li v-if="actionPermission(operateItem, 'voice2text')" @click="onOperate('voice2text')">
<i class="taskfont">&#xe628;</i>
<span>{{ $L('转文字') }}</span>
</li>
<li v-for="item in operateCopys" @click="onOperate('copy', item)">
<i class="taskfont" v-html="item.icon"></i>
<span>{{ $L(item.label) }}</span>
@ -2891,6 +2895,10 @@ export default {
this.onUpdate()
break;
case "voice2text":
this.onVoice2text()
break;
case "copy":
this.onCopy(value)
break;
@ -3005,6 +3013,26 @@ export default {
}
},
onVoice2text() {
if (!this.actionPermission(this.operateItem, 'voice2text')) {
return;
}
const {id: msg_id} = this.operateItem
this.$store.dispatch("setLoad", `msg-${msg_id}`)
this.$store.dispatch("call", {
url: 'dialog/msg/voice2text',
data: {
msg_id
},
}).then(({data}) => {
this.$store.dispatch("saveDialogMsg", data);
}).catch(({msg}) => {
$A.messageError(msg);
}).finally(_ => {
this.$store.dispatch("cancelLoad", `msg-${msg_id}`)
});
},
onCopy(data) {
if (!$A.isJson(data)) {
return
@ -3552,6 +3580,16 @@ export default {
return typeof item.msg.approve_type === 'undefined' //
}
return false
} else if (permission === 'voice2text') {
if (item.type !== 'record') {
return false;
}
if (item.msg.text) {
return false;
}
if (this.isLoad(`msg-${item.id}`)) {
return false;
}
}
return true // true
},

View File

@ -124,14 +124,6 @@
<div class="block-setting-box">
<h3>{{ $L('消息相关') }}</h3>
<div class="form-box">
<FormItem :label="$L('全员群组禁言')" prop="allGroupMute">
<RadioGroup v-model="formDatum.all_group_mute">
<Radio label="open">{{$L('开放')}}</Radio>
<Radio label="close">{{$L('禁言')}}</Radio>
</RadioGroup>
<div v-if="formDatum.all_group_mute == 'open'" class="form-tip">{{$L('开放所有人都可以在全员群组发言')}}</div>
<div v-else-if="formDatum.all_group_mute == 'close'" class="form-tip">{{$L('禁言:除管理员外所有人都禁止在全员群组发言。')}}</div>
</FormItem>
<FormItem :label="$L('自动进入全员群')" prop="allGroupAutoin">
<RadioGroup v-model="formDatum.all_group_autoin">
<Radio label="yes">{{$L('自动')}}</Radio>
@ -140,6 +132,14 @@
<div v-if="formDatum.all_group_autoin == 'yes'" class="form-tip">{{$L('自动注册成功后自动进入全员群')}}</div>
<div v-else-if="formDatum.all_group_autoin == 'no'" class="form-tip">{{$L('关闭:其他成员通过@邀请进入。')}}</div>
</FormItem>
<FormItem :label="$L('全员群组禁言')" prop="allGroupMute">
<RadioGroup v-model="formDatum.all_group_mute">
<Radio label="open">{{$L('开放')}}</Radio>
<Radio label="close">{{$L('禁言')}}</Radio>
</RadioGroup>
<div v-if="formDatum.all_group_mute == 'open'" class="form-tip">{{$L('开放所有人都可以在全员群组发言')}}</div>
<div v-else-if="formDatum.all_group_mute == 'close'" class="form-tip">{{$L('禁言:除管理员外所有人都禁止在全员群组发言。')}}</div>
</FormItem>
<FormItem :label="$L('私聊禁言')" prop="userPrivateChatMute">
<RadioGroup v-model="formDatum.user_private_chat_mute">
<Radio label="open">{{$L('开放')}}</Radio>
@ -177,6 +177,14 @@
<div v-if="formDatum.anon_message == 'open'" class="form-tip">{{$L('允许匿名发送消息给其他成员')}}</div>
<div v-else class="form-tip">{{$L('禁止匿名发送消息。')}}</div>
</FormItem>
<FormItem :label="$L('语音转文字')" prop="voice2text">
<RadioGroup v-model="formDatum.voice2text">
<Radio label="open">{{$L('开启')}}</Radio>
<Radio label="close">{{$L('关闭')}}</Radio>
</RadioGroup>
<div v-if="formDatum.voice2text == 'open'" class="form-tip">{{$L('长按语音消息可转换成文字')}} ({{$L('需要在应用中开启 ChatGPT AI 机器人')}})</div>
<div v-else class="form-tip">{{$L('关闭语音转文字功能。')}}</div>
</FormItem>
<FormItem :label="$L('端到端加密')" prop="e2eMessage">
<RadioGroup v-model="formDatum.e2e_message">
<Radio label="open">{{$L('开启')}}</Radio>

View File

@ -654,7 +654,8 @@
margin: 0 0 0 8px;
position: relative;
&.text {
&.text,
&.record {
max-width: 70%;
}
@ -1067,7 +1068,10 @@
.content-record {
display: flex;
flex-direction: column;
align-items: flex-start;
color: $primary-title-color;
max-width: 100%;
.dialog-record {
display: flex;
@ -1075,6 +1079,7 @@
justify-content: flex-end;
align-content: center;
line-height: 24px;
max-width: 100%;
cursor: pointer;
.record-time {
@ -1112,6 +1117,22 @@
}
}
}
.dialog-record-text {
position: relative;
margin-top: 8px;
padding-top: 8px;
&:before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background-color: rgba(255, 255, 255, 0.2);
transform: scaleY(0.5);
}
}
}
.content-meeting {
@ -1607,6 +1628,7 @@
}
.content-record {
align-items: flex-end;
color: #ffffff;
.dialog-record {