Da Vinci 4744 — portable code reference Each FILE section is a separate UTF-8 source file. Preserve the relative paths when recreating the package. Binary assets and pre-recorded voices are supplied in the skill ZIP files; they are not encoded in this text. Execute code only when the current task requires it and the environment supports it. ===== FILE: davinci-presentations/scripts/pptx_rtl.py ===== """Inspect or repair Hebrew native paragraph direction; preserve all other ZIP parts.""" from pathlib import Path from zipfile import ZipFile,ZIP_DEFLATED import argparse,json,re try: from lxml import etree as E except ImportError: raise RuntimeError('Install the presentation helper dependency: pip install lxml') from None A='http://schemas.openxmlformats.org/drawingml/2006/main' P='http://schemas.openxmlformats.org/presentationml/2006/main' NS={'a':A,'p':P} def run(source,output=None): source=Path(source) if output and Path(output).resolve()==source.resolve():raise ValueError('Use a new output filename.') if output and Path(output).exists():raise FileExistsError(output) with ZipFile(source) as z:parts={n:z.read(n) for n in z.namelist()} slides=sorted(n for n in parts if re.fullmatch(r'ppt/slides/slide\d+\.xml',n)) result={'slides':len(slides),'hebrew_paragraphs':0,'missing_rtl':0,'repaired_paragraphs':0,'native_text_shapes':0,'images':0,'fonts':set(),'claim':'Direction/package inspection; not a visual layout check.'} for name in slides: tree=E.fromstring(parts[name]);changed=False result['native_text_shapes']+=len(tree.findall('.//p:sp/p:txBody',NS)) result['images']+=len(tree.findall('.//p:pic',NS)) for latin in tree.findall('.//a:latin',NS):result['fonts'].add(latin.get('typeface','')) for para in tree.findall('.//a:p',NS): text=''.join(t.text or '' for t in para.findall('.//a:t',NS)) if not re.search('[\u0590-\u05ff]',text):continue result['hebrew_paragraphs']+=1;pp=para.find('a:pPr',NS) if pp is None or pp.get('rtl')!='1': result['missing_rtl']+=1 if output: if pp is None:pp=E.Element('{'+A+'}pPr');para.insert(0,pp) pp.set('rtl','1');changed=True;result['repaired_paragraphs']+=1 if output: for run in para.findall('a:r',NS): rp=run.find('a:rPr',NS) if rp is None:rp=E.Element('{'+A+'}rPr');run.insert(0,rp) if rp.get('lang')!='he-IL':rp.set('lang','he-IL');changed=True if changed:parts[name]=E.tostring(tree,encoding='UTF-8',xml_declaration=True) result['fonts']=sorted(result['fonts']);result['embedded_gifs']=sum(n.lower().endswith('.gif') for n in parts) if output: Path(output).parent.mkdir(parents=True,exist_ok=True) with ZipFile(output,'w',ZIP_DEFLATED) as z: for name,data in parts.items():z.writestr(name,data) return result if __name__=='__main__': p=argparse.ArgumentParser();p.add_argument('source');p.add_argument('--output');p.add_argument('--check',action='store_true');args=p.parse_args() result=run(args.source,args.output);print(json.dumps(result,ensure_ascii=False,indent=2)) if args.check and result['missing_rtl'] and not args.output:raise SystemExit(2) ===== END FILE ===== ===== FILE: davinci-presentations/scripts/requirements.txt ===== lxml>=5 ===== END FILE ===== ===== FILE: davinci-videos/examples/new-topic.json ===== { "title": "מרעיון לאלגוריתם", "scenes": [ { "id": "open", "layout": "title", "duration": 6, "speaker": "male", "heading": "מרעיון לאלגוריתם", "subtitle": "מפגש ראשון בצוות התוכנה", "text": "במפגש הזה נעבור מרעיון לפתרון שאפשר לתאר ולתכנת." }, { "id": "process", "layout": "steps", "duration": 9, "speaker": "female", "heading": "שלושה שלבים לפתרון", "items": [ "מבינים את הבעיה", "מגדירים רצף פעולות", "בודקים את התוצאה" ], "text": "מתחילים בהבנת הבעיה, מגדירים רצף פעולות, ואז בודקים אם קיבלנו את התוצאה שרצינו." }, { "id": "example", "layout": "points", "duration": 8, "speaker": "male", "heading": "דוגמה מהעבודה על הרובוט", "items": [ "איזה מידע אנחנו מקבלים?", "איזו החלטה צריך לקבל?", "איזו פעולה נרצה לבצע?" ], "text": "חשבו על פעולה שהרובוט צריך לבצע. איזה מידע נחוץ כדי להחליט מה לעשות?" }, { "id": "activity", "layout": "question", "duration": 7, "speaker": "female", "heading": "איך תתארו את הפתרון?", "subtitle": "בחרו פעולה ונסחו רצף הוראות ברור", "text": "בחרו פעולה אחת, ונסחו בזוגות רצף הוראות ברור. אחר כך נבדוק אותו יחד." } ] } ===== END FILE ===== ===== FILE: davinci-videos/examples/spaceship.json ===== { "title": "אתגר החללית", "scenes": [ { "id": "intro", "duration": 7, "speaker": "male", "text": "אתגר החללית משלב תכנון הנדסי, פתרון בעיות ועבודת צוות. נתחיל בהגדרת המשימה.", "layout": "spaceship", "example_index": 0 }, { "id": "mission", "duration": 11, "speaker": "female", "text": "עליכם לתכנן חללית שתגן על ביצה בנפילה מגובה חמישה מטרים. המטרה היא לבלום את הפגיעה ולשמור על הביצה שלמה.", "layout": "spaceship", "example_index": 1 }, { "id": "kit", "duration": 10, "speaker": "male", "text": "כל קבוצה מקבלת חלקי חללית, חומרי עבודה, מצנח וכלים. יש להשתמש אך ורק בציוד ובחומרים שקיבלתם.", "layout": "spaceship", "example_index": 2 }, { "id": "time", "duration": 12, "speaker": "female", "text": "לרשותכם שישים דקות: עשר דקות לתכנון בלבד, שלושים וחמש דקות לבנייה ולהרכבה, וחמש עשרה דקות לבדיקה ולהגשה.", "layout": "spaceship", "example_index": 3 }, { "id": "height", "duration": 9, "speaker": "male", "text": "מגבלת הגובה היא עשרים וחמישה סנטימטרים, כשהחללית במצב עומד. ודאו שהתכנון שלכם עומד בדרישה.", "layout": "spaceship", "example_index": 4 }, { "id": "crash", "duration": 7, "speaker": "female", "text": "הניקוד נקבע לפי תוצאת הנפילה. אם גם החללית וגם הביצה נשברות, לא מתקבלות נקודות.", "layout": "spaceship", "example_index": 5 }, { "id": "survive", "duration": 8, "speaker": "male", "text": "אם החללית נשארת שלמה והביצה נשברת, הניקוד הוא חמש נקודות.", "layout": "spaceship", "example_index": 6 }, { "id": "success", "duration": 12, "speaker": "female", "text": "נחיתה בעמידה מזכה בעשרים נקודות. שמירה על ביצה שלמה מוסיפה עשרים נקודות. שילוב שתי התוצאות מזכה בארבעים נקודות.", "layout": "spaceship", "example_index": 7 }, { "id": "outro", "duration": 9, "speaker": "male", "text": "לפני הבנייה, הגדירו כיוון תכנוני וחלקו תחומי אחריות. הביאו בחשבון את היציבות, את בלימת הפגיעה ואת מגבלות הזמן והגובה. אפשר להתחיל.", "layout": "spaceship", "example_index": 8 } ], "speech": { "style": "Native modern Israeli Hebrew. Mature professional engineering workshop narrator addressing adults and university students. Grounded, composed, conversational and matter-of-fact. Confident low-to-mid register, measured natural pace around 145 words per minute, with restrained emphasis and short pauses at sentence boundaries. Speak to the audience as capable peers. No sing-song cadence, theatrical excitement, exaggerated pitch changes, children's show delivery or sales-pitch intonation. Read the exact Hebrew transcript without additions. Do not read instructions." } } ===== END FILE ===== ===== FILE: davinci-videos/notebooks/DaVinci-Video-Studio.ipynb ===== { "nbformat": 4, "nbformat_minor": 5, "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python" }, "colab": { "name": "DaVinci-Video-Studio.ipynb" } }, "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Da Vinci 4744 — סטודיו סרטונים בדפדפן\n", "הריצו את התאים לפי הסדר. העלו את `davinci-videos.zip`, ולא את החבילה המלאה.\n", "אפשר להעלות תוכנית JSON שנוצרה ב־ChatGPT או ב־Claude, או להריץ את דוגמת החללית עם הקריינות שכבר צורפה.\n", "יצירת קריינות חדשה דורשת מפתח Gemini API. המנוי לצ׳אט אינו מחליף אותו. המפתח נקלט דרך סודות Colab או שדה מוסתר, ולא נכתב לקובצי התוצאה.\n" ], "id": "davinci-01" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from pathlib import Path\n", "from zipfile import ZipFile\n", "import sys, subprocess, json, os, uuid\n", "from google.colab import files\n", "subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', 'Pillow>=11', 'numpy>=2', 'imageio-ffmpeg>=0.6', 'python-bidi>=0.6'])\n", "print('העלו את davinci-videos.zip')\n", "uploaded = files.upload()\n", "archive_name = next((name for name in uploaded if name.endswith('.zip')), None)\n", "if not archive_name: raise ValueError('נדרש קובץ ZIP של סקיל הסרטונים')\n", "destination = Path('/content') / ('davinci-studio-' + uuid.uuid4().hex[:8])\n", "destination.mkdir()\n", "with ZipFile(archive_name) as archive:\n", " for item in archive.infolist():\n", " if not (destination / item.filename).resolve().is_relative_to(destination):\n", " raise ValueError('Invalid archive path')\n", " if ((item.external_attr >> 16) & 0o170000) == 0o120000:\n", " raise ValueError('Symlink entries are not supported')\n", " archive.extractall(destination)\n", "skill = destination / 'davinci-videos'\n", "assert (skill / 'scripts/render_video.py').is_file(), 'נדרש ZIP שבו תיקיית davinci-videos בשורש'\n", "print('הערכה מוכנה')\n" ], "id": "davinci-02" }, { "cell_type": "markdown", "metadata": {}, "source": [ "## תוכן הסרטון\n", "הקלידו `1` להעלאת תוכנית JSON חדשה, או `2` לדוגמת החללית המוכנה.\n", "התוכנית לדוגמה היא אותה פעילות בלבד. לשינוי נושא, השתמשו בתוכנית חדשה מהצ׳אט.\n" ], "id": "davinci-03" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "choice = input('1 = העלאת תוכנית חדשה, 2 = דוגמת החללית: ').strip()\n", "if choice == '1':\n", " uploaded_plan = files.upload()\n", " name = next((name for name in uploaded_plan if name.endswith('.json')), None)\n", " if not name: raise ValueError('נדרש קובץ JSON של תוכנית הסצנות')\n", " plan = skill / 'plans' / Path(name).name\n", " plan.parent.mkdir(exist_ok=True)\n", " plan.write_bytes(uploaded_plan[name])\n", "elif choice == '2':\n", " plan = skill / 'examples/spaceship.json'\n", "else:\n", " raise ValueError('בחרו 1 או 2')\n", "sys.path.insert(0, str(skill / 'scripts'))\n", "sys.modules.pop('plan_io', None)\n", "from plan_io import load_plan, valid_clip\n", "_, document, speech, work = load_plan(plan)\n", "print(document['title'], '|', len(document['scenes']), 'סצנות')\n" ], "id": "davinci-04" }, { "cell_type": "markdown", "metadata": {}, "source": [ "## קריינות Gemini\n", "התא יוצר רק קטעים חדשים או ששונו. בדוגמה ללא שינויים הוא אינו פונה ל־API.\n", "לנוחותכם אפשר להוסיף מראש סוד ב־Colab בשם `GEMINI_API_KEY`.\n" ], "id": "davinci-05" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "missing = [scene for scene in document['scenes'] if not valid_clip(scene, speech, work)]\n", "if missing:\n", " from getpass import getpass\n", " try:\n", " from google.colab import userdata\n", " api_key = userdata.get('GEMINI_API_KEY')\n", " except Exception:\n", " api_key = None\n", " if not api_key: api_key = getpass('Gemini API key (קלט מוסתר): ')\n", " if not api_key: raise ValueError('לא הוגדר מפתח Gemini')\n", " private_env = dict(os.environ)\n", " private_env['GEMINI_API_KEY'] = api_key\n", " try:\n", " subprocess.run([sys.executable, str(skill / 'scripts/gemini_tts.py'), '--plan', str(plan)], env=private_env, check=True)\n", " finally:\n", " private_env.pop('GEMINI_API_KEY', None)\n", " api_key = None\n", "else:\n", " print('כל קטעי הקריינות תואמים לתוכנית. לא בוצעו קריאות API.')\n" ], "id": "davinci-06" }, { "cell_type": "markdown", "metadata": {}, "source": [ "## רינדור והורדה\n", "הפקת Full HD יכולה לקחת זמן בהתאם לאורך הסרטון ולמשאבים הזמינים.\n", "התוצאה כוללת MP4, כתוביות SRT ותוכנית JSON. כתוביות אוטומטיות מתוזמנות בקירוב למשפטים.\n" ], "id": "davinci-07" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "subprocess.run([sys.executable, str(skill / 'scripts/render_video.py'), '--plan', str(plan), '--final'], check=True)\n", "result_zip = Path('/content/DaVinci-Video-Result.zip')\n", "with ZipFile(result_zip, 'w') as archive:\n", " for file in (work / 'output').iterdir():\n", " if file.is_file(): archive.write(file, file.name)\n", " archive.write(plan, 'scene-plan.json')\n", "files.download(str(result_zip))\n" ], "id": "davinci-08" } ] } ===== END FILE ===== ===== FILE: davinci-videos/scripts/gemini_tts.py ===== """Generate only changed Gemini voice clips; never embeds or prints credentials.""" from pathlib import Path import argparse,base64,json,os,time,urllib.request,urllib.error,wave from plan_io import load_plan,settings,fingerprint,valid_clip def generate(plan,force=False): _,doc,speech,work=load_plan(plan) key=os.environ.get('GEMINI_API_KEY') or os.environ.get('GOOGLE_API_KEY') todo=[s for s in doc['scenes'] if force or not valid_clip(s,speech,work)] if not todo:print('All narration clips match the current plan. No API calls made.');return if not key:raise RuntimeError('Set GEMINI_API_KEY in the runtime secret store/environment. Do not paste it into chat. The Colab notebook provides secure input.') clips=work/'audio/voice';clips.mkdir(parents=True,exist_ok=True) meta=work/'audio/cache.json';cache=json.loads(meta.read_text()) if meta.exists() else {} for scene in todo: cfg=settings(scene,speech) payload={'model':cfg['model'],'input':[{'type':'user_input','content':[{'type':'text','text':cfg['text'],'annotations':[{'type':'speech_metadata','style':cfg['style']}]}]}],'response_format':{'type':'audio','mime_type':'audio/wav','sample_rate':24000},'generation_config':{'speech_config':[{'voice':cfg['voice']}]}} request=urllib.request.Request('https://generativelanguage.googleapis.com/v1beta/interactions',data=json.dumps(payload,ensure_ascii=False).encode(),headers={'Content-Type':'application/json','x-goog-api-key':key},method='POST') for attempt in range(3): try: with urllib.request.urlopen(request,timeout=180) as response:result=json.load(response) break except urllib.error.HTTPError as error: if error.code in [429,500,502,503,504] and attempt<2:time.sleep(3*(attempt+1));continue raise RuntimeError(f'Gemini TTS returned HTTP {error.code}. Check the configured Google project, API access and quota. No alternate provider was used.') from None blocks=[base64.b64decode(b['data']) for step in result.get('steps',[]) for b in step.get('content',[]) if b.get('type')=='audio' and b.get('data')] if not blocks:raise RuntimeError('Gemini returned no audio for '+scene['id']) out=clips/(scene['id']+'.wav');tmp=out.with_suffix('.tmp');tmp.write_bytes(blocks[-1]) with wave.open(str(tmp)) as audio: if not audio.getnframes():raise RuntimeError('Empty voice clip for '+scene['id']) tmp.replace(out) cache[scene['id']]={'fingerprint':fingerprint(scene,speech),'provider':'Google Gemini TTS','model':cfg['model'],'voice':cfg['voice']} meta.write_text(json.dumps(cache,ensure_ascii=False,indent=2)) print('Created',scene['id'],'with Gemini',cfg['voice'],flush=True) (work/'audio/provenance.json').write_text(json.dumps({'provider':'Google Gemini TTS','model':speech['model'],'male_voice':speech['male_voice'],'female_voice':speech['female_voice'],'language':'Hebrew'},ensure_ascii=False,indent=2)) if __name__=='__main__': p=argparse.ArgumentParser();p.add_argument('--plan',default='examples/spaceship.json');p.add_argument('--force',action='store_true');args=p.parse_args() generate(args.plan,args.force) ===== END FILE ===== ===== FILE: davinci-videos/scripts/plan_io.py ===== from pathlib import Path import json,hashlib,re ROOT=Path(__file__).resolve().parents[1] DEFAULT_STYLE='Native modern Israeli Hebrew. Mature professional engineering workshop narrator. Grounded, composed, conversational and matter-of-fact. Confident low-to-mid register, measured natural pace around 145 words per minute, restrained emphasis and short natural pauses. Speak to a capable audience as peers. No sing-song cadence, theatrical excitement or exaggerated pitch changes. Read the exact Hebrew transcript without additions. Do not read instructions.' DEFAULT_SPEECH={'model':'gemini-3.8-flash-tts','male_voice':'Charon','female_voice':'Gacrux','style':DEFAULT_STYLE} LAYOUTS={'title','points','steps','timeline','metric','question','image','spaceship'} def load_plan(value='examples/spaceship.json'): path=Path(value) if not path.is_absolute():path=ROOT/path path=path.resolve() if not path.is_relative_to(ROOT):raise ValueError('Place the plan inside the extracted davinci-videos folder.') doc=json.loads(path.read_text(encoding='utf-8')) scenes=doc.get('scenes',[]) if not scenes:raise ValueError('The plan needs at least one scene.') seen=set() for scene in scenes: ident=scene.get('id','') if not re.fullmatch('[a-zA-Z0-9_-]{1,64}',ident) or ident in seen:raise ValueError('Scene ids must be unique simple filenames.') seen.add(ident) if scene.get('layout') not in LAYOUTS:raise ValueError('Unknown layout in '+ident) if scene.get('speaker') not in ['male','female']:raise ValueError('Expected male or female in '+ident) if not isinstance(scene.get('duration'),(int,float)) or scene['duration']<=0:raise ValueError('Positive duration required in '+ident) if not isinstance(scene.get('text'),str) or not scene['text'].strip():raise ValueError('Exact narration text required in '+ident) if scene['layout']=='spaceship' and scene.get('example_index') not in range(9):raise ValueError('Spaceship example_index must be 0–8.') if scene['layout'] in ['steps','timeline'] and not 1<=len(scene.get('items',[]))<=4:raise ValueError('Split this timeline/step scene into groups of 1–4 items.') if scene['layout']=='points' and not 1<=len(scene.get('items',[]))<=5:raise ValueError('Split this points scene into groups of 1–5 items.') speech={**DEFAULT_SPEECH,**doc.get('speech',{})} work=ROOT/'work'/path.stem return path,doc,speech,work def settings(scene,speech): voice=speech['male_voice'] if scene['speaker']=='male' else speech['female_voice'] style=speech['style']+(' Adult male voice.' if scene['speaker']=='male' else ' Adult female voice.') return {'model':speech['model'],'voice':voice,'style':style,'text':scene['text']} def fingerprint(scene,speech): return hashlib.sha256(json.dumps(settings(scene,speech),ensure_ascii=False,sort_keys=True).encode()).hexdigest() def valid_clip(scene,speech,work): file=work/'audio/voice'/(scene['id']+'.wav');meta=work/'audio/cache.json' if not file.exists() or not meta.exists():return False try:return json.loads(meta.read_text()).get(scene['id'],{}).get('fingerprint')==fingerprint(scene,speech) except (ValueError,OSError):return False ===== END FILE ===== ===== FILE: davinci-videos/scripts/render_video.py ===== """Da Vinci 4744 motion picture. Python/Pillow/NumPy graphics and original audio.""" from pathlib import Path from functools import lru_cache import argparse, json, math, re, subprocess, wave, hashlib import numpy as np from PIL import Image, ImageDraw, ImageFont, ImageFilter from bidi.algorithm import get_display import imageio_ffmpeg from plan_io import ROOT,load_plan,valid_clip PARSER=argparse.ArgumentParser() PARSER.add_argument('--plan',default='examples/spaceship.json') PARSER.add_argument('--final',action='store_true') PARSER.add_argument('--preview',action='store_true') PARSER.add_argument('--force',action='store_true') PARSER.add_argument('--width',type=int,default=1920,choices=[640,960,1280,1920]) PARSER.add_argument('--fps',type=int,default=30,choices=[24,30]) OPTIONS,_=PARSER.parse_known_args() PLAN_PATH,PLAN,SPEECH,WORK=load_plan(OPTIONS.plan) ASSETS=ROOT/'assets';BUILD=WORK/'build';OUTPUT=WORK/'output' for path in [BUILD,OUTPUT]:path.mkdir(parents=True,exist_ok=True) FFMPEG=imageio_ffmpeg.get_ffmpeg_exe() FPS=OPTIONS.fps W,H=OPTIONS.width,round(OPTIONS.width*9/16) S=W/1280 GOLD=(255,219,162) WHITE=(248,251,255) BLUE=(105,190,237) GREEN=(105,235,174) RED=(255,115,110) NAVY=(9,28,51) SR=48000 MANIFEST=PLAN['scenes'] def clamp(x,a=0,b=1):return max(a,min(b,x)) def ease(x):x=clamp(x);return 1-(1-x)**3 def smooth(x):x=clamp(x);return x*x*(3-2*x) def P(x):return round(x*S) def rect(box):return tuple(P(x) for x in box) def draw_shape(im,kind,coords,**kwargs): transparent=any(isinstance(v,tuple) and len(v)==4 and v[3]<255 for v in kwargs.values()) if not transparent: getattr(ImageDraw.Draw(im),kind)(coords,**kwargs);return points=coords if kind in ['line','polygon'] else [(coords[0],coords[1]),(coords[2],coords[3])] pad=kwargs.get('width',1)+2 left=max(0,math.floor(min(p[0] for p in points)-pad));top=max(0,math.floor(min(p[1] for p in points)-pad)) right=min(im.width,math.ceil(max(p[0] for p in points)+pad));bottom=min(im.height,math.ceil(max(p[1] for p in points)+pad)) if right<=left or bottom<=top:return shifted=[(x-left,y-top) for x,y in coords] if kind in ['line','polygon'] else (coords[0]-left,coords[1]-top,coords[2]-left,coords[3]-top) layer=Image.new('RGBA',(right-left,bottom-top)) getattr(ImageDraw.Draw(layer),kind)(shifted,**kwargs) im.alpha_composite(layer,(left,top)) def line(im,pts,fill=BLUE,width=1):draw_shape(im,'line',[(P(x),P(y)) for x,y in pts],fill=fill,width=max(1,P(width))) def ellipse(im,box,fill=None,outline=None,width=1):draw_shape(im,'ellipse',rect(box),fill=fill,outline=outline,width=max(1,P(width))) def poly(im,pts,fill,outline=None,width=1):draw_shape(im,'polygon',[(P(x),P(y)) for x,y in pts],fill=fill,outline=outline,width=max(1,P(width))) @lru_cache(maxsize=128) def font(size,bold=False):return ImageFont.truetype(str(ASSETS/('Rubik-Bold.ttf' if bold else 'Rubik-Regular.ttf')),P(size)) def text(im,txt,x,y,size=32,color=WHITE,bold=False,anchor='mm',alpha=255): if not txt or alpha<=0:return layer=Image.new('RGBA',im.size) if alpha<255 else im d=ImageDraw.Draw(layer) for i,part in enumerate(txt.split('\n')): d.text((P(x),P(y+i*size*1.22)),get_display(part),font=font(size,bold),fill=(*color,alpha),anchor=anchor,stroke_width=0) if alpha<255:im.alpha_composite(layer) def glow(im,x,y,r,color=GOLD,alpha=80): rr=P(r); tile=Image.new('RGBA',(rr*4,rr*4)) ImageDraw.Draw(tile).ellipse((rr,rr,rr*3,rr*3),fill=(*color,alpha)) tile=tile.filter(ImageFilter.GaussianBlur(rr*.6)) im.alpha_composite(tile,(P(x)-rr*2,P(y)-rr*2)) def paste(im,asset,x,y,w,h=None,angle=0,opacity=255): hh=h if h else w*asset.height/asset.width pic=asset.resize((max(1,P(w)),max(1,P(hh))),Image.Resampling.LANCZOS) if opacity<255:pic.putalpha(pic.getchannel('A').point(lambda a:round(a*opacity/255))) if angle:pic=pic.rotate(angle,resample=Image.Resampling.BICUBIC,expand=True) im.alpha_composite(pic,(P(x)-pic.width//2,P(y)-pic.height//2)) LOGO=Image.open(ASSETS/'logo.png').convert('RGBA') WORDMARK=Image.open(ASSETS/'wordmark.png').convert('RGBA') BACKGROUNDS={} for key,file in [('hero','space-hero.png'),('content','space-content.png')]: im=Image.open(ASSETS/file).convert('RGBA').resize((W,H),Image.Resampling.LANCZOS) im.alpha_composite(Image.new('RGBA',(W,H),(4,20,38,65 if key=='content' else 35))) BACKGROUNDS[key]=im def rocket_asset(): # Faithful reference-style transparent sprite, prepared with built-in ImageGen. asset=Image.open(ASSETS/'rocket-reference-style.png').convert('RGBA') box=asset.getchannel('A').getbbox() if not box:raise RuntimeError('Rocket asset has no visible pixels.') asset=asset.crop(box) scale=min(570/asset.width,1160/asset.height) asset=asset.resize((round(asset.width*scale),round(asset.height*scale)),Image.Resampling.LANCZOS) canvas=Image.new('RGBA',(600,1200)) canvas.alpha_composite(asset,((600-asset.width)//2,1200-20-asset.height)) return canvas ROCKET=rocket_asset() def parachute(im,x,apex,width=155,deploy=1): if deploy<=0:return rx=width/2*deploy;ry=rx*.57 y=apex-118*deploy for q in np.linspace(-1,1,7):line(im,[(x+rx*q,y),(x,apex)],(*WHITE,195),1) ImageDraw.Draw(im).pieslice(rect((x-rx,y-ry,x+rx,y+ry)),180,360,fill=(43,108,154,255),outline=(*GOLD,255),width=P(2)) line(im,[(x-rx,y),(x+rx,y)],GOLD,2) for q in [-.5,0,.5]: draw_shape(im,'arc',rect((x-rx*(1-abs(q)*.25),y-ry,x+rx*(1-abs(q)*.25),y+ry)),start=185,end=355,fill=(*GOLD,150),width=P(1)) def rocket(im,x,bottom,h=300,tilt=0,chute=0,fire=0,t=0): if chute:parachute(im,x,bottom-h*.97,155*h/300,chute) if fire: fy=bottom-h*.03;fh=fire*(46+12*math.sin(t*32));fw=12*h/300 glow(im,x,fy+fh/2,30,GOLD,75) poly(im,[(x-fw,fy),(x,fy+fh),(x+fw,fy)],(*GOLD,230)) poly(im,[(x-fw*.45,fy),(x,fy+fh*.65),(x+fw*.45,fy)],(255,250,224,255)) paste(im,ROCKET,x,bottom-h/2,h*.5,h,tilt) def egg(im,x,y,r=45,crack=0): pts=[] for a in np.linspace(0,2*math.pi,80): yy=math.cos(a);xx=math.sin(a)*(.80+.18*(1+yy)/2) pts.append((x+xx*r*.72,y+yy*r)) poly(im,pts,(249,235,201,255)) ellipse(im,(x-r*.29,y-r*.66,x-r*.02,y-r*.15),(255,250,230,180)) if crack: line(im,[(x,y-r*.75),(x-r*.12,y-r*.26),(x+r*.17,y-r*.05),(x-r*.17,y+r*.24),(x+r*.07,y+r*.74)],(105,73,64,255),2) if crack>1: for i in range(7): a=i*2.4;dx=math.cos(a)*r*.8*crack;dy=abs(math.sin(a))*r*.33*crack poly(im,[(x+dx,y+dy),(x+dx+6,y+dy-9),(x+dx+12,y+dy+3)],(245,225,190,235)) STARS=np.random.default_rng(4744).uniform(0,1,(62,4)) CHAPTERS=['אתגר החללית','המשימה','ציוד המשימה','שעון המשימה','מגבלת הגובה','בדיקת הפגיעה','ניקוד חלקי','נחיתה מוצלחת','מתכנון לביצוע'] def background(index,t,absolute,duration): item=MANIFEST[index] kind='hero' if item.get('layout') in ['title','question'] or (item.get('layout')=='spaceship' and item['example_index'] in [0,7,8]) else 'content' im=BACKGROUNDS[kind].copy() for x,y,depth,phase in STARS: px=(x*1280+math.sin(absolute*.2+phase*6)*depth*10)%1280 py=(y*660+absolute*(1+depth*5))%660 alpha=int(35+60*(.5+.5*math.sin(absolute*1.5+phase*8))) rr=.7+depth ellipse(im,(px-rr,py-rr,px+rr,py+rr),(*GOLD,alpha)) paste(im,LOGO,88,58,53,53.4) paste(im,WORDMARK,231,58,205) chapter=item.get('chapter',CHAPTERS[item['example_index']] if item.get('layout')=='spaceship' else PLAN.get('title','')) text(im,chapter[:38],1212,58,23,GOLD,anchor='rm') line(im,[(64,106),(1216,106)],(*GOLD,85),1) # A quiet progress rail, with one pulse per chapter. line(im,[(64,699),(1216,699)],(110,153,183,95),2) done=(absolute/TOTAL)*1152 line(im,[(64,699),(64+done,699)],GOLD,2) for i,start in enumerate(STARTS): xx=64+start/TOTAL*1152 ellipse(im,(xx-2,697,xx+2,701),GOLD if i<=index else (95,132,162,255)) return im def title(im,txt,t,size=48): offset=15*(1-ease(t/.65)) text(im,txt,640,155+offset,size,WHITE,True,alpha=round(255*ease(t/.5))) def shadow(im,x,y,rx=75):ellipse(im,(x-rx,y-8,x+rx,y+8),(0,15,28,100)) def ring(im,x,y,r,t,color=GOLD): for j in range(3): start=(t*28+j*120)%360 draw_shape(im,'arc',rect((x-r,y-r,x+r,y+r)),start=start,end=start+65,fill=(*color,150),width=P(2)) def ruler(im,x,top,bottom,label): line(im,[(x,top),(x,bottom)],GOLD,2) for j in range(11): yy=top+(bottom-top)*j/10 line(im,[(x-10 if j in [0,10] else x-5,yy),(x+10 if j in [0,10] else x+5,yy)],GOLD,2 if j in [0,10] else 1) text(im,label,x-24,(top+bottom)/2,26,GOLD,True,'rm') def burst(im,x,y,t,color=GOLD,count=30): if t<0:return for i in range(count): a=i*2.39996; speed=30+(i*47)%170 xx=x+math.cos(a)*speed*t; yy=y+math.sin(a)*speed*t+70*t*t opacity=round(255*clamp(1-t/2.4)) if opacity: ellipse(im,(xx-2,yy-2,xx+2,yy+2),(*color,opacity)) def spaceship_scene(index,t,absolute,duration,actual_index): im=background(actual_index,t,absolute,duration) # Scene animation follows the nominal chapter pace even if a voice requires extra time. q=t/duration*[7,11,10,12,9,7,8,12,9][index] if index==0: glow(im,350,390,110,BLUE,35) bottom=880-290*ease(q/2.1)+5*math.sin(q*2) rocket(im,340,bottom,360,tilt=-3*math.sin(q),fire=1,t=q) text(im,'אתגר החללית',925,240,72,WHITE,True) text(im,'אתגר בתכנון הנדסי',925,332,35,GOLD,True) if q>3.6: n=max(1,3-int(q-3.6));ring(im,925,479,64,q) text(im,str(n),925,479,78,GOLD,True) text(im,'תדריך המשימה שלכם',925,588,27,WHITE) elif index==1: title(im,'מתכננים פתרון לבלימת פגיעה',t) ruler(im,145,232,594,'5 מטרים') fall=ease((q-1.1)/7) bottom=315+275*fall shadow(im,365,602,40+40*fall) rocket(im,365,bottom,190,chute=smooth((q-1.3)/1.1),t=q) glow(im,920,345,95,GOLD,38);ring(im,920,345,96,q) egg(im,920,345,64) text(im,'להגן על הביצה',920,488,42,WHITE,True) text(im,'ולספוג את הפגיעה',920,541,32,GOLD,True) elif index==2: title(im,'מה מקבלים למשימה?',t) for j,(xx,lab) in enumerate([(1080,'חלקי חללית'),(790,'חומרים'),(500,'מצנח'),(210,'כלים')]): z=ease((q-.25-j*.35)/.8) yy=350+(1-z)*55+3*math.sin(q*1.7+j) if z<=0:continue ring(im,xx,yy,100,q*.4+j,BLUE) if j==0: # Separate the removable nose from the cylindrical body and fins. cap=ROCKET.crop((0,0,ROCKET.width,328)) body=ROCKET.crop((0,328,ROCKET.width,ROCKET.height)) gap=1+0.04*math.sin(q*1.7) paste(im,cap,xx,yy-76*gap,90,angle=2*math.sin(q)) paste(im,body,xx,yy+37*gap,90) elif j==1: poly(im,[(xx-48,yy-55),(xx+48,yy-55),(xx+60,yy+63),(xx-60,yy+63)],(40,91,130,255),GOLD,2) line(im,[(xx-36,yy-53),(xx-23,yy-80),(xx+23,yy-80),(xx+36,yy-53)],GOLD,3) text(im,'?',xx,yy+5,62,GOLD,True) elif j==2:parachute(im,xx,yy+75,160) else: line(im,[(xx-40,yy-30),(xx+48,yy+64)],WHITE,8) line(im,[(xx+40,yy-30),(xx-48,yy+64)],BLUE,8) ellipse(im,(xx-65,yy-75,xx-15,yy-25),None,GOLD,7) ellipse(im,(xx+15,yy-75,xx+65,yy-25),None,GOLD,7) text(im,lab,xx,495,30,WHITE,True) text(im,'משתמשים רק בחומרים שקיבלתם',640,591,34,GOLD,True) elif index==3: title(im,'60 דקות. משימה אחת.',t,54) line(im,[(280,354),(1000,354)],(*BLUE,120),2) for j,(xx,num,label,c) in enumerate([(1000,'10','תכנון בלבד',GOLD),(640,'35','בנייה והרכבה',BLUE),(280,'15','בדיקה והגשה',GREEN)]): z=ease((q-.6-j*1.7)/1.1) ellipse(im,(xx-90,264,xx+90,444),(*NAVY,230),(*c,100),2) ImageDraw.Draw(im).arc(rect((xx-90,264,xx+90,444)),-90,-90+max(1,360*z),fill=c,width=P(5)) text(im,num,xx,350,76,c,True,alpha=round(255*z)) text(im,'דקות',xx,400,24,WHITE,alpha=round(255*z)) text(im,label,xx,502,30,c,True,alpha=round(255*z)) text(im,'קודם מתכננים. אחר כך בונים.',640,592,29,WHITE) elif index==4: title(im,'בודקים לפני שמגישים',t) shadow(im,435,599,95);rocket(im,435,600,350) ruler(im,615,267,600,'25 ס״מ') yy=205+62*ease(q/2) line(im,[(330,yy),(670,yy)],(*GOLD,180),2) glow(im,955,360,85,GOLD,24) text(im,'עד 25 ס״מ',958,350,70,GOLD,True) text(im,'גובה החללית במצב עומד',958,458,31,WHITE,True) text(im,'הגודל הוא חלק מהאתגר',958,519,26,BLUE) elif index==5: title(im,'כשהחללית והביצה נשברות',t) f=clamp((q-.6)/2.2);bottom=210+385*f*f shadow(im,355,603,50+35*f) if q<2.85:rocket(im,355,bottom,265,tilt=8*math.sin(q*2)) else: dt=q-2.85 # Separate existing rendered rocket artwork into two visibly broken pieces. top=ROCKET.crop((0,0,ROCKET.width,round(ROCKET.height*.48))) low=ROCKET.crop((0,round(ROCKET.height*.48),ROCKET.width,ROCKET.height)) z=ease(dt/.8) paste(im,top,350-75*z,461+84*z,133,128,24*z) paste(im,low,355+55*z,545+38*z,133,138,-36*z) burst(im,355,571,dt,RED,35);egg(im,300,597,23,2) for rr in [1,1.4]: r=90*clamp(dt*rr) ellipse(im,(355-r,586-r*.18,355+r,586+r*.18),None,(*RED,round(200*clamp(1-dt/2))),2) text(im,'0',947,337,138,RED,True) text(im,'נקודות',947,452,35,WHITE,True) text(im,'מתכננים לבלימת הפגיעה',947,540,28,WHITE) elif index==6: title(im,'החללית שרדה. הביצה נשברה.',t,46) f=smooth((q-.5)/4.1);bottom=300+295*f shadow(im,355,602,85) rocket(im,355,bottom,255,tilt=12*smooth((q-4.4)/.7),chute=1-clamp((q-4.5)/1.4),t=q) if q>4.6:egg(im,476,584,35,1.4) text(im,'5',947,337,138,(255,186,111),True) text(im,'נקודות',947,452,35,WHITE,True) text(im,'חללית שלמה וביצה פגועה',947,540,28,WHITE) elif index==7: title(im,'נחיתה בעמידה. ביצה שלמה.',t,48) f=smooth((q-.4)/5.0);bottom=287+304*f bounce=7*math.exp(-max(0,q-5.4)*4)*math.sin(max(0,q-5.4)*10) if q>5.4 else 0 shadow(im,350,601,80) for k in range(3):ellipse(im,(245-k*13,583-k*4,455+k*13,615+k*4),None,(*GOLD,70+40*k),1) rocket(im,350,bottom-bounce,255,chute=1-clamp((q-6.0)/1.7),t=q) if q>5.7:egg(im,505,552,33) text(im,'+20',1005,302,68,GOLD,True,'rm') text(im,'נחיתה בעמידה',1210,302,27,WHITE,True,'rm') text(im,'+20',1005,398,68,GREEN,True,'rm',alpha=round(255*ease((q-4.0)/1))) text(im,'הביצה שלמה',1210,398,27,WHITE,True,'rm',alpha=round(255*ease((q-4.0)/1))) if q>7.7: count=round(40*ease((q-7.7)/1.25)) text(im,str(count),965,530,90,GOLD,True) text(im,'עד 40 נקודות',965,595,26,WHITE,True) for i in range(34): a=i*2.39996;dt=q-7.7 xx=640+math.cos(a)*(65+i*4)*dt;yy=170+math.sin(a)*(70+i*2)*dt+50*dt*dt if 0P(width): lines.append(current);current=word else:current=trial if current:lines.append(current) return lines or [''] def copy_block(im,value,x,y,width,size=34,bold=False,color=WHITE,anchor='rm',max_lines=3,alpha=255): while size>=22: lines=wrap_words(value,size,width,bold) if len(lines)<=max_lines:break size-=1 if len(lines)>max_lines:raise ValueError('Split this scene: text does not fit: '+str(value)[:50]) text(im,'\n'.join(lines),x,y,size,color,bold,anchor,alpha) return len(lines)*size*1.22 @lru_cache(maxsize=24) def image_asset(path):return Image.open(path).convert('RGBA') def scene(index,t,absolute,duration): item=MANIFEST[index];layout=item['layout'] if layout=='spaceship':return spaceship_scene(item['example_index'],t,absolute,duration,index) im=background(index,t,absolute,duration) reveal=round(255*ease(t/.65));offset=20*(1-ease(t/.65)) if layout in ['title','question']: size=70 if layout=='title' else 78 copy_block(im,item.get('heading',PLAN['title']),640,250+offset,1100,size,True,anchor='mm',max_lines=2,alpha=reveal) copy_block(im,item.get('subtitle',''),640,475,1020,33,color=GOLD,anchor='mm',max_lines=3,alpha=reveal) else: copy_block(im,item.get('heading',''),640,156+offset,1120,48,True,anchor='mm',max_lines=2,alpha=reveal) if layout=='points': items=item['items'] for size in range(36,25,-1): groups=[wrap_words(v,size,1000) for v in items] if sum(len(g)*size*1.22+24 for g in groups)<=350:break else:raise ValueError('Split this points scene into shorter scenes.') yy=265 for j,group in enumerate(groups): z=ease((t-.5-j*.5)/.6) ellipse(im,(1173,yy-4,1181,yy+4),(*GOLD,round(255*z))) text(im,'\n'.join(group),1146,yy+12*(1-z),size,WHITE,anchor='rm',alpha=round(255*z)) yy+=len(group)*size*1.22+24 elif layout in ['steps','timeline']: items=item['items'];centers=np.linspace(1100,180,len(items)) if len(items)>1 else [640] if len(items)>1: line(im,[(180,350),(1100,350)],(*GOLD,150),2) poly(im,[(160,350),(179,340),(179,360)],GOLD) for j,(xx,label) in enumerate(zip(centers,items)): z=ease((t-.4-j*.65)/.7) ellipse(im,(xx-53,297,xx+53,403),(*NAVY,230),(*GOLD,round(255*z)),2) text(im,str(j+1).zfill(2),xx,350,50,GOLD,True,alpha=round(255*z)) copy_block(im,label,xx,463,250,28,True,anchor='mm',max_lines=3,alpha=round(255*z)) elif layout=='metric': text(im,str(item.get('value','')),640,350,132,GOLD,True,alpha=reveal) copy_block(im,item.get('label',''),640,487,1100,36,True,anchor='mm',max_lines=2,alpha=reveal) copy_block(im,item.get('subtitle',''),640,572,1100,26,color=BLUE,anchor='mm',max_lines=2,alpha=reveal) elif layout=='image': file=(PLAN_PATH.parent/item['image']).resolve() if not file.is_relative_to(ROOT):raise ValueError('Copy the supplied image inside the skill folder.') pic=image_asset(str(file));ratio=min(500/pic.width,355/pic.height) paste(im,pic,330,412,pic.width*ratio,pic.height*ratio) yy=275 for j,label in enumerate(item.get('items',[])[:4]): z=ease((t-.5-j*.55)/.6) yy+=copy_block(im,label,1200,yy,550,30,max_lines=3,alpha=round(255*z))+24 return im def caption_chunks(txt,limit=57): chunks=[];cur='' for word in txt.split(): if len(cur)+len(word)+1>limit and cur:chunks.append(cur);cur='' cur=(cur+' '+word).strip() if word.endswith(('.','?','!')) and len(cur)>25:chunks.append(cur);cur='' if cur:chunks.append(cur) return chunks def add_caption(im,index,t,duration): chunks=caption_chunks(MANIFEST[index]['text']) weights=np.array([len(x) for x in chunks]);bounds=np.cumsum(weights)/weights.sum() spoken=len(VOICES[index])/SR if index in VOICES else duration-1.1 progress=clamp((t-.35)/max(.1,spoken)) k=min(len(chunks)-1,int(np.searchsorted(bounds,progress))) if t<.3 or t>min(duration-.45,spoken+.65):return label=chunks[k] overlay=Image.new('RGBA',im.size) ImageDraw.Draw(overlay).rounded_rectangle(rect((126,639,1154,688)),radius=P(8),fill=(3,18,35,220)) im.alpha_composite(overlay) text(im,label,640,663,25,WHITE) def read_voice(p): out=subprocess.run([FFMPEG,'-v','error','-i',str(p),'-f','f32le','-ar',str(SR),'-ac','2','pipe:1'],capture_output=True,check=True).stdout return np.frombuffer(out,dtype='.83:data*=.83/peak at=round((STARTS[i]+.35)*SR) end=min(n,at+len(data));voice_mix[at:end]+=data[:end-at] a=max(0,at-round(.15*SR));b=min(n,end+round(.15*SR)) duck[a:b]=np.minimum(duck[a:b],.34) ramp=min(round(.15*SR),(b-a)//2) if ramp:duck[a:a+ramp]=np.linspace(1,.34,ramp);duck[b-ramp:b]=np.linspace(.34,1,ramp) mix=music*duck[:,None]+sfx*.8+voice_mix mix*=np.minimum(1,np.arange(n)/SR/.25)[:,None] mix*=np.minimum(1,np.maximum(0,(n-np.arange(n))/SR/.65))[:,None] peak=float(np.max(np.abs(mix))) if peak>.94:mix*=.94/peak path=BUILD/'soundtrack.wav' with wave.open(str(path),'wb') as wf: wf.setnchannels(2);wf.setsampwidth(2);wf.setframerate(SR);wf.writeframes((mix*32767).astype(' {fmt(end)}\n{chunk}\n');cursor=end (OUTPUT/(PLAN_PATH.stem+'.he.srt')).write_text('\n'.join(lines),encoding='utf-8') def main(): args=PARSER.parse_args() final=args.final if final and len(VOICES)!=len(MANIFEST):raise RuntimeError('Missing Gemini narration; refusing to label an unvoiced movie as final.') master=BUILD/'picture-master.mp4';timeline=BUILD/'picture-timeline.json' expected={'durations':DURS,'fps':FPS,'width':W,'height':H,'plan':hashlib.sha256(PLAN_PATH.read_bytes()).hexdigest(),'renderer':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),'assets':{p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in ASSETS.iterdir() if p.is_file()}} reuse=master.exists() and timeline.exists() and json.loads(timeline.read_text())==expected and not args.force if not reuse: cmd=[FFMPEG,'-hide_banner','-loglevel','error','-y','-f','rawvideo','-pixel_format','rgb24','-video_size',f'{W}x{H}','-framerate',str(FPS),'-i','pipe:0','-an','-c:v','libx264','-preset','veryfast','-crf','18','-pix_fmt','yuv420p','-movflags','+faststart',str(master)] proc=subprocess.Popen(cmd,stdin=subprocess.PIPE) cuts=[scene(i,DURS[i]-.01,STARTS[i]+DURS[i]-.01,DURS[i]) for i in range(len(MANIFEST))] total_frames=round(TOTAL*FPS) for f in range(total_frames): absolute=f/FPS;i=max(j for j,start in enumerate(STARTS) if start<=absolute);local=absolute-STARTS[i] im=scene(i,local,absolute,DURS[i]) if i and local<.35: im=Image.blend(cuts[i-1],im,smooth(local/.35)) if .08=11 numpy>=2 imageio-ffmpeg>=0.6 python-bidi>=0.6 ===== END FILE =====