Spaces:
Running
on
Zero
Running
on
Zero
import { client } from "./client.mjs"; | |
import { html, create, styled } from "./misc.mjs"; | |
const default_ssml = ` | |
<speak version="0.1"> | |
<voice spk="Bob" seed="-1" style="narration-relaxed"> | |
这里是一个简单的 SSML 示例。 | |
</voice> | |
</speak> | |
`.trim(); | |
const useStore = create((set, get) => ({ | |
params: { | |
ssml: default_ssml, | |
format: "mp3", | |
}, | |
setParams: (params) => set({ params }), | |
loading: false, | |
/** | |
* @type {Array<{ id: number, params: { ssml: string; format: string }, url: string }>} | |
*/ | |
history: [], | |
setHistory: (history) => set({ history }), | |
})); | |
const SSMLFormContainer = styled.div` | |
display: flex; | |
flex-direction: column; | |
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; | |
} | |
.ssml-body { | |
display: flex; | |
gap: 1rem; | |
} | |
table { | |
width: 100%; | |
border-collapse: collapse; | |
} | |
th, | |
td { | |
padding: 0.5rem; | |
border: 1px solid #333; | |
} | |
th { | |
background-color: #333; | |
color: white; | |
} | |
.btn-danger { | |
background-color: #dc3545; | |
color: white; | |
border: none; | |
cursor: pointer; | |
} | |
.btn-danger:hover { | |
background-color: #bd2130; | |
} | |
`; | |
const SSMLOptions = () => { | |
const { params, setParams } = useStore(); | |
return html` | |
<fieldset style="flex: 2"> | |
<legend>Options</legend> | |
<label> | |
Format | |
<select | |
value=${params.format} | |
onChange=${(e) => setParams({ ...params, format: e.target.value })} | |
> | |
<option value="mp3">MP3</option> | |
<option value="wav">WAV</option> | |
</select> | |
</label> | |
</fieldset> | |
`; | |
}; | |
const SSMLHistory = () => { | |
const { history } = useStore(); | |
return html` | |
<fieldset style="flex: 5"> | |
<legend>History</legend> | |
<table> | |
<thead> | |
<tr> | |
<th>index</th> | |
<th>SSML</th> | |
<th>Audio</th> | |
</tr> | |
</thead> | |
<tbody> | |
${[...history].reverse().map( | |
(item) => html` | |
<tr key=${item.id}> | |
<td>${item.id}</td> | |
<td> | |
<textarea | |
readonly | |
style="width: 100%; height: 5rem; resize: none;" | |
> | |
${item.params.ssml} | |
</textarea | |
> | |
</td> | |
<td> | |
<audio controls> | |
<source | |
src=${item.url} | |
type="audio/${item.params.format}" | |
/> | |
</audio> | |
</td> | |
</tr> | |
` | |
)} | |
</tbody> | |
</table> | |
</fieldset> | |
`; | |
}; | |
let generate_index = 0; | |
const SSMLForm = () => { | |
const { params, setParams, loading } = useStore(); | |
const request = async () => { | |
useStore.set({ loading: true }); | |
try { | |
const blob = await client.synthesizeSSML(params); | |
const blob_url = URL.createObjectURL(blob); | |
useStore.set({ | |
history: [ | |
...useStore.get().history, | |
{ | |
id: generate_index++, | |
params, | |
url: blob_url, | |
}, | |
], | |
}); | |
} finally { | |
useStore.set({ loading: false }); | |
} | |
}; | |
return html` | |
<${SSMLFormContainer}> | |
<textarea | |
placeholder="Enter SSML here..." | |
value=${params.ssml} | |
onInput=${(e) => setParams({ ...params, ssml: e.target.value })} | |
/> | |
<div> | |
<button onClick=${request} disabled=${!params.ssml || loading}> | |
Submit | |
</button> | |
<button | |
class="btn btn-danger" | |
onClick=${() => { | |
useStore.set({ history: [] }); | |
}} | |
disabled=${loading} | |
> | |
Clear History | |
</button> | |
</div> | |
<div class="ssml-body"> | |
<${SSMLOptions} /> | |
<${SSMLHistory} /> | |
</div> | |
<//> | |
`; | |
}; | |
const SSMLPageContainer = styled.div` | |
display: flex; | |
flex-direction: column; | |
height: 100%; | |
`; | |
export const SSMLPage = () => { | |
return html` <${SSMLPageContainer}> | |
<${SSMLForm} /> | |
<//>`; | |
}; | |