64 lines
2.9 KiB
HTML
64 lines
2.9 KiB
HTML
|
|
{% extends "base.html" %}
|
||
|
|
{% block content %}
|
||
|
|
<div x-data="voicePage()">
|
||
|
|
<header class="page-header">
|
||
|
|
<h1>Voice · Whisper</h1>
|
||
|
|
<p class="subtitle page-tip">Spreek of upload audio — Whisper transcribeert, Herman antwoordt. Ook via Telegram spraakbericht 🎤</p>
|
||
|
|
</header>
|
||
|
|
<section class="panel">
|
||
|
|
<div class="browser-action-bar">
|
||
|
|
<button type="button" class="btn btn-primary" @click="toggleRecord()" x-text="recording ? '⏹ Stop opname' : '🎤 Live opnemen'"></button>
|
||
|
|
<input type="file" accept="audio/*" @change="onFile($event)" />
|
||
|
|
<button class="btn btn-secondary" @click="transcribe()" :disabled="!file && !recordedBlob">Transcribeer</button>
|
||
|
|
<button class="btn btn-primary" @click="sendHerman()" :disabled="!text">→ Herman</button>
|
||
|
|
</div>
|
||
|
|
<textarea class="form-input" rows="6" x-model="text" placeholder="Transcript verschijnt hier…"></textarea>
|
||
|
|
<pre class="browser-preview-text" x-show="reply" x-text="reply"></pre>
|
||
|
|
</section>
|
||
|
|
<ul>{% for ev in voice_events %}<li>{{ ev.title }} — {{ ev.created_at }}</li>{% else %}<li class="empty-state">Nog geen voice events.</li>{% endfor %}</ul>
|
||
|
|
</div>
|
||
|
|
{% endblock %}
|
||
|
|
{% block scripts %}
|
||
|
|
<script>
|
||
|
|
function voicePage() {
|
||
|
|
return {
|
||
|
|
file: null, text: '', reply: '', recording: false, recordedBlob: null, mediaRecorder: null, chunks: [],
|
||
|
|
onFile(e) { this.file = e.target.files[0]; this.recordedBlob = null; },
|
||
|
|
async toggleRecord() {
|
||
|
|
if (this.recording && this.mediaRecorder) {
|
||
|
|
this.mediaRecorder.stop(); this.recording = false; return;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||
|
|
this.chunks = [];
|
||
|
|
this.mediaRecorder = new MediaRecorder(stream);
|
||
|
|
this.mediaRecorder.ondataavailable = e => this.chunks.push(e.data);
|
||
|
|
this.mediaRecorder.onstop = () => {
|
||
|
|
this.recordedBlob = new Blob(this.chunks, { type: 'audio/webm' });
|
||
|
|
this.file = new File([this.recordedBlob], 'live.webm', { type: 'audio/webm' });
|
||
|
|
stream.getTracks().forEach(t => t.stop());
|
||
|
|
Cockpit.toast('Opname klaar — klik Transcribeer', 'success');
|
||
|
|
};
|
||
|
|
this.mediaRecorder.start(); this.recording = true;
|
||
|
|
} catch (e) { Cockpit.toast('Microfoon geweigerd: ' + e.message, 'error'); }
|
||
|
|
},
|
||
|
|
async transcribe() {
|
||
|
|
const f = this.file;
|
||
|
|
if (!f) return;
|
||
|
|
const fd = new FormData(); fd.append('file', f);
|
||
|
|
const r = await fetch('/api/voice/transcribe', { method: 'POST', body: fd });
|
||
|
|
const j = await r.json();
|
||
|
|
if (!r.ok) throw new Error(j.detail || 'Transcribe failed');
|
||
|
|
this.text = j.text || j.transcript || '';
|
||
|
|
Cockpit.toast('Whisper klaar', 'success');
|
||
|
|
},
|
||
|
|
async sendHerman() {
|
||
|
|
const r = await Cockpit.api('/api/herman/chat', { method: 'POST', body: JSON.stringify({ message: this.text }) });
|
||
|
|
this.reply = r.reply || JSON.stringify(r);
|
||
|
|
Cockpit.toast('Herman antwoordde', 'success');
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
{% endblock %}
|