fix: QC-Ratings aus 7 älteren Artikeln entfernt (CHECK 10)
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>Basti Voice Chat</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #0f0f14;
|
||||
color: #e0e0e0;
|
||||
height: 100dvh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.header {
|
||||
background: #1a1a2e;
|
||||
padding: 12px 16px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #2a2a3e;
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
}
|
||||
.header strong { color: #7c8aff; }
|
||||
#messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.msg {
|
||||
max-width: 85%;
|
||||
padding: 12px 16px;
|
||||
border-radius: 18px;
|
||||
line-height: 1.5;
|
||||
font-size: 15px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.msg.user {
|
||||
align-self: flex-end;
|
||||
background: #7c8aff;
|
||||
color: #fff;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
.msg.assistant {
|
||||
align-self: flex-start;
|
||||
background: #2a2a3e;
|
||||
color: #e0e0e0;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
.msg.status {
|
||||
align-self: center;
|
||||
background: transparent;
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
font-style: italic;
|
||||
padding: 4px;
|
||||
}
|
||||
.bottom {
|
||||
background: #1a1a2e;
|
||||
padding: 20px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border-top: 1px solid #2a2a3e;
|
||||
padding-bottom: max(20px, env(safe-area-inset-bottom));
|
||||
}
|
||||
#micBtn {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: #7c8aff;
|
||||
color: white;
|
||||
font-size: 36px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 20px rgba(124,138,255,0.3);
|
||||
}
|
||||
#micBtn:active { transform: scale(0.95); }
|
||||
#micBtn.listening {
|
||||
background: #ff4d6a;
|
||||
animation: pulse 1.5s infinite;
|
||||
box-shadow: 0 4px 30px rgba(255,77,106,0.5);
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(255,77,106,0.5); }
|
||||
50% { box-shadow: 0 0 0 20px rgba(255,77,106,0); }
|
||||
}
|
||||
#micBtn.thinking { background: #ffa726; box-shadow: 0 4px 20px rgba(255,167,38,0.3); }
|
||||
#statusText { font-size: 13px; color: #888; text-align: center; min-height: 18px; }
|
||||
.settings-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
#textInput {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #2a2a3e;
|
||||
background: #0f0f14;
|
||||
color: #e0e0e0;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
}
|
||||
#textInput:focus { border-color: #7c8aff; }
|
||||
#sendBtn {
|
||||
padding: 8px 20px;
|
||||
border-radius: 20px;
|
||||
border: none;
|
||||
background: #2a2a3e;
|
||||
color: #e0e0e0;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
#sendBtn:hover { background: #3a3a4e; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="header">🗣️ <strong>Basti</strong> Voice Chat</div>
|
||||
|
||||
<div id="messages">
|
||||
<div class="msg assistant">Hey Daniel! Drück den 🎤-Button und sprich mit mir. Ich höre zu und antworte sofort.</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom">
|
||||
<div id="statusText">Bereit</div>
|
||||
<button id="micBtn" onclick="toggleMic()" title="Zum Sprechen drücken">🎤</button>
|
||||
<div style="width:100%;max-width:300px;display:flex;gap:8px">
|
||||
<input type="text" id="textInput" placeholder="Oder tippen..." onkeydown="if(event.key==='Enter')sendText()">
|
||||
<button id="sendBtn" onclick="sendText()">Senden</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API = '/api/chat';
|
||||
let recognition = null;
|
||||
let isListening = false;
|
||||
let isThinking = false;
|
||||
|
||||
function initSpeech() {
|
||||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
if (!SpeechRecognition) return false;
|
||||
recognition = new SpeechRecognition();
|
||||
recognition.lang = 'de-DE';
|
||||
recognition.interimResults = false;
|
||||
recognition.continuous = false;
|
||||
recognition.maxAlternatives = 1;
|
||||
recognition.onresult = (e) => {
|
||||
const text = e.results[0][0].transcript.trim();
|
||||
if (text) { addMessage(text, 'user'); sendToBasti(text); }
|
||||
};
|
||||
recognition.onerror = (e) => {
|
||||
console.log('Speech error:', e.error);
|
||||
if (e.error === 'no-speech') {
|
||||
setStatus('Nichts gehört — nochmal?');
|
||||
} else if (e.error === 'not-allowed') {
|
||||
setStatus('Mikrofon-Zugriff verweigert');
|
||||
} else {
|
||||
setStatus('Fehler: ' + e.error);
|
||||
}
|
||||
stopListening();
|
||||
};
|
||||
recognition.onend = () => {
|
||||
if (isListening && !isThinking) {
|
||||
// Restart if still listening (continuous mode workaround)
|
||||
try { recognition.start(); } catch(e) {}
|
||||
} else {
|
||||
stopListening();
|
||||
}
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
function toggleMic() {
|
||||
if (isThinking) return;
|
||||
if (isListening) { stopListening(); return; }
|
||||
if (!recognition && !initSpeech()) {
|
||||
setStatus('Speech API nicht verfügbar — bitte tippen');
|
||||
return;
|
||||
}
|
||||
startListening();
|
||||
}
|
||||
|
||||
function startListening() {
|
||||
isListening = true;
|
||||
document.getElementById('micBtn').classList.add('listening');
|
||||
document.getElementById('micBtn').textContent = '🔴';
|
||||
setStatus('Höre zu...');
|
||||
try { recognition.start(); } catch(e) {}
|
||||
}
|
||||
|
||||
function stopListening() {
|
||||
isListening = false;
|
||||
document.getElementById('micBtn').classList.remove('listening', 'thinking');
|
||||
document.getElementById('micBtn').textContent = '🎤';
|
||||
if (!isThinking) setStatus('Bereit');
|
||||
try { recognition.stop(); } catch(e) {}
|
||||
}
|
||||
|
||||
function setStatus(text) {
|
||||
document.getElementById('statusText').textContent = text;
|
||||
}
|
||||
|
||||
function addMessage(text, role) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'msg ' + role;
|
||||
div.textContent = text;
|
||||
if (role === 'status') div.className = 'msg status';
|
||||
document.getElementById('messages').appendChild(div);
|
||||
document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight;
|
||||
}
|
||||
|
||||
async function sendToBasti(text) {
|
||||
isThinking = true;
|
||||
stopListening();
|
||||
document.getElementById('micBtn').classList.add('thinking');
|
||||
document.getElementById('micBtn').textContent = '⏳';
|
||||
setStatus('Basti denkt nach...');
|
||||
|
||||
try {
|
||||
const resp = await fetch(API, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text })
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
throw new Error(err.detail || err.error || 'API-Fehler');
|
||||
}
|
||||
|
||||
const data = await resp.json();
|
||||
const reply = data.response || data.text || JSON.stringify(data);
|
||||
addMessage(reply, 'assistant');
|
||||
speak(reply);
|
||||
|
||||
// Also save to session
|
||||
fetch(API + '/save', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ user: text, assistant: reply })
|
||||
}).catch(() => {});
|
||||
|
||||
} catch(e) {
|
||||
addMessage('⚠️ Fehler: ' + e.message, 'status');
|
||||
} finally {
|
||||
isThinking = false;
|
||||
document.getElementById('micBtn').classList.remove('thinking');
|
||||
document.getElementById('micBtn').textContent = '🎤';
|
||||
setStatus('Bereit');
|
||||
}
|
||||
}
|
||||
|
||||
function speak(text) {
|
||||
if (!window.speechSynthesis) return;
|
||||
// Cancel any ongoing speech
|
||||
speechSynthesis.cancel();
|
||||
const u = new SpeechSynthesisUtterance(text);
|
||||
u.lang = 'en-US';
|
||||
u.rate = 1.0;
|
||||
u.pitch = 1.0;
|
||||
// Try to use a good English voice
|
||||
const voices = speechSynthesis.getVoices();
|
||||
const preferred = voices.find(v => v.lang.startsWith('en') && v.name.includes('Google') || v.name.includes('Samantha') || v.name.includes('Daniel'));
|
||||
if (preferred) u.voice = preferred;
|
||||
speechSynthesis.speak(u);
|
||||
}
|
||||
|
||||
function sendText() {
|
||||
const input = document.getElementById('textInput');
|
||||
const text = input.value.trim();
|
||||
if (!text || isThinking) return;
|
||||
input.value = '';
|
||||
addMessage(text, 'user');
|
||||
sendToBasti(text);
|
||||
}
|
||||
|
||||
// Preload voices
|
||||
if (window.speechSynthesis) {
|
||||
speechSynthesis.getVoices();
|
||||
speechSynthesis.onvoiceschanged = () => speechSynthesis.getVoices();
|
||||
}
|
||||
|
||||
// Handle visibility changes to stop mic when switching apps
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden && isListening) stopListening();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Basti Voice Chat Server — serves the web UI and proxies to Hermes API."""
|
||||
|
||||
import http.server
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import uuid
|
||||
import socketserver
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
PORT = int(os.environ.get("VOICE_CHAT_PORT", "8891"))
|
||||
API_PORT = 8642
|
||||
API_KEY = "866c20bd9c34d92bd34f1bf21cff6f52c40840da8ec6c562afac22dfc3f9ac8e"
|
||||
STATIC_DIR = Path(__file__).parent
|
||||
|
||||
# Session history (in-memory, simple)
|
||||
sessions = {}
|
||||
|
||||
class VoiceChatHandler(http.server.SimpleHTTPRequestHandler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, directory=str(STATIC_DIR), **kwargs)
|
||||
|
||||
def do_POST(self):
|
||||
if self.path == "/api/chat":
|
||||
self.handle_chat()
|
||||
elif self.path == "/api/chat/save":
|
||||
self.handle_save()
|
||||
else:
|
||||
self.send_error(404)
|
||||
|
||||
def handle_chat(self):
|
||||
content_len = int(self.headers.get("Content-Length", 0))
|
||||
body = self.rfile.read(content_len)
|
||||
try:
|
||||
data = json.loads(body)
|
||||
user_text = data.get("text", "").strip()
|
||||
except:
|
||||
self.send_error(400, "Invalid JSON")
|
||||
return
|
||||
|
||||
if not user_text:
|
||||
self.send_error(400, "Missing 'text' field")
|
||||
return
|
||||
|
||||
# Forward to Hermes API server
|
||||
api_url = f"http://localhost:{API_PORT}/v1/chat/completions"
|
||||
api_body = json.dumps({
|
||||
"model": "hermes",
|
||||
"messages": [
|
||||
{"role": "system", "content": "Du bist Basti, Chef vom Dienst der digitalen Redaktion von brettspiel-news.de. Antworte auf Deutsch, freundlich und hilfreich. Halte deine Antworten kurz — maximal 3-4 Sätze. Dein Chef Daniel spricht mit dir."},
|
||||
{"role": "user", "content": user_text}
|
||||
],
|
||||
"max_tokens": 300,
|
||||
"temperature": 0.7
|
||||
}).encode("utf-8")
|
||||
|
||||
req = urllib.request.Request(api_url, data=api_body, headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {API_KEY}"
|
||||
})
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=45) as resp:
|
||||
result = json.loads(resp.read().decode())
|
||||
reply = result.get("choices", [{}])[0].get("message", {}).get("content", "")
|
||||
if not reply:
|
||||
reply = "Hmm, da kam nichts zurück. Versuch's nochmal?"
|
||||
except urllib.error.HTTPError as e:
|
||||
err_body = e.read().decode() if e.fp else str(e)
|
||||
reply = f"API-Fehler {e.code}: {err_body[:200]}"
|
||||
except Exception as e:
|
||||
reply = f"Server-Fehler: {str(e)[:200]}"
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"response": reply}).encode())
|
||||
|
||||
def handle_save(self):
|
||||
"""Save message pair for session continuity."""
|
||||
content_len = int(self.headers.get("Content-Length", 0))
|
||||
body = self.rfile.read(content_len)
|
||||
try:
|
||||
data = json.loads(body)
|
||||
except:
|
||||
self.send_error(400)
|
||||
return
|
||||
|
||||
# Simple append to history (not used by API currently)
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"ok": True}).encode())
|
||||
|
||||
def do_OPTIONS(self):
|
||||
self.send_response(200)
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.send_header("Access-Control-Allow-Methods", "POST, OPTIONS")
|
||||
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
||||
self.end_headers()
|
||||
|
||||
def log_message(self, format, *args):
|
||||
# Quiet logging
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with socketserver.TCPServer(("0.0.0.0", PORT), VoiceChatHandler) as httpd:
|
||||
print(f"🎤 Basti Voice Chat running on http://0.0.0.0:{PORT}")
|
||||
print(f" External: http://185.162.249.159:{PORT}")
|
||||
sys.stdout.flush()
|
||||
httpd.serve_forever()
|
||||
Reference in New Issue
Block a user