Feat: Chat-Verbesserungen — echte Umlaute, TTS-Filter, Lautstärke-Regler, Sprechblasen
- Umlaute: System-Prompt erzwingt echte ä/ö/ü/ß (Hermes spiegelte bei Technik den ASCII-Hint). - TTS-Filter: cleanForTTS entfernt URLs/Markdown/Code/Emojis vor der Sprachausgabe (Anzeige bleibt). - Lautstärke: GainNode in AudioQueue + Regler (Default 60%, live); Probe-hören respektiert ihn. (Bugfix: Number(null)===0 → wäre sonst stumm gewesen.) - Chat: Sprechblasen (User rechts / Hermes links), Zeilenumbrüche, Auto-Scroll, Tipp-Indikator. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
export class AudioQueue {
|
||||
private ctx: AudioContext
|
||||
private analyser: AnalyserNode
|
||||
private gain: GainNode
|
||||
private queue: ArrayBuffer[] = []
|
||||
private playing = false
|
||||
private raf = 0
|
||||
@@ -16,14 +17,31 @@ export class AudioQueue {
|
||||
readonly level = { current: 0 }
|
||||
onSpeaking?: (speaking: boolean) => void
|
||||
|
||||
/** Lautstärke 0..1.5 aus localStorage (Default 0.6 — die Stimmen waren zu laut). */
|
||||
static readVolume(): number {
|
||||
const raw = localStorage.getItem("mc_voice_volume")
|
||||
if (raw === null || raw === "") return 0.6 // Number(null)===0 → sonst aus Versehen stumm
|
||||
const v = Number(raw)
|
||||
return Number.isNaN(v) ? 0.6 : Math.max(0, Math.min(1.5, v))
|
||||
}
|
||||
|
||||
constructor() {
|
||||
const Ctor = window.AudioContext || (window as any).webkitAudioContext
|
||||
this.ctx = new Ctor()
|
||||
this.analyser = this.ctx.createAnalyser()
|
||||
this.analyser.fftSize = 256
|
||||
this.analyser.smoothingTimeConstant = 0.6
|
||||
this.analyser.connect(this.ctx.destination)
|
||||
// Kette: Quelle → Analyser (Lippensync liest vollen Pegel) → Gain (Lautstärke) → Ausgang.
|
||||
this.gain = this.ctx.createGain()
|
||||
this.gain.gain.value = AudioQueue.readVolume()
|
||||
this.analyser.connect(this.gain)
|
||||
this.gain.connect(this.ctx.destination)
|
||||
this.freq = new Uint8Array(new ArrayBuffer(this.analyser.frequencyBinCount))
|
||||
// Live-Lautstärke vom Slider.
|
||||
window.addEventListener("mc-voice-volume", (e: Event) => {
|
||||
const v = Number((e as CustomEvent).detail)
|
||||
if (!Number.isNaN(v)) this.gain.gain.value = Math.max(0, Math.min(1.5, v))
|
||||
})
|
||||
}
|
||||
|
||||
async enqueue(buf: ArrayBuffer) {
|
||||
|
||||
@@ -18,7 +18,25 @@ export interface ChatMsg { role: "user" | "assistant"; text: string }
|
||||
const SYSTEM_PROMPT =
|
||||
"Du sprichst per Sprache mit dem Nutzer. Antworte natürlich, freundlich und KNAPP in ganzen, " +
|
||||
"gut vorlesbaren Sätzen. Kein Markdown, keine Codeblöcke, keine Aufzählungszeichen, keine Emojis — " +
|
||||
"reiner Fließtext, den man laut vorlesen kann."
|
||||
"reiner Fließtext, den man laut vorlesen kann. Verwende IMMER echte deutsche Umlaute (ä, ö, ü, ß) " +
|
||||
"und NIEMALS Umschreibungen wie ae, oe, ue oder ss. Nenne möglichst keine langen URLs oder Codebefehle."
|
||||
|
||||
// Entfernt, was nicht vorgelesen werden soll (Links, Markdown, Code, Emojis), bevor der Satz ans TTS geht.
|
||||
// Die Anzeige im Chat bleibt unangetastet — nur die gesprochene Fassung wird gesäubert.
|
||||
function cleanForTTS(s: string): string {
|
||||
return s
|
||||
.replace(/```[\s\S]*?```/g, " ") // Codeblöcke
|
||||
.replace(/`([^`]*)`/g, "$1") // Inline-Code
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") // [Text](url) → Text
|
||||
.replace(/https?:\/\/\S+/gi, " ") // URLs
|
||||
.replace(/www\.\S+/gi, " ")
|
||||
.replace(/\b\S+@\S+\.\S+\b/g, " ") // E-Mails
|
||||
.replace(/[*_#>~|`]+/g, " ") // Markdown-Zeichen
|
||||
.replace(/^\s*[-•·]\s+/gm, " ") // Listen-Bullets
|
||||
.replace(/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{2190}-\u{21FF}\u{2B00}-\u{2BFF}]/gu, "") // Emojis/Symbole
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
}
|
||||
|
||||
function getSessionId(): string {
|
||||
let id = localStorage.getItem("mc_voice_session")
|
||||
@@ -105,13 +123,14 @@ export function useVoiceAgent() {
|
||||
let spokeAny = false
|
||||
let ttsFailed = false
|
||||
const speak = (sentence: string) => {
|
||||
if (!sentence.trim()) return
|
||||
const spoken = cleanForTTS(sentence)
|
||||
if (!spoken) return // z.B. ein Satz, der nur aus einer URL bestand
|
||||
ttsChain = ttsChain.then(async () => {
|
||||
try {
|
||||
const r = await fetch("/api/voice/tts", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ text: sentence, engine, voice }),
|
||||
body: JSON.stringify({ text: spoken, engine, voice }),
|
||||
})
|
||||
if (!r.ok) throw new Error(`TTS ${r.status}`)
|
||||
await queue.enqueue(await r.arrayBuffer())
|
||||
|
||||
Reference in New Issue
Block a user