Spaces:
Running
Running
Add suspect profile image prompts and metadata for rendering
Browse files- TODO.md +3 -1
- prompts/image_generation.md +36 -0
- ui/static/assets/suspects/metadata.json +15 -0
- ui/static/js/game_logic.js +56 -4
TODO.md
CHANGED
|
@@ -5,13 +5,15 @@
|
|
| 5 |
- players can't know there's more suspects down the list as we removed the scroll bar, can you scroll a bit down by default to show there's more cards down
|
| 6 |
- find the texture missing
|
| 7 |
- typing effect
|
|
|
|
| 8 |
|
| 9 |
|
| 10 |
# Doing
|
|
|
|
|
|
|
| 11 |
- mode selection
|
| 12 |
- numbered steps to launch game
|
| 13 |
|
| 14 |
-
- pre-generate suspects cartoony potraits
|
| 15 |
- sound/music
|
| 16 |
- eleven labs tts/voice
|
| 17 |
- video embeded
|
|
|
|
| 5 |
- players can't know there's more suspects down the list as we removed the scroll bar, can you scroll a bit down by default to show there's more cards down
|
| 6 |
- find the texture missing
|
| 7 |
- typing effect
|
| 8 |
+
- pre-generate suspects cartoony potraits
|
| 9 |
|
| 10 |
|
| 11 |
# Doing
|
| 12 |
+
- bottom gradio box to see internal MCP tools calls out/in
|
| 13 |
+
- add ai
|
| 14 |
- mode selection
|
| 15 |
- numbered steps to launch game
|
| 16 |
|
|
|
|
| 17 |
- sound/music
|
| 18 |
- eleven labs tts/voice
|
| 19 |
- video embeded
|
prompts/image_generation.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Suspect Profile Image Prompts
|
| 2 |
+
## Model: Nano Banana (or standard SDXL)
|
| 3 |
+
## Style: Noir Graphic Novel, Digital Art, Semi-Realistic Game Asset
|
| 4 |
+
## Note: Square aspect ratio (1:1). NO polaroid frames in the image itself.
|
| 5 |
+
|
| 6 |
+
Use these prompts to generate profile images. Replace `{gender}` with "man" or "woman".
|
| 7 |
+
|
| 8 |
+
### 1. Executive (Archetype: "executive")
|
| 9 |
+
> **Prompt:**
|
| 10 |
+
> close up portrait of a {gender} corporate executive, sharp business suit, looking tired and stressed, office background, noir graphic novel style, digital painting, high contrast, dramatic lighting, detailed face, 8k, no frame
|
| 11 |
+
|
| 12 |
+
### 2. Worker (Archetype: "worker")
|
| 13 |
+
> **Prompt:**
|
| 14 |
+
> close up portrait of a {gender} blue collar worker, janitor uniform or apron, rough skin texture, suspicious eyes, dark alley or kitchen background, noir style, gritty, sepia tone, shadow over face, digital art, game asset, no frame
|
| 15 |
+
|
| 16 |
+
### 3. Socialite (Archetype: "socialite")
|
| 17 |
+
> **Prompt:**
|
| 18 |
+
> close up portrait of a wealthy {gender} socialite, elegant evening wear, pearl necklace or tuxedo, arrogant expression, luxury interior background, noir style, dramatic shadows, vintage aesthetic, digital painting, 8k, no frame
|
| 19 |
+
|
| 20 |
+
### 4. Artist (Archetype: "artist")
|
| 21 |
+
> **Prompt:**
|
| 22 |
+
> close up portrait of a {gender} artist, bohemian style, messy hair, scarf, intense stare, art studio background, noir graphic novel style, high contrast, black and white with red accent, digital art, no frame
|
| 23 |
+
|
| 24 |
+
### 5. Criminal / Suspect (Archetype: "criminal")
|
| 25 |
+
> **Prompt:**
|
| 26 |
+
> close up portrait of a {gender} suspect, hoodie or leather jacket, face partially shadowed, street background, noir detective style, gritty, mystery game character, digital painting, 8k, no frame
|
| 27 |
+
|
| 28 |
+
### 6. Detective / Private Eye (Archetype: "detective")
|
| 29 |
+
> **Prompt:**
|
| 30 |
+
> close up portrait of a {gender} detective, trench coat, fedora, rainy background, noir style, gritty, cynical expression, digital art, game asset, high contrast, no frame
|
| 31 |
+
|
| 32 |
+
---
|
| 33 |
+
**Instructions:**
|
| 34 |
+
1. Generate 2-3 variations for each archetype (Male and Female).
|
| 35 |
+
2. Save as `suspect_1.jpg`, `suspect_2.jpg`, etc.
|
| 36 |
+
3. Update `metadata.json` with the filename, gender, and archetype.
|
ui/static/assets/suspects/metadata.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{"id": "suspect_10.png", "gender": "male", "archetype": "detective"},
|
| 3 |
+
{"id": "suspect_11.png", "gender": "female", "archetype": "socialite"},
|
| 4 |
+
{"id": "suspect_9.png", "gender": "male", "archetype": "socialite"},
|
| 5 |
+
{"id": "suspect_3.png", "gender": "female", "archetype": "executive"},
|
| 6 |
+
{"id": "suspect_5.png", "gender": "male", "archetype": "executive"},
|
| 7 |
+
{"id": "suspect_4.png", "gender": "female", "archetype": "artist"},
|
| 8 |
+
{"id": "suspect_1.png", "gender": "male", "archetype": "artist"},
|
| 9 |
+
{"id": "suspect_7.png", "gender": "male", "archetype": "worker"},
|
| 10 |
+
{"id": "suspect_2.png", "gender": "female", "archetype": "worker"},
|
| 11 |
+
{"id": "suspect_6.png", "gender": "male", "archetype": "criminal"},
|
| 12 |
+
{"id": "suspect_8.png", "gender": "female", "archetype": "criminal"},
|
| 13 |
+
{"id": "suspect_12.png", "gender": "female", "archetype": "customer"},
|
| 14 |
+
{"id": "suspect_13.png", "gender": "male", "archetype": "customer"}
|
| 15 |
+
]
|
ui/static/js/game_logic.js
CHANGED
|
@@ -11,7 +11,8 @@ let gameState = {
|
|
| 11 |
nextEvidenceSlot: 0, // Track grid position for new cards
|
| 12 |
availableCameras: [],
|
| 13 |
dnaMap: {},
|
| 14 |
-
unlockedEvidence: []
|
|
|
|
| 15 |
};
|
| 16 |
|
| 17 |
// --- Bridge: Communication with Parent (Python/Gradio) ---
|
|
@@ -114,7 +115,19 @@ function updateStatus(data) {
|
|
| 114 |
|
| 115 |
function initializeGame(data) {
|
| 116 |
gameState = data;
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
document.getElementById('loading-overlay').style.opacity = 0;
|
| 119 |
setTimeout(() => {
|
| 120 |
document.getElementById('loading-overlay').style.display = 'none';
|
|
@@ -244,7 +257,7 @@ function renderSuspects() {
|
|
| 244 |
|
| 245 |
if (!gameState.scenario || !gameState.scenario.suspects) return;
|
| 246 |
|
| 247 |
-
gameState.scenario.suspects.forEach(suspect => {
|
| 248 |
const card = document.createElement('div');
|
| 249 |
card.className = 'suspect-card';
|
| 250 |
card.dataset.id = suspect.id;
|
|
@@ -253,8 +266,47 @@ function renderSuspects() {
|
|
| 253 |
// Placeholder Avatar (Initials)
|
| 254 |
const initials = suspect.name.split(' ').map(n => n[0]).join('');
|
| 255 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
card.innerHTML = `
|
| 257 |
-
<div class="suspect-img"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
<div class="suspect-name">${suspect.name}</div>
|
| 259 |
<div class="suspect-role">${suspect.role}</div>
|
| 260 |
<div class="suspect-phone" title="Click to copy" onclick="event.stopPropagation(); copyToClipboard('${suspect.phone_number}')">
|
|
|
|
| 11 |
nextEvidenceSlot: 0, // Track grid position for new cards
|
| 12 |
availableCameras: [],
|
| 13 |
dnaMap: {},
|
| 14 |
+
unlockedEvidence: [],
|
| 15 |
+
imageMetadata: []
|
| 16 |
};
|
| 17 |
|
| 18 |
// --- Bridge: Communication with Parent (Python/Gradio) ---
|
|
|
|
| 115 |
|
| 116 |
function initializeGame(data) {
|
| 117 |
gameState = data;
|
| 118 |
+
|
| 119 |
+
// Fetch image metadata then render
|
| 120 |
+
fetch('/static/assets/suspects/metadata.json')
|
| 121 |
+
.then(res => res.json())
|
| 122 |
+
.then(metadata => {
|
| 123 |
+
gameState.imageMetadata = metadata;
|
| 124 |
+
renderSuspects();
|
| 125 |
+
})
|
| 126 |
+
.catch(err => {
|
| 127 |
+
console.error("Failed to load image metadata:", err);
|
| 128 |
+
renderSuspects(); // Fallback
|
| 129 |
+
});
|
| 130 |
+
|
| 131 |
document.getElementById('loading-overlay').style.opacity = 0;
|
| 132 |
setTimeout(() => {
|
| 133 |
document.getElementById('loading-overlay').style.display = 'none';
|
|
|
|
| 257 |
|
| 258 |
if (!gameState.scenario || !gameState.scenario.suspects) return;
|
| 259 |
|
| 260 |
+
gameState.scenario.suspects.forEach((suspect, index) => {
|
| 261 |
const card = document.createElement('div');
|
| 262 |
card.className = 'suspect-card';
|
| 263 |
card.dataset.id = suspect.id;
|
|
|
|
| 266 |
// Placeholder Avatar (Initials)
|
| 267 |
const initials = suspect.name.split(' ').map(n => n[0]).join('');
|
| 268 |
|
| 269 |
+
// Select Image based on Gender & Archetype
|
| 270 |
+
let imgFilename = `suspect_${(index % 8) + 1}.jpg`; // Default fallback
|
| 271 |
+
|
| 272 |
+
if (gameState.imageMetadata && gameState.imageMetadata.length > 0 && suspect.gender) {
|
| 273 |
+
// 1. Filter by Gender
|
| 274 |
+
const genderMatches = gameState.imageMetadata.filter(img => img.gender === suspect.gender.toLowerCase());
|
| 275 |
+
|
| 276 |
+
if (genderMatches.length > 0) {
|
| 277 |
+
// 2. Try to match Archetype
|
| 278 |
+
const role = suspect.role.toLowerCase();
|
| 279 |
+
let targetArchetype = "detective"; // default
|
| 280 |
+
|
| 281 |
+
if (role.includes("ceo") || role.includes("cfo") || role.includes("manager") || role.includes("dealer")) targetArchetype = "executive";
|
| 282 |
+
else if (role.includes("janitor") || role.includes("chef") || role.includes("caterer")) targetArchetype = "worker";
|
| 283 |
+
else if (role.includes("artist") || role.includes("curator")) targetArchetype = "artist";
|
| 284 |
+
else if (role.includes("heir") || role.includes("collector") || role.includes("sister") || role.includes("socialite")) targetArchetype = "socialite";
|
| 285 |
+
else if (role.includes("ex-")) targetArchetype = "criminal";
|
| 286 |
+
else if (role.includes("customer")) targetArchetype = "customer"; // Rough/casual look
|
| 287 |
+
|
| 288 |
+
const archetypeMatches = genderMatches.filter(img => img.archetype === targetArchetype);
|
| 289 |
+
|
| 290 |
+
// Use archetype matches if available, otherwise use all gender matches
|
| 291 |
+
const pool = archetypeMatches.length > 0 ? archetypeMatches : genderMatches;
|
| 292 |
+
|
| 293 |
+
// Deterministic pick based on name
|
| 294 |
+
let hash = 0;
|
| 295 |
+
for (let i = 0; i < suspect.name.length; i++) {
|
| 296 |
+
hash = ((hash << 5) - hash) + suspect.name.charCodeAt(i);
|
| 297 |
+
hash |= 0;
|
| 298 |
+
}
|
| 299 |
+
const selected = pool[Math.abs(hash) % pool.length];
|
| 300 |
+
imgFilename = selected.id;
|
| 301 |
+
}
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
card.innerHTML = `
|
| 305 |
+
<div class="suspect-img">
|
| 306 |
+
<img src="/static/assets/suspects/${imgFilename}"
|
| 307 |
+
style="width:100%; height:100%; object-fit:cover; display:block;"
|
| 308 |
+
onerror="this.style.display='none'; this.parentElement.innerText='${initials}'; this.parentElement.style.display='flex'; this.parentElement.style.alignItems='center'; this.parentElement.style.justifyContent='center';">
|
| 309 |
+
</div>
|
| 310 |
<div class="suspect-name">${suspect.name}</div>
|
| 311 |
<div class="suspect-role">${suspect.role}</div>
|
| 312 |
<div class="suspect-phone" title="Click to copy" onclick="event.stopPropagation(); copyToClipboard('${suspect.phone_number}')">
|