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 {Number} duration 语音时长(毫秒)
* @apiParam {String} [language] 语音语言比如zh默认当前用户语言
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@ -1368,6 +1369,7 @@ class DialogController extends AbstractController
//
$path = "uploads/tmp/chat/" . date("Ym") . "/" . $user->userid . "/";
$base64 = Request::input('base64');
$language = Request::input('language');
$duration = intval(Request::input('duration'));
if ($duration < 600) {
return Base::retError('说话时间太短');
@ -1380,7 +1382,21 @@ class DialogController extends AbstractController
return Base::retError($data['msg']);
}
$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)) {
return $res;
}

View File

@ -15,9 +15,10 @@ class Extranet
/**
* 通过 openAI 语音转文字
* @param string $filePath
* @param array $extParams
* @return array
*/
public static function openAItranscriptions($filePath)
public static function openAItranscriptions($filePath, $extParams = [])
{
if (!file_exists($filePath)) {
return Base::retError("语音文件不存在");
@ -36,19 +37,33 @@ class Extranet
$extra['CURLOPT_PROXY'] = $aibotSetting['openai_agency'];
$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),
'model' => 'whisper-1'
], $extra, 15);
if (Base::isError($res)) {
return Base::retError("语音转文字失败", $res);
'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);
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']);
if (empty($resData['text'])) {
return Base::retError("语音转文字失败", $resData);
}
//
return Base::retSuccess("success", $resData['text']);
return $result;
}
/**
@ -72,32 +87,41 @@ class Extranet
$extra['CURLOPT_PROXY'] = $aibotSetting['openai_agency'];
$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",
"messages" => [
[
"role" => "system",
"content" => "你是一个专业的翻译器,翻译的结果尽量符合“项目任务管理系统”的使用,并且翻译的结果不用额外添加换行尽量保持原格式,将提供的文本翻译成“{$targetLanguage}”语言"
"content" => "你是一个专业的翻译器,请将<text>标签里面的内容翻译成“{$targetLanguage}”语言,翻译的结果尽量符合“项目任务管理系统”的使用并且保持原格式"
],
[
"role" => "user",
"content" => $text
"content" => "<text>{$text}</text>"
]
]
]), $extra, 15);
if (Base::isError($res)) {
return Base::retError("翻译失败", $res);
]);
$cacheKey = "openAItranslations::" . md5(Base::array2json($extra) . '_' . Base::array2json($post));
$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']);
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);
return $result;
}
/**

View File

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

View File

@ -761,37 +761,59 @@
justify-content: flex-end;
align-items: center;
.convert-content {
display: flex;
flex-direction: column;
align-items: flex-end;
position: relative;
background-color: $primary-color;
color: #000000;
width: 88%;
padding: 18px;
border-radius: 14px;
transform: translateY(12px);
transition: transform 0.3s;
&: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 {
.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;
align-items: center;
cursor: pointer;
> i {
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);
}
}
.convert-input {
width: 100%;
padding: 18px;
border-radius: 14px;
background-color: $primary-color;
color: #000000;
&: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: #ffffff;
caret-color: #ffffff;
border-radius: 0;
outline: none;
resize: none;
&::placeholder {
color: rgba(255, 255, 255, 0.7);
}
}
}
}