fix: correct ELAI render URL and split long scripts into multiple slides
Made-with: Cursor
This commit is contained in:
Binary file not shown.
@@ -72,9 +72,36 @@ class ElaiService:
|
|||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
return resp.json()
|
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"):
|
def create_video(self, script, *, avatar_id=None, title="EnCoach Video", language="en"):
|
||||||
"""Create an avatar video from a script.
|
"""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:
|
Returns:
|
||||||
dict with 'video_id', 'status'
|
dict with 'video_id', 'status'
|
||||||
"""
|
"""
|
||||||
@@ -85,39 +112,43 @@ class ElaiService:
|
|||||||
avatar_code = avatar_preset["code"]
|
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"
|
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 = {
|
payload = {
|
||||||
"name": title,
|
"name": title,
|
||||||
"slides": [
|
"slides": 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,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": ["encoach"],
|
"tags": ["encoach"],
|
||||||
}
|
}
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
@@ -135,7 +166,7 @@ class ElaiService:
|
|||||||
# Step 2: trigger rendering -- ELAI requires a separate render call
|
# Step 2: trigger rendering -- ELAI requires a separate render call
|
||||||
try:
|
try:
|
||||||
render_resp = _requests.post(
|
render_resp = _requests.post(
|
||||||
f"{ELAI_BASE}/videos/{video_id}/render",
|
f"{ELAI_BASE}/videos/render/{video_id}",
|
||||||
headers=self._headers(),
|
headers=self._headers(),
|
||||||
timeout=30,
|
timeout=30,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user