Skip to content

Fix load media base64 v2#51

Open
JasonBuildAI wants to merge 10 commits intoFireRedTeam:mainfrom
JasonBuildAI:fix-load-media-base64-v2
Open

Fix load media base64 v2#51
JasonBuildAI wants to merge 10 commits intoFireRedTeam:mainfrom
JasonBuildAI:fix-load-media-base64-v2

Conversation

@JasonBuildAI
Copy link
Contributor

@JasonBuildAI JasonBuildAI commented Mar 9, 2026

这次改动老实说比我一开始想象的复杂得多。因为它同时牵涉到:

  • 本地上传视频后的一整条剪辑链路(导入素材 → 理解素材 → 分镜/分析 → 生成文案 → 渲染);
  • 以及通过网络搜索(search_media)下载素材再进入同一条剪辑链路。

每一步都和「素材是 path 还是 base64」、「路径相对哪个目录」有关,所以非常容易在某个场景上思考不周、踩到边界。
目前这个版本我已经在本地实际跑过多个流程:无论是上传本地视频剪辑,还是网络搜索素材剪辑,都可以完整跑通且不报错,并且在同机/path-only 模式下,本地文件处理的速度快了非常多
不过我也必须承认,系统场景很多,依然有可能在我没覆盖到的情形下出现 bug,还请维护者帮忙多看一眼,有任何不合理的地方我都会再跟进修正。

0. 为什么新开 PR(v2)

之前的 PR 在多轮 review + 实测中不断补边界情况,提交历史变得分散,阅读/审核成本较高。为降低维护者的理解成本,我将最终确定需要的改动整理到一个干净分支并新开 PR(v2),聚焦解决同一个问题(issue #11),同时把实测踩到的关键边界一并补齐。

1. 这个 PR 主要解决什么问题(issue #11

当 Agent 与 MCP server 在同一台机器(本地 / 单机 / 同机部署)时,现有实现会把媒体文件 gzip+base64 内联进 MCP HTTP 请求体(包括 load_media 及依赖节点的 payload)。素材多/文件大时请求体会非常大,容易引发连接中断(例如 ReadError/ClientDisconnect),从而导致 load_media 失败。

本 PR 的目标是:同机默认走 path-only(不内联 base64)以避免超大 payload;跨机仍可走 base64;并且保证 path-only 模式在真实工作流(Web UI 上传、search_media 下载、BGM/资源路径、嵌套 dict path)下可用。

2. v2 相比 v1 多解决了什么

除了“避免同机巨型 base64 payload”这个核心点,v2 额外补齐了这些在实测中会导致“找不到素材/渲染失败”的边界:

  • 可配置策略:新增 inline_media = auto|always|never,并用同一策略函数驱动客户端/服务端决策。
  • Web UI session 媒体目录差异:path-only 下 load_media 优先生成 project.media_dir 相对路径,否则回退绝对路径,避免服务端按错误基准解析相对路径。
  • search_media 下载素材可被 load_media 使用:path-only 下把本 session 最近一次 search_media 的产出路径合并进 load_media.inputs,并兼容 list[{"path":...}]list[str]
  • BGM/资源路径不在 media_dir:path-only 回包时对不在 media_dir 下的资源路径回退绝对路径,避免下游错误解析。
  • dict 形态的 path 统一 pack/load:不仅处理 list[dict],也处理 dict 中的 {"path": ...},减少“结构不同就漏处理”。

3. 具体怎么解决(逻辑/实现)

3.1 单一策略来源:should_inline_media_as_base64(server_cfg)

  • 新增配置字段:inline_mediasrc/open_storyline/config.py)与 config.toml 示例项。
  • inline_media=always 强制 base64;never 强制 path-only;autoconnect_host 判断(127.0.0.1/localhost/::1/0.0.0.0 视为本地)。

3.2 客户端侧:只在需要时才内联 base64

compress_payload_to_base64(payload, server_cfg) 在 path-only 时是 no-op;只在策略要求 base64 时才会把 path 压缩/编码并写入 base64/md5 字段。

3.3 load_media 的 path-only 输入生成 + 合并 search_media

ToolInterceptor.inject_media_content_before() 中:

  • path-only 生成 inputs 时:优先 project.media_dir 相对路径,否则绝对路径兜底(兼容 Web UI session 目录)。
  • path-only 下追加本 session 最近一次 search_media 的下载产出路径到 load_media.inputs(兼容 {"path": ...} / "...path..."),并对路径去重。

3.4 服务端侧统一合约(输入解析 + 输出打包)

BaseNode 中:

  • _load_item
    • 绝对路径直接接受;
    • 相对路径只允许在 project.media_dir 下解析并防止逃逸(路径穿越保护)。
  • _pack_item
    • 服务端也使用同一策略函数决定返回 path-only 还是 base64;
    • path-only 时尽量返回 media_dir 相对路径;若目标不在 media_dir(如 BGM/资源)则返回绝对路径,避免下游再按 media_dir 误解析。
  • pack_outputs_to_client/load_inputs_from_client
    • 对 dict 形态的 {"path": ...} 也执行同一套 pack/load(不再只覆盖 list[dict]),覆盖更多节点输出形态。

Test plan

  • 本地 Web UI 上传 → load_media(path-only)可正常读到素材。
  • search_mediaload_media(path-only)包含下载素材,后续节点可使用。
  • select_bgm + render_video(path-only)BGM 路径可正确解析,渲染可正常跑通。
  • python -m compileall src

再次感谢维护者之前在 PR #49 里给的详细建议(尤其是对“本地/path-only 合约”和“单一策略函数”的讨论),也感谢这次愿意再帮我 review 这个 v2 版本。很可能还会有v3,v4版本。如果还有我没有考虑到的场景或更合理的设计方向,也请直接指出,我会继续跟进修正。也非常非常非常非常感谢你们的耐心,我从这次改进中学到很多的内容。麻烦你们了,不胜感激🙏!

…ixes FireRedTeam#11)

- Add should_inline_media_as_base64() strategy (default False for local deployment)
- load_media: send path relative to media_dir; fallback to absolute on ValueError
- compress_payload_to_base64: no-op when local mode
- BaseNode._load_item: path-only branch with media_dir resolution and traversal check
- BaseNode._pack_item: return path-only when input was path-only (orig_md5 None)

Resolves httpx.ReadError / ClientDisconnect when loading many media files.

Made-with: Cursor
…remote

- should_inline_media_as_base64(server_cfg) now reads local_mcp_server.connect_host
- 127.0.0.1, localhost, ::1 -> path-only (local)
- Any other host -> base64 (remote MCP)

Made-with: Cursor
…es auto-searched media not readable)

