tfrere commited on
Commit
dd2e4cf
·
1 Parent(s): d091da8

add elevenlabs

Browse files
client/src/App.jsx CHANGED
@@ -46,6 +46,7 @@ function App() {
46
  const isInitializedRef = useRef(false);
47
  const currentImageRequestRef = useRef(null);
48
  const pendingImageRequests = useRef(new Set()); // Track pending image requests
 
49
 
50
  const generateImageForStory = async (storyText, segmentIndex) => {
51
  try {
@@ -120,6 +121,48 @@ function App() {
120
  }
121
  };
122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  const handleStoryAction = async (action, choiceId = null) => {
124
  setIsLoading(true);
125
  try {
@@ -162,7 +205,10 @@ function App() {
162
  // 5. Désactiver le loading car l'histoire est affichée
163
  setIsLoading(false);
164
 
165
- // 6. Tenter de générer l'image en arrière-plan
 
 
 
166
  try {
167
  const image_base64 = await generateImageForStory(
168
  response.data.story_text,
 
46
  const isInitializedRef = useRef(false);
47
  const currentImageRequestRef = useRef(null);
48
  const pendingImageRequests = useRef(new Set()); // Track pending image requests
49
+ const audioRef = useRef(new Audio());
50
 
51
  const generateImageForStory = async (storyText, segmentIndex) => {
52
  try {
 
121
  }
122
  };
123
 
124
+ const playAudio = async (text) => {
125
+ try {
126
+ console.log("Requesting audio for text:", text);
127
+ const response = await api.post(`${API_URL}/api/text-to-speech`, {
128
+ text: text,
129
+ });
130
+
131
+ if (response.data.success) {
132
+ console.log("Audio received successfully");
133
+ // Arrêter l'audio en cours s'il y en a un
134
+ audioRef.current.pause();
135
+ audioRef.current.currentTime = 0;
136
+
137
+ // Créer et jouer le nouvel audio
138
+ const audioBlob = await fetch(
139
+ `data:audio/mpeg;base64,${response.data.audio_base64}`
140
+ ).then((r) => r.blob());
141
+ console.log("Audio blob created:", audioBlob.size, "bytes");
142
+
143
+ const audioUrl = URL.createObjectURL(audioBlob);
144
+ audioRef.current.src = audioUrl;
145
+ audioRef.current.volume = 1.0; // S'assurer que le volume est au maximum
146
+
147
+ try {
148
+ console.log("Attempting to play audio...");
149
+ await audioRef.current.play();
150
+ console.log("Audio playing successfully");
151
+ } catch (playError) {
152
+ console.error("Error playing audio:", playError);
153
+ }
154
+
155
+ // Nettoyer l'URL une fois l'audio terminé
156
+ audioRef.current.onended = () => {
157
+ URL.revokeObjectURL(audioUrl);
158
+ console.log("Audio finished, URL cleaned up");
159
+ };
160
+ }
161
+ } catch (error) {
162
+ console.error("Error in playAudio:", error);
163
+ }
164
+ };
165
+
166
  const handleStoryAction = async (action, choiceId = null) => {
167
  setIsLoading(true);
168
  try {
 
205
  // 5. Désactiver le loading car l'histoire est affichée
206
  setIsLoading(false);
207
 
208
+ // 6. Lancer la synthèse vocale pour le nouveau segment
209
+ await playAudio(response.data.story_text);
210
+
211
+ // 7. Tenter de générer l'image en arrière-plan
212
  try {
213
  const image_base64 = await generateImageForStory(
214
  response.data.story_text,
client/src/index.css CHANGED
@@ -1,3 +1,5 @@
 
 
1
  * {
2
  margin: 0;
3
  padding: 0;
 
1
+ @import url("https://fonts.googleapis.com/css2?family=Comic+Neue:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap");
2
+
3
  * {
4
  margin: 0;
5
  padding: 0;
client/src/layouts/Layout.jsx DELETED
@@ -1,235 +0,0 @@
1
- import { Box, CircularProgress, Typography } from "@mui/material";
2
-
3
- // Layout settings for different types
4
- export const LAYOUTS = {
5
- COVER: {
6
- gridCols: 1,
7
- gridRows: 1,
8
- panels: [
9
- { width: 1024, height: 1536, gridColumn: "1", gridRow: "1" }, // Format pleine page (2:3 ratio)
10
- ],
11
- },
12
- LAYOUT_1: {
13
- gridCols: 2,
14
- gridRows: 3,
15
- panels: [
16
- { width: 1024, height: 768, gridColumn: "1", gridRow: "1" }, // Landscape top left
17
- { width: 768, height: 1024, gridColumn: "2", gridRow: "1 / span 2" }, // Portrait top right, spans 2 rows
18
- { width: 768, height: 1024, gridColumn: "1", gridRow: "2 / span 2" }, // Portrait bottom left, spans 2 rows
19
- { width: 1024, height: 768, gridColumn: "2", gridRow: "3" }, // Landscape bottom right
20
- ],
21
- },
22
- LAYOUT_2: {
23
- gridCols: 3,
24
- gridRows: 2,
25
- panels: [
26
- { width: 768, height: 1024, gridColumn: "1", gridRow: "1" }, // Portrait top left
27
- { width: 768, height: 1024, gridColumn: "2", gridRow: "1" }, // Portrait top middle
28
- { width: 512, height: 1024, gridColumn: "3", gridRow: "1 / span 2" }, // Tall portrait right, spans full height
29
- { width: 1024, height: 768, gridColumn: "1 / span 2", gridRow: "2" }, // Landscape bottom, spans 2 columns
30
- ],
31
- },
32
- LAYOUT_3: {
33
- gridCols: 3,
34
- gridRows: 2,
35
- panels: [
36
- { width: 1024, height: 768, gridColumn: "1 / span 2", gridRow: "1" }, // Landscape top, spans 2 columns
37
- { width: 768, height: 1024, gridColumn: "3", gridRow: "1" }, // Portrait top right
38
- { width: 768, height: 1024, gridColumn: "1", gridRow: "2" }, // Portrait bottom left
39
- { width: 1024, height: 768, gridColumn: "2 / span 2", gridRow: "2" }, // Landscape bottom right, spans 2 columns
40
- ],
41
- },
42
- LAYOUT_4: {
43
- gridCols: 8,
44
- gridRows: 8,
45
- panels: [
46
- {
47
- width: 512,
48
- height: 1024,
49
- gridColumn: "1 / span 6",
50
- gridRow: "1 / span 2",
51
- }, // Wide top
52
- {
53
- width: 1024,
54
- height: 768,
55
- gridColumn: "3 / span 6",
56
- gridRow: "3 / span 1",
57
- }, // Middle right
58
- {
59
- width: 768,
60
- height: 1024,
61
- gridColumn: "2 / span 6",
62
- gridRow: "4 / span 2",
63
- }, // Middle center
64
- {
65
- width: 1024,
66
- height: 512,
67
- gridColumn: "1 / span 8",
68
- gridRow: "6 / span 2",
69
- }, // Wide bottom
70
- ],
71
- },
72
- };
73
-
74
- // Function to group segments into layouts
75
- function groupSegmentsIntoLayouts(segments) {
76
- if (segments.length === 0) return [];
77
-
78
- const layouts = [];
79
-
80
- // Premier segment toujours en COVER s'il est marqué comme first_step
81
- if (segments[0].is_first_step) {
82
- layouts.push({
83
- type: "COVER",
84
- segments: [segments[0]],
85
- });
86
- }
87
-
88
- // Segments du milieu (on exclut le premier s'il était en COVER)
89
- const startIndex = segments[0].is_first_step ? 1 : 0;
90
- const middleSegments = segments.slice(startIndex);
91
- let currentIndex = 0;
92
-
93
- while (currentIndex < middleSegments.length) {
94
- const segment = middleSegments[currentIndex];
95
-
96
- // Si c'est le dernier segment (mort ou victoire), on le met en COVER
97
- if (segment.is_last_step) {
98
- layouts.push({
99
- type: "COVER",
100
- segments: [segment],
101
- });
102
- } else {
103
- // Sinon on utilise un layout normal
104
- const layoutType = `LAYOUT_${(layouts.length % 3) + 1}`;
105
- const maxPanels = LAYOUTS[layoutType].panels.length;
106
- const availableSegments = middleSegments
107
- .slice(currentIndex)
108
- .filter((s) => !s.is_last_step);
109
-
110
- if (availableSegments.length > 0) {
111
- layouts.push({
112
- type: layoutType,
113
- segments: availableSegments.slice(0, maxPanels),
114
- });
115
- currentIndex += Math.min(maxPanels, availableSegments.length) - 1;
116
- }
117
- }
118
-
119
- currentIndex++;
120
- }
121
-
122
- console.log("Generated layouts:", layouts); // Debug log
123
- return layouts;
124
- }
125
-
126
- export function ComicLayout({ segments }) {
127
- const layouts = groupSegmentsIntoLayouts(segments);
128
-
129
- return (
130
- <Box
131
- sx={{
132
- display: "flex",
133
- flexDirection: "row",
134
- gap: 4,
135
- height: "100%",
136
- width: "100%",
137
- }}
138
- >
139
- {layouts.map((layout, layoutIndex) => (
140
- <Box
141
- key={layoutIndex}
142
- sx={{
143
- display: "grid",
144
- gridTemplateColumns: `repeat(${
145
- LAYOUTS[layout.type].gridCols
146
- }, 1fr)`,
147
- gridTemplateRows: `repeat(${LAYOUTS[layout.type].gridRows}, 1fr)`,
148
- gap: 2,
149
- height: "100%",
150
- aspectRatio: "0.7",
151
- backgroundColor: "white",
152
- boxShadow: "0 0 10px rgba(0,0,0,0.1)",
153
- borderRadius: "4px",
154
- p: 2,
155
- flexShrink: 0,
156
- }}
157
- >
158
- {/* Render all panels of the layout */}
159
- {LAYOUTS[layout.type].panels.map((panel, panelIndex) => {
160
- // Find the segment for this panel position if it exists
161
- const segment = layout.segments[panelIndex];
162
-
163
- return (
164
- <Box
165
- key={panelIndex}
166
- sx={{
167
- position: "relative",
168
- width: "100%",
169
- height: "100%",
170
- gridColumn: panel.gridColumn,
171
- gridRow: panel.gridRow,
172
- bgcolor: "white",
173
- border: "1px solid",
174
- borderColor: "grey.200",
175
- borderRadius: "8px",
176
- overflow: "hidden",
177
- }}
178
- >
179
- {segment ? (
180
- // If there's a segment, render image and text
181
- <>
182
- {segment.image_base64 ? (
183
- <img
184
- src={`data:image/jpeg;base64,${segment.image_base64}`}
185
- alt="Story scene"
186
- style={{
187
- width: "100%",
188
- height: "100%",
189
- objectFit: "cover",
190
- borderRadius: "8px",
191
- opacity: 0,
192
- transition: "opacity 0.5s ease-in-out",
193
- }}
194
- onLoad={(e) => {
195
- e.target.style.opacity = "1";
196
- }}
197
- />
198
- ) : (
199
- <Box
200
- sx={{
201
- width: "100%",
202
- height: "100%",
203
- display: "flex",
204
- alignItems: "center",
205
- justifyContent: "center",
206
- }}
207
- >
208
- <CircularProgress sx={{ opacity: 0.3 }} />
209
- </Box>
210
- )}
211
- <Box
212
- sx={{
213
- position: "absolute",
214
- bottom: "20px",
215
- left: "20px",
216
- right: "20px",
217
- backgroundColor: "rgba(255, 255, 255, 0.9)",
218
- fontSize: ".9rem",
219
- padding: "24px",
220
- borderRadius: "8px",
221
- boxShadow: "0 -2px 4px rgba(0,0,0,0.1)",
222
- }}
223
- >
224
- {segment.text}
225
- </Box>
226
- </>
227
- ) : null}
228
- </Box>
229
- );
230
- })}
231
- </Box>
232
- ))}
233
- </Box>
234
- );
235
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
client/src/layouts/config.js CHANGED
@@ -4,70 +4,108 @@ export const LAYOUTS = {
4
  gridCols: 1,
5
  gridRows: 1,
6
  panels: [
7
- { width: 1024, height: 1024, gridColumn: "1", gridRow: "1" }, // Format pleine page (1:1 ratio)
8
  ],
9
  },
10
  LAYOUT_1: {
11
- gridCols: 2,
12
- gridRows: 2,
13
  panels: [
14
- { width: 1024, height: 768, gridColumn: "1", gridRow: "1" }, // 1. Landscape top left
15
- { width: 768, height: 1024, gridColumn: "2", gridRow: "1" }, // 2. Portrait top right
16
- { width: 1024, height: 768, gridColumn: "1", gridRow: "2" }, // 3. Landscape middle left
17
- { width: 768, height: 1024, gridColumn: "2", gridRow: "2" }, // 4. Portrait right, spans bottom rows
18
  ],
19
  },
20
  LAYOUT_2: {
21
- gridCols: 3,
22
- gridRows: 2,
23
  panels: [
24
- { width: 1024, height: 1024, gridColumn: "1 / span 2", gridRow: "1" }, // 1. Large square top left
25
- { width: 512, height: 1024, gridColumn: "3", gridRow: "1" }, // 2. Portrait top right
26
- { width: 1024, height: 768, gridColumn: "1 / span 3", gridRow: "2" }, // 3. Landscape bottom, spans full width
27
  ],
28
  },
29
  LAYOUT_3: {
30
- gridCols: 3,
31
- gridRows: 2,
32
  panels: [
33
- { width: 1024, height: 768, gridColumn: "1 / span 2", gridRow: "1" }, // 1. Landscape top left, spans 2 columns
34
- { width: 768, height: 1024, gridColumn: "3", gridRow: "1" }, // 2. Portrait top right
35
- { width: 768, height: 1024, gridColumn: "1", gridRow: "2" }, // 3. Portrait bottom left
36
- { width: 1024, height: 768, gridColumn: "2 / span 2", gridRow: "2" }, // 4. Landscape bottom right, spans 2 columns
37
  ],
38
  },
39
  LAYOUT_4: {
40
- gridCols: 8,
41
- gridRows: 8,
42
  panels: [
43
- {
44
- width: 768,
45
- height: 768,
46
- gridColumn: "1 / span 3",
47
- gridRow: "1 / span 3",
48
- }, // 1. Square top left
49
- {
50
- width: 768,
51
- height: 1024,
52
- gridColumn: "1 / span 3",
53
- gridRow: "4 / span 5",
54
- }, // 2. Long portrait bottom left
55
- {
56
- width: 768,
57
- height: 1024,
58
- gridColumn: "5 / span 3",
59
- gridRow: "1 / span 5",
60
- }, // 3. Long portrait top right
61
- {
62
- width: 768,
63
- height: 768,
64
- gridColumn: "5 / span 3",
65
- gridRow: "6 / span 3",
66
- }, // 4. Square bottom right
67
  ],
68
  },
69
  };
70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  export const defaultLayout = "LAYOUT_1";
72
  export const nonRandomLayouts = Object.keys(LAYOUTS).filter(
73
  (layout) => layout !== "random"
 
4
  gridCols: 1,
5
  gridRows: 1,
6
  panels: [
7
+ { width: 512, height: 1024, gridColumn: "1", gridRow: "1" }, // Format pleine page (1:1 ratio)
8
  ],
9
  },
10
  LAYOUT_1: {
11
+ gridCols: 1,
12
+ gridRows: 1,
13
  panels: [
14
+ { width: 512, height: 1024, gridColumn: "1", gridRow: "1" }, // Format pleine page (1:1 ratio)
 
 
 
15
  ],
16
  },
17
  LAYOUT_2: {
18
+ gridCols: 1,
19
+ gridRows: 1,
20
  panels: [
21
+ { width: 512, height: 1024, gridColumn: "1", gridRow: "1" }, // Format pleine page (1:1 ratio)
 
 
22
  ],
23
  },
24
  LAYOUT_3: {
25
+ gridCols: 1,
26
+ gridRows: 1,
27
  panels: [
28
+ { width: 512, height: 1024, gridColumn: "1", gridRow: "1" }, // Format pleine page (1:1 ratio)
 
 
 
29
  ],
30
  },
31
  LAYOUT_4: {
32
+ gridCols: 1,
33
+ gridRows: 1,
34
  panels: [
35
+ { width: 512, height: 1024, gridColumn: "1", gridRow: "1" }, // Format pleine page (1:1 ratio)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  ],
37
  },
38
  };
39
 
40
+ // export const LAYOUTS = {
41
+ // COVER: {
42
+ // gridCols: 1,
43
+ // gridRows: 1,
44
+ // panels: [
45
+ // { width: 1024, height: 512, gridColumn: "1", gridRow: "1" }, // Format pleine page (1:1 ratio)
46
+ // ],
47
+ // },
48
+ // LAYOUT_1: {
49
+ // gridCols: 2,
50
+ // gridRows: 2,
51
+ // panels: [
52
+ // { width: 1024, height: 768, gridColumn: "1", gridRow: "1" }, // 1. Landscape top left
53
+ // { width: 768, height: 1024, gridColumn: "2", gridRow: "1" }, // 2. Portrait top right
54
+ // { width: 1024, height: 768, gridColumn: "1", gridRow: "2" }, // 3. Landscape middle left
55
+ // { width: 768, height: 1024, gridColumn: "2", gridRow: "2" }, // 4. Portrait right, spans bottom rows
56
+ // ],
57
+ // },
58
+ // LAYOUT_2: {
59
+ // gridCols: 3,
60
+ // gridRows: 2,
61
+ // panels: [
62
+ // { width: 1024, height: 1024, gridColumn: "1 / span 2", gridRow: "1" }, // 1. Large square top left
63
+ // { width: 512, height: 1024, gridColumn: "3", gridRow: "1" }, // 2. Portrait top right
64
+ // { width: 1024, height: 768, gridColumn: "1 / span 3", gridRow: "2" }, // 3. Landscape bottom, spans full width
65
+ // ],
66
+ // },
67
+ // LAYOUT_3: {
68
+ // gridCols: 3,
69
+ // gridRows: 2,
70
+ // panels: [
71
+ // { width: 1024, height: 768, gridColumn: "1 / span 2", gridRow: "1" }, // 1. Landscape top left, spans 2 columns
72
+ // { width: 512, height: 1024, gridColumn: "3", gridRow: "1" }, // 2. Portrait top right
73
+ // { width: 512, height: 1024, gridColumn: "1", gridRow: "2" }, // 3. Portrait bottom left
74
+ // { width: 1024, height: 768, gridColumn: "2 / span 2", gridRow: "2" }, // 4. Landscape bottom right, spans 2 columns
75
+ // ],
76
+ // },
77
+ // LAYOUT_4: {
78
+ // gridCols: 8,
79
+ // gridRows: 8,
80
+ // panels: [
81
+ // {
82
+ // width: 768,
83
+ // height: 768,
84
+ // gridColumn: "1 / span 3",
85
+ // gridRow: "1 / span 3",
86
+ // }, // 1. Square top left
87
+ // {
88
+ // width: 768,
89
+ // height: 1024,
90
+ // gridColumn: "1 / span 3",
91
+ // gridRow: "4 / span 5",
92
+ // }, // 2. Long portrait bottom left
93
+ // {
94
+ // width: 768,
95
+ // height: 1024,
96
+ // gridColumn: "5 / span 3",
97
+ // gridRow: "1 / span 5",
98
+ // }, // 3. Long portrait top right
99
+ // {
100
+ // width: 768,
101
+ // height: 768,
102
+ // gridColumn: "5 / span 3",
103
+ // gridRow: "6 / span 3",
104
+ // }, // 4. Square bottom right
105
+ // ],
106
+ // },
107
+ // };
108
+
109
  export const defaultLayout = "LAYOUT_1";
110
  export const nonRandomLayouts = Object.keys(LAYOUTS).filter(
111
  (layout) => layout !== "random"
server/api_clients.py CHANGED
@@ -61,7 +61,7 @@ class MistralClient:
61
  class FluxClient:
62
  def __init__(self, api_key: str):
63
  self.api_key = api_key
64
- self.endpoint = os.getenv("FLUX_ENDPOINT", "https://api-inference.huggingface.co/models/stabilityai/sdxl-turbo")
65
 
66
  def generate_image(self,
67
  prompt: str,
@@ -92,7 +92,7 @@ class FluxClient:
92
  "guidance_scale": guidance_scale,
93
  "width": width,
94
  "height": height,
95
- "negative_prompt": "text, watermark, logo, signature, blurry, low quality"
96
  }
97
  }
98
  )
 
61
  class FluxClient:
62
  def __init__(self, api_key: str):
63
  self.api_key = api_key
64
+ self.endpoint = os.getenv("FLUX_ENDPOINT")
65
 
66
  def generate_image(self,
67
  prompt: str,
 
92
  "guidance_scale": guidance_scale,
93
  "width": width,
94
  "height": height,
95
+ "negative_prompt": "speech bubble, caption, subtitle"
96
  }
97
  }
98
  )
server/server.py CHANGED
@@ -34,6 +34,7 @@ API_PORT = int(os.getenv("API_PORT", "8000"))
34
  STATIC_FILES_DIR = os.getenv("STATIC_FILES_DIR", "../client/dist")
35
  HF_API_KEY = os.getenv("HF_API_KEY")
36
  AWS_TOKEN = os.getenv("AWS_TOKEN", "VHVlIEZlYiAyNyAwOTowNzoyMiBDRVQgMjAyNA==") # Token par défaut pour le développement
 
37
 
38
  app = FastAPI(title="Echoes of Influence")
39
 
@@ -116,6 +117,10 @@ class ImageGenerationResponse(BaseModel):
116
  image_base64: Optional[str] = None
117
  error: Optional[str] = None
118
 
 
 
 
 
119
  async def get_test_image(client_id: str, width=1024, height=1024):
120
  """Get a random image from Lorem Picsum"""
121
  # Build the Lorem Picsum URL with blur and grayscale effects
@@ -354,6 +359,47 @@ async def test_generate_image(request: Request, image_request: ImageGenerationRe
354
  "error": str(e)
355
  }
356
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  @app.on_event("shutdown")
358
  async def shutdown_event():
359
  """Clean up sessions on shutdown"""
 
34
  STATIC_FILES_DIR = os.getenv("STATIC_FILES_DIR", "../client/dist")
35
  HF_API_KEY = os.getenv("HF_API_KEY")
36
  AWS_TOKEN = os.getenv("AWS_TOKEN", "VHVlIEZlYiAyNyAwOTowNzoyMiBDRVQgMjAyNA==") # Token par défaut pour le développement
37
+ ELEVEN_LABS_API_KEY = os.getenv("ELEVEN_LABS_API_KEY") # Nouvelle clé d'API
38
 
39
  app = FastAPI(title="Echoes of Influence")
40
 
 
117
  image_base64: Optional[str] = None
118
  error: Optional[str] = None
119
 
120
+ class TextToSpeechRequest(BaseModel):
121
+ text: str
122
+ voice_id: str = "nPczCjzI2devNBz1zQrb" # Default voice ID (Rachel)
123
+
124
  async def get_test_image(client_id: str, width=1024, height=1024):
125
  """Get a random image from Lorem Picsum"""
126
  # Build the Lorem Picsum URL with blur and grayscale effects
 
359
  "error": str(e)
360
  }
361
 
362
+ @app.post("/api/text-to-speech")
363
+ async def text_to_speech(request: TextToSpeechRequest):
364
+ """Endpoint pour convertir du texte en audio via ElevenLabs"""
365
+ try:
366
+ if not ELEVEN_LABS_API_KEY:
367
+ raise HTTPException(status_code=500, detail="ElevenLabs API key not configured")
368
+
369
+ # Nettoyer le texte des balises markdown **
370
+ clean_text = request.text.replace("**", "")
371
+
372
+ # Appel à l'API ElevenLabs
373
+ url = f"https://api.elevenlabs.io/v1/text-to-speech/{request.voice_id}"
374
+ headers = {
375
+ "Accept": "audio/mpeg",
376
+ "Content-Type": "application/json",
377
+ "xi-api-key": ELEVEN_LABS_API_KEY
378
+ }
379
+ data = {
380
+ "text": clean_text,
381
+ "model_id": "eleven_multilingual_v2",
382
+ "voice_settings": {
383
+ "stability": 0.5,
384
+ "similarity_boost": 0.75
385
+ }
386
+ }
387
+
388
+ async with aiohttp.ClientSession() as session:
389
+ async with session.post(url, json=data, headers=headers) as response:
390
+ if response.status == 200:
391
+ audio_content = await response.read()
392
+ # Convertir l'audio en base64 pour l'envoyer au client
393
+ audio_base64 = base64.b64encode(audio_content).decode('utf-8')
394
+ return {"success": True, "audio_base64": audio_base64}
395
+ else:
396
+ error_text = await response.text()
397
+ raise HTTPException(status_code=response.status, detail=error_text)
398
+
399
+ except Exception as e:
400
+ print(f"Error in text_to_speech: {str(e)}")
401
+ raise HTTPException(status_code=500, detail=str(e))
402
+
403
  @app.on_event("shutdown")
404
  async def shutdown_event():
405
  """Clean up sessions on shutdown"""