Bandersnatch Maker
This is a program I wrote on a live stream on the 14th (and 15th) of June 2019. https://www.twitch.tv/videos/438871676
You'll need some things.
- The movie (complete, with all clips, should be 05:12:14). See https://mehotkhan.github.io/BandersnatchInteractive/ for info on that.
- Some data from BandersnatchInteractive.
- ffmpeg
You'll need to edit the two js files to remove bandersnatch= and SegmentMap= respectively so they parse as valid json. If you
encounter an error about utf-8 bom use dos2unix to fix it. Rename the files to bandersnatch.json and SegmentMap.json.
Open a python interpreter in the same directory as SegmentMap.js. I'd probably recommend 3.7. Paste this in:
import json
with open("SegmentMap.json") as f:
smap = json.load(f)
def msToTS(ms):
s,ms = divmod(ms,1000)
m,s = divmod(s,60)
h,m = divmod(m,60)
return "{:02d}:{:02d}:{:02d}.{:03d}".format(h,m,s,ms)
for segment in smap.values():
ss = ""
t = ""
# working around ffmpeg seek to previous keyframe
if segment["startTimeMs"] > 5000:
# 40ms subtracted to account for one frame difference in 25fps source video
ss = " -ss " + msToTS(segment["startTimeMs"]-4960)
if "endTimeMs" in segment:
t = " -t {}".format((segment["endTimeMs"]-segment["startTimeMs"])/1000)
print("""ffmpeg{} -i bandersnatch.mkv -ss 5{} {}.mkv 2>/dev/null""".format(ss, t, segment))
You'll get a bunch of lines to run to generate clips from the full movie. I'd suggest putting these lines into a bash script and leaving it to run for a while in screen or tmux. This will take a very long time dependent on your CPU.
While that's going, generate some movies!
$ python bandersnatch.py > concat.txt
picked 1A
out=['1E']
out=['1D']
poss=['1E', '1D']
picked 1D
out=['1H']
out=['1G']
poss=['1H', '1G']
picked 1G
...
out=['8JA']
out=['8JB1']
poss=['8JA', '8JB1']
picked 8JA
out=['0Cr4', '0cr3']
out=['0Cr4', '0cr3']
poss=['0Cr4', '0cr3']
picked 0Cr4 and added IDNT
01:04:02.610
The stderr output displays what choices were possible at each stage, which segment it picked, and the total length of the movie.
The output file concat.txt is produced in ffmpeg concat format, render it in the same directory as the segment files (e.g IDNT.mkv) with:
ffmpeg -f concat -i concat.txt -c copy `date +%Y%m%d-%H%M%S.mkv`
| 1 | from random import choice |
| 2 | import json |
| 3 | from sys import stderr |
| 4 | |
| 5 | with open("bandersnatch.json") as f: |
| 6 | bandersnatch = json.load(f) |
| 7 | |
| 8 | with open("/srv/http/bi/assets/segmentmap.json") as f: |
| 9 | smap = json.load(f) |
| 10 | |
| 11 | initial_state = { "p_sp": True, "p_tt": True, "p_8a": False, "p_td": True, "p_cs": False, "p_w1": False, "p_2b": False, "p_3j": False, "p_pt": False, "p_cd": False, "p_cj": False, "p_sj": False, "p_sj2": False, "p_tud": False, "p_lsd": False, "p_vh": False, "p_3l": False, "p_3s": False, "p_3z": False, "p_ps": "n", "p_wb": False, "p_kd": False, "p_bo": False, "p_5v": False, "p_pc": "n", "p_sc": False, "p_ty": False, "p_cm": False, "p_pr": False, "p_3ad": False, "p_s3af": False, "p_nf": False, "p_np": False, "p_ne": False, "p_pp": False, "p_tp": False, "p_bup": False, "p_be": False, "p_pe": False, "p_pae": False, "p_te": False, "p_snt": False, "p_8j": False, "p_8d": False, "p_8m": False, "p_8q": False, "p_8s": False, "p_8v": False, "p_vs": "n", "p_scs": False, "p_3ab": False, "p_3ac": False, "p_3aj": False, "p_3ah": False, "p_3ak": False, "p_3al": False, "p_3af": False, "p_5h": False, "p_5ac": False, "p_5ag": False, "p_5ad": False, "p_6c": False, "length": 0 } |
| 12 | state = dict(initial_state) |
| 13 | |
| 14 | moments = bandersnatch["videos"]["80988062"]["interactiveVideoMoments"]["value"]["momentsBySegment"] |
| 15 | preconditions = bandersnatch["videos"]["80988062"]["interactiveVideoMoments"]["value"]["preconditions"] |
| 16 | segmentGroups = bandersnatch["videos"]["80988062"]["interactiveVideoMoments"]["value"]["segmentGroups"] |
| 17 | |
| 18 | def msToTS(ms): |
| 19 | s,ms = divmod(ms,1000) |
| 20 | m,s = divmod(s,60) |
| 21 | h,m = divmod(m,60) |
| 22 | return "{:02d}:{:02d}:{:02d}.{:03d}".format(h,m,s,ms) |
| 23 | |
| 24 | def conditionHandler(cond): |
| 25 | global state |
| 26 | if not cond: |
| 27 | return True |
| 28 | if cond[0] == "persistentState": |
| 29 | return state[cond[1]] |
| 30 | if cond[0] == "not": |
| 31 | return not all(conditionHandler(c) for c in cond[1:]) |
| 32 | if cond[0] == "and": |
| 33 | return all(conditionHandler(c) for c in cond[1:]) |
| 34 | if cond[0] == "eql": |
| 35 | return conditionHandler(cond[1]) == cond[2] |
| 36 | if cond[0] == "or": |
| 37 | return any(conditionHandler(c) for c in cond[1:]) |
| 38 | |
| 39 | def groupHandler(group, segment=None): |
| 40 | out = [] |
| 41 | if segment: |
| 42 | group.append(segment) |
| 43 | for item in group: |
| 44 | if type(item) is str and conditionHandler( preconditions.get(item,[]) ): |
| 45 | out.append(item) |
| 46 | if type(item) is dict: |
| 47 | if "segmentGroup" in item: |
| 48 | out += groupHandler(segmentGroups[item["segmentGroup"]]) |
| 49 | if "precondition" in item: |
| 50 | if conditionHandler( preconditions.get(item["precondition"],[]) ): |
| 51 | out.append(item["segment"]) |
| 52 | print("out="+repr(out),file=stderr) |
| 53 | return out |
| 54 | |
| 55 | |
| 56 | def followTheStory(segment): |
| 57 | global state |
| 58 | global history |
| 59 | possibilities = [] |
| 60 | if segment in moments: |
| 61 | m = moments[segment] |
| 62 | for moment in m: |
| 63 | if moment["type"] == "notification:playbackImpression": |
| 64 | state.update( moment.get("impressionData",{}).get("data", {}).get("persistent", {}) ) |
| 65 | if moment["type"] == "scene:cs_bs": |
| 66 | for option in moment["choices"]: |
| 67 | state.update( option.get("impressionData",{}).get("data", {}).get("persistent", {}) ) |
| 68 | if "segmentId" in option: |
| 69 | p = groupHandler([option["segmentId"]]) |
| 70 | elif "sg" in option: |
| 71 | p = groupHandler(segmentGroups[option["sg"]]) |
| 72 | elif moment["trackingInfo"]["optionType"] == "fakeOption": |
| 73 | continue |
| 74 | else: |
| 75 | raise Exception(option["id"]) |
| 76 | possibilities += p |
| 77 | if moment["type"] == "notification:action": |
| 78 | possibilities.append(segment) |
| 79 | if segment in segmentGroups: |
| 80 | possibilities += groupHandler(segmentGroups[segment]) |
| 81 | print("poss="+repr(possibilities),file=stderr) |
| 82 | if not possibilities: |
| 83 | # raise Exception("hoi") |
| 84 | possibilities += groupHandler(segmentGroups["respawnOptions"]) |
| 85 | return choice(possibilities) |
| 86 | |
| 87 | def bandersnatch(): |
| 88 | global state |
| 89 | state = dict(initial_state) |
| 90 | current_segment = "1A" |
| 91 | while True: |
| 92 | state["length"] += smap[current_segment]["endTimeMs"] - smap[current_segment]["startTimeMs"] |
| 93 | if current_segment[:3].lower() == "0cr": |
| 94 | print("file '{}.mkv'".format(current_segment)) |
| 95 | print("file 'IDNT.mkv'") |
| 96 | print("picked {} and added IDNT".format(current_segment), file=stderr) |
| 97 | state["length"] += 10 |
| 98 | break |
| 99 | print("file '{}.mkv'".format(current_segment)) |
| 100 | print("picked {}".format(current_segment), file=stderr) |
| 101 | current_segment = followTheStory(current_segment) |
| 102 | if current_segment is None: |
| 103 | break |
| 104 | return msToTS(state["length"]) |
| 105 | |
| 106 | if __name__ == "__main__": |
| 107 | print(bandersnatch(),file=stderr) |
| 108 |