Made-with: Cursor
@Anlittledy
Copy link
Collaborator

辛苦了!感谢你对这个 PR 的耐心迭代,我们这边会尽快给出详细的review

@JasonBuildAI JasonBuildAI force-pushed the fix-load-media-base64-v2 branch 2 times, most recently from 0e3c2b6 to 8de7bca Compare March 9, 2026 10:39
# Dict: recursively process nested data (without saving)
loaded_input[k] = self.load_inputs_from_client(node_state, payload_input, user_info, save=False)
# Dict: if it looks like a file-path payload, load it like an item; otherwise recurse.
if self._looks_like_file_path(payload_input.get("path")):
Copy link
Collaborator

@jcl-2026 jcl-2026 Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

此处原代码无需改动:

  1. _looks_like_file_path 不是一个必要改动,path目前暂没有兼容http这种格式。

packed_output[k] = [self._pack_item(node_state, item) for item in payload_output]
elif isinstance(payload_output, dict):
packed_output[k] = self.pack_outputs_to_client(node_state, payload_output)
# Dict: if it looks like a file-path payload, pack it like an item; otherwise recurse.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

与Comment on line R210 相同

meta_collector: NodeManager = context.node_manager
input_data = defaultdict(list)

server_cfg = getattr(context, "cfg", None) or getattr(context, "server_cfg", None)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. server_cfg 变量表意不对,context对应<class 'open_storyline.agent.ClientContext'>,client_cfg更合理。
  2. getattr(context, "server_cfg", None) 可以删去

@JasonBuildAI
Copy link
Contributor Author

三处不合理的地方已经修改

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants