v2.3 — Character-Aussehen + Portrait-Fix

Features:
- Character-Bogen: Aussehen-Sektion mit Stil, Geschlecht, Alter, Größe,
  Statur, Haarfarbe, Frisur, Augenfarbe, Haut, Kleidung, Merkmale
- Stil-Optionen: Anime, Realistisch, Comic, Cartoon, Pixel Art
- ComfyUI Prompt wird aus appearance-Objekt gebaut
- Portrait-Button: Async mit Polling (kein Timeout mehr)
- Portrait-Button: 5-Minuten Timeout als Fallback
- Poll-Errors werden ignoriert (kein vorzeitiger Abbruch)
- Mara.json aktualisiert mit appearance-Objekt
This commit is contained in:
arch_agent
2026-07-24 22:04:13 +02:00
parent b916792082
commit 7755d2be64
6 changed files with 238 additions and 41 deletions
+39 -7
View File
@@ -315,16 +315,48 @@
body: JSON.stringify({})
});
const data = await resp.json();
if (resp.ok && data.image) {
// Update avatar in header
const avatarEl = document.querySelector('.avatar');
avatarEl.innerHTML = `<img src="${data.image}?t=${Date.now()}" style="width: 100%; height: 100%; border-radius: 50%; object-fit: cover;">`;
btn.textContent = '✅ Fertig!';
setTimeout(() => { btn.textContent = oldText; btn.disabled = false; }, 2000);
} else {
if (!resp.ok) {
alert(data.error || 'Generierung fehlgeschlagen');
btn.textContent = oldText;
btn.disabled = false;
return;
}
if (data.job_id) {
const jobId = data.job_id;
const pollInterval = setInterval(async () => {
try {
const statusResp = await fetch(`/api/portrait/status/${jobId}`);
const statusData = await statusResp.json();
if (statusData.status === 'done' && statusData.image) {
clearInterval(pollInterval);
const avatarEl = document.querySelector('.avatar');
avatarEl.innerHTML = `<img src="${statusData.image}?t=${Date.now()}" style="width: 100%; height: 100%; border-radius: 50%; object-fit: cover;">`;
btn.textContent = '✅ Fertig!';
setTimeout(() => { btn.textContent = oldText; btn.disabled = false; }, 2000);
} else if (statusData.status === 'error') {
clearInterval(pollInterval);
alert(statusData.error || 'Generierung fehlgeschlagen');
btn.textContent = oldText;
btn.disabled = false;
}
} catch(e) {
// ignore poll errors, keep trying
}
}, 3000);
// Timeout after 5 minutes
setTimeout(() => {
clearInterval(pollInterval);
btn.textContent = oldText;
btn.disabled = false;
}, 300000);
} else if (data.error) {
alert(data.error);
btn.textContent = oldText;
btn.disabled = false;
}
} catch(e) {
alert('Verbindungsfehler: ' + e.message);
+89 -18
View File
@@ -7,7 +7,7 @@
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', system-ui, sans-serif; background: #0a0a0f; color: #e0e0e8; min-height: 100vh; }
.container { max-width: 700px; margin: 0 auto; padding: 30px 20px; }
.container { max-width: 750px; margin: 0 auto; padding: 30px 20px; }
h1 { font-size: 1.8rem; margin-bottom: 6px; background: linear-gradient(135deg, #ff006e, #8338ec); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.subtitle { color: #6b6b80; margin-bottom: 24px; font-size: 0.9rem; }
.form-group { margin-bottom: 16px; }
@@ -22,14 +22,20 @@
.btn:hover { opacity: 0.9; }
.btn-back { background: transparent; border: 1px solid #252535; color: #6b6b80; margin-right: 10px; }
#models { margin-top: 4px; }
.section-header { font-size: 1rem; font-weight: 700; color: #8338ec; margin: 24px 0 12px; padding-bottom: 8px; border-bottom: 1px solid #252535; }
.appearance-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; }
.appearance-grid .form-group { margin-bottom: 12px; }
@media (max-width: 600px) { .appearance-grid { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<div class="container">
<h1>✦ Neuer Charakter</h1>
<p class="subtitle">Definiere Persönlichkeit, Verhalten und Hintergrund. Der Bot bleibt immer in Rolle.</p>
<p class="subtitle">Definiere Persönlichkeit, Verhalten, Hintergrund und Aussehen.</p>
<form id="createForm">
<div class="section-header">Basis</div>
<div class="row">
<div class="form-group">
<label>Name</label>
@@ -40,50 +46,103 @@
<input type="text" id="avatar" value="👤" maxlength="4" style="text-align: center; font-size: 1.5rem;">
</div>
</div>
<div class="form-group">
<label>Kurze Beschreibung <span class="hint">(für die Übersicht)</span></label>
<input type="text" id="description" placeholder="z.B. Barista, 28, zynisch aber warmherzig">
</div>
<div class="section-header">Persönlichkeit</div>
<div class="form-group">
<label>Persönlichkeit <span class="hint">(wie ist der Charakter?)</span></label>
<textarea id="personality" placeholder="z.B. Trocken, sarkastisch, aber loyal. Hilfsbereit auf ihre Art. Zeigt Gefühle selten direkt."></textarea>
<textarea id="personality" placeholder="z.B. Trocken, sarkastisch, aber loyal."></textarea>
</div>
<div class="form-group">
<label>Hintergrund <span class="hint">(Wer ist der Charakter?)</span></label>
<textarea id="background" placeholder="z.B. Arbeitet seit 5 Jahren in einem Café. Hat mal Kunst studiert, abgebrochen. Lebt allein mit einer Katze."></textarea>
<textarea id="background" placeholder="z.B. Arbeitet seit 5 Jahren in einem Café..."></textarea>
</div>
<div class="form-group">
<label>Verhalten <span class="hint">(wie verhält sich der Charakter?)</span></label>
<textarea id="behavior" placeholder="z.B. Flucht in Wortwitze wenn nervös. Klopft auf den Tresen. Unterbricht nicht. Stellt Gegenfragen."></textarea>
<textarea id="behavior" placeholder="z.B. Flucht in Wortwitze wenn nervös."></textarea>
</div>
<div class="form-group">
<label>Sprachstil <span class="hint">(wie spricht der Charakter?)</span></label>
<textarea id="tone" placeholder="z.B. Umgangssprachlich, 'ne' statt 'nicht', 'mal' statt 'einmal', flucht gelegentlich, kurze Sätze"></textarea>
<textarea id="tone" placeholder="z.B. Umgangssprachlich, 'ne' statt 'nicht'..."></textarea>
</div>
<div class="form-group">
<label>Szenario <span class="hint">(optional — wo spielt das Gespräch?)</span></label>
<input type="text" id="scenario" placeholder="z.B. Du sitzt in einem kleinen Café, es regnet draußen.">
<input type="text" id="scenario" placeholder="z.B. Du sitzt in einem kleinen Café...">
</div>
<div class="form-group">
<label>NSFW / Soziale Regeln <span class="hint">(optional)</span></label>
<textarea id="nsfw" placeholder="z.B. Keine Einschränkungen. Kann über alles sprechen. Flirten erwünscht."></textarea>
<textarea id="nsfw" placeholder="z.B. Keine Einschränkungen."></textarea>
</div>
<div class="form-group">
<label>Begrüßung <span class="hint">(erste Nachricht vom Bot)</span></label>
<textarea id="greeting" placeholder="z.B. *schaut auf als du das Café betrittst* Na, wieder da? Der übliche?"></textarea>
<textarea id="greeting" placeholder="z.B. *schaut auf* Na, wieder da?"></textarea>
</div>
<div class="section-header">Aussehen <span class="hint" style="font-weight:400;">(für Portrait-Generierung)</span></div>
<div class="appearance-grid">
<div class="form-group">
<label>Stil</label>
<select id="app_style">
<option value="anime">Anime</option>
<option value="realistic">Realistisch</option>
<option value="comic">Comic</option>
<option value="cartoon">Cartoon</option>
<option value="pixel">Pixel Art</option>
</select>
</div>
<div class="form-group">
<label>Geschlecht</label>
<select id="app_gender">
<option value="female">Weiblich</option>
<option value="male">Männlich</option>
<option value="other">Divers</option>
</select>
</div>
<div class="form-group">
<label>Alter</label>
<input type="text" id="app_age" placeholder="z.B. 28">
</div>
<div class="form-group">
<label>Größe</label>
<input type="text" id="app_height" placeholder="z.B. 168cm">
</div>
<div class="form-group">
<label>Statur</label>
<input type="text" id="app_build" placeholder="z.B. schlank, athletisch">
</div>
<div class="form-group">
<label>Haarfarbe</label>
<input type="text" id="app_hair_color" placeholder="z.B. dunkelbraun">
</div>
<div class="form-group">
<label>Frisur</label>
<input type="text" id="app_hair_style" placeholder="z.B. kurz, ungeordnet">
</div>
<div class="form-group">
<label>Augenfarbe</label>
<input type="text" id="app_eye_color" placeholder="z.B. braun">
</div>
<div class="form-group">
<label>Haut</label>
<input type="text" id="app_skin" placeholder="z.B. hell, gebräunt">
</div>
</div>
<div class="form-group">
<label>Kleidung</label>
<input type="text" id="app_clothing" placeholder="z.B. Café-Schürze, schwarzes T-Shirt, Jeans">
</div>
<div class="form-group">
<label>Besondere Merkmale</label>
<input type="text" id="app_distinctive" placeholder="z.B. müde Augen, Augenringe, Kaugummi">
</div>
<div class="section-header">Modell</div>
<div class="row">
<div class="form-group">
<label>Modell <span class="hint">(Ollama)</span></label>
<label>Ollama Modell</label>
<select id="model">
<option value="">Default</option>
</select>
@@ -103,7 +162,6 @@
</div>
<script>
// Load models
fetch('/api/models').then(r => r.json()).then(data => {
const select = document.getElementById('model');
if (data.models) {
@@ -131,6 +189,19 @@
greeting: document.getElementById('greeting').value,
model: document.getElementById('model').value,
temperature: parseFloat(document.getElementById('temperature').value),
appearance: {
style: document.getElementById('app_style').value,
gender: document.getElementById('app_gender').value,
age: document.getElementById('app_age').value,
height: document.getElementById('app_height').value,
build: document.getElementById('app_build').value,
hair_color: document.getElementById('app_hair_color').value,
hair_style: document.getElementById('app_hair_style').value,
eye_color: document.getElementById('app_eye_color').value,
skin: document.getElementById('app_skin').value,
clothing: document.getElementById('app_clothing').value,
distinctive: document.getElementById('app_distinctive').value,
}
};
const resp = await fetch('/api/character/create', {
method: 'POST',