1.对转写文本显示进行重写

This commit is contained in:
amass 2023-06-26 10:13:59 +08:00
parent 561ce68b70
commit 807df7f747
11 changed files with 222 additions and 62 deletions

View File

@ -3,7 +3,11 @@ import { useNavigate } from "react-router-dom";
import yzs from "./business/request.js";
import styles from './LoginPage.module.css';
import { useSelector, useDispatch } from 'react-redux'
import { setFlushToken, setAccessToken, setUserInfo, setAgreeAgreement } from "./business/userSlice.js"
import {
setAccount, setPassword, setVerificationCode,
setFlushToken, setAccessToken, setUserInfo, setAgreeAgreement,
setSelectInfo,
} from "./business/userSlice.js"
import logo from './assets/logo.png';
import { Container, Tab, Box, Snackbar, Alert, Button } from '@mui/material';
import TabPanel from '@mui/lab/TabPanel';
@ -34,6 +38,7 @@ export default function () {
const navigate = useNavigate();
const dispatch = useDispatch();
const [cookies, setCookie] = useCookies(['accessToken']);
const [firstEnter, setFirstEnter] = useState(true);
const [value, setValue] = useState("1");
const [message, setMessage] = useState("");
const [openTooltip, setOpenTooltip] = useState(false);
@ -59,7 +64,24 @@ export default function () {
// const debug_test = () => {
// console.log("accessToken", accessToken, yzs.uniqueDeviceIdentifier());
// console.log("userExists", yzs.userExists(yzs.uniqueDeviceIdentifier(), account));
// testPromiseLoading(2000, "你好").then(v => {
// console.log(v);
// })
// }
const handleInputChange = (event) => {
const { name, value } = event.target;
if (name === 'username') {
dispatch(setAccount(value));
if (firstEnter) setFirstEnter(false);
} else if (name === 'password') {
dispatch(setPassword(value));
} else if (name === 'verification_code') {
dispatch(setVerificationCode(value));
}
};
const handleSubmit = (event) => {
event.preventDefault();
let result = null;
@ -77,9 +99,9 @@ export default function () {
yzs.get_user_info(yzs.uniqueDeviceIdentifier(), token).then(info => {
dispatch(setUserInfo(info));
yzs.user_select(yzs.uniqueDeviceIdentifier(), token).then(info => {
dispatch(setSelectInfo(info));
navigate("/");
})
});
})
}).catch(error => {
@ -121,10 +143,17 @@ export default function () {
</Box>
<TabPanel value="1" >
<DynamicCodeForm udid={yzs.uniqueDeviceIdentifier()} agreeAgreement={agreeAgreement} onAgreeChange={onAgreeChange} />
<DynamicCodeForm udid={yzs.uniqueDeviceIdentifier()}
firstEnter={firstEnter}
agreeAgreement={agreeAgreement} onAgreeChange={onAgreeChange}
onChange={handleInputChange}
/>
</TabPanel>
<TabPanel value="2" >
<PasswordForm agreeAgreement={agreeAgreement} onAgreeChange={onAgreeChange} />
<PasswordForm firstEnter={firstEnter}
agreeAgreement={agreeAgreement} onAgreeChange={onAgreeChange}
onChange={handleInputChange}
/>
</TabPanel>
</TabContext>
</Container>

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { React, useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux'
import AppBar from './AppBar';
import RecordList from './components/RecordList';
@ -7,6 +7,7 @@ import store from './business/store';
import yzs from "./business/request.js";
import { setList, setCurrentLyric, setCurrentBlob, setCurrentWaveData } from "./business/recorderSlice.js"
import { CssBaseline, Box } from '@mui/material';
import MainSkeleton from './MainSkeleton';
import RecordLyrics from './RecordLyrics';
import { createTheme, ThemeProvider, styled } from '@mui/material/styles';
import expand from './assets/expand.png';
@ -104,6 +105,19 @@ const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })(
}),
);
const RecordPlayer = ({ loading, playerBarWidth, currentTime, hasLyric, currentLyric }) => {
if (loading) {
return <MainSkeleton />
} else {
return <div>
<PlayerBar width={playerBarWidth} currentTime={currentTime} />
{hasLyric ? <RecordLyrics style={lyricsBrowserStyle} currentLyric={currentLyric} currentTime={currentTime} /> :
<div style={lyricsBrowserStyle}
/>}
</div>
}
};
export default function () {
const dispatch = useDispatch()
const accessToken = useSelector(state => state.user.accessToken);
@ -112,15 +126,16 @@ export default function () {
const currentLyric = useSelector(state => state.recorder.currentLyric);
const currentIndex = useSelector(state => state.recorder.currentIndex);
const recordList = useSelector(state => state.recorder.list);
const loading = useSelector(state => state.recorder.loading);
const [playerBarWidth, setPlayerBarWidth] = useState(0);
const [open, setOpen] = useState(true);
const [hasLyric, setHasLyric] = useState(true);
useEffect(() => {
if (passportId <= 0) return;
yzs.get_record_list(accessToken, passportId).then(list => {
dispatch(setList(list.result));
if (list.result.length > 0) {
fetchRecord(accessToken, list.result.at(0));
dispatch(setList(list));
if (list.length > 0) {
fetchRecord(accessToken, list.at(0));
}
}).catch(error => {
console.log("get list failed", error);
@ -168,10 +183,8 @@ export default function () {
<Main open={open}
onTransitionEnd={onTransitionEnd}
>
<PlayerBar width={playerBarWidth} currentTime={currentTime} />
{hasLyric ? <RecordLyrics style={lyricsBrowserStyle} currentLyric={currentLyric} currentTime={currentTime} /> :
<div style={lyricsBrowserStyle}
/>}
<RecordPlayer loading={loading}
playerBarWidth={playerBarWidth} currentTime={currentTime} hasLyric={hasLyric} currentLyric={currentLyric} />
</Main>
</ThemeProvider>
</Box >

34
src/MainSkeleton.js Normal file
View File

@ -0,0 +1,34 @@
import Stack from '@mui/material/Stack';
import Box from '@mui/material/Box';
import Skeleton from '@mui/material/Skeleton';
import { Container } from '@mui/material';
export default function () {
return (
<Box spacing={1} sx={{
display: "flex",
flexDirection: "column",
// justifyContent: "space-between",
height: `calc(100vh - 48px)`,
}}>
<Skeleton variant="text" sx={{ fontSize: '1rem' }} />
<Container disableGutters maxWidth={false} sx={{
height: 60,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginTop: 2,
marginBottom: 0.5,
}}>
<Skeleton variant="rounded" width={"30%"} height={60} />
<Skeleton variant="circular" width={30} height={30} />
</Container>
{/* For variant="text", adjust the height via font-size */}
{/* For other variants, adjust the size with `width` and `height` */}
<Skeleton variant="rounded" animation="wave" width="100%" height={70} sx={{ marginBottom: 0.5 }} />
<Skeleton variant="rounded" animation="wave" width="100%" height={32} sx={{ marginBottom: 2 }} />
<Skeleton variant="rounded" width="100%" sx={{ marginBottom: 2, flexGrow: 1 }} />
</Box>
);
}

View File

@ -20,6 +20,7 @@ const durationFormat = (time) => {
export default function ({ width, currentTime }) {
const dispatch = useDispatch();
const [duration, setDuration] = useState(0); // 秒,有小数点
const [playbackRate, setPlaybackRate] = useState(1.0);
const currentIndex = useSelector(state => state.recorder.currentIndex);
const recordList = useSelector(state => state.recorder.list);
const currentBlob = useSelector(state => state.recorder.currentBlob);
@ -28,6 +29,7 @@ export default function ({ width, currentTime }) {
const player = useRef(null);
useEffect(() => {
player.current.url = currentBlob
setPlaybackRate(1.0); // 恢复默认
console.log(player.current.url);
}, [currentBlob]);
@ -66,6 +68,7 @@ export default function ({ width, currentTime }) {
}
const onChange = (event) => {
setPlaybackRate(event.target.value);
player.current.playbackRate = event.target.value;
};
@ -107,7 +110,7 @@ export default function ({ width, currentTime }) {
waveData={currentWaveData}
/>
<Select
defaultValue={1.0}
value={playbackRate}
onChange={onChange}
sx={{ width: 90, height: 70 }}
>

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { useMemo } from "react";
import { Typography, Paper } from "@mui/material";
import { useSelector, useDispatch } from 'react-redux'
@ -7,17 +7,56 @@ function isHighlight(currentTime, { start, end }) {
return (currentTime > start) && (currentTime <= end);
}
// type: 0 --> 声文速记 纯文本,已适配
// type: 1 --> 导入音频
// type: 2 --> 同传翻译 纯文本,已适配
// type: 3 --> 双语对话
const PlainText = ({ lyrics }) => {
return <div style={{ whiteSpace: "pre-wrap" }}>{lyrics}</div>
}
const ImportAudio = ({ lyrics, currentTime }) => { // 导入音频
const onClick = (index) => {
console.log("onClick", index);
}
return <div>{lyrics.map((lyric, index) => {
return <div style={{ paddingBottom: 10, display: "inline-block" }} onDoubleClick={() => onClick(index)}>
<Typography align="left" color={isHighlight(currentTime, lyric) ? "red" : "black"}>{lyric.text}</Typography>
</div>
})}
</div>
};
const BilingualDialogue = ({ lyrics }) => { // 双语对话
return <div> {lyrics.map((lyric, index) => {
return <div index={index} style={{ paddingBottom: 40 }}>
<Typography align="left" >{lyric.asr}</Typography>
<Typography align="left" >{lyric.translate}</Typography>
</div>
})}</div>
}
const LyricsContent = ({ type, lyrics, currentTime }) => {
if (type === 0 || type === 2) {
return <PlainText lyrics={lyrics} />
} else if (type === 1) {
return <ImportAudio lyrics={lyrics} currentTime={currentTime} />
} else if (type === 3) {
return <BilingualDialogue lyrics={lyrics} />
} else {
return <React.Fragment />
}
}
export default function ({ style, currentLyric, currentTime }) {
const currentIndex = useSelector(state => state.recorder.currentIndex);
const recordList = useSelector(state => state.recorder.list);
if (recordList.length === 0) return <React.Fragment />;
const currentType = useMemo(() => {
if (recordList.length <= 0) return -1;
return recordList.at(currentIndex).type;
}, [currentLyric]);
return <Paper style={style}>
{recordList.at(currentIndex).type === 1 ? (typeof currentLyric === "object" ? currentLyric.map((lyric, index) => {
return <div style={{ paddingBottom: 40 }}>
<Typography align="left" color={isHighlight(currentTime, lyric) ? "red" : "black"}>{lyric.text}</Typography>
</div>
}) : <React.Fragment />) : <div style={{ whiteSpace: "pre-wrap" }}>{typeof currentLyric === "string" ? currentLyric : ""}</div>}
<LyricsContent type={currentType} lyrics={currentLyric} currentTime={currentTime} />
</Paper>
}

View File

@ -15,6 +15,7 @@ export const recorderSlice = createSlice({
currentWaveData: [],
currentTime: 0, // 当前音频播放时间
pause: true,
loading: false,
},
reducers: {
setList: (state, action) => {
@ -41,11 +42,35 @@ export const recorderSlice = createSlice({
},
setPauseState: (state, action) => {
state.pause = action.payload;
}
},
setLoading: (state, action) => {
state.loading = true;
},
setLoadFinished: (state, action) => {
state.loading = false;
},
}
})
// Action creators are generated for each case reducer function
export const { setCurrentIndex, setList, setCurrentLyric, setCurrentBlob, togglePauseState, setPauseState, setCurrentTime, setCurrentWaveData } = recorderSlice.actions
export const {
setCurrentIndex, setList, setCurrentLyric, setCurrentBlob, togglePauseState, setPauseState, setCurrentTime,
setCurrentWaveData,
setLoading,
setLoadFinished,
} = recorderSlice.actions
export default recorderSlice.reducer
export default recorderSlice.reducer
const fecthRecord = (index) => {
console.log("begin fetch record item", index);
return (dispatch) => {
dispatch(setLoading());
// testPromiseLoading(2000, true).then(e => {
// console.log("end fetch record item", index);
// dispatch(setLoadFinished());
// });
};
}
export { fecthRecord };

View File

@ -139,8 +139,11 @@ const yzs = {
if (json.errorCode !== "0") {
throw json;
}
console.log(json)
return json;
let list = json.result;
list.sort((lfs, rhs) => { // 要求倒序排序
return rhs.modifyTime - lfs.modifyTime;
})
return list;
});
},
download: function (accessToken, url) {
@ -234,6 +237,31 @@ const yzs = {
console.log(error);
});
},
userExists: function (udid, account) {
let body = {};
body.subsystemId = 16;
body.clientId = udid;
body.timestamp = Math.round(new Date().getTime() / 1000);
body.account = account;
return fetch("/rest/v2/user/is_user_exist", {
method: "POST",
body: constructParameter(body),
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
},
}).then(response => response.json()).then((json) => {
console.log("userExists: ", json);
if (json.returnCode === "uc_0206") { // 用户已存在
return true;
} else if (json.returnCode === "uc_0205") {// 用户不存在
return false;
} else {
throw json;
}
}).catch(error => {
console.log(error);
});
},
uniqueDeviceIdentifier: function () {
let udid = localStorage.getItem('uniqueDeviceIdentifier');
if (!udid) {

View File

@ -3,29 +3,16 @@ import React, { useState, useEffect, useRef } from 'react';
import PhoneIphoneIcon from '@mui/icons-material/PhoneIphone';
import LockIcon from '@mui/icons-material/Lock';
import { useSelector, useDispatch } from 'react-redux'
import { setAccount, setVerificationCode } from "../business/userSlice.js"
import yzs from "../business/request.js";
import Agreement from "./Agreement.js";
import { validatePhoneNumber, textHintOfValidatePhoneNumber } from "../business/utilities.js"
export default function ({ udid, agreeAgreement, onAgreeChange }) {
const dispatch = useDispatch();
export default function ({ udid, firstEnter, onChange, agreeAgreement, onAgreeChange }) {
const code = useRef(null);
const [firstEnter, setFirstEnter] = useState(true);
const [seconds, setSeconds] = useState(0); // 倒计时
const account = useSelector(state => state.user.account)
const verificationCode = useSelector(state => state.user.verificationCode)
const handleInputChange = (event) => {
const { name, value } = event.target;
if (name === 'username') {
dispatch(setAccount(value));
if (firstEnter) setFirstEnter(false);
}
if (name === 'password') dispatch(setVerificationCode(value));
};
useEffect(() => {
const timer = window.setInterval(() => {
if (seconds > 0) {
@ -64,7 +51,7 @@ export default function ({ udid, agreeAgreement, onAgreeChange }) {
fullWidth
error={firstEnter ? false : !validatePhoneNumber(account)}
helperText={firstEnter ? "" : textHintOfValidatePhoneNumber(account)}
onChange={handleInputChange}
onChange={onChange}
InputProps={{
startAdornment: (
<InputAdornment position="start">
@ -82,14 +69,14 @@ export default function ({ udid, agreeAgreement, onAgreeChange }) {
}}
hiddenLabel
fullWidth
name="password"
name="verification_code"
placeholder="请输入验证码"
type="text"
autoComplete="current-password"
variant="outlined"
size="small"
value={verificationCode}
onChange={handleInputChange}
onChange={onChange}
InputProps={{
startAdornment: (
<InputAdornment position="start">
@ -121,7 +108,7 @@ export default function ({ udid, agreeAgreement, onAgreeChange }) {
},
}}
>
注册/登录
登录
</Button>
<Agreement agree={agreeAgreement} onChange={onAgreeChange} />
</Container>

View File

@ -3,23 +3,12 @@ import { Container, TextField, Button, InputAdornment } from "@mui/material";
import PhoneIphoneIcon from '@mui/icons-material/PhoneIphone';
import LockIcon from '@mui/icons-material/Lock';
import { useSelector, useDispatch } from 'react-redux'
import { setAccount, setPassword } from "../business/userSlice.js"
import Agreement from './Agreement.js';
import { validatePhoneNumber, textHintOfValidatePhoneNumber } from "../business/utilities.js"
export default function ({ agreeAgreement, onAgreeChange }) {
const dispatch = useDispatch();
export default function ({ firstEnter, onChange, agreeAgreement, onAgreeChange }) {
const account = useSelector(state => state.user.account)
const password = useSelector(state => state.user.password)
const [firstEnter, setFirstEnter] = useState(true);
const handleInputChange = (event) => {
const { name, value } = event.target;
if (name === 'username') {
dispatch(setAccount(value));
if (firstEnter) setFirstEnter(false);
}
if (name === 'password') dispatch(setPassword(value));
};
return <Container disableGutters={true}
sx={{
@ -38,7 +27,7 @@ export default function ({ agreeAgreement, onAgreeChange }) {
helperText={firstEnter ? "" : textHintOfValidatePhoneNumber(account)}
value={account}
fullWidth
onChange={handleInputChange}
onChange={onChange}
InputProps={{
startAdornment: (
<InputAdornment position="start">
@ -63,7 +52,7 @@ export default function ({ agreeAgreement, onAgreeChange }) {
variant="outlined"
size="small"
value={password}
onChange={handleInputChange}
onChange={onChange}
InputProps={{
startAdornment: (
<InputAdornment position="start">

View File

@ -8,7 +8,7 @@ import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
import Typography from '@mui/material/Typography';
import Toolbar from '@mui/material/Toolbar';
import { setCurrentIndex } from "../business/recorderSlice.js"
import { setCurrentIndex, fecthRecord } from "../business/recorderSlice.js"
import AccessTimeFilledIcon from '@mui/icons-material/AccessTimeFilled';
const drawerWidth = 240;
@ -20,6 +20,7 @@ export default function ({ open, recordList, currentIndex, fetchRecord }) {
console.log("onSelected", index, recordList.at(index).transResultUrl)
dispatch(setCurrentIndex(index));
fetchRecord(accessToken, recordList.at(index));
dispatch(fecthRecord(index));
}
return <Drawer
variant="persistent"
@ -40,9 +41,13 @@ export default function ({ open, recordList, currentIndex, fetchRecord }) {
{recordList === undefined ? <React.Fragment /> : recordList.map((item, index) => (
<ListItem key={index} disablePadding>
<ListItemButton selected={currentIndex === index} onClick={(event) => onSelected(event, index)}>
<ListItemText primary={item.editName} secondary={
<ListItemText primary={item.editName} sx={{ color: currentIndex === index ? "#FF595A" : "#000000" }} secondary={
<React.Fragment>
<Typography component="span" variant="body1">{item.content.slice(0, 50) + '......'}</Typography>
<Typography component="span" variant="body1"
sx={{
overflowWrap: "anywhere"
}}
>{item.content.slice(0, 50) + '......'}</Typography>
<br />
<AccessTimeFilledIcon sx={{ fontSize: 12 }} />
<Typography component="span" variant="body2"> 更新于 {new Date(item.createTime).toLocaleString()}</Typography>

View File

@ -9,6 +9,14 @@ const server = "https://ai-api.hivoice.cn";
const accessServer = "https://uc.hivoice.cn";
module.exports = function (app) {
app.use(
'/rest/v2/user/is_user_exist',
createProxyMiddleware({
target: accessServer,
changeOrigin: true,
logger: console,
})
);
app.use(
'/rest/v2/phone/login',
createProxyMiddleware({