Spaces:
Running
Running
import { client } from "./client.mjs"; | |
import { html, create, styled } from "./misc.mjs"; | |
const sample_texts = [ | |
{ | |
text: "天气预报显示,今天会有小雨,请大家出门时记得带伞。降温的天气也提醒我们要适时添衣保暖。", | |
}, | |
{ | |
text: "公司的年度总结会议将在下周三举行,请各部门提前准备好相关材料,确保会议顺利进行。", | |
}, | |
{ | |
text: "今天的午餐菜单包括烤鸡、沙拉和蔬菜汤,大家可以根据自己的口味选择适合的菜品。", | |
}, | |
{ | |
text: "请注意,电梯将在下午两点进行例行维护,预计需要一个小时的时间,请大家在此期间使用楼梯。", | |
}, | |
{ | |
text: "图书馆新到了一批书籍,涵盖了文学、科学和历史等多个领域,欢迎大家前来借阅。", | |
}, | |
]; | |
let history_index = 0; | |
const useStore = create((set, get) => ({ | |
tts: { | |
text: "你好,这里是一段ChatTTS Forge项目的示例文本。", | |
spk: "female2", | |
style: "chat", | |
temperature: 0.3, | |
top_P: 1, | |
top_K: 20, | |
seed: -1, | |
format: "mp3", | |
prompt1: "", | |
prompt2: "", | |
prefix: "", | |
}, | |
styles: [], | |
speakers: [], | |
ui: { | |
loading: false, | |
// 历史生成结果 { audio: Blob, url: string, params: object } | |
history: [], | |
}, | |
async synthesizeTTS() { | |
const params = structuredClone(get().tts); | |
const blob = await client.synthesizeTTS({ | |
...params, | |
}); | |
const blob_url = URL.createObjectURL(blob); | |
set({ | |
ui: { | |
...get().ui, | |
history: [ | |
...get().ui.history, | |
{ | |
id: history_index++, | |
audio: blob, | |
url: blob_url, | |
params: params, | |
}, | |
], | |
}, | |
}); | |
}, | |
setStyles(styles) { | |
set({ styles }); | |
}, | |
setSpeakers(speakers) { | |
set({ speakers }); | |
}, | |
setTTS(tts) { | |
set({ | |
tts: { | |
...get().tts, | |
...tts, | |
}, | |
}); | |
}, | |
setUI(ui) { | |
set({ | |
ui: { | |
...get().ui, | |
...ui, | |
}, | |
}); | |
}, | |
})); | |
window.addEventListener("load", async () => { | |
const styles = await client.listStyles(); | |
const speakers = await client.listSpeakers(); | |
console.log("styles:", styles); | |
console.log("speakers:", speakers); | |
useStore.get().setStyles(styles.data); | |
useStore.get().setSpeakers(speakers.data); | |
}); | |
const TTSPageContainer = styled.div` | |
h1 { | |
margin-bottom: 1rem; | |
} | |
p { | |
margin-bottom: 1rem; | |
} | |
#app { | |
margin-top: 1rem; | |
} | |
textarea { | |
width: 100%; | |
height: 10rem; | |
margin-bottom: 1rem; | |
min-height: 10rem; | |
resize: vertical; | |
} | |
button { | |
padding: 0.5rem 1rem; | |
background-color: #007bff; | |
color: white; | |
border: none; | |
cursor: pointer; | |
} | |
button:hover { | |
background-color: #0056b3; | |
} | |
button:disabled { | |
background-color: #6c757d; | |
cursor: not-allowed; | |
} | |
fieldset { | |
margin-top: 1rem; | |
padding: 1rem; | |
border: 1px solid #333; | |
} | |
legend { | |
font-weight: bold; | |
} | |
label { | |
display: block; | |
margin-bottom: 0.5rem; | |
} | |
select, | |
input[type="range"], | |
input[type="number"] { | |
width: 100%; | |
margin-top: 0.25rem; | |
} | |
input[type="range"] { | |
width: calc(100% - 2rem); | |
} | |
input[type="number"] { | |
width: calc(100% - 2rem); | |
padding: 0.5rem; | |
} | |
input[type="text"] { | |
width: 100%; | |
padding: 0.5rem; | |
} | |
audio { | |
margin-top: 1rem; | |
} | |
textarea, | |
input, | |
select { | |
background-color: #333; | |
color: white; | |
border: 1px solid #333; | |
border-radius: 0.25rem; | |
padding: 0.5rem; | |
} | |
table { | |
width: 100%; | |
border-collapse: collapse; | |
} | |
th, | |
td { | |
padding: 0.5rem; | |
border: 1px solid #333; | |
} | |
th { | |
background-color: #333; | |
color: white; | |
} | |
th:nth-child(2), | |
td:nth-child(2) { | |
width: 60%; | |
} | |
.content-body { | |
display: flex; | |
gap: 1rem; | |
} | |
.content-left { | |
flex: 1; | |
} | |
.content-right { | |
flex: 4; | |
} | |
h1 small { | |
font-weight: 100; | |
font-size: 0.5em; | |
font-weight: normal; | |
} | |
.btn-synthesize { | |
background-color: #007bff; | |
color: white; | |
border: none; | |
cursor: pointer; | |
padding: 0.5rem 1rem; | |
} | |
.btn-synthesize:hover { | |
background-color: #0056b3; | |
} | |
.btn-synthesize:disabled { | |
background-color: #6c757d; | |
cursor: not-allowed; | |
} | |
.btn-clear { | |
background-color: #dc3545; | |
color: white; | |
border: none; | |
cursor: pointer; | |
padding: 0.5rem 1rem; | |
} | |
.btn-clear:hover { | |
background-color: #bd2130; | |
} | |
.btn-clear:disabled { | |
background-color: #6c757d; | |
cursor: not-allowed; | |
} | |
.btn-random { | |
background-color: #28a745; | |
color: white; | |
border: none; | |
cursor: pointer; | |
padding: 0.5rem 1rem; | |
} | |
.btn-random:hover { | |
background-color: #218838; | |
} | |
pre { | |
white-space: pre-wrap; | |
} | |
.sample-texts { | |
width: unset; | |
display: inline-block; | |
padding: 0.5rem; | |
margin-bottom: 1rem; | |
} | |
`; | |
export const TTSPage = () => { | |
const { tts, setTTS, synthesizeTTS, ui, setUI, speakers, styles } = | |
useStore(); | |
const request = async () => { | |
if (ui.loading) { | |
return; | |
} | |
setUI({ loading: true }); | |
try { | |
await synthesizeTTS(); | |
} catch (error) { | |
console.error("Error synthesizing TTS:", error); | |
} finally { | |
setUI({ loading: false }); | |
} | |
}; | |
return html` | |
<${TTSPageContainer}> | |
<textarea | |
value=${tts.text} | |
onInput=${(e) => setTTS({ text: e.target.value })} | |
></textarea> | |
<button class="btn-synthesize" disabled=${ui.loading} onClick=${request}> | |
${ui.loading ? "Synthesizing..." : "Synthesize"} | |
</button> | |
<button | |
class="btn-clear" | |
disabled=${ui.loading} | |
onClick=${() => setUI({ history: [] })} | |
> | |
Clear History | |
</button> | |
<select | |
placeholder="Sample Text" | |
class="sample-texts" | |
value=${tts.text} | |
onChange=${(e) => setTTS({ text: e.target.value })} | |
> | |
${sample_texts.map( | |
(item, index) => html` | |
<option key=${index} value=${item.text}> | |
Sample ${index + 1}: ${item.text.slice(0, 10) + "..."} | |
</option> | |
` | |
)} | |
</select> | |
<div class="content-body"> | |
<fieldset class="content-left"> | |
<legend>Options</legend> | |
<label> | |
Speaker: | |
<select | |
value=${tts.spk} | |
onChange=${(e) => setTTS({ spk: e.target.value })} | |
> | |
<option value="-1">*random</option> | |
${speakers.map( | |
(spk) => html` | |
<option key=${spk.index} value=${spk.name}> | |
${spk.name} | |
</option> | |
` | |
)} | |
</select> | |
</label> | |
<label> | |
Style: | |
<select | |
value=${tts.style} | |
onChange=${(e) => setTTS({ style: e.target.value })} | |
> | |
<option value="">*auto</option> | |
${styles.map( | |
(style) => html` | |
<option key=${style.id} value=${style.name}> | |
${style.name} | |
</option> | |
` | |
)} | |
</select> | |
</label> | |
<label> | |
Temperature: | |
<input | |
type="range" | |
min="0.01" | |
max="2" | |
step="0.01" | |
value=${tts.temperature} | |
onInput=${(e) => setTTS({ temperature: e.target.value })} | |
/> | |
${tts.temperature} | |
</label> | |
<label> | |
Top P: | |
<input | |
type="range" | |
min="0.01" | |
max="1" | |
step="0.01" | |
value=${tts.top_P} | |
onInput=${(e) => setTTS({ top_P: e.target.value })} | |
/> | |
${tts.top_P} | |
</label> | |
<label> | |
Top K: | |
<input | |
type="range" | |
min="1" | |
max="50" | |
step="1" | |
value=${tts.top_K} | |
onInput=${(e) => setTTS({ top_K: e.target.value })} | |
/> | |
${tts.top_K} | |
</label> | |
<label> | |
Seed: | |
<input | |
type="number" | |
value=${tts.seed} | |
onInput=${(e) => setTTS({ seed: e.target.value })} | |
/> | |
<button | |
class="btn-random" | |
onClick=${() => | |
setTTS({ seed: Math.floor(Math.random() * 2 ** 32 - 1) })} | |
> | |
Random | |
</button> | |
</label> | |
<label> | |
Format | |
<select | |
value=${tts.format} | |
onChange=${(e) => setTTS({ format: e.target.value })} | |
> | |
<option value="mp3">MP3</option> | |
<option value="wav">WAV</option> | |
</select> | |
</label> | |
<label> | |
Prompt1: | |
<input | |
type="text" | |
value=${tts.prompt1} | |
onInput=${(e) => setTTS({ prompt1: e.target.value })} | |
/> | |
</label> | |
<label> | |
Prompt2: | |
<input | |
type="text" | |
value=${tts.prompt2} | |
onInput=${(e) => setTTS({ prompt2: e.target.value })} | |
/> | |
</label> | |
<label> | |
Prefix: | |
<input | |
type="text" | |
value=${tts.prefix} | |
onInput=${(e) => setTTS({ prefix: e.target.value })} | |
/> | |
</label> | |
</fieldset> | |
<fieldset class="content-right"> | |
<legend>History</legend> | |
<table> | |
<thead> | |
<tr> | |
<th>id</th> | |
<th>Params</th> | |
<th>Audio</th> | |
</tr> | |
</thead> | |
<tbody> | |
${[...ui.history].reverse().map( | |
(item, index) => html` | |
<tr key=${item.id}> | |
<td>${item.id}</td> | |
<td> | |
<pre>${JSON.stringify(item.params, null, 2)}</pre> | |
</td> | |
<td> | |
<audio controls> | |
<source | |
src=${item.url} | |
type="audio/${item.params.format}" | |
/> | |
</audio> | |
</td> | |
</tr> | |
` | |
)} | |
</tbody> | |
</table> | |
</fieldset> | |
</div> | |
<//> | |
`; | |
}; | |