| <!DOCTYPE html> |
| <html lang="zh-CN"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>音频转录</title> |
| <link rel="stylesheet" href="/static/plugin-ui.css"> |
| </head> |
| <body> |
| <main class="plugin-shell"> |
| <header class="plugin-header"> |
| <div> |
| <h1 class="plugin-title">音频转录</h1> |
| <p class="plugin-subtitle">上传音频并转录为文本。模型首次加载约需 10-30 秒。</p> |
| </div> |
| <span id="statusPill" class="plugin-status">检查中</span> |
| </header> |
| <div id="message" class="plugin-message"></div> |
|
|
| <section class="plugin-grid"> |
| <div class="plugin-panel"> |
| <h2 class="plugin-panel-title">音频</h2> |
| <div class="plugin-stack"> |
| |
| <div class="plugin-field"> |
| <label for="provider">ASR Provider</label> |
| <select id="provider" class="plugin-select"> |
| <option value="">加载中...</option> |
| </select> |
| <div id="providerStatus" class="plugin-muted"></div> |
| </div> |
| |
| <input id="file" type="file" accept="audio/*"> |
| <input id="language" placeholder="语言代码(可选,如 zh、en)"> |
| <button id="transcribeBtn" class="plugin-button" onclick="transcribe()">开始转录</button> |
| </div> |
| </div> |
| |
| <div id="logPanel"></div> |
| </section> |
|
|
| |
| <section class="plugin-grid" id="resultSection" style="display:none;"> |
| <div class="plugin-panel"> |
| <h2 class="plugin-panel-title">转录结果</h2> |
| <pre id="output" class="plugin-output"></pre> |
| </div> |
| </section> |
| </main> |
| <script src="/static/plugin-ui.js"></script> |
| <script> |
| const API = "/plugins/audio/api"; |
| let currentLogPanel = null; |
| |
| |
| async function loadProviders() { |
| try { |
| const data = await PluginUI.request(`${API}/providers`); |
| const select = document.getElementById("provider"); |
| select.innerHTML = ""; |
| for (const p of data.providers) { |
| const opt = document.createElement("option"); |
| opt.value = p.name; |
| opt.textContent = `${p.name} (${p.status})`; |
| opt.disabled = p.status !== "available"; |
| select.appendChild(opt); |
| } |
| |
| if (data.default) { |
| select.value = data.default; |
| } |
| updateProviderStatus(); |
| } catch (err) { |
| console.error("加载 provider 失败:", err); |
| } |
| } |
| |
| |
| function updateProviderStatus() { |
| const select = document.getElementById("provider"); |
| const statusEl = document.getElementById("providerStatus"); |
| const selected = select.options[select.selectedIndex]; |
| if (selected && selected.disabled) { |
| statusEl.textContent = `⚠ 该 provider 在当前环境不可用`; |
| statusEl.className = "plugin-text-error"; |
| } else { |
| statusEl.textContent = ""; |
| statusEl.className = "plugin-muted"; |
| } |
| } |
| |
| document.getElementById("provider").addEventListener("change", updateProviderStatus); |
| |
| |
| async function loadStatus() { |
| try { |
| const data = await PluginUI.request(`${API}/status`); |
| const pill = document.getElementById("statusPill"); |
| pill.textContent = data.enabled === false ? "插件未启用" : "可用"; |
| pill.className = `plugin-status ${data.enabled === false ? "warn" : "ok"}`; |
| } catch { |
| document.getElementById("statusPill").textContent = "状态未知"; |
| } |
| } |
| |
| |
| async function transcribe() { |
| const btn = document.getElementById("transcribeBtn"); |
| btn.disabled = true; |
| |
| |
| if (currentLogPanel) { |
| PluginUI.stopPolling(currentLogPanel); |
| currentLogPanel.remove(); |
| } |
| |
| |
| document.getElementById("resultSection").style.display = "none"; |
| PluginUI.clearMessage("message"); |
| |
| try { |
| const file = document.getElementById("file").files[0]; |
| if (!file) throw new Error("请选择音频文件"); |
| |
| const provider = document.getElementById("provider").value; |
| const language = document.getElementById("language").value.trim(); |
| |
| |
| let url = `${API}/upload`; |
| const params = []; |
| if (provider) params.push(`provider=${encodeURIComponent(provider)}`); |
| if (language) params.push(`language=${encodeURIComponent(language)}`); |
| if (params.length > 0) url += `?${params.join("&")}`; |
| |
| |
| const form = new FormData(); |
| form.append("file", file); |
| const data = await PluginUI.request(url, { method: "POST", body: form }); |
| |
| if (data.run_id) { |
| |
| currentLogPanel = PluginUI.createRunLog({ title: "转录日志" }); |
| document.getElementById("logPanel").appendChild(currentLogPanel); |
| |
| |
| PluginUI.pollRunEvents("audio", data.run_id, currentLogPanel, { |
| onRunComplete: (run) => { |
| btn.disabled = false; |
| if (run.status === "succeeded" && run.result) { |
| |
| document.getElementById("resultSection").style.display = "block"; |
| PluginUI.setJson("output", run.result); |
| } |
| }, |
| onError: (err) => { |
| btn.disabled = false; |
| PluginUI.showMessage("message", `轮询错误: ${err.message}`, "error"); |
| }, |
| }); |
| } else { |
| |
| btn.disabled = false; |
| if (data.status === "succeeded") { |
| document.getElementById("resultSection").style.display = "block"; |
| PluginUI.setJson("output", data); |
| } else { |
| PluginUI.showMessage("message", data.error || "转录失败", "error"); |
| } |
| } |
| } catch (error) { |
| btn.disabled = false; |
| PluginUI.showMessage("message", error.message, "error"); |
| } |
| } |
| |
| |
| loadProviders(); |
| loadStatus(); |
| </script> |
| </body> |
| </html> |
|
|