- MockHostAdapter (tests/mock_host): in-memory editing model with opaque entity ids, id invalidation, token-bucket rate limiting, snapshot/restore with cap, event subscription, and a stdlib-zlib PNG encoder producing time-varying non-black frames - HostToolExecutor: all 26 tool handlers mapped to HostAdapter calls, snapshot orchestration for mutating batches, concurrent scan_timeline frame pipeline with token bucket and RateLimited backoff, undo_session() restoring per-batch snapshots in reverse, get_params min/max/choices reflection - Web UI (core[webui] extra): FastAPI + single-page vanilla JS, chat log, pending-confirmation list with approve/reject endpoints (timeout defaults to reject), progress, undo-session, snapshot management; SSE push + POST, bound to 127.0.0.1 - Host error types (HostError/EntityNotFound/RateLimited) and shared TokenBucket - P2 acceptance chain: import -> place -> split -> ripple delete -> effect + param -> frame verification (non-black, pixel changes at cut and after effect) -> undo_session restores original state Tests: 129 passed via uv run pytest (no network)
140 lines
3.5 KiB
Python
140 lines
3.5 KiB
Python
"""Mock 宿主内存剪辑模型(tests/mock_host 可 import 包)。
|
||
|
||
形状对齐计划文档 §3.1 / §3.3:
|
||
- 不透明 id(``EntityId``),删除后失效(再访问抛 ``EntityNotFound``);
|
||
- clip 携带 footage 引用、in/out(素材内入出点)、media_in、enabled;
|
||
- clip 上挂 effects(node id + 参数),参数带 min/max/choices;
|
||
- sequence 含 fps 有理数、video/audio 轨道、markers、播放状态;
|
||
- snapshot = 对 ``MockState`` 的深拷贝(带时间戳与会话 id)。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass, field
|
||
from typing import Any
|
||
|
||
from quercus_core.types import EntityId, Rational
|
||
|
||
|
||
@dataclass
|
||
class MockKeyframe:
|
||
"""效果参数关键帧(时间 + 值)。"""
|
||
|
||
time: Rational
|
||
value: float
|
||
|
||
|
||
@dataclass
|
||
class MockEffect:
|
||
"""片段上的效果节点。params 值需满足 effects.py 注册表的 min/max/choices。"""
|
||
|
||
node_id: EntityId
|
||
effect_type: str
|
||
preset: str = ""
|
||
params: dict[str, Any] = field(default_factory=dict)
|
||
keyframes: dict[str, list[MockKeyframe]] = field(default_factory=dict)
|
||
|
||
|
||
@dataclass
|
||
class MockTransition:
|
||
"""片段转场(mock 仅记录,不参与像素渲染)。"""
|
||
|
||
transition_type: str
|
||
duration: Rational = Rational(0)
|
||
|
||
|
||
@dataclass
|
||
class MockClip:
|
||
"""时间线片段。media_in/media_out 为素材内入出点(源区间)。"""
|
||
|
||
id: EntityId
|
||
name: str
|
||
footage_id: str
|
||
track_index: int
|
||
media_in: Rational
|
||
media_out: Rational
|
||
start: Rational # 时间线上位置
|
||
enabled: bool = True
|
||
effects: list[MockEffect] = field(default_factory=list)
|
||
transition: MockTransition | None = None
|
||
|
||
@property
|
||
def duration(self) -> Rational:
|
||
return self.media_out - self.media_in
|
||
|
||
|
||
@dataclass
|
||
class MockTrack:
|
||
"""时间线轨道。"""
|
||
|
||
index: int
|
||
kind: str = "video" # video | audio | subtitle
|
||
clips: list[MockClip] = field(default_factory=list)
|
||
|
||
|
||
@dataclass
|
||
class MockMarker:
|
||
"""时间线标记(可携带 AI 元数据 custom_data)。"""
|
||
|
||
id: EntityId
|
||
time: Rational
|
||
label: str = ""
|
||
color: str = "yellow"
|
||
custom_data: dict = field(default_factory=dict)
|
||
|
||
|
||
@dataclass
|
||
class MockSequence:
|
||
"""时间线序列。fps 为有理数(对齐决策 D5)。"""
|
||
|
||
id: EntityId
|
||
name: str
|
||
fps: Rational
|
||
tracks: list[MockTrack] = field(default_factory=list)
|
||
markers: list[MockMarker] = field(default_factory=list)
|
||
playhead: Rational = Rational(0)
|
||
playing: bool = False
|
||
|
||
|
||
@dataclass
|
||
class MockFootage:
|
||
"""媒体池素材(id 不透明,内容由 frames.py 按 id 确定性渲染)。"""
|
||
|
||
id: EntityId
|
||
name: str
|
||
path: str
|
||
duration: Rational
|
||
width: int = 1920
|
||
height: int = 1080
|
||
fps: Rational = Rational(25, 1)
|
||
|
||
|
||
@dataclass
|
||
class MockProject:
|
||
"""工程:name / modified(计划文档 §3.3 的工程状态)。"""
|
||
|
||
name: str = "未命名工程"
|
||
path: str = ""
|
||
modified: bool = False
|
||
|
||
|
||
@dataclass
|
||
class MockState:
|
||
"""宿主全部状态;快照 = 对它的深拷贝。"""
|
||
|
||
project: MockProject = field(default_factory=MockProject)
|
||
footage: dict[str, MockFootage] = field(default_factory=dict)
|
||
sequences: dict[str, MockSequence] = field(default_factory=dict)
|
||
current_seq_id: EntityId | None = None
|
||
|
||
|
||
@dataclass
|
||
class MockSnapshot:
|
||
"""一份时间线快照(带时间戳与会话 id,有上限见 Limits.max_snapshot_count)。"""
|
||
|
||
id: str
|
||
session_id: str
|
||
created_at: str
|
||
label: str
|
||
action_count: int
|
||
state: Any # deepcopy(MockState)
|