Files
Mike-Solar 1b66a70859 P2: tool adapter layer, mock host, frame pipeline, and local web UI
- 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)
2026-08-25 02:40:43 +08:00

140 lines
3.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)