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