diff --git a/custom_addons/encoach_ai/services/__pycache__/elai_service.cpython-312.pyc b/custom_addons/encoach_ai/services/__pycache__/elai_service.cpython-312.pyc index 5caae276..41b46b2d 100644 Binary files a/custom_addons/encoach_ai/services/__pycache__/elai_service.cpython-312.pyc and b/custom_addons/encoach_ai/services/__pycache__/elai_service.cpython-312.pyc differ diff --git a/custom_addons/encoach_ai/services/elai_service.py b/custom_addons/encoach_ai/services/elai_service.py index 8fb55d31..8bc0c406 100644 --- a/custom_addons/encoach_ai/services/elai_service.py +++ b/custom_addons/encoach_ai/services/elai_service.py @@ -72,9 +72,36 @@ class ElaiService: resp.raise_for_status() return resp.json() + @staticmethod + def _split_script(script, max_chars=500): + """Split a long script into chunks that stay under ELAI's 60s/slide limit. + + Uses ~10 chars/second heuristic, so 500 chars ~ 50s (with safety margin). + Splits on sentence boundaries ('. ', '? ', '! ', newlines). + """ + if len(script) <= max_chars: + return [script] + import re + sentences = re.split(r'(?<=[.!?])\s+|\n+', script) + chunks, current = [], "" + for sent in sentences: + if not sent.strip(): + continue + if current and len(current) + len(sent) + 1 > max_chars: + chunks.append(current.strip()) + current = sent + else: + current = f"{current} {sent}" if current else sent + if current.strip(): + chunks.append(current.strip()) + return chunks or [script] + def create_video(self, script, *, avatar_id=None, title="EnCoach Video", language="en"): """Create an avatar video from a script. + Automatically splits long scripts into multiple slides to stay under + ELAI's 60-second-per-slide limit. + Returns: dict with 'video_id', 'status' """ @@ -85,39 +112,43 @@ class ElaiService: avatar_code = avatar_preset["code"] avatar_src = f"https://elai-avatars.s3.us-east-2.amazonaws.com/common/{avatar_code.replace('.', '/')}/{avatar_code.replace('.', '_')}.png" + chunks = self._split_script(script) + slides = [] + for idx, chunk in enumerate(chunks, 1): + slides.append({ + "id": idx, + "speech": chunk, + "language": "English", + "voice": avatar_preset["voice"], + "voiceType": "text", + "voiceProvider": "azure", + "avatar": { + "code": avatar_code, + "gender": avatar_preset["gender"], + }, + "canvas": { + "version": "4.4.0", + "background": "#ffffff", + "objects": [ + { + "type": "avatar", + "left": 151.5, + "top": 36, + "fill": "#4868FF", + "scaleX": 0.3, + "scaleY": 0.3, + "height": 1080, + "width": 1080, + "src": avatar_src, + } + ], + }, + }) + _logger.info("ELAI: creating video with %d slide(s) from %d chars", len(slides), len(script)) + payload = { "name": title, - "slides": [ - { - "id": 1, - "speech": script, - "language": "English", - "voice": avatar_preset["voice"], - "voiceType": "text", - "voiceProvider": "azure", - "avatar": { - "code": avatar_code, - "gender": avatar_preset["gender"], - }, - "canvas": { - "version": "4.4.0", - "background": "#ffffff", - "objects": [ - { - "type": "avatar", - "left": 151.5, - "top": 36, - "fill": "#4868FF", - "scaleX": 0.3, - "scaleY": 0.3, - "height": 1080, - "width": 1080, - "src": avatar_src, - } - ], - }, - } - ], + "slides": slides, "tags": ["encoach"], } t0 = time.time() @@ -135,7 +166,7 @@ class ElaiService: # Step 2: trigger rendering -- ELAI requires a separate render call try: render_resp = _requests.post( - f"{ELAI_BASE}/videos/{video_id}/render", + f"{ELAI_BASE}/videos/render/{video_id}", headers=self._headers(), timeout=30, )