RASMUS's picture
Upload webapp/src/app.ts with huggingface_hub
d811d47 verified
import { createWavBlob, loadReferenceAudio } from './audio'
import { APP_CONFIG } from './config'
import type { WorkerRequest, WorkerResponse } from './types'
export function mountApp(root: HTMLElement): void {
root.innerHTML = `
<main class="app-shell">
<section class="panel">
<h1>${APP_CONFIG.appTitle}</h1>
<p class="intro">
WebGPU-only Finnish TTS test app using the uploaded ONNX assets and a fixed Finnish reference voice.
</p>
<label class="field">
<span>Finnish text</span>
<textarea id="tts-text" rows="5">${APP_CONFIG.sampleText}</textarea>
</label>
<div class="actions">
<button id="speak-button" type="button">Speak</button>
<span id="busy-indicator" class="muted">Idle</span>
</div>
<div class="status-box">
<strong>Status</strong>
<pre id="status-log">Ready to load reference audio.</pre>
</div>
<div class="status-box">
<strong>Output</strong>
<audio id="audio-player" controls></audio>
<p id="result-meta" class="muted">No audio generated yet.</p>
</div>
</section>
</main>
`
const textArea = root.querySelector<HTMLTextAreaElement>('#tts-text')
const speakButton = root.querySelector<HTMLButtonElement>('#speak-button')
const busyIndicator = root.querySelector<HTMLSpanElement>('#busy-indicator')
const statusLog = root.querySelector<HTMLPreElement>('#status-log')
const audioPlayer = root.querySelector<HTMLAudioElement>('#audio-player')
const resultMeta = root.querySelector<HTMLParagraphElement>('#result-meta')
if (!textArea || !speakButton || !busyIndicator || !statusLog || !audioPlayer || !resultMeta) {
throw new Error('App UI failed to initialize.')
}
const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' })
let referenceAudioPromise: Promise<Float32Array> | null = null
let currentAudioUrl: string | null = null
const setBusy = (busy: boolean) => {
speakButton.disabled = busy
busyIndicator.textContent = busy ? 'Working...' : 'Idle'
}
const appendStatus = (message: string) => {
statusLog.textContent = `${statusLog.textContent}\n${message}`.trim()
}
const ensureReferenceAudio = async (): Promise<Float32Array> => {
referenceAudioPromise ??= loadReferenceAudio()
return referenceAudioPromise
}
worker.onmessage = (event: MessageEvent<WorkerResponse>) => {
if (event.data.type === 'status') {
appendStatus(event.data.message)
return
}
if (event.data.type === 'ready') {
appendStatus('Models loaded and worker is ready.')
return
}
if (event.data.type === 'error') {
appendStatus(`ERROR: ${event.data.message}`)
resultMeta.textContent = event.data.message
setBusy(false)
return
}
if (event.data.type === 'result') {
if (currentAudioUrl) {
URL.revokeObjectURL(currentAudioUrl)
}
currentAudioUrl = URL.createObjectURL(
createWavBlob(event.data.audio, event.data.sampleRate),
)
audioPlayer.src = currentAudioUrl
resultMeta.textContent = `Generated ${event.data.speechTokenCount} speech tokens at ${event.data.sampleRate} Hz.`
void audioPlayer.play()
appendStatus('Audio generation complete.')
setBusy(false)
}
}
speakButton.addEventListener('click', async () => {
try {
setBusy(true)
statusLog.textContent = 'Loading reference audio...'
const referenceAudio = await ensureReferenceAudio()
appendStatus('Sending request to worker...')
const message: WorkerRequest = {
type: 'speak',
text: textArea.value,
referenceAudio: new Float32Array(referenceAudio),
}
worker.postMessage(message, [message.referenceAudio.buffer])
} catch (error) {
appendStatus(
`ERROR: ${error instanceof Error ? error.message : String(error)}`,
)
setBusy(false)
}
})
}