ChatTTS-Forge / playground /tts.page.mjs
zhzluke96
update
01e655b
raw
history blame
10.7 kB
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>
<//>
`;
};