Xin Zhang commited on
Commit
b1cc7ae
·
1 Parent(s): 5873ede

[feature]: add frontend src.

Browse files
frontend/.gitattributes ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.mp4 filter=lfs diff=lfs merge=lfs -text
frontend/.gitignore ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ www
14
+ www-ssr
15
+ *.local
16
+
17
+ # Editor directories and files
18
+ .vscode
19
+ !.vscode/extensions.json
20
+ .idea
21
+ .DS_Store
22
+ *.suo
23
+ *.ntvs*
24
+ *.njsproj
25
+ *.sln
26
+ *.sw?
frontend/README.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ ---
4
+
5
+ This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
6
+
7
+ ## Recommended IDE Setup
8
+
9
+ - [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
10
+
11
+ ## Type Support For `.vue` Imports in TS
12
+
13
+ TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
14
+
15
+ If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
16
+
17
+ 1. Disable the built-in TypeScript Extension
18
+ 1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
19
+ 2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
20
+ 2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
21
+
22
+
23
+ ## 当前项目是一个独立的纯前端项目,按照以下流程运行
24
+
25
+ 1. 安装依赖
26
+ ```bash
27
+ yarn install
28
+ ```
29
+ 2. 启动项目(本地开发测试,后端api配置在config/client_config.ts中)
30
+ ```bash
31
+ yarn dev
32
+ ```
33
+ 3. 打包项目 (打包后的项目在当前目录的www下,cp他们到python的web目录下)
34
+ ```bash
35
+ yarn build
36
+ ```
frontend/index.html CHANGED
@@ -2,14 +2,12 @@
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="./favicon.ico" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <title>Translator</title>
8
- <script type="module" crossorigin src="./assets/index-fc3a0f87.js"></script>
9
- <link rel="stylesheet" href="./assets/index-b1f15c01.css">
10
  </head>
11
  <body>
12
  <div id="app"></div>
13
-
14
  </body>
15
  </html>
 
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.ico" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <title>Translator</title>
 
 
8
  </head>
9
  <body>
10
  <div id="app"></div>
11
+ <script type="module" src="/src/main.ts"></script>
12
  </body>
13
  </html>
frontend/package.json ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "RealTime-Translator",
3
+ "private": true,
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": " vite build",
9
+ "preview": "vite preview",
10
+ "generate-client": "openapi --input api.json --output ./src/client --client axios",
11
+ "update-api": "wget http://localhost:8086/api/openapi.json -O api.json && yarn generate-client"
12
+ },
13
+ "dependencies": {
14
+ "@ricky0123/vad-web": "^0.0.18",
15
+ "@types/node": "^20.6.2",
16
+ "ant-design-vue": "4.x",
17
+ "axios": "^1.6.0",
18
+ "pinia": "^2.1.6",
19
+ "pinia-plugin-persist": "^1.0.0",
20
+ "pinia-plugin-persistedstate": "^3.2.0",
21
+ "qs": "^6.11.2",
22
+ "tools-javascript": "^1.0.26",
23
+ "tools-vue3": "^1.1.5",
24
+ "vue": "3.3.4",
25
+ "vue-i18n": "^9.10.1",
26
+ "vue-router": "4",
27
+ "web-storage-cache": "^1.1.1"
28
+ },
29
+ "devDependencies": {
30
+ "@types/spark-md5": "^3.0.4",
31
+ "@vitejs/plugin-vue": "^4.2.3",
32
+ "autoprefixer": "^10.4.16",
33
+ "openapi-typescript-codegen": "^0.25.0",
34
+ "postcss": "^8.4.31",
35
+ "sass": "^1.68.0",
36
+ "tailwindcss": "^3.3.5",
37
+ "typescript": "^5.0.2",
38
+ "vite": "^4.4.5",
39
+ "vite-plugin-static-copy": "^1.0.1",
40
+ "vue-tsc": "^1.8.5"
41
+ }
42
+ }
frontend/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
frontend/{favicon.ico → public/favicon.ico} RENAMED
File without changes
frontend/src/App.vue ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <!-- <Header/> -->
3
+ <router-view class="content"/>
4
+ <!-- <Footer/> -->
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import Header from "@/views/Header.vue";
9
+ import Footer from "@/views/Footer.vue";
10
+
11
+ // import * as api from "@/client";
12
+ import {onBeforeMount, onMounted,watch, provide, ref} from "vue";
13
+ import {useSettingsStore} from "@/stores/config.ts";
14
+
15
+ import axios from "axios";
16
+ import {getRandomNumInt} from "@/utils/size.ts";
17
+
18
+
19
+ const base_url = axios.defaults.baseURL
20
+
21
+ // const registerSession = async () => {
22
+ // console.log('register ...')
23
+ // const role = settingsStore.$state.role_name
24
+
25
+ // const response = await fetch(`${base_url}/register?role=${role}`)
26
+ // const res = await response.json()
27
+ // console.log('res: ', res)
28
+ // return res['session_id']
29
+ // }
30
+
31
+ // watch(() => settingsStore.$state.role_name, async (role_name: any) => {
32
+ // console.log('>>>>> role updated', role_name)
33
+ // let session_id = await registerSession()
34
+ // if (!session_id) {
35
+ // console.log('register session failed')
36
+ // session_id = getRandomNumInt(100000, 999999)
37
+ // }
38
+ // // @ts-ignore
39
+ // sessionsStore.$patch({current_session_id: session_id + ''})
40
+ // console.log('session id: ', sessionsStore.$state.current_session_id)
41
+ // })
42
+
43
+ onMounted(async () => {
44
+ // console.log('app mounted', settingsStore.$state)
45
+
46
+ // let session_id = await registerSession()
47
+ // if (!session_id) {
48
+ // console.log('register session failed')
49
+ // session_id = getRandomNumInt(100000, 999999)
50
+ // }
51
+ // // @ts-ignore
52
+ // sessionsStore.$patch({current_session_id: session_id + ''})
53
+ // console.log('session id: ', sessionsStore.$state.current_session_id)
54
+ })
55
+
56
+ </script>
57
+
58
+ <style scoped>
59
+ .content {
60
+ background-color: white;
61
+ max-width: 1280px;
62
+ min-height: 720px;
63
+ margin: 0 auto;
64
+ display: flex;
65
+ flex-direction: column;
66
+ align-items: center;
67
+ justify-content: space-between;
68
+ }
69
+ </style>
frontend/src/config/axios/config.ts ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ const config: {
4
+ base_url: {
5
+ base: string
6
+ dev: string
7
+ pro: string
8
+ test: string
9
+ }
10
+ result_code: number | string
11
+ default_headers: AxiosHeaders
12
+ request_timeout: number
13
+ } = {
14
+ /**
15
+ * api请求基础路径
16
+ */
17
+ base_url: {
18
+ // 开发环境接口前缀
19
+ base: '',
20
+
21
+ // 打包开发环境接口前缀
22
+ dev: '',
23
+
24
+ // 打包生产环境接口前缀
25
+ pro: '',
26
+
27
+ // 打包测试环境接口前缀
28
+ test: ''
29
+ },
30
+
31
+ /**
32
+ * 接口成功返回状态码
33
+ */
34
+ result_code: '0000',
35
+
36
+ /**
37
+ * 接口请求超时时间
38
+ */
39
+ request_timeout: 60000,
40
+
41
+ /**
42
+ * 默认接口请求类型
43
+ * 可选值:application/x-www-form-urlencoded multipart/form-data
44
+ */
45
+ default_headers: 'application/json'
46
+ }
47
+
48
+ export { config }
frontend/src/config/axios/index.ts ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios, {
2
+ AxiosInstance,
3
+ AxiosRequestConfig,
4
+ AxiosRequestHeaders,
5
+ AxiosResponse,
6
+ AxiosError
7
+ } from 'axios'
8
+
9
+ import { notification } from 'ant-design-vue';
10
+
11
+ import qs from 'qs'
12
+
13
+ import { config } from '@/config/axios/config'
14
+
15
+ const { result_code, base_url } = config
16
+
17
+ // export const PATH_URL = base_url[import.meta.env.VITE_API_BASEPATH]
18
+
19
+ // export const PATH_URL = 'localhost:8000/'
20
+ export const PATH_URL = '/'
21
+
22
+ // 创建axios实例
23
+ const service: AxiosInstance = axios.create({
24
+ baseURL: PATH_URL, // api 的 base_url
25
+ timeout: config.request_timeout // 请求超时时间
26
+ })
27
+
28
+ // request拦截器
29
+ service.interceptors.request.use(
30
+ (config: any ) => {
31
+ if (
32
+ config.method === 'post' &&
33
+ (config.headers as AxiosRequestHeaders)['Content-Type'] ===
34
+ 'application/x-www-form-urlencoded'
35
+ ) {
36
+ config.data = qs.stringify(config.data)
37
+ }
38
+ // get参数编码
39
+ if (config.method === 'get' && config.params) {
40
+ let url = config.url as string
41
+ url += '?'
42
+ const keys = Object.keys(config.params)
43
+ for (const key of keys) {
44
+ if (config.params[key] !== void 0 && config.params[key] !== null) {
45
+ url += `${key}=${encodeURIComponent(config.params[key])}&`
46
+ }
47
+ }
48
+ url = url.substring(0, url.length - 1)
49
+ config.params = {}
50
+ config.url = url
51
+ }
52
+ return config
53
+ },
54
+ (error: AxiosError) => {
55
+ // Do something with request error
56
+ Promise.reject(error)
57
+ }
58
+ )
59
+
60
+ service.interceptors.response.use(
61
+ (response: any) => {
62
+ if (response.data.code === result_code) {
63
+ return response.data
64
+ } else {
65
+ notification.error(response.data.message)
66
+ }
67
+ },
68
+ (error: AxiosError) => {
69
+ console.log('err' + error) // for debug
70
+ notification.error(error)
71
+ return Promise.reject(error)
72
+ }
73
+ )
74
+
75
+ export { service }
frontend/src/config/client_config.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios, {AxiosResponse} from "axios";
2
+ import {useCache} from "@/hooks/useCache";
3
+ import {toggleError} from '@/hooks/showError'
4
+ import router from "@/router";
5
+
6
+ const { wsCache } = useCache();
7
+
8
+ export const test_server = '127.0.0.1:9191'
9
+ // export const test_server = '59.110.18.232:19001'
10
+
11
+ axios.defaults.baseURL = import.meta.env.PROD ? '/api' : `http://${test_server}/api`;
12
+ axios.interceptors.request.use(
13
+ (config: any) => {
14
+ // Do something before request is sent
15
+ const {wsCache} = useCache()
16
+ const token = wsCache.get('token')
17
+ if (token) {
18
+ //@ts-ignore
19
+ config.headers.Authorization = 'Bearer ' + token
20
+ }
21
+ return config;
22
+ }
23
+ )
frontend/src/hooks/showError.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ import { reactive} from 'vue'
2
+ const toggleError = reactive({show:false, title:'error',msg:'Failed to connect to server'})
3
+ export {toggleError}
frontend/src/hooks/useCache.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * 配置浏览器本地存储的方式,可直接存储对象数组。
3
+ */
4
+
5
+ import WebStorageCache from 'web-storage-cache'
6
+
7
+ type CacheType = 'sessionStorage' | 'localStorage'
8
+
9
+ export const useCache = (type: CacheType = 'sessionStorage') => {
10
+ const wsCache: WebStorageCache = new WebStorageCache({
11
+ storage: type
12
+ })
13
+
14
+ return {
15
+ wsCache
16
+ }
17
+ }
frontend/src/index.d.ts ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /// <reference types="vite/client" />
2
+ declare module "*.txt" {
3
+ const content: string;
4
+ export default content;
5
+ }
6
+ declare module '*.vue' {
7
+ import { DefineComponent } from 'vue'
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
9
+ const component: DefineComponent<{}, {}, any>
10
+ export default component
11
+ }
12
+
13
+ declare module 'qs';
14
+
15
+ declare type Recordable<T = any, K = string> = Record<K extends null | undefined ? string : K, T>
16
+
17
+ declare type AxiosHeaders =
18
+ | 'application/json'
19
+ | 'application/x-www-form-urlencoded'
20
+ | 'multipart/form-data'
21
+
22
+ declare type AxiosMethod = 'get' | 'post' | 'delete' | 'put'
23
+
24
+ declare type AxiosResponseType = 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'
25
+
26
+ declare type AxiosConfig = {
27
+ params?: any
28
+ data?: any
29
+ url?: string
30
+ method?: AxiosMethod
31
+ headersType?: string
32
+ responseType?: AxiosResponseType
33
+ }
34
+
frontend/src/main.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createApp } from 'vue'
2
+ import Antd from 'ant-design-vue';
3
+ import './config/client_config'
4
+ import { createPinia } from 'pinia';
5
+ import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
6
+ import 'ant-design-vue/dist/reset.css';
7
+ import './style.scss'
8
+
9
+ import App from './App.vue'
10
+ import router from './router'
11
+
12
+
13
+ // import * as Sentry from "@sentry/browser";
14
+ //
15
+ // Sentry.init({
16
+ // dsn: "http://1f5e3e8958a24528b5030068902e177e@127.0.0.1:8543/7",
17
+ // debug: true,
18
+ // });
19
+
20
+ const pinia = createPinia();
21
+ pinia.use(piniaPluginPersistedstate);
22
+
23
+
24
+ createApp(App)
25
+ .use(pinia)
26
+ .use(router)
27
+ .use(Antd)
28
+ .mount('#app')
frontend/src/router.ts ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
2
+
3
+ import NotFoundVue from '@/views/404/index.vue';
4
+ import WelcomeVue from '@/views/Home/index.vue';
5
+ import SettingsVue from '@/views/Settings/index.vue';
6
+
7
+ const routes: Array<RouteRecordRaw> = [
8
+ {
9
+ name:"home",
10
+ path: '/',
11
+ component: WelcomeVue,
12
+ meta: {
13
+ requiresAgreement: false,
14
+ }
15
+ },
16
+ {
17
+ name:"settings",
18
+ path:'/settings',
19
+ component: SettingsVue,
20
+ },
21
+ {
22
+ name:"404",
23
+ path:'/404',
24
+ component: NotFoundVue,
25
+ }
26
+ ];
27
+
28
+ const router = createRouter({
29
+ // history: createWebHistory(),
30
+ history: createWebHistory('/app/'),
31
+ routes,
32
+ });
33
+
34
+ router.beforeEach((to, from, next) => {
35
+ console.log('=============== router to : ', to)
36
+ if (to.matched.length === 0) {
37
+ next({ name: '404' });
38
+ } else {
39
+ next();
40
+ }
41
+ });
42
+
43
+ export default router;
frontend/src/stores/config.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { defineStore } from 'pinia';
3
+
4
+
5
+ export const useSettingsStore = defineStore({
6
+ id: 'settings',
7
+ persist: true,
8
+ state: () => {
9
+ return {
10
+ vad: 0.3,
11
+ fs: 'trans-font-size-18',
12
+ width_max: false,
13
+ role_name: 'assistant',
14
+ file_type: 'wav',
15
+ }
16
+ },
17
+ actions: {
18
+ }
19
+ });
20
+
frontend/src/stores/measure.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineStore } from 'pinia';
2
+
3
+ export const useMeasureStore = defineStore({
4
+ id: 'measure_store',
5
+ persist: false,
6
+ state: () => {
7
+ return {
8
+ first_request_time: 0,
9
+ first_response_time: 0,
10
+ }
11
+ },
12
+ actions: {
13
+ }
14
+ });
15
+
frontend/src/stores/session.ts ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineStore } from 'pinia';
2
+ import { ref, computed } from 'vue';
3
+
4
+ // 定义会话中单个节点的结构 (如果比纯文本更复杂)
5
+ export interface SessionNode {
6
+ id: string; // 或者其他唯一标识符
7
+ text: string;
8
+ translatedText?: string; // 可选的翻译文本
9
+ timestamp: number; // 时间戳
10
+ // 可以添加其他元数据,如语言、说话人等
11
+ }
12
+
13
+ // 定义存储在 Pinia 和用于 Modal 列表的会话摘要结构
14
+ export interface SessionSummary {
15
+ startTime: number; // 作为唯一 ID 和排序依据
16
+ title: string; // 第一句话的前10个字
17
+ outline: string[]; // 前两行内容
18
+ nodeCount: number; // 会话中的节点总数
19
+ }
20
+
21
+ const LOCAL_STORAGE_SESSION_PREFIX = 'rt_session_'; // 本地存储键前缀
22
+
23
+ export const useSessionStore = defineStore('session', () => {
24
+ // --- State ---
25
+
26
+ // 会话摘要列表,将由 pinia-plugin-persistedstate 自动持久化
27
+ const sessionSummaries = ref<SessionSummary[]>([]);
28
+
29
+ // 当前活动会话的节点 (不持久化)
30
+ const currentSessionNodes = ref<SessionNode[]>([]);
31
+ // 当前活动会话的开始时间 (不持久化)
32
+ const currentSessionStartTime = ref<number | null>(null);
33
+ // 标记会话是否正在进行中 (不持久化)
34
+ const isSessionActive = ref(false);
35
+
36
+ // --- Getters ---
37
+
38
+ // 按开始时间降序排列的会话摘要
39
+ const sortedSessionSummaries = computed(() => {
40
+ // 创建副本进行排序,避免直接修改响应式 ref
41
+ return [...sessionSummaries.value].sort((a, b) => b.startTime - a.startTime);
42
+ });
43
+
44
+ // --- Actions ---
45
+
46
+ /**
47
+ * 开始一个新的会话
48
+ */
49
+ function startSession() {
50
+ if (isSessionActive.value) {
51
+ console.warn("尝试在已有活动会话时开始新会话。");
52
+ // 可以选择结束旧会话或直接返回
53
+ // endSession(); // 如果需要自动结束旧会话
54
+ return;
55
+ }
56
+ currentSessionStartTime.value = Date.now();
57
+ currentSessionNodes.value = [];
58
+ isSessionActive.value = true;
59
+ console.log(`新会话开始于: ${new Date(currentSessionStartTime.value).toLocaleString()}`);
60
+ }
61
+
62
+ /**
63
+ * 向当前活动会话添加一个节点
64
+ * @param node - 要添加的会话节点
65
+ */
66
+ function addNode(node: SessionNode) {
67
+ if (!isSessionActive.value || !currentSessionStartTime.value) {
68
+ console.warn("没有活动的会话来添加节点。");
69
+ return;
70
+ }
71
+ currentSessionNodes.value.push(node);
72
+ // 可选:如果需要更强的容错性,可以在这里进行增量保存到 localStorage
73
+ // saveCurrentSessionToLocalStorage();
74
+ }
75
+
76
+ /**
77
+ * 结束当前活动会话,保存完整内容到 localStorage,并更新摘要列表
78
+ */
79
+ function endSession() {
80
+ if (!isSessionActive.value || !currentSessionStartTime.value) {
81
+ console.log("没有活动的会话可以结束。");
82
+ // 确保状态被重置
83
+ isSessionActive.value = false;
84
+ currentSessionStartTime.value = null;
85
+ currentSessionNodes.value = [];
86
+ return;
87
+ }
88
+
89
+ const startTime = currentSessionStartTime.value;
90
+ const nodes = [...currentSessionNodes.value]; // 创建副本
91
+
92
+ // 重置当前会话状态
93
+ isSessionActive.value = false;
94
+ currentSessionStartTime.value = null;
95
+ currentSessionNodes.value = [];
96
+
97
+ if (nodes.length === 0) {
98
+ console.log("会话结束,但没有节点需要保存。");
99
+ return;
100
+ }
101
+
102
+ // 1. 生成摘要信息
103
+ const title = nodes[0]?.text.substring(0, 10) || '无标题会话';
104
+ const n1 = nodes[0];
105
+ const outline = [
106
+ `${n1?.text.substring(0, 56)}...\n`,
107
+ `${n1?.translatedText?.substring(0, 56)}...\n`,
108
+ ]
109
+ // `${n1?.text.substring(0, 56)}\n${'-'.repeat(60)}\n${n1.translatedText?.substring(0, 56)}\n`
110
+ const summary: SessionSummary = {
111
+ startTime,
112
+ title,
113
+ outline,
114
+ nodeCount: nodes.length,
115
+ };
116
+
117
+ // 2. 保存完整会话内容到 Local Storage
118
+ try {
119
+ const storageKey = `${LOCAL_STORAGE_SESSION_PREFIX}${startTime}`;
120
+ localStorage.setItem(storageKey, JSON.stringify(nodes));
121
+ console.log(`完整会话 ${startTime} 已保存到 localStorage.`);
122
+
123
+ // 3. 更新 Pinia 中的摘要列表
124
+ // 检查是否已存在相同 startTime 的摘要 (理论上不应发生,除非手动操作或错误)
125
+ const existingIndex = sessionSummaries.value.findIndex(s => s.startTime === startTime);
126
+ if (existingIndex === -1) {
127
+ sessionSummaries.value.push(summary);
128
+ } else {
129
+ console.warn(`会话摘要 ${startTime} 已存在,将进行覆盖��`);
130
+ sessionSummaries.value[existingIndex] = summary;
131
+ }
132
+ // pinia-plugin-persistedstate 会自动处理 sessionSummaries 的持久化
133
+
134
+ console.log(`会话 ${startTime} 结束并已处理。`);
135
+
136
+ } catch (error) {
137
+ console.error("保存会话到 localStorage 时出错:", error);
138
+ // 这里可以添加用户反馈,例如提示存储空间不足
139
+ // 也许需要决定是否回滚摘要列表的添加
140
+ }
141
+ }
142
+
143
+ /**
144
+ * 从 Local Storage 加载指定会话的完整内容
145
+ * @param startTime - 会话的开始时间戳 (作为 ID)
146
+ * @returns SessionNode[] | null - 会话节点数组或在未找到/出错时返回 null
147
+ */
148
+ function loadSessionContent(startTime: number): SessionNode[] | null {
149
+ try {
150
+ const storageKey = `${LOCAL_STORAGE_SESSION_PREFIX}${startTime}`;
151
+ const storedData = localStorage.getItem(storageKey);
152
+ if (storedData) {
153
+ const nodes = JSON.parse(storedData) as SessionNode[];
154
+ console.log(`从 localStorage 加载了会话 ${startTime} 的内容 (${nodes.length} 个节点)`);
155
+ return nodes;
156
+ }
157
+ console.warn(`在 localStorage 中未找到键为 ${storageKey} 的会话数据。`);
158
+ return null;
159
+ } catch (error) {
160
+ console.error(`从 localStorage 加载会话 ${startTime} 时出错:`, error);
161
+ return null;
162
+ }
163
+ }
164
+
165
+ /**
166
+ * 删除指定的会话 (包括摘要和本地存储的完整内容)
167
+ * @param startTime - 要删除的会话的开始时间戳
168
+ */
169
+ function deleteSession(startTime: number) {
170
+ try {
171
+ // 1. 从摘要列表中移除
172
+ const index = sessionSummaries.value.findIndex(s => s.startTime === startTime);
173
+ if (index > -1) {
174
+ sessionSummaries.value.splice(index, 1);
175
+ console.log(`会话摘要 ${startTime} 已从 Pinia store 中移除。`);
176
+ // pinia-plugin-persistedstate 会自动更新持久化的摘要列表
177
+ } else {
178
+ console.warn(`尝试删除一个不存在的会话摘要: ${startTime}`);
179
+ }
180
+
181
+ // 2. 从 Local Storage 中移除完整内容
182
+ const storageKey = `${LOCAL_STORAGE_SESSION_PREFIX}${startTime}`;
183
+ localStorage.removeItem(storageKey);
184
+ console.log(`会话 ${startTime} 的完整内容已从 localStorage 中移除。`);
185
+
186
+ } catch (error) {
187
+ console.error(`删除会话 ${startTime} 时出错:`, error);
188
+ }
189
+ }
190
+
191
+ // --- 返回 State, Getters, Actions ---
192
+ return {
193
+ // State
194
+ sessionSummaries, // 摘要列表 (将被持久化)
195
+ currentSessionNodes, // 当前活动会话的节点 (用于可能的实时显示)
196
+ currentSessionStartTime, // 当前活动会话的开始时间
197
+ isSessionActive, // 会话是否活动
198
+
199
+ // Getters
200
+ sortedSessionSummaries, // 排序后的摘要列表
201
+
202
+ // Actions
203
+ startSession,
204
+ addNode,
205
+ endSession,
206
+ loadSessionContent, // 用于下载按钮点击时加载数据
207
+ deleteSession,
208
+ };
209
+ }, {
210
+ // Pinia 持久化配置
211
+ persist: {
212
+ // 只持久化 sessionSummaries 状态
213
+ paths: ['sessionSummaries'],
214
+ // 默认使用 localStorage,如果需要可以指定
215
+ // storage: localStorage,
216
+ },
217
+ });
218
+
219
+ /**
220
+ * 辅助函数:触发浏览器下载会话数据
221
+ * @param startTime - 会话开始时间,用于文件名
222
+ * @param nodes - 要下载的会话节点数据
223
+ * @param format - 'json' 或 'txt' (默认为 'json')
224
+ */
225
+ export function downloadSessionData(startTime: number, nodes: SessionNode[], format: 'json' | 'txt' = 'json') {
226
+ if (!nodes || nodes.length === 0) {
227
+ console.error("没有数据可供下载:", startTime);
228
+ alert("没有内容可以下载。"); // 给用户反馈
229
+ return;
230
+ }
231
+ try {
232
+ const dateStr = new Date(startTime).toISOString().split('T')[0]; // YYYY-MM-DD
233
+ let dataStr: string;
234
+ let mimeType: string;
235
+ let fileExtension: string;
236
+
237
+ if (format === 'txt') {
238
+ dataStr = nodes.map(n => `${new Date(n.timestamp).toLocaleTimeString()} - ${n.text}`).join('\n');
239
+ mimeType = 'text/plain;charset=utf-8;';
240
+ fileExtension = 'txt';
241
+ } else { // 默认为 json
242
+ dataStr = JSON.stringify(nodes, null, 2); // 美化 JSON 输出
243
+ mimeType = 'application/json;charset=utf-8;';
244
+ fileExtension = 'json';
245
+ }
246
+
247
+ const filename = `session_${dateStr}_${startTime}.${fileExtension}`;
248
+ const blob = new Blob([dataStr], { type: mimeType });
249
+ const link = document.createElement("a");
250
+
251
+ // 使用 createObjectURL 创建一个临时的 URL 指向 Blob 对象
252
+ const url = URL.createObjectURL(blob);
253
+ link.setAttribute("href", url);
254
+ link.setAttribute("download", filename);
255
+ link.style.visibility = 'hidden';
256
+ document.body.appendChild(link);
257
+ link.click(); // 模拟点击下载链接
258
+
259
+ // 清理:移除链接并释放 URL 对象
260
+ document.body.removeChild(link);
261
+ URL.revokeObjectURL(url);
262
+
263
+ console.log(`已触发下载会话 ${startTime} 为 ${filename}`);
264
+
265
+ } catch (error) {
266
+ console.error(`下载会话 ${startTime} 时出错:`, error);
267
+ alert("下载文件时发生错误。"); // 给用户反馈
268
+ }
269
+ }
frontend/src/style.scss ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3
+ line-height: 1.5;
4
+ font-weight: 400;
5
+
6
+ color-scheme: light dark;
7
+ color: rgba(255, 255, 255, 0.87);
8
+ background-color: #242424;
9
+
10
+ font-synthesis: none;
11
+ text-rendering: optimizeLegibility;
12
+ -webkit-font-smoothing: antialiased;
13
+ -moz-osx-font-smoothing: grayscale;
14
+ -webkit-text-size-adjust: 100%;
15
+ }
16
+
17
+ a {
18
+ font-weight: 500;
19
+ color: #646cff;
20
+ text-decoration: inherit;
21
+ }
22
+ a:hover {
23
+ color: #535bf2;
24
+ }
25
+
26
+ body {
27
+ margin: 0;
28
+ display: flex;
29
+ place-items: center;
30
+ min-width: 320px;
31
+ height: auto;
32
+ min-height: auto;
33
+ color: #333;
34
+ background: #fff;
35
+ }
36
+
37
+ h1 {
38
+ font-size: 3.2em;
39
+ line-height: 1.1;
40
+ }
41
+
42
+ button {
43
+ border-radius: 8px;
44
+ border: 1px solid transparent;
45
+ padding: 0.6em 1.2em;
46
+ font-size: 1em;
47
+ font-weight: 500;
48
+ font-family: inherit;
49
+ background-color: #1a1a1a;
50
+ cursor: pointer;
51
+ transition: border-color 0.25s;
52
+ }
53
+ //button:hover {
54
+ // border-color: #646cff;
55
+ //}
56
+ //button:focus,
57
+ //button:focus-visible {
58
+ // outline: 4px auto -webkit-focus-ring-color;
59
+ //}
60
+
61
+
62
+ $FormMaxWidth: 1024px;
63
+ $FormItemWidth: 1022px;
64
+
65
+ .card {
66
+ border-bottom: solid 2px lightgray;
67
+ //border-radius: 4px;
68
+ align-items: center;
69
+ justify-content: center;
70
+ /* padding: 2em; */
71
+ margin-top: 40px;
72
+ display: flex;
73
+ max-width: $FormMaxWidth;
74
+ width: 100%;
75
+ }
76
+
77
+ .seg-title {
78
+ margin: 24px 0;
79
+ font-size: 20px;
80
+ font-weight: 500;
81
+ }
82
+
83
+ .seg-co {
84
+ width: 1022px;
85
+ text-align: left;
86
+ border-left: solid 6px midnightblue;
87
+ padding-left: 8px;
88
+ margin-left: 2px;
89
+ margin-top: 36px;
90
+ line-height: 24px;
91
+ }
92
+
93
+ #app {
94
+ margin: 0 auto;
95
+ padding: 0 ;
96
+ text-align: center;
97
+ width: 100%;
98
+ }
99
+
100
+ .ant-btn {
101
+ padding: 4px 12px;
102
+ }
103
+
104
+ @media (prefers-color-scheme: light) {
105
+ :root {
106
+ color: #213547;
107
+ background-color: #ffffff;
108
+ }
109
+ a:hover {
110
+ color: #747bff;
111
+ }
112
+ button {
113
+ background-color: #f9f9f9;
114
+ }
115
+ }
116
+
117
+ .ant-card {
118
+ background: #f5f6fa;
119
+ }
120
+
121
+ .ant-card .ant-card-actions {
122
+ background-color: rgba(232, 232, 248, 0.8) !important;
123
+ }
124
+
125
+ .ant-popover {
126
+ max-width: 800px !important;
127
+ }
128
+
129
+ .ant-form-item {
130
+ background: transparent;
131
+ margin-bottom: 40px !important;
132
+ .ant-form-item-explain-error {
133
+ color: #ff4d4f;
134
+ text-align: left !important;
135
+ }
136
+ }
137
+
138
+ .ant-form-item-label {
139
+ label {
140
+ font-size: 18px !important;
141
+ color: #1a1a1a !important;
142
+ font-weight: 500 !important;
143
+ }
144
+ }
145
+ .ant-tooltip {
146
+ max-width: $FormItemWidth !important;
147
+ }
148
+
149
+ .ant-page-header-heading {
150
+ width: 1022px !important;
151
+ }
152
+
153
+ .highlight {
154
+ //background: #feffe6;
155
+ background: ghostwhite;
156
+ }
frontend/src/utils/audio_utils.ts ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // AudioUtils.ts
2
+ export class AudioUtils {
3
+ static async createPCM16Data(audioBuffer: AudioBuffer): Promise<ArrayBuffer> {
4
+ const numChannels = 1; // Mono
5
+ const sampleRate = 16000; // Target sample rate
6
+ const format = 1; // PCM
7
+ const bitDepth = 16;
8
+
9
+ // Resample if needed
10
+ let samples = audioBuffer.getChannelData(0);
11
+ if (audioBuffer.sampleRate !== sampleRate) {
12
+ samples = await this.resampleAudio(samples, audioBuffer.sampleRate, sampleRate);
13
+ }
14
+
15
+ const dataLength = samples.length * (bitDepth / 8);
16
+ const headerLength = 44;
17
+ const totalLength = headerLength + dataLength;
18
+
19
+ const buffer = new ArrayBuffer(totalLength);
20
+ const view = new DataView(buffer);
21
+
22
+ // Write WAV header
23
+ this.writeString(view, 0, 'RIFF');
24
+ view.setUint32(4, totalLength - 8, true);
25
+ this.writeString(view, 8, 'WAVE');
26
+ this.writeString(view, 12, 'fmt ');
27
+ view.setUint32(16, 16, true);
28
+ view.setUint16(20, format, true);
29
+ view.setUint16(22, numChannels, true);
30
+ view.setUint32(24, sampleRate, true);
31
+ view.setUint32(28, sampleRate * numChannels * (bitDepth / 8), true);
32
+ view.setUint16(32, numChannels * (bitDepth / 8), true);
33
+ view.setUint16(34, bitDepth, true);
34
+ this.writeString(view, 36, 'data');
35
+ view.setUint32(40, dataLength, true);
36
+
37
+ // Write audio data
38
+ this.floatTo16BitPCM(view, 44, samples);
39
+
40
+ return buffer;
41
+ }
42
+
43
+ static writeString(view: DataView, offset: number, string: string): void {
44
+ for (let i = 0; i < string.length; i++) {
45
+ view.setUint8(offset + i, string.charCodeAt(i));
46
+ }
47
+ }
48
+
49
+ static floatTo16BitPCM(view: DataView, offset: number, input: Float32Array): void {
50
+ for (let i = 0; i < input.length; i++, offset += 2) {
51
+ const s = Math.max(-1, Math.min(1, input[i]));
52
+ view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
53
+ }
54
+ }
55
+
56
+ static async resampleAudio(
57
+ audioData: Float32Array,
58
+ originalSampleRate: number,
59
+ targetSampleRate: number
60
+ ): Promise<Float32Array> {
61
+ const originalLength = audioData.length;
62
+ const ratio = targetSampleRate / originalSampleRate;
63
+ const newLength = Math.round(originalLength * ratio);
64
+ const result = new Float32Array(newLength);
65
+
66
+ for (let i = 0; i < newLength; i++) {
67
+ const position = i / ratio;
68
+ const index = Math.floor(position);
69
+ const fraction = position - index;
70
+
71
+ if (index + 1 < originalLength) {
72
+ result[i] = audioData[index] * (1 - fraction) + audioData[index + 1] * fraction;
73
+ } else {
74
+ result[i] = audioData[index];
75
+ }
76
+ }
77
+
78
+ return result;
79
+ }
80
+
81
+ }
82
+
83
+ export default AudioUtils;
frontend/src/utils/retry.ts ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const retryAsyncFn = async (fn: any, retry: number) => {
2
+ try {
3
+ await fn()
4
+ } catch (e) {
5
+ if (retry > 0) {
6
+ setTimeout(async () => {
7
+ await retryAsyncFn(fn, retry - 1)
8
+ }, 500)
9
+ } else {
10
+ throw e
11
+ }
12
+ }
13
+ }
14
+
15
+ export const retryFn = (fn: any, retry: number) => {
16
+ try {
17
+ fn()
18
+ } catch (e) {
19
+ if (retry > 0) {
20
+ setTimeout(() => {
21
+ retryFn(fn, retry - 1)
22
+ }, 500)
23
+ } else {
24
+ throw e
25
+ }
26
+ }
27
+ }
frontend/src/utils/size.ts ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const sizeCalculator = (size: number) => {
2
+ if (size < 1024) {
3
+ return size + ' B'
4
+ } else if (size < 1024 * 1024) {
5
+ return (size / 1024).toFixed(2) + ' KB'
6
+ } else if (size < 1024 * 1024 * 1024) {
7
+ return (size / 1024 / 1024).toFixed(2) + ' MB'
8
+ } else {
9
+ return (size / 1024 / 1024 / 1024).toFixed(2) + ' GB'
10
+ }
11
+ }
12
+
13
+ export const getRandomNumInt = (min: number, max: number) => {
14
+ var Range = max - min;
15
+ var Rand = Math.random(); //获取[0-1)的随机数
16
+ return (min + Math.round(Rand * Range)); //放大取整
17
+ }
18
+
19
+ export const formatMs = (ms:any ,all: any) => {
20
+ let ss=ms%1000;ms=(ms-ss)/1000;
21
+ let s=ms%60;ms=(ms-s)/60;
22
+ let m=ms%60;ms=(ms-m)/60;
23
+ let h=ms;
24
+ let t=(h?h+":":"")
25
+ +(all||h+m?("0"+m).substr(-2)+":":"")
26
+ +(all||h+m+s?("0"+s).substr(-2)+"″":"")
27
+ +("00"+ss).substr(-3);
28
+ return t;
29
+ }
30
+
31
+ export const getRandomItems = (arr: [], num: number) => {
32
+ if (arr.length < num) {
33
+ throw new Error('The array does not contain enough elements.');
34
+ }
35
+
36
+ // 复制原数组,避免修改原数组
37
+ let tempArray = [...arr];
38
+
39
+ // 打乱数组
40
+ for (let i = tempArray.length - 1; i > 0; i--) {
41
+ const j = Math.floor(Math.random() * (i + 1));
42
+ [tempArray[i], tempArray[j]] = [tempArray[j], tempArray[i]]; // ES6 的解构赋值来交换元素
43
+ }
44
+
45
+ // 返回前num个项目
46
+ return tempArray.slice(0, num);
47
+ }
frontend/src/views/404/index.vue ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+
3
+ import router from "@/router.ts";
4
+
5
+ const backAction = () => {
6
+ router.replace('/')
7
+ }
8
+ </script>
9
+
10
+ <template>
11
+ <div class="not-found-wrapper">
12
+ <a-result status="404" title="404" sub-title="Sorry, the page you visited does not exist.">
13
+ <template #extra>
14
+ <a-button @click="backAction" type="primary">Back Home</a-button>
15
+ </template>
16
+ </a-result>
17
+ </div>
18
+ </template>
19
+
20
+ <style lang="scss" scoped>
21
+ .not-found-wrapper {
22
+ height: calc(100vh - 104px);
23
+ }
24
+ </style>
frontend/src/views/Footer.vue ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ </script>
3
+
4
+ <template>
5
+ <div class="right">
6
+ © 2025 MoYoYo Inc. All Rights Reserved.
7
+ </div>
8
+ </template>
9
+
10
+ <style scoped>
11
+
12
+ .right {
13
+ position: fixed;
14
+ bottom: 0;
15
+ width: 100%;
16
+ height: 40px;
17
+ line-height: 40px;
18
+ text-align: center;
19
+ font-size: 0.8em;
20
+ background: white;
21
+ }
22
+
23
+ </style>
frontend/src/views/Header.vue ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts" setup>
2
+ import router from "@/router.ts";
3
+ import {watch, ref, onMounted, onUnmounted} from "vue";
4
+ import {ArrowRightOutlined, DashboardOutlined,
5
+ ExperimentOutlined, SettingOutlined} from "@ant-design/icons-vue";
6
+
7
+ const windowWidth = ref(window.innerWidth)
8
+
9
+ onMounted(() => {
10
+ window.addEventListener('resize', () => {
11
+ windowWidth.value = window.innerWidth
12
+ })
13
+ })
14
+ onUnmounted(() => {
15
+ window.removeEventListener('resize', () => {
16
+ windowWidth.value = window.innerWidth
17
+ })
18
+ })
19
+
20
+ const sessionActive = ref(false)
21
+ const toolsActive = ref(false)
22
+ watch(router.currentRoute, (to, from) => {
23
+ console.log('router changed', to, from)
24
+
25
+ const sessionActivePath = ['/sessions', '/sessions/', '/form', '/form/', '/output']
26
+
27
+ if (sessionActivePath.includes(to.path)) {
28
+ sessionActive.value = true
29
+ toolsActive.value = false
30
+ return
31
+ }
32
+
33
+ const toolsActivePath = ['/tools', '/tools/', '/benchmark', '/benchmark/', '/benchmark_detail', '/benchmark_detail/']
34
+ if (toolsActivePath.includes(to.path)) {
35
+ sessionActive.value = false
36
+ toolsActive.value = true
37
+ return
38
+ }
39
+
40
+ sessionActive.value = false
41
+ toolsActive.value = false
42
+ })
43
+
44
+ const gotoSettings = (e: any) => {
45
+ e.preventDefault()
46
+ router.push('/settings')
47
+ }
48
+
49
+ const gotoProfiling = (e: any) => {
50
+ e.preventDefault()
51
+ router.push('/profiling')
52
+ }
53
+
54
+ const gotoHealthcheck = (e: any) => {
55
+ e.preventDefault()
56
+ router.push('/healthcheck')
57
+ }
58
+
59
+ </script>
60
+ <template>
61
+ <nav class="header-nav">
62
+ <div class="nav-left flex flex-row justify-between items-center">
63
+ <router-link to="/" >
64
+ <img
65
+ alt="logo"
66
+ src="/logo.webp"
67
+ class="logo"
68
+ />
69
+ </router-link>
70
+
71
+ </div>
72
+ <div class="title">
73
+ <div v-if="windowWidth >= 1280">
74
+ <div class="primary">AVATAR
75
+ </div>
76
+ <div class="secondary">(power by AI)</div>
77
+ </div>
78
+ <h3 v-if="windowWidth < 1280"
79
+ class="text-3xl font-medium text-primary-dark dark:text-ternary-light hidden sm:block"
80
+ style="font-size: 22px; line-height: 60px; font-weight: 600;"
81
+ >AVATAR</h3>
82
+ </div>
83
+ <div class="nav-right flex flex-row justify-between items-center">
84
+ <!-- <a-button type="link" href="/">-->
85
+ <!-- <span :style="router.currentRoute.value.path == '/' ? 'border-bottom: solid 2px; ' : ''" >Home</span>-->
86
+ <!-- </a-button>-->
87
+ <!-- <router-link to="/settings" >-->
88
+ <!-- <img-->
89
+ <!-- alt="logo"-->
90
+ <!-- src="/logo.webp"-->
91
+ <!-- class="logo"-->
92
+ <!-- />-->
93
+ <!-- </router-link>-->
94
+ <a-button type="ghost" style="margin-right: 12px;"
95
+ size="large" @click="gotoProfiling">
96
+ <template #icon>
97
+ <ExperimentOutlined style="font-size: 24px;"/>
98
+ </template>
99
+ </a-button>
100
+ <a-button type="ghost" style="margin-right: 12px;"
101
+ size="large" @click="gotoHealthcheck">
102
+ <template #icon>
103
+ <DashboardOutlined style="font-size: 24px;"/>
104
+ </template>
105
+ </a-button>
106
+ <a-button type="ghost" size="large" @click="gotoSettings">
107
+ <template #icon>
108
+ <SettingOutlined style="font-size: 24px;"/>
109
+ </template>
110
+ </a-button>
111
+ <!-- <a-button type="link" target="_blank" >-->
112
+ <!-- <span>Contact</span>-->
113
+ <!-- </a-button>-->
114
+ </div>
115
+ </nav>
116
+ </template>
117
+ <style scoped lang="scss">
118
+ .header-nav {
119
+ display: flex;
120
+ align-items: center;
121
+ justify-content: space-between;
122
+ width: 100vw;
123
+ height: 72px;
124
+ background: aliceblue;
125
+ box-shadow: 1px 1px 2px 1px #d9d9d9;
126
+ top: 0;
127
+ position: sticky;
128
+ z-index: 99;
129
+ }
130
+
131
+ .nav-left {
132
+ position: absolute;
133
+ left: 20px;
134
+ top: 12px;
135
+ }
136
+
137
+ .nav-right {
138
+ .ant-btn {
139
+ span {
140
+ font-size: 15px;
141
+ font-weight: 600;
142
+ }
143
+ }
144
+ }
145
+
146
+ .logo {
147
+ width: 48px;
148
+ height: 48px;
149
+ border-radius: 24px;
150
+ will-change: filter;
151
+ transition: filter 300ms;
152
+ }
153
+
154
+ .logo:hover {
155
+ filter: drop-shadow(0 0 2em #646cffaa);
156
+ }
157
+
158
+ .logo.vue:hover {
159
+ filter: drop-shadow(0 0 2em #42b883aa);
160
+ }
161
+
162
+ .nav-items {
163
+ margin-left: 10px;
164
+ }
165
+
166
+ .nav-right {
167
+ position: absolute;
168
+ right: 20px;
169
+ height: 64px;
170
+ align-items: center;
171
+ display: flex;
172
+ }
173
+
174
+ .title {
175
+ margin-left: 84px;
176
+ line-height: 48px;
177
+ height: 54px;
178
+
179
+ .primary {
180
+ font-weight: bold;
181
+ text-align: left;
182
+ font-size: 16px;
183
+ letter-spacing: -0.8px;
184
+ font-family: Courier,Menlo, monospace, 'SFMono-Regular', Consolas, 'Liberation Mono';
185
+ }
186
+ .secondary {
187
+ font-family: Courier,Menlo, monospace, 'SFMono-Regular', Consolas, 'Liberation Mono';
188
+ text-align: left;
189
+ font-size: 8px;
190
+ letter-spacing: -0.2px;
191
+ font-weight: 400;
192
+ line-height: 20px;
193
+ margin-top: -12px;
194
+ }
195
+ }
196
+
197
+ .switch-icon {
198
+ margin-left: 20px;
199
+ }
200
+ </style>
frontend/src/views/Home/index.vue ADDED
@@ -0,0 +1,905 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ import router from "@/router.ts";
3
+ import {
4
+ DownloadOutlined, FileMarkdownOutlined, SettingOutlined,
5
+ AudioOutlined, DeleteOutlined, FileTextOutlined,
6
+ ArrowRightOutlined, ContainerOutlined, UploadOutlined
7
+ } from "@ant-design/icons-vue";
8
+ import { test_server } from "@/config/client_config.ts";
9
+ import axios from "axios";
10
+ import { nextTick, onMounted, onUnmounted, reactive, ref, createVNode, computed, watch } from "vue";
11
+
12
+ import { useSessionStore, downloadSessionData } from "@/stores/session";
13
+ import type { SessionSummary, SessionNode } from '@/stores/session'; // 导入类型
14
+ import { useSettingsStore } from "@/stores/config.ts";
15
+ const sessionStore = useSessionStore()
16
+ const settingsStore = useSettingsStore();
17
+ // https://github.surmon.me/videojs-player video.js debug page;
18
+ // const base_url = 'http://192.168.110.102:8000'
19
+
20
+
21
+ const host = import.meta.env.PROD ? window.location.host : test_server
22
+ // const pathname = window.location.pathname
23
+ let ws_prefix = 'ws'
24
+ if (host.startsWith('127.0.0.1') || host.startsWith('localhost')) {
25
+ ws_prefix = 'ws'
26
+ } else {
27
+ ws_prefix = 'wss'
28
+ }
29
+ const ws_url = `${ws_prefix}://` + host + `/ws?`
30
+ console.warn('ws_url: ', ws_url)
31
+
32
+ const sock = ref(null)
33
+ const startWebSock = async (lang_str: string) => {
34
+ console.warn('start websocket ...')
35
+ // 确保在创建 WebSocket 连接之前关闭已有连接
36
+ // @ts-ignore
37
+ if (sock.value && sock.value.readyState !== WebSocket.CLOSED) {
38
+ // @ts-ignore
39
+ sock.value.close();
40
+ }
41
+
42
+ // const socket_url = `${ws_url}${lang_str}${'&vad=' + vadValueRef.value}`
43
+ const socket_url = `${ws_url}${lang_str}`
44
+ // @ts-ignore
45
+ sock.value = new WebSocket(socket_url)
46
+ // @ts-ignore
47
+ sock.value.binaryType = "arraybuffer";
48
+ console.warn('created web socket ...')
49
+ // @ts-ignore
50
+ sock.value.addEventListener('open', () => {
51
+ console.log('WebSocket 连接成功');
52
+ startAudioCapture(); // WebSocket 连接成功后开始音频捕获
53
+ isRecording.value = true; // 连接成功时自动开始录音
54
+ });
55
+ // @ts-ignore
56
+ sock.value.addEventListener('close', () => {
57
+ console.log('WebSocket 连接已关闭');
58
+ });
59
+ // @ts-ignore
60
+ sock.value.onclose = (event: any) => {
61
+ console.log('code:', event.code, 'reason:', event.reason, 'wasClean:', event.wasClean)
62
+ // https://www.cnblogs.com/gxp69/p/11736749.html
63
+ console.log('WebSocket 连接已关闭:', event);
64
+ };
65
+ // @ts-ignore
66
+ sock.value.addEventListener('error', (error) => {
67
+ console.error('WebSocket 连接错误:', error);
68
+ });
69
+ // @ts-ignore
70
+ sock.value.addEventListener('message', (event) => {
71
+ try { // 添加 try-catch 保证 JSON 解析失败不中断程序
72
+ const data = JSON.parse(event.data)
73
+ console.log('WebSocket 收到消息:', data);
74
+ if (data && data['result']) {
75
+ updateViewData(data['result'])
76
+ }
77
+ } catch (e) {
78
+ console.error("解析 WebSocket 消息失败:", e, "原始数据:", event.data);
79
+ }
80
+ });
81
+ }
82
+
83
+ const stopWebSock = async () => {
84
+ if (sock.value) {
85
+ console.log("主动关闭 WebSocket 连接");
86
+ // @ts-ignore
87
+ sock.value.close(1000, "User closed connection"); // 使用标准关闭代码
88
+ sock.value = null;
89
+ }
90
+ }
91
+
92
+ const audioStreamRef = ref<MediaStream | null>(null);
93
+ const recorderRef = ref(null);
94
+ const audioContextRef = ref<AudioContext | null>(null);
95
+ const sourceNodeRef = ref(null)
96
+ const processorNodeRef = ref(null)
97
+ const isRecording = ref<boolean>(false);
98
+
99
+ // 启动音频捕获
100
+ const startAudioCapture = async () => {
101
+ try {
102
+ // 检查 AudioContext 是否支持
103
+ // @ts-ignore
104
+ if (!window.AudioContext && !window.webkitAudioContext) {
105
+ alert("浏览器不支持 Web Audio API");
106
+ throw new Error("浏览器不支持 Web Audio API");
107
+ }
108
+
109
+ const stream = await navigator.mediaDevices.getUserMedia({
110
+ audio: {
111
+ // @ts-ignore
112
+ sampleRate: 16000,
113
+ channelCount: 1,
114
+ // echoCancellation: true,
115
+ // noiseSuppression: true,
116
+ // autoGainControl: true,
117
+ },
118
+ });
119
+ audioStreamRef.value = stream;
120
+
121
+ // 创建 AudioContext,指定采样率为 16kHz
122
+ const audioContext = new AudioContext({ sampleRate: 16000 });
123
+ audioContextRef.value = audioContext;
124
+
125
+ // 创建媒体流源节点
126
+ const source = audioContext.createMediaStreamSource(stream);
127
+ // @ts-ignore
128
+ sourceNodeRef.value = source;
129
+
130
+ // 创建脚本处理器节点
131
+ const processor = audioContext.createScriptProcessor(4096, 1, 1);
132
+ // @ts-ignore
133
+ processorNodeRef.value = processor;
134
+
135
+ // 连接节点
136
+ source.connect(processor);
137
+ processor.connect(audioContext.destination);
138
+
139
+ // 设置音频处理回调
140
+ processor.onaudioprocess = (e: AudioProcessingEvent) => {
141
+ // @ts-ignore
142
+ if (!isRecording.value || !sock.value || sock.value.readyState !== WebSocket.OPEN) return;
143
+
144
+ const input = e.inputBuffer.getChannelData(0);
145
+ const buffer = new Int16Array(input.length);
146
+
147
+ for (let i = 0; i < input.length; i++) {
148
+ buffer[i] = Math.max(-1, Math.min(1, input[i])) * 0x7FFF;
149
+ }
150
+
151
+ sendAudioChunk(buffer);
152
+ };
153
+
154
+ isRecording.value = true;
155
+ console.log('音频捕获已启动');
156
+
157
+ } catch (err) {
158
+ console.error('音频捕获失败:', err);
159
+ }
160
+ }
161
+
162
+ const sendAudioChunk = (audioBuffer: any) => {
163
+ // @ts-ignore
164
+ if (sock.value && sock.value.readyState === WebSocket.OPEN) {
165
+ // @ts-ignore
166
+ sock.value.send(audioBuffer);
167
+ // console.log('WebSocket send audio chunk success'); // 减少日志量
168
+ } else {
169
+ console.error('WebSocket 未连接或未打开,无法发送音频');
170
+ // 考虑停止录音
171
+ // handleRecordingSwitch(false);
172
+ }
173
+ };
174
+
175
+
176
+ // 请求录音权限并开始录音
177
+ const requirePermissionAction = async () => {
178
+ console.log('requirePermissionAction');
179
+ try {
180
+ // 确保 WebSocket 连接已建立
181
+ // @ts-ignore
182
+ if (!sock.value || sock.value.readyState !== WebSocket.OPEN) {
183
+ console.log('current lang_str : ', transLanguageValue.value)
184
+ const lang_str = transLanguageValue.value
185
+
186
+ resetViewData();
187
+ await startWebSock(lang_str);
188
+ } else {
189
+ // 如果 WebSocket 已经连接,可能只需要重新开始音频捕获(如果之前停止了)
190
+ if (!audioStreamRef.value) {
191
+ await startAudioCapture();
192
+ }
193
+ }
194
+ } catch (e: any) {
195
+ isRecording.value = false; // 确保出错时关闭开关
196
+ console.log('Error accessing microphone: ', e);
197
+ }
198
+ };
199
+
200
+
201
+ // 格式化时间戳函数
202
+ const formatTimestamp = (ms: number): string => {
203
+ const date = new Date(ms);
204
+ const year = date.getFullYear();
205
+ const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份从 0 开始,需要 +1
206
+ const day = String(date.getDate()).padStart(2, '0');
207
+ const hours = String(date.getHours()).padStart(2, '0');
208
+ const minutes = String(date.getMinutes()).padStart(2, '0');
209
+ const seconds = String(date.getSeconds()).padStart(2, '0');
210
+
211
+ return `${year}-${month}-${day}-${hours}:${minutes}:${seconds}`;
212
+ }
213
+
214
+ onMounted(() => {
215
+ console.log('[translator]: mounted')
216
+ fontSizeRef.value = settingsStore.$state.fs
217
+ maxWidthRef.value = settingsStore.$state.width_max
218
+ vadValueRef.value = settingsStore.$state.vad
219
+
220
+ if (sessionStore.isSessionActive) {
221
+ console.warn("检测到上次会话未正常结束,重置状态。");
222
+ sessionStore.$reset(); // 或者手动重置相关状态
223
+ // isRecording.value = false; // 确保 UI 同步
224
+ }
225
+ })
226
+
227
+ onUnmounted(() => {
228
+ console.log('[HomePage]: unmounted')
229
+ if (sock.value) {
230
+ // @ts-ignore
231
+ sock.value.close();
232
+ }
233
+ if (recorderRef.value) {
234
+ stopRecording();
235
+ sessionStore.endSession(); // 结束当前会话
236
+ }
237
+ })
238
+
239
+ // 停止录音
240
+ const stopRecording = () => {
241
+ isRecording.value = false;
242
+
243
+ stopWebSock();
244
+ console.log('音频捕获已停止');
245
+
246
+ if (processorNodeRef.value) {
247
+ // @ts-ignore
248
+ processorNodeRef.value.disconnect();
249
+ processorNodeRef.value = null;
250
+ }
251
+
252
+ if (sourceNodeRef.value) {
253
+ // @ts-ignore
254
+ sourceNodeRef.value.disconnect();
255
+ sourceNodeRef.value = null;
256
+ }
257
+
258
+ if (audioStreamRef.value) {
259
+ audioStreamRef.value.getTracks().forEach(track => track.stop());
260
+ audioStreamRef.value = null;
261
+ }
262
+
263
+ if (audioContextRef.value) {
264
+ // @ts-ignore
265
+ audioContextRef.value.close();
266
+ audioContextRef.value = null;
267
+ }
268
+
269
+ current_node_text.value = "";
270
+ current_node_trans_text.value = "";
271
+ current_node_seg_id.value = "";
272
+ console.log('录音已停止');
273
+ };
274
+
275
+
276
+ const handleLanguageChange = async (value: string) => {
277
+ console.log(`selected ${value}`);
278
+ isRecording.value = false;
279
+ await stopRecording();
280
+ sessionStore.endSession();
281
+
282
+ console.log('new lang_str: ', value)
283
+ console.log('trans_lang : ', transLanguageValue.value)
284
+ // requirePermissionAction();
285
+ };
286
+
287
+
288
+ const handleRecordingSwitch = (checked: boolean) => {
289
+ isRecording.value = checked;
290
+ if (checked) {
291
+ isRecording.value = true; // 更新UI状态
292
+ requirePermissionAction();
293
+ sessionStore.startSession(); // 开始新的会话
294
+ } else {
295
+ isRecording.value = false; // 更新UI状态
296
+ stopRecording();
297
+ sessionStore.endSession();
298
+ }
299
+ };
300
+
301
+
302
+ const transLanguageValue = ref("from=en&to=zh");
303
+ const options = [
304
+ { value: "from=en&to=zh", label: "English -> Chinese" },
305
+ { value: "from=zh&to=en", label: "Chinese -> English" },
306
+ ];
307
+
308
+ // @ts-ignore
309
+ const completedNodesForDisplay: any = reactive([])
310
+
311
+ const current_node_text = ref("");
312
+ const current_node_trans_text = ref("");
313
+ const current_node_seg_id = ref("");
314
+
315
+ const updateViewData = (data: any) => {
316
+ console.log('updateViewData: ', data)
317
+ if (data) {
318
+ const { context, from, to, seg_id, partial, tranContent } = data;
319
+
320
+ if (partial == true) {
321
+ current_node_text.value = context;
322
+ current_node_trans_text.value = tranContent;
323
+ current_node_seg_id.value = seg_id;
324
+
325
+ return;
326
+ } else {
327
+ // partial == false,表示一句话结束
328
+ const finalNode: SessionNode = {
329
+ id: seg_id || crypto.randomUUID(), // 使用后端 ID 或生成一个
330
+ text: context,
331
+ translatedText: tranContent,
332
+ timestamp: Date.now(),
333
+ };
334
+
335
+ sessionStore.addNode(finalNode);
336
+
337
+ if (completedNodesForDisplay.length > 100) {
338
+ // 控制显示列表长度,避免 DOM 过多
339
+ completedNodesForDisplay.splice(0, 40);
340
+ }
341
+ completedNodesForDisplay.push(finalNode);
342
+ current_node_text.value = "";
343
+ current_node_trans_text.value = "";
344
+ current_node_seg_id.value = "";
345
+ }
346
+ }
347
+
348
+ scrollToBottom();
349
+ }
350
+
351
+ const resetViewData = () => {
352
+ completedNodesForDisplay.splice(0, completedNodesForDisplay.length); // 清空显示列表
353
+ current_node_text.value = "";
354
+ current_node_trans_text.value = "";
355
+ current_node_seg_id.value = "";
356
+ }
357
+
358
+ const downloadText2 = async (sid: string, nodes: SessionNode[]) => {
359
+ // @ts-ignore
360
+ // 将数组中的每个字符串元素用换行符连接起来
361
+ // const textContent = all_nodes.join('\n');
362
+ const textContent = nodes.map((node: any) => {
363
+ return `[src]: ${node.text}\n${"-".repeat(80)}\n[dst]: ${node.translatedText}\n\n`
364
+ }).join('\n');
365
+ // 创建 Blob 时指定 MIME 类型为 text/plain
366
+ const blob = new Blob([textContent], { type: "text/plain" });
367
+ const url = URL.createObjectURL(blob);
368
+ const a = document.createElement("a");
369
+ a.href = url;
370
+ // 修改下载文件名为 .txt
371
+ a.download = `${sid}.txt`;
372
+ document.body.appendChild(a);
373
+ a.click();
374
+ document.body.removeChild(a);
375
+ URL.revokeObjectURL(url);
376
+ }
377
+
378
+
379
+ const downloadText = (sid: number, nodes: SessionNode[]) => { // Removed async as it's not needed here
380
+ try {
381
+ if (!nodes || nodes.length === 0) {
382
+ console.warn("No nodes provided for download.");
383
+ alert("No content available to download for this session.");
384
+ return;
385
+ }
386
+
387
+ const textContent = nodes.map((node: SessionNode) => { // Use correct type SessionNode
388
+ // Safely access properties, provide fallback for undefined translatedText
389
+ const srcText = node.text || '(No original text)';
390
+ const dstText = node.translatedText || '(No translation)';
391
+ return `[src]: ${srcText}\n${"-".repeat(80)}\n[dst]: ${dstText}\n\n`;
392
+ }).join(''); // Join without extra newline, as \n\n is already added
393
+
394
+ if (!textContent.trim()) {
395
+ console.warn("Generated text content is empty.");
396
+ alert("Generated content is empty, cannot download.");
397
+ return;
398
+ }
399
+
400
+ // Create Blob with UTF-8 charset
401
+ const blob = new Blob([textContent], { type: "text/plain;charset=utf-8;" });
402
+ const url = URL.createObjectURL(blob);
403
+ const a = document.createElement("a");
404
+
405
+ a.href = url;
406
+ a.download = `${formatTimestamp(sid)}.txt`; // Ensure filename is set
407
+ a.style.display = 'none'; // Hide the element
408
+
409
+ document.body.appendChild(a);
410
+ console.log(`Attempting to click download link for ${sid}.txt`);
411
+ a.click(); // Trigger the download
412
+
413
+ // Delay cleanup to ensure download initiation
414
+ setTimeout(() => {
415
+ try {
416
+ document.body.removeChild(a);
417
+ URL.revokeObjectURL(url);
418
+ console.log(`Cleaned up resources for ${sid}.txt`);
419
+ } catch (cleanupError) {
420
+ console.error("Error during download cleanup:", cleanupError);
421
+ }
422
+ }, 100); // Delay cleanup by 100ms
423
+
424
+ } catch (error) {
425
+ console.error("Error creating download file:", error);
426
+ alert("An error occurred while preparing the download.");
427
+ }
428
+ }
429
+
430
+
431
+ const placeholder_zh = "体验前请检查麦克风是否可用,指定音频语言、译文语言,点击开关按钮开始录音,即可实时获取识别及翻译的文字。"
432
+ const placeholder_en = "Please check if the microphone is available before the experience, specify the audio language and translation language, click the switch button to start recording, and you can get the recognized and translated text in real time."
433
+
434
+
435
+ const transListRef = ref(null);
436
+ // 自动滚动到底部的函数
437
+ const scrollToBottom = () => {
438
+ nextTick(() => {
439
+ if (transListRef.value) {
440
+ // @ts-ignore
441
+ transListRef.value.scrollTop = transListRef.value.scrollHeight + 144;
442
+ }
443
+ });
444
+ };
445
+
446
+ watch(() => [...completedNodesForDisplay], () => {
447
+ scrollToBottom();
448
+ }, { deep: true });
449
+
450
+ watch(() => current_node_trans_text.value, () => {
451
+ scrollToBottom();
452
+ }), { deep: true };
453
+
454
+ // config
455
+ const configVisible = ref(false);
456
+ const showConfig = () => {
457
+ configVisible.value = true;
458
+ }
459
+ const hideConfig = () => {
460
+ configVisible.value = false;
461
+ if (vadValueRef.value != settingsStore.$state.vad) {
462
+ settingsStore.$state.vad = vadValueRef.value
463
+
464
+ if (recorderRef.value) {
465
+ stopRecording();
466
+ sessionStore.endSession();
467
+ }
468
+ }
469
+ }
470
+
471
+ const vadValueRef = ref(0.3);
472
+ const maxWidthRef = ref(false);
473
+ const fontSizeRef = ref('trans-font-size-18')
474
+ const showRealTimeBufferRef = ref(true)
475
+
476
+ const onFontSizeChange = (e: any) => {
477
+ console.log('onFontSizeChange', e.target.value)
478
+ fontSizeRef.value = e.target.value
479
+ settingsStore.$state.fs = e.target.value
480
+ }
481
+
482
+ const sessionsModalVisible = ref(false);
483
+
484
+
485
+ // Modal 中下载按钮的处理函数
486
+ const handleDownloadSession = (summary: SessionSummary) => {
487
+ console.log(`请求下载会话: ${summary.startTime}`);
488
+ const nodes = sessionStore.loadSessionContent(summary.startTime);
489
+ if (nodes) {
490
+ // 弹出格式选择或直接下载 JSON
491
+ // const format = prompt("选择下载格式: 'json' 或 'txt'", "json");
492
+ // if (format === 'json' || format === 'txt') {
493
+ // downloadSessionData(summary.startTime, nodes, format);
494
+ // }
495
+ // downloadSessionData(summary.startTime, nodes, 'json'); // 默认下载 JSON
496
+ downloadText(summary.startTime, nodes);
497
+
498
+ } else {
499
+ alert(`无法加载会话 ${summary.startTime} 的内容进行下载。`);
500
+ }
501
+ }
502
+
503
+ // Modal 中删除按钮的处理函数
504
+ const handleDeleteSession = (summary: SessionSummary) => {
505
+ if (confirm(`确定要删除开始于 ${new Date(summary.startTime).toLocaleString()} 的会话吗?\n标题: ${summary.title}`)) {
506
+ sessionStore.deleteSession(summary.startTime);
507
+ console.log(`已删除会话: ${summary.startTime}`);
508
+ }
509
+ }
510
+
511
+ // 计算属性,用于模板中方便访问排序后的摘要
512
+ const sortedSummaries = computed(() => sessionStore.sortedSessionSummaries);
513
+
514
+
515
+ </script>
516
+
517
+ <template>
518
+ <div class="view-wrapper">
519
+ <div :class="['content-wrapper', maxWidthRef ? 'wrapper-width-auto': 'wrapper-width-fixed']">
520
+ <div style="margin-top: 10vh; padding: 32px">
521
+ <a-card :bordered="false" style="width: 100%;min-width: 100%;">
522
+ <div v-show="!(completedNodesForDisplay.length || current_node_text)" class="chat-box-placeholder">
523
+ {{
524
+ placeholder_en }}</div>
525
+ <div v-show="(completedNodesForDisplay.length || current_node_text)" class="trans-list"
526
+ ref="transListRef">
527
+ <div v-for="node in completedNodesForDisplay" :key="node.id" :class="['node']"
528
+ :data-seg-id="node.id">
529
+ <div :class="['trans-src-lang', fontSizeRef]">{{ node.text }}</div>
530
+ <div :class="['trans-dst-lang', fontSizeRef]">{{ node.translatedText }}</div>
531
+ </div>
532
+ <div v-show="showRealTimeBufferRef" class=" node current_node" :key="current_node_seg_id">
533
+ <div :class="['trans-src-lang', fontSizeRef]">{{ current_node_text }}</div>
534
+ <div :class="['trans-dst-lang', fontSizeRef]">{{ current_node_trans_text }}</div>
535
+ </div>
536
+ </div>
537
+ <template #actions>
538
+ <div class="actions-box">
539
+ <div class="left-actions">
540
+ <a-popover v-model:open="configVisible" placement="topLeft" trigger="click">
541
+ <template #content>
542
+ <div class="config-content">
543
+ <div v-if="false" class="config-block">
544
+ <h4 style="font-weight: 500;">Speaking Speed:</h4>
545
+ <a-radio-group v-model:value="vadValueRef">
546
+ <a-radio :value="0.1">fastest</a-radio>
547
+ <a-radio :value="0.3">fast</a-radio>
548
+ <a-radio :value="0.5">normal</a-radio>
549
+ <a-radio :value="0.75">slow</a-radio>
550
+ <a-radio :value="1">slowest</a-radio>
551
+ </a-radio-group>
552
+ </div>
553
+ <div class="config-block">
554
+ <h4 style="font-weight: 500;">Page Max Width:</h4>
555
+ <a-switch v-model:checked="maxWidthRef" />
556
+ </div>
557
+ <div class="config-block">
558
+ <h4 style="font-weight: 500;">Show Realtime Buffer:</h4>
559
+ <a-switch v-model:checked="showRealTimeBufferRef" />
560
+ </div>
561
+ <div class="config-block">
562
+ <h4 style="font-weight: 500;">Text Font Size:</h4>
563
+ <a-radio-group v-model:value="fontSizeRef" @change="onFontSizeChange">
564
+ <a-radio :value="'trans-font-size-16'">Small</a-radio>
565
+ <a-radio :value="'trans-font-size-18'">Default</a-radio>
566
+ <a-radio :value="'trans-font-size-20'">Normal</a-radio>
567
+ <a-radio :value="'trans-font-size-22'">Medium</a-radio>
568
+ <a-radio :value="'trans-font-size-24'">Large</a-radio>
569
+ </a-radio-group>
570
+ </div>
571
+ </div>
572
+ <div style="display: flex; justify-content: end;">
573
+ <a-button type="primary" @click="hideConfig">Done</a-button>
574
+ </div>
575
+ </template>
576
+ <a-button type="dashed" shape="circle" size="middle" @click="showConfig">
577
+ <template #icon>
578
+ <SettingOutlined />
579
+ </template>
580
+ </a-button>
581
+ </a-popover>
582
+
583
+ <a-select v-model:value="transLanguageValue" style="width: 240px;"
584
+ placeholder="Select Language" :options="options"
585
+ @change="handleLanguageChange"></a-select>
586
+ <a-button type="dashed" shape="circle" size="middle"
587
+ @click="sessionsModalVisible = true">
588
+ <template #icon>
589
+ <FileTextOutlined />
590
+ </template>
591
+ </a-button>
592
+ <a-modal v-model:open="sessionsModalVisible" width="960px" title="Session History"
593
+ centered :closable="true" ok-text="OK" @ok="sessionsModalVisible = false"
594
+ :footer="null">
595
+ <div class="sessions">
596
+ <div v-if="sortedSummaries.length > 0">
597
+ <div v-for="summary in sortedSummaries" :key="summary.startTime"
598
+ class="session-node">
599
+ <div class="content">
600
+ <!-- <div class="content-title">{{ summary.title }}</div> -->
601
+ <div class="content-text">
602
+ Start at: {{ new Date(summary.startTime).toLocaleString() }} ({{
603
+ summary.nodeCount }} items)
604
+ </div>
605
+ <div class="content-outline">
606
+ <div v-if="summary.outline.length > 0">
607
+ <div class="outline-line" v-for="(line, index) in summary.outline" :key="index">{{
608
+ line
609
+ }}</div>
610
+ </div>
611
+ <i v-else>(No outline available)</i>
612
+ </div>
613
+ </div>
614
+ <div class="session-action">
615
+ <!-- <a-popconfirm title="Are you sure you want to delete this session?"
616
+ ok-text="Yes" cancel-text="No"
617
+ @confirm="handleDeleteSession(summary)">
618
+ <a-button type="danger" shape="circle" size="middle"
619
+ style="margin-left: 8px;">
620
+ <template #icon>
621
+ <DeleteOutlined />
622
+ </template>
623
+ </a-button>
624
+ </a-popconfirm> -->
625
+ <a-button danger type="dashed" shape="circle" size="middle"
626
+ @click="handleDeleteSession(summary)" style="margin-left: 8px;">
627
+ <template #icon>
628
+ <DeleteOutlined />
629
+ </template>
630
+ </a-button>
631
+ <a-button type="dashed" shape="circle" size="middle"
632
+ @click="handleDownloadSession(summary)">
633
+ <template #icon>
634
+ <DownloadOutlined />
635
+ </template>
636
+ </a-button>
637
+ </div>
638
+ </div>
639
+ </div>
640
+ </div>
641
+ </a-modal>
642
+ <!-- <a-popconfirm title="download session text content?" @confirm="downloadClick"
643
+ ok-text="Yes" cancel-text="No">
644
+ <template #icon>
645
+ <FileMarkdownOutlined style="color: red" />
646
+ </template>
647
+ <a-button type="dashed" shape="circle" size="middle">
648
+ <template #icon>
649
+ <DownloadOutlined />
650
+ </template>
651
+ </a-button>
652
+ </a-popconfirm> -->
653
+ </div>
654
+
655
+ <a-switch key="switcher" size="large" type="danger" checked-children="ON"
656
+ un-checked-children="OFF" v-model:checked="isRecording" @change="handleRecordingSwitch">
657
+ </a-switch>
658
+ <!-- <div class="right-actions">
659
+ <a-button type="dashed" shape="circle" size="middle" @click="downloadClick">
660
+ <template #icon>
661
+ <DownloadOutlined />
662
+ </template>
663
+ </a-button>
664
+ <a-switch key="switcher" size="large" type="danger" checked-children="ON"
665
+ un-checked-children="OFF" v-model:checked="isRecording"
666
+ @change="handleRecordingSwitch">
667
+ </a-switch>
668
+ </div> -->
669
+
670
+ </div>
671
+ </template>
672
+ </a-card>
673
+ </div>
674
+ </div>
675
+ </div>
676
+ </template>
677
+
678
+ <style lang="scss" scoped>
679
+ .config-content {
680
+ width: 420px;
681
+ margin:12px;
682
+
683
+ .config-block {
684
+ margin: 12px;
685
+ padding-bottom: 12px;
686
+ }
687
+ }
688
+
689
+ .sessions {
690
+ width: 100%;
691
+ height: 100%;
692
+ min-height: 50vh;
693
+ max-height: 80vh;
694
+ overflow-y: scroll;
695
+ margin-top:24px;
696
+ display: flex;
697
+ flex-direction: column;
698
+ justify-content: flex-start;
699
+
700
+ .session-node {
701
+ width: 100%;
702
+ height: 100%;
703
+ display: flex;
704
+ justify-content: space-between;
705
+ align-items: center;
706
+ padding: 12px;
707
+ margin-bottom: 12px;
708
+ background-color: rgba(240, 241, 247, 1);
709
+ border-radius: 4px;
710
+
711
+ .content {
712
+ display: flex;
713
+ flex-direction: column;
714
+ justify-content: center;
715
+ align-items: self-start;
716
+
717
+ .content-title {
718
+ font-size: 18px;
719
+ font-weight: bold;
720
+ color: #2e2f33;
721
+ }
722
+
723
+ .content-text {
724
+ font-size: 18px;
725
+ font-weight: 500;
726
+ color: #2e2f33;
727
+ }
728
+ .content-outline {
729
+ width: 100%;
730
+
731
+ .outline-line {
732
+ font-size: 16px;
733
+ font-weight: 500;
734
+ color: #909299;
735
+ margin: 8px 0 4px 0;
736
+ }
737
+ }
738
+ }
739
+
740
+ .session-action {
741
+ width: 96px;
742
+ display: flex;
743
+ justify-content: space-around;
744
+ align-items: center;
745
+
746
+ .ant-btn-primary {
747
+ background-color: #1890ff !important;
748
+ border-color: #1890ff !important;
749
+ }
750
+ }
751
+ }
752
+ }
753
+
754
+ .view-wrapper {
755
+ width: 100%;
756
+ height: 100%;
757
+ background-color: #fff;
758
+
759
+ .wrapper-width-fixed {
760
+ width: 1280px;
761
+ }
762
+
763
+ .wrapper-width-auto {
764
+ width: 100vw;
765
+ }
766
+
767
+ .content-wrapper {
768
+ text-align: left;
769
+ max-width: 100vw;
770
+ min-width: 320px;
771
+ margin-bottom: 64px;
772
+ min-height: calc(100vh - 438px);
773
+
774
+ .chat-box {
775
+ width: 100%;
776
+ height: 54vh;
777
+
778
+ // border: solid 1px lightgray;
779
+ border-radius: 4px;
780
+ padding: 12px;
781
+ color: #2e2f33;
782
+ font-size: 18px;
783
+ }
784
+ .chat-box-placeholder {
785
+ width: 100%;
786
+ height: 58vh;
787
+ border-radius: 4px;
788
+ padding: 12px;
789
+ font-size: 18px;
790
+ color: #a4a6ac;
791
+ }
792
+
793
+ .actions-box {
794
+ display: flex;
795
+ align-items: center;
796
+ justify-content: space-between;
797
+ margin: 0 24px;
798
+ height: 48px;
799
+
800
+ .left-actions {
801
+ display: flex;
802
+ align-items: center;
803
+ justify-content: space-between;
804
+ // width: 288px;
805
+ width: 332px;
806
+ }
807
+
808
+ .right-actions {
809
+ display: flex;
810
+ align-items: center;
811
+ justify-content: space-between;
812
+ width: 108px;
813
+ }
814
+ }
815
+
816
+ .trans-list {
817
+ overflow-y: auto;
818
+ width: 100%;
819
+ height: 58vh;
820
+
821
+ scrollbar-width: none;
822
+ -ms-overflow-style: none;
823
+ &::-webkit-scrollbar {
824
+ display: none;
825
+ }
826
+
827
+ .node {
828
+ margin-bottom: 36px;
829
+ width: 100% !important;
830
+ transition: all 0.3s ease;
831
+
832
+ .trans-time {
833
+ font-size: 14px;
834
+ color: #c4c6cc;
835
+ }
836
+
837
+ .trans-font-size-16 {
838
+ font-size: 16px;
839
+ }
840
+ .trans-font-size-18 {
841
+ font-size: 18px;
842
+ }
843
+
844
+ .trans-font-size-20 {
845
+ font-size: 20px;
846
+ }
847
+
848
+ .trans-font-size-22 {
849
+ font-size: 22px;
850
+ }
851
+
852
+ .trans-font-size-24 {
853
+ font-size: 24px;
854
+ }
855
+
856
+
857
+ .trans-src-lang {
858
+ // font-size: 18px;
859
+ color: #909299;
860
+ font-weight: 500;
861
+ }
862
+
863
+ .trans-dst-lang {
864
+ // font-size: 18px;
865
+ color: #2e2f33;
866
+ font-weight: 600;
867
+ }
868
+ }
869
+
870
+ .current_node {
871
+ background-color: rgba(240, 241, 247, 1);
872
+ padding: 4px 8px;
873
+ }
874
+
875
+ }
876
+ }
877
+ }
878
+
879
+ // 动画关键帧定义 - 添加这部分
880
+ @keyframes highlight {
881
+ 0% {
882
+ background-color: transparent;
883
+ }
884
+
885
+ 50% {
886
+ background-color: rgba(255, 241, 206, 0.5);
887
+ }
888
+
889
+ 100% {
890
+ background-color: transparent;
891
+ }
892
+ }
893
+
894
+ @keyframes slideIn {
895
+ from {
896
+ opacity: 0;
897
+ transform: translateY(10px);
898
+ }
899
+
900
+ to {
901
+ opacity: 1;
902
+ transform: translateY(0);
903
+ }
904
+ }
905
+ </style>
frontend/src/views/Settings/index.vue ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+
3
+ import router from "@/router.ts";
4
+ import { useSettingsStore } from "@/stores/config.ts";
5
+ import {onMounted, ref} from "vue";
6
+ import {SettingTwoTone} from "@ant-design/icons-vue";
7
+
8
+ const settingsStore = useSettingsStore()
9
+
10
+ onMounted(() => {
11
+ console.log('config', settingsStore.$state)
12
+ })
13
+ const backAction = () => {
14
+ router.replace('/')
15
+ }
16
+
17
+ // const inputType = ref<string>(settingsStore.$state.file_type);
18
+ // const role = ref<string>(settingsStore.$state.role_name);
19
+ // const onTypeChange = (e: any) => {
20
+ // console.log('onTypeChange', e.target.value)
21
+ // settingsStore.$state.file_type = e.target.value
22
+ // }
23
+
24
+ // const onRoleChange = (e: any) => {
25
+ // console.log('onRoleChange', e.target.value)
26
+ // settingsStore.$state.role_name = e.target.value
27
+ // stateStore.changeRole(e.target.value)
28
+ // console.log('role_name', settingsStore.$state.role_name)
29
+ // }
30
+
31
+
32
+ </script>
33
+
34
+ <template>
35
+ <div class="content-wrapper">
36
+ <a-result style="width: 100%;" title="Settings">
37
+ <!-- <template #icon>
38
+ <img
39
+ alt="logo"
40
+ src="/logo.webp"
41
+ style="width: 128px; border-radius: 24px;"
42
+ />
43
+ </template>
44
+ <template #extra>
45
+ <div class="content-box">
46
+ <a-form layout="vertical">
47
+ <a-form-item
48
+ label="Choose your input type:"
49
+ name="inputType"
50
+ >
51
+ <a-radio-group v-model:value="inputType" @change="onTypeChange">
52
+ <a-radio :value="'file'">Audio File</a-radio>
53
+ <a-radio :value="'audio'">Speak</a-radio>
54
+ </a-radio-group>
55
+ </a-form-item>
56
+
57
+ <a-form-item
58
+ label="Choose your desire role:"
59
+ name="role"
60
+ >
61
+ <a-radio-group v-model:value="role" @change="onRoleChange">
62
+ <a-radio :value="'trump'">Trump</a-radio>
63
+ <a-radio :value="'ellen'">Ellen</a-radio>
64
+ </a-radio-group>
65
+ </a-form-item>
66
+ </a-form>
67
+ </div>
68
+
69
+ <a-button @click="backAction" type="primary">Back</a-button>
70
+ </template> -->
71
+
72
+ </a-result>
73
+ </div>
74
+ </template>
75
+
76
+ <style lang="scss" scoped>
77
+
78
+ .content-wrapper {
79
+ text-align: left;
80
+ max-width: 800px;
81
+ min-width: 320px;
82
+ margin-bottom: 64px;
83
+ min-height: calc(100vh - 438px);
84
+
85
+ .content-box {
86
+ padding: 24px;
87
+ height: 240px;
88
+ background-color: #e8e8e8;
89
+ border-radius: 16px;
90
+ width: 50%;
91
+ margin: 48px auto;
92
+ min-width: 300px;
93
+ }
94
+
95
+ .video-box {
96
+ max-width: 800px;
97
+ min-width: 320px;
98
+ width: 90vw;
99
+ height: auto;
100
+ }
101
+ }
102
+
103
+ </style>
frontend/src/vite-env.d.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ /// <reference types="vite/client" />
frontend/tailwind.config.js ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: [
4
+ "./index.html",
5
+ "./src/**/*.{vue,js,ts,jsx,tsx}",
6
+ ],
7
+ theme: {
8
+ extend: {},
9
+ },
10
+ plugins: [],
11
+ }
12
+
frontend/tsconfig.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "module": "ESNext",
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+ "skipLibCheck": true,
8
+
9
+ /* Bundler mode */
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "noEmit": true,
15
+ "jsx": "preserve",
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": false,
20
+ "noUnusedParameters": false,
21
+ "noFallthroughCasesInSwitch": true,
22
+ "baseUrl": ".",
23
+ "paths": {
24
+ "@/*": ["src/*"]
25
+ },
26
+ "allowSyntheticDefaultImports": true
27
+ },
28
+ "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
29
+ "references": [{ "path": "./tsconfig.node.json" }]
30
+ }
frontend/tsconfig.node.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "skipLibCheck": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "allowSyntheticDefaultImports": true
8
+ },
9
+ "include": ["vite.config.ts"]
10
+ }
frontend/vite.config.ts ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import vue from '@vitejs/plugin-vue'
3
+ import { resolve } from 'path'
4
+ import { viteStaticCopy } from 'vite-plugin-static-copy'
5
+
6
+ const absPath = (fp: string): string => {
7
+ return resolve(__dirname, fp)
8
+ }
9
+ // https://vitejs.dev/config/
10
+ export default defineConfig({
11
+ define: {
12
+ __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'true'
13
+ },
14
+ base: "./",
15
+ build: {
16
+ outDir: 'www',
17
+ },
18
+ plugins: [vue({
19
+ script: {
20
+ defineModel: true
21
+ }
22
+ }),
23
+ // viteStaticCopy({
24
+ // targets: [
25
+ // {
26
+ // src: 'node_modules/@ricky0123/vad-web/dist/vad.worklet.bundle.min.js',
27
+ // dest: './assets/'
28
+ // },
29
+ // {
30
+ // src: 'node_modules/@ricky0123/vad-web/dist/silero_vad.onnx',
31
+ // dest: './assets/'
32
+ // },
33
+ // {
34
+ // src: 'node_modules/onnxruntime-web/dist/*.wasm',
35
+ // dest: './assets/'
36
+ // },
37
+ // {
38
+ // src: 'node_modules/onnxruntime-web/dist/*.mjs',
39
+ // dest: './assets/'
40
+ // }
41
+ // ]
42
+ // })
43
+ ],
44
+ assetsInclude: [
45
+ "**/*.txt",
46
+ ],
47
+ resolve: {
48
+ alias: {
49
+ // @ is an alias to /src
50
+ '@': absPath('src'),
51
+ }
52
+ }
53
+ })
main.py CHANGED
@@ -39,11 +39,11 @@ async def lifespan(app:FastAPI):
39
  yield
40
 
41
 
42
- FRONTEND_DIR = os.path.join(BASE_DIR, "frontend")
43
 
44
 
45
  app = FastAPI(lifespan=lifespan)
46
- app.mount("/app", StaticFiles(directory=FRONTEND_DIR, html=True), name="frontend")
47
  pipe = None
48
 
49
  @app.get("/")
 
39
  yield
40
 
41
 
42
+ FRONTEND_DIR = os.path.join(BASE_DIR, "web")
43
 
44
 
45
  app = FastAPI(lifespan=lifespan)
46
+ app.mount("/app", StaticFiles(directory=FRONTEND_DIR, html=True), name="web")
47
  pipe = None
48
 
49
  @app.get("/")
{frontend → web}/assets/index-b1f15c01.css RENAMED
File without changes
{frontend → web}/assets/index-fc3a0f87.js RENAMED
File without changes
web/favicon.ico ADDED
web/index.html ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="./favicon.ico" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Translator</title>
8
+ <script type="module" crossorigin src="./assets/index-fc3a0f87.js"></script>
9
+ <link rel="stylesheet" href="./assets/index-b1f15c01.css">
10
+ </head>
11
+ <body>
12
+ <div id="app"></div>
13
+
14
+ </body>
15
+ </html>