mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-11 18:42:54 +00:00
perf: 录音转文字支持自定义语言
This commit is contained in:
parent
e53b65496f
commit
e34aa77a54
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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"></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();
|
||||
|
||||
@ -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"></i>
|
||||
<div class="convert-setting">
|
||||
<i class="taskfont" :class="{active: !!cacheTranscriptionLanguage}" @click="convertSetting('transcription', $event)"></i>
|
||||
<i class="taskfont" :class="{active: !!recordConvertTranslate}" @click="convertSetting('translate', $event)"></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()
|
||||
}
|
||||
}
|
||||
|
||||
19
resources/assets/js/store/actions.js
vendored
19
resources/assets/js/store/actions.js
vendored
@ -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 *********************************************/
|
||||
/** *****************************************************************************************/
|
||||
|
||||
3
resources/assets/js/store/state.js
vendored
3
resources/assets/js/store/state.js
vendored
@ -245,6 +245,9 @@ export default {
|
||||
cacheTranslationLanguage: '',
|
||||
cacheTranslations: [],
|
||||
|
||||
// 语音转文字(识别语言)
|
||||
cacheTranscriptionLanguage: '',
|
||||
|
||||
// 下拉菜单操作
|
||||
menuOperation: {}
|
||||
};
|
||||
|
||||
11
resources/assets/sass/dark.scss
vendored
11
resources/assets/sass/dark.scss
vendored
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user