修复问题
This commit is contained in:
61
frontend/app/web-gold/src/hooks/web/useVoiceText.ts
Normal file
61
frontend/app/web-gold/src/hooks/web/useVoiceText.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import type {
|
||||||
|
AudioItem,
|
||||||
|
TranscriptionResult,
|
||||||
|
TranscriptionResponse,
|
||||||
|
TranscriptionData
|
||||||
|
} from '@gold/config/types'
|
||||||
|
|
||||||
|
import BaiLianService from '@/api/bailian'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音频转文本
|
||||||
|
* @param list - 音频项列表
|
||||||
|
* @returns 转录结果数组
|
||||||
|
*/
|
||||||
|
export async function getVoiceText(
|
||||||
|
list: AudioItem[]
|
||||||
|
): Promise<TranscriptionResult[]> {
|
||||||
|
const ret = await (BaiLianService as any).videoToCharacters({
|
||||||
|
fileLinkList: list.map(item => item.audio_url),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data: string = ret.data
|
||||||
|
const rst: TranscriptionResponse = JSON.parse(data)
|
||||||
|
const transcription_url: string[] = rst.results.map(item => item.transcription_url)
|
||||||
|
|
||||||
|
const transcriptions: TranscriptionResult[] = await Promise.all(
|
||||||
|
(transcription_url || []).filter(Boolean).map(async (url: string): Promise<TranscriptionResult> => {
|
||||||
|
try {
|
||||||
|
const resp: Response = await fetch(url)
|
||||||
|
const contentType: string = resp.headers.get('content-type') || ''
|
||||||
|
const value: string = contentType.includes('application/json')
|
||||||
|
? JSON.stringify(await resp.json())
|
||||||
|
: await resp.text()
|
||||||
|
const parsed: TranscriptionData = JSON.parse(value)
|
||||||
|
return {
|
||||||
|
key: url,
|
||||||
|
audio_url: parsed.file_url,
|
||||||
|
value: parsed.transcripts?.[0]?.text || ''
|
||||||
|
}
|
||||||
|
} catch (e: unknown) {
|
||||||
|
console.warn('获取转写内容失败:', url, e)
|
||||||
|
return { key: url, value: '' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return transcriptions
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseVoiceTextReturn {
|
||||||
|
getVoiceText: (list: AudioItem[]) => Promise<TranscriptionResult[]>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 语音文本转换Hook
|
||||||
|
* @returns 包含getVoiceText方法的对象
|
||||||
|
*/
|
||||||
|
export default function useVoiceText(): UseVoiceTextReturn {
|
||||||
|
return {
|
||||||
|
getVoiceText
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import { usePromptStore } from '@/stores/prompt'
|
|||||||
import MarkdownIt from 'markdown-it'
|
import MarkdownIt from 'markdown-it'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import { CommonService } from '@/api/common'
|
import { CommonService } from '@/api/common'
|
||||||
import useVoiceText from '@gold/hooks/web/useVoiceText'
|
import useVoiceText from '@/hooks/web/useVoiceText'
|
||||||
import GmIcon from '@/components/icons/Icon.vue'
|
import GmIcon from '@/components/icons/Icon.vue'
|
||||||
import { UserPromptApi } from '@/api/userPrompt'
|
import { UserPromptApi } from '@/api/userPrompt'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import { ChatMessageApi } from '@/api/chat'
|
import { ChatMessageApi } from '@/api/chat'
|
||||||
import useVoiceText from '@gold/hooks/web/useVoiceText'
|
import useVoiceText from '@/hooks/web/useVoiceText'
|
||||||
import { streamChat } from '@/utils/streamChat'
|
import { streamChat } from '@/utils/streamChat'
|
||||||
import { buildPromptFromTranscription } from '../utils/benchmarkUtils'
|
import { buildPromptFromTranscription } from '../utils/benchmarkUtils'
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
ok-text="保存"
|
ok-text="保存"
|
||||||
cancel-text="取消"
|
cancel-text="取消"
|
||||||
:confirm-loading="submitting"
|
:confirm-loading="submitting"
|
||||||
|
:ok-button-props="{ disabled: isSubmitDisabled }"
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
centered
|
centered
|
||||||
@ok="handleSubmit"
|
@ok="handleSubmit"
|
||||||
@@ -100,11 +101,21 @@
|
|||||||
<a-progress type="circle" :percent="50" :width="60" status="active" />
|
<a-progress type="circle" :percent="50" :width="60" status="active" />
|
||||||
<p class="upload-text" style="margin-top: 12px">正在上传...</p>
|
<p class="upload-text" style="margin-top: 12px">正在上传...</p>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="extractingText">
|
||||||
|
<a-progress type="circle" :percent="50" :width="60" status="active" />
|
||||||
|
<p class="upload-text" style="margin-top: 12px">正在识别语音...</p>
|
||||||
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="file-preview">
|
<div class="file-preview">
|
||||||
<SoundOutlined class="file-icon" />
|
<SoundOutlined class="file-icon" />
|
||||||
<div class="file-info">
|
<div class="file-info">
|
||||||
<span class="file-name">{{ fileList[0]?.name || '音频文件' }}</span>
|
<span class="file-name">{{ fileList[0]?.name || '音频文件' }}</span>
|
||||||
|
<span v-if="formData.text" class="text-status success">
|
||||||
|
<CheckCircleOutlined /> 已识别语音文本
|
||||||
|
</span>
|
||||||
|
<span v-else class="text-status warning">
|
||||||
|
<ExclamationCircleOutlined /> 未识别到语音文本
|
||||||
|
</span>
|
||||||
<a-button type="link" size="small" danger @click.stop="handleRemoveFile">
|
<a-button type="link" size="small" danger @click.stop="handleRemoveFile">
|
||||||
<DeleteOutlined /> 移除
|
<DeleteOutlined /> 移除
|
||||||
</a-button>
|
</a-button>
|
||||||
@@ -134,13 +145,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
import { message, Modal } from 'ant-design-vue'
|
import { message, Modal } from 'ant-design-vue'
|
||||||
import { PlusOutlined, SearchOutlined, PlayCircleOutlined, CloudUploadOutlined, SoundOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
import { PlusOutlined, SearchOutlined, PlayCircleOutlined, CloudUploadOutlined, SoundOutlined, DeleteOutlined, CheckCircleOutlined, ExclamationCircleOutlined } from '@ant-design/icons-vue'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import BasicLayout from '@/layouts/components/BasicLayout.vue'
|
import BasicLayout from '@/layouts/components/BasicLayout.vue'
|
||||||
import { MaterialService } from '@/api/material'
|
import { MaterialService } from '@/api/material'
|
||||||
import { VoiceService } from '@/api/voice'
|
import { VoiceService } from '@/api/voice'
|
||||||
import { useUpload } from '@/composables/useUpload'
|
import { useUpload } from '@/composables/useUpload'
|
||||||
import useVoiceText from '@gold/hooks/web/useVoiceText'
|
import useVoiceText from '@/hooks/web/useVoiceText'
|
||||||
|
|
||||||
// ========== 常量 ==========
|
// ========== 常量 ==========
|
||||||
|
|
||||||
@@ -169,6 +180,7 @@ const formMode = ref('create')
|
|||||||
const formRef = ref(null)
|
const formRef = ref(null)
|
||||||
const audioPlayer = ref(null)
|
const audioPlayer = ref(null)
|
||||||
const fileList = ref([])
|
const fileList = ref([])
|
||||||
|
const extractingText = ref(false) // 语音识别中状态
|
||||||
|
|
||||||
const searchParams = reactive({
|
const searchParams = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -195,6 +207,22 @@ const { getVoiceText } = useVoiceText()
|
|||||||
// ========== 计算属性 ==========
|
// ========== 计算属性 ==========
|
||||||
const isCreateMode = computed(() => formMode.value === 'create')
|
const isCreateMode = computed(() => formMode.value === 'create')
|
||||||
|
|
||||||
|
// 保存按钮是否禁用(新建模式下,文本为空或正在提取时禁用)
|
||||||
|
const isSubmitDisabled = computed(function() {
|
||||||
|
if (!isCreateMode.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// 正在提取文本时禁用
|
||||||
|
if (extractingText.value) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// 已上传文件但文本为空时禁用
|
||||||
|
if (formData.fileId && !formData.text) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
// ========== 表格配置 ==========
|
// ========== 表格配置 ==========
|
||||||
const columns = [
|
const columns = [
|
||||||
{ title: '配音名称', key: 'name', dataIndex: 'name', width: 160 },
|
{ title: '配音名称', key: 'name', dataIndex: 'name', width: 160 },
|
||||||
@@ -351,7 +379,6 @@ async function handleCustomUpload(options) {
|
|||||||
onSuccess: async function(id, fileUrl) {
|
onSuccess: async function(id, fileUrl) {
|
||||||
formData.fileId = id
|
formData.fileId = id
|
||||||
formData.fileUrl = fileUrl
|
formData.fileUrl = fileUrl
|
||||||
message.success('文件上传成功')
|
|
||||||
await fetchAudioTextById(id)
|
await fetchAudioTextById(id)
|
||||||
onSuccess?.({ code: 0, data: id }, file)
|
onSuccess?.({ code: 0, data: id }, file)
|
||||||
},
|
},
|
||||||
@@ -372,6 +399,8 @@ async function handleCustomUpload(options) {
|
|||||||
// 通过fileId获取音频文本
|
// 通过fileId获取音频文本
|
||||||
async function fetchAudioTextById(fileId) {
|
async function fetchAudioTextById(fileId) {
|
||||||
if (!fileId) return
|
if (!fileId) return
|
||||||
|
|
||||||
|
extractingText.value = true
|
||||||
try {
|
try {
|
||||||
const res = await MaterialService.getAudioPlayUrl(fileId)
|
const res = await MaterialService.getAudioPlayUrl(fileId)
|
||||||
if (res.code === 0 && res.data) {
|
if (res.code === 0 && res.data) {
|
||||||
@@ -380,18 +409,19 @@ async function fetchAudioTextById(fileId) {
|
|||||||
if (results && results.length > 0) {
|
if (results && results.length > 0) {
|
||||||
const text = results[0].value
|
const text = results[0].value
|
||||||
formData.text = text
|
formData.text = text
|
||||||
if (text) {
|
|
||||||
message.success('音频文本获取成功')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取音频文本失败:', error)
|
console.error('获取音频文本失败:', error)
|
||||||
|
message.error('语音识别失败,请重试')
|
||||||
|
} finally {
|
||||||
|
extractingText.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRemoveFile() {
|
function handleRemoveFile() {
|
||||||
formData.fileId = null
|
formData.fileId = null
|
||||||
|
formData.text = ''
|
||||||
fileList.value = []
|
fileList.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,5 +577,20 @@ onMounted(function() {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-status {
|
||||||
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
color: var(--color-success, #52c41a);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
color: var(--color-warning, #faad14);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { rewriteStream } from '@/api/forecast'
|
|||||||
import { getAgentList } from '@/api/agent'
|
import { getAgentList } from '@/api/agent'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { usePointsConfigStore } from '@/stores/pointsConfig'
|
import { usePointsConfigStore } from '@/stores/pointsConfig'
|
||||||
import { getVoiceText } from '@gold/hooks/web/useVoiceText'
|
import { getVoiceText } from '@/hooks/web/useVoiceText'
|
||||||
import { copyToClipboard } from '@/utils/clipboard'
|
import { copyToClipboard } from '@/utils/clipboard'
|
||||||
|
|
||||||
defineOptions({ name: 'ForecastView' })
|
defineOptions({ name: 'ForecastView' })
|
||||||
|
|||||||
@@ -124,10 +124,8 @@ public class MemberUserProfileServiceImpl implements MemberUserProfileService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void validateStorage(String userId, long fileSizeBytes) {
|
public void validateStorage(String userId, long fileSizeBytes) {
|
||||||
MemberUserProfileDO profile = memberUserProfileMapper.selectByUserId(userId);
|
// 档案不存在时自动创建(兼容旧用户)
|
||||||
if (profile == null) {
|
MemberUserProfileDO profile = createIfAbsent(Long.parseLong(userId));
|
||||||
throw exception(new ErrorCode(1004, "会员用户档案不存在"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将剩余存储空间(GB)转换为字节进行比较
|
// 将剩余存储空间(GB)转换为字节进行比较
|
||||||
BigDecimal remainingBytes = profile.getRemainingStorage().multiply(GB_TO_BYTES);
|
BigDecimal remainingBytes = profile.getRemainingStorage().multiply(GB_TO_BYTES);
|
||||||
@@ -138,6 +136,9 @@ public class MemberUserProfileServiceImpl implements MemberUserProfileService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean increaseUsedStorage(String userId, long fileSizeBytes) {
|
public boolean increaseUsedStorage(String userId, long fileSizeBytes) {
|
||||||
|
// 确保档案存在(兼容旧用户)
|
||||||
|
createIfAbsent(Long.parseLong(userId));
|
||||||
|
|
||||||
// 将字节转换为GB(保留6位小数)
|
// 将字节转换为GB(保留6位小数)
|
||||||
BigDecimal storageGb = new BigDecimal(fileSizeBytes).divide(GB_TO_BYTES, 6, RoundingMode.HALF_UP);
|
BigDecimal storageGb = new BigDecimal(fileSizeBytes).divide(GB_TO_BYTES, 6, RoundingMode.HALF_UP);
|
||||||
String storageGbStr = storageGb.toPlainString();
|
String storageGbStr = storageGb.toPlainString();
|
||||||
@@ -148,6 +149,9 @@ public class MemberUserProfileServiceImpl implements MemberUserProfileService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean decreaseUsedStorage(String userId, long fileSizeBytes) {
|
public boolean decreaseUsedStorage(String userId, long fileSizeBytes) {
|
||||||
|
// 确保档案存在(兼容旧用户)
|
||||||
|
createIfAbsent(Long.parseLong(userId));
|
||||||
|
|
||||||
// 将字节转换为GB(保留6位小数)
|
// 将字节转换为GB(保留6位小数)
|
||||||
BigDecimal storageGb = new BigDecimal(fileSizeBytes).divide(GB_TO_BYTES, 6, RoundingMode.HALF_UP);
|
BigDecimal storageGb = new BigDecimal(fileSizeBytes).divide(GB_TO_BYTES, 6, RoundingMode.HALF_UP);
|
||||||
String storageGbStr = storageGb.toPlainString();
|
String storageGbStr = storageGb.toPlainString();
|
||||||
|
|||||||
Reference in New Issue
Block a user