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

This commit is contained in:
kuaifan 2025-03-09 15:32:38 +08:00
parent e53b65496f
commit e34aa77a54
9 changed files with 152 additions and 50 deletions

View File

@ -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;
}
/**

View File

@ -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" => "你是一个专业的翻译器,请将<text>标签里面的内容翻译成“{$targetLanguage}”语言,翻译的结果尽量符合“项目任务管理系统”的使用并且保持原格式。"
"content" => <<<EOF
你是一名专业翻译人员,请将 <translation_original_text> 标签内的内容翻译为{$targetLanguage}
翻译要求:
- 翻译结果需符合“项目任务管理系统”的专业术语和使用场景。
- 保持原文格式、结构和排版不变。
- 语言表达准确、简洁,符合项目管理领域的行业规范。
- 注意专业术语的一致性和连贯性。
EOF
],
[
"role" => "user",
"content" => "<text>{$text}</text>"
"content" => "<translation_original_text>{$text}</translation_original_text>"
]
]
]);
@ -112,7 +112,7 @@ class Extranet
}
$result = $resData['choices'][0]['message']['content'];
$result = preg_replace('/^\"|\"$/', '', trim($result));
$result = preg_replace('/^<text>|<\/text>$/', '', trim($result));
$result = preg_replace('/<\/*translation_original_text>/', '', trim($result));
if (empty($result)) {
return Base::retError("翻译失败", $result);
}

View File

@ -11,14 +11,17 @@
<div ref="icon" class="general-operation-icon"></div>
<EDropdownMenu ref="dropdownMenu" slot="dropdown" class="general-operation-more-dropdown menu-dropdown">
<li class="general-operation-more-warp small">
<ul>
<ul :style="ulStyle">
<EDropdownItem
v-for="(item, key) in list"
:key="key"
:command="item.value"
:divided="!!item.divided"
:disabled="active === item.value">
:disabled="active === item.value || !!item.disabled">
<div class="item">{{item.label}}</div>
<div v-if="tickShow" class="tick">
<i v-if="active === item.value && !item.disabled" class="taskfont">&#xe684;</i>
</div>
</EDropdownItem>
</ul>
</li>
@ -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();

View File

@ -208,8 +208,9 @@
<div class="convert-box">
<div class="convert-body">
<div class="convert-content">
<div class="convert-setting" @click="convertSetting">
<i class="taskfont">&#xe691;</i>
<div class="convert-setting">
<i class="taskfont" :class="{active: !!cacheTranscriptionLanguage}" @click="convertSetting('transcription', $event)">&#xe628;</i>
<i class="taskfont" :class="{active: !!recordConvertTranslate}" @click="convertSetting('translate', $event)">&#xe795;</i>
</div>
<div class="convert-input">
<Input
@ -390,7 +391,7 @@ export default {
recordConvertFocus: false,
recordConvertStatus: 0, // 0: 1: 2:
recordConvertResult: '',
recordConvertLanguage: '',
recordConvertTranslate: '', //
touchStart: {},
touchFocus: false,
@ -528,6 +529,7 @@ export default {
'cacheDialogs',
'dialogMsgs',
'cacheTranscriptionLanguage',
'cacheKeyboard',
'keyboardType',
'isModKey',
@ -1408,7 +1410,8 @@ export default {
dialog_id: this.dialogId,
base64: reader.result,
duration: this.recordDuration,
language: this.recordConvertLanguage || getLanguage()
language: this.cacheTranscriptionLanguage,
translate: this.recordConvertTranslate
},
method: 'post',
}).then(({data}) => {
@ -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
}))
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()
}
}

View File

@ -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 *********************************************/
/** *****************************************************************************************/

View File

@ -245,6 +245,9 @@ export default {
cacheTranslationLanguage: '',
cacheTranslations: [],
// 语音转文字(识别语言)
cacheTranscriptionLanguage: '',
// 下拉菜单操作
menuOperation: {}
};

View File

@ -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 {

View File

@ -768,11 +768,15 @@
width: 88%;
transform: translateY(12px);
.convert-setting {
margin: 0 2px 8px 0;
display: flex;
gap: 12px;
align-items: center;
> i {
color: #4d4d4d;
background-color: #c7c7c7;
opacity: 0.7;
padding: 5px;
margin: 0 2px 8px 0;
border-radius: 50%;
width: 26px;
height: 26px;
@ -780,8 +784,12 @@
justify-content: center;
align-items: center;
cursor: pointer;
> i {
font-size: 18px;
&.active {
background-color: $primary-color;
color: #ffffff;
opacity: 1
}
}
}
.convert-input {

View File

@ -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;