diff --git a/app/Http/Controllers/Api/DialogController.php b/app/Http/Controllers/Api/DialogController.php index 991ad963d..74d451d1c 100755 --- a/app/Http/Controllers/Api/DialogController.php +++ b/app/Http/Controllers/Api/DialogController.php @@ -1356,7 +1356,15 @@ class DialogController extends AbstractController * * @apiParam {String} base64 语音base64 * @apiParam {Number} duration 语音时长(毫秒) - * @apiParam {String} [language] 语音语言(比如:zh,默认:当前用户语言) + * @apiParam {String} [language] 识别语言 + * - 比如:zh + * - 默认:自动识别 + * - 格式:符合 ISO_639 标准 + * - 此参数不一定起效果,AI会根据语音和language参考翻译识别结果 + * @apiParam {String} [translate] 翻译识别结果 + * - 比如:zh + * - 默认:不翻译结果 + * - 格式:符合 ISO_639 标准 * * @apiSuccess {Number} ret 返回状态码(1正确、0错误) * @apiSuccess {String} msg 返回信息(错误描述) @@ -1370,10 +1378,12 @@ class DialogController extends AbstractController $path = "uploads/tmp/chat/" . date("Ym") . "/" . $user->userid . "/"; $base64 = Request::input('base64'); $language = Request::input('language'); + $translate = Request::input('translate'); $duration = intval(Request::input('duration')); if ($duration < 600) { return Base::retError('说话时间太短'); } + // 保存录音 $data = Base::record64save([ "base64" => $base64, "path" => $path, @@ -1382,28 +1392,30 @@ class DialogController extends AbstractController return Base::retError($data['msg']); } $recordData = $data['data']; + // 转文字 $extParams = []; if ($language) { - $targetLanguage = Doo::getLanguages($language); - if (empty($targetLanguage)) { - return Base::retError("参数错误"); - } $extParams = [ - 'language' => match ($language) { - 'zh-CHT' => 'zh', - default => $language, - }, - 'prompt' => "此音频为“{$targetLanguage}”语言。", + 'language' => $language === 'zh-CHT' ? 'zh' : $language, + 'prompt' => "将此语音识别为“" . Doo::getLanguages($language) . "”。", ]; } - $res = Extranet::openAItranscriptions($recordData['file'], $extParams); - if (Base::isError($res)) { - return $res; + $result = Extranet::openAItranscriptions($recordData['file'], $extParams); + if (Base::isError($result)) { + return $result; } - if (strlen($res['data']) < 1) { + if (strlen($result['data']) < 1) { return Base::retError('转文字失败'); } - return $res; + // 翻译 + if ($translate) { + $result = Extranet::openAItranslations($result['data'], Doo::getLanguages($translate)); + if (Base::isError($result)) { + return $result; + } + } + // 返回 + return $result; } /** diff --git a/app/Module/Extranet.php b/app/Module/Extranet.php index b74283050..5f8eb5dd5 100644 --- a/app/Module/Extranet.php +++ b/app/Module/Extranet.php @@ -28,7 +28,6 @@ class Extranet if ($systemSetting['voice2text'] !== 'open' || empty($aibotSetting['openai_key'])) { return Base::retError("语音转文字功能未开启"); } - // $extra = [ 'Content-Type' => 'multipart/form-data', 'Authorization' => 'Bearer ' . $aibotSetting['openai_key'], @@ -41,7 +40,6 @@ class Extranet 'file' => new \CURLFile($filePath), 'model' => 'whisper-1', ]); - // 转文字 $cacheKey = "openAItranscriptions::" . md5($filePath . '_' . Base::array2json($extra) . '_' . Base::array2json($extParams)); $result = Cache::remember($cacheKey, Carbon::now()->addDays(), function() use ($extra, $post) { $res = Ihttp::ihttp_request('https://api.openai.com/v1/audio/transcriptions', $post, $extra, 15); @@ -56,12 +54,6 @@ class Extranet }); if (Base::isError($result)) { Cache::forget($cacheKey); - } elseif ($extParams['language']) { - // 翻译 - $translResult = self::openAItranslations($result['data'], Doo::getLanguages($extParams['language'])); - if (Base::isSuccess($result)) { - $result = $translResult; - } } return $result; } @@ -92,11 +84,19 @@ class Extranet "messages" => [ [ "role" => "system", - "content" => "你是一个专业的翻译器,请将标签里面的内容翻译成“{$targetLanguage}”语言,翻译的结果尽量符合“项目任务管理系统”的使用并且保持原格式。" + "content" => << 标签内的内容翻译为{$targetLanguage}。 + + 翻译要求: + - 翻译结果需符合“项目任务管理系统”的专业术语和使用场景。 + - 保持原文格式、结构和排版不变。 + - 语言表达准确、简洁,符合项目管理领域的行业规范。 + - 注意专业术语的一致性和连贯性。 + EOF ], [ "role" => "user", - "content" => "{$text}" + "content" => "{$text}" ] ] ]); @@ -112,7 +112,7 @@ class Extranet } $result = $resData['choices'][0]['message']['content']; $result = preg_replace('/^\"|\"$/', '', trim($result)); - $result = preg_replace('/^|<\/text>$/', '', trim($result)); + $result = preg_replace('/<\/*translation_original_text>/', '', trim($result)); if (empty($result)) { return Base::retError("翻译失败", $result); } diff --git a/resources/assets/js/components/DropdownMenu.vue b/resources/assets/js/components/DropdownMenu.vue index 15fd83553..c9818700d 100644 --- a/resources/assets/js/components/DropdownMenu.vue +++ b/resources/assets/js/components/DropdownMenu.vue @@ -11,14 +11,17 @@
  • -
      +
        + :disabled="active === item.value || !!item.disabled">
        {{item.label}}
        +
        + +
      @@ -37,6 +40,8 @@ export default { active: '', // 当前选中的值 onUpdate: null, // 选中后的回调函数 scrollHide: false, // 滚动立即隐藏 + tickShow: true, // 是否显示打勾 + maxHeight: 0, // 滚动区域最大高度 element: null, target: null, @@ -51,7 +56,11 @@ export default { }, computed: { - ...mapState(['menuOperation']) + ...mapState(['menuOperation']), + + ulStyle({maxHeight}) { + return maxHeight > 0 ? {maxHeight: `${maxHeight}px`} : {}; + } }, watch: { @@ -72,6 +81,8 @@ export default { this.active = data.active && this.list.find(item => item.value === data.active) ? data.active : ''; this.onUpdate = typeof data.onUpdate === "function" ? data.onUpdate : null; this.scrollHide = typeof data.scrollHide === "boolean" ? data.scrollHide : false; + this.tickShow = typeof data.tickShow === "boolean" ? data.tickShow : true; + this.maxHeight = typeof data.maxHeight === "number" ? data.maxHeight : 0; // this.$refs.icon.focus(); this.show(); diff --git a/resources/assets/js/pages/manage/components/ChatInput/index.vue b/resources/assets/js/pages/manage/components/ChatInput/index.vue index b95490918..752c0ac54 100755 --- a/resources/assets/js/pages/manage/components/ChatInput/index.vue +++ b/resources/assets/js/pages/manage/components/ChatInput/index.vue @@ -208,8 +208,9 @@
      -
      - +
      + +
      { @@ -1422,22 +1425,43 @@ export default { reader.readAsDataURL(this.recordBlob); }, - async convertSetting(event) { + async convertSetting(type, event) { + if (this.recordConvertStatus !== 1) { + $A.messageWarning("正在识别中,请稍后") + return; + } await this.$nextTick() const list = Object.keys(languageList).map(item => ({ label: languageList[item], value: item })) - list.unshift(...[ - {label: '自动识别', value: ''}, - ]) + let active + if (type === 'transcription') { + // 语音转文字 + list.unshift(...[ + {label: '选择识别语言', value: '', disabled: true}, + {label: '自动识别', value: ''}, + ]) + active = this.cacheTranscriptionLanguage + } else { + // 翻译 + list.unshift(...[ + {label: '选择翻译结果', value: '', disabled: true}, + {label: '不翻译结果', value: ''}, + ]) + active = this.recordConvertTranslate + } this.$store.state.menuOperation = { event, list, - active: this.recordConvertLanguage, + active, scrollHide: true, onUpdate: async (language) => { - this.recordConvertLanguage = language + if (type === 'transcription') { + await this.$store.dispatch('setTranscriptionLanguage', language) + } else { + this.recordConvertTranslate = language + } this.convertRecord() } } diff --git a/resources/assets/js/store/actions.js b/resources/assets/js/store/actions.js index 9453763a1..5e9546915 100644 --- a/resources/assets/js/store/actions.js +++ b/resources/assets/js/store/actions.js @@ -922,6 +922,7 @@ export default { cacheFileSort: await $A.IDBJson("cacheFileSort"), cacheTaskBrowse: await $A.IDBArray("cacheTaskBrowse"), cacheTranslationLanguage: await $A.IDBString("cacheTranslationLanguage"), + cacheTranscriptionLanguage: await $A.IDBString("cacheTranscriptionLanguage"), cacheTranslations: await $A.IDBArray("cacheTranslations"), cacheEmojis: await $A.IDBArray("cacheEmojis"), userInfo: await $A.IDBJson("userInfo"), @@ -957,7 +958,8 @@ export default { string: [ 'clientId', 'cacheServerUrl', - 'cacheTranslationLanguage' + 'cacheTranslationLanguage', + 'cacheTranscriptionLanguage' ], array: [ 'cacheUserBasic', @@ -1002,6 +1004,11 @@ export default { state.cacheTranslationLanguage = languageName; } + // TranscriptionLanguage检查 + if (typeof languageList[state.cacheTranscriptionLanguage] === "undefined") { + state.cacheTranscriptionLanguage = ''; + } + // 处理用户信息 if (state.userInfo.userid) { state.userId = state.userInfo.userid = $A.runNum(state.userInfo.userid); @@ -3627,6 +3634,16 @@ export default { $A.IDBSave('cacheTranslationLanguage', language); }, + /** + * 设置语音转文字语言 + * @param state + * @param language + */ + setTranscriptionLanguage({state}, language) { + state.cacheTranscriptionLanguage = language + $A.IDBSave('cacheTranscriptionLanguage', language); + }, + /** *****************************************************************************************/ /** ************************************* loads *********************************************/ /** *****************************************************************************************/ diff --git a/resources/assets/js/store/state.js b/resources/assets/js/store/state.js index fb914964c..8d755d0ed 100644 --- a/resources/assets/js/store/state.js +++ b/resources/assets/js/store/state.js @@ -245,6 +245,9 @@ export default { cacheTranslationLanguage: '', cacheTranslations: [], + // 语音转文字(识别语言) + cacheTranscriptionLanguage: '', + // 下拉菜单操作 menuOperation: {} }; diff --git a/resources/assets/sass/dark.scss b/resources/assets/sass/dark.scss index cb65c227a..f7d7ccd26 100644 --- a/resources/assets/sass/dark.scss +++ b/resources/assets/sass/dark.scss @@ -580,6 +580,17 @@ body.dark-mode-reverse { .chat-input-convert-transfer { background-color: rgba(255, 255, 255, 0.9); .convert-box { + .convert-body { + .convert-content { + .convert-setting { + > i { + &.active { + color: #000000; + } + } + } + } + } .convert-footer { color: #000000; > li { diff --git a/resources/assets/sass/pages/components/chat-input.scss b/resources/assets/sass/pages/components/chat-input.scss index 5b301bbe4..3884f90cc 100755 --- a/resources/assets/sass/pages/components/chat-input.scss +++ b/resources/assets/sass/pages/components/chat-input.scss @@ -768,20 +768,28 @@ width: 88%; transform: translateY(12px); .convert-setting { - color: #4d4d4d; - background-color: #c7c7c7; - opacity: 0.7; - padding: 5px; margin: 0 2px 8px 0; - border-radius: 50%; - width: 26px; - height: 26px; display: flex; - justify-content: center; + gap: 12px; align-items: center; - cursor: pointer; > i { + color: #4d4d4d; + background-color: #c7c7c7; + opacity: 0.7; + padding: 5px; + border-radius: 50%; + width: 26px; + height: 26px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; font-size: 18px; + &.active { + background-color: $primary-color; + color: #ffffff; + opacity: 1 + } } } .convert-input { diff --git a/resources/assets/sass/pages/components/general-operation.scss b/resources/assets/sass/pages/components/general-operation.scss index 8fa8a935d..5c2b07eb5 100644 --- a/resources/assets/sass/pages/components/general-operation.scss +++ b/resources/assets/sass/pages/components/general-operation.scss @@ -32,6 +32,10 @@ } > li { + display: flex; + align-items: center; + justify-content: space-between; + .item { display: flex; align-items: center; @@ -63,6 +67,18 @@ } } + .tick { + color: $primary-color; + transform: translateX(40%); + width: 26px; + height: 26px; + text-align: right; + margin-left: 6px; + > i { + font-size: 14px; + } + } + .flow { padding: 4px 0;