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