timcuhk's picture
Add spacing within ASR settings panel
9f6404d verified
Raw
History Blame Contribute Delete
53.6 kB
import base64
import mimetypes
import os
import secrets
import time
from concurrent.futures import ThreadPoolExecutor
from html import escape
from functools import lru_cache
import gradio as gr
import torch
from qwen_asr import Qwen3ASRModel
try:
import spaces
except Exception: # pragma: no cover - local CPU development fallback
class _SpacesFallback:
@staticmethod
def GPU(duration=120):
def decorator(func):
return func
return decorator
spaces = _SpacesFallback()
FINETUNED_MODEL_KEY = "heritagelab_finetuned"
OFF_THE_SHELF_MODEL_KEY = "off_the_shelf"
DEFAULT_MODEL_CHOICE = FINETUNED_MODEL_KEY
MODEL_CONFIGS = {
FINETUNED_MODEL_KEY: {
"id": os.getenv(
"FINETUNED_MODEL_ID",
os.getenv("MODEL_ID", "timcuhk/NRC-HeritageLab-Qwen3-ASR-0p6B-Inuktitut"),
),
"label": "HeritageLab fine-tuned Qwen3-ASR 0.6B (recommended)",
"display": "Qwen3-ASR 0.6B fine-tuned on HeritageLab",
"short": "Fine-tuned on HeritageLab",
"description": "Recommended for Inuktitut ASR in this demo; this checkpoint was fine-tuned on the HeritageLab dataset.",
},
OFF_THE_SHELF_MODEL_KEY: {
"id": os.getenv("OFF_THE_SHELF_MODEL_ID", "Qwen/Qwen3-ASR-0.6B"),
"label": "Off-the-shelf Qwen3-ASR 0.6B (not fine-tuned)",
"display": "Off-the-shelf Qwen3-ASR 0.6B",
"short": "Off-the-shelf baseline",
"description": "Original public Qwen3-ASR model without HeritageLab fine-tuning; included to show why fine-tuning matters.",
},
}
MODEL_CHOICES = [(config["label"], key) for key, config in MODEL_CONFIGS.items()]
DEFAULT_CONTEXT = os.getenv(
"DEFAULT_CONTEXT",
"The audio is Inuktitut in roman orthography. Transcribe using lowercase roman letters when possible.",
)
HF_ACCESS_TOKEN = os.getenv("HF_TOKEN") or os.getenv("HUGGING_FACE_HUB_TOKEN")
DEMO_USERNAME = os.getenv("DEMO_USERNAME", "nrc")
DEMO_PASSWORD = os.getenv("DEMO_PASSWORD")
NO_FORCED_LANGUAGE = "Auto / no forced language"
LANGUAGE_CHOICES = [
NO_FORCED_LANGUAGE,
"English",
"French",
]
EXAMPLE_AUDIO = {
"demo1": "examples/siqiniq.wav",
"demo2": "examples/tusaalanga_weekend.wav",
}
DEMO_MAX_NEW_TOKENS = 64
DEFAULT_DEVICE = "CPU"
CUSTOM_CSS = """
body {
background: #f8fafc !important;
}
.gradio-container {
max-width: 1280px !important;
margin: 0 auto !important;
padding: 24px 34px 44px 34px !important;
}
@media (max-width: 900px) {
.gradio-container {
padding-left: 14px !important;
padding-right: 14px !important;
}
}
.dark body,
body.dark,
html.dark body,
[data-theme="dark"] body {
background: #020617 !important;
}
.generating {
border-color: inherit !important;
box-shadow: none !important;
}
button.show-api {
display: none !important;
}
.nrc-model-notice {
border: 1px solid #cbd5e1 !important;
border-left: 4px solid #2563eb !important;
background: linear-gradient(180deg, #ffffff, #f8fafc) !important;
color: #1f2937 !important;
border-radius: 14px !important;
padding: 11px 13px !important;
font-size: 0.92rem !important;
line-height: 1.35 !important;
margin-top: 8px !important;
}
.nrc-model-notice.warning {
border-left-color: #d97706 !important;
}
.nrc-model-notice strong {
color: #1d4ed8 !important;
}
.nrc-model-notice.warning strong {
color: #b45309 !important;
}
.dark .nrc-model-notice,
body.dark .nrc-model-notice,
html.dark .nrc-model-notice,
[data-theme="dark"] .nrc-model-notice,
.gradio-container.dark .nrc-model-notice {
border-color: #475569 !important;
border-left-color: #60a5fa !important;
background: linear-gradient(180deg, #1f2937, #111827) !important;
color: #e5e7eb !important;
}
.dark .nrc-model-notice strong,
body.dark .nrc-model-notice strong,
html.dark .nrc-model-notice strong,
[data-theme="dark"] .nrc-model-notice strong,
.gradio-container.dark .nrc-model-notice strong {
color: #93c5fd !important;
}
.dark .nrc-model-notice.warning,
body.dark .nrc-model-notice.warning,
html.dark .nrc-model-notice.warning,
[data-theme="dark"] .nrc-model-notice.warning,
.gradio-container.dark .nrc-model-notice.warning {
border-left-color: #f59e0b !important;
}
.dark .nrc-model-notice.warning strong,
body.dark .nrc-model-notice.warning strong,
html.dark .nrc-model-notice.warning strong,
[data-theme="dark"] .nrc-model-notice.warning strong,
.gradio-container.dark .nrc-model-notice.warning strong {
color: #fbbf24 !important;
}
#settings-section > .styler,
#audio-section > .styler,
#result-section > .styler {
border-radius: 18px !important;
padding: 22px !important;
}
#settings-section,
#audio-section,
#result-section {
margin-top: 18px !important;
}
#settings-section .block,
#audio-section .block,
#result-section .block {
border-radius: 14px !important;
}
#settings-main-row {
gap: 26px !important;
align-items: stretch !important;
}
#model-settings-column,
#runtime-settings-column {
gap: 20px !important;
}
#device-language-row {
gap: 18px !important;
margin-bottom: 20px !important;
}
#settings-section .nrc-model-notice {
margin-top: 20px !important;
}
#settings-section .block {
margin-bottom: 0 !important;
}
#audio-section {
padding-top: 12px !important;
padding-bottom: 12px !important;
}
#audio-control-row {
align-items: center !important;
gap: 12px !important;
margin: 2px 0 16px 0 !important;
}
#audio-control-row .compact-help {
flex: 1 1 360px !important;
min-width: 280px !important;
}
#audio-control-row .compact-help .prose,
#audio-control-row .compact-help p {
margin: 0 !important;
font-size: 0.92rem !important;
line-height: 1.25 !important;
}
#audio-control-row button,
#transcribe-button {
min-height: 40px !important;
padding: 8px 18px !important;
border-radius: 999px !important;
}
#transcribe-button {
min-width: 190px !important;
font-weight: 700 !important;
}
#audio-input {
height: 205px !important;
min-height: 205px !important;
max-height: 205px !important;
margin-bottom: 0 !important;
}
#audio-input > .wrap {
height: 205px !important;
min-height: 205px !important;
max-height: 205px !important;
}
#audio-input .audio-container {
height: 178px !important;
min-height: 178px !important;
max-height: 178px !important;
}
#audio-input button.center.boundedheight.flex {
height: 148px !important;
min-height: 148px !important;
max-height: 148px !important;
padding: 10px !important;
}
#audio-input .source-selection {
padding: 2px 0 !important;
}
#status-panel .html-container {
max-height: 240px !important;
overflow-y: auto !important;
}
.nrc-ready-status {
border: 1px solid #e2e8f0 !important;
background: linear-gradient(180deg, #ffffff, #f8fafc) !important;
color: #0f172a !important;
border-radius: 18px !important;
padding: 14px 16px !important;
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.05) !important;
}
.nrc-ready-title {
color: #475569 !important;
font-size: 0.78rem !important;
font-weight: 700 !important;
letter-spacing: 0.08em !important;
text-transform: uppercase !important;
margin-bottom: 4px !important;
}
.nrc-ready-main {
color: #0f172a !important;
font-size: 1.02rem !important;
font-weight: 800 !important;
line-height: 1.25 !important;
}
.nrc-ready-note {
color: #475569 !important;
margin-top: 8px !important;
font-size: 0.92rem !important;
line-height: 1.35 !important;
}
.nrc-ready-meta {
display: flex !important;
flex-wrap: wrap !important;
gap: 8px !important;
margin-top: 10px !important;
}
.nrc-ready-pill {
border: 1px solid #dbeafe !important;
background: #eff6ff !important;
color: #1d4ed8 !important;
border-radius: 999px !important;
padding: 4px 9px !important;
font-size: 0.82rem !important;
font-weight: 700 !important;
}
.dark .nrc-ready-status,
body.dark .nrc-ready-status,
html.dark .nrc-ready-status,
[data-theme="dark"] .nrc-ready-status,
.gradio-container.dark .nrc-ready-status {
border-color: #334155 !important;
background: linear-gradient(180deg, #1f2937, #111827) !important;
color: #f8fafc !important;
}
.dark .nrc-ready-title,
body.dark .nrc-ready-title,
html.dark .nrc-ready-title,
[data-theme="dark"] .nrc-ready-title,
.gradio-container.dark .nrc-ready-title,
.dark .nrc-ready-note,
body.dark .nrc-ready-note,
html.dark .nrc-ready-note,
[data-theme="dark"] .nrc-ready-note,
.gradio-container.dark .nrc-ready-note {
color: #cbd5e1 !important;
}
.dark .nrc-ready-main,
body.dark .nrc-ready-main,
html.dark .nrc-ready-main,
[data-theme="dark"] .nrc-ready-main,
.gradio-container.dark .nrc-ready-main {
color: #f8fafc !important;
}
.dark .nrc-ready-pill,
body.dark .nrc-ready-pill,
html.dark .nrc-ready-pill,
[data-theme="dark"] .nrc-ready-pill,
.gradio-container.dark .nrc-ready-pill {
border-color: #1e40af !important;
background: #172554 !important;
color: #bfdbfe !important;
}
.nrc-working-status {
border: 2px solid #93c5fd !important;
border-left: 7px solid #2563eb !important;
background: linear-gradient(180deg, #ffffff, #dbeafe) !important;
color: #1e3a8a !important;
border-radius: 18px !important;
padding: 18px 20px !important;
box-shadow: 0 14px 32px rgba(37, 99, 235, 0.16) !important;
}
.nrc-working-head {
display: flex !important;
align-items: flex-start !important;
gap: 12px !important;
}
.nrc-working-spinner {
width: 18px !important;
height: 18px !important;
border: 3px solid #bfdbfe !important;
border-top-color: #2563eb !important;
border-radius: 999px !important;
flex: 0 0 auto !important;
margin-top: 2px !important;
animation: nrc-spin 0.9s linear infinite !important;
}
@keyframes nrc-spin {
to { transform: rotate(360deg); }
}
.nrc-working-status .nrc-working-title {
color: #1d4ed8 !important;
font-size: 0.8rem !important;
font-weight: 800 !important;
letter-spacing: 0.08em !important;
text-transform: uppercase !important;
margin-bottom: 4px !important;
}
.nrc-working-status .nrc-working-main {
color: #0f172a !important;
font-size: 1.12rem !important;
font-weight: 800 !important;
line-height: 1.25 !important;
}
.nrc-working-status .nrc-working-note {
color: #334155 !important;
margin-top: 10px !important;
font-size: 0.95rem !important;
font-weight: 650 !important;
line-height: 1.35 !important;
}
.nrc-working-steps {
display: flex !important;
flex-wrap: wrap !important;
gap: 8px !important;
margin-top: 13px !important;
}
.nrc-working-step,
.nrc-working-time {
border: 1px solid #bfdbfe !important;
background: rgba(255, 255, 255, 0.82) !important;
color: #1e40af !important;
border-radius: 999px !important;
padding: 6px 10px !important;
font-size: 0.84rem !important;
font-weight: 800 !important;
}
.dark .nrc-working-status,
body.dark .nrc-working-status,
html.dark .nrc-working-status,
[data-theme="dark"] .nrc-working-status,
.gradio-container.dark .nrc-working-status {
border-color: #1d4ed8 !important;
border-left-color: #60a5fa !important;
background: linear-gradient(180deg, #1e293b, #0f172a) !important;
color: #e5e7eb !important;
}
.dark .nrc-working-spinner,
body.dark .nrc-working-spinner,
html.dark .nrc-working-spinner,
[data-theme="dark"] .nrc-working-spinner,
.gradio-container.dark .nrc-working-spinner {
border-color: #1e40af !important;
border-top-color: #93c5fd !important;
}
.dark .nrc-working-status .nrc-working-title,
body.dark .nrc-working-status .nrc-working-title,
html.dark .nrc-working-status .nrc-working-title,
[data-theme="dark"] .nrc-working-status .nrc-working-title,
.gradio-container.dark .nrc-working-status .nrc-working-title,
.dark .nrc-working-status .nrc-working-note,
body.dark .nrc-working-status .nrc-working-note,
html.dark .nrc-working-status .nrc-working-note,
[data-theme="dark"] .nrc-working-status .nrc-working-note,
.gradio-container.dark .nrc-working-status .nrc-working-note {
color: #cbd5e1 !important;
}
.dark .nrc-working-status .nrc-working-main,
body.dark .nrc-working-status .nrc-working-main,
html.dark .nrc-working-status .nrc-working-main,
[data-theme="dark"] .nrc-working-status .nrc-working-main,
.gradio-container.dark .nrc-working-status .nrc-working-main {
color: #f8fafc !important;
}
.dark .nrc-working-step,
body.dark .nrc-working-step,
html.dark .nrc-working-step,
[data-theme="dark"] .nrc-working-step,
.gradio-container.dark .nrc-working-step,
.dark .nrc-working-time,
body.dark .nrc-working-time,
html.dark .nrc-working-time,
[data-theme="dark"] .nrc-working-time,
.gradio-container.dark .nrc-working-time {
border-color: #1e40af !important;
background: rgba(30, 41, 59, 0.92) !important;
color: #bfdbfe !important;
}
"""
APP_JS = r"""
() => {
const playbackState = {
armed: false,
playStarted: false,
};
let activeAudio = null;
const syncAudioElement = () => document.querySelector("#nrc-sync-audio");
const allAudioElements = () => Array.from(document.querySelectorAll("audio"));
const visibleAudioElements = () => allAudioElements().filter((audio) => audio.id !== "nrc-sync-audio");
const audioElement = () => activeAudio || syncAudioElement() || visibleAudioElements()[0] || document.querySelector("audio");
const transcriptPanel = () => document.querySelector("#transcript-panel");
const transcriptWords = () => Array.from(document.querySelectorAll("#transcript-panel span[data-word-index]"));
const transcriptText = () => {
const panel = transcriptPanel();
return panel ? panel.innerText.replace(/\s+/g, " ").trim() : "";
};
const statusContentElement = () => (
document.querySelector("#status-panel .prose") ||
document.querySelector("#status-panel .html-container > div") ||
document.querySelector("#status-panel .html-container") ||
document.querySelector("#status-panel")
);
const statusRootElement = () => document.querySelector("#status-panel");
const hasTranscript = () => transcriptWords().length > 0 && !transcriptText().includes("Transcript will appear here.");
const updateTranscriptHighlight = (sourceAudio = null) => {
const audio = sourceAudio || audioElement();
const words = transcriptWords();
if (!audio || !words.length) return;
const duration = Number.isFinite(audio.duration) && audio.duration > 0 ? audio.duration : Number(audio.dataset.duration || 0);
const ratio = duration > 0 ? Math.min(1, Math.max(0, audio.currentTime / duration)) : 0;
const activeCount = Math.min(words.length, Math.max(0, Math.ceil(ratio * words.length)));
words.forEach((word, index) => {
const isActive = index < activeCount;
word.style.color = isActive ? "#0f172a" : "#94a3b8";
word.style.fontWeight = isActive ? "800" : "500";
word.style.background = index === activeCount - 1 ? "#dbeafe" : "transparent";
word.style.borderRadius = index === activeCount - 1 ? "4px" : "0";
word.style.padding = index === activeCount - 1 ? "0 2px" : "0";
});
};
const attachAudioTimeSync = (audio) => {
if (!audio || audio.dataset.nrcSyncAttached === "1") return;
audio.dataset.nrcSyncAttached = "1";
const setActiveAndUpdate = () => {
activeAudio = audio;
updateTranscriptHighlight(audio);
};
audio.addEventListener("play", () => {
activeAudio = audio;
if (hasTranscript()) {
const duration = Number.isFinite(audio.duration) && audio.duration > 0 ? audio.duration : Number(audio.dataset.duration || 0);
if (duration > 0 && audio.currentTime >= duration - 0.05) {
try { audio.currentTime = 0; } catch (_) {}
}
}
updateTranscriptHighlight(audio);
});
audio.addEventListener("timeupdate", setActiveAndUpdate);
audio.addEventListener("seeking", setActiveAndUpdate);
audio.addEventListener("seeked", setActiveAndUpdate);
audio.addEventListener("pause", setActiveAndUpdate);
audio.addEventListener("ended", setActiveAndUpdate);
audio.addEventListener("loadedmetadata", setActiveAndUpdate);
};
const attachAllAudioTimeSync = () => allAudioElements().forEach((audio) => attachAudioTimeSync(audio));
const primeAudio = () => {
const audio = syncAudioElement();
attachAudioTimeSync(audio);
if (!audio || audio.dataset.primed === "1") return;
try {
audio.dataset.primed = "1";
audio.loop = true;
audio.muted = true;
audio.volume = 0;
audio.currentTime = 0;
const primed = audio.play();
if (primed && typeof primed.catch === "function") primed.catch(() => {});
} catch (_) {}
};
window.nrcDisarmTranscribePlayback = () => {
playbackState.armed = false;
playbackState.playStarted = false;
activeAudio = null;
allAudioElements().forEach((audio) => {
try {
audio.pause();
audio.loop = false;
audio.muted = false;
audio.volume = 1;
audio.currentTime = 0;
} catch (_) {}
});
updateTranscriptHighlight();
return [];
};
window.nrcArmTranscribePlayback = () => {
playbackState.armed = true;
playbackState.playStarted = false;
primeAudio();
return [];
};
window.nrcShowImmediateWorkingStatus = () => {
window.nrcArmTranscribePlayback();
const statusPanel = statusContentElement();
if (!statusPanel) return [];
const startedAt = Date.now();
window.nrcTranscribeStartedAt = startedAt;
const render = () => {
const elapsed = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
statusPanel.innerHTML = `
<div class="nrc-working-status" data-nrc-working="1" data-nrc-immediate-working="1">
<div class="nrc-working-head">
<div class="nrc-working-spinner" aria-hidden="true"></div>
<div>
<div class="nrc-working-title">Transcribing now</div>
<div class="nrc-working-main">Please wait — the page is actively processing.</div>
<div class="nrc-working-note">The app is not frozen. The model is loading or running, and the transcript will appear automatically when finished.</div>
</div>
</div>
<div class="nrc-working-steps">
<span class="nrc-working-step">Request sent</span>
<span class="nrc-working-step">Model starting/running</span>
<span class="nrc-working-time">Elapsed: <span class="nrc-elapsed-timer" data-nrc-started-at="${startedAt}">${elapsed}s</span></span>
</div>
</div>
`;
};
render();
const timer = window.setInterval(() => {
if (!document.querySelector("[data-nrc-working]")) {
window.clearInterval(timer);
return;
}
window.nrcRefreshElapsedTimers();
}, 1000);
return [];
};
window.nrcRefreshElapsedTimers = () => {
window.nrcCleanupCompletedStatus();
window.nrcCleanupStaleImmediateWorking();
const startedAt = window.nrcTranscribeStartedAt || Date.now();
document.querySelectorAll("[data-nrc-working]").forEach((panel) => {
panel.querySelectorAll(".nrc-elapsed-timer").forEach((timer) => {
const timerStartedAt = Number(timer.dataset.nrcStartedAt || startedAt);
if (!timer.dataset.nrcStartedAt) timer.dataset.nrcStartedAt = String(timerStartedAt);
const elapsed = Math.max(0, Math.round((Date.now() - timerStartedAt) / 1000));
timer.textContent = `${elapsed}s`;
});
});
};
window.nrcCleanupStaleImmediateWorking = () => {
const root = statusRootElement();
if (!root) return [];
const serverWorking = Array.from(root.querySelectorAll("[data-nrc-working]")).filter(
(panel) => !panel.hasAttribute("data-nrc-immediate-working")
);
if (!serverWorking.length) return [];
root.querySelectorAll("[data-nrc-immediate-working]").forEach((panel) => panel.remove());
return [];
};
window.nrcCleanupCompletedStatus = () => {
const root = statusRootElement();
if (!root) return [];
const hasCompletedReady = root.querySelector(".nrc-ready-status") && root.innerText.includes("Transcription complete");
if (!hasCompletedReady) return [];
root.querySelectorAll("[data-nrc-working]").forEach((panel) => panel.remove());
return [];
};
window.nrcStartTranscribePlayback = () => {
if (!playbackState.armed && playbackState.playStarted) return [];
if (!hasTranscript()) return [];
const audio = syncAudioElement() || audioElement();
attachAudioTimeSync(audio);
playbackState.playStarted = true;
playbackState.armed = false;
if (audio) {
try {
audio.loop = false;
audio.currentTime = 0;
audio.volume = 1;
audio.muted = false;
const started = audio.play();
if (started && typeof started.catch === "function") started.catch(() => {});
} catch (_) {}
}
updateTranscriptHighlight(audio);
return [];
};
window.nrcMirrorVisibleAudioButton = (button, intent = "") => {
const audio = syncAudioElement();
if (!button || !audio || !hasTranscript()) return [];
attachAudioTimeSync(audio);
const aria = button.getAttribute("aria-label") || "";
const duration = Number.isFinite(audio.duration) && audio.duration > 0 ? audio.duration : Number(audio.dataset.duration || 0);
if (intent === "play" || aria === "Pause") {
activeAudio = audio;
try {
audio.loop = false;
audio.muted = true;
audio.volume = 0;
if (duration > 0 && audio.currentTime >= duration - 0.05) audio.currentTime = 0;
const started = audio.play();
if (started && typeof started.catch === "function") started.catch(() => {});
} catch (_) {}
updateTranscriptHighlight(audio);
} else if (intent === "pause" || aria === "Play") {
try { audio.pause(); } catch (_) {}
updateTranscriptHighlight(audio);
}
return [];
};
window.nrcResetSyncedAudio = () => {
const audio = syncAudioElement();
if (!audio) return [];
activeAudio = audio;
try {
audio.pause();
audio.currentTime = 0;
} catch (_) {}
updateTranscriptHighlight(audio);
return [];
};
const playWhenTranscriptStarts = () => {
if (!playbackState.armed || playbackState.playStarted) return;
if (!hasTranscript()) return;
window.nrcStartTranscribePlayback();
};
const observeTranscript = () => {
const panel = transcriptPanel();
if (!panel) {
setTimeout(observeTranscript, 500);
return;
}
new MutationObserver(() => {
if (playbackState.armed) primeAudio();
updateTranscriptHighlight();
playWhenTranscriptStarts();
}).observe(panel, {
childList: true,
subtree: true,
characterData: true,
attributes: true,
});
};
const attachButtonGuards = () => {
document.querySelectorAll("button").forEach((button) => {
const label = button.innerText.trim();
if (label === "Transcribe" && !button.dataset.nrcPlaybackArm) {
button.dataset.nrcPlaybackArm = "1";
button.addEventListener("click", () => window.nrcShowImmediateWorkingStatus(), true);
}
if ((label === "Demo 1" || label === "Demo 2") && !button.dataset.nrcPlaybackDisarm) {
button.dataset.nrcPlaybackDisarm = "1";
button.addEventListener("click", () => window.nrcDisarmTranscribePlayback(), true);
}
const aria = button.getAttribute("aria-label") || "";
if (button.classList.contains("play-pause-button") && !button.dataset.nrcVisibleAudioBridge) {
button.dataset.nrcVisibleAudioBridge = "1";
button.addEventListener("click", () => {
const intent = (button.getAttribute("aria-label") || "") === "Play" ? "play" : "pause";
setTimeout(() => window.nrcMirrorVisibleAudioButton(button, intent), 80);
}, false);
}
if (aria === "Reset audio" && !button.dataset.nrcVisibleAudioReset) {
button.dataset.nrcVisibleAudioReset = "1";
button.addEventListener("click", () => setTimeout(() => window.nrcResetSyncedAudio(), 80), false);
}
});
if (playbackState.armed) primeAudio();
attachAllAudioTimeSync();
};
observeTranscript();
attachButtonGuards();
setInterval(() => {
attachButtonGuards();
window.nrcCleanupCompletedStatus();
window.nrcCleanupStaleImmediateWorking();
window.nrcRefreshElapsedTimers();
attachAllAudioTimeSync();
updateTranscriptHighlight();
playWhenTranscriptStarts();
}, 250);
}
"""
def _torch_dtype(device: str):
if device == "cuda" and torch.cuda.is_available():
return torch.float16
return torch.float32
def _model_config(model_choice: str):
return MODEL_CONFIGS.get(model_choice, MODEL_CONFIGS[DEFAULT_MODEL_CHOICE])
def _model_display_name(model_choice: str):
return _model_config(model_choice)["display"]
def _format_model_notice(model_choice: str = DEFAULT_MODEL_CHOICE):
config = _model_config(model_choice)
notice_class = "nrc-model-notice" if model_choice == FINETUNED_MODEL_KEY else "nrc-model-notice warning"
return f"""
<div class="{notice_class}">
<strong>Selected model:</strong> {escape(config["short"])}. {escape(config["description"])}
</div>
"""
def _handle_model_choice(model_choice: str):
return _format_model_notice(model_choice), _format_transcript_html(), _format_ready_details(model_choice), {}
@lru_cache(maxsize=4)
def _load_model(model_choice: str, device: str):
model_id = _model_config(model_choice)["id"]
kwargs = {
"trust_remote_code": True,
"max_inference_batch_size": 1,
"max_new_tokens": 192,
"torch_dtype": _torch_dtype(device),
}
if HF_ACCESS_TOKEN:
kwargs["token"] = HF_ACCESS_TOKEN
if device == "cuda" and torch.cuda.is_available():
kwargs["device_map"] = "cuda"
else:
kwargs["device_map"] = "cpu"
return Qwen3ASRModel.from_pretrained(model_id, **kwargs)
def _normalize_language(language: str):
language = (language or "").strip()
if not language or language == NO_FORCED_LANGUAGE:
return None
return language or None
def _context_for_language(language: str):
forced_language = _normalize_language(language)
if forced_language:
return f"Transcribe the audio in {forced_language}. Return only the transcript."
return DEFAULT_CONTEXT
def _best_available_device():
return "GPU" if torch.cuda.is_available() else "CPU"
def _format_run_details(model_choice: str, requested_device: str, actual_device: str, detected_language: str, elapsed: float):
actual_display = "GPU / CUDA" if actual_device == "cuda" else "CPU"
requested_display = f"Requested {requested_device}"
language_display = detected_language if detected_language != "not reported" else "Auto language"
device_color = "#166534" if actual_device == "cuda" else "#854d0e"
device_background = "#dcfce7" if actual_device == "cuda" else "#fef3c7"
return f"""
<div class="nrc-ready-status">
<div style="display:flex; justify-content:space-between; gap:12px; align-items:flex-start; flex-wrap:wrap;">
<div>
<div class="nrc-ready-title">Ready</div>
<div class="nrc-ready-main">Transcription complete</div>
<div class="nrc-ready-note">{escape(_model_display_name(model_choice))}</div>
</div>
<div style="background:{device_background}; color:{device_color}; border-radius:999px; padding:7px 12px; font-size:0.9rem; font-weight:800; white-space:nowrap;">{escape(actual_display)}</div>
</div>
<div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(145px,1fr)); gap:10px; margin-top:14px;">
<div style="border:1px solid #e2e8f0; background:#ffffff; border-radius:12px; padding:10px 12px;">
<div style="color:#64748b; font-size:0.78rem; font-weight:700; margin-bottom:3px;">Device request</div>
<div style="color:#0f172a; font-weight:700;">{escape(requested_display)}</div>
</div>
<div style="border:1px solid #e2e8f0; background:#ffffff; border-radius:12px; padding:10px 12px;">
<div style="color:#64748b; font-size:0.78rem; font-weight:700; margin-bottom:3px;">Language</div>
<div style="color:#0f172a; font-weight:700;">{escape(language_display)}</div>
</div>
<div style="border:1px solid #e2e8f0; background:#ffffff; border-radius:12px; padding:10px 12px;">
<div style="color:#64748b; font-size:0.78rem; font-weight:700; margin-bottom:3px;">Runtime</div>
<div style="color:#0f172a; font-weight:700;">{elapsed:.1f} seconds</div>
</div>
</div>
</div>
"""
def _format_pending_details(device: str = "GPU", model_choice: str = DEFAULT_MODEL_CHOICE):
device_note = "Requesting GPU inference" if device == "GPU" else "Running on CPU"
return f"""
<div class="nrc-working-status" data-nrc-working="1">
<div class="nrc-working-head">
<div class="nrc-working-spinner" aria-hidden="true"></div>
<div>
<div class="nrc-working-title">Transcribing now</div>
<div class="nrc-working-main">Please wait — the page is actively processing.</div>
<div class="nrc-working-note">The app is not frozen. {escape(device_note)} for {escape(_model_display_name(model_choice))}; the transcript will appear automatically when finished.</div>
</div>
</div>
<div class="nrc-working-steps">
<span class="nrc-working-step">Audio received</span>
<span class="nrc-working-step">Model starting</span>
<span class="nrc-working-time">Elapsed: <span class="nrc-elapsed-timer">0s</span></span>
</div>
</div>
"""
def _format_inference_progress_details(device: str, elapsed: float, model_choice: str = DEFAULT_MODEL_CHOICE):
device_note = "GPU inference is running" if device == "GPU" else "CPU inference is running"
return f"""
<div class="nrc-working-status" data-nrc-working="1">
<div class="nrc-working-head">
<div class="nrc-working-spinner" aria-hidden="true"></div>
<div>
<div class="nrc-working-title">Transcribing now</div>
<div class="nrc-working-main">Please wait — the page is actively processing.</div>
<div class="nrc-working-note">The app is not frozen. {escape(device_note)} for {escape(_model_display_name(model_choice))}; the transcript and audio highlighting will start automatically after inference finishes.</div>
</div>
</div>
<div class="nrc-working-steps">
<span class="nrc-working-step">Model running</span>
<span class="nrc-working-step">Transcript pending</span>
<span class="nrc-working-time">Elapsed: <span class="nrc-elapsed-timer">{elapsed:.0f}s</span></span>
</div>
</div>
"""
def _format_ready_details(model_choice: str = DEFAULT_MODEL_CHOICE, message: str | None = None, elapsed: float | None = None, actual_device: str | None = None):
note = message or "Choose or upload audio, then click Transcribe. Results will appear below."
meta_items = []
if elapsed is not None:
meta_items.append(f"<span class='nrc-ready-pill'>Runtime: {elapsed:.1f}s</span>")
if actual_device:
device_label = "GPU / CUDA" if actual_device == "cuda" else "CPU"
meta_items.append(f"<span class='nrc-ready-pill'>{escape(device_label)}</span>")
meta = f"<div class='nrc-ready-meta'>{''.join(meta_items)}</div>" if meta_items else ""
return f"""
<div class="nrc-ready-status">
<div class="nrc-ready-title">Ready</div>
<div class="nrc-ready-main">{escape(_model_display_name(model_choice))}</div>
<div class="nrc-ready-note">{escape(note)}</div>
{meta}
</div>
"""
def _format_demo_loading_details(demo_label: str):
return f"""
<div style="border:1px solid #dbeafe; background:linear-gradient(180deg,#ffffff,#f8fbff); border-radius:18px; padding:16px 18px; box-shadow:0 10px 24px rgba(15,23,42,0.06);">
<div style="color:#475569; font-size:0.78rem; font-weight:700; letter-spacing:0.08em; text-transform:uppercase; margin-bottom:4px;">Demo status</div>
<div style="color:#0f172a; font-size:1.02rem; font-weight:800; line-height:1.25;">Loading {escape(demo_label)}</div>
<div style="color:#475569; margin-top:10px; font-size:0.95rem;">Preparing the demo audio. Click play to listen, then click Transcribe to run ASR.</div>
</div>
"""
def _format_demo_ready_details(demo_label: str):
return f"""
<div style="border:1px solid #bbf7d0; background:linear-gradient(180deg,#ffffff,#f0fdf4); border-radius:18px; padding:16px 18px; box-shadow:0 10px 24px rgba(15,23,42,0.06);">
<div style="color:#166534; font-size:0.78rem; font-weight:700; letter-spacing:0.08em; text-transform:uppercase; margin-bottom:4px;">Demo ready</div>
<div style="color:#0f172a; font-size:1.02rem; font-weight:800; line-height:1.25;">{escape(demo_label)} is loaded</div>
<div style="color:#475569; margin-top:10px; font-size:0.95rem;">Click play to listen. Click Transcribe to run ASR; repeated Transcribe clicks replay the transcript highlighting.</div>
</div>
"""
def _format_audio_loading_notice(demo_label: str):
return f"""
<div style="border:1px solid #bfdbfe; background:#eff6ff; color:#1e3a8a; border-radius:12px; padding:10px 12px; font-size:0.92rem; margin-top:8px;">
Loading {escape(demo_label)} into the audio input...
</div>
"""
def _format_demo_ready_notice(demo_label: str):
return f"""
<div style="border:1px solid #bbf7d0; background:#f0fdf4; color:#166534; border-radius:12px; padding:10px 12px; font-size:0.92rem; margin-top:8px;">
{escape(demo_label)} is loaded. Click play to listen, then click Transcribe.
</div>
"""
def _format_upload_reminder(audio_path: str):
if not audio_path:
return ""
return """
<div style="border:1px solid #fde68a; background:#fffbeb; color:#854d0e; border-radius:12px; padding:10px 12px; font-size:0.92rem; margin-top:8px;">
If GPU is unavailable or the queue fails, select CPU and use a short clip.
</div>
"""
def _reset_demo_outputs():
return "", _format_playback_html(), _format_transcript_html(), _format_ready_details(), {}
def _handle_audio_change(audio_path: str):
if audio_path:
return _format_upload_reminder(audio_path), _format_playback_html(audio_path), _format_transcript_html(), _format_ready_details(), {}
return _reset_demo_outputs()
def _demo_selection(example_key: str, demo_label: str):
selected_device = DEFAULT_DEVICE
audio_path = EXAMPLE_AUDIO[example_key]
yield gr.update(label=f"Audio input — loading {demo_label}...", autoplay=False), selected_device, NO_FORCED_LANGUAGE, DEMO_MAX_NEW_TOKENS, _format_playback_html(), _format_transcript_html(), _format_demo_loading_details(demo_label), {}, _format_audio_loading_notice(demo_label)
yield gr.update(value=audio_path, label=f"Audio input — {demo_label} loaded", autoplay=False), selected_device, NO_FORCED_LANGUAGE, DEMO_MAX_NEW_TOKENS, _format_playback_html(audio_path), _format_transcript_html(), _format_demo_ready_details(demo_label), {}, _format_demo_ready_notice(demo_label)
def _audio_duration_seconds(audio_path: str):
try:
import soundfile as sf
audio_info = sf.info(audio_path)
if audio_info.frames and audio_info.samplerate:
return max(0.5, audio_info.frames / audio_info.samplerate)
except Exception:
pass
try:
import librosa
return max(0.5, float(librosa.get_duration(path=audio_path)))
except Exception:
return 2.0
def _audio_data_uri(audio_path: str):
if not audio_path or not os.path.exists(audio_path):
return ""
mime_type = mimetypes.guess_type(audio_path)[0] or "audio/wav"
with open(audio_path, "rb") as audio_file:
encoded_audio = base64.b64encode(audio_file.read()).decode("ascii")
return f"data:{mime_type};base64,{encoded_audio}"
def _format_playback_html(audio_path: str = "", autoplay_muted: bool = False):
if not audio_path:
return ""
data_uri = _audio_data_uri(audio_path)
if not data_uri:
return ""
duration_seconds = _audio_duration_seconds(audio_path)
priming_attrs = " autoplay muted loop data-priming='1'" if autoplay_muted else ""
return f"""
<audio id="nrc-sync-audio" preload="auto" playsinline data-duration="{duration_seconds:.6f}" style="display:none;"{priming_attrs} src="{data_uri}"></audio>
"""
def _format_transcript_html(text: str = "", visible_words: int = 0, show_all_words: bool = False):
words = (text or "").split()
visible = words[:visible_words]
if show_all_words and words:
body = " ".join(
f"<span data-word-index='{index}' style='color:{'#0f172a' if index < visible_words else '#94a3b8'}; font-weight:{'800' if index < visible_words else '500'};'>{escape(word)}</span>"
for index, word in enumerate(words)
)
elif not visible:
body = "<span style='color:#94a3b8;'>Transcript will appear here.</span>"
else:
body = " ".join(
f"<span data-word-index='{index}' style='color:#0f172a; font-weight:800;'>{escape(word)}</span>"
for index, word in enumerate(visible)
)
return f"""
<div style="border:1px solid #e2e8f0; background:#ffffff; border-radius:16px; padding:16px 18px; min-height:180px; line-height:1.8; font-size:1.2rem;">
<div style="color:#64748b; font-size:0.78rem; font-weight:800; letter-spacing:0.08em; text-transform:uppercase; margin-bottom:10px;">Transcript</div>
<div>{body}</div>
</div>
"""
def _highlight_transcript(text: str, duration_seconds: float):
words = (text or "").split()
yield _format_transcript_html(text, 0)
if not words:
return
pause = min(0.8, max(0.08, duration_seconds / len(words)))
for index in range(1, len(words) + 1):
time.sleep(pause)
yield _format_transcript_html(text, index)
def _highlight_replayed_transcript(audio_path: str, transcript_text: str):
transcript_text = (transcript_text or "").strip()
if not audio_path or not transcript_text:
yield _format_transcript_html()
return
words = transcript_text.split()
duration_seconds = _audio_duration_seconds(audio_path)
yield _format_transcript_html(transcript_text, 0, show_all_words=True)
if not words:
return
pause = min(0.8, max(0.08, duration_seconds / len(words)))
for index in range(1, len(words) + 1):
time.sleep(pause)
yield _format_transcript_html(transcript_text, index, show_all_words=True)
def _stream_fresh_transcript_with_audio(audio_path: str, transcript: str, duration_seconds: float):
words = (transcript or "").split()
if not words:
yield gr.update(), _format_transcript_html(transcript, 0)
return
pause = min(0.8, max(0.08, duration_seconds / len(words)))
yield gr.update(), _format_transcript_html(transcript, 0, show_all_words=True)
def _stream_replayed_transcript_with_audio(audio_path: str, transcript_text: str):
transcript_text = (transcript_text or "").strip()
if not audio_path or not transcript_text:
yield gr.update(), _format_transcript_html()
return
words = transcript_text.split()
duration_seconds = _audio_duration_seconds(audio_path)
pause = min(0.8, max(0.08, duration_seconds / len(words))) if words else 0.08
yield gr.update(), _format_transcript_html(transcript_text, 0, show_all_words=True)
for index in range(1, len(words) + 1):
time.sleep(pause)
yield gr.update(), _format_transcript_html(transcript_text, index, show_all_words=True)
def _reset_audio_to_start(audio_path: str):
yield gr.update()
time.sleep(0.25)
def _transcript_cache_matches(cache: dict, audio_path: str, model_choice: str, language: str, max_new_tokens: int):
if not isinstance(cache, dict) or not cache.get("text"):
return False
return (
cache.get("audio_path") == audio_path
and cache.get("model_choice") == model_choice
and cache.get("language") == language
and int(cache.get("max_new_tokens", 0)) == int(max_new_tokens)
)
def _make_transcript_cache(audio_path: str, model_choice: str, language: str, max_new_tokens: int, transcript: str, status: str):
return {
"audio_path": audio_path,
"model_choice": model_choice,
"language": language,
"max_new_tokens": int(max_new_tokens),
"text": transcript,
"status": status,
}
def _run_asr(audio_path: str, device: str, model_choice: str, language: str, max_new_tokens: int):
if not audio_path:
raise gr.Error("Please upload or record an audio file first.")
resolved_device = "cuda" if device == "GPU" and torch.cuda.is_available() else "cpu"
started = time.time()
model = _load_model(model_choice, resolved_device)
model.max_new_tokens = int(max_new_tokens)
outputs = model.transcribe(
[audio_path],
context=[_context_for_language(language)],
language=[_normalize_language(language)] if _normalize_language(language) else None,
)
result = outputs[0]
detected = getattr(result, "language", None) or "not reported"
text = getattr(result, "text", "").strip()
elapsed = time.time() - started
status = _format_run_details(model_choice, device, resolved_device, detected, elapsed)
return text, status
def _stream_transcript(audio_path, device, model_choice, language, max_new_tokens):
started = time.time()
with ThreadPoolExecutor(max_workers=1) as executor:
future = executor.submit(_run_asr, audio_path, device, model_choice, language, max_new_tokens)
while not future.done():
yield gr.update(), gr.update(), _format_transcript_html(), _format_inference_progress_details(device, time.time() - started, model_choice), {}
time.sleep(0.5)
transcript, status = future.result()
duration_seconds = _audio_duration_seconds(audio_path)
cache = _make_transcript_cache(audio_path, model_choice, language, max_new_tokens, transcript, status)
for audio_update, transcript_html in _stream_fresh_transcript_with_audio(audio_path, transcript, duration_seconds):
yield audio_update, gr.update(), transcript_html, status, cache
def transcribe_cpu(audio_path, model_choice, language, max_new_tokens):
yield from _stream_transcript(audio_path, "CPU", model_choice, language, max_new_tokens)
@spaces.GPU(duration=90)
def transcribe_gpu(audio_path, model_choice, language, max_new_tokens):
yield from _stream_transcript(audio_path, "GPU", model_choice, language, max_new_tokens)
def transcribe(audio_path, device, model_choice, language, max_new_tokens, transcript_cache):
if not audio_path:
raise gr.Error("Please upload, record, or select an audio file first.")
if _transcript_cache_matches(transcript_cache, audio_path, model_choice, language, max_new_tokens):
status = transcript_cache.get("status", "")
text = transcript_cache.get("text", "")
for audio_update in _reset_audio_to_start(audio_path):
yield audio_update, _format_playback_html(audio_path, autoplay_muted=True), _format_transcript_html(text, 0, show_all_words=True), status, transcript_cache
for audio_update, transcript_html in _stream_replayed_transcript_with_audio(audio_path, text):
yield audio_update, gr.update(), transcript_html, status, transcript_cache
return
for audio_update in _reset_audio_to_start(audio_path):
yield audio_update, _format_playback_html(audio_path, autoplay_muted=True), _format_transcript_html(), _format_pending_details(device, model_choice), {}
if device == "GPU":
yield from transcribe_gpu(audio_path, model_choice, language, max_new_tokens)
else:
yield from transcribe_cpu(audio_path, model_choice, language, max_new_tokens)
def _format_login_status(message: str = "", success: bool = False):
if not message:
return ""
border = "#bbf7d0" if success else "#fecaca"
background = "#f0fdf4" if success else "#fef2f2"
color = "#166534" if success else "#991b1b"
return f"""
<div style="border:1px solid {border}; background:{background}; color:{color}; border-radius:12px; padding:10px 12px; font-size:0.92rem; margin-top:8px;">
{escape(message)}
</div>
"""
def _validate_access(username: str, password: str):
if not DEMO_PASSWORD:
raise gr.Error("DEMO_PASSWORD Space secret is not configured. Please contact the demo owner.")
username_ok = secrets.compare_digest((username or "").strip(), DEMO_USERNAME)
password_ok = secrets.compare_digest(password or "", DEMO_PASSWORD)
if username_ok and password_ok:
return gr.update(visible=False), gr.update(visible=True), True, _format_login_status("Access granted.", True)
return gr.update(visible=True), gr.update(visible=False), False, _format_login_status("Incorrect username or password.")
def _require_access(access_granted: bool):
if access_granted is not True:
raise gr.Error("Please enter the demo password first.")
def _handle_audio_change_unlocked(audio_path: str, access_granted: bool):
_require_access(access_granted)
return _handle_audio_change(audio_path)
def demo_one_unlocked(access_granted: bool):
_require_access(access_granted)
yield from demo_one()
def demo_two_unlocked(access_granted: bool):
_require_access(access_granted)
yield from demo_two()
def transcribe_unlocked(audio_path, device, model_choice, language, max_new_tokens, transcript_cache, access_granted):
_require_access(access_granted)
yield from transcribe(audio_path, device, model_choice, language, max_new_tokens, transcript_cache)
def demo_one():
yield from _demo_selection("demo1", "Demo 1")
def demo_two():
yield from _demo_selection("demo2", "Demo 2")
with gr.Blocks(title="NRC-HeritageLab ASR Demo", css=CUSTOM_CSS, js=APP_JS) as demo:
gr.HTML(
"""
<div style="background: linear-gradient(135deg, #102a43, #315c7c); color: #ffffff; padding: 28px; border-radius: 18px; margin-bottom: 12px; box-shadow: 0 8px 24px rgba(15, 23, 42, 0.18);">
<h1 style="color: #ffffff; margin: 0 0 10px 0; font-size: 2.25rem; line-height: 1.15; font-weight: 800;">NRC-HeritageLab ASR Demo</h1>
<p style="color: #e0f2fe; margin: 0; font-size: 1.05rem; line-height: 1.45;">Inuktitut automatic speech recognition with a recommended HeritageLab fine-tuned Qwen3-ASR 0.6B checkpoint and an off-the-shelf Qwen3-ASR comparison model.</p>
</div>
"""
)
access_state = gr.State(False)
with gr.Column(visible=True) as login_panel:
gr.Markdown(
"**Password required**\n\n"
"This public demo is shared with restricted access. Enter the demo username and password to continue."
)
with gr.Row():
login_username = gr.Textbox(value=DEMO_USERNAME, label="Username")
login_password = gr.Textbox(label="Password", type="password")
login_button = gr.Button("Unlock demo", variant="primary")
login_status = gr.HTML()
with gr.Column(visible=False) as app_panel:
gr.Markdown(
"Follow the steps below: choose the model, provide audio, then run transcription. "
"The off-the-shelf model is included only for comparison with the HeritageLab fine-tuned checkpoint."
)
with gr.Group(elem_id="settings-section"):
gr.Markdown("**1. Choose model and recognition settings**")
with gr.Row(equal_height=True, elem_id="settings-main-row"):
with gr.Column(scale=5, min_width=360, elem_id="model-settings-column"):
model_choice = gr.Dropdown(
choices=MODEL_CHOICES,
value=DEFAULT_MODEL_CHOICE,
label="ASR model",
info="Use the fine-tuned model for normal demo use; choose off-the-shelf only to compare before fine-tuning.",
)
model_notice = gr.HTML(value=_format_model_notice(DEFAULT_MODEL_CHOICE))
with gr.Column(scale=4, min_width=320, elem_id="runtime-settings-column"):
with gr.Row(elem_id="device-language-row"):
device = gr.Radio(
choices=["CPU", "GPU"],
value=DEFAULT_DEVICE,
label="Inference device",
info="CPU is the default; GPU is faster when available.",
)
language = gr.Dropdown(
choices=LANGUAGE_CHOICES,
value=NO_FORCED_LANGUAGE,
label="Language constraint",
info="For Inuktitut, keep Auto.",
)
max_new_tokens = gr.Slider(
minimum=32,
maximum=384,
value=192,
step=16,
label="Maximum transcript length (tokens)",
info="Default is enough for short clips.",
)
with gr.Group(elem_id="audio-section"):
gr.Markdown("**2. Provide audio**")
with gr.Row(elem_id="audio-control-row", equal_height=False):
gr.Markdown(
"Use a demo, upload audio, or record with the microphone.",
elem_classes=["compact-help"],
)
demo_one_button = gr.Button("Demo 1", variant="secondary")
demo_two_button = gr.Button("Demo 2", variant="secondary")
run = gr.Button("Transcribe", variant="primary", elem_id="transcribe-button")
audio = gr.Audio(
sources=["upload", "microphone"],
type="filepath",
label="Audio input",
elem_id="audio-input",
)
upload_notice = gr.HTML()
playback = gr.HTML(value=_format_playback_html(), elem_id="playback-panel")
with gr.Group(elem_id="result-section"):
gr.Markdown("**3. Transcript and run status**")
status = gr.HTML(value=_format_ready_details(DEFAULT_MODEL_CHOICE), elem_id="status-panel")
transcript = gr.HTML(value=_format_transcript_html(), elem_id="transcript-panel")
transcript_cache_state = gr.State({})
with gr.Accordion("Notes", open=False):
gr.Markdown(
"""
- This demo runs locally inside the Hugging Face Space; it does not call an external ASR API.
- The default language setting does not force English, which is important for Inuktitut speech.
- Model choice matters: the fine-tuned checkpoint is recommended; the off-the-shelf model is included as a comparison baseline.
- CPU inference is included for portability, but GPU inference is recommended for longer clips.
"""
)
model_choice.change(
_handle_model_choice,
inputs=[model_choice],
outputs=[model_notice, transcript, status, transcript_cache_state],
show_progress="hidden",
api_name=False,
)
audio.change(
_handle_audio_change_unlocked,
inputs=[audio, access_state],
outputs=[upload_notice, playback, transcript, status, transcript_cache_state],
show_progress="hidden",
api_name=False,
)
audio.clear(
_reset_demo_outputs,
outputs=[upload_notice, playback, transcript, status, transcript_cache_state],
show_progress="hidden",
api_name=False,
)
demo_one_button.click(
demo_one_unlocked,
inputs=[access_state],
outputs=[audio, device, language, max_new_tokens, playback, transcript, status, transcript_cache_state, upload_notice],
show_progress="hidden",
api_name=False,
)
demo_two_button.click(
demo_two_unlocked,
inputs=[access_state],
outputs=[audio, device, language, max_new_tokens, playback, transcript, status, transcript_cache_state, upload_notice],
show_progress="hidden",
api_name=False,
)
run.click(
transcribe_unlocked,
inputs=[audio, device, model_choice, language, max_new_tokens, transcript_cache_state, access_state],
outputs=[audio, playback, transcript, status, transcript_cache_state],
show_progress="hidden",
api_name=False,
)
login_button.click(
_validate_access,
inputs=[login_username, login_password],
outputs=[login_panel, app_panel, access_state, login_status],
show_progress="hidden",
api_name="unlock_demo",
)
if __name__ == "__main__":
if not DEMO_PASSWORD:
raise RuntimeError("DEMO_PASSWORD Space secret must be configured before launch.")
demo.queue(default_concurrency_limit=1).launch()