perf: 录音转文字支持自定义语言

This commit is contained in:
kuaifan 2025-03-09 11:33:37 +08:00
parent f6ee630615
commit e53b65496f
4 changed files with 160 additions and 68 deletions

View File

@ -1356,6 +1356,7 @@ class DialogController extends AbstractController
* *
* @apiParam {String} base64 语音base64 * @apiParam {String} base64 语音base64
* @apiParam {Number} duration 语音时长(毫秒) * @apiParam {Number} duration 语音时长(毫秒)
* @apiParam {String} [language] 语音语言比如zh默认当前用户语言
* *
* @apiSuccess {Number} ret 返回状态码1正确、0错误 * @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述) * @apiSuccess {String} msg 返回信息(错误描述)
@ -1368,6 +1369,7 @@ class DialogController extends AbstractController
// //
$path = "uploads/tmp/chat/" . date("Ym") . "/" . $user->userid . "/"; $path = "uploads/tmp/chat/" . date("Ym") . "/" . $user->userid . "/";
$base64 = Request::input('base64'); $base64 = Request::input('base64');
$language = Request::input('language');
$duration = intval(Request::input('duration')); $duration = intval(Request::input('duration'));
if ($duration < 600) { if ($duration < 600) {
return Base::retError('说话时间太短'); return Base::retError('说话时间太短');
@ -1380,7 +1382,21 @@ class DialogController extends AbstractController
return Base::retError($data['msg']); return Base::retError($data['msg']);
} }
$recordData = $data['data']; $recordData = $data['data'];
$res = Extranet::openAItranscriptions($recordData['file']); $extParams = [];
if ($language) {
$targetLanguage = Doo::getLanguages($language);
if (empty($targetLanguage)) {
return Base::retError("参数错误");
}
$extParams = [
'language' => match ($language) {
'zh-CHT' => 'zh',
default => $language,
},
'prompt' => "此音频为“{$targetLanguage}”语言。",
];
}
$res = Extranet::openAItranscriptions($recordData['file'], $extParams);
if (Base::isError($res)) { if (Base::isError($res)) {
return $res; return $res;
} }

View File

