Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
xuwx1
LightX2V
Commits
d8558a0c
Unverified
Commit
d8558a0c
authored
Nov 13, 2025
by
LiangLiu
Committed by
GitHub
Nov 13, 2025
Browse files
update froentend (#470)
Co-authored-by:
qinxinyi
<
qxy118045534@163.com
>
parent
2a31ba43
Changes
26
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
1345 additions
and
543 deletions
+1345
-543
lightx2v/deploy/server/frontend/index.html
lightx2v/deploy/server/frontend/index.html
+105
-13
lightx2v/deploy/server/frontend/public/logo.svg
lightx2v/deploy/server/frontend/public/logo.svg
+1
-0
lightx2v/deploy/server/frontend/public/robots.txt
lightx2v/deploy/server/frontend/public/robots.txt
+3
-0
lightx2v/deploy/server/frontend/public/sitemap.xml
lightx2v/deploy/server/frontend/public/sitemap.xml
+14
-0
lightx2v/deploy/server/frontend/src/components/Alert.vue
lightx2v/deploy/server/frontend/src/components/Alert.vue
+9
-24
lightx2v/deploy/server/frontend/src/components/Confirm.vue
lightx2v/deploy/server/frontend/src/components/Confirm.vue
+1
-1
lightx2v/deploy/server/frontend/src/components/Generate.vue
lightx2v/deploy/server/frontend/src/components/Generate.vue
+161
-44
lightx2v/deploy/server/frontend/src/components/Inspirations.vue
...2v/deploy/server/frontend/src/components/Inspirations.vue
+11
-2
lightx2v/deploy/server/frontend/src/components/LeftBar.vue
lightx2v/deploy/server/frontend/src/components/LeftBar.vue
+4
-4
lightx2v/deploy/server/frontend/src/components/MediaTemplate.vue
...v/deploy/server/frontend/src/components/MediaTemplate.vue
+70
-91
lightx2v/deploy/server/frontend/src/components/Projects.vue
lightx2v/deploy/server/frontend/src/components/Projects.vue
+35
-108
lightx2v/deploy/server/frontend/src/components/SiteFooter.vue
...tx2v/deploy/server/frontend/src/components/SiteFooter.vue
+38
-0
lightx2v/deploy/server/frontend/src/components/TaskCarousel.vue
...2v/deploy/server/frontend/src/components/TaskCarousel.vue
+61
-6
lightx2v/deploy/server/frontend/src/components/TaskDetails.vue
...x2v/deploy/server/frontend/src/components/TaskDetails.vue
+57
-23
lightx2v/deploy/server/frontend/src/components/TemplateDetails.vue
...deploy/server/frontend/src/components/TemplateDetails.vue
+14
-169
lightx2v/deploy/server/frontend/src/components/TopBar.vue
lightx2v/deploy/server/frontend/src/components/TopBar.vue
+5
-2
lightx2v/deploy/server/frontend/src/components/VoiceTtsHistoryPanel.vue
...y/server/frontend/src/components/VoiceTtsHistoryPanel.vue
+308
-0
lightx2v/deploy/server/frontend/src/components/Voice_tts.vue
lightx2v/deploy/server/frontend/src/components/Voice_tts.vue
+248
-38
lightx2v/deploy/server/frontend/src/locales/en.json
lightx2v/deploy/server/frontend/src/locales/en.json
+94
-7
lightx2v/deploy/server/frontend/src/locales/zh.json
lightx2v/deploy/server/frontend/src/locales/zh.json
+106
-11
No files found.
lightx2v/deploy/server/frontend/index.html
View file @
d8558a0c
...
...
@@ -2,24 +2,116 @@
<html>
<head>
<meta
charset=
"UTF-8"
>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1.0"
/>
<title>
LightX2V
</title>
<meta
charset=
"UTF-8"
>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1.0"
>
<!-- 主要图标库 -->
<meta
http-equiv=
"Content-Type"
content=
"text/html; charset=UTF-8"
>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1, viewport-fit=cover"
/>
<title>
LightX2V — AI数字人视频生成平台
</title>
<meta
name=
"description"
content=
"LightX2V:轻量、快速的AI数字人视频生成平台。在线体验、示例视频与使用说明。"
>
<meta
name=
"keywords"
content=
"AI数字人, 文生视频, 图生视频, LightX2V, Light AI, 视频生成, 文本生成视频, 数字人视频平台"
>
<meta
name=
"robots"
content=
"index,follow"
/>
<meta
name=
"mobile-web-app-capable"
content=
"yes"
>
<meta
name=
"apple-mobile-web-app-status-bar-style"
content=
"black-translucent"
>
<meta
name=
"theme-color"
content=
"#0f1329"
>
<link
rel=
"canonical"
href=
"https://x2v.light-ai.top/"
/>
<link
rel=
"alternate"
hreflang=
"zh-CN"
href=
"https://x2v.light-ai.top/"
/>
<link
rel=
"alternate"
hreflang=
"en"
href=
"https://x2v.light-ai.top/en/"
/>
<link
rel=
"alternate"
hreflang=
"x-default"
href=
"https://x2v.light-ai.top/"
/>
<meta
property=
"og:type"
content=
"website"
>
<meta
property=
"og:site_name"
content=
"LightX2V"
>
<meta
property=
"og:title"
content=
"LightX2V — AI数字人视频生成平台"
>
<meta
property=
"og:description"
content=
"轻量、快速的AI数字人视频生成平台。在线体验、示例视频与使用说明。"
>
<meta
property=
"og:url"
content=
"https://x2v.light-ai.top/"
>
<meta
property=
"og:image"
content=
"https://x2v.light-ai.top/og-cover.jpg"
>
<meta
name=
"twitter:card"
content=
"summary_large_image"
>
<meta
name=
"twitter:title"
content=
"LightX2V — AI数字人视频生成平台"
>
<meta
name=
"twitter:description"
content=
"轻量、快速的AI数字人视频生成平台。由 Light AI 工具链驱动,支持文本/图像到视频的高效生成。"
>
<meta
name=
"twitter:image"
content=
"https://x2v.light-ai.top/og-cover.jpg"
>
<link
rel=
"icon"
href=
"/favicon.ico"
sizes=
"32x32"
>
<link
rel=
"icon"
href=
"/favicon.svg"
type=
"image/svg+xml"
>
<link
rel=
"apple-touch-icon"
sizes=
"180x180"
href=
"/apple-touch-icon.png"
>
<link
rel=
"manifest"
href=
"/site.webmanifest"
>
<link
rel=
"preconnect"
href=
"https://x2v.light-ai.top"
crossorigin
>
<link
rel=
"preconnect"
href=
"https://cdn.bootcdn.net"
crossorigin
>
<link
rel=
"preconnect"
href=
"https://cdnjs.cloudflare.com"
crossorigin
>
<link
rel=
"dns-prefetch"
href=
"https://x2v.light-ai.top"
>
<link
rel=
"dns-prefetch"
href=
"https://cdn.bootcdn.net"
>
<link
rel=
"dns-prefetch"
href=
"https://cdnjs.cloudflare.com"
>
<link
rel=
"preload"
href=
"/src/style.css"
as=
"style"
>
<link
rel=
"preload"
href=
"/src/main.js"
as=
"script"
type=
"module"
>
<link
href=
"https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css"
rel=
"stylesheet"
>
<!-- 备用图标库 -->
<link
href=
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
rel=
"stylesheet"
media=
"print"
onload=
"this.media='all'"
>
<link
rel=
'stylesheet'
href=
'https://cdn-uicons.flaticon.com/3.0.0/uicons-solid-rounded/css/uicons-solid-rounded.css'
>
<link
rel=
'stylesheet'
href=
'https://cdn-uicons.flaticon.com/3.0.0/uicons-bold-rounded/css/uicons-bold-rounded.css'
>
<link
rel=
'stylesheet'
href=
'https://cdn-uicons.flaticon.com/3.0.0/uicons-solid-rounded/css/uicons-solid-rounded.css'
>
<link
rel=
'stylesheet'
href=
'https://cdn-uicons.flaticon.com/3.0.0/uicons-regular-rounded/css/uicons-regular-rounded.css'
>
<link
href=
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
rel=
"stylesheet"
media=
"print"
onload=
"this.media='all'"
>
<link
rel=
'stylesheet'
href=
'https://cdn-uicons.flaticon.com/3.0.0/uicons-solid-rounded/css/uicons-solid-rounded.css'
>
<link
rel=
'stylesheet'
href=
'https://cdn-uicons.flaticon.com/3.0.0/uicons-bold-rounded/css/uicons-bold-rounded.css'
>
<link
rel=
'stylesheet'
href=
'https://cdn-uicons.flaticon.com/3.0.0/uicons-solid-rounded/css/uicons-solid-rounded.css'
>
<link
rel=
'stylesheet'
href=
'https://cdn-uicons.flaticon.com/3.0.0/uicons-regular-rounded/css/uicons-regular-rounded.css'
>
<link
href=
"/src/style.css"
rel=
"stylesheet"
>
<style>
.seo-shell
{
position
:
absolute
!important
;
width
:
1px
;
height
:
1px
;
padding
:
0
;
margin
:
-1px
;
overflow
:
hidden
;
clip
:
rect
(
0
,
0
,
0
,
0
);
clip-path
:
inset
(
50%
);
border
:
0
;
white-space
:
nowrap
;
}
</style>
<script
type=
"application/ld+json"
>
{
"
@context
"
:
"
https://schema.org
"
,
"
@type
"
:
"
Organization
"
,
"
name
"
:
"
Light AI
"
,
"
url
"
:
"
https://www.light-ai.top/
"
,
"
logo
"
:
"
https://x2v.light-ai.top/og-cover.jpg
"
,
"
sameAs
"
:[
"
https://github.com/ModelTC/LightX2V
"
]
}
</script>
<script
type=
"application/ld+json"
>
{
"
@context
"
:
"
https://schema.org
"
,
"
@type
"
:
"
WebSite
"
,
"
name
"
:
"
LightX2V
"
,
"
url
"
:
"
https://x2v.light-ai.top/
"
,
"
publisher
"
:{
"
@type
"
:
"
Organization
"
,
"
name
"
:
"
Light AI
"
},
"
inLanguage
"
:
"
zh-CN
"
}
</script>
</head>
<body>
<div
id=
"app"
>
<main
id=
"app"
>
<!-- 这是爬虫可读的首屏静态内容;JS 启动后可增强或替换 -->
<div
class=
"seo-shell"
>
<header>
<h1>
LightX2V
</h1>
<p>
免费、轻量、快速的AI数字人视频生成平台,由 Light AI 工具链提供端到端加速支持。
</p>
<p>
了解更多关于工具链与最新动态,请访问
<a
href=
"https://www.light-ai.top/"
rel=
"noopener"
target=
"_blank"
>
Light AI 官网
</a>
与
<a
href=
"https://github.com/ModelTC/LightX2V"
rel=
"noopener"
target=
"_blank"
>
LightX2V GitHub
</a>
。
</p>
</header>
<section>
<h2>
功能亮点
</h2>
<ul>
<li>
电影级数字人视频生成
</li>
<li>
20倍生成提速
</li>
<li>
超低成本生成
</li>
<li>
精准口型对齐
</li>
<li>
分钟级视频时长
</li>
<li>
多场景应用
</li>
<li>
最新tts语音合成技术,支持多种语言,支持100+种音色,支持语音指令控制合成语音细节
</li>
</ul>
</section>
<section>
<h2>
快速开始
</h2>
<ol>
<li>
上传图片及音频,输入视频生成提示词,点击开始生成
</li>
<li>
生成并下载视频
</li>
<li>
应用模版,一键生成同款数字人视频
</li>
</ol>
</section>
</div>
</main>
<script
type=
"module"
src=
"/src/main.js"
></script>
</body>
</html>
lightx2v/deploy/server/frontend/public/logo.svg
0 → 100644
View file @
d8558a0c
<svg
width=
"766"
height=
"664"
xmlns=
"http://www.w3.org/2000/svg"
xmlns:xlink=
"http://www.w3.org/1999/xlink"
xml:space=
"preserve"
overflow=
"hidden"
><g
transform=
"translate(-1750 -715)"
><g><path
d=
"M2243.72 715.11C2246.74 715.113 2246.74 715.113 2249.82 715.117 2252.12 715.114 2254.42 715.11 2256.79 715.107 2259.33 715.116 2261.88 715.124 2264.5 715.133 2268.5 715.133 2268.5 715.133 2272.57 715.132 2281.42 715.134 2290.27 715.153 2299.12 715.172 2305.24 715.176 2311.36 715.18 2317.48 715.182 2331.96 715.19 2346.45 715.209 2360.94 715.234 2377.43 715.261 2393.92 715.274 2410.41 715.286 2444.34 715.311 2478.26 715.354 2512.19 715.408 2508.04 724.065 2503.01 731.098 2497.09 738.687 2496.04 740.034 2495 741.382 2493.92 742.769 2491.6 745.746 2489.29 748.72 2486.97 751.692 2483 756.781 2479.05 761.885 2475.1 766.992 2460.44 785.934 2445.67 804.763 2430.67 823.436 2422.04 834.206 2413.58 845.099 2405.2 856.068 2397.85 865.678 2390.41 875.208 2382.93 884.711 2373.96 896.105 2365.04 907.535 2356.2 919.024 2354.25 921.553 2352.3 924.081 2350.3 926.687 2346.94 931.137 2343.73 935.699 2340.64 940.339 2395.66 940.339 2450.68 940.339 2507.36 940.339 2503.06 948.942 2500.32 952.707 2493.62 959.083 2485.57 966.934 2478.08 975.031 2470.75 983.552 2465.71 989.373 2460.57 995.09 2455.41 1000.8 2448.91 1008.01 2442.45 1015.25 2436.08 1022.57 2427.88 1031.99 2419.53 1041.26 2411.16 1050.54 2406.54 1055.69 2401.97 1060.88 2397.42 1066.11 2389.23 1075.53 2380.87 1084.8 2372.51 1094.07 2367.88 1099.23 2363.31 1104.42 2358.76 1109.64 2352.39 1116.96 2345.93 1124.2 2339.43 1131.41 2327.79 1144.34 2316.29 1157.39 2305 1170.64 2291.53 1186.46 2277.92 1202.08 2263.88 1217.4 2259.2 1222.54 2254.59 1227.74 2250.04 1232.99 2243.3 1240.75 2236.42 1248.37 2229.5 1255.97 2217.53 1269.12 2205.76 1282.39 2194.29 1295.98 2179.33 1313.68 2163.93 1330.84 2147.96 1347.65 2138.56 1357.55 2129.57 1367.67 2120.77 1378.11 2118.35 1373.27 2118.35 1373.27 2120.39 1367.04 2122.03 1363.06 2122.03 1363.06 2123.71 1359.01 2124.32 1357.55 2124.92 1356.08 2125.54 1354.57 2127.56 1349.66 2129.6 1344.76 2131.64 1339.86 2133.07 1336.41 2134.49 1332.96 2135.91 1329.51 2138.15 1324.04 2140.41 1318.58 2142.66 1313.11 2150.65 1293.77 2158.48 1274.36 2166.31 1254.95 2199.1 1173.69 2199.1 1173.69 2211.83 1146.37 2217.42 1134.28 2221.81 1121.88 2226.1 1109.27 2230.83 1096.33 2236.28 1083.66 2241.58 1070.94 2192.94 1070.94 2144.3 1070.94 2094.19 1070.94 2099.03 1056.4 2104.01 1042.11 2109.59 1027.86 2110.27 1026.13 2110.94 1024.39 2111.63 1022.6 2117.65 1007.17 2124.03 991.896 2130.43 976.618 2149.93 930.008 2169.27 883.341 2188.17 836.485 2197.34 813.764 2206.57 791.079 2216.17 768.533 2222.38 753.917 2228.24 739.174 2234.02 724.383 2237.71 715.502 2237.71 715.502 2243.72 715.11Z"
fill=
"#85EEEE"
fill-rule=
"evenodd"
fill-opacity=
"1"
/><path
d=
"M1927.2 721.344C1930.12 721.342 1933.05 721.34 1936.06 721.338 1944.07 721.346 1952.07 721.407 1960.07 721.492 1968.44 721.569 1976.81 721.576 1985.19 721.59 2001.03 721.628 2016.87 721.729 2032.71 721.852 2050.76 721.989 2068.8 722.056 2086.84 722.117 2123.94 722.245 2161.04 722.463 2198.14 722.733 2193.36 735.655 2188.57 748.578 2183.64 761.891 2146.96 762.118 2110.28 762.293 2073.6 762.399 2056.57 762.449 2039.54 762.518 2022.5 762.63 2006.07 762.738 1989.64 762.795 1973.21 762.82 1966.94 762.838 1960.66 762.874 1954.39 762.926 1945.61 762.998 1936.84 763.007 1928.06 763.003 1925.46 763.038 1922.86 763.073 1920.17 763.109 1902.63 763.011 1902.63 763.011 1893.13 757.453 1886.75 749.869 1885.86 744.872 1886.21 734.97 1896.56 718.983 1910.1 721.071 1927.2 721.344Z"
fill=
"#8CF1EA"
fill-rule=
"evenodd"
fill-opacity=
"1"
/><path
d=
"M1788.49 873.25C1793.21 873.192 1793.21 873.192 1798.03 873.133 1801.45 873.155 1804.86 873.18 1808.28 873.208 1811.8 873.2 1815.33 873.188 1818.85 873.173 1826.23 873.154 1833.6 873.181 1840.97 873.238 1850.41 873.308 1859.84 873.268 1869.28 873.194 1876.55 873.15 1883.82 873.164 1891.1 873.196 1894.58 873.204 1898.05 873.194 1901.53 873.165 1906.4 873.134 1911.27 873.19 1916.13 873.25 1918.9 873.26 1921.66 873.269 1924.51 873.28 1934.25 875.329 1937.93 879.24 1943.99 887.125 1944.29 895.878 1944.29 895.878 1941.58 904.324 1932.61 913.514 1926.51 915.397 1913.94 915.502 1910.84 915.548 1907.74 915.595 1904.55 915.643 1901.18 915.642 1897.82 915.638 1894.46 915.63 1890.99 915.649 1887.52 915.67 1884.05 915.694 1876.79 915.73 1869.53 915.729 1862.27 915.703 1852.98 915.675 1843.69 915.758 1834.4 915.872 1827.24 915.944 1820.08 915.949 1812.92 915.933 1809.49 915.936 1806.07 915.962 1802.64 916.011 1776 916.349 1776 916.349 1767.9 910.971 1763.19 905.453 1760.82 900.185 1759.12 893.114 1763.01 877.664 1774.07 873.301 1788.49 873.25Z"
fill=
"#75DCE5"
fill-rule=
"evenodd"
fill-opacity=
"1"
/><path
d=
"M1932.29 1029.16C1935.39 1029.16 1938.5 1029.16 1941.7 1029.16 1945.08 1029.2 1948.46 1029.24 1951.84 1029.27 1955.3 1029.29 1958.77 1029.3 1962.23 1029.31 1971.34 1029.33 1980.45 1029.4 1989.55 1029.48 1998.85 1029.56 2008.15 1029.59 2017.45 1029.63 2035.68 1029.7 2053.91 1029.83 2072.15 1029.98 2068.1 1041.98 2064.06 1053.99 2060.02 1066 2041.53 1066.22 2023.04 1066.39 2004.56 1066.5 1995.97 1066.55 1987.39 1066.61 1978.8 1066.72 1968.93 1066.85 1959.06 1066.89 1949.18 1066.94 1946.1 1066.99 1943.02 1067.03 1939.85 1067.09 1935.55 1067.09 1935.55 1067.09 1931.17 1067.09 1928.65 1067.11 1926.13 1067.13 1923.53 1067.15 1915.24 1065.7 1912.41 1062.84 1907.24 1056.39 1905.36 1049.29 1906.27 1044.7 1907.24 1037.18 1914.88 1029.2 1921.51 1029.1 1932.29 1029.16Z"
fill=
"#69D7E9"
fill-rule=
"evenodd"
fill-opacity=
"1"
/><path
d=
"M2026.67 873.654C2029.76 873.671 2032.86 873.688 2036.06 873.706 2044.27 873.752 2052.47 873.87 2060.68 874.004 2069.07 874.128 2077.46 874.183 2085.85 874.244 2102.29 874.377 2118.72 874.579 2135.16 874.833 2134.69 876.207 2134.22 877.58 2133.74 878.995 2129.88 890.425 2126.28 901.862 2123.06 913.488 2105.01 913.855 2086.96 914.109 2068.91 914.284 2062.77 914.357 2056.63 914.456 2050.5 914.581 2041.67 914.757 2032.85 914.84 2024.02 914.903 2021.27 914.978 2018.53 915.053 2015.7 915.13 1997.14 915.138 1997.14 915.138 1990.33 910.382 1985.56 904.899 1982.88 899.841 1981.16 892.802 1982.85 886.236 1985.24 882.088 1990.17 877.396 2000.97 871.389 2014.68 873.406 2026.67 873.654Z"
fill=
"#7FE8E8"
fill-rule=
"evenodd"
fill-opacity=
"1"
/><path
d=
"M1913.53 1105.25C1916.22 1105.25 1918.92 1105.24 1921.7 1105.23 1927.38 1105.24 1933.07 1105.27 1938.76 1105.33 1947.47 1105.41 1956.18 1105.38 1964.9 1105.33 1970.43 1105.35 1975.95 1105.37 1981.48 1105.4 1984.09 1105.39 1986.7 1105.38 1989.39 1105.37 1993.03 1105.42 1993.03 1105.42 1996.74 1105.48 1999.94 1105.5 1999.94 1105.5 2003.2 1105.53 2010.81 1107.18 2013.75 1110.52 2018.62 1116.47 2019.99 1121.31 2019.99 1121.31 2019.98 1126.16 2020.03 1127.76 2020.09 1129.36 2020.14 1131.01 2017.62 1139.06 2013.73 1141.19 2006.54 1145.54 1997.49 1147.03 1988.52 1146.96 1979.36 1146.92 1976.72 1146.93 1974.09 1146.95 1971.37 1146.96 1965.81 1146.98 1960.24 1146.97 1954.68 1146.94 1946.17 1146.91 1937.66 1146.99 1929.15 1147.08 1923.74 1147.09 1918.33 1147.08 1912.92 1147.07 1910.37 1147.1 1907.83 1147.13 1905.21 1147.17 1887.61 1146.94 1887.61 1146.94 1878.09 1139.85 1873.65 1133.43 1873.65 1133.43 1872.14 1126.16 1876.78 1103.84 1894.84 1105.02 1913.53 1105.25Z"
fill=
"#58C5E9"
fill-rule=
"evenodd"
fill-opacity=
"1"
/><path
d=
"M2097.24 1106.37C2099.9 1106.38 2102.56 1106.4 2105.29 1106.42 2113.76 1106.48 2122.23 1106.63 2130.7 1106.78 2136.45 1106.84 2142.19 1106.9 2147.94 1106.95 2162.02 1107.08 2176.09 1107.28 2190.17 1107.53 2185.96 1120.52 2181.36 1133.09 2175.72 1145.55 2160.84 1145.9 2145.97 1146.16 2131.09 1146.33 2126.03 1146.4 2120.97 1146.5 2115.91 1146.62 2108.64 1146.8 2101.36 1146.88 2094.08 1146.94 2091.83 1147.01 2089.57 1147.09 2087.24 1147.16 2071.91 1147.17 2071.91 1147.17 2065.59 1142.49 2060.81 1137.05 2057.9 1132.2 2056.17 1125.2 2061.58 1104.39 2079.4 1105.94 2097.24 1106.37Z"
fill=
"#70D8E4"
fill-rule=
"evenodd"
fill-opacity=
"1"
/><path
d=
"M2011.84 951.436C2015.75 951.457 2015.75 951.457 2019.75 951.478 2028.07 951.532 2036.39 951.653 2044.71 951.776 2050.36 951.824 2056.01 951.869 2061.66 951.908 2075.49 952.015 2089.32 952.176 2103.16 952.38 2098.93 965.593 2094.32 978.379 2088.66 991.047 2073.7 991.286 2058.73 991.461 2043.77 991.578 2038.68 991.627 2033.58 991.693 2028.49 991.776 2021.18 991.893 2013.86 991.948 2006.54 991.991 2003.13 992.066 2003.13 992.066 1999.65 992.142 1983.84 992.148 1983.84 992.148 1976.08 986.457 1972.43 981.018 1971.3 978.21 1971.32 971.713 1971.26 970.118 1971.21 968.523 1971.16 966.88 1976.92 948.547 1996.36 951.13 2011.84 951.436Z"
fill=
"#76DEE4"
fill-rule=
"evenodd"
fill-opacity=
"1"
/><path
d=
"M2091.61 796.255C2093.74 796.272 2095.88 796.289 2098.08 796.307 2104.87 796.373 2111.66 796.525 2118.45 796.678 2123.07 796.738 2127.68 796.793 2132.3 796.843 2143.59 796.975 2154.88 797.183 2166.17 797.431 2164.51 806.146 2162.27 813.891 2158.9 822.117 2157.62 825.265 2157.62 825.265 2156.32 828.477 2154.05 833.557 2154.05 833.557 2151.63 835.965 2145.45 836.298 2139.35 836.479 2133.17 836.539 2131.32 836.559 2129.46 836.578 2127.55 836.598 2123.61 836.632 2119.67 836.658 2115.73 836.676 2109.71 836.718 2103.7 836.823 2097.68 836.93 2093.86 836.954 2090.04 836.975 2086.21 836.991 2083.52 837.054 2083.52 837.054 2080.76 837.119 2072.38 837.084 2067.66 836.597 2060.82 831.602 2055.3 823.783 2055.62 818.791 2057.1 809.473 2067.1 795.805 2075.86 795.779 2091.61 796.255Z"
fill=
"#88EEE8"
fill-rule=
"evenodd"
fill-opacity=
"1"
/><path
d=
"M1980.23 796.731C1983.33 796.745 1986.43 796.759 1989.63 796.774 2015.53 797.115 2015.53 797.115 2023.74 802.658 2029.39 811.148 2029.81 814.554 2028.59 824.543 2024.06 830.142 2020.45 833.487 2014.04 836.701 2004.08 837.382 1994.12 837.463 1984.15 837.552 1980.83 837.592 1977.52 837.659 1974.21 837.752 1969.42 837.885 1964.64 837.93 1959.85 837.974 1956.99 838.022 1954.12 838.07 1951.17 838.12 1943.69 836.701 1943.69 836.701 1937.91 830.998 1933.05 823.014 1932.26 819.146 1933.98 809.953 1944.67 792.74 1961.84 796.256 1980.23 796.731Z"
fill=
"#87ECE8"
fill-rule=
"evenodd"
fill-opacity=
"1"
/><path
d=
"M1796.39 721.481C1801.44 721.501 1806.48 721.432 1811.53 721.356 1816.33 721.358 1816.33 721.358 1821.23 721.361 1824.15 721.356 1827.07 721.352 1830.08 721.347 1839.5 722.784 1843.54 725.399 1850.1 732.16 1851.47 740.567 1851.47 740.567 1850.1 748.974 1844.61 756.484 1841.67 758.381 1832.34 759.863 1827.63 759.903 1827.63 759.903 1822.83 759.943 1821.14 759.958 1819.46 759.972 1817.72 759.988 1814.17 760.005 1810.61 759.997 1807.06 759.967 1801.63 759.933 1796.21 760.015 1790.78 760.107 1787.32 760.109 1783.86 760.105 1780.4 760.093 1777.26 760.094 1774.12 760.096 1770.89 760.098 1762.82 758.582 1762.82 758.582 1756.88 753.027 1753.12 746.572 1753.12 746.572 1753.12 737.865 1759.23 717.439 1778.26 721.369 1796.39 721.481Z"
fill=
"#89F0EB"
fill-rule=
"evenodd"
fill-opacity=
"1"
/><path
d=
"M2116.42 1184.19C2120.16 1184.23 2120.16 1184.23 2123.97 1184.26 2127.85 1184.36 2127.85 1184.36 2131.81 1184.45 2135.75 1184.5 2135.75 1184.5 2139.76 1184.55 2146.23 1184.63 2152.7 1184.75 2159.17 1184.9 2155.12 1196.94 2151.07 1208.98 2147.01 1221.02 2139.51 1221.38 2132.02 1221.59 2124.51 1221.78 2122.39 1221.88 2120.27 1221.98 2118.08 1222.08 2102.36 1222.38 2102.36 1222.38 2094.45 1215.5 2089.82 1206.58 2090.66 1201.63 2093.5 1192.12 2100.9 1184.93 2106.24 1184.03 2116.42 1184.19Z"
fill=
"#5FCCE5"
fill-rule=
"evenodd"
fill-opacity=
"1"
/></g></g></svg>
lightx2v/deploy/server/frontend/public/robots.txt
0 → 100644
View file @
d8558a0c
User-agent: *
Allow: /
Sitemap: https://x2v.light-ai.top/sitemap.xml
lightx2v/deploy/server/frontend/public/sitemap.xml
0 → 100644
View file @
d8558a0c
<?xml version="1.0" encoding="UTF-8"?>
<urlset
xmlns=
"http://www.sitemaps.org/schemas/sitemap/0.9"
>
<url>
<loc>
https://x2v.light-ai.top/
</loc>
<changefreq>
daily
</changefreq>
<priority>
1.0
</priority>
</url>
<url>
<loc>
https://x2v.light-ai.top/examples
</loc>
</url>
<url>
<loc>
https://x2v.light-ai.top/docs
</loc>
</url>
</urlset>
lightx2v/deploy/server/frontend/src/components/Alert.vue
View file @
d8558a0c
...
...
@@ -62,10 +62,10 @@ const handleScroll = () => {
if
(
scrollY
>
50
)
{
// 计算Alert应该显示的位置,确保在视口内可见
// 距离顶部80px(TopBar高度 + 一些间距)
alertPosition
.
value
=
{
top
:
'
80px
'
}
alertPosition
.
value
=
{
top
:
'
6rem
'
}
}
else
{
// 在页面顶部时,显示在固定位置
alertPosition
.
value
=
{
top
:
'
1rem
'
}
alertPosition
.
value
=
{
top
:
'
1
.5
rem
'
}
}
},
10
)
// 10ms防抖延迟
}
...
...
@@ -106,9 +106,9 @@ onUnmounted(() => {
@
after-leave=
"handleAfterLeave"
>
<div
v-if=
"alert.show"
:key=
"alert._timestamp || alert.message"
class=
"fixed
left-1/2 transform -translate-x-1/2
z-[9999] w-auto min-w-[2
8
0px] sm:min-w-[3
2
0px] max-w-[calc(100vw-
3
rem)] sm:max-w-
xl
px-
6
sm:px-
6
transition-all duration-500 ease-out"
:style=
"alertPosition"
>
<div
class=
"alert-container"
>
class=
"fixed
right-6
z-[9999] w-auto min-w-[2
6
0px] sm:min-w-[3
0
0px] max-w-[calc(100vw-
2.5
rem)] sm:max-w-
md
px-
4
sm:px-
5
transition-all duration-500 ease-out"
:style=
"
{ top:
alertPosition
.top }
">
<div
class=
"alert-container
text-[#1d1d1f] bg-white/95 dark:text-white dark:bg-[#0d0d12]/90 dark:shadow-[0_12px_32px_rgba(0,0,0,0.6),0_4px_12px_rgba(0,0,0,0.4),0_0_0_1px_rgba(255,255,255,0.06)]
"
>
<div
class=
"alert-content"
>
<!-- 图标 -->
<div
class=
"alert-icon-wrapper"
>
...
...
@@ -139,7 +139,6 @@ onUnmounted(() => {
<
style
scoped
>
/* Apple 风格 Alert 容器 */
.alert-container
{
background
:
rgba
(
255
,
255
,
255
,
0.95
);
backdrop-filter
:
blur
(
20px
)
saturate
(
180%
);
-webkit-backdrop-filter
:
blur
(
20px
)
saturate
(
180%
);
border-radius
:
16px
;
...
...
@@ -150,15 +149,6 @@ onUnmounted(() => {
overflow
:
hidden
;
}
/* 深色模式下的容器样式 */
:global
(
.dark
)
.alert-container
{
background
:
rgba
(
30
,
30
,
30
,
0.95
);
box-shadow
:
0
4px
6px
-1px
rgba
(
0
,
0
,
0
,
0.3
),
0
2px
4px
-1px
rgba
(
0
,
0
,
0
,
0.2
),
0
0
0
1px
rgba
(
255
,
255
,
255
,
0.08
);
}
/* Alert 内容 */
.alert-content
{
display
:
flex
;
...
...
@@ -178,11 +168,11 @@ onUnmounted(() => {
/* 图标样式 */
.alert-icon
{
font-size
:
18px
;
color
:
#1d1d1f
;
color
:
var
(
--brand-primary
)
;
}
:global
(
.dark
)
.alert-icon
{
color
:
#f5f5f7
;
color
:
var
(
--brand-primary-light
)
;
}
/* 消息文本 */
...
...
@@ -191,15 +181,10 @@ onUnmounted(() => {
font-size
:
14px
;
font-weight
:
500
;
line-height
:
1.5
;
color
:
#1d1d1f
;
letter-spacing
:
-0.01em
;
min-width
:
0
;
/* 允许文本收缩 */
}
:global
(
.dark
)
.alert-message
{
color
:
#f5f5f7
;
}
/* 操作按钮和关闭按钮容器 */
.alert-actions
{
display
:
flex
;
...
...
@@ -273,11 +258,11 @@ onUnmounted(() => {
}
:global
(
.dark
)
.alert-action-link
{
color
:
var
(
--brand-primary-light
)
;
color
:
#ffffff
;
}
:global
(
.dark
)
.alert-action-link
:hover
{
color
:
var
(
--brand-primary-light
)
;
color
:
#ffffff
;
}
/* 进入动画 */
...
...
lightx2v/deploy/server/frontend/src/components/Confirm.vue
View file @
d8558a0c
...
...
@@ -7,7 +7,7 @@ const { t, locale } = useI18n()
<
template
>
<!-- 自定义确认对话框 - Apple 极简风格 -->
<div
v-cloak
>
<div
v-if=
"confirmDialog.show"
class=
"fixed inset-0 z-[
9999
] flex items-center justify-center p-4"
>
<div
v-if=
"confirmDialog.show"
class=
"fixed inset-0 z-[
70
] flex items-center justify-center p-4"
>
<!-- 背景遮罩 - Apple 风格 -->
<div
class=
"absolute inset-0 bg-black/50 dark:bg-black/60 backdrop-blur-sm"
@
click=
"confirmDialog.cancel()"
>
</div>
...
...
lightx2v/deploy/server/frontend/src/components/Generate.vue
View file @
d8558a0c
This diff is collapsed.
Click to expand it.
lightx2v/deploy/server/frontend/src/components/Inspirations.vue
View file @
d8558a0c
...
...
@@ -42,7 +42,8 @@ import {
onVideoError
,
onVideoEnded
,
openTemplateFromRoute
,
copyShareLink
copyShareLink
,
isPageLoading
}
from
'
../utils/other
'
// 监听模板详情路由
...
...
@@ -201,7 +202,14 @@ onMounted(() => {
</div>
<!-- 灵感内容网格 - Apple 风格 -->
<div
class=
"columns-2 md:columns-3 lg:columns-4 xl:columns-5 gap-4"
>
<div
class=
"space-y-4"
>
<div
v-if=
"isPageLoading"
class=
"flex items-center justify-center"
>
<div
class=
"inline-flex items-center gap-3 px-4 py-2 rounded-full bg-white/90 dark:bg-[#2c2c2e]/90 border border-black/8 dark:border-white/8 text-sm text-[#1d1d1f] dark:text-[#f5f5f7] shadow-[0_4px_16px_rgba(0,0,0,0.08)] dark:shadow-[0_4px_16px_rgba(0,0,0,0.35)]"
>
<i
class=
"fas fa-spinner fa-spin text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"
></i>
<span>
{{ t('loading') }}
</span>
</div>
</div>
<div
class=
"columns-2 md:columns-3 lg:columns-4 xl:columns-5 gap-4"
>
<!-- 灵感卡片 - Apple 风格 -->
<div
v-for=
"item in inspirationItems"
:key=
"item.task_id"
class=
"break-inside-avoid mb-4 group relative bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] rounded-2xl overflow-hidden border border-black/8 dark:border-white/8 hover:border-[color:var(--brand-primary)]/30 dark:hover:border-[color:var(--brand-primary-light)]/30 hover:bg-white dark:hover:bg-[#3a3a3c] transition-all duration-200 hover:shadow-[0_8px_24px_rgba(var(--brand-primary-rgb),0.15)] dark:hover:shadow-[0_8px_24px_rgba(var(--brand-primary-light-rgb),0.2)]"
>
...
...
@@ -260,6 +268,7 @@ onMounted(() => {
</div>
</div>
</div>
</div>
</div>
<!-- GitHub 仓库链接 - Apple 极简风格 -->
...
...
lightx2v/deploy/server/frontend/src/components/LeftBar.vue
View file @
d8558a0c
...
...
@@ -4,12 +4,12 @@ import { useI18n } from 'vue-i18n'
const
{
t
,
locale
}
=
useI18n
()
</
script
>
<
template
>
<!-- 左侧功能区 - 响应式悬浮按钮 -->
<!-- 左侧功能区 -
Apple 极简风格 -
响应式悬浮按钮 -->
<div
class=
"fixed top-20 sm:top-1/2 sm:-translate-y-1/2 right-3 sm:right-auto sm:left-5 z-[10] w-auto"
>
<!-- 功能导航-->
<!-- 功能导航
- Apple 风格统一容器
-->
<div
class=
"p-2 flex flex-col justify-center"
>
<!-- 统一的圆角矩形容器-->
<nav
class=
"bg-white/95 dark:bg-[#1e1e1e]/95 backdrop-blur-[40px] border border-black/10 dark:border-white/10 rounded-
ful
l shadow-[0_4px_16px_rgba(0,0,0,0.1)] dark:shadow-[0_4px_16px_rgba(0,0,0,0.4)] overflow-hidden flex flex-col w-1
2
sm:w-1
4
"
>
<!-- 统一的圆角矩形容器
- Apple 风格
-->
<nav
class=
"bg-white/95 dark:bg-[#1e1e1e]/95 backdrop-blur-[40px] border border-black/10 dark:border-white/10 rounded-
3x
l shadow-[0_4px_16px_rgba(0,0,0,0.1)] dark:shadow-[0_4px_16px_rgba(0,0,0,0.4)] overflow-hidden flex flex-col w-1
4
sm:w-1
6
"
>
<!-- 生成视频功能 -->
<div
...
...
lightx2v/deploy/server/frontend/src/components/MediaTemplate.vue
View file @
d8558a0c
This diff is collapsed.
Click to expand it.
lightx2v/deploy/server/frontend/src/components/Projects.vue
View file @
d8558a0c
...
...
@@ -32,6 +32,7 @@ import {
loginLoading
,
initLoading
,
downloadLoading
,
downloadLoadingMessage
,
// 录音相关
isRecording
,
...
...
@@ -105,7 +106,7 @@ import {
templateFileCache
,
templateFileCacheLoaded
,
loadTaskFiles
,
d
ownloadFile
,
handleD
ownloadFile
,
viewFile
,
handleImageUpload
,
selectTask
,
...
...
@@ -292,107 +293,14 @@ import {
generateShareUrl
,
copyShareLink
,
shareToSocial
,
openTaskFromRoute
openTaskFromRoute
,
isPageLoading
}
from
'
../utils/other
'
// 路由监听
const
route
=
useRoute
()
const
router
=
useRouter
()
// 处理任务下载
const
handleDownloadTask
=
async
(
task
)
=>
{
try
{
console
.
log
(
'
开始下载任务文件:
'
,
{
taskId
:
task
.
task_id
,
outputs
:
task
.
outputs
})
// 处理文件名,确保有正确的后缀名
let
fileName
=
task
.
outputs
?.
output_video
||
'
video.mp4
'
if
(
fileName
&&
typeof
fileName
===
'
string
'
)
{
// 检查是否已有后缀名
const
hasExtension
=
/
\.[
a-zA-Z0-9
]
+$/
.
test
(
fileName
)
if
(
!
hasExtension
)
{
// 没有后缀名,添加mp4后缀
fileName
=
`
${
fileName
}
.mp4`
console
.
log
(
'
添加后缀名:
'
,
fileName
)
}
}
// 先尝试从缓存获取
let
fileData
=
getTaskFileFromCache
(
task
.
task_id
,
'
output_video
'
)
console
.
log
(
'
缓存中的文件数据:
'
,
fileData
)
if
(
fileData
&&
fileData
.
blob
)
{
// 缓存中有blob数据,直接使用
console
.
log
(
'
使用缓存中的文件数据
'
)
downloadFile
({
...
fileData
,
name
:
fileName
})
return
}
if
(
fileData
&&
fileData
.
url
)
{
// 缓存中有URL,使用URL下载
console
.
log
(
'
使用缓存中的URL下载:
'
,
fileData
.
url
)
try
{
const
response
=
await
fetch
(
fileData
.
url
)
console
.
log
(
'
文件响应状态:
'
,
response
.
status
,
response
.
ok
)
if
(
response
.
ok
)
{
const
blob
=
await
response
.
blob
()
console
.
log
(
'
文件blob大小:
'
,
blob
.
size
)
const
downloadData
=
{
blob
:
blob
,
name
:
fileName
}
console
.
log
(
'
构造的文件数据:
'
,
downloadData
)
downloadFile
(
downloadData
)
return
}
else
{
console
.
error
(
'
文件响应失败:
'
,
response
.
status
,
response
.
statusText
)
}
}
catch
(
error
)
{
console
.
error
(
'
使用缓存URL下载失败:
'
,
error
)
}
}
if
(
!
fileData
)
{
console
.
log
(
'
缓存中没有文件,尝试异步获取...
'
)
// 缓存中没有,尝试异步获取
const
url
=
await
getTaskFileUrl
(
task
.
task_id
,
'
output_video
'
)
console
.
log
(
'
获取到的文件URL:
'
,
url
)
if
(
url
)
{
const
response
=
await
fetch
(
url
)
console
.
log
(
'
文件响应状态:
'
,
response
.
status
,
response
.
ok
)
if
(
response
.
ok
)
{
const
blob
=
await
response
.
blob
()
console
.
log
(
'
文件blob大小:
'
,
blob
.
size
)
fileData
=
{
blob
:
blob
,
name
:
fileName
}
console
.
log
(
'
构造的文件数据:
'
,
fileData
)
}
else
{
console
.
error
(
'
文件响应失败:
'
,
response
.
status
,
response
.
statusText
)
}
}
else
{
console
.
error
(
'
无法获取文件URL
'
)
}
}
if
(
fileData
&&
fileData
.
blob
)
{
console
.
log
(
'
开始下载文件:
'
,
fileData
.
name
)
downloadFile
(
fileData
)
}
else
{
console
.
error
(
'
文件数据无效:
'
,
fileData
)
showAlert
(
t
(
'
fileUnavailableAlert
'
),
'
danger
'
)
}
}
catch
(
error
)
{
console
.
error
(
'
下载失败:
'
,
error
)
showAlert
(
t
(
'
downloadFailedAlert
'
),
'
danger
'
)
}
}
// 监听路由变化,处理任务详情路由
watch
(()
=>
route
.
params
.
taskId
,
(
newTaskId
)
=>
{
if
(
newTaskId
&&
route
.
name
===
'
TaskDetail
'
)
{
...
...
@@ -436,6 +344,12 @@ watch([taskSearchQuery, statusFilter, currentTaskPage], () => {
</
script
>
<
template
>
<div
v-if=
"downloadLoading"
class=
"fixed inset-0 z-[120] flex justify-center items-center bg-black/10 dark:bg-black/30 pointer-events-none backdrop-blur-sm"
>
<div
class=
"pointer-events-auto px-5 py-3 rounded-2xl bg-white/85 dark:bg-[#2c2c2e]/85 border border-black/10 dark:border-white/10 backdrop-blur-[14px] shadow-[0_16px_40px_rgba(0,0,0,0.18)] dark:shadow-[0_16px_40px_rgba(0,0,0,0.5)] flex items-center gap-2 text-sm text-[#1d1d1f] dark:text-[#f5f5f7]"
>
<i
class=
"fas fa-spinner fa-spin text-base text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"
></i>
<span>
{{
downloadLoadingMessage
||
t
(
'
downloadPreparing
'
)
}}
</span>
</div>
</div>
<!-- 历史任务区域 - Apple 极简风格 -->
<div
class=
"flex-1 flex flex-col min-h-0 mobile-content"
>
<!-- 内容区域 -->
...
...
@@ -545,15 +459,25 @@ watch([taskSearchQuery, statusFilter, currentTaskPage], () => {
</div>
<!-- 任务内容网格 - Apple 风格 -->
<div
v-if=
"filteredTasks.length === 0"
class=
"flex flex-col items-center justify-center py-16 text-center"
>
<div
class=
"w-20 h-20 bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 rounded-full flex items-center justify-center mb-6"
>
<i
class=
"fas fa-video text-3xl text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"
></i>
</div>
<p
class=
"text-lg font-medium text-[#1d1d1f] dark:text-[#f5f5f7] mb-2 tracking-tight"
>
{{ t('noHistoryTasks') }}
</p>
<p
class=
"text-sm text-[#86868b] dark:text-[#98989d] tracking-tight"
>
{{ t('startToCreateYourFirstAIVideo') }}
</p>
<div
class=
"space-y-4"
>
<div
v-if=
"isPageLoading"
class=
"flex items-center justify-center"
>
<div
class=
"inline-flex items-center gap-3 px-4 py-2 rounded-full bg-white/90 dark:bg-[#2c2c2e]/90 border border-black/8 dark:border-white/8 text-sm text-[#1d1d1f] dark:text-[#f5f5f7] shadow-[0_4px_16px_rgba(0,0,0,0.08)] dark:shadow-[0_4px_16px_rgba(0,0,0,0.35)]"
>
<i
class=
"fas fa-spinner fa-spin text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"
></i>
<span>
{{ t('loading') }}
</span>
</div>
</div>
<
template
v-if=
"filteredTasks.length === 0"
>
<div
class=
"flex flex-col items-center justify-center py-16 text-center"
>
<div
class=
"w-20 h-20 bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 rounded-full flex items-center justify-center mb-6"
>
<i
class=
"fas fa-video text-3xl text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"
></i>
</div>
<p
class=
"text-lg font-medium text-[#1d1d1f] dark:text-[#f5f5f7] mb-2 tracking-tight"
>
{{
t
(
'
noHistoryTasks
'
)
}}
</p>
<p
class=
"text-sm text-[#86868b] dark:text-[#98989d] tracking-tight"
>
{{
t
(
'
startToCreateYourFirstAIVideo
'
)
}}
</p>
</div>
</
template
>
<div
v-else
class=
"columns-2 md:columns-3 lg:columns-4 xl:columns-5 gap-4"
>
<div
v-else
class=
"columns-2 md:columns-3 lg:columns-4 xl:columns-5 gap-4"
>
<!-- 任务卡片 - Apple 风格 -->
<div
v-for=
"task in filteredTasks"
:key=
"task.task_id"
class=
"break-inside-avoid mb-4 group relative bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] rounded-2xl overflow-hidden border border-black/8 dark:border-white/8 hover:border-[color:var(--brand-primary)]/30 dark:hover:border-[color:var(--brand-primary-light)]/30 hover:bg-white dark:hover:bg-[#3a3a3c] transition-all duration-200 hover:shadow-[0_8px_24px_rgba(var(--brand-primary-rgb),0.15)] dark:hover:shadow-[0_8px_24px_rgba(var(--brand-primary-light-rgb),0.2)]"
>
...
...
@@ -633,8 +557,10 @@ watch([taskSearchQuery, statusFilter, currentTaskPage], () => {
<!-- 下载按钮 - 成功状态 -->
<button
v-if=
"task.status === 'SUCCEED'"
@
click.stop=
"handleDownloadTask(task)"
class=
"w-10 h-10 rounded-full bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] backdrop-blur-[20px] shadow-[0_2px_8px_rgba(var(--brand-primary-rgb),0.3)] dark:shadow-[0_2px_8px_rgba(var(--brand-primary-light-rgb),0.4)] flex items-center justify-center text-white hover:scale-110 active:scale-100 transition-all duration-200"
@
click.stop=
"handleDownloadFile(task.task_id, 'output_video', task.outputs.output_video)"
:disabled=
"downloadLoading"
class=
"w-10 h-10 rounded-full bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] backdrop-blur-[20px] shadow-[0_2px_8px_rgba(var(--brand-primary-rgb),0.3)] dark:shadow-[0_2px_8px_rgba(var(--brand-primary-light-rgb),0.4)] flex items-center justify-center text-white transition-all duration-200"
:class=
"downloadLoading ? 'opacity-60 cursor-not-allowed' : 'hover:scale-110 active:scale-100'"
:title=
"t('downloadTask')"
>
<i
class=
"fas fa-download text-sm"
></i>
</button>
...
...
@@ -662,9 +588,10 @@ watch([taskSearchQuery, statusFilter, currentTaskPage], () => {
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- GitHub 仓库链接 - Apple 极简风格 -->
<div
class=
"fixed bottom-6 right-6 z-50"
>
...
...
lightx2v/deploy/server/frontend/src/components/SiteFooter.vue
0 → 100644
View file @
d8558a0c
<
script
setup
>
import
{
useI18n
}
from
'
vue-i18n
'
const
currentYear
=
new
Date
().
getFullYear
()
const
{
t
}
=
useI18n
()
</
script
>
<
template
>
<div
class=
"bg-transparent text-neutral-600 dark:text-neutral-300"
>
<div
class=
"mx-auto w-full max-w-4xl pt-16 pb-16 text-center"
>
<div
class=
"flex flex-col gap-10 items-center justify-center"
>
<div
class=
"max-w-sm space-y-6"
>
<div
class=
"inline-flex items-center justify-center gap-3 text-xl font-semibold tracking-tight text-neutral-900 dark:text-white"
>
<img
src=
"../../public/logo.svg"
alt=
"LightX2V"
class=
"h-8 w-8"
loading=
"lazy"
/>
<span>
LightX2V
</span>
</div>
<p
class=
"leading-relaxed text-neutral-500 dark:text-neutral-400"
>
{{
t
(
'
footer.tagline
'
)
}}
</p>
<div
class=
"flex flex-wrap items-center justify-center gap-5 text-sm text-neutral-500 dark:text-neutral-400"
>
<a
href=
"https://www.light-ai.top/"
target=
"_blank"
rel=
"noopener"
class=
"transition hover:text-neutral-900 dark:hover:text-white"
>
{{
t
(
'
footer.links.home
'
)
}}
</a>
<a
href=
"https://github.com/ModelTC/LightX2V"
target=
"_blank"
rel=
"noopener"
class=
"inline-flex items-center gap-2 transition hover:text-neutral-900 dark:hover:text-white"
>
<img
src=
"https://github.githubassets.com/favicons/favicon.svg"
:alt=
"t('footer.alt.github')"
class=
"h-4 w-4 transition dark:invert"
loading=
"lazy"
/>
{{
t
(
'
footer.links.github
'
)
}}
</a>
<a
href=
"https://xhslink.com/m/45NsEK8minq"
class=
"inline-flex items-center gap-2 transition hover:text-neutral-900 dark:hover:text-white"
>
<img
src=
"https://www.xiaohongshu.com/favicon.ico"
:alt=
"t('footer.alt.xiaohongshu')"
class=
"h-4 w-4"
loading=
"lazy"
/>
{{
t
(
'
footer.links.xiaohongshu
'
)
}}
</a>
</div>
</div>
</div>
<div
class=
"mt-12 border-t border-black/10 dark:border-white/10 pt-8 text-xs text-neutral-400 dark:text-neutral-500 flex items-center justify-center"
>
<p>
{{
t
(
'
footer.copyright
'
,
{
year
:
currentYear
}
)
}}
<
/p
>
<
/div
>
<
/div
>
<
/div
>
<
/template
>
lightx2v/deploy/server/frontend/src/components/TaskCarousel.vue
View file @
d8558a0c
...
...
@@ -17,14 +17,17 @@ import {
showAlert
,
cancelTask
,
resumeTask
,
download
File
,
download
Loading
,
handleDownloadFile
,
getTaskFileFromCache
,
apiRequest
,
copyShareLink
,
deleteTask
,
currentTask
,
startPollingTask
,
openTaskDetailModal
,
playVideo
,
pauseVideo
,
}
from
'
../utils/other
'
const
{
t
}
=
useI18n
()
...
...
@@ -42,6 +45,7 @@ const props = defineProps({
const
isVideoLoaded
=
ref
(
false
)
const
isVideoError
=
ref
(
false
)
const
videoElement
=
ref
(
null
)
const
isMuted
=
ref
(
true
)
// 计算属性
const
sortedTasks
=
computed
(()
=>
{
...
...
@@ -147,6 +151,9 @@ const resetVideoState = () => {
const
onVideoLoaded
=
()
=>
{
isVideoLoaded
.
value
=
true
isVideoError
.
value
=
false
if
(
videoElement
.
value
&&
isMuted
.
value
)
{
videoElement
.
value
.
muted
=
true
}
}
const
onVideoError
=
()
=>
{
...
...
@@ -154,6 +161,24 @@ const onVideoError = () => {
isVideoLoaded
.
value
=
false
}
const
toggleMute
=
(
event
)
=>
{
event
.
stopPropagation
()
isMuted
.
value
=
!
isMuted
.
value
if
(
videoElement
.
value
)
{
videoElement
.
value
.
muted
=
isMuted
.
value
if
(
!
isMuted
.
value
)
{
videoElement
.
value
.
play
().
catch
(()
=>
{})
}
}
}
const
openDetail
=
(
event
)
=>
{
event
?.
stopPropagation
()
if
(
currentTask
.
value
)
{
openTaskDetailModal
(
currentTask
.
value
)
}
}
// 处理取消任务
const
handleCancel
=
async
()
=>
{
...
...
@@ -257,8 +282,15 @@ onUnmounted(() => {
<!-- 视频容器 - Apple 圆角和阴影 -->
<div
class=
"w-full max-w-[280px] sm:max-w-[300px] md:max-w-[400px] lg:max-w-[400px] aspect-[9/16] bg-black dark:bg-[#000000] rounded-[16px] overflow-hidden shadow-[0_8px_24px_rgba(0,0,0,0.15)] dark:shadow-[0_8px_24px_rgba(0,0,0,0.5)] relative cursor-pointer transition-all duration-200 hover:shadow-[0_12px_32px_rgba(0,0,0,0.2)] dark:hover:shadow-[0_12px_32px_rgba(0,0,0,0.6)]"
@
click=
"open
Task
Detail
Modal(currentTask)
"
@
click=
"openDetail"
:title=
"t('viewTaskDetails')"
>
<button
class=
"absolute top-3 left-3 z-20 w-10 h-10 flex items-center justify-center rounded-full bg-black/40 text-white backdrop-blur-sm transition hover:bg-black/55 active:scale-95"
@
click.stop=
"openDetail"
:title=
"t('viewTaskDetails')"
:aria-label=
"t('viewTaskDetails')"
>
<i
class=
"fas fa-info"
></i>
</button>
<!-- 已完成:显示视频播放器 -->
<video
v-if=
"isCompleted && videoUrl"
...
...
@@ -266,12 +298,26 @@ onUnmounted(() => {
:poster=
"imageUrl"
class=
"w-full h-full object-contain"
controls
preload=
"metadata"
@
loadeddata=
"onVideoLoaded"
@
error=
"onVideoError"
preload=
"auto"
autoplay
muted
playsinline
webkit-playsinline
@
mouseenter=
"playVideo($event)"
@
mouseleave=
"pauseVideo($event)"
@
loadeddata=
"onVideoLoaded($event)"
@
ended=
"onVideoEnded($event)"
@
error=
"onVideoError($event)"
ref=
"videoElement"
>
{{
t
(
'
browserNotSupported
'
)
}}
</video>
<button
v-if=
"isCompleted && videoUrl"
class=
"absolute top-3 right-3 z-20 w-10 h-10 flex items-center justify-center rounded-full bg-black/40 text-white backdrop-blur-sm transition hover:bg-black/55 active:scale-95"
@
click.stop=
"toggleMute"
:title=
"isMuted ? t('unmute') : t('mute')"
>
<i
:class=
"isMuted ? 'fas fa-volume-mute' : 'fas fa-volume-up'"
></i>
</button>
<!-- 进行中:Apple 风格加载状态 -->
<div
v-else-if=
"isRunning"
class=
"w-full h-full flex flex-col items-center justify-center relative bg-[#f5f5f7] dark:bg-[#1c1c1e]"
>
...
...
@@ -370,11 +416,20 @@ onUnmounted(() => {
<!-- Apple 风格操作按钮 -->
<div
class=
"flex justify-center gap-3"
>
<button
v-if=
"(isCompleted || isFailed || isCancelled) && currentTask?.task_id"
@
click=
"deleteTask(currentTask.task_id, false)"
class=
"w-[40px] h-[40px] sm:w-[44px] sm:h-[44px] rounded-full flex items-center justify-center text-base transition-all.duration-200.ease-out border-0 cursor-pointer bg-white/95 dark:bg-[#2c2c2e]/95 backdrop-blur-[20px] shadow-[0_2px_8px_rgba(0,0,0,0.12)] dark:shadow-[0_2px_8px_rgba(0,0,0,0.4)] text-red-500 dark:text-red-400 hover:scale-105 active:scale-95"
:title=
"t('delete')"
>
<i
class=
"fas fa-trash"
></i>
</button>
<!-- 已完成:下载按钮 -->
<button
v-if=
"isCompleted && currentTask?.outputs?.output_video"
@
click=
"handleDownloadFile(currentTask.task_id, 'output_video', currentTask.outputs.output_video)"
class=
"w-[40px] h-[40px] sm:w-[44px] sm:h-[44px] rounded-full flex items-center justify-center text-base transition-all duration-200 ease-out border-0 cursor-pointer bg-white/95 dark:bg-[#2c2c2e]/95 backdrop-blur-[20px] shadow-[0_2px_8px_rgba(0,0,0,0.12)] dark:shadow-[0_2px_8px_rgba(0,0,0,0.4)] text-[#1d1d1f] dark:text-[#f5f5f7] hover:scale-105 active:scale-95"
:disabled=
"downloadLoading"
class=
"w-[40px] h-[40px] sm:w-[44px] sm:h-[44px] rounded-full flex items-center justify-center text-base transition-all duration-200 ease-out border-0 cursor-pointer bg-white/95 dark:bg-[#2c2c2e]/95 backdrop-blur-[20px] shadow-[0_2px_8px_rgba(0,0,0,0.12)] dark:shadow-[0_2px_8px_rgba(0,0,0,0.4)] text-[#1d1d1f] dark:text-[#f5f5f7]"
:class=
"downloadLoading ? 'opacity-60 cursor-not-allowed' : 'hover:scale-105 active:scale-95'"
:title=
"t('download')"
>
<i
class=
"fas fa-download"
></i>
</button>
...
...
lightx2v/deploy/server/frontend/src/components/TaskDetails.vue
View file @
d8558a0c
...
...
@@ -5,7 +5,6 @@ import { showTaskDetailModal,
closeTaskDetailModal
,
cancelTask
,
reuseTask
,
downloadFile
,
handleDownloadFile
,
deleteTask
,
getTaskTypeName
,
...
...
@@ -26,6 +25,7 @@ import { showTaskDetailModal,
getTaskFileUrlSync
,
getTaskFileFromCache
,
getTaskFileUrl
,
downloadLoading
,
showAlert
,
apiRequest
,
startPollingTask
,
...
...
@@ -78,6 +78,31 @@ const closeWithRoute = () => {
// 如果不是任务详情路由,不做任何路由跳转,保持在当前页面
}
// 滚动到生成区域(仅在 generate 页面)
const
scrollToCreationArea
=
()
=>
{
const
mainScrollable
=
document
.
querySelector
(
'
.main-scrollbar
'
)
if
(
mainScrollable
)
{
mainScrollable
.
scrollTo
({
top
:
0
,
behavior
:
'
smooth
'
})
}
}
// 包装 reuseTask 函数,复用任务后回到生成区域
const
handleReuseTask
=
()
=>
{
const
task
=
modalTask
.
value
if
(
!
task
)
{
return
}
void
reuseTask
(
task
)
if
(
route
.
path
===
'
/generate
'
||
route
.
name
===
'
Generate
'
)
{
setTimeout
(()
=>
{
scrollToCreationArea
()
},
300
)
}
}
// 键盘事件处理
const
handleKeydown
=
(
event
)
=>
{
if
(
event
.
key
===
'
Escape
'
&&
showTaskDetailModal
.
value
)
{
...
...
@@ -303,25 +328,24 @@ watch(() => getAudioMaterials(), (newMaterials) => {
<!-- 右侧信息区域 -->
<div
class=
"flex items-center justify-center"
>
<div
class=
"w-full max-w-[400px] aspect-[9/16] relative flex flex-col"
>
<!-- 右上角操作按钮 - Apple 极简风格 -->
<div
class=
"absolute top-0 right-0 flex items-center gap-2 z-10"
>
<!-- 分享按钮 -->
<button
@
click=
"copyShareLink(modalTask.task_id, 'task')"
class=
"w-10 h-10 flex items-center justify-center bg-white/95 dark:bg-[#1e1e1e]/95 backdrop-blur-[20px] border border-black/10 dark:border-white/10 rounded-full shadow-[0_2px_8px_rgba(0,0,0,0.12)] dark:shadow-[0_2px_8px_rgba(0,0,0,0.4)] text-[#1d1d1f] dark:text-[#f5f5f7] hover:scale-110 hover:shadow-[0_4px_12px_rgba(0,0,0,0.15)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.5)] active:scale-100 transition-all duration-200"
:title=
"t('share')"
>
<i
class=
"fas fa-share-alt text-xs"
></i>
</button>
<!-- 删除按钮 -->
<button
@
click=
"deleteTask(modalTask.task_id, true)"
class=
"w-10 h-10 flex items-center justify-center bg-white/95 dark:bg-[#1e1e1e]/95 backdrop-blur-[20px] border border-black/10 dark:border-white/10 rounded-full shadow-[0_2px_8px_rgba(0,0,0,0.12)] dark:shadow-[0_2px_8px_rgba(0,0,0,0.4)] text-red-500 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 hover:scale-110 hover:shadow-[0_4px_12px_rgba(239,68,68,0.2)] dark:hover:shadow-[0_4px_12px_rgba(248,113,113,0.3)] active:scale-100 transition-all duration-200"
:title=
"t('delete')"
>
<i
class=
"fas fa-trash text-xs"
></i>
</button>
</div>
<!-- 居中的内容区域 -->
<div
class=
"flex-1 flex items-center justify-center px-8 py-6"
>
<div
class=
"w-full"
>
<div
class=
"flex flex-col items-center gap-3 mb-6"
>
<div
class=
"flex items-center gap-3"
>
<button
@
click=
"copyShareLink(modalTask.task_id, 'task')"
class=
"w-12 h-12 flex items-center justify-center bg-white/95 dark:bg-[#1e1e1e]/95 backdrop-blur-[20px] border border-black/8 dark:border-white/8 text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] rounded-full shadow-[0_4px_16px_rgba(0,0,0,0.12)] dark:shadow-[0_4px_16px_rgba(0,0,0,0.4)] hover:scale-110 active:scale-100 transition-all duration-200"
:title=
"t('share')"
>
<i
class=
"fas fa-share-alt text-base"
></i>
</button>
<button
@
click=
"deleteTask(modalTask.task_id, true)"
class=
"w-12 h-12 flex items-center justify-center bg-white/95 dark:bg-[#1e1e1e]/95 backdrop-blur-[20px] border border-black/8 dark:border-white/8 text-red-500 dark:text-red-400 rounded-full shadow-[0_4px_16px_rgba(0,0,0,0.12)] dark:shadow-[0_4px_16px_rgba(0,0,0,0.4)] hover:scale-110 active:scale-100 transition-all duration-200"
:title=
"t('delete')"
>
<i
class=
"fas fa-trash text-base"
></i>
</button>
</div>
</div>
<!-- 标题 -->
<div
class=
"text-center mb-6"
>
<h1
class=
"text-3xl sm:text-4xl font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] mb-3 tracking-tight"
>
...
...
@@ -352,11 +376,13 @@ watch(() => getAudioMaterials(), (newMaterials) => {
<div
class=
"space-y-2.5"
>
<button
v-if=
"modalTask?.outputs?.output_video"
@
click=
"handleDownloadFile(modalTask.task_id, 'output_video', modalTask.outputs.output_video)"
class=
"w-full rounded-full bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] border-0 px-6 py-3 text-[15px] font-semibold text-white hover:scale-[1.02] hover:shadow-[0_8px_24px_rgba(var(--brand-primary-rgb),0.35)] dark:hover:shadow-[0_8px_24px_rgba(var(--brand-primary-light-rgb),0.4)] active:scale-100 transition-all duration-200 ease-out tracking-tight flex items-center justify-center gap-2"
>
:disabled=
"downloadLoading"
class=
"w-full rounded-full bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] border-0 px-6 py-3 text-[15px] font-semibold text-white transition-all duration-200 ease-out tracking-tight flex items-center justify-center gap-2"
:class=
"downloadLoading ? 'opacity-60 cursor-not-allowed' : 'hover:scale-[1.02] hover:shadow-[0_8px_24px_rgba(var(--brand-primary-rgb),0.35)] dark:hover:shadow-[0_8px_24px_rgba(var(--brand-primary-light-rgb),0.4)] active:scale-100'"
>
<i
class=
"fas fa-download text-sm"
></i>
<span>
{{
t
(
'
downloadVideo
'
)
}}
</span>
</button>
<button
@
click=
"
r
euseTask
(modalTask)
"
<button
@
click=
"
handleR
euseTask"
class=
"w-full rounded-full bg-white dark:bg-[#3a3a3c] border border-black/8 dark:border-white/8 px-6 py-2.5 text-[15px] font-medium text-[#1d1d1f] dark:text-[#f5f5f7] hover:bg-white/80 dark:hover:bg-[#3a3a3c]/80 hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_4px_12px_rgba(0,0,0,0.1)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.3)] active:scale-[0.98] transition-all duration-200 tracking-tight flex items-center justify-center gap-2"
>
<i
class=
"fas fa-magic text-sm"
></i>
<span>
{{
t
(
'
reuseTask
'
)
}}
</span>
...
...
@@ -394,7 +420,9 @@ watch(() => getAudioMaterials(), (newMaterials) => {
</div>
<button
v-if=
"getImageMaterials().length > 0"
@
click=
"handleDownloadFile(modalTask.task_id, 'input_image', modalTask.inputs.input_image)"
class=
"w-8 h-8 flex items-center justify-center bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 border border-[color:var(--brand-primary)]/20 dark:border-[color:var(--brand-primary-light)]/20 text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] rounded-lg transition-all duration-200 hover:scale-110 active:scale-100"
:disabled=
"downloadLoading"
class=
"w-8 h-8 flex items-center justify-center bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 border border-[color:var(--brand-primary)]/20 dark:border-[color:var(--brand-primary-light)]/20 text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] rounded-lg transition-all duration-200"
:class=
"downloadLoading ? 'opacity-60 cursor-not-allowed' : 'hover:scale-110 active:scale-100'"
:title=
"t('download')"
>
<i
class=
"fas fa-download text-xs"
></i>
</button>
...
...
@@ -423,7 +451,9 @@ watch(() => getAudioMaterials(), (newMaterials) => {
</div>
<button
v-if=
"getAudioMaterials().length > 0"
@
click=
"handleDownloadFile(modalTask.task_id, 'input_audio', modalTask.inputs.input_audio)"
class=
"w-8 h-8 flex items-center justify-center bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 border border-[color:var(--brand-primary)]/20 dark:border-[color:var(--brand-primary-light)]/20 text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] rounded-lg transition-all duration-200 hover:scale-110 active:scale-100"
:disabled=
"downloadLoading"
class=
"w-8 h-8 flex items-center justify-center bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 border border-[color:var(--brand-primary)]/20 dark:border-[color:var(--brand-primary-light)]/20 text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] rounded-lg transition-all duration-200"
:class=
"downloadLoading ? 'opacity-60 cursor-not-allowed' : 'hover:scale-110 active:scale-100'"
:title=
"t('download')"
>
<i
class=
"fas fa-download text-xs"
></i>
</button>
...
...
@@ -725,7 +755,7 @@ watch(() => getAudioMaterials(), (newMaterials) => {
<!-- 通用按钮 -->
<button
v-if=
"['SUCCEED', 'FAILED', 'CANCEL','CREATED', 'PENDING', 'RUNNING'].includes(modalTask?.status)"
@
click=
"
r
euseTask
(modalTask)
"
@
click=
"
handleR
euseTask"
class=
"w-full rounded-full bg-white dark:bg-[#3a3a3c] border border-black/8 dark:border-white/8 px-6 py-2.5 text-[15px] font-medium text-[#1d1d1f] dark:text-[#f5f5f7] hover:bg-white/80 dark:hover:bg-[#3a3a3c]/80 hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_4px_12px_rgba(0,0,0,0.1)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.3)] active:scale-[0.98] transition-all duration-200 tracking-tight flex items-center justify-center gap-2"
>
<i
class=
"fas fa-copy text-sm"
></i>
<span>
{{
t
(
'
reuseTask
'
)
}}
</span>
...
...
@@ -764,7 +794,9 @@ watch(() => getAudioMaterials(), (newMaterials) => {
</div>
<button
v-if=
"getImageMaterials().length > 0"
@
click=
"handleDownloadFile(modalTask.task_id, 'input_image', modalTask.inputs.input_image)"
class=
"w-8 h-8 flex items-center justify-center bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 border border-[color:var(--brand-primary)]/20 dark:border-[color:var(--brand-primary-light)]/20 text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] rounded-lg transition-all duration-200 hover:scale-110 active:scale-100"
:disabled=
"downloadLoading"
class=
"w-8 h-8 flex items-center justify-center bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 border border-[color:var(--brand-primary)]/20 dark:border-[color:var(--brand-primary-light)]/20 text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] rounded-lg transition-all duration-200"
:class=
"downloadLoading ? 'opacity-60 cursor-not-allowed' : 'hover:scale-110 active:scale-100'"
:title=
"t('download')"
>
<i
class=
"fas fa-download text-xs"
></i>
</button>
...
...
@@ -793,7 +825,9 @@ watch(() => getAudioMaterials(), (newMaterials) => {
</div>
<button
v-if=
"getAudioMaterials().length > 0"
@
click=
"handleDownloadFile(modalTask.task_id, 'input_audio', modalTask.inputs.input_audio)"
class=
"w-8 h-8 flex items-center justify-center bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 border border-[color:var(--brand-primary)]/20 dark:border-[color:var(--brand-primary-light)]/20 text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] rounded-lg transition-all duration-200 hover:scale-110 active:scale-100"
:disabled=
"downloadLoading"
class=
"w-8 h-8 flex items-center justify-center bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 border border-[color:var(--brand-primary)]/20 dark:border-[color:var(--brand-primary-light)]/20 text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] rounded-lg transition-all duration-200"
:class=
"downloadLoading ? 'opacity-60 cursor-not-allowed' : 'hover:scale-110 active:scale-100'"
:title=
"t('download')"
>
<i
class=
"fas fa-download text-xs"
></i>
</button>
...
...
lightx2v/deploy/server/frontend/src/components/TemplateDetails.vue
View file @
d8558a0c
...
...
@@ -16,7 +16,7 @@ import { showTemplateDetailModal,
}
from
'
../utils/other
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useRoute
,
useRouter
}
from
'
vue-router
'
import
{
ref
,
onMounted
,
onUnmounted
,
nextTick
,
watch
}
from
'
vue
'
import
{
ref
,
onMounted
,
onUnmounted
}
from
'
vue
'
const
{
t
,
locale
}
=
useI18n
()
const
route
=
useRoute
()
const
router
=
useRouter
()
...
...
@@ -24,14 +24,6 @@ const router = useRouter()
// 添加响应式变量
const
showDetails
=
ref
(
false
)
// 音频播放器相关
const
audioElement
=
ref
(
null
)
const
isPlaying
=
ref
(
false
)
const
audioDuration
=
ref
(
0
)
const
currentTime
=
ref
(
0
)
const
isDragging
=
ref
(
false
)
const
currentAudioUrl
=
ref
(
''
)
// 获取图片素材
const
getImageMaterials
=
()
=>
{
if
(
!
selectedTemplate
.
value
?.
inputs
?.
input_image
)
return
[]
...
...
@@ -62,19 +54,22 @@ const closeWithRoute = () => {
// 滚动到生成区域(仅在 generate 页面)
const
scrollToCreationArea
=
()
=>
{
const
mainScrollable
=
document
.
querySelector
(
'
.main-scrollba
r
'
)
;
if
(
mainScrollable
)
{
mainScrollable
.
scrollTo
({
top
:
0
,
b
ehavior
:
'
smooth
'
})
;
const
creationArea
=
document
.
querySelector
(
'
#task-creato
r
'
)
if
(
creationArea
)
{
creationArea
.
scrollIntoView
({
behavior
:
'
smooth
'
,
b
lock
:
'
start
'
})
}
}
// 包装 useTemplate 函数,在 generate 页面时滚动到生成区域
const
handleUseTemplate
=
async
()
=>
{
await
useTemplate
(
selectedTemplate
.
value
)
const
handleUseTemplate
=
()
=>
{
const
template
=
selectedTemplate
.
value
if
(
!
template
)
{
return
}
void
useTemplate
(
template
)
// 如果当前在 generate 页面,滚动到生成区域
if
(
route
.
path
===
'
/generate
'
||
route
.
name
===
'
Generate
'
)
{
// 等待 DOM 更新和展开动画完成
...
...
@@ -98,100 +93,7 @@ onMounted(() => {
onUnmounted
(()
=>
{
document
.
removeEventListener
(
'
keydown
'
,
handleKeydown
)
// 清理音频资源
const
audio
=
getCurrentAudioElement
()
if
(
audio
)
{
audio
.
pause
()
}
})
// 格式化音频时间
const
formatAudioTime
=
(
seconds
)
=>
{
if
(
!
seconds
||
isNaN
(
seconds
))
return
'
0:00
'
const
mins
=
Math
.
floor
(
seconds
/
60
)
const
secs
=
Math
.
floor
(
seconds
%
60
)
return
`
${
mins
}
:
${
secs
.
toString
().
padStart
(
2
,
'
0
'
)}
`
}
// 获取当前音频元素(处理可能是数组的情况)
const
getCurrentAudioElement
=
()
=>
{
return
Array
.
isArray
(
audioElement
.
value
)
?
audioElement
.
value
[
0
]
:
audioElement
.
value
}
// 切换播放/暂停
const
toggleAudioPlayback
=
()
=>
{
const
audio
=
getCurrentAudioElement
()
if
(
!
audio
)
return
if
(
audio
.
paused
)
{
audio
.
play
().
catch
(
error
=>
{
console
.
log
(
'
播放失败:
'
,
error
)
})
}
else
{
audio
.
pause
()
}
}
// 音频加载完成
const
onAudioLoaded
=
()
=>
{
const
audio
=
getCurrentAudioElement
()
if
(
audio
)
{
audioDuration
.
value
=
audio
.
duration
||
0
}
}
// 时间更新
const
onTimeUpdate
=
()
=>
{
const
audio
=
getCurrentAudioElement
()
if
(
audio
&&
!
isDragging
.
value
)
{
currentTime
.
value
=
audio
.
currentTime
||
0
}
}
// 进度条变化处理
const
onProgressChange
=
(
event
)
=>
{
const
audio
=
getCurrentAudioElement
()
if
(
audioDuration
.
value
>
0
&&
audio
&&
event
.
target
)
{
const
newTime
=
parseFloat
(
event
.
target
.
value
)
currentTime
.
value
=
newTime
audio
.
currentTime
=
newTime
}
}
// 进度条拖拽结束处理
const
onProgressEnd
=
(
event
)
=>
{
const
audio
=
getCurrentAudioElement
()
if
(
audio
&&
audioDuration
.
value
>
0
&&
event
.
target
)
{
const
newTime
=
parseFloat
(
event
.
target
.
value
)
audio
.
currentTime
=
newTime
currentTime
.
value
=
newTime
}
isDragging
.
value
=
false
}
// 播放结束
const
onAudioEnded
=
()
=>
{
isPlaying
.
value
=
false
currentTime
.
value
=
0
}
// 监听音频URL变化
watch
(()
=>
getAudioMaterials
(),
(
newMaterials
)
=>
{
if
(
newMaterials
&&
newMaterials
.
length
>
0
)
{
currentAudioUrl
.
value
=
newMaterials
[
0
][
1
]
nextTick
(()
=>
{
const
audio
=
getCurrentAudioElement
()
if
(
audio
)
{
audio
.
load
()
}
})
}
else
{
currentAudioUrl
.
value
=
''
isPlaying
.
value
=
false
currentTime
.
value
=
0
audioDuration
.
value
=
0
}
},
{
immediate
:
true
})
</
script
>
<
template
>
<!-- 模板详情弹窗 - Apple 极简风格 -->
...
...
@@ -370,64 +272,7 @@ watch(() => getAudioMaterials(), (newMaterials) => {
<div
class=
"p-6 min-h-[200px]"
>
<div
v-if=
"getAudioMaterials().length > 0"
class=
"space-y-4"
>
<div
v-for=
"[inputName, url] in getAudioMaterials()"
:key=
"inputName"
>
<!-- 音频播放器卡片 - Apple 风格 -->
<div
class=
"bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-xl transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.2)] w-full p-4"
>
<div
class=
"relative flex items-center mb-3"
>
<!-- 头像容器 -->
<div
class=
"relative mr-3 flex-shrink-0"
>
<!-- 透明白色头像 -->
<div
class=
"w-12 h-12 rounded-full bg-white/40 dark:bg-white/20 border border-white/30 dark:border-white/20 transition-all duration-200"
></div>
<!-- 播放/暂停按钮 -->
<button
@
click=
"toggleAudioPlayback"
class=
"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-8 h-8 bg-[color:var(--brand-primary)]/90 dark:bg-[color:var(--brand-primary-light)]/90 rounded-full flex items-center justify-center text-white cursor-pointer hover:scale-110 transition-all duration-200 z-20 shadow-[0_2px_8px_rgba(var(--brand-primary-rgb),0.3)] dark:shadow-[0_2px_8px_rgba(var(--brand-primary-light-rgb),0.4)]"
>
<i
:class=
"isPlaying ? 'fas fa-pause' : 'fas fa-play'"
class=
"text-xs ml-0.5"
></i>
</button>
</div>
<!-- 音频信息 -->
<div
class=
"flex-1 min-w-0"
>
<div
class=
"text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight truncate"
>
{{
t
(
'
audio
'
)
}}
</div>
</div>
<!-- 音频时长 -->
<div
class=
"text-xs font-medium text-[#86868b] dark:text-[#98989d] tracking-tight flex-shrink-0"
>
{{
formatAudioTime
(
currentTime
)
}}
/
{{
formatAudioTime
(
audioDuration
)
}}
</div>
</div>
<!-- 进度条 -->
<div
class=
"flex items-center gap-2"
v-if=
"audioDuration > 0"
>
<input
type=
"range"
:min=
"0"
:max=
"audioDuration"
:value=
"currentTime"
@
input=
"onProgressChange"
@
change=
"onProgressChange"
@
mousedown=
"isDragging = true"
@
mouseup=
"onProgressEnd"
@
touchstart=
"isDragging = true"
@
touchend=
"onProgressEnd"
class=
"flex-1 h-1 bg-black/6 dark:bg-white/15 rounded-full appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:bg-[color:var(--brand-primary)] dark:[&::-webkit-slider-thumb]:bg-[color:var(--brand-primary-light)] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:cursor-pointer"
/>
</div>
</div>
<!-- 隐藏的音频元素 -->
<audio
ref=
"audioElement"
:src=
"url"
@
loadedmetadata=
"onAudioLoaded"
@
timeupdate=
"onTimeUpdate"
@
ended=
"onAudioEnded"
@
play=
"isPlaying = true"
@
pause=
"isPlaying = false"
class=
"hidden"
></audio>
<audio
:src=
"url"
controls
class=
"w-full rounded-xl"
></audio>
</div>
</div>
<div
v-else
class=
"flex flex-col items-center justify-center h-[150px]"
>
...
...
lightx2v/deploy/server/frontend/src/components/TopBar.vue
View file @
d8558a0c
...
...
@@ -40,8 +40,11 @@ onMounted(() => {
<button
@
click=
"goToHome"
class=
"flex items-center gap-2.5 px-3 py-2 bg-transparent border-0 rounded-[10px] cursor-pointer transition-all duration-200 hover:bg-black/4 dark:hover:bg-white/6 hover:-translate-y-px active:scale-[0.97]"
:title=
"t('goToHome')"
>
<i
class=
"fas fa-film text-xl text-[#1d1d1f] dark:text-[#f5f5f7] transition-colors"
></i>
<span
class=
"text-[17px] font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] tracking-[-0.02em]"
>
LightX2V
</span>
<img
src=
"../../public/logo.svg"
alt=
"LightX2V"
class=
"w-6 h-6 sm:w-6 sm:h-6 md:w-8 md:h-8 lg:w-8 lg:h-8"
loading=
"lazy"
/>
<span
class=
"inline-flex items-baseline text-[20px] font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] tracking-[-0.025em]"
>
<span>
Light
</span>
<span
class=
"text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"
>
X2V
</span>
</span>
</button>
</div>
...
...
lightx2v/deploy/server/frontend/src/components/VoiceTtsHistoryPanel.vue
0 → 100644
View file @
d8558a0c
<
script
setup
>
import
{
ref
,
watch
,
computed
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
const
props
=
defineProps
({
visible
:
{
type
:
Boolean
,
default
:
false
},
history
:
{
type
:
Array
,
default
:
()
=>
[]
},
mode
:
{
type
:
String
,
default
:
'
combined
'
},
getVoiceName
:
{
type
:
Function
,
default
:
()
=>
''
}
})
const
emit
=
defineEmits
([
'
close
'
,
'
apply
'
,
'
delete
'
])
const
{
t
}
=
useI18n
()
const
normalizedMode
=
computed
(()
=>
{
const
modes
=
[
'
combined
'
,
'
text
'
,
'
instruction
'
,
'
voice
'
]
return
modes
.
includes
(
props
.
mode
)
?
props
.
mode
:
'
combined
'
})
const
makeTextEntries
=
()
=>
{
const
seen
=
new
Set
()
const
list
=
[]
for
(
const
entry
of
props
.
history
||
[])
{
const
value
=
(
entry
?.
text
||
''
).
trim
()
if
(
!
value
||
seen
.
has
(
value
))
continue
seen
.
add
(
value
)
list
.
push
({
id
:
value
,
value
,
label
:
value
})
}
return
list
}
const
makeInstructionEntries
=
()
=>
{
const
seen
=
new
Set
()
const
list
=
[]
for
(
const
entry
of
props
.
history
||
[])
{
const
value
=
(
entry
?.
instruction
||
''
).
trim
()
if
(
!
value
||
seen
.
has
(
value
))
continue
seen
.
add
(
value
)
list
.
push
({
id
:
value
,
value
,
label
:
value
})
}
return
list
}
const
makeVoiceEntries
=
()
=>
{
const
seen
=
new
Set
()
const
list
=
[]
for
(
const
entry
of
props
.
history
||
[])
{
const
value
=
(
entry
?.
voiceType
||
''
).
trim
()
const
label
=
props
.
getVoiceName
(
entry
)
||
entry
?.
voiceName
||
value
if
(
!
value
||
seen
.
has
(
value
))
continue
seen
.
add
(
value
)
list
.
push
({
id
:
value
,
value
,
label
})
}
return
list
}
const
filteredHistory
=
computed
(()
=>
{
switch
(
normalizedMode
.
value
)
{
case
'
text
'
:
return
makeTextEntries
()
case
'
instruction
'
:
return
makeInstructionEntries
()
case
'
voice
'
:
return
makeVoiceEntries
()
case
'
combined
'
:
default
:
return
props
.
history
||
[]
}
})
const
totalCount
=
computed
(()
=>
filteredHistory
.
value
.
length
)
const
selectedKey
=
ref
(
null
)
const
panelTitle
=
computed
(()
=>
{
const
map
=
{
combined
:
t
(
'
ttsHistoryTitleCombined
'
),
text
:
t
(
'
ttsHistoryTitleText
'
),
instruction
:
t
(
'
ttsHistoryTitleInstruction
'
),
voice
:
t
(
'
ttsHistoryTitleVoice
'
)
}
return
map
[
normalizedMode
.
value
]
||
t
(
'
ttsHistoryTitle
'
)
})
const
isFemaleVoice
=
(
entry
)
=>
{
const
value
=
(
entry
?.
voiceType
||
entry
?.
voiceName
||
entry
?.
label
||
''
).
toLowerCase
()
return
value
.
includes
(
'
female
'
)
||
value
.
includes
(
'
女
'
)
}
const
getEntryKey
=
(
entry
)
=>
{
if
(
normalizedMode
.
value
===
'
combined
'
)
{
return
entry
?.
id
??
null
}
return
entry
?.
value
??
null
}
const
ensureSelection
=
()
=>
{
if
(
!
props
.
visible
)
{
selectedKey
.
value
=
null
return
}
const
list
=
filteredHistory
.
value
if
(
!
list
.
length
)
{
selectedKey
.
value
=
null
return
}
const
currentKey
=
selectedKey
.
value
if
(
list
.
some
((
entry
)
=>
getEntryKey
(
entry
)
===
currentKey
))
{
return
}
selectedKey
.
value
=
getEntryKey
(
list
[
0
])
}
watch
(()
=>
props
.
visible
,
ensureSelection
)
watch
(
filteredHistory
,
ensureSelection
)
watch
(
normalizedMode
,
ensureSelection
)
const
isCombinedMode
=
computed
(()
=>
normalizedMode
.
value
===
'
combined
'
)
const
isApplyDisabled
=
computed
(()
=>
!
props
.
visible
||
!
selectedKey
.
value
)
const
handleOverlayClick
=
()
=>
{
emit
(
'
close
'
)
}
const
handlePanelClick
=
(
event
)
=>
{
event
.
stopPropagation
()
}
const
handleEntryClick
=
(
entry
)
=>
{
selectedKey
.
value
=
getEntryKey
(
entry
)
}
const
handleApplyClick
=
()
=>
{
if
(
isApplyDisabled
.
value
)
return
if
(
normalizedMode
.
value
===
'
combined
'
)
{
const
entry
=
filteredHistory
.
value
.
find
((
item
)
=>
getEntryKey
(
item
)
===
selectedKey
.
value
)
if
(
entry
)
{
emit
(
'
apply
'
,
entry
)
}
}
else
{
emit
(
'
apply
'
,
selectedKey
.
value
)
}
}
const
handleDeleteClick
=
(
event
,
entry
)
=>
{
if
(
!
isCombinedMode
.
value
)
return
event
.
stopPropagation
()
emit
(
'
delete
'
,
entry
)
}
const
getEntryVoiceLabel
=
(
entry
)
=>
{
return
props
.
getVoiceName
(
entry
)
||
entry
.
voiceType
||
t
(
'
ttsHistoryVoiceEmpty
'
)
}
</
script
>
<
template
>
<div
v-if=
"visible"
class=
"fixed inset-0 bg-black/50 dark:bg-black/60 backdrop-blur-sm z-[100] flex items-center justify-center p-4"
@
click=
"handleOverlayClick"
>
<div
class=
"bg-white/95 dark:bg-[#1e1e1e]/95 backdrop-blur-[40px] backdrop-saturate-[180%] border border-black/10 dark:border-white/10 rounded-3xl w-full max-w-2xl max-h-[85vh] overflow-hidden shadow-[0_20px_60px_rgba(0,0,0,0.2)] dark:shadow-[0_20px_60px_rgba(0,0,0,0.6)] flex flex-col"
@
click.stop=
"handlePanelClick"
>
<div
class=
"flex items-center justify-between px-6 py-4 border-b border-black/8 dark:border-white/8 bg-white/50 dark:bg-[#1e1e1e]/50 backdrop-blur-[20px]"
>
<div
class=
"flex items-center gap-3"
>
<h3
class=
"text-lg font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight flex items-center gap-2"
>
<i
class=
"fas fa-history text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"
></i>
<span>
{{
panelTitle
}}
</span>
</h3>
<span
v-if=
"totalCount > 0"
class=
"px-2 py-0.5 rounded-full text-xs font-medium bg-black/5 dark:bg-white/10 text-[#86868b] dark:text-[#98989d]"
>
{{
totalCount
}}
</span>
</div>
<div
class=
"flex items-center gap-2"
>
<button
@
click.stop=
"handleApplyClick"
:disabled=
"isApplyDisabled"
class=
"w-9 h-9 flex items-center justify-center rounded-full transition-all duration-200 bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] text-white disabled:opacity-50 disabled:cursor-not-allowed hover:scale-105 active:scale-100"
:title=
"t('ttsHistoryApplySelected')"
>
<i
class=
"fas fa-check text-sm"
></i>
</button>
<button
@
click.stop=
"emit('close')"
class=
"w-9 h-9 flex items-center justify-center bg-white/80 dark:bg-[#2c2c2e]/80 border border-black/8 dark:border-white/8 text-[#86868b] dark:text-[#98989d] hover:text-[#1d1d1f] dark:hover:text-[#f5f5f7] hover:bg-white dark:hover:bg-[#3a3a3c] rounded-full transition-all duration-200 hover:scale-110 active:scale-100"
>
<i
class=
"fas fa-times text-sm"
></i>
</button>
</div>
</div>
<div
class=
"flex-1 min-h-[50vh] overflow-y-auto p-6 main-scrollbar"
>
<div
v-if=
"!filteredHistory.length"
class=
"flex flex-col items-center justify-center py-12 text-center"
>
<div
class=
"w-16 h-16 bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 rounded-full flex items-center justify-center mb-4"
>
<i
class=
"fas fa-book text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] text-2xl"
></i>
</div>
<p
class=
"text-[#1d1d1f] dark:text-[#f5f5f7] text-lg font-medium mb-2 tracking-tight"
>
{{
t
(
'
ttsHistoryEmpty
'
)
}}
</p>
<p
class=
"text-[#86868b] dark:text-[#98989d] text-sm tracking-tight"
>
{{
t
(
'
ttsHistoryEmptyHint
'
)
}}
</p>
</div>
<template
v-else
>
<div
v-if=
"normalizedMode === 'voice'"
class=
"grid grid-cols-1 sm:grid-cols-2 gap-3"
>
<div
v-for=
"entry in filteredHistory"
:key=
"getEntryKey(entry)"
@
click=
"handleEntryClick(entry)"
class=
"p-4 border border-black/8 dark:border-white/8 rounded-2xl bg-white/80 dark:bg-[#2c2c2e]/80 hover:bg-white dark:hover:bg-[#3a3a3c] transition-all duration-200 cursor-pointer flex items-center gap-3"
:class=
"
{
'border-[color:var(--brand-primary)] dark:border-[color:var(--brand-primary-light)] shadow-[0_0_0_2px_rgba(var(--brand-primary-rgb),0.2)] dark:shadow-[0_0_0_2px_rgba(var(--brand-primary-light-rgb),0.25)] ring-2 ring-[color:var(--brand-primary)]/20 dark:ring-[color:var(--brand-primary-light)]/25': getEntryKey(entry) === selectedKey
}"
>
<div
class=
"relative flex-shrink-0"
>
<img
v-if=
"isFemaleVoice(entry)"
src=
"../../public/female.svg"
alt=
"Female Avatar"
class=
"w-12 h-12 rounded-full object-cover bg-white transition-all duration-200"
/>
<!-- Male Avatar -->
<img
v-else
src=
"../../public/male.svg"
alt=
"Male Avatar"
class=
"w-12 h-12 rounded-full object-cover bg-white transition-all duration-200"
/>
</div>
<div
class=
"flex-1 min-w-0 space-y-1"
>
<div
class=
"text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight truncate"
>
{{
entry
.
label
}}
</div>
<div
class=
"text-xs text-[#86868b] dark:text-[#98989d] tracking-tight truncate"
>
{{
entry
.
voiceType
}}
</div>
</div>
</div>
</div>
<div
v-else
class=
"space-y-3"
>
<div
v-for=
"entry in filteredHistory"
:key=
"getEntryKey(entry)"
@
click=
"handleEntryClick(entry)"
class=
"p-4 border border-black/8 dark:border-white/8 rounded-2xl bg-white/80 dark:bg-[#2c2c2e]/80 hover:bg-white dark:hover:bg-[#3a3a3c] transition-all duration-200 cursor-pointer"
:class=
"
{
'border-[color:var(--brand-primary)] dark:border-[color:var(--brand-primary-light)] shadow-[0_0_0_2px_rgba(var(--brand-primary-rgb),0.2)] dark:shadow-[0_0_0_2px_rgba(var(--brand-primary-light-rgb),0.25)]': getEntryKey(entry) === selectedKey
}"
>
<div
class=
"flex flex-col gap-3"
>
<div
class=
"flex items-start justify-between gap-3"
>
<div
class=
"flex-1 min-w-0 space-y-2"
>
<template
v-if=
"normalizedMode === 'combined'"
>
<div
class=
"text-sm font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight break-words whitespace-pre-line"
>
<span
class=
"text-xs uppercase text-[#86868b] dark:text-[#98989d] mr-2"
>
{{
t
(
'
ttsHistoryTextLabel
'
)
}}
:
</span>
<span>
{{
entry
.
text
||
t
(
'
ttsHistoryTextEmpty
'
)
}}
</span>
</div>
<div
class=
"text-sm text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight break-words whitespace-pre-line"
>
<span
class=
"text-xs uppercase text-[#86868b] dark:text-[#98989d] mr-2"
>
{{
t
(
'
ttsHistoryInstructionLabel
'
)
}}
:
</span>
<span>
{{
entry
.
instruction
||
t
(
'
ttsHistoryInstructionEmpty
'
)
}}
</span>
</div>
<div
class=
"text-sm text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight break-words"
>
<span
class=
"text-xs uppercase text-[#86868b] dark:text-[#98989d] mr-2"
>
{{
t
(
'
ttsHistoryVoiceLabel
'
)
}}
:
</span>
<span>
{{
getEntryVoiceLabel
(
entry
)
}}
</span>
</div>
</
template
>
<
template
v-else
>
<div
class=
"text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight break-words whitespace-pre-line"
>
<span>
{{
entry
.
label
}}
</span>
</div>
</
template
>
</div>
<button
v-if=
"isCombinedMode"
@
click=
"handleDeleteClick($event, entry)"
class=
"w-9 h-9 flex items-center justify-center bg-white/80 dark:bg-[#2c2c2e]/80 border border-black/8 dark:border-white/8 text-red-500 dark:text-red-400 hover:text-red-600 dark:hover:text-red-300 hover:bg-white dark:hover:bg-[#3a3a3c] rounded-full transition-all duration-200"
:title=
"t('ttsHistoryDeleteEntry')"
>
<i
class=
"fas fa-trash text-sm"
></i>
</button>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
</template>
lightx2v/deploy/server/frontend/src/components/Voice_tts.vue
View file @
d8558a0c
...
...
@@ -4,10 +4,19 @@
<div
class=
"relative w-full h-full max-w-6xl max-h-[100vh] bg-white/95 dark:bg-[#1e1e1e]/95 backdrop-blur-[40px] backdrop-saturate-[180%] border border-black/10 dark:border-white/10 rounded-3xl shadow-[0_20px_60px_rgba(0,0,0,0.2)] dark:shadow-[0_20px_60px_rgba(0,0,0,0.6)] overflow-hidden flex flex-col"
>
<!-- 模态框头部 - Apple 风格 -->
<div
class=
"flex items-center justify-between px-6 py-4 border-b border-black/8 dark:border-white/8 bg-white/50 dark:bg-[#1e1e1e]/50 backdrop-blur-[20px] flex-shrink-0"
>
<h3
class=
"text-xl font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] flex items-center gap-3 tracking-tight"
>
<i
class=
"fas fa-volume-up text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"
></i>
<span>
{{
t
(
'
voiceSynthesis
'
)
}}
</span>
</h3>
<div
class=
"flex items-center gap-3"
>
<h3
class=
"text-xl font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] flex items-center gap-3 tracking-tight"
>
<i
class=
"fas fa-volume-up text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"
></i>
<span>
{{
t
(
'
voiceSynthesis
'
)
}}
</span>
</h3>
<button
@
click=
"openHistoryPanel"
class=
"w-9 h-9 flex items-center justify-center bg-white/80 dark:bg-[#2c2c2e]/80 border border-black/8 dark:border-white/8 text-[#86868b] dark:text-[#98989d] hover:text-[#1d1d1f] dark:hover:text-[#f5f5f7] hover:bg-white dark:hover:bg-[#3a3a3c] rounded-full transition-all duration-200 hover:scale-110 active:scale-100"
:title=
"t('ttsHistoryTitle')"
>
<i
class=
"fas fa-history text-sm"
></i>
</button>
</div>
<div
class=
"flex items-center gap-2"
>
<!-- 应用按钮 - Apple 风格 -->
<button
...
...
@@ -180,10 +189,20 @@
<div
class=
"max-w-5xl mx-auto space-y-6"
>
<!-- 文本输入区域 - Apple 风格 -->
<div>
<label
class=
"block text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] mb-3 tracking-tight"
>
<i
class=
"fas fa-keyboard mr-2 text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"
></i>
{{
t
(
'
enterTextToConvert
'
)
}}
</label>
<div
class=
"flex items-center justify-between mb-3"
>
<div
class=
"flex items-center gap-2"
>
<i
class=
"fas fa-keyboard text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"
></i>
<span
class=
"text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight"
>
{{
t
(
'
enterTextToConvert
'
)
}}
</span>
<button
@
click=
"openTextHistoryPanel"
class=
"w-8 h-8 flex items-center justify-center rounded-full bg-white/80 dark:bg-[#2c2c2e]/80 border border-black/8 dark:border-white/8 text-[#86868b] dark:text-[#98989d] hover:text-[#1d1d1f] dark:hover:text-[#f5f5f7] hover:bg-white dark:hover:bg-[#3a3a3c] transition-all duration-200"
:title=
"t('ttsHistoryTabText')"
>
<i
class=
"fas fa-history text-xs"
></i>
</button>
</div>
</div>
<textarea
v-model=
"inputText"
:placeholder=
"t('ttsPlaceholder')"
...
...
@@ -194,11 +213,21 @@
<!-- 语音指令区域 - Apple 风格 -->
<div>
<label
class=
"block text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] mb-3 tracking-tight"
>
<i
class=
"fas fa-magic mr-2 text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"
></i>
{{
t
(
'
voiceInstruction
'
)
}}
<span
class=
"text-xs text-[#86868b] dark:text-[#98989d] ml-2"
>
{{
t
(
'
voiceInstructionHint
'
)
}}
</span>
</label>
<div
class=
"flex items-center justify-between mb-3"
>
<div
class=
"flex items-center gap-2"
>
<i
class=
"fas fa-magic text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"
></i>
<span
class=
"text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight"
>
{{
t
(
'
voiceInstruction
'
)
}}
</span>
<span
class=
"text-xs text-[#86868b] dark:text-[#98989d]"
>
{{
t
(
'
voiceInstructionHint
'
)
}}
</span>
<button
@
click=
"openInstructionHistoryPanel"
class=
"w-8 h-8 flex items-center justify-center rounded-full bg-white/80 dark:bg-[#2c2c2e]/80 border border-black/8 dark:border-white/8 text-[#86868b] dark:text-[#98989d] hover:text-[#1d1d1f] dark:hover:text-[#f5f5f7] hover:bg-white dark:hover:bg-[#3a3a3c] transition-all duration-200"
:title=
"t('ttsHistoryTabInstruction')"
>
<i
class=
"fas fa-history text-xs"
></i>
</button>
</div>
</div>
<textarea
v-model=
"contextText"
:placeholder=
"t('voiceInstructionPlaceholder')"
...
...
@@ -210,29 +239,39 @@
<!-- 音色选择区域 - Apple 风格 -->
<div>
<div
class=
"flex items-center justify-between mb-4"
>
<label
class=
"text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight"
>
<i
class=
"fas fa-microphone-alt mr-2 text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"
></i>
{{
t
(
'
selectVoice
'
)
}}
</label>
<div
class=
"flex items-center gap-3"
>
<!-- 搜索框 - Apple 风格 -->
<div
class=
"relative w-52"
>
<i
class=
"fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-[#86868b] dark:text-[#98989d] text-xs pointer-events-none z-10"
></i>
<input
v-model=
"searchQuery"
:placeholder=
"t('searchVoice')"
class=
"w-full bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-lg py-2 pl-9 pr-3 text-sm text-[#1d1d1f] dark:text-[#f5f5f7] placeholder-[#86868b] dark:placeholder-[#98989d] tracking-tight hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 focus:outline-none focus:border-[color:var(--brand-primary)]/50 dark:focus:border-[color:var(--brand-primary-light)]/60 transition-all duration-200"
type=
"text"
/>
</div>
<div
class=
"flex items-center gap-2"
>
<i
class=
"fas fa-microphone-alt text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"
></i>
<span
class=
"text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight"
>
{{
t
(
'
selectVoice
'
)
}}
</span>
<button
@
click=
"openVoiceHistoryPanel"
class=
"w-8 h-8 flex items-center justify-center rounded-full bg-white/80 dark:bg-[#2c2c2e]/80 border border-black/8 dark:border-white/8 text-[#86868b] dark:text-[#98989d] hover:text-[#1d1d1f] dark:hover:text-[#f5f5f7] hover:bg-white dark:hover:bg-[#3a3a3c] transition-all duration-200"
:title=
"t('ttsHistoryTabVoice')"
>
<i
class=
"fas fa-history text-xs"
></i>
</button>
</div>
<!-- 筛选按钮 - Apple 风格 -->
<button
@
click=
"toggleFilterPanel"
class=
"flex items-center gap-2 px-4 py-2 bg-white/80 dark:bg-[#2c2c2e]/80 border border-black/8 dark:border-white/8 text-[#86868b] dark:text-[#98989d] hover:text-[#1d1d1f] dark:hover:text-[#f5f5f7] hover:bg-white dark:hover:bg-[#3a3a3c] rounded-lg transition-all duration-200 text-sm font-medium tracking-tight"
>
<i
class=
"fas fa-filter text-xs"
></i>
<span>
{{
t
(
'
filter
'
)
}}
</span>
</button>
<div
class=
"flex items-center gap-3"
>
<!-- 搜索框 - Apple 风格 -->
<div
class=
"relative w-52"
>
<i
class=
"fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-[#86868b] dark:text-[#98989d] text-xs pointer-events-none z-10"
></i>
<input
v-model=
"searchQuery"
:placeholder=
"t('searchVoice')"
class=
"w-full bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-lg py-2 pl-9 pr-3 text-sm text-[#1d1d1f] dark:text-[#f5f5f7] placeholder-[#86868b] dark:placeholder-[#98989d] tracking-tight hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 focus:outline-none focus:border-[color:var(--brand-primary)]/50 dark:focus:border-[color:var(--brand-primary-light)]/60 transition-all duration-200"
type=
"text"
/>
</div>
<!-- 筛选按钮 - Apple 风格 -->
<button
@
click=
"toggleFilterPanel"
class=
"flex items-center gap-2 px-4 py-2 bg-white/80 dark:bg-[#2c2c2e]/80 border border-black/8 dark:border-white/8 text-[#86868b] dark:text-[#98989d] hover:text-[#1d1d1f] dark:hover:text-[#f5f5f7] hover:bg-white dark:hover:bg-[#3a3a3c] rounded-lg transition-all duration-200 text-sm font-medium tracking-tight"
>
<i
class=
"fas fa-filter text-xs"
></i>
<span>
{{
t
(
'
filter
'
)
}}
</span>
</button>
</div>
</div>
</div>
<!-- 音色列表容器 - Apple 风格 -->
<div
class=
"bg-white/50 dark:bg-[#2c2c2e]/50 backdrop-blur-[10px] border border-black/6 dark:border-white/6 rounded-2xl p-5 max-h-[500px] overflow-y-auto main-scrollbar pr-3"
ref=
"voiceListContainer"
>
...
...
@@ -309,12 +348,47 @@
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<VoiceTtsHistoryPanel
:visible=
"showHistoryPanel"
:history=
"ttsHistory"
mode=
"combined"
:get-voice-name=
"getHistoryVoiceName"
@
close=
"closeHistoryPanel"
@
apply=
"applyCombinedHistoryEntry"
@
delete=
"handleDeleteHistoryEntry"
/>
<VoiceTtsHistoryPanel
:visible=
"showTextHistoryPanel"
:history=
"ttsHistory"
mode=
"text"
@
close=
"closeTextHistoryPanel"
@
apply=
"applyTextHistoryEntry"
/>
<VoiceTtsHistoryPanel
:visible=
"showInstructionHistoryPanel"
:history=
"ttsHistory"
mode=
"instruction"
@
close=
"closeInstructionHistoryPanel"
@
apply=
"applyInstructionHistoryEntry"
/>
<VoiceTtsHistoryPanel
:visible=
"showVoiceHistoryPanel"
:history=
"ttsHistory"
mode=
"voice"
:get-voice-name=
"getHistoryVoiceName"
@
close=
"closeVoiceHistoryPanel"
@
apply=
"applyVoiceHistoryEntry"
/>
<!-- 筛选面板遮罩 - Apple 风格 -->
<div
v-if=
"showFilterPanel"
class=
"fixed inset-0 bg-black/50 dark:bg-black/60 backdrop-blur-sm z-[100] flex items-center justify-center p-4"
@
click=
"closeFilterPanel"
>
<div
class=
"bg-white/95 dark:bg-[#1e1e1e]/95 backdrop-blur-[40px] backdrop-saturate-[180%] border border-black/10 dark:border-white/10 rounded-3xl w-full max-w-2xl max-h-[85vh] overflow-hidden shadow-[0_20px_60px_rgba(0,0,0,0.2)] dark:shadow-[0_20px_60px_rgba(0,0,0,0.6)] flex flex-col"
@
click.stop
>
...
...
@@ -426,11 +500,14 @@
import
{
ref
,
computed
,
onMounted
,
onUnmounted
,
watch
,
nextTick
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
DropdownMenu
from
'
./DropdownMenu.vue
'
import
VoiceTtsHistoryPanel
from
'
./VoiceTtsHistoryPanel.vue
'
import
{
ttsHistory
,
loadTtsHistory
,
addTtsHistoryEntry
,
removeTtsHistoryEntry
}
from
'
../utils/other
'
export
default
{
name
:
'
VoiceTTS
'
,
components
:
{
DropdownMenu
DropdownMenu
,
VoiceTtsHistoryPanel
},
emits
:
[
'
tts-complete
'
,
'
close-modal
'
],
setup
(
props
,
{
emit
})
{
...
...
@@ -459,6 +536,10 @@ export default {
const
voiceListContainer
=
ref
(
null
)
const
showControls
=
ref
(
false
)
const
showFilterPanel
=
ref
(
false
)
const
showHistoryPanel
=
ref
(
false
)
const
showTextHistoryPanel
=
ref
(
false
)
const
showInstructionHistoryPanel
=
ref
(
false
)
const
showVoiceHistoryPanel
=
ref
(
false
)
// Category filtering - 存储原始中文值
const
selectedCategory
=
ref
(
'
全部场景
'
)
...
...
@@ -508,9 +589,52 @@ export default {
return
map
[
gender
]
||
gender
}
const
openHistoryPanel
=
()
=>
{
loadTtsHistory
()
showHistoryPanel
.
value
=
true
}
const
closeHistoryPanel
=
()
=>
{
showHistoryPanel
.
value
=
false
}
const
openTextHistoryPanel
=
()
=>
{
loadTtsHistory
()
showTextHistoryPanel
.
value
=
true
}
const
openInstructionHistoryPanel
=
()
=>
{
loadTtsHistory
()
showInstructionHistoryPanel
.
value
=
true
}
const
openVoiceHistoryPanel
=
()
=>
{
loadTtsHistory
()
showVoiceHistoryPanel
.
value
=
true
}
const
closeTextHistoryPanel
=
()
=>
{
showTextHistoryPanel
.
value
=
false
}
const
closeInstructionHistoryPanel
=
()
=>
{
showInstructionHistoryPanel
.
value
=
false
}
const
closeVoiceHistoryPanel
=
()
=>
{
showVoiceHistoryPanel
.
value
=
false
}
const
handleDeleteHistoryEntry
=
(
entry
)
=>
{
if
(
!
entry
?.
id
)
return
removeTtsHistoryEntry
(
entry
.
id
)
loadTtsHistory
()
}
// Load voices data
onMounted
(
async
()
=>
{
loadTtsHistory
()
try
{
const
response
=
await
fetch
(
'
/api/v1/voices/list
'
)
const
data
=
await
response
.
json
()
...
...
@@ -784,6 +908,14 @@ export default {
audioUrl
.
value
=
URL
.
createObjectURL
(
blob
)
// 标记需要自动播放
shouldAutoPlay
.
value
=
true
addTtsHistoryEntry
(
inputText
.
value
,
contextText
.
value
,
{
voiceType
:
selectedVoice
.
value
,
voiceName
:
selectedVoiceData
.
value
?.
name
||
''
}
)
}
else
{
throw
new
Error
(
'
TTS generation failed
'
)
}
...
...
@@ -795,6 +927,65 @@ export default {
}
}
const
applyCombinedHistoryEntry
=
async
(
entry
)
=>
{
if
(
!
entry
)
return
inputText
.
value
=
entry
.
text
||
''
contextText
.
value
=
entry
.
instruction
||
''
if
(
entry
.
voiceType
)
{
const
voice
=
voices
.
value
.
find
(
v
=>
v
.
voice_type
===
entry
.
voiceType
)
if
(
voice
)
{
await
onVoiceSelect
(
voice
)
return
}
selectedVoice
.
value
=
entry
.
voiceType
selectedVoiceResourceId
.
value
=
''
}
nextTick
(()
=>
{
generateTTS
()
})
showHistoryPanel
.
value
=
false
}
const
applyTextHistoryEntry
=
(
value
)
=>
{
if
(
!
value
)
return
inputText
.
value
=
value
showTextHistoryPanel
.
value
=
false
}
const
applyInstructionHistoryEntry
=
(
value
)
=>
{
if
(
!
value
)
return
contextText
.
value
=
value
showInstructionHistoryPanel
.
value
=
false
}
const
applyVoiceHistoryEntry
=
async
(
voiceType
)
=>
{
if
(
!
voiceType
)
return
const
voice
=
voices
.
value
.
find
(
v
=>
v
.
voice_type
===
voiceType
)
if
(
voice
)
{
await
onVoiceSelect
(
voice
)
}
else
{
selectedVoice
.
value
=
voiceType
selectedVoiceResourceId
.
value
=
''
nextTick
(()
=>
{
generateTTS
()
})
}
showVoiceHistoryPanel
.
value
=
false
}
const
getHistoryVoiceName
=
(
entry
)
=>
{
if
(
!
entry
)
return
''
if
(
entry
.
voiceName
)
return
entry
.
voiceName
if
(
entry
.
voiceType
)
{
const
voice
=
voices
.
value
.
find
(
v
=>
v
.
voice_type
===
entry
.
voiceType
)
return
voice
?.
name
||
''
}
return
''
}
// 格式化音频时间
const
formatAudioTime
=
(
seconds
)
=>
{
if
(
!
seconds
||
isNaN
(
seconds
))
return
'
0:00
'
...
...
@@ -1051,7 +1242,26 @@ export default {
translateCategory
,
translateVersion
,
translateLanguage
,
translateGender
translateGender
,
ttsHistory
,
showHistoryPanel
,
openHistoryPanel
,
closeHistoryPanel
,
applyCombinedHistoryEntry
,
applyTextHistoryEntry
,
applyInstructionHistoryEntry
,
applyVoiceHistoryEntry
,
getHistoryVoiceName
,
handleDeleteHistoryEntry
,
showTextHistoryPanel
,
showInstructionHistoryPanel
,
showVoiceHistoryPanel
,
openTextHistoryPanel
,
openInstructionHistoryPanel
,
openVoiceHistoryPanel
,
closeTextHistoryPanel
,
closeInstructionHistoryPanel
,
closeVoiceHistoryPanel
}
}
}
...
...
lightx2v/deploy/server/frontend/src/locales/en.json
View file @
d8558a0c
...
...
@@ -132,10 +132,22 @@
"deleteTask"
:
"Delete"
,
"cancelTask"
:
"Cancel"
,
"download"
:
"Download"
,
"downloadPreparing"
:
"Preparing download..."
,
"downloadFetching"
:
"Fetching file..."
,
"downloadSaving"
:
"Saving file..."
,
"mobileSaveToAlbumTip"
:
"Long press the video in the new tab to save it to your gallery."
,
"mobileSavePreviewTitle"
:
"Preview & Save"
,
"mobileSaveInstruction"
:
"Tap the full-screen button or long press the video to save it to your photo library."
,
"mute"
:
"Mute"
,
"unmute"
:
"Unmute"
,
"unsupportedAudioOrVideo"
:
"Please select an audio or video file."
,
"unsupportedVideoFormat"
:
"Only MP4/M4V/MPEG video files are supported for audio extraction."
,
"downloadInProgressNotice"
:
"A download is already in progress. Please wait."
,
"downloadCancelledAlert"
:
"Download cancelled"
,
"createVideo"
:
"Create Video"
,
"selectTemplate"
:
"Select Template"
,
"uploadImage"
:
"Upload Image"
,
"uploadAudio"
:
"Upload Audio"
,
"uploadAudio"
:
"Upload Audio
or Video
"
,
"recordAudio"
:
"Record Audio"
,
"recording"
:
"Recording..."
,
"takePhoto"
:
"Take Photo"
,
...
...
@@ -175,7 +187,7 @@
"textToVideo"
:
"Text to Video"
,
"imageToVideo"
:
"Image to Video"
,
"speechToVideo"
:
"Speech to Video"
,
"prompt"
:
"Prompt"
,
"prompt"
:
"Prompt
(Optional)
"
,
"negativePrompt"
:
"Negative Prompt"
,
"promptTemplates"
:
"Prompt Templates"
,
"promptHistory"
:
"Prompt History"
,
...
...
@@ -192,11 +204,14 @@
"s2vHint3"
:
"Let your role become alive."
,
"s2vHint4"
:
"Create your own digital person."
,
"uploadImageFile"
:
"Upload Image File"
,
"uploadAudioFile"
:
"Upload Audio File"
,
"uploadAudioFile"
:
"Upload Audio
or Video
File"
,
"dragDropHere"
:
"Drag and drop files here or click to upload"
,
"supportedImageFormats"
:
"Supported jpg, png, webp image formats (< 10MB)"
,
"supportedAudioFormats"
:
"Supported mp3, m4a, wav audio formats (< 120s)"
,
"supportedAudioFormatsShort"
:
"Supported mp3, m4a, wav formats"
,
"supportedAudioFormats"
:
"Supports audio or video formats (< 120s)."
,
"supportedAudioFormatsShort"
:
"Supports audio or video formats (< 120s)."
,
"prefillLoadingDefault"
:
"Preparing materials..."
,
"prefillLoadingTemplate"
:
"Loading template assets..."
,
"prefillLoadingTask"
:
"Loading task materials..."
,
"clearCharacterImageTip"
:
"Upload a clear character image"
,
"maxFileSize"
:
"Max file size"
,
"taskDetail"
:
"Task Details"
,
...
...
@@ -244,7 +259,7 @@
"taskMaterialReuseSuccessAlert"
:
"Task material reuse successfully"
,
"loadTaskDataFailedAlert"
:
"Load task data failed"
,
"fileUnavailableAlert"
:
"File unavailable"
,
"downloadFailedAlert"
:
"Download failed"
,
"downloadFailedAlert"
:
"Download failed
. Please try again.
"
,
"taskSubmitSuccessAlert"
:
"Task submit successfully"
,
"taskSubmitFailedAlert"
:
"Task submit failed"
,
"submitTaskFailedAlert"
:
"Submit task failed"
,
...
...
@@ -309,7 +324,7 @@
"oneClickReplication"
:
"One-click replication"
,
"customizableContent"
:
"Customizable content"
,
"poweredByLightX2V"
:
"Speed-generated video - LightX2V"
,
"latestAIModel"
:
"Latest AI model, rapid video generation"
,
"latestAIModel"
:
"Latest AI
digital human
model, rapid video generation"
,
"customizableCharacter"
:
"Freely customizable characters and audio"
,
"userGeneratedVideo"
:
" generated video"
,
"noImage"
:
"No Images"
,
...
...
@@ -360,9 +375,81 @@
"multilingual"
:
"Multilingual"
,
"multiEmotion"
:
"Multi-emotion"
,
"videoDubbing"
:
"Video Dubbing"
,
"ttsHistoryTitle"
:
"History"
,
"ttsHistoryHint"
:
"We automatically keep the last 20 voice texts and instructions you used."
,
"ttsHistoryEmpty"
:
"No saved entries yet"
,
"ttsHistoryEmptyHint"
:
"Generate voice once to create your first history entry."
,
"ttsHistoryTextLabel"
:
"Voice Text"
,
"ttsHistoryInstructionLabel"
:
"Voice Instruction"
,
"ttsHistoryTextEmpty"
:
"Empty text"
,
"ttsHistoryInstructionEmpty"
:
"Empty instruction"
,
"ttsHistoryVoiceLabel"
:
"Voice"
,
"ttsHistoryVoiceEmpty"
:
"Not set"
,
"ttsHistoryApply"
:
"Use This"
,
"ttsHistoryApplySelected"
:
"Apply"
,
"ttsHistoryDeleteEntry"
:
"Remove"
,
"ttsHistoryTabCombined"
:
"All"
,
"ttsHistoryTabText"
:
"Text"
,
"ttsHistoryTabInstruction"
:
"Instruction"
,
"ttsHistoryTabVoice"
:
"Voice"
,
"ttsHistoryTitleCombined"
:
"All History"
,
"ttsHistoryTitleText"
:
"Text History"
,
"ttsHistoryTitleInstruction"
:
"Instruction History"
,
"ttsHistoryTitleVoice"
:
"Voice History"
,
"ttsHistoryClear"
:
"Clear History"
,
"allVersions"
:
"All Versions"
,
"allLanguages"
:
"All Languages"
,
"allGenders"
:
"All Genders"
,
"female"
:
"Female"
,
"male"
:
"Male"
,
"taskCountdown"
:
"Task countdown"
,
"footer"
:
{
"tagline"
:
"AI digital human video generation powered by the Light AI toolchain"
,
"links"
:
{
"home"
:
"Light AI Homepage"
,
"github"
:
"GitHub"
,
"xiaohongshu"
:
"Xiaohongshu"
},
"alt"
:
{
"github"
:
"GitHub logo"
,
"xiaohongshu"
:
"Xiaohongshu logo"
},
"copyright"
:
"© {year} Light AI. All rights reserved."
},
"tts"
:
{
"title"
:
"AI Voice Synthesis"
,
"subtitle"
:
"Synthesize your voice with AI"
,
"inputText"
:
"Enter text to synthesize"
,
"voiceSelection"
:
"Select voice"
,
"voiceSettings"
:
"Voice settings"
,
"speechRate"
:
"Speech rate"
,
"volume"
:
"Volume"
,
"pitch"
:
"Pitch"
,
"emotionIntensity"
:
"Emotion intensity"
,
"emotionType"
:
"Emotion type"
,
"neutral"
:
"Neutral"
,
"scene"
:
"Scene"
,
"version"
:
"Version"
,
"language"
:
"Language"
,
"gender"
:
"Gender"
,
"reset"
:
"Reset"
,
"done"
:
"Done"
,
"ttsGenerationFailed"
:
"TTS generation failed, please retry"
,
"applyAudioFailed"
:
"Apply audio failed, please retry"
,
"allScenes"
:
"All Scenes"
,
"generalScene"
:
"General"
,
"customerServiceScene"
:
"Customer Service"
,
"educationScene"
:
"Education"
,
"funAccent"
:
"Fun Accent"
,
"rolePlaying"
:
"Role Playing"
,
"audiobook"
:
"Audiobook"
,
"multilingual"
:
"Multilingual"
,
"multiEmotion"
:
"Multi-emotion"
,
"videoDubbing"
:
"Video Dubbing"
,
"allVersions"
:
"All Versions"
,
"allLanguages"
:
"All Languages"
,
"allGenders"
:
"All Genders"
,
"female"
:
"Female"
,
"male"
:
"Male"
}
}
lightx2v/deploy/server/frontend/src/locales/zh.json
View file @
d8558a0c
...
...
@@ -55,9 +55,9 @@
"audio"
:
"音频"
,
"optional"
:
"(选填)"
,
"pageTitle"
:
"LightX2V服务"
,
"pleaseEnterThePromptForVideoGeneration"
:
"
请输入
视频生成提示词"
,
"pleaseEnterThePromptForVideoGeneration"
:
"视频生成提示词"
,
"describeTheContentStyleSceneOfTheVideo"
:
"描述视频内容、风格、场景等..."
,
"describeTheDigitalHumanImageBackgroundStyleActionRequirements"
:
"描述数字人表情、
语气、动作等...
"
,
"describeTheDigitalHumanImageBackgroundStyleActionRequirements"
:
"描述数字人表情、
动作等,例如:角色应根据音频做出夸张的动作
"
,
"describeTheContentActionRequirementsBasedOnTheImage"
:
"描述基于图片的视频内容、动作要求等..."
,
"loginSubtitle"
:
"一个强大的视频生成平台"
,
"loginWithGitHub"
:
"使用GitHub登录"
,
...
...
@@ -159,6 +159,18 @@
"retryTask"
:
"重试"
,
"downloadTask"
:
"下载视频"
,
"downloadVideo"
:
"下载视频"
,
"downloadPreparing"
:
"正在准备下载…"
,
"downloadFetching"
:
"正在获取文件…"
,
"downloadSaving"
:
"正在保存文件..."
,
"mobileSaveToAlbumTip"
:
"新窗口打开后长按视频即可保存到相册。"
,
"mobileSavePreviewTitle"
:
"预览并保存"
,
"mobileSaveInstruction"
:
"可点击全屏或长按视频,将其保存到手机相册。"
,
"mute"
:
"静音"
,
"unmute"
:
"取消静音"
,
"unsupportedAudioOrVideo"
:
"请选择音频或视频文件。"
,
"unsupportedVideoFormat"
:
"仅支持 MP4/M4V/MPEG 视频文件转换音频。"
,
"downloadInProgressNotice"
:
"已有下载任务正在进行,请稍候。"
,
"downloadCancelledAlert"
:
"已取消下载"
,
"deleteTask"
:
"删除"
,
"createVideo"
:
"创建视频"
,
"selectTemplate"
:
"选择模板"
,
...
...
@@ -182,16 +194,19 @@
"textToVideo"
:
"文生视频"
,
"imageToVideo"
:
"图生视频"
,
"speechToVideo"
:
"数字人"
,
"prompt"
:
"提示词"
,
"prompt"
:
"提示词
(选填)
"
,
"negativePrompt"
:
"负面提示词"
,
"promptTemplates"
:
"提示词模板"
,
"promptHistory"
:
"提示词历史"
,
"uploadImageFile"
:
"上传图片
文件
"
,
"uploadAudioFile"
:
"上传音频
文件
"
,
"uploadImageFile"
:
"上传图片"
,
"uploadAudioFile"
:
"上传音频"
,
"dragDropHere"
:
"拖拽文件到此处或点击上传"
,
"supportedImageFormats"
:
"支持jpg、png、webp图片格式(10MB以内)"
,
"supportedAudioFormats"
:
"支持mp3、m4a、wav音频格式(120s以内)"
,
"supportedAudioFormatsShort"
:
"支持mp3、m4a、wav等格式"
,
"supportedImageFormats"
:
"支持10MB以内的图片"
,
"supportedAudioFormats"
:
"支持120s以内的音频/视频"
,
"supportedAudioFormatsShort"
:
"支持120s以内的音频/视频"
,
"prefillLoadingDefault"
:
"正在准备素材..."
,
"prefillLoadingTemplate"
:
"正在加载模板素材..."
,
"prefillLoadingTask"
:
"正在加载任务素材..."
,
"maxFileSize"
:
"最大文件大小"
,
"taskId"
:
"任务ID"
,
"taskStatus"
:
"任务状态"
,
...
...
@@ -246,7 +261,7 @@
"taskMaterialReuseSuccessAlert"
:
"任务素材复用成功"
,
"loadTaskDataFailedAlert"
:
"加载任务数据失败"
,
"fileUnavailableAlert"
:
"文件不可用"
,
"downloadFailedAlert"
:
"下载失败"
,
"downloadFailedAlert"
:
"下载失败
,请重试。
"
,
"taskSubmitSuccessAlert"
:
"任务提交成功"
,
"taskSubmitFailedAlert"
:
"任务提交失败"
,
"submitTaskFailedAlert"
:
"任务提交失败"
,
...
...
@@ -323,7 +338,7 @@
"oneClickReplication"
:
"一键复刻同款"
,
"customizableContent"
:
"可自定义内容"
,
"poweredByLightX2V"
:
"速生视频 - LightX2V"
,
"latestAIModel"
:
"最新AI模型,飞速生成视频"
,
"latestAIModel"
:
"最新AI
数字人
模型,飞速生成视频"
,
"customizableCharacter"
:
"可自由更换角色与音频"
,
"userGeneratedVideo"
:
"生成的视频"
,
"noImage"
:
"暂无图片"
,
...
...
@@ -374,9 +389,89 @@
"multilingual"
:
"多语种"
,
"multiEmotion"
:
"多情感"
,
"videoDubbing"
:
"视频配音"
,
"ttsHistoryTitle"
:
"历史记录"
,
"ttsHistoryHint"
:
"系统会自动保留最近 20 条使用过的文本与语音指令。"
,
"ttsHistoryEmpty"
:
"暂未保存任何记录"
,
"ttsHistoryEmptyHint"
:
"生成一次语音即可创建首条历史记录。"
,
"ttsHistoryTextLabel"
:
"语音文本"
,
"ttsHistoryInstructionLabel"
:
"语音指令"
,
"ttsHistoryTextEmpty"
:
"(文本为空)"
,
"ttsHistoryInstructionEmpty"
:
"(指令为空)"
,
"ttsHistoryVoiceLabel"
:
"使用音色"
,
"ttsHistoryVoiceEmpty"
:
"未设置音色"
,
"ttsHistoryApply"
:
"使用该记录"
,
"ttsHistoryApplySelected"
:
"应用"
,
"ttsHistoryDeleteEntry"
:
"删除此记录"
,
"ttsHistoryTabCombined"
:
"全部"
,
"ttsHistoryTabText"
:
"输入文本"
,
"ttsHistoryTabInstruction"
:
"语音指令"
,
"ttsHistoryTabVoice"
:
"音色"
,
"ttsHistoryTitleCombined"
:
"全部历史记录"
,
"ttsHistoryTitleText"
:
"输入文本历史"
,
"ttsHistoryTitleInstruction"
:
"语音指令历史"
,
"ttsHistoryTitleVoice"
:
"音色历史"
,
"ttsHistoryClear"
:
"清空历史"
,
"allVersions"
:
"全部版本"
,
"allLanguages"
:
"全部语言"
,
"allGenders"
:
"全部性别"
,
"female"
:
"女性"
,
"male"
:
"男性"
"male"
:
"男性"
,
"taskCountdown"
:
"任务倒计时"
,
"footer"
:
{
"tagline"
:
"由 Light AI 工具链驱动的 AI 数字人视频生成平台"
,
"links"
:
{
"home"
:
"Light AI 官网"
,
"github"
:
"GitHub"
,
"xiaohongshu"
:
"小红书"
},
"alt"
:
{
"github"
:
"GitHub 标志"
,
"xiaohongshu"
:
"小红书标志"
},
"copyright"
:
"© {year} Light AI 版权所有"
},
"tts"
:
{
"title"
:
"AI 语音合成"
,
"subtitle"
:
"让您的文字变成动听的声音"
,
"inputText"
:
"请输入要合成的文字"
,
"voice"
:
"选择音色"
,
"speed"
:
"语速"
,
"volume"
:
"音量"
,
"pitch"
:
"音调"
,
"emotion"
:
"情感"
,
"generate"
:
"生成语音"
,
"cancel"
:
"取消"
,
"generating"
:
"生成中..."
,
"generated"
:
"已生成"
,
"error"
:
"生成失败"
,
"errorMessage"
:
"请检查输入文本或选择音色"
,
"voiceOptions"
:
"音色选项"
,
"fast"
:
"快速"
,
"normal"
:
"正常"
,
"slow"
:
"慢速"
,
"angry"
:
"愤怒"
,
"happy"
:
"开心"
,
"sad"
:
"悲伤"
,
"neutral"
:
"中性"
,
"excited"
:
"兴奋"
,
"calm"
:
"平静"
,
"gentle"
:
"温柔"
,
"serious"
:
"严肃"
,
"friendly"
:
"友好"
,
"professional"
:
"专业"
,
"child"
:
"儿童"
,
"robot"
:
"机器人"
,
"male"
:
"男声"
,
"female"
:
"女声"
,
"other"
:
"其他"
,
"search"
:
"搜索音色"
,
"noResults"
:
"没有找到相关音色"
,
"placeholder"
:
"请输入要合成的文字"
,
"placeholderVoice"
:
"请选择音色"
,
"placeholderSpeed"
:
"请选择语速"
,
"placeholderVolume"
:
"请选择音量"
,
"placeholderPitch"
:
"请选择音调"
,
"placeholderEmotion"
:
"请选择情感"
,
"placeholderGenerate"
:
"请点击生成按钮"
}
}
Prev
1
2
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment