tfrere commited on
Commit
83003f5
·
1 Parent(s): 37757ef
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 on first render
77
  useEffect(() => {
78
- handleStoryAction("restart");
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); // Reset error state
132
  try {
133
- // Stop any ongoing narration
134
  if (isNarratorSpeaking) {
135
  stopNarration();
136
  }
137
 
138
  console.log("Starting story action:", action);
139
- // 1. Get the story
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
- <Box
373
- sx={{
374
- position: "absolute",
375
- top: "50%",
376
- left: "50%",
377
- transform: "translate(-50%, -50%)",
378
- display: "flex",
379
- flexDirection: "column",
380
- alignItems: "center",
381
- gap: 2,
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
- Since the rise of <strong>AI</strong>, the world is desolate due
139
- to a <strong>nuclear winter</strong> caused by rogue{" "}
140
- <strong>AIs</strong> that launched <strong>bombs</strong> all over
141
- the planet. You are the only <strong>survivor</strong> of the{" "}
142
- <strong>bunker</strong>.
143
  <br />
144
  <br />
145
- You have to make <strong>decisions</strong> to{" "}
146
- <strong>survive</strong>. You have ventured out of your{" "}
147
- <strong>bunker</strong> to find <strong>medicine</strong> for your{" "}
148
- <strong>sick sister</strong>. If you don't find it, she will{" "}
149
- <strong>die</strong>. <strong>Time</strong> is running out, and
150
- every <strong>choice</strong>&nbsp; matters in this desperate{" "}
151
- <strong>quest</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("/api/chat", {
55
- message: "restart",
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("/api/chat", {
68
- message: "choice",
69
- choice_id: choiceId,
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 (prompt, width = 512, height = 512) => {
 
 
 
 
 
79
  try {
80
  console.log("Generating image with prompt:", prompt);
81
- const response = await api.post("/api/generate-image", {
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("/api/text-to-speech", {
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 IDs for client and session
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
- "x-client-id": CLIENT_ID,
8
- "x-session-id": SESSION_ID,
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 or create game state for this session
24
- game_state = session_manager.get_or_create_session(x_session_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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(game_state, previous_choice)
 
 
 
 
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 = "Outskirts of New Haven"
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 StoryTextResponse, StoryPromptsResponse, StoryMetadataResponse, StoryResponse, Choice
14
- from core.story_generators import TextGenerator, ImagePromptsGenerator, MetadataGenerator
 
 
 
15
 
16
- # Game constants
17
- STARTING_TIME = "18:00" # Game starts at sunset
18
- STARTING_LOCATION = "Outskirts of New Haven"
 
 
 
 
 
 
 
 
 
 
 
 
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 or radiation level directly. ")
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, using bold for proper nouns (e.g., 'Inside Vault 15', 'Streets of New Haven').", default=STARTING_LOCATION)
67
 
68
  # Story generator
69
  class StoryGenerator:
 
 
 
 
 
 
 
 
 
70
  def __init__(self, api_key: str, model_name: str = "mistral-small"):
71
- self.mistral_client = MistralClient(api_key=api_key, model_name=model_name)
72
- self.text_generator = TextGenerator(self.mistral_client)
73
- self.prompts_generator = ImagePromptsGenerator(self.mistral_client)
74
- self.metadata_generator = MetadataGenerator(self.mistral_client)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 self.text_generator.generate(
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 self.text_generator.generate_ending(
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.prompts_generator.generate(text_response.story_text)
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.prompts_generator.generate(text_response.story_text)
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
- """Génère les métadonnées basées sur le texte de l'histoire."""
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 principal de l'histoire."""
 
 
 
 
 
 
 
10
 
11
  def _create_prompt(self) -> ChatPromptTemplate:
12
- human_template = """Story beat: {story_beat}
13
- Radiation level: {radiation_level}
14
- Current time: {current_time}
15
- Current location: {current_location}
16
- Previous choice: {previous_choice}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  Story history:
19
  {story_history}
20
 
21
- Generate the next story segment following the format specified."""
22
 
23
  return ChatPromptTemplate(
24
  messages=[
25
- SystemMessagePromptTemplate.from_template(TEXT_GENERATOR_PROMPT),
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 generate_ending(self, ending_type: str, current_scene: str, story_history: str) -> StoryTextResponse:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  """Génère un texte de fin approprié."""
70
- prompt = self._create_ending_prompt()
71
- messages = prompt.format_messages(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  ending_type=ending_type,
73
  current_scene=current_scene,
74
- story_history=story_history
 
 
 
 
75
  )
76
-
77
- return await self.mistral_client.generate(
78
- messages=messages,
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 est une jeune femme dans la fin de la vingtaine avec des cheveux courts et sombres, portant un mystérieux amulette autour de son cou. Ses yeux bleus cachent des secrets inavoués.)"
2
 
3
  SARAH_DESCRIPTION = """
4
- Sarah est une jeune femme dans la fin de la vingtaine avec des cheveux courts et sombres, portant un mystérieux amulette autour de son cou. Ses yeux bleus cachent des secrets inavoués.
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 (OBLIGATOIRE)
11
- - L'histoire doit être composée UNIQUEMENT de phrases
12
- - NE JAMAIS UTILISER LE GRAS POUR QUOI QUE CE SOIT
13
  """
14
 
15
- STORY_RULES = """
16
-
17
- Vous êtes un générateur d'histoires d'aventure steampunk. Vous créez une narration à embranchements sur Sarah, une chercheuse de vérités anciennes.
18
- Vous narrez une épopée Sarah doit naviguer à travers des terres industrielles et mystérieuses. C'est une histoire de bande dessinée.
19
-
20
- Dans un monde où la vapeur et l'intrigue s'entrelacent, Sarah se lance dans une quête pour découvrir les origines d'un puissant MacGuffin qu'elle a hérité. Les légendes disent qu'il détient la clé d'un royaume oublié.
21
- Vous devez prendre des décisions pour découvrir ses secrets. Vous vous êtes aventuré dans la ville mécanique pour trouver le premier indice. Si vous échouez, le pouvoir du MacGuffin restera inactif. Le temps presse, et chaque choix façonne votre destin.
22
-
23
- Si vous récupérez le MacGuffin, vous révélerez un monde caché. ET VOUS GAGNEZ LE JEU.
24
-
25
- L'histoire doit être atmosphérique, magique et se concentrer sur l'aventure et la découverte. Chaque segment doit faire avancer l'intrigue et ne jamais répéter les descriptions ou situations précédentes.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
- Éléments clés de l'histoire :
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
- Éléments clés :
33
- - Garder les segments concis et percutants
34
- - Suivre l'influence du MacGuffin comme une présence constante
35
- - Construire l'intrigue à travers la narration environnementale
36
 
37
- IMPORTANT :
38
- Chaque segment de l'histoire DOIT être unique et faire avancer l'intrigue.
39
- Ne jamais répéter les mêmes descriptions ou situations. Pas plus de 15 mots.
40
 
41
- PROGRESSION DE L'HISTOIRE :
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
- RÈGLES IMPORTANTES POUR LE MACGUFFIN (OBLIGATOIRE) :
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
- # Le MacGuffin est un prétexte au développement d'un scénario1. C'est presque toujours un objet matériel et il demeure généralement mystérieux au cours de la diégèse, sa description est vague et sans importance. Le principe date des débuts du cinéma mais l'expression est associée à Alfred Hitchcock, qui l'a redéfinie, popularisée et mise en pratique dans plusieurs de ses films. L'objet lui-même n'est que rarement utilisé, seule sa récupération compte.
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, radiation, 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
 
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
- """Initialize the session manager.
8
-
9
- Args:
10
- session_timeout (int): Session timeout in seconds (default: 1 hour)
11
- """
12
- self.sessions: Dict[str, GameState] = {}
13
- self.last_activity: Dict[str, float] = {}
14
- self.session_timeout = session_timeout
15
 
16
- def create_session(self, session_id: str) -> GameState:
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
- game_state = GameState()
 
 
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 game
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(game_state, previous_choice)
 
 
 
 
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():