Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="description" content="PromptDialog multi-speaker dialogue audio demo."> | |
| <meta name="keywords" content="PromptDialog, text-to-speech, dialogue, audio demo"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>PromptDialog</title> | |
| <link rel="stylesheet" href="./static/css/bulma.min.css"> | |
| <link rel="stylesheet" href="./static/css/index.css"> | |
| </head> | |
| <body> | |
| <main> | |
| <section class="prompt-hero"> | |
| <div class="container wide-container"> | |
| <h1 class="publication-title">PromptDialog</h1> | |
| </div> | |
| </section> | |
| <section class="section demo-section" id="audio-demo"> | |
| <div class="container wide-container"> | |
| <nav class="task-directory" id="task-directory" aria-label="Audio demo tasks"> | |
| <p class="loading">Loading task directory...</p> | |
| </nav> | |
| <div class="task-list" id="task-list" aria-live="polite"> | |
| <div class="task-card"> | |
| <p class="loading">Loading audio demos...</p> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| </main> | |
| <script> | |
| const TASKS = [ | |
| { id: "multi", root: "./static/task/multi" }, | |
| { id: "paral", root: "./static/task/paral" }, | |
| { id: "punct", root: "./static/task/punct" }, | |
| { id: "emo", root: "./static/task/emo" }, | |
| { id: "mixed-style", root: "./static/task/mixed-style" } | |
| ]; | |
| function parseTaskYaml(source) { | |
| const task = { name: "", description: "", show_reference: true, models: [] }; | |
| let listKey = ""; | |
| source.split(/\r?\n/).forEach((rawLine) => { | |
| const line = rawLine.trim(); | |
| if (!line || line.startsWith("#")) return; | |
| if (line.startsWith("- ") && listKey) { | |
| task[listKey].push(line.slice(2).trim().replace(/^["']|["']$/g, "")); | |
| return; | |
| } | |
| const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/); | |
| if (!match) return; | |
| const key = match[1]; | |
| const value = match[2].trim().replace(/^["']|["']$/g, ""); | |
| if (value === "true" || value === "false") { | |
| task[key] = value === "true"; | |
| listKey = ""; | |
| } else if (Array.isArray(task[key])) { | |
| listKey = key; | |
| } else { | |
| task[key] = value; | |
| listKey = ""; | |
| } | |
| }); | |
| return task; | |
| } | |
| async function fetchText(path) { | |
| const response = await fetch(path); | |
| if (!response.ok) { | |
| throw new Error(`Failed to load ${path}`); | |
| } | |
| return response.text(); | |
| } | |
| function parseJsonl(source) { | |
| return source | |
| .split(/\r?\n/) | |
| .map((line) => line.trim()) | |
| .filter(Boolean) | |
| .map((line) => JSON.parse(line)); | |
| } | |
| function createAudio(src) { | |
| const audio = document.createElement("audio"); | |
| audio.controls = true; | |
| audio.preload = "none"; | |
| audio.src = src; | |
| return audio; | |
| } | |
| function formatScriptText(text, highlightParalinguistic) { | |
| const escapedText = escapeHtml(text); | |
| if (!highlightParalinguistic) { | |
| return escapedText; | |
| } | |
| return escapedText.replace(/[唉嗯哈呵嘿哼呼咳啧]/g, '<em class="paralinguistic-cue">$&</em>'); | |
| } | |
| function createScriptCell(item, highlightParalinguistic = false) { | |
| const wrapper = document.createElement("div"); | |
| wrapper.className = "script-cell"; | |
| if (Array.isArray(item.variants)) { | |
| const target = document.createElement("p"); | |
| target.innerHTML = formatScriptText(item.target_text, highlightParalinguistic); | |
| wrapper.appendChild(target); | |
| item.variants.forEach((variant, index) => { | |
| const context = document.createElement("p"); | |
| context.className = "variant-context"; | |
| context.innerHTML = `<strong>instruction${index + 1}</strong> <span class="context-cue">${escapeHtml(variant.context)}</span>`; | |
| wrapper.appendChild(context); | |
| }); | |
| return wrapper; | |
| } | |
| const texts = Array.isArray(item.target_text) ? item.target_text : [item.target_text]; | |
| const speakers = Array.isArray(item.speaker) ? item.speaker : []; | |
| const contexts = Array.isArray(item.context) ? item.context : [item.context]; | |
| texts.forEach((text, index) => { | |
| const turn = document.createElement("p"); | |
| const speaker = speakers[index]; | |
| const context = contexts[index]; | |
| const cleanContext = context ? String(context).replace(/[,,。.!!??;;::、\s]+$/u, "") : ""; | |
| const contextBadge = cleanContext ? ` <span class="context-cue">${escapeHtml(cleanContext)}</span>` : ""; | |
| const formattedText = formatScriptText(text, highlightParalinguistic); | |
| if (speaker !== undefined && speaker !== "") { | |
| turn.innerHTML = `<strong>[S${Number(speaker) + 1}]</strong> ${formattedText}${contextBadge}`; | |
| } else { | |
| turn.innerHTML = `${formattedText}${contextBadge}`; | |
| } | |
| wrapper.appendChild(turn); | |
| }); | |
| return wrapper; | |
| } | |
| function createReferenceCell(item, modelRoot) { | |
| const wrapper = document.createElement("div"); | |
| wrapper.className = "reference-cell"; | |
| const prompts = Array.isArray(item.prompt_audio) ? item.prompt_audio : []; | |
| prompts.forEach((audioPath, index) => { | |
| const block = document.createElement("div"); | |
| const label = document.createElement("span"); | |
| label.textContent = `Speaker ${index + 1}`; | |
| block.appendChild(label); | |
| block.appendChild(createAudio(`${modelRoot}/${audioPath}`)); | |
| wrapper.appendChild(block); | |
| }); | |
| return wrapper; | |
| } | |
| function escapeHtml(text) { | |
| return text.replace(/[&<>"']/g, (char) => ({ | |
| "&": "&", | |
| "<": "<", | |
| ">": ">", | |
| '"': """, | |
| "'": "'" | |
| }[char])); | |
| } | |
| function createAudioCard(label, content) { | |
| const card = document.createElement("div"); | |
| card.className = "audio-card"; | |
| const title = document.createElement("div"); | |
| title.className = "audio-card-title"; | |
| title.textContent = label; | |
| const body = document.createElement("div"); | |
| body.className = "audio-card-body"; | |
| if (content) { | |
| body.appendChild(content); | |
| } | |
| card.appendChild(title); | |
| card.appendChild(body); | |
| return card; | |
| } | |
| function createVariantAudioCell(modelItem, taskRoot, model) { | |
| const wrapper = document.createElement("div"); | |
| wrapper.className = "variant-audio-list"; | |
| if (!modelItem || !Array.isArray(modelItem.variants)) { | |
| return wrapper; | |
| } | |
| modelItem.variants.forEach((variant, index) => { | |
| const row = document.createElement("div"); | |
| row.className = "variant-audio-row"; | |
| const label = document.createElement("span"); | |
| label.textContent = `instruction${index + 1}`; | |
| row.appendChild(label); | |
| row.appendChild(createAudio(`${taskRoot}/${model}/${variant.output_audio}`)); | |
| wrapper.appendChild(row); | |
| }); | |
| return wrapper; | |
| } | |
| function renderTable(task, manifests, taskRoot) { | |
| const wrapper = document.createElement("div"); | |
| wrapper.className = "demo-cases"; | |
| const highlightParalinguistic = task.name === "Paralinguistic Cue Generation"; | |
| manifests[task.models[0]].forEach((item) => { | |
| const caseBlock = document.createElement("article"); | |
| caseBlock.className = "case-block"; | |
| const scriptBlock = document.createElement("div"); | |
| scriptBlock.className = "case-script"; | |
| const scriptTitle = document.createElement("div"); | |
| scriptTitle.className = "case-section-title"; | |
| scriptTitle.textContent = "Script"; | |
| scriptBlock.appendChild(scriptTitle); | |
| scriptBlock.appendChild(createScriptCell(item, highlightParalinguistic)); | |
| caseBlock.appendChild(scriptBlock); | |
| const audioGrid = document.createElement("div"); | |
| audioGrid.className = "audio-grid"; | |
| if (task.show_reference) { | |
| const modelRoot = `${taskRoot}/${task.models[0]}`; | |
| audioGrid.appendChild(createAudioCard("Reference", createReferenceCell(item, modelRoot))); | |
| } | |
| task.models.forEach((model) => { | |
| const modelItem = manifests[model].find((entry) => entry.utt === item.utt); | |
| const content = document.createElement("div"); | |
| content.className = "audio-card-content"; | |
| if (modelItem && Array.isArray(modelItem.variants)) { | |
| content.appendChild(createVariantAudioCell(modelItem, taskRoot, model)); | |
| } else if (modelItem) { | |
| content.appendChild(createAudio(`${taskRoot}/${model}/${modelItem.output_audio}`)); | |
| } | |
| audioGrid.appendChild(createAudioCard(model, content)); | |
| }); | |
| caseBlock.appendChild(audioGrid); | |
| wrapper.appendChild(caseBlock); | |
| }); | |
| return wrapper; | |
| } | |
| function renderDirectory(tasks) { | |
| const directory = document.getElementById("task-directory"); | |
| const title = document.createElement("div"); | |
| title.className = "task-directory-title"; | |
| title.textContent = "Tasks"; | |
| const links = document.createElement("div"); | |
| links.className = "task-directory-links"; | |
| tasks.forEach(({ id, task }) => { | |
| const link = document.createElement("a"); | |
| link.href = `#task-${id}`; | |
| link.textContent = task.name; | |
| links.appendChild(link); | |
| }); | |
| directory.replaceChildren(title, links); | |
| } | |
| async function loadTask(config) { | |
| const task = parseTaskYaml(await fetchText(`${config.root}/task.yaml`)); | |
| const manifests = {}; | |
| await Promise.all(task.models.map(async (model) => { | |
| manifests[model] = parseJsonl(await fetchText(`${config.root}/${model}/manifest.jsonl`)); | |
| })); | |
| return { ...config, task, manifests }; | |
| } | |
| function renderTaskCard({ id, root, task, manifests }) { | |
| const card = document.createElement("section"); | |
| card.className = "task-card"; | |
| card.id = `task-${id}`; | |
| const heading = document.createElement("div"); | |
| heading.className = "task-card-heading"; | |
| const title = document.createElement("h3"); | |
| title.textContent = task.name; | |
| const description = document.createElement("p"); | |
| description.textContent = task.description; | |
| heading.appendChild(title); | |
| heading.appendChild(description); | |
| card.appendChild(heading); | |
| card.appendChild(renderTable(task, manifests, root)); | |
| return card; | |
| } | |
| async function initDemo() { | |
| const tasks = await Promise.all(TASKS.map(loadTask)); | |
| renderDirectory(tasks); | |
| const list = document.getElementById("task-list"); | |
| list.replaceChildren(...tasks.map(renderTaskCard)); | |
| } | |
| initDemo().catch((error) => { | |
| document.getElementById("task-directory").textContent = "Failed to load task metadata."; | |
| document.getElementById("task-list").textContent = error.message; | |
| }); | |
| </script> | |
| </body> | |
| </html> | |