euler314 commited on
Commit
304e00c
·
verified ·
1 Parent(s): 6b23c81

Bavi pressure forecast MP4 with README video and poster

Browse files
.gitattributes CHANGED
@@ -12,3 +12,6 @@ paper/trackformer_1_2_vs_1_1_route_examples.png filter=lfs diff=lfs merge=lfs -t
12
  paper/release_data/pressure_showcase.npz filter=lfs diff=lfs merge=lfs -text
13
  paper/trackformer.pdf filter=lfs diff=lfs merge=lfs -text
14
  paper/trackformer_1_2_interactive_pressure_preview.png filter=lfs diff=lfs merge=lfs -text
 
 
 
 
12
  paper/release_data/pressure_showcase.npz filter=lfs diff=lfs merge=lfs -text
13
  paper/trackformer.pdf filter=lfs diff=lfs merge=lfs -text
14
  paper/trackformer_1_2_interactive_pressure_preview.png filter=lfs diff=lfs merge=lfs -text
15
+ docs/trackformer_1_2_bavi.mp4 filter=lfs diff=lfs merge=lfs -text
16
+ paper/release_data/bavi_video.npz filter=lfs diff=lfs merge=lfs -text
17
+ paper/trackformer_1_2_bavi_video_poster.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -65,6 +65,18 @@ The 1.2 mean has better aggregate direction, shape and position scores on this p
65
 
66
  A new **1,473-case / 270-storm** benchmark is in progress; it is not the completed 270-case result above. Each storm contributes at most one forecast per UTC day, through +120 h. Daily errors are averaged within each storm, then the 270 storm scores receive equal weight. Selection was frozen before new inference, without filtering on forecast quality. See the [protocol](docs/daily_storm_benchmark.md) and [frozen cohort](paper/release_data/daily_storm_cohort.json). Results are pending; no improvement is claimed from the partial run.
67
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  ## Interactive forecast pressure and observed route
69
 
