mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-15 13:22:49 +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 {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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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"></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;
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user