
1 简介1.1 这篇文章讲什么这篇文章面向第一次接触 WorldVLN 的读者。目标不是记住每个脚本名而是理解一条完整链路自然语言指令和真实画面如何变成无人机动作以及闭环试飞结果如何反过来改进世界模型。文章只把网络公开资料作为背景校准把实现细节落在当前仓库的infer/、train/、Worldmodel/和action_aware_grpo/代码上。1.2 为什么需要闭环空中视觉语言导航的困难不在于看懂一张图而在于动作会改变下一刻的观测。无人机向前、偏航、上升或下降都会让第一视角画面发生连续变化。如果模型只根据当前帧输出动作它会忽略动作后果对后续判断的影响。arXiv 页面把 WorldVLN 定义为在 3D 环境中根据自然语言进行闭环感知与动作。官方项目页强调它基于 observe-act-update 的闭环过程。这不是一次性分类而是不断观察、行动、修正的系统问题。1.3 闭环协议一句话版这类闭环在仓库里最终落到同一个 HTTP 接口反复调用而不是一次性上传完整未来。首轮1 帧真实图像 instruction - 预测 16 个动作 后续执行 16 个动作 - 回传 16 帧真实图像 - 再预测 16 个动作2 核心思想2.1 不是直接动作是先预测世界变化WorldVLN 的关键思想是把导航建模成 prediction-driven world-action problem。模型先预测 latent world evolution再根据预测后果采取行动。落实到仓库主线不是image - action而是real observation prefix instruction - summed_codes - actions。summed_codes是世界模型预测出的 latent 世界转移用于让动作解码器获得接下来世界如何变化的结构信号。对应到仓库真实代码infer/server.py中的_predict_delta_actions_impl()是总调度器四层逻辑清晰可见# infer/server.py: _predict_delta_actions_impl() 主流程简化def_predict_delta_actions_impl(req)-PredictDeltaActionsResponse:# 第一层session 管理 — 获取/创建 TrajectoryStatesession_id_SESSION_ALIAS.get(external_session_id,external_session_id)st_get_or_create_traj(session_id,raw_prompt,req.negative_promptor)# 第二层真实帧入库 — 解码 base64 并追加到 st.frames_cpunew_imgs[_load_image_from_base64(s)forsinreq.images_base64]foriminnew_imgs:tinfinity_transform(im,tgt_hst.tgt_h,tgt_wst.tgt_w)st.frames_cpu.append(t.cpu())# 第三层segment ready 判定# 默认n points[seg1] 才输出future 模式n points[seg] 就输出nst.num_frames()can_emitnpoints[seg1]# 或 allow_future_segments 时 n points[seg]# 第四层世界模型预测 动作解码infer_res_infer_latents_for_actions_and_advance_cache(st,segment_indexseg,seedseed,advance_gt_obs_to_nextcan_emit)actions_stage2_predict_16_actions_for_segment_cm_deg(stst,infer_resinfer_res)returnPredictDeltaActionsResponse(actionsactions,segment_indexseg,...)其中预测世界这一步由_infer_summed_codes_for_step()完成# infer/server.py: _infer_summed_codes_for_step()# 它不直接输出动作而是让 InfinityStar 根据 prompt 真实观测帧# 预测一段 latent 视频轨迹 summed_codes。st.stream.correction_clear_pred()# 清理上一轮预测 cachesummed_codes,pred_vid_infer_summed_codes_for_step(st,step_isegment_index,obs_lenobs_len,# 当前可信任的真实帧数量infer_num_framesinfer_num_frames,# 让世界模型预测的窗口长度seedlocal_seed,top_kstep_top_k,top_pstep_top_p,injectioninj,# gt_obs / official_leak / hybridneed_pred_videoneed_pred_video,)# summed_codes 形状: [1, 16, pt, H, W] — 这就是世界如何变化的 latent 表达2.2 外部概念到仓库实现的映射从公开信息看WorldVLN官方项目页列出三个重点自回归 WAM、Action-aware GRPO、以及真实无人机部署。本文把这些外部结论对应到仓库实现公开概念仓库实现入口Autoregressive WAMWorldmodel/runtime/infinity/infer/server.pyClosed-loop observe-act-updateinfer/client.pysession_idimages_base64Action-aware GRPOaction_aware_grpo/reward_uavflow.pyReal-world deploymentREADME 中的推理、训练和 rollout 脚本# 在线推理服务bashinfer/run_server.sh# 世界模型监督训练bashtrain/scripts/train_from_base.sh# 动作解码器训练分 Stage A 蒸馏 Stage B 监督bashtrain/action_decoder/scripts/train_stageA_ddp.shbashtrain/action_decoder/scripts/train_stageB_ddp.sh# Action-aware GRPO分 collect trainbashaction_aware_grpo/scripts/run_stagea_collect.shRUN_IDmy_runbashaction_aware_grpo/scripts/run_stageb_partialfreeze.shRUN_IDmy_run3 基础协议3.1 两个 checkpoint 的角色在线服务需要两个不同角色的权重。INFINITY_CKPT是世界模型权重负责预测 latent 世界变化。STAGE2_LATENT2ACTION_CKPT是动作解码器权重负责把 latent 转成可执行动作。把二者混用轻则加载失败重则产生语义完全错误的输出。GRPO StageB 产出世界模型 checkpoint动作 Stage B 产出 latent-to-action checkpoint。# 服务启动时必须同时指定两类权重exportPYTHON_BIN$(whichpython)exportINFINITY_CKPT/path/to/infinity/global_step_xxx.pthexportSTAGE2_LATENT2ACTION_CKPT/path/to/stage2_latent2action_combined.ptbashinfer/run_server.sh服务端启动后/health接口可以确认权重是否加载成功# infer/server.py: health 接口返回的关键字段{status:ok,ts_ckpt_loaded:True,# 动作解码器是否加载stage2_ckpt:/path/to/...,# 动作解码器路径infinity_loaded:True,# 世界模型是否加载num_frames:49,step:16,points:[1,17,33,49],# segment 边界}3.2 时间协议49 帧、48 动作、16 步一段帧是状态点动作是两个状态点之间的边。49 帧对应 48 个相邻动作。仓库默认step1648 个动作被切成 3 段。服务端内部用_obs_points()函数计算 segment 边界# infer/server.py: segment 边界计算def_obs_points(pred_num_frames:int,step:int)-List[int]: 闭环 segment 边界公式points [1, 1step, 12*step, ..., num_frames] 例如 pred_num_frames49, step16 时返回 [1, 17, 33, 49] endint(pred_num_frames)pts[1]k1whileTrue:v1k*int(step)ifvend:breakpts.append(v)k1ifpts[-1]!end:pts.append(end)returnpts时间线全景49 frames: frame0 ... frame48 48 actions: frame0-frame1 ... frame47-frame48 clip1: frame0 - frame16, 16 actions clip2: frame16 - frame32, 16 actions clip3: frame32 - frame48, 16 actions server points: [1, 17, 33, 49] (1-based)3.3 Latent 时间压缩17 帧对应 5 个 latent视频 VAE 压缩时间维对应关系由InfinityConfig.pt_total()计算# infer/server.py: InfinityConfig 中的 latent 时间长度计算defpt_total(self)-int:返回整条 RGB 序列对应的 latent 时间长度默认 video VAE 时间压缩率为 4。# latent_index(frame f) (f - 1) // temporal_compress_rate 1return(int(self.num_frames)-1)//41一个 17 帧 segment 对应 5 个 latent。动作解码器需要起点 latent 后续 4 个新 latent来描述 16-action 运动。第二段复用上一段终点 latent保证动作片段在时间上连续。seg0: [latent1, latent2, latent3, latent4, latent5] - 16 actions seg1: [latent5, latent6, latent7, latent8, latent9] - 16 actions seg2: [latent9, latent10, latent11, latent12, latent13] - 16 actions ↑ 段间公共边界last_latent_1代码中的last_latent_1就是段间公共边界的实现# infer/server.py: _infer_latents_for_actions_and_advance_cache() 注释# seg0 使用上一段保存的 last_latent_1 作为边界再拼当前段的 4 个新 latent。# latent5 只是一帧边界 latent保留它是为了让相邻段在 frame17 这个公共边界上连续。4 世界模型训练4.1 Stage-1 SFT训练会预演的世界模型Stage-1 监督训练不直接回归动作。它训练 InfinityStar 在语言条件下预测真实视频的 latent token。训练数据来自视频 JSONL记录视频路径、帧范围、fps 和 caption。脚本采样 49 帧经 video VAE 编码再把文本条件和视觉 token 送入自回归模型做 teacher-forcing。这个阶段给模型建立导航视频 latent 会怎样演化的先验。{video_path:relative/or/absolute/path/to/video.mp4,begin_frame_id:0,end_frame_id:48,fps:16.0,tarsier2_caption:A UAV flies over a road.}训练脚本期望的目录结构train/ ├── train.py # 主训练脚本 ├── scripts/train_from_base.sh # 启动入口 ├── checkpoints/ │ ├── text_encoder/flan-t5-xl-official/ │ ├── infinitystar_videovae.pth │ └── infinitystar_8b_480p_weights/ ├── data/ │ └── your jsonl shard directory # 分 8 片的 JSONL └── outputs/关键环境变量可覆盖默认路径# 文本编码器exportT5_PATHcheckpoints/text_encoder/flan-t5-xl-official# Video VAEexportVAE_PATHcheckpoints/infinitystar_videovae.pth# 基础模型分片权重exportTORCHSHARD_RESUME_PATHcheckpoints/infinitystar_8b_480p_weights# 训练数据目录exportVIDEO_DATA_PATHdata/uavflow_49f_from_40_60_split8_jsonl5 动作解码器训练5.0 为什么需要 TimeSformerTSFormer动作解码器的任务是从 latent 时空特征中读出无人机的 6D 飞行动作。一个朴素方案是直接用 MLP 回归但 MLP 只能逐帧独立映射无法理解连续帧之间的运动关系——而运动关系恰恰是动作的本质。TimeSformerDivided Space-Time Attention Transformer是 Facebook 提出的视频理解模型通过交替的时间注意力和空间注意力天然具备从视频 token 序列中提取帧间运动模式的能力。WorldVLN 利用这一预训练能力把 TSFormer 作为动作解码器的骨干网络设计动机 • 动作 帧间运动的数值表达位移 旋转 • TSFormer 的 divided space-time attention 已经学会看视频理解运动 • 复用预训练权重无需从零学习时空关系 系统定位 latent → VAE decoder 中间特征 → Adapter → [TSFormer] → 6D 动作 ↑ 运动感知骨干核心推理引擎TSFormer 在两个训练阶段中扮演不同角色阶段TSFormer 状态角色目的Stage A冻结参数不更新Teacher真实 RGB → TSFormer patch embedding → “标准 token”Adapter 学习让 VAE 特征也能产出相同格式的 tokenStage B加载预训练可微调Backbone接收 Adapter 输出的 token用 4 帧滑窗推理每窗口输出 3 个 6D 动作 delta简言之Stage A 让 Adapter 学会说 TSFormer 听得懂的语言Stage B 让 TSFormer 利用预训练的时空理解能力从这些 token 中读出实际飞行动作。5.1 Stage AAdapter 蒸馏Stage A 解决的是语言不通的问题。真实 RGB 经过 frozen TimesFormer patch embedding 得到 teacher tokenslatent 经过 VAE decoder 中间层 hook 和 Adapter 得到 student tokens。训练目标是让两者对齐。这里不使用动作标签也不判断轨迹是否成功。它只训练一座桥让 latent 走出来的 token 更像 TimesFormer 熟悉的输入。# train/action_decoder/tools/train_stageA_ddp.py: 蒸馏 loss 组合lossw_cos*L_cosw_mse*L_msew_mean*L_meanw_std*L_stdStage A 必需环境变量exportMANIFEST_JSON/path/to/manifest.json# 训练数据清单exportTSFORMER_CKPT/path/to/tsformer.pth# frozen TimesFormer 权重exportINF_VAE_PATH/path/to/videovae.pth# Video VAE 权重bashtrain/action_decoder/scripts/train_stageA_ddp.sh训练数据清单格式{items_train:[{latent_path:path/to/latents.pt,traj_json_path:path/to/preprocessed_logs.json,images_dir:path/to/images}]}5.2 Adapter 的内部结构Vae96ToTSformerEmbedAdapter是动作解码器中最关键的形状转换层。它接收 VAE decoder 的 96 通道时空特征(B,96,T,H,W)通过卷积和 16x16 patch 投影得到(B*T,480,384)token。直接取 VAE 中间特征而不完整解码成 RGB因为动作头需要运动和几何信息而不是纹理细节。# Worldmodel/action_decoder/src/models/vae96_to_tsformer_adapter.pyclassVae96ToTSformerEmbedAdapter(nn.Module):defforward(self,f96_up3):# 输入VAE decoder 中间层 96 通道时空特征B,C,T,H,Wf96_up3.shape# (B, 96, T, H, W)# 时间维并入 batchxf96_up3.permute(0,2,1,3,4).reshape(B*T,C,H,W)# resize 到动作头训练使用的空间比例xF.interpolate(x,size(192,640),modebilinear,align_cornersFalse)# 卷积精炼 16x16 patch 投影hself.conv_a(x)hself.patch(h)# patch projection# 输出 token 序列tokensh.flatten(2).transpose(1,2).contiguous()# (B*T, 480, 384)returnself.out_norm(tokens),int(T),int(h.shape[-1])形状变化全景VAE decoder 中间特征 (B, 96, T, H, W) - permute reshape (B*T, 96, H, W) - interpolate (B*T, 96, 192, 640) - conv_a (B*T, C, 192, 640) - patch 16x16 (B*T, C, 12, 40) - flatten transpose (B*T, 480, 384) - LayerNorm (B*T, 480, 384) TimesFormer-compatible tokens5.3 Stage B从 latent 到 6D 动作Stage B 接在 Stage A 后面开始使用专家轨迹标签。latent 走 VAE decoder hook 和 Adapter然后交给 TimesFormer 的 4 帧滑窗。每个窗口输出 3 个相邻动作 delta多个窗口重叠平均切出 16 个动作。训练内部用 m/radAPI 对外输出[dx_cm, dy_cm, dz_cm, droll_deg, dyaw_deg, dpitch_deg]。这一步产出STAGE2_LATENT2ACTION_CKPT。# infer/server.py: _stage2_predict_16_actions_for_segment_cm_deg() 核心注释 数据流 1. 从 infer_res.summed_codes 得到整段预测 horizon 的 z_ext 2. VAE decoder Adapter 生成整段 tokens_tnd 3. 对当前 segment 只取左侧上下文 [ctx_start..clip_end]不看右侧未来帧 4. 用 window4 的 TimesFormer 滑窗推理并对重叠帧做平均 5. 切出本 segment 对应的 16 个动作并转成 cm/deg 关键公式 - meters - cm: *100, radians - degrees: *180/pi - 4 帧滑窗聚合 delta[t] sum(predictions covering t) / count[t] 具体例子seg0seg0: obs_len1, next_obs_len17, 需要输出 16 个动作 TimesFormer 输入token 覆盖帧 1..17 窗口数量 K 17 - 4 1 14 个窗口打包成一个 batch 每个窗口输出 3 个相邻帧 delta frame4 的 delta 来自 3 个窗口的平均 [1,2,3,4] 的第 3 个输出 [2,3,4,5] 的第 2 个输出 [3,4,5,6] 的第 1 个输出 最终输出frame2..frame17 共 16 个动作 (cm/deg)Stage B 必需环境变量exportMANIFEST_JSON/path/to/manifest.jsonexportTSFORMER_PRETRAINED/path/to/tsformer.pthexportADAPTER_CKPT/path/to/stageA_adapter.pth# Stage A 产出exportINFINITYSTAR_VAE_PATH/path/to/videovae.pthbashtrain/action_decoder/scripts/train_stageB_ddp.sh6 在线推理服务6.1 HTTP 接口在线推理入口是infer/server.py中的/v1/predict_delta_actions。请求体包含session_id、instruction、本轮真实 RGB 图像images_base64。返回体里的actions默认每段 16 个动作单位 cm/deg。# infer/server.py: 请求体完整定义classPredictDeltaActionsRequest(BaseModel):session_id:str# 轨迹标识符同一条路线多次请求必须保持一致instruction:Optional[str]None# 导航指令首次请求必须提供prompt:Optional[str]None# instruction 的兼容别名negative_prompt:Optional[str]images_base64:List[str]# base64 编码的 RGB 图像首次 1 帧后续 16 帧reset_session:boolFalse# 强制开启新 sessionaction_head_mode:strtsformer_latent# Stage-2 latent-to-actionallow_future_segments:boolFalse# 允许提前发射动作prefix_mode:boolFalse# True每次携带完整前缀seed:Optional[int]None# infer/server.py: 响应体完整定义classPredictDeltaActionsResponse(BaseModel):actions:List[List[float]]# [dx_cm, dy_cm, dz_cm, droll_deg, dyaw_deg, dpitch_deg]segment_index:int# 本次输出哪个 segment-1 表示尚未 readynum_received_frames:intprefix_latents:intdone:boolused_prompt:Optional[str]NoneFastAPI 入口用全局锁保证 GPU 推理串行# infer/server.py: FastAPI 路由入口app.post(/v1/predict_delta_actions,response_modelPredictDeltaActionsResponse)asyncdefpredict_delta_actions(req:PredictDeltaActionsRequest):if_LOCKisnotNone:asyncwith_LOCK:return_predict_delta_actions_impl(req)return_predict_delta_actions_impl(req)6.2 客户端闭环协议infer/client.py展示了客户端如何按协议发送帧。核心是_post_frames()函数# infer/client.py: _post_frames() 核心逻辑def_post_frames(frames:List[Image.Image],*,include_instruction:bool)-Dict:payload{session_id:session_id,images_base64:[_pil_to_data_url(im,codecimage_codec)foriminframes],prefix_mode:False,# 增量协议allow_future_segments:True,# 允许提前输出动作action_head_mode:tsformer_latent,}ifinclude_instruction:payload[instruction]instruction payload[reset_session]True# 首轮强制新建 sessionreturn_http_post_json(url/v1/predict_delta_actions,payload)# 闭环主循环im0capture_first_frame()resp_post_frames([im0],include_instructionTrue)# 第 1 帧 instructionforsegmentinrange(num_segments):actionsresp[actions]# 16 个 [dx,dy,dz,droll,dyaw,dpitch]new_framesexecute_and_capture(actions)# 执行动作采集 16 帧真实图像resp_post_frames(new_frames,include_instructionFalse)# 回传真实帧客户端输出的 JSON 格式# infer/client.py: 每个 segment 输出两个文件# 1. {session_id}_seg00_actions.json{session_id:route_001,segment_index:0,units:{translation:cm,angles:deg},action_order_server:[dx,dy,dz,droll,dyaw,dpitch],actions_server_order:[[1.2,-0.3,0.5,0.0,2.1,-0.1],...],# 16 个cumsum_server_order:[[1.2,-0.3,0.5,0.0,2.1,-0.1],...],# 累加}# 2. {session_id}_seg00_poses.json (绝对坐标)6.3 TrajectoryState闭环的最低条件TrajectoryState对应单条 session 的闭环状态。它保存已收到的真实帧、prompt、streaming session以及跨 segment 复用的last_latent_1。…详情请参照古月居