70
  **[Open the interactive 1.2 pressure map](https://yu314-coder.github.io/typhoon-predict/trackformer_1_2_pressure.html)** — play or scrub from +6 to +120 h, toggle forecast/observed routes, and hover or tap to inspect pressure and position. Click the preview below to open it. GitHub and Hugging Face Markdown cannot execute the interactive controls inside the README itself.
 
65
 
66
  A new **1,473-case / 270-storm** benchmark is in progress; it is not the completed 270-case result above. Each storm contributes at most one forecast per UTC day, through +120 h. Daily errors are averaged within each storm, then the 270 storm scores receive equal weight. Selection was frozen before new inference, without filtering on forecast quality. See the [protocol](docs/daily_storm_benchmark.md) and [frozen cohort](paper/release_data/daily_storm_cohort.json). Results are pending; no improvement is claimed from the partial run.
67
 
68
+ ## Bavi pressure forecast — MP4
69
+
70
+ <video controls playsinline preload="metadata" width="100%" src="https://huggingface.co/euler314/typhoon-predict/resolve/main/docs/trackformer_1_2_bavi.mp4" poster="https://huggingface.co/euler314/typhoon-predict/resolve/main/paper/trackformer_1_2_bavi_video_poster.png">
71
+ <a href="https://huggingface.co/euler314/typhoon-predict/resolve/main/docs/trackformer_1_2_bavi.mp4">Play the Bavi forecast MP4</a>
72
+ </video>
73
+
74
+ [![Play the Trackformer 1.2 Bavi pressure forecast MP4](paper/trackformer_1_2_bavi_video_poster.png)](https://yu314-coder.github.io/typhoon-predict/trackformer_1_2_bavi.mp4)
75
+
76
+ **[Play or download the Bavi MP4](https://yu314-coder.github.io/typhoon-predict/trackformer_1_2_bavi.mp4)** · 23 seconds · +6 to +120 h · **50-member mean**. This is a video, not an interactive map. Click the preview above to play it; inline video playback depends on the Markdown host.
77
+
78
+ The forecast starts **3 July 2026 at 06 UTC**. It shows actual saved model-generated pressure with 4 hPa isobars, the mean forecast track, the observed route, and a central-pressure timeline. Observed pressure is the local IBTrACS **USA** series, not JMA. The dotted map boundary marks the fixed regional reconstruction: after the centre leaves it, only the coarser basin field is available at that location. No detailed pressure core is invented outside coverage. This requested Bavi example is not evidence of typical skill. [Data, source hashes and video provenance](paper/release_data/bavi_video.json).
79
+
80
  ## Interactive forecast pressure and observed route
81
 
82
  **[Open the interactive 1.2 pressure map](https://yu314-coder.github.io/typhoon-predict/trackformer_1_2_pressure.html)** — play or scrub from +6 to +120 h, toggle forecast/observed routes, and hover or tap to inspect pressure and position. Click the preview below to open it. GitHub and Hugging Face Markdown cannot execute the interactive controls inside the README itself.
RELEASE_NOTES_TRACKFORMER_1_2.md CHANGED
@@ -10,6 +10,8 @@ The route gallery now shows six explicitly selected best-performing examples fro
10
 
11
  The README preview now links to an interactive PRAPIROON pressure/route showcase with playback, lead selection, hover details and observed-track overlays. It uses actual saved 50-member mean pressure fields and a disclosed selected-best rule. The main paper has been replaced with an architecture-only Trackformer 1.2 description; the 1.1 source is preserved separately.
12
 
 
 
13
  The Surigae illustration is a single forecast issued 2026-09-27 12 UTC, with 4 hPa isobars at +6/+24/+36 h. It is separate from the 50-member historical benchmark. Reproduction data and metric definitions are included. The model weights are unchanged in this documentation revision.
14
 
15
  This is not an operational warning service. Do not use for safety-critical decisions. A genuinely untouched storm-level holdout is still required before generalization claims.
 
10
 
11
  The README preview now links to an interactive PRAPIROON pressure/route showcase with playback, lead selection, hover details and observed-track overlays. It uses actual saved 50-member mean pressure fields and a disclosed selected-best rule. The main paper has been replaced with an architecture-only Trackformer 1.2 description; the 1.1 source is preserved separately.
12
 
13
+ A separate Bavi MP4 now shows the saved 50-member mean pressure and forecast route against observations through +120 h. The clickable README poster opens the video directly. Regional coverage, the coarse-field fallback and the observed pressure source are explicitly labelled; the paper is unchanged.
14
+
15
  The Surigae illustration is a single forecast issued 2026-09-27 12 UTC, with 4 hPa isobars at +6/+24/+36 h. It is separate from the 50-member historical benchmark. Reproduction data and metric definitions are included. The model weights are unchanged in this documentation revision.
16
 
17
  This is not an operational warning service. Do not use for safety-critical decisions. A genuinely untouched storm-level holdout is still required before generalization claims.
docs/trackformer_1_2_bavi.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:33ecd068f1eee144b10301d6261dfad673557b2b38050e159e39935cccc3872c
3
+ size 7501391
paper/release_data/bavi_coastlines.json ADDED
@@ -0,0 +1 @@
 
 
1
+ [[[126.376814,8.414706],[126.478513,7.750354],[126.537424,7.189381],[126.196773,6.274294],[125.831421,7.293715],[125.363852,6.786485],[125.683161,6.049657],[125.396512,5.581003],[124.219788,6.161355],[123.93872,6.885136],[124.243662,7.36061],[123.610212,7.833527],[123.296071,7.418876],[122.825506,7.457375],[122.085499,6.899424],[121.919928,7.192119],[122.312359,8.034962],[122.942398,8.316237],[123.487688,8.69301],[123.841154,8.240324],[124.60147,8.514158],[124.764612,8.960409],[125.471391,8.986997],[125.412118,9.760335],[126.222714,9.286074],[126.306637,8.782487],[126.376814,8.414706]],[[125.502552,12.162695],[125.783465,11.046122],[125.011884,11.311455],[125.032761,10.975816],[125.277449,10.358722],[124.801819,10.134679],[124.760168,10.837995],[124.459101,10.88993],[124.302522,11.495371],[124.891013,11.415583],[124.87799,11.79419],[124.266762,12.557761],[125.227116,12.535721],[125.502552,12.162695]]]
paper/release_data/bavi_video.json ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model": "Trackformer 1.2",
3
+ "storm": "BAVI",
4
+ "storm_id": "2026182N09163",
5
+ "issue_time_utc": "2026-07-03T06:00:00Z",
6
+ "members": 50,
7
+ "leads_hours": [
8
+ 6,
9
+ 12,
10
+ 18,
11
+ 24,
12
+ 30,
13
+ 36,
14
+ 42,
15
+ 48,
16
+ 54,
17
+ 60,
18
+ 66,
19
+ 72,
20
+ 78,
21
+ 84,
22
+ 90,
23
+ 96,
24
+ 102,
25
+ 108,
26
+ 114,
27
+ 120
28
+ ],
29
+ "source_archive_sha256": "96392e7c44e2050b2b67803262ff470b093cf93c0bcd061b59312dc0502bb860",
30
+ "inputs_sha256": "1ca0e8087e5efca02e0f72421fbe5f7aff747cd08172ab4d4c92f0acc8a0926e",
31
+ "observed_csv_sha256": "4348af2679b4be0d53ec630bccda759044663a1f57e2bbd5c1d19428aab562fc",
32
+ "data_sha256": "9251fb82ebde819eced999c3440aa9b0a24251c29a4c2f6a4670f4137b9c5aff",
33
+ "checkpoint_sha256": "f194a23d3f91ea76ad776dfad942fabd669eeae8b3fd665815463095367e9ee0",
34
+ "forecast_source": "Saved 50 smooth historical-input perturbation means from the released checkpoint; no new inference for this video.",
35
+ "route_projection_correction": "Undo the archived predicted-latitude longitude conversion and restore the issue-latitude projection used by ensemble inference. Original route retained; round-trip verified.",
36
+ "observed_route_source": "Exact six-hour LAT/LON from local IBTrACS WP archive, storm 2026182N09163.",
37
+ "observed_pressure_source": "IBTrACS USA_PRES; not JMA/WMO pressure. Missing labels are masked.",
38
+ "field_display": "Unchanged physical member-mean basin field, overlaid with the unchanged regional reconstruction only within its fixed coverage. No synthetic pressure extension or future observed pressure map.",
39
+ "regional_bounds": [
40
+ 139.25,
41
+ 169.25,
42
+ -2.0,
43
+ 28.0
44
+ ],
45
+ "qualification": "Requested Bavi development showcase, not an untouched holdout or a claim of typical skill.",
46
+ "video": {
47
+ "frames": 20,
48
+ "duration_seconds": 23,
49
+ "codec": "H.264",
50
+ "encoder": "h264_videotoolbox",
51
+ "file": "docs/trackformer_1_2_bavi.mp4",
52
+ "sha256": "33ecd068f1eee144b10301d6261dfad673557b2b38050e159e39935cccc3872c",
53
+ "playback": "1 second per six-hour lead; final frame held for 3 seconds; no interpolated forecast states"
54
+ }
55
+ }
paper/release_data/bavi_video.npz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9251fb82ebde819eced999c3440aa9b0a24251c29a4c2f6a4670f4137b9c5aff
3
+ size 781189
paper/trackformer_1_2_bavi_video_poster.png ADDED

Git LFS Details

  • SHA256: dd1cf75ec7331c4c952211cdc8a1ef62057373ff2455028570c866a54fdd418d
  • Pointer size: 131 Bytes
  • Size of remote file: 209 kB
release_tools/build_bavi_video.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Render saved Trackformer 1.2 Bavi means as an MP4; no new inference.
2
+
3
+ Requires numpy, matplotlib and ffmpeg. --source-root prepares the public subset.
4
+ The active GPU benchmark is not stopped or modified.
5
+ """
6
+ import argparse,csv,hashlib,json,subprocess
7
+ from datetime import datetime,timedelta,timezone
8
+ from pathlib import Path
9
+ import numpy as np
10
+ import matplotlib
11
+ matplotlib.use('Agg')
12
+ import matplotlib.pyplot as plt
13
+ from matplotlib.patches import Rectangle
14
+
15
+ REPO=Path(__file__).resolve().parents[1]
16
+ DATA=REPO/'paper/release_data'
17
+
18
+ def sha(path):
19
+ h=hashlib.sha256()
20
+ with Path(path).open('rb') as f:
21
+ for b in iter(lambda:f.read(8*1024**2),b''):h.update(b)
22
+ return h.hexdigest()
23
+
24
+ def prepare(root):
25
+ source=root/'benchmark_ensemble50/v173_e4/showcase_four.npz'
26
+ inputs=root/'runs/four_v166_maps/bavi/inputs.npz'
27
+ with np.load(inputs) as z:
28
+ lat,lon,center=z['latitude'],z['longitude'],z['center']
29
+ times=z['time'];issue=int(z['observation_ns'])
30
+ assert int(times[-1])<=issue and len(times)==9
31
+ with np.load(source) as z:
32
+ i=z['storms'].tolist().index('bavi');assert int(z['member_count'])==50
33
+ raw=z['route'][i].astype('float64');p=z['pressure'][i];coarse=z['fields'][i];native=z['native'][i]
34
+ # The old writer converted issue-centred local km back to longitude with
35
+ # predicted latitude. Undo that projection and use the actual issue latitude,
36
+ # recovering the member-mean geographic centres (to stored float precision).
37
+ east=((raw[:,1]-center[1]+180)%360-180)*111.2*np.maximum(np.cos(np.deg2rad(raw[:,0])),.2)
38
+ route=raw.copy();route[:,1]=center[1]+east/(111.2*max(np.cos(np.deg2rad(center[0])),.2))
39
+ roundtrip=center[1]+(route[:,1]-center[1])*max(np.cos(np.deg2rad(center[0])),.2)/np.maximum(np.cos(np.deg2rad(raw[:,0])),.2)
40
+ assert np.allclose(roundtrip,raw[:,1],atol=1e-6)
41
+ rows={}
42
+ csvpath=root/'data/ibtracs/ibtracs.WP.list.v04r01.csv'
43
+ with csvpath.open() as f:
44
+ for r in csv.DictReader(f):
45
+ if r['SID']=='2026182N09163':rows[r['ISO_TIME']]=r
46
+ t=datetime.fromtimestamp(issue/1e9,timezone.utc);obs=[];truthp=[]
47
+ for k in range(21):
48
+ r=rows[(t+timedelta(hours=6*k)).strftime('%Y-%m-%d %H:%M:%S')]
49
+ obs.append([float(r['LAT']),float(r['LON'])]);value=r['USA_PRES'].strip()
50
+ truthp.append(float(value) if value else np.nan)
51
+ assert np.allclose(obs[0],center,atol=.001)
52
+ path=DATA/'bavi_video.npz'
53
+ np.savez_compressed(path,forecast_lat_lon=np.vstack([center,route]),original_archive_route=raw,observed_lat_lon=np.array(obs),central_pressure_hpa=p,observed_pressure_hpa=np.array(truthp)[1:],regional_pressure_hpa=native,basin_pressure_hpa=coarse,latitude=lat,longitude=lon)
54
+ rings=json.loads((root/'trackformer-weatherlab-site/public/data/history/coastlines.json').read_text())
55
+ rings=[r for r in rings if any(125<=x<=161 and 5<=y<=30 for x,y in r)]
56
+ (DATA/'bavi_coastlines.json').write_text(json.dumps(rings,separators=(',',':'))+'\n')
57
+ meta={'model':'Trackformer 1.2','storm':'BAVI','storm_id':'2026182N09163','issue_time_utc':t.isoformat().replace('+00:00','Z'),'members':50,'leads_hours':list(range(6,121,6)),
58
+ 'source_archive_sha256':sha(source),'inputs_sha256':sha(inputs),'observed_csv_sha256':sha(csvpath),'data_sha256':sha(path),
59
+ 'checkpoint_sha256':'f194a23d3f91ea76ad776dfad942fabd669eeae8b3fd665815463095367e9ee0',
60
+ 'forecast_source':'Saved 50 smooth historical-input perturbation means from the released checkpoint; no new inference for this video.',
61
+ 'route_projection_correction':'Undo the archived predicted-latitude longitude conversion and restore the issue-latitude projection used by ensemble inference. Original route retained; round-trip verified.',
62
+ 'observed_route_source':'Exact six-hour LAT/LON from local IBTrACS WP archive, storm 2026182N09163.',
63
+ 'observed_pressure_source':'IBTrACS USA_PRES; not JMA/WMO pressure. Missing labels are masked.',
64
+ 'field_display':'Unchanged physical member-mean basin field, overlaid with the unchanged regional reconstruction only within its fixed coverage. No synthetic pressure extension or future observed pressure map.',
65
+ 'regional_bounds':[float(lon.min()),float(lon.max()),float(lat.min()),float(lat.max())],
66
+ 'qualification':'Requested Bavi development showcase, not an untouched holdout or a claim of typical skill.'}
67
+ (DATA/'bavi_video.json').write_text(json.dumps(meta,indent=2)+'\n')
68
+
69
+ def build(work):
70
+ work.mkdir(parents=True,exist_ok=True)
71
+ meta=json.loads((DATA/'bavi_video.json').read_text());assert sha(DATA/'bavi_video.npz')==meta['data_sha256']
72
+ with np.load(DATA/'bavi_video.npz') as z:a={k:z[k] for k in z.files}
73
+ rings=json.loads((DATA/'bavi_coastlines.json').read_text())
74
+ f,o=a['forecast_lat_lon'],a['observed_lat_lon'];lat,lon=a['latitude'],a['longitude']
75
+ bounds=[127,159,6,27];levels=np.arange(932,1025,2);contours=np.arange(932,1025,4)
76
+ glat=np.linspace(60,0,25);glon=np.linspace(100,180,33);lead=np.arange(6,121,6)
77
+ start=datetime.fromisoformat(meta['issue_time_utc'].replace('Z','+00:00'))
78
+ plt.rcParams.update({'font.size':12,'axes.titlesize':15})
79
+ for k in range(20):
80
+ fig=plt.figure(figsize=(16,10),dpi=100,facecolor='#f8fafb')
81
+ gs=fig.add_gridspec(2,2,width_ratios=[3.9,1.25],height_ratios=[4.5,1.25],left=.055,right=.94,bottom=.10,top=.86,wspace=.25,hspace=.35)
82
+ ax=fig.add_subplot(gs[0,:]);ax.set_facecolor('#f8fafb')
83
+ ax.contourf(glon,glat,a['basin_pressure_hpa'][k],levels=levels,cmap='RdYlBu_r',extend='both')
84
+ ax.contour(glon,glat,a['basin_pressure_hpa'][k],levels=contours,colors='#546971',linewidths=.45)
85
+ im=ax.contourf(lon,lat,a['regional_pressure_hpa'][k],levels=levels,cmap='RdYlBu_r',extend='both')
86
+ cs=ax.contour(lon,lat,a['regional_pressure_hpa'][k],levels=contours,colors='#354b58',linewidths=.65)
87
+ ax.clabel(cs,levels=np.arange(936,1025,12),fmt='%d',fontsize=9)
88
+ for ring in rings:
89
+ xy=np.asarray(ring);ax.plot(xy[:,0],xy[:,1],c='#66716b',lw=.8)
90
+ ax.add_patch(Rectangle((lon.min(),lat.min()),np.ptp(lon),np.ptp(lat),fill=False,edgecolor='#265b61',lw=1.2,ls=':'))
91
+ for points,color,label,style in [(o,'#182f42','Observed best track','--'),(f,'#b31565','Trackformer 1.2 mean of 50','-')]:
92
+ ax.plot(points[:,1],points[:,0],ls=style,c=color,alpha=.25,lw=1.6)
93
+ ax.plot(points[:k+2,1],points[:k+2,0],ls=style,c=color,lw=2.8,label=label)
94
+ ax.scatter(points[k+1,1],points[k+1,0],s=75,c=color,edgecolors='white',linewidths=1.8,zorder=6)
95
+ ax.set(xlim=bounds[:2],ylim=bounds[2:],xlabel='Longitude °E',ylabel='Latitude °N');ax.grid(alpha=.13);ax.set_aspect(1/np.cos(np.deg2rad(16.5)))
96
+ ax.legend(loc='lower left',fontsize=11,framealpha=.95)
97
+ ax.text(.985,.03,'Dotted boundary: fixed regional reconstruction\nOutside: 2.5° basin pressure only',transform=ax.transAxes,ha='right',va='bottom',fontsize=10,bbox={'facecolor':'white','alpha':.9,'edgecolor':'none','pad':5})
98
+ cax=fig.add_axes([.952,.35,.014,.45]);fig.colorbar(im,cax=cax,label='Model mean sea-level pressure (hPa)')
99
+ curve=fig.add_subplot(gs[1,0]);curve.plot(lead,a['central_pressure_hpa'],c='#b31565',lw=2,label='Model central pressure');curve.plot(lead,a['observed_pressure_hpa'],'--',c='#182f42',lw=2,label='Observed pressure (IBTrACS USA)')
100
+ curve.axvline(lead[k],c='#688e98',lw=1.5);curve.scatter([lead[k]],[a['central_pressure_hpa'][k]],c='#b31565',s=40,zorder=4)
101
+ curve.set(xlim=(6,120),xlabel='Forecast lead (hours)',ylabel='Central pressure (hPa)',xticks=[6,24,48,72,96,120]);curve.grid(alpha=.2);curve.legend(loc='upper right',fontsize=10,ncol=2)
102
+ info=fig.add_subplot(gs[1,1]);info.axis('off');p=a['central_pressure_hpa'][k];op=a['observed_pressure_hpa'][k]
103
+ inside=lon.min()<=f[k+1,1]<=lon.max() and lat.min()<=f[k+1,0]<=lat.max()
104
+ msg=f'Model centre: {p:.1f} hPa\nObserved: {op:.0f} hPa\n\n'+('Centre within regional field' if inside else 'Centre outside regional patch\nDetailed core is not shown here')
105
+ info.text(0,1,msg,va='top',fontsize=13,color='#244754')
106
+ valid=start+timedelta(hours=int(lead[k]));fig.suptitle(f'Trackformer 1.2 / BAVI / +{lead[k]:03d} h',x=.055,y=.97,ha='left',fontsize=24,fontweight='bold',color='#173947')
107
+ fig.text(.055,.915,f'Issue: {start:%d %b %Y %H:%M} UTC | Valid: {valid:%d %b %Y %H:%M} UTC | 50-member mean',fontsize=14,color='#385865')
108
+ fig.text(.055,.035,'Actual model pressure fields; observed route is verification only. Mean central pressure is not the minimum of the mean map.',fontsize=11,color='#47636d')
109
+ fig.savefig(work/f'frame_{k:03d}.png',dpi=100);
110
+ if k==9:fig.savefig(REPO/'paper/trackformer_1_2_bavi_video_poster.png',dpi=100)
111
+ plt.close(fig)
112
+ output=REPO/'docs/trackformer_1_2_bavi.mp4'
113
+ cmd=['ffmpeg','-y','-hide_banner','-loglevel','warning','-framerate','1','-i',str(work/'frame_%03d.png'),'-vf','fps=30,tpad=stop_mode=clone:stop_duration=3','-c:v','h264_videotoolbox','-b:v','4M','-pix_fmt','yuv420p','-movflags','+faststart','-an',str(output)]
114
+ subprocess.run(cmd,check=True)
115
+ meta['video']={'frames':20,'duration_seconds':23,'codec':'H.264','encoder':'h264_videotoolbox','file':'docs/trackformer_1_2_bavi.mp4','sha256':sha(output),'playback':'1 second per six-hour lead; final frame held for 3 seconds; no interpolated forecast states'}
116
+ (DATA/'bavi_video.json').write_text(json.dumps(meta,indent=2)+'\n')
117
+ print('Saved',output,output.stat().st_size,'bytes')
118
+
119
+ if __name__=='__main__':
120
+ p=argparse.ArgumentParser();p.add_argument('--source-root',type=Path);p.add_argument('--work',type=Path,required=True);args=p.parse_args()
121
+ if args.source_root:prepare(args.source_root)
122
+ build(args.work)