← back to Filibuster Marathon Cartoon
anim: file-relative paths, bounded retrying Replicate poll, status check before download; ignore render binaries
c50e6cd0cdf336448366295f08aeb75c388c4c46 · 2026-09-24 15:01:03 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LfkajczsrHUsBBKvhRwcCG
Files touched
M .gitignoreA anim/animate.pyA anim/animate_wan.pyA anim/edit.py
Diff
commit c50e6cd0cdf336448366295f08aeb75c388c4c46
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Sep 24 15:01:03 2026 -0700
anim: file-relative paths, bounded retrying Replicate poll, status check before download; ignore render binaries
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LfkajczsrHUsBBKvhRwcCG
---
.gitignore | 4 ++++
anim/animate.py | 32 ++++++++++++++++++++++++++++++++
anim/animate_wan.py | 30 ++++++++++++++++++++++++++++++
anim/edit.py | 31 +++++++++++++++++++++++++++++++
4 files changed, 97 insertions(+)
diff --git a/.gitignore b/.gitignore
index 1924158..9e31127 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,7 @@ tmp/
dist/
build/
.next/
+
+# anim/ render outputs — regenerable, large binaries
+anim/*.mp4
+anim/*.jpg
diff --git a/anim/animate.py b/anim/animate.py
new file mode 100644
index 0000000..7fb791b
--- /dev/null
+++ b/anim/animate.py
@@ -0,0 +1,32 @@
+import base64,json,os,time,urllib.request
+tok=[l.split('=',1)[1].strip().strip('"\'') for l in open(os.path.expanduser('~/Projects/secrets-manager/.env')) if l.startswith('REPLICATE_API_TOKEN=')][0]
+def api(url,data=None):
+ r=urllib.request.Request(url,data=json.dumps(data).encode() if data else None,headers={"Authorization":f"Bearer {tok}","Content-Type":"application/json"})
+ return json.load(urllib.request.urlopen(r,timeout=300))
+HERE=os.path.dirname(os.path.abspath(__file__)) # paths resolve from this file, not the cwd
+def P(name): return os.path.join(HERE,name)
+def poll(pr,interval,deadline_s):
+ # Replicate bills at create time: never lose the id, retry transient poll errors, bound the wait.
+ end=time.time()+deadline_s
+ while pr["status"] not in("succeeded","failed","canceled"):
+ if time.time()>end: raise SystemExit(f"TIMEOUT after {deadline_s}s — recover later: GET {pr['urls']['get']}")
+ time.sleep(interval)
+ for a in range(5):
+ try: pr=api(pr["urls"]["get"]); break
+ except Exception as e:
+ print(f"poll error (attempt {a+1}/5): {e}"); time.sleep(interval*(a+1))
+ else: raise SystemExit(f"poll failed 5x — recover later: GET {pr['urls']['get']}")
+ return pr
+img="data:image/jpeg;base64,"+base64.b64encode(open(P('edited.jpg'),'rb').read()).decode()
+p=("Animate this black-and-white pen-and-ink editorial cartoon, keep the hand-drawn ink style throughout, static camera. "
+ "The deadpan politician keeps reading aloud from the huge telephone directory, lips moving in a monotone, expression never changing; he licks a finger and turns a page. "
+ "Loose pages tear free and flutter slowly through the air. The melted wall clock keeps oozing, its drips sliding slowly down the wall into the spreading puddle on the floor. "
+ "The legislators in the background slowly nod off, heads drooping onto their chests, one yawns. Slow, dry, absurd, deadpan comedy timing.")
+neg="color, photorealistic, 3d render, camera shake, zoom, morphing faces, text changes"
+pr=api("https://api.replicate.com/v1/models/kwaivgi/kling-v2.5-turbo-pro/predictions",{"input":{"prompt":p,"negative_prompt":neg,"start_image":img,"duration":10}})
+print("id",pr["id"],pr["status"])
+pr=poll(pr,10,1800)
+print(pr["status"],pr.get("error"),pr.get("metrics"))
+if pr["status"]!="succeeded": raise SystemExit(1)
+out=pr["output"]; out=out[0] if isinstance(out,list) else out
+urllib.request.urlretrieve(out,P('raw.mp4')); print('saved')
diff --git a/anim/animate_wan.py b/anim/animate_wan.py
new file mode 100644
index 0000000..95616a0
--- /dev/null
+++ b/anim/animate_wan.py
@@ -0,0 +1,30 @@
+import base64,json,os,time,urllib.request
+tok=[l.split('=',1)[1].strip().strip('"\'') for l in open(os.path.expanduser('~/Projects/secrets-manager/.env')) if l.startswith('REPLICATE_API_TOKEN=')][0]
+def api(url,data=None):
+ r=urllib.request.Request(url,data=json.dumps(data).encode() if data else None,headers={"Authorization":f"Bearer {tok}","Content-Type":"application/json"})
+ return json.load(urllib.request.urlopen(r,timeout=300))
+HERE=os.path.dirname(os.path.abspath(__file__)) # paths resolve from this file, not the cwd
+def P(name): return os.path.join(HERE,name)
+def poll(pr,interval,deadline_s):
+ # Replicate bills at create time: never lose the id, retry transient poll errors, bound the wait.
+ end=time.time()+deadline_s
+ while pr["status"] not in("succeeded","failed","canceled"):
+ if time.time()>end: raise SystemExit(f"TIMEOUT after {deadline_s}s — recover later: GET {pr['urls']['get']}")
+ time.sleep(interval)
+ for a in range(5):
+ try: pr=api(pr["urls"]["get"]); break
+ except Exception as e:
+ print(f"poll error (attempt {a+1}/5): {e}"); time.sleep(interval*(a+1))
+ else: raise SystemExit(f"poll failed 5x — recover later: GET {pr['urls']['get']}")
+ return pr
+img="data:image/jpeg;base64,"+base64.b64encode(open(P('edited.jpg'),'rb').read()).decode()
+p=("Animate this black-and-white pen-and-ink editorial cartoon, keep the hand-drawn ink style throughout, static camera. "
+ "The deadpan politician keeps reading aloud from the huge telephone directory, lips moving in a monotone, expression never changing; he licks a finger and turns a page. "
+ "Loose pages tear free and flutter slowly through the air. The melted wall clock keeps oozing, its drips sliding slowly down the wall into the spreading puddle on the floor. "
+ "The legislators in the background slowly nod off, heads drooping onto their chests, one yawns. Slow, dry, absurd, deadpan comedy timing.")
+pr=api("https://api.replicate.com/v1/models/wan-video/wan-2.2-i2v-fast/predictions",{"input":{"prompt":p,"image":img,"num_frames":121,"frames_per_second":12,"resolution":"720p","go_fast":True}})
+print("id",pr["id"],pr["status"])
+pr=poll(pr,10,1800)
+print(pr["status"],pr.get("error"),pr.get("metrics"))
+if pr["status"]=="succeeded": urllib.request.urlretrieve(pr["output"],P('raw_wan.mp4')); print('saved')
+else: raise SystemExit(1)
diff --git a/anim/edit.py b/anim/edit.py
new file mode 100644
index 0000000..6d5e118
--- /dev/null
+++ b/anim/edit.py
@@ -0,0 +1,31 @@
+import base64,json,os,time,urllib.request
+tok=[l.split('=',1)[1].strip().strip('"\'') for l in open(os.path.expanduser('~/Projects/secrets-manager/.env')) if l.startswith('REPLICATE_API_TOKEN=')][0]
+def api(url,data=None):
+ r=urllib.request.Request(url,data=json.dumps(data).encode() if data else None,headers={"Authorization":f"Bearer {tok}","Content-Type":"application/json","Prefer":"wait"})
+ return json.load(urllib.request.urlopen(r,timeout=300))
+HERE=os.path.dirname(os.path.abspath(__file__)) # paths resolve from this file, not the cwd
+def P(name): return os.path.join(HERE,name)
+def poll(pr,interval,deadline_s):
+ # Replicate bills at create time: never lose the id, retry transient poll errors, bound the wait.
+ end=time.time()+deadline_s
+ while pr["status"] not in("succeeded","failed","canceled"):
+ if time.time()>end: raise SystemExit(f"TIMEOUT after {deadline_s}s — recover later: GET {pr['urls']['get']}")
+ time.sleep(interval)
+ for a in range(5):
+ try: pr=api(pr["urls"]["get"]); break
+ except Exception as e:
+ print(f"poll error (attempt {a+1}/5): {e}"); time.sleep(interval*(a+1))
+ else: raise SystemExit(f"poll failed 5x — recover later: GET {pr['urls']['get']}")
+ return pr
+img="data:image/jpeg;base64,"+base64.b64encode(open(P('base.jpg'),'rb').read()).decode()
+p=("Keep this exact pen-and-ink editorial cartoon, same style, same delegate, same lectern, same telephone directory, same composition. "
+ "Edit only: 1) the melted wall clock's minute hand slides off its face and hangs down the wall like a limp noodle all the way to the floor, its tip resting in the puddle on the floor at bottom right; "
+ "2) remove the tangled shape lying in the puddle at bottom right, replace it with plain floor and puddle; "
+ "3) the rows of legislators in the background are asleep, heads slumped forward onto their chests. No text added, no signature.")
+pr=api("https://api.replicate.com/v1/models/black-forest-labs/flux-kontext-pro/predictions",{"input":{"prompt":p,"input_image":img,"aspect_ratio":"match_input_image","output_format":"jpg"}})
+print("id",pr["id"],pr["status"])
+pr=poll(pr,3,900)
+print(pr["status"],pr.get("error"))
+if pr["status"]!="succeeded": raise SystemExit(1)
+out=pr["output"]; out=out[0] if isinstance(out,list) else out
+urllib.request.urlretrieve(out,P('edited.jpg')); print('saved')
← b30dd51 Replace boxy PIL/SVG art with AI-inked Conrad-style illustra
·
back to Filibuster Marathon Cartoon
·
gitignore: __pycache__ ecb6531 →