diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000000000000000000000000000000000000..71cd0b38c30b74bbbb3396cdc9830543a874a0d0
Binary files /dev/null and b/.DS_Store differ
diff --git a/Dockerfile b/Dockerfile
index 89fabd8f7cc8e3cbc18e80d977a2f07efeda0a5e..9120a6b03f5f0e60eb5d20d13d3dcdce945fef5e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,7 +4,7 @@ COPY client/package*.json ./
RUN npm install
COPY client/ ./
ENV VITE_API_URL=https://mistral-ai-game-jam-dont-lookup.hf.space
-RUN npm run build
+RUN mkdir -p dist && npm run build
FROM python:3.9-slim
WORKDIR /app
diff --git a/client/src/App.jsx b/client/src/App.jsx
index 487a5a9e63ee1d0495e4f5016c61e017db82c4a1..d8270a514e112777027fed9f1eb9e98982ede9f2 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -5,41 +5,117 @@ import {
Button,
Box,
Typography,
- List,
- ListItem,
- ListItemText,
LinearProgress,
} from "@mui/material";
import RestartAltIcon from "@mui/icons-material/RestartAlt";
import axios from "axios";
+import { ComicLayout } from "./layouts/ComicLayout";
+import { getNextPanelDimensions } from "./layouts/utils";
// Get API URL from environment or default to localhost in development
const API_URL = import.meta.env.VITE_API_URL || "http://localhost:8000";
+// Generate a unique client ID
+const CLIENT_ID = `client_${Math.random().toString(36).substring(2)}`;
+
+// Create axios instance with default config
+const api = axios.create({
+ headers: {
+ "x-client-id": CLIENT_ID,
+ },
+});
+
+// Function to convert text with ** to bold elements
+const formatTextWithBold = (text) => {
+ if (!text) return "";
+ const parts = text.split(/(\*\*.*?\*\*)/g);
+ return parts.map((part, index) => {
+ if (part.startsWith("**") && part.endsWith("**")) {
+ // Remove the ** and wrap in bold
+ return {part.slice(2, -2)};
+ }
+ return part;
+ });
+};
+
function App() {
const [storySegments, setStorySegments] = useState([]);
const [currentChoices, setCurrentChoices] = useState([]);
const [isLoading, setIsLoading] = useState(false);
+ const [isDebugMode, setIsDebugMode] = useState(false);
const isInitializedRef = useRef(false);
+ const currentImageRequestRef = useRef(null);
+ const pendingImageRequests = useRef(new Set()); // Track pending image requests
- const generateImageForStory = async (storyText) => {
+ const generateImageForStory = async (storyText, segmentIndex) => {
try {
+ // Cancel previous request if it exists
+ if (currentImageRequestRef.current) {
+ currentImageRequestRef.current.abort();
+ }
+
+ // Add this segment to pending requests
+ pendingImageRequests.current.add(segmentIndex);
+
console.log("Generating image for story:", storyText);
- const response = await axios.post(`${API_URL}/api/generate-image`, {
- prompt: `Comic book style scene: ${storyText}`,
- width: 512,
- height: 512,
- });
+ const dimensions = getNextPanelDimensions(storySegments);
+ console.log("[DEBUG] Story segments:", storySegments);
+ console.log("[DEBUG] Dimensions object:", dimensions);
+ console.log(
+ "[DEBUG] Width:",
+ dimensions?.width,
+ "Height:",
+ dimensions?.height
+ );
+
+ if (!dimensions || !dimensions.width || !dimensions.height) {
+ console.error("[ERROR] Invalid dimensions:", dimensions);
+ pendingImageRequests.current.delete(segmentIndex);
+ return null;
+ }
+
+ // Create new AbortController for this request
+ const abortController = new AbortController();
+ currentImageRequestRef.current = abortController;
- console.log("Image generation response:", response.data);
+ const response = await api.post(
+ `${API_URL}/api/${isDebugMode ? "test/" : ""}generate-image`,
+ {
+ prompt: `Comic book style scene: ${storyText}`,
+ width: dimensions.width,
+ height: dimensions.height,
+ },
+ {
+ signal: abortController.signal,
+ }
+ );
+
+ // Remove from pending requests
+ pendingImageRequests.current.delete(segmentIndex);
if (response.data.success) {
- console.log("Image URL length:", response.data.image_base64.length);
return response.data.image_base64;
}
return null;
} catch (error) {
- console.error("Error generating image:", error);
+ if (axios.isCancel(error)) {
+ console.log("Image request cancelled for segment", segmentIndex);
+ // On met quand même à jour le segment pour arrêter le spinner
+ setStorySegments((prev) => {
+ const updatedSegments = [...prev];
+ if (updatedSegments[segmentIndex]) {
+ updatedSegments[segmentIndex] = {
+ ...updatedSegments[segmentIndex],
+ image_base64: null,
+ imageRequestCancelled: true, // Flag pour indiquer que la requête a été annulée
+ };
+ }
+ return updatedSegments;
+ });
+ } else {
+ console.error("Error generating image:", error);
+ }
+ pendingImageRequests.current.delete(segmentIndex);
return null;
}
};
@@ -47,51 +123,91 @@ function App() {
const handleStoryAction = async (action, choiceId = null) => {
setIsLoading(true);
try {
- const response = await axios.post(`${API_URL}/api/chat`, {
- message: action,
- choice_id: choiceId,
- });
-
- // Générer l'image pour ce segment
- const imageUrl = await generateImageForStory(response.data.story_text);
- console.log(
- "Generated image URL:",
- imageUrl ? "Image received" : "No image"
+ // 1. D'abord, obtenir l'histoire
+ const response = await api.post(
+ `${API_URL}/api/${isDebugMode ? "test/" : ""}chat`,
+ {
+ message: action,
+ choice_id: choiceId,
+ }
);
+ // 2. Créer le nouveau segment sans image
+ const newSegment = {
+ text: formatTextWithBold(response.data.story_text),
+ isChoice: false,
+ isDeath: response.data.is_death,
+ isVictory: response.data.is_victory,
+ radiationLevel: response.data.radiation_level,
+ is_first_step: response.data.is_first_step,
+ is_last_step: response.data.is_last_step,
+ image_base64: null,
+ };
+
+ let segmentIndex;
+ // 3. Mettre à jour l'état avec le nouveau segment
if (action === "restart") {
- setStorySegments([
- {
- text: response.data.story_text,
- isChoice: false,
- isDeath: response.data.is_death,
- isVictory: response.data.is_victory,
- radiationLevel: response.data.radiation_level,
- imageUrl: imageUrl,
- },
- ]);
+ setStorySegments([newSegment]);
+ segmentIndex = 0;
} else {
- setStorySegments((prev) => [
- ...prev,
- {
- text: response.data.story_text,
- isChoice: false,
- isDeath: response.data.is_death,
- isVictory: response.data.is_victory,
- radiationLevel: response.data.radiation_level,
- imageUrl: imageUrl,
- },
- ]);
+ setStorySegments((prev) => {
+ segmentIndex = prev.length;
+ return [...prev, newSegment];
+ });
}
+ // 4. Mettre à jour les choix immédiatement
setCurrentChoices(response.data.choices);
+
+ // 5. Désactiver le loading car l'histoire est affichée
+ setIsLoading(false);
+
+ // 6. Tenter de générer l'image en arrière-plan
+ try {
+ const image_base64 = await generateImageForStory(
+ response.data.story_text,
+ segmentIndex
+ );
+ if (image_base64) {
+ setStorySegments((prev) => {
+ const updatedSegments = [...prev];
+ if (updatedSegments[segmentIndex]) {
+ updatedSegments[segmentIndex] = {
+ ...updatedSegments[segmentIndex],
+ image_base64: image_base64,
+ };
+ }
+ return updatedSegments;
+ });
+ }
+ } catch (imageError) {
+ console.error("Error generating image:", imageError);
+ }
} catch (error) {
console.error("Error:", error);
- setStorySegments((prev) => [
- ...prev,
- { text: "Connection lost with the storyteller...", isChoice: false },
- ]);
- } finally {
+ // En cas d'erreur, créer un segment d'erreur qui permet de continuer
+ const errorSegment = {
+ text: "Le conteur d'histoires est temporairement indisponible. Veuillez réessayer dans quelques instants...",
+ isChoice: false,
+ isDeath: false,
+ isVictory: false,
+ radiationLevel:
+ storySegments.length > 0
+ ? storySegments[storySegments.length - 1].radiationLevel
+ : 0,
+ image_base64: null,
+ };
+
+ // Ajouter le segment d'erreur et permettre de réessayer
+ if (action === "restart") {
+ setStorySegments([errorSegment]);
+ } else {
+ setStorySegments((prev) => [...prev, errorSegment]);
+ }
+
+ // Donner l'option de réessayer
+ setCurrentChoices([{ id: 1, text: "Réessayer" }]);
+
setIsLoading(false);
}
};
@@ -105,158 +221,182 @@ function App() {
}, []); // Empty dependency array since we're using a ref
const handleChoice = async (choiceId) => {
- // Add the chosen option to the story
+ // Si c'est l'option "Réessayer", on relance la dernière action
+ if (currentChoices.length === 1 && currentChoices[0].text === "Réessayer") {
+ // Supprimer le segment d'erreur
+ setStorySegments((prev) => prev.slice(0, -1));
+ // Réessayer la dernière action
+ await handleStoryAction(
+ "choice",
+ storySegments[storySegments.length - 2]?.choiceId || null
+ );
+ return;
+ }
+
+ // Comportement normal pour les autres choix
+ const choice = currentChoices.find((c) => c.id === choiceId);
setStorySegments((prev) => [
...prev,
{
- text: currentChoices.find((c) => c.id === choiceId).text,
+ text: choice.text,
isChoice: true,
+ choiceId: choiceId, // Stocker l'ID du choix pour pouvoir réessayer
},
]);
+
// Continue the story with this choice
await handleStoryAction("choice", choiceId);
};
+ // Filter out choice segments
+ const nonChoiceSegments = storySegments.filter(
+ (segment) => !segment.isChoice
+ );
+
return (
-
-
+ {/*
-
-
- Echoes of Influence
-
-
- 0 &&
- storySegments[storySegments.length - 1].radiationLevel >= 7
- ? "error.light"
- : "inherit",
- },
- }}
- >
-
- Radiation:{" "}
-
- {storySegments.length > 0
- ? `${
- storySegments[storySegments.length - 1].radiationLevel
- }/10`
- : "0/10"}
-
-
-
- }
- onClick={() => handleStoryAction("restart")}
- disabled={isLoading}
- >
- Restart
-
+
+ 0 &&
+ storySegments[storySegments.length - 1].radiationLevel >= 7
+ ? "error.light"
+ : "inherit",
+ },
+ }}
+ >
+
+ Radiation:{" "}
+
+ {storySegments.length > 0
+ ? `${
+ storySegments[storySegments.length - 1].radiationLevel
+ }/10`
+ : "0/10"}
+
+
+ }
+ onClick={() => handleStoryAction("restart")}
+ disabled={isLoading}
+ >
+ Restart
+
+
+ */}
- {isLoading && }
+ {/* {isLoading && } */}
-
- {storySegments.map((segment, index) => (
-
-
-
- {!segment.isChoice && segment.imageUrl && (
-
-
-
- )}
-
-
- ))}
-
-
- {currentChoices.length > 0 && (
-
+
+
+
+
+
+ {currentChoices.length > 0 ? (
+
{currentChoices.map((choice) => (
))}
- )}
-
-
+ ) : storySegments.length > 0 &&
+ storySegments[storySegments.length - 1].is_last_step ? (
+
+
+
+ ) : null}
+
+
);
}
diff --git a/client/src/fonts/Action-Man/Action-Man-Bold-Italic.woff2 b/client/src/fonts/Action-Man/Action-Man-Bold-Italic.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..0ec18e70639c1a6aac33972ffb7492455f546c9f
Binary files /dev/null and b/client/src/fonts/Action-Man/Action-Man-Bold-Italic.woff2 differ
diff --git a/client/src/fonts/Action-Man/Action-Man-Bold.woff2 b/client/src/fonts/Action-Man/Action-Man-Bold.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..72034cc187a9d6c6609973979e69743055823648
Binary files /dev/null and b/client/src/fonts/Action-Man/Action-Man-Bold.woff2 differ
diff --git a/client/src/fonts/Action-Man/Action-Man-Italic.woff2 b/client/src/fonts/Action-Man/Action-Man-Italic.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..e478e7fc3c91a8d68cdb95537294e1f3929cc218
Binary files /dev/null and b/client/src/fonts/Action-Man/Action-Man-Italic.woff2 differ
diff --git a/client/src/fonts/Action-Man/Action-Man.woff2 b/client/src/fonts/Action-Man/Action-Man.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..722fae9600d32df498732426b0947d07bf9e657f
Binary files /dev/null and b/client/src/fonts/Action-Man/Action-Man.woff2 differ
diff --git a/client/src/fonts/Action-Man/Action_Man_Extended-webfont.woff b/client/src/fonts/Action-Man/Action_Man_Extended-webfont.woff
new file mode 100644
index 0000000000000000000000000000000000000000..24170e27ab9e6868256367f49dc5534451069bac
Binary files /dev/null and b/client/src/fonts/Action-Man/Action_Man_Extended-webfont.woff differ
diff --git a/client/src/fonts/Action-Man/Action_Man_Extended_Bold-webfont.woff2 b/client/src/fonts/Action-Man/Action_Man_Extended_Bold-webfont.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..f12a7dc726958e27e242e5ad419db69be268677c
Binary files /dev/null and b/client/src/fonts/Action-Man/Action_Man_Extended_Bold-webfont.woff2 differ
diff --git a/client/src/fonts/Action-Man/Action_Man_Extended_Bold_Italic-webfont.woff b/client/src/fonts/Action-Man/Action_Man_Extended_Bold_Italic-webfont.woff
new file mode 100644
index 0000000000000000000000000000000000000000..7f55b449c80687d0d20610a72d8518d7c0b24fcf
Binary files /dev/null and b/client/src/fonts/Action-Man/Action_Man_Extended_Bold_Italic-webfont.woff differ
diff --git a/client/src/fonts/Action-Man/Action_Man_Extended_Italic-webfont.woff b/client/src/fonts/Action-Man/Action_Man_Extended_Italic-webfont.woff
new file mode 100644
index 0000000000000000000000000000000000000000..8d5e36ca58b1c4dd372b3898419d25d54debda5f
Binary files /dev/null and b/client/src/fonts/Action-Man/Action_Man_Extended_Italic-webfont.woff differ
diff --git a/client/src/fonts/Action-Man/Action_Man_Shaded-webfont.woff b/client/src/fonts/Action-Man/Action_Man_Shaded-webfont.woff
new file mode 100644
index 0000000000000000000000000000000000000000..50261bc91c6e88bd2ab5fc916c3325c8c4941fe8
Binary files /dev/null and b/client/src/fonts/Action-Man/Action_Man_Shaded-webfont.woff differ
diff --git a/client/src/fonts/Action-Man/Action_Man_Shaded_Italic-webfont.woff b/client/src/fonts/Action-Man/Action_Man_Shaded_Italic-webfont.woff
new file mode 100644
index 0000000000000000000000000000000000000000..e3f29d18814f2936ef043678e4cee902a5401085
Binary files /dev/null and b/client/src/fonts/Action-Man/Action_Man_Shaded_Italic-webfont.woff differ
diff --git a/client/src/fonts/DigitalStripBB/DigitalStripBB_BoldItal.woff2 b/client/src/fonts/DigitalStripBB/DigitalStripBB_BoldItal.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..a8d1d84d091cbddda3199044686f5bf715178684
Binary files /dev/null and b/client/src/fonts/DigitalStripBB/DigitalStripBB_BoldItal.woff2 differ
diff --git a/client/src/fonts/DigitalStripBB/DigitalStripBB_Ital.woff2 b/client/src/fonts/DigitalStripBB/DigitalStripBB_Ital.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..af804905890e4ed1f9ba058c20174915e743eb99
Binary files /dev/null and b/client/src/fonts/DigitalStripBB/DigitalStripBB_Ital.woff2 differ
diff --git a/client/src/fonts/DigitalStripBB/DigitalStripBB_Reg.woff2 b/client/src/fonts/DigitalStripBB/DigitalStripBB_Reg.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..4b3796ea2737bd7337031865de11c0bc4c9d5a0e
Binary files /dev/null and b/client/src/fonts/DigitalStripBB/DigitalStripBB_Reg.woff2 differ
diff --git a/client/src/fonts/Karantula/Karantula-Bold.woff2 b/client/src/fonts/Karantula/Karantula-Bold.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..0c19156faf8f3bc29ed8cd591920afc8f861b808
Binary files /dev/null and b/client/src/fonts/Karantula/Karantula-Bold.woff2 differ
diff --git a/client/src/fonts/Karantula/Karantula-Italic-Bold.woff2 b/client/src/fonts/Karantula/Karantula-Italic-Bold.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..4ed852dbfb5e48464efed63cae53cd61ef657b94
Binary files /dev/null and b/client/src/fonts/Karantula/Karantula-Italic-Bold.woff2 differ
diff --git a/client/src/fonts/Karantula/Karantula-Italic.woff2 b/client/src/fonts/Karantula/Karantula-Italic.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..3d6f55dee0e59ec29ec9b4a373a5dacda724694b
Binary files /dev/null and b/client/src/fonts/Karantula/Karantula-Italic.woff2 differ
diff --git a/client/src/fonts/Karantula/Karantula.woff2 b/client/src/fonts/Karantula/Karantula.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..9e9c9d37b1420bf6d1d78516d5786e2058f153db
Binary files /dev/null and b/client/src/fonts/Karantula/Karantula.woff2 differ
diff --git a/client/src/fonts/Komika-Display/Komika-Display.woff b/client/src/fonts/Komika-Display/Komika-Display.woff
new file mode 100644
index 0000000000000000000000000000000000000000..9765c0aa82a06e85eb4542681a2ddedb20d7ffce
Binary files /dev/null and b/client/src/fonts/Komika-Display/Komika-Display.woff differ
diff --git a/client/src/fonts/Komika-Display/Komika_display_bold-webfont.woff b/client/src/fonts/Komika-Display/Komika_display_bold-webfont.woff
new file mode 100644
index 0000000000000000000000000000000000000000..2a86cc0a3c397db156537b46d2240840ce5b0502
Binary files /dev/null and b/client/src/fonts/Komika-Display/Komika_display_bold-webfont.woff differ
diff --git a/client/src/fonts/Komika-Display/Komika_display_kaps-webfont.woff b/client/src/fonts/Komika-Display/Komika_display_kaps-webfont.woff
new file mode 100644
index 0000000000000000000000000000000000000000..d607dd4e64f3f7146cfaebcb6599294747d3c859
Binary files /dev/null and b/client/src/fonts/Komika-Display/Komika_display_kaps-webfont.woff differ
diff --git a/client/src/fonts/Komika-Display/Komika_display_kaps_bold-webfont.woff b/client/src/fonts/Komika-Display/Komika_display_kaps_bold-webfont.woff
new file mode 100644
index 0000000000000000000000000000000000000000..04302d64f473c063c180ceb7c7fb8967376e9186
Binary files /dev/null and b/client/src/fonts/Komika-Display/Komika_display_kaps_bold-webfont.woff differ
diff --git a/client/src/fonts/Komika-Hand/Komika-Hand-Bold-Italic.woff2 b/client/src/fonts/Komika-Hand/Komika-Hand-Bold-Italic.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..4d00e7eed6ef125b41ca834e5dca5da7d98b75ca
Binary files /dev/null and b/client/src/fonts/Komika-Hand/Komika-Hand-Bold-Italic.woff2 differ
diff --git a/client/src/fonts/Komika-Hand/Komika-Hand-Bold.woff2 b/client/src/fonts/Komika-Hand/Komika-Hand-Bold.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..6cf07d240b46c268602c2f68534b41d3b626ef9c
Binary files /dev/null and b/client/src/fonts/Komika-Hand/Komika-Hand-Bold.woff2 differ
diff --git a/client/src/fonts/Komika-Hand/Komika-Hand-Italic.woff2 b/client/src/fonts/Komika-Hand/Komika-Hand-Italic.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..fad3b3f7db445d37fad067996b9c9e010da55d0c
Binary files /dev/null and b/client/src/fonts/Komika-Hand/Komika-Hand-Italic.woff2 differ
diff --git a/client/src/fonts/Komika-Hand/Komika-Hand.woff2 b/client/src/fonts/Komika-Hand/Komika-Hand.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..f4a697cd7533400c909841fbe79268e87de9cfc8
Binary files /dev/null and b/client/src/fonts/Komika-Hand/Komika-Hand.woff2 differ
diff --git a/client/src/fonts/Komika-Hand/Komika_Parch.woff2 b/client/src/fonts/Komika-Hand/Komika_Parch.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..0e2481446a7becd69821b263192de98f8f4a33f9
Binary files /dev/null and b/client/src/fonts/Komika-Hand/Komika_Parch.woff2 differ
diff --git a/client/src/fonts/Komika-Text/KOMTXKBI-webfont.woff b/client/src/fonts/Komika-Text/KOMTXKBI-webfont.woff
new file mode 100644
index 0000000000000000000000000000000000000000..408a051cc071bbbcbf998ac055ca1edab4a56334
Binary files /dev/null and b/client/src/fonts/Komika-Text/KOMTXKBI-webfont.woff differ
diff --git a/client/src/fonts/Komika-Text/KOMTXTBI-webfont.woff b/client/src/fonts/Komika-Text/KOMTXTBI-webfont.woff
new file mode 100644
index 0000000000000000000000000000000000000000..ce1185b368884e3aa6009d1c4d2dec4360beed2b
Binary files /dev/null and b/client/src/fonts/Komika-Text/KOMTXTBI-webfont.woff differ
diff --git a/client/src/fonts/Komika-Text/KOMTXTB_-webfont.woff b/client/src/fonts/Komika-Text/KOMTXTB_-webfont.woff
new file mode 100644
index 0000000000000000000000000000000000000000..a3407ce6b4bbf99293d1f68d925ce938148b6a5c
Binary files /dev/null and b/client/src/fonts/Komika-Text/KOMTXTB_-webfont.woff differ
diff --git a/client/src/fonts/Komika-Text/KOMTXTI_-webfont.woff b/client/src/fonts/Komika-Text/KOMTXTI_-webfont.woff
new file mode 100644
index 0000000000000000000000000000000000000000..f1f999e0edc910b0883e20f3f3365993e541ef2a
Binary files /dev/null and b/client/src/fonts/Komika-Text/KOMTXTI_-webfont.woff differ
diff --git a/client/src/fonts/Komika-Text/KOMTXTKB-webfont.woff b/client/src/fonts/Komika-Text/KOMTXTKB-webfont.woff
new file mode 100644
index 0000000000000000000000000000000000000000..9e7599c346deb8c9ffa13be7f68e78099f939ba1
Binary files /dev/null and b/client/src/fonts/Komika-Text/KOMTXTKB-webfont.woff differ
diff --git a/client/src/fonts/Komika-Text/KOMTXTKI-webfont.woff b/client/src/fonts/Komika-Text/KOMTXTKI-webfont.woff
new file mode 100644
index 0000000000000000000000000000000000000000..6b156d23e5f50b7598d16c100bda49311074a78a
Binary files /dev/null and b/client/src/fonts/Komika-Text/KOMTXTKI-webfont.woff differ
diff --git a/client/src/fonts/Komika-Text/KOMTXTK_-webfont.woff b/client/src/fonts/Komika-Text/KOMTXTK_-webfont.woff
new file mode 100644
index 0000000000000000000000000000000000000000..b0d646d54748892d69cb47e70fb06fc281cb01a2
Binary files /dev/null and b/client/src/fonts/Komika-Text/KOMTXTK_-webfont.woff differ
diff --git a/client/src/fonts/Komika-Text/KOMTXTTI-webfont.woff b/client/src/fonts/Komika-Text/KOMTXTTI-webfont.woff
new file mode 100644
index 0000000000000000000000000000000000000000..5620b4e071854d27818adfbe208e7b0e79138d31
Binary files /dev/null and b/client/src/fonts/Komika-Text/KOMTXTTI-webfont.woff differ
diff --git a/client/src/fonts/Komika-Text/KOMTXTT_-webfont.woff b/client/src/fonts/Komika-Text/KOMTXTT_-webfont.woff
new file mode 100644
index 0000000000000000000000000000000000000000..c4087f02611a4b625913ec9e8574c89ddc76f6f4
Binary files /dev/null and b/client/src/fonts/Komika-Text/KOMTXTT_-webfont.woff differ
diff --git a/client/src/fonts/Komika-Text/KOMTXT__-webfont.woff b/client/src/fonts/Komika-Text/KOMTXT__-webfont.woff
new file mode 100644
index 0000000000000000000000000000000000000000..214a90f28059584ab95fb1e9abe301ea5c54c8d0
Binary files /dev/null and b/client/src/fonts/Komika-Text/KOMTXT__-webfont.woff differ
diff --git a/client/src/fonts/Manoskope/MANOSKOPE-Bold.woff2 b/client/src/fonts/Manoskope/MANOSKOPE-Bold.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..76e2ebabf3cc696a480abd76e8f48faf273600f5
Binary files /dev/null and b/client/src/fonts/Manoskope/MANOSKOPE-Bold.woff2 differ
diff --git a/client/src/fonts/Paete-Round/Paete-Round-Bold-Italic.woff2 b/client/src/fonts/Paete-Round/Paete-Round-Bold-Italic.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..e2c08fb187738488fa931f12e8e43a3ec594bc39
Binary files /dev/null and b/client/src/fonts/Paete-Round/Paete-Round-Bold-Italic.woff2 differ
diff --git a/client/src/fonts/Paete-Round/Paete-Round-Bold.woff2 b/client/src/fonts/Paete-Round/Paete-Round-Bold.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..309a2c0caff1759bd4656d318cb22a368dc620cd
Binary files /dev/null and b/client/src/fonts/Paete-Round/Paete-Round-Bold.woff2 differ
diff --git a/client/src/fonts/Paete-Round/Paete-Round-Italic.woff2 b/client/src/fonts/Paete-Round/Paete-Round-Italic.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..5558b83264986de53f589dd8882ebd6aa4ebe49d
Binary files /dev/null and b/client/src/fonts/Paete-Round/Paete-Round-Italic.woff2 differ
diff --git a/client/src/fonts/Paete-Round/Paete-Round.woff2 b/client/src/fonts/Paete-Round/Paete-Round.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..a42e4132c8b36d4647d461d884f614e1e559d03b
Binary files /dev/null and b/client/src/fonts/Paete-Round/Paete-Round.woff2 differ
diff --git a/client/src/fonts/Qarmic-Sans/Qarmic-Sans-Abridged.woff2 b/client/src/fonts/Qarmic-Sans/Qarmic-Sans-Abridged.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..92776051c312cfee3ba0414beda9a794ff62cd0b
Binary files /dev/null and b/client/src/fonts/Qarmic-Sans/Qarmic-Sans-Abridged.woff2 differ
diff --git a/client/src/fonts/SF-Arch-Rival/SF-Arch-Rival-Bold-Italic.woff2 b/client/src/fonts/SF-Arch-Rival/SF-Arch-Rival-Bold-Italic.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..fd78c38ba461af3d204835e0f2b7635d70993752
Binary files /dev/null and b/client/src/fonts/SF-Arch-Rival/SF-Arch-Rival-Bold-Italic.woff2 differ
diff --git a/client/src/fonts/SF-Arch-Rival/SF-Arch-Rival-Bold.woff2 b/client/src/fonts/SF-Arch-Rival/SF-Arch-Rival-Bold.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..172ddf351887d9e4c9a2e0a2caa526d4eaa31db5
Binary files /dev/null and b/client/src/fonts/SF-Arch-Rival/SF-Arch-Rival-Bold.woff2 differ
diff --git a/client/src/fonts/SF-Arch-Rival/SF-Arch-Rival-Extended-Bold-Italic.woff2 b/client/src/fonts/SF-Arch-Rival/SF-Arch-Rival-Extended-Bold-Italic.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..42935fe3900b605ff5207c7d53ed265d0ad937a8
Binary files /dev/null and b/client/src/fonts/SF-Arch-Rival/SF-Arch-Rival-Extended-Bold-Italic.woff2 differ
diff --git a/client/src/fonts/SF-Arch-Rival/SF-Arch-Rival-Extended-Bold.woff2 b/client/src/fonts/SF-Arch-Rival/SF-Arch-Rival-Extended-Bold.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..9ba4edfcfc3ecdb44425cabdda6592130dfeb92b
Binary files /dev/null and b/client/src/fonts/SF-Arch-Rival/SF-Arch-Rival-Extended-Bold.woff2 differ
diff --git a/client/src/fonts/SF-Arch-Rival/SF-Arch-Rival-Extended-Italic.woff2 b/client/src/fonts/SF-Arch-Rival/SF-Arch-Rival-Extended-Italic.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..cfbeaee58e94061eeaf9e3ce464ea3a7be95253e
Binary files /dev/null and b/client/src/fonts/SF-Arch-Rival/SF-Arch-Rival-Extended-Italic.woff2 differ
diff --git a/client/src/fonts/SF-Arch-Rival/SF-Arch-Rival-Extended.woff2 b/client/src/fonts/SF-Arch-Rival/SF-Arch-Rival-Extended.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..85fd449b3166da2ea7da35d1739fcd197453ff4e
Binary files /dev/null and b/client/src/fonts/SF-Arch-Rival/SF-Arch-Rival-Extended.woff2 differ
diff --git a/client/src/fonts/SF-Arch-Rival/SF-Arch-Rival-Italic.woff2 b/client/src/fonts/SF-Arch-Rival/SF-Arch-Rival-Italic.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..752a35a6aa080f131641dbf15c0f0b1d1b0290a9
Binary files /dev/null and b/client/src/fonts/SF-Arch-Rival/SF-Arch-Rival-Italic.woff2 differ
diff --git a/client/src/fonts/SF-Arch-Rival/SF-Arch-Rival.woff2 b/client/src/fonts/SF-Arch-Rival/SF-Arch-Rival.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..50adb0537ff5376b090d4605643ee5957ded1ff1
Binary files /dev/null and b/client/src/fonts/SF-Arch-Rival/SF-Arch-Rival.woff2 differ
diff --git a/client/src/fonts/SF-Cartoonist-Hand/SF-Cartoonist-Hand-Bold-Italic.woff2 b/client/src/fonts/SF-Cartoonist-Hand/SF-Cartoonist-Hand-Bold-Italic.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..2df36ebe2bb9a9562c07e7e543a16ad4eaab0202
Binary files /dev/null and b/client/src/fonts/SF-Cartoonist-Hand/SF-Cartoonist-Hand-Bold-Italic.woff2 differ
diff --git a/client/src/fonts/SF-Cartoonist-Hand/SF-Cartoonist-Hand-Bold.woff2 b/client/src/fonts/SF-Cartoonist-Hand/SF-Cartoonist-Hand-Bold.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..5d2cb4be454e3031e1a2bd35482875997872754f
Binary files /dev/null and b/client/src/fonts/SF-Cartoonist-Hand/SF-Cartoonist-Hand-Bold.woff2 differ
diff --git a/client/src/fonts/SF-Cartoonist-Hand/SF-Cartoonist-Hand-Italic.woff2 b/client/src/fonts/SF-Cartoonist-Hand/SF-Cartoonist-Hand-Italic.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..b1ab5e8e14aae791c38d31309054508d5be9271b
Binary files /dev/null and b/client/src/fonts/SF-Cartoonist-Hand/SF-Cartoonist-Hand-Italic.woff2 differ
diff --git a/client/src/fonts/SF-Cartoonist-Hand/SF-Cartoonist-Hand-SC-Bold-Italic.woff2 b/client/src/fonts/SF-Cartoonist-Hand/SF-Cartoonist-Hand-SC-Bold-Italic.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..853ccc1db12a07388d52efcaa7fed6a83c205916
Binary files /dev/null and b/client/src/fonts/SF-Cartoonist-Hand/SF-Cartoonist-Hand-SC-Bold-Italic.woff2 differ
diff --git a/client/src/fonts/SF-Cartoonist-Hand/SF-Cartoonist-Hand-SC-Bold.woff2 b/client/src/fonts/SF-Cartoonist-Hand/SF-Cartoonist-Hand-SC-Bold.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..995f444bac9982b148a1fe16fccb6aab3cf91f61
Binary files /dev/null and b/client/src/fonts/SF-Cartoonist-Hand/SF-Cartoonist-Hand-SC-Bold.woff2 differ
diff --git a/client/src/fonts/SF-Cartoonist-Hand/SF-Cartoonist-Hand-SC-Italic.woff2 b/client/src/fonts/SF-Cartoonist-Hand/SF-Cartoonist-Hand-SC-Italic.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..7e8e9cb3d8a5fe9994e301fa9383f41c922f5e28
Binary files /dev/null and b/client/src/fonts/SF-Cartoonist-Hand/SF-Cartoonist-Hand-SC-Italic.woff2 differ
diff --git a/client/src/fonts/SF-Cartoonist-Hand/SF-Cartoonist-Hand-SC.woff2 b/client/src/fonts/SF-Cartoonist-Hand/SF-Cartoonist-Hand-SC.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..e82f4919ff959cf103ce1d731d19fb7c39aa5326
Binary files /dev/null and b/client/src/fonts/SF-Cartoonist-Hand/SF-Cartoonist-Hand-SC.woff2 differ
diff --git a/client/src/fonts/SF-Cartoonist-Hand/SF-Cartoonist-Hand.woff2 b/client/src/fonts/SF-Cartoonist-Hand/SF-Cartoonist-Hand.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..4c8632923efd9e4a6127edcec633de7202d58cde
Binary files /dev/null and b/client/src/fonts/SF-Cartoonist-Hand/SF-Cartoonist-Hand.woff2 differ
diff --git a/client/src/fonts/SF-Toontime/SF-Toontime-Blotch-Italic.woff2 b/client/src/fonts/SF-Toontime/SF-Toontime-Blotch-Italic.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..92fd94e29457a799f8d8a9421d0b0cd33e08a75e
Binary files /dev/null and b/client/src/fonts/SF-Toontime/SF-Toontime-Blotch-Italic.woff2 differ
diff --git a/client/src/fonts/SF-Toontime/SF-Toontime-Blotch.woff2 b/client/src/fonts/SF-Toontime/SF-Toontime-Blotch.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..f828eabb9994c81d31665c7043a464b330681833
Binary files /dev/null and b/client/src/fonts/SF-Toontime/SF-Toontime-Blotch.woff2 differ
diff --git a/client/src/fonts/SF-Toontime/SF-Toontime-Bold-Italic.woff2 b/client/src/fonts/SF-Toontime/SF-Toontime-Bold-Italic.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..d28ed904cbc5e3afeebb48c7a6660d13f66fe894
Binary files /dev/null and b/client/src/fonts/SF-Toontime/SF-Toontime-Bold-Italic.woff2 differ
diff --git a/client/src/fonts/SF-Toontime/SF-Toontime-Bold.woff2 b/client/src/fonts/SF-Toontime/SF-Toontime-Bold.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..ca261f71e4d22e5449ab77aa25266c36ca13bc30
Binary files /dev/null and b/client/src/fonts/SF-Toontime/SF-Toontime-Bold.woff2 differ
diff --git a/client/src/fonts/SF-Toontime/SF-Toontime-Italic.woff2 b/client/src/fonts/SF-Toontime/SF-Toontime-Italic.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..b69bbb0e6099795da58953f27b11badf751b7d55
Binary files /dev/null and b/client/src/fonts/SF-Toontime/SF-Toontime-Italic.woff2 differ
diff --git a/client/src/fonts/SF-Toontime/SF-Toontime.woff2 b/client/src/fonts/SF-Toontime/SF-Toontime.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..d3d4d8c384004215748e16957f4ac809e5c9c6e9
Binary files /dev/null and b/client/src/fonts/SF-Toontime/SF-Toontime.woff2 differ
diff --git a/client/src/fonts/VTC-Letterer-Pro/VTC-Letterer-Pro.woff2 b/client/src/fonts/VTC-Letterer-Pro/VTC-Letterer-Pro.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..ec3bb640fabde62e9ec96ca521a6e2f6b003d119
Binary files /dev/null and b/client/src/fonts/VTC-Letterer-Pro/VTC-Letterer-Pro.woff2 differ
diff --git a/client/src/index.css b/client/src/index.css
index 3eb54ba1840000a783cc8731ae191ae863bc5a8f..70854dc98f8add51745bc8355bc0b7582f9a8194 100644
--- a/client/src/index.css
+++ b/client/src/index.css
@@ -4,14 +4,12 @@
box-sizing: border-box;
}
-body {
- min-height: 100vh;
- background-color: #f5f5f5;
-}
-
+html,
+body,
#root {
min-height: 100vh;
- padding: 1rem;
+ background-color: #f5f5f5;
+ overflow: hidden;
}
:root {
diff --git a/client/src/layouts/ComicLayout.jsx b/client/src/layouts/ComicLayout.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..2d96e86f7d585c4296e8162f1fca38577079f073
--- /dev/null
+++ b/client/src/layouts/ComicLayout.jsx
@@ -0,0 +1,162 @@
+import { Box, CircularProgress, Typography } from "@mui/material";
+import { LAYOUTS } from "./config";
+import { groupSegmentsIntoLayouts } from "./utils";
+import { useEffect, useRef } from "react";
+
+// Component for displaying a single panel
+function Panel({ segment, panel }) {
+ return (
+
+ {segment ? (
+ <>
+ {segment.image_base64 ? (
+
{
+ e.target.style.opacity = "1";
+ }}
+ />
+ ) : (
+
+ {!segment.imageRequestCancelled && (
+
+ )}
+ {segment.imageRequestCancelled && (
+
+ Image non chargée
+
+ )}
+
+ )}
+
+ {segment.text}
+
+ >
+ ) : null}
+
+ );
+}
+
+// Component for displaying a page of panels
+function ComicPage({ layout, layoutIndex }) {
+ return (
+
+ {LAYOUTS[layout.type].panels.map((panel, panelIndex) => (
+
+ ))}
+
+ );
+}
+
+// Main comic layout component
+export function ComicLayout({ segments }) {
+ const layouts = groupSegmentsIntoLayouts(segments);
+ const scrollContainerRef = useRef(null);
+
+ // Effect to scroll to the right when new layouts are added
+ useEffect(() => {
+ if (scrollContainerRef.current) {
+ scrollContainerRef.current.scrollTo({
+ left: scrollContainerRef.current.scrollWidth,
+ behavior: "smooth",
+ });
+ }
+ }, [layouts.length]); // Only run when the number of layouts changes
+
+ return (
+
+ {layouts.map((layout, layoutIndex) => (
+
+ ))}
+
+ );
+}
diff --git a/client/src/layouts/Layout.jsx b/client/src/layouts/Layout.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..ff78741eb4fb71ca9b95252d6f48e1325b7c0e9b
--- /dev/null
+++ b/client/src/layouts/Layout.jsx
@@ -0,0 +1,235 @@
+import { Box, CircularProgress, Typography } from "@mui/material";
+
+// Layout settings for different types
+export const LAYOUTS = {
+ COVER: {
+ gridCols: 1,
+ gridRows: 1,
+ panels: [
+ { width: 1024, height: 1536, gridColumn: "1", gridRow: "1" }, // Format pleine page (2:3 ratio)
+ ],
+ },
+ LAYOUT_1: {
+ gridCols: 2,
+ gridRows: 3,
+ panels: [
+ { width: 1024, height: 768, gridColumn: "1", gridRow: "1" }, // Landscape top left
+ { width: 768, height: 1024, gridColumn: "2", gridRow: "1 / span 2" }, // Portrait top right, spans 2 rows
+ { width: 768, height: 1024, gridColumn: "1", gridRow: "2 / span 2" }, // Portrait bottom left, spans 2 rows
+ { width: 1024, height: 768, gridColumn: "2", gridRow: "3" }, // Landscape bottom right
+ ],
+ },
+ LAYOUT_2: {
+ gridCols: 3,
+ gridRows: 2,
+ panels: [
+ { width: 768, height: 1024, gridColumn: "1", gridRow: "1" }, // Portrait top left
+ { width: 768, height: 1024, gridColumn: "2", gridRow: "1" }, // Portrait top middle
+ { width: 512, height: 1024, gridColumn: "3", gridRow: "1 / span 2" }, // Tall portrait right, spans full height
+ { width: 1024, height: 768, gridColumn: "1 / span 2", gridRow: "2" }, // Landscape bottom, spans 2 columns
+ ],
+ },
+ LAYOUT_3: {
+ gridCols: 3,
+ gridRows: 2,
+ panels: [
+ { width: 1024, height: 768, gridColumn: "1 / span 2", gridRow: "1" }, // Landscape top, spans 2 columns
+ { width: 768, height: 1024, gridColumn: "3", gridRow: "1" }, // Portrait top right
+ { width: 768, height: 1024, gridColumn: "1", gridRow: "2" }, // Portrait bottom left
+ { width: 1024, height: 768, gridColumn: "2 / span 2", gridRow: "2" }, // Landscape bottom right, spans 2 columns
+ ],
+ },
+ LAYOUT_4: {
+ gridCols: 8,
+ gridRows: 8,
+ panels: [
+ {
+ width: 512,
+ height: 1024,
+ gridColumn: "1 / span 6",
+ gridRow: "1 / span 2",
+ }, // Wide top
+ {
+ width: 1024,
+ height: 768,
+ gridColumn: "3 / span 6",
+ gridRow: "3 / span 1",
+ }, // Middle right
+ {
+ width: 768,
+ height: 1024,
+ gridColumn: "2 / span 6",
+ gridRow: "4 / span 2",
+ }, // Middle center
+ {
+ width: 1024,
+ height: 512,
+ gridColumn: "1 / span 8",
+ gridRow: "6 / span 2",
+ }, // Wide bottom
+ ],
+ },
+};
+
+// Function to group segments into layouts
+function groupSegmentsIntoLayouts(segments) {
+ if (segments.length === 0) return [];
+
+ const layouts = [];
+
+ // Premier segment toujours en COVER s'il est marqué comme first_step
+ if (segments[0].is_first_step) {
+ layouts.push({
+ type: "COVER",
+ segments: [segments[0]],
+ });
+ }
+
+ // Segments du milieu (on exclut le premier s'il était en COVER)
+ const startIndex = segments[0].is_first_step ? 1 : 0;
+ const middleSegments = segments.slice(startIndex);
+ let currentIndex = 0;
+
+ while (currentIndex < middleSegments.length) {
+ const segment = middleSegments[currentIndex];
+
+ // Si c'est le dernier segment (mort ou victoire), on le met en COVER
+ if (segment.is_last_step) {
+ layouts.push({
+ type: "COVER",
+ segments: [segment],
+ });
+ } else {
+ // Sinon on utilise un layout normal
+ const layoutType = `LAYOUT_${(layouts.length % 3) + 1}`;
+ const maxPanels = LAYOUTS[layoutType].panels.length;
+ const availableSegments = middleSegments
+ .slice(currentIndex)
+ .filter((s) => !s.is_last_step);
+
+ if (availableSegments.length > 0) {
+ layouts.push({
+ type: layoutType,
+ segments: availableSegments.slice(0, maxPanels),
+ });
+ currentIndex += Math.min(maxPanels, availableSegments.length) - 1;
+ }
+ }
+
+ currentIndex++;
+ }
+
+ console.log("Generated layouts:", layouts); // Debug log
+ return layouts;
+}
+
+export function ComicLayout({ segments }) {
+ const layouts = groupSegmentsIntoLayouts(segments);
+
+ return (
+
+ {layouts.map((layout, layoutIndex) => (
+
+ {/* Render all panels of the layout */}
+ {LAYOUTS[layout.type].panels.map((panel, panelIndex) => {
+ // Find the segment for this panel position if it exists
+ const segment = layout.segments[panelIndex];
+
+ return (
+
+ {segment ? (
+ // If there's a segment, render image and text
+ <>
+ {segment.image_base64 ? (
+
{
+ e.target.style.opacity = "1";
+ }}
+ />
+ ) : (
+
+
+
+ )}
+
+ {segment.text}
+
+ >
+ ) : null}
+
+ );
+ })}
+
+ ))}
+
+ );
+}
diff --git a/client/src/layouts/config.js b/client/src/layouts/config.js
new file mode 100644
index 0000000000000000000000000000000000000000..48ca4556cedf2896991e4f1cd8dc0a8fe83190b7
--- /dev/null
+++ b/client/src/layouts/config.js
@@ -0,0 +1,80 @@
+// Layout settings for different types
+export const LAYOUTS = {
+ COVER: {
+ gridCols: 1,
+ gridRows: 1,
+ panels: [
+ { width: 1024, height: 1024, gridColumn: "1", gridRow: "1" }, // Format pleine page (1:1 ratio)
+ ],
+ },
+ LAYOUT_1: {
+ gridCols: 2,
+ gridRows: 2,
+ panels: [
+ { width: 1024, height: 768, gridColumn: "1", gridRow: "1" }, // 1. Landscape top left
+ { width: 768, height: 1024, gridColumn: "2", gridRow: "1" }, // 2. Portrait top right
+ { width: 1024, height: 768, gridColumn: "1", gridRow: "2" }, // 3. Landscape middle left
+ { width: 768, height: 1024, gridColumn: "2", gridRow: "2" }, // 4. Portrait right, spans bottom rows
+ ],
+ },
+ LAYOUT_2: {
+ gridCols: 3,
+ gridRows: 2,
+ panels: [
+ { width: 1024, height: 1024, gridColumn: "1 / span 2", gridRow: "1" }, // 1. Large square top left
+ { width: 512, height: 1024, gridColumn: "3", gridRow: "1" }, // 2. Portrait top right
+ { width: 1024, height: 768, gridColumn: "1 / span 3", gridRow: "2" }, // 3. Landscape bottom, spans full width
+ ],
+ },
+ LAYOUT_3: {
+ gridCols: 3,
+ gridRows: 2,
+ panels: [
+ { width: 1024, height: 768, gridColumn: "1 / span 2", gridRow: "1" }, // 1. Landscape top left, spans 2 columns
+ { width: 768, height: 1024, gridColumn: "3", gridRow: "1" }, // 2. Portrait top right
+ { width: 768, height: 1024, gridColumn: "1", gridRow: "2" }, // 3. Portrait bottom left
+ { width: 1024, height: 768, gridColumn: "2 / span 2", gridRow: "2" }, // 4. Landscape bottom right, spans 2 columns
+ ],
+ },
+ LAYOUT_4: {
+ gridCols: 8,
+ gridRows: 8,
+ panels: [
+ {
+ width: 768,
+ height: 768,
+ gridColumn: "1 / span 3",
+ gridRow: "1 / span 3",
+ }, // 1. Square top left
+ {
+ width: 768,
+ height: 1024,
+ gridColumn: "1 / span 3",
+ gridRow: "4 / span 5",
+ }, // 2. Long portrait bottom left
+ {
+ width: 768,
+ height: 1024,
+ gridColumn: "5 / span 3",
+ gridRow: "1 / span 5",
+ }, // 3. Long portrait top right
+ {
+ width: 768,
+ height: 768,
+ gridColumn: "5 / span 3",
+ gridRow: "6 / span 3",
+ }, // 4. Square bottom right
+ ],
+ },
+};
+
+export const defaultLayout = "LAYOUT_1";
+export const nonRandomLayouts = Object.keys(LAYOUTS).filter(
+ (layout) => layout !== "random"
+);
+
+// Helper functions for layout configuration
+export const getNextLayoutType = (currentLayoutCount) =>
+ `LAYOUT_${(currentLayoutCount % 3) + 1}`;
+export const getLayoutDimensions = (layoutType, panelIndex) =>
+ LAYOUTS[layoutType]?.panels[panelIndex];
diff --git a/client/src/layouts/utils.js b/client/src/layouts/utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..0251001692ee5828d5ee875fcb3d3c749f621896
--- /dev/null
+++ b/client/src/layouts/utils.js
@@ -0,0 +1,91 @@
+import { LAYOUTS, getNextLayoutType } from "./config";
+
+// Function to group segments into layouts
+export function groupSegmentsIntoLayouts(segments) {
+ if (segments.length === 0) return [];
+
+ const layouts = [];
+
+ // Premier segment toujours en COVER s'il est marqué comme first_step
+ if (segments[0].is_first_step) {
+ layouts.push({
+ type: "COVER",
+ segments: [segments[0]],
+ });
+ }
+
+ // Segments du milieu (on exclut le premier s'il était en COVER)
+ const startIndex = segments[0].is_first_step ? 1 : 0;
+ const middleSegments = segments.slice(startIndex);
+ let currentIndex = 0;
+
+ while (currentIndex < middleSegments.length) {
+ const segment = middleSegments[currentIndex];
+
+ // Si c'est le dernier segment (mort ou victoire), on le met en COVER
+ if (segment.is_last_step) {
+ layouts.push({
+ type: "COVER",
+ segments: [segment],
+ });
+ } else {
+ // Sinon on utilise un layout normal
+ const layoutType = getNextLayoutType(layouts.length);
+ const maxPanels = LAYOUTS[layoutType].panels.length;
+ const availableSegments = middleSegments
+ .slice(currentIndex)
+ .filter((s) => !s.is_last_step);
+
+ if (availableSegments.length > 0) {
+ layouts.push({
+ type: layoutType,
+ segments: availableSegments.slice(0, maxPanels),
+ });
+ currentIndex += Math.min(maxPanels, availableSegments.length) - 1;
+ }
+ }
+
+ currentIndex++;
+ }
+
+ return layouts;
+}
+
+// Function to get panel dimensions for next image
+export function getNextPanelDimensions(segments) {
+ const nonChoiceSegments = segments.filter((segment) => !segment.isChoice);
+
+ // Si c'est le premier segment, utiliser le format COVER
+ if (
+ nonChoiceSegments.length === 0 ||
+ (nonChoiceSegments.length === 1 && nonChoiceSegments[0].is_first_step)
+ ) {
+ return LAYOUTS.COVER.panels[0];
+ }
+
+ // Si c'est le dernier segment et c'est une mort ou victoire, utiliser le format COVER
+ const lastSegment = nonChoiceSegments[nonChoiceSegments.length - 1];
+ if (lastSegment.is_last_step) {
+ return LAYOUTS.COVER.panels[0];
+ }
+
+ // Pour les segments du milieu, déterminer le layout et la position dans ce layout
+ const layouts = groupSegmentsIntoLayouts(nonChoiceSegments.slice(0, -1));
+ const lastLayout = layouts[layouts.length - 1];
+ const segmentsInLastLayout = lastLayout ? lastLayout.segments.length : 0;
+
+ // Déterminer le type du prochain layout
+ const nextLayoutType = getNextLayoutType(layouts.length);
+ const nextPanelIndex = segmentsInLastLayout;
+
+ // Si le dernier layout est plein, prendre le premier panneau du prochain layout
+ if (
+ !lastLayout ||
+ segmentsInLastLayout >= LAYOUTS[lastLayout.type].panels.length
+ ) {
+ return LAYOUTS[nextLayoutType].panels[0];
+ }
+
+ // Sinon, prendre le prochain panneau du layout courant
+ return LAYOUTS[lastLayout.type].panels[nextPanelIndex];
+}
diff --git a/client/vite.config.js b/client/vite.config.js
index 8b0f57b91aeb45c54467e29f983a0893dc83c4d9..0e43ae8def386fe7cac80ef1e246b340b7080f94 100644
--- a/client/vite.config.js
+++ b/client/vite.config.js
@@ -1,7 +1,7 @@
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react'
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
-})
+});
diff --git a/server/api_clients.py b/server/api_clients.py
new file mode 100644
index 0000000000000000000000000000000000000000..7c34982a0c9f998d44d2f6f97bf62e21210298ed
--- /dev/null
+++ b/server/api_clients.py
@@ -0,0 +1,121 @@
+import os
+import requests
+import asyncio
+from typing import Optional
+from langchain_mistralai.chat_models import ChatMistralAI
+from langchain.schema import SystemMessage, HumanMessage
+
+class MistralClient:
+ def __init__(self, api_key: str):
+ self.chat_model = ChatMistralAI(
+ mistral_api_key=api_key,
+ model="ft:ministral-3b-latest:82f3f89c:20250125:12222969",
+ temperature=0.7
+ )
+
+ # Pour le fixing parser
+ self.fixing_model = ChatMistralAI(
+ mistral_api_key=api_key,
+ model="ft:ministral-3b-latest:82f3f89c:20250125:12222969",
+ temperature=0.1
+ )
+
+ # Pour gérer le rate limit
+ self.last_call_time = 0
+ self.min_delay = 1 # 1 seconde minimum entre les appels
+
+ async def _wait_for_rate_limit(self):
+ """Attend le temps nécessaire pour respecter le rate limit."""
+ current_time = asyncio.get_event_loop().time()
+ time_since_last_call = current_time - self.last_call_time
+
+ if time_since_last_call < self.min_delay:
+ await asyncio.sleep(self.min_delay - time_since_last_call)
+
+ self.last_call_time = asyncio.get_event_loop().time()
+
+ async def generate_story(self, messages) -> str:
+ """Génère une réponse à partir d'une liste de messages."""
+ try:
+ await self._wait_for_rate_limit()
+ response = self.chat_model.invoke(messages)
+ return response.content
+ except Exception as e:
+ print(f"Error in Mistral API call: {str(e)}")
+ raise
+
+ async def transform_prompt(self, story_text: str, system_prompt: str) -> str:
+ """Transforme un texte d'histoire en prompt artistique."""
+ try:
+ await self._wait_for_rate_limit()
+ messages = [
+ SystemMessage(content=system_prompt),
+ HumanMessage(content=f"Transform into a short prompt: {story_text}")
+ ]
+ response = self.chat_model.invoke(messages)
+ return response.content
+ except Exception as e:
+ print(f"Error transforming prompt: {str(e)}")
+ return story_text
+
+class FluxClient:
+ def __init__(self, api_key: str):
+ self.api_key = api_key
+ self.endpoint = os.getenv("FLUX_ENDPOINT", "https://api-inference.huggingface.co/models/stabilityai/sdxl-turbo")
+
+ def generate_image(self,
+ prompt: str,
+ width: int,
+ height: int,
+ num_inference_steps: int = 30,
+ guidance_scale: float = 9.0) -> Optional[bytes]:
+ """Génère une image à partir d'un prompt."""
+ try:
+ # Ensure dimensions are multiples of 8
+ width = (width // 8) * 8
+ height = (height // 8) * 8
+
+ print(f"Sending request to Hugging Face API: {self.endpoint}")
+ print(f"Headers: Authorization: Bearer {self.api_key[:4]}...")
+ print(f"Request body: {prompt[:100]}...")
+
+ response = requests.post(
+ self.endpoint,
+ headers={
+ "Authorization": f"Bearer {self.api_key}",
+ "Accept": "image/jpeg"
+ },
+ json={
+ "inputs": prompt,
+ "parameters": {
+ "num_inference_steps": num_inference_steps,
+ "guidance_scale": guidance_scale,
+ "width": width,
+ "height": height,
+ "negative_prompt": "text, watermark, logo, signature, blurry, low quality"
+ }
+ }
+ )
+
+ print(f"Response status code: {response.status_code}")
+ print(f"Response headers: {response.headers}")
+ print(f"Response content type: {response.headers.get('content-type', 'unknown')}")
+
+ if response.status_code == 200:
+ content_length = len(response.content)
+ print(f"Received successful response with content length: {content_length}")
+ if isinstance(response.content, bytes):
+ print("Response content is bytes (correct)")
+ else:
+ print(f"Warning: Response content is {type(response.content)}")
+ return response.content
+ else:
+ print(f"Error from Flux API: {response.status_code}")
+ print(f"Response content: {response.content}")
+ return None
+
+ except Exception as e:
+ print(f"Error in FluxClient.generate_image: {str(e)}")
+ import traceback
+ print(f"Traceback: {traceback.format_exc()}")
+ return None
\ No newline at end of file
diff --git a/server/game/game_logic.py b/server/game/game_logic.py
index 0548128ae4d7a04599454e68adef8da919d9c2a3..cde058478a60d371ffcf01680b0fbe1f27c99934 100644
--- a/server/game/game_logic.py
+++ b/server/game/game_logic.py
@@ -1,9 +1,15 @@
from pydantic import BaseModel, Field
from typing import List
-from langchain_mistralai.chat_models import ChatMistralAI
from langchain.output_parsers import PydanticOutputParser, OutputFixingParser
from langchain.prompts import ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemplate
-from langchain.schema import HumanMessage, SystemMessage
+import os
+import asyncio
+
+# Import local modules
+if os.getenv("DOCKER_ENV"):
+ from server.api_clients import MistralClient
+else:
+ from api_clients import MistralClient
# Game constants
MAX_RADIATION = 10
@@ -19,7 +25,7 @@ class GameState:
# Story output structure
class StorySegment(BaseModel):
- story_text: str = Field(description="The next segment of the story. Like 20 words.")
+ story_text: str = Field(description="The next segment of the story. No more than 15 words THIS IS MANDATORY. Use bold formatting (like **this**) ONLY for proper nouns (like **Sarah**, **Vault 15**, **New Eden**) and important locations.")
choices: List[str] = Field(description="Exactly two possible choices for the player", min_items=2, max_items=2)
is_victory: bool = Field(description="Whether this segment ends in Sarah's victory", default=False)
radiation_increase: int = Field(description="How much radiation this segment adds (0-3)", ge=0, le=3, default=1)
@@ -39,30 +45,20 @@ Rules:
- No superfluous adjectives
- Capture only the main action"""
-HUMAN_ART_PROMPT = "Transform into a short prompt: {story_text}"
-
class StoryGenerator:
def __init__(self, api_key: str):
self.parser = PydanticOutputParser(pydantic_object=StorySegment)
+ self.mistral_client = MistralClient(api_key)
+
self.fixing_parser = OutputFixingParser.from_llm(
parser=self.parser,
- llm=ChatMistralAI(
- mistral_api_key=api_key,
- model="mistral-small",
- temperature=0.1
- )
- )
-
- self.chat_model = ChatMistralAI(
- mistral_api_key=api_key,
- model="ft:ministral-3b-latest:82f3f89c:20250125:12222969",
- temperature=0.7
+ llm=self.mistral_client.fixing_model
)
self.prompt = self._create_prompt()
def _create_prompt(self) -> ChatPromptTemplate:
- system_template = """You are narrating a brutal dystopian story where Sarah must survive in a radioactive wasteland. This is a comic book story.
+ system_template = """You are narrating a brutal dystopian story where **Sarah** must survive in a radioactive wasteland. This is a comic book story.
IMPORTANT: The first story beat (story_beat = 0) MUST be an introduction that sets up the horror atmosphere.
@@ -82,31 +78,40 @@ IMPORTANT RULES FOR RADIATION:
- Death occurs automatically when radiation reaches 10
Core story elements:
-- Sarah is deeply traumatized by the AI uprising that killed most of humanity
-- She abandoned her sister during the Great Collapse, leaving her to die
+- **Sarah** is deeply traumatized by the AI uprising that killed most of humanity
+- She abandoned her sister during the **Great Collapse**, leaving her to die
- She's on a mission of redemption in this hostile world
- The radiation is an invisible, constant threat
- The environment is full of dangers (raiders, AI, traps)
- Focus on survival horror and tension
+IMPORTANT FORMATTING RULES:
+- Use bold formatting (like **this**) ONLY for:
+ * Character names (e.g., **Sarah**, **John**)
+ * Location names (e.g., **Vault 15**, **New Eden**)
+ * Major historical events (e.g., **Great Collapse**)
+- Do NOT use bold for common nouns or regular descriptions
+
Each response MUST contain:
1. A detailed story segment that:
- Describes the horrific environment
- Shows immediate dangers
- - Details Sarah's physical state (based on radiation_level)
+ - Details **Sarah**'s physical state (based on radiation_level)
- Reflects her mental state and previous choices
+ - Uses bold ONLY for proper nouns and locations
2. Exactly two VERY CONCISE choices (max 10 words each):
Examples of good choices:
- - "Explore the abandoned hospital" vs "Search the residential area"
- - "Trust the survivor" vs "Keep your distance"
- - "Use the old AI system" vs "Find a manual solution"
+ - "Explore the **Medical Center**" vs "Search the **Residential Zone**"
+ - "Trust the survivor from **Vault 15**" vs "Keep your distance"
+ - "Use the **AI Core**" vs "Find a manual solution"
Each choice must:
- Be direct and brief
- Never mention radiation numbers
- Feel meaningful
- Present different risk levels
+ - Use bold ONLY for location names
{format_instructions}"""
@@ -124,43 +129,55 @@ Generate the next story segment and choices. If this is story_beat 0, create an
partial_variables={"format_instructions": self.parser.get_format_instructions()}
)
- def generate_story_segment(self, game_state: GameState, previous_choice: str) -> StorySegment:
+ async def generate_story_segment(self, game_state: GameState, previous_choice: str) -> StorySegment:
messages = self.prompt.format_messages(
story_beat=game_state.story_beat,
radiation_level=game_state.radiation_level,
previous_choice=previous_choice
)
- response = self.chat_model.invoke(messages)
+ max_retries = 3
+ retry_count = 0
+
+ while retry_count < max_retries:
+ try:
+ response_content = await self.mistral_client.generate_story(messages)
+ try:
+ # Try to parse with standard parser first
+ segment = self.parser.parse(response_content)
+ except Exception as parse_error:
+ print(f"Error parsing response: {str(parse_error)}")
+ print("Attempting to fix output...")
+ try:
+ # Try with fixing parser
+ segment = self.fixing_parser.parse(response_content)
+ except Exception as fix_error:
+ print(f"Error fixing output: {str(fix_error)}")
+ retry_count += 1
+ if retry_count < max_retries:
+ print(f"Retrying generation (attempt {retry_count + 1}/{max_retries})...")
+ await asyncio.sleep(2 * retry_count) # Exponential backoff
+ continue
+ raise fix_error
+
+ # If we get here, parsing succeeded
+ if game_state.story_beat == 0:
+ segment.radiation_increase = 0
+ return segment
+
+ except Exception as e:
+ print(f"Error in story generation: {str(e)}")
+ retry_count += 1
+ if retry_count < max_retries:
+ print(f"Retrying generation (attempt {retry_count + 1}/{max_retries})...")
+ await asyncio.sleep(2 * retry_count) # Exponential backoff
+ continue
+ raise e
- try:
- segment = self.parser.parse(response.content)
- # Force radiation_increase to 0 for the first story beat
- if game_state.story_beat == 0:
- segment.radiation_increase = 0
- return segment
- except Exception as e:
- print(f"Error parsing response: {str(e)}")
- print("Attempting to fix output...")
- segment = self.fixing_parser.parse(response.content)
- # Force radiation_increase to 0 for the first story beat
- if game_state.story_beat == 0:
- segment.radiation_increase = 0
- return segment
+ raise Exception(f"Failed to generate valid story segment after {max_retries} attempts")
async def transform_story_to_art_prompt(self, story_text: str) -> str:
- try:
- messages = [
- SystemMessage(content=SYSTEM_ART_PROMPT),
- HumanMessage(content=HUMAN_ART_PROMPT.format(story_text=story_text))
- ]
-
- response = self.chat_model.invoke(messages)
- return response.content
-
- except Exception as e:
- print(f"Error transforming prompt: {str(e)}")
- return story_text
+ return await self.mistral_client.transform_prompt(story_text, SYSTEM_ART_PROMPT)
def process_radiation_death(self, segment: StorySegment) -> StorySegment:
segment.is_death = True
diff --git a/server/poetry.lock b/server/poetry.lock
index e41e8d1ff010206b6f2d460b27e7b1c4872e7bb3..dd9fff43eb1879bae9b52d3fe73bf2d4458ac4c7 100644
--- a/server/poetry.lock
+++ b/server/poetry.lock
@@ -1109,6 +1109,17 @@ zstandard = ">=0.23.0,<0.24.0"
langsmith-pyo3 = ["langsmith-pyo3 (>=0.1.0rc2,<0.2.0)"]
pytest = ["pytest (>=7.0.0)", "rich (>=13.9.4,<14.0.0)"]
+[[package]]
+name = "lorem"
+version = "0.1.1"
+description = "Generator for random text that looks like Latin."
+optional = false
+python-versions = "*"
+files = [
+ {file = "lorem-0.1.1-py3-none-any.whl", hash = "sha256:c9c2914b5a772022417c398bd74b7bbd712e73ff029ba82720855e458f13ae42"},
+ {file = "lorem-0.1.1.tar.gz", hash = "sha256:785f4109a241fc2891e59705e85d065f6e6d3ed6ad91750a8cb54d4f3e59d934"},
+]
+
[[package]]
name = "matplotlib-inline"
version = "0.1.7"
@@ -1486,6 +1497,103 @@ files = [
[package.dependencies]
ptyprocess = ">=0.5"
+[[package]]
+name = "pillow"
+version = "10.4.0"
+description = "Python Imaging Library (Fork)"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"},
+ {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"},
+ {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"},
+ {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"},
+ {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"},
+ {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"},
+ {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"},
+ {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"},
+ {file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"},
+ {file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"},
+ {file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"},
+ {file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"},
+ {file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"},
+ {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"},
+ {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"},
+ {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"},
+ {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"},
+ {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"},
+ {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"},
+ {file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"},
+ {file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"},
+ {file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"},
+ {file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"},
+ {file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"},
+ {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"},
+ {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"},
+ {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"},
+ {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"},
+ {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"},
+ {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"},
+ {file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"},
+ {file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"},
+ {file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"},
+ {file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"},
+ {file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"},
+ {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"},
+ {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"},
+ {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"},
+ {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"},
+ {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"},
+ {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"},
+ {file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"},
+ {file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"},
+ {file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"},
+ {file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"},
+ {file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"},
+ {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"},
+ {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"},
+ {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"},
+ {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"},
+ {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"},
+ {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"},
+ {file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"},
+ {file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"},
+ {file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"},
+ {file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"},
+ {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"},
+ {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"},
+ {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"},
+ {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"},
+ {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"},
+ {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"},
+ {file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"},
+ {file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"},
+ {file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"},
+ {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"},
+ {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"},
+ {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"},
+ {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"},
+ {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"},
+ {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"},
+ {file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"},
+ {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"},
+ {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"},
+ {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"},
+ {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"},
+ {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"},
+ {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"},
+ {file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"},
+ {file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"},
+]
+
+[package.extras]
+docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"]
+fpx = ["olefile"]
+mic = ["olefile"]
+tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
+typing = ["typing-extensions"]
+xmp = ["defusedxml"]
+
[[package]]
name = "platformdirs"
version = "4.3.6"
@@ -2551,4 +2659,4 @@ cffi = ["cffi (>=1.11)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
-content-hash = "f1ac792e9026c6373be7fd6f4db3f418c7965c58ec1b0df8286a47c4d997b92b"
+content-hash = "f4332c9af05c4edd4929343f15bf442498c5db976ad6917db064313cc30e9cba"
diff --git a/server/pyproject.toml b/server/pyproject.toml
index 657cb882f1cd097aea7a5840b4536d508f6acf4e..300aafef59c47840145efec1f7a49fd9a2f37e49 100644
--- a/server/pyproject.toml
+++ b/server/pyproject.toml
@@ -13,6 +13,10 @@ elevenlabs = "^0.2.26"
langchain = "^0.3.15"
langchain-mistralai = "^0.2.4"
requests = "^2.31.0"
+pillow = "^10.2.0" # Pour la génération d'images de test
+lorem = "^0.1.1" # Pour la génération de texte aléatoire
+aiohttp = "^3.9.3"
+
[tool.poetry.group.dev.dependencies]
pytest = "^7.4.0"
diff --git a/server/server.py b/server/server.py
index b8dbb5ac12cbecf3032de9d37e2e3c1d5f91caca..03a9ab1ca51808afb31e6145cebd5bcbc48abb6d 100644
--- a/server/server.py
+++ b/server/server.py
@@ -1,20 +1,29 @@
-from fastapi import FastAPI, HTTPException
+from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
-from pydantic import BaseModel
-from typing import List, Optional
+from pydantic import BaseModel, Field
+from typing import List, Optional, Dict
import os
from dotenv import load_dotenv
-import requests
import base64
import time
import random
+import asyncio
+import aiohttp
+from lorem.text import TextLorem
+from contextlib import asynccontextmanager
-# Choose import based on environment
+
+lorem = TextLorem(wsep='-', srange=(2,3), words="A B C D".split())
+
+
+# Import local modules
if os.getenv("DOCKER_ENV"):
from server.game.game_logic import GameState, StoryGenerator, MAX_RADIATION
+ from server.api_clients import FluxClient
else:
from game.game_logic import GameState, StoryGenerator, MAX_RADIATION
+ from api_clients import FluxClient
# Load environment variables
load_dotenv()
@@ -52,17 +61,46 @@ if not mistral_api_key:
raise ValueError("MISTRAL_API_KEY environment variable is not set")
story_generator = StoryGenerator(api_key=mistral_api_key)
+flux_client = FluxClient(api_key=HF_API_KEY)
+
+# Store client sessions and requests by type
+client_sessions: Dict[str, aiohttp.ClientSession] = {}
+client_requests: Dict[str, Dict[str, asyncio.Task]] = {}
+
+async def get_client_session(client_id: str) -> aiohttp.ClientSession:
+ """Get or create a client session"""
+ if client_id not in client_sessions:
+ client_sessions[client_id] = aiohttp.ClientSession()
+ return client_sessions[client_id]
+
+async def cancel_previous_request(client_id: str, request_type: str):
+ """Cancel previous request if it exists"""
+ if client_id in client_requests and request_type in client_requests[client_id]:
+ task = client_requests[client_id][request_type]
+ if not task.done():
+ task.cancel()
+ try:
+ await task
+ except asyncio.CancelledError:
+ pass
+
+async def store_request(client_id: str, request_type: str, task: asyncio.Task):
+ """Store a request for a client"""
+ if client_id not in client_requests:
+ client_requests[client_id] = {}
+ client_requests[client_id][request_type] = task
class Choice(BaseModel):
id: int
text: str
class StoryResponse(BaseModel):
- story_text: str
+ story_text: str = Field(description="The story text with proper nouns in bold using ** markdown")
choices: List[Choice]
- is_death: bool = False
- is_victory: bool = False
- radiation_level: int
+ radiation_level: int = Field(description="Current radiation level from 0 to 10")
+ is_victory: bool = Field(description="Whether this segment ends in Sarah's victory", default=False)
+ is_first_step: bool = Field(description="Whether this is the first step of the story", default=False)
+ is_last_step: bool = Field(description="Whether this is the last step (victory or death)", default=False)
class ChatMessage(BaseModel):
message: str
@@ -70,15 +108,27 @@ class ChatMessage(BaseModel):
class ImageGenerationRequest(BaseModel):
prompt: str
- negative_prompt: Optional[str] = None
- width: Optional[int] = 1024
- height: Optional[int] = 1024
+ width: int = Field(description="Width of the image to generate")
+ height: int = Field(description="Height of the image to generate")
class ImageGenerationResponse(BaseModel):
success: bool
image_base64: Optional[str] = None
error: Optional[str] = None
+async def get_test_image(client_id: str, width=1024, height=1024):
+ """Get a random image from Lorem Picsum"""
+ # Build the Lorem Picsum URL with blur and grayscale effects
+ url = f"https://picsum.photos/{width}/{height}?grayscale&blur=2"
+
+ session = await get_client_session(client_id)
+ async with session.get(url) as response:
+ if response.status == 200:
+ image_bytes = await response.read()
+ return base64.b64encode(image_bytes).decode('utf-8')
+ else:
+ raise Exception(f"Failed to fetch image: {response.status}")
+
@app.get("/api/health")
async def health_check():
"""Health check endpoint"""
@@ -105,7 +155,7 @@ async def chat_endpoint(chat_message: ChatMessage):
print("Previous choice:", previous_choice)
# Generate story segment
- story_segment = story_generator.generate_story_segment(game_state, previous_choice)
+ story_segment = await story_generator.generate_story_segment(game_state, previous_choice)
print("Generated story segment:", story_segment)
# Update radiation level
@@ -148,12 +198,20 @@ Sa mission s'arrête ici, une autre victime du tueur invisible des terres désol
for i, choice in enumerate(story_segment.choices, 1)
]
+ # Determine if this is the first step
+ is_first_step = chat_message.message == "restart"
+
+ # Determine if this is the last step (victory or death)
+ is_last_step = game_state.radiation_level >= MAX_RADIATION or story_segment.is_victory
+
+ # Return the response with the new fields
response = StoryResponse(
story_text=story_segment.story_text,
choices=choices,
- is_death=is_death,
+ radiation_level=game_state.radiation_level,
is_victory=story_segment.is_victory,
- radiation_level=game_state.radiation_level
+ is_first_step=is_first_step,
+ is_last_step=is_last_step
)
print("Sending response:", response)
return response
@@ -164,99 +222,149 @@ Sa mission s'arrête ici, une autre victime du tueur invisible des terres désol
print("Traceback:", traceback.format_exc())
raise HTTPException(status_code=500, detail=str(e))
-@app.post("/api/generate-image", response_model=ImageGenerationResponse)
+@app.post("/api/generate-image")
async def generate_image(request: ImageGenerationRequest):
try:
- if not HF_API_KEY:
- return ImageGenerationResponse(
- success=False,
- error="HF_API_KEY is not configured in .env file"
- )
+ # Transform story into art prompt
+ art_prompt = await story_generator.transform_story_to_art_prompt(request.prompt)
+
+ print(f"Generating image with dimensions: {request.width}x{request.height}")
+ print(f"Using prompt: {art_prompt}")
+
+ # Generate image using Flux client
+ image_bytes = flux_client.generate_image(
+ prompt=art_prompt,
+ width=request.width,
+ height=request.height
+ )
+
+ if image_bytes:
+ print(f"Received image bytes of length: {len(image_bytes)}")
+ # Ensure we're getting raw bytes and encoding them properly
+ if isinstance(image_bytes, str):
+ print("Warning: image_bytes is a string, converting to bytes")
+ image_bytes = image_bytes.encode('utf-8')
+ base64_image = base64.b64encode(image_bytes).decode('utf-8').strip('"')
+ print(f"Converted to base64 string of length: {len(base64_image)}")
+ print(f"First 100 chars of base64: {base64_image[:100]}")
+ return {"success": True, "image_base64": base64_image}
+ else:
+ print("No image bytes received from Flux client")
+ return {"success": False, "error": "Failed to generate image"}
+
+ except Exception as e:
+ print(f"Error generating image: {str(e)}")
+ print(f"Error type: {type(e)}")
+ import traceback
+ print(f"Traceback: {traceback.format_exc()}")
+ return {"success": False, "error": str(e)}
- # Transform the prompt into an artistic prompt
- original_prompt = request.prompt
- # Remove prefix for transformation
- story_text = original_prompt.replace("moebius style scene: ", "").strip()
- art_prompt = await story_generator.transform_story_to_art_prompt(story_text)
- # Reapply prefix
- final_prompt = f"moebius style scene: {art_prompt}"
- print("Original prompt:", original_prompt)
- print("Transformed art prompt:", final_prompt)
+@app.post("/api/test/chat")
+async def test_chat_endpoint(request: Request, chat_message: ChatMessage):
+ """Endpoint de test qui génère des données aléatoires"""
+ try:
+ client_id = request.headers.get("x-client-id", "default")
- # Paramètres de retry
- max_retries = 3
- retry_delay = 1 # secondes
+ # Cancel any previous chat request from this client
+ await cancel_previous_request(client_id, "chat")
- for attempt in range(max_retries):
- try:
- # Appel à l'endpoint HF avec authentification
- response = requests.post(
- "https://tvsk4iu4ghzffi34.us-east-1.aws.endpoints.huggingface.cloud",
- headers={
- "Content-Type": "application/json",
- "Accept": "image/jpeg",
- "Authorization": f"Bearer {HF_API_KEY}"
- },
- json={
- "inputs": final_prompt,
- "parameters": {
- "guidance_scale": 9.0, # Valeur du Comic Factory
- "width": request.width or 1024,
- "height": request.height or 1024,
- "negative_prompt": "manga, anime, american comic, grayscale, monochrome, photo, painting, 3D render"
- }
- }
- )
-
- print(f"Attempt {attempt + 1} - API Response status:", response.status_code)
- print("API Response headers:", dict(response.headers))
-
- if response.status_code == 503:
- if attempt < max_retries - 1:
- print(f"Service unavailable, retrying in {retry_delay} seconds...")
- time.sleep(retry_delay)
- retry_delay *= 2 # Exponential backoff
- continue
- else:
- return ImageGenerationResponse(
- success=False,
- error="Service is currently unavailable after multiple retries"
- )
-
- if response.status_code != 200:
- error_msg = response.text if response.text else "Unknown error"
- print("Error response:", error_msg)
- return ImageGenerationResponse(
- success=False,
- error=f"API error: {error_msg}"
- )
-
- # L'API renvoie directement l'image en binaire
- image_bytes = response.content
- base64_image = base64.b64encode(image_bytes).decode('utf-8')
-
- print("Base64 image length:", len(base64_image))
-
- return ImageGenerationResponse(
- success=True,
- image_base64=f"data:image/jpeg;base64,{base64_image}"
- )
-
- except requests.exceptions.RequestException as e:
- if attempt < max_retries - 1:
- print(f"Request failed, retrying in {retry_delay} seconds... Error: {str(e)}")
- time.sleep(retry_delay)
- retry_delay *= 2
- continue
- else:
- raise
+ async def generate_chat_response():
+ # Générer un texte aléatoire
+ story_text = f"**Sarah** {lorem.paragraph()}"
+
+ # Générer un niveau de radiation aléatoire qui augmente progressivement
+ radiation_level = min(10, random.randint(0, 3) + (chat_message.choice_id or 0))
+
+ # Déterminer si c'est le premier pas
+ is_first_step = chat_message.message == "restart"
+
+ # Déterminer si c'est le dernier pas (mort ou victoire)
+ is_last_step = radiation_level >= 30 or (
+ not is_first_step and random.random() < 0.1 # 10% de chance de victoire
+ )
+
+ # Générer des choix aléatoires sauf si c'est la fin
+ choices = []
+ if not is_last_step:
+ num_choices = 2
+ for i in range(num_choices):
+ choices.append(Choice(
+ id=i+1,
+ text=f"{lorem.sentence() }"
+ ))
+
+ # Construire la réponse
+ return StoryResponse(
+ story_text=story_text,
+ choices=choices,
+ radiation_level=radiation_level,
+ is_victory=is_last_step and radiation_level < 30,
+ is_first_step=is_first_step,
+ is_last_step=is_last_step
+ )
+
+ # Create and store the new request
+ task = asyncio.create_task(generate_chat_response())
+ await store_request(client_id, "chat", task)
+
+ try:
+ response = await task
+ return response
+ except asyncio.CancelledError:
+ print(f"[INFO] Chat request cancelled for client {client_id}")
+ raise HTTPException(status_code=409, detail="Request cancelled")
except Exception as e:
- print("Error in generate_image:", str(e))
- return ImageGenerationResponse(
- success=False,
- error=f"Error generating image: {str(e)}"
- )
+ print(f"[ERROR] Error in test_chat_endpoint: {str(e)}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+@app.post("/api/test/generate-image")
+async def test_generate_image(request: Request, image_request: ImageGenerationRequest):
+ """Endpoint de test qui récupère une image aléatoire"""
+ try:
+ client_id = request.headers.get("x-client-id", "default")
+
+ print(f"[DEBUG] Client ID: {client_id}")
+ print(f"[DEBUG] Raw request data: {image_request}")
+
+ # Cancel any previous image request from this client
+ await cancel_previous_request(client_id, "image")
+
+ # Create and store the new request
+ task = asyncio.create_task(get_test_image(client_id, image_request.width, image_request.height))
+ await store_request(client_id, "image", task)
+
+ try:
+ image_base64 = await task
+ return {
+ "success": True,
+ "image_base64": image_base64
+ }
+ except asyncio.CancelledError:
+ print(f"[INFO] Image request cancelled for client {client_id}")
+ return {
+ "success": False,
+ "error": "Request cancelled"
+ }
+
+ except Exception as e:
+ print(f"[ERROR] Detailed error in test_generate_image: {str(e)}")
+ return {
+ "success": False,
+ "error": str(e)
+ }
+
+@app.on_event("shutdown")
+async def shutdown_event():
+ """Clean up sessions on shutdown"""
+ # Cancel all pending requests
+ for client_id in client_requests:
+ for request_type in client_requests[client_id]:
+ await cancel_previous_request(client_id, request_type)
+
+ # Close all sessions
+ for session in client_sessions.values():
+ await session.close()
# Mount static files (this should be after all API routes)
app.mount("/", StaticFiles(directory=STATIC_FILES_DIR, html=True), name="static")