mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-13 03:52:50 +00:00
perf: 录音转文字支持自定义语言
This commit is contained in:
parent
f6ee630615
commit
e53b65496f
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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"></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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user