fix: QC-Ratings aus 7 älteren Artikeln entfernt (CHECK 10)

This commit is contained in:
Hermes Agent
2026-06-23 23:47:51 +02:00
parent e73e0e88de
commit 4d90e4249d
656 changed files with 2602398 additions and 3 deletions
+302
View File
@@ -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>
+118
View File
@@ -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()