This commit is contained in:
2025-07-01 23:28:07 +08:00
parent 33f70e0564
commit 55c8f877cd
21 changed files with 901 additions and 1 deletions

View File

@@ -0,0 +1,32 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/png" href="https://kevisual.xiongxiao.me/root/center/panda.jpg" />
<title>Echo Text</title>
<style>
html,
body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
#root {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="./src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,16 @@
{
"name": "create-audio",
"version": "0.0.1",
"description": "",
"main": "index.js",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"pub": "ev deploy ./dist -k create-audio -v 0.0.1 -u "
},
"keywords": [],
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
"license": "MIT",
"packageManager": "pnpm@10.11.1",
"type": "module"
}

View File

@@ -0,0 +1,350 @@
import { ToastContainer, toast } from 'react-toastify';
import { useState, useRef, useEffect } from 'react';
import { query } from '@/modules/query';
import { nanoid } from 'nanoid';
export const postCreateVideos = async ({ text }) => {
return await query.post({
path: 'aliyun-ai',
key: 'createVideos',
payload: {
text,
model: '',
},
});
};
export const App = () => {
return (
<>
<CreateVideos />
<ToastContainer />
</>
);
};
const initGetText = () => {
let text = sessionStorage.getItem('createVideosText');
if (text) {
sessionStorage.removeItem('createVideosText');
return text;
}
text = localStorage.getItem('createVideosText');
if (text) {
localStorage.removeItem('createVideosText');
return text;
}
const url = new URL(window.location.href);
text = url.searchParams.get('text');
if (text) {
text = decodeURIComponent(text);
return text;
}
return null;
};
const downloadAudio = (url: string, filename?: string) => {
const link = document.createElement('a');
link.href = url;
const generatedFilename = filename || nanoid(10) + '.wav'; // Generate a random filename
link.download = generatedFilename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
export const CreateVideos = () => {
const [url, setUrl] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);
const [text, setText] = useState<string>('');
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const audioRef = useRef<HTMLAudioElement>(null);
useEffect(() => {
const initialText = initGetText();
if (initialText) {
setText(initialText);
}
}, []);
const handleGenerateAudio = async () => {
if (!text.trim()) {
toast.error('请输入要转换的文字内容');
return;
}
setLoading(true);
try {
const response = await postCreateVideos({
text: text.trim(),
});
if (response?.data?.audioUrl) {
setUrl(response.data.audioUrl);
toast.success('音频生成成功!');
} else {
toast.error('音频生成失败,请稍后重试');
}
} catch (error) {
console.error('生成音频时出错:', error);
toast.error('生成音频时出错,请稍后重试');
} finally {
setLoading(false);
}
};
const handlePlay = () => {
if (audioRef.current) {
if (isPlaying) {
audioRef.current.pause();
setIsPlaying(false);
} else {
audioRef.current.play();
setIsPlaying(true);
}
}
};
const handleStop = () => {
if (audioRef.current) {
audioRef.current.pause();
audioRef.current.currentTime = 0;
setIsPlaying(false);
}
};
const handleAudioEnded = () => {
setIsPlaying(false);
};
return (
<div
style={{
maxWidth: '800px',
margin: '0 auto',
padding: '40px 20px',
fontFamily: 'Arial, sans-serif',
}}>
<div
style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
borderRadius: '16px',
padding: '40px',
color: 'white',
marginBottom: '30px',
}}>
<h1
style={{
textAlign: 'center',
margin: '0 0 20px 0',
fontSize: '2.5rem',
fontWeight: 'bold',
}}>
AI
</h1>
<p
style={{
textAlign: 'center',
margin: '0',
fontSize: '1.1rem',
opacity: '0.9',
}}>
AI语音
</p>
</div>
<div
style={{
background: 'white',
borderRadius: '12px',
padding: '30px',
boxShadow: '0 4px 20px rgba(0,0,0,0.1)',
border: '1px solid #e5e7eb',
}}>
<div style={{ marginBottom: '25px' }}>
<label
style={{
display: 'block',
marginBottom: '10px',
fontSize: '16px',
fontWeight: '600',
color: '#374151',
}}>
</label>
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
placeholder='请输入要转换为语音的文字内容...'
rows={6}
style={{
width: '100%',
padding: '12px 16px',
border: '2px solid #e5e7eb',
borderRadius: '8px',
fontSize: '16px',
resize: 'vertical',
outline: 'none',
transition: 'border-color 0.2s ease',
fontFamily: 'inherit',
}}
onFocus={(e) => (e.target.style.borderColor = '#667eea')}
onBlur={(e) => (e.target.style.borderColor = '#e5e7eb')}
/>
</div>
<div style={{ marginBottom: '25px' }}>
<button
onClick={handleGenerateAudio}
disabled={loading || !text.trim()}
style={{
width: '100%',
padding: '14px 28px',
background: loading ? '#9ca3af' : 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
border: 'none',
borderRadius: '8px',
fontSize: '16px',
fontWeight: '600',
cursor: loading ? 'not-allowed' : 'pointer',
transition: 'all 0.2s ease',
transform: loading ? 'none' : 'translateY(0)',
}}
onMouseEnter={(e) => {
if (!loading) {
(e.target as HTMLButtonElement).style.transform = 'translateY(-2px)';
(e.target as HTMLButtonElement).style.boxShadow = '0 8px 25px rgba(102, 126, 234, 0.4)';
}
}}
onMouseLeave={(e) => {
if (!loading) {
(e.target as HTMLButtonElement).style.transform = 'translateY(0)';
(e.target as HTMLButtonElement).style.boxShadow = 'none';
}
}}>
{loading ? '生成中...' : '生成音频'}
</button>
</div>
{url && (
<div
style={{
background: '#f8fafc',
border: '1px solid #e2e8f0',
borderRadius: '8px',
padding: '20px',
}}>
<h3
style={{
margin: '0 0 15px 0',
fontSize: '18px',
color: '#374151',
}}>
</h3>
<div
style={{
background: 'white',
borderRadius: '8px',
padding: '15px',
marginBottom: '15px',
border: '1px solid #e5e7eb',
}}>
<p
style={{
margin: '0 0 10px 0',
fontSize: '14px',
color: '#6b7280',
wordBreak: 'break-all',
}}
onClick={() => {
// copy
navigator.clipboard.writeText(url);
toast.success('音频链接已复制到剪贴板');
}}>
{url}
</p>
<audio ref={audioRef} src={url} onEnded={handleAudioEnded} style={{ display: 'none' }} />
</div>
<div
style={{
display: 'flex',
gap: '12px',
justifyContent: 'center',
}}>
<button
onClick={handlePlay}
style={{
padding: '10px 20px',
background: isPlaying ? '#ef4444' : '#10b981',
color: 'white',
border: 'none',
borderRadius: '6px',
fontSize: '14px',
fontWeight: '500',
cursor: 'pointer',
transition: 'all 0.2s ease',
minWidth: '80px',
}}
onMouseEnter={(e) => {
(e.target as HTMLButtonElement).style.opacity = '0.9';
(e.target as HTMLButtonElement).style.transform = 'translateY(-1px)';
}}
onMouseLeave={(e) => {
(e.target as HTMLButtonElement).style.opacity = '1';
(e.target as HTMLButtonElement).style.transform = 'translateY(0)';
}}>
{isPlaying ? '暂停' : '播放'}
</button>
<button
onClick={handleStop}
style={{
padding: '10px 20px',
background: '#6b7280',
color: 'white',
border: 'none',
borderRadius: '6px',
fontSize: '14px',
fontWeight: '500',
cursor: 'pointer',
transition: 'all 0.2s ease',
minWidth: '80px',
}}
onMouseEnter={(e) => {
(e.target as HTMLButtonElement).style.opacity = '0.9';
(e.target as HTMLButtonElement).style.transform = 'translateY(-1px)';
}}
onMouseLeave={(e) => {
(e.target as HTMLButtonElement).style.opacity = '1';
(e.target as HTMLButtonElement).style.transform = 'translateY(0)';
}}>
</button>
<button
onClick={() => downloadAudio(url)}
style={{
padding: '10px 20px',
background: '#3b82f6',
color: 'white',
border: 'none',
borderRadius: '6px',
fontSize: '14px',
fontWeight: '500',
cursor: 'pointer',
transition: 'all 0.2s ease',
minWidth: '80px',
}}
onMouseEnter={(e) => {
(e.target as HTMLButtonElement).style.opacity = '0.9';
(e.target as HTMLButtonElement).style.transform = 'translateY(-1px)';
}}
onMouseLeave={(e) => {
(e.target as HTMLButtonElement).style.opacity = '1';
(e.target as HTMLButtonElement).style.transform = 'translateY(0)';
}}>
</button>
</div>
</div>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,9 @@
import ReactDOM from 'react-dom/client';
import { App } from './App.tsx';
import '@/styles/global.css';
import '@/styles/theme.css';
document.title = 'Create Audio - AI Pages';
const container = document.getElementById('root');
const root = ReactDOM.createRoot(container!);
root.render(<App />);

View File

@@ -0,0 +1,15 @@
{
"extends": "@kevisual/types/json/frontend.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"../../src/*"
]
}
},
"include": [
"src/**/*",
"../../src/**/*"
]
}

View File

@@ -0,0 +1,41 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import tailwindcss from '@tailwindcss/vite';
const plugins = [react(), tailwindcss()];
let target = process.env.VITE_API_URL || 'https://kevisual.xiongxiao.me';
console.log('API Target:', target);
const apiProxy = { target: target, changeOrigin: true, ws: true, rewriteWsOrigin: true, secure: false, cookieDomainRewrite: 'localhost' };
let proxy = {
'/root/': apiProxy,
'/user/login/': apiProxy,
'/api': apiProxy,
'/client': apiProxy,
};
/**
* @see https://vitejs.dev/config/
*/
export default defineConfig(() => {
return {
plugins,
resolve: {
alias: {
'@': path.resolve(__dirname, '../../src'),
},
},
base: './',
define: {
DEV_SERVER: JSON.stringify(process.env.NODE_ENV === 'development'),
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
},
build: {
target: 'modules',
},
server: {
port: 7009,
host: '0.0.0.0',
allowedHosts: true,
proxy,
},
};
});