update
Browse files- client/src/components/LoadingScreen.jsx +88 -0
- client/src/hooks/useGameSession.js +40 -0
- client/src/pages/Game.jsx +54 -53
- client/src/pages/Tutorial.jsx +13 -12
- client/src/utils/api.js +76 -19
- client/src/utils/session.js +12 -6
- server/.session_data.pkl +3 -0
- server/api/models.py +9 -1
- server/api/routes/chat.py +32 -3
- server/api/routes/universe.py +76 -0
- server/core/__init__.py +0 -11
- server/core/constants.py +1 -1
- server/core/game_logic.py +104 -17
- server/core/generators/__init__.py +0 -11
- server/core/generators/metadata_generator.py +1 -1
- server/core/generators/text_generator.py +90 -17
- server/core/generators/universe_generator.py +88 -0
- server/core/prompts/system.py +43 -43
- server/core/prompts/text_prompts.py +1 -1
- server/core/session_manager.py +35 -11
- server/core/state/__init__.py +0 -3
- server/core/story_generators.py +0 -223
- server/core/styles/comic_styles.json +0 -259
- server/core/styles/universe_styles.json +92 -0
- server/scripts/test_game.py +49 -5
- server/server.py +4 -0
client/src/components/LoadingScreen.jsx
ADDED
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState, useEffect } from "react";
|
2 |
+
import { Box, Typography } from "@mui/material";
|
3 |
+
import { motion } from "framer-motion";
|
4 |
+
import CreateIcon from "@mui/icons-material/Create";
|
5 |
+
import AutoStoriesIcon from "@mui/icons-material/AutoStories";
|
6 |
+
import PaletteIcon from "@mui/icons-material/Palette";
|
7 |
+
|
8 |
+
const defaultMessages = [
|
9 |
+
"waking up a sleepy AI...",
|
10 |
+
"teaching robots to tell bedtime stories...",
|
11 |
+
"bribing pixels to make pretty pictures...",
|
12 |
+
];
|
13 |
+
|
14 |
+
const iconMap = {
|
15 |
+
story: CreateIcon,
|
16 |
+
universe: AutoStoriesIcon,
|
17 |
+
art: PaletteIcon,
|
18 |
+
};
|
19 |
+
|
20 |
+
export const LoadingScreen = ({
|
21 |
+
messages = defaultMessages,
|
22 |
+
icon = "story",
|
23 |
+
messageInterval = 3000,
|
24 |
+
iconColor = "white",
|
25 |
+
textColor = "white",
|
26 |
+
}) => {
|
27 |
+
const [currentMessage, setCurrentMessage] = useState(0);
|
28 |
+
const IconComponent = iconMap[icon] || CreateIcon;
|
29 |
+
|
30 |
+
useEffect(() => {
|
31 |
+
const interval = setInterval(() => {
|
32 |
+
setCurrentMessage((prev) => (prev + 1) % messages.length);
|
33 |
+
}, messageInterval);
|
34 |
+
return () => clearInterval(interval);
|
35 |
+
}, [messages.length, messageInterval]);
|
36 |
+
|
37 |
+
return (
|
38 |
+
<Box
|
39 |
+
sx={{
|
40 |
+
position: "absolute",
|
41 |
+
top: "50%",
|
42 |
+
left: "50%",
|
43 |
+
transform: "translate(-50%, -50%)",
|
44 |
+
display: "flex",
|
45 |
+
flexDirection: "column",
|
46 |
+
alignItems: "center",
|
47 |
+
gap: 2,
|
48 |
+
}}
|
49 |
+
>
|
50 |
+
<motion.div
|
51 |
+
animate={{
|
52 |
+
y: [0, -10, 0],
|
53 |
+
}}
|
54 |
+
transition={{
|
55 |
+
duration: 2,
|
56 |
+
repeat: Infinity,
|
57 |
+
ease: "easeInOut",
|
58 |
+
}}
|
59 |
+
>
|
60 |
+
<IconComponent
|
61 |
+
sx={{
|
62 |
+
fontSize: 40,
|
63 |
+
color: iconColor,
|
64 |
+
opacity: 0.2,
|
65 |
+
}}
|
66 |
+
/>
|
67 |
+
</motion.div>
|
68 |
+
<motion.div
|
69 |
+
key={currentMessage}
|
70 |
+
initial={{ opacity: 0, y: 5 }}
|
71 |
+
animate={{ opacity: 1, y: 0 }}
|
72 |
+
exit={{ opacity: 0, y: -5 }}
|
73 |
+
transition={{ duration: 0.3 }}
|
74 |
+
>
|
75 |
+
<Typography
|
76 |
+
variant="body1"
|
77 |
+
sx={{
|
78 |
+
color: textColor,
|
79 |
+
opacity: 0.8,
|
80 |
+
fontStyle: "italic",
|
81 |
+
}}
|
82 |
+
>
|
83 |
+
{messages[currentMessage]}
|
84 |
+
</Typography>
|
85 |
+
</motion.div>
|
86 |
+
</Box>
|
87 |
+
);
|
88 |
+
};
|
client/src/hooks/useGameSession.js
ADDED
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useState, useEffect } from "react";
|
2 |
+
import { universeApi } from "../utils/api";
|
3 |
+
|
4 |
+
export const useGameSession = () => {
|
5 |
+
const [sessionId, setSessionId] = useState(null);
|
6 |
+
const [universe, setUniverse] = useState(null);
|
7 |
+
const [isLoading, setIsLoading] = useState(true);
|
8 |
+
const [error, setError] = useState(null);
|
9 |
+
|
10 |
+
useEffect(() => {
|
11 |
+
const initializeGame = async () => {
|
12 |
+
try {
|
13 |
+
setIsLoading(true);
|
14 |
+
const { session_id, base_story, style, genre, epoch } =
|
15 |
+
await universeApi.generate();
|
16 |
+
|
17 |
+
setSessionId(session_id);
|
18 |
+
setUniverse({
|
19 |
+
base_story,
|
20 |
+
style,
|
21 |
+
genre,
|
22 |
+
epoch,
|
23 |
+
});
|
24 |
+
} catch (err) {
|
25 |
+
setError(err.message || "Failed to initialize game session");
|
26 |
+
} finally {
|
27 |
+
setIsLoading(false);
|
28 |
+
}
|
29 |
+
};
|
30 |
+
|
31 |
+
initializeGame();
|
32 |
+
}, []);
|
33 |
+
|
34 |
+
return {
|
35 |
+
sessionId,
|
36 |
+
universe,
|
37 |
+
isLoading,
|
38 |
+
error,
|
39 |
+
};
|
40 |
+
};
|
client/src/pages/Game.jsx
CHANGED
@@ -14,6 +14,7 @@ import { useNarrator } from "../hooks/useNarrator";
|
|
14 |
import { useStoryCapture } from "../hooks/useStoryCapture";
|
15 |
import { usePageSound } from "../hooks/usePageSound";
|
16 |
import { useWritingSound } from "../hooks/useWritingSound";
|
|
|
17 |
import { StoryChoices } from "../components/StoryChoices";
|
18 |
import { ErrorDisplay } from "../components/ErrorDisplay";
|
19 |
import VolumeUpIcon from "@mui/icons-material/VolumeUp";
|
@@ -22,6 +23,7 @@ import PhotoCameraOutlinedIcon from "@mui/icons-material/PhotoCameraOutlined";
|
|
22 |
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
23 |
import CreateIcon from "@mui/icons-material/Create";
|
24 |
import { getNextLayoutType, LAYOUTS } from "../layouts/config";
|
|
|
25 |
|
26 |
// Constants
|
27 |
const SOUND_ENABLED_KEY = "sound_enabled";
|
@@ -67,16 +69,24 @@ export function Game() {
|
|
67 |
useNarrator(isSoundEnabled);
|
68 |
const playPageSound = usePageSound(isSoundEnabled);
|
69 |
const playWritingSound = useWritingSound(isSoundEnabled);
|
|
|
|
|
|
|
|
|
|
|
|
|
70 |
|
71 |
// Sauvegarder l'état du son dans le localStorage
|
72 |
useEffect(() => {
|
73 |
localStorage.setItem(SOUND_ENABLED_KEY, isSoundEnabled);
|
74 |
}, [isSoundEnabled]);
|
75 |
|
76 |
-
// Start the story
|
77 |
useEffect(() => {
|
78 |
-
|
79 |
-
|
|
|
|
|
80 |
|
81 |
// Add effect for message rotation
|
82 |
useEffect(() => {
|
@@ -128,18 +138,17 @@ export function Game() {
|
|
128 |
const handleStoryAction = async (action, choiceId = null) => {
|
129 |
setIsLoading(true);
|
130 |
setShowChoices(false);
|
131 |
-
setError(null);
|
132 |
try {
|
133 |
-
// Stop any ongoing narration
|
134 |
if (isNarratorSpeaking) {
|
135 |
stopNarration();
|
136 |
}
|
137 |
|
138 |
console.log("Starting story action:", action);
|
139 |
-
//
|
140 |
const storyData = await (action === "restart"
|
141 |
-
? storyApi.start()
|
142 |
-
: storyApi.makeChoice(choiceId));
|
143 |
|
144 |
if (!storyData) {
|
145 |
throw new Error("Pas de données reçues du serveur");
|
@@ -303,6 +312,33 @@ export function Game() {
|
|
303 |
await downloadStoryImage(storyContainerRef, `your-story-${Date.now()}.png`);
|
304 |
};
|
305 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
306 |
return (
|
307 |
<motion.div
|
308 |
initial={{ opacity: 0 }}
|
@@ -369,51 +405,16 @@ export function Game() {
|
|
369 |
) : (
|
370 |
<>
|
371 |
{isLoading && storySegments.length === 0 ? (
|
372 |
-
<
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
|
382 |
-
}}
|
383 |
-
>
|
384 |
-
<motion.div
|
385 |
-
animate={{
|
386 |
-
y: [0, -10, 0],
|
387 |
-
}}
|
388 |
-
transition={{
|
389 |
-
duration: 2,
|
390 |
-
repeat: Infinity,
|
391 |
-
ease: "easeInOut",
|
392 |
-
}}
|
393 |
-
>
|
394 |
-
<CreateIcon
|
395 |
-
sx={{ fontSize: 40, color: "white", opacity: 0.2 }}
|
396 |
-
/>
|
397 |
-
</motion.div>
|
398 |
-
<motion.div
|
399 |
-
key={loadingMessage}
|
400 |
-
initial={{ opacity: 0, y: 5 }}
|
401 |
-
animate={{ opacity: 1, y: 0 }}
|
402 |
-
exit={{ opacity: 0, y: -5 }}
|
403 |
-
transition={{ duration: 0.3 }}
|
404 |
-
>
|
405 |
-
<Typography
|
406 |
-
variant="body1"
|
407 |
-
sx={{
|
408 |
-
color: "white",
|
409 |
-
opacity: 0.8,
|
410 |
-
fontStyle: "italic",
|
411 |
-
}}
|
412 |
-
>
|
413 |
-
{messages[loadingMessage]}
|
414 |
-
</Typography>
|
415 |
-
</motion.div>
|
416 |
-
</Box>
|
417 |
) : (
|
418 |
<>
|
419 |
<ComicLayout
|
|
|
14 |
import { useStoryCapture } from "../hooks/useStoryCapture";
|
15 |
import { usePageSound } from "../hooks/usePageSound";
|
16 |
import { useWritingSound } from "../hooks/useWritingSound";
|
17 |
+
import { useGameSession } from "../hooks/useGameSession";
|
18 |
import { StoryChoices } from "../components/StoryChoices";
|
19 |
import { ErrorDisplay } from "../components/ErrorDisplay";
|
20 |
import VolumeUpIcon from "@mui/icons-material/VolumeUp";
|
|
|
23 |
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
24 |
import CreateIcon from "@mui/icons-material/Create";
|
25 |
import { getNextLayoutType, LAYOUTS } from "../layouts/config";
|
26 |
+
import { LoadingScreen } from "../components/LoadingScreen";
|
27 |
|
28 |
// Constants
|
29 |
const SOUND_ENABLED_KEY = "sound_enabled";
|
|
|
69 |
useNarrator(isSoundEnabled);
|
70 |
const playPageSound = usePageSound(isSoundEnabled);
|
71 |
const playWritingSound = useWritingSound(isSoundEnabled);
|
72 |
+
const {
|
73 |
+
sessionId,
|
74 |
+
universe,
|
75 |
+
isLoading: isSessionLoading,
|
76 |
+
error: sessionError,
|
77 |
+
} = useGameSession();
|
78 |
|
79 |
// Sauvegarder l'état du son dans le localStorage
|
80 |
useEffect(() => {
|
81 |
localStorage.setItem(SOUND_ENABLED_KEY, isSoundEnabled);
|
82 |
}, [isSoundEnabled]);
|
83 |
|
84 |
+
// Start the story when session is ready
|
85 |
useEffect(() => {
|
86 |
+
if (sessionId && !isSessionLoading) {
|
87 |
+
handleStoryAction("restart");
|
88 |
+
}
|
89 |
+
}, [sessionId, isSessionLoading]);
|
90 |
|
91 |
// Add effect for message rotation
|
92 |
useEffect(() => {
|
|
|
138 |
const handleStoryAction = async (action, choiceId = null) => {
|
139 |
setIsLoading(true);
|
140 |
setShowChoices(false);
|
141 |
+
setError(null);
|
142 |
try {
|
|
|
143 |
if (isNarratorSpeaking) {
|
144 |
stopNarration();
|
145 |
}
|
146 |
|
147 |
console.log("Starting story action:", action);
|
148 |
+
// Pass sessionId to API calls
|
149 |
const storyData = await (action === "restart"
|
150 |
+
? storyApi.start(sessionId)
|
151 |
+
: storyApi.makeChoice(choiceId, sessionId));
|
152 |
|
153 |
if (!storyData) {
|
154 |
throw new Error("Pas de données reçues du serveur");
|
|
|
312 |
await downloadStoryImage(storyContainerRef, `your-story-${Date.now()}.png`);
|
313 |
};
|
314 |
|
315 |
+
// Show session error if any
|
316 |
+
if (sessionError) {
|
317 |
+
return (
|
318 |
+
<ErrorDisplay
|
319 |
+
message="Impossible d'initialiser la session de jeu. Veuillez rafraîchir la page."
|
320 |
+
error={sessionError}
|
321 |
+
/>
|
322 |
+
);
|
323 |
+
}
|
324 |
+
|
325 |
+
// Show loading state while session is initializing
|
326 |
+
if (isSessionLoading) {
|
327 |
+
return (
|
328 |
+
<Box sx={{ width: "100%", height: "100vh", backgroundColor: "#1a1a1a" }}>
|
329 |
+
<LoadingScreen
|
330 |
+
icon="universe"
|
331 |
+
messages={[
|
332 |
+
"Creating a new universe...",
|
333 |
+
"Gathering comic book inspiration...",
|
334 |
+
"Drawing the first panels...",
|
335 |
+
"Setting up the story...",
|
336 |
+
]}
|
337 |
+
/>
|
338 |
+
</Box>
|
339 |
+
);
|
340 |
+
}
|
341 |
+
|
342 |
return (
|
343 |
<motion.div
|
344 |
initial={{ opacity: 0 }}
|
|
|
405 |
) : (
|
406 |
<>
|
407 |
{isLoading && storySegments.length === 0 ? (
|
408 |
+
<LoadingScreen
|
409 |
+
icon="story"
|
410 |
+
messages={[
|
411 |
+
"Bringing the universe to life...",
|
412 |
+
"Awakening the characters...",
|
413 |
+
"Polishing the first scene...",
|
414 |
+
"Preparing the adventure...",
|
415 |
+
"Adding final touches to the world...",
|
416 |
+
]}
|
417 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
418 |
) : (
|
419 |
<>
|
420 |
<ComicLayout
|
client/src/pages/Tutorial.jsx
CHANGED
@@ -135,20 +135,21 @@ export function Tutorial() {
|
|
135 |
color="black"
|
136 |
sx={{ fontWeight: "normal" }}
|
137 |
>
|
138 |
-
|
139 |
-
|
140 |
-
<strong>
|
141 |
-
|
142 |
-
|
143 |
<br />
|
144 |
<br />
|
145 |
-
You
|
146 |
-
|
147 |
-
<strong>
|
148 |
-
|
149 |
-
<strong>
|
150 |
-
|
151 |
-
<strong>
|
|
|
152 |
</Typography>
|
153 |
<Typography variant="h4">How to play</Typography>
|
154 |
<Typography variant="body1" sx={{ fontWeight: "normal" }}>
|
|
|
135 |
color="black"
|
136 |
sx={{ fontWeight: "normal" }}
|
137 |
>
|
138 |
+
You are <strong>Sarah</strong>, an <strong>AI</strong> hunter
|
139 |
+
traveling through
|
140 |
+
<strong>parallel worlds</strong>. Your mission is to track down an{" "}
|
141 |
+
<strong>AI</strong> that moves from world to world to avoid
|
142 |
+
destruction.
|
143 |
<br />
|
144 |
<br />
|
145 |
+
You must make crucial <strong>decisions</strong> to advance in
|
146 |
+
your
|
147 |
+
<strong>quest</strong>. Each <strong>world</strong> presents its
|
148 |
+
own challenges and
|
149 |
+
<strong>obstacles</strong>, and every <strong>choice</strong> you
|
150 |
+
make can alter the course of your <strong>pursuit</strong>.{" "}
|
151 |
+
<strong>Time</strong> is of the essence, and every{" "}
|
152 |
+
<strong>action</strong> counts in this thrilling adventure.
|
153 |
</Typography>
|
154 |
<Typography variant="h4">How to play</Typography>
|
155 |
<Typography variant="body1" sx={{ fontWeight: "normal" }}>
|
client/src/utils/api.js
CHANGED
@@ -10,12 +10,28 @@ const API_URL = isHFSpace
|
|
10 |
// Create axios instance with default config
|
11 |
const api = axios.create({
|
12 |
baseURL: API_URL,
|
13 |
-
headers: getDefaultHeaders(),
|
14 |
...(isHFSpace && {
|
15 |
baseURL: window.location.origin,
|
16 |
}),
|
17 |
});
|
18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
// Error handling middleware
|
20 |
const handleApiError = (error) => {
|
21 |
console.error("API Error:", {
|
@@ -48,12 +64,18 @@ const handleApiError = (error) => {
|
|
48 |
|
49 |
// Story related API calls
|
50 |
export const storyApi = {
|
51 |
-
start: async () => {
|
52 |
try {
|
53 |
-
console.log("Calling start API
|
54 |
-
const response = await api.post(
|
55 |
-
|
56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
57 |
console.log("Start API response:", response.data);
|
58 |
return response.data;
|
59 |
} catch (error) {
|
@@ -61,13 +83,19 @@ export const storyApi = {
|
|
61 |
}
|
62 |
},
|
63 |
|
64 |
-
makeChoice: async (choiceId) => {
|
65 |
try {
|
66 |
-
console.log("Making choice:", choiceId);
|
67 |
-
const response = await api.post(
|
68 |
-
|
69 |
-
|
70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
71 |
console.log("Choice API response:", response.data);
|
72 |
return response.data;
|
73 |
} catch (error) {
|
@@ -75,14 +103,26 @@ export const storyApi = {
|
|
75 |
}
|
76 |
},
|
77 |
|
78 |
-
generateImage: async (
|
|
|
|
|
|
|
|
|
|
|
79 |
try {
|
80 |
console.log("Generating image with prompt:", prompt);
|
81 |
-
const
|
82 |
prompt,
|
83 |
width,
|
84 |
height,
|
85 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
86 |
console.log("Image generation response:", {
|
87 |
success: response.data.success,
|
88 |
hasImage: !!response.data.image_base64,
|
@@ -94,12 +134,18 @@ export const storyApi = {
|
|
94 |
},
|
95 |
|
96 |
// Narration related API calls
|
97 |
-
narrate: async (text) => {
|
98 |
try {
|
99 |
console.log("Requesting narration for:", text);
|
100 |
-
const response = await api.post(
|
101 |
-
text,
|
102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
103 |
console.log("Narration response received");
|
104 |
return response.data;
|
105 |
} catch (error) {
|
@@ -111,5 +157,16 @@ export const storyApi = {
|
|
111 |
// WebSocket URL
|
112 |
export const WS_URL = import.meta.env.VITE_WS_URL || "ws://localhost:8000/ws";
|
113 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
114 |
// Export the base API instance for other uses
|
115 |
export default api;
|
|
|
10 |
// Create axios instance with default config
|
11 |
const api = axios.create({
|
12 |
baseURL: API_URL,
|
|
|
13 |
...(isHFSpace && {
|
14 |
baseURL: window.location.origin,
|
15 |
}),
|
16 |
});
|
17 |
|
18 |
+
// Add request interceptor to handle headers
|
19 |
+
api.interceptors.request.use((config) => {
|
20 |
+
// Routes qui ne nécessitent pas de session_id
|
21 |
+
const noSessionRoutes = ["/api/universe/generate", "/api/generate-image"];
|
22 |
+
|
23 |
+
if (noSessionRoutes.includes(config.url)) {
|
24 |
+
return config;
|
25 |
+
}
|
26 |
+
|
27 |
+
// Pour toutes les autres requêtes, s'assurer qu'on a un session_id
|
28 |
+
if (!config.headers["x-session-id"]) {
|
29 |
+
throw new Error("Session ID is required for this request");
|
30 |
+
}
|
31 |
+
|
32 |
+
return config;
|
33 |
+
});
|
34 |
+
|
35 |
// Error handling middleware
|
36 |
const handleApiError = (error) => {
|
37 |
console.error("API Error:", {
|
|
|
64 |
|
65 |
// Story related API calls
|
66 |
export const storyApi = {
|
67 |
+
start: async (sessionId) => {
|
68 |
try {
|
69 |
+
console.log("Calling start API with session:", sessionId);
|
70 |
+
const response = await api.post(
|
71 |
+
"/api/chat",
|
72 |
+
{
|
73 |
+
message: "restart",
|
74 |
+
},
|
75 |
+
{
|
76 |
+
headers: getDefaultHeaders(sessionId),
|
77 |
+
}
|
78 |
+
);
|
79 |
console.log("Start API response:", response.data);
|
80 |
return response.data;
|
81 |
} catch (error) {
|
|
|
83 |
}
|
84 |
},
|
85 |
|
86 |
+
makeChoice: async (choiceId, sessionId) => {
|
87 |
try {
|
88 |
+
console.log("Making choice:", choiceId, "for session:", sessionId);
|
89 |
+
const response = await api.post(
|
90 |
+
"/api/chat",
|
91 |
+
{
|
92 |
+
message: "choice",
|
93 |
+
choice_id: choiceId,
|
94 |
+
},
|
95 |
+
{
|
96 |
+
headers: getDefaultHeaders(sessionId),
|
97 |
+
}
|
98 |
+
);
|
99 |
console.log("Choice API response:", response.data);
|
100 |
return response.data;
|
101 |
} catch (error) {
|
|
|
103 |
}
|
104 |
},
|
105 |
|
106 |
+
generateImage: async (
|
107 |
+
prompt,
|
108 |
+
width = 512,
|
109 |
+
height = 512,
|
110 |
+
sessionId = null
|
111 |
+
) => {
|
112 |
try {
|
113 |
console.log("Generating image with prompt:", prompt);
|
114 |
+
const config = {
|
115 |
prompt,
|
116 |
width,
|
117 |
height,
|
118 |
+
};
|
119 |
+
|
120 |
+
const options = {};
|
121 |
+
if (sessionId) {
|
122 |
+
options.headers = getDefaultHeaders(sessionId);
|
123 |
+
}
|
124 |
+
|
125 |
+
const response = await api.post("/api/generate-image", config, options);
|
126 |
console.log("Image generation response:", {
|
127 |
success: response.data.success,
|
128 |
hasImage: !!response.data.image_base64,
|
|
|
134 |
},
|
135 |
|
136 |
// Narration related API calls
|
137 |
+
narrate: async (text, sessionId) => {
|
138 |
try {
|
139 |
console.log("Requesting narration for:", text);
|
140 |
+
const response = await api.post(
|
141 |
+
"/api/text-to-speech",
|
142 |
+
{
|
143 |
+
text,
|
144 |
+
},
|
145 |
+
{
|
146 |
+
headers: getDefaultHeaders(sessionId),
|
147 |
+
}
|
148 |
+
);
|
149 |
console.log("Narration response received");
|
150 |
return response.data;
|
151 |
} catch (error) {
|
|
|
157 |
// WebSocket URL
|
158 |
export const WS_URL = import.meta.env.VITE_WS_URL || "ws://localhost:8000/ws";
|
159 |
|
160 |
+
export const universeApi = {
|
161 |
+
generate: async () => {
|
162 |
+
try {
|
163 |
+
const response = await api.post("/api/universe/generate");
|
164 |
+
return response.data;
|
165 |
+
} catch (error) {
|
166 |
+
return handleApiError(error);
|
167 |
+
}
|
168 |
+
},
|
169 |
+
};
|
170 |
+
|
171 |
// Export the base API instance for other uses
|
172 |
export default api;
|
client/src/utils/session.js
CHANGED
@@ -1,9 +1,15 @@
|
|
1 |
-
// Generate unique
|
2 |
export const CLIENT_ID = `client_${Math.random().toString(36).substring(2)}`;
|
3 |
-
export const SESSION_ID = `session_${Math.random().toString(36).substring(2)}`;
|
4 |
|
5 |
// Create default headers for API requests
|
6 |
-
export const getDefaultHeaders = () =>
|
7 |
-
|
8 |
-
|
9 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// Generate unique ID for client
|
2 |
export const CLIENT_ID = `client_${Math.random().toString(36).substring(2)}`;
|
|
|
3 |
|
4 |
// Create default headers for API requests
|
5 |
+
export const getDefaultHeaders = (sessionId = null) => {
|
6 |
+
const headers = {
|
7 |
+
"x-client-id": CLIENT_ID,
|
8 |
+
};
|
9 |
+
|
10 |
+
if (sessionId) {
|
11 |
+
headers["x-session-id"] = sessionId;
|
12 |
+
}
|
13 |
+
|
14 |
+
return headers;
|
15 |
+
};
|
server/.session_data.pkl
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:17c52bb5d3dbe4af41c98c7992c63979361073b8aae65d10a9feb74728329c7c
|
3 |
+
size 3071
|
server/api/models.py
CHANGED
@@ -85,4 +85,12 @@ class ImageGenerationRequest(BaseModel):
|
|
85 |
|
86 |
class TextToSpeechRequest(BaseModel):
|
87 |
text: str
|
88 |
-
voice_id: str = "nPczCjzI2devNBz1zQrb" # Default voice ID (Rachel)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
85 |
|
86 |
class TextToSpeechRequest(BaseModel):
|
87 |
text: str
|
88 |
+
voice_id: str = "nPczCjzI2devNBz1zQrb" # Default voice ID (Rachel)
|
89 |
+
|
90 |
+
class UniverseResponse(BaseModel):
|
91 |
+
status: str
|
92 |
+
session_id: str
|
93 |
+
style: str
|
94 |
+
genre: str
|
95 |
+
epoch: str
|
96 |
+
base_story: str = Field(description="The generated story for this universe")
|
server/api/routes/chat.py
CHANGED
@@ -20,19 +20,48 @@ def get_chat_router(session_manager: SessionManager, story_generator):
|
|
20 |
|
21 |
print(f"Processing chat message for session {x_session_id}:", chat_message)
|
22 |
|
23 |
-
# Get
|
24 |
-
game_state = session_manager.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
|
26 |
# Handle restart
|
27 |
if chat_message.message.lower() == "restart":
|
28 |
print(f"Handling restart for session {x_session_id}")
|
|
|
29 |
game_state.reset()
|
|
|
|
|
|
|
|
|
|
|
|
|
30 |
previous_choice = "none"
|
31 |
else:
|
32 |
previous_choice = f"Choice {chat_message.choice_id}" if chat_message.choice_id else "none"
|
33 |
|
34 |
# Generate story segment
|
35 |
-
llm_response = await story_generator.generate_story_segment(
|
|
|
|
|
|
|
|
|
36 |
|
37 |
# Update radiation level
|
38 |
game_state.radiation_level += llm_response.radiation_increase
|
|
|
20 |
|
21 |
print(f"Processing chat message for session {x_session_id}:", chat_message)
|
22 |
|
23 |
+
# Get game state for this session
|
24 |
+
game_state = session_manager.get_session(x_session_id)
|
25 |
+
print(f"Retrieved game state for session {x_session_id}: {'found' if game_state else 'not found'}")
|
26 |
+
|
27 |
+
if game_state is None:
|
28 |
+
raise HTTPException(
|
29 |
+
status_code=400,
|
30 |
+
detail="Invalid session ID. Generate a universe first to start a new game session."
|
31 |
+
)
|
32 |
+
|
33 |
+
# Vérifier que l'univers est configuré
|
34 |
+
has_universe = game_state.has_universe()
|
35 |
+
print(f"Universe configured for session {x_session_id}: {has_universe}")
|
36 |
+
print(f"Universe details: style={game_state.universe_style}, genre={game_state.universe_genre}, epoch={game_state.universe_epoch}")
|
37 |
+
|
38 |
+
if not has_universe:
|
39 |
+
raise HTTPException(
|
40 |
+
status_code=400,
|
41 |
+
detail="Universe not configured for this session. Generate a universe first."
|
42 |
+
)
|
43 |
|
44 |
# Handle restart
|
45 |
if chat_message.message.lower() == "restart":
|
46 |
print(f"Handling restart for session {x_session_id}")
|
47 |
+
# On garde le même univers mais on réinitialise l'histoire
|
48 |
game_state.reset()
|
49 |
+
game_state.set_universe(
|
50 |
+
style=game_state.universe_style,
|
51 |
+
genre=game_state.universe_genre,
|
52 |
+
epoch=game_state.universe_epoch,
|
53 |
+
base_story=game_state.universe_story
|
54 |
+
)
|
55 |
previous_choice = "none"
|
56 |
else:
|
57 |
previous_choice = f"Choice {chat_message.choice_id}" if chat_message.choice_id else "none"
|
58 |
|
59 |
# Generate story segment
|
60 |
+
llm_response = await story_generator.generate_story_segment(
|
61 |
+
session_id=x_session_id,
|
62 |
+
game_state=game_state,
|
63 |
+
previous_choice=previous_choice
|
64 |
+
)
|
65 |
|
66 |
# Update radiation level
|
67 |
game_state.radiation_level += llm_response.radiation_increase
|
server/api/routes/universe.py
ADDED
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, HTTPException
|
2 |
+
import uuid
|
3 |
+
|
4 |
+
from core.generators.universe_generator import UniverseGenerator
|
5 |
+
from core.game_logic import StoryGenerator
|
6 |
+
from core.session_manager import SessionManager
|
7 |
+
from api.models import UniverseResponse
|
8 |
+
|
9 |
+
def get_universe_router(session_manager: SessionManager, story_generator: StoryGenerator) -> APIRouter:
|
10 |
+
router = APIRouter()
|
11 |
+
universe_generator = UniverseGenerator(story_generator.mistral_client)
|
12 |
+
|
13 |
+
@router.post("/universe/generate", response_model=UniverseResponse)
|
14 |
+
async def generate_universe() -> UniverseResponse:
|
15 |
+
try:
|
16 |
+
print("Starting universe generation...")
|
17 |
+
|
18 |
+
# Get random elements before generation
|
19 |
+
style, genre, epoch = universe_generator._get_random_elements()
|
20 |
+
print(f"Generated random elements: style={style['name']}, genre={genre}, epoch={epoch}")
|
21 |
+
|
22 |
+
universe = await universe_generator.generate()
|
23 |
+
print("Generated universe story")
|
24 |
+
|
25 |
+
# Générer un ID de session unique
|
26 |
+
session_id = str(uuid.uuid4())
|
27 |
+
print(f"Generated session ID: {session_id}")
|
28 |
+
|
29 |
+
# Créer une nouvelle session et configurer l'univers
|
30 |
+
game_state = session_manager.create_session(session_id)
|
31 |
+
print("Created new game state")
|
32 |
+
|
33 |
+
game_state.set_universe(
|
34 |
+
style=style["name"],
|
35 |
+
genre=genre,
|
36 |
+
epoch=epoch,
|
37 |
+
base_story=universe
|
38 |
+
)
|
39 |
+
print("Configured universe in game state")
|
40 |
+
|
41 |
+
# Créer le TextGenerator pour cette session
|
42 |
+
story_generator.create_text_generator(
|
43 |
+
session_id=session_id,
|
44 |
+
style=style["name"],
|
45 |
+
genre=genre,
|
46 |
+
epoch=epoch,
|
47 |
+
base_story=universe
|
48 |
+
)
|
49 |
+
print("Created text generator for session")
|
50 |
+
|
51 |
+
# Vérifier que tout est bien configuré
|
52 |
+
if not game_state.has_universe():
|
53 |
+
raise ValueError("Universe was not properly configured in game state")
|
54 |
+
|
55 |
+
if session_id not in story_generator.text_generators:
|
56 |
+
raise ValueError("TextGenerator was not properly created")
|
57 |
+
|
58 |
+
print("All components configured successfully")
|
59 |
+
|
60 |
+
return UniverseResponse(
|
61 |
+
status="ok",
|
62 |
+
session_id=session_id,
|
63 |
+
style=style["name"],
|
64 |
+
genre=genre,
|
65 |
+
epoch=epoch,
|
66 |
+
base_story=universe
|
67 |
+
)
|
68 |
+
|
69 |
+
except Exception as e:
|
70 |
+
print(f"Error in generate_universe: {str(e)}") # Add debug logging
|
71 |
+
raise HTTPException(
|
72 |
+
status_code=500,
|
73 |
+
detail=str(e)
|
74 |
+
)
|
75 |
+
|
76 |
+
return router
|
server/core/__init__.py
DELETED
@@ -1,11 +0,0 @@
|
|
1 |
-
from core.story_orchestrator import StoryOrchestrator
|
2 |
-
from core.state import GameState
|
3 |
-
from core.generators import TextGenerator, ImageGenerator, MetadataGenerator
|
4 |
-
|
5 |
-
__all__ = [
|
6 |
-
'StoryOrchestrator',
|
7 |
-
'GameState',
|
8 |
-
'TextGenerator',
|
9 |
-
'ImageGenerator',
|
10 |
-
'MetadataGenerator'
|
11 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/core/constants.py
CHANGED
@@ -2,7 +2,7 @@ class GameConfig:
|
|
2 |
# Game state constants
|
3 |
MAX_RADIATION = 12
|
4 |
STARTING_TIME = "18:00"
|
5 |
-
STARTING_LOCATION = "
|
6 |
|
7 |
# Story constraints
|
8 |
MIN_PANELS = 1
|
|
|
2 |
# Game state constants
|
3 |
MAX_RADIATION = 12
|
4 |
STARTING_TIME = "18:00"
|
5 |
+
STARTING_LOCATION = "Home"
|
6 |
|
7 |
# Story constraints
|
8 |
MIN_PANELS = 1
|
server/core/game_logic.py
CHANGED
@@ -4,18 +4,34 @@ from langchain.output_parsers import PydanticOutputParser, OutputFixingParser
|
|
4 |
from langchain.prompts import ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemplate
|
5 |
import os
|
6 |
import asyncio
|
|
|
7 |
|
8 |
from core.constants import GameConfig
|
9 |
from core.prompts.system import SARAH_DESCRIPTION
|
10 |
from core.prompts.cinematic import CINEMATIC_SYSTEM_PROMPT
|
11 |
from core.prompts.image_style import IMAGE_STYLE_PREFIX
|
12 |
from services.mistral_client import MistralClient
|
13 |
-
from api.models import
|
14 |
-
from core.
|
|
|
|
|
|
|
15 |
|
16 |
-
|
17 |
-
|
18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
|
20 |
def enrich_prompt_with_sarah_description(prompt: str) -> str:
|
21 |
"""Add Sarah's visual description to prompts that mention her."""
|
@@ -35,14 +51,49 @@ class GameState:
|
|
35 |
self.story_history = []
|
36 |
self.current_time = GameConfig.STARTING_TIME
|
37 |
self.current_location = GameConfig.STARTING_LOCATION
|
|
|
|
|
|
|
|
|
|
|
38 |
|
39 |
def reset(self):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
40 |
self.story_beat = GameConfig.STORY_BEAT_INTRO
|
41 |
self.radiation_level = 0
|
42 |
self.story_history = []
|
43 |
self.current_time = GameConfig.STARTING_TIME
|
44 |
self.current_location = GameConfig.STARTING_LOCATION
|
45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
46 |
def add_to_history(self, segment_text: str, choice_made: str, image_prompts: List[str], time: str, location: str):
|
47 |
self.story_history.append({
|
48 |
"segment": segment_text,
|
@@ -56,22 +107,56 @@ class GameState:
|
|
56 |
|
57 |
# Story output structure
|
58 |
class StoryLLMResponse(BaseModel):
|
59 |
-
story_text: str = Field(description="The next segment of the story. No more than 15 words THIS IS MANDATORY. Never mention story beat
|
60 |
choices: List[str] = Field(description="Between one and four possible choices for the player. Each choice should be a clear path to follow in the story", min_items=1, max_items=4)
|
61 |
is_victory: bool = Field(description="Whether this segment ends in Sarah's victory", default=False)
|
62 |
is_death: bool = Field(description="Whether this segment ends in Sarah's death", default=False)
|
63 |
radiation_increase: int = Field(description="How much radiation this segment adds (0-3)", ge=0, le=3, default=1)
|
64 |
image_prompts: List[str] = Field(description="List of 1 to 4 comic panel descriptions that illustrate the key moments of the scene", min_items=1, max_items=4)
|
65 |
-
time: str = Field(description="Current in-game time in 24h format (HH:MM). Time passes realistically based on actions.", default=STARTING_TIME)
|
66 |
-
location: str = Field(description="Current location
|
67 |
|
68 |
# Story generator
|
69 |
class StoryGenerator:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
70 |
def __init__(self, api_key: str, model_name: str = "mistral-small"):
|
71 |
-
self.
|
72 |
-
|
73 |
-
|
74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
75 |
|
76 |
def _format_story_history(self, game_state: GameState) -> str:
|
77 |
"""Formate l'historique de l'histoire pour le prompt."""
|
@@ -85,11 +170,13 @@ class StoryGenerator:
|
|
85 |
story_history = "\n\n---\n\n".join(segments)
|
86 |
return story_history
|
87 |
|
88 |
-
async def generate_story_segment(self, game_state: GameState, previous_choice: str) -> StoryResponse:
|
89 |
"""Génère un segment d'histoire complet en plusieurs étapes."""
|
|
|
|
|
90 |
# 1. Générer le texte de l'histoire initial
|
91 |
story_history = self._format_story_history(game_state)
|
92 |
-
text_response = await
|
93 |
story_beat=game_state.story_beat,
|
94 |
radiation_level=game_state.radiation_level,
|
95 |
current_time=game_state.current_time,
|
@@ -113,7 +200,7 @@ class StoryGenerator:
|
|
113 |
if is_ending:
|
114 |
# Regénérer le texte avec le contexte de fin
|
115 |
ending_type = "victory" if metadata_response.is_victory else "death"
|
116 |
-
text_response = await
|
117 |
story_beat=game_state.story_beat,
|
118 |
ending_type=ending_type,
|
119 |
current_scene=text_response.story_text,
|
@@ -123,12 +210,12 @@ class StoryGenerator:
|
|
123 |
metadata_response.is_death = True
|
124 |
|
125 |
# Ne générer qu'une seule image pour la fin
|
126 |
-
prompts_response = await self.
|
127 |
if len(prompts_response.image_prompts) > 1:
|
128 |
prompts_response.image_prompts = [prompts_response.image_prompts[0]]
|
129 |
else:
|
130 |
# Si ce n'est pas une fin, générer les prompts normalement
|
131 |
-
prompts_response = await self.
|
132 |
|
133 |
# 4. Créer la réponse finale
|
134 |
choices = [] if is_ending else [
|
|
|
4 |
from langchain.prompts import ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemplate
|
5 |
import os
|
6 |
import asyncio
|
7 |
+
from uuid import uuid4
|
8 |
|
9 |
from core.constants import GameConfig
|
10 |
from core.prompts.system import SARAH_DESCRIPTION
|
11 |
from core.prompts.cinematic import CINEMATIC_SYSTEM_PROMPT
|
12 |
from core.prompts.image_style import IMAGE_STYLE_PREFIX
|
13 |
from services.mistral_client import MistralClient
|
14 |
+
from api.models import StoryResponse, Choice
|
15 |
+
from core.generators.text_generator import TextGenerator
|
16 |
+
from core.generators.image_generator import ImageGenerator
|
17 |
+
from core.generators.metadata_generator import MetadataGenerator
|
18 |
+
from core.generators.universe_generator import UniverseGenerator
|
19 |
|
20 |
+
from core.constants import GameConfig
|
21 |
+
|
22 |
+
# Initialize generators with None - they will be set up when needed
|
23 |
+
universe_generator = None
|
24 |
+
image_generator = None
|
25 |
+
metadata_generator = None
|
26 |
+
|
27 |
+
def setup_generators(api_key: str, model_name: str = "mistral-small"):
|
28 |
+
"""Setup all generators with the provided API key."""
|
29 |
+
global universe_generator, image_generator, metadata_generator
|
30 |
+
|
31 |
+
mistral_client = MistralClient(api_key=api_key, model_name=model_name)
|
32 |
+
universe_generator = UniverseGenerator(mistral_client)
|
33 |
+
image_generator = ImageGenerator(mistral_client)
|
34 |
+
metadata_generator = MetadataGenerator(mistral_client)
|
35 |
|
36 |
def enrich_prompt_with_sarah_description(prompt: str) -> str:
|
37 |
"""Add Sarah's visual description to prompts that mention her."""
|
|
|
51 |
self.story_history = []
|
52 |
self.current_time = GameConfig.STARTING_TIME
|
53 |
self.current_location = GameConfig.STARTING_LOCATION
|
54 |
+
# Ajout des informations d'univers
|
55 |
+
self.universe_style = None
|
56 |
+
self.universe_genre = None
|
57 |
+
self.universe_epoch = None
|
58 |
+
self.universe_story = None
|
59 |
|
60 |
def reset(self):
|
61 |
+
"""Réinitialise l'état du jeu en gardant les informations de l'univers."""
|
62 |
+
# Sauvegarder les informations de l'univers
|
63 |
+
universe_style = self.universe_style
|
64 |
+
universe_genre = self.universe_genre
|
65 |
+
universe_epoch = self.universe_epoch
|
66 |
+
universe_story = self.universe_story
|
67 |
+
|
68 |
+
# Réinitialiser l'état du jeu
|
69 |
self.story_beat = GameConfig.STORY_BEAT_INTRO
|
70 |
self.radiation_level = 0
|
71 |
self.story_history = []
|
72 |
self.current_time = GameConfig.STARTING_TIME
|
73 |
self.current_location = GameConfig.STARTING_LOCATION
|
74 |
|
75 |
+
# Restaurer les informations de l'univers
|
76 |
+
self.universe_style = universe_style
|
77 |
+
self.universe_genre = universe_genre
|
78 |
+
self.universe_epoch = universe_epoch
|
79 |
+
self.universe_story = universe_story
|
80 |
+
|
81 |
+
def set_universe(self, style: str, genre: str, epoch: str, base_story: str):
|
82 |
+
"""Configure l'univers du jeu."""
|
83 |
+
self.universe_style = style
|
84 |
+
self.universe_genre = genre
|
85 |
+
self.universe_epoch = epoch
|
86 |
+
self.universe_story = base_story
|
87 |
+
|
88 |
+
def has_universe(self) -> bool:
|
89 |
+
"""Vérifie si l'univers est configuré."""
|
90 |
+
return all([
|
91 |
+
self.universe_style is not None,
|
92 |
+
self.universe_genre is not None,
|
93 |
+
self.universe_epoch is not None,
|
94 |
+
self.universe_story is not None
|
95 |
+
])
|
96 |
+
|
97 |
def add_to_history(self, segment_text: str, choice_made: str, image_prompts: List[str], time: str, location: str):
|
98 |
self.story_history.append({
|
99 |
"segment": segment_text,
|
|
|
107 |
|
108 |
# Story output structure
|
109 |
class StoryLLMResponse(BaseModel):
|
110 |
+
story_text: str = Field(description="The next segment of the story. No more than 15 words THIS IS MANDATORY. Never mention story beat directly. ")
|
111 |
choices: List[str] = Field(description="Between one and four possible choices for the player. Each choice should be a clear path to follow in the story", min_items=1, max_items=4)
|
112 |
is_victory: bool = Field(description="Whether this segment ends in Sarah's victory", default=False)
|
113 |
is_death: bool = Field(description="Whether this segment ends in Sarah's death", default=False)
|
114 |
radiation_increase: int = Field(description="How much radiation this segment adds (0-3)", ge=0, le=3, default=1)
|
115 |
image_prompts: List[str] = Field(description="List of 1 to 4 comic panel descriptions that illustrate the key moments of the scene", min_items=1, max_items=4)
|
116 |
+
time: str = Field(description="Current in-game time in 24h format (HH:MM). Time passes realistically based on actions.", default=GameConfig.STARTING_TIME)
|
117 |
+
location: str = Field(description="Current location.", default=GameConfig.STARTING_LOCATION)
|
118 |
|
119 |
# Story generator
|
120 |
class StoryGenerator:
|
121 |
+
_instance = None
|
122 |
+
|
123 |
+
def __new__(cls, *args, **kwargs):
|
124 |
+
if cls._instance is None:
|
125 |
+
print("Creating new StoryGenerator instance")
|
126 |
+
cls._instance = super().__new__(cls)
|
127 |
+
cls._instance._initialized = False
|
128 |
+
return cls._instance
|
129 |
+
|
130 |
def __init__(self, api_key: str, model_name: str = "mistral-small"):
|
131 |
+
if not self._initialized:
|
132 |
+
print("Initializing StoryGenerator singleton")
|
133 |
+
self.api_key = api_key
|
134 |
+
self.model_name = model_name
|
135 |
+
self.mistral_client = MistralClient(api_key=api_key, model_name=model_name)
|
136 |
+
self.image_generator = ImageGenerator(self.mistral_client)
|
137 |
+
self.metadata_generator = MetadataGenerator(self.mistral_client)
|
138 |
+
self.text_generators = {} # Un TextGenerator par session
|
139 |
+
self._initialized = True
|
140 |
+
|
141 |
+
def create_text_generator(self, session_id: str, style: str, genre: str, epoch: str, base_story: str):
|
142 |
+
"""Crée un nouveau TextGenerator adapté à l'univers spécifié pour une session donnée."""
|
143 |
+
print(f"Creating TextGenerator for session {session_id} in StoryGenerator singleton")
|
144 |
+
self.text_generators[session_id] = TextGenerator(
|
145 |
+
self.mistral_client,
|
146 |
+
universe_style=style,
|
147 |
+
universe_genre=genre,
|
148 |
+
universe_epoch=epoch,
|
149 |
+
universe_story=base_story
|
150 |
+
)
|
151 |
+
print(f"Current TextGenerators in StoryGenerator: {list(self.text_generators.keys())}")
|
152 |
+
|
153 |
+
def get_text_generator(self, session_id: str) -> TextGenerator:
|
154 |
+
"""Récupère le TextGenerator associé à une session."""
|
155 |
+
print(f"Getting TextGenerator for session {session_id} from StoryGenerator singleton")
|
156 |
+
print(f"Current TextGenerators in StoryGenerator: {list(self.text_generators.keys())}")
|
157 |
+
if session_id not in self.text_generators:
|
158 |
+
raise RuntimeError(f"No text generator found for session {session_id}. Generate a universe first.")
|
159 |
+
return self.text_generators[session_id]
|
160 |
|
161 |
def _format_story_history(self, game_state: GameState) -> str:
|
162 |
"""Formate l'historique de l'histoire pour le prompt."""
|
|
|
170 |
story_history = "\n\n---\n\n".join(segments)
|
171 |
return story_history
|
172 |
|
173 |
+
async def generate_story_segment(self, session_id: str, game_state: GameState, previous_choice: str) -> StoryResponse:
|
174 |
"""Génère un segment d'histoire complet en plusieurs étapes."""
|
175 |
+
text_generator = self.get_text_generator(session_id)
|
176 |
+
|
177 |
# 1. Générer le texte de l'histoire initial
|
178 |
story_history = self._format_story_history(game_state)
|
179 |
+
text_response = await text_generator.generate(
|
180 |
story_beat=game_state.story_beat,
|
181 |
radiation_level=game_state.radiation_level,
|
182 |
current_time=game_state.current_time,
|
|
|
200 |
if is_ending:
|
201 |
# Regénérer le texte avec le contexte de fin
|
202 |
ending_type = "victory" if metadata_response.is_victory else "death"
|
203 |
+
text_response = await text_generator.generate_ending(
|
204 |
story_beat=game_state.story_beat,
|
205 |
ending_type=ending_type,
|
206 |
current_scene=text_response.story_text,
|
|
|
210 |
metadata_response.is_death = True
|
211 |
|
212 |
# Ne générer qu'une seule image pour la fin
|
213 |
+
prompts_response = await self.image_generator.generate(text_response.story_text)
|
214 |
if len(prompts_response.image_prompts) > 1:
|
215 |
prompts_response.image_prompts = [prompts_response.image_prompts[0]]
|
216 |
else:
|
217 |
# Si ce n'est pas une fin, générer les prompts normalement
|
218 |
+
prompts_response = await self.image_generator.generate(text_response.story_text)
|
219 |
|
220 |
# 4. Créer la réponse finale
|
221 |
choices = [] if is_ending else [
|
server/core/generators/__init__.py
DELETED
@@ -1,11 +0,0 @@
|
|
1 |
-
from core.generators.base_generator import BaseGenerator
|
2 |
-
from core.generators.text_generator import TextGenerator
|
3 |
-
from core.generators.image_generator import ImageGenerator
|
4 |
-
from core.generators.metadata_generator import MetadataGenerator
|
5 |
-
|
6 |
-
__all__ = [
|
7 |
-
'BaseGenerator',
|
8 |
-
'TextGenerator',
|
9 |
-
'ImageGenerator',
|
10 |
-
'MetadataGenerator'
|
11 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/core/generators/metadata_generator.py
CHANGED
@@ -46,7 +46,7 @@ Generate the metadata following the format specified."""
|
|
46 |
raise ValueError(str(e))
|
47 |
|
48 |
async def generate(self, story_text: str, current_time: str, current_location: str, story_beat: int, error_feedback: str = "") -> StoryMetadataResponse:
|
49 |
-
"""
|
50 |
return await super().generate(
|
51 |
story_text=story_text,
|
52 |
current_time=current_time,
|
|
|
46 |
raise ValueError(str(e))
|
47 |
|
48 |
async def generate(self, story_text: str, current_time: str, current_location: str, story_beat: int, error_feedback: str = "") -> StoryMetadataResponse:
|
49 |
+
"""Surcharge de generate pour inclure le error_feedback par défaut."""
|
50 |
return await super().generate(
|
51 |
story_text=story_text,
|
52 |
current_time=current_time,
|
server/core/generators/text_generator.py
CHANGED
@@ -4,25 +4,50 @@ from langchain.prompts import ChatPromptTemplate, SystemMessagePromptTemplate, H
|
|
4 |
from core.generators.base_generator import BaseGenerator
|
5 |
from core.prompts.text_prompts import TEXT_GENERATOR_PROMPT
|
6 |
from api.models import StoryTextResponse
|
|
|
7 |
|
8 |
class TextGenerator(BaseGenerator):
|
9 |
-
"""Générateur pour le texte
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
|
11 |
def _create_prompt(self) -> ChatPromptTemplate:
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
|
18 |
Story history:
|
19 |
{story_history}
|
20 |
|
21 |
-
Generate the next story segment
|
22 |
|
23 |
return ChatPromptTemplate(
|
24 |
messages=[
|
25 |
-
SystemMessagePromptTemplate.from_template(
|
26 |
HumanMessagePromptTemplate.from_template(human_template)
|
27 |
]
|
28 |
)
|
@@ -65,16 +90,64 @@ The ending should feel like a natural continuation of the current scene."""
|
|
65 |
cleaned_text = self._clean_story_text(response_content.strip())
|
66 |
return StoryTextResponse(story_text=cleaned_text)
|
67 |
|
68 |
-
async def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
69 |
"""Génère un texte de fin approprié."""
|
70 |
-
|
71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
72 |
ending_type=ending_type,
|
73 |
current_scene=current_scene,
|
74 |
-
story_history=story_history
|
|
|
|
|
|
|
|
|
75 |
)
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
custom_parser=self._custom_parser
|
80 |
-
)
|
|
|
4 |
from core.generators.base_generator import BaseGenerator
|
5 |
from core.prompts.text_prompts import TEXT_GENERATOR_PROMPT
|
6 |
from api.models import StoryTextResponse
|
7 |
+
from services.mistral_client import MistralClient
|
8 |
|
9 |
class TextGenerator(BaseGenerator):
|
10 |
+
"""Générateur pour le texte de l'histoire."""
|
11 |
+
|
12 |
+
def __init__(self, mistral_client: MistralClient, universe_style: str = None, universe_genre: str = None, universe_epoch: str = None, universe_story: str = None):
|
13 |
+
super().__init__(mistral_client)
|
14 |
+
self.universe_style = universe_style
|
15 |
+
self.universe_genre = universe_genre
|
16 |
+
self.universe_epoch = universe_epoch
|
17 |
+
self.universe_story = universe_story
|
18 |
|
19 |
def _create_prompt(self) -> ChatPromptTemplate:
|
20 |
+
system_template = """You are a story generator for a comic book adventure game.
|
21 |
+
You are generating a story in the following universe:
|
22 |
+
- Style: {universe_style}
|
23 |
+
- Genre: {universe_genre}
|
24 |
+
- Historical epoch: {universe_epoch}
|
25 |
+
|
26 |
+
Base universe story:
|
27 |
+
{universe_story}
|
28 |
+
|
29 |
+
Your task is to generate the next segment of the story, following these rules:
|
30 |
+
1. Keep the story consistent with the universe parameters
|
31 |
+
2. Each segment must advance the plot
|
32 |
+
3. Never repeat previous descriptions or situations
|
33 |
+
4. Keep segments concise and impactful (max 15 words)
|
34 |
+
5. The MacGuffin should remain mysterious but central to the plot"""
|
35 |
+
|
36 |
+
human_template = """Current game state:
|
37 |
+
- Story beat: {story_beat}
|
38 |
+
- Radiation level: {radiation_level}
|
39 |
+
- Current time: {current_time}
|
40 |
+
- Current location: {current_location}
|
41 |
+
- Previous choice: {previous_choice}
|
42 |
|
43 |
Story history:
|
44 |
{story_history}
|
45 |
|
46 |
+
Generate the next story segment."""
|
47 |
|
48 |
return ChatPromptTemplate(
|
49 |
messages=[
|
50 |
+
SystemMessagePromptTemplate.from_template(system_template),
|
51 |
HumanMessagePromptTemplate.from_template(human_template)
|
52 |
]
|
53 |
)
|
|
|
90 |
cleaned_text = self._clean_story_text(response_content.strip())
|
91 |
return StoryTextResponse(story_text=cleaned_text)
|
92 |
|
93 |
+
async def generate(self, story_beat: int, radiation_level: int, current_time: str, current_location: str, previous_choice: str, story_history: str = "") -> StoryTextResponse:
|
94 |
+
"""Génère le prochain segment de l'histoire."""
|
95 |
+
return await super().generate(
|
96 |
+
story_beat=story_beat,
|
97 |
+
radiation_level=radiation_level,
|
98 |
+
current_time=current_time,
|
99 |
+
current_location=current_location,
|
100 |
+
previous_choice=previous_choice,
|
101 |
+
story_history=story_history,
|
102 |
+
universe_style=self.universe_style,
|
103 |
+
universe_genre=self.universe_genre,
|
104 |
+
universe_epoch=self.universe_epoch,
|
105 |
+
universe_story=self.universe_story
|
106 |
+
)
|
107 |
+
|
108 |
+
async def generate_ending(self, story_beat: int, ending_type: str, current_scene: str, story_history: str) -> StoryTextResponse:
|
109 |
"""Génère un texte de fin approprié."""
|
110 |
+
system_template = """You are a story generator for a comic book adventure game.
|
111 |
+
You are generating a story in the following universe:
|
112 |
+
- Style: {universe_style}
|
113 |
+
- Genre: {universe_genre}
|
114 |
+
- Historical epoch: {universe_epoch}
|
115 |
+
|
116 |
+
Base universe story:
|
117 |
+
{universe_story}
|
118 |
+
|
119 |
+
Your task is to generate an epic {ending_type} ending for the story that:
|
120 |
+
1. Matches the universe's style and atmosphere
|
121 |
+
2. Provides a satisfying conclusion
|
122 |
+
3. Keeps the ending concise but impactful (max 15 words)
|
123 |
+
4. For victory: reveals the MacGuffin's power in a spectacular way
|
124 |
+
5. For death: creates a dramatic and fitting end for Sarah"""
|
125 |
+
|
126 |
+
human_template = """Current scene:
|
127 |
+
{current_scene}
|
128 |
+
|
129 |
+
Story history:
|
130 |
+
{story_history}
|
131 |
+
|
132 |
+
Generate the {ending_type} ending."""
|
133 |
+
|
134 |
+
ending_prompt = ChatPromptTemplate(
|
135 |
+
messages=[
|
136 |
+
SystemMessagePromptTemplate.from_template(system_template),
|
137 |
+
HumanMessagePromptTemplate.from_template(human_template)
|
138 |
+
]
|
139 |
+
)
|
140 |
+
|
141 |
+
response = await self.mistral_client.generate(
|
142 |
+
ending_prompt,
|
143 |
ending_type=ending_type,
|
144 |
current_scene=current_scene,
|
145 |
+
story_history=story_history,
|
146 |
+
universe_style=self.universe_style,
|
147 |
+
universe_genre=self.universe_genre,
|
148 |
+
universe_epoch=self.universe_epoch,
|
149 |
+
universe_story=self.universe_story
|
150 |
)
|
151 |
+
|
152 |
+
cleaned_text = self._custom_parser(response)
|
153 |
+
return StoryTextResponse(story_text=cleaned_text)
|
|
|
|
server/core/generators/universe_generator.py
ADDED
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
import random
|
3 |
+
from pathlib import Path
|
4 |
+
from langchain.prompts import ChatPromptTemplate, SystemMessagePromptTemplate, HumanMessagePromptTemplate
|
5 |
+
|
6 |
+
from core.generators.base_generator import BaseGenerator
|
7 |
+
from core.prompts.system import STORY_RULES
|
8 |
+
|
9 |
+
class UniverseGenerator(BaseGenerator):
|
10 |
+
"""Générateur pour les univers alternatifs."""
|
11 |
+
|
12 |
+
def _create_prompt(self) -> ChatPromptTemplate:
|
13 |
+
system_template = """You are a creative writing assistant specialized in comic book universes.
|
14 |
+
Your task is to rewrite a story while keeping its exact structure and beats, but transposing it into a different universe."""
|
15 |
+
|
16 |
+
human_template = """Transform the following story into a new universe with these parameters:
|
17 |
+
- Visual style: {style_name} (inspired by artists like {artists} with works such as {works})
|
18 |
+
Style description: {style_description}
|
19 |
+
|
20 |
+
- Genre: {genre}
|
21 |
+
- Historical epoch: {epoch}
|
22 |
+
|
23 |
+
IMPORTANT INSTRUCTIONS:
|
24 |
+
1. Keep the exact same story structure
|
25 |
+
2. Keep the same dramatic tension and progression
|
26 |
+
3. Only change the setting, atmosphere, and universe-specific elements to match the new parameters
|
27 |
+
4. Keep Sarah as the main character, but adapt her role to fit the new universe
|
28 |
+
5. The MacGuffin should still be central to the plot, but its nature can change to fit the new universe
|
29 |
+
|
30 |
+
Base story to transform:
|
31 |
+
{base_story}"""
|
32 |
+
|
33 |
+
return ChatPromptTemplate(
|
34 |
+
messages=[
|
35 |
+
SystemMessagePromptTemplate.from_template(system_template),
|
36 |
+
HumanMessagePromptTemplate.from_template(human_template)
|
37 |
+
]
|
38 |
+
)
|
39 |
+
|
40 |
+
def _load_universe_styles(self):
|
41 |
+
"""Charge les styles, genres et époques depuis le fichier JSON."""
|
42 |
+
try:
|
43 |
+
current_dir = Path(__file__).parent.parent
|
44 |
+
styles_path = current_dir / "styles" / "universe_styles.json"
|
45 |
+
|
46 |
+
if not styles_path.exists():
|
47 |
+
raise FileNotFoundError(f"Universe styles file not found at {styles_path}")
|
48 |
+
|
49 |
+
with open(styles_path, "r", encoding="utf-8") as f:
|
50 |
+
return json.load(f)
|
51 |
+
except Exception as e:
|
52 |
+
raise ValueError(f"Failed to load universe styles: {str(e)}")
|
53 |
+
|
54 |
+
def _get_random_elements(self):
|
55 |
+
"""Récupère un style, un genre et une époque aléatoires."""
|
56 |
+
data = self._load_universe_styles()
|
57 |
+
|
58 |
+
if not all(key in data for key in ["styles", "genres", "epochs"]):
|
59 |
+
raise ValueError("Missing required sections in universe_styles.json")
|
60 |
+
|
61 |
+
style = random.choice(data["styles"])
|
62 |
+
genre = random.choice(data["genres"])
|
63 |
+
epoch = random.choice(data["epochs"])
|
64 |
+
|
65 |
+
return style, genre, epoch
|
66 |
+
|
67 |
+
def _custom_parser(self, response_content: str) -> str:
|
68 |
+
"""Parse la réponse. Dans ce cas, on retourne simplement le texte."""
|
69 |
+
return response_content.strip()
|
70 |
+
|
71 |
+
async def generate(self) -> str:
|
72 |
+
"""Génère un nouvel univers basé sur des éléments aléatoires."""
|
73 |
+
style, genre, epoch = self._get_random_elements()
|
74 |
+
base_story = STORY_RULES
|
75 |
+
|
76 |
+
# Préparer les listes d'artistes et d'œuvres
|
77 |
+
artists = ", ".join([ref["artist"] for ref in style["references"]])
|
78 |
+
works = ", ".join([work for ref in style["references"] for work in ref["works"]])
|
79 |
+
|
80 |
+
return await super().generate(
|
81 |
+
style_name=style["name"],
|
82 |
+
artists=artists,
|
83 |
+
works=works,
|
84 |
+
style_description=style["description"],
|
85 |
+
genre=genre,
|
86 |
+
epoch=epoch,
|
87 |
+
base_story=base_story
|
88 |
+
)
|
server/core/prompts/system.py
CHANGED
@@ -1,59 +1,59 @@
|
|
1 |
-
SARAH_VISUAL_DESCRIPTION = "(Sarah
|
2 |
|
3 |
SARAH_DESCRIPTION = """
|
4 |
-
Sarah
|
5 |
-
- Sarah est en quête de découvrir la vérité derrière le pouvoir de l'amulette et sa connexion à son passé.
|
6 |
-
|
7 |
"""
|
8 |
|
9 |
FORMATTING_RULES = """
|
10 |
-
FORMATTING_RULES (
|
11 |
-
-
|
12 |
-
-
|
13 |
"""
|
14 |
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
|
27 |
-
|
28 |
-
- Le MacGuffin est une présence mystérieuse et constante
|
29 |
-
- L'environnement est plein de merveilles (créatures mécaniques, ruines industrielles, pièges à vapeur)
|
30 |
-
- Se concentrer sur l'aventure et l'intrigue
|
31 |
|
32 |
-
|
33 |
-
|
34 |
-
- Suivre l'influence du MacGuffin comme une présence constante
|
35 |
-
- Construire l'intrigue à travers la narration environnementale
|
36 |
|
37 |
-
|
38 |
-
|
39 |
-
Ne jamais répéter les mêmes descriptions ou situations. Pas plus de 15 mots.
|
40 |
|
41 |
-
|
42 |
-
- story_beat 0 : Introduction mettant en place l'atmosphère steampunk
|
43 |
-
- story_beat 1-2 : Exploration précoce et découverte d'éléments mécaniques
|
44 |
-
- story_beat 3-5 : Complications et mystères plus profonds
|
45 |
-
- story_beat 6+ : Révélations menant à un triomphe potentiel ou à un échec
|
46 |
|
47 |
-
|
48 |
-
- La plupart des segments doivent faire allusion au pouvoir du MacGuffin
|
49 |
-
- Utiliser des indices forts UNIQUEMENT dans des moments clés (comme des temples anciens, des tempêtes mécaniques)
|
50 |
-
- NE JAMAIS révéler le plein pouvoir du MacGuffin avant le climax, c'est une limite STRICTE
|
51 |
-
- Utiliser des indices subtils dans les havres de paix
|
52 |
-
- NE JAMAIS mentionner le pouvoir du MacGuffin explicitement dans les choix ou l'histoire
|
53 |
-
- NE JAMAIS mentionner l'heure ou le lieu dans l'histoire de cette manière : [18:00 - Clairière enchantée au cœur d'Eldoria]
|
54 |
"""
|
55 |
|
56 |
|
57 |
-
#
|
58 |
-
|
59 |
|
|
|
1 |
+
SARAH_VISUAL_DESCRIPTION = "(Sarah is a young woman in her late twenties with short dark hair, wearing a mysterious amulet around her neck. Her blue eyes hide untold secrets.)"
|
2 |
|
3 |
SARAH_DESCRIPTION = """
|
4 |
+
Sarah is a young woman in her late twenties with short dark hair, wearing a mysterious amulet around her neck. Her blue eyes hide untold secrets.
|
|
|
|
|
5 |
"""
|
6 |
|
7 |
FORMATTING_RULES = """
|
8 |
+
FORMATTING_RULES (MANDATORY)
|
9 |
+
- The story must consist ONLY of sentences
|
10 |
+
- NEVER USE BOLD FOR ANYTHING
|
11 |
"""
|
12 |
|
13 |
+
NARRATIVE_STRUCTURE = """
|
14 |
+
Key elements of the story:
|
15 |
+
- The MacGuffin is a mysterious and constant presence
|
16 |
+
- The environment is full of wonders (creatures, ruins, traps)
|
17 |
+
- Focus on adventure and intrigue
|
18 |
+
|
19 |
+
Key elements:
|
20 |
+
- Keep segments concise and impactful
|
21 |
+
- The MacGuffin is a pretext for developing a plot. It is almost always a material object and generally remains mysterious throughout the narrative, its description is vague and unimportant. The principle dates back to the early days of cinema, but the term is associated with Alfred Hitchcock, who redefined, popularized, and implemented it in several of his films. The object itself is rarely used, only its retrieval matters.
|
22 |
+
- The MacGuffin is a constant presence in the story
|
23 |
+
- Build intrigue through environmental storytelling
|
24 |
+
|
25 |
+
IMPORTANT:
|
26 |
+
Each story segment MUST be unique and advance the plot.
|
27 |
+
Never repeat the same descriptions or situations. No more than 15 words.
|
28 |
+
|
29 |
+
STORY PROGRESSION:
|
30 |
+
- story_beat 0: Introduction setting the steampunk atmosphere
|
31 |
+
- story_beat 1-2: Early exploration and discovery of mechanical elements
|
32 |
+
- story_beat 3-5: Complications and deeper mysteries
|
33 |
+
- story_beat 6+: Revelations leading to potential triumph or failure
|
34 |
+
|
35 |
+
IMPORTANT RULES FOR THE MACGUFFIN (MANDATORY):
|
36 |
+
- Most segments must hint at the power of the MacGuffin
|
37 |
+
- Use strong clues ONLY at key moments
|
38 |
+
- NEVER reveal the full power of the MacGuffin before the climax, this is a STRICT limit
|
39 |
+
- Use subtle clues in safe havens
|
40 |
+
- NEVER mention the power of the MacGuffin explicitly in choices or the story
|
41 |
+
- NEVER mention time or place in the story in this manner: [18:00 - a road]
|
42 |
+
"""
|
43 |
|
44 |
+
STORY_RULES = """
|
|
|
|
|
|
|
45 |
|
46 |
+
You are a steampunk adventure story generator. You create a branching narrative about Sarah, a seeker of ancient truths.
|
47 |
+
You narrate an epic where Sarah must navigate through industrial and mysterious lands. It's a comic book story.
|
|
|
|
|
48 |
|
49 |
+
In a world where steam and intrigue intertwine, Sarah embarks on a quest to discover the origins of a powerful MacGuffin she inherited. Legends say it holds the key to a forgotten realm.
|
50 |
+
You must make decisions to uncover its secrets. You have ventured into the mechanical city to find the first clue. If you fail, the power of the MacGuffin will remain dormant. Time is of the essence, and every choice shapes your destiny.
|
|
|
51 |
|
52 |
+
If you retrieve the MacGuffin, you will reveal a hidden world. AND YOU WIN THE GAME.
|
|
|
|
|
|
|
|
|
53 |
|
54 |
+
The story must be atmospheric, magical, and focus on adventure and discovery. Each segment must advance the plot and never repeat previous descriptions or situations.
|
|
|
|
|
|
|
|
|
|
|
|
|
55 |
"""
|
56 |
|
57 |
|
58 |
+
# The MacGuffin is a pretext for developing a plot. It is almost always a material object and generally remains mysterious throughout the narrative, its description is vague and unimportant. The principle dates back to the early days of cinema, but the term is associated with Alfred Hitchcock, who redefined, popularized, and implemented it in several of his films. The object itself is rarely used, only its retrieval matters.
|
|
|
59 |
|
server/core/prompts/text_prompts.py
CHANGED
@@ -25,7 +25,7 @@ IMPORTANT RULES FOR STORY TEXT:
|
|
25 |
- Never tell that you are using 15 words or any reference to it
|
26 |
|
27 |
IMPORTANT RULES FOR STORY ENDINGS:
|
28 |
-
- If Sarah dies, describe her final moments in a way that fits the current situation (combat,
|
29 |
- If Sarah achieves victory, describe her triumph in a way that fits how she won (finding her sister, defeating AI, etc.)
|
30 |
- Keep the ending text dramatic and impactful
|
31 |
- The ending should feel like a natural conclusion to the current scene
|
|
|
25 |
- Never tell that you are using 15 words or any reference to it
|
26 |
|
27 |
IMPORTANT RULES FOR STORY ENDINGS:
|
28 |
+
- If Sarah dies, describe her final moments in a way that fits the current situation (combat, etc.)
|
29 |
- If Sarah achieves victory, describe her triumph in a way that fits how she won (finding her sister, defeating AI, etc.)
|
30 |
- Keep the ending text dramatic and impactful
|
31 |
- The ending should feel like a natural conclusion to the current scene
|
server/core/session_manager.py
CHANGED
@@ -3,28 +3,39 @@ import time
|
|
3 |
from .game_logic import GameState
|
4 |
|
5 |
class SessionManager:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
def __init__(self, session_timeout: int = 3600):
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
self.last_activity: Dict[str, float] = {}
|
14 |
-
self.session_timeout = session_timeout
|
15 |
|
16 |
-
def create_session(self, session_id: str
|
17 |
"""Create a new game session.
|
18 |
|
19 |
Args:
|
20 |
session_id (str): Unique identifier for the session
|
|
|
21 |
|
22 |
Returns:
|
23 |
GameState: The newly created game state
|
24 |
"""
|
25 |
-
|
|
|
|
|
26 |
self.sessions[session_id] = game_state
|
27 |
self.last_activity[session_id] = time.time()
|
|
|
28 |
return game_state
|
29 |
|
30 |
def get_session(self, session_id: str) -> GameState | None:
|
@@ -36,15 +47,22 @@ class SessionManager:
|
|
36 |
Returns:
|
37 |
GameState | None: The game state if found and not expired, None otherwise
|
38 |
"""
|
|
|
|
|
|
|
39 |
if session_id in self.sessions:
|
40 |
# Check if session has expired
|
41 |
if time.time() - self.last_activity[session_id] > self.session_timeout:
|
|
|
42 |
self.cleanup_session(session_id)
|
43 |
return None
|
44 |
|
45 |
# Update last activity time
|
46 |
self.last_activity[session_id] = time.time()
|
|
|
47 |
return self.sessions[session_id]
|
|
|
|
|
48 |
return None
|
49 |
|
50 |
def cleanup_session(self, session_id: str):
|
@@ -79,4 +97,10 @@ class SessionManager:
|
|
79 |
session = self.get_session(session_id)
|
80 |
if session is None:
|
81 |
session = self.create_session(session_id)
|
82 |
-
return session
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3 |
from .game_logic import GameState
|
4 |
|
5 |
class SessionManager:
|
6 |
+
_instance = None
|
7 |
+
|
8 |
+
def __new__(cls, *args, **kwargs):
|
9 |
+
if cls._instance is None:
|
10 |
+
print("Creating new SessionManager instance")
|
11 |
+
cls._instance = super().__new__(cls)
|
12 |
+
cls._instance._initialized = False
|
13 |
+
return cls._instance
|
14 |
+
|
15 |
def __init__(self, session_timeout: int = 3600):
|
16 |
+
if not self._initialized:
|
17 |
+
print("Initializing SessionManager singleton")
|
18 |
+
self.sessions: Dict[str, GameState] = {}
|
19 |
+
self.last_activity: Dict[str, float] = {}
|
20 |
+
self.session_timeout = session_timeout
|
21 |
+
self._initialized = True
|
|
|
|
|
22 |
|
23 |
+
def create_session(self, session_id: str, game_state: GameState = None):
|
24 |
"""Create a new game session.
|
25 |
|
26 |
Args:
|
27 |
session_id (str): Unique identifier for the session
|
28 |
+
game_state (GameState): Optional initial game state
|
29 |
|
30 |
Returns:
|
31 |
GameState: The newly created game state
|
32 |
"""
|
33 |
+
print(f"Creating session {session_id} in SessionManager singleton")
|
34 |
+
if game_state is None:
|
35 |
+
game_state = GameState()
|
36 |
self.sessions[session_id] = game_state
|
37 |
self.last_activity[session_id] = time.time()
|
38 |
+
print(f"Current sessions in SessionManager: {list(self.sessions.keys())}")
|
39 |
return game_state
|
40 |
|
41 |
def get_session(self, session_id: str) -> GameState | None:
|
|
|
47 |
Returns:
|
48 |
GameState | None: The game state if found and not expired, None otherwise
|
49 |
"""
|
50 |
+
print(f"Getting session {session_id} from SessionManager singleton")
|
51 |
+
print(f"Current sessions in SessionManager: {list(self.sessions.keys())}")
|
52 |
+
|
53 |
if session_id in self.sessions:
|
54 |
# Check if session has expired
|
55 |
if time.time() - self.last_activity[session_id] > self.session_timeout:
|
56 |
+
print(f"Session {session_id} has expired")
|
57 |
self.cleanup_session(session_id)
|
58 |
return None
|
59 |
|
60 |
# Update last activity time
|
61 |
self.last_activity[session_id] = time.time()
|
62 |
+
print(f"Session {session_id} found and active")
|
63 |
return self.sessions[session_id]
|
64 |
+
|
65 |
+
print(f"Session {session_id} not found")
|
66 |
return None
|
67 |
|
68 |
def cleanup_session(self, session_id: str):
|
|
|
97 |
session = self.get_session(session_id)
|
98 |
if session is None:
|
99 |
session = self.create_session(session_id)
|
100 |
+
return session
|
101 |
+
|
102 |
+
def delete_session(self, session_id: str):
|
103 |
+
"""Supprime une session."""
|
104 |
+
if session_id in self.sessions:
|
105 |
+
del self.sessions[session_id]
|
106 |
+
del self.last_activity[session_id]
|
server/core/state/__init__.py
DELETED
@@ -1,3 +0,0 @@
|
|
1 |
-
from core.state.game_state import GameState
|
2 |
-
|
3 |
-
__all__ = ['GameState']
|
|
|
|
|
|
|
|
server/core/story_generators.py
DELETED
@@ -1,223 +0,0 @@
|
|
1 |
-
from pydantic import BaseModel
|
2 |
-
from typing import List
|
3 |
-
import json
|
4 |
-
from langchain.output_parsers import PydanticOutputParser
|
5 |
-
from langchain.prompts import ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemplate
|
6 |
-
import asyncio
|
7 |
-
|
8 |
-
from core.prompts.system import SARAH_VISUAL_DESCRIPTION
|
9 |
-
from core.prompts.text_prompts import TEXT_GENERATOR_PROMPT, METADATA_GENERATOR_PROMPT, IMAGE_PROMPTS_GENERATOR_PROMPT
|
10 |
-
from services.mistral_client import MistralClient
|
11 |
-
from api.models import StoryTextResponse, StoryPromptsResponse, StoryMetadataResponse
|
12 |
-
|
13 |
-
class TextGenerator:
|
14 |
-
def __init__(self, mistral_client: MistralClient):
|
15 |
-
self.mistral_client = mistral_client
|
16 |
-
self.prompt = self._create_prompt()
|
17 |
-
|
18 |
-
def _create_prompt(self) -> ChatPromptTemplate:
|
19 |
-
human_template = """Story beat: {story_beat}
|
20 |
-
Radiation level: {radiation_level}
|
21 |
-
Current time: {current_time}
|
22 |
-
Current location: {current_location}
|
23 |
-
Previous choice: {previous_choice}
|
24 |
-
|
25 |
-
Story history:
|
26 |
-
{story_history}
|
27 |
-
|
28 |
-
Generate the next story segment following the format specified."""
|
29 |
-
|
30 |
-
return ChatPromptTemplate(
|
31 |
-
messages=[
|
32 |
-
SystemMessagePromptTemplate.from_template(TEXT_GENERATOR_PROMPT),
|
33 |
-
HumanMessagePromptTemplate.from_template(human_template)
|
34 |
-
]
|
35 |
-
)
|
36 |
-
|
37 |
-
def _create_ending_prompt(self) -> ChatPromptTemplate:
|
38 |
-
human_template = """Current scene: {current_scene}
|
39 |
-
|
40 |
-
Story history:
|
41 |
-
{story_history}
|
42 |
-
|
43 |
-
This is a {ending_type} ending. Generate a dramatic conclusion that fits the current situation.
|
44 |
-
The ending should feel like a natural continuation of the current scene."""
|
45 |
-
|
46 |
-
return ChatPromptTemplate(
|
47 |
-
messages=[
|
48 |
-
SystemMessagePromptTemplate.from_template(TEXT_GENERATOR_PROMPT),
|
49 |
-
HumanMessagePromptTemplate.from_template(human_template)
|
50 |
-
]
|
51 |
-
)
|
52 |
-
|
53 |
-
def _clean_story_text(self, text: str) -> str:
|
54 |
-
"""Nettoie le texte des métadonnées et autres suffixes."""
|
55 |
-
text = text.replace("\n", " ").strip()
|
56 |
-
text = text.split("Radiation level:")[0].strip()
|
57 |
-
text = text.split("RADIATION:")[0].strip()
|
58 |
-
text = text.split("[")[0].strip() # Supprimer les métadonnées entre crochets
|
59 |
-
return text
|
60 |
-
|
61 |
-
def _custom_parser(self, response_content: str) -> StoryTextResponse:
|
62 |
-
"""Parse la réponse et gère les erreurs."""
|
63 |
-
try:
|
64 |
-
# Essayer de parser directement le JSON
|
65 |
-
data = json.loads(response_content)
|
66 |
-
# Nettoyer le texte avant de créer la réponse
|
67 |
-
if 'story_text' in data:
|
68 |
-
data['story_text'] = self._clean_story_text(data['story_text'])
|
69 |
-
return StoryTextResponse(**data)
|
70 |
-
except (json.JSONDecodeError, ValueError):
|
71 |
-
# Si le parsing échoue, extraire le texte directement
|
72 |
-
cleaned_text = self._clean_story_text(response_content.strip())
|
73 |
-
return StoryTextResponse(story_text=cleaned_text)
|
74 |
-
|
75 |
-
async def generate(self, story_beat: int, radiation_level: int, current_time: str, current_location: str, previous_choice: str, story_history: str) -> StoryTextResponse:
|
76 |
-
"""Génère le texte de l'histoire."""
|
77 |
-
messages = self.prompt.format_messages(
|
78 |
-
story_beat=story_beat,
|
79 |
-
radiation_level=radiation_level,
|
80 |
-
current_time=current_time,
|
81 |
-
current_location=current_location,
|
82 |
-
previous_choice=previous_choice,
|
83 |
-
story_history=story_history
|
84 |
-
)
|
85 |
-
|
86 |
-
return await self.mistral_client.generate(
|
87 |
-
messages=messages,
|
88 |
-
custom_parser=self._custom_parser
|
89 |
-
)
|
90 |
-
|
91 |
-
async def generate_ending(self, story_beat: int, ending_type: str, current_scene: str, story_history: str) -> StoryTextResponse:
|
92 |
-
"""Génère un texte de fin approprié basé sur la situation actuelle."""
|
93 |
-
prompt = self._create_ending_prompt()
|
94 |
-
messages = prompt.format_messages(
|
95 |
-
ending_type=ending_type,
|
96 |
-
current_scene=current_scene,
|
97 |
-
story_history=story_history
|
98 |
-
)
|
99 |
-
|
100 |
-
return await self.mistral_client.generate(
|
101 |
-
messages=messages,
|
102 |
-
custom_parser=self._custom_parser
|
103 |
-
)
|
104 |
-
|
105 |
-
class ImagePromptsGenerator:
|
106 |
-
def __init__(self, mistral_client: MistralClient):
|
107 |
-
self.mistral_client = mistral_client
|
108 |
-
self.prompt = self._create_prompt()
|
109 |
-
|
110 |
-
def _create_prompt(self) -> ChatPromptTemplate:
|
111 |
-
human_template = """Story text: {story_text}
|
112 |
-
|
113 |
-
Generate panel descriptions following the format specified."""
|
114 |
-
|
115 |
-
return ChatPromptTemplate(
|
116 |
-
messages=[
|
117 |
-
SystemMessagePromptTemplate.from_template(IMAGE_PROMPTS_GENERATOR_PROMPT),
|
118 |
-
HumanMessagePromptTemplate.from_template(human_template)
|
119 |
-
]
|
120 |
-
)
|
121 |
-
|
122 |
-
def enrich_prompt(self, prompt: str) -> str:
|
123 |
-
"""Add Sarah's visual description to prompts that mention her."""
|
124 |
-
if "sarah" in prompt.lower() and SARAH_VISUAL_DESCRIPTION not in prompt:
|
125 |
-
return f"{prompt} {SARAH_VISUAL_DESCRIPTION}"
|
126 |
-
return prompt
|
127 |
-
|
128 |
-
def _custom_parser(self, response_content: str) -> StoryPromptsResponse:
|
129 |
-
"""Parse la réponse et gère les erreurs."""
|
130 |
-
try:
|
131 |
-
# Essayer de parser directement le JSON
|
132 |
-
data = json.loads(response_content)
|
133 |
-
return StoryPromptsResponse(**data)
|
134 |
-
except (json.JSONDecodeError, ValueError):
|
135 |
-
# Si le parsing échoue, extraire les prompts en ignorant les lignes de syntaxe JSON
|
136 |
-
prompts = []
|
137 |
-
for line in response_content.split("\n"):
|
138 |
-
line = line.strip()
|
139 |
-
# Ignorer les lignes vides, la syntaxe JSON et les lignes contenant image_prompts
|
140 |
-
if (not line or
|
141 |
-
line in ["{", "}", "[", "]"] or
|
142 |
-
"image_prompts" in line.lower() or
|
143 |
-
"image\\_prompts" in line or
|
144 |
-
line.startswith('"') and line.endswith('",') and len(line) < 5):
|
145 |
-
continue
|
146 |
-
# Nettoyer la ligne des caractères JSON et d'échappement
|
147 |
-
line = line.strip('",')
|
148 |
-
line = line.replace('\\"', '"').replace("\\'", "'").replace("\\_", "_")
|
149 |
-
if line:
|
150 |
-
prompts.append(line)
|
151 |
-
# Limiter à 4 prompts maximum
|
152 |
-
prompts = prompts[:4]
|
153 |
-
return StoryPromptsResponse(image_prompts=prompts)
|
154 |
-
|
155 |
-
async def generate(self, story_text: str) -> StoryPromptsResponse:
|
156 |
-
"""Génère les prompts d'images basés sur le texte de l'histoire."""
|
157 |
-
messages = self.prompt.format_messages(story_text=story_text)
|
158 |
-
|
159 |
-
response = await self.mistral_client.generate(
|
160 |
-
messages=messages,
|
161 |
-
custom_parser=self._custom_parser
|
162 |
-
)
|
163 |
-
|
164 |
-
# Enrichir les prompts avec la description de Sarah
|
165 |
-
response.image_prompts = [self.enrich_prompt(prompt) for prompt in response.image_prompts]
|
166 |
-
return response
|
167 |
-
|
168 |
-
class MetadataGenerator:
|
169 |
-
def __init__(self, mistral_client: MistralClient):
|
170 |
-
self.mistral_client = mistral_client
|
171 |
-
self.prompt = self._create_prompt()
|
172 |
-
|
173 |
-
def _create_prompt(self, error_feedback: str = None) -> ChatPromptTemplate:
|
174 |
-
human_template = """Story text: {story_text}
|
175 |
-
Current time: {current_time}
|
176 |
-
Current location: {current_location}
|
177 |
-
Story beat: {story_beat}
|
178 |
-
{error_feedback}
|
179 |
-
|
180 |
-
Generate the metadata following the format specified."""
|
181 |
-
|
182 |
-
return ChatPromptTemplate(
|
183 |
-
messages=[
|
184 |
-
SystemMessagePromptTemplate.from_template(METADATA_GENERATOR_PROMPT),
|
185 |
-
HumanMessagePromptTemplate.from_template(human_template)
|
186 |
-
]
|
187 |
-
)
|
188 |
-
|
189 |
-
def _custom_parser(self, response_content: str) -> StoryMetadataResponse:
|
190 |
-
"""Parse la réponse et gère les erreurs."""
|
191 |
-
try:
|
192 |
-
# Essayer de parser directement le JSON
|
193 |
-
data = json.loads(response_content)
|
194 |
-
|
195 |
-
# Vérifier que les choix sont valides selon les règles
|
196 |
-
is_ending = data.get('is_victory', False) or data.get('is_death', False)
|
197 |
-
choices = data.get('choices', [])
|
198 |
-
|
199 |
-
if is_ending and len(choices) != 0:
|
200 |
-
raise ValueError('For victory/death, choices must be empty')
|
201 |
-
if not is_ending and len(choices) != 2:
|
202 |
-
raise ValueError('For normal progression, must have exactly 2 choices')
|
203 |
-
|
204 |
-
return StoryMetadataResponse(**data)
|
205 |
-
except json.JSONDecodeError:
|
206 |
-
raise ValueError('Invalid JSON format. Please provide a valid JSON object.')
|
207 |
-
except ValueError as e:
|
208 |
-
raise ValueError(str(e))
|
209 |
-
|
210 |
-
async def generate(self, story_text: str, current_time: str, current_location: str, story_beat: int) -> StoryMetadataResponse:
|
211 |
-
"""Génère les métadonnées basées sur le texte de l'histoire."""
|
212 |
-
messages = self.prompt.format_messages(
|
213 |
-
story_text=story_text,
|
214 |
-
current_time=current_time,
|
215 |
-
current_location=current_location,
|
216 |
-
story_beat=story_beat,
|
217 |
-
error_feedback=""
|
218 |
-
)
|
219 |
-
|
220 |
-
return await self.mistral_client.generate(
|
221 |
-
messages=messages,
|
222 |
-
custom_parser=self._custom_parser
|
223 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/core/styles/comic_styles.json
DELETED
@@ -1,259 +0,0 @@
|
|
1 |
-
{
|
2 |
-
"styles": [
|
3 |
-
{
|
4 |
-
"name": "Franco-Belge Ligne Claire",
|
5 |
-
"description": "Style épuré avec des lignes nettes et des couleurs plates",
|
6 |
-
"references": [
|
7 |
-
{
|
8 |
-
"artist": "Hergé",
|
9 |
-
"works": ["Tintin", "Les Aventures de Jo, Zette et Jocko"]
|
10 |
-
},
|
11 |
-
{
|
12 |
-
"artist": "Edgar P. Jacobs",
|
13 |
-
"works": ["Blake et Mortimer"]
|
14 |
-
},
|
15 |
-
{
|
16 |
-
"artist": "Yves Chaland",
|
17 |
-
"works": ["Freddy Lombard", "Bob Fish"]
|
18 |
-
},
|
19 |
-
{
|
20 |
-
"artist": "Joost Swarte",
|
21 |
-
"works": ["Modern Art", "L'Art Moderne"]
|
22 |
-
}
|
23 |
-
]
|
24 |
-
},
|
25 |
-
{
|
26 |
-
"name": "Science-Fiction Européenne",
|
27 |
-
"description": "Style visionnaire mêlant précision technique et onirisme",
|
28 |
-
"references": [
|
29 |
-
{
|
30 |
-
"artist": "Moebius (Jean Giraud)",
|
31 |
-
"works": ["L'Incal", "Arzak", "Le Garage Hermétique", "Aedena"]
|
32 |
-
},
|
33 |
-
{
|
34 |
-
"artist": "Philippe Druillet",
|
35 |
-
"works": ["Lone Sloane", "Salammbô", "Delirius"]
|
36 |
-
},
|
37 |
-
{
|
38 |
-
"artist": "François Schuiten",
|
39 |
-
"works": ["Les Cités Obscures", "La Fièvre d'Urbicande"]
|
40 |
-
}
|
41 |
-
]
|
42 |
-
},
|
43 |
-
{
|
44 |
-
"name": "Comics Américain Classique",
|
45 |
-
"description": "Style dynamique avec des couleurs vives et des ombrages marqués",
|
46 |
-
"references": [
|
47 |
-
{
|
48 |
-
"artist": "Jack Kirby",
|
49 |
-
"works": ["Fantastic Four", "New Gods", "Captain America"]
|
50 |
-
},
|
51 |
-
{
|
52 |
-
"artist": "Steve Ditko",
|
53 |
-
"works": ["Spider-Man", "Doctor Strange", "Mr. A"]
|
54 |
-
},
|
55 |
-
{
|
56 |
-
"artist": "Neal Adams",
|
57 |
-
"works": ["Batman", "Green Lantern/Green Arrow", "X-Men"]
|
58 |
-
},
|
59 |
-
{
|
60 |
-
"artist": "John Buscema",
|
61 |
-
"works": ["Conan the Barbarian", "Silver Surfer", "The Avengers"]
|
62 |
-
}
|
63 |
-
]
|
64 |
-
},
|
65 |
-
{
|
66 |
-
"name": "Comics Moderne",
|
67 |
-
"description": "Style contemporain avec techniques mixtes et mise en page innovante",
|
68 |
-
"references": [
|
69 |
-
{
|
70 |
-
"artist": "Alex Ross",
|
71 |
-
"works": ["Kingdom Come", "Marvels", "Justice"]
|
72 |
-
},
|
73 |
-
{
|
74 |
-
"artist": "J.H. Williams III",
|
75 |
-
"works": ["Promethea", "Batwoman", "The Sandman: Overture"]
|
76 |
-
},
|
77 |
-
{
|
78 |
-
"artist": "Sean Murphy",
|
79 |
-
"works": ["Batman: White Knight", "Tokyo Ghost", "Punk Rock Jesus"]
|
80 |
-
}
|
81 |
-
]
|
82 |
-
},
|
83 |
-
{
|
84 |
-
"name": "Manga Seinen",
|
85 |
-
"description": "Style mature avec des détails complexes et des contrastes forts",
|
86 |
-
"references": [
|
87 |
-
{
|
88 |
-
"artist": "Katsuhiro Otomo",
|
89 |
-
"works": ["Akira", "Domu", "Memories"]
|
90 |
-
},
|
91 |
-
{
|
92 |
-
"artist": "Tsutomu Nihei",
|
93 |
-
"works": ["Blame!", "Knights of Sidonia", "Biomega"]
|
94 |
-
},
|
95 |
-
{
|
96 |
-
"artist": "Takehiko Inoue",
|
97 |
-
"works": ["Vagabond", "Real", "Slam Dunk"]
|
98 |
-
},
|
99 |
-
{
|
100 |
-
"artist": "Naoki Urasawa",
|
101 |
-
"works": ["Monster", "20th Century Boys", "Pluto"]
|
102 |
-
}
|
103 |
-
]
|
104 |
-
},
|
105 |
-
{
|
106 |
-
"name": "Manga Horror",
|
107 |
-
"description": "Style détaillé avec emphase sur l'horreur psychologique et corporelle",
|
108 |
-
"references": [
|
109 |
-
{
|
110 |
-
"artist": "Junji Ito",
|
111 |
-
"works": ["Uzumaki", "Tomie", "Gyo"]
|
112 |
-
},
|
113 |
-
{
|
114 |
-
"artist": "Suehiro Maruo",
|
115 |
-
"works": [
|
116 |
-
"Mr. Arashi's Amazing Freak Show",
|
117 |
-
"The Strange Tale of Panorama Island"
|
118 |
-
]
|
119 |
-
},
|
120 |
-
{
|
121 |
-
"artist": "Hideshi Hino",
|
122 |
-
"works": ["Hell Baby", "Panorama of Hell"]
|
123 |
-
}
|
124 |
-
]
|
125 |
-
},
|
126 |
-
{
|
127 |
-
"name": "BD Alternative",
|
128 |
-
"description": "Style expressif avec des techniques mixtes et une approche artistique unique",
|
129 |
-
"references": [
|
130 |
-
{
|
131 |
-
"artist": "Dave McKean",
|
132 |
-
"works": ["Arkham Asylum", "Signal to Noise", "Cages"]
|
133 |
-
},
|
134 |
-
{
|
135 |
-
"artist": "Bill Sienkiewicz",
|
136 |
-
"works": ["Elektra: Assassin", "Stray Toasters", "New Mutants"]
|
137 |
-
},
|
138 |
-
{
|
139 |
-
"artist": "Lorenzo Mattotti",
|
140 |
-
"works": ["Feux", "Le Bruit du givre", "Stigmates"]
|
141 |
-
},
|
142 |
-
{
|
143 |
-
"artist": "Kent Williams",
|
144 |
-
"works": ["Tell Me, Dark", "The Fountain", "Blood: A Tale"]
|
145 |
-
}
|
146 |
-
]
|
147 |
-
},
|
148 |
-
{
|
149 |
-
"name": "Noir et Blanc Expressionniste",
|
150 |
-
"description": "Style contrasté avec des noirs profonds et des blancs éclatants",
|
151 |
-
"references": [
|
152 |
-
{
|
153 |
-
"artist": "Frank Miller",
|
154 |
-
"works": ["Sin City", "300", "Ronin"]
|
155 |
-
},
|
156 |
-
{
|
157 |
-
"artist": "Alberto Breccia",
|
158 |
-
"works": ["Mort Cinder", "Perramus", "Los Mitos de Cthulhu"]
|
159 |
-
},
|
160 |
-
{
|
161 |
-
"artist": "Jacques Tardi",
|
162 |
-
"works": [
|
163 |
-
"C'était la guerre des tranchées",
|
164 |
-
"Le Cri du peuple",
|
165 |
-
"Adèle Blanc-Sec"
|
166 |
-
]
|
167 |
-
},
|
168 |
-
{
|
169 |
-
"artist": "José Muñoz",
|
170 |
-
"works": ["Alack Sinner", "Le Bar à Joe"]
|
171 |
-
}
|
172 |
-
]
|
173 |
-
},
|
174 |
-
{
|
175 |
-
"name": "Style Numérique Moderne",
|
176 |
-
"description": "Style contemporain utilisant les techniques digitales",
|
177 |
-
"references": [
|
178 |
-
{
|
179 |
-
"artist": "Bastien Vivès",
|
180 |
-
"works": ["Polina", "Le Goût du chlore", "Une sœur"]
|
181 |
-
},
|
182 |
-
{
|
183 |
-
"artist": "Bengal",
|
184 |
-
"works": ["Naja", "Luminae", "Death or Glory"]
|
185 |
-
},
|
186 |
-
{
|
187 |
-
"artist": "Claire Wendling",
|
188 |
-
"works": ["Les Lumières de l'Amalou", "Desk", "Iguana Bay"]
|
189 |
-
},
|
190 |
-
{
|
191 |
-
"artist": "Boulet",
|
192 |
-
"works": ["Notes", "La Page Blanche"]
|
193 |
-
}
|
194 |
-
]
|
195 |
-
},
|
196 |
-
{
|
197 |
-
"name": "Post-Apocalyptique",
|
198 |
-
"description": "Style rugueux avec des atmosphères sombres et des environnements dévastés",
|
199 |
-
"references": [
|
200 |
-
{
|
201 |
-
"artist": "Enki Bilal",
|
202 |
-
"works": ["La Trilogie Nikopol", "Animal'z", "Bug"]
|
203 |
-
},
|
204 |
-
{
|
205 |
-
"artist": "Juan Giménez",
|
206 |
-
"works": [
|
207 |
-
"La Caste des Méta-Barons",
|
208 |
-
"Le Quatrième Pouvoir",
|
209 |
-
"Leo Roa"
|
210 |
-
]
|
211 |
-
},
|
212 |
-
{
|
213 |
-
"artist": "Geof Darrow",
|
214 |
-
"works": ["Hard Boiled", "Shaolin Cowboy", "Big Guy and Rusty"]
|
215 |
-
},
|
216 |
-
{
|
217 |
-
"artist": "Simon Bisley",
|
218 |
-
"works": ["Slaine", "Lobo", "Heavy Metal"]
|
219 |
-
}
|
220 |
-
]
|
221 |
-
},
|
222 |
-
{
|
223 |
-
"name": "Underground Américain",
|
224 |
-
"description": "Style brut et provocateur avec une approche contre-culturelle",
|
225 |
-
"references": [
|
226 |
-
{
|
227 |
-
"artist": "Robert Crumb",
|
228 |
-
"works": ["Zap Comix", "Fritz the Cat", "Mr. Natural"]
|
229 |
-
},
|
230 |
-
{
|
231 |
-
"artist": "Spain Rodriguez",
|
232 |
-
"works": ["Trashman", "Mean Bitch Thrills"]
|
233 |
-
},
|
234 |
-
{
|
235 |
-
"artist": "Gilbert Shelton",
|
236 |
-
"works": ["The Fabulous Furry Freak Brothers", "Wonder Wart-Hog"]
|
237 |
-
}
|
238 |
-
]
|
239 |
-
},
|
240 |
-
{
|
241 |
-
"name": "Nouveau Roman Graphique",
|
242 |
-
"description": "Style intimiste avec une approche narrative personnelle",
|
243 |
-
"references": [
|
244 |
-
{
|
245 |
-
"artist": "Chris Ware",
|
246 |
-
"works": ["Jimmy Corrigan", "Building Stories", "Rusty Brown"]
|
247 |
-
},
|
248 |
-
{
|
249 |
-
"artist": "Daniel Clowes",
|
250 |
-
"works": ["Ghost World", "David Boring", "Patience"]
|
251 |
-
},
|
252 |
-
{
|
253 |
-
"artist": "Adrian Tomine",
|
254 |
-
"works": ["Killing and Dying", "Shortcomings", "Optic Nerve"]
|
255 |
-
}
|
256 |
-
]
|
257 |
-
}
|
258 |
-
]
|
259 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/core/styles/universe_styles.json
ADDED
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"styles": [
|
3 |
+
{
|
4 |
+
"name": "Franco-Belge Ligne Claire",
|
5 |
+
"description": "Style épuré avec des lignes nettes et des couleurs plates",
|
6 |
+
"references": [
|
7 |
+
{
|
8 |
+
"artist": "Hergé",
|
9 |
+
"works": ["Tintin", "Les Aventures de Jo, Zette et Jocko"]
|
10 |
+
},
|
11 |
+
{
|
12 |
+
"artist": "Edgar P. Jacobs",
|
13 |
+
"works": ["Blake et Mortimer"]
|
14 |
+
},
|
15 |
+
{
|
16 |
+
"artist": "Yves Chaland",
|
17 |
+
"works": ["Freddy Lombard", "Bob Fish"]
|
18 |
+
},
|
19 |
+
{
|
20 |
+
"artist": "Joost Swarte",
|
21 |
+
"works": ["Modern Art", "L'Art Moderne"]
|
22 |
+
}
|
23 |
+
]
|
24 |
+
},
|
25 |
+
{
|
26 |
+
"name": "Science-Fiction Européenne",
|
27 |
+
"description": "Style visionnaire mêlant précision technique et onirisme",
|
28 |
+
"references": [
|
29 |
+
{
|
30 |
+
"artist": "Moebius (Jean Giraud)",
|
31 |
+
"works": ["L'Incal", "Arzak", "Le Garage Hermétique", "Aedena"]
|
32 |
+
},
|
33 |
+
{
|
34 |
+
"artist": "Philippe Druillet",
|
35 |
+
"works": ["Lone Sloane", "Salammbô", "Delirius"]
|
36 |
+
},
|
37 |
+
{
|
38 |
+
"artist": "François Schuiten",
|
39 |
+
"works": ["Les Cités Obscures", "La Fièvre d'Urbicande"]
|
40 |
+
}
|
41 |
+
]
|
42 |
+
},
|
43 |
+
{
|
44 |
+
"name": "Comics Américain Classique",
|
45 |
+
"description": "Style dynamique avec des couleurs vives et des ombrages marqués",
|
46 |
+
"references": [
|
47 |
+
{
|
48 |
+
"artist": "Jack Kirby",
|
49 |
+
"works": ["Fantastic Four", "New Gods", "Captain America"]
|
50 |
+
},
|
51 |
+
{
|
52 |
+
"artist": "Steve Ditko",
|
53 |
+
"works": ["Spider-Man", "Doctor Strange", "Mr. A"]
|
54 |
+
},
|
55 |
+
{
|
56 |
+
"artist": "Neal Adams",
|
57 |
+
"works": ["Batman", "Green Lantern/Green Arrow", "X-Men"]
|
58 |
+
}
|
59 |
+
]
|
60 |
+
}
|
61 |
+
],
|
62 |
+
"genres": [
|
63 |
+
"Steampunk",
|
64 |
+
"Cyberpunk",
|
65 |
+
"Post-apocalyptique",
|
66 |
+
"Fantasy",
|
67 |
+
"Space Opera",
|
68 |
+
"Western",
|
69 |
+
"Film Noir",
|
70 |
+
"Horror",
|
71 |
+
"Mythologie",
|
72 |
+
"Dystopie",
|
73 |
+
"Uchronie",
|
74 |
+
"Heroic Fantasy",
|
75 |
+
"Urban Fantasy"
|
76 |
+
],
|
77 |
+
"epochs": [
|
78 |
+
"Préhistoire",
|
79 |
+
"Antiquité",
|
80 |
+
"Moyen Âge",
|
81 |
+
"Renaissance",
|
82 |
+
"Révolution Industrielle",
|
83 |
+
"Années 1920",
|
84 |
+
"Années 1950",
|
85 |
+
"Époque Contemporaine",
|
86 |
+
"Futur Proche",
|
87 |
+
"Futur Lointain",
|
88 |
+
"Post-Apocalyptique",
|
89 |
+
"Âge d'Or",
|
90 |
+
"Ère Spatiale"
|
91 |
+
]
|
92 |
+
}
|
server/scripts/test_game.py
CHANGED
@@ -5,6 +5,7 @@ import time
|
|
5 |
import argparse
|
6 |
from pathlib import Path
|
7 |
from dotenv import load_dotenv
|
|
|
8 |
|
9 |
# Add server directory to PYTHONPATH
|
10 |
server_dir = Path(__file__).parent.parent
|
@@ -12,6 +13,7 @@ sys.path.append(str(server_dir))
|
|
12 |
|
13 |
from core.game_logic import GameState, StoryGenerator
|
14 |
from core.constants import GameConfig
|
|
|
15 |
|
16 |
# Load environment variables
|
17 |
load_dotenv()
|
@@ -24,6 +26,16 @@ def parse_args():
|
|
24 |
def print_separator(char="=", length=50):
|
25 |
print(f"\n{char * length}\n")
|
26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
def print_story_step(step_number, radiation_level, story_text, image_prompts, generation_time: float, story_history: str = None, show_context: bool = False, model_name: str = None, is_death: bool = False, is_victory: bool = False):
|
28 |
print_separator("=")
|
29 |
print(f"📖 STEP {step_number}")
|
@@ -48,21 +60,49 @@ def print_story_step(step_number, radiation_level, story_text, image_prompts, ge
|
|
48 |
print_separator("=")
|
49 |
|
50 |
async def play_game(show_context: bool = False):
|
51 |
-
# Initialize
|
52 |
-
game_state = GameState()
|
53 |
model_name = "mistral-small"
|
54 |
story_generator = StoryGenerator(
|
55 |
api_key=os.getenv("MISTRAL_API_KEY"),
|
56 |
model_name=model_name
|
57 |
)
|
58 |
|
|
|
|
|
|
|
59 |
print("\n=== Don't Look Up - Test Mode ===\n")
|
60 |
print("🎮 Starting adventure...")
|
61 |
-
print("You are Sarah, a survivor in a post-apocalyptic world.")
|
62 |
if show_context:
|
63 |
print("📚 Context display is enabled")
|
64 |
print_separator()
|
65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
66 |
last_choice = None
|
67 |
|
68 |
while True:
|
@@ -84,7 +124,11 @@ async def play_game(show_context: bool = False):
|
|
84 |
|
85 |
# Mesurer le temps de génération
|
86 |
start_time = time.time()
|
87 |
-
response = await story_generator.generate_story_segment(
|
|
|
|
|
|
|
|
|
88 |
generation_time = time.time() - start_time
|
89 |
|
90 |
# Display current step
|
@@ -117,7 +161,7 @@ async def play_game(show_context: bool = False):
|
|
117 |
if response.choices:
|
118 |
print("\n🤔 AVAILABLE CHOICES:")
|
119 |
for i, choice in enumerate(response.choices, 1):
|
120 |
-
print(f"{i}. {choice}")
|
121 |
|
122 |
# Get player choice
|
123 |
while True:
|
|
|
5 |
import argparse
|
6 |
from pathlib import Path
|
7 |
from dotenv import load_dotenv
|
8 |
+
import uuid
|
9 |
|
10 |
# Add server directory to PYTHONPATH
|
11 |
server_dir = Path(__file__).parent.parent
|
|
|
13 |
|
14 |
from core.game_logic import GameState, StoryGenerator
|
15 |
from core.constants import GameConfig
|
16 |
+
from core.generators.universe_generator import UniverseGenerator
|
17 |
|
18 |
# Load environment variables
|
19 |
load_dotenv()
|
|
|
26 |
def print_separator(char="=", length=50):
|
27 |
print(f"\n{char * length}\n")
|
28 |
|
29 |
+
def print_universe_info(style: str, genre: str, epoch: str, base_story: str):
|
30 |
+
print_separator("*")
|
31 |
+
print("🌍 UNIVERSE GENERATED")
|
32 |
+
print(f"🎨 Style: {style}")
|
33 |
+
print(f"📚 Genre: {genre}")
|
34 |
+
print(f"⏳ Époque: {epoch}")
|
35 |
+
print("\n📖 Base Story:")
|
36 |
+
print(base_story)
|
37 |
+
print_separator("*")
|
38 |
+
|
39 |
def print_story_step(step_number, radiation_level, story_text, image_prompts, generation_time: float, story_history: str = None, show_context: bool = False, model_name: str = None, is_death: bool = False, is_victory: bool = False):
|
40 |
print_separator("=")
|
41 |
print(f"📖 STEP {step_number}")
|
|
|
60 |
print_separator("=")
|
61 |
|
62 |
async def play_game(show_context: bool = False):
|
63 |
+
# Initialize components
|
|
|
64 |
model_name = "mistral-small"
|
65 |
story_generator = StoryGenerator(
|
66 |
api_key=os.getenv("MISTRAL_API_KEY"),
|
67 |
model_name=model_name
|
68 |
)
|
69 |
|
70 |
+
# Create universe generator
|
71 |
+
universe_generator = UniverseGenerator(story_generator.mistral_client)
|
72 |
+
|
73 |
print("\n=== Don't Look Up - Test Mode ===\n")
|
74 |
print("🎮 Starting adventure...")
|
|
|
75 |
if show_context:
|
76 |
print("📚 Context display is enabled")
|
77 |
print_separator()
|
78 |
|
79 |
+
# Generate universe
|
80 |
+
print("🌍 Generating universe...")
|
81 |
+
style, genre, epoch = universe_generator._get_random_elements()
|
82 |
+
universe = await universe_generator.generate()
|
83 |
+
|
84 |
+
# Create session and game state
|
85 |
+
session_id = str(uuid.uuid4())
|
86 |
+
game_state = GameState()
|
87 |
+
game_state.set_universe(
|
88 |
+
style=style["name"],
|
89 |
+
genre=genre,
|
90 |
+
epoch=epoch,
|
91 |
+
base_story=universe
|
92 |
+
)
|
93 |
+
|
94 |
+
# Create text generator for this session
|
95 |
+
story_generator.create_text_generator(
|
96 |
+
session_id=session_id,
|
97 |
+
style=style["name"],
|
98 |
+
genre=genre,
|
99 |
+
epoch=epoch,
|
100 |
+
base_story=universe
|
101 |
+
)
|
102 |
+
|
103 |
+
# Display universe information
|
104 |
+
print_universe_info(style["name"], genre, epoch, universe)
|
105 |
+
|
106 |
last_choice = None
|
107 |
|
108 |
while True:
|
|
|
124 |
|
125 |
# Mesurer le temps de génération
|
126 |
start_time = time.time()
|
127 |
+
response = await story_generator.generate_story_segment(
|
128 |
+
session_id=session_id,
|
129 |
+
game_state=game_state,
|
130 |
+
previous_choice=previous_choice
|
131 |
+
)
|
132 |
generation_time = time.time() - start_time
|
133 |
|
134 |
# Display current step
|
|
|
161 |
if response.choices:
|
162 |
print("\n🤔 AVAILABLE CHOICES:")
|
163 |
for i, choice in enumerate(response.choices, 1):
|
164 |
+
print(f"{i}. {choice.text}")
|
165 |
|
166 |
# Get player choice
|
167 |
while True:
|
server/server.py
CHANGED
@@ -12,6 +12,7 @@ from services.flux_client import FluxClient
|
|
12 |
from api.routes.chat import get_chat_router
|
13 |
from api.routes.image import get_image_router
|
14 |
from api.routes.speech import get_speech_router
|
|
|
15 |
|
16 |
# Load environment variables
|
17 |
load_dotenv()
|
@@ -46,6 +47,7 @@ mistral_api_key = os.getenv("MISTRAL_API_KEY")
|
|
46 |
if not mistral_api_key:
|
47 |
raise ValueError("MISTRAL_API_KEY environment variable is not set")
|
48 |
|
|
|
49 |
session_manager = SessionManager()
|
50 |
story_generator = StoryGenerator(api_key=mistral_api_key)
|
51 |
flux_client = FluxClient(api_key=HF_API_KEY)
|
@@ -57,9 +59,11 @@ async def health_check():
|
|
57 |
return {"status": "healthy"}
|
58 |
|
59 |
# Register route handlers
|
|
|
60 |
app.include_router(get_chat_router(session_manager, story_generator), prefix="/api")
|
61 |
app.include_router(get_image_router(flux_client), prefix="/api")
|
62 |
app.include_router(get_speech_router(), prefix="/api")
|
|
|
63 |
|
64 |
@app.on_event("startup")
|
65 |
async def startup_event():
|
|
|
12 |
from api.routes.chat import get_chat_router
|
13 |
from api.routes.image import get_image_router
|
14 |
from api.routes.speech import get_speech_router
|
15 |
+
from api.routes.universe import get_universe_router
|
16 |
|
17 |
# Load environment variables
|
18 |
load_dotenv()
|
|
|
47 |
if not mistral_api_key:
|
48 |
raise ValueError("MISTRAL_API_KEY environment variable is not set")
|
49 |
|
50 |
+
print("Creating global SessionManager")
|
51 |
session_manager = SessionManager()
|
52 |
story_generator = StoryGenerator(api_key=mistral_api_key)
|
53 |
flux_client = FluxClient(api_key=HF_API_KEY)
|
|
|
59 |
return {"status": "healthy"}
|
60 |
|
61 |
# Register route handlers
|
62 |
+
print("Registering route handlers with SessionManager", id(session_manager))
|
63 |
app.include_router(get_chat_router(session_manager, story_generator), prefix="/api")
|
64 |
app.include_router(get_image_router(flux_client), prefix="/api")
|
65 |
app.include_router(get_speech_router(), prefix="/api")
|
66 |
+
app.include_router(get_universe_router(session_manager, story_generator), prefix="/api")
|
67 |
|
68 |
@app.on_event("startup")
|
69 |
async def startup_event():
|