index.vue 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. <script setup lang="ts">
  2. import { h, onMounted, ref } from 'vue'
  3. import { NButton,NDataTable,DrawerPlacement,NDrawer,NDrawerContent,
  4. NForm,NFormItem,NInput,NDivider,NSpace,UploadFileInfo,NUpload,NUploadDragger,
  5. NText,NIcon,NP,useMessage,NModal,NSlider,NProgress
  6. } from 'naive-ui'
  7. import { getToken } from '@/store/modules/auth/helper'
  8. import { createRole,getRole,simpleGenerateReq } from '@/api/voice'
  9. import { Icon } from '@iconify/vue';
  10. import to from "await-to-js";
  11. onMounted(() => { fetchData() });
  12. const token = getToken()
  13. const message = useMessage()
  14. const audioUrl = ref()
  15. // 是否显示进度条
  16. const isPercentage = ref(false)
  17. // 定义进度值为响应式数据
  18. const percentage = ref(0);
  19. function increaseProgress() {
  20. // 打开进度条
  21. isPercentage.value = true
  22. const interval = setInterval(() => {
  23. // 只要进度小于99,就随机增加进度
  24. if (percentage.value < 99) {
  25. // 随机增加的进度值,这里假设每次增加1-5之间的随机值
  26. percentage.value += Math.floor(Math.random() * 5) + 1;
  27. // 防止进度超过99
  28. if (percentage.value > 99) {
  29. percentage.value = 99;
  30. }
  31. } else {
  32. // 达到或超过99,停止增加
  33. clearInterval(interval);
  34. }
  35. }, 1000); // 每1000毫秒(即1秒)更新一次进度
  36. }
  37. const headers = {
  38. Authorization: `Bearer ${token}`
  39. }
  40. // 初始化表单数据对象
  41. const formValue = ref({
  42. name: '',
  43. description: '',
  44. prompt: '',
  45. });
  46. // 初始化表单数据对象
  47. const simpleGenerate = ref({
  48. model: 'reecho-neural-voice-001',
  49. randomness: 97,
  50. stability_boost: 0,
  51. voiceId: '',
  52. text: ''
  53. });
  54. function handleFinish({event,file}: {
  55. file: UploadFileInfo
  56. event?: ProgressEvent
  57. }) {
  58. const ext = (event?.target as XMLHttpRequest).response
  59. // ext 转成json对象
  60. const fileData = JSON.parse(ext);
  61. formValue.value.prompt = fileData.data.url
  62. console.log("ext==================="+fileData.data.url,file.file?.size)
  63. message.success('上传成功!')
  64. }
  65. async function submitForm() {
  66. //关闭弹框
  67. active.value = false
  68. // 发起一个请求
  69. const [err, result] = await to(createRole(formValue.value));
  70. console.log("result===", result)
  71. if (err) {
  72. message.error(err.message)
  73. } else {
  74. message.success('角色创建成功!')
  75. }
  76. // 处理响应
  77. console.log('角色创建成功',formValue.value);
  78. }
  79. async function submitSimpleGenerate() {
  80. try {
  81. //打开进度条
  82. increaseProgress();
  83. //发起一个请求
  84. const [err, result] = await to(simpleGenerateReq(simpleGenerate.value));
  85. console.log("语音生成成功result===", result)
  86. if (err) {
  87. // 关闭进度条
  88. isPercentage.value = false
  89. message.error(err.message)
  90. return
  91. }else{
  92. message.success("语音生成成功,消耗点数:"+result?.data.credit_used)
  93. }
  94. audioUrl.value = result?.data.audio
  95. if(audioUrl.value){
  96. // 关闭进度条
  97. isPercentage.value = false
  98. }
  99. } catch (error) {
  100. // 处理错误
  101. console.error('语音生成失败', error);
  102. }
  103. }
  104. function handleActionButtonClick(row: any): void {
  105. simpleGenerate.value.voiceId = row.voiceId
  106. showModal.value = true
  107. }
  108. // 使用 ref 来创建响应式变量
  109. const active = ref(false)
  110. const placement = ref<DrawerPlacement>('right')
  111. const showModal = ref(false)
  112. // 定义一个激活抽屉的函数,接受一个 DrawerPlacement 类型的参数
  113. const activate = (place: DrawerPlacement) => {
  114. active.value = true
  115. placement.value = place
  116. }
  117. const createColumns = () => {
  118. return [
  119. ...(false
  120. ? [{
  121. title: '角色ID',
  122. key: 'voiceId',
  123. width: 80,
  124. ellipsis: true,
  125. }]
  126. : []),
  127. {
  128. title: '角色名称',
  129. key: 'name'
  130. },
  131. {
  132. title: '角色描述',
  133. key: 'description'
  134. },
  135. {
  136. title: '预览',
  137. key: 'fileUrl',
  138. render: (row: { fileUrl: string | undefined; }) => {
  139. return h(NButton, {
  140. icon: () =>
  141. h(NIcon, null, {
  142. default: () => h(Icon, { icon: 'ic:baseline-play-arrow' }),
  143. }),
  144. onClick: () => {
  145. const audio = new Audio(row.fileUrl);
  146. audio.play();
  147. },
  148. }, '播放');
  149. },
  150. },
  151. {
  152. title: '操作',
  153. key: 'actions',
  154. render: (row: any) => {
  155. return h(NButton, {
  156. onClick: () => handleActionButtonClick(row)
  157. }, { default: () => '生成语音' })
  158. }
  159. }
  160. ]
  161. }
  162. const fetchData = async () => {
  163. try {
  164. // 发起一个请求
  165. const [err, result] = await to(getRole());
  166. console.log("result===", result)
  167. if (err) {
  168. message.error(err.message)
  169. } else {
  170. tableData.value = result;
  171. }
  172. } catch (error) {
  173. console.error('Error fetching data:', error);
  174. }
  175. };
  176. const tableData = ref([]);
  177. const columns = ref(createColumns());
  178. </script>
  179. <template>
  180. <br>
  181. <div style="display: flex; justify-content: flex-start; margin:10px;">
  182. <n-button @click="activate('right')" type="primary">
  183. 创建角色
  184. </n-button>
  185. </div>
  186. <div class="flex h-full">
  187. <main class="flex-1 overflow-hidden h-full">
  188. <n-data-table :columns="columns" :data="tableData" />
  189. </main>
  190. </div>
  191. <n-drawer v-model:show="active" :width="502" :placement="placement">
  192. <n-drawer-content title="添加角色">
  193. 在这里添加你的角色
  194. <n-divider />
  195. <n-space vertical>
  196. <n-form ref="formRef" >
  197. <n-form-item
  198. label="角色名称" path="formValue.name">
  199. <n-input v-model:value="formValue.name" placeholder="输入角色名称" />
  200. </n-form-item>
  201. <n-form-item label="角色描述" path="formValue.description">
  202. <n-input v-model:value="formValue.description" placeholder="输入角色描述" />
  203. </n-form-item>
  204. <n-form-item label="音频样本">
  205. <!-- @before-upload="beforeUpload" -->
  206. <n-upload
  207. directory-dnd
  208. action="/api/resource/oss/upload"
  209. name="file"
  210. :headers="headers"
  211. @finish="handleFinish"
  212. :max="1">
  213. <n-upload-dragger>
  214. <div style="margin-bottom: 12px">
  215. <n-icon size="48" :depth="3">
  216. <archive-icon />
  217. </n-icon>
  218. </div>
  219. <n-text style="font-size: 16px">
  220. 请上传一个5MB以内的音频文件
  221. </n-text>
  222. <n-p depth="3" style="margin: 8px 0 0 0">
  223. *样本语音需大于2秒,样本质量比长度更重要,过长的样本反而可能导致效果不如预期,通常将样本长度控制在5-8秒的最具代表性片段即可。
  224. </n-p>
  225. </n-upload-dragger>
  226. </n-upload>
  227. </n-form-item>
  228. <n-col :span="24"
  229. >
  230. <div style="display: flex; justify-content: flex-end">
  231. <n-button @click="submitForm" type="primary">
  232. 添加
  233. </n-button>
  234. </div>
  235. </n-col>
  236. </n-form>
  237. </n-space>
  238. </n-drawer-content>
  239. </n-drawer>
  240. <n-modal v-model:show="showModal" title="生成语音" :auto-focus="false" preset="card"
  241. style="width: 95%; max-width: 500px;">
  242. <n-input maxlength="1000"
  243. type="textarea"
  244. v-model:value="simpleGenerate.text"
  245. placeholder="建议填写不超过50个字符的单句以保证最佳效果(消耗1元/1000字符)"
  246. />
  247. <n-space vertical>
  248. <br>
  249. <section class=" flex justify-between items-center" >
  250. <div> 多样性 (0-100,默认为97)
  251. </div>
  252. <div class=" flex justify-end items-center w-[80%] max-w-[240px]">
  253. <div class=" w-[200px]"><n-slider v-model:value="simpleGenerate.randomness" :step="1" :max="100" /></div>
  254. <div class="w-[40px] text-right">{{ simpleGenerate.randomness }}</div>
  255. </div>
  256. </section>
  257. <div class="mb-4 text-[12px] text-gray-300 dark:text-gray-300/20">控制语音生成的多样性,值越大,生成的语音将具备更高的表现力上限与随机性范围;应为0到100的整数,建议不小于95</div>
  258. <section class=" flex justify-between items-center" >
  259. <div> 稳定性过滤 (0-100,默认为0)
  260. </div>
  261. <div class=" flex justify-end items-center w-[80%] max-w-[240px]">
  262. <div class=" w-[200px]"><n-slider v-model:value="simpleGenerate.stability_boost" :step="1" :max="100" /></div>
  263. <div class="w-[40px] text-right">{{ simpleGenerate.stability_boost }}</div>
  264. </div>
  265. </section>
  266. <div class="mb-4 text-[12px] text-gray-300 dark:text-gray-300/20">限制在生成过程中仅选择前n个最佳的路径,值越小,生成的语音通常越平淡与稳定,但同时也可能导致表达某些内容或音色时的效果下降或异常;应为1到100的整数,为0时关闭限制,启用时建议不小于40</div>
  267. <!-- 进度条 -->
  268. <n-progress v-if="isPercentage" :percentage="percentage"></n-progress>
  269. <audio v-if="audioUrl" :src="audioUrl" controls></audio>
  270. </n-space>
  271. <br>
  272. <div style="display: flex; justify-content: flex-end">
  273. <n-button @click="submitSimpleGenerate" type="primary">
  274. 开始生成
  275. </n-button>
  276. </div>
  277. </n-modal>
  278. </template>