diff --git a/app/Http/Controllers/Api/DialogController.php b/app/Http/Controllers/Api/DialogController.php index 2a76cb5d1..b3fc0f9ea 100755 --- a/app/Http/Controllers/Api/DialogController.php +++ b/app/Http/Controllers/Api/DialogController.php @@ -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. 消息标记操作 * diff --git a/app/Http/Controllers/Api/SystemController.php b/app/Http/Controllers/Api/SystemController.php index bd4581a40..f65a0829d 100755 --- a/app/Http/Controllers/Api/SystemController.php +++ b/app/Http/Controllers/Api/SystemController.php @@ -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; diff --git a/app/Module/Extranet.php b/app/Module/Extranet.php index eb0ea021a..9556ef7cf 100644 --- a/app/Module/Extranet.php +++ b/app/Module/Extranet.php @@ -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 diff --git a/app/Module/Ihttp.php b/app/Module/Ihttp.php index 587f35a53..158e60798 100755 --- a/app/Module/Ihttp.php +++ b/app/Module/Ihttp.php @@ -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; } diff --git a/language/original-api.txt b/language/original-api.txt index 01c3bf717..af194c6a5 100644 --- a/language/original-api.txt +++ b/language/original-api.txt @@ -497,3 +497,10 @@ Api接口文档 请选择举报类型 请填写举报原因 + + +开启语音转文字功能需要在应用中开启 ChatGPT AI 机器人。 +语音转文字功能未开启 +语音文件不存在 +语音转文字失败 +仅支持语音消息 diff --git a/language/original-web.txt b/language/original-web.txt index 93661f768..0324622f9 100644 --- a/language/original-web.txt +++ b/language/original-web.txt @@ -1594,3 +1594,9 @@ License Key 举报原因 举报图 举报投诉 + +转文字 +语音转文字 +长按语音消息可转换成文字。 +需要在应用中开启 ChatGPT AI 机器人 +关闭语音转文字功能。 diff --git a/resources/assets/js/pages/manage/components/DialogView.vue b/resources/assets/js/pages/manage/components/DialogView.vue index 884edd58f..363a7069d 100644 --- a/resources/assets/js/pages/manage/components/DialogView.vue +++ b/resources/assets/js/pages/manage/components/DialogView.vue @@ -52,6 +52,9 @@
{{recordDuration(msgData.msg.duration)}}
+
+ {{msgData.msg.text}} +
diff --git a/resources/assets/js/pages/manage/components/DialogWrapper.vue b/resources/assets/js/pages/manage/components/DialogWrapper.vue index 7a8ac7fdb..12b8955be 100644 --- a/resources/assets/js/pages/manage/components/DialogWrapper.vue +++ b/resources/assets/js/pages/manage/components/DialogWrapper.vue @@ -297,6 +297,10 @@ {{ $L('编辑') }} +
  • + + {{ $L('转文字') }} +
  • {{ $L(item.label) }} @@ -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 允许操作 }, diff --git a/resources/assets/js/pages/manage/setting/components/SystemSetting.vue b/resources/assets/js/pages/manage/setting/components/SystemSetting.vue index 8f272f198..47ff2d9f6 100644 --- a/resources/assets/js/pages/manage/setting/components/SystemSetting.vue +++ b/resources/assets/js/pages/manage/setting/components/SystemSetting.vue @@ -124,14 +124,6 @@

    {{ $L('消息相关') }}

    - - - {{$L('开放')}} - {{$L('禁言')}} - -
    {{$L('开放:所有人都可以在全员群组发言。')}}
    -
    {{$L('禁言:除管理员外所有人都禁止在全员群组发言。')}}
    -
    {{$L('自动')}} @@ -140,6 +132,14 @@
    {{$L('自动:注册成功后自动进入全员群。')}}
    {{$L('关闭:其他成员通过@邀请进入。')}}
    + + + {{$L('开放')}} + {{$L('禁言')}} + +
    {{$L('开放:所有人都可以在全员群组发言。')}}
    +
    {{$L('禁言:除管理员外所有人都禁止在全员群组发言。')}}
    +
    {{$L('开放')}} @@ -177,6 +177,14 @@
    {{$L('允许匿名发送消息给其他成员。')}}
    {{$L('禁止匿名发送消息。')}}
    + + + {{$L('开启')}} + {{$L('关闭')}} + +
    {{$L('长按语音消息可转换成文字。')}} ({{$L('需要在应用中开启 ChatGPT AI 机器人')}})
    +
    {{$L('关闭语音转文字功能。')}}
    +
    {{$L('开启')}} diff --git a/resources/assets/sass/pages/components/dialog-wrapper.scss b/resources/assets/sass/pages/components/dialog-wrapper.scss index bc7064eae..b97a9734b 100644 --- a/resources/assets/sass/pages/components/dialog-wrapper.scss +++ b/resources/assets/sass/pages/components/dialog-wrapper.scss @@ -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 { diff --git a/resources/assets/statics/public/css/fonts/taskfont/iconfont.ttf b/resources/assets/statics/public/css/fonts/taskfont/iconfont.ttf index 3296830d6..0172b1fee 100644 Binary files a/resources/assets/statics/public/css/fonts/taskfont/iconfont.ttf and b/resources/assets/statics/public/css/fonts/taskfont/iconfont.ttf differ diff --git a/resources/assets/statics/public/css/fonts/taskfont/iconfont.woff b/resources/assets/statics/public/css/fonts/taskfont/iconfont.woff index a8e201075..dc17137f9 100644 Binary files a/resources/assets/statics/public/css/fonts/taskfont/iconfont.woff and b/resources/assets/statics/public/css/fonts/taskfont/iconfont.woff differ diff --git a/resources/assets/statics/public/css/fonts/taskfont/iconfont.woff2 b/resources/assets/statics/public/css/fonts/taskfont/iconfont.woff2 index b333eb0f9..93cd6d0cb 100644 Binary files a/resources/assets/statics/public/css/fonts/taskfont/iconfont.woff2 and b/resources/assets/statics/public/css/fonts/taskfont/iconfont.woff2 differ