@ -15,9 +15,10 @@ class Extranet
/** /**
* 通过 openAI 语音转文字 * 通过 openAI 语音转文字
* @param string $filePath * @param string $filePath
* @param array $extParams
* @return array * @return array
*/ */
public static function openAItranscriptions($filePath) public static function openAItranscriptions($filePath, $extParams = [])
{ {
if (!file_exists($filePath)) { if (!file_exists($filePath)) {
return Base::retError("语音文件不存在"); return Base::retError("语音文件不存在");
@ -36,19 +37,33 @@ class Extranet
$extra['CURLOPT_PROXY'] = $aibotSetting['openai_agency']; $extra['CURLOPT_PROXY'] = $aibotSetting['openai_agency'];
$extra['CURLOPT_PROXYTYPE'] = str_contains($aibotSetting['openai_agency'], 'socks') ? CURLPROXY_SOCKS5 : CURLPROXY_HTTP; $extra['CURLOPT_PROXYTYPE'] = str_contains($aibotSetting['openai_agency'], 'socks') ? CURLPROXY_SOCKS5 : CURLPROXY_HTTP;
} }
$res = Ihttp::ihttp_request('https://api.openai.com/v1/audio/transcriptions', [ $post = array_merge($extParams, [
'file' => new \CURLFile($filePath), 'file' => new \CURLFile($filePath),
'model' => 'whisper-1' 'model' => 'whisper-1',
], $extra, 15); ]);
if (Base::isError($res)) { // 转文字
return Base::retError("语音转文字失败", $res); $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);
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']);
});
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;
}
} }
$resData = Base::json2array($res['data']); return $result;
if (empty($resData['text'])) {
return Base::retError("语音转文字失败", $resData);
}
//
return Base::retSuccess("success", $resData['text']);
} }
/** /**
@ -72,32 +87,41 @@ class Extranet
$extra['CURLOPT_PROXY'] = $aibotSetting['openai_agency']; $extra['CURLOPT_PROXY'] = $aibotSetting['openai_agency'];
$extra['CURLOPT_PROXYTYPE'] = str_contains($aibotSetting['openai_agency'], 'socks') ? CURLPROXY_SOCKS5 : CURLPROXY_HTTP; $extra['CURLOPT_PROXYTYPE'] = str_contains($aibotSetting['openai_agency'], 'socks') ? CURLPROXY_SOCKS5 : CURLPROXY_HTTP;
} }
$res = Ihttp::ihttp_request('https://api.openai.com/v1/chat/completions', json_encode([ $post = json_encode([
"model" => "gpt-4o-mini", "model" => "gpt-4o-mini",
"messages" => [ "messages" => [
[ [
"role" => "system", "role" => "system",
"content" => "你是一个专业的翻译器,翻译的结果尽量符合“项目任务管理系统”的使用,并且翻译的结果不用额外添加换行尽量保持原格式,将提供的文本翻译成“{$targetLanguage}”语言" "content" => "你是一个专业的翻译器,请将<text>标签里面的内容翻译成“{$targetLanguage}”语言,翻译的结果尽量符合“项目任务管理系统”的使用并且保持原格式"
], ],
[ [
"role" => "user", "role" => "user",
"content" => $text "content" => "<text>{$text}</text>"
] ]
] ]
]), $extra, 15); ]);
if (Base::isError($res)) { $cacheKey = "openAItranslations::" . md5(Base::array2json($extra) . '_' . Base::array2json($post));
return Base::retError("翻译失败", $res); $result = Cache::remember($cacheKey, Carbon::now()->addDays(), function() use ($extra, $post) {
$res = Ihttp::ihttp_request('https://api.openai.com/v1/chat/completions', $post, $extra, 15);
if (Base::isError($res)) {
return Base::retError("翻译失败", $res);
}
$resData = Base::json2array($res['data']);
if (empty($resData['choices'])) {
return Base::retError("翻译失败", $resData);
}
$result = $resData['choices'][0]['message']['content'];
$result = preg_replace('/^\"|\"$/', '', trim($result));
$result = preg_replace('/^<text>|<\/text>$/', '', trim($result));
if (empty($result)) {
return Base::retError("翻译失败", $result);
}
return Base::retSuccess("success", $result);
});
if (Base::isError($result)) {
Cache::forget($cacheKey);
} }
$resData = Base::json2array($res['data']); return $result;
if (empty($resData['choices'])) {
return Base::retError("翻译失败", $resData);
}
$result = $resData['choices'][0]['message']['content'];
$result = preg_replace('/^\"|\"$/', '', $result);
if (empty($result)) {
return Base::retError("翻译失败", $result);
}
return Base::retSuccess("success", $result);
} }
/** /**

View File

@ -208,15 +208,21 @@
<div class="convert-box"> <div class="convert-box">
<div class="convert-body"> <div class="convert-body">
<div class="convert-content"> <div class="convert-content">
<Input <div class="convert-setting" @click="convertSetting">
type="textarea" <i class="taskfont">&#xe691;</i>
class="convert-result no-dark-content" </div>
v-model="recordConvertResult" <div class="convert-input">
:rows="1" <Input
:autosize="{minRows: 1, maxRows: 5}" type="textarea"
:placeholder="recordConvertStatus === 0 ? '...' : ''" class="convert-result no-dark-content"
@on-focus="recordConvertFocus=true" v-model="recordConvertResult"
@on-blur="recordConvertFocus=false"/> :rows="1"
:autosize="{minRows: 1, maxRows: 5}"
:placeholder="recordConvertStatus === 0 ? '...' : ''"
:disabled="recordConvertStatus !== 1"
@on-focus="recordConvertFocus=true"
@on-blur="recordConvertFocus=false"/>
</div>
</div> </div>
</div> </div>
<ul class="convert-footer" :style="recordConvertFooterStyle"> <ul class="convert-footer" :style="recordConvertFooterStyle">
@ -277,6 +283,7 @@ import TransferDom from "../../../../directives/transfer-dom";
import clickoutside from "../../../../directives/clickoutside"; import clickoutside from "../../../../directives/clickoutside";
import longpress from "../../../../directives/longpress"; import longpress from "../../../../directives/longpress";
import {inputLoadAdd, inputLoadIsLast, inputLoadRemove} from "./one"; import {inputLoadAdd, inputLoadIsLast, inputLoadRemove} from "./one";
import {getLanguage, languageList} from "../../../../language";
import {isMarkdownFormat} from "../../../../store/markdown"; import {isMarkdownFormat} from "../../../../store/markdown";
import emitter from "../../../../store/events"; import emitter from "../../../../store/events";
@ -383,6 +390,7 @@ export default {
recordConvertFocus: false, recordConvertFocus: false,
recordConvertStatus: 0, // 0: 1: 2: recordConvertStatus: 0, // 0: 1: 2:
recordConvertResult: '', recordConvertResult: '',
recordConvertLanguage: '',
touchStart: {}, touchStart: {},
touchFocus: false, touchFocus: false,
@ -521,7 +529,7 @@ export default {
'dialogMsgs', 'dialogMsgs',
'cacheKeyboard', 'cacheKeyboard',
'keyboardType',
'isModKey', 'isModKey',
]), ]),
@ -572,8 +580,8 @@ export default {
}, },
recordConvertFooterStyle() { recordConvertFooterStyle() {
const {recordConvertFocus} = this; const {recordConvertFocus, keyboardType} = this;
return recordConvertFocus ? { return recordConvertFocus && keyboardType === 'show' ? {
alignItems: 'flex-start', alignItems: 'flex-start',
transform: 'translateY(12px)' transform: 'translateY(12px)'
} : {} } : {}
@ -1400,6 +1408,7 @@ export default {
dialog_id: this.dialogId, dialog_id: this.dialogId,
base64: reader.result, base64: reader.result,
duration: this.recordDuration, duration: this.recordDuration,
language: this.recordConvertLanguage || getLanguage()
}, },
method: 'post', method: 'post',
}).then(({data}) => { }).then(({data}) => {
@ -1413,6 +1422,27 @@ export default {
reader.readAsDataURL(this.recordBlob); reader.readAsDataURL(this.recordBlob);
}, },
async convertSetting(event) {
await this.$nextTick()
const list = Object.keys(languageList).map(item => ({
label: languageList[item],
value: item
}))
list.unshift(...[
{label: '自动识别', value: ''},
])
this.$store.state.menuOperation = {
event,
list,
active: this.recordConvertLanguage,
scrollHide: true,
onUpdate: async (language) => {
this.recordConvertLanguage = language
this.convertRecord()
}
}
},
convertSend(type) { convertSend(type) {
if (!this.recordConvertIng) { if (!this.recordConvertIng) {
return; return;

View File

@ -761,37 +761,59 @@
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
.convert-content { .convert-content {
display: flex;
flex-direction: column;
align-items: flex-end;
position: relative; position: relative;
background-color: $primary-color;
color: #000000;
width: 88%; width: 88%;
padding: 18px;
border-radius: 14px;
transform: translateY(12px); transform: translateY(12px);
transition: transform 0.3s; .convert-setting {
&:before { color: #4d4d4d;
content: ""; background-color: #c7c7c7;
position: absolute; opacity: 0.7;
bottom: -15px; padding: 5px;
right: 12%; margin: 0 2px 8px 0;
transform: translateX(-50%); border-radius: 50%;
border-width: 8px; width: 26px;
border-style: solid; height: 26px;
border-color: $primary-color transparent transparent transparent; display: flex;
} justify-content: center;
.convert-result { align-items: center;
.ivu-input { cursor: pointer;
> i {
font-size: 18px; font-size: 18px;
border: 0; }
box-shadow: none; }
background: transparent; .convert-input {
color: #ffffff; width: 100%;
caret-color: #ffffff; padding: 18px;
border-radius: 0; border-radius: 14px;
outline: none; background-color: $primary-color;
resize: none; color: #000000;
&::placeholder { &:before {
color: rgba(255, 255, 255, 0.7); 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: #ffffff;
caret-color: #ffffff;
border-radius: 0;
outline: none;
resize: none;
&::placeholder {
color: rgba(255, 255, 255, 0.7);
}
} }
} }
} }