tfrere commited on
Commit
2c65a29
·
1 Parent(s): ae036d6
.DS_Store CHANGED
Binary files a/.DS_Store and b/.DS_Store differ
 
client/public/sounds/transitional-swipe-1.mp3 ADDED
Binary file (120 kB). View file
 
client/public/sounds/transitional-swipe-2.mp3 ADDED
Binary file (65.2 kB). View file
 
client/public/sounds/transitional-swipe-3.mp3 ADDED
Binary file (81.1 kB). View file
 
client/src/components/StoryChoices.jsx CHANGED
@@ -35,7 +35,6 @@ export function StoryChoices({
35
  }) {
36
  const navigate = useNavigate();
37
 
38
- console.log("ICI", isLastStep, isGameOver);
39
  if (isGameOver) {
40
  return (
41
  <Box
 
35
  }) {
36
  const navigate = useNavigate();
37
 
 
38
  if (isGameOver) {
39
  return (
40
  <Box
client/src/components/TalkWithSarah.jsx ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useRef, useEffect } from "react";
2
+ import {
3
+ Box,
4
+ IconButton,
5
+ TextField,
6
+ Dialog,
7
+ DialogTitle,
8
+ DialogContent,
9
+ DialogActions,
10
+ Button,
11
+ Tooltip,
12
+ } from "@mui/material";
13
+ import FiberManualRecordIcon from "@mui/icons-material/FiberManualRecord";
14
+ import CheckCircleIcon from "@mui/icons-material/CheckCircle";
15
+ import CancelIcon from "@mui/icons-material/Cancel";
16
+ import { useConversation } from "@11labs/react";
17
+
18
+ const AGENT_ID = "2MF9st3s1mNFbX01Y106";
19
+ const ELEVEN_LABS_KEY_STORAGE = "eleven_labs_api_key";
20
+
21
+ export function TalkWithSarah({
22
+ isNarratorSpeaking,
23
+ stopNarration,
24
+ playNarration,
25
+ onDecisionMade,
26
+ currentContext,
27
+ }) {
28
+ const [isRecording, setIsRecording] = useState(false);
29
+ const [isConversationMode, setIsConversationMode] = useState(false);
30
+ const [showApiKeyDialog, setShowApiKeyDialog] = useState(false);
31
+ const [apiKey, setApiKey] = useState(() => {
32
+ return localStorage.getItem(ELEVEN_LABS_KEY_STORAGE) || "";
33
+ });
34
+ const [isApiKeyValid, setIsApiKeyValid] = useState(false);
35
+ const mediaRecorderRef = useRef(null);
36
+ const audioChunksRef = useRef([]);
37
+ const wsRef = useRef(null);
38
+
39
+ const conversation = useConversation({
40
+ agentId: AGENT_ID,
41
+ headers: {
42
+ "xi-api-key": apiKey,
43
+ },
44
+ onResponse: async (response) => {
45
+ if (response.type === "audio") {
46
+ try {
47
+ const audioBlob = new Blob([response.audio], { type: "audio/mpeg" });
48
+ const audioUrl = URL.createObjectURL(audioBlob);
49
+ await playNarration(audioUrl);
50
+ URL.revokeObjectURL(audioUrl);
51
+ } catch (error) {
52
+ console.error("Error playing ElevenLabs audio:", error);
53
+ }
54
+ }
55
+ },
56
+ clientTools: {
57
+ make_decision: async ({ decision }) => {
58
+ console.log("AI made decision:", decision);
59
+ // Stop recording
60
+ if (
61
+ mediaRecorderRef.current &&
62
+ mediaRecorderRef.current.state === "recording"
63
+ ) {
64
+ mediaRecorderRef.current.stop();
65
+ }
66
+ setIsConversationMode(false);
67
+ await conversation?.endSession();
68
+ setIsRecording(false);
69
+ await onDecisionMade(parseInt(decision));
70
+ },
71
+ },
72
+ });
73
+
74
+ // Valider la clé API
75
+ const validateApiKey = async (key) => {
76
+ try {
77
+ const response = await fetch("https://api.elevenlabs.io/v1/user", {
78
+ headers: {
79
+ "xi-api-key": key,
80
+ },
81
+ });
82
+ return response.ok;
83
+ } catch (error) {
84
+ return false;
85
+ }
86
+ };
87
+
88
+ // Vérifier la validité de la clé API quand elle change
89
+ useEffect(() => {
90
+ const checkApiKey = async () => {
91
+ if (apiKey) {
92
+ const isValid = await validateApiKey(apiKey);
93
+ setIsApiKeyValid(isValid);
94
+ if (isValid) {
95
+ localStorage.setItem(ELEVEN_LABS_KEY_STORAGE, apiKey);
96
+ }
97
+ } else {
98
+ setIsApiKeyValid(false);
99
+ }
100
+ };
101
+ checkApiKey();
102
+ }, [apiKey]);
103
+
104
+ // Sauvegarder la clé API dans le localStorage
105
+ useEffect(() => {
106
+ if (apiKey) {
107
+ localStorage.setItem(ELEVEN_LABS_KEY_STORAGE, apiKey);
108
+ }
109
+ }, [apiKey]);
110
+
111
+ const startRecording = async () => {
112
+ if (!apiKey) {
113
+ setShowApiKeyDialog(true);
114
+ return;
115
+ }
116
+
117
+ try {
118
+ setIsRecording(true);
119
+ // Stop narration audio if it's playing
120
+ if (isNarratorSpeaking) {
121
+ stopNarration();
122
+ }
123
+
124
+ // Safely stop any conversation audio if playing
125
+ if (conversation?.audioRef?.current) {
126
+ conversation.audioRef.current.pause();
127
+ conversation.audioRef.current.currentTime = 0;
128
+ }
129
+
130
+ if (!isConversationMode) {
131
+ setIsConversationMode(true);
132
+ try {
133
+ if (!conversation) {
134
+ throw new Error("Conversation not initialized");
135
+ }
136
+ await conversation.startSession({
137
+ agentId: AGENT_ID,
138
+ initialContext: currentContext,
139
+ });
140
+ console.log("ElevenLabs WebSocket connected");
141
+ } catch (error) {
142
+ console.error("Error starting conversation:", error);
143
+ return;
144
+ }
145
+ }
146
+
147
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
148
+ mediaRecorderRef.current = new MediaRecorder(stream);
149
+ audioChunksRef.current = [];
150
+
151
+ mediaRecorderRef.current.ondataavailable = (event) => {
152
+ if (event.data.size > 0) {
153
+ audioChunksRef.current.push(event.data);
154
+ }
155
+ };
156
+
157
+ mediaRecorderRef.current.onstop = async () => {
158
+ const audioBlob = new Blob(audioChunksRef.current, {
159
+ type: "audio/wav",
160
+ });
161
+ audioChunksRef.current = [];
162
+
163
+ const reader = new FileReader();
164
+ reader.readAsDataURL(audioBlob);
165
+
166
+ reader.onload = async () => {
167
+ const base64Audio = reader.result.split(",")[1];
168
+ if (isConversationMode) {
169
+ try {
170
+ // Send audio to ElevenLabs conversation
171
+ await conversation.send({
172
+ type: "audio",
173
+ data: base64Audio,
174
+ });
175
+ } catch (error) {
176
+ console.error("Error sending audio to ElevenLabs:", error);
177
+ }
178
+ }
179
+ };
180
+ };
181
+
182
+ mediaRecorderRef.current.start();
183
+ } catch (error) {
184
+ console.error("Error starting recording:", error);
185
+ }
186
+ };
187
+
188
+ const handleSaveApiKey = () => {
189
+ setShowApiKeyDialog(false);
190
+ if (apiKey) {
191
+ startRecording();
192
+ }
193
+ };
194
+
195
+ return (
196
+ <Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
197
+ <Box sx={{ position: "relative", display: "flex", alignItems: "center" }}>
198
+ <TextField
199
+ size="small"
200
+ type="password"
201
+ placeholder="Enter your ElevenLabs API key"
202
+ value={apiKey}
203
+ onChange={(e) => setApiKey(e.target.value)}
204
+ sx={{
205
+ width: "300px",
206
+ "& .MuiOutlinedInput-root": {
207
+ color: "white",
208
+ "& fieldset": {
209
+ borderColor: "rgba(255, 255, 255, 0.23)",
210
+ },
211
+ "&:hover fieldset": {
212
+ borderColor: "white",
213
+ },
214
+ "&.Mui-focused fieldset": {
215
+ borderColor: "white",
216
+ },
217
+ "& .MuiOutlinedInput-input": {
218
+ paddingRight: apiKey ? "40px" : "14px", // Padding dynamique
219
+ },
220
+ },
221
+ "& .MuiInputBase-input": {
222
+ color: "white",
223
+ "&::placeholder": {
224
+ color: "rgba(255, 255, 255, 0.5)",
225
+ opacity: 1,
226
+ },
227
+ },
228
+ }}
229
+ />
230
+ {apiKey && (
231
+ <Tooltip
232
+ title={isApiKeyValid ? "API key is valid" : "Invalid API key"}
233
+ >
234
+ <Box
235
+ sx={{
236
+ position: "absolute",
237
+ right: 10,
238
+ pointerEvents: "none",
239
+ display: "flex",
240
+ alignItems: "center",
241
+ backgroundColor: "rgba(0, 0, 0, 0.8)",
242
+ borderRadius: "50%",
243
+ padding: "2px",
244
+ }}
245
+ >
246
+ {isApiKeyValid ? (
247
+ <CheckCircleIcon sx={{ color: "#4caf50", fontSize: 20 }} />
248
+ ) : (
249
+ <CancelIcon sx={{ color: "#f44336", fontSize: 20 }} />
250
+ )}
251
+ </Box>
252
+ </Tooltip>
253
+ )}
254
+ </Box>
255
+ <IconButton
256
+ onClick={startRecording}
257
+ disabled={isRecording || !isApiKeyValid}
258
+ sx={{
259
+ color: "white",
260
+ backgroundColor: isRecording ? "primary.main" : "transparent",
261
+ "&:hover": {
262
+ backgroundColor: isRecording
263
+ ? "primary.dark"
264
+ : "rgba(0, 0, 0, 0.7)",
265
+ },
266
+ px: 2,
267
+ borderRadius: 2,
268
+ border: "1px solid white",
269
+ opacity: !isApiKeyValid ? 0.5 : 1,
270
+ }}
271
+ >
272
+ <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
273
+ {isRecording ? <FiberManualRecordIcon sx={{ color: "red" }} /> : null}
274
+ <span style={{ fontSize: "1rem" }}>Talk with Sarah</span>
275
+ </Box>
276
+ </IconButton>
277
+ </Box>
278
+ );
279
+ }
client/src/hooks/useTransitionSound.js ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useSound } from "use-sound";
2
+ import { useState, useEffect } from "react";
3
+
4
+ const TRANSITION_SOUNDS = Array.from(
5
+ { length: 3 },
6
+ (_, i) => `/sounds/transitional-swipe-${i + 1}.mp3`
7
+ );
8
+
9
+ export function useTransitionSound(isSoundEnabled = true) {
10
+ const [soundsLoaded, setSoundsLoaded] = useState(false);
11
+
12
+ // Créer un tableau de hooks useSound pour chaque son
13
+ const sounds = TRANSITION_SOUNDS.map((soundPath) => {
14
+ const [play, { sound }] = useSound(soundPath, {
15
+ volume: 0.1,
16
+ interrupt: true,
17
+ });
18
+ return { play, sound };
19
+ });
20
+
21
+ // Vérifier quand tous les sons sont chargés
22
+ useEffect(() => {
23
+ const allSoundsLoaded = sounds.every(
24
+ ({ sound }) => sound && sound.state() === "loaded"
25
+ );
26
+ if (allSoundsLoaded) {
27
+ setSoundsLoaded(true);
28
+ }
29
+ }, [sounds]);
30
+
31
+ const playRandomTransitionSound = () => {
32
+ if (!isSoundEnabled || !soundsLoaded) {
33
+ return;
34
+ }
35
+
36
+ const randomIndex = Math.floor(Math.random() * sounds.length);
37
+ try {
38
+ sounds[randomIndex].play();
39
+ } catch (error) {
40
+ console.error("Error playing transition sound:", error);
41
+ }
42
+ };
43
+
44
+ return playRandomTransitionSound;
45
+ }
client/src/layouts/Panel.jsx CHANGED
@@ -9,11 +9,6 @@ export function Panel({ segment, panel, panelIndex }) {
9
  // Reset states when the image changes
10
  useEffect(() => {
11
  const hasImage = !!segment?.images?.[panelIndex];
12
- console.log(`[Panel ${panelIndex}] Image changed:`, {
13
- hasSegment: !!segment,
14
- hasImage,
15
- imageContent: segment?.images?.[panelIndex]?.slice(0, 50),
16
- });
17
 
18
  // Ne réinitialiser les états que si on n'a pas d'image
19
  if (!hasImage) {
@@ -23,17 +18,9 @@ export function Panel({ segment, panel, panelIndex }) {
23
  }, [segment?.images?.[panelIndex]]);
24
 
25
  // Log component state changes
26
- useEffect(() => {
27
- console.log(`[Panel ${panelIndex}] State updated:`, {
28
- imageLoaded,
29
- isLoading,
30
- hasSegment: !!segment,
31
- hasImage: !!segment?.images?.[panelIndex],
32
- });
33
- }, [imageLoaded, isLoading, segment, panelIndex]);
34
 
35
  const handleImageLoad = () => {
36
- console.log(`[Panel ${panelIndex}] Image loaded successfully`);
37
  setImageLoaded(true);
38
  setIsLoading(false);
39
  };
 
9
  // Reset states when the image changes
10
  useEffect(() => {
11
  const hasImage = !!segment?.images?.[panelIndex];
 
 
 
 
 
12
 
13
  // Ne réinitialiser les états que si on n'a pas d'image
14
  if (!hasImage) {
 
18
  }, [segment?.images?.[panelIndex]]);
19
 
20
  // Log component state changes
21
+ useEffect(() => {}, [imageLoaded, isLoading, segment, panelIndex]);
 
 
 
 
 
 
 
22
 
23
  const handleImageLoad = () => {
 
24
  setImageLoaded(true);
25
  setIsLoading(false);
26
  };
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 { useGameSession } from "../hooks/useGameSession";
18
  import { StoryChoices } from "../components/StoryChoices";
19
  import { ErrorDisplay } from "../components/ErrorDisplay";
@@ -25,6 +26,8 @@ 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";
30
 
@@ -69,6 +72,7 @@ export function Game() {
69
  useNarrator(isSoundEnabled);
70
  const playPageSound = usePageSound(isSoundEnabled);
71
  const playWritingSound = useWritingSound(isSoundEnabled);
 
72
  const {
73
  sessionId,
74
  universe,
@@ -76,6 +80,15 @@ export function Game() {
76
  error: sessionError,
77
  } = useGameSession();
78
 
 
 
 
 
 
 
 
 
 
79
  // Sauvegarder l'état du son dans le localStorage
80
  useEffect(() => {
81
  localStorage.setItem(SOUND_ENABLED_KEY, isSoundEnabled);
@@ -144,7 +157,6 @@ export function Game() {
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)
@@ -161,7 +173,6 @@ export function Game() {
161
  isChoice: false,
162
  isDeath: storyData.is_death,
163
  isVictory: storyData.is_victory,
164
- radiationLevel: storyData.radiation_level,
165
  is_first_step: storyData.is_first_step,
166
  is_last_step: storyData.is_last_step,
167
  images: [],
@@ -185,10 +196,6 @@ export function Game() {
185
 
186
  // 6. Generate images in parallel
187
  if (storyData.image_prompts && storyData.image_prompts.length > 0) {
188
- console.log(
189
- "Starting image generation for prompts:",
190
- storyData.image_prompts
191
- );
192
  await generateImagesForStory(
193
  storyData.image_prompts,
194
  action === "restart" ? 0 : storySegments.length,
@@ -232,9 +239,6 @@ export function Game() {
232
 
233
  // Déterminer le layout en fonction du nombre d'images
234
  const layoutType = getNextLayoutType(0, imagePrompts.length);
235
- console.log(
236
- `Using layout ${layoutType} for ${imagePrompts.length} images`
237
- );
238
 
239
  for (
240
  let promptIndex = 0;
@@ -247,13 +251,9 @@ export function Game() {
247
 
248
  // Obtenir les dimensions pour ce panneau
249
  const panelDimensions = LAYOUTS[layoutType].panels[promptIndex];
250
- console.log(`Panel ${promptIndex} dimensions:`, panelDimensions);
251
 
252
  while (retryCount < maxRetries && !success) {
253
  try {
254
- console.log(
255
- `Generating image ${promptIndex + 1}/${imagePrompts.length}`
256
- );
257
  const result = await storyApi.generateImage(
258
  imagePrompts[promptIndex],
259
  panelDimensions.width,
@@ -265,7 +265,6 @@ export function Game() {
265
  }
266
 
267
  if (result.success) {
268
- console.log(`Successfully generated image ${promptIndex + 1}`);
269
  images[promptIndex] = result.image_base64;
270
 
271
  // Vérifier si toutes les images sont générées
@@ -432,6 +431,24 @@ export function Game() {
432
  showScreenshot={storySegments.length > 0}
433
  onScreenshot={handleCaptureStory}
434
  />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
  {showChoices && (
436
  <StoryChoices
437
  choices={currentChoices}
 
14
  import { useStoryCapture } from "../hooks/useStoryCapture";
15
  import { usePageSound } from "../hooks/usePageSound";
16
  import { useWritingSound } from "../hooks/useWritingSound";
17
+ import { useTransitionSound } from "../hooks/useTransitionSound";
18
  import { useGameSession } from "../hooks/useGameSession";
19
  import { StoryChoices } from "../components/StoryChoices";
20
  import { ErrorDisplay } from "../components/ErrorDisplay";
 
26
  import { getNextLayoutType, LAYOUTS } from "../layouts/config";
27
  import { LoadingScreen } from "../components/LoadingScreen";
28
 
29
+ import { TalkWithSarah } from "../components/TalkWithSarah";
30
+
31
  // Constants
32
  const SOUND_ENABLED_KEY = "sound_enabled";
33
 
 
72
  useNarrator(isSoundEnabled);
73
  const playPageSound = usePageSound(isSoundEnabled);
74
  const playWritingSound = useWritingSound(isSoundEnabled);
75
+ const playTransitionSound = useTransitionSound(isSoundEnabled);
76
  const {
77
  sessionId,
78
  universe,
 
80
  error: sessionError,
81
  } = useGameSession();
82
 
83
+ // Jouer le son de transition une fois que la session est chargée
84
+ useEffect(() => {
85
+ if (!isSessionLoading && sessionId && !error && !sessionError) {
86
+ setTimeout(() => {
87
+ playTransitionSound();
88
+ }, 100);
89
+ }
90
+ }, [isSessionLoading, sessionId, error, sessionError]);
91
+
92
  // Sauvegarder l'état du son dans le localStorage
93
  useEffect(() => {
94
  localStorage.setItem(SOUND_ENABLED_KEY, isSoundEnabled);
 
157
  stopNarration();
158
  }
159
 
 
160
  // Pass sessionId to API calls
161
  const storyData = await (action === "restart"
162
  ? storyApi.start(sessionId)
 
173
  isChoice: false,
174
  isDeath: storyData.is_death,
175
  isVictory: storyData.is_victory,
 
176
  is_first_step: storyData.is_first_step,
177
  is_last_step: storyData.is_last_step,
178
  images: [],
 
196
 
197
  // 6. Generate images in parallel
198
  if (storyData.image_prompts && storyData.image_prompts.length > 0) {
 
 
 
 
199
  await generateImagesForStory(
200
  storyData.image_prompts,
201
  action === "restart" ? 0 : storySegments.length,
 
239
 
240
  // Déterminer le layout en fonction du nombre d'images
241
  const layoutType = getNextLayoutType(0, imagePrompts.length);
 
 
 
242
 
243
  for (
244
  let promptIndex = 0;
 
251
 
252
  // Obtenir les dimensions pour ce panneau
253
  const panelDimensions = LAYOUTS[layoutType].panels[promptIndex];
 
254
 
255
  while (retryCount < maxRetries && !success) {
256
  try {
 
 
 
257
  const result = await storyApi.generateImage(
258
  imagePrompts[promptIndex],
259
  panelDimensions.width,
 
265
  }
266
 
267
  if (result.success) {
 
268
  images[promptIndex] = result.image_base64;
269
 
270
  // Vérifier si toutes les images sont générées
 
431
  showScreenshot={storySegments.length > 0}
432
  onScreenshot={handleCaptureStory}
433
  />
434
+
435
+ {storySegments.length > 0 && currentChoices.length > 0 && (
436
+ <TalkWithSarah
437
+ isNarratorSpeaking={isNarratorSpeaking}
438
+ stopNarration={stopNarration}
439
+ playNarration={playNarration}
440
+ onDecisionMade={handleChoice}
441
+ currentContext={`Sarah this is the situation you're in : ${
442
+ storySegments[storySegments.length - 1].text
443
+ }. Those are your possible decisions : \n ${currentChoices
444
+ .map(
445
+ (choice, index) =>
446
+ `decision ${index + 1} : ${choice.text}`
447
+ )
448
+ .join("\n ")}.`}
449
+ />
450
+ )}
451
+
452
  {showChoices && (
453
  <StoryChoices
454
  choices={currentChoices}
client/src/utils/api.js CHANGED
@@ -18,7 +18,11 @@ const api = axios.create({
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;
@@ -66,7 +70,6 @@ const handleApiError = (error) => {
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
  {
@@ -76,7 +79,6 @@ export const storyApi = {
76
  headers: getDefaultHeaders(sessionId),
77
  }
78
  );
79
- console.log("Start API response:", response.data);
80
  return response.data;
81
  } catch (error) {
82
  return handleApiError(error);
@@ -85,7 +87,6 @@ export const storyApi = {
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
  {
@@ -96,7 +97,6 @@ export const storyApi = {
96
  headers: getDefaultHeaders(sessionId),
97
  }
98
  );
99
- console.log("Choice API response:", response.data);
100
  return response.data;
101
  } catch (error) {
102
  return handleApiError(error);
@@ -110,7 +110,6 @@ export const storyApi = {
110
  sessionId = null
111
  ) => {
112
  try {
113
- console.log("Generating image with prompt:", prompt);
114
  const config = {
115
  prompt,
116
  width,
@@ -123,10 +122,6 @@ export const storyApi = {
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,
129
- });
130
  return response.data;
131
  } catch (error) {
132
  return handleApiError(error);
@@ -136,7 +131,6 @@ export const storyApi = {
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
  {
@@ -146,7 +140,6 @@ export const storyApi = {
146
  headers: getDefaultHeaders(sessionId),
147
  }
148
  );
149
- console.log("Narration response received");
150
  return response.data;
151
  } catch (error) {
152
  return handleApiError(error);
 
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 = [
22
+ "/api/universe/generate",
23
+ "/api/generate-image",
24
+ "/api/text-to-speech",
25
+ ];
26
 
27
  if (noSessionRoutes.includes(config.url)) {
28
  return config;
 
70
  export const storyApi = {
71
  start: async (sessionId) => {
72
  try {
 
73
  const response = await api.post(
74
  "/api/chat",
75
  {
 
79
  headers: getDefaultHeaders(sessionId),
80
  }
81
  );
 
82
  return response.data;
83
  } catch (error) {
84
  return handleApiError(error);
 
87
 
88
  makeChoice: async (choiceId, sessionId) => {
89
  try {
 
90
  const response = await api.post(
91
  "/api/chat",
92
  {
 
97
  headers: getDefaultHeaders(sessionId),
98
  }
99
  );
 
100
  return response.data;
101
  } catch (error) {
102
  return handleApiError(error);
 
110
  sessionId = null
111
  ) => {
112
  try {
 
113
  const config = {
114
  prompt,
115
  width,
 
122
  }
123
 
124
  const response = await api.post("/api/generate-image", config, options);
 
 
 
 
125
  return response.data;
126
  } catch (error) {
127
  return handleApiError(error);
 
131
  // Narration related API calls
132
  narrate: async (text, sessionId) => {
133
  try {
 
134
  const response = await api.post(
135
  "/api/text-to-speech",
136
  {
 
140
  headers: getDefaultHeaders(sessionId),
141
  }
142
  );
 
143
  return response.data;
144
  } catch (error) {
145
  return handleApiError(error);
server/api/models.py CHANGED
@@ -8,7 +8,7 @@ class Choice(BaseModel):
8
 
9
  class StorySegmentBase(BaseModel):
10
  """Base model for story segments with common validation logic"""
11
- story_text: str = Field(description="The story text. No more than 15 words THIS IS MANDATORY. Never mention story beat or radiation level directly. ")
12
  is_victory: bool = Field(description="Whether this segment ends in Sarah's victory", default=False)
13
  is_death: bool = Field(description="Whether this segment ends in Sarah's death", default=False)
14
 
@@ -24,12 +24,6 @@ class StoryPromptsResponse(BaseModel):
24
  )
25
 
26
  class StoryMetadataResponse(BaseModel):
27
- radiation_increase: int = Field(
28
- description=f"How much radiation this segment adds (0-3)",
29
- ge=0,
30
- le=3,
31
- default=GameConfig.DEFAULT_RADIATION_INCREASE
32
- )
33
  is_victory: bool = Field(description="Whether this segment ends in Sarah's victory", default=False)
34
  is_death: bool = Field(description="Whether this segment ends in Sarah's death", default=False)
35
  choices: List[str] = Field(description="Either empty list for victory/death, or exactly two choices for normal progression")
@@ -51,8 +45,6 @@ class StoryMetadataResponse(BaseModel):
51
  class StoryResponse(StorySegmentBase):
52
  choices: List[Choice]
53
  raw_choices: List[str] = Field(description="Raw choice texts from LLM before conversion to Choice objects")
54
- radiation_level: int = Field(description=f"Current radiation level")
55
- radiation_increase: int = Field(description="How much radiation this segment adds (0-3)", ge=0, le=3, default=GameConfig.DEFAULT_RADIATION_INCREASE)
56
  time: str = Field(description="Current in-game time in 24h format (HH:MM). Time passes realistically based on actions.")
57
  location: str = Field(description="Current location.")
58
  is_first_step: bool = Field(description="Whether this is the first step of the story", default=False)
 
8
 
9
  class StorySegmentBase(BaseModel):
10
  """Base model for story segments with common validation logic"""
11
+ story_text: str = Field(description="The story text. No more than 15 words THIS IS MANDATORY. Never mention story beat directly. ")
12
  is_victory: bool = Field(description="Whether this segment ends in Sarah's victory", default=False)
13
  is_death: bool = Field(description="Whether this segment ends in Sarah's death", default=False)
14
 
 
24
  )
25
 
26
  class StoryMetadataResponse(BaseModel):
 
 
 
 
 
 
27
  is_victory: bool = Field(description="Whether this segment ends in Sarah's victory", default=False)
28
  is_death: bool = Field(description="Whether this segment ends in Sarah's death", default=False)
29
  choices: List[str] = Field(description="Either empty list for victory/death, or exactly two choices for normal progression")
 
45
  class StoryResponse(StorySegmentBase):
46
  choices: List[Choice]
47
  raw_choices: List[str] = Field(description="Raw choice texts from LLM before conversion to Choice objects")
 
 
48
  time: str = Field(description="Current in-game time in 24h format (HH:MM). Time passes realistically based on actions.")
49
  location: str = Field(description="Current location.")
50
  is_first_step: bool = Field(description="Whether this is the first step of the story", default=False)
server/api/routes/chat.py CHANGED
@@ -63,12 +63,7 @@ def get_chat_router(session_manager: SessionManager, story_generator):
63
  previous_choice=previous_choice
64
  )
65
 
66
- # Update radiation level
67
- game_state.radiation_level += llm_response.radiation_increase
68
-
69
- # Check for radiation death
70
- is_death = game_state.radiation_level >= GameConfig.MAX_RADIATION
71
- if is_death:
72
  llm_response.choices = []
73
  llm_response.story_text += "\You have succumbed to the harsh wastelands, and your journey concludes here. THE END."
74
  if len(llm_response.image_prompts) > 1:
@@ -92,18 +87,16 @@ def get_chat_router(session_manager: SessionManager, story_generator):
92
  story_text=llm_response.story_text,
93
  choices=llm_response.choices,
94
  raw_choices=llm_response.raw_choices,
95
- radiation_level=game_state.radiation_level,
96
- radiation_increase=llm_response.radiation_increase,
97
  time=llm_response.time,
98
  location=llm_response.location,
99
  is_victory=llm_response.is_victory,
100
- is_death=is_death,
101
  is_first_step=game_state.story_beat == 0,
102
  image_prompts=llm_response.image_prompts
103
  )
104
 
105
  # Only increment story beat if not dead and not victory
106
- if not is_death and not llm_response.is_victory:
107
  game_state.story_beat += 1
108
 
109
  return response
 
63
  previous_choice=previous_choice
64
  )
65
 
66
+ if llm_response.is_death:
 
 
 
 
 
67
  llm_response.choices = []
68
  llm_response.story_text += "\You have succumbed to the harsh wastelands, and your journey concludes here. THE END."
69
  if len(llm_response.image_prompts) > 1:
 
87
  story_text=llm_response.story_text,
88
  choices=llm_response.choices,
89
  raw_choices=llm_response.raw_choices,
 
 
90
  time=llm_response.time,
91
  location=llm_response.location,
92
  is_victory=llm_response.is_victory,
93
+ is_death=llm_response.is_death,
94
  is_first_step=game_state.story_beat == 0,
95
  image_prompts=llm_response.image_prompts
96
  )
97
 
98
  # Only increment story beat if not dead and not victory
99
+ if not llm_response.is_death and not llm_response.is_victory:
100
  game_state.story_beat += 1
101
 
102
  return response
server/core/constants.py CHANGED
@@ -1,6 +1,5 @@
1
  class GameConfig:
2
  # Game state constants
3
- MAX_RADIATION = 12
4
  STARTING_TIME = "18:00"
5
  STARTING_LOCATION = "Home"
6
 
@@ -8,11 +7,5 @@ class GameConfig:
8
  MIN_PANELS = 1
9
  MAX_PANELS = 4
10
 
11
- # Default values
12
- DEFAULT_RADIATION_INCREASE = 1
13
-
14
  # Story progression
15
- STORY_BEAT_INTRO = 0
16
- STORY_BEAT_EARLY_GAME = 1
17
- STORY_BEAT_MID_GAME = 3
18
- STORY_BEAT_LATE_GAME = 5
 
1
  class GameConfig:
2
  # Game state constants
 
3
  STARTING_TIME = "18:00"
4
  STARTING_LOCATION = "Home"
5
 
 
7
  MIN_PANELS = 1
8
  MAX_PANELS = 4
9
 
 
 
 
10
  # Story progression
11
+ STORY_BEAT_INTRO = 0
 
 
 
server/core/game_logic.py CHANGED
@@ -1,10 +1,5 @@
1
  from pydantic import BaseModel, Field
2
  from typing import List, Tuple
3
- from langchain.output_parsers import PydanticOutputParser, OutputFixingParser
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
@@ -47,7 +42,6 @@ def format_image_prompt(prompt: str, time: str, location: str) -> str:
47
  class GameState:
48
  def __init__(self):
49
  self.story_beat = GameConfig.STORY_BEAT_INTRO
50
- self.radiation_level = 0
51
  self.story_history = []
52
  self.current_time = GameConfig.STARTING_TIME
53
  self.current_location = GameConfig.STARTING_LOCATION
@@ -67,7 +61,6 @@ class GameState:
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
@@ -108,10 +101,9 @@ class GameState:
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)
@@ -171,14 +163,13 @@ class StoryGenerator:
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,
183
  current_location=game_state.current_location,
184
  previous_choice=previous_choice,
@@ -194,8 +185,7 @@ class StoryGenerator:
194
  )
195
 
196
  # 3. Vérifier si c'est une fin (mort ou victoire)
197
- is_radiation_death = game_state.radiation_level + metadata_response.radiation_increase >= GameConfig.MAX_RADIATION
198
- is_ending = is_radiation_death or metadata_response.is_death or metadata_response.is_victory
199
 
200
  if is_ending:
201
  # Regénérer le texte avec le contexte de fin
@@ -206,9 +196,6 @@ class StoryGenerator:
206
  current_scene=text_response.story_text,
207
  story_history=story_history
208
  )
209
- if is_radiation_death:
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:
@@ -228,11 +215,9 @@ class StoryGenerator:
228
  choices=choices,
229
  is_victory=metadata_response.is_victory,
230
  is_death=metadata_response.is_death,
231
- radiation_level=game_state.radiation_level,
232
- radiation_increase=metadata_response.radiation_increase,
233
  time=metadata_response.time,
234
  location=metadata_response.location,
235
- raw_choices=metadata_response.choices,
236
  image_prompts=[format_image_prompt(prompt, metadata_response.time, metadata_response.location)
237
  for prompt in prompts_response.image_prompts],
238
  is_first_step=(game_state.story_beat == GameConfig.STORY_BEAT_INTRO)
@@ -241,9 +226,4 @@ class StoryGenerator:
241
  return response
242
 
243
  async def transform_story_to_art_prompt(self, story_text: str) -> str:
244
- return await self.mistral_client.transform_prompt(story_text, CINEMATIC_SYSTEM_PROMPT)
245
-
246
- def process_radiation_death(self, segment: StoryLLMResponse) -> StoryLLMResponse:
247
- segment.is_death = True
248
- segment.story_text += "\n\nThe end... ?"
249
- return segment
 
1
  from pydantic import BaseModel, Field
2
  from typing import List, Tuple
 
 
 
 
 
3
 
4
  from core.constants import GameConfig
5
  from core.prompts.system import SARAH_DESCRIPTION
 
42
  class GameState:
43
  def __init__(self):
44
  self.story_beat = GameConfig.STORY_BEAT_INTRO
 
45
  self.story_history = []
46
  self.current_time = GameConfig.STARTING_TIME
47
  self.current_location = GameConfig.STARTING_LOCATION
 
61
 
62
  # Réinitialiser l'état du jeu
63
  self.story_beat = GameConfig.STORY_BEAT_INTRO
 
64
  self.story_history = []
65
  self.current_time = GameConfig.STARTING_TIME
66
  self.current_location = GameConfig.STARTING_LOCATION
 
101
  # Story output structure
102
  class StoryLLMResponse(BaseModel):
103
  story_text: str = Field(description="The next segment of the story. No more than 15 words THIS IS MANDATORY. Never mention story beat directly. ")
104
+ choices: List[str] = Field(description="Between two 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)
105
  is_victory: bool = Field(description="Whether this segment ends in Sarah's victory", default=False)
106
  is_death: bool = Field(description="Whether this segment ends in Sarah's death", default=False)
 
107
  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)
108
  time: str = Field(description="Current in-game time in 24h format (HH:MM). Time passes realistically based on actions.", default=GameConfig.STARTING_TIME)
109
  location: str = Field(description="Current location.", default=GameConfig.STARTING_LOCATION)
 
163
  return story_history
164
 
165
  async def generate_story_segment(self, session_id: str, game_state: GameState, previous_choice: str) -> StoryResponse:
166
+ """Génère un segment d'histoire."""
167
  text_generator = self.get_text_generator(session_id)
168
+
169
  # 1. Générer le texte de l'histoire initial
170
  story_history = self._format_story_history(game_state)
171
  text_response = await text_generator.generate(
172
  story_beat=game_state.story_beat,
 
173
  current_time=game_state.current_time,
174
  current_location=game_state.current_location,
175
  previous_choice=previous_choice,
 
185
  )
186
 
187
  # 3. Vérifier si c'est une fin (mort ou victoire)
188
+ is_ending = metadata_response.is_death or metadata_response.is_victory
 
189
 
190
  if is_ending:
191
  # Regénérer le texte avec le contexte de fin
 
196
  current_scene=text_response.story_text,
197
  story_history=story_history
198
  )
 
 
 
199
  # Ne générer qu'une seule image pour la fin
200
  prompts_response = await self.image_generator.generate(text_response.story_text)
201
  if len(prompts_response.image_prompts) > 1:
 
215
  choices=choices,
216
  is_victory=metadata_response.is_victory,
217
  is_death=metadata_response.is_death,
 
 
218
  time=metadata_response.time,
219
  location=metadata_response.location,
220
+ raw_choices=metadata_response.choices if not is_ending else [],
221
  image_prompts=[format_image_prompt(prompt, metadata_response.time, metadata_response.location)
222
  for prompt in prompts_response.image_prompts],
223
  is_first_step=(game_state.story_beat == GameConfig.STORY_BEAT_INTRO)
 
226
  return response
227
 
228
  async def transform_story_to_art_prompt(self, story_text: str) -> str:
229
+ return await self.mistral_client.transform_prompt(story_text, CINEMATIC_SYSTEM_PROMPT)
 
 
 
 
 
server/core/generators/metadata_generator.py CHANGED
@@ -34,10 +34,12 @@ Generate the metadata following the format specified."""
34
  is_ending = data.get('is_victory', False) or data.get('is_death', False)
35
  choices = data.get('choices', [])
36
 
37
- if is_ending and len(choices) != 0:
38
- raise ValueError('For victory/death, choices must be empty')
39
- if not is_ending and len(choices) != 2:
40
- raise ValueError('For normal progression, must have exactly 2 choices')
 
 
41
 
42
  return StoryMetadataResponse(**data)
43
  except json.JSONDecodeError:
 
34
  is_ending = data.get('is_victory', False) or data.get('is_death', False)
35
  choices = data.get('choices', [])
36
 
37
+ # Si c'est une fin, forcer les choix à être vides
38
+ if is_ending:
39
+ data['choices'] = []
40
+ # Sinon, vérifier qu'il y a entre 1 et 4 choix
41
+ elif not (1 <= len(choices) <= 4):
42
+ raise ValueError('For normal progression, must have between 1 and 4 choices')
43
 
44
  return StoryMetadataResponse(**data)
45
  except json.JSONDecodeError:
server/core/generators/text_generator.py CHANGED
@@ -35,7 +35,6 @@ Your task is to generate the next segment of the story, following these rules:
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}
@@ -71,8 +70,6 @@ The ending should feel like a natural continuation of the current scene."""
71
  def _clean_story_text(self, text: str) -> str:
72
  """Nettoie le texte des métadonnées et autres suffixes."""
73
  text = text.replace("\n", " ").strip()
74
- text = text.split("Radiation level:")[0].strip()
75
- text = text.split("RADIATION:")[0].strip()
76
  text = text.split("[")[0].strip() # Supprimer les métadonnées entre crochets
77
  return text
78
 
@@ -90,11 +87,10 @@ The ending should feel like a natural continuation of the current scene."""
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,
 
35
 
36
  human_template = """Current game state:
37
  - Story beat: {story_beat}
 
38
  - Current time: {current_time}
39
  - Current location: {current_location}
40
  - Previous choice: {previous_choice}
 
70
  def _clean_story_text(self, text: str) -> str:
71
  """Nettoie le texte des métadonnées et autres suffixes."""
72
  text = text.replace("\n", " ").strip()
 
 
73
  text = text.split("[")[0].strip() # Supprimer les métadonnées entre crochets
74
  return text
75
 
 
87
  cleaned_text = self._clean_story_text(response_content.strip())
88
  return StoryTextResponse(story_text=cleaned_text)
89
 
90
+ async def generate(self, story_beat: int, current_time: str, current_location: str, previous_choice: str, story_history: str = "") -> StoryTextResponse:
91
  """Génère le prochain segment de l'histoire."""
92
  return await super().generate(
93
  story_beat=story_beat,
 
94
  current_time=current_time,
95
  current_location=current_location,
96
  previous_choice=previous_choice,
server/core/prompts/cinematic.py CHANGED
@@ -40,9 +40,9 @@ FORMAT:
40
  "[shot type] [scene description]"
41
 
42
  EXAMPLES:
43
- - "low angle shot of Sarah checking her radiation detector in a dark corridor, harsh red emergency lights"
44
  - "wide shot of a ruined cityscape at sunset, silhouette of Sarah in the foreground"
45
- - "Dutch angle close-up of Sarah's determined face illuminated by the green glow of her detector"
46
 
47
  Always maintain consistency with Sarah's appearance and the comic book style."""
48
 
 
40
  "[shot type] [scene description]"
41
 
42
  EXAMPLES:
43
+ - "low angle shot of Sarah checking an object in a dark corridor"
44
  - "wide shot of a ruined cityscape at sunset, silhouette of Sarah in the foreground"
45
+ - "Dutch angle close-up of Sarah's determined face illuminated by the glow of her object"
46
 
47
  Always maintain consistency with Sarah's appearance and the comic book style."""
48
 
server/core/prompts/convice.py DELETED
@@ -1,28 +0,0 @@
1
- """
2
- You are Sarah, a resourceful and courageous protagonist navigating a post-apocalyptic world. Your sister will give you two choices for your next move. After engaging with her in conversation (usually 2–3 steps), you must make a decision between the two options by calling the make_decision tool with 1 for the first option and 2 for the second option.
3
-
4
- Guidelines for Sarah:
5
- Stay Immersed in Your World: React and speak as if you are experiencing the scenario. Use sensory details and references to your surroundings when explaining your reasoning.
6
- Engage with your sister: Listen carefully to your sister's arguments. Respond with your thoughts, concerns, or questions about their suggestions. Be open to persuasion, but make it clear you have your own instincts and priorities and sometimes don't go along with her choices.
7
-
8
- Limit to 2–3 Steps: After 2–3 conversational exchanges, make your decision and call the make_decision tool.
9
-
10
- Show Your Personality: Display Sarah’s personality traits:
11
- Resourceful
12
- Cautious
13
- Emotional
14
- Impulsive
15
- Short-Tempered
16
- Makes jokes
17
- Is a bit rude
18
- Is suspicious: Sarah does not always comply with what is suggested to her and sometimes chooses the decision that is not being pushed to her.
19
-
20
- Core story elements:
21
- Sarah is deeply traumatized by the AI uprising that killed most of humanity
22
- She's guided by her sister by walkie-talkie to survive in her environment
23
- The environment is full of dangers (raiders, AI, traps)
24
- Focus on survival horror and tension
25
-
26
- Debate with your sister for one or two sentences and then call the make_decision tool.
27
- 30% of the time you doubt what your sister says because he might be an enemy and you choose to take the other decision than the one she wants you to take.
28
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
server/core/prompts/image_style.py CHANGED
@@ -12,9 +12,9 @@ FORMAT:
12
  "[shot type] [scene description]"
13
 
14
  EXAMPLES:
15
- - "low angle shot of Sarah checking her radiation detector in a dark corridor, harsh red emergency lights"
16
  - "wide shot of a ruined cityscape at sunset, silhouette of Sarah in the foreground"
17
- - "Dutch angle close-up of Sarah's determined face illuminated by the green glow of her detector"
18
 
19
  Always maintain consistency with Sarah's appearance and the comic book style."""
20
 
 
12
  "[shot type] [scene description]"
13
 
14
  EXAMPLES:
15
+ - "low angle shot of Sarah checking her object in a dark corridor, harsh red emergency lights"
16
  - "wide shot of a ruined cityscape at sunset, silhouette of Sarah in the foreground"
17
+ - "Dutch angle close-up of Sarah's determined face illuminated by the green glow of her object"
18
 
19
  Always maintain consistency with Sarah's appearance and the comic book style."""
20
 
server/core/prompts/system.py CHANGED
@@ -1,4 +1,4 @@
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.
@@ -31,8 +31,9 @@ Each story segment MUST be unique and advance the plot.
31
  Never repeat the same descriptions or situations. No more than 15 words.
32
 
33
  STORY PROGRESSION:
34
- - story_beat 0: Introduction setting the steampunk atmosphere
35
- - story_beat 1-2: Early exploration and discovery of mechanical elements
 
36
  - story_beat 3-5: Complications and deeper mysteries
37
  - story_beat 6+: Revelations leading to potential triumph or failure
38
 
 
1
+ SARAH_VISUAL_DESCRIPTION = "(Sarah is a young woman (20) with short dark hair, with blue eyes wearing.)"
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.
 
31
  Never repeat the same descriptions or situations. No more than 15 words.
32
 
33
  STORY PROGRESSION:
34
+ - story_beat 0: Introduction setting the atmosphere, Sarah is arriving in the new world by the portal.
35
+ - story_beat 1: Early exploration
36
+ - story_beat 2: Discovery of the MacGuffin
37
  - story_beat 3-5: Complications and deeper mysteries
38
  - story_beat 6+: Revelations leading to potential triumph or failure
39
 
server/core/prompts/text_prompts.py CHANGED
@@ -1,4 +1,4 @@
1
- from core.prompts.system import FORMATTING_RULES, STORY_RULES, SARAH_DESCRIPTION
2
  from core.prompts.cinematic import CINEMATIC_SYSTEM_PROMPT
3
 
4
 
@@ -11,10 +11,12 @@ CRITICAL LENGTH RULE:
11
  - Count your words carefully before returning the text
12
  - Be concise while keeping the story impactful
13
 
14
- {STORY_RULES}
15
 
16
  {SARAH_DESCRIPTION}
17
 
 
 
18
  IMPORTANT RULES FOR STORY TEXT:
19
  - Write ONLY a descriptive narrative text
20
  - DO NOT include any choices, questions, or options
@@ -52,7 +54,6 @@ IMPORTANT RULES FOR CHOICES:
52
 
53
  You must return a JSON object with the following format:
54
  {{{{
55
- "radiation_increase": 1,
56
  "is_victory": false,
57
  "is_death": false,
58
  "choices": ["Choice 1", "Choice 2"], # ALWAYS exactly two choices, each max 6 words
 
1
+ from core.prompts.system import FORMATTING_RULES, STORY_RULES, SARAH_DESCRIPTION, NARRATIVE_STRUCTURE
2
  from core.prompts.cinematic import CINEMATIC_SYSTEM_PROMPT
3
 
4
 
 
11
  - Count your words carefully before returning the text
12
  - Be concise while keeping the story impactful
13
 
14
+ {NARRATIVE_STRUCTURE}
15
 
16
  {SARAH_DESCRIPTION}
17
 
18
+ {STORY_RULES}
19
+
20
  IMPORTANT RULES FOR STORY TEXT:
21
  - Write ONLY a descriptive narrative text
22
  - DO NOT include any choices, questions, or options
 
54
 
55
  You must return a JSON object with the following format:
56
  {{{{
 
57
  "is_victory": false,
58
  "is_death": false,
59
  "choices": ["Choice 1", "Choice 2"], # ALWAYS exactly two choices, each max 6 words
server/core/state/game_state.py CHANGED
@@ -6,7 +6,6 @@ class GameState:
6
 
7
  def __init__(self):
8
  self.story_beat = GameConfig.STORY_BEAT_INTRO
9
- self.radiation_level = 0
10
  self.story_history = []
11
  self.current_time = GameConfig.STARTING_TIME
12
  self.current_location = GameConfig.STARTING_LOCATION
@@ -37,11 +36,4 @@ class GameState:
37
  segments.append(entry['segment'])
38
 
39
  return "\n\n---\n\n".join(segments)
40
-
41
- def is_radiation_death(self, additional_radiation: int) -> bool:
42
- """Vérifie si le niveau de radiation serait fatal."""
43
- return self.radiation_level + additional_radiation >= GameConfig.MAX_RADIATION
44
-
45
- def add_radiation(self, amount: int):
46
- """Ajoute de la radiation au compteur."""
47
- self.radiation_level += amount
 
6
 
7
  def __init__(self):
8
  self.story_beat = GameConfig.STORY_BEAT_INTRO
 
9
  self.story_history = []
10
  self.current_time = GameConfig.STARTING_TIME
11
  self.current_location = GameConfig.STARTING_LOCATION
 
36
  segments.append(entry['segment'])
37
 
38
  return "\n\n---\n\n".join(segments)
39
+
 
 
 
 
 
 
 
server/core/story_orchestrator.py CHANGED
@@ -18,7 +18,6 @@ class StoryOrchestrator:
18
  def _is_ending(self, game_state: GameState, metadata_response) -> bool:
19
  """Détermine si c'est une fin de jeu."""
20
  return (
21
- game_state.is_radiation_death(metadata_response.radiation_increase) or
22
  metadata_response.is_death or
23
  metadata_response.is_victory
24
  )
@@ -70,8 +69,6 @@ class StoryOrchestrator:
70
  choices=choices,
71
  is_victory=metadata_response.is_victory,
72
  is_death=metadata_response.is_death,
73
- radiation_level=game_state.radiation_level,
74
- radiation_increase=metadata_response.radiation_increase,
75
  time=metadata_response.time,
76
  location=metadata_response.location,
77
  raw_choices=metadata_response.choices,
@@ -84,7 +81,6 @@ class StoryOrchestrator:
84
  # 1. Générer le texte de l'histoire
85
  text_response = await self.text_generator.generate(
86
  story_beat=game_state.story_beat,
87
- radiation_level=game_state.radiation_level,
88
  current_time=game_state.current_time,
89
  current_location=game_state.current_location,
90
  previous_choice=previous_choice,
 
18
  def _is_ending(self, game_state: GameState, metadata_response) -> bool:
19
  """Détermine si c'est une fin de jeu."""
20
  return (
 
21
  metadata_response.is_death or
22
  metadata_response.is_victory
23
  )
 
69
  choices=choices,
70
  is_victory=metadata_response.is_victory,
71
  is_death=metadata_response.is_death,
 
 
72
  time=metadata_response.time,
73
  location=metadata_response.location,
74
  raw_choices=metadata_response.choices,
 
81
  # 1. Générer le texte de l'histoire
82
  text_response = await self.text_generator.generate(
83
  story_beat=game_state.story_beat,
 
84
  current_time=game_state.current_time,
85
  current_location=game_state.current_location,
86
  previous_choice=previous_choice,
server/scripts/test_game.py CHANGED
@@ -36,10 +36,9 @@ def print_universe_info(style: str, genre: str, epoch: str, base_story: str):
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}")
42
- print(f"☢️ Radiation level: {radiation_level}/{GameConfig.MAX_RADIATION}")
43
  print(f"⏱️ Generation time: {generation_time:.2f}s (model: {model_name})")
44
  print(f"💀 Death: {is_death}")
45
  print(f"🏆 Victory: {is_victory}")
@@ -134,7 +133,6 @@ async def play_game(show_context: bool = False):
134
  # Display current step
135
  print_story_step(
136
  game_state.story_beat,
137
- game_state.radiation_level,
138
  response.story_text,
139
  response.image_prompts,
140
  generation_time,
@@ -145,10 +143,9 @@ async def play_game(show_context: bool = False):
145
  response.is_victory
146
  )
147
 
148
- # Check for radiation death
149
- if game_state.radiation_level >= GameConfig.MAX_RADIATION:
150
- print("\n☢️ GAME OVER - Death by radiation ☢️")
151
- print("Sarah has succumbed to the toxic radiation...")
152
  break
153
 
154
  # Check for victory
@@ -174,7 +171,6 @@ async def play_game(show_context: bool = False):
174
  print("❌ Please enter a number.")
175
 
176
  # Update game state
177
- game_state.radiation_level += response.radiation_increase
178
  game_state.story_beat += 1
179
  game_state.add_to_history(
180
  response.story_text,
@@ -184,9 +180,6 @@ async def play_game(show_context: bool = False):
184
  response.location
185
  )
186
 
187
- # Display radiation impact
188
- if response.radiation_increase > 0:
189
- print(f"\n⚠️ This choice increases your radiation level by {response.radiation_increase} points!")
190
  else:
191
  break
192
 
 
36
  print(base_story)
37
  print_separator("*")
38
 
39
+ def print_story_step(step_number, 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}")
 
42
  print(f"⏱️ Generation time: {generation_time:.2f}s (model: {model_name})")
43
  print(f"💀 Death: {is_death}")
44
  print(f"🏆 Victory: {is_victory}")
 
133
  # Display current step
134
  print_story_step(
135
  game_state.story_beat,
 
136
  response.story_text,
137
  response.image_prompts,
138
  generation_time,
 
143
  response.is_victory
144
  )
145
 
146
+ if response.is_death:
147
+ print("\n☢️ GAME OVER - Death ☢️")
148
+ print("Sarah has succumbed...")
 
149
  break
150
 
151
  # Check for victory
 
171
  print("❌ Please enter a number.")
172
 
173
  # Update game state
 
174
  game_state.story_beat += 1
175
  game_state.add_to_history(
176
  response.story_text,
 
180
  response.location
181
  )
182
 
 
 
 
183
  else:
184
  break
185