Skip to content

[bug] 画布:导入已导出的 .canvas 归档时报「时间线片段超出实际素材时长」 #256

Open

Description

@allendyu

问题与影响

导入已导出的 .canvas 画布归档后,若工程内的时间线含有从属素材的片段,界面弹出:

时间线片段超出实际素材时长。

导入过程中断,用户无法把自己刚导出的画布在另一台机器或另一个工程中恢复。源工程本身是合法的——它能正常导出,说明素材与片段的时长关系在导出侧已通过校验。

复现步骤

  1. 新建工程,上传一段视频素材(时长记为 T),加入时间线,使片段的 outPoint 等于 T。
  2. 通过画布工具栏「导出画布」得到 .canvas 文件。
  3. 在任意工程中点击「导入画布」,选择该文件。
  4. 导入时前端会把时间线文档连同素材登记到当前工程,随后保存 editorDocument。

第 4 步的 editor.create / editor.save 命令在服务端触发时间线校验,即为报错来源。复现不需要调用任何付费模型接口,也不要求真正渲染。

预期与实际

  • 预期:outPoint == 素材时长 的片段可以正常导入,导入后时间线结构与导出前一致。
  • 实际:服务端抛出 invalid_input,错误码内容为「时间线片段超出实际素材时长。」,导入失败。

定位与证据

基线 c08e19ba39a9e4f941c8ca6cdabbb8150ee4c45b。已用真实的 CreationCanvas._validate_timeline(内存副本,未改动任何工程数据)复现,输出与截图完全一致。

服务端校验位于 creation_canvas.py:

def _validate_timeline(self, document, snapshot):
    ...
    for track in document.data.tracks:
        for clip in track.clips:
            asset = self.assets._reference(snapshot, clip.resource.asset)
            duration = asset.metadata.durationSeconds
            if asset.kind != 'image' and (duration is None or clip.outPoint > duration + 1e-6):
                raise ContractError('invalid_input', '时间线片段超出实际素材时长。', asset.id)

判定用的是快照里素材的实际时长 asset.metadata.durationSeconds,而不是片段自带的 clip.resource.durationSeconds。两者来源不同:后者随文档从归档原样带过来,前者由服务端在上传素材时用 ffprobe 重新探测。

客户端执行的是另一套规则,见 dataBoundary.ts 与 editors.py:两者都只在 durationSeconds is not None 时比较,时长未知时直接放过。

两条规则的分歧由此产生:

场景 客户端 服务端
素材时长未知(durationSeconds 为 None) 放过 拒绝,抛上述错误
outPoint 比探测时长多出 1e-6 以上 放过 拒绝

实测结果:

PASS    video outPoint == probed duration
RAISE   video outPoint 1e-4 past duration -> 时间线片段超出实际素材时长。
RAISE   video probed duration is None     -> 时间线片段超出实际素材时长。
PASS    image outPoint 5.0, no duration
PASS    audio outPoint == probed duration

因此报错对应两条可能的触发路径,本次复现确认了机制但未确定用户数据落在哪一条:

  1. 素材时长在导入后未知。 归档导入会重新登记素材,若探测拿不到时长(特殊容器、探测失败等),durationSeconds 为 None,此时任何非图片片段都会被拒,与 outPoint 多小无关。这条路径与片段内容无关,最容易造成「同样的画布在别的机器导不进来」。
  2. 探测时长与片段的 outPoint 存在浮点偏差。 时间线创建片段时把 outPoint 直接取为素材时长(见 timeline.ts),重新探测得到的时长若略小于原值,超出部分即触发 1e-6 容差。

按上述机制,这两条都指向同一个设计问题:客户端允许「时长未知」的片段通过,服务端却把它当作致命错误,且两端比较的时长来源不同。

验收标准

  • 导入一个 outPoint 等于素材时长的正常画布归档,不再出现「时间线片段超出实际素材时长。」,导入成功。
  • 素材时长无法探测时,导入行为与客户端 dataBoundary 的判定一致(或给出指向素材本身的明确提示,而不是笼统的越界错误)。
  • 真正越界的片段(outPoint 明显超过素材时长)仍然被拒绝,不会被静默截断。
  • 图片片段不受影响:图片没有源时长,应继续跳过该比较。

Activity

  1. added theissue type on Nov 10, 2026
  2. self-assigned this
    on Nov 10, 2026
  3. hsliuustc0106 commented on Nov 10, 2026

    @hsliuustc0106
    Contributor

    #256 itself does not need a breaking format change. This issue is a client/server mismatch on timeline duration validation: the server _validate_timeline path compares clip.outPoint against a re-probed asset.metadata.durationSeconds and rejects an unknown duration or an overflow beyond 1e-6; the client (dataBoundary / editors.py) skips that check when duration is unknown. Aligning those two rules should meet the acceptance criteria without changing the archive format.

    The recurring “import/export breaks after a version bump” bugs look like a different layer. Today a .canvas file packs the portable canvas document and media bytes (manifest.json + canvas.json + assets/), and import then translates that document into a new project. The last large failure of that translation is documented in canvas-archive-import-bug: generation-node provenance, whitelist field drops, and group create order all failed in the document → project step, not because the zip itself was corrupt. Zipping and unzipping the on-disk project directory would skip that translation and keep durations, provenance, and bindings intact, which is why same-version round-trips feel reliable in development. That direction makes sense.

    I would not accept making existing exported .canvas files unreadable. The product is still early (v0.1.2), so a new project-pack export format can be fine if we keep a read-only importer for old archives — even if generation nodes have to degrade to plain material, the same way v1 → v2 already does.

    A project-directory pack is also a different product: it restores a project rather than bringing a canvas into another project. Local paths, project id collisions, caches/locks, and later project-schema changes can still make an old pack fail to open. Cross-version compatibility does not go away; it just moves onto a new format.

    Suggested path:

    1. Fix this issue by aligning the duration validation rules.
    2. Open a separate RFC/issue for a project-pack rewrite, covering the new vs old format, how old .canvas archives are read, and whether the new file is a full project backup or a portable canvas.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

type:bugSomething is broken

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions