|
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, |
|
|
|
|
|
|
|
|
|
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} /> |
|
<//>`; |
|
}; |
|
|