Repository navigation
Open
Labels
type:bugSomething is brokenSomething is broken
Description
问题与影响
导入已导出的 .canvas 画布归档后,若工程内的时间线含有从属素材的片段,界面弹出:
时间线片段超出实际素材时长。
导入过程中断,用户无法把自己刚导出的画布在另一台机器或另一个工程中恢复。源工程本身是合法的——它能正常导出,说明素材与片段的时长关系在导出侧已通过校验。
复现步骤
- 新建工程,上传一段视频素材(时长记为 T),加入时间线,使片段的
outPoint等于 T。 - 通过画布工具栏「导出画布」得到
.canvas文件。 - 在任意工程中点击「导入画布」,选择该文件。
- 导入时前端会把时间线文档连同素材登记到当前工程,随后保存
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
因此报错对应两条可能的触发路径,本次复现确认了机制但未确定用户数据落在哪一条:
- 素材时长在导入后未知。 归档导入会重新登记素材,若探测拿不到时长(特殊容器、探测失败等),
durationSeconds为None,此时任何非图片片段都会被拒,与outPoint多小无关。这条路径与片段内容无关,最容易造成「同样的画布在别的机器导不进来」。 - 探测时长与片段的
outPoint存在浮点偏差。 时间线创建片段时把outPoint直接取为素材时长(见timeline.ts),重新探测得到的时长若略小于原值,超出部分即触发 1e-6 容差。
按上述机制,这两条都指向同一个设计问题:客户端允许「时长未知」的片段通过,服务端却把它当作致命错误,且两端比较的时长来源不同。
验收标准
- 导入一个
outPoint等于素材时长的正常画布归档,不再出现「时间线片段超出实际素材时长。」,导入成功。 - 素材时长无法探测时,导入行为与客户端
dataBoundary的判定一致(或给出指向素材本身的明确提示,而不是笼统的越界错误)。 - 真正越界的片段(
outPoint明显超过素材时长)仍然被拒绝,不会被静默截断。 - 图片片段不受影响:图片没有源时长,应继续跳过该比较。
Metadata
Metadata
Assignees
Labels
type:bugSomething is brokenSomething is broken
Activity
hsliuustc0106 commentedon Nov 10, 2026
#256 itself does not need a breaking format change. This issue is a client/server mismatch on timeline duration validation: the server
_validate_timelinepath comparesclip.outPointagainst a re-probedasset.metadata.durationSecondsand rejects an unknown duration or an overflow beyond1e-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
.canvasfile 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
.canvasfiles 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:
.canvasarchives are read, and whether the new file is a full project backup or a portable canvas.