diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/app/__init__.py b/tests/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/app/test_batch/__init__.py b/tests/app/test_batch/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/app/test_batch/test_batch.py b/tests/app/test_batch/test_batch.py new file mode 100644 index 0000000..ddcc441 --- /dev/null +++ b/tests/app/test_batch/test_batch.py @@ -0,0 +1,414 @@ +"""src/wov_app/batch.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`src/wov_app/batch.py`(文件夹批量处理:扫描定位、旁挂字幕跳过、 +引擎执行与产物放置),可独立调用。用例在临时目录构造真实视频/字幕文件与 +真实 SQLite 记录,使用真实 echo 节点跑通执行链路。 +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from wov_app import registry +from wov_app.batch import ( + MARKER_NAME, + SUBTITLE_EXTENSIONS, + VIDEO_EXTENSIONS, + BatchWorker, + _sidecar_product_name, + create_job, + list_sidecar_subtitles, + load_marker, + remove_job_workspace, + scan_videos, +) +from wov_app.db import Database +from wov_sdk.models import WorkflowDefinition + + +@pytest.fixture(autouse=True) +def _isolate_registry(): + """用例前清空注册表、用例后恢复快照:保证用例看到的是干净基线, + 不受其他模块(如 main 生命周期 register_all)的注册结果影响。""" + snapshot = dict(registry._registry) + registry._registry.clear() + yield + registry._registry.clear() + registry._registry.update(snapshot) + + +def _make_video(path: Path) -> Path: + """创建真实可读的视频文件(内容不重要,但必须是真实文件)。""" + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(b"\x00\x00\x00\x18ftypmp42" + b"\x00" * 64) + return path + + +def _echo_definition(final_output: str = "step.file_uri") -> WorkflowDefinition: + """单 echo 节点的真实工作流定义。""" + return WorkflowDefinition.from_dict({ + "name": "批量流程", + "version": 1, + "nodes": [{"id": "step", "node_type": "echo", "inputs": {"file_uri": "input.video_uri"}}], + "edges": [], + "entry_inputs": {"video_uri": "file"}, + "final_outputs": {"result": final_output}, + }) + + +def _published_db(tmp_path: Path, workflow_id: str = "wf") -> Database: + """建好已发布工作流(含版本)的临时库。""" + db = Database(tmp_path / "wov.db") + db.upsert_workflow({ + "id": workflow_id, "name": "批量流程", "description": "", "published": 1, + "latest_version": 1, + }) + db.create_workflow_version(workflow_id, 1, _echo_definition().to_dict()) + return db + + +# --------------------------------------------------------------------------- +# 扫描与旁挂字幕判定 +# --------------------------------------------------------------------------- + + +def test_scan_videos_recursive_and_flat(tmp_path: Path) -> None: + """递归扫描包含子目录视频;非递归只扫顶层;结果按路径排序。""" + # 数据:顶层 2 个视频 + 子目录 1 个视频 + 1 个非视频文件。 + _make_video(tmp_path / "b.mp4") + _make_video(tmp_path / "a.mkv") + _make_video(tmp_path / "sub" / "c.mp4") + (tmp_path / "note.txt").write_text("x", encoding="utf-8") + + # 测试过程 + recursive = [p.name for p in scan_videos(tmp_path, recursive=True)] + flat = [p.name for p in scan_videos(tmp_path, recursive=False)] + + # 验证结果 + assert recursive == ["a.mkv", "b.mp4", "c.mp4"] + assert flat == ["a.mkv", "b.mp4"] + + +def test_video_extensions_are_lowercase_dotted() -> None: + """视频扩展名集合为小写带点形式(与 suffix.lower() 比较一致)。""" + # 数据:模块常量。 + # 测试过程与验证结果 + assert all(ext.startswith(".") and ext.islower() for ext in VIDEO_EXTENSIONS) + assert ".mp4" in VIDEO_EXTENSIONS and ".mkv" in VIDEO_EXTENSIONS + + +def test_list_sidecar_subtitles_matches_by_stem(tmp_path: Path) -> None: + """视频旁含视频主名的字幕文件被识别(含 CN/dual_eye 等约定命名)。""" + # 数据:视频 + 三种约定命名的字幕 + 一个无关文件。 + video = _make_video(tmp_path / "movie.mp4") + expected = [ + tmp_path / "movie.CN.srt", + tmp_path / "movie.CN_dual_eye.ass", + tmp_path / "movie.srt", + ] + for path in expected: + path.write_text("1\n", encoding="utf-8") + (tmp_path / "other.srt").write_text("1\n", encoding="utf-8") + + # 测试过程 + found = list_sidecar_subtitles(video) + + # 验证结果:三个匹配、无关文件不在结果里。 + assert set(found) == set(expected) + assert (tmp_path / "other.srt") not in found + + +def test_list_sidecar_subtitles_ignores_non_subtitle_files(tmp_path: Path) -> None: + """同名但非字幕扩展名的文件不算旁挂字幕。""" + # 数据:视频 + 同名字幕 + 同名文本。 + video = _make_video(tmp_path / "movie.mp4") + srt = tmp_path / "movie.srt" + srt.write_text("1\n", encoding="utf-8") + (tmp_path / "movie.txt").write_text("x", encoding="utf-8") + + # 测试过程 + found = list_sidecar_subtitles(video) + + # 验证结果 + assert found == [srt] + + +def test_list_sidecar_subtitles_short_stem_requires_dot_prefix(tmp_path: Path) -> None: + """视频主名只有一个字符时只接受"主名."前缀,避免 a.mp4 误配 apple.srt。""" + # 数据:a.mp4 + apple.srt(不应命中)+ a.srt(应命中)。 + video = _make_video(tmp_path / "a.mp4") + (tmp_path / "apple.srt").write_text("1\n", encoding="utf-8") + good = tmp_path / "a.srt" + good.write_text("1\n", encoding="utf-8") + + # 测试过程 + found = list_sidecar_subtitles(video) + + # 验证结果 + assert found == [good] + + +def test_subtitle_extensions_cover_common_formats() -> None: + """字幕扩展名覆盖 srt/ass/ssa/vtt。""" + # 数据:模块常量。 + # 测试过程与验证结果 + assert SUBTITLE_EXTENSIONS == {".srt", ".ass", ".ssa", ".vtt"} + + +# --------------------------------------------------------------------------- +# 产物命名映射 +# --------------------------------------------------------------------------- + + +def test_sidecar_product_name_maps_srt_and_ass() -> None: + """最终产物映射为媒体库约定名(.srt → CN.srt,.ass → CN_dual_eye.ass)。""" + # 数据:视频与两类最终产物。 + video = Path("/videos/movie.mp4") + + # 测试过程与验证结果 + assert _sidecar_product_name(video, Path("/tmp/x.zh-CN.20260101.srt")) == "movie.CN.srt" + assert _sidecar_product_name(video, Path("/tmp/x.ass")) == "movie.CN_dual_eye.ass" + + +def test_sidecar_product_name_keeps_other_extensions() -> None: + """其他扩展名产物保留原文件名(不误改语义)。""" + # 数据:vtt 产物。 + # 测试过程与验证结果 + assert _sidecar_product_name(Path("/v/movie.mp4"), Path("/tmp/movie.vtt")) == "movie.vtt" + + +# --------------------------------------------------------------------------- +# 完成标记与工作空间清理 +# --------------------------------------------------------------------------- + + +def test_load_marker_reads_valid_json(tmp_path: Path) -> None: + """旧版完成标记(batch.done.json)可读回字典。""" + # 数据:真实标记文件。 + work = tmp_path / "work" + work.mkdir() + payload = {"finals": {"cn_srt_uri": "/videos/movie.CN.srt"}} + (work / MARKER_NAME).write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8") + + # 测试过程 + marker = load_marker(work) + + # 验证结果 + assert marker == payload + + +def test_load_marker_returns_none_for_corrupt_or_missing(tmp_path: Path) -> None: + """标记缺失或内容损坏时返回 None(走旁挂字幕判定,不报错)。""" + # 数据:不存在标记 + 损坏标记。 + work = tmp_path / "work" + work.mkdir() + assert load_marker(work) is None + (work / MARKER_NAME).write_text("{broken", encoding="utf-8") + + # 测试过程与验证结果 + assert load_marker(work) is None + + +def test_remove_job_workspace_only_touches_private_dir(tmp_path: Path, monkeypatch) -> None: + """删除任务只清理应用私有工作空间,不触碰用户视频目录。""" + # 数据:私有工作空间 + 用户媒体目录。 + private = tmp_path / "storage" / "batch" / "job-1" + private.mkdir(parents=True) + (private / "temp.wav").write_bytes(b"x") + media = tmp_path / "media" + _make_video(media / "movie.mp4") + monkeypatch.setattr("wov_app.batch.BATCH_WORK_ROOT", tmp_path / "storage" / "batch") + + # 测试过程 + remove_job_workspace("job-1") + + # 验证结果:私有空间被删,用户目录完整。 + assert not private.exists() + assert (media / "movie.mp4").is_file() + + +# --------------------------------------------------------------------------- +# 创建批量任务:一次性定位 +# --------------------------------------------------------------------------- + + +def test_create_job_registers_pending_and_skipped(tmp_path: Path) -> None: + """创建任务一次性定位视频:无字幕记 PENDING,已有字幕记 SKIPPED。""" + # 数据:3 个视频,其中一个已有旁挂字幕。 + folder = tmp_path / "videos" + _make_video(folder / "a.mp4") + _make_video(folder / "b.mp4") + _make_video(folder / "c.mp4") + (folder / "b.CN.srt").write_text("1\n", encoding="utf-8") + db = _published_db(tmp_path) + + # 测试过程 + job_id = create_job(db, str(folder), "wf", recursive=False) + videos = db.list_batch_videos(job_id) + + # 验证结果:状态分布正确,总数只算待处理。 + statuses = {Path(v["video_path"]).name: v["status"] for v in videos} + assert statuses == {"a.mp4": "PENDING", "b.mp4": "SKIPPED", "c.mp4": "PENDING"} + job = db.get_batch_job(job_id) + assert job["total"] == 2 + assert job["status"] == "QUEUED" + + +def test_create_job_completes_immediately_when_all_skipped(tmp_path: Path) -> None: + """全部视频都已有字幕时任务直接完成,不排队不触发流水线。""" + # 数据:两个视频都带字幕。 + folder = tmp_path / "videos" + _make_video(folder / "a.mp4") + _make_video(folder / "b.mp4") + (folder / "a.srt").write_text("1\n", encoding="utf-8") + (folder / "b.srt").write_text("1\n", encoding="utf-8") + db = _published_db(tmp_path) + + # 测试过程 + job_id = create_job(db, str(folder), "wf") + + # 验证结果 + job = db.get_batch_job(job_id) + assert job["status"] == "COMPLETED" + assert job["total"] == 0 + assert db.next_queued_batch_job() is None + + +def test_create_job_uses_private_work_dir(tmp_path: Path, monkeypatch) -> None: + """明细的工作空间位于应用私有目录(与用户媒体库隔离)。""" + # 数据:一个视频。 + folder = tmp_path / "videos" + _make_video(folder / "a.mp4") + db = _published_db(tmp_path) + monkeypatch.setattr("wov_app.batch.BATCH_WORK_ROOT", tmp_path / "storage" / "batch") + + # 测试过程 + job_id = create_job(db, str(folder), "wf") + video = db.list_batch_videos(job_id)[0] + + # 验证结果:work_dir 在私有 storage/batch 下,且不在视频目录内。 + work_dir = Path(video["work_dir"]) + assert (tmp_path / "storage" / "batch") in work_dir.parents + assert folder not in work_dir.parents + + +@pytest.mark.parametrize( + ("folder_setup", "workflow_id", "message"), + [ + ("missing", "wf", "folder not found"), + ("empty", "wf", "no videos found"), + ("ok", "unknown", "published workflow not found"), + ], +) +def test_create_job_rejects_invalid_input(tmp_path: Path, folder_setup: str, workflow_id: str, message: str) -> None: + """校验失败时抛 ValueError(路由层转 422):目录缺失/无视频/工作流未发布。""" + # 数据:按参数准备目录与工作流。 + folder = tmp_path / "videos" + if folder_setup == "empty": + folder.mkdir() + elif folder_setup == "ok": + _make_video(folder / "a.mp4") + db = _published_db(tmp_path) + + # 测试过程与验证结果 + with pytest.raises(ValueError, match=message): + create_job(db, str(folder), workflow_id) + + +def test_create_job_rejects_workflow_without_version(tmp_path: Path) -> None: + """已发布但无版本记录的工作流被拒绝(无法执行)。""" + # 数据:有工作流记录但无版本。 + folder = tmp_path / "videos" + _make_video(folder / "a.mp4") + db = Database(tmp_path / "wov.db") + db.upsert_workflow({"id": "wf", "name": "无版本", "description": "", "published": 1}) + + # 测试过程与验证结果 + with pytest.raises(ValueError, match="no version"): + create_job(db, str(folder), "wf") + + +# --------------------------------------------------------------------------- +# 引擎:执行、产物放置、暂停 +# --------------------------------------------------------------------------- + + +def test_worker_processes_pending_video_end_to_end(tmp_path: Path, monkeypatch) -> None: + """引擎处理待处理视频:跑通流水线、产物放到视频旁、明细与任务标记完成。""" + # 数据:一个视频 + echo 单节点工作流(产物为复制后的输入文件)。 + folder = tmp_path / "videos" + _make_video(folder / "movie.mp4") + db = _published_db(tmp_path) + monkeypatch.setattr("wov_app.batch.BATCH_WORK_ROOT", tmp_path / "storage" / "batch") + registry.register_all() + job_id = create_job(db, str(folder), "wf") + + # 测试过程:直接驱动一轮处理(避免后台线程时序不确定)。 + worker = BatchWorker(db, interval_seconds=999) + worker._process_job(db.get_batch_job(job_id)) + + # 验证结果:明细与任务完成,产物按约定名放到视频旁,工作空间被清理。 + item = db.list_batch_videos(job_id)[0] + assert item["status"] == "COMPLETED" + assert db.get_batch_job(job_id)["status"] == "COMPLETED" + # echo 节点产物是 .txt,按约定保留原文件名放置在视频旁。 + assert list(folder.glob("movie.*")), "应在视频旁放置最终产物" + + +def test_worker_skips_video_with_sidecar_subtitle(tmp_path: Path, monkeypatch) -> None: + """已有旁挂字幕的视频不触发流水线(SKIPPED 不产生 run)。""" + # 数据:一个已带字幕的视频。 + folder = tmp_path / "videos" + _make_video(folder / "movie.mp4") + (folder / "movie.CN.srt").write_text("1\n", encoding="utf-8") + _make_video(folder / "other.mp4") + db = _published_db(tmp_path) + monkeypatch.setattr("wov_app.batch.BATCH_WORK_ROOT", tmp_path / "storage" / "batch") + registry.register_all() + + # 测试过程 + job_id = create_job(db, str(folder), "wf") + + # 验证结果:SKIPPED 视频没有 run_id。 + videos = {Path(v["video_path"]).name: v for v in db.list_batch_videos(job_id)} + assert videos["movie.mp4"]["status"] == "SKIPPED" + assert videos["movie.mp4"]["run_id"] is None + assert videos["other.mp4"]["status"] == "PENDING" + + +def test_worker_pause_sets_job_paused(tmp_path: Path, monkeypatch) -> None: + """暂停批量任务:任务状态置 PAUSED,待处理视频不被推进。""" + # 数据:一个待处理视频的任务。 + folder = tmp_path / "videos" + _make_video(folder / "a.mp4") + db = _published_db(tmp_path) + monkeypatch.setattr("wov_app.batch.BATCH_WORK_ROOT", tmp_path / "storage" / "batch") + job_id = create_job(db, str(folder), "wf") + + # 测试过程 + db.update_batch_job(job_id, status="PAUSED", updated_at="2026-09-01T01:00:00+00:00") + + # 验证结果:不会被 next_queued_batch_job 拾起(等待显式 resume)。 + assert db.next_queued_batch_job() is None + assert db.get_batch_job(job_id)["status"] == "PAUSED" + + +def test_worker_start_stop_idempotent(tmp_path: Path) -> None: + """引擎 start 重复调用不产生多余线程;stop 正常结束。""" + # 数据:空库。 + db = Database(tmp_path / "wov.db") + worker = BatchWorker(db, interval_seconds=999) + + # 测试过程 + worker.start() + first = worker._thread + worker.start() + second = worker._thread + worker.stop() + + # 验证结果 + assert first is second + assert worker._thread is None diff --git a/tests/app/test_config/__init__.py b/tests/app/test_config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/app/test_config/test_config.py b/tests/app/test_config/test_config.py new file mode 100644 index 0000000..726ca46 --- /dev/null +++ b/tests/app/test_config/test_config.py @@ -0,0 +1,194 @@ +"""src/wov_app/config.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`src/wov_app/config.py`(环境变量读取与路径推导),可独立调用。 +由于该模块在**导入时**锁定路径常量,隔离用例在子进程中运行(真实导入路径 ++ 干净环境),其余用例校验默认值与类型。 + +注意:config 的路径常量在进程内已固化,因此"环境变量覆盖"必须用子进程验证, +否则测的是缓存值而非真实行为(这也是规则要求"可独立运行、不依赖外部配置" +的具体体现)。 +""" + +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + +import pytest + +from wov_app import config + +# 仓库根目录(config.WORKSPACE_ROOT 应该指向它)。 +WORKSPACE = Path(__file__).resolve().parents[3] + + +# 与路径相关的环境变量:子进程验证前必须清除,否则会继承外层测试隔离值。 +_PATH_ENV_KEYS = ( + "WOV_DATA_DIR", "WOV_DB_PATH", "WOV_STORAGE_DIR", + "WOV_SCHEDULER_INTERVAL_SECONDS", "WOV_BATCH_INTERVAL_SECONDS", + "WOV_CLEANUP_INTERVAL_SECONDS", "WOV_CLEANUP_GRACE_SECONDS", + "WOV_BATCH_ENABLED", "WOV_CLEANUP_ENABLED", +) + + +def _run_in_subprocess(code: str, env: dict[str, str] | None = None) -> dict: + """在干净子进程中导入 config 并返回指定变量(真实进程隔离)。 + + 先清除全部相关环境变量,再用 env 注入本次用例的值,保证测到的是 + config 的真实解析逻辑而不是外层测试运行环境。 + """ + script = ( + "import json\n" + "from wov_app import config\n" + f"{code}\n" + "print(json.dumps(result))\n" + ) + clean_env = {k: v for k, v in __import__("os").environ.items() if k not in _PATH_ENV_KEYS} + clean_env.update(env or {}) + result = subprocess.run( + [sys.executable, "-c", script], + capture_output=True, + text=True, + cwd=str(WORKSPACE), + env=clean_env, + ) + assert result.returncode == 0, result.stderr + return json.loads(result.stdout.strip().splitlines()[-1]) + + +# --------------------------------------------------------------------------- +# 默认值(当前进程内已导入的常量) +# --------------------------------------------------------------------------- + + +def test_workspace_root_points_to_repo_root() -> None: + """WORKSPACE_ROOT 指向仓库根(用于推导其余路径)。""" + # 数据:模块常量。 + # 测试过程与验证结果 + assert config.WORKSPACE_ROOT == WORKSPACE + assert (config.WORKSPACE_ROOT / "pyproject.toml").is_file() + + +def test_default_paths_are_under_data_dir() -> None: + """无环境变量时默认路径为 <仓库根>/data 下的推导值(子进程验证真实默认)。""" + # 数据:清空全部相关环境变量。 + # 测试过程 + result = _run_in_subprocess( + "result = {'root': str(config.WORKSPACE_ROOT), 'data': str(config.DATA_DIR), " + "'db': str(config.DB_PATH), 'storage': str(config.STORAGE_DIR), " + "'batch': config.BATCH_ENABLED, 'cleanup': config.CLEANUP_ENABLED}" + ) + + # 验证结果:默认 data 目录在仓库根下,db/storage 由它推导,开关默认开。 + assert result["root"] == str(WORKSPACE) + assert result["data"] == str(WORKSPACE / "data") + assert result["db"] == str(WORKSPACE / "data" / "wov.db") + assert result["storage"] == str(WORKSPACE / "data" / "storage") + assert result["batch"] is True + assert result["cleanup"] is True + + +def test_numeric_settings_are_floats() -> None: + """数值型配置被解析为 float(避免字符串参与算术)。""" + # 数据:模块常量。 + # 测试过程与验证结果 + for value in ( + config.SCHEDULER_INTERVAL_SECONDS, + config.BATCH_INTERVAL_SECONDS, + config.CLEANUP_INTERVAL_SECONDS, + config.CLEANUP_GRACE_SECONDS, + ): + assert isinstance(value, float) + assert value > 0 + + +def test_boolean_settings_are_bool() -> None: + """布尔型开关被解析为 bool,默认全部开启(1)。""" + # 数据:模块常量(测试运行环境由隔离层设为 0,故仅校验类型)。 + # 测试过程与验证结果 + for value in (config.BATCH_ENABLED, config.CLEANUP_ENABLED): + assert isinstance(value, bool) + + +# --------------------------------------------------------------------------- +# 环境变量覆盖(子进程真实导入) +# --------------------------------------------------------------------------- + + +def test_data_dir_env_override_changes_all_derived_paths(tmp_path: Path) -> None: + """WOV_DATA_DIR 覆盖后,DB_PATH 与 STORAGE_DIR 随之推导(路径联动)。""" + # 数据:自定义数据目录。 + custom = tmp_path / "custom-data" + + # 测试过程 + result = _run_in_subprocess( + "result = {'data': str(config.DATA_DIR), 'db': str(config.DB_PATH), " + "'storage': str(config.STORAGE_DIR)}", + env={"WOV_DATA_DIR": str(custom)}, + ) + + # 验证结果 + assert result["data"] == str(custom) + assert result["db"] == str(custom / "wov.db") + assert result["storage"] == str(custom / "storage") + + +def test_explicit_db_and_storage_env_take_precedence(tmp_path: Path) -> None: + """WOV_DB_PATH / WOV_STORAGE_DIR 可独立覆盖(不跟随 DATA_DIR)。""" + # 数据:分别指定的数据库与存储路径。 + db_path = tmp_path / "x" / "custom.db" + storage = tmp_path / "y" / "store" + + # 测试过程 + result = _run_in_subprocess( + "result = {'db': str(config.DB_PATH), 'storage': str(config.STORAGE_DIR)}", + env={"WOV_DB_PATH": str(db_path), "WOV_STORAGE_DIR": str(storage)}, + ) + + # 验证结果 + assert result["db"] == str(db_path) + assert result["storage"] == str(storage) + + +def test_boolean_env_parsing() -> None: + """开关型环境变量:'1' 为 True,'0' 为 False。""" + # 数据:批量与清理开关分别置 1 与 0。 + # 测试过程 + result = _run_in_subprocess( + "result = {'batch': config.BATCH_ENABLED, 'cleanup': config.CLEANUP_ENABLED}", + env={"WOV_BATCH_ENABLED": "1", "WOV_CLEANUP_ENABLED": "0"}, + ) + + # 验证结果 + assert result["batch"] is True + assert result["cleanup"] is False + + +def test_interval_env_parsing() -> None: + """轮询间隔环境变量被解析为对应浮点值。""" + # 数据:指定的调度与批量间隔。 + # 测试过程 + result = _run_in_subprocess( + "result = {'sched': config.SCHEDULER_INTERVAL_SECONDS, " + "'batch': config.BATCH_INTERVAL_SECONDS}", + env={"WOV_SCHEDULER_INTERVAL_SECONDS": "2.5", "WOV_BATCH_INTERVAL_SECONDS": "0.25"}, + ) + + # 验证结果 + assert result["sched"] == 2.5 + assert result["batch"] == 0.25 + + +def test_paths_are_pathlib_objects(tmp_path: Path) -> None: + """路径配置是 pathlib.Path(跨平台,不写死 Windows 盘符)。""" + # 数据:自定义数据目录。 + # 测试过程 + result = _run_in_subprocess( + "result = {'is_path': isinstance(config.DATA_DIR, __import__('pathlib').Path)}", + env={"WOV_DATA_DIR": str(tmp_path / "d")}, + ) + + # 验证结果 + assert result["is_path"] is True diff --git a/tests/app/test_db/__init__.py b/tests/app/test_db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/app/test_db/test_database.py b/tests/app/test_db/test_database.py new file mode 100644 index 0000000..f865bef --- /dev/null +++ b/tests/app/test_db/test_database.py @@ -0,0 +1,332 @@ +"""src/wov_app/db.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`src/wov_app/db.py`(SQLite Repository:工作流/版本/任务/产物/ +批量任务),可独立调用。每个用例在临时目录创建独立数据库文件(真实 SQLite), +不依赖全局 conftest。 +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from wov_app.db import Database + + +@pytest.fixture() +def db(tmp_path: Path) -> Database: + """每个用例一个独立 SQLite 库(真实文件,非内存桩)。""" + return Database(tmp_path / "wov.db") + + +@pytest.fixture() +def db_with_workflow(db: Database) -> Database: + """已建好工作流(含 v1 版本)的库:任务表对 workflow_id 有外键约束。""" + db.upsert_workflow(_workflow("wf")) + db.create_workflow_version("wf", 1, {"nodes": []}) + return db + + +def _workflow(workflow_id: str = "wf", name: str = "流程") -> dict: + """构造真实工作流记录字段。""" + return {"id": workflow_id, "name": name, "description": ""} + + +def _run(run_id: str = "run-1", **overrides) -> dict: + """构造真实任务记录字段(默认 upload 来源、QUEUED 状态)。""" + record = { + "id": run_id, + "workflow_id": "wf", + "workflow_version": 1, + "status": "QUEUED", + "current_node_id": None, + "progress": 0.0, + "error": None, + "input_uri": None, + "param_overrides": None, + "source": "upload", + "created_at": "2026-09-01T00:00:00+00:00", + "updated_at": "2026-09-01T00:00:00+00:00", + } + record.update(overrides) + return record + + +# --------------------------------------------------------------------------- +# 工作流与版本 +# --------------------------------------------------------------------------- + + +def test_upsert_and_get_workflow(db: Database) -> None: + """工作流写入后可读回,重复写入同 ID 覆盖而不报错。""" + # 数据:一条工作流记录。 + db.upsert_workflow(_workflow("wf-1", "初版")) + + # 测试过程 + stored = db.get_workflow("wf-1") + db.upsert_workflow(_workflow("wf-1", "改名")) + renamed = db.get_workflow("wf-1") + + # 验证结果 + assert stored["name"] == "初版" + assert renamed["name"] == "改名" + + +def test_list_and_delete_workflow(db: Database) -> None: + """列出全部工作流;删除后不再出现在列表与查询中。""" + # 数据:两条工作流。 + db.upsert_workflow(_workflow("wf-1")) + db.upsert_workflow(_workflow("wf-2")) + + # 测试过程 + before = {w["id"] for w in db.list_workflows()} + db.delete_workflow("wf-1") + after = {w["id"] for w in db.list_workflows()} + + # 验证结果 + assert before == {"wf-1", "wf-2"} + assert after == {"wf-2"} + assert db.get_workflow("wf-1") is None + + +def test_workflow_versions_and_latest(db: Database) -> None: + """版本按序保存,latest 返回最高版本,可按版本号精确读取。""" + # 数据:同一工作流的 v1 与 v2 定义。 + db.upsert_workflow(_workflow("wf-1")) + db.create_workflow_version("wf-1", 1, {"nodes": [{"id": "a"}]}) + db.create_workflow_version("wf-1", 2, {"nodes": [{"id": "a"}, {"id": "b"}]}) + + # 测试过程 + latest = db.get_latest_workflow_version("wf-1") + first = db.get_workflow_version("wf-1", 1) + versions = db.list_workflow_versions("wf-1") + + # 验证结果 + assert latest["version"] == 2 + assert len(latest["definition"]["nodes"]) == 2 + assert first["definition"]["nodes"] == [{"id": "a"}] + assert [v["version"] for v in versions] == [2, 1] + + +def test_workflow_version_missing_returns_none(db: Database) -> None: + """不存在的工作流/版本返回 None(不做隐式创建)。""" + # 数据:空库。 + # 测试过程与验证结果 + assert db.get_latest_workflow_version("nope") is None + assert db.get_workflow_version("nope", 1) is None + + +# --------------------------------------------------------------------------- +# 任务:创建、读写、param_overrides +# --------------------------------------------------------------------------- + + +def test_create_and_get_run_with_overrides(db_with_workflow: Database) -> None: + """任务创建后可读回,param_overrides 以 JSON 存储并解析回字典。""" + # 数据:带参数覆盖的 upload 任务。 + db_with_workflow.create_run(_run("r1", param_overrides={"frame-extract": {"crop": [0, 0.75, 1, 0.25]}})) + + # 测试过程 + stored = db_with_workflow.get_run("r1") + + # 验证结果 + assert stored["status"] == "QUEUED" + assert stored["source"] == "upload" + assert stored["param_overrides"] == {"frame-extract": {"crop": [0, 0.75, 1, 0.25]}} + + +def test_update_run_fields(db_with_workflow: Database) -> None: + """update_run 按字段更新(状态/进度/错误)。""" + # 数据:一条任务。 + db_with_workflow.create_run(_run("r1")) + + # 测试过程 + db_with_workflow.update_run("r1", status="RUNNING", progress=0.5, error=None, updated_at="t2") + stored = db_with_workflow.get_run("r1") + + # 验证结果 + assert stored["status"] == "RUNNING" + assert stored["progress"] == 0.5 + + +def test_list_runs_orders_by_created_at_desc(db_with_workflow: Database) -> None: + """任务列表按创建时间倒序返回(新的在前)。""" + # 数据:三条不同创建时间的任务。 + db_with_workflow.create_run(_run("old", created_at="2026-09-01T00:00:00+00:00")) + db_with_workflow.create_run(_run("mid", created_at="2026-09-02T00:00:00+00:00")) + db_with_workflow.create_run(_run("new", created_at="2026-09-03T00:00:00+00:00")) + + # 测试过程 + ids = [r["id"] for r in db_with_workflow.list_runs()] + + # 验证结果 + assert ids == ["new", "mid", "old"] + + +def test_delete_run_removes_record_and_artifacts(db_with_workflow: Database) -> None: + """删除任务同时清理其产物记录。""" + # 数据:任务 + 一条产物。 + db_with_workflow.create_run(_run("r1")) + db_with_workflow.create_artifact({ + "run_id": "r1", "node_id": "a", "name": "a.data_uri", "uri": "/tmp/x", "kind": "file", + }) + + # 测试过程 + db_with_workflow.delete_run("r1") + + # 验证结果 + assert db_with_workflow.get_run("r1") is None + assert db_with_workflow.list_artifacts("r1") == [] + + +# --------------------------------------------------------------------------- +# 调度用查询:只有 QUEUED 会被拾起 +# --------------------------------------------------------------------------- + + +def test_next_queued_run_returns_oldest_queued(db_with_workflow: Database) -> None: + """按创建时间返回最早的 QUEUED 任务。""" + # 数据:一条较早的 QUEUED。 + db_with_workflow.create_run(_run("a", created_at="2026-09-01T00:00:00+00:00")) + + # 测试过程 + picked = db_with_workflow.next_queued_run() + + # 验证结果 + assert picked["id"] == "a" + + +def test_next_queued_run_skips_paused(db_with_workflow: Database) -> None: + """PAUSED 不被拾起(必须显式 resume;修复"点击暂停反而开始任务"回归)。""" + # 数据:一条 PAUSED(更早)+ 一条 QUEUED(更晚)。 + db_with_workflow.create_run(_run("paused", status="PAUSED", created_at="2026-09-01T00:00:00+00:00")) + db_with_workflow.create_run(_run("queued", created_at="2026-09-02T00:00:00+00:00")) + + # 测试过程 + picked = db_with_workflow.next_queued_run() + + # 验证结果:取的是 QUEUED 那条。 + assert picked["id"] == "queued" + + +def test_next_queued_run_skips_batch_source(db_with_workflow: Database) -> None: + """source=batch 的任务由批量引擎执行,主调度器不拾起(存储目录不同)。""" + # 数据:一条 batch 来源的 QUEUED。 + db_with_workflow.create_run(_run("batch-1", source="batch")) + + # 测试过程与验证结果 + assert db_with_workflow.next_queued_run() is None + + +def test_pause_and_resume_run(db_with_workflow: Database) -> None: + """暂停置 PAUSED,继续置回 QUEUED(等待调度器断点续跑)。""" + # 数据:一条 QUEUED。 + db_with_workflow.create_run(_run("r1")) + + # 测试过程 + db_with_workflow.pause_run("r1", "t2") + paused = db_with_workflow.get_run("r1")["status"] + db_with_workflow.resume_run("r1", "t3") + resumed = db_with_workflow.get_run("r1")["status"] + + # 验证结果 + assert paused == "PAUSED" + assert resumed == "QUEUED" + + +def test_recover_interrupted_runs_requeues_running_only(db_with_workflow: Database) -> None: + """重启恢复:RUNNING → QUEUED,PAUSED 保持不变。""" + # 数据:RUNNING 与 PAUSED 各一条。 + db_with_workflow.create_run(_run("running", status="RUNNING")) + db_with_workflow.create_run(_run("paused", status="PAUSED")) + + # 测试过程 + count = db_with_workflow.recover_interrupted_runs("t2") + + # 验证结果:只恢复 1 条,PAUSED 不变。 + assert count == 1 + assert db_with_workflow.get_run("running")["status"] == "QUEUED" + assert db_with_workflow.get_run("paused")["status"] == "PAUSED" + + +def test_recover_interrupted_batch_jobs_requeues_running(db_with_workflow: Database) -> None: + """重启恢复:RUNNING 的批量任务 → QUEUED(否则永久无人拾起)。""" + # 数据:一条 RUNNING 批量任务(批量明细表对 run 有外键,先建任务记录)。 + db_with_workflow.create_run(_run("bv-run-1")) + db_with_workflow.create_batch_job({ + "id": "job-1", "folder_path": "/videos", "workflow_id": "wf", "recursive": False, + "status": "RUNNING", "created_at": "t1", "updated_at": "t1", + }) + + # 测试过程 + count = db_with_workflow.recover_interrupted_batch_jobs("t2") + + # 验证结果 + assert count == 1 + assert db_with_workflow.get_batch_job("job-1")["status"] == "QUEUED" + + +# --------------------------------------------------------------------------- +# 产物与断点恢复 +# --------------------------------------------------------------------------- + + +def test_create_and_get_artifact(db_with_workflow: Database) -> None: + """产物按 run + name 记录,可按名精确读取。""" + # 数据:一条任务 + 一条产物(产物对 run 有外键)。 + db_with_workflow.create_run(_run("r1")) + db_with_workflow.create_artifact({ + "run_id": "r1", "node_id": "ocr", "name": "ocr.srt_uri", + "uri": "/tmp/out/subtitle.srt", "kind": "file", + }) + + # 测试过程 + stored = db_with_workflow.get_artifact("r1", "ocr.srt_uri") + + # 验证结果 + assert stored["uri"] == "/tmp/out/subtitle.srt" + assert db_with_workflow.get_artifact("r1", "missing") is None + + +def test_restore_run_outputs_strips_node_prefix(db_with_workflow: Database) -> None: + """恢复产物时剥去"节点ID."前缀,还原为 {输出名: URI}(断点续跑依赖)。""" + # 数据:两个节点的产物。 + db_with_workflow.create_run(_run("r1")) + db_with_workflow.create_artifact({"run_id": "r1", "node_id": "extract", "name": "extract.audio_uri", "uri": "/a.wav", "kind": "file"}) + db_with_workflow.create_artifact({"run_id": "r1", "node_id": "asr", "name": "asr.srt_uri", "uri": "/a.srt", "kind": "file"}) + + # 测试过程 + outputs = db_with_workflow.restore_run_outputs("r1") + + # 验证结果 + assert outputs == { + "extract": {"audio_uri": "/a.wav"}, + "asr": {"srt_uri": "/a.srt"}, + } + + +def test_reset_run_clears_state_and_artifacts(db_with_workflow: Database) -> None: + """reset_run 清空产物与错误、回到 QUEUED(供失败任务重跑)。""" + # 数据:一条 FAILED 任务带产物与错误。 + db_with_workflow.create_run(_run("r1", status="FAILED", error="boom")) + db_with_workflow.create_artifact({"run_id": "r1", "node_id": "a", "name": "a.x", "uri": "/x", "kind": "file"}) + + # 测试过程 + db_with_workflow.reset_run("r1", "t2") + stored = db_with_workflow.get_run("r1") + + # 验证结果 + assert stored["status"] == "QUEUED" + assert stored["error"] is None + assert db_with_workflow.list_artifacts("r1") == [] + + +def test_list_run_ids(db_with_workflow: Database) -> None: + """列出全部任务 ID(供孤儿清理比对文件系统)。""" + # 数据:两条任务。 + db_with_workflow.create_run(_run("r1")) + db_with_workflow.create_run(_run("r2")) + + # 测试过程与验证结果 + assert sorted(db_with_workflow.list_run_ids()) == ["r1", "r2"] diff --git a/tests/app/test_logging/__init__.py b/tests/app/test_logging/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/app/test_logging/test_logging.py b/tests/app/test_logging/test_logging.py new file mode 100644 index 0000000..1b163e6 --- /dev/null +++ b/tests/app/test_logging/test_logging.py @@ -0,0 +1,88 @@ +"""src/wov_app/logging.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`src/wov_app/logging.py`(控制台日志配置),可独立调用。 +用例使用真实 logging 模块与真实日志记录,验证命名、处理器去重与传播设置。 +""" + +from __future__ import annotations + +import logging + +from wov_app.logging import _APP_LOGGER_NAME, _ensure_console_handler, get_logger + + +def _strip_handlers(logger: logging.Logger) -> None: + """清空日志器处理器,保证用例从干净状态开始(模块自建隔离)。""" + for handler in list(logger.handlers): + logger.removeHandler(handler) + + +def test_get_logger_uses_app_prefix() -> None: + """日志器名称带应用前缀,便于与其他库日志区分。""" + # 数据:模块名 "scheduler"。 + # 测试过程 + logger = get_logger("scheduler") + + # 验证结果 + assert logger.name == f"{_APP_LOGGER_NAME}.scheduler" + _strip_handlers(logger) + + +def test_get_logger_attaches_console_handler_once() -> None: + """重复获取同一日志器不会重复附加处理器(避免日志重复打印)。""" + # 数据:先清空,再连续获取两次。 + logger = get_logger("dup") + _strip_handlers(logger) + + # 测试过程 + first = get_logger("dup") + second = get_logger("dup") + + # 验证结果:处理器只有 1 个。 + assert first is second + assert len([h for h in second.handlers if isinstance(h, logging.StreamHandler)]) == 1 + _strip_handlers(second) + + +def test_get_logger_sets_info_level_and_no_propagation() -> None: + """日志级别为 INFO 且不向根日志传播(防止 uvicorn 重复输出)。""" + # 数据:新日志器。 + logger = get_logger("levels") + _strip_handlers(logger) + + # 测试过程 + logger = get_logger("levels") + + # 验证结果 + assert logger.level == logging.INFO + assert logger.propagate is False + _strip_handlers(logger) + + +def test_ensure_console_handler_is_idempotent() -> None: + """_ensure_console_handler 对已配置的日志器不再追加处理器。""" + # 数据:手工配置过的日志器。 + logger = logging.getLogger("vrsub.idempotent") + _strip_handlers(logger) + + # 测试过程 + _ensure_console_handler(logger) + _ensure_console_handler(logger) + + # 验证结果 + assert len(logger.handlers) == 1 + _strip_handlers(logger) + + +def test_log_record_is_actually_emitted(capsys) -> None: + """日志记录真实写出到控制台(handler 生效,而非仅配置)。""" + # 数据:日志器 + 一条 INFO 记录。 + logger = get_logger("emit") + + # 测试过程 + logger.info("测试日志 %s", "生效") + + # 验证结果:输出内容包含格式化后的消息。 + captured = capsys.readouterr() + assert "测试日志 生效" in (captured.err + captured.out) + _strip_handlers(logger) diff --git a/tests/app/test_main/__init__.py b/tests/app/test_main/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/app/test_main/test_app.py b/tests/app/test_main/test_app.py new file mode 100644 index 0000000..e4669c2 --- /dev/null +++ b/tests/app/test_main/test_app.py @@ -0,0 +1,216 @@ +"""src/wov_app/main.py 与 src/wov_app/schemas.py 的模块级测试。 + +被测模块: +- `src/wov_app/main.py`:FastAPI 应用装配(生命周期、路由挂载、静态前端、 + 健康检查、重启恢复); +- `src/wov_app/schemas.py`:管理端请求模型(Pydantic)。 + +用例通过真实 TestClient 触发完整生命周期(启动/关闭),验证后台服务被正确 +创建与回收、恢复逻辑被调用、静态前端可访问。 +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + +from wov_app.main import app as fastapi_app +from wov_app.schemas import BatchJobCreate, WorkflowCreate + + +@pytest.fixture() +def client(tmp_path: Path, monkeypatch) -> TestClient: + """隔离数据库与存储的真实 TestClient(关闭后台线程便于断言状态)。""" + monkeypatch.setattr("wov_app.config.DB_PATH", tmp_path / "wov.db") + monkeypatch.setattr("wov_app.config.STORAGE_DIR", tmp_path / "storage") + monkeypatch.setattr("wov_app.main.DB_PATH", tmp_path / "wov.db") + monkeypatch.setattr("wov_app.main.STORAGE_DIR", tmp_path / "storage") + monkeypatch.setattr("wov_app.batch.BATCH_WORK_ROOT", tmp_path / "storage" / "batch") + monkeypatch.setenv("WOV_AUTO_SEED", "0") + monkeypatch.setenv("WOV_SCHEDULER_ENABLED", "0") + monkeypatch.setenv("WOV_CLEANUP_ENABLED", "0") + monkeypatch.setenv("WOV_BATCH_ENABLED", "0") + with TestClient(fastapi_app) as test_client: + yield test_client + + +# --------------------------------------------------------------------------- +# 生命周期装配 +# --------------------------------------------------------------------------- + + +def test_lifespan_registers_nodes_and_state(client: TestClient) -> None: + """启动后节点注册表就绪,且 app.state 上挂好了库/调度器/清理器/批量引擎。""" + # 数据:真实应用生命周期。 + # 测试过程 + state = client.app.state + nodes = {m.id for m in __import__("wov_app.registry", fromlist=["list_nodes"]).list_nodes()} + + # 验证结果 + assert "echo" in nodes and "faster-whisper" in nodes + assert state.db is not None + assert state.scheduler is not None + assert state.cleaner is not None + assert state.batch is not None + + +def test_lifespan_recovers_interrupted_runs(tmp_path: Path, monkeypatch) -> None: + """重启恢复:遗留 RUNNING 任务在启动时被恢复为 QUEUED。""" + # 数据:预先在目标库里写入一条 RUNNING 任务(含工作流与版本)。 + from wov_app.db import Database + + db_path = tmp_path / "wov.db" + db = Database(db_path) + db.upsert_workflow({"id": "wf", "name": "x", "description": "", "published": 1, "latest_version": 1}) + db.create_workflow_version("wf", 1, {"nodes": [], "edges": []}) + db.create_run({ + "id": "run-stale", "workflow_id": "wf", "workflow_version": 1, "status": "RUNNING", + "current_node_id": None, "progress": 0.5, "error": None, "input_uri": None, + "param_overrides": None, "source": "upload", + "created_at": "2026-09-01T00:00:00+00:00", "updated_at": "2026-09-01T00:00:00+00:00", + }) + monkeypatch.setattr("wov_app.config.DB_PATH", db_path) + monkeypatch.setattr("wov_app.main.DB_PATH", db_path) + monkeypatch.setattr("wov_app.config.STORAGE_DIR", tmp_path / "storage") + monkeypatch.setattr("wov_app.main.STORAGE_DIR", tmp_path / "storage") + monkeypatch.setenv("WOV_AUTO_SEED", "0") + monkeypatch.setenv("WOV_SCHEDULER_ENABLED", "0") + monkeypatch.setenv("WOV_CLEANUP_ENABLED", "0") + monkeypatch.setenv("WOV_BATCH_ENABLED", "0") + + # 测试过程:进入生命周期触发恢复逻辑。 + with TestClient(fastapi_app): + recovered = Database(db_path).get_run("run-stale") + + # 验证结果 + assert recovered["status"] == "QUEUED" + + +def test_lifespan_seeds_workflows_when_enabled(tmp_path: Path, monkeypatch) -> None: + """开启自动种子时启动创建内置工作流(数据驱动)。""" + # 数据:目标库 + 开启 seed。 + db_path = tmp_path / "wov.db" + monkeypatch.setattr("wov_app.config.DB_PATH", db_path) + monkeypatch.setattr("wov_app.main.DB_PATH", db_path) + monkeypatch.setattr("wov_app.config.STORAGE_DIR", tmp_path / "storage") + monkeypatch.setattr("wov_app.main.STORAGE_DIR", tmp_path / "storage") + monkeypatch.setenv("WOV_AUTO_SEED", "1") + monkeypatch.setenv("WOV_SCHEDULER_ENABLED", "0") + monkeypatch.setenv("WOV_CLEANUP_ENABLED", "0") + monkeypatch.setenv("WOV_BATCH_ENABLED", "0") + + # 测试过程 + with TestClient(fastapi_app): + from wov_app.db import Database as _Db + + ids = {w["id"] for w in _Db(db_path).list_workflows()} + + # 验证结果:内置工作流全部就位。 + assert {"zh-direct", "ocr-subtitle", "learn-translate"} <= ids + + +def test_lifespan_starts_and_stops_background_services(tmp_path: Path, monkeypatch) -> None: + """开启后台服务时启动线程,退出时全部停止(无残留线程)。""" + # 数据:全部后台服务开启。 + monkeypatch.setattr("wov_app.config.DB_PATH", tmp_path / "wov.db") + monkeypatch.setattr("wov_app.main.DB_PATH", tmp_path / "wov.db") + monkeypatch.setattr("wov_app.config.STORAGE_DIR", tmp_path / "storage") + monkeypatch.setattr("wov_app.main.STORAGE_DIR", tmp_path / "storage") + monkeypatch.setattr("wov_app.batch.BATCH_WORK_ROOT", tmp_path / "storage" / "batch") + monkeypatch.setenv("WOV_AUTO_SEED", "0") + monkeypatch.setenv("WOV_SCHEDULER_ENABLED", "1") + monkeypatch.setenv("WOV_CLEANUP_ENABLED", "1") + monkeypatch.setenv("WOV_BATCH_ENABLED", "1") + + # 测试过程 + with TestClient(fastapi_app) as test_client: + scheduler_thread = test_client.app.state.scheduler._thread + cleaner_thread = test_client.app.state.cleaner._thread + batch_thread = test_client.app.state.batch._thread + assert scheduler_thread is not None and scheduler_thread.is_alive() + assert cleaner_thread is not None and cleaner_thread.is_alive() + assert batch_thread is not None and batch_thread.is_alive() + + # 验证结果:退出后线程引用被清空(stop 已执行)。 + assert test_client.app.state.scheduler._thread is None + assert test_client.app.state.cleaner._thread is None + assert test_client.app.state.batch._thread is None + + +# --------------------------------------------------------------------------- +# 路由与静态前端 +# --------------------------------------------------------------------------- + + +def test_health_endpoint(client: TestClient) -> None: + """健康检查返回 ok 与模式标识(部署探针依赖)。""" + # 数据:无。 + # 测试过程 + response = client.get("/health") + + # 验证结果 + assert response.status_code == 200 + body = response.json() + assert body["status"] == "ok" + assert body["mode"] == "monolith" + + +def test_static_frontend_is_mounted(client: TestClient) -> None: + """静态前端挂载在根路径,首页可访问(真实 web/ 目录)。""" + # 数据:真实前端文件。 + # 测试过程 + response = client.get("/") + + # 验证结果 + assert response.status_code == 200 + assert "text/html" in response.headers["content-type"] + + +def test_openapi_lists_routers(client: TestClient) -> None: + """OpenAPI 文档包含三组路由(管理端/用户端/批量)。""" + # 数据:无。 + # 测试过程 + paths = client.get("/openapi.json").json()["paths"] + + # 验证结果:每组至少一个端点。 + assert any(path.startswith("/api/admin/workflows") for path in paths) + assert any(path.startswith("/api/apps") for path in paths) + assert any(path.startswith("/api/batch") for path in paths) + + +# --------------------------------------------------------------------------- +# 请求模型(schemas) +# --------------------------------------------------------------------------- + + +def test_workflow_create_schema_defaults() -> None: + """WorkflowCreate:id/description 可省略,definition 必填。""" + # 数据:最小合法载荷。 + model = WorkflowCreate(name="流程", definition={"nodes": [], "edges": []}) + + # 测试过程与验证结果 + assert model.id is None + assert model.description == "" + assert model.definition == {"nodes": [], "edges": []} + + +def test_batch_job_create_schema_defaults() -> None: + """BatchJobCreate:recursive 默认 True(批量页默认递归扫描)。""" + # 数据:最小载荷。 + model = BatchJobCreate(folder="/videos", workflow_id="wf") + + # 测试过程与验证结果 + assert model.recursive is True + assert model.folder == "/videos" + + +def test_schemas_reject_missing_required_fields() -> None: + """缺少必填字段时 Pydantic 校验失败(由 FastAPI 转 422)。""" + # 数据:缺少 name / folder。 + # 测试过程与验证结果 + with pytest.raises(Exception): + WorkflowCreate(definition={}) + with pytest.raises(Exception): + BatchJobCreate(workflow_id="wf") diff --git a/tests/app/test_maintenance/__init__.py b/tests/app/test_maintenance/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/app/test_maintenance/test_cleaner.py b/tests/app/test_maintenance/test_cleaner.py new file mode 100644 index 0000000..a40ebdc --- /dev/null +++ b/tests/app/test_maintenance/test_cleaner.py @@ -0,0 +1,257 @@ +"""src/wov_app/maintenance.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`src/wov_app/maintenance.py`(孤儿数据清理:只删明确死数据), +可独立调用。用例在临时目录构造真实存储布局与真实 SQLite 记录,验证 +"该删的删、不该删的绝不删"(R01/R03 的安全约定)。 +""" + +from __future__ import annotations + +from datetime import datetime, timedelta, timezone +from pathlib import Path + +from wov_app.db import Database +from wov_app.maintenance import OrphanCleaner + + +def _now_iso(offset_seconds: int = 0) -> str: + """返回当前 UTC 时间字符串,可偏移秒数(构造过期/未过期数据)。""" + return (datetime.now(timezone.utc) + timedelta(seconds=offset_seconds)).isoformat() + + +def _db_with_workflow(tmp_path: Path) -> Database: + """建好工作流(任务表对 workflow_id 有外键约束)的临时库。""" + db = Database(tmp_path / "wov.db") + db.upsert_workflow({"id": "wf", "name": "流程", "description": ""}) + db.create_workflow_version("wf", 1, {"nodes": []}) + return db + + +def _run(run_id: str, **overrides) -> dict: + """构造真实任务记录。""" + record = { + "id": run_id, "workflow_id": "wf", "workflow_version": 1, "status": "COMPLETED", + "current_node_id": None, "progress": 1.0, "error": None, "input_uri": None, + "param_overrides": None, "source": "upload", + "created_at": _now_iso(-7200), "updated_at": _now_iso(-7200), + } + record.update(overrides) + return record + + +def _cleaner(db: Database, storage: Path, grace_seconds: int = 3600) -> OrphanCleaner: + """构造清理器(不启动后台线程,直接调用 clean_once)。""" + return OrphanCleaner(db, storage, interval_seconds=999, grace_seconds=grace_seconds) + + +# --------------------------------------------------------------------------- +# 残留目录清理 +# --------------------------------------------------------------------------- + + +def test_removes_dangling_upload_and_run_dirs(tmp_path: Path) -> None: + """无对应任务记录的 uploads/runs 残留目录被删除。""" + # 数据:两个残留目录(库中无记录)。 + storage = tmp_path / "storage" + (storage / "uploads" / "ghost-1").mkdir(parents=True) + (storage / "runs" / "ghost-2").mkdir(parents=True) + (storage / "uploads" / "ghost-1" / "video.mp4").write_bytes(b"data") + db = _db_with_workflow(tmp_path) + + # 测试过程 + removed = _cleaner(db, storage).clean_once() + + # 验证结果:两个目录都被清除。 + assert removed == 2 + assert not (storage / "uploads" / "ghost-1").exists() + assert not (storage / "runs" / "ghost-2").exists() + + +def test_keeps_dirs_with_task_records(tmp_path: Path) -> None: + """有任务记录的目录不删(即使任务已完成)。""" + # 数据:一个有效任务及其目录。 + storage = tmp_path / "storage" + db = _db_with_workflow(tmp_path) + db.create_run(_run("run-1")) + (storage / "runs" / "run-1").mkdir(parents=True) + (storage / "runs" / "run-1" / "out.srt").write_text("字幕", encoding="utf-8") + + # 测试过程 + _cleaner(db, storage).clean_once() + + # 验证结果:目录与任务记录都保留。 + assert (storage / "runs" / "run-1" / "out.srt").is_file() + assert db.get_run("run-1") is not None + + +# --------------------------------------------------------------------------- +# 任务记录清理 +# --------------------------------------------------------------------------- + + +def test_removes_expired_completed_run_without_files(tmp_path: Path) -> None: + """COMPLETED、超过宽限期、产物文件全失的任务记录被删除。""" + # 数据:过期完成任务,目录为空。 + storage = tmp_path / "storage" + db = _db_with_workflow(tmp_path) + db.create_run(_run("run-1")) + (storage / "runs" / "run-1").mkdir(parents=True) + + # 测试过程 + removed = _cleaner(db, storage).clean_once() + + # 验证结果 + assert removed == 1 + assert db.get_run("run-1") is None + + +def test_keeps_failed_run(tmp_path: Path) -> None: + """FAILED 任务绝不自动删除(用户可重试)。""" + # 数据:过期失败任务,无产物文件。 + storage = tmp_path / "storage" + db = _db_with_workflow(tmp_path) + db.create_run(_run("run-failed", status="FAILED", error="boom")) + + # 测试过程 + removed = _cleaner(db, storage).clean_once() + + # 验证结果 + assert removed == 0 + assert db.get_run("run-failed") is not None + + +def test_keeps_running_and_queued_runs(tmp_path: Path) -> None: + """QUEUED / RUNNING 任务不删(可能仍在执行或等待执行)。""" + # 数据:排队中与运行中的任务。 + storage = tmp_path / "storage" + db = _db_with_workflow(tmp_path) + db.create_run(_run("run-q", status="QUEUED")) + db.create_run(_run("run-r", status="RUNNING")) + + # 测试过程 + removed = _cleaner(db, storage).clean_once() + + # 验证结果 + assert removed == 0 + assert db.get_run("run-q") is not None + assert db.get_run("run-r") is not None + + +def test_keeps_completed_run_within_grace_period(tmp_path: Path) -> None: + """宽限期内的完成任务不删(下载可能还在进行)。""" + # 数据:刚完成、无产物文件的任务。 + storage = tmp_path / "storage" + db = _db_with_workflow(tmp_path) + db.create_run(_run("run-fresh", updated_at=_now_iso(-10))) + + # 测试过程 + removed = _cleaner(db, storage, grace_seconds=3600).clean_once() + + # 验证结果 + assert removed == 0 + assert db.get_run("run-fresh") is not None + + +def test_keeps_completed_run_with_existing_files(tmp_path: Path) -> None: + """仍有产物文件的完成任务不删(下载仍可用)。""" + # 数据:过期但产物仍存在的任务。 + storage = tmp_path / "storage" + db = _db_with_workflow(tmp_path) + db.create_run(_run("run-1")) + payload = storage / "runs" / "run-1" / "finals" / "out.srt" + payload.parent.mkdir(parents=True) + payload.write_text("字幕", encoding="utf-8") + + # 测试过程 + removed = _cleaner(db, storage).clean_once() + + # 验证结果 + assert removed == 0 + assert payload.is_file() + + +def test_skips_batch_source_runs(tmp_path: Path) -> None: + """source=batch 的运行跳过清理(工作空间在私有层级,用户媒体目录必须保留)。""" + # 数据:过期完成的批量运行,产物不在主 runs 目录下。 + storage = tmp_path / "storage" + db = _db_with_workflow(tmp_path) + db.create_run(_run("run-batch", source="batch")) + # 用户媒体目录(若被误删会造成数据丢失)。 + media_dir = tmp_path / "user-videos" + media_dir.mkdir() + (media_dir / "movie.mp4").write_bytes(b"video") + + # 测试过程 + removed = _cleaner(db, storage).clean_once() + + # 验证结果:批量 run 与用户媒体都保留。 + assert removed == 0 + assert db.get_run("run-batch") is not None + assert (media_dir / "movie.mp4").is_file() + + +def test_keeps_external_input_directory(tmp_path: Path) -> None: + """删除孤儿任务时不动其 input_uri 指向的外部目录(R01:不误删媒体库)。""" + # 数据:过期完成、无产物的任务,但 input_uri 指向用户媒体目录。 + storage = tmp_path / "storage" + db = _db_with_workflow(tmp_path) + media_dir = tmp_path / "media" + media_dir.mkdir() + video = media_dir / "movie.mp4" + video.write_bytes(b"video") + db.create_run(_run("run-1", input_uri=str(video))) + + # 测试过程 + _cleaner(db, storage).clean_once() + + # 验证结果:任务记录被清理,但用户媒体目录完整保留。 + assert db.get_run("run-1") is None + assert video.is_file() + + +def test_invalid_timestamp_is_kept(tmp_path: Path) -> None: + """时间戳无法解析时保守保留(不因数据损坏误删)。""" + # 数据:updated_at 非法。 + storage = tmp_path / "storage" + db = _db_with_workflow(tmp_path) + db.create_run(_run("run-1", updated_at="not-a-timestamp")) + + # 测试过程 + removed = _cleaner(db, storage).clean_once() + + # 验证结果 + assert removed == 0 + assert db.get_run("run-1") is not None + + +# --------------------------------------------------------------------------- +# 线程生命周期 +# --------------------------------------------------------------------------- + + +def test_start_and_stop_are_idempotent(tmp_path: Path) -> None: + """start 重复调用不产生多个线程;stop 能正常结束线程。""" + # 数据:清理器(间隔很大,循环来不及真正清理)。 + storage = tmp_path / "storage" + db = _db_with_workflow(tmp_path) + cleaner = OrphanCleaner(db, storage, interval_seconds=999, grace_seconds=3600) + + # 测试过程 + cleaner.start() + first_thread = cleaner._thread + cleaner.start() + second_thread = cleaner._thread + cleaner.stop() + + # 验证结果:同一线程对象,停止后引用清空。 + assert first_thread is second_thread + assert cleaner._thread is None + + +def test_stop_without_start_is_safe(tmp_path: Path) -> None: + """未启动就 stop 不报错。""" + # 数据:未启动的清理器。 + db = _db_with_workflow(tmp_path) + + # 测试过程与验证结果:不抛异常。 + OrphanCleaner(db, tmp_path / "storage").stop() diff --git a/tests/app/test_registry/__init__.py b/tests/app/test_registry/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/app/test_registry/test_registry.py b/tests/app/test_registry/test_registry.py new file mode 100644 index 0000000..c152301 --- /dev/null +++ b/tests/app/test_registry/test_registry.py @@ -0,0 +1,192 @@ +"""src/wov_app/registry.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`src/wov_app/registry.py`(节点注册表:节点调用的唯一入口), +可独立调用。注册表是进程内全局状态,用例通过保存/恢复快照隔离,不依赖 +全局 conftest(原 tests/conftest.py 的 autouse 夹具已按规则取消)。 +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from wov_app import registry +from wov_sdk.models import InvokeRequest, InvokeResponse, NodeManifest + +# 仓库根目录(用于定位真实 manifests/)。 +WORKSPACE = Path(__file__).resolve().parents[3] + + +@pytest.fixture(autouse=True) +def _restore_registry(): + """用例前清空注册表、用例后恢复快照:保证用例看到的是干净基线, + 不受其他模块(如 main 生命周期 register_all)的注册结果影响。""" + snapshot = dict(registry._registry) + registry._registry.clear() + yield + registry._registry.clear() + registry._registry.update(snapshot) + + +def _manifest(node_id: str = "demo-node") -> NodeManifest: + """构造一个最小合法清单(真实字段结构)。""" + return NodeManifest( + id=node_id, + name="演示节点", + version="0.1.0", + capability="demo", + command=["python", "-m", "demo"], + ) + + +def _request(tmp_path: Path) -> InvokeRequest: + """构造真实调用请求。""" + return InvokeRequest( + run_id="run-test", + node_instance_id="n1", + params={}, + inputs={"text": "hi"}, + output_dir=str(tmp_path), + ) + + +# --------------------------------------------------------------------------- +# 注册与查询 +# --------------------------------------------------------------------------- + + +def test_register_and_get_node() -> None: + """注册后可查询到清单,未注册返回 None。""" + # 数据:一个清单 + 一个真实处理器。 + registry.register(_manifest("echo-x"), lambda request: InvokeResponse(status="completed")) + + # 测试过程 + found = registry.get_node("echo-x") + missing = registry.get_node("not-registered") + + # 验证结果 + assert found is not None and found.id == "echo-x" + assert missing is None + + +def test_register_overwrites_same_id() -> None: + """同一 ID 重复注册按后者覆盖(启动时 register_all 幂等)。""" + # 数据:同一 ID 注册两次,名称不同。 + first = _manifest("node-x") + second = NodeManifest( + id="node-x", name="覆盖后", version="0.2.0", capability="demo", + command=["python", "-m", "demo"], + ) + registry.register(first, lambda request: InvokeResponse(status="completed")) + registry.register(second, lambda request: InvokeResponse(status="completed")) + + # 测试过程 + found = registry.get_node("node-x") + + # 验证结果 + assert found.name == "覆盖后" + assert found.version == "0.2.0" + + +def test_register_rejects_invalid_manifest() -> None: + """非法清单(空 ID)注册时校验失败并抛错。""" + # 数据:id 为空的清单。 + invalid = NodeManifest(id="", name="x", version="1", capability="c", command=["python"]) + + # 测试过程与验证结果 + with pytest.raises(ValueError): + registry.register(invalid, lambda request: InvokeResponse(status="completed")) + + +def test_register_all_registers_builtin_nodes() -> None: + """register_all 从真实 manifests/ 注册全部内置节点(含关键节点)。""" + # 数据:真实仓库清单目录。 + assert (WORKSPACE / "manifests").is_dir() + + # 测试过程 + registry.register_all() + ids = {manifest.id for manifest in registry.list_nodes()} + + # 验证结果:字幕流水线所需的节点全部在册(ID 取自 manifests/*.json)。 + for expected in ( + "echo", "ffmpeg-extract", "faster-whisper", "llm-translate", + "vlm-ocr", "frame-extract", "subtitle-ocr", "llm-filter", + "subtitle-correction", "srt-to-dual-eye-ass", + ): + assert expected in ids, f"内置节点未注册:{expected}" + + +def test_register_all_is_idempotent() -> None: + """重复调用 register_all 不产生重复条目(按 ID 覆盖)。""" + # 数据:无。 + # 测试过程 + registry.register_all() + first = len(registry.list_nodes()) + registry.register_all() + second = len(registry.list_nodes()) + + # 验证结果 + assert first == second + + +# --------------------------------------------------------------------------- +# invoke:唯一调用入口 +# --------------------------------------------------------------------------- + + +def test_invoke_routes_to_registered_handler(tmp_path: Path) -> None: + """invoke 把请求路由到注册的处理器并原样返回其响应。""" + # 数据:记录收到的请求的处理器。 + received: list[InvokeRequest] = [] + + def handler(request: InvokeRequest) -> InvokeResponse: + received.append(request) + return InvokeResponse(status="completed", outputs={"text": "ok"}) + + registry.register(_manifest("node-x"), handler) + + # 测试过程 + response = registry.invoke("node-x", _request(tmp_path)) + + # 验证结果:响应来自处理器,且请求原样传递。 + assert response.status == "completed" + assert response.outputs == {"text": "ok"} + assert received[0].inputs == {"text": "hi"} + + +def test_invoke_raises_for_unregistered_node(tmp_path: Path) -> None: + """未注册节点调用抛 ValueError(不静默返回空结果)。""" + # 数据:未注册的 node_id。 + # 测试过程与验证结果 + with pytest.raises(ValueError, match="not registered"): + registry.invoke("nope", _request(tmp_path)) + + +def test_invoke_returns_failed_response_unchanged(tmp_path: Path) -> None: + """处理器返回 failed 时原样透传(注册表不改变节点语义)。""" + # 数据:总是失败的处理器。 + registry.register( + _manifest("node-x"), + lambda request: InvokeResponse(status="failed", error="boom"), + ) + + # 测试过程 + response = registry.invoke("node-x", _request(tmp_path)) + + # 验证结果 + assert response.status == "failed" + assert response.error == "boom" + + +def test_list_nodes_returns_sorted_by_id() -> None: + """list_nodes 按节点 ID 排序返回(前端展示稳定)。""" + # 数据:乱序注册的节点。 + for node_id in ("z-node", "a-node", "m-node"): + registry.register(_manifest(node_id), lambda request: InvokeResponse(status="completed")) + + # 测试过程 + ids = [m.id for m in registry.list_nodes()] + + # 验证结果 + assert ids == ["a-node", "m-node", "z-node"] diff --git a/tests/app/test_routers/__init__.py b/tests/app/test_routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/app/test_routers/test_apps_api.py b/tests/app/test_routers/test_apps_api.py new file mode 100644 index 0000000..2a7a58a --- /dev/null +++ b/tests/app/test_routers/test_apps_api.py @@ -0,0 +1,319 @@ +"""src/wov_app/routers/apps.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`src/wov_app/routers/apps.py`(用户端 API:应用列表、上传建任务、 +进度查询、产物下载、重试/暂停/继续/删除),可独立调用。用例通过真实 +FastAPI TestClient 走完整 HTTP 链路,数据库为临时 SQLite。 +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + +from wov_app import registry +from wov_app.db import Database +from wov_app.main import app as fastapi_app + + +@pytest.fixture(autouse=True) +def _isolate_registry(): + """保存/恢复注册表(模块自带隔离)。""" + snapshot = dict(registry._registry) + yield + registry._registry.clear() + registry._registry.update(snapshot) + + +@pytest.fixture() +def client(tmp_path: Path, monkeypatch) -> TestClient: + """构造隔离存储与库的真实 TestClient。 + + `wov_app.config` 在导入时锁定路径常量,因此这里同时替换 main 与 config + 的 STORAGE_DIR(路由在函数内 `from wov_app.config import STORAGE_DIR`), + 保证测试不写入真实 data/。 + """ + storage = tmp_path / "storage" + monkeypatch.setattr("wov_app.config.STORAGE_DIR", storage) + monkeypatch.setattr("wov_app.config.DB_PATH", tmp_path / "wov.db") + monkeypatch.setattr("wov_app.main.DB_PATH", tmp_path / "wov.db") + monkeypatch.setattr("wov_app.main.STORAGE_DIR", storage) + monkeypatch.setenv("WOV_AUTO_SEED", "0") + monkeypatch.setenv("WOV_SCHEDULER_ENABLED", "0") + monkeypatch.setenv("WOV_CLEANUP_ENABLED", "0") + monkeypatch.setenv("WOV_BATCH_ENABLED", "0") + # 生命周期用 main.DB_PATH 建库;库文件路径与断言用的库保持一致。 + with TestClient(fastapi_app) as test_client: + yield test_client + + +def _publish_echo_app(client: TestClient, app_id: str = "echo-app") -> str: + """创建并发布一个单 echo 节点应用(echo 为内置节点)。""" + definition = { + "name": "echo-flow", + "version": 1, + "nodes": [{"id": "step", "node_type": "echo", "inputs": {"file_uri": "input.video_uri"}}], + "edges": [], + "entry_inputs": {"video_uri": "file"}, + "final_outputs": {"result": "step.file_uri"}, + } + client.post("/api/admin/workflows", json={ + "id": app_id, "name": "Echo App", "description": "upload a file", "definition": definition, + }) + client.post(f"/api/admin/workflows/{app_id}/publish") + return app_id + + +# --------------------------------------------------------------------------- +# 应用列表与创建任务 +# --------------------------------------------------------------------------- + + +def test_list_apps_returns_only_published(client: TestClient) -> None: + """应用列表只返回已发布工作流(用户看不到草稿)。""" + # 数据:一个草稿 + 一个已发布。 + client.post("/api/admin/workflows", json={ + "id": "draft", "name": "草稿", "description": "", + "definition": {"name": "d", "version": 1, "nodes": [], "edges": []}, + }) + _publish_echo_app(client) + + # 测试过程 + response = client.get("/api/apps") + + # 验证结果 + assert response.status_code == 200 + ids = {item["id"] for item in response.json()} + assert ids == {"echo-app"} + + +def test_create_run_accepts_upload_and_persists_params(client: TestClient) -> None: + """上传视频创建任务:返回 run_id,参数覆盖被持久化。""" + # 数据:已发布应用 + 真实上传文件 + 裁剪参数覆盖。 + app_id = _publish_echo_app(client) + + # 测试过程 + response = client.post( + f"/api/apps/{app_id}/runs", + files={"file": ("movie.mp4", b"fake video bytes", "video/mp4")}, + data={"params": '{"step": {"interval_seconds": 3}}'}, + ) + + # 验证结果:任务创建成功且参数被保存。 + assert response.status_code == 200, response.text + run_id = response.json()["id"] + stored = client.get(f"/api/runs/{run_id}").json() + assert stored["param_overrides"] == {"step": {"interval_seconds": 3}} + assert stored["status"] in ("QUEUED", "RUNNING", "COMPLETED") + + +def test_create_run_rejects_unpublished_app(client: TestClient) -> None: + """未发布应用不接单(404)。""" + # 数据:只创建不发布。 + client.post("/api/admin/workflows", json={ + "id": "draft-app", "name": "草稿", "description": "", + "definition": {"name": "d", "version": 1, "nodes": [], "edges": []}, + }) + + # 测试过程 + response = client.post( + "/api/apps/draft-app/runs", + files={"file": ("a.mp4", b"x", "video/mp4")}, + ) + + # 验证结果 + assert response.status_code == 404 + + +def test_create_run_rejects_unknown_app(client: TestClient) -> None: + """不存在的应用返回 404。""" + # 数据:未注册的应用 ID。 + # 测试过程 + response = client.post( + "/api/apps/nope/runs", files={"file": ("a.mp4", b"x", "video/mp4")}, + ) + + # 验证结果 + assert response.status_code == 404 + + +def test_create_run_rejects_invalid_params_json(client: TestClient) -> None: + """params 不是合法 JSON 时返回 422(参数错误不产生任务)。""" + # 数据:已发布应用 + 非法 params。 + app_id = _publish_echo_app(client) + + # 测试过程 + response = client.post( + f"/api/apps/{app_id}/runs", + files={"file": ("a.mp4", b"x", "video/mp4")}, + data={"params": "{not json"}, + ) + + # 验证结果 + assert response.status_code == 422 + + +# --------------------------------------------------------------------------- +# 任务查询、暂停、继续、重试、删除 +# --------------------------------------------------------------------------- + + +def _create_run(client: TestClient) -> str: + """创建一条任务并返回 run_id(测试辅助)。""" + app_id = _publish_echo_app(client) + response = client.post( + f"/api/apps/{app_id}/runs", files={"file": ("m.mp4", b"data", "video/mp4")}, + ) + assert response.status_code == 200, response.text + return response.json()["id"] + + +def test_list_and_get_runs(client: TestClient) -> None: + """任务列表与详情可读(详情含状态与进度)。""" + # 数据:一条任务。 + run_id = _create_run(client) + + # 测试过程 + listed = client.get("/api/runs").json() + detail = client.get(f"/api/runs/{run_id}") + + # 验证结果 + assert any(item["id"] == run_id for item in listed) + assert detail.status_code == 200 + assert {"status", "progress", "id"} <= set(detail.json()) + + +def test_get_missing_run_returns_404(client: TestClient) -> None: + """不存在的任务返回 404。""" + # 数据:未创建的任务 ID。 + # 测试过程与验证结果 + assert client.get("/api/runs/nope").status_code == 404 + + +def test_pause_and_resume_run(client: TestClient) -> None: + """暂停置 PAUSED、继续置 QUEUED,并写入/清除暂停信号文件。""" + # 数据:一条任务。 + run_id = _create_run(client) + + # 测试过程 + paused = client.post(f"/api/runs/{run_id}/pause") + paused_status = client.get(f"/api/runs/{run_id}").json()["status"] + resumed = client.post(f"/api/runs/{run_id}/resume") + resumed_status = client.get(f"/api/runs/{run_id}").json()["status"] + + # 验证结果 + assert paused.status_code == 200 and resumed.status_code == 200 + assert paused_status == "PAUSED" + assert resumed_status == "QUEUED" + + +def test_resume_rejects_non_paused_run(client: TestClient) -> None: + """非 PAUSED 状态不能继续(409/422 语义,避免误触发执行)。""" + # 数据:一条刚创建的任务(QUEUED)。 + run_id = _create_run(client) + + # 测试过程 + response = client.post(f"/api/runs/{run_id}/resume") + + # 验证结果:不是 200 成功。 + assert response.status_code != 200 + + +def test_retry_requeues_failed_run(client: TestClient) -> None: + """FAILED 任务可重试:状态回到 QUEUED 并清空错误。""" + # 数据:手动把任务置为 FAILED。 + run_id = _create_run(client) + db: Database = client.app.state.db + db.update_run(run_id, status="FAILED", error="boom", updated_at="2026-09-01T00:00:00+00:00") + + # 测试过程 + response = client.post(f"/api/runs/{run_id}/retry") + + # 验证结果 + assert response.status_code == 200 + stored = client.get(f"/api/runs/{run_id}").json() + assert stored["status"] == "QUEUED" + assert stored["error"] is None + + +def test_delete_run_removes_record_and_files(client: TestClient) -> None: + """删除任务清理记录与私有上传/产物目录(不动外部路径)。""" + # 数据:一条任务。 + run_id = _create_run(client) + db: Database = client.app.state.db + + # 测试过程 + response = client.delete(f"/api/runs/{run_id}") + + # 验证结果:记录消失,再次查询 404。 + assert response.status_code == 200 + assert db.get_run(run_id) is None + assert client.get(f"/api/runs/{run_id}").status_code == 404 + + +def test_delete_rejects_batch_source_run(client: TestClient) -> None: + """source=batch 的任务拒绝普通删除(R01:避免误删用户视频目录)。""" + # 数据:一条 batch 来源任务(source 不在 update_run 白名单,直连库改)。 + run_id = _create_run(client) + db: Database = client.app.state.db + with db._connect() as conn: + conn.execute("UPDATE workflow_runs SET source='batch' WHERE id = ?", (run_id,)) + + # 测试过程 + response = client.delete(f"/api/runs/{run_id}") + + # 验证结果:被拒绝(422),记录仍存在。 + assert response.status_code == 422 + assert db.get_run(run_id) is not None + + +# --------------------------------------------------------------------------- +# 产物查询与下载 +# --------------------------------------------------------------------------- + + +def test_list_artifacts_and_download(client: TestClient, tmp_path: Path) -> None: + """产物列表可查,产物内容可按名下载。""" + # 数据:任务 + 真实产物文件 + 产物记录。 + run_id = _create_run(client) + db: Database = client.app.state.db + payload = tmp_path / "out.srt" + payload.write_text("1\n00:00:01,000 --> 00:00:02,000\n你好\n", encoding="utf-8") + db.create_artifact({ + "run_id": run_id, "node_id": "step", "name": "result", + "uri": str(payload), "mime_type": "application/x-subrip", "size": payload.stat().st_size, + }) + + # 测试过程 + listed = client.get(f"/api/runs/{run_id}/artifacts") + downloaded = client.get(f"/api/runs/{run_id}/artifacts/result") + + # 验证结果 + assert listed.status_code == 200 + assert any(item["name"] == "result" for item in listed.json()) + assert downloaded.status_code == 200 + assert "你好" in downloaded.content.decode("utf-8") + + +def test_download_missing_artifact_returns_404(client: TestClient) -> None: + """下载不存在的产物返回 404。""" + # 数据:一条任务,无该产物。 + run_id = _create_run(client) + + # 测试过程与验证结果 + assert client.get(f"/api/runs/{run_id}/artifacts/nope").status_code == 404 + + +def test_download_missing_file_returns_404(client: TestClient) -> None: + """产物记录存在但文件已被删除时返回 404(不返回残缺内容)。""" + # 数据:产物记录指向不存在的文件。 + run_id = _create_run(client) + db: Database = client.app.state.db + db.create_artifact({ + "run_id": run_id, "node_id": "step", "name": "gone", + "uri": "/nonexistent/file.srt", "mime_type": "application/x-subrip", "size": 0, + }) + + # 测试过程与验证结果 + assert client.get(f"/api/runs/{run_id}/artifacts/gone").status_code == 404 diff --git a/tests/app/test_routers/test_batch_api.py b/tests/app/test_routers/test_batch_api.py new file mode 100644 index 0000000..d8441a7 --- /dev/null +++ b/tests/app/test_routers/test_batch_api.py @@ -0,0 +1,310 @@ +"""src/wov_app/routers/batch.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`src/wov_app/routers/batch.py`(批量处理 API:目录浏览、任务创建 +与列表、暂停/继续、删除、产物下载),可独立调用。用例通过真实 TestClient, +文件夹为临时目录下的真实视频文件。 +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + +from wov_app import registry +from wov_app.main import app as fastapi_app +from wov_sdk.models import WorkflowDefinition + + +@pytest.fixture(autouse=True) +def _isolate_registry(): + """保存/恢复注册表(模块自带隔离)。""" + snapshot = dict(registry._registry) + yield + registry._registry.clear() + registry._registry.update(snapshot) + + +@pytest.fixture() +def client(tmp_path: Path, monkeypatch) -> TestClient: + """构造隔离数据库与存储的真实 TestClient。""" + monkeypatch.setattr("wov_app.config.DB_PATH", tmp_path / "wov.db") + monkeypatch.setattr("wov_app.config.STORAGE_DIR", tmp_path / "storage") + monkeypatch.setattr("wov_app.main.DB_PATH", tmp_path / "wov.db") + monkeypatch.setattr("wov_app.main.STORAGE_DIR", tmp_path / "storage") + # 批量工作空间也指向临时目录(模块常量在导入时绑定)。 + monkeypatch.setattr("wov_app.batch.BATCH_WORK_ROOT", tmp_path / "storage" / "batch") + monkeypatch.setenv("WOV_AUTO_SEED", "0") + monkeypatch.setenv("WOV_SCHEDULER_ENABLED", "0") + monkeypatch.setenv("WOV_CLEANUP_ENABLED", "0") + monkeypatch.setenv("WOV_BATCH_ENABLED", "0") + with TestClient(fastapi_app) as test_client: + yield test_client + + +def _make_video(path: Path) -> Path: + """创建真实可读的视频文件。""" + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(b"\x00\x00\x00\x18ftypmp42" + b"\x00" * 32) + return path + + +def _publish_workflow(client: TestClient, workflow_id: str = "wf") -> None: + """创建并发布单 echo 节点工作流。""" + definition = WorkflowDefinition.from_dict({ + "name": "批量流程", "version": 1, + "nodes": [{"id": "step", "node_type": "echo", "inputs": {"file_uri": "input.video_uri"}}], + "edges": [], "entry_inputs": {"video_uri": "file"}, + "final_outputs": {"result": "step.file_uri"}, + }).to_dict() + client.post("/api/admin/workflows", json={ + "id": workflow_id, "name": "批量流程", "description": "", "definition": definition, + }) + client.post(f"/api/admin/workflows/{workflow_id}/publish") + + +# --------------------------------------------------------------------------- +# 目录浏览(本地后端提供,浏览器拿不到绝对路径) +# --------------------------------------------------------------------------- + + +def test_list_roots_returns_browsable_roots(client: TestClient) -> None: + """根目录列表返回可浏览位置(POSIX 返回 / 与家目录)。""" + # 数据:无(真实文件系统)。 + # 测试过程 + response = client.get("/api/batch/roots") + + # 验证结果:非空且每项含 path/name。 + assert response.status_code == 200 + roots = response.json() + assert roots, "至少应返回一个可浏览根目录" + assert all({"path", "name"} <= set(item) for item in roots) + + +def test_list_dirs_returns_only_directories(client: TestClient, tmp_path: Path) -> None: + """列目录只返回直接子目录,且过滤隐藏目录。""" + # 数据:两个子目录 + 一个隐藏目录 + 一个文件。 + folder = tmp_path / "root" + (folder / "sub1").mkdir(parents=True) + (folder / "sub2").mkdir() + (folder / ".hidden").mkdir() + (folder / "file.txt").write_text("x", encoding="utf-8") + + # 测试过程 + body = client.get("/api/batch/dirs", params={"path": str(folder)}).json() + + # 验证结果 + names = {item["name"] for item in body["dirs"]} + assert names == {"sub1", "sub2"} + + +def test_list_dirs_returns_empty_for_missing_path(client: TestClient, tmp_path: Path) -> None: + """目录不存在时返回空列表而非 500(前端树保持可用)。""" + # 数据:不存在的路径。 + # 测试过程 + response = client.get("/api/batch/dirs", params={"path": str(tmp_path / "nope")}) + + # 验证结果 + assert response.status_code == 200 + assert response.json()["dirs"] == [] + + +# --------------------------------------------------------------------------- +# 创建与查询批量任务 +# --------------------------------------------------------------------------- + + +def test_create_job_returns_job_with_videos(client: TestClient, tmp_path: Path) -> None: + """创建批量任务:返回任务 ID、总数与明细列表(含跳过项)。""" + # 数据:两个视频,其中一个已有旁挂字幕。 + folder = tmp_path / "videos" + _make_video(folder / "a.mp4") + _make_video(folder / "b.mp4") + (folder / "b.CN.srt").write_text("1\n", encoding="utf-8") + _publish_workflow(client) + + # 测试过程 + response = client.post("/api/batch/jobs", json={ + "folder": str(folder), "workflow_id": "wf", "recursive": False, + }) + + # 验证结果 + assert response.status_code == 200, response.text + body = response.json() + assert body["total"] == 1 + assert len(body["videos"]) == 2 + + +def test_create_job_rejects_missing_folder(client: TestClient, tmp_path: Path) -> None: + """文件夹不存在返回 422。""" + # 数据:未发布的目录路径。 + _publish_workflow(client) + + # 测试过程 + response = client.post("/api/batch/jobs", json={ + "folder": str(tmp_path / "nope"), "workflow_id": "wf", "recursive": False, + }) + + # 验证结果 + assert response.status_code == 422 + + +def test_create_job_rejects_unpublished_workflow(client: TestClient, tmp_path: Path) -> None: + """工作流未发布返回 422。""" + # 数据:有视频但工作流未发布。 + folder = tmp_path / "videos" + _make_video(folder / "a.mp4") + + # 测试过程 + response = client.post("/api/batch/jobs", json={ + "folder": str(folder), "workflow_id": "wf", "recursive": False, + }) + + # 验证结果 + assert response.status_code == 422 + + +def test_create_job_rejects_folder_without_videos(client: TestClient, tmp_path: Path) -> None: + """文件夹内无视频返回 422。""" + # 数据:空文件夹 + 已发布工作流。 + folder = tmp_path / "videos" + folder.mkdir() + _publish_workflow(client) + + # 测试过程 + response = client.post("/api/batch/jobs", json={ + "folder": str(folder), "workflow_id": "wf", "recursive": False, + }) + + # 验证结果 + assert response.status_code == 422 + + +def test_list_and_get_job(client: TestClient, tmp_path: Path) -> None: + """任务列表与详情可读,详情含视频明细。""" + # 数据:一条批量任务。 + folder = tmp_path / "videos" + _make_video(folder / "a.mp4") + _publish_workflow(client) + job_id = client.post("/api/batch/jobs", json={ + "folder": str(folder), "workflow_id": "wf", "recursive": False, + }).json()["id"] + + # 测试过程 + listed = client.get("/api/batch/jobs").json() + detail = client.get(f"/api/batch/jobs/{job_id}") + + # 验证结果 + assert any(item["id"] == job_id for item in listed) + assert detail.status_code == 200 + assert detail.json()["videos"][0]["video_path"].endswith("a.mp4") + + +def test_get_missing_job_returns_404(client: TestClient) -> None: + """不存在的批量任务返回 404。""" + # 数据:未创建。 + # 测试过程与验证结果 + assert client.get("/api/batch/jobs/nope").status_code == 404 + + +# --------------------------------------------------------------------------- +# 暂停 / 继续 / 删除 +# --------------------------------------------------------------------------- + + +def test_pause_and_resume_job(client: TestClient, tmp_path: Path) -> None: + """暂停置 PAUSED、继续置 QUEUED。""" + # 数据:一条待处理批量任务。 + folder = tmp_path / "videos" + _make_video(folder / "a.mp4") + _publish_workflow(client) + job_id = client.post("/api/batch/jobs", json={ + "folder": str(folder), "workflow_id": "wf", "recursive": False, + }).json()["id"] + + # 测试过程 + paused = client.post(f"/api/batch/jobs/{job_id}/pause") + after_pause = client.get(f"/api/batch/jobs/{job_id}").json()["status"] + resumed = client.post(f"/api/batch/jobs/{job_id}/resume") + after_resume = client.get(f"/api/batch/jobs/{job_id}").json()["status"] + + # 验证结果 + assert paused.status_code == 200 and resumed.status_code == 200 + assert after_pause == "PAUSED" + assert after_resume == "QUEUED" + + +def test_delete_job_removes_records_and_keeps_media(client: TestClient, tmp_path: Path) -> None: + """删除任务清理记录与私有工作空间,保留用户视频。""" + # 数据:一条批量任务。 + folder = tmp_path / "videos" + video = _make_video(folder / "a.mp4") + _publish_workflow(client) + job_id = client.post("/api/batch/jobs", json={ + "folder": str(folder), "workflow_id": "wf", "recursive": False, + }).json()["id"] + + # 测试过程 + response = client.delete(f"/api/batch/jobs/{job_id}") + + # 验证结果:任务消失但视频保留。 + assert response.status_code == 200 + assert client.get(f"/api/batch/jobs/{job_id}").status_code == 404 + assert video.is_file() + + +def test_delete_missing_job_returns_404(client: TestClient) -> None: + """删除不存在的任务返回 404。""" + # 数据:未创建。 + # 测试过程与验证结果 + assert client.delete("/api/batch/jobs/nope").status_code == 404 + + +# --------------------------------------------------------------------------- +# 产物下载 +# --------------------------------------------------------------------------- + + +def test_download_sidecar_product(client: TestClient, tmp_path: Path) -> None: + """下载接口返回视频旁的产物文件内容。""" + # 数据:一条批量任务 + 视频旁的成品字幕。 + folder = tmp_path / "videos" + _make_video(folder / "movie.mp4") + _publish_workflow(client) + body = client.post("/api/batch/jobs", json={ + "folder": str(folder), "workflow_id": "wf", "recursive": False, + }).json() + job_id = body["id"] + video_id = body["videos"][0]["id"] + (folder / "movie.CN.srt").write_text("1\n00:00:01,000 --> 00:00:02,000\n你好\n", encoding="utf-8") + + # 测试过程 + response = client.get( + f"/api/batch/jobs/{job_id}/videos/{video_id}/download", + params={"alias": "movie.CN.srt"}, + ) + + # 验证结果 + assert response.status_code == 200 + assert "你好" in response.content.decode("utf-8") + + +def test_download_missing_product_returns_404(client: TestClient, tmp_path: Path) -> None: + """产物不存在时返回 404。""" + # 数据:一条批量任务(无产物)。 + folder = tmp_path / "videos" + _make_video(folder / "movie.mp4") + _publish_workflow(client) + body = client.post("/api/batch/jobs", json={ + "folder": str(folder), "workflow_id": "wf", "recursive": False, + }).json() + + # 测试过程 + response = client.get( + f"/api/batch/jobs/{body['id']}/videos/{body['videos'][0]['id']}/download", + params={"alias": "movie.CN.srt"}, + ) + + # 验证结果 + assert response.status_code == 404 diff --git a/tests/app/test_routers/test_workflows_api.py b/tests/app/test_routers/test_workflows_api.py new file mode 100644 index 0000000..12c29e0 --- /dev/null +++ b/tests/app/test_routers/test_workflows_api.py @@ -0,0 +1,311 @@ +"""src/wov_app/routers/workflows.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`src/wov_app/routers/workflows.py`(管理端 API:工作流 CRUD、 +校验、发布、版本历史),可独立调用。用例通过真实 TestClient 走 HTTP 链路, +数据库为临时 SQLite。 +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + +from wov_app.main import app as fastapi_app + + +@pytest.fixture() +def client(tmp_path: Path, monkeypatch) -> TestClient: + """构造隔离数据库与存储的真实 TestClient。""" + monkeypatch.setattr("wov_app.config.DB_PATH", tmp_path / "wov.db") + monkeypatch.setattr("wov_app.config.STORAGE_DIR", tmp_path / "storage") + monkeypatch.setattr("wov_app.main.DB_PATH", tmp_path / "wov.db") + monkeypatch.setattr("wov_app.main.STORAGE_DIR", tmp_path / "storage") + monkeypatch.setenv("WOV_AUTO_SEED", "0") + monkeypatch.setenv("WOV_SCHEDULER_ENABLED", "0") + monkeypatch.setenv("WOV_CLEANUP_ENABLED", "0") + monkeypatch.setenv("WOV_BATCH_ENABLED", "0") + with TestClient(fastapi_app) as test_client: + yield test_client + + +def _definition(name: str = "echo-flow", nodes: list[dict] | None = None, edges: list[dict] | None = None) -> dict: + """构造合法 DAG 定义(默认单 echo 节点)。""" + return { + "name": name, + "version": 1, + "nodes": nodes or [ + {"id": "step", "node_type": "echo", "inputs": {"file_uri": "input.video_uri"}}, + ], + "edges": edges or [], + "entry_inputs": {"video_uri": "file"}, + "final_outputs": {"result": "step.file_uri"}, + } + + +def _create(client: TestClient, workflow_id: str = "wf", definition: dict | None = None) -> dict: + """创建一个工作流并返回响应体。""" + response = client.post("/api/admin/workflows", json={ + "id": workflow_id, "name": "测试流程", "description": "说明", + "definition": definition or _definition(), + }) + assert response.status_code == 200, response.text + return response.json() + + +# --------------------------------------------------------------------------- +# 创建与读取 +# --------------------------------------------------------------------------- + + +def test_create_workflow_saves_first_version(client: TestClient) -> None: + """创建工作流保存为 v1,初始未发布。""" + # 数据:合法定义。 + # 测试过程 + created = _create(client, "wf-1") + + # 验证结果 + assert created["id"] == "wf-1" + assert created["latest_version"] == 1 + assert created["published"] is False + + +def test_create_workflow_appends_new_version(client: TestClient) -> None: + """对已存在工作流再次创建 → 追加新版本(保存即新版本)。""" + # 数据:同一 ID 两次创建。 + _create(client, "wf-1") + + # 测试过程 + second = _create(client, "wf-1") + + # 验证结果:版本递增。 + assert second["latest_version"] == 2 + + +def test_get_workflow_returns_latest_definition(client: TestClient) -> None: + """详情返回最新版本定义(供编排页加载编辑)。""" + # 数据:已创建工作流。 + _create(client, "wf-1") + + # 测试过程 + detail = client.get("/api/admin/workflows/wf-1") + + # 验证结果 + assert detail.status_code == 200 + body = detail.json() + assert body["latest_version_data"]["version"] == 1 + assert body["latest_version_data"]["definition"]["nodes"][0]["id"] == "step" + + +def test_get_missing_workflow_returns_404(client: TestClient) -> None: + """不存在的工作流返回 404。""" + # 数据:未创建。 + # 测试过程与验证结果 + assert client.get("/api/admin/workflows/nope").status_code == 404 + + +def test_list_workflows(client: TestClient) -> None: + """列表返回全部工作流概要。""" + # 数据:两个工作流。 + _create(client, "wf-1") + _create(client, "wf-2") + + # 测试过程 + listed = client.get("/api/admin/workflows").json() + + # 验证结果 + assert {item["id"] for item in listed} == {"wf-1", "wf-2"} + + +# --------------------------------------------------------------------------- +# 校验:拒绝非法 DAG +# --------------------------------------------------------------------------- + + +def test_create_rejects_cycle(client: TestClient) -> None: + """环形 DAG 保存被拒(422),不产生工作流记录(R04)。""" + # 数据:A→B→A 的环形定义。 + cyclic = _definition(nodes=[ + {"id": "a", "node_type": "echo", "inputs": {"file_uri": "b.file_uri"}}, + {"id": "b", "node_type": "echo", "inputs": {"file_uri": "a.file_uri"}}, + ], edges=[{"from": "a", "to": "b"}, {"from": "b", "to": "a"}]) + + # 测试过程 + response = client.post("/api/admin/workflows", json={ + "id": "wf-cycle", "name": "环形", "description": "", "definition": cyclic, + }) + + # 验证结果:422,且未写库。 + assert response.status_code == 422 + assert client.get("/api/admin/workflows/wf-cycle").status_code == 404 + + +def test_create_rejects_self_loop(client: TestClient) -> None: + """自环(节点指向自己)同样被拒绝。""" + # 数据:单节点自环。 + loop = _definition( + nodes=[{"id": "a", "node_type": "echo", "inputs": {"file_uri": "a.file_uri"}}], + edges=[{"from": "a", "to": "a"}], + ) + + # 测试过程 + response = client.post("/api/admin/workflows", json={ + "id": "wf-loop", "name": "自环", "description": "", "definition": loop, + }) + + # 验证结果 + assert response.status_code == 422 + + +def test_create_rejects_edge_to_unknown_node(client: TestClient) -> None: + """边引用不存在的节点被拒绝。""" + # 数据:边指向 ghost 节点。 + bad = _definition(edges=[{"from": "step", "to": "ghost"}]) + + # 测试过程 + response = client.post("/api/admin/workflows", json={ + "id": "wf-bad", "name": "坏边", "description": "", "definition": bad, + }) + + # 验证结果 + assert response.status_code == 422 + + +def test_create_rejects_duplicate_node_ids(client: TestClient) -> None: + """节点 ID 重复被拒绝。""" + # 数据:两个同 ID 节点。 + dup = _definition(nodes=[ + {"id": "step", "node_type": "echo", "inputs": {}}, + {"id": "step", "node_type": "echo", "inputs": {}}, + ]) + + # 测试过程 + response = client.post("/api/admin/workflows", json={ + "id": "wf-dup", "name": "重复", "description": "", "definition": dup, + }) + + # 验证结果 + assert response.status_code == 422 + + +def test_validate_endpoint_returns_node_ids(client: TestClient) -> None: + """校验接口对合法定义返回 valid 与节点 ID 列表(不保存)。""" + # 数据:已创建工作流 + 待校验定义。 + _create(client, "wf-1") + + # 测试过程 + response = client.post("/api/admin/workflows/wf-1/validate", json=_definition()) + + # 验证结果 + assert response.status_code == 200 + assert response.json() == {"valid": True, "node_ids": ["step"]} + + +def test_validate_endpoint_rejects_cycle(client: TestClient) -> None: + """校验接口对环形定义返回 422。""" + # 数据:环形定义。 + _create(client, "wf-1") + cyclic = _definition(nodes=[ + {"id": "a", "node_type": "echo", "inputs": {"file_uri": "b.file_uri"}}, + {"id": "b", "node_type": "echo", "inputs": {"file_uri": "a.file_uri"}}, + ], edges=[{"from": "a", "to": "b"}, {"from": "b", "to": "a"}]) + + # 测试过程与验证结果 + assert client.post("/api/admin/workflows/wf-1/validate", json=cyclic).status_code == 422 + + +# --------------------------------------------------------------------------- +# 发布与版本 +# --------------------------------------------------------------------------- + + +def test_publish_marks_workflow_published(client: TestClient) -> None: + """发布后工作流标记为已发布(出现在用户应用中心)。""" + # 数据:已创建工作流。 + _create(client, "wf-1") + + # 测试过程 + response = client.post("/api/admin/workflows/wf-1/publish") + detail = client.get("/api/admin/workflows/wf-1").json() + + # 验证结果 + assert response.status_code == 200 + assert detail["published"] == 1 + + +def test_publish_rejects_legacy_cycle_version(client: TestClient) -> None: + """历史遗留的环形版本无法发布(发布前重新校验,R04)。""" + # 数据:绕过创建校验,直接写入环形版本(模拟历史数据)。 + _create(client, "wf-1") + db = client.app.state.db + cyclic = _definition(nodes=[ + {"id": "a", "node_type": "echo", "inputs": {"file_uri": "b.file_uri"}}, + {"id": "b", "node_type": "echo", "inputs": {"file_uri": "a.file_uri"}}, + ], edges=[{"from": "a", "to": "b"}, {"from": "b", "to": "a"}]) + db.create_workflow_version("wf-1", 9, cyclic) + + # 测试过程 + response = client.post("/api/admin/workflows/wf-1/publish") + + # 验证结果:被拒绝。 + assert response.status_code == 422 + + +def test_publish_rejects_workflow_without_version(client: TestClient) -> None: + """无版本的工作流不能发布。""" + # 数据:只有工作流记录,无版本(绕过创建接口)。 + db = client.app.state.db + db.upsert_workflow({"id": "empty", "name": "空", "description": "", "latest_version": 0}) + + # 测试过程 + response = client.post("/api/admin/workflows/empty/publish") + + # 验证结果 + assert response.status_code == 422 + + +def test_list_versions_returns_history_desc(client: TestClient) -> None: + """版本历史按版本号倒序返回,供对比与回滚。""" + # 数据:两个版本。 + _create(client, "wf-1") + _create(client, "wf-1") + + # 测试过程 + versions = client.get("/api/admin/workflows/wf-1/versions").json() + + # 验证结果 + assert [item["version"] for item in versions] == [2, 1] + + +def test_delete_workflow_removes_it(client: TestClient) -> None: + """删除工作流后查询 404。""" + # 数据:已创建工作流。 + _create(client, "wf-1") + + # 测试过程 + response = client.delete("/api/admin/workflows/wf-1") + + # 验证结果 + assert response.status_code == 200 + assert client.get("/api/admin/workflows/wf-1").status_code == 404 + + +def test_delete_missing_workflow_returns_404(client: TestClient) -> None: + """删除不存在的工作流返回 404。""" + # 数据:未创建。 + # 测试过程与验证结果 + assert client.delete("/api/admin/workflows/nope").status_code == 404 + + +def test_created_workflow_id_slugified_from_name_when_absent(client: TestClient) -> None: + """未给 ID 时由名称生成 slug 形式 ID(管理端便利行为)。""" + # 数据:只给名称。 + # 测试过程 + response = client.post("/api/admin/workflows", json={ + "name": "My Cool Flow", "description": "", "definition": _definition(), + }) + + # 验证结果 + assert response.status_code == 200 + assert response.json()["id"] == "my-cool-flow" diff --git a/tests/app/test_scheduler/__init__.py b/tests/app/test_scheduler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/app/test_scheduler/test_scheduler.py b/tests/app/test_scheduler/test_scheduler.py new file mode 100644 index 0000000..6848ead --- /dev/null +++ b/tests/app/test_scheduler/test_scheduler.py @@ -0,0 +1,494 @@ +"""src/wov_app/scheduler.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`src/wov_app/scheduler.py`(DAG 拓扑调度、断点续跑、暂停语义), +可独立调用。用例使用真实 SQLite、真实存储目录与真实节点(echo/echo 派生), +仅对需要外部服务的节点不做测试。 +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from wov_app import registry +from wov_app.db import Database +from wov_app.scheduler import WorkflowScheduler, topological_sort +from wov_sdk.models import WorkflowDefinition + + +@pytest.fixture(autouse=True) +def _isolate_registry(): + """用例前清空注册表、用例后恢复快照:保证用例看到的是干净基线, + 不受其他模块(如 main 生命周期 register_all)的注册结果影响。""" + snapshot = dict(registry._registry) + registry._registry.clear() + yield + registry._registry.clear() + registry._registry.update(snapshot) + + +def _definition(nodes: list[dict], edges: list[dict], **extra) -> WorkflowDefinition: + """构造真实 WorkflowDefinition(节点 ID/类型/输入与边)。""" + payload = { + "name": "测试流程", + "version": 1, + "nodes": nodes, + "edges": edges, + "entry_inputs": {"video_uri": "file"}, + "final_outputs": extra.pop("final_outputs", {}), + **extra, + } + return WorkflowDefinition.from_dict(payload) + + +def _db_with_workflow(tmp_path: Path, definition: WorkflowDefinition, workflow_id: str = "wf") -> Database: + """建好工作流 + 版本记录的临时库(任务表有外键约束)。""" + db = Database(tmp_path / "wov.db") + db.upsert_workflow({"id": workflow_id, "name": "流程", "description": ""}) + db.create_workflow_version(workflow_id, 1, definition.to_dict()) + return db + + +def _run(run_id: str, input_uri: str, **overrides) -> dict: + """构造真实任务记录。""" + record = { + "id": run_id, "workflow_id": "wf", "workflow_version": 1, "status": "QUEUED", + "current_node_id": None, "progress": 0.0, "error": None, "input_uri": input_uri, + "param_overrides": None, "source": "upload", + "created_at": "2026-09-01T00:00:00+00:00", "updated_at": "2026-09-01T00:00:00+00:00", + } + record.update(overrides) + return record + + +def _scheduler(db: Database, storage: Path) -> WorkflowScheduler: + """构造调度器(不启动后台线程,直接调用 execute_run)。""" + return WorkflowScheduler(db, storage, interval_seconds=999) + + +# --------------------------------------------------------------------------- +# 拓扑排序 +# --------------------------------------------------------------------------- + + +def test_topological_sort_linear_chain() -> None: + """线性链按依赖顺序返回。""" + # 数据:a → b → c。 + definition = _definition( + nodes=[ + {"id": "a", "node_type": "echo", "inputs": {}}, + {"id": "b", "node_type": "echo", "inputs": {"file_uri": "a.file_uri"}}, + {"id": "c", "node_type": "echo", "inputs": {"file_uri": "b.file_uri"}}, + ], + edges=[{"from": "a", "to": "b"}, {"from": "b", "to": "c"}], + ) + + # 测试过程 + order = topological_sort(definition) + + # 验证结果 + assert order == ["a", "b", "c"] + + +def test_topological_sort_diamond() -> None: + """菱形依赖中,汇合节点排在其全部前驱之后。""" + # 数据:a → (b, c) → d。 + definition = _definition( + nodes=[ + {"id": "a", "node_type": "echo", "inputs": {}}, + {"id": "b", "node_type": "echo", "inputs": {"file_uri": "a.file_uri"}}, + {"id": "c", "node_type": "echo", "inputs": {"file_uri": "a.file_uri"}}, + {"id": "d", "node_type": "echo", "inputs": {"file_uri": "b.file_uri"}}, + ], + edges=[ + {"from": "a", "to": "b"}, {"from": "a", "to": "c"}, + {"from": "b", "to": "d"}, {"from": "c", "to": "d"}, + ], + ) + + # 测试过程 + order = topological_sort(definition) + + # 验证结果:a 最先、d 最后,b/c 在中间。 + assert order[0] == "a" + assert order[-1] == "d" + assert set(order[1:3]) == {"b", "c"} + + +# --------------------------------------------------------------------------- +# 执行:成功路径 +# --------------------------------------------------------------------------- + + +def test_execute_run_completes_and_records_artifacts(tmp_path: Path) -> None: + """单节点任务执行成功:状态 COMPLETED、产物登记、进度到位。""" + # 数据:echo 单节点工作流 + 真实输入文件。 + definition = _definition( + nodes=[{"id": "step", "node_type": "echo", "inputs": {"file_uri": "input.video_uri"}}], + edges=[], + final_outputs={"result": "step.file_uri"}, + ) + storage = tmp_path / "storage" + db = _db_with_workflow(tmp_path, definition) + source = tmp_path / "input.txt" + source.write_text("输入内容", encoding="utf-8") + db.create_run(_run("run-1", str(source))) + registry.register_all() + + # 测试过程 + _scheduler(db, storage).execute_run("run-1") + + # 验证结果:任务完成、进度 1.0、节点产物与最终别名都已登记。 + stored = db.get_run("run-1") + assert stored["status"] == "COMPLETED" + assert stored["progress"] == 1.0 + names = {a["name"] for a in db.list_artifacts("run-1")} + assert "step.file_uri" in names + assert "result" in names + + +def test_execute_run_multi_node_chain_passes_artifacts(tmp_path: Path) -> None: + """多节点链:后序节点通过 URI 拿到前序产物(节点间只经产物交换数据)。""" + # 数据:step1 → step2 两节点链。 + definition = _definition( + nodes=[ + {"id": "step1", "node_type": "echo", "inputs": {"file_uri": "input.video_uri"}}, + {"id": "step2", "node_type": "echo", "inputs": {"file_uri": "step1.file_uri"}}, + ], + edges=[{"from": "step1", "to": "step2"}], + final_outputs={"out": "step2.file_uri"}, + ) + storage = tmp_path / "storage" + db = _db_with_workflow(tmp_path, definition) + source = tmp_path / "input.txt" + source.write_text("链式内容", encoding="utf-8") + db.create_run(_run("run-2", str(source))) + registry.register_all() + + # 测试过程 + _scheduler(db, storage).execute_run("run-2") + + # 验证结果:两节点产物都存在,且 step2 的产物内容来自 step1(传递一致)。 + assert db.get_run("run-2")["status"] == "COMPLETED" + artifacts = {a["name"]: a["uri"] for a in db.list_artifacts("run-2")} + assert "step1.file_uri" in artifacts and "step2.file_uri" in artifacts + assert Path(artifacts["step2.file_uri"]).read_text(encoding="utf-8") == "链式内容" + assert Path(artifacts["step2.file_uri"]).parent != Path(artifacts["step1.file_uri"]).parent + + +def test_execute_run_creates_final_alias_with_stable_name(tmp_path: Path) -> None: + """最终产物按 上传文件名.标识.时间戳 生成别名,并保留节点原始文件。""" + # 数据:单节点 + target_language 参数(决定别名标识)。 + definition = _definition( + nodes=[{ + "id": "step", "node_type": "echo", + "inputs": {"file_uri": "input.video_uri"}, + "params": {"target_language": "zh-CN"}, + }], + edges=[], + final_outputs={"cn_srt_uri": "step.file_uri"}, + ) + storage = tmp_path / "storage" + db = _db_with_workflow(tmp_path, definition) + source = tmp_path / "test01.mp4" + source.write_text("数据", encoding="utf-8") + db.create_run(_run("run-3", str(source))) + registry.register_all() + + # 测试过程 + _scheduler(db, storage).execute_run("run-3") + + # 验证结果:别名指向 finals 下的稳定路径,含 zh-CN 标识;节点原文件仍在。 + artifacts = {a["name"]: a["uri"] for a in db.list_artifacts("run-3")} + final = Path(artifacts["cn_srt_uri"]) + assert final.is_file() + assert "zh-CN" in final.name + assert "finals" in final.parts + assert Path(artifacts["step.file_uri"]).is_file() + + +# --------------------------------------------------------------------------- +# 失败与无效 DAG +# --------------------------------------------------------------------------- + + +def test_execute_run_fails_when_workflow_version_missing(tmp_path: Path) -> None: + """工作流版本记录丢失时任务失败(不留 QUEUED 堵塞队列)。""" + # 数据:工作流存在但没有 v1 版本记录(外键仍满足)。 + db = Database(tmp_path / "wov.db") + storage = tmp_path / "storage" + db.upsert_workflow({"id": "wf-no-version", "name": "流程", "description": ""}) + db.create_run(_run("run-x", "input", workflow_id="wf-no-version")) + + # 测试过程 + _scheduler(db, storage).execute_run("run-x") + + # 验证结果 + stored = db.get_run("run-x") + assert stored["status"] == "FAILED" + assert "workflow version not found" in stored["error"] + + +def test_execute_run_marks_failed_on_cycle_and_does_not_block_queue(tmp_path: Path) -> None: + """环形 DAG(历史无效版本)立即失败且不堵塞后续任务(R04 回归)。""" + # 数据:A→B→A 的环形定义。 + definition = _definition( + nodes=[ + {"id": "a", "node_type": "echo", "inputs": {"file_uri": "b.file_uri"}}, + {"id": "b", "node_type": "echo", "inputs": {"file_uri": "a.file_uri"}}, + ], + edges=[{"from": "a", "to": "b"}, {"from": "b", "to": "a"}], + ) + storage = tmp_path / "storage" + db = _db_with_workflow(tmp_path, definition) + db.create_run(_run("run-cycle", "input")) + registry.register_all() + + # 测试过程 + _scheduler(db, storage).execute_run("run-cycle") + + # 验证结果:任务 FAILED,且队首前移(不再返回该任务)。 + assert db.get_run("run-cycle")["status"] == "FAILED" + assert db.next_queued_run() is None + + +def test_execute_run_fails_when_input_reference_missing(tmp_path: Path) -> None: + """输入引用无法解析(前序产物缺失)时任务失败并记录原因。""" + # 数据:节点引用不存在的产物。 + definition = _definition( + nodes=[{"id": "step", "node_type": "echo", "inputs": {"file_uri": "ghost.file_uri"}}], + edges=[], + ) + storage = tmp_path / "storage" + db = _db_with_workflow(tmp_path, definition) + db.create_run(_run("run-4", "input")) + registry.register_all() + + # 测试过程 + _scheduler(db, storage).execute_run("run-4") + + # 验证结果 + stored = db.get_run("run-4") + assert stored["status"] == "FAILED" + assert "missing input" in stored["error"] + + +def test_execute_run_fails_when_node_returns_failed(tmp_path: Path) -> None: + """节点返回 failed 时任务失败并保留错误信息。""" + # 数据:注册一个总是失败的节点。 + from wov_sdk.models import InvokeResponse, NodeManifest + + definition = _definition( + nodes=[{"id": "step", "node_type": "boom", "inputs": {}}], + edges=[], + ) + storage = tmp_path / "storage" + db = _db_with_workflow(tmp_path, definition) + db.create_run(_run("run-5", "input")) + registry.register( + NodeManifest(id="boom", name="失败节点", version="1", capability="c", command=["python"]), + lambda request: InvokeResponse(status="failed", error="节点内部错误"), + ) + + # 测试过程 + _scheduler(db, storage).execute_run("run-5") + + # 验证结果 + stored = db.get_run("run-5") + assert stored["status"] == "FAILED" + assert "节点内部错误" in stored["error"] + + +# --------------------------------------------------------------------------- +# 暂停语义 +# --------------------------------------------------------------------------- + + +def test_execute_run_leaves_paused_run_untouched(tmp_path: Path) -> None: + """以 PAUSED 进入时直接返回保持暂停(修复"点击暂停反而开始任务")。""" + # 数据:PAUSED 状态的任务。 + definition = _definition( + nodes=[{"id": "step", "node_type": "echo", "inputs": {}}], + edges=[], + ) + storage = tmp_path / "storage" + db = _db_with_workflow(tmp_path, definition) + db.create_run(_run("run-6", "input", status="PAUSED")) + registry.register_all() + + # 测试过程 + _scheduler(db, storage).execute_run("run-6") + + # 验证结果:仍为 PAUSED,且未产生任何产物。 + assert db.get_run("run-6")["status"] == "PAUSED" + assert db.list_artifacts("run-6") == [] + + +def test_execute_run_stops_at_node_boundary_when_paused(tmp_path: Path) -> None: + """运行中被暂停:在当前节点边界停下保持 PAUSED,不标 FAILED。""" + # 数据:两节点链;第一个节点执行时把任务置 PAUSED。 + from wov_sdk.models import InvokeResponse, NodeManifest + + definition = _definition( + nodes=[ + {"id": "step1", "node_type": "pause-me", "inputs": {}}, + {"id": "step2", "node_type": "echo", "inputs": {}}, + ], + edges=[{"from": "step1", "to": "step2"}], + ) + storage = tmp_path / "storage" + db = _db_with_workflow(tmp_path, definition) + db.create_run(_run("run-7", "input")) + registry.register_all() + + def pause_handler(request): + """模拟用户在该节点执行期间点击暂停。""" + db.pause_run("run-7", "t2") + return InvokeResponse(status="completed", outputs={"file_uri": "/tmp/x"}) + + registry.register( + NodeManifest(id="pause-me", name="暂停节点", version="1", capability="c", command=["python"]), + pause_handler, + ) + + # 测试过程 + _scheduler(db, storage).execute_run("run-7") + + # 验证结果:保持 PAUSED,step2 未执行。 + assert db.get_run("run-7")["status"] == "PAUSED" + names = {a["name"] for a in db.list_artifacts("run-7")} + assert not any(name.startswith("step2") for name in names) + + +def test_execute_run_clears_stale_pause_flag(tmp_path: Path) -> None: + """执行前清理残留的 paused.flag(避免误触发节点内暂停)。""" + # 数据:单节点任务 + 已存在的 paused.flag。 + definition = _definition( + nodes=[{"id": "step", "node_type": "echo", "inputs": {}}], + edges=[], + ) + storage = tmp_path / "storage" + db = _db_with_workflow(tmp_path, definition) + db.create_run(_run("run-8", "input")) + registry.register_all() + run_root = storage / "runs" / "run-8" + run_root.mkdir(parents=True) + (run_root / "paused.flag").write_text("", encoding="utf-8") + + # 测试过程 + _scheduler(db, storage).execute_run("run-8") + + # 验证结果:标志被清除,任务正常完成。 + assert not (run_root / "paused.flag").exists() + assert db.get_run("run-8")["status"] == "COMPLETED" + + +# --------------------------------------------------------------------------- +# 断点续跑与参数覆盖 +# --------------------------------------------------------------------------- + + +def test_execute_run_resumes_from_existing_artifacts(tmp_path: Path) -> None: + """断点续跑:已有产物的节点被跳过,只执行剩余节点。""" + # 数据:两节点链,step1 产物已登记。 + calls: list[str] = [] + + def tracking_handler(node_id: str): + def handler(request): + from wov_sdk.models import InvokeResponse + + calls.append(node_id) + return InvokeResponse(status="completed", outputs={"file_uri": f"/tmp/{node_id}"}) + + return handler + + definition = _definition( + nodes=[ + {"id": "step1", "node_type": "track1", "inputs": {}}, + {"id": "step2", "node_type": "track2", "inputs": {}}, + ], + edges=[{"from": "step1", "to": "step2"}], + ) + storage = tmp_path / "storage" + db = _db_with_workflow(tmp_path, definition) + db.create_run(_run("run-9", "input")) + db.create_artifact({ + "run_id": "run-9", "node_id": "step1", "name": "step1.file_uri", + "uri": "/tmp/step1", "kind": "file", + }) + from wov_sdk.models import NodeManifest + + registry.register( + NodeManifest(id="track1", name="t1", version="1", capability="c", command=["python"]), + tracking_handler("step1"), + ) + registry.register( + NodeManifest(id="track2", name="t2", version="1", capability="c", command=["python"]), + tracking_handler("step2"), + ) + + # 测试过程 + _scheduler(db, storage).execute_run("run-9") + + # 验证结果:只调用了 step2。 + assert calls == ["step2"] + assert db.get_run("run-9")["status"] == "COMPLETED" + + +def test_execute_run_applies_param_overrides(tmp_path: Path) -> None: + """param_overrides 按节点 ID 合并进节点参数(前端框选 crop 的通道)。""" + # 数据:节点参数与覆盖值同时存在。 + received: list[dict] = [] + + def handler(request): + from wov_sdk.models import InvokeResponse + + received.append(dict(request.params)) + return InvokeResponse(status="completed", outputs={"file_uri": "/tmp/x"}) + + definition = _definition( + nodes=[{ + "id": "step", "node_type": "param-node", + "inputs": {}, "params": {"interval_seconds": 0.5, "crop": [0, 0, 1, 1]}, + }], + edges=[], + ) + storage = tmp_path / "storage" + db = _db_with_workflow(tmp_path, definition) + db.create_run(_run("run-10", "input", param_overrides={"step": {"crop": [0, 0.75, 1, 0.25]}})) + from wov_sdk.models import NodeManifest + + registry.register( + NodeManifest(id="param-node", name="p", version="1", capability="c", command=["python"]), + handler, + ) + + # 测试过程 + _scheduler(db, storage).execute_run("run-10") + + # 验证结果:覆盖值生效,未覆盖的参数保持原样。 + assert received == [{"interval_seconds": 0.5, "crop": [0, 0.75, 1, 0.25]}] + + +# --------------------------------------------------------------------------- +# 线程生命周期 +# --------------------------------------------------------------------------- + + +def test_start_and_stop_are_idempotent(tmp_path: Path) -> None: + """start 重复调用不产生多余线程;stop 正常结束。""" + # 数据:空库 + 调度器。 + db = Database(tmp_path / "wov.db") + scheduler = WorkflowScheduler(db, tmp_path / "storage", interval_seconds=999) + + # 测试过程 + scheduler.start() + first = scheduler._thread + scheduler.start() + second = scheduler._thread + scheduler.stop() + + # 验证结果 + assert first is second + assert scheduler._thread is None diff --git a/tests/app/test_seed/__init__.py b/tests/app/test_seed/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/app/test_seed/test_seed.py b/tests/app/test_seed/test_seed.py new file mode 100644 index 0000000..4a7773a --- /dev/null +++ b/tests/app/test_seed/test_seed.py @@ -0,0 +1,127 @@ +"""src/wov_app/seed.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`src/wov_app/seed.py`(从 workflows/*.json 载入内置工作流,幂等), +可独立调用。用例使用真实仓库工作流 JSON 与临时 SQLite 库。 +""" + +from __future__ import annotations + +import json +from pathlib import Path + +from wov_app.db import Database +from wov_app.seed import seed_default_workflows + +# 仓库根与真实内置工作流目录。 +WORKSPACE = Path(__file__).resolve().parents[3] +WORKFLOWS_DIR = WORKSPACE / "workflows" + + +def _db(tmp_path: Path) -> Database: + """每个用例一个临时 SQLite 库。""" + return Database(tmp_path / "wov.db") + + +def test_seed_loads_all_builtin_workflows(tmp_path: Path) -> None: + """真实 workflows/*.json 全部写入库,且数量与文件数一致。""" + # 数据:仓库真实工作流数据文件。 + db = _db(tmp_path) + expected = {p.stem for p in WORKFLOWS_DIR.glob("*.json")} + assert expected, "仓库应至少有一个内置工作流数据文件" + + # 测试过程 + created = seed_default_workflows(db, WORKFLOWS_DIR) + stored = {w["id"] for w in db.list_workflows()} + + # 验证结果 + assert created == len(expected) + assert stored == expected + + +def test_seed_creates_version_record_with_valid_dag(tmp_path: Path) -> None: + """每个工作流写入 v1 版本记录,DAG 通过结构校验(节点/边合法)。""" + # 数据:真实工作流目录。 + db = _db(tmp_path) + + # 测试过程 + seed_default_workflows(db, WORKFLOWS_DIR) + + # 验证结果:以真实 OCR 工作流为例,节点与版本齐全。 + version = db.get_latest_workflow_version("ocr-subtitle") + assert version is not None + definition = version["definition"] + node_ids = {n["id"] for n in definition["nodes"]} + assert {"extract", "ocr", "filter"} <= node_ids + assert definition["edges"] + + +def test_seed_is_idempotent(tmp_path: Path) -> None: + """重复 seed 不覆盖已有工作流,第二次返回创建数 0。""" + # 数据:先 seed 一次。 + db = _db(tmp_path) + first = seed_default_workflows(db, WORKFLOWS_DIR) + + # 测试过程:再次 seed。 + second = seed_default_workflows(db, WORKFLOWS_DIR) + + # 验证结果 + assert first > 0 + assert second == 0 + assert len(db.list_workflows()) == first + + +def test_seed_does_not_overwrite_user_modification(tmp_path: Path) -> None: + """已存在的工作流不被 seed 覆盖(保护用户改过的数据)。""" + # 数据:先写入一个与内置同 ID 的自定义工作流。 + db = _db(tmp_path) + db.upsert_workflow({ + "id": "zh-direct", "name": "用户改过的名字", "description": "", "published": 0, + "latest_version": 1, + }) + + # 测试过程 + seed_default_workflows(db, WORKFLOWS_DIR) + + # 验证结果:用户版本保留。 + assert db.get_workflow("zh-direct")["name"] == "用户改过的名字" + + +def test_seed_from_empty_directory_creates_nothing(tmp_path: Path) -> None: + """空目录不创建任何工作流(数据驱动,无硬编码兜底)。""" + # 数据:空目录。 + empty = tmp_path / "workflows" + empty.mkdir() + db = _db(tmp_path) + + # 测试过程 + created = seed_default_workflows(db, empty) + + # 验证结果 + assert created == 0 + assert db.list_workflows() == [] + + +def test_seed_marks_workflows_published(tmp_path: Path) -> None: + """内置工作流默认已发布(用户端可直接选择执行)。""" + # 数据:真实工作流目录。 + db = _db(tmp_path) + + # 测试过程 + seed_default_workflows(db, WORKFLOWS_DIR) + + # 验证结果 + for workflow in db.list_workflows(): + assert workflow["published"] == 1 + + +def test_seed_honors_version_from_data_file(tmp_path: Path) -> None: + """版本号取自数据文件(不同工作流可有不同当前版本)。""" + # 数据:真实工作流目录(含 v7 的 ocr-subtitle)。 + db = _db(tmp_path) + + # 测试过程 + seed_default_workflows(db, WORKFLOWS_DIR) + + # 验证结果:与数据文件声明一致。 + payload = json.loads((WORKFLOWS_DIR / "ocr-subtitle.json").read_text(encoding="utf-8")) + assert db.get_workflow("ocr-subtitle")["latest_version"] == int(payload.get("version", 1)) diff --git a/tests/app/test_storage/__init__.py b/tests/app/test_storage/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/app/test_storage/test_atomic_copy.py b/tests/app/test_storage/test_atomic_copy.py new file mode 100644 index 0000000..11645b1 --- /dev/null +++ b/tests/app/test_storage/test_atomic_copy.py @@ -0,0 +1,131 @@ +"""src/wov_app/storage.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`src/wov_app/storage.py`(产物文件原子复制),被 scheduler 与 +batch 复用,也可独立调用。用例使用真实文件系统(tmp_path)验证复制完整性与 +失败时对原目标的保护。 +""" + +from __future__ import annotations + +import stat +from pathlib import Path + +import pytest + +from wov_app.storage import atomic_copy + + +def test_atomic_copy_creates_target_with_same_content(tmp_path: Path) -> None: + """复制后目标存在且内容与源一致(源保留)。""" + # 数据:真实的源文件(含中文内容)。 + source = tmp_path / "src.srt" + source.write_text("1\n00:00:01,000 --> 00:00:02,000\n你好\n", encoding="utf-8") + target = tmp_path / "out" / "movie.CN.srt" + + # 测试过程 + atomic_copy(source, target) + + # 验证结果:源保留、目标内容一致。 + assert source.is_file() + assert target.read_text(encoding="utf-8") == source.read_text(encoding="utf-8") + + +def test_atomic_copy_creates_parent_directories(tmp_path: Path) -> None: + """目标父目录不存在时自动创建(产物目录可能尚未建立)。""" + # 数据:深层不存在的目录。 + source = tmp_path / "a.txt" + source.write_text("x", encoding="utf-8") + target = tmp_path / "deep" / "nested" / "dir" / "a.txt" + + # 测试过程 + atomic_copy(source, target) + + # 验证结果 + assert target.is_file() + assert target.parent.is_dir() + + +def test_atomic_copy_overwrites_existing_target(tmp_path: Path) -> None: + """目标已存在时被完整替换(旧内容不残留)。""" + # 数据:已存在的旧目标(内容更长,用于检测残留)。 + source = tmp_path / "new.srt" + source.write_text("新", encoding="utf-8") + target = tmp_path / "movie.CN.srt" + target.write_text("旧的非常长的内容" * 100, encoding="utf-8") + + # 测试过程 + atomic_copy(source, target) + + # 验证结果:内容被完全替换,无旧内容残留。 + assert target.read_text(encoding="utf-8") == "新" + + +def test_atomic_copy_preserves_binary_content(tmp_path: Path) -> None: + """二进制内容(如 ASS 的 BOM/CRLF)逐字节一致。""" + # 数据:带 BOM 与 CRLF 的字节序列。 + payload = "\ufeff[Script Info]\r\nTitle: 测试\r\n".encode("utf-8") + source = tmp_path / "src.ass" + source.write_bytes(payload) + target = tmp_path / "dst.ass" + + # 测试过程 + atomic_copy(source, target) + + # 验证结果:逐字节一致。 + assert target.read_bytes() == payload + + +def test_atomic_copy_cleans_temp_file_on_success(tmp_path: Path) -> None: + """成功后不留下临时文件(目录里只有源与目标)。""" + # 数据:源文件。 + source = tmp_path / "src.txt" + source.write_text("x", encoding="utf-8") + target = tmp_path / "dst.txt" + + # 测试过程 + atomic_copy(source, target) + + # 验证结果:无 *.tmp 残留。 + assert not list(tmp_path.glob("*.tmp")) + + +def test_atomic_copy_keeps_target_and_cleans_temp_on_failure(tmp_path: Path) -> None: + """复制失败时保留原目标、清理临时文件(R03:不暴露写了一半的成品)。""" + # 数据:源文件不存在(触发 copy2 失败),目标已存在有效内容。 + missing = tmp_path / "missing.txt" + target = tmp_path / "movie.CN.srt" + target.write_text("原有有效成品", encoding="utf-8") + + # 测试过程与验证结果:抛错,原目标未被破坏,无临时文件残留。 + with pytest.raises(FileNotFoundError): + atomic_copy(missing, target) + assert target.read_text(encoding="utf-8") == "原有有效成品" + assert not list(tmp_path.glob("*.tmp")) + + +def test_atomic_copy_exists_with_missing_target_on_failure(tmp_path: Path) -> None: + """复制失败且目标原本不存在时,不产生残缺目标文件。""" + # 数据:源不存在、目标不存在。 + missing = tmp_path / "missing.txt" + target = tmp_path / "new.CN.srt" + + # 测试过程与验证结果 + with pytest.raises(FileNotFoundError): + atomic_copy(missing, target) + assert not target.exists() + assert not list(tmp_path.glob("*.tmp")) + + +def test_atomic_copy_preserves_source_permissions_semantics(tmp_path: Path) -> None: + """目标可读(copy2 保留元数据,权限不会导致后续读取失败)。""" + # 数据:普通源文件。 + source = tmp_path / "src.txt" + source.write_text("内容", encoding="utf-8") + target = tmp_path / "dst.txt" + + # 测试过程 + atomic_copy(source, target) + + # 验证结果:目标可读且非空。 + assert target.read_text(encoding="utf-8") == "内容" + assert target.stat().st_size > 0 diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 2fc5635..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,44 +0,0 @@ -"""pytest 全局配置。 - -在测试进程启动时创建独立临时目录,并通过环境变量把应用的数据目录、数据库、 -存储和后台服务全部指向测试环境,避免污染本地开发数据;同时隔离进程内节点 -注册表,防止测试之间互相泄漏注册条目。 -""" - -import atexit -import os -import shutil -import tempfile -from pathlib import Path - -import pytest - -# 每个测试进程使用独立临时根目录,保证测试之间互不干扰。 -TEST_ROOT = Path(tempfile.mkdtemp(prefix="vrsub-test-")) -os.environ["WOV_DATA_DIR"] = str(TEST_ROOT / "data") -os.environ["WOV_DB_PATH"] = str(TEST_ROOT / "data" / "wov.db") -os.environ["WOV_STORAGE_DIR"] = str(TEST_ROOT / "storage") -# 默认关闭自动种子和后台调度,测试显式控制执行时机。 -os.environ["WOV_AUTO_SEED"] = "0" -os.environ["WOV_SCHEDULER_ENABLED"] = "0" -os.environ["WOV_CLEANUP_ENABLED"] = "0" -# 默认关闭批量引擎后台线程:API 测试手工控制执行时机,避免后台线程与断言竞态。 -os.environ["WOV_BATCH_ENABLED"] = "0" - -def _cleanup() -> None: - """进程退出时清理临时测试目录。""" - shutil.rmtree(TEST_ROOT, ignore_errors=True) - - -atexit.register(_cleanup) - - -@pytest.fixture(autouse=True) -def _isolate_registry(): - """快照并恢复进程内节点注册表,避免测试之间互相污染。""" - from wov_app import registry - - snapshot = dict(registry._registry) - yield - registry._registry.clear() - registry._registry.update(snapshot) diff --git a/tests/nodes/__init__.py b/tests/nodes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/nodes/test_adaptive_pool/__init__.py b/tests/nodes/test_adaptive_pool/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/nodes/test_adaptive_pool/test_pool.py b/tests/nodes/test_adaptive_pool/test_pool.py new file mode 100644 index 0000000..e97c5b9 --- /dev/null +++ b/tests/nodes/test_adaptive_pool/test_pool.py @@ -0,0 +1,346 @@ +"""nodes/adaptive_pool.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`nodes/adaptive_pool.py`(自适应并发额度控制),被 subtitle-ocr 与 +llm-filter 复用,也可独立使用,因此拥有独立模块目录。 + +测试只调用真实并发池,`clock` 注入假时钟以实现确定性的窗口行为;worker 用 +真实可执行函数(无 I/O 依赖),不重写被测逻辑。 +""" + +from __future__ import annotations + +import threading +import time + +from nodes.adaptive_pool import AdaptiveThreadPool, decide + + +class FakeClock: + """可手动拨动的假时钟(时间属于允许在 I/O 边界注入的依赖)。""" + + def __init__(self, now: float = 0.0) -> None: + self.now = now + + def __call__(self) -> float: + return self.now + + def advance(self, seconds: float) -> None: + self.now += seconds + + +# --------------------------------------------------------------------------- +# 决策函数 +# --------------------------------------------------------------------------- + + +def test_decide_increases_when_response_fast() -> None: + """平均响应低于快阈值且未达上限:额度 +1。""" + # 数据:当前 1、均值 0.1、上限 16。 + # 测试过程与验证结果 + assert decide(1, 0.1, 1, 16, 0.3, 1.0) == 2 + + +def test_decide_decreases_when_response_slow() -> None: + """平均响应高于慢阈值且高于下限:额度 -1。""" + # 数据:当前 3、均值 2.0。 + # 测试过程与验证结果 + assert decide(3, 2.0, 1, 16, 0.3, 1.0) == 2 + + +def test_decide_keeps_when_response_between_thresholds() -> None: + """响应介于两阈值之间时额度不变。""" + # 数据:当前 2、均值 0.5。 + # 测试过程与验证结果 + assert decide(2, 0.5, 1, 16, 0.3, 1.0) == 2 + + +def test_decide_respects_bounds() -> None: + """已达上限不再增、已达下限不再减。""" + # 数据:上限 16 且很快;下限 1 且很慢。 + # 测试过程与验证结果 + assert decide(16, 0.1, 1, 16, 0.3, 1.0) == 16 + assert decide(1, 2.0, 1, 16, 0.3, 1.0) == 1 + + +# --------------------------------------------------------------------------- +# map 基本行为 +# --------------------------------------------------------------------------- + + +def test_map_returns_results_in_input_order() -> None: + """并发执行但结果严格按输入顺序返回,保证字幕时间轴不被并发打乱。""" + # 数据:让后面的任务更早开始的输入;worker 是确定性的纯函数。 + items = [3, 2, 1] + pool = AdaptiveThreadPool(worker=lambda value: value * 100) + + # 测试过程 + results = pool.map(items) + + # 验证结果:顺序与输入一致。 + assert results == [300, 200, 100] + + +def test_map_empty_input_returns_empty_list() -> None: + """空输入返回空结果,不启动任务也不报错。""" + # 数据:空列表。 + pool = AdaptiveThreadPool(worker=lambda item: item) + + # 测试过程与验证结果 + assert pool.map([]) == [] + + +def test_map_isolates_worker_exception_as_result() -> None: + """worker 抛出的异常作为该位置的结果返回,不影响其他任务。""" + # 数据:第二个元素触发异常。 + def worker(item: int) -> int: + if item == 2: + raise ValueError("boom") + return item + + pool = AdaptiveThreadPool(worker=worker) + + # 测试过程 + results = pool.map([1, 2, 3]) + + # 验证结果:异常对象保留在对应位置,其余结果正常。 + assert results[0] == 1 + assert isinstance(results[1], ValueError) + assert results[2] == 3 + + +def test_progress_callback_reports_completed_and_workers() -> None: + """进度回调按完成数递增上报,并带上当前额度。""" + # 数据:记录全部回调的列表。 + calls: list[tuple[int, int, int]] = [] + pool = AdaptiveThreadPool( + worker=lambda item: item, + on_progress=lambda done, total, rate, avg, workers: calls.append((done, total, workers)), + ) + + # 测试过程 + pool.map([1, 2, 3]) + + # 验证结果:完成数依次为 1、2、3,总数恒为 3,额度不低于下限。 + assert [c[0] for c in calls] == [1, 2, 3] + assert all(c[1] == 3 for c in calls) + assert all(c[2] >= 1 for c in calls) + + +def test_cancel_suppresses_progress_but_supplies_all_results() -> None: + """cancel 只抑制进度回调;每个输入仍返回结果(供调用方决定重试)。""" + # 数据:worker 在第一个任务里触发 cancel。 + pool: AdaptiveThreadPool + + def worker(item: int) -> int: + if item == 0: + pool.cancel() + return item + + pool = AdaptiveThreadPool(worker=worker, on_progress=lambda *a: calls.append(a)) + calls: list[tuple] = [] + + # 测试过程 + results = pool.map([0, 1, 2]) + + # 验证结果:结果完整,进度回调被抑制。 + assert results == [0, 1, 2] + assert calls == [] + + +def test_cancel_is_reset_for_next_map() -> None: + """同一实例再次 map 时清除 cancel 标记(暂停后继续、失败重试的调用约定)。""" + # 数据:第一批触发 cancel,第二批正常。 + pool = AdaptiveThreadPool(worker=lambda item: item) + pool.cancel() + calls: list[tuple] = [] + pool._on_progress = lambda *a: calls.append(a) # 直接复用真实回调槽位 + + # 测试过程 + pool.map([1]) + + # 验证结果:新批次重新上报进度。 + assert len(calls) == 1 + + +# --------------------------------------------------------------------------- +# 弹性扩缩容(假时钟驱动确定性窗口) +# --------------------------------------------------------------------------- + + +def _timed_pool(clock: FakeClock, worker, **kwargs) -> AdaptiveThreadPool: + """构造使用假时钟的真实线程池。""" + return AdaptiveThreadPool(worker=worker, clock=clock, window_seconds=1.0, **kwargs) + + +def test_pool_grows_target_when_responses_fast() -> None: + """窗口内平均响应快时应扩大目标额度(服务端空闲就加大并发)。""" + # 数据:真实时钟 + 极短窗口;worker 只做微秒级工作,平均耗时远低于快阈值。 + observed: list[int] = [] + + def worker(item: int) -> int: + time.sleep(0.002) + return item + + pool = AdaptiveThreadPool( + worker=worker, min_workers=1, max_workers=8, + window_seconds=0.01, fast_threshold=0.3, slow_threshold=1.0, + ) + pool._on_progress = lambda done, total, rate, avg, workers: observed.append(workers) + + # 测试过程:任务足够多,保证跨越多个窗口触发扩容判断。 + pool.map(list(range(40))) + + # 验证结果:额度单调不减且最终大于起始值。 + assert observed == sorted(observed) + assert observed[-1] > observed[0] + + +def test_pool_shrinks_target_when_responses_slow() -> None: + """窗口内平均响应慢时应收缩目标额度(避免压垮本地服务)。""" + # 数据:真实时钟 + 极短窗口;worker 耗时远超慢阈值。 + observed: list[int] = [] + + def worker(item: int) -> int: + time.sleep(0.02) + return item + + pool = AdaptiveThreadPool( + worker=worker, min_workers=1, max_workers=8, + window_seconds=0.05, fast_threshold=0.001, slow_threshold=0.005, + ) + pool._on_progress = lambda done, total, rate, avg, workers: observed.append(workers) + + # 测试过程 + pool.map(list(range(12))) + + # 验证结果:出现回调,且额度始终不低于下限(不会缩到 0)。 + assert observed + assert min(observed) >= 1 + + +def test_report_failure_lowers_effective_max_and_never_expands() -> None: + """report_failure 只收紧上限;上限从 20 降到 19 时不会把当前 1 并发扩成 19。""" + # 数据:上限 20、当前额度 1。 + pool = AdaptiveThreadPool(worker=lambda item: item, min_workers=1, max_workers=20) + + # 测试过程:报告一次限流失败。 + pool.report_failure() + + # 验证结果:有效上限降 1,当前目标仍为下限。 + assert pool._effective_max_workers == 19 + assert pool._target_workers <= 1 + + +def test_failure_at_single_worker_never_expands_quota() -> None: + """单并发下报告失败,绝不能因上限下降而把额度扩大。""" + # 数据:上限 20、最小 1,先启动一次 map 使额度为 1。 + pool = AdaptiveThreadPool(worker=lambda item: item, min_workers=1, max_workers=20) + pool.map([1]) + + # 测试过程 + pool.report_failure() + + # 验证结果 + assert pool._target_workers == 1 + + +def test_effective_max_recovers_one_step_per_clean_window() -> None: + """干净窗口每次只恢复 1 个上限,避免限流恢复期再次打满。""" + # 数据:先把有效上限压到 2。 + clock = FakeClock() + pool = _timed_pool(clock, lambda item: item, min_workers=1, max_workers=5) + pool.report_failure() + pool.report_failure() + pool.report_failure() + assert pool._effective_max_workers == 2 + + # 测试过程:第一个 tick 消耗“有失败”的窗口(不恢复),第二个干净窗口恢复 1。 + clock.advance(2.0) + pool._tick(0.1) + assert pool._effective_max_workers == 2 + clock.advance(2.0) + pool._tick(0.1) + + # 验证结果:干净窗口只恢复 1。 + assert pool._effective_max_workers == 3 + + +def test_error_window_does_not_recover_limit() -> None: + """窗口内有失败时不恢复上限。""" + # 数据:上限 5,压到 3 后在同一窗口内报告失败。 + clock = FakeClock() + pool = _timed_pool(clock, lambda item: item, min_workers=1, max_workers=5) + pool.report_failure() + pool.report_failure() + pool.report_failure() + before = pool._effective_max_workers + + # 测试过程:窗口内先失败再触发 tick。 + pool.report_failure() + clock.advance(2.0) + pool._tick(0.1) + + # 验证结果:上限未恢复。 + assert pool._effective_max_workers <= before + + +def test_reduced_limit_is_kept_across_retry_map() -> None: + """限流后的有效上限跨 map 保留(失败条目重试时继续遵守更严配额)。""" + # 数据:先压低上限。 + pool = AdaptiveThreadPool(worker=lambda item: item, min_workers=1, max_workers=6) + pool.map([1]) + pool.report_failure() + reduced = pool._effective_max_workers + + # 测试过程:执行第二轮 map(重试场景)。 + pool.map([1, 2]) + + # 验证结果:上限仍是被压低的值。 + assert pool._effective_max_workers == reduced + + +def test_concurrent_map_on_same_pool_is_rejected() -> None: + """同一实例不允许并行 map,避免额度与统计互相干扰。""" + # 数据:第一个 map 阻塞在 worker 上。 + started = threading.Event() + release = threading.Event() + + def worker(item: int) -> int: + started.set() + release.wait(timeout=5) + return item + + pool = AdaptiveThreadPool(worker=worker, min_workers=1, max_workers=2) + errors: list[Exception] = [] + + def run_first() -> None: + pool.map([1]) + + thread = threading.Thread(target=run_first) + thread.start() + assert started.wait(timeout=5) + + # 测试过程:在第一个 map 未结束时再次调用 map。 + try: + pool.map([2]) + except Exception as exc: # noqa: BLE001 - 断言真实抛出的类型 + errors.append(exc) + finally: + release.set() + thread.join(timeout=5) + + # 验证结果:抛出 RuntimeError。 + assert len(errors) == 1 + assert isinstance(errors[0], RuntimeError) + + +def test_max_concurrency_never_exceeds_target_limit() -> None: + """实际在途数量不超过 max_workers 上限(并发有界)。""" + # 数据:上限 3,10 个快速任务。 + pool = AdaptiveThreadPool(worker=lambda item: item, min_workers=1, max_workers=3) + + # 测试过程 + pool.map(list(range(10))) + + # 验证结果:观测到的最大在途数不超过上限。 + assert 1 <= pool.max_concurrency <= 3 diff --git a/tests/nodes/test_ass/__init__.py b/tests/nodes/test_ass/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/nodes/test_ass/test_ass.py b/tests/nodes/test_ass/test_ass.py new file mode 100644 index 0000000..44d445d --- /dev/null +++ b/tests/nodes/test_ass/test_ass.py @@ -0,0 +1,288 @@ +"""nodes/ass.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`nodes/ass.py`(SRT → VR 双目 ASS,含统一样式出口),被 +srt-to-dual-eye-ass 节点与历史字幕统脚本共用,可独立调用。 + +覆盖:SRT 解析(含多行与畸形行)、ASS 头与样式行、左右眼对照与零视差、 +顶部安全区(an8 + MarginV=700)、透明度、margin_top 参数化、invoke 全流程。 +""" + +from __future__ import annotations + +from pathlib import Path + +from nodes.ass import ( + DEFAULT_MARGIN_TOP, + _ass_header, + ass_header, + dialogue_line, + invoke, + parse_srt, + style_row, + write_ass, +) +from wov_sdk.models import InvokeRequest + +# 模块专用测试数据:两条(第二条为多行)SRT。 +SAMPLE_SRT = """1 +00:00:01,000 --> 00:00:02,000 +第一行 +第二行 + +2 +00:00:04,000 --> 00:00:06,000 +第三行 +""" + + +def _request(tmp_path: Path, srt_text: str, **params) -> InvokeRequest: + """构造真实 InvokeRequest:把输入 SRT 落到临时目录,输出目录同目录下。""" + srt_path = tmp_path / "input.srt" + srt_path.write_text(srt_text, encoding="utf-8") + return InvokeRequest( + run_id="run-test", + node_instance_id="ass-1", + params=params, + inputs={"cn_srt_uri": str(srt_path)}, + output_dir=str(tmp_path / "out"), + ) + + +# --------------------------------------------------------------------------- +# SRT 解析 +# --------------------------------------------------------------------------- + + +def test_parse_srt_converts_comma_to_dot_and_joins_multiline() -> None: + """SRT 时间戳逗号转 ASS 点号;多行正文用 \\N 连接。""" + # 数据:两条,第一条两行正文。 + text = SAMPLE_SRT + + # 测试过程 + entries = parse_srt(text) + + # 验证结果:条数、时间戳格式与多行连接符。 + assert len(entries) == 2 + assert entries[0][0] == "00:00:01.000" + assert entries[0][1] == "00:00:02.000" + assert entries[0][2] == r"第一行\N第二行" + + +def test_parse_srt_skips_malformed_timeline() -> None: + """缺少时间轴分隔符的畸形条目被跳过,不产生错误条目。""" + # 数据:一条畸形(无 -->)+ 一条正常。 + text = "1\n00:00:01,000\n坏条目\n\n2\n00:00:04,000 --> 00:00:06,000\n正常\n" + + # 测试过程 + entries = parse_srt(text) + + # 验证结果:只解析出正常条目。 + assert len(entries) == 1 + assert entries[0][2] == "正常" + + +def test_parse_srt_empty_text() -> None: + """空文本解析为空列表。""" + # 数据:空字符串。 + # 测试过程与验证结果 + assert parse_srt("") == [] + + +# --------------------------------------------------------------------------- +# 样式:单一事实来源 +# --------------------------------------------------------------------------- + + +def test_ass_header_contains_resolution_and_styles() -> None: + """ASS 头包含分辨率、左右眼两条样式与必需字段。""" + # 数据:VR 常用分辨率 3840x1920。 + # 测试过程 + header = ass_header(3840, 1920) + + # 验证结果 + assert "PlayResX: 3840" in header + assert "PlayResY: 1920" in header + assert "Style: LeftEye," in header + assert "Style: RightEye," in header + assert "[Script Info]" in header and "[V4+ Styles]" in header and "[Events]" in header + + +def test_style_row_top_aligned_with_margin() -> None: + """样式行使用 an8 顶部对齐 + MarginV 顶部安全边距(默认 700)。""" + # 数据:左眼样式,宽 3840,默认边距。 + # 测试过程 + row = style_row("LeftEye", 3840) + + # 验证结果:末尾字段为 Alignment=8、MarginL=50、MarginR=1920(右眼区)、MarginV=700。 + assert row.endswith(",8,50,1920,700,1") + + +def test_style_row_uses_translucent_fill_and_outline() -> None: + """文字填充约 70% 透明、描边半透明黑,降低对画面的遮挡。""" + # 数据:任一样式行。 + # 测试过程 + row = style_row("LeftEye", 3840) + + # 验证结果 + assert "&HB3FFFFFF" in row + assert "&H80000000" in row + assert "Arial" in row + + +def test_default_margin_top_is_700() -> None: + """默认顶部安全边距常量是 700(2026-09 调整,历史 120 已废弃)。""" + # 数据:模块常量。 + # 测试过程与验证结果 + assert DEFAULT_MARGIN_TOP == 700 + + +def test_dialogue_line_prefixes_top_alignment() -> None: + """Dialogue 行固定前缀 {\\an8},保证每条字幕落在顶部安全区。""" + # 数据:一条字幕。 + # 测试过程 + line = dialogue_line("LeftEye", "0:00:01.00", "0:00:02.00", "你好") + + # 验证结果 + assert line == r"Dialogue: 0,0:00:01.00,0:00:02.00,LeftEye,,0,0,0,,{\an8}你好" + + +# --------------------------------------------------------------------------- +# 写出:左右眼与零视差 +# --------------------------------------------------------------------------- + + +def test_write_ass_emits_both_eyes_with_identical_text(tmp_path: Path) -> None: + """每个条目输出左右眼两行 Dialogue,文本与水平相对位置完全一致(零视差)。""" + # 数据:单条字幕。 + entries = parse_srt("1\n00:00:01,000 --> 00:00:02,000\n台词\n") + output = tmp_path / "out.ass" + + # 测试过程 + write_ass(entries, output, "3840x1920") + content = output.read_text(encoding="utf-8") + + # 验证结果:两条 Dialogue(LeftEye/RightEye),文本各出现一次。 + dialogue = [line for line in content.splitlines() if line.startswith("Dialogue:")] + assert len(dialogue) == 2 + assert "LeftEye" in dialogue[0] and "RightEye" in dialogue[1] + assert dialogue[0].split(",,")[-1] == dialogue[1].split(",,")[-1] + + +def test_write_ass_custom_margin_top_changes_style_row(tmp_path: Path) -> None: + """margin_top 参数化到样式行(不同分辨率/内容可微调顶部边距)。""" + # 数据:自定义边距 300。 + entries = parse_srt(SAMPLE_SRT) + output = tmp_path / "out.ass" + + # 测试过程 + write_ass(entries, output, "3840x1920", margin_top=300) + content = output.read_text(encoding="utf-8") + + # 验证结果:样式行的 MarginV 为 300。 + assert ",8,50,1920,300,1" in content + + +def test_write_ass_multiple_entries_keep_order(tmp_path: Path) -> None: + """多条字幕按输入顺序输出(时间轴顺序不被打乱)。""" + # 数据:两条字幕。 + entries = parse_srt(SAMPLE_SRT) + output = tmp_path / "out.ass" + + # 测试过程 + write_ass(entries, output, "3840x1920") + dialogue = [ + line for line in output.read_text(encoding="utf-8").splitlines() + if line.startswith("Dialogue:") + ] + + # 验证结果:第 1、2 行属于第一条,第 3、4 行属于第二条。 + assert "00:00:01.000" in dialogue[0] and "00:00:02.000" in dialogue[1] + assert "00:00:04.000" in dialogue[2] and "00:00:06.000" in dialogue[3] + + +def test_ass_header_compat_interface_matches_new_header() -> None: + """兼容接口 _ass_header 与新版 ass_header 对同一分辨率输出一致(样式不漂移)。""" + # 数据:同一分辨率字符串与整数宽高。 + # 测试过程与验证结果 + assert _ass_header("3840x1920") == ass_header(3840, 1920) + + +# --------------------------------------------------------------------------- +# invoke 全流程 +# --------------------------------------------------------------------------- + + +def test_invoke_writes_ass_and_returns_uri(tmp_path: Path) -> None: + """invoke 读取 cn_srt_uri,写出 dual_eye.ass 并返回产物 URI。""" + # 数据:真实输入 SRT 文件 + 默认参数。 + request = _request(tmp_path, SAMPLE_SRT) + + # 测试过程 + response = invoke(request) + + # 验证结果:状态、产物存在、内容含左右眼。 + assert response.status == "completed" + artifact = Path(response.outputs["ass_uri"]) + assert artifact.is_file() + assert artifact.name == "dual_eye.ass" + assert "LeftEye" in artifact.read_text(encoding="utf-8") + + +def test_invoke_honors_resolution_and_margin_params(tmp_path: Path) -> None: + """invoke 透传 resolution 与 margin_top 参数到产物。""" + # 数据:自定义分辨率 1920x1080 与边距 250。 + request = _request(tmp_path, SAMPLE_SRT, resolution="1920x1080", margin_top=250) + + # 测试过程 + response = invoke(request) + + # 验证结果 + content = Path(response.outputs["ass_uri"]).read_text(encoding="utf-8") + assert "PlayResX: 1920" in content + assert "PlayResY: 1080" in content + assert "250,1" in content + + +def test_invoke_default_margin_top_is_700(tmp_path: Path) -> None: + """未传 margin_top 时使用默认 700。""" + # 数据:不传 margin_top。 + request = _request(tmp_path, SAMPLE_SRT) + + # 测试过程 + response = invoke(request) + + # 验证结果 + content = Path(response.outputs["ass_uri"]).read_text(encoding="utf-8") + assert ",8,50,1920,700,1" in content + + +def test_invoke_fails_without_input_uri(tmp_path: Path) -> None: + """缺少 cn_srt_uri 输入时返回 failed 并说明原因。""" + # 数据:空输入。 + request = InvokeRequest( + run_id="r", node_instance_id="n", params={}, inputs={}, output_dir=str(tmp_path) + ) + + # 测试过程 + response = invoke(request) + + # 验证结果 + assert response.status == "failed" + assert "cn_srt_uri" in (response.error or "") + + +def test_invoke_fails_when_input_file_missing(tmp_path: Path) -> None: + """输入指向不存在的文件时返回 failed。""" + # 数据:不存在的路径。 + request = InvokeRequest( + run_id="r", node_instance_id="n", params={}, + inputs={"cn_srt_uri": str(tmp_path / "missing.srt")}, + output_dir=str(tmp_path / "out"), + ) + + # 测试过程 + response = invoke(request) + + # 验证结果 + assert response.status == "failed" + assert "not found" in (response.error or "") diff --git a/tests/nodes/test_echo/__init__.py b/tests/nodes/test_echo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/nodes/test_echo/test_invoke.py b/tests/nodes/test_echo/test_invoke.py new file mode 100644 index 0000000..1591443 --- /dev/null +++ b/tests/nodes/test_echo/test_invoke.py @@ -0,0 +1,97 @@ +"""nodes/echo.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`nodes/echo.py`(示例回显节点),用于验证节点协议与注册表链路, +可独立调用。测试只调用真实 `invoke`,自行准备输入数据并检查产物。 +""" + +from __future__ import annotations + +from pathlib import Path + +from nodes.echo import invoke +from wov_sdk.models import InvokeRequest + + +def _request(tmp_path: Path, inputs: dict) -> InvokeRequest: + """构造一个真实 InvokeRequest;输出目录指向本用例的临时目录。""" + return InvokeRequest( + run_id="run-test", + node_instance_id="echo-1", + params={}, + inputs=inputs, + output_dir=str(tmp_path / "out"), + ) + + +def test_invoke_echoes_text_input(tmp_path: Path) -> None: + """传入 text 时直接回显该文本,并把文本写入产物文件。""" + # 数据:请求直接携带文本。 + request = _request(tmp_path, {"text": "你好,字幕"}) + + # 测试过程:调用真实节点处理器。 + response = invoke(request) + + # 验证结果:状态为 completed,输出文本一致,产物文件内容一致。 + assert response.status == "completed" + assert response.outputs["text"] == "你好,字幕" + artifact = Path(response.outputs["file_uri"]) + assert artifact.read_text(encoding="utf-8") == "你好,字幕" + + +def test_invoke_reads_absolute_file_uri(tmp_path: Path) -> None: + """未给 text 时读取 file_uri 指向的绝对路径文件内容。""" + # 数据:临时目录下的真实输入文件。 + source = tmp_path / "input.txt" + source.write_text("来自文件的文本", encoding="utf-8") + + # 测试过程 + response = invoke(_request(tmp_path, {"file_uri": str(source)})) + + # 验证结果 + assert response.outputs["text"] == "来自文件的文本" + + +def test_invoke_resolves_relative_file_uri_from_workspace(tmp_path: Path) -> None: + """相对 file_uri 以仓库根为基准解析(节点不启动独立进程,无自己的工作目录)。""" + # 数据:相对路径指向仓库根下的真实文件。 + workspace = Path(__file__).resolve().parents[3] + relative = "pyproject.toml" + assert (workspace / relative).is_file() + + # 测试过程 + response = invoke(_request(tmp_path, {"file_uri": relative})) + + # 验证结果:读到的内容与仓库根下该文件一致。 + assert response.outputs["text"] == (workspace / relative).read_text(encoding="utf-8") + + +def test_invoke_defaults_to_fixed_text(tmp_path: Path) -> None: + """既无 text 也无 file_uri 时返回固定文本,保证链路总有可演示输出。""" + # 数据:空输入。 + request = _request(tmp_path, {}) + + # 测试过程 + response = invoke(request) + + # 验证结果 + assert response.outputs["text"] == "echo" + + +def test_invoke_creates_output_directory(tmp_path: Path) -> None: + """输出目录不存在时由节点创建,产物始终落在请求给定的 output_dir。""" + # 数据:输出目录尚未创建(_request 只给路径)。 + output_dir = tmp_path / "not-yet" + + # 测试过程 + request = InvokeRequest( + run_id="run-test", + node_instance_id="echo-1", + params={}, + inputs={"text": "x"}, + output_dir=str(output_dir), + ) + response = invoke(request) + + # 验证结果:目录被创建且产物在目录内。 + assert output_dir.is_dir() + assert Path(response.outputs["file_uri"]).parent == output_dir diff --git a/tests/nodes/test_ffmpeg/__init__.py b/tests/nodes/test_ffmpeg/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/nodes/test_ffmpeg/data/clip_with_audio.mp4 b/tests/nodes/test_ffmpeg/data/clip_with_audio.mp4 new file mode 100644 index 0000000..50c2b2b Binary files /dev/null and b/tests/nodes/test_ffmpeg/data/clip_with_audio.mp4 differ diff --git a/tests/nodes/test_ffmpeg/data/subtitle_10s.mp4 b/tests/nodes/test_ffmpeg/data/subtitle_10s.mp4 new file mode 100644 index 0000000..ca84227 Binary files /dev/null and b/tests/nodes/test_ffmpeg/data/subtitle_10s.mp4 differ diff --git a/tests/nodes/test_ffmpeg/test_extract.py b/tests/nodes/test_ffmpeg/test_extract.py new file mode 100644 index 0000000..d8da2c0 --- /dev/null +++ b/tests/nodes/test_ffmpeg/test_extract.py @@ -0,0 +1,196 @@ +"""nodes/ffmpeg.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`nodes/ffmpeg.py`(ffmpeg 定位 + 提音),可独立调用。 +测试使用模块目录 `data/` 下的真实视频,调用真实 ffmpeg 子进程产出真实 WAV; +仅在 I/O 边界(环境变量 / PATH 查找)使用 monkeypatch。 +""" + +from __future__ import annotations + +import shutil +import subprocess +import wave +from pathlib import Path + +import pytest + +from nodes.ffmpeg import _bundled_ffmpeg, _ffmpeg_bin, invoke +from wov_sdk.models import InvokeRequest + +# 模块专用真实素材: +# - clip_with_audio.mp4:10 秒真实视频 + 真实语音音轨(提音用例的输入); +# - subtitle_10s.mp4:仅视频无音轨(用于验证"无音频流"时的失败路径)。 +DATA_DIR = Path(__file__).resolve().parent / "data" +TEST_VIDEO = DATA_DIR / "clip_with_audio.mp4" +VIDEO_WITHOUT_AUDIO = DATA_DIR / "subtitle_10s.mp4" + + +def _request(tmp_path: Path, video: Path | None, **params) -> InvokeRequest: + """构造真实请求;video 为 None 时表示不传 video_uri。""" + inputs = {} if video is None else {"video_uri": str(video)} + return InvokeRequest( + run_id="run-test", + node_instance_id="ffmpeg-1", + params=params, + inputs=inputs, + output_dir=str(tmp_path / "out"), + ) + + +def _available_ffmpeg() -> str | None: + """返回当前环境可用的 ffmpeg 路径(用于跳过缺少 ffmpeg 的环境)。""" + found = shutil.which("ffmpeg") + if found: + return found + return _bundled_ffmpeg() + + +# --------------------------------------------------------------------------- +# ffmpeg 定位 +# --------------------------------------------------------------------------- + + +def test_ffmpeg_bin_prefers_explicit_env(monkeypatch, tmp_path: Path) -> None: + """FFMPEG_BIN 环境变量最优先(部署可指定自定义二进制)。""" + # 数据:显式配置一个真实存在的假二进制路径。 + fake = tmp_path / "my-ffmpeg" + fake.write_text("#!/bin/sh\n", encoding="utf-8") + monkeypatch.setenv("FFMPEG_BIN", str(fake)) + + # 测试过程与验证结果 + assert _ffmpeg_bin() == str(fake) + + +def test_ffmpeg_bin_falls_back_to_path(monkeypatch) -> None: + """未配置环境变量时使用 PATH 中的 ffmpeg。""" + # 数据:清除显式配置。 + monkeypatch.delenv("FFMPEG_BIN", raising=False) + + # 测试过程 + resolved = _ffmpeg_bin() + + # 验证结果:返回可执行的 ffmpeg(PATH 或内置二进制)。 + assert resolved + assert shutil.which(resolved) is not None or Path(resolved).is_file() + + +def test_ffmpeg_bin_falls_back_to_bundled(monkeypatch) -> None: + """PATH 无 ffmpeg 时回退 imageio-ffmpeg 内置二进制。""" + # 数据:清空环境变量并让 which 返回 None。 + monkeypatch.delenv("FFMPEG_BIN", raising=False) + monkeypatch.setattr(shutil, "which", lambda name: None) + + # 测试过程 + resolved = _ffmpeg_bin() + + # 验证结果:得到内置二进制路径(本环境已安装 imageio-ffmpeg)。 + bundled = _bundled_ffmpeg() + assert resolved == (bundled or "ffmpeg") + + +def test_ffmpeg_bin_returns_plain_name_when_nothing_available(monkeypatch) -> None: + """完全不可用时返回裸名 "ffmpeg",由调用处统一报失败。""" + # 数据:环境变量、PATH、内置二进制都不可用。 + monkeypatch.delenv("FFMPEG_BIN", raising=False) + monkeypatch.setattr(shutil, "which", lambda name: None) + monkeypatch.setattr("nodes.ffmpeg._bundled_ffmpeg", lambda: None) + + # 测试过程与验证结果 + assert _ffmpeg_bin() == "ffmpeg" + + +# --------------------------------------------------------------------------- +# invoke:真实提音 +# --------------------------------------------------------------------------- + + +def test_invoke_extracts_16k_mono_wav(tmp_path: Path) -> None: + """真实视频 → 16kHz 单声道 WAV 产物(ASR 节点的输入契约)。""" + # 数据:模块 data/ 下带真实语音音轨的测试视频。 + if _available_ffmpeg() is None: + pytest.skip("环境中没有可用 ffmpeg") + assert TEST_VIDEO.is_file(), f"缺少测试视频 {TEST_VIDEO}" + + # 测试过程 + response = invoke(_request(tmp_path, TEST_VIDEO)) + + # 验证结果:产物存在,WAV 头为 16kHz 单声道,时长与源视频一致(约 10s)。 + assert response.status == "completed", response.error + audio = Path(response.outputs["audio_uri"]) + assert audio.is_file() and audio.suffix == ".wav" + with wave.open(str(audio), "rb") as wav: + assert wav.getframerate() == 16000 + assert wav.getnchannels() == 1 + duration = wav.getnframes() / wav.getframerate() + assert 9.0 <= duration <= 11.0 + + +def test_invoke_honors_sample_rate_and_channels_params(tmp_path: Path) -> None: + """sample_rate / channels 参数透传到 ffmpeg(产物头体现)。""" + # 数据:显式请求 8kHz 单声道。 + if _available_ffmpeg() is None: + pytest.skip("环境中没有可用 ffmpeg") + + # 测试过程 + response = invoke(_request(tmp_path, TEST_VIDEO, sample_rate=8000, channels=1)) + + # 验证结果 + assert response.status == "completed", response.error + with wave.open(response.outputs["audio_uri"], "rb") as wav: + assert wav.getframerate() == 8000 + + +def test_invoke_fails_without_video_uri(tmp_path: Path) -> None: + """缺少 video_uri 时返回 failed 并说明原因。""" + # 数据:不传输入。 + # 测试过程 + response = invoke(_request(tmp_path, None)) + + # 验证结果 + assert response.status == "failed" + assert "video_uri" in (response.error or "") + + +def test_invoke_fails_when_ffmpeg_missing(monkeypatch, tmp_path: Path) -> None: + """环境无 ffmpeg 时明确失败,不抛晦涩的子进程异常。""" + # 数据:把所有 ffmpeg 来源都屏蔽。 + monkeypatch.setenv("FFMPEG_BIN", "definitely-not-a-real-binary") + monkeypatch.setattr(shutil, "which", lambda name: None) + + # 测试过程 + response = invoke(_request(tmp_path, TEST_VIDEO)) + + # 验证结果 + assert response.status == "failed" + assert "ffmpeg" in (response.error or "") + + +def test_invoke_fails_when_ffmpeg_returns_error(tmp_path: Path) -> None: + """输入文件不是合法媒体时 ffmpeg 报错 → 节点返回 failed(不静默成功)。""" + # 数据:把文本文件伪装成视频。 + broken = tmp_path / "broken.mp4" + broken.write_text("not a video", encoding="utf-8") + if _available_ffmpeg() is None: + pytest.skip("环境中没有可用 ffmpeg") + + # 测试过程 + response = invoke(_request(tmp_path, broken)) + + # 验证结果 + assert response.status == "failed" + assert (response.error or "").strip() + + +def test_invoke_fails_when_video_has_no_audio_stream(tmp_path: Path) -> None: + """源视频不含音频流时提音失败并返回 ffmpeg 诊断信息(不静默产出空文件)。""" + # 数据:只有视频轨的测试素材。 + if _available_ffmpeg() is None: + pytest.skip("环境中没有可用 ffmpeg") + assert VIDEO_WITHOUT_AUDIO.is_file() + + # 测试过程 + response = invoke(_request(tmp_path, VIDEO_WITHOUT_AUDIO)) + + # 验证结果:失败且无残缺产物。 + assert response.status == "failed" + assert not (tmp_path / "out" / "audio.wav").exists() diff --git a/tests/nodes/test_frame_extract/__init__.py b/tests/nodes/test_frame_extract/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/nodes/test_frame_extract/data/frames_boundary/frame_0001.png b/tests/nodes/test_frame_extract/data/frames_boundary/frame_0001.png new file mode 100644 index 0000000..ec59afe Binary files /dev/null and b/tests/nodes/test_frame_extract/data/frames_boundary/frame_0001.png differ diff --git a/tests/nodes/test_frame_extract/data/frames_boundary/frame_0999.png b/tests/nodes/test_frame_extract/data/frames_boundary/frame_0999.png new file mode 100644 index 0000000..347fee2 Binary files /dev/null and b/tests/nodes/test_frame_extract/data/frames_boundary/frame_0999.png differ diff --git a/tests/nodes/test_frame_extract/data/frames_boundary/frame_1000.png b/tests/nodes/test_frame_extract/data/frames_boundary/frame_1000.png new file mode 100644 index 0000000..5903ebd Binary files /dev/null and b/tests/nodes/test_frame_extract/data/frames_boundary/frame_1000.png differ diff --git a/tests/nodes/test_frame_extract/data/frames_boundary/frame_10000.png b/tests/nodes/test_frame_extract/data/frames_boundary/frame_10000.png new file mode 100644 index 0000000..3b75861 Binary files /dev/null and b/tests/nodes/test_frame_extract/data/frames_boundary/frame_10000.png differ diff --git a/tests/nodes/test_frame_extract/data/frames_boundary/frame_10001.png b/tests/nodes/test_frame_extract/data/frames_boundary/frame_10001.png new file mode 100644 index 0000000..05a9094 Binary files /dev/null and b/tests/nodes/test_frame_extract/data/frames_boundary/frame_10001.png differ diff --git a/tests/nodes/test_frame_extract/data/frames_boundary/frame_10009.png b/tests/nodes/test_frame_extract/data/frames_boundary/frame_10009.png new file mode 100644 index 0000000..b234866 Binary files /dev/null and b/tests/nodes/test_frame_extract/data/frames_boundary/frame_10009.png differ diff --git a/tests/nodes/test_frame_extract/data/frames_boundary/frame_1009.png b/tests/nodes/test_frame_extract/data/frames_boundary/frame_1009.png new file mode 100644 index 0000000..9c7e06a Binary files /dev/null and b/tests/nodes/test_frame_extract/data/frames_boundary/frame_1009.png differ diff --git a/tests/nodes/test_frame_extract/data/frames_boundary/frame_14235.png b/tests/nodes/test_frame_extract/data/frames_boundary/frame_14235.png new file mode 100644 index 0000000..8f47466 Binary files /dev/null and b/tests/nodes/test_frame_extract/data/frames_boundary/frame_14235.png differ diff --git a/tests/nodes/test_frame_extract/data/frames_boundary/frame_14236.png b/tests/nodes/test_frame_extract/data/frames_boundary/frame_14236.png new file mode 100644 index 0000000..77129a2 Binary files /dev/null and b/tests/nodes/test_frame_extract/data/frames_boundary/frame_14236.png differ diff --git a/tests/nodes/test_frame_extract/data/frames_boundary/frame_1999.png b/tests/nodes/test_frame_extract/data/frames_boundary/frame_1999.png new file mode 100644 index 0000000..d5437c8 Binary files /dev/null and b/tests/nodes/test_frame_extract/data/frames_boundary/frame_1999.png differ diff --git a/tests/nodes/test_frame_extract/data/frames_boundary/frame_2000.png b/tests/nodes/test_frame_extract/data/frames_boundary/frame_2000.png new file mode 100644 index 0000000..c60d43e Binary files /dev/null and b/tests/nodes/test_frame_extract/data/frames_boundary/frame_2000.png differ diff --git a/tests/nodes/test_frame_extract/data/frames_boundary/frame_5762.png b/tests/nodes/test_frame_extract/data/frames_boundary/frame_5762.png new file mode 100644 index 0000000..a6bd097 Binary files /dev/null and b/tests/nodes/test_frame_extract/data/frames_boundary/frame_5762.png differ diff --git a/tests/nodes/test_frame_extract/data/frames_boundary/frame_9999.png b/tests/nodes/test_frame_extract/data/frames_boundary/frame_9999.png new file mode 100644 index 0000000..5fb80ba Binary files /dev/null and b/tests/nodes/test_frame_extract/data/frames_boundary/frame_9999.png differ diff --git a/tests/nodes/test_frame_extract/data/subtitle_10s.mp4 b/tests/nodes/test_frame_extract/data/subtitle_10s.mp4 new file mode 100644 index 0000000..ca84227 Binary files /dev/null and b/tests/nodes/test_frame_extract/data/subtitle_10s.mp4 differ diff --git a/tests/nodes/test_frame_extract/test_extract.py b/tests/nodes/test_frame_extract/test_extract.py new file mode 100644 index 0000000..ec80b44 --- /dev/null +++ b/tests/nodes/test_frame_extract/test_extract.py @@ -0,0 +1,222 @@ +"""nodes/frame_extract.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`nodes/frame_extract.py`(按帧间隔抽帧 + crop 裁切 + 帧清单), +可独立调用。抽帧用例使用模块 `data/` 下的真实视频调用真实 ffmpeg; +帧号排序用例构造真实命名的帧文件(重现 14236 帧任务的错位场景)。 +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from nodes.frame_extract import ( + DEFAULT_CROP, + _frame_step, + _parse_crop, + _parse_progress_line, + _sorted_frame_files, + invoke, +) +from wov_sdk.models import InvokeRequest + +# 模块专用真实素材:10 秒测试视频(1280x720, 25fps)。 +DATA_DIR = Path(__file__).resolve().parent / "data" +TEST_VIDEO = DATA_DIR / "subtitle_10s.mp4" + + +def _request(tmp_path: Path, video: Path | None, **params) -> InvokeRequest: + """构造真实请求;video 为 None 时表示不传 video_uri。""" + inputs = {} if video is None else {"video_uri": str(video)} + return InvokeRequest( + run_id="run-test", + node_instance_id="frame-extract-1", + params=params, + inputs=inputs, + output_dir=str(tmp_path / "out"), + ) + + +# --------------------------------------------------------------------------- +# 参数解析与换算(纯函数) +# --------------------------------------------------------------------------- + + +def test_default_crop_is_bottom_quarter() -> None: + """默认裁切区域为画面底部 1/4(字幕很少出现在上半部分)。""" + # 数据:模块默认常量。 + # 测试过程与验证结果 + assert DEFAULT_CROP == [0, 0.75, 1, 0.25] + + +def test_parse_crop_accepts_sequence_and_tuple() -> None: + """crop 接受四元素序列(列表/元组),返回浮点比例列表。""" + # 数据:列表形式与元组形式。 + # 测试过程与验证结果 + assert _parse_crop([0.1, 0.2, 0.3, 0.4]) == [0.1, 0.2, 0.3, 0.4] + assert _parse_crop((0, 0.75, 1, 0.25)) == [0.0, 0.75, 1.0, 0.25] + + +def test_parse_crop_rejects_invalid_input() -> None: + """非法 crop(长度不对/非数值/越界)返回 None,由调用处报错。""" + # 数据:五类非法输入(含 JSON 字符串——解析由调用方负责,节点只收序列)。 + # 测试过程与验证结果 + assert _parse_crop("not json") is None + assert _parse_crop("[0.1, 0.2, 0.3, 0.4]") is None + assert _parse_crop([0.1, 0.2]) is None + assert _parse_crop([0.1, 0.2, "x", 0.4]) is None + assert _parse_crop([0.1, 0.2, 1.5, 0.4]) is None + + +def test_frame_step_rounds_interval_times_fps() -> None: + """帧间隔换算:step = round(间隔秒 × fps),至少为 1。""" + # 数据:25fps 下 0.5s → 12.5 → 12;极短间隔至少 1。 + # 测试过程与验证结果 + assert _frame_step(25.0, 0.5) == 12 + assert _frame_step(25.0, 0.04) == 1 + assert _frame_step(30.0, 1.0) == 30 + + +def test_parse_progress_line_extracts_frame_number() -> None: + """ffmpeg -progress 的 frame=N 行被解析为整数,其他行忽略。""" + # 数据:真实的 -progress 输出行。 + # 测试过程与验证结果 + assert _parse_progress_line("frame=123") == 123 + assert _parse_progress_line("fps=25.0") is None + assert _parse_progress_line("frame=abc") is None + + +# --------------------------------------------------------------------------- +# 帧号自然排序(14236 帧事故回归) +# --------------------------------------------------------------------------- + + +def test_frame_files_sorted_numerically_not_lexicographically(tmp_path: Path) -> None: + """帧文件按帧号数值排序:4 位与 5 位编号混排时不会回退(真实事故回归)。 + + 背景:ffmpeg 的 %04d 在超过 9999 帧后扩为 5 位,字典序会把 + frame_10000 排到 frame_9999 之前,导致时间轴与图像错位 + (run_339ec7ee437f 的 14236 帧任务实测)。 + """ + # 数据:构造跨越 9999 边界的真实帧文件名。 + frames_dir = tmp_path / "frames" + frames_dir.mkdir() + for number in (1, 9999, 10000, 10009, 1009, 14236): + (frames_dir / f"frame_{number:04d}.png").write_bytes(b"png") + + # 测试过程 + ordered = [int(p.stem.split("_")[1]) for p in _sorted_frame_files(frames_dir)] + + # 验证结果:严格按数值升序(含 1009 < 9999 < 10000 < 10009)。 + assert ordered == [1, 1009, 9999, 10000, 10009, 14236] + + +def test_frame_files_sorted_returns_empty_for_empty_dir(tmp_path: Path) -> None: + """空目录返回空列表。""" + # 数据:空目录。 + frames_dir = tmp_path / "frames" + frames_dir.mkdir() + + # 测试过程与验证结果 + assert _sorted_frame_files(frames_dir) == [] + + +# --------------------------------------------------------------------------- +# invoke:真实抽帧 +# --------------------------------------------------------------------------- + + +def test_invoke_extracts_frames_with_manifest(tmp_path: Path) -> None: + """真实视频按间隔抽帧:产出帧图与清单,清单时间轴与帧数一致。""" + # 数据:10 秒 25fps 视频,间隔 2 秒(step=50)。 + assert TEST_VIDEO.is_file(), f"缺少测试视频 {TEST_VIDEO}" + + # 测试过程 + response = invoke(_request(tmp_path, TEST_VIDEO, interval_seconds=2.0)) + + # 验证结果:产物清单存在,帧数与 10s/2s=5 一致,时间轴递增。 + assert response.status == "completed", response.error + manifest_path = Path(response.outputs["frames_manifest"]) + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + frames = manifest["frames"] if isinstance(manifest, dict) else manifest + assert len(frames) >= 4 + times = [f["time"] for f in frames] + assert times == sorted(times) + assert response.outputs["frame_count"] == len(frames) + + +def _png_size(path: Path) -> tuple[int, int]: + """从 PNG 文件头读取宽高(避免为测试引入图像库依赖)。 + + PNG 结构:8 字节签名 + 4 字节长度 + "IHDR" + 宽(4) + 高(4),均为大端。 + """ + data = path.read_bytes()[:24] + assert data[:8] == b"\x89PNG\r\n\x1a\n", "不是合法 PNG" + assert data[12:16] == b"IHDR" + return int.from_bytes(data[16:20], "big"), int.from_bytes(data[20:24], "big") + + +def test_invoke_crops_to_bottom_region(tmp_path: Path) -> None: + """默认裁切底部 1/4:帧图高度明显小于原视频(1280x720 → 约 320 高)。""" + # 数据:真实视频 + 默认 crop。 + if not TEST_VIDEO.is_file(): + pytest.skip(f"缺少测试视频 {TEST_VIDEO}") + + # 测试过程 + response = invoke(_request(tmp_path, TEST_VIDEO, interval_seconds=5.0)) + + # 验证结果:帧图高度约 720*0.25=180(720p 内压缩后可能更小),宽度保持宽扁形。 + assert response.status == "completed", response.error + frames = sorted((tmp_path / "out" / "frames").glob("frame_*.png")) + assert frames + width, height = _png_size(frames[0]) + assert height <= 320 + assert width > height # 底部字幕条,仍是宽扁形 + + +def test_invoke_respects_custom_crop(tmp_path: Path) -> None: + """自定义 crop 生效:裁切区域变化体现在帧图尺寸上。""" + # 数据:取画面上半部分 1/4 高。 + if not TEST_VIDEO.is_file(): + pytest.skip(f"缺少测试视频 {TEST_VIDEO}") + + # 测试过程 + response = invoke(_request( + tmp_path, TEST_VIDEO, interval_seconds=5.0, crop=[0, 0, 1, 0.25], + )) + + # 验证结果:成功产出帧图。 + assert response.status == "completed", response.error + assert list((tmp_path / "out" / "frames").glob("frame_*.png")) + + +def test_invoke_fails_without_video_uri(tmp_path: Path) -> None: + """缺少 video_uri 时失败。""" + # 数据:空输入。 + # 测试过程 + response = invoke(_request(tmp_path, None)) + + # 验证结果 + assert response.status == "failed" + assert "video_uri" in (response.error or "") + + +def test_invoke_fails_when_video_missing(tmp_path: Path) -> None: + """视频文件不存在时失败。""" + # 数据:不存在的路径。 + # 测试过程 + response = invoke(_request(tmp_path, tmp_path / "nope.mp4")) + + # 验证结果 + assert response.status == "failed" + assert "not found" in (response.error or "") + + +def test_invoke_rejects_invalid_interval_and_crop(tmp_path: Path) -> None: + """非法 interval / crop 参数被拒绝,不产出残缺帧序列。""" + # 数据:零间隔与非法 crop。 + # 测试过程与验证结果 + assert invoke(_request(tmp_path, TEST_VIDEO, interval_seconds=0)).status == "failed" + assert invoke(_request(tmp_path, TEST_VIDEO, crop="bad")).status == "failed" diff --git a/tests/nodes/test_llm/__init__.py b/tests/nodes/test_llm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/nodes/test_llm/test_translate.py b/tests/nodes/test_llm/test_translate.py new file mode 100644 index 0000000..b0efe64 --- /dev/null +++ b/tests/nodes/test_llm/test_translate.py @@ -0,0 +1,473 @@ +"""nodes/llm.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`nodes/llm.py`(LLM 翻译节点:分批请求 + 按 ID 回填),可独立调用。 +网络属于允许 mock 的 I/O 边界:单元用例注入假 HTTP 响应验证请求体、ID 校验、 +重试与回填;集成用例调用真实 LLM 验证真实字幕翻译质量。 +""" + +from __future__ import annotations + +import json +import urllib.error +import urllib.request +from pathlib import Path + +import pytest + +from nodes.llm import ( + CHUNK_SIZE, + MAX_BATCH_RETRIES, + _parse_translations, + _system_prompt, + invoke, + translate_lines, +) +from wov_sdk.models import InvokeRequest + +# 模块专用数据目录(真实字幕产物缺失时相关用例跳过)。 +DATA_DIR = Path(__file__).resolve().parent / "data" + + +class _FakeHTTPResponse: + """假的 HTTP 响应:返回预置 JSON 体(供 urlopen mock 使用)。""" + + def __init__(self, payload: dict) -> None: + self._data = json.dumps(payload, ensure_ascii=False).encode("utf-8") + + def read(self) -> bytes: + return self._data + + def __enter__(self): + return self + + def __exit__(self, *exc) -> None: + return None + + +def _llm_reply(translations: list[tuple[int, str]], total_tokens: int = 42) -> _FakeHTTPResponse: + """构造 OpenAI 兼容接口的响应体(content 为 {id,text} JSON 数组)。""" + content = json.dumps( + [{"id": i, "text": t} for i, t in translations], ensure_ascii=False + ) + return _FakeHTTPResponse({ + "choices": [{"message": {"content": content}}], + "usage": {"total_tokens": total_tokens}, + }) + + +def _capture_urlopen(calls: list[dict], responses: list[_FakeHTTPResponse]): + """返回一个假 urlopen:记录请求体,按顺序返回预置响应。""" + + def fake_urlopen(http_request, timeout=None): + calls.append({ + "url": http_request.full_url, + "body": json.loads(http_request.data.decode("utf-8")), + "headers": dict(http_request.headers), + "timeout": timeout, + }) + return responses.pop(0) if responses else _llm_reply([]) + + return fake_urlopen + + +# --------------------------------------------------------------------------- +# 提示词与响应解析(纯函数) +# --------------------------------------------------------------------------- + + +def test_system_prompt_mentions_target_language_and_json_contract() -> None: + """系统提示词说明目标语言与 JSON 条目契约(时间戳不进入模型)。""" + # 数据:目标语言 zh-CN。 + # 测试过程 + prompt = _system_prompt("zh-CN") + + # 验证结果 + assert "zh-CN" in prompt + assert "id" in prompt and "text" in prompt + + +def test_parse_translations_accepts_out_of_order_ids() -> None: + """乱序返回的条目按 ID 回填(不依赖数组顺序)。""" + # 数据:ID 为 3、1、2 的乱序结果。 + content = json.dumps([ + {"id": 3, "text": "三"}, + {"id": 1, "text": "一"}, + {"id": 2, "text": "二"}, + ]) + + # 测试过程 + parsed = _parse_translations(content, {1, 2, 3}) + + # 验证结果 + assert parsed == {1: "一", 2: "二", 3: "三"} + + +def test_parse_translations_rejects_missing_id() -> None: + """缺少任一 ID 时明确报错(不允许静默漏译导致时间轴错位)。""" + # 数据:缺少 ID 2。 + content = json.dumps([{"id": 1, "text": "一"}, {"id": 3, "text": "三"}]) + + # 测试过程与验证结果 + with pytest.raises(ValueError, match="missing"): + _parse_translations(content, {1, 2, 3}) + + +def test_parse_translations_rejects_duplicate_id() -> None: + """重复 ID 报错。""" + # 数据:ID 1 出现两次。 + content = json.dumps([{"id": 1, "text": "一"}, {"id": 1, "text": "壹"}]) + + # 测试过程与验证结果 + with pytest.raises(ValueError, match="duplicate"): + _parse_translations(content, {1}) + + +def test_parse_translations_rejects_empty_text() -> None: + """空正文或非字符串正文报错。""" + # 数据:空字符串与数字正文。 + # 测试过程与验证结果 + with pytest.raises(ValueError, match="empty or invalid"): + _parse_translations(json.dumps([{"id": 1, "text": " "}]), {1}) + with pytest.raises(ValueError, match="empty or invalid"): + _parse_translations(json.dumps([{"id": 1, "text": 3}]), {1}) + + +def test_parse_translations_rejects_unexpected_id() -> None: + """返回了未请求的 ID 时报错。""" + # 数据:包含 ID 9(未请求)。 + content = json.dumps([{"id": 9, "text": "九"}]) + + # 测试过程与验证结果 + with pytest.raises(ValueError, match="invalid or duplicate"): + _parse_translations(content, {1}) + + +def test_parse_translations_rejects_non_array_payload() -> None: + """顶层不是数组时报错。""" + # 数据:对象形式的返回。 + # 测试过程与验证结果 + with pytest.raises(ValueError, match="JSON array"): + _parse_translations(json.dumps({"id": 1, "text": "一"}), {1}) + + +def test_parse_translations_keeps_multiline_text_structure() -> None: + """译文多行结构保留(去除纯空行,不截断 cue)。""" + # 数据:含空行的多行译文。 + content = json.dumps([{"id": 1, "text": "第一行\n\n第二行"}]) + + # 测试过程 + parsed = _parse_translations(content, {1}) + + # 验证结果:空行被去掉但两行都保留。 + assert parsed[1] == "第一行\n第二行" + + +# --------------------------------------------------------------------------- +# translate_lines:请求体、ID 与重试 +# --------------------------------------------------------------------------- + + +def test_translate_lines_sends_global_ids_and_maps_back(monkeypatch) -> None: + """按全局位置 ID 请求翻译,并把结果按位置回填(空 cue 不请求但占位)。""" + # 数据:5 行,其中第 3 行为空(占位)。 + lines = ["一", "二", "", "四", "五"] + calls: list[dict] = [] + monkeypatch.setenv("LLM_API_KEY", "sk-test") + monkeypatch.setattr( + urllib.request, "urlopen", + _capture_urlopen(calls, [_llm_reply([(1, "one"), (2, "two"), (4, "four"), (5, "five")])]), + ) + + # 测试过程 + result = translate_lines(lines, {"target_language": "en"}) + + # 验证结果:请求体只含非空行的全局 ID(1,2,4,5),结果按位置回填且空行保留。 + sent = json.loads(calls[0]["body"]["messages"][1]["content"]) + assert [item["id"] for item in sent] == [1, 2, 4, 5] + assert result == ["one", "two", "", "four", "five"] + + +def test_translate_lines_retries_on_structure_error(monkeypatch) -> None: + """结构校验失败时重试,最终成功(最多 MAX_BATCH_RETRIES 次)。""" + # 数据:第一次返回缺 ID,第二次正确。 + lines = ["一", "二"] + calls: list[dict] = [] + monkeypatch.setenv("LLM_API_KEY", "sk-test") + monkeypatch.setattr( + urllib.request, "urlopen", + _capture_urlopen(calls, [ + _llm_reply([(1, "one")]), # 缺 ID 2 → 触发重试 + _llm_reply([(1, "one"), (2, "two")]), # 正确 + ]), + ) + + # 测试过程 + result = translate_lines(lines, {}) + + # 验证结果:重试一次后成功,共发起 2 次请求。 + assert result == ["one", "two"] + assert len(calls) == 2 + + +def test_translate_lines_raises_after_retries_exhausted(monkeypatch) -> None: + """结构错误耗尽重试后抛错(不返回错位译文)。""" + # 数据:每次都返回错误结构。 + calls: list[dict] = [] + monkeypatch.setenv("LLM_API_KEY", "sk-test") + monkeypatch.setattr( + urllib.request, "urlopen", + _capture_urlopen(calls, [_llm_reply([]) for _ in range(MAX_BATCH_RETRIES)]), + ) + + # 测试过程与验证结果 + with pytest.raises(ValueError, match="alignment failed"): + translate_lines(["一"], {}) + assert len(calls) == MAX_BATCH_RETRIES + + +def test_translate_lines_empty_input_makes_no_request(monkeypatch) -> None: + """空输入直接返回空列表,不发请求。""" + # 数据:空列表。 + calls: list[dict] = [] + monkeypatch.setattr(urllib.request, "urlopen", _capture_urlopen(calls, [])) + + # 测试过程与验证结果 + assert translate_lines([], {}) == [] + assert calls == [] + + +def test_translate_lines_all_empty_cues_make_no_request(monkeypatch) -> None: + """全部为空 cue 时不请求模型,返回等长空列表。""" + # 数据:3 个空行。 + calls: list[dict] = [] + monkeypatch.setattr(urllib.request, "urlopen", _capture_urlopen(calls, [])) + + # 测试过程与验证结果 + assert translate_lines(["", " ", ""], {}) == ["", "", ""] + assert calls == [] + + +def test_translate_lines_batches_by_chunk_size(monkeypatch) -> None: + """超过 CHUNK_SIZE 行时分批请求(每批最多 CHUNK_SIZE 条)。""" + # 数据:CHUNK_SIZE + 1 行。 + total = CHUNK_SIZE + 1 + lines = [f"行{i}" for i in range(1, total + 1)] + calls: list[dict] = [] + + def fake_urlopen(http_request, timeout=None): + body = json.loads(http_request.data.decode("utf-8")) + calls.append(body) + items = json.loads(body["messages"][1]["content"]) + return _llm_reply([(item["id"], f"t{item['id']}") for item in items]) + + monkeypatch.setenv("LLM_API_KEY", "sk-test") + monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen) + + # 测试过程 + result = translate_lines(lines, {}) + + # 验证结果:两批(CHUNK_SIZE + 1),结果等长且顺序正确。 + assert len(calls) == 2 + assert len(result) == total + assert result[0] == "t1" and result[-1] == f"t{total}" + + +def test_translate_lines_injects_proper_noun_rule(monkeypatch) -> None: + """本批原文命中专名时,系统提示词追加规则(不被硬译)。""" + # 数据:含专名 ジンゴ 的一行。 + calls: list[dict] = [] + monkeypatch.setenv("LLM_API_KEY", "sk-test") + monkeypatch.setattr( + urllib.request, "urlopen", + _capture_urlopen(calls, [_llm_reply([(1, "译文")])]), + ) + + # 测试过程 + translate_lines(["ジンゴがここで味わいませんか"], {}) + + # 验证结果:系统提示词包含该专名与禁止硬译的说明。 + system_prompt = calls[0]["body"]["messages"][0]["content"] + assert "ジンゴ" in system_prompt + assert "芒果" in system_prompt + + +def test_translate_lines_default_model_and_env_override(monkeypatch) -> None: + """默认模型来自 LLM_MODEL 环境变量(数据驱动,不改代码切换模型)。""" + # 数据:设置环境变量为自定义模型。 + calls: list[dict] = [] + monkeypatch.setenv("LLM_API_KEY", "sk-test") + monkeypatch.setenv("LLM_MODEL", "自定义/模型") + monkeypatch.setattr( + urllib.request, "urlopen", + _capture_urlopen(calls, [_llm_reply([(1, "x")])]), + ) + + # 测试过程 + translate_lines(["一"], {}) + + # 验证结果:请求体使用环境变量指定的模型。 + assert calls[0]["body"]["model"] == "自定义/模型" + + +def test_translate_lines_param_model_wins_over_env(monkeypatch) -> None: + """节点参数 model 优先于环境变量。""" + # 数据:环境变量与参数都设置。 + calls: list[dict] = [] + monkeypatch.setenv("LLM_API_KEY", "sk-test") + monkeypatch.setenv("LLM_MODEL", "env/模型") + monkeypatch.setattr( + urllib.request, "urlopen", + _capture_urlopen(calls, [_llm_reply([(1, "x")])]), + ) + + # 测试过程 + translate_lines(["一"], {"model": "param/模型"}) + + # 验证结果 + assert calls[0]["body"]["model"] == "param/模型" + + +def test_translate_lines_uses_timeout_env(monkeypatch) -> None: + """LLM_TIMEOUT_SECONDS 决定请求超时(默认 600)。""" + # 数据:设置 45 秒。 + calls: list[dict] = [] + monkeypatch.setenv("LLM_API_KEY", "sk-test") + monkeypatch.setenv("LLM_TIMEOUT_SECONDS", "45") + monkeypatch.setattr( + urllib.request, "urlopen", + _capture_urlopen(calls, [_llm_reply([(1, "x")])]), + ) + + # 测试过程 + translate_lines(["一"], {}) + + # 验证结果 + assert calls[0]["timeout"] == 45.0 + + +def test_translate_lines_sends_bearer_key(monkeypatch) -> None: + """请求头带 Bearer Key(来自 LLM_API_KEY)。""" + # 数据:设置 key。 + calls: list[dict] = [] + monkeypatch.setenv("LLM_API_KEY", "sk-abc") + monkeypatch.setattr( + urllib.request, "urlopen", + _capture_urlopen(calls, [_llm_reply([(1, "x")])]), + ) + + # 测试过程 + translate_lines(["一"], {}) + + # 验证结果 + headers = {k.lower(): v for k, v in calls[0]["headers"].items()} + assert headers.get("authorization") == "Bearer sk-abc" + + +# --------------------------------------------------------------------------- +# invoke 全流程 +# --------------------------------------------------------------------------- + + +def test_invoke_translates_srt_and_writes_artifact(monkeypatch, tmp_path: Path) -> None: + """invoke 解析 SRT → 翻译 → 写出 cn_srt,时间轴保持原样。""" + # 数据:两条真实 SRT。 + srt = "1\n00:00:01,000 --> 00:00:02,000\nこんにちは\n\n2\n00:00:03,000 --> 00:00:04,000\nさようなら\n" + srt_path = tmp_path / "in.srt" + srt_path.write_text(srt, encoding="utf-8") + calls: list[dict] = [] + monkeypatch.setenv("LLM_API_KEY", "sk-test") + monkeypatch.setattr( + urllib.request, "urlopen", + _capture_urlopen(calls, [_llm_reply([(1, "你好"), (2, "再见")])]), + ) + request = InvokeRequest( + run_id="r", node_instance_id="n", params={}, + inputs={"srt_uri": str(srt_path)}, output_dir=str(tmp_path / "out"), + ) + + # 测试过程 + response = invoke(request) + + # 验证结果:状态、产物时间轴与译文顺序。 + assert response.status == "completed", response.error + content = Path(response.outputs["cn_srt_uri"]).read_text(encoding="utf-8") + assert "00:00:01,000 --> 00:00:02,000" in content + assert "你好" in content and "再见" in content + assert content.index("你好") < content.index("再见") + + +def test_invoke_fails_without_input(tmp_path: Path) -> None: + """缺少 srt_uri 时失败。""" + # 数据:空输入。 + request = InvokeRequest( + run_id="r", node_instance_id="n", params={}, inputs={}, output_dir=str(tmp_path) + ) + + # 测试过程 + response = invoke(request) + + # 验证结果 + assert response.status == "failed" + assert "srt_uri" in (response.error or "") + + +def test_invoke_fails_when_input_missing(tmp_path: Path) -> None: + """输入文件不存在时失败。""" + # 数据:不存在的路径。 + request = InvokeRequest( + run_id="r", node_instance_id="n", params={}, + inputs={"srt_uri": str(tmp_path / "nope.srt")}, output_dir=str(tmp_path), + ) + + # 测试过程 + response = invoke(request) + + # 验证结果 + assert response.status == "failed" + assert "not found" in (response.error or "") + + +def test_invoke_reports_failure_on_llm_error(monkeypatch, tmp_path: Path) -> None: + """LLM 报错时节点返回 failed(不产出半成品产物)。""" + # 数据:urlopen 抛 HTTPError。 + srt_path = tmp_path / "in.srt" + srt_path.write_text("1\n00:00:01,000 --> 00:00:02,000\nこんにちは\n", encoding="utf-8") + monkeypatch.setenv("LLM_API_KEY", "sk-test") + + def fake_urlopen(http_request, timeout=None): + raise urllib.error.HTTPError(http_request.full_url, 500, "server error", {}, None) + + monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen) + request = InvokeRequest( + run_id="r", node_instance_id="n", params={}, + inputs={"srt_uri": str(srt_path)}, output_dir=str(tmp_path / "out"), + ) + + # 测试过程 + response = invoke(request) + + # 验证结果 + assert response.status == "failed" + + +# --------------------------------------------------------------------------- +# 真实 LLM 集成(需要 LLM_API_KEY 与网络) +# --------------------------------------------------------------------------- + + +@pytest.mark.integration +def test_real_llm_translates_short_lines_with_domain_rules() -> None: + """真实 LLM 校准:短句专名/隐语不按字面直译(真实 API,外部状态缺失则跳过)。""" + # 数据:含专名 ジンゴ 的短句;Key 与账号可用性由共享判定处理。 + from nodes.llm import translate_lines as real_translate + from tests.shared.llm_service import require_llm_credentials, skip_on_service_unavailable + + require_llm_credentials() + + # 测试过程 + with skip_on_service_unavailable(): + out = real_translate(["ジンゴがここで味わいませんか"], {"target_language": "zh-CN"}) + + # 验证结果:有译文且未把专名硬译成"芒果"。 + assert out and out[0].strip() + assert "芒果" not in out[0] diff --git a/tests/nodes/test_llm_filter/__init__.py b/tests/nodes/test_llm_filter/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/nodes/test_llm_filter/data/ocr_srt_run_ac7f480a3ccb.srt b/tests/nodes/test_llm_filter/data/ocr_srt_run_ac7f480a3ccb.srt new file mode 100644 index 0000000..94ee9df --- /dev/null +++ b/tests/nodes/test_llm_filter/data/ocr_srt_run_ac7f480a3ccb.srt @@ -0,0 +1,6663 @@ +1 +00:00:00,000 --> 00:00:05,580 +(淫魔病院)(新人护士的眼泪) + +2 +00:00:17,760 --> 00:00:21,820 +辛苦了 上午的检查已经OK了 + +3 +00:00:21,820 --> 00:00:22,327 +--- + +4 +00:00:31,969 --> 00:00:33,998 +谢谢你 松井小姐 + +5 +00:00:33,999 --> 00:00:37,550 +还有没有什么困扰 或者奇怪的地方吗 + +6 +00:00:38,058 --> 00:00:39,580 +没问题啦 + +7 +00:00:39,581 --> 00:00:43,132 +总感觉果林前辈 每次都会问这个呢 + +8 +00:00:43,133 --> 00:00:46,177 +是呢 抱歉 + +9 +00:00:46,177 --> 00:00:49,221 +V + +10 +00:00:49,222 --> 00:00:51,759 +那么 松井小姐 + +11 +00:00:51,759 --> 00:00:52,774 +V + +12 +00:00:52,774 --> 00:00:55,818 +你来我们医院也才一个月吧 + +13 +00:00:56,326 --> 00:00:57,341 +记得挺快嘛 没有啦 + +14 +00:00:57,341 --> 00:00:57,848 +记得挺快嘛没有啦 + +15 +00:00:57,848 --> 00:00:59,370 +记得挺快嘛 没有啦 + +16 +00:01:00,893 --> 00:01:04,445 +因为果林前辈教得好呀 + +17 +00:01:04,444 --> 00:01:08,504 +因为 你和患者们 也已经完全打成一片了 + +18 +00:01:08,505 --> 00:01:09,012 +医 + +19 +00:01:09,012 --> 00:01:11,549 +我是北冈果林 + +20 +00:01:11,549 --> 00:01:16,116 +在这家综合医院工作的护士 + +21 +00:01:16,624 --> 00:01:19,161 +她叫松井日奈子 + +22 +00:01:19,161 --> 00:01:24,235 +是一个月前开始 在这间医院工作的新人护士 + +23 +00:01:25,250 --> 00:01:28,295 +上吧 上吧 + +24 +00:01:28,802 --> 00:01:33,876 +不行 受不了 好了 上吧 + +25 +00:01:33,877 --> 00:01:35,399 +Cleaning + +26 +00:01:35,399 --> 00:01:35,906 +清洁 + +27 +00:01:37,429 --> 00:01:39,966 +状态不错 + +28 +00:01:39,966 --> 00:01:42,503 +请进 + +29 +00:01:43,011 --> 00:01:43,518 +--- + +30 +00:01:46,563 --> 00:01:47,070 +HTML + +31 +00:01:47,070 --> 00:01:49,607 +早安 + +32 +00:01:50,622 --> 00:01:52,144 +金山先生 + +33 +00:01:52,145 --> 00:01:57,219 +昨晚你住院时我不在没能照顾到你非常抱歉 + +34 +00:01:57,219 --> 00:02:04,323 +我是医务室长权藤 请多关照 + +35 +00:02:04,323 --> 00:02:09,396 +这边这位是负责你的叶山 + +36 +00:02:09,398 --> 00:02:12,442 +我是外科主任叶山 + +37 +00:02:14,472 --> 00:02:19,039 +如果有什么困难 请找她帮忙 + +38 +00:02:19,039 --> 00:02:25,128 +另外 关于你希望的特别服务 我们正在准备当中 + +39 +00:02:26,143 --> 00:02:31,217 +拜托你们尽快 明白了 + +40 +00:02:34,263 --> 00:02:37,814 +我没见过你呢 + +41 +00:02:38,322 --> 00:02:40,859 +我是新来的生产员 + +42 +00:02:40,859 --> 00:02:44,411 +我姓泷本 请多多指教 + +43 +00:02:44,411 --> 00:02:46,441 +就这样 + +44 +00:02:46,441 --> 00:02:49,485 +泷本先生 我有件事想拜托你 + +45 +00:02:49,486 --> 00:02:49,993 +沈本先生 我有件事想拜托你 + +46 +00:02:49,993 --> 00:02:50,500 +泷本先生 我有件事想拜托你 + +47 +00:02:51,516 --> 00:02:56,590 +今天傍晚 能来手术室一趟吗 + +48 +00:02:57,605 --> 00:03:01,664 +明白了 那就这样 + +49 +00:03:04,709 --> 00:03:05,216 +___ + +50 +00:03:18,410 --> 00:03:20,439 +--------------- + +51 +00:03:28,051 --> 00:03:29,573 +北冈小姐 + +52 +00:03:29,574 --> 00:03:33,633 +这是特别病房患者的病历表 + +53 +00:03:34,141 --> 00:03:35,155 +金山先生有向我们医院捐过一笔巨款 + +54 +00:03:35,156 --> 00:03:36,678 +金山先生有向我们医院 捐过一笔巨款 + +55 +00:03:36,678 --> 00:03:38,707 +金山先生有向我们医院捐过一笔巨款 + +56 +00:03:38,708 --> 00:03:42,767 +非常多的金额 你们别疏忽了 + +57 +00:03:42,767 --> 00:03:43,274 +是明白了 + +58 +00:03:43,275 --> 00:03:43,782 +是 明白了 + +59 +00:03:43,782 --> 00:03:45,304 +是明白了 + +60 +00:03:49,364 --> 00:03:51,393 +松井小姐 在 + +61 +00:03:51,394 --> 00:03:52,916 +你来这边还没多久吧 是的 + +62 +00:03:52,916 --> 00:03:53,423 +你来这边还没多久吧是的 + +63 +00:03:53,424 --> 00:03:56,975 +你来这边还没多久吧 是的 + +64 +00:03:57,991 --> 00:04:02,050 +我想指导你新的工作内容 + +65 +00:04:02,050 --> 00:04:04,079 +不是护理 + +66 +00:04:04,080 --> 00:04:05,602 +而是我主导的属于我的特殊业务 + +67 +00:04:05,602 --> 00:04:06,617 +而是我主导的 属于我的特殊业务 + +68 +00:04:06,617 --> 00:04:07,124 +而是我主导的属于我的特殊业务 + +69 +00:04:07,124 --> 00:04:08,139 +而是我主导的 属于我的特殊业务 + +70 +00:04:08,139 --> 00:04:08,646 +而是我主导的属于我的特殊业务 + +71 +00:04:08,647 --> 00:04:09,661 +而是我主导的 属于我的特殊业务 + +72 +00:04:09,662 --> 00:04:12,706 +下次夜班是什么时候呢 + +73 +00:04:13,214 --> 00:04:16,257 +是的 明天就是了 + +74 +00:04:16,257 --> 00:04:16,765 +图 + +75 +00:04:16,766 --> 00:04:19,810 +那么明天晚上十点 + +76 +00:04:19,810 --> 00:04:24,885 +你能来空病房209号室吗 + +77 +00:04:24,885 --> 00:04:27,422 +我明白了 + +78 +00:04:27,930 --> 00:04:28,437 +留言 + +79 +00:04:28,437 --> 00:04:30,466 +北冈小姐 + +80 +00:04:30,467 --> 00:04:36,556 +你也作为她的指导员 一起参加吧 + +81 +00:04:36,556 --> 00:04:37,063 +维森 健康 + +82 +00:04:37,064 --> 00:04:37,571 +歹史融合刑務 + +83 +00:04:37,571 --> 00:04:40,108 +我知道了 + +84 +00:04:40,108 --> 00:04:40,615 +天文教研小组 + +85 +00:04:40,616 --> 00:04:41,123 +爱尔联合机构 + +86 +00:04:42,645 --> 00:04:43,152 +___ + +87 +00:04:43,153 --> 00:04:48,227 +--- + +88 +00:04:48,735 --> 00:04:52,794 +HTML + +89 +00:04:53,302 --> 00:04:53,809 +--- --- + +90 +00:04:53,809 --> 00:04:54,316 +------ + +91 +00:04:54,824 --> 00:04:57,361 +那个 + +92 +00:04:57,361 --> 00:05:04,972 +桐谷先生还在头痛 需要医生开药 + +93 +00:05:04,973 --> 00:05:08,525 +让你久等了 北冈小姐 + +94 +00:05:09,540 --> 00:05:10,554 +不会 + +95 +00:05:10,555 --> 00:05:14,614 +能让权藤医生亲自指导我 这可是千载难逢的机会 + +96 +00:05:14,614 --> 00:05:16,644 +完全不觉得麻烦 + +97 +00:05:16,644 --> 00:05:19,688 +你说的指导是 + +98 +00:05:26,793 --> 00:05:32,882 +请不要这样 不要 + +99 +00:05:34,405 --> 00:05:36,941 +请住手 + +100 +00:05:42,016 --> 00:05:45,568 +等一下 你在做什么 + +101 +00:05:45,568 --> 00:05:51,150 +请住手 等一下 你干什么 + +102 +00:05:51,150 --> 00:05:52,672 +--- + +103 +00:05:55,210 --> 00:06:00,284 +好痛 北冈小姐 + +104 +00:06:00,284 --> 00:06:05,866 +你对我做这种事 + +105 +00:06:05,866 --> 00:06:11,955 +就不怕你妈妈出什么事吗 + +106 +00:06:13,985 --> 00:06:19,059 +你妈妈住院的医院 + +107 +00:06:19,060 --> 00:06:24,134 +就是这间医院的旗下医院 + +108 +00:06:24,134 --> 00:06:30,223 +而且负责治疗你妈妈的医生 + +109 +00:06:30,731 --> 00:06:36,312 +是我的手下 + +110 +00:06:37,835 --> 00:06:42,401 +这是什么意思呢 你应该明白吧 + +111 +00:06:44,939 --> 00:06:48,491 +聪明的你 + +112 +00:06:48,491 --> 00:06:49,506 +应该已经察觉到 至今为止的一切了吧 + +113 +00:06:49,506 --> 00:06:50,013 +应该已经察觉到至今为止的一切了吧 + +114 +00:06:50,014 --> 00:06:54,073 +应该已经察觉到 至今为止的一切了吧 + +115 +00:06:54,581 --> 00:07:00,669 +你妈妈的生命 + +116 +00:07:01,177 --> 00:07:08,788 +等同于掌握在你的手中了 + +117 +00:07:17,415 --> 00:07:19,445 +--- + +118 +00:07:23,505 --> 00:07:26,042 +请住手 + +119 +00:07:26,042 --> 00:07:29,086 +放心吧 放心吧 + +120 +00:07:32,131 --> 00:07:39,235 +就当作是我的触诊吧 + +121 +00:07:39,235 --> 00:07:41,772 +请住手 + +122 +00:07:45,832 --> 00:07:46,339 +HTML + +123 +00:07:47,355 --> 00:07:50,399 +不要做这种事 + +124 +00:07:50,907 --> 00:07:51,414 +------------------ + +125 +00:07:53,951 --> 00:07:59,025 +--------------- + +126 +00:07:59,533 --> 00:08:00,040 +------------------ + +127 +00:08:01,055 --> 00:08:07,144 +医生 请住手 安静点 + +128 +00:08:07,145 --> 00:08:09,174 +--- + +129 +00:08:09,175 --> 00:08:13,741 +你想想妈妈的生命 + +130 +00:08:13,742 --> 00:08:19,323 +你应该知道作为女儿 该做些什么才对的 + +131 +00:08:19,323 --> 00:08:19,830 +--- + +132 +00:08:22,876 --> 00:08:28,457 +真的原谅我吧 为什么权藤医生要做这种事呢 + +133 +00:08:28,965 --> 00:08:31,502 +这也是没办法的事 + +134 +00:08:31,502 --> 00:08:34,039 +在这家医院呢 + +135 +00:08:34,039 --> 00:08:41,142 +为了吸引入住 单人病房的VIP患者 + +136 +00:08:41,143 --> 00:08:44,694 +我们提供了一种特别服务 + +137 +00:08:46,725 --> 00:08:50,784 +北冈小姐你 + +138 +00:08:50,785 --> 00:08:56,366 +看起来很纯真嘛 + +139 +00:08:56,367 --> 00:08:56,873 +不这么做的话 可没法胜任患者的对象 + +140 +00:08:56,874 --> 00:08:57,381 +不这么做的话可没法胜任患者的对象 + +141 +00:08:57,382 --> 00:08:58,902 +不这么做的话 可没法胜任患者的对象 + +142 +00:08:58,904 --> 00:09:00,425 +不这么做的话可没法胜任患者的对象 + +143 +00:09:00,426 --> 00:09:00,933 +不这么做的话 可没法胜任患者的对象 + +144 +00:09:00,934 --> 00:09:01,948 +不这么做的话可没法胜任患者的对象 + +145 +00:09:01,949 --> 00:09:03,978 +不这么做的话 可没法胜任患者的对象 + +146 +00:09:03,978 --> 00:09:04,484 +不这么做的话可没法胜任患者的对象 + +147 +00:09:07,023 --> 00:09:12,097 +请住手 这个跟照顾患者没关系 + +148 +00:09:12,605 --> 00:09:13,112 +只会说漂亮话 在这个世上是行不通的 + +149 +00:09:13,112 --> 00:09:13,618 +只会说漂亮话在这个世上是行不通的 + +150 +00:09:13,620 --> 00:09:14,127 +只会说漂亮话 在这个世上是行不通的 + +151 +00:09:14,127 --> 00:09:14,633 +只会说漂亮话在这个世上是行不通的 + +152 +00:09:14,635 --> 00:09:15,141 +只会说漂亮话 在这个世上是行不通的 + +153 +00:09:15,142 --> 00:09:15,649 +只会说漂亮话在这个世上是行不通的 + +154 +00:09:15,650 --> 00:09:16,664 +只会说漂亮话 在这个世上是行不通的 + +155 +00:09:16,664 --> 00:09:17,679 +只会说漂亮话在这个世上是行不通的 + +156 +00:09:17,679 --> 00:09:18,185 +只会说漂亮话 在这个世上是行不通的 + +157 +00:09:18,187 --> 00:09:19,200 +只会说漂亮话在这个世上是行不通的 + +158 +00:09:19,202 --> 00:09:19,709 +只会说漂亮话 在这个世上是行不通的 + +159 +00:09:19,709 --> 00:09:20,215 +--- + +160 +00:09:22,246 --> 00:09:22,752 +2.0 + +161 +00:09:23,769 --> 00:09:24,276 +JILLIAN MILLER + +162 +00:09:24,276 --> 00:09:24,782 +女厨 黑袜 + +163 +00:09:24,783 --> 00:09:25,290 +北医三附院 + +164 +00:09:25,291 --> 00:09:28,334 +身体放松一点吧 + +165 +00:09:41,529 --> 00:09:49,647 +你要练习跟患者接吻 + +166 +00:10:00,304 --> 00:10:08,930 +北冈小姐 面对患者的时候呢 要像这样脱掉对方的衣服 + +167 +00:10:16,035 --> 00:10:18,064 +--- + +168 +00:10:18,065 --> 00:10:21,108 +患者呀 + +169 +00:10:21,617 --> 00:10:27,198 +并不是只有年轻英后的男性而已 + +170 +00:10:27,199 --> 00:10:30,242 +有的远比我还要 + +171 +00:10:30,244 --> 00:10:38,362 +偶尔也要接待 比自己年长的大叔 + +172 +00:10:46,989 --> 00:10:50,032 +医生请住手 + +173 +00:10:50,541 --> 00:10:53,584 +真是漂亮的后背 + +174 +00:10:53,586 --> 00:10:54,093 +说不定会有喜欢后背的客人来呢 + +175 +00:10:54,093 --> 00:10:55,614 +说不定会有 喜欢后背的客人来呢 + +176 +00:10:55,616 --> 00:10:58,151 +说不定会有喜欢后背的客人来呢 + +177 +00:10:58,153 --> 00:10:58,660 +说不定会有 喜欢后背的客人来呢 + +178 +00:10:58,660 --> 00:10:59,166 +说不定会有喜欢后背的客人来呢 + +179 +00:11:01,705 --> 00:11:05,257 +请住手 不要 + +180 +00:11:09,317 --> 00:11:12,361 +来 把手拿开 + +181 +00:11:12,361 --> 00:11:13,376 +你以为你的妈妈的生命 是谁掌握着呀 + +182 +00:11:13,376 --> 00:11:14,391 +你以为你的妈妈的生命是谁掌握着呀 + +183 +00:11:14,391 --> 00:11:14,897 +你以为你的妈妈的生命 是谁掌握着呀 + +184 +00:11:14,899 --> 00:11:16,420 +你以为你的妈妈的生命是谁掌握着呀 + +185 +00:11:16,421 --> 00:11:18,958 +你以为你的妈妈的生命 是谁掌握着呀 + +186 +00:11:18,958 --> 00:11:19,464 +你以为你的妈妈的生命是谁掌握着呀 + +187 +00:11:19,465 --> 00:11:20,479 +你以为你的妈妈的生命 是谁掌握着呀 + +188 +00:11:23,525 --> 00:11:26,061 +--- + +189 +00:11:27,585 --> 00:11:32,151 +不抵抗的话就马上结束了 + +190 +00:11:32,152 --> 00:11:33,673 +只是如果反抗的话 + +191 +00:11:33,674 --> 00:11:34,180 +只是 如果反抗的话 + +192 +00:11:34,181 --> 00:11:36,210 +只是如果反抗的话 + +193 +00:11:36,211 --> 00:11:40,777 +我多少也会变得粗暴起来 + +194 +00:11:40,778 --> 00:11:41,792 +--- + +195 +00:11:51,434 --> 00:11:51,940 +------------------ + +196 +00:11:59,553 --> 00:12:00,060 +. + +197 +00:12:00,061 --> 00:12:04,120 +年轻又有弹性的屁股 + +198 +00:12:04,120 --> 00:12:08,686 +臀大肌很发达 + +199 +00:12:08,687 --> 00:12:10,209 +应该能生下健康的孩子 + +200 +00:12:10,210 --> 00:12:10,717 +健康的孩子 + +201 +00:12:10,717 --> 00:12:11,223 +应该熊孩子 + +202 +00:12:11,225 --> 00:12:11,732 +应该熊生,健康的孩子 + +203 +00:12:11,732 --> 00:12:12,238 +应该能生,健康的孩子 + +204 +00:12:12,240 --> 00:12:12,747 +应该熊生,健康的孩子 + +205 +00:12:12,747 --> 00:12:13,253 +应该能生下健康的孩子 + +206 +00:12:13,254 --> 00:12:15,284 +请住手 + +207 +00:12:15,284 --> 00:12:21,372 +作为护士是很棒的身材 + +208 +00:12:23,403 --> 00:12:34,059 +--- + +209 +00:12:47,760 --> 00:12:50,296 +身体不要用力了 + +210 +00:12:50,298 --> 00:12:50,805 +B + +211 +00:13:01,461 --> 00:13:14,654 +--- + +212 +00:13:14,655 --> 00:13:19,728 +即使吵闹 也不会有人来的 + +213 +00:13:19,729 --> 00:13:24,803 +这里只有你我两人 + +214 +00:13:24,804 --> 00:13:27,340 +把手拿开 + +215 +00:13:27,848 --> 00:13:30,892 +接下来检查奶子的状况 + +216 +00:13:30,893 --> 00:13:31,400 +女妾来坐奶子的状况 + +217 +00:13:31,401 --> 00:13:31,907 +妾也来坐奶江的状况。 + +218 +00:13:39,012 --> 00:13:42,564 +腹部没什么问题呢 + +219 +00:13:43,072 --> 00:13:47,637 +没有皮肤科医生出场的机会了 + +220 +00:13:50,683 --> 00:13:53,219 +我看看 + +221 +00:13:53,221 --> 00:13:56,265 +这可是女性的重要部位 我要直接摸了 + +222 +00:13:56,265 --> 00:14:00,324 +请住手 让我来帮你检查吧 + +223 +00:14:04,892 --> 00:14:07,429 +把手拿开啦 + +224 +00:14:08,444 --> 00:14:11,488 +别让我重复同样的话 + +225 +00:14:11,996 --> 00:14:16,055 +妈妈得救不了也没关系吗 + +226 +00:14:16,055 --> 00:14:18,592 +请住手 + +227 +00:14:22,652 --> 00:14:27,218 +怎么 你嘴上说的倒是好听 + +228 +00:14:28,234 --> 00:14:31,786 +乳头都这么硬了 + +229 +00:14:32,801 --> 00:14:33,308 +四学综合硬 + +230 +00:14:33,309 --> 00:14:33,815 +国家质监来说呢 有硬 + +231 +00:14:33,816 --> 00:14:34,323 +国家质保来说呢 有缺失补交硬 + +232 +00:14:34,323 --> 00:14:34,829 +医学发达来说呢 有快感的话就不硬 + +233 +00:14:34,831 --> 00:14:40,411 +医学角度来说呢 有快感的话就会变硬 + +234 +00:14:41,935 --> 00:14:53,098 +--- + +235 +00:15:01,725 --> 00:15:05,276 +怎么了 忍着不发出声音吗 + +236 +00:15:07,815 --> 00:15:11,873 +你是怕羞的孩子吗 + +237 +00:15:11,874 --> 00:15:14,411 +看吧 你自己看 + +238 +00:15:14,411 --> 00:15:20,500 +左边的乳头和右边的乳头 硬度完全不一样 + +239 +00:15:22,023 --> 00:15:25,066 +这个也变硬了 + +240 +00:15:42,828 --> 00:15:43,334 +--- + +241 +00:15:46,888 --> 00:15:55,006 +不稍微发出点舒服的声音的话 患者们可不会兴奋起来 + +242 +00:15:56,529 --> 00:15:58,050 +好嘞 + +243 +00:15:58,051 --> 00:16:02,111 +必须得让你更害羞才行 + +244 +00:16:02,111 --> 00:16:06,678 +下来吧 快点 + +245 +00:16:09,215 --> 00:16:11,752 +站在这里 + +246 +00:16:23,424 --> 00:16:27,989 +你现在是什么心情 放过我吧 + +247 +00:16:27,991 --> 00:16:30,020 +不行 + +248 +00:16:30,020 --> 00:16:32,556 +这才刚开始呢 + +249 +00:16:34,080 --> 00:16:37,123 +坐在这里 + +250 +00:16:51,333 --> 00:16:51,839 +HTML + +251 +00:16:54,378 --> 00:16:57,929 +原来如此 + +252 +00:16:59,452 --> 00:17:05,033 +你有好好处理这边的阴毛呢 + +253 +00:17:07,064 --> 00:17:09,600 +A + +254 +00:17:12,645 --> 00:17:16,705 +气味也没异常 很好 + +255 +00:17:18,227 --> 00:17:24,316 +味道如何 不要这样 + +256 +00:17:35,480 --> 00:17:39,032 +目前没有问题 + +257 +00:17:40,047 --> 00:17:45,629 +因为重点在体内嘛 来吧 + +258 +00:17:45,629 --> 00:17:50,703 +对于患者 对于VIP患者 + +259 +00:17:50,704 --> 00:17:53,748 +可不能失礼了呢 + +260 +00:17:53,748 --> 00:18:00,852 +我会好好地诊察的 别这样 + +261 +00:18:00,853 --> 00:18:04,912 +这是诊察不要 + +262 +00:18:07,957 --> 00:18:11,508 +求求你了 放过我吧 + +263 +00:18:14,046 --> 00:18:17,598 +外观上没有异常呢 + +264 +00:18:21,658 --> 00:18:24,702 +身为护士你就做好觉悟吧 + +265 +00:18:25,210 --> 00:18:27,747 +不要 住手 + +266 +00:18:40,433 --> 00:18:40,940 +--- + +267 +00:18:41,448 --> 00:18:41,955 +--------------- + +268 +00:18:45,000 --> 00:18:47,537 +不要这样 + +269 +00:18:53,119 --> 00:18:53,626 +--- + +270 +00:18:54,134 --> 00:18:54,641 +------ + +271 +00:18:54,641 --> 00:18:55,148 +------------------ + +272 +00:18:56,671 --> 00:19:03,267 +北冈小姐 你不用忍耐的 + +273 +00:19:04,790 --> 00:19:07,834 +这里都一抖一抖了不是吗 + +274 +00:19:07,835 --> 00:19:09,864 +这可是呢 + +275 +00:19:09,865 --> 00:19:12,909 +身体在意识中渴求男人的证据 + +276 +00:19:12,909 --> 00:19:15,954 +不对 请住手 + +277 +00:19:15,954 --> 00:19:17,983 +你妈妈身体健康的时候一定也渴望着男人的滋润吧 + +278 +00:19:17,984 --> 00:19:18,491 +你妈妈身体健康的时候 一定也渴望着男人的滋润吧 + +279 +00:19:18,491 --> 00:19:21,028 +你妈妈身体健康的时候一定也渴望着男人的滋润吧 + +280 +00:19:21,028 --> 00:19:23,058 +别说了 + +281 +00:19:25,595 --> 00:19:26,102 +------ + +282 +00:19:26,103 --> 00:19:26,610 +HTML + +283 +00:19:26,610 --> 00:19:32,699 +------ + +284 +00:19:32,700 --> 00:19:33,714 +--------------------- + +285 +00:19:33,714 --> 00:19:35,236 +------ + +286 +00:19:35,237 --> 00:19:35,744 +--- + +287 +00:19:35,744 --> 00:19:42,341 +接下来要分泌出健康体液了 + +288 +00:19:42,341 --> 00:19:42,848 +------ + +289 +00:19:42,848 --> 00:19:43,355 +--- + +290 +00:19:43,356 --> 00:19:43,863 +------ + +291 +00:19:43,863 --> 00:19:45,893 +--- + +292 +00:19:45,893 --> 00:19:46,400 +------ + +293 +00:19:46,401 --> 00:19:47,415 +--- + +294 +00:19:47,923 --> 00:19:48,430 +------ + +295 +00:19:48,430 --> 00:19:49,952 +--- + +296 +00:19:50,968 --> 00:19:51,982 +------------------ + +297 +00:19:51,982 --> 00:19:52,489 +--- + +298 +00:19:52,490 --> 00:19:52,997 +------ + +299 +00:19:54,012 --> 00:19:54,519 +--- + +300 +00:20:10,250 --> 00:20:15,324 +放过我吧 求求你 + +301 +00:20:24,966 --> 00:20:25,473 +HTML + +302 +00:20:26,488 --> 00:20:30,040 +不要 停下来 + +303 +00:20:41,204 --> 00:20:44,756 +你其实也挺乐意的嘛 北冈小姐 + +304 +00:20:56,935 --> 00:21:00,487 +就这样放松吧 + +305 +00:21:07,084 --> 00:21:10,128 +已经准备好了 + +306 +00:21:10,129 --> 00:21:18,755 +接下来就要服侍男人的身体了 + +307 +00:21:18,755 --> 00:21:23,322 +不要 这不是说不要的时候 + +308 +00:21:23,322 --> 00:21:26,366 +你妈妈变成什么样都无所谓吗 + +309 +00:21:26,367 --> 00:21:29,918 +你想救妈妈吧 + +310 +00:21:29,919 --> 00:21:32,963 +只要我一声令下 + +311 +00:21:32,963 --> 00:21:38,037 +她就能进行最棒的手术了 + +312 +00:21:46,664 --> 00:21:49,201 +不要停下来 要继续动 + +313 +00:21:50,724 --> 00:21:53,768 +要好好看着我这边 + +314 +00:21:59,858 --> 00:22:03,410 +距离太远了 靠近一点 + +315 +00:22:05,440 --> 00:22:08,991 +保持社交距离已经结束了 + +316 +00:22:10,007 --> 00:22:13,051 +靠近点放过我吧 + +317 +00:22:24,215 --> 00:22:26,752 +摸上去 + +318 +00:22:27,767 --> 00:22:33,349 +不要用太多力气 适应的力量 + +319 +00:22:35,886 --> 00:22:41,468 +不要停下的 快点继续动 + +320 +00:22:42,483 --> 00:22:45,020 +我不要 + +321 +00:22:47,558 --> 00:22:52,631 +偶尔也要用嘴巴 + +322 +00:22:52,632 --> 00:22:57,706 +嘴巴 先张开嘴巴吧 + +323 +00:23:00,751 --> 00:23:12,929 +--- + +324 +00:23:14,959 --> 00:23:18,004 +不要碰到牙齿 + +325 +00:23:19,526 --> 00:23:20,033 +--- + +326 +00:23:20,034 --> 00:23:23,585 +要多分泌点唾液 + +327 +00:23:24,093 --> 00:23:24,600 +___ + +328 +00:23:24,601 --> 00:23:31,197 +然后把舌头缠在肉棒上 + +329 +00:23:37,287 --> 00:23:40,331 +那是什么表情 + +330 +00:23:40,839 --> 00:23:43,883 +你能不能再配合一点呀 + +331 +00:23:45,406 --> 00:23:49,465 +站起来 到检查台上 + +332 +00:23:50,988 --> 00:23:52,510 +--- + +333 +00:23:52,510 --> 00:23:55,554 +我也说了好几遍了 + +334 +00:23:55,555 --> 00:23:58,092 +不想做洗手的动作 + +335 +00:23:58,092 --> 00:23:59,106 +就是因为你不配合 才变成这样 + +336 +00:23:59,107 --> 00:23:59,614 +就是因为你不配合才变成这样 + +337 +00:23:59,614 --> 00:24:00,629 +就是因为你不配合 才变成这样 + +338 +00:24:00,629 --> 00:24:01,136 +就是因为你不配合才变成这样 + +339 +00:24:01,137 --> 00:24:02,151 +就是因为你不配合 才变成这样 + +340 +00:24:02,152 --> 00:24:06,211 +知道了吗 张嘴 + +341 +00:24:07,733 --> 00:24:10,270 +快点张开 听话 + +342 +00:24:27,524 --> 00:24:30,060 +------ + +343 +00:24:30,061 --> 00:24:30,568 +--- + +344 +00:24:30,568 --> 00:24:31,583 +------ + +345 +00:24:32,091 --> 00:24:32,598 +------------------ + +346 +00:24:33,106 --> 00:24:35,642 +含进喉咙里 + +347 +00:24:36,150 --> 00:24:37,165 +------ + +348 +00:24:37,165 --> 00:24:37,672 +--------------- + +349 +00:24:38,180 --> 00:24:39,702 +------ + +350 +00:24:39,702 --> 00:24:40,209 +------------ + +351 +00:24:40,210 --> 00:24:40,717 +--------------- + +352 +00:24:40,717 --> 00:24:41,224 +------------------ + +353 +00:24:41,732 --> 00:24:42,239 +------ + +354 +00:24:42,240 --> 00:24:43,761 +------------------ + +355 +00:24:43,762 --> 00:24:44,269 +------ + +356 +00:24:45,792 --> 00:24:46,299 +--- + +357 +00:24:47,821 --> 00:24:50,358 +别这么痛苦嘛 + +358 +00:24:50,359 --> 00:24:55,433 +有很多患者的肉棒会更大的 + +359 +00:24:55,940 --> 00:24:56,447 +------ + +360 +00:24:56,955 --> 00:25:03,044 +--- + +361 +00:25:03,045 --> 00:25:04,059 +只要你努力的话你妈妈获救的机率也会提高的 + +362 +00:25:04,060 --> 00:25:04,567 +只要你努力的话 你妈妈获救的机率也会提高的 + +363 +00:25:04,567 --> 00:25:07,611 +只要你努力的话你妈妈获救的机率也会提高的 + +364 +00:25:07,612 --> 00:25:10,148 +加油 + +365 +00:25:10,656 --> 00:25:11,163 +--- + +366 +00:25:11,164 --> 00:25:11,671 +------------ + +367 +00:25:12,179 --> 00:25:12,686 +------ + +368 +00:25:13,194 --> 00:25:19,790 +--- + +369 +00:25:29,432 --> 00:25:32,476 +越来越好了呢 + +370 +00:25:33,491 --> 00:25:36,535 +我再动快一点 + +371 +00:25:37,551 --> 00:25:38,058 +--- + +372 +00:25:40,595 --> 00:25:41,102 +------------------ + +373 +00:25:41,103 --> 00:25:42,625 +--- + +374 +00:25:48,207 --> 00:25:52,774 +不错 就这样继续下去 + +375 +00:25:52,774 --> 00:25:55,311 +坚持住 + +376 +00:26:03,938 --> 00:26:07,997 +很好 北冈小姐 + +377 +00:26:11,549 --> 00:26:14,086 +还没有结束 + +378 +00:26:32,862 --> 00:26:33,369 +--- + +379 +00:26:33,369 --> 00:26:35,399 +继续加油吧 + +380 +00:26:45,548 --> 00:26:46,055 +--- + +381 +00:26:46,563 --> 00:26:47,070 +Background + +382 +00:26:47,070 --> 00:26:47,577 +--- + +383 +00:26:48,085 --> 00:26:48,592 +------ + +384 +00:26:49,100 --> 00:26:50,115 +--- + +385 +00:26:50,115 --> 00:26:50,622 +------ + +386 +00:26:52,145 --> 00:26:52,652 +--- + +387 +00:26:53,160 --> 00:26:53,667 +HTML:
+ +388 +00:27:02,801 --> 00:27:03,308 +--- + +389 +00:27:05,846 --> 00:27:06,353 +Background + +390 +00:27:07,876 --> 00:27:08,383 +--- + +391 +00:27:09,398 --> 00:27:09,905 +--------------- + +392 +00:27:11,428 --> 00:27:11,935 +--- + +393 +00:27:11,935 --> 00:27:12,442 +HTML + +394 +00:27:13,965 --> 00:27:14,472 +--- + +395 +00:27:14,472 --> 00:27:14,979 +--------------- + +396 +00:27:15,487 --> 00:27:15,994 +--- + +397 +00:27:15,995 --> 00:27:18,531 +把身心都放松出来吧 + +398 +00:27:18,532 --> 00:27:19,039 +98室[巴花堂]永久地址489155.com把身心都放松出来吧 + +399 +00:27:19,039 --> 00:27:19,546 +98室[芭花堂]永久地址489155.com把身心都放松出来吧 + +400 +00:27:19,547 --> 00:27:21,069 +98室[巴花堂]永久地址489155.com把身心都放松出来吧 + +401 +00:27:21,069 --> 00:27:21,576 +98室[芭花室]永久地址489155.com把身心都放松出来吧 + +402 +00:27:21,576 --> 00:27:22,083 +把身心都放松出来吧 + +403 +00:27:22,591 --> 00:27:23,098 +--- + +404 +00:27:24,621 --> 00:27:29,188 +把腿张开 等一下 + +405 +00:27:29,188 --> 00:27:31,217 +你想做什么 + +406 +00:27:31,218 --> 00:27:33,755 +到这一步 要做的事只有一件了吧 + +407 +00:27:33,755 --> 00:27:37,307 +求求你 这个千万不要 + +408 +00:27:37,307 --> 00:27:39,844 +为什么呢 + +409 +00:27:39,844 --> 00:27:41,874 +--- + +410 +00:27:41,874 --> 00:27:46,441 +难道说你是第一次吗 + +411 +00:27:46,441 --> 00:27:46,948 +--- + +412 +00:27:46,949 --> 00:27:52,023 +如果是的话那我就更应该负责 + +413 +00:27:52,023 --> 00:27:56,082 +帮你把处女膜捅破了 + +414 +00:27:56,083 --> 00:28:02,679 +可不能给VIP患者们便宜 + +415 +00:28:02,679 --> 00:28:04,709 +不要紧 不要紧 + +416 +00:28:04,709 --> 00:28:09,783 +谁都会在遥远的路上 + +417 +00:28:09,783 --> 00:28:18,410 +求求你 不要插进小穴 被我这样高明的想法贯穿 + +418 +00:28:18,410 --> 00:28:20,439 +你真是过方不要 + +419 +00:28:20,440 --> 00:28:20,947 +你真是过方 不要 + +420 +00:28:20,947 --> 00:28:23,484 +放松点吧 + +421 +00:28:28,559 --> 00:28:31,603 +小穴好紧 + +422 +00:28:32,111 --> 00:28:34,648 +处女膜 + +423 +00:28:39,215 --> 00:28:44,289 +不要紧 只要进去了就没事了 + +424 +00:28:44,290 --> 00:28:44,797 +--- + +425 +00:28:44,797 --> 00:28:46,826 +不行马上就会习惯啦 + +426 +00:28:46,827 --> 00:28:47,334 +不行 马上就会习惯啦 + +427 +00:28:47,334 --> 00:28:49,871 +不行马上就会习惯啦 + +428 +00:28:49,871 --> 00:28:54,438 +真的不行 不要 + +429 +00:28:56,976 --> 00:28:58,498 +--- + +430 +00:28:58,498 --> 00:28:59,005 +------ + +431 +00:28:59,005 --> 00:29:02,557 +--- + +432 +00:29:02,558 --> 00:29:06,109 +只有刚开始会痛 + +433 +00:29:06,110 --> 00:29:11,184 +不要 请住手 我不行了 + +434 +00:29:11,184 --> 00:29:12,706 +--- + +435 +00:29:17,781 --> 00:29:21,840 +深呼吸 深呼吸 + +436 +00:29:22,348 --> 00:29:25,899 +--- + +437 +00:29:29,452 --> 00:29:34,526 +你慢慢习惯了吧 不行 + +438 +00:29:38,586 --> 00:29:41,630 +医生请住手吧 没事的 + +439 +00:29:42,138 --> 00:29:44,167 +我做不到 + +440 +00:29:49,242 --> 00:29:53,301 +真不错呢 看到你处女膜被摸 + +441 +00:29:53,302 --> 00:29:58,883 +被我侵犯的样子让我好兴奋 + +442 +00:30:00,913 --> 00:30:01,420 +--------------- + +443 +00:30:01,421 --> 00:30:04,465 +--- + +444 +00:30:04,973 --> 00:30:05,480 +------------ + +445 +00:30:13,599 --> 00:30:14,106 +--- + +446 +00:30:14,107 --> 00:30:19,181 +我再插深一点 + +447 +00:30:19,181 --> 00:30:22,226 +请住手 + +448 +00:30:24,256 --> 00:30:25,270 +--- + +449 +00:30:26,286 --> 00:30:26,793 +------------------ + +450 +00:30:26,793 --> 00:30:27,300 +------ + +451 +00:30:27,300 --> 00:30:27,807 +------------------ + +452 +00:30:27,808 --> 00:30:33,389 +我不行了啦 就快结束了 + +453 +00:30:33,390 --> 00:30:33,897 +------------------ + +454 +00:30:33,897 --> 00:30:36,941 +快点结束 求求你 + +455 +00:30:36,942 --> 00:30:37,449 +------------------ + +456 +00:30:37,957 --> 00:30:41,508 +求求你 对不起 我不行了 + +457 +00:30:41,509 --> 00:30:44,046 +不用道歉啦 + +458 +00:30:44,046 --> 00:30:46,075 +你只要帮我就好 + +459 +00:30:46,076 --> 00:30:48,613 +我不行 真的不行 + +460 +00:30:48,613 --> 00:30:50,642 +------------------ + +461 +00:30:50,643 --> 00:30:51,150 +------ + +462 +00:30:51,150 --> 00:30:51,657 +--- + +463 +00:30:51,658 --> 00:30:52,165 +------ + +464 +00:30:52,165 --> 00:30:53,180 +------------------ + +465 +00:30:53,180 --> 00:30:54,194 +------ + +466 +00:30:54,195 --> 00:30:54,702 +------------ + +467 +00:30:54,702 --> 00:30:55,209 +------------------ + +468 +00:30:55,210 --> 00:30:55,717 +------ + +469 +00:30:55,717 --> 00:30:56,224 +--- + +470 +00:30:56,225 --> 00:30:57,239 +------ + +471 +00:30:57,747 --> 00:30:58,761 +--- + +472 +00:30:59,269 --> 00:30:59,776 +------ + +473 +00:30:59,777 --> 00:31:00,284 +--- + +474 +00:31:01,299 --> 00:31:01,806 +--------------- + +475 +00:31:01,806 --> 00:31:11,447 +--- + +476 +00:31:14,493 --> 00:31:17,537 +你现在也舒服起来了嘛 + +477 +00:31:20,582 --> 00:31:28,193 +--- + +478 +00:31:41,387 --> 00:31:44,431 +现在做的就是所谓的正常位 + +479 +00:31:44,432 --> 00:31:44,939 +现在做的 就是所谓的正常位 + +480 +00:31:44,939 --> 00:31:46,968 +现在做的就是所谓的正常位 + +481 +00:31:46,969 --> 00:31:50,013 +根据患者的不同也有各种各样的体位 + +482 +00:31:50,014 --> 00:31:50,521 +根据患者的不同 也有各种各样的体位 + +483 +00:31:50,521 --> 00:31:53,058 +根据患者的不同也有各种各样的体位 + +484 +00:31:53,058 --> 00:31:55,088 +所以就和我一起试试吧 + +485 +00:31:55,088 --> 00:31:57,625 +不要 我不要 + +486 +00:32:00,670 --> 00:32:04,221 +你可以趴起来了 + +487 +00:32:04,729 --> 00:32:07,266 +快点 + +488 +00:32:08,281 --> 00:32:13,355 +--- + +489 +00:32:18,430 --> 00:32:18,937 +A + +490 +00:32:22,997 --> 00:32:24,519 +不要 不要乱动 + +491 +00:32:24,520 --> 00:32:25,027 +不要不要乱动 + +492 +00:32:25,027 --> 00:32:26,042 +不要 不要乱动 + +493 +00:32:26,042 --> 00:32:28,579 +要试试看各种体位 + +494 +00:32:28,579 --> 00:32:29,594 +我做不到 + +495 +00:32:29,594 --> 00:32:32,638 +真正上场的时候 你就不会太紧张了 + +496 +00:32:32,639 --> 00:32:33,146 +真正上场的时候你就不会太紧张了 + +497 +00:32:33,146 --> 00:32:34,668 +真正上场的时候 你就不会太紧张了 + +498 +00:32:45,325 --> 00:32:45,832 +毕竟有些人就是喜欢那种女性嘛 + +499 +00:32:45,832 --> 00:32:51,921 +毕竟有些人 就是喜欢那种女性嘛 + +500 +00:32:51,922 --> 00:32:56,488 +我做不到 已经不行了 + +501 +00:32:58,011 --> 00:32:58,518 +HTML + +502 +00:32:59,026 --> 00:33:03,085 +这种事怎么可能做得到呢 明天可就来不及适应了 + +503 +00:33:03,085 --> 00:33:05,622 +--- + +504 +00:33:05,622 --> 00:33:06,129 +------------------ + +505 +00:33:06,130 --> 00:33:07,144 +------ + +506 +00:33:07,145 --> 00:33:07,652 +--- + +507 +00:33:07,652 --> 00:33:09,174 +------ + +508 +00:33:09,175 --> 00:33:11,204 +--- + +509 +00:33:12,219 --> 00:33:12,726 +------ + +510 +00:33:12,727 --> 00:33:16,278 +--- + +511 +00:33:16,279 --> 00:33:16,786 +------ + +512 +00:33:17,294 --> 00:33:18,308 +--- + +513 +00:33:18,309 --> 00:33:18,816 +------ + +514 +00:33:19,323 --> 00:33:21,860 +--- + +515 +00:33:22,876 --> 00:33:23,383 +------ + +516 +00:33:25,920 --> 00:33:34,039 +抬起头来 好好看看自己现在身处的状况 + +517 +00:33:58,396 --> 00:34:02,456 +不行 请住手 + +518 +00:34:03,978 --> 00:34:10,067 +求求你 放过我吧 我不会放过你 + +519 +00:34:13,112 --> 00:34:32,395 +--- + +520 +00:34:34,931 --> 00:34:35,438 +------ + +521 +00:34:35,440 --> 00:34:36,454 +--- + +522 +00:34:36,455 --> 00:34:36,962 +------------------ + +523 +00:34:36,962 --> 00:34:40,005 +--- + +524 +00:34:40,007 --> 00:34:40,514 +------ + +525 +00:34:40,514 --> 00:34:43,558 +--- + +526 +00:34:48,126 --> 00:34:51,678 +就是这种感觉 + +527 +00:34:55,737 --> 00:34:59,797 +要插进小穴最里面才可以 + +528 +00:34:59,797 --> 00:35:27,198 +--- + +529 +00:35:27,199 --> 00:35:29,736 +--------------- + +530 +00:35:30,244 --> 00:35:31,765 +------ + +531 +00:35:31,766 --> 00:35:32,780 +--- + +532 +00:35:33,796 --> 00:35:36,840 +别再这样了 不行 + +533 +00:35:36,840 --> 00:35:40,392 +正好舒服起来了 不会停的 + +534 +00:35:40,392 --> 00:35:40,899 +--- + +535 +00:35:41,407 --> 00:35:43,944 +住手 + +536 +00:35:47,497 --> 00:35:50,033 +救救我 + +537 +00:36:05,765 --> 00:36:11,346 +还没结束呢 把腿抬起来 + +538 +00:36:12,361 --> 00:36:12,868 +--------------- + +539 +00:36:20,480 --> 00:36:25,554 +不行 不要 + +540 +00:36:27,077 --> 00:36:29,614 +看清楚我的脸 + +541 +00:36:30,629 --> 00:36:31,136 +第一次的对象可是我这个医务室长 + +542 +00:36:31,137 --> 00:36:31,644 +第一次的对象 可是我这个医务室长 + +543 +00:36:31,644 --> 00:36:32,151 +第一次的对象可是我这个医务室长 + +544 +00:36:32,152 --> 00:36:32,659 +第一次的对象 可是我这个医务室长 + +545 +00:36:32,659 --> 00:36:33,166 +第一次的对象可是我这个医务室长 + +546 +00:36:33,166 --> 00:36:34,181 +第一次的对象 可是我这个医务室长 + +547 +00:36:34,181 --> 00:36:35,196 +第一次的对象可是我这个医务室长 + +548 +00:36:35,196 --> 00:36:35,703 +第一次的对象 可是我这个医务室长 + +549 +00:36:35,704 --> 00:36:36,211 +第一次的对象可是我这个医务室长 + +550 +00:36:36,211 --> 00:36:36,718 +第一次的对象 可是我这个医务室长 + +551 +00:36:36,719 --> 00:36:40,270 +不要 我不要 + +552 +00:36:40,271 --> 00:36:40,778 +--------------- + +553 +00:36:40,778 --> 00:36:41,285 +------ + +554 +00:36:41,286 --> 00:36:41,793 +------------------ + +555 +00:36:42,300 --> 00:36:45,345 +把眼睛睁开看过来 + +556 +00:36:45,345 --> 00:36:49,912 +不要 不可以 + +557 +00:36:49,912 --> 00:36:53,971 +以后你会觉得第一次 + +558 +00:36:53,972 --> 00:36:58,538 +跟权藤医生做真的太好了 + +559 +00:36:58,539 --> 00:37:05,135 +我不要 不可以 住手 + +560 +00:37:08,180 --> 00:37:08,687 +------------------ + +561 +00:37:09,195 --> 00:37:09,702 +--- + +562 +00:37:09,702 --> 00:37:15,284 +让我用力搅动里面 + +563 +00:37:15,284 --> 00:37:16,299 +--- + +564 +00:37:16,299 --> 00:37:17,821 +------ + +565 +00:37:17,821 --> 00:37:19,851 +不行 + +566 +00:37:19,851 --> 00:37:20,866 +------------------ + +567 +00:37:20,866 --> 00:37:21,373 +--- + +568 +00:37:21,881 --> 00:37:22,388 +--------------- + +569 +00:37:22,388 --> 00:37:26,447 +------ + +570 +00:37:26,448 --> 00:37:26,955 +--------------- + +571 +00:37:26,955 --> 00:37:27,462 +------------------ + +572 +00:37:27,463 --> 00:37:28,477 +------ + +573 +00:37:28,478 --> 00:37:28,985 +--- + +574 +00:37:28,985 --> 00:37:30,000 +------ + +575 +00:37:30,000 --> 00:37:30,507 +------------------ + +576 +00:37:30,507 --> 00:37:31,522 +------ + +577 +00:37:31,522 --> 00:37:33,044 +------------------ + +578 +00:37:34,060 --> 00:37:34,567 +------ + +579 +00:37:34,567 --> 00:37:35,074 +------------------ + +580 +00:37:35,074 --> 00:37:35,581 +--- + +581 +00:37:35,582 --> 00:37:36,089 +------------------ + +582 +00:37:36,089 --> 00:37:36,596 +------ + +583 +00:37:37,104 --> 00:37:37,611 +--- --- + +584 +00:37:37,612 --> 00:37:38,626 +------------------ + +585 +00:37:38,627 --> 00:37:39,641 +------ + +586 +00:37:39,641 --> 00:37:40,148 +------------------ + +587 +00:37:40,149 --> 00:37:41,163 +------ + +588 +00:37:41,164 --> 00:37:41,671 +--- + +589 +00:37:41,671 --> 00:37:42,178 +------ + +590 +00:37:42,179 --> 00:37:42,686 +--- + +591 +00:37:42,686 --> 00:37:43,193 +--- --- + +592 +00:37:43,194 --> 00:37:43,701 +------------ + +593 +00:37:43,701 --> 00:37:44,208 +------ + +594 +00:37:44,208 --> 00:37:44,715 +--- + +595 +00:37:44,716 --> 00:37:45,223 +--------------- + +596 +00:37:45,731 --> 00:37:46,238 +--- + +597 +00:37:46,238 --> 00:37:46,745 +------ + +598 +00:37:47,253 --> 00:37:48,267 +--- + +599 +00:37:48,268 --> 00:37:48,775 +------ + +600 +00:37:49,283 --> 00:37:49,790 +--- + +601 +00:37:49,790 --> 00:37:50,297 +------ + +602 +00:37:50,298 --> 00:37:51,312 +--- + +603 +00:37:51,313 --> 00:37:51,820 +------ + +604 +00:37:51,820 --> 00:37:54,357 +--- + +605 +00:37:54,865 --> 00:37:56,387 +------ + +606 +00:37:56,894 --> 00:37:57,401 +--------------- + +607 +00:37:57,402 --> 00:37:57,909 +--- + +608 +00:37:57,909 --> 00:37:58,416 +------ + +609 +00:37:58,417 --> 00:37:58,924 +--- + +610 +00:37:58,924 --> 00:37:59,431 +------ + +611 +00:37:59,432 --> 00:37:59,939 +--- + +612 +00:37:59,939 --> 00:38:00,446 +------------------ + +613 +00:38:00,954 --> 00:38:02,476 +--- + +614 +00:38:02,476 --> 00:38:02,983 +------------------ + +615 +00:38:03,999 --> 00:38:06,028 +--- + +616 +00:38:12,118 --> 00:38:15,162 +你刚刚感觉到了 + +617 +00:38:16,685 --> 00:38:19,729 +用力抓住我 + +618 +00:38:21,252 --> 00:38:22,266 +--- + +619 +00:38:22,267 --> 00:38:22,774 +------ + +620 +00:38:33,430 --> 00:38:37,489 +这个是骑乘位 + +621 +00:38:46,116 --> 00:38:49,668 +不要 不可以 + +622 +00:38:50,176 --> 00:38:54,742 +--- + +623 +00:38:54,743 --> 00:38:55,250 +--------------- + +624 +00:38:55,250 --> 00:38:55,757 +--- + +625 +00:38:55,758 --> 00:38:59,817 +医生 请住手 + +626 +00:38:59,817 --> 00:39:03,369 +--- + +627 +00:39:03,877 --> 00:39:04,384 +------------------ + +628 +00:39:04,892 --> 00:39:05,399 +------ + +629 +00:39:14,026 --> 00:39:17,070 +我都说了你没必要忍耐的 + +630 +00:39:17,070 --> 00:39:19,100 +尽情舒服起来 + +631 +00:39:19,100 --> 00:39:21,129 +不要 把手撑在后面试试吧 + +632 +00:39:21,130 --> 00:39:21,637 +不要把手撑在后面试试吧 + +633 +00:39:21,637 --> 00:39:24,174 +不要 把手撑在后面试试吧 + +634 +00:39:35,846 --> 00:39:38,890 +等一下 别再这样了 + +635 +00:39:42,442 --> 00:39:45,994 +不要 不可以 + +636 +00:39:45,995 --> 00:39:49,546 +不要 求求你停下来 + +637 +00:39:49,547 --> 00:39:50,054 +------ + +638 +00:39:50,054 --> 00:39:51,576 +--- + +639 +00:39:51,576 --> 00:39:52,083 +------------------ + +640 +00:39:52,084 --> 00:39:56,143 +--- + +641 +00:39:58,173 --> 00:39:58,680 +------------ + +642 +00:40:06,800 --> 00:40:09,336 +再做点别的吧 + +643 +00:40:10,859 --> 00:40:13,903 +接吻呢 接吻 + +644 +00:40:13,904 --> 00:40:17,456 +为什么不接吻呢 + +645 +00:40:27,605 --> 00:40:33,694 +别总是反抗哦 + +646 +00:40:37,246 --> 00:40:40,798 +--- + +647 +00:40:42,321 --> 00:40:42,828 +------ + +648 +00:40:44,350 --> 00:40:48,410 +我是要负起责任的 + +649 +00:40:48,917 --> 00:40:53,991 +即使跟VIP患者接触 我也会这样帮你的 + +650 +00:41:00,081 --> 00:41:07,185 +--- + +651 +00:41:07,693 --> 00:41:08,200 +------ + +652 +00:41:08,708 --> 00:41:12,259 +--- + +653 +00:41:12,767 --> 00:41:13,274 +--------------- + +654 +00:41:15,304 --> 00:41:19,364 +危险 危险 要倒了 + +655 +00:41:23,931 --> 00:41:26,468 +你躺下去吧 + +656 +00:41:26,976 --> 00:41:32,557 +果然 第一次 这个姿势最好 + +657 +00:41:36,617 --> 00:41:37,631 +--- + +658 +00:41:40,169 --> 00:41:40,676 +------ + +659 +00:41:40,677 --> 00:42:04,526 +--- + +660 +00:42:04,526 --> 00:42:09,093 +让我好好看看你的脸 + +661 +00:42:09,093 --> 00:42:11,123 +--- + +662 +00:42:11,123 --> 00:42:11,630 +------------------ + +663 +00:42:11,631 --> 00:42:14,167 +不要 + +664 +00:42:14,168 --> 00:42:14,675 +------ + +665 +00:42:14,675 --> 00:42:15,182 +------------------ + +666 +00:42:15,183 --> 00:42:17,212 +--- + +667 +00:42:17,212 --> 00:42:17,719 +------ + +668 +00:42:17,720 --> 00:42:20,257 +--- + +669 +00:42:20,257 --> 00:42:20,764 +------ + +670 +00:42:20,765 --> 00:42:21,272 +------------------ + +671 +00:42:22,287 --> 00:42:25,331 +--- + +672 +00:42:25,332 --> 00:42:25,839 +------ + +673 +00:42:27,361 --> 00:42:28,376 +--- + +674 +00:42:28,376 --> 00:42:28,883 +------------------ + +675 +00:42:29,391 --> 00:42:29,898 +--- + +676 +00:42:30,913 --> 00:42:31,420 +------ + +677 +00:42:31,928 --> 00:42:32,435 +------------------ + +678 +00:42:33,451 --> 00:42:34,972 +------ + +679 +00:42:34,973 --> 00:42:35,480 +--------------------- + +680 +00:42:35,480 --> 00:42:40,047 +------ + +681 +00:42:41,062 --> 00:42:41,569 +------------------ + +682 +00:42:41,570 --> 00:42:42,077 +------ + +683 +00:42:42,077 --> 00:42:43,092 +--- + +684 +00:42:51,211 --> 00:42:53,240 +北冈小姐 + +685 +00:42:53,241 --> 00:42:57,807 +你要接受现实了 + +686 +00:43:00,853 --> 00:43:14,046 +--- + +687 +00:43:15,061 --> 00:43:18,613 +这就是所谓的做爱 + +688 +00:43:19,120 --> 00:43:19,627 +--- + +689 +00:43:22,673 --> 00:43:25,209 +做爱这种事呢 + +690 +00:43:25,210 --> 00:43:29,269 +最后男人射精之后就结束了 + +691 +00:43:29,269 --> 00:43:31,299 +今天 + +692 +00:43:31,299 --> 00:43:34,343 +就要直接在你的里面射出来了 + +693 +00:43:34,344 --> 00:43:39,925 +不要 不可以这样 不要管了 没事的 + +694 +00:43:39,926 --> 00:43:45,507 +住手 不要射进去 我不行了 + +695 +00:43:45,507 --> 00:43:50,581 +拜托你 请住手 不要这样 + +696 +00:43:50,582 --> 00:43:55,148 +接受它吧 求求你了 不要 + +697 +00:43:55,149 --> 00:43:55,656 +--- + +698 +00:43:57,179 --> 00:43:57,686 +--------------- + +699 +00:43:58,701 --> 00:44:01,745 +要射了 + +700 +00:44:02,253 --> 00:44:04,790 +射了 + +701 +00:44:05,298 --> 00:44:09,864 +来吧 好好接住它 + +702 +00:44:14,939 --> 00:44:15,446 +--- + +703 +00:44:18,491 --> 00:44:22,550 +别担心 全部射完就拔出来了 + +704 +00:44:24,073 --> 00:44:24,580 +--------------- + +705 +00:44:30,670 --> 00:44:31,177 +--- + +706 +00:44:31,177 --> 00:44:31,684 +--------------- + +707 +00:44:31,685 --> 00:44:33,207 +------------ + +708 +00:44:33,207 --> 00:44:33,714 +------ + +709 +00:44:33,714 --> 00:44:34,221 +------------ + +710 +00:44:34,729 --> 00:44:35,236 +--- + +711 +00:44:35,237 --> 00:44:36,251 +------------ + +712 +00:44:36,252 --> 00:44:36,759 +--- + +713 +00:44:37,774 --> 00:44:38,281 +------------ + +714 +00:44:38,281 --> 00:44:38,788 +--------------- + +715 +00:44:38,789 --> 00:44:39,296 +------------ + +716 +00:44:39,296 --> 00:44:39,803 +------ + +717 +00:44:40,311 --> 00:44:41,326 +------------ + +718 +00:44:41,326 --> 00:44:41,833 +--- + +719 +00:44:41,834 --> 00:44:42,341 +--------------- + +720 +00:44:42,341 --> 00:44:43,355 +--- + +721 +00:44:43,356 --> 00:44:43,863 +------------ + +722 +00:44:43,863 --> 00:44:44,370 +--------------- + +723 +00:44:44,371 --> 00:44:44,878 +------------ + +724 +00:44:45,386 --> 00:44:45,893 +--- + +725 +00:44:46,401 --> 00:44:46,908 +------------ + +726 +00:44:47,415 --> 00:44:48,430 +--------------- + +727 +00:44:48,430 --> 00:44:48,937 +------------ + +728 +00:44:50,968 --> 00:44:51,982 +明天开始你就是特别单间病房患者的负责人 + +729 +00:44:51,982 --> 00:44:53,504 +明天开始你就是特别单间 病房患者的负责人 + +730 +00:44:53,505 --> 00:44:54,519 +明天开始你就是特别单间病房患者的负责人 + +731 +00:44:54,520 --> 00:44:55,534 +明天开始你就是特别单间 病房患者的负责人 + +732 +00:44:55,535 --> 00:44:56,549 +明天开始你就是特别单间病房患者的负责人 + +733 +00:44:56,549 --> 00:45:05,683 +请感到光荣吧 你妈妈也会很高兴的 + +734 +00:45:06,698 --> 00:45:21,414 +--- + +735 +00:45:22,429 --> 00:45:22,936 +------------------ + +736 +00:45:22,936 --> 00:45:23,443 +Background + +737 +00:45:25,474 --> 00:45:26,995 +------ + +738 +00:45:27,503 --> 00:45:28,010 +------------------ + +739 +00:45:28,011 --> 00:45:32,577 +--- + +740 +00:45:33,085 --> 00:45:33,592 +------ + +741 +00:45:33,593 --> 00:45:34,100 +--- + +742 +00:45:40,697 --> 00:45:45,263 +权藤医生把松井小姐叫出来 + +743 +00:45:45,264 --> 00:45:51,353 +特别单人VIP患者 夜班的呼叫 + +744 +00:45:51,861 --> 00:45:57,949 +那让我想起了讨厌的事情 从那天起我就觉得 + +745 +00:45:57,950 --> 00:46:01,502 +这家医院疯了 + +746 +00:46:04,039 --> 00:46:04,546 +。 + +747 +00:46:09,114 --> 00:46:13,173 +上次真是非常抱歉 + +748 +00:46:17,233 --> 00:46:19,770 +因为我的指导不足 + +749 +00:46:19,770 --> 00:46:22,814 +对患者说了失礼的话 + +750 +00:46:28,396 --> 00:46:32,963 +不过 这次请你放心吧 + +751 +00:46:32,963 --> 00:46:36,515 +我们准备了方便指导的工作人员 + +752 +00:46:36,516 --> 00:46:37,023 +我们准备了 方便指导的工作人员 + +753 +00:46:37,530 --> 00:46:41,082 +好的 好的 + +754 +00:46:42,097 --> 00:46:44,127 +必须要 + +755 +00:46:44,127 --> 00:46:48,694 +让你体验一下我们这家医院 引以为豪的独特服务专案 + +756 +00:46:52,246 --> 00:46:52,753 +------------------ + +757 +00:46:53,261 --> 00:46:53,768 +--- + +758 +00:46:55,291 --> 00:46:55,798 +--------------- + +759 +00:47:05,440 --> 00:47:05,947 +------ + +760 +00:47:05,947 --> 00:47:07,469 +--- + +761 +00:47:07,470 --> 00:47:08,484 +------ + +762 +00:47:16,096 --> 00:47:16,603 +基本信息 + +763 +00:47:16,604 --> 00:47:17,111 +资本 偶上郎 + +764 +00:47:18,126 --> 00:47:18,633 +No text is visible in the provided image. + +765 +00:47:18,633 --> 00:47:19,140 +ITMMA PRO + +766 +00:47:19,141 --> 00:47:19,648 +医生 屈立豪 + +767 +00:47:20,663 --> 00:47:21,170 +HTML + +768 +00:47:23,708 --> 00:47:26,244 +叶山医生 + +769 +00:47:27,767 --> 00:47:31,826 +那个 有什么事吗 + +770 +00:47:31,827 --> 00:47:32,334 +大 大 大 + +771 +00:47:32,842 --> 00:47:35,378 +权藤医生有指示 + +772 +00:47:35,379 --> 00:47:35,886 +98室[巴花堂]永久地址489155.com权藤医生有指示 + +773 +00:47:35,886 --> 00:47:36,393 +98室[芭花堂]永久地址489155.com权藤医生有指示 + +774 +00:47:36,394 --> 00:47:36,901 +98室[巴花堂]永久地址489155.com权藤医生有指示 + +775 +00:47:36,901 --> 00:47:37,408 +98室[芭花室]永久地址489155.com + +776 +00:47:37,409 --> 00:47:38,423 +98室[巴花堂]永久地址489155.com + +777 +00:47:38,424 --> 00:47:45,020 +我们的工作需要你帮忙 + +778 +00:47:50,602 --> 00:47:54,661 +只要是我能做的我都愿意 + +779 +00:48:32,212 --> 00:48:32,719 +HTML + +780 +00:48:33,735 --> 00:48:43,376 +这家医院 为VIP患者提供了特别服务 + +781 +00:48:43,376 --> 00:48:44,391 +而且 我也希望你能参加呢 + +782 +00:48:44,391 --> 00:48:45,406 +而且我也希望你能参加呢 + +783 +00:48:45,406 --> 00:48:46,420 +而且 我也希望你能参加呢 + +784 +00:48:46,421 --> 00:48:47,435 +而且我也希望你能参加呢 + +785 +00:48:47,436 --> 00:48:48,450 +而且 我也希望你能参加呢 + +786 +00:48:49,973 --> 00:48:52,510 +特别服务 + +787 +00:48:58,092 --> 00:48:58,599 +------------ + +788 +00:48:58,599 --> 00:48:59,106 +--------------- + +789 +00:48:59,107 --> 00:48:59,614 +--- + +790 +00:48:59,614 --> 00:49:00,629 +--------------- + +791 +00:49:00,629 --> 00:49:01,136 +------------ + +792 +00:49:01,137 --> 00:49:01,644 +--- + +793 +00:49:01,644 --> 00:49:02,151 +--------------- + +794 +00:49:03,166 --> 00:49:03,673 +------------ + +795 +00:49:03,674 --> 00:49:04,181 +--------------- + +796 +00:49:04,181 --> 00:49:05,196 +------------ + +797 +00:49:05,196 --> 00:49:05,703 +------ + +798 +00:49:05,704 --> 00:49:06,211 +--------------- + +799 +00:49:11,286 --> 00:49:11,793 +A + +800 +00:50:30,447 --> 00:50:30,954 +--- + +801 +00:50:56,326 --> 00:50:56,833 +A + +802 +00:50:57,341 --> 00:50:57,848 +HTML + +803 +00:51:04,445 --> 00:51:06,982 +HTML:
+ +804 +00:51:09,520 --> 00:51:10,027 +background + +805 +00:51:23,728 --> 00:51:24,235 +--- + +806 +00:51:49,100 --> 00:51:49,607 +HTML + +807 +00:52:12,442 --> 00:52:14,979 +--- + +808 +00:52:15,487 --> 00:52:15,994 +------------------ + +809 +00:52:18,024 --> 00:52:18,531 +HTML + +810 +00:52:20,054 --> 00:52:20,561 +--- + +811 +00:52:21,069 --> 00:52:22,083 +HTML + +812 +00:52:27,666 --> 00:52:28,173 +--- + +813 +00:52:28,173 --> 00:52:28,680 +___ + +814 +00:52:33,755 --> 00:52:34,262 +2011-11-27 + +815 +00:52:41,874 --> 00:52:42,381 +0-1KE + +816 +00:52:44,919 --> 00:52:45,426 +500+ + +817 +00:52:45,426 --> 00:52:45,933 +SA + +818 +00:53:15,365 --> 00:53:20,947 +医生 这样真的可以吗 + +819 +00:53:34,648 --> 00:53:57,990 +--- + +820 +00:54:00,020 --> 00:54:00,527 +------ + +821 +00:54:01,543 --> 00:54:18,288 +--- + +822 +00:54:23,363 --> 00:54:34,526 +A + +823 +00:54:53,302 --> 00:54:53,809 +--- + +824 +00:55:07,003 --> 00:55:09,539 +可以吧 + +825 +00:55:17,659 --> 00:55:18,166 +床 + +826 +00:55:24,763 --> 00:55:29,330 +再用力舔这里 + +827 +00:55:30,345 --> 00:55:30,852 +超舒服 超舒服 + +828 +00:55:30,853 --> 00:55:32,374 +超舒服的 超舒服 + +829 +00:55:32,375 --> 00:55:32,882 +超舒服 超舒服 + +830 +00:55:32,882 --> 00:55:33,389 +超舒服的 超舒服 + +831 +00:55:33,390 --> 00:55:34,404 +超舒服 超舒服 + +832 +00:55:36,942 --> 00:55:40,493 +再用舌头多舔舔 + +833 +00:55:43,539 --> 00:55:46,583 +好舒服 + +834 +00:55:49,628 --> 00:55:53,687 +高潮了 我要高潮了 + +835 +00:56:02,314 --> 00:56:05,358 +继续舔 + +836 +00:56:06,373 --> 00:56:07,388 +------------------ + +837 +00:56:07,388 --> 00:56:09,418 +--- + +838 +00:56:09,418 --> 00:56:09,925 +------ + +839 +00:56:09,926 --> 00:56:15,000 +好舒服 太舒服了 + +840 +00:56:15,000 --> 00:56:17,537 +--- + +841 +00:56:17,537 --> 00:56:18,552 +--- --- + +842 +00:56:18,552 --> 00:56:25,656 +舌头好舒服 继续舔吧 + +843 +00:56:25,656 --> 00:56:28,193 +就是这里 + +844 +00:56:30,731 --> 00:56:31,745 +--- + +845 +00:56:32,253 --> 00:56:32,760 +--------------- + +846 +00:56:40,880 --> 00:56:45,446 +太舒服了 手指也插进小穴 + +847 +00:56:45,447 --> 00:56:45,954 +NK + +848 +00:56:45,954 --> 00:56:46,461 +NUV + +849 +00:56:46,969 --> 00:56:47,476 +N + +850 +00:56:53,058 --> 00:56:53,565 +lake + +851 +00:56:56,103 --> 00:57:01,684 +舒服 像这样抽插小穴 + +852 +00:57:04,222 --> 00:57:06,759 +高潮了 + +853 +00:57:09,296 --> 00:57:10,818 +--- + +854 +00:57:21,475 --> 00:57:24,519 +那里好舒服 + +855 +00:57:26,042 --> 00:57:28,579 +好爽 + +856 +00:57:42,280 --> 00:57:44,817 +再插深一点 + +857 +00:57:46,340 --> 00:57:51,921 +--- + +858 +00:57:51,922 --> 00:57:55,473 +高潮了 我高潮了 + +859 +00:57:55,474 --> 00:57:55,981 +------------ + +860 +00:57:56,488 --> 00:57:56,995 +--- + +861 +00:57:56,996 --> 00:57:57,503 +------ + +862 +00:57:57,503 --> 00:58:03,592 +好厉害 再动快一点 + +863 +00:58:03,593 --> 00:58:12,726 +--- + +864 +00:58:12,727 --> 00:58:17,801 +好厉害 不要停下来 + +865 +00:58:17,801 --> 00:58:18,816 +--- + +866 +00:58:19,323 --> 00:58:23,890 +高潮了 我高潮了 + +867 +00:58:23,890 --> 00:58:25,920 +--- + +868 +00:58:25,920 --> 00:58:26,427 +------------ + +869 +00:58:26,428 --> 00:58:26,935 +------ + +870 +00:58:26,935 --> 00:58:27,442 +--------------- + +871 +00:58:27,442 --> 00:58:28,964 +--- + +872 +00:58:29,980 --> 00:58:32,009 +------ + +873 +00:58:32,517 --> 00:58:33,024 +--------------- + +874 +00:58:38,606 --> 00:58:42,665 +好厉害 肉棒好硬 + +875 +00:58:43,681 --> 00:58:47,740 +肉棒跟我想的一样呢 + +876 +00:58:47,740 --> 00:58:56,874 +--- + +877 +00:58:56,874 --> 00:58:57,381 +6. + +878 +00:58:57,889 --> 00:58:58,396 +--- + +879 +00:58:58,904 --> 00:58:59,411 +------ + +880 +00:58:59,411 --> 00:58:59,918 +--- + +881 +00:58:59,919 --> 00:59:00,933 +------ + +882 +00:59:00,934 --> 00:59:01,441 +------------------ + +883 +00:59:01,441 --> 00:59:05,500 +--- + +884 +00:59:07,530 --> 00:59:11,082 +喜欢我的屁股吗 + +885 +00:59:11,083 --> 00:59:11,590 +--- + +886 +00:59:23,261 --> 00:59:23,768 +HTML + +887 +00:59:25,798 --> 00:59:26,305 +--- + +888 +00:59:26,813 --> 00:59:30,365 +好舒服 + +889 +00:59:31,888 --> 00:59:32,395 +background + +890 +00:59:52,693 --> 00:59:53,200 +V + +891 +00:59:53,200 --> 00:59:56,244 +继续 + +892 +00:59:57,767 --> 01:00:04,364 +继续搅动我的小穴吧 + +893 +01:00:05,379 --> 01:00:07,916 +好舒服 + +894 +01:00:19,587 --> 01:00:23,646 +再用力舔小穴 + +895 +01:00:34,303 --> 01:00:34,810 +Marketing + +896 +01:00:37,855 --> 01:00:41,407 +小穴美味吗 + +897 +01:01:11,854 --> 01:01:12,868 +--- + +898 +01:01:13,376 --> 01:01:13,883 +------------------ + +899 +01:01:14,899 --> 01:01:28,599 +--- + +900 +01:01:29,107 --> 01:01:32,151 +你躺下去吧 + +901 +01:01:45,853 --> 01:01:50,419 +怎么样 喜欢我的小穴吗 + +902 +01:01:50,927 --> 01:01:54,986 +很好 再用力舔一舔 + +903 +01:01:57,524 --> 01:01:58,031 +--- + +904 +01:02:03,613 --> 01:02:04,120 +HTML + +905 +01:02:07,673 --> 01:02:08,180 +------ + +906 +01:02:09,195 --> 01:02:13,761 +和我一起工作吧 + +907 +01:02:13,762 --> 01:02:16,806 +然后就这样子 + +908 +01:02:16,806 --> 01:02:20,866 +舔舒服的地方 + +909 +01:02:22,388 --> 01:02:22,895 +--- + +910 +01:02:23,911 --> 01:02:24,925 +------------------ + +911 +01:02:25,433 --> 01:02:32,537 +--- + +912 +01:02:34,060 --> 01:02:36,596 +好舒服 + +913 +01:02:37,104 --> 01:02:37,611 +--- + +914 +01:02:41,671 --> 01:02:45,730 +太厉害了 你舌头好会舔 + +915 +01:02:58,417 --> 01:02:58,924 +--- + +916 +01:02:58,924 --> 01:03:01,461 +好舒服 + +917 +01:03:01,969 --> 01:03:07,550 +--- + +918 +01:03:10,595 --> 01:03:11,102 +------ + +919 +01:03:11,103 --> 01:03:17,192 +--- + +920 +01:03:17,700 --> 01:03:21,251 +想插进我的小穴里吗 想 + +921 +01:03:21,252 --> 01:03:21,759 +想插进我的小穴里吗想 + +922 +01:03:21,759 --> 01:03:22,774 +想插进我的小穴里吗 想 + +923 +01:03:22,774 --> 01:03:23,281 +--- + +924 +01:03:32,923 --> 01:03:36,475 +太舒服了 + +925 +01:03:51,191 --> 01:03:51,698 +--- + +926 +01:04:10,981 --> 01:04:15,548 +乳头都变得这么硬了 + +927 +01:04:20,622 --> 01:04:21,129 +--- + +928 +01:04:23,160 --> 01:04:26,711 +肉棒好大呀 + +929 +01:04:39,905 --> 01:04:42,949 +硬梆梆的呢 + +930 +01:04:44,980 --> 01:04:47,516 +再让我好好看看 + +931 +01:04:48,024 --> 01:04:50,561 +不要再愣着了 + +932 +01:04:54,114 --> 01:04:56,650 +过来这边 + +933 +01:05:14,919 --> 01:05:18,978 +太厉害了 血管都清晰可见 + +934 +01:05:23,545 --> 01:05:24,052 +___ + +935 +01:05:24,560 --> 01:05:34,709 +--- + +936 +01:05:34,709 --> 01:05:38,261 +好棒 你舒服吗 + +937 +01:05:40,798 --> 01:05:41,305 +background + +938 +01:05:45,873 --> 01:05:50,439 +--- + +939 +01:05:50,440 --> 01:05:50,947 +--------------- + +940 +01:05:50,947 --> 01:05:51,962 +--- + +941 +01:05:52,977 --> 01:05:55,006 +太爽了 + +942 +01:06:00,589 --> 01:06:05,155 +--- + +943 +01:06:05,156 --> 01:06:08,200 +我们也太契合了 + +944 +01:06:12,767 --> 01:06:14,797 +--- + +945 +01:06:29,513 --> 01:06:30,020 +HTML + +946 +01:06:30,528 --> 01:06:40,676 +--- + +947 +01:06:40,677 --> 01:06:44,228 +肉棒比刚刚还要硬呢 + +948 +01:06:51,840 --> 01:06:55,392 +我想舔肉棒了 + +949 +01:06:56,407 --> 01:06:56,914 +。 + +950 +01:07:03,004 --> 01:07:03,511 +P4 + +951 +01:07:39,032 --> 01:07:47,659 +--- + +952 +01:07:47,659 --> 01:07:48,166 +--------------- + +953 +01:07:50,196 --> 01:07:53,748 +跟我想像中一样舒服 + +954 +01:07:53,748 --> 01:07:54,255 +--------------- + +955 +01:07:54,256 --> 01:08:00,345 +--- + +956 +01:08:01,867 --> 01:08:02,374 +------ + +957 +01:08:03,390 --> 01:08:06,434 +一直在搅动小穴 + +958 +01:08:07,449 --> 01:08:07,956 +HTML + +959 +01:08:11,001 --> 01:08:14,553 +怎么样 感觉舒服吗 好舒服 + +960 +01:08:17,598 --> 01:08:19,626 +好爽 + +961 +01:08:21,149 --> 01:08:25,209 +不行 这里 太舒服了 + +962 +01:08:43,478 --> 01:08:46,522 +高潮了 我高潮了 + +963 +01:08:52,612 --> 01:08:55,656 +这根肉棒太棒了 + +964 +01:08:57,179 --> 01:09:00,223 +摸我的奶子 + +965 +01:09:15,447 --> 01:09:15,953 +--- + +966 +01:09:16,461 --> 01:09:16,968 +--------------- + +967 +01:09:18,491 --> 01:09:20,520 +--- + +968 +01:09:22,042 --> 01:09:23,058 +------------------ + +969 +01:09:26,609 --> 01:09:31,683 +--- + +970 +01:09:32,700 --> 01:09:37,265 +我忍不住了 好厉害 + +971 +01:09:37,267 --> 01:09:37,773 +我忍不住了好厉害 + +972 +01:09:40,819 --> 01:09:44,370 +你也很兴奋呢 + +973 +01:09:45,893 --> 01:09:52,488 +再用力抽插小穴 用力顶那里 好舒服 + +974 +01:09:57,564 --> 01:09:58,071 +--- + +975 +01:09:59,087 --> 01:09:59,594 +------------------ + +976 +01:09:59,594 --> 01:10:00,101 +------ + +977 +01:10:00,101 --> 01:10:00,607 +--- + +978 +01:10:05,176 --> 01:10:05,683 +--- --- + +979 +01:10:05,683 --> 01:10:08,219 +插进小穴最里面了 + +980 +01:10:10,758 --> 01:10:11,264 +--- + +981 +01:10:11,265 --> 01:10:16,339 +高潮了 我高潮了 + +982 +01:10:17,355 --> 01:10:17,861 +------ + +983 +01:10:18,877 --> 01:10:19,384 +--- + +984 +01:10:20,399 --> 01:10:23,442 +------ + +985 +01:10:23,951 --> 01:10:24,458 +--- + +986 +01:10:26,488 --> 01:10:26,995 +------ + +987 +01:10:26,996 --> 01:10:29,025 +--- + +988 +01:10:29,026 --> 01:10:29,532 +------------------ + +989 +01:10:29,533 --> 01:10:31,054 +------ + +990 +01:10:31,055 --> 01:10:31,562 +--- + +991 +01:10:32,578 --> 01:10:33,085 +------ + +992 +01:10:36,637 --> 01:10:38,666 +--- + +993 +01:10:38,667 --> 01:10:41,710 +再用力抽插 + +994 +01:10:42,219 --> 01:10:42,726 +--- + +995 +01:10:44,756 --> 01:10:45,263 +------ + +996 +01:10:47,294 --> 01:10:49,830 +好舒服 + +997 +01:10:49,831 --> 01:10:50,338 +------------------ + +998 +01:10:51,861 --> 01:10:52,367 +------ + +999 +01:10:52,368 --> 01:10:52,875 +--- + +1000 +01:10:52,876 --> 01:10:53,383 +------ + +1001 +01:10:53,383 --> 01:10:53,889 +___ + +1002 +01:10:58,965 --> 01:11:05,053 +用力抽插小穴 很好 好舒服 + +1003 +01:11:05,562 --> 01:11:06,068 +SP + +1004 +01:11:08,099 --> 01:11:11,143 +要高潮了 高潮了 我高潮了 + +1005 +01:11:16,218 --> 01:11:19,769 +你的肉棒最棒了 + +1006 +01:11:20,785 --> 01:11:23,829 +我也会自己动起来的 + +1007 +01:11:24,844 --> 01:11:27,888 +舒服吗 好舒服 + +1008 +01:11:29,411 --> 01:11:31,947 +A + +1009 +01:11:31,949 --> 01:11:34,485 +抽插得停不下来了吗 + +1010 +01:11:42,605 --> 01:11:47,678 +这里好舒服 用力顶这里 + +1011 +01:11:48,694 --> 01:11:51,231 +高潮了 我高潮了 + +1012 +01:11:58,336 --> 01:11:58,843 +图 + +1013 +01:12:00,365 --> 01:12:03,410 +继续插进小穴 + +1014 +01:12:06,455 --> 01:12:09,499 +--- + +1015 +01:12:09,499 --> 01:12:14,066 +好厉害 肉棒插的好深 + +1016 +01:12:15,081 --> 01:12:18,633 +肉棒跟我想像中一样舒服 + +1017 +01:12:18,633 --> 01:12:20,155 +--- + +1018 +01:12:24,215 --> 01:12:28,781 +好厉害 你看 肉棒插进小穴了 + +1019 +01:12:29,290 --> 01:12:32,334 +好舒服 + +1020 +01:12:35,379 --> 01:12:42,482 +好厉害 肉棒一直在搅动我的小穴 + +1021 +01:12:42,483 --> 01:12:44,004 +--- + +1022 +01:12:44,005 --> 01:12:46,035 +好舒服 + +1023 +01:12:47,050 --> 01:12:47,557 +------------ + +1024 +01:12:47,558 --> 01:12:49,587 +--- + +1025 +01:12:50,095 --> 01:12:53,138 +好厉害 很好 + +1026 +01:12:55,169 --> 01:12:57,705 +这里好舒服 + +1027 +01:12:58,214 --> 01:13:00,750 +高潮了 + +1028 +01:13:02,781 --> 01:13:03,288 +好厉害 那里 用力顶 + +1029 +01:13:03,288 --> 01:13:03,794 +好厉害那里用力顶 + +1030 +01:13:03,796 --> 01:13:04,303 +好厉害 那里 用力顶 + +1031 +01:13:04,303 --> 01:13:05,317 +好厉害那里用力顶 + +1032 +01:13:05,318 --> 01:13:05,825 +好厉害 那里 用力顶 + +1033 +01:13:05,825 --> 01:13:06,839 +好厉害那里用力顶 + +1034 +01:13:06,840 --> 01:13:09,884 +高潮了 我高潮了 + +1035 +01:13:13,945 --> 01:13:20,540 +再插深一点 插深一点 好舒服 好爽 + +1036 +01:13:32,212 --> 01:13:34,749 +肉棒插的好深 + +1037 +01:13:47,436 --> 01:13:50,480 +总是你在动不公平 + +1038 +01:14:02,152 --> 01:14:08,240 +看着插进小穴的样子 要插进小穴了 + +1039 +01:14:14,838 --> 01:14:15,344 +------ + +1040 +01:14:16,360 --> 01:14:19,404 +肉棒一直在抽插小穴 + +1041 +01:14:20,419 --> 01:14:20,925 +--- + +1042 +01:14:21,942 --> 01:14:24,478 +好舒服 + +1043 +01:14:28,031 --> 01:14:30,059 +好厉害 + +1044 +01:14:39,702 --> 01:14:42,239 +不行 高潮了 我高潮了 + +1045 +01:14:46,806 --> 01:14:49,851 +这样搅动小穴好棒 + +1046 +01:14:57,463 --> 01:15:03,552 +这样从下面往上顶要高潮了 好爽 真是太爽了 + +1047 +01:15:03,552 --> 01:15:06,596 +高潮了 我高潮了 + +1048 +01:15:12,686 --> 01:15:15,730 +这样动的话 你会累的吧 让我来动吧 + +1049 +01:15:15,731 --> 01:15:16,237 +这样动的话你会累的吧 让我来动吧 + +1050 +01:15:16,238 --> 01:15:17,759 +这样动的话 你会累的吧 让我来动吧 + +1051 +01:15:17,760 --> 01:15:18,267 +这样动的话你会累的吧让我来动吧 + +1052 +01:15:20,805 --> 01:15:25,879 +好舒服 我喜欢顶到 小穴最里面 + +1053 +01:15:27,402 --> 01:15:29,938 +超舒服的 + +1054 +01:15:30,447 --> 01:15:34,505 +这样就要高潮了 要高潮了 我要高潮了 + +1055 +01:15:35,014 --> 01:15:37,549 +高潮了 + +1056 +01:15:40,595 --> 01:15:46,683 +我的小穴怎么样 舒服吗 + +1057 +01:15:59,878 --> 01:16:00,384 +--- + +1058 +01:16:00,893 --> 01:16:01,400 +HTML + +1059 +01:16:17,131 --> 01:16:17,638 +--- + +1060 +01:16:18,654 --> 01:16:19,161 +HTML + +1061 +01:16:19,668 --> 01:16:21,697 +--- + +1062 +01:16:23,728 --> 01:16:26,772 +肉棒在小穴里面好硬 + +1063 +01:16:27,280 --> 01:16:27,786 +--------------- + +1064 +01:16:34,892 --> 01:16:38,951 +高潮了 我高潮了 + +1065 +01:16:39,966 --> 01:16:45,040 +--- + +1066 +01:16:48,593 --> 01:16:53,666 +好厉害 肉棒在里面动呢 好舒服 + +1067 +01:16:54,175 --> 01:16:57,219 +我的唾液 + +1068 +01:16:58,742 --> 01:17:04,322 +怎么样 美味吗 好美味 + +1069 +01:17:05,338 --> 01:17:07,875 +好舒服 + +1070 +01:17:10,920 --> 01:17:11,427 +--- + +1071 +01:17:19,547 --> 01:17:22,590 +你还想跟我做爱吧 + +1072 +01:17:25,636 --> 01:17:28,173 +好厉害 + +1073 +01:17:33,755 --> 01:17:38,321 +高潮了 我高潮了 + +1074 +01:17:41,874 --> 01:17:44,918 +--- + +1075 +01:17:53,545 --> 01:17:56,081 +高潮了 + +1076 +01:17:58,112 --> 01:17:58,619 +___ + +1077 +01:18:03,187 --> 01:18:05,723 +你还能继续吗 + +1078 +01:18:08,261 --> 01:18:10,798 +来这边 + +1079 +01:18:21,962 --> 01:18:26,529 +太厉害了 肉棒一直都好硬 + +1080 +01:18:32,111 --> 01:18:35,663 +这样呢 好舒服 + +1081 +01:18:37,693 --> 01:18:39,722 +好厉害 + +1082 +01:18:39,723 --> 01:18:43,273 +硬梆梆的肉棒摩擦的感觉 好舒服啊 + +1083 +01:18:43,275 --> 01:18:43,781 +A + +1084 +01:18:43,782 --> 01:18:44,289 +SPH + +1085 +01:18:44,290 --> 01:18:44,797 +SP10+ + +1086 +01:18:44,797 --> 01:18:45,303 +SP10-1型 + +1087 +01:18:45,304 --> 01:18:45,811 +SPH-1专用 + +1088 +01:18:45,812 --> 01:18:46,318 +SPIO-1 FEB + +1089 +01:18:46,319 --> 01:18:49,364 +不过你想插进小穴吧 + +1090 +01:18:53,931 --> 01:18:56,468 +用力插进小穴吧 + +1091 +01:18:57,483 --> 01:18:57,990 +------ + +1092 +01:19:00,528 --> 01:19:03,065 +好厉害 + +1093 +01:19:04,587 --> 01:19:05,094 +你的肉棒好厉害 太舒服了 + +1094 +01:19:05,095 --> 01:19:05,602 +你的肉棒好厉害太舒服了 + +1095 +01:19:05,602 --> 01:19:10,169 +你的肉棒好厉害 太舒服了 + +1096 +01:19:13,214 --> 01:19:16,258 +真的好舒服 + +1097 +01:19:20,318 --> 01:19:21,839 +--- + +1098 +01:19:21,840 --> 01:19:24,884 +那里 好舒服啊 + +1099 +01:19:25,392 --> 01:19:25,898 +------ + +1100 +01:19:26,407 --> 01:19:29,451 +--- + +1101 +01:19:29,452 --> 01:19:32,495 +顶到舒服的地方了 + +1102 +01:19:33,004 --> 01:19:33,510 +--- + +1103 +01:19:34,019 --> 01:19:37,062 +高潮了 我高潮了 + +1104 +01:19:37,064 --> 01:19:40,107 +--- + +1105 +01:19:42,645 --> 01:19:43,152 +NJ + +1106 +01:19:45,183 --> 01:19:45,690 +DAY + +1107 +01:19:55,839 --> 01:19:57,867 +好厉害 + +1108 +01:20:05,988 --> 01:20:09,031 +舒服极了 不行 那里要高潮了 + +1109 +01:20:09,032 --> 01:20:11,568 +我要高潮了 + +1110 +01:20:12,585 --> 01:20:15,629 +别停 别停下 + +1111 +01:20:27,808 --> 01:20:30,344 +肉棒太爽了 + +1112 +01:20:31,867 --> 01:20:32,374 +------ + +1113 +01:20:35,927 --> 01:20:36,433 +___ + +1114 +01:20:48,613 --> 01:20:51,656 +舔遍我的全身上下 + +1115 +01:20:51,658 --> 01:20:52,165 +--- + +1116 +01:20:57,240 --> 01:20:59,776 +太舒服了 + +1117 +01:20:59,777 --> 01:21:02,312 +--- + +1118 +01:21:02,314 --> 01:21:04,851 +高潮了 + +1119 +01:21:04,851 --> 01:21:05,357 +--------------- + +1120 +01:21:05,359 --> 01:21:07,388 +--- + +1121 +01:21:07,388 --> 01:21:09,924 +怎么样 你舒服吗 + +1122 +01:21:16,015 --> 01:21:19,058 +不用忍着 + +1123 +01:21:22,104 --> 01:21:24,641 +好舒服 + +1124 +01:21:24,641 --> 01:21:25,147 +--------------- + +1125 +01:21:25,149 --> 01:21:29,208 +--- + +1126 +01:21:29,208 --> 01:21:31,745 +好舒服 + +1127 +01:21:33,775 --> 01:21:36,820 +射在小穴里面吧 全都射进来 + +1128 +01:21:36,820 --> 01:21:38,848 +射了 + +1129 +01:21:43,924 --> 01:21:44,431 +__________________ + +1130 +01:21:45,447 --> 01:21:47,982 +--- + +1131 +01:21:50,014 --> 01:21:52,043 +好舒服 + +1132 +01:21:53,058 --> 01:21:53,565 +------------------ + +1133 +01:21:58,133 --> 01:21:58,639 +--- + +1134 +01:22:12,848 --> 01:22:14,878 +医生 + +1135 +01:22:16,401 --> 01:22:16,907 +10-14岁 + +1136 +01:22:16,908 --> 01:22:17,415 +10-1期B + +1137 +01:22:17,415 --> 01:22:17,922 +NO.1专用 + +1138 +01:22:17,923 --> 01:22:18,429 +PHD-手術 + +1139 +01:22:19,445 --> 01:22:19,951 +P10一手机 + +1140 +01:22:19,953 --> 01:22:20,460 +Pトローネ電話 + +1141 +01:22:20,460 --> 01:22:20,967 +PHD一专用 + +1142 +01:22:20,968 --> 01:22:21,474 +PH0一1瓶盖 + +1143 +01:22:21,475 --> 01:22:21,982 +PHO一号馆 + +1144 +01:22:21,982 --> 01:22:22,489 +SPHO一专用 + +1145 +01:22:22,490 --> 01:22:22,996 +SPHO-1 + +1146 +01:22:22,997 --> 01:22:23,504 +SPNO-1 + +1147 +01:22:23,505 --> 01:22:24,012 +SPH + +1148 +01:22:24,012 --> 01:22:24,518 +SP + +1149 +01:22:43,295 --> 01:22:46,339 +比我想像中还要舒服 + +1150 +01:22:47,862 --> 01:22:52,428 +那么后面你要帮忙哦 + +1151 +01:22:52,429 --> 01:22:55,981 +好的 我很荣幸 + +1152 +01:23:18,309 --> 01:23:20,338 +果林前辈 + +1153 +01:23:20,338 --> 01:23:22,875 +抱歉 明明不是你上夜班 + +1154 +01:23:22,876 --> 01:23:25,411 +感谢你的陪同 + +1155 +01:23:27,442 --> 01:23:29,978 +不过话说回来 权藤医生说的 + +1156 +01:23:29,980 --> 01:23:32,516 +业务到底是什么 + +1157 +01:23:34,039 --> 01:23:34,545 +M + +1158 +01:23:51,292 --> 01:23:55,351 +辛苦了 还请多多指教 + +1159 +01:24:03,471 --> 01:24:03,977 +A + +1160 +01:24:03,978 --> 01:24:08,036 +你在干什么 松井小姐 + +1161 +01:24:08,038 --> 01:24:10,575 +请放开我 + +1162 +01:24:11,590 --> 01:24:16,156 +这是新业务的培训 松井小姐 + +1163 +01:24:18,187 --> 01:24:20,215 +北冈小姐是吧 + +1164 +01:24:20,724 --> 01:24:23,768 +来吧 快动手 + +1165 +01:24:27,321 --> 01:24:27,827 +--- + +1166 +01:24:31,380 --> 01:24:33,410 +果林前辈 + +1167 +01:24:33,410 --> 01:24:39,499 +松井小姐 请你闭嘴 好好学习 + +1168 +01:24:42,037 --> 01:24:42,544 +--- + +1169 +01:24:47,618 --> 01:24:52,184 +真是吵死了 北冈小姐她 + +1170 +01:24:52,693 --> 01:24:57,767 +已经在这里3年了 早就习惯了 + +1171 +01:25:05,379 --> 01:25:07,915 +松井小姐 请不要看 + +1172 +01:25:11,976 --> 01:25:15,019 +为什么 这是为什么 + +1173 +01:25:18,065 --> 01:25:22,630 +是想帮助重要的前辈吗 + +1174 +01:25:24,154 --> 01:25:26,691 +很遗憾 + +1175 +01:25:26,691 --> 01:25:31,258 +接下来 你也会有同样的遭遇 + +1176 +01:25:31,258 --> 01:25:33,288 +不要 + +1177 +01:25:41,915 --> 01:25:49,526 +--- + +1178 +01:25:51,556 --> 01:25:54,093 +住手 不要 + +1179 +01:26:00,690 --> 01:26:01,196 +The image is blurry and does not contain any discernible text. + +1180 +01:26:05,257 --> 01:26:11,346 +要怎么说才好呢 你也会遇到一样的事的 + +1181 +01:26:19,973 --> 01:26:20,480 +A + +1182 +01:26:20,988 --> 01:26:24,539 +不要 不可以 + +1183 +01:26:31,137 --> 01:26:34,181 +看看北冈小姐吧 + +1184 +01:26:34,181 --> 01:26:39,763 +明明你都任我摆布了 + +1185 +01:26:45,345 --> 01:26:54,478 +身体不要紧绷着 + +1186 +01:26:54,479 --> 01:26:54,986 +不要 怎么样呀 + +1187 +01:26:54,986 --> 01:26:55,492 +不要怎么样呀 + +1188 +01:26:55,494 --> 01:26:59,045 +不要 怎么样呀 + +1189 +01:27:10,717 --> 01:27:11,223 +--- + +1190 +01:27:11,225 --> 01:27:16,299 +松井小姐 你真可爱呢 + +1191 +01:27:18,836 --> 01:27:24,924 +不要 + +1192 +01:27:38,119 --> 01:27:38,625 +--- + +1193 +01:27:41,671 --> 01:27:44,208 +你很敏感呢 + +1194 +01:27:46,238 --> 01:27:51,312 +不要 住手 别这样 + +1195 +01:27:51,313 --> 01:27:51,820 +I + +1196 +01:27:53,342 --> 01:27:55,879 +安静点 + +1197 +01:27:58,417 --> 01:28:02,476 +那个地方被摸到了 + +1198 +01:28:04,506 --> 01:28:08,058 +看你一副好舒服的样子呢 + +1199 +01:28:09,581 --> 01:28:11,610 +别看我了 + +1200 +01:28:21,759 --> 01:28:24,803 +--- + +1201 +01:28:24,804 --> 01:28:27,848 +不行 快住手 + +1202 +01:28:27,848 --> 01:28:28,355 +--- + +1203 +01:28:28,356 --> 01:28:28,862 +HTML + +1204 +01:28:30,893 --> 01:28:31,400 +background + +1205 +01:28:31,401 --> 01:28:34,445 +已经湿了 + +1206 +01:28:38,505 --> 01:28:39,012 +--- + +1207 +01:28:45,609 --> 01:28:46,116 +b.o + +1208 +01:29:10,474 --> 01:29:12,503 +--- + +1209 +01:29:12,503 --> 01:29:18,085 +松井小姐的阴蒂和里面 + +1210 +01:29:20,622 --> 01:29:23,666 +喜欢刺激哪里 + +1211 +01:29:26,712 --> 01:29:30,263 +快回 + +1212 +01:29:30,771 --> 01:29:36,352 +--- + +1213 +01:29:39,398 --> 01:29:44,472 +真啰嗦 下面都这么湿了 + +1214 +01:29:44,472 --> 01:29:48,023 +还叫什么呢 + +1215 +01:29:48,024 --> 01:29:51,068 +--- + +1216 +01:29:51,069 --> 01:29:51,576 +------ + +1217 +01:29:51,576 --> 01:29:52,083 +--------------- + +1218 +01:30:10,859 --> 01:30:11,366 +--- + +1219 +01:30:15,934 --> 01:30:16,441 +___ + +1220 +01:30:17,963 --> 01:30:18,469 +--- + +1221 +01:30:22,530 --> 01:30:26,081 +放松一点 哪里舒服 + +1222 +01:30:26,083 --> 01:30:26,589 +放松一点哪里舒服 + +1223 +01:30:26,590 --> 01:30:27,097 +放松一点 哪里舒服 + +1224 +01:30:34,709 --> 01:30:38,260 +--- + +1225 +01:30:38,261 --> 01:30:41,304 +你感觉怎么样 + +1226 +01:30:43,843 --> 01:30:46,887 +好漂亮 好可爱 + +1227 +01:30:48,917 --> 01:30:57,544 +--- + +1228 +01:31:01,096 --> 01:31:03,125 +真啰嗦 + +1229 +01:31:03,126 --> 01:31:07,184 +--- + +1230 +01:31:11,752 --> 01:31:14,797 +你不安静的话 是不会让你舒服的 + +1231 +01:31:17,842 --> 01:31:20,885 +想继续做下去吗 + +1232 +01:31:21,394 --> 01:31:26,468 +--- + +1233 +01:31:28,498 --> 01:31:29,004 +------------ + +1234 +01:31:30,020 --> 01:31:30,527 +--------------- + +1235 +01:31:30,528 --> 01:31:31,035 +------------ + +1236 +01:31:31,543 --> 01:31:32,049 +------ + +1237 +01:31:35,095 --> 01:31:36,616 +--- + +1238 +01:31:37,632 --> 01:31:41,183 +我会负起责任来教育你的 + +1239 +01:31:41,184 --> 01:31:43,213 +没事的 + +1240 +01:31:47,273 --> 01:31:54,884 +--- + +1241 +01:32:29,899 --> 01:32:33,450 +住手 不要 + +1242 +01:32:34,973 --> 01:32:35,480 +--------------- + +1243 +01:32:36,495 --> 01:32:37,001 +background + +1244 +01:32:39,540 --> 01:32:44,614 +你看北冈小姐都摆出 那么羞耻的姿势了 + +1245 +01:32:47,659 --> 01:32:48,165 +------------------ + +1246 +01:32:48,166 --> 01:32:51,210 +因为你们会变成同样的姿势嘛 + +1247 +01:32:58,315 --> 01:33:00,344 +保持这个姿势 + +1248 +01:33:02,882 --> 01:33:05,419 +吵死了 + +1249 +01:33:09,986 --> 01:33:11,508 +--- + +1250 +01:33:11,509 --> 01:33:12,016 +------------------ + +1251 +01:33:20,135 --> 01:33:20,642 +A + +1252 +01:33:35,359 --> 01:33:37,894 +真的可以吗 + +1253 +01:33:41,448 --> 01:33:45,000 +好好教育她一番吧 + +1254 +01:33:46,015 --> 01:33:47,029 +--- + +1255 +01:33:47,030 --> 01:33:53,625 +松井小姐下流的样子 就让我拍下来啦 + +1256 +01:33:56,671 --> 01:33:59,208 +这只是记录而已 + +1257 +01:34:08,850 --> 01:34:12,401 +不放松点的话 + +1258 +01:34:22,043 --> 01:34:25,594 +小穴好紧啊 要插进小穴了 + +1259 +01:34:25,595 --> 01:34:30,669 +我这边也很紧呢 松井小姐 + +1260 +01:34:33,207 --> 01:34:33,714 +HTML + +1261 +01:34:35,744 --> 01:34:36,250 +------ + +1262 +01:34:36,252 --> 01:34:36,759 +--- + +1263 +01:34:37,267 --> 01:34:40,817 +下面这么湿 很舒服吗 + +1264 +01:34:42,848 --> 01:34:45,384 +舒服吗 + +1265 +01:34:45,386 --> 01:34:45,893 +--------------- + +1266 +01:34:45,893 --> 01:34:48,429 +快一点就好了 + +1267 +01:34:48,430 --> 01:34:48,937 +--- + +1268 +01:34:48,938 --> 01:34:49,445 +------ + +1269 +01:34:49,445 --> 01:34:49,951 +--- + +1270 +01:34:49,953 --> 01:34:50,460 +--------------- + +1271 +01:34:50,460 --> 01:34:50,967 +--- + +1272 +01:34:50,968 --> 01:34:51,474 +------ + +1273 +01:34:51,475 --> 01:34:51,982 +--- + +1274 +01:34:52,997 --> 01:34:53,504 +HTML + +1275 +01:34:53,505 --> 01:34:54,012 +------ + +1276 +01:34:54,012 --> 01:34:56,549 +--- + +1277 +01:34:56,549 --> 01:34:57,056 +------ + +1278 +01:34:57,564 --> 01:35:04,161 +--- + +1279 +01:35:04,161 --> 01:35:04,668 +HTML + +1280 +01:35:06,698 --> 01:35:08,727 +--- + +1281 +01:35:09,235 --> 01:35:09,741 +Markdown: + +1282 +01:35:10,758 --> 01:35:11,264 +------------------ + +1283 +01:35:11,773 --> 01:35:12,280 +--------------- + +1284 +01:35:12,280 --> 01:35:12,786 +--- + +1285 +01:35:12,788 --> 01:35:13,294 +------------------ + +1286 +01:35:13,295 --> 01:35:14,308 +--- + +1287 +01:35:14,310 --> 01:35:14,817 +HTML + +1288 +01:35:16,340 --> 01:35:21,414 +--- + +1289 +01:35:22,429 --> 01:35:22,936 +------ + +1290 +01:35:23,951 --> 01:35:32,576 +--- + +1291 +01:35:35,115 --> 01:35:37,652 +好淫荡的味道啊 + +1292 +01:35:40,697 --> 01:35:41,710 +--- + +1293 +01:35:41,712 --> 01:35:42,219 +------ + +1294 +01:35:42,219 --> 01:35:42,726 +--- + +1295 +01:35:42,727 --> 01:35:43,741 +------ + +1296 +01:35:43,742 --> 01:35:45,263 +--- + +1297 +01:35:45,264 --> 01:35:46,277 +--------------- + +1298 +01:35:46,279 --> 01:35:46,786 +------ + +1299 +01:35:46,786 --> 01:35:48,308 +--- + +1300 +01:35:48,309 --> 01:35:48,816 +------------------ + +1301 +01:35:48,816 --> 01:35:49,830 +--- + +1302 +01:35:50,338 --> 01:35:50,844 +------ + +1303 +01:35:50,846 --> 01:35:52,875 +--- + +1304 +01:35:52,876 --> 01:35:53,383 +------ + +1305 +01:35:53,383 --> 01:35:55,411 +怎么样 + +1306 +01:35:55,413 --> 01:35:55,919 +------ + +1307 +01:35:55,920 --> 01:35:57,949 +--- + +1308 +01:36:15,710 --> 01:36:16,217 +--------------- + +1309 +01:36:16,725 --> 01:36:17,232 +------------ + +1310 +01:36:17,740 --> 01:36:19,262 +------ + +1311 +01:36:20,785 --> 01:36:22,813 +--- + +1312 +01:36:24,337 --> 01:36:24,844 +------ + +1313 +01:36:25,859 --> 01:36:27,888 +--- + +1314 +01:36:27,889 --> 01:36:28,396 +------ + +1315 +01:36:35,501 --> 01:36:38,544 +手指这么简单就进去了 + +1316 +01:36:39,053 --> 01:36:43,111 +--- + +1317 +01:36:43,620 --> 01:36:44,126 +------------ + +1318 +01:36:44,127 --> 01:36:45,142 +--------------- + +1319 +01:36:45,142 --> 01:36:46,156 +------------------ + +1320 +01:36:46,157 --> 01:36:46,664 +------------ + +1321 +01:36:46,664 --> 01:36:49,709 +插进小穴这么深 + +1322 +01:36:49,709 --> 01:36:50,215 +--- + +1323 +01:36:50,217 --> 01:36:51,231 +------------------ + +1324 +01:36:51,231 --> 01:36:51,737 +--- + +1325 +01:36:52,754 --> 01:36:53,260 +------------------ + +1326 +01:36:53,769 --> 01:36:55,798 +------ + +1327 +01:36:57,828 --> 01:36:58,843 +--- + +1328 +01:36:58,843 --> 01:36:59,349 +------------ + +1329 +01:36:59,350 --> 01:36:59,857 +------ + +1330 +01:36:59,858 --> 01:37:00,365 +--- + +1331 +01:37:00,365 --> 01:37:00,871 +------------ + +1332 +01:37:00,873 --> 01:37:01,379 +--------------- + +1333 +01:37:01,888 --> 01:37:20,155 +--- + +1334 +01:37:20,156 --> 01:37:23,706 +给我看看你舒服的表情 + +1335 +01:37:23,708 --> 01:37:28,781 +--- + +1336 +01:37:28,782 --> 01:37:29,289 +Background + +1337 +01:37:44,005 --> 01:37:44,512 +--- + +1338 +01:37:45,528 --> 01:37:46,035 +------ + +1339 +01:37:46,543 --> 01:37:55,169 +--- + +1340 +01:37:59,229 --> 01:38:01,764 +果然还是这边吗 + +1341 +01:38:03,288 --> 01:38:05,825 +快把手拿开 + +1342 +01:38:08,363 --> 01:38:08,870 +--------------- + +1343 +01:38:09,885 --> 01:38:10,392 +--- + +1344 +01:38:11,407 --> 01:38:11,914 +HTML + +1345 +01:38:12,422 --> 01:38:15,973 +--- + +1346 +01:38:16,989 --> 01:38:19,526 +98堂[色花堂] 永久地址 489155.com + +1347 +01:38:19,526 --> 01:38:20,032 +98堂[色花堂]永久地址489155.com + +1348 +01:38:20,034 --> 01:38:29,166 +--- + +1349 +01:38:30,183 --> 01:38:33,733 +再把腿张开一点 + +1350 +01:38:34,750 --> 01:38:35,257 +--- + +1351 +01:38:37,287 --> 01:38:40,330 +给我看看屁股 + +1352 +01:39:09,256 --> 01:39:16,866 +我会把松井小姐重要的地方里流出来的汁液全部吸干的 + +1353 +01:39:19,912 --> 01:39:20,419 +------ + +1354 +01:39:23,464 --> 01:39:26,000 +--- + +1355 +01:39:27,016 --> 01:39:27,522 +--------------- + +1356 +01:39:29,046 --> 01:39:31,583 +--- + +1357 +01:39:32,091 --> 01:39:32,598 +------ + +1358 +01:39:36,150 --> 01:39:36,656 +--- + +1359 +01:39:36,658 --> 01:39:38,687 +不要 + +1360 +01:39:41,732 --> 01:39:42,239 +--- + +1361 +01:39:45,284 --> 01:39:45,790 +--------------- + +1362 +01:39:45,792 --> 01:39:46,299 +------------ + +1363 +01:39:47,314 --> 01:39:56,446 +--- + +1364 +01:39:56,955 --> 01:39:57,462 +HTML + +1365 +01:39:59,493 --> 01:40:00,000 +--------------- + +1366 +01:40:01,522 --> 01:40:02,029 +--- + +1367 +01:40:07,104 --> 01:40:07,611 +------ + +1368 +01:40:07,612 --> 01:40:08,625 +--------------- + +1369 +01:40:09,134 --> 01:40:10,147 +--- + +1370 +01:40:10,149 --> 01:40:10,656 +--------------- + +1371 +01:40:10,656 --> 01:40:11,163 +--- + +1372 +01:40:11,671 --> 01:40:12,178 +------ + +1373 +01:40:13,701 --> 01:40:16,745 +--- + +1374 +01:40:18,268 --> 01:40:21,312 +差不多想要肉棒了吧 + +1375 +01:40:21,313 --> 01:40:22,326 +--- + +1376 +01:40:24,865 --> 01:40:27,401 +你自己插进小穴吧 + +1377 +01:40:32,984 --> 01:40:33,491 +床 + +1378 +01:40:38,058 --> 01:40:39,072 +1 + +1379 +01:40:39,073 --> 01:40:39,580 +床 + +1380 +01:40:39,581 --> 01:40:40,088 +210 康林 + +1381 +01:40:40,088 --> 01:40:40,594 +北岳 顾月 + +1382 +01:40:40,595 --> 01:40:41,102 +实习 黑林 + +1383 +01:40:41,103 --> 01:40:41,610 +医补 + +1384 +01:40:44,655 --> 01:40:50,236 +很好 就是这样 自己动起来 + +1385 +01:40:51,252 --> 01:40:54,295 +你看腰动起来了 + +1386 +01:40:54,296 --> 01:40:54,803 +D:0 + +1387 +01:40:54,804 --> 01:40:55,311 +d:q + +1388 +01:40:57,848 --> 01:40:58,355 +DQ + +1389 +01:41:00,386 --> 01:41:00,893 +DQDQ + +1390 +01:41:02,415 --> 01:41:02,922 +DSQ + +1391 +01:41:09,520 --> 01:41:10,027 +--- + +1392 +01:41:29,310 --> 01:41:31,847 +不要看 + +1393 +01:41:39,459 --> 01:41:41,995 +你高潮了吗 + +1394 +01:42:00,771 --> 01:42:01,277 +--------------- + +1395 +01:42:02,801 --> 01:42:07,875 +--- + +1396 +01:42:08,383 --> 01:42:10,919 +你给我坐起来 + +1397 +01:42:14,472 --> 01:42:19,545 +松井小姐 这边也 要插进小穴吧 + +1398 +01:42:19,547 --> 01:42:20,053 +4 + +1399 +01:42:20,562 --> 01:42:21,068 +4.0 + +1400 +01:42:24,114 --> 01:42:28,173 +怎么了 不愿意吗 + +1401 +01:42:28,681 --> 01:42:29,187 +--- + +1402 +01:42:29,696 --> 01:42:36,799 +偶尔和你这样 表里不一的女人做也不错 + +1403 +01:42:38,322 --> 01:42:41,874 +我要强行插进小穴了 + +1404 +01:42:44,411 --> 01:42:46,441 +--- + +1405 +01:42:46,949 --> 01:42:47,455 +------------ + +1406 +01:42:47,456 --> 01:42:49,992 +--- + +1407 +01:42:50,501 --> 01:42:53,036 +小穴好紧 + +1408 +01:42:54,560 --> 01:42:55,067 +--- + +1409 +01:42:57,097 --> 01:43:00,648 +要动起来了 + +1410 +01:43:01,664 --> 01:43:05,723 +--- + +1411 +01:43:05,724 --> 01:43:07,753 +松井小姐 + +1412 +01:43:07,754 --> 01:43:13,335 +意外的是被强迫动起来 你感觉还不错呢 + +1413 +01:43:20,440 --> 01:43:20,946 +------ + +1414 +01:43:20,947 --> 01:43:21,454 +--- --- + +1415 +01:43:21,455 --> 01:43:21,962 +------ + +1416 +01:43:21,962 --> 01:43:22,469 +--------------- + +1417 +01:43:22,470 --> 01:43:24,499 +--- + +1418 +01:43:24,499 --> 01:43:25,005 +------------ + +1419 +01:43:25,007 --> 01:43:26,529 +--- + +1420 +01:43:26,529 --> 01:43:27,544 +------ + +1421 +01:43:27,544 --> 01:43:28,050 +------------------ + +1422 +01:43:28,051 --> 01:43:28,558 +--- + +1423 +01:43:28,559 --> 01:43:29,066 +------ + +1424 +01:43:29,066 --> 01:43:30,588 +--- + +1425 +01:43:30,589 --> 01:43:34,139 +你的情况非常好呢 + +1426 +01:43:34,141 --> 01:43:34,647 +--- + +1427 +01:43:34,648 --> 01:43:35,155 +------------------ + +1428 +01:43:35,156 --> 01:43:36,169 +--- + +1429 +01:43:36,171 --> 01:43:36,678 +------ + +1430 +01:43:36,678 --> 01:43:37,184 +------------------ + +1431 +01:43:37,185 --> 01:43:39,722 +--- + +1432 +01:43:39,723 --> 01:43:40,230 +--------------- + +1433 +01:43:40,737 --> 01:43:46,826 +--- + +1434 +01:44:01,543 --> 01:44:03,065 +--------------- + +1435 +01:44:03,065 --> 01:44:03,571 +------------------ + +1436 +01:44:03,572 --> 01:44:04,587 +------ + +1437 +01:44:05,095 --> 01:44:06,108 +--------------- + +1438 +01:44:07,632 --> 01:44:08,138 +--- + +1439 +01:44:08,139 --> 01:44:10,675 +------------------ + +1440 +01:44:12,199 --> 01:44:12,705 +--- + +1441 +01:44:12,706 --> 01:44:13,213 +------------------ + +1442 +01:44:17,273 --> 01:44:18,288 +------ + +1443 +01:44:18,288 --> 01:44:18,794 +--- + +1444 +01:44:19,811 --> 01:44:20,317 +------ + +1445 +01:44:23,870 --> 01:44:34,018 +--- + +1446 +01:44:34,019 --> 01:44:34,526 +------------------ + +1447 +01:44:34,526 --> 01:44:37,062 +--- + +1448 +01:44:37,571 --> 01:44:38,077 +------ + +1449 +01:44:38,078 --> 01:44:39,093 +--- + +1450 +01:44:39,093 --> 01:44:39,599 +------------------ + +1451 +01:44:39,601 --> 01:44:40,107 +------------ + +1452 +01:44:40,108 --> 01:44:44,166 +我再插深一点吧 + +1453 +01:44:45,690 --> 01:44:46,196 +--------------- + +1454 +01:44:46,705 --> 01:44:47,212 +--- + +1455 +01:44:47,212 --> 01:44:47,719 +------------------ + +1456 +01:44:47,720 --> 01:44:48,227 +--------------- + +1457 +01:44:48,735 --> 01:44:53,300 +--- + +1458 +01:44:53,302 --> 01:44:53,808 +------------------ + +1459 +01:44:53,809 --> 01:44:54,316 +------ + +1460 +01:44:54,317 --> 01:44:54,824 +------------------ + +1461 +01:44:54,824 --> 01:44:55,330 +------ + +1462 +01:44:55,332 --> 01:44:55,839 +--- + +1463 +01:44:55,839 --> 01:44:56,346 +--------------- + +1464 +01:44:56,346 --> 01:44:56,852 +------ + +1465 +01:44:56,854 --> 01:44:58,375 +--------------- + +1466 +01:44:58,884 --> 01:44:59,897 +------ + +1467 +01:44:59,899 --> 01:45:00,406 +--------------- + +1468 +01:45:00,406 --> 01:45:00,913 +--- + +1469 +01:45:00,913 --> 01:45:02,434 +------ + +1470 +01:45:02,436 --> 01:45:03,450 +------------------ + +1471 +01:45:03,451 --> 01:45:03,958 +------------ + +1472 +01:45:03,958 --> 01:45:04,464 +------ + +1473 +01:45:04,465 --> 01:45:04,972 +------------ + +1474 +01:45:05,480 --> 01:45:05,986 +------ + +1475 +01:45:05,988 --> 01:45:06,495 +--------------- + +1476 +01:45:07,003 --> 01:45:07,509 +------------ + +1477 +01:45:08,018 --> 01:45:08,525 +------------------ + +1478 +01:45:08,525 --> 01:45:09,031 +--- + +1479 +01:45:09,032 --> 01:45:09,539 +------------------ + +1480 +01:45:09,540 --> 01:45:10,047 +------ + +1481 +01:45:10,047 --> 01:45:10,553 +--- --- + +1482 +01:45:10,555 --> 01:45:13,598 +--- + +1483 +01:45:13,599 --> 01:45:14,106 +------ + +1484 +01:45:14,107 --> 01:45:14,614 +------------------ + +1485 +01:45:15,629 --> 01:45:18,165 +松井小姐 + +1486 +01:45:18,166 --> 01:45:18,673 +Background + +1487 +01:45:20,196 --> 01:45:26,793 +因为你必须能够应对各种各样的情况呢 + +1488 +01:45:27,808 --> 01:45:29,330 +不要 不可以 + +1489 +01:45:29,330 --> 01:45:34,911 +泷本 这次换你来这边做吧 + +1490 +01:45:35,419 --> 01:45:40,492 +不要 不可以过来 不要 + +1491 +01:45:46,583 --> 01:45:49,626 +那么 会是怎样的呢 + +1492 +01:45:52,165 --> 01:45:54,193 +把腿张开点 + +1493 +01:45:54,195 --> 01:45:57,239 +泷本君 好久不见啦 + +1494 +01:46:00,284 --> 01:46:02,312 +--- + +1495 +01:46:02,314 --> 01:46:05,357 +看吧 不错吧 + +1496 +01:46:06,881 --> 01:46:10,433 +我也要插进小穴了 不要 + +1497 +01:46:12,970 --> 01:46:14,491 +--- + +1498 +01:46:14,493 --> 01:46:15,000 +--------------- + +1499 +01:46:16,015 --> 01:46:18,044 +小穴好紧 小穴好紧 + +1500 +01:46:18,045 --> 01:46:31,237 +--- + +1501 +01:46:31,746 --> 01:46:34,790 +你还没习惯吗 + +1502 +01:46:35,805 --> 01:46:36,312 +--- + +1503 +01:46:36,313 --> 01:46:38,848 +太棒了 + +1504 +01:46:38,850 --> 01:46:40,879 +--- + +1505 +01:46:41,387 --> 01:46:42,401 +--------------- + +1506 +01:46:42,909 --> 01:46:43,415 +------ + +1507 +01:46:43,417 --> 01:46:43,924 +--- + +1508 +01:46:43,924 --> 01:46:44,431 +------ + +1509 +01:46:44,432 --> 01:46:44,938 +------------------ + +1510 +01:46:52,551 --> 01:46:55,088 +这表情也不错 + +1511 +01:46:56,103 --> 01:46:56,610 +--- + +1512 +01:46:59,147 --> 01:46:59,654 +--------------- + +1513 +01:47:00,162 --> 01:47:00,669 +--- + +1514 +01:47:10,819 --> 01:47:14,878 +我就让你听听抽插的声音吧 + +1515 +01:47:21,475 --> 01:47:21,982 +background + +1516 +01:47:24,520 --> 01:47:25,027 +------------------ + +1517 +01:47:25,535 --> 01:47:26,041 +--- + +1518 +01:47:26,549 --> 01:47:27,056 +------ + +1519 +01:47:28,072 --> 01:47:28,579 +--------------- + +1520 +01:47:28,579 --> 01:47:29,085 +------------------ + +1521 +01:47:29,087 --> 01:47:29,594 +--- + +1522 +01:47:29,594 --> 01:47:30,101 +------ + +1523 +01:47:30,101 --> 01:47:30,607 +------------------ + +1524 +01:47:31,116 --> 01:47:31,623 +--------------- + +1525 +01:47:42,788 --> 01:47:43,294 +background + +1526 +01:47:47,862 --> 01:47:48,369 +Background + +1527 +01:47:49,892 --> 01:47:50,398 +------ + +1528 +01:47:51,414 --> 01:47:53,442 +泷本 那边的情况怎么样 + +1529 +01:47:53,444 --> 01:47:53,951 +泷本那边的情况怎么样 + +1530 +01:47:53,951 --> 01:47:54,458 +泷本 那边的情况怎么样 + +1531 +01:47:54,459 --> 01:47:54,965 +泷本那边的情况怎么样 + +1532 +01:47:54,966 --> 01:47:55,981 +------ + +1533 +01:47:57,503 --> 01:48:01,054 +这样子会让我很兴奋 + +1534 +01:48:01,563 --> 01:48:02,070 +--- + +1535 +01:48:03,085 --> 01:48:09,174 +你看着我的眼睛 加油哦 + +1536 +01:48:23,383 --> 01:48:25,411 +身体趴起来 + +1537 +01:48:25,413 --> 01:48:25,919 +--- + +1538 +01:48:26,428 --> 01:48:26,934 +------------------ + +1539 +01:48:26,935 --> 01:48:27,442 +--------------- + +1540 +01:48:32,009 --> 01:48:32,516 +--- + +1541 +01:48:41,143 --> 01:48:45,202 +把头抬起来 好好看着松井小姐 + +1542 +01:48:47,740 --> 01:48:50,277 +松井小姐 + +1543 +01:48:51,292 --> 01:48:53,321 +不要 + +1544 +01:48:54,844 --> 01:48:58,396 +再让你听听抽插的声音吧 + +1545 +01:49:03,471 --> 01:49:03,977 +------ + +1546 +01:49:04,486 --> 01:49:22,245 +--- + +1547 +01:49:23,769 --> 01:49:26,812 +把手给我吧 + +1548 +01:49:31,380 --> 01:49:34,424 +你看那边 + +1549 +01:49:35,440 --> 01:49:37,977 +还是想近距离看吗 + +1550 +01:49:43,051 --> 01:49:45,588 +要不要近距离看看 + +1551 +01:49:53,708 --> 01:49:58,781 +跟前辈关系很好呢 互相看着对方 + +1552 +01:50:03,349 --> 01:50:03,856 +--- + +1553 +01:50:16,035 --> 01:50:16,541 +你要看着北冈小姐的脸 你们俩相互看看嘛 + +1554 +01:50:16,543 --> 01:50:17,049 +你要看着北冈小姐的脸你们俩相互看看嘛 + +1555 +01:50:17,050 --> 01:50:19,079 +你要看着北冈小姐的脸 你们俩相互看看嘛 + +1556 +01:50:19,080 --> 01:50:19,587 +你要看着北冈小姐的脸你们俩相互看看嘛 + +1557 +01:50:19,587 --> 01:50:20,602 +你要看着北冈小姐的脸 你们俩相互看看嘛 + +1558 +01:50:20,602 --> 01:50:21,108 +你要看着北冈小姐的脸你们俩相互看看嘛 + +1559 +01:50:21,617 --> 01:50:24,154 +多多关照前辈后辈 + +1560 +01:50:24,154 --> 01:50:27,197 +你们都很舒服吧 + +1561 +01:50:33,288 --> 01:50:33,794 +--- + +1562 +01:50:53,078 --> 01:50:53,585 +x + +1563 +01:50:59,168 --> 01:50:59,674 +--- + +1564 +01:51:01,705 --> 01:51:04,749 +松井小姐 怎么样 + +1565 +01:51:04,750 --> 01:51:08,808 +特殊业务讲座还行吧 + +1566 +01:51:11,346 --> 01:51:13,883 +北冈小姐也是 把你的优点教给她吧 + +1567 +01:51:13,884 --> 01:51:14,391 +北冈小姐也是把你的优点教给她吧 + +1568 +01:51:14,391 --> 01:51:17,434 +看来你们俩挺合得来的 + +1569 +01:51:19,465 --> 01:51:25,553 +那么 前辈后辈就友好地站在一起吧 + +1570 +01:51:26,570 --> 01:51:28,598 +北冈小姐 + +1571 +01:51:28,599 --> 01:51:29,106 +在松井小姐旁边摆出一样的姿势 + +1572 +01:51:29,107 --> 01:51:29,614 +在松井小姐旁边 摆出一样的姿势 + +1573 +01:51:29,614 --> 01:51:30,120 +在松井小姐旁边摆出一样的姿势 + +1574 +01:51:30,122 --> 01:51:30,629 +在松井小姐旁边 摆出一样的姿势 + +1575 +01:51:30,629 --> 01:51:32,151 +在松井小姐旁边摆出一样的姿势 + +1576 +01:51:32,152 --> 01:51:32,659 +在松井小姐旁边 摆出一样的姿势 + +1577 +01:51:35,704 --> 01:51:37,732 +要好好相处 + +1578 +01:51:49,912 --> 01:51:50,419 +background + +1579 +01:52:01,583 --> 01:52:04,120 +真是舒服 + +1580 +01:52:06,150 --> 01:52:08,179 +--- + +1581 +01:52:08,180 --> 01:52:08,687 +--------------- + +1582 +01:52:19,851 --> 01:52:22,388 +你们俩手牵着手吧 + +1583 +01:52:27,463 --> 01:52:27,969 +DSQ + +1584 +01:52:34,060 --> 01:52:34,567 +--- + +1585 +01:52:36,597 --> 01:52:37,103 +HTML + +1586 +01:52:48,775 --> 01:52:50,804 +好舒服 + +1587 +01:52:57,402 --> 01:52:59,938 +已经忍不住了 + +1588 +01:53:04,506 --> 01:53:07,043 +感觉要射了 就在里面射吧 + +1589 +01:53:07,043 --> 01:53:07,549 +感觉要射了就在里面射吧 + +1590 +01:53:07,551 --> 01:53:09,580 +感觉要射了 就在里面射吧 + +1591 +01:53:12,118 --> 01:53:12,625 +--- + +1592 +01:53:17,700 --> 01:53:18,206 +------------ + +1593 +01:53:18,207 --> 01:53:18,714 +--------------- + +1594 +01:53:25,311 --> 01:53:27,340 +可以吗 + +1595 +01:53:31,908 --> 01:53:32,415 +------------ + +1596 +01:53:32,415 --> 01:53:34,445 +--- + +1597 +01:53:35,460 --> 01:53:42,563 +北冈小姐 那让我先射吧 + +1598 +01:53:42,564 --> 01:53:45,101 +不要这样 + +1599 +01:53:49,668 --> 01:53:51,697 +要射了 + +1600 +01:53:51,698 --> 01:53:52,205 +--- + +1601 +01:53:53,728 --> 01:53:55,757 +射了 + +1602 +01:54:04,384 --> 01:54:04,891 +background + +1603 +01:54:25,189 --> 01:54:37,875 +--- + +1604 +01:54:40,413 --> 01:54:42,949 +我也忍不住了 + +1605 +01:54:42,950 --> 01:54:47,516 +不要 不可以 + +1606 +01:54:52,591 --> 01:54:53,098 +--- + +1607 +01:55:04,263 --> 01:55:06,799 +我要射了 + +1608 +01:55:11,367 --> 01:55:13,902 +要射了 + +1609 +01:55:22,023 --> 01:55:29,634 +--- + +1610 +01:55:29,635 --> 01:55:31,664 +不行了 + +1611 +01:55:34,709 --> 01:55:37,753 +要射了 我也要射了 + +1612 +01:55:38,261 --> 01:55:38,768 +--- + +1613 +01:55:46,380 --> 01:55:46,887 +HTML + +1614 +01:55:51,455 --> 01:56:43,721 +--- + +1615 +01:56:43,721 --> 01:56:48,794 +(出演) + +1616 +01:56:49,811 --> 01:56:55,898 +(北冈果林) + +1617 +01:56:56,915 --> 01:57:00,973 +(松井日奈子) + +1618 +01:57:00,974 --> 01:57:03,510 +绫野理事长 + +1619 +01:57:04,019 --> 01:57:10,107 +(叶山小百合) + +1620 +01:57:10,616 --> 01:57:14,166 +理事长 这次的新来的孩子呢 + +1621 +01:57:16,198 --> 01:57:18,227 +不错嘛 + +1622 +01:57:19,242 --> 01:57:22,794 +就这样好好教导她吧 + +1623 +01:57:23,809 --> 01:57:26,852 +这家医院疯了 + +1624 +01:57:31,421 --> 01:57:33,450 +差不多准备好了 + +1625 +01:57:33,451 --> 01:57:33,958 +还有什么吗 快点 + +1626 +01:57:33,958 --> 01:57:34,464 +还有什么吗快点 + +1627 +01:57:34,465 --> 01:57:37,001 +还有什么吗 快点 + +1628 +01:57:48,674 --> 01:57:49,181 +--- + +1629 +01:57:55,271 --> 01:57:57,807 +高潮了 我高潮了 + +1630 +01:58:00,345 --> 01:58:03,897 +在嘴里射满 + +1631 +01:58:09,479 --> 01:58:09,986 +--------------- + +1632 +01:58:11,509 --> 01:58:14,045 +我可不允许你做这种事 + +1633 +01:58:14,046 --> 01:58:16,583 +完全没教养嘛 + +1634 +01:58:16,583 --> 01:58:19,626 +必须好好教育一番才行呢 + +1635 +01:58:19,628 --> 01:58:21,656 +请住手吧 让理事长生气 + +1636 +01:58:21,658 --> 01:58:23,179 +请住手吧让理事长生气 + +1637 +01:58:23,180 --> 01:58:24,193 +请住手吧 让理事长生气 + +1638 +01:58:26,732 --> 01:58:27,239 +--- + +1639 +01:58:30,284 --> 01:58:31,806 +住手不要 + +1640 +01:58:31,806 --> 01:58:32,821 +住手 不要 + +1641 +01:58:32,821 --> 01:58:33,327 +阴蒂变大了呢 + +1642 +01:58:33,329 --> 01:58:33,835 +98室[色花堂]永久地址489155.com阴蒂变大了呢 + +1643 +01:58:33,836 --> 01:58:34,851 +98室[芭花堂]永久地址489155.com阴蒂变大了呢 + +1644 +01:58:34,851 --> 01:58:35,357 +98室[巴花堂]永久地址489155.com怎么这样请住手吧 + +1645 +01:58:35,359 --> 01:58:35,866 +98室[巴花室]永久地址 489155.com怎么这样 请住手吧 + +1646 +01:58:35,866 --> 01:58:36,373 +98室[巴花堂]永久地址489155.com怎么这样请住手吧 + +1647 +01:58:36,373 --> 01:58:38,402 +怎么这样 请住手吧 + +1648 +01:58:38,403 --> 01:58:41,446 +住手 快住手啦 + +1649 +01:58:41,448 --> 01:58:41,955 +--- + +1650 +01:58:44,493 --> 01:58:47,536 +你也可以叫出声 + +1651 +01:58:47,537 --> 01:58:48,044 +--- + +1652 +01:58:49,060 --> 01:58:52,103 +腿不要合上 + +1653 +01:58:52,104 --> 01:58:55,147 +不要过来 不要过来 + +1654 +01:58:55,149 --> 01:58:58,192 +给我老实一点 + +1655 +01:58:58,194 --> 01:59:00,730 +--- + +1656 +01:59:00,731 --> 01:59:05,297 +不要想着要反抗 + +1657 +01:59:11,387 --> 01:59:13,415 +真是久等了呢 + +1658 +01:59:13,417 --> 01:59:16,460 +无礼的家伙玩得很开心呢 + +1659 +01:59:35,744 --> 01:59:36,250 +--- + +1660 +02:00:06,191 --> 02:00:09,235 +尽情射出来吧 + +1661 +02:00:09,235 --> 02:00:09,741 +------ + +1662 +02:00:09,743 --> 02:00:10,250 +--- + +1663 +02:00:10,250 --> 02:00:10,757 +------------ + +1664 +02:00:10,758 --> 02:00:11,264 +--- + +1665 +02:00:11,265 --> 02:00:15,831 +我也要射了 不行 + +1666 +02:00:21,414 --> 02:00:23,951 +不要道歉 果林前辈 diff --git a/tests/nodes/test_llm_filter/test_filter.py b/tests/nodes/test_llm_filter/test_filter.py new file mode 100644 index 0000000..7d40056 --- /dev/null +++ b/tests/nodes/test_llm_filter/test_filter.py @@ -0,0 +1,442 @@ +"""nodes/llm_filter.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`nodes/llm_filter.py`(字幕两级过滤:确定性规则层 + LLM 分类层), +可独立调用。规则层用例直接用真实 OCR 文本(不调模型);LLM 层用例在 I/O +边界 mock HTTP;集成用例使用真实 OCR 回归数据验证保留量。 +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from nodes.llm_filter import ( + CATEGORY_DIALOGUE, + CATEGORY_GARBAGE, + CATEGORY_NOISE, + CATEGORY_OVERLAY, + CATEGORY_REPEAT, + DEFAULT_CONTEXT_SIZE, + DEFAULT_MIN_KEEP_LEN, + DEFAULT_USE_LLM, + DELETE_CATEGORIES, + TARGET_MARK, + _dedup_key, + _load_partial, + _rule_verdict, + _should_delete, + invoke, + parse_srt, + serialize_srt, +) +from wov_sdk.models import InvokeRequest + +# 模块专用数据:真实任务 1666 条 OCR 输出(规则层回归基线)。 +DATA_DIR = Path(__file__).resolve().parent / "data" +REAL_OCR_SRT = DATA_DIR / "ocr_srt_run_ac7f480a3ccb.srt" + + +def _request(tmp_path: Path, srt_text: str | Path, **params) -> InvokeRequest: + """构造真实请求;srt_text 为字符串时落到临时文件。""" + if isinstance(srt_text, Path): + srt_path = srt_text + else: + srt_path = tmp_path / "in.srt" + srt_path.write_text(srt_text, encoding="utf-8") + return InvokeRequest( + run_id="run-test", + node_instance_id="llm-filter-1", + params=params, + inputs={"srt_uri": str(srt_path)}, + output_dir=str(tmp_path / "out"), + ) + + +# --------------------------------------------------------------------------- +# 规则层:确定要删的噪声 +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + ("text", "reason"), + [ + ("", "空文本"), + (" ", "纯空白"), + ("---", "横线装饰"), + ("====", "等号装饰"), + ("http://example.com/x", "URL"), + ("www.example.com", "www 网址"), + ("user@example.com", "邮箱"), + ("example.com", "裸域名"), + ("code", "HTML 标签"), + ("javascript:void(0)", "JS 片段"), + ("2011-11-27", "日期"), + ("4.0", "数值"), + ("SPHO-1", "水印编号"), + ("(出演)", "角色标注"), + ("a", "单 ASCII 字符"), + ("AB", "双 ASCII 字符"), + ("no text is visible", "VLM 提示回显"), + ], +) +def test_rule_layer_deletes_known_noise(text: str, reason: str) -> None: + """规则层对已确认的噪声模式返回 True(删除)。""" + # 数据:上表列出的噪声文本。 + # 测试过程 + verdict = _rule_verdict(text, set()) + + # 验证结果:明确删除。 + assert verdict is True, f"{reason} 应被规则层删除:{text!r}" + + +@pytest.mark.parametrize( + "text", + [ + "好好教育她一番吧", + "腿不要合上", + "这家医院 为VIP患者提供了特殊服务", + "嗯", + "好", + "(小声)不要啊", + ], +) +def test_rule_layer_keeps_real_dialogue(text: str) -> None: + """真实对话(含短中文与括号语气)不被规则层删除。""" + # 数据:真实对话文本(含曾被我误删的样本)。 + # 测试过程 + verdict = _rule_verdict(text, set()) + + # 验证结果:返回 None(交给下游/保留),不是 True。 + assert verdict is not True + + +def test_rule_layer_honors_custom_overlay_tokens() -> None: + """自定义 overlay token 生效(参数可扩展水印词表,传入需小写)。 + + 用不含点号的词,避免与“裸域名”规则混淆。 + """ + # 数据:自定义水印词(中文与无点号英文各一)。 + # 测试过程与验证结果 + assert _rule_verdict("私人水印", {"私人水印"}) is True + assert _rule_verdict("私人水印", set()) is not True + assert _rule_verdict("mywatermark", {"mywatermark"}) is True + + +def test_rule_layer_deletes_short_ascii_but_keeps_cjk() -> None: + """≤2 个 ASCII 字符删除,但含中文的短词保留(可能是内容)。""" + # 数据:ASCII 与 CJK 短词。 + # 测试过程与验证结果 + assert _rule_verdict("ab", set()) is True + assert _rule_verdict("好", set()) is not True + + +# --------------------------------------------------------------------------- +# LLM 层判定组合逻辑 +# --------------------------------------------------------------------------- + + +def test_should_delete_semantics() -> None: + """类别 → 删除判定:garbage/overlay 必删;repeat/dialogue 必留;noise 短删长留。""" + # 数据:五类类别与长短文本。 + # 测试过程与验证结果 + assert _should_delete(CATEGORY_GARBAGE, "任意", 12) is True + assert _should_delete(CATEGORY_OVERLAY, "任意", 12) is True + assert _should_delete(CATEGORY_REPEAT, "任意", 12) is False + assert _should_delete(CATEGORY_DIALOGUE, "任意", 12) is False + assert _should_delete(CATEGORY_NOISE, "短文本", 12) is True + assert _should_delete(CATEGORY_NOISE, "这是一句足够长的真实对话内容", 12) is False + assert DELETE_CATEGORIES == {CATEGORY_GARBAGE, CATEGORY_OVERLAY, CATEGORY_NOISE} + + +def test_dedup_key_ignores_whitespace_and_case() -> None: + """去重键忽略空白与大小写(OCR 同句带/不带空格视为同一文本)。""" + # 数据:带空格与不带空格的同一句话。 + # 测试过程与验证结果 + assert _dedup_key("可 没法胜任") == _dedup_key("可没法胜任") + assert _dedup_key("ABC") == _dedup_key("abc") + + +def test_default_flags() -> None: + """默认参数:LLM 层关闭、上下文 10 条、长文本保护阈值 12。""" + # 数据:模块常量。 + # 测试过程与验证结果 + assert DEFAULT_USE_LLM is False + assert DEFAULT_CONTEXT_SIZE == 10 + assert DEFAULT_MIN_KEEP_LEN == 12 + assert TARGET_MARK == "【目标】" + + +# --------------------------------------------------------------------------- +# SRT 解析与断点存档 +# --------------------------------------------------------------------------- + + +def test_parse_and_serialize_srt_round_trip() -> None: + """解析后序列化保留时间轴与正文,序号重排。""" + # 数据:两条 SRT。 + text = "7\n00:00:01,000 --> 00:00:02,000\n甲\n\n9\n00:00:03,000 --> 00:00:04,000\n乙\n" + + # 测试过程 + entries = parse_srt(text) + out = serialize_srt(entries) + + # 验证结果 + assert [e["text"] for e in entries] == ["甲", "乙"] + assert out.startswith("1\n00:00:01,000 --> 00:00:02,000\n甲") + assert parse_srt(out) == [ + {"start": "00:00:01,000", "end": "00:00:02,000", "text": "甲"}, + {"start": "00:00:03,000", "end": "00:00:04,000", "text": "乙"}, + ] + + +def test_load_partial_reads_checkpoint(tmp_path: Path) -> None: + """断点存档按 index → category 读回(重跑时不重复判定)。""" + # 数据:一份真实格式的存档文件。 + output_dir = tmp_path / "out" + output_dir.mkdir() + (output_dir / "filter_partial.jsonl").write_text( + json.dumps({"index": 3, "category": "garbage"}) + "\n" + + json.dumps({"index": 5, "category": "dialogue"}) + "\n", + encoding="utf-8", + ) + + # 测试过程 + partial = _load_partial(output_dir) + + # 验证结果 + assert partial == {3: "garbage", 5: "dialogue"} + + +def test_load_partial_ignores_corrupt_lines(tmp_path: Path) -> None: + """存档中的坏行被忽略(不因单行损坏丢掉全部断点)。""" + # 数据:一行合法 + 一行截断。 + output_dir = tmp_path / "out" + output_dir.mkdir() + (output_dir / "filter_partial.jsonl").write_text( + json.dumps({"index": 1, "category": "noise"}) + "\n{broken\n", + encoding="utf-8", + ) + + # 测试过程 + partial = _load_partial(output_dir) + + # 验证结果 + assert partial == {1: "noise"} + + +def test_load_partial_missing_file_returns_empty(tmp_path: Path) -> None: + """无存档时返回空字典(首次运行)。""" + # 数据:空目录。 + # 测试过程与验证结果 + assert _load_partial(tmp_path) == {} + + +# --------------------------------------------------------------------------- +# invoke:规则层默认行为(不调 LLM) +# --------------------------------------------------------------------------- + + +def test_invoke_rule_only_removes_noise_keeps_dialogue(tmp_path: Path) -> None: + """默认(use_llm=0)只跑规则层:噪声删除、真实对话保留,且不调 LLM。""" + # 数据:混合了噪声与真实对话的 SRT。 + srt = ( + "1\n00:00:01,000 --> 00:00:02,000\n---\n\n" + "2\n00:00:03,000 --> 00:00:04,000\n好好教育她一番吧\n\n" + "3\n00:00:05,000 --> 00:00:06,000\nhttp://spam.example/x\n\n" + "4\n00:00:07,000 --> 00:00:08,000\n腿不要合上\n" + ) + + # 测试过程 + response = invoke(_request(tmp_path, srt)) + + # 验证结果:产物只保留两条真实对话,kept/removed 计数正确。 + assert response.status == "completed", response.error + content = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8") + assert "好好教育她一番吧" in content + assert "腿不要合上" in content + assert "---" not in content + assert "spam.example" not in content + assert response.outputs["kept"] == 2 + assert response.outputs["removed"] == 2 + + +def test_invoke_default_does_not_call_llm(monkeypatch, tmp_path: Path) -> None: + """默认关闭 LLM 层,绝不发起网络请求(零 LLM 调用)。""" + # 数据:全部是规则层无法判定的普通对话。 + srt = "1\n00:00:01,000 --> 00:00:02,000\n这是一句普通对话内容\n" + calls: list = [] + monkeypatch.setattr( + "urllib.request.urlopen", + lambda *a, **k: calls.append(1) or (_ for _ in ()).throw(AssertionError("不应调用网络")), + ) + + # 测试过程 + response = invoke(_request(tmp_path, srt)) + + # 验证结果:成功且无网络调用。 + assert response.status == "completed" + assert calls == [] + + +def test_invoke_use_llm_deletes_by_category(monkeypatch, tmp_path: Path) -> None: + """use_llm=1 时按 LLM 类别删除(overlay 删除,dialogue 保留)。""" + # 数据:两条待判定文本 + mock 返回不同类别。 + srt = ( + "1\n00:00:01,000 --> 00:00:02,000\n水印文字内容\n\n" + "2\n00:00:03,000 --> 00:00:04,000\n真实的对话内容\n" + ) + replies = iter(["overlay", "dialogue"]) + + class _Resp: + def __init__(self, content: str) -> None: + self._body = json.dumps({"choices": [{"message": {"content": content}}]}).encode() + + def read(self) -> bytes: + return self._body + + def __enter__(self): + return self + + def __exit__(self, *exc): + return None + + monkeypatch.setenv("LLM_API_KEY", "sk-test") + monkeypatch.setattr("urllib.request.urlopen", lambda *a, **k: _Resp(next(replies))) + + # 测试过程 + response = invoke(_request(tmp_path, srt, use_llm=1, pool_max_workers=1)) + + # 验证结果:overlay 被删、dialogue 保留。 + assert response.status == "completed", response.error + content = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8") + assert "真实的对话内容" in content + assert "水印文字内容" not in content + + +def test_invoke_llm_long_text_protection(monkeypatch, tmp_path: Path) -> None: + """LLM 判 noise 时,长文本受保护不被删除。""" + # 数据:一条长文本,LLM 返回 noise。 + long_text = "这是一句相当长的真实对话内容不应该被当作噪声删除掉" + srt = f"1\n00:00:01,000 --> 00:00:02,000\n{long_text}\n" + + class _Resp: + def read(self) -> bytes: + return json.dumps({"choices": [{"message": {"content": "noise"}}]}).encode() + + def __enter__(self): + return self + + def __exit__(self, *exc): + return None + + monkeypatch.setenv("LLM_API_KEY", "sk-test") + monkeypatch.setattr("urllib.request.urlopen", lambda *a, **k: _Resp()) + + # 测试过程 + response = invoke(_request(tmp_path, srt, use_llm=1, pool_max_workers=1)) + + # 验证结果:长文本保留。 + assert response.status == "completed" + content = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8") + assert long_text in content + + +def test_invoke_fails_on_llm_error(monkeypatch, tmp_path: Path) -> None: + """LLM 持续报错时节点失败(不静默产出错误结果)。""" + # 数据:所有请求都失败。 + srt = "1\n00:00:01,000 --> 00:00:02,000\n待判定文本\n" + + def fake_urlopen(*a, **k): + raise OSError("network down") + + monkeypatch.setenv("LLM_API_KEY", "sk-test") + monkeypatch.setattr("urllib.request.urlopen", fake_urlopen) + + # 测试过程 + response = invoke(_request(tmp_path, srt, use_llm=1, pool_max_workers=1)) + + # 验证结果 + assert response.status == "failed" + + +def test_invoke_fails_without_input(tmp_path: Path) -> None: + """缺少 srt_uri 时失败。""" + # 数据:空输入。 + request = InvokeRequest( + run_id="r", node_instance_id="n", params={}, inputs={}, output_dir=str(tmp_path) + ) + + # 测试过程 + response = invoke(request) + + # 验证结果 + assert response.status == "failed" + assert "srt_uri" in (response.error or "") + + +def test_invoke_fails_when_input_missing(tmp_path: Path) -> None: + """输入文件不存在时失败。""" + # 数据:不存在的路径。 + request = InvokeRequest( + run_id="r", node_instance_id="n", params={}, + inputs={"srt_uri": str(tmp_path / "nope.srt")}, output_dir=str(tmp_path), + ) + + # 测试过程 + response = invoke(request) + + # 验证结果 + assert response.status == "failed" + assert "not found" in (response.error or "") + + +# --------------------------------------------------------------------------- +# 真实 OCR 数据回归(规则层) +# --------------------------------------------------------------------------- + + +@pytest.mark.integration +def test_rule_layer_on_real_ocr_output(tmp_path: Path) -> None: + """真实任务 1666 条 OCR 输出:规则层产出与已确认基线一致。 + + 基线(2026-09 人工审查确认):保留 863 条、删除 803 条,且不再有任何 + 真实对话被误删(旧 LLM 层误删 73 条)。基线变动需同步 docs/decisions.md。 + """ + # 数据:真实任务 run_ac7f480a3ccb 的 OCR 输出。 + if not REAL_OCR_SRT.is_file(): + pytest.skip(f"缺少真实 OCR 回归数据 {REAL_OCR_SRT}") + + # 测试过程 + response = invoke(_request(tmp_path, REAL_OCR_SRT)) + + # 验证结果:成功、总数守恒、保留/删除量与基线一致。 + assert response.status == "completed", response.error + kept = int(response.outputs["kept"]) + removed = int(response.outputs["removed"]) + assert kept + removed == 1666 + assert (kept, removed) == (863, 803), f"规则层结果偏离基线:保留 {kept} 删除 {removed}" + + +@pytest.mark.integration +def test_rule_layer_keeps_known_real_dialogue_from_regression_set() -> None: + """回归数据中的已知真实对话逐条验证不被规则层删除(误删防护)。""" + # 数据:真实 OCR 输出中人工确认的真实对话样本。 + if not REAL_OCR_SRT.is_file(): + pytest.skip(f"缺少真实 OCR 回归数据 {REAL_OCR_SRT}") + known_dialogue = [ + "好好教育她一番吧", + "腿不要合上", + ] + + # 测试过程 + entries = parse_srt(REAL_OCR_SRT.read_text(encoding="utf-8")) + + # 验证结果:样本确实存在于数据中(防止数据被替换后测试空转)。 + texts = [e["text"] for e in entries] + present = [d for d in known_dialogue if any(d in t for t in texts)] + assert present, "回归数据中未找到已知真实对话,请检查数据文件" + for text in texts: + if any(d in text for d in present): + assert _rule_verdict(text, set()) is not True, f"真实对话被误删:{text!r}" diff --git a/tests/nodes/test_proper_nouns/__init__.py b/tests/nodes/test_proper_nouns/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/nodes/test_proper_nouns/test_rule.py b/tests/nodes/test_proper_nouns/test_rule.py new file mode 100644 index 0000000..85caf63 --- /dev/null +++ b/tests/nodes/test_proper_nouns/test_rule.py @@ -0,0 +1,179 @@ +"""nodes/proper_nouns.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`nodes/proper_nouns.py`(专名/拟声/成人隐语提示词规则),供 +llm-translate 注入系统提示词,纯函数可独立调用。 + +覆盖:未命中不注入、三类规则(专名/拟声/隐语)各自命中、批量文本输入、 +规则文本包含可执行的翻译指令。 +""" + +from __future__ import annotations + +import pytest + +from nodes.proper_nouns import ( + ADULT_EUPHEMISMS, + ONOMATOPOEIA, + PROPER_NOUNS, + build_proper_noun_rule, +) + + +def test_returns_none_when_nothing_matches() -> None: + """普通文本未命中任何规则表时不注入(避免干扰正常翻译)。""" + # 数据:不含规则表词条的普通句子。 + text = "今天天气真好。\n我们一起去散步吧。" + + # 测试过程 + rule = build_proper_noun_rule(text) + + # 验证结果 + assert rule is None + + +def test_empty_input_returns_none() -> None: + """空文本与空列表都不注入规则。""" + # 数据:空字符串与空列表。 + # 测试过程与验证结果 + assert build_proper_noun_rule("") is None + assert build_proper_noun_rule([]) is None + + +def test_injects_proper_noun_rule_with_advice() -> None: + """命中专名表时注入该词的处置建议(禁止硬译)。""" + # 数据:真实字幕片段,含角色专名 ジンゴ。 + text = "クモ穴にパンパンになって ジンゴがここで味わいませんか" + + # 测试过程 + rule = build_proper_noun_rule(text) + + # 验证结果:规则非空,含原词与处置说明。 + assert rule is not None + assert "ジンゴ" in rule + assert "芒果" in rule # 说明文本中包含"禁止译作芒果"的约束 + + +def test_injects_person_name_rule() -> None: + """人名类专名命中时给出音译建议(不保留日文写法)。""" + # 数据:含人名 カンタくん。 + text = "カンタくん、そこにいたの" + + # 测试过程 + rule = build_proper_noun_rule(text) + + # 验证结果 + assert rule is not None + assert "カンタくん" in rule + assert "音译" in rule + + +def test_injects_onomatopoeia_rule() -> None: + """拟声/拟态词命中时给出按语境翻译的建议。""" + # 数据:含拟态词 ビクビク。 + text = "体がビクビク震えてる" + + # 测试过程 + rule = build_proper_noun_rule(text) + + # 验证结果 + assert rule is not None + assert "ビクビク" in rule + + +def test_injects_adult_euphemism_with_actual_meaning() -> None: + """成人语境隐语命中时注入"实际含义 + 应译词 + 禁止字面直译"。""" + # 数据:含隐语 マンゴー(实际指女性性器官)。 + text = "そろそろマンゴーが濡れてきました" + + # 测试过程 + rule = build_proper_noun_rule(text) + + # 验证结果:规则提示这是隐语并禁止字面直译。 + assert rule is not None + assert "マンゴー" in rule + assert "隐语" in rule + assert "切勿按字面直译" in rule + + +def test_accepts_batch_line_list() -> None: + """接受原文行列表(按批翻译时传入多行),跨行命中同样注入。""" + # 数据:命中词分散在不同行。 + lines = ["普通的一句话", "相手がバナナをしゃぶってくれて"] + + # 测试过程 + rule = build_proper_noun_rule(lines) + + # 验证结果 + assert rule is not None + assert "バナナ" in rule + + +def test_rule_text_covers_all_injected_tokens() -> None: + """多词同时命中时每条都出现在规则文本中(不丢词)。""" + # 数据:同时含专名、拟声、隐语各一个。 + text = "ジンゴとビクビクとマンゴーの話" + + # 测试过程 + rule = build_proper_noun_rule(text) + + # 验证结果:三个词都在规则里。 + assert rule is not None + for token in ("ジンゴ", "ビクビク", "マンゴー"): + assert token in rule + + +def test_each_rule_table_entry_is_self_consistent() -> None: + """规则表结构自检:每张表的每条记录字段完整,避免维护时漏字段。 + + 这不重复业务逻辑,而是保证数据资产(表)本身可用——新增词条时若写错 + 结构,此处会立刻失败。 + """ + # 数据:三张规则表。 + tables = (PROPER_NOUNS, ONOMATOPOEIA, ADULT_EUPHEMISMS) + + # 测试过程与验证结果 + for table in tables: + assert table, "规则表不应为空" + for token, payload in table.items(): + assert token.strip(), "词条不能为空白" + assert all(str(part).strip() for part in payload), f"{token} 的说明字段不完整" + + +@pytest.mark.integration +def test_rule_matches_real_transcript_data() -> None: + """真实数据校准:真实日文 transcript 中含专名的片段应命中并注入规则。 + + 数据来源:`data/` 下真实任务的 transcript.srt(gitignored,缺失即跳过)。 + """ + # 数据:仓库 data/ 下真实产物的 transcript 文件。 + import json + from pathlib import Path + + from tests.shared.srt_entries import parse_srt_entries + + workspace = Path(__file__).resolve().parents[3] + # 真实产物里的字幕(ASR 转录或过滤后字幕都可能含专名,两者都扫)。 + candidates = sorted( + p for pattern in ("steps/asr/transcript.srt", "steps/filter/*.srt", "steps/ocr/subtitle.srt") + for p in (workspace / "data" / "storage").glob(f"runs/*/{pattern}") + if p.is_file() + ) + if not candidates: + pytest.skip("缺少真实字幕后端产物(data/ 未保留运行产物),跳过") + + # 测试过程:在真实字幕里找**实际出现的**专名(不写死某个词, + # 数据内容随影片变化),用它的上下文构造规则。 + for path in candidates: + entries = parse_srt_entries(path.read_text(encoding="utf-8")) + for token in PROPER_NOUNS: + batch = [e["text"] for e in entries if token in e["text"]] + if not batch: + continue + rule = build_proper_noun_rule(batch) + + # 验证结果:命中专名即注入规则,且规则可安全序列化进提示词。 + assert rule is not None, f"真实数据含专名 {token},应注入规则" + assert token in rule, f"规则应包含命中的专名 {token}" + assert json.dumps(rule, ensure_ascii=False) + return + pytest.skip("真实字幕中未出现规则表内的专名,跳过") diff --git a/tests/nodes/test_srt/__init__.py b/tests/nodes/test_srt/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/nodes/test_srt/data/sample.reference.srt b/tests/nodes/test_srt/data/sample.reference.srt new file mode 100644 index 0000000..c869510 --- /dev/null +++ b/tests/nodes/test_srt/data/sample.reference.srt @@ -0,0 +1,19 @@ +1 +00:00:00,000 --> 00:00:01,440 +--- + +2 +00:00:01,440 --> 00:00:04,320 +SUB 001 + +3 +00:00:05,760 --> 00:00:06,240 +--- + +4 +00:00:06,240 --> 00:00:09,120 +SUB 002 + +5 +00:00:09,600 --> 00:00:10,080 +--- diff --git a/tests/nodes/test_srt/test_parsing.py b/tests/nodes/test_srt/test_parsing.py new file mode 100644 index 0000000..c442ae3 --- /dev/null +++ b/tests/nodes/test_srt/test_parsing.py @@ -0,0 +1,224 @@ +"""nodes/srt.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`nodes/srt.py`(SRT 条目解析与序列化),是 `llm-translate`、 +`llm-filter` 等节点的公共依赖,可独立调用也可被组合调用,因此按规则 +拥有独立的模块测试目录 `tests/nodes/test_srt/`。 + +结构约定:每个用例先准备输入数据,再调用真实生产代码 `parse_srt` / +`serialize_srt`,最后断言输出结果;数据为测试代码内常量或本目录 `data/` +下的真实字幕文件,不依赖全局 conftest 与其他测试的执行顺序。 +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from nodes.srt import Cue, parse_srt, serialize_srt + +# 模块专用数据目录:真实字幕文件放在测试代码所在目录内,与其他测试区分。 +DATA_DIR = Path(__file__).resolve().parent / "data" + +# 真实字幕参考文件(5 条,含 `---` 装饰正文与多条连续条目)。 +SAMPLE_REFERENCE_SRT = DATA_DIR / "sample.reference.srt" + + +def test_parses_single_cue() -> None: + """最基本的合法 SRT:一条字幕解析出时间戳与正文。""" + # 数据:标准格式的单条字幕。 + text = "1\n00:00:01,000 --> 00:00:02,000\n你好\n" + + # 测试过程:调用真实解析函数。 + cues = parse_srt(text) + + # 验证结果:条目数量、时间戳与正文完全一致。 + assert cues == [Cue("00:00:01,000", "00:00:02,000", "你好")] + + +def test_accepts_bom_and_crlf() -> None: + """兼容 Windows BOM 与 CRLF 换行(真实字幕文件常见编码形态)。""" + # 数据:带 BOM 且行尾为 \r\n 的 SRT。 + text = "\ufeff1\r\n00:00:01,000 --> 00:00:02,000\r\n你好\r\n" + + # 测试过程 + cues = parse_srt(text) + + # 验证结果:BOM 与 \r 不残留到正文。 + assert cues == [Cue("00:00:01,000", "00:00:02,000", "你好")] + + +def test_keeps_multiline_body() -> None: + """多行正文(含逗号类标点)必须完整保留内部换行。""" + # 数据:第二条以正文结尾(文件末尾无空行)。 + text = ( + "1\n00:00:01,000 --> 00:00:02,000\n第一行\n第二行\n\n" + "2\n00:00:03,000 --> 00:00:04,000\n末尾无空行" + ) + + # 测试过程 + cues = parse_srt(text) + + # 验证结果:两条条目、正文分别保留内部换行。 + assert [cue.text for cue in cues] == ["第一行\n第二行", "末尾无空行"] + + +def test_keeps_empty_body_with_timeline() -> None: + """空正文条目保留时间轴(翻译节点需要空 cue 占位对齐)。""" + # 数据:第一条正文为空,第二条有正文。 + text = ( + "1\n00:00:01,000 --> 00:00:02,000\n\n" + "2\n00:00:03,000 --> 00:00:04,000\n有词\n" + ) + + # 测试过程 + cues = parse_srt(text) + + # 验证结果:空正文解析为空字符串,但时间轴不被丢弃。 + assert cues[0] == Cue("00:00:01,000", "00:00:02,000", "") + assert len(cues) == 2 + + +def test_treats_whitespace_only_body_as_empty() -> None: + """仅含空白的正文视为空正文,不作为正文内容写入。""" + # 数据:第一条正文是三个空格。 + text = ( + "1\n00:00:01,000 --> 00:00:02,000\n \n" + "2\n00:00:03,000 --> 00:00:04,000\nB\n" + ) + + # 测试过程 + cues = parse_srt(text) + + # 验证结果 + assert [cue.text for cue in cues] == ["", "B"] + + +def test_accepts_extra_spaces_around_arrow() -> None: + """时间戳箭头两侧多余空格不导致解析失败。""" + # 数据:箭头两侧各三个空格。 + text = "1\n00:00:01,000 --> 00:00:02,000\nA\n" + + # 测试过程与验证结果 + assert parse_srt(text) == [Cue("00:00:01,000", "00:00:02,000", "A")] + + +def test_accepts_arbitrary_index_numbers() -> None: + """序号只需是数字:真实字幕(如参考 SRT)序号可以不从 1 连续。""" + # 数据:序号为 7 与 16(真实参考字幕的形态)。 + text = "7\n00:00:01,000 --> 00:00:02,000\nA\n\n16\n00:00:03,000 --> 00:00:04,000\nB\n" + + # 测试过程 + cues = parse_srt(text) + + # 验证结果:序号不参与输出,只保留时间戳与正文。 + assert cues == [Cue("00:00:01,000", "00:00:02,000", "A"), Cue("00:00:03,000", "00:00:04,000", "B")] + + +def test_accepts_hours_over_99() -> None: + """超过两位的小时数(长视频)必须保留原样。""" + # 数据:小时为 100。 + text = "1\n100:00:01,000 --> 100:00:02,000\nA\n" + + # 测试过程与验证结果 + assert parse_srt(text)[0].start == "100:00:01,000" + + +def test_empty_and_blank_input_return_no_cues() -> None: + """空文件与仅含空行的文件都返回空列表,而不是报错。""" + # 数据:空字符串、纯空行两类输入。 + # 测试过程与验证结果 + assert parse_srt("") == [] + assert parse_srt("\n\n\n") == [] + + +def test_rejects_dot_millisecond_separator() -> None: + """毫秒分隔符是逗号;点号(VTT 风格)必须明确报错而非静默错解析。""" + # 数据:时间戳使用点号。 + text = "1\n00:00:01.000 --> 00:00:02.000\nA\n" + + # 测试过程与验证结果:抛出 ValueError 并给出行号。 + with pytest.raises(ValueError, match="timestamp"): + parse_srt(text) + + +def test_rejects_missing_index() -> None: + """缺少序号行时明确报错(否则时间轴行会被当成序号)。""" + # 数据:直接以时间戳开头。 + text = "00:00:01,000 --> 00:00:02,000\nA\n" + + # 测试过程与验证结果 + with pytest.raises(ValueError, match="index"): + parse_srt(text) + + +def test_rejects_truncated_last_entry() -> None: + """末尾条目只有序号、没有时间戳时明确报错。""" + # 数据:最后一行是孤立的序号 2。 + text = "1\n00:00:01,000 --> 00:00:02,000\nA\n\n2\n" + + # 测试过程与验证结果 + with pytest.raises(ValueError, match="index"): + parse_srt(text) + + +def test_rejects_negative_timestamp() -> None: + """负时间戳非法,必须报错。""" + # 数据:起始时间为负数。 + text = "1\n-00:00:01,000 --> 00:00:02,000\nA\n" + + # 测试过程与验证结果 + with pytest.raises(ValueError, match="timestamp"): + parse_srt(text) + + +def test_no_blank_line_between_cues_keeps_timeline_in_body() -> None: + """缺少空行分隔时必须报错,而不是把下一条的时间轴吞进上一条正文。 + + 真实形态:`...\\nA\\n2\\n00:00:03,000 --> 00:00:04,000\\nB\\n`。 + 当前实现会把 `2` 与时间轴行当作上一条正文,静默产出时间轴错位的字幕 + (同 R05 类"静默错位"缺陷:解析不报错,但字幕时间与文本不对应)。 + """ + # 数据:两条条目之间没有空行分隔。 + text = "1\n00:00:01,000 --> 00:00:02,000\nA\n2\n00:00:03,000 --> 00:00:04,000\nB\n" + + # 测试过程与验证结果:应明确报错,不能静默吞并。 + with pytest.raises(ValueError): + parse_srt(text) + + +def test_serialize_renumbers_and_keeps_empty_cue() -> None: + """序列化按顺序重排序号,空正文条目仍保留时间轴行。""" + # 数据:两条条目,第二条正文为空。 + cues = [Cue("00:00:01,000", "00:00:02,000", "A"), Cue("00:00:03,000", "00:00:04,000", "")] + + # 测试过程 + text = serialize_srt(cues) + + # 验证结果:序号连续、空正文条目保留时间轴与尾随空行。 + assert text == ( + "1\n00:00:01,000 --> 00:00:02,000\nA\n\n" + "2\n00:00:03,000 --> 00:00:04,000\n\n" + ) + + +def test_serialize_empty_list_returns_empty_text() -> None: + """空列表序列化为空字符串(供节点写出空字幕)。""" + # 数据:空条目列表。 + # 测试过程与验证结果 + assert serialize_srt([]) == "" + + +def test_round_trip_is_stable_on_real_reference_srt() -> None: + """真实参考字幕:解析 → 序列化 → 再解析结果完全一致(时间轴无损)。""" + # 数据:本模块 data/ 下的真实参考字幕(5 条,含 `---` 正文)。 + raw = SAMPLE_REFERENCE_SRT.read_text(encoding="utf-8") + + # 测试过程:解析后序列化,再解析一次。 + first = parse_srt(raw) + second = parse_srt(serialize_srt(first)) + + # 验证结果:条目数与内容不变,且时间轴行数与源文件一致。 + assert first == second + assert len(first) == sum(1 for line in raw.splitlines() if "-->" in line) + assert first[2].text == "---" diff --git a/tests/nodes/test_subtitle_cleanup/__init__.py b/tests/nodes/test_subtitle_cleanup/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/nodes/test_subtitle_cleanup/test_cleanup.py b/tests/nodes/test_subtitle_cleanup/test_cleanup.py new file mode 100644 index 0000000..e9e5e28 --- /dev/null +++ b/tests/nodes/test_subtitle_cleanup/test_cleanup.py @@ -0,0 +1,248 @@ +"""nodes/subtitle_cleanup.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`nodes/subtitle_cleanup.py`(幻觉整条删除 + 短呻吟过滤),被 +whisper(日语链路)与 llm-translate(中文链路)复用,纯函数可独立调用。 + +每个用例构造真实 SRT 文本,调用真实清理函数,并解析输出验证时间轴与序号。 +""" + +from __future__ import annotations + +from nodes.subtitle_cleanup import ( + DEFAULT_MOAN_MAX_CHARS, + HALLUCINATION_TOKENS, + JAPANESE_HALLUCINATION_TOKENS, + clean_japanese_hallucinations, + clean_srt_text, + remove_hallucination_entries, + remove_short_moan_entries, +) +from tests.shared.srt_entries import parse_srt_entries + + +def _srt(*cues: tuple[str, str, str]) -> str: + """把 (起始, 结束, 文本) 列表拼成标准 SRT 文本,供各用例作为输入数据。""" + blocks = [ + f"{i}\n{start} --> {end}\n{text}\n" + for i, (start, end, text) in enumerate(cues, 1) + ] + return "\n".join(blocks) + + +# --------------------------------------------------------------------------- +# 幻觉整条删除(中文词表 / 日语词表) +# --------------------------------------------------------------------------- + + +def test_removes_long_hallucination_cue_entirely() -> None: + """展示时长达到阈值的寒暄幻觉整条删除(时间轴不残留空 cue)。""" + # 数据:一条 20s 的"谢谢观看"(超过默认 15s 阈值)。 + text = _srt( + ("00:00:01,000", "00:00:02,000", "真实的对话"), + ("00:00:03,000", "00:00:23,000", "谢谢观看"), + ) + + # 测试过程 + cleaned = clean_srt_text(text) + + # 验证结果:只剩真实对话,序号重排为 1,幻想的行完全消失。 + entries = parse_srt_entries(cleaned) + assert [e["text"] for e in entries] == ["真实的对话"] + assert cleaned.startswith("1\n") + assert "谢谢观看" not in cleaned + + +def test_keeps_short_hallucination_when_inside_threshold() -> None: + """展示时长低于阈值的相同词可能是剧情真实内容,必须保留。""" + # 数据:一条 2s 的"晚安"(剧情中真实互道晚安)。 + text = _srt(("00:00:01,000", "00:00:03,000", "晚安")) + + # 测试过程 + cleaned = clean_srt_text(text) + + # 验证结果:保留。 + assert [e["text"] for e in parse_srt_entries(cleaned)] == ["晚安"] + + +def test_keeps_non_hallucination_long_cue() -> None: + """长时但不是幻觉词的内容必须保留(只按词表删除)。""" + # 数据:一条 30s 的正常长台词。 + text = _srt(("00:00:01,000", "00:00:31,000", "这是一段很长的真实独白内容")) + + # 测试过程 + cleaned = clean_srt_text(text) + + # 验证结果 + assert "这是一段很长的真实独白内容" in cleaned + + +def test_threshold_boundary_is_inclusive() -> None: + """阈值边界按"≥ 阈值"删除(严格等于阈值即删除)。""" + # 数据:恰好 15s 的幻觉条目与 14.999s 的同类条目。 + text = _srt( + ("00:00:00,000", "00:00:15,000", "谢谢观看"), + ("00:00:16,000", "00:00:30,999", "感谢观看"), + ) + + # 测试过程 + cleaned = clean_srt_text(text, threshold_seconds=15.0) + + # 验证结果:15s 的被删除,14.999s 的保留。 + kept = [e["text"] for e in parse_srt_entries(cleaned)] + assert kept == ["感谢观看"] + + +def test_resequences_after_middle_removal() -> None: + """删除中间条目后剩余条目从 1 连续编号,保持合法 SRT。""" + # 数据:三条,中间一条是长时幻觉。 + text = _srt( + ("00:00:01,000", "00:00:02,000", "第一条"), + ("00:00:03,000", "00:00:25,000", "谢谢观看"), + ("00:00:26,000", "00:00:27,000", "第三条"), + ) + + # 测试过程 + cleaned = clean_srt_text(text) + + # 验证结果:序号连续且内容为第一、三条。 + assert cleaned.splitlines()[0] == "1" + assert "\n2\n" in cleaned + assert [e["text"] for e in parse_srt_entries(cleaned)] == ["第一条", "第三条"] + + +def test_japanese_hallucination_removed_and_short_kept() -> None: + """日语词表:长时"おやすみなさい"删除,短时保留。""" + # 数据:一条 30s 日语幻觉 + 一条 3s 同词。 + text = _srt( + ("00:01:00,000", "00:01:30,000", "おやすみなさい"), + ("00:02:00,000", "00:02:03,000", "おやすみなさい"), + ) + + # 测试过程 + cleaned = clean_japanese_hallucinations(text) + + # 验证结果:只保留短的那条。 + entries = parse_srt_entries(cleaned) + assert len(entries) == 1 + assert entries[0]["start"] == 120.0 + + +def test_custom_token_list_is_honored() -> None: + """自定义词表生效:只删除传入词命中的条目。""" + # 数据:两个不同的长时条目。 + text = _srt( + ("00:00:01,000", "00:00:20,000", "自定义幻觉词"), + ("00:00:21,000", "00:00:40,000", "谢谢观看"), + ) + + # 测试过程:只传"自定义幻觉词"。 + cleaned = remove_hallucination_entries(text, ("自定义幻觉词",)) + + # 验证结果:只删除自定义词,"谢谢观看"保留。 + assert [e["text"] for e in parse_srt_entries(cleaned)] == ["谢谢观看"] + + +def test_default_tables_are_not_empty() -> None: + """两张默认词表都非空(防止重构时误清空导致清理失效)。""" + # 数据:模块导出的词表常量。 + # 测试过程与验证结果 + assert len(HALLUCINATION_TOKENS) > 0 + assert len(JAPANESE_HALLUCINATION_TOKENS) > 0 + + +# --------------------------------------------------------------------------- +# 短呻吟过滤(decode_full 去噪) +# --------------------------------------------------------------------------- + + +def test_removes_pure_moan_fragments() -> None: + """纯呻吟碎片(あ…/ん?/はぁ…)整条删除。""" + # 数据:三条纯呻吟与一条真实短对话。 + text = _srt( + ("00:00:01,000", "00:00:02,000", "あ…"), + ("00:00:03,000", "00:00:04,000", "ん?"), + ("00:00:05,000", "00:00:06,000", "はぁ…"), + ("00:00:07,000", "00:00:09,000", "そこ、だめ"), + ) + + # 测试过程 + cleaned = remove_short_moan_entries(text) + + # 验证结果:只剩真实短对话。 + assert [e["text"] for e in parse_srt_entries(cleaned)] == ["そこ、だめ"] + + +def test_keeps_real_short_dialogue() -> None: + """含真实假名(そ/や/ね)的短对话不命中判据,必须保留。""" + # 数据:四条真实短对话。 + dialog = ["そこ", "やばい", "ねえ", "やだ"] + + # 测试过程 + cleaned = remove_short_moan_entries(_srt(*[ + (f"00:00:0{i},000", f"00:00:0{i + 1},000", text) + for i, text in enumerate(dialog, 1) + ])) + + # 验证结果:全部保留。 + assert [e["text"] for e in parse_srt_entries(cleaned)] == dialog + + +def test_moan_threshold_boundary() -> None: + """有效假名数超过阈值(默认 3)的纯呻吟串保留,等于阈值的删除。""" + # 数据:3 个假名(删除)与 4 个假名(保留)。 + text = _srt( + ("00:00:01,000", "00:00:02,000", "あんあ"), + ("00:00:03,000", "00:00:04,000", "あんあん"), + ) + + # 测试过程 + cleaned = remove_short_moan_entries(text, max_chars=3) + + # 验证结果 + assert [e["text"] for e in parse_srt_entries(cleaned)] == ["あんあん"] + + +def test_moan_default_max_chars_constant() -> None: + """默认阈值常量为 3(与文档约定一致,改动需同步文档)。""" + # 数据:模块常量。 + # 测试过程与验证结果 + assert DEFAULT_MOAN_MAX_CHARS == 3 + + +def test_moan_filter_can_be_disabled() -> None: + """max_chars=0 时关闭过滤,输入原样返回。""" + # 数据:一条纯呻吟。 + text = _srt(("00:00:01,000", "00:00:02,000", "あ…")) + + # 测试过程与验证结果 + assert remove_short_moan_entries(text, max_chars=0) == text + + +def test_moan_removal_in_middle_resequences() -> None: + """删除中间呻吟后剩余条目序号连续。""" + # 数据:真实对话、呻吟、真实对话。 + text = _srt( + ("00:00:01,000", "00:00:02,000", "行くよ"), + ("00:00:03,000", "00:00:04,000", "ん…"), + ("00:00:05,000", "00:00:06,000", "だめ"), + ) + + # 测试过程 + cleaned = remove_short_moan_entries(text) + + # 验证结果 + assert [e["text"] for e in parse_srt_entries(cleaned)] == ["行くよ", "だめ"] + assert "\n2\n" in cleaned + + +def test_multiline_moan_entry_removed_as_one_cue() -> None: + """多行呻吟条目整体作为一条 cue 删除(不残留半条)。""" + # 数据:两行纯呻吟组成一条 cue。 + text = "1\n00:00:01,000 --> 00:00:03,000\nあ…\nん…\n\n2\n00:00:04,000 --> 00:00:05,000\nそこ\n" + + # 测试过程 + cleaned = remove_short_moan_entries(text) + + # 验证结果:只剩第二条并重编号。 + assert [e["text"] for e in parse_srt_entries(cleaned)] == ["そこ"] + assert cleaned.startswith("1\n") diff --git a/tests/nodes/test_subtitle_correction/__init__.py b/tests/nodes/test_subtitle_correction/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/nodes/test_subtitle_correction/test_correction.py b/tests/nodes/test_subtitle_correction/test_correction.py new file mode 100644 index 0000000..7eee2c8 --- /dev/null +++ b/tests/nodes/test_subtitle_correction/test_correction.py @@ -0,0 +1,416 @@ +"""nodes/subtitle_correction.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`nodes/subtitle_correction.py`(字幕领域纠错:上下文过滤 + LLM +推断误听词),可独立调用。网络属于允许 mock 的 I/O 边界;集成用例调用真实 +LLM 验证误听泛化能力。 +""" + +from __future__ import annotations + +import json +import os +import urllib.error +import urllib.request +from pathlib import Path + +import pytest + +from nodes.subtitle_correction import ( + CONTEXT_WINDOW, + PROPER_SESSION_WORDS, + _build_context, + _extract_target_line, + _is_fragment, + _read_srt_entries, + _serialize_srt, + _system_prompt, + correct_entry, + invoke, +) +from wov_sdk.models import InvokeRequest + +# 模块专用数据目录(真实素材缺失时相关用例跳过)。 +DATA_DIR = Path(__file__).resolve().parent / "data" + + +class _FakeResponse: + """假 HTTP 响应:返回给定的 content 字符串。""" + + def __init__(self, content: str) -> None: + self._payload = { + "choices": [{"message": {"content": content}}], + } + + def read(self) -> bytes: + return json.dumps(self._payload, ensure_ascii=False).encode("utf-8") + + def __enter__(self): + return self + + def __exit__(self, *exc) -> None: + return None + + +def _srt(*cues: tuple[float, float, str]) -> str: + """把 (起始秒, 结束秒, 文本) 拼成标准 SRT 文本。""" + + def fmt(seconds: float) -> str: + hours, rest = divmod(seconds, 3600) + minutes, secs = divmod(rest, 60) + return f"{int(hours):02d}:{int(minutes):02d}:{int(secs):02d},{int(round((secs - int(secs)) * 1000)):03d}" + + return "\n".join( + f"{i}\n{fmt(start)} --> {fmt(end)}\n{text}\n" + for i, (start, end, text) in enumerate(cues, 1) + ) + + +# --------------------------------------------------------------------------- +# 碎片识别与上下文构造 +# --------------------------------------------------------------------------- + + +def test_is_fragment_detects_pure_phrases() -> None: + """纯语气词/单字为碎片;有实义的句子不是。""" + # 数据:碎片与正常句子。 + # 测试过程与验证结果 + assert _is_fragment("あ") is True + assert _is_fragment("ん?") is True + assert _is_fragment("はい") is True + assert _is_fragment("") is True + assert _is_fragment("それじゃあ、始めましょう") is False + + +def test_build_context_filters_fragments_but_keeps_target() -> None: + """上下文过滤语气词碎片,但目标条目始终保留并标记。""" + # 数据:目标条目周围有碎片与正常句。 + entries = [ + {"start": 100.0, "end": 101.0, "text": "あ"}, + {"start": 101.0, "end": 104.0, "text": "それじゃあ、始めましょう"}, + {"start": 104.0, "end": 107.0, "text": "相手がバナナをしゃぶってくれて"}, + {"start": 107.0, "end": 108.0, "text": "ん"}, + ] + + # 测试过程:以索引 2 为目标。 + context = _build_context(entries, 2) + + # 验证结果:碎片不出现,目标带标记,正常上下文保留。 + assert "相手がバナナをしゃぶってくれて <-- 目标" in context + assert "それじゃあ" in context + assert "あ\n" not in context and "[100.00] あ" not in context + + +def test_build_context_respects_time_window() -> None: + """窗口外的条目不进上下文(默认 ±60 秒)。""" + # 数据:目标与远处条目。 + entries = [ + {"start": 0.0, "end": 2.0, "text": "很早之前说的话"}, + {"start": 500.0, "end": 502.0, "text": "目标所在位置"}, + {"start": 1000.0, "end": 1002.0, "text": "很久之后说的话"}, + ] + + # 测试过程 + context = _build_context(entries, 1) + + # 验证结果:只有目标出现。 + assert "目标所在位置" in context + assert "很早之前" not in context + assert "很久之后" not in context + assert CONTEXT_WINDOW == 60 + + +def test_serialize_srt_round_trip() -> None: + """序列化输出合法 SRT(时间戳格式正确、条数一致)。""" + # 数据:两个条目。 + entries = [ + {"start": 1.0, "end": 2.5, "text": "第一句"}, + {"start": 3.0, "end": 4.0, "text": "第二句"}, + ] + + # 测试过程 + text = _serialize_srt(entries) + + # 验证结果:序号、时间戳格式与正文。 + assert text.startswith("1\n00:00:01,000 --> 00:00:02,500\n第一句") + assert "\n2\n" in text + assert text.count("-->") == 2 + + +def test_read_srt_entries_uses_shared_parser(tmp_path: Path) -> None: + """读取真实 SRT 文件返回带秒级时间轴的条目。""" + # 数据:真实文件。 + srt_path = tmp_path / "in.srt" + srt_path.write_text(_srt((1.0, 2.0, "你好")), encoding="utf-8") + + # 测试过程 + entries = _read_srt_entries(srt_path) + + # 验证结果 + assert len(entries) == 1 + assert entries[0]["text"] == "你好" + assert entries[0]["start"] == 1.0 + + +# --------------------------------------------------------------------------- +# 提示词 +# --------------------------------------------------------------------------- + + +def test_system_prompt_contains_domain_terms_and_no_mishearing_examples() -> None: + """系统提示词含领域词表但不含具体误听例子(避免过拟合)。""" + # 数据:目标语言 zh-CN。 + # 测试过程 + prompt = _system_prompt("zh-CN") + + # 验证结果:包含领域词(如 チンポ),且要求按上下文推断而非照搬字面。 + assert "zh-CN" in prompt + assert any(term in prompt for term in PROPER_SESSION_WORDS) + assert "不要机械照搬字面词" in prompt + + +# --------------------------------------------------------------------------- +# correct_entry / _extract_target_line +# --------------------------------------------------------------------------- + + +def test_extract_target_line_matches_by_time() -> None: + """按时间戳匹配目标行(容忍 LLM 输出的编号差异)。""" + # 数据:LLM 输出两行,目标时间 120.50。 + content = "120.00 这是上一句\n120.50 这是目标句\n" + + # 测试过程 + target = _extract_target_line(content, 120.5) + + # 验证结果 + assert target == "这是目标句" + + +def test_extract_target_line_picks_closest_timestamp() -> None: + """多行输出时取时间戳最接近目标的那一行(不依赖行顺序)。""" + # 数据:三行,中间一行最接近 120.50。 + content = "100.00 甲\n120.40 目标句\n200.00 乙\n" + + # 测试过程 + target = _extract_target_line(content, 120.5) + + # 验证结果 + assert target == "目标句" + + +def test_extract_target_line_returns_empty_without_timestamps() -> None: + """输出行不带时间戳前缀时无法定位目标,返回空串(不猜)。""" + # 数据:单行纯译文,无时间戳。 + content = "目标译文\n" + + # 测试过程与验证结果 + assert _extract_target_line(content, 120.5) == "" + + +def test_extract_target_line_ignores_unparseable_lines() -> None: + """无法解析时间戳的行被跳过,仍能取到最接近的可解析行。""" + # 数据:首行无时间戳,次行有。 + content = "这是解释性文字\n120.50 真正的译文\n" + + # 测试过程与验证结果 + assert _extract_target_line(content, 120.5) == "真正的译文" + + +def test_correct_entry_sends_context_and_returns_translation(monkeypatch) -> None: + """correct_entry 把目标前后上下文发给 LLM,返回目标条目译文。""" + # 数据:真实形态的条目列表(含误听词)。 + entries = [ + {"start": 100.0, "end": 103.0, "text": "気持ちいいところに当たってるね"}, + {"start": 103.0, "end": 106.0, "text": "そろそろマンゴーが濡れてきました"}, + ] + captured: dict = {} + + def fake_urlopen(http_request, timeout=None): + captured["body"] = json.loads(http_request.data.decode("utf-8")) + return _FakeResponse("103.00 那里已经湿了呢\n") + + monkeypatch.setenv("LLM_API_KEY", "sk-test") + monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen) + + # 测试过程 + result = correct_entry(entries[1], entries, 1, {"target_language": "zh-CN"}) + + # 验证结果:返回译文,且请求体带上下文与系统提示词。 + assert result == "那里已经湿了呢" + body = captured["body"] + assert body["messages"][0]["role"] == "system" + assert "マンゴー" in body["messages"][1]["content"] + + +def test_correct_entry_uses_default_model_when_not_configured(monkeypatch) -> None: + """未指定模型/环境变量时使用节点自有兜底模型(与全局 LLM_MODEL 解耦)。""" + # 数据:清空环境变量,捕获请求体。 + entries = [{"start": 1.0, "end": 2.0, "text": "テスト"}] + captured: dict = {} + monkeypatch.delenv("LLM_MODEL", raising=False) + monkeypatch.setenv("LLM_API_KEY", "sk-test") + monkeypatch.setattr( + urllib.request, "urlopen", + lambda req, timeout=None: (captured.update(json.loads(req.data.decode("utf-8"))), + _FakeResponse("1.00 测试"))[1], + ) + + # 测试过程 + correct_entry(entries[0], entries, 0, {}) + + # 验证结果:模型名固定为节点兜底(Qwen3.6 系列,有意不跟随全局默认)。 + assert captured["model"] == "Qwen/Qwen3.6-35B-A3B" + + +def test_correct_entry_param_model_wins(monkeypatch) -> None: + """参数 model 优先于兜底值。""" + # 数据:传入自定义模型。 + entries = [{"start": 1.0, "end": 2.0, "text": "テスト"}] + captured: dict = {} + monkeypatch.setenv("LLM_API_KEY", "sk-test") + monkeypatch.setattr( + urllib.request, "urlopen", + lambda req, timeout=None: (captured.update(json.loads(req.data.decode("utf-8"))), + _FakeResponse("1.00 测试"))[1], + ) + + # 测试过程 + correct_entry(entries[0], entries, 0, {"model": "自定义/纠错模型"}) + + # 验证结果 + assert captured["model"] == "自定义/纠错模型" + + +def test_correct_entry_returns_empty_on_network_error(monkeypatch) -> None: + """网络错误时返回空字符串(调用方按"未纠错"处理,不打断整节点)。""" + # 数据:urlopen 抛错。 + entries = [{"start": 1.0, "end": 2.0, "text": "テスト"}] + monkeypatch.setenv("LLM_API_KEY", "sk-test") + + def fake_urlopen(*a, **k): + raise urllib.error.URLError("boom") + + monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen) + + # 测试过程与验证结果 + assert correct_entry(entries[0], entries, 0, {}) == "" + + +# --------------------------------------------------------------------------- +# invoke 全流程 +# --------------------------------------------------------------------------- + + +def test_invoke_writes_corrected_srt(monkeypatch, tmp_path: Path) -> None: + """invoke 读取 SRT、逐条纠错并写出 corrected.srt。""" + # 数据:两条字幕,LLM 每次都返回目标译文。 + srt_path = tmp_path / "asr.srt" + srt_path.write_text(_srt((1.0, 2.0, "こんにちは"), (3.0, 4.0, "さようなら")), encoding="utf-8") + monkeypatch.setenv("LLM_API_KEY", "sk-test") + + def fake_urlopen(http_request, timeout=None): + body = json.loads(http_request.data.decode("utf-8")) + # 回显目标时间戳(模拟真实模型按约定格式输出)。 + user = body["messages"][1]["content"] + stamp = user.split("[")[1].split("]")[0].strip() + return _FakeResponse(f"{stamp} 纠错后的译文\n") + + monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen) + request = InvokeRequest( + run_id="r", node_instance_id="n", params={}, + inputs={"srt_uri": str(srt_path)}, output_dir=str(tmp_path / "out"), + ) + + # 测试过程 + response = invoke(request) + + # 验证结果:产物存在,时间轴保留,正文被替换。 + assert response.status == "completed", response.error + content = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8") + assert "00:00:01,000 --> 00:00:02,000" in content + assert "纠错后的译文" in content + assert content.count("-->") == 2 + + +def test_invoke_keeps_original_when_correction_empty(monkeypatch, tmp_path: Path) -> None: + """纠错返回空时保留原始文本(不产出空字幕)。""" + # 数据:LLM 返回空内容。 + srt_path = tmp_path / "asr.srt" + srt_path.write_text(_srt((1.0, 2.0, "原文内容")), encoding="utf-8") + monkeypatch.setenv("LLM_API_KEY", "sk-test") + monkeypatch.setattr( + urllib.request, "urlopen", + lambda *a, **k: _FakeResponse(""), + ) + request = InvokeRequest( + run_id="r", node_instance_id="n", params={}, + inputs={"srt_uri": str(srt_path)}, output_dir=str(tmp_path / "out"), + ) + + # 测试过程 + response = invoke(request) + + # 验证结果 + assert response.status == "completed" + content = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8") + assert "原文内容" in content + + +def test_invoke_fails_without_input(tmp_path: Path) -> None: + """缺少 srt_uri 时失败。""" + # 数据:空输入。 + request = InvokeRequest( + run_id="r", node_instance_id="n", params={}, inputs={}, output_dir=str(tmp_path) + ) + + # 测试过程 + response = invoke(request) + + # 验证结果 + assert response.status == "failed" + assert "srt_uri" in (response.error or "") + + +def test_invoke_fails_when_input_missing(tmp_path: Path) -> None: + """输入文件不存在时失败。""" + # 数据:不存在的路径。 + request = InvokeRequest( + run_id="r", node_instance_id="n", params={}, + inputs={"srt_uri": str(tmp_path / "nope.srt")}, output_dir=str(tmp_path), + ) + + # 测试过程 + response = invoke(request) + + # 验证结果 + assert response.status == "failed" + assert "not found" in (response.error or "") + + +# --------------------------------------------------------------------------- +# 真实 LLM 集成:误听泛化 +# --------------------------------------------------------------------------- + + +@pytest.mark.integration +def test_real_llm_generalizes_to_unseen_mishearing() -> None: + """真实 LLM 校准:未在提示词中出现的误听词也能结合上下文正确推断。""" + # 数据:模拟 ASR 把 チンポ/マンコ 听成 バナナ/マンゴー。 + from tests.shared.llm_service import probe_llm_or_skip + + # 先探针真实服务:不可用(无 Key / 余额 / 限流)时跳过,避免把外部 + # 状态问题误判成"模型未泛化"(correct_entry 会把调用异常吞成空串)。 + probe_llm_or_skip() + entries = [ + {"start": 1200.0, "end": 1203.0, "text": "相手がバナナをしゃぶってくれて"}, + {"start": 1203.0, "end": 1206.0, "text": "そろそろマンゴーが濡れてきました"}, + {"start": 1206.0, "end": 1209.0, "text": "気持ちいいところに当たってるね"}, + {"start": 1220.0, "end": 1224.0, "text": "もっとマンゴーを舐めてください"}, + {"start": 1224.0, "end": 1226.0, "text": "いっぱい出してね"}, + ] + + # 测试过程:对含误听词的第二条纠错。 + target = correct_entry(entries[1], entries, 1, {"target_language": "zh-CN"}) + + # 验证结果:输出体现性器官语义而非字面"芒果"。 + flagged = [k for k in ("肉棒", "鸡巴", "阴部", "小穴", "敏感", "那里", "湿") if k in target] + assert flagged, f"泛化失败:模型仍字面直译,输出'{target}'" diff --git a/tests/nodes/test_subtitle_ocr/__init__.py b/tests/nodes/test_subtitle_ocr/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/nodes/test_subtitle_ocr/data/frames_manifest_full.json b/tests/nodes/test_subtitle_ocr/data/frames_manifest_full.json new file mode 100644 index 0000000..5ff7c11 --- /dev/null +++ b/tests/nodes/test_subtitle_ocr/data/frames_manifest_full.json @@ -0,0 +1 @@ +[{"time": 0.0, "image_uri": "frame_0001.png"}, {"time": 0.507, "image_uri": "frame_0002.png"}, {"time": 1.015, "image_uri": "frame_0003.png"}, {"time": 1.522, "image_uri": "frame_0004.png"}, {"time": 2.03, "image_uri": "frame_0005.png"}, {"time": 2.537, "image_uri": "frame_0006.png"}, {"time": 3.045, "image_uri": "frame_0007.png"}, {"time": 3.552, "image_uri": "frame_0008.png"}, {"time": 4.06, "image_uri": "frame_0009.png"}, {"time": 4.567, "image_uri": "frame_0010.png"}, {"time": 5.074, "image_uri": "frame_0011.png"}, {"time": 5.582, "image_uri": "frame_0012.png"}, {"time": 6.089, "image_uri": "frame_0013.png"}, {"time": 6.597, "image_uri": "frame_0014.png"}, {"time": 7.104, "image_uri": "frame_0015.png"}, {"time": 7.612, "image_uri": "frame_0016.png"}, {"time": 8.119, "image_uri": "frame_0017.png"}, {"time": 8.627, "image_uri": "frame_0018.png"}, {"time": 9.134, "image_uri": "frame_0019.png"}, {"time": 9.641, "image_uri": "frame_0020.png"}, {"time": 10.149, "image_uri": "frame_0021.png"}, {"time": 10.656, "image_uri": "frame_0022.png"}, {"time": 11.164, "image_uri": "frame_0023.png"}, {"time": 11.671, "image_uri": "frame_0024.png"}, {"time": 12.179, "image_uri": "frame_0025.png"}, {"time": 12.686, "image_uri": "frame_0026.png"}, {"time": 13.194, "image_uri": "frame_0027.png"}, {"time": 13.701, "image_uri": "frame_0028.png"}, {"time": 14.208, "image_uri": "frame_0029.png"}, {"time": 14.716, "image_uri": "frame_0030.png"}, {"time": 15.223, "image_uri": "frame_0031.png"}, {"time": 15.731, "image_uri": "frame_0032.png"}, {"time": 16.238, "image_uri": "frame_0033.png"}, {"time": 16.746, "image_uri": "frame_0034.png"}, {"time": 17.253, "image_uri": "frame_0035.png"}, {"time": 17.76, "image_uri": "frame_0036.png"}, {"time": 18.268, "image_uri": "frame_0037.png"}, {"time": 18.775, "image_uri": "frame_0038.png"}, {"time": 19.283, "image_uri": "frame_0039.png"}, {"time": 19.79, "image_uri": "frame_0040.png"}, {"time": 20.298, "image_uri": "frame_0041.png"}, {"time": 20.805, "image_uri": "frame_0042.png"}, {"time": 21.313, "image_uri": "frame_0043.png"}, {"time": 21.82, "image_uri": "frame_0044.png"}, {"time": 22.327, "image_uri": "frame_0045.png"}, {"time": 22.835, "image_uri": "frame_0046.png"}, {"time": 23.342, "image_uri": "frame_0047.png"}, {"time": 23.85, "image_uri": "frame_0048.png"}, {"time": 24.357, "image_uri": "frame_0049.png"}, {"time": 24.865, "image_uri": "frame_0050.png"}, {"time": 25.372, "image_uri": "frame_0051.png"}, {"time": 25.88, "image_uri": "frame_0052.png"}, {"time": 26.387, "image_uri": "frame_0053.png"}, {"time": 26.894, "image_uri": "frame_0054.png"}, {"time": 27.402, "image_uri": "frame_0055.png"}, {"time": 27.909, "image_uri": "frame_0056.png"}, {"time": 28.417, "image_uri": "frame_0057.png"}, {"time": 28.924, "image_uri": "frame_0058.png"}, {"time": 29.432, "image_uri": "frame_0059.png"}, {"time": 29.939, "image_uri": "frame_0060.png"}, {"time": 30.447, "image_uri": "frame_0061.png"}, {"time": 30.954, "image_uri": "frame_0062.png"}, {"time": 31.461, "image_uri": "frame_0063.png"}, {"time": 31.969, "image_uri": "frame_0064.png"}, {"time": 32.476, "image_uri": "frame_0065.png"}, {"time": 32.984, "image_uri": "frame_0066.png"}, {"time": 33.491, "image_uri": "frame_0067.png"}, {"time": 33.999, "image_uri": "frame_0068.png"}, {"time": 34.506, "image_uri": "frame_0069.png"}, {"time": 35.014, "image_uri": "frame_0070.png"}, {"time": 35.521, "image_uri": "frame_0071.png"}, {"time": 36.028, "image_uri": "frame_0072.png"}, {"time": 36.536, "image_uri": "frame_0073.png"}, {"time": 37.043, "image_uri": "frame_0074.png"}, {"time": 37.551, "image_uri": "frame_0075.png"}, {"time": 38.058, "image_uri": "frame_0076.png"}, {"time": 38.566, "image_uri": "frame_0077.png"}, {"time": 39.073, "image_uri": "frame_0078.png"}, {"time": 39.581, "image_uri": "frame_0079.png"}, {"time": 40.088, "image_uri": "frame_0080.png"}, {"time": 40.595, "image_uri": "frame_0081.png"}, {"time": 41.103, "image_uri": "frame_0082.png"}, {"time": 41.61, "image_uri": "frame_0083.png"}, {"time": 42.118, "image_uri": "frame_0084.png"}, {"time": 42.625, "image_uri": "frame_0085.png"}, {"time": 43.133, "image_uri": "frame_0086.png"}, {"time": 43.64, "image_uri": "frame_0087.png"}, {"time": 44.147, "image_uri": "frame_0088.png"}, {"time": 44.655, "image_uri": "frame_0089.png"}, {"time": 45.162, "image_uri": "frame_0090.png"}, {"time": 45.67, "image_uri": "frame_0091.png"}, {"time": 46.177, "image_uri": "frame_0092.png"}, {"time": 46.685, "image_uri": "frame_0093.png"}, {"time": 47.192, "image_uri": "frame_0094.png"}, {"time": 47.7, "image_uri": "frame_0095.png"}, {"time": 48.207, "image_uri": "frame_0096.png"}, {"time": 48.714, "image_uri": "frame_0097.png"}, {"time": 49.222, "image_uri": "frame_0098.png"}, {"time": 49.729, "image_uri": "frame_0099.png"}, {"time": 50.237, "image_uri": "frame_0100.png"}, {"time": 50.744, "image_uri": "frame_0101.png"}, {"time": 51.252, "image_uri": "frame_0102.png"}, {"time": 51.759, "image_uri": "frame_0103.png"}, {"time": 52.267, "image_uri": "frame_0104.png"}, {"time": 52.774, "image_uri": "frame_0105.png"}, {"time": 53.281, "image_uri": "frame_0106.png"}, {"time": 53.789, "image_uri": "frame_0107.png"}, {"time": 54.296, "image_uri": "frame_0108.png"}, {"time": 54.804, "image_uri": "frame_0109.png"}, {"time": 55.311, "image_uri": "frame_0110.png"}, {"time": 55.819, "image_uri": "frame_0111.png"}, {"time": 56.326, "image_uri": "frame_0112.png"}, {"time": 56.834, "image_uri": "frame_0113.png"}, {"time": 57.341, "image_uri": "frame_0114.png"}, {"time": 57.848, "image_uri": "frame_0115.png"}, {"time": 58.356, "image_uri": "frame_0116.png"}, {"time": 58.863, "image_uri": "frame_0117.png"}, {"time": 59.371, "image_uri": "frame_0118.png"}, {"time": 59.878, "image_uri": "frame_0119.png"}, {"time": 60.386, "image_uri": "frame_0120.png"}, {"time": 60.893, "image_uri": "frame_0121.png"}, {"time": 61.401, "image_uri": "frame_0122.png"}, {"time": 61.908, "image_uri": "frame_0123.png"}, {"time": 62.415, "image_uri": "frame_0124.png"}, {"time": 62.923, "image_uri": "frame_0125.png"}, {"time": 63.43, "image_uri": "frame_0126.png"}, {"time": 63.938, "image_uri": "frame_0127.png"}, {"time": 64.445, "image_uri": "frame_0128.png"}, {"time": 64.953, "image_uri": "frame_0129.png"}, {"time": 65.46, "image_uri": "frame_0130.png"}, {"time": 65.968, "image_uri": "frame_0131.png"}, {"time": 66.475, "image_uri": "frame_0132.png"}, {"time": 66.982, "image_uri": "frame_0133.png"}, {"time": 67.49, "image_uri": "frame_0134.png"}, {"time": 67.997, "image_uri": "frame_0135.png"}, {"time": 68.505, "image_uri": "frame_0136.png"}, {"time": 69.012, "image_uri": "frame_0137.png"}, {"time": 69.52, "image_uri": "frame_0138.png"}, {"time": 70.027, "image_uri": "frame_0139.png"}, {"time": 70.535, "image_uri": "frame_0140.png"}, {"time": 71.042, "image_uri": "frame_0141.png"}, {"time": 71.549, "image_uri": "frame_0142.png"}, {"time": 72.057, "image_uri": "frame_0143.png"}, {"time": 72.564, "image_uri": "frame_0144.png"}, {"time": 73.072, "image_uri": "frame_0145.png"}, {"time": 73.579, "image_uri": "frame_0146.png"}, {"time": 74.087, "image_uri": "frame_0147.png"}, {"time": 74.594, "image_uri": "frame_0148.png"}, {"time": 75.101, "image_uri": "frame_0149.png"}, {"time": 75.609, "image_uri": "frame_0150.png"}, {"time": 76.116, "image_uri": "frame_0151.png"}, {"time": 76.624, "image_uri": "frame_0152.png"}, {"time": 77.131, "image_uri": "frame_0153.png"}, {"time": 77.639, "image_uri": "frame_0154.png"}, {"time": 78.146, "image_uri": "frame_0155.png"}, {"time": 78.654, "image_uri": "frame_0156.png"}, {"time": 79.161, "image_uri": "frame_0157.png"}, {"time": 79.668, "image_uri": "frame_0158.png"}, {"time": 80.176, "image_uri": "frame_0159.png"}, {"time": 80.683, "image_uri": "frame_0160.png"}, {"time": 81.191, "image_uri": "frame_0161.png"}, {"time": 81.698, "image_uri": "frame_0162.png"}, {"time": 82.206, "image_uri": "frame_0163.png"}, {"time": 82.713, "image_uri": "frame_0164.png"}, {"time": 83.221, "image_uri": "frame_0165.png"}, {"time": 83.728, "image_uri": "frame_0166.png"}, {"time": 84.235, "image_uri": "frame_0167.png"}, {"time": 84.743, "image_uri": "frame_0168.png"}, {"time": 85.25, "image_uri": "frame_0169.png"}, {"time": 85.758, "image_uri": "frame_0170.png"}, {"time": 86.265, "image_uri": "frame_0171.png"}, {"time": 86.773, "image_uri": "frame_0172.png"}, {"time": 87.28, "image_uri": "frame_0173.png"}, {"time": 87.788, "image_uri": "frame_0174.png"}, {"time": 88.295, "image_uri": "frame_0175.png"}, {"time": 88.802, "image_uri": "frame_0176.png"}, {"time": 89.31, "image_uri": "frame_0177.png"}, {"time": 89.817, "image_uri": "frame_0178.png"}, {"time": 90.325, "image_uri": "frame_0179.png"}, {"time": 90.832, "image_uri": "frame_0180.png"}, {"time": 91.34, "image_uri": "frame_0181.png"}, {"time": 91.847, "image_uri": "frame_0182.png"}, {"time": 92.355, "image_uri": "frame_0183.png"}, {"time": 92.862, "image_uri": "frame_0184.png"}, {"time": 93.369, "image_uri": "frame_0185.png"}, {"time": 93.877, "image_uri": "frame_0186.png"}, {"time": 94.384, "image_uri": "frame_0187.png"}, {"time": 94.892, "image_uri": "frame_0188.png"}, {"time": 95.399, "image_uri": "frame_0189.png"}, {"time": 95.907, "image_uri": "frame_0190.png"}, {"time": 96.414, "image_uri": "frame_0191.png"}, {"time": 96.922, "image_uri": "frame_0192.png"}, {"time": 97.429, "image_uri": "frame_0193.png"}, {"time": 97.936, "image_uri": "frame_0194.png"}, {"time": 98.444, "image_uri": "frame_0195.png"}, {"time": 98.951, "image_uri": "frame_0196.png"}, {"time": 99.459, "image_uri": "frame_0197.png"}, {"time": 99.966, "image_uri": "frame_0198.png"}, {"time": 100.474, "image_uri": "frame_0199.png"}, {"time": 100.981, "image_uri": "frame_0200.png"}, {"time": 101.488, "image_uri": "frame_0201.png"}, {"time": 101.996, "image_uri": "frame_0202.png"}, {"time": 102.503, "image_uri": "frame_0203.png"}, {"time": 103.011, "image_uri": "frame_0204.png"}, {"time": 103.518, "image_uri": "frame_0205.png"}, {"time": 104.026, "image_uri": "frame_0206.png"}, {"time": 104.533, "image_uri": "frame_0207.png"}, {"time": 105.041, "image_uri": "frame_0208.png"}, {"time": 105.548, "image_uri": "frame_0209.png"}, {"time": 106.055, "image_uri": "frame_0210.png"}, {"time": 106.563, "image_uri": "frame_0211.png"}, {"time": 107.07, "image_uri": "frame_0212.png"}, {"time": 107.578, "image_uri": "frame_0213.png"}, {"time": 108.085, "image_uri": "frame_0214.png"}, {"time": 108.593, "image_uri": "frame_0215.png"}, {"time": 109.1, "image_uri": "frame_0216.png"}, {"time": 109.608, "image_uri": "frame_0217.png"}, {"time": 110.115, "image_uri": "frame_0218.png"}, {"time": 110.622, "image_uri": "frame_0219.png"}, {"time": 111.13, "image_uri": "frame_0220.png"}, {"time": 111.637, "image_uri": "frame_0221.png"}, {"time": 112.145, "image_uri": "frame_0222.png"}, {"time": 112.652, "image_uri": "frame_0223.png"}, {"time": 113.16, "image_uri": "frame_0224.png"}, {"time": 113.667, "image_uri": "frame_0225.png"}, {"time": 114.175, "image_uri": "frame_0226.png"}, {"time": 114.682, "image_uri": "frame_0227.png"}, {"time": 115.189, "image_uri": "frame_0228.png"}, {"time": 115.697, "image_uri": "frame_0229.png"}, {"time": 116.204, "image_uri": "frame_0230.png"}, {"time": 116.712, "image_uri": "frame_0231.png"}, {"time": 117.219, "image_uri": "frame_0232.png"}, {"time": 117.727, "image_uri": "frame_0233.png"}, {"time": 118.234, "image_uri": "frame_0234.png"}, {"time": 118.742, "image_uri": "frame_0235.png"}, {"time": 119.249, "image_uri": "frame_0236.png"}, {"time": 119.756, "image_uri": "frame_0237.png"}, {"time": 120.264, "image_uri": "frame_0238.png"}, {"time": 120.771, "image_uri": "frame_0239.png"}, {"time": 121.279, "image_uri": "frame_0240.png"}, {"time": 121.786, "image_uri": "frame_0241.png"}, {"time": 122.294, "image_uri": "frame_0242.png"}, {"time": 122.801, "image_uri": "frame_0243.png"}, {"time": 123.309, "image_uri": "frame_0244.png"}, {"time": 123.816, "image_uri": "frame_0245.png"}, {"time": 124.323, "image_uri": "frame_0246.png"}, {"time": 124.831, "image_uri": "frame_0247.png"}, {"time": 125.338, "image_uri": "frame_0248.png"}, {"time": 125.846, "image_uri": "frame_0249.png"}, {"time": 126.353, "image_uri": "frame_0250.png"}, {"time": 126.861, "image_uri": "frame_0251.png"}, {"time": 127.368, "image_uri": "frame_0252.png"}, {"time": 127.876, "image_uri": "frame_0253.png"}, {"time": 128.383, "image_uri": "frame_0254.png"}, {"time": 128.89, "image_uri": "frame_0255.png"}, {"time": 129.398, "image_uri": "frame_0256.png"}, {"time": 129.905, "image_uri": "frame_0257.png"}, {"time": 130.413, "image_uri": "frame_0258.png"}, {"time": 130.92, "image_uri": "frame_0259.png"}, {"time": 131.428, "image_uri": "frame_0260.png"}, {"time": 131.935, "image_uri": "frame_0261.png"}, {"time": 132.442, "image_uri": "frame_0262.png"}, {"time": 132.95, "image_uri": "frame_0263.png"}, {"time": 133.457, "image_uri": "frame_0264.png"}, {"time": 133.965, "image_uri": "frame_0265.png"}, {"time": 134.472, "image_uri": "frame_0266.png"}, {"time": 134.98, "image_uri": "frame_0267.png"}, {"time": 135.487, "image_uri": "frame_0268.png"}, {"time": 135.995, "image_uri": "frame_0269.png"}, {"time": 136.502, "image_uri": "frame_0270.png"}, {"time": 137.009, "image_uri": "frame_0271.png"}, {"time": 137.517, "image_uri": "frame_0272.png"}, {"time": 138.024, "image_uri": "frame_0273.png"}, {"time": 138.532, "image_uri": "frame_0274.png"}, {"time": 139.039, "image_uri": "frame_0275.png"}, {"time": 139.547, "image_uri": "frame_0276.png"}, {"time": 140.054, "image_uri": "frame_0277.png"}, {"time": 140.562, "image_uri": "frame_0278.png"}, {"time": 141.069, "image_uri": "frame_0279.png"}, {"time": 141.576, "image_uri": "frame_0280.png"}, {"time": 142.084, "image_uri": "frame_0281.png"}, {"time": 142.591, "image_uri": "frame_0282.png"}, {"time": 143.099, "image_uri": "frame_0283.png"}, {"time": 143.606, "image_uri": "frame_0284.png"}, {"time": 144.114, "image_uri": "frame_0285.png"}, {"time": 144.621, "image_uri": "frame_0286.png"}, {"time": 145.129, "image_uri": "frame_0287.png"}, {"time": 145.636, "image_uri": "frame_0288.png"}, {"time": 146.143, "image_uri": "frame_0289.png"}, {"time": 146.651, "image_uri": "frame_0290.png"}, {"time": 147.158, "image_uri": "frame_0291.png"}, {"time": 147.666, "image_uri": "frame_0292.png"}, {"time": 148.173, "image_uri": "frame_0293.png"}, {"time": 148.681, "image_uri": "frame_0294.png"}, {"time": 149.188, "image_uri": "frame_0295.png"}, {"time": 149.696, "image_uri": "frame_0296.png"}, {"time": 150.203, "image_uri": "frame_0297.png"}, {"time": 150.71, "image_uri": "frame_0298.png"}, {"time": 151.218, "image_uri": "frame_0299.png"}, {"time": 151.725, "image_uri": "frame_0300.png"}, {"time": 152.233, "image_uri": "frame_0301.png"}, {"time": 152.74, "image_uri": "frame_0302.png"}, {"time": 153.248, "image_uri": "frame_0303.png"}, {"time": 153.755, "image_uri": "frame_0304.png"}, {"time": 154.263, "image_uri": "frame_0305.png"}, {"time": 154.77, "image_uri": "frame_0306.png"}, {"time": 155.277, "image_uri": "frame_0307.png"}, {"time": 155.785, "image_uri": "frame_0308.png"}, {"time": 156.292, "image_uri": "frame_0309.png"}, {"time": 156.8, "image_uri": "frame_0310.png"}, {"time": 157.307, "image_uri": "frame_0311.png"}, {"time": 157.815, "image_uri": "frame_0312.png"}, {"time": 158.322, "image_uri": "frame_0313.png"}, {"time": 158.829, "image_uri": "frame_0314.png"}, {"time": 159.337, "image_uri": "frame_0315.png"}, {"time": 159.844, "image_uri": "frame_0316.png"}, {"time": 160.352, "image_uri": "frame_0317.png"}, {"time": 160.859, "image_uri": "frame_0318.png"}, {"time": 161.367, "image_uri": "frame_0319.png"}, {"time": 161.874, "image_uri": "frame_0320.png"}, {"time": 162.382, "image_uri": "frame_0321.png"}, {"time": 162.889, "image_uri": "frame_0322.png"}, {"time": 163.396, "image_uri": "frame_0323.png"}, {"time": 163.904, "image_uri": "frame_0324.png"}, {"time": 164.411, "image_uri": "frame_0325.png"}, {"time": 164.919, "image_uri": "frame_0326.png"}, {"time": 165.426, "image_uri": "frame_0327.png"}, {"time": 165.934, "image_uri": "frame_0328.png"}, {"time": 166.441, "image_uri": "frame_0329.png"}, {"time": 166.949, "image_uri": "frame_0330.png"}, {"time": 167.456, "image_uri": "frame_0331.png"}, {"time": 167.963, "image_uri": "frame_0332.png"}, {"time": 168.471, "image_uri": "frame_0333.png"}, {"time": 168.978, "image_uri": "frame_0334.png"}, {"time": 169.486, "image_uri": "frame_0335.png"}, {"time": 169.993, "image_uri": "frame_0336.png"}, {"time": 170.501, "image_uri": "frame_0337.png"}, {"time": 171.008, "image_uri": "frame_0338.png"}, {"time": 171.516, "image_uri": "frame_0339.png"}, {"time": 172.023, "image_uri": "frame_0340.png"}, {"time": 172.53, "image_uri": "frame_0341.png"}, {"time": 173.038, "image_uri": "frame_0342.png"}, {"time": 173.545, "image_uri": "frame_0343.png"}, {"time": 174.053, "image_uri": "frame_0344.png"}, {"time": 174.56, "image_uri": "frame_0345.png"}, {"time": 175.068, "image_uri": "frame_0346.png"}, {"time": 175.575, "image_uri": "frame_0347.png"}, {"time": 176.083, "image_uri": "frame_0348.png"}, {"time": 176.59, "image_uri": "frame_0349.png"}, {"time": 177.097, "image_uri": "frame_0350.png"}, {"time": 177.605, "image_uri": "frame_0351.png"}, {"time": 178.112, "image_uri": "frame_0352.png"}, {"time": 178.62, "image_uri": "frame_0353.png"}, {"time": 179.127, "image_uri": "frame_0354.png"}, {"time": 179.635, "image_uri": "frame_0355.png"}, {"time": 180.142, "image_uri": "frame_0356.png"}, {"time": 180.65, "image_uri": "frame_0357.png"}, {"time": 181.157, "image_uri": "frame_0358.png"}, {"time": 181.664, "image_uri": "frame_0359.png"}, {"time": 182.172, "image_uri": "frame_0360.png"}, {"time": 182.679, "image_uri": "frame_0361.png"}, {"time": 183.187, "image_uri": "frame_0362.png"}, {"time": 183.694, "image_uri": "frame_0363.png"}, {"time": 184.202, "image_uri": "frame_0364.png"}, {"time": 184.709, "image_uri": "frame_0365.png"}, {"time": 185.217, "image_uri": "frame_0366.png"}, {"time": 185.724, "image_uri": "frame_0367.png"}, {"time": 186.231, "image_uri": "frame_0368.png"}, {"time": 186.739, "image_uri": "frame_0369.png"}, {"time": 187.246, "image_uri": "frame_0370.png"}, {"time": 187.754, "image_uri": "frame_0371.png"}, {"time": 188.261, "image_uri": "frame_0372.png"}, {"time": 188.769, "image_uri": "frame_0373.png"}, {"time": 189.276, "image_uri": "frame_0374.png"}, {"time": 189.783, "image_uri": "frame_0375.png"}, {"time": 190.291, "image_uri": "frame_0376.png"}, {"time": 190.798, "image_uri": "frame_0377.png"}, {"time": 191.306, "image_uri": "frame_0378.png"}, {"time": 191.813, "image_uri": "frame_0379.png"}, {"time": 192.321, "image_uri": "frame_0380.png"}, {"time": 192.828, "image_uri": "frame_0381.png"}, {"time": 193.336, "image_uri": "frame_0382.png"}, {"time": 193.843, "image_uri": "frame_0383.png"}, {"time": 194.35, "image_uri": "frame_0384.png"}, {"time": 194.858, "image_uri": "frame_0385.png"}, {"time": 195.365, "image_uri": "frame_0386.png"}, {"time": 195.873, "image_uri": "frame_0387.png"}, {"time": 196.38, "image_uri": "frame_0388.png"}, {"time": 196.888, "image_uri": "frame_0389.png"}, {"time": 197.395, "image_uri": "frame_0390.png"}, {"time": 197.903, "image_uri": "frame_0391.png"}, {"time": 198.41, "image_uri": "frame_0392.png"}, {"time": 198.917, "image_uri": "frame_0393.png"}, {"time": 199.425, "image_uri": "frame_0394.png"}, {"time": 199.932, "image_uri": "frame_0395.png"}, {"time": 200.44, "image_uri": "frame_0396.png"}, {"time": 200.947, "image_uri": "frame_0397.png"}, {"time": 201.455, "image_uri": "frame_0398.png"}, {"time": 201.962, "image_uri": "frame_0399.png"}, {"time": 202.47, "image_uri": "frame_0400.png"}, {"time": 202.977, "image_uri": "frame_0401.png"}, {"time": 203.484, "image_uri": "frame_0402.png"}, {"time": 203.992, "image_uri": "frame_0403.png"}, {"time": 204.499, "image_uri": "frame_0404.png"}, {"time": 205.007, "image_uri": "frame_0405.png"}, {"time": 205.514, "image_uri": "frame_0406.png"}, {"time": 206.022, "image_uri": "frame_0407.png"}, {"time": 206.529, "image_uri": "frame_0408.png"}, {"time": 207.037, "image_uri": "frame_0409.png"}, {"time": 207.544, "image_uri": "frame_0410.png"}, {"time": 208.051, "image_uri": "frame_0411.png"}, {"time": 208.559, "image_uri": "frame_0412.png"}, {"time": 209.066, "image_uri": "frame_0413.png"}, {"time": 209.574, "image_uri": "frame_0414.png"}, {"time": 210.081, "image_uri": "frame_0415.png"}, {"time": 210.589, "image_uri": "frame_0416.png"}, {"time": 211.096, "image_uri": "frame_0417.png"}, {"time": 211.604, "image_uri": "frame_0418.png"}, {"time": 212.111, "image_uri": "frame_0419.png"}, {"time": 212.618, "image_uri": "frame_0420.png"}, {"time": 213.126, "image_uri": "frame_0421.png"}, {"time": 213.633, "image_uri": "frame_0422.png"}, {"time": 214.141, "image_uri": "frame_0423.png"}, {"time": 214.648, "image_uri": "frame_0424.png"}, {"time": 215.156, "image_uri": "frame_0425.png"}, {"time": 215.663, "image_uri": "frame_0426.png"}, {"time": 216.171, "image_uri": "frame_0427.png"}, {"time": 216.678, "image_uri": "frame_0428.png"}, {"time": 217.185, "image_uri": "frame_0429.png"}, {"time": 217.693, "image_uri": "frame_0430.png"}, {"time": 218.2, "image_uri": "frame_0431.png"}, {"time": 218.708, "image_uri": "frame_0432.png"}, {"time": 219.215, "image_uri": "frame_0433.png"}, {"time": 219.723, "image_uri": "frame_0434.png"}, {"time": 220.23, "image_uri": "frame_0435.png"}, {"time": 220.737, "image_uri": "frame_0436.png"}, {"time": 221.245, "image_uri": "frame_0437.png"}, {"time": 221.752, "image_uri": "frame_0438.png"}, {"time": 222.26, "image_uri": "frame_0439.png"}, {"time": 222.767, "image_uri": "frame_0440.png"}, {"time": 223.275, "image_uri": "frame_0441.png"}, {"time": 223.782, "image_uri": "frame_0442.png"}, {"time": 224.29, "image_uri": "frame_0443.png"}, {"time": 224.797, "image_uri": "frame_0444.png"}, {"time": 225.304, "image_uri": "frame_0445.png"}, {"time": 225.812, "image_uri": "frame_0446.png"}, {"time": 226.319, "image_uri": "frame_0447.png"}, {"time": 226.827, "image_uri": "frame_0448.png"}, {"time": 227.334, "image_uri": "frame_0449.png"}, {"time": 227.842, "image_uri": "frame_0450.png"}, {"time": 228.349, "image_uri": "frame_0451.png"}, {"time": 228.857, "image_uri": "frame_0452.png"}, {"time": 229.364, "image_uri": "frame_0453.png"}, {"time": 229.871, "image_uri": "frame_0454.png"}, {"time": 230.379, "image_uri": "frame_0455.png"}, {"time": 230.886, "image_uri": "frame_0456.png"}, {"time": 231.394, "image_uri": "frame_0457.png"}, {"time": 231.901, "image_uri": "frame_0458.png"}, {"time": 232.409, "image_uri": "frame_0459.png"}, {"time": 232.916, "image_uri": "frame_0460.png"}, {"time": 233.424, "image_uri": "frame_0461.png"}, {"time": 233.931, "image_uri": "frame_0462.png"}, {"time": 234.438, "image_uri": "frame_0463.png"}, {"time": 234.946, "image_uri": "frame_0464.png"}, {"time": 235.453, "image_uri": "frame_0465.png"}, {"time": 235.961, "image_uri": "frame_0466.png"}, {"time": 236.468, "image_uri": "frame_0467.png"}, {"time": 236.976, "image_uri": "frame_0468.png"}, {"time": 237.483, "image_uri": "frame_0469.png"}, {"time": 237.991, "image_uri": "frame_0470.png"}, {"time": 238.498, "image_uri": "frame_0471.png"}, {"time": 239.005, "image_uri": "frame_0472.png"}, {"time": 239.513, "image_uri": "frame_0473.png"}, {"time": 240.02, "image_uri": "frame_0474.png"}, {"time": 240.528, "image_uri": "frame_0475.png"}, {"time": 241.035, "image_uri": "frame_0476.png"}, {"time": 241.543, "image_uri": "frame_0477.png"}, {"time": 242.05, "image_uri": "frame_0478.png"}, {"time": 242.558, "image_uri": "frame_0479.png"}, {"time": 243.065, "image_uri": "frame_0480.png"}, {"time": 243.572, "image_uri": "frame_0481.png"}, {"time": 244.08, "image_uri": "frame_0482.png"}, {"time": 244.587, "image_uri": "frame_0483.png"}, {"time": 245.095, "image_uri": "frame_0484.png"}, {"time": 245.602, "image_uri": "frame_0485.png"}, {"time": 246.11, "image_uri": "frame_0486.png"}, {"time": 246.617, "image_uri": "frame_0487.png"}, {"time": 247.124, "image_uri": "frame_0488.png"}, {"time": 247.632, "image_uri": "frame_0489.png"}, {"time": 248.139, "image_uri": "frame_0490.png"}, {"time": 248.647, "image_uri": "frame_0491.png"}, {"time": 249.154, "image_uri": "frame_0492.png"}, {"time": 249.662, "image_uri": "frame_0493.png"}, {"time": 250.169, "image_uri": "frame_0494.png"}, {"time": 250.677, "image_uri": "frame_0495.png"}, {"time": 251.184, "image_uri": "frame_0496.png"}, {"time": 251.691, "image_uri": "frame_0497.png"}, {"time": 252.199, "image_uri": "frame_0498.png"}, {"time": 252.706, "image_uri": "frame_0499.png"}, {"time": 253.214, "image_uri": "frame_0500.png"}, {"time": 253.721, "image_uri": "frame_0501.png"}, {"time": 254.229, "image_uri": "frame_0502.png"}, {"time": 254.736, "image_uri": "frame_0503.png"}, {"time": 255.244, "image_uri": "frame_0504.png"}, {"time": 255.751, "image_uri": "frame_0505.png"}, {"time": 256.258, "image_uri": "frame_0506.png"}, {"time": 256.766, "image_uri": "frame_0507.png"}, {"time": 257.273, "image_uri": "frame_0508.png"}, {"time": 257.781, "image_uri": "frame_0509.png"}, {"time": 258.288, "image_uri": "frame_0510.png"}, {"time": 258.796, "image_uri": "frame_0511.png"}, {"time": 259.303, "image_uri": "frame_0512.png"}, {"time": 259.811, "image_uri": "frame_0513.png"}, {"time": 260.318, "image_uri": "frame_0514.png"}, {"time": 260.825, "image_uri": "frame_0515.png"}, {"time": 261.333, "image_uri": "frame_0516.png"}, {"time": 261.84, "image_uri": "frame_0517.png"}, {"time": 262.348, "image_uri": "frame_0518.png"}, {"time": 262.855, "image_uri": "frame_0519.png"}, {"time": 263.363, "image_uri": "frame_0520.png"}, {"time": 263.87, "image_uri": "frame_0521.png"}, {"time": 264.378, "image_uri": "frame_0522.png"}, {"time": 264.885, "image_uri": "frame_0523.png"}, {"time": 265.392, "image_uri": "frame_0524.png"}, {"time": 265.9, "image_uri": "frame_0525.png"}, {"time": 266.407, "image_uri": "frame_0526.png"}, {"time": 266.915, "image_uri": "frame_0527.png"}, {"time": 267.422, "image_uri": "frame_0528.png"}, {"time": 267.93, "image_uri": "frame_0529.png"}, {"time": 268.437, "image_uri": "frame_0530.png"}, {"time": 268.945, "image_uri": "frame_0531.png"}, {"time": 269.452, "image_uri": "frame_0532.png"}, {"time": 269.959, "image_uri": "frame_0533.png"}, {"time": 270.467, "image_uri": "frame_0534.png"}, {"time": 270.974, "image_uri": "frame_0535.png"}, {"time": 271.482, "image_uri": "frame_0536.png"}, {"time": 271.989, "image_uri": "frame_0537.png"}, {"time": 272.497, "image_uri": "frame_0538.png"}, {"time": 273.004, "image_uri": "frame_0539.png"}, {"time": 273.512, "image_uri": "frame_0540.png"}, {"time": 274.019, "image_uri": "frame_0541.png"}, {"time": 274.526, "image_uri": "frame_0542.png"}, {"time": 275.034, "image_uri": "frame_0543.png"}, {"time": 275.541, "image_uri": "frame_0544.png"}, {"time": 276.049, "image_uri": "frame_0545.png"}, {"time": 276.556, "image_uri": "frame_0546.png"}, {"time": 277.064, "image_uri": "frame_0547.png"}, {"time": 277.571, "image_uri": "frame_0548.png"}, {"time": 278.078, "image_uri": "frame_0549.png"}, {"time": 278.586, "image_uri": "frame_0550.png"}, {"time": 279.093, "image_uri": "frame_0551.png"}, {"time": 279.601, "image_uri": "frame_0552.png"}, {"time": 280.108, "image_uri": "frame_0553.png"}, {"time": 280.616, "image_uri": "frame_0554.png"}, {"time": 281.123, "image_uri": "frame_0555.png"}, {"time": 281.631, "image_uri": "frame_0556.png"}, {"time": 282.138, "image_uri": "frame_0557.png"}, {"time": 282.645, "image_uri": "frame_0558.png"}, {"time": 283.153, "image_uri": "frame_0559.png"}, {"time": 283.66, "image_uri": "frame_0560.png"}, {"time": 284.168, "image_uri": "frame_0561.png"}, {"time": 284.675, "image_uri": "frame_0562.png"}, {"time": 285.183, "image_uri": "frame_0563.png"}, {"time": 285.69, "image_uri": "frame_0564.png"}, {"time": 286.198, "image_uri": "frame_0565.png"}, {"time": 286.705, "image_uri": "frame_0566.png"}, {"time": 287.212, "image_uri": "frame_0567.png"}, {"time": 287.72, "image_uri": "frame_0568.png"}, {"time": 288.227, "image_uri": "frame_0569.png"}, {"time": 288.735, "image_uri": "frame_0570.png"}, {"time": 289.242, "image_uri": "frame_0571.png"}, {"time": 289.75, "image_uri": "frame_0572.png"}, {"time": 290.257, "image_uri": "frame_0573.png"}, {"time": 290.765, "image_uri": "frame_0574.png"}, {"time": 291.272, "image_uri": "frame_0575.png"}, {"time": 291.779, "image_uri": "frame_0576.png"}, {"time": 292.287, "image_uri": "frame_0577.png"}, {"time": 292.794, "image_uri": "frame_0578.png"}, {"time": 293.302, "image_uri": "frame_0579.png"}, {"time": 293.809, "image_uri": "frame_0580.png"}, {"time": 294.317, "image_uri": "frame_0581.png"}, {"time": 294.824, "image_uri": "frame_0582.png"}, {"time": 295.332, "image_uri": "frame_0583.png"}, {"time": 295.839, "image_uri": "frame_0584.png"}, {"time": 296.346, "image_uri": "frame_0585.png"}, {"time": 296.854, "image_uri": "frame_0586.png"}, {"time": 297.361, "image_uri": "frame_0587.png"}, {"time": 297.869, "image_uri": "frame_0588.png"}, {"time": 298.376, "image_uri": "frame_0589.png"}, {"time": 298.884, "image_uri": "frame_0590.png"}, {"time": 299.391, "image_uri": "frame_0591.png"}, {"time": 299.899, "image_uri": "frame_0592.png"}, {"time": 300.406, "image_uri": "frame_0593.png"}, {"time": 300.913, "image_uri": "frame_0594.png"}, {"time": 301.421, "image_uri": "frame_0595.png"}, {"time": 301.928, "image_uri": "frame_0596.png"}, {"time": 302.436, "image_uri": "frame_0597.png"}, {"time": 302.943, "image_uri": "frame_0598.png"}, {"time": 303.451, "image_uri": "frame_0599.png"}, {"time": 303.958, "image_uri": "frame_0600.png"}, {"time": 304.465, "image_uri": "frame_0601.png"}, {"time": 304.973, "image_uri": "frame_0602.png"}, {"time": 305.48, "image_uri": "frame_0603.png"}, {"time": 305.988, "image_uri": "frame_0604.png"}, {"time": 306.495, "image_uri": "frame_0605.png"}, {"time": 307.003, "image_uri": "frame_0606.png"}, {"time": 307.51, "image_uri": "frame_0607.png"}, {"time": 308.018, "image_uri": "frame_0608.png"}, {"time": 308.525, "image_uri": "frame_0609.png"}, {"time": 309.032, "image_uri": "frame_0610.png"}, {"time": 309.54, "image_uri": "frame_0611.png"}, {"time": 310.047, "image_uri": "frame_0612.png"}, {"time": 310.555, "image_uri": "frame_0613.png"}, {"time": 311.062, "image_uri": "frame_0614.png"}, {"time": 311.57, "image_uri": "frame_0615.png"}, {"time": 312.077, "image_uri": "frame_0616.png"}, {"time": 312.585, "image_uri": "frame_0617.png"}, {"time": 313.092, "image_uri": "frame_0618.png"}, {"time": 313.599, "image_uri": "frame_0619.png"}, {"time": 314.107, "image_uri": "frame_0620.png"}, {"time": 314.614, "image_uri": "frame_0621.png"}, {"time": 315.122, "image_uri": "frame_0622.png"}, {"time": 315.629, "image_uri": "frame_0623.png"}, {"time": 316.137, "image_uri": "frame_0624.png"}, {"time": 316.644, "image_uri": "frame_0625.png"}, {"time": 317.152, "image_uri": "frame_0626.png"}, {"time": 317.659, "image_uri": "frame_0627.png"}, {"time": 318.166, "image_uri": "frame_0628.png"}, {"time": 318.674, "image_uri": "frame_0629.png"}, {"time": 319.181, "image_uri": "frame_0630.png"}, {"time": 319.689, "image_uri": "frame_0631.png"}, {"time": 320.196, "image_uri": "frame_0632.png"}, {"time": 320.704, "image_uri": "frame_0633.png"}, {"time": 321.211, "image_uri": "frame_0634.png"}, {"time": 321.719, "image_uri": "frame_0635.png"}, {"time": 322.226, "image_uri": "frame_0636.png"}, {"time": 322.733, "image_uri": "frame_0637.png"}, {"time": 323.241, "image_uri": "frame_0638.png"}, {"time": 323.748, "image_uri": "frame_0639.png"}, {"time": 324.256, "image_uri": "frame_0640.png"}, {"time": 324.763, "image_uri": "frame_0641.png"}, {"time": 325.271, "image_uri": "frame_0642.png"}, {"time": 325.778, "image_uri": "frame_0643.png"}, {"time": 326.286, "image_uri": "frame_0644.png"}, {"time": 326.793, "image_uri": "frame_0645.png"}, {"time": 327.3, "image_uri": "frame_0646.png"}, {"time": 327.808, "image_uri": "frame_0647.png"}, {"time": 328.315, "image_uri": "frame_0648.png"}, {"time": 328.823, "image_uri": "frame_0649.png"}, {"time": 329.33, "image_uri": "frame_0650.png"}, {"time": 329.838, "image_uri": "frame_0651.png"}, {"time": 330.345, "image_uri": "frame_0652.png"}, {"time": 330.853, "image_uri": "frame_0653.png"}, {"time": 331.36, "image_uri": "frame_0654.png"}, {"time": 331.867, "image_uri": "frame_0655.png"}, {"time": 332.375, "image_uri": "frame_0656.png"}, {"time": 332.882, "image_uri": "frame_0657.png"}, {"time": 333.39, "image_uri": "frame_0658.png"}, {"time": 333.897, "image_uri": "frame_0659.png"}, {"time": 334.405, "image_uri": "frame_0660.png"}, {"time": 334.912, "image_uri": "frame_0661.png"}, {"time": 335.419, "image_uri": "frame_0662.png"}, {"time": 335.927, "image_uri": "frame_0663.png"}, {"time": 336.434, "image_uri": "frame_0664.png"}, {"time": 336.942, "image_uri": "frame_0665.png"}, {"time": 337.449, "image_uri": "frame_0666.png"}, {"time": 337.957, "image_uri": "frame_0667.png"}, {"time": 338.464, "image_uri": "frame_0668.png"}, {"time": 338.972, "image_uri": "frame_0669.png"}, {"time": 339.479, "image_uri": "frame_0670.png"}, {"time": 339.986, "image_uri": "frame_0671.png"}, {"time": 340.494, "image_uri": "frame_0672.png"}, {"time": 341.001, "image_uri": "frame_0673.png"}, {"time": 341.509, "image_uri": "frame_0674.png"}, {"time": 342.016, "image_uri": "frame_0675.png"}, {"time": 342.524, "image_uri": "frame_0676.png"}, {"time": 343.031, "image_uri": "frame_0677.png"}, {"time": 343.539, "image_uri": "frame_0678.png"}, {"time": 344.046, "image_uri": "frame_0679.png"}, {"time": 344.553, "image_uri": "frame_0680.png"}, {"time": 345.061, "image_uri": "frame_0681.png"}, {"time": 345.568, "image_uri": "frame_0682.png"}, {"time": 346.076, "image_uri": "frame_0683.png"}, {"time": 346.583, "image_uri": "frame_0684.png"}, {"time": 347.091, "image_uri": "frame_0685.png"}, {"time": 347.598, "image_uri": "frame_0686.png"}, {"time": 348.106, "image_uri": "frame_0687.png"}, {"time": 348.613, "image_uri": "frame_0688.png"}, {"time": 349.12, "image_uri": "frame_0689.png"}, {"time": 349.628, "image_uri": "frame_0690.png"}, {"time": 350.135, "image_uri": "frame_0691.png"}, {"time": 350.643, "image_uri": "frame_0692.png"}, {"time": 351.15, "image_uri": "frame_0693.png"}, {"time": 351.658, "image_uri": "frame_0694.png"}, {"time": 352.165, "image_uri": "frame_0695.png"}, {"time": 352.673, "image_uri": "frame_0696.png"}, {"time": 353.18, "image_uri": "frame_0697.png"}, {"time": 353.687, "image_uri": "frame_0698.png"}, {"time": 354.195, "image_uri": "frame_0699.png"}, {"time": 354.702, "image_uri": "frame_0700.png"}, {"time": 355.21, "image_uri": "frame_0701.png"}, {"time": 355.717, "image_uri": "frame_0702.png"}, {"time": 356.225, "image_uri": "frame_0703.png"}, {"time": 356.732, "image_uri": "frame_0704.png"}, {"time": 357.24, "image_uri": "frame_0705.png"}, {"time": 357.747, "image_uri": "frame_0706.png"}, {"time": 358.254, "image_uri": "frame_0707.png"}, {"time": 358.762, "image_uri": "frame_0708.png"}, {"time": 359.269, "image_uri": "frame_0709.png"}, {"time": 359.777, "image_uri": "frame_0710.png"}, {"time": 360.284, "image_uri": "frame_0711.png"}, {"time": 360.792, "image_uri": "frame_0712.png"}, {"time": 361.299, "image_uri": "frame_0713.png"}, {"time": 361.806, "image_uri": "frame_0714.png"}, {"time": 362.314, "image_uri": "frame_0715.png"}, {"time": 362.821, "image_uri": "frame_0716.png"}, {"time": 363.329, "image_uri": "frame_0717.png"}, {"time": 363.836, "image_uri": "frame_0718.png"}, {"time": 364.344, "image_uri": "frame_0719.png"}, {"time": 364.851, "image_uri": "frame_0720.png"}, {"time": 365.359, "image_uri": "frame_0721.png"}, {"time": 365.866, "image_uri": "frame_0722.png"}, {"time": 366.373, "image_uri": "frame_0723.png"}, {"time": 366.881, "image_uri": "frame_0724.png"}, {"time": 367.388, "image_uri": "frame_0725.png"}, {"time": 367.896, "image_uri": "frame_0726.png"}, {"time": 368.403, "image_uri": "frame_0727.png"}, {"time": 368.911, "image_uri": "frame_0728.png"}, {"time": 369.418, "image_uri": "frame_0729.png"}, {"time": 369.926, "image_uri": "frame_0730.png"}, {"time": 370.433, "image_uri": "frame_0731.png"}, {"time": 370.94, "image_uri": "frame_0732.png"}, {"time": 371.448, "image_uri": "frame_0733.png"}, {"time": 371.955, "image_uri": "frame_0734.png"}, {"time": 372.463, "image_uri": "frame_0735.png"}, {"time": 372.97, "image_uri": "frame_0736.png"}, {"time": 373.478, "image_uri": "frame_0737.png"}, {"time": 373.985, "image_uri": "frame_0738.png"}, {"time": 374.493, "image_uri": "frame_0739.png"}, {"time": 375.0, "image_uri": "frame_0740.png"}, {"time": 375.507, "image_uri": "frame_0741.png"}, {"time": 376.015, "image_uri": "frame_0742.png"}, {"time": 376.522, "image_uri": "frame_0743.png"}, {"time": 377.03, "image_uri": "frame_0744.png"}, {"time": 377.537, "image_uri": "frame_0745.png"}, {"time": 378.045, "image_uri": "frame_0746.png"}, {"time": 378.552, "image_uri": "frame_0747.png"}, {"time": 379.06, "image_uri": "frame_0748.png"}, {"time": 379.567, "image_uri": "frame_0749.png"}, {"time": 380.074, "image_uri": "frame_0750.png"}, {"time": 380.582, "image_uri": "frame_0751.png"}, {"time": 381.089, "image_uri": "frame_0752.png"}, {"time": 381.597, "image_uri": "frame_0753.png"}, {"time": 382.104, "image_uri": "frame_0754.png"}, {"time": 382.612, "image_uri": "frame_0755.png"}, {"time": 383.119, "image_uri": "frame_0756.png"}, {"time": 383.627, "image_uri": "frame_0757.png"}, {"time": 384.134, "image_uri": "frame_0758.png"}, {"time": 384.641, "image_uri": "frame_0759.png"}, {"time": 385.149, "image_uri": "frame_0760.png"}, {"time": 385.656, "image_uri": "frame_0761.png"}, {"time": 386.164, "image_uri": "frame_0762.png"}, {"time": 386.671, "image_uri": "frame_0763.png"}, {"time": 387.179, "image_uri": "frame_0764.png"}, {"time": 387.686, "image_uri": "frame_0765.png"}, {"time": 388.194, "image_uri": "frame_0766.png"}, {"time": 388.701, "image_uri": "frame_0767.png"}, {"time": 389.208, "image_uri": "frame_0768.png"}, {"time": 389.716, "image_uri": "frame_0769.png"}, {"time": 390.223, "image_uri": "frame_0770.png"}, {"time": 390.731, "image_uri": "frame_0771.png"}, {"time": 391.238, "image_uri": "frame_0772.png"}, {"time": 391.746, "image_uri": "frame_0773.png"}, {"time": 392.253, "image_uri": "frame_0774.png"}, {"time": 392.76, "image_uri": "frame_0775.png"}, {"time": 393.268, "image_uri": "frame_0776.png"}, {"time": 393.775, "image_uri": "frame_0777.png"}, {"time": 394.283, "image_uri": "frame_0778.png"}, {"time": 394.79, "image_uri": "frame_0779.png"}, {"time": 395.298, "image_uri": "frame_0780.png"}, {"time": 395.805, "image_uri": "frame_0781.png"}, {"time": 396.313, "image_uri": "frame_0782.png"}, {"time": 396.82, "image_uri": "frame_0783.png"}, {"time": 397.327, "image_uri": "frame_0784.png"}, {"time": 397.835, "image_uri": "frame_0785.png"}, {"time": 398.342, "image_uri": "frame_0786.png"}, {"time": 398.85, "image_uri": "frame_0787.png"}, {"time": 399.357, "image_uri": "frame_0788.png"}, {"time": 399.865, "image_uri": "frame_0789.png"}, {"time": 400.372, "image_uri": "frame_0790.png"}, {"time": 400.88, "image_uri": "frame_0791.png"}, {"time": 401.387, "image_uri": "frame_0792.png"}, {"time": 401.894, "image_uri": "frame_0793.png"}, {"time": 402.402, "image_uri": "frame_0794.png"}, {"time": 402.909, "image_uri": "frame_0795.png"}, {"time": 403.417, "image_uri": "frame_0796.png"}, {"time": 403.924, "image_uri": "frame_0797.png"}, {"time": 404.432, "image_uri": "frame_0798.png"}, {"time": 404.939, "image_uri": "frame_0799.png"}, {"time": 405.447, "image_uri": "frame_0800.png"}, {"time": 405.954, "image_uri": "frame_0801.png"}, {"time": 406.461, "image_uri": "frame_0802.png"}, {"time": 406.969, "image_uri": "frame_0803.png"}, {"time": 407.476, "image_uri": "frame_0804.png"}, {"time": 407.984, "image_uri": "frame_0805.png"}, {"time": 408.491, "image_uri": "frame_0806.png"}, {"time": 408.999, "image_uri": "frame_0807.png"}, {"time": 409.506, "image_uri": "frame_0808.png"}, {"time": 410.014, "image_uri": "frame_0809.png"}, {"time": 410.521, "image_uri": "frame_0810.png"}, {"time": 411.028, "image_uri": "frame_0811.png"}, {"time": 411.536, "image_uri": "frame_0812.png"}, {"time": 412.043, "image_uri": "frame_0813.png"}, {"time": 412.551, "image_uri": "frame_0814.png"}, {"time": 413.058, "image_uri": "frame_0815.png"}, {"time": 413.566, "image_uri": "frame_0816.png"}, {"time": 414.073, "image_uri": "frame_0817.png"}, {"time": 414.581, "image_uri": "frame_0818.png"}, {"time": 415.088, "image_uri": "frame_0819.png"}, {"time": 415.595, "image_uri": "frame_0820.png"}, {"time": 416.103, "image_uri": "frame_0821.png"}, {"time": 416.61, "image_uri": "frame_0822.png"}, {"time": 417.118, "image_uri": "frame_0823.png"}, {"time": 417.625, "image_uri": "frame_0824.png"}, {"time": 418.133, "image_uri": "frame_0825.png"}, {"time": 418.64, "image_uri": "frame_0826.png"}, {"time": 419.147, "image_uri": "frame_0827.png"}, {"time": 419.655, "image_uri": "frame_0828.png"}, {"time": 420.162, "image_uri": "frame_0829.png"}, {"time": 420.67, "image_uri": "frame_0830.png"}, {"time": 421.177, "image_uri": "frame_0831.png"}, {"time": 421.685, "image_uri": "frame_0832.png"}, {"time": 422.192, "image_uri": "frame_0833.png"}, {"time": 422.7, "image_uri": "frame_0834.png"}, {"time": 423.207, "image_uri": "frame_0835.png"}, {"time": 423.714, "image_uri": "frame_0836.png"}, {"time": 424.222, "image_uri": "frame_0837.png"}, {"time": 424.729, "image_uri": "frame_0838.png"}, {"time": 425.237, "image_uri": "frame_0839.png"}, {"time": 425.744, "image_uri": "frame_0840.png"}, {"time": 426.252, "image_uri": "frame_0841.png"}, {"time": 426.759, "image_uri": "frame_0842.png"}, {"time": 427.267, "image_uri": "frame_0843.png"}, {"time": 427.774, "image_uri": "frame_0844.png"}, {"time": 428.281, "image_uri": "frame_0845.png"}, {"time": 428.789, "image_uri": "frame_0846.png"}, {"time": 429.296, "image_uri": "frame_0847.png"}, {"time": 429.804, "image_uri": "frame_0848.png"}, {"time": 430.311, "image_uri": "frame_0849.png"}, {"time": 430.819, "image_uri": "frame_0850.png"}, {"time": 431.326, "image_uri": "frame_0851.png"}, {"time": 431.834, "image_uri": "frame_0852.png"}, {"time": 432.341, "image_uri": "frame_0853.png"}, {"time": 432.848, "image_uri": "frame_0854.png"}, {"time": 433.356, "image_uri": "frame_0855.png"}, {"time": 433.863, "image_uri": "frame_0856.png"}, {"time": 434.371, "image_uri": "frame_0857.png"}, {"time": 434.878, "image_uri": "frame_0858.png"}, {"time": 435.386, "image_uri": "frame_0859.png"}, {"time": 435.893, "image_uri": "frame_0860.png"}, {"time": 436.401, "image_uri": "frame_0861.png"}, {"time": 436.908, "image_uri": "frame_0862.png"}, {"time": 437.415, "image_uri": "frame_0863.png"}, {"time": 437.923, "image_uri": "frame_0864.png"}, {"time": 438.43, "image_uri": "frame_0865.png"}, {"time": 438.938, "image_uri": "frame_0866.png"}, {"time": 439.445, "image_uri": "frame_0867.png"}, {"time": 439.953, "image_uri": "frame_0868.png"}, {"time": 440.46, "image_uri": "frame_0869.png"}, {"time": 440.968, "image_uri": "frame_0870.png"}, {"time": 441.475, "image_uri": "frame_0871.png"}, {"time": 441.982, "image_uri": "frame_0872.png"}, {"time": 442.49, "image_uri": "frame_0873.png"}, {"time": 442.997, "image_uri": "frame_0874.png"}, {"time": 443.505, "image_uri": "frame_0875.png"}, {"time": 444.012, "image_uri": "frame_0876.png"}, {"time": 444.52, "image_uri": "frame_0877.png"}, {"time": 445.027, "image_uri": "frame_0878.png"}, {"time": 445.535, "image_uri": "frame_0879.png"}, {"time": 446.042, "image_uri": "frame_0880.png"}, {"time": 446.549, "image_uri": "frame_0881.png"}, {"time": 447.057, "image_uri": "frame_0882.png"}, {"time": 447.564, "image_uri": "frame_0883.png"}, {"time": 448.072, "image_uri": "frame_0884.png"}, {"time": 448.579, "image_uri": "frame_0885.png"}, {"time": 449.087, "image_uri": "frame_0886.png"}, {"time": 449.594, "image_uri": "frame_0887.png"}, {"time": 450.101, "image_uri": "frame_0888.png"}, {"time": 450.609, "image_uri": "frame_0889.png"}, {"time": 451.116, "image_uri": "frame_0890.png"}, {"time": 451.624, "image_uri": "frame_0891.png"}, {"time": 452.131, "image_uri": "frame_0892.png"}, {"time": 452.639, "image_uri": "frame_0893.png"}, {"time": 453.146, "image_uri": "frame_0894.png"}, {"time": 453.654, "image_uri": "frame_0895.png"}, {"time": 454.161, "image_uri": "frame_0896.png"}, {"time": 454.668, "image_uri": "frame_0897.png"}, {"time": 455.176, "image_uri": "frame_0898.png"}, {"time": 455.683, "image_uri": "frame_0899.png"}, {"time": 456.191, "image_uri": "frame_0900.png"}, {"time": 456.698, "image_uri": "frame_0901.png"}, {"time": 457.206, "image_uri": "frame_0902.png"}, {"time": 457.713, "image_uri": "frame_0903.png"}, {"time": 458.221, "image_uri": "frame_0904.png"}, {"time": 458.728, "image_uri": "frame_0905.png"}, {"time": 459.235, "image_uri": "frame_0906.png"}, {"time": 459.743, "image_uri": "frame_0907.png"}, {"time": 460.25, "image_uri": "frame_0908.png"}, {"time": 460.758, "image_uri": "frame_0909.png"}, {"time": 461.265, "image_uri": "frame_0910.png"}, {"time": 461.773, "image_uri": "frame_0911.png"}, {"time": 462.28, "image_uri": "frame_0912.png"}, {"time": 462.788, "image_uri": "frame_0913.png"}, {"time": 463.295, "image_uri": "frame_0914.png"}, {"time": 463.802, "image_uri": "frame_0915.png"}, {"time": 464.31, "image_uri": "frame_0916.png"}, {"time": 464.817, "image_uri": "frame_0917.png"}, {"time": 465.325, "image_uri": "frame_0918.png"}, {"time": 465.832, "image_uri": "frame_0919.png"}, {"time": 466.34, "image_uri": "frame_0920.png"}, {"time": 466.847, "image_uri": "frame_0921.png"}, {"time": 467.355, "image_uri": "frame_0922.png"}, {"time": 467.862, "image_uri": "frame_0923.png"}, {"time": 468.369, "image_uri": "frame_0924.png"}, {"time": 468.877, "image_uri": "frame_0925.png"}, {"time": 469.384, "image_uri": "frame_0926.png"}, {"time": 469.892, "image_uri": "frame_0927.png"}, {"time": 470.399, "image_uri": "frame_0928.png"}, {"time": 470.907, "image_uri": "frame_0929.png"}, {"time": 471.414, "image_uri": "frame_0930.png"}, {"time": 471.922, "image_uri": "frame_0931.png"}, {"time": 472.429, "image_uri": "frame_0932.png"}, {"time": 472.936, "image_uri": "frame_0933.png"}, {"time": 473.444, "image_uri": "frame_0934.png"}, {"time": 473.951, "image_uri": "frame_0935.png"}, {"time": 474.459, "image_uri": "frame_0936.png"}, {"time": 474.966, "image_uri": "frame_0937.png"}, {"time": 475.474, "image_uri": "frame_0938.png"}, {"time": 475.981, "image_uri": "frame_0939.png"}, {"time": 476.488, "image_uri": "frame_0940.png"}, {"time": 476.996, "image_uri": "frame_0941.png"}, {"time": 477.503, "image_uri": "frame_0942.png"}, {"time": 478.011, "image_uri": "frame_0943.png"}, {"time": 478.518, "image_uri": "frame_0944.png"}, {"time": 479.026, "image_uri": "frame_0945.png"}, {"time": 479.533, "image_uri": "frame_0946.png"}, {"time": 480.041, "image_uri": "frame_0947.png"}, {"time": 480.548, "image_uri": "frame_0948.png"}, {"time": 481.055, "image_uri": "frame_0949.png"}, {"time": 481.563, "image_uri": "frame_0950.png"}, {"time": 482.07, "image_uri": "frame_0951.png"}, {"time": 482.578, "image_uri": "frame_0952.png"}, {"time": 483.085, "image_uri": "frame_0953.png"}, {"time": 483.593, "image_uri": "frame_0954.png"}, {"time": 484.1, "image_uri": "frame_0955.png"}, {"time": 484.608, "image_uri": "frame_0956.png"}, {"time": 485.115, "image_uri": "frame_0957.png"}, {"time": 485.622, "image_uri": "frame_0958.png"}, {"time": 486.13, "image_uri": "frame_0959.png"}, {"time": 486.637, "image_uri": "frame_0960.png"}, {"time": 487.145, "image_uri": "frame_0961.png"}, {"time": 487.652, "image_uri": "frame_0962.png"}, {"time": 488.16, "image_uri": "frame_0963.png"}, {"time": 488.667, "image_uri": "frame_0964.png"}, {"time": 489.175, "image_uri": "frame_0965.png"}, {"time": 489.682, "image_uri": "frame_0966.png"}, {"time": 490.189, "image_uri": "frame_0967.png"}, {"time": 490.697, "image_uri": "frame_0968.png"}, {"time": 491.204, "image_uri": "frame_0969.png"}, {"time": 491.712, "image_uri": "frame_0970.png"}, {"time": 492.219, "image_uri": "frame_0971.png"}, {"time": 492.727, "image_uri": "frame_0972.png"}, {"time": 493.234, "image_uri": "frame_0973.png"}, {"time": 493.742, "image_uri": "frame_0974.png"}, {"time": 494.249, "image_uri": "frame_0975.png"}, {"time": 494.756, "image_uri": "frame_0976.png"}, {"time": 495.264, "image_uri": "frame_0977.png"}, {"time": 495.771, "image_uri": "frame_0978.png"}, {"time": 496.279, "image_uri": "frame_0979.png"}, {"time": 496.786, "image_uri": "frame_0980.png"}, {"time": 497.294, "image_uri": "frame_0981.png"}, {"time": 497.801, "image_uri": "frame_0982.png"}, {"time": 498.309, "image_uri": "frame_0983.png"}, {"time": 498.816, "image_uri": "frame_0984.png"}, {"time": 499.323, "image_uri": "frame_0985.png"}, {"time": 499.831, "image_uri": "frame_0986.png"}, {"time": 500.338, "image_uri": "frame_0987.png"}, {"time": 500.846, "image_uri": "frame_0988.png"}, {"time": 501.353, "image_uri": "frame_0989.png"}, {"time": 501.861, "image_uri": "frame_0990.png"}, {"time": 502.368, "image_uri": "frame_0991.png"}, {"time": 502.876, "image_uri": "frame_0992.png"}, {"time": 503.383, "image_uri": "frame_0993.png"}, {"time": 503.89, "image_uri": "frame_0994.png"}, {"time": 504.398, "image_uri": "frame_0995.png"}, {"time": 504.905, "image_uri": "frame_0996.png"}, {"time": 505.413, "image_uri": "frame_0997.png"}, {"time": 505.92, "image_uri": "frame_0998.png"}, {"time": 506.428, "image_uri": "frame_0999.png"}, {"time": 506.935, "image_uri": "frame_1000.png"}, {"time": 507.442, "image_uri": "frame_1001.png"}, {"time": 507.95, "image_uri": "frame_1002.png"}, {"time": 508.457, "image_uri": "frame_1003.png"}, {"time": 508.965, "image_uri": "frame_1004.png"}, {"time": 509.472, "image_uri": "frame_1005.png"}, {"time": 509.98, "image_uri": "frame_1006.png"}, {"time": 510.487, "image_uri": "frame_1007.png"}, {"time": 510.995, "image_uri": "frame_1008.png"}, {"time": 511.502, "image_uri": "frame_1009.png"}, {"time": 512.009, "image_uri": "frame_1010.png"}, {"time": 512.517, "image_uri": "frame_1011.png"}, {"time": 513.024, "image_uri": "frame_1012.png"}, {"time": 513.532, "image_uri": "frame_1013.png"}, {"time": 514.039, "image_uri": "frame_1014.png"}, {"time": 514.547, "image_uri": "frame_1015.png"}, {"time": 515.054, "image_uri": "frame_1016.png"}, {"time": 515.562, "image_uri": "frame_1017.png"}, {"time": 516.069, "image_uri": "frame_1018.png"}, {"time": 516.576, "image_uri": "frame_1019.png"}, {"time": 517.084, "image_uri": "frame_1020.png"}, {"time": 517.591, "image_uri": "frame_1021.png"}, {"time": 518.099, "image_uri": "frame_1022.png"}, {"time": 518.606, "image_uri": "frame_1023.png"}, {"time": 519.114, "image_uri": "frame_1024.png"}, {"time": 519.621, "image_uri": "frame_1025.png"}, {"time": 520.129, "image_uri": "frame_1026.png"}, {"time": 520.636, "image_uri": "frame_1027.png"}, {"time": 521.143, "image_uri": "frame_1028.png"}, {"time": 521.651, "image_uri": "frame_1029.png"}, {"time": 522.158, "image_uri": "frame_1030.png"}, {"time": 522.666, "image_uri": "frame_1031.png"}, {"time": 523.173, "image_uri": "frame_1032.png"}, {"time": 523.681, "image_uri": "frame_1033.png"}, {"time": 524.188, "image_uri": "frame_1034.png"}, {"time": 524.696, "image_uri": "frame_1035.png"}, {"time": 525.203, "image_uri": "frame_1036.png"}, {"time": 525.71, "image_uri": "frame_1037.png"}, {"time": 526.218, "image_uri": "frame_1038.png"}, {"time": 526.725, "image_uri": "frame_1039.png"}, {"time": 527.233, "image_uri": "frame_1040.png"}, {"time": 527.74, "image_uri": "frame_1041.png"}, {"time": 528.248, "image_uri": "frame_1042.png"}, {"time": 528.755, "image_uri": "frame_1043.png"}, {"time": 529.263, "image_uri": "frame_1044.png"}, {"time": 529.77, "image_uri": "frame_1045.png"}, {"time": 530.277, "image_uri": "frame_1046.png"}, {"time": 530.785, "image_uri": "frame_1047.png"}, {"time": 531.292, "image_uri": "frame_1048.png"}, {"time": 531.8, "image_uri": "frame_1049.png"}, {"time": 532.307, "image_uri": "frame_1050.png"}, {"time": 532.815, "image_uri": "frame_1051.png"}, {"time": 533.322, "image_uri": "frame_1052.png"}, {"time": 533.829, "image_uri": "frame_1053.png"}, {"time": 534.337, "image_uri": "frame_1054.png"}, {"time": 534.844, "image_uri": "frame_1055.png"}, {"time": 535.352, "image_uri": "frame_1056.png"}, {"time": 535.859, "image_uri": "frame_1057.png"}, {"time": 536.367, "image_uri": "frame_1058.png"}, {"time": 536.874, "image_uri": "frame_1059.png"}, {"time": 537.382, "image_uri": "frame_1060.png"}, {"time": 537.889, "image_uri": "frame_1061.png"}, {"time": 538.396, "image_uri": "frame_1062.png"}, {"time": 538.904, "image_uri": "frame_1063.png"}, {"time": 539.411, "image_uri": "frame_1064.png"}, {"time": 539.919, "image_uri": "frame_1065.png"}, {"time": 540.426, "image_uri": "frame_1066.png"}, {"time": 540.934, "image_uri": "frame_1067.png"}, {"time": 541.441, "image_uri": "frame_1068.png"}, {"time": 541.949, "image_uri": "frame_1069.png"}, {"time": 542.456, "image_uri": "frame_1070.png"}, {"time": 542.963, "image_uri": "frame_1071.png"}, {"time": 543.471, "image_uri": "frame_1072.png"}, {"time": 543.978, "image_uri": "frame_1073.png"}, {"time": 544.486, "image_uri": "frame_1074.png"}, {"time": 544.993, "image_uri": "frame_1075.png"}, {"time": 545.501, "image_uri": "frame_1076.png"}, {"time": 546.008, "image_uri": "frame_1077.png"}, {"time": 546.516, "image_uri": "frame_1078.png"}, {"time": 547.023, "image_uri": "frame_1079.png"}, {"time": 547.53, "image_uri": "frame_1080.png"}, {"time": 548.038, "image_uri": "frame_1081.png"}, {"time": 548.545, "image_uri": "frame_1082.png"}, {"time": 549.053, "image_uri": "frame_1083.png"}, {"time": 549.56, "image_uri": "frame_1084.png"}, {"time": 550.068, "image_uri": "frame_1085.png"}, {"time": 550.575, "image_uri": "frame_1086.png"}, {"time": 551.083, "image_uri": "frame_1087.png"}, {"time": 551.59, "image_uri": "frame_1088.png"}, {"time": 552.097, "image_uri": "frame_1089.png"}, {"time": 552.605, "image_uri": "frame_1090.png"}, {"time": 553.112, "image_uri": "frame_1091.png"}, {"time": 553.62, "image_uri": "frame_1092.png"}, {"time": 554.127, "image_uri": "frame_1093.png"}, {"time": 554.635, "image_uri": "frame_1094.png"}, {"time": 555.142, "image_uri": "frame_1095.png"}, {"time": 555.65, "image_uri": "frame_1096.png"}, {"time": 556.157, "image_uri": "frame_1097.png"}, {"time": 556.664, "image_uri": "frame_1098.png"}, {"time": 557.172, "image_uri": "frame_1099.png"}, {"time": 557.679, "image_uri": "frame_1100.png"}, {"time": 558.187, "image_uri": "frame_1101.png"}, {"time": 558.694, "image_uri": "frame_1102.png"}, {"time": 559.202, "image_uri": "frame_1103.png"}, {"time": 559.709, "image_uri": "frame_1104.png"}, {"time": 560.217, "image_uri": "frame_1105.png"}, {"time": 560.724, "image_uri": "frame_1106.png"}, {"time": 561.231, "image_uri": "frame_1107.png"}, {"time": 561.739, "image_uri": "frame_1108.png"}, {"time": 562.246, "image_uri": "frame_1109.png"}, {"time": 562.754, "image_uri": "frame_1110.png"}, {"time": 563.261, "image_uri": "frame_1111.png"}, {"time": 563.769, "image_uri": "frame_1112.png"}, {"time": 564.276, "image_uri": "frame_1113.png"}, {"time": 564.783, "image_uri": "frame_1114.png"}, {"time": 565.291, "image_uri": "frame_1115.png"}, {"time": 565.798, "image_uri": "frame_1116.png"}, {"time": 566.306, "image_uri": "frame_1117.png"}, {"time": 566.813, "image_uri": "frame_1118.png"}, {"time": 567.321, "image_uri": "frame_1119.png"}, {"time": 567.828, "image_uri": "frame_1120.png"}, {"time": 568.336, "image_uri": "frame_1121.png"}, {"time": 568.843, "image_uri": "frame_1122.png"}, {"time": 569.35, "image_uri": "frame_1123.png"}, {"time": 569.858, "image_uri": "frame_1124.png"}, {"time": 570.365, "image_uri": "frame_1125.png"}, {"time": 570.873, "image_uri": "frame_1126.png"}, {"time": 571.38, "image_uri": "frame_1127.png"}, {"time": 571.888, "image_uri": "frame_1128.png"}, {"time": 572.395, "image_uri": "frame_1129.png"}, {"time": 572.903, "image_uri": "frame_1130.png"}, {"time": 573.41, "image_uri": "frame_1131.png"}, {"time": 573.917, "image_uri": "frame_1132.png"}, {"time": 574.425, "image_uri": "frame_1133.png"}, {"time": 574.932, "image_uri": "frame_1134.png"}, {"time": 575.44, "image_uri": "frame_1135.png"}, {"time": 575.947, "image_uri": "frame_1136.png"}, {"time": 576.455, "image_uri": "frame_1137.png"}, {"time": 576.962, "image_uri": "frame_1138.png"}, {"time": 577.47, "image_uri": "frame_1139.png"}, {"time": 577.977, "image_uri": "frame_1140.png"}, {"time": 578.484, "image_uri": "frame_1141.png"}, {"time": 578.992, "image_uri": "frame_1142.png"}, {"time": 579.499, "image_uri": "frame_1143.png"}, {"time": 580.007, "image_uri": "frame_1144.png"}, {"time": 580.514, "image_uri": "frame_1145.png"}, {"time": 581.022, "image_uri": "frame_1146.png"}, {"time": 581.529, "image_uri": "frame_1147.png"}, {"time": 582.037, "image_uri": "frame_1148.png"}, {"time": 582.544, "image_uri": "frame_1149.png"}, {"time": 583.051, "image_uri": "frame_1150.png"}, {"time": 583.559, "image_uri": "frame_1151.png"}, {"time": 584.066, "image_uri": "frame_1152.png"}, {"time": 584.574, "image_uri": "frame_1153.png"}, {"time": 585.081, "image_uri": "frame_1154.png"}, {"time": 585.589, "image_uri": "frame_1155.png"}, {"time": 586.096, "image_uri": "frame_1156.png"}, {"time": 586.604, "image_uri": "frame_1157.png"}, {"time": 587.111, "image_uri": "frame_1158.png"}, {"time": 587.618, "image_uri": "frame_1159.png"}, {"time": 588.126, "image_uri": "frame_1160.png"}, {"time": 588.633, "image_uri": "frame_1161.png"}, {"time": 589.141, "image_uri": "frame_1162.png"}, {"time": 589.648, "image_uri": "frame_1163.png"}, {"time": 590.156, "image_uri": "frame_1164.png"}, {"time": 590.663, "image_uri": "frame_1165.png"}, {"time": 591.171, "image_uri": "frame_1166.png"}, {"time": 591.678, "image_uri": "frame_1167.png"}, {"time": 592.185, "image_uri": "frame_1168.png"}, {"time": 592.693, "image_uri": "frame_1169.png"}, {"time": 593.2, "image_uri": "frame_1170.png"}, {"time": 593.708, "image_uri": "frame_1171.png"}, {"time": 594.215, "image_uri": "frame_1172.png"}, {"time": 594.723, "image_uri": "frame_1173.png"}, {"time": 595.23, "image_uri": "frame_1174.png"}, {"time": 595.737, "image_uri": "frame_1175.png"}, {"time": 596.245, "image_uri": "frame_1176.png"}, {"time": 596.752, "image_uri": "frame_1177.png"}, {"time": 597.26, "image_uri": "frame_1178.png"}, {"time": 597.767, "image_uri": "frame_1179.png"}, {"time": 598.275, "image_uri": "frame_1180.png"}, {"time": 598.782, "image_uri": "frame_1181.png"}, {"time": 599.29, "image_uri": "frame_1182.png"}, {"time": 599.797, "image_uri": "frame_1183.png"}, {"time": 600.304, "image_uri": "frame_1184.png"}, {"time": 600.812, "image_uri": "frame_1185.png"}, {"time": 601.319, "image_uri": "frame_1186.png"}, {"time": 601.827, "image_uri": "frame_1187.png"}, {"time": 602.334, "image_uri": "frame_1188.png"}, {"time": 602.842, "image_uri": "frame_1189.png"}, {"time": 603.349, "image_uri": "frame_1190.png"}, {"time": 603.857, "image_uri": "frame_1191.png"}, {"time": 604.364, "image_uri": "frame_1192.png"}, {"time": 604.871, "image_uri": "frame_1193.png"}, {"time": 605.379, "image_uri": "frame_1194.png"}, {"time": 605.886, "image_uri": "frame_1195.png"}, {"time": 606.394, "image_uri": "frame_1196.png"}, {"time": 606.901, "image_uri": "frame_1197.png"}, {"time": 607.409, "image_uri": "frame_1198.png"}, {"time": 607.916, "image_uri": "frame_1199.png"}, {"time": 608.424, "image_uri": "frame_1200.png"}, {"time": 608.931, "image_uri": "frame_1201.png"}, {"time": 609.438, "image_uri": "frame_1202.png"}, {"time": 609.946, "image_uri": "frame_1203.png"}, {"time": 610.453, "image_uri": "frame_1204.png"}, {"time": 610.961, "image_uri": "frame_1205.png"}, {"time": 611.468, "image_uri": "frame_1206.png"}, {"time": 611.976, "image_uri": "frame_1207.png"}, {"time": 612.483, "image_uri": "frame_1208.png"}, {"time": 612.991, "image_uri": "frame_1209.png"}, {"time": 613.498, "image_uri": "frame_1210.png"}, {"time": 614.005, "image_uri": "frame_1211.png"}, {"time": 614.513, "image_uri": "frame_1212.png"}, {"time": 615.02, "image_uri": "frame_1213.png"}, {"time": 615.528, "image_uri": "frame_1214.png"}, {"time": 616.035, "image_uri": "frame_1215.png"}, {"time": 616.543, "image_uri": "frame_1216.png"}, {"time": 617.05, "image_uri": "frame_1217.png"}, {"time": 617.558, "image_uri": "frame_1218.png"}, {"time": 618.065, "image_uri": "frame_1219.png"}, {"time": 618.572, "image_uri": "frame_1220.png"}, {"time": 619.08, "image_uri": "frame_1221.png"}, {"time": 619.587, "image_uri": "frame_1222.png"}, {"time": 620.095, "image_uri": "frame_1223.png"}, {"time": 620.602, "image_uri": "frame_1224.png"}, {"time": 621.11, "image_uri": "frame_1225.png"}, {"time": 621.617, "image_uri": "frame_1226.png"}, {"time": 622.124, "image_uri": "frame_1227.png"}, {"time": 622.632, "image_uri": "frame_1228.png"}, {"time": 623.139, "image_uri": "frame_1229.png"}, {"time": 623.647, "image_uri": "frame_1230.png"}, {"time": 624.154, "image_uri": "frame_1231.png"}, {"time": 624.662, "image_uri": "frame_1232.png"}, {"time": 625.169, "image_uri": "frame_1233.png"}, {"time": 625.677, "image_uri": "frame_1234.png"}, {"time": 626.184, "image_uri": "frame_1235.png"}, {"time": 626.691, "image_uri": "frame_1236.png"}, {"time": 627.199, "image_uri": "frame_1237.png"}, {"time": 627.706, "image_uri": "frame_1238.png"}, {"time": 628.214, "image_uri": "frame_1239.png"}, {"time": 628.721, "image_uri": "frame_1240.png"}, {"time": 629.229, "image_uri": "frame_1241.png"}, {"time": 629.736, "image_uri": "frame_1242.png"}, {"time": 630.244, "image_uri": "frame_1243.png"}, {"time": 630.751, "image_uri": "frame_1244.png"}, {"time": 631.258, "image_uri": "frame_1245.png"}, {"time": 631.766, "image_uri": "frame_1246.png"}, {"time": 632.273, "image_uri": "frame_1247.png"}, {"time": 632.781, "image_uri": "frame_1248.png"}, {"time": 633.288, "image_uri": "frame_1249.png"}, {"time": 633.796, "image_uri": "frame_1250.png"}, {"time": 634.303, "image_uri": "frame_1251.png"}, {"time": 634.811, "image_uri": "frame_1252.png"}, {"time": 635.318, "image_uri": "frame_1253.png"}, {"time": 635.825, "image_uri": "frame_1254.png"}, {"time": 636.333, "image_uri": "frame_1255.png"}, {"time": 636.84, "image_uri": "frame_1256.png"}, {"time": 637.348, "image_uri": "frame_1257.png"}, {"time": 637.855, "image_uri": "frame_1258.png"}, {"time": 638.363, "image_uri": "frame_1259.png"}, {"time": 638.87, "image_uri": "frame_1260.png"}, {"time": 639.378, "image_uri": "frame_1261.png"}, {"time": 639.885, "image_uri": "frame_1262.png"}, {"time": 640.392, "image_uri": "frame_1263.png"}, {"time": 640.9, "image_uri": "frame_1264.png"}, {"time": 641.407, "image_uri": "frame_1265.png"}, {"time": 641.915, "image_uri": "frame_1266.png"}, {"time": 642.422, "image_uri": "frame_1267.png"}, {"time": 642.93, "image_uri": "frame_1268.png"}, {"time": 643.437, "image_uri": "frame_1269.png"}, {"time": 643.945, "image_uri": "frame_1270.png"}, {"time": 644.452, "image_uri": "frame_1271.png"}, {"time": 644.959, "image_uri": "frame_1272.png"}, {"time": 645.467, "image_uri": "frame_1273.png"}, {"time": 645.974, "image_uri": "frame_1274.png"}, {"time": 646.482, "image_uri": "frame_1275.png"}, {"time": 646.989, "image_uri": "frame_1276.png"}, {"time": 647.497, "image_uri": "frame_1277.png"}, {"time": 648.004, "image_uri": "frame_1278.png"}, {"time": 648.512, "image_uri": "frame_1279.png"}, {"time": 649.019, "image_uri": "frame_1280.png"}, {"time": 649.526, "image_uri": "frame_1281.png"}, {"time": 650.034, "image_uri": "frame_1282.png"}, {"time": 650.541, "image_uri": "frame_1283.png"}, {"time": 651.049, "image_uri": "frame_1284.png"}, {"time": 651.556, "image_uri": "frame_1285.png"}, {"time": 652.064, "image_uri": "frame_1286.png"}, {"time": 652.571, "image_uri": "frame_1287.png"}, {"time": 653.078, "image_uri": "frame_1288.png"}, {"time": 653.586, "image_uri": "frame_1289.png"}, {"time": 654.093, "image_uri": "frame_1290.png"}, {"time": 654.601, "image_uri": "frame_1291.png"}, {"time": 655.108, "image_uri": "frame_1292.png"}, {"time": 655.616, "image_uri": "frame_1293.png"}, {"time": 656.123, "image_uri": "frame_1294.png"}, {"time": 656.631, "image_uri": "frame_1295.png"}, {"time": 657.138, "image_uri": "frame_1296.png"}, {"time": 657.645, "image_uri": "frame_1297.png"}, {"time": 658.153, "image_uri": "frame_1298.png"}, {"time": 658.66, "image_uri": "frame_1299.png"}, {"time": 659.168, "image_uri": "frame_1300.png"}, {"time": 659.675, "image_uri": "frame_1301.png"}, {"time": 660.183, "image_uri": "frame_1302.png"}, {"time": 660.69, "image_uri": "frame_1303.png"}, {"time": 661.198, "image_uri": "frame_1304.png"}, {"time": 661.705, "image_uri": "frame_1305.png"}, {"time": 662.212, "image_uri": "frame_1306.png"}, {"time": 662.72, "image_uri": "frame_1307.png"}, {"time": 663.227, "image_uri": "frame_1308.png"}, {"time": 663.735, "image_uri": "frame_1309.png"}, {"time": 664.242, "image_uri": "frame_1310.png"}, {"time": 664.75, "image_uri": "frame_1311.png"}, {"time": 665.257, "image_uri": "frame_1312.png"}, {"time": 665.765, "image_uri": "frame_1313.png"}, {"time": 666.272, "image_uri": "frame_1314.png"}, {"time": 666.779, "image_uri": "frame_1315.png"}, {"time": 667.287, "image_uri": "frame_1316.png"}, {"time": 667.794, "image_uri": "frame_1317.png"}, {"time": 668.302, "image_uri": "frame_1318.png"}, {"time": 668.809, "image_uri": "frame_1319.png"}, {"time": 669.317, "image_uri": "frame_1320.png"}, {"time": 669.824, "image_uri": "frame_1321.png"}, {"time": 670.332, "image_uri": "frame_1322.png"}, {"time": 670.839, "image_uri": "frame_1323.png"}, {"time": 671.346, "image_uri": "frame_1324.png"}, {"time": 671.854, "image_uri": "frame_1325.png"}, {"time": 672.361, "image_uri": "frame_1326.png"}, {"time": 672.869, "image_uri": "frame_1327.png"}, {"time": 673.376, "image_uri": "frame_1328.png"}, {"time": 673.884, "image_uri": "frame_1329.png"}, {"time": 674.391, "image_uri": "frame_1330.png"}, {"time": 674.899, "image_uri": "frame_1331.png"}, {"time": 675.406, "image_uri": "frame_1332.png"}, {"time": 675.913, "image_uri": "frame_1333.png"}, {"time": 676.421, "image_uri": "frame_1334.png"}, {"time": 676.928, "image_uri": "frame_1335.png"}, {"time": 677.436, "image_uri": "frame_1336.png"}, {"time": 677.943, "image_uri": "frame_1337.png"}, {"time": 678.451, "image_uri": "frame_1338.png"}, {"time": 678.958, "image_uri": "frame_1339.png"}, {"time": 679.465, "image_uri": "frame_1340.png"}, {"time": 679.973, "image_uri": "frame_1341.png"}, {"time": 680.48, "image_uri": "frame_1342.png"}, {"time": 680.988, "image_uri": "frame_1343.png"}, {"time": 681.495, "image_uri": "frame_1344.png"}, {"time": 682.003, "image_uri": "frame_1345.png"}, {"time": 682.51, "image_uri": "frame_1346.png"}, {"time": 683.018, "image_uri": "frame_1347.png"}, {"time": 683.525, "image_uri": "frame_1348.png"}, {"time": 684.032, "image_uri": "frame_1349.png"}, {"time": 684.54, "image_uri": "frame_1350.png"}, {"time": 685.047, "image_uri": "frame_1351.png"}, {"time": 685.555, "image_uri": "frame_1352.png"}, {"time": 686.062, "image_uri": "frame_1353.png"}, {"time": 686.57, "image_uri": "frame_1354.png"}, {"time": 687.077, "image_uri": "frame_1355.png"}, {"time": 687.585, "image_uri": "frame_1356.png"}, {"time": 688.092, "image_uri": "frame_1357.png"}, {"time": 688.599, "image_uri": "frame_1358.png"}, {"time": 689.107, "image_uri": "frame_1359.png"}, {"time": 689.614, "image_uri": "frame_1360.png"}, {"time": 690.122, "image_uri": "frame_1361.png"}, {"time": 690.629, "image_uri": "frame_1362.png"}, {"time": 691.137, "image_uri": "frame_1363.png"}, {"time": 691.644, "image_uri": "frame_1364.png"}, {"time": 692.152, "image_uri": "frame_1365.png"}, {"time": 692.659, "image_uri": "frame_1366.png"}, {"time": 693.166, "image_uri": "frame_1367.png"}, {"time": 693.674, "image_uri": "frame_1368.png"}, {"time": 694.181, "image_uri": "frame_1369.png"}, {"time": 694.689, "image_uri": "frame_1370.png"}, {"time": 695.196, "image_uri": "frame_1371.png"}, {"time": 695.704, "image_uri": "frame_1372.png"}, {"time": 696.211, "image_uri": "frame_1373.png"}, {"time": 696.719, "image_uri": "frame_1374.png"}, {"time": 697.226, "image_uri": "frame_1375.png"}, {"time": 697.733, "image_uri": "frame_1376.png"}, {"time": 698.241, "image_uri": "frame_1377.png"}, {"time": 698.748, "image_uri": "frame_1378.png"}, {"time": 699.256, "image_uri": "frame_1379.png"}, {"time": 699.763, "image_uri": "frame_1380.png"}, {"time": 700.271, "image_uri": "frame_1381.png"}, {"time": 700.778, "image_uri": "frame_1382.png"}, {"time": 701.286, "image_uri": "frame_1383.png"}, {"time": 701.793, "image_uri": "frame_1384.png"}, {"time": 702.3, "image_uri": "frame_1385.png"}, {"time": 702.808, "image_uri": "frame_1386.png"}, {"time": 703.315, "image_uri": "frame_1387.png"}, {"time": 703.823, "image_uri": "frame_1388.png"}, {"time": 704.33, "image_uri": "frame_1389.png"}, {"time": 704.838, "image_uri": "frame_1390.png"}, {"time": 705.345, "image_uri": "frame_1391.png"}, {"time": 705.853, "image_uri": "frame_1392.png"}, {"time": 706.36, "image_uri": "frame_1393.png"}, {"time": 706.867, "image_uri": "frame_1394.png"}, {"time": 707.375, "image_uri": "frame_1395.png"}, {"time": 707.882, "image_uri": "frame_1396.png"}, {"time": 708.39, "image_uri": "frame_1397.png"}, {"time": 708.897, "image_uri": "frame_1398.png"}, {"time": 709.405, "image_uri": "frame_1399.png"}, {"time": 709.912, "image_uri": "frame_1400.png"}, {"time": 710.419, "image_uri": "frame_1401.png"}, {"time": 710.927, "image_uri": "frame_1402.png"}, {"time": 711.434, "image_uri": "frame_1403.png"}, {"time": 711.942, "image_uri": "frame_1404.png"}, {"time": 712.449, "image_uri": "frame_1405.png"}, {"time": 712.957, "image_uri": "frame_1406.png"}, {"time": 713.464, "image_uri": "frame_1407.png"}, {"time": 713.972, "image_uri": "frame_1408.png"}, {"time": 714.479, "image_uri": "frame_1409.png"}, {"time": 714.986, "image_uri": "frame_1410.png"}, {"time": 715.494, "image_uri": "frame_1411.png"}, {"time": 716.001, "image_uri": "frame_1412.png"}, {"time": 716.509, "image_uri": "frame_1413.png"}, {"time": 717.016, "image_uri": "frame_1414.png"}, {"time": 717.524, "image_uri": "frame_1415.png"}, {"time": 718.031, "image_uri": "frame_1416.png"}, {"time": 718.539, "image_uri": "frame_1417.png"}, {"time": 719.046, "image_uri": "frame_1418.png"}, {"time": 719.553, "image_uri": "frame_1419.png"}, {"time": 720.061, "image_uri": "frame_1420.png"}, {"time": 720.568, "image_uri": "frame_1421.png"}, {"time": 721.076, "image_uri": "frame_1422.png"}, {"time": 721.583, "image_uri": "frame_1423.png"}, {"time": 722.091, "image_uri": "frame_1424.png"}, {"time": 722.598, "image_uri": "frame_1425.png"}, {"time": 723.106, "image_uri": "frame_1426.png"}, {"time": 723.613, "image_uri": "frame_1427.png"}, {"time": 724.12, "image_uri": "frame_1428.png"}, {"time": 724.628, "image_uri": "frame_1429.png"}, {"time": 725.135, "image_uri": "frame_1430.png"}, {"time": 725.643, "image_uri": "frame_1431.png"}, {"time": 726.15, "image_uri": "frame_1432.png"}, {"time": 726.658, "image_uri": "frame_1433.png"}, {"time": 727.165, "image_uri": "frame_1434.png"}, {"time": 727.673, "image_uri": "frame_1435.png"}, {"time": 728.18, "image_uri": "frame_1436.png"}, {"time": 728.687, "image_uri": "frame_1437.png"}, {"time": 729.195, "image_uri": "frame_1438.png"}, {"time": 729.702, "image_uri": "frame_1439.png"}, {"time": 730.21, "image_uri": "frame_1440.png"}, {"time": 730.717, "image_uri": "frame_1441.png"}, {"time": 731.225, "image_uri": "frame_1442.png"}, {"time": 731.732, "image_uri": "frame_1443.png"}, {"time": 732.24, "image_uri": "frame_1444.png"}, {"time": 732.747, "image_uri": "frame_1445.png"}, {"time": 733.254, "image_uri": "frame_1446.png"}, {"time": 733.762, "image_uri": "frame_1447.png"}, {"time": 734.269, "image_uri": "frame_1448.png"}, {"time": 734.777, "image_uri": "frame_1449.png"}, {"time": 735.284, "image_uri": "frame_1450.png"}, {"time": 735.792, "image_uri": "frame_1451.png"}, {"time": 736.299, "image_uri": "frame_1452.png"}, {"time": 736.806, "image_uri": "frame_1453.png"}, {"time": 737.314, "image_uri": "frame_1454.png"}, {"time": 737.821, "image_uri": "frame_1455.png"}, {"time": 738.329, "image_uri": "frame_1456.png"}, {"time": 738.836, "image_uri": "frame_1457.png"}, {"time": 739.344, "image_uri": "frame_1458.png"}, {"time": 739.851, "image_uri": "frame_1459.png"}, {"time": 740.359, "image_uri": "frame_1460.png"}, {"time": 740.866, "image_uri": "frame_1461.png"}, {"time": 741.373, "image_uri": "frame_1462.png"}, {"time": 741.881, "image_uri": "frame_1463.png"}, {"time": 742.388, "image_uri": "frame_1464.png"}, {"time": 742.896, "image_uri": "frame_1465.png"}, {"time": 743.403, "image_uri": "frame_1466.png"}, {"time": 743.911, "image_uri": "frame_1467.png"}, {"time": 744.418, "image_uri": "frame_1468.png"}, {"time": 744.926, "image_uri": "frame_1469.png"}, {"time": 745.433, "image_uri": "frame_1470.png"}, {"time": 745.94, "image_uri": "frame_1471.png"}, {"time": 746.448, "image_uri": "frame_1472.png"}, {"time": 746.955, "image_uri": "frame_1473.png"}, {"time": 747.463, "image_uri": "frame_1474.png"}, {"time": 747.97, "image_uri": "frame_1475.png"}, {"time": 748.478, "image_uri": "frame_1476.png"}, {"time": 748.985, "image_uri": "frame_1477.png"}, {"time": 749.493, "image_uri": "frame_1478.png"}, {"time": 750.0, "image_uri": "frame_1479.png"}, {"time": 750.507, "image_uri": "frame_1480.png"}, {"time": 751.015, "image_uri": "frame_1481.png"}, {"time": 751.522, "image_uri": "frame_1482.png"}, {"time": 752.03, "image_uri": "frame_1483.png"}, {"time": 752.537, "image_uri": "frame_1484.png"}, {"time": 753.045, "image_uri": "frame_1485.png"}, {"time": 753.552, "image_uri": "frame_1486.png"}, {"time": 754.06, "image_uri": "frame_1487.png"}, {"time": 754.567, "image_uri": "frame_1488.png"}, {"time": 755.074, "image_uri": "frame_1489.png"}, {"time": 755.582, "image_uri": "frame_1490.png"}, {"time": 756.089, "image_uri": "frame_1491.png"}, {"time": 756.597, "image_uri": "frame_1492.png"}, {"time": 757.104, "image_uri": "frame_1493.png"}, {"time": 757.612, "image_uri": "frame_1494.png"}, {"time": 758.119, "image_uri": "frame_1495.png"}, {"time": 758.627, "image_uri": "frame_1496.png"}, {"time": 759.134, "image_uri": "frame_1497.png"}, {"time": 759.641, "image_uri": "frame_1498.png"}, {"time": 760.149, "image_uri": "frame_1499.png"}, {"time": 760.656, "image_uri": "frame_1500.png"}, {"time": 761.164, "image_uri": "frame_1501.png"}, {"time": 761.671, "image_uri": "frame_1502.png"}, {"time": 762.179, "image_uri": "frame_1503.png"}, {"time": 762.686, "image_uri": "frame_1504.png"}, {"time": 763.194, "image_uri": "frame_1505.png"}, {"time": 763.701, "image_uri": "frame_1506.png"}, {"time": 764.208, "image_uri": "frame_1507.png"}, {"time": 764.716, "image_uri": "frame_1508.png"}, {"time": 765.223, "image_uri": "frame_1509.png"}, {"time": 765.731, "image_uri": "frame_1510.png"}, {"time": 766.238, "image_uri": "frame_1511.png"}, {"time": 766.746, "image_uri": "frame_1512.png"}, {"time": 767.253, "image_uri": "frame_1513.png"}, {"time": 767.76, "image_uri": "frame_1514.png"}, {"time": 768.268, "image_uri": "frame_1515.png"}, {"time": 768.775, "image_uri": "frame_1516.png"}, {"time": 769.283, "image_uri": "frame_1517.png"}, {"time": 769.79, "image_uri": "frame_1518.png"}, {"time": 770.298, "image_uri": "frame_1519.png"}, {"time": 770.805, "image_uri": "frame_1520.png"}, {"time": 771.313, "image_uri": "frame_1521.png"}, {"time": 771.82, "image_uri": "frame_1522.png"}, {"time": 772.327, "image_uri": "frame_1523.png"}, {"time": 772.835, "image_uri": "frame_1524.png"}, {"time": 773.342, "image_uri": "frame_1525.png"}, {"time": 773.85, "image_uri": "frame_1526.png"}, {"time": 774.357, "image_uri": "frame_1527.png"}, {"time": 774.865, "image_uri": "frame_1528.png"}, {"time": 775.372, "image_uri": "frame_1529.png"}, {"time": 775.88, "image_uri": "frame_1530.png"}, {"time": 776.387, "image_uri": "frame_1531.png"}, {"time": 776.894, "image_uri": "frame_1532.png"}, {"time": 777.402, "image_uri": "frame_1533.png"}, {"time": 777.909, "image_uri": "frame_1534.png"}, {"time": 778.417, "image_uri": "frame_1535.png"}, {"time": 778.924, "image_uri": "frame_1536.png"}, {"time": 779.432, "image_uri": "frame_1537.png"}, {"time": 779.939, "image_uri": "frame_1538.png"}, {"time": 780.447, "image_uri": "frame_1539.png"}, {"time": 780.954, "image_uri": "frame_1540.png"}, {"time": 781.461, "image_uri": "frame_1541.png"}, {"time": 781.969, "image_uri": "frame_1542.png"}, {"time": 782.476, "image_uri": "frame_1543.png"}, {"time": 782.984, "image_uri": "frame_1544.png"}, {"time": 783.491, "image_uri": "frame_1545.png"}, {"time": 783.999, "image_uri": "frame_1546.png"}, {"time": 784.506, "image_uri": "frame_1547.png"}, {"time": 785.014, "image_uri": "frame_1548.png"}, {"time": 785.521, "image_uri": "frame_1549.png"}, {"time": 786.028, "image_uri": "frame_1550.png"}, {"time": 786.536, "image_uri": "frame_1551.png"}, {"time": 787.043, "image_uri": "frame_1552.png"}, {"time": 787.551, "image_uri": "frame_1553.png"}, {"time": 788.058, "image_uri": "frame_1554.png"}, {"time": 788.566, "image_uri": "frame_1555.png"}, {"time": 789.073, "image_uri": "frame_1556.png"}, {"time": 789.581, "image_uri": "frame_1557.png"}, {"time": 790.088, "image_uri": "frame_1558.png"}, {"time": 790.595, "image_uri": "frame_1559.png"}, {"time": 791.103, "image_uri": "frame_1560.png"}, {"time": 791.61, "image_uri": "frame_1561.png"}, {"time": 792.118, "image_uri": "frame_1562.png"}, {"time": 792.625, "image_uri": "frame_1563.png"}, {"time": 793.133, "image_uri": "frame_1564.png"}, {"time": 793.64, "image_uri": "frame_1565.png"}, {"time": 794.147, "image_uri": "frame_1566.png"}, {"time": 794.655, "image_uri": "frame_1567.png"}, {"time": 795.162, "image_uri": "frame_1568.png"}, {"time": 795.67, "image_uri": "frame_1569.png"}, {"time": 796.177, "image_uri": "frame_1570.png"}, {"time": 796.685, "image_uri": "frame_1571.png"}, {"time": 797.192, "image_uri": "frame_1572.png"}, {"time": 797.7, "image_uri": "frame_1573.png"}, {"time": 798.207, "image_uri": "frame_1574.png"}, {"time": 798.714, "image_uri": "frame_1575.png"}, {"time": 799.222, "image_uri": "frame_1576.png"}, {"time": 799.729, "image_uri": "frame_1577.png"}, {"time": 800.237, "image_uri": "frame_1578.png"}, {"time": 800.744, "image_uri": "frame_1579.png"}, {"time": 801.252, "image_uri": "frame_1580.png"}, {"time": 801.759, "image_uri": "frame_1581.png"}, {"time": 802.267, "image_uri": "frame_1582.png"}, {"time": 802.774, "image_uri": "frame_1583.png"}, {"time": 803.281, "image_uri": "frame_1584.png"}, {"time": 803.789, "image_uri": "frame_1585.png"}, {"time": 804.296, "image_uri": "frame_1586.png"}, {"time": 804.804, "image_uri": "frame_1587.png"}, {"time": 805.311, "image_uri": "frame_1588.png"}, {"time": 805.819, "image_uri": "frame_1589.png"}, {"time": 806.326, "image_uri": "frame_1590.png"}, {"time": 806.834, "image_uri": "frame_1591.png"}, {"time": 807.341, "image_uri": "frame_1592.png"}, {"time": 807.848, "image_uri": "frame_1593.png"}, {"time": 808.356, "image_uri": "frame_1594.png"}, {"time": 808.863, "image_uri": "frame_1595.png"}, {"time": 809.371, "image_uri": "frame_1596.png"}, {"time": 809.878, "image_uri": "frame_1597.png"}, {"time": 810.386, "image_uri": "frame_1598.png"}, {"time": 810.893, "image_uri": "frame_1599.png"}, {"time": 811.401, "image_uri": "frame_1600.png"}, {"time": 811.908, "image_uri": "frame_1601.png"}, {"time": 812.415, "image_uri": "frame_1602.png"}, {"time": 812.923, "image_uri": "frame_1603.png"}, {"time": 813.43, "image_uri": "frame_1604.png"}, {"time": 813.938, "image_uri": "frame_1605.png"}, {"time": 814.445, "image_uri": "frame_1606.png"}, {"time": 814.953, "image_uri": "frame_1607.png"}, {"time": 815.46, "image_uri": "frame_1608.png"}, {"time": 815.968, "image_uri": "frame_1609.png"}, {"time": 816.475, "image_uri": "frame_1610.png"}, {"time": 816.982, "image_uri": "frame_1611.png"}, {"time": 817.49, "image_uri": "frame_1612.png"}, {"time": 817.997, "image_uri": "frame_1613.png"}, {"time": 818.505, "image_uri": "frame_1614.png"}, {"time": 819.012, "image_uri": "frame_1615.png"}, {"time": 819.52, "image_uri": "frame_1616.png"}, {"time": 820.027, "image_uri": "frame_1617.png"}, {"time": 820.535, "image_uri": "frame_1618.png"}, {"time": 821.042, "image_uri": "frame_1619.png"}, {"time": 821.549, "image_uri": "frame_1620.png"}, {"time": 822.057, "image_uri": "frame_1621.png"}, {"time": 822.564, "image_uri": "frame_1622.png"}, {"time": 823.072, "image_uri": "frame_1623.png"}, {"time": 823.579, "image_uri": "frame_1624.png"}, {"time": 824.087, "image_uri": "frame_1625.png"}, {"time": 824.594, "image_uri": "frame_1626.png"}, {"time": 825.101, "image_uri": "frame_1627.png"}, {"time": 825.609, "image_uri": "frame_1628.png"}, {"time": 826.116, "image_uri": "frame_1629.png"}, {"time": 826.624, "image_uri": "frame_1630.png"}, {"time": 827.131, "image_uri": "frame_1631.png"}, {"time": 827.639, "image_uri": "frame_1632.png"}, {"time": 828.146, "image_uri": "frame_1633.png"}, {"time": 828.654, "image_uri": "frame_1634.png"}, {"time": 829.161, "image_uri": "frame_1635.png"}, {"time": 829.668, "image_uri": "frame_1636.png"}, {"time": 830.176, "image_uri": "frame_1637.png"}, {"time": 830.683, "image_uri": "frame_1638.png"}, {"time": 831.191, "image_uri": "frame_1639.png"}, {"time": 831.698, "image_uri": "frame_1640.png"}, {"time": 832.206, "image_uri": "frame_1641.png"}, {"time": 832.713, "image_uri": "frame_1642.png"}, {"time": 833.221, "image_uri": "frame_1643.png"}, {"time": 833.728, "image_uri": "frame_1644.png"}, {"time": 834.235, "image_uri": "frame_1645.png"}, {"time": 834.743, "image_uri": "frame_1646.png"}, {"time": 835.25, "image_uri": "frame_1647.png"}, {"time": 835.758, "image_uri": "frame_1648.png"}, {"time": 836.265, "image_uri": "frame_1649.png"}, {"time": 836.773, "image_uri": "frame_1650.png"}, {"time": 837.28, "image_uri": "frame_1651.png"}, {"time": 837.788, "image_uri": "frame_1652.png"}, {"time": 838.295, "image_uri": "frame_1653.png"}, {"time": 838.802, "image_uri": "frame_1654.png"}, {"time": 839.31, "image_uri": "frame_1655.png"}, {"time": 839.817, "image_uri": "frame_1656.png"}, {"time": 840.325, "image_uri": "frame_1657.png"}, {"time": 840.832, "image_uri": "frame_1658.png"}, {"time": 841.34, "image_uri": "frame_1659.png"}, {"time": 841.847, "image_uri": "frame_1660.png"}, {"time": 842.355, "image_uri": "frame_1661.png"}, {"time": 842.862, "image_uri": "frame_1662.png"}, {"time": 843.369, "image_uri": "frame_1663.png"}, {"time": 843.877, "image_uri": "frame_1664.png"}, {"time": 844.384, "image_uri": "frame_1665.png"}, {"time": 844.892, "image_uri": "frame_1666.png"}, {"time": 845.399, "image_uri": "frame_1667.png"}, {"time": 845.907, "image_uri": "frame_1668.png"}, {"time": 846.414, "image_uri": "frame_1669.png"}, {"time": 846.922, "image_uri": "frame_1670.png"}, {"time": 847.429, "image_uri": "frame_1671.png"}, {"time": 847.936, "image_uri": "frame_1672.png"}, {"time": 848.444, "image_uri": "frame_1673.png"}, {"time": 848.951, "image_uri": "frame_1674.png"}, {"time": 849.459, "image_uri": "frame_1675.png"}, {"time": 849.966, "image_uri": "frame_1676.png"}, {"time": 850.474, "image_uri": "frame_1677.png"}, {"time": 850.981, "image_uri": "frame_1678.png"}, {"time": 851.488, "image_uri": "frame_1679.png"}, {"time": 851.996, "image_uri": "frame_1680.png"}, {"time": 852.503, "image_uri": "frame_1681.png"}, {"time": 853.011, "image_uri": "frame_1682.png"}, {"time": 853.518, "image_uri": "frame_1683.png"}, {"time": 854.026, "image_uri": "frame_1684.png"}, {"time": 854.533, "image_uri": "frame_1685.png"}, {"time": 855.041, "image_uri": "frame_1686.png"}, {"time": 855.548, "image_uri": "frame_1687.png"}, {"time": 856.055, "image_uri": "frame_1688.png"}, {"time": 856.563, "image_uri": "frame_1689.png"}, {"time": 857.07, "image_uri": "frame_1690.png"}, {"time": 857.578, "image_uri": "frame_1691.png"}, {"time": 858.085, "image_uri": "frame_1692.png"}, {"time": 858.593, "image_uri": "frame_1693.png"}, {"time": 859.1, "image_uri": "frame_1694.png"}, {"time": 859.608, "image_uri": "frame_1695.png"}, {"time": 860.115, "image_uri": "frame_1696.png"}, {"time": 860.622, "image_uri": "frame_1697.png"}, {"time": 861.13, "image_uri": "frame_1698.png"}, {"time": 861.637, "image_uri": "frame_1699.png"}, {"time": 862.145, "image_uri": "frame_1700.png"}, {"time": 862.652, "image_uri": "frame_1701.png"}, {"time": 863.16, "image_uri": "frame_1702.png"}, {"time": 863.667, "image_uri": "frame_1703.png"}, {"time": 864.175, "image_uri": "frame_1704.png"}, {"time": 864.682, "image_uri": "frame_1705.png"}, {"time": 865.189, "image_uri": "frame_1706.png"}, {"time": 865.697, "image_uri": "frame_1707.png"}, {"time": 866.204, "image_uri": "frame_1708.png"}, {"time": 866.712, "image_uri": "frame_1709.png"}, {"time": 867.219, "image_uri": "frame_1710.png"}, {"time": 867.727, "image_uri": "frame_1711.png"}, {"time": 868.234, "image_uri": "frame_1712.png"}, {"time": 868.742, "image_uri": "frame_1713.png"}, {"time": 869.249, "image_uri": "frame_1714.png"}, {"time": 869.756, "image_uri": "frame_1715.png"}, {"time": 870.264, "image_uri": "frame_1716.png"}, {"time": 870.771, "image_uri": "frame_1717.png"}, {"time": 871.279, "image_uri": "frame_1718.png"}, {"time": 871.786, "image_uri": "frame_1719.png"}, {"time": 872.294, "image_uri": "frame_1720.png"}, {"time": 872.801, "image_uri": "frame_1721.png"}, {"time": 873.309, "image_uri": "frame_1722.png"}, {"time": 873.816, "image_uri": "frame_1723.png"}, {"time": 874.323, "image_uri": "frame_1724.png"}, {"time": 874.831, "image_uri": "frame_1725.png"}, {"time": 875.338, "image_uri": "frame_1726.png"}, {"time": 875.846, "image_uri": "frame_1727.png"}, {"time": 876.353, "image_uri": "frame_1728.png"}, {"time": 876.861, "image_uri": "frame_1729.png"}, {"time": 877.368, "image_uri": "frame_1730.png"}, {"time": 877.876, "image_uri": "frame_1731.png"}, {"time": 878.383, "image_uri": "frame_1732.png"}, {"time": 878.89, "image_uri": "frame_1733.png"}, {"time": 879.398, "image_uri": "frame_1734.png"}, {"time": 879.905, "image_uri": "frame_1735.png"}, {"time": 880.413, "image_uri": "frame_1736.png"}, {"time": 880.92, "image_uri": "frame_1737.png"}, {"time": 881.428, "image_uri": "frame_1738.png"}, {"time": 881.935, "image_uri": "frame_1739.png"}, {"time": 882.442, "image_uri": "frame_1740.png"}, {"time": 882.95, "image_uri": "frame_1741.png"}, {"time": 883.457, "image_uri": "frame_1742.png"}, {"time": 883.965, "image_uri": "frame_1743.png"}, {"time": 884.472, "image_uri": "frame_1744.png"}, {"time": 884.98, "image_uri": "frame_1745.png"}, {"time": 885.487, "image_uri": "frame_1746.png"}, {"time": 885.995, "image_uri": "frame_1747.png"}, {"time": 886.502, "image_uri": "frame_1748.png"}, {"time": 887.009, "image_uri": "frame_1749.png"}, {"time": 887.517, "image_uri": "frame_1750.png"}, {"time": 888.024, "image_uri": "frame_1751.png"}, {"time": 888.532, "image_uri": "frame_1752.png"}, {"time": 889.039, "image_uri": "frame_1753.png"}, {"time": 889.547, "image_uri": "frame_1754.png"}, {"time": 890.054, "image_uri": "frame_1755.png"}, {"time": 890.562, "image_uri": "frame_1756.png"}, {"time": 891.069, "image_uri": "frame_1757.png"}, {"time": 891.576, "image_uri": "frame_1758.png"}, {"time": 892.084, "image_uri": "frame_1759.png"}, {"time": 892.591, "image_uri": "frame_1760.png"}, {"time": 893.099, "image_uri": "frame_1761.png"}, {"time": 893.606, "image_uri": "frame_1762.png"}, {"time": 894.114, "image_uri": "frame_1763.png"}, {"time": 894.621, "image_uri": "frame_1764.png"}, {"time": 895.129, "image_uri": "frame_1765.png"}, {"time": 895.636, "image_uri": "frame_1766.png"}, {"time": 896.143, "image_uri": "frame_1767.png"}, {"time": 896.651, "image_uri": "frame_1768.png"}, {"time": 897.158, "image_uri": "frame_1769.png"}, {"time": 897.666, "image_uri": "frame_1770.png"}, {"time": 898.173, "image_uri": "frame_1771.png"}, {"time": 898.681, "image_uri": "frame_1772.png"}, {"time": 899.188, "image_uri": "frame_1773.png"}, {"time": 899.696, "image_uri": "frame_1774.png"}, {"time": 900.203, "image_uri": "frame_1775.png"}, {"time": 900.71, "image_uri": "frame_1776.png"}, {"time": 901.218, "image_uri": "frame_1777.png"}, {"time": 901.725, "image_uri": "frame_1778.png"}, {"time": 902.233, "image_uri": "frame_1779.png"}, {"time": 902.74, "image_uri": "frame_1780.png"}, {"time": 903.248, "image_uri": "frame_1781.png"}, {"time": 903.755, "image_uri": "frame_1782.png"}, {"time": 904.263, "image_uri": "frame_1783.png"}, {"time": 904.77, "image_uri": "frame_1784.png"}, {"time": 905.277, "image_uri": "frame_1785.png"}, {"time": 905.785, "image_uri": "frame_1786.png"}, {"time": 906.292, "image_uri": "frame_1787.png"}, {"time": 906.8, "image_uri": "frame_1788.png"}, {"time": 907.307, "image_uri": "frame_1789.png"}, {"time": 907.815, "image_uri": "frame_1790.png"}, {"time": 908.322, "image_uri": "frame_1791.png"}, {"time": 908.829, "image_uri": "frame_1792.png"}, {"time": 909.337, "image_uri": "frame_1793.png"}, {"time": 909.844, "image_uri": "frame_1794.png"}, {"time": 910.352, "image_uri": "frame_1795.png"}, {"time": 910.859, "image_uri": "frame_1796.png"}, {"time": 911.367, "image_uri": "frame_1797.png"}, {"time": 911.874, "image_uri": "frame_1798.png"}, {"time": 912.382, "image_uri": "frame_1799.png"}, {"time": 912.889, "image_uri": "frame_1800.png"}, {"time": 913.396, "image_uri": "frame_1801.png"}, {"time": 913.904, "image_uri": "frame_1802.png"}, {"time": 914.411, "image_uri": "frame_1803.png"}, {"time": 914.919, "image_uri": "frame_1804.png"}, {"time": 915.426, "image_uri": "frame_1805.png"}, {"time": 915.934, "image_uri": "frame_1806.png"}, {"time": 916.441, "image_uri": "frame_1807.png"}, {"time": 916.949, "image_uri": "frame_1808.png"}, {"time": 917.456, "image_uri": "frame_1809.png"}, {"time": 917.963, "image_uri": "frame_1810.png"}, {"time": 918.471, "image_uri": "frame_1811.png"}, {"time": 918.978, "image_uri": "frame_1812.png"}, {"time": 919.486, "image_uri": "frame_1813.png"}, {"time": 919.993, "image_uri": "frame_1814.png"}, {"time": 920.501, "image_uri": "frame_1815.png"}, {"time": 921.008, "image_uri": "frame_1816.png"}, {"time": 921.516, "image_uri": "frame_1817.png"}, {"time": 922.023, "image_uri": "frame_1818.png"}, {"time": 922.53, "image_uri": "frame_1819.png"}, {"time": 923.038, "image_uri": "frame_1820.png"}, {"time": 923.545, "image_uri": "frame_1821.png"}, {"time": 924.053, "image_uri": "frame_1822.png"}, {"time": 924.56, "image_uri": "frame_1823.png"}, {"time": 925.068, "image_uri": "frame_1824.png"}, {"time": 925.575, "image_uri": "frame_1825.png"}, {"time": 926.083, "image_uri": "frame_1826.png"}, {"time": 926.59, "image_uri": "frame_1827.png"}, {"time": 927.097, "image_uri": "frame_1828.png"}, {"time": 927.605, "image_uri": "frame_1829.png"}, {"time": 928.112, "image_uri": "frame_1830.png"}, {"time": 928.62, "image_uri": "frame_1831.png"}, {"time": 929.127, "image_uri": "frame_1832.png"}, {"time": 929.635, "image_uri": "frame_1833.png"}, {"time": 930.142, "image_uri": "frame_1834.png"}, {"time": 930.65, "image_uri": "frame_1835.png"}, {"time": 931.157, "image_uri": "frame_1836.png"}, {"time": 931.664, "image_uri": "frame_1837.png"}, {"time": 932.172, "image_uri": "frame_1838.png"}, {"time": 932.679, "image_uri": "frame_1839.png"}, {"time": 933.187, "image_uri": "frame_1840.png"}, {"time": 933.694, "image_uri": "frame_1841.png"}, {"time": 934.202, "image_uri": "frame_1842.png"}, {"time": 934.709, "image_uri": "frame_1843.png"}, {"time": 935.217, "image_uri": "frame_1844.png"}, {"time": 935.724, "image_uri": "frame_1845.png"}, {"time": 936.231, "image_uri": "frame_1846.png"}, {"time": 936.739, "image_uri": "frame_1847.png"}, {"time": 937.246, "image_uri": "frame_1848.png"}, {"time": 937.754, "image_uri": "frame_1849.png"}, {"time": 938.261, "image_uri": "frame_1850.png"}, {"time": 938.769, "image_uri": "frame_1851.png"}, {"time": 939.276, "image_uri": "frame_1852.png"}, {"time": 939.783, "image_uri": "frame_1853.png"}, {"time": 940.291, "image_uri": "frame_1854.png"}, {"time": 940.798, "image_uri": "frame_1855.png"}, {"time": 941.306, "image_uri": "frame_1856.png"}, {"time": 941.813, "image_uri": "frame_1857.png"}, {"time": 942.321, "image_uri": "frame_1858.png"}, {"time": 942.828, "image_uri": "frame_1859.png"}, {"time": 943.336, "image_uri": "frame_1860.png"}, {"time": 943.843, "image_uri": "frame_1861.png"}, {"time": 944.35, "image_uri": "frame_1862.png"}, {"time": 944.858, "image_uri": "frame_1863.png"}, {"time": 945.365, "image_uri": "frame_1864.png"}, {"time": 945.873, "image_uri": "frame_1865.png"}, {"time": 946.38, "image_uri": "frame_1866.png"}, {"time": 946.888, "image_uri": "frame_1867.png"}, {"time": 947.395, "image_uri": "frame_1868.png"}, {"time": 947.903, "image_uri": "frame_1869.png"}, {"time": 948.41, "image_uri": "frame_1870.png"}, {"time": 948.917, "image_uri": "frame_1871.png"}, {"time": 949.425, "image_uri": "frame_1872.png"}, {"time": 949.932, "image_uri": "frame_1873.png"}, {"time": 950.44, "image_uri": "frame_1874.png"}, {"time": 950.947, "image_uri": "frame_1875.png"}, {"time": 951.455, "image_uri": "frame_1876.png"}, {"time": 951.962, "image_uri": "frame_1877.png"}, {"time": 952.47, "image_uri": "frame_1878.png"}, {"time": 952.977, "image_uri": "frame_1879.png"}, {"time": 953.484, "image_uri": "frame_1880.png"}, {"time": 953.992, "image_uri": "frame_1881.png"}, {"time": 954.499, "image_uri": "frame_1882.png"}, {"time": 955.007, "image_uri": "frame_1883.png"}, {"time": 955.514, "image_uri": "frame_1884.png"}, {"time": 956.022, "image_uri": "frame_1885.png"}, {"time": 956.529, "image_uri": "frame_1886.png"}, {"time": 957.037, "image_uri": "frame_1887.png"}, {"time": 957.544, "image_uri": "frame_1888.png"}, {"time": 958.051, "image_uri": "frame_1889.png"}, {"time": 958.559, "image_uri": "frame_1890.png"}, {"time": 959.066, "image_uri": "frame_1891.png"}, {"time": 959.574, "image_uri": "frame_1892.png"}, {"time": 960.081, "image_uri": "frame_1893.png"}, {"time": 960.589, "image_uri": "frame_1894.png"}, {"time": 961.096, "image_uri": "frame_1895.png"}, {"time": 961.604, "image_uri": "frame_1896.png"}, {"time": 962.111, "image_uri": "frame_1897.png"}, {"time": 962.618, "image_uri": "frame_1898.png"}, {"time": 963.126, "image_uri": "frame_1899.png"}, {"time": 963.633, "image_uri": "frame_1900.png"}, {"time": 964.141, "image_uri": "frame_1901.png"}, {"time": 964.648, "image_uri": "frame_1902.png"}, {"time": 965.156, "image_uri": "frame_1903.png"}, {"time": 965.663, "image_uri": "frame_1904.png"}, {"time": 966.171, "image_uri": "frame_1905.png"}, {"time": 966.678, "image_uri": "frame_1906.png"}, {"time": 967.185, "image_uri": "frame_1907.png"}, {"time": 967.693, "image_uri": "frame_1908.png"}, {"time": 968.2, "image_uri": "frame_1909.png"}, {"time": 968.708, "image_uri": "frame_1910.png"}, {"time": 969.215, "image_uri": "frame_1911.png"}, {"time": 969.723, "image_uri": "frame_1912.png"}, {"time": 970.23, "image_uri": "frame_1913.png"}, {"time": 970.737, "image_uri": "frame_1914.png"}, {"time": 971.245, "image_uri": "frame_1915.png"}, {"time": 971.752, "image_uri": "frame_1916.png"}, {"time": 972.26, "image_uri": "frame_1917.png"}, {"time": 972.767, "image_uri": "frame_1918.png"}, {"time": 973.275, "image_uri": "frame_1919.png"}, {"time": 973.782, "image_uri": "frame_1920.png"}, {"time": 974.29, "image_uri": "frame_1921.png"}, {"time": 974.797, "image_uri": "frame_1922.png"}, {"time": 975.304, "image_uri": "frame_1923.png"}, {"time": 975.812, "image_uri": "frame_1924.png"}, {"time": 976.319, "image_uri": "frame_1925.png"}, {"time": 976.827, "image_uri": "frame_1926.png"}, {"time": 977.334, "image_uri": "frame_1927.png"}, {"time": 977.842, "image_uri": "frame_1928.png"}, {"time": 978.349, "image_uri": "frame_1929.png"}, {"time": 978.857, "image_uri": "frame_1930.png"}, {"time": 979.364, "image_uri": "frame_1931.png"}, {"time": 979.871, "image_uri": "frame_1932.png"}, {"time": 980.379, "image_uri": "frame_1933.png"}, {"time": 980.886, "image_uri": "frame_1934.png"}, {"time": 981.394, "image_uri": "frame_1935.png"}, {"time": 981.901, "image_uri": "frame_1936.png"}, {"time": 982.409, "image_uri": "frame_1937.png"}, {"time": 982.916, "image_uri": "frame_1938.png"}, {"time": 983.424, "image_uri": "frame_1939.png"}, {"time": 983.931, "image_uri": "frame_1940.png"}, {"time": 984.438, "image_uri": "frame_1941.png"}, {"time": 984.946, "image_uri": "frame_1942.png"}, {"time": 985.453, "image_uri": "frame_1943.png"}, {"time": 985.961, "image_uri": "frame_1944.png"}, {"time": 986.468, "image_uri": "frame_1945.png"}, {"time": 986.976, "image_uri": "frame_1946.png"}, {"time": 987.483, "image_uri": "frame_1947.png"}, {"time": 987.991, "image_uri": "frame_1948.png"}, {"time": 988.498, "image_uri": "frame_1949.png"}, {"time": 989.005, "image_uri": "frame_1950.png"}, {"time": 989.513, "image_uri": "frame_1951.png"}, {"time": 990.02, "image_uri": "frame_1952.png"}, {"time": 990.528, "image_uri": "frame_1953.png"}, {"time": 991.035, "image_uri": "frame_1954.png"}, {"time": 991.543, "image_uri": "frame_1955.png"}, {"time": 992.05, "image_uri": "frame_1956.png"}, {"time": 992.558, "image_uri": "frame_1957.png"}, {"time": 993.065, "image_uri": "frame_1958.png"}, {"time": 993.572, "image_uri": "frame_1959.png"}, {"time": 994.08, "image_uri": "frame_1960.png"}, {"time": 994.587, "image_uri": "frame_1961.png"}, {"time": 995.095, "image_uri": "frame_1962.png"}, {"time": 995.602, "image_uri": "frame_1963.png"}, {"time": 996.11, "image_uri": "frame_1964.png"}, {"time": 996.617, "image_uri": "frame_1965.png"}, {"time": 997.124, "image_uri": "frame_1966.png"}, {"time": 997.632, "image_uri": "frame_1967.png"}, {"time": 998.139, "image_uri": "frame_1968.png"}, {"time": 998.647, "image_uri": "frame_1969.png"}, {"time": 999.154, "image_uri": "frame_1970.png"}, {"time": 999.662, "image_uri": "frame_1971.png"}, {"time": 1000.169, "image_uri": "frame_1972.png"}, {"time": 1000.677, "image_uri": "frame_1973.png"}, {"time": 1001.184, "image_uri": "frame_1974.png"}, {"time": 1001.691, "image_uri": "frame_1975.png"}, {"time": 1002.199, "image_uri": "frame_1976.png"}, {"time": 1002.706, "image_uri": "frame_1977.png"}, {"time": 1003.214, "image_uri": "frame_1978.png"}, {"time": 1003.721, "image_uri": "frame_1979.png"}, {"time": 1004.229, "image_uri": "frame_1980.png"}, {"time": 1004.736, "image_uri": "frame_1981.png"}, {"time": 1005.244, "image_uri": "frame_1982.png"}, {"time": 1005.751, "image_uri": "frame_1983.png"}, {"time": 1006.258, "image_uri": "frame_1984.png"}, {"time": 1006.766, "image_uri": "frame_1985.png"}, {"time": 1007.273, "image_uri": "frame_1986.png"}, {"time": 1007.781, "image_uri": "frame_1987.png"}, {"time": 1008.288, "image_uri": "frame_1988.png"}, {"time": 1008.796, "image_uri": "frame_1989.png"}, {"time": 1009.303, "image_uri": "frame_1990.png"}, {"time": 1009.811, "image_uri": "frame_1991.png"}, {"time": 1010.318, "image_uri": "frame_1992.png"}, {"time": 1010.825, "image_uri": "frame_1993.png"}, {"time": 1011.333, "image_uri": "frame_1994.png"}, {"time": 1011.84, "image_uri": "frame_1995.png"}, {"time": 1012.348, "image_uri": "frame_1996.png"}, {"time": 1012.855, "image_uri": "frame_1997.png"}, {"time": 1013.363, "image_uri": "frame_1998.png"}, {"time": 1013.87, "image_uri": "frame_1999.png"}, {"time": 1014.378, "image_uri": "frame_2000.png"}, {"time": 1014.885, "image_uri": "frame_2001.png"}, {"time": 1015.392, "image_uri": "frame_2002.png"}, {"time": 1015.9, "image_uri": "frame_2003.png"}, {"time": 1016.407, "image_uri": "frame_2004.png"}, {"time": 1016.915, "image_uri": "frame_2005.png"}, {"time": 1017.422, "image_uri": "frame_2006.png"}, {"time": 1017.93, "image_uri": "frame_2007.png"}, {"time": 1018.437, "image_uri": "frame_2008.png"}, {"time": 1018.945, "image_uri": "frame_2009.png"}, {"time": 1019.452, "image_uri": "frame_2010.png"}, {"time": 1019.959, "image_uri": "frame_2011.png"}, {"time": 1020.467, "image_uri": "frame_2012.png"}, {"time": 1020.974, "image_uri": "frame_2013.png"}, {"time": 1021.482, "image_uri": "frame_2014.png"}, {"time": 1021.989, "image_uri": "frame_2015.png"}, {"time": 1022.497, "image_uri": "frame_2016.png"}, {"time": 1023.004, "image_uri": "frame_2017.png"}, {"time": 1023.512, "image_uri": "frame_2018.png"}, {"time": 1024.019, "image_uri": "frame_2019.png"}, {"time": 1024.526, "image_uri": "frame_2020.png"}, {"time": 1025.034, "image_uri": "frame_2021.png"}, {"time": 1025.541, "image_uri": "frame_2022.png"}, {"time": 1026.049, "image_uri": "frame_2023.png"}, {"time": 1026.556, "image_uri": "frame_2024.png"}, {"time": 1027.064, "image_uri": "frame_2025.png"}, {"time": 1027.571, "image_uri": "frame_2026.png"}, {"time": 1028.078, "image_uri": "frame_2027.png"}, {"time": 1028.586, "image_uri": "frame_2028.png"}, {"time": 1029.093, "image_uri": "frame_2029.png"}, {"time": 1029.601, "image_uri": "frame_2030.png"}, {"time": 1030.108, "image_uri": "frame_2031.png"}, {"time": 1030.616, "image_uri": "frame_2032.png"}, {"time": 1031.123, "image_uri": "frame_2033.png"}, {"time": 1031.631, "image_uri": "frame_2034.png"}, {"time": 1032.138, "image_uri": "frame_2035.png"}, {"time": 1032.645, "image_uri": "frame_2036.png"}, {"time": 1033.153, "image_uri": "frame_2037.png"}, {"time": 1033.66, "image_uri": "frame_2038.png"}, {"time": 1034.168, "image_uri": "frame_2039.png"}, {"time": 1034.675, "image_uri": "frame_2040.png"}, {"time": 1035.183, "image_uri": "frame_2041.png"}, {"time": 1035.69, "image_uri": "frame_2042.png"}, {"time": 1036.198, "image_uri": "frame_2043.png"}, {"time": 1036.705, "image_uri": "frame_2044.png"}, {"time": 1037.212, "image_uri": "frame_2045.png"}, {"time": 1037.72, "image_uri": "frame_2046.png"}, {"time": 1038.227, "image_uri": "frame_2047.png"}, {"time": 1038.735, "image_uri": "frame_2048.png"}, {"time": 1039.242, "image_uri": "frame_2049.png"}, {"time": 1039.75, "image_uri": "frame_2050.png"}, {"time": 1040.257, "image_uri": "frame_2051.png"}, {"time": 1040.765, "image_uri": "frame_2052.png"}, {"time": 1041.272, "image_uri": "frame_2053.png"}, {"time": 1041.779, "image_uri": "frame_2054.png"}, {"time": 1042.287, "image_uri": "frame_2055.png"}, {"time": 1042.794, "image_uri": "frame_2056.png"}, {"time": 1043.302, "image_uri": "frame_2057.png"}, {"time": 1043.809, "image_uri": "frame_2058.png"}, {"time": 1044.317, "image_uri": "frame_2059.png"}, {"time": 1044.824, "image_uri": "frame_2060.png"}, {"time": 1045.332, "image_uri": "frame_2061.png"}, {"time": 1045.839, "image_uri": "frame_2062.png"}, {"time": 1046.346, "image_uri": "frame_2063.png"}, {"time": 1046.854, "image_uri": "frame_2064.png"}, {"time": 1047.361, "image_uri": "frame_2065.png"}, {"time": 1047.869, "image_uri": "frame_2066.png"}, {"time": 1048.376, "image_uri": "frame_2067.png"}, {"time": 1048.884, "image_uri": "frame_2068.png"}, {"time": 1049.391, "image_uri": "frame_2069.png"}, {"time": 1049.899, "image_uri": "frame_2070.png"}, {"time": 1050.406, "image_uri": "frame_2071.png"}, {"time": 1050.913, "image_uri": "frame_2072.png"}, {"time": 1051.421, "image_uri": "frame_2073.png"}, {"time": 1051.928, "image_uri": "frame_2074.png"}, {"time": 1052.436, "image_uri": "frame_2075.png"}, {"time": 1052.943, "image_uri": "frame_2076.png"}, {"time": 1053.451, "image_uri": "frame_2077.png"}, {"time": 1053.958, "image_uri": "frame_2078.png"}, {"time": 1054.465, "image_uri": "frame_2079.png"}, {"time": 1054.973, "image_uri": "frame_2080.png"}, {"time": 1055.48, "image_uri": "frame_2081.png"}, {"time": 1055.988, "image_uri": "frame_2082.png"}, {"time": 1056.495, "image_uri": "frame_2083.png"}, {"time": 1057.003, "image_uri": "frame_2084.png"}, {"time": 1057.51, "image_uri": "frame_2085.png"}, {"time": 1058.018, "image_uri": "frame_2086.png"}, {"time": 1058.525, "image_uri": "frame_2087.png"}, {"time": 1059.032, "image_uri": "frame_2088.png"}, {"time": 1059.54, "image_uri": "frame_2089.png"}, {"time": 1060.047, "image_uri": "frame_2090.png"}, {"time": 1060.555, "image_uri": "frame_2091.png"}, {"time": 1061.062, "image_uri": "frame_2092.png"}, {"time": 1061.57, "image_uri": "frame_2093.png"}, {"time": 1062.077, "image_uri": "frame_2094.png"}, {"time": 1062.585, "image_uri": "frame_2095.png"}, {"time": 1063.092, "image_uri": "frame_2096.png"}, {"time": 1063.599, "image_uri": "frame_2097.png"}, {"time": 1064.107, "image_uri": "frame_2098.png"}, {"time": 1064.614, "image_uri": "frame_2099.png"}, {"time": 1065.122, "image_uri": "frame_2100.png"}, {"time": 1065.629, "image_uri": "frame_2101.png"}, {"time": 1066.137, "image_uri": "frame_2102.png"}, {"time": 1066.644, "image_uri": "frame_2103.png"}, {"time": 1067.152, "image_uri": "frame_2104.png"}, {"time": 1067.659, "image_uri": "frame_2105.png"}, {"time": 1068.166, "image_uri": "frame_2106.png"}, {"time": 1068.674, "image_uri": "frame_2107.png"}, {"time": 1069.181, "image_uri": "frame_2108.png"}, {"time": 1069.689, "image_uri": "frame_2109.png"}, {"time": 1070.196, "image_uri": "frame_2110.png"}, {"time": 1070.704, "image_uri": "frame_2111.png"}, {"time": 1071.211, "image_uri": "frame_2112.png"}, {"time": 1071.719, "image_uri": "frame_2113.png"}, {"time": 1072.226, "image_uri": "frame_2114.png"}, {"time": 1072.733, "image_uri": "frame_2115.png"}, {"time": 1073.241, "image_uri": "frame_2116.png"}, {"time": 1073.748, "image_uri": "frame_2117.png"}, {"time": 1074.256, "image_uri": "frame_2118.png"}, {"time": 1074.763, "image_uri": "frame_2119.png"}, {"time": 1075.271, "image_uri": "frame_2120.png"}, {"time": 1075.778, "image_uri": "frame_2121.png"}, {"time": 1076.286, "image_uri": "frame_2122.png"}, {"time": 1076.793, "image_uri": "frame_2123.png"}, {"time": 1077.3, "image_uri": "frame_2124.png"}, {"time": 1077.808, "image_uri": "frame_2125.png"}, {"time": 1078.315, "image_uri": "frame_2126.png"}, {"time": 1078.823, "image_uri": "frame_2127.png"}, {"time": 1079.33, "image_uri": "frame_2128.png"}, {"time": 1079.838, "image_uri": "frame_2129.png"}, {"time": 1080.345, "image_uri": "frame_2130.png"}, {"time": 1080.853, "image_uri": "frame_2131.png"}, {"time": 1081.36, "image_uri": "frame_2132.png"}, {"time": 1081.867, "image_uri": "frame_2133.png"}, {"time": 1082.375, "image_uri": "frame_2134.png"}, {"time": 1082.882, "image_uri": "frame_2135.png"}, {"time": 1083.39, "image_uri": "frame_2136.png"}, {"time": 1083.897, "image_uri": "frame_2137.png"}, {"time": 1084.405, "image_uri": "frame_2138.png"}, {"time": 1084.912, "image_uri": "frame_2139.png"}, {"time": 1085.419, "image_uri": "frame_2140.png"}, {"time": 1085.927, "image_uri": "frame_2141.png"}, {"time": 1086.434, "image_uri": "frame_2142.png"}, {"time": 1086.942, "image_uri": "frame_2143.png"}, {"time": 1087.449, "image_uri": "frame_2144.png"}, {"time": 1087.957, "image_uri": "frame_2145.png"}, {"time": 1088.464, "image_uri": "frame_2146.png"}, {"time": 1088.972, "image_uri": "frame_2147.png"}, {"time": 1089.479, "image_uri": "frame_2148.png"}, {"time": 1089.986, "image_uri": "frame_2149.png"}, {"time": 1090.494, "image_uri": "frame_2150.png"}, {"time": 1091.001, "image_uri": "frame_2151.png"}, {"time": 1091.509, "image_uri": "frame_2152.png"}, {"time": 1092.016, "image_uri": "frame_2153.png"}, {"time": 1092.524, "image_uri": "frame_2154.png"}, {"time": 1093.031, "image_uri": "frame_2155.png"}, {"time": 1093.539, "image_uri": "frame_2156.png"}, {"time": 1094.046, "image_uri": "frame_2157.png"}, {"time": 1094.553, "image_uri": "frame_2158.png"}, {"time": 1095.061, "image_uri": "frame_2159.png"}, {"time": 1095.568, "image_uri": "frame_2160.png"}, {"time": 1096.076, "image_uri": "frame_2161.png"}, {"time": 1096.583, "image_uri": "frame_2162.png"}, {"time": 1097.091, "image_uri": "frame_2163.png"}, {"time": 1097.598, "image_uri": "frame_2164.png"}, {"time": 1098.106, "image_uri": "frame_2165.png"}, {"time": 1098.613, "image_uri": "frame_2166.png"}, {"time": 1099.12, "image_uri": "frame_2167.png"}, {"time": 1099.628, "image_uri": "frame_2168.png"}, {"time": 1100.135, "image_uri": "frame_2169.png"}, {"time": 1100.643, "image_uri": "frame_2170.png"}, {"time": 1101.15, "image_uri": "frame_2171.png"}, {"time": 1101.658, "image_uri": "frame_2172.png"}, {"time": 1102.165, "image_uri": "frame_2173.png"}, {"time": 1102.673, "image_uri": "frame_2174.png"}, {"time": 1103.18, "image_uri": "frame_2175.png"}, {"time": 1103.687, "image_uri": "frame_2176.png"}, {"time": 1104.195, "image_uri": "frame_2177.png"}, {"time": 1104.702, "image_uri": "frame_2178.png"}, {"time": 1105.21, "image_uri": "frame_2179.png"}, {"time": 1105.717, "image_uri": "frame_2180.png"}, {"time": 1106.225, "image_uri": "frame_2181.png"}, {"time": 1106.732, "image_uri": "frame_2182.png"}, {"time": 1107.24, "image_uri": "frame_2183.png"}, {"time": 1107.747, "image_uri": "frame_2184.png"}, {"time": 1108.254, "image_uri": "frame_2185.png"}, {"time": 1108.762, "image_uri": "frame_2186.png"}, {"time": 1109.269, "image_uri": "frame_2187.png"}, {"time": 1109.777, "image_uri": "frame_2188.png"}, {"time": 1110.284, "image_uri": "frame_2189.png"}, {"time": 1110.792, "image_uri": "frame_2190.png"}, {"time": 1111.299, "image_uri": "frame_2191.png"}, {"time": 1111.806, "image_uri": "frame_2192.png"}, {"time": 1112.314, "image_uri": "frame_2193.png"}, {"time": 1112.821, "image_uri": "frame_2194.png"}, {"time": 1113.329, "image_uri": "frame_2195.png"}, {"time": 1113.836, "image_uri": "frame_2196.png"}, {"time": 1114.344, "image_uri": "frame_2197.png"}, {"time": 1114.851, "image_uri": "frame_2198.png"}, {"time": 1115.359, "image_uri": "frame_2199.png"}, {"time": 1115.866, "image_uri": "frame_2200.png"}, {"time": 1116.373, "image_uri": "frame_2201.png"}, {"time": 1116.881, "image_uri": "frame_2202.png"}, {"time": 1117.388, "image_uri": "frame_2203.png"}, {"time": 1117.896, "image_uri": "frame_2204.png"}, {"time": 1118.403, "image_uri": "frame_2205.png"}, {"time": 1118.911, "image_uri": "frame_2206.png"}, {"time": 1119.418, "image_uri": "frame_2207.png"}, {"time": 1119.926, "image_uri": "frame_2208.png"}, {"time": 1120.433, "image_uri": "frame_2209.png"}, {"time": 1120.94, "image_uri": "frame_2210.png"}, {"time": 1121.448, "image_uri": "frame_2211.png"}, {"time": 1121.955, "image_uri": "frame_2212.png"}, {"time": 1122.463, "image_uri": "frame_2213.png"}, {"time": 1122.97, "image_uri": "frame_2214.png"}, {"time": 1123.478, "image_uri": "frame_2215.png"}, {"time": 1123.985, "image_uri": "frame_2216.png"}, {"time": 1124.493, "image_uri": "frame_2217.png"}, {"time": 1125.0, "image_uri": "frame_2218.png"}, {"time": 1125.507, "image_uri": "frame_2219.png"}, {"time": 1126.015, "image_uri": "frame_2220.png"}, {"time": 1126.522, "image_uri": "frame_2221.png"}, {"time": 1127.03, "image_uri": "frame_2222.png"}, {"time": 1127.537, "image_uri": "frame_2223.png"}, {"time": 1128.045, "image_uri": "frame_2224.png"}, {"time": 1128.552, "image_uri": "frame_2225.png"}, {"time": 1129.06, "image_uri": "frame_2226.png"}, {"time": 1129.567, "image_uri": "frame_2227.png"}, {"time": 1130.074, "image_uri": "frame_2228.png"}, {"time": 1130.582, "image_uri": "frame_2229.png"}, {"time": 1131.089, "image_uri": "frame_2230.png"}, {"time": 1131.597, "image_uri": "frame_2231.png"}, {"time": 1132.104, "image_uri": "frame_2232.png"}, {"time": 1132.612, "image_uri": "frame_2233.png"}, {"time": 1133.119, "image_uri": "frame_2234.png"}, {"time": 1133.627, "image_uri": "frame_2235.png"}, {"time": 1134.134, "image_uri": "frame_2236.png"}, {"time": 1134.641, "image_uri": "frame_2237.png"}, {"time": 1135.149, "image_uri": "frame_2238.png"}, {"time": 1135.656, "image_uri": "frame_2239.png"}, {"time": 1136.164, "image_uri": "frame_2240.png"}, {"time": 1136.671, "image_uri": "frame_2241.png"}, {"time": 1137.179, "image_uri": "frame_2242.png"}, {"time": 1137.686, "image_uri": "frame_2243.png"}, {"time": 1138.194, "image_uri": "frame_2244.png"}, {"time": 1138.701, "image_uri": "frame_2245.png"}, {"time": 1139.208, "image_uri": "frame_2246.png"}, {"time": 1139.716, "image_uri": "frame_2247.png"}, {"time": 1140.223, "image_uri": "frame_2248.png"}, {"time": 1140.731, "image_uri": "frame_2249.png"}, {"time": 1141.238, "image_uri": "frame_2250.png"}, {"time": 1141.746, "image_uri": "frame_2251.png"}, {"time": 1142.253, "image_uri": "frame_2252.png"}, {"time": 1142.76, "image_uri": "frame_2253.png"}, {"time": 1143.268, "image_uri": "frame_2254.png"}, {"time": 1143.775, "image_uri": "frame_2255.png"}, {"time": 1144.283, "image_uri": "frame_2256.png"}, {"time": 1144.79, "image_uri": "frame_2257.png"}, {"time": 1145.298, "image_uri": "frame_2258.png"}, {"time": 1145.805, "image_uri": "frame_2259.png"}, {"time": 1146.313, "image_uri": "frame_2260.png"}, {"time": 1146.82, "image_uri": "frame_2261.png"}, {"time": 1147.327, "image_uri": "frame_2262.png"}, {"time": 1147.835, "image_uri": "frame_2263.png"}, {"time": 1148.342, "image_uri": "frame_2264.png"}, {"time": 1148.85, "image_uri": "frame_2265.png"}, {"time": 1149.357, "image_uri": "frame_2266.png"}, {"time": 1149.865, "image_uri": "frame_2267.png"}, {"time": 1150.372, "image_uri": "frame_2268.png"}, {"time": 1150.88, "image_uri": "frame_2269.png"}, {"time": 1151.387, "image_uri": "frame_2270.png"}, {"time": 1151.894, "image_uri": "frame_2271.png"}, {"time": 1152.402, "image_uri": "frame_2272.png"}, {"time": 1152.909, "image_uri": "frame_2273.png"}, {"time": 1153.417, "image_uri": "frame_2274.png"}, {"time": 1153.924, "image_uri": "frame_2275.png"}, {"time": 1154.432, "image_uri": "frame_2276.png"}, {"time": 1154.939, "image_uri": "frame_2277.png"}, {"time": 1155.447, "image_uri": "frame_2278.png"}, {"time": 1155.954, "image_uri": "frame_2279.png"}, {"time": 1156.461, "image_uri": "frame_2280.png"}, {"time": 1156.969, "image_uri": "frame_2281.png"}, {"time": 1157.476, "image_uri": "frame_2282.png"}, {"time": 1157.984, "image_uri": "frame_2283.png"}, {"time": 1158.491, "image_uri": "frame_2284.png"}, {"time": 1158.999, "image_uri": "frame_2285.png"}, {"time": 1159.506, "image_uri": "frame_2286.png"}, {"time": 1160.014, "image_uri": "frame_2287.png"}, {"time": 1160.521, "image_uri": "frame_2288.png"}, {"time": 1161.028, "image_uri": "frame_2289.png"}, {"time": 1161.536, "image_uri": "frame_2290.png"}, {"time": 1162.043, "image_uri": "frame_2291.png"}, {"time": 1162.551, "image_uri": "frame_2292.png"}, {"time": 1163.058, "image_uri": "frame_2293.png"}, {"time": 1163.566, "image_uri": "frame_2294.png"}, {"time": 1164.073, "image_uri": "frame_2295.png"}, {"time": 1164.581, "image_uri": "frame_2296.png"}, {"time": 1165.088, "image_uri": "frame_2297.png"}, {"time": 1165.595, "image_uri": "frame_2298.png"}, {"time": 1166.103, "image_uri": "frame_2299.png"}, {"time": 1166.61, "image_uri": "frame_2300.png"}, {"time": 1167.118, "image_uri": "frame_2301.png"}, {"time": 1167.625, "image_uri": "frame_2302.png"}, {"time": 1168.133, "image_uri": "frame_2303.png"}, {"time": 1168.64, "image_uri": "frame_2304.png"}, {"time": 1169.147, "image_uri": "frame_2305.png"}, {"time": 1169.655, "image_uri": "frame_2306.png"}, {"time": 1170.162, "image_uri": "frame_2307.png"}, {"time": 1170.67, "image_uri": "frame_2308.png"}, {"time": 1171.177, "image_uri": "frame_2309.png"}, {"time": 1171.685, "image_uri": "frame_2310.png"}, {"time": 1172.192, "image_uri": "frame_2311.png"}, {"time": 1172.7, "image_uri": "frame_2312.png"}, {"time": 1173.207, "image_uri": "frame_2313.png"}, {"time": 1173.714, "image_uri": "frame_2314.png"}, {"time": 1174.222, "image_uri": "frame_2315.png"}, {"time": 1174.729, "image_uri": "frame_2316.png"}, {"time": 1175.237, "image_uri": "frame_2317.png"}, {"time": 1175.744, "image_uri": "frame_2318.png"}, {"time": 1176.252, "image_uri": "frame_2319.png"}, {"time": 1176.759, "image_uri": "frame_2320.png"}, {"time": 1177.267, "image_uri": "frame_2321.png"}, {"time": 1177.774, "image_uri": "frame_2322.png"}, {"time": 1178.281, "image_uri": "frame_2323.png"}, {"time": 1178.789, "image_uri": "frame_2324.png"}, {"time": 1179.296, "image_uri": "frame_2325.png"}, {"time": 1179.804, "image_uri": "frame_2326.png"}, {"time": 1180.311, "image_uri": "frame_2327.png"}, {"time": 1180.819, "image_uri": "frame_2328.png"}, {"time": 1181.326, "image_uri": "frame_2329.png"}, {"time": 1181.834, "image_uri": "frame_2330.png"}, {"time": 1182.341, "image_uri": "frame_2331.png"}, {"time": 1182.848, "image_uri": "frame_2332.png"}, {"time": 1183.356, "image_uri": "frame_2333.png"}, {"time": 1183.863, "image_uri": "frame_2334.png"}, {"time": 1184.371, "image_uri": "frame_2335.png"}, {"time": 1184.878, "image_uri": "frame_2336.png"}, {"time": 1185.386, "image_uri": "frame_2337.png"}, {"time": 1185.893, "image_uri": "frame_2338.png"}, {"time": 1186.401, "image_uri": "frame_2339.png"}, {"time": 1186.908, "image_uri": "frame_2340.png"}, {"time": 1187.415, "image_uri": "frame_2341.png"}, {"time": 1187.923, "image_uri": "frame_2342.png"}, {"time": 1188.43, "image_uri": "frame_2343.png"}, {"time": 1188.938, "image_uri": "frame_2344.png"}, {"time": 1189.445, "image_uri": "frame_2345.png"}, {"time": 1189.953, "image_uri": "frame_2346.png"}, {"time": 1190.46, "image_uri": "frame_2347.png"}, {"time": 1190.968, "image_uri": "frame_2348.png"}, {"time": 1191.475, "image_uri": "frame_2349.png"}, {"time": 1191.982, "image_uri": "frame_2350.png"}, {"time": 1192.49, "image_uri": "frame_2351.png"}, {"time": 1192.997, "image_uri": "frame_2352.png"}, {"time": 1193.505, "image_uri": "frame_2353.png"}, {"time": 1194.012, "image_uri": "frame_2354.png"}, {"time": 1194.52, "image_uri": "frame_2355.png"}, {"time": 1195.027, "image_uri": "frame_2356.png"}, {"time": 1195.535, "image_uri": "frame_2357.png"}, {"time": 1196.042, "image_uri": "frame_2358.png"}, {"time": 1196.549, "image_uri": "frame_2359.png"}, {"time": 1197.057, "image_uri": "frame_2360.png"}, {"time": 1197.564, "image_uri": "frame_2361.png"}, {"time": 1198.072, "image_uri": "frame_2362.png"}, {"time": 1198.579, "image_uri": "frame_2363.png"}, {"time": 1199.087, "image_uri": "frame_2364.png"}, {"time": 1199.594, "image_uri": "frame_2365.png"}, {"time": 1200.101, "image_uri": "frame_2366.png"}, {"time": 1200.609, "image_uri": "frame_2367.png"}, {"time": 1201.116, "image_uri": "frame_2368.png"}, {"time": 1201.624, "image_uri": "frame_2369.png"}, {"time": 1202.131, "image_uri": "frame_2370.png"}, {"time": 1202.639, "image_uri": "frame_2371.png"}, {"time": 1203.146, "image_uri": "frame_2372.png"}, {"time": 1203.654, "image_uri": "frame_2373.png"}, {"time": 1204.161, "image_uri": "frame_2374.png"}, {"time": 1204.668, "image_uri": "frame_2375.png"}, {"time": 1205.176, "image_uri": "frame_2376.png"}, {"time": 1205.683, "image_uri": "frame_2377.png"}, {"time": 1206.191, "image_uri": "frame_2378.png"}, {"time": 1206.698, "image_uri": "frame_2379.png"}, {"time": 1207.206, "image_uri": "frame_2380.png"}, {"time": 1207.713, "image_uri": "frame_2381.png"}, {"time": 1208.221, "image_uri": "frame_2382.png"}, {"time": 1208.728, "image_uri": "frame_2383.png"}, {"time": 1209.235, "image_uri": "frame_2384.png"}, {"time": 1209.743, "image_uri": "frame_2385.png"}, {"time": 1210.25, "image_uri": "frame_2386.png"}, {"time": 1210.758, "image_uri": "frame_2387.png"}, {"time": 1211.265, "image_uri": "frame_2388.png"}, {"time": 1211.773, "image_uri": "frame_2389.png"}, {"time": 1212.28, "image_uri": "frame_2390.png"}, {"time": 1212.788, "image_uri": "frame_2391.png"}, {"time": 1213.295, "image_uri": "frame_2392.png"}, {"time": 1213.802, "image_uri": "frame_2393.png"}, {"time": 1214.31, "image_uri": "frame_2394.png"}, {"time": 1214.817, "image_uri": "frame_2395.png"}, {"time": 1215.325, "image_uri": "frame_2396.png"}, {"time": 1215.832, "image_uri": "frame_2397.png"}, {"time": 1216.34, "image_uri": "frame_2398.png"}, {"time": 1216.847, "image_uri": "frame_2399.png"}, {"time": 1217.355, "image_uri": "frame_2400.png"}, {"time": 1217.862, "image_uri": "frame_2401.png"}, {"time": 1218.369, "image_uri": "frame_2402.png"}, {"time": 1218.877, "image_uri": "frame_2403.png"}, {"time": 1219.384, "image_uri": "frame_2404.png"}, {"time": 1219.892, "image_uri": "frame_2405.png"}, {"time": 1220.399, "image_uri": "frame_2406.png"}, {"time": 1220.907, "image_uri": "frame_2407.png"}, {"time": 1221.414, "image_uri": "frame_2408.png"}, {"time": 1221.922, "image_uri": "frame_2409.png"}, {"time": 1222.429, "image_uri": "frame_2410.png"}, {"time": 1222.936, "image_uri": "frame_2411.png"}, {"time": 1223.444, "image_uri": "frame_2412.png"}, {"time": 1223.951, "image_uri": "frame_2413.png"}, {"time": 1224.459, "image_uri": "frame_2414.png"}, {"time": 1224.966, "image_uri": "frame_2415.png"}, {"time": 1225.474, "image_uri": "frame_2416.png"}, {"time": 1225.981, "image_uri": "frame_2417.png"}, {"time": 1226.488, "image_uri": "frame_2418.png"}, {"time": 1226.996, "image_uri": "frame_2419.png"}, {"time": 1227.503, "image_uri": "frame_2420.png"}, {"time": 1228.011, "image_uri": "frame_2421.png"}, {"time": 1228.518, "image_uri": "frame_2422.png"}, {"time": 1229.026, "image_uri": "frame_2423.png"}, {"time": 1229.533, "image_uri": "frame_2424.png"}, {"time": 1230.041, "image_uri": "frame_2425.png"}, {"time": 1230.548, "image_uri": "frame_2426.png"}, {"time": 1231.055, "image_uri": "frame_2427.png"}, {"time": 1231.563, "image_uri": "frame_2428.png"}, {"time": 1232.07, "image_uri": "frame_2429.png"}, {"time": 1232.578, "image_uri": "frame_2430.png"}, {"time": 1233.085, "image_uri": "frame_2431.png"}, {"time": 1233.593, "image_uri": "frame_2432.png"}, {"time": 1234.1, "image_uri": "frame_2433.png"}, {"time": 1234.608, "image_uri": "frame_2434.png"}, {"time": 1235.115, "image_uri": "frame_2435.png"}, {"time": 1235.622, "image_uri": "frame_2436.png"}, {"time": 1236.13, "image_uri": "frame_2437.png"}, {"time": 1236.637, "image_uri": "frame_2438.png"}, {"time": 1237.145, "image_uri": "frame_2439.png"}, {"time": 1237.652, "image_uri": "frame_2440.png"}, {"time": 1238.16, "image_uri": "frame_2441.png"}, {"time": 1238.667, "image_uri": "frame_2442.png"}, {"time": 1239.175, "image_uri": "frame_2443.png"}, {"time": 1239.682, "image_uri": "frame_2444.png"}, {"time": 1240.189, "image_uri": "frame_2445.png"}, {"time": 1240.697, "image_uri": "frame_2446.png"}, {"time": 1241.204, "image_uri": "frame_2447.png"}, {"time": 1241.712, "image_uri": "frame_2448.png"}, {"time": 1242.219, "image_uri": "frame_2449.png"}, {"time": 1242.727, "image_uri": "frame_2450.png"}, {"time": 1243.234, "image_uri": "frame_2451.png"}, {"time": 1243.742, "image_uri": "frame_2452.png"}, {"time": 1244.249, "image_uri": "frame_2453.png"}, {"time": 1244.756, "image_uri": "frame_2454.png"}, {"time": 1245.264, "image_uri": "frame_2455.png"}, {"time": 1245.771, "image_uri": "frame_2456.png"}, {"time": 1246.279, "image_uri": "frame_2457.png"}, {"time": 1246.786, "image_uri": "frame_2458.png"}, {"time": 1247.294, "image_uri": "frame_2459.png"}, {"time": 1247.801, "image_uri": "frame_2460.png"}, {"time": 1248.309, "image_uri": "frame_2461.png"}, {"time": 1248.816, "image_uri": "frame_2462.png"}, {"time": 1249.323, "image_uri": "frame_2463.png"}, {"time": 1249.831, "image_uri": "frame_2464.png"}, {"time": 1250.338, "image_uri": "frame_2465.png"}, {"time": 1250.846, "image_uri": "frame_2466.png"}, {"time": 1251.353, "image_uri": "frame_2467.png"}, {"time": 1251.861, "image_uri": "frame_2468.png"}, {"time": 1252.368, "image_uri": "frame_2469.png"}, {"time": 1252.876, "image_uri": "frame_2470.png"}, {"time": 1253.383, "image_uri": "frame_2471.png"}, {"time": 1253.89, "image_uri": "frame_2472.png"}, {"time": 1254.398, "image_uri": "frame_2473.png"}, {"time": 1254.905, "image_uri": "frame_2474.png"}, {"time": 1255.413, "image_uri": "frame_2475.png"}, {"time": 1255.92, "image_uri": "frame_2476.png"}, {"time": 1256.428, "image_uri": "frame_2477.png"}, {"time": 1256.935, "image_uri": "frame_2478.png"}, {"time": 1257.442, "image_uri": "frame_2479.png"}, {"time": 1257.95, "image_uri": "frame_2480.png"}, {"time": 1258.457, "image_uri": "frame_2481.png"}, {"time": 1258.965, "image_uri": "frame_2482.png"}, {"time": 1259.472, "image_uri": "frame_2483.png"}, {"time": 1259.98, "image_uri": "frame_2484.png"}, {"time": 1260.487, "image_uri": "frame_2485.png"}, {"time": 1260.995, "image_uri": "frame_2486.png"}, {"time": 1261.502, "image_uri": "frame_2487.png"}, {"time": 1262.009, "image_uri": "frame_2488.png"}, {"time": 1262.517, "image_uri": "frame_2489.png"}, {"time": 1263.024, "image_uri": "frame_2490.png"}, {"time": 1263.532, "image_uri": "frame_2491.png"}, {"time": 1264.039, "image_uri": "frame_2492.png"}, {"time": 1264.547, "image_uri": "frame_2493.png"}, {"time": 1265.054, "image_uri": "frame_2494.png"}, {"time": 1265.562, "image_uri": "frame_2495.png"}, {"time": 1266.069, "image_uri": "frame_2496.png"}, {"time": 1266.576, "image_uri": "frame_2497.png"}, {"time": 1267.084, "image_uri": "frame_2498.png"}, {"time": 1267.591, "image_uri": "frame_2499.png"}, {"time": 1268.099, "image_uri": "frame_2500.png"}, {"time": 1268.606, "image_uri": "frame_2501.png"}, {"time": 1269.114, "image_uri": "frame_2502.png"}, {"time": 1269.621, "image_uri": "frame_2503.png"}, {"time": 1270.129, "image_uri": "frame_2504.png"}, {"time": 1270.636, "image_uri": "frame_2505.png"}, {"time": 1271.143, "image_uri": "frame_2506.png"}, {"time": 1271.651, "image_uri": "frame_2507.png"}, {"time": 1272.158, "image_uri": "frame_2508.png"}, {"time": 1272.666, "image_uri": "frame_2509.png"}, {"time": 1273.173, "image_uri": "frame_2510.png"}, {"time": 1273.681, "image_uri": "frame_2511.png"}, {"time": 1274.188, "image_uri": "frame_2512.png"}, {"time": 1274.696, "image_uri": "frame_2513.png"}, {"time": 1275.203, "image_uri": "frame_2514.png"}, {"time": 1275.71, "image_uri": "frame_2515.png"}, {"time": 1276.218, "image_uri": "frame_2516.png"}, {"time": 1276.725, "image_uri": "frame_2517.png"}, {"time": 1277.233, "image_uri": "frame_2518.png"}, {"time": 1277.74, "image_uri": "frame_2519.png"}, {"time": 1278.248, "image_uri": "frame_2520.png"}, {"time": 1278.755, "image_uri": "frame_2521.png"}, {"time": 1279.263, "image_uri": "frame_2522.png"}, {"time": 1279.77, "image_uri": "frame_2523.png"}, {"time": 1280.277, "image_uri": "frame_2524.png"}, {"time": 1280.785, "image_uri": "frame_2525.png"}, {"time": 1281.292, "image_uri": "frame_2526.png"}, {"time": 1281.8, "image_uri": "frame_2527.png"}, {"time": 1282.307, "image_uri": "frame_2528.png"}, {"time": 1282.815, "image_uri": "frame_2529.png"}, {"time": 1283.322, "image_uri": "frame_2530.png"}, {"time": 1283.829, "image_uri": "frame_2531.png"}, {"time": 1284.337, "image_uri": "frame_2532.png"}, {"time": 1284.844, "image_uri": "frame_2533.png"}, {"time": 1285.352, "image_uri": "frame_2534.png"}, {"time": 1285.859, "image_uri": "frame_2535.png"}, {"time": 1286.367, "image_uri": "frame_2536.png"}, {"time": 1286.874, "image_uri": "frame_2537.png"}, {"time": 1287.382, "image_uri": "frame_2538.png"}, {"time": 1287.889, "image_uri": "frame_2539.png"}, {"time": 1288.396, "image_uri": "frame_2540.png"}, {"time": 1288.904, "image_uri": "frame_2541.png"}, {"time": 1289.411, "image_uri": "frame_2542.png"}, {"time": 1289.919, "image_uri": "frame_2543.png"}, {"time": 1290.426, "image_uri": "frame_2544.png"}, {"time": 1290.934, "image_uri": "frame_2545.png"}, {"time": 1291.441, "image_uri": "frame_2546.png"}, {"time": 1291.949, "image_uri": "frame_2547.png"}, {"time": 1292.456, "image_uri": "frame_2548.png"}, {"time": 1292.963, "image_uri": "frame_2549.png"}, {"time": 1293.471, "image_uri": "frame_2550.png"}, {"time": 1293.978, "image_uri": "frame_2551.png"}, {"time": 1294.486, "image_uri": "frame_2552.png"}, {"time": 1294.993, "image_uri": "frame_2553.png"}, {"time": 1295.501, "image_uri": "frame_2554.png"}, {"time": 1296.008, "image_uri": "frame_2555.png"}, {"time": 1296.516, "image_uri": "frame_2556.png"}, {"time": 1297.023, "image_uri": "frame_2557.png"}, {"time": 1297.53, "image_uri": "frame_2558.png"}, {"time": 1298.038, "image_uri": "frame_2559.png"}, {"time": 1298.545, "image_uri": "frame_2560.png"}, {"time": 1299.053, "image_uri": "frame_2561.png"}, {"time": 1299.56, "image_uri": "frame_2562.png"}, {"time": 1300.068, "image_uri": "frame_2563.png"}, {"time": 1300.575, "image_uri": "frame_2564.png"}, {"time": 1301.083, "image_uri": "frame_2565.png"}, {"time": 1301.59, "image_uri": "frame_2566.png"}, {"time": 1302.097, "image_uri": "frame_2567.png"}, {"time": 1302.605, "image_uri": "frame_2568.png"}, {"time": 1303.112, "image_uri": "frame_2569.png"}, {"time": 1303.62, "image_uri": "frame_2570.png"}, {"time": 1304.127, "image_uri": "frame_2571.png"}, {"time": 1304.635, "image_uri": "frame_2572.png"}, {"time": 1305.142, "image_uri": "frame_2573.png"}, {"time": 1305.65, "image_uri": "frame_2574.png"}, {"time": 1306.157, "image_uri": "frame_2575.png"}, {"time": 1306.664, "image_uri": "frame_2576.png"}, {"time": 1307.172, "image_uri": "frame_2577.png"}, {"time": 1307.679, "image_uri": "frame_2578.png"}, {"time": 1308.187, "image_uri": "frame_2579.png"}, {"time": 1308.694, "image_uri": "frame_2580.png"}, {"time": 1309.202, "image_uri": "frame_2581.png"}, {"time": 1309.709, "image_uri": "frame_2582.png"}, {"time": 1310.217, "image_uri": "frame_2583.png"}, {"time": 1310.724, "image_uri": "frame_2584.png"}, {"time": 1311.231, "image_uri": "frame_2585.png"}, {"time": 1311.739, "image_uri": "frame_2586.png"}, {"time": 1312.246, "image_uri": "frame_2587.png"}, {"time": 1312.754, "image_uri": "frame_2588.png"}, {"time": 1313.261, "image_uri": "frame_2589.png"}, {"time": 1313.769, "image_uri": "frame_2590.png"}, {"time": 1314.276, "image_uri": "frame_2591.png"}, {"time": 1314.783, "image_uri": "frame_2592.png"}, {"time": 1315.291, "image_uri": "frame_2593.png"}, {"time": 1315.798, "image_uri": "frame_2594.png"}, {"time": 1316.306, "image_uri": "frame_2595.png"}, {"time": 1316.813, "image_uri": "frame_2596.png"}, {"time": 1317.321, "image_uri": "frame_2597.png"}, {"time": 1317.828, "image_uri": "frame_2598.png"}, {"time": 1318.336, "image_uri": "frame_2599.png"}, {"time": 1318.843, "image_uri": "frame_2600.png"}, {"time": 1319.35, "image_uri": "frame_2601.png"}, {"time": 1319.858, "image_uri": "frame_2602.png"}, {"time": 1320.365, "image_uri": "frame_2603.png"}, {"time": 1320.873, "image_uri": "frame_2604.png"}, {"time": 1321.38, "image_uri": "frame_2605.png"}, {"time": 1321.888, "image_uri": "frame_2606.png"}, {"time": 1322.395, "image_uri": "frame_2607.png"}, {"time": 1322.903, "image_uri": "frame_2608.png"}, {"time": 1323.41, "image_uri": "frame_2609.png"}, {"time": 1323.917, "image_uri": "frame_2610.png"}, {"time": 1324.425, "image_uri": "frame_2611.png"}, {"time": 1324.932, "image_uri": "frame_2612.png"}, {"time": 1325.44, "image_uri": "frame_2613.png"}, {"time": 1325.947, "image_uri": "frame_2614.png"}, {"time": 1326.455, "image_uri": "frame_2615.png"}, {"time": 1326.962, "image_uri": "frame_2616.png"}, {"time": 1327.47, "image_uri": "frame_2617.png"}, {"time": 1327.977, "image_uri": "frame_2618.png"}, {"time": 1328.484, "image_uri": "frame_2619.png"}, {"time": 1328.992, "image_uri": "frame_2620.png"}, {"time": 1329.499, "image_uri": "frame_2621.png"}, {"time": 1330.007, "image_uri": "frame_2622.png"}, {"time": 1330.514, "image_uri": "frame_2623.png"}, {"time": 1331.022, "image_uri": "frame_2624.png"}, {"time": 1331.529, "image_uri": "frame_2625.png"}, {"time": 1332.037, "image_uri": "frame_2626.png"}, {"time": 1332.544, "image_uri": "frame_2627.png"}, {"time": 1333.051, "image_uri": "frame_2628.png"}, {"time": 1333.559, "image_uri": "frame_2629.png"}, {"time": 1334.066, "image_uri": "frame_2630.png"}, {"time": 1334.574, "image_uri": "frame_2631.png"}, {"time": 1335.081, "image_uri": "frame_2632.png"}, {"time": 1335.589, "image_uri": "frame_2633.png"}, {"time": 1336.096, "image_uri": "frame_2634.png"}, {"time": 1336.604, "image_uri": "frame_2635.png"}, {"time": 1337.111, "image_uri": "frame_2636.png"}, {"time": 1337.618, "image_uri": "frame_2637.png"}, {"time": 1338.126, "image_uri": "frame_2638.png"}, {"time": 1338.633, "image_uri": "frame_2639.png"}, {"time": 1339.141, "image_uri": "frame_2640.png"}, {"time": 1339.648, "image_uri": "frame_2641.png"}, {"time": 1340.156, "image_uri": "frame_2642.png"}, {"time": 1340.663, "image_uri": "frame_2643.png"}, {"time": 1341.171, "image_uri": "frame_2644.png"}, {"time": 1341.678, "image_uri": "frame_2645.png"}, {"time": 1342.185, "image_uri": "frame_2646.png"}, {"time": 1342.693, "image_uri": "frame_2647.png"}, {"time": 1343.2, "image_uri": "frame_2648.png"}, {"time": 1343.708, "image_uri": "frame_2649.png"}, {"time": 1344.215, "image_uri": "frame_2650.png"}, {"time": 1344.723, "image_uri": "frame_2651.png"}, {"time": 1345.23, "image_uri": "frame_2652.png"}, {"time": 1345.737, "image_uri": "frame_2653.png"}, {"time": 1346.245, "image_uri": "frame_2654.png"}, {"time": 1346.752, "image_uri": "frame_2655.png"}, {"time": 1347.26, "image_uri": "frame_2656.png"}, {"time": 1347.767, "image_uri": "frame_2657.png"}, {"time": 1348.275, "image_uri": "frame_2658.png"}, {"time": 1348.782, "image_uri": "frame_2659.png"}, {"time": 1349.29, "image_uri": "frame_2660.png"}, {"time": 1349.797, "image_uri": "frame_2661.png"}, {"time": 1350.304, "image_uri": "frame_2662.png"}, {"time": 1350.812, "image_uri": "frame_2663.png"}, {"time": 1351.319, "image_uri": "frame_2664.png"}, {"time": 1351.827, "image_uri": "frame_2665.png"}, {"time": 1352.334, "image_uri": "frame_2666.png"}, {"time": 1352.842, "image_uri": "frame_2667.png"}, {"time": 1353.349, "image_uri": "frame_2668.png"}, {"time": 1353.857, "image_uri": "frame_2669.png"}, {"time": 1354.364, "image_uri": "frame_2670.png"}, {"time": 1354.871, "image_uri": "frame_2671.png"}, {"time": 1355.379, "image_uri": "frame_2672.png"}, {"time": 1355.886, "image_uri": "frame_2673.png"}, {"time": 1356.394, "image_uri": "frame_2674.png"}, {"time": 1356.901, "image_uri": "frame_2675.png"}, {"time": 1357.409, "image_uri": "frame_2676.png"}, {"time": 1357.916, "image_uri": "frame_2677.png"}, {"time": 1358.424, "image_uri": "frame_2678.png"}, {"time": 1358.931, "image_uri": "frame_2679.png"}, {"time": 1359.438, "image_uri": "frame_2680.png"}, {"time": 1359.946, "image_uri": "frame_2681.png"}, {"time": 1360.453, "image_uri": "frame_2682.png"}, {"time": 1360.961, "image_uri": "frame_2683.png"}, {"time": 1361.468, "image_uri": "frame_2684.png"}, {"time": 1361.976, "image_uri": "frame_2685.png"}, {"time": 1362.483, "image_uri": "frame_2686.png"}, {"time": 1362.991, "image_uri": "frame_2687.png"}, {"time": 1363.498, "image_uri": "frame_2688.png"}, {"time": 1364.005, "image_uri": "frame_2689.png"}, {"time": 1364.513, "image_uri": "frame_2690.png"}, {"time": 1365.02, "image_uri": "frame_2691.png"}, {"time": 1365.528, "image_uri": "frame_2692.png"}, {"time": 1366.035, "image_uri": "frame_2693.png"}, {"time": 1366.543, "image_uri": "frame_2694.png"}, {"time": 1367.05, "image_uri": "frame_2695.png"}, {"time": 1367.558, "image_uri": "frame_2696.png"}, {"time": 1368.065, "image_uri": "frame_2697.png"}, {"time": 1368.572, "image_uri": "frame_2698.png"}, {"time": 1369.08, "image_uri": "frame_2699.png"}, {"time": 1369.587, "image_uri": "frame_2700.png"}, {"time": 1370.095, "image_uri": "frame_2701.png"}, {"time": 1370.602, "image_uri": "frame_2702.png"}, {"time": 1371.11, "image_uri": "frame_2703.png"}, {"time": 1371.617, "image_uri": "frame_2704.png"}, {"time": 1372.124, "image_uri": "frame_2705.png"}, {"time": 1372.632, "image_uri": "frame_2706.png"}, {"time": 1373.139, "image_uri": "frame_2707.png"}, {"time": 1373.647, "image_uri": "frame_2708.png"}, {"time": 1374.154, "image_uri": "frame_2709.png"}, {"time": 1374.662, "image_uri": "frame_2710.png"}, {"time": 1375.169, "image_uri": "frame_2711.png"}, {"time": 1375.677, "image_uri": "frame_2712.png"}, {"time": 1376.184, "image_uri": "frame_2713.png"}, {"time": 1376.691, "image_uri": "frame_2714.png"}, {"time": 1377.199, "image_uri": "frame_2715.png"}, {"time": 1377.706, "image_uri": "frame_2716.png"}, {"time": 1378.214, "image_uri": "frame_2717.png"}, {"time": 1378.721, "image_uri": "frame_2718.png"}, {"time": 1379.229, "image_uri": "frame_2719.png"}, {"time": 1379.736, "image_uri": "frame_2720.png"}, {"time": 1380.244, "image_uri": "frame_2721.png"}, {"time": 1380.751, "image_uri": "frame_2722.png"}, {"time": 1381.258, "image_uri": "frame_2723.png"}, {"time": 1381.766, "image_uri": "frame_2724.png"}, {"time": 1382.273, "image_uri": "frame_2725.png"}, {"time": 1382.781, "image_uri": "frame_2726.png"}, {"time": 1383.288, "image_uri": "frame_2727.png"}, {"time": 1383.796, "image_uri": "frame_2728.png"}, {"time": 1384.303, "image_uri": "frame_2729.png"}, {"time": 1384.811, "image_uri": "frame_2730.png"}, {"time": 1385.318, "image_uri": "frame_2731.png"}, {"time": 1385.825, "image_uri": "frame_2732.png"}, {"time": 1386.333, "image_uri": "frame_2733.png"}, {"time": 1386.84, "image_uri": "frame_2734.png"}, {"time": 1387.348, "image_uri": "frame_2735.png"}, {"time": 1387.855, "image_uri": "frame_2736.png"}, {"time": 1388.363, "image_uri": "frame_2737.png"}, {"time": 1388.87, "image_uri": "frame_2738.png"}, {"time": 1389.378, "image_uri": "frame_2739.png"}, {"time": 1389.885, "image_uri": "frame_2740.png"}, {"time": 1390.392, "image_uri": "frame_2741.png"}, {"time": 1390.9, "image_uri": "frame_2742.png"}, {"time": 1391.407, "image_uri": "frame_2743.png"}, {"time": 1391.915, "image_uri": "frame_2744.png"}, {"time": 1392.422, "image_uri": "frame_2745.png"}, {"time": 1392.93, "image_uri": "frame_2746.png"}, {"time": 1393.437, "image_uri": "frame_2747.png"}, {"time": 1393.945, "image_uri": "frame_2748.png"}, {"time": 1394.452, "image_uri": "frame_2749.png"}, {"time": 1394.959, "image_uri": "frame_2750.png"}, {"time": 1395.467, "image_uri": "frame_2751.png"}, {"time": 1395.974, "image_uri": "frame_2752.png"}, {"time": 1396.482, "image_uri": "frame_2753.png"}, {"time": 1396.989, "image_uri": "frame_2754.png"}, {"time": 1397.497, "image_uri": "frame_2755.png"}, {"time": 1398.004, "image_uri": "frame_2756.png"}, {"time": 1398.512, "image_uri": "frame_2757.png"}, {"time": 1399.019, "image_uri": "frame_2758.png"}, {"time": 1399.526, "image_uri": "frame_2759.png"}, {"time": 1400.034, "image_uri": "frame_2760.png"}, {"time": 1400.541, "image_uri": "frame_2761.png"}, {"time": 1401.049, "image_uri": "frame_2762.png"}, {"time": 1401.556, "image_uri": "frame_2763.png"}, {"time": 1402.064, "image_uri": "frame_2764.png"}, {"time": 1402.571, "image_uri": "frame_2765.png"}, {"time": 1403.078, "image_uri": "frame_2766.png"}, {"time": 1403.586, "image_uri": "frame_2767.png"}, {"time": 1404.093, "image_uri": "frame_2768.png"}, {"time": 1404.601, "image_uri": "frame_2769.png"}, {"time": 1405.108, "image_uri": "frame_2770.png"}, {"time": 1405.616, "image_uri": "frame_2771.png"}, {"time": 1406.123, "image_uri": "frame_2772.png"}, {"time": 1406.631, "image_uri": "frame_2773.png"}, {"time": 1407.138, "image_uri": "frame_2774.png"}, {"time": 1407.645, "image_uri": "frame_2775.png"}, {"time": 1408.153, "image_uri": "frame_2776.png"}, {"time": 1408.66, "image_uri": "frame_2777.png"}, {"time": 1409.168, "image_uri": "frame_2778.png"}, {"time": 1409.675, "image_uri": "frame_2779.png"}, {"time": 1410.183, "image_uri": "frame_2780.png"}, {"time": 1410.69, "image_uri": "frame_2781.png"}, {"time": 1411.198, "image_uri": "frame_2782.png"}, {"time": 1411.705, "image_uri": "frame_2783.png"}, {"time": 1412.212, "image_uri": "frame_2784.png"}, {"time": 1412.72, "image_uri": "frame_2785.png"}, {"time": 1413.227, "image_uri": "frame_2786.png"}, {"time": 1413.735, "image_uri": "frame_2787.png"}, {"time": 1414.242, "image_uri": "frame_2788.png"}, {"time": 1414.75, "image_uri": "frame_2789.png"}, {"time": 1415.257, "image_uri": "frame_2790.png"}, {"time": 1415.765, "image_uri": "frame_2791.png"}, {"time": 1416.272, "image_uri": "frame_2792.png"}, {"time": 1416.779, "image_uri": "frame_2793.png"}, {"time": 1417.287, "image_uri": "frame_2794.png"}, {"time": 1417.794, "image_uri": "frame_2795.png"}, {"time": 1418.302, "image_uri": "frame_2796.png"}, {"time": 1418.809, "image_uri": "frame_2797.png"}, {"time": 1419.317, "image_uri": "frame_2798.png"}, {"time": 1419.824, "image_uri": "frame_2799.png"}, {"time": 1420.332, "image_uri": "frame_2800.png"}, {"time": 1420.839, "image_uri": "frame_2801.png"}, {"time": 1421.346, "image_uri": "frame_2802.png"}, {"time": 1421.854, "image_uri": "frame_2803.png"}, {"time": 1422.361, "image_uri": "frame_2804.png"}, {"time": 1422.869, "image_uri": "frame_2805.png"}, {"time": 1423.376, "image_uri": "frame_2806.png"}, {"time": 1423.884, "image_uri": "frame_2807.png"}, {"time": 1424.391, "image_uri": "frame_2808.png"}, {"time": 1424.899, "image_uri": "frame_2809.png"}, {"time": 1425.406, "image_uri": "frame_2810.png"}, {"time": 1425.913, "image_uri": "frame_2811.png"}, {"time": 1426.421, "image_uri": "frame_2812.png"}, {"time": 1426.928, "image_uri": "frame_2813.png"}, {"time": 1427.436, "image_uri": "frame_2814.png"}, {"time": 1427.943, "image_uri": "frame_2815.png"}, {"time": 1428.451, "image_uri": "frame_2816.png"}, {"time": 1428.958, "image_uri": "frame_2817.png"}, {"time": 1429.465, "image_uri": "frame_2818.png"}, {"time": 1429.973, "image_uri": "frame_2819.png"}, {"time": 1430.48, "image_uri": "frame_2820.png"}, {"time": 1430.988, "image_uri": "frame_2821.png"}, {"time": 1431.495, "image_uri": "frame_2822.png"}, {"time": 1432.003, "image_uri": "frame_2823.png"}, {"time": 1432.51, "image_uri": "frame_2824.png"}, {"time": 1433.018, "image_uri": "frame_2825.png"}, {"time": 1433.525, "image_uri": "frame_2826.png"}, {"time": 1434.032, "image_uri": "frame_2827.png"}, {"time": 1434.54, "image_uri": "frame_2828.png"}, {"time": 1435.047, "image_uri": "frame_2829.png"}, {"time": 1435.555, "image_uri": "frame_2830.png"}, {"time": 1436.062, "image_uri": "frame_2831.png"}, {"time": 1436.57, "image_uri": "frame_2832.png"}, {"time": 1437.077, "image_uri": "frame_2833.png"}, {"time": 1437.585, "image_uri": "frame_2834.png"}, {"time": 1438.092, "image_uri": "frame_2835.png"}, {"time": 1438.599, "image_uri": "frame_2836.png"}, {"time": 1439.107, "image_uri": "frame_2837.png"}, {"time": 1439.614, "image_uri": "frame_2838.png"}, {"time": 1440.122, "image_uri": "frame_2839.png"}, {"time": 1440.629, "image_uri": "frame_2840.png"}, {"time": 1441.137, "image_uri": "frame_2841.png"}, {"time": 1441.644, "image_uri": "frame_2842.png"}, {"time": 1442.152, "image_uri": "frame_2843.png"}, {"time": 1442.659, "image_uri": "frame_2844.png"}, {"time": 1443.166, "image_uri": "frame_2845.png"}, {"time": 1443.674, "image_uri": "frame_2846.png"}, {"time": 1444.181, "image_uri": "frame_2847.png"}, {"time": 1444.689, "image_uri": "frame_2848.png"}, {"time": 1445.196, "image_uri": "frame_2849.png"}, {"time": 1445.704, "image_uri": "frame_2850.png"}, {"time": 1446.211, "image_uri": "frame_2851.png"}, {"time": 1446.719, "image_uri": "frame_2852.png"}, {"time": 1447.226, "image_uri": "frame_2853.png"}, {"time": 1447.733, "image_uri": "frame_2854.png"}, {"time": 1448.241, "image_uri": "frame_2855.png"}, {"time": 1448.748, "image_uri": "frame_2856.png"}, {"time": 1449.256, "image_uri": "frame_2857.png"}, {"time": 1449.763, "image_uri": "frame_2858.png"}, {"time": 1450.271, "image_uri": "frame_2859.png"}, {"time": 1450.778, "image_uri": "frame_2860.png"}, {"time": 1451.286, "image_uri": "frame_2861.png"}, {"time": 1451.793, "image_uri": "frame_2862.png"}, {"time": 1452.3, "image_uri": "frame_2863.png"}, {"time": 1452.808, "image_uri": "frame_2864.png"}, {"time": 1453.315, "image_uri": "frame_2865.png"}, {"time": 1453.823, "image_uri": "frame_2866.png"}, {"time": 1454.33, "image_uri": "frame_2867.png"}, {"time": 1454.838, "image_uri": "frame_2868.png"}, {"time": 1455.345, "image_uri": "frame_2869.png"}, {"time": 1455.853, "image_uri": "frame_2870.png"}, {"time": 1456.36, "image_uri": "frame_2871.png"}, {"time": 1456.867, "image_uri": "frame_2872.png"}, {"time": 1457.375, "image_uri": "frame_2873.png"}, {"time": 1457.882, "image_uri": "frame_2874.png"}, {"time": 1458.39, "image_uri": "frame_2875.png"}, {"time": 1458.897, "image_uri": "frame_2876.png"}, {"time": 1459.405, "image_uri": "frame_2877.png"}, {"time": 1459.912, "image_uri": "frame_2878.png"}, {"time": 1460.419, "image_uri": "frame_2879.png"}, {"time": 1460.927, "image_uri": "frame_2880.png"}, {"time": 1461.434, "image_uri": "frame_2881.png"}, {"time": 1461.942, "image_uri": "frame_2882.png"}, {"time": 1462.449, "image_uri": "frame_2883.png"}, {"time": 1462.957, "image_uri": "frame_2884.png"}, {"time": 1463.464, "image_uri": "frame_2885.png"}, {"time": 1463.972, "image_uri": "frame_2886.png"}, {"time": 1464.479, "image_uri": "frame_2887.png"}, {"time": 1464.986, "image_uri": "frame_2888.png"}, {"time": 1465.494, "image_uri": "frame_2889.png"}, {"time": 1466.001, "image_uri": "frame_2890.png"}, {"time": 1466.509, "image_uri": "frame_2891.png"}, {"time": 1467.016, "image_uri": "frame_2892.png"}, {"time": 1467.524, "image_uri": "frame_2893.png"}, {"time": 1468.031, "image_uri": "frame_2894.png"}, {"time": 1468.539, "image_uri": "frame_2895.png"}, {"time": 1469.046, "image_uri": "frame_2896.png"}, {"time": 1469.553, "image_uri": "frame_2897.png"}, {"time": 1470.061, "image_uri": "frame_2898.png"}, {"time": 1470.568, "image_uri": "frame_2899.png"}, {"time": 1471.076, "image_uri": "frame_2900.png"}, {"time": 1471.583, "image_uri": "frame_2901.png"}, {"time": 1472.091, "image_uri": "frame_2902.png"}, {"time": 1472.598, "image_uri": "frame_2903.png"}, {"time": 1473.106, "image_uri": "frame_2904.png"}, {"time": 1473.613, "image_uri": "frame_2905.png"}, {"time": 1474.12, "image_uri": "frame_2906.png"}, {"time": 1474.628, "image_uri": "frame_2907.png"}, {"time": 1475.135, "image_uri": "frame_2908.png"}, {"time": 1475.643, "image_uri": "frame_2909.png"}, {"time": 1476.15, "image_uri": "frame_2910.png"}, {"time": 1476.658, "image_uri": "frame_2911.png"}, {"time": 1477.165, "image_uri": "frame_2912.png"}, {"time": 1477.673, "image_uri": "frame_2913.png"}, {"time": 1478.18, "image_uri": "frame_2914.png"}, {"time": 1478.687, "image_uri": "frame_2915.png"}, {"time": 1479.195, "image_uri": "frame_2916.png"}, {"time": 1479.702, "image_uri": "frame_2917.png"}, {"time": 1480.21, "image_uri": "frame_2918.png"}, {"time": 1480.717, "image_uri": "frame_2919.png"}, {"time": 1481.225, "image_uri": "frame_2920.png"}, {"time": 1481.732, "image_uri": "frame_2921.png"}, {"time": 1482.24, "image_uri": "frame_2922.png"}, {"time": 1482.747, "image_uri": "frame_2923.png"}, {"time": 1483.254, "image_uri": "frame_2924.png"}, {"time": 1483.762, "image_uri": "frame_2925.png"}, {"time": 1484.269, "image_uri": "frame_2926.png"}, {"time": 1484.777, "image_uri": "frame_2927.png"}, {"time": 1485.284, "image_uri": "frame_2928.png"}, {"time": 1485.792, "image_uri": "frame_2929.png"}, {"time": 1486.299, "image_uri": "frame_2930.png"}, {"time": 1486.806, "image_uri": "frame_2931.png"}, {"time": 1487.314, "image_uri": "frame_2932.png"}, {"time": 1487.821, "image_uri": "frame_2933.png"}, {"time": 1488.329, "image_uri": "frame_2934.png"}, {"time": 1488.836, "image_uri": "frame_2935.png"}, {"time": 1489.344, "image_uri": "frame_2936.png"}, {"time": 1489.851, "image_uri": "frame_2937.png"}, {"time": 1490.359, "image_uri": "frame_2938.png"}, {"time": 1490.866, "image_uri": "frame_2939.png"}, {"time": 1491.373, "image_uri": "frame_2940.png"}, {"time": 1491.881, "image_uri": "frame_2941.png"}, {"time": 1492.388, "image_uri": "frame_2942.png"}, {"time": 1492.896, "image_uri": "frame_2943.png"}, {"time": 1493.403, "image_uri": "frame_2944.png"}, {"time": 1493.911, "image_uri": "frame_2945.png"}, {"time": 1494.418, "image_uri": "frame_2946.png"}, {"time": 1494.926, "image_uri": "frame_2947.png"}, {"time": 1495.433, "image_uri": "frame_2948.png"}, {"time": 1495.94, "image_uri": "frame_2949.png"}, {"time": 1496.448, "image_uri": "frame_2950.png"}, {"time": 1496.955, "image_uri": "frame_2951.png"}, {"time": 1497.463, "image_uri": "frame_2952.png"}, {"time": 1497.97, "image_uri": "frame_2953.png"}, {"time": 1498.478, "image_uri": "frame_2954.png"}, {"time": 1498.985, "image_uri": "frame_2955.png"}, {"time": 1499.493, "image_uri": "frame_2956.png"}, {"time": 1500.0, "image_uri": "frame_2957.png"}, {"time": 1500.507, "image_uri": "frame_2958.png"}, {"time": 1501.015, "image_uri": "frame_2959.png"}, {"time": 1501.522, "image_uri": "frame_2960.png"}, {"time": 1502.03, "image_uri": "frame_2961.png"}, {"time": 1502.537, "image_uri": "frame_2962.png"}, {"time": 1503.045, "image_uri": "frame_2963.png"}, {"time": 1503.552, "image_uri": "frame_2964.png"}, {"time": 1504.06, "image_uri": "frame_2965.png"}, {"time": 1504.567, "image_uri": "frame_2966.png"}, {"time": 1505.074, "image_uri": "frame_2967.png"}, {"time": 1505.582, "image_uri": "frame_2968.png"}, {"time": 1506.089, "image_uri": "frame_2969.png"}, {"time": 1506.597, "image_uri": "frame_2970.png"}, {"time": 1507.104, "image_uri": "frame_2971.png"}, {"time": 1507.612, "image_uri": "frame_2972.png"}, {"time": 1508.119, "image_uri": "frame_2973.png"}, {"time": 1508.627, "image_uri": "frame_2974.png"}, {"time": 1509.134, "image_uri": "frame_2975.png"}, {"time": 1509.641, "image_uri": "frame_2976.png"}, {"time": 1510.149, "image_uri": "frame_2977.png"}, {"time": 1510.656, "image_uri": "frame_2978.png"}, {"time": 1511.164, "image_uri": "frame_2979.png"}, {"time": 1511.671, "image_uri": "frame_2980.png"}, {"time": 1512.179, "image_uri": "frame_2981.png"}, {"time": 1512.686, "image_uri": "frame_2982.png"}, {"time": 1513.194, "image_uri": "frame_2983.png"}, {"time": 1513.701, "image_uri": "frame_2984.png"}, {"time": 1514.208, "image_uri": "frame_2985.png"}, {"time": 1514.716, "image_uri": "frame_2986.png"}, {"time": 1515.223, "image_uri": "frame_2987.png"}, {"time": 1515.731, "image_uri": "frame_2988.png"}, {"time": 1516.238, "image_uri": "frame_2989.png"}, {"time": 1516.746, "image_uri": "frame_2990.png"}, {"time": 1517.253, "image_uri": "frame_2991.png"}, {"time": 1517.76, "image_uri": "frame_2992.png"}, {"time": 1518.268, "image_uri": "frame_2993.png"}, {"time": 1518.775, "image_uri": "frame_2994.png"}, {"time": 1519.283, "image_uri": "frame_2995.png"}, {"time": 1519.79, "image_uri": "frame_2996.png"}, {"time": 1520.298, "image_uri": "frame_2997.png"}, {"time": 1520.805, "image_uri": "frame_2998.png"}, {"time": 1521.313, "image_uri": "frame_2999.png"}, {"time": 1521.82, "image_uri": "frame_3000.png"}, {"time": 1522.327, "image_uri": "frame_3001.png"}, {"time": 1522.835, "image_uri": "frame_3002.png"}, {"time": 1523.342, "image_uri": "frame_3003.png"}, {"time": 1523.85, "image_uri": "frame_3004.png"}, {"time": 1524.357, "image_uri": "frame_3005.png"}, {"time": 1524.865, "image_uri": "frame_3006.png"}, {"time": 1525.372, "image_uri": "frame_3007.png"}, {"time": 1525.88, "image_uri": "frame_3008.png"}, {"time": 1526.387, "image_uri": "frame_3009.png"}, {"time": 1526.894, "image_uri": "frame_3010.png"}, {"time": 1527.402, "image_uri": "frame_3011.png"}, {"time": 1527.909, "image_uri": "frame_3012.png"}, {"time": 1528.417, "image_uri": "frame_3013.png"}, {"time": 1528.924, "image_uri": "frame_3014.png"}, {"time": 1529.432, "image_uri": "frame_3015.png"}, {"time": 1529.939, "image_uri": "frame_3016.png"}, {"time": 1530.447, "image_uri": "frame_3017.png"}, {"time": 1530.954, "image_uri": "frame_3018.png"}, {"time": 1531.461, "image_uri": "frame_3019.png"}, {"time": 1531.969, "image_uri": "frame_3020.png"}, {"time": 1532.476, "image_uri": "frame_3021.png"}, {"time": 1532.984, "image_uri": "frame_3022.png"}, {"time": 1533.491, "image_uri": "frame_3023.png"}, {"time": 1533.999, "image_uri": "frame_3024.png"}, {"time": 1534.506, "image_uri": "frame_3025.png"}, {"time": 1535.014, "image_uri": "frame_3026.png"}, {"time": 1535.521, "image_uri": "frame_3027.png"}, {"time": 1536.028, "image_uri": "frame_3028.png"}, {"time": 1536.536, "image_uri": "frame_3029.png"}, {"time": 1537.043, "image_uri": "frame_3030.png"}, {"time": 1537.551, "image_uri": "frame_3031.png"}, {"time": 1538.058, "image_uri": "frame_3032.png"}, {"time": 1538.566, "image_uri": "frame_3033.png"}, {"time": 1539.073, "image_uri": "frame_3034.png"}, {"time": 1539.581, "image_uri": "frame_3035.png"}, {"time": 1540.088, "image_uri": "frame_3036.png"}, {"time": 1540.595, "image_uri": "frame_3037.png"}, {"time": 1541.103, "image_uri": "frame_3038.png"}, {"time": 1541.61, "image_uri": "frame_3039.png"}, {"time": 1542.118, "image_uri": "frame_3040.png"}, {"time": 1542.625, "image_uri": "frame_3041.png"}, {"time": 1543.133, "image_uri": "frame_3042.png"}, {"time": 1543.64, "image_uri": "frame_3043.png"}, {"time": 1544.147, "image_uri": "frame_3044.png"}, {"time": 1544.655, "image_uri": "frame_3045.png"}, {"time": 1545.162, "image_uri": "frame_3046.png"}, {"time": 1545.67, "image_uri": "frame_3047.png"}, {"time": 1546.177, "image_uri": "frame_3048.png"}, {"time": 1546.685, "image_uri": "frame_3049.png"}, {"time": 1547.192, "image_uri": "frame_3050.png"}, {"time": 1547.7, "image_uri": "frame_3051.png"}, {"time": 1548.207, "image_uri": "frame_3052.png"}, {"time": 1548.714, "image_uri": "frame_3053.png"}, {"time": 1549.222, "image_uri": "frame_3054.png"}, {"time": 1549.729, "image_uri": "frame_3055.png"}, {"time": 1550.237, "image_uri": "frame_3056.png"}, {"time": 1550.744, "image_uri": "frame_3057.png"}, {"time": 1551.252, "image_uri": "frame_3058.png"}, {"time": 1551.759, "image_uri": "frame_3059.png"}, {"time": 1552.267, "image_uri": "frame_3060.png"}, {"time": 1552.774, "image_uri": "frame_3061.png"}, {"time": 1553.281, "image_uri": "frame_3062.png"}, {"time": 1553.789, "image_uri": "frame_3063.png"}, {"time": 1554.296, "image_uri": "frame_3064.png"}, {"time": 1554.804, "image_uri": "frame_3065.png"}, {"time": 1555.311, "image_uri": "frame_3066.png"}, {"time": 1555.819, "image_uri": "frame_3067.png"}, {"time": 1556.326, "image_uri": "frame_3068.png"}, {"time": 1556.834, "image_uri": "frame_3069.png"}, {"time": 1557.341, "image_uri": "frame_3070.png"}, {"time": 1557.848, "image_uri": "frame_3071.png"}, {"time": 1558.356, "image_uri": "frame_3072.png"}, {"time": 1558.863, "image_uri": "frame_3073.png"}, {"time": 1559.371, "image_uri": "frame_3074.png"}, {"time": 1559.878, "image_uri": "frame_3075.png"}, {"time": 1560.386, "image_uri": "frame_3076.png"}, {"time": 1560.893, "image_uri": "frame_3077.png"}, {"time": 1561.401, "image_uri": "frame_3078.png"}, {"time": 1561.908, "image_uri": "frame_3079.png"}, {"time": 1562.415, "image_uri": "frame_3080.png"}, {"time": 1562.923, "image_uri": "frame_3081.png"}, {"time": 1563.43, "image_uri": "frame_3082.png"}, {"time": 1563.938, "image_uri": "frame_3083.png"}, {"time": 1564.445, "image_uri": "frame_3084.png"}, {"time": 1564.953, "image_uri": "frame_3085.png"}, {"time": 1565.46, "image_uri": "frame_3086.png"}, {"time": 1565.968, "image_uri": "frame_3087.png"}, {"time": 1566.475, "image_uri": "frame_3088.png"}, {"time": 1566.982, "image_uri": "frame_3089.png"}, {"time": 1567.49, "image_uri": "frame_3090.png"}, {"time": 1567.997, "image_uri": "frame_3091.png"}, {"time": 1568.505, "image_uri": "frame_3092.png"}, {"time": 1569.012, "image_uri": "frame_3093.png"}, {"time": 1569.52, "image_uri": "frame_3094.png"}, {"time": 1570.027, "image_uri": "frame_3095.png"}, {"time": 1570.535, "image_uri": "frame_3096.png"}, {"time": 1571.042, "image_uri": "frame_3097.png"}, {"time": 1571.549, "image_uri": "frame_3098.png"}, {"time": 1572.057, "image_uri": "frame_3099.png"}, {"time": 1572.564, "image_uri": "frame_3100.png"}, {"time": 1573.072, "image_uri": "frame_3101.png"}, {"time": 1573.579, "image_uri": "frame_3102.png"}, {"time": 1574.087, "image_uri": "frame_3103.png"}, {"time": 1574.594, "image_uri": "frame_3104.png"}, {"time": 1575.101, "image_uri": "frame_3105.png"}, {"time": 1575.609, "image_uri": "frame_3106.png"}, {"time": 1576.116, "image_uri": "frame_3107.png"}, {"time": 1576.624, "image_uri": "frame_3108.png"}, {"time": 1577.131, "image_uri": "frame_3109.png"}, {"time": 1577.639, "image_uri": "frame_3110.png"}, {"time": 1578.146, "image_uri": "frame_3111.png"}, {"time": 1578.654, "image_uri": "frame_3112.png"}, {"time": 1579.161, "image_uri": "frame_3113.png"}, {"time": 1579.668, "image_uri": "frame_3114.png"}, {"time": 1580.176, "image_uri": "frame_3115.png"}, {"time": 1580.683, "image_uri": "frame_3116.png"}, {"time": 1581.191, "image_uri": "frame_3117.png"}, {"time": 1581.698, "image_uri": "frame_3118.png"}, {"time": 1582.206, "image_uri": "frame_3119.png"}, {"time": 1582.713, "image_uri": "frame_3120.png"}, {"time": 1583.221, "image_uri": "frame_3121.png"}, {"time": 1583.728, "image_uri": "frame_3122.png"}, {"time": 1584.235, "image_uri": "frame_3123.png"}, {"time": 1584.743, "image_uri": "frame_3124.png"}, {"time": 1585.25, "image_uri": "frame_3125.png"}, {"time": 1585.758, "image_uri": "frame_3126.png"}, {"time": 1586.265, "image_uri": "frame_3127.png"}, {"time": 1586.773, "image_uri": "frame_3128.png"}, {"time": 1587.28, "image_uri": "frame_3129.png"}, {"time": 1587.788, "image_uri": "frame_3130.png"}, {"time": 1588.295, "image_uri": "frame_3131.png"}, {"time": 1588.802, "image_uri": "frame_3132.png"}, {"time": 1589.31, "image_uri": "frame_3133.png"}, {"time": 1589.817, "image_uri": "frame_3134.png"}, {"time": 1590.325, "image_uri": "frame_3135.png"}, {"time": 1590.832, "image_uri": "frame_3136.png"}, {"time": 1591.34, "image_uri": "frame_3137.png"}, {"time": 1591.847, "image_uri": "frame_3138.png"}, {"time": 1592.355, "image_uri": "frame_3139.png"}, {"time": 1592.862, "image_uri": "frame_3140.png"}, {"time": 1593.369, "image_uri": "frame_3141.png"}, {"time": 1593.877, "image_uri": "frame_3142.png"}, {"time": 1594.384, "image_uri": "frame_3143.png"}, {"time": 1594.892, "image_uri": "frame_3144.png"}, {"time": 1595.399, "image_uri": "frame_3145.png"}, {"time": 1595.907, "image_uri": "frame_3146.png"}, {"time": 1596.414, "image_uri": "frame_3147.png"}, {"time": 1596.922, "image_uri": "frame_3148.png"}, {"time": 1597.429, "image_uri": "frame_3149.png"}, {"time": 1597.936, "image_uri": "frame_3150.png"}, {"time": 1598.444, "image_uri": "frame_3151.png"}, {"time": 1598.951, "image_uri": "frame_3152.png"}, {"time": 1599.459, "image_uri": "frame_3153.png"}, {"time": 1599.966, "image_uri": "frame_3154.png"}, {"time": 1600.474, "image_uri": "frame_3155.png"}, {"time": 1600.981, "image_uri": "frame_3156.png"}, {"time": 1601.488, "image_uri": "frame_3157.png"}, {"time": 1601.996, "image_uri": "frame_3158.png"}, {"time": 1602.503, "image_uri": "frame_3159.png"}, {"time": 1603.011, "image_uri": "frame_3160.png"}, {"time": 1603.518, "image_uri": "frame_3161.png"}, {"time": 1604.026, "image_uri": "frame_3162.png"}, {"time": 1604.533, "image_uri": "frame_3163.png"}, {"time": 1605.041, "image_uri": "frame_3164.png"}, {"time": 1605.548, "image_uri": "frame_3165.png"}, {"time": 1606.055, "image_uri": "frame_3166.png"}, {"time": 1606.563, "image_uri": "frame_3167.png"}, {"time": 1607.07, "image_uri": "frame_3168.png"}, {"time": 1607.578, "image_uri": "frame_3169.png"}, {"time": 1608.085, "image_uri": "frame_3170.png"}, {"time": 1608.593, "image_uri": "frame_3171.png"}, {"time": 1609.1, "image_uri": "frame_3172.png"}, {"time": 1609.608, "image_uri": "frame_3173.png"}, {"time": 1610.115, "image_uri": "frame_3174.png"}, {"time": 1610.622, "image_uri": "frame_3175.png"}, {"time": 1611.13, "image_uri": "frame_3176.png"}, {"time": 1611.637, "image_uri": "frame_3177.png"}, {"time": 1612.145, "image_uri": "frame_3178.png"}, {"time": 1612.652, "image_uri": "frame_3179.png"}, {"time": 1613.16, "image_uri": "frame_3180.png"}, {"time": 1613.667, "image_uri": "frame_3181.png"}, {"time": 1614.175, "image_uri": "frame_3182.png"}, {"time": 1614.682, "image_uri": "frame_3183.png"}, {"time": 1615.189, "image_uri": "frame_3184.png"}, {"time": 1615.697, "image_uri": "frame_3185.png"}, {"time": 1616.204, "image_uri": "frame_3186.png"}, {"time": 1616.712, "image_uri": "frame_3187.png"}, {"time": 1617.219, "image_uri": "frame_3188.png"}, {"time": 1617.727, "image_uri": "frame_3189.png"}, {"time": 1618.234, "image_uri": "frame_3190.png"}, {"time": 1618.742, "image_uri": "frame_3191.png"}, {"time": 1619.249, "image_uri": "frame_3192.png"}, {"time": 1619.756, "image_uri": "frame_3193.png"}, {"time": 1620.264, "image_uri": "frame_3194.png"}, {"time": 1620.771, "image_uri": "frame_3195.png"}, {"time": 1621.279, "image_uri": "frame_3196.png"}, {"time": 1621.786, "image_uri": "frame_3197.png"}, {"time": 1622.294, "image_uri": "frame_3198.png"}, {"time": 1622.801, "image_uri": "frame_3199.png"}, {"time": 1623.309, "image_uri": "frame_3200.png"}, {"time": 1623.816, "image_uri": "frame_3201.png"}, {"time": 1624.323, "image_uri": "frame_3202.png"}, {"time": 1624.831, "image_uri": "frame_3203.png"}, {"time": 1625.338, "image_uri": "frame_3204.png"}, {"time": 1625.846, "image_uri": "frame_3205.png"}, {"time": 1626.353, "image_uri": "frame_3206.png"}, {"time": 1626.861, "image_uri": "frame_3207.png"}, {"time": 1627.368, "image_uri": "frame_3208.png"}, {"time": 1627.876, "image_uri": "frame_3209.png"}, {"time": 1628.383, "image_uri": "frame_3210.png"}, {"time": 1628.89, "image_uri": "frame_3211.png"}, {"time": 1629.398, "image_uri": "frame_3212.png"}, {"time": 1629.905, "image_uri": "frame_3213.png"}, {"time": 1630.413, "image_uri": "frame_3214.png"}, {"time": 1630.92, "image_uri": "frame_3215.png"}, {"time": 1631.428, "image_uri": "frame_3216.png"}, {"time": 1631.935, "image_uri": "frame_3217.png"}, {"time": 1632.442, "image_uri": "frame_3218.png"}, {"time": 1632.95, "image_uri": "frame_3219.png"}, {"time": 1633.457, "image_uri": "frame_3220.png"}, {"time": 1633.965, "image_uri": "frame_3221.png"}, {"time": 1634.472, "image_uri": "frame_3222.png"}, {"time": 1634.98, "image_uri": "frame_3223.png"}, {"time": 1635.487, "image_uri": "frame_3224.png"}, {"time": 1635.995, "image_uri": "frame_3225.png"}, {"time": 1636.502, "image_uri": "frame_3226.png"}, {"time": 1637.009, "image_uri": "frame_3227.png"}, {"time": 1637.517, "image_uri": "frame_3228.png"}, {"time": 1638.024, "image_uri": "frame_3229.png"}, {"time": 1638.532, "image_uri": "frame_3230.png"}, {"time": 1639.039, "image_uri": "frame_3231.png"}, {"time": 1639.547, "image_uri": "frame_3232.png"}, {"time": 1640.054, "image_uri": "frame_3233.png"}, {"time": 1640.562, "image_uri": "frame_3234.png"}, {"time": 1641.069, "image_uri": "frame_3235.png"}, {"time": 1641.576, "image_uri": "frame_3236.png"}, {"time": 1642.084, "image_uri": "frame_3237.png"}, {"time": 1642.591, "image_uri": "frame_3238.png"}, {"time": 1643.099, "image_uri": "frame_3239.png"}, {"time": 1643.606, "image_uri": "frame_3240.png"}, {"time": 1644.114, "image_uri": "frame_3241.png"}, {"time": 1644.621, "image_uri": "frame_3242.png"}, {"time": 1645.129, "image_uri": "frame_3243.png"}, {"time": 1645.636, "image_uri": "frame_3244.png"}, {"time": 1646.143, "image_uri": "frame_3245.png"}, {"time": 1646.651, "image_uri": "frame_3246.png"}, {"time": 1647.158, "image_uri": "frame_3247.png"}, {"time": 1647.666, "image_uri": "frame_3248.png"}, {"time": 1648.173, "image_uri": "frame_3249.png"}, {"time": 1648.681, "image_uri": "frame_3250.png"}, {"time": 1649.188, "image_uri": "frame_3251.png"}, {"time": 1649.696, "image_uri": "frame_3252.png"}, {"time": 1650.203, "image_uri": "frame_3253.png"}, {"time": 1650.71, "image_uri": "frame_3254.png"}, {"time": 1651.218, "image_uri": "frame_3255.png"}, {"time": 1651.725, "image_uri": "frame_3256.png"}, {"time": 1652.233, "image_uri": "frame_3257.png"}, {"time": 1652.74, "image_uri": "frame_3258.png"}, {"time": 1653.248, "image_uri": "frame_3259.png"}, {"time": 1653.755, "image_uri": "frame_3260.png"}, {"time": 1654.263, "image_uri": "frame_3261.png"}, {"time": 1654.77, "image_uri": "frame_3262.png"}, {"time": 1655.277, "image_uri": "frame_3263.png"}, {"time": 1655.785, "image_uri": "frame_3264.png"}, {"time": 1656.292, "image_uri": "frame_3265.png"}, {"time": 1656.8, "image_uri": "frame_3266.png"}, {"time": 1657.307, "image_uri": "frame_3267.png"}, {"time": 1657.815, "image_uri": "frame_3268.png"}, {"time": 1658.322, "image_uri": "frame_3269.png"}, {"time": 1658.829, "image_uri": "frame_3270.png"}, {"time": 1659.337, "image_uri": "frame_3271.png"}, {"time": 1659.844, "image_uri": "frame_3272.png"}, {"time": 1660.352, "image_uri": "frame_3273.png"}, {"time": 1660.859, "image_uri": "frame_3274.png"}, {"time": 1661.367, "image_uri": "frame_3275.png"}, {"time": 1661.874, "image_uri": "frame_3276.png"}, {"time": 1662.382, "image_uri": "frame_3277.png"}, {"time": 1662.889, "image_uri": "frame_3278.png"}, {"time": 1663.396, "image_uri": "frame_3279.png"}, {"time": 1663.904, "image_uri": "frame_3280.png"}, {"time": 1664.411, "image_uri": "frame_3281.png"}, {"time": 1664.919, "image_uri": "frame_3282.png"}, {"time": 1665.426, "image_uri": "frame_3283.png"}, {"time": 1665.934, "image_uri": "frame_3284.png"}, {"time": 1666.441, "image_uri": "frame_3285.png"}, {"time": 1666.949, "image_uri": "frame_3286.png"}, {"time": 1667.456, "image_uri": "frame_3287.png"}, {"time": 1667.963, "image_uri": "frame_3288.png"}, {"time": 1668.471, "image_uri": "frame_3289.png"}, {"time": 1668.978, "image_uri": "frame_3290.png"}, {"time": 1669.486, "image_uri": "frame_3291.png"}, {"time": 1669.993, "image_uri": "frame_3292.png"}, {"time": 1670.501, "image_uri": "frame_3293.png"}, {"time": 1671.008, "image_uri": "frame_3294.png"}, {"time": 1671.516, "image_uri": "frame_3295.png"}, {"time": 1672.023, "image_uri": "frame_3296.png"}, {"time": 1672.53, "image_uri": "frame_3297.png"}, {"time": 1673.038, "image_uri": "frame_3298.png"}, {"time": 1673.545, "image_uri": "frame_3299.png"}, {"time": 1674.053, "image_uri": "frame_3300.png"}, {"time": 1674.56, "image_uri": "frame_3301.png"}, {"time": 1675.068, "image_uri": "frame_3302.png"}, {"time": 1675.575, "image_uri": "frame_3303.png"}, {"time": 1676.083, "image_uri": "frame_3304.png"}, {"time": 1676.59, "image_uri": "frame_3305.png"}, {"time": 1677.097, "image_uri": "frame_3306.png"}, {"time": 1677.605, "image_uri": "frame_3307.png"}, {"time": 1678.112, "image_uri": "frame_3308.png"}, {"time": 1678.62, "image_uri": "frame_3309.png"}, {"time": 1679.127, "image_uri": "frame_3310.png"}, {"time": 1679.635, "image_uri": "frame_3311.png"}, {"time": 1680.142, "image_uri": "frame_3312.png"}, {"time": 1680.65, "image_uri": "frame_3313.png"}, {"time": 1681.157, "image_uri": "frame_3314.png"}, {"time": 1681.664, "image_uri": "frame_3315.png"}, {"time": 1682.172, "image_uri": "frame_3316.png"}, {"time": 1682.679, "image_uri": "frame_3317.png"}, {"time": 1683.187, "image_uri": "frame_3318.png"}, {"time": 1683.694, "image_uri": "frame_3319.png"}, {"time": 1684.202, "image_uri": "frame_3320.png"}, {"time": 1684.709, "image_uri": "frame_3321.png"}, {"time": 1685.217, "image_uri": "frame_3322.png"}, {"time": 1685.724, "image_uri": "frame_3323.png"}, {"time": 1686.231, "image_uri": "frame_3324.png"}, {"time": 1686.739, "image_uri": "frame_3325.png"}, {"time": 1687.246, "image_uri": "frame_3326.png"}, {"time": 1687.754, "image_uri": "frame_3327.png"}, {"time": 1688.261, "image_uri": "frame_3328.png"}, {"time": 1688.769, "image_uri": "frame_3329.png"}, {"time": 1689.276, "image_uri": "frame_3330.png"}, {"time": 1689.783, "image_uri": "frame_3331.png"}, {"time": 1690.291, "image_uri": "frame_3332.png"}, {"time": 1690.798, "image_uri": "frame_3333.png"}, {"time": 1691.306, "image_uri": "frame_3334.png"}, {"time": 1691.813, "image_uri": "frame_3335.png"}, {"time": 1692.321, "image_uri": "frame_3336.png"}, {"time": 1692.828, "image_uri": "frame_3337.png"}, {"time": 1693.336, "image_uri": "frame_3338.png"}, {"time": 1693.843, "image_uri": "frame_3339.png"}, {"time": 1694.35, "image_uri": "frame_3340.png"}, {"time": 1694.858, "image_uri": "frame_3341.png"}, {"time": 1695.365, "image_uri": "frame_3342.png"}, {"time": 1695.873, "image_uri": "frame_3343.png"}, {"time": 1696.38, "image_uri": "frame_3344.png"}, {"time": 1696.888, "image_uri": "frame_3345.png"}, {"time": 1697.395, "image_uri": "frame_3346.png"}, {"time": 1697.903, "image_uri": "frame_3347.png"}, {"time": 1698.41, "image_uri": "frame_3348.png"}, {"time": 1698.917, "image_uri": "frame_3349.png"}, {"time": 1699.425, "image_uri": "frame_3350.png"}, {"time": 1699.932, "image_uri": "frame_3351.png"}, {"time": 1700.44, "image_uri": "frame_3352.png"}, {"time": 1700.947, "image_uri": "frame_3353.png"}, {"time": 1701.455, "image_uri": "frame_3354.png"}, {"time": 1701.962, "image_uri": "frame_3355.png"}, {"time": 1702.47, "image_uri": "frame_3356.png"}, {"time": 1702.977, "image_uri": "frame_3357.png"}, {"time": 1703.484, "image_uri": "frame_3358.png"}, {"time": 1703.992, "image_uri": "frame_3359.png"}, {"time": 1704.499, "image_uri": "frame_3360.png"}, {"time": 1705.007, "image_uri": "frame_3361.png"}, {"time": 1705.514, "image_uri": "frame_3362.png"}, {"time": 1706.022, "image_uri": "frame_3363.png"}, {"time": 1706.529, "image_uri": "frame_3364.png"}, {"time": 1707.037, "image_uri": "frame_3365.png"}, {"time": 1707.544, "image_uri": "frame_3366.png"}, {"time": 1708.051, "image_uri": "frame_3367.png"}, {"time": 1708.559, "image_uri": "frame_3368.png"}, {"time": 1709.066, "image_uri": "frame_3369.png"}, {"time": 1709.574, "image_uri": "frame_3370.png"}, {"time": 1710.081, "image_uri": "frame_3371.png"}, {"time": 1710.589, "image_uri": "frame_3372.png"}, {"time": 1711.096, "image_uri": "frame_3373.png"}, {"time": 1711.604, "image_uri": "frame_3374.png"}, {"time": 1712.111, "image_uri": "frame_3375.png"}, {"time": 1712.618, "image_uri": "frame_3376.png"}, {"time": 1713.126, "image_uri": "frame_3377.png"}, {"time": 1713.633, "image_uri": "frame_3378.png"}, {"time": 1714.141, "image_uri": "frame_3379.png"}, {"time": 1714.648, "image_uri": "frame_3380.png"}, {"time": 1715.156, "image_uri": "frame_3381.png"}, {"time": 1715.663, "image_uri": "frame_3382.png"}, {"time": 1716.171, "image_uri": "frame_3383.png"}, {"time": 1716.678, "image_uri": "frame_3384.png"}, {"time": 1717.185, "image_uri": "frame_3385.png"}, {"time": 1717.693, "image_uri": "frame_3386.png"}, {"time": 1718.2, "image_uri": "frame_3387.png"}, {"time": 1718.708, "image_uri": "frame_3388.png"}, {"time": 1719.215, "image_uri": "frame_3389.png"}, {"time": 1719.723, "image_uri": "frame_3390.png"}, {"time": 1720.23, "image_uri": "frame_3391.png"}, {"time": 1720.737, "image_uri": "frame_3392.png"}, {"time": 1721.245, "image_uri": "frame_3393.png"}, {"time": 1721.752, "image_uri": "frame_3394.png"}, {"time": 1722.26, "image_uri": "frame_3395.png"}, {"time": 1722.767, "image_uri": "frame_3396.png"}, {"time": 1723.275, "image_uri": "frame_3397.png"}, {"time": 1723.782, "image_uri": "frame_3398.png"}, {"time": 1724.29, "image_uri": "frame_3399.png"}, {"time": 1724.797, "image_uri": "frame_3400.png"}, {"time": 1725.304, "image_uri": "frame_3401.png"}, {"time": 1725.812, "image_uri": "frame_3402.png"}, {"time": 1726.319, "image_uri": "frame_3403.png"}, {"time": 1726.827, "image_uri": "frame_3404.png"}, {"time": 1727.334, "image_uri": "frame_3405.png"}, {"time": 1727.842, "image_uri": "frame_3406.png"}, {"time": 1728.349, "image_uri": "frame_3407.png"}, {"time": 1728.857, "image_uri": "frame_3408.png"}, {"time": 1729.364, "image_uri": "frame_3409.png"}, {"time": 1729.871, "image_uri": "frame_3410.png"}, {"time": 1730.379, "image_uri": "frame_3411.png"}, {"time": 1730.886, "image_uri": "frame_3412.png"}, {"time": 1731.394, "image_uri": "frame_3413.png"}, {"time": 1731.901, "image_uri": "frame_3414.png"}, {"time": 1732.409, "image_uri": "frame_3415.png"}, {"time": 1732.916, "image_uri": "frame_3416.png"}, {"time": 1733.424, "image_uri": "frame_3417.png"}, {"time": 1733.931, "image_uri": "frame_3418.png"}, {"time": 1734.438, "image_uri": "frame_3419.png"}, {"time": 1734.946, "image_uri": "frame_3420.png"}, {"time": 1735.453, "image_uri": "frame_3421.png"}, {"time": 1735.961, "image_uri": "frame_3422.png"}, {"time": 1736.468, "image_uri": "frame_3423.png"}, {"time": 1736.976, "image_uri": "frame_3424.png"}, {"time": 1737.483, "image_uri": "frame_3425.png"}, {"time": 1737.991, "image_uri": "frame_3426.png"}, {"time": 1738.498, "image_uri": "frame_3427.png"}, {"time": 1739.005, "image_uri": "frame_3428.png"}, {"time": 1739.513, "image_uri": "frame_3429.png"}, {"time": 1740.02, "image_uri": "frame_3430.png"}, {"time": 1740.528, "image_uri": "frame_3431.png"}, {"time": 1741.035, "image_uri": "frame_3432.png"}, {"time": 1741.543, "image_uri": "frame_3433.png"}, {"time": 1742.05, "image_uri": "frame_3434.png"}, {"time": 1742.558, "image_uri": "frame_3435.png"}, {"time": 1743.065, "image_uri": "frame_3436.png"}, {"time": 1743.572, "image_uri": "frame_3437.png"}, {"time": 1744.08, "image_uri": "frame_3438.png"}, {"time": 1744.587, "image_uri": "frame_3439.png"}, {"time": 1745.095, "image_uri": "frame_3440.png"}, {"time": 1745.602, "image_uri": "frame_3441.png"}, {"time": 1746.11, "image_uri": "frame_3442.png"}, {"time": 1746.617, "image_uri": "frame_3443.png"}, {"time": 1747.124, "image_uri": "frame_3444.png"}, {"time": 1747.632, "image_uri": "frame_3445.png"}, {"time": 1748.139, "image_uri": "frame_3446.png"}, {"time": 1748.647, "image_uri": "frame_3447.png"}, {"time": 1749.154, "image_uri": "frame_3448.png"}, {"time": 1749.662, "image_uri": "frame_3449.png"}, {"time": 1750.169, "image_uri": "frame_3450.png"}, {"time": 1750.677, "image_uri": "frame_3451.png"}, {"time": 1751.184, "image_uri": "frame_3452.png"}, {"time": 1751.691, "image_uri": "frame_3453.png"}, {"time": 1752.199, "image_uri": "frame_3454.png"}, {"time": 1752.706, "image_uri": "frame_3455.png"}, {"time": 1753.214, "image_uri": "frame_3456.png"}, {"time": 1753.721, "image_uri": "frame_3457.png"}, {"time": 1754.229, "image_uri": "frame_3458.png"}, {"time": 1754.736, "image_uri": "frame_3459.png"}, {"time": 1755.244, "image_uri": "frame_3460.png"}, {"time": 1755.751, "image_uri": "frame_3461.png"}, {"time": 1756.258, "image_uri": "frame_3462.png"}, {"time": 1756.766, "image_uri": "frame_3463.png"}, {"time": 1757.273, "image_uri": "frame_3464.png"}, {"time": 1757.781, "image_uri": "frame_3465.png"}, {"time": 1758.288, "image_uri": "frame_3466.png"}, {"time": 1758.796, "image_uri": "frame_3467.png"}, {"time": 1759.303, "image_uri": "frame_3468.png"}, {"time": 1759.811, "image_uri": "frame_3469.png"}, {"time": 1760.318, "image_uri": "frame_3470.png"}, {"time": 1760.825, "image_uri": "frame_3471.png"}, {"time": 1761.333, "image_uri": "frame_3472.png"}, {"time": 1761.84, "image_uri": "frame_3473.png"}, {"time": 1762.348, "image_uri": "frame_3474.png"}, {"time": 1762.855, "image_uri": "frame_3475.png"}, {"time": 1763.363, "image_uri": "frame_3476.png"}, {"time": 1763.87, "image_uri": "frame_3477.png"}, {"time": 1764.378, "image_uri": "frame_3478.png"}, {"time": 1764.885, "image_uri": "frame_3479.png"}, {"time": 1765.392, "image_uri": "frame_3480.png"}, {"time": 1765.9, "image_uri": "frame_3481.png"}, {"time": 1766.407, "image_uri": "frame_3482.png"}, {"time": 1766.915, "image_uri": "frame_3483.png"}, {"time": 1767.422, "image_uri": "frame_3484.png"}, {"time": 1767.93, "image_uri": "frame_3485.png"}, {"time": 1768.437, "image_uri": "frame_3486.png"}, {"time": 1768.945, "image_uri": "frame_3487.png"}, {"time": 1769.452, "image_uri": "frame_3488.png"}, {"time": 1769.959, "image_uri": "frame_3489.png"}, {"time": 1770.467, "image_uri": "frame_3490.png"}, {"time": 1770.974, "image_uri": "frame_3491.png"}, {"time": 1771.482, "image_uri": "frame_3492.png"}, {"time": 1771.989, "image_uri": "frame_3493.png"}, {"time": 1772.497, "image_uri": "frame_3494.png"}, {"time": 1773.004, "image_uri": "frame_3495.png"}, {"time": 1773.512, "image_uri": "frame_3496.png"}, {"time": 1774.019, "image_uri": "frame_3497.png"}, {"time": 1774.526, "image_uri": "frame_3498.png"}, {"time": 1775.034, "image_uri": "frame_3499.png"}, {"time": 1775.541, "image_uri": "frame_3500.png"}, {"time": 1776.049, "image_uri": "frame_3501.png"}, {"time": 1776.556, "image_uri": "frame_3502.png"}, {"time": 1777.064, "image_uri": "frame_3503.png"}, {"time": 1777.571, "image_uri": "frame_3504.png"}, {"time": 1778.078, "image_uri": "frame_3505.png"}, {"time": 1778.586, "image_uri": "frame_3506.png"}, {"time": 1779.093, "image_uri": "frame_3507.png"}, {"time": 1779.601, "image_uri": "frame_3508.png"}, {"time": 1780.108, "image_uri": "frame_3509.png"}, {"time": 1780.616, "image_uri": "frame_3510.png"}, {"time": 1781.123, "image_uri": "frame_3511.png"}, {"time": 1781.631, "image_uri": "frame_3512.png"}, {"time": 1782.138, "image_uri": "frame_3513.png"}, {"time": 1782.645, "image_uri": "frame_3514.png"}, {"time": 1783.153, "image_uri": "frame_3515.png"}, {"time": 1783.66, "image_uri": "frame_3516.png"}, {"time": 1784.168, "image_uri": "frame_3517.png"}, {"time": 1784.675, "image_uri": "frame_3518.png"}, {"time": 1785.183, "image_uri": "frame_3519.png"}, {"time": 1785.69, "image_uri": "frame_3520.png"}, {"time": 1786.198, "image_uri": "frame_3521.png"}, {"time": 1786.705, "image_uri": "frame_3522.png"}, {"time": 1787.212, "image_uri": "frame_3523.png"}, {"time": 1787.72, "image_uri": "frame_3524.png"}, {"time": 1788.227, "image_uri": "frame_3525.png"}, {"time": 1788.735, "image_uri": "frame_3526.png"}, {"time": 1789.242, "image_uri": "frame_3527.png"}, {"time": 1789.75, "image_uri": "frame_3528.png"}, {"time": 1790.257, "image_uri": "frame_3529.png"}, {"time": 1790.765, "image_uri": "frame_3530.png"}, {"time": 1791.272, "image_uri": "frame_3531.png"}, {"time": 1791.779, "image_uri": "frame_3532.png"}, {"time": 1792.287, "image_uri": "frame_3533.png"}, {"time": 1792.794, "image_uri": "frame_3534.png"}, {"time": 1793.302, "image_uri": "frame_3535.png"}, {"time": 1793.809, "image_uri": "frame_3536.png"}, {"time": 1794.317, "image_uri": "frame_3537.png"}, {"time": 1794.824, "image_uri": "frame_3538.png"}, {"time": 1795.332, "image_uri": "frame_3539.png"}, {"time": 1795.839, "image_uri": "frame_3540.png"}, {"time": 1796.346, "image_uri": "frame_3541.png"}, {"time": 1796.854, "image_uri": "frame_3542.png"}, {"time": 1797.361, "image_uri": "frame_3543.png"}, {"time": 1797.869, "image_uri": "frame_3544.png"}, {"time": 1798.376, "image_uri": "frame_3545.png"}, {"time": 1798.884, "image_uri": "frame_3546.png"}, {"time": 1799.391, "image_uri": "frame_3547.png"}, {"time": 1799.899, "image_uri": "frame_3548.png"}, {"time": 1800.406, "image_uri": "frame_3549.png"}, {"time": 1800.913, "image_uri": "frame_3550.png"}, {"time": 1801.421, "image_uri": "frame_3551.png"}, {"time": 1801.928, "image_uri": "frame_3552.png"}, {"time": 1802.436, "image_uri": "frame_3553.png"}, {"time": 1802.943, "image_uri": "frame_3554.png"}, {"time": 1803.451, "image_uri": "frame_3555.png"}, {"time": 1803.958, "image_uri": "frame_3556.png"}, {"time": 1804.465, "image_uri": "frame_3557.png"}, {"time": 1804.973, "image_uri": "frame_3558.png"}, {"time": 1805.48, "image_uri": "frame_3559.png"}, {"time": 1805.988, "image_uri": "frame_3560.png"}, {"time": 1806.495, "image_uri": "frame_3561.png"}, {"time": 1807.003, "image_uri": "frame_3562.png"}, {"time": 1807.51, "image_uri": "frame_3563.png"}, {"time": 1808.018, "image_uri": "frame_3564.png"}, {"time": 1808.525, "image_uri": "frame_3565.png"}, {"time": 1809.032, "image_uri": "frame_3566.png"}, {"time": 1809.54, "image_uri": "frame_3567.png"}, {"time": 1810.047, "image_uri": "frame_3568.png"}, {"time": 1810.555, "image_uri": "frame_3569.png"}, {"time": 1811.062, "image_uri": "frame_3570.png"}, {"time": 1811.57, "image_uri": "frame_3571.png"}, {"time": 1812.077, "image_uri": "frame_3572.png"}, {"time": 1812.585, "image_uri": "frame_3573.png"}, {"time": 1813.092, "image_uri": "frame_3574.png"}, {"time": 1813.599, "image_uri": "frame_3575.png"}, {"time": 1814.107, "image_uri": "frame_3576.png"}, {"time": 1814.614, "image_uri": "frame_3577.png"}, {"time": 1815.122, "image_uri": "frame_3578.png"}, {"time": 1815.629, "image_uri": "frame_3579.png"}, {"time": 1816.137, "image_uri": "frame_3580.png"}, {"time": 1816.644, "image_uri": "frame_3581.png"}, {"time": 1817.152, "image_uri": "frame_3582.png"}, {"time": 1817.659, "image_uri": "frame_3583.png"}, {"time": 1818.166, "image_uri": "frame_3584.png"}, {"time": 1818.674, "image_uri": "frame_3585.png"}, {"time": 1819.181, "image_uri": "frame_3586.png"}, {"time": 1819.689, "image_uri": "frame_3587.png"}, {"time": 1820.196, "image_uri": "frame_3588.png"}, {"time": 1820.704, "image_uri": "frame_3589.png"}, {"time": 1821.211, "image_uri": "frame_3590.png"}, {"time": 1821.719, "image_uri": "frame_3591.png"}, {"time": 1822.226, "image_uri": "frame_3592.png"}, {"time": 1822.733, "image_uri": "frame_3593.png"}, {"time": 1823.241, "image_uri": "frame_3594.png"}, {"time": 1823.748, "image_uri": "frame_3595.png"}, {"time": 1824.256, "image_uri": "frame_3596.png"}, {"time": 1824.763, "image_uri": "frame_3597.png"}, {"time": 1825.271, "image_uri": "frame_3598.png"}, {"time": 1825.778, "image_uri": "frame_3599.png"}, {"time": 1826.286, "image_uri": "frame_3600.png"}, {"time": 1826.793, "image_uri": "frame_3601.png"}, {"time": 1827.3, "image_uri": "frame_3602.png"}, {"time": 1827.808, "image_uri": "frame_3603.png"}, {"time": 1828.315, "image_uri": "frame_3604.png"}, {"time": 1828.823, "image_uri": "frame_3605.png"}, {"time": 1829.33, "image_uri": "frame_3606.png"}, {"time": 1829.838, "image_uri": "frame_3607.png"}, {"time": 1830.345, "image_uri": "frame_3608.png"}, {"time": 1830.853, "image_uri": "frame_3609.png"}, {"time": 1831.36, "image_uri": "frame_3610.png"}, {"time": 1831.867, "image_uri": "frame_3611.png"}, {"time": 1832.375, "image_uri": "frame_3612.png"}, {"time": 1832.882, "image_uri": "frame_3613.png"}, {"time": 1833.39, "image_uri": "frame_3614.png"}, {"time": 1833.897, "image_uri": "frame_3615.png"}, {"time": 1834.405, "image_uri": "frame_3616.png"}, {"time": 1834.912, "image_uri": "frame_3617.png"}, {"time": 1835.419, "image_uri": "frame_3618.png"}, {"time": 1835.927, "image_uri": "frame_3619.png"}, {"time": 1836.434, "image_uri": "frame_3620.png"}, {"time": 1836.942, "image_uri": "frame_3621.png"}, {"time": 1837.449, "image_uri": "frame_3622.png"}, {"time": 1837.957, "image_uri": "frame_3623.png"}, {"time": 1838.464, "image_uri": "frame_3624.png"}, {"time": 1838.972, "image_uri": "frame_3625.png"}, {"time": 1839.479, "image_uri": "frame_3626.png"}, {"time": 1839.986, "image_uri": "frame_3627.png"}, {"time": 1840.494, "image_uri": "frame_3628.png"}, {"time": 1841.001, "image_uri": "frame_3629.png"}, {"time": 1841.509, "image_uri": "frame_3630.png"}, {"time": 1842.016, "image_uri": "frame_3631.png"}, {"time": 1842.524, "image_uri": "frame_3632.png"}, {"time": 1843.031, "image_uri": "frame_3633.png"}, {"time": 1843.539, "image_uri": "frame_3634.png"}, {"time": 1844.046, "image_uri": "frame_3635.png"}, {"time": 1844.553, "image_uri": "frame_3636.png"}, {"time": 1845.061, "image_uri": "frame_3637.png"}, {"time": 1845.568, "image_uri": "frame_3638.png"}, {"time": 1846.076, "image_uri": "frame_3639.png"}, {"time": 1846.583, "image_uri": "frame_3640.png"}, {"time": 1847.091, "image_uri": "frame_3641.png"}, {"time": 1847.598, "image_uri": "frame_3642.png"}, {"time": 1848.106, "image_uri": "frame_3643.png"}, {"time": 1848.613, "image_uri": "frame_3644.png"}, {"time": 1849.12, "image_uri": "frame_3645.png"}, {"time": 1849.628, "image_uri": "frame_3646.png"}, {"time": 1850.135, "image_uri": "frame_3647.png"}, {"time": 1850.643, "image_uri": "frame_3648.png"}, {"time": 1851.15, "image_uri": "frame_3649.png"}, {"time": 1851.658, "image_uri": "frame_3650.png"}, {"time": 1852.165, "image_uri": "frame_3651.png"}, {"time": 1852.673, "image_uri": "frame_3652.png"}, {"time": 1853.18, "image_uri": "frame_3653.png"}, {"time": 1853.687, "image_uri": "frame_3654.png"}, {"time": 1854.195, "image_uri": "frame_3655.png"}, {"time": 1854.702, "image_uri": "frame_3656.png"}, {"time": 1855.21, "image_uri": "frame_3657.png"}, {"time": 1855.717, "image_uri": "frame_3658.png"}, {"time": 1856.225, "image_uri": "frame_3659.png"}, {"time": 1856.732, "image_uri": "frame_3660.png"}, {"time": 1857.24, "image_uri": "frame_3661.png"}, {"time": 1857.747, "image_uri": "frame_3662.png"}, {"time": 1858.254, "image_uri": "frame_3663.png"}, {"time": 1858.762, "image_uri": "frame_3664.png"}, {"time": 1859.269, "image_uri": "frame_3665.png"}, {"time": 1859.777, "image_uri": "frame_3666.png"}, {"time": 1860.284, "image_uri": "frame_3667.png"}, {"time": 1860.792, "image_uri": "frame_3668.png"}, {"time": 1861.299, "image_uri": "frame_3669.png"}, {"time": 1861.806, "image_uri": "frame_3670.png"}, {"time": 1862.314, "image_uri": "frame_3671.png"}, {"time": 1862.821, "image_uri": "frame_3672.png"}, {"time": 1863.329, "image_uri": "frame_3673.png"}, {"time": 1863.836, "image_uri": "frame_3674.png"}, {"time": 1864.344, "image_uri": "frame_3675.png"}, {"time": 1864.851, "image_uri": "frame_3676.png"}, {"time": 1865.359, "image_uri": "frame_3677.png"}, {"time": 1865.866, "image_uri": "frame_3678.png"}, {"time": 1866.373, "image_uri": "frame_3679.png"}, {"time": 1866.881, "image_uri": "frame_3680.png"}, {"time": 1867.388, "image_uri": "frame_3681.png"}, {"time": 1867.896, "image_uri": "frame_3682.png"}, {"time": 1868.403, "image_uri": "frame_3683.png"}, {"time": 1868.911, "image_uri": "frame_3684.png"}, {"time": 1869.418, "image_uri": "frame_3685.png"}, {"time": 1869.926, "image_uri": "frame_3686.png"}, {"time": 1870.433, "image_uri": "frame_3687.png"}, {"time": 1870.94, "image_uri": "frame_3688.png"}, {"time": 1871.448, "image_uri": "frame_3689.png"}, {"time": 1871.955, "image_uri": "frame_3690.png"}, {"time": 1872.463, "image_uri": "frame_3691.png"}, {"time": 1872.97, "image_uri": "frame_3692.png"}, {"time": 1873.478, "image_uri": "frame_3693.png"}, {"time": 1873.985, "image_uri": "frame_3694.png"}, {"time": 1874.493, "image_uri": "frame_3695.png"}, {"time": 1875.0, "image_uri": "frame_3696.png"}, {"time": 1875.507, "image_uri": "frame_3697.png"}, {"time": 1876.015, "image_uri": "frame_3698.png"}, {"time": 1876.522, "image_uri": "frame_3699.png"}, {"time": 1877.03, "image_uri": "frame_3700.png"}, {"time": 1877.537, "image_uri": "frame_3701.png"}, {"time": 1878.045, "image_uri": "frame_3702.png"}, {"time": 1878.552, "image_uri": "frame_3703.png"}, {"time": 1879.06, "image_uri": "frame_3704.png"}, {"time": 1879.567, "image_uri": "frame_3705.png"}, {"time": 1880.074, "image_uri": "frame_3706.png"}, {"time": 1880.582, "image_uri": "frame_3707.png"}, {"time": 1881.089, "image_uri": "frame_3708.png"}, {"time": 1881.597, "image_uri": "frame_3709.png"}, {"time": 1882.104, "image_uri": "frame_3710.png"}, {"time": 1882.612, "image_uri": "frame_3711.png"}, {"time": 1883.119, "image_uri": "frame_3712.png"}, {"time": 1883.627, "image_uri": "frame_3713.png"}, {"time": 1884.134, "image_uri": "frame_3714.png"}, {"time": 1884.641, "image_uri": "frame_3715.png"}, {"time": 1885.149, "image_uri": "frame_3716.png"}, {"time": 1885.656, "image_uri": "frame_3717.png"}, {"time": 1886.164, "image_uri": "frame_3718.png"}, {"time": 1886.671, "image_uri": "frame_3719.png"}, {"time": 1887.179, "image_uri": "frame_3720.png"}, {"time": 1887.686, "image_uri": "frame_3721.png"}, {"time": 1888.194, "image_uri": "frame_3722.png"}, {"time": 1888.701, "image_uri": "frame_3723.png"}, {"time": 1889.208, "image_uri": "frame_3724.png"}, {"time": 1889.716, "image_uri": "frame_3725.png"}, {"time": 1890.223, "image_uri": "frame_3726.png"}, {"time": 1890.731, "image_uri": "frame_3727.png"}, {"time": 1891.238, "image_uri": "frame_3728.png"}, {"time": 1891.746, "image_uri": "frame_3729.png"}, {"time": 1892.253, "image_uri": "frame_3730.png"}, {"time": 1892.76, "image_uri": "frame_3731.png"}, {"time": 1893.268, "image_uri": "frame_3732.png"}, {"time": 1893.775, "image_uri": "frame_3733.png"}, {"time": 1894.283, "image_uri": "frame_3734.png"}, {"time": 1894.79, "image_uri": "frame_3735.png"}, {"time": 1895.298, "image_uri": "frame_3736.png"}, {"time": 1895.805, "image_uri": "frame_3737.png"}, {"time": 1896.313, "image_uri": "frame_3738.png"}, {"time": 1896.82, "image_uri": "frame_3739.png"}, {"time": 1897.327, "image_uri": "frame_3740.png"}, {"time": 1897.835, "image_uri": "frame_3741.png"}, {"time": 1898.342, "image_uri": "frame_3742.png"}, {"time": 1898.85, "image_uri": "frame_3743.png"}, {"time": 1899.357, "image_uri": "frame_3744.png"}, {"time": 1899.865, "image_uri": "frame_3745.png"}, {"time": 1900.372, "image_uri": "frame_3746.png"}, {"time": 1900.88, "image_uri": "frame_3747.png"}, {"time": 1901.387, "image_uri": "frame_3748.png"}, {"time": 1901.894, "image_uri": "frame_3749.png"}, {"time": 1902.402, "image_uri": "frame_3750.png"}, {"time": 1902.909, "image_uri": "frame_3751.png"}, {"time": 1903.417, "image_uri": "frame_3752.png"}, {"time": 1903.924, "image_uri": "frame_3753.png"}, {"time": 1904.432, "image_uri": "frame_3754.png"}, {"time": 1904.939, "image_uri": "frame_3755.png"}, {"time": 1905.447, "image_uri": "frame_3756.png"}, {"time": 1905.954, "image_uri": "frame_3757.png"}, {"time": 1906.461, "image_uri": "frame_3758.png"}, {"time": 1906.969, "image_uri": "frame_3759.png"}, {"time": 1907.476, "image_uri": "frame_3760.png"}, {"time": 1907.984, "image_uri": "frame_3761.png"}, {"time": 1908.491, "image_uri": "frame_3762.png"}, {"time": 1908.999, "image_uri": "frame_3763.png"}, {"time": 1909.506, "image_uri": "frame_3764.png"}, {"time": 1910.014, "image_uri": "frame_3765.png"}, {"time": 1910.521, "image_uri": "frame_3766.png"}, {"time": 1911.028, "image_uri": "frame_3767.png"}, {"time": 1911.536, "image_uri": "frame_3768.png"}, {"time": 1912.043, "image_uri": "frame_3769.png"}, {"time": 1912.551, "image_uri": "frame_3770.png"}, {"time": 1913.058, "image_uri": "frame_3771.png"}, {"time": 1913.566, "image_uri": "frame_3772.png"}, {"time": 1914.073, "image_uri": "frame_3773.png"}, {"time": 1914.581, "image_uri": "frame_3774.png"}, {"time": 1915.088, "image_uri": "frame_3775.png"}, {"time": 1915.595, "image_uri": "frame_3776.png"}, {"time": 1916.103, "image_uri": "frame_3777.png"}, {"time": 1916.61, "image_uri": "frame_3778.png"}, {"time": 1917.118, "image_uri": "frame_3779.png"}, {"time": 1917.625, "image_uri": "frame_3780.png"}, {"time": 1918.133, "image_uri": "frame_3781.png"}, {"time": 1918.64, "image_uri": "frame_3782.png"}, {"time": 1919.147, "image_uri": "frame_3783.png"}, {"time": 1919.655, "image_uri": "frame_3784.png"}, {"time": 1920.162, "image_uri": "frame_3785.png"}, {"time": 1920.67, "image_uri": "frame_3786.png"}, {"time": 1921.177, "image_uri": "frame_3787.png"}, {"time": 1921.685, "image_uri": "frame_3788.png"}, {"time": 1922.192, "image_uri": "frame_3789.png"}, {"time": 1922.7, "image_uri": "frame_3790.png"}, {"time": 1923.207, "image_uri": "frame_3791.png"}, {"time": 1923.714, "image_uri": "frame_3792.png"}, {"time": 1924.222, "image_uri": "frame_3793.png"}, {"time": 1924.729, "image_uri": "frame_3794.png"}, {"time": 1925.237, "image_uri": "frame_3795.png"}, {"time": 1925.744, "image_uri": "frame_3796.png"}, {"time": 1926.252, "image_uri": "frame_3797.png"}, {"time": 1926.759, "image_uri": "frame_3798.png"}, {"time": 1927.267, "image_uri": "frame_3799.png"}, {"time": 1927.774, "image_uri": "frame_3800.png"}, {"time": 1928.281, "image_uri": "frame_3801.png"}, {"time": 1928.789, "image_uri": "frame_3802.png"}, {"time": 1929.296, "image_uri": "frame_3803.png"}, {"time": 1929.804, "image_uri": "frame_3804.png"}, {"time": 1930.311, "image_uri": "frame_3805.png"}, {"time": 1930.819, "image_uri": "frame_3806.png"}, {"time": 1931.326, "image_uri": "frame_3807.png"}, {"time": 1931.834, "image_uri": "frame_3808.png"}, {"time": 1932.341, "image_uri": "frame_3809.png"}, {"time": 1932.848, "image_uri": "frame_3810.png"}, {"time": 1933.356, "image_uri": "frame_3811.png"}, {"time": 1933.863, "image_uri": "frame_3812.png"}, {"time": 1934.371, "image_uri": "frame_3813.png"}, {"time": 1934.878, "image_uri": "frame_3814.png"}, {"time": 1935.386, "image_uri": "frame_3815.png"}, {"time": 1935.893, "image_uri": "frame_3816.png"}, {"time": 1936.401, "image_uri": "frame_3817.png"}, {"time": 1936.908, "image_uri": "frame_3818.png"}, {"time": 1937.415, "image_uri": "frame_3819.png"}, {"time": 1937.923, "image_uri": "frame_3820.png"}, {"time": 1938.43, "image_uri": "frame_3821.png"}, {"time": 1938.938, "image_uri": "frame_3822.png"}, {"time": 1939.445, "image_uri": "frame_3823.png"}, {"time": 1939.953, "image_uri": "frame_3824.png"}, {"time": 1940.46, "image_uri": "frame_3825.png"}, {"time": 1940.968, "image_uri": "frame_3826.png"}, {"time": 1941.475, "image_uri": "frame_3827.png"}, {"time": 1941.982, "image_uri": "frame_3828.png"}, {"time": 1942.49, "image_uri": "frame_3829.png"}, {"time": 1942.997, "image_uri": "frame_3830.png"}, {"time": 1943.505, "image_uri": "frame_3831.png"}, {"time": 1944.012, "image_uri": "frame_3832.png"}, {"time": 1944.52, "image_uri": "frame_3833.png"}, {"time": 1945.027, "image_uri": "frame_3834.png"}, {"time": 1945.535, "image_uri": "frame_3835.png"}, {"time": 1946.042, "image_uri": "frame_3836.png"}, {"time": 1946.549, "image_uri": "frame_3837.png"}, {"time": 1947.057, "image_uri": "frame_3838.png"}, {"time": 1947.564, "image_uri": "frame_3839.png"}, {"time": 1948.072, "image_uri": "frame_3840.png"}, {"time": 1948.579, "image_uri": "frame_3841.png"}, {"time": 1949.087, "image_uri": "frame_3842.png"}, {"time": 1949.594, "image_uri": "frame_3843.png"}, {"time": 1950.101, "image_uri": "frame_3844.png"}, {"time": 1950.609, "image_uri": "frame_3845.png"}, {"time": 1951.116, "image_uri": "frame_3846.png"}, {"time": 1951.624, "image_uri": "frame_3847.png"}, {"time": 1952.131, "image_uri": "frame_3848.png"}, {"time": 1952.639, "image_uri": "frame_3849.png"}, {"time": 1953.146, "image_uri": "frame_3850.png"}, {"time": 1953.654, "image_uri": "frame_3851.png"}, {"time": 1954.161, "image_uri": "frame_3852.png"}, {"time": 1954.668, "image_uri": "frame_3853.png"}, {"time": 1955.176, "image_uri": "frame_3854.png"}, {"time": 1955.683, "image_uri": "frame_3855.png"}, {"time": 1956.191, "image_uri": "frame_3856.png"}, {"time": 1956.698, "image_uri": "frame_3857.png"}, {"time": 1957.206, "image_uri": "frame_3858.png"}, {"time": 1957.713, "image_uri": "frame_3859.png"}, {"time": 1958.221, "image_uri": "frame_3860.png"}, {"time": 1958.728, "image_uri": "frame_3861.png"}, {"time": 1959.235, "image_uri": "frame_3862.png"}, {"time": 1959.743, "image_uri": "frame_3863.png"}, {"time": 1960.25, "image_uri": "frame_3864.png"}, {"time": 1960.758, "image_uri": "frame_3865.png"}, {"time": 1961.265, "image_uri": "frame_3866.png"}, {"time": 1961.773, "image_uri": "frame_3867.png"}, {"time": 1962.28, "image_uri": "frame_3868.png"}, {"time": 1962.788, "image_uri": "frame_3869.png"}, {"time": 1963.295, "image_uri": "frame_3870.png"}, {"time": 1963.802, "image_uri": "frame_3871.png"}, {"time": 1964.31, "image_uri": "frame_3872.png"}, {"time": 1964.817, "image_uri": "frame_3873.png"}, {"time": 1965.325, "image_uri": "frame_3874.png"}, {"time": 1965.832, "image_uri": "frame_3875.png"}, {"time": 1966.34, "image_uri": "frame_3876.png"}, {"time": 1966.847, "image_uri": "frame_3877.png"}, {"time": 1967.355, "image_uri": "frame_3878.png"}, {"time": 1967.862, "image_uri": "frame_3879.png"}, {"time": 1968.369, "image_uri": "frame_3880.png"}, {"time": 1968.877, "image_uri": "frame_3881.png"}, {"time": 1969.384, "image_uri": "frame_3882.png"}, {"time": 1969.892, "image_uri": "frame_3883.png"}, {"time": 1970.399, "image_uri": "frame_3884.png"}, {"time": 1970.907, "image_uri": "frame_3885.png"}, {"time": 1971.414, "image_uri": "frame_3886.png"}, {"time": 1971.922, "image_uri": "frame_3887.png"}, {"time": 1972.429, "image_uri": "frame_3888.png"}, {"time": 1972.936, "image_uri": "frame_3889.png"}, {"time": 1973.444, "image_uri": "frame_3890.png"}, {"time": 1973.951, "image_uri": "frame_3891.png"}, {"time": 1974.459, "image_uri": "frame_3892.png"}, {"time": 1974.966, "image_uri": "frame_3893.png"}, {"time": 1975.474, "image_uri": "frame_3894.png"}, {"time": 1975.981, "image_uri": "frame_3895.png"}, {"time": 1976.488, "image_uri": "frame_3896.png"}, {"time": 1976.996, "image_uri": "frame_3897.png"}, {"time": 1977.503, "image_uri": "frame_3898.png"}, {"time": 1978.011, "image_uri": "frame_3899.png"}, {"time": 1978.518, "image_uri": "frame_3900.png"}, {"time": 1979.026, "image_uri": "frame_3901.png"}, {"time": 1979.533, "image_uri": "frame_3902.png"}, {"time": 1980.041, "image_uri": "frame_3903.png"}, {"time": 1980.548, "image_uri": "frame_3904.png"}, {"time": 1981.055, "image_uri": "frame_3905.png"}, {"time": 1981.563, "image_uri": "frame_3906.png"}, {"time": 1982.07, "image_uri": "frame_3907.png"}, {"time": 1982.578, "image_uri": "frame_3908.png"}, {"time": 1983.085, "image_uri": "frame_3909.png"}, {"time": 1983.593, "image_uri": "frame_3910.png"}, {"time": 1984.1, "image_uri": "frame_3911.png"}, {"time": 1984.608, "image_uri": "frame_3912.png"}, {"time": 1985.115, "image_uri": "frame_3913.png"}, {"time": 1985.622, "image_uri": "frame_3914.png"}, {"time": 1986.13, "image_uri": "frame_3915.png"}, {"time": 1986.637, "image_uri": "frame_3916.png"}, {"time": 1987.145, "image_uri": "frame_3917.png"}, {"time": 1987.652, "image_uri": "frame_3918.png"}, {"time": 1988.16, "image_uri": "frame_3919.png"}, {"time": 1988.667, "image_uri": "frame_3920.png"}, {"time": 1989.175, "image_uri": "frame_3921.png"}, {"time": 1989.682, "image_uri": "frame_3922.png"}, {"time": 1990.189, "image_uri": "frame_3923.png"}, {"time": 1990.697, "image_uri": "frame_3924.png"}, {"time": 1991.204, "image_uri": "frame_3925.png"}, {"time": 1991.712, "image_uri": "frame_3926.png"}, {"time": 1992.219, "image_uri": "frame_3927.png"}, {"time": 1992.727, "image_uri": "frame_3928.png"}, {"time": 1993.234, "image_uri": "frame_3929.png"}, {"time": 1993.742, "image_uri": "frame_3930.png"}, {"time": 1994.249, "image_uri": "frame_3931.png"}, {"time": 1994.756, "image_uri": "frame_3932.png"}, {"time": 1995.264, "image_uri": "frame_3933.png"}, {"time": 1995.771, "image_uri": "frame_3934.png"}, {"time": 1996.279, "image_uri": "frame_3935.png"}, {"time": 1996.786, "image_uri": "frame_3936.png"}, {"time": 1997.294, "image_uri": "frame_3937.png"}, {"time": 1997.801, "image_uri": "frame_3938.png"}, {"time": 1998.309, "image_uri": "frame_3939.png"}, {"time": 1998.816, "image_uri": "frame_3940.png"}, {"time": 1999.323, "image_uri": "frame_3941.png"}, {"time": 1999.831, "image_uri": "frame_3942.png"}, {"time": 2000.338, "image_uri": "frame_3943.png"}, {"time": 2000.846, "image_uri": "frame_3944.png"}, {"time": 2001.353, "image_uri": "frame_3945.png"}, {"time": 2001.861, "image_uri": "frame_3946.png"}, {"time": 2002.368, "image_uri": "frame_3947.png"}, {"time": 2002.876, "image_uri": "frame_3948.png"}, {"time": 2003.383, "image_uri": "frame_3949.png"}, {"time": 2003.89, "image_uri": "frame_3950.png"}, {"time": 2004.398, "image_uri": "frame_3951.png"}, {"time": 2004.905, "image_uri": "frame_3952.png"}, {"time": 2005.413, "image_uri": "frame_3953.png"}, {"time": 2005.92, "image_uri": "frame_3954.png"}, {"time": 2006.428, "image_uri": "frame_3955.png"}, {"time": 2006.935, "image_uri": "frame_3956.png"}, {"time": 2007.442, "image_uri": "frame_3957.png"}, {"time": 2007.95, "image_uri": "frame_3958.png"}, {"time": 2008.457, "image_uri": "frame_3959.png"}, {"time": 2008.965, "image_uri": "frame_3960.png"}, {"time": 2009.472, "image_uri": "frame_3961.png"}, {"time": 2009.98, "image_uri": "frame_3962.png"}, {"time": 2010.487, "image_uri": "frame_3963.png"}, {"time": 2010.995, "image_uri": "frame_3964.png"}, {"time": 2011.502, "image_uri": "frame_3965.png"}, {"time": 2012.009, "image_uri": "frame_3966.png"}, {"time": 2012.517, "image_uri": "frame_3967.png"}, {"time": 2013.024, "image_uri": "frame_3968.png"}, {"time": 2013.532, "image_uri": "frame_3969.png"}, {"time": 2014.039, "image_uri": "frame_3970.png"}, {"time": 2014.547, "image_uri": "frame_3971.png"}, {"time": 2015.054, "image_uri": "frame_3972.png"}, {"time": 2015.562, "image_uri": "frame_3973.png"}, {"time": 2016.069, "image_uri": "frame_3974.png"}, {"time": 2016.576, "image_uri": "frame_3975.png"}, {"time": 2017.084, "image_uri": "frame_3976.png"}, {"time": 2017.591, "image_uri": "frame_3977.png"}, {"time": 2018.099, "image_uri": "frame_3978.png"}, {"time": 2018.606, "image_uri": "frame_3979.png"}, {"time": 2019.114, "image_uri": "frame_3980.png"}, {"time": 2019.621, "image_uri": "frame_3981.png"}, {"time": 2020.129, "image_uri": "frame_3982.png"}, {"time": 2020.636, "image_uri": "frame_3983.png"}, {"time": 2021.143, "image_uri": "frame_3984.png"}, {"time": 2021.651, "image_uri": "frame_3985.png"}, {"time": 2022.158, "image_uri": "frame_3986.png"}, {"time": 2022.666, "image_uri": "frame_3987.png"}, {"time": 2023.173, "image_uri": "frame_3988.png"}, {"time": 2023.681, "image_uri": "frame_3989.png"}, {"time": 2024.188, "image_uri": "frame_3990.png"}, {"time": 2024.696, "image_uri": "frame_3991.png"}, {"time": 2025.203, "image_uri": "frame_3992.png"}, {"time": 2025.71, "image_uri": "frame_3993.png"}, {"time": 2026.218, "image_uri": "frame_3994.png"}, {"time": 2026.725, "image_uri": "frame_3995.png"}, {"time": 2027.233, "image_uri": "frame_3996.png"}, {"time": 2027.74, "image_uri": "frame_3997.png"}, {"time": 2028.248, "image_uri": "frame_3998.png"}, {"time": 2028.755, "image_uri": "frame_3999.png"}, {"time": 2029.263, "image_uri": "frame_4000.png"}, {"time": 2029.77, "image_uri": "frame_4001.png"}, {"time": 2030.277, "image_uri": "frame_4002.png"}, {"time": 2030.785, "image_uri": "frame_4003.png"}, {"time": 2031.292, "image_uri": "frame_4004.png"}, {"time": 2031.8, "image_uri": "frame_4005.png"}, {"time": 2032.307, "image_uri": "frame_4006.png"}, {"time": 2032.815, "image_uri": "frame_4007.png"}, {"time": 2033.322, "image_uri": "frame_4008.png"}, {"time": 2033.829, "image_uri": "frame_4009.png"}, {"time": 2034.337, "image_uri": "frame_4010.png"}, {"time": 2034.844, "image_uri": "frame_4011.png"}, {"time": 2035.352, "image_uri": "frame_4012.png"}, {"time": 2035.859, "image_uri": "frame_4013.png"}, {"time": 2036.367, "image_uri": "frame_4014.png"}, {"time": 2036.874, "image_uri": "frame_4015.png"}, {"time": 2037.382, "image_uri": "frame_4016.png"}, {"time": 2037.889, "image_uri": "frame_4017.png"}, {"time": 2038.396, "image_uri": "frame_4018.png"}, {"time": 2038.904, "image_uri": "frame_4019.png"}, {"time": 2039.411, "image_uri": "frame_4020.png"}, {"time": 2039.919, "image_uri": "frame_4021.png"}, {"time": 2040.426, "image_uri": "frame_4022.png"}, {"time": 2040.934, "image_uri": "frame_4023.png"}, {"time": 2041.441, "image_uri": "frame_4024.png"}, {"time": 2041.949, "image_uri": "frame_4025.png"}, {"time": 2042.456, "image_uri": "frame_4026.png"}, {"time": 2042.963, "image_uri": "frame_4027.png"}, {"time": 2043.471, "image_uri": "frame_4028.png"}, {"time": 2043.978, "image_uri": "frame_4029.png"}, {"time": 2044.486, "image_uri": "frame_4030.png"}, {"time": 2044.993, "image_uri": "frame_4031.png"}, {"time": 2045.501, "image_uri": "frame_4032.png"}, {"time": 2046.008, "image_uri": "frame_4033.png"}, {"time": 2046.516, "image_uri": "frame_4034.png"}, {"time": 2047.023, "image_uri": "frame_4035.png"}, {"time": 2047.53, "image_uri": "frame_4036.png"}, {"time": 2048.038, "image_uri": "frame_4037.png"}, {"time": 2048.545, "image_uri": "frame_4038.png"}, {"time": 2049.053, "image_uri": "frame_4039.png"}, {"time": 2049.56, "image_uri": "frame_4040.png"}, {"time": 2050.068, "image_uri": "frame_4041.png"}, {"time": 2050.575, "image_uri": "frame_4042.png"}, {"time": 2051.083, "image_uri": "frame_4043.png"}, {"time": 2051.59, "image_uri": "frame_4044.png"}, {"time": 2052.097, "image_uri": "frame_4045.png"}, {"time": 2052.605, "image_uri": "frame_4046.png"}, {"time": 2053.112, "image_uri": "frame_4047.png"}, {"time": 2053.62, "image_uri": "frame_4048.png"}, {"time": 2054.127, "image_uri": "frame_4049.png"}, {"time": 2054.635, "image_uri": "frame_4050.png"}, {"time": 2055.142, "image_uri": "frame_4051.png"}, {"time": 2055.65, "image_uri": "frame_4052.png"}, {"time": 2056.157, "image_uri": "frame_4053.png"}, {"time": 2056.664, "image_uri": "frame_4054.png"}, {"time": 2057.172, "image_uri": "frame_4055.png"}, {"time": 2057.679, "image_uri": "frame_4056.png"}, {"time": 2058.187, "image_uri": "frame_4057.png"}, {"time": 2058.694, "image_uri": "frame_4058.png"}, {"time": 2059.202, "image_uri": "frame_4059.png"}, {"time": 2059.709, "image_uri": "frame_4060.png"}, {"time": 2060.217, "image_uri": "frame_4061.png"}, {"time": 2060.724, "image_uri": "frame_4062.png"}, {"time": 2061.231, "image_uri": "frame_4063.png"}, {"time": 2061.739, "image_uri": "frame_4064.png"}, {"time": 2062.246, "image_uri": "frame_4065.png"}, {"time": 2062.754, "image_uri": "frame_4066.png"}, {"time": 2063.261, "image_uri": "frame_4067.png"}, {"time": 2063.769, "image_uri": "frame_4068.png"}, {"time": 2064.276, "image_uri": "frame_4069.png"}, {"time": 2064.783, "image_uri": "frame_4070.png"}, {"time": 2065.291, "image_uri": "frame_4071.png"}, {"time": 2065.798, "image_uri": "frame_4072.png"}, {"time": 2066.306, "image_uri": "frame_4073.png"}, {"time": 2066.813, "image_uri": "frame_4074.png"}, {"time": 2067.321, "image_uri": "frame_4075.png"}, {"time": 2067.828, "image_uri": "frame_4076.png"}, {"time": 2068.336, "image_uri": "frame_4077.png"}, {"time": 2068.843, "image_uri": "frame_4078.png"}, {"time": 2069.35, "image_uri": "frame_4079.png"}, {"time": 2069.858, "image_uri": "frame_4080.png"}, {"time": 2070.365, "image_uri": "frame_4081.png"}, {"time": 2070.873, "image_uri": "frame_4082.png"}, {"time": 2071.38, "image_uri": "frame_4083.png"}, {"time": 2071.888, "image_uri": "frame_4084.png"}, {"time": 2072.395, "image_uri": "frame_4085.png"}, {"time": 2072.903, "image_uri": "frame_4086.png"}, {"time": 2073.41, "image_uri": "frame_4087.png"}, {"time": 2073.917, "image_uri": "frame_4088.png"}, {"time": 2074.425, "image_uri": "frame_4089.png"}, {"time": 2074.932, "image_uri": "frame_4090.png"}, {"time": 2075.44, "image_uri": "frame_4091.png"}, {"time": 2075.947, "image_uri": "frame_4092.png"}, {"time": 2076.455, "image_uri": "frame_4093.png"}, {"time": 2076.962, "image_uri": "frame_4094.png"}, {"time": 2077.47, "image_uri": "frame_4095.png"}, {"time": 2077.977, "image_uri": "frame_4096.png"}, {"time": 2078.484, "image_uri": "frame_4097.png"}, {"time": 2078.992, "image_uri": "frame_4098.png"}, {"time": 2079.499, "image_uri": "frame_4099.png"}, {"time": 2080.007, "image_uri": "frame_4100.png"}, {"time": 2080.514, "image_uri": "frame_4101.png"}, {"time": 2081.022, "image_uri": "frame_4102.png"}, {"time": 2081.529, "image_uri": "frame_4103.png"}, {"time": 2082.037, "image_uri": "frame_4104.png"}, {"time": 2082.544, "image_uri": "frame_4105.png"}, {"time": 2083.051, "image_uri": "frame_4106.png"}, {"time": 2083.559, "image_uri": "frame_4107.png"}, {"time": 2084.066, "image_uri": "frame_4108.png"}, {"time": 2084.574, "image_uri": "frame_4109.png"}, {"time": 2085.081, "image_uri": "frame_4110.png"}, {"time": 2085.589, "image_uri": "frame_4111.png"}, {"time": 2086.096, "image_uri": "frame_4112.png"}, {"time": 2086.604, "image_uri": "frame_4113.png"}, {"time": 2087.111, "image_uri": "frame_4114.png"}, {"time": 2087.618, "image_uri": "frame_4115.png"}, {"time": 2088.126, "image_uri": "frame_4116.png"}, {"time": 2088.633, "image_uri": "frame_4117.png"}, {"time": 2089.141, "image_uri": "frame_4118.png"}, {"time": 2089.648, "image_uri": "frame_4119.png"}, {"time": 2090.156, "image_uri": "frame_4120.png"}, {"time": 2090.663, "image_uri": "frame_4121.png"}, {"time": 2091.171, "image_uri": "frame_4122.png"}, {"time": 2091.678, "image_uri": "frame_4123.png"}, {"time": 2092.185, "image_uri": "frame_4124.png"}, {"time": 2092.693, "image_uri": "frame_4125.png"}, {"time": 2093.2, "image_uri": "frame_4126.png"}, {"time": 2093.708, "image_uri": "frame_4127.png"}, {"time": 2094.215, "image_uri": "frame_4128.png"}, {"time": 2094.723, "image_uri": "frame_4129.png"}, {"time": 2095.23, "image_uri": "frame_4130.png"}, {"time": 2095.737, "image_uri": "frame_4131.png"}, {"time": 2096.245, "image_uri": "frame_4132.png"}, {"time": 2096.752, "image_uri": "frame_4133.png"}, {"time": 2097.26, "image_uri": "frame_4134.png"}, {"time": 2097.767, "image_uri": "frame_4135.png"}, {"time": 2098.275, "image_uri": "frame_4136.png"}, {"time": 2098.782, "image_uri": "frame_4137.png"}, {"time": 2099.29, "image_uri": "frame_4138.png"}, {"time": 2099.797, "image_uri": "frame_4139.png"}, {"time": 2100.304, "image_uri": "frame_4140.png"}, {"time": 2100.812, "image_uri": "frame_4141.png"}, {"time": 2101.319, "image_uri": "frame_4142.png"}, {"time": 2101.827, "image_uri": "frame_4143.png"}, {"time": 2102.334, "image_uri": "frame_4144.png"}, {"time": 2102.842, "image_uri": "frame_4145.png"}, {"time": 2103.349, "image_uri": "frame_4146.png"}, {"time": 2103.857, "image_uri": "frame_4147.png"}, {"time": 2104.364, "image_uri": "frame_4148.png"}, {"time": 2104.871, "image_uri": "frame_4149.png"}, {"time": 2105.379, "image_uri": "frame_4150.png"}, {"time": 2105.886, "image_uri": "frame_4151.png"}, {"time": 2106.394, "image_uri": "frame_4152.png"}, {"time": 2106.901, "image_uri": "frame_4153.png"}, {"time": 2107.409, "image_uri": "frame_4154.png"}, {"time": 2107.916, "image_uri": "frame_4155.png"}, {"time": 2108.424, "image_uri": "frame_4156.png"}, {"time": 2108.931, "image_uri": "frame_4157.png"}, {"time": 2109.438, "image_uri": "frame_4158.png"}, {"time": 2109.946, "image_uri": "frame_4159.png"}, {"time": 2110.453, "image_uri": "frame_4160.png"}, {"time": 2110.961, "image_uri": "frame_4161.png"}, {"time": 2111.468, "image_uri": "frame_4162.png"}, {"time": 2111.976, "image_uri": "frame_4163.png"}, {"time": 2112.483, "image_uri": "frame_4164.png"}, {"time": 2112.991, "image_uri": "frame_4165.png"}, {"time": 2113.498, "image_uri": "frame_4166.png"}, {"time": 2114.005, "image_uri": "frame_4167.png"}, {"time": 2114.513, "image_uri": "frame_4168.png"}, {"time": 2115.02, "image_uri": "frame_4169.png"}, {"time": 2115.528, "image_uri": "frame_4170.png"}, {"time": 2116.035, "image_uri": "frame_4171.png"}, {"time": 2116.543, "image_uri": "frame_4172.png"}, {"time": 2117.05, "image_uri": "frame_4173.png"}, {"time": 2117.558, "image_uri": "frame_4174.png"}, {"time": 2118.065, "image_uri": "frame_4175.png"}, {"time": 2118.572, "image_uri": "frame_4176.png"}, {"time": 2119.08, "image_uri": "frame_4177.png"}, {"time": 2119.587, "image_uri": "frame_4178.png"}, {"time": 2120.095, "image_uri": "frame_4179.png"}, {"time": 2120.602, "image_uri": "frame_4180.png"}, {"time": 2121.11, "image_uri": "frame_4181.png"}, {"time": 2121.617, "image_uri": "frame_4182.png"}, {"time": 2122.124, "image_uri": "frame_4183.png"}, {"time": 2122.632, "image_uri": "frame_4184.png"}, {"time": 2123.139, "image_uri": "frame_4185.png"}, {"time": 2123.647, "image_uri": "frame_4186.png"}, {"time": 2124.154, "image_uri": "frame_4187.png"}, {"time": 2124.662, "image_uri": "frame_4188.png"}, {"time": 2125.169, "image_uri": "frame_4189.png"}, {"time": 2125.677, "image_uri": "frame_4190.png"}, {"time": 2126.184, "image_uri": "frame_4191.png"}, {"time": 2126.691, "image_uri": "frame_4192.png"}, {"time": 2127.199, "image_uri": "frame_4193.png"}, {"time": 2127.706, "image_uri": "frame_4194.png"}, {"time": 2128.214, "image_uri": "frame_4195.png"}, {"time": 2128.721, "image_uri": "frame_4196.png"}, {"time": 2129.229, "image_uri": "frame_4197.png"}, {"time": 2129.736, "image_uri": "frame_4198.png"}, {"time": 2130.244, "image_uri": "frame_4199.png"}, {"time": 2130.751, "image_uri": "frame_4200.png"}, {"time": 2131.258, "image_uri": "frame_4201.png"}, {"time": 2131.766, "image_uri": "frame_4202.png"}, {"time": 2132.273, "image_uri": "frame_4203.png"}, {"time": 2132.781, "image_uri": "frame_4204.png"}, {"time": 2133.288, "image_uri": "frame_4205.png"}, {"time": 2133.796, "image_uri": "frame_4206.png"}, {"time": 2134.303, "image_uri": "frame_4207.png"}, {"time": 2134.811, "image_uri": "frame_4208.png"}, {"time": 2135.318, "image_uri": "frame_4209.png"}, {"time": 2135.825, "image_uri": "frame_4210.png"}, {"time": 2136.333, "image_uri": "frame_4211.png"}, {"time": 2136.84, "image_uri": "frame_4212.png"}, {"time": 2137.348, "image_uri": "frame_4213.png"}, {"time": 2137.855, "image_uri": "frame_4214.png"}, {"time": 2138.363, "image_uri": "frame_4215.png"}, {"time": 2138.87, "image_uri": "frame_4216.png"}, {"time": 2139.378, "image_uri": "frame_4217.png"}, {"time": 2139.885, "image_uri": "frame_4218.png"}, {"time": 2140.392, "image_uri": "frame_4219.png"}, {"time": 2140.9, "image_uri": "frame_4220.png"}, {"time": 2141.407, "image_uri": "frame_4221.png"}, {"time": 2141.915, "image_uri": "frame_4222.png"}, {"time": 2142.422, "image_uri": "frame_4223.png"}, {"time": 2142.93, "image_uri": "frame_4224.png"}, {"time": 2143.437, "image_uri": "frame_4225.png"}, {"time": 2143.945, "image_uri": "frame_4226.png"}, {"time": 2144.452, "image_uri": "frame_4227.png"}, {"time": 2144.959, "image_uri": "frame_4228.png"}, {"time": 2145.467, "image_uri": "frame_4229.png"}, {"time": 2145.974, "image_uri": "frame_4230.png"}, {"time": 2146.482, "image_uri": "frame_4231.png"}, {"time": 2146.989, "image_uri": "frame_4232.png"}, {"time": 2147.497, "image_uri": "frame_4233.png"}, {"time": 2148.004, "image_uri": "frame_4234.png"}, {"time": 2148.512, "image_uri": "frame_4235.png"}, {"time": 2149.019, "image_uri": "frame_4236.png"}, {"time": 2149.526, "image_uri": "frame_4237.png"}, {"time": 2150.034, "image_uri": "frame_4238.png"}, {"time": 2150.541, "image_uri": "frame_4239.png"}, {"time": 2151.049, "image_uri": "frame_4240.png"}, {"time": 2151.556, "image_uri": "frame_4241.png"}, {"time": 2152.064, "image_uri": "frame_4242.png"}, {"time": 2152.571, "image_uri": "frame_4243.png"}, {"time": 2153.078, "image_uri": "frame_4244.png"}, {"time": 2153.586, "image_uri": "frame_4245.png"}, {"time": 2154.093, "image_uri": "frame_4246.png"}, {"time": 2154.601, "image_uri": "frame_4247.png"}, {"time": 2155.108, "image_uri": "frame_4248.png"}, {"time": 2155.616, "image_uri": "frame_4249.png"}, {"time": 2156.123, "image_uri": "frame_4250.png"}, {"time": 2156.631, "image_uri": "frame_4251.png"}, {"time": 2157.138, "image_uri": "frame_4252.png"}, {"time": 2157.645, "image_uri": "frame_4253.png"}, {"time": 2158.153, "image_uri": "frame_4254.png"}, {"time": 2158.66, "image_uri": "frame_4255.png"}, {"time": 2159.168, "image_uri": "frame_4256.png"}, {"time": 2159.675, "image_uri": "frame_4257.png"}, {"time": 2160.183, "image_uri": "frame_4258.png"}, {"time": 2160.69, "image_uri": "frame_4259.png"}, {"time": 2161.198, "image_uri": "frame_4260.png"}, {"time": 2161.705, "image_uri": "frame_4261.png"}, {"time": 2162.212, "image_uri": "frame_4262.png"}, {"time": 2162.72, "image_uri": "frame_4263.png"}, {"time": 2163.227, "image_uri": "frame_4264.png"}, {"time": 2163.735, "image_uri": "frame_4265.png"}, {"time": 2164.242, "image_uri": "frame_4266.png"}, {"time": 2164.75, "image_uri": "frame_4267.png"}, {"time": 2165.257, "image_uri": "frame_4268.png"}, {"time": 2165.765, "image_uri": "frame_4269.png"}, {"time": 2166.272, "image_uri": "frame_4270.png"}, {"time": 2166.779, "image_uri": "frame_4271.png"}, {"time": 2167.287, "image_uri": "frame_4272.png"}, {"time": 2167.794, "image_uri": "frame_4273.png"}, {"time": 2168.302, "image_uri": "frame_4274.png"}, {"time": 2168.809, "image_uri": "frame_4275.png"}, {"time": 2169.317, "image_uri": "frame_4276.png"}, {"time": 2169.824, "image_uri": "frame_4277.png"}, {"time": 2170.332, "image_uri": "frame_4278.png"}, {"time": 2170.839, "image_uri": "frame_4279.png"}, {"time": 2171.346, "image_uri": "frame_4280.png"}, {"time": 2171.854, "image_uri": "frame_4281.png"}, {"time": 2172.361, "image_uri": "frame_4282.png"}, {"time": 2172.869, "image_uri": "frame_4283.png"}, {"time": 2173.376, "image_uri": "frame_4284.png"}, {"time": 2173.884, "image_uri": "frame_4285.png"}, {"time": 2174.391, "image_uri": "frame_4286.png"}, {"time": 2174.899, "image_uri": "frame_4287.png"}, {"time": 2175.406, "image_uri": "frame_4288.png"}, {"time": 2175.913, "image_uri": "frame_4289.png"}, {"time": 2176.421, "image_uri": "frame_4290.png"}, {"time": 2176.928, "image_uri": "frame_4291.png"}, {"time": 2177.436, "image_uri": "frame_4292.png"}, {"time": 2177.943, "image_uri": "frame_4293.png"}, {"time": 2178.451, "image_uri": "frame_4294.png"}, {"time": 2178.958, "image_uri": "frame_4295.png"}, {"time": 2179.465, "image_uri": "frame_4296.png"}, {"time": 2179.973, "image_uri": "frame_4297.png"}, {"time": 2180.48, "image_uri": "frame_4298.png"}, {"time": 2180.988, "image_uri": "frame_4299.png"}, {"time": 2181.495, "image_uri": "frame_4300.png"}, {"time": 2182.003, "image_uri": "frame_4301.png"}, {"time": 2182.51, "image_uri": "frame_4302.png"}, {"time": 2183.018, "image_uri": "frame_4303.png"}, {"time": 2183.525, "image_uri": "frame_4304.png"}, {"time": 2184.032, "image_uri": "frame_4305.png"}, {"time": 2184.54, "image_uri": "frame_4306.png"}, {"time": 2185.047, "image_uri": "frame_4307.png"}, {"time": 2185.555, "image_uri": "frame_4308.png"}, {"time": 2186.062, "image_uri": "frame_4309.png"}, {"time": 2186.57, "image_uri": "frame_4310.png"}, {"time": 2187.077, "image_uri": "frame_4311.png"}, {"time": 2187.585, "image_uri": "frame_4312.png"}, {"time": 2188.092, "image_uri": "frame_4313.png"}, {"time": 2188.599, "image_uri": "frame_4314.png"}, {"time": 2189.107, "image_uri": "frame_4315.png"}, {"time": 2189.614, "image_uri": "frame_4316.png"}, {"time": 2190.122, "image_uri": "frame_4317.png"}, {"time": 2190.629, "image_uri": "frame_4318.png"}, {"time": 2191.137, "image_uri": "frame_4319.png"}, {"time": 2191.644, "image_uri": "frame_4320.png"}, {"time": 2192.152, "image_uri": "frame_4321.png"}, {"time": 2192.659, "image_uri": "frame_4322.png"}, {"time": 2193.166, "image_uri": "frame_4323.png"}, {"time": 2193.674, "image_uri": "frame_4324.png"}, {"time": 2194.181, "image_uri": "frame_4325.png"}, {"time": 2194.689, "image_uri": "frame_4326.png"}, {"time": 2195.196, "image_uri": "frame_4327.png"}, {"time": 2195.704, "image_uri": "frame_4328.png"}, {"time": 2196.211, "image_uri": "frame_4329.png"}, {"time": 2196.719, "image_uri": "frame_4330.png"}, {"time": 2197.226, "image_uri": "frame_4331.png"}, {"time": 2197.733, "image_uri": "frame_4332.png"}, {"time": 2198.241, "image_uri": "frame_4333.png"}, {"time": 2198.748, "image_uri": "frame_4334.png"}, {"time": 2199.256, "image_uri": "frame_4335.png"}, {"time": 2199.763, "image_uri": "frame_4336.png"}, {"time": 2200.271, "image_uri": "frame_4337.png"}, {"time": 2200.778, "image_uri": "frame_4338.png"}, {"time": 2201.286, "image_uri": "frame_4339.png"}, {"time": 2201.793, "image_uri": "frame_4340.png"}, {"time": 2202.3, "image_uri": "frame_4341.png"}, {"time": 2202.808, "image_uri": "frame_4342.png"}, {"time": 2203.315, "image_uri": "frame_4343.png"}, {"time": 2203.823, "image_uri": "frame_4344.png"}, {"time": 2204.33, "image_uri": "frame_4345.png"}, {"time": 2204.838, "image_uri": "frame_4346.png"}, {"time": 2205.345, "image_uri": "frame_4347.png"}, {"time": 2205.853, "image_uri": "frame_4348.png"}, {"time": 2206.36, "image_uri": "frame_4349.png"}, {"time": 2206.867, "image_uri": "frame_4350.png"}, {"time": 2207.375, "image_uri": "frame_4351.png"}, {"time": 2207.882, "image_uri": "frame_4352.png"}, {"time": 2208.39, "image_uri": "frame_4353.png"}, {"time": 2208.897, "image_uri": "frame_4354.png"}, {"time": 2209.405, "image_uri": "frame_4355.png"}, {"time": 2209.912, "image_uri": "frame_4356.png"}, {"time": 2210.419, "image_uri": "frame_4357.png"}, {"time": 2210.927, "image_uri": "frame_4358.png"}, {"time": 2211.434, "image_uri": "frame_4359.png"}, {"time": 2211.942, "image_uri": "frame_4360.png"}, {"time": 2212.449, "image_uri": "frame_4361.png"}, {"time": 2212.957, "image_uri": "frame_4362.png"}, {"time": 2213.464, "image_uri": "frame_4363.png"}, {"time": 2213.972, "image_uri": "frame_4364.png"}, {"time": 2214.479, "image_uri": "frame_4365.png"}, {"time": 2214.986, "image_uri": "frame_4366.png"}, {"time": 2215.494, "image_uri": "frame_4367.png"}, {"time": 2216.001, "image_uri": "frame_4368.png"}, {"time": 2216.509, "image_uri": "frame_4369.png"}, {"time": 2217.016, "image_uri": "frame_4370.png"}, {"time": 2217.524, "image_uri": "frame_4371.png"}, {"time": 2218.031, "image_uri": "frame_4372.png"}, {"time": 2218.539, "image_uri": "frame_4373.png"}, {"time": 2219.046, "image_uri": "frame_4374.png"}, {"time": 2219.553, "image_uri": "frame_4375.png"}, {"time": 2220.061, "image_uri": "frame_4376.png"}, {"time": 2220.568, "image_uri": "frame_4377.png"}, {"time": 2221.076, "image_uri": "frame_4378.png"}, {"time": 2221.583, "image_uri": "frame_4379.png"}, {"time": 2222.091, "image_uri": "frame_4380.png"}, {"time": 2222.598, "image_uri": "frame_4381.png"}, {"time": 2223.106, "image_uri": "frame_4382.png"}, {"time": 2223.613, "image_uri": "frame_4383.png"}, {"time": 2224.12, "image_uri": "frame_4384.png"}, {"time": 2224.628, "image_uri": "frame_4385.png"}, {"time": 2225.135, "image_uri": "frame_4386.png"}, {"time": 2225.643, "image_uri": "frame_4387.png"}, {"time": 2226.15, "image_uri": "frame_4388.png"}, {"time": 2226.658, "image_uri": "frame_4389.png"}, {"time": 2227.165, "image_uri": "frame_4390.png"}, {"time": 2227.673, "image_uri": "frame_4391.png"}, {"time": 2228.18, "image_uri": "frame_4392.png"}, {"time": 2228.687, "image_uri": "frame_4393.png"}, {"time": 2229.195, "image_uri": "frame_4394.png"}, {"time": 2229.702, "image_uri": "frame_4395.png"}, {"time": 2230.21, "image_uri": "frame_4396.png"}, {"time": 2230.717, "image_uri": "frame_4397.png"}, {"time": 2231.225, "image_uri": "frame_4398.png"}, {"time": 2231.732, "image_uri": "frame_4399.png"}, {"time": 2232.24, "image_uri": "frame_4400.png"}, {"time": 2232.747, "image_uri": "frame_4401.png"}, {"time": 2233.254, "image_uri": "frame_4402.png"}, {"time": 2233.762, "image_uri": "frame_4403.png"}, {"time": 2234.269, "image_uri": "frame_4404.png"}, {"time": 2234.777, "image_uri": "frame_4405.png"}, {"time": 2235.284, "image_uri": "frame_4406.png"}, {"time": 2235.792, "image_uri": "frame_4407.png"}, {"time": 2236.299, "image_uri": "frame_4408.png"}, {"time": 2236.806, "image_uri": "frame_4409.png"}, {"time": 2237.314, "image_uri": "frame_4410.png"}, {"time": 2237.821, "image_uri": "frame_4411.png"}, {"time": 2238.329, "image_uri": "frame_4412.png"}, {"time": 2238.836, "image_uri": "frame_4413.png"}, {"time": 2239.344, "image_uri": "frame_4414.png"}, {"time": 2239.851, "image_uri": "frame_4415.png"}, {"time": 2240.359, "image_uri": "frame_4416.png"}, {"time": 2240.866, "image_uri": "frame_4417.png"}, {"time": 2241.373, "image_uri": "frame_4418.png"}, {"time": 2241.881, "image_uri": "frame_4419.png"}, {"time": 2242.388, "image_uri": "frame_4420.png"}, {"time": 2242.896, "image_uri": "frame_4421.png"}, {"time": 2243.403, "image_uri": "frame_4422.png"}, {"time": 2243.911, "image_uri": "frame_4423.png"}, {"time": 2244.418, "image_uri": "frame_4424.png"}, {"time": 2244.926, "image_uri": "frame_4425.png"}, {"time": 2245.433, "image_uri": "frame_4426.png"}, {"time": 2245.94, "image_uri": "frame_4427.png"}, {"time": 2246.448, "image_uri": "frame_4428.png"}, {"time": 2246.955, "image_uri": "frame_4429.png"}, {"time": 2247.463, "image_uri": "frame_4430.png"}, {"time": 2247.97, "image_uri": "frame_4431.png"}, {"time": 2248.478, "image_uri": "frame_4432.png"}, {"time": 2248.985, "image_uri": "frame_4433.png"}, {"time": 2249.493, "image_uri": "frame_4434.png"}, {"time": 2250.0, "image_uri": "frame_4435.png"}, {"time": 2250.507, "image_uri": "frame_4436.png"}, {"time": 2251.015, "image_uri": "frame_4437.png"}, {"time": 2251.522, "image_uri": "frame_4438.png"}, {"time": 2252.03, "image_uri": "frame_4439.png"}, {"time": 2252.537, "image_uri": "frame_4440.png"}, {"time": 2253.045, "image_uri": "frame_4441.png"}, {"time": 2253.552, "image_uri": "frame_4442.png"}, {"time": 2254.06, "image_uri": "frame_4443.png"}, {"time": 2254.567, "image_uri": "frame_4444.png"}, {"time": 2255.074, "image_uri": "frame_4445.png"}, {"time": 2255.582, "image_uri": "frame_4446.png"}, {"time": 2256.089, "image_uri": "frame_4447.png"}, {"time": 2256.597, "image_uri": "frame_4448.png"}, {"time": 2257.104, "image_uri": "frame_4449.png"}, {"time": 2257.612, "image_uri": "frame_4450.png"}, {"time": 2258.119, "image_uri": "frame_4451.png"}, {"time": 2258.627, "image_uri": "frame_4452.png"}, {"time": 2259.134, "image_uri": "frame_4453.png"}, {"time": 2259.641, "image_uri": "frame_4454.png"}, {"time": 2260.149, "image_uri": "frame_4455.png"}, {"time": 2260.656, "image_uri": "frame_4456.png"}, {"time": 2261.164, "image_uri": "frame_4457.png"}, {"time": 2261.671, "image_uri": "frame_4458.png"}, {"time": 2262.179, "image_uri": "frame_4459.png"}, {"time": 2262.686, "image_uri": "frame_4460.png"}, {"time": 2263.194, "image_uri": "frame_4461.png"}, {"time": 2263.701, "image_uri": "frame_4462.png"}, {"time": 2264.208, "image_uri": "frame_4463.png"}, {"time": 2264.716, "image_uri": "frame_4464.png"}, {"time": 2265.223, "image_uri": "frame_4465.png"}, {"time": 2265.731, "image_uri": "frame_4466.png"}, {"time": 2266.238, "image_uri": "frame_4467.png"}, {"time": 2266.746, "image_uri": "frame_4468.png"}, {"time": 2267.253, "image_uri": "frame_4469.png"}, {"time": 2267.76, "image_uri": "frame_4470.png"}, {"time": 2268.268, "image_uri": "frame_4471.png"}, {"time": 2268.775, "image_uri": "frame_4472.png"}, {"time": 2269.283, "image_uri": "frame_4473.png"}, {"time": 2269.79, "image_uri": "frame_4474.png"}, {"time": 2270.298, "image_uri": "frame_4475.png"}, {"time": 2270.805, "image_uri": "frame_4476.png"}, {"time": 2271.313, "image_uri": "frame_4477.png"}, {"time": 2271.82, "image_uri": "frame_4478.png"}, {"time": 2272.327, "image_uri": "frame_4479.png"}, {"time": 2272.835, "image_uri": "frame_4480.png"}, {"time": 2273.342, "image_uri": "frame_4481.png"}, {"time": 2273.85, "image_uri": "frame_4482.png"}, {"time": 2274.357, "image_uri": "frame_4483.png"}, {"time": 2274.865, "image_uri": "frame_4484.png"}, {"time": 2275.372, "image_uri": "frame_4485.png"}, {"time": 2275.88, "image_uri": "frame_4486.png"}, {"time": 2276.387, "image_uri": "frame_4487.png"}, {"time": 2276.894, "image_uri": "frame_4488.png"}, {"time": 2277.402, "image_uri": "frame_4489.png"}, {"time": 2277.909, "image_uri": "frame_4490.png"}, {"time": 2278.417, "image_uri": "frame_4491.png"}, {"time": 2278.924, "image_uri": "frame_4492.png"}, {"time": 2279.432, "image_uri": "frame_4493.png"}, {"time": 2279.939, "image_uri": "frame_4494.png"}, {"time": 2280.447, "image_uri": "frame_4495.png"}, {"time": 2280.954, "image_uri": "frame_4496.png"}, {"time": 2281.461, "image_uri": "frame_4497.png"}, {"time": 2281.969, "image_uri": "frame_4498.png"}, {"time": 2282.476, "image_uri": "frame_4499.png"}, {"time": 2282.984, "image_uri": "frame_4500.png"}, {"time": 2283.491, "image_uri": "frame_4501.png"}, {"time": 2283.999, "image_uri": "frame_4502.png"}, {"time": 2284.506, "image_uri": "frame_4503.png"}, {"time": 2285.014, "image_uri": "frame_4504.png"}, {"time": 2285.521, "image_uri": "frame_4505.png"}, {"time": 2286.028, "image_uri": "frame_4506.png"}, {"time": 2286.536, "image_uri": "frame_4507.png"}, {"time": 2287.043, "image_uri": "frame_4508.png"}, {"time": 2287.551, "image_uri": "frame_4509.png"}, {"time": 2288.058, "image_uri": "frame_4510.png"}, {"time": 2288.566, "image_uri": "frame_4511.png"}, {"time": 2289.073, "image_uri": "frame_4512.png"}, {"time": 2289.581, "image_uri": "frame_4513.png"}, {"time": 2290.088, "image_uri": "frame_4514.png"}, {"time": 2290.595, "image_uri": "frame_4515.png"}, {"time": 2291.103, "image_uri": "frame_4516.png"}, {"time": 2291.61, "image_uri": "frame_4517.png"}, {"time": 2292.118, "image_uri": "frame_4518.png"}, {"time": 2292.625, "image_uri": "frame_4519.png"}, {"time": 2293.133, "image_uri": "frame_4520.png"}, {"time": 2293.64, "image_uri": "frame_4521.png"}, {"time": 2294.147, "image_uri": "frame_4522.png"}, {"time": 2294.655, "image_uri": "frame_4523.png"}, {"time": 2295.162, "image_uri": "frame_4524.png"}, {"time": 2295.67, "image_uri": "frame_4525.png"}, {"time": 2296.177, "image_uri": "frame_4526.png"}, {"time": 2296.685, "image_uri": "frame_4527.png"}, {"time": 2297.192, "image_uri": "frame_4528.png"}, {"time": 2297.7, "image_uri": "frame_4529.png"}, {"time": 2298.207, "image_uri": "frame_4530.png"}, {"time": 2298.714, "image_uri": "frame_4531.png"}, {"time": 2299.222, "image_uri": "frame_4532.png"}, {"time": 2299.729, "image_uri": "frame_4533.png"}, {"time": 2300.237, "image_uri": "frame_4534.png"}, {"time": 2300.744, "image_uri": "frame_4535.png"}, {"time": 2301.252, "image_uri": "frame_4536.png"}, {"time": 2301.759, "image_uri": "frame_4537.png"}, {"time": 2302.267, "image_uri": "frame_4538.png"}, {"time": 2302.774, "image_uri": "frame_4539.png"}, {"time": 2303.281, "image_uri": "frame_4540.png"}, {"time": 2303.789, "image_uri": "frame_4541.png"}, {"time": 2304.296, "image_uri": "frame_4542.png"}, {"time": 2304.804, "image_uri": "frame_4543.png"}, {"time": 2305.311, "image_uri": "frame_4544.png"}, {"time": 2305.819, "image_uri": "frame_4545.png"}, {"time": 2306.326, "image_uri": "frame_4546.png"}, {"time": 2306.834, "image_uri": "frame_4547.png"}, {"time": 2307.341, "image_uri": "frame_4548.png"}, {"time": 2307.848, "image_uri": "frame_4549.png"}, {"time": 2308.356, "image_uri": "frame_4550.png"}, {"time": 2308.863, "image_uri": "frame_4551.png"}, {"time": 2309.371, "image_uri": "frame_4552.png"}, {"time": 2309.878, "image_uri": "frame_4553.png"}, {"time": 2310.386, "image_uri": "frame_4554.png"}, {"time": 2310.893, "image_uri": "frame_4555.png"}, {"time": 2311.401, "image_uri": "frame_4556.png"}, {"time": 2311.908, "image_uri": "frame_4557.png"}, {"time": 2312.415, "image_uri": "frame_4558.png"}, {"time": 2312.923, "image_uri": "frame_4559.png"}, {"time": 2313.43, "image_uri": "frame_4560.png"}, {"time": 2313.938, "image_uri": "frame_4561.png"}, {"time": 2314.445, "image_uri": "frame_4562.png"}, {"time": 2314.953, "image_uri": "frame_4563.png"}, {"time": 2315.46, "image_uri": "frame_4564.png"}, {"time": 2315.968, "image_uri": "frame_4565.png"}, {"time": 2316.475, "image_uri": "frame_4566.png"}, {"time": 2316.982, "image_uri": "frame_4567.png"}, {"time": 2317.49, "image_uri": "frame_4568.png"}, {"time": 2317.997, "image_uri": "frame_4569.png"}, {"time": 2318.505, "image_uri": "frame_4570.png"}, {"time": 2319.012, "image_uri": "frame_4571.png"}, {"time": 2319.52, "image_uri": "frame_4572.png"}, {"time": 2320.027, "image_uri": "frame_4573.png"}, {"time": 2320.535, "image_uri": "frame_4574.png"}, {"time": 2321.042, "image_uri": "frame_4575.png"}, {"time": 2321.549, "image_uri": "frame_4576.png"}, {"time": 2322.057, "image_uri": "frame_4577.png"}, {"time": 2322.564, "image_uri": "frame_4578.png"}, {"time": 2323.072, "image_uri": "frame_4579.png"}, {"time": 2323.579, "image_uri": "frame_4580.png"}, {"time": 2324.087, "image_uri": "frame_4581.png"}, {"time": 2324.594, "image_uri": "frame_4582.png"}, {"time": 2325.101, "image_uri": "frame_4583.png"}, {"time": 2325.609, "image_uri": "frame_4584.png"}, {"time": 2326.116, "image_uri": "frame_4585.png"}, {"time": 2326.624, "image_uri": "frame_4586.png"}, {"time": 2327.131, "image_uri": "frame_4587.png"}, {"time": 2327.639, "image_uri": "frame_4588.png"}, {"time": 2328.146, "image_uri": "frame_4589.png"}, {"time": 2328.654, "image_uri": "frame_4590.png"}, {"time": 2329.161, "image_uri": "frame_4591.png"}, {"time": 2329.668, "image_uri": "frame_4592.png"}, {"time": 2330.176, "image_uri": "frame_4593.png"}, {"time": 2330.683, "image_uri": "frame_4594.png"}, {"time": 2331.191, "image_uri": "frame_4595.png"}, {"time": 2331.698, "image_uri": "frame_4596.png"}, {"time": 2332.206, "image_uri": "frame_4597.png"}, {"time": 2332.713, "image_uri": "frame_4598.png"}, {"time": 2333.221, "image_uri": "frame_4599.png"}, {"time": 2333.728, "image_uri": "frame_4600.png"}, {"time": 2334.235, "image_uri": "frame_4601.png"}, {"time": 2334.743, "image_uri": "frame_4602.png"}, {"time": 2335.25, "image_uri": "frame_4603.png"}, {"time": 2335.758, "image_uri": "frame_4604.png"}, {"time": 2336.265, "image_uri": "frame_4605.png"}, {"time": 2336.773, "image_uri": "frame_4606.png"}, {"time": 2337.28, "image_uri": "frame_4607.png"}, {"time": 2337.788, "image_uri": "frame_4608.png"}, {"time": 2338.295, "image_uri": "frame_4609.png"}, {"time": 2338.802, "image_uri": "frame_4610.png"}, {"time": 2339.31, "image_uri": "frame_4611.png"}, {"time": 2339.817, "image_uri": "frame_4612.png"}, {"time": 2340.325, "image_uri": "frame_4613.png"}, {"time": 2340.832, "image_uri": "frame_4614.png"}, {"time": 2341.34, "image_uri": "frame_4615.png"}, {"time": 2341.847, "image_uri": "frame_4616.png"}, {"time": 2342.355, "image_uri": "frame_4617.png"}, {"time": 2342.862, "image_uri": "frame_4618.png"}, {"time": 2343.369, "image_uri": "frame_4619.png"}, {"time": 2343.877, "image_uri": "frame_4620.png"}, {"time": 2344.384, "image_uri": "frame_4621.png"}, {"time": 2344.892, "image_uri": "frame_4622.png"}, {"time": 2345.399, "image_uri": "frame_4623.png"}, {"time": 2345.907, "image_uri": "frame_4624.png"}, {"time": 2346.414, "image_uri": "frame_4625.png"}, {"time": 2346.922, "image_uri": "frame_4626.png"}, {"time": 2347.429, "image_uri": "frame_4627.png"}, {"time": 2347.936, "image_uri": "frame_4628.png"}, {"time": 2348.444, "image_uri": "frame_4629.png"}, {"time": 2348.951, "image_uri": "frame_4630.png"}, {"time": 2349.459, "image_uri": "frame_4631.png"}, {"time": 2349.966, "image_uri": "frame_4632.png"}, {"time": 2350.474, "image_uri": "frame_4633.png"}, {"time": 2350.981, "image_uri": "frame_4634.png"}, {"time": 2351.488, "image_uri": "frame_4635.png"}, {"time": 2351.996, "image_uri": "frame_4636.png"}, {"time": 2352.503, "image_uri": "frame_4637.png"}, {"time": 2353.011, "image_uri": "frame_4638.png"}, {"time": 2353.518, "image_uri": "frame_4639.png"}, {"time": 2354.026, "image_uri": "frame_4640.png"}, {"time": 2354.533, "image_uri": "frame_4641.png"}, {"time": 2355.041, "image_uri": "frame_4642.png"}, {"time": 2355.548, "image_uri": "frame_4643.png"}, {"time": 2356.055, "image_uri": "frame_4644.png"}, {"time": 2356.563, "image_uri": "frame_4645.png"}, {"time": 2357.07, "image_uri": "frame_4646.png"}, {"time": 2357.578, "image_uri": "frame_4647.png"}, {"time": 2358.085, "image_uri": "frame_4648.png"}, {"time": 2358.593, "image_uri": "frame_4649.png"}, {"time": 2359.1, "image_uri": "frame_4650.png"}, {"time": 2359.608, "image_uri": "frame_4651.png"}, {"time": 2360.115, "image_uri": "frame_4652.png"}, {"time": 2360.622, "image_uri": "frame_4653.png"}, {"time": 2361.13, "image_uri": "frame_4654.png"}, {"time": 2361.637, "image_uri": "frame_4655.png"}, {"time": 2362.145, "image_uri": "frame_4656.png"}, {"time": 2362.652, "image_uri": "frame_4657.png"}, {"time": 2363.16, "image_uri": "frame_4658.png"}, {"time": 2363.667, "image_uri": "frame_4659.png"}, {"time": 2364.175, "image_uri": "frame_4660.png"}, {"time": 2364.682, "image_uri": "frame_4661.png"}, {"time": 2365.189, "image_uri": "frame_4662.png"}, {"time": 2365.697, "image_uri": "frame_4663.png"}, {"time": 2366.204, "image_uri": "frame_4664.png"}, {"time": 2366.712, "image_uri": "frame_4665.png"}, {"time": 2367.219, "image_uri": "frame_4666.png"}, {"time": 2367.727, "image_uri": "frame_4667.png"}, {"time": 2368.234, "image_uri": "frame_4668.png"}, {"time": 2368.742, "image_uri": "frame_4669.png"}, {"time": 2369.249, "image_uri": "frame_4670.png"}, {"time": 2369.756, "image_uri": "frame_4671.png"}, {"time": 2370.264, "image_uri": "frame_4672.png"}, {"time": 2370.771, "image_uri": "frame_4673.png"}, {"time": 2371.279, "image_uri": "frame_4674.png"}, {"time": 2371.786, "image_uri": "frame_4675.png"}, {"time": 2372.294, "image_uri": "frame_4676.png"}, {"time": 2372.801, "image_uri": "frame_4677.png"}, {"time": 2373.309, "image_uri": "frame_4678.png"}, {"time": 2373.816, "image_uri": "frame_4679.png"}, {"time": 2374.323, "image_uri": "frame_4680.png"}, {"time": 2374.831, "image_uri": "frame_4681.png"}, {"time": 2375.338, "image_uri": "frame_4682.png"}, {"time": 2375.846, "image_uri": "frame_4683.png"}, {"time": 2376.353, "image_uri": "frame_4684.png"}, {"time": 2376.861, "image_uri": "frame_4685.png"}, {"time": 2377.368, "image_uri": "frame_4686.png"}, {"time": 2377.876, "image_uri": "frame_4687.png"}, {"time": 2378.383, "image_uri": "frame_4688.png"}, {"time": 2378.89, "image_uri": "frame_4689.png"}, {"time": 2379.398, "image_uri": "frame_4690.png"}, {"time": 2379.905, "image_uri": "frame_4691.png"}, {"time": 2380.413, "image_uri": "frame_4692.png"}, {"time": 2380.92, "image_uri": "frame_4693.png"}, {"time": 2381.428, "image_uri": "frame_4694.png"}, {"time": 2381.935, "image_uri": "frame_4695.png"}, {"time": 2382.442, "image_uri": "frame_4696.png"}, {"time": 2382.95, "image_uri": "frame_4697.png"}, {"time": 2383.457, "image_uri": "frame_4698.png"}, {"time": 2383.965, "image_uri": "frame_4699.png"}, {"time": 2384.472, "image_uri": "frame_4700.png"}, {"time": 2384.98, "image_uri": "frame_4701.png"}, {"time": 2385.487, "image_uri": "frame_4702.png"}, {"time": 2385.995, "image_uri": "frame_4703.png"}, {"time": 2386.502, "image_uri": "frame_4704.png"}, {"time": 2387.009, "image_uri": "frame_4705.png"}, {"time": 2387.517, "image_uri": "frame_4706.png"}, {"time": 2388.024, "image_uri": "frame_4707.png"}, {"time": 2388.532, "image_uri": "frame_4708.png"}, {"time": 2389.039, "image_uri": "frame_4709.png"}, {"time": 2389.547, "image_uri": "frame_4710.png"}, {"time": 2390.054, "image_uri": "frame_4711.png"}, {"time": 2390.562, "image_uri": "frame_4712.png"}, {"time": 2391.069, "image_uri": "frame_4713.png"}, {"time": 2391.576, "image_uri": "frame_4714.png"}, {"time": 2392.084, "image_uri": "frame_4715.png"}, {"time": 2392.591, "image_uri": "frame_4716.png"}, {"time": 2393.099, "image_uri": "frame_4717.png"}, {"time": 2393.606, "image_uri": "frame_4718.png"}, {"time": 2394.114, "image_uri": "frame_4719.png"}, {"time": 2394.621, "image_uri": "frame_4720.png"}, {"time": 2395.129, "image_uri": "frame_4721.png"}, {"time": 2395.636, "image_uri": "frame_4722.png"}, {"time": 2396.143, "image_uri": "frame_4723.png"}, {"time": 2396.651, "image_uri": "frame_4724.png"}, {"time": 2397.158, "image_uri": "frame_4725.png"}, {"time": 2397.666, "image_uri": "frame_4726.png"}, {"time": 2398.173, "image_uri": "frame_4727.png"}, {"time": 2398.681, "image_uri": "frame_4728.png"}, {"time": 2399.188, "image_uri": "frame_4729.png"}, {"time": 2399.696, "image_uri": "frame_4730.png"}, {"time": 2400.203, "image_uri": "frame_4731.png"}, {"time": 2400.71, "image_uri": "frame_4732.png"}, {"time": 2401.218, "image_uri": "frame_4733.png"}, {"time": 2401.725, "image_uri": "frame_4734.png"}, {"time": 2402.233, "image_uri": "frame_4735.png"}, {"time": 2402.74, "image_uri": "frame_4736.png"}, {"time": 2403.248, "image_uri": "frame_4737.png"}, {"time": 2403.755, "image_uri": "frame_4738.png"}, {"time": 2404.263, "image_uri": "frame_4739.png"}, {"time": 2404.77, "image_uri": "frame_4740.png"}, {"time": 2405.277, "image_uri": "frame_4741.png"}, {"time": 2405.785, "image_uri": "frame_4742.png"}, {"time": 2406.292, "image_uri": "frame_4743.png"}, {"time": 2406.8, "image_uri": "frame_4744.png"}, {"time": 2407.307, "image_uri": "frame_4745.png"}, {"time": 2407.815, "image_uri": "frame_4746.png"}, {"time": 2408.322, "image_uri": "frame_4747.png"}, {"time": 2408.829, "image_uri": "frame_4748.png"}, {"time": 2409.337, "image_uri": "frame_4749.png"}, {"time": 2409.844, "image_uri": "frame_4750.png"}, {"time": 2410.352, "image_uri": "frame_4751.png"}, {"time": 2410.859, "image_uri": "frame_4752.png"}, {"time": 2411.367, "image_uri": "frame_4753.png"}, {"time": 2411.874, "image_uri": "frame_4754.png"}, {"time": 2412.382, "image_uri": "frame_4755.png"}, {"time": 2412.889, "image_uri": "frame_4756.png"}, {"time": 2413.396, "image_uri": "frame_4757.png"}, {"time": 2413.904, "image_uri": "frame_4758.png"}, {"time": 2414.411, "image_uri": "frame_4759.png"}, {"time": 2414.919, "image_uri": "frame_4760.png"}, {"time": 2415.426, "image_uri": "frame_4761.png"}, {"time": 2415.934, "image_uri": "frame_4762.png"}, {"time": 2416.441, "image_uri": "frame_4763.png"}, {"time": 2416.949, "image_uri": "frame_4764.png"}, {"time": 2417.456, "image_uri": "frame_4765.png"}, {"time": 2417.963, "image_uri": "frame_4766.png"}, {"time": 2418.471, "image_uri": "frame_4767.png"}, {"time": 2418.978, "image_uri": "frame_4768.png"}, {"time": 2419.486, "image_uri": "frame_4769.png"}, {"time": 2419.993, "image_uri": "frame_4770.png"}, {"time": 2420.501, "image_uri": "frame_4771.png"}, {"time": 2421.008, "image_uri": "frame_4772.png"}, {"time": 2421.516, "image_uri": "frame_4773.png"}, {"time": 2422.023, "image_uri": "frame_4774.png"}, {"time": 2422.53, "image_uri": "frame_4775.png"}, {"time": 2423.038, "image_uri": "frame_4776.png"}, {"time": 2423.545, "image_uri": "frame_4777.png"}, {"time": 2424.053, "image_uri": "frame_4778.png"}, {"time": 2424.56, "image_uri": "frame_4779.png"}, {"time": 2425.068, "image_uri": "frame_4780.png"}, {"time": 2425.575, "image_uri": "frame_4781.png"}, {"time": 2426.083, "image_uri": "frame_4782.png"}, {"time": 2426.59, "image_uri": "frame_4783.png"}, {"time": 2427.097, "image_uri": "frame_4784.png"}, {"time": 2427.605, "image_uri": "frame_4785.png"}, {"time": 2428.112, "image_uri": "frame_4786.png"}, {"time": 2428.62, "image_uri": "frame_4787.png"}, {"time": 2429.127, "image_uri": "frame_4788.png"}, {"time": 2429.635, "image_uri": "frame_4789.png"}, {"time": 2430.142, "image_uri": "frame_4790.png"}, {"time": 2430.65, "image_uri": "frame_4791.png"}, {"time": 2431.157, "image_uri": "frame_4792.png"}, {"time": 2431.664, "image_uri": "frame_4793.png"}, {"time": 2432.172, "image_uri": "frame_4794.png"}, {"time": 2432.679, "image_uri": "frame_4795.png"}, {"time": 2433.187, "image_uri": "frame_4796.png"}, {"time": 2433.694, "image_uri": "frame_4797.png"}, {"time": 2434.202, "image_uri": "frame_4798.png"}, {"time": 2434.709, "image_uri": "frame_4799.png"}, {"time": 2435.217, "image_uri": "frame_4800.png"}, {"time": 2435.724, "image_uri": "frame_4801.png"}, {"time": 2436.231, "image_uri": "frame_4802.png"}, {"time": 2436.739, "image_uri": "frame_4803.png"}, {"time": 2437.246, "image_uri": "frame_4804.png"}, {"time": 2437.754, "image_uri": "frame_4805.png"}, {"time": 2438.261, "image_uri": "frame_4806.png"}, {"time": 2438.769, "image_uri": "frame_4807.png"}, {"time": 2439.276, "image_uri": "frame_4808.png"}, {"time": 2439.783, "image_uri": "frame_4809.png"}, {"time": 2440.291, "image_uri": "frame_4810.png"}, {"time": 2440.798, "image_uri": "frame_4811.png"}, {"time": 2441.306, "image_uri": "frame_4812.png"}, {"time": 2441.813, "image_uri": "frame_4813.png"}, {"time": 2442.321, "image_uri": "frame_4814.png"}, {"time": 2442.828, "image_uri": "frame_4815.png"}, {"time": 2443.336, "image_uri": "frame_4816.png"}, {"time": 2443.843, "image_uri": "frame_4817.png"}, {"time": 2444.35, "image_uri": "frame_4818.png"}, {"time": 2444.858, "image_uri": "frame_4819.png"}, {"time": 2445.365, "image_uri": "frame_4820.png"}, {"time": 2445.873, "image_uri": "frame_4821.png"}, {"time": 2446.38, "image_uri": "frame_4822.png"}, {"time": 2446.888, "image_uri": "frame_4823.png"}, {"time": 2447.395, "image_uri": "frame_4824.png"}, {"time": 2447.903, "image_uri": "frame_4825.png"}, {"time": 2448.41, "image_uri": "frame_4826.png"}, {"time": 2448.917, "image_uri": "frame_4827.png"}, {"time": 2449.425, "image_uri": "frame_4828.png"}, {"time": 2449.932, "image_uri": "frame_4829.png"}, {"time": 2450.44, "image_uri": "frame_4830.png"}, {"time": 2450.947, "image_uri": "frame_4831.png"}, {"time": 2451.455, "image_uri": "frame_4832.png"}, {"time": 2451.962, "image_uri": "frame_4833.png"}, {"time": 2452.47, "image_uri": "frame_4834.png"}, {"time": 2452.977, "image_uri": "frame_4835.png"}, {"time": 2453.484, "image_uri": "frame_4836.png"}, {"time": 2453.992, "image_uri": "frame_4837.png"}, {"time": 2454.499, "image_uri": "frame_4838.png"}, {"time": 2455.007, "image_uri": "frame_4839.png"}, {"time": 2455.514, "image_uri": "frame_4840.png"}, {"time": 2456.022, "image_uri": "frame_4841.png"}, {"time": 2456.529, "image_uri": "frame_4842.png"}, {"time": 2457.037, "image_uri": "frame_4843.png"}, {"time": 2457.544, "image_uri": "frame_4844.png"}, {"time": 2458.051, "image_uri": "frame_4845.png"}, {"time": 2458.559, "image_uri": "frame_4846.png"}, {"time": 2459.066, "image_uri": "frame_4847.png"}, {"time": 2459.574, "image_uri": "frame_4848.png"}, {"time": 2460.081, "image_uri": "frame_4849.png"}, {"time": 2460.589, "image_uri": "frame_4850.png"}, {"time": 2461.096, "image_uri": "frame_4851.png"}, {"time": 2461.604, "image_uri": "frame_4852.png"}, {"time": 2462.111, "image_uri": "frame_4853.png"}, {"time": 2462.618, "image_uri": "frame_4854.png"}, {"time": 2463.126, "image_uri": "frame_4855.png"}, {"time": 2463.633, "image_uri": "frame_4856.png"}, {"time": 2464.141, "image_uri": "frame_4857.png"}, {"time": 2464.648, "image_uri": "frame_4858.png"}, {"time": 2465.156, "image_uri": "frame_4859.png"}, {"time": 2465.663, "image_uri": "frame_4860.png"}, {"time": 2466.171, "image_uri": "frame_4861.png"}, {"time": 2466.678, "image_uri": "frame_4862.png"}, {"time": 2467.185, "image_uri": "frame_4863.png"}, {"time": 2467.693, "image_uri": "frame_4864.png"}, {"time": 2468.2, "image_uri": "frame_4865.png"}, {"time": 2468.708, "image_uri": "frame_4866.png"}, {"time": 2469.215, "image_uri": "frame_4867.png"}, {"time": 2469.723, "image_uri": "frame_4868.png"}, {"time": 2470.23, "image_uri": "frame_4869.png"}, {"time": 2470.737, "image_uri": "frame_4870.png"}, {"time": 2471.245, "image_uri": "frame_4871.png"}, {"time": 2471.752, "image_uri": "frame_4872.png"}, {"time": 2472.26, "image_uri": "frame_4873.png"}, {"time": 2472.767, "image_uri": "frame_4874.png"}, {"time": 2473.275, "image_uri": "frame_4875.png"}, {"time": 2473.782, "image_uri": "frame_4876.png"}, {"time": 2474.29, "image_uri": "frame_4877.png"}, {"time": 2474.797, "image_uri": "frame_4878.png"}, {"time": 2475.304, "image_uri": "frame_4879.png"}, {"time": 2475.812, "image_uri": "frame_4880.png"}, {"time": 2476.319, "image_uri": "frame_4881.png"}, {"time": 2476.827, "image_uri": "frame_4882.png"}, {"time": 2477.334, "image_uri": "frame_4883.png"}, {"time": 2477.842, "image_uri": "frame_4884.png"}, {"time": 2478.349, "image_uri": "frame_4885.png"}, {"time": 2478.857, "image_uri": "frame_4886.png"}, {"time": 2479.364, "image_uri": "frame_4887.png"}, {"time": 2479.871, "image_uri": "frame_4888.png"}, {"time": 2480.379, "image_uri": "frame_4889.png"}, {"time": 2480.886, "image_uri": "frame_4890.png"}, {"time": 2481.394, "image_uri": "frame_4891.png"}, {"time": 2481.901, "image_uri": "frame_4892.png"}, {"time": 2482.409, "image_uri": "frame_4893.png"}, {"time": 2482.916, "image_uri": "frame_4894.png"}, {"time": 2483.424, "image_uri": "frame_4895.png"}, {"time": 2483.931, "image_uri": "frame_4896.png"}, {"time": 2484.438, "image_uri": "frame_4897.png"}, {"time": 2484.946, "image_uri": "frame_4898.png"}, {"time": 2485.453, "image_uri": "frame_4899.png"}, {"time": 2485.961, "image_uri": "frame_4900.png"}, {"time": 2486.468, "image_uri": "frame_4901.png"}, {"time": 2486.976, "image_uri": "frame_4902.png"}, {"time": 2487.483, "image_uri": "frame_4903.png"}, {"time": 2487.991, "image_uri": "frame_4904.png"}, {"time": 2488.498, "image_uri": "frame_4905.png"}, {"time": 2489.005, "image_uri": "frame_4906.png"}, {"time": 2489.513, "image_uri": "frame_4907.png"}, {"time": 2490.02, "image_uri": "frame_4908.png"}, {"time": 2490.528, "image_uri": "frame_4909.png"}, {"time": 2491.035, "image_uri": "frame_4910.png"}, {"time": 2491.543, "image_uri": "frame_4911.png"}, {"time": 2492.05, "image_uri": "frame_4912.png"}, {"time": 2492.558, "image_uri": "frame_4913.png"}, {"time": 2493.065, "image_uri": "frame_4914.png"}, {"time": 2493.572, "image_uri": "frame_4915.png"}, {"time": 2494.08, "image_uri": "frame_4916.png"}, {"time": 2494.587, "image_uri": "frame_4917.png"}, {"time": 2495.095, "image_uri": "frame_4918.png"}, {"time": 2495.602, "image_uri": "frame_4919.png"}, {"time": 2496.11, "image_uri": "frame_4920.png"}, {"time": 2496.617, "image_uri": "frame_4921.png"}, {"time": 2497.124, "image_uri": "frame_4922.png"}, {"time": 2497.632, "image_uri": "frame_4923.png"}, {"time": 2498.139, "image_uri": "frame_4924.png"}, {"time": 2498.647, "image_uri": "frame_4925.png"}, {"time": 2499.154, "image_uri": "frame_4926.png"}, {"time": 2499.662, "image_uri": "frame_4927.png"}, {"time": 2500.169, "image_uri": "frame_4928.png"}, {"time": 2500.677, "image_uri": "frame_4929.png"}, {"time": 2501.184, "image_uri": "frame_4930.png"}, {"time": 2501.691, "image_uri": "frame_4931.png"}, {"time": 2502.199, "image_uri": "frame_4932.png"}, {"time": 2502.706, "image_uri": "frame_4933.png"}, {"time": 2503.214, "image_uri": "frame_4934.png"}, {"time": 2503.721, "image_uri": "frame_4935.png"}, {"time": 2504.229, "image_uri": "frame_4936.png"}, {"time": 2504.736, "image_uri": "frame_4937.png"}, {"time": 2505.244, "image_uri": "frame_4938.png"}, {"time": 2505.751, "image_uri": "frame_4939.png"}, {"time": 2506.258, "image_uri": "frame_4940.png"}, {"time": 2506.766, "image_uri": "frame_4941.png"}, {"time": 2507.273, "image_uri": "frame_4942.png"}, {"time": 2507.781, "image_uri": "frame_4943.png"}, {"time": 2508.288, "image_uri": "frame_4944.png"}, {"time": 2508.796, "image_uri": "frame_4945.png"}, {"time": 2509.303, "image_uri": "frame_4946.png"}, {"time": 2509.811, "image_uri": "frame_4947.png"}, {"time": 2510.318, "image_uri": "frame_4948.png"}, {"time": 2510.825, "image_uri": "frame_4949.png"}, {"time": 2511.333, "image_uri": "frame_4950.png"}, {"time": 2511.84, "image_uri": "frame_4951.png"}, {"time": 2512.348, "image_uri": "frame_4952.png"}, {"time": 2512.855, "image_uri": "frame_4953.png"}, {"time": 2513.363, "image_uri": "frame_4954.png"}, {"time": 2513.87, "image_uri": "frame_4955.png"}, {"time": 2514.378, "image_uri": "frame_4956.png"}, {"time": 2514.885, "image_uri": "frame_4957.png"}, {"time": 2515.392, "image_uri": "frame_4958.png"}, {"time": 2515.9, "image_uri": "frame_4959.png"}, {"time": 2516.407, "image_uri": "frame_4960.png"}, {"time": 2516.915, "image_uri": "frame_4961.png"}, {"time": 2517.422, "image_uri": "frame_4962.png"}, {"time": 2517.93, "image_uri": "frame_4963.png"}, {"time": 2518.437, "image_uri": "frame_4964.png"}, {"time": 2518.945, "image_uri": "frame_4965.png"}, {"time": 2519.452, "image_uri": "frame_4966.png"}, {"time": 2519.959, "image_uri": "frame_4967.png"}, {"time": 2520.467, "image_uri": "frame_4968.png"}, {"time": 2520.974, "image_uri": "frame_4969.png"}, {"time": 2521.482, "image_uri": "frame_4970.png"}, {"time": 2521.989, "image_uri": "frame_4971.png"}, {"time": 2522.497, "image_uri": "frame_4972.png"}, {"time": 2523.004, "image_uri": "frame_4973.png"}, {"time": 2523.512, "image_uri": "frame_4974.png"}, {"time": 2524.019, "image_uri": "frame_4975.png"}, {"time": 2524.526, "image_uri": "frame_4976.png"}, {"time": 2525.034, "image_uri": "frame_4977.png"}, {"time": 2525.541, "image_uri": "frame_4978.png"}, {"time": 2526.049, "image_uri": "frame_4979.png"}, {"time": 2526.556, "image_uri": "frame_4980.png"}, {"time": 2527.064, "image_uri": "frame_4981.png"}, {"time": 2527.571, "image_uri": "frame_4982.png"}, {"time": 2528.078, "image_uri": "frame_4983.png"}, {"time": 2528.586, "image_uri": "frame_4984.png"}, {"time": 2529.093, "image_uri": "frame_4985.png"}, {"time": 2529.601, "image_uri": "frame_4986.png"}, {"time": 2530.108, "image_uri": "frame_4987.png"}, {"time": 2530.616, "image_uri": "frame_4988.png"}, {"time": 2531.123, "image_uri": "frame_4989.png"}, {"time": 2531.631, "image_uri": "frame_4990.png"}, {"time": 2532.138, "image_uri": "frame_4991.png"}, {"time": 2532.645, "image_uri": "frame_4992.png"}, {"time": 2533.153, "image_uri": "frame_4993.png"}, {"time": 2533.66, "image_uri": "frame_4994.png"}, {"time": 2534.168, "image_uri": "frame_4995.png"}, {"time": 2534.675, "image_uri": "frame_4996.png"}, {"time": 2535.183, "image_uri": "frame_4997.png"}, {"time": 2535.69, "image_uri": "frame_4998.png"}, {"time": 2536.198, "image_uri": "frame_4999.png"}, {"time": 2536.705, "image_uri": "frame_5000.png"}, {"time": 2537.212, "image_uri": "frame_5001.png"}, {"time": 2537.72, "image_uri": "frame_5002.png"}, {"time": 2538.227, "image_uri": "frame_5003.png"}, {"time": 2538.735, "image_uri": "frame_5004.png"}, {"time": 2539.242, "image_uri": "frame_5005.png"}, {"time": 2539.75, "image_uri": "frame_5006.png"}, {"time": 2540.257, "image_uri": "frame_5007.png"}, {"time": 2540.765, "image_uri": "frame_5008.png"}, {"time": 2541.272, "image_uri": "frame_5009.png"}, {"time": 2541.779, "image_uri": "frame_5010.png"}, {"time": 2542.287, "image_uri": "frame_5011.png"}, {"time": 2542.794, "image_uri": "frame_5012.png"}, {"time": 2543.302, "image_uri": "frame_5013.png"}, {"time": 2543.809, "image_uri": "frame_5014.png"}, {"time": 2544.317, "image_uri": "frame_5015.png"}, {"time": 2544.824, "image_uri": "frame_5016.png"}, {"time": 2545.332, "image_uri": "frame_5017.png"}, {"time": 2545.839, "image_uri": "frame_5018.png"}, {"time": 2546.346, "image_uri": "frame_5019.png"}, {"time": 2546.854, "image_uri": "frame_5020.png"}, {"time": 2547.361, "image_uri": "frame_5021.png"}, {"time": 2547.869, "image_uri": "frame_5022.png"}, {"time": 2548.376, "image_uri": "frame_5023.png"}, {"time": 2548.884, "image_uri": "frame_5024.png"}, {"time": 2549.391, "image_uri": "frame_5025.png"}, {"time": 2549.899, "image_uri": "frame_5026.png"}, {"time": 2550.406, "image_uri": "frame_5027.png"}, {"time": 2550.913, "image_uri": "frame_5028.png"}, {"time": 2551.421, "image_uri": "frame_5029.png"}, {"time": 2551.928, "image_uri": "frame_5030.png"}, {"time": 2552.436, "image_uri": "frame_5031.png"}, {"time": 2552.943, "image_uri": "frame_5032.png"}, {"time": 2553.451, "image_uri": "frame_5033.png"}, {"time": 2553.958, "image_uri": "frame_5034.png"}, {"time": 2554.465, "image_uri": "frame_5035.png"}, {"time": 2554.973, "image_uri": "frame_5036.png"}, {"time": 2555.48, "image_uri": "frame_5037.png"}, {"time": 2555.988, "image_uri": "frame_5038.png"}, {"time": 2556.495, "image_uri": "frame_5039.png"}, {"time": 2557.003, "image_uri": "frame_5040.png"}, {"time": 2557.51, "image_uri": "frame_5041.png"}, {"time": 2558.018, "image_uri": "frame_5042.png"}, {"time": 2558.525, "image_uri": "frame_5043.png"}, {"time": 2559.032, "image_uri": "frame_5044.png"}, {"time": 2559.54, "image_uri": "frame_5045.png"}, {"time": 2560.047, "image_uri": "frame_5046.png"}, {"time": 2560.555, "image_uri": "frame_5047.png"}, {"time": 2561.062, "image_uri": "frame_5048.png"}, {"time": 2561.57, "image_uri": "frame_5049.png"}, {"time": 2562.077, "image_uri": "frame_5050.png"}, {"time": 2562.585, "image_uri": "frame_5051.png"}, {"time": 2563.092, "image_uri": "frame_5052.png"}, {"time": 2563.599, "image_uri": "frame_5053.png"}, {"time": 2564.107, "image_uri": "frame_5054.png"}, {"time": 2564.614, "image_uri": "frame_5055.png"}, {"time": 2565.122, "image_uri": "frame_5056.png"}, {"time": 2565.629, "image_uri": "frame_5057.png"}, {"time": 2566.137, "image_uri": "frame_5058.png"}, {"time": 2566.644, "image_uri": "frame_5059.png"}, {"time": 2567.152, "image_uri": "frame_5060.png"}, {"time": 2567.659, "image_uri": "frame_5061.png"}, {"time": 2568.166, "image_uri": "frame_5062.png"}, {"time": 2568.674, "image_uri": "frame_5063.png"}, {"time": 2569.181, "image_uri": "frame_5064.png"}, {"time": 2569.689, "image_uri": "frame_5065.png"}, {"time": 2570.196, "image_uri": "frame_5066.png"}, {"time": 2570.704, "image_uri": "frame_5067.png"}, {"time": 2571.211, "image_uri": "frame_5068.png"}, {"time": 2571.719, "image_uri": "frame_5069.png"}, {"time": 2572.226, "image_uri": "frame_5070.png"}, {"time": 2572.733, "image_uri": "frame_5071.png"}, {"time": 2573.241, "image_uri": "frame_5072.png"}, {"time": 2573.748, "image_uri": "frame_5073.png"}, {"time": 2574.256, "image_uri": "frame_5074.png"}, {"time": 2574.763, "image_uri": "frame_5075.png"}, {"time": 2575.271, "image_uri": "frame_5076.png"}, {"time": 2575.778, "image_uri": "frame_5077.png"}, {"time": 2576.286, "image_uri": "frame_5078.png"}, {"time": 2576.793, "image_uri": "frame_5079.png"}, {"time": 2577.3, "image_uri": "frame_5080.png"}, {"time": 2577.808, "image_uri": "frame_5081.png"}, {"time": 2578.315, "image_uri": "frame_5082.png"}, {"time": 2578.823, "image_uri": "frame_5083.png"}, {"time": 2579.33, "image_uri": "frame_5084.png"}, {"time": 2579.838, "image_uri": "frame_5085.png"}, {"time": 2580.345, "image_uri": "frame_5086.png"}, {"time": 2580.853, "image_uri": "frame_5087.png"}, {"time": 2581.36, "image_uri": "frame_5088.png"}, {"time": 2581.867, "image_uri": "frame_5089.png"}, {"time": 2582.375, "image_uri": "frame_5090.png"}, {"time": 2582.882, "image_uri": "frame_5091.png"}, {"time": 2583.39, "image_uri": "frame_5092.png"}, {"time": 2583.897, "image_uri": "frame_5093.png"}, {"time": 2584.405, "image_uri": "frame_5094.png"}, {"time": 2584.912, "image_uri": "frame_5095.png"}, {"time": 2585.419, "image_uri": "frame_5096.png"}, {"time": 2585.927, "image_uri": "frame_5097.png"}, {"time": 2586.434, "image_uri": "frame_5098.png"}, {"time": 2586.942, "image_uri": "frame_5099.png"}, {"time": 2587.449, "image_uri": "frame_5100.png"}, {"time": 2587.957, "image_uri": "frame_5101.png"}, {"time": 2588.464, "image_uri": "frame_5102.png"}, {"time": 2588.972, "image_uri": "frame_5103.png"}, {"time": 2589.479, "image_uri": "frame_5104.png"}, {"time": 2589.986, "image_uri": "frame_5105.png"}, {"time": 2590.494, "image_uri": "frame_5106.png"}, {"time": 2591.001, "image_uri": "frame_5107.png"}, {"time": 2591.509, "image_uri": "frame_5108.png"}, {"time": 2592.016, "image_uri": "frame_5109.png"}, {"time": 2592.524, "image_uri": "frame_5110.png"}, {"time": 2593.031, "image_uri": "frame_5111.png"}, {"time": 2593.539, "image_uri": "frame_5112.png"}, {"time": 2594.046, "image_uri": "frame_5113.png"}, {"time": 2594.553, "image_uri": "frame_5114.png"}, {"time": 2595.061, "image_uri": "frame_5115.png"}, {"time": 2595.568, "image_uri": "frame_5116.png"}, {"time": 2596.076, "image_uri": "frame_5117.png"}, {"time": 2596.583, "image_uri": "frame_5118.png"}, {"time": 2597.091, "image_uri": "frame_5119.png"}, {"time": 2597.598, "image_uri": "frame_5120.png"}, {"time": 2598.106, "image_uri": "frame_5121.png"}, {"time": 2598.613, "image_uri": "frame_5122.png"}, {"time": 2599.12, "image_uri": "frame_5123.png"}, {"time": 2599.628, "image_uri": "frame_5124.png"}, {"time": 2600.135, "image_uri": "frame_5125.png"}, {"time": 2600.643, "image_uri": "frame_5126.png"}, {"time": 2601.15, "image_uri": "frame_5127.png"}, {"time": 2601.658, "image_uri": "frame_5128.png"}, {"time": 2602.165, "image_uri": "frame_5129.png"}, {"time": 2602.673, "image_uri": "frame_5130.png"}, {"time": 2603.18, "image_uri": "frame_5131.png"}, {"time": 2603.687, "image_uri": "frame_5132.png"}, {"time": 2604.195, "image_uri": "frame_5133.png"}, {"time": 2604.702, "image_uri": "frame_5134.png"}, {"time": 2605.21, "image_uri": "frame_5135.png"}, {"time": 2605.717, "image_uri": "frame_5136.png"}, {"time": 2606.225, "image_uri": "frame_5137.png"}, {"time": 2606.732, "image_uri": "frame_5138.png"}, {"time": 2607.24, "image_uri": "frame_5139.png"}, {"time": 2607.747, "image_uri": "frame_5140.png"}, {"time": 2608.254, "image_uri": "frame_5141.png"}, {"time": 2608.762, "image_uri": "frame_5142.png"}, {"time": 2609.269, "image_uri": "frame_5143.png"}, {"time": 2609.777, "image_uri": "frame_5144.png"}, {"time": 2610.284, "image_uri": "frame_5145.png"}, {"time": 2610.792, "image_uri": "frame_5146.png"}, {"time": 2611.299, "image_uri": "frame_5147.png"}, {"time": 2611.806, "image_uri": "frame_5148.png"}, {"time": 2612.314, "image_uri": "frame_5149.png"}, {"time": 2612.821, "image_uri": "frame_5150.png"}, {"time": 2613.329, "image_uri": "frame_5151.png"}, {"time": 2613.836, "image_uri": "frame_5152.png"}, {"time": 2614.344, "image_uri": "frame_5153.png"}, {"time": 2614.851, "image_uri": "frame_5154.png"}, {"time": 2615.359, "image_uri": "frame_5155.png"}, {"time": 2615.866, "image_uri": "frame_5156.png"}, {"time": 2616.373, "image_uri": "frame_5157.png"}, {"time": 2616.881, "image_uri": "frame_5158.png"}, {"time": 2617.388, "image_uri": "frame_5159.png"}, {"time": 2617.896, "image_uri": "frame_5160.png"}, {"time": 2618.403, "image_uri": "frame_5161.png"}, {"time": 2618.911, "image_uri": "frame_5162.png"}, {"time": 2619.418, "image_uri": "frame_5163.png"}, {"time": 2619.926, "image_uri": "frame_5164.png"}, {"time": 2620.433, "image_uri": "frame_5165.png"}, {"time": 2620.94, "image_uri": "frame_5166.png"}, {"time": 2621.448, "image_uri": "frame_5167.png"}, {"time": 2621.955, "image_uri": "frame_5168.png"}, {"time": 2622.463, "image_uri": "frame_5169.png"}, {"time": 2622.97, "image_uri": "frame_5170.png"}, {"time": 2623.478, "image_uri": "frame_5171.png"}, {"time": 2623.985, "image_uri": "frame_5172.png"}, {"time": 2624.493, "image_uri": "frame_5173.png"}, {"time": 2625.0, "image_uri": "frame_5174.png"}, {"time": 2625.507, "image_uri": "frame_5175.png"}, {"time": 2626.015, "image_uri": "frame_5176.png"}, {"time": 2626.522, "image_uri": "frame_5177.png"}, {"time": 2627.03, "image_uri": "frame_5178.png"}, {"time": 2627.537, "image_uri": "frame_5179.png"}, {"time": 2628.045, "image_uri": "frame_5180.png"}, {"time": 2628.552, "image_uri": "frame_5181.png"}, {"time": 2629.06, "image_uri": "frame_5182.png"}, {"time": 2629.567, "image_uri": "frame_5183.png"}, {"time": 2630.074, "image_uri": "frame_5184.png"}, {"time": 2630.582, "image_uri": "frame_5185.png"}, {"time": 2631.089, "image_uri": "frame_5186.png"}, {"time": 2631.597, "image_uri": "frame_5187.png"}, {"time": 2632.104, "image_uri": "frame_5188.png"}, {"time": 2632.612, "image_uri": "frame_5189.png"}, {"time": 2633.119, "image_uri": "frame_5190.png"}, {"time": 2633.627, "image_uri": "frame_5191.png"}, {"time": 2634.134, "image_uri": "frame_5192.png"}, {"time": 2634.641, "image_uri": "frame_5193.png"}, {"time": 2635.149, "image_uri": "frame_5194.png"}, {"time": 2635.656, "image_uri": "frame_5195.png"}, {"time": 2636.164, "image_uri": "frame_5196.png"}, {"time": 2636.671, "image_uri": "frame_5197.png"}, {"time": 2637.179, "image_uri": "frame_5198.png"}, {"time": 2637.686, "image_uri": "frame_5199.png"}, {"time": 2638.194, "image_uri": "frame_5200.png"}, {"time": 2638.701, "image_uri": "frame_5201.png"}, {"time": 2639.208, "image_uri": "frame_5202.png"}, {"time": 2639.716, "image_uri": "frame_5203.png"}, {"time": 2640.223, "image_uri": "frame_5204.png"}, {"time": 2640.731, "image_uri": "frame_5205.png"}, {"time": 2641.238, "image_uri": "frame_5206.png"}, {"time": 2641.746, "image_uri": "frame_5207.png"}, {"time": 2642.253, "image_uri": "frame_5208.png"}, {"time": 2642.76, "image_uri": "frame_5209.png"}, {"time": 2643.268, "image_uri": "frame_5210.png"}, {"time": 2643.775, "image_uri": "frame_5211.png"}, {"time": 2644.283, "image_uri": "frame_5212.png"}, {"time": 2644.79, "image_uri": "frame_5213.png"}, {"time": 2645.298, "image_uri": "frame_5214.png"}, {"time": 2645.805, "image_uri": "frame_5215.png"}, {"time": 2646.313, "image_uri": "frame_5216.png"}, {"time": 2646.82, "image_uri": "frame_5217.png"}, {"time": 2647.327, "image_uri": "frame_5218.png"}, {"time": 2647.835, "image_uri": "frame_5219.png"}, {"time": 2648.342, "image_uri": "frame_5220.png"}, {"time": 2648.85, "image_uri": "frame_5221.png"}, {"time": 2649.357, "image_uri": "frame_5222.png"}, {"time": 2649.865, "image_uri": "frame_5223.png"}, {"time": 2650.372, "image_uri": "frame_5224.png"}, {"time": 2650.88, "image_uri": "frame_5225.png"}, {"time": 2651.387, "image_uri": "frame_5226.png"}, {"time": 2651.894, "image_uri": "frame_5227.png"}, {"time": 2652.402, "image_uri": "frame_5228.png"}, {"time": 2652.909, "image_uri": "frame_5229.png"}, {"time": 2653.417, "image_uri": "frame_5230.png"}, {"time": 2653.924, "image_uri": "frame_5231.png"}, {"time": 2654.432, "image_uri": "frame_5232.png"}, {"time": 2654.939, "image_uri": "frame_5233.png"}, {"time": 2655.447, "image_uri": "frame_5234.png"}, {"time": 2655.954, "image_uri": "frame_5235.png"}, {"time": 2656.461, "image_uri": "frame_5236.png"}, {"time": 2656.969, "image_uri": "frame_5237.png"}, {"time": 2657.476, "image_uri": "frame_5238.png"}, {"time": 2657.984, "image_uri": "frame_5239.png"}, {"time": 2658.491, "image_uri": "frame_5240.png"}, {"time": 2658.999, "image_uri": "frame_5241.png"}, {"time": 2659.506, "image_uri": "frame_5242.png"}, {"time": 2660.014, "image_uri": "frame_5243.png"}, {"time": 2660.521, "image_uri": "frame_5244.png"}, {"time": 2661.028, "image_uri": "frame_5245.png"}, {"time": 2661.536, "image_uri": "frame_5246.png"}, {"time": 2662.043, "image_uri": "frame_5247.png"}, {"time": 2662.551, "image_uri": "frame_5248.png"}, {"time": 2663.058, "image_uri": "frame_5249.png"}, {"time": 2663.566, "image_uri": "frame_5250.png"}, {"time": 2664.073, "image_uri": "frame_5251.png"}, {"time": 2664.581, "image_uri": "frame_5252.png"}, {"time": 2665.088, "image_uri": "frame_5253.png"}, {"time": 2665.595, "image_uri": "frame_5254.png"}, {"time": 2666.103, "image_uri": "frame_5255.png"}, {"time": 2666.61, "image_uri": "frame_5256.png"}, {"time": 2667.118, "image_uri": "frame_5257.png"}, {"time": 2667.625, "image_uri": "frame_5258.png"}, {"time": 2668.133, "image_uri": "frame_5259.png"}, {"time": 2668.64, "image_uri": "frame_5260.png"}, {"time": 2669.147, "image_uri": "frame_5261.png"}, {"time": 2669.655, "image_uri": "frame_5262.png"}, {"time": 2670.162, "image_uri": "frame_5263.png"}, {"time": 2670.67, "image_uri": "frame_5264.png"}, {"time": 2671.177, "image_uri": "frame_5265.png"}, {"time": 2671.685, "image_uri": "frame_5266.png"}, {"time": 2672.192, "image_uri": "frame_5267.png"}, {"time": 2672.7, "image_uri": "frame_5268.png"}, {"time": 2673.207, "image_uri": "frame_5269.png"}, {"time": 2673.714, "image_uri": "frame_5270.png"}, {"time": 2674.222, "image_uri": "frame_5271.png"}, {"time": 2674.729, "image_uri": "frame_5272.png"}, {"time": 2675.237, "image_uri": "frame_5273.png"}, {"time": 2675.744, "image_uri": "frame_5274.png"}, {"time": 2676.252, "image_uri": "frame_5275.png"}, {"time": 2676.759, "image_uri": "frame_5276.png"}, {"time": 2677.267, "image_uri": "frame_5277.png"}, {"time": 2677.774, "image_uri": "frame_5278.png"}, {"time": 2678.281, "image_uri": "frame_5279.png"}, {"time": 2678.789, "image_uri": "frame_5280.png"}, {"time": 2679.296, "image_uri": "frame_5281.png"}, {"time": 2679.804, "image_uri": "frame_5282.png"}, {"time": 2680.311, "image_uri": "frame_5283.png"}, {"time": 2680.819, "image_uri": "frame_5284.png"}, {"time": 2681.326, "image_uri": "frame_5285.png"}, {"time": 2681.834, "image_uri": "frame_5286.png"}, {"time": 2682.341, "image_uri": "frame_5287.png"}, {"time": 2682.848, "image_uri": "frame_5288.png"}, {"time": 2683.356, "image_uri": "frame_5289.png"}, {"time": 2683.863, "image_uri": "frame_5290.png"}, {"time": 2684.371, "image_uri": "frame_5291.png"}, {"time": 2684.878, "image_uri": "frame_5292.png"}, {"time": 2685.386, "image_uri": "frame_5293.png"}, {"time": 2685.893, "image_uri": "frame_5294.png"}, {"time": 2686.401, "image_uri": "frame_5295.png"}, {"time": 2686.908, "image_uri": "frame_5296.png"}, {"time": 2687.415, "image_uri": "frame_5297.png"}, {"time": 2687.923, "image_uri": "frame_5298.png"}, {"time": 2688.43, "image_uri": "frame_5299.png"}, {"time": 2688.938, "image_uri": "frame_5300.png"}, {"time": 2689.445, "image_uri": "frame_5301.png"}, {"time": 2689.953, "image_uri": "frame_5302.png"}, {"time": 2690.46, "image_uri": "frame_5303.png"}, {"time": 2690.968, "image_uri": "frame_5304.png"}, {"time": 2691.475, "image_uri": "frame_5305.png"}, {"time": 2691.982, "image_uri": "frame_5306.png"}, {"time": 2692.49, "image_uri": "frame_5307.png"}, {"time": 2692.997, "image_uri": "frame_5308.png"}, {"time": 2693.505, "image_uri": "frame_5309.png"}, {"time": 2694.012, "image_uri": "frame_5310.png"}, {"time": 2694.52, "image_uri": "frame_5311.png"}, {"time": 2695.027, "image_uri": "frame_5312.png"}, {"time": 2695.535, "image_uri": "frame_5313.png"}, {"time": 2696.042, "image_uri": "frame_5314.png"}, {"time": 2696.549, "image_uri": "frame_5315.png"}, {"time": 2697.057, "image_uri": "frame_5316.png"}, {"time": 2697.564, "image_uri": "frame_5317.png"}, {"time": 2698.072, "image_uri": "frame_5318.png"}, {"time": 2698.579, "image_uri": "frame_5319.png"}, {"time": 2699.087, "image_uri": "frame_5320.png"}, {"time": 2699.594, "image_uri": "frame_5321.png"}, {"time": 2700.101, "image_uri": "frame_5322.png"}, {"time": 2700.609, "image_uri": "frame_5323.png"}, {"time": 2701.116, "image_uri": "frame_5324.png"}, {"time": 2701.624, "image_uri": "frame_5325.png"}, {"time": 2702.131, "image_uri": "frame_5326.png"}, {"time": 2702.639, "image_uri": "frame_5327.png"}, {"time": 2703.146, "image_uri": "frame_5328.png"}, {"time": 2703.654, "image_uri": "frame_5329.png"}, {"time": 2704.161, "image_uri": "frame_5330.png"}, {"time": 2704.668, "image_uri": "frame_5331.png"}, {"time": 2705.176, "image_uri": "frame_5332.png"}, {"time": 2705.683, "image_uri": "frame_5333.png"}, {"time": 2706.191, "image_uri": "frame_5334.png"}, {"time": 2706.698, "image_uri": "frame_5335.png"}, {"time": 2707.206, "image_uri": "frame_5336.png"}, {"time": 2707.713, "image_uri": "frame_5337.png"}, {"time": 2708.221, "image_uri": "frame_5338.png"}, {"time": 2708.728, "image_uri": "frame_5339.png"}, {"time": 2709.235, "image_uri": "frame_5340.png"}, {"time": 2709.743, "image_uri": "frame_5341.png"}, {"time": 2710.25, "image_uri": "frame_5342.png"}, {"time": 2710.758, "image_uri": "frame_5343.png"}, {"time": 2711.265, "image_uri": "frame_5344.png"}, {"time": 2711.773, "image_uri": "frame_5345.png"}, {"time": 2712.28, "image_uri": "frame_5346.png"}, {"time": 2712.788, "image_uri": "frame_5347.png"}, {"time": 2713.295, "image_uri": "frame_5348.png"}, {"time": 2713.802, "image_uri": "frame_5349.png"}, {"time": 2714.31, "image_uri": "frame_5350.png"}, {"time": 2714.817, "image_uri": "frame_5351.png"}, {"time": 2715.325, "image_uri": "frame_5352.png"}, {"time": 2715.832, "image_uri": "frame_5353.png"}, {"time": 2716.34, "image_uri": "frame_5354.png"}, {"time": 2716.847, "image_uri": "frame_5355.png"}, {"time": 2717.355, "image_uri": "frame_5356.png"}, {"time": 2717.862, "image_uri": "frame_5357.png"}, {"time": 2718.369, "image_uri": "frame_5358.png"}, {"time": 2718.877, "image_uri": "frame_5359.png"}, {"time": 2719.384, "image_uri": "frame_5360.png"}, {"time": 2719.892, "image_uri": "frame_5361.png"}, {"time": 2720.399, "image_uri": "frame_5362.png"}, {"time": 2720.907, "image_uri": "frame_5363.png"}, {"time": 2721.414, "image_uri": "frame_5364.png"}, {"time": 2721.922, "image_uri": "frame_5365.png"}, {"time": 2722.429, "image_uri": "frame_5366.png"}, {"time": 2722.936, "image_uri": "frame_5367.png"}, {"time": 2723.444, "image_uri": "frame_5368.png"}, {"time": 2723.951, "image_uri": "frame_5369.png"}, {"time": 2724.459, "image_uri": "frame_5370.png"}, {"time": 2724.966, "image_uri": "frame_5371.png"}, {"time": 2725.474, "image_uri": "frame_5372.png"}, {"time": 2725.981, "image_uri": "frame_5373.png"}, {"time": 2726.488, "image_uri": "frame_5374.png"}, {"time": 2726.996, "image_uri": "frame_5375.png"}, {"time": 2727.503, "image_uri": "frame_5376.png"}, {"time": 2728.011, "image_uri": "frame_5377.png"}, {"time": 2728.518, "image_uri": "frame_5378.png"}, {"time": 2729.026, "image_uri": "frame_5379.png"}, {"time": 2729.533, "image_uri": "frame_5380.png"}, {"time": 2730.041, "image_uri": "frame_5381.png"}, {"time": 2730.548, "image_uri": "frame_5382.png"}, {"time": 2731.055, "image_uri": "frame_5383.png"}, {"time": 2731.563, "image_uri": "frame_5384.png"}, {"time": 2732.07, "image_uri": "frame_5385.png"}, {"time": 2732.578, "image_uri": "frame_5386.png"}, {"time": 2733.085, "image_uri": "frame_5387.png"}, {"time": 2733.593, "image_uri": "frame_5388.png"}, {"time": 2734.1, "image_uri": "frame_5389.png"}, {"time": 2734.608, "image_uri": "frame_5390.png"}, {"time": 2735.115, "image_uri": "frame_5391.png"}, {"time": 2735.622, "image_uri": "frame_5392.png"}, {"time": 2736.13, "image_uri": "frame_5393.png"}, {"time": 2736.637, "image_uri": "frame_5394.png"}, {"time": 2737.145, "image_uri": "frame_5395.png"}, {"time": 2737.652, "image_uri": "frame_5396.png"}, {"time": 2738.16, "image_uri": "frame_5397.png"}, {"time": 2738.667, "image_uri": "frame_5398.png"}, {"time": 2739.175, "image_uri": "frame_5399.png"}, {"time": 2739.682, "image_uri": "frame_5400.png"}, {"time": 2740.189, "image_uri": "frame_5401.png"}, {"time": 2740.697, "image_uri": "frame_5402.png"}, {"time": 2741.204, "image_uri": "frame_5403.png"}, {"time": 2741.712, "image_uri": "frame_5404.png"}, {"time": 2742.219, "image_uri": "frame_5405.png"}, {"time": 2742.727, "image_uri": "frame_5406.png"}, {"time": 2743.234, "image_uri": "frame_5407.png"}, {"time": 2743.742, "image_uri": "frame_5408.png"}, {"time": 2744.249, "image_uri": "frame_5409.png"}, {"time": 2744.756, "image_uri": "frame_5410.png"}, {"time": 2745.264, "image_uri": "frame_5411.png"}, {"time": 2745.771, "image_uri": "frame_5412.png"}, {"time": 2746.279, "image_uri": "frame_5413.png"}, {"time": 2746.786, "image_uri": "frame_5414.png"}, {"time": 2747.294, "image_uri": "frame_5415.png"}, {"time": 2747.801, "image_uri": "frame_5416.png"}, {"time": 2748.309, "image_uri": "frame_5417.png"}, {"time": 2748.816, "image_uri": "frame_5418.png"}, {"time": 2749.323, "image_uri": "frame_5419.png"}, {"time": 2749.831, "image_uri": "frame_5420.png"}, {"time": 2750.338, "image_uri": "frame_5421.png"}, {"time": 2750.846, "image_uri": "frame_5422.png"}, {"time": 2751.353, "image_uri": "frame_5423.png"}, {"time": 2751.861, "image_uri": "frame_5424.png"}, {"time": 2752.368, "image_uri": "frame_5425.png"}, {"time": 2752.876, "image_uri": "frame_5426.png"}, {"time": 2753.383, "image_uri": "frame_5427.png"}, {"time": 2753.89, "image_uri": "frame_5428.png"}, {"time": 2754.398, "image_uri": "frame_5429.png"}, {"time": 2754.905, "image_uri": "frame_5430.png"}, {"time": 2755.413, "image_uri": "frame_5431.png"}, {"time": 2755.92, "image_uri": "frame_5432.png"}, {"time": 2756.428, "image_uri": "frame_5433.png"}, {"time": 2756.935, "image_uri": "frame_5434.png"}, {"time": 2757.442, "image_uri": "frame_5435.png"}, {"time": 2757.95, "image_uri": "frame_5436.png"}, {"time": 2758.457, "image_uri": "frame_5437.png"}, {"time": 2758.965, "image_uri": "frame_5438.png"}, {"time": 2759.472, "image_uri": "frame_5439.png"}, {"time": 2759.98, "image_uri": "frame_5440.png"}, {"time": 2760.487, "image_uri": "frame_5441.png"}, {"time": 2760.995, "image_uri": "frame_5442.png"}, {"time": 2761.502, "image_uri": "frame_5443.png"}, {"time": 2762.009, "image_uri": "frame_5444.png"}, {"time": 2762.517, "image_uri": "frame_5445.png"}, {"time": 2763.024, "image_uri": "frame_5446.png"}, {"time": 2763.532, "image_uri": "frame_5447.png"}, {"time": 2764.039, "image_uri": "frame_5448.png"}, {"time": 2764.547, "image_uri": "frame_5449.png"}, {"time": 2765.054, "image_uri": "frame_5450.png"}, {"time": 2765.562, "image_uri": "frame_5451.png"}, {"time": 2766.069, "image_uri": "frame_5452.png"}, {"time": 2766.576, "image_uri": "frame_5453.png"}, {"time": 2767.084, "image_uri": "frame_5454.png"}, {"time": 2767.591, "image_uri": "frame_5455.png"}, {"time": 2768.099, "image_uri": "frame_5456.png"}, {"time": 2768.606, "image_uri": "frame_5457.png"}, {"time": 2769.114, "image_uri": "frame_5458.png"}, {"time": 2769.621, "image_uri": "frame_5459.png"}, {"time": 2770.129, "image_uri": "frame_5460.png"}, {"time": 2770.636, "image_uri": "frame_5461.png"}, {"time": 2771.143, "image_uri": "frame_5462.png"}, {"time": 2771.651, "image_uri": "frame_5463.png"}, {"time": 2772.158, "image_uri": "frame_5464.png"}, {"time": 2772.666, "image_uri": "frame_5465.png"}, {"time": 2773.173, "image_uri": "frame_5466.png"}, {"time": 2773.681, "image_uri": "frame_5467.png"}, {"time": 2774.188, "image_uri": "frame_5468.png"}, {"time": 2774.696, "image_uri": "frame_5469.png"}, {"time": 2775.203, "image_uri": "frame_5470.png"}, {"time": 2775.71, "image_uri": "frame_5471.png"}, {"time": 2776.218, "image_uri": "frame_5472.png"}, {"time": 2776.725, "image_uri": "frame_5473.png"}, {"time": 2777.233, "image_uri": "frame_5474.png"}, {"time": 2777.74, "image_uri": "frame_5475.png"}, {"time": 2778.248, "image_uri": "frame_5476.png"}, {"time": 2778.755, "image_uri": "frame_5477.png"}, {"time": 2779.263, "image_uri": "frame_5478.png"}, {"time": 2779.77, "image_uri": "frame_5479.png"}, {"time": 2780.277, "image_uri": "frame_5480.png"}, {"time": 2780.785, "image_uri": "frame_5481.png"}, {"time": 2781.292, "image_uri": "frame_5482.png"}, {"time": 2781.8, "image_uri": "frame_5483.png"}, {"time": 2782.307, "image_uri": "frame_5484.png"}, {"time": 2782.815, "image_uri": "frame_5485.png"}, {"time": 2783.322, "image_uri": "frame_5486.png"}, {"time": 2783.829, "image_uri": "frame_5487.png"}, {"time": 2784.337, "image_uri": "frame_5488.png"}, {"time": 2784.844, "image_uri": "frame_5489.png"}, {"time": 2785.352, "image_uri": "frame_5490.png"}, {"time": 2785.859, "image_uri": "frame_5491.png"}, {"time": 2786.367, "image_uri": "frame_5492.png"}, {"time": 2786.874, "image_uri": "frame_5493.png"}, {"time": 2787.382, "image_uri": "frame_5494.png"}, {"time": 2787.889, "image_uri": "frame_5495.png"}, {"time": 2788.396, "image_uri": "frame_5496.png"}, {"time": 2788.904, "image_uri": "frame_5497.png"}, {"time": 2789.411, "image_uri": "frame_5498.png"}, {"time": 2789.919, "image_uri": "frame_5499.png"}, {"time": 2790.426, "image_uri": "frame_5500.png"}, {"time": 2790.934, "image_uri": "frame_5501.png"}, {"time": 2791.441, "image_uri": "frame_5502.png"}, {"time": 2791.949, "image_uri": "frame_5503.png"}, {"time": 2792.456, "image_uri": "frame_5504.png"}, {"time": 2792.963, "image_uri": "frame_5505.png"}, {"time": 2793.471, "image_uri": "frame_5506.png"}, {"time": 2793.978, "image_uri": "frame_5507.png"}, {"time": 2794.486, "image_uri": "frame_5508.png"}, {"time": 2794.993, "image_uri": "frame_5509.png"}, {"time": 2795.501, "image_uri": "frame_5510.png"}, {"time": 2796.008, "image_uri": "frame_5511.png"}, {"time": 2796.516, "image_uri": "frame_5512.png"}, {"time": 2797.023, "image_uri": "frame_5513.png"}, {"time": 2797.53, "image_uri": "frame_5514.png"}, {"time": 2798.038, "image_uri": "frame_5515.png"}, {"time": 2798.545, "image_uri": "frame_5516.png"}, {"time": 2799.053, "image_uri": "frame_5517.png"}, {"time": 2799.56, "image_uri": "frame_5518.png"}, {"time": 2800.068, "image_uri": "frame_5519.png"}, {"time": 2800.575, "image_uri": "frame_5520.png"}, {"time": 2801.083, "image_uri": "frame_5521.png"}, {"time": 2801.59, "image_uri": "frame_5522.png"}, {"time": 2802.097, "image_uri": "frame_5523.png"}, {"time": 2802.605, "image_uri": "frame_5524.png"}, {"time": 2803.112, "image_uri": "frame_5525.png"}, {"time": 2803.62, "image_uri": "frame_5526.png"}, {"time": 2804.127, "image_uri": "frame_5527.png"}, {"time": 2804.635, "image_uri": "frame_5528.png"}, {"time": 2805.142, "image_uri": "frame_5529.png"}, {"time": 2805.65, "image_uri": "frame_5530.png"}, {"time": 2806.157, "image_uri": "frame_5531.png"}, {"time": 2806.664, "image_uri": "frame_5532.png"}, {"time": 2807.172, "image_uri": "frame_5533.png"}, {"time": 2807.679, "image_uri": "frame_5534.png"}, {"time": 2808.187, "image_uri": "frame_5535.png"}, {"time": 2808.694, "image_uri": "frame_5536.png"}, {"time": 2809.202, "image_uri": "frame_5537.png"}, {"time": 2809.709, "image_uri": "frame_5538.png"}, {"time": 2810.217, "image_uri": "frame_5539.png"}, {"time": 2810.724, "image_uri": "frame_5540.png"}, {"time": 2811.231, "image_uri": "frame_5541.png"}, {"time": 2811.739, "image_uri": "frame_5542.png"}, {"time": 2812.246, "image_uri": "frame_5543.png"}, {"time": 2812.754, "image_uri": "frame_5544.png"}, {"time": 2813.261, "image_uri": "frame_5545.png"}, {"time": 2813.769, "image_uri": "frame_5546.png"}, {"time": 2814.276, "image_uri": "frame_5547.png"}, {"time": 2814.783, "image_uri": "frame_5548.png"}, {"time": 2815.291, "image_uri": "frame_5549.png"}, {"time": 2815.798, "image_uri": "frame_5550.png"}, {"time": 2816.306, "image_uri": "frame_5551.png"}, {"time": 2816.813, "image_uri": "frame_5552.png"}, {"time": 2817.321, "image_uri": "frame_5553.png"}, {"time": 2817.828, "image_uri": "frame_5554.png"}, {"time": 2818.336, "image_uri": "frame_5555.png"}, {"time": 2818.843, "image_uri": "frame_5556.png"}, {"time": 2819.35, "image_uri": "frame_5557.png"}, {"time": 2819.858, "image_uri": "frame_5558.png"}, {"time": 2820.365, "image_uri": "frame_5559.png"}, {"time": 2820.873, "image_uri": "frame_5560.png"}, {"time": 2821.38, "image_uri": "frame_5561.png"}, {"time": 2821.888, "image_uri": "frame_5562.png"}, {"time": 2822.395, "image_uri": "frame_5563.png"}, {"time": 2822.903, "image_uri": "frame_5564.png"}, {"time": 2823.41, "image_uri": "frame_5565.png"}, {"time": 2823.917, "image_uri": "frame_5566.png"}, {"time": 2824.425, "image_uri": "frame_5567.png"}, {"time": 2824.932, "image_uri": "frame_5568.png"}, {"time": 2825.44, "image_uri": "frame_5569.png"}, {"time": 2825.947, "image_uri": "frame_5570.png"}, {"time": 2826.455, "image_uri": "frame_5571.png"}, {"time": 2826.962, "image_uri": "frame_5572.png"}, {"time": 2827.47, "image_uri": "frame_5573.png"}, {"time": 2827.977, "image_uri": "frame_5574.png"}, {"time": 2828.484, "image_uri": "frame_5575.png"}, {"time": 2828.992, "image_uri": "frame_5576.png"}, {"time": 2829.499, "image_uri": "frame_5577.png"}, {"time": 2830.007, "image_uri": "frame_5578.png"}, {"time": 2830.514, "image_uri": "frame_5579.png"}, {"time": 2831.022, "image_uri": "frame_5580.png"}, {"time": 2831.529, "image_uri": "frame_5581.png"}, {"time": 2832.037, "image_uri": "frame_5582.png"}, {"time": 2832.544, "image_uri": "frame_5583.png"}, {"time": 2833.051, "image_uri": "frame_5584.png"}, {"time": 2833.559, "image_uri": "frame_5585.png"}, {"time": 2834.066, "image_uri": "frame_5586.png"}, {"time": 2834.574, "image_uri": "frame_5587.png"}, {"time": 2835.081, "image_uri": "frame_5588.png"}, {"time": 2835.589, "image_uri": "frame_5589.png"}, {"time": 2836.096, "image_uri": "frame_5590.png"}, {"time": 2836.604, "image_uri": "frame_5591.png"}, {"time": 2837.111, "image_uri": "frame_5592.png"}, {"time": 2837.618, "image_uri": "frame_5593.png"}, {"time": 2838.126, "image_uri": "frame_5594.png"}, {"time": 2838.633, "image_uri": "frame_5595.png"}, {"time": 2839.141, "image_uri": "frame_5596.png"}, {"time": 2839.648, "image_uri": "frame_5597.png"}, {"time": 2840.156, "image_uri": "frame_5598.png"}, {"time": 2840.663, "image_uri": "frame_5599.png"}, {"time": 2841.171, "image_uri": "frame_5600.png"}, {"time": 2841.678, "image_uri": "frame_5601.png"}, {"time": 2842.185, "image_uri": "frame_5602.png"}, {"time": 2842.693, "image_uri": "frame_5603.png"}, {"time": 2843.2, "image_uri": "frame_5604.png"}, {"time": 2843.708, "image_uri": "frame_5605.png"}, {"time": 2844.215, "image_uri": "frame_5606.png"}, {"time": 2844.723, "image_uri": "frame_5607.png"}, {"time": 2845.23, "image_uri": "frame_5608.png"}, {"time": 2845.737, "image_uri": "frame_5609.png"}, {"time": 2846.245, "image_uri": "frame_5610.png"}, {"time": 2846.752, "image_uri": "frame_5611.png"}, {"time": 2847.26, "image_uri": "frame_5612.png"}, {"time": 2847.767, "image_uri": "frame_5613.png"}, {"time": 2848.275, "image_uri": "frame_5614.png"}, {"time": 2848.782, "image_uri": "frame_5615.png"}, {"time": 2849.29, "image_uri": "frame_5616.png"}, {"time": 2849.797, "image_uri": "frame_5617.png"}, {"time": 2850.304, "image_uri": "frame_5618.png"}, {"time": 2850.812, "image_uri": "frame_5619.png"}, {"time": 2851.319, "image_uri": "frame_5620.png"}, {"time": 2851.827, "image_uri": "frame_5621.png"}, {"time": 2852.334, "image_uri": "frame_5622.png"}, {"time": 2852.842, "image_uri": "frame_5623.png"}, {"time": 2853.349, "image_uri": "frame_5624.png"}, {"time": 2853.857, "image_uri": "frame_5625.png"}, {"time": 2854.364, "image_uri": "frame_5626.png"}, {"time": 2854.871, "image_uri": "frame_5627.png"}, {"time": 2855.379, "image_uri": "frame_5628.png"}, {"time": 2855.886, "image_uri": "frame_5629.png"}, {"time": 2856.394, "image_uri": "frame_5630.png"}, {"time": 2856.901, "image_uri": "frame_5631.png"}, {"time": 2857.409, "image_uri": "frame_5632.png"}, {"time": 2857.916, "image_uri": "frame_5633.png"}, {"time": 2858.424, "image_uri": "frame_5634.png"}, {"time": 2858.931, "image_uri": "frame_5635.png"}, {"time": 2859.438, "image_uri": "frame_5636.png"}, {"time": 2859.946, "image_uri": "frame_5637.png"}, {"time": 2860.453, "image_uri": "frame_5638.png"}, {"time": 2860.961, "image_uri": "frame_5639.png"}, {"time": 2861.468, "image_uri": "frame_5640.png"}, {"time": 2861.976, "image_uri": "frame_5641.png"}, {"time": 2862.483, "image_uri": "frame_5642.png"}, {"time": 2862.991, "image_uri": "frame_5643.png"}, {"time": 2863.498, "image_uri": "frame_5644.png"}, {"time": 2864.005, "image_uri": "frame_5645.png"}, {"time": 2864.513, "image_uri": "frame_5646.png"}, {"time": 2865.02, "image_uri": "frame_5647.png"}, {"time": 2865.528, "image_uri": "frame_5648.png"}, {"time": 2866.035, "image_uri": "frame_5649.png"}, {"time": 2866.543, "image_uri": "frame_5650.png"}, {"time": 2867.05, "image_uri": "frame_5651.png"}, {"time": 2867.558, "image_uri": "frame_5652.png"}, {"time": 2868.065, "image_uri": "frame_5653.png"}, {"time": 2868.572, "image_uri": "frame_5654.png"}, {"time": 2869.08, "image_uri": "frame_5655.png"}, {"time": 2869.587, "image_uri": "frame_5656.png"}, {"time": 2870.095, "image_uri": "frame_5657.png"}, {"time": 2870.602, "image_uri": "frame_5658.png"}, {"time": 2871.11, "image_uri": "frame_5659.png"}, {"time": 2871.617, "image_uri": "frame_5660.png"}, {"time": 2872.124, "image_uri": "frame_5661.png"}, {"time": 2872.632, "image_uri": "frame_5662.png"}, {"time": 2873.139, "image_uri": "frame_5663.png"}, {"time": 2873.647, "image_uri": "frame_5664.png"}, {"time": 2874.154, "image_uri": "frame_5665.png"}, {"time": 2874.662, "image_uri": "frame_5666.png"}, {"time": 2875.169, "image_uri": "frame_5667.png"}, {"time": 2875.677, "image_uri": "frame_5668.png"}, {"time": 2876.184, "image_uri": "frame_5669.png"}, {"time": 2876.691, "image_uri": "frame_5670.png"}, {"time": 2877.199, "image_uri": "frame_5671.png"}, {"time": 2877.706, "image_uri": "frame_5672.png"}, {"time": 2878.214, "image_uri": "frame_5673.png"}, {"time": 2878.721, "image_uri": "frame_5674.png"}, {"time": 2879.229, "image_uri": "frame_5675.png"}, {"time": 2879.736, "image_uri": "frame_5676.png"}, {"time": 2880.244, "image_uri": "frame_5677.png"}, {"time": 2880.751, "image_uri": "frame_5678.png"}, {"time": 2881.258, "image_uri": "frame_5679.png"}, {"time": 2881.766, "image_uri": "frame_5680.png"}, {"time": 2882.273, "image_uri": "frame_5681.png"}, {"time": 2882.781, "image_uri": "frame_5682.png"}, {"time": 2883.288, "image_uri": "frame_5683.png"}, {"time": 2883.796, "image_uri": "frame_5684.png"}, {"time": 2884.303, "image_uri": "frame_5685.png"}, {"time": 2884.811, "image_uri": "frame_5686.png"}, {"time": 2885.318, "image_uri": "frame_5687.png"}, {"time": 2885.825, "image_uri": "frame_5688.png"}, {"time": 2886.333, "image_uri": "frame_5689.png"}, {"time": 2886.84, "image_uri": "frame_5690.png"}, {"time": 2887.348, "image_uri": "frame_5691.png"}, {"time": 2887.855, "image_uri": "frame_5692.png"}, {"time": 2888.363, "image_uri": "frame_5693.png"}, {"time": 2888.87, "image_uri": "frame_5694.png"}, {"time": 2889.378, "image_uri": "frame_5695.png"}, {"time": 2889.885, "image_uri": "frame_5696.png"}, {"time": 2890.392, "image_uri": "frame_5697.png"}, {"time": 2890.9, "image_uri": "frame_5698.png"}, {"time": 2891.407, "image_uri": "frame_5699.png"}, {"time": 2891.915, "image_uri": "frame_5700.png"}, {"time": 2892.422, "image_uri": "frame_5701.png"}, {"time": 2892.93, "image_uri": "frame_5702.png"}, {"time": 2893.437, "image_uri": "frame_5703.png"}, {"time": 2893.945, "image_uri": "frame_5704.png"}, {"time": 2894.452, "image_uri": "frame_5705.png"}, {"time": 2894.959, "image_uri": "frame_5706.png"}, {"time": 2895.467, "image_uri": "frame_5707.png"}, {"time": 2895.974, "image_uri": "frame_5708.png"}, {"time": 2896.482, "image_uri": "frame_5709.png"}, {"time": 2896.989, "image_uri": "frame_5710.png"}, {"time": 2897.497, "image_uri": "frame_5711.png"}, {"time": 2898.004, "image_uri": "frame_5712.png"}, {"time": 2898.512, "image_uri": "frame_5713.png"}, {"time": 2899.019, "image_uri": "frame_5714.png"}, {"time": 2899.526, "image_uri": "frame_5715.png"}, {"time": 2900.034, "image_uri": "frame_5716.png"}, {"time": 2900.541, "image_uri": "frame_5717.png"}, {"time": 2901.049, "image_uri": "frame_5718.png"}, {"time": 2901.556, "image_uri": "frame_5719.png"}, {"time": 2902.064, "image_uri": "frame_5720.png"}, {"time": 2902.571, "image_uri": "frame_5721.png"}, {"time": 2903.078, "image_uri": "frame_5722.png"}, {"time": 2903.586, "image_uri": "frame_5723.png"}, {"time": 2904.093, "image_uri": "frame_5724.png"}, {"time": 2904.601, "image_uri": "frame_5725.png"}, {"time": 2905.108, "image_uri": "frame_5726.png"}, {"time": 2905.616, "image_uri": "frame_5727.png"}, {"time": 2906.123, "image_uri": "frame_5728.png"}, {"time": 2906.631, "image_uri": "frame_5729.png"}, {"time": 2907.138, "image_uri": "frame_5730.png"}, {"time": 2907.645, "image_uri": "frame_5731.png"}, {"time": 2908.153, "image_uri": "frame_5732.png"}, {"time": 2908.66, "image_uri": "frame_5733.png"}, {"time": 2909.168, "image_uri": "frame_5734.png"}, {"time": 2909.675, "image_uri": "frame_5735.png"}, {"time": 2910.183, "image_uri": "frame_5736.png"}, {"time": 2910.69, "image_uri": "frame_5737.png"}, {"time": 2911.198, "image_uri": "frame_5738.png"}, {"time": 2911.705, "image_uri": "frame_5739.png"}, {"time": 2912.212, "image_uri": "frame_5740.png"}, {"time": 2912.72, "image_uri": "frame_5741.png"}, {"time": 2913.227, "image_uri": "frame_5742.png"}, {"time": 2913.735, "image_uri": "frame_5743.png"}, {"time": 2914.242, "image_uri": "frame_5744.png"}, {"time": 2914.75, "image_uri": "frame_5745.png"}, {"time": 2915.257, "image_uri": "frame_5746.png"}, {"time": 2915.765, "image_uri": "frame_5747.png"}, {"time": 2916.272, "image_uri": "frame_5748.png"}, {"time": 2916.779, "image_uri": "frame_5749.png"}, {"time": 2917.287, "image_uri": "frame_5750.png"}, {"time": 2917.794, "image_uri": "frame_5751.png"}, {"time": 2918.302, "image_uri": "frame_5752.png"}, {"time": 2918.809, "image_uri": "frame_5753.png"}, {"time": 2919.317, "image_uri": "frame_5754.png"}, {"time": 2919.824, "image_uri": "frame_5755.png"}, {"time": 2920.332, "image_uri": "frame_5756.png"}, {"time": 2920.839, "image_uri": "frame_5757.png"}, {"time": 2921.346, "image_uri": "frame_5758.png"}, {"time": 2921.854, "image_uri": "frame_5759.png"}, {"time": 2922.361, "image_uri": "frame_5760.png"}, {"time": 2922.869, "image_uri": "frame_5761.png"}, {"time": 2923.376, "image_uri": "frame_5762.png"}, {"time": 2923.884, "image_uri": "frame_5763.png"}, {"time": 2924.391, "image_uri": "frame_5764.png"}, {"time": 2924.899, "image_uri": "frame_5765.png"}, {"time": 2925.406, "image_uri": "frame_5766.png"}, {"time": 2925.913, "image_uri": "frame_5767.png"}, {"time": 2926.421, "image_uri": "frame_5768.png"}, {"time": 2926.928, "image_uri": "frame_5769.png"}, {"time": 2927.436, "image_uri": "frame_5770.png"}, {"time": 2927.943, "image_uri": "frame_5771.png"}, {"time": 2928.451, "image_uri": "frame_5772.png"}, {"time": 2928.958, "image_uri": "frame_5773.png"}, {"time": 2929.465, "image_uri": "frame_5774.png"}, {"time": 2929.973, "image_uri": "frame_5775.png"}, {"time": 2930.48, "image_uri": "frame_5776.png"}, {"time": 2930.988, "image_uri": "frame_5777.png"}, {"time": 2931.495, "image_uri": "frame_5778.png"}, {"time": 2932.003, "image_uri": "frame_5779.png"}, {"time": 2932.51, "image_uri": "frame_5780.png"}, {"time": 2933.018, "image_uri": "frame_5781.png"}, {"time": 2933.525, "image_uri": "frame_5782.png"}, {"time": 2934.032, "image_uri": "frame_5783.png"}, {"time": 2934.54, "image_uri": "frame_5784.png"}, {"time": 2935.047, "image_uri": "frame_5785.png"}, {"time": 2935.555, "image_uri": "frame_5786.png"}, {"time": 2936.062, "image_uri": "frame_5787.png"}, {"time": 2936.57, "image_uri": "frame_5788.png"}, {"time": 2937.077, "image_uri": "frame_5789.png"}, {"time": 2937.585, "image_uri": "frame_5790.png"}, {"time": 2938.092, "image_uri": "frame_5791.png"}, {"time": 2938.599, "image_uri": "frame_5792.png"}, {"time": 2939.107, "image_uri": "frame_5793.png"}, {"time": 2939.614, "image_uri": "frame_5794.png"}, {"time": 2940.122, "image_uri": "frame_5795.png"}, {"time": 2940.629, "image_uri": "frame_5796.png"}, {"time": 2941.137, "image_uri": "frame_5797.png"}, {"time": 2941.644, "image_uri": "frame_5798.png"}, {"time": 2942.152, "image_uri": "frame_5799.png"}, {"time": 2942.659, "image_uri": "frame_5800.png"}, {"time": 2943.166, "image_uri": "frame_5801.png"}, {"time": 2943.674, "image_uri": "frame_5802.png"}, {"time": 2944.181, "image_uri": "frame_5803.png"}, {"time": 2944.689, "image_uri": "frame_5804.png"}, {"time": 2945.196, "image_uri": "frame_5805.png"}, {"time": 2945.704, "image_uri": "frame_5806.png"}, {"time": 2946.211, "image_uri": "frame_5807.png"}, {"time": 2946.719, "image_uri": "frame_5808.png"}, {"time": 2947.226, "image_uri": "frame_5809.png"}, {"time": 2947.733, "image_uri": "frame_5810.png"}, {"time": 2948.241, "image_uri": "frame_5811.png"}, {"time": 2948.748, "image_uri": "frame_5812.png"}, {"time": 2949.256, "image_uri": "frame_5813.png"}, {"time": 2949.763, "image_uri": "frame_5814.png"}, {"time": 2950.271, "image_uri": "frame_5815.png"}, {"time": 2950.778, "image_uri": "frame_5816.png"}, {"time": 2951.286, "image_uri": "frame_5817.png"}, {"time": 2951.793, "image_uri": "frame_5818.png"}, {"time": 2952.3, "image_uri": "frame_5819.png"}, {"time": 2952.808, "image_uri": "frame_5820.png"}, {"time": 2953.315, "image_uri": "frame_5821.png"}, {"time": 2953.823, "image_uri": "frame_5822.png"}, {"time": 2954.33, "image_uri": "frame_5823.png"}, {"time": 2954.838, "image_uri": "frame_5824.png"}, {"time": 2955.345, "image_uri": "frame_5825.png"}, {"time": 2955.853, "image_uri": "frame_5826.png"}, {"time": 2956.36, "image_uri": "frame_5827.png"}, {"time": 2956.867, "image_uri": "frame_5828.png"}, {"time": 2957.375, "image_uri": "frame_5829.png"}, {"time": 2957.882, "image_uri": "frame_5830.png"}, {"time": 2958.39, "image_uri": "frame_5831.png"}, {"time": 2958.897, "image_uri": "frame_5832.png"}, {"time": 2959.405, "image_uri": "frame_5833.png"}, {"time": 2959.912, "image_uri": "frame_5834.png"}, {"time": 2960.419, "image_uri": "frame_5835.png"}, {"time": 2960.927, "image_uri": "frame_5836.png"}, {"time": 2961.434, "image_uri": "frame_5837.png"}, {"time": 2961.942, "image_uri": "frame_5838.png"}, {"time": 2962.449, "image_uri": "frame_5839.png"}, {"time": 2962.957, "image_uri": "frame_5840.png"}, {"time": 2963.464, "image_uri": "frame_5841.png"}, {"time": 2963.972, "image_uri": "frame_5842.png"}, {"time": 2964.479, "image_uri": "frame_5843.png"}, {"time": 2964.986, "image_uri": "frame_5844.png"}, {"time": 2965.494, "image_uri": "frame_5845.png"}, {"time": 2966.001, "image_uri": "frame_5846.png"}, {"time": 2966.509, "image_uri": "frame_5847.png"}, {"time": 2967.016, "image_uri": "frame_5848.png"}, {"time": 2967.524, "image_uri": "frame_5849.png"}, {"time": 2968.031, "image_uri": "frame_5850.png"}, {"time": 2968.539, "image_uri": "frame_5851.png"}, {"time": 2969.046, "image_uri": "frame_5852.png"}, {"time": 2969.553, "image_uri": "frame_5853.png"}, {"time": 2970.061, "image_uri": "frame_5854.png"}, {"time": 2970.568, "image_uri": "frame_5855.png"}, {"time": 2971.076, "image_uri": "frame_5856.png"}, {"time": 2971.583, "image_uri": "frame_5857.png"}, {"time": 2972.091, "image_uri": "frame_5858.png"}, {"time": 2972.598, "image_uri": "frame_5859.png"}, {"time": 2973.106, "image_uri": "frame_5860.png"}, {"time": 2973.613, "image_uri": "frame_5861.png"}, {"time": 2974.12, "image_uri": "frame_5862.png"}, {"time": 2974.628, "image_uri": "frame_5863.png"}, {"time": 2975.135, "image_uri": "frame_5864.png"}, {"time": 2975.643, "image_uri": "frame_5865.png"}, {"time": 2976.15, "image_uri": "frame_5866.png"}, {"time": 2976.658, "image_uri": "frame_5867.png"}, {"time": 2977.165, "image_uri": "frame_5868.png"}, {"time": 2977.673, "image_uri": "frame_5869.png"}, {"time": 2978.18, "image_uri": "frame_5870.png"}, {"time": 2978.687, "image_uri": "frame_5871.png"}, {"time": 2979.195, "image_uri": "frame_5872.png"}, {"time": 2979.702, "image_uri": "frame_5873.png"}, {"time": 2980.21, "image_uri": "frame_5874.png"}, {"time": 2980.717, "image_uri": "frame_5875.png"}, {"time": 2981.225, "image_uri": "frame_5876.png"}, {"time": 2981.732, "image_uri": "frame_5877.png"}, {"time": 2982.24, "image_uri": "frame_5878.png"}, {"time": 2982.747, "image_uri": "frame_5879.png"}, {"time": 2983.254, "image_uri": "frame_5880.png"}, {"time": 2983.762, "image_uri": "frame_5881.png"}, {"time": 2984.269, "image_uri": "frame_5882.png"}, {"time": 2984.777, "image_uri": "frame_5883.png"}, {"time": 2985.284, "image_uri": "frame_5884.png"}, {"time": 2985.792, "image_uri": "frame_5885.png"}, {"time": 2986.299, "image_uri": "frame_5886.png"}, {"time": 2986.806, "image_uri": "frame_5887.png"}, {"time": 2987.314, "image_uri": "frame_5888.png"}, {"time": 2987.821, "image_uri": "frame_5889.png"}, {"time": 2988.329, "image_uri": "frame_5890.png"}, {"time": 2988.836, "image_uri": "frame_5891.png"}, {"time": 2989.344, "image_uri": "frame_5892.png"}, {"time": 2989.851, "image_uri": "frame_5893.png"}, {"time": 2990.359, "image_uri": "frame_5894.png"}, {"time": 2990.866, "image_uri": "frame_5895.png"}, {"time": 2991.373, "image_uri": "frame_5896.png"}, {"time": 2991.881, "image_uri": "frame_5897.png"}, {"time": 2992.388, "image_uri": "frame_5898.png"}, {"time": 2992.896, "image_uri": "frame_5899.png"}, {"time": 2993.403, "image_uri": "frame_5900.png"}, {"time": 2993.911, "image_uri": "frame_5901.png"}, {"time": 2994.418, "image_uri": "frame_5902.png"}, {"time": 2994.926, "image_uri": "frame_5903.png"}, {"time": 2995.433, "image_uri": "frame_5904.png"}, {"time": 2995.94, "image_uri": "frame_5905.png"}, {"time": 2996.448, "image_uri": "frame_5906.png"}, {"time": 2996.955, "image_uri": "frame_5907.png"}, {"time": 2997.463, "image_uri": "frame_5908.png"}, {"time": 2997.97, "image_uri": "frame_5909.png"}, {"time": 2998.478, "image_uri": "frame_5910.png"}, {"time": 2998.985, "image_uri": "frame_5911.png"}, {"time": 2999.493, "image_uri": "frame_5912.png"}, {"time": 3000.0, "image_uri": "frame_5913.png"}, {"time": 3000.507, "image_uri": "frame_5914.png"}, {"time": 3001.015, "image_uri": "frame_5915.png"}, {"time": 3001.522, "image_uri": "frame_5916.png"}, {"time": 3002.03, "image_uri": "frame_5917.png"}, {"time": 3002.537, "image_uri": "frame_5918.png"}, {"time": 3003.045, "image_uri": "frame_5919.png"}, {"time": 3003.552, "image_uri": "frame_5920.png"}, {"time": 3004.06, "image_uri": "frame_5921.png"}, {"time": 3004.567, "image_uri": "frame_5922.png"}, {"time": 3005.074, "image_uri": "frame_5923.png"}, {"time": 3005.582, "image_uri": "frame_5924.png"}, {"time": 3006.089, "image_uri": "frame_5925.png"}, {"time": 3006.597, "image_uri": "frame_5926.png"}, {"time": 3007.104, "image_uri": "frame_5927.png"}, {"time": 3007.612, "image_uri": "frame_5928.png"}, {"time": 3008.119, "image_uri": "frame_5929.png"}, {"time": 3008.627, "image_uri": "frame_5930.png"}, {"time": 3009.134, "image_uri": "frame_5931.png"}, {"time": 3009.641, "image_uri": "frame_5932.png"}, {"time": 3010.149, "image_uri": "frame_5933.png"}, {"time": 3010.656, "image_uri": "frame_5934.png"}, {"time": 3011.164, "image_uri": "frame_5935.png"}, {"time": 3011.671, "image_uri": "frame_5936.png"}, {"time": 3012.179, "image_uri": "frame_5937.png"}, {"time": 3012.686, "image_uri": "frame_5938.png"}, {"time": 3013.194, "image_uri": "frame_5939.png"}, {"time": 3013.701, "image_uri": "frame_5940.png"}, {"time": 3014.208, "image_uri": "frame_5941.png"}, {"time": 3014.716, "image_uri": "frame_5942.png"}, {"time": 3015.223, "image_uri": "frame_5943.png"}, {"time": 3015.731, "image_uri": "frame_5944.png"}, {"time": 3016.238, "image_uri": "frame_5945.png"}, {"time": 3016.746, "image_uri": "frame_5946.png"}, {"time": 3017.253, "image_uri": "frame_5947.png"}, {"time": 3017.76, "image_uri": "frame_5948.png"}, {"time": 3018.268, "image_uri": "frame_5949.png"}, {"time": 3018.775, "image_uri": "frame_5950.png"}, {"time": 3019.283, "image_uri": "frame_5951.png"}, {"time": 3019.79, "image_uri": "frame_5952.png"}, {"time": 3020.298, "image_uri": "frame_5953.png"}, {"time": 3020.805, "image_uri": "frame_5954.png"}, {"time": 3021.313, "image_uri": "frame_5955.png"}, {"time": 3021.82, "image_uri": "frame_5956.png"}, {"time": 3022.327, "image_uri": "frame_5957.png"}, {"time": 3022.835, "image_uri": "frame_5958.png"}, {"time": 3023.342, "image_uri": "frame_5959.png"}, {"time": 3023.85, "image_uri": "frame_5960.png"}, {"time": 3024.357, "image_uri": "frame_5961.png"}, {"time": 3024.865, "image_uri": "frame_5962.png"}, {"time": 3025.372, "image_uri": "frame_5963.png"}, {"time": 3025.88, "image_uri": "frame_5964.png"}, {"time": 3026.387, "image_uri": "frame_5965.png"}, {"time": 3026.894, "image_uri": "frame_5966.png"}, {"time": 3027.402, "image_uri": "frame_5967.png"}, {"time": 3027.909, "image_uri": "frame_5968.png"}, {"time": 3028.417, "image_uri": "frame_5969.png"}, {"time": 3028.924, "image_uri": "frame_5970.png"}, {"time": 3029.432, "image_uri": "frame_5971.png"}, {"time": 3029.939, "image_uri": "frame_5972.png"}, {"time": 3030.447, "image_uri": "frame_5973.png"}, {"time": 3030.954, "image_uri": "frame_5974.png"}, {"time": 3031.461, "image_uri": "frame_5975.png"}, {"time": 3031.969, "image_uri": "frame_5976.png"}, {"time": 3032.476, "image_uri": "frame_5977.png"}, {"time": 3032.984, "image_uri": "frame_5978.png"}, {"time": 3033.491, "image_uri": "frame_5979.png"}, {"time": 3033.999, "image_uri": "frame_5980.png"}, {"time": 3034.506, "image_uri": "frame_5981.png"}, {"time": 3035.014, "image_uri": "frame_5982.png"}, {"time": 3035.521, "image_uri": "frame_5983.png"}, {"time": 3036.028, "image_uri": "frame_5984.png"}, {"time": 3036.536, "image_uri": "frame_5985.png"}, {"time": 3037.043, "image_uri": "frame_5986.png"}, {"time": 3037.551, "image_uri": "frame_5987.png"}, {"time": 3038.058, "image_uri": "frame_5988.png"}, {"time": 3038.566, "image_uri": "frame_5989.png"}, {"time": 3039.073, "image_uri": "frame_5990.png"}, {"time": 3039.581, "image_uri": "frame_5991.png"}, {"time": 3040.088, "image_uri": "frame_5992.png"}, {"time": 3040.595, "image_uri": "frame_5993.png"}, {"time": 3041.103, "image_uri": "frame_5994.png"}, {"time": 3041.61, "image_uri": "frame_5995.png"}, {"time": 3042.118, "image_uri": "frame_5996.png"}, {"time": 3042.625, "image_uri": "frame_5997.png"}, {"time": 3043.133, "image_uri": "frame_5998.png"}, {"time": 3043.64, "image_uri": "frame_5999.png"}, {"time": 3044.147, "image_uri": "frame_6000.png"}, {"time": 3044.655, "image_uri": "frame_6001.png"}, {"time": 3045.162, "image_uri": "frame_6002.png"}, {"time": 3045.67, "image_uri": "frame_6003.png"}, {"time": 3046.177, "image_uri": "frame_6004.png"}, {"time": 3046.685, "image_uri": "frame_6005.png"}, {"time": 3047.192, "image_uri": "frame_6006.png"}, {"time": 3047.7, "image_uri": "frame_6007.png"}, {"time": 3048.207, "image_uri": "frame_6008.png"}, {"time": 3048.714, "image_uri": "frame_6009.png"}, {"time": 3049.222, "image_uri": "frame_6010.png"}, {"time": 3049.729, "image_uri": "frame_6011.png"}, {"time": 3050.237, "image_uri": "frame_6012.png"}, {"time": 3050.744, "image_uri": "frame_6013.png"}, {"time": 3051.252, "image_uri": "frame_6014.png"}, {"time": 3051.759, "image_uri": "frame_6015.png"}, {"time": 3052.267, "image_uri": "frame_6016.png"}, {"time": 3052.774, "image_uri": "frame_6017.png"}, {"time": 3053.281, "image_uri": "frame_6018.png"}, {"time": 3053.789, "image_uri": "frame_6019.png"}, {"time": 3054.296, "image_uri": "frame_6020.png"}, {"time": 3054.804, "image_uri": "frame_6021.png"}, {"time": 3055.311, "image_uri": "frame_6022.png"}, {"time": 3055.819, "image_uri": "frame_6023.png"}, {"time": 3056.326, "image_uri": "frame_6024.png"}, {"time": 3056.834, "image_uri": "frame_6025.png"}, {"time": 3057.341, "image_uri": "frame_6026.png"}, {"time": 3057.848, "image_uri": "frame_6027.png"}, {"time": 3058.356, "image_uri": "frame_6028.png"}, {"time": 3058.863, "image_uri": "frame_6029.png"}, {"time": 3059.371, "image_uri": "frame_6030.png"}, {"time": 3059.878, "image_uri": "frame_6031.png"}, {"time": 3060.386, "image_uri": "frame_6032.png"}, {"time": 3060.893, "image_uri": "frame_6033.png"}, {"time": 3061.401, "image_uri": "frame_6034.png"}, {"time": 3061.908, "image_uri": "frame_6035.png"}, {"time": 3062.415, "image_uri": "frame_6036.png"}, {"time": 3062.923, "image_uri": "frame_6037.png"}, {"time": 3063.43, "image_uri": "frame_6038.png"}, {"time": 3063.938, "image_uri": "frame_6039.png"}, {"time": 3064.445, "image_uri": "frame_6040.png"}, {"time": 3064.953, "image_uri": "frame_6041.png"}, {"time": 3065.46, "image_uri": "frame_6042.png"}, {"time": 3065.968, "image_uri": "frame_6043.png"}, {"time": 3066.475, "image_uri": "frame_6044.png"}, {"time": 3066.982, "image_uri": "frame_6045.png"}, {"time": 3067.49, "image_uri": "frame_6046.png"}, {"time": 3067.997, "image_uri": "frame_6047.png"}, {"time": 3068.505, "image_uri": "frame_6048.png"}, {"time": 3069.012, "image_uri": "frame_6049.png"}, {"time": 3069.52, "image_uri": "frame_6050.png"}, {"time": 3070.027, "image_uri": "frame_6051.png"}, {"time": 3070.535, "image_uri": "frame_6052.png"}, {"time": 3071.042, "image_uri": "frame_6053.png"}, {"time": 3071.549, "image_uri": "frame_6054.png"}, {"time": 3072.057, "image_uri": "frame_6055.png"}, {"time": 3072.564, "image_uri": "frame_6056.png"}, {"time": 3073.072, "image_uri": "frame_6057.png"}, {"time": 3073.579, "image_uri": "frame_6058.png"}, {"time": 3074.087, "image_uri": "frame_6059.png"}, {"time": 3074.594, "image_uri": "frame_6060.png"}, {"time": 3075.101, "image_uri": "frame_6061.png"}, {"time": 3075.609, "image_uri": "frame_6062.png"}, {"time": 3076.116, "image_uri": "frame_6063.png"}, {"time": 3076.624, "image_uri": "frame_6064.png"}, {"time": 3077.131, "image_uri": "frame_6065.png"}, {"time": 3077.639, "image_uri": "frame_6066.png"}, {"time": 3078.146, "image_uri": "frame_6067.png"}, {"time": 3078.654, "image_uri": "frame_6068.png"}, {"time": 3079.161, "image_uri": "frame_6069.png"}, {"time": 3079.668, "image_uri": "frame_6070.png"}, {"time": 3080.176, "image_uri": "frame_6071.png"}, {"time": 3080.683, "image_uri": "frame_6072.png"}, {"time": 3081.191, "image_uri": "frame_6073.png"}, {"time": 3081.698, "image_uri": "frame_6074.png"}, {"time": 3082.206, "image_uri": "frame_6075.png"}, {"time": 3082.713, "image_uri": "frame_6076.png"}, {"time": 3083.221, "image_uri": "frame_6077.png"}, {"time": 3083.728, "image_uri": "frame_6078.png"}, {"time": 3084.235, "image_uri": "frame_6079.png"}, {"time": 3084.743, "image_uri": "frame_6080.png"}, {"time": 3085.25, "image_uri": "frame_6081.png"}, {"time": 3085.758, "image_uri": "frame_6082.png"}, {"time": 3086.265, "image_uri": "frame_6083.png"}, {"time": 3086.773, "image_uri": "frame_6084.png"}, {"time": 3087.28, "image_uri": "frame_6085.png"}, {"time": 3087.788, "image_uri": "frame_6086.png"}, {"time": 3088.295, "image_uri": "frame_6087.png"}, {"time": 3088.802, "image_uri": "frame_6088.png"}, {"time": 3089.31, "image_uri": "frame_6089.png"}, {"time": 3089.817, "image_uri": "frame_6090.png"}, {"time": 3090.325, "image_uri": "frame_6091.png"}, {"time": 3090.832, "image_uri": "frame_6092.png"}, {"time": 3091.34, "image_uri": "frame_6093.png"}, {"time": 3091.847, "image_uri": "frame_6094.png"}, {"time": 3092.355, "image_uri": "frame_6095.png"}, {"time": 3092.862, "image_uri": "frame_6096.png"}, {"time": 3093.369, "image_uri": "frame_6097.png"}, {"time": 3093.877, "image_uri": "frame_6098.png"}, {"time": 3094.384, "image_uri": "frame_6099.png"}, {"time": 3094.892, "image_uri": "frame_6100.png"}, {"time": 3095.399, "image_uri": "frame_6101.png"}, {"time": 3095.907, "image_uri": "frame_6102.png"}, {"time": 3096.414, "image_uri": "frame_6103.png"}, {"time": 3096.922, "image_uri": "frame_6104.png"}, {"time": 3097.429, "image_uri": "frame_6105.png"}, {"time": 3097.936, "image_uri": "frame_6106.png"}, {"time": 3098.444, "image_uri": "frame_6107.png"}, {"time": 3098.951, "image_uri": "frame_6108.png"}, {"time": 3099.459, "image_uri": "frame_6109.png"}, {"time": 3099.966, "image_uri": "frame_6110.png"}, {"time": 3100.474, "image_uri": "frame_6111.png"}, {"time": 3100.981, "image_uri": "frame_6112.png"}, {"time": 3101.488, "image_uri": "frame_6113.png"}, {"time": 3101.996, "image_uri": "frame_6114.png"}, {"time": 3102.503, "image_uri": "frame_6115.png"}, {"time": 3103.011, "image_uri": "frame_6116.png"}, {"time": 3103.518, "image_uri": "frame_6117.png"}, {"time": 3104.026, "image_uri": "frame_6118.png"}, {"time": 3104.533, "image_uri": "frame_6119.png"}, {"time": 3105.041, "image_uri": "frame_6120.png"}, {"time": 3105.548, "image_uri": "frame_6121.png"}, {"time": 3106.055, "image_uri": "frame_6122.png"}, {"time": 3106.563, "image_uri": "frame_6123.png"}, {"time": 3107.07, "image_uri": "frame_6124.png"}, {"time": 3107.578, "image_uri": "frame_6125.png"}, {"time": 3108.085, "image_uri": "frame_6126.png"}, {"time": 3108.593, "image_uri": "frame_6127.png"}, {"time": 3109.1, "image_uri": "frame_6128.png"}, {"time": 3109.608, "image_uri": "frame_6129.png"}, {"time": 3110.115, "image_uri": "frame_6130.png"}, {"time": 3110.622, "image_uri": "frame_6131.png"}, {"time": 3111.13, "image_uri": "frame_6132.png"}, {"time": 3111.637, "image_uri": "frame_6133.png"}, {"time": 3112.145, "image_uri": "frame_6134.png"}, {"time": 3112.652, "image_uri": "frame_6135.png"}, {"time": 3113.16, "image_uri": "frame_6136.png"}, {"time": 3113.667, "image_uri": "frame_6137.png"}, {"time": 3114.175, "image_uri": "frame_6138.png"}, {"time": 3114.682, "image_uri": "frame_6139.png"}, {"time": 3115.189, "image_uri": "frame_6140.png"}, {"time": 3115.697, "image_uri": "frame_6141.png"}, {"time": 3116.204, "image_uri": "frame_6142.png"}, {"time": 3116.712, "image_uri": "frame_6143.png"}, {"time": 3117.219, "image_uri": "frame_6144.png"}, {"time": 3117.727, "image_uri": "frame_6145.png"}, {"time": 3118.234, "image_uri": "frame_6146.png"}, {"time": 3118.742, "image_uri": "frame_6147.png"}, {"time": 3119.249, "image_uri": "frame_6148.png"}, {"time": 3119.756, "image_uri": "frame_6149.png"}, {"time": 3120.264, "image_uri": "frame_6150.png"}, {"time": 3120.771, "image_uri": "frame_6151.png"}, {"time": 3121.279, "image_uri": "frame_6152.png"}, {"time": 3121.786, "image_uri": "frame_6153.png"}, {"time": 3122.294, "image_uri": "frame_6154.png"}, {"time": 3122.801, "image_uri": "frame_6155.png"}, {"time": 3123.309, "image_uri": "frame_6156.png"}, {"time": 3123.816, "image_uri": "frame_6157.png"}, {"time": 3124.323, "image_uri": "frame_6158.png"}, {"time": 3124.831, "image_uri": "frame_6159.png"}, {"time": 3125.338, "image_uri": "frame_6160.png"}, {"time": 3125.846, "image_uri": "frame_6161.png"}, {"time": 3126.353, "image_uri": "frame_6162.png"}, {"time": 3126.861, "image_uri": "frame_6163.png"}, {"time": 3127.368, "image_uri": "frame_6164.png"}, {"time": 3127.876, "image_uri": "frame_6165.png"}, {"time": 3128.383, "image_uri": "frame_6166.png"}, {"time": 3128.89, "image_uri": "frame_6167.png"}, {"time": 3129.398, "image_uri": "frame_6168.png"}, {"time": 3129.905, "image_uri": "frame_6169.png"}, {"time": 3130.413, "image_uri": "frame_6170.png"}, {"time": 3130.92, "image_uri": "frame_6171.png"}, {"time": 3131.428, "image_uri": "frame_6172.png"}, {"time": 3131.935, "image_uri": "frame_6173.png"}, {"time": 3132.442, "image_uri": "frame_6174.png"}, {"time": 3132.95, "image_uri": "frame_6175.png"}, {"time": 3133.457, "image_uri": "frame_6176.png"}, {"time": 3133.965, "image_uri": "frame_6177.png"}, {"time": 3134.472, "image_uri": "frame_6178.png"}, {"time": 3134.98, "image_uri": "frame_6179.png"}, {"time": 3135.487, "image_uri": "frame_6180.png"}, {"time": 3135.995, "image_uri": "frame_6181.png"}, {"time": 3136.502, "image_uri": "frame_6182.png"}, {"time": 3137.009, "image_uri": "frame_6183.png"}, {"time": 3137.517, "image_uri": "frame_6184.png"}, {"time": 3138.024, "image_uri": "frame_6185.png"}, {"time": 3138.532, "image_uri": "frame_6186.png"}, {"time": 3139.039, "image_uri": "frame_6187.png"}, {"time": 3139.547, "image_uri": "frame_6188.png"}, {"time": 3140.054, "image_uri": "frame_6189.png"}, {"time": 3140.562, "image_uri": "frame_6190.png"}, {"time": 3141.069, "image_uri": "frame_6191.png"}, {"time": 3141.576, "image_uri": "frame_6192.png"}, {"time": 3142.084, "image_uri": "frame_6193.png"}, {"time": 3142.591, "image_uri": "frame_6194.png"}, {"time": 3143.099, "image_uri": "frame_6195.png"}, {"time": 3143.606, "image_uri": "frame_6196.png"}, {"time": 3144.114, "image_uri": "frame_6197.png"}, {"time": 3144.621, "image_uri": "frame_6198.png"}, {"time": 3145.129, "image_uri": "frame_6199.png"}, {"time": 3145.636, "image_uri": "frame_6200.png"}, {"time": 3146.143, "image_uri": "frame_6201.png"}, {"time": 3146.651, "image_uri": "frame_6202.png"}, {"time": 3147.158, "image_uri": "frame_6203.png"}, {"time": 3147.666, "image_uri": "frame_6204.png"}, {"time": 3148.173, "image_uri": "frame_6205.png"}, {"time": 3148.681, "image_uri": "frame_6206.png"}, {"time": 3149.188, "image_uri": "frame_6207.png"}, {"time": 3149.696, "image_uri": "frame_6208.png"}, {"time": 3150.203, "image_uri": "frame_6209.png"}, {"time": 3150.71, "image_uri": "frame_6210.png"}, {"time": 3151.218, "image_uri": "frame_6211.png"}, {"time": 3151.725, "image_uri": "frame_6212.png"}, {"time": 3152.233, "image_uri": "frame_6213.png"}, {"time": 3152.74, "image_uri": "frame_6214.png"}, {"time": 3153.248, "image_uri": "frame_6215.png"}, {"time": 3153.755, "image_uri": "frame_6216.png"}, {"time": 3154.263, "image_uri": "frame_6217.png"}, {"time": 3154.77, "image_uri": "frame_6218.png"}, {"time": 3155.277, "image_uri": "frame_6219.png"}, {"time": 3155.785, "image_uri": "frame_6220.png"}, {"time": 3156.292, "image_uri": "frame_6221.png"}, {"time": 3156.8, "image_uri": "frame_6222.png"}, {"time": 3157.307, "image_uri": "frame_6223.png"}, {"time": 3157.815, "image_uri": "frame_6224.png"}, {"time": 3158.322, "image_uri": "frame_6225.png"}, {"time": 3158.829, "image_uri": "frame_6226.png"}, {"time": 3159.337, "image_uri": "frame_6227.png"}, {"time": 3159.844, "image_uri": "frame_6228.png"}, {"time": 3160.352, "image_uri": "frame_6229.png"}, {"time": 3160.859, "image_uri": "frame_6230.png"}, {"time": 3161.367, "image_uri": "frame_6231.png"}, {"time": 3161.874, "image_uri": "frame_6232.png"}, {"time": 3162.382, "image_uri": "frame_6233.png"}, {"time": 3162.889, "image_uri": "frame_6234.png"}, {"time": 3163.396, "image_uri": "frame_6235.png"}, {"time": 3163.904, "image_uri": "frame_6236.png"}, {"time": 3164.411, "image_uri": "frame_6237.png"}, {"time": 3164.919, "image_uri": "frame_6238.png"}, {"time": 3165.426, "image_uri": "frame_6239.png"}, {"time": 3165.934, "image_uri": "frame_6240.png"}, {"time": 3166.441, "image_uri": "frame_6241.png"}, {"time": 3166.949, "image_uri": "frame_6242.png"}, {"time": 3167.456, "image_uri": "frame_6243.png"}, {"time": 3167.963, "image_uri": "frame_6244.png"}, {"time": 3168.471, "image_uri": "frame_6245.png"}, {"time": 3168.978, "image_uri": "frame_6246.png"}, {"time": 3169.486, "image_uri": "frame_6247.png"}, {"time": 3169.993, "image_uri": "frame_6248.png"}, {"time": 3170.501, "image_uri": "frame_6249.png"}, {"time": 3171.008, "image_uri": "frame_6250.png"}, {"time": 3171.516, "image_uri": "frame_6251.png"}, {"time": 3172.023, "image_uri": "frame_6252.png"}, {"time": 3172.53, "image_uri": "frame_6253.png"}, {"time": 3173.038, "image_uri": "frame_6254.png"}, {"time": 3173.545, "image_uri": "frame_6255.png"}, {"time": 3174.053, "image_uri": "frame_6256.png"}, {"time": 3174.56, "image_uri": "frame_6257.png"}, {"time": 3175.068, "image_uri": "frame_6258.png"}, {"time": 3175.575, "image_uri": "frame_6259.png"}, {"time": 3176.083, "image_uri": "frame_6260.png"}, {"time": 3176.59, "image_uri": "frame_6261.png"}, {"time": 3177.097, "image_uri": "frame_6262.png"}, {"time": 3177.605, "image_uri": "frame_6263.png"}, {"time": 3178.112, "image_uri": "frame_6264.png"}, {"time": 3178.62, "image_uri": "frame_6265.png"}, {"time": 3179.127, "image_uri": "frame_6266.png"}, {"time": 3179.635, "image_uri": "frame_6267.png"}, {"time": 3180.142, "image_uri": "frame_6268.png"}, {"time": 3180.65, "image_uri": "frame_6269.png"}, {"time": 3181.157, "image_uri": "frame_6270.png"}, {"time": 3181.664, "image_uri": "frame_6271.png"}, {"time": 3182.172, "image_uri": "frame_6272.png"}, {"time": 3182.679, "image_uri": "frame_6273.png"}, {"time": 3183.187, "image_uri": "frame_6274.png"}, {"time": 3183.694, "image_uri": "frame_6275.png"}, {"time": 3184.202, "image_uri": "frame_6276.png"}, {"time": 3184.709, "image_uri": "frame_6277.png"}, {"time": 3185.217, "image_uri": "frame_6278.png"}, {"time": 3185.724, "image_uri": "frame_6279.png"}, {"time": 3186.231, "image_uri": "frame_6280.png"}, {"time": 3186.739, "image_uri": "frame_6281.png"}, {"time": 3187.246, "image_uri": "frame_6282.png"}, {"time": 3187.754, "image_uri": "frame_6283.png"}, {"time": 3188.261, "image_uri": "frame_6284.png"}, {"time": 3188.769, "image_uri": "frame_6285.png"}, {"time": 3189.276, "image_uri": "frame_6286.png"}, {"time": 3189.783, "image_uri": "frame_6287.png"}, {"time": 3190.291, "image_uri": "frame_6288.png"}, {"time": 3190.798, "image_uri": "frame_6289.png"}, {"time": 3191.306, "image_uri": "frame_6290.png"}, {"time": 3191.813, "image_uri": "frame_6291.png"}, {"time": 3192.321, "image_uri": "frame_6292.png"}, {"time": 3192.828, "image_uri": "frame_6293.png"}, {"time": 3193.336, "image_uri": "frame_6294.png"}, {"time": 3193.843, "image_uri": "frame_6295.png"}, {"time": 3194.35, "image_uri": "frame_6296.png"}, {"time": 3194.858, "image_uri": "frame_6297.png"}, {"time": 3195.365, "image_uri": "frame_6298.png"}, {"time": 3195.873, "image_uri": "frame_6299.png"}, {"time": 3196.38, "image_uri": "frame_6300.png"}, {"time": 3196.888, "image_uri": "frame_6301.png"}, {"time": 3197.395, "image_uri": "frame_6302.png"}, {"time": 3197.903, "image_uri": "frame_6303.png"}, {"time": 3198.41, "image_uri": "frame_6304.png"}, {"time": 3198.917, "image_uri": "frame_6305.png"}, {"time": 3199.425, "image_uri": "frame_6306.png"}, {"time": 3199.932, "image_uri": "frame_6307.png"}, {"time": 3200.44, "image_uri": "frame_6308.png"}, {"time": 3200.947, "image_uri": "frame_6309.png"}, {"time": 3201.455, "image_uri": "frame_6310.png"}, {"time": 3201.962, "image_uri": "frame_6311.png"}, {"time": 3202.47, "image_uri": "frame_6312.png"}, {"time": 3202.977, "image_uri": "frame_6313.png"}, {"time": 3203.484, "image_uri": "frame_6314.png"}, {"time": 3203.992, "image_uri": "frame_6315.png"}, {"time": 3204.499, "image_uri": "frame_6316.png"}, {"time": 3205.007, "image_uri": "frame_6317.png"}, {"time": 3205.514, "image_uri": "frame_6318.png"}, {"time": 3206.022, "image_uri": "frame_6319.png"}, {"time": 3206.529, "image_uri": "frame_6320.png"}, {"time": 3207.037, "image_uri": "frame_6321.png"}, {"time": 3207.544, "image_uri": "frame_6322.png"}, {"time": 3208.051, "image_uri": "frame_6323.png"}, {"time": 3208.559, "image_uri": "frame_6324.png"}, {"time": 3209.066, "image_uri": "frame_6325.png"}, {"time": 3209.574, "image_uri": "frame_6326.png"}, {"time": 3210.081, "image_uri": "frame_6327.png"}, {"time": 3210.589, "image_uri": "frame_6328.png"}, {"time": 3211.096, "image_uri": "frame_6329.png"}, {"time": 3211.604, "image_uri": "frame_6330.png"}, {"time": 3212.111, "image_uri": "frame_6331.png"}, {"time": 3212.618, "image_uri": "frame_6332.png"}, {"time": 3213.126, "image_uri": "frame_6333.png"}, {"time": 3213.633, "image_uri": "frame_6334.png"}, {"time": 3214.141, "image_uri": "frame_6335.png"}, {"time": 3214.648, "image_uri": "frame_6336.png"}, {"time": 3215.156, "image_uri": "frame_6337.png"}, {"time": 3215.663, "image_uri": "frame_6338.png"}, {"time": 3216.171, "image_uri": "frame_6339.png"}, {"time": 3216.678, "image_uri": "frame_6340.png"}, {"time": 3217.185, "image_uri": "frame_6341.png"}, {"time": 3217.693, "image_uri": "frame_6342.png"}, {"time": 3218.2, "image_uri": "frame_6343.png"}, {"time": 3218.708, "image_uri": "frame_6344.png"}, {"time": 3219.215, "image_uri": "frame_6345.png"}, {"time": 3219.723, "image_uri": "frame_6346.png"}, {"time": 3220.23, "image_uri": "frame_6347.png"}, {"time": 3220.737, "image_uri": "frame_6348.png"}, {"time": 3221.245, "image_uri": "frame_6349.png"}, {"time": 3221.752, "image_uri": "frame_6350.png"}, {"time": 3222.26, "image_uri": "frame_6351.png"}, {"time": 3222.767, "image_uri": "frame_6352.png"}, {"time": 3223.275, "image_uri": "frame_6353.png"}, {"time": 3223.782, "image_uri": "frame_6354.png"}, {"time": 3224.29, "image_uri": "frame_6355.png"}, {"time": 3224.797, "image_uri": "frame_6356.png"}, {"time": 3225.304, "image_uri": "frame_6357.png"}, {"time": 3225.812, "image_uri": "frame_6358.png"}, {"time": 3226.319, "image_uri": "frame_6359.png"}, {"time": 3226.827, "image_uri": "frame_6360.png"}, {"time": 3227.334, "image_uri": "frame_6361.png"}, {"time": 3227.842, "image_uri": "frame_6362.png"}, {"time": 3228.349, "image_uri": "frame_6363.png"}, {"time": 3228.857, "image_uri": "frame_6364.png"}, {"time": 3229.364, "image_uri": "frame_6365.png"}, {"time": 3229.871, "image_uri": "frame_6366.png"}, {"time": 3230.379, "image_uri": "frame_6367.png"}, {"time": 3230.886, "image_uri": "frame_6368.png"}, {"time": 3231.394, "image_uri": "frame_6369.png"}, {"time": 3231.901, "image_uri": "frame_6370.png"}, {"time": 3232.409, "image_uri": "frame_6371.png"}, {"time": 3232.916, "image_uri": "frame_6372.png"}, {"time": 3233.424, "image_uri": "frame_6373.png"}, {"time": 3233.931, "image_uri": "frame_6374.png"}, {"time": 3234.438, "image_uri": "frame_6375.png"}, {"time": 3234.946, "image_uri": "frame_6376.png"}, {"time": 3235.453, "image_uri": "frame_6377.png"}, {"time": 3235.961, "image_uri": "frame_6378.png"}, {"time": 3236.468, "image_uri": "frame_6379.png"}, {"time": 3236.976, "image_uri": "frame_6380.png"}, {"time": 3237.483, "image_uri": "frame_6381.png"}, {"time": 3237.991, "image_uri": "frame_6382.png"}, {"time": 3238.498, "image_uri": "frame_6383.png"}, {"time": 3239.005, "image_uri": "frame_6384.png"}, {"time": 3239.513, "image_uri": "frame_6385.png"}, {"time": 3240.02, "image_uri": "frame_6386.png"}, {"time": 3240.528, "image_uri": "frame_6387.png"}, {"time": 3241.035, "image_uri": "frame_6388.png"}, {"time": 3241.543, "image_uri": "frame_6389.png"}, {"time": 3242.05, "image_uri": "frame_6390.png"}, {"time": 3242.558, "image_uri": "frame_6391.png"}, {"time": 3243.065, "image_uri": "frame_6392.png"}, {"time": 3243.572, "image_uri": "frame_6393.png"}, {"time": 3244.08, "image_uri": "frame_6394.png"}, {"time": 3244.587, "image_uri": "frame_6395.png"}, {"time": 3245.095, "image_uri": "frame_6396.png"}, {"time": 3245.602, "image_uri": "frame_6397.png"}, {"time": 3246.11, "image_uri": "frame_6398.png"}, {"time": 3246.617, "image_uri": "frame_6399.png"}, {"time": 3247.124, "image_uri": "frame_6400.png"}, {"time": 3247.632, "image_uri": "frame_6401.png"}, {"time": 3248.139, "image_uri": "frame_6402.png"}, {"time": 3248.647, "image_uri": "frame_6403.png"}, {"time": 3249.154, "image_uri": "frame_6404.png"}, {"time": 3249.662, "image_uri": "frame_6405.png"}, {"time": 3250.169, "image_uri": "frame_6406.png"}, {"time": 3250.677, "image_uri": "frame_6407.png"}, {"time": 3251.184, "image_uri": "frame_6408.png"}, {"time": 3251.691, "image_uri": "frame_6409.png"}, {"time": 3252.199, "image_uri": "frame_6410.png"}, {"time": 3252.706, "image_uri": "frame_6411.png"}, {"time": 3253.214, "image_uri": "frame_6412.png"}, {"time": 3253.721, "image_uri": "frame_6413.png"}, {"time": 3254.229, "image_uri": "frame_6414.png"}, {"time": 3254.736, "image_uri": "frame_6415.png"}, {"time": 3255.244, "image_uri": "frame_6416.png"}, {"time": 3255.751, "image_uri": "frame_6417.png"}, {"time": 3256.258, "image_uri": "frame_6418.png"}, {"time": 3256.766, "image_uri": "frame_6419.png"}, {"time": 3257.273, "image_uri": "frame_6420.png"}, {"time": 3257.781, "image_uri": "frame_6421.png"}, {"time": 3258.288, "image_uri": "frame_6422.png"}, {"time": 3258.796, "image_uri": "frame_6423.png"}, {"time": 3259.303, "image_uri": "frame_6424.png"}, {"time": 3259.811, "image_uri": "frame_6425.png"}, {"time": 3260.318, "image_uri": "frame_6426.png"}, {"time": 3260.825, "image_uri": "frame_6427.png"}, {"time": 3261.333, "image_uri": "frame_6428.png"}, {"time": 3261.84, "image_uri": "frame_6429.png"}, {"time": 3262.348, "image_uri": "frame_6430.png"}, {"time": 3262.855, "image_uri": "frame_6431.png"}, {"time": 3263.363, "image_uri": "frame_6432.png"}, {"time": 3263.87, "image_uri": "frame_6433.png"}, {"time": 3264.378, "image_uri": "frame_6434.png"}, {"time": 3264.885, "image_uri": "frame_6435.png"}, {"time": 3265.392, "image_uri": "frame_6436.png"}, {"time": 3265.9, "image_uri": "frame_6437.png"}, {"time": 3266.407, "image_uri": "frame_6438.png"}, {"time": 3266.915, "image_uri": "frame_6439.png"}, {"time": 3267.422, "image_uri": "frame_6440.png"}, {"time": 3267.93, "image_uri": "frame_6441.png"}, {"time": 3268.437, "image_uri": "frame_6442.png"}, {"time": 3268.945, "image_uri": "frame_6443.png"}, {"time": 3269.452, "image_uri": "frame_6444.png"}, {"time": 3269.959, "image_uri": "frame_6445.png"}, {"time": 3270.467, "image_uri": "frame_6446.png"}, {"time": 3270.974, "image_uri": "frame_6447.png"}, {"time": 3271.482, "image_uri": "frame_6448.png"}, {"time": 3271.989, "image_uri": "frame_6449.png"}, {"time": 3272.497, "image_uri": "frame_6450.png"}, {"time": 3273.004, "image_uri": "frame_6451.png"}, {"time": 3273.512, "image_uri": "frame_6452.png"}, {"time": 3274.019, "image_uri": "frame_6453.png"}, {"time": 3274.526, "image_uri": "frame_6454.png"}, {"time": 3275.034, "image_uri": "frame_6455.png"}, {"time": 3275.541, "image_uri": "frame_6456.png"}, {"time": 3276.049, "image_uri": "frame_6457.png"}, {"time": 3276.556, "image_uri": "frame_6458.png"}, {"time": 3277.064, "image_uri": "frame_6459.png"}, {"time": 3277.571, "image_uri": "frame_6460.png"}, {"time": 3278.078, "image_uri": "frame_6461.png"}, {"time": 3278.586, "image_uri": "frame_6462.png"}, {"time": 3279.093, "image_uri": "frame_6463.png"}, {"time": 3279.601, "image_uri": "frame_6464.png"}, {"time": 3280.108, "image_uri": "frame_6465.png"}, {"time": 3280.616, "image_uri": "frame_6466.png"}, {"time": 3281.123, "image_uri": "frame_6467.png"}, {"time": 3281.631, "image_uri": "frame_6468.png"}, {"time": 3282.138, "image_uri": "frame_6469.png"}, {"time": 3282.645, "image_uri": "frame_6470.png"}, {"time": 3283.153, "image_uri": "frame_6471.png"}, {"time": 3283.66, "image_uri": "frame_6472.png"}, {"time": 3284.168, "image_uri": "frame_6473.png"}, {"time": 3284.675, "image_uri": "frame_6474.png"}, {"time": 3285.183, "image_uri": "frame_6475.png"}, {"time": 3285.69, "image_uri": "frame_6476.png"}, {"time": 3286.198, "image_uri": "frame_6477.png"}, {"time": 3286.705, "image_uri": "frame_6478.png"}, {"time": 3287.212, "image_uri": "frame_6479.png"}, {"time": 3287.72, "image_uri": "frame_6480.png"}, {"time": 3288.227, "image_uri": "frame_6481.png"}, {"time": 3288.735, "image_uri": "frame_6482.png"}, {"time": 3289.242, "image_uri": "frame_6483.png"}, {"time": 3289.75, "image_uri": "frame_6484.png"}, {"time": 3290.257, "image_uri": "frame_6485.png"}, {"time": 3290.765, "image_uri": "frame_6486.png"}, {"time": 3291.272, "image_uri": "frame_6487.png"}, {"time": 3291.779, "image_uri": "frame_6488.png"}, {"time": 3292.287, "image_uri": "frame_6489.png"}, {"time": 3292.794, "image_uri": "frame_6490.png"}, {"time": 3293.302, "image_uri": "frame_6491.png"}, {"time": 3293.809, "image_uri": "frame_6492.png"}, {"time": 3294.317, "image_uri": "frame_6493.png"}, {"time": 3294.824, "image_uri": "frame_6494.png"}, {"time": 3295.332, "image_uri": "frame_6495.png"}, {"time": 3295.839, "image_uri": "frame_6496.png"}, {"time": 3296.346, "image_uri": "frame_6497.png"}, {"time": 3296.854, "image_uri": "frame_6498.png"}, {"time": 3297.361, "image_uri": "frame_6499.png"}, {"time": 3297.869, "image_uri": "frame_6500.png"}, {"time": 3298.376, "image_uri": "frame_6501.png"}, {"time": 3298.884, "image_uri": "frame_6502.png"}, {"time": 3299.391, "image_uri": "frame_6503.png"}, {"time": 3299.899, "image_uri": "frame_6504.png"}, {"time": 3300.406, "image_uri": "frame_6505.png"}, {"time": 3300.913, "image_uri": "frame_6506.png"}, {"time": 3301.421, "image_uri": "frame_6507.png"}, {"time": 3301.928, "image_uri": "frame_6508.png"}, {"time": 3302.436, "image_uri": "frame_6509.png"}, {"time": 3302.943, "image_uri": "frame_6510.png"}, {"time": 3303.451, "image_uri": "frame_6511.png"}, {"time": 3303.958, "image_uri": "frame_6512.png"}, {"time": 3304.465, "image_uri": "frame_6513.png"}, {"time": 3304.973, "image_uri": "frame_6514.png"}, {"time": 3305.48, "image_uri": "frame_6515.png"}, {"time": 3305.988, "image_uri": "frame_6516.png"}, {"time": 3306.495, "image_uri": "frame_6517.png"}, {"time": 3307.003, "image_uri": "frame_6518.png"}, {"time": 3307.51, "image_uri": "frame_6519.png"}, {"time": 3308.018, "image_uri": "frame_6520.png"}, {"time": 3308.525, "image_uri": "frame_6521.png"}, {"time": 3309.032, "image_uri": "frame_6522.png"}, {"time": 3309.54, "image_uri": "frame_6523.png"}, {"time": 3310.047, "image_uri": "frame_6524.png"}, {"time": 3310.555, "image_uri": "frame_6525.png"}, {"time": 3311.062, "image_uri": "frame_6526.png"}, {"time": 3311.57, "image_uri": "frame_6527.png"}, {"time": 3312.077, "image_uri": "frame_6528.png"}, {"time": 3312.585, "image_uri": "frame_6529.png"}, {"time": 3313.092, "image_uri": "frame_6530.png"}, {"time": 3313.599, "image_uri": "frame_6531.png"}, {"time": 3314.107, "image_uri": "frame_6532.png"}, {"time": 3314.614, "image_uri": "frame_6533.png"}, {"time": 3315.122, "image_uri": "frame_6534.png"}, {"time": 3315.629, "image_uri": "frame_6535.png"}, {"time": 3316.137, "image_uri": "frame_6536.png"}, {"time": 3316.644, "image_uri": "frame_6537.png"}, {"time": 3317.152, "image_uri": "frame_6538.png"}, {"time": 3317.659, "image_uri": "frame_6539.png"}, {"time": 3318.166, "image_uri": "frame_6540.png"}, {"time": 3318.674, "image_uri": "frame_6541.png"}, {"time": 3319.181, "image_uri": "frame_6542.png"}, {"time": 3319.689, "image_uri": "frame_6543.png"}, {"time": 3320.196, "image_uri": "frame_6544.png"}, {"time": 3320.704, "image_uri": "frame_6545.png"}, {"time": 3321.211, "image_uri": "frame_6546.png"}, {"time": 3321.719, "image_uri": "frame_6547.png"}, {"time": 3322.226, "image_uri": "frame_6548.png"}, {"time": 3322.733, "image_uri": "frame_6549.png"}, {"time": 3323.241, "image_uri": "frame_6550.png"}, {"time": 3323.748, "image_uri": "frame_6551.png"}, {"time": 3324.256, "image_uri": "frame_6552.png"}, {"time": 3324.763, "image_uri": "frame_6553.png"}, {"time": 3325.271, "image_uri": "frame_6554.png"}, {"time": 3325.778, "image_uri": "frame_6555.png"}, {"time": 3326.286, "image_uri": "frame_6556.png"}, {"time": 3326.793, "image_uri": "frame_6557.png"}, {"time": 3327.3, "image_uri": "frame_6558.png"}, {"time": 3327.808, "image_uri": "frame_6559.png"}, {"time": 3328.315, "image_uri": "frame_6560.png"}, {"time": 3328.823, "image_uri": "frame_6561.png"}, {"time": 3329.33, "image_uri": "frame_6562.png"}, {"time": 3329.838, "image_uri": "frame_6563.png"}, {"time": 3330.345, "image_uri": "frame_6564.png"}, {"time": 3330.853, "image_uri": "frame_6565.png"}, {"time": 3331.36, "image_uri": "frame_6566.png"}, {"time": 3331.867, "image_uri": "frame_6567.png"}, {"time": 3332.375, "image_uri": "frame_6568.png"}, {"time": 3332.882, "image_uri": "frame_6569.png"}, {"time": 3333.39, "image_uri": "frame_6570.png"}, {"time": 3333.897, "image_uri": "frame_6571.png"}, {"time": 3334.405, "image_uri": "frame_6572.png"}, {"time": 3334.912, "image_uri": "frame_6573.png"}, {"time": 3335.419, "image_uri": "frame_6574.png"}, {"time": 3335.927, "image_uri": "frame_6575.png"}, {"time": 3336.434, "image_uri": "frame_6576.png"}, {"time": 3336.942, "image_uri": "frame_6577.png"}, {"time": 3337.449, "image_uri": "frame_6578.png"}, {"time": 3337.957, "image_uri": "frame_6579.png"}, {"time": 3338.464, "image_uri": "frame_6580.png"}, {"time": 3338.972, "image_uri": "frame_6581.png"}, {"time": 3339.479, "image_uri": "frame_6582.png"}, {"time": 3339.986, "image_uri": "frame_6583.png"}, {"time": 3340.494, "image_uri": "frame_6584.png"}, {"time": 3341.001, "image_uri": "frame_6585.png"}, {"time": 3341.509, "image_uri": "frame_6586.png"}, {"time": 3342.016, "image_uri": "frame_6587.png"}, {"time": 3342.524, "image_uri": "frame_6588.png"}, {"time": 3343.031, "image_uri": "frame_6589.png"}, {"time": 3343.539, "image_uri": "frame_6590.png"}, {"time": 3344.046, "image_uri": "frame_6591.png"}, {"time": 3344.553, "image_uri": "frame_6592.png"}, {"time": 3345.061, "image_uri": "frame_6593.png"}, {"time": 3345.568, "image_uri": "frame_6594.png"}, {"time": 3346.076, "image_uri": "frame_6595.png"}, {"time": 3346.583, "image_uri": "frame_6596.png"}, {"time": 3347.091, "image_uri": "frame_6597.png"}, {"time": 3347.598, "image_uri": "frame_6598.png"}, {"time": 3348.106, "image_uri": "frame_6599.png"}, {"time": 3348.613, "image_uri": "frame_6600.png"}, {"time": 3349.12, "image_uri": "frame_6601.png"}, {"time": 3349.628, "image_uri": "frame_6602.png"}, {"time": 3350.135, "image_uri": "frame_6603.png"}, {"time": 3350.643, "image_uri": "frame_6604.png"}, {"time": 3351.15, "image_uri": "frame_6605.png"}, {"time": 3351.658, "image_uri": "frame_6606.png"}, {"time": 3352.165, "image_uri": "frame_6607.png"}, {"time": 3352.673, "image_uri": "frame_6608.png"}, {"time": 3353.18, "image_uri": "frame_6609.png"}, {"time": 3353.687, "image_uri": "frame_6610.png"}, {"time": 3354.195, "image_uri": "frame_6611.png"}, {"time": 3354.702, "image_uri": "frame_6612.png"}, {"time": 3355.21, "image_uri": "frame_6613.png"}, {"time": 3355.717, "image_uri": "frame_6614.png"}, {"time": 3356.225, "image_uri": "frame_6615.png"}, {"time": 3356.732, "image_uri": "frame_6616.png"}, {"time": 3357.24, "image_uri": "frame_6617.png"}, {"time": 3357.747, "image_uri": "frame_6618.png"}, {"time": 3358.254, "image_uri": "frame_6619.png"}, {"time": 3358.762, "image_uri": "frame_6620.png"}, {"time": 3359.269, "image_uri": "frame_6621.png"}, {"time": 3359.777, "image_uri": "frame_6622.png"}, {"time": 3360.284, "image_uri": "frame_6623.png"}, {"time": 3360.792, "image_uri": "frame_6624.png"}, {"time": 3361.299, "image_uri": "frame_6625.png"}, {"time": 3361.806, "image_uri": "frame_6626.png"}, {"time": 3362.314, "image_uri": "frame_6627.png"}, {"time": 3362.821, "image_uri": "frame_6628.png"}, {"time": 3363.329, "image_uri": "frame_6629.png"}, {"time": 3363.836, "image_uri": "frame_6630.png"}, {"time": 3364.344, "image_uri": "frame_6631.png"}, {"time": 3364.851, "image_uri": "frame_6632.png"}, {"time": 3365.359, "image_uri": "frame_6633.png"}, {"time": 3365.866, "image_uri": "frame_6634.png"}, {"time": 3366.373, "image_uri": "frame_6635.png"}, {"time": 3366.881, "image_uri": "frame_6636.png"}, {"time": 3367.388, "image_uri": "frame_6637.png"}, {"time": 3367.896, "image_uri": "frame_6638.png"}, {"time": 3368.403, "image_uri": "frame_6639.png"}, {"time": 3368.911, "image_uri": "frame_6640.png"}, {"time": 3369.418, "image_uri": "frame_6641.png"}, {"time": 3369.926, "image_uri": "frame_6642.png"}, {"time": 3370.433, "image_uri": "frame_6643.png"}, {"time": 3370.94, "image_uri": "frame_6644.png"}, {"time": 3371.448, "image_uri": "frame_6645.png"}, {"time": 3371.955, "image_uri": "frame_6646.png"}, {"time": 3372.463, "image_uri": "frame_6647.png"}, {"time": 3372.97, "image_uri": "frame_6648.png"}, {"time": 3373.478, "image_uri": "frame_6649.png"}, {"time": 3373.985, "image_uri": "frame_6650.png"}, {"time": 3374.493, "image_uri": "frame_6651.png"}, {"time": 3375.0, "image_uri": "frame_6652.png"}, {"time": 3375.507, "image_uri": "frame_6653.png"}, {"time": 3376.015, "image_uri": "frame_6654.png"}, {"time": 3376.522, "image_uri": "frame_6655.png"}, {"time": 3377.03, "image_uri": "frame_6656.png"}, {"time": 3377.537, "image_uri": "frame_6657.png"}, {"time": 3378.045, "image_uri": "frame_6658.png"}, {"time": 3378.552, "image_uri": "frame_6659.png"}, {"time": 3379.06, "image_uri": "frame_6660.png"}, {"time": 3379.567, "image_uri": "frame_6661.png"}, {"time": 3380.074, "image_uri": "frame_6662.png"}, {"time": 3380.582, "image_uri": "frame_6663.png"}, {"time": 3381.089, "image_uri": "frame_6664.png"}, {"time": 3381.597, "image_uri": "frame_6665.png"}, {"time": 3382.104, "image_uri": "frame_6666.png"}, {"time": 3382.612, "image_uri": "frame_6667.png"}, {"time": 3383.119, "image_uri": "frame_6668.png"}, {"time": 3383.627, "image_uri": "frame_6669.png"}, {"time": 3384.134, "image_uri": "frame_6670.png"}, {"time": 3384.641, "image_uri": "frame_6671.png"}, {"time": 3385.149, "image_uri": "frame_6672.png"}, {"time": 3385.656, "image_uri": "frame_6673.png"}, {"time": 3386.164, "image_uri": "frame_6674.png"}, {"time": 3386.671, "image_uri": "frame_6675.png"}, {"time": 3387.179, "image_uri": "frame_6676.png"}, {"time": 3387.686, "image_uri": "frame_6677.png"}, {"time": 3388.194, "image_uri": "frame_6678.png"}, {"time": 3388.701, "image_uri": "frame_6679.png"}, {"time": 3389.208, "image_uri": "frame_6680.png"}, {"time": 3389.716, "image_uri": "frame_6681.png"}, {"time": 3390.223, "image_uri": "frame_6682.png"}, {"time": 3390.731, "image_uri": "frame_6683.png"}, {"time": 3391.238, "image_uri": "frame_6684.png"}, {"time": 3391.746, "image_uri": "frame_6685.png"}, {"time": 3392.253, "image_uri": "frame_6686.png"}, {"time": 3392.76, "image_uri": "frame_6687.png"}, {"time": 3393.268, "image_uri": "frame_6688.png"}, {"time": 3393.775, "image_uri": "frame_6689.png"}, {"time": 3394.283, "image_uri": "frame_6690.png"}, {"time": 3394.79, "image_uri": "frame_6691.png"}, {"time": 3395.298, "image_uri": "frame_6692.png"}, {"time": 3395.805, "image_uri": "frame_6693.png"}, {"time": 3396.313, "image_uri": "frame_6694.png"}, {"time": 3396.82, "image_uri": "frame_6695.png"}, {"time": 3397.327, "image_uri": "frame_6696.png"}, {"time": 3397.835, "image_uri": "frame_6697.png"}, {"time": 3398.342, "image_uri": "frame_6698.png"}, {"time": 3398.85, "image_uri": "frame_6699.png"}, {"time": 3399.357, "image_uri": "frame_6700.png"}, {"time": 3399.865, "image_uri": "frame_6701.png"}, {"time": 3400.372, "image_uri": "frame_6702.png"}, {"time": 3400.88, "image_uri": "frame_6703.png"}, {"time": 3401.387, "image_uri": "frame_6704.png"}, {"time": 3401.894, "image_uri": "frame_6705.png"}, {"time": 3402.402, "image_uri": "frame_6706.png"}, {"time": 3402.909, "image_uri": "frame_6707.png"}, {"time": 3403.417, "image_uri": "frame_6708.png"}, {"time": 3403.924, "image_uri": "frame_6709.png"}, {"time": 3404.432, "image_uri": "frame_6710.png"}, {"time": 3404.939, "image_uri": "frame_6711.png"}, {"time": 3405.447, "image_uri": "frame_6712.png"}, {"time": 3405.954, "image_uri": "frame_6713.png"}, {"time": 3406.461, "image_uri": "frame_6714.png"}, {"time": 3406.969, "image_uri": "frame_6715.png"}, {"time": 3407.476, "image_uri": "frame_6716.png"}, {"time": 3407.984, "image_uri": "frame_6717.png"}, {"time": 3408.491, "image_uri": "frame_6718.png"}, {"time": 3408.999, "image_uri": "frame_6719.png"}, {"time": 3409.506, "image_uri": "frame_6720.png"}, {"time": 3410.014, "image_uri": "frame_6721.png"}, {"time": 3410.521, "image_uri": "frame_6722.png"}, {"time": 3411.028, "image_uri": "frame_6723.png"}, {"time": 3411.536, "image_uri": "frame_6724.png"}, {"time": 3412.043, "image_uri": "frame_6725.png"}, {"time": 3412.551, "image_uri": "frame_6726.png"}, {"time": 3413.058, "image_uri": "frame_6727.png"}, {"time": 3413.566, "image_uri": "frame_6728.png"}, {"time": 3414.073, "image_uri": "frame_6729.png"}, {"time": 3414.581, "image_uri": "frame_6730.png"}, {"time": 3415.088, "image_uri": "frame_6731.png"}, {"time": 3415.595, "image_uri": "frame_6732.png"}, {"time": 3416.103, "image_uri": "frame_6733.png"}, {"time": 3416.61, "image_uri": "frame_6734.png"}, {"time": 3417.118, "image_uri": "frame_6735.png"}, {"time": 3417.625, "image_uri": "frame_6736.png"}, {"time": 3418.133, "image_uri": "frame_6737.png"}, {"time": 3418.64, "image_uri": "frame_6738.png"}, {"time": 3419.147, "image_uri": "frame_6739.png"}, {"time": 3419.655, "image_uri": "frame_6740.png"}, {"time": 3420.162, "image_uri": "frame_6741.png"}, {"time": 3420.67, "image_uri": "frame_6742.png"}, {"time": 3421.177, "image_uri": "frame_6743.png"}, {"time": 3421.685, "image_uri": "frame_6744.png"}, {"time": 3422.192, "image_uri": "frame_6745.png"}, {"time": 3422.7, "image_uri": "frame_6746.png"}, {"time": 3423.207, "image_uri": "frame_6747.png"}, {"time": 3423.714, "image_uri": "frame_6748.png"}, {"time": 3424.222, "image_uri": "frame_6749.png"}, {"time": 3424.729, "image_uri": "frame_6750.png"}, {"time": 3425.237, "image_uri": "frame_6751.png"}, {"time": 3425.744, "image_uri": "frame_6752.png"}, {"time": 3426.252, "image_uri": "frame_6753.png"}, {"time": 3426.759, "image_uri": "frame_6754.png"}, {"time": 3427.267, "image_uri": "frame_6755.png"}, {"time": 3427.774, "image_uri": "frame_6756.png"}, {"time": 3428.281, "image_uri": "frame_6757.png"}, {"time": 3428.789, "image_uri": "frame_6758.png"}, {"time": 3429.296, "image_uri": "frame_6759.png"}, {"time": 3429.804, "image_uri": "frame_6760.png"}, {"time": 3430.311, "image_uri": "frame_6761.png"}, {"time": 3430.819, "image_uri": "frame_6762.png"}, {"time": 3431.326, "image_uri": "frame_6763.png"}, {"time": 3431.834, "image_uri": "frame_6764.png"}, {"time": 3432.341, "image_uri": "frame_6765.png"}, {"time": 3432.848, "image_uri": "frame_6766.png"}, {"time": 3433.356, "image_uri": "frame_6767.png"}, {"time": 3433.863, "image_uri": "frame_6768.png"}, {"time": 3434.371, "image_uri": "frame_6769.png"}, {"time": 3434.878, "image_uri": "frame_6770.png"}, {"time": 3435.386, "image_uri": "frame_6771.png"}, {"time": 3435.893, "image_uri": "frame_6772.png"}, {"time": 3436.401, "image_uri": "frame_6773.png"}, {"time": 3436.908, "image_uri": "frame_6774.png"}, {"time": 3437.415, "image_uri": "frame_6775.png"}, {"time": 3437.923, "image_uri": "frame_6776.png"}, {"time": 3438.43, "image_uri": "frame_6777.png"}, {"time": 3438.938, "image_uri": "frame_6778.png"}, {"time": 3439.445, "image_uri": "frame_6779.png"}, {"time": 3439.953, "image_uri": "frame_6780.png"}, {"time": 3440.46, "image_uri": "frame_6781.png"}, {"time": 3440.968, "image_uri": "frame_6782.png"}, {"time": 3441.475, "image_uri": "frame_6783.png"}, {"time": 3441.982, "image_uri": "frame_6784.png"}, {"time": 3442.49, "image_uri": "frame_6785.png"}, {"time": 3442.997, "image_uri": "frame_6786.png"}, {"time": 3443.505, "image_uri": "frame_6787.png"}, {"time": 3444.012, "image_uri": "frame_6788.png"}, {"time": 3444.52, "image_uri": "frame_6789.png"}, {"time": 3445.027, "image_uri": "frame_6790.png"}, {"time": 3445.535, "image_uri": "frame_6791.png"}, {"time": 3446.042, "image_uri": "frame_6792.png"}, {"time": 3446.549, "image_uri": "frame_6793.png"}, {"time": 3447.057, "image_uri": "frame_6794.png"}, {"time": 3447.564, "image_uri": "frame_6795.png"}, {"time": 3448.072, "image_uri": "frame_6796.png"}, {"time": 3448.579, "image_uri": "frame_6797.png"}, {"time": 3449.087, "image_uri": "frame_6798.png"}, {"time": 3449.594, "image_uri": "frame_6799.png"}, {"time": 3450.101, "image_uri": "frame_6800.png"}, {"time": 3450.609, "image_uri": "frame_6801.png"}, {"time": 3451.116, "image_uri": "frame_6802.png"}, {"time": 3451.624, "image_uri": "frame_6803.png"}, {"time": 3452.131, "image_uri": "frame_6804.png"}, {"time": 3452.639, "image_uri": "frame_6805.png"}, {"time": 3453.146, "image_uri": "frame_6806.png"}, {"time": 3453.654, "image_uri": "frame_6807.png"}, {"time": 3454.161, "image_uri": "frame_6808.png"}, {"time": 3454.668, "image_uri": "frame_6809.png"}, {"time": 3455.176, "image_uri": "frame_6810.png"}, {"time": 3455.683, "image_uri": "frame_6811.png"}, {"time": 3456.191, "image_uri": "frame_6812.png"}, {"time": 3456.698, "image_uri": "frame_6813.png"}, {"time": 3457.206, "image_uri": "frame_6814.png"}, {"time": 3457.713, "image_uri": "frame_6815.png"}, {"time": 3458.221, "image_uri": "frame_6816.png"}, {"time": 3458.728, "image_uri": "frame_6817.png"}, {"time": 3459.235, "image_uri": "frame_6818.png"}, {"time": 3459.743, "image_uri": "frame_6819.png"}, {"time": 3460.25, "image_uri": "frame_6820.png"}, {"time": 3460.758, "image_uri": "frame_6821.png"}, {"time": 3461.265, "image_uri": "frame_6822.png"}, {"time": 3461.773, "image_uri": "frame_6823.png"}, {"time": 3462.28, "image_uri": "frame_6824.png"}, {"time": 3462.788, "image_uri": "frame_6825.png"}, {"time": 3463.295, "image_uri": "frame_6826.png"}, {"time": 3463.802, "image_uri": "frame_6827.png"}, {"time": 3464.31, "image_uri": "frame_6828.png"}, {"time": 3464.817, "image_uri": "frame_6829.png"}, {"time": 3465.325, "image_uri": "frame_6830.png"}, {"time": 3465.832, "image_uri": "frame_6831.png"}, {"time": 3466.34, "image_uri": "frame_6832.png"}, {"time": 3466.847, "image_uri": "frame_6833.png"}, {"time": 3467.355, "image_uri": "frame_6834.png"}, {"time": 3467.862, "image_uri": "frame_6835.png"}, {"time": 3468.369, "image_uri": "frame_6836.png"}, {"time": 3468.877, "image_uri": "frame_6837.png"}, {"time": 3469.384, "image_uri": "frame_6838.png"}, {"time": 3469.892, "image_uri": "frame_6839.png"}, {"time": 3470.399, "image_uri": "frame_6840.png"}, {"time": 3470.907, "image_uri": "frame_6841.png"}, {"time": 3471.414, "image_uri": "frame_6842.png"}, {"time": 3471.922, "image_uri": "frame_6843.png"}, {"time": 3472.429, "image_uri": "frame_6844.png"}, {"time": 3472.936, "image_uri": "frame_6845.png"}, {"time": 3473.444, "image_uri": "frame_6846.png"}, {"time": 3473.951, "image_uri": "frame_6847.png"}, {"time": 3474.459, "image_uri": "frame_6848.png"}, {"time": 3474.966, "image_uri": "frame_6849.png"}, {"time": 3475.474, "image_uri": "frame_6850.png"}, {"time": 3475.981, "image_uri": "frame_6851.png"}, {"time": 3476.488, "image_uri": "frame_6852.png"}, {"time": 3476.996, "image_uri": "frame_6853.png"}, {"time": 3477.503, "image_uri": "frame_6854.png"}, {"time": 3478.011, "image_uri": "frame_6855.png"}, {"time": 3478.518, "image_uri": "frame_6856.png"}, {"time": 3479.026, "image_uri": "frame_6857.png"}, {"time": 3479.533, "image_uri": "frame_6858.png"}, {"time": 3480.041, "image_uri": "frame_6859.png"}, {"time": 3480.548, "image_uri": "frame_6860.png"}, {"time": 3481.055, "image_uri": "frame_6861.png"}, {"time": 3481.563, "image_uri": "frame_6862.png"}, {"time": 3482.07, "image_uri": "frame_6863.png"}, {"time": 3482.578, "image_uri": "frame_6864.png"}, {"time": 3483.085, "image_uri": "frame_6865.png"}, {"time": 3483.593, "image_uri": "frame_6866.png"}, {"time": 3484.1, "image_uri": "frame_6867.png"}, {"time": 3484.608, "image_uri": "frame_6868.png"}, {"time": 3485.115, "image_uri": "frame_6869.png"}, {"time": 3485.622, "image_uri": "frame_6870.png"}, {"time": 3486.13, "image_uri": "frame_6871.png"}, {"time": 3486.637, "image_uri": "frame_6872.png"}, {"time": 3487.145, "image_uri": "frame_6873.png"}, {"time": 3487.652, "image_uri": "frame_6874.png"}, {"time": 3488.16, "image_uri": "frame_6875.png"}, {"time": 3488.667, "image_uri": "frame_6876.png"}, {"time": 3489.175, "image_uri": "frame_6877.png"}, {"time": 3489.682, "image_uri": "frame_6878.png"}, {"time": 3490.189, "image_uri": "frame_6879.png"}, {"time": 3490.697, "image_uri": "frame_6880.png"}, {"time": 3491.204, "image_uri": "frame_6881.png"}, {"time": 3491.712, "image_uri": "frame_6882.png"}, {"time": 3492.219, "image_uri": "frame_6883.png"}, {"time": 3492.727, "image_uri": "frame_6884.png"}, {"time": 3493.234, "image_uri": "frame_6885.png"}, {"time": 3493.742, "image_uri": "frame_6886.png"}, {"time": 3494.249, "image_uri": "frame_6887.png"}, {"time": 3494.756, "image_uri": "frame_6888.png"}, {"time": 3495.264, "image_uri": "frame_6889.png"}, {"time": 3495.771, "image_uri": "frame_6890.png"}, {"time": 3496.279, "image_uri": "frame_6891.png"}, {"time": 3496.786, "image_uri": "frame_6892.png"}, {"time": 3497.294, "image_uri": "frame_6893.png"}, {"time": 3497.801, "image_uri": "frame_6894.png"}, {"time": 3498.309, "image_uri": "frame_6895.png"}, {"time": 3498.816, "image_uri": "frame_6896.png"}, {"time": 3499.323, "image_uri": "frame_6897.png"}, {"time": 3499.831, "image_uri": "frame_6898.png"}, {"time": 3500.338, "image_uri": "frame_6899.png"}, {"time": 3500.846, "image_uri": "frame_6900.png"}, {"time": 3501.353, "image_uri": "frame_6901.png"}, {"time": 3501.861, "image_uri": "frame_6902.png"}, {"time": 3502.368, "image_uri": "frame_6903.png"}, {"time": 3502.876, "image_uri": "frame_6904.png"}, {"time": 3503.383, "image_uri": "frame_6905.png"}, {"time": 3503.89, "image_uri": "frame_6906.png"}, {"time": 3504.398, "image_uri": "frame_6907.png"}, {"time": 3504.905, "image_uri": "frame_6908.png"}, {"time": 3505.413, "image_uri": "frame_6909.png"}, {"time": 3505.92, "image_uri": "frame_6910.png"}, {"time": 3506.428, "image_uri": "frame_6911.png"}, {"time": 3506.935, "image_uri": "frame_6912.png"}, {"time": 3507.442, "image_uri": "frame_6913.png"}, {"time": 3507.95, "image_uri": "frame_6914.png"}, {"time": 3508.457, "image_uri": "frame_6915.png"}, {"time": 3508.965, "image_uri": "frame_6916.png"}, {"time": 3509.472, "image_uri": "frame_6917.png"}, {"time": 3509.98, "image_uri": "frame_6918.png"}, {"time": 3510.487, "image_uri": "frame_6919.png"}, {"time": 3510.995, "image_uri": "frame_6920.png"}, {"time": 3511.502, "image_uri": "frame_6921.png"}, {"time": 3512.009, "image_uri": "frame_6922.png"}, {"time": 3512.517, "image_uri": "frame_6923.png"}, {"time": 3513.024, "image_uri": "frame_6924.png"}, {"time": 3513.532, "image_uri": "frame_6925.png"}, {"time": 3514.039, "image_uri": "frame_6926.png"}, {"time": 3514.547, "image_uri": "frame_6927.png"}, {"time": 3515.054, "image_uri": "frame_6928.png"}, {"time": 3515.562, "image_uri": "frame_6929.png"}, {"time": 3516.069, "image_uri": "frame_6930.png"}, {"time": 3516.576, "image_uri": "frame_6931.png"}, {"time": 3517.084, "image_uri": "frame_6932.png"}, {"time": 3517.591, "image_uri": "frame_6933.png"}, {"time": 3518.099, "image_uri": "frame_6934.png"}, {"time": 3518.606, "image_uri": "frame_6935.png"}, {"time": 3519.114, "image_uri": "frame_6936.png"}, {"time": 3519.621, "image_uri": "frame_6937.png"}, {"time": 3520.129, "image_uri": "frame_6938.png"}, {"time": 3520.636, "image_uri": "frame_6939.png"}, {"time": 3521.143, "image_uri": "frame_6940.png"}, {"time": 3521.651, "image_uri": "frame_6941.png"}, {"time": 3522.158, "image_uri": "frame_6942.png"}, {"time": 3522.666, "image_uri": "frame_6943.png"}, {"time": 3523.173, "image_uri": "frame_6944.png"}, {"time": 3523.681, "image_uri": "frame_6945.png"}, {"time": 3524.188, "image_uri": "frame_6946.png"}, {"time": 3524.696, "image_uri": "frame_6947.png"}, {"time": 3525.203, "image_uri": "frame_6948.png"}, {"time": 3525.71, "image_uri": "frame_6949.png"}, {"time": 3526.218, "image_uri": "frame_6950.png"}, {"time": 3526.725, "image_uri": "frame_6951.png"}, {"time": 3527.233, "image_uri": "frame_6952.png"}, {"time": 3527.74, "image_uri": "frame_6953.png"}, {"time": 3528.248, "image_uri": "frame_6954.png"}, {"time": 3528.755, "image_uri": "frame_6955.png"}, {"time": 3529.263, "image_uri": "frame_6956.png"}, {"time": 3529.77, "image_uri": "frame_6957.png"}, {"time": 3530.277, "image_uri": "frame_6958.png"}, {"time": 3530.785, "image_uri": "frame_6959.png"}, {"time": 3531.292, "image_uri": "frame_6960.png"}, {"time": 3531.8, "image_uri": "frame_6961.png"}, {"time": 3532.307, "image_uri": "frame_6962.png"}, {"time": 3532.815, "image_uri": "frame_6963.png"}, {"time": 3533.322, "image_uri": "frame_6964.png"}, {"time": 3533.829, "image_uri": "frame_6965.png"}, {"time": 3534.337, "image_uri": "frame_6966.png"}, {"time": 3534.844, "image_uri": "frame_6967.png"}, {"time": 3535.352, "image_uri": "frame_6968.png"}, {"time": 3535.859, "image_uri": "frame_6969.png"}, {"time": 3536.367, "image_uri": "frame_6970.png"}, {"time": 3536.874, "image_uri": "frame_6971.png"}, {"time": 3537.382, "image_uri": "frame_6972.png"}, {"time": 3537.889, "image_uri": "frame_6973.png"}, {"time": 3538.396, "image_uri": "frame_6974.png"}, {"time": 3538.904, "image_uri": "frame_6975.png"}, {"time": 3539.411, "image_uri": "frame_6976.png"}, {"time": 3539.919, "image_uri": "frame_6977.png"}, {"time": 3540.426, "image_uri": "frame_6978.png"}, {"time": 3540.934, "image_uri": "frame_6979.png"}, {"time": 3541.441, "image_uri": "frame_6980.png"}, {"time": 3541.949, "image_uri": "frame_6981.png"}, {"time": 3542.456, "image_uri": "frame_6982.png"}, {"time": 3542.963, "image_uri": "frame_6983.png"}, {"time": 3543.471, "image_uri": "frame_6984.png"}, {"time": 3543.978, "image_uri": "frame_6985.png"}, {"time": 3544.486, "image_uri": "frame_6986.png"}, {"time": 3544.993, "image_uri": "frame_6987.png"}, {"time": 3545.501, "image_uri": "frame_6988.png"}, {"time": 3546.008, "image_uri": "frame_6989.png"}, {"time": 3546.516, "image_uri": "frame_6990.png"}, {"time": 3547.023, "image_uri": "frame_6991.png"}, {"time": 3547.53, "image_uri": "frame_6992.png"}, {"time": 3548.038, "image_uri": "frame_6993.png"}, {"time": 3548.545, "image_uri": "frame_6994.png"}, {"time": 3549.053, "image_uri": "frame_6995.png"}, {"time": 3549.56, "image_uri": "frame_6996.png"}, {"time": 3550.068, "image_uri": "frame_6997.png"}, {"time": 3550.575, "image_uri": "frame_6998.png"}, {"time": 3551.083, "image_uri": "frame_6999.png"}, {"time": 3551.59, "image_uri": "frame_7000.png"}, {"time": 3552.097, "image_uri": "frame_7001.png"}, {"time": 3552.605, "image_uri": "frame_7002.png"}, {"time": 3553.112, "image_uri": "frame_7003.png"}, {"time": 3553.62, "image_uri": "frame_7004.png"}, {"time": 3554.127, "image_uri": "frame_7005.png"}, {"time": 3554.635, "image_uri": "frame_7006.png"}, {"time": 3555.142, "image_uri": "frame_7007.png"}, {"time": 3555.65, "image_uri": "frame_7008.png"}, {"time": 3556.157, "image_uri": "frame_7009.png"}, {"time": 3556.664, "image_uri": "frame_7010.png"}, {"time": 3557.172, "image_uri": "frame_7011.png"}, {"time": 3557.679, "image_uri": "frame_7012.png"}, {"time": 3558.187, "image_uri": "frame_7013.png"}, {"time": 3558.694, "image_uri": "frame_7014.png"}, {"time": 3559.202, "image_uri": "frame_7015.png"}, {"time": 3559.709, "image_uri": "frame_7016.png"}, {"time": 3560.217, "image_uri": "frame_7017.png"}, {"time": 3560.724, "image_uri": "frame_7018.png"}, {"time": 3561.231, "image_uri": "frame_7019.png"}, {"time": 3561.739, "image_uri": "frame_7020.png"}, {"time": 3562.246, "image_uri": "frame_7021.png"}, {"time": 3562.754, "image_uri": "frame_7022.png"}, {"time": 3563.261, "image_uri": "frame_7023.png"}, {"time": 3563.769, "image_uri": "frame_7024.png"}, {"time": 3564.276, "image_uri": "frame_7025.png"}, {"time": 3564.783, "image_uri": "frame_7026.png"}, {"time": 3565.291, "image_uri": "frame_7027.png"}, {"time": 3565.798, "image_uri": "frame_7028.png"}, {"time": 3566.306, "image_uri": "frame_7029.png"}, {"time": 3566.813, "image_uri": "frame_7030.png"}, {"time": 3567.321, "image_uri": "frame_7031.png"}, {"time": 3567.828, "image_uri": "frame_7032.png"}, {"time": 3568.336, "image_uri": "frame_7033.png"}, {"time": 3568.843, "image_uri": "frame_7034.png"}, {"time": 3569.35, "image_uri": "frame_7035.png"}, {"time": 3569.858, "image_uri": "frame_7036.png"}, {"time": 3570.365, "image_uri": "frame_7037.png"}, {"time": 3570.873, "image_uri": "frame_7038.png"}, {"time": 3571.38, "image_uri": "frame_7039.png"}, {"time": 3571.888, "image_uri": "frame_7040.png"}, {"time": 3572.395, "image_uri": "frame_7041.png"}, {"time": 3572.903, "image_uri": "frame_7042.png"}, {"time": 3573.41, "image_uri": "frame_7043.png"}, {"time": 3573.917, "image_uri": "frame_7044.png"}, {"time": 3574.425, "image_uri": "frame_7045.png"}, {"time": 3574.932, "image_uri": "frame_7046.png"}, {"time": 3575.44, "image_uri": "frame_7047.png"}, {"time": 3575.947, "image_uri": "frame_7048.png"}, {"time": 3576.455, "image_uri": "frame_7049.png"}, {"time": 3576.962, "image_uri": "frame_7050.png"}, {"time": 3577.47, "image_uri": "frame_7051.png"}, {"time": 3577.977, "image_uri": "frame_7052.png"}, {"time": 3578.484, "image_uri": "frame_7053.png"}, {"time": 3578.992, "image_uri": "frame_7054.png"}, {"time": 3579.499, "image_uri": "frame_7055.png"}, {"time": 3580.007, "image_uri": "frame_7056.png"}, {"time": 3580.514, "image_uri": "frame_7057.png"}, {"time": 3581.022, "image_uri": "frame_7058.png"}, {"time": 3581.529, "image_uri": "frame_7059.png"}, {"time": 3582.037, "image_uri": "frame_7060.png"}, {"time": 3582.544, "image_uri": "frame_7061.png"}, {"time": 3583.051, "image_uri": "frame_7062.png"}, {"time": 3583.559, "image_uri": "frame_7063.png"}, {"time": 3584.066, "image_uri": "frame_7064.png"}, {"time": 3584.574, "image_uri": "frame_7065.png"}, {"time": 3585.081, "image_uri": "frame_7066.png"}, {"time": 3585.589, "image_uri": "frame_7067.png"}, {"time": 3586.096, "image_uri": "frame_7068.png"}, {"time": 3586.604, "image_uri": "frame_7069.png"}, {"time": 3587.111, "image_uri": "frame_7070.png"}, {"time": 3587.618, "image_uri": "frame_7071.png"}, {"time": 3588.126, "image_uri": "frame_7072.png"}, {"time": 3588.633, "image_uri": "frame_7073.png"}, {"time": 3589.141, "image_uri": "frame_7074.png"}, {"time": 3589.648, "image_uri": "frame_7075.png"}, {"time": 3590.156, "image_uri": "frame_7076.png"}, {"time": 3590.663, "image_uri": "frame_7077.png"}, {"time": 3591.171, "image_uri": "frame_7078.png"}, {"time": 3591.678, "image_uri": "frame_7079.png"}, {"time": 3592.185, "image_uri": "frame_7080.png"}, {"time": 3592.693, "image_uri": "frame_7081.png"}, {"time": 3593.2, "image_uri": "frame_7082.png"}, {"time": 3593.708, "image_uri": "frame_7083.png"}, {"time": 3594.215, "image_uri": "frame_7084.png"}, {"time": 3594.723, "image_uri": "frame_7085.png"}, {"time": 3595.23, "image_uri": "frame_7086.png"}, {"time": 3595.737, "image_uri": "frame_7087.png"}, {"time": 3596.245, "image_uri": "frame_7088.png"}, {"time": 3596.752, "image_uri": "frame_7089.png"}, {"time": 3597.26, "image_uri": "frame_7090.png"}, {"time": 3597.767, "image_uri": "frame_7091.png"}, {"time": 3598.275, "image_uri": "frame_7092.png"}, {"time": 3598.782, "image_uri": "frame_7093.png"}, {"time": 3599.29, "image_uri": "frame_7094.png"}, {"time": 3599.797, "image_uri": "frame_7095.png"}, {"time": 3600.304, "image_uri": "frame_7096.png"}, {"time": 3600.812, "image_uri": "frame_7097.png"}, {"time": 3601.319, "image_uri": "frame_7098.png"}, {"time": 3601.827, "image_uri": "frame_7099.png"}, {"time": 3602.334, "image_uri": "frame_7100.png"}, {"time": 3602.842, "image_uri": "frame_7101.png"}, {"time": 3603.349, "image_uri": "frame_7102.png"}, {"time": 3603.857, "image_uri": "frame_7103.png"}, {"time": 3604.364, "image_uri": "frame_7104.png"}, {"time": 3604.871, "image_uri": "frame_7105.png"}, {"time": 3605.379, "image_uri": "frame_7106.png"}, {"time": 3605.886, "image_uri": "frame_7107.png"}, {"time": 3606.394, "image_uri": "frame_7108.png"}, {"time": 3606.901, "image_uri": "frame_7109.png"}, {"time": 3607.409, "image_uri": "frame_7110.png"}, {"time": 3607.916, "image_uri": "frame_7111.png"}, {"time": 3608.424, "image_uri": "frame_7112.png"}, {"time": 3608.931, "image_uri": "frame_7113.png"}, {"time": 3609.438, "image_uri": "frame_7114.png"}, {"time": 3609.946, "image_uri": "frame_7115.png"}, {"time": 3610.453, "image_uri": "frame_7116.png"}, {"time": 3610.961, "image_uri": "frame_7117.png"}, {"time": 3611.468, "image_uri": "frame_7118.png"}, {"time": 3611.976, "image_uri": "frame_7119.png"}, {"time": 3612.483, "image_uri": "frame_7120.png"}, {"time": 3612.991, "image_uri": "frame_7121.png"}, {"time": 3613.498, "image_uri": "frame_7122.png"}, {"time": 3614.005, "image_uri": "frame_7123.png"}, {"time": 3614.513, "image_uri": "frame_7124.png"}, {"time": 3615.02, "image_uri": "frame_7125.png"}, {"time": 3615.528, "image_uri": "frame_7126.png"}, {"time": 3616.035, "image_uri": "frame_7127.png"}, {"time": 3616.543, "image_uri": "frame_7128.png"}, {"time": 3617.05, "image_uri": "frame_7129.png"}, {"time": 3617.558, "image_uri": "frame_7130.png"}, {"time": 3618.065, "image_uri": "frame_7131.png"}, {"time": 3618.572, "image_uri": "frame_7132.png"}, {"time": 3619.08, "image_uri": "frame_7133.png"}, {"time": 3619.587, "image_uri": "frame_7134.png"}, {"time": 3620.095, "image_uri": "frame_7135.png"}, {"time": 3620.602, "image_uri": "frame_7136.png"}, {"time": 3621.11, "image_uri": "frame_7137.png"}, {"time": 3621.617, "image_uri": "frame_7138.png"}, {"time": 3622.124, "image_uri": "frame_7139.png"}, {"time": 3622.632, "image_uri": "frame_7140.png"}, {"time": 3623.139, "image_uri": "frame_7141.png"}, {"time": 3623.647, "image_uri": "frame_7142.png"}, {"time": 3624.154, "image_uri": "frame_7143.png"}, {"time": 3624.662, "image_uri": "frame_7144.png"}, {"time": 3625.169, "image_uri": "frame_7145.png"}, {"time": 3625.677, "image_uri": "frame_7146.png"}, {"time": 3626.184, "image_uri": "frame_7147.png"}, {"time": 3626.691, "image_uri": "frame_7148.png"}, {"time": 3627.199, "image_uri": "frame_7149.png"}, {"time": 3627.706, "image_uri": "frame_7150.png"}, {"time": 3628.214, "image_uri": "frame_7151.png"}, {"time": 3628.721, "image_uri": "frame_7152.png"}, {"time": 3629.229, "image_uri": "frame_7153.png"}, {"time": 3629.736, "image_uri": "frame_7154.png"}, {"time": 3630.244, "image_uri": "frame_7155.png"}, {"time": 3630.751, "image_uri": "frame_7156.png"}, {"time": 3631.258, "image_uri": "frame_7157.png"}, {"time": 3631.766, "image_uri": "frame_7158.png"}, {"time": 3632.273, "image_uri": "frame_7159.png"}, {"time": 3632.781, "image_uri": "frame_7160.png"}, {"time": 3633.288, "image_uri": "frame_7161.png"}, {"time": 3633.796, "image_uri": "frame_7162.png"}, {"time": 3634.303, "image_uri": "frame_7163.png"}, {"time": 3634.811, "image_uri": "frame_7164.png"}, {"time": 3635.318, "image_uri": "frame_7165.png"}, {"time": 3635.825, "image_uri": "frame_7166.png"}, {"time": 3636.333, "image_uri": "frame_7167.png"}, {"time": 3636.84, "image_uri": "frame_7168.png"}, {"time": 3637.348, "image_uri": "frame_7169.png"}, {"time": 3637.855, "image_uri": "frame_7170.png"}, {"time": 3638.363, "image_uri": "frame_7171.png"}, {"time": 3638.87, "image_uri": "frame_7172.png"}, {"time": 3639.378, "image_uri": "frame_7173.png"}, {"time": 3639.885, "image_uri": "frame_7174.png"}, {"time": 3640.392, "image_uri": "frame_7175.png"}, {"time": 3640.9, "image_uri": "frame_7176.png"}, {"time": 3641.407, "image_uri": "frame_7177.png"}, {"time": 3641.915, "image_uri": "frame_7178.png"}, {"time": 3642.422, "image_uri": "frame_7179.png"}, {"time": 3642.93, "image_uri": "frame_7180.png"}, {"time": 3643.437, "image_uri": "frame_7181.png"}, {"time": 3643.945, "image_uri": "frame_7182.png"}, {"time": 3644.452, "image_uri": "frame_7183.png"}, {"time": 3644.959, "image_uri": "frame_7184.png"}, {"time": 3645.467, "image_uri": "frame_7185.png"}, {"time": 3645.974, "image_uri": "frame_7186.png"}, {"time": 3646.482, "image_uri": "frame_7187.png"}, {"time": 3646.989, "image_uri": "frame_7188.png"}, {"time": 3647.497, "image_uri": "frame_7189.png"}, {"time": 3648.004, "image_uri": "frame_7190.png"}, {"time": 3648.512, "image_uri": "frame_7191.png"}, {"time": 3649.019, "image_uri": "frame_7192.png"}, {"time": 3649.526, "image_uri": "frame_7193.png"}, {"time": 3650.034, "image_uri": "frame_7194.png"}, {"time": 3650.541, "image_uri": "frame_7195.png"}, {"time": 3651.049, "image_uri": "frame_7196.png"}, {"time": 3651.556, "image_uri": "frame_7197.png"}, {"time": 3652.064, "image_uri": "frame_7198.png"}, {"time": 3652.571, "image_uri": "frame_7199.png"}, {"time": 3653.078, "image_uri": "frame_7200.png"}, {"time": 3653.586, "image_uri": "frame_7201.png"}, {"time": 3654.093, "image_uri": "frame_7202.png"}, {"time": 3654.601, "image_uri": "frame_7203.png"}, {"time": 3655.108, "image_uri": "frame_7204.png"}, {"time": 3655.616, "image_uri": "frame_7205.png"}, {"time": 3656.123, "image_uri": "frame_7206.png"}, {"time": 3656.631, "image_uri": "frame_7207.png"}, {"time": 3657.138, "image_uri": "frame_7208.png"}, {"time": 3657.645, "image_uri": "frame_7209.png"}, {"time": 3658.153, "image_uri": "frame_7210.png"}, {"time": 3658.66, "image_uri": "frame_7211.png"}, {"time": 3659.168, "image_uri": "frame_7212.png"}, {"time": 3659.675, "image_uri": "frame_7213.png"}, {"time": 3660.183, "image_uri": "frame_7214.png"}, {"time": 3660.69, "image_uri": "frame_7215.png"}, {"time": 3661.198, "image_uri": "frame_7216.png"}, {"time": 3661.705, "image_uri": "frame_7217.png"}, {"time": 3662.212, "image_uri": "frame_7218.png"}, {"time": 3662.72, "image_uri": "frame_7219.png"}, {"time": 3663.227, "image_uri": "frame_7220.png"}, {"time": 3663.735, "image_uri": "frame_7221.png"}, {"time": 3664.242, "image_uri": "frame_7222.png"}, {"time": 3664.75, "image_uri": "frame_7223.png"}, {"time": 3665.257, "image_uri": "frame_7224.png"}, {"time": 3665.765, "image_uri": "frame_7225.png"}, {"time": 3666.272, "image_uri": "frame_7226.png"}, {"time": 3666.779, "image_uri": "frame_7227.png"}, {"time": 3667.287, "image_uri": "frame_7228.png"}, {"time": 3667.794, "image_uri": "frame_7229.png"}, {"time": 3668.302, "image_uri": "frame_7230.png"}, {"time": 3668.809, "image_uri": "frame_7231.png"}, {"time": 3669.317, "image_uri": "frame_7232.png"}, {"time": 3669.824, "image_uri": "frame_7233.png"}, {"time": 3670.332, "image_uri": "frame_7234.png"}, {"time": 3670.839, "image_uri": "frame_7235.png"}, {"time": 3671.346, "image_uri": "frame_7236.png"}, {"time": 3671.854, "image_uri": "frame_7237.png"}, {"time": 3672.361, "image_uri": "frame_7238.png"}, {"time": 3672.869, "image_uri": "frame_7239.png"}, {"time": 3673.376, "image_uri": "frame_7240.png"}, {"time": 3673.884, "image_uri": "frame_7241.png"}, {"time": 3674.391, "image_uri": "frame_7242.png"}, {"time": 3674.899, "image_uri": "frame_7243.png"}, {"time": 3675.406, "image_uri": "frame_7244.png"}, {"time": 3675.913, "image_uri": "frame_7245.png"}, {"time": 3676.421, "image_uri": "frame_7246.png"}, {"time": 3676.928, "image_uri": "frame_7247.png"}, {"time": 3677.436, "image_uri": "frame_7248.png"}, {"time": 3677.943, "image_uri": "frame_7249.png"}, {"time": 3678.451, "image_uri": "frame_7250.png"}, {"time": 3678.958, "image_uri": "frame_7251.png"}, {"time": 3679.465, "image_uri": "frame_7252.png"}, {"time": 3679.973, "image_uri": "frame_7253.png"}, {"time": 3680.48, "image_uri": "frame_7254.png"}, {"time": 3680.988, "image_uri": "frame_7255.png"}, {"time": 3681.495, "image_uri": "frame_7256.png"}, {"time": 3682.003, "image_uri": "frame_7257.png"}, {"time": 3682.51, "image_uri": "frame_7258.png"}, {"time": 3683.018, "image_uri": "frame_7259.png"}, {"time": 3683.525, "image_uri": "frame_7260.png"}, {"time": 3684.032, "image_uri": "frame_7261.png"}, {"time": 3684.54, "image_uri": "frame_7262.png"}, {"time": 3685.047, "image_uri": "frame_7263.png"}, {"time": 3685.555, "image_uri": "frame_7264.png"}, {"time": 3686.062, "image_uri": "frame_7265.png"}, {"time": 3686.57, "image_uri": "frame_7266.png"}, {"time": 3687.077, "image_uri": "frame_7267.png"}, {"time": 3687.585, "image_uri": "frame_7268.png"}, {"time": 3688.092, "image_uri": "frame_7269.png"}, {"time": 3688.599, "image_uri": "frame_7270.png"}, {"time": 3689.107, "image_uri": "frame_7271.png"}, {"time": 3689.614, "image_uri": "frame_7272.png"}, {"time": 3690.122, "image_uri": "frame_7273.png"}, {"time": 3690.629, "image_uri": "frame_7274.png"}, {"time": 3691.137, "image_uri": "frame_7275.png"}, {"time": 3691.644, "image_uri": "frame_7276.png"}, {"time": 3692.152, "image_uri": "frame_7277.png"}, {"time": 3692.659, "image_uri": "frame_7278.png"}, {"time": 3693.166, "image_uri": "frame_7279.png"}, {"time": 3693.674, "image_uri": "frame_7280.png"}, {"time": 3694.181, "image_uri": "frame_7281.png"}, {"time": 3694.689, "image_uri": "frame_7282.png"}, {"time": 3695.196, "image_uri": "frame_7283.png"}, {"time": 3695.704, "image_uri": "frame_7284.png"}, {"time": 3696.211, "image_uri": "frame_7285.png"}, {"time": 3696.719, "image_uri": "frame_7286.png"}, {"time": 3697.226, "image_uri": "frame_7287.png"}, {"time": 3697.733, "image_uri": "frame_7288.png"}, {"time": 3698.241, "image_uri": "frame_7289.png"}, {"time": 3698.748, "image_uri": "frame_7290.png"}, {"time": 3699.256, "image_uri": "frame_7291.png"}, {"time": 3699.763, "image_uri": "frame_7292.png"}, {"time": 3700.271, "image_uri": "frame_7293.png"}, {"time": 3700.778, "image_uri": "frame_7294.png"}, {"time": 3701.286, "image_uri": "frame_7295.png"}, {"time": 3701.793, "image_uri": "frame_7296.png"}, {"time": 3702.3, "image_uri": "frame_7297.png"}, {"time": 3702.808, "image_uri": "frame_7298.png"}, {"time": 3703.315, "image_uri": "frame_7299.png"}, {"time": 3703.823, "image_uri": "frame_7300.png"}, {"time": 3704.33, "image_uri": "frame_7301.png"}, {"time": 3704.838, "image_uri": "frame_7302.png"}, {"time": 3705.345, "image_uri": "frame_7303.png"}, {"time": 3705.853, "image_uri": "frame_7304.png"}, {"time": 3706.36, "image_uri": "frame_7305.png"}, {"time": 3706.867, "image_uri": "frame_7306.png"}, {"time": 3707.375, "image_uri": "frame_7307.png"}, {"time": 3707.882, "image_uri": "frame_7308.png"}, {"time": 3708.39, "image_uri": "frame_7309.png"}, {"time": 3708.897, "image_uri": "frame_7310.png"}, {"time": 3709.405, "image_uri": "frame_7311.png"}, {"time": 3709.912, "image_uri": "frame_7312.png"}, {"time": 3710.419, "image_uri": "frame_7313.png"}, {"time": 3710.927, "image_uri": "frame_7314.png"}, {"time": 3711.434, "image_uri": "frame_7315.png"}, {"time": 3711.942, "image_uri": "frame_7316.png"}, {"time": 3712.449, "image_uri": "frame_7317.png"}, {"time": 3712.957, "image_uri": "frame_7318.png"}, {"time": 3713.464, "image_uri": "frame_7319.png"}, {"time": 3713.972, "image_uri": "frame_7320.png"}, {"time": 3714.479, "image_uri": "frame_7321.png"}, {"time": 3714.986, "image_uri": "frame_7322.png"}, {"time": 3715.494, "image_uri": "frame_7323.png"}, {"time": 3716.001, "image_uri": "frame_7324.png"}, {"time": 3716.509, "image_uri": "frame_7325.png"}, {"time": 3717.016, "image_uri": "frame_7326.png"}, {"time": 3717.524, "image_uri": "frame_7327.png"}, {"time": 3718.031, "image_uri": "frame_7328.png"}, {"time": 3718.539, "image_uri": "frame_7329.png"}, {"time": 3719.046, "image_uri": "frame_7330.png"}, {"time": 3719.553, "image_uri": "frame_7331.png"}, {"time": 3720.061, "image_uri": "frame_7332.png"}, {"time": 3720.568, "image_uri": "frame_7333.png"}, {"time": 3721.076, "image_uri": "frame_7334.png"}, {"time": 3721.583, "image_uri": "frame_7335.png"}, {"time": 3722.091, "image_uri": "frame_7336.png"}, {"time": 3722.598, "image_uri": "frame_7337.png"}, {"time": 3723.106, "image_uri": "frame_7338.png"}, {"time": 3723.613, "image_uri": "frame_7339.png"}, {"time": 3724.12, "image_uri": "frame_7340.png"}, {"time": 3724.628, "image_uri": "frame_7341.png"}, {"time": 3725.135, "image_uri": "frame_7342.png"}, {"time": 3725.643, "image_uri": "frame_7343.png"}, {"time": 3726.15, "image_uri": "frame_7344.png"}, {"time": 3726.658, "image_uri": "frame_7345.png"}, {"time": 3727.165, "image_uri": "frame_7346.png"}, {"time": 3727.673, "image_uri": "frame_7347.png"}, {"time": 3728.18, "image_uri": "frame_7348.png"}, {"time": 3728.687, "image_uri": "frame_7349.png"}, {"time": 3729.195, "image_uri": "frame_7350.png"}, {"time": 3729.702, "image_uri": "frame_7351.png"}, {"time": 3730.21, "image_uri": "frame_7352.png"}, {"time": 3730.717, "image_uri": "frame_7353.png"}, {"time": 3731.225, "image_uri": "frame_7354.png"}, {"time": 3731.732, "image_uri": "frame_7355.png"}, {"time": 3732.24, "image_uri": "frame_7356.png"}, {"time": 3732.747, "image_uri": "frame_7357.png"}, {"time": 3733.254, "image_uri": "frame_7358.png"}, {"time": 3733.762, "image_uri": "frame_7359.png"}, {"time": 3734.269, "image_uri": "frame_7360.png"}, {"time": 3734.777, "image_uri": "frame_7361.png"}, {"time": 3735.284, "image_uri": "frame_7362.png"}, {"time": 3735.792, "image_uri": "frame_7363.png"}, {"time": 3736.299, "image_uri": "frame_7364.png"}, {"time": 3736.806, "image_uri": "frame_7365.png"}, {"time": 3737.314, "image_uri": "frame_7366.png"}, {"time": 3737.821, "image_uri": "frame_7367.png"}, {"time": 3738.329, "image_uri": "frame_7368.png"}, {"time": 3738.836, "image_uri": "frame_7369.png"}, {"time": 3739.344, "image_uri": "frame_7370.png"}, {"time": 3739.851, "image_uri": "frame_7371.png"}, {"time": 3740.359, "image_uri": "frame_7372.png"}, {"time": 3740.866, "image_uri": "frame_7373.png"}, {"time": 3741.373, "image_uri": "frame_7374.png"}, {"time": 3741.881, "image_uri": "frame_7375.png"}, {"time": 3742.388, "image_uri": "frame_7376.png"}, {"time": 3742.896, "image_uri": "frame_7377.png"}, {"time": 3743.403, "image_uri": "frame_7378.png"}, {"time": 3743.911, "image_uri": "frame_7379.png"}, {"time": 3744.418, "image_uri": "frame_7380.png"}, {"time": 3744.926, "image_uri": "frame_7381.png"}, {"time": 3745.433, "image_uri": "frame_7382.png"}, {"time": 3745.94, "image_uri": "frame_7383.png"}, {"time": 3746.448, "image_uri": "frame_7384.png"}, {"time": 3746.955, "image_uri": "frame_7385.png"}, {"time": 3747.463, "image_uri": "frame_7386.png"}, {"time": 3747.97, "image_uri": "frame_7387.png"}, {"time": 3748.478, "image_uri": "frame_7388.png"}, {"time": 3748.985, "image_uri": "frame_7389.png"}, {"time": 3749.493, "image_uri": "frame_7390.png"}, {"time": 3750.0, "image_uri": "frame_7391.png"}, {"time": 3750.507, "image_uri": "frame_7392.png"}, {"time": 3751.015, "image_uri": "frame_7393.png"}, {"time": 3751.522, "image_uri": "frame_7394.png"}, {"time": 3752.03, "image_uri": "frame_7395.png"}, {"time": 3752.537, "image_uri": "frame_7396.png"}, {"time": 3753.045, "image_uri": "frame_7397.png"}, {"time": 3753.552, "image_uri": "frame_7398.png"}, {"time": 3754.06, "image_uri": "frame_7399.png"}, {"time": 3754.567, "image_uri": "frame_7400.png"}, {"time": 3755.074, "image_uri": "frame_7401.png"}, {"time": 3755.582, "image_uri": "frame_7402.png"}, {"time": 3756.089, "image_uri": "frame_7403.png"}, {"time": 3756.597, "image_uri": "frame_7404.png"}, {"time": 3757.104, "image_uri": "frame_7405.png"}, {"time": 3757.612, "image_uri": "frame_7406.png"}, {"time": 3758.119, "image_uri": "frame_7407.png"}, {"time": 3758.627, "image_uri": "frame_7408.png"}, {"time": 3759.134, "image_uri": "frame_7409.png"}, {"time": 3759.641, "image_uri": "frame_7410.png"}, {"time": 3760.149, "image_uri": "frame_7411.png"}, {"time": 3760.656, "image_uri": "frame_7412.png"}, {"time": 3761.164, "image_uri": "frame_7413.png"}, {"time": 3761.671, "image_uri": "frame_7414.png"}, {"time": 3762.179, "image_uri": "frame_7415.png"}, {"time": 3762.686, "image_uri": "frame_7416.png"}, {"time": 3763.194, "image_uri": "frame_7417.png"}, {"time": 3763.701, "image_uri": "frame_7418.png"}, {"time": 3764.208, "image_uri": "frame_7419.png"}, {"time": 3764.716, "image_uri": "frame_7420.png"}, {"time": 3765.223, "image_uri": "frame_7421.png"}, {"time": 3765.731, "image_uri": "frame_7422.png"}, {"time": 3766.238, "image_uri": "frame_7423.png"}, {"time": 3766.746, "image_uri": "frame_7424.png"}, {"time": 3767.253, "image_uri": "frame_7425.png"}, {"time": 3767.76, "image_uri": "frame_7426.png"}, {"time": 3768.268, "image_uri": "frame_7427.png"}, {"time": 3768.775, "image_uri": "frame_7428.png"}, {"time": 3769.283, "image_uri": "frame_7429.png"}, {"time": 3769.79, "image_uri": "frame_7430.png"}, {"time": 3770.298, "image_uri": "frame_7431.png"}, {"time": 3770.805, "image_uri": "frame_7432.png"}, {"time": 3771.313, "image_uri": "frame_7433.png"}, {"time": 3771.82, "image_uri": "frame_7434.png"}, {"time": 3772.327, "image_uri": "frame_7435.png"}, {"time": 3772.835, "image_uri": "frame_7436.png"}, {"time": 3773.342, "image_uri": "frame_7437.png"}, {"time": 3773.85, "image_uri": "frame_7438.png"}, {"time": 3774.357, "image_uri": "frame_7439.png"}, {"time": 3774.865, "image_uri": "frame_7440.png"}, {"time": 3775.372, "image_uri": "frame_7441.png"}, {"time": 3775.88, "image_uri": "frame_7442.png"}, {"time": 3776.387, "image_uri": "frame_7443.png"}, {"time": 3776.894, "image_uri": "frame_7444.png"}, {"time": 3777.402, "image_uri": "frame_7445.png"}, {"time": 3777.909, "image_uri": "frame_7446.png"}, {"time": 3778.417, "image_uri": "frame_7447.png"}, {"time": 3778.924, "image_uri": "frame_7448.png"}, {"time": 3779.432, "image_uri": "frame_7449.png"}, {"time": 3779.939, "image_uri": "frame_7450.png"}, {"time": 3780.447, "image_uri": "frame_7451.png"}, {"time": 3780.954, "image_uri": "frame_7452.png"}, {"time": 3781.461, "image_uri": "frame_7453.png"}, {"time": 3781.969, "image_uri": "frame_7454.png"}, {"time": 3782.476, "image_uri": "frame_7455.png"}, {"time": 3782.984, "image_uri": "frame_7456.png"}, {"time": 3783.491, "image_uri": "frame_7457.png"}, {"time": 3783.999, "image_uri": "frame_7458.png"}, {"time": 3784.506, "image_uri": "frame_7459.png"}, {"time": 3785.014, "image_uri": "frame_7460.png"}, {"time": 3785.521, "image_uri": "frame_7461.png"}, {"time": 3786.028, "image_uri": "frame_7462.png"}, {"time": 3786.536, "image_uri": "frame_7463.png"}, {"time": 3787.043, "image_uri": "frame_7464.png"}, {"time": 3787.551, "image_uri": "frame_7465.png"}, {"time": 3788.058, "image_uri": "frame_7466.png"}, {"time": 3788.566, "image_uri": "frame_7467.png"}, {"time": 3789.073, "image_uri": "frame_7468.png"}, {"time": 3789.581, "image_uri": "frame_7469.png"}, {"time": 3790.088, "image_uri": "frame_7470.png"}, {"time": 3790.595, "image_uri": "frame_7471.png"}, {"time": 3791.103, "image_uri": "frame_7472.png"}, {"time": 3791.61, "image_uri": "frame_7473.png"}, {"time": 3792.118, "image_uri": "frame_7474.png"}, {"time": 3792.625, "image_uri": "frame_7475.png"}, {"time": 3793.133, "image_uri": "frame_7476.png"}, {"time": 3793.64, "image_uri": "frame_7477.png"}, {"time": 3794.147, "image_uri": "frame_7478.png"}, {"time": 3794.655, "image_uri": "frame_7479.png"}, {"time": 3795.162, "image_uri": "frame_7480.png"}, {"time": 3795.67, "image_uri": "frame_7481.png"}, {"time": 3796.177, "image_uri": "frame_7482.png"}, {"time": 3796.685, "image_uri": "frame_7483.png"}, {"time": 3797.192, "image_uri": "frame_7484.png"}, {"time": 3797.7, "image_uri": "frame_7485.png"}, {"time": 3798.207, "image_uri": "frame_7486.png"}, {"time": 3798.714, "image_uri": "frame_7487.png"}, {"time": 3799.222, "image_uri": "frame_7488.png"}, {"time": 3799.729, "image_uri": "frame_7489.png"}, {"time": 3800.237, "image_uri": "frame_7490.png"}, {"time": 3800.744, "image_uri": "frame_7491.png"}, {"time": 3801.252, "image_uri": "frame_7492.png"}, {"time": 3801.759, "image_uri": "frame_7493.png"}, {"time": 3802.267, "image_uri": "frame_7494.png"}, {"time": 3802.774, "image_uri": "frame_7495.png"}, {"time": 3803.281, "image_uri": "frame_7496.png"}, {"time": 3803.789, "image_uri": "frame_7497.png"}, {"time": 3804.296, "image_uri": "frame_7498.png"}, {"time": 3804.804, "image_uri": "frame_7499.png"}, {"time": 3805.311, "image_uri": "frame_7500.png"}, {"time": 3805.819, "image_uri": "frame_7501.png"}, {"time": 3806.326, "image_uri": "frame_7502.png"}, {"time": 3806.834, "image_uri": "frame_7503.png"}, {"time": 3807.341, "image_uri": "frame_7504.png"}, {"time": 3807.848, "image_uri": "frame_7505.png"}, {"time": 3808.356, "image_uri": "frame_7506.png"}, {"time": 3808.863, "image_uri": "frame_7507.png"}, {"time": 3809.371, "image_uri": "frame_7508.png"}, {"time": 3809.878, "image_uri": "frame_7509.png"}, {"time": 3810.386, "image_uri": "frame_7510.png"}, {"time": 3810.893, "image_uri": "frame_7511.png"}, {"time": 3811.401, "image_uri": "frame_7512.png"}, {"time": 3811.908, "image_uri": "frame_7513.png"}, {"time": 3812.415, "image_uri": "frame_7514.png"}, {"time": 3812.923, "image_uri": "frame_7515.png"}, {"time": 3813.43, "image_uri": "frame_7516.png"}, {"time": 3813.938, "image_uri": "frame_7517.png"}, {"time": 3814.445, "image_uri": "frame_7518.png"}, {"time": 3814.953, "image_uri": "frame_7519.png"}, {"time": 3815.46, "image_uri": "frame_7520.png"}, {"time": 3815.968, "image_uri": "frame_7521.png"}, {"time": 3816.475, "image_uri": "frame_7522.png"}, {"time": 3816.982, "image_uri": "frame_7523.png"}, {"time": 3817.49, "image_uri": "frame_7524.png"}, {"time": 3817.997, "image_uri": "frame_7525.png"}, {"time": 3818.505, "image_uri": "frame_7526.png"}, {"time": 3819.012, "image_uri": "frame_7527.png"}, {"time": 3819.52, "image_uri": "frame_7528.png"}, {"time": 3820.027, "image_uri": "frame_7529.png"}, {"time": 3820.535, "image_uri": "frame_7530.png"}, {"time": 3821.042, "image_uri": "frame_7531.png"}, {"time": 3821.549, "image_uri": "frame_7532.png"}, {"time": 3822.057, "image_uri": "frame_7533.png"}, {"time": 3822.564, "image_uri": "frame_7534.png"}, {"time": 3823.072, "image_uri": "frame_7535.png"}, {"time": 3823.579, "image_uri": "frame_7536.png"}, {"time": 3824.087, "image_uri": "frame_7537.png"}, {"time": 3824.594, "image_uri": "frame_7538.png"}, {"time": 3825.101, "image_uri": "frame_7539.png"}, {"time": 3825.609, "image_uri": "frame_7540.png"}, {"time": 3826.116, "image_uri": "frame_7541.png"}, {"time": 3826.624, "image_uri": "frame_7542.png"}, {"time": 3827.131, "image_uri": "frame_7543.png"}, {"time": 3827.639, "image_uri": "frame_7544.png"}, {"time": 3828.146, "image_uri": "frame_7545.png"}, {"time": 3828.654, "image_uri": "frame_7546.png"}, {"time": 3829.161, "image_uri": "frame_7547.png"}, {"time": 3829.668, "image_uri": "frame_7548.png"}, {"time": 3830.176, "image_uri": "frame_7549.png"}, {"time": 3830.683, "image_uri": "frame_7550.png"}, {"time": 3831.191, "image_uri": "frame_7551.png"}, {"time": 3831.698, "image_uri": "frame_7552.png"}, {"time": 3832.206, "image_uri": "frame_7553.png"}, {"time": 3832.713, "image_uri": "frame_7554.png"}, {"time": 3833.221, "image_uri": "frame_7555.png"}, {"time": 3833.728, "image_uri": "frame_7556.png"}, {"time": 3834.235, "image_uri": "frame_7557.png"}, {"time": 3834.743, "image_uri": "frame_7558.png"}, {"time": 3835.25, "image_uri": "frame_7559.png"}, {"time": 3835.758, "image_uri": "frame_7560.png"}, {"time": 3836.265, "image_uri": "frame_7561.png"}, {"time": 3836.773, "image_uri": "frame_7562.png"}, {"time": 3837.28, "image_uri": "frame_7563.png"}, {"time": 3837.788, "image_uri": "frame_7564.png"}, {"time": 3838.295, "image_uri": "frame_7565.png"}, {"time": 3838.802, "image_uri": "frame_7566.png"}, {"time": 3839.31, "image_uri": "frame_7567.png"}, {"time": 3839.817, "image_uri": "frame_7568.png"}, {"time": 3840.325, "image_uri": "frame_7569.png"}, {"time": 3840.832, "image_uri": "frame_7570.png"}, {"time": 3841.34, "image_uri": "frame_7571.png"}, {"time": 3841.847, "image_uri": "frame_7572.png"}, {"time": 3842.355, "image_uri": "frame_7573.png"}, {"time": 3842.862, "image_uri": "frame_7574.png"}, {"time": 3843.369, "image_uri": "frame_7575.png"}, {"time": 3843.877, "image_uri": "frame_7576.png"}, {"time": 3844.384, "image_uri": "frame_7577.png"}, {"time": 3844.892, "image_uri": "frame_7578.png"}, {"time": 3845.399, "image_uri": "frame_7579.png"}, {"time": 3845.907, "image_uri": "frame_7580.png"}, {"time": 3846.414, "image_uri": "frame_7581.png"}, {"time": 3846.922, "image_uri": "frame_7582.png"}, {"time": 3847.429, "image_uri": "frame_7583.png"}, {"time": 3847.936, "image_uri": "frame_7584.png"}, {"time": 3848.444, "image_uri": "frame_7585.png"}, {"time": 3848.951, "image_uri": "frame_7586.png"}, {"time": 3849.459, "image_uri": "frame_7587.png"}, {"time": 3849.966, "image_uri": "frame_7588.png"}, {"time": 3850.474, "image_uri": "frame_7589.png"}, {"time": 3850.981, "image_uri": "frame_7590.png"}, {"time": 3851.488, "image_uri": "frame_7591.png"}, {"time": 3851.996, "image_uri": "frame_7592.png"}, {"time": 3852.503, "image_uri": "frame_7593.png"}, {"time": 3853.011, "image_uri": "frame_7594.png"}, {"time": 3853.518, "image_uri": "frame_7595.png"}, {"time": 3854.026, "image_uri": "frame_7596.png"}, {"time": 3854.533, "image_uri": "frame_7597.png"}, {"time": 3855.041, "image_uri": "frame_7598.png"}, {"time": 3855.548, "image_uri": "frame_7599.png"}, {"time": 3856.055, "image_uri": "frame_7600.png"}, {"time": 3856.563, "image_uri": "frame_7601.png"}, {"time": 3857.07, "image_uri": "frame_7602.png"}, {"time": 3857.578, "image_uri": "frame_7603.png"}, {"time": 3858.085, "image_uri": "frame_7604.png"}, {"time": 3858.593, "image_uri": "frame_7605.png"}, {"time": 3859.1, "image_uri": "frame_7606.png"}, {"time": 3859.608, "image_uri": "frame_7607.png"}, {"time": 3860.115, "image_uri": "frame_7608.png"}, {"time": 3860.622, "image_uri": "frame_7609.png"}, {"time": 3861.13, "image_uri": "frame_7610.png"}, {"time": 3861.637, "image_uri": "frame_7611.png"}, {"time": 3862.145, "image_uri": "frame_7612.png"}, {"time": 3862.652, "image_uri": "frame_7613.png"}, {"time": 3863.16, "image_uri": "frame_7614.png"}, {"time": 3863.667, "image_uri": "frame_7615.png"}, {"time": 3864.175, "image_uri": "frame_7616.png"}, {"time": 3864.682, "image_uri": "frame_7617.png"}, {"time": 3865.189, "image_uri": "frame_7618.png"}, {"time": 3865.697, "image_uri": "frame_7619.png"}, {"time": 3866.204, "image_uri": "frame_7620.png"}, {"time": 3866.712, "image_uri": "frame_7621.png"}, {"time": 3867.219, "image_uri": "frame_7622.png"}, {"time": 3867.727, "image_uri": "frame_7623.png"}, {"time": 3868.234, "image_uri": "frame_7624.png"}, {"time": 3868.742, "image_uri": "frame_7625.png"}, {"time": 3869.249, "image_uri": "frame_7626.png"}, {"time": 3869.756, "image_uri": "frame_7627.png"}, {"time": 3870.264, "image_uri": "frame_7628.png"}, {"time": 3870.771, "image_uri": "frame_7629.png"}, {"time": 3871.279, "image_uri": "frame_7630.png"}, {"time": 3871.786, "image_uri": "frame_7631.png"}, {"time": 3872.294, "image_uri": "frame_7632.png"}, {"time": 3872.801, "image_uri": "frame_7633.png"}, {"time": 3873.309, "image_uri": "frame_7634.png"}, {"time": 3873.816, "image_uri": "frame_7635.png"}, {"time": 3874.323, "image_uri": "frame_7636.png"}, {"time": 3874.831, "image_uri": "frame_7637.png"}, {"time": 3875.338, "image_uri": "frame_7638.png"}, {"time": 3875.846, "image_uri": "frame_7639.png"}, {"time": 3876.353, "image_uri": "frame_7640.png"}, {"time": 3876.861, "image_uri": "frame_7641.png"}, {"time": 3877.368, "image_uri": "frame_7642.png"}, {"time": 3877.876, "image_uri": "frame_7643.png"}, {"time": 3878.383, "image_uri": "frame_7644.png"}, {"time": 3878.89, "image_uri": "frame_7645.png"}, {"time": 3879.398, "image_uri": "frame_7646.png"}, {"time": 3879.905, "image_uri": "frame_7647.png"}, {"time": 3880.413, "image_uri": "frame_7648.png"}, {"time": 3880.92, "image_uri": "frame_7649.png"}, {"time": 3881.428, "image_uri": "frame_7650.png"}, {"time": 3881.935, "image_uri": "frame_7651.png"}, {"time": 3882.442, "image_uri": "frame_7652.png"}, {"time": 3882.95, "image_uri": "frame_7653.png"}, {"time": 3883.457, "image_uri": "frame_7654.png"}, {"time": 3883.965, "image_uri": "frame_7655.png"}, {"time": 3884.472, "image_uri": "frame_7656.png"}, {"time": 3884.98, "image_uri": "frame_7657.png"}, {"time": 3885.487, "image_uri": "frame_7658.png"}, {"time": 3885.995, "image_uri": "frame_7659.png"}, {"time": 3886.502, "image_uri": "frame_7660.png"}, {"time": 3887.009, "image_uri": "frame_7661.png"}, {"time": 3887.517, "image_uri": "frame_7662.png"}, {"time": 3888.024, "image_uri": "frame_7663.png"}, {"time": 3888.532, "image_uri": "frame_7664.png"}, {"time": 3889.039, "image_uri": "frame_7665.png"}, {"time": 3889.547, "image_uri": "frame_7666.png"}, {"time": 3890.054, "image_uri": "frame_7667.png"}, {"time": 3890.562, "image_uri": "frame_7668.png"}, {"time": 3891.069, "image_uri": "frame_7669.png"}, {"time": 3891.576, "image_uri": "frame_7670.png"}, {"time": 3892.084, "image_uri": "frame_7671.png"}, {"time": 3892.591, "image_uri": "frame_7672.png"}, {"time": 3893.099, "image_uri": "frame_7673.png"}, {"time": 3893.606, "image_uri": "frame_7674.png"}, {"time": 3894.114, "image_uri": "frame_7675.png"}, {"time": 3894.621, "image_uri": "frame_7676.png"}, {"time": 3895.129, "image_uri": "frame_7677.png"}, {"time": 3895.636, "image_uri": "frame_7678.png"}, {"time": 3896.143, "image_uri": "frame_7679.png"}, {"time": 3896.651, "image_uri": "frame_7680.png"}, {"time": 3897.158, "image_uri": "frame_7681.png"}, {"time": 3897.666, "image_uri": "frame_7682.png"}, {"time": 3898.173, "image_uri": "frame_7683.png"}, {"time": 3898.681, "image_uri": "frame_7684.png"}, {"time": 3899.188, "image_uri": "frame_7685.png"}, {"time": 3899.696, "image_uri": "frame_7686.png"}, {"time": 3900.203, "image_uri": "frame_7687.png"}, {"time": 3900.71, "image_uri": "frame_7688.png"}, {"time": 3901.218, "image_uri": "frame_7689.png"}, {"time": 3901.725, "image_uri": "frame_7690.png"}, {"time": 3902.233, "image_uri": "frame_7691.png"}, {"time": 3902.74, "image_uri": "frame_7692.png"}, {"time": 3903.248, "image_uri": "frame_7693.png"}, {"time": 3903.755, "image_uri": "frame_7694.png"}, {"time": 3904.263, "image_uri": "frame_7695.png"}, {"time": 3904.77, "image_uri": "frame_7696.png"}, {"time": 3905.277, "image_uri": "frame_7697.png"}, {"time": 3905.785, "image_uri": "frame_7698.png"}, {"time": 3906.292, "image_uri": "frame_7699.png"}, {"time": 3906.8, "image_uri": "frame_7700.png"}, {"time": 3907.307, "image_uri": "frame_7701.png"}, {"time": 3907.815, "image_uri": "frame_7702.png"}, {"time": 3908.322, "image_uri": "frame_7703.png"}, {"time": 3908.829, "image_uri": "frame_7704.png"}, {"time": 3909.337, "image_uri": "frame_7705.png"}, {"time": 3909.844, "image_uri": "frame_7706.png"}, {"time": 3910.352, "image_uri": "frame_7707.png"}, {"time": 3910.859, "image_uri": "frame_7708.png"}, {"time": 3911.367, "image_uri": "frame_7709.png"}, {"time": 3911.874, "image_uri": "frame_7710.png"}, {"time": 3912.382, "image_uri": "frame_7711.png"}, {"time": 3912.889, "image_uri": "frame_7712.png"}, {"time": 3913.396, "image_uri": "frame_7713.png"}, {"time": 3913.904, "image_uri": "frame_7714.png"}, {"time": 3914.411, "image_uri": "frame_7715.png"}, {"time": 3914.919, "image_uri": "frame_7716.png"}, {"time": 3915.426, "image_uri": "frame_7717.png"}, {"time": 3915.934, "image_uri": "frame_7718.png"}, {"time": 3916.441, "image_uri": "frame_7719.png"}, {"time": 3916.949, "image_uri": "frame_7720.png"}, {"time": 3917.456, "image_uri": "frame_7721.png"}, {"time": 3917.963, "image_uri": "frame_7722.png"}, {"time": 3918.471, "image_uri": "frame_7723.png"}, {"time": 3918.978, "image_uri": "frame_7724.png"}, {"time": 3919.486, "image_uri": "frame_7725.png"}, {"time": 3919.993, "image_uri": "frame_7726.png"}, {"time": 3920.501, "image_uri": "frame_7727.png"}, {"time": 3921.008, "image_uri": "frame_7728.png"}, {"time": 3921.516, "image_uri": "frame_7729.png"}, {"time": 3922.023, "image_uri": "frame_7730.png"}, {"time": 3922.53, "image_uri": "frame_7731.png"}, {"time": 3923.038, "image_uri": "frame_7732.png"}, {"time": 3923.545, "image_uri": "frame_7733.png"}, {"time": 3924.053, "image_uri": "frame_7734.png"}, {"time": 3924.56, "image_uri": "frame_7735.png"}, {"time": 3925.068, "image_uri": "frame_7736.png"}, {"time": 3925.575, "image_uri": "frame_7737.png"}, {"time": 3926.083, "image_uri": "frame_7738.png"}, {"time": 3926.59, "image_uri": "frame_7739.png"}, {"time": 3927.097, "image_uri": "frame_7740.png"}, {"time": 3927.605, "image_uri": "frame_7741.png"}, {"time": 3928.112, "image_uri": "frame_7742.png"}, {"time": 3928.62, "image_uri": "frame_7743.png"}, {"time": 3929.127, "image_uri": "frame_7744.png"}, {"time": 3929.635, "image_uri": "frame_7745.png"}, {"time": 3930.142, "image_uri": "frame_7746.png"}, {"time": 3930.65, "image_uri": "frame_7747.png"}, {"time": 3931.157, "image_uri": "frame_7748.png"}, {"time": 3931.664, "image_uri": "frame_7749.png"}, {"time": 3932.172, "image_uri": "frame_7750.png"}, {"time": 3932.679, "image_uri": "frame_7751.png"}, {"time": 3933.187, "image_uri": "frame_7752.png"}, {"time": 3933.694, "image_uri": "frame_7753.png"}, {"time": 3934.202, "image_uri": "frame_7754.png"}, {"time": 3934.709, "image_uri": "frame_7755.png"}, {"time": 3935.217, "image_uri": "frame_7756.png"}, {"time": 3935.724, "image_uri": "frame_7757.png"}, {"time": 3936.231, "image_uri": "frame_7758.png"}, {"time": 3936.739, "image_uri": "frame_7759.png"}, {"time": 3937.246, "image_uri": "frame_7760.png"}, {"time": 3937.754, "image_uri": "frame_7761.png"}, {"time": 3938.261, "image_uri": "frame_7762.png"}, {"time": 3938.769, "image_uri": "frame_7763.png"}, {"time": 3939.276, "image_uri": "frame_7764.png"}, {"time": 3939.783, "image_uri": "frame_7765.png"}, {"time": 3940.291, "image_uri": "frame_7766.png"}, {"time": 3940.798, "image_uri": "frame_7767.png"}, {"time": 3941.306, "image_uri": "frame_7768.png"}, {"time": 3941.813, "image_uri": "frame_7769.png"}, {"time": 3942.321, "image_uri": "frame_7770.png"}, {"time": 3942.828, "image_uri": "frame_7771.png"}, {"time": 3943.336, "image_uri": "frame_7772.png"}, {"time": 3943.843, "image_uri": "frame_7773.png"}, {"time": 3944.35, "image_uri": "frame_7774.png"}, {"time": 3944.858, "image_uri": "frame_7775.png"}, {"time": 3945.365, "image_uri": "frame_7776.png"}, {"time": 3945.873, "image_uri": "frame_7777.png"}, {"time": 3946.38, "image_uri": "frame_7778.png"}, {"time": 3946.888, "image_uri": "frame_7779.png"}, {"time": 3947.395, "image_uri": "frame_7780.png"}, {"time": 3947.903, "image_uri": "frame_7781.png"}, {"time": 3948.41, "image_uri": "frame_7782.png"}, {"time": 3948.917, "image_uri": "frame_7783.png"}, {"time": 3949.425, "image_uri": "frame_7784.png"}, {"time": 3949.932, "image_uri": "frame_7785.png"}, {"time": 3950.44, "image_uri": "frame_7786.png"}, {"time": 3950.947, "image_uri": "frame_7787.png"}, {"time": 3951.455, "image_uri": "frame_7788.png"}, {"time": 3951.962, "image_uri": "frame_7789.png"}, {"time": 3952.47, "image_uri": "frame_7790.png"}, {"time": 3952.977, "image_uri": "frame_7791.png"}, {"time": 3953.484, "image_uri": "frame_7792.png"}, {"time": 3953.992, "image_uri": "frame_7793.png"}, {"time": 3954.499, "image_uri": "frame_7794.png"}, {"time": 3955.007, "image_uri": "frame_7795.png"}, {"time": 3955.514, "image_uri": "frame_7796.png"}, {"time": 3956.022, "image_uri": "frame_7797.png"}, {"time": 3956.529, "image_uri": "frame_7798.png"}, {"time": 3957.037, "image_uri": "frame_7799.png"}, {"time": 3957.544, "image_uri": "frame_7800.png"}, {"time": 3958.051, "image_uri": "frame_7801.png"}, {"time": 3958.559, "image_uri": "frame_7802.png"}, {"time": 3959.066, "image_uri": "frame_7803.png"}, {"time": 3959.574, "image_uri": "frame_7804.png"}, {"time": 3960.081, "image_uri": "frame_7805.png"}, {"time": 3960.589, "image_uri": "frame_7806.png"}, {"time": 3961.096, "image_uri": "frame_7807.png"}, {"time": 3961.604, "image_uri": "frame_7808.png"}, {"time": 3962.111, "image_uri": "frame_7809.png"}, {"time": 3962.618, "image_uri": "frame_7810.png"}, {"time": 3963.126, "image_uri": "frame_7811.png"}, {"time": 3963.633, "image_uri": "frame_7812.png"}, {"time": 3964.141, "image_uri": "frame_7813.png"}, {"time": 3964.648, "image_uri": "frame_7814.png"}, {"time": 3965.156, "image_uri": "frame_7815.png"}, {"time": 3965.663, "image_uri": "frame_7816.png"}, {"time": 3966.171, "image_uri": "frame_7817.png"}, {"time": 3966.678, "image_uri": "frame_7818.png"}, {"time": 3967.185, "image_uri": "frame_7819.png"}, {"time": 3967.693, "image_uri": "frame_7820.png"}, {"time": 3968.2, "image_uri": "frame_7821.png"}, {"time": 3968.708, "image_uri": "frame_7822.png"}, {"time": 3969.215, "image_uri": "frame_7823.png"}, {"time": 3969.723, "image_uri": "frame_7824.png"}, {"time": 3970.23, "image_uri": "frame_7825.png"}, {"time": 3970.737, "image_uri": "frame_7826.png"}, {"time": 3971.245, "image_uri": "frame_7827.png"}, {"time": 3971.752, "image_uri": "frame_7828.png"}, {"time": 3972.26, "image_uri": "frame_7829.png"}, {"time": 3972.767, "image_uri": "frame_7830.png"}, {"time": 3973.275, "image_uri": "frame_7831.png"}, {"time": 3973.782, "image_uri": "frame_7832.png"}, {"time": 3974.29, "image_uri": "frame_7833.png"}, {"time": 3974.797, "image_uri": "frame_7834.png"}, {"time": 3975.304, "image_uri": "frame_7835.png"}, {"time": 3975.812, "image_uri": "frame_7836.png"}, {"time": 3976.319, "image_uri": "frame_7837.png"}, {"time": 3976.827, "image_uri": "frame_7838.png"}, {"time": 3977.334, "image_uri": "frame_7839.png"}, {"time": 3977.842, "image_uri": "frame_7840.png"}, {"time": 3978.349, "image_uri": "frame_7841.png"}, {"time": 3978.857, "image_uri": "frame_7842.png"}, {"time": 3979.364, "image_uri": "frame_7843.png"}, {"time": 3979.871, "image_uri": "frame_7844.png"}, {"time": 3980.379, "image_uri": "frame_7845.png"}, {"time": 3980.886, "image_uri": "frame_7846.png"}, {"time": 3981.394, "image_uri": "frame_7847.png"}, {"time": 3981.901, "image_uri": "frame_7848.png"}, {"time": 3982.409, "image_uri": "frame_7849.png"}, {"time": 3982.916, "image_uri": "frame_7850.png"}, {"time": 3983.424, "image_uri": "frame_7851.png"}, {"time": 3983.931, "image_uri": "frame_7852.png"}, {"time": 3984.438, "image_uri": "frame_7853.png"}, {"time": 3984.946, "image_uri": "frame_7854.png"}, {"time": 3985.453, "image_uri": "frame_7855.png"}, {"time": 3985.961, "image_uri": "frame_7856.png"}, {"time": 3986.468, "image_uri": "frame_7857.png"}, {"time": 3986.976, "image_uri": "frame_7858.png"}, {"time": 3987.483, "image_uri": "frame_7859.png"}, {"time": 3987.991, "image_uri": "frame_7860.png"}, {"time": 3988.498, "image_uri": "frame_7861.png"}, {"time": 3989.005, "image_uri": "frame_7862.png"}, {"time": 3989.513, "image_uri": "frame_7863.png"}, {"time": 3990.02, "image_uri": "frame_7864.png"}, {"time": 3990.528, "image_uri": "frame_7865.png"}, {"time": 3991.035, "image_uri": "frame_7866.png"}, {"time": 3991.543, "image_uri": "frame_7867.png"}, {"time": 3992.05, "image_uri": "frame_7868.png"}, {"time": 3992.558, "image_uri": "frame_7869.png"}, {"time": 3993.065, "image_uri": "frame_7870.png"}, {"time": 3993.572, "image_uri": "frame_7871.png"}, {"time": 3994.08, "image_uri": "frame_7872.png"}, {"time": 3994.587, "image_uri": "frame_7873.png"}, {"time": 3995.095, "image_uri": "frame_7874.png"}, {"time": 3995.602, "image_uri": "frame_7875.png"}, {"time": 3996.11, "image_uri": "frame_7876.png"}, {"time": 3996.617, "image_uri": "frame_7877.png"}, {"time": 3997.124, "image_uri": "frame_7878.png"}, {"time": 3997.632, "image_uri": "frame_7879.png"}, {"time": 3998.139, "image_uri": "frame_7880.png"}, {"time": 3998.647, "image_uri": "frame_7881.png"}, {"time": 3999.154, "image_uri": "frame_7882.png"}, {"time": 3999.662, "image_uri": "frame_7883.png"}, {"time": 4000.169, "image_uri": "frame_7884.png"}, {"time": 4000.677, "image_uri": "frame_7885.png"}, {"time": 4001.184, "image_uri": "frame_7886.png"}, {"time": 4001.691, "image_uri": "frame_7887.png"}, {"time": 4002.199, "image_uri": "frame_7888.png"}, {"time": 4002.706, "image_uri": "frame_7889.png"}, {"time": 4003.214, "image_uri": "frame_7890.png"}, {"time": 4003.721, "image_uri": "frame_7891.png"}, {"time": 4004.229, "image_uri": "frame_7892.png"}, {"time": 4004.736, "image_uri": "frame_7893.png"}, {"time": 4005.244, "image_uri": "frame_7894.png"}, {"time": 4005.751, "image_uri": "frame_7895.png"}, {"time": 4006.258, "image_uri": "frame_7896.png"}, {"time": 4006.766, "image_uri": "frame_7897.png"}, {"time": 4007.273, "image_uri": "frame_7898.png"}, {"time": 4007.781, "image_uri": "frame_7899.png"}, {"time": 4008.288, "image_uri": "frame_7900.png"}, {"time": 4008.796, "image_uri": "frame_7901.png"}, {"time": 4009.303, "image_uri": "frame_7902.png"}, {"time": 4009.811, "image_uri": "frame_7903.png"}, {"time": 4010.318, "image_uri": "frame_7904.png"}, {"time": 4010.825, "image_uri": "frame_7905.png"}, {"time": 4011.333, "image_uri": "frame_7906.png"}, {"time": 4011.84, "image_uri": "frame_7907.png"}, {"time": 4012.348, "image_uri": "frame_7908.png"}, {"time": 4012.855, "image_uri": "frame_7909.png"}, {"time": 4013.363, "image_uri": "frame_7910.png"}, {"time": 4013.87, "image_uri": "frame_7911.png"}, {"time": 4014.378, "image_uri": "frame_7912.png"}, {"time": 4014.885, "image_uri": "frame_7913.png"}, {"time": 4015.392, "image_uri": "frame_7914.png"}, {"time": 4015.9, "image_uri": "frame_7915.png"}, {"time": 4016.407, "image_uri": "frame_7916.png"}, {"time": 4016.915, "image_uri": "frame_7917.png"}, {"time": 4017.422, "image_uri": "frame_7918.png"}, {"time": 4017.93, "image_uri": "frame_7919.png"}, {"time": 4018.437, "image_uri": "frame_7920.png"}, {"time": 4018.945, "image_uri": "frame_7921.png"}, {"time": 4019.452, "image_uri": "frame_7922.png"}, {"time": 4019.959, "image_uri": "frame_7923.png"}, {"time": 4020.467, "image_uri": "frame_7924.png"}, {"time": 4020.974, "image_uri": "frame_7925.png"}, {"time": 4021.482, "image_uri": "frame_7926.png"}, {"time": 4021.989, "image_uri": "frame_7927.png"}, {"time": 4022.497, "image_uri": "frame_7928.png"}, {"time": 4023.004, "image_uri": "frame_7929.png"}, {"time": 4023.512, "image_uri": "frame_7930.png"}, {"time": 4024.019, "image_uri": "frame_7931.png"}, {"time": 4024.526, "image_uri": "frame_7932.png"}, {"time": 4025.034, "image_uri": "frame_7933.png"}, {"time": 4025.541, "image_uri": "frame_7934.png"}, {"time": 4026.049, "image_uri": "frame_7935.png"}, {"time": 4026.556, "image_uri": "frame_7936.png"}, {"time": 4027.064, "image_uri": "frame_7937.png"}, {"time": 4027.571, "image_uri": "frame_7938.png"}, {"time": 4028.078, "image_uri": "frame_7939.png"}, {"time": 4028.586, "image_uri": "frame_7940.png"}, {"time": 4029.093, "image_uri": "frame_7941.png"}, {"time": 4029.601, "image_uri": "frame_7942.png"}, {"time": 4030.108, "image_uri": "frame_7943.png"}, {"time": 4030.616, "image_uri": "frame_7944.png"}, {"time": 4031.123, "image_uri": "frame_7945.png"}, {"time": 4031.631, "image_uri": "frame_7946.png"}, {"time": 4032.138, "image_uri": "frame_7947.png"}, {"time": 4032.645, "image_uri": "frame_7948.png"}, {"time": 4033.153, "image_uri": "frame_7949.png"}, {"time": 4033.66, "image_uri": "frame_7950.png"}, {"time": 4034.168, "image_uri": "frame_7951.png"}, {"time": 4034.675, "image_uri": "frame_7952.png"}, {"time": 4035.183, "image_uri": "frame_7953.png"}, {"time": 4035.69, "image_uri": "frame_7954.png"}, {"time": 4036.198, "image_uri": "frame_7955.png"}, {"time": 4036.705, "image_uri": "frame_7956.png"}, {"time": 4037.212, "image_uri": "frame_7957.png"}, {"time": 4037.72, "image_uri": "frame_7958.png"}, {"time": 4038.227, "image_uri": "frame_7959.png"}, {"time": 4038.735, "image_uri": "frame_7960.png"}, {"time": 4039.242, "image_uri": "frame_7961.png"}, {"time": 4039.75, "image_uri": "frame_7962.png"}, {"time": 4040.257, "image_uri": "frame_7963.png"}, {"time": 4040.765, "image_uri": "frame_7964.png"}, {"time": 4041.272, "image_uri": "frame_7965.png"}, {"time": 4041.779, "image_uri": "frame_7966.png"}, {"time": 4042.287, "image_uri": "frame_7967.png"}, {"time": 4042.794, "image_uri": "frame_7968.png"}, {"time": 4043.302, "image_uri": "frame_7969.png"}, {"time": 4043.809, "image_uri": "frame_7970.png"}, {"time": 4044.317, "image_uri": "frame_7971.png"}, {"time": 4044.824, "image_uri": "frame_7972.png"}, {"time": 4045.332, "image_uri": "frame_7973.png"}, {"time": 4045.839, "image_uri": "frame_7974.png"}, {"time": 4046.346, "image_uri": "frame_7975.png"}, {"time": 4046.854, "image_uri": "frame_7976.png"}, {"time": 4047.361, "image_uri": "frame_7977.png"}, {"time": 4047.869, "image_uri": "frame_7978.png"}, {"time": 4048.376, "image_uri": "frame_7979.png"}, {"time": 4048.884, "image_uri": "frame_7980.png"}, {"time": 4049.391, "image_uri": "frame_7981.png"}, {"time": 4049.899, "image_uri": "frame_7982.png"}, {"time": 4050.406, "image_uri": "frame_7983.png"}, {"time": 4050.913, "image_uri": "frame_7984.png"}, {"time": 4051.421, "image_uri": "frame_7985.png"}, {"time": 4051.928, "image_uri": "frame_7986.png"}, {"time": 4052.436, "image_uri": "frame_7987.png"}, {"time": 4052.943, "image_uri": "frame_7988.png"}, {"time": 4053.451, "image_uri": "frame_7989.png"}, {"time": 4053.958, "image_uri": "frame_7990.png"}, {"time": 4054.465, "image_uri": "frame_7991.png"}, {"time": 4054.973, "image_uri": "frame_7992.png"}, {"time": 4055.48, "image_uri": "frame_7993.png"}, {"time": 4055.988, "image_uri": "frame_7994.png"}, {"time": 4056.495, "image_uri": "frame_7995.png"}, {"time": 4057.003, "image_uri": "frame_7996.png"}, {"time": 4057.51, "image_uri": "frame_7997.png"}, {"time": 4058.018, "image_uri": "frame_7998.png"}, {"time": 4058.525, "image_uri": "frame_7999.png"}, {"time": 4059.032, "image_uri": "frame_8000.png"}, {"time": 4059.54, "image_uri": "frame_8001.png"}, {"time": 4060.047, "image_uri": "frame_8002.png"}, {"time": 4060.555, "image_uri": "frame_8003.png"}, {"time": 4061.062, "image_uri": "frame_8004.png"}, {"time": 4061.57, "image_uri": "frame_8005.png"}, {"time": 4062.077, "image_uri": "frame_8006.png"}, {"time": 4062.585, "image_uri": "frame_8007.png"}, {"time": 4063.092, "image_uri": "frame_8008.png"}, {"time": 4063.599, "image_uri": "frame_8009.png"}, {"time": 4064.107, "image_uri": "frame_8010.png"}, {"time": 4064.614, "image_uri": "frame_8011.png"}, {"time": 4065.122, "image_uri": "frame_8012.png"}, {"time": 4065.629, "image_uri": "frame_8013.png"}, {"time": 4066.137, "image_uri": "frame_8014.png"}, {"time": 4066.644, "image_uri": "frame_8015.png"}, {"time": 4067.152, "image_uri": "frame_8016.png"}, {"time": 4067.659, "image_uri": "frame_8017.png"}, {"time": 4068.166, "image_uri": "frame_8018.png"}, {"time": 4068.674, "image_uri": "frame_8019.png"}, {"time": 4069.181, "image_uri": "frame_8020.png"}, {"time": 4069.689, "image_uri": "frame_8021.png"}, {"time": 4070.196, "image_uri": "frame_8022.png"}, {"time": 4070.704, "image_uri": "frame_8023.png"}, {"time": 4071.211, "image_uri": "frame_8024.png"}, {"time": 4071.719, "image_uri": "frame_8025.png"}, {"time": 4072.226, "image_uri": "frame_8026.png"}, {"time": 4072.733, "image_uri": "frame_8027.png"}, {"time": 4073.241, "image_uri": "frame_8028.png"}, {"time": 4073.748, "image_uri": "frame_8029.png"}, {"time": 4074.256, "image_uri": "frame_8030.png"}, {"time": 4074.763, "image_uri": "frame_8031.png"}, {"time": 4075.271, "image_uri": "frame_8032.png"}, {"time": 4075.778, "image_uri": "frame_8033.png"}, {"time": 4076.286, "image_uri": "frame_8034.png"}, {"time": 4076.793, "image_uri": "frame_8035.png"}, {"time": 4077.3, "image_uri": "frame_8036.png"}, {"time": 4077.808, "image_uri": "frame_8037.png"}, {"time": 4078.315, "image_uri": "frame_8038.png"}, {"time": 4078.823, "image_uri": "frame_8039.png"}, {"time": 4079.33, "image_uri": "frame_8040.png"}, {"time": 4079.838, "image_uri": "frame_8041.png"}, {"time": 4080.345, "image_uri": "frame_8042.png"}, {"time": 4080.853, "image_uri": "frame_8043.png"}, {"time": 4081.36, "image_uri": "frame_8044.png"}, {"time": 4081.867, "image_uri": "frame_8045.png"}, {"time": 4082.375, "image_uri": "frame_8046.png"}, {"time": 4082.882, "image_uri": "frame_8047.png"}, {"time": 4083.39, "image_uri": "frame_8048.png"}, {"time": 4083.897, "image_uri": "frame_8049.png"}, {"time": 4084.405, "image_uri": "frame_8050.png"}, {"time": 4084.912, "image_uri": "frame_8051.png"}, {"time": 4085.419, "image_uri": "frame_8052.png"}, {"time": 4085.927, "image_uri": "frame_8053.png"}, {"time": 4086.434, "image_uri": "frame_8054.png"}, {"time": 4086.942, "image_uri": "frame_8055.png"}, {"time": 4087.449, "image_uri": "frame_8056.png"}, {"time": 4087.957, "image_uri": "frame_8057.png"}, {"time": 4088.464, "image_uri": "frame_8058.png"}, {"time": 4088.972, "image_uri": "frame_8059.png"}, {"time": 4089.479, "image_uri": "frame_8060.png"}, {"time": 4089.986, "image_uri": "frame_8061.png"}, {"time": 4090.494, "image_uri": "frame_8062.png"}, {"time": 4091.001, "image_uri": "frame_8063.png"}, {"time": 4091.509, "image_uri": "frame_8064.png"}, {"time": 4092.016, "image_uri": "frame_8065.png"}, {"time": 4092.524, "image_uri": "frame_8066.png"}, {"time": 4093.031, "image_uri": "frame_8067.png"}, {"time": 4093.539, "image_uri": "frame_8068.png"}, {"time": 4094.046, "image_uri": "frame_8069.png"}, {"time": 4094.553, "image_uri": "frame_8070.png"}, {"time": 4095.061, "image_uri": "frame_8071.png"}, {"time": 4095.568, "image_uri": "frame_8072.png"}, {"time": 4096.076, "image_uri": "frame_8073.png"}, {"time": 4096.583, "image_uri": "frame_8074.png"}, {"time": 4097.091, "image_uri": "frame_8075.png"}, {"time": 4097.598, "image_uri": "frame_8076.png"}, {"time": 4098.106, "image_uri": "frame_8077.png"}, {"time": 4098.613, "image_uri": "frame_8078.png"}, {"time": 4099.12, "image_uri": "frame_8079.png"}, {"time": 4099.628, "image_uri": "frame_8080.png"}, {"time": 4100.135, "image_uri": "frame_8081.png"}, {"time": 4100.643, "image_uri": "frame_8082.png"}, {"time": 4101.15, "image_uri": "frame_8083.png"}, {"time": 4101.658, "image_uri": "frame_8084.png"}, {"time": 4102.165, "image_uri": "frame_8085.png"}, {"time": 4102.673, "image_uri": "frame_8086.png"}, {"time": 4103.18, "image_uri": "frame_8087.png"}, {"time": 4103.687, "image_uri": "frame_8088.png"}, {"time": 4104.195, "image_uri": "frame_8089.png"}, {"time": 4104.702, "image_uri": "frame_8090.png"}, {"time": 4105.21, "image_uri": "frame_8091.png"}, {"time": 4105.717, "image_uri": "frame_8092.png"}, {"time": 4106.225, "image_uri": "frame_8093.png"}, {"time": 4106.732, "image_uri": "frame_8094.png"}, {"time": 4107.24, "image_uri": "frame_8095.png"}, {"time": 4107.747, "image_uri": "frame_8096.png"}, {"time": 4108.254, "image_uri": "frame_8097.png"}, {"time": 4108.762, "image_uri": "frame_8098.png"}, {"time": 4109.269, "image_uri": "frame_8099.png"}, {"time": 4109.777, "image_uri": "frame_8100.png"}, {"time": 4110.284, "image_uri": "frame_8101.png"}, {"time": 4110.792, "image_uri": "frame_8102.png"}, {"time": 4111.299, "image_uri": "frame_8103.png"}, {"time": 4111.806, "image_uri": "frame_8104.png"}, {"time": 4112.314, "image_uri": "frame_8105.png"}, {"time": 4112.821, "image_uri": "frame_8106.png"}, {"time": 4113.329, "image_uri": "frame_8107.png"}, {"time": 4113.836, "image_uri": "frame_8108.png"}, {"time": 4114.344, "image_uri": "frame_8109.png"}, {"time": 4114.851, "image_uri": "frame_8110.png"}, {"time": 4115.359, "image_uri": "frame_8111.png"}, {"time": 4115.866, "image_uri": "frame_8112.png"}, {"time": 4116.373, "image_uri": "frame_8113.png"}, {"time": 4116.881, "image_uri": "frame_8114.png"}, {"time": 4117.388, "image_uri": "frame_8115.png"}, {"time": 4117.896, "image_uri": "frame_8116.png"}, {"time": 4118.403, "image_uri": "frame_8117.png"}, {"time": 4118.911, "image_uri": "frame_8118.png"}, {"time": 4119.418, "image_uri": "frame_8119.png"}, {"time": 4119.926, "image_uri": "frame_8120.png"}, {"time": 4120.433, "image_uri": "frame_8121.png"}, {"time": 4120.94, "image_uri": "frame_8122.png"}, {"time": 4121.448, "image_uri": "frame_8123.png"}, {"time": 4121.955, "image_uri": "frame_8124.png"}, {"time": 4122.463, "image_uri": "frame_8125.png"}, {"time": 4122.97, "image_uri": "frame_8126.png"}, {"time": 4123.478, "image_uri": "frame_8127.png"}, {"time": 4123.985, "image_uri": "frame_8128.png"}, {"time": 4124.493, "image_uri": "frame_8129.png"}, {"time": 4125.0, "image_uri": "frame_8130.png"}, {"time": 4125.507, "image_uri": "frame_8131.png"}, {"time": 4126.015, "image_uri": "frame_8132.png"}, {"time": 4126.522, "image_uri": "frame_8133.png"}, {"time": 4127.03, "image_uri": "frame_8134.png"}, {"time": 4127.537, "image_uri": "frame_8135.png"}, {"time": 4128.045, "image_uri": "frame_8136.png"}, {"time": 4128.552, "image_uri": "frame_8137.png"}, {"time": 4129.06, "image_uri": "frame_8138.png"}, {"time": 4129.567, "image_uri": "frame_8139.png"}, {"time": 4130.074, "image_uri": "frame_8140.png"}, {"time": 4130.582, "image_uri": "frame_8141.png"}, {"time": 4131.089, "image_uri": "frame_8142.png"}, {"time": 4131.597, "image_uri": "frame_8143.png"}, {"time": 4132.104, "image_uri": "frame_8144.png"}, {"time": 4132.612, "image_uri": "frame_8145.png"}, {"time": 4133.119, "image_uri": "frame_8146.png"}, {"time": 4133.627, "image_uri": "frame_8147.png"}, {"time": 4134.134, "image_uri": "frame_8148.png"}, {"time": 4134.641, "image_uri": "frame_8149.png"}, {"time": 4135.149, "image_uri": "frame_8150.png"}, {"time": 4135.656, "image_uri": "frame_8151.png"}, {"time": 4136.164, "image_uri": "frame_8152.png"}, {"time": 4136.671, "image_uri": "frame_8153.png"}, {"time": 4137.179, "image_uri": "frame_8154.png"}, {"time": 4137.686, "image_uri": "frame_8155.png"}, {"time": 4138.194, "image_uri": "frame_8156.png"}, {"time": 4138.701, "image_uri": "frame_8157.png"}, {"time": 4139.208, "image_uri": "frame_8158.png"}, {"time": 4139.716, "image_uri": "frame_8159.png"}, {"time": 4140.223, "image_uri": "frame_8160.png"}, {"time": 4140.731, "image_uri": "frame_8161.png"}, {"time": 4141.238, "image_uri": "frame_8162.png"}, {"time": 4141.746, "image_uri": "frame_8163.png"}, {"time": 4142.253, "image_uri": "frame_8164.png"}, {"time": 4142.76, "image_uri": "frame_8165.png"}, {"time": 4143.268, "image_uri": "frame_8166.png"}, {"time": 4143.775, "image_uri": "frame_8167.png"}, {"time": 4144.283, "image_uri": "frame_8168.png"}, {"time": 4144.79, "image_uri": "frame_8169.png"}, {"time": 4145.298, "image_uri": "frame_8170.png"}, {"time": 4145.805, "image_uri": "frame_8171.png"}, {"time": 4146.313, "image_uri": "frame_8172.png"}, {"time": 4146.82, "image_uri": "frame_8173.png"}, {"time": 4147.327, "image_uri": "frame_8174.png"}, {"time": 4147.835, "image_uri": "frame_8175.png"}, {"time": 4148.342, "image_uri": "frame_8176.png"}, {"time": 4148.85, "image_uri": "frame_8177.png"}, {"time": 4149.357, "image_uri": "frame_8178.png"}, {"time": 4149.865, "image_uri": "frame_8179.png"}, {"time": 4150.372, "image_uri": "frame_8180.png"}, {"time": 4150.88, "image_uri": "frame_8181.png"}, {"time": 4151.387, "image_uri": "frame_8182.png"}, {"time": 4151.894, "image_uri": "frame_8183.png"}, {"time": 4152.402, "image_uri": "frame_8184.png"}, {"time": 4152.909, "image_uri": "frame_8185.png"}, {"time": 4153.417, "image_uri": "frame_8186.png"}, {"time": 4153.924, "image_uri": "frame_8187.png"}, {"time": 4154.432, "image_uri": "frame_8188.png"}, {"time": 4154.939, "image_uri": "frame_8189.png"}, {"time": 4155.447, "image_uri": "frame_8190.png"}, {"time": 4155.954, "image_uri": "frame_8191.png"}, {"time": 4156.461, "image_uri": "frame_8192.png"}, {"time": 4156.969, "image_uri": "frame_8193.png"}, {"time": 4157.476, "image_uri": "frame_8194.png"}, {"time": 4157.984, "image_uri": "frame_8195.png"}, {"time": 4158.491, "image_uri": "frame_8196.png"}, {"time": 4158.999, "image_uri": "frame_8197.png"}, {"time": 4159.506, "image_uri": "frame_8198.png"}, {"time": 4160.014, "image_uri": "frame_8199.png"}, {"time": 4160.521, "image_uri": "frame_8200.png"}, {"time": 4161.028, "image_uri": "frame_8201.png"}, {"time": 4161.536, "image_uri": "frame_8202.png"}, {"time": 4162.043, "image_uri": "frame_8203.png"}, {"time": 4162.551, "image_uri": "frame_8204.png"}, {"time": 4163.058, "image_uri": "frame_8205.png"}, {"time": 4163.566, "image_uri": "frame_8206.png"}, {"time": 4164.073, "image_uri": "frame_8207.png"}, {"time": 4164.581, "image_uri": "frame_8208.png"}, {"time": 4165.088, "image_uri": "frame_8209.png"}, {"time": 4165.595, "image_uri": "frame_8210.png"}, {"time": 4166.103, "image_uri": "frame_8211.png"}, {"time": 4166.61, "image_uri": "frame_8212.png"}, {"time": 4167.118, "image_uri": "frame_8213.png"}, {"time": 4167.625, "image_uri": "frame_8214.png"}, {"time": 4168.133, "image_uri": "frame_8215.png"}, {"time": 4168.64, "image_uri": "frame_8216.png"}, {"time": 4169.147, "image_uri": "frame_8217.png"}, {"time": 4169.655, "image_uri": "frame_8218.png"}, {"time": 4170.162, "image_uri": "frame_8219.png"}, {"time": 4170.67, "image_uri": "frame_8220.png"}, {"time": 4171.177, "image_uri": "frame_8221.png"}, {"time": 4171.685, "image_uri": "frame_8222.png"}, {"time": 4172.192, "image_uri": "frame_8223.png"}, {"time": 4172.7, "image_uri": "frame_8224.png"}, {"time": 4173.207, "image_uri": "frame_8225.png"}, {"time": 4173.714, "image_uri": "frame_8226.png"}, {"time": 4174.222, "image_uri": "frame_8227.png"}, {"time": 4174.729, "image_uri": "frame_8228.png"}, {"time": 4175.237, "image_uri": "frame_8229.png"}, {"time": 4175.744, "image_uri": "frame_8230.png"}, {"time": 4176.252, "image_uri": "frame_8231.png"}, {"time": 4176.759, "image_uri": "frame_8232.png"}, {"time": 4177.267, "image_uri": "frame_8233.png"}, {"time": 4177.774, "image_uri": "frame_8234.png"}, {"time": 4178.281, "image_uri": "frame_8235.png"}, {"time": 4178.789, "image_uri": "frame_8236.png"}, {"time": 4179.296, "image_uri": "frame_8237.png"}, {"time": 4179.804, "image_uri": "frame_8238.png"}, {"time": 4180.311, "image_uri": "frame_8239.png"}, {"time": 4180.819, "image_uri": "frame_8240.png"}, {"time": 4181.326, "image_uri": "frame_8241.png"}, {"time": 4181.834, "image_uri": "frame_8242.png"}, {"time": 4182.341, "image_uri": "frame_8243.png"}, {"time": 4182.848, "image_uri": "frame_8244.png"}, {"time": 4183.356, "image_uri": "frame_8245.png"}, {"time": 4183.863, "image_uri": "frame_8246.png"}, {"time": 4184.371, "image_uri": "frame_8247.png"}, {"time": 4184.878, "image_uri": "frame_8248.png"}, {"time": 4185.386, "image_uri": "frame_8249.png"}, {"time": 4185.893, "image_uri": "frame_8250.png"}, {"time": 4186.401, "image_uri": "frame_8251.png"}, {"time": 4186.908, "image_uri": "frame_8252.png"}, {"time": 4187.415, "image_uri": "frame_8253.png"}, {"time": 4187.923, "image_uri": "frame_8254.png"}, {"time": 4188.43, "image_uri": "frame_8255.png"}, {"time": 4188.938, "image_uri": "frame_8256.png"}, {"time": 4189.445, "image_uri": "frame_8257.png"}, {"time": 4189.953, "image_uri": "frame_8258.png"}, {"time": 4190.46, "image_uri": "frame_8259.png"}, {"time": 4190.968, "image_uri": "frame_8260.png"}, {"time": 4191.475, "image_uri": "frame_8261.png"}, {"time": 4191.982, "image_uri": "frame_8262.png"}, {"time": 4192.49, "image_uri": "frame_8263.png"}, {"time": 4192.997, "image_uri": "frame_8264.png"}, {"time": 4193.505, "image_uri": "frame_8265.png"}, {"time": 4194.012, "image_uri": "frame_8266.png"}, {"time": 4194.52, "image_uri": "frame_8267.png"}, {"time": 4195.027, "image_uri": "frame_8268.png"}, {"time": 4195.535, "image_uri": "frame_8269.png"}, {"time": 4196.042, "image_uri": "frame_8270.png"}, {"time": 4196.549, "image_uri": "frame_8271.png"}, {"time": 4197.057, "image_uri": "frame_8272.png"}, {"time": 4197.564, "image_uri": "frame_8273.png"}, {"time": 4198.072, "image_uri": "frame_8274.png"}, {"time": 4198.579, "image_uri": "frame_8275.png"}, {"time": 4199.087, "image_uri": "frame_8276.png"}, {"time": 4199.594, "image_uri": "frame_8277.png"}, {"time": 4200.101, "image_uri": "frame_8278.png"}, {"time": 4200.609, "image_uri": "frame_8279.png"}, {"time": 4201.116, "image_uri": "frame_8280.png"}, {"time": 4201.624, "image_uri": "frame_8281.png"}, {"time": 4202.131, "image_uri": "frame_8282.png"}, {"time": 4202.639, "image_uri": "frame_8283.png"}, {"time": 4203.146, "image_uri": "frame_8284.png"}, {"time": 4203.654, "image_uri": "frame_8285.png"}, {"time": 4204.161, "image_uri": "frame_8286.png"}, {"time": 4204.668, "image_uri": "frame_8287.png"}, {"time": 4205.176, "image_uri": "frame_8288.png"}, {"time": 4205.683, "image_uri": "frame_8289.png"}, {"time": 4206.191, "image_uri": "frame_8290.png"}, {"time": 4206.698, "image_uri": "frame_8291.png"}, {"time": 4207.206, "image_uri": "frame_8292.png"}, {"time": 4207.713, "image_uri": "frame_8293.png"}, {"time": 4208.221, "image_uri": "frame_8294.png"}, {"time": 4208.728, "image_uri": "frame_8295.png"}, {"time": 4209.235, "image_uri": "frame_8296.png"}, {"time": 4209.743, "image_uri": "frame_8297.png"}, {"time": 4210.25, "image_uri": "frame_8298.png"}, {"time": 4210.758, "image_uri": "frame_8299.png"}, {"time": 4211.265, "image_uri": "frame_8300.png"}, {"time": 4211.773, "image_uri": "frame_8301.png"}, {"time": 4212.28, "image_uri": "frame_8302.png"}, {"time": 4212.788, "image_uri": "frame_8303.png"}, {"time": 4213.295, "image_uri": "frame_8304.png"}, {"time": 4213.802, "image_uri": "frame_8305.png"}, {"time": 4214.31, "image_uri": "frame_8306.png"}, {"time": 4214.817, "image_uri": "frame_8307.png"}, {"time": 4215.325, "image_uri": "frame_8308.png"}, {"time": 4215.832, "image_uri": "frame_8309.png"}, {"time": 4216.34, "image_uri": "frame_8310.png"}, {"time": 4216.847, "image_uri": "frame_8311.png"}, {"time": 4217.355, "image_uri": "frame_8312.png"}, {"time": 4217.862, "image_uri": "frame_8313.png"}, {"time": 4218.369, "image_uri": "frame_8314.png"}, {"time": 4218.877, "image_uri": "frame_8315.png"}, {"time": 4219.384, "image_uri": "frame_8316.png"}, {"time": 4219.892, "image_uri": "frame_8317.png"}, {"time": 4220.399, "image_uri": "frame_8318.png"}, {"time": 4220.907, "image_uri": "frame_8319.png"}, {"time": 4221.414, "image_uri": "frame_8320.png"}, {"time": 4221.922, "image_uri": "frame_8321.png"}, {"time": 4222.429, "image_uri": "frame_8322.png"}, {"time": 4222.936, "image_uri": "frame_8323.png"}, {"time": 4223.444, "image_uri": "frame_8324.png"}, {"time": 4223.951, "image_uri": "frame_8325.png"}, {"time": 4224.459, "image_uri": "frame_8326.png"}, {"time": 4224.966, "image_uri": "frame_8327.png"}, {"time": 4225.474, "image_uri": "frame_8328.png"}, {"time": 4225.981, "image_uri": "frame_8329.png"}, {"time": 4226.488, "image_uri": "frame_8330.png"}, {"time": 4226.996, "image_uri": "frame_8331.png"}, {"time": 4227.503, "image_uri": "frame_8332.png"}, {"time": 4228.011, "image_uri": "frame_8333.png"}, {"time": 4228.518, "image_uri": "frame_8334.png"}, {"time": 4229.026, "image_uri": "frame_8335.png"}, {"time": 4229.533, "image_uri": "frame_8336.png"}, {"time": 4230.041, "image_uri": "frame_8337.png"}, {"time": 4230.548, "image_uri": "frame_8338.png"}, {"time": 4231.055, "image_uri": "frame_8339.png"}, {"time": 4231.563, "image_uri": "frame_8340.png"}, {"time": 4232.07, "image_uri": "frame_8341.png"}, {"time": 4232.578, "image_uri": "frame_8342.png"}, {"time": 4233.085, "image_uri": "frame_8343.png"}, {"time": 4233.593, "image_uri": "frame_8344.png"}, {"time": 4234.1, "image_uri": "frame_8345.png"}, {"time": 4234.608, "image_uri": "frame_8346.png"}, {"time": 4235.115, "image_uri": "frame_8347.png"}, {"time": 4235.622, "image_uri": "frame_8348.png"}, {"time": 4236.13, "image_uri": "frame_8349.png"}, {"time": 4236.637, "image_uri": "frame_8350.png"}, {"time": 4237.145, "image_uri": "frame_8351.png"}, {"time": 4237.652, "image_uri": "frame_8352.png"}, {"time": 4238.16, "image_uri": "frame_8353.png"}, {"time": 4238.667, "image_uri": "frame_8354.png"}, {"time": 4239.175, "image_uri": "frame_8355.png"}, {"time": 4239.682, "image_uri": "frame_8356.png"}, {"time": 4240.189, "image_uri": "frame_8357.png"}, {"time": 4240.697, "image_uri": "frame_8358.png"}, {"time": 4241.204, "image_uri": "frame_8359.png"}, {"time": 4241.712, "image_uri": "frame_8360.png"}, {"time": 4242.219, "image_uri": "frame_8361.png"}, {"time": 4242.727, "image_uri": "frame_8362.png"}, {"time": 4243.234, "image_uri": "frame_8363.png"}, {"time": 4243.742, "image_uri": "frame_8364.png"}, {"time": 4244.249, "image_uri": "frame_8365.png"}, {"time": 4244.756, "image_uri": "frame_8366.png"}, {"time": 4245.264, "image_uri": "frame_8367.png"}, {"time": 4245.771, "image_uri": "frame_8368.png"}, {"time": 4246.279, "image_uri": "frame_8369.png"}, {"time": 4246.786, "image_uri": "frame_8370.png"}, {"time": 4247.294, "image_uri": "frame_8371.png"}, {"time": 4247.801, "image_uri": "frame_8372.png"}, {"time": 4248.309, "image_uri": "frame_8373.png"}, {"time": 4248.816, "image_uri": "frame_8374.png"}, {"time": 4249.323, "image_uri": "frame_8375.png"}, {"time": 4249.831, "image_uri": "frame_8376.png"}, {"time": 4250.338, "image_uri": "frame_8377.png"}, {"time": 4250.846, "image_uri": "frame_8378.png"}, {"time": 4251.353, "image_uri": "frame_8379.png"}, {"time": 4251.861, "image_uri": "frame_8380.png"}, {"time": 4252.368, "image_uri": "frame_8381.png"}, {"time": 4252.876, "image_uri": "frame_8382.png"}, {"time": 4253.383, "image_uri": "frame_8383.png"}, {"time": 4253.89, "image_uri": "frame_8384.png"}, {"time": 4254.398, "image_uri": "frame_8385.png"}, {"time": 4254.905, "image_uri": "frame_8386.png"}, {"time": 4255.413, "image_uri": "frame_8387.png"}, {"time": 4255.92, "image_uri": "frame_8388.png"}, {"time": 4256.428, "image_uri": "frame_8389.png"}, {"time": 4256.935, "image_uri": "frame_8390.png"}, {"time": 4257.442, "image_uri": "frame_8391.png"}, {"time": 4257.95, "image_uri": "frame_8392.png"}, {"time": 4258.457, "image_uri": "frame_8393.png"}, {"time": 4258.965, "image_uri": "frame_8394.png"}, {"time": 4259.472, "image_uri": "frame_8395.png"}, {"time": 4259.98, "image_uri": "frame_8396.png"}, {"time": 4260.487, "image_uri": "frame_8397.png"}, {"time": 4260.995, "image_uri": "frame_8398.png"}, {"time": 4261.502, "image_uri": "frame_8399.png"}, {"time": 4262.009, "image_uri": "frame_8400.png"}, {"time": 4262.517, "image_uri": "frame_8401.png"}, {"time": 4263.024, "image_uri": "frame_8402.png"}, {"time": 4263.532, "image_uri": "frame_8403.png"}, {"time": 4264.039, "image_uri": "frame_8404.png"}, {"time": 4264.547, "image_uri": "frame_8405.png"}, {"time": 4265.054, "image_uri": "frame_8406.png"}, {"time": 4265.562, "image_uri": "frame_8407.png"}, {"time": 4266.069, "image_uri": "frame_8408.png"}, {"time": 4266.576, "image_uri": "frame_8409.png"}, {"time": 4267.084, "image_uri": "frame_8410.png"}, {"time": 4267.591, "image_uri": "frame_8411.png"}, {"time": 4268.099, "image_uri": "frame_8412.png"}, {"time": 4268.606, "image_uri": "frame_8413.png"}, {"time": 4269.114, "image_uri": "frame_8414.png"}, {"time": 4269.621, "image_uri": "frame_8415.png"}, {"time": 4270.129, "image_uri": "frame_8416.png"}, {"time": 4270.636, "image_uri": "frame_8417.png"}, {"time": 4271.143, "image_uri": "frame_8418.png"}, {"time": 4271.651, "image_uri": "frame_8419.png"}, {"time": 4272.158, "image_uri": "frame_8420.png"}, {"time": 4272.666, "image_uri": "frame_8421.png"}, {"time": 4273.173, "image_uri": "frame_8422.png"}, {"time": 4273.681, "image_uri": "frame_8423.png"}, {"time": 4274.188, "image_uri": "frame_8424.png"}, {"time": 4274.696, "image_uri": "frame_8425.png"}, {"time": 4275.203, "image_uri": "frame_8426.png"}, {"time": 4275.71, "image_uri": "frame_8427.png"}, {"time": 4276.218, "image_uri": "frame_8428.png"}, {"time": 4276.725, "image_uri": "frame_8429.png"}, {"time": 4277.233, "image_uri": "frame_8430.png"}, {"time": 4277.74, "image_uri": "frame_8431.png"}, {"time": 4278.248, "image_uri": "frame_8432.png"}, {"time": 4278.755, "image_uri": "frame_8433.png"}, {"time": 4279.263, "image_uri": "frame_8434.png"}, {"time": 4279.77, "image_uri": "frame_8435.png"}, {"time": 4280.277, "image_uri": "frame_8436.png"}, {"time": 4280.785, "image_uri": "frame_8437.png"}, {"time": 4281.292, "image_uri": "frame_8438.png"}, {"time": 4281.8, "image_uri": "frame_8439.png"}, {"time": 4282.307, "image_uri": "frame_8440.png"}, {"time": 4282.815, "image_uri": "frame_8441.png"}, {"time": 4283.322, "image_uri": "frame_8442.png"}, {"time": 4283.829, "image_uri": "frame_8443.png"}, {"time": 4284.337, "image_uri": "frame_8444.png"}, {"time": 4284.844, "image_uri": "frame_8445.png"}, {"time": 4285.352, "image_uri": "frame_8446.png"}, {"time": 4285.859, "image_uri": "frame_8447.png"}, {"time": 4286.367, "image_uri": "frame_8448.png"}, {"time": 4286.874, "image_uri": "frame_8449.png"}, {"time": 4287.382, "image_uri": "frame_8450.png"}, {"time": 4287.889, "image_uri": "frame_8451.png"}, {"time": 4288.396, "image_uri": "frame_8452.png"}, {"time": 4288.904, "image_uri": "frame_8453.png"}, {"time": 4289.411, "image_uri": "frame_8454.png"}, {"time": 4289.919, "image_uri": "frame_8455.png"}, {"time": 4290.426, "image_uri": "frame_8456.png"}, {"time": 4290.934, "image_uri": "frame_8457.png"}, {"time": 4291.441, "image_uri": "frame_8458.png"}, {"time": 4291.949, "image_uri": "frame_8459.png"}, {"time": 4292.456, "image_uri": "frame_8460.png"}, {"time": 4292.963, "image_uri": "frame_8461.png"}, {"time": 4293.471, "image_uri": "frame_8462.png"}, {"time": 4293.978, "image_uri": "frame_8463.png"}, {"time": 4294.486, "image_uri": "frame_8464.png"}, {"time": 4294.993, "image_uri": "frame_8465.png"}, {"time": 4295.501, "image_uri": "frame_8466.png"}, {"time": 4296.008, "image_uri": "frame_8467.png"}, {"time": 4296.516, "image_uri": "frame_8468.png"}, {"time": 4297.023, "image_uri": "frame_8469.png"}, {"time": 4297.53, "image_uri": "frame_8470.png"}, {"time": 4298.038, "image_uri": "frame_8471.png"}, {"time": 4298.545, "image_uri": "frame_8472.png"}, {"time": 4299.053, "image_uri": "frame_8473.png"}, {"time": 4299.56, "image_uri": "frame_8474.png"}, {"time": 4300.068, "image_uri": "frame_8475.png"}, {"time": 4300.575, "image_uri": "frame_8476.png"}, {"time": 4301.083, "image_uri": "frame_8477.png"}, {"time": 4301.59, "image_uri": "frame_8478.png"}, {"time": 4302.097, "image_uri": "frame_8479.png"}, {"time": 4302.605, "image_uri": "frame_8480.png"}, {"time": 4303.112, "image_uri": "frame_8481.png"}, {"time": 4303.62, "image_uri": "frame_8482.png"}, {"time": 4304.127, "image_uri": "frame_8483.png"}, {"time": 4304.635, "image_uri": "frame_8484.png"}, {"time": 4305.142, "image_uri": "frame_8485.png"}, {"time": 4305.65, "image_uri": "frame_8486.png"}, {"time": 4306.157, "image_uri": "frame_8487.png"}, {"time": 4306.664, "image_uri": "frame_8488.png"}, {"time": 4307.172, "image_uri": "frame_8489.png"}, {"time": 4307.679, "image_uri": "frame_8490.png"}, {"time": 4308.187, "image_uri": "frame_8491.png"}, {"time": 4308.694, "image_uri": "frame_8492.png"}, {"time": 4309.202, "image_uri": "frame_8493.png"}, {"time": 4309.709, "image_uri": "frame_8494.png"}, {"time": 4310.217, "image_uri": "frame_8495.png"}, {"time": 4310.724, "image_uri": "frame_8496.png"}, {"time": 4311.231, "image_uri": "frame_8497.png"}, {"time": 4311.739, "image_uri": "frame_8498.png"}, {"time": 4312.246, "image_uri": "frame_8499.png"}, {"time": 4312.754, "image_uri": "frame_8500.png"}, {"time": 4313.261, "image_uri": "frame_8501.png"}, {"time": 4313.769, "image_uri": "frame_8502.png"}, {"time": 4314.276, "image_uri": "frame_8503.png"}, {"time": 4314.783, "image_uri": "frame_8504.png"}, {"time": 4315.291, "image_uri": "frame_8505.png"}, {"time": 4315.798, "image_uri": "frame_8506.png"}, {"time": 4316.306, "image_uri": "frame_8507.png"}, {"time": 4316.813, "image_uri": "frame_8508.png"}, {"time": 4317.321, "image_uri": "frame_8509.png"}, {"time": 4317.828, "image_uri": "frame_8510.png"}, {"time": 4318.336, "image_uri": "frame_8511.png"}, {"time": 4318.843, "image_uri": "frame_8512.png"}, {"time": 4319.35, "image_uri": "frame_8513.png"}, {"time": 4319.858, "image_uri": "frame_8514.png"}, {"time": 4320.365, "image_uri": "frame_8515.png"}, {"time": 4320.873, "image_uri": "frame_8516.png"}, {"time": 4321.38, "image_uri": "frame_8517.png"}, {"time": 4321.888, "image_uri": "frame_8518.png"}, {"time": 4322.395, "image_uri": "frame_8519.png"}, {"time": 4322.903, "image_uri": "frame_8520.png"}, {"time": 4323.41, "image_uri": "frame_8521.png"}, {"time": 4323.917, "image_uri": "frame_8522.png"}, {"time": 4324.425, "image_uri": "frame_8523.png"}, {"time": 4324.932, "image_uri": "frame_8524.png"}, {"time": 4325.44, "image_uri": "frame_8525.png"}, {"time": 4325.947, "image_uri": "frame_8526.png"}, {"time": 4326.455, "image_uri": "frame_8527.png"}, {"time": 4326.962, "image_uri": "frame_8528.png"}, {"time": 4327.47, "image_uri": "frame_8529.png"}, {"time": 4327.977, "image_uri": "frame_8530.png"}, {"time": 4328.484, "image_uri": "frame_8531.png"}, {"time": 4328.992, "image_uri": "frame_8532.png"}, {"time": 4329.499, "image_uri": "frame_8533.png"}, {"time": 4330.007, "image_uri": "frame_8534.png"}, {"time": 4330.514, "image_uri": "frame_8535.png"}, {"time": 4331.022, "image_uri": "frame_8536.png"}, {"time": 4331.529, "image_uri": "frame_8537.png"}, {"time": 4332.037, "image_uri": "frame_8538.png"}, {"time": 4332.544, "image_uri": "frame_8539.png"}, {"time": 4333.051, "image_uri": "frame_8540.png"}, {"time": 4333.559, "image_uri": "frame_8541.png"}, {"time": 4334.066, "image_uri": "frame_8542.png"}, {"time": 4334.574, "image_uri": "frame_8543.png"}, {"time": 4335.081, "image_uri": "frame_8544.png"}, {"time": 4335.589, "image_uri": "frame_8545.png"}, {"time": 4336.096, "image_uri": "frame_8546.png"}, {"time": 4336.604, "image_uri": "frame_8547.png"}, {"time": 4337.111, "image_uri": "frame_8548.png"}, {"time": 4337.618, "image_uri": "frame_8549.png"}, {"time": 4338.126, "image_uri": "frame_8550.png"}, {"time": 4338.633, "image_uri": "frame_8551.png"}, {"time": 4339.141, "image_uri": "frame_8552.png"}, {"time": 4339.648, "image_uri": "frame_8553.png"}, {"time": 4340.156, "image_uri": "frame_8554.png"}, {"time": 4340.663, "image_uri": "frame_8555.png"}, {"time": 4341.171, "image_uri": "frame_8556.png"}, {"time": 4341.678, "image_uri": "frame_8557.png"}, {"time": 4342.185, "image_uri": "frame_8558.png"}, {"time": 4342.693, "image_uri": "frame_8559.png"}, {"time": 4343.2, "image_uri": "frame_8560.png"}, {"time": 4343.708, "image_uri": "frame_8561.png"}, {"time": 4344.215, "image_uri": "frame_8562.png"}, {"time": 4344.723, "image_uri": "frame_8563.png"}, {"time": 4345.23, "image_uri": "frame_8564.png"}, {"time": 4345.737, "image_uri": "frame_8565.png"}, {"time": 4346.245, "image_uri": "frame_8566.png"}, {"time": 4346.752, "image_uri": "frame_8567.png"}, {"time": 4347.26, "image_uri": "frame_8568.png"}, {"time": 4347.767, "image_uri": "frame_8569.png"}, {"time": 4348.275, "image_uri": "frame_8570.png"}, {"time": 4348.782, "image_uri": "frame_8571.png"}, {"time": 4349.29, "image_uri": "frame_8572.png"}, {"time": 4349.797, "image_uri": "frame_8573.png"}, {"time": 4350.304, "image_uri": "frame_8574.png"}, {"time": 4350.812, "image_uri": "frame_8575.png"}, {"time": 4351.319, "image_uri": "frame_8576.png"}, {"time": 4351.827, "image_uri": "frame_8577.png"}, {"time": 4352.334, "image_uri": "frame_8578.png"}, {"time": 4352.842, "image_uri": "frame_8579.png"}, {"time": 4353.349, "image_uri": "frame_8580.png"}, {"time": 4353.857, "image_uri": "frame_8581.png"}, {"time": 4354.364, "image_uri": "frame_8582.png"}, {"time": 4354.871, "image_uri": "frame_8583.png"}, {"time": 4355.379, "image_uri": "frame_8584.png"}, {"time": 4355.886, "image_uri": "frame_8585.png"}, {"time": 4356.394, "image_uri": "frame_8586.png"}, {"time": 4356.901, "image_uri": "frame_8587.png"}, {"time": 4357.409, "image_uri": "frame_8588.png"}, {"time": 4357.916, "image_uri": "frame_8589.png"}, {"time": 4358.424, "image_uri": "frame_8590.png"}, {"time": 4358.931, "image_uri": "frame_8591.png"}, {"time": 4359.438, "image_uri": "frame_8592.png"}, {"time": 4359.946, "image_uri": "frame_8593.png"}, {"time": 4360.453, "image_uri": "frame_8594.png"}, {"time": 4360.961, "image_uri": "frame_8595.png"}, {"time": 4361.468, "image_uri": "frame_8596.png"}, {"time": 4361.976, "image_uri": "frame_8597.png"}, {"time": 4362.483, "image_uri": "frame_8598.png"}, {"time": 4362.991, "image_uri": "frame_8599.png"}, {"time": 4363.498, "image_uri": "frame_8600.png"}, {"time": 4364.005, "image_uri": "frame_8601.png"}, {"time": 4364.513, "image_uri": "frame_8602.png"}, {"time": 4365.02, "image_uri": "frame_8603.png"}, {"time": 4365.528, "image_uri": "frame_8604.png"}, {"time": 4366.035, "image_uri": "frame_8605.png"}, {"time": 4366.543, "image_uri": "frame_8606.png"}, {"time": 4367.05, "image_uri": "frame_8607.png"}, {"time": 4367.558, "image_uri": "frame_8608.png"}, {"time": 4368.065, "image_uri": "frame_8609.png"}, {"time": 4368.572, "image_uri": "frame_8610.png"}, {"time": 4369.08, "image_uri": "frame_8611.png"}, {"time": 4369.587, "image_uri": "frame_8612.png"}, {"time": 4370.095, "image_uri": "frame_8613.png"}, {"time": 4370.602, "image_uri": "frame_8614.png"}, {"time": 4371.11, "image_uri": "frame_8615.png"}, {"time": 4371.617, "image_uri": "frame_8616.png"}, {"time": 4372.124, "image_uri": "frame_8617.png"}, {"time": 4372.632, "image_uri": "frame_8618.png"}, {"time": 4373.139, "image_uri": "frame_8619.png"}, {"time": 4373.647, "image_uri": "frame_8620.png"}, {"time": 4374.154, "image_uri": "frame_8621.png"}, {"time": 4374.662, "image_uri": "frame_8622.png"}, {"time": 4375.169, "image_uri": "frame_8623.png"}, {"time": 4375.677, "image_uri": "frame_8624.png"}, {"time": 4376.184, "image_uri": "frame_8625.png"}, {"time": 4376.691, "image_uri": "frame_8626.png"}, {"time": 4377.199, "image_uri": "frame_8627.png"}, {"time": 4377.706, "image_uri": "frame_8628.png"}, {"time": 4378.214, "image_uri": "frame_8629.png"}, {"time": 4378.721, "image_uri": "frame_8630.png"}, {"time": 4379.229, "image_uri": "frame_8631.png"}, {"time": 4379.736, "image_uri": "frame_8632.png"}, {"time": 4380.244, "image_uri": "frame_8633.png"}, {"time": 4380.751, "image_uri": "frame_8634.png"}, {"time": 4381.258, "image_uri": "frame_8635.png"}, {"time": 4381.766, "image_uri": "frame_8636.png"}, {"time": 4382.273, "image_uri": "frame_8637.png"}, {"time": 4382.781, "image_uri": "frame_8638.png"}, {"time": 4383.288, "image_uri": "frame_8639.png"}, {"time": 4383.796, "image_uri": "frame_8640.png"}, {"time": 4384.303, "image_uri": "frame_8641.png"}, {"time": 4384.811, "image_uri": "frame_8642.png"}, {"time": 4385.318, "image_uri": "frame_8643.png"}, {"time": 4385.825, "image_uri": "frame_8644.png"}, {"time": 4386.333, "image_uri": "frame_8645.png"}, {"time": 4386.84, "image_uri": "frame_8646.png"}, {"time": 4387.348, "image_uri": "frame_8647.png"}, {"time": 4387.855, "image_uri": "frame_8648.png"}, {"time": 4388.363, "image_uri": "frame_8649.png"}, {"time": 4388.87, "image_uri": "frame_8650.png"}, {"time": 4389.378, "image_uri": "frame_8651.png"}, {"time": 4389.885, "image_uri": "frame_8652.png"}, {"time": 4390.392, "image_uri": "frame_8653.png"}, {"time": 4390.9, "image_uri": "frame_8654.png"}, {"time": 4391.407, "image_uri": "frame_8655.png"}, {"time": 4391.915, "image_uri": "frame_8656.png"}, {"time": 4392.422, "image_uri": "frame_8657.png"}, {"time": 4392.93, "image_uri": "frame_8658.png"}, {"time": 4393.437, "image_uri": "frame_8659.png"}, {"time": 4393.945, "image_uri": "frame_8660.png"}, {"time": 4394.452, "image_uri": "frame_8661.png"}, {"time": 4394.959, "image_uri": "frame_8662.png"}, {"time": 4395.467, "image_uri": "frame_8663.png"}, {"time": 4395.974, "image_uri": "frame_8664.png"}, {"time": 4396.482, "image_uri": "frame_8665.png"}, {"time": 4396.989, "image_uri": "frame_8666.png"}, {"time": 4397.497, "image_uri": "frame_8667.png"}, {"time": 4398.004, "image_uri": "frame_8668.png"}, {"time": 4398.512, "image_uri": "frame_8669.png"}, {"time": 4399.019, "image_uri": "frame_8670.png"}, {"time": 4399.526, "image_uri": "frame_8671.png"}, {"time": 4400.034, "image_uri": "frame_8672.png"}, {"time": 4400.541, "image_uri": "frame_8673.png"}, {"time": 4401.049, "image_uri": "frame_8674.png"}, {"time": 4401.556, "image_uri": "frame_8675.png"}, {"time": 4402.064, "image_uri": "frame_8676.png"}, {"time": 4402.571, "image_uri": "frame_8677.png"}, {"time": 4403.078, "image_uri": "frame_8678.png"}, {"time": 4403.586, "image_uri": "frame_8679.png"}, {"time": 4404.093, "image_uri": "frame_8680.png"}, {"time": 4404.601, "image_uri": "frame_8681.png"}, {"time": 4405.108, "image_uri": "frame_8682.png"}, {"time": 4405.616, "image_uri": "frame_8683.png"}, {"time": 4406.123, "image_uri": "frame_8684.png"}, {"time": 4406.631, "image_uri": "frame_8685.png"}, {"time": 4407.138, "image_uri": "frame_8686.png"}, {"time": 4407.645, "image_uri": "frame_8687.png"}, {"time": 4408.153, "image_uri": "frame_8688.png"}, {"time": 4408.66, "image_uri": "frame_8689.png"}, {"time": 4409.168, "image_uri": "frame_8690.png"}, {"time": 4409.675, "image_uri": "frame_8691.png"}, {"time": 4410.183, "image_uri": "frame_8692.png"}, {"time": 4410.69, "image_uri": "frame_8693.png"}, {"time": 4411.198, "image_uri": "frame_8694.png"}, {"time": 4411.705, "image_uri": "frame_8695.png"}, {"time": 4412.212, "image_uri": "frame_8696.png"}, {"time": 4412.72, "image_uri": "frame_8697.png"}, {"time": 4413.227, "image_uri": "frame_8698.png"}, {"time": 4413.735, "image_uri": "frame_8699.png"}, {"time": 4414.242, "image_uri": "frame_8700.png"}, {"time": 4414.75, "image_uri": "frame_8701.png"}, {"time": 4415.257, "image_uri": "frame_8702.png"}, {"time": 4415.765, "image_uri": "frame_8703.png"}, {"time": 4416.272, "image_uri": "frame_8704.png"}, {"time": 4416.779, "image_uri": "frame_8705.png"}, {"time": 4417.287, "image_uri": "frame_8706.png"}, {"time": 4417.794, "image_uri": "frame_8707.png"}, {"time": 4418.302, "image_uri": "frame_8708.png"}, {"time": 4418.809, "image_uri": "frame_8709.png"}, {"time": 4419.317, "image_uri": "frame_8710.png"}, {"time": 4419.824, "image_uri": "frame_8711.png"}, {"time": 4420.332, "image_uri": "frame_8712.png"}, {"time": 4420.839, "image_uri": "frame_8713.png"}, {"time": 4421.346, "image_uri": "frame_8714.png"}, {"time": 4421.854, "image_uri": "frame_8715.png"}, {"time": 4422.361, "image_uri": "frame_8716.png"}, {"time": 4422.869, "image_uri": "frame_8717.png"}, {"time": 4423.376, "image_uri": "frame_8718.png"}, {"time": 4423.884, "image_uri": "frame_8719.png"}, {"time": 4424.391, "image_uri": "frame_8720.png"}, {"time": 4424.899, "image_uri": "frame_8721.png"}, {"time": 4425.406, "image_uri": "frame_8722.png"}, {"time": 4425.913, "image_uri": "frame_8723.png"}, {"time": 4426.421, "image_uri": "frame_8724.png"}, {"time": 4426.928, "image_uri": "frame_8725.png"}, {"time": 4427.436, "image_uri": "frame_8726.png"}, {"time": 4427.943, "image_uri": "frame_8727.png"}, {"time": 4428.451, "image_uri": "frame_8728.png"}, {"time": 4428.958, "image_uri": "frame_8729.png"}, {"time": 4429.465, "image_uri": "frame_8730.png"}, {"time": 4429.973, "image_uri": "frame_8731.png"}, {"time": 4430.48, "image_uri": "frame_8732.png"}, {"time": 4430.988, "image_uri": "frame_8733.png"}, {"time": 4431.495, "image_uri": "frame_8734.png"}, {"time": 4432.003, "image_uri": "frame_8735.png"}, {"time": 4432.51, "image_uri": "frame_8736.png"}, {"time": 4433.018, "image_uri": "frame_8737.png"}, {"time": 4433.525, "image_uri": "frame_8738.png"}, {"time": 4434.032, "image_uri": "frame_8739.png"}, {"time": 4434.54, "image_uri": "frame_8740.png"}, {"time": 4435.047, "image_uri": "frame_8741.png"}, {"time": 4435.555, "image_uri": "frame_8742.png"}, {"time": 4436.062, "image_uri": "frame_8743.png"}, {"time": 4436.57, "image_uri": "frame_8744.png"}, {"time": 4437.077, "image_uri": "frame_8745.png"}, {"time": 4437.585, "image_uri": "frame_8746.png"}, {"time": 4438.092, "image_uri": "frame_8747.png"}, {"time": 4438.599, "image_uri": "frame_8748.png"}, {"time": 4439.107, "image_uri": "frame_8749.png"}, {"time": 4439.614, "image_uri": "frame_8750.png"}, {"time": 4440.122, "image_uri": "frame_8751.png"}, {"time": 4440.629, "image_uri": "frame_8752.png"}, {"time": 4441.137, "image_uri": "frame_8753.png"}, {"time": 4441.644, "image_uri": "frame_8754.png"}, {"time": 4442.152, "image_uri": "frame_8755.png"}, {"time": 4442.659, "image_uri": "frame_8756.png"}, {"time": 4443.166, "image_uri": "frame_8757.png"}, {"time": 4443.674, "image_uri": "frame_8758.png"}, {"time": 4444.181, "image_uri": "frame_8759.png"}, {"time": 4444.689, "image_uri": "frame_8760.png"}, {"time": 4445.196, "image_uri": "frame_8761.png"}, {"time": 4445.704, "image_uri": "frame_8762.png"}, {"time": 4446.211, "image_uri": "frame_8763.png"}, {"time": 4446.719, "image_uri": "frame_8764.png"}, {"time": 4447.226, "image_uri": "frame_8765.png"}, {"time": 4447.733, "image_uri": "frame_8766.png"}, {"time": 4448.241, "image_uri": "frame_8767.png"}, {"time": 4448.748, "image_uri": "frame_8768.png"}, {"time": 4449.256, "image_uri": "frame_8769.png"}, {"time": 4449.763, "image_uri": "frame_8770.png"}, {"time": 4450.271, "image_uri": "frame_8771.png"}, {"time": 4450.778, "image_uri": "frame_8772.png"}, {"time": 4451.286, "image_uri": "frame_8773.png"}, {"time": 4451.793, "image_uri": "frame_8774.png"}, {"time": 4452.3, "image_uri": "frame_8775.png"}, {"time": 4452.808, "image_uri": "frame_8776.png"}, {"time": 4453.315, "image_uri": "frame_8777.png"}, {"time": 4453.823, "image_uri": "frame_8778.png"}, {"time": 4454.33, "image_uri": "frame_8779.png"}, {"time": 4454.838, "image_uri": "frame_8780.png"}, {"time": 4455.345, "image_uri": "frame_8781.png"}, {"time": 4455.853, "image_uri": "frame_8782.png"}, {"time": 4456.36, "image_uri": "frame_8783.png"}, {"time": 4456.867, "image_uri": "frame_8784.png"}, {"time": 4457.375, "image_uri": "frame_8785.png"}, {"time": 4457.882, "image_uri": "frame_8786.png"}, {"time": 4458.39, "image_uri": "frame_8787.png"}, {"time": 4458.897, "image_uri": "frame_8788.png"}, {"time": 4459.405, "image_uri": "frame_8789.png"}, {"time": 4459.912, "image_uri": "frame_8790.png"}, {"time": 4460.419, "image_uri": "frame_8791.png"}, {"time": 4460.927, "image_uri": "frame_8792.png"}, {"time": 4461.434, "image_uri": "frame_8793.png"}, {"time": 4461.942, "image_uri": "frame_8794.png"}, {"time": 4462.449, "image_uri": "frame_8795.png"}, {"time": 4462.957, "image_uri": "frame_8796.png"}, {"time": 4463.464, "image_uri": "frame_8797.png"}, {"time": 4463.972, "image_uri": "frame_8798.png"}, {"time": 4464.479, "image_uri": "frame_8799.png"}, {"time": 4464.986, "image_uri": "frame_8800.png"}, {"time": 4465.494, "image_uri": "frame_8801.png"}, {"time": 4466.001, "image_uri": "frame_8802.png"}, {"time": 4466.509, "image_uri": "frame_8803.png"}, {"time": 4467.016, "image_uri": "frame_8804.png"}, {"time": 4467.524, "image_uri": "frame_8805.png"}, {"time": 4468.031, "image_uri": "frame_8806.png"}, {"time": 4468.539, "image_uri": "frame_8807.png"}, {"time": 4469.046, "image_uri": "frame_8808.png"}, {"time": 4469.553, "image_uri": "frame_8809.png"}, {"time": 4470.061, "image_uri": "frame_8810.png"}, {"time": 4470.568, "image_uri": "frame_8811.png"}, {"time": 4471.076, "image_uri": "frame_8812.png"}, {"time": 4471.583, "image_uri": "frame_8813.png"}, {"time": 4472.091, "image_uri": "frame_8814.png"}, {"time": 4472.598, "image_uri": "frame_8815.png"}, {"time": 4473.106, "image_uri": "frame_8816.png"}, {"time": 4473.613, "image_uri": "frame_8817.png"}, {"time": 4474.12, "image_uri": "frame_8818.png"}, {"time": 4474.628, "image_uri": "frame_8819.png"}, {"time": 4475.135, "image_uri": "frame_8820.png"}, {"time": 4475.643, "image_uri": "frame_8821.png"}, {"time": 4476.15, "image_uri": "frame_8822.png"}, {"time": 4476.658, "image_uri": "frame_8823.png"}, {"time": 4477.165, "image_uri": "frame_8824.png"}, {"time": 4477.673, "image_uri": "frame_8825.png"}, {"time": 4478.18, "image_uri": "frame_8826.png"}, {"time": 4478.687, "image_uri": "frame_8827.png"}, {"time": 4479.195, "image_uri": "frame_8828.png"}, {"time": 4479.702, "image_uri": "frame_8829.png"}, {"time": 4480.21, "image_uri": "frame_8830.png"}, {"time": 4480.717, "image_uri": "frame_8831.png"}, {"time": 4481.225, "image_uri": "frame_8832.png"}, {"time": 4481.732, "image_uri": "frame_8833.png"}, {"time": 4482.24, "image_uri": "frame_8834.png"}, {"time": 4482.747, "image_uri": "frame_8835.png"}, {"time": 4483.254, "image_uri": "frame_8836.png"}, {"time": 4483.762, "image_uri": "frame_8837.png"}, {"time": 4484.269, "image_uri": "frame_8838.png"}, {"time": 4484.777, "image_uri": "frame_8839.png"}, {"time": 4485.284, "image_uri": "frame_8840.png"}, {"time": 4485.792, "image_uri": "frame_8841.png"}, {"time": 4486.299, "image_uri": "frame_8842.png"}, {"time": 4486.806, "image_uri": "frame_8843.png"}, {"time": 4487.314, "image_uri": "frame_8844.png"}, {"time": 4487.821, "image_uri": "frame_8845.png"}, {"time": 4488.329, "image_uri": "frame_8846.png"}, {"time": 4488.836, "image_uri": "frame_8847.png"}, {"time": 4489.344, "image_uri": "frame_8848.png"}, {"time": 4489.851, "image_uri": "frame_8849.png"}, {"time": 4490.359, "image_uri": "frame_8850.png"}, {"time": 4490.866, "image_uri": "frame_8851.png"}, {"time": 4491.373, "image_uri": "frame_8852.png"}, {"time": 4491.881, "image_uri": "frame_8853.png"}, {"time": 4492.388, "image_uri": "frame_8854.png"}, {"time": 4492.896, "image_uri": "frame_8855.png"}, {"time": 4493.403, "image_uri": "frame_8856.png"}, {"time": 4493.911, "image_uri": "frame_8857.png"}, {"time": 4494.418, "image_uri": "frame_8858.png"}, {"time": 4494.926, "image_uri": "frame_8859.png"}, {"time": 4495.433, "image_uri": "frame_8860.png"}, {"time": 4495.94, "image_uri": "frame_8861.png"}, {"time": 4496.448, "image_uri": "frame_8862.png"}, {"time": 4496.955, "image_uri": "frame_8863.png"}, {"time": 4497.463, "image_uri": "frame_8864.png"}, {"time": 4497.97, "image_uri": "frame_8865.png"}, {"time": 4498.478, "image_uri": "frame_8866.png"}, {"time": 4498.985, "image_uri": "frame_8867.png"}, {"time": 4499.493, "image_uri": "frame_8868.png"}, {"time": 4500.0, "image_uri": "frame_8869.png"}, {"time": 4500.507, "image_uri": "frame_8870.png"}, {"time": 4501.015, "image_uri": "frame_8871.png"}, {"time": 4501.522, "image_uri": "frame_8872.png"}, {"time": 4502.03, "image_uri": "frame_8873.png"}, {"time": 4502.537, "image_uri": "frame_8874.png"}, {"time": 4503.045, "image_uri": "frame_8875.png"}, {"time": 4503.552, "image_uri": "frame_8876.png"}, {"time": 4504.06, "image_uri": "frame_8877.png"}, {"time": 4504.567, "image_uri": "frame_8878.png"}, {"time": 4505.074, "image_uri": "frame_8879.png"}, {"time": 4505.582, "image_uri": "frame_8880.png"}, {"time": 4506.089, "image_uri": "frame_8881.png"}, {"time": 4506.597, "image_uri": "frame_8882.png"}, {"time": 4507.104, "image_uri": "frame_8883.png"}, {"time": 4507.612, "image_uri": "frame_8884.png"}, {"time": 4508.119, "image_uri": "frame_8885.png"}, {"time": 4508.627, "image_uri": "frame_8886.png"}, {"time": 4509.134, "image_uri": "frame_8887.png"}, {"time": 4509.641, "image_uri": "frame_8888.png"}, {"time": 4510.149, "image_uri": "frame_8889.png"}, {"time": 4510.656, "image_uri": "frame_8890.png"}, {"time": 4511.164, "image_uri": "frame_8891.png"}, {"time": 4511.671, "image_uri": "frame_8892.png"}, {"time": 4512.179, "image_uri": "frame_8893.png"}, {"time": 4512.686, "image_uri": "frame_8894.png"}, {"time": 4513.194, "image_uri": "frame_8895.png"}, {"time": 4513.701, "image_uri": "frame_8896.png"}, {"time": 4514.208, "image_uri": "frame_8897.png"}, {"time": 4514.716, "image_uri": "frame_8898.png"}, {"time": 4515.223, "image_uri": "frame_8899.png"}, {"time": 4515.731, "image_uri": "frame_8900.png"}, {"time": 4516.238, "image_uri": "frame_8901.png"}, {"time": 4516.746, "image_uri": "frame_8902.png"}, {"time": 4517.253, "image_uri": "frame_8903.png"}, {"time": 4517.76, "image_uri": "frame_8904.png"}, {"time": 4518.268, "image_uri": "frame_8905.png"}, {"time": 4518.775, "image_uri": "frame_8906.png"}, {"time": 4519.283, "image_uri": "frame_8907.png"}, {"time": 4519.79, "image_uri": "frame_8908.png"}, {"time": 4520.298, "image_uri": "frame_8909.png"}, {"time": 4520.805, "image_uri": "frame_8910.png"}, {"time": 4521.313, "image_uri": "frame_8911.png"}, {"time": 4521.82, "image_uri": "frame_8912.png"}, {"time": 4522.327, "image_uri": "frame_8913.png"}, {"time": 4522.835, "image_uri": "frame_8914.png"}, {"time": 4523.342, "image_uri": "frame_8915.png"}, {"time": 4523.85, "image_uri": "frame_8916.png"}, {"time": 4524.357, "image_uri": "frame_8917.png"}, {"time": 4524.865, "image_uri": "frame_8918.png"}, {"time": 4525.372, "image_uri": "frame_8919.png"}, {"time": 4525.88, "image_uri": "frame_8920.png"}, {"time": 4526.387, "image_uri": "frame_8921.png"}, {"time": 4526.894, "image_uri": "frame_8922.png"}, {"time": 4527.402, "image_uri": "frame_8923.png"}, {"time": 4527.909, "image_uri": "frame_8924.png"}, {"time": 4528.417, "image_uri": "frame_8925.png"}, {"time": 4528.924, "image_uri": "frame_8926.png"}, {"time": 4529.432, "image_uri": "frame_8927.png"}, {"time": 4529.939, "image_uri": "frame_8928.png"}, {"time": 4530.447, "image_uri": "frame_8929.png"}, {"time": 4530.954, "image_uri": "frame_8930.png"}, {"time": 4531.461, "image_uri": "frame_8931.png"}, {"time": 4531.969, "image_uri": "frame_8932.png"}, {"time": 4532.476, "image_uri": "frame_8933.png"}, {"time": 4532.984, "image_uri": "frame_8934.png"}, {"time": 4533.491, "image_uri": "frame_8935.png"}, {"time": 4533.999, "image_uri": "frame_8936.png"}, {"time": 4534.506, "image_uri": "frame_8937.png"}, {"time": 4535.014, "image_uri": "frame_8938.png"}, {"time": 4535.521, "image_uri": "frame_8939.png"}, {"time": 4536.028, "image_uri": "frame_8940.png"}, {"time": 4536.536, "image_uri": "frame_8941.png"}, {"time": 4537.043, "image_uri": "frame_8942.png"}, {"time": 4537.551, "image_uri": "frame_8943.png"}, {"time": 4538.058, "image_uri": "frame_8944.png"}, {"time": 4538.566, "image_uri": "frame_8945.png"}, {"time": 4539.073, "image_uri": "frame_8946.png"}, {"time": 4539.581, "image_uri": "frame_8947.png"}, {"time": 4540.088, "image_uri": "frame_8948.png"}, {"time": 4540.595, "image_uri": "frame_8949.png"}, {"time": 4541.103, "image_uri": "frame_8950.png"}, {"time": 4541.61, "image_uri": "frame_8951.png"}, {"time": 4542.118, "image_uri": "frame_8952.png"}, {"time": 4542.625, "image_uri": "frame_8953.png"}, {"time": 4543.133, "image_uri": "frame_8954.png"}, {"time": 4543.64, "image_uri": "frame_8955.png"}, {"time": 4544.147, "image_uri": "frame_8956.png"}, {"time": 4544.655, "image_uri": "frame_8957.png"}, {"time": 4545.162, "image_uri": "frame_8958.png"}, {"time": 4545.67, "image_uri": "frame_8959.png"}, {"time": 4546.177, "image_uri": "frame_8960.png"}, {"time": 4546.685, "image_uri": "frame_8961.png"}, {"time": 4547.192, "image_uri": "frame_8962.png"}, {"time": 4547.7, "image_uri": "frame_8963.png"}, {"time": 4548.207, "image_uri": "frame_8964.png"}, {"time": 4548.714, "image_uri": "frame_8965.png"}, {"time": 4549.222, "image_uri": "frame_8966.png"}, {"time": 4549.729, "image_uri": "frame_8967.png"}, {"time": 4550.237, "image_uri": "frame_8968.png"}, {"time": 4550.744, "image_uri": "frame_8969.png"}, {"time": 4551.252, "image_uri": "frame_8970.png"}, {"time": 4551.759, "image_uri": "frame_8971.png"}, {"time": 4552.267, "image_uri": "frame_8972.png"}, {"time": 4552.774, "image_uri": "frame_8973.png"}, {"time": 4553.281, "image_uri": "frame_8974.png"}, {"time": 4553.789, "image_uri": "frame_8975.png"}, {"time": 4554.296, "image_uri": "frame_8976.png"}, {"time": 4554.804, "image_uri": "frame_8977.png"}, {"time": 4555.311, "image_uri": "frame_8978.png"}, {"time": 4555.819, "image_uri": "frame_8979.png"}, {"time": 4556.326, "image_uri": "frame_8980.png"}, {"time": 4556.834, "image_uri": "frame_8981.png"}, {"time": 4557.341, "image_uri": "frame_8982.png"}, {"time": 4557.848, "image_uri": "frame_8983.png"}, {"time": 4558.356, "image_uri": "frame_8984.png"}, {"time": 4558.863, "image_uri": "frame_8985.png"}, {"time": 4559.371, "image_uri": "frame_8986.png"}, {"time": 4559.878, "image_uri": "frame_8987.png"}, {"time": 4560.386, "image_uri": "frame_8988.png"}, {"time": 4560.893, "image_uri": "frame_8989.png"}, {"time": 4561.401, "image_uri": "frame_8990.png"}, {"time": 4561.908, "image_uri": "frame_8991.png"}, {"time": 4562.415, "image_uri": "frame_8992.png"}, {"time": 4562.923, "image_uri": "frame_8993.png"}, {"time": 4563.43, "image_uri": "frame_8994.png"}, {"time": 4563.938, "image_uri": "frame_8995.png"}, {"time": 4564.445, "image_uri": "frame_8996.png"}, {"time": 4564.953, "image_uri": "frame_8997.png"}, {"time": 4565.46, "image_uri": "frame_8998.png"}, {"time": 4565.968, "image_uri": "frame_8999.png"}, {"time": 4566.475, "image_uri": "frame_9000.png"}, {"time": 4566.982, "image_uri": "frame_9001.png"}, {"time": 4567.49, "image_uri": "frame_9002.png"}, {"time": 4567.997, "image_uri": "frame_9003.png"}, {"time": 4568.505, "image_uri": "frame_9004.png"}, {"time": 4569.012, "image_uri": "frame_9005.png"}, {"time": 4569.52, "image_uri": "frame_9006.png"}, {"time": 4570.027, "image_uri": "frame_9007.png"}, {"time": 4570.535, "image_uri": "frame_9008.png"}, {"time": 4571.042, "image_uri": "frame_9009.png"}, {"time": 4571.549, "image_uri": "frame_9010.png"}, {"time": 4572.057, "image_uri": "frame_9011.png"}, {"time": 4572.564, "image_uri": "frame_9012.png"}, {"time": 4573.072, "image_uri": "frame_9013.png"}, {"time": 4573.579, "image_uri": "frame_9014.png"}, {"time": 4574.087, "image_uri": "frame_9015.png"}, {"time": 4574.594, "image_uri": "frame_9016.png"}, {"time": 4575.101, "image_uri": "frame_9017.png"}, {"time": 4575.609, "image_uri": "frame_9018.png"}, {"time": 4576.116, "image_uri": "frame_9019.png"}, {"time": 4576.624, "image_uri": "frame_9020.png"}, {"time": 4577.131, "image_uri": "frame_9021.png"}, {"time": 4577.639, "image_uri": "frame_9022.png"}, {"time": 4578.146, "image_uri": "frame_9023.png"}, {"time": 4578.654, "image_uri": "frame_9024.png"}, {"time": 4579.161, "image_uri": "frame_9025.png"}, {"time": 4579.668, "image_uri": "frame_9026.png"}, {"time": 4580.176, "image_uri": "frame_9027.png"}, {"time": 4580.683, "image_uri": "frame_9028.png"}, {"time": 4581.191, "image_uri": "frame_9029.png"}, {"time": 4581.698, "image_uri": "frame_9030.png"}, {"time": 4582.206, "image_uri": "frame_9031.png"}, {"time": 4582.713, "image_uri": "frame_9032.png"}, {"time": 4583.221, "image_uri": "frame_9033.png"}, {"time": 4583.728, "image_uri": "frame_9034.png"}, {"time": 4584.235, "image_uri": "frame_9035.png"}, {"time": 4584.743, "image_uri": "frame_9036.png"}, {"time": 4585.25, "image_uri": "frame_9037.png"}, {"time": 4585.758, "image_uri": "frame_9038.png"}, {"time": 4586.265, "image_uri": "frame_9039.png"}, {"time": 4586.773, "image_uri": "frame_9040.png"}, {"time": 4587.28, "image_uri": "frame_9041.png"}, {"time": 4587.788, "image_uri": "frame_9042.png"}, {"time": 4588.295, "image_uri": "frame_9043.png"}, {"time": 4588.802, "image_uri": "frame_9044.png"}, {"time": 4589.31, "image_uri": "frame_9045.png"}, {"time": 4589.817, "image_uri": "frame_9046.png"}, {"time": 4590.325, "image_uri": "frame_9047.png"}, {"time": 4590.832, "image_uri": "frame_9048.png"}, {"time": 4591.34, "image_uri": "frame_9049.png"}, {"time": 4591.847, "image_uri": "frame_9050.png"}, {"time": 4592.355, "image_uri": "frame_9051.png"}, {"time": 4592.862, "image_uri": "frame_9052.png"}, {"time": 4593.369, "image_uri": "frame_9053.png"}, {"time": 4593.877, "image_uri": "frame_9054.png"}, {"time": 4594.384, "image_uri": "frame_9055.png"}, {"time": 4594.892, "image_uri": "frame_9056.png"}, {"time": 4595.399, "image_uri": "frame_9057.png"}, {"time": 4595.907, "image_uri": "frame_9058.png"}, {"time": 4596.414, "image_uri": "frame_9059.png"}, {"time": 4596.922, "image_uri": "frame_9060.png"}, {"time": 4597.429, "image_uri": "frame_9061.png"}, {"time": 4597.936, "image_uri": "frame_9062.png"}, {"time": 4598.444, "image_uri": "frame_9063.png"}, {"time": 4598.951, "image_uri": "frame_9064.png"}, {"time": 4599.459, "image_uri": "frame_9065.png"}, {"time": 4599.966, "image_uri": "frame_9066.png"}, {"time": 4600.474, "image_uri": "frame_9067.png"}, {"time": 4600.981, "image_uri": "frame_9068.png"}, {"time": 4601.488, "image_uri": "frame_9069.png"}, {"time": 4601.996, "image_uri": "frame_9070.png"}, {"time": 4602.503, "image_uri": "frame_9071.png"}, {"time": 4603.011, "image_uri": "frame_9072.png"}, {"time": 4603.518, "image_uri": "frame_9073.png"}, {"time": 4604.026, "image_uri": "frame_9074.png"}, {"time": 4604.533, "image_uri": "frame_9075.png"}, {"time": 4605.041, "image_uri": "frame_9076.png"}, {"time": 4605.548, "image_uri": "frame_9077.png"}, {"time": 4606.055, "image_uri": "frame_9078.png"}, {"time": 4606.563, "image_uri": "frame_9079.png"}, {"time": 4607.07, "image_uri": "frame_9080.png"}, {"time": 4607.578, "image_uri": "frame_9081.png"}, {"time": 4608.085, "image_uri": "frame_9082.png"}, {"time": 4608.593, "image_uri": "frame_9083.png"}, {"time": 4609.1, "image_uri": "frame_9084.png"}, {"time": 4609.608, "image_uri": "frame_9085.png"}, {"time": 4610.115, "image_uri": "frame_9086.png"}, {"time": 4610.622, "image_uri": "frame_9087.png"}, {"time": 4611.13, "image_uri": "frame_9088.png"}, {"time": 4611.637, "image_uri": "frame_9089.png"}, {"time": 4612.145, "image_uri": "frame_9090.png"}, {"time": 4612.652, "image_uri": "frame_9091.png"}, {"time": 4613.16, "image_uri": "frame_9092.png"}, {"time": 4613.667, "image_uri": "frame_9093.png"}, {"time": 4614.175, "image_uri": "frame_9094.png"}, {"time": 4614.682, "image_uri": "frame_9095.png"}, {"time": 4615.189, "image_uri": "frame_9096.png"}, {"time": 4615.697, "image_uri": "frame_9097.png"}, {"time": 4616.204, "image_uri": "frame_9098.png"}, {"time": 4616.712, "image_uri": "frame_9099.png"}, {"time": 4617.219, "image_uri": "frame_9100.png"}, {"time": 4617.727, "image_uri": "frame_9101.png"}, {"time": 4618.234, "image_uri": "frame_9102.png"}, {"time": 4618.742, "image_uri": "frame_9103.png"}, {"time": 4619.249, "image_uri": "frame_9104.png"}, {"time": 4619.756, "image_uri": "frame_9105.png"}, {"time": 4620.264, "image_uri": "frame_9106.png"}, {"time": 4620.771, "image_uri": "frame_9107.png"}, {"time": 4621.279, "image_uri": "frame_9108.png"}, {"time": 4621.786, "image_uri": "frame_9109.png"}, {"time": 4622.294, "image_uri": "frame_9110.png"}, {"time": 4622.801, "image_uri": "frame_9111.png"}, {"time": 4623.309, "image_uri": "frame_9112.png"}, {"time": 4623.816, "image_uri": "frame_9113.png"}, {"time": 4624.323, "image_uri": "frame_9114.png"}, {"time": 4624.831, "image_uri": "frame_9115.png"}, {"time": 4625.338, "image_uri": "frame_9116.png"}, {"time": 4625.846, "image_uri": "frame_9117.png"}, {"time": 4626.353, "image_uri": "frame_9118.png"}, {"time": 4626.861, "image_uri": "frame_9119.png"}, {"time": 4627.368, "image_uri": "frame_9120.png"}, {"time": 4627.876, "image_uri": "frame_9121.png"}, {"time": 4628.383, "image_uri": "frame_9122.png"}, {"time": 4628.89, "image_uri": "frame_9123.png"}, {"time": 4629.398, "image_uri": "frame_9124.png"}, {"time": 4629.905, "image_uri": "frame_9125.png"}, {"time": 4630.413, "image_uri": "frame_9126.png"}, {"time": 4630.92, "image_uri": "frame_9127.png"}, {"time": 4631.428, "image_uri": "frame_9128.png"}, {"time": 4631.935, "image_uri": "frame_9129.png"}, {"time": 4632.442, "image_uri": "frame_9130.png"}, {"time": 4632.95, "image_uri": "frame_9131.png"}, {"time": 4633.457, "image_uri": "frame_9132.png"}, {"time": 4633.965, "image_uri": "frame_9133.png"}, {"time": 4634.472, "image_uri": "frame_9134.png"}, {"time": 4634.98, "image_uri": "frame_9135.png"}, {"time": 4635.487, "image_uri": "frame_9136.png"}, {"time": 4635.995, "image_uri": "frame_9137.png"}, {"time": 4636.502, "image_uri": "frame_9138.png"}, {"time": 4637.009, "image_uri": "frame_9139.png"}, {"time": 4637.517, "image_uri": "frame_9140.png"}, {"time": 4638.024, "image_uri": "frame_9141.png"}, {"time": 4638.532, "image_uri": "frame_9142.png"}, {"time": 4639.039, "image_uri": "frame_9143.png"}, {"time": 4639.547, "image_uri": "frame_9144.png"}, {"time": 4640.054, "image_uri": "frame_9145.png"}, {"time": 4640.562, "image_uri": "frame_9146.png"}, {"time": 4641.069, "image_uri": "frame_9147.png"}, {"time": 4641.576, "image_uri": "frame_9148.png"}, {"time": 4642.084, "image_uri": "frame_9149.png"}, {"time": 4642.591, "image_uri": "frame_9150.png"}, {"time": 4643.099, "image_uri": "frame_9151.png"}, {"time": 4643.606, "image_uri": "frame_9152.png"}, {"time": 4644.114, "image_uri": "frame_9153.png"}, {"time": 4644.621, "image_uri": "frame_9154.png"}, {"time": 4645.129, "image_uri": "frame_9155.png"}, {"time": 4645.636, "image_uri": "frame_9156.png"}, {"time": 4646.143, "image_uri": "frame_9157.png"}, {"time": 4646.651, "image_uri": "frame_9158.png"}, {"time": 4647.158, "image_uri": "frame_9159.png"}, {"time": 4647.666, "image_uri": "frame_9160.png"}, {"time": 4648.173, "image_uri": "frame_9161.png"}, {"time": 4648.681, "image_uri": "frame_9162.png"}, {"time": 4649.188, "image_uri": "frame_9163.png"}, {"time": 4649.696, "image_uri": "frame_9164.png"}, {"time": 4650.203, "image_uri": "frame_9165.png"}, {"time": 4650.71, "image_uri": "frame_9166.png"}, {"time": 4651.218, "image_uri": "frame_9167.png"}, {"time": 4651.725, "image_uri": "frame_9168.png"}, {"time": 4652.233, "image_uri": "frame_9169.png"}, {"time": 4652.74, "image_uri": "frame_9170.png"}, {"time": 4653.248, "image_uri": "frame_9171.png"}, {"time": 4653.755, "image_uri": "frame_9172.png"}, {"time": 4654.263, "image_uri": "frame_9173.png"}, {"time": 4654.77, "image_uri": "frame_9174.png"}, {"time": 4655.277, "image_uri": "frame_9175.png"}, {"time": 4655.785, "image_uri": "frame_9176.png"}, {"time": 4656.292, "image_uri": "frame_9177.png"}, {"time": 4656.8, "image_uri": "frame_9178.png"}, {"time": 4657.307, "image_uri": "frame_9179.png"}, {"time": 4657.815, "image_uri": "frame_9180.png"}, {"time": 4658.322, "image_uri": "frame_9181.png"}, {"time": 4658.829, "image_uri": "frame_9182.png"}, {"time": 4659.337, "image_uri": "frame_9183.png"}, {"time": 4659.844, "image_uri": "frame_9184.png"}, {"time": 4660.352, "image_uri": "frame_9185.png"}, {"time": 4660.859, "image_uri": "frame_9186.png"}, {"time": 4661.367, "image_uri": "frame_9187.png"}, {"time": 4661.874, "image_uri": "frame_9188.png"}, {"time": 4662.382, "image_uri": "frame_9189.png"}, {"time": 4662.889, "image_uri": "frame_9190.png"}, {"time": 4663.396, "image_uri": "frame_9191.png"}, {"time": 4663.904, "image_uri": "frame_9192.png"}, {"time": 4664.411, "image_uri": "frame_9193.png"}, {"time": 4664.919, "image_uri": "frame_9194.png"}, {"time": 4665.426, "image_uri": "frame_9195.png"}, {"time": 4665.934, "image_uri": "frame_9196.png"}, {"time": 4666.441, "image_uri": "frame_9197.png"}, {"time": 4666.949, "image_uri": "frame_9198.png"}, {"time": 4667.456, "image_uri": "frame_9199.png"}, {"time": 4667.963, "image_uri": "frame_9200.png"}, {"time": 4668.471, "image_uri": "frame_9201.png"}, {"time": 4668.978, "image_uri": "frame_9202.png"}, {"time": 4669.486, "image_uri": "frame_9203.png"}, {"time": 4669.993, "image_uri": "frame_9204.png"}, {"time": 4670.501, "image_uri": "frame_9205.png"}, {"time": 4671.008, "image_uri": "frame_9206.png"}, {"time": 4671.516, "image_uri": "frame_9207.png"}, {"time": 4672.023, "image_uri": "frame_9208.png"}, {"time": 4672.53, "image_uri": "frame_9209.png"}, {"time": 4673.038, "image_uri": "frame_9210.png"}, {"time": 4673.545, "image_uri": "frame_9211.png"}, {"time": 4674.053, "image_uri": "frame_9212.png"}, {"time": 4674.56, "image_uri": "frame_9213.png"}, {"time": 4675.068, "image_uri": "frame_9214.png"}, {"time": 4675.575, "image_uri": "frame_9215.png"}, {"time": 4676.083, "image_uri": "frame_9216.png"}, {"time": 4676.59, "image_uri": "frame_9217.png"}, {"time": 4677.097, "image_uri": "frame_9218.png"}, {"time": 4677.605, "image_uri": "frame_9219.png"}, {"time": 4678.112, "image_uri": "frame_9220.png"}, {"time": 4678.62, "image_uri": "frame_9221.png"}, {"time": 4679.127, "image_uri": "frame_9222.png"}, {"time": 4679.635, "image_uri": "frame_9223.png"}, {"time": 4680.142, "image_uri": "frame_9224.png"}, {"time": 4680.65, "image_uri": "frame_9225.png"}, {"time": 4681.157, "image_uri": "frame_9226.png"}, {"time": 4681.664, "image_uri": "frame_9227.png"}, {"time": 4682.172, "image_uri": "frame_9228.png"}, {"time": 4682.679, "image_uri": "frame_9229.png"}, {"time": 4683.187, "image_uri": "frame_9230.png"}, {"time": 4683.694, "image_uri": "frame_9231.png"}, {"time": 4684.202, "image_uri": "frame_9232.png"}, {"time": 4684.709, "image_uri": "frame_9233.png"}, {"time": 4685.217, "image_uri": "frame_9234.png"}, {"time": 4685.724, "image_uri": "frame_9235.png"}, {"time": 4686.231, "image_uri": "frame_9236.png"}, {"time": 4686.739, "image_uri": "frame_9237.png"}, {"time": 4687.246, "image_uri": "frame_9238.png"}, {"time": 4687.754, "image_uri": "frame_9239.png"}, {"time": 4688.261, "image_uri": "frame_9240.png"}, {"time": 4688.769, "image_uri": "frame_9241.png"}, {"time": 4689.276, "image_uri": "frame_9242.png"}, {"time": 4689.783, "image_uri": "frame_9243.png"}, {"time": 4690.291, "image_uri": "frame_9244.png"}, {"time": 4690.798, "image_uri": "frame_9245.png"}, {"time": 4691.306, "image_uri": "frame_9246.png"}, {"time": 4691.813, "image_uri": "frame_9247.png"}, {"time": 4692.321, "image_uri": "frame_9248.png"}, {"time": 4692.828, "image_uri": "frame_9249.png"}, {"time": 4693.336, "image_uri": "frame_9250.png"}, {"time": 4693.843, "image_uri": "frame_9251.png"}, {"time": 4694.35, "image_uri": "frame_9252.png"}, {"time": 4694.858, "image_uri": "frame_9253.png"}, {"time": 4695.365, "image_uri": "frame_9254.png"}, {"time": 4695.873, "image_uri": "frame_9255.png"}, {"time": 4696.38, "image_uri": "frame_9256.png"}, {"time": 4696.888, "image_uri": "frame_9257.png"}, {"time": 4697.395, "image_uri": "frame_9258.png"}, {"time": 4697.903, "image_uri": "frame_9259.png"}, {"time": 4698.41, "image_uri": "frame_9260.png"}, {"time": 4698.917, "image_uri": "frame_9261.png"}, {"time": 4699.425, "image_uri": "frame_9262.png"}, {"time": 4699.932, "image_uri": "frame_9263.png"}, {"time": 4700.44, "image_uri": "frame_9264.png"}, {"time": 4700.947, "image_uri": "frame_9265.png"}, {"time": 4701.455, "image_uri": "frame_9266.png"}, {"time": 4701.962, "image_uri": "frame_9267.png"}, {"time": 4702.47, "image_uri": "frame_9268.png"}, {"time": 4702.977, "image_uri": "frame_9269.png"}, {"time": 4703.484, "image_uri": "frame_9270.png"}, {"time": 4703.992, "image_uri": "frame_9271.png"}, {"time": 4704.499, "image_uri": "frame_9272.png"}, {"time": 4705.007, "image_uri": "frame_9273.png"}, {"time": 4705.514, "image_uri": "frame_9274.png"}, {"time": 4706.022, "image_uri": "frame_9275.png"}, {"time": 4706.529, "image_uri": "frame_9276.png"}, {"time": 4707.037, "image_uri": "frame_9277.png"}, {"time": 4707.544, "image_uri": "frame_9278.png"}, {"time": 4708.051, "image_uri": "frame_9279.png"}, {"time": 4708.559, "image_uri": "frame_9280.png"}, {"time": 4709.066, "image_uri": "frame_9281.png"}, {"time": 4709.574, "image_uri": "frame_9282.png"}, {"time": 4710.081, "image_uri": "frame_9283.png"}, {"time": 4710.589, "image_uri": "frame_9284.png"}, {"time": 4711.096, "image_uri": "frame_9285.png"}, {"time": 4711.604, "image_uri": "frame_9286.png"}, {"time": 4712.111, "image_uri": "frame_9287.png"}, {"time": 4712.618, "image_uri": "frame_9288.png"}, {"time": 4713.126, "image_uri": "frame_9289.png"}, {"time": 4713.633, "image_uri": "frame_9290.png"}, {"time": 4714.141, "image_uri": "frame_9291.png"}, {"time": 4714.648, "image_uri": "frame_9292.png"}, {"time": 4715.156, "image_uri": "frame_9293.png"}, {"time": 4715.663, "image_uri": "frame_9294.png"}, {"time": 4716.171, "image_uri": "frame_9295.png"}, {"time": 4716.678, "image_uri": "frame_9296.png"}, {"time": 4717.185, "image_uri": "frame_9297.png"}, {"time": 4717.693, "image_uri": "frame_9298.png"}, {"time": 4718.2, "image_uri": "frame_9299.png"}, {"time": 4718.708, "image_uri": "frame_9300.png"}, {"time": 4719.215, "image_uri": "frame_9301.png"}, {"time": 4719.723, "image_uri": "frame_9302.png"}, {"time": 4720.23, "image_uri": "frame_9303.png"}, {"time": 4720.737, "image_uri": "frame_9304.png"}, {"time": 4721.245, "image_uri": "frame_9305.png"}, {"time": 4721.752, "image_uri": "frame_9306.png"}, {"time": 4722.26, "image_uri": "frame_9307.png"}, {"time": 4722.767, "image_uri": "frame_9308.png"}, {"time": 4723.275, "image_uri": "frame_9309.png"}, {"time": 4723.782, "image_uri": "frame_9310.png"}, {"time": 4724.29, "image_uri": "frame_9311.png"}, {"time": 4724.797, "image_uri": "frame_9312.png"}, {"time": 4725.304, "image_uri": "frame_9313.png"}, {"time": 4725.812, "image_uri": "frame_9314.png"}, {"time": 4726.319, "image_uri": "frame_9315.png"}, {"time": 4726.827, "image_uri": "frame_9316.png"}, {"time": 4727.334, "image_uri": "frame_9317.png"}, {"time": 4727.842, "image_uri": "frame_9318.png"}, {"time": 4728.349, "image_uri": "frame_9319.png"}, {"time": 4728.857, "image_uri": "frame_9320.png"}, {"time": 4729.364, "image_uri": "frame_9321.png"}, {"time": 4729.871, "image_uri": "frame_9322.png"}, {"time": 4730.379, "image_uri": "frame_9323.png"}, {"time": 4730.886, "image_uri": "frame_9324.png"}, {"time": 4731.394, "image_uri": "frame_9325.png"}, {"time": 4731.901, "image_uri": "frame_9326.png"}, {"time": 4732.409, "image_uri": "frame_9327.png"}, {"time": 4732.916, "image_uri": "frame_9328.png"}, {"time": 4733.424, "image_uri": "frame_9329.png"}, {"time": 4733.931, "image_uri": "frame_9330.png"}, {"time": 4734.438, "image_uri": "frame_9331.png"}, {"time": 4734.946, "image_uri": "frame_9332.png"}, {"time": 4735.453, "image_uri": "frame_9333.png"}, {"time": 4735.961, "image_uri": "frame_9334.png"}, {"time": 4736.468, "image_uri": "frame_9335.png"}, {"time": 4736.976, "image_uri": "frame_9336.png"}, {"time": 4737.483, "image_uri": "frame_9337.png"}, {"time": 4737.991, "image_uri": "frame_9338.png"}, {"time": 4738.498, "image_uri": "frame_9339.png"}, {"time": 4739.005, "image_uri": "frame_9340.png"}, {"time": 4739.513, "image_uri": "frame_9341.png"}, {"time": 4740.02, "image_uri": "frame_9342.png"}, {"time": 4740.528, "image_uri": "frame_9343.png"}, {"time": 4741.035, "image_uri": "frame_9344.png"}, {"time": 4741.543, "image_uri": "frame_9345.png"}, {"time": 4742.05, "image_uri": "frame_9346.png"}, {"time": 4742.558, "image_uri": "frame_9347.png"}, {"time": 4743.065, "image_uri": "frame_9348.png"}, {"time": 4743.572, "image_uri": "frame_9349.png"}, {"time": 4744.08, "image_uri": "frame_9350.png"}, {"time": 4744.587, "image_uri": "frame_9351.png"}, {"time": 4745.095, "image_uri": "frame_9352.png"}, {"time": 4745.602, "image_uri": "frame_9353.png"}, {"time": 4746.11, "image_uri": "frame_9354.png"}, {"time": 4746.617, "image_uri": "frame_9355.png"}, {"time": 4747.124, "image_uri": "frame_9356.png"}, {"time": 4747.632, "image_uri": "frame_9357.png"}, {"time": 4748.139, "image_uri": "frame_9358.png"}, {"time": 4748.647, "image_uri": "frame_9359.png"}, {"time": 4749.154, "image_uri": "frame_9360.png"}, {"time": 4749.662, "image_uri": "frame_9361.png"}, {"time": 4750.169, "image_uri": "frame_9362.png"}, {"time": 4750.677, "image_uri": "frame_9363.png"}, {"time": 4751.184, "image_uri": "frame_9364.png"}, {"time": 4751.691, "image_uri": "frame_9365.png"}, {"time": 4752.199, "image_uri": "frame_9366.png"}, {"time": 4752.706, "image_uri": "frame_9367.png"}, {"time": 4753.214, "image_uri": "frame_9368.png"}, {"time": 4753.721, "image_uri": "frame_9369.png"}, {"time": 4754.229, "image_uri": "frame_9370.png"}, {"time": 4754.736, "image_uri": "frame_9371.png"}, {"time": 4755.244, "image_uri": "frame_9372.png"}, {"time": 4755.751, "image_uri": "frame_9373.png"}, {"time": 4756.258, "image_uri": "frame_9374.png"}, {"time": 4756.766, "image_uri": "frame_9375.png"}, {"time": 4757.273, "image_uri": "frame_9376.png"}, {"time": 4757.781, "image_uri": "frame_9377.png"}, {"time": 4758.288, "image_uri": "frame_9378.png"}, {"time": 4758.796, "image_uri": "frame_9379.png"}, {"time": 4759.303, "image_uri": "frame_9380.png"}, {"time": 4759.811, "image_uri": "frame_9381.png"}, {"time": 4760.318, "image_uri": "frame_9382.png"}, {"time": 4760.825, "image_uri": "frame_9383.png"}, {"time": 4761.333, "image_uri": "frame_9384.png"}, {"time": 4761.84, "image_uri": "frame_9385.png"}, {"time": 4762.348, "image_uri": "frame_9386.png"}, {"time": 4762.855, "image_uri": "frame_9387.png"}, {"time": 4763.363, "image_uri": "frame_9388.png"}, {"time": 4763.87, "image_uri": "frame_9389.png"}, {"time": 4764.378, "image_uri": "frame_9390.png"}, {"time": 4764.885, "image_uri": "frame_9391.png"}, {"time": 4765.392, "image_uri": "frame_9392.png"}, {"time": 4765.9, "image_uri": "frame_9393.png"}, {"time": 4766.407, "image_uri": "frame_9394.png"}, {"time": 4766.915, "image_uri": "frame_9395.png"}, {"time": 4767.422, "image_uri": "frame_9396.png"}, {"time": 4767.93, "image_uri": "frame_9397.png"}, {"time": 4768.437, "image_uri": "frame_9398.png"}, {"time": 4768.945, "image_uri": "frame_9399.png"}, {"time": 4769.452, "image_uri": "frame_9400.png"}, {"time": 4769.959, "image_uri": "frame_9401.png"}, {"time": 4770.467, "image_uri": "frame_9402.png"}, {"time": 4770.974, "image_uri": "frame_9403.png"}, {"time": 4771.482, "image_uri": "frame_9404.png"}, {"time": 4771.989, "image_uri": "frame_9405.png"}, {"time": 4772.497, "image_uri": "frame_9406.png"}, {"time": 4773.004, "image_uri": "frame_9407.png"}, {"time": 4773.512, "image_uri": "frame_9408.png"}, {"time": 4774.019, "image_uri": "frame_9409.png"}, {"time": 4774.526, "image_uri": "frame_9410.png"}, {"time": 4775.034, "image_uri": "frame_9411.png"}, {"time": 4775.541, "image_uri": "frame_9412.png"}, {"time": 4776.049, "image_uri": "frame_9413.png"}, {"time": 4776.556, "image_uri": "frame_9414.png"}, {"time": 4777.064, "image_uri": "frame_9415.png"}, {"time": 4777.571, "image_uri": "frame_9416.png"}, {"time": 4778.078, "image_uri": "frame_9417.png"}, {"time": 4778.586, "image_uri": "frame_9418.png"}, {"time": 4779.093, "image_uri": "frame_9419.png"}, {"time": 4779.601, "image_uri": "frame_9420.png"}, {"time": 4780.108, "image_uri": "frame_9421.png"}, {"time": 4780.616, "image_uri": "frame_9422.png"}, {"time": 4781.123, "image_uri": "frame_9423.png"}, {"time": 4781.631, "image_uri": "frame_9424.png"}, {"time": 4782.138, "image_uri": "frame_9425.png"}, {"time": 4782.645, "image_uri": "frame_9426.png"}, {"time": 4783.153, "image_uri": "frame_9427.png"}, {"time": 4783.66, "image_uri": "frame_9428.png"}, {"time": 4784.168, "image_uri": "frame_9429.png"}, {"time": 4784.675, "image_uri": "frame_9430.png"}, {"time": 4785.183, "image_uri": "frame_9431.png"}, {"time": 4785.69, "image_uri": "frame_9432.png"}, {"time": 4786.198, "image_uri": "frame_9433.png"}, {"time": 4786.705, "image_uri": "frame_9434.png"}, {"time": 4787.212, "image_uri": "frame_9435.png"}, {"time": 4787.72, "image_uri": "frame_9436.png"}, {"time": 4788.227, "image_uri": "frame_9437.png"}, {"time": 4788.735, "image_uri": "frame_9438.png"}, {"time": 4789.242, "image_uri": "frame_9439.png"}, {"time": 4789.75, "image_uri": "frame_9440.png"}, {"time": 4790.257, "image_uri": "frame_9441.png"}, {"time": 4790.765, "image_uri": "frame_9442.png"}, {"time": 4791.272, "image_uri": "frame_9443.png"}, {"time": 4791.779, "image_uri": "frame_9444.png"}, {"time": 4792.287, "image_uri": "frame_9445.png"}, {"time": 4792.794, "image_uri": "frame_9446.png"}, {"time": 4793.302, "image_uri": "frame_9447.png"}, {"time": 4793.809, "image_uri": "frame_9448.png"}, {"time": 4794.317, "image_uri": "frame_9449.png"}, {"time": 4794.824, "image_uri": "frame_9450.png"}, {"time": 4795.332, "image_uri": "frame_9451.png"}, {"time": 4795.839, "image_uri": "frame_9452.png"}, {"time": 4796.346, "image_uri": "frame_9453.png"}, {"time": 4796.854, "image_uri": "frame_9454.png"}, {"time": 4797.361, "image_uri": "frame_9455.png"}, {"time": 4797.869, "image_uri": "frame_9456.png"}, {"time": 4798.376, "image_uri": "frame_9457.png"}, {"time": 4798.884, "image_uri": "frame_9458.png"}, {"time": 4799.391, "image_uri": "frame_9459.png"}, {"time": 4799.899, "image_uri": "frame_9460.png"}, {"time": 4800.406, "image_uri": "frame_9461.png"}, {"time": 4800.913, "image_uri": "frame_9462.png"}, {"time": 4801.421, "image_uri": "frame_9463.png"}, {"time": 4801.928, "image_uri": "frame_9464.png"}, {"time": 4802.436, "image_uri": "frame_9465.png"}, {"time": 4802.943, "image_uri": "frame_9466.png"}, {"time": 4803.451, "image_uri": "frame_9467.png"}, {"time": 4803.958, "image_uri": "frame_9468.png"}, {"time": 4804.465, "image_uri": "frame_9469.png"}, {"time": 4804.973, "image_uri": "frame_9470.png"}, {"time": 4805.48, "image_uri": "frame_9471.png"}, {"time": 4805.988, "image_uri": "frame_9472.png"}, {"time": 4806.495, "image_uri": "frame_9473.png"}, {"time": 4807.003, "image_uri": "frame_9474.png"}, {"time": 4807.51, "image_uri": "frame_9475.png"}, {"time": 4808.018, "image_uri": "frame_9476.png"}, {"time": 4808.525, "image_uri": "frame_9477.png"}, {"time": 4809.032, "image_uri": "frame_9478.png"}, {"time": 4809.54, "image_uri": "frame_9479.png"}, {"time": 4810.047, "image_uri": "frame_9480.png"}, {"time": 4810.555, "image_uri": "frame_9481.png"}, {"time": 4811.062, "image_uri": "frame_9482.png"}, {"time": 4811.57, "image_uri": "frame_9483.png"}, {"time": 4812.077, "image_uri": "frame_9484.png"}, {"time": 4812.585, "image_uri": "frame_9485.png"}, {"time": 4813.092, "image_uri": "frame_9486.png"}, {"time": 4813.599, "image_uri": "frame_9487.png"}, {"time": 4814.107, "image_uri": "frame_9488.png"}, {"time": 4814.614, "image_uri": "frame_9489.png"}, {"time": 4815.122, "image_uri": "frame_9490.png"}, {"time": 4815.629, "image_uri": "frame_9491.png"}, {"time": 4816.137, "image_uri": "frame_9492.png"}, {"time": 4816.644, "image_uri": "frame_9493.png"}, {"time": 4817.152, "image_uri": "frame_9494.png"}, {"time": 4817.659, "image_uri": "frame_9495.png"}, {"time": 4818.166, "image_uri": "frame_9496.png"}, {"time": 4818.674, "image_uri": "frame_9497.png"}, {"time": 4819.181, "image_uri": "frame_9498.png"}, {"time": 4819.689, "image_uri": "frame_9499.png"}, {"time": 4820.196, "image_uri": "frame_9500.png"}, {"time": 4820.704, "image_uri": "frame_9501.png"}, {"time": 4821.211, "image_uri": "frame_9502.png"}, {"time": 4821.719, "image_uri": "frame_9503.png"}, {"time": 4822.226, "image_uri": "frame_9504.png"}, {"time": 4822.733, "image_uri": "frame_9505.png"}, {"time": 4823.241, "image_uri": "frame_9506.png"}, {"time": 4823.748, "image_uri": "frame_9507.png"}, {"time": 4824.256, "image_uri": "frame_9508.png"}, {"time": 4824.763, "image_uri": "frame_9509.png"}, {"time": 4825.271, "image_uri": "frame_9510.png"}, {"time": 4825.778, "image_uri": "frame_9511.png"}, {"time": 4826.286, "image_uri": "frame_9512.png"}, {"time": 4826.793, "image_uri": "frame_9513.png"}, {"time": 4827.3, "image_uri": "frame_9514.png"}, {"time": 4827.808, "image_uri": "frame_9515.png"}, {"time": 4828.315, "image_uri": "frame_9516.png"}, {"time": 4828.823, "image_uri": "frame_9517.png"}, {"time": 4829.33, "image_uri": "frame_9518.png"}, {"time": 4829.838, "image_uri": "frame_9519.png"}, {"time": 4830.345, "image_uri": "frame_9520.png"}, {"time": 4830.853, "image_uri": "frame_9521.png"}, {"time": 4831.36, "image_uri": "frame_9522.png"}, {"time": 4831.867, "image_uri": "frame_9523.png"}, {"time": 4832.375, "image_uri": "frame_9524.png"}, {"time": 4832.882, "image_uri": "frame_9525.png"}, {"time": 4833.39, "image_uri": "frame_9526.png"}, {"time": 4833.897, "image_uri": "frame_9527.png"}, {"time": 4834.405, "image_uri": "frame_9528.png"}, {"time": 4834.912, "image_uri": "frame_9529.png"}, {"time": 4835.419, "image_uri": "frame_9530.png"}, {"time": 4835.927, "image_uri": "frame_9531.png"}, {"time": 4836.434, "image_uri": "frame_9532.png"}, {"time": 4836.942, "image_uri": "frame_9533.png"}, {"time": 4837.449, "image_uri": "frame_9534.png"}, {"time": 4837.957, "image_uri": "frame_9535.png"}, {"time": 4838.464, "image_uri": "frame_9536.png"}, {"time": 4838.972, "image_uri": "frame_9537.png"}, {"time": 4839.479, "image_uri": "frame_9538.png"}, {"time": 4839.986, "image_uri": "frame_9539.png"}, {"time": 4840.494, "image_uri": "frame_9540.png"}, {"time": 4841.001, "image_uri": "frame_9541.png"}, {"time": 4841.509, "image_uri": "frame_9542.png"}, {"time": 4842.016, "image_uri": "frame_9543.png"}, {"time": 4842.524, "image_uri": "frame_9544.png"}, {"time": 4843.031, "image_uri": "frame_9545.png"}, {"time": 4843.539, "image_uri": "frame_9546.png"}, {"time": 4844.046, "image_uri": "frame_9547.png"}, {"time": 4844.553, "image_uri": "frame_9548.png"}, {"time": 4845.061, "image_uri": "frame_9549.png"}, {"time": 4845.568, "image_uri": "frame_9550.png"}, {"time": 4846.076, "image_uri": "frame_9551.png"}, {"time": 4846.583, "image_uri": "frame_9552.png"}, {"time": 4847.091, "image_uri": "frame_9553.png"}, {"time": 4847.598, "image_uri": "frame_9554.png"}, {"time": 4848.106, "image_uri": "frame_9555.png"}, {"time": 4848.613, "image_uri": "frame_9556.png"}, {"time": 4849.12, "image_uri": "frame_9557.png"}, {"time": 4849.628, "image_uri": "frame_9558.png"}, {"time": 4850.135, "image_uri": "frame_9559.png"}, {"time": 4850.643, "image_uri": "frame_9560.png"}, {"time": 4851.15, "image_uri": "frame_9561.png"}, {"time": 4851.658, "image_uri": "frame_9562.png"}, {"time": 4852.165, "image_uri": "frame_9563.png"}, {"time": 4852.673, "image_uri": "frame_9564.png"}, {"time": 4853.18, "image_uri": "frame_9565.png"}, {"time": 4853.687, "image_uri": "frame_9566.png"}, {"time": 4854.195, "image_uri": "frame_9567.png"}, {"time": 4854.702, "image_uri": "frame_9568.png"}, {"time": 4855.21, "image_uri": "frame_9569.png"}, {"time": 4855.717, "image_uri": "frame_9570.png"}, {"time": 4856.225, "image_uri": "frame_9571.png"}, {"time": 4856.732, "image_uri": "frame_9572.png"}, {"time": 4857.24, "image_uri": "frame_9573.png"}, {"time": 4857.747, "image_uri": "frame_9574.png"}, {"time": 4858.254, "image_uri": "frame_9575.png"}, {"time": 4858.762, "image_uri": "frame_9576.png"}, {"time": 4859.269, "image_uri": "frame_9577.png"}, {"time": 4859.777, "image_uri": "frame_9578.png"}, {"time": 4860.284, "image_uri": "frame_9579.png"}, {"time": 4860.792, "image_uri": "frame_9580.png"}, {"time": 4861.299, "image_uri": "frame_9581.png"}, {"time": 4861.806, "image_uri": "frame_9582.png"}, {"time": 4862.314, "image_uri": "frame_9583.png"}, {"time": 4862.821, "image_uri": "frame_9584.png"}, {"time": 4863.329, "image_uri": "frame_9585.png"}, {"time": 4863.836, "image_uri": "frame_9586.png"}, {"time": 4864.344, "image_uri": "frame_9587.png"}, {"time": 4864.851, "image_uri": "frame_9588.png"}, {"time": 4865.359, "image_uri": "frame_9589.png"}, {"time": 4865.866, "image_uri": "frame_9590.png"}, {"time": 4866.373, "image_uri": "frame_9591.png"}, {"time": 4866.881, "image_uri": "frame_9592.png"}, {"time": 4867.388, "image_uri": "frame_9593.png"}, {"time": 4867.896, "image_uri": "frame_9594.png"}, {"time": 4868.403, "image_uri": "frame_9595.png"}, {"time": 4868.911, "image_uri": "frame_9596.png"}, {"time": 4869.418, "image_uri": "frame_9597.png"}, {"time": 4869.926, "image_uri": "frame_9598.png"}, {"time": 4870.433, "image_uri": "frame_9599.png"}, {"time": 4870.94, "image_uri": "frame_9600.png"}, {"time": 4871.448, "image_uri": "frame_9601.png"}, {"time": 4871.955, "image_uri": "frame_9602.png"}, {"time": 4872.463, "image_uri": "frame_9603.png"}, {"time": 4872.97, "image_uri": "frame_9604.png"}, {"time": 4873.478, "image_uri": "frame_9605.png"}, {"time": 4873.985, "image_uri": "frame_9606.png"}, {"time": 4874.493, "image_uri": "frame_9607.png"}, {"time": 4875.0, "image_uri": "frame_9608.png"}, {"time": 4875.507, "image_uri": "frame_9609.png"}, {"time": 4876.015, "image_uri": "frame_9610.png"}, {"time": 4876.522, "image_uri": "frame_9611.png"}, {"time": 4877.03, "image_uri": "frame_9612.png"}, {"time": 4877.537, "image_uri": "frame_9613.png"}, {"time": 4878.045, "image_uri": "frame_9614.png"}, {"time": 4878.552, "image_uri": "frame_9615.png"}, {"time": 4879.06, "image_uri": "frame_9616.png"}, {"time": 4879.567, "image_uri": "frame_9617.png"}, {"time": 4880.074, "image_uri": "frame_9618.png"}, {"time": 4880.582, "image_uri": "frame_9619.png"}, {"time": 4881.089, "image_uri": "frame_9620.png"}, {"time": 4881.597, "image_uri": "frame_9621.png"}, {"time": 4882.104, "image_uri": "frame_9622.png"}, {"time": 4882.612, "image_uri": "frame_9623.png"}, {"time": 4883.119, "image_uri": "frame_9624.png"}, {"time": 4883.627, "image_uri": "frame_9625.png"}, {"time": 4884.134, "image_uri": "frame_9626.png"}, {"time": 4884.641, "image_uri": "frame_9627.png"}, {"time": 4885.149, "image_uri": "frame_9628.png"}, {"time": 4885.656, "image_uri": "frame_9629.png"}, {"time": 4886.164, "image_uri": "frame_9630.png"}, {"time": 4886.671, "image_uri": "frame_9631.png"}, {"time": 4887.179, "image_uri": "frame_9632.png"}, {"time": 4887.686, "image_uri": "frame_9633.png"}, {"time": 4888.194, "image_uri": "frame_9634.png"}, {"time": 4888.701, "image_uri": "frame_9635.png"}, {"time": 4889.208, "image_uri": "frame_9636.png"}, {"time": 4889.716, "image_uri": "frame_9637.png"}, {"time": 4890.223, "image_uri": "frame_9638.png"}, {"time": 4890.731, "image_uri": "frame_9639.png"}, {"time": 4891.238, "image_uri": "frame_9640.png"}, {"time": 4891.746, "image_uri": "frame_9641.png"}, {"time": 4892.253, "image_uri": "frame_9642.png"}, {"time": 4892.76, "image_uri": "frame_9643.png"}, {"time": 4893.268, "image_uri": "frame_9644.png"}, {"time": 4893.775, "image_uri": "frame_9645.png"}, {"time": 4894.283, "image_uri": "frame_9646.png"}, {"time": 4894.79, "image_uri": "frame_9647.png"}, {"time": 4895.298, "image_uri": "frame_9648.png"}, {"time": 4895.805, "image_uri": "frame_9649.png"}, {"time": 4896.313, "image_uri": "frame_9650.png"}, {"time": 4896.82, "image_uri": "frame_9651.png"}, {"time": 4897.327, "image_uri": "frame_9652.png"}, {"time": 4897.835, "image_uri": "frame_9653.png"}, {"time": 4898.342, "image_uri": "frame_9654.png"}, {"time": 4898.85, "image_uri": "frame_9655.png"}, {"time": 4899.357, "image_uri": "frame_9656.png"}, {"time": 4899.865, "image_uri": "frame_9657.png"}, {"time": 4900.372, "image_uri": "frame_9658.png"}, {"time": 4900.88, "image_uri": "frame_9659.png"}, {"time": 4901.387, "image_uri": "frame_9660.png"}, {"time": 4901.894, "image_uri": "frame_9661.png"}, {"time": 4902.402, "image_uri": "frame_9662.png"}, {"time": 4902.909, "image_uri": "frame_9663.png"}, {"time": 4903.417, "image_uri": "frame_9664.png"}, {"time": 4903.924, "image_uri": "frame_9665.png"}, {"time": 4904.432, "image_uri": "frame_9666.png"}, {"time": 4904.939, "image_uri": "frame_9667.png"}, {"time": 4905.447, "image_uri": "frame_9668.png"}, {"time": 4905.954, "image_uri": "frame_9669.png"}, {"time": 4906.461, "image_uri": "frame_9670.png"}, {"time": 4906.969, "image_uri": "frame_9671.png"}, {"time": 4907.476, "image_uri": "frame_9672.png"}, {"time": 4907.984, "image_uri": "frame_9673.png"}, {"time": 4908.491, "image_uri": "frame_9674.png"}, {"time": 4908.999, "image_uri": "frame_9675.png"}, {"time": 4909.506, "image_uri": "frame_9676.png"}, {"time": 4910.014, "image_uri": "frame_9677.png"}, {"time": 4910.521, "image_uri": "frame_9678.png"}, {"time": 4911.028, "image_uri": "frame_9679.png"}, {"time": 4911.536, "image_uri": "frame_9680.png"}, {"time": 4912.043, "image_uri": "frame_9681.png"}, {"time": 4912.551, "image_uri": "frame_9682.png"}, {"time": 4913.058, "image_uri": "frame_9683.png"}, {"time": 4913.566, "image_uri": "frame_9684.png"}, {"time": 4914.073, "image_uri": "frame_9685.png"}, {"time": 4914.581, "image_uri": "frame_9686.png"}, {"time": 4915.088, "image_uri": "frame_9687.png"}, {"time": 4915.595, "image_uri": "frame_9688.png"}, {"time": 4916.103, "image_uri": "frame_9689.png"}, {"time": 4916.61, "image_uri": "frame_9690.png"}, {"time": 4917.118, "image_uri": "frame_9691.png"}, {"time": 4917.625, "image_uri": "frame_9692.png"}, {"time": 4918.133, "image_uri": "frame_9693.png"}, {"time": 4918.64, "image_uri": "frame_9694.png"}, {"time": 4919.147, "image_uri": "frame_9695.png"}, {"time": 4919.655, "image_uri": "frame_9696.png"}, {"time": 4920.162, "image_uri": "frame_9697.png"}, {"time": 4920.67, "image_uri": "frame_9698.png"}, {"time": 4921.177, "image_uri": "frame_9699.png"}, {"time": 4921.685, "image_uri": "frame_9700.png"}, {"time": 4922.192, "image_uri": "frame_9701.png"}, {"time": 4922.7, "image_uri": "frame_9702.png"}, {"time": 4923.207, "image_uri": "frame_9703.png"}, {"time": 4923.714, "image_uri": "frame_9704.png"}, {"time": 4924.222, "image_uri": "frame_9705.png"}, {"time": 4924.729, "image_uri": "frame_9706.png"}, {"time": 4925.237, "image_uri": "frame_9707.png"}, {"time": 4925.744, "image_uri": "frame_9708.png"}, {"time": 4926.252, "image_uri": "frame_9709.png"}, {"time": 4926.759, "image_uri": "frame_9710.png"}, {"time": 4927.267, "image_uri": "frame_9711.png"}, {"time": 4927.774, "image_uri": "frame_9712.png"}, {"time": 4928.281, "image_uri": "frame_9713.png"}, {"time": 4928.789, "image_uri": "frame_9714.png"}, {"time": 4929.296, "image_uri": "frame_9715.png"}, {"time": 4929.804, "image_uri": "frame_9716.png"}, {"time": 4930.311, "image_uri": "frame_9717.png"}, {"time": 4930.819, "image_uri": "frame_9718.png"}, {"time": 4931.326, "image_uri": "frame_9719.png"}, {"time": 4931.834, "image_uri": "frame_9720.png"}, {"time": 4932.341, "image_uri": "frame_9721.png"}, {"time": 4932.848, "image_uri": "frame_9722.png"}, {"time": 4933.356, "image_uri": "frame_9723.png"}, {"time": 4933.863, "image_uri": "frame_9724.png"}, {"time": 4934.371, "image_uri": "frame_9725.png"}, {"time": 4934.878, "image_uri": "frame_9726.png"}, {"time": 4935.386, "image_uri": "frame_9727.png"}, {"time": 4935.893, "image_uri": "frame_9728.png"}, {"time": 4936.401, "image_uri": "frame_9729.png"}, {"time": 4936.908, "image_uri": "frame_9730.png"}, {"time": 4937.415, "image_uri": "frame_9731.png"}, {"time": 4937.923, "image_uri": "frame_9732.png"}, {"time": 4938.43, "image_uri": "frame_9733.png"}, {"time": 4938.938, "image_uri": "frame_9734.png"}, {"time": 4939.445, "image_uri": "frame_9735.png"}, {"time": 4939.953, "image_uri": "frame_9736.png"}, {"time": 4940.46, "image_uri": "frame_9737.png"}, {"time": 4940.968, "image_uri": "frame_9738.png"}, {"time": 4941.475, "image_uri": "frame_9739.png"}, {"time": 4941.982, "image_uri": "frame_9740.png"}, {"time": 4942.49, "image_uri": "frame_9741.png"}, {"time": 4942.997, "image_uri": "frame_9742.png"}, {"time": 4943.505, "image_uri": "frame_9743.png"}, {"time": 4944.012, "image_uri": "frame_9744.png"}, {"time": 4944.52, "image_uri": "frame_9745.png"}, {"time": 4945.027, "image_uri": "frame_9746.png"}, {"time": 4945.535, "image_uri": "frame_9747.png"}, {"time": 4946.042, "image_uri": "frame_9748.png"}, {"time": 4946.549, "image_uri": "frame_9749.png"}, {"time": 4947.057, "image_uri": "frame_9750.png"}, {"time": 4947.564, "image_uri": "frame_9751.png"}, {"time": 4948.072, "image_uri": "frame_9752.png"}, {"time": 4948.579, "image_uri": "frame_9753.png"}, {"time": 4949.087, "image_uri": "frame_9754.png"}, {"time": 4949.594, "image_uri": "frame_9755.png"}, {"time": 4950.101, "image_uri": "frame_9756.png"}, {"time": 4950.609, "image_uri": "frame_9757.png"}, {"time": 4951.116, "image_uri": "frame_9758.png"}, {"time": 4951.624, "image_uri": "frame_9759.png"}, {"time": 4952.131, "image_uri": "frame_9760.png"}, {"time": 4952.639, "image_uri": "frame_9761.png"}, {"time": 4953.146, "image_uri": "frame_9762.png"}, {"time": 4953.654, "image_uri": "frame_9763.png"}, {"time": 4954.161, "image_uri": "frame_9764.png"}, {"time": 4954.668, "image_uri": "frame_9765.png"}, {"time": 4955.176, "image_uri": "frame_9766.png"}, {"time": 4955.683, "image_uri": "frame_9767.png"}, {"time": 4956.191, "image_uri": "frame_9768.png"}, {"time": 4956.698, "image_uri": "frame_9769.png"}, {"time": 4957.206, "image_uri": "frame_9770.png"}, {"time": 4957.713, "image_uri": "frame_9771.png"}, {"time": 4958.221, "image_uri": "frame_9772.png"}, {"time": 4958.728, "image_uri": "frame_9773.png"}, {"time": 4959.235, "image_uri": "frame_9774.png"}, {"time": 4959.743, "image_uri": "frame_9775.png"}, {"time": 4960.25, "image_uri": "frame_9776.png"}, {"time": 4960.758, "image_uri": "frame_9777.png"}, {"time": 4961.265, "image_uri": "frame_9778.png"}, {"time": 4961.773, "image_uri": "frame_9779.png"}, {"time": 4962.28, "image_uri": "frame_9780.png"}, {"time": 4962.788, "image_uri": "frame_9781.png"}, {"time": 4963.295, "image_uri": "frame_9782.png"}, {"time": 4963.802, "image_uri": "frame_9783.png"}, {"time": 4964.31, "image_uri": "frame_9784.png"}, {"time": 4964.817, "image_uri": "frame_9785.png"}, {"time": 4965.325, "image_uri": "frame_9786.png"}, {"time": 4965.832, "image_uri": "frame_9787.png"}, {"time": 4966.34, "image_uri": "frame_9788.png"}, {"time": 4966.847, "image_uri": "frame_9789.png"}, {"time": 4967.355, "image_uri": "frame_9790.png"}, {"time": 4967.862, "image_uri": "frame_9791.png"}, {"time": 4968.369, "image_uri": "frame_9792.png"}, {"time": 4968.877, "image_uri": "frame_9793.png"}, {"time": 4969.384, "image_uri": "frame_9794.png"}, {"time": 4969.892, "image_uri": "frame_9795.png"}, {"time": 4970.399, "image_uri": "frame_9796.png"}, {"time": 4970.907, "image_uri": "frame_9797.png"}, {"time": 4971.414, "image_uri": "frame_9798.png"}, {"time": 4971.922, "image_uri": "frame_9799.png"}, {"time": 4972.429, "image_uri": "frame_9800.png"}, {"time": 4972.936, "image_uri": "frame_9801.png"}, {"time": 4973.444, "image_uri": "frame_9802.png"}, {"time": 4973.951, "image_uri": "frame_9803.png"}, {"time": 4974.459, "image_uri": "frame_9804.png"}, {"time": 4974.966, "image_uri": "frame_9805.png"}, {"time": 4975.474, "image_uri": "frame_9806.png"}, {"time": 4975.981, "image_uri": "frame_9807.png"}, {"time": 4976.488, "image_uri": "frame_9808.png"}, {"time": 4976.996, "image_uri": "frame_9809.png"}, {"time": 4977.503, "image_uri": "frame_9810.png"}, {"time": 4978.011, "image_uri": "frame_9811.png"}, {"time": 4978.518, "image_uri": "frame_9812.png"}, {"time": 4979.026, "image_uri": "frame_9813.png"}, {"time": 4979.533, "image_uri": "frame_9814.png"}, {"time": 4980.041, "image_uri": "frame_9815.png"}, {"time": 4980.548, "image_uri": "frame_9816.png"}, {"time": 4981.055, "image_uri": "frame_9817.png"}, {"time": 4981.563, "image_uri": "frame_9818.png"}, {"time": 4982.07, "image_uri": "frame_9819.png"}, {"time": 4982.578, "image_uri": "frame_9820.png"}, {"time": 4983.085, "image_uri": "frame_9821.png"}, {"time": 4983.593, "image_uri": "frame_9822.png"}, {"time": 4984.1, "image_uri": "frame_9823.png"}, {"time": 4984.608, "image_uri": "frame_9824.png"}, {"time": 4985.115, "image_uri": "frame_9825.png"}, {"time": 4985.622, "image_uri": "frame_9826.png"}, {"time": 4986.13, "image_uri": "frame_9827.png"}, {"time": 4986.637, "image_uri": "frame_9828.png"}, {"time": 4987.145, "image_uri": "frame_9829.png"}, {"time": 4987.652, "image_uri": "frame_9830.png"}, {"time": 4988.16, "image_uri": "frame_9831.png"}, {"time": 4988.667, "image_uri": "frame_9832.png"}, {"time": 4989.175, "image_uri": "frame_9833.png"}, {"time": 4989.682, "image_uri": "frame_9834.png"}, {"time": 4990.189, "image_uri": "frame_9835.png"}, {"time": 4990.697, "image_uri": "frame_9836.png"}, {"time": 4991.204, "image_uri": "frame_9837.png"}, {"time": 4991.712, "image_uri": "frame_9838.png"}, {"time": 4992.219, "image_uri": "frame_9839.png"}, {"time": 4992.727, "image_uri": "frame_9840.png"}, {"time": 4993.234, "image_uri": "frame_9841.png"}, {"time": 4993.742, "image_uri": "frame_9842.png"}, {"time": 4994.249, "image_uri": "frame_9843.png"}, {"time": 4994.756, "image_uri": "frame_9844.png"}, {"time": 4995.264, "image_uri": "frame_9845.png"}, {"time": 4995.771, "image_uri": "frame_9846.png"}, {"time": 4996.279, "image_uri": "frame_9847.png"}, {"time": 4996.786, "image_uri": "frame_9848.png"}, {"time": 4997.294, "image_uri": "frame_9849.png"}, {"time": 4997.801, "image_uri": "frame_9850.png"}, {"time": 4998.309, "image_uri": "frame_9851.png"}, {"time": 4998.816, "image_uri": "frame_9852.png"}, {"time": 4999.323, "image_uri": "frame_9853.png"}, {"time": 4999.831, "image_uri": "frame_9854.png"}, {"time": 5000.338, "image_uri": "frame_9855.png"}, {"time": 5000.846, "image_uri": "frame_9856.png"}, {"time": 5001.353, "image_uri": "frame_9857.png"}, {"time": 5001.861, "image_uri": "frame_9858.png"}, {"time": 5002.368, "image_uri": "frame_9859.png"}, {"time": 5002.876, "image_uri": "frame_9860.png"}, {"time": 5003.383, "image_uri": "frame_9861.png"}, {"time": 5003.89, "image_uri": "frame_9862.png"}, {"time": 5004.398, "image_uri": "frame_9863.png"}, {"time": 5004.905, "image_uri": "frame_9864.png"}, {"time": 5005.413, "image_uri": "frame_9865.png"}, {"time": 5005.92, "image_uri": "frame_9866.png"}, {"time": 5006.428, "image_uri": "frame_9867.png"}, {"time": 5006.935, "image_uri": "frame_9868.png"}, {"time": 5007.442, "image_uri": "frame_9869.png"}, {"time": 5007.95, "image_uri": "frame_9870.png"}, {"time": 5008.457, "image_uri": "frame_9871.png"}, {"time": 5008.965, "image_uri": "frame_9872.png"}, {"time": 5009.472, "image_uri": "frame_9873.png"}, {"time": 5009.98, "image_uri": "frame_9874.png"}, {"time": 5010.487, "image_uri": "frame_9875.png"}, {"time": 5010.995, "image_uri": "frame_9876.png"}, {"time": 5011.502, "image_uri": "frame_9877.png"}, {"time": 5012.009, "image_uri": "frame_9878.png"}, {"time": 5012.517, "image_uri": "frame_9879.png"}, {"time": 5013.024, "image_uri": "frame_9880.png"}, {"time": 5013.532, "image_uri": "frame_9881.png"}, {"time": 5014.039, "image_uri": "frame_9882.png"}, {"time": 5014.547, "image_uri": "frame_9883.png"}, {"time": 5015.054, "image_uri": "frame_9884.png"}, {"time": 5015.562, "image_uri": "frame_9885.png"}, {"time": 5016.069, "image_uri": "frame_9886.png"}, {"time": 5016.576, "image_uri": "frame_9887.png"}, {"time": 5017.084, "image_uri": "frame_9888.png"}, {"time": 5017.591, "image_uri": "frame_9889.png"}, {"time": 5018.099, "image_uri": "frame_9890.png"}, {"time": 5018.606, "image_uri": "frame_9891.png"}, {"time": 5019.114, "image_uri": "frame_9892.png"}, {"time": 5019.621, "image_uri": "frame_9893.png"}, {"time": 5020.129, "image_uri": "frame_9894.png"}, {"time": 5020.636, "image_uri": "frame_9895.png"}, {"time": 5021.143, "image_uri": "frame_9896.png"}, {"time": 5021.651, "image_uri": "frame_9897.png"}, {"time": 5022.158, "image_uri": "frame_9898.png"}, {"time": 5022.666, "image_uri": "frame_9899.png"}, {"time": 5023.173, "image_uri": "frame_9900.png"}, {"time": 5023.681, "image_uri": "frame_9901.png"}, {"time": 5024.188, "image_uri": "frame_9902.png"}, {"time": 5024.696, "image_uri": "frame_9903.png"}, {"time": 5025.203, "image_uri": "frame_9904.png"}, {"time": 5025.71, "image_uri": "frame_9905.png"}, {"time": 5026.218, "image_uri": "frame_9906.png"}, {"time": 5026.725, "image_uri": "frame_9907.png"}, {"time": 5027.233, "image_uri": "frame_9908.png"}, {"time": 5027.74, "image_uri": "frame_9909.png"}, {"time": 5028.248, "image_uri": "frame_9910.png"}, {"time": 5028.755, "image_uri": "frame_9911.png"}, {"time": 5029.263, "image_uri": "frame_9912.png"}, {"time": 5029.77, "image_uri": "frame_9913.png"}, {"time": 5030.277, "image_uri": "frame_9914.png"}, {"time": 5030.785, "image_uri": "frame_9915.png"}, {"time": 5031.292, "image_uri": "frame_9916.png"}, {"time": 5031.8, "image_uri": "frame_9917.png"}, {"time": 5032.307, "image_uri": "frame_9918.png"}, {"time": 5032.815, "image_uri": "frame_9919.png"}, {"time": 5033.322, "image_uri": "frame_9920.png"}, {"time": 5033.829, "image_uri": "frame_9921.png"}, {"time": 5034.337, "image_uri": "frame_9922.png"}, {"time": 5034.844, "image_uri": "frame_9923.png"}, {"time": 5035.352, "image_uri": "frame_9924.png"}, {"time": 5035.859, "image_uri": "frame_9925.png"}, {"time": 5036.367, "image_uri": "frame_9926.png"}, {"time": 5036.874, "image_uri": "frame_9927.png"}, {"time": 5037.382, "image_uri": "frame_9928.png"}, {"time": 5037.889, "image_uri": "frame_9929.png"}, {"time": 5038.396, "image_uri": "frame_9930.png"}, {"time": 5038.904, "image_uri": "frame_9931.png"}, {"time": 5039.411, "image_uri": "frame_9932.png"}, {"time": 5039.919, "image_uri": "frame_9933.png"}, {"time": 5040.426, "image_uri": "frame_9934.png"}, {"time": 5040.934, "image_uri": "frame_9935.png"}, {"time": 5041.441, "image_uri": "frame_9936.png"}, {"time": 5041.949, "image_uri": "frame_9937.png"}, {"time": 5042.456, "image_uri": "frame_9938.png"}, {"time": 5042.963, "image_uri": "frame_9939.png"}, {"time": 5043.471, "image_uri": "frame_9940.png"}, {"time": 5043.978, "image_uri": "frame_9941.png"}, {"time": 5044.486, "image_uri": "frame_9942.png"}, {"time": 5044.993, "image_uri": "frame_9943.png"}, {"time": 5045.501, "image_uri": "frame_9944.png"}, {"time": 5046.008, "image_uri": "frame_9945.png"}, {"time": 5046.516, "image_uri": "frame_9946.png"}, {"time": 5047.023, "image_uri": "frame_9947.png"}, {"time": 5047.53, "image_uri": "frame_9948.png"}, {"time": 5048.038, "image_uri": "frame_9949.png"}, {"time": 5048.545, "image_uri": "frame_9950.png"}, {"time": 5049.053, "image_uri": "frame_9951.png"}, {"time": 5049.56, "image_uri": "frame_9952.png"}, {"time": 5050.068, "image_uri": "frame_9953.png"}, {"time": 5050.575, "image_uri": "frame_9954.png"}, {"time": 5051.083, "image_uri": "frame_9955.png"}, {"time": 5051.59, "image_uri": "frame_9956.png"}, {"time": 5052.097, "image_uri": "frame_9957.png"}, {"time": 5052.605, "image_uri": "frame_9958.png"}, {"time": 5053.112, "image_uri": "frame_9959.png"}, {"time": 5053.62, "image_uri": "frame_9960.png"}, {"time": 5054.127, "image_uri": "frame_9961.png"}, {"time": 5054.635, "image_uri": "frame_9962.png"}, {"time": 5055.142, "image_uri": "frame_9963.png"}, {"time": 5055.65, "image_uri": "frame_9964.png"}, {"time": 5056.157, "image_uri": "frame_9965.png"}, {"time": 5056.664, "image_uri": "frame_9966.png"}, {"time": 5057.172, "image_uri": "frame_9967.png"}, {"time": 5057.679, "image_uri": "frame_9968.png"}, {"time": 5058.187, "image_uri": "frame_9969.png"}, {"time": 5058.694, "image_uri": "frame_9970.png"}, {"time": 5059.202, "image_uri": "frame_9971.png"}, {"time": 5059.709, "image_uri": "frame_9972.png"}, {"time": 5060.217, "image_uri": "frame_9973.png"}, {"time": 5060.724, "image_uri": "frame_9974.png"}, {"time": 5061.231, "image_uri": "frame_9975.png"}, {"time": 5061.739, "image_uri": "frame_9976.png"}, {"time": 5062.246, "image_uri": "frame_9977.png"}, {"time": 5062.754, "image_uri": "frame_9978.png"}, {"time": 5063.261, "image_uri": "frame_9979.png"}, {"time": 5063.769, "image_uri": "frame_9980.png"}, {"time": 5064.276, "image_uri": "frame_9981.png"}, {"time": 5064.783, "image_uri": "frame_9982.png"}, {"time": 5065.291, "image_uri": "frame_9983.png"}, {"time": 5065.798, "image_uri": "frame_9984.png"}, {"time": 5066.306, "image_uri": "frame_9985.png"}, {"time": 5066.813, "image_uri": "frame_9986.png"}, {"time": 5067.321, "image_uri": "frame_9987.png"}, {"time": 5067.828, "image_uri": "frame_9988.png"}, {"time": 5068.336, "image_uri": "frame_9989.png"}, {"time": 5068.843, "image_uri": "frame_9990.png"}, {"time": 5069.35, "image_uri": "frame_9991.png"}, {"time": 5069.858, "image_uri": "frame_9992.png"}, {"time": 5070.365, "image_uri": "frame_9993.png"}, {"time": 5070.873, "image_uri": "frame_9994.png"}, {"time": 5071.38, "image_uri": "frame_9995.png"}, {"time": 5071.888, "image_uri": "frame_9996.png"}, {"time": 5072.395, "image_uri": "frame_9997.png"}, {"time": 5072.903, "image_uri": "frame_9998.png"}, {"time": 5073.41, "image_uri": "frame_9999.png"}, {"time": 5073.917, "image_uri": "frame_10000.png"}, {"time": 5074.425, "image_uri": "frame_10001.png"}, {"time": 5074.932, "image_uri": "frame_10002.png"}, {"time": 5075.44, "image_uri": "frame_10003.png"}, {"time": 5075.947, "image_uri": "frame_10004.png"}, {"time": 5076.455, "image_uri": "frame_10005.png"}, {"time": 5076.962, "image_uri": "frame_10006.png"}, {"time": 5077.47, "image_uri": "frame_10007.png"}, {"time": 5077.977, "image_uri": "frame_10008.png"}, {"time": 5078.484, "image_uri": "frame_10009.png"}, {"time": 5078.992, "image_uri": "frame_10010.png"}, {"time": 5079.499, "image_uri": "frame_10011.png"}, {"time": 5080.007, "image_uri": "frame_10012.png"}, {"time": 5080.514, "image_uri": "frame_10013.png"}, {"time": 5081.022, "image_uri": "frame_10014.png"}, {"time": 5081.529, "image_uri": "frame_10015.png"}, {"time": 5082.037, "image_uri": "frame_10016.png"}, {"time": 5082.544, "image_uri": "frame_10017.png"}, {"time": 5083.051, "image_uri": "frame_10018.png"}, {"time": 5083.559, "image_uri": "frame_10019.png"}, {"time": 5084.066, "image_uri": "frame_10020.png"}, {"time": 5084.574, "image_uri": "frame_10021.png"}, {"time": 5085.081, "image_uri": "frame_10022.png"}, {"time": 5085.589, "image_uri": "frame_10023.png"}, {"time": 5086.096, "image_uri": "frame_10024.png"}, {"time": 5086.604, "image_uri": "frame_10025.png"}, {"time": 5087.111, "image_uri": "frame_10026.png"}, {"time": 5087.618, "image_uri": "frame_10027.png"}, {"time": 5088.126, "image_uri": "frame_10028.png"}, {"time": 5088.633, "image_uri": "frame_10029.png"}, {"time": 5089.141, "image_uri": "frame_10030.png"}, {"time": 5089.648, "image_uri": "frame_10031.png"}, {"time": 5090.156, "image_uri": "frame_10032.png"}, {"time": 5090.663, "image_uri": "frame_10033.png"}, {"time": 5091.171, "image_uri": "frame_10034.png"}, {"time": 5091.678, "image_uri": "frame_10035.png"}, {"time": 5092.185, "image_uri": "frame_10036.png"}, {"time": 5092.693, "image_uri": "frame_10037.png"}, {"time": 5093.2, "image_uri": "frame_10038.png"}, {"time": 5093.708, "image_uri": "frame_10039.png"}, {"time": 5094.215, "image_uri": "frame_10040.png"}, {"time": 5094.723, "image_uri": "frame_10041.png"}, {"time": 5095.23, "image_uri": "frame_10042.png"}, {"time": 5095.737, "image_uri": "frame_10043.png"}, {"time": 5096.245, "image_uri": "frame_10044.png"}, {"time": 5096.752, "image_uri": "frame_10045.png"}, {"time": 5097.26, "image_uri": "frame_10046.png"}, {"time": 5097.767, "image_uri": "frame_10047.png"}, {"time": 5098.275, "image_uri": "frame_10048.png"}, {"time": 5098.782, "image_uri": "frame_10049.png"}, {"time": 5099.29, "image_uri": "frame_10050.png"}, {"time": 5099.797, "image_uri": "frame_10051.png"}, {"time": 5100.304, "image_uri": "frame_10052.png"}, {"time": 5100.812, "image_uri": "frame_10053.png"}, {"time": 5101.319, "image_uri": "frame_10054.png"}, {"time": 5101.827, "image_uri": "frame_10055.png"}, {"time": 5102.334, "image_uri": "frame_10056.png"}, {"time": 5102.842, "image_uri": "frame_10057.png"}, {"time": 5103.349, "image_uri": "frame_10058.png"}, {"time": 5103.857, "image_uri": "frame_10059.png"}, {"time": 5104.364, "image_uri": "frame_10060.png"}, {"time": 5104.871, "image_uri": "frame_10061.png"}, {"time": 5105.379, "image_uri": "frame_10062.png"}, {"time": 5105.886, "image_uri": "frame_10063.png"}, {"time": 5106.394, "image_uri": "frame_10064.png"}, {"time": 5106.901, "image_uri": "frame_10065.png"}, {"time": 5107.409, "image_uri": "frame_10066.png"}, {"time": 5107.916, "image_uri": "frame_10067.png"}, {"time": 5108.424, "image_uri": "frame_10068.png"}, {"time": 5108.931, "image_uri": "frame_10069.png"}, {"time": 5109.438, "image_uri": "frame_10070.png"}, {"time": 5109.946, "image_uri": "frame_10071.png"}, {"time": 5110.453, "image_uri": "frame_10072.png"}, {"time": 5110.961, "image_uri": "frame_10073.png"}, {"time": 5111.468, "image_uri": "frame_10074.png"}, {"time": 5111.976, "image_uri": "frame_10075.png"}, {"time": 5112.483, "image_uri": "frame_10076.png"}, {"time": 5112.991, "image_uri": "frame_10077.png"}, {"time": 5113.498, "image_uri": "frame_10078.png"}, {"time": 5114.005, "image_uri": "frame_10079.png"}, {"time": 5114.513, "image_uri": "frame_10080.png"}, {"time": 5115.02, "image_uri": "frame_10081.png"}, {"time": 5115.528, "image_uri": "frame_10082.png"}, {"time": 5116.035, "image_uri": "frame_10083.png"}, {"time": 5116.543, "image_uri": "frame_10084.png"}, {"time": 5117.05, "image_uri": "frame_10085.png"}, {"time": 5117.558, "image_uri": "frame_10086.png"}, {"time": 5118.065, "image_uri": "frame_10087.png"}, {"time": 5118.572, "image_uri": "frame_10088.png"}, {"time": 5119.08, "image_uri": "frame_10089.png"}, {"time": 5119.587, "image_uri": "frame_10090.png"}, {"time": 5120.095, "image_uri": "frame_10091.png"}, {"time": 5120.602, "image_uri": "frame_10092.png"}, {"time": 5121.11, "image_uri": "frame_10093.png"}, {"time": 5121.617, "image_uri": "frame_10094.png"}, {"time": 5122.124, "image_uri": "frame_10095.png"}, {"time": 5122.632, "image_uri": "frame_10096.png"}, {"time": 5123.139, "image_uri": "frame_10097.png"}, {"time": 5123.647, "image_uri": "frame_10098.png"}, {"time": 5124.154, "image_uri": "frame_10099.png"}, {"time": 5124.662, "image_uri": "frame_10100.png"}, {"time": 5125.169, "image_uri": "frame_10101.png"}, {"time": 5125.677, "image_uri": "frame_10102.png"}, {"time": 5126.184, "image_uri": "frame_10103.png"}, {"time": 5126.691, "image_uri": "frame_10104.png"}, {"time": 5127.199, "image_uri": "frame_10105.png"}, {"time": 5127.706, "image_uri": "frame_10106.png"}, {"time": 5128.214, "image_uri": "frame_10107.png"}, {"time": 5128.721, "image_uri": "frame_10108.png"}, {"time": 5129.229, "image_uri": "frame_10109.png"}, {"time": 5129.736, "image_uri": "frame_10110.png"}, {"time": 5130.244, "image_uri": "frame_10111.png"}, {"time": 5130.751, "image_uri": "frame_10112.png"}, {"time": 5131.258, "image_uri": "frame_10113.png"}, {"time": 5131.766, "image_uri": "frame_10114.png"}, {"time": 5132.273, "image_uri": "frame_10115.png"}, {"time": 5132.781, "image_uri": "frame_10116.png"}, {"time": 5133.288, "image_uri": "frame_10117.png"}, {"time": 5133.796, "image_uri": "frame_10118.png"}, {"time": 5134.303, "image_uri": "frame_10119.png"}, {"time": 5134.811, "image_uri": "frame_10120.png"}, {"time": 5135.318, "image_uri": "frame_10121.png"}, {"time": 5135.825, "image_uri": "frame_10122.png"}, {"time": 5136.333, "image_uri": "frame_10123.png"}, {"time": 5136.84, "image_uri": "frame_10124.png"}, {"time": 5137.348, "image_uri": "frame_10125.png"}, {"time": 5137.855, "image_uri": "frame_10126.png"}, {"time": 5138.363, "image_uri": "frame_10127.png"}, {"time": 5138.87, "image_uri": "frame_10128.png"}, {"time": 5139.378, "image_uri": "frame_10129.png"}, {"time": 5139.885, "image_uri": "frame_10130.png"}, {"time": 5140.392, "image_uri": "frame_10131.png"}, {"time": 5140.9, "image_uri": "frame_10132.png"}, {"time": 5141.407, "image_uri": "frame_10133.png"}, {"time": 5141.915, "image_uri": "frame_10134.png"}, {"time": 5142.422, "image_uri": "frame_10135.png"}, {"time": 5142.93, "image_uri": "frame_10136.png"}, {"time": 5143.437, "image_uri": "frame_10137.png"}, {"time": 5143.945, "image_uri": "frame_10138.png"}, {"time": 5144.452, "image_uri": "frame_10139.png"}, {"time": 5144.959, "image_uri": "frame_10140.png"}, {"time": 5145.467, "image_uri": "frame_10141.png"}, {"time": 5145.974, "image_uri": "frame_10142.png"}, {"time": 5146.482, "image_uri": "frame_10143.png"}, {"time": 5146.989, "image_uri": "frame_10144.png"}, {"time": 5147.497, "image_uri": "frame_10145.png"}, {"time": 5148.004, "image_uri": "frame_10146.png"}, {"time": 5148.512, "image_uri": "frame_10147.png"}, {"time": 5149.019, "image_uri": "frame_10148.png"}, {"time": 5149.526, "image_uri": "frame_10149.png"}, {"time": 5150.034, "image_uri": "frame_10150.png"}, {"time": 5150.541, "image_uri": "frame_10151.png"}, {"time": 5151.049, "image_uri": "frame_10152.png"}, {"time": 5151.556, "image_uri": "frame_10153.png"}, {"time": 5152.064, "image_uri": "frame_10154.png"}, {"time": 5152.571, "image_uri": "frame_10155.png"}, {"time": 5153.078, "image_uri": "frame_10156.png"}, {"time": 5153.586, "image_uri": "frame_10157.png"}, {"time": 5154.093, "image_uri": "frame_10158.png"}, {"time": 5154.601, "image_uri": "frame_10159.png"}, {"time": 5155.108, "image_uri": "frame_10160.png"}, {"time": 5155.616, "image_uri": "frame_10161.png"}, {"time": 5156.123, "image_uri": "frame_10162.png"}, {"time": 5156.631, "image_uri": "frame_10163.png"}, {"time": 5157.138, "image_uri": "frame_10164.png"}, {"time": 5157.645, "image_uri": "frame_10165.png"}, {"time": 5158.153, "image_uri": "frame_10166.png"}, {"time": 5158.66, "image_uri": "frame_10167.png"}, {"time": 5159.168, "image_uri": "frame_10168.png"}, {"time": 5159.675, "image_uri": "frame_10169.png"}, {"time": 5160.183, "image_uri": "frame_10170.png"}, {"time": 5160.69, "image_uri": "frame_10171.png"}, {"time": 5161.198, "image_uri": "frame_10172.png"}, {"time": 5161.705, "image_uri": "frame_10173.png"}, {"time": 5162.212, "image_uri": "frame_10174.png"}, {"time": 5162.72, "image_uri": "frame_10175.png"}, {"time": 5163.227, "image_uri": "frame_10176.png"}, {"time": 5163.735, "image_uri": "frame_10177.png"}, {"time": 5164.242, "image_uri": "frame_10178.png"}, {"time": 5164.75, "image_uri": "frame_10179.png"}, {"time": 5165.257, "image_uri": "frame_10180.png"}, {"time": 5165.765, "image_uri": "frame_10181.png"}, {"time": 5166.272, "image_uri": "frame_10182.png"}, {"time": 5166.779, "image_uri": "frame_10183.png"}, {"time": 5167.287, "image_uri": "frame_10184.png"}, {"time": 5167.794, "image_uri": "frame_10185.png"}, {"time": 5168.302, "image_uri": "frame_10186.png"}, {"time": 5168.809, "image_uri": "frame_10187.png"}, {"time": 5169.317, "image_uri": "frame_10188.png"}, {"time": 5169.824, "image_uri": "frame_10189.png"}, {"time": 5170.332, "image_uri": "frame_10190.png"}, {"time": 5170.839, "image_uri": "frame_10191.png"}, {"time": 5171.346, "image_uri": "frame_10192.png"}, {"time": 5171.854, "image_uri": "frame_10193.png"}, {"time": 5172.361, "image_uri": "frame_10194.png"}, {"time": 5172.869, "image_uri": "frame_10195.png"}, {"time": 5173.376, "image_uri": "frame_10196.png"}, {"time": 5173.884, "image_uri": "frame_10197.png"}, {"time": 5174.391, "image_uri": "frame_10198.png"}, {"time": 5174.899, "image_uri": "frame_10199.png"}, {"time": 5175.406, "image_uri": "frame_10200.png"}, {"time": 5175.913, "image_uri": "frame_10201.png"}, {"time": 5176.421, "image_uri": "frame_10202.png"}, {"time": 5176.928, "image_uri": "frame_10203.png"}, {"time": 5177.436, "image_uri": "frame_10204.png"}, {"time": 5177.943, "image_uri": "frame_10205.png"}, {"time": 5178.451, "image_uri": "frame_10206.png"}, {"time": 5178.958, "image_uri": "frame_10207.png"}, {"time": 5179.465, "image_uri": "frame_10208.png"}, {"time": 5179.973, "image_uri": "frame_10209.png"}, {"time": 5180.48, "image_uri": "frame_10210.png"}, {"time": 5180.988, "image_uri": "frame_10211.png"}, {"time": 5181.495, "image_uri": "frame_10212.png"}, {"time": 5182.003, "image_uri": "frame_10213.png"}, {"time": 5182.51, "image_uri": "frame_10214.png"}, {"time": 5183.018, "image_uri": "frame_10215.png"}, {"time": 5183.525, "image_uri": "frame_10216.png"}, {"time": 5184.032, "image_uri": "frame_10217.png"}, {"time": 5184.54, "image_uri": "frame_10218.png"}, {"time": 5185.047, "image_uri": "frame_10219.png"}, {"time": 5185.555, "image_uri": "frame_10220.png"}, {"time": 5186.062, "image_uri": "frame_10221.png"}, {"time": 5186.57, "image_uri": "frame_10222.png"}, {"time": 5187.077, "image_uri": "frame_10223.png"}, {"time": 5187.585, "image_uri": "frame_10224.png"}, {"time": 5188.092, "image_uri": "frame_10225.png"}, {"time": 5188.599, "image_uri": "frame_10226.png"}, {"time": 5189.107, "image_uri": "frame_10227.png"}, {"time": 5189.614, "image_uri": "frame_10228.png"}, {"time": 5190.122, "image_uri": "frame_10229.png"}, {"time": 5190.629, "image_uri": "frame_10230.png"}, {"time": 5191.137, "image_uri": "frame_10231.png"}, {"time": 5191.644, "image_uri": "frame_10232.png"}, {"time": 5192.152, "image_uri": "frame_10233.png"}, {"time": 5192.659, "image_uri": "frame_10234.png"}, {"time": 5193.166, "image_uri": "frame_10235.png"}, {"time": 5193.674, "image_uri": "frame_10236.png"}, {"time": 5194.181, "image_uri": "frame_10237.png"}, {"time": 5194.689, "image_uri": "frame_10238.png"}, {"time": 5195.196, "image_uri": "frame_10239.png"}, {"time": 5195.704, "image_uri": "frame_10240.png"}, {"time": 5196.211, "image_uri": "frame_10241.png"}, {"time": 5196.719, "image_uri": "frame_10242.png"}, {"time": 5197.226, "image_uri": "frame_10243.png"}, {"time": 5197.733, "image_uri": "frame_10244.png"}, {"time": 5198.241, "image_uri": "frame_10245.png"}, {"time": 5198.748, "image_uri": "frame_10246.png"}, {"time": 5199.256, "image_uri": "frame_10247.png"}, {"time": 5199.763, "image_uri": "frame_10248.png"}, {"time": 5200.271, "image_uri": "frame_10249.png"}, {"time": 5200.778, "image_uri": "frame_10250.png"}, {"time": 5201.286, "image_uri": "frame_10251.png"}, {"time": 5201.793, "image_uri": "frame_10252.png"}, {"time": 5202.3, "image_uri": "frame_10253.png"}, {"time": 5202.808, "image_uri": "frame_10254.png"}, {"time": 5203.315, "image_uri": "frame_10255.png"}, {"time": 5203.823, "image_uri": "frame_10256.png"}, {"time": 5204.33, "image_uri": "frame_10257.png"}, {"time": 5204.838, "image_uri": "frame_10258.png"}, {"time": 5205.345, "image_uri": "frame_10259.png"}, {"time": 5205.853, "image_uri": "frame_10260.png"}, {"time": 5206.36, "image_uri": "frame_10261.png"}, {"time": 5206.867, "image_uri": "frame_10262.png"}, {"time": 5207.375, "image_uri": "frame_10263.png"}, {"time": 5207.882, "image_uri": "frame_10264.png"}, {"time": 5208.39, "image_uri": "frame_10265.png"}, {"time": 5208.897, "image_uri": "frame_10266.png"}, {"time": 5209.405, "image_uri": "frame_10267.png"}, {"time": 5209.912, "image_uri": "frame_10268.png"}, {"time": 5210.419, "image_uri": "frame_10269.png"}, {"time": 5210.927, "image_uri": "frame_10270.png"}, {"time": 5211.434, "image_uri": "frame_10271.png"}, {"time": 5211.942, "image_uri": "frame_10272.png"}, {"time": 5212.449, "image_uri": "frame_10273.png"}, {"time": 5212.957, "image_uri": "frame_10274.png"}, {"time": 5213.464, "image_uri": "frame_10275.png"}, {"time": 5213.972, "image_uri": "frame_10276.png"}, {"time": 5214.479, "image_uri": "frame_10277.png"}, {"time": 5214.986, "image_uri": "frame_10278.png"}, {"time": 5215.494, "image_uri": "frame_10279.png"}, {"time": 5216.001, "image_uri": "frame_10280.png"}, {"time": 5216.509, "image_uri": "frame_10281.png"}, {"time": 5217.016, "image_uri": "frame_10282.png"}, {"time": 5217.524, "image_uri": "frame_10283.png"}, {"time": 5218.031, "image_uri": "frame_10284.png"}, {"time": 5218.539, "image_uri": "frame_10285.png"}, {"time": 5219.046, "image_uri": "frame_10286.png"}, {"time": 5219.553, "image_uri": "frame_10287.png"}, {"time": 5220.061, "image_uri": "frame_10288.png"}, {"time": 5220.568, "image_uri": "frame_10289.png"}, {"time": 5221.076, "image_uri": "frame_10290.png"}, {"time": 5221.583, "image_uri": "frame_10291.png"}, {"time": 5222.091, "image_uri": "frame_10292.png"}, {"time": 5222.598, "image_uri": "frame_10293.png"}, {"time": 5223.106, "image_uri": "frame_10294.png"}, {"time": 5223.613, "image_uri": "frame_10295.png"}, {"time": 5224.12, "image_uri": "frame_10296.png"}, {"time": 5224.628, "image_uri": "frame_10297.png"}, {"time": 5225.135, "image_uri": "frame_10298.png"}, {"time": 5225.643, "image_uri": "frame_10299.png"}, {"time": 5226.15, "image_uri": "frame_10300.png"}, {"time": 5226.658, "image_uri": "frame_10301.png"}, {"time": 5227.165, "image_uri": "frame_10302.png"}, {"time": 5227.673, "image_uri": "frame_10303.png"}, {"time": 5228.18, "image_uri": "frame_10304.png"}, {"time": 5228.687, "image_uri": "frame_10305.png"}, {"time": 5229.195, "image_uri": "frame_10306.png"}, {"time": 5229.702, "image_uri": "frame_10307.png"}, {"time": 5230.21, "image_uri": "frame_10308.png"}, {"time": 5230.717, "image_uri": "frame_10309.png"}, {"time": 5231.225, "image_uri": "frame_10310.png"}, {"time": 5231.732, "image_uri": "frame_10311.png"}, {"time": 5232.24, "image_uri": "frame_10312.png"}, {"time": 5232.747, "image_uri": "frame_10313.png"}, {"time": 5233.254, "image_uri": "frame_10314.png"}, {"time": 5233.762, "image_uri": "frame_10315.png"}, {"time": 5234.269, "image_uri": "frame_10316.png"}, {"time": 5234.777, "image_uri": "frame_10317.png"}, {"time": 5235.284, "image_uri": "frame_10318.png"}, {"time": 5235.792, "image_uri": "frame_10319.png"}, {"time": 5236.299, "image_uri": "frame_10320.png"}, {"time": 5236.806, "image_uri": "frame_10321.png"}, {"time": 5237.314, "image_uri": "frame_10322.png"}, {"time": 5237.821, "image_uri": "frame_10323.png"}, {"time": 5238.329, "image_uri": "frame_10324.png"}, {"time": 5238.836, "image_uri": "frame_10325.png"}, {"time": 5239.344, "image_uri": "frame_10326.png"}, {"time": 5239.851, "image_uri": "frame_10327.png"}, {"time": 5240.359, "image_uri": "frame_10328.png"}, {"time": 5240.866, "image_uri": "frame_10329.png"}, {"time": 5241.373, "image_uri": "frame_10330.png"}, {"time": 5241.881, "image_uri": "frame_10331.png"}, {"time": 5242.388, "image_uri": "frame_10332.png"}, {"time": 5242.896, "image_uri": "frame_10333.png"}, {"time": 5243.403, "image_uri": "frame_10334.png"}, {"time": 5243.911, "image_uri": "frame_10335.png"}, {"time": 5244.418, "image_uri": "frame_10336.png"}, {"time": 5244.926, "image_uri": "frame_10337.png"}, {"time": 5245.433, "image_uri": "frame_10338.png"}, {"time": 5245.94, "image_uri": "frame_10339.png"}, {"time": 5246.448, "image_uri": "frame_10340.png"}, {"time": 5246.955, "image_uri": "frame_10341.png"}, {"time": 5247.463, "image_uri": "frame_10342.png"}, {"time": 5247.97, "image_uri": "frame_10343.png"}, {"time": 5248.478, "image_uri": "frame_10344.png"}, {"time": 5248.985, "image_uri": "frame_10345.png"}, {"time": 5249.493, "image_uri": "frame_10346.png"}, {"time": 5250.0, "image_uri": "frame_10347.png"}, {"time": 5250.507, "image_uri": "frame_10348.png"}, {"time": 5251.015, "image_uri": "frame_10349.png"}, {"time": 5251.522, "image_uri": "frame_10350.png"}, {"time": 5252.03, "image_uri": "frame_10351.png"}, {"time": 5252.537, "image_uri": "frame_10352.png"}, {"time": 5253.045, "image_uri": "frame_10353.png"}, {"time": 5253.552, "image_uri": "frame_10354.png"}, {"time": 5254.06, "image_uri": "frame_10355.png"}, {"time": 5254.567, "image_uri": "frame_10356.png"}, {"time": 5255.074, "image_uri": "frame_10357.png"}, {"time": 5255.582, "image_uri": "frame_10358.png"}, {"time": 5256.089, "image_uri": "frame_10359.png"}, {"time": 5256.597, "image_uri": "frame_10360.png"}, {"time": 5257.104, "image_uri": "frame_10361.png"}, {"time": 5257.612, "image_uri": "frame_10362.png"}, {"time": 5258.119, "image_uri": "frame_10363.png"}, {"time": 5258.627, "image_uri": "frame_10364.png"}, {"time": 5259.134, "image_uri": "frame_10365.png"}, {"time": 5259.641, "image_uri": "frame_10366.png"}, {"time": 5260.149, "image_uri": "frame_10367.png"}, {"time": 5260.656, "image_uri": "frame_10368.png"}, {"time": 5261.164, "image_uri": "frame_10369.png"}, {"time": 5261.671, "image_uri": "frame_10370.png"}, {"time": 5262.179, "image_uri": "frame_10371.png"}, {"time": 5262.686, "image_uri": "frame_10372.png"}, {"time": 5263.194, "image_uri": "frame_10373.png"}, {"time": 5263.701, "image_uri": "frame_10374.png"}, {"time": 5264.208, "image_uri": "frame_10375.png"}, {"time": 5264.716, "image_uri": "frame_10376.png"}, {"time": 5265.223, "image_uri": "frame_10377.png"}, {"time": 5265.731, "image_uri": "frame_10378.png"}, {"time": 5266.238, "image_uri": "frame_10379.png"}, {"time": 5266.746, "image_uri": "frame_10380.png"}, {"time": 5267.253, "image_uri": "frame_10381.png"}, {"time": 5267.76, "image_uri": "frame_10382.png"}, {"time": 5268.268, "image_uri": "frame_10383.png"}, {"time": 5268.775, "image_uri": "frame_10384.png"}, {"time": 5269.283, "image_uri": "frame_10385.png"}, {"time": 5269.79, "image_uri": "frame_10386.png"}, {"time": 5270.298, "image_uri": "frame_10387.png"}, {"time": 5270.805, "image_uri": "frame_10388.png"}, {"time": 5271.313, "image_uri": "frame_10389.png"}, {"time": 5271.82, "image_uri": "frame_10390.png"}, {"time": 5272.327, "image_uri": "frame_10391.png"}, {"time": 5272.835, "image_uri": "frame_10392.png"}, {"time": 5273.342, "image_uri": "frame_10393.png"}, {"time": 5273.85, "image_uri": "frame_10394.png"}, {"time": 5274.357, "image_uri": "frame_10395.png"}, {"time": 5274.865, "image_uri": "frame_10396.png"}, {"time": 5275.372, "image_uri": "frame_10397.png"}, {"time": 5275.88, "image_uri": "frame_10398.png"}, {"time": 5276.387, "image_uri": "frame_10399.png"}, {"time": 5276.894, "image_uri": "frame_10400.png"}, {"time": 5277.402, "image_uri": "frame_10401.png"}, {"time": 5277.909, "image_uri": "frame_10402.png"}, {"time": 5278.417, "image_uri": "frame_10403.png"}, {"time": 5278.924, "image_uri": "frame_10404.png"}, {"time": 5279.432, "image_uri": "frame_10405.png"}, {"time": 5279.939, "image_uri": "frame_10406.png"}, {"time": 5280.447, "image_uri": "frame_10407.png"}, {"time": 5280.954, "image_uri": "frame_10408.png"}, {"time": 5281.461, "image_uri": "frame_10409.png"}, {"time": 5281.969, "image_uri": "frame_10410.png"}, {"time": 5282.476, "image_uri": "frame_10411.png"}, {"time": 5282.984, "image_uri": "frame_10412.png"}, {"time": 5283.491, "image_uri": "frame_10413.png"}, {"time": 5283.999, "image_uri": "frame_10414.png"}, {"time": 5284.506, "image_uri": "frame_10415.png"}, {"time": 5285.014, "image_uri": "frame_10416.png"}, {"time": 5285.521, "image_uri": "frame_10417.png"}, {"time": 5286.028, "image_uri": "frame_10418.png"}, {"time": 5286.536, "image_uri": "frame_10419.png"}, {"time": 5287.043, "image_uri": "frame_10420.png"}, {"time": 5287.551, "image_uri": "frame_10421.png"}, {"time": 5288.058, "image_uri": "frame_10422.png"}, {"time": 5288.566, "image_uri": "frame_10423.png"}, {"time": 5289.073, "image_uri": "frame_10424.png"}, {"time": 5289.581, "image_uri": "frame_10425.png"}, {"time": 5290.088, "image_uri": "frame_10426.png"}, {"time": 5290.595, "image_uri": "frame_10427.png"}, {"time": 5291.103, "image_uri": "frame_10428.png"}, {"time": 5291.61, "image_uri": "frame_10429.png"}, {"time": 5292.118, "image_uri": "frame_10430.png"}, {"time": 5292.625, "image_uri": "frame_10431.png"}, {"time": 5293.133, "image_uri": "frame_10432.png"}, {"time": 5293.64, "image_uri": "frame_10433.png"}, {"time": 5294.147, "image_uri": "frame_10434.png"}, {"time": 5294.655, "image_uri": "frame_10435.png"}, {"time": 5295.162, "image_uri": "frame_10436.png"}, {"time": 5295.67, "image_uri": "frame_10437.png"}, {"time": 5296.177, "image_uri": "frame_10438.png"}, {"time": 5296.685, "image_uri": "frame_10439.png"}, {"time": 5297.192, "image_uri": "frame_10440.png"}, {"time": 5297.7, "image_uri": "frame_10441.png"}, {"time": 5298.207, "image_uri": "frame_10442.png"}, {"time": 5298.714, "image_uri": "frame_10443.png"}, {"time": 5299.222, "image_uri": "frame_10444.png"}, {"time": 5299.729, "image_uri": "frame_10445.png"}, {"time": 5300.237, "image_uri": "frame_10446.png"}, {"time": 5300.744, "image_uri": "frame_10447.png"}, {"time": 5301.252, "image_uri": "frame_10448.png"}, {"time": 5301.759, "image_uri": "frame_10449.png"}, {"time": 5302.267, "image_uri": "frame_10450.png"}, {"time": 5302.774, "image_uri": "frame_10451.png"}, {"time": 5303.281, "image_uri": "frame_10452.png"}, {"time": 5303.789, "image_uri": "frame_10453.png"}, {"time": 5304.296, "image_uri": "frame_10454.png"}, {"time": 5304.804, "image_uri": "frame_10455.png"}, {"time": 5305.311, "image_uri": "frame_10456.png"}, {"time": 5305.819, "image_uri": "frame_10457.png"}, {"time": 5306.326, "image_uri": "frame_10458.png"}, {"time": 5306.834, "image_uri": "frame_10459.png"}, {"time": 5307.341, "image_uri": "frame_10460.png"}, {"time": 5307.848, "image_uri": "frame_10461.png"}, {"time": 5308.356, "image_uri": "frame_10462.png"}, {"time": 5308.863, "image_uri": "frame_10463.png"}, {"time": 5309.371, "image_uri": "frame_10464.png"}, {"time": 5309.878, "image_uri": "frame_10465.png"}, {"time": 5310.386, "image_uri": "frame_10466.png"}, {"time": 5310.893, "image_uri": "frame_10467.png"}, {"time": 5311.401, "image_uri": "frame_10468.png"}, {"time": 5311.908, "image_uri": "frame_10469.png"}, {"time": 5312.415, "image_uri": "frame_10470.png"}, {"time": 5312.923, "image_uri": "frame_10471.png"}, {"time": 5313.43, "image_uri": "frame_10472.png"}, {"time": 5313.938, "image_uri": "frame_10473.png"}, {"time": 5314.445, "image_uri": "frame_10474.png"}, {"time": 5314.953, "image_uri": "frame_10475.png"}, {"time": 5315.46, "image_uri": "frame_10476.png"}, {"time": 5315.968, "image_uri": "frame_10477.png"}, {"time": 5316.475, "image_uri": "frame_10478.png"}, {"time": 5316.982, "image_uri": "frame_10479.png"}, {"time": 5317.49, "image_uri": "frame_10480.png"}, {"time": 5317.997, "image_uri": "frame_10481.png"}, {"time": 5318.505, "image_uri": "frame_10482.png"}, {"time": 5319.012, "image_uri": "frame_10483.png"}, {"time": 5319.52, "image_uri": "frame_10484.png"}, {"time": 5320.027, "image_uri": "frame_10485.png"}, {"time": 5320.535, "image_uri": "frame_10486.png"}, {"time": 5321.042, "image_uri": "frame_10487.png"}, {"time": 5321.549, "image_uri": "frame_10488.png"}, {"time": 5322.057, "image_uri": "frame_10489.png"}, {"time": 5322.564, "image_uri": "frame_10490.png"}, {"time": 5323.072, "image_uri": "frame_10491.png"}, {"time": 5323.579, "image_uri": "frame_10492.png"}, {"time": 5324.087, "image_uri": "frame_10493.png"}, {"time": 5324.594, "image_uri": "frame_10494.png"}, {"time": 5325.101, "image_uri": "frame_10495.png"}, {"time": 5325.609, "image_uri": "frame_10496.png"}, {"time": 5326.116, "image_uri": "frame_10497.png"}, {"time": 5326.624, "image_uri": "frame_10498.png"}, {"time": 5327.131, "image_uri": "frame_10499.png"}, {"time": 5327.639, "image_uri": "frame_10500.png"}, {"time": 5328.146, "image_uri": "frame_10501.png"}, {"time": 5328.654, "image_uri": "frame_10502.png"}, {"time": 5329.161, "image_uri": "frame_10503.png"}, {"time": 5329.668, "image_uri": "frame_10504.png"}, {"time": 5330.176, "image_uri": "frame_10505.png"}, {"time": 5330.683, "image_uri": "frame_10506.png"}, {"time": 5331.191, "image_uri": "frame_10507.png"}, {"time": 5331.698, "image_uri": "frame_10508.png"}, {"time": 5332.206, "image_uri": "frame_10509.png"}, {"time": 5332.713, "image_uri": "frame_10510.png"}, {"time": 5333.221, "image_uri": "frame_10511.png"}, {"time": 5333.728, "image_uri": "frame_10512.png"}, {"time": 5334.235, "image_uri": "frame_10513.png"}, {"time": 5334.743, "image_uri": "frame_10514.png"}, {"time": 5335.25, "image_uri": "frame_10515.png"}, {"time": 5335.758, "image_uri": "frame_10516.png"}, {"time": 5336.265, "image_uri": "frame_10517.png"}, {"time": 5336.773, "image_uri": "frame_10518.png"}, {"time": 5337.28, "image_uri": "frame_10519.png"}, {"time": 5337.788, "image_uri": "frame_10520.png"}, {"time": 5338.295, "image_uri": "frame_10521.png"}, {"time": 5338.802, "image_uri": "frame_10522.png"}, {"time": 5339.31, "image_uri": "frame_10523.png"}, {"time": 5339.817, "image_uri": "frame_10524.png"}, {"time": 5340.325, "image_uri": "frame_10525.png"}, {"time": 5340.832, "image_uri": "frame_10526.png"}, {"time": 5341.34, "image_uri": "frame_10527.png"}, {"time": 5341.847, "image_uri": "frame_10528.png"}, {"time": 5342.355, "image_uri": "frame_10529.png"}, {"time": 5342.862, "image_uri": "frame_10530.png"}, {"time": 5343.369, "image_uri": "frame_10531.png"}, {"time": 5343.877, "image_uri": "frame_10532.png"}, {"time": 5344.384, "image_uri": "frame_10533.png"}, {"time": 5344.892, "image_uri": "frame_10534.png"}, {"time": 5345.399, "image_uri": "frame_10535.png"}, {"time": 5345.907, "image_uri": "frame_10536.png"}, {"time": 5346.414, "image_uri": "frame_10537.png"}, {"time": 5346.922, "image_uri": "frame_10538.png"}, {"time": 5347.429, "image_uri": "frame_10539.png"}, {"time": 5347.936, "image_uri": "frame_10540.png"}, {"time": 5348.444, "image_uri": "frame_10541.png"}, {"time": 5348.951, "image_uri": "frame_10542.png"}, {"time": 5349.459, "image_uri": "frame_10543.png"}, {"time": 5349.966, "image_uri": "frame_10544.png"}, {"time": 5350.474, "image_uri": "frame_10545.png"}, {"time": 5350.981, "image_uri": "frame_10546.png"}, {"time": 5351.488, "image_uri": "frame_10547.png"}, {"time": 5351.996, "image_uri": "frame_10548.png"}, {"time": 5352.503, "image_uri": "frame_10549.png"}, {"time": 5353.011, "image_uri": "frame_10550.png"}, {"time": 5353.518, "image_uri": "frame_10551.png"}, {"time": 5354.026, "image_uri": "frame_10552.png"}, {"time": 5354.533, "image_uri": "frame_10553.png"}, {"time": 5355.041, "image_uri": "frame_10554.png"}, {"time": 5355.548, "image_uri": "frame_10555.png"}, {"time": 5356.055, "image_uri": "frame_10556.png"}, {"time": 5356.563, "image_uri": "frame_10557.png"}, {"time": 5357.07, "image_uri": "frame_10558.png"}, {"time": 5357.578, "image_uri": "frame_10559.png"}, {"time": 5358.085, "image_uri": "frame_10560.png"}, {"time": 5358.593, "image_uri": "frame_10561.png"}, {"time": 5359.1, "image_uri": "frame_10562.png"}, {"time": 5359.608, "image_uri": "frame_10563.png"}, {"time": 5360.115, "image_uri": "frame_10564.png"}, {"time": 5360.622, "image_uri": "frame_10565.png"}, {"time": 5361.13, "image_uri": "frame_10566.png"}, {"time": 5361.637, "image_uri": "frame_10567.png"}, {"time": 5362.145, "image_uri": "frame_10568.png"}, {"time": 5362.652, "image_uri": "frame_10569.png"}, {"time": 5363.16, "image_uri": "frame_10570.png"}, {"time": 5363.667, "image_uri": "frame_10571.png"}, {"time": 5364.175, "image_uri": "frame_10572.png"}, {"time": 5364.682, "image_uri": "frame_10573.png"}, {"time": 5365.189, "image_uri": "frame_10574.png"}, {"time": 5365.697, "image_uri": "frame_10575.png"}, {"time": 5366.204, "image_uri": "frame_10576.png"}, {"time": 5366.712, "image_uri": "frame_10577.png"}, {"time": 5367.219, "image_uri": "frame_10578.png"}, {"time": 5367.727, "image_uri": "frame_10579.png"}, {"time": 5368.234, "image_uri": "frame_10580.png"}, {"time": 5368.742, "image_uri": "frame_10581.png"}, {"time": 5369.249, "image_uri": "frame_10582.png"}, {"time": 5369.756, "image_uri": "frame_10583.png"}, {"time": 5370.264, "image_uri": "frame_10584.png"}, {"time": 5370.771, "image_uri": "frame_10585.png"}, {"time": 5371.279, "image_uri": "frame_10586.png"}, {"time": 5371.786, "image_uri": "frame_10587.png"}, {"time": 5372.294, "image_uri": "frame_10588.png"}, {"time": 5372.801, "image_uri": "frame_10589.png"}, {"time": 5373.309, "image_uri": "frame_10590.png"}, {"time": 5373.816, "image_uri": "frame_10591.png"}, {"time": 5374.323, "image_uri": "frame_10592.png"}, {"time": 5374.831, "image_uri": "frame_10593.png"}, {"time": 5375.338, "image_uri": "frame_10594.png"}, {"time": 5375.846, "image_uri": "frame_10595.png"}, {"time": 5376.353, "image_uri": "frame_10596.png"}, {"time": 5376.861, "image_uri": "frame_10597.png"}, {"time": 5377.368, "image_uri": "frame_10598.png"}, {"time": 5377.876, "image_uri": "frame_10599.png"}, {"time": 5378.383, "image_uri": "frame_10600.png"}, {"time": 5378.89, "image_uri": "frame_10601.png"}, {"time": 5379.398, "image_uri": "frame_10602.png"}, {"time": 5379.905, "image_uri": "frame_10603.png"}, {"time": 5380.413, "image_uri": "frame_10604.png"}, {"time": 5380.92, "image_uri": "frame_10605.png"}, {"time": 5381.428, "image_uri": "frame_10606.png"}, {"time": 5381.935, "image_uri": "frame_10607.png"}, {"time": 5382.442, "image_uri": "frame_10608.png"}, {"time": 5382.95, "image_uri": "frame_10609.png"}, {"time": 5383.457, "image_uri": "frame_10610.png"}, {"time": 5383.965, "image_uri": "frame_10611.png"}, {"time": 5384.472, "image_uri": "frame_10612.png"}, {"time": 5384.98, "image_uri": "frame_10613.png"}, {"time": 5385.487, "image_uri": "frame_10614.png"}, {"time": 5385.995, "image_uri": "frame_10615.png"}, {"time": 5386.502, "image_uri": "frame_10616.png"}, {"time": 5387.009, "image_uri": "frame_10617.png"}, {"time": 5387.517, "image_uri": "frame_10618.png"}, {"time": 5388.024, "image_uri": "frame_10619.png"}, {"time": 5388.532, "image_uri": "frame_10620.png"}, {"time": 5389.039, "image_uri": "frame_10621.png"}, {"time": 5389.547, "image_uri": "frame_10622.png"}, {"time": 5390.054, "image_uri": "frame_10623.png"}, {"time": 5390.562, "image_uri": "frame_10624.png"}, {"time": 5391.069, "image_uri": "frame_10625.png"}, {"time": 5391.576, "image_uri": "frame_10626.png"}, {"time": 5392.084, "image_uri": "frame_10627.png"}, {"time": 5392.591, "image_uri": "frame_10628.png"}, {"time": 5393.099, "image_uri": "frame_10629.png"}, {"time": 5393.606, "image_uri": "frame_10630.png"}, {"time": 5394.114, "image_uri": "frame_10631.png"}, {"time": 5394.621, "image_uri": "frame_10632.png"}, {"time": 5395.129, "image_uri": "frame_10633.png"}, {"time": 5395.636, "image_uri": "frame_10634.png"}, {"time": 5396.143, "image_uri": "frame_10635.png"}, {"time": 5396.651, "image_uri": "frame_10636.png"}, {"time": 5397.158, "image_uri": "frame_10637.png"}, {"time": 5397.666, "image_uri": "frame_10638.png"}, {"time": 5398.173, "image_uri": "frame_10639.png"}, {"time": 5398.681, "image_uri": "frame_10640.png"}, {"time": 5399.188, "image_uri": "frame_10641.png"}, {"time": 5399.696, "image_uri": "frame_10642.png"}, {"time": 5400.203, "image_uri": "frame_10643.png"}, {"time": 5400.71, "image_uri": "frame_10644.png"}, {"time": 5401.218, "image_uri": "frame_10645.png"}, {"time": 5401.725, "image_uri": "frame_10646.png"}, {"time": 5402.233, "image_uri": "frame_10647.png"}, {"time": 5402.74, "image_uri": "frame_10648.png"}, {"time": 5403.248, "image_uri": "frame_10649.png"}, {"time": 5403.755, "image_uri": "frame_10650.png"}, {"time": 5404.263, "image_uri": "frame_10651.png"}, {"time": 5404.77, "image_uri": "frame_10652.png"}, {"time": 5405.277, "image_uri": "frame_10653.png"}, {"time": 5405.785, "image_uri": "frame_10654.png"}, {"time": 5406.292, "image_uri": "frame_10655.png"}, {"time": 5406.8, "image_uri": "frame_10656.png"}, {"time": 5407.307, "image_uri": "frame_10657.png"}, {"time": 5407.815, "image_uri": "frame_10658.png"}, {"time": 5408.322, "image_uri": "frame_10659.png"}, {"time": 5408.829, "image_uri": "frame_10660.png"}, {"time": 5409.337, "image_uri": "frame_10661.png"}, {"time": 5409.844, "image_uri": "frame_10662.png"}, {"time": 5410.352, "image_uri": "frame_10663.png"}, {"time": 5410.859, "image_uri": "frame_10664.png"}, {"time": 5411.367, "image_uri": "frame_10665.png"}, {"time": 5411.874, "image_uri": "frame_10666.png"}, {"time": 5412.382, "image_uri": "frame_10667.png"}, {"time": 5412.889, "image_uri": "frame_10668.png"}, {"time": 5413.396, "image_uri": "frame_10669.png"}, {"time": 5413.904, "image_uri": "frame_10670.png"}, {"time": 5414.411, "image_uri": "frame_10671.png"}, {"time": 5414.919, "image_uri": "frame_10672.png"}, {"time": 5415.426, "image_uri": "frame_10673.png"}, {"time": 5415.934, "image_uri": "frame_10674.png"}, {"time": 5416.441, "image_uri": "frame_10675.png"}, {"time": 5416.949, "image_uri": "frame_10676.png"}, {"time": 5417.456, "image_uri": "frame_10677.png"}, {"time": 5417.963, "image_uri": "frame_10678.png"}, {"time": 5418.471, "image_uri": "frame_10679.png"}, {"time": 5418.978, "image_uri": "frame_10680.png"}, {"time": 5419.486, "image_uri": "frame_10681.png"}, {"time": 5419.993, "image_uri": "frame_10682.png"}, {"time": 5420.501, "image_uri": "frame_10683.png"}, {"time": 5421.008, "image_uri": "frame_10684.png"}, {"time": 5421.516, "image_uri": "frame_10685.png"}, {"time": 5422.023, "image_uri": "frame_10686.png"}, {"time": 5422.53, "image_uri": "frame_10687.png"}, {"time": 5423.038, "image_uri": "frame_10688.png"}, {"time": 5423.545, "image_uri": "frame_10689.png"}, {"time": 5424.053, "image_uri": "frame_10690.png"}, {"time": 5424.56, "image_uri": "frame_10691.png"}, {"time": 5425.068, "image_uri": "frame_10692.png"}, {"time": 5425.575, "image_uri": "frame_10693.png"}, {"time": 5426.083, "image_uri": "frame_10694.png"}, {"time": 5426.59, "image_uri": "frame_10695.png"}, {"time": 5427.097, "image_uri": "frame_10696.png"}, {"time": 5427.605, "image_uri": "frame_10697.png"}, {"time": 5428.112, "image_uri": "frame_10698.png"}, {"time": 5428.62, "image_uri": "frame_10699.png"}, {"time": 5429.127, "image_uri": "frame_10700.png"}, {"time": 5429.635, "image_uri": "frame_10701.png"}, {"time": 5430.142, "image_uri": "frame_10702.png"}, {"time": 5430.65, "image_uri": "frame_10703.png"}, {"time": 5431.157, "image_uri": "frame_10704.png"}, {"time": 5431.664, "image_uri": "frame_10705.png"}, {"time": 5432.172, "image_uri": "frame_10706.png"}, {"time": 5432.679, "image_uri": "frame_10707.png"}, {"time": 5433.187, "image_uri": "frame_10708.png"}, {"time": 5433.694, "image_uri": "frame_10709.png"}, {"time": 5434.202, "image_uri": "frame_10710.png"}, {"time": 5434.709, "image_uri": "frame_10711.png"}, {"time": 5435.217, "image_uri": "frame_10712.png"}, {"time": 5435.724, "image_uri": "frame_10713.png"}, {"time": 5436.231, "image_uri": "frame_10714.png"}, {"time": 5436.739, "image_uri": "frame_10715.png"}, {"time": 5437.246, "image_uri": "frame_10716.png"}, {"time": 5437.754, "image_uri": "frame_10717.png"}, {"time": 5438.261, "image_uri": "frame_10718.png"}, {"time": 5438.769, "image_uri": "frame_10719.png"}, {"time": 5439.276, "image_uri": "frame_10720.png"}, {"time": 5439.783, "image_uri": "frame_10721.png"}, {"time": 5440.291, "image_uri": "frame_10722.png"}, {"time": 5440.798, "image_uri": "frame_10723.png"}, {"time": 5441.306, "image_uri": "frame_10724.png"}, {"time": 5441.813, "image_uri": "frame_10725.png"}, {"time": 5442.321, "image_uri": "frame_10726.png"}, {"time": 5442.828, "image_uri": "frame_10727.png"}, {"time": 5443.336, "image_uri": "frame_10728.png"}, {"time": 5443.843, "image_uri": "frame_10729.png"}, {"time": 5444.35, "image_uri": "frame_10730.png"}, {"time": 5444.858, "image_uri": "frame_10731.png"}, {"time": 5445.365, "image_uri": "frame_10732.png"}, {"time": 5445.873, "image_uri": "frame_10733.png"}, {"time": 5446.38, "image_uri": "frame_10734.png"}, {"time": 5446.888, "image_uri": "frame_10735.png"}, {"time": 5447.395, "image_uri": "frame_10736.png"}, {"time": 5447.903, "image_uri": "frame_10737.png"}, {"time": 5448.41, "image_uri": "frame_10738.png"}, {"time": 5448.917, "image_uri": "frame_10739.png"}, {"time": 5449.425, "image_uri": "frame_10740.png"}, {"time": 5449.932, "image_uri": "frame_10741.png"}, {"time": 5450.44, "image_uri": "frame_10742.png"}, {"time": 5450.947, "image_uri": "frame_10743.png"}, {"time": 5451.455, "image_uri": "frame_10744.png"}, {"time": 5451.962, "image_uri": "frame_10745.png"}, {"time": 5452.47, "image_uri": "frame_10746.png"}, {"time": 5452.977, "image_uri": "frame_10747.png"}, {"time": 5453.484, "image_uri": "frame_10748.png"}, {"time": 5453.992, "image_uri": "frame_10749.png"}, {"time": 5454.499, "image_uri": "frame_10750.png"}, {"time": 5455.007, "image_uri": "frame_10751.png"}, {"time": 5455.514, "image_uri": "frame_10752.png"}, {"time": 5456.022, "image_uri": "frame_10753.png"}, {"time": 5456.529, "image_uri": "frame_10754.png"}, {"time": 5457.037, "image_uri": "frame_10755.png"}, {"time": 5457.544, "image_uri": "frame_10756.png"}, {"time": 5458.051, "image_uri": "frame_10757.png"}, {"time": 5458.559, "image_uri": "frame_10758.png"}, {"time": 5459.066, "image_uri": "frame_10759.png"}, {"time": 5459.574, "image_uri": "frame_10760.png"}, {"time": 5460.081, "image_uri": "frame_10761.png"}, {"time": 5460.589, "image_uri": "frame_10762.png"}, {"time": 5461.096, "image_uri": "frame_10763.png"}, {"time": 5461.604, "image_uri": "frame_10764.png"}, {"time": 5462.111, "image_uri": "frame_10765.png"}, {"time": 5462.618, "image_uri": "frame_10766.png"}, {"time": 5463.126, "image_uri": "frame_10767.png"}, {"time": 5463.633, "image_uri": "frame_10768.png"}, {"time": 5464.141, "image_uri": "frame_10769.png"}, {"time": 5464.648, "image_uri": "frame_10770.png"}, {"time": 5465.156, "image_uri": "frame_10771.png"}, {"time": 5465.663, "image_uri": "frame_10772.png"}, {"time": 5466.171, "image_uri": "frame_10773.png"}, {"time": 5466.678, "image_uri": "frame_10774.png"}, {"time": 5467.185, "image_uri": "frame_10775.png"}, {"time": 5467.693, "image_uri": "frame_10776.png"}, {"time": 5468.2, "image_uri": "frame_10777.png"}, {"time": 5468.708, "image_uri": "frame_10778.png"}, {"time": 5469.215, "image_uri": "frame_10779.png"}, {"time": 5469.723, "image_uri": "frame_10780.png"}, {"time": 5470.23, "image_uri": "frame_10781.png"}, {"time": 5470.737, "image_uri": "frame_10782.png"}, {"time": 5471.245, "image_uri": "frame_10783.png"}, {"time": 5471.752, "image_uri": "frame_10784.png"}, {"time": 5472.26, "image_uri": "frame_10785.png"}, {"time": 5472.767, "image_uri": "frame_10786.png"}, {"time": 5473.275, "image_uri": "frame_10787.png"}, {"time": 5473.782, "image_uri": "frame_10788.png"}, {"time": 5474.29, "image_uri": "frame_10789.png"}, {"time": 5474.797, "image_uri": "frame_10790.png"}, {"time": 5475.304, "image_uri": "frame_10791.png"}, {"time": 5475.812, "image_uri": "frame_10792.png"}, {"time": 5476.319, "image_uri": "frame_10793.png"}, {"time": 5476.827, "image_uri": "frame_10794.png"}, {"time": 5477.334, "image_uri": "frame_10795.png"}, {"time": 5477.842, "image_uri": "frame_10796.png"}, {"time": 5478.349, "image_uri": "frame_10797.png"}, {"time": 5478.857, "image_uri": "frame_10798.png"}, {"time": 5479.364, "image_uri": "frame_10799.png"}, {"time": 5479.871, "image_uri": "frame_10800.png"}, {"time": 5480.379, "image_uri": "frame_10801.png"}, {"time": 5480.886, "image_uri": "frame_10802.png"}, {"time": 5481.394, "image_uri": "frame_10803.png"}, {"time": 5481.901, "image_uri": "frame_10804.png"}, {"time": 5482.409, "image_uri": "frame_10805.png"}, {"time": 5482.916, "image_uri": "frame_10806.png"}, {"time": 5483.424, "image_uri": "frame_10807.png"}, {"time": 5483.931, "image_uri": "frame_10808.png"}, {"time": 5484.438, "image_uri": "frame_10809.png"}, {"time": 5484.946, "image_uri": "frame_10810.png"}, {"time": 5485.453, "image_uri": "frame_10811.png"}, {"time": 5485.961, "image_uri": "frame_10812.png"}, {"time": 5486.468, "image_uri": "frame_10813.png"}, {"time": 5486.976, "image_uri": "frame_10814.png"}, {"time": 5487.483, "image_uri": "frame_10815.png"}, {"time": 5487.991, "image_uri": "frame_10816.png"}, {"time": 5488.498, "image_uri": "frame_10817.png"}, {"time": 5489.005, "image_uri": "frame_10818.png"}, {"time": 5489.513, "image_uri": "frame_10819.png"}, {"time": 5490.02, "image_uri": "frame_10820.png"}, {"time": 5490.528, "image_uri": "frame_10821.png"}, {"time": 5491.035, "image_uri": "frame_10822.png"}, {"time": 5491.543, "image_uri": "frame_10823.png"}, {"time": 5492.05, "image_uri": "frame_10824.png"}, {"time": 5492.558, "image_uri": "frame_10825.png"}, {"time": 5493.065, "image_uri": "frame_10826.png"}, {"time": 5493.572, "image_uri": "frame_10827.png"}, {"time": 5494.08, "image_uri": "frame_10828.png"}, {"time": 5494.587, "image_uri": "frame_10829.png"}, {"time": 5495.095, "image_uri": "frame_10830.png"}, {"time": 5495.602, "image_uri": "frame_10831.png"}, {"time": 5496.11, "image_uri": "frame_10832.png"}, {"time": 5496.617, "image_uri": "frame_10833.png"}, {"time": 5497.124, "image_uri": "frame_10834.png"}, {"time": 5497.632, "image_uri": "frame_10835.png"}, {"time": 5498.139, "image_uri": "frame_10836.png"}, {"time": 5498.647, "image_uri": "frame_10837.png"}, {"time": 5499.154, "image_uri": "frame_10838.png"}, {"time": 5499.662, "image_uri": "frame_10839.png"}, {"time": 5500.169, "image_uri": "frame_10840.png"}, {"time": 5500.677, "image_uri": "frame_10841.png"}, {"time": 5501.184, "image_uri": "frame_10842.png"}, {"time": 5501.691, "image_uri": "frame_10843.png"}, {"time": 5502.199, "image_uri": "frame_10844.png"}, {"time": 5502.706, "image_uri": "frame_10845.png"}, {"time": 5503.214, "image_uri": "frame_10846.png"}, {"time": 5503.721, "image_uri": "frame_10847.png"}, {"time": 5504.229, "image_uri": "frame_10848.png"}, {"time": 5504.736, "image_uri": "frame_10849.png"}, {"time": 5505.244, "image_uri": "frame_10850.png"}, {"time": 5505.751, "image_uri": "frame_10851.png"}, {"time": 5506.258, "image_uri": "frame_10852.png"}, {"time": 5506.766, "image_uri": "frame_10853.png"}, {"time": 5507.273, "image_uri": "frame_10854.png"}, {"time": 5507.781, "image_uri": "frame_10855.png"}, {"time": 5508.288, "image_uri": "frame_10856.png"}, {"time": 5508.796, "image_uri": "frame_10857.png"}, {"time": 5509.303, "image_uri": "frame_10858.png"}, {"time": 5509.811, "image_uri": "frame_10859.png"}, {"time": 5510.318, "image_uri": "frame_10860.png"}, {"time": 5510.825, "image_uri": "frame_10861.png"}, {"time": 5511.333, "image_uri": "frame_10862.png"}, {"time": 5511.84, "image_uri": "frame_10863.png"}, {"time": 5512.348, "image_uri": "frame_10864.png"}, {"time": 5512.855, "image_uri": "frame_10865.png"}, {"time": 5513.363, "image_uri": "frame_10866.png"}, {"time": 5513.87, "image_uri": "frame_10867.png"}, {"time": 5514.378, "image_uri": "frame_10868.png"}, {"time": 5514.885, "image_uri": "frame_10869.png"}, {"time": 5515.392, "image_uri": "frame_10870.png"}, {"time": 5515.9, "image_uri": "frame_10871.png"}, {"time": 5516.407, "image_uri": "frame_10872.png"}, {"time": 5516.915, "image_uri": "frame_10873.png"}, {"time": 5517.422, "image_uri": "frame_10874.png"}, {"time": 5517.93, "image_uri": "frame_10875.png"}, {"time": 5518.437, "image_uri": "frame_10876.png"}, {"time": 5518.945, "image_uri": "frame_10877.png"}, {"time": 5519.452, "image_uri": "frame_10878.png"}, {"time": 5519.959, "image_uri": "frame_10879.png"}, {"time": 5520.467, "image_uri": "frame_10880.png"}, {"time": 5520.974, "image_uri": "frame_10881.png"}, {"time": 5521.482, "image_uri": "frame_10882.png"}, {"time": 5521.989, "image_uri": "frame_10883.png"}, {"time": 5522.497, "image_uri": "frame_10884.png"}, {"time": 5523.004, "image_uri": "frame_10885.png"}, {"time": 5523.512, "image_uri": "frame_10886.png"}, {"time": 5524.019, "image_uri": "frame_10887.png"}, {"time": 5524.526, "image_uri": "frame_10888.png"}, {"time": 5525.034, "image_uri": "frame_10889.png"}, {"time": 5525.541, "image_uri": "frame_10890.png"}, {"time": 5526.049, "image_uri": "frame_10891.png"}, {"time": 5526.556, "image_uri": "frame_10892.png"}, {"time": 5527.064, "image_uri": "frame_10893.png"}, {"time": 5527.571, "image_uri": "frame_10894.png"}, {"time": 5528.078, "image_uri": "frame_10895.png"}, {"time": 5528.586, "image_uri": "frame_10896.png"}, {"time": 5529.093, "image_uri": "frame_10897.png"}, {"time": 5529.601, "image_uri": "frame_10898.png"}, {"time": 5530.108, "image_uri": "frame_10899.png"}, {"time": 5530.616, "image_uri": "frame_10900.png"}, {"time": 5531.123, "image_uri": "frame_10901.png"}, {"time": 5531.631, "image_uri": "frame_10902.png"}, {"time": 5532.138, "image_uri": "frame_10903.png"}, {"time": 5532.645, "image_uri": "frame_10904.png"}, {"time": 5533.153, "image_uri": "frame_10905.png"}, {"time": 5533.66, "image_uri": "frame_10906.png"}, {"time": 5534.168, "image_uri": "frame_10907.png"}, {"time": 5534.675, "image_uri": "frame_10908.png"}, {"time": 5535.183, "image_uri": "frame_10909.png"}, {"time": 5535.69, "image_uri": "frame_10910.png"}, {"time": 5536.198, "image_uri": "frame_10911.png"}, {"time": 5536.705, "image_uri": "frame_10912.png"}, {"time": 5537.212, "image_uri": "frame_10913.png"}, {"time": 5537.72, "image_uri": "frame_10914.png"}, {"time": 5538.227, "image_uri": "frame_10915.png"}, {"time": 5538.735, "image_uri": "frame_10916.png"}, {"time": 5539.242, "image_uri": "frame_10917.png"}, {"time": 5539.75, "image_uri": "frame_10918.png"}, {"time": 5540.257, "image_uri": "frame_10919.png"}, {"time": 5540.765, "image_uri": "frame_10920.png"}, {"time": 5541.272, "image_uri": "frame_10921.png"}, {"time": 5541.779, "image_uri": "frame_10922.png"}, {"time": 5542.287, "image_uri": "frame_10923.png"}, {"time": 5542.794, "image_uri": "frame_10924.png"}, {"time": 5543.302, "image_uri": "frame_10925.png"}, {"time": 5543.809, "image_uri": "frame_10926.png"}, {"time": 5544.317, "image_uri": "frame_10927.png"}, {"time": 5544.824, "image_uri": "frame_10928.png"}, {"time": 5545.332, "image_uri": "frame_10929.png"}, {"time": 5545.839, "image_uri": "frame_10930.png"}, {"time": 5546.346, "image_uri": "frame_10931.png"}, {"time": 5546.854, "image_uri": "frame_10932.png"}, {"time": 5547.361, "image_uri": "frame_10933.png"}, {"time": 5547.869, "image_uri": "frame_10934.png"}, {"time": 5548.376, "image_uri": "frame_10935.png"}, {"time": 5548.884, "image_uri": "frame_10936.png"}, {"time": 5549.391, "image_uri": "frame_10937.png"}, {"time": 5549.899, "image_uri": "frame_10938.png"}, {"time": 5550.406, "image_uri": "frame_10939.png"}, {"time": 5550.913, "image_uri": "frame_10940.png"}, {"time": 5551.421, "image_uri": "frame_10941.png"}, {"time": 5551.928, "image_uri": "frame_10942.png"}, {"time": 5552.436, "image_uri": "frame_10943.png"}, {"time": 5552.943, "image_uri": "frame_10944.png"}, {"time": 5553.451, "image_uri": "frame_10945.png"}, {"time": 5553.958, "image_uri": "frame_10946.png"}, {"time": 5554.465, "image_uri": "frame_10947.png"}, {"time": 5554.973, "image_uri": "frame_10948.png"}, {"time": 5555.48, "image_uri": "frame_10949.png"}, {"time": 5555.988, "image_uri": "frame_10950.png"}, {"time": 5556.495, "image_uri": "frame_10951.png"}, {"time": 5557.003, "image_uri": "frame_10952.png"}, {"time": 5557.51, "image_uri": "frame_10953.png"}, {"time": 5558.018, "image_uri": "frame_10954.png"}, {"time": 5558.525, "image_uri": "frame_10955.png"}, {"time": 5559.032, "image_uri": "frame_10956.png"}, {"time": 5559.54, "image_uri": "frame_10957.png"}, {"time": 5560.047, "image_uri": "frame_10958.png"}, {"time": 5560.555, "image_uri": "frame_10959.png"}, {"time": 5561.062, "image_uri": "frame_10960.png"}, {"time": 5561.57, "image_uri": "frame_10961.png"}, {"time": 5562.077, "image_uri": "frame_10962.png"}, {"time": 5562.585, "image_uri": "frame_10963.png"}, {"time": 5563.092, "image_uri": "frame_10964.png"}, {"time": 5563.599, "image_uri": "frame_10965.png"}, {"time": 5564.107, "image_uri": "frame_10966.png"}, {"time": 5564.614, "image_uri": "frame_10967.png"}, {"time": 5565.122, "image_uri": "frame_10968.png"}, {"time": 5565.629, "image_uri": "frame_10969.png"}, {"time": 5566.137, "image_uri": "frame_10970.png"}, {"time": 5566.644, "image_uri": "frame_10971.png"}, {"time": 5567.152, "image_uri": "frame_10972.png"}, {"time": 5567.659, "image_uri": "frame_10973.png"}, {"time": 5568.166, "image_uri": "frame_10974.png"}, {"time": 5568.674, "image_uri": "frame_10975.png"}, {"time": 5569.181, "image_uri": "frame_10976.png"}, {"time": 5569.689, "image_uri": "frame_10977.png"}, {"time": 5570.196, "image_uri": "frame_10978.png"}, {"time": 5570.704, "image_uri": "frame_10979.png"}, {"time": 5571.211, "image_uri": "frame_10980.png"}, {"time": 5571.719, "image_uri": "frame_10981.png"}, {"time": 5572.226, "image_uri": "frame_10982.png"}, {"time": 5572.733, "image_uri": "frame_10983.png"}, {"time": 5573.241, "image_uri": "frame_10984.png"}, {"time": 5573.748, "image_uri": "frame_10985.png"}, {"time": 5574.256, "image_uri": "frame_10986.png"}, {"time": 5574.763, "image_uri": "frame_10987.png"}, {"time": 5575.271, "image_uri": "frame_10988.png"}, {"time": 5575.778, "image_uri": "frame_10989.png"}, {"time": 5576.286, "image_uri": "frame_10990.png"}, {"time": 5576.793, "image_uri": "frame_10991.png"}, {"time": 5577.3, "image_uri": "frame_10992.png"}, {"time": 5577.808, "image_uri": "frame_10993.png"}, {"time": 5578.315, "image_uri": "frame_10994.png"}, {"time": 5578.823, "image_uri": "frame_10995.png"}, {"time": 5579.33, "image_uri": "frame_10996.png"}, {"time": 5579.838, "image_uri": "frame_10997.png"}, {"time": 5580.345, "image_uri": "frame_10998.png"}, {"time": 5580.853, "image_uri": "frame_10999.png"}, {"time": 5581.36, "image_uri": "frame_11000.png"}, {"time": 5581.867, "image_uri": "frame_11001.png"}, {"time": 5582.375, "image_uri": "frame_11002.png"}, {"time": 5582.882, "image_uri": "frame_11003.png"}, {"time": 5583.39, "image_uri": "frame_11004.png"}, {"time": 5583.897, "image_uri": "frame_11005.png"}, {"time": 5584.405, "image_uri": "frame_11006.png"}, {"time": 5584.912, "image_uri": "frame_11007.png"}, {"time": 5585.419, "image_uri": "frame_11008.png"}, {"time": 5585.927, "image_uri": "frame_11009.png"}, {"time": 5586.434, "image_uri": "frame_11010.png"}, {"time": 5586.942, "image_uri": "frame_11011.png"}, {"time": 5587.449, "image_uri": "frame_11012.png"}, {"time": 5587.957, "image_uri": "frame_11013.png"}, {"time": 5588.464, "image_uri": "frame_11014.png"}, {"time": 5588.972, "image_uri": "frame_11015.png"}, {"time": 5589.479, "image_uri": "frame_11016.png"}, {"time": 5589.986, "image_uri": "frame_11017.png"}, {"time": 5590.494, "image_uri": "frame_11018.png"}, {"time": 5591.001, "image_uri": "frame_11019.png"}, {"time": 5591.509, "image_uri": "frame_11020.png"}, {"time": 5592.016, "image_uri": "frame_11021.png"}, {"time": 5592.524, "image_uri": "frame_11022.png"}, {"time": 5593.031, "image_uri": "frame_11023.png"}, {"time": 5593.539, "image_uri": "frame_11024.png"}, {"time": 5594.046, "image_uri": "frame_11025.png"}, {"time": 5594.553, "image_uri": "frame_11026.png"}, {"time": 5595.061, "image_uri": "frame_11027.png"}, {"time": 5595.568, "image_uri": "frame_11028.png"}, {"time": 5596.076, "image_uri": "frame_11029.png"}, {"time": 5596.583, "image_uri": "frame_11030.png"}, {"time": 5597.091, "image_uri": "frame_11031.png"}, {"time": 5597.598, "image_uri": "frame_11032.png"}, {"time": 5598.106, "image_uri": "frame_11033.png"}, {"time": 5598.613, "image_uri": "frame_11034.png"}, {"time": 5599.12, "image_uri": "frame_11035.png"}, {"time": 5599.628, "image_uri": "frame_11036.png"}, {"time": 5600.135, "image_uri": "frame_11037.png"}, {"time": 5600.643, "image_uri": "frame_11038.png"}, {"time": 5601.15, "image_uri": "frame_11039.png"}, {"time": 5601.658, "image_uri": "frame_11040.png"}, {"time": 5602.165, "image_uri": "frame_11041.png"}, {"time": 5602.673, "image_uri": "frame_11042.png"}, {"time": 5603.18, "image_uri": "frame_11043.png"}, {"time": 5603.687, "image_uri": "frame_11044.png"}, {"time": 5604.195, "image_uri": "frame_11045.png"}, {"time": 5604.702, "image_uri": "frame_11046.png"}, {"time": 5605.21, "image_uri": "frame_11047.png"}, {"time": 5605.717, "image_uri": "frame_11048.png"}, {"time": 5606.225, "image_uri": "frame_11049.png"}, {"time": 5606.732, "image_uri": "frame_11050.png"}, {"time": 5607.24, "image_uri": "frame_11051.png"}, {"time": 5607.747, "image_uri": "frame_11052.png"}, {"time": 5608.254, "image_uri": "frame_11053.png"}, {"time": 5608.762, "image_uri": "frame_11054.png"}, {"time": 5609.269, "image_uri": "frame_11055.png"}, {"time": 5609.777, "image_uri": "frame_11056.png"}, {"time": 5610.284, "image_uri": "frame_11057.png"}, {"time": 5610.792, "image_uri": "frame_11058.png"}, {"time": 5611.299, "image_uri": "frame_11059.png"}, {"time": 5611.806, "image_uri": "frame_11060.png"}, {"time": 5612.314, "image_uri": "frame_11061.png"}, {"time": 5612.821, "image_uri": "frame_11062.png"}, {"time": 5613.329, "image_uri": "frame_11063.png"}, {"time": 5613.836, "image_uri": "frame_11064.png"}, {"time": 5614.344, "image_uri": "frame_11065.png"}, {"time": 5614.851, "image_uri": "frame_11066.png"}, {"time": 5615.359, "image_uri": "frame_11067.png"}, {"time": 5615.866, "image_uri": "frame_11068.png"}, {"time": 5616.373, "image_uri": "frame_11069.png"}, {"time": 5616.881, "image_uri": "frame_11070.png"}, {"time": 5617.388, "image_uri": "frame_11071.png"}, {"time": 5617.896, "image_uri": "frame_11072.png"}, {"time": 5618.403, "image_uri": "frame_11073.png"}, {"time": 5618.911, "image_uri": "frame_11074.png"}, {"time": 5619.418, "image_uri": "frame_11075.png"}, {"time": 5619.926, "image_uri": "frame_11076.png"}, {"time": 5620.433, "image_uri": "frame_11077.png"}, {"time": 5620.94, "image_uri": "frame_11078.png"}, {"time": 5621.448, "image_uri": "frame_11079.png"}, {"time": 5621.955, "image_uri": "frame_11080.png"}, {"time": 5622.463, "image_uri": "frame_11081.png"}, {"time": 5622.97, "image_uri": "frame_11082.png"}, {"time": 5623.478, "image_uri": "frame_11083.png"}, {"time": 5623.985, "image_uri": "frame_11084.png"}, {"time": 5624.493, "image_uri": "frame_11085.png"}, {"time": 5625.0, "image_uri": "frame_11086.png"}, {"time": 5625.507, "image_uri": "frame_11087.png"}, {"time": 5626.015, "image_uri": "frame_11088.png"}, {"time": 5626.522, "image_uri": "frame_11089.png"}, {"time": 5627.03, "image_uri": "frame_11090.png"}, {"time": 5627.537, "image_uri": "frame_11091.png"}, {"time": 5628.045, "image_uri": "frame_11092.png"}, {"time": 5628.552, "image_uri": "frame_11093.png"}, {"time": 5629.06, "image_uri": "frame_11094.png"}, {"time": 5629.567, "image_uri": "frame_11095.png"}, {"time": 5630.074, "image_uri": "frame_11096.png"}, {"time": 5630.582, "image_uri": "frame_11097.png"}, {"time": 5631.089, "image_uri": "frame_11098.png"}, {"time": 5631.597, "image_uri": "frame_11099.png"}, {"time": 5632.104, "image_uri": "frame_11100.png"}, {"time": 5632.612, "image_uri": "frame_11101.png"}, {"time": 5633.119, "image_uri": "frame_11102.png"}, {"time": 5633.627, "image_uri": "frame_11103.png"}, {"time": 5634.134, "image_uri": "frame_11104.png"}, {"time": 5634.641, "image_uri": "frame_11105.png"}, {"time": 5635.149, "image_uri": "frame_11106.png"}, {"time": 5635.656, "image_uri": "frame_11107.png"}, {"time": 5636.164, "image_uri": "frame_11108.png"}, {"time": 5636.671, "image_uri": "frame_11109.png"}, {"time": 5637.179, "image_uri": "frame_11110.png"}, {"time": 5637.686, "image_uri": "frame_11111.png"}, {"time": 5638.194, "image_uri": "frame_11112.png"}, {"time": 5638.701, "image_uri": "frame_11113.png"}, {"time": 5639.208, "image_uri": "frame_11114.png"}, {"time": 5639.716, "image_uri": "frame_11115.png"}, {"time": 5640.223, "image_uri": "frame_11116.png"}, {"time": 5640.731, "image_uri": "frame_11117.png"}, {"time": 5641.238, "image_uri": "frame_11118.png"}, {"time": 5641.746, "image_uri": "frame_11119.png"}, {"time": 5642.253, "image_uri": "frame_11120.png"}, {"time": 5642.76, "image_uri": "frame_11121.png"}, {"time": 5643.268, "image_uri": "frame_11122.png"}, {"time": 5643.775, "image_uri": "frame_11123.png"}, {"time": 5644.283, "image_uri": "frame_11124.png"}, {"time": 5644.79, "image_uri": "frame_11125.png"}, {"time": 5645.298, "image_uri": "frame_11126.png"}, {"time": 5645.805, "image_uri": "frame_11127.png"}, {"time": 5646.313, "image_uri": "frame_11128.png"}, {"time": 5646.82, "image_uri": "frame_11129.png"}, {"time": 5647.327, "image_uri": "frame_11130.png"}, {"time": 5647.835, "image_uri": "frame_11131.png"}, {"time": 5648.342, "image_uri": "frame_11132.png"}, {"time": 5648.85, "image_uri": "frame_11133.png"}, {"time": 5649.357, "image_uri": "frame_11134.png"}, {"time": 5649.865, "image_uri": "frame_11135.png"}, {"time": 5650.372, "image_uri": "frame_11136.png"}, {"time": 5650.88, "image_uri": "frame_11137.png"}, {"time": 5651.387, "image_uri": "frame_11138.png"}, {"time": 5651.894, "image_uri": "frame_11139.png"}, {"time": 5652.402, "image_uri": "frame_11140.png"}, {"time": 5652.909, "image_uri": "frame_11141.png"}, {"time": 5653.417, "image_uri": "frame_11142.png"}, {"time": 5653.924, "image_uri": "frame_11143.png"}, {"time": 5654.432, "image_uri": "frame_11144.png"}, {"time": 5654.939, "image_uri": "frame_11145.png"}, {"time": 5655.447, "image_uri": "frame_11146.png"}, {"time": 5655.954, "image_uri": "frame_11147.png"}, {"time": 5656.461, "image_uri": "frame_11148.png"}, {"time": 5656.969, "image_uri": "frame_11149.png"}, {"time": 5657.476, "image_uri": "frame_11150.png"}, {"time": 5657.984, "image_uri": "frame_11151.png"}, {"time": 5658.491, "image_uri": "frame_11152.png"}, {"time": 5658.999, "image_uri": "frame_11153.png"}, {"time": 5659.506, "image_uri": "frame_11154.png"}, {"time": 5660.014, "image_uri": "frame_11155.png"}, {"time": 5660.521, "image_uri": "frame_11156.png"}, {"time": 5661.028, "image_uri": "frame_11157.png"}, {"time": 5661.536, "image_uri": "frame_11158.png"}, {"time": 5662.043, "image_uri": "frame_11159.png"}, {"time": 5662.551, "image_uri": "frame_11160.png"}, {"time": 5663.058, "image_uri": "frame_11161.png"}, {"time": 5663.566, "image_uri": "frame_11162.png"}, {"time": 5664.073, "image_uri": "frame_11163.png"}, {"time": 5664.581, "image_uri": "frame_11164.png"}, {"time": 5665.088, "image_uri": "frame_11165.png"}, {"time": 5665.595, "image_uri": "frame_11166.png"}, {"time": 5666.103, "image_uri": "frame_11167.png"}, {"time": 5666.61, "image_uri": "frame_11168.png"}, {"time": 5667.118, "image_uri": "frame_11169.png"}, {"time": 5667.625, "image_uri": "frame_11170.png"}, {"time": 5668.133, "image_uri": "frame_11171.png"}, {"time": 5668.64, "image_uri": "frame_11172.png"}, {"time": 5669.147, "image_uri": "frame_11173.png"}, {"time": 5669.655, "image_uri": "frame_11174.png"}, {"time": 5670.162, "image_uri": "frame_11175.png"}, {"time": 5670.67, "image_uri": "frame_11176.png"}, {"time": 5671.177, "image_uri": "frame_11177.png"}, {"time": 5671.685, "image_uri": "frame_11178.png"}, {"time": 5672.192, "image_uri": "frame_11179.png"}, {"time": 5672.7, "image_uri": "frame_11180.png"}, {"time": 5673.207, "image_uri": "frame_11181.png"}, {"time": 5673.714, "image_uri": "frame_11182.png"}, {"time": 5674.222, "image_uri": "frame_11183.png"}, {"time": 5674.729, "image_uri": "frame_11184.png"}, {"time": 5675.237, "image_uri": "frame_11185.png"}, {"time": 5675.744, "image_uri": "frame_11186.png"}, {"time": 5676.252, "image_uri": "frame_11187.png"}, {"time": 5676.759, "image_uri": "frame_11188.png"}, {"time": 5677.267, "image_uri": "frame_11189.png"}, {"time": 5677.774, "image_uri": "frame_11190.png"}, {"time": 5678.281, "image_uri": "frame_11191.png"}, {"time": 5678.789, "image_uri": "frame_11192.png"}, {"time": 5679.296, "image_uri": "frame_11193.png"}, {"time": 5679.804, "image_uri": "frame_11194.png"}, {"time": 5680.311, "image_uri": "frame_11195.png"}, {"time": 5680.819, "image_uri": "frame_11196.png"}, {"time": 5681.326, "image_uri": "frame_11197.png"}, {"time": 5681.834, "image_uri": "frame_11198.png"}, {"time": 5682.341, "image_uri": "frame_11199.png"}, {"time": 5682.848, "image_uri": "frame_11200.png"}, {"time": 5683.356, "image_uri": "frame_11201.png"}, {"time": 5683.863, "image_uri": "frame_11202.png"}, {"time": 5684.371, "image_uri": "frame_11203.png"}, {"time": 5684.878, "image_uri": "frame_11204.png"}, {"time": 5685.386, "image_uri": "frame_11205.png"}, {"time": 5685.893, "image_uri": "frame_11206.png"}, {"time": 5686.401, "image_uri": "frame_11207.png"}, {"time": 5686.908, "image_uri": "frame_11208.png"}, {"time": 5687.415, "image_uri": "frame_11209.png"}, {"time": 5687.923, "image_uri": "frame_11210.png"}, {"time": 5688.43, "image_uri": "frame_11211.png"}, {"time": 5688.938, "image_uri": "frame_11212.png"}, {"time": 5689.445, "image_uri": "frame_11213.png"}, {"time": 5689.953, "image_uri": "frame_11214.png"}, {"time": 5690.46, "image_uri": "frame_11215.png"}, {"time": 5690.968, "image_uri": "frame_11216.png"}, {"time": 5691.475, "image_uri": "frame_11217.png"}, {"time": 5691.982, "image_uri": "frame_11218.png"}, {"time": 5692.49, "image_uri": "frame_11219.png"}, {"time": 5692.997, "image_uri": "frame_11220.png"}, {"time": 5693.505, "image_uri": "frame_11221.png"}, {"time": 5694.012, "image_uri": "frame_11222.png"}, {"time": 5694.52, "image_uri": "frame_11223.png"}, {"time": 5695.027, "image_uri": "frame_11224.png"}, {"time": 5695.535, "image_uri": "frame_11225.png"}, {"time": 5696.042, "image_uri": "frame_11226.png"}, {"time": 5696.549, "image_uri": "frame_11227.png"}, {"time": 5697.057, "image_uri": "frame_11228.png"}, {"time": 5697.564, "image_uri": "frame_11229.png"}, {"time": 5698.072, "image_uri": "frame_11230.png"}, {"time": 5698.579, "image_uri": "frame_11231.png"}, {"time": 5699.087, "image_uri": "frame_11232.png"}, {"time": 5699.594, "image_uri": "frame_11233.png"}, {"time": 5700.101, "image_uri": "frame_11234.png"}, {"time": 5700.609, "image_uri": "frame_11235.png"}, {"time": 5701.116, "image_uri": "frame_11236.png"}, {"time": 5701.624, "image_uri": "frame_11237.png"}, {"time": 5702.131, "image_uri": "frame_11238.png"}, {"time": 5702.639, "image_uri": "frame_11239.png"}, {"time": 5703.146, "image_uri": "frame_11240.png"}, {"time": 5703.654, "image_uri": "frame_11241.png"}, {"time": 5704.161, "image_uri": "frame_11242.png"}, {"time": 5704.668, "image_uri": "frame_11243.png"}, {"time": 5705.176, "image_uri": "frame_11244.png"}, {"time": 5705.683, "image_uri": "frame_11245.png"}, {"time": 5706.191, "image_uri": "frame_11246.png"}, {"time": 5706.698, "image_uri": "frame_11247.png"}, {"time": 5707.206, "image_uri": "frame_11248.png"}, {"time": 5707.713, "image_uri": "frame_11249.png"}, {"time": 5708.221, "image_uri": "frame_11250.png"}, {"time": 5708.728, "image_uri": "frame_11251.png"}, {"time": 5709.235, "image_uri": "frame_11252.png"}, {"time": 5709.743, "image_uri": "frame_11253.png"}, {"time": 5710.25, "image_uri": "frame_11254.png"}, {"time": 5710.758, "image_uri": "frame_11255.png"}, {"time": 5711.265, "image_uri": "frame_11256.png"}, {"time": 5711.773, "image_uri": "frame_11257.png"}, {"time": 5712.28, "image_uri": "frame_11258.png"}, {"time": 5712.788, "image_uri": "frame_11259.png"}, {"time": 5713.295, "image_uri": "frame_11260.png"}, {"time": 5713.802, "image_uri": "frame_11261.png"}, {"time": 5714.31, "image_uri": "frame_11262.png"}, {"time": 5714.817, "image_uri": "frame_11263.png"}, {"time": 5715.325, "image_uri": "frame_11264.png"}, {"time": 5715.832, "image_uri": "frame_11265.png"}, {"time": 5716.34, "image_uri": "frame_11266.png"}, {"time": 5716.847, "image_uri": "frame_11267.png"}, {"time": 5717.355, "image_uri": "frame_11268.png"}, {"time": 5717.862, "image_uri": "frame_11269.png"}, {"time": 5718.369, "image_uri": "frame_11270.png"}, {"time": 5718.877, "image_uri": "frame_11271.png"}, {"time": 5719.384, "image_uri": "frame_11272.png"}, {"time": 5719.892, "image_uri": "frame_11273.png"}, {"time": 5720.399, "image_uri": "frame_11274.png"}, {"time": 5720.907, "image_uri": "frame_11275.png"}, {"time": 5721.414, "image_uri": "frame_11276.png"}, {"time": 5721.922, "image_uri": "frame_11277.png"}, {"time": 5722.429, "image_uri": "frame_11278.png"}, {"time": 5722.936, "image_uri": "frame_11279.png"}, {"time": 5723.444, "image_uri": "frame_11280.png"}, {"time": 5723.951, "image_uri": "frame_11281.png"}, {"time": 5724.459, "image_uri": "frame_11282.png"}, {"time": 5724.966, "image_uri": "frame_11283.png"}, {"time": 5725.474, "image_uri": "frame_11284.png"}, {"time": 5725.981, "image_uri": "frame_11285.png"}, {"time": 5726.488, "image_uri": "frame_11286.png"}, {"time": 5726.996, "image_uri": "frame_11287.png"}, {"time": 5727.503, "image_uri": "frame_11288.png"}, {"time": 5728.011, "image_uri": "frame_11289.png"}, {"time": 5728.518, "image_uri": "frame_11290.png"}, {"time": 5729.026, "image_uri": "frame_11291.png"}, {"time": 5729.533, "image_uri": "frame_11292.png"}, {"time": 5730.041, "image_uri": "frame_11293.png"}, {"time": 5730.548, "image_uri": "frame_11294.png"}, {"time": 5731.055, "image_uri": "frame_11295.png"}, {"time": 5731.563, "image_uri": "frame_11296.png"}, {"time": 5732.07, "image_uri": "frame_11297.png"}, {"time": 5732.578, "image_uri": "frame_11298.png"}, {"time": 5733.085, "image_uri": "frame_11299.png"}, {"time": 5733.593, "image_uri": "frame_11300.png"}, {"time": 5734.1, "image_uri": "frame_11301.png"}, {"time": 5734.608, "image_uri": "frame_11302.png"}, {"time": 5735.115, "image_uri": "frame_11303.png"}, {"time": 5735.622, "image_uri": "frame_11304.png"}, {"time": 5736.13, "image_uri": "frame_11305.png"}, {"time": 5736.637, "image_uri": "frame_11306.png"}, {"time": 5737.145, "image_uri": "frame_11307.png"}, {"time": 5737.652, "image_uri": "frame_11308.png"}, {"time": 5738.16, "image_uri": "frame_11309.png"}, {"time": 5738.667, "image_uri": "frame_11310.png"}, {"time": 5739.175, "image_uri": "frame_11311.png"}, {"time": 5739.682, "image_uri": "frame_11312.png"}, {"time": 5740.189, "image_uri": "frame_11313.png"}, {"time": 5740.697, "image_uri": "frame_11314.png"}, {"time": 5741.204, "image_uri": "frame_11315.png"}, {"time": 5741.712, "image_uri": "frame_11316.png"}, {"time": 5742.219, "image_uri": "frame_11317.png"}, {"time": 5742.727, "image_uri": "frame_11318.png"}, {"time": 5743.234, "image_uri": "frame_11319.png"}, {"time": 5743.742, "image_uri": "frame_11320.png"}, {"time": 5744.249, "image_uri": "frame_11321.png"}, {"time": 5744.756, "image_uri": "frame_11322.png"}, {"time": 5745.264, "image_uri": "frame_11323.png"}, {"time": 5745.771, "image_uri": "frame_11324.png"}, {"time": 5746.279, "image_uri": "frame_11325.png"}, {"time": 5746.786, "image_uri": "frame_11326.png"}, {"time": 5747.294, "image_uri": "frame_11327.png"}, {"time": 5747.801, "image_uri": "frame_11328.png"}, {"time": 5748.309, "image_uri": "frame_11329.png"}, {"time": 5748.816, "image_uri": "frame_11330.png"}, {"time": 5749.323, "image_uri": "frame_11331.png"}, {"time": 5749.831, "image_uri": "frame_11332.png"}, {"time": 5750.338, "image_uri": "frame_11333.png"}, {"time": 5750.846, "image_uri": "frame_11334.png"}, {"time": 5751.353, "image_uri": "frame_11335.png"}, {"time": 5751.861, "image_uri": "frame_11336.png"}, {"time": 5752.368, "image_uri": "frame_11337.png"}, {"time": 5752.876, "image_uri": "frame_11338.png"}, {"time": 5753.383, "image_uri": "frame_11339.png"}, {"time": 5753.89, "image_uri": "frame_11340.png"}, {"time": 5754.398, "image_uri": "frame_11341.png"}, {"time": 5754.905, "image_uri": "frame_11342.png"}, {"time": 5755.413, "image_uri": "frame_11343.png"}, {"time": 5755.92, "image_uri": "frame_11344.png"}, {"time": 5756.428, "image_uri": "frame_11345.png"}, {"time": 5756.935, "image_uri": "frame_11346.png"}, {"time": 5757.442, "image_uri": "frame_11347.png"}, {"time": 5757.95, "image_uri": "frame_11348.png"}, {"time": 5758.457, "image_uri": "frame_11349.png"}, {"time": 5758.965, "image_uri": "frame_11350.png"}, {"time": 5759.472, "image_uri": "frame_11351.png"}, {"time": 5759.98, "image_uri": "frame_11352.png"}, {"time": 5760.487, "image_uri": "frame_11353.png"}, {"time": 5760.995, "image_uri": "frame_11354.png"}, {"time": 5761.502, "image_uri": "frame_11355.png"}, {"time": 5762.009, "image_uri": "frame_11356.png"}, {"time": 5762.517, "image_uri": "frame_11357.png"}, {"time": 5763.024, "image_uri": "frame_11358.png"}, {"time": 5763.532, "image_uri": "frame_11359.png"}, {"time": 5764.039, "image_uri": "frame_11360.png"}, {"time": 5764.547, "image_uri": "frame_11361.png"}, {"time": 5765.054, "image_uri": "frame_11362.png"}, {"time": 5765.562, "image_uri": "frame_11363.png"}, {"time": 5766.069, "image_uri": "frame_11364.png"}, {"time": 5766.576, "image_uri": "frame_11365.png"}, {"time": 5767.084, "image_uri": "frame_11366.png"}, {"time": 5767.591, "image_uri": "frame_11367.png"}, {"time": 5768.099, "image_uri": "frame_11368.png"}, {"time": 5768.606, "image_uri": "frame_11369.png"}, {"time": 5769.114, "image_uri": "frame_11370.png"}, {"time": 5769.621, "image_uri": "frame_11371.png"}, {"time": 5770.129, "image_uri": "frame_11372.png"}, {"time": 5770.636, "image_uri": "frame_11373.png"}, {"time": 5771.143, "image_uri": "frame_11374.png"}, {"time": 5771.651, "image_uri": "frame_11375.png"}, {"time": 5772.158, "image_uri": "frame_11376.png"}, {"time": 5772.666, "image_uri": "frame_11377.png"}, {"time": 5773.173, "image_uri": "frame_11378.png"}, {"time": 5773.681, "image_uri": "frame_11379.png"}, {"time": 5774.188, "image_uri": "frame_11380.png"}, {"time": 5774.696, "image_uri": "frame_11381.png"}, {"time": 5775.203, "image_uri": "frame_11382.png"}, {"time": 5775.71, "image_uri": "frame_11383.png"}, {"time": 5776.218, "image_uri": "frame_11384.png"}, {"time": 5776.725, "image_uri": "frame_11385.png"}, {"time": 5777.233, "image_uri": "frame_11386.png"}, {"time": 5777.74, "image_uri": "frame_11387.png"}, {"time": 5778.248, "image_uri": "frame_11388.png"}, {"time": 5778.755, "image_uri": "frame_11389.png"}, {"time": 5779.263, "image_uri": "frame_11390.png"}, {"time": 5779.77, "image_uri": "frame_11391.png"}, {"time": 5780.277, "image_uri": "frame_11392.png"}, {"time": 5780.785, "image_uri": "frame_11393.png"}, {"time": 5781.292, "image_uri": "frame_11394.png"}, {"time": 5781.8, "image_uri": "frame_11395.png"}, {"time": 5782.307, "image_uri": "frame_11396.png"}, {"time": 5782.815, "image_uri": "frame_11397.png"}, {"time": 5783.322, "image_uri": "frame_11398.png"}, {"time": 5783.829, "image_uri": "frame_11399.png"}, {"time": 5784.337, "image_uri": "frame_11400.png"}, {"time": 5784.844, "image_uri": "frame_11401.png"}, {"time": 5785.352, "image_uri": "frame_11402.png"}, {"time": 5785.859, "image_uri": "frame_11403.png"}, {"time": 5786.367, "image_uri": "frame_11404.png"}, {"time": 5786.874, "image_uri": "frame_11405.png"}, {"time": 5787.382, "image_uri": "frame_11406.png"}, {"time": 5787.889, "image_uri": "frame_11407.png"}, {"time": 5788.396, "image_uri": "frame_11408.png"}, {"time": 5788.904, "image_uri": "frame_11409.png"}, {"time": 5789.411, "image_uri": "frame_11410.png"}, {"time": 5789.919, "image_uri": "frame_11411.png"}, {"time": 5790.426, "image_uri": "frame_11412.png"}, {"time": 5790.934, "image_uri": "frame_11413.png"}, {"time": 5791.441, "image_uri": "frame_11414.png"}, {"time": 5791.949, "image_uri": "frame_11415.png"}, {"time": 5792.456, "image_uri": "frame_11416.png"}, {"time": 5792.963, "image_uri": "frame_11417.png"}, {"time": 5793.471, "image_uri": "frame_11418.png"}, {"time": 5793.978, "image_uri": "frame_11419.png"}, {"time": 5794.486, "image_uri": "frame_11420.png"}, {"time": 5794.993, "image_uri": "frame_11421.png"}, {"time": 5795.501, "image_uri": "frame_11422.png"}, {"time": 5796.008, "image_uri": "frame_11423.png"}, {"time": 5796.516, "image_uri": "frame_11424.png"}, {"time": 5797.023, "image_uri": "frame_11425.png"}, {"time": 5797.53, "image_uri": "frame_11426.png"}, {"time": 5798.038, "image_uri": "frame_11427.png"}, {"time": 5798.545, "image_uri": "frame_11428.png"}, {"time": 5799.053, "image_uri": "frame_11429.png"}, {"time": 5799.56, "image_uri": "frame_11430.png"}, {"time": 5800.068, "image_uri": "frame_11431.png"}, {"time": 5800.575, "image_uri": "frame_11432.png"}, {"time": 5801.083, "image_uri": "frame_11433.png"}, {"time": 5801.59, "image_uri": "frame_11434.png"}, {"time": 5802.097, "image_uri": "frame_11435.png"}, {"time": 5802.605, "image_uri": "frame_11436.png"}, {"time": 5803.112, "image_uri": "frame_11437.png"}, {"time": 5803.62, "image_uri": "frame_11438.png"}, {"time": 5804.127, "image_uri": "frame_11439.png"}, {"time": 5804.635, "image_uri": "frame_11440.png"}, {"time": 5805.142, "image_uri": "frame_11441.png"}, {"time": 5805.65, "image_uri": "frame_11442.png"}, {"time": 5806.157, "image_uri": "frame_11443.png"}, {"time": 5806.664, "image_uri": "frame_11444.png"}, {"time": 5807.172, "image_uri": "frame_11445.png"}, {"time": 5807.679, "image_uri": "frame_11446.png"}, {"time": 5808.187, "image_uri": "frame_11447.png"}, {"time": 5808.694, "image_uri": "frame_11448.png"}, {"time": 5809.202, "image_uri": "frame_11449.png"}, {"time": 5809.709, "image_uri": "frame_11450.png"}, {"time": 5810.217, "image_uri": "frame_11451.png"}, {"time": 5810.724, "image_uri": "frame_11452.png"}, {"time": 5811.231, "image_uri": "frame_11453.png"}, {"time": 5811.739, "image_uri": "frame_11454.png"}, {"time": 5812.246, "image_uri": "frame_11455.png"}, {"time": 5812.754, "image_uri": "frame_11456.png"}, {"time": 5813.261, "image_uri": "frame_11457.png"}, {"time": 5813.769, "image_uri": "frame_11458.png"}, {"time": 5814.276, "image_uri": "frame_11459.png"}, {"time": 5814.783, "image_uri": "frame_11460.png"}, {"time": 5815.291, "image_uri": "frame_11461.png"}, {"time": 5815.798, "image_uri": "frame_11462.png"}, {"time": 5816.306, "image_uri": "frame_11463.png"}, {"time": 5816.813, "image_uri": "frame_11464.png"}, {"time": 5817.321, "image_uri": "frame_11465.png"}, {"time": 5817.828, "image_uri": "frame_11466.png"}, {"time": 5818.336, "image_uri": "frame_11467.png"}, {"time": 5818.843, "image_uri": "frame_11468.png"}, {"time": 5819.35, "image_uri": "frame_11469.png"}, {"time": 5819.858, "image_uri": "frame_11470.png"}, {"time": 5820.365, "image_uri": "frame_11471.png"}, {"time": 5820.873, "image_uri": "frame_11472.png"}, {"time": 5821.38, "image_uri": "frame_11473.png"}, {"time": 5821.888, "image_uri": "frame_11474.png"}, {"time": 5822.395, "image_uri": "frame_11475.png"}, {"time": 5822.903, "image_uri": "frame_11476.png"}, {"time": 5823.41, "image_uri": "frame_11477.png"}, {"time": 5823.917, "image_uri": "frame_11478.png"}, {"time": 5824.425, "image_uri": "frame_11479.png"}, {"time": 5824.932, "image_uri": "frame_11480.png"}, {"time": 5825.44, "image_uri": "frame_11481.png"}, {"time": 5825.947, "image_uri": "frame_11482.png"}, {"time": 5826.455, "image_uri": "frame_11483.png"}, {"time": 5826.962, "image_uri": "frame_11484.png"}, {"time": 5827.47, "image_uri": "frame_11485.png"}, {"time": 5827.977, "image_uri": "frame_11486.png"}, {"time": 5828.484, "image_uri": "frame_11487.png"}, {"time": 5828.992, "image_uri": "frame_11488.png"}, {"time": 5829.499, "image_uri": "frame_11489.png"}, {"time": 5830.007, "image_uri": "frame_11490.png"}, {"time": 5830.514, "image_uri": "frame_11491.png"}, {"time": 5831.022, "image_uri": "frame_11492.png"}, {"time": 5831.529, "image_uri": "frame_11493.png"}, {"time": 5832.037, "image_uri": "frame_11494.png"}, {"time": 5832.544, "image_uri": "frame_11495.png"}, {"time": 5833.051, "image_uri": "frame_11496.png"}, {"time": 5833.559, "image_uri": "frame_11497.png"}, {"time": 5834.066, "image_uri": "frame_11498.png"}, {"time": 5834.574, "image_uri": "frame_11499.png"}, {"time": 5835.081, "image_uri": "frame_11500.png"}, {"time": 5835.589, "image_uri": "frame_11501.png"}, {"time": 5836.096, "image_uri": "frame_11502.png"}, {"time": 5836.604, "image_uri": "frame_11503.png"}, {"time": 5837.111, "image_uri": "frame_11504.png"}, {"time": 5837.618, "image_uri": "frame_11505.png"}, {"time": 5838.126, "image_uri": "frame_11506.png"}, {"time": 5838.633, "image_uri": "frame_11507.png"}, {"time": 5839.141, "image_uri": "frame_11508.png"}, {"time": 5839.648, "image_uri": "frame_11509.png"}, {"time": 5840.156, "image_uri": "frame_11510.png"}, {"time": 5840.663, "image_uri": "frame_11511.png"}, {"time": 5841.171, "image_uri": "frame_11512.png"}, {"time": 5841.678, "image_uri": "frame_11513.png"}, {"time": 5842.185, "image_uri": "frame_11514.png"}, {"time": 5842.693, "image_uri": "frame_11515.png"}, {"time": 5843.2, "image_uri": "frame_11516.png"}, {"time": 5843.708, "image_uri": "frame_11517.png"}, {"time": 5844.215, "image_uri": "frame_11518.png"}, {"time": 5844.723, "image_uri": "frame_11519.png"}, {"time": 5845.23, "image_uri": "frame_11520.png"}, {"time": 5845.737, "image_uri": "frame_11521.png"}, {"time": 5846.245, "image_uri": "frame_11522.png"}, {"time": 5846.752, "image_uri": "frame_11523.png"}, {"time": 5847.26, "image_uri": "frame_11524.png"}, {"time": 5847.767, "image_uri": "frame_11525.png"}, {"time": 5848.275, "image_uri": "frame_11526.png"}, {"time": 5848.782, "image_uri": "frame_11527.png"}, {"time": 5849.29, "image_uri": "frame_11528.png"}, {"time": 5849.797, "image_uri": "frame_11529.png"}, {"time": 5850.304, "image_uri": "frame_11530.png"}, {"time": 5850.812, "image_uri": "frame_11531.png"}, {"time": 5851.319, "image_uri": "frame_11532.png"}, {"time": 5851.827, "image_uri": "frame_11533.png"}, {"time": 5852.334, "image_uri": "frame_11534.png"}, {"time": 5852.842, "image_uri": "frame_11535.png"}, {"time": 5853.349, "image_uri": "frame_11536.png"}, {"time": 5853.857, "image_uri": "frame_11537.png"}, {"time": 5854.364, "image_uri": "frame_11538.png"}, {"time": 5854.871, "image_uri": "frame_11539.png"}, {"time": 5855.379, "image_uri": "frame_11540.png"}, {"time": 5855.886, "image_uri": "frame_11541.png"}, {"time": 5856.394, "image_uri": "frame_11542.png"}, {"time": 5856.901, "image_uri": "frame_11543.png"}, {"time": 5857.409, "image_uri": "frame_11544.png"}, {"time": 5857.916, "image_uri": "frame_11545.png"}, {"time": 5858.424, "image_uri": "frame_11546.png"}, {"time": 5858.931, "image_uri": "frame_11547.png"}, {"time": 5859.438, "image_uri": "frame_11548.png"}, {"time": 5859.946, "image_uri": "frame_11549.png"}, {"time": 5860.453, "image_uri": "frame_11550.png"}, {"time": 5860.961, "image_uri": "frame_11551.png"}, {"time": 5861.468, "image_uri": "frame_11552.png"}, {"time": 5861.976, "image_uri": "frame_11553.png"}, {"time": 5862.483, "image_uri": "frame_11554.png"}, {"time": 5862.991, "image_uri": "frame_11555.png"}, {"time": 5863.498, "image_uri": "frame_11556.png"}, {"time": 5864.005, "image_uri": "frame_11557.png"}, {"time": 5864.513, "image_uri": "frame_11558.png"}, {"time": 5865.02, "image_uri": "frame_11559.png"}, {"time": 5865.528, "image_uri": "frame_11560.png"}, {"time": 5866.035, "image_uri": "frame_11561.png"}, {"time": 5866.543, "image_uri": "frame_11562.png"}, {"time": 5867.05, "image_uri": "frame_11563.png"}, {"time": 5867.558, "image_uri": "frame_11564.png"}, {"time": 5868.065, "image_uri": "frame_11565.png"}, {"time": 5868.572, "image_uri": "frame_11566.png"}, {"time": 5869.08, "image_uri": "frame_11567.png"}, {"time": 5869.587, "image_uri": "frame_11568.png"}, {"time": 5870.095, "image_uri": "frame_11569.png"}, {"time": 5870.602, "image_uri": "frame_11570.png"}, {"time": 5871.11, "image_uri": "frame_11571.png"}, {"time": 5871.617, "image_uri": "frame_11572.png"}, {"time": 5872.124, "image_uri": "frame_11573.png"}, {"time": 5872.632, "image_uri": "frame_11574.png"}, {"time": 5873.139, "image_uri": "frame_11575.png"}, {"time": 5873.647, "image_uri": "frame_11576.png"}, {"time": 5874.154, "image_uri": "frame_11577.png"}, {"time": 5874.662, "image_uri": "frame_11578.png"}, {"time": 5875.169, "image_uri": "frame_11579.png"}, {"time": 5875.677, "image_uri": "frame_11580.png"}, {"time": 5876.184, "image_uri": "frame_11581.png"}, {"time": 5876.691, "image_uri": "frame_11582.png"}, {"time": 5877.199, "image_uri": "frame_11583.png"}, {"time": 5877.706, "image_uri": "frame_11584.png"}, {"time": 5878.214, "image_uri": "frame_11585.png"}, {"time": 5878.721, "image_uri": "frame_11586.png"}, {"time": 5879.229, "image_uri": "frame_11587.png"}, {"time": 5879.736, "image_uri": "frame_11588.png"}, {"time": 5880.244, "image_uri": "frame_11589.png"}, {"time": 5880.751, "image_uri": "frame_11590.png"}, {"time": 5881.258, "image_uri": "frame_11591.png"}, {"time": 5881.766, "image_uri": "frame_11592.png"}, {"time": 5882.273, "image_uri": "frame_11593.png"}, {"time": 5882.781, "image_uri": "frame_11594.png"}, {"time": 5883.288, "image_uri": "frame_11595.png"}, {"time": 5883.796, "image_uri": "frame_11596.png"}, {"time": 5884.303, "image_uri": "frame_11597.png"}, {"time": 5884.811, "image_uri": "frame_11598.png"}, {"time": 5885.318, "image_uri": "frame_11599.png"}, {"time": 5885.825, "image_uri": "frame_11600.png"}, {"time": 5886.333, "image_uri": "frame_11601.png"}, {"time": 5886.84, "image_uri": "frame_11602.png"}, {"time": 5887.348, "image_uri": "frame_11603.png"}, {"time": 5887.855, "image_uri": "frame_11604.png"}, {"time": 5888.363, "image_uri": "frame_11605.png"}, {"time": 5888.87, "image_uri": "frame_11606.png"}, {"time": 5889.378, "image_uri": "frame_11607.png"}, {"time": 5889.885, "image_uri": "frame_11608.png"}, {"time": 5890.392, "image_uri": "frame_11609.png"}, {"time": 5890.9, "image_uri": "frame_11610.png"}, {"time": 5891.407, "image_uri": "frame_11611.png"}, {"time": 5891.915, "image_uri": "frame_11612.png"}, {"time": 5892.422, "image_uri": "frame_11613.png"}, {"time": 5892.93, "image_uri": "frame_11614.png"}, {"time": 5893.437, "image_uri": "frame_11615.png"}, {"time": 5893.945, "image_uri": "frame_11616.png"}, {"time": 5894.452, "image_uri": "frame_11617.png"}, {"time": 5894.959, "image_uri": "frame_11618.png"}, {"time": 5895.467, "image_uri": "frame_11619.png"}, {"time": 5895.974, "image_uri": "frame_11620.png"}, {"time": 5896.482, "image_uri": "frame_11621.png"}, {"time": 5896.989, "image_uri": "frame_11622.png"}, {"time": 5897.497, "image_uri": "frame_11623.png"}, {"time": 5898.004, "image_uri": "frame_11624.png"}, {"time": 5898.512, "image_uri": "frame_11625.png"}, {"time": 5899.019, "image_uri": "frame_11626.png"}, {"time": 5899.526, "image_uri": "frame_11627.png"}, {"time": 5900.034, "image_uri": "frame_11628.png"}, {"time": 5900.541, "image_uri": "frame_11629.png"}, {"time": 5901.049, "image_uri": "frame_11630.png"}, {"time": 5901.556, "image_uri": "frame_11631.png"}, {"time": 5902.064, "image_uri": "frame_11632.png"}, {"time": 5902.571, "image_uri": "frame_11633.png"}, {"time": 5903.078, "image_uri": "frame_11634.png"}, {"time": 5903.586, "image_uri": "frame_11635.png"}, {"time": 5904.093, "image_uri": "frame_11636.png"}, {"time": 5904.601, "image_uri": "frame_11637.png"}, {"time": 5905.108, "image_uri": "frame_11638.png"}, {"time": 5905.616, "image_uri": "frame_11639.png"}, {"time": 5906.123, "image_uri": "frame_11640.png"}, {"time": 5906.631, "image_uri": "frame_11641.png"}, {"time": 5907.138, "image_uri": "frame_11642.png"}, {"time": 5907.645, "image_uri": "frame_11643.png"}, {"time": 5908.153, "image_uri": "frame_11644.png"}, {"time": 5908.66, "image_uri": "frame_11645.png"}, {"time": 5909.168, "image_uri": "frame_11646.png"}, {"time": 5909.675, "image_uri": "frame_11647.png"}, {"time": 5910.183, "image_uri": "frame_11648.png"}, {"time": 5910.69, "image_uri": "frame_11649.png"}, {"time": 5911.198, "image_uri": "frame_11650.png"}, {"time": 5911.705, "image_uri": "frame_11651.png"}, {"time": 5912.212, "image_uri": "frame_11652.png"}, {"time": 5912.72, "image_uri": "frame_11653.png"}, {"time": 5913.227, "image_uri": "frame_11654.png"}, {"time": 5913.735, "image_uri": "frame_11655.png"}, {"time": 5914.242, "image_uri": "frame_11656.png"}, {"time": 5914.75, "image_uri": "frame_11657.png"}, {"time": 5915.257, "image_uri": "frame_11658.png"}, {"time": 5915.765, "image_uri": "frame_11659.png"}, {"time": 5916.272, "image_uri": "frame_11660.png"}, {"time": 5916.779, "image_uri": "frame_11661.png"}, {"time": 5917.287, "image_uri": "frame_11662.png"}, {"time": 5917.794, "image_uri": "frame_11663.png"}, {"time": 5918.302, "image_uri": "frame_11664.png"}, {"time": 5918.809, "image_uri": "frame_11665.png"}, {"time": 5919.317, "image_uri": "frame_11666.png"}, {"time": 5919.824, "image_uri": "frame_11667.png"}, {"time": 5920.332, "image_uri": "frame_11668.png"}, {"time": 5920.839, "image_uri": "frame_11669.png"}, {"time": 5921.346, "image_uri": "frame_11670.png"}, {"time": 5921.854, "image_uri": "frame_11671.png"}, {"time": 5922.361, "image_uri": "frame_11672.png"}, {"time": 5922.869, "image_uri": "frame_11673.png"}, {"time": 5923.376, "image_uri": "frame_11674.png"}, {"time": 5923.884, "image_uri": "frame_11675.png"}, {"time": 5924.391, "image_uri": "frame_11676.png"}, {"time": 5924.899, "image_uri": "frame_11677.png"}, {"time": 5925.406, "image_uri": "frame_11678.png"}, {"time": 5925.913, "image_uri": "frame_11679.png"}, {"time": 5926.421, "image_uri": "frame_11680.png"}, {"time": 5926.928, "image_uri": "frame_11681.png"}, {"time": 5927.436, "image_uri": "frame_11682.png"}, {"time": 5927.943, "image_uri": "frame_11683.png"}, {"time": 5928.451, "image_uri": "frame_11684.png"}, {"time": 5928.958, "image_uri": "frame_11685.png"}, {"time": 5929.465, "image_uri": "frame_11686.png"}, {"time": 5929.973, "image_uri": "frame_11687.png"}, {"time": 5930.48, "image_uri": "frame_11688.png"}, {"time": 5930.988, "image_uri": "frame_11689.png"}, {"time": 5931.495, "image_uri": "frame_11690.png"}, {"time": 5932.003, "image_uri": "frame_11691.png"}, {"time": 5932.51, "image_uri": "frame_11692.png"}, {"time": 5933.018, "image_uri": "frame_11693.png"}, {"time": 5933.525, "image_uri": "frame_11694.png"}, {"time": 5934.032, "image_uri": "frame_11695.png"}, {"time": 5934.54, "image_uri": "frame_11696.png"}, {"time": 5935.047, "image_uri": "frame_11697.png"}, {"time": 5935.555, "image_uri": "frame_11698.png"}, {"time": 5936.062, "image_uri": "frame_11699.png"}, {"time": 5936.57, "image_uri": "frame_11700.png"}, {"time": 5937.077, "image_uri": "frame_11701.png"}, {"time": 5937.585, "image_uri": "frame_11702.png"}, {"time": 5938.092, "image_uri": "frame_11703.png"}, {"time": 5938.599, "image_uri": "frame_11704.png"}, {"time": 5939.107, "image_uri": "frame_11705.png"}, {"time": 5939.614, "image_uri": "frame_11706.png"}, {"time": 5940.122, "image_uri": "frame_11707.png"}, {"time": 5940.629, "image_uri": "frame_11708.png"}, {"time": 5941.137, "image_uri": "frame_11709.png"}, {"time": 5941.644, "image_uri": "frame_11710.png"}, {"time": 5942.152, "image_uri": "frame_11711.png"}, {"time": 5942.659, "image_uri": "frame_11712.png"}, {"time": 5943.166, "image_uri": "frame_11713.png"}, {"time": 5943.674, "image_uri": "frame_11714.png"}, {"time": 5944.181, "image_uri": "frame_11715.png"}, {"time": 5944.689, "image_uri": "frame_11716.png"}, {"time": 5945.196, "image_uri": "frame_11717.png"}, {"time": 5945.704, "image_uri": "frame_11718.png"}, {"time": 5946.211, "image_uri": "frame_11719.png"}, {"time": 5946.719, "image_uri": "frame_11720.png"}, {"time": 5947.226, "image_uri": "frame_11721.png"}, {"time": 5947.733, "image_uri": "frame_11722.png"}, {"time": 5948.241, "image_uri": "frame_11723.png"}, {"time": 5948.748, "image_uri": "frame_11724.png"}, {"time": 5949.256, "image_uri": "frame_11725.png"}, {"time": 5949.763, "image_uri": "frame_11726.png"}, {"time": 5950.271, "image_uri": "frame_11727.png"}, {"time": 5950.778, "image_uri": "frame_11728.png"}, {"time": 5951.286, "image_uri": "frame_11729.png"}, {"time": 5951.793, "image_uri": "frame_11730.png"}, {"time": 5952.3, "image_uri": "frame_11731.png"}, {"time": 5952.808, "image_uri": "frame_11732.png"}, {"time": 5953.315, "image_uri": "frame_11733.png"}, {"time": 5953.823, "image_uri": "frame_11734.png"}, {"time": 5954.33, "image_uri": "frame_11735.png"}, {"time": 5954.838, "image_uri": "frame_11736.png"}, {"time": 5955.345, "image_uri": "frame_11737.png"}, {"time": 5955.853, "image_uri": "frame_11738.png"}, {"time": 5956.36, "image_uri": "frame_11739.png"}, {"time": 5956.867, "image_uri": "frame_11740.png"}, {"time": 5957.375, "image_uri": "frame_11741.png"}, {"time": 5957.882, "image_uri": "frame_11742.png"}, {"time": 5958.39, "image_uri": "frame_11743.png"}, {"time": 5958.897, "image_uri": "frame_11744.png"}, {"time": 5959.405, "image_uri": "frame_11745.png"}, {"time": 5959.912, "image_uri": "frame_11746.png"}, {"time": 5960.419, "image_uri": "frame_11747.png"}, {"time": 5960.927, "image_uri": "frame_11748.png"}, {"time": 5961.434, "image_uri": "frame_11749.png"}, {"time": 5961.942, "image_uri": "frame_11750.png"}, {"time": 5962.449, "image_uri": "frame_11751.png"}, {"time": 5962.957, "image_uri": "frame_11752.png"}, {"time": 5963.464, "image_uri": "frame_11753.png"}, {"time": 5963.972, "image_uri": "frame_11754.png"}, {"time": 5964.479, "image_uri": "frame_11755.png"}, {"time": 5964.986, "image_uri": "frame_11756.png"}, {"time": 5965.494, "image_uri": "frame_11757.png"}, {"time": 5966.001, "image_uri": "frame_11758.png"}, {"time": 5966.509, "image_uri": "frame_11759.png"}, {"time": 5967.016, "image_uri": "frame_11760.png"}, {"time": 5967.524, "image_uri": "frame_11761.png"}, {"time": 5968.031, "image_uri": "frame_11762.png"}, {"time": 5968.539, "image_uri": "frame_11763.png"}, {"time": 5969.046, "image_uri": "frame_11764.png"}, {"time": 5969.553, "image_uri": "frame_11765.png"}, {"time": 5970.061, "image_uri": "frame_11766.png"}, {"time": 5970.568, "image_uri": "frame_11767.png"}, {"time": 5971.076, "image_uri": "frame_11768.png"}, {"time": 5971.583, "image_uri": "frame_11769.png"}, {"time": 5972.091, "image_uri": "frame_11770.png"}, {"time": 5972.598, "image_uri": "frame_11771.png"}, {"time": 5973.106, "image_uri": "frame_11772.png"}, {"time": 5973.613, "image_uri": "frame_11773.png"}, {"time": 5974.12, "image_uri": "frame_11774.png"}, {"time": 5974.628, "image_uri": "frame_11775.png"}, {"time": 5975.135, "image_uri": "frame_11776.png"}, {"time": 5975.643, "image_uri": "frame_11777.png"}, {"time": 5976.15, "image_uri": "frame_11778.png"}, {"time": 5976.658, "image_uri": "frame_11779.png"}, {"time": 5977.165, "image_uri": "frame_11780.png"}, {"time": 5977.673, "image_uri": "frame_11781.png"}, {"time": 5978.18, "image_uri": "frame_11782.png"}, {"time": 5978.687, "image_uri": "frame_11783.png"}, {"time": 5979.195, "image_uri": "frame_11784.png"}, {"time": 5979.702, "image_uri": "frame_11785.png"}, {"time": 5980.21, "image_uri": "frame_11786.png"}, {"time": 5980.717, "image_uri": "frame_11787.png"}, {"time": 5981.225, "image_uri": "frame_11788.png"}, {"time": 5981.732, "image_uri": "frame_11789.png"}, {"time": 5982.24, "image_uri": "frame_11790.png"}, {"time": 5982.747, "image_uri": "frame_11791.png"}, {"time": 5983.254, "image_uri": "frame_11792.png"}, {"time": 5983.762, "image_uri": "frame_11793.png"}, {"time": 5984.269, "image_uri": "frame_11794.png"}, {"time": 5984.777, "image_uri": "frame_11795.png"}, {"time": 5985.284, "image_uri": "frame_11796.png"}, {"time": 5985.792, "image_uri": "frame_11797.png"}, {"time": 5986.299, "image_uri": "frame_11798.png"}, {"time": 5986.806, "image_uri": "frame_11799.png"}, {"time": 5987.314, "image_uri": "frame_11800.png"}, {"time": 5987.821, "image_uri": "frame_11801.png"}, {"time": 5988.329, "image_uri": "frame_11802.png"}, {"time": 5988.836, "image_uri": "frame_11803.png"}, {"time": 5989.344, "image_uri": "frame_11804.png"}, {"time": 5989.851, "image_uri": "frame_11805.png"}, {"time": 5990.359, "image_uri": "frame_11806.png"}, {"time": 5990.866, "image_uri": "frame_11807.png"}, {"time": 5991.373, "image_uri": "frame_11808.png"}, {"time": 5991.881, "image_uri": "frame_11809.png"}, {"time": 5992.388, "image_uri": "frame_11810.png"}, {"time": 5992.896, "image_uri": "frame_11811.png"}, {"time": 5993.403, "image_uri": "frame_11812.png"}, {"time": 5993.911, "image_uri": "frame_11813.png"}, {"time": 5994.418, "image_uri": "frame_11814.png"}, {"time": 5994.926, "image_uri": "frame_11815.png"}, {"time": 5995.433, "image_uri": "frame_11816.png"}, {"time": 5995.94, "image_uri": "frame_11817.png"}, {"time": 5996.448, "image_uri": "frame_11818.png"}, {"time": 5996.955, "image_uri": "frame_11819.png"}, {"time": 5997.463, "image_uri": "frame_11820.png"}, {"time": 5997.97, "image_uri": "frame_11821.png"}, {"time": 5998.478, "image_uri": "frame_11822.png"}, {"time": 5998.985, "image_uri": "frame_11823.png"}, {"time": 5999.493, "image_uri": "frame_11824.png"}, {"time": 6000.0, "image_uri": "frame_11825.png"}, {"time": 6000.507, "image_uri": "frame_11826.png"}, {"time": 6001.015, "image_uri": "frame_11827.png"}, {"time": 6001.522, "image_uri": "frame_11828.png"}, {"time": 6002.03, "image_uri": "frame_11829.png"}, {"time": 6002.537, "image_uri": "frame_11830.png"}, {"time": 6003.045, "image_uri": "frame_11831.png"}, {"time": 6003.552, "image_uri": "frame_11832.png"}, {"time": 6004.06, "image_uri": "frame_11833.png"}, {"time": 6004.567, "image_uri": "frame_11834.png"}, {"time": 6005.074, "image_uri": "frame_11835.png"}, {"time": 6005.582, "image_uri": "frame_11836.png"}, {"time": 6006.089, "image_uri": "frame_11837.png"}, {"time": 6006.597, "image_uri": "frame_11838.png"}, {"time": 6007.104, "image_uri": "frame_11839.png"}, {"time": 6007.612, "image_uri": "frame_11840.png"}, {"time": 6008.119, "image_uri": "frame_11841.png"}, {"time": 6008.627, "image_uri": "frame_11842.png"}, {"time": 6009.134, "image_uri": "frame_11843.png"}, {"time": 6009.641, "image_uri": "frame_11844.png"}, {"time": 6010.149, "image_uri": "frame_11845.png"}, {"time": 6010.656, "image_uri": "frame_11846.png"}, {"time": 6011.164, "image_uri": "frame_11847.png"}, {"time": 6011.671, "image_uri": "frame_11848.png"}, {"time": 6012.179, "image_uri": "frame_11849.png"}, {"time": 6012.686, "image_uri": "frame_11850.png"}, {"time": 6013.194, "image_uri": "frame_11851.png"}, {"time": 6013.701, "image_uri": "frame_11852.png"}, {"time": 6014.208, "image_uri": "frame_11853.png"}, {"time": 6014.716, "image_uri": "frame_11854.png"}, {"time": 6015.223, "image_uri": "frame_11855.png"}, {"time": 6015.731, "image_uri": "frame_11856.png"}, {"time": 6016.238, "image_uri": "frame_11857.png"}, {"time": 6016.746, "image_uri": "frame_11858.png"}, {"time": 6017.253, "image_uri": "frame_11859.png"}, {"time": 6017.76, "image_uri": "frame_11860.png"}, {"time": 6018.268, "image_uri": "frame_11861.png"}, {"time": 6018.775, "image_uri": "frame_11862.png"}, {"time": 6019.283, "image_uri": "frame_11863.png"}, {"time": 6019.79, "image_uri": "frame_11864.png"}, {"time": 6020.298, "image_uri": "frame_11865.png"}, {"time": 6020.805, "image_uri": "frame_11866.png"}, {"time": 6021.313, "image_uri": "frame_11867.png"}, {"time": 6021.82, "image_uri": "frame_11868.png"}, {"time": 6022.327, "image_uri": "frame_11869.png"}, {"time": 6022.835, "image_uri": "frame_11870.png"}, {"time": 6023.342, "image_uri": "frame_11871.png"}, {"time": 6023.85, "image_uri": "frame_11872.png"}, {"time": 6024.357, "image_uri": "frame_11873.png"}, {"time": 6024.865, "image_uri": "frame_11874.png"}, {"time": 6025.372, "image_uri": "frame_11875.png"}, {"time": 6025.88, "image_uri": "frame_11876.png"}, {"time": 6026.387, "image_uri": "frame_11877.png"}, {"time": 6026.894, "image_uri": "frame_11878.png"}, {"time": 6027.402, "image_uri": "frame_11879.png"}, {"time": 6027.909, "image_uri": "frame_11880.png"}, {"time": 6028.417, "image_uri": "frame_11881.png"}, {"time": 6028.924, "image_uri": "frame_11882.png"}, {"time": 6029.432, "image_uri": "frame_11883.png"}, {"time": 6029.939, "image_uri": "frame_11884.png"}, {"time": 6030.447, "image_uri": "frame_11885.png"}, {"time": 6030.954, "image_uri": "frame_11886.png"}, {"time": 6031.461, "image_uri": "frame_11887.png"}, {"time": 6031.969, "image_uri": "frame_11888.png"}, {"time": 6032.476, "image_uri": "frame_11889.png"}, {"time": 6032.984, "image_uri": "frame_11890.png"}, {"time": 6033.491, "image_uri": "frame_11891.png"}, {"time": 6033.999, "image_uri": "frame_11892.png"}, {"time": 6034.506, "image_uri": "frame_11893.png"}, {"time": 6035.014, "image_uri": "frame_11894.png"}, {"time": 6035.521, "image_uri": "frame_11895.png"}, {"time": 6036.028, "image_uri": "frame_11896.png"}, {"time": 6036.536, "image_uri": "frame_11897.png"}, {"time": 6037.043, "image_uri": "frame_11898.png"}, {"time": 6037.551, "image_uri": "frame_11899.png"}, {"time": 6038.058, "image_uri": "frame_11900.png"}, {"time": 6038.566, "image_uri": "frame_11901.png"}, {"time": 6039.073, "image_uri": "frame_11902.png"}, {"time": 6039.581, "image_uri": "frame_11903.png"}, {"time": 6040.088, "image_uri": "frame_11904.png"}, {"time": 6040.595, "image_uri": "frame_11905.png"}, {"time": 6041.103, "image_uri": "frame_11906.png"}, {"time": 6041.61, "image_uri": "frame_11907.png"}, {"time": 6042.118, "image_uri": "frame_11908.png"}, {"time": 6042.625, "image_uri": "frame_11909.png"}, {"time": 6043.133, "image_uri": "frame_11910.png"}, {"time": 6043.64, "image_uri": "frame_11911.png"}, {"time": 6044.147, "image_uri": "frame_11912.png"}, {"time": 6044.655, "image_uri": "frame_11913.png"}, {"time": 6045.162, "image_uri": "frame_11914.png"}, {"time": 6045.67, "image_uri": "frame_11915.png"}, {"time": 6046.177, "image_uri": "frame_11916.png"}, {"time": 6046.685, "image_uri": "frame_11917.png"}, {"time": 6047.192, "image_uri": "frame_11918.png"}, {"time": 6047.7, "image_uri": "frame_11919.png"}, {"time": 6048.207, "image_uri": "frame_11920.png"}, {"time": 6048.714, "image_uri": "frame_11921.png"}, {"time": 6049.222, "image_uri": "frame_11922.png"}, {"time": 6049.729, "image_uri": "frame_11923.png"}, {"time": 6050.237, "image_uri": "frame_11924.png"}, {"time": 6050.744, "image_uri": "frame_11925.png"}, {"time": 6051.252, "image_uri": "frame_11926.png"}, {"time": 6051.759, "image_uri": "frame_11927.png"}, {"time": 6052.267, "image_uri": "frame_11928.png"}, {"time": 6052.774, "image_uri": "frame_11929.png"}, {"time": 6053.281, "image_uri": "frame_11930.png"}, {"time": 6053.789, "image_uri": "frame_11931.png"}, {"time": 6054.296, "image_uri": "frame_11932.png"}, {"time": 6054.804, "image_uri": "frame_11933.png"}, {"time": 6055.311, "image_uri": "frame_11934.png"}, {"time": 6055.819, "image_uri": "frame_11935.png"}, {"time": 6056.326, "image_uri": "frame_11936.png"}, {"time": 6056.834, "image_uri": "frame_11937.png"}, {"time": 6057.341, "image_uri": "frame_11938.png"}, {"time": 6057.848, "image_uri": "frame_11939.png"}, {"time": 6058.356, "image_uri": "frame_11940.png"}, {"time": 6058.863, "image_uri": "frame_11941.png"}, {"time": 6059.371, "image_uri": "frame_11942.png"}, {"time": 6059.878, "image_uri": "frame_11943.png"}, {"time": 6060.386, "image_uri": "frame_11944.png"}, {"time": 6060.893, "image_uri": "frame_11945.png"}, {"time": 6061.401, "image_uri": "frame_11946.png"}, {"time": 6061.908, "image_uri": "frame_11947.png"}, {"time": 6062.415, "image_uri": "frame_11948.png"}, {"time": 6062.923, "image_uri": "frame_11949.png"}, {"time": 6063.43, "image_uri": "frame_11950.png"}, {"time": 6063.938, "image_uri": "frame_11951.png"}, {"time": 6064.445, "image_uri": "frame_11952.png"}, {"time": 6064.953, "image_uri": "frame_11953.png"}, {"time": 6065.46, "image_uri": "frame_11954.png"}, {"time": 6065.968, "image_uri": "frame_11955.png"}, {"time": 6066.475, "image_uri": "frame_11956.png"}, {"time": 6066.982, "image_uri": "frame_11957.png"}, {"time": 6067.49, "image_uri": "frame_11958.png"}, {"time": 6067.997, "image_uri": "frame_11959.png"}, {"time": 6068.505, "image_uri": "frame_11960.png"}, {"time": 6069.012, "image_uri": "frame_11961.png"}, {"time": 6069.52, "image_uri": "frame_11962.png"}, {"time": 6070.027, "image_uri": "frame_11963.png"}, {"time": 6070.535, "image_uri": "frame_11964.png"}, {"time": 6071.042, "image_uri": "frame_11965.png"}, {"time": 6071.549, "image_uri": "frame_11966.png"}, {"time": 6072.057, "image_uri": "frame_11967.png"}, {"time": 6072.564, "image_uri": "frame_11968.png"}, {"time": 6073.072, "image_uri": "frame_11969.png"}, {"time": 6073.579, "image_uri": "frame_11970.png"}, {"time": 6074.087, "image_uri": "frame_11971.png"}, {"time": 6074.594, "image_uri": "frame_11972.png"}, {"time": 6075.101, "image_uri": "frame_11973.png"}, {"time": 6075.609, "image_uri": "frame_11974.png"}, {"time": 6076.116, "image_uri": "frame_11975.png"}, {"time": 6076.624, "image_uri": "frame_11976.png"}, {"time": 6077.131, "image_uri": "frame_11977.png"}, {"time": 6077.639, "image_uri": "frame_11978.png"}, {"time": 6078.146, "image_uri": "frame_11979.png"}, {"time": 6078.654, "image_uri": "frame_11980.png"}, {"time": 6079.161, "image_uri": "frame_11981.png"}, {"time": 6079.668, "image_uri": "frame_11982.png"}, {"time": 6080.176, "image_uri": "frame_11983.png"}, {"time": 6080.683, "image_uri": "frame_11984.png"}, {"time": 6081.191, "image_uri": "frame_11985.png"}, {"time": 6081.698, "image_uri": "frame_11986.png"}, {"time": 6082.206, "image_uri": "frame_11987.png"}, {"time": 6082.713, "image_uri": "frame_11988.png"}, {"time": 6083.221, "image_uri": "frame_11989.png"}, {"time": 6083.728, "image_uri": "frame_11990.png"}, {"time": 6084.235, "image_uri": "frame_11991.png"}, {"time": 6084.743, "image_uri": "frame_11992.png"}, {"time": 6085.25, "image_uri": "frame_11993.png"}, {"time": 6085.758, "image_uri": "frame_11994.png"}, {"time": 6086.265, "image_uri": "frame_11995.png"}, {"time": 6086.773, "image_uri": "frame_11996.png"}, {"time": 6087.28, "image_uri": "frame_11997.png"}, {"time": 6087.788, "image_uri": "frame_11998.png"}, {"time": 6088.295, "image_uri": "frame_11999.png"}, {"time": 6088.802, "image_uri": "frame_12000.png"}, {"time": 6089.31, "image_uri": "frame_12001.png"}, {"time": 6089.817, "image_uri": "frame_12002.png"}, {"time": 6090.325, "image_uri": "frame_12003.png"}, {"time": 6090.832, "image_uri": "frame_12004.png"}, {"time": 6091.34, "image_uri": "frame_12005.png"}, {"time": 6091.847, "image_uri": "frame_12006.png"}, {"time": 6092.355, "image_uri": "frame_12007.png"}, {"time": 6092.862, "image_uri": "frame_12008.png"}, {"time": 6093.369, "image_uri": "frame_12009.png"}, {"time": 6093.877, "image_uri": "frame_12010.png"}, {"time": 6094.384, "image_uri": "frame_12011.png"}, {"time": 6094.892, "image_uri": "frame_12012.png"}, {"time": 6095.399, "image_uri": "frame_12013.png"}, {"time": 6095.907, "image_uri": "frame_12014.png"}, {"time": 6096.414, "image_uri": "frame_12015.png"}, {"time": 6096.922, "image_uri": "frame_12016.png"}, {"time": 6097.429, "image_uri": "frame_12017.png"}, {"time": 6097.936, "image_uri": "frame_12018.png"}, {"time": 6098.444, "image_uri": "frame_12019.png"}, {"time": 6098.951, "image_uri": "frame_12020.png"}, {"time": 6099.459, "image_uri": "frame_12021.png"}, {"time": 6099.966, "image_uri": "frame_12022.png"}, {"time": 6100.474, "image_uri": "frame_12023.png"}, {"time": 6100.981, "image_uri": "frame_12024.png"}, {"time": 6101.488, "image_uri": "frame_12025.png"}, {"time": 6101.996, "image_uri": "frame_12026.png"}, {"time": 6102.503, "image_uri": "frame_12027.png"}, {"time": 6103.011, "image_uri": "frame_12028.png"}, {"time": 6103.518, "image_uri": "frame_12029.png"}, {"time": 6104.026, "image_uri": "frame_12030.png"}, {"time": 6104.533, "image_uri": "frame_12031.png"}, {"time": 6105.041, "image_uri": "frame_12032.png"}, {"time": 6105.548, "image_uri": "frame_12033.png"}, {"time": 6106.055, "image_uri": "frame_12034.png"}, {"time": 6106.563, "image_uri": "frame_12035.png"}, {"time": 6107.07, "image_uri": "frame_12036.png"}, {"time": 6107.578, "image_uri": "frame_12037.png"}, {"time": 6108.085, "image_uri": "frame_12038.png"}, {"time": 6108.593, "image_uri": "frame_12039.png"}, {"time": 6109.1, "image_uri": "frame_12040.png"}, {"time": 6109.608, "image_uri": "frame_12041.png"}, {"time": 6110.115, "image_uri": "frame_12042.png"}, {"time": 6110.622, "image_uri": "frame_12043.png"}, {"time": 6111.13, "image_uri": "frame_12044.png"}, {"time": 6111.637, "image_uri": "frame_12045.png"}, {"time": 6112.145, "image_uri": "frame_12046.png"}, {"time": 6112.652, "image_uri": "frame_12047.png"}, {"time": 6113.16, "image_uri": "frame_12048.png"}, {"time": 6113.667, "image_uri": "frame_12049.png"}, {"time": 6114.175, "image_uri": "frame_12050.png"}, {"time": 6114.682, "image_uri": "frame_12051.png"}, {"time": 6115.189, "image_uri": "frame_12052.png"}, {"time": 6115.697, "image_uri": "frame_12053.png"}, {"time": 6116.204, "image_uri": "frame_12054.png"}, {"time": 6116.712, "image_uri": "frame_12055.png"}, {"time": 6117.219, "image_uri": "frame_12056.png"}, {"time": 6117.727, "image_uri": "frame_12057.png"}, {"time": 6118.234, "image_uri": "frame_12058.png"}, {"time": 6118.742, "image_uri": "frame_12059.png"}, {"time": 6119.249, "image_uri": "frame_12060.png"}, {"time": 6119.756, "image_uri": "frame_12061.png"}, {"time": 6120.264, "image_uri": "frame_12062.png"}, {"time": 6120.771, "image_uri": "frame_12063.png"}, {"time": 6121.279, "image_uri": "frame_12064.png"}, {"time": 6121.786, "image_uri": "frame_12065.png"}, {"time": 6122.294, "image_uri": "frame_12066.png"}, {"time": 6122.801, "image_uri": "frame_12067.png"}, {"time": 6123.309, "image_uri": "frame_12068.png"}, {"time": 6123.816, "image_uri": "frame_12069.png"}, {"time": 6124.323, "image_uri": "frame_12070.png"}, {"time": 6124.831, "image_uri": "frame_12071.png"}, {"time": 6125.338, "image_uri": "frame_12072.png"}, {"time": 6125.846, "image_uri": "frame_12073.png"}, {"time": 6126.353, "image_uri": "frame_12074.png"}, {"time": 6126.861, "image_uri": "frame_12075.png"}, {"time": 6127.368, "image_uri": "frame_12076.png"}, {"time": 6127.876, "image_uri": "frame_12077.png"}, {"time": 6128.383, "image_uri": "frame_12078.png"}, {"time": 6128.89, "image_uri": "frame_12079.png"}, {"time": 6129.398, "image_uri": "frame_12080.png"}, {"time": 6129.905, "image_uri": "frame_12081.png"}, {"time": 6130.413, "image_uri": "frame_12082.png"}, {"time": 6130.92, "image_uri": "frame_12083.png"}, {"time": 6131.428, "image_uri": "frame_12084.png"}, {"time": 6131.935, "image_uri": "frame_12085.png"}, {"time": 6132.442, "image_uri": "frame_12086.png"}, {"time": 6132.95, "image_uri": "frame_12087.png"}, {"time": 6133.457, "image_uri": "frame_12088.png"}, {"time": 6133.965, "image_uri": "frame_12089.png"}, {"time": 6134.472, "image_uri": "frame_12090.png"}, {"time": 6134.98, "image_uri": "frame_12091.png"}, {"time": 6135.487, "image_uri": "frame_12092.png"}, {"time": 6135.995, "image_uri": "frame_12093.png"}, {"time": 6136.502, "image_uri": "frame_12094.png"}, {"time": 6137.009, "image_uri": "frame_12095.png"}, {"time": 6137.517, "image_uri": "frame_12096.png"}, {"time": 6138.024, "image_uri": "frame_12097.png"}, {"time": 6138.532, "image_uri": "frame_12098.png"}, {"time": 6139.039, "image_uri": "frame_12099.png"}, {"time": 6139.547, "image_uri": "frame_12100.png"}, {"time": 6140.054, "image_uri": "frame_12101.png"}, {"time": 6140.562, "image_uri": "frame_12102.png"}, {"time": 6141.069, "image_uri": "frame_12103.png"}, {"time": 6141.576, "image_uri": "frame_12104.png"}, {"time": 6142.084, "image_uri": "frame_12105.png"}, {"time": 6142.591, "image_uri": "frame_12106.png"}, {"time": 6143.099, "image_uri": "frame_12107.png"}, {"time": 6143.606, "image_uri": "frame_12108.png"}, {"time": 6144.114, "image_uri": "frame_12109.png"}, {"time": 6144.621, "image_uri": "frame_12110.png"}, {"time": 6145.129, "image_uri": "frame_12111.png"}, {"time": 6145.636, "image_uri": "frame_12112.png"}, {"time": 6146.143, "image_uri": "frame_12113.png"}, {"time": 6146.651, "image_uri": "frame_12114.png"}, {"time": 6147.158, "image_uri": "frame_12115.png"}, {"time": 6147.666, "image_uri": "frame_12116.png"}, {"time": 6148.173, "image_uri": "frame_12117.png"}, {"time": 6148.681, "image_uri": "frame_12118.png"}, {"time": 6149.188, "image_uri": "frame_12119.png"}, {"time": 6149.696, "image_uri": "frame_12120.png"}, {"time": 6150.203, "image_uri": "frame_12121.png"}, {"time": 6150.71, "image_uri": "frame_12122.png"}, {"time": 6151.218, "image_uri": "frame_12123.png"}, {"time": 6151.725, "image_uri": "frame_12124.png"}, {"time": 6152.233, "image_uri": "frame_12125.png"}, {"time": 6152.74, "image_uri": "frame_12126.png"}, {"time": 6153.248, "image_uri": "frame_12127.png"}, {"time": 6153.755, "image_uri": "frame_12128.png"}, {"time": 6154.263, "image_uri": "frame_12129.png"}, {"time": 6154.77, "image_uri": "frame_12130.png"}, {"time": 6155.277, "image_uri": "frame_12131.png"}, {"time": 6155.785, "image_uri": "frame_12132.png"}, {"time": 6156.292, "image_uri": "frame_12133.png"}, {"time": 6156.8, "image_uri": "frame_12134.png"}, {"time": 6157.307, "image_uri": "frame_12135.png"}, {"time": 6157.815, "image_uri": "frame_12136.png"}, {"time": 6158.322, "image_uri": "frame_12137.png"}, {"time": 6158.829, "image_uri": "frame_12138.png"}, {"time": 6159.337, "image_uri": "frame_12139.png"}, {"time": 6159.844, "image_uri": "frame_12140.png"}, {"time": 6160.352, "image_uri": "frame_12141.png"}, {"time": 6160.859, "image_uri": "frame_12142.png"}, {"time": 6161.367, "image_uri": "frame_12143.png"}, {"time": 6161.874, "image_uri": "frame_12144.png"}, {"time": 6162.382, "image_uri": "frame_12145.png"}, {"time": 6162.889, "image_uri": "frame_12146.png"}, {"time": 6163.396, "image_uri": "frame_12147.png"}, {"time": 6163.904, "image_uri": "frame_12148.png"}, {"time": 6164.411, "image_uri": "frame_12149.png"}, {"time": 6164.919, "image_uri": "frame_12150.png"}, {"time": 6165.426, "image_uri": "frame_12151.png"}, {"time": 6165.934, "image_uri": "frame_12152.png"}, {"time": 6166.441, "image_uri": "frame_12153.png"}, {"time": 6166.949, "image_uri": "frame_12154.png"}, {"time": 6167.456, "image_uri": "frame_12155.png"}, {"time": 6167.963, "image_uri": "frame_12156.png"}, {"time": 6168.471, "image_uri": "frame_12157.png"}, {"time": 6168.978, "image_uri": "frame_12158.png"}, {"time": 6169.486, "image_uri": "frame_12159.png"}, {"time": 6169.993, "image_uri": "frame_12160.png"}, {"time": 6170.501, "image_uri": "frame_12161.png"}, {"time": 6171.008, "image_uri": "frame_12162.png"}, {"time": 6171.516, "image_uri": "frame_12163.png"}, {"time": 6172.023, "image_uri": "frame_12164.png"}, {"time": 6172.53, "image_uri": "frame_12165.png"}, {"time": 6173.038, "image_uri": "frame_12166.png"}, {"time": 6173.545, "image_uri": "frame_12167.png"}, {"time": 6174.053, "image_uri": "frame_12168.png"}, {"time": 6174.56, "image_uri": "frame_12169.png"}, {"time": 6175.068, "image_uri": "frame_12170.png"}, {"time": 6175.575, "image_uri": "frame_12171.png"}, {"time": 6176.083, "image_uri": "frame_12172.png"}, {"time": 6176.59, "image_uri": "frame_12173.png"}, {"time": 6177.097, "image_uri": "frame_12174.png"}, {"time": 6177.605, "image_uri": "frame_12175.png"}, {"time": 6178.112, "image_uri": "frame_12176.png"}, {"time": 6178.62, "image_uri": "frame_12177.png"}, {"time": 6179.127, "image_uri": "frame_12178.png"}, {"time": 6179.635, "image_uri": "frame_12179.png"}, {"time": 6180.142, "image_uri": "frame_12180.png"}, {"time": 6180.65, "image_uri": "frame_12181.png"}, {"time": 6181.157, "image_uri": "frame_12182.png"}, {"time": 6181.664, "image_uri": "frame_12183.png"}, {"time": 6182.172, "image_uri": "frame_12184.png"}, {"time": 6182.679, "image_uri": "frame_12185.png"}, {"time": 6183.187, "image_uri": "frame_12186.png"}, {"time": 6183.694, "image_uri": "frame_12187.png"}, {"time": 6184.202, "image_uri": "frame_12188.png"}, {"time": 6184.709, "image_uri": "frame_12189.png"}, {"time": 6185.217, "image_uri": "frame_12190.png"}, {"time": 6185.724, "image_uri": "frame_12191.png"}, {"time": 6186.231, "image_uri": "frame_12192.png"}, {"time": 6186.739, "image_uri": "frame_12193.png"}, {"time": 6187.246, "image_uri": "frame_12194.png"}, {"time": 6187.754, "image_uri": "frame_12195.png"}, {"time": 6188.261, "image_uri": "frame_12196.png"}, {"time": 6188.769, "image_uri": "frame_12197.png"}, {"time": 6189.276, "image_uri": "frame_12198.png"}, {"time": 6189.783, "image_uri": "frame_12199.png"}, {"time": 6190.291, "image_uri": "frame_12200.png"}, {"time": 6190.798, "image_uri": "frame_12201.png"}, {"time": 6191.306, "image_uri": "frame_12202.png"}, {"time": 6191.813, "image_uri": "frame_12203.png"}, {"time": 6192.321, "image_uri": "frame_12204.png"}, {"time": 6192.828, "image_uri": "frame_12205.png"}, {"time": 6193.336, "image_uri": "frame_12206.png"}, {"time": 6193.843, "image_uri": "frame_12207.png"}, {"time": 6194.35, "image_uri": "frame_12208.png"}, {"time": 6194.858, "image_uri": "frame_12209.png"}, {"time": 6195.365, "image_uri": "frame_12210.png"}, {"time": 6195.873, "image_uri": "frame_12211.png"}, {"time": 6196.38, "image_uri": "frame_12212.png"}, {"time": 6196.888, "image_uri": "frame_12213.png"}, {"time": 6197.395, "image_uri": "frame_12214.png"}, {"time": 6197.903, "image_uri": "frame_12215.png"}, {"time": 6198.41, "image_uri": "frame_12216.png"}, {"time": 6198.917, "image_uri": "frame_12217.png"}, {"time": 6199.425, "image_uri": "frame_12218.png"}, {"time": 6199.932, "image_uri": "frame_12219.png"}, {"time": 6200.44, "image_uri": "frame_12220.png"}, {"time": 6200.947, "image_uri": "frame_12221.png"}, {"time": 6201.455, "image_uri": "frame_12222.png"}, {"time": 6201.962, "image_uri": "frame_12223.png"}, {"time": 6202.47, "image_uri": "frame_12224.png"}, {"time": 6202.977, "image_uri": "frame_12225.png"}, {"time": 6203.484, "image_uri": "frame_12226.png"}, {"time": 6203.992, "image_uri": "frame_12227.png"}, {"time": 6204.499, "image_uri": "frame_12228.png"}, {"time": 6205.007, "image_uri": "frame_12229.png"}, {"time": 6205.514, "image_uri": "frame_12230.png"}, {"time": 6206.022, "image_uri": "frame_12231.png"}, {"time": 6206.529, "image_uri": "frame_12232.png"}, {"time": 6207.037, "image_uri": "frame_12233.png"}, {"time": 6207.544, "image_uri": "frame_12234.png"}, {"time": 6208.051, "image_uri": "frame_12235.png"}, {"time": 6208.559, "image_uri": "frame_12236.png"}, {"time": 6209.066, "image_uri": "frame_12237.png"}, {"time": 6209.574, "image_uri": "frame_12238.png"}, {"time": 6210.081, "image_uri": "frame_12239.png"}, {"time": 6210.589, "image_uri": "frame_12240.png"}, {"time": 6211.096, "image_uri": "frame_12241.png"}, {"time": 6211.604, "image_uri": "frame_12242.png"}, {"time": 6212.111, "image_uri": "frame_12243.png"}, {"time": 6212.618, "image_uri": "frame_12244.png"}, {"time": 6213.126, "image_uri": "frame_12245.png"}, {"time": 6213.633, "image_uri": "frame_12246.png"}, {"time": 6214.141, "image_uri": "frame_12247.png"}, {"time": 6214.648, "image_uri": "frame_12248.png"}, {"time": 6215.156, "image_uri": "frame_12249.png"}, {"time": 6215.663, "image_uri": "frame_12250.png"}, {"time": 6216.171, "image_uri": "frame_12251.png"}, {"time": 6216.678, "image_uri": "frame_12252.png"}, {"time": 6217.185, "image_uri": "frame_12253.png"}, {"time": 6217.693, "image_uri": "frame_12254.png"}, {"time": 6218.2, "image_uri": "frame_12255.png"}, {"time": 6218.708, "image_uri": "frame_12256.png"}, {"time": 6219.215, "image_uri": "frame_12257.png"}, {"time": 6219.723, "image_uri": "frame_12258.png"}, {"time": 6220.23, "image_uri": "frame_12259.png"}, {"time": 6220.737, "image_uri": "frame_12260.png"}, {"time": 6221.245, "image_uri": "frame_12261.png"}, {"time": 6221.752, "image_uri": "frame_12262.png"}, {"time": 6222.26, "image_uri": "frame_12263.png"}, {"time": 6222.767, "image_uri": "frame_12264.png"}, {"time": 6223.275, "image_uri": "frame_12265.png"}, {"time": 6223.782, "image_uri": "frame_12266.png"}, {"time": 6224.29, "image_uri": "frame_12267.png"}, {"time": 6224.797, "image_uri": "frame_12268.png"}, {"time": 6225.304, "image_uri": "frame_12269.png"}, {"time": 6225.812, "image_uri": "frame_12270.png"}, {"time": 6226.319, "image_uri": "frame_12271.png"}, {"time": 6226.827, "image_uri": "frame_12272.png"}, {"time": 6227.334, "image_uri": "frame_12273.png"}, {"time": 6227.842, "image_uri": "frame_12274.png"}, {"time": 6228.349, "image_uri": "frame_12275.png"}, {"time": 6228.857, "image_uri": "frame_12276.png"}, {"time": 6229.364, "image_uri": "frame_12277.png"}, {"time": 6229.871, "image_uri": "frame_12278.png"}, {"time": 6230.379, "image_uri": "frame_12279.png"}, {"time": 6230.886, "image_uri": "frame_12280.png"}, {"time": 6231.394, "image_uri": "frame_12281.png"}, {"time": 6231.901, "image_uri": "frame_12282.png"}, {"time": 6232.409, "image_uri": "frame_12283.png"}, {"time": 6232.916, "image_uri": "frame_12284.png"}, {"time": 6233.424, "image_uri": "frame_12285.png"}, {"time": 6233.931, "image_uri": "frame_12286.png"}, {"time": 6234.438, "image_uri": "frame_12287.png"}, {"time": 6234.946, "image_uri": "frame_12288.png"}, {"time": 6235.453, "image_uri": "frame_12289.png"}, {"time": 6235.961, "image_uri": "frame_12290.png"}, {"time": 6236.468, "image_uri": "frame_12291.png"}, {"time": 6236.976, "image_uri": "frame_12292.png"}, {"time": 6237.483, "image_uri": "frame_12293.png"}, {"time": 6237.991, "image_uri": "frame_12294.png"}, {"time": 6238.498, "image_uri": "frame_12295.png"}, {"time": 6239.005, "image_uri": "frame_12296.png"}, {"time": 6239.513, "image_uri": "frame_12297.png"}, {"time": 6240.02, "image_uri": "frame_12298.png"}, {"time": 6240.528, "image_uri": "frame_12299.png"}, {"time": 6241.035, "image_uri": "frame_12300.png"}, {"time": 6241.543, "image_uri": "frame_12301.png"}, {"time": 6242.05, "image_uri": "frame_12302.png"}, {"time": 6242.558, "image_uri": "frame_12303.png"}, {"time": 6243.065, "image_uri": "frame_12304.png"}, {"time": 6243.572, "image_uri": "frame_12305.png"}, {"time": 6244.08, "image_uri": "frame_12306.png"}, {"time": 6244.587, "image_uri": "frame_12307.png"}, {"time": 6245.095, "image_uri": "frame_12308.png"}, {"time": 6245.602, "image_uri": "frame_12309.png"}, {"time": 6246.11, "image_uri": "frame_12310.png"}, {"time": 6246.617, "image_uri": "frame_12311.png"}, {"time": 6247.124, "image_uri": "frame_12312.png"}, {"time": 6247.632, "image_uri": "frame_12313.png"}, {"time": 6248.139, "image_uri": "frame_12314.png"}, {"time": 6248.647, "image_uri": "frame_12315.png"}, {"time": 6249.154, "image_uri": "frame_12316.png"}, {"time": 6249.662, "image_uri": "frame_12317.png"}, {"time": 6250.169, "image_uri": "frame_12318.png"}, {"time": 6250.677, "image_uri": "frame_12319.png"}, {"time": 6251.184, "image_uri": "frame_12320.png"}, {"time": 6251.691, "image_uri": "frame_12321.png"}, {"time": 6252.199, "image_uri": "frame_12322.png"}, {"time": 6252.706, "image_uri": "frame_12323.png"}, {"time": 6253.214, "image_uri": "frame_12324.png"}, {"time": 6253.721, "image_uri": "frame_12325.png"}, {"time": 6254.229, "image_uri": "frame_12326.png"}, {"time": 6254.736, "image_uri": "frame_12327.png"}, {"time": 6255.244, "image_uri": "frame_12328.png"}, {"time": 6255.751, "image_uri": "frame_12329.png"}, {"time": 6256.258, "image_uri": "frame_12330.png"}, {"time": 6256.766, "image_uri": "frame_12331.png"}, {"time": 6257.273, "image_uri": "frame_12332.png"}, {"time": 6257.781, "image_uri": "frame_12333.png"}, {"time": 6258.288, "image_uri": "frame_12334.png"}, {"time": 6258.796, "image_uri": "frame_12335.png"}, {"time": 6259.303, "image_uri": "frame_12336.png"}, {"time": 6259.811, "image_uri": "frame_12337.png"}, {"time": 6260.318, "image_uri": "frame_12338.png"}, {"time": 6260.825, "image_uri": "frame_12339.png"}, {"time": 6261.333, "image_uri": "frame_12340.png"}, {"time": 6261.84, "image_uri": "frame_12341.png"}, {"time": 6262.348, "image_uri": "frame_12342.png"}, {"time": 6262.855, "image_uri": "frame_12343.png"}, {"time": 6263.363, "image_uri": "frame_12344.png"}, {"time": 6263.87, "image_uri": "frame_12345.png"}, {"time": 6264.378, "image_uri": "frame_12346.png"}, {"time": 6264.885, "image_uri": "frame_12347.png"}, {"time": 6265.392, "image_uri": "frame_12348.png"}, {"time": 6265.9, "image_uri": "frame_12349.png"}, {"time": 6266.407, "image_uri": "frame_12350.png"}, {"time": 6266.915, "image_uri": "frame_12351.png"}, {"time": 6267.422, "image_uri": "frame_12352.png"}, {"time": 6267.93, "image_uri": "frame_12353.png"}, {"time": 6268.437, "image_uri": "frame_12354.png"}, {"time": 6268.945, "image_uri": "frame_12355.png"}, {"time": 6269.452, "image_uri": "frame_12356.png"}, {"time": 6269.959, "image_uri": "frame_12357.png"}, {"time": 6270.467, "image_uri": "frame_12358.png"}, {"time": 6270.974, "image_uri": "frame_12359.png"}, {"time": 6271.482, "image_uri": "frame_12360.png"}, {"time": 6271.989, "image_uri": "frame_12361.png"}, {"time": 6272.497, "image_uri": "frame_12362.png"}, {"time": 6273.004, "image_uri": "frame_12363.png"}, {"time": 6273.512, "image_uri": "frame_12364.png"}, {"time": 6274.019, "image_uri": "frame_12365.png"}, {"time": 6274.526, "image_uri": "frame_12366.png"}, {"time": 6275.034, "image_uri": "frame_12367.png"}, {"time": 6275.541, "image_uri": "frame_12368.png"}, {"time": 6276.049, "image_uri": "frame_12369.png"}, {"time": 6276.556, "image_uri": "frame_12370.png"}, {"time": 6277.064, "image_uri": "frame_12371.png"}, {"time": 6277.571, "image_uri": "frame_12372.png"}, {"time": 6278.078, "image_uri": "frame_12373.png"}, {"time": 6278.586, "image_uri": "frame_12374.png"}, {"time": 6279.093, "image_uri": "frame_12375.png"}, {"time": 6279.601, "image_uri": "frame_12376.png"}, {"time": 6280.108, "image_uri": "frame_12377.png"}, {"time": 6280.616, "image_uri": "frame_12378.png"}, {"time": 6281.123, "image_uri": "frame_12379.png"}, {"time": 6281.631, "image_uri": "frame_12380.png"}, {"time": 6282.138, "image_uri": "frame_12381.png"}, {"time": 6282.645, "image_uri": "frame_12382.png"}, {"time": 6283.153, "image_uri": "frame_12383.png"}, {"time": 6283.66, "image_uri": "frame_12384.png"}, {"time": 6284.168, "image_uri": "frame_12385.png"}, {"time": 6284.675, "image_uri": "frame_12386.png"}, {"time": 6285.183, "image_uri": "frame_12387.png"}, {"time": 6285.69, "image_uri": "frame_12388.png"}, {"time": 6286.198, "image_uri": "frame_12389.png"}, {"time": 6286.705, "image_uri": "frame_12390.png"}, {"time": 6287.212, "image_uri": "frame_12391.png"}, {"time": 6287.72, "image_uri": "frame_12392.png"}, {"time": 6288.227, "image_uri": "frame_12393.png"}, {"time": 6288.735, "image_uri": "frame_12394.png"}, {"time": 6289.242, "image_uri": "frame_12395.png"}, {"time": 6289.75, "image_uri": "frame_12396.png"}, {"time": 6290.257, "image_uri": "frame_12397.png"}, {"time": 6290.765, "image_uri": "frame_12398.png"}, {"time": 6291.272, "image_uri": "frame_12399.png"}, {"time": 6291.779, "image_uri": "frame_12400.png"}, {"time": 6292.287, "image_uri": "frame_12401.png"}, {"time": 6292.794, "image_uri": "frame_12402.png"}, {"time": 6293.302, "image_uri": "frame_12403.png"}, {"time": 6293.809, "image_uri": "frame_12404.png"}, {"time": 6294.317, "image_uri": "frame_12405.png"}, {"time": 6294.824, "image_uri": "frame_12406.png"}, {"time": 6295.332, "image_uri": "frame_12407.png"}, {"time": 6295.839, "image_uri": "frame_12408.png"}, {"time": 6296.346, "image_uri": "frame_12409.png"}, {"time": 6296.854, "image_uri": "frame_12410.png"}, {"time": 6297.361, "image_uri": "frame_12411.png"}, {"time": 6297.869, "image_uri": "frame_12412.png"}, {"time": 6298.376, "image_uri": "frame_12413.png"}, {"time": 6298.884, "image_uri": "frame_12414.png"}, {"time": 6299.391, "image_uri": "frame_12415.png"}, {"time": 6299.899, "image_uri": "frame_12416.png"}, {"time": 6300.406, "image_uri": "frame_12417.png"}, {"time": 6300.913, "image_uri": "frame_12418.png"}, {"time": 6301.421, "image_uri": "frame_12419.png"}, {"time": 6301.928, "image_uri": "frame_12420.png"}, {"time": 6302.436, "image_uri": "frame_12421.png"}, {"time": 6302.943, "image_uri": "frame_12422.png"}, {"time": 6303.451, "image_uri": "frame_12423.png"}, {"time": 6303.958, "image_uri": "frame_12424.png"}, {"time": 6304.465, "image_uri": "frame_12425.png"}, {"time": 6304.973, "image_uri": "frame_12426.png"}, {"time": 6305.48, "image_uri": "frame_12427.png"}, {"time": 6305.988, "image_uri": "frame_12428.png"}, {"time": 6306.495, "image_uri": "frame_12429.png"}, {"time": 6307.003, "image_uri": "frame_12430.png"}, {"time": 6307.51, "image_uri": "frame_12431.png"}, {"time": 6308.018, "image_uri": "frame_12432.png"}, {"time": 6308.525, "image_uri": "frame_12433.png"}, {"time": 6309.032, "image_uri": "frame_12434.png"}, {"time": 6309.54, "image_uri": "frame_12435.png"}, {"time": 6310.047, "image_uri": "frame_12436.png"}, {"time": 6310.555, "image_uri": "frame_12437.png"}, {"time": 6311.062, "image_uri": "frame_12438.png"}, {"time": 6311.57, "image_uri": "frame_12439.png"}, {"time": 6312.077, "image_uri": "frame_12440.png"}, {"time": 6312.585, "image_uri": "frame_12441.png"}, {"time": 6313.092, "image_uri": "frame_12442.png"}, {"time": 6313.599, "image_uri": "frame_12443.png"}, {"time": 6314.107, "image_uri": "frame_12444.png"}, {"time": 6314.614, "image_uri": "frame_12445.png"}, {"time": 6315.122, "image_uri": "frame_12446.png"}, {"time": 6315.629, "image_uri": "frame_12447.png"}, {"time": 6316.137, "image_uri": "frame_12448.png"}, {"time": 6316.644, "image_uri": "frame_12449.png"}, {"time": 6317.152, "image_uri": "frame_12450.png"}, {"time": 6317.659, "image_uri": "frame_12451.png"}, {"time": 6318.166, "image_uri": "frame_12452.png"}, {"time": 6318.674, "image_uri": "frame_12453.png"}, {"time": 6319.181, "image_uri": "frame_12454.png"}, {"time": 6319.689, "image_uri": "frame_12455.png"}, {"time": 6320.196, "image_uri": "frame_12456.png"}, {"time": 6320.704, "image_uri": "frame_12457.png"}, {"time": 6321.211, "image_uri": "frame_12458.png"}, {"time": 6321.719, "image_uri": "frame_12459.png"}, {"time": 6322.226, "image_uri": "frame_12460.png"}, {"time": 6322.733, "image_uri": "frame_12461.png"}, {"time": 6323.241, "image_uri": "frame_12462.png"}, {"time": 6323.748, "image_uri": "frame_12463.png"}, {"time": 6324.256, "image_uri": "frame_12464.png"}, {"time": 6324.763, "image_uri": "frame_12465.png"}, {"time": 6325.271, "image_uri": "frame_12466.png"}, {"time": 6325.778, "image_uri": "frame_12467.png"}, {"time": 6326.286, "image_uri": "frame_12468.png"}, {"time": 6326.793, "image_uri": "frame_12469.png"}, {"time": 6327.3, "image_uri": "frame_12470.png"}, {"time": 6327.808, "image_uri": "frame_12471.png"}, {"time": 6328.315, "image_uri": "frame_12472.png"}, {"time": 6328.823, "image_uri": "frame_12473.png"}, {"time": 6329.33, "image_uri": "frame_12474.png"}, {"time": 6329.838, "image_uri": "frame_12475.png"}, {"time": 6330.345, "image_uri": "frame_12476.png"}, {"time": 6330.853, "image_uri": "frame_12477.png"}, {"time": 6331.36, "image_uri": "frame_12478.png"}, {"time": 6331.867, "image_uri": "frame_12479.png"}, {"time": 6332.375, "image_uri": "frame_12480.png"}, {"time": 6332.882, "image_uri": "frame_12481.png"}, {"time": 6333.39, "image_uri": "frame_12482.png"}, {"time": 6333.897, "image_uri": "frame_12483.png"}, {"time": 6334.405, "image_uri": "frame_12484.png"}, {"time": 6334.912, "image_uri": "frame_12485.png"}, {"time": 6335.419, "image_uri": "frame_12486.png"}, {"time": 6335.927, "image_uri": "frame_12487.png"}, {"time": 6336.434, "image_uri": "frame_12488.png"}, {"time": 6336.942, "image_uri": "frame_12489.png"}, {"time": 6337.449, "image_uri": "frame_12490.png"}, {"time": 6337.957, "image_uri": "frame_12491.png"}, {"time": 6338.464, "image_uri": "frame_12492.png"}, {"time": 6338.972, "image_uri": "frame_12493.png"}, {"time": 6339.479, "image_uri": "frame_12494.png"}, {"time": 6339.986, "image_uri": "frame_12495.png"}, {"time": 6340.494, "image_uri": "frame_12496.png"}, {"time": 6341.001, "image_uri": "frame_12497.png"}, {"time": 6341.509, "image_uri": "frame_12498.png"}, {"time": 6342.016, "image_uri": "frame_12499.png"}, {"time": 6342.524, "image_uri": "frame_12500.png"}, {"time": 6343.031, "image_uri": "frame_12501.png"}, {"time": 6343.539, "image_uri": "frame_12502.png"}, {"time": 6344.046, "image_uri": "frame_12503.png"}, {"time": 6344.553, "image_uri": "frame_12504.png"}, {"time": 6345.061, "image_uri": "frame_12505.png"}, {"time": 6345.568, "image_uri": "frame_12506.png"}, {"time": 6346.076, "image_uri": "frame_12507.png"}, {"time": 6346.583, "image_uri": "frame_12508.png"}, {"time": 6347.091, "image_uri": "frame_12509.png"}, {"time": 6347.598, "image_uri": "frame_12510.png"}, {"time": 6348.106, "image_uri": "frame_12511.png"}, {"time": 6348.613, "image_uri": "frame_12512.png"}, {"time": 6349.12, "image_uri": "frame_12513.png"}, {"time": 6349.628, "image_uri": "frame_12514.png"}, {"time": 6350.135, "image_uri": "frame_12515.png"}, {"time": 6350.643, "image_uri": "frame_12516.png"}, {"time": 6351.15, "image_uri": "frame_12517.png"}, {"time": 6351.658, "image_uri": "frame_12518.png"}, {"time": 6352.165, "image_uri": "frame_12519.png"}, {"time": 6352.673, "image_uri": "frame_12520.png"}, {"time": 6353.18, "image_uri": "frame_12521.png"}, {"time": 6353.687, "image_uri": "frame_12522.png"}, {"time": 6354.195, "image_uri": "frame_12523.png"}, {"time": 6354.702, "image_uri": "frame_12524.png"}, {"time": 6355.21, "image_uri": "frame_12525.png"}, {"time": 6355.717, "image_uri": "frame_12526.png"}, {"time": 6356.225, "image_uri": "frame_12527.png"}, {"time": 6356.732, "image_uri": "frame_12528.png"}, {"time": 6357.24, "image_uri": "frame_12529.png"}, {"time": 6357.747, "image_uri": "frame_12530.png"}, {"time": 6358.254, "image_uri": "frame_12531.png"}, {"time": 6358.762, "image_uri": "frame_12532.png"}, {"time": 6359.269, "image_uri": "frame_12533.png"}, {"time": 6359.777, "image_uri": "frame_12534.png"}, {"time": 6360.284, "image_uri": "frame_12535.png"}, {"time": 6360.792, "image_uri": "frame_12536.png"}, {"time": 6361.299, "image_uri": "frame_12537.png"}, {"time": 6361.806, "image_uri": "frame_12538.png"}, {"time": 6362.314, "image_uri": "frame_12539.png"}, {"time": 6362.821, "image_uri": "frame_12540.png"}, {"time": 6363.329, "image_uri": "frame_12541.png"}, {"time": 6363.836, "image_uri": "frame_12542.png"}, {"time": 6364.344, "image_uri": "frame_12543.png"}, {"time": 6364.851, "image_uri": "frame_12544.png"}, {"time": 6365.359, "image_uri": "frame_12545.png"}, {"time": 6365.866, "image_uri": "frame_12546.png"}, {"time": 6366.373, "image_uri": "frame_12547.png"}, {"time": 6366.881, "image_uri": "frame_12548.png"}, {"time": 6367.388, "image_uri": "frame_12549.png"}, {"time": 6367.896, "image_uri": "frame_12550.png"}, {"time": 6368.403, "image_uri": "frame_12551.png"}, {"time": 6368.911, "image_uri": "frame_12552.png"}, {"time": 6369.418, "image_uri": "frame_12553.png"}, {"time": 6369.926, "image_uri": "frame_12554.png"}, {"time": 6370.433, "image_uri": "frame_12555.png"}, {"time": 6370.94, "image_uri": "frame_12556.png"}, {"time": 6371.448, "image_uri": "frame_12557.png"}, {"time": 6371.955, "image_uri": "frame_12558.png"}, {"time": 6372.463, "image_uri": "frame_12559.png"}, {"time": 6372.97, "image_uri": "frame_12560.png"}, {"time": 6373.478, "image_uri": "frame_12561.png"}, {"time": 6373.985, "image_uri": "frame_12562.png"}, {"time": 6374.493, "image_uri": "frame_12563.png"}, {"time": 6375.0, "image_uri": "frame_12564.png"}, {"time": 6375.507, "image_uri": "frame_12565.png"}, {"time": 6376.015, "image_uri": "frame_12566.png"}, {"time": 6376.522, "image_uri": "frame_12567.png"}, {"time": 6377.03, "image_uri": "frame_12568.png"}, {"time": 6377.537, "image_uri": "frame_12569.png"}, {"time": 6378.045, "image_uri": "frame_12570.png"}, {"time": 6378.552, "image_uri": "frame_12571.png"}, {"time": 6379.06, "image_uri": "frame_12572.png"}, {"time": 6379.567, "image_uri": "frame_12573.png"}, {"time": 6380.074, "image_uri": "frame_12574.png"}, {"time": 6380.582, "image_uri": "frame_12575.png"}, {"time": 6381.089, "image_uri": "frame_12576.png"}, {"time": 6381.597, "image_uri": "frame_12577.png"}, {"time": 6382.104, "image_uri": "frame_12578.png"}, {"time": 6382.612, "image_uri": "frame_12579.png"}, {"time": 6383.119, "image_uri": "frame_12580.png"}, {"time": 6383.627, "image_uri": "frame_12581.png"}, {"time": 6384.134, "image_uri": "frame_12582.png"}, {"time": 6384.641, "image_uri": "frame_12583.png"}, {"time": 6385.149, "image_uri": "frame_12584.png"}, {"time": 6385.656, "image_uri": "frame_12585.png"}, {"time": 6386.164, "image_uri": "frame_12586.png"}, {"time": 6386.671, "image_uri": "frame_12587.png"}, {"time": 6387.179, "image_uri": "frame_12588.png"}, {"time": 6387.686, "image_uri": "frame_12589.png"}, {"time": 6388.194, "image_uri": "frame_12590.png"}, {"time": 6388.701, "image_uri": "frame_12591.png"}, {"time": 6389.208, "image_uri": "frame_12592.png"}, {"time": 6389.716, "image_uri": "frame_12593.png"}, {"time": 6390.223, "image_uri": "frame_12594.png"}, {"time": 6390.731, "image_uri": "frame_12595.png"}, {"time": 6391.238, "image_uri": "frame_12596.png"}, {"time": 6391.746, "image_uri": "frame_12597.png"}, {"time": 6392.253, "image_uri": "frame_12598.png"}, {"time": 6392.76, "image_uri": "frame_12599.png"}, {"time": 6393.268, "image_uri": "frame_12600.png"}, {"time": 6393.775, "image_uri": "frame_12601.png"}, {"time": 6394.283, "image_uri": "frame_12602.png"}, {"time": 6394.79, "image_uri": "frame_12603.png"}, {"time": 6395.298, "image_uri": "frame_12604.png"}, {"time": 6395.805, "image_uri": "frame_12605.png"}, {"time": 6396.313, "image_uri": "frame_12606.png"}, {"time": 6396.82, "image_uri": "frame_12607.png"}, {"time": 6397.327, "image_uri": "frame_12608.png"}, {"time": 6397.835, "image_uri": "frame_12609.png"}, {"time": 6398.342, "image_uri": "frame_12610.png"}, {"time": 6398.85, "image_uri": "frame_12611.png"}, {"time": 6399.357, "image_uri": "frame_12612.png"}, {"time": 6399.865, "image_uri": "frame_12613.png"}, {"time": 6400.372, "image_uri": "frame_12614.png"}, {"time": 6400.88, "image_uri": "frame_12615.png"}, {"time": 6401.387, "image_uri": "frame_12616.png"}, {"time": 6401.894, "image_uri": "frame_12617.png"}, {"time": 6402.402, "image_uri": "frame_12618.png"}, {"time": 6402.909, "image_uri": "frame_12619.png"}, {"time": 6403.417, "image_uri": "frame_12620.png"}, {"time": 6403.924, "image_uri": "frame_12621.png"}, {"time": 6404.432, "image_uri": "frame_12622.png"}, {"time": 6404.939, "image_uri": "frame_12623.png"}, {"time": 6405.447, "image_uri": "frame_12624.png"}, {"time": 6405.954, "image_uri": "frame_12625.png"}, {"time": 6406.461, "image_uri": "frame_12626.png"}, {"time": 6406.969, "image_uri": "frame_12627.png"}, {"time": 6407.476, "image_uri": "frame_12628.png"}, {"time": 6407.984, "image_uri": "frame_12629.png"}, {"time": 6408.491, "image_uri": "frame_12630.png"}, {"time": 6408.999, "image_uri": "frame_12631.png"}, {"time": 6409.506, "image_uri": "frame_12632.png"}, {"time": 6410.014, "image_uri": "frame_12633.png"}, {"time": 6410.521, "image_uri": "frame_12634.png"}, {"time": 6411.028, "image_uri": "frame_12635.png"}, {"time": 6411.536, "image_uri": "frame_12636.png"}, {"time": 6412.043, "image_uri": "frame_12637.png"}, {"time": 6412.551, "image_uri": "frame_12638.png"}, {"time": 6413.058, "image_uri": "frame_12639.png"}, {"time": 6413.566, "image_uri": "frame_12640.png"}, {"time": 6414.073, "image_uri": "frame_12641.png"}, {"time": 6414.581, "image_uri": "frame_12642.png"}, {"time": 6415.088, "image_uri": "frame_12643.png"}, {"time": 6415.595, "image_uri": "frame_12644.png"}, {"time": 6416.103, "image_uri": "frame_12645.png"}, {"time": 6416.61, "image_uri": "frame_12646.png"}, {"time": 6417.118, "image_uri": "frame_12647.png"}, {"time": 6417.625, "image_uri": "frame_12648.png"}, {"time": 6418.133, "image_uri": "frame_12649.png"}, {"time": 6418.64, "image_uri": "frame_12650.png"}, {"time": 6419.147, "image_uri": "frame_12651.png"}, {"time": 6419.655, "image_uri": "frame_12652.png"}, {"time": 6420.162, "image_uri": "frame_12653.png"}, {"time": 6420.67, "image_uri": "frame_12654.png"}, {"time": 6421.177, "image_uri": "frame_12655.png"}, {"time": 6421.685, "image_uri": "frame_12656.png"}, {"time": 6422.192, "image_uri": "frame_12657.png"}, {"time": 6422.7, "image_uri": "frame_12658.png"}, {"time": 6423.207, "image_uri": "frame_12659.png"}, {"time": 6423.714, "image_uri": "frame_12660.png"}, {"time": 6424.222, "image_uri": "frame_12661.png"}, {"time": 6424.729, "image_uri": "frame_12662.png"}, {"time": 6425.237, "image_uri": "frame_12663.png"}, {"time": 6425.744, "image_uri": "frame_12664.png"}, {"time": 6426.252, "image_uri": "frame_12665.png"}, {"time": 6426.759, "image_uri": "frame_12666.png"}, {"time": 6427.267, "image_uri": "frame_12667.png"}, {"time": 6427.774, "image_uri": "frame_12668.png"}, {"time": 6428.281, "image_uri": "frame_12669.png"}, {"time": 6428.789, "image_uri": "frame_12670.png"}, {"time": 6429.296, "image_uri": "frame_12671.png"}, {"time": 6429.804, "image_uri": "frame_12672.png"}, {"time": 6430.311, "image_uri": "frame_12673.png"}, {"time": 6430.819, "image_uri": "frame_12674.png"}, {"time": 6431.326, "image_uri": "frame_12675.png"}, {"time": 6431.834, "image_uri": "frame_12676.png"}, {"time": 6432.341, "image_uri": "frame_12677.png"}, {"time": 6432.848, "image_uri": "frame_12678.png"}, {"time": 6433.356, "image_uri": "frame_12679.png"}, {"time": 6433.863, "image_uri": "frame_12680.png"}, {"time": 6434.371, "image_uri": "frame_12681.png"}, {"time": 6434.878, "image_uri": "frame_12682.png"}, {"time": 6435.386, "image_uri": "frame_12683.png"}, {"time": 6435.893, "image_uri": "frame_12684.png"}, {"time": 6436.401, "image_uri": "frame_12685.png"}, {"time": 6436.908, "image_uri": "frame_12686.png"}, {"time": 6437.415, "image_uri": "frame_12687.png"}, {"time": 6437.923, "image_uri": "frame_12688.png"}, {"time": 6438.43, "image_uri": "frame_12689.png"}, {"time": 6438.938, "image_uri": "frame_12690.png"}, {"time": 6439.445, "image_uri": "frame_12691.png"}, {"time": 6439.953, "image_uri": "frame_12692.png"}, {"time": 6440.46, "image_uri": "frame_12693.png"}, {"time": 6440.968, "image_uri": "frame_12694.png"}, {"time": 6441.475, "image_uri": "frame_12695.png"}, {"time": 6441.982, "image_uri": "frame_12696.png"}, {"time": 6442.49, "image_uri": "frame_12697.png"}, {"time": 6442.997, "image_uri": "frame_12698.png"}, {"time": 6443.505, "image_uri": "frame_12699.png"}, {"time": 6444.012, "image_uri": "frame_12700.png"}, {"time": 6444.52, "image_uri": "frame_12701.png"}, {"time": 6445.027, "image_uri": "frame_12702.png"}, {"time": 6445.535, "image_uri": "frame_12703.png"}, {"time": 6446.042, "image_uri": "frame_12704.png"}, {"time": 6446.549, "image_uri": "frame_12705.png"}, {"time": 6447.057, "image_uri": "frame_12706.png"}, {"time": 6447.564, "image_uri": "frame_12707.png"}, {"time": 6448.072, "image_uri": "frame_12708.png"}, {"time": 6448.579, "image_uri": "frame_12709.png"}, {"time": 6449.087, "image_uri": "frame_12710.png"}, {"time": 6449.594, "image_uri": "frame_12711.png"}, {"time": 6450.101, "image_uri": "frame_12712.png"}, {"time": 6450.609, "image_uri": "frame_12713.png"}, {"time": 6451.116, "image_uri": "frame_12714.png"}, {"time": 6451.624, "image_uri": "frame_12715.png"}, {"time": 6452.131, "image_uri": "frame_12716.png"}, {"time": 6452.639, "image_uri": "frame_12717.png"}, {"time": 6453.146, "image_uri": "frame_12718.png"}, {"time": 6453.654, "image_uri": "frame_12719.png"}, {"time": 6454.161, "image_uri": "frame_12720.png"}, {"time": 6454.668, "image_uri": "frame_12721.png"}, {"time": 6455.176, "image_uri": "frame_12722.png"}, {"time": 6455.683, "image_uri": "frame_12723.png"}, {"time": 6456.191, "image_uri": "frame_12724.png"}, {"time": 6456.698, "image_uri": "frame_12725.png"}, {"time": 6457.206, "image_uri": "frame_12726.png"}, {"time": 6457.713, "image_uri": "frame_12727.png"}, {"time": 6458.221, "image_uri": "frame_12728.png"}, {"time": 6458.728, "image_uri": "frame_12729.png"}, {"time": 6459.235, "image_uri": "frame_12730.png"}, {"time": 6459.743, "image_uri": "frame_12731.png"}, {"time": 6460.25, "image_uri": "frame_12732.png"}, {"time": 6460.758, "image_uri": "frame_12733.png"}, {"time": 6461.265, "image_uri": "frame_12734.png"}, {"time": 6461.773, "image_uri": "frame_12735.png"}, {"time": 6462.28, "image_uri": "frame_12736.png"}, {"time": 6462.788, "image_uri": "frame_12737.png"}, {"time": 6463.295, "image_uri": "frame_12738.png"}, {"time": 6463.802, "image_uri": "frame_12739.png"}, {"time": 6464.31, "image_uri": "frame_12740.png"}, {"time": 6464.817, "image_uri": "frame_12741.png"}, {"time": 6465.325, "image_uri": "frame_12742.png"}, {"time": 6465.832, "image_uri": "frame_12743.png"}, {"time": 6466.34, "image_uri": "frame_12744.png"}, {"time": 6466.847, "image_uri": "frame_12745.png"}, {"time": 6467.355, "image_uri": "frame_12746.png"}, {"time": 6467.862, "image_uri": "frame_12747.png"}, {"time": 6468.369, "image_uri": "frame_12748.png"}, {"time": 6468.877, "image_uri": "frame_12749.png"}, {"time": 6469.384, "image_uri": "frame_12750.png"}, {"time": 6469.892, "image_uri": "frame_12751.png"}, {"time": 6470.399, "image_uri": "frame_12752.png"}, {"time": 6470.907, "image_uri": "frame_12753.png"}, {"time": 6471.414, "image_uri": "frame_12754.png"}, {"time": 6471.922, "image_uri": "frame_12755.png"}, {"time": 6472.429, "image_uri": "frame_12756.png"}, {"time": 6472.936, "image_uri": "frame_12757.png"}, {"time": 6473.444, "image_uri": "frame_12758.png"}, {"time": 6473.951, "image_uri": "frame_12759.png"}, {"time": 6474.459, "image_uri": "frame_12760.png"}, {"time": 6474.966, "image_uri": "frame_12761.png"}, {"time": 6475.474, "image_uri": "frame_12762.png"}, {"time": 6475.981, "image_uri": "frame_12763.png"}, {"time": 6476.488, "image_uri": "frame_12764.png"}, {"time": 6476.996, "image_uri": "frame_12765.png"}, {"time": 6477.503, "image_uri": "frame_12766.png"}, {"time": 6478.011, "image_uri": "frame_12767.png"}, {"time": 6478.518, "image_uri": "frame_12768.png"}, {"time": 6479.026, "image_uri": "frame_12769.png"}, {"time": 6479.533, "image_uri": "frame_12770.png"}, {"time": 6480.041, "image_uri": "frame_12771.png"}, {"time": 6480.548, "image_uri": "frame_12772.png"}, {"time": 6481.055, "image_uri": "frame_12773.png"}, {"time": 6481.563, "image_uri": "frame_12774.png"}, {"time": 6482.07, "image_uri": "frame_12775.png"}, {"time": 6482.578, "image_uri": "frame_12776.png"}, {"time": 6483.085, "image_uri": "frame_12777.png"}, {"time": 6483.593, "image_uri": "frame_12778.png"}, {"time": 6484.1, "image_uri": "frame_12779.png"}, {"time": 6484.608, "image_uri": "frame_12780.png"}, {"time": 6485.115, "image_uri": "frame_12781.png"}, {"time": 6485.622, "image_uri": "frame_12782.png"}, {"time": 6486.13, "image_uri": "frame_12783.png"}, {"time": 6486.637, "image_uri": "frame_12784.png"}, {"time": 6487.145, "image_uri": "frame_12785.png"}, {"time": 6487.652, "image_uri": "frame_12786.png"}, {"time": 6488.16, "image_uri": "frame_12787.png"}, {"time": 6488.667, "image_uri": "frame_12788.png"}, {"time": 6489.175, "image_uri": "frame_12789.png"}, {"time": 6489.682, "image_uri": "frame_12790.png"}, {"time": 6490.189, "image_uri": "frame_12791.png"}, {"time": 6490.697, "image_uri": "frame_12792.png"}, {"time": 6491.204, "image_uri": "frame_12793.png"}, {"time": 6491.712, "image_uri": "frame_12794.png"}, {"time": 6492.219, "image_uri": "frame_12795.png"}, {"time": 6492.727, "image_uri": "frame_12796.png"}, {"time": 6493.234, "image_uri": "frame_12797.png"}, {"time": 6493.742, "image_uri": "frame_12798.png"}, {"time": 6494.249, "image_uri": "frame_12799.png"}, {"time": 6494.756, "image_uri": "frame_12800.png"}, {"time": 6495.264, "image_uri": "frame_12801.png"}, {"time": 6495.771, "image_uri": "frame_12802.png"}, {"time": 6496.279, "image_uri": "frame_12803.png"}, {"time": 6496.786, "image_uri": "frame_12804.png"}, {"time": 6497.294, "image_uri": "frame_12805.png"}, {"time": 6497.801, "image_uri": "frame_12806.png"}, {"time": 6498.309, "image_uri": "frame_12807.png"}, {"time": 6498.816, "image_uri": "frame_12808.png"}, {"time": 6499.323, "image_uri": "frame_12809.png"}, {"time": 6499.831, "image_uri": "frame_12810.png"}, {"time": 6500.338, "image_uri": "frame_12811.png"}, {"time": 6500.846, "image_uri": "frame_12812.png"}, {"time": 6501.353, "image_uri": "frame_12813.png"}, {"time": 6501.861, "image_uri": "frame_12814.png"}, {"time": 6502.368, "image_uri": "frame_12815.png"}, {"time": 6502.876, "image_uri": "frame_12816.png"}, {"time": 6503.383, "image_uri": "frame_12817.png"}, {"time": 6503.89, "image_uri": "frame_12818.png"}, {"time": 6504.398, "image_uri": "frame_12819.png"}, {"time": 6504.905, "image_uri": "frame_12820.png"}, {"time": 6505.413, "image_uri": "frame_12821.png"}, {"time": 6505.92, "image_uri": "frame_12822.png"}, {"time": 6506.428, "image_uri": "frame_12823.png"}, {"time": 6506.935, "image_uri": "frame_12824.png"}, {"time": 6507.442, "image_uri": "frame_12825.png"}, {"time": 6507.95, "image_uri": "frame_12826.png"}, {"time": 6508.457, "image_uri": "frame_12827.png"}, {"time": 6508.965, "image_uri": "frame_12828.png"}, {"time": 6509.472, "image_uri": "frame_12829.png"}, {"time": 6509.98, "image_uri": "frame_12830.png"}, {"time": 6510.487, "image_uri": "frame_12831.png"}, {"time": 6510.995, "image_uri": "frame_12832.png"}, {"time": 6511.502, "image_uri": "frame_12833.png"}, {"time": 6512.009, "image_uri": "frame_12834.png"}, {"time": 6512.517, "image_uri": "frame_12835.png"}, {"time": 6513.024, "image_uri": "frame_12836.png"}, {"time": 6513.532, "image_uri": "frame_12837.png"}, {"time": 6514.039, "image_uri": "frame_12838.png"}, {"time": 6514.547, "image_uri": "frame_12839.png"}, {"time": 6515.054, "image_uri": "frame_12840.png"}, {"time": 6515.562, "image_uri": "frame_12841.png"}, {"time": 6516.069, "image_uri": "frame_12842.png"}, {"time": 6516.576, "image_uri": "frame_12843.png"}, {"time": 6517.084, "image_uri": "frame_12844.png"}, {"time": 6517.591, "image_uri": "frame_12845.png"}, {"time": 6518.099, "image_uri": "frame_12846.png"}, {"time": 6518.606, "image_uri": "frame_12847.png"}, {"time": 6519.114, "image_uri": "frame_12848.png"}, {"time": 6519.621, "image_uri": "frame_12849.png"}, {"time": 6520.129, "image_uri": "frame_12850.png"}, {"time": 6520.636, "image_uri": "frame_12851.png"}, {"time": 6521.143, "image_uri": "frame_12852.png"}, {"time": 6521.651, "image_uri": "frame_12853.png"}, {"time": 6522.158, "image_uri": "frame_12854.png"}, {"time": 6522.666, "image_uri": "frame_12855.png"}, {"time": 6523.173, "image_uri": "frame_12856.png"}, {"time": 6523.681, "image_uri": "frame_12857.png"}, {"time": 6524.188, "image_uri": "frame_12858.png"}, {"time": 6524.696, "image_uri": "frame_12859.png"}, {"time": 6525.203, "image_uri": "frame_12860.png"}, {"time": 6525.71, "image_uri": "frame_12861.png"}, {"time": 6526.218, "image_uri": "frame_12862.png"}, {"time": 6526.725, "image_uri": "frame_12863.png"}, {"time": 6527.233, "image_uri": "frame_12864.png"}, {"time": 6527.74, "image_uri": "frame_12865.png"}, {"time": 6528.248, "image_uri": "frame_12866.png"}, {"time": 6528.755, "image_uri": "frame_12867.png"}, {"time": 6529.263, "image_uri": "frame_12868.png"}, {"time": 6529.77, "image_uri": "frame_12869.png"}, {"time": 6530.277, "image_uri": "frame_12870.png"}, {"time": 6530.785, "image_uri": "frame_12871.png"}, {"time": 6531.292, "image_uri": "frame_12872.png"}, {"time": 6531.8, "image_uri": "frame_12873.png"}, {"time": 6532.307, "image_uri": "frame_12874.png"}, {"time": 6532.815, "image_uri": "frame_12875.png"}, {"time": 6533.322, "image_uri": "frame_12876.png"}, {"time": 6533.829, "image_uri": "frame_12877.png"}, {"time": 6534.337, "image_uri": "frame_12878.png"}, {"time": 6534.844, "image_uri": "frame_12879.png"}, {"time": 6535.352, "image_uri": "frame_12880.png"}, {"time": 6535.859, "image_uri": "frame_12881.png"}, {"time": 6536.367, "image_uri": "frame_12882.png"}, {"time": 6536.874, "image_uri": "frame_12883.png"}, {"time": 6537.382, "image_uri": "frame_12884.png"}, {"time": 6537.889, "image_uri": "frame_12885.png"}, {"time": 6538.396, "image_uri": "frame_12886.png"}, {"time": 6538.904, "image_uri": "frame_12887.png"}, {"time": 6539.411, "image_uri": "frame_12888.png"}, {"time": 6539.919, "image_uri": "frame_12889.png"}, {"time": 6540.426, "image_uri": "frame_12890.png"}, {"time": 6540.934, "image_uri": "frame_12891.png"}, {"time": 6541.441, "image_uri": "frame_12892.png"}, {"time": 6541.949, "image_uri": "frame_12893.png"}, {"time": 6542.456, "image_uri": "frame_12894.png"}, {"time": 6542.963, "image_uri": "frame_12895.png"}, {"time": 6543.471, "image_uri": "frame_12896.png"}, {"time": 6543.978, "image_uri": "frame_12897.png"}, {"time": 6544.486, "image_uri": "frame_12898.png"}, {"time": 6544.993, "image_uri": "frame_12899.png"}, {"time": 6545.501, "image_uri": "frame_12900.png"}, {"time": 6546.008, "image_uri": "frame_12901.png"}, {"time": 6546.516, "image_uri": "frame_12902.png"}, {"time": 6547.023, "image_uri": "frame_12903.png"}, {"time": 6547.53, "image_uri": "frame_12904.png"}, {"time": 6548.038, "image_uri": "frame_12905.png"}, {"time": 6548.545, "image_uri": "frame_12906.png"}, {"time": 6549.053, "image_uri": "frame_12907.png"}, {"time": 6549.56, "image_uri": "frame_12908.png"}, {"time": 6550.068, "image_uri": "frame_12909.png"}, {"time": 6550.575, "image_uri": "frame_12910.png"}, {"time": 6551.083, "image_uri": "frame_12911.png"}, {"time": 6551.59, "image_uri": "frame_12912.png"}, {"time": 6552.097, "image_uri": "frame_12913.png"}, {"time": 6552.605, "image_uri": "frame_12914.png"}, {"time": 6553.112, "image_uri": "frame_12915.png"}, {"time": 6553.62, "image_uri": "frame_12916.png"}, {"time": 6554.127, "image_uri": "frame_12917.png"}, {"time": 6554.635, "image_uri": "frame_12918.png"}, {"time": 6555.142, "image_uri": "frame_12919.png"}, {"time": 6555.65, "image_uri": "frame_12920.png"}, {"time": 6556.157, "image_uri": "frame_12921.png"}, {"time": 6556.664, "image_uri": "frame_12922.png"}, {"time": 6557.172, "image_uri": "frame_12923.png"}, {"time": 6557.679, "image_uri": "frame_12924.png"}, {"time": 6558.187, "image_uri": "frame_12925.png"}, {"time": 6558.694, "image_uri": "frame_12926.png"}, {"time": 6559.202, "image_uri": "frame_12927.png"}, {"time": 6559.709, "image_uri": "frame_12928.png"}, {"time": 6560.217, "image_uri": "frame_12929.png"}, {"time": 6560.724, "image_uri": "frame_12930.png"}, {"time": 6561.231, "image_uri": "frame_12931.png"}, {"time": 6561.739, "image_uri": "frame_12932.png"}, {"time": 6562.246, "image_uri": "frame_12933.png"}, {"time": 6562.754, "image_uri": "frame_12934.png"}, {"time": 6563.261, "image_uri": "frame_12935.png"}, {"time": 6563.769, "image_uri": "frame_12936.png"}, {"time": 6564.276, "image_uri": "frame_12937.png"}, {"time": 6564.783, "image_uri": "frame_12938.png"}, {"time": 6565.291, "image_uri": "frame_12939.png"}, {"time": 6565.798, "image_uri": "frame_12940.png"}, {"time": 6566.306, "image_uri": "frame_12941.png"}, {"time": 6566.813, "image_uri": "frame_12942.png"}, {"time": 6567.321, "image_uri": "frame_12943.png"}, {"time": 6567.828, "image_uri": "frame_12944.png"}, {"time": 6568.336, "image_uri": "frame_12945.png"}, {"time": 6568.843, "image_uri": "frame_12946.png"}, {"time": 6569.35, "image_uri": "frame_12947.png"}, {"time": 6569.858, "image_uri": "frame_12948.png"}, {"time": 6570.365, "image_uri": "frame_12949.png"}, {"time": 6570.873, "image_uri": "frame_12950.png"}, {"time": 6571.38, "image_uri": "frame_12951.png"}, {"time": 6571.888, "image_uri": "frame_12952.png"}, {"time": 6572.395, "image_uri": "frame_12953.png"}, {"time": 6572.903, "image_uri": "frame_12954.png"}, {"time": 6573.41, "image_uri": "frame_12955.png"}, {"time": 6573.917, "image_uri": "frame_12956.png"}, {"time": 6574.425, "image_uri": "frame_12957.png"}, {"time": 6574.932, "image_uri": "frame_12958.png"}, {"time": 6575.44, "image_uri": "frame_12959.png"}, {"time": 6575.947, "image_uri": "frame_12960.png"}, {"time": 6576.455, "image_uri": "frame_12961.png"}, {"time": 6576.962, "image_uri": "frame_12962.png"}, {"time": 6577.47, "image_uri": "frame_12963.png"}, {"time": 6577.977, "image_uri": "frame_12964.png"}, {"time": 6578.484, "image_uri": "frame_12965.png"}, {"time": 6578.992, "image_uri": "frame_12966.png"}, {"time": 6579.499, "image_uri": "frame_12967.png"}, {"time": 6580.007, "image_uri": "frame_12968.png"}, {"time": 6580.514, "image_uri": "frame_12969.png"}, {"time": 6581.022, "image_uri": "frame_12970.png"}, {"time": 6581.529, "image_uri": "frame_12971.png"}, {"time": 6582.037, "image_uri": "frame_12972.png"}, {"time": 6582.544, "image_uri": "frame_12973.png"}, {"time": 6583.051, "image_uri": "frame_12974.png"}, {"time": 6583.559, "image_uri": "frame_12975.png"}, {"time": 6584.066, "image_uri": "frame_12976.png"}, {"time": 6584.574, "image_uri": "frame_12977.png"}, {"time": 6585.081, "image_uri": "frame_12978.png"}, {"time": 6585.589, "image_uri": "frame_12979.png"}, {"time": 6586.096, "image_uri": "frame_12980.png"}, {"time": 6586.604, "image_uri": "frame_12981.png"}, {"time": 6587.111, "image_uri": "frame_12982.png"}, {"time": 6587.618, "image_uri": "frame_12983.png"}, {"time": 6588.126, "image_uri": "frame_12984.png"}, {"time": 6588.633, "image_uri": "frame_12985.png"}, {"time": 6589.141, "image_uri": "frame_12986.png"}, {"time": 6589.648, "image_uri": "frame_12987.png"}, {"time": 6590.156, "image_uri": "frame_12988.png"}, {"time": 6590.663, "image_uri": "frame_12989.png"}, {"time": 6591.171, "image_uri": "frame_12990.png"}, {"time": 6591.678, "image_uri": "frame_12991.png"}, {"time": 6592.185, "image_uri": "frame_12992.png"}, {"time": 6592.693, "image_uri": "frame_12993.png"}, {"time": 6593.2, "image_uri": "frame_12994.png"}, {"time": 6593.708, "image_uri": "frame_12995.png"}, {"time": 6594.215, "image_uri": "frame_12996.png"}, {"time": 6594.723, "image_uri": "frame_12997.png"}, {"time": 6595.23, "image_uri": "frame_12998.png"}, {"time": 6595.737, "image_uri": "frame_12999.png"}, {"time": 6596.245, "image_uri": "frame_13000.png"}, {"time": 6596.752, "image_uri": "frame_13001.png"}, {"time": 6597.26, "image_uri": "frame_13002.png"}, {"time": 6597.767, "image_uri": "frame_13003.png"}, {"time": 6598.275, "image_uri": "frame_13004.png"}, {"time": 6598.782, "image_uri": "frame_13005.png"}, {"time": 6599.29, "image_uri": "frame_13006.png"}, {"time": 6599.797, "image_uri": "frame_13007.png"}, {"time": 6600.304, "image_uri": "frame_13008.png"}, {"time": 6600.812, "image_uri": "frame_13009.png"}, {"time": 6601.319, "image_uri": "frame_13010.png"}, {"time": 6601.827, "image_uri": "frame_13011.png"}, {"time": 6602.334, "image_uri": "frame_13012.png"}, {"time": 6602.842, "image_uri": "frame_13013.png"}, {"time": 6603.349, "image_uri": "frame_13014.png"}, {"time": 6603.857, "image_uri": "frame_13015.png"}, {"time": 6604.364, "image_uri": "frame_13016.png"}, {"time": 6604.871, "image_uri": "frame_13017.png"}, {"time": 6605.379, "image_uri": "frame_13018.png"}, {"time": 6605.886, "image_uri": "frame_13019.png"}, {"time": 6606.394, "image_uri": "frame_13020.png"}, {"time": 6606.901, "image_uri": "frame_13021.png"}, {"time": 6607.409, "image_uri": "frame_13022.png"}, {"time": 6607.916, "image_uri": "frame_13023.png"}, {"time": 6608.424, "image_uri": "frame_13024.png"}, {"time": 6608.931, "image_uri": "frame_13025.png"}, {"time": 6609.438, "image_uri": "frame_13026.png"}, {"time": 6609.946, "image_uri": "frame_13027.png"}, {"time": 6610.453, "image_uri": "frame_13028.png"}, {"time": 6610.961, "image_uri": "frame_13029.png"}, {"time": 6611.468, "image_uri": "frame_13030.png"}, {"time": 6611.976, "image_uri": "frame_13031.png"}, {"time": 6612.483, "image_uri": "frame_13032.png"}, {"time": 6612.991, "image_uri": "frame_13033.png"}, {"time": 6613.498, "image_uri": "frame_13034.png"}, {"time": 6614.005, "image_uri": "frame_13035.png"}, {"time": 6614.513, "image_uri": "frame_13036.png"}, {"time": 6615.02, "image_uri": "frame_13037.png"}, {"time": 6615.528, "image_uri": "frame_13038.png"}, {"time": 6616.035, "image_uri": "frame_13039.png"}, {"time": 6616.543, "image_uri": "frame_13040.png"}, {"time": 6617.05, "image_uri": "frame_13041.png"}, {"time": 6617.558, "image_uri": "frame_13042.png"}, {"time": 6618.065, "image_uri": "frame_13043.png"}, {"time": 6618.572, "image_uri": "frame_13044.png"}, {"time": 6619.08, "image_uri": "frame_13045.png"}, {"time": 6619.587, "image_uri": "frame_13046.png"}, {"time": 6620.095, "image_uri": "frame_13047.png"}, {"time": 6620.602, "image_uri": "frame_13048.png"}, {"time": 6621.11, "image_uri": "frame_13049.png"}, {"time": 6621.617, "image_uri": "frame_13050.png"}, {"time": 6622.124, "image_uri": "frame_13051.png"}, {"time": 6622.632, "image_uri": "frame_13052.png"}, {"time": 6623.139, "image_uri": "frame_13053.png"}, {"time": 6623.647, "image_uri": "frame_13054.png"}, {"time": 6624.154, "image_uri": "frame_13055.png"}, {"time": 6624.662, "image_uri": "frame_13056.png"}, {"time": 6625.169, "image_uri": "frame_13057.png"}, {"time": 6625.677, "image_uri": "frame_13058.png"}, {"time": 6626.184, "image_uri": "frame_13059.png"}, {"time": 6626.691, "image_uri": "frame_13060.png"}, {"time": 6627.199, "image_uri": "frame_13061.png"}, {"time": 6627.706, "image_uri": "frame_13062.png"}, {"time": 6628.214, "image_uri": "frame_13063.png"}, {"time": 6628.721, "image_uri": "frame_13064.png"}, {"time": 6629.229, "image_uri": "frame_13065.png"}, {"time": 6629.736, "image_uri": "frame_13066.png"}, {"time": 6630.244, "image_uri": "frame_13067.png"}, {"time": 6630.751, "image_uri": "frame_13068.png"}, {"time": 6631.258, "image_uri": "frame_13069.png"}, {"time": 6631.766, "image_uri": "frame_13070.png"}, {"time": 6632.273, "image_uri": "frame_13071.png"}, {"time": 6632.781, "image_uri": "frame_13072.png"}, {"time": 6633.288, "image_uri": "frame_13073.png"}, {"time": 6633.796, "image_uri": "frame_13074.png"}, {"time": 6634.303, "image_uri": "frame_13075.png"}, {"time": 6634.811, "image_uri": "frame_13076.png"}, {"time": 6635.318, "image_uri": "frame_13077.png"}, {"time": 6635.825, "image_uri": "frame_13078.png"}, {"time": 6636.333, "image_uri": "frame_13079.png"}, {"time": 6636.84, "image_uri": "frame_13080.png"}, {"time": 6637.348, "image_uri": "frame_13081.png"}, {"time": 6637.855, "image_uri": "frame_13082.png"}, {"time": 6638.363, "image_uri": "frame_13083.png"}, {"time": 6638.87, "image_uri": "frame_13084.png"}, {"time": 6639.378, "image_uri": "frame_13085.png"}, {"time": 6639.885, "image_uri": "frame_13086.png"}, {"time": 6640.392, "image_uri": "frame_13087.png"}, {"time": 6640.9, "image_uri": "frame_13088.png"}, {"time": 6641.407, "image_uri": "frame_13089.png"}, {"time": 6641.915, "image_uri": "frame_13090.png"}, {"time": 6642.422, "image_uri": "frame_13091.png"}, {"time": 6642.93, "image_uri": "frame_13092.png"}, {"time": 6643.437, "image_uri": "frame_13093.png"}, {"time": 6643.945, "image_uri": "frame_13094.png"}, {"time": 6644.452, "image_uri": "frame_13095.png"}, {"time": 6644.959, "image_uri": "frame_13096.png"}, {"time": 6645.467, "image_uri": "frame_13097.png"}, {"time": 6645.974, "image_uri": "frame_13098.png"}, {"time": 6646.482, "image_uri": "frame_13099.png"}, {"time": 6646.989, "image_uri": "frame_13100.png"}, {"time": 6647.497, "image_uri": "frame_13101.png"}, {"time": 6648.004, "image_uri": "frame_13102.png"}, {"time": 6648.512, "image_uri": "frame_13103.png"}, {"time": 6649.019, "image_uri": "frame_13104.png"}, {"time": 6649.526, "image_uri": "frame_13105.png"}, {"time": 6650.034, "image_uri": "frame_13106.png"}, {"time": 6650.541, "image_uri": "frame_13107.png"}, {"time": 6651.049, "image_uri": "frame_13108.png"}, {"time": 6651.556, "image_uri": "frame_13109.png"}, {"time": 6652.064, "image_uri": "frame_13110.png"}, {"time": 6652.571, "image_uri": "frame_13111.png"}, {"time": 6653.078, "image_uri": "frame_13112.png"}, {"time": 6653.586, "image_uri": "frame_13113.png"}, {"time": 6654.093, "image_uri": "frame_13114.png"}, {"time": 6654.601, "image_uri": "frame_13115.png"}, {"time": 6655.108, "image_uri": "frame_13116.png"}, {"time": 6655.616, "image_uri": "frame_13117.png"}, {"time": 6656.123, "image_uri": "frame_13118.png"}, {"time": 6656.631, "image_uri": "frame_13119.png"}, {"time": 6657.138, "image_uri": "frame_13120.png"}, {"time": 6657.645, "image_uri": "frame_13121.png"}, {"time": 6658.153, "image_uri": "frame_13122.png"}, {"time": 6658.66, "image_uri": "frame_13123.png"}, {"time": 6659.168, "image_uri": "frame_13124.png"}, {"time": 6659.675, "image_uri": "frame_13125.png"}, {"time": 6660.183, "image_uri": "frame_13126.png"}, {"time": 6660.69, "image_uri": "frame_13127.png"}, {"time": 6661.198, "image_uri": "frame_13128.png"}, {"time": 6661.705, "image_uri": "frame_13129.png"}, {"time": 6662.212, "image_uri": "frame_13130.png"}, {"time": 6662.72, "image_uri": "frame_13131.png"}, {"time": 6663.227, "image_uri": "frame_13132.png"}, {"time": 6663.735, "image_uri": "frame_13133.png"}, {"time": 6664.242, "image_uri": "frame_13134.png"}, {"time": 6664.75, "image_uri": "frame_13135.png"}, {"time": 6665.257, "image_uri": "frame_13136.png"}, {"time": 6665.765, "image_uri": "frame_13137.png"}, {"time": 6666.272, "image_uri": "frame_13138.png"}, {"time": 6666.779, "image_uri": "frame_13139.png"}, {"time": 6667.287, "image_uri": "frame_13140.png"}, {"time": 6667.794, "image_uri": "frame_13141.png"}, {"time": 6668.302, "image_uri": "frame_13142.png"}, {"time": 6668.809, "image_uri": "frame_13143.png"}, {"time": 6669.317, "image_uri": "frame_13144.png"}, {"time": 6669.824, "image_uri": "frame_13145.png"}, {"time": 6670.332, "image_uri": "frame_13146.png"}, {"time": 6670.839, "image_uri": "frame_13147.png"}, {"time": 6671.346, "image_uri": "frame_13148.png"}, {"time": 6671.854, "image_uri": "frame_13149.png"}, {"time": 6672.361, "image_uri": "frame_13150.png"}, {"time": 6672.869, "image_uri": "frame_13151.png"}, {"time": 6673.376, "image_uri": "frame_13152.png"}, {"time": 6673.884, "image_uri": "frame_13153.png"}, {"time": 6674.391, "image_uri": "frame_13154.png"}, {"time": 6674.899, "image_uri": "frame_13155.png"}, {"time": 6675.406, "image_uri": "frame_13156.png"}, {"time": 6675.913, "image_uri": "frame_13157.png"}, {"time": 6676.421, "image_uri": "frame_13158.png"}, {"time": 6676.928, "image_uri": "frame_13159.png"}, {"time": 6677.436, "image_uri": "frame_13160.png"}, {"time": 6677.943, "image_uri": "frame_13161.png"}, {"time": 6678.451, "image_uri": "frame_13162.png"}, {"time": 6678.958, "image_uri": "frame_13163.png"}, {"time": 6679.465, "image_uri": "frame_13164.png"}, {"time": 6679.973, "image_uri": "frame_13165.png"}, {"time": 6680.48, "image_uri": "frame_13166.png"}, {"time": 6680.988, "image_uri": "frame_13167.png"}, {"time": 6681.495, "image_uri": "frame_13168.png"}, {"time": 6682.003, "image_uri": "frame_13169.png"}, {"time": 6682.51, "image_uri": "frame_13170.png"}, {"time": 6683.018, "image_uri": "frame_13171.png"}, {"time": 6683.525, "image_uri": "frame_13172.png"}, {"time": 6684.032, "image_uri": "frame_13173.png"}, {"time": 6684.54, "image_uri": "frame_13174.png"}, {"time": 6685.047, "image_uri": "frame_13175.png"}, {"time": 6685.555, "image_uri": "frame_13176.png"}, {"time": 6686.062, "image_uri": "frame_13177.png"}, {"time": 6686.57, "image_uri": "frame_13178.png"}, {"time": 6687.077, "image_uri": "frame_13179.png"}, {"time": 6687.585, "image_uri": "frame_13180.png"}, {"time": 6688.092, "image_uri": "frame_13181.png"}, {"time": 6688.599, "image_uri": "frame_13182.png"}, {"time": 6689.107, "image_uri": "frame_13183.png"}, {"time": 6689.614, "image_uri": "frame_13184.png"}, {"time": 6690.122, "image_uri": "frame_13185.png"}, {"time": 6690.629, "image_uri": "frame_13186.png"}, {"time": 6691.137, "image_uri": "frame_13187.png"}, {"time": 6691.644, "image_uri": "frame_13188.png"}, {"time": 6692.152, "image_uri": "frame_13189.png"}, {"time": 6692.659, "image_uri": "frame_13190.png"}, {"time": 6693.166, "image_uri": "frame_13191.png"}, {"time": 6693.674, "image_uri": "frame_13192.png"}, {"time": 6694.181, "image_uri": "frame_13193.png"}, {"time": 6694.689, "image_uri": "frame_13194.png"}, {"time": 6695.196, "image_uri": "frame_13195.png"}, {"time": 6695.704, "image_uri": "frame_13196.png"}, {"time": 6696.211, "image_uri": "frame_13197.png"}, {"time": 6696.719, "image_uri": "frame_13198.png"}, {"time": 6697.226, "image_uri": "frame_13199.png"}, {"time": 6697.733, "image_uri": "frame_13200.png"}, {"time": 6698.241, "image_uri": "frame_13201.png"}, {"time": 6698.748, "image_uri": "frame_13202.png"}, {"time": 6699.256, "image_uri": "frame_13203.png"}, {"time": 6699.763, "image_uri": "frame_13204.png"}, {"time": 6700.271, "image_uri": "frame_13205.png"}, {"time": 6700.778, "image_uri": "frame_13206.png"}, {"time": 6701.286, "image_uri": "frame_13207.png"}, {"time": 6701.793, "image_uri": "frame_13208.png"}, {"time": 6702.3, "image_uri": "frame_13209.png"}, {"time": 6702.808, "image_uri": "frame_13210.png"}, {"time": 6703.315, "image_uri": "frame_13211.png"}, {"time": 6703.823, "image_uri": "frame_13212.png"}, {"time": 6704.33, "image_uri": "frame_13213.png"}, {"time": 6704.838, "image_uri": "frame_13214.png"}, {"time": 6705.345, "image_uri": "frame_13215.png"}, {"time": 6705.853, "image_uri": "frame_13216.png"}, {"time": 6706.36, "image_uri": "frame_13217.png"}, {"time": 6706.867, "image_uri": "frame_13218.png"}, {"time": 6707.375, "image_uri": "frame_13219.png"}, {"time": 6707.882, "image_uri": "frame_13220.png"}, {"time": 6708.39, "image_uri": "frame_13221.png"}, {"time": 6708.897, "image_uri": "frame_13222.png"}, {"time": 6709.405, "image_uri": "frame_13223.png"}, {"time": 6709.912, "image_uri": "frame_13224.png"}, {"time": 6710.419, "image_uri": "frame_13225.png"}, {"time": 6710.927, "image_uri": "frame_13226.png"}, {"time": 6711.434, "image_uri": "frame_13227.png"}, {"time": 6711.942, "image_uri": "frame_13228.png"}, {"time": 6712.449, "image_uri": "frame_13229.png"}, {"time": 6712.957, "image_uri": "frame_13230.png"}, {"time": 6713.464, "image_uri": "frame_13231.png"}, {"time": 6713.972, "image_uri": "frame_13232.png"}, {"time": 6714.479, "image_uri": "frame_13233.png"}, {"time": 6714.986, "image_uri": "frame_13234.png"}, {"time": 6715.494, "image_uri": "frame_13235.png"}, {"time": 6716.001, "image_uri": "frame_13236.png"}, {"time": 6716.509, "image_uri": "frame_13237.png"}, {"time": 6717.016, "image_uri": "frame_13238.png"}, {"time": 6717.524, "image_uri": "frame_13239.png"}, {"time": 6718.031, "image_uri": "frame_13240.png"}, {"time": 6718.539, "image_uri": "frame_13241.png"}, {"time": 6719.046, "image_uri": "frame_13242.png"}, {"time": 6719.553, "image_uri": "frame_13243.png"}, {"time": 6720.061, "image_uri": "frame_13244.png"}, {"time": 6720.568, "image_uri": "frame_13245.png"}, {"time": 6721.076, "image_uri": "frame_13246.png"}, {"time": 6721.583, "image_uri": "frame_13247.png"}, {"time": 6722.091, "image_uri": "frame_13248.png"}, {"time": 6722.598, "image_uri": "frame_13249.png"}, {"time": 6723.106, "image_uri": "frame_13250.png"}, {"time": 6723.613, "image_uri": "frame_13251.png"}, {"time": 6724.12, "image_uri": "frame_13252.png"}, {"time": 6724.628, "image_uri": "frame_13253.png"}, {"time": 6725.135, "image_uri": "frame_13254.png"}, {"time": 6725.643, "image_uri": "frame_13255.png"}, {"time": 6726.15, "image_uri": "frame_13256.png"}, {"time": 6726.658, "image_uri": "frame_13257.png"}, {"time": 6727.165, "image_uri": "frame_13258.png"}, {"time": 6727.673, "image_uri": "frame_13259.png"}, {"time": 6728.18, "image_uri": "frame_13260.png"}, {"time": 6728.687, "image_uri": "frame_13261.png"}, {"time": 6729.195, "image_uri": "frame_13262.png"}, {"time": 6729.702, "image_uri": "frame_13263.png"}, {"time": 6730.21, "image_uri": "frame_13264.png"}, {"time": 6730.717, "image_uri": "frame_13265.png"}, {"time": 6731.225, "image_uri": "frame_13266.png"}, {"time": 6731.732, "image_uri": "frame_13267.png"}, {"time": 6732.24, "image_uri": "frame_13268.png"}, {"time": 6732.747, "image_uri": "frame_13269.png"}, {"time": 6733.254, "image_uri": "frame_13270.png"}, {"time": 6733.762, "image_uri": "frame_13271.png"}, {"time": 6734.269, "image_uri": "frame_13272.png"}, {"time": 6734.777, "image_uri": "frame_13273.png"}, {"time": 6735.284, "image_uri": "frame_13274.png"}, {"time": 6735.792, "image_uri": "frame_13275.png"}, {"time": 6736.299, "image_uri": "frame_13276.png"}, {"time": 6736.806, "image_uri": "frame_13277.png"}, {"time": 6737.314, "image_uri": "frame_13278.png"}, {"time": 6737.821, "image_uri": "frame_13279.png"}, {"time": 6738.329, "image_uri": "frame_13280.png"}, {"time": 6738.836, "image_uri": "frame_13281.png"}, {"time": 6739.344, "image_uri": "frame_13282.png"}, {"time": 6739.851, "image_uri": "frame_13283.png"}, {"time": 6740.359, "image_uri": "frame_13284.png"}, {"time": 6740.866, "image_uri": "frame_13285.png"}, {"time": 6741.373, "image_uri": "frame_13286.png"}, {"time": 6741.881, "image_uri": "frame_13287.png"}, {"time": 6742.388, "image_uri": "frame_13288.png"}, {"time": 6742.896, "image_uri": "frame_13289.png"}, {"time": 6743.403, "image_uri": "frame_13290.png"}, {"time": 6743.911, "image_uri": "frame_13291.png"}, {"time": 6744.418, "image_uri": "frame_13292.png"}, {"time": 6744.926, "image_uri": "frame_13293.png"}, {"time": 6745.433, "image_uri": "frame_13294.png"}, {"time": 6745.94, "image_uri": "frame_13295.png"}, {"time": 6746.448, "image_uri": "frame_13296.png"}, {"time": 6746.955, "image_uri": "frame_13297.png"}, {"time": 6747.463, "image_uri": "frame_13298.png"}, {"time": 6747.97, "image_uri": "frame_13299.png"}, {"time": 6748.478, "image_uri": "frame_13300.png"}, {"time": 6748.985, "image_uri": "frame_13301.png"}, {"time": 6749.493, "image_uri": "frame_13302.png"}, {"time": 6750.0, "image_uri": "frame_13303.png"}, {"time": 6750.507, "image_uri": "frame_13304.png"}, {"time": 6751.015, "image_uri": "frame_13305.png"}, {"time": 6751.522, "image_uri": "frame_13306.png"}, {"time": 6752.03, "image_uri": "frame_13307.png"}, {"time": 6752.537, "image_uri": "frame_13308.png"}, {"time": 6753.045, "image_uri": "frame_13309.png"}, {"time": 6753.552, "image_uri": "frame_13310.png"}, {"time": 6754.06, "image_uri": "frame_13311.png"}, {"time": 6754.567, "image_uri": "frame_13312.png"}, {"time": 6755.074, "image_uri": "frame_13313.png"}, {"time": 6755.582, "image_uri": "frame_13314.png"}, {"time": 6756.089, "image_uri": "frame_13315.png"}, {"time": 6756.597, "image_uri": "frame_13316.png"}, {"time": 6757.104, "image_uri": "frame_13317.png"}, {"time": 6757.612, "image_uri": "frame_13318.png"}, {"time": 6758.119, "image_uri": "frame_13319.png"}, {"time": 6758.627, "image_uri": "frame_13320.png"}, {"time": 6759.134, "image_uri": "frame_13321.png"}, {"time": 6759.641, "image_uri": "frame_13322.png"}, {"time": 6760.149, "image_uri": "frame_13323.png"}, {"time": 6760.656, "image_uri": "frame_13324.png"}, {"time": 6761.164, "image_uri": "frame_13325.png"}, {"time": 6761.671, "image_uri": "frame_13326.png"}, {"time": 6762.179, "image_uri": "frame_13327.png"}, {"time": 6762.686, "image_uri": "frame_13328.png"}, {"time": 6763.194, "image_uri": "frame_13329.png"}, {"time": 6763.701, "image_uri": "frame_13330.png"}, {"time": 6764.208, "image_uri": "frame_13331.png"}, {"time": 6764.716, "image_uri": "frame_13332.png"}, {"time": 6765.223, "image_uri": "frame_13333.png"}, {"time": 6765.731, "image_uri": "frame_13334.png"}, {"time": 6766.238, "image_uri": "frame_13335.png"}, {"time": 6766.746, "image_uri": "frame_13336.png"}, {"time": 6767.253, "image_uri": "frame_13337.png"}, {"time": 6767.76, "image_uri": "frame_13338.png"}, {"time": 6768.268, "image_uri": "frame_13339.png"}, {"time": 6768.775, "image_uri": "frame_13340.png"}, {"time": 6769.283, "image_uri": "frame_13341.png"}, {"time": 6769.79, "image_uri": "frame_13342.png"}, {"time": 6770.298, "image_uri": "frame_13343.png"}, {"time": 6770.805, "image_uri": "frame_13344.png"}, {"time": 6771.313, "image_uri": "frame_13345.png"}, {"time": 6771.82, "image_uri": "frame_13346.png"}, {"time": 6772.327, "image_uri": "frame_13347.png"}, {"time": 6772.835, "image_uri": "frame_13348.png"}, {"time": 6773.342, "image_uri": "frame_13349.png"}, {"time": 6773.85, "image_uri": "frame_13350.png"}, {"time": 6774.357, "image_uri": "frame_13351.png"}, {"time": 6774.865, "image_uri": "frame_13352.png"}, {"time": 6775.372, "image_uri": "frame_13353.png"}, {"time": 6775.88, "image_uri": "frame_13354.png"}, {"time": 6776.387, "image_uri": "frame_13355.png"}, {"time": 6776.894, "image_uri": "frame_13356.png"}, {"time": 6777.402, "image_uri": "frame_13357.png"}, {"time": 6777.909, "image_uri": "frame_13358.png"}, {"time": 6778.417, "image_uri": "frame_13359.png"}, {"time": 6778.924, "image_uri": "frame_13360.png"}, {"time": 6779.432, "image_uri": "frame_13361.png"}, {"time": 6779.939, "image_uri": "frame_13362.png"}, {"time": 6780.447, "image_uri": "frame_13363.png"}, {"time": 6780.954, "image_uri": "frame_13364.png"}, {"time": 6781.461, "image_uri": "frame_13365.png"}, {"time": 6781.969, "image_uri": "frame_13366.png"}, {"time": 6782.476, "image_uri": "frame_13367.png"}, {"time": 6782.984, "image_uri": "frame_13368.png"}, {"time": 6783.491, "image_uri": "frame_13369.png"}, {"time": 6783.999, "image_uri": "frame_13370.png"}, {"time": 6784.506, "image_uri": "frame_13371.png"}, {"time": 6785.014, "image_uri": "frame_13372.png"}, {"time": 6785.521, "image_uri": "frame_13373.png"}, {"time": 6786.028, "image_uri": "frame_13374.png"}, {"time": 6786.536, "image_uri": "frame_13375.png"}, {"time": 6787.043, "image_uri": "frame_13376.png"}, {"time": 6787.551, "image_uri": "frame_13377.png"}, {"time": 6788.058, "image_uri": "frame_13378.png"}, {"time": 6788.566, "image_uri": "frame_13379.png"}, {"time": 6789.073, "image_uri": "frame_13380.png"}, {"time": 6789.581, "image_uri": "frame_13381.png"}, {"time": 6790.088, "image_uri": "frame_13382.png"}, {"time": 6790.595, "image_uri": "frame_13383.png"}, {"time": 6791.103, "image_uri": "frame_13384.png"}, {"time": 6791.61, "image_uri": "frame_13385.png"}, {"time": 6792.118, "image_uri": "frame_13386.png"}, {"time": 6792.625, "image_uri": "frame_13387.png"}, {"time": 6793.133, "image_uri": "frame_13388.png"}, {"time": 6793.64, "image_uri": "frame_13389.png"}, {"time": 6794.147, "image_uri": "frame_13390.png"}, {"time": 6794.655, "image_uri": "frame_13391.png"}, {"time": 6795.162, "image_uri": "frame_13392.png"}, {"time": 6795.67, "image_uri": "frame_13393.png"}, {"time": 6796.177, "image_uri": "frame_13394.png"}, {"time": 6796.685, "image_uri": "frame_13395.png"}, {"time": 6797.192, "image_uri": "frame_13396.png"}, {"time": 6797.7, "image_uri": "frame_13397.png"}, {"time": 6798.207, "image_uri": "frame_13398.png"}, {"time": 6798.714, "image_uri": "frame_13399.png"}, {"time": 6799.222, "image_uri": "frame_13400.png"}, {"time": 6799.729, "image_uri": "frame_13401.png"}, {"time": 6800.237, "image_uri": "frame_13402.png"}, {"time": 6800.744, "image_uri": "frame_13403.png"}, {"time": 6801.252, "image_uri": "frame_13404.png"}, {"time": 6801.759, "image_uri": "frame_13405.png"}, {"time": 6802.267, "image_uri": "frame_13406.png"}, {"time": 6802.774, "image_uri": "frame_13407.png"}, {"time": 6803.281, "image_uri": "frame_13408.png"}, {"time": 6803.789, "image_uri": "frame_13409.png"}, {"time": 6804.296, "image_uri": "frame_13410.png"}, {"time": 6804.804, "image_uri": "frame_13411.png"}, {"time": 6805.311, "image_uri": "frame_13412.png"}, {"time": 6805.819, "image_uri": "frame_13413.png"}, {"time": 6806.326, "image_uri": "frame_13414.png"}, {"time": 6806.834, "image_uri": "frame_13415.png"}, {"time": 6807.341, "image_uri": "frame_13416.png"}, {"time": 6807.848, "image_uri": "frame_13417.png"}, {"time": 6808.356, "image_uri": "frame_13418.png"}, {"time": 6808.863, "image_uri": "frame_13419.png"}, {"time": 6809.371, "image_uri": "frame_13420.png"}, {"time": 6809.878, "image_uri": "frame_13421.png"}, {"time": 6810.386, "image_uri": "frame_13422.png"}, {"time": 6810.893, "image_uri": "frame_13423.png"}, {"time": 6811.401, "image_uri": "frame_13424.png"}, {"time": 6811.908, "image_uri": "frame_13425.png"}, {"time": 6812.415, "image_uri": "frame_13426.png"}, {"time": 6812.923, "image_uri": "frame_13427.png"}, {"time": 6813.43, "image_uri": "frame_13428.png"}, {"time": 6813.938, "image_uri": "frame_13429.png"}, {"time": 6814.445, "image_uri": "frame_13430.png"}, {"time": 6814.953, "image_uri": "frame_13431.png"}, {"time": 6815.46, "image_uri": "frame_13432.png"}, {"time": 6815.968, "image_uri": "frame_13433.png"}, {"time": 6816.475, "image_uri": "frame_13434.png"}, {"time": 6816.982, "image_uri": "frame_13435.png"}, {"time": 6817.49, "image_uri": "frame_13436.png"}, {"time": 6817.997, "image_uri": "frame_13437.png"}, {"time": 6818.505, "image_uri": "frame_13438.png"}, {"time": 6819.012, "image_uri": "frame_13439.png"}, {"time": 6819.52, "image_uri": "frame_13440.png"}, {"time": 6820.027, "image_uri": "frame_13441.png"}, {"time": 6820.535, "image_uri": "frame_13442.png"}, {"time": 6821.042, "image_uri": "frame_13443.png"}, {"time": 6821.549, "image_uri": "frame_13444.png"}, {"time": 6822.057, "image_uri": "frame_13445.png"}, {"time": 6822.564, "image_uri": "frame_13446.png"}, {"time": 6823.072, "image_uri": "frame_13447.png"}, {"time": 6823.579, "image_uri": "frame_13448.png"}, {"time": 6824.087, "image_uri": "frame_13449.png"}, {"time": 6824.594, "image_uri": "frame_13450.png"}, {"time": 6825.101, "image_uri": "frame_13451.png"}, {"time": 6825.609, "image_uri": "frame_13452.png"}, {"time": 6826.116, "image_uri": "frame_13453.png"}, {"time": 6826.624, "image_uri": "frame_13454.png"}, {"time": 6827.131, "image_uri": "frame_13455.png"}, {"time": 6827.639, "image_uri": "frame_13456.png"}, {"time": 6828.146, "image_uri": "frame_13457.png"}, {"time": 6828.654, "image_uri": "frame_13458.png"}, {"time": 6829.161, "image_uri": "frame_13459.png"}, {"time": 6829.668, "image_uri": "frame_13460.png"}, {"time": 6830.176, "image_uri": "frame_13461.png"}, {"time": 6830.683, "image_uri": "frame_13462.png"}, {"time": 6831.191, "image_uri": "frame_13463.png"}, {"time": 6831.698, "image_uri": "frame_13464.png"}, {"time": 6832.206, "image_uri": "frame_13465.png"}, {"time": 6832.713, "image_uri": "frame_13466.png"}, {"time": 6833.221, "image_uri": "frame_13467.png"}, {"time": 6833.728, "image_uri": "frame_13468.png"}, {"time": 6834.235, "image_uri": "frame_13469.png"}, {"time": 6834.743, "image_uri": "frame_13470.png"}, {"time": 6835.25, "image_uri": "frame_13471.png"}, {"time": 6835.758, "image_uri": "frame_13472.png"}, {"time": 6836.265, "image_uri": "frame_13473.png"}, {"time": 6836.773, "image_uri": "frame_13474.png"}, {"time": 6837.28, "image_uri": "frame_13475.png"}, {"time": 6837.788, "image_uri": "frame_13476.png"}, {"time": 6838.295, "image_uri": "frame_13477.png"}, {"time": 6838.802, "image_uri": "frame_13478.png"}, {"time": 6839.31, "image_uri": "frame_13479.png"}, {"time": 6839.817, "image_uri": "frame_13480.png"}, {"time": 6840.325, "image_uri": "frame_13481.png"}, {"time": 6840.832, "image_uri": "frame_13482.png"}, {"time": 6841.34, "image_uri": "frame_13483.png"}, {"time": 6841.847, "image_uri": "frame_13484.png"}, {"time": 6842.355, "image_uri": "frame_13485.png"}, {"time": 6842.862, "image_uri": "frame_13486.png"}, {"time": 6843.369, "image_uri": "frame_13487.png"}, {"time": 6843.877, "image_uri": "frame_13488.png"}, {"time": 6844.384, "image_uri": "frame_13489.png"}, {"time": 6844.892, "image_uri": "frame_13490.png"}, {"time": 6845.399, "image_uri": "frame_13491.png"}, {"time": 6845.907, "image_uri": "frame_13492.png"}, {"time": 6846.414, "image_uri": "frame_13493.png"}, {"time": 6846.922, "image_uri": "frame_13494.png"}, {"time": 6847.429, "image_uri": "frame_13495.png"}, {"time": 6847.936, "image_uri": "frame_13496.png"}, {"time": 6848.444, "image_uri": "frame_13497.png"}, {"time": 6848.951, "image_uri": "frame_13498.png"}, {"time": 6849.459, "image_uri": "frame_13499.png"}, {"time": 6849.966, "image_uri": "frame_13500.png"}, {"time": 6850.474, "image_uri": "frame_13501.png"}, {"time": 6850.981, "image_uri": "frame_13502.png"}, {"time": 6851.488, "image_uri": "frame_13503.png"}, {"time": 6851.996, "image_uri": "frame_13504.png"}, {"time": 6852.503, "image_uri": "frame_13505.png"}, {"time": 6853.011, "image_uri": "frame_13506.png"}, {"time": 6853.518, "image_uri": "frame_13507.png"}, {"time": 6854.026, "image_uri": "frame_13508.png"}, {"time": 6854.533, "image_uri": "frame_13509.png"}, {"time": 6855.041, "image_uri": "frame_13510.png"}, {"time": 6855.548, "image_uri": "frame_13511.png"}, {"time": 6856.055, "image_uri": "frame_13512.png"}, {"time": 6856.563, "image_uri": "frame_13513.png"}, {"time": 6857.07, "image_uri": "frame_13514.png"}, {"time": 6857.578, "image_uri": "frame_13515.png"}, {"time": 6858.085, "image_uri": "frame_13516.png"}, {"time": 6858.593, "image_uri": "frame_13517.png"}, {"time": 6859.1, "image_uri": "frame_13518.png"}, {"time": 6859.608, "image_uri": "frame_13519.png"}, {"time": 6860.115, "image_uri": "frame_13520.png"}, {"time": 6860.622, "image_uri": "frame_13521.png"}, {"time": 6861.13, "image_uri": "frame_13522.png"}, {"time": 6861.637, "image_uri": "frame_13523.png"}, {"time": 6862.145, "image_uri": "frame_13524.png"}, {"time": 6862.652, "image_uri": "frame_13525.png"}, {"time": 6863.16, "image_uri": "frame_13526.png"}, {"time": 6863.667, "image_uri": "frame_13527.png"}, {"time": 6864.175, "image_uri": "frame_13528.png"}, {"time": 6864.682, "image_uri": "frame_13529.png"}, {"time": 6865.189, "image_uri": "frame_13530.png"}, {"time": 6865.697, "image_uri": "frame_13531.png"}, {"time": 6866.204, "image_uri": "frame_13532.png"}, {"time": 6866.712, "image_uri": "frame_13533.png"}, {"time": 6867.219, "image_uri": "frame_13534.png"}, {"time": 6867.727, "image_uri": "frame_13535.png"}, {"time": 6868.234, "image_uri": "frame_13536.png"}, {"time": 6868.742, "image_uri": "frame_13537.png"}, {"time": 6869.249, "image_uri": "frame_13538.png"}, {"time": 6869.756, "image_uri": "frame_13539.png"}, {"time": 6870.264, "image_uri": "frame_13540.png"}, {"time": 6870.771, "image_uri": "frame_13541.png"}, {"time": 6871.279, "image_uri": "frame_13542.png"}, {"time": 6871.786, "image_uri": "frame_13543.png"}, {"time": 6872.294, "image_uri": "frame_13544.png"}, {"time": 6872.801, "image_uri": "frame_13545.png"}, {"time": 6873.309, "image_uri": "frame_13546.png"}, {"time": 6873.816, "image_uri": "frame_13547.png"}, {"time": 6874.323, "image_uri": "frame_13548.png"}, {"time": 6874.831, "image_uri": "frame_13549.png"}, {"time": 6875.338, "image_uri": "frame_13550.png"}, {"time": 6875.846, "image_uri": "frame_13551.png"}, {"time": 6876.353, "image_uri": "frame_13552.png"}, {"time": 6876.861, "image_uri": "frame_13553.png"}, {"time": 6877.368, "image_uri": "frame_13554.png"}, {"time": 6877.876, "image_uri": "frame_13555.png"}, {"time": 6878.383, "image_uri": "frame_13556.png"}, {"time": 6878.89, "image_uri": "frame_13557.png"}, {"time": 6879.398, "image_uri": "frame_13558.png"}, {"time": 6879.905, "image_uri": "frame_13559.png"}, {"time": 6880.413, "image_uri": "frame_13560.png"}, {"time": 6880.92, "image_uri": "frame_13561.png"}, {"time": 6881.428, "image_uri": "frame_13562.png"}, {"time": 6881.935, "image_uri": "frame_13563.png"}, {"time": 6882.442, "image_uri": "frame_13564.png"}, {"time": 6882.95, "image_uri": "frame_13565.png"}, {"time": 6883.457, "image_uri": "frame_13566.png"}, {"time": 6883.965, "image_uri": "frame_13567.png"}, {"time": 6884.472, "image_uri": "frame_13568.png"}, {"time": 6884.98, "image_uri": "frame_13569.png"}, {"time": 6885.487, "image_uri": "frame_13570.png"}, {"time": 6885.995, "image_uri": "frame_13571.png"}, {"time": 6886.502, "image_uri": "frame_13572.png"}, {"time": 6887.009, "image_uri": "frame_13573.png"}, {"time": 6887.517, "image_uri": "frame_13574.png"}, {"time": 6888.024, "image_uri": "frame_13575.png"}, {"time": 6888.532, "image_uri": "frame_13576.png"}, {"time": 6889.039, "image_uri": "frame_13577.png"}, {"time": 6889.547, "image_uri": "frame_13578.png"}, {"time": 6890.054, "image_uri": "frame_13579.png"}, {"time": 6890.562, "image_uri": "frame_13580.png"}, {"time": 6891.069, "image_uri": "frame_13581.png"}, {"time": 6891.576, "image_uri": "frame_13582.png"}, {"time": 6892.084, "image_uri": "frame_13583.png"}, {"time": 6892.591, "image_uri": "frame_13584.png"}, {"time": 6893.099, "image_uri": "frame_13585.png"}, {"time": 6893.606, "image_uri": "frame_13586.png"}, {"time": 6894.114, "image_uri": "frame_13587.png"}, {"time": 6894.621, "image_uri": "frame_13588.png"}, {"time": 6895.129, "image_uri": "frame_13589.png"}, {"time": 6895.636, "image_uri": "frame_13590.png"}, {"time": 6896.143, "image_uri": "frame_13591.png"}, {"time": 6896.651, "image_uri": "frame_13592.png"}, {"time": 6897.158, "image_uri": "frame_13593.png"}, {"time": 6897.666, "image_uri": "frame_13594.png"}, {"time": 6898.173, "image_uri": "frame_13595.png"}, {"time": 6898.681, "image_uri": "frame_13596.png"}, {"time": 6899.188, "image_uri": "frame_13597.png"}, {"time": 6899.696, "image_uri": "frame_13598.png"}, {"time": 6900.203, "image_uri": "frame_13599.png"}, {"time": 6900.71, "image_uri": "frame_13600.png"}, {"time": 6901.218, "image_uri": "frame_13601.png"}, {"time": 6901.725, "image_uri": "frame_13602.png"}, {"time": 6902.233, "image_uri": "frame_13603.png"}, {"time": 6902.74, "image_uri": "frame_13604.png"}, {"time": 6903.248, "image_uri": "frame_13605.png"}, {"time": 6903.755, "image_uri": "frame_13606.png"}, {"time": 6904.263, "image_uri": "frame_13607.png"}, {"time": 6904.77, "image_uri": "frame_13608.png"}, {"time": 6905.277, "image_uri": "frame_13609.png"}, {"time": 6905.785, "image_uri": "frame_13610.png"}, {"time": 6906.292, "image_uri": "frame_13611.png"}, {"time": 6906.8, "image_uri": "frame_13612.png"}, {"time": 6907.307, "image_uri": "frame_13613.png"}, {"time": 6907.815, "image_uri": "frame_13614.png"}, {"time": 6908.322, "image_uri": "frame_13615.png"}, {"time": 6908.829, "image_uri": "frame_13616.png"}, {"time": 6909.337, "image_uri": "frame_13617.png"}, {"time": 6909.844, "image_uri": "frame_13618.png"}, {"time": 6910.352, "image_uri": "frame_13619.png"}, {"time": 6910.859, "image_uri": "frame_13620.png"}, {"time": 6911.367, "image_uri": "frame_13621.png"}, {"time": 6911.874, "image_uri": "frame_13622.png"}, {"time": 6912.382, "image_uri": "frame_13623.png"}, {"time": 6912.889, "image_uri": "frame_13624.png"}, {"time": 6913.396, "image_uri": "frame_13625.png"}, {"time": 6913.904, "image_uri": "frame_13626.png"}, {"time": 6914.411, "image_uri": "frame_13627.png"}, {"time": 6914.919, "image_uri": "frame_13628.png"}, {"time": 6915.426, "image_uri": "frame_13629.png"}, {"time": 6915.934, "image_uri": "frame_13630.png"}, {"time": 6916.441, "image_uri": "frame_13631.png"}, {"time": 6916.949, "image_uri": "frame_13632.png"}, {"time": 6917.456, "image_uri": "frame_13633.png"}, {"time": 6917.963, "image_uri": "frame_13634.png"}, {"time": 6918.471, "image_uri": "frame_13635.png"}, {"time": 6918.978, "image_uri": "frame_13636.png"}, {"time": 6919.486, "image_uri": "frame_13637.png"}, {"time": 6919.993, "image_uri": "frame_13638.png"}, {"time": 6920.501, "image_uri": "frame_13639.png"}, {"time": 6921.008, "image_uri": "frame_13640.png"}, {"time": 6921.516, "image_uri": "frame_13641.png"}, {"time": 6922.023, "image_uri": "frame_13642.png"}, {"time": 6922.53, "image_uri": "frame_13643.png"}, {"time": 6923.038, "image_uri": "frame_13644.png"}, {"time": 6923.545, "image_uri": "frame_13645.png"}, {"time": 6924.053, "image_uri": "frame_13646.png"}, {"time": 6924.56, "image_uri": "frame_13647.png"}, {"time": 6925.068, "image_uri": "frame_13648.png"}, {"time": 6925.575, "image_uri": "frame_13649.png"}, {"time": 6926.083, "image_uri": "frame_13650.png"}, {"time": 6926.59, "image_uri": "frame_13651.png"}, {"time": 6927.097, "image_uri": "frame_13652.png"}, {"time": 6927.605, "image_uri": "frame_13653.png"}, {"time": 6928.112, "image_uri": "frame_13654.png"}, {"time": 6928.62, "image_uri": "frame_13655.png"}, {"time": 6929.127, "image_uri": "frame_13656.png"}, {"time": 6929.635, "image_uri": "frame_13657.png"}, {"time": 6930.142, "image_uri": "frame_13658.png"}, {"time": 6930.65, "image_uri": "frame_13659.png"}, {"time": 6931.157, "image_uri": "frame_13660.png"}, {"time": 6931.664, "image_uri": "frame_13661.png"}, {"time": 6932.172, "image_uri": "frame_13662.png"}, {"time": 6932.679, "image_uri": "frame_13663.png"}, {"time": 6933.187, "image_uri": "frame_13664.png"}, {"time": 6933.694, "image_uri": "frame_13665.png"}, {"time": 6934.202, "image_uri": "frame_13666.png"}, {"time": 6934.709, "image_uri": "frame_13667.png"}, {"time": 6935.217, "image_uri": "frame_13668.png"}, {"time": 6935.724, "image_uri": "frame_13669.png"}, {"time": 6936.231, "image_uri": "frame_13670.png"}, {"time": 6936.739, "image_uri": "frame_13671.png"}, {"time": 6937.246, "image_uri": "frame_13672.png"}, {"time": 6937.754, "image_uri": "frame_13673.png"}, {"time": 6938.261, "image_uri": "frame_13674.png"}, {"time": 6938.769, "image_uri": "frame_13675.png"}, {"time": 6939.276, "image_uri": "frame_13676.png"}, {"time": 6939.783, "image_uri": "frame_13677.png"}, {"time": 6940.291, "image_uri": "frame_13678.png"}, {"time": 6940.798, "image_uri": "frame_13679.png"}, {"time": 6941.306, "image_uri": "frame_13680.png"}, {"time": 6941.813, "image_uri": "frame_13681.png"}, {"time": 6942.321, "image_uri": "frame_13682.png"}, {"time": 6942.828, "image_uri": "frame_13683.png"}, {"time": 6943.336, "image_uri": "frame_13684.png"}, {"time": 6943.843, "image_uri": "frame_13685.png"}, {"time": 6944.35, "image_uri": "frame_13686.png"}, {"time": 6944.858, "image_uri": "frame_13687.png"}, {"time": 6945.365, "image_uri": "frame_13688.png"}, {"time": 6945.873, "image_uri": "frame_13689.png"}, {"time": 6946.38, "image_uri": "frame_13690.png"}, {"time": 6946.888, "image_uri": "frame_13691.png"}, {"time": 6947.395, "image_uri": "frame_13692.png"}, {"time": 6947.903, "image_uri": "frame_13693.png"}, {"time": 6948.41, "image_uri": "frame_13694.png"}, {"time": 6948.917, "image_uri": "frame_13695.png"}, {"time": 6949.425, "image_uri": "frame_13696.png"}, {"time": 6949.932, "image_uri": "frame_13697.png"}, {"time": 6950.44, "image_uri": "frame_13698.png"}, {"time": 6950.947, "image_uri": "frame_13699.png"}, {"time": 6951.455, "image_uri": "frame_13700.png"}, {"time": 6951.962, "image_uri": "frame_13701.png"}, {"time": 6952.47, "image_uri": "frame_13702.png"}, {"time": 6952.977, "image_uri": "frame_13703.png"}, {"time": 6953.484, "image_uri": "frame_13704.png"}, {"time": 6953.992, "image_uri": "frame_13705.png"}, {"time": 6954.499, "image_uri": "frame_13706.png"}, {"time": 6955.007, "image_uri": "frame_13707.png"}, {"time": 6955.514, "image_uri": "frame_13708.png"}, {"time": 6956.022, "image_uri": "frame_13709.png"}, {"time": 6956.529, "image_uri": "frame_13710.png"}, {"time": 6957.037, "image_uri": "frame_13711.png"}, {"time": 6957.544, "image_uri": "frame_13712.png"}, {"time": 6958.051, "image_uri": "frame_13713.png"}, {"time": 6958.559, "image_uri": "frame_13714.png"}, {"time": 6959.066, "image_uri": "frame_13715.png"}, {"time": 6959.574, "image_uri": "frame_13716.png"}, {"time": 6960.081, "image_uri": "frame_13717.png"}, {"time": 6960.589, "image_uri": "frame_13718.png"}, {"time": 6961.096, "image_uri": "frame_13719.png"}, {"time": 6961.604, "image_uri": "frame_13720.png"}, {"time": 6962.111, "image_uri": "frame_13721.png"}, {"time": 6962.618, "image_uri": "frame_13722.png"}, {"time": 6963.126, "image_uri": "frame_13723.png"}, {"time": 6963.633, "image_uri": "frame_13724.png"}, {"time": 6964.141, "image_uri": "frame_13725.png"}, {"time": 6964.648, "image_uri": "frame_13726.png"}, {"time": 6965.156, "image_uri": "frame_13727.png"}, {"time": 6965.663, "image_uri": "frame_13728.png"}, {"time": 6966.171, "image_uri": "frame_13729.png"}, {"time": 6966.678, "image_uri": "frame_13730.png"}, {"time": 6967.185, "image_uri": "frame_13731.png"}, {"time": 6967.693, "image_uri": "frame_13732.png"}, {"time": 6968.2, "image_uri": "frame_13733.png"}, {"time": 6968.708, "image_uri": "frame_13734.png"}, {"time": 6969.215, "image_uri": "frame_13735.png"}, {"time": 6969.723, "image_uri": "frame_13736.png"}, {"time": 6970.23, "image_uri": "frame_13737.png"}, {"time": 6970.737, "image_uri": "frame_13738.png"}, {"time": 6971.245, "image_uri": "frame_13739.png"}, {"time": 6971.752, "image_uri": "frame_13740.png"}, {"time": 6972.26, "image_uri": "frame_13741.png"}, {"time": 6972.767, "image_uri": "frame_13742.png"}, {"time": 6973.275, "image_uri": "frame_13743.png"}, {"time": 6973.782, "image_uri": "frame_13744.png"}, {"time": 6974.29, "image_uri": "frame_13745.png"}, {"time": 6974.797, "image_uri": "frame_13746.png"}, {"time": 6975.304, "image_uri": "frame_13747.png"}, {"time": 6975.812, "image_uri": "frame_13748.png"}, {"time": 6976.319, "image_uri": "frame_13749.png"}, {"time": 6976.827, "image_uri": "frame_13750.png"}, {"time": 6977.334, "image_uri": "frame_13751.png"}, {"time": 6977.842, "image_uri": "frame_13752.png"}, {"time": 6978.349, "image_uri": "frame_13753.png"}, {"time": 6978.857, "image_uri": "frame_13754.png"}, {"time": 6979.364, "image_uri": "frame_13755.png"}, {"time": 6979.871, "image_uri": "frame_13756.png"}, {"time": 6980.379, "image_uri": "frame_13757.png"}, {"time": 6980.886, "image_uri": "frame_13758.png"}, {"time": 6981.394, "image_uri": "frame_13759.png"}, {"time": 6981.901, "image_uri": "frame_13760.png"}, {"time": 6982.409, "image_uri": "frame_13761.png"}, {"time": 6982.916, "image_uri": "frame_13762.png"}, {"time": 6983.424, "image_uri": "frame_13763.png"}, {"time": 6983.931, "image_uri": "frame_13764.png"}, {"time": 6984.438, "image_uri": "frame_13765.png"}, {"time": 6984.946, "image_uri": "frame_13766.png"}, {"time": 6985.453, "image_uri": "frame_13767.png"}, {"time": 6985.961, "image_uri": "frame_13768.png"}, {"time": 6986.468, "image_uri": "frame_13769.png"}, {"time": 6986.976, "image_uri": "frame_13770.png"}, {"time": 6987.483, "image_uri": "frame_13771.png"}, {"time": 6987.991, "image_uri": "frame_13772.png"}, {"time": 6988.498, "image_uri": "frame_13773.png"}, {"time": 6989.005, "image_uri": "frame_13774.png"}, {"time": 6989.513, "image_uri": "frame_13775.png"}, {"time": 6990.02, "image_uri": "frame_13776.png"}, {"time": 6990.528, "image_uri": "frame_13777.png"}, {"time": 6991.035, "image_uri": "frame_13778.png"}, {"time": 6991.543, "image_uri": "frame_13779.png"}, {"time": 6992.05, "image_uri": "frame_13780.png"}, {"time": 6992.558, "image_uri": "frame_13781.png"}, {"time": 6993.065, "image_uri": "frame_13782.png"}, {"time": 6993.572, "image_uri": "frame_13783.png"}, {"time": 6994.08, "image_uri": "frame_13784.png"}, {"time": 6994.587, "image_uri": "frame_13785.png"}, {"time": 6995.095, "image_uri": "frame_13786.png"}, {"time": 6995.602, "image_uri": "frame_13787.png"}, {"time": 6996.11, "image_uri": "frame_13788.png"}, {"time": 6996.617, "image_uri": "frame_13789.png"}, {"time": 6997.124, "image_uri": "frame_13790.png"}, {"time": 6997.632, "image_uri": "frame_13791.png"}, {"time": 6998.139, "image_uri": "frame_13792.png"}, {"time": 6998.647, "image_uri": "frame_13793.png"}, {"time": 6999.154, "image_uri": "frame_13794.png"}, {"time": 6999.662, "image_uri": "frame_13795.png"}, {"time": 7000.169, "image_uri": "frame_13796.png"}, {"time": 7000.677, "image_uri": "frame_13797.png"}, {"time": 7001.184, "image_uri": "frame_13798.png"}, {"time": 7001.691, "image_uri": "frame_13799.png"}, {"time": 7002.199, "image_uri": "frame_13800.png"}, {"time": 7002.706, "image_uri": "frame_13801.png"}, {"time": 7003.214, "image_uri": "frame_13802.png"}, {"time": 7003.721, "image_uri": "frame_13803.png"}, {"time": 7004.229, "image_uri": "frame_13804.png"}, {"time": 7004.736, "image_uri": "frame_13805.png"}, {"time": 7005.244, "image_uri": "frame_13806.png"}, {"time": 7005.751, "image_uri": "frame_13807.png"}, {"time": 7006.258, "image_uri": "frame_13808.png"}, {"time": 7006.766, "image_uri": "frame_13809.png"}, {"time": 7007.273, "image_uri": "frame_13810.png"}, {"time": 7007.781, "image_uri": "frame_13811.png"}, {"time": 7008.288, "image_uri": "frame_13812.png"}, {"time": 7008.796, "image_uri": "frame_13813.png"}, {"time": 7009.303, "image_uri": "frame_13814.png"}, {"time": 7009.811, "image_uri": "frame_13815.png"}, {"time": 7010.318, "image_uri": "frame_13816.png"}, {"time": 7010.825, "image_uri": "frame_13817.png"}, {"time": 7011.333, "image_uri": "frame_13818.png"}, {"time": 7011.84, "image_uri": "frame_13819.png"}, {"time": 7012.348, "image_uri": "frame_13820.png"}, {"time": 7012.855, "image_uri": "frame_13821.png"}, {"time": 7013.363, "image_uri": "frame_13822.png"}, {"time": 7013.87, "image_uri": "frame_13823.png"}, {"time": 7014.378, "image_uri": "frame_13824.png"}, {"time": 7014.885, "image_uri": "frame_13825.png"}, {"time": 7015.392, "image_uri": "frame_13826.png"}, {"time": 7015.9, "image_uri": "frame_13827.png"}, {"time": 7016.407, "image_uri": "frame_13828.png"}, {"time": 7016.915, "image_uri": "frame_13829.png"}, {"time": 7017.422, "image_uri": "frame_13830.png"}, {"time": 7017.93, "image_uri": "frame_13831.png"}, {"time": 7018.437, "image_uri": "frame_13832.png"}, {"time": 7018.945, "image_uri": "frame_13833.png"}, {"time": 7019.452, "image_uri": "frame_13834.png"}, {"time": 7019.959, "image_uri": "frame_13835.png"}, {"time": 7020.467, "image_uri": "frame_13836.png"}, {"time": 7020.974, "image_uri": "frame_13837.png"}, {"time": 7021.482, "image_uri": "frame_13838.png"}, {"time": 7021.989, "image_uri": "frame_13839.png"}, {"time": 7022.497, "image_uri": "frame_13840.png"}, {"time": 7023.004, "image_uri": "frame_13841.png"}, {"time": 7023.512, "image_uri": "frame_13842.png"}, {"time": 7024.019, "image_uri": "frame_13843.png"}, {"time": 7024.526, "image_uri": "frame_13844.png"}, {"time": 7025.034, "image_uri": "frame_13845.png"}, {"time": 7025.541, "image_uri": "frame_13846.png"}, {"time": 7026.049, "image_uri": "frame_13847.png"}, {"time": 7026.556, "image_uri": "frame_13848.png"}, {"time": 7027.064, "image_uri": "frame_13849.png"}, {"time": 7027.571, "image_uri": "frame_13850.png"}, {"time": 7028.078, "image_uri": "frame_13851.png"}, {"time": 7028.586, "image_uri": "frame_13852.png"}, {"time": 7029.093, "image_uri": "frame_13853.png"}, {"time": 7029.601, "image_uri": "frame_13854.png"}, {"time": 7030.108, "image_uri": "frame_13855.png"}, {"time": 7030.616, "image_uri": "frame_13856.png"}, {"time": 7031.123, "image_uri": "frame_13857.png"}, {"time": 7031.631, "image_uri": "frame_13858.png"}, {"time": 7032.138, "image_uri": "frame_13859.png"}, {"time": 7032.645, "image_uri": "frame_13860.png"}, {"time": 7033.153, "image_uri": "frame_13861.png"}, {"time": 7033.66, "image_uri": "frame_13862.png"}, {"time": 7034.168, "image_uri": "frame_13863.png"}, {"time": 7034.675, "image_uri": "frame_13864.png"}, {"time": 7035.183, "image_uri": "frame_13865.png"}, {"time": 7035.69, "image_uri": "frame_13866.png"}, {"time": 7036.198, "image_uri": "frame_13867.png"}, {"time": 7036.705, "image_uri": "frame_13868.png"}, {"time": 7037.212, "image_uri": "frame_13869.png"}, {"time": 7037.72, "image_uri": "frame_13870.png"}, {"time": 7038.227, "image_uri": "frame_13871.png"}, {"time": 7038.735, "image_uri": "frame_13872.png"}, {"time": 7039.242, "image_uri": "frame_13873.png"}, {"time": 7039.75, "image_uri": "frame_13874.png"}, {"time": 7040.257, "image_uri": "frame_13875.png"}, {"time": 7040.765, "image_uri": "frame_13876.png"}, {"time": 7041.272, "image_uri": "frame_13877.png"}, {"time": 7041.779, "image_uri": "frame_13878.png"}, {"time": 7042.287, "image_uri": "frame_13879.png"}, {"time": 7042.794, "image_uri": "frame_13880.png"}, {"time": 7043.302, "image_uri": "frame_13881.png"}, {"time": 7043.809, "image_uri": "frame_13882.png"}, {"time": 7044.317, "image_uri": "frame_13883.png"}, {"time": 7044.824, "image_uri": "frame_13884.png"}, {"time": 7045.332, "image_uri": "frame_13885.png"}, {"time": 7045.839, "image_uri": "frame_13886.png"}, {"time": 7046.346, "image_uri": "frame_13887.png"}, {"time": 7046.854, "image_uri": "frame_13888.png"}, {"time": 7047.361, "image_uri": "frame_13889.png"}, {"time": 7047.869, "image_uri": "frame_13890.png"}, {"time": 7048.376, "image_uri": "frame_13891.png"}, {"time": 7048.884, "image_uri": "frame_13892.png"}, {"time": 7049.391, "image_uri": "frame_13893.png"}, {"time": 7049.899, "image_uri": "frame_13894.png"}, {"time": 7050.406, "image_uri": "frame_13895.png"}, {"time": 7050.913, "image_uri": "frame_13896.png"}, {"time": 7051.421, "image_uri": "frame_13897.png"}, {"time": 7051.928, "image_uri": "frame_13898.png"}, {"time": 7052.436, "image_uri": "frame_13899.png"}, {"time": 7052.943, "image_uri": "frame_13900.png"}, {"time": 7053.451, "image_uri": "frame_13901.png"}, {"time": 7053.958, "image_uri": "frame_13902.png"}, {"time": 7054.465, "image_uri": "frame_13903.png"}, {"time": 7054.973, "image_uri": "frame_13904.png"}, {"time": 7055.48, "image_uri": "frame_13905.png"}, {"time": 7055.988, "image_uri": "frame_13906.png"}, {"time": 7056.495, "image_uri": "frame_13907.png"}, {"time": 7057.003, "image_uri": "frame_13908.png"}, {"time": 7057.51, "image_uri": "frame_13909.png"}, {"time": 7058.018, "image_uri": "frame_13910.png"}, {"time": 7058.525, "image_uri": "frame_13911.png"}, {"time": 7059.032, "image_uri": "frame_13912.png"}, {"time": 7059.54, "image_uri": "frame_13913.png"}, {"time": 7060.047, "image_uri": "frame_13914.png"}, {"time": 7060.555, "image_uri": "frame_13915.png"}, {"time": 7061.062, "image_uri": "frame_13916.png"}, {"time": 7061.57, "image_uri": "frame_13917.png"}, {"time": 7062.077, "image_uri": "frame_13918.png"}, {"time": 7062.585, "image_uri": "frame_13919.png"}, {"time": 7063.092, "image_uri": "frame_13920.png"}, {"time": 7063.599, "image_uri": "frame_13921.png"}, {"time": 7064.107, "image_uri": "frame_13922.png"}, {"time": 7064.614, "image_uri": "frame_13923.png"}, {"time": 7065.122, "image_uri": "frame_13924.png"}, {"time": 7065.629, "image_uri": "frame_13925.png"}, {"time": 7066.137, "image_uri": "frame_13926.png"}, {"time": 7066.644, "image_uri": "frame_13927.png"}, {"time": 7067.152, "image_uri": "frame_13928.png"}, {"time": 7067.659, "image_uri": "frame_13929.png"}, {"time": 7068.166, "image_uri": "frame_13930.png"}, {"time": 7068.674, "image_uri": "frame_13931.png"}, {"time": 7069.181, "image_uri": "frame_13932.png"}, {"time": 7069.689, "image_uri": "frame_13933.png"}, {"time": 7070.196, "image_uri": "frame_13934.png"}, {"time": 7070.704, "image_uri": "frame_13935.png"}, {"time": 7071.211, "image_uri": "frame_13936.png"}, {"time": 7071.719, "image_uri": "frame_13937.png"}, {"time": 7072.226, "image_uri": "frame_13938.png"}, {"time": 7072.733, "image_uri": "frame_13939.png"}, {"time": 7073.241, "image_uri": "frame_13940.png"}, {"time": 7073.748, "image_uri": "frame_13941.png"}, {"time": 7074.256, "image_uri": "frame_13942.png"}, {"time": 7074.763, "image_uri": "frame_13943.png"}, {"time": 7075.271, "image_uri": "frame_13944.png"}, {"time": 7075.778, "image_uri": "frame_13945.png"}, {"time": 7076.286, "image_uri": "frame_13946.png"}, {"time": 7076.793, "image_uri": "frame_13947.png"}, {"time": 7077.3, "image_uri": "frame_13948.png"}, {"time": 7077.808, "image_uri": "frame_13949.png"}, {"time": 7078.315, "image_uri": "frame_13950.png"}, {"time": 7078.823, "image_uri": "frame_13951.png"}, {"time": 7079.33, "image_uri": "frame_13952.png"}, {"time": 7079.838, "image_uri": "frame_13953.png"}, {"time": 7080.345, "image_uri": "frame_13954.png"}, {"time": 7080.853, "image_uri": "frame_13955.png"}, {"time": 7081.36, "image_uri": "frame_13956.png"}, {"time": 7081.867, "image_uri": "frame_13957.png"}, {"time": 7082.375, "image_uri": "frame_13958.png"}, {"time": 7082.882, "image_uri": "frame_13959.png"}, {"time": 7083.39, "image_uri": "frame_13960.png"}, {"time": 7083.897, "image_uri": "frame_13961.png"}, {"time": 7084.405, "image_uri": "frame_13962.png"}, {"time": 7084.912, "image_uri": "frame_13963.png"}, {"time": 7085.419, "image_uri": "frame_13964.png"}, {"time": 7085.927, "image_uri": "frame_13965.png"}, {"time": 7086.434, "image_uri": "frame_13966.png"}, {"time": 7086.942, "image_uri": "frame_13967.png"}, {"time": 7087.449, "image_uri": "frame_13968.png"}, {"time": 7087.957, "image_uri": "frame_13969.png"}, {"time": 7088.464, "image_uri": "frame_13970.png"}, {"time": 7088.972, "image_uri": "frame_13971.png"}, {"time": 7089.479, "image_uri": "frame_13972.png"}, {"time": 7089.986, "image_uri": "frame_13973.png"}, {"time": 7090.494, "image_uri": "frame_13974.png"}, {"time": 7091.001, "image_uri": "frame_13975.png"}, {"time": 7091.509, "image_uri": "frame_13976.png"}, {"time": 7092.016, "image_uri": "frame_13977.png"}, {"time": 7092.524, "image_uri": "frame_13978.png"}, {"time": 7093.031, "image_uri": "frame_13979.png"}, {"time": 7093.539, "image_uri": "frame_13980.png"}, {"time": 7094.046, "image_uri": "frame_13981.png"}, {"time": 7094.553, "image_uri": "frame_13982.png"}, {"time": 7095.061, "image_uri": "frame_13983.png"}, {"time": 7095.568, "image_uri": "frame_13984.png"}, {"time": 7096.076, "image_uri": "frame_13985.png"}, {"time": 7096.583, "image_uri": "frame_13986.png"}, {"time": 7097.091, "image_uri": "frame_13987.png"}, {"time": 7097.598, "image_uri": "frame_13988.png"}, {"time": 7098.106, "image_uri": "frame_13989.png"}, {"time": 7098.613, "image_uri": "frame_13990.png"}, {"time": 7099.12, "image_uri": "frame_13991.png"}, {"time": 7099.628, "image_uri": "frame_13992.png"}, {"time": 7100.135, "image_uri": "frame_13993.png"}, {"time": 7100.643, "image_uri": "frame_13994.png"}, {"time": 7101.15, "image_uri": "frame_13995.png"}, {"time": 7101.658, "image_uri": "frame_13996.png"}, {"time": 7102.165, "image_uri": "frame_13997.png"}, {"time": 7102.673, "image_uri": "frame_13998.png"}, {"time": 7103.18, "image_uri": "frame_13999.png"}, {"time": 7103.687, "image_uri": "frame_14000.png"}, {"time": 7104.195, "image_uri": "frame_14001.png"}, {"time": 7104.702, "image_uri": "frame_14002.png"}, {"time": 7105.21, "image_uri": "frame_14003.png"}, {"time": 7105.717, "image_uri": "frame_14004.png"}, {"time": 7106.225, "image_uri": "frame_14005.png"}, {"time": 7106.732, "image_uri": "frame_14006.png"}, {"time": 7107.24, "image_uri": "frame_14007.png"}, {"time": 7107.747, "image_uri": "frame_14008.png"}, {"time": 7108.254, "image_uri": "frame_14009.png"}, {"time": 7108.762, "image_uri": "frame_14010.png"}, {"time": 7109.269, "image_uri": "frame_14011.png"}, {"time": 7109.777, "image_uri": "frame_14012.png"}, {"time": 7110.284, "image_uri": "frame_14013.png"}, {"time": 7110.792, "image_uri": "frame_14014.png"}, {"time": 7111.299, "image_uri": "frame_14015.png"}, {"time": 7111.806, "image_uri": "frame_14016.png"}, {"time": 7112.314, "image_uri": "frame_14017.png"}, {"time": 7112.821, "image_uri": "frame_14018.png"}, {"time": 7113.329, "image_uri": "frame_14019.png"}, {"time": 7113.836, "image_uri": "frame_14020.png"}, {"time": 7114.344, "image_uri": "frame_14021.png"}, {"time": 7114.851, "image_uri": "frame_14022.png"}, {"time": 7115.359, "image_uri": "frame_14023.png"}, {"time": 7115.866, "image_uri": "frame_14024.png"}, {"time": 7116.373, "image_uri": "frame_14025.png"}, {"time": 7116.881, "image_uri": "frame_14026.png"}, {"time": 7117.388, "image_uri": "frame_14027.png"}, {"time": 7117.896, "image_uri": "frame_14028.png"}, {"time": 7118.403, "image_uri": "frame_14029.png"}, {"time": 7118.911, "image_uri": "frame_14030.png"}, {"time": 7119.418, "image_uri": "frame_14031.png"}, {"time": 7119.926, "image_uri": "frame_14032.png"}, {"time": 7120.433, "image_uri": "frame_14033.png"}, {"time": 7120.94, "image_uri": "frame_14034.png"}, {"time": 7121.448, "image_uri": "frame_14035.png"}, {"time": 7121.955, "image_uri": "frame_14036.png"}, {"time": 7122.463, "image_uri": "frame_14037.png"}, {"time": 7122.97, "image_uri": "frame_14038.png"}, {"time": 7123.478, "image_uri": "frame_14039.png"}, {"time": 7123.985, "image_uri": "frame_14040.png"}, {"time": 7124.493, "image_uri": "frame_14041.png"}, {"time": 7125.0, "image_uri": "frame_14042.png"}, {"time": 7125.507, "image_uri": "frame_14043.png"}, {"time": 7126.015, "image_uri": "frame_14044.png"}, {"time": 7126.522, "image_uri": "frame_14045.png"}, {"time": 7127.03, "image_uri": "frame_14046.png"}, {"time": 7127.537, "image_uri": "frame_14047.png"}, {"time": 7128.045, "image_uri": "frame_14048.png"}, {"time": 7128.552, "image_uri": "frame_14049.png"}, {"time": 7129.06, "image_uri": "frame_14050.png"}, {"time": 7129.567, "image_uri": "frame_14051.png"}, {"time": 7130.074, "image_uri": "frame_14052.png"}, {"time": 7130.582, "image_uri": "frame_14053.png"}, {"time": 7131.089, "image_uri": "frame_14054.png"}, {"time": 7131.597, "image_uri": "frame_14055.png"}, {"time": 7132.104, "image_uri": "frame_14056.png"}, {"time": 7132.612, "image_uri": "frame_14057.png"}, {"time": 7133.119, "image_uri": "frame_14058.png"}, {"time": 7133.627, "image_uri": "frame_14059.png"}, {"time": 7134.134, "image_uri": "frame_14060.png"}, {"time": 7134.641, "image_uri": "frame_14061.png"}, {"time": 7135.149, "image_uri": "frame_14062.png"}, {"time": 7135.656, "image_uri": "frame_14063.png"}, {"time": 7136.164, "image_uri": "frame_14064.png"}, {"time": 7136.671, "image_uri": "frame_14065.png"}, {"time": 7137.179, "image_uri": "frame_14066.png"}, {"time": 7137.686, "image_uri": "frame_14067.png"}, {"time": 7138.194, "image_uri": "frame_14068.png"}, {"time": 7138.701, "image_uri": "frame_14069.png"}, {"time": 7139.208, "image_uri": "frame_14070.png"}, {"time": 7139.716, "image_uri": "frame_14071.png"}, {"time": 7140.223, "image_uri": "frame_14072.png"}, {"time": 7140.731, "image_uri": "frame_14073.png"}, {"time": 7141.238, "image_uri": "frame_14074.png"}, {"time": 7141.746, "image_uri": "frame_14075.png"}, {"time": 7142.253, "image_uri": "frame_14076.png"}, {"time": 7142.76, "image_uri": "frame_14077.png"}, {"time": 7143.268, "image_uri": "frame_14078.png"}, {"time": 7143.775, "image_uri": "frame_14079.png"}, {"time": 7144.283, "image_uri": "frame_14080.png"}, {"time": 7144.79, "image_uri": "frame_14081.png"}, {"time": 7145.298, "image_uri": "frame_14082.png"}, {"time": 7145.805, "image_uri": "frame_14083.png"}, {"time": 7146.313, "image_uri": "frame_14084.png"}, {"time": 7146.82, "image_uri": "frame_14085.png"}, {"time": 7147.327, "image_uri": "frame_14086.png"}, {"time": 7147.835, "image_uri": "frame_14087.png"}, {"time": 7148.342, "image_uri": "frame_14088.png"}, {"time": 7148.85, "image_uri": "frame_14089.png"}, {"time": 7149.357, "image_uri": "frame_14090.png"}, {"time": 7149.865, "image_uri": "frame_14091.png"}, {"time": 7150.372, "image_uri": "frame_14092.png"}, {"time": 7150.88, "image_uri": "frame_14093.png"}, {"time": 7151.387, "image_uri": "frame_14094.png"}, {"time": 7151.894, "image_uri": "frame_14095.png"}, {"time": 7152.402, "image_uri": "frame_14096.png"}, {"time": 7152.909, "image_uri": "frame_14097.png"}, {"time": 7153.417, "image_uri": "frame_14098.png"}, {"time": 7153.924, "image_uri": "frame_14099.png"}, {"time": 7154.432, "image_uri": "frame_14100.png"}, {"time": 7154.939, "image_uri": "frame_14101.png"}, {"time": 7155.447, "image_uri": "frame_14102.png"}, {"time": 7155.954, "image_uri": "frame_14103.png"}, {"time": 7156.461, "image_uri": "frame_14104.png"}, {"time": 7156.969, "image_uri": "frame_14105.png"}, {"time": 7157.476, "image_uri": "frame_14106.png"}, {"time": 7157.984, "image_uri": "frame_14107.png"}, {"time": 7158.491, "image_uri": "frame_14108.png"}, {"time": 7158.999, "image_uri": "frame_14109.png"}, {"time": 7159.506, "image_uri": "frame_14110.png"}, {"time": 7160.014, "image_uri": "frame_14111.png"}, {"time": 7160.521, "image_uri": "frame_14112.png"}, {"time": 7161.028, "image_uri": "frame_14113.png"}, {"time": 7161.536, "image_uri": "frame_14114.png"}, {"time": 7162.043, "image_uri": "frame_14115.png"}, {"time": 7162.551, "image_uri": "frame_14116.png"}, {"time": 7163.058, "image_uri": "frame_14117.png"}, {"time": 7163.566, "image_uri": "frame_14118.png"}, {"time": 7164.073, "image_uri": "frame_14119.png"}, {"time": 7164.581, "image_uri": "frame_14120.png"}, {"time": 7165.088, "image_uri": "frame_14121.png"}, {"time": 7165.595, "image_uri": "frame_14122.png"}, {"time": 7166.103, "image_uri": "frame_14123.png"}, {"time": 7166.61, "image_uri": "frame_14124.png"}, {"time": 7167.118, "image_uri": "frame_14125.png"}, {"time": 7167.625, "image_uri": "frame_14126.png"}, {"time": 7168.133, "image_uri": "frame_14127.png"}, {"time": 7168.64, "image_uri": "frame_14128.png"}, {"time": 7169.147, "image_uri": "frame_14129.png"}, {"time": 7169.655, "image_uri": "frame_14130.png"}, {"time": 7170.162, "image_uri": "frame_14131.png"}, {"time": 7170.67, "image_uri": "frame_14132.png"}, {"time": 7171.177, "image_uri": "frame_14133.png"}, {"time": 7171.685, "image_uri": "frame_14134.png"}, {"time": 7172.192, "image_uri": "frame_14135.png"}, {"time": 7172.7, "image_uri": "frame_14136.png"}, {"time": 7173.207, "image_uri": "frame_14137.png"}, {"time": 7173.714, "image_uri": "frame_14138.png"}, {"time": 7174.222, "image_uri": "frame_14139.png"}, {"time": 7174.729, "image_uri": "frame_14140.png"}, {"time": 7175.237, "image_uri": "frame_14141.png"}, {"time": 7175.744, "image_uri": "frame_14142.png"}, {"time": 7176.252, "image_uri": "frame_14143.png"}, {"time": 7176.759, "image_uri": "frame_14144.png"}, {"time": 7177.267, "image_uri": "frame_14145.png"}, {"time": 7177.774, "image_uri": "frame_14146.png"}, {"time": 7178.281, "image_uri": "frame_14147.png"}, {"time": 7178.789, "image_uri": "frame_14148.png"}, {"time": 7179.296, "image_uri": "frame_14149.png"}, {"time": 7179.804, "image_uri": "frame_14150.png"}, {"time": 7180.311, "image_uri": "frame_14151.png"}, {"time": 7180.819, "image_uri": "frame_14152.png"}, {"time": 7181.326, "image_uri": "frame_14153.png"}, {"time": 7181.834, "image_uri": "frame_14154.png"}, {"time": 7182.341, "image_uri": "frame_14155.png"}, {"time": 7182.848, "image_uri": "frame_14156.png"}, {"time": 7183.356, "image_uri": "frame_14157.png"}, {"time": 7183.863, "image_uri": "frame_14158.png"}, {"time": 7184.371, "image_uri": "frame_14159.png"}, {"time": 7184.878, "image_uri": "frame_14160.png"}, {"time": 7185.386, "image_uri": "frame_14161.png"}, {"time": 7185.893, "image_uri": "frame_14162.png"}, {"time": 7186.401, "image_uri": "frame_14163.png"}, {"time": 7186.908, "image_uri": "frame_14164.png"}, {"time": 7187.415, "image_uri": "frame_14165.png"}, {"time": 7187.923, "image_uri": "frame_14166.png"}, {"time": 7188.43, "image_uri": "frame_14167.png"}, {"time": 7188.938, "image_uri": "frame_14168.png"}, {"time": 7189.445, "image_uri": "frame_14169.png"}, {"time": 7189.953, "image_uri": "frame_14170.png"}, {"time": 7190.46, "image_uri": "frame_14171.png"}, {"time": 7190.968, "image_uri": "frame_14172.png"}, {"time": 7191.475, "image_uri": "frame_14173.png"}, {"time": 7191.982, "image_uri": "frame_14174.png"}, {"time": 7192.49, "image_uri": "frame_14175.png"}, {"time": 7192.997, "image_uri": "frame_14176.png"}, {"time": 7193.505, "image_uri": "frame_14177.png"}, {"time": 7194.012, "image_uri": "frame_14178.png"}, {"time": 7194.52, "image_uri": "frame_14179.png"}, {"time": 7195.027, "image_uri": "frame_14180.png"}, {"time": 7195.535, "image_uri": "frame_14181.png"}, {"time": 7196.042, "image_uri": "frame_14182.png"}, {"time": 7196.549, "image_uri": "frame_14183.png"}, {"time": 7197.057, "image_uri": "frame_14184.png"}, {"time": 7197.564, "image_uri": "frame_14185.png"}, {"time": 7198.072, "image_uri": "frame_14186.png"}, {"time": 7198.579, "image_uri": "frame_14187.png"}, {"time": 7199.087, "image_uri": "frame_14188.png"}, {"time": 7199.594, "image_uri": "frame_14189.png"}, {"time": 7200.101, "image_uri": "frame_14190.png"}, {"time": 7200.609, "image_uri": "frame_14191.png"}, {"time": 7201.116, "image_uri": "frame_14192.png"}, {"time": 7201.624, "image_uri": "frame_14193.png"}, {"time": 7202.131, "image_uri": "frame_14194.png"}, {"time": 7202.639, "image_uri": "frame_14195.png"}, {"time": 7203.146, "image_uri": "frame_14196.png"}, {"time": 7203.654, "image_uri": "frame_14197.png"}, {"time": 7204.161, "image_uri": "frame_14198.png"}, {"time": 7204.668, "image_uri": "frame_14199.png"}, {"time": 7205.176, "image_uri": "frame_14200.png"}, {"time": 7205.683, "image_uri": "frame_14201.png"}, {"time": 7206.191, "image_uri": "frame_14202.png"}, {"time": 7206.698, "image_uri": "frame_14203.png"}, {"time": 7207.206, "image_uri": "frame_14204.png"}, {"time": 7207.713, "image_uri": "frame_14205.png"}, {"time": 7208.221, "image_uri": "frame_14206.png"}, {"time": 7208.728, "image_uri": "frame_14207.png"}, {"time": 7209.235, "image_uri": "frame_14208.png"}, {"time": 7209.743, "image_uri": "frame_14209.png"}, {"time": 7210.25, "image_uri": "frame_14210.png"}, {"time": 7210.758, "image_uri": "frame_14211.png"}, {"time": 7211.265, "image_uri": "frame_14212.png"}, {"time": 7211.773, "image_uri": "frame_14213.png"}, {"time": 7212.28, "image_uri": "frame_14214.png"}, {"time": 7212.788, "image_uri": "frame_14215.png"}, {"time": 7213.295, "image_uri": "frame_14216.png"}, {"time": 7213.802, "image_uri": "frame_14217.png"}, {"time": 7214.31, "image_uri": "frame_14218.png"}, {"time": 7214.817, "image_uri": "frame_14219.png"}, {"time": 7215.325, "image_uri": "frame_14220.png"}, {"time": 7215.832, "image_uri": "frame_14221.png"}, {"time": 7216.34, "image_uri": "frame_14222.png"}, {"time": 7216.847, "image_uri": "frame_14223.png"}, {"time": 7217.355, "image_uri": "frame_14224.png"}, {"time": 7217.862, "image_uri": "frame_14225.png"}, {"time": 7218.369, "image_uri": "frame_14226.png"}, {"time": 7218.877, "image_uri": "frame_14227.png"}, {"time": 7219.384, "image_uri": "frame_14228.png"}, {"time": 7219.892, "image_uri": "frame_14229.png"}, {"time": 7220.399, "image_uri": "frame_14230.png"}, {"time": 7220.907, "image_uri": "frame_14231.png"}, {"time": 7221.414, "image_uri": "frame_14232.png"}, {"time": 7221.922, "image_uri": "frame_14233.png"}, {"time": 7222.429, "image_uri": "frame_14234.png"}, {"time": 7222.936, "image_uri": "frame_14235.png"}, {"time": 7223.444, "image_uri": "frame_14236.png"}] \ No newline at end of file diff --git a/tests/nodes/test_subtitle_ocr/data/ocr_frames_full.json b/tests/nodes/test_subtitle_ocr/data/ocr_frames_full.json new file mode 100644 index 0000000..b3d5049 --- /dev/null +++ b/tests/nodes/test_subtitle_ocr/data/ocr_frames_full.json @@ -0,0 +1 @@ +{"1": "(淫魔病院)(新人护士的眼泪)", "2": "(淫魔病院)(新人护士的眼泪)", "3": "(淫魔病院)(新人护士的眼泪)", "4": "(淫魔病院)(新人护士的眼泪)", "5": "(淫魔病院)(新人护士的眼泪)", "6": "(淫魔病院)(新人护士的眼泪)", "7": "(淫魔病院)(新人护士的眼泪)", "8": "(淫魔病院)(新人护士的眼泪)", "9": "(淫魔病院)(新人护士的眼泪)", "10": "(淫魔病院)(新人护士的眼泪)", "11": "(淫魔病院)(新人护士的眼泪)", "12": "", "13": "", "14": "", "15": "", "16": "", "17": "", "18": "", "19": "", "20": "", "21": "", "22": "", "23": "", "24": "", "25": "", "26": "", "27": "", "28": "", "29": "", "30": "", "31": "", "32": "", "33": "", "34": "", "35": "", "36": "辛苦了 上午的检查已经OK了", "37": "辛苦了 上午的检查已经OK了", "38": "辛苦了 上午的检查已经OK了", "39": "辛苦了 上午的检查已经OK了", "40": "辛苦了 上午的检查已经OK了", "41": "辛苦了 上午的检查已经OK了", "42": "辛苦了 上午的检查已经OK了", "43": "辛苦了 上午的检查已经OK了", "44": "---", "45": "", "46": "", "47": "", "48": "", "49": "", "50": "", "51": "", "52": "", "53": "", "54": "", "55": "", "56": "", "57": "", "58": "", "59": "", "60": "", "61": "", "62": "", "63": "", "64": "谢谢你 松井小姐", "65": "谢谢你 松井小姐", "66": "谢谢你 松井小姐", "67": "谢谢你 松井小姐", "68": "还有没有什么困扰 或者奇怪的地方吗", "69": "还有没有什么困扰 或者奇怪的地方吗", "70": "还有没有什么困扰 或者奇怪的地方吗", "71": "还有没有什么困扰 或者奇怪的地方吗", "72": "还有没有什么困扰 或者奇怪的地方吗", "73": "还有没有什么困扰 或者奇怪的地方吗", "74": "还有没有什么困扰 或者奇怪的地方吗", "75": "", "76": "没问题啦", "77": "没问题啦", "78": "没问题啦", "79": "总感觉果林前辈 每次都会问这个呢", "80": "总感觉果林前辈 每次都会问这个呢", "81": "总感觉果林前辈 每次都会问这个呢", "82": "总感觉果林前辈 每次都会问这个呢", "83": "总感觉果林前辈 每次都会问这个呢", "84": "总感觉果林前辈 每次都会问这个呢", "85": "总感觉果林前辈 每次都会问这个呢", "86": "是呢 抱歉", "87": "是呢 抱歉", "88": "是呢 抱歉", "89": "是呢 抱歉", "90": "是呢 抱歉", "91": "是呢 抱歉", "92": "V", "93": "V", "94": "", "95": "", "96": "V", "97": "V", "98": "那么 松井小姐", "99": "那么 松井小姐", "100": "那么 松井小姐", "101": "那么 松井小姐", "102": "那么 松井小姐", "103": "V", "104": "V", "105": "你来我们医院也才一个月吧", "106": "你来我们医院也才一个月吧", "107": "你来我们医院也才一个月吧", "108": "你来我们医院也才一个月吧", "109": "你来我们医院也才一个月吧", "110": "你来我们医院也才一个月吧", "111": "", "112": "记得挺快嘛 没有啦", "113": "记得挺快嘛 没有啦", "114": "记得挺快嘛没有啦", "115": "记得挺快嘛 没有啦", "116": "记得挺快嘛 没有啦", "117": "记得挺快嘛 没有啦", "118": "", "119": "", "120": "", "121": "因为果林前辈教得好呀", "122": "因为果林前辈教得好呀", "123": "因为果林前辈教得好呀", "124": "因为果林前辈教得好呀", "125": "因为果林前辈教得好呀", "126": "因为果林前辈教得好呀", "127": "因为果林前辈教得好呀", "128": "因为 你和患者们 也已经完全打成一片了", "129": "因为 你和患者们 也已经完全打成一片了", "130": "因为 你和患者们 也已经完全打成一片了", "131": "因为 你和患者们 也已经完全打成一片了", "132": "因为 你和患者们 也已经完全打成一片了", "133": "因为 你和患者们 也已经完全打成一片了", "134": "因为 你和患者们 也已经完全打成一片了", "135": "因为 你和患者们 也已经完全打成一片了", "136": "医", "137": "我是北冈果林", "138": "我是北冈果林", "139": "我是北冈果林", "140": "我是北冈果林", "141": "我是北冈果林", "142": "在这家综合医院工作的护士", "143": "在这家综合医院工作的护士", "144": "在这家综合医院工作的护士", "145": "在这家综合医院工作的护士", "146": "在这家综合医院工作的护士", "147": "在这家综合医院工作的护士", "148": "在这家综合医院工作的护士", "149": "在这家综合医院工作的护士", "150": "在这家综合医院工作的护士", "151": "", "152": "她叫松井日奈子", "153": "她叫松井日奈子", "154": "她叫松井日奈子", "155": "她叫松井日奈子", "156": "她叫松井日奈子", "157": "是一个月前开始 在这间医院工作的新人护士", "158": "是一个月前开始 在这间医院工作的新人护士", "159": "是一个月前开始 在这间医院工作的新人护士", "160": "是一个月前开始 在这间医院工作的新人护士", "161": "是一个月前开始 在这间医院工作的新人护士", "162": "是一个月前开始 在这间医院工作的新人护士", "163": "是一个月前开始 在这间医院工作的新人护士", "164": "是一个月前开始 在这间医院工作的新人护士", "165": "是一个月前开始 在这间医院工作的新人护士", "166": "是一个月前开始 在这间医院工作的新人护士", "167": "", "168": "", "169": "上吧 上吧", "170": "上吧 上吧", "171": "上吧 上吧", "172": "上吧 上吧", "173": "上吧 上吧", "174": "上吧 上吧", "175": "", "176": "不行 受不了 好了 上吧", "177": "不行 受不了 好了 上吧", "178": "不行 受不了 好了 上吧", "179": "不行 受不了 好了 上吧", "180": "不行 受不了 好了 上吧", "181": "不行 受不了 好了 上吧", "182": "不行 受不了 好了 上吧", "183": "不行 受不了 好了 上吧", "184": "不行 受不了 好了 上吧", "185": "不行 受不了 好了 上吧", "186": "Cleaning", "187": "", "188": "Cleaning", "189": "清洁", "190": "", "191": "", "192": "", "193": "状态不错", "194": "状态不错", "195": "状态不错", "196": "状态不错", "197": "状态不错", "198": "请进", "199": "请进", "200": "请进", "201": "请进", "202": "请进", "203": "", "204": "---", "205": "", "206": "", "207": "", "208": "", "209": "", "210": "", "211": "HTML", "212": "早安", "213": "早安", "214": "早安", "215": "早安", "216": "早安", "217": "", "218": "", "219": "金山先生", "220": "金山先生", "221": "金山先生", "222": "昨晚你住院时我不在没能照顾到你非常抱歉", "223": "昨晚你住院时我不在没能照顾到你非常抱歉", "224": "昨晚你住院时我不在没能照顾到你非常抱歉", "225": "昨晚你住院时我不在没能照顾到你非常抱歉", "226": "昨晚你住院时我不在没能照顾到你非常抱歉", "227": "昨晚你住院时我不在没能照顾到你非常抱歉", "228": "昨晚你住院时我不在没能照顾到你非常抱歉", "229": "昨晚你住院时我不在没能照顾到你非常抱歉", "230": "昨晚你住院时我不在没能照顾到你非常抱歉", "231": "昨晚你住院时我不在没能照顾到你非常抱歉", "232": "我是医务室长权藤 请多关照", "233": "我是医务室长权藤 请多关照", "234": "我是医务室长权藤 请多关照", "235": "我是医务室长权藤 请多关照", "236": "我是医务室长权藤 请多关照", "237": "我是医务室长权藤 请多关照", "238": "我是医务室长权藤 请多关照", "239": "我是医务室长权藤 请多关照", "240": "我是医务室长权藤 请多关照", "241": "我是医务室长权藤 请多关照", "242": "我是医务室长权藤 请多关照", "243": "我是医务室长权藤 请多关照", "244": "我是医务室长权藤 请多关照", "245": "我是医务室长权藤 请多关照", "246": "这边这位是负责你的叶山", "247": "这边这位是负责你的叶山", "248": "这边这位是负责你的叶山", "249": "这边这位是负责你的叶山", "250": "这边这位是负责你的叶山", "251": "这边这位是负责你的叶山", "252": "这边这位是负责你的叶山", "253": "这边这位是负责你的叶山", "254": "这边这位是负责你的叶山", "255": "这边这位是负责你的叶山", "256": "我是外科主任叶山", "257": "我是外科主任叶山", "258": "我是外科主任叶山", "259": "我是外科主任叶山", "260": "我是外科主任叶山", "261": "我是外科主任叶山", "262": "", "263": "", "264": "", "265": "", "266": "如果有什么困难 请找她帮忙", "267": "如果有什么困难 请找她帮忙", "268": "如果有什么困难 请找她帮忙", "269": "如果有什么困难 请找她帮忙", "270": "如果有什么困难 请找她帮忙", "271": "如果有什么困难 请找她帮忙", "272": "如果有什么困难 请找她帮忙", "273": "如果有什么困难 请找她帮忙", "274": "如果有什么困难 请找她帮忙", "275": "另外 关于你希望的特别服务 我们正在准备当中", "276": "另外 关于你希望的特别服务 我们正在准备当中", "277": "另外 关于你希望的特别服务 我们正在准备当中", "278": "另外 关于你希望的特别服务 我们正在准备当中", "279": "另外 关于你希望的特别服务 我们正在准备当中", "280": "另外 关于你希望的特别服务 我们正在准备当中", "281": "另外 关于你希望的特别服务 我们正在准备当中", "282": "另外 关于你希望的特别服务 我们正在准备当中", "283": "另外 关于你希望的特别服务 我们正在准备当中", "284": "另外 关于你希望的特别服务 我们正在准备当中", "285": "另外 关于你希望的特别服务 我们正在准备当中", "286": "另外 关于你希望的特别服务 我们正在准备当中", "287": "", "288": "", "289": "拜托你们尽快 明白了", "290": "拜托你们尽快 明白了", "291": "拜托你们尽快 明白了", "292": "拜托你们尽快 明白了", "293": "拜托你们尽快 明白了", "294": "拜托你们尽快 明白了", "295": "拜托你们尽快 明白了", "296": "拜托你们尽快 明白了", "297": "拜托你们尽快 明白了", "298": "拜托你们尽快 明白了", "299": "", "300": "", "301": "", "302": "", "303": "", "304": "", "305": "我没见过你呢", "306": "我没见过你呢", "307": "我没见过你呢", "308": "我没见过你呢", "309": "我没见过你呢", "310": "我没见过你呢", "311": "我没见过你呢", "312": "", "313": "我是新来的生产员", "314": "我是新来的生产员", "315": "我是新来的生产员", "316": "我是新来的生产员", "317": "我是新来的生产员", "318": "我姓泷本 请多多指教", "319": "我姓泷本 请多多指教", "320": "我姓泷本 请多多指教", "321": "我姓泷本 请多多指教", "322": "我姓泷本 请多多指教", "323": "我姓泷本 请多多指教", "324": "我姓泷本 请多多指教", "325": "就这样", "326": "就这样", "327": "就这样", "328": "就这样", "329": "泷本先生 我有件事想拜托你", "330": "泷本先生 我有件事想拜托你", "331": "泷本先生 我有件事想拜托你", "332": "泷本先生 我有件事想拜托你", "333": "泷本先生 我有件事想拜托你", "334": "泷本先生 我有件事想拜托你", "335": "沈本先生 我有件事想拜托你", "336": "泷本先生 我有件事想拜托你", "337": "", "338": "", "339": "今天傍晚 能来手术室一趟吗", "340": "今天傍晚 能来手术室一趟吗", "341": "今天傍晚 能来手术室一趟吗", "342": "今天傍晚 能来手术室一趟吗", "343": "今天傍晚 能来手术室一趟吗", "344": "今天傍晚 能来手术室一趟吗", "345": "今天傍晚 能来手术室一趟吗", "346": "今天傍晚 能来手术室一趟吗", "347": "今天傍晚 能来手术室一趟吗", "348": "今天傍晚 能来手术室一趟吗", "349": "", "350": "", "351": "明白了 那就这样", "352": "明白了 那就这样", "353": "明白了 那就这样", "354": "明白了 那就这样", "355": "明白了 那就这样", "356": "明白了 那就这样", "357": "明白了 那就这样", "358": "明白了 那就这样", "359": "", "360": "", "361": "", "362": "", "363": "", "364": "", "365": "___", "366": "", "367": "", "368": "", "369": "", "370": "", "371": "", "372": "", "373": "", "374": "", "375": "", "376": "", "377": "", "378": "", "379": "", "380": "", "381": "", "382": "", "383": "", "384": "", "385": "", "386": "", "387": "", "388": "", "389": "", "390": "", "391": "", "392": "---------------", "393": "", "394": "", "395": "---------------", "396": "", "397": "", "398": "", "399": "", "400": "", "401": "", "402": "", "403": "", "404": "", "405": "", "406": "", "407": "", "408": "", "409": "", "410": "", "411": "北冈小姐", "412": "北冈小姐", "413": "北冈小姐", "414": "这是特别病房患者的病历表", "415": "这是特别病房患者的病历表", "416": "这是特别病房患者的病历表", "417": "这是特别病房患者的病历表", "418": "这是特别病房患者的病历表", "419": "这是特别病房患者的病历表", "420": "这是特别病房患者的病历表", "421": "这是特别病房患者的病历表", "422": "", "423": "金山先生有向我们医院捐过一笔巨款", "424": "金山先生有向我们医院捐过一笔巨款", "425": "金山先生有向我们医院 捐过一笔巨款", "426": "金山先生有向我们医院 捐过一笔巨款", "427": "金山先生有向我们医院 捐过一笔巨款", "428": "金山先生有向我们医院捐过一笔巨款", "429": "金山先生有向我们医院捐过一笔巨款", "430": "金山先生有向我们医院捐过一笔巨款", "431": "金山先生有向我们医院捐过一笔巨款", "432": "非常多的金额 你们别疏忽了", "433": "非常多的金额 你们别疏忽了", "434": "非常多的金额 你们别疏忽了", "435": "非常多的金额 你们别疏忽了", "436": "非常多的金额 你们别疏忽了", "437": "非常多的金额 你们别疏忽了", "438": "非常多的金额 你们别疏忽了", "439": "非常多的金额 你们别疏忽了", "440": "是明白了", "441": "是 明白了", "442": "是明白了", "443": "是明白了", "444": "是明白了", "445": "", "446": "", "447": "", "448": "", "449": "", "450": "", "451": "", "452": "", "453": "松井小姐 在", "454": "松井小姐 在", "455": "松井小姐 在", "456": "松井小姐 在", "457": "你来这边还没多久吧 是的", "458": "你来这边还没多久吧 是的", "459": "你来这边还没多久吧 是的", "460": "你来这边还没多久吧是的", "461": "你来这边还没多久吧 是的", "462": "你来这边还没多久吧 是的", "463": "你来这边还没多久吧 是的", "464": "你来这边还没多久吧 是的", "465": "你来这边还没多久吧 是的", "466": "你来这边还没多久吧 是的", "467": "你来这边还没多久吧 是的", "468": "", "469": "", "470": "我想指导你新的工作内容", "471": "我想指导你新的工作内容", "472": "我想指导你新的工作内容", "473": "我想指导你新的工作内容", "474": "我想指导你新的工作内容", "475": "我想指导你新的工作内容", "476": "我想指导你新的工作内容", "477": "我想指导你新的工作内容", "478": "不是护理", "479": "不是护理", "480": "不是护理", "481": "不是护理", "482": "而是我主导的属于我的特殊业务", "483": "而是我主导的属于我的特殊业务", "484": "而是我主导的属于我的特殊业务", "485": "而是我主导的 属于我的特殊业务", "486": "而是我主导的 属于我的特殊业务", "487": "而是我主导的属于我的特殊业务", "488": "而是我主导的 属于我的特殊业务", "489": "而是我主导的 属于我的特殊业务", "490": "而是我主导的属于我的特殊业务", "491": "而是我主导的 属于我的特殊业务", "492": "而是我主导的 属于我的特殊业务", "493": "下次夜班是什么时候呢", "494": "下次夜班是什么时候呢", "495": "下次夜班是什么时候呢", "496": "下次夜班是什么时候呢", "497": "下次夜班是什么时候呢", "498": "下次夜班是什么时候呢", "499": "", "500": "是的 明天就是了", "501": "是的 明天就是了", "502": "是的 明天就是了", "503": "是的 明天就是了", "504": "是的 明天就是了", "505": "是的 明天就是了", "506": "图", "507": "那么明天晚上十点", "508": "那么明天晚上十点", "509": "那么明天晚上十点", "510": "那么明天晚上十点", "511": "那么明天晚上十点", "512": "那么明天晚上十点", "513": "你能来空病房209号室吗", "514": "你能来空病房209号室吗", "515": "你能来空病房209号室吗", "516": "你能来空病房209号室吗", "517": "你能来空病房209号室吗", "518": "你能来空病房209号室吗", "519": "你能来空病房209号室吗", "520": "你能来空病房209号室吗", "521": "你能来空病房209号室吗", "522": "你能来空病房209号室吗", "523": "我明白了", "524": "我明白了", "525": "我明白了", "526": "我明白了", "527": "我明白了", "528": "", "529": "留言", "530": "北冈小姐", "531": "北冈小姐", "532": "北冈小姐", "533": "北冈小姐", "534": "你也作为她的指导员 一起参加吧", "535": "你也作为她的指导员 一起参加吧", "536": "你也作为她的指导员 一起参加吧", "537": "你也作为她的指导员 一起参加吧", "538": "你也作为她的指导员 一起参加吧", "539": "你也作为她的指导员 一起参加吧", "540": "你也作为她的指导员 一起参加吧", "541": "你也作为她的指导员 一起参加吧", "542": "你也作为她的指导员 一起参加吧", "543": "你也作为她的指导员 一起参加吧", "544": "你也作为她的指导员 一起参加吧", "545": "你也作为她的指导员 一起参加吧", "546": "维森 健康", "547": "歹史融合刑務", "548": "我知道了", "549": "我知道了", "550": "我知道了", "551": "我知道了", "552": "我知道了", "553": "天文教研小组", "554": "爱尔联合机构", "555": "", "556": "", "557": "", "558": "___", "559": "---", "560": "", "561": "---", "562": "---", "563": "", "564": "---", "565": "---", "566": "", "567": "", "568": "---", "569": "", "570": "HTML", "571": "HTML", "572": "HTML", "573": "HTML", "574": "HTML", "575": "HTML", "576": "HTML", "577": "HTML", "578": "", "579": "--- ---", "580": "------", "581": "", "582": "那个", "583": "那个", "584": "那个", "585": "那个", "586": "那个", "587": "桐谷先生还在头痛 需要医生开药", "588": "桐谷先生还在头痛 需要医生开药", "589": "桐谷先生还在头痛 需要医生开药", "590": "桐谷先生还在头痛 需要医生开药", "591": "桐谷先生还在头痛 需要医生开药", "592": "桐谷先生还在头痛 需要医生开药", "593": "桐谷先生还在头痛 需要医生开药", "594": "桐谷先生还在头痛 需要医生开药", "595": "桐谷先生还在头痛 需要医生开药", "596": "桐谷先生还在头痛 需要医生开药", "597": "桐谷先生还在头痛 需要医生开药", "598": "桐谷先生还在头痛 需要医生开药", "599": "桐谷先生还在头痛 需要医生开药", "600": "桐谷先生还在头痛 需要医生开药", "601": "桐谷先生还在头痛 需要医生开药", "602": "让你久等了 北冈小姐", "603": "让你久等了 北冈小姐", "604": "让你久等了 北冈小姐", "605": "让你久等了 北冈小姐", "606": "让你久等了 北冈小姐", "607": "让你久等了 北冈小姐", "608": "让你久等了 北冈小姐", "609": "", "610": "", "611": "不会", "612": "不会", "613": "能让权藤医生亲自指导我 这可是千载难逢的机会", "614": "能让权藤医生亲自指导我 这可是千载难逢的机会", "615": "能让权藤医生亲自指导我 这可是千载难逢的机会", "616": "能让权藤医生亲自指导我 这可是千载难逢的机会", "617": "能让权藤医生亲自指导我 这可是千载难逢的机会", "618": "能让权藤医生亲自指导我 这可是千载难逢的机会", "619": "能让权藤医生亲自指导我 这可是千载难逢的机会", "620": "能让权藤医生亲自指导我 这可是千载难逢的机会", "621": "完全不觉得麻烦", "622": "完全不觉得麻烦", "623": "完全不觉得麻烦", "624": "完全不觉得麻烦", "625": "你说的指导是", "626": "你说的指导是", "627": "你说的指导是", "628": "你说的指导是", "629": "你说的指导是", "630": "你说的指导是", "631": "", "632": "", "633": "", "634": "", "635": "", "636": "", "637": "", "638": "", "639": "", "640": "", "641": "", "642": "", "643": "", "644": "", "645": "请不要这样 不要", "646": "请不要这样 不要", "647": "请不要这样 不要", "648": "请不要这样 不要", "649": "请不要这样 不要", "650": "请不要这样 不要", "651": "请不要这样 不要", "652": "请不要这样 不要", "653": "请不要这样 不要", "654": "请不要这样 不要", "655": "请不要这样 不要", "656": "请不要这样 不要", "657": "", "658": "", "659": "", "660": "请住手", "661": "请住手", "662": "请住手", "663": "请住手", "664": "请住手", "665": "", "666": "", "667": "", "668": "", "669": "", "670": "", "671": "", "672": "", "673": "", "674": "", "675": "等一下 你在做什么", "676": "等一下 你在做什么", "677": "等一下 你在做什么", "678": "等一下 你在做什么", "679": "等一下 你在做什么", "680": "等一下 你在做什么", "681": "等一下 你在做什么", "682": "请住手 等一下 你干什么", "683": "请住手 等一下 你干什么", "684": "请住手 等一下 你干什么", "685": "请住手 等一下 你干什么", "686": "请住手 等一下 你干什么", "687": "请住手 等一下 你干什么", "688": "请住手 等一下 你干什么", "689": "请住手 等一下 你干什么", "690": "请住手 等一下 你干什么", "691": "请住手 等一下 你干什么", "692": "请住手 等一下 你干什么", "693": "---", "694": "", "695": "---", "696": "", "697": "", "698": "", "699": "", "700": "", "701": "好痛 北冈小姐", "702": "好痛 北冈小姐", "703": "好痛 北冈小姐", "704": "好痛 北冈小姐", "705": "好痛 北冈小姐", "706": "好痛 北冈小姐", "707": "好痛 北冈小姐", "708": "好痛 北冈小姐", "709": "好痛 北冈小姐", "710": "好痛 北冈小姐", "711": "你对我做这种事", "712": "你对我做这种事", "713": "你对我做这种事", "714": "你对我做这种事", "715": "你对我做这种事", "716": "你对我做这种事", "717": "你对我做这种事", "718": "你对我做这种事", "719": "你对我做这种事", "720": "你对我做这种事", "721": "你对我做这种事", "722": "就不怕你妈妈出什么事吗", "723": "就不怕你妈妈出什么事吗", "724": "就不怕你妈妈出什么事吗", "725": "就不怕你妈妈出什么事吗", "726": "就不怕你妈妈出什么事吗", "727": "就不怕你妈妈出什么事吗", "728": "就不怕你妈妈出什么事吗", "729": "就不怕你妈妈出什么事吗", "730": "就不怕你妈妈出什么事吗", "731": "就不怕你妈妈出什么事吗", "732": "就不怕你妈妈出什么事吗", "733": "就不怕你妈妈出什么事吗", "734": "", "735": "", "736": "", "737": "", "738": "你妈妈住院的医院", "739": "你妈妈住院的医院", "740": "你妈妈住院的医院", "741": "你妈妈住院的医院", "742": "你妈妈住院的医院", "743": "你妈妈住院的医院", "744": "你妈妈住院的医院", "745": "你妈妈住院的医院", "746": "你妈妈住院的医院", "747": "你妈妈住院的医院", "748": "就是这间医院的旗下医院", "749": "就是这间医院的旗下医院", "750": "就是这间医院的旗下医院", "751": "就是这间医院的旗下医院", "752": "就是这间医院的旗下医院", "753": "就是这间医院的旗下医院", "754": "就是这间医院的旗下医院", "755": "就是这间医院的旗下医院", "756": "就是这间医院的旗下医院", "757": "就是这间医院的旗下医院", "758": "而且负责治疗你妈妈的医生", "759": "而且负责治疗你妈妈的医生", "760": "而且负责治疗你妈妈的医生", "761": "而且负责治疗你妈妈的医生", "762": "而且负责治疗你妈妈的医生", "763": "而且负责治疗你妈妈的医生", "764": "而且负责治疗你妈妈的医生", "765": "而且负责治疗你妈妈的医生", "766": "而且负责治疗你妈妈的医生", "767": "而且负责治疗你妈妈的医生", "768": "而且负责治疗你妈妈的医生", "769": "而且负责治疗你妈妈的医生", "770": "", "771": "是我的手下", "772": "是我的手下", "773": "是我的手下", "774": "是我的手下", "775": "是我的手下", "776": "是我的手下", "777": "是我的手下", "778": "是我的手下", "779": "是我的手下", "780": "是我的手下", "781": "是我的手下", "782": "", "783": "", "784": "", "785": "这是什么意思呢 你应该明白吧", "786": "这是什么意思呢 你应该明白吧", "787": "这是什么意思呢 你应该明白吧", "788": "这是什么意思呢 你应该明白吧", "789": "这是什么意思呢 你应该明白吧", "790": "这是什么意思呢 你应该明白吧", "791": "这是什么意思呢 你应该明白吧", "792": "这是什么意思呢 你应该明白吧", "793": "这是什么意思呢 你应该明白吧", "794": "", "795": "", "796": "", "797": "", "798": "", "799": "聪明的你", "800": "聪明的你", "801": "聪明的你", "802": "聪明的你", "803": "聪明的你", "804": "聪明的你", "805": "聪明的你", "806": "应该已经察觉到 至今为止的一切了吧", "807": "应该已经察觉到 至今为止的一切了吧", "808": "应该已经察觉到至今为止的一切了吧", "809": "应该已经察觉到 至今为止的一切了吧", "810": "应该已经察觉到 至今为止的一切了吧", "811": "应该已经察觉到 至今为止的一切了吧", "812": "应该已经察觉到 至今为止的一切了吧", "813": "应该已经察觉到 至今为止的一切了吧", "814": "应该已经察觉到 至今为止的一切了吧", "815": "应该已经察觉到 至今为止的一切了吧", "816": "应该已经察觉到 至今为止的一切了吧", "817": "", "818": "你妈妈的生命", "819": "你妈妈的生命", "820": "你妈妈的生命", "821": "你妈妈的生命", "822": "你妈妈的生命", "823": "你妈妈的生命", "824": "你妈妈的生命", "825": "你妈妈的生命", "826": "你妈妈的生命", "827": "你妈妈的生命", "828": "你妈妈的生命", "829": "你妈妈的生命", "830": "", "831": "等同于掌握在你的手中了", "832": "等同于掌握在你的手中了", "833": "等同于掌握在你的手中了", "834": "等同于掌握在你的手中了", "835": "等同于掌握在你的手中了", "836": "等同于掌握在你的手中了", "837": "等同于掌握在你的手中了", "838": "等同于掌握在你的手中了", "839": "等同于掌握在你的手中了", "840": "等同于掌握在你的手中了", "841": "等同于掌握在你的手中了", "842": "等同于掌握在你的手中了", "843": "等同于掌握在你的手中了", "844": "等同于掌握在你的手中了", "845": "等同于掌握在你的手中了", "846": "", "847": "", "848": "", "849": "", "850": "", "851": "", "852": "", "853": "", "854": "", "855": "", "856": "", "857": "", "858": "", "859": "", "860": "", "861": "", "862": "", "863": "---", "864": "", "865": "", "866": "---", "867": "", "868": "", "869": "", "870": "", "871": "", "872": "", "873": "", "874": "", "875": "请住手", "876": "请住手", "877": "请住手", "878": "请住手", "879": "请住手", "880": "放心吧 放心吧", "881": "放心吧 放心吧", "882": "放心吧 放心吧", "883": "放心吧 放心吧", "884": "放心吧 放心吧", "885": "放心吧 放心吧", "886": "", "887": "", "888": "", "889": "", "890": "", "891": "", "892": "就当作是我的触诊吧", "893": "就当作是我的触诊吧", "894": "就当作是我的触诊吧", "895": "就当作是我的触诊吧", "896": "就当作是我的触诊吧", "897": "就当作是我的触诊吧", "898": "就当作是我的触诊吧", "899": "就当作是我的触诊吧", "900": "就当作是我的触诊吧", "901": "就当作是我的触诊吧", "902": "就当作是我的触诊吧", "903": "就当作是我的触诊吧", "904": "就当作是我的触诊吧", "905": "就当作是我的触诊吧", "906": "请住手", "907": "请住手", "908": "请住手", "909": "请住手", "910": "请住手", "911": "", "912": "", "913": "", "914": "", "915": "", "916": "", "917": "", "918": "", "919": "HTML", "920": "", "921": "", "922": "不要做这种事", "923": "不要做这种事", "924": "不要做这种事", "925": "不要做这种事", "926": "不要做这种事", "927": "不要做这种事", "928": "", "929": "------------------", "930": "", "931": "", "932": "", "933": "", "934": "", "935": "---------------", "936": "", "937": "", "938": "", "939": "", "940": "", "941": "", "942": "---------------", "943": "---------------", "944": "---------------", "945": "", "946": "------------------", "947": "", "948": "", "949": "医生 请住手 安静点", "950": "医生 请住手 安静点", "951": "医生 请住手 安静点", "952": "医生 请住手 安静点", "953": "医生 请住手 安静点", "954": "医生 请住手 安静点", "955": "医生 请住手 安静点", "956": "医生 请住手 安静点", "957": "医生 请住手 安静点", "958": "医生 请住手 安静点", "959": "医生 请住手 安静点", "960": "医生 请住手 安静点", "961": "---", "962": "", "963": "---", "964": "---", "965": "你想想妈妈的生命", "966": "你想想妈妈的生命", "967": "你想想妈妈的生命", "968": "你想想妈妈的生命", "969": "你想想妈妈的生命", "970": "你想想妈妈的生命", "971": "你想想妈妈的生命", "972": "你想想妈妈的生命", "973": "你想想妈妈的生命", "974": "你应该知道作为女儿 该做些什么才对的", "975": "你应该知道作为女儿 该做些什么才对的", "976": "你应该知道作为女儿 该做些什么才对的", "977": "你应该知道作为女儿 该做些什么才对的", "978": "你应该知道作为女儿 该做些什么才对的", "979": "你应该知道作为女儿 该做些什么才对的", "980": "你应该知道作为女儿 该做些什么才对的", "981": "你应该知道作为女儿 该做些什么才对的", "982": "你应该知道作为女儿 该做些什么才对的", "983": "你应该知道作为女儿 该做些什么才对的", "984": "你应该知道作为女儿 该做些什么才对的", "985": "---", "986": "", "987": "", "988": "", "989": "", "990": "", "991": "", "992": "真的原谅我吧 为什么权藤医生要做这种事呢", "993": "真的原谅我吧 为什么权藤医生要做这种事呢", "994": "真的原谅我吧 为什么权藤医生要做这种事呢", "995": "真的原谅我吧 为什么权藤医生要做这种事呢", "996": "真的原谅我吧 为什么权藤医生要做这种事呢", "997": "真的原谅我吧 为什么权藤医生要做这种事呢", "998": "真的原谅我吧 为什么权藤医生要做这种事呢", "999": "真的原谅我吧 为什么权藤医生要做这种事呢", "1000": "真的原谅我吧 为什么权藤医生要做这种事呢", "1001": "真的原谅我吧 为什么权藤医生要做这种事呢", "1002": "真的原谅我吧 为什么权藤医生要做这种事呢", "1003": "", "1004": "这也是没办法的事", "1005": "这也是没办法的事", "1006": "这也是没办法的事", "1007": "这也是没办法的事", "1008": "这也是没办法的事", "1009": "在这家医院呢", "1010": "在这家医院呢", "1011": "在这家医院呢", "1012": "在这家医院呢", "1013": "在这家医院呢", "1014": "为了吸引入住 单人病房的VIP患者", "1015": "为了吸引入住 单人病房的VIP患者", "1016": "为了吸引入住 单人病房的VIP患者", "1017": "为了吸引入住 单人病房的VIP患者", "1018": "为了吸引入住 单人病房的VIP患者", "1019": "为了吸引入住 单人病房的VIP患者", "1020": "为了吸引入住 单人病房的VIP患者", "1021": "为了吸引入住 单人病房的VIP患者", "1022": "为了吸引入住 单人病房的VIP患者", "1023": "为了吸引入住 单人病房的VIP患者", "1024": "为了吸引入住 单人病房的VIP患者", "1025": "为了吸引入住 单人病房的VIP患者", "1026": "为了吸引入住 单人病房的VIP患者", "1027": "为了吸引入住 单人病房的VIP患者", "1028": "我们提供了一种特别服务", "1029": "我们提供了一种特别服务", "1030": "我们提供了一种特别服务", "1031": "我们提供了一种特别服务", "1032": "我们提供了一种特别服务", "1033": "我们提供了一种特别服务", "1034": "我们提供了一种特别服务", "1035": "", "1036": "", "1037": "", "1038": "", "1039": "北冈小姐你", "1040": "北冈小姐你", "1041": "北冈小姐你", "1042": "北冈小姐你", "1043": "北冈小姐你", "1044": "北冈小姐你", "1045": "北冈小姐你", "1046": "北冈小姐你", "1047": "看起来很纯真嘛", "1048": "看起来很纯真嘛", "1049": "看起来很纯真嘛", "1050": "看起来很纯真嘛", "1051": "看起来很纯真嘛", "1052": "看起来很纯真嘛", "1053": "看起来很纯真嘛", "1054": "看起来很纯真嘛", "1055": "看起来很纯真嘛", "1056": "看起来很纯真嘛", "1057": "看起来很纯真嘛", "1058": "不这么做的话 可没法胜任患者的对象", "1059": "不这么做的话可没法胜任患者的对象", "1060": "不这么做的话 可没法胜任患者的对象", "1061": "不这么做的话 可没法胜任患者的对象", "1062": "不这么做的话 可没法胜任患者的对象", "1063": "不这么做的话可没法胜任患者的对象", "1064": "不这么做的话可没法胜任患者的对象", "1065": "不这么做的话可没法胜任患者的对象", "1066": "不这么做的话 可没法胜任患者的对象", "1067": "不这么做的话可没法胜任患者的对象", "1068": "不这么做的话可没法胜任患者的对象", "1069": "不这么做的话 可没法胜任患者的对象", "1070": "不这么做的话 可没法胜任患者的对象", "1071": "不这么做的话 可没法胜任患者的对象", "1072": "不这么做的话 可没法胜任患者的对象", "1073": "不这么做的话可没法胜任患者的对象", "1074": "", "1075": "", "1076": "", "1077": "", "1078": "", "1079": "请住手 这个跟照顾患者没关系", "1080": "请住手 这个跟照顾患者没关系", "1081": "请住手 这个跟照顾患者没关系", "1082": "请住手 这个跟照顾患者没关系", "1083": "请住手 这个跟照顾患者没关系", "1084": "请住手 这个跟照顾患者没关系", "1085": "请住手 这个跟照顾患者没关系", "1086": "请住手 这个跟照顾患者没关系", "1087": "请住手 这个跟照顾患者没关系", "1088": "请住手 这个跟照顾患者没关系", "1089": "", "1090": "只会说漂亮话 在这个世上是行不通的", "1091": "只会说漂亮话在这个世上是行不通的", "1092": "只会说漂亮话 在这个世上是行不通的", "1093": "只会说漂亮话在这个世上是行不通的", "1094": "只会说漂亮话 在这个世上是行不通的", "1095": "只会说漂亮话在这个世上是行不通的", "1096": "只会说漂亮话 在这个世上是行不通的", "1097": "只会说漂亮话 在这个世上是行不通的", "1098": "只会说漂亮话在这个世上是行不通的", "1099": "只会说漂亮话在这个世上是行不通的", "1100": "只会说漂亮话 在这个世上是行不通的", "1101": "只会说漂亮话在这个世上是行不通的", "1102": "只会说漂亮话在这个世上是行不通的", "1103": "只会说漂亮话 在这个世上是行不通的", "1104": "---", "1105": "", "1106": "", "1107": "", "1108": "", "1109": "2.0", "1110": "", "1111": "", "1112": "JILLIAN MILLER", "1113": "女厨 黑袜", "1114": "北医三附院", "1115": "身体放松一点吧", "1116": "身体放松一点吧", "1117": "身体放松一点吧", "1118": "身体放松一点吧", "1119": "身体放松一点吧", "1120": "身体放松一点吧", "1121": "", "1122": "", "1123": "", "1124": "", "1125": "", "1126": "", "1127": "", "1128": "", "1129": "", "1130": "", "1131": "", "1132": "", "1133": "", "1134": "", "1135": "", "1136": "", "1137": "", "1138": "", "1139": "", "1140": "", "1141": "", "1142": "", "1143": "", "1144": "", "1145": "", "1146": "", "1147": "你要练习跟患者接吻", "1148": "你要练习跟患者接吻", "1149": "你要练习跟患者接吻", "1150": "你要练习跟患者接吻", "1151": "你要练习跟患者接吻", "1152": "你要练习跟患者接吻", "1153": "你要练习跟患者接吻", "1154": "你要练习跟患者接吻", "1155": "你要练习跟患者接吻", "1156": "你要练习跟患者接吻", "1157": "你要练习跟患者接吻", "1158": "你要练习跟患者接吻", "1159": "你要练习跟患者接吻", "1160": "你要练习跟患者接吻", "1161": "你要练习跟患者接吻", "1162": "你要练习跟患者接吻", "1163": "", "1164": "", "1165": "", "1166": "", "1167": "", "1168": "", "1169": "", "1170": "", "1171": "", "1172": "", "1173": "", "1174": "", "1175": "", "1176": "", "1177": "", "1178": "", "1179": "", "1180": "", "1181": "", "1182": "", "1183": "", "1184": "北冈小姐 面对患者的时候呢 要像这样脱掉对方的衣服", "1185": "北冈小姐 面对患者的时候呢 要像这样脱掉对方的衣服", "1186": "北冈小姐 面对患者的时候呢 要像这样脱掉对方的衣服", "1187": "北冈小姐 面对患者的时候呢 要像这样脱掉对方的衣服", "1188": "北冈小姐 面对患者的时候呢 要像这样脱掉对方的衣服", "1189": "北冈小姐 面对患者的时候呢 要像这样脱掉对方的衣服", "1190": "北冈小姐 面对患者的时候呢 要像这样脱掉对方的衣服", "1191": "北冈小姐 面对患者的时候呢 要像这样脱掉对方的衣服", "1192": "北冈小姐 面对患者的时候呢 要像这样脱掉对方的衣服", "1193": "北冈小姐 面对患者的时候呢 要像这样脱掉对方的衣服", "1194": "北冈小姐 面对患者的时候呢 要像这样脱掉对方的衣服", "1195": "北冈小姐 面对患者的时候呢 要像这样脱掉对方的衣服", "1196": "北冈小姐 面对患者的时候呢 要像这样脱掉对方的衣服", "1197": "北冈小姐 面对患者的时候呢 要像这样脱掉对方的衣服", "1198": "北冈小姐 面对患者的时候呢 要像这样脱掉对方的衣服", "1199": "北冈小姐 面对患者的时候呢 要像这样脱掉对方的衣服", "1200": "北冈小姐 面对患者的时候呢 要像这样脱掉对方的衣服", "1201": "", "1202": "", "1203": "", "1204": "", "1205": "", "1206": "", "1207": "", "1208": "", "1209": "", "1210": "", "1211": "", "1212": "", "1213": "", "1214": "", "1215": "---", "1216": "---", "1217": "", "1218": "---", "1219": "患者呀", "1220": "患者呀", "1221": "患者呀", "1222": "患者呀", "1223": "患者呀", "1224": "患者呀", "1225": "", "1226": "并不是只有年轻英后的男性而已", "1227": "并不是只有年轻英后的男性而已", "1228": "并不是只有年轻英后的男性而已", "1229": "并不是只有年轻英后的男性而已", "1230": "并不是只有年轻英后的男性而已", "1231": "并不是只有年轻英后的男性而已", "1232": "并不是只有年轻英后的男性而已", "1233": "并不是只有年轻英后的男性而已", "1234": "并不是只有年轻英后的男性而已", "1235": "并不是只有年轻英后的男性而已", "1236": "并不是只有年轻英后的男性而已", "1237": "有的远比我还要", "1238": "有的远比我还要", "1239": "有的远比我还要", "1240": "有的远比我还要", "1241": "有的远比我还要", "1242": "有的远比我还要", "1243": "偶尔也要接待 比自己年长的大叔", "1244": "偶尔也要接待 比自己年长的大叔", "1245": "偶尔也要接待 比自己年长的大叔", "1246": "偶尔也要接待 比自己年长的大叔", "1247": "偶尔也要接待 比自己年长的大叔", "1248": "偶尔也要接待 比自己年长的大叔", "1249": "偶尔也要接待 比自己年长的大叔", "1250": "偶尔也要接待 比自己年长的大叔", "1251": "偶尔也要接待 比自己年长的大叔", "1252": "偶尔也要接待 比自己年长的大叔", "1253": "偶尔也要接待 比自己年长的大叔", "1254": "偶尔也要接待 比自己年长的大叔", "1255": "偶尔也要接待 比自己年长的大叔", "1256": "偶尔也要接待 比自己年长的大叔", "1257": "偶尔也要接待 比自己年长的大叔", "1258": "偶尔也要接待 比自己年长的大叔", "1259": "", "1260": "", "1261": "", "1262": "", "1263": "", "1264": "", "1265": "", "1266": "", "1267": "", "1268": "", "1269": "", "1270": "", "1271": "", "1272": "", "1273": "", "1274": "", "1275": "", "1276": "医生请住手", "1277": "医生请住手", "1278": "医生请住手", "1279": "医生请住手", "1280": "医生请住手", "1281": "医生请住手", "1282": "", "1283": "真是漂亮的后背", "1284": "真是漂亮的后背", "1285": "真是漂亮的后背", "1286": "真是漂亮的后背", "1287": "真是漂亮的后背", "1288": "真是漂亮的后背", "1289": "说不定会有喜欢后背的客人来呢", "1290": "说不定会有 喜欢后背的客人来呢", "1291": "说不定会有 喜欢后背的客人来呢", "1292": "说不定会有 喜欢后背的客人来呢", "1293": "说不定会有喜欢后背的客人来呢", "1294": "说不定会有喜欢后背的客人来呢", "1295": "说不定会有喜欢后背的客人来呢", "1296": "说不定会有喜欢后背的客人来呢", "1297": "说不定会有喜欢后背的客人来呢", "1298": "说不定会有 喜欢后背的客人来呢", "1299": "说不定会有喜欢后背的客人来呢", "1300": "", "1301": "", "1302": "", "1303": "", "1304": "", "1305": "请住手 不要", "1306": "请住手 不要", "1307": "请住手 不要", "1308": "请住手 不要", "1309": "请住手 不要", "1310": "请住手 不要", "1311": "请住手 不要", "1312": "", "1313": "", "1314": "", "1315": "", "1316": "", "1317": "", "1318": "", "1319": "", "1320": "来 把手拿开", "1321": "来 把手拿开", "1322": "来 把手拿开", "1323": "来 把手拿开", "1324": "来 把手拿开", "1325": "来 把手拿开", "1326": "你以为你的妈妈的生命 是谁掌握着呀", "1327": "你以为你的妈妈的生命 是谁掌握着呀", "1328": "你以为你的妈妈的生命是谁掌握着呀", "1329": "你以为你的妈妈的生命是谁掌握着呀", "1330": "你以为你的妈妈的生命 是谁掌握着呀", "1331": "你以为你的妈妈的生命是谁掌握着呀", "1332": "你以为你的妈妈的生命是谁掌握着呀", "1333": "你以为你的妈妈的生命是谁掌握着呀", "1334": "你以为你的妈妈的生命 是谁掌握着呀", "1335": "你以为你的妈妈的生命 是谁掌握着呀", "1336": "你以为你的妈妈的生命 是谁掌握着呀", "1337": "你以为你的妈妈的生命 是谁掌握着呀", "1338": "你以为你的妈妈的生命 是谁掌握着呀", "1339": "你以为你的妈妈的生命是谁掌握着呀", "1340": "你以为你的妈妈的生命 是谁掌握着呀", "1341": "你以为你的妈妈的生命 是谁掌握着呀", "1342": "", "1343": "", "1344": "", "1345": "", "1346": "", "1347": "", "1348": "---", "1349": "", "1350": "", "1351": "", "1352": "---", "1353": "", "1354": "", "1355": "", "1356": "不抵抗的话就马上结束了", "1357": "不抵抗的话就马上结束了", "1358": "不抵抗的话就马上结束了", "1359": "不抵抗的话就马上结束了", "1360": "不抵抗的话就马上结束了", "1361": "不抵抗的话就马上结束了", "1362": "不抵抗的话就马上结束了", "1363": "不抵抗的话就马上结束了", "1364": "不抵抗的话就马上结束了", "1365": "只是如果反抗的话", "1366": "只是如果反抗的话", "1367": "只是如果反抗的话", "1368": "只是 如果反抗的话", "1369": "只是如果反抗的话", "1370": "只是如果反抗的话", "1371": "只是如果反抗的话", "1372": "只是如果反抗的话", "1373": "我多少也会变得粗暴起来", "1374": "我多少也会变得粗暴起来", "1375": "我多少也会变得粗暴起来", "1376": "我多少也会变得粗暴起来", "1377": "我多少也会变得粗暴起来", "1378": "我多少也会变得粗暴起来", "1379": "我多少也会变得粗暴起来", "1380": "我多少也会变得粗暴起来", "1381": "我多少也会变得粗暴起来", "1382": "---", "1383": "---", "1384": "", "1385": "", "1386": "", "1387": "", "1388": "", "1389": "", "1390": "", "1391": "", "1392": "", "1393": "", "1394": "", "1395": "", "1396": "", "1397": "", "1398": "", "1399": "", "1400": "", "1401": "", "1402": "", "1403": "------------------", "1404": "", "1405": "", "1406": "", "1407": "", "1408": "", "1409": "", "1410": "", "1411": "", "1412": "", "1413": "", "1414": "", "1415": "", "1416": "", "1417": "", "1418": "", "1419": ".", "1420": "年轻又有弹性的屁股", "1421": "年轻又有弹性的屁股", "1422": "年轻又有弹性的屁股", "1423": "年轻又有弹性的屁股", "1424": "年轻又有弹性的屁股", "1425": "年轻又有弹性的屁股", "1426": "年轻又有弹性的屁股", "1427": "年轻又有弹性的屁股", "1428": "臀大肌很发达", "1429": "臀大肌很发达", "1430": "臀大肌很发达", "1431": "臀大肌很发达", "1432": "臀大肌很发达", "1433": "臀大肌很发达", "1434": "臀大肌很发达", "1435": "臀大肌很发达", "1436": "臀大肌很发达", "1437": "应该能生下健康的孩子", "1438": "应该能生下健康的孩子", "1439": "应该能生下健康的孩子", "1440": "健康的孩子", "1441": "应该熊孩子", "1442": "应该熊生,健康的孩子", "1443": "应该能生,健康的孩子", "1444": "应该熊生,健康的孩子", "1445": "应该能生下健康的孩子", "1446": "请住手", "1447": "请住手", "1448": "请住手", "1449": "请住手", "1450": "作为护士是很棒的身材", "1451": "作为护士是很棒的身材", "1452": "作为护士是很棒的身材", "1453": "作为护士是很棒的身材", "1454": "作为护士是很棒的身材", "1455": "作为护士是很棒的身材", "1456": "作为护士是很棒的身材", "1457": "作为护士是很棒的身材", "1458": "作为护士是很棒的身材", "1459": "作为护士是很棒的身材", "1460": "作为护士是很棒的身材", "1461": "作为护士是很棒的身材", "1462": "", "1463": "", "1464": "", "1465": "", "1466": "---", "1467": "", "1468": "", "1469": "", "1470": "", "1471": "", "1472": "", "1473": "", "1474": "", "1475": "", "1476": "", "1477": "", "1478": "", "1479": "", "1480": "", "1481": "", "1482": "", "1483": "", "1484": "", "1485": "", "1486": "---", "1487": "", "1488": "", "1489": "", "1490": "", "1491": "", "1492": "", "1493": "", "1494": "", "1495": "", "1496": "", "1497": "", "1498": "", "1499": "", "1500": "", "1501": "", "1502": "", "1503": "", "1504": "", "1505": "", "1506": "", "1507": "", "1508": "", "1509": "", "1510": "", "1511": "", "1512": "", "1513": "", "1514": "身体不要用力了", "1515": "身体不要用力了", "1516": "身体不要用力了", "1517": "身体不要用力了", "1518": "身体不要用力了", "1519": "B", "1520": "", "1521": "", "1522": "", "1523": "", "1524": "", "1525": "", "1526": "", "1527": "", "1528": "", "1529": "", "1530": "", "1531": "", "1532": "", "1533": "", "1534": "", "1535": "", "1536": "", "1537": "", "1538": "", "1539": "", "1540": "", "1541": "---", "1542": "", "1543": "", "1544": "", "1545": "", "1546": "", "1547": "", "1548": "", "1549": "", "1550": "", "1551": "", "1552": "", "1553": "", "1554": "", "1555": "", "1556": "", "1557": "", "1558": "", "1559": "", "1560": "", "1561": "", "1562": "", "1563": "", "1564": "", "1565": "", "1566": "---", "1567": "即使吵闹 也不会有人来的", "1568": "即使吵闹 也不会有人来的", "1569": "即使吵闹 也不会有人来的", "1570": "即使吵闹 也不会有人来的", "1571": "即使吵闹 也不会有人来的", "1572": "即使吵闹 也不会有人来的", "1573": "即使吵闹 也不会有人来的", "1574": "即使吵闹 也不会有人来的", "1575": "即使吵闹 也不会有人来的", "1576": "即使吵闹 也不会有人来的", "1577": "这里只有你我两人", "1578": "这里只有你我两人", "1579": "这里只有你我两人", "1580": "这里只有你我两人", "1581": "这里只有你我两人", "1582": "这里只有你我两人", "1583": "这里只有你我两人", "1584": "这里只有你我两人", "1585": "这里只有你我两人", "1586": "这里只有你我两人", "1587": "把手拿开", "1588": "把手拿开", "1589": "把手拿开", "1590": "把手拿开", "1591": "把手拿开", "1592": "", "1593": "接下来检查奶子的状况", "1594": "接下来检查奶子的状况", "1595": "接下来检查奶子的状况", "1596": "接下来检查奶子的状况", "1597": "接下来检查奶子的状况", "1598": "接下来检查奶子的状况", "1599": "女妾来坐奶子的状况", "1600": "妾也来坐奶江的状况。", "1601": "", "1602": "", "1603": "", "1604": "", "1605": "", "1606": "", "1607": "", "1608": "", "1609": "", "1610": "", "1611": "", "1612": "", "1613": "", "1614": "", "1615": "腹部没什么问题呢", "1616": "腹部没什么问题呢", "1617": "腹部没什么问题呢", "1618": "腹部没什么问题呢", "1619": "腹部没什么问题呢", "1620": "腹部没什么问题呢", "1621": "腹部没什么问题呢", "1622": "", "1623": "没有皮肤科医生出场的机会了", "1624": "没有皮肤科医生出场的机会了", "1625": "没有皮肤科医生出场的机会了", "1626": "没有皮肤科医生出场的机会了", "1627": "没有皮肤科医生出场的机会了", "1628": "没有皮肤科医生出场的机会了", "1629": "没有皮肤科医生出场的机会了", "1630": "没有皮肤科医生出场的机会了", "1631": "没有皮肤科医生出场的机会了", "1632": "", "1633": "", "1634": "", "1635": "", "1636": "", "1637": "", "1638": "我看看", "1639": "我看看", "1640": "我看看", "1641": "我看看", "1642": "我看看", "1643": "这可是女性的重要部位 我要直接摸了", "1644": "这可是女性的重要部位 我要直接摸了", "1645": "这可是女性的重要部位 我要直接摸了", "1646": "这可是女性的重要部位 我要直接摸了", "1647": "这可是女性的重要部位 我要直接摸了", "1648": "这可是女性的重要部位 我要直接摸了", "1649": "请住手 让我来帮你检查吧", "1650": "请住手 让我来帮你检查吧", "1651": "请住手 让我来帮你检查吧", "1652": "请住手 让我来帮你检查吧", "1653": "请住手 让我来帮你检查吧", "1654": "请住手 让我来帮你检查吧", "1655": "请住手 让我来帮你检查吧", "1656": "请住手 让我来帮你检查吧", "1657": "", "1658": "", "1659": "", "1660": "", "1661": "", "1662": "", "1663": "", "1664": "", "1665": "", "1666": "把手拿开啦", "1667": "把手拿开啦", "1668": "把手拿开啦", "1669": "把手拿开啦", "1670": "把手拿开啦", "1671": "", "1672": "", "1673": "别让我重复同样的话", "1674": "别让我重复同样的话", "1675": "别让我重复同样的话", "1676": "别让我重复同样的话", "1677": "别让我重复同样的话", "1678": "别让我重复同样的话", "1679": "", "1680": "妈妈得救不了也没关系吗", "1681": "妈妈得救不了也没关系吗", "1682": "妈妈得救不了也没关系吗", "1683": "妈妈得救不了也没关系吗", "1684": "妈妈得救不了也没关系吗", "1685": "妈妈得救不了也没关系吗", "1686": "妈妈得救不了也没关系吗", "1687": "妈妈得救不了也没关系吗", "1688": "请住手", "1689": "请住手", "1690": "请住手", "1691": "请住手", "1692": "请住手", "1693": "", "1694": "", "1695": "", "1696": "", "1697": "", "1698": "", "1699": "", "1700": "", "1701": "怎么 你嘴上说的倒是好听", "1702": "怎么 你嘴上说的倒是好听", "1703": "怎么 你嘴上说的倒是好听", "1704": "怎么 你嘴上说的倒是好听", "1705": "怎么 你嘴上说的倒是好听", "1706": "怎么 你嘴上说的倒是好听", "1707": "怎么 你嘴上说的倒是好听", "1708": "怎么 你嘴上说的倒是好听", "1709": "怎么 你嘴上说的倒是好听", "1710": "", "1711": "", "1712": "乳头都这么硬了", "1713": "乳头都这么硬了", "1714": "乳头都这么硬了", "1715": "乳头都这么硬了", "1716": "乳头都这么硬了", "1717": "乳头都这么硬了", "1718": "乳头都这么硬了", "1719": "", "1720": "", "1721": "四学综合硬", "1722": "国家质监来说呢 有硬", "1723": "国家质保来说呢 有缺失补交硬", "1724": "医学发达来说呢 有快感的话就不硬", "1725": "医学角度来说呢 有快感的话就会变硬", "1726": "医学角度来说呢 有快感的话就会变硬", "1727": "医学角度来说呢 有快感的话就会变硬", "1728": "医学角度来说呢 有快感的话就会变硬", "1729": "医学角度来说呢 有快感的话就会变硬", "1730": "医学角度来说呢 有快感的话就会变硬", "1731": "医学角度来说呢 有快感的话就会变硬", "1732": "医学角度来说呢 有快感的话就会变硬", "1733": "医学角度来说呢 有快感的话就会变硬", "1734": "医学角度来说呢 有快感的话就会变硬", "1735": "医学角度来说呢 有快感的话就会变硬", "1736": "", "1737": "", "1738": "", "1739": "---", "1740": "", "1741": "", "1742": "", "1743": "---", "1744": "---", "1745": "", "1746": "", "1747": "", "1748": "", "1749": "", "1750": "", "1751": "", "1752": "", "1753": "", "1754": "", "1755": "", "1756": "", "1757": "", "1758": "", "1759": "", "1760": "---", "1761": "", "1762": "", "1763": "", "1764": "", "1765": "", "1766": "", "1767": "", "1768": "", "1769": "", "1770": "", "1771": "", "1772": "", "1773": "", "1774": "", "1775": "", "1776": "", "1777": "", "1778": "怎么了 忍着不发出声音吗", "1779": "怎么了 忍着不发出声音吗", "1780": "怎么了 忍着不发出声音吗", "1781": "怎么了 忍着不发出声音吗", "1782": "怎么了 忍着不发出声音吗", "1783": "怎么了 忍着不发出声音吗", "1784": "怎么了 忍着不发出声音吗", "1785": "", "1786": "", "1787": "", "1788": "", "1789": "", "1790": "你是怕羞的孩子吗", "1791": "你是怕羞的孩子吗", "1792": "你是怕羞的孩子吗", "1793": "你是怕羞的孩子吗", "1794": "你是怕羞的孩子吗", "1795": "你是怕羞的孩子吗", "1796": "你是怕羞的孩子吗", "1797": "你是怕羞的孩子吗", "1798": "看吧 你自己看", "1799": "看吧 你自己看", "1800": "看吧 你自己看", "1801": "看吧 你自己看", "1802": "看吧 你自己看", "1803": "左边的乳头和右边的乳头 硬度完全不一样", "1804": "左边的乳头和右边的乳头 硬度完全不一样", "1805": "左边的乳头和右边的乳头 硬度完全不一样", "1806": "左边的乳头和右边的乳头 硬度完全不一样", "1807": "左边的乳头和右边的乳头 硬度完全不一样", "1808": "左边的乳头和右边的乳头 硬度完全不一样", "1809": "左边的乳头和右边的乳头 硬度完全不一样", "1810": "左边的乳头和右边的乳头 硬度完全不一样", "1811": "左边的乳头和右边的乳头 硬度完全不一样", "1812": "左边的乳头和右边的乳头 硬度完全不一样", "1813": "左边的乳头和右边的乳头 硬度完全不一样", "1814": "左边的乳头和右边的乳头 硬度完全不一样", "1815": "", "1816": "", "1817": "", "1818": "这个也变硬了", "1819": "这个也变硬了", "1820": "这个也变硬了", "1821": "这个也变硬了", "1822": "这个也变硬了", "1823": "这个也变硬了", "1824": "", "1825": "", "1826": "", "1827": "", "1828": "", "1829": "", "1830": "", "1831": "", "1832": "", "1833": "", "1834": "", "1835": "", "1836": "", "1837": "", "1838": "", "1839": "", "1840": "", "1841": "", "1842": "", "1843": "", "1844": "", "1845": "", "1846": "", "1847": "", "1848": "", "1849": "", "1850": "", "1851": "", "1852": "", "1853": "", "1854": "", "1855": "", "1856": "", "1857": "", "1858": "", "1859": "---", "1860": "", "1861": "", "1862": "", "1863": "", "1864": "", "1865": "", "1866": "", "1867": "不稍微发出点舒服的声音的话 患者们可不会兴奋起来", "1868": "不稍微发出点舒服的声音的话 患者们可不会兴奋起来", "1869": "不稍微发出点舒服的声音的话 患者们可不会兴奋起来", "1870": "不稍微发出点舒服的声音的话 患者们可不会兴奋起来", "1871": "不稍微发出点舒服的声音的话 患者们可不会兴奋起来", "1872": "不稍微发出点舒服的声音的话 患者们可不会兴奋起来", "1873": "不稍微发出点舒服的声音的话 患者们可不会兴奋起来", "1874": "不稍微发出点舒服的声音的话 患者们可不会兴奋起来", "1875": "不稍微发出点舒服的声音的话 患者们可不会兴奋起来", "1876": "不稍微发出点舒服的声音的话 患者们可不会兴奋起来", "1877": "不稍微发出点舒服的声音的话 患者们可不会兴奋起来", "1878": "不稍微发出点舒服的声音的话 患者们可不会兴奋起来", "1879": "不稍微发出点舒服的声音的话 患者们可不会兴奋起来", "1880": "不稍微发出点舒服的声音的话 患者们可不会兴奋起来", "1881": "不稍微发出点舒服的声音的话 患者们可不会兴奋起来", "1882": "不稍微发出点舒服的声音的话 患者们可不会兴奋起来", "1883": "", "1884": "", "1885": "", "1886": "好嘞", "1887": "好嘞", "1888": "好嘞", "1889": "必须得让你更害羞才行", "1890": "必须得让你更害羞才行", "1891": "必须得让你更害羞才行", "1892": "必须得让你更害羞才行", "1893": "必须得让你更害羞才行", "1894": "必须得让你更害羞才行", "1895": "必须得让你更害羞才行", "1896": "必须得让你更害羞才行", "1897": "下来吧 快点", "1898": "下来吧 快点", "1899": "下来吧 快点", "1900": "下来吧 快点", "1901": "下来吧 快点", "1902": "下来吧 快点", "1903": "下来吧 快点", "1904": "下来吧 快点", "1905": "下来吧 快点", "1906": "", "1907": "", "1908": "", "1909": "", "1910": "", "1911": "站在这里", "1912": "站在这里", "1913": "站在这里", "1914": "站在这里", "1915": "站在这里", "1916": "", "1917": "", "1918": "", "1919": "", "1920": "", "1921": "", "1922": "", "1923": "", "1924": "", "1925": "", "1926": "", "1927": "", "1928": "", "1929": "", "1930": "", "1931": "", "1932": "", "1933": "", "1934": "", "1935": "", "1936": "", "1937": "", "1938": "", "1939": "你现在是什么心情 放过我吧", "1940": "你现在是什么心情 放过我吧", "1941": "你现在是什么心情 放过我吧", "1942": "你现在是什么心情 放过我吧", "1943": "你现在是什么心情 放过我吧", "1944": "你现在是什么心情 放过我吧", "1945": "你现在是什么心情 放过我吧", "1946": "你现在是什么心情 放过我吧", "1947": "你现在是什么心情 放过我吧", "1948": "不行", "1949": "不行", "1950": "不行", "1951": "不行", "1952": "这才刚开始呢", "1953": "这才刚开始呢", "1954": "这才刚开始呢", "1955": "这才刚开始呢", "1956": "这才刚开始呢", "1957": "", "1958": "", "1959": "", "1960": "坐在这里", "1961": "坐在这里", "1962": "坐在这里", "1963": "坐在这里", "1964": "坐在这里", "1965": "坐在这里", "1966": "", "1967": "", "1968": "", "1969": "", "1970": "", "1971": "", "1972": "", "1973": "", "1974": "", "1975": "", "1976": "", "1977": "", "1978": "", "1979": "", "1980": "", "1981": "", "1982": "", "1983": "", "1984": "", "1985": "", "1986": "", "1987": "", "1988": "", "1989": "", "1990": "", "1991": "", "1992": "", "1993": "", "1994": "HTML", "1995": "", "1996": "", "1997": "", "1998": "", "1999": "", "2000": "原来如此", "2001": "原来如此", "2002": "原来如此", "2003": "原来如此", "2004": "原来如此", "2005": "原来如此", "2006": "原来如此", "2007": "", "2008": "", "2009": "", "2010": "你有好好处理这边的阴毛呢", "2011": "你有好好处理这边的阴毛呢", "2012": "你有好好处理这边的阴毛呢", "2013": "你有好好处理这边的阴毛呢", "2014": "你有好好处理这边的阴毛呢", "2015": "你有好好处理这边的阴毛呢", "2016": "你有好好处理这边的阴毛呢", "2017": "你有好好处理这边的阴毛呢", "2018": "你有好好处理这边的阴毛呢", "2019": "你有好好处理这边的阴毛呢", "2020": "你有好好处理这边的阴毛呢", "2021": "", "2022": "", "2023": "", "2024": "", "2025": "A", "2026": "", "2027": "", "2028": "", "2029": "A", "2030": "", "2031": "", "2032": "", "2033": "", "2034": "", "2035": "", "2036": "气味也没异常 很好", "2037": "气味也没异常 很好", "2038": "气味也没异常 很好", "2039": "气味也没异常 很好", "2040": "气味也没异常 很好", "2041": "气味也没异常 很好", "2042": "气味也没异常 很好", "2043": "气味也没异常 很好", "2044": "", "2045": "", "2046": "", "2047": "味道如何 不要这样", "2048": "味道如何 不要这样", "2049": "味道如何 不要这样", "2050": "味道如何 不要这样", "2051": "味道如何 不要这样", "2052": "味道如何 不要这样", "2053": "味道如何 不要这样", "2054": "味道如何 不要这样", "2055": "味道如何 不要这样", "2056": "味道如何 不要这样", "2057": "味道如何 不要这样", "2058": "味道如何 不要这样", "2059": "", "2060": "", "2061": "", "2062": "", "2063": "", "2064": "", "2065": "", "2066": "", "2067": "", "2068": "", "2069": "", "2070": "", "2071": "", "2072": "", "2073": "", "2074": "", "2075": "", "2076": "", "2077": "", "2078": "", "2079": "", "2080": "", "2081": "目前没有问题", "2082": "目前没有问题", "2083": "目前没有问题", "2084": "目前没有问题", "2085": "目前没有问题", "2086": "目前没有问题", "2087": "目前没有问题", "2088": "", "2089": "", "2090": "因为重点在体内嘛 来吧", "2091": "因为重点在体内嘛 来吧", "2092": "因为重点在体内嘛 来吧", "2093": "因为重点在体内嘛 来吧", "2094": "因为重点在体内嘛 来吧", "2095": "因为重点在体内嘛 来吧", "2096": "因为重点在体内嘛 来吧", "2097": "因为重点在体内嘛 来吧", "2098": "因为重点在体内嘛 来吧", "2099": "因为重点在体内嘛 来吧", "2100": "因为重点在体内嘛 来吧", "2101": "对于患者 对于VIP患者", "2102": "对于患者 对于VIP患者", "2103": "对于患者 对于VIP患者", "2104": "对于患者 对于VIP患者", "2105": "对于患者 对于VIP患者", "2106": "对于患者 对于VIP患者", "2107": "对于患者 对于VIP患者", "2108": "对于患者 对于VIP患者", "2109": "对于患者 对于VIP患者", "2110": "对于患者 对于VIP患者", "2111": "可不能失礼了呢", "2112": "可不能失礼了呢", "2113": "可不能失礼了呢", "2114": "可不能失礼了呢", "2115": "可不能失礼了呢", "2116": "可不能失礼了呢", "2117": "我会好好地诊察的 别这样", "2118": "我会好好地诊察的 别这样", "2119": "我会好好地诊察的 别这样", "2120": "我会好好地诊察的 别这样", "2121": "我会好好地诊察的 别这样", "2122": "我会好好地诊察的 别这样", "2123": "我会好好地诊察的 别这样", "2124": "我会好好地诊察的 别这样", "2125": "我会好好地诊察的 别这样", "2126": "我会好好地诊察的 别这样", "2127": "我会好好地诊察的 别这样", "2128": "我会好好地诊察的 别这样", "2129": "我会好好地诊察的 别这样", "2130": "我会好好地诊察的 别这样", "2131": "这是诊察不要", "2132": "这是诊察不要", "2133": "这是诊察不要", "2134": "这是诊察不要", "2135": "这是诊察不要", "2136": "这是诊察不要", "2137": "这是诊察不要", "2138": "这是诊察不要", "2139": "", "2140": "", "2141": "", "2142": "", "2143": "", "2144": "", "2145": "求求你了 放过我吧", "2146": "求求你了 放过我吧", "2147": "求求你了 放过我吧", "2148": "求求你了 放过我吧", "2149": "求求你了 放过我吧", "2150": "求求你了 放过我吧", "2151": "求求你了 放过我吧", "2152": "", "2153": "", "2154": "", "2155": "", "2156": "", "2157": "外观上没有异常呢", "2158": "外观上没有异常呢", "2159": "外观上没有异常呢", "2160": "外观上没有异常呢", "2161": "外观上没有异常呢", "2162": "外观上没有异常呢", "2163": "外观上没有异常呢", "2164": "", "2165": "", "2166": "", "2167": "", "2168": "", "2169": "", "2170": "", "2171": "", "2172": "身为护士你就做好觉悟吧", "2173": "身为护士你就做好觉悟吧", "2174": "身为护士你就做好觉悟吧", "2175": "身为护士你就做好觉悟吧", "2176": "身为护士你就做好觉悟吧", "2177": "身为护士你就做好觉悟吧", "2178": "", "2179": "不要 住手", "2180": "不要 住手", "2181": "不要 住手", "2182": "不要 住手", "2183": "不要 住手", "2184": "", "2185": "", "2186": "", "2187": "", "2188": "", "2189": "", "2190": "", "2191": "", "2192": "", "2193": "", "2194": "", "2195": "", "2196": "", "2197": "", "2198": "", "2199": "", "2200": "", "2201": "", "2202": "", "2203": "", "2204": "", "2205": "", "2206": "", "2207": "", "2208": "", "2209": "---", "2210": "", "2211": "---------------", "2212": "", "2213": "", "2214": "", "2215": "", "2216": "", "2217": "", "2218": "不要这样", "2219": "不要这样", "2220": "不要这样", "2221": "不要这样", "2222": "不要这样", "2223": "", "2224": "", "2225": "", "2226": "", "2227": "", "2228": "", "2229": "", "2230": "", "2231": "", "2232": "", "2233": "", "2234": "---", "2235": "", "2236": "------", "2237": "------------------", "2238": "", "2239": "", "2240": "", "2241": "北冈小姐 你不用忍耐的", "2242": "北冈小姐 你不用忍耐的", "2243": "北冈小姐 你不用忍耐的", "2244": "北冈小姐 你不用忍耐的", "2245": "北冈小姐 你不用忍耐的", "2246": "北冈小姐 你不用忍耐的", "2247": "北冈小姐 你不用忍耐的", "2248": "北冈小姐 你不用忍耐的", "2249": "北冈小姐 你不用忍耐的", "2250": "北冈小姐 你不用忍耐的", "2251": "北冈小姐 你不用忍耐的", "2252": "北冈小姐 你不用忍耐的", "2253": "北冈小姐 你不用忍耐的", "2254": "", "2255": "", "2256": "", "2257": "这里都一抖一抖了不是吗", "2258": "这里都一抖一抖了不是吗", "2259": "这里都一抖一抖了不是吗", "2260": "这里都一抖一抖了不是吗", "2261": "这里都一抖一抖了不是吗", "2262": "这里都一抖一抖了不是吗", "2263": "这可是呢", "2264": "这可是呢", "2265": "这可是呢", "2266": "这可是呢", "2267": "身体在意识中渴求男人的证据", "2268": "身体在意识中渴求男人的证据", "2269": "身体在意识中渴求男人的证据", "2270": "身体在意识中渴求男人的证据", "2271": "身体在意识中渴求男人的证据", "2272": "身体在意识中渴求男人的证据", "2273": "不对 请住手", "2274": "不对 请住手", "2275": "不对 请住手", "2276": "不对 请住手", "2277": "不对 请住手", "2278": "不对 请住手", "2279": "你妈妈身体健康的时候一定也渴望着男人的滋润吧", "2280": "你妈妈身体健康的时候一定也渴望着男人的滋润吧", "2281": "你妈妈身体健康的时候一定也渴望着男人的滋润吧", "2282": "你妈妈身体健康的时候一定也渴望着男人的滋润吧", "2283": "你妈妈身体健康的时候 一定也渴望着男人的滋润吧", "2284": "你妈妈身体健康的时候一定也渴望着男人的滋润吧", "2285": "你妈妈身体健康的时候一定也渴望着男人的滋润吧", "2286": "你妈妈身体健康的时候一定也渴望着男人的滋润吧", "2287": "你妈妈身体健康的时候一定也渴望着男人的滋润吧", "2288": "你妈妈身体健康的时候一定也渴望着男人的滋润吧", "2289": "别说了", "2290": "别说了", "2291": "别说了", "2292": "别说了", "2293": "", "2294": "", "2295": "", "2296": "", "2297": "", "2298": "------", "2299": "HTML", "2300": "------", "2301": "------", "2302": "------", "2303": "", "2304": "", "2305": "", "2306": "", "2307": "", "2308": "", "2309": "", "2310": "", "2311": "------", "2312": "---------------------", "2313": "---------------------", "2314": "------", "2315": "------", "2316": "------", "2317": "---", "2318": "接下来要分泌出健康体液了", "2319": "接下来要分泌出健康体液了", "2320": "接下来要分泌出健康体液了", "2321": "接下来要分泌出健康体液了", "2322": "接下来要分泌出健康体液了", "2323": "接下来要分泌出健康体液了", "2324": "接下来要分泌出健康体液了", "2325": "接下来要分泌出健康体液了", "2326": "接下来要分泌出健康体液了", "2327": "接下来要分泌出健康体液了", "2328": "接下来要分泌出健康体液了", "2329": "接下来要分泌出健康体液了", "2330": "接下来要分泌出健康体液了", "2331": "------", "2332": "---", "2333": "------", "2334": "---", "2335": "---", "2336": "---", "2337": "---", "2338": "------", "2339": "---", "2340": "---", "2341": "", "2342": "------", "2343": "---", "2344": "---", "2345": "---", "2346": "", "2347": "", "2348": "------------------", "2349": "------------------", "2350": "---", "2351": "------", "2352": "", "2353": "", "2354": "---", "2355": "", "2356": "", "2357": "", "2358": "", "2359": "", "2360": "", "2361": "", "2362": "", "2363": "", "2364": "", "2365": "", "2366": "", "2367": "", "2368": "", "2369": "", "2370": "", "2371": "", "2372": "", "2373": "", "2374": "", "2375": "", "2376": "", "2377": "", "2378": "", "2379": "", "2380": "", "2381": "", "2382": "", "2383": "", "2384": "", "2385": "", "2386": "放过我吧 求求你", "2387": "放过我吧 求求你", "2388": "放过我吧 求求你", "2389": "放过我吧 求求你", "2390": "放过我吧 求求你", "2391": "放过我吧 求求你", "2392": "放过我吧 求求你", "2393": "放过我吧 求求你", "2394": "放过我吧 求求你", "2395": "放过我吧 求求你", "2396": "", "2397": "", "2398": "", "2399": "", "2400": "", "2401": "", "2402": "", "2403": "", "2404": "", "2405": "", "2406": "", "2407": "", "2408": "", "2409": "", "2410": "", "2411": "", "2412": "", "2413": "", "2414": "", "2415": "HTML", "2416": "", "2417": "", "2418": "不要 停下来", "2419": "不要 停下来", "2420": "不要 停下来", "2421": "不要 停下来", "2422": "不要 停下来", "2423": "不要 停下来", "2424": "不要 停下来", "2425": "", "2426": "", "2427": "", "2428": "", "2429": "", "2430": "", "2431": "", "2432": "", "2433": "", "2434": "", "2435": "", "2436": "", "2437": "", "2438": "", "2439": "", "2440": "", "2441": "", "2442": "", "2443": "", "2444": "", "2445": "", "2446": "", "2447": "你其实也挺乐意的嘛 北冈小姐", "2448": "你其实也挺乐意的嘛 北冈小姐", "2449": "你其实也挺乐意的嘛 北冈小姐", "2450": "你其实也挺乐意的嘛 北冈小姐", "2451": "你其实也挺乐意的嘛 北冈小姐", "2452": "你其实也挺乐意的嘛 北冈小姐", "2453": "你其实也挺乐意的嘛 北冈小姐", "2454": "", "2455": "", "2456": "", "2457": "", "2458": "", "2459": "", "2460": "", "2461": "", "2462": "", "2463": "", "2464": "", "2465": "", "2466": "", "2467": "", "2468": "", "2469": "", "2470": "", "2471": "", "2472": "", "2473": "", "2474": "", "2475": "", "2476": "", "2477": "", "2478": "就这样放松吧", "2479": "就这样放松吧", "2480": "就这样放松吧", "2481": "就这样放松吧", "2482": "就这样放松吧", "2483": "就这样放松吧", "2484": "就这样放松吧", "2485": "", "2486": "", "2487": "", "2488": "", "2489": "", "2490": "", "2491": "", "2492": "", "2493": "", "2494": "", "2495": "", "2496": "", "2497": "", "2498": "已经准备好了", "2499": "已经准备好了", "2500": "已经准备好了", "2501": "已经准备好了", "2502": "已经准备好了", "2503": "已经准备好了", "2504": "接下来就要服侍男人的身体了", "2505": "接下来就要服侍男人的身体了", "2506": "接下来就要服侍男人的身体了", "2507": "接下来就要服侍男人的身体了", "2508": "接下来就要服侍男人的身体了", "2509": "接下来就要服侍男人的身体了", "2510": "接下来就要服侍男人的身体了", "2511": "接下来就要服侍男人的身体了", "2512": "接下来就要服侍男人的身体了", "2513": "接下来就要服侍男人的身体了", "2514": "接下来就要服侍男人的身体了", "2515": "接下来就要服侍男人的身体了", "2516": "接下来就要服侍男人的身体了", "2517": "接下来就要服侍男人的身体了", "2518": "接下来就要服侍男人的身体了", "2519": "接下来就要服侍男人的身体了", "2520": "接下来就要服侍男人的身体了", "2521": "不要 这不是说不要的时候", "2522": "不要 这不是说不要的时候", "2523": "不要 这不是说不要的时候", "2524": "不要 这不是说不要的时候", "2525": "不要 这不是说不要的时候", "2526": "不要 这不是说不要的时候", "2527": "不要 这不是说不要的时候", "2528": "不要 这不是说不要的时候", "2529": "不要 这不是说不要的时候", "2530": "你妈妈变成什么样都无所谓吗", "2531": "你妈妈变成什么样都无所谓吗", "2532": "你妈妈变成什么样都无所谓吗", "2533": "你妈妈变成什么样都无所谓吗", "2534": "你妈妈变成什么样都无所谓吗", "2535": "你妈妈变成什么样都无所谓吗", "2536": "你想救妈妈吧", "2537": "你想救妈妈吧", "2538": "你想救妈妈吧", "2539": "你想救妈妈吧", "2540": "你想救妈妈吧", "2541": "你想救妈妈吧", "2542": "你想救妈妈吧", "2543": "只要我一声令下", "2544": "只要我一声令下", "2545": "只要我一声令下", "2546": "只要我一声令下", "2547": "只要我一声令下", "2548": "只要我一声令下", "2549": "她就能进行最棒的手术了", "2550": "她就能进行最棒的手术了", "2551": "她就能进行最棒的手术了", "2552": "她就能进行最棒的手术了", "2553": "她就能进行最棒的手术了", "2554": "她就能进行最棒的手术了", "2555": "她就能进行最棒的手术了", "2556": "她就能进行最棒的手术了", "2557": "她就能进行最棒的手术了", "2558": "她就能进行最棒的手术了", "2559": "", "2560": "", "2561": "", "2562": "", "2563": "", "2564": "", "2565": "", "2566": "", "2567": "", "2568": "", "2569": "", "2570": "", "2571": "", "2572": "", "2573": "", "2574": "", "2575": "", "2576": "不要停下来 要继续动", "2577": "不要停下来 要继续动", "2578": "不要停下来 要继续动", "2579": "不要停下来 要继续动", "2580": "不要停下来 要继续动", "2581": "", "2582": "", "2583": "", "2584": "要好好看着我这边", "2585": "要好好看着我这边", "2586": "要好好看着我这边", "2587": "要好好看着我这边", "2588": "要好好看着我这边", "2589": "要好好看着我这边", "2590": "", "2591": "", "2592": "", "2593": "", "2594": "", "2595": "", "2596": "", "2597": "", "2598": "", "2599": "", "2600": "", "2601": "", "2602": "距离太远了 靠近一点", "2603": "距离太远了 靠近一点", "2604": "距离太远了 靠近一点", "2605": "距离太远了 靠近一点", "2606": "距离太远了 靠近一点", "2607": "距离太远了 靠近一点", "2608": "距离太远了 靠近一点", "2609": "", "2610": "", "2611": "", "2612": "", "2613": "保持社交距离已经结束了", "2614": "保持社交距离已经结束了", "2615": "保持社交距离已经结束了", "2616": "保持社交距离已经结束了", "2617": "保持社交距离已经结束了", "2618": "保持社交距离已经结束了", "2619": "保持社交距离已经结束了", "2620": "", "2621": "", "2622": "靠近点放过我吧", "2623": "靠近点放过我吧", "2624": "靠近点放过我吧", "2625": "靠近点放过我吧", "2626": "靠近点放过我吧", "2627": "靠近点放过我吧", "2628": "", "2629": "", "2630": "", "2631": "", "2632": "", "2633": "", "2634": "", "2635": "", "2636": "", "2637": "", "2638": "", "2639": "", "2640": "", "2641": "", "2642": "", "2643": "", "2644": "", "2645": "", "2646": "", "2647": "", "2648": "", "2649": "", "2650": "摸上去", "2651": "摸上去", "2652": "摸上去", "2653": "摸上去", "2654": "摸上去", "2655": "", "2656": "", "2657": "不要用太多力气 适应的力量", "2658": "不要用太多力气 适应的力量", "2659": "不要用太多力气 适应的力量", "2660": "不要用太多力气 适应的力量", "2661": "不要用太多力气 适应的力量", "2662": "不要用太多力气 适应的力量", "2663": "不要用太多力气 适应的力量", "2664": "不要用太多力气 适应的力量", "2665": "不要用太多力气 适应的力量", "2666": "不要用太多力气 适应的力量", "2667": "不要用太多力气 适应的力量", "2668": "", "2669": "", "2670": "", "2671": "", "2672": "", "2673": "不要停下的 快点继续动", "2674": "不要停下的 快点继续动", "2675": "不要停下的 快点继续动", "2676": "不要停下的 快点继续动", "2677": "不要停下的 快点继续动", "2678": "不要停下的 快点继续动", "2679": "不要停下的 快点继续动", "2680": "不要停下的 快点继续动", "2681": "不要停下的 快点继续动", "2682": "不要停下的 快点继续动", "2683": "不要停下的 快点继续动", "2684": "", "2685": "", "2686": "我不要", "2687": "我不要", "2688": "我不要", "2689": "我不要", "2690": "我不要", "2691": "", "2692": "", "2693": "", "2694": "", "2695": "", "2696": "偶尔也要用嘴巴", "2697": "偶尔也要用嘴巴", "2698": "偶尔也要用嘴巴", "2699": "偶尔也要用嘴巴", "2700": "偶尔也要用嘴巴", "2701": "偶尔也要用嘴巴", "2702": "偶尔也要用嘴巴", "2703": "偶尔也要用嘴巴", "2704": "偶尔也要用嘴巴", "2705": "偶尔也要用嘴巴", "2706": "嘴巴 先张开嘴巴吧", "2707": "嘴巴 先张开嘴巴吧", "2708": "嘴巴 先张开嘴巴吧", "2709": "嘴巴 先张开嘴巴吧", "2710": "嘴巴 先张开嘴巴吧", "2711": "嘴巴 先张开嘴巴吧", "2712": "嘴巴 先张开嘴巴吧", "2713": "嘴巴 先张开嘴巴吧", "2714": "嘴巴 先张开嘴巴吧", "2715": "嘴巴 先张开嘴巴吧", "2716": "", "2717": "", "2718": "", "2719": "", "2720": "", "2721": "", "2722": "---", "2723": "", "2724": "", "2725": "---", "2726": "", "2727": "---", "2728": "", "2729": "---", "2730": "", "2731": "---", "2732": "---", "2733": "---", "2734": "", "2735": "---", "2736": "", "2737": "---", "2738": "", "2739": "---", "2740": "", "2741": "---", "2742": "", "2743": "", "2744": "", "2745": "---", "2746": "", "2747": "", "2748": "", "2749": "", "2750": "不要碰到牙齿", "2751": "不要碰到牙齿", "2752": "不要碰到牙齿", "2753": "不要碰到牙齿", "2754": "不要碰到牙齿", "2755": "不要碰到牙齿", "2756": "", "2757": "", "2758": "", "2759": "---", "2760": "要多分泌点唾液", "2761": "要多分泌点唾液", "2762": "要多分泌点唾液", "2763": "要多分泌点唾液", "2764": "要多分泌点唾液", "2765": "要多分泌点唾液", "2766": "要多分泌点唾液", "2767": "", "2768": "___", "2769": "然后把舌头缠在肉棒上", "2770": "然后把舌头缠在肉棒上", "2771": "然后把舌头缠在肉棒上", "2772": "然后把舌头缠在肉棒上", "2773": "然后把舌头缠在肉棒上", "2774": "然后把舌头缠在肉棒上", "2775": "然后把舌头缠在肉棒上", "2776": "然后把舌头缠在肉棒上", "2777": "然后把舌头缠在肉棒上", "2778": "然后把舌头缠在肉棒上", "2779": "然后把舌头缠在肉棒上", "2780": "然后把舌头缠在肉棒上", "2781": "然后把舌头缠在肉棒上", "2782": "", "2783": "", "2784": "", "2785": "", "2786": "", "2787": "", "2788": "", "2789": "", "2790": "", "2791": "", "2792": "", "2793": "", "2794": "那是什么表情", "2795": "那是什么表情", "2796": "那是什么表情", "2797": "那是什么表情", "2798": "那是什么表情", "2799": "那是什么表情", "2800": "", "2801": "你能不能再配合一点呀", "2802": "你能不能再配合一点呀", "2803": "你能不能再配合一点呀", "2804": "你能不能再配合一点呀", "2805": "你能不能再配合一点呀", "2806": "你能不能再配合一点呀", "2807": "", "2808": "", "2809": "", "2810": "站起来 到检查台上", "2811": "站起来 到检查台上", "2812": "站起来 到检查台上", "2813": "站起来 到检查台上", "2814": "站起来 到检查台上", "2815": "站起来 到检查台上", "2816": "站起来 到检查台上", "2817": "站起来 到检查台上", "2818": "", "2819": "", "2820": "", "2821": "---", "2822": "", "2823": "---", "2824": "我也说了好几遍了", "2825": "我也说了好几遍了", "2826": "我也说了好几遍了", "2827": "我也说了好几遍了", "2828": "我也说了好几遍了", "2829": "我也说了好几遍了", "2830": "不想做洗手的动作", "2831": "不想做洗手的动作", "2832": "不想做洗手的动作", "2833": "不想做洗手的动作", "2834": "不想做洗手的动作", "2835": "就是因为你不配合 才变成这样", "2836": "就是因为你不配合 才变成这样", "2837": "就是因为你不配合才变成这样", "2838": "就是因为你不配合 才变成这样", "2839": "就是因为你不配合 才变成这样", "2840": "就是因为你不配合才变成这样", "2841": "就是因为你不配合 才变成这样", "2842": "就是因为你不配合 才变成这样", "2843": "知道了吗 张嘴", "2844": "知道了吗 张嘴", "2845": "知道了吗 张嘴", "2846": "知道了吗 张嘴", "2847": "知道了吗 张嘴", "2848": "知道了吗 张嘴", "2849": "知道了吗 张嘴", "2850": "知道了吗 张嘴", "2851": "", "2852": "", "2853": "", "2854": "快点张开 听话", "2855": "快点张开 听话", "2856": "快点张开 听话", "2857": "快点张开 听话", "2858": "快点张开 听话", "2859": "", "2860": "", "2861": "", "2862": "", "2863": "", "2864": "", "2865": "", "2866": "", "2867": "", "2868": "", "2869": "", "2870": "", "2871": "", "2872": "", "2873": "", "2874": "", "2875": "", "2876": "", "2877": "", "2878": "", "2879": "", "2880": "", "2881": "", "2882": "", "2883": "", "2884": "", "2885": "", "2886": "", "2887": "", "2888": "", "2889": "", "2890": "", "2891": "", "2892": "", "2893": "------", "2894": "------", "2895": "------", "2896": "", "2897": "------", "2898": "---", "2899": "------", "2900": "------", "2901": "", "2902": "------------------", "2903": "", "2904": "含进喉咙里", "2905": "含进喉咙里", "2906": "含进喉咙里", "2907": "含进喉咙里", "2908": "含进喉咙里", "2909": "", "2910": "------", "2911": "------", "2912": "---------------", "2913": "", "2914": "------", "2915": "", "2916": "------", "2917": "------------", "2918": "---------------", "2919": "------------------", "2920": "", "2921": "------", "2922": "------------------", "2923": "", "2924": "------------------", "2925": "------", "2926": "", "2927": "", "2928": "", "2929": "---", "2930": "", "2931": "", "2932": "", "2933": "别这么痛苦嘛", "2934": "别这么痛苦嘛", "2935": "别这么痛苦嘛", "2936": "别这么痛苦嘛", "2937": "别这么痛苦嘛", "2938": "有很多患者的肉棒会更大的", "2939": "有很多患者的肉棒会更大的", "2940": "有很多患者的肉棒会更大的", "2941": "有很多患者的肉棒会更大的", "2942": "有很多患者的肉棒会更大的", "2943": "有很多患者的肉棒会更大的", "2944": "有很多患者的肉棒会更大的", "2945": "有很多患者的肉棒会更大的", "2946": "有很多患者的肉棒会更大的", "2947": "有很多患者的肉棒会更大的", "2948": "", "2949": "------", "2950": "", "2951": "---", "2952": "", "2953": "---", "2954": "", "2955": "", "2956": "", "2957": "", "2958": "", "2959": "---", "2960": "---", "2961": "", "2962": "---", "2963": "只要你努力的话你妈妈获救的机率也会提高的", "2964": "只要你努力的话你妈妈获救的机率也会提高的", "2965": "只要你努力的话 你妈妈获救的机率也会提高的", "2966": "只要你努力的话你妈妈获救的机率也会提高的", "2967": "只要你努力的话你妈妈获救的机率也会提高的", "2968": "只要你努力的话你妈妈获救的机率也会提高的", "2969": "只要你努力的话你妈妈获救的机率也会提高的", "2970": "只要你努力的话你妈妈获救的机率也会提高的", "2971": "只要你努力的话你妈妈获救的机率也会提高的", "2972": "加油", "2973": "加油", "2974": "加油", "2975": "加油", "2976": "加油", "2977": "", "2978": "---", "2979": "------------", "2980": "", "2981": "------", "2982": "", "2983": "---", "2984": "", "2985": "---", "2986": "", "2987": "", "2988": "", "2989": "", "2990": "", "2991": "---", "2992": "", "2993": "", "2994": "", "2995": "---", "2996": "", "2997": "", "2998": "", "2999": "", "3000": "", "3001": "", "3002": "", "3003": "", "3004": "", "3005": "", "3006": "", "3007": "", "3008": "", "3009": "", "3010": "", "3011": "", "3012": "", "3013": "", "3014": "", "3015": "越来越好了呢", "3016": "越来越好了呢", "3017": "越来越好了呢", "3018": "越来越好了呢", "3019": "越来越好了呢", "3020": "越来越好了呢", "3021": "", "3022": "", "3023": "我再动快一点", "3024": "我再动快一点", "3025": "我再动快一点", "3026": "我再动快一点", "3027": "我再动快一点", "3028": "我再动快一点", "3029": "", "3030": "", "3031": "---", "3032": "", "3033": "", "3034": "", "3035": "", "3036": "", "3037": "------------------", "3038": "---", "3039": "", "3040": "---", "3041": "", "3042": "", "3043": "", "3044": "", "3045": "", "3046": "", "3047": "", "3048": "", "3049": "", "3050": "", "3051": "", "3052": "不错 就这样继续下去", "3053": "不错 就这样继续下去", "3054": "不错 就这样继续下去", "3055": "不错 就这样继续下去", "3056": "不错 就这样继续下去", "3057": "不错 就这样继续下去", "3058": "不错 就这样继续下去", "3059": "不错 就这样继续下去", "3060": "不错 就这样继续下去", "3061": "坚持住", "3062": "坚持住", "3063": "坚持住", "3064": "坚持住", "3065": "坚持住", "3066": "", "3067": "", "3068": "", "3069": "", "3070": "", "3071": "", "3072": "", "3073": "", "3074": "", "3075": "", "3076": "", "3077": "", "3078": "", "3079": "", "3080": "", "3081": "", "3082": "", "3083": "很好 北冈小姐", "3084": "很好 北冈小姐", "3085": "很好 北冈小姐", "3086": "很好 北冈小姐", "3087": "很好 北冈小姐", "3088": "很好 北冈小姐", "3089": "很好 北冈小姐", "3090": "很好 北冈小姐", "3091": "", "3092": "", "3093": "", "3094": "", "3095": "", "3096": "", "3097": "", "3098": "还没有结束", "3099": "还没有结束", "3100": "还没有结束", "3101": "还没有结束", "3102": "还没有结束", "3103": "", "3104": "", "3105": "", "3106": "", "3107": "", "3108": "", "3109": "", "3110": "", "3111": "", "3112": "", "3113": "", "3114": "", "3115": "", "3116": "", "3117": "", "3118": "", "3119": "", "3120": "", "3121": "", "3122": "", "3123": "", "3124": "", "3125": "", "3126": "", "3127": "", "3128": "", "3129": "", "3130": "", "3131": "", "3132": "", "3133": "", "3134": "", "3135": "", "3136": "", "3137": "", "3138": "", "3139": "", "3140": "---", "3141": "继续加油吧", "3142": "继续加油吧", "3143": "继续加油吧", "3144": "继续加油吧", "3145": "", "3146": "", "3147": "", "3148": "", "3149": "", "3150": "", "3151": "", "3152": "", "3153": "", "3154": "", "3155": "", "3156": "", "3157": "", "3158": "", "3159": "", "3160": "", "3161": "", "3162": "", "3163": "", "3164": "", "3165": "---", "3166": "", "3167": "Background", "3168": "---", "3169": "", "3170": "------", "3171": "", "3172": "---", "3173": "---", "3174": "------", "3175": "", "3176": "", "3177": "", "3178": "---", "3179": "", "3180": "HTML:
", "3181": "", "3182": "", "3183": "", "3184": "", "3185": "", "3186": "", "3187": "", "3188": "", "3189": "", "3190": "", "3191": "", "3192": "", "3193": "", "3194": "", "3195": "", "3196": "", "3197": "", "3198": "", "3199": "---", "3200": "", "3201": "", "3202": "", "3203": "", "3204": "", "3205": "Background", "3206": "", "3207": "", "3208": "", "3209": "---", "3210": "", "3211": "", "3212": "---------------", "3213": "", "3214": "", "3215": "", "3216": "---", "3217": "HTML", "3218": "", "3219": "", "3220": "", "3221": "---", "3222": "---------------", "3223": "", "3224": "---", "3225": "把身心都放松出来吧", "3226": "把身心都放松出来吧", "3227": "把身心都放松出来吧", "3228": "把身心都放松出来吧", "3229": "把身心都放松出来吧", "3230": "98室[巴花堂]永久地址489155.com把身心都放松出来吧", "3231": "98室[芭花堂]永久地址489155.com把身心都放松出来吧", "3232": "98室[巴花堂]永久地址489155.com把身心都放松出来吧", "3233": "98室[巴花堂]永久地址489155.com把身心都放松出来吧", "3234": "98室[巴花堂]永久地址489155.com把身心都放松出来吧", "3235": "98室[芭花室]永久地址489155.com把身心都放松出来吧", "3236": "把身心都放松出来吧", "3237": "", "3238": "---", "3239": "", "3240": "", "3241": "", "3242": "把腿张开 等一下", "3243": "把腿张开 等一下", "3244": "把腿张开 等一下", "3245": "把腿张开 等一下", "3246": "把腿张开 等一下", "3247": "把腿张开 等一下", "3248": "把腿张开 等一下", "3249": "把腿张开 等一下", "3250": "把腿张开 等一下", "3251": "你想做什么", "3252": "你想做什么", "3253": "你想做什么", "3254": "你想做什么", "3255": "到这一步 要做的事只有一件了吧", "3256": "到这一步 要做的事只有一件了吧", "3257": "到这一步 要做的事只有一件了吧", "3258": "到这一步 要做的事只有一件了吧", "3259": "到这一步 要做的事只有一件了吧", "3260": "求求你 这个千万不要", "3261": "求求你 这个千万不要", "3262": "求求你 这个千万不要", "3263": "求求你 这个千万不要", "3264": "求求你 这个千万不要", "3265": "求求你 这个千万不要", "3266": "求求你 这个千万不要", "3267": "为什么呢", "3268": "为什么呢", "3269": "为什么呢", "3270": "为什么呢", "3271": "为什么呢", "3272": "---", "3273": "---", "3274": "---", "3275": "---", "3276": "难道说你是第一次吗", "3277": "难道说你是第一次吗", "3278": "难道说你是第一次吗", "3279": "难道说你是第一次吗", "3280": "难道说你是第一次吗", "3281": "难道说你是第一次吗", "3282": "难道说你是第一次吗", "3283": "难道说你是第一次吗", "3284": "难道说你是第一次吗", "3285": "---", "3286": "如果是的话那我就更应该负责", "3287": "如果是的话那我就更应该负责", "3288": "如果是的话那我就更应该负责", "3289": "如果是的话那我就更应该负责", "3290": "如果是的话那我就更应该负责", "3291": "如果是的话那我就更应该负责", "3292": "如果是的话那我就更应该负责", "3293": "如果是的话那我就更应该负责", "3294": "如果是的话那我就更应该负责", "3295": "如果是的话那我就更应该负责", "3296": "帮你把处女膜捅破了", "3297": "帮你把处女膜捅破了", "3298": "帮你把处女膜捅破了", "3299": "帮你把处女膜捅破了", "3300": "帮你把处女膜捅破了", "3301": "帮你把处女膜捅破了", "3302": "帮你把处女膜捅破了", "3303": "帮你把处女膜捅破了", "3304": "可不能给VIP患者们便宜", "3305": "可不能给VIP患者们便宜", "3306": "可不能给VIP患者们便宜", "3307": "可不能给VIP患者们便宜", "3308": "可不能给VIP患者们便宜", "3309": "可不能给VIP患者们便宜", "3310": "可不能给VIP患者们便宜", "3311": "可不能给VIP患者们便宜", "3312": "可不能给VIP患者们便宜", "3313": "可不能给VIP患者们便宜", "3314": "可不能给VIP患者们便宜", "3315": "可不能给VIP患者们便宜", "3316": "可不能给VIP患者们便宜", "3317": "不要紧 不要紧", "3318": "不要紧 不要紧", "3319": "不要紧 不要紧", "3320": "不要紧 不要紧", "3321": "谁都会在遥远的路上", "3322": "谁都会在遥远的路上", "3323": "谁都会在遥远的路上", "3324": "谁都会在遥远的路上", "3325": "谁都会在遥远的路上", "3326": "谁都会在遥远的路上", "3327": "谁都会在遥远的路上", "3328": "谁都会在遥远的路上", "3329": "谁都会在遥远的路上", "3330": "谁都会在遥远的路上", "3331": "求求你 不要插进小穴 被我这样高明的想法贯穿", "3332": "求求你 不要插进小穴 被我这样高明的想法贯穿", "3333": "求求你 不要插进小穴 被我这样高明的想法贯穿", "3334": "求求你 不要插进小穴 被我这样高明的想法贯穿", "3335": "求求你 不要插进小穴 被我这样高明的想法贯穿", "3336": "求求你 不要插进小穴 被我这样高明的想法贯穿", "3337": "求求你 不要插进小穴 被我这样高明的想法贯穿", "3338": "求求你 不要插进小穴 被我这样高明的想法贯穿", "3339": "求求你 不要插进小穴 被我这样高明的想法贯穿", "3340": "求求你 不要插进小穴 被我这样高明的想法贯穿", "3341": "求求你 不要插进小穴 被我这样高明的想法贯穿", "3342": "求求你 不要插进小穴 被我这样高明的想法贯穿", "3343": "求求你 不要插进小穴 被我这样高明的想法贯穿", "3344": "求求你 不要插进小穴 被我这样高明的想法贯穿", "3345": "求求你 不要插进小穴 被我这样高明的想法贯穿", "3346": "求求你 不要插进小穴 被我这样高明的想法贯穿", "3347": "求求你 不要插进小穴 被我这样高明的想法贯穿", "3348": "你真是过方不要", "3349": "你真是过方不要", "3350": "你真是过方不要", "3351": "你真是过方不要", "3352": "你真是过方 不要", "3353": "放松点吧", "3354": "放松点吧", "3355": "放松点吧", "3356": "放松点吧", "3357": "放松点吧", "3358": "", "3359": "", "3360": "", "3361": "", "3362": "", "3363": "", "3364": "", "3365": "", "3366": "", "3367": "", "3368": "小穴好紧", "3369": "小穴好紧", "3370": "小穴好紧", "3371": "小穴好紧", "3372": "小穴好紧", "3373": "小穴好紧", "3374": "", "3375": "处女膜", "3376": "处女膜", "3377": "处女膜", "3378": "处女膜", "3379": "处女膜", "3380": "", "3381": "", "3382": "", "3383": "", "3384": "", "3385": "", "3386": "", "3387": "", "3388": "", "3389": "不要紧 只要进去了就没事了", "3390": "不要紧 只要进去了就没事了", "3391": "不要紧 只要进去了就没事了", "3392": "不要紧 只要进去了就没事了", "3393": "不要紧 只要进去了就没事了", "3394": "不要紧 只要进去了就没事了", "3395": "不要紧 只要进去了就没事了", "3396": "不要紧 只要进去了就没事了", "3397": "不要紧 只要进去了就没事了", "3398": "不要紧 只要进去了就没事了", "3399": "---", "3400": "不行马上就会习惯啦", "3401": "不行马上就会习惯啦", "3402": "不行马上就会习惯啦", "3403": "不行马上就会习惯啦", "3404": "不行 马上就会习惯啦", "3405": "不行马上就会习惯啦", "3406": "不行马上就会习惯啦", "3407": "不行马上就会习惯啦", "3408": "不行马上就会习惯啦", "3409": "不行马上就会习惯啦", "3410": "真的不行 不要", "3411": "真的不行 不要", "3412": "真的不行 不要", "3413": "真的不行 不要", "3414": "真的不行 不要", "3415": "真的不行 不要", "3416": "真的不行 不要", "3417": "真的不行 不要", "3418": "真的不行 不要", "3419": "", "3420": "", "3421": "", "3422": "", "3423": "", "3424": "---", "3425": "---", "3426": "---", "3427": "------", "3428": "---", "3429": "---", "3430": "", "3431": "---", "3432": "---", "3433": "", "3434": "---", "3435": "只有刚开始会痛", "3436": "只有刚开始会痛", "3437": "只有刚开始会痛", "3438": "只有刚开始会痛", "3439": "只有刚开始会痛", "3440": "只有刚开始会痛", "3441": "只有刚开始会痛", "3442": "不要 请住手 我不行了", "3443": "不要 请住手 我不行了", "3444": "不要 请住手 我不行了", "3445": "不要 请住手 我不行了", "3446": "不要 请住手 我不行了", "3447": "不要 请住手 我不行了", "3448": "不要 请住手 我不行了", "3449": "不要 请住手 我不行了", "3450": "不要 请住手 我不行了", "3451": "不要 请住手 我不行了", "3452": "---", "3453": "---", "3454": "---", "3455": "", "3456": "", "3457": "", "3458": "", "3459": "", "3460": "", "3461": "", "3462": "", "3463": "", "3464": "", "3465": "深呼吸 深呼吸", "3466": "深呼吸 深呼吸", "3467": "深呼吸 深呼吸", "3468": "深呼吸 深呼吸", "3469": "深呼吸 深呼吸", "3470": "深呼吸 深呼吸", "3471": "深呼吸 深呼吸", "3472": "深呼吸 深呼吸", "3473": "", "3474": "---", "3475": "---", "3476": "---", "3477": "", "3478": "", "3479": "---", "3480": "---", "3481": "", "3482": "", "3483": "", "3484": "", "3485": "", "3486": "", "3487": "", "3488": "你慢慢习惯了吧 不行", "3489": "你慢慢习惯了吧 不行", "3490": "你慢慢习惯了吧 不行", "3491": "你慢慢习惯了吧 不行", "3492": "你慢慢习惯了吧 不行", "3493": "你慢慢习惯了吧 不行", "3494": "你慢慢习惯了吧 不行", "3495": "你慢慢习惯了吧 不行", "3496": "你慢慢习惯了吧 不行", "3497": "你慢慢习惯了吧 不行", "3498": "", "3499": "", "3500": "", "3501": "", "3502": "", "3503": "", "3504": "", "3505": "", "3506": "医生请住手吧 没事的", "3507": "医生请住手吧 没事的", "3508": "医生请住手吧 没事的", "3509": "医生请住手吧 没事的", "3510": "医生请住手吧 没事的", "3511": "医生请住手吧 没事的", "3512": "", "3513": "我做不到", "3514": "我做不到", "3515": "我做不到", "3516": "我做不到", "3517": "", "3518": "", "3519": "", "3520": "", "3521": "", "3522": "", "3523": "", "3524": "", "3525": "", "3526": "", "3527": "真不错呢 看到你处女膜被摸", "3528": "真不错呢 看到你处女膜被摸", "3529": "真不错呢 看到你处女膜被摸", "3530": "真不错呢 看到你处女膜被摸", "3531": "真不错呢 看到你处女膜被摸", "3532": "真不错呢 看到你处女膜被摸", "3533": "真不错呢 看到你处女膜被摸", "3534": "真不错呢 看到你处女膜被摸", "3535": "被我侵犯的样子让我好兴奋", "3536": "被我侵犯的样子让我好兴奋", "3537": "被我侵犯的样子让我好兴奋", "3538": "被我侵犯的样子让我好兴奋", "3539": "被我侵犯的样子让我好兴奋", "3540": "被我侵犯的样子让我好兴奋", "3541": "被我侵犯的样子让我好兴奋", "3542": "被我侵犯的样子让我好兴奋", "3543": "被我侵犯的样子让我好兴奋", "3544": "被我侵犯的样子让我好兴奋", "3545": "被我侵犯的样子让我好兴奋", "3546": "", "3547": "", "3548": "", "3549": "", "3550": "---------------", "3551": "---", "3552": "---", "3553": "---", "3554": "---", "3555": "", "3556": "---", "3557": "", "3558": "------------", "3559": "", "3560": "", "3561": "", "3562": "", "3563": "", "3564": "", "3565": "", "3566": "", "3567": "", "3568": "", "3569": "", "3570": "", "3571": "", "3572": "", "3573": "", "3574": "", "3575": "---", "3576": "我再插深一点", "3577": "我再插深一点", "3578": "我再插深一点", "3579": "我再插深一点", "3580": "我再插深一点", "3581": "我再插深一点", "3582": "我再插深一点", "3583": "我再插深一点", "3584": "我再插深一点", "3585": "我再插深一点", "3586": "请住手", "3587": "请住手", "3588": "请住手", "3589": "请住手", "3590": "请住手", "3591": "请住手", "3592": "", "3593": "", "3594": "", "3595": "", "3596": "---", "3597": "---", "3598": "", "3599": "", "3600": "------------------", "3601": "------", "3602": "------------------", "3603": "我不行了啦 就快结束了", "3604": "我不行了啦 就快结束了", "3605": "我不行了啦 就快结束了", "3606": "我不行了啦 就快结束了", "3607": "我不行了啦 就快结束了", "3608": "我不行了啦 就快结束了", "3609": "我不行了啦 就快结束了", "3610": "我不行了啦 就快结束了", "3611": "我不行了啦 就快结束了", "3612": "我不行了啦 就快结束了", "3613": "我不行了啦 就快结束了", "3614": "------------------", "3615": "快点结束 求求你", "3616": "快点结束 求求你", "3617": "快点结束 求求你", "3618": "快点结束 求求你", "3619": "快点结束 求求你", "3620": "快点结束 求求你", "3621": "------------------", "3622": "", "3623": "求求你 对不起 我不行了", "3624": "求求你 对不起 我不行了", "3625": "求求你 对不起 我不行了", "3626": "求求你 对不起 我不行了", "3627": "求求你 对不起 我不行了", "3628": "求求你 对不起 我不行了", "3629": "求求你 对不起 我不行了", "3630": "不用道歉啦", "3631": "不用道歉啦", "3632": "不用道歉啦", "3633": "不用道歉啦", "3634": "不用道歉啦", "3635": "你只要帮我就好", "3636": "你只要帮我就好", "3637": "你只要帮我就好", "3638": "你只要帮我就好", "3639": "我不行 真的不行", "3640": "我不行 真的不行", "3641": "我不行 真的不行", "3642": "我不行 真的不行", "3643": "我不行 真的不行", "3644": "------------------", "3645": "", "3646": "------------------", "3647": "------------------", "3648": "------", "3649": "---", "3650": "------", "3651": "------------------", "3652": "------------------", "3653": "------", "3654": "------", "3655": "------------", "3656": "------------------", "3657": "------", "3658": "---", "3659": "------", "3660": "------", "3661": "", "3662": "---", "3663": "---", "3664": "", "3665": "------", "3666": "---", "3667": "", "3668": "", "3669": "---------------", "3670": "---", "3671": "", "3672": "", "3673": "", "3674": "", "3675": "", "3676": "", "3677": "", "3678": "", "3679": "", "3680": "", "3681": "", "3682": "", "3683": "---", "3684": "", "3685": "---", "3686": "", "3687": "", "3688": "---", "3689": "", "3690": "", "3691": "", "3692": "", "3693": "", "3694": "", "3695": "你现在也舒服起来了嘛", "3696": "你现在也舒服起来了嘛", "3697": "你现在也舒服起来了嘛", "3698": "你现在也舒服起来了嘛", "3699": "你现在也舒服起来了嘛", "3700": "你现在也舒服起来了嘛", "3701": "", "3702": "", "3703": "", "3704": "", "3705": "", "3706": "", "3707": "---", "3708": "", "3709": "", "3710": "", "3711": "", "3712": "", "3713": "---", "3714": "", "3715": "---", "3716": "---", "3717": "---", "3718": "", "3719": "---", "3720": "", "3721": "---", "3722": "", "3723": "", "3724": "", "3725": "", "3726": "", "3727": "", "3728": "", "3729": "", "3730": "", "3731": "", "3732": "", "3733": "", "3734": "", "3735": "", "3736": "", "3737": "", "3738": "", "3739": "", "3740": "", "3741": "", "3742": "", "3743": "", "3744": "", "3745": "", "3746": "", "3747": "", "3748": "现在做的就是所谓的正常位", "3749": "现在做的就是所谓的正常位", "3750": "现在做的就是所谓的正常位", "3751": "现在做的就是所谓的正常位", "3752": "现在做的就是所谓的正常位", "3753": "现在做的就是所谓的正常位", "3754": "现在做的 就是所谓的正常位", "3755": "现在做的就是所谓的正常位", "3756": "现在做的就是所谓的正常位", "3757": "现在做的就是所谓的正常位", "3758": "现在做的就是所谓的正常位", "3759": "根据患者的不同也有各种各样的体位", "3760": "根据患者的不同也有各种各样的体位", "3761": "根据患者的不同也有各种各样的体位", "3762": "根据患者的不同也有各种各样的体位", "3763": "根据患者的不同也有各种各样的体位", "3764": "根据患者的不同也有各种各样的体位", "3765": "根据患者的不同 也有各种各样的体位", "3766": "根据患者的不同也有各种各样的体位", "3767": "根据患者的不同也有各种各样的体位", "3768": "根据患者的不同也有各种各样的体位", "3769": "根据患者的不同也有各种各样的体位", "3770": "根据患者的不同也有各种各样的体位", "3771": "所以就和我一起试试吧", "3772": "所以就和我一起试试吧", "3773": "所以就和我一起试试吧", "3774": "所以就和我一起试试吧", "3775": "不要 我不要", "3776": "不要 我不要", "3777": "不要 我不要", "3778": "不要 我不要", "3779": "不要 我不要", "3780": "", "3781": "", "3782": "", "3783": "", "3784": "", "3785": "", "3786": "你可以趴起来了", "3787": "你可以趴起来了", "3788": "你可以趴起来了", "3789": "你可以趴起来了", "3790": "你可以趴起来了", "3791": "你可以趴起来了", "3792": "你可以趴起来了", "3793": "", "3794": "快点", "3795": "快点", "3796": "快点", "3797": "快点", "3798": "快点", "3799": "", "3800": "", "3801": "---", "3802": "", "3803": "---", "3804": "", "3805": "", "3806": "---", "3807": "", "3808": "", "3809": "", "3810": "---", "3811": "", "3812": "", "3813": "", "3814": "", "3815": "", "3816": "", "3817": "", "3818": "", "3819": "", "3820": "", "3821": "A", "3822": "", "3823": "", "3824": "", "3825": "", "3826": "", "3827": "", "3828": "", "3829": "", "3830": "不要 不要乱动", "3831": "不要 不要乱动", "3832": "不要 不要乱动", "3833": "不要不要乱动", "3834": "不要 不要乱动", "3835": "不要 不要乱动", "3836": "要试试看各种体位", "3837": "要试试看各种体位", "3838": "要试试看各种体位", "3839": "要试试看各种体位", "3840": "要试试看各种体位", "3841": "我做不到", "3842": "我做不到", "3843": "真正上场的时候 你就不会太紧张了", "3844": "真正上场的时候 你就不会太紧张了", "3845": "真正上场的时候 你就不会太紧张了", "3846": "真正上场的时候 你就不会太紧张了", "3847": "真正上场的时候 你就不会太紧张了", "3848": "真正上场的时候 你就不会太紧张了", "3849": "真正上场的时候你就不会太紧张了", "3850": "真正上场的时候 你就不会太紧张了", "3851": "真正上场的时候 你就不会太紧张了", "3852": "真正上场的时候 你就不会太紧张了", "3853": "", "3854": "", "3855": "", "3856": "", "3857": "", "3858": "", "3859": "", "3860": "", "3861": "", "3862": "", "3863": "", "3864": "", "3865": "", "3866": "", "3867": "", "3868": "", "3869": "", "3870": "", "3871": "", "3872": "", "3873": "", "3874": "毕竟有些人就是喜欢那种女性嘛", "3875": "毕竟有些人 就是喜欢那种女性嘛", "3876": "毕竟有些人 就是喜欢那种女性嘛", "3877": "毕竟有些人 就是喜欢那种女性嘛", "3878": "毕竟有些人 就是喜欢那种女性嘛", "3879": "毕竟有些人 就是喜欢那种女性嘛", "3880": "毕竟有些人 就是喜欢那种女性嘛", "3881": "毕竟有些人 就是喜欢那种女性嘛", "3882": "毕竟有些人 就是喜欢那种女性嘛", "3883": "毕竟有些人 就是喜欢那种女性嘛", "3884": "毕竟有些人 就是喜欢那种女性嘛", "3885": "毕竟有些人 就是喜欢那种女性嘛", "3886": "毕竟有些人 就是喜欢那种女性嘛", "3887": "我做不到 已经不行了", "3888": "我做不到 已经不行了", "3889": "我做不到 已经不行了", "3890": "我做不到 已经不行了", "3891": "我做不到 已经不行了", "3892": "我做不到 已经不行了", "3893": "我做不到 已经不行了", "3894": "我做不到 已经不行了", "3895": "我做不到 已经不行了", "3896": "", "3897": "", "3898": "", "3899": "HTML", "3900": "", "3901": "这种事怎么可能做得到呢 明天可就来不及适应了", "3902": "这种事怎么可能做得到呢 明天可就来不及适应了", "3903": "这种事怎么可能做得到呢 明天可就来不及适应了", "3904": "这种事怎么可能做得到呢 明天可就来不及适应了", "3905": "这种事怎么可能做得到呢 明天可就来不及适应了", "3906": "这种事怎么可能做得到呢 明天可就来不及适应了", "3907": "这种事怎么可能做得到呢 明天可就来不及适应了", "3908": "这种事怎么可能做得到呢 明天可就来不及适应了", "3909": "---", "3910": "", "3911": "---", "3912": "---", "3913": "---", "3914": "------------------", "3915": "------", "3916": "------", "3917": "---", "3918": "------", "3919": "", "3920": "------", "3921": "---", "3922": "---", "3923": "", "3924": "---", "3925": "", "3926": "", "3927": "------", "3928": "---", "3929": "---", "3930": "---", "3931": "---", "3932": "---", "3933": "---", "3934": "---", "3935": "------", "3936": "", "3937": "---", "3938": "---", "3939": "------", "3940": "", "3941": "---", "3942": "---", "3943": "---", "3944": "---", "3945": "---", "3946": "", "3947": "", "3948": "------", "3949": "", "3950": "", "3951": "", "3952": "", "3953": "", "3954": "抬起头来 好好看看自己现在身处的状况", "3955": "抬起头来 好好看看自己现在身处的状况", "3956": "抬起头来 好好看看自己现在身处的状况", "3957": "抬起头来 好好看看自己现在身处的状况", "3958": "抬起头来 好好看看自己现在身处的状况", "3959": "抬起头来 好好看看自己现在身处的状况", "3960": "抬起头来 好好看看自己现在身处的状况", "3961": "抬起头来 好好看看自己现在身处的状况", "3962": "抬起头来 好好看看自己现在身处的状况", "3963": "抬起头来 好好看看自己现在身处的状况", "3964": "抬起头来 好好看看自己现在身处的状况", "3965": "抬起头来 好好看看自己现在身处的状况", "3966": "抬起头来 好好看看自己现在身处的状况", "3967": "抬起头来 好好看看自己现在身处的状况", "3968": "抬起头来 好好看看自己现在身处的状况", "3969": "抬起头来 好好看看自己现在身处的状况", "3970": "", "3971": "", "3972": "", "3973": "", "3974": "", "3975": "", "3976": "", "3977": "", "3978": "", "3979": "", "3980": "", "3981": "", "3982": "", "3983": "", "3984": "", "3985": "", "3986": "", "3987": "", "3988": "", "3989": "", "3990": "", "3991": "", "3992": "", "3993": "", "3994": "", "3995": "", "3996": "", "3997": "", "3998": "", "3999": "", "4000": "", "4001": "", "4002": "", "4003": "", "4004": "", "4005": "", "4006": "", "4007": "", "4008": "", "4009": "", "4010": "", "4011": "", "4012": "", "4013": "", "4014": "", "4015": "", "4016": "", "4017": "", "4018": "不行 请住手", "4019": "不行 请住手", "4020": "不行 请住手", "4021": "不行 请住手", "4022": "不行 请住手", "4023": "不行 请住手", "4024": "不行 请住手", "4025": "不行 请住手", "4026": "", "4027": "", "4028": "", "4029": "求求你 放过我吧 我不会放过你", "4030": "求求你 放过我吧 我不会放过你", "4031": "求求你 放过我吧 我不会放过你", "4032": "求求你 放过我吧 我不会放过你", "4033": "求求你 放过我吧 我不会放过你", "4034": "求求你 放过我吧 我不会放过你", "4035": "求求你 放过我吧 我不会放过你", "4036": "求求你 放过我吧 我不会放过你", "4037": "求求你 放过我吧 我不会放过你", "4038": "求求你 放过我吧 我不会放过你", "4039": "求求你 放过我吧 我不会放过你", "4040": "求求你 放过我吧 我不会放过你", "4041": "", "4042": "", "4043": "", "4044": "", "4045": "", "4046": "", "4047": "---", "4048": "", "4049": "", "4050": "", "4051": "", "4052": "", "4053": "", "4054": "", "4055": "", "4056": "", "4057": "", "4058": "", "4059": "", "4060": "", "4061": "", "4062": "", "4063": "", "4064": "", "4065": "", "4066": "", "4067": "", "4068": "", "4069": "", "4070": "", "4071": "", "4072": "", "4073": "", "4074": "", "4075": "", "4076": "", "4077": "", "4078": "", "4079": "", "4080": "---", "4081": "", "4082": "", "4083": "", "4084": "---", "4085": "", "4086": "", "4087": "", "4088": "", "4089": "", "4090": "------", "4091": "---", "4092": "---", "4093": "------------------", "4094": "---", "4095": "", "4096": "---", "4097": "---", "4098": "---", "4099": "---", "4100": "------", "4101": "---", "4102": "---", "4103": "", "4104": "", "4105": "", "4106": "---", "4107": "", "4108": "", "4109": "", "4110": "", "4111": "", "4112": "", "4113": "", "4114": "", "4115": "", "4116": "就是这种感觉", "4117": "就是这种感觉", "4118": "就是这种感觉", "4119": "就是这种感觉", "4120": "就是这种感觉", "4121": "就是这种感觉", "4122": "就是这种感觉", "4123": "", "4124": "", "4125": "", "4126": "", "4127": "", "4128": "", "4129": "", "4130": "", "4131": "要插进小穴最里面才可以", "4132": "要插进小穴最里面才可以", "4133": "要插进小穴最里面才可以", "4134": "要插进小穴最里面才可以", "4135": "要插进小穴最里面才可以", "4136": "要插进小穴最里面才可以", "4137": "要插进小穴最里面才可以", "4138": "要插进小穴最里面才可以", "4139": "---", "4140": "---", "4141": "", "4142": "", "4143": "", "4144": "", "4145": "---", "4146": "", "4147": "---", "4148": "", "4149": "", "4150": "", "4151": "", "4152": "---", "4153": "", "4154": "", "4155": "", "4156": "", "4157": "", "4158": "", "4159": "---", "4160": "", "4161": "", "4162": "", "4163": "", "4164": "", "4165": "", "4166": "", "4167": "", "4168": "", "4169": "", "4170": "", "4171": "", "4172": "", "4173": "", "4174": "", "4175": "", "4176": "---", "4177": "", "4178": "", "4179": "", "4180": "", "4181": "", "4182": "", "4183": "", "4184": "", "4185": "", "4186": "", "4187": "", "4188": "---", "4189": "", "4190": "", "4191": "", "4192": "---", "4193": "---------------", "4194": "", "4195": "", "4196": "", "4197": "---------------", "4198": "", "4199": "------", "4200": "------", "4201": "------", "4202": "---", "4203": "---", "4204": "", "4205": "", "4206": "别再这样了 不行", "4207": "别再这样了 不行", "4208": "别再这样了 不行", "4209": "别再这样了 不行", "4210": "别再这样了 不行", "4211": "别再这样了 不行", "4212": "正好舒服起来了 不会停的", "4213": "正好舒服起来了 不会停的", "4214": "正好舒服起来了 不会停的", "4215": "正好舒服起来了 不会停的", "4216": "正好舒服起来了 不会停的", "4217": "正好舒服起来了 不会停的", "4218": "正好舒服起来了 不会停的", "4219": "---", "4220": "", "4221": "住手", "4222": "住手", "4223": "住手", "4224": "住手", "4225": "住手", "4226": "", "4227": "", "4228": "", "4229": "", "4230": "", "4231": "", "4232": "", "4233": "救救我", "4234": "救救我", "4235": "救救我", "4236": "救救我", "4237": "救救我", "4238": "", "4239": "", "4240": "", "4241": "", "4242": "", "4243": "", "4244": "", "4245": "", "4246": "", "4247": "", "4248": "", "4249": "", "4250": "", "4251": "", "4252": "", "4253": "", "4254": "", "4255": "", "4256": "", "4257": "", "4258": "", "4259": "", "4260": "", "4261": "", "4262": "", "4263": "", "4264": "", "4265": "", "4266": "", "4267": "", "4268": "", "4269": "还没结束呢 把腿抬起来", "4270": "还没结束呢 把腿抬起来", "4271": "还没结束呢 把腿抬起来", "4272": "还没结束呢 把腿抬起来", "4273": "还没结束呢 把腿抬起来", "4274": "还没结束呢 把腿抬起来", "4275": "还没结束呢 把腿抬起来", "4276": "还没结束呢 把腿抬起来", "4277": "还没结束呢 把腿抬起来", "4278": "还没结束呢 把腿抬起来", "4279": "还没结束呢 把腿抬起来", "4280": "", "4281": "", "4282": "---------------", "4283": "", "4284": "", "4285": "", "4286": "", "4287": "", "4288": "", "4289": "", "4290": "", "4291": "", "4292": "", "4293": "", "4294": "", "4295": "", "4296": "", "4297": "", "4298": "不行 不要", "4299": "不行 不要", "4300": "不行 不要", "4301": "不行 不要", "4302": "不行 不要", "4303": "不行 不要", "4304": "不行 不要", "4305": "不行 不要", "4306": "不行 不要", "4307": "不行 不要", "4308": "", "4309": "", "4310": "", "4311": "看清楚我的脸", "4312": "看清楚我的脸", "4313": "看清楚我的脸", "4314": "看清楚我的脸", "4315": "看清楚我的脸", "4316": "", "4317": "", "4318": "第一次的对象可是我这个医务室长", "4319": "第一次的对象 可是我这个医务室长", "4320": "第一次的对象可是我这个医务室长", "4321": "第一次的对象 可是我这个医务室长", "4322": "第一次的对象可是我这个医务室长", "4323": "第一次的对象 可是我这个医务室长", "4324": "第一次的对象 可是我这个医务室长", "4325": "第一次的对象可是我这个医务室长", "4326": "第一次的对象可是我这个医务室长", "4327": "第一次的对象 可是我这个医务室长", "4328": "第一次的对象可是我这个医务室长", "4329": "第一次的对象 可是我这个医务室长", "4330": "不要 我不要", "4331": "不要 我不要", "4332": "不要 我不要", "4333": "不要 我不要", "4334": "不要 我不要", "4335": "不要 我不要", "4336": "不要 我不要", "4337": "---------------", "4338": "------", "4339": "------------------", "4340": "", "4341": "把眼睛睁开看过来", "4342": "把眼睛睁开看过来", "4343": "把眼睛睁开看过来", "4344": "把眼睛睁开看过来", "4345": "把眼睛睁开看过来", "4346": "把眼睛睁开看过来", "4347": "不要 不可以", "4348": "不要 不可以", "4349": "不要 不可以", "4350": "不要 不可以", "4351": "不要 不可以", "4352": "不要 不可以", "4353": "不要 不可以", "4354": "不要 不可以", "4355": "不要 不可以", "4356": "以后你会觉得第一次", "4357": "以后你会觉得第一次", "4358": "以后你会觉得第一次", "4359": "以后你会觉得第一次", "4360": "以后你会觉得第一次", "4361": "以后你会觉得第一次", "4362": "以后你会觉得第一次", "4363": "以后你会觉得第一次", "4364": "跟权藤医生做真的太好了", "4365": "跟权藤医生做真的太好了", "4366": "跟权藤医生做真的太好了", "4367": "跟权藤医生做真的太好了", "4368": "跟权藤医生做真的太好了", "4369": "跟权藤医生做真的太好了", "4370": "跟权藤医生做真的太好了", "4371": "跟权藤医生做真的太好了", "4372": "跟权藤医生做真的太好了", "4373": "我不要 不可以 住手", "4374": "我不要 不可以 住手", "4375": "我不要 不可以 住手", "4376": "我不要 不可以 住手", "4377": "我不要 不可以 住手", "4378": "我不要 不可以 住手", "4379": "我不要 不可以 住手", "4380": "我不要 不可以 住手", "4381": "我不要 不可以 住手", "4382": "我不要 不可以 住手", "4383": "我不要 不可以 住手", "4384": "我不要 不可以 住手", "4385": "我不要 不可以 住手", "4386": "", "4387": "", "4388": "", "4389": "", "4390": "", "4391": "", "4392": "------------------", "4393": "", "4394": "---", "4395": "让我用力搅动里面", "4396": "让我用力搅动里面", "4397": "让我用力搅动里面", "4398": "让我用力搅动里面", "4399": "让我用力搅动里面", "4400": "让我用力搅动里面", "4401": "让我用力搅动里面", "4402": "让我用力搅动里面", "4403": "让我用力搅动里面", "4404": "让我用力搅动里面", "4405": "让我用力搅动里面", "4406": "---", "4407": "---", "4408": "------", "4409": "", "4410": "------", "4411": "不行", "4412": "不行", "4413": "不行", "4414": "不行", "4415": "------------------", "4416": "------------------", "4417": "---", "4418": "", "4419": "---------------", "4420": "------", "4421": "------", "4422": "------", "4423": "------", "4424": "------", "4425": "", "4426": "------", "4427": "------", "4428": "---------------", "4429": "------------------", "4430": "------", "4431": "------", "4432": "---", "4433": "------", "4434": "------", "4435": "------------------", "4436": "------", "4437": "------", "4438": "------------------", "4439": "", "4440": "------------------", "4441": "", "4442": "", "4443": "------", "4444": "------------------", "4445": "---", "4446": "------------------", "4447": "------", "4448": "", "4449": "--- ---", "4450": "------------------", "4451": "------------------", "4452": "------", "4453": "------", "4454": "------------------", "4455": "------", "4456": "------", "4457": "---", "4458": "------", "4459": "---", "4460": "--- ---", "4461": "------------", "4462": "------", "4463": "---", "4464": "---------------", "4465": "", "4466": "---", "4467": "------", "4468": "", "4469": "---", "4470": "---", "4471": "------", "4472": "", "4473": "---", "4474": "------", "4475": "---", "4476": "---", "4477": "------", "4478": "---", "4479": "---", "4480": "---", "4481": "---", "4482": "---", "4483": "", "4484": "------", "4485": "", "4486": "------", "4487": "", "4488": "---------------", "4489": "---", "4490": "------", "4491": "---", "4492": "------", "4493": "---", "4494": "------------------", "4495": "", "4496": "---", "4497": "---", "4498": "---", "4499": "------------------", "4500": "", "4501": "", "4502": "---", "4503": "---", "4504": "", "4505": "---", "4506": "", "4507": "", "4508": "", "4509": "", "4510": "", "4511": "", "4512": "", "4513": "", "4514": "", "4515": "", "4516": "", "4517": "", "4518": "你刚刚感觉到了", "4519": "你刚刚感觉到了", "4520": "你刚刚感觉到了", "4521": "你刚刚感觉到了", "4522": "你刚刚感觉到了", "4523": "你刚刚感觉到了", "4524": "", "4525": "", "4526": "", "4527": "用力抓住我", "4528": "用力抓住我", "4529": "用力抓住我", "4530": "用力抓住我", "4531": "用力抓住我", "4532": "用力抓住我", "4533": "", "4534": "", "4535": "", "4536": "---", "4537": "---", "4538": "------", "4539": "", "4540": "", "4541": "", "4542": "", "4543": "", "4544": "", "4545": "", "4546": "", "4547": "", "4548": "", "4549": "", "4550": "", "4551": "", "4552": "", "4553": "", "4554": "", "4555": "", "4556": "", "4557": "", "4558": "", "4559": "", "4560": "这个是骑乘位", "4561": "这个是骑乘位", "4562": "这个是骑乘位", "4563": "这个是骑乘位", "4564": "这个是骑乘位", "4565": "这个是骑乘位", "4566": "这个是骑乘位", "4567": "这个是骑乘位", "4568": "", "4569": "", "4570": "", "4571": "", "4572": "", "4573": "", "4574": "", "4575": "", "4576": "", "4577": "", "4578": "", "4579": "", "4580": "", "4581": "", "4582": "", "4583": "", "4584": "", "4585": "不要 不可以", "4586": "不要 不可以", "4587": "不要 不可以", "4588": "不要 不可以", "4589": "不要 不可以", "4590": "不要 不可以", "4591": "不要 不可以", "4592": "", "4593": "---", "4594": "---", "4595": "---", "4596": "---", "4597": "", "4598": "---", "4599": "---", "4600": "---", "4601": "---", "4602": "---------------", "4603": "---", "4604": "医生 请住手", "4605": "医生 请住手", "4606": "医生 请住手", "4607": "医生 请住手", "4608": "医生 请住手", "4609": "医生 请住手", "4610": "医生 请住手", "4611": "医生 请住手", "4612": "---", "4613": "", "4614": "", "4615": "---", "4616": "---", "4617": "---", "4618": "---", "4619": "", "4620": "------------------", "4621": "", "4622": "------", "4623": "", "4624": "", "4625": "", "4626": "", "4627": "", "4628": "", "4629": "", "4630": "", "4631": "", "4632": "", "4633": "", "4634": "", "4635": "", "4636": "", "4637": "", "4638": "", "4639": "", "4640": "我都说了你没必要忍耐的", "4641": "我都说了你没必要忍耐的", "4642": "我都说了你没必要忍耐的", "4643": "我都说了你没必要忍耐的", "4644": "我都说了你没必要忍耐的", "4645": "我都说了你没必要忍耐的", "4646": "尽情舒服起来", "4647": "尽情舒服起来", "4648": "尽情舒服起来", "4649": "尽情舒服起来", "4650": "不要 把手撑在后面试试吧", "4651": "不要 把手撑在后面试试吧", "4652": "不要 把手撑在后面试试吧", "4653": "不要 把手撑在后面试试吧", "4654": "不要把手撑在后面试试吧", "4655": "不要 把手撑在后面试试吧", "4656": "不要 把手撑在后面试试吧", "4657": "不要 把手撑在后面试试吧", "4658": "不要 把手撑在后面试试吧", "4659": "不要 把手撑在后面试试吧", "4660": "", "4661": "", "4662": "", "4663": "", "4664": "", "4665": "", "4666": "", "4667": "", "4668": "", "4669": "", "4670": "", "4671": "", "4672": "", "4673": "", "4674": "", "4675": "", "4676": "", "4677": "", "4678": "", "4679": "", "4680": "", "4681": "", "4682": "", "4683": "等一下 别再这样了", "4684": "等一下 别再这样了", "4685": "等一下 别再这样了", "4686": "等一下 别再这样了", "4687": "等一下 别再这样了", "4688": "等一下 别再这样了", "4689": "", "4690": "", "4691": "", "4692": "", "4693": "", "4694": "", "4695": "", "4696": "不要 不可以", "4697": "不要 不可以", "4698": "不要 不可以", "4699": "不要 不可以", "4700": "不要 不可以", "4701": "不要 不可以", "4702": "不要 不可以", "4703": "不要 求求你停下来", "4704": "不要 求求你停下来", "4705": "不要 求求你停下来", "4706": "不要 求求你停下来", "4707": "不要 求求你停下来", "4708": "不要 求求你停下来", "4709": "不要 求求你停下来", "4710": "------", "4711": "---", "4712": "", "4713": "---", "4714": "------------------", "4715": "---", "4716": "---", "4717": "---", "4718": "---", "4719": "", "4720": "", "4721": "", "4722": "---", "4723": "", "4724": "", "4725": "", "4726": "", "4727": "------------", "4728": "", "4729": "", "4730": "", "4731": "", "4732": "", "4733": "", "4734": "", "4735": "", "4736": "", "4737": "", "4738": "", "4739": "", "4740": "", "4741": "", "4742": "", "4743": "", "4744": "再做点别的吧", "4745": "再做点别的吧", "4746": "再做点别的吧", "4747": "再做点别的吧", "4748": "再做点别的吧", "4749": "", "4750": "", "4751": "", "4752": "接吻呢 接吻", "4753": "接吻呢 接吻", "4754": "接吻呢 接吻", "4755": "接吻呢 接吻", "4756": "接吻呢 接吻", "4757": "接吻呢 接吻", "4758": "为什么不接吻呢", "4759": "为什么不接吻呢", "4760": "为什么不接吻呢", "4761": "为什么不接吻呢", "4762": "为什么不接吻呢", "4763": "为什么不接吻呢", "4764": "为什么不接吻呢", "4765": "", "4766": "", "4767": "", "4768": "", "4769": "", "4770": "", "4771": "", "4772": "", "4773": "", "4774": "", "4775": "", "4776": "", "4777": "", "4778": "", "4779": "", "4780": "", "4781": "", "4782": "", "4783": "", "4784": "", "4785": "别总是反抗哦", "4786": "别总是反抗哦", "4787": "别总是反抗哦", "4788": "别总是反抗哦", "4789": "别总是反抗哦", "4790": "别总是反抗哦", "4791": "别总是反抗哦", "4792": "别总是反抗哦", "4793": "别总是反抗哦", "4794": "别总是反抗哦", "4795": "别总是反抗哦", "4796": "别总是反抗哦", "4797": "", "4798": "", "4799": "", "4800": "", "4801": "", "4802": "", "4803": "", "4804": "---", "4805": "", "4806": "", "4807": "", "4808": "", "4809": "", "4810": "---", "4811": "", "4812": "", "4813": "", "4814": "------", "4815": "", "4816": "", "4817": "", "4818": "我是要负起责任的", "4819": "我是要负起责任的", "4820": "我是要负起责任的", "4821": "我是要负起责任的", "4822": "我是要负起责任的", "4823": "我是要负起责任的", "4824": "我是要负起责任的", "4825": "我是要负起责任的", "4826": "", "4827": "即使跟VIP患者接触 我也会这样帮你的", "4828": "即使跟VIP患者接触 我也会这样帮你的", "4829": "即使跟VIP患者接触 我也会这样帮你的", "4830": "即使跟VIP患者接触 我也会这样帮你的", "4831": "即使跟VIP患者接触 我也会这样帮你的", "4832": "即使跟VIP患者接触 我也会这样帮你的", "4833": "即使跟VIP患者接触 我也会这样帮你的", "4834": "即使跟VIP患者接触 我也会这样帮你的", "4835": "即使跟VIP患者接触 我也会这样帮你的", "4836": "即使跟VIP患者接触 我也会这样帮你的", "4837": "", "4838": "", "4839": "", "4840": "", "4841": "", "4842": "", "4843": "", "4844": "", "4845": "", "4846": "", "4847": "", "4848": "", "4849": "---", "4850": "", "4851": "", "4852": "", "4853": "---", "4854": "", "4855": "", "4856": "", "4857": "", "4858": "", "4859": "", "4860": "", "4861": "---", "4862": "---", "4863": "", "4864": "------", "4865": "", "4866": "---", "4867": "", "4868": "", "4869": "", "4870": "", "4871": "", "4872": "---", "4873": "", "4874": "---------------", "4875": "", "4876": "", "4877": "", "4878": "", "4879": "危险 危险 要倒了", "4880": "危险 危险 要倒了", "4881": "危险 危险 要倒了", "4882": "危险 危险 要倒了", "4883": "危险 危险 要倒了", "4884": "危险 危险 要倒了", "4885": "危险 危险 要倒了", "4886": "危险 危险 要倒了", "4887": "", "4888": "", "4889": "", "4890": "", "4891": "", "4892": "", "4893": "", "4894": "", "4895": "", "4896": "你躺下去吧", "4897": "你躺下去吧", "4898": "你躺下去吧", "4899": "你躺下去吧", "4900": "你躺下去吧", "4901": "", "4902": "果然 第一次 这个姿势最好", "4903": "果然 第一次 这个姿势最好", "4904": "果然 第一次 这个姿势最好", "4905": "果然 第一次 这个姿势最好", "4906": "果然 第一次 这个姿势最好", "4907": "果然 第一次 这个姿势最好", "4908": "果然 第一次 这个姿势最好", "4909": "果然 第一次 这个姿势最好", "4910": "果然 第一次 这个姿势最好", "4911": "果然 第一次 这个姿势最好", "4912": "果然 第一次 这个姿势最好", "4913": "", "4914": "", "4915": "", "4916": "", "4917": "", "4918": "", "4919": "", "4920": "", "4921": "---", "4922": "---", "4923": "", "4924": "", "4925": "", "4926": "", "4927": "", "4928": "------", "4929": "---", "4930": "", "4931": "---", "4932": "---", "4933": "", "4934": "---", "4935": "", "4936": "", "4937": "", "4938": "---", "4939": "", "4940": "", "4941": "", "4942": "", "4943": "", "4944": "---", "4945": "", "4946": "---", "4947": "---", "4948": "", "4949": "", "4950": "", "4951": "", "4952": "", "4953": "", "4954": "", "4955": "", "4956": "", "4957": "", "4958": "", "4959": "", "4960": "", "4961": "", "4962": "", "4963": "", "4964": "", "4965": "", "4966": "", "4967": "", "4968": "", "4969": "", "4970": "", "4971": "", "4972": "", "4973": "", "4974": "", "4975": "---", "4976": "让我好好看看你的脸", "4977": "让我好好看看你的脸", "4978": "让我好好看看你的脸", "4979": "让我好好看看你的脸", "4980": "让我好好看看你的脸", "4981": "让我好好看看你的脸", "4982": "让我好好看看你的脸", "4983": "让我好好看看你的脸", "4984": "让我好好看看你的脸", "4985": "---", "4986": "", "4987": "", "4988": "---", "4989": "------------------", "4990": "不要", "4991": "不要", "4992": "不要", "4993": "不要", "4994": "不要", "4995": "------", "4996": "------------------", "4997": "---", "4998": "---", "4999": "---", "5000": "---", "5001": "------", "5002": "---", "5003": "", "5004": "---", "5005": "", "5006": "---", "5007": "------", "5008": "------------------", "5009": "", "5010": "", "5011": "---", "5012": "---", "5013": "", "5014": "---", "5015": "", "5016": "---", "5017": "------", "5018": "", "5019": "", "5020": "", "5021": "---", "5022": "---", "5023": "------------------", "5024": "", "5025": "---", "5026": "", "5027": "", "5028": "------", "5029": "", "5030": "------------------", "5031": "", "5032": "", "5033": "------", "5034": "", "5035": "------", "5036": "---------------------", "5037": "------", "5038": "------", "5039": "------", "5040": "------", "5041": "------", "5042": "------", "5043": "------", "5044": "------", "5045": "------", "5046": "", "5047": "", "5048": "------------------", "5049": "------", "5050": "---", "5051": "---", "5052": "", "5053": "", "5054": "", "5055": "", "5056": "", "5057": "", "5058": "", "5059": "", "5060": "", "5061": "", "5062": "", "5063": "", "5064": "", "5065": "", "5066": "", "5067": "", "5068": "北冈小姐", "5069": "北冈小姐", "5070": "北冈小姐", "5071": "北冈小姐", "5072": "你要接受现实了", "5073": "你要接受现实了", "5074": "你要接受现实了", "5075": "你要接受现实了", "5076": "你要接受现实了", "5077": "你要接受现实了", "5078": "你要接受现实了", "5079": "你要接受现实了", "5080": "你要接受现实了", "5081": "", "5082": "", "5083": "", "5084": "", "5085": "", "5086": "", "5087": "---", "5088": "---", "5089": "---", "5090": "---", "5091": "", "5092": "---", "5093": "---", "5094": "---", "5095": "---", "5096": "", "5097": "", "5098": "", "5099": "", "5100": "", "5101": "---", "5102": "", "5103": "", "5104": "", "5105": "", "5106": "", "5107": "", "5108": "", "5109": "", "5110": "", "5111": "", "5112": "---", "5113": "", "5114": "", "5115": "这就是所谓的做爱", "5116": "这就是所谓的做爱", "5117": "这就是所谓的做爱", "5118": "这就是所谓的做爱", "5119": "这就是所谓的做爱", "5120": "这就是所谓的做爱", "5121": "这就是所谓的做爱", "5122": "", "5123": "---", "5124": "", "5125": "", "5126": "", "5127": "", "5128": "", "5129": "", "5130": "做爱这种事呢", "5131": "做爱这种事呢", "5132": "做爱这种事呢", "5133": "做爱这种事呢", "5134": "做爱这种事呢", "5135": "最后男人射精之后就结束了", "5136": "最后男人射精之后就结束了", "5137": "最后男人射精之后就结束了", "5138": "最后男人射精之后就结束了", "5139": "最后男人射精之后就结束了", "5140": "最后男人射精之后就结束了", "5141": "最后男人射精之后就结束了", "5142": "最后男人射精之后就结束了", "5143": "今天", "5144": "今天", "5145": "今天", "5146": "今天", "5147": "就要直接在你的里面射出来了", "5148": "就要直接在你的里面射出来了", "5149": "就要直接在你的里面射出来了", "5150": "就要直接在你的里面射出来了", "5151": "就要直接在你的里面射出来了", "5152": "就要直接在你的里面射出来了", "5153": "不要 不可以这样 不要管了 没事的", "5154": "不要 不可以这样 不要管了 没事的", "5155": "不要 不可以这样 不要管了 没事的", "5156": "不要 不可以这样 不要管了 没事的", "5157": "不要 不可以这样 不要管了 没事的", "5158": "不要 不可以这样 不要管了 没事的", "5159": "不要 不可以这样 不要管了 没事的", "5160": "不要 不可以这样 不要管了 没事的", "5161": "不要 不可以这样 不要管了 没事的", "5162": "不要 不可以这样 不要管了 没事的", "5163": "不要 不可以这样 不要管了 没事的", "5164": "住手 不要射进去 我不行了", "5165": "住手 不要射进去 我不行了", "5166": "住手 不要射进去 我不行了", "5167": "住手 不要射进去 我不行了", "5168": "住手 不要射进去 我不行了", "5169": "住手 不要射进去 我不行了", "5170": "住手 不要射进去 我不行了", "5171": "住手 不要射进去 我不行了", "5172": "住手 不要射进去 我不行了", "5173": "住手 不要射进去 我不行了", "5174": "住手 不要射进去 我不行了", "5175": "拜托你 请住手 不要这样", "5176": "拜托你 请住手 不要这样", "5177": "拜托你 请住手 不要这样", "5178": "拜托你 请住手 不要这样", "5179": "拜托你 请住手 不要这样", "5180": "拜托你 请住手 不要这样", "5181": "拜托你 请住手 不要这样", "5182": "拜托你 请住手 不要这样", "5183": "拜托你 请住手 不要这样", "5184": "拜托你 请住手 不要这样", "5185": "接受它吧 求求你了 不要", "5186": "接受它吧 求求你了 不要", "5187": "接受它吧 求求你了 不要", "5188": "接受它吧 求求你了 不要", "5189": "接受它吧 求求你了 不要", "5190": "接受它吧 求求你了 不要", "5191": "接受它吧 求求你了 不要", "5192": "接受它吧 求求你了 不要", "5193": "接受它吧 求求你了 不要", "5194": "---", "5195": "", "5196": "", "5197": "", "5198": "---------------", "5199": "", "5200": "", "5201": "要射了", "5202": "要射了", "5203": "要射了", "5204": "要射了", "5205": "要射了", "5206": "要射了", "5207": "", "5208": "射了", "5209": "射了", "5210": "射了", "5211": "射了", "5212": "射了", "5213": "", "5214": "来吧 好好接住它", "5215": "来吧 好好接住它", "5216": "来吧 好好接住它", "5217": "来吧 好好接住它", "5218": "来吧 好好接住它", "5219": "来吧 好好接住它", "5220": "来吧 好好接住它", "5221": "来吧 好好接住它", "5222": "来吧 好好接住它", "5223": "", "5224": "", "5225": "", "5226": "", "5227": "", "5228": "", "5229": "", "5230": "", "5231": "", "5232": "", "5233": "---", "5234": "", "5235": "", "5236": "", "5237": "", "5238": "", "5239": "", "5240": "别担心 全部射完就拔出来了", "5241": "别担心 全部射完就拔出来了", "5242": "别担心 全部射完就拔出来了", "5243": "别担心 全部射完就拔出来了", "5244": "别担心 全部射完就拔出来了", "5245": "别担心 全部射完就拔出来了", "5246": "别担心 全部射完就拔出来了", "5247": "别担心 全部射完就拔出来了", "5248": "", "5249": "", "5250": "", "5251": "---------------", "5252": "", "5253": "", "5254": "", "5255": "", "5256": "", "5257": "", "5258": "", "5259": "", "5260": "", "5261": "", "5262": "", "5263": "", "5264": "---", "5265": "---------------", "5266": "------------", "5267": "", "5268": "------------", "5269": "------", "5270": "------------", "5271": "", "5272": "---", "5273": "------------", "5274": "------------", "5275": "---", "5276": "", "5277": "", "5278": "------------", "5279": "---------------", "5280": "------------", "5281": "------", "5282": "", "5283": "------------", "5284": "------------", "5285": "---", "5286": "---------------", "5287": "---", "5288": "---", "5289": "------------", "5290": "---------------", "5291": "------------", "5292": "", "5293": "---", "5294": "", "5295": "------------", "5296": "", "5297": "---------------", "5298": "---------------", "5299": "------------", "5300": "", "5301": "", "5302": "", "5303": "", "5304": "明天开始你就是特别单间病房患者的负责人", "5305": "明天开始你就是特别单间病房患者的负责人", "5306": "明天开始你就是特别单间 病房患者的负责人", "5307": "明天开始你就是特别单间 病房患者的负责人", "5308": "明天开始你就是特别单间 病房患者的负责人", "5309": "明天开始你就是特别单间病房患者的负责人", "5310": "明天开始你就是特别单间病房患者的负责人", "5311": "明天开始你就是特别单间 病房患者的负责人", "5312": "明天开始你就是特别单间 病房患者的负责人", "5313": "明天开始你就是特别单间病房患者的负责人", "5314": "明天开始你就是特别单间病房患者的负责人", "5315": "请感到光荣吧 你妈妈也会很高兴的", "5316": "请感到光荣吧 你妈妈也会很高兴的", "5317": "请感到光荣吧 你妈妈也会很高兴的", "5318": "请感到光荣吧 你妈妈也会很高兴的", "5319": "请感到光荣吧 你妈妈也会很高兴的", "5320": "请感到光荣吧 你妈妈也会很高兴的", "5321": "请感到光荣吧 你妈妈也会很高兴的", "5322": "请感到光荣吧 你妈妈也会很高兴的", "5323": "请感到光荣吧 你妈妈也会很高兴的", "5324": "请感到光荣吧 你妈妈也会很高兴的", "5325": "请感到光荣吧 你妈妈也会很高兴的", "5326": "请感到光荣吧 你妈妈也会很高兴的", "5327": "请感到光荣吧 你妈妈也会很高兴的", "5328": "请感到光荣吧 你妈妈也会很高兴的", "5329": "请感到光荣吧 你妈妈也会很高兴的", "5330": "请感到光荣吧 你妈妈也会很高兴的", "5331": "请感到光荣吧 你妈妈也会很高兴的", "5332": "请感到光荣吧 你妈妈也会很高兴的", "5333": "", "5334": "", "5335": "---", "5336": "", "5337": "", "5338": "", "5339": "", "5340": "", "5341": "---", "5342": "---", "5343": "", "5344": "", "5345": "", "5346": "", "5347": "---", "5348": "", "5349": "", "5350": "", "5351": "", "5352": "", "5353": "", "5354": "", "5355": "", "5356": "", "5357": "", "5358": "", "5359": "", "5360": "", "5361": "", "5362": "", "5363": "---", "5364": "", "5365": "", "5366": "------------------", "5367": "Background", "5368": "", "5369": "", "5370": "", "5371": "", "5372": "------", "5373": "------", "5374": "------", "5375": "", "5376": "------------------", "5377": "---", "5378": "", "5379": "---", "5380": "", "5381": "", "5382": "", "5383": "", "5384": "", "5385": "---", "5386": "", "5387": "------", "5388": "---", "5389": "", "5390": "", "5391": "", "5392": "", "5393": "", "5394": "", "5395": "", "5396": "", "5397": "", "5398": "", "5399": "", "5400": "", "5401": "", "5402": "权藤医生把松井小姐叫出来", "5403": "权藤医生把松井小姐叫出来", "5404": "权藤医生把松井小姐叫出来", "5405": "权藤医生把松井小姐叫出来", "5406": "权藤医生把松井小姐叫出来", "5407": "权藤医生把松井小姐叫出来", "5408": "权藤医生把松井小姐叫出来", "5409": "权藤医生把松井小姐叫出来", "5410": "权藤医生把松井小姐叫出来", "5411": "特别单人VIP患者 夜班的呼叫", "5412": "特别单人VIP患者 夜班的呼叫", "5413": "特别单人VIP患者 夜班的呼叫", "5414": "特别单人VIP患者 夜班的呼叫", "5415": "特别单人VIP患者 夜班的呼叫", "5416": "特别单人VIP患者 夜班的呼叫", "5417": "特别单人VIP患者 夜班的呼叫", "5418": "特别单人VIP患者 夜班的呼叫", "5419": "特别单人VIP患者 夜班的呼叫", "5420": "特别单人VIP患者 夜班的呼叫", "5421": "特别单人VIP患者 夜班的呼叫", "5422": "特别单人VIP患者 夜班的呼叫", "5423": "", "5424": "那让我想起了讨厌的事情 从那天起我就觉得", "5425": "那让我想起了讨厌的事情 从那天起我就觉得", "5426": "那让我想起了讨厌的事情 从那天起我就觉得", "5427": "那让我想起了讨厌的事情 从那天起我就觉得", "5428": "那让我想起了讨厌的事情 从那天起我就觉得", "5429": "那让我想起了讨厌的事情 从那天起我就觉得", "5430": "那让我想起了讨厌的事情 从那天起我就觉得", "5431": "那让我想起了讨厌的事情 从那天起我就觉得", "5432": "那让我想起了讨厌的事情 从那天起我就觉得", "5433": "那让我想起了讨厌的事情 从那天起我就觉得", "5434": "那让我想起了讨厌的事情 从那天起我就觉得", "5435": "那让我想起了讨厌的事情 从那天起我就觉得", "5436": "这家医院疯了", "5437": "这家医院疯了", "5438": "这家医院疯了", "5439": "这家医院疯了", "5440": "这家医院疯了", "5441": "这家医院疯了", "5442": "这家医院疯了", "5443": "", "5444": "", "5445": "", "5446": "", "5447": "", "5448": "。", "5449": "", "5450": "", "5451": "", "5452": "", "5453": "", "5454": "", "5455": "", "5456": "", "5457": "", "5458": "上次真是非常抱歉", "5459": "上次真是非常抱歉", "5460": "上次真是非常抱歉", "5461": "上次真是非常抱歉", "5462": "上次真是非常抱歉", "5463": "上次真是非常抱歉", "5464": "上次真是非常抱歉", "5465": "上次真是非常抱歉", "5466": "", "5467": "", "5468": "", "5469": "", "5470": "", "5471": "", "5472": "", "5473": "", "5474": "因为我的指导不足", "5475": "因为我的指导不足", "5476": "因为我的指导不足", "5477": "因为我的指导不足", "5478": "因为我的指导不足", "5479": "对患者说了失礼的话", "5480": "对患者说了失礼的话", "5481": "对患者说了失礼的话", "5482": "对患者说了失礼的话", "5483": "对患者说了失礼的话", "5484": "对患者说了失礼的话", "5485": "", "5486": "", "5487": "", "5488": "", "5489": "", "5490": "", "5491": "", "5492": "", "5493": "", "5494": "", "5495": "", "5496": "不过 这次请你放心吧", "5497": "不过 这次请你放心吧", "5498": "不过 这次请你放心吧", "5499": "不过 这次请你放心吧", "5500": "不过 这次请你放心吧", "5501": "不过 这次请你放心吧", "5502": "不过 这次请你放心吧", "5503": "不过 这次请你放心吧", "5504": "不过 这次请你放心吧", "5505": "我们准备了方便指导的工作人员", "5506": "我们准备了方便指导的工作人员", "5507": "我们准备了方便指导的工作人员", "5508": "我们准备了方便指导的工作人员", "5509": "我们准备了方便指导的工作人员", "5510": "我们准备了方便指导的工作人员", "5511": "我们准备了方便指导的工作人员", "5512": "我们准备了 方便指导的工作人员", "5513": "", "5514": "好的 好的", "5515": "好的 好的", "5516": "好的 好的", "5517": "好的 好的", "5518": "好的 好的", "5519": "好的 好的", "5520": "好的 好的", "5521": "", "5522": "", "5523": "必须要", "5524": "必须要", "5525": "必须要", "5526": "必须要", "5527": "让你体验一下我们这家医院 引以为豪的独特服务专案", "5528": "让你体验一下我们这家医院 引以为豪的独特服务专案", "5529": "让你体验一下我们这家医院 引以为豪的独特服务专案", "5530": "让你体验一下我们这家医院 引以为豪的独特服务专案", "5531": "让你体验一下我们这家医院 引以为豪的独特服务专案", "5532": "让你体验一下我们这家医院 引以为豪的独特服务专案", "5533": "让你体验一下我们这家医院 引以为豪的独特服务专案", "5534": "让你体验一下我们这家医院 引以为豪的独特服务专案", "5535": "让你体验一下我们这家医院 引以为豪的独特服务专案", "5536": "", "5537": "", "5538": "", "5539": "", "5540": "", "5541": "", "5542": "", "5543": "------------------", "5544": "", "5545": "---", "5546": "", "5547": "", "5548": "", "5549": "---------------", "5550": "", "5551": "", "5552": "", "5553": "", "5554": "", "5555": "", "5556": "", "5557": "", "5558": "", "5559": "", "5560": "", "5561": "", "5562": "", "5563": "", "5564": "", "5565": "", "5566": "", "5567": "", "5568": "", "5569": "------", "5570": "---", "5571": "", "5572": "---", "5573": "------", "5574": "------", "5575": "", "5576": "", "5577": "", "5578": "", "5579": "", "5580": "", "5581": "", "5582": "", "5583": "", "5584": "", "5585": "", "5586": "", "5587": "", "5588": "", "5589": "", "5590": "基本信息", "5591": "资本 偶上郎", "5592": "", "5593": "", "5594": "No text is visible in the provided image.", "5595": "ITMMA PRO", "5596": "医生 屈立豪", "5597": "", "5598": "", "5599": "HTML", "5600": "", "5601": "", "5602": "", "5603": "", "5604": "", "5605": "叶山医生", "5606": "叶山医生", "5607": "叶山医生", "5608": "叶山医生", "5609": "叶山医生", "5610": "", "5611": "", "5612": "", "5613": "那个 有什么事吗", "5614": "那个 有什么事吗", "5615": "那个 有什么事吗", "5616": "那个 有什么事吗", "5617": "那个 有什么事吗", "5618": "那个 有什么事吗", "5619": "那个 有什么事吗", "5620": "那个 有什么事吗", "5621": "大 大 大", "5622": "", "5623": "权藤医生有指示", "5624": "权藤医生有指示", "5625": "权藤医生有指示", "5626": "权藤医生有指示", "5627": "权藤医生有指示", "5628": "98室[巴花堂]永久地址489155.com权藤医生有指示", "5629": "98室[芭花堂]永久地址489155.com权藤医生有指示", "5630": "98室[巴花堂]永久地址489155.com权藤医生有指示", "5631": "98室[芭花室]永久地址489155.com", "5632": "98室[巴花堂]永久地址489155.com", "5633": "98室[巴花堂]永久地址489155.com", "5634": "我们的工作需要你帮忙", "5635": "我们的工作需要你帮忙", "5636": "我们的工作需要你帮忙", "5637": "我们的工作需要你帮忙", "5638": "我们的工作需要你帮忙", "5639": "我们的工作需要你帮忙", "5640": "我们的工作需要你帮忙", "5641": "我们的工作需要你帮忙", "5642": "我们的工作需要你帮忙", "5643": "我们的工作需要你帮忙", "5644": "我们的工作需要你帮忙", "5645": "我们的工作需要你帮忙", "5646": "我们的工作需要你帮忙", "5647": "", "5648": "", "5649": "", "5650": "", "5651": "", "5652": "", "5653": "", "5654": "", "5655": "", "5656": "", "5657": "", "5658": "只要是我能做的我都愿意", "5659": "只要是我能做的我都愿意", "5660": "只要是我能做的我都愿意", "5661": "只要是我能做的我都愿意", "5662": "只要是我能做的我都愿意", "5663": "只要是我能做的我都愿意", "5664": "只要是我能做的我都愿意", "5665": "只要是我能做的我都愿意", "5666": "", "5667": "", "5668": "", "5669": "", "5670": "", "5671": "", "5672": "", "5673": "", "5674": "", "5675": "", "5676": "", "5677": "", "5678": "", "5679": "", "5680": "", "5681": "", "5682": "", "5683": "", "5684": "", "5685": "", "5686": "", "5687": "", "5688": "", "5689": "", "5690": "", "5691": "", "5692": "", "5693": "", "5694": "", "5695": "", "5696": "", "5697": "", "5698": "", "5699": "", "5700": "", "5701": "", "5702": "", "5703": "", "5704": "", "5705": "", "5706": "", "5707": "", "5708": "", "5709": "", "5710": "", "5711": "", "5712": "", "5713": "", "5714": "", "5715": "", "5716": "", "5717": "", "5718": "", "5719": "", "5720": "", "5721": "", "5722": "", "5723": "", "5724": "", "5725": "", "5726": "", "5727": "", "5728": "", "5729": "", "5730": "", "5731": "", "5732": "", "5733": "", "5734": "", "5735": "", "5736": "", "5737": "", "5738": "", "5739": "", "5740": "HTML", "5741": "", "5742": "", "5743": "这家医院 为VIP患者提供了特别服务", "5744": "这家医院 为VIP患者提供了特别服务", "5745": "这家医院 为VIP患者提供了特别服务", "5746": "这家医院 为VIP患者提供了特别服务", "5747": "这家医院 为VIP患者提供了特别服务", "5748": "这家医院 为VIP患者提供了特别服务", "5749": "这家医院 为VIP患者提供了特别服务", "5750": "这家医院 为VIP患者提供了特别服务", "5751": "这家医院 为VIP患者提供了特别服务", "5752": "这家医院 为VIP患者提供了特别服务", "5753": "这家医院 为VIP患者提供了特别服务", "5754": "这家医院 为VIP患者提供了特别服务", "5755": "这家医院 为VIP患者提供了特别服务", "5756": "这家医院 为VIP患者提供了特别服务", "5757": "这家医院 为VIP患者提供了特别服务", "5758": "这家医院 为VIP患者提供了特别服务", "5759": "这家医院 为VIP患者提供了特别服务", "5760": "这家医院 为VIP患者提供了特别服务", "5761": "这家医院 为VIP患者提供了特别服务", "5762": "而且 我也希望你能参加呢", "5763": "而且 我也希望你能参加呢", "5764": "而且我也希望你能参加呢", "5765": "而且我也希望你能参加呢", "5766": "而且 我也希望你能参加呢", "5767": "而且 我也希望你能参加呢", "5768": "而且我也希望你能参加呢", "5769": "而且我也希望你能参加呢", "5770": "而且 我也希望你能参加呢", "5771": "而且 我也希望你能参加呢", "5772": "", "5773": "", "5774": "", "5775": "特别服务", "5776": "特别服务", "5777": "特别服务", "5778": "特别服务", "5779": "特别服务", "5780": "", "5781": "", "5782": "", "5783": "", "5784": "", "5785": "", "5786": "", "5787": "", "5788": "", "5789": "", "5790": "", "5791": "------------", "5792": "---------------", "5793": "---", "5794": "---------------", "5795": "---------------", "5796": "------------", "5797": "---", "5798": "---------------", "5799": "", "5800": "", "5801": "------------", "5802": "---------------", "5803": "------------", "5804": "------------", "5805": "------", "5806": "---------------", "5807": "", "5808": "", "5809": "", "5810": "", "5811": "", "5812": "", "5813": "", "5814": "", "5815": "", "5816": "", "5817": "A", "5818": "", "5819": "", "5820": "", "5821": "", "5822": "", "5823": "", "5824": "", "5825": "", "5826": "", "5827": "", "5828": "", "5829": "", "5830": "", "5831": "", "5832": "", "5833": "", "5834": "", "5835": "", "5836": "", "5837": "", "5838": "", "5839": "", "5840": "", "5841": "", "5842": "", "5843": "", "5844": "", "5845": "", "5846": "", "5847": "", "5848": "", "5849": "", "5850": "", "5851": "", "5852": "", "5853": "", "5854": "", "5855": "", "5856": "", "5857": "", "5858": "", "5859": "", "5860": "", "5861": "", "5862": "", "5863": "", "5864": "", "5865": "", "5866": "", "5867": "", "5868": "", "5869": "", "5870": "", "5871": "", "5872": "", "5873": "", "5874": "", "5875": "", "5876": "", "5877": "", "5878": "", "5879": "", "5880": "", "5881": "", "5882": "", "5883": "", "5884": "", "5885": "", "5886": "", "5887": "", "5888": "", "5889": "", "5890": "", "5891": "", "5892": "", "5893": "", "5894": "", "5895": "", "5896": "", "5897": "", "5898": "", "5899": "", "5900": "", "5901": "", "5902": "", "5903": "", "5904": "", "5905": "", "5906": "", "5907": "", "5908": "", "5909": "", "5910": "", "5911": "", "5912": "", "5913": "", "5914": "", "5915": "", "5916": "", "5917": "", "5918": "", "5919": "", "5920": "", "5921": "", "5922": "", "5923": "", "5924": "", "5925": "", "5926": "", "5927": "", "5928": "", "5929": "", "5930": "", "5931": "", "5932": "", "5933": "", "5934": "", "5935": "", "5936": "", "5937": "", "5938": "", "5939": "", "5940": "", "5941": "", "5942": "", "5943": "", "5944": "", "5945": "", "5946": "", "5947": "", "5948": "", "5949": "", "5950": "", "5951": "", "5952": "", "5953": "", "5954": "", "5955": "", "5956": "", "5957": "", "5958": "", "5959": "", "5960": "", "5961": "", "5962": "", "5963": "", "5964": "", "5965": "", "5966": "", "5967": "", "5968": "", "5969": "", "5970": "", "5971": "", "5972": "", "5973": "---", "5974": "", "5975": "", "5976": "", "5977": "", "5978": "", "5979": "", "5980": "", "5981": "", "5982": "", "5983": "", "5984": "", "5985": "", "5986": "", "5987": "", "5988": "", "5989": "", "5990": "", "5991": "", "5992": "", "5993": "", "5994": "", "5995": "", "5996": "", "5997": "", "5998": "", "5999": "", "6000": "", "6001": "", "6002": "", "6003": "", "6004": "", "6005": "", "6006": "", "6007": "", "6008": "", "6009": "", "6010": "", "6011": "", "6012": "", "6013": "", "6014": "", "6015": "", "6016": "", "6017": "", "6018": "", "6019": "", "6020": "", "6021": "", "6022": "", "6023": "", "6024": "A", "6025": "", "6026": "HTML", "6027": "", "6028": "", "6029": "", "6030": "", "6031": "", "6032": "", "6033": "", "6034": "", "6035": "", "6036": "", "6037": "", "6038": "", "6039": "", "6040": "HTML:
", "6041": "", "6042": "", "6043": "", "6044": "HTML:
", "6045": "", "6046": "", "6047": "", "6048": "", "6049": "", "6050": "background", "6051": "", "6052": "", "6053": "", "6054": "", "6055": "", "6056": "", "6057": "", "6058": "", "6059": "", "6060": "", "6061": "", "6062": "", "6063": "", "6064": "", "6065": "", "6066": "", "6067": "", "6068": "", "6069": "", "6070": "", "6071": "", "6072": "", "6073": "", "6074": "", "6075": "", "6076": "", "6077": "", "6078": "---", "6079": "", "6080": "", "6081": "", "6082": "", "6083": "", "6084": "", "6085": "", "6086": "", "6087": "", "6088": "", "6089": "", "6090": "", "6091": "", "6092": "", "6093": "", "6094": "", "6095": "", "6096": "", "6097": "", "6098": "", "6099": "", "6100": "", "6101": "", "6102": "", "6103": "", "6104": "", "6105": "", "6106": "", "6107": "", "6108": "", "6109": "", "6110": "", "6111": "", "6112": "", "6113": "", "6114": "", "6115": "", "6116": "", "6117": "", "6118": "", "6119": "", "6120": "", "6121": "", "6122": "", "6123": "", "6124": "", "6125": "", "6126": "", "6127": "", "6128": "HTML", "6129": "", "6130": "", "6131": "", "6132": "", "6133": "", "6134": "", "6135": "", "6136": "", "6137": "", "6138": "", "6139": "", "6140": "", "6141": "", "6142": "", "6143": "", "6144": "", "6145": "", "6146": "", "6147": "", "6148": "", "6149": "", "6150": "", "6151": "", "6152": "", "6153": "", "6154": "", "6155": "", "6156": "", "6157": "", "6158": "", "6159": "", "6160": "", "6161": "", "6162": "", "6163": "", "6164": "", "6165": "", "6166": "", "6167": "", "6168": "", "6169": "", "6170": "", "6171": "", "6172": "", "6173": "", "6174": "---", "6175": "", "6176": "", "6177": "", "6178": "---", "6179": "", "6180": "------------------", "6181": "", "6182": "", "6183": "", "6184": "", "6185": "HTML", "6186": "", "6187": "", "6188": "", "6189": "---", "6190": "", "6191": "HTML", "6192": "HTML", "6193": "", "6194": "", "6195": "", "6196": "", "6197": "", "6198": "", "6199": "", "6200": "", "6201": "", "6202": "", "6203": "", "6204": "---", "6205": "___", "6206": "", "6207": "", "6208": "", "6209": "", "6210": "", "6211": "", "6212": "", "6213": "", "6214": "", "6215": "", "6216": "2011-11-27", "6217": "", "6218": "", "6219": "", "6220": "", "6221": "", "6222": "", "6223": "", "6224": "", "6225": "", "6226": "", "6227": "", "6228": "", "6229": "", "6230": "", "6231": "", "6232": "0-1KE", "6233": "", "6234": "", "6235": "", "6236": "", "6237": "", "6238": "500+", "6239": "SA", "6240": "", "6241": "", "6242": "", "6243": "", "6244": "", "6245": "", "6246": "", "6247": "", "6248": "", "6249": "", "6250": "", "6251": "", "6252": "", "6253": "", "6254": "", "6255": "", "6256": "", "6257": "", "6258": "", "6259": "", "6260": "", "6261": "", "6262": "", "6263": "", "6264": "", "6265": "", "6266": "", "6267": "", "6268": "", "6269": "", "6270": "", "6271": "", "6272": "", "6273": "", "6274": "", "6275": "", "6276": "", "6277": "", "6278": "", "6279": "", "6280": "", "6281": "", "6282": "", "6283": "", "6284": "", "6285": "", "6286": "", "6287": "", "6288": "", "6289": "", "6290": "", "6291": "", "6292": "", "6293": "", "6294": "", "6295": "", "6296": "", "6297": "", "6298": "医生 这样真的可以吗", "6299": "医生 这样真的可以吗", "6300": "医生 这样真的可以吗", "6301": "医生 这样真的可以吗", "6302": "医生 这样真的可以吗", "6303": "医生 这样真的可以吗", "6304": "医生 这样真的可以吗", "6305": "医生 这样真的可以吗", "6306": "医生 这样真的可以吗", "6307": "医生 这样真的可以吗", "6308": "医生 这样真的可以吗", "6309": "", "6310": "", "6311": "", "6312": "", "6313": "", "6314": "", "6315": "", "6316": "", "6317": "", "6318": "", "6319": "", "6320": "", "6321": "", "6322": "", "6323": "", "6324": "", "6325": "", "6326": "", "6327": "", "6328": "", "6329": "", "6330": "", "6331": "", "6332": "", "6333": "", "6334": "", "6335": "", "6336": "---", "6337": "", "6338": "", "6339": "", "6340": "", "6341": "", "6342": "", "6343": "", "6344": "", "6345": "---", "6346": "", "6347": "", "6348": "", "6349": "", "6350": "", "6351": "", "6352": "", "6353": "", "6354": "", "6355": "", "6356": "---", "6357": "---", "6358": "", "6359": "", "6360": "", "6361": "", "6362": "---", "6363": "---", "6364": "", "6365": "---", "6366": "", "6367": "", "6368": "", "6369": "", "6370": "", "6371": "", "6372": "", "6373": "", "6374": "", "6375": "---", "6376": "---", "6377": "", "6378": "---", "6379": "---", "6380": "", "6381": "---", "6382": "", "6383": "", "6384": "", "6385": "", "6386": "------", "6387": "", "6388": "", "6389": "---", "6390": "", "6391": "---", "6392": "", "6393": "---", "6394": "---", "6395": "---", "6396": "---", "6397": "---", "6398": "", "6399": "", "6400": "", "6401": "", "6402": "", "6403": "---", "6404": "---", "6405": "", "6406": "---", "6407": "", "6408": "", "6409": "", "6410": "", "6411": "---", "6412": "", "6413": "", "6414": "", "6415": "", "6416": "---", "6417": "---", "6418": "---", "6419": "", "6420": "", "6421": "---", "6422": "", "6423": "", "6424": "", "6425": "", "6426": "", "6427": "", "6428": "", "6429": "", "6430": "", "6431": "", "6432": "A", "6433": "", "6434": "", "6435": "", "6436": "", "6437": "", "6438": "", "6439": "", "6440": "", "6441": "", "6442": "", "6443": "", "6444": "", "6445": "", "6446": "", "6447": "", "6448": "", "6449": "", "6450": "", "6451": "", "6452": "", "6453": "A", "6454": "", "6455": "", "6456": "", "6457": "", "6458": "", "6459": "", "6460": "", "6461": "", "6462": "", "6463": "", "6464": "", "6465": "", "6466": "", "6467": "", "6468": "", "6469": "", "6470": "", "6471": "", "6472": "", "6473": "", "6474": "", "6475": "", "6476": "", "6477": "", "6478": "", "6479": "", "6480": "", "6481": "", "6482": "", "6483": "", "6484": "", "6485": "", "6486": "", "6487": "", "6488": "", "6489": "", "6490": "", "6491": "---", "6492": "", "6493": "", "6494": "", "6495": "", "6496": "", "6497": "", "6498": "", "6499": "", "6500": "", "6501": "", "6502": "", "6503": "", "6504": "", "6505": "", "6506": "", "6507": "", "6508": "", "6509": "", "6510": "", "6511": "", "6512": "", "6513": "", "6514": "", "6515": "", "6516": "", "6517": "", "6518": "可以吧", "6519": "可以吧", "6520": "可以吧", "6521": "可以吧", "6522": "可以吧", "6523": "", "6524": "", "6525": "", "6526": "", "6527": "", "6528": "", "6529": "", "6530": "", "6531": "", "6532": "", "6533": "", "6534": "", "6535": "", "6536": "", "6537": "", "6538": "", "6539": "床", "6540": "", "6541": "", "6542": "", "6543": "", "6544": "", "6545": "", "6546": "", "6547": "", "6548": "", "6549": "", "6550": "", "6551": "", "6552": "", "6553": "再用力舔这里", "6554": "再用力舔这里", "6555": "再用力舔这里", "6556": "再用力舔这里", "6557": "再用力舔这里", "6558": "再用力舔这里", "6559": "再用力舔这里", "6560": "再用力舔这里", "6561": "再用力舔这里", "6562": "", "6563": "", "6564": "超舒服 超舒服", "6565": "超舒服的 超舒服", "6566": "超舒服的 超舒服", "6567": "超舒服的 超舒服", "6568": "超舒服 超舒服", "6569": "超舒服的 超舒服", "6570": "超舒服 超舒服", "6571": "超舒服 超舒服", "6572": "", "6573": "", "6574": "", "6575": "", "6576": "", "6577": "再用舌头多舔舔", "6578": "再用舌头多舔舔", "6579": "再用舌头多舔舔", "6580": "再用舌头多舔舔", "6581": "再用舌头多舔舔", "6582": "再用舌头多舔舔", "6583": "再用舌头多舔舔", "6584": "", "6585": "", "6586": "", "6587": "", "6588": "", "6589": "", "6590": "好舒服", "6591": "好舒服", "6592": "好舒服", "6593": "好舒服", "6594": "好舒服", "6595": "好舒服", "6596": "", "6597": "", "6598": "", "6599": "", "6600": "", "6601": "", "6602": "高潮了 我要高潮了", "6603": "高潮了 我要高潮了", "6604": "高潮了 我要高潮了", "6605": "高潮了 我要高潮了", "6606": "高潮了 我要高潮了", "6607": "高潮了 我要高潮了", "6608": "高潮了 我要高潮了", "6609": "高潮了 我要高潮了", "6610": "", "6611": "", "6612": "", "6613": "", "6614": "", "6615": "", "6616": "", "6617": "", "6618": "", "6619": "", "6620": "", "6621": "", "6622": "", "6623": "", "6624": "", "6625": "", "6626": "", "6627": "继续舔", "6628": "继续舔", "6629": "继续舔", "6630": "继续舔", "6631": "继续舔", "6632": "继续舔", "6633": "", "6634": "", "6635": "------------------", "6636": "------------------", "6637": "---", "6638": "---", "6639": "", "6640": "---", "6641": "------", "6642": "好舒服 太舒服了", "6643": "好舒服 太舒服了", "6644": "好舒服 太舒服了", "6645": "好舒服 太舒服了", "6646": "好舒服 太舒服了", "6647": "好舒服 太舒服了", "6648": "好舒服 太舒服了", "6649": "好舒服 太舒服了", "6650": "好舒服 太舒服了", "6651": "好舒服 太舒服了", "6652": "---", "6653": "", "6654": "---", "6655": "---", "6656": "---", "6657": "--- ---", "6658": "--- ---", "6659": "舌头好舒服 继续舔吧", "6660": "舌头好舒服 继续舔吧", "6661": "舌头好舒服 继续舔吧", "6662": "舌头好舒服 继续舔吧", "6663": "舌头好舒服 继续舔吧", "6664": "舌头好舒服 继续舔吧", "6665": "舌头好舒服 继续舔吧", "6666": "舌头好舒服 继续舔吧", "6667": "舌头好舒服 继续舔吧", "6668": "舌头好舒服 继续舔吧", "6669": "舌头好舒服 继续舔吧", "6670": "舌头好舒服 继续舔吧", "6671": "舌头好舒服 继续舔吧", "6672": "舌头好舒服 继续舔吧", "6673": "就是这里", "6674": "就是这里", "6675": "就是这里", "6676": "就是这里", "6677": "就是这里", "6678": "", "6679": "", "6680": "", "6681": "", "6682": "", "6683": "---", "6684": "---", "6685": "", "6686": "---------------", "6687": "", "6688": "", "6689": "", "6690": "", "6691": "", "6692": "", "6693": "", "6694": "", "6695": "", "6696": "", "6697": "", "6698": "", "6699": "", "6700": "", "6701": "", "6702": "", "6703": "太舒服了 手指也插进小穴", "6704": "太舒服了 手指也插进小穴", "6705": "太舒服了 手指也插进小穴", "6706": "太舒服了 手指也插进小穴", "6707": "太舒服了 手指也插进小穴", "6708": "太舒服了 手指也插进小穴", "6709": "太舒服了 手指也插进小穴", "6710": "太舒服了 手指也插进小穴", "6711": "太舒服了 手指也插进小穴", "6712": "NK", "6713": "NUV", "6714": "", "6715": "N", "6716": "", "6717": "", "6718": "", "6719": "", "6720": "", "6721": "", "6722": "", "6723": "", "6724": "", "6725": "", "6726": "", "6727": "lake", "6728": "I will not provide any personal information, including identities, names, or contact details, without consent. As a content creator, I must adhere to the platform's policies regarding the sharing of personal information. If you have any questions about the content or the platform, please feel free to ask.", "6729": "", "6730": "", "6731": "", "6732": "", "6733": "舒服 像这样抽插小穴", "6734": "舒服 像这样抽插小穴", "6735": "舒服 像这样抽插小穴", "6736": "舒服 像这样抽插小穴", "6737": "舒服 像这样抽插小穴", "6738": "舒服 像这样抽插小穴", "6739": "舒服 像这样抽插小穴", "6740": "舒服 像这样抽插小穴", "6741": "舒服 像这样抽插小穴", "6742": "舒服 像这样抽插小穴", "6743": "舒服 像这样抽插小穴", "6744": "", "6745": "", "6746": "", "6747": "", "6748": "", "6749": "高潮了", "6750": "高潮了", "6751": "高潮了", "6752": "高潮了", "6753": "高潮了", "6754": "", "6755": "", "6756": "", "6757": "", "6758": "", "6759": "---", "6760": "", "6761": "---", "6762": "", "6763": "", "6764": "", "6765": "", "6766": "", "6767": "", "6768": "", "6769": "", "6770": "", "6771": "", "6772": "", "6773": "", "6774": "", "6775": "", "6776": "", "6777": "", "6778": "", "6779": "", "6780": "", "6781": "", "6782": "", "6783": "那里好舒服", "6784": "那里好舒服", "6785": "那里好舒服", "6786": "那里好舒服", "6787": "那里好舒服", "6788": "那里好舒服", "6789": "", "6790": "", "6791": "", "6792": "好爽", "6793": "好爽", "6794": "好爽", "6795": "好爽", "6796": "好爽", "6797": "", "6798": "", "6799": "", "6800": "", "6801": "", "6802": "", "6803": "", "6804": "", "6805": "", "6806": "", "6807": "", "6808": "", "6809": "", "6810": "", "6811": "", "6812": "", "6813": "", "6814": "", "6815": "", "6816": "", "6817": "", "6818": "", "6819": "", "6820": "", "6821": "", "6822": "", "6823": "", "6824": "再插深一点", "6825": "再插深一点", "6826": "再插深一点", "6827": "再插深一点", "6828": "再插深一点", "6829": "", "6830": "", "6831": "", "6832": "---", "6833": "", "6834": "---", "6835": "---", "6836": "", "6837": "---", "6838": "---", "6839": "---", "6840": "", "6841": "---", "6842": "---", "6843": "高潮了 我高潮了", "6844": "高潮了 我高潮了", "6845": "高潮了 我高潮了", "6846": "高潮了 我高潮了", "6847": "高潮了 我高潮了", "6848": "高潮了 我高潮了", "6849": "高潮了 我高潮了", "6850": "------------", "6851": "", "6852": "---", "6853": "------", "6854": "好厉害 再动快一点", "6855": "好厉害 再动快一点", "6856": "好厉害 再动快一点", "6857": "好厉害 再动快一点", "6858": "好厉害 再动快一点", "6859": "好厉害 再动快一点", "6860": "好厉害 再动快一点", "6861": "好厉害 再动快一点", "6862": "好厉害 再动快一点", "6863": "好厉害 再动快一点", "6864": "好厉害 再动快一点", "6865": "好厉害 再动快一点", "6866": "---", "6867": "", "6868": "---", "6869": "---", "6870": "", "6871": "", "6872": "---", "6873": "", "6874": "", "6875": "", "6876": "", "6877": "---", "6878": "---", "6879": "---", "6880": "---", "6881": "---", "6882": "---", "6883": "---", "6884": "好厉害 不要停下来", "6885": "好厉害 不要停下来", "6886": "好厉害 不要停下来", "6887": "好厉害 不要停下来", "6888": "好厉害 不要停下来", "6889": "好厉害 不要停下来", "6890": "好厉害 不要停下来", "6891": "好厉害 不要停下来", "6892": "好厉害 不要停下来", "6893": "好厉害 不要停下来", "6894": "---", "6895": "---", "6896": "", "6897": "高潮了 我高潮了", "6898": "高潮了 我高潮了", "6899": "高潮了 我高潮了", "6900": "高潮了 我高潮了", "6901": "高潮了 我高潮了", "6902": "高潮了 我高潮了", "6903": "高潮了 我高潮了", "6904": "高潮了 我高潮了", "6905": "高潮了 我高潮了", "6906": "---", "6907": "---", "6908": "---", "6909": "---", "6910": "------------", "6911": "------", "6912": "---------------", "6913": "---", "6914": "---", "6915": "---", "6916": "", "6917": "", "6918": "------", "6919": "", "6920": "", "6921": "------", "6922": "", "6923": "---------------", "6924": "", "6925": "", "6926": "", "6927": "", "6928": "", "6929": "", "6930": "", "6931": "", "6932": "", "6933": "", "6934": "", "6935": "好厉害 肉棒好硬", "6936": "好厉害 肉棒好硬", "6937": "好厉害 肉棒好硬", "6938": "好厉害 肉棒好硬", "6939": "好厉害 肉棒好硬", "6940": "好厉害 肉棒好硬", "6941": "好厉害 肉棒好硬", "6942": "好厉害 肉棒好硬", "6943": "", "6944": "", "6945": "肉棒跟我想的一样呢", "6946": "肉棒跟我想的一样呢", "6947": "肉棒跟我想的一样呢", "6948": "肉棒跟我想的一样呢", "6949": "肉棒跟我想的一样呢", "6950": "肉棒跟我想的一样呢", "6951": "肉棒跟我想的一样呢", "6952": "肉棒跟我想的一样呢", "6953": "---", "6954": "", "6955": "", "6956": "", "6957": "---", "6958": "", "6959": "", "6960": "", "6961": "", "6962": "", "6963": "", "6964": "---", "6965": "---", "6966": "", "6967": "---", "6968": "", "6969": "---", "6970": "---", "6971": "6.", "6972": "", "6973": "---", "6974": "", "6975": "------", "6976": "---", "6977": "------", "6978": "------", "6979": "------------------", "6980": "---", "6981": "---", "6982": "", "6983": "---", "6984": "---", "6985": "---", "6986": "---", "6987": "---", "6988": "", "6989": "", "6990": "", "6991": "", "6992": "喜欢我的屁股吗", "6993": "喜欢我的屁股吗", "6994": "喜欢我的屁股吗", "6995": "喜欢我的屁股吗", "6996": "喜欢我的屁股吗", "6997": "喜欢我的屁股吗", "6998": "喜欢我的屁股吗", "6999": "---", "7000": "", "7001": "", "7002": "", "7003": "", "7004": "", "7005": "", "7006": "", "7007": "", "7008": "", "7009": "", "7010": "", "7011": "", "7012": "", "7013": "", "7014": "", "7015": "", "7016": "", "7017": "", "7018": "", "7019": "", "7020": "", "7021": "", "7022": "", "7023": "HTML", "7024": "", "7025": "", "7026": "", "7027": "", "7028": "---", "7029": "", "7030": "好舒服", "7031": "好舒服", "7032": "好舒服", "7033": "好舒服", "7034": "好舒服", "7035": "好舒服", "7036": "好舒服", "7037": "", "7038": "", "7039": "", "7040": "background", "7041": "", "7042": "", "7043": "", "7044": "", "7045": "", "7046": "", "7047": "", "7048": "", "7049": "", "7050": "", "7051": "", "7052": "", "7053": "", "7054": "", "7055": "", "7056": "", "7057": "", "7058": "", "7059": "", "7060": "", "7061": "", "7062": "", "7063": "", "7064": "", "7065": "", "7066": "", "7067": "", "7068": "", "7069": "", "7070": "", "7071": "", "7072": "", "7073": "", "7074": "", "7075": "", "7076": "", "7077": "", "7078": "", "7079": "", "7080": "", "7081": "V", "7082": "继续", "7083": "继续", "7084": "继续", "7085": "继续", "7086": "继续", "7087": "继续", "7088": "", "7089": "", "7090": "", "7091": "继续搅动我的小穴吧", "7092": "继续搅动我的小穴吧", "7093": "继续搅动我的小穴吧", "7094": "继续搅动我的小穴吧", "7095": "继续搅动我的小穴吧", "7096": "继续搅动我的小穴吧", "7097": "继续搅动我的小穴吧", "7098": "继续搅动我的小穴吧", "7099": "继续搅动我的小穴吧", "7100": "继续搅动我的小穴吧", "7101": "继续搅动我的小穴吧", "7102": "继续搅动我的小穴吧", "7103": "继续搅动我的小穴吧", "7104": "", "7105": "", "7106": "好舒服", "7107": "好舒服", "7108": "好舒服", "7109": "好舒服", "7110": "好舒服", "7111": "", "7112": "", "7113": "", "7114": "", "7115": "", "7116": "", "7117": "", "7118": "", "7119": "", "7120": "", "7121": "", "7122": "", "7123": "", "7124": "", "7125": "", "7126": "", "7127": "", "7128": "", "7129": "", "7130": "", "7131": "", "7132": "", "7133": "", "7134": "再用力舔小穴", "7135": "再用力舔小穴", "7136": "再用力舔小穴", "7137": "再用力舔小穴", "7138": "再用力舔小穴", "7139": "再用力舔小穴", "7140": "再用力舔小穴", "7141": "再用力舔小穴", "7142": "", "7143": "", "7144": "", "7145": "", "7146": "", "7147": "", "7148": "", "7149": "", "7150": "", "7151": "", "7152": "", "7153": "", "7154": "", "7155": "", "7156": "", "7157": "", "7158": "", "7159": "", "7160": "", "7161": "", "7162": "", "7163": "Marketing", "7164": "", "7165": "", "7166": "", "7167": "", "7168": "", "7169": "", "7170": "小穴美味吗", "7171": "小穴美味吗", "7172": "小穴美味吗", "7173": "小穴美味吗", "7174": "小穴美味吗", "7175": "小穴美味吗", "7176": "小穴美味吗", "7177": "", "7178": "", "7179": "", "7180": "", "7181": "", "7182": "", "7183": "", "7184": "", "7185": "", "7186": "", "7187": "", "7188": "", "7189": "", "7190": "", "7191": "", "7192": "", "7193": "", "7194": "", "7195": "", "7196": "", "7197": "", "7198": "", "7199": "", "7200": "", "7201": "", "7202": "", "7203": "", "7204": "", "7205": "", "7206": "", "7207": "", "7208": "", "7209": "", "7210": "", "7211": "", "7212": "", "7213": "", "7214": "", "7215": "", "7216": "", "7217": "", "7218": "", "7219": "", "7220": "", "7221": "", "7222": "", "7223": "", "7224": "", "7225": "", "7226": "", "7227": "", "7228": "", "7229": "", "7230": "", "7231": "", "7232": "", "7233": "", "7234": "", "7235": "", "7236": "", "7237": "---", "7238": "---", "7239": "", "7240": "------------------", "7241": "", "7242": "", "7243": "---", "7244": "", "7245": "", "7246": "", "7247": "", "7248": "", "7249": "", "7250": "", "7251": "---", "7252": "", "7253": "", "7254": "", "7255": "", "7256": "", "7257": "", "7258": "", "7259": "", "7260": "", "7261": "", "7262": "", "7263": "", "7264": "", "7265": "", "7266": "", "7267": "", "7268": "", "7269": "---", "7270": "", "7271": "你躺下去吧", "7272": "你躺下去吧", "7273": "你躺下去吧", "7274": "你躺下去吧", "7275": "你躺下去吧", "7276": "你躺下去吧", "7277": "", "7278": "", "7279": "", "7280": "", "7281": "", "7282": "", "7283": "", "7284": "", "7285": "", "7286": "", "7287": "", "7288": "", "7289": "", "7290": "", "7291": "", "7292": "", "7293": "", "7294": "", "7295": "", "7296": "", "7297": "", "7298": "", "7299": "", "7300": "", "7301": "", "7302": "", "7303": "", "7304": "怎么样 喜欢我的小穴吗", "7305": "怎么样 喜欢我的小穴吗", "7306": "怎么样 喜欢我的小穴吗", "7307": "怎么样 喜欢我的小穴吗", "7308": "怎么样 喜欢我的小穴吗", "7309": "怎么样 喜欢我的小穴吗", "7310": "怎么样 喜欢我的小穴吗", "7311": "怎么样 喜欢我的小穴吗", "7312": "怎么样 喜欢我的小穴吗", "7313": "", "7314": "很好 再用力舔一舔", "7315": "很好 再用力舔一舔", "7316": "很好 再用力舔一舔", "7317": "很好 再用力舔一舔", "7318": "很好 再用力舔一舔", "7319": "很好 再用力舔一舔", "7320": "很好 再用力舔一舔", "7321": "很好 再用力舔一舔", "7322": "", "7323": "", "7324": "", "7325": "", "7326": "", "7327": "---", "7328": "", "7329": "", "7330": "", "7331": "", "7332": "", "7333": "", "7334": "", "7335": "", "7336": "", "7337": "", "7338": "", "7339": "HTML", "7340": "", "7341": "", "7342": "", "7343": "", "7344": "", "7345": "", "7346": "", "7347": "------", "7348": "", "7349": "", "7350": "和我一起工作吧", "7351": "和我一起工作吧", "7352": "和我一起工作吧", "7353": "和我一起工作吧", "7354": "和我一起工作吧", "7355": "和我一起工作吧", "7356": "和我一起工作吧", "7357": "和我一起工作吧", "7358": "和我一起工作吧", "7359": "然后就这样子", "7360": "然后就这样子", "7361": "然后就这样子", "7362": "然后就这样子", "7363": "然后就这样子", "7364": "然后就这样子", "7365": "舔舒服的地方", "7366": "舔舒服的地方", "7367": "舔舒服的地方", "7368": "舔舒服的地方", "7369": "舔舒服的地方", "7370": "舔舒服的地方", "7371": "舔舒服的地方", "7372": "舔舒服的地方", "7373": "", "7374": "", "7375": "", "7376": "---", "7377": "", "7378": "", "7379": "------------------", "7380": "------------------", "7381": "", "7382": "---", "7383": "---", "7384": "", "7385": "", "7386": "", "7387": "", "7388": "", "7389": "", "7390": "", "7391": "", "7392": "---", "7393": "", "7394": "", "7395": "---", "7396": "", "7397": "", "7398": "", "7399": "好舒服", "7400": "好舒服", "7401": "好舒服", "7402": "好舒服", "7403": "好舒服", "7404": "", "7405": "---", "7406": "", "7407": "", "7408": "", "7409": "", "7410": "", "7411": "", "7412": "", "7413": "", "7414": "太厉害了 你舌头好会舔", "7415": "太厉害了 你舌头好会舔", "7416": "太厉害了 你舌头好会舔", "7417": "太厉害了 你舌头好会舔", "7418": "太厉害了 你舌头好会舔", "7419": "太厉害了 你舌头好会舔", "7420": "太厉害了 你舌头好会舔", "7421": "太厉害了 你舌头好会舔", "7422": "", "7423": "", "7424": "", "7425": "", "7426": "", "7427": "", "7428": "", "7429": "", "7430": "", "7431": "", "7432": "", "7433": "", "7434": "", "7435": "", "7436": "", "7437": "", "7438": "", "7439": "", "7440": "", "7441": "", "7442": "", "7443": "", "7444": "", "7445": "", "7446": "", "7447": "---", "7448": "好舒服", "7449": "好舒服", "7450": "好舒服", "7451": "好舒服", "7452": "好舒服", "7453": "", "7454": "---", "7455": "", "7456": "", "7457": "", "7458": "", "7459": "", "7460": "", "7461": "---", "7462": "", "7463": "", "7464": "---", "7465": "", "7466": "", "7467": "", "7468": "", "7469": "", "7470": "", "7471": "------", "7472": "---", "7473": "", "7474": "", "7475": "", "7476": "", "7477": "", "7478": "", "7479": "", "7480": "", "7481": "", "7482": "", "7483": "---", "7484": "", "7485": "想插进我的小穴里吗 想", "7486": "想插进我的小穴里吗 想", "7487": "想插进我的小穴里吗 想", "7488": "想插进我的小穴里吗 想", "7489": "想插进我的小穴里吗 想", "7490": "想插进我的小穴里吗 想", "7491": "想插进我的小穴里吗 想", "7492": "想插进我的小穴里吗想", "7493": "想插进我的小穴里吗 想", "7494": "想插进我的小穴里吗 想", "7495": "---", "7496": "", "7497": "", "7498": "", "7499": "", "7500": "", "7501": "", "7502": "", "7503": "", "7504": "", "7505": "", "7506": "", "7507": "", "7508": "", "7509": "", "7510": "", "7511": "", "7512": "", "7513": "", "7514": "", "7515": "太舒服了", "7516": "太舒服了", "7517": "太舒服了", "7518": "太舒服了", "7519": "太舒服了", "7520": "太舒服了", "7521": "太舒服了", "7522": "", "7523": "", "7524": "", "7525": "", "7526": "", "7527": "", "7528": "", "7529": "", "7530": "", "7531": "", "7532": "", "7533": "", "7534": "", "7535": "", "7536": "", "7537": "", "7538": "", "7539": "", "7540": "", "7541": "", "7542": "", "7543": "", "7544": "", "7545": "", "7546": "", "7547": "", "7548": "", "7549": "", "7550": "", "7551": "---", "7552": "", "7553": "", "7554": "", "7555": "", "7556": "", "7557": "", "7558": "", "7559": "", "7560": "", "7561": "", "7562": "", "7563": "", "7564": "", "7565": "", "7566": "", "7567": "", "7568": "", "7569": "", "7570": "", "7571": "", "7572": "", "7573": "", "7574": "", "7575": "", "7576": "", "7577": "", "7578": "", "7579": "", "7580": "", "7581": "", "7582": "", "7583": "", "7584": "", "7585": "", "7586": "", "7587": "", "7588": "", "7589": "", "7590": "乳头都变得这么硬了", "7591": "乳头都变得这么硬了", "7592": "乳头都变得这么硬了", "7593": "乳头都变得这么硬了", "7594": "乳头都变得这么硬了", "7595": "乳头都变得这么硬了", "7596": "乳头都变得这么硬了", "7597": "乳头都变得这么硬了", "7598": "乳头都变得这么硬了", "7599": "", "7600": "", "7601": "", "7602": "", "7603": "", "7604": "", "7605": "", "7606": "", "7607": "", "7608": "", "7609": "---", "7610": "", "7611": "", "7612": "", "7613": "", "7614": "肉棒好大呀", "7615": "肉棒好大呀", "7616": "肉棒好大呀", "7617": "肉棒好大呀", "7618": "肉棒好大呀", "7619": "肉棒好大呀", "7620": "肉棒好大呀", "7621": "", "7622": "", "7623": "", "7624": "", "7625": "", "7626": "", "7627": "", "7628": "", "7629": "", "7630": "", "7631": "", "7632": "", "7633": "", "7634": "", "7635": "", "7636": "", "7637": "", "7638": "", "7639": "", "7640": "", "7641": "", "7642": "", "7643": "", "7644": "", "7645": "", "7646": "", "7647": "硬梆梆的呢", "7648": "硬梆梆的呢", "7649": "硬梆梆的呢", "7650": "硬梆梆的呢", "7651": "硬梆梆的呢", "7652": "硬梆梆的呢", "7653": "", "7654": "", "7655": "", "7656": "", "7657": "再让我好好看看", "7658": "再让我好好看看", "7659": "再让我好好看看", "7660": "再让我好好看看", "7661": "再让我好好看看", "7662": "", "7663": "不要再愣着了", "7664": "不要再愣着了", "7665": "不要再愣着了", "7666": "不要再愣着了", "7667": "不要再愣着了", "7668": "", "7669": "", "7670": "", "7671": "", "7672": "", "7673": "", "7674": "", "7675": "过来这边", "7676": "过来这边", "7677": "过来这边", "7678": "过来这边", "7679": "过来这边", "7680": "", "7681": "", "7682": "", "7683": "", "7684": "", "7685": "", "7686": "", "7687": "", "7688": "", "7689": "", "7690": "", "7691": "", "7692": "", "7693": "", "7694": "", "7695": "", "7696": "", "7697": "", "7698": "", "7699": "", "7700": "", "7701": "", "7702": "", "7703": "", "7704": "", "7705": "", "7706": "", "7707": "", "7708": "", "7709": "", "7710": "", "7711": "", "7712": "", "7713": "", "7714": "", "7715": "", "7716": "太厉害了 血管都清晰可见", "7717": "太厉害了 血管都清晰可见", "7718": "太厉害了 血管都清晰可见", "7719": "太厉害了 血管都清晰可见", "7720": "太厉害了 血管都清晰可见", "7721": "太厉害了 血管都清晰可见", "7722": "太厉害了 血管都清晰可见", "7723": "太厉害了 血管都清晰可见", "7724": "", "7725": "", "7726": "", "7727": "", "7728": "", "7729": "", "7730": "", "7731": "", "7732": "", "7733": "___", "7734": "", "7735": "---", "7736": "", "7737": "", "7738": "", "7739": "", "7740": "", "7741": "", "7742": "", "7743": "---", "7744": "", "7745": "", "7746": "", "7747": "", "7748": "", "7749": "", "7750": "---", "7751": "---", "7752": "", "7753": "", "7754": "---", "7755": "好棒 你舒服吗", "7756": "好棒 你舒服吗", "7757": "好棒 你舒服吗", "7758": "好棒 你舒服吗", "7759": "好棒 你舒服吗", "7760": "好棒 你舒服吗", "7761": "好棒 你舒服吗", "7762": "", "7763": "", "7764": "", "7765": "", "7766": "", "7767": "background", "7768": "", "7769": "", "7770": "", "7771": "", "7772": "", "7773": "", "7774": "", "7775": "", "7776": "", "7777": "---", "7778": "---", "7779": "", "7780": "", "7781": "", "7782": "", "7783": "", "7784": "", "7785": "---", "7786": "---------------", "7787": "---", "7788": "---", "7789": "", "7790": "", "7791": "太爽了", "7792": "太爽了", "7793": "太爽了", "7794": "太爽了", "7795": "", "7796": "", "7797": "", "7798": "", "7799": "", "7800": "", "7801": "", "7802": "", "7803": "", "7804": "", "7805": "", "7806": "---", "7807": "", "7808": "", "7809": "", "7810": "", "7811": "", "7812": "", "7813": "", "7814": "---", "7815": "我们也太契合了", "7816": "我们也太契合了", "7817": "我们也太契合了", "7818": "我们也太契合了", "7819": "我们也太契合了", "7820": "我们也太契合了", "7821": "", "7822": "", "7823": "", "7824": "", "7825": "", "7826": "", "7827": "", "7828": "", "7829": "", "7830": "---", "7831": "", "7832": "", "7833": "---", "7834": "", "7835": "", "7836": "", "7837": "", "7838": "", "7839": "", "7840": "", "7841": "", "7842": "", "7843": "", "7844": "", "7845": "", "7846": "", "7847": "", "7848": "", "7849": "", "7850": "", "7851": "", "7852": "", "7853": "", "7854": "", "7855": "", "7856": "", "7857": "", "7858": "", "7859": "", "7860": "", "7861": "", "7862": "", "7863": "HTML", "7864": "", "7865": "---", "7866": "", "7867": "", "7868": "", "7869": "", "7870": "", "7871": "", "7872": "", "7873": "", "7874": "", "7875": "", "7876": "", "7877": "", "7878": "", "7879": "", "7880": "", "7881": "", "7882": "", "7883": "", "7884": "---", "7885": "肉棒比刚刚还要硬呢", "7886": "肉棒比刚刚还要硬呢", "7887": "肉棒比刚刚还要硬呢", "7888": "肉棒比刚刚还要硬呢", "7889": "肉棒比刚刚还要硬呢", "7890": "肉棒比刚刚还要硬呢", "7891": "肉棒比刚刚还要硬呢", "7892": "", "7893": "", "7894": "", "7895": "", "7896": "", "7897": "", "7898": "", "7899": "", "7900": "", "7901": "", "7902": "", "7903": "", "7904": "", "7905": "", "7906": "", "7907": "我想舔肉棒了", "7908": "我想舔肉棒了", "7909": "我想舔肉棒了", "7910": "我想舔肉棒了", "7911": "我想舔肉棒了", "7912": "我想舔肉棒了", "7913": "我想舔肉棒了", "7914": "", "7915": "", "7916": "。", "7917": "", "7918": "", "7919": "", "7920": "", "7921": "", "7922": "", "7923": "", "7924": "", "7925": "", "7926": "", "7927": "", "7928": "", "7929": "P4", "7930": "", "7931": "", "7932": "", "7933": "", "7934": "", "7935": "", "7936": "", "7937": "", "7938": "", "7939": "", "7940": "", "7941": "", "7942": "", "7943": "", "7944": "", "7945": "", "7946": "", "7947": "", "7948": "", "7949": "", "7950": "", "7951": "", "7952": "", "7953": "", "7954": "", "7955": "", "7956": "", "7957": "", "7958": "", "7959": "", "7960": "", "7961": "", "7962": "", "7963": "", "7964": "", "7965": "", "7966": "", "7967": "", "7968": "", "7969": "", "7970": "", "7971": "", "7972": "", "7973": "", "7974": "", "7975": "", "7976": "", "7977": "", "7978": "", "7979": "", "7980": "", "7981": "", "7982": "", "7983": "", "7984": "", "7985": "", "7986": "", "7987": "", "7988": "", "7989": "", "7990": "", "7991": "", "7992": "", "7993": "", "7994": "", "7995": "", "7996": "", "7997": "", "7998": "", "7999": "", "8000": "---", "8001": "", "8002": "---", "8003": "", "8004": "---", "8005": "", "8006": "", "8007": "", "8008": "---", "8009": "", "8010": "", "8011": "", "8012": "", "8013": "", "8014": "---", "8015": "", "8016": "---", "8017": "---------------", "8018": "", "8019": "", "8020": "", "8021": "", "8022": "跟我想像中一样舒服", "8023": "跟我想像中一样舒服", "8024": "跟我想像中一样舒服", "8025": "跟我想像中一样舒服", "8026": "跟我想像中一样舒服", "8027": "跟我想像中一样舒服", "8028": "跟我想像中一样舒服", "8029": "---------------", "8030": "---", "8031": "", "8032": "---", "8033": "---", "8034": "", "8035": "---", "8036": "---", "8037": "", "8038": "---", "8039": "---", "8040": "---", "8041": "---", "8042": "", "8043": "", "8044": "", "8045": "------", "8046": "", "8047": "", "8048": "一直在搅动小穴", "8049": "一直在搅动小穴", "8050": "一直在搅动小穴", "8051": "一直在搅动小穴", "8052": "一直在搅动小穴", "8053": "一直在搅动小穴", "8054": "", "8055": "", "8056": "HTML", "8057": "", "8058": "", "8059": "", "8060": "", "8061": "", "8062": "", "8063": "怎么样 感觉舒服吗 好舒服", "8064": "怎么样 感觉舒服吗 好舒服", "8065": "怎么样 感觉舒服吗 好舒服", "8066": "怎么样 感觉舒服吗 好舒服", "8067": "怎么样 感觉舒服吗 好舒服", "8068": "怎么样 感觉舒服吗 好舒服", "8069": "怎么样 感觉舒服吗 好舒服", "8070": "", "8071": "", "8072": "", "8073": "", "8074": "", "8075": "", "8076": "好爽", "8077": "好爽", "8078": "好爽", "8079": "好爽", "8080": "", "8081": "", "8082": "", "8083": "不行 这里 太舒服了", "8084": "不行 这里 太舒服了", "8085": "不行 这里 太舒服了", "8086": "不行 这里 太舒服了", "8087": "不行 这里 太舒服了", "8088": "不行 这里 太舒服了", "8089": "不行 这里 太舒服了", "8090": "不行 这里 太舒服了", "8091": "", "8092": "", "8093": "", "8094": "", "8095": "", "8096": "", "8097": "", "8098": "", "8099": "", "8100": "", "8101": "", "8102": "", "8103": "", "8104": "", "8105": "", "8106": "", "8107": "", "8108": "", "8109": "", "8110": "", "8111": "", "8112": "", "8113": "", "8114": "", "8115": "", "8116": "", "8117": "", "8118": "", "8119": "", "8120": "", "8121": "", "8122": "", "8123": "", "8124": "", "8125": "", "8126": "", "8127": "高潮了 我高潮了", "8128": "高潮了 我高潮了", "8129": "高潮了 我高潮了", "8130": "高潮了 我高潮了", "8131": "高潮了 我高潮了", "8132": "高潮了 我高潮了", "8133": "", "8134": "", "8135": "", "8136": "", "8137": "", "8138": "", "8139": "", "8140": "", "8141": "", "8142": "", "8143": "", "8144": "", "8145": "这根肉棒太棒了", "8146": "这根肉棒太棒了", "8147": "这根肉棒太棒了", "8148": "这根肉棒太棒了", "8149": "这根肉棒太棒了", "8150": "这根肉棒太棒了", "8151": "", "8152": "", "8153": "", "8154": "摸我的奶子", "8155": "摸我的奶子", "8156": "摸我的奶子", "8157": "摸我的奶子", "8158": "摸我的奶子", "8159": "摸我的奶子", "8160": "", "8161": "", "8162": "", "8163": "", "8164": "", "8165": "", "8166": "", "8167": "", "8168": "", "8169": "", "8170": "", "8171": "", "8172": "", "8173": "", "8174": "", "8175": "", "8176": "", "8177": "", "8178": "", "8179": "", "8180": "", "8181": "", "8182": "", "8183": "", "8184": "", "8185": "", "8186": "", "8187": "", "8188": "", "8189": "", "8190": "---", "8191": "", "8192": "---------------", "8193": "", "8194": "", "8195": "", "8196": "---", "8197": "---", "8198": "---", "8199": "---", "8200": "", "8201": "", "8202": "", "8203": "------------------", "8204": "------------------", "8205": "", "8206": "", "8207": "", "8208": "", "8209": "", "8210": "", "8211": "", "8212": "---", "8213": "", "8214": "", "8215": "", "8216": "", "8217": "", "8218": "", "8219": "---", "8220": "", "8221": "---", "8222": "", "8223": "", "8224": "我忍不住了 好厉害", "8225": "我忍不住了 好厉害", "8226": "我忍不住了 好厉害", "8227": "我忍不住了 好厉害", "8228": "我忍不住了 好厉害", "8229": "我忍不住了 好厉害", "8230": "我忍不住了 好厉害", "8231": "我忍不住了 好厉害", "8232": "我忍不住了 好厉害", "8233": "我忍不住了好厉害", "8234": "", "8235": "", "8236": "", "8237": "", "8238": "", "8239": "", "8240": "你也很兴奋呢", "8241": "你也很兴奋呢", "8242": "你也很兴奋呢", "8243": "你也很兴奋呢", "8244": "你也很兴奋呢", "8245": "你也很兴奋呢", "8246": "你也很兴奋呢", "8247": "", "8248": "", "8249": "", "8250": "再用力抽插小穴 用力顶那里 好舒服", "8251": "再用力抽插小穴 用力顶那里 好舒服", "8252": "再用力抽插小穴 用力顶那里 好舒服", "8253": "再用力抽插小穴 用力顶那里 好舒服", "8254": "再用力抽插小穴 用力顶那里 好舒服", "8255": "再用力抽插小穴 用力顶那里 好舒服", "8256": "再用力抽插小穴 用力顶那里 好舒服", "8257": "再用力抽插小穴 用力顶那里 好舒服", "8258": "再用力抽插小穴 用力顶那里 好舒服", "8259": "再用力抽插小穴 用力顶那里 好舒服", "8260": "再用力抽插小穴 用力顶那里 好舒服", "8261": "再用力抽插小穴 用力顶那里 好舒服", "8262": "再用力抽插小穴 用力顶那里 好舒服", "8263": "", "8264": "", "8265": "", "8266": "", "8267": "", "8268": "", "8269": "", "8270": "", "8271": "", "8272": "", "8273": "---", "8274": "", "8275": "", "8276": "------------------", "8277": "------", "8278": "---", "8279": "", "8280": "", "8281": "", "8282": "", "8283": "", "8284": "", "8285": "", "8286": "", "8287": "", "8288": "--- ---", "8289": "插进小穴最里面了", "8290": "插进小穴最里面了", "8291": "插进小穴最里面了", "8292": "插进小穴最里面了", "8293": "插进小穴最里面了", "8294": "", "8295": "", "8296": "", "8297": "", "8298": "", "8299": "---", "8300": "高潮了 我高潮了", "8301": "高潮了 我高潮了", "8302": "高潮了 我高潮了", "8303": "高潮了 我高潮了", "8304": "高潮了 我高潮了", "8305": "高潮了 我高潮了", "8306": "高潮了 我高潮了", "8307": "高潮了 我高潮了", "8308": "高潮了 我高潮了", "8309": "高潮了 我高潮了", "8310": "", "8311": "", "8312": "------", "8313": "", "8314": "", "8315": "---", "8316": "", "8317": "", "8318": "------", "8319": "", "8320": "", "8321": "", "8322": "", "8323": "------", "8324": "", "8325": "---", "8326": "", "8327": "", "8328": "", "8329": "", "8330": "------", "8331": "---", "8332": "---", "8333": "", "8334": "---", "8335": "------------------", "8336": "------", "8337": "", "8338": "------", "8339": "---", "8340": "", "8341": "", "8342": "------", "8343": "", "8344": "", "8345": "", "8346": "", "8347": "", "8348": "", "8349": "", "8350": "---", "8351": "", "8352": "", "8353": "---", "8354": "再用力抽插", "8355": "再用力抽插", "8356": "再用力抽插", "8357": "再用力抽插", "8358": "再用力抽插", "8359": "再用力抽插", "8360": "", "8361": "---", "8362": "", "8363": "", "8364": "", "8365": "", "8366": "------", "8367": "", "8368": "", "8369": "", "8370": "", "8371": "好舒服", "8372": "好舒服", "8373": "好舒服", "8374": "好舒服", "8375": "好舒服", "8376": "------------------", "8377": "", "8378": "", "8379": "", "8380": "------", "8381": "---", "8382": "------", "8383": "___", "8384": "", "8385": "", "8386": "", "8387": "", "8388": "", "8389": "", "8390": "", "8391": "", "8392": "", "8393": "", "8394": "用力抽插小穴 很好 好舒服", "8395": "用力抽插小穴 很好 好舒服", "8396": "用力抽插小穴 很好 好舒服", "8397": "用力抽插小穴 很好 好舒服", "8398": "用力抽插小穴 很好 好舒服", "8399": "用力抽插小穴 很好 好舒服", "8400": "用力抽插小穴 很好 好舒服", "8401": "用力抽插小穴 很好 好舒服", "8402": "用力抽插小穴 很好 好舒服", "8403": "用力抽插小穴 很好 好舒服", "8404": "用力抽插小穴 很好 好舒服", "8405": "用力抽插小穴 很好 好舒服", "8406": "", "8407": "SP", "8408": "", "8409": "", "8410": "", "8411": "", "8412": "要高潮了 高潮了 我高潮了", "8413": "要高潮了 高潮了 我高潮了", "8414": "要高潮了 高潮了 我高潮了", "8415": "要高潮了 高潮了 我高潮了", "8416": "要高潮了 高潮了 我高潮了", "8417": "要高潮了 高潮了 我高潮了", "8418": "", "8419": "", "8420": "", "8421": "", "8422": "", "8423": "", "8424": "", "8425": "", "8426": "", "8427": "", "8428": "你的肉棒最棒了", "8429": "你的肉棒最棒了", "8430": "你的肉棒最棒了", "8431": "你的肉棒最棒了", "8432": "你的肉棒最棒了", "8433": "你的肉棒最棒了", "8434": "你的肉棒最棒了", "8435": "", "8436": "", "8437": "我也会自己动起来的", "8438": "我也会自己动起来的", "8439": "我也会自己动起来的", "8440": "我也会自己动起来的", "8441": "我也会自己动起来的", "8442": "我也会自己动起来的", "8443": "", "8444": "", "8445": "舒服吗 好舒服", "8446": "舒服吗 好舒服", "8447": "舒服吗 好舒服", "8448": "舒服吗 好舒服", "8449": "舒服吗 好舒服", "8450": "舒服吗 好舒服", "8451": "", "8452": "", "8453": "", "8454": "A", "8455": "", "8456": "", "8457": "", "8458": "A", "8459": "抽插得停不下来了吗", "8460": "抽插得停不下来了吗", "8461": "抽插得停不下来了吗", "8462": "抽插得停不下来了吗", "8463": "抽插得停不下来了吗", "8464": "", "8465": "", "8466": "", "8467": "", "8468": "", "8469": "", "8470": "", "8471": "", "8472": "", "8473": "", "8474": "", "8475": "", "8476": "", "8477": "", "8478": "", "8479": "", "8480": "这里好舒服 用力顶这里", "8481": "这里好舒服 用力顶这里", "8482": "这里好舒服 用力顶这里", "8483": "这里好舒服 用力顶这里", "8484": "这里好舒服 用力顶这里", "8485": "这里好舒服 用力顶这里", "8486": "这里好舒服 用力顶这里", "8487": "这里好舒服 用力顶这里", "8488": "这里好舒服 用力顶这里", "8489": "这里好舒服 用力顶这里", "8490": "", "8491": "", "8492": "高潮了 我高潮了", "8493": "高潮了 我高潮了", "8494": "高潮了 我高潮了", "8495": "高潮了 我高潮了", "8496": "高潮了 我高潮了", "8497": "", "8498": "", "8499": "", "8500": "", "8501": "", "8502": "", "8503": "", "8504": "", "8505": "", "8506": "", "8507": "", "8508": "", "8509": "", "8510": "", "8511": "图", "8512": "", "8513": "", "8514": "", "8515": "继续插进小穴", "8516": "继续插进小穴", "8517": "继续插进小穴", "8518": "继续插进小穴", "8519": "继续插进小穴", "8520": "继续插进小穴", "8521": "", "8522": "", "8523": "", "8524": "", "8525": "", "8526": "", "8527": "---", "8528": "", "8529": "", "8530": "", "8531": "", "8532": "---", "8533": "好厉害 肉棒插的好深", "8534": "好厉害 肉棒插的好深", "8535": "好厉害 肉棒插的好深", "8536": "好厉害 肉棒插的好深", "8537": "好厉害 肉棒插的好深", "8538": "好厉害 肉棒插的好深", "8539": "好厉害 肉棒插的好深", "8540": "好厉害 肉棒插的好深", "8541": "好厉害 肉棒插的好深", "8542": "", "8543": "", "8544": "肉棒跟我想像中一样舒服", "8545": "肉棒跟我想像中一样舒服", "8546": "肉棒跟我想像中一样舒服", "8547": "肉棒跟我想像中一样舒服", "8548": "肉棒跟我想像中一样舒服", "8549": "肉棒跟我想像中一样舒服", "8550": "肉棒跟我想像中一样舒服", "8551": "---", "8552": "", "8553": "---", "8554": "", "8555": "", "8556": "", "8557": "", "8558": "", "8559": "", "8560": "", "8561": "", "8562": "好厉害 你看 肉棒插进小穴了", "8563": "好厉害 你看 肉棒插进小穴了", "8564": "好厉害 你看 肉棒插进小穴了", "8565": "好厉害 你看 肉棒插进小穴了", "8566": "好厉害 你看 肉棒插进小穴了", "8567": "好厉害 你看 肉棒插进小穴了", "8568": "好厉害 你看 肉棒插进小穴了", "8569": "好厉害 你看 肉棒插进小穴了", "8570": "好厉害 你看 肉棒插进小穴了", "8571": "", "8572": "好舒服", "8573": "好舒服", "8574": "好舒服", "8575": "好舒服", "8576": "好舒服", "8577": "好舒服", "8578": "", "8579": "", "8580": "", "8581": "", "8582": "", "8583": "", "8584": "好厉害 肉棒一直在搅动我的小穴", "8585": "好厉害 肉棒一直在搅动我的小穴", "8586": "好厉害 肉棒一直在搅动我的小穴", "8587": "好厉害 肉棒一直在搅动我的小穴", "8588": "好厉害 肉棒一直在搅动我的小穴", "8589": "好厉害 肉棒一直在搅动我的小穴", "8590": "好厉害 肉棒一直在搅动我的小穴", "8591": "好厉害 肉棒一直在搅动我的小穴", "8592": "好厉害 肉棒一直在搅动我的小穴", "8593": "好厉害 肉棒一直在搅动我的小穴", "8594": "好厉害 肉棒一直在搅动我的小穴", "8595": "好厉害 肉棒一直在搅动我的小穴", "8596": "好厉害 肉棒一直在搅动我的小穴", "8597": "好厉害 肉棒一直在搅动我的小穴", "8598": "---", "8599": "", "8600": "---", "8601": "好舒服", "8602": "好舒服", "8603": "好舒服", "8604": "好舒服", "8605": "", "8606": "", "8607": "------------", "8608": "---", "8609": "---", "8610": "---", "8611": "---", "8612": "", "8613": "好厉害 很好", "8614": "好厉害 很好", "8615": "好厉害 很好", "8616": "好厉害 很好", "8617": "好厉害 很好", "8618": "好厉害 很好", "8619": "", "8620": "", "8621": "", "8622": "", "8623": "这里好舒服", "8624": "这里好舒服", "8625": "这里好舒服", "8626": "这里好舒服", "8627": "这里好舒服", "8628": "", "8629": "高潮了", "8630": "高潮了", "8631": "高潮了", "8632": "高潮了", "8633": "高潮了", "8634": "", "8635": "", "8636": "", "8637": "", "8638": "好厉害 那里 用力顶", "8639": "好厉害那里用力顶", "8640": "好厉害 那里 用力顶", "8641": "好厉害那里用力顶", "8642": "好厉害那里用力顶", "8643": "好厉害 那里 用力顶", "8644": "好厉害那里用力顶", "8645": "好厉害那里用力顶", "8646": "高潮了 我高潮了", "8647": "高潮了 我高潮了", "8648": "高潮了 我高潮了", "8649": "高潮了 我高潮了", "8650": "高潮了 我高潮了", "8651": "高潮了 我高潮了", "8652": "", "8653": "", "8654": "", "8655": "", "8656": "", "8657": "", "8658": "", "8659": "", "8660": "再插深一点 插深一点 好舒服 好爽", "8661": "再插深一点 插深一点 好舒服 好爽", "8662": "再插深一点 插深一点 好舒服 好爽", "8663": "再插深一点 插深一点 好舒服 好爽", "8664": "再插深一点 插深一点 好舒服 好爽", "8665": "再插深一点 插深一点 好舒服 好爽", "8666": "再插深一点 插深一点 好舒服 好爽", "8667": "再插深一点 插深一点 好舒服 好爽", "8668": "再插深一点 插深一点 好舒服 好爽", "8669": "再插深一点 插深一点 好舒服 好爽", "8670": "再插深一点 插深一点 好舒服 好爽", "8671": "再插深一点 插深一点 好舒服 好爽", "8672": "再插深一点 插深一点 好舒服 好爽", "8673": "", "8674": "", "8675": "", "8676": "", "8677": "", "8678": "", "8679": "", "8680": "", "8681": "", "8682": "", "8683": "", "8684": "", "8685": "", "8686": "", "8687": "", "8688": "", "8689": "", "8690": "", "8691": "", "8692": "", "8693": "", "8694": "", "8695": "", "8696": "肉棒插的好深", "8697": "肉棒插的好深", "8698": "肉棒插的好深", "8699": "肉棒插的好深", "8700": "肉棒插的好深", "8701": "", "8702": "", "8703": "", "8704": "", "8705": "", "8706": "", "8707": "", "8708": "", "8709": "", "8710": "", "8711": "", "8712": "", "8713": "", "8714": "", "8715": "", "8716": "", "8717": "", "8718": "", "8719": "", "8720": "", "8721": "", "8722": "", "8723": "", "8724": "", "8725": "", "8726": "总是你在动不公平", "8727": "总是你在动不公平", "8728": "总是你在动不公平", "8729": "总是你在动不公平", "8730": "总是你在动不公平", "8731": "总是你在动不公平", "8732": "", "8733": "", "8734": "", "8735": "", "8736": "", "8737": "", "8738": "", "8739": "", "8740": "", "8741": "", "8742": "", "8743": "", "8744": "", "8745": "", "8746": "", "8747": "", "8748": "", "8749": "", "8750": "", "8751": "", "8752": "", "8753": "", "8754": "", "8755": "看着插进小穴的样子 要插进小穴了", "8756": "看着插进小穴的样子 要插进小穴了", "8757": "看着插进小穴的样子 要插进小穴了", "8758": "看着插进小穴的样子 要插进小穴了", "8759": "看着插进小穴的样子 要插进小穴了", "8760": "看着插进小穴的样子 要插进小穴了", "8761": "看着插进小穴的样子 要插进小穴了", "8762": "看着插进小穴的样子 要插进小穴了", "8763": "看着插进小穴的样子 要插进小穴了", "8764": "看着插进小穴的样子 要插进小穴了", "8765": "看着插进小穴的样子 要插进小穴了", "8766": "看着插进小穴的样子 要插进小穴了", "8767": "", "8768": "", "8769": "", "8770": "", "8771": "", "8772": "", "8773": "", "8774": "", "8775": "", "8776": "", "8777": "", "8778": "", "8779": "", "8780": "------", "8781": "", "8782": "", "8783": "肉棒一直在抽插小穴", "8784": "肉棒一直在抽插小穴", "8785": "肉棒一直在抽插小穴", "8786": "肉棒一直在抽插小穴", "8787": "肉棒一直在抽插小穴", "8788": "肉棒一直在抽插小穴", "8789": "", "8790": "", "8791": "---", "8792": "", "8793": "", "8794": "好舒服", "8795": "好舒服", "8796": "好舒服", "8797": "好舒服", "8798": "好舒服", "8799": "", "8800": "", "8801": "", "8802": "", "8803": "", "8804": "", "8805": "", "8806": "好厉害", "8807": "好厉害", "8808": "好厉害", "8809": "好厉害", "8810": "", "8811": "", "8812": "", "8813": "", "8814": "", "8815": "", "8816": "", "8817": "", "8818": "", "8819": "", "8820": "", "8821": "", "8822": "", "8823": "", "8824": "", "8825": "", "8826": "", "8827": "", "8828": "", "8829": "不行 高潮了 我高潮了", "8830": "不行 高潮了 我高潮了", "8831": "不行 高潮了 我高潮了", "8832": "不行 高潮了 我高潮了", "8833": "不行 高潮了 我高潮了", "8834": "", "8835": "", "8836": "", "8837": "", "8838": "", "8839": "", "8840": "", "8841": "", "8842": "", "8843": "这样搅动小穴好棒", "8844": "这样搅动小穴好棒", "8845": "这样搅动小穴好棒", "8846": "这样搅动小穴好棒", "8847": "这样搅动小穴好棒", "8848": "这样搅动小穴好棒", "8849": "", "8850": "", "8851": "", "8852": "", "8853": "", "8854": "", "8855": "", "8856": "", "8857": "", "8858": "", "8859": "", "8860": "", "8861": "", "8862": "", "8863": "", "8864": "这样从下面往上顶要高潮了 好爽 真是太爽了", "8865": "这样从下面往上顶要高潮了 好爽 真是太爽了", "8866": "这样从下面往上顶要高潮了 好爽 真是太爽了", "8867": "这样从下面往上顶要高潮了 好爽 真是太爽了", "8868": "这样从下面往上顶要高潮了 好爽 真是太爽了", "8869": "这样从下面往上顶要高潮了 好爽 真是太爽了", "8870": "这样从下面往上顶要高潮了 好爽 真是太爽了", "8871": "这样从下面往上顶要高潮了 好爽 真是太爽了", "8872": "这样从下面往上顶要高潮了 好爽 真是太爽了", "8873": "这样从下面往上顶要高潮了 好爽 真是太爽了", "8874": "这样从下面往上顶要高潮了 好爽 真是太爽了", "8875": "这样从下面往上顶要高潮了 好爽 真是太爽了", "8876": "高潮了 我高潮了", "8877": "高潮了 我高潮了", "8878": "高潮了 我高潮了", "8879": "高潮了 我高潮了", "8880": "高潮了 我高潮了", "8881": "高潮了 我高潮了", "8882": "", "8883": "", "8884": "", "8885": "", "8886": "", "8887": "", "8888": "", "8889": "", "8890": "", "8891": "", "8892": "", "8893": "", "8894": "这样动的话 你会累的吧 让我来动吧", "8895": "这样动的话 你会累的吧 让我来动吧", "8896": "这样动的话 你会累的吧 让我来动吧", "8897": "这样动的话 你会累的吧 让我来动吧", "8898": "这样动的话 你会累的吧 让我来动吧", "8899": "这样动的话 你会累的吧 让我来动吧", "8900": "这样动的话你会累的吧 让我来动吧", "8901": "这样动的话 你会累的吧 让我来动吧", "8902": "这样动的话 你会累的吧 让我来动吧", "8903": "这样动的话 你会累的吧 让我来动吧", "8904": "这样动的话你会累的吧让我来动吧", "8905": "", "8906": "", "8907": "", "8908": "", "8909": "", "8910": "好舒服 我喜欢顶到 小穴最里面", "8911": "好舒服 我喜欢顶到 小穴最里面", "8912": "好舒服 我喜欢顶到 小穴最里面", "8913": "好舒服 我喜欢顶到 小穴最里面", "8914": "好舒服 我喜欢顶到 小穴最里面", "8915": "好舒服 我喜欢顶到 小穴最里面", "8916": "好舒服 我喜欢顶到 小穴最里面", "8917": "好舒服 我喜欢顶到 小穴最里面", "8918": "好舒服 我喜欢顶到 小穴最里面", "8919": "好舒服 我喜欢顶到 小穴最里面", "8920": "", "8921": "", "8922": "", "8923": "超舒服的", "8924": "超舒服的", "8925": "超舒服的", "8926": "超舒服的", "8927": "超舒服的", "8928": "", "8929": "这样就要高潮了 要高潮了 我要高潮了", "8930": "这样就要高潮了 要高潮了 我要高潮了", "8931": "这样就要高潮了 要高潮了 我要高潮了", "8932": "这样就要高潮了 要高潮了 我要高潮了", "8933": "这样就要高潮了 要高潮了 我要高潮了", "8934": "这样就要高潮了 要高潮了 我要高潮了", "8935": "这样就要高潮了 要高潮了 我要高潮了", "8936": "这样就要高潮了 要高潮了 我要高潮了", "8937": "", "8938": "高潮了", "8939": "高潮了", "8940": "高潮了", "8941": "高潮了", "8942": "高潮了", "8943": "", "8944": "", "8945": "", "8946": "", "8947": "", "8948": "", "8949": "我的小穴怎么样 舒服吗", "8950": "我的小穴怎么样 舒服吗", "8951": "我的小穴怎么样 舒服吗", "8952": "我的小穴怎么样 舒服吗", "8953": "我的小穴怎么样 舒服吗", "8954": "我的小穴怎么样 舒服吗", "8955": "我的小穴怎么样 舒服吗", "8956": "我的小穴怎么样 舒服吗", "8957": "我的小穴怎么样 舒服吗", "8958": "我的小穴怎么样 舒服吗", "8959": "我的小穴怎么样 舒服吗", "8960": "我的小穴怎么样 舒服吗", "8961": "", "8962": "", "8963": "", "8964": "", "8965": "", "8966": "", "8967": "", "8968": "", "8969": "", "8970": "", "8971": "", "8972": "", "8973": "", "8974": "", "8975": "", "8976": "", "8977": "", "8978": "", "8979": "", "8980": "", "8981": "", "8982": "", "8983": "", "8984": "", "8985": "", "8986": "", "8987": "---", "8988": "", "8989": "HTML", "8990": "", "8991": "", "8992": "", "8993": "", "8994": "", "8995": "", "8996": "", "8997": "", "8998": "", "8999": "", "9000": "", "9001": "", "9002": "", "9003": "", "9004": "", "9005": "", "9006": "", "9007": "", "9008": "", "9009": "", "9010": "", "9011": "", "9012": "", "9013": "", "9014": "", "9015": "", "9016": "", "9017": "", "9018": "", "9019": "", "9020": "", "9021": "---", "9022": "", "9023": "", "9024": "HTML", "9025": "", "9026": "---", "9027": "", "9028": "---", "9029": "---", "9030": "", "9031": "", "9032": "", "9033": "", "9034": "肉棒在小穴里面好硬", "9035": "肉棒在小穴里面好硬", "9036": "肉棒在小穴里面好硬", "9037": "肉棒在小穴里面好硬", "9038": "肉棒在小穴里面好硬", "9039": "肉棒在小穴里面好硬", "9040": "", "9041": "---------------", "9042": "", "9043": "", "9044": "", "9045": "", "9046": "", "9047": "", "9048": "", "9049": "", "9050": "", "9051": "", "9052": "", "9053": "", "9054": "", "9055": "", "9056": "高潮了 我高潮了", "9057": "高潮了 我高潮了", "9058": "高潮了 我高潮了", "9059": "高潮了 我高潮了", "9060": "高潮了 我高潮了", "9061": "高潮了 我高潮了", "9062": "高潮了 我高潮了", "9063": "高潮了 我高潮了", "9064": "", "9065": "", "9066": "---", "9067": "", "9068": "", "9069": "", "9070": "", "9071": "", "9072": "---", "9073": "---", "9074": "", "9075": "---", "9076": "", "9077": "", "9078": "", "9079": "", "9080": "", "9081": "", "9082": "", "9083": "好厉害 肉棒在里面动呢 好舒服", "9084": "好厉害 肉棒在里面动呢 好舒服", "9085": "好厉害 肉棒在里面动呢 好舒服", "9086": "好厉害 肉棒在里面动呢 好舒服", "9087": "好厉害 肉棒在里面动呢 好舒服", "9088": "好厉害 肉棒在里面动呢 好舒服", "9089": "好厉害 肉棒在里面动呢 好舒服", "9090": "好厉害 肉棒在里面动呢 好舒服", "9091": "好厉害 肉棒在里面动呢 好舒服", "9092": "好厉害 肉棒在里面动呢 好舒服", "9093": "", "9094": "我的唾液", "9095": "我的唾液", "9096": "我的唾液", "9097": "我的唾液", "9098": "我的唾液", "9099": "我的唾液", "9100": "", "9101": "", "9102": "", "9103": "怎么样 美味吗 好美味", "9104": "怎么样 美味吗 好美味", "9105": "怎么样 美味吗 好美味", "9106": "怎么样 美味吗 好美味", "9107": "怎么样 美味吗 好美味", "9108": "怎么样 美味吗 好美味", "9109": "怎么样 美味吗 好美味", "9110": "怎么样 美味吗 好美味", "9111": "怎么样 美味吗 好美味", "9112": "怎么样 美味吗 好美味", "9113": "怎么样 美味吗 好美味", "9114": "", "9115": "", "9116": "好舒服", "9117": "好舒服", "9118": "好舒服", "9119": "好舒服", "9120": "好舒服", "9121": "", "9122": "", "9123": "", "9124": "", "9125": "", "9126": "", "9127": "---", "9128": "", "9129": "", "9130": "", "9131": "", "9132": "", "9133": "", "9134": "", "9135": "", "9136": "", "9137": "", "9138": "", "9139": "", "9140": "", "9141": "", "9142": "", "9143": "", "9144": "你还想跟我做爱吧", "9145": "你还想跟我做爱吧", "9146": "你还想跟我做爱吧", "9147": "你还想跟我做爱吧", "9148": "你还想跟我做爱吧", "9149": "你还想跟我做爱吧", "9150": "", "9151": "", "9152": "", "9153": "", "9154": "", "9155": "", "9156": "好厉害", "9157": "好厉害", "9158": "好厉害", "9159": "好厉害", "9160": "好厉害", "9161": "", "9162": "", "9163": "", "9164": "", "9165": "", "9166": "", "9167": "", "9168": "", "9169": "", "9170": "", "9171": "", "9172": "高潮了 我高潮了", "9173": "高潮了 我高潮了", "9174": "高潮了 我高潮了", "9175": "高潮了 我高潮了", "9176": "高潮了 我高潮了", "9177": "高潮了 我高潮了", "9178": "高潮了 我高潮了", "9179": "高潮了 我高潮了", "9180": "高潮了 我高潮了", "9181": "", "9182": "", "9183": "", "9184": "", "9185": "", "9186": "", "9187": "", "9188": "---", "9189": "", "9190": "", "9191": "", "9192": "", "9193": "---", "9194": "", "9195": "", "9196": "", "9197": "", "9198": "", "9199": "", "9200": "", "9201": "", "9202": "", "9203": "", "9204": "", "9205": "", "9206": "", "9207": "", "9208": "", "9209": "", "9210": "", "9211": "高潮了", "9212": "高潮了", "9213": "高潮了", "9214": "高潮了", "9215": "高潮了", "9216": "", "9217": "", "9218": "", "9219": "", "9220": "___", "9221": "", "9222": "", "9223": "", "9224": "", "9225": "", "9226": "", "9227": "", "9228": "", "9229": "", "9230": "你还能继续吗", "9231": "你还能继续吗", "9232": "你还能继续吗", "9233": "你还能继续吗", "9234": "你还能继续吗", "9235": "", "9236": "", "9237": "", "9238": "", "9239": "", "9240": "来这边", "9241": "来这边", "9242": "来这边", "9243": "来这边", "9244": "来这边", "9245": "", "9246": "", "9247": "", "9248": "", "9249": "", "9250": "", "9251": "", "9252": "", "9253": "", "9254": "", "9255": "", "9256": "", "9257": "", "9258": "", "9259": "", "9260": "", "9261": "", "9262": "", "9263": "", "9264": "", "9265": "", "9266": "", "9267": "太厉害了 肉棒一直都好硬", "9268": "太厉害了 肉棒一直都好硬", "9269": "太厉害了 肉棒一直都好硬", "9270": "太厉害了 肉棒一直都好硬", "9271": "太厉害了 肉棒一直都好硬", "9272": "太厉害了 肉棒一直都好硬", "9273": "太厉害了 肉棒一直都好硬", "9274": "太厉害了 肉棒一直都好硬", "9275": "太厉害了 肉棒一直都好硬", "9276": "", "9277": "", "9278": "", "9279": "", "9280": "", "9281": "", "9282": "", "9283": "", "9284": "", "9285": "", "9286": "", "9287": "这样呢 好舒服", "9288": "这样呢 好舒服", "9289": "这样呢 好舒服", "9290": "这样呢 好舒服", "9291": "这样呢 好舒服", "9292": "这样呢 好舒服", "9293": "这样呢 好舒服", "9294": "", "9295": "", "9296": "", "9297": "", "9298": "好厉害", "9299": "好厉害", "9300": "好厉害", "9301": "好厉害", "9302": "硬梆梆的肉棒摩擦的感觉 好舒服啊", "9303": "硬梆梆的肉棒摩擦的感觉 好舒服啊", "9304": "硬梆梆的肉棒摩擦的感觉 好舒服啊", "9305": "硬梆梆的肉棒摩擦的感觉 好舒服啊", "9306": "硬梆梆的肉棒摩擦的感觉 好舒服啊", "9307": "硬梆梆的肉棒摩擦的感觉 好舒服啊", "9308": "硬梆梆的肉棒摩擦的感觉 好舒服啊", "9309": "A", "9310": "SPH", "9311": "SP10+", "9312": "SP10-1型", "9313": "SPH-1专用", "9314": "SPIO-1 FEB", "9315": "不过你想插进小穴吧", "9316": "不过你想插进小穴吧", "9317": "不过你想插进小穴吧", "9318": "不过你想插进小穴吧", "9319": "不过你想插进小穴吧", "9320": "不过你想插进小穴吧", "9321": "", "9322": "", "9323": "", "9324": "", "9325": "", "9326": "", "9327": "", "9328": "", "9329": "", "9330": "用力插进小穴吧", "9331": "用力插进小穴吧", "9332": "用力插进小穴吧", "9333": "用力插进小穴吧", "9334": "用力插进小穴吧", "9335": "", "9336": "", "9337": "------", "9338": "", "9339": "", "9340": "", "9341": "", "9342": "", "9343": "好厉害", "9344": "好厉害", "9345": "好厉害", "9346": "好厉害", "9347": "好厉害", "9348": "", "9349": "", "9350": "", "9351": "你的肉棒好厉害 太舒服了", "9352": "你的肉棒好厉害太舒服了", "9353": "你的肉棒好厉害 太舒服了", "9354": "你的肉棒好厉害 太舒服了", "9355": "你的肉棒好厉害 太舒服了", "9356": "你的肉棒好厉害 太舒服了", "9357": "你的肉棒好厉害 太舒服了", "9358": "你的肉棒好厉害 太舒服了", "9359": "你的肉棒好厉害 太舒服了", "9360": "你的肉棒好厉害 太舒服了", "9361": "你的肉棒好厉害 太舒服了", "9362": "", "9363": "", "9364": "", "9365": "", "9366": "", "9367": "", "9368": "真的好舒服", "9369": "真的好舒服", "9370": "真的好舒服", "9371": "真的好舒服", "9372": "真的好舒服", "9373": "真的好舒服", "9374": "", "9375": "", "9376": "", "9377": "", "9378": "", "9379": "", "9380": "", "9381": "", "9382": "---", "9383": "---", "9384": "---", "9385": "那里 好舒服啊", "9386": "那里 好舒服啊", "9387": "那里 好舒服啊", "9388": "那里 好舒服啊", "9389": "那里 好舒服啊", "9390": "那里 好舒服啊", "9391": "", "9392": "------", "9393": "", "9394": "---", "9395": "", "9396": "---", "9397": "---", "9398": "", "9399": "---", "9400": "顶到舒服的地方了", "9401": "顶到舒服的地方了", "9402": "顶到舒服的地方了", "9403": "顶到舒服的地方了", "9404": "顶到舒服的地方了", "9405": "顶到舒服的地方了", "9406": "", "9407": "---", "9408": "", "9409": "高潮了 我高潮了", "9410": "高潮了 我高潮了", "9411": "高潮了 我高潮了", "9412": "高潮了 我高潮了", "9413": "高潮了 我高潮了", "9414": "高潮了 我高潮了", "9415": "---", "9416": "", "9417": "", "9418": "", "9419": "---", "9420": "---", "9421": "", "9422": "", "9423": "", "9424": "", "9425": "", "9426": "NJ", "9427": "", "9428": "", "9429": "", "9430": "", "9431": "DAY", "9432": "", "9433": "", "9434": "", "9435": "", "9436": "", "9437": "", "9438": "", "9439": "", "9440": "", "9441": "", "9442": "", "9443": "", "9444": "", "9445": "", "9446": "", "9447": "", "9448": "", "9449": "", "9450": "", "9451": "", "9452": "好厉害", "9453": "好厉害", "9454": "好厉害", "9455": "好厉害", "9456": "", "9457": "", "9458": "", "9459": "", "9460": "", "9461": "", "9462": "", "9463": "", "9464": "", "9465": "", "9466": "", "9467": "", "9468": "", "9469": "", "9470": "", "9471": "", "9472": "舒服极了 不行 那里要高潮了", "9473": "舒服极了 不行 那里要高潮了", "9474": "舒服极了 不行 那里要高潮了", "9475": "舒服极了 不行 那里要高潮了", "9476": "舒服极了 不行 那里要高潮了", "9477": "舒服极了 不行 那里要高潮了", "9478": "我要高潮了", "9479": "我要高潮了", "9480": "我要高潮了", "9481": "我要高潮了", "9482": "我要高潮了", "9483": "", "9484": "", "9485": "别停 别停下", "9486": "别停 别停下", "9487": "别停 别停下", "9488": "别停 别停下", "9489": "别停 别停下", "9490": "别停 别停下", "9491": "", "9492": "", "9493": "", "9494": "", "9495": "", "9496": "", "9497": "", "9498": "", "9499": "", "9500": "", "9501": "", "9502": "", "9503": "", "9504": "", "9505": "", "9506": "", "9507": "", "9508": "", "9509": "", "9510": "", "9511": "", "9512": "", "9513": "", "9514": "", "9515": "肉棒太爽了", "9516": "肉棒太爽了", "9517": "肉棒太爽了", "9518": "肉棒太爽了", "9519": "肉棒太爽了", "9520": "", "9521": "", "9522": "", "9523": "------", "9524": "", "9525": "", "9526": "", "9527": "", "9528": "", "9529": "", "9530": "", "9531": "___", "9532": "", "9533": "", "9534": "", "9535": "", "9536": "", "9537": "", "9538": "", "9539": "", "9540": "", "9541": "", "9542": "", "9543": "", "9544": "", "9545": "", "9546": "", "9547": "", "9548": "", "9549": "", "9550": "", "9551": "", "9552": "", "9553": "", "9554": "", "9555": "", "9556": "舔遍我的全身上下", "9557": "舔遍我的全身上下", "9558": "舔遍我的全身上下", "9559": "舔遍我的全身上下", "9560": "舔遍我的全身上下", "9561": "舔遍我的全身上下", "9562": "---", "9563": "", "9564": "", "9565": "", "9566": "", "9567": "", "9568": "", "9569": "", "9570": "", "9571": "", "9572": "", "9573": "太舒服了", "9574": "太舒服了", "9575": "太舒服了", "9576": "太舒服了", "9577": "太舒服了", "9578": "---", "9579": "", "9580": "---", "9581": "", "9582": "---", "9583": "高潮了", "9584": "高潮了", "9585": "高潮了", "9586": "高潮了", "9587": "高潮了", "9588": "---------------", "9589": "---", "9590": "", "9591": "", "9592": "---", "9593": "怎么样 你舒服吗", "9594": "怎么样 你舒服吗", "9595": "怎么样 你舒服吗", "9596": "怎么样 你舒服吗", "9597": "怎么样 你舒服吗", "9598": "", "9599": "", "9600": "", "9601": "", "9602": "", "9603": "", "9604": "", "9605": "", "9606": "", "9607": "", "9608": "", "9609": "", "9610": "不用忍着", "9611": "不用忍着", "9612": "不用忍着", "9613": "不用忍着", "9614": "不用忍着", "9615": "不用忍着", "9616": "", "9617": "", "9618": "", "9619": "", "9620": "", "9621": "", "9622": "好舒服", "9623": "好舒服", "9624": "好舒服", "9625": "好舒服", "9626": "好舒服", "9627": "---------------", "9628": "---", "9629": "", "9630": "", "9631": "", "9632": "---", "9633": "", "9634": "---", "9635": "---", "9636": "好舒服", "9637": "好舒服", "9638": "好舒服", "9639": "好舒服", "9640": "好舒服", "9641": "", "9642": "", "9643": "", "9644": "", "9645": "射在小穴里面吧 全都射进来", "9646": "射在小穴里面吧 全都射进来", "9647": "射在小穴里面吧 全都射进来", "9648": "射在小穴里面吧 全都射进来", "9649": "射在小穴里面吧 全都射进来", "9650": "射在小穴里面吧 全都射进来", "9651": "射了", "9652": "射了", "9653": "射了", "9654": "射了", "9655": "", "9656": "", "9657": "", "9658": "", "9659": "", "9660": "", "9661": "", "9662": "", "9663": "", "9664": "", "9665": "__________________", "9666": "", "9667": "", "9668": "---", "9669": "", "9670": "", "9671": "", "9672": "---", "9673": "", "9674": "", "9675": "", "9676": "", "9677": "好舒服", "9678": "好舒服", "9679": "好舒服", "9680": "好舒服", "9681": "", "9682": "", "9683": "------------------", "9684": "", "9685": "", "9686": "", "9687": "", "9688": "", "9689": "", "9690": "", "9691": "", "9692": "", "9693": "---", "9694": "", "9695": "", "9696": "", "9697": "", "9698": "", "9699": "", "9700": "", "9701": "", "9702": "", "9703": "", "9704": "", "9705": "", "9706": "", "9707": "", "9708": "", "9709": "", "9710": "", "9711": "", "9712": "", "9713": "", "9714": "", "9715": "", "9716": "", "9717": "", "9718": "", "9719": "", "9720": "", "9721": "", "9722": "医生", "9723": "医生", "9724": "医生", "9725": "医生", "9726": "", "9727": "", "9728": "", "9729": "10-14岁", "9730": "10-1期B", "9731": "NO.1专用", "9732": "PHD-手術", "9733": "", "9734": "", "9735": "P10一手机", "9736": "Pトローネ電話", "9737": "PHD一专用", "9738": "PH0一1瓶盖", "9739": "PHO一号馆", "9740": "SPHO一专用", "9741": "SPHO-1", "9742": "SPNO-1", "9743": "SPH", "9744": "SP", "9745": "", "9746": "", "9747": "", "9748": "", "9749": "", "9750": "", "9751": "", "9752": "", "9753": "", "9754": "", "9755": "", "9756": "", "9757": "", "9758": "", "9759": "", "9760": "", "9761": "", "9762": "", "9763": "", "9764": "", "9765": "", "9766": "", "9767": "", "9768": "", "9769": "", "9770": "", "9771": "", "9772": "", "9773": "", "9774": "", "9775": "", "9776": "", "9777": "", "9778": "", "9779": "", "9780": "", "9781": "", "9782": "比我想像中还要舒服", "9783": "比我想像中还要舒服", "9784": "比我想像中还要舒服", "9785": "比我想像中还要舒服", "9786": "比我想像中还要舒服", "9787": "比我想像中还要舒服", "9788": "", "9789": "", "9790": "", "9791": "那么后面你要帮忙哦", "9792": "那么后面你要帮忙哦", "9793": "那么后面你要帮忙哦", "9794": "那么后面你要帮忙哦", "9795": "那么后面你要帮忙哦", "9796": "那么后面你要帮忙哦", "9797": "那么后面你要帮忙哦", "9798": "那么后面你要帮忙哦", "9799": "那么后面你要帮忙哦", "9800": "好的 我很荣幸", "9801": "好的 我很荣幸", "9802": "好的 我很荣幸", "9803": "好的 我很荣幸", "9804": "好的 我很荣幸", "9805": "好的 我很荣幸", "9806": "好的 我很荣幸", "9807": "", "9808": "", "9809": "", "9810": "", "9811": "", "9812": "", "9813": "", "9814": "", "9815": "", "9816": "", "9817": "", "9818": "", "9819": "", "9820": "", "9821": "", "9822": "", "9823": "", "9824": "", "9825": "", "9826": "", "9827": "", "9828": "", "9829": "", "9830": "", "9831": "", "9832": "", "9833": "", "9834": "", "9835": "", "9836": "", "9837": "", "9838": "", "9839": "", "9840": "", "9841": "", "9842": "", "9843": "", "9844": "", "9845": "", "9846": "", "9847": "", "9848": "", "9849": "", "9850": "", "9851": "果林前辈", "9852": "果林前辈", "9853": "果林前辈", "9854": "果林前辈", "9855": "抱歉 明明不是你上夜班", "9856": "抱歉 明明不是你上夜班", "9857": "抱歉 明明不是你上夜班", "9858": "抱歉 明明不是你上夜班", "9859": "抱歉 明明不是你上夜班", "9860": "感谢你的陪同", "9861": "感谢你的陪同", "9862": "感谢你的陪同", "9863": "感谢你的陪同", "9864": "感谢你的陪同", "9865": "", "9866": "", "9867": "", "9868": "", "9869": "不过话说回来 权藤医生说的", "9870": "不过话说回来 权藤医生说的", "9871": "不过话说回来 权藤医生说的", "9872": "不过话说回来 权藤医生说的", "9873": "不过话说回来 权藤医生说的", "9874": "业务到底是什么", "9875": "业务到底是什么", "9876": "业务到底是什么", "9877": "业务到底是什么", "9878": "业务到底是什么", "9879": "", "9880": "", "9881": "", "9882": "M", "9883": "", "9884": "", "9885": "", "9886": "", "9887": "", "9888": "", "9889": "", "9890": "", "9891": "", "9892": "", "9893": "", "9894": "", "9895": "", "9896": "", "9897": "", "9898": "", "9899": "", "9900": "", "9901": "", "9902": "", "9903": "", "9904": "", "9905": "", "9906": "", "9907": "", "9908": "", "9909": "", "9910": "", "9911": "", "9912": "", "9913": "", "9914": "", "9915": "", "9916": "辛苦了 还请多多指教", "9917": "辛苦了 还请多多指教", "9918": "辛苦了 还请多多指教", "9919": "辛苦了 还请多多指教", "9920": "辛苦了 还请多多指教", "9921": "辛苦了 还请多多指教", "9922": "辛苦了 还请多多指教", "9923": "辛苦了 还请多多指教", "9924": "", "9925": "", "9926": "", "9927": "", "9928": "", "9929": "", "9930": "", "9931": "", "9932": "", "9933": "", "9934": "", "9935": "", "9936": "", "9937": "", "9938": "", "9939": "", "9940": "A", "9941": "你在干什么 松井小姐", "9942": "你在干什么 松井小姐", "9943": "你在干什么 松井小姐", "9944": "你在干什么 松井小姐", "9945": "你在干什么 松井小姐", "9946": "你在干什么 松井小姐", "9947": "你在干什么 松井小姐", "9948": "你在干什么 松井小姐", "9949": "请放开我", "9950": "请放开我", "9951": "请放开我", "9952": "请放开我", "9953": "请放开我", "9954": "", "9955": "", "9956": "这是新业务的培训 松井小姐", "9957": "这是新业务的培训 松井小姐", "9958": "这是新业务的培训 松井小姐", "9959": "这是新业务的培训 松井小姐", "9960": "这是新业务的培训 松井小姐", "9961": "这是新业务的培训 松井小姐", "9962": "这是新业务的培训 松井小姐", "9963": "这是新业务的培训 松井小姐", "9964": "这是新业务的培训 松井小姐", "9965": "", "9966": "", "9967": "", "9968": "", "9969": "北冈小姐是吧", "9970": "北冈小姐是吧", "9971": "北冈小姐是吧", "9972": "北冈小姐是吧", "9973": "", "9974": "来吧 快动手", "9975": "来吧 快动手", "9976": "来吧 快动手", "9977": "来吧 快动手", "9978": "来吧 快动手", "9979": "来吧 快动手", "9980": "", "9981": "", "9982": "", "9983": "", "9984": "", "9985": "", "9986": "", "9987": "---", "9988": "", "9989": "", "9990": "", "9991": "", "9992": "", "9993": "", "9994": "", "9995": "果林前辈", "9996": "果林前辈", "9997": "果林前辈", "9998": "果林前辈", "9999": "松井小姐 请你闭嘴 好好学习", "10000": "松井小姐 请你闭嘴 好好学习", "10001": "松井小姐 请你闭嘴 好好学习", "10002": "松井小姐 请你闭嘴 好好学习", "10003": "松井小姐 请你闭嘴 好好学习", "10004": "松井小姐 请你闭嘴 好好学习", "10005": "松井小姐 请你闭嘴 好好学习", "10006": "松井小姐 请你闭嘴 好好学习", "10007": "松井小姐 请你闭嘴 好好学习", "10008": "松井小姐 请你闭嘴 好好学习", "10009": "松井小姐 请你闭嘴 好好学习", "10010": "松井小姐 请你闭嘴 好好学习", "10011": "", "10012": "", "10013": "", "10014": "", "10015": "", "10016": "---", "10017": "", "10018": "", "10019": "", "10020": "", "10021": "", "10022": "", "10023": "", "10024": "", "10025": "", "10026": "", "10027": "真是吵死了 北冈小姐她", "10028": "真是吵死了 北冈小姐她", "10029": "真是吵死了 北冈小姐她", "10030": "真是吵死了 北冈小姐她", "10031": "真是吵死了 北冈小姐她", "10032": "真是吵死了 北冈小姐她", "10033": "真是吵死了 北冈小姐她", "10034": "真是吵死了 北冈小姐她", "10035": "真是吵死了 北冈小姐她", "10036": "", "10037": "已经在这里3年了 早就习惯了", "10038": "已经在这里3年了 早就习惯了", "10039": "已经在这里3年了 早就习惯了", "10040": "已经在这里3年了 早就习惯了", "10041": "已经在这里3年了 早就习惯了", "10042": "已经在这里3年了 早就习惯了", "10043": "已经在这里3年了 早就习惯了", "10044": "已经在这里3年了 早就习惯了", "10045": "已经在这里3年了 早就习惯了", "10046": "已经在这里3年了 早就习惯了", "10047": "", "10048": "", "10049": "", "10050": "", "10051": "", "10052": "", "10053": "", "10054": "", "10055": "", "10056": "", "10057": "", "10058": "", "10059": "", "10060": "", "10061": "", "10062": "松井小姐 请不要看", "10063": "松井小姐 请不要看", "10064": "松井小姐 请不要看", "10065": "松井小姐 请不要看", "10066": "松井小姐 请不要看", "10067": "", "10068": "", "10069": "", "10070": "", "10071": "", "10072": "", "10073": "", "10074": "", "10075": "为什么 这是为什么", "10076": "为什么 这是为什么", "10077": "为什么 这是为什么", "10078": "为什么 这是为什么", "10079": "为什么 这是为什么", "10080": "为什么 这是为什么", "10081": "", "10082": "", "10083": "", "10084": "", "10085": "", "10086": "", "10087": "是想帮助重要的前辈吗", "10088": "是想帮助重要的前辈吗", "10089": "是想帮助重要的前辈吗", "10090": "是想帮助重要的前辈吗", "10091": "是想帮助重要的前辈吗", "10092": "是想帮助重要的前辈吗", "10093": "是想帮助重要的前辈吗", "10094": "是想帮助重要的前辈吗", "10095": "是想帮助重要的前辈吗", "10096": "", "10097": "", "10098": "", "10099": "很遗憾", "10100": "很遗憾", "10101": "很遗憾", "10102": "很遗憾", "10103": "很遗憾", "10104": "接下来 你也会有同样的遭遇", "10105": "接下来 你也会有同样的遭遇", "10106": "接下来 你也会有同样的遭遇", "10107": "接下来 你也会有同样的遭遇", "10108": "接下来 你也会有同样的遭遇", "10109": "接下来 你也会有同样的遭遇", "10110": "接下来 你也会有同样的遭遇", "10111": "接下来 你也会有同样的遭遇", "10112": "接下来 你也会有同样的遭遇", "10113": "不要", "10114": "不要", "10115": "不要", "10116": "不要", "10117": "", "10118": "", "10119": "", "10120": "", "10121": "", "10122": "", "10123": "", "10124": "", "10125": "", "10126": "", "10127": "", "10128": "", "10129": "", "10130": "", "10131": "", "10132": "", "10133": "", "10134": "---", "10135": "", "10136": "", "10137": "", "10138": "", "10139": "", "10140": "", "10141": "", "10142": "", "10143": "", "10144": "---", "10145": "", "10146": "", "10147": "", "10148": "---", "10149": "", "10150": "", "10151": "", "10152": "", "10153": "住手 不要", "10154": "住手 不要", "10155": "住手 不要", "10156": "住手 不要", "10157": "住手 不要", "10158": "", "10159": "", "10160": "", "10161": "", "10162": "", "10163": "", "10164": "", "10165": "", "10166": "", "10167": "", "10168": "", "10169": "", "10170": "", "10171": "The image is blurry and does not contain any discernible text.", "10172": "", "10173": "", "10174": "", "10175": "", "10176": "", "10177": "", "10178": "", "10179": "", "10180": "要怎么说才好呢 你也会遇到一样的事的", "10181": "要怎么说才好呢 你也会遇到一样的事的", "10182": "要怎么说才好呢 你也会遇到一样的事的", "10183": "要怎么说才好呢 你也会遇到一样的事的", "10184": "要怎么说才好呢 你也会遇到一样的事的", "10185": "要怎么说才好呢 你也会遇到一样的事的", "10186": "要怎么说才好呢 你也会遇到一样的事的", "10187": "要怎么说才好呢 你也会遇到一样的事的", "10188": "要怎么说才好呢 你也会遇到一样的事的", "10189": "要怎么说才好呢 你也会遇到一样的事的", "10190": "要怎么说才好呢 你也会遇到一样的事的", "10191": "要怎么说才好呢 你也会遇到一样的事的", "10192": "", "10193": "", "10194": "", "10195": "", "10196": "", "10197": "", "10198": "", "10199": "", "10200": "", "10201": "", "10202": "", "10203": "", "10204": "", "10205": "", "10206": "", "10207": "", "10208": "", "10209": "A", "10210": "", "10211": "不要 不可以", "10212": "不要 不可以", "10213": "不要 不可以", "10214": "不要 不可以", "10215": "不要 不可以", "10216": "不要 不可以", "10217": "不要 不可以", "10218": "", "10219": "", "10220": "", "10221": "", "10222": "", "10223": "", "10224": "", "10225": "", "10226": "", "10227": "", "10228": "", "10229": "", "10230": "", "10231": "看看北冈小姐吧", "10232": "看看北冈小姐吧", "10233": "看看北冈小姐吧", "10234": "看看北冈小姐吧", "10235": "看看北冈小姐吧", "10236": "看看北冈小姐吧", "10237": "明明你都任我摆布了", "10238": "明明你都任我摆布了", "10239": "明明你都任我摆布了", "10240": "明明你都任我摆布了", "10241": "明明你都任我摆布了", "10242": "明明你都任我摆布了", "10243": "明明你都任我摆布了", "10244": "明明你都任我摆布了", "10245": "明明你都任我摆布了", "10246": "明明你都任我摆布了", "10247": "明明你都任我摆布了", "10248": "", "10249": "", "10250": "", "10251": "", "10252": "", "10253": "", "10254": "", "10255": "", "10256": "", "10257": "", "10258": "", "10259": "身体不要紧绷着", "10260": "身体不要紧绷着", "10261": "身体不要紧绷着", "10262": "身体不要紧绷着", "10263": "身体不要紧绷着", "10264": "身体不要紧绷着", "10265": "", "10266": "", "10267": "", "10268": "", "10269": "", "10270": "身体不要紧绷着", "10271": "身体不要紧绷着", "10272": "身体不要紧绷着", "10273": "身体不要紧绷着", "10274": "身体不要紧绷着", "10275": "身体不要紧绷着", "10276": "身体不要紧绷着", "10277": "不要 怎么样呀", "10278": "不要怎么样呀", "10279": "不要 怎么样呀", "10280": "不要 怎么样呀", "10281": "不要 怎么样呀", "10282": "不要 怎么样呀", "10283": "不要 怎么样呀", "10284": "不要 怎么样呀", "10285": "不要 怎么样呀", "10286": "", "10287": "", "10288": "", "10289": "", "10290": "", "10291": "", "10292": "", "10293": "", "10294": "", "10295": "", "10296": "", "10297": "", "10298": "", "10299": "", "10300": "", "10301": "", "10302": "", "10303": "", "10304": "", "10305": "", "10306": "", "10307": "", "10308": "", "10309": "---", "10310": "松井小姐 你真可爱呢", "10311": "松井小姐 你真可爱呢", "10312": "松井小姐 你真可爱呢", "10313": "松井小姐 你真可爱呢", "10314": "松井小姐 你真可爱呢", "10315": "松井小姐 你真可爱呢", "10316": "松井小姐 你真可爱呢", "10317": "松井小姐 你真可爱呢", "10318": "松井小姐 你真可爱呢", "10319": "松井小姐 你真可爱呢", "10320": "", "10321": "", "10322": "", "10323": "", "10324": "", "10325": "不要", "10326": "不要", "10327": "不要", "10328": "不要", "10329": "", "10330": "", "10331": "不要", "10332": "不要", "10333": "不要", "10334": "不要", "10335": "不要", "10336": "不要", "10337": "", "10338": "", "10339": "", "10340": "", "10341": "", "10342": "", "10343": "", "10344": "", "10345": "", "10346": "", "10347": "", "10348": "", "10349": "", "10350": "", "10351": "", "10352": "", "10353": "", "10354": "", "10355": "", "10356": "", "10357": "", "10358": "", "10359": "", "10360": "", "10361": "", "10362": "", "10363": "---", "10364": "", "10365": "", "10366": "", "10367": "", "10368": "", "10369": "", "10370": "你很敏感呢", "10371": "你很敏感呢", "10372": "你很敏感呢", "10373": "你很敏感呢", "10374": "你很敏感呢", "10375": "", "10376": "", "10377": "", "10378": "", "10379": "不要 住手 别这样", "10380": "不要 住手 别这样", "10381": "不要 住手 别这样", "10382": "不要 住手 别这样", "10383": "不要 住手 别这样", "10384": "不要 住手 别这样", "10385": "不要 住手 别这样", "10386": "不要 住手 别这样", "10387": "不要 住手 别这样", "10388": "不要 住手 别这样", "10389": "I", "10390": "", "10391": "", "10392": "", "10393": "安静点", "10394": "安静点", "10395": "安静点", "10396": "安静点", "10397": "安静点", "10398": "", "10399": "", "10400": "", "10401": "", "10402": "", "10403": "那个地方被摸到了", "10404": "那个地方被摸到了", "10405": "那个地方被摸到了", "10406": "那个地方被摸到了", "10407": "那个地方被摸到了", "10408": "那个地方被摸到了", "10409": "那个地方被摸到了", "10410": "那个地方被摸到了", "10411": "", "10412": "", "10413": "", "10414": "", "10415": "看你一副好舒服的样子呢", "10416": "看你一副好舒服的样子呢", "10417": "看你一副好舒服的样子呢", "10418": "看你一副好舒服的样子呢", "10419": "看你一副好舒服的样子呢", "10420": "看你一副好舒服的样子呢", "10421": "看你一副好舒服的样子呢", "10422": "", "10423": "", "10424": "", "10425": "别看我了", "10426": "别看我了", "10427": "别看我了", "10428": "别看我了", "10429": "", "10430": "", "10431": "", "10432": "", "10433": "", "10434": "", "10435": "", "10436": "", "10437": "", "10438": "", "10439": "", "10440": "", "10441": "", "10442": "", "10443": "", "10444": "", "10445": "", "10446": "", "10447": "", "10448": "", "10449": "---", "10450": "", "10451": "---", "10452": "", "10453": "", "10454": "---", "10455": "不行 快住手", "10456": "不行 快住手", "10457": "不行 快住手", "10458": "不行 快住手", "10459": "不行 快住手", "10460": "不行 快住手", "10461": "---", "10462": "HTML", "10463": "", "10464": "", "10465": "", "10466": "", "10467": "background", "10468": "已经湿了", "10469": "已经湿了", "10470": "已经湿了", "10471": "已经湿了", "10472": "已经湿了", "10473": "已经湿了", "10474": "", "10475": "", "10476": "", "10477": "", "10478": "", "10479": "", "10480": "", "10481": "", "10482": "---", "10483": "", "10484": "", "10485": "", "10486": "", "10487": "", "10488": "", "10489": "", "10490": "", "10491": "", "10492": "", "10493": "", "10494": "", "10495": "", "10496": "b.o", "10497": "", "10498": "", "10499": "", "10500": "", "10501": "", "10502": "", "10503": "", "10504": "", "10505": "", "10506": "", "10507": "", "10508": "", "10509": "", "10510": "", "10511": "", "10512": "", "10513": "", "10514": "", "10515": "", "10516": "", "10517": "", "10518": "", "10519": "", "10520": "", "10521": "", "10522": "", "10523": "", "10524": "", "10525": "", "10526": "", "10527": "", "10528": "", "10529": "", "10530": "", "10531": "", "10532": "", "10533": "", "10534": "", "10535": "", "10536": "", "10537": "", "10538": "", "10539": "", "10540": "", "10541": "", "10542": "", "10543": "", "10544": "", "10545": "---", "10546": "", "10547": "", "10548": "---", "10549": "松井小姐的阴蒂和里面", "10550": "松井小姐的阴蒂和里面", "10551": "松井小姐的阴蒂和里面", "10552": "松井小姐的阴蒂和里面", "10553": "松井小姐的阴蒂和里面", "10554": "松井小姐的阴蒂和里面", "10555": "松井小姐的阴蒂和里面", "10556": "松井小姐的阴蒂和里面", "10557": "松井小姐的阴蒂和里面", "10558": "松井小姐的阴蒂和里面", "10559": "松井小姐的阴蒂和里面", "10560": "", "10561": "", "10562": "", "10563": "", "10564": "", "10565": "喜欢刺激哪里", "10566": "喜欢刺激哪里", "10567": "喜欢刺激哪里", "10568": "喜欢刺激哪里", "10569": "喜欢刺激哪里", "10570": "喜欢刺激哪里", "10571": "", "10572": "", "10573": "", "10574": "", "10575": "", "10576": "", "10577": "快回", "10578": "快回", "10579": "快回", "10580": "快回", "10581": "快回", "10582": "快回", "10583": "快回", "10584": "", "10585": "---", "10586": "---", "10587": "", "10588": "", "10589": "", "10590": "", "10591": "---", "10592": "", "10593": "", "10594": "", "10595": "---", "10596": "", "10597": "", "10598": "", "10599": "", "10600": "", "10601": "", "10602": "真啰嗦 下面都这么湿了", "10603": "真啰嗦 下面都这么湿了", "10604": "真啰嗦 下面都这么湿了", "10605": "真啰嗦 下面都这么湿了", "10606": "真啰嗦 下面都这么湿了", "10607": "真啰嗦 下面都这么湿了", "10608": "真啰嗦 下面都这么湿了", "10609": "真啰嗦 下面都这么湿了", "10610": "真啰嗦 下面都这么湿了", "10611": "真啰嗦 下面都这么湿了", "10612": "还叫什么呢", "10613": "还叫什么呢", "10614": "还叫什么呢", "10615": "还叫什么呢", "10616": "还叫什么呢", "10617": "还叫什么呢", "10618": "还叫什么呢", "10619": "---", "10620": "", "10621": "", "10622": "", "10623": "", "10624": "---", "10625": "------", "10626": "---------------", "10627": "", "10628": "", "10629": "", "10630": "", "10631": "", "10632": "", "10633": "", "10634": "", "10635": "", "10636": "", "10637": "", "10638": "", "10639": "", "10640": "", "10641": "", "10642": "", "10643": "", "10644": "", "10645": "", "10646": "", "10647": "", "10648": "", "10649": "", "10650": "", "10651": "", "10652": "", "10653": "", "10654": "", "10655": "", "10656": "", "10657": "", "10658": "", "10659": "", "10660": "", "10661": "", "10662": "", "10663": "", "10664": "---", "10665": "", "10666": "", "10667": "", "10668": "", "10669": "", "10670": "", "10671": "", "10672": "", "10673": "", "10674": "___", "10675": "", "10676": "", "10677": "", "10678": "---", "10679": "", "10680": "", "10681": "", "10682": "", "10683": "", "10684": "", "10685": "", "10686": "", "10687": "放松一点 哪里舒服", "10688": "放松一点 哪里舒服", "10689": "放松一点 哪里舒服", "10690": "放松一点 哪里舒服", "10691": "放松一点 哪里舒服", "10692": "放松一点 哪里舒服", "10693": "放松一点 哪里舒服", "10694": "放松一点哪里舒服", "10695": "放松一点 哪里舒服", "10696": "", "10697": "", "10698": "", "10699": "", "10700": "", "10701": "", "10702": "", "10703": "", "10704": "", "10705": "", "10706": "", "10707": "", "10708": "", "10709": "", "10710": "", "10711": "---", "10712": "", "10713": "", "10714": "", "10715": "", "10716": "---", "10717": "---", "10718": "你感觉怎么样", "10719": "你感觉怎么样", "10720": "你感觉怎么样", "10721": "你感觉怎么样", "10722": "你感觉怎么样", "10723": "你感觉怎么样", "10724": "", "10725": "", "10726": "", "10727": "", "10728": "", "10729": "好漂亮 好可爱", "10730": "好漂亮 好可爱", "10731": "好漂亮 好可爱", "10732": "好漂亮 好可爱", "10733": "好漂亮 好可爱", "10734": "好漂亮 好可爱", "10735": "", "10736": "", "10737": "", "10738": "", "10739": "---", "10740": "", "10741": "", "10742": "", "10743": "---", "10744": "", "10745": "", "10746": "", "10747": "", "10748": "", "10749": "---", "10750": "", "10751": "", "10752": "", "10753": "", "10754": "", "10755": "---", "10756": "", "10757": "", "10758": "", "10759": "", "10760": "", "10761": "", "10762": "", "10763": "真啰嗦", "10764": "真啰嗦", "10765": "真啰嗦", "10766": "真啰嗦", "10767": "---", "10768": "", "10769": "---", "10770": "---", "10771": "", "10772": "---", "10773": "---", "10774": "---", "10775": "", "10776": "", "10777": "", "10778": "", "10779": "", "10780": "", "10781": "", "10782": "", "10783": "", "10784": "你不安静的话 是不会让你舒服的", "10785": "你不安静的话 是不会让你舒服的", "10786": "你不安静的话 是不会让你舒服的", "10787": "你不安静的话 是不会让你舒服的", "10788": "你不安静的话 是不会让你舒服的", "10789": "你不安静的话 是不会让你舒服的", "10790": "", "10791": "", "10792": "", "10793": "", "10794": "", "10795": "", "10796": "想继续做下去吗", "10797": "想继续做下去吗", "10798": "想继续做下去吗", "10799": "想继续做下去吗", "10800": "想继续做下去吗", "10801": "想继续做下去吗", "10802": "", "10803": "---", "10804": "", "10805": "", "10806": "", "10807": "", "10808": "", "10809": "", "10810": "", "10811": "", "10812": "---", "10813": "", "10814": "", "10815": "", "10816": "", "10817": "------------", "10818": "", "10819": "", "10820": "---------------", "10821": "------------", "10822": "", "10823": "------", "10824": "", "10825": "", "10826": "", "10827": "", "10828": "", "10829": "", "10830": "---", "10831": "", "10832": "---", "10833": "", "10834": "", "10835": "我会负起责任来教育你的", "10836": "我会负起责任来教育你的", "10837": "我会负起责任来教育你的", "10838": "我会负起责任来教育你的", "10839": "我会负起责任来教育你的", "10840": "我会负起责任来教育你的", "10841": "我会负起责任来教育你的", "10842": "没事的", "10843": "没事的", "10844": "没事的", "10845": "没事的", "10846": "", "10847": "", "10848": "", "10849": "", "10850": "", "10851": "", "10852": "", "10853": "", "10854": "---", "10855": "---", "10856": "", "10857": "---", "10858": "", "10859": "---", "10860": "---", "10861": "---", "10862": "---", "10863": "", "10864": "", "10865": "", "10866": "---", "10867": "", "10868": "---", "10869": "", "10870": "", "10871": "", "10872": "", "10873": "", "10874": "", "10875": "", "10876": "", "10877": "", "10878": "", "10879": "", "10880": "", "10881": "", "10882": "", "10883": "", "10884": "", "10885": "", "10886": "", "10887": "", "10888": "", "10889": "", "10890": "", "10891": "", "10892": "", "10893": "", "10894": "", "10895": "", "10896": "", "10897": "", "10898": "", "10899": "", "10900": "", "10901": "", "10902": "", "10903": "", "10904": "", "10905": "", "10906": "", "10907": "", "10908": "", "10909": "", "10910": "", "10911": "", "10912": "", "10913": "", "10914": "", "10915": "", "10916": "", "10917": "", "10918": "", "10919": "", "10920": "", "10921": "", "10922": "", "10923": "", "10924": "", "10925": "", "10926": "", "10927": "", "10928": "", "10929": "", "10930": "", "10931": "", "10932": "", "10933": "", "10934": "", "10935": "", "10936": "", "10937": "", "10938": "住手 不要", "10939": "住手 不要", "10940": "住手 不要", "10941": "住手 不要", "10942": "住手 不要", "10943": "住手 不要", "10944": "住手 不要", "10945": "", "10946": "", "10947": "", "10948": "---------------", "10949": "", "10950": "", "10951": "background", "10952": "", "10953": "", "10954": "", "10955": "", "10956": "", "10957": "你看北冈小姐都摆出 那么羞耻的姿势了", "10958": "你看北冈小姐都摆出 那么羞耻的姿势了", "10959": "你看北冈小姐都摆出 那么羞耻的姿势了", "10960": "你看北冈小姐都摆出 那么羞耻的姿势了", "10961": "你看北冈小姐都摆出 那么羞耻的姿势了", "10962": "你看北冈小姐都摆出 那么羞耻的姿势了", "10963": "你看北冈小姐都摆出 那么羞耻的姿势了", "10964": "你看北冈小姐都摆出 那么羞耻的姿势了", "10965": "你看北冈小姐都摆出 那么羞耻的姿势了", "10966": "你看北冈小姐都摆出 那么羞耻的姿势了", "10967": "", "10968": "", "10969": "", "10970": "", "10971": "", "10972": "", "10973": "------------------", "10974": "因为你们会变成同样的姿势嘛", "10975": "因为你们会变成同样的姿势嘛", "10976": "因为你们会变成同样的姿势嘛", "10977": "因为你们会变成同样的姿势嘛", "10978": "因为你们会变成同样的姿势嘛", "10979": "因为你们会变成同样的姿势嘛", "10980": "", "10981": "", "10982": "", "10983": "", "10984": "", "10985": "", "10986": "", "10987": "", "10988": "", "10989": "", "10990": "", "10991": "", "10992": "", "10993": "", "10994": "保持这个姿势", "10995": "保持这个姿势", "10996": "保持这个姿势", "10997": "保持这个姿势", "10998": "", "10999": "", "11000": "", "11001": "", "11002": "", "11003": "吵死了", "11004": "吵死了", "11005": "吵死了", "11006": "吵死了", "11007": "吵死了", "11008": "", "11009": "", "11010": "", "11011": "", "11012": "", "11013": "", "11014": "", "11015": "", "11016": "", "11017": "---", "11018": "---", "11019": "---", "11020": "------------------", "11021": "", "11022": "", "11023": "", "11024": "", "11025": "", "11026": "", "11027": "", "11028": "", "11029": "", "11030": "", "11031": "", "11032": "", "11033": "", "11034": "", "11035": "", "11036": "", "11037": "A", "11038": "", "11039": "", "11040": "", "11041": "", "11042": "", "11043": "", "11044": "", "11045": "", "11046": "", "11047": "", "11048": "", "11049": "", "11050": "", "11051": "", "11052": "", "11053": "", "11054": "", "11055": "", "11056": "", "11057": "", "11058": "", "11059": "", "11060": "", "11061": "", "11062": "", "11063": "", "11064": "", "11065": "", "11066": "", "11067": "真的可以吗", "11068": "真的可以吗", "11069": "真的可以吗", "11070": "真的可以吗", "11071": "真的可以吗", "11072": "", "11073": "", "11074": "", "11075": "", "11076": "", "11077": "", "11078": "", "11079": "好好教育她一番吧", "11080": "好好教育她一番吧", "11081": "好好教育她一番吧", "11082": "好好教育她一番吧", "11083": "好好教育她一番吧", "11084": "好好教育她一番吧", "11085": "好好教育她一番吧", "11086": "", "11087": "", "11088": "---", "11089": "---", "11090": "松井小姐下流的样子 就让我拍下来啦", "11091": "松井小姐下流的样子 就让我拍下来啦", "11092": "松井小姐下流的样子 就让我拍下来啦", "11093": "松井小姐下流的样子 就让我拍下来啦", "11094": "松井小姐下流的样子 就让我拍下来啦", "11095": "松井小姐下流的样子 就让我拍下来啦", "11096": "松井小姐下流的样子 就让我拍下来啦", "11097": "松井小姐下流的样子 就让我拍下来啦", "11098": "松井小姐下流的样子 就让我拍下来啦", "11099": "松井小姐下流的样子 就让我拍下来啦", "11100": "松井小姐下流的样子 就让我拍下来啦", "11101": "松井小姐下流的样子 就让我拍下来啦", "11102": "松井小姐下流的样子 就让我拍下来啦", "11103": "", "11104": "", "11105": "", "11106": "", "11107": "", "11108": "", "11109": "这只是记录而已", "11110": "这只是记录而已", "11111": "这只是记录而已", "11112": "这只是记录而已", "11113": "这只是记录而已", "11114": "", "11115": "", "11116": "", "11117": "", "11118": "", "11119": "", "11120": "", "11121": "", "11122": "", "11123": "", "11124": "", "11125": "", "11126": "", "11127": "", "11128": "", "11129": "", "11130": "", "11131": "", "11132": "", "11133": "不放松点的话", "11134": "不放松点的话", "11135": "不放松点的话", "11136": "不放松点的话", "11137": "不放松点的话", "11138": "不放松点的话", "11139": "不放松点的话", "11140": "", "11141": "", "11142": "", "11143": "", "11144": "", "11145": "", "11146": "", "11147": "", "11148": "", "11149": "", "11150": "", "11151": "", "11152": "", "11153": "", "11154": "", "11155": "", "11156": "", "11157": "", "11158": "", "11159": "小穴好紧啊 要插进小穴了", "11160": "小穴好紧啊 要插进小穴了", "11161": "小穴好紧啊 要插进小穴了", "11162": "小穴好紧啊 要插进小穴了", "11163": "小穴好紧啊 要插进小穴了", "11164": "小穴好紧啊 要插进小穴了", "11165": "小穴好紧啊 要插进小穴了", "11166": "我这边也很紧呢 松井小姐", "11167": "我这边也很紧呢 松井小姐", "11168": "我这边也很紧呢 松井小姐", "11169": "我这边也很紧呢 松井小姐", "11170": "我这边也很紧呢 松井小姐", "11171": "我这边也很紧呢 松井小姐", "11172": "我这边也很紧呢 松井小姐", "11173": "我这边也很紧呢 松井小姐", "11174": "我这边也很紧呢 松井小姐", "11175": "我这边也很紧呢 松井小姐", "11176": "", "11177": "", "11178": "", "11179": "", "11180": "", "11181": "HTML", "11182": "", "11183": "", "11184": "", "11185": "", "11186": "------", "11187": "---", "11188": "", "11189": "下面这么湿 很舒服吗", "11190": "下面这么湿 很舒服吗", "11191": "下面这么湿 很舒服吗", "11192": "下面这么湿 很舒服吗", "11193": "下面这么湿 很舒服吗", "11194": "下面这么湿 很舒服吗", "11195": "下面这么湿 很舒服吗", "11196": "", "11197": "", "11198": "", "11199": "", "11200": "舒服吗", "11201": "舒服吗", "11202": "舒服吗", "11203": "舒服吗", "11204": "舒服吗", "11205": "---------------", "11206": "快一点就好了", "11207": "快一点就好了", "11208": "快一点就好了", "11209": "快一点就好了", "11210": "快一点就好了", "11211": "---", "11212": "------", "11213": "---", "11214": "---------------", "11215": "---", "11216": "------", "11217": "---", "11218": "", "11219": "", "11220": "HTML", "11221": "------", "11222": "---", "11223": "---", "11224": "---", "11225": "---", "11226": "---", "11227": "------", "11228": "", "11229": "---", "11230": "---", "11231": "---", "11232": "---", "11233": "---", "11234": "---", "11235": "---", "11236": "---", "11237": "---", "11238": "---", "11239": "---", "11240": "---", "11241": "---", "11242": "HTML", "11243": "", "11244": "", "11245": "", "11246": "", "11247": "---", "11248": "---", "11249": "", "11250": "---", "11251": "", "11252": "Markdown: ", "11253": "", "11254": "", "11255": "------------------", "11256": "", "11257": "---------------", "11258": "---", "11259": "------------------", "11260": "---", "11261": "---", "11262": "HTML", "11263": "", "11264": "", "11265": "", "11266": "---", "11267": "---", "11268": "---", "11269": "", "11270": "", "11271": "", "11272": "", "11273": "", "11274": "", "11275": "---", "11276": "", "11277": "", "11278": "------", "11279": "", "11280": "", "11281": "---", "11282": "", "11283": "", "11284": "---", "11285": "", "11286": "", "11287": "", "11288": "", "11289": "", "11290": "", "11291": "", "11292": "", "11293": "", "11294": "", "11295": "", "11296": "", "11297": "---", "11298": "", "11299": "", "11300": "", "11301": "", "11302": "", "11303": "好淫荡的味道啊", "11304": "好淫荡的味道啊", "11305": "好淫荡的味道啊", "11306": "好淫荡的味道啊", "11307": "好淫荡的味道啊", "11308": "", "11309": "", "11310": "", "11311": "", "11312": "", "11313": "", "11314": "---", "11315": "---", "11316": "------", "11317": "---", "11318": "------", "11319": "------", "11320": "---", "11321": "---", "11322": "---", "11323": "---------------", "11324": "---------------", "11325": "------", "11326": "---", "11327": "---", "11328": "---", "11329": "------------------", "11330": "---", "11331": "---", "11332": "", "11333": "------", "11334": "---", "11335": "---", "11336": "---", "11337": "---", "11338": "------", "11339": "怎么样", "11340": "怎么样", "11341": "怎么样", "11342": "怎么样", "11343": "------", "11344": "---", "11345": "", "11346": "", "11347": "---", "11348": "", "11349": "", "11350": "", "11351": "", "11352": "", "11353": "", "11354": "", "11355": "", "11356": "", "11357": "", "11358": "", "11359": "", "11360": "", "11361": "", "11362": "", "11363": "", "11364": "", "11365": "", "11366": "", "11367": "", "11368": "", "11369": "", "11370": "", "11371": "", "11372": "", "11373": "", "11374": "", "11375": "", "11376": "", "11377": "", "11378": "", "11379": "", "11380": "", "11381": "", "11382": "", "11383": "---------------", "11384": "", "11385": "------------", "11386": "", "11387": "------", "11388": "------", "11389": "------", "11390": "", "11391": "", "11392": "", "11393": "---", "11394": "---", "11395": "---", "11396": "---", "11397": "", "11398": "", "11399": "", "11400": "------", "11401": "", "11402": "", "11403": "---", "11404": "", "11405": "", "11406": "---", "11407": "------", "11408": "", "11409": "", "11410": "", "11411": "", "11412": "", "11413": "", "11414": "", "11415": "", "11416": "", "11417": "", "11418": "", "11419": "", "11420": "", "11421": "", "11422": "手指这么简单就进去了", "11423": "手指这么简单就进去了", "11424": "手指这么简单就进去了", "11425": "手指这么简单就进去了", "11426": "手指这么简单就进去了", "11427": "手指这么简单就进去了", "11428": "", "11429": "---", "11430": "", "11431": "", "11432": "", "11433": "", "11434": "", "11435": "---", "11436": "---", "11437": "", "11438": "------------", "11439": "---------------", "11440": "---------------", "11441": "------------------", "11442": "------------------", "11443": "------------", "11444": "插进小穴这么深", "11445": "插进小穴这么深", "11446": "插进小穴这么深", "11447": "插进小穴这么深", "11448": "插进小穴这么深", "11449": "插进小穴这么深", "11450": "---", "11451": "------------------", "11452": "------------------", "11453": "---", "11454": "", "11455": "", "11456": "------------------", "11457": "", "11458": "------", "11459": "", "11460": "", "11461": "------", "11462": "", "11463": "", "11464": "", "11465": "", "11466": "---", "11467": "---", "11468": "------------", "11469": "------", "11470": "---", "11471": "------------", "11472": "---------------", "11473": "", "11474": "---", "11475": "", "11476": "", "11477": "", "11478": "", "11479": "---", "11480": "", "11481": "", "11482": "---", "11483": "", "11484": "", "11485": "", "11486": "", "11487": "", "11488": "", "11489": "", "11490": "", "11491": "---", "11492": "", "11493": "", "11494": "", "11495": "", "11496": "---", "11497": "", "11498": "", "11499": "---", "11500": "", "11501": "", "11502": "", "11503": "", "11504": "", "11505": "", "11506": "", "11507": "", "11508": "---", "11509": "---", "11510": "给我看看你舒服的表情", "11511": "给我看看你舒服的表情", "11512": "给我看看你舒服的表情", "11513": "给我看看你舒服的表情", "11514": "给我看看你舒服的表情", "11515": "给我看看你舒服的表情", "11516": "给我看看你舒服的表情", "11517": "---", "11518": "", "11519": "---", "11520": "---", "11521": "", "11522": "", "11523": "---", "11524": "", "11525": "", "11526": "---", "11527": "Background", "11528": "", "11529": "", "11530": "", "11531": "", "11532": "", "11533": "", "11534": "", "11535": "", "11536": "", "11537": "", "11538": "", "11539": "", "11540": "", "11541": "", "11542": "", "11543": "", "11544": "", "11545": "", "11546": "", "11547": "", "11548": "", "11549": "", "11550": "", "11551": "", "11552": "", "11553": "", "11554": "", "11555": "", "11556": "", "11557": "---", "11558": "", "11559": "", "11560": "------", "11561": "", "11562": "---", "11563": "", "11564": "", "11565": "", "11566": "", "11567": "", "11568": "", "11569": "", "11570": "", "11571": "", "11572": "", "11573": "", "11574": "---", "11575": "---", "11576": "", "11577": "", "11578": "---", "11579": "", "11580": "", "11581": "", "11582": "", "11583": "", "11584": "", "11585": "", "11586": "", "11587": "果然还是这边吗", "11588": "果然还是这边吗", "11589": "果然还是这边吗", "11590": "果然还是这边吗", "11591": "果然还是这边吗", "11592": "", "11593": "", "11594": "", "11595": "快把手拿开", "11596": "快把手拿开", "11597": "快把手拿开", "11598": "快把手拿开", "11599": "快把手拿开", "11600": "", "11601": "", "11602": "", "11603": "", "11604": "", "11605": "---------------", "11606": "", "11607": "", "11608": "---", "11609": "", "11610": "", "11611": "HTML", "11612": "", "11613": "---", "11614": "", "11615": "", "11616": "", "11617": "---", "11618": "", "11619": "---", "11620": "", "11621": "", "11622": "98堂[色花堂] 永久地址 489155.com", "11623": "98堂[色花堂] 永久地址 489155.com", "11624": "98堂[色花堂] 永久地址 489155.com", "11625": "98堂[色花堂] 永久地址 489155.com", "11626": "98堂[色花堂] 永久地址 489155.com", "11627": "98堂[色花堂]永久地址489155.com", "11628": "---", "11629": "---", "11630": "", "11631": "", "11632": "", "11633": "---", "11634": "", "11635": "", "11636": "", "11637": "", "11638": "", "11639": "", "11640": "", "11641": "", "11642": "", "11643": "", "11644": "", "11645": "---", "11646": "", "11647": "", "11648": "再把腿张开一点", "11649": "再把腿张开一点", "11650": "再把腿张开一点", "11651": "再把腿张开一点", "11652": "再把腿张开一点", "11653": "再把腿张开一点", "11654": "再把腿张开一点", "11655": "", "11656": "", "11657": "---", "11658": "", "11659": "", "11660": "", "11661": "", "11662": "给我看看屁股", "11663": "给我看看屁股", "11664": "给我看看屁股", "11665": "给我看看屁股", "11666": "给我看看屁股", "11667": "给我看看屁股", "11668": "", "11669": "", "11670": "", "11671": "", "11672": "", "11673": "", "11674": "", "11675": "", "11676": "", "11677": "", "11678": "", "11679": "", "11680": "", "11681": "", "11682": "", "11683": "", "11684": "", "11685": "", "11686": "", "11687": "", "11688": "", "11689": "", "11690": "", "11691": "", "11692": "", "11693": "", "11694": "", "11695": "", "11696": "", "11697": "", "11698": "", "11699": "", "11700": "", "11701": "", "11702": "", "11703": "", "11704": "", "11705": "", "11706": "", "11707": "", "11708": "", "11709": "", "11710": "", "11711": "", "11712": "", "11713": "", "11714": "", "11715": "", "11716": "", "11717": "", "11718": "", "11719": "", "11720": "", "11721": "", "11722": "", "11723": "", "11724": "", "11725": "我会把松井小姐重要的地方里流出来的汁液全部吸干的", "11726": "我会把松井小姐重要的地方里流出来的汁液全部吸干的", "11727": "我会把松井小姐重要的地方里流出来的汁液全部吸干的", "11728": "我会把松井小姐重要的地方里流出来的汁液全部吸干的", "11729": "我会把松井小姐重要的地方里流出来的汁液全部吸干的", "11730": "我会把松井小姐重要的地方里流出来的汁液全部吸干的", "11731": "我会把松井小姐重要的地方里流出来的汁液全部吸干的", "11732": "我会把松井小姐重要的地方里流出来的汁液全部吸干的", "11733": "我会把松井小姐重要的地方里流出来的汁液全部吸干的", "11734": "我会把松井小姐重要的地方里流出来的汁液全部吸干的", "11735": "我会把松井小姐重要的地方里流出来的汁液全部吸干的", "11736": "我会把松井小姐重要的地方里流出来的汁液全部吸干的", "11737": "我会把松井小姐重要的地方里流出来的汁液全部吸干的", "11738": "我会把松井小姐重要的地方里流出来的汁液全部吸干的", "11739": "我会把松井小姐重要的地方里流出来的汁液全部吸干的", "11740": "", "11741": "", "11742": "", "11743": "", "11744": "", "11745": "", "11746": "------", "11747": "", "11748": "", "11749": "", "11750": "", "11751": "", "11752": "", "11753": "---", "11754": "", "11755": "", "11756": "---", "11757": "---", "11758": "", "11759": "", "11760": "---------------", "11761": "", "11762": "", "11763": "", "11764": "---", "11765": "---", "11766": "", "11767": "", "11768": "---", "11769": "", "11770": "------", "11771": "", "11772": "", "11773": "", "11774": "", "11775": "", "11776": "", "11777": "", "11778": "---", "11779": "不要", "11780": "不要", "11781": "不要", "11782": "不要", "11783": "", "11784": "", "11785": "", "11786": "", "11787": "", "11788": "", "11789": "---", "11790": "", "11791": "", "11792": "", "11793": "", "11794": "", "11795": "", "11796": "---------------", "11797": "------------", "11798": "", "11799": "", "11800": "---", "11801": "---", "11802": "", "11803": "---", "11804": "---", "11805": "---", "11806": "---", "11807": "---", "11808": "---", "11809": "---", "11810": "", "11811": "", "11812": "---", "11813": "", "11814": "", "11815": "", "11816": "", "11817": "---", "11818": "", "11819": "HTML", "11820": "", "11821": "", "11822": "", "11823": "", "11824": "---------------", "11825": "", "11826": "", "11827": "", "11828": "---", "11829": "", "11830": "", "11831": "", "11832": "", "11833": "", "11834": "", "11835": "", "11836": "", "11837": "", "11838": "", "11839": "------", "11840": "---------------", "11841": "---------------", "11842": "", "11843": "---", "11844": "---", "11845": "---------------", "11846": "---", "11847": "", "11848": "------", "11849": "", "11850": "", "11851": "", "11852": "---", "11853": "", "11854": "---", "11855": "", "11856": "---", "11857": "---", "11858": "", "11859": "", "11860": "", "11861": "差不多想要肉棒了吧", "11862": "差不多想要肉棒了吧", "11863": "差不多想要肉棒了吧", "11864": "差不多想要肉棒了吧", "11865": "差不多想要肉棒了吧", "11866": "差不多想要肉棒了吧", "11867": "---", "11868": "---", "11869": "", "11870": "", "11871": "", "11872": "", "11873": "", "11874": "你自己插进小穴吧", "11875": "你自己插进小穴吧", "11876": "你自己插进小穴吧", "11877": "你自己插进小穴吧", "11878": "你自己插进小穴吧", "11879": "", "11880": "", "11881": "", "11882": "", "11883": "", "11884": "", "11885": "", "11886": "", "11887": "", "11888": "", "11889": "", "11890": "床", "11891": "", "11892": "", "11893": "", "11894": "", "11895": "", "11896": "", "11897": "", "11898": "", "11899": "", "11900": "1", "11901": "1", "11902": "床", "11903": "210 康林", "11904": "北岳 顾月", "11905": "实习 黑林", "11906": "医补", "11907": "", "11908": "", "11909": "", "11910": "", "11911": "", "11912": "", "11913": "很好 就是这样 自己动起来", "11914": "很好 就是这样 自己动起来", "11915": "很好 就是这样 自己动起来", "11916": "很好 就是这样 自己动起来", "11917": "很好 就是这样 自己动起来", "11918": "很好 就是这样 自己动起来", "11919": "很好 就是这样 自己动起来", "11920": "很好 就是这样 自己动起来", "11921": "很好 就是这样 自己动起来", "11922": "很好 就是这样 自己动起来", "11923": "很好 就是这样 自己动起来", "11924": "", "11925": "", "11926": "你看腰动起来了", "11927": "你看腰动起来了", "11928": "你看腰动起来了", "11929": "你看腰动起来了", "11930": "你看腰动起来了", "11931": "你看腰动起来了", "11932": "D:0", "11933": "d:q", "11934": "", "11935": "", "11936": "", "11937": "", "11938": "", "11939": "DQ", "11940": "", "11941": "", "11942": "", "11943": "", "11944": "DQDQ", "11945": "", "11946": "", "11947": "", "11948": "DSQ", "11949": "", "11950": "", "11951": "", "11952": "", "11953": "", "11954": "", "11955": "", "11956": "", "11957": "", "11958": "", "11959": "", "11960": "", "11961": "", "11962": "---", "11963": "", "11964": "", "11965": "", "11966": "", "11967": "", "11968": "", "11969": "", "11970": "", "11971": "", "11972": "", "11973": "", "11974": "", "11975": "", "11976": "", "11977": "", "11978": "", "11979": "", "11980": "", "11981": "", "11982": "", "11983": "", "11984": "", "11985": "", "11986": "", "11987": "", "11988": "", "11989": "", "11990": "", "11991": "", "11992": "", "11993": "", "11994": "", "11995": "", "11996": "", "11997": "", "11998": "", "11999": "", "12000": "", "12001": "不要看", "12002": "不要看", "12003": "不要看", "12004": "不要看", "12005": "不要看", "12006": "", "12007": "", "12008": "", "12009": "", "12010": "", "12011": "", "12012": "", "12013": "", "12014": "", "12015": "", "12016": "", "12017": "", "12018": "", "12019": "", "12020": "", "12021": "你高潮了吗", "12022": "你高潮了吗", "12023": "你高潮了吗", "12024": "你高潮了吗", "12025": "你高潮了吗", "12026": "", "12027": "", "12028": "", "12029": "", "12030": "", "12031": "", "12032": "", "12033": "", "12034": "", "12035": "", "12036": "", "12037": "", "12038": "", "12039": "", "12040": "", "12041": "", "12042": "", "12043": "", "12044": "", "12045": "", "12046": "", "12047": "", "12048": "", "12049": "", "12050": "", "12051": "", "12052": "", "12053": "", "12054": "", "12055": "", "12056": "", "12057": "", "12058": "", "12059": "", "12060": "", "12061": "", "12062": "", "12063": "---------------", "12064": "", "12065": "", "12066": "", "12067": "---", "12068": "", "12069": "", "12070": "---", "12071": "", "12072": "", "12073": "", "12074": "", "12075": "", "12076": "---", "12077": "", "12078": "你给我坐起来", "12079": "你给我坐起来", "12080": "你给我坐起来", "12081": "你给我坐起来", "12082": "你给我坐起来", "12083": "", "12084": "", "12085": "", "12086": "", "12087": "", "12088": "", "12089": "", "12090": "松井小姐 这边也 要插进小穴吧", "12091": "松井小姐 这边也 要插进小穴吧", "12092": "松井小姐 这边也 要插进小穴吧", "12093": "松井小姐 这边也 要插进小穴吧", "12094": "松井小姐 这边也 要插进小穴吧", "12095": "松井小姐 这边也 要插进小穴吧", "12096": "松井小姐 这边也 要插进小穴吧", "12097": "松井小姐 这边也 要插进小穴吧", "12098": "松井小姐 这边也 要插进小穴吧", "12099": "松井小姐 这边也 要插进小穴吧", "12100": "4", "12101": "", "12102": "4.0", "12103": "", "12104": "", "12105": "", "12106": "", "12107": "", "12108": "", "12109": "怎么了 不愿意吗", "12110": "怎么了 不愿意吗", "12111": "怎么了 不愿意吗", "12112": "怎么了 不愿意吗", "12113": "怎么了 不愿意吗", "12114": "怎么了 不愿意吗", "12115": "怎么了 不愿意吗", "12116": "怎么了 不愿意吗", "12117": "", "12118": "---", "12119": "", "12120": "偶尔和你这样 表里不一的女人做也不错", "12121": "偶尔和你这样 表里不一的女人做也不错", "12122": "偶尔和你这样 表里不一的女人做也不错", "12123": "偶尔和你这样 表里不一的女人做也不错", "12124": "偶尔和你这样 表里不一的女人做也不错", "12125": "偶尔和你这样 表里不一的女人做也不错", "12126": "偶尔和你这样 表里不一的女人做也不错", "12127": "偶尔和你这样 表里不一的女人做也不错", "12128": "偶尔和你这样 表里不一的女人做也不错", "12129": "偶尔和你这样 表里不一的女人做也不错", "12130": "偶尔和你这样 表里不一的女人做也不错", "12131": "偶尔和你这样 表里不一的女人做也不错", "12132": "偶尔和你这样 表里不一的女人做也不错", "12133": "偶尔和你这样 表里不一的女人做也不错", "12134": "", "12135": "", "12136": "", "12137": "我要强行插进小穴了", "12138": "我要强行插进小穴了", "12139": "我要强行插进小穴了", "12140": "我要强行插进小穴了", "12141": "我要强行插进小穴了", "12142": "我要强行插进小穴了", "12143": "我要强行插进小穴了", "12144": "", "12145": "", "12146": "", "12147": "", "12148": "", "12149": "---", "12150": "", "12151": "", "12152": "---", "12153": "", "12154": "------------", "12155": "---", "12156": "", "12157": "", "12158": "", "12159": "---", "12160": "", "12161": "小穴好紧", "12162": "小穴好紧", "12163": "小穴好紧", "12164": "小穴好紧", "12165": "小穴好紧", "12166": "", "12167": "", "12168": "", "12169": "---", "12170": "", "12171": "", "12172": "", "12173": "", "12174": "要动起来了", "12175": "要动起来了", "12176": "要动起来了", "12177": "要动起来了", "12178": "要动起来了", "12179": "要动起来了", "12180": "要动起来了", "12181": "", "12182": "", "12183": "---", "12184": "", "12185": "", "12186": "", "12187": "---", "12188": "", "12189": "---", "12190": "---", "12191": "松井小姐", "12192": "松井小姐", "12193": "松井小姐", "12194": "松井小姐", "12195": "意外的是被强迫动起来 你感觉还不错呢", "12196": "意外的是被强迫动起来 你感觉还不错呢", "12197": "意外的是被强迫动起来 你感觉还不错呢", "12198": "意外的是被强迫动起来 你感觉还不错呢", "12199": "意外的是被强迫动起来 你感觉还不错呢", "12200": "意外的是被强迫动起来 你感觉还不错呢", "12201": "意外的是被强迫动起来 你感觉还不错呢", "12202": "意外的是被强迫动起来 你感觉还不错呢", "12203": "意外的是被强迫动起来 你感觉还不错呢", "12204": "意外的是被强迫动起来 你感觉还不错呢", "12205": "意外的是被强迫动起来 你感觉还不错呢", "12206": "", "12207": "", "12208": "", "12209": "", "12210": "", "12211": "", "12212": "", "12213": "", "12214": "", "12215": "", "12216": "", "12217": "", "12218": "", "12219": "", "12220": "------", "12221": "--- ---", "12222": "------", "12223": "---------------", "12224": "---", "12225": "---", "12226": "---", "12227": "---", "12228": "------------", "12229": "---", "12230": "---", "12231": "---", "12232": "------", "12233": "------", "12234": "------------------", "12235": "---", "12236": "------", "12237": "---", "12238": "---", "12239": "---", "12240": "你的情况非常好呢", "12241": "你的情况非常好呢", "12242": "你的情况非常好呢", "12243": "你的情况非常好呢", "12244": "你的情况非常好呢", "12245": "你的情况非常好呢", "12246": "你的情况非常好呢", "12247": "---", "12248": "------------------", "12249": "---", "12250": "---", "12251": "------", "12252": "------------------", "12253": "---", "12254": "---", "12255": "", "12256": "", "12257": "---", "12258": "---------------", "12259": "", "12260": "---", "12261": "---", "12262": "", "12263": "", "12264": "---", "12265": "---", "12266": "", "12267": "", "12268": "", "12269": "---", "12270": "", "12271": "---", "12272": "", "12273": "", "12274": "", "12275": "", "12276": "", "12277": "", "12278": "", "12279": "", "12280": "", "12281": "", "12282": "", "12283": "", "12284": "", "12285": "", "12286": "", "12287": "", "12288": "", "12289": "", "12290": "", "12291": "", "12292": "", "12293": "", "12294": "", "12295": "", "12296": "", "12297": "", "12298": "", "12299": "", "12300": "", "12301": "---------------", "12302": "---------------", "12303": "---------------", "12304": "------------------", "12305": "------", "12306": "------", "12307": "", "12308": "---------------", "12309": "---------------", "12310": "", "12311": "", "12312": "", "12313": "---", "12314": "------------------", "12315": "", "12316": "", "12317": "", "12318": "------------------", "12319": "", "12320": "", "12321": "", "12322": "---", "12323": "------------------", "12324": "", "12325": "", "12326": "", "12327": "", "12328": "", "12329": "", "12330": "", "12331": "", "12332": "------", "12333": "------", "12334": "---", "12335": "", "12336": "", "12337": "------", "12338": "", "12339": "", "12340": "", "12341": "", "12342": "", "12343": "", "12344": "", "12345": "---", "12346": "", "12347": "", "12348": "", "12349": "", "12350": "", "12351": "", "12352": "", "12353": "", "12354": "", "12355": "", "12356": "", "12357": "", "12358": "", "12359": "", "12360": "", "12361": "", "12362": "", "12363": "", "12364": "---", "12365": "------------------", "12366": "---", "12367": "---", "12368": "---", "12369": "---", "12370": "---", "12371": "", "12372": "------", "12373": "---", "12374": "---", "12375": "------------------", "12376": "------------", "12377": "我再插深一点吧", "12378": "我再插深一点吧", "12379": "我再插深一点吧", "12380": "我再插深一点吧", "12381": "我再插深一点吧", "12382": "我再插深一点吧", "12383": "我再插深一点吧", "12384": "我再插深一点吧", "12385": "", "12386": "", "12387": "", "12388": "---------------", "12389": "", "12390": "---", "12391": "------------------", "12392": "---------------", "12393": "", "12394": "---", "12395": "---", "12396": "", "12397": "---", "12398": "---", "12399": "---", "12400": "---", "12401": "---", "12402": "---", "12403": "------------------", "12404": "------", "12405": "------------------", "12406": "------", "12407": "---", "12408": "---------------", "12409": "------", "12410": "---------------", "12411": "---------------", "12412": "---------------", "12413": "", "12414": "------", "12415": "------", "12416": "---------------", "12417": "---", "12418": "------", "12419": "------", "12420": "------", "12421": "------------------", "12422": "------------------", "12423": "------------", "12424": "------", "12425": "------------", "12426": "", "12427": "------", "12428": "---------------", "12429": "", "12430": "------------", "12431": "", "12432": "------------------", "12433": "---", "12434": "------------------", "12435": "------", "12436": "--- ---", "12437": "---", "12438": "---", "12439": "---", "12440": "---", "12441": "", "12442": "---", "12443": "------", "12444": "------------------", "12445": "", "12446": "", "12447": "松井小姐", "12448": "松井小姐", "12449": "松井小姐", "12450": "松井小姐", "12451": "松井小姐", "12452": "Background", "12453": "", "12454": "", "12455": "", "12456": "因为你必须能够应对各种各样的情况呢", "12457": "因为你必须能够应对各种各样的情况呢", "12458": "因为你必须能够应对各种各样的情况呢", "12459": "因为你必须能够应对各种各样的情况呢", "12460": "因为你必须能够应对各种各样的情况呢", "12461": "因为你必须能够应对各种各样的情况呢", "12462": "因为你必须能够应对各种各样的情况呢", "12463": "因为你必须能够应对各种各样的情况呢", "12464": "因为你必须能够应对各种各样的情况呢", "12465": "因为你必须能够应对各种各样的情况呢", "12466": "因为你必须能够应对各种各样的情况呢", "12467": "因为你必须能够应对各种各样的情况呢", "12468": "因为你必须能够应对各种各样的情况呢", "12469": "", "12470": "", "12471": "不要 不可以", "12472": "不要 不可以", "12473": "不要 不可以", "12474": "泷本 这次换你来这边做吧", "12475": "泷本 这次换你来这边做吧", "12476": "泷本 这次换你来这边做吧", "12477": "泷本 这次换你来这边做吧", "12478": "泷本 这次换你来这边做吧", "12479": "泷本 这次换你来这边做吧", "12480": "泷本 这次换你来这边做吧", "12481": "泷本 这次换你来这边做吧", "12482": "泷本 这次换你来这边做吧", "12483": "泷本 这次换你来这边做吧", "12484": "泷本 这次换你来这边做吧", "12485": "", "12486": "不要 不可以过来 不要", "12487": "不要 不可以过来 不要", "12488": "不要 不可以过来 不要", "12489": "不要 不可以过来 不要", "12490": "不要 不可以过来 不要", "12491": "不要 不可以过来 不要", "12492": "不要 不可以过来 不要", "12493": "不要 不可以过来 不要", "12494": "不要 不可以过来 不要", "12495": "不要 不可以过来 不要", "12496": "", "12497": "", "12498": "", "12499": "", "12500": "", "12501": "", "12502": "", "12503": "", "12504": "", "12505": "", "12506": "", "12507": "", "12508": "那么 会是怎样的呢", "12509": "那么 会是怎样的呢", "12510": "那么 会是怎样的呢", "12511": "那么 会是怎样的呢", "12512": "那么 会是怎样的呢", "12513": "那么 会是怎样的呢", "12514": "", "12515": "", "12516": "", "12517": "", "12518": "", "12519": "把腿张开点", "12520": "把腿张开点", "12521": "把腿张开点", "12522": "把腿张开点", "12523": "泷本君 好久不见啦", "12524": "泷本君 好久不见啦", "12525": "泷本君 好久不见啦", "12526": "泷本君 好久不见啦", "12527": "泷本君 好久不见啦", "12528": "泷本君 好久不见啦", "12529": "", "12530": "", "12531": "", "12532": "", "12533": "", "12534": "", "12535": "---", "12536": "", "12537": "---", "12538": "---", "12539": "看吧 不错吧", "12540": "看吧 不错吧", "12541": "看吧 不错吧", "12542": "看吧 不错吧", "12543": "看吧 不错吧", "12544": "看吧 不错吧", "12545": "", "12546": "", "12547": "", "12548": "我也要插进小穴了 不要", "12549": "我也要插进小穴了 不要", "12550": "我也要插进小穴了 不要", "12551": "我也要插进小穴了 不要", "12552": "我也要插进小穴了 不要", "12553": "我也要插进小穴了 不要", "12554": "我也要插进小穴了 不要", "12555": "", "12556": "", "12557": "", "12558": "", "12559": "", "12560": "---", "12561": "", "12562": "---", "12563": "---------------", "12564": "", "12565": "", "12566": "小穴好紧 小穴好紧", "12567": "小穴好紧 小穴好紧", "12568": "小穴好紧 小穴好紧", "12569": "小穴好紧 小穴好紧", "12570": "---", "12571": "", "12572": "", "12573": "---", "12574": "---", "12575": "", "12576": "", "12577": "", "12578": "", "12579": "", "12580": "---", "12581": "---", "12582": "---", "12583": "---", "12584": "---", "12585": "---", "12586": "---", "12587": "---", "12588": "", "12589": "---", "12590": "---", "12591": "", "12592": "---", "12593": "", "12594": "", "12595": "---", "12596": "", "12597": "你还没习惯吗", "12598": "你还没习惯吗", "12599": "你还没习惯吗", "12600": "你还没习惯吗", "12601": "你还没习惯吗", "12602": "你还没习惯吗", "12603": "", "12604": "", "12605": "---", "12606": "太棒了", "12607": "太棒了", "12608": "太棒了", "12609": "太棒了", "12610": "太棒了", "12611": "---", "12612": "---", "12613": "", "12614": "---", "12615": "", "12616": "---------------", "12617": "---------------", "12618": "", "12619": "------", "12620": "---", "12621": "------", "12622": "------------------", "12623": "", "12624": "", "12625": "", "12626": "", "12627": "", "12628": "", "12629": "", "12630": "", "12631": "", "12632": "", "12633": "", "12634": "", "12635": "", "12636": "", "12637": "", "12638": "这表情也不错", "12639": "这表情也不错", "12640": "这表情也不错", "12641": "这表情也不错", "12642": "这表情也不错", "12643": "", "12644": "", "12645": "---", "12646": "", "12647": "", "12648": "", "12649": "", "12650": "", "12651": "---------------", "12652": "", "12653": "---", "12654": "", "12655": "", "12656": "", "12657": "", "12658": "", "12659": "", "12660": "", "12661": "", "12662": "", "12663": "", "12664": "", "12665": "", "12666": "", "12667": "", "12668": "", "12669": "", "12670": "", "12671": "", "12672": "", "12673": "", "12674": "我就让你听听抽插的声音吧", "12675": "我就让你听听抽插的声音吧", "12676": "我就让你听听抽插的声音吧", "12677": "我就让你听听抽插的声音吧", "12678": "我就让你听听抽插的声音吧", "12679": "我就让你听听抽插的声音吧", "12680": "我就让你听听抽插的声音吧", "12681": "我就让你听听抽插的声音吧", "12682": "", "12683": "", "12684": "", "12685": "", "12686": "", "12687": "", "12688": "", "12689": "", "12690": "", "12691": "", "12692": "", "12693": "", "12694": "", "12695": "background", "12696": "", "12697": "", "12698": "", "12699": "", "12700": "", "12701": "------------------", "12702": "", "12703": "---", "12704": "", "12705": "------", "12706": "", "12707": "", "12708": "---------------", "12709": "------------------", "12710": "---", "12711": "------", "12712": "------------------", "12713": "", "12714": "---------------", "12715": "", "12716": "", "12717": "", "12718": "", "12719": "", "12720": "", "12721": "", "12722": "", "12723": "", "12724": "", "12725": "", "12726": "", "12727": "", "12728": "", "12729": "", "12730": "", "12731": "", "12732": "", "12733": "", "12734": "", "12735": "", "12736": "", "12737": "background", "12738": "", "12739": "", "12740": "", "12741": "", "12742": "", "12743": "", "12744": "", "12745": "", "12746": "", "12747": "Background", "12748": "", "12749": "", "12750": "", "12751": "------", "12752": "", "12753": "", "12754": "泷本 那边的情况怎么样", "12755": "泷本 那边的情况怎么样", "12756": "泷本 那边的情况怎么样", "12757": "泷本 那边的情况怎么样", "12758": "泷本那边的情况怎么样", "12759": "泷本 那边的情况怎么样", "12760": "泷本那边的情况怎么样", "12761": "------", "12762": "------", "12763": "", "12764": "", "12765": "", "12766": "这样子会让我很兴奋", "12767": "这样子会让我很兴奋", "12768": "这样子会让我很兴奋", "12769": "这样子会让我很兴奋", "12770": "这样子会让我很兴奋", "12771": "这样子会让我很兴奋", "12772": "这样子会让我很兴奋", "12773": "", "12774": "---", "12775": "", "12776": "", "12777": "你看着我的眼睛 加油哦", "12778": "你看着我的眼睛 加油哦", "12779": "你看着我的眼睛 加油哦", "12780": "你看着我的眼睛 加油哦", "12781": "你看着我的眼睛 加油哦", "12782": "你看着我的眼睛 加油哦", "12783": "你看着我的眼睛 加油哦", "12784": "你看着我的眼睛 加油哦", "12785": "你看着我的眼睛 加油哦", "12786": "你看着我的眼睛 加油哦", "12787": "你看着我的眼睛 加油哦", "12788": "你看着我的眼睛 加油哦", "12789": "", "12790": "", "12791": "", "12792": "", "12793": "", "12794": "", "12795": "", "12796": "", "12797": "", "12798": "", "12799": "", "12800": "", "12801": "", "12802": "", "12803": "", "12804": "", "12805": "", "12806": "", "12807": "", "12808": "", "12809": "", "12810": "", "12811": "", "12812": "", "12813": "", "12814": "", "12815": "", "12816": "", "12817": "身体趴起来", "12818": "身体趴起来", "12819": "身体趴起来", "12820": "身体趴起来", "12821": "---", "12822": "", "12823": "------------------", "12824": "---------------", "12825": "", "12826": "", "12827": "", "12828": "", "12829": "", "12830": "", "12831": "", "12832": "", "12833": "", "12834": "---", "12835": "", "12836": "", "12837": "", "12838": "", "12839": "", "12840": "", "12841": "", "12842": "", "12843": "", "12844": "", "12845": "", "12846": "", "12847": "", "12848": "", "12849": "", "12850": "", "12851": "", "12852": "把头抬起来 好好看着松井小姐", "12853": "把头抬起来 好好看着松井小姐", "12854": "把头抬起来 好好看着松井小姐", "12855": "把头抬起来 好好看着松井小姐", "12856": "把头抬起来 好好看着松井小姐", "12857": "把头抬起来 好好看着松井小姐", "12858": "把头抬起来 好好看着松井小姐", "12859": "把头抬起来 好好看着松井小姐", "12860": "", "12861": "", "12862": "", "12863": "", "12864": "", "12865": "松井小姐", "12866": "松井小姐", "12867": "松井小姐", "12868": "松井小姐", "12869": "松井小姐", "12870": "", "12871": "", "12872": "不要", "12873": "不要", "12874": "不要", "12875": "不要", "12876": "", "12877": "", "12878": "", "12879": "再让你听听抽插的声音吧", "12880": "再让你听听抽插的声音吧", "12881": "再让你听听抽插的声音吧", "12882": "再让你听听抽插的声音吧", "12883": "再让你听听抽插的声音吧", "12884": "再让你听听抽插的声音吧", "12885": "再让你听听抽插的声音吧", "12886": "", "12887": "", "12888": "", "12889": "", "12890": "", "12891": "", "12892": "", "12893": "", "12894": "", "12895": "", "12896": "------", "12897": "", "12898": "---", "12899": "---", "12900": "", "12901": "---", "12902": "", "12903": "", "12904": "", "12905": "", "12906": "", "12907": "", "12908": "", "12909": "---", "12910": "", "12911": "---", "12912": "---", "12913": "", "12914": "", "12915": "", "12916": "", "12917": "", "12918": "", "12919": "", "12920": "", "12921": "", "12922": "", "12923": "", "12924": "", "12925": "", "12926": "", "12927": "", "12928": "", "12929": "", "12930": "---", "12931": "", "12932": "---", "12933": "", "12934": "", "12935": "", "12936": "把手给我吧", "12937": "把手给我吧", "12938": "把手给我吧", "12939": "把手给我吧", "12940": "把手给我吧", "12941": "把手给我吧", "12942": "", "12943": "", "12944": "", "12945": "", "12946": "", "12947": "", "12948": "", "12949": "", "12950": "", "12951": "你看那边", "12952": "你看那边", "12953": "你看那边", "12954": "你看那边", "12955": "你看那边", "12956": "你看那边", "12957": "", "12958": "", "12959": "还是想近距离看吗", "12960": "还是想近距离看吗", "12961": "还是想近距离看吗", "12962": "还是想近距离看吗", "12963": "还是想近距离看吗", "12964": "", "12965": "", "12966": "", "12967": "", "12968": "", "12969": "", "12970": "", "12971": "", "12972": "", "12973": "", "12974": "要不要近距离看看", "12975": "要不要近距离看看", "12976": "要不要近距离看看", "12977": "要不要近距离看看", "12978": "要不要近距离看看", "12979": "", "12980": "", "12981": "", "12982": "", "12983": "", "12984": "", "12985": "", "12986": "", "12987": "", "12988": "", "12989": "", "12990": "", "12991": "", "12992": "", "12993": "", "12994": "", "12995": "跟前辈关系很好呢 互相看着对方", "12996": "跟前辈关系很好呢 互相看着对方", "12997": "跟前辈关系很好呢 互相看着对方", "12998": "跟前辈关系很好呢 互相看着对方", "12999": "跟前辈关系很好呢 互相看着对方", "13000": "跟前辈关系很好呢 互相看着对方", "13001": "跟前辈关系很好呢 互相看着对方", "13002": "跟前辈关系很好呢 互相看着对方", "13003": "跟前辈关系很好呢 互相看着对方", "13004": "跟前辈关系很好呢 互相看着对方", "13005": "", "13006": "", "13007": "", "13008": "", "13009": "", "13010": "", "13011": "", "13012": "", "13013": "", "13014": "---", "13015": "", "13016": "", "13017": "", "13018": "", "13019": "", "13020": "", "13021": "", "13022": "", "13023": "", "13024": "", "13025": "", "13026": "", "13027": "", "13028": "", "13029": "", "13030": "", "13031": "", "13032": "", "13033": "", "13034": "", "13035": "", "13036": "", "13037": "", "13038": "", "13039": "你要看着北冈小姐的脸 你们俩相互看看嘛", "13040": "你要看着北冈小姐的脸你们俩相互看看嘛", "13041": "你要看着北冈小姐的脸 你们俩相互看看嘛", "13042": "你要看着北冈小姐的脸 你们俩相互看看嘛", "13043": "你要看着北冈小姐的脸 你们俩相互看看嘛", "13044": "你要看着北冈小姐的脸 你们俩相互看看嘛", "13045": "你要看着北冈小姐的脸你们俩相互看看嘛", "13046": "你要看着北冈小姐的脸 你们俩相互看看嘛", "13047": "你要看着北冈小姐的脸 你们俩相互看看嘛", "13048": "你要看着北冈小姐的脸你们俩相互看看嘛", "13049": "", "13050": "多多关照前辈后辈", "13051": "多多关照前辈后辈", "13052": "多多关照前辈后辈", "13053": "多多关照前辈后辈", "13054": "多多关照前辈后辈", "13055": "你们都很舒服吧", "13056": "你们都很舒服吧", "13057": "你们都很舒服吧", "13058": "你们都很舒服吧", "13059": "你们都很舒服吧", "13060": "你们都很舒服吧", "13061": "", "13062": "", "13063": "", "13064": "", "13065": "", "13066": "", "13067": "", "13068": "", "13069": "", "13070": "", "13071": "", "13072": "", "13073": "---", "13074": "", "13075": "", "13076": "", "13077": "", "13078": "", "13079": "", "13080": "", "13081": "", "13082": "", "13083": "", "13084": "", "13085": "", "13086": "", "13087": "", "13088": "", "13089": "", "13090": "", "13091": "", "13092": "", "13093": "", "13094": "", "13095": "", "13096": "", "13097": "", "13098": "", "13099": "", "13100": "", "13101": "", "13102": "", "13103": "", "13104": "", "13105": "", "13106": "", "13107": "", "13108": "", "13109": "", "13110": "", "13111": "", "13112": "x", "13113": "", "13114": "", "13115": "", "13116": "", "13117": "", "13118": "", "13119": "", "13120": "", "13121": "", "13122": "", "13123": "", "13124": "---", "13125": "", "13126": "", "13127": "", "13128": "", "13129": "松井小姐 怎么样", "13130": "松井小姐 怎么样", "13131": "松井小姐 怎么样", "13132": "松井小姐 怎么样", "13133": "松井小姐 怎么样", "13134": "松井小姐 怎么样", "13135": "特殊业务讲座还行吧", "13136": "特殊业务讲座还行吧", "13137": "特殊业务讲座还行吧", "13138": "特殊业务讲座还行吧", "13139": "特殊业务讲座还行吧", "13140": "特殊业务讲座还行吧", "13141": "特殊业务讲座还行吧", "13142": "特殊业务讲座还行吧", "13143": "", "13144": "", "13145": "", "13146": "", "13147": "", "13148": "北冈小姐也是 把你的优点教给她吧", "13149": "北冈小姐也是 把你的优点教给她吧", "13150": "北冈小姐也是 把你的优点教给她吧", "13151": "北冈小姐也是 把你的优点教给她吧", "13152": "北冈小姐也是 把你的优点教给她吧", "13153": "北冈小姐也是把你的优点教给她吧", "13154": "看来你们俩挺合得来的", "13155": "看来你们俩挺合得来的", "13156": "看来你们俩挺合得来的", "13157": "看来你们俩挺合得来的", "13158": "看来你们俩挺合得来的", "13159": "看来你们俩挺合得来的", "13160": "", "13161": "", "13162": "", "13163": "", "13164": "那么 前辈后辈就友好地站在一起吧", "13165": "那么 前辈后辈就友好地站在一起吧", "13166": "那么 前辈后辈就友好地站在一起吧", "13167": "那么 前辈后辈就友好地站在一起吧", "13168": "那么 前辈后辈就友好地站在一起吧", "13169": "那么 前辈后辈就友好地站在一起吧", "13170": "那么 前辈后辈就友好地站在一起吧", "13171": "那么 前辈后辈就友好地站在一起吧", "13172": "那么 前辈后辈就友好地站在一起吧", "13173": "那么 前辈后辈就友好地站在一起吧", "13174": "那么 前辈后辈就友好地站在一起吧", "13175": "那么 前辈后辈就友好地站在一起吧", "13176": "", "13177": "", "13178": "北冈小姐", "13179": "北冈小姐", "13180": "北冈小姐", "13181": "北冈小姐", "13182": "在松井小姐旁边摆出一样的姿势", "13183": "在松井小姐旁边 摆出一样的姿势", "13184": "在松井小姐旁边摆出一样的姿势", "13185": "在松井小姐旁边 摆出一样的姿势", "13186": "在松井小姐旁边摆出一样的姿势", "13187": "在松井小姐旁边摆出一样的姿势", "13188": "在松井小姐旁边摆出一样的姿势", "13189": "在松井小姐旁边 摆出一样的姿势", "13190": "", "13191": "", "13192": "", "13193": "", "13194": "", "13195": "", "13196": "要好好相处", "13197": "要好好相处", "13198": "要好好相处", "13199": "要好好相处", "13200": "", "13201": "", "13202": "", "13203": "", "13204": "", "13205": "", "13206": "", "13207": "", "13208": "", "13209": "", "13210": "", "13211": "", "13212": "", "13213": "", "13214": "", "13215": "", "13216": "", "13217": "", "13218": "", "13219": "", "13220": "", "13221": "", "13222": "", "13223": "", "13224": "background", "13225": "", "13226": "", "13227": "", "13228": "", "13229": "", "13230": "", "13231": "", "13232": "", "13233": "", "13234": "", "13235": "", "13236": "", "13237": "", "13238": "", "13239": "", "13240": "", "13241": "", "13242": "", "13243": "", "13244": "", "13245": "", "13246": "", "13247": "真是舒服", "13248": "真是舒服", "13249": "真是舒服", "13250": "真是舒服", "13251": "真是舒服", "13252": "", "13253": "", "13254": "", "13255": "", "13256": "---", "13257": "", "13258": "---", "13259": "---", "13260": "---------------", "13261": "", "13262": "", "13263": "", "13264": "", "13265": "", "13266": "", "13267": "", "13268": "", "13269": "", "13270": "", "13271": "", "13272": "", "13273": "", "13274": "", "13275": "", "13276": "", "13277": "", "13278": "", "13279": "", "13280": "", "13281": "", "13282": "", "13283": "你们俩手牵着手吧", "13284": "你们俩手牵着手吧", "13285": "你们俩手牵着手吧", "13286": "你们俩手牵着手吧", "13287": "你们俩手牵着手吧", "13288": "", "13289": "", "13290": "", "13291": "", "13292": "", "13293": "", "13294": "", "13295": "", "13296": "", "13297": "", "13298": "DSQ", "13299": "", "13300": "", "13301": "", "13302": "", "13303": "", "13304": "", "13305": "", "13306": "", "13307": "", "13308": "", "13309": "", "13310": "", "13311": "---", "13312": "", "13313": "", "13314": "", "13315": "", "13316": "HTML", "13317": "", "13318": "", "13319": "", "13320": "", "13321": "", "13322": "", "13323": "", "13324": "", "13325": "", "13326": "", "13327": "", "13328": "", "13329": "", "13330": "", "13331": "", "13332": "", "13333": "", "13334": "", "13335": "", "13336": "", "13337": "", "13338": "", "13339": "", "13340": "好舒服", "13341": "好舒服", "13342": "好舒服", "13343": "好舒服", "13344": "", "13345": "", "13346": "", "13347": "", "13348": "", "13349": "", "13350": "", "13351": "", "13352": "", "13353": "", "13354": "", "13355": "", "13356": "", "13357": "已经忍不住了", "13358": "已经忍不住了", "13359": "已经忍不住了", "13360": "已经忍不住了", "13361": "已经忍不住了", "13362": "", "13363": "", "13364": "", "13365": "", "13366": "", "13367": "", "13368": "", "13369": "", "13370": "", "13371": "感觉要射了 就在里面射吧", "13372": "感觉要射了 就在里面射吧", "13373": "感觉要射了 就在里面射吧", "13374": "感觉要射了 就在里面射吧", "13375": "感觉要射了 就在里面射吧", "13376": "感觉要射了就在里面射吧", "13377": "感觉要射了 就在里面射吧", "13378": "感觉要射了 就在里面射吧", "13379": "感觉要射了 就在里面射吧", "13380": "感觉要射了 就在里面射吧", "13381": "", "13382": "", "13383": "", "13384": "", "13385": "", "13386": "---", "13387": "", "13388": "", "13389": "", "13390": "", "13391": "", "13392": "", "13393": "", "13394": "", "13395": "", "13396": "", "13397": "------------", "13398": "---------------", "13399": "", "13400": "", "13401": "", "13402": "", "13403": "", "13404": "", "13405": "", "13406": "", "13407": "", "13408": "", "13409": "", "13410": "", "13411": "", "13412": "可以吗", "13413": "可以吗", "13414": "可以吗", "13415": "可以吗", "13416": "", "13417": "", "13418": "", "13419": "", "13420": "", "13421": "", "13422": "", "13423": "", "13424": "", "13425": "------------", "13426": "---", "13427": "---", "13428": "---", "13429": "---", "13430": "", "13431": "", "13432": "北冈小姐 那让我先射吧", "13433": "北冈小姐 那让我先射吧", "13434": "北冈小姐 那让我先射吧", "13435": "北冈小姐 那让我先射吧", "13436": "北冈小姐 那让我先射吧", "13437": "北冈小姐 那让我先射吧", "13438": "北冈小姐 那让我先射吧", "13439": "北冈小姐 那让我先射吧", "13440": "北冈小姐 那让我先射吧", "13441": "北冈小姐 那让我先射吧", "13442": "北冈小姐 那让我先射吧", "13443": "北冈小姐 那让我先射吧", "13444": "北冈小姐 那让我先射吧", "13445": "北冈小姐 那让我先射吧", "13446": "不要这样", "13447": "不要这样", "13448": "不要这样", "13449": "不要这样", "13450": "不要这样", "13451": "", "13452": "", "13453": "", "13454": "", "13455": "", "13456": "", "13457": "", "13458": "", "13459": "", "13460": "要射了", "13461": "要射了", "13462": "要射了", "13463": "要射了", "13464": "---", "13465": "", "13466": "", "13467": "", "13468": "射了", "13469": "射了", "13470": "射了", "13471": "射了", "13472": "", "13473": "", "13474": "", "13475": "", "13476": "", "13477": "", "13478": "", "13479": "", "13480": "", "13481": "", "13482": "", "13483": "", "13484": "", "13485": "", "13486": "", "13487": "", "13488": "", "13489": "background", "13490": "", "13491": "", "13492": "", "13493": "", "13494": "", "13495": "", "13496": "", "13497": "", "13498": "", "13499": "", "13500": "", "13501": "", "13502": "", "13503": "", "13504": "", "13505": "", "13506": "", "13507": "", "13508": "", "13509": "", "13510": "", "13511": "", "13512": "", "13513": "", "13514": "", "13515": "", "13516": "", "13517": "", "13518": "", "13519": "", "13520": "", "13521": "", "13522": "", "13523": "", "13524": "", "13525": "", "13526": "", "13527": "", "13528": "", "13529": "", "13530": "---", "13531": "", "13532": "", "13533": "", "13534": "", "13535": "", "13536": "", "13537": "", "13538": "", "13539": "", "13540": "", "13541": "", "13542": "", "13543": "", "13544": "", "13545": "", "13546": "", "13547": "---", "13548": "---", "13549": "", "13550": "", "13551": "", "13552": "", "13553": "", "13554": "---", "13555": "", "13556": "", "13557": "", "13558": "", "13559": "", "13560": "我也忍不住了", "13561": "我也忍不住了", "13562": "我也忍不住了", "13563": "我也忍不住了", "13564": "我也忍不住了", "13565": "不要 不可以", "13566": "不要 不可以", "13567": "不要 不可以", "13568": "不要 不可以", "13569": "不要 不可以", "13570": "不要 不可以", "13571": "不要 不可以", "13572": "不要 不可以", "13573": "不要 不可以", "13574": "", "13575": "", "13576": "", "13577": "", "13578": "", "13579": "", "13580": "", "13581": "", "13582": "", "13583": "", "13584": "---", "13585": "", "13586": "", "13587": "", "13588": "", "13589": "", "13590": "", "13591": "", "13592": "", "13593": "", "13594": "", "13595": "", "13596": "", "13597": "", "13598": "", "13599": "", "13600": "", "13601": "", "13602": "", "13603": "", "13604": "", "13605": "", "13606": "", "13607": "我要射了", "13608": "我要射了", "13609": "我要射了", "13610": "我要射了", "13611": "我要射了", "13612": "", "13613": "", "13614": "", "13615": "", "13616": "", "13617": "", "13618": "", "13619": "", "13620": "", "13621": "要射了", "13622": "要射了", "13623": "要射了", "13624": "要射了", "13625": "要射了", "13626": "", "13627": "", "13628": "", "13629": "", "13630": "", "13631": "", "13632": "", "13633": "", "13634": "", "13635": "", "13636": "", "13637": "", "13638": "", "13639": "", "13640": "", "13641": "", "13642": "---", "13643": "", "13644": "", "13645": "", "13646": "", "13647": "", "13648": "", "13649": "---", "13650": "---", "13651": "---", "13652": "---", "13653": "---", "13654": "---", "13655": "---", "13656": "---", "13657": "不行了", "13658": "不行了", "13659": "不行了", "13660": "不行了", "13661": "", "13662": "", "13663": "", "13664": "", "13665": "", "13666": "", "13667": "要射了 我也要射了", "13668": "要射了 我也要射了", "13669": "要射了 我也要射了", "13670": "要射了 我也要射了", "13671": "要射了 我也要射了", "13672": "要射了 我也要射了", "13673": "", "13674": "---", "13675": "", "13676": "", "13677": "", "13678": "", "13679": "", "13680": "", "13681": "", "13682": "", "13683": "", "13684": "", "13685": "", "13686": "", "13687": "", "13688": "", "13689": "", "13690": "HTML", "13691": "", "13692": "", "13693": "", "13694": "", "13695": "", "13696": "", "13697": "", "13698": "", "13699": "", "13700": "---", "13701": "", "13702": "", "13703": "", "13704": "", "13705": "", "13706": "---", "13707": "", "13708": "", "13709": "", "13710": "", "13711": "---", "13712": "", "13713": "", "13714": "", "13715": "", "13716": "", "13717": "", "13718": "", "13719": "---", "13720": "", "13721": "", "13722": "", "13723": "", "13724": "", "13725": "", "13726": "", "13727": "", "13728": "", "13729": "---", "13730": "---", "13731": "", "13732": "", "13733": "---", "13734": "---", "13735": "---", "13736": "", "13737": "", "13738": "---", "13739": "---", "13740": "", "13741": "", "13742": "", "13743": "---", "13744": "", "13745": "", "13746": "", "13747": "", "13748": "", "13749": "", "13750": "", "13751": "", "13752": "---", "13753": "---", "13754": "", "13755": "", "13756": "", "13757": "", "13758": "", "13759": "", "13760": "---", "13761": "", "13762": "", "13763": "", "13764": "", "13765": "", "13766": "", "13767": "", "13768": "", "13769": "", "13770": "", "13771": "", "13772": "", "13773": "", "13774": "", "13775": "", "13776": "", "13777": "", "13778": "", "13779": "", "13780": "", "13781": "", "13782": "---", "13783": "", "13784": "", "13785": "", "13786": "", "13787": "", "13788": "", "13789": "", "13790": "", "13791": "", "13792": "", "13793": "---", "13794": "", "13795": "", "13796": "", "13797": "", "13798": "", "13799": "", "13800": "", "13801": "", "13802": "---", "13803": "(出演)", "13804": "(出演)", "13805": "(出演)", "13806": "(出演)", "13807": "(出演)", "13808": "(出演)", "13809": "(出演)", "13810": "(出演)", "13811": "(出演)", "13812": "(出演)", "13813": "", "13814": "", "13815": "(北冈果林)", "13816": "(北冈果林)", "13817": "(北冈果林)", "13818": "(北冈果林)", "13819": "(北冈果林)", "13820": "(北冈果林)", "13821": "(北冈果林)", "13822": "(北冈果林)", "13823": "(北冈果林)", "13824": "(北冈果林)", "13825": "(北冈果林)", "13826": "(北冈果林)", "13827": "", "13828": "", "13829": "(松井日奈子)", "13830": "(松井日奈子)", "13831": "(松井日奈子)", "13832": "(松井日奈子)", "13833": "(松井日奈子)", "13834": "(松井日奈子)", "13835": "(松井日奈子)", "13836": "(松井日奈子)", "13837": "绫野理事长", "13838": "绫野理事长", "13839": "绫野理事长", "13840": "绫野理事长", "13841": "绫野理事长", "13842": "", "13843": "(叶山小百合)", "13844": "(叶山小百合)", "13845": "(叶山小百合)", "13846": "(叶山小百合)", "13847": "(叶山小百合)", "13848": "(叶山小百合)", "13849": "(叶山小百合)", "13850": "(叶山小百合)", "13851": "(叶山小百合)", "13852": "(叶山小百合)", "13853": "(叶山小百合)", "13854": "(叶山小百合)", "13855": "", "13856": "理事长 这次的新来的孩子呢", "13857": "理事长 这次的新来的孩子呢", "13858": "理事长 这次的新来的孩子呢", "13859": "理事长 这次的新来的孩子呢", "13860": "理事长 这次的新来的孩子呢", "13861": "理事长 这次的新来的孩子呢", "13862": "理事长 这次的新来的孩子呢", "13863": "", "13864": "", "13865": "", "13866": "", "13867": "不错嘛", "13868": "不错嘛", "13869": "不错嘛", "13870": "不错嘛", "13871": "", "13872": "", "13873": "就这样好好教导她吧", "13874": "就这样好好教导她吧", "13875": "就这样好好教导她吧", "13876": "就这样好好教导她吧", "13877": "就这样好好教导她吧", "13878": "就这样好好教导她吧", "13879": "就这样好好教导她吧", "13880": "", "13881": "", "13882": "这家医院疯了", "13883": "这家医院疯了", "13884": "这家医院疯了", "13885": "这家医院疯了", "13886": "这家医院疯了", "13887": "这家医院疯了", "13888": "", "13889": "", "13890": "", "13891": "", "13892": "", "13893": "", "13894": "", "13895": "", "13896": "", "13897": "差不多准备好了", "13898": "差不多准备好了", "13899": "差不多准备好了", "13900": "差不多准备好了", "13901": "还有什么吗 快点", "13902": "还有什么吗快点", "13903": "还有什么吗 快点", "13904": "还有什么吗 快点", "13905": "还有什么吗 快点", "13906": "还有什么吗 快点", "13907": "还有什么吗 快点", "13908": "", "13909": "", "13910": "", "13911": "", "13912": "", "13913": "", "13914": "", "13915": "", "13916": "", "13917": "", "13918": "", "13919": "", "13920": "", "13921": "", "13922": "", "13923": "", "13924": "", "13925": "", "13926": "", "13927": "", "13928": "", "13929": "", "13930": "", "13931": "---", "13932": "", "13933": "", "13934": "", "13935": "", "13936": "", "13937": "", "13938": "", "13939": "", "13940": "", "13941": "", "13942": "", "13943": "", "13944": "高潮了 我高潮了", "13945": "高潮了 我高潮了", "13946": "高潮了 我高潮了", "13947": "高潮了 我高潮了", "13948": "高潮了 我高潮了", "13949": "", "13950": "", "13951": "", "13952": "", "13953": "", "13954": "在嘴里射满", "13955": "在嘴里射满", "13956": "在嘴里射满", "13957": "在嘴里射满", "13958": "在嘴里射满", "13959": "在嘴里射满", "13960": "在嘴里射满", "13961": "", "13962": "", "13963": "", "13964": "", "13965": "", "13966": "", "13967": "", "13968": "", "13969": "", "13970": "", "13971": "", "13972": "---------------", "13973": "", "13974": "", "13975": "", "13976": "我可不允许你做这种事", "13977": "我可不允许你做这种事", "13978": "我可不允许你做这种事", "13979": "我可不允许你做这种事", "13980": "我可不允许你做这种事", "13981": "完全没教养嘛", "13982": "完全没教养嘛", "13983": "完全没教养嘛", "13984": "完全没教养嘛", "13985": "完全没教养嘛", "13986": "必须好好教育一番才行呢", "13987": "必须好好教育一番才行呢", "13988": "必须好好教育一番才行呢", "13989": "必须好好教育一番才行呢", "13990": "必须好好教育一番才行呢", "13991": "必须好好教育一番才行呢", "13992": "请住手吧 让理事长生气", "13993": "请住手吧 让理事长生气", "13994": "请住手吧 让理事长生气", "13995": "请住手吧 让理事长生气", "13996": "请住手吧让理事长生气", "13997": "请住手吧让理事长生气", "13998": "请住手吧让理事长生气", "13999": "请住手吧 让理事长生气", "14000": "请住手吧 让理事长生气", "14001": "", "14002": "", "14003": "", "14004": "", "14005": "", "14006": "---", "14007": "", "14008": "", "14009": "", "14010": "", "14011": "", "14012": "", "14013": "住手不要", "14014": "住手不要", "14015": "住手不要", "14016": "住手 不要", "14017": "住手 不要", "14018": "阴蒂变大了呢", "14019": "98室[色花堂]永久地址489155.com阴蒂变大了呢", "14020": "98室[芭花堂]永久地址489155.com阴蒂变大了呢", "14021": "98室[芭花堂]永久地址489155.com阴蒂变大了呢", "14022": "98室[巴花堂]永久地址489155.com怎么这样请住手吧", "14023": "98室[巴花室]永久地址 489155.com怎么这样 请住手吧", "14024": "98室[巴花堂]永久地址489155.com怎么这样请住手吧", "14025": "怎么这样 请住手吧", "14026": "怎么这样 请住手吧", "14027": "怎么这样 请住手吧", "14028": "怎么这样 请住手吧", "14029": "住手 快住手啦", "14030": "住手 快住手啦", "14031": "住手 快住手啦", "14032": "住手 快住手啦", "14033": "住手 快住手啦", "14034": "住手 快住手啦", "14035": "---", "14036": "", "14037": "", "14038": "", "14039": "", "14040": "", "14041": "你也可以叫出声", "14042": "你也可以叫出声", "14043": "你也可以叫出声", "14044": "你也可以叫出声", "14045": "你也可以叫出声", "14046": "你也可以叫出声", "14047": "---", "14048": "", "14049": "", "14050": "腿不要合上", "14051": "腿不要合上", "14052": "腿不要合上", "14053": "腿不要合上", "14054": "腿不要合上", "14055": "腿不要合上", "14056": "不要过来 不要过来", "14057": "不要过来 不要过来", "14058": "不要过来 不要过来", "14059": "不要过来 不要过来", "14060": "不要过来 不要过来", "14061": "不要过来 不要过来", "14062": "给我老实一点", "14063": "给我老实一点", "14064": "给我老实一点", "14065": "给我老实一点", "14066": "给我老实一点", "14067": "给我老实一点", "14068": "---", "14069": "", "14070": "", "14071": "", "14072": "---", "14073": "不要想着要反抗", "14074": "不要想着要反抗", "14075": "不要想着要反抗", "14076": "不要想着要反抗", "14077": "不要想着要反抗", "14078": "不要想着要反抗", "14079": "不要想着要反抗", "14080": "不要想着要反抗", "14081": "不要想着要反抗", "14082": "", "14083": "", "14084": "", "14085": "", "14086": "", "14087": "", "14088": "", "14089": "", "14090": "", "14091": "", "14092": "", "14093": "", "14094": "真是久等了呢", "14095": "真是久等了呢", "14096": "真是久等了呢", "14097": "真是久等了呢", "14098": "无礼的家伙玩得很开心呢", "14099": "无礼的家伙玩得很开心呢", "14100": "无礼的家伙玩得很开心呢", "14101": "无礼的家伙玩得很开心呢", "14102": "无礼的家伙玩得很开心呢", "14103": "无礼的家伙玩得很开心呢", "14104": "", "14105": "", "14106": "", "14107": "", "14108": "", "14109": "", "14110": "", "14111": "", "14112": "", "14113": "", "14114": "", "14115": "", "14116": "", "14117": "", "14118": "", "14119": "", "14120": "", "14121": "", "14122": "", "14123": "", "14124": "", "14125": "", "14126": "", "14127": "", "14128": "", "14129": "", "14130": "", "14131": "", "14132": "", "14133": "", "14134": "", "14135": "", "14136": "", "14137": "", "14138": "", "14139": "", "14140": "", "14141": "", "14142": "---", "14143": "", "14144": "", "14145": "", "14146": "", "14147": "", "14148": "", "14149": "", "14150": "", "14151": "", "14152": "", "14153": "", "14154": "", "14155": "", "14156": "", "14157": "", "14158": "", "14159": "", "14160": "", "14161": "", "14162": "", "14163": "", "14164": "", "14165": "", "14166": "", "14167": "", "14168": "", "14169": "", "14170": "", "14171": "", "14172": "", "14173": "", "14174": "", "14175": "", "14176": "", "14177": "", "14178": "", "14179": "", "14180": "", "14181": "", "14182": "", "14183": "", "14184": "", "14185": "", "14186": "", "14187": "", "14188": "", "14189": "", "14190": "", "14191": "", "14192": "", "14193": "", "14194": "", "14195": "", "14196": "", "14197": "", "14198": "", "14199": "", "14200": "", "14201": "", "14202": "尽情射出来吧", "14203": "尽情射出来吧", "14204": "尽情射出来吧", "14205": "尽情射出来吧", "14206": "尽情射出来吧", "14207": "尽情射出来吧", "14208": "------", "14209": "---", "14210": "------------", "14211": "---", "14212": "我也要射了 不行", "14213": "我也要射了 不行", "14214": "我也要射了 不行", "14215": "我也要射了 不行", "14216": "我也要射了 不行", "14217": "我也要射了 不行", "14218": "我也要射了 不行", "14219": "我也要射了 不行", "14220": "我也要射了 不行", "14221": "", "14222": "", "14223": "", "14224": "", "14225": "", "14226": "", "14227": "", "14228": "", "14229": "", "14230": "", "14231": "", "14232": "不要道歉 果林前辈", "14233": "不要道歉 果林前辈", "14234": "不要道歉 果林前辈", "14235": "不要道歉 果林前辈", "14236": "不要道歉 果林前辈"} \ No newline at end of file diff --git a/tests/nodes/test_subtitle_ocr/test_ocr.py b/tests/nodes/test_subtitle_ocr/test_ocr.py new file mode 100644 index 0000000..ffd9b9d --- /dev/null +++ b/tests/nodes/test_subtitle_ocr/test_ocr.py @@ -0,0 +1,461 @@ +"""nodes/subtitle_ocr.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`nodes/subtitle_ocr.py`(逐帧 OCR → 合并 → 汇总 SRT,含并发与 +断点续跑),可独立调用。vlm-ocr 属同一进程内的下游模块,测试通过注入真实 +帧图 + 在 registry 注册假 vlm 处理器(I/O 边界)来隔离模型调用;真实 14236 +帧夹具用于验证汇总与顺序。 +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from nodes.adaptive_pool import AdaptiveThreadPool +from nodes.subtitle_ocr import ( + PauseRequested, + _assemble_srt, + _eta_suffix, + _format_eta, + _load_partial, + _merge_kept, + _sampling_interval, + invoke, +) +from wov_app import registry +from wov_sdk.models import InvokeRequest + +# 模块专用数据:真实任务 run_ac7f480a3ccb 的 14236 帧清单与逐帧 OCR 文本。 +DATA_DIR = Path(__file__).resolve().parent / "data" +FULL_MANIFEST = DATA_DIR / "frames_manifest_full.json" +FULL_TEXTS = DATA_DIR / "ocr_frames_full.json" + + +# --------------------------------------------------------------------------- +# 辅件:帧清单与假 vlm 处理器 +# --------------------------------------------------------------------------- + + +def _manifest(tmp_path: Path, texts_per_frame: int = 3) -> tuple[Path, list[dict]]: + """构造真实帧清单:生成真实 PNG 帧文件(1x1 合法 PNG)并写清单 JSON。 + + 返回 (清单路径, 清单数据)。时间轴按 2 秒间隔,与真实抽帧一致。 + """ + frames_dir = tmp_path / "frames" + frames_dir.mkdir(parents=True, exist_ok=True) + # 最小合法 PNG(1x1 透明像素),供下游假处理器读取真实文件。 + png = bytes.fromhex( + "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489" + "0000000a49444154789c6300010000050001od".replace("od", "0d") + + "0a2db40000000049454e44ae426082" + ) + entries = [] + for i in range(texts_per_frame): + frame_path = frames_dir / f"frame_{i + 1:04d}.png" + frame_path.write_bytes(png) + entries.append({"time": round(i * 2.0, 3), "image_uri": str(frame_path)}) + manifest_path = tmp_path / "frames.json" + manifest_path.write_text(json.dumps(entries, ensure_ascii=False), encoding="utf-8") + return manifest_path, entries + + +def _register_fake_vlm(texts: list[str], calls: list[int] | None = None) -> None: + """在节点注册表中注册假 vlm-ocr:按调用次序返回预置文本。 + + 这是允许的 I/O 边界替身(模型推理不进入单元测试);使用仓库真实的 + vlm 清单(manifests/vlm.json)注册,保证注册表校验路径也被执行, + 返回结构为真实 InvokeResponse(含 text 字段)。 + """ + from pathlib import Path as _Path + + from wov_sdk.models import InvokeResponse, NodeManifest + + state = {"index": 0} + + def fake_vlm(request: InvokeRequest): + index = state["index"] + state["index"] += 1 + if calls is not None: + calls.append(index) + return InvokeResponse(status="completed", outputs={"text": texts[index]}) + + manifest_path = _Path(__file__).resolve().parents[3] / "manifests" / "vlm.json" + registry.register(NodeManifest.load(str(manifest_path)), fake_vlm) + + +def _request(tmp_path: Path, manifest_path: Path, **params) -> InvokeRequest: + """构造真实请求,输出目录位于 tmp_path/out。""" + return InvokeRequest( + run_id="run-test", + node_instance_id="subtitle-ocr-1", + params=params, + inputs={"frames_manifest": str(manifest_path)}, + output_dir=str(tmp_path / "out"), + ) + + +# --------------------------------------------------------------------------- +# 纯函数:时间与汇总 +# --------------------------------------------------------------------------- + + +def test_format_eta_variants() -> None: + """ETA 格式化覆盖秒/分/小时三种量级。""" + # 数据:45 秒、34 分 13 秒、2 小时 5 分。 + # 测试过程与验证结果 + assert _format_eta(45) == "45秒" + assert _format_eta(2053) == "34分13秒" + assert _format_eta(7500) == "2小时05分" + + +def test_eta_suffix_empty_when_rate_zero() -> None: + """速度为 0 时不显示 ETA(无法推算)。""" + # 数据:速度为 0 与正常速度。 + # 测试过程与验证结果 + assert _eta_suffix(10, 100, 0.0) == "" + assert "预计剩余" in _eta_suffix(50, 100, 1.0) + + +def test_sampling_interval_from_manifest_median() -> None: + """采样间隔取相邻帧时间差中位数(与抽帧参数一致)。""" + # 数据:0.5 秒间隔的帧清单。 + manifest = [{"time": round(i * 0.5, 3)} for i in range(6)] + + # 测试过程与验证结果 + assert _sampling_interval(manifest, default=2.0) == 0.5 + + +def test_sampling_interval_falls_back_when_insufficient() -> None: + """清单不足两帧时回退默认间隔。""" + # 数据:单帧与空清单。 + # 测试过程与验证结果 + assert _sampling_interval([{"time": 0.0}], default=1.5) == 1.5 + assert _sampling_interval([], default=1.5) == 1.5 + + +def test_merge_kept_merges_consecutive_and_breaks_on_empty() -> None: + """连续相同字幕合并(记录最后可见帧),空帧结束当前段。""" + # 数据:A A 空 A —— 第一段两条 A,空帧后是新的一段。 + manifest = [{"time": t} for t in (0.0, 2.0, 4.0, 6.0)] + texts = ["A", "A", "", "A"] + + # 测试过程 + kept = _merge_kept(manifest, texts) + + # 验证结果:两段,第一段从 0 到最后可见 2,第二段从 6 到 6。 + assert kept == [(0.0, 2.0, "A"), (6.0, 6.0, "A")] + + +def test_merge_kept_tracks_last_visible_frame() -> None: + """同一字幕停留多帧时起始时间不变、结束时间更新为最后可见帧。""" + # 数据:三条相同字幕。 + manifest = [{"time": t} for t in (0.0, 2.0, 4.0)] + + # 测试过程 + kept = _merge_kept(manifest, ["字幕", "字幕", "字幕"]) + + # 验证结果 + assert kept == [(0.0, 4.0, "字幕")] + + +def test_assemble_srt_end_time_is_last_seen_plus_interval() -> None: + """SRT 结束时间 = 最后可见帧时间 + 采样间隔(字幕在下一采样点消失)。""" + # 数据:一段字幕,采样间隔 0.5。 + kept = [(1.0, 2.0, "台词")] + + # 测试过程 + lines = _assemble_srt(kept, 0.5) + + # 验证结果 + assert lines[0] == "1" + assert lines[1] == "00:00:01,000 --> 00:00:02,500" + assert lines[2] == "台词" + + +def test_assemble_srt_renumbers_sequentially() -> None: + """多段字幕序号从 1 连续编号。""" + # 数据:两段字幕。 + kept = [(0.0, 1.0, "甲"), (5.0, 6.0, "乙")] + + # 测试过程 + lines = _assemble_srt(kept, 1.0) + + # 验证结果:序号 1、2。 + text = "\n".join(lines) + assert "\n1\n" in f"\n{text}" or text.startswith("1\n") + assert "\n2\n" in text + + +# --------------------------------------------------------------------------- +# 断点存档 +# --------------------------------------------------------------------------- + + +def test_load_partial_reuses_completed_and_skipped(tmp_path: Path) -> None: + """存档中 completed(含空文字)与 skipped 都复用,视为已处理帧。""" + # 数据:一份含成功空帧、成功文本、跳过、以及旧版空串的存档。 + lines = [ + {"frame": 0, "text": "", "status": "completed"}, + {"frame": 1, "text": "文本", "status": "completed"}, + {"frame": 2, "text": "", "status": "skipped"}, + {"frame": 3, "text": "", "status": "failed"}, + {"frame": 4, "text": ""}, # 旧版无 status 的空串 → 不视为已处理 + ] + output_dir = tmp_path / "out" + output_dir.mkdir() + (output_dir / "ocr_partial.jsonl").write_text( + "\n".join(json.dumps(item, ensure_ascii=False) for item in lines), encoding="utf-8" + ) + + # 测试过程 + partial = _load_partial(output_dir) + + # 验证结果:0/1/2 复用,3/4 不复用。 + assert partial == {0: "", 1: "文本", 2: ""} + + +def test_load_partial_skips_corrupt_line(tmp_path: Path) -> None: + """进程被杀留下的半行写入被跳过,对应帧视为未处理。""" + # 数据:合法行 + 截断行。 + output_dir = tmp_path / "out" + output_dir.mkdir() + (output_dir / "ocr_partial.jsonl").write_text( + json.dumps({"frame": 0, "text": "A", "status": "completed"}) + "\n" + + '{"frame": 1, "text": "B", "sta', + encoding="utf-8", + ) + + # 测试过程 + partial = _load_partial(output_dir) + + # 验证结果 + assert partial == {0: "A"} + + +# --------------------------------------------------------------------------- +# invoke:汇总主流程(假 vlm) +# --------------------------------------------------------------------------- + + +def test_invoke_assembles_srt_from_frame_texts(tmp_path: Path) -> None: + """逐帧 OCR 结果汇总为 SRT:连续相同字幕合并、空帧分段。""" + # 数据:A A 空 的帧序列。 + manifest_path, _ = _manifest(tmp_path, texts_per_frame=3) + _register_fake_vlm(["字幕A", "字幕A", ""]) + + # 测试过程 + response = invoke(_request(tmp_path, manifest_path)) + + # 验证结果:一条字幕(0~2 秒 + 采样间隔),时间轴正确。 + assert response.status == "completed", response.error + content = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8") + assert "字幕A" in content + assert response.outputs["count"] == 1 + assert "00:00:00,000 --> 00:00:04,000" in content + + +def test_invoke_skips_overlong_output(tmp_path: Path) -> None: + """超长 OCR 输出按既有规则跳过(记 skipped,不进字幕)。""" + # 数据:一帧超长输出 + 一帧正常。 + manifest_path, _ = _manifest(tmp_path, texts_per_frame=2) + _register_fake_vlm(["超长内容" * 100, "正常字幕"]) + + # 测试过程 + response = invoke(_request(tmp_path, manifest_path, max_result_chars=200)) + + # 验证结果:超长帧被跳过,只保留正常字幕。 + assert response.status == "completed", response.error + content = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8") + assert "正常字幕" in content + assert "超长内容" not in content + + +def test_invoke_resumes_from_checkpoint(tmp_path: Path) -> None: + """断点续跑:已存档帧不再调用 vlm,只处理剩余帧。""" + # 数据:3 帧,其中第 0 帧已有存档。 + manifest_path, entries = _manifest(tmp_path, texts_per_frame=3) + output_dir = tmp_path / "out" + output_dir.mkdir() + (output_dir / "ocr_partial.jsonl").write_text( + json.dumps({"frame": 0, "text": "已处理字幕", "status": "completed"}, ensure_ascii=False) + "\n", + encoding="utf-8", + ) + calls: list[int] = [] + _register_fake_vlm(["新帧一", "新帧二"], calls=calls) + + # 测试过程 + response = invoke(_request(tmp_path, manifest_path)) + + # 验证结果:只调用 2 次(剩余帧),产物同时含存档与新增字幕。 + assert response.status == "completed", response.error + assert len(calls) == 2 + content = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8") + assert "已处理字幕" in content + assert "新帧一" in content + + +def test_invoke_all_frames_cached_makes_no_vlm_call(tmp_path: Path) -> None: + """全部帧已存档时不再调用 vlm(幂等重跑)。""" + # 数据:2 帧全部已存档。 + manifest_path, _ = _manifest(tmp_path, texts_per_frame=2) + output_dir = tmp_path / "out" + output_dir.mkdir() + (output_dir / "ocr_partial.jsonl").write_text( + "\n".join([ + json.dumps({"frame": 0, "text": "甲", "status": "completed"}, ensure_ascii=False), + json.dumps({"frame": 1, "text": "乙", "status": "completed"}, ensure_ascii=False), + ]) + "\n", + encoding="utf-8", + ) + calls: list[int] = [] + _register_fake_vlm([], calls=calls) + + # 测试过程 + response = invoke(_request(tmp_path, manifest_path)) + + # 验证结果:零调用且产物完整。 + assert response.status == "completed", response.error + assert calls == [] + content = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8") + assert "甲" in content and "乙" in content + + +def test_invoke_writes_checkpoint_per_frame(tmp_path: Path) -> None: + """每帧完成后立即写断点存档(进程被杀后可续跑)。""" + # 数据:2 帧。 + manifest_path, _ = _manifest(tmp_path, texts_per_frame=2) + _register_fake_vlm(["一", "二"]) + + # 测试过程 + response = invoke(_request(tmp_path, manifest_path)) + + # 验证结果:存档含两帧且带 status=completed。 + assert response.status == "completed" + archived = (tmp_path / "out" / "ocr_partial.jsonl").read_text(encoding="utf-8").splitlines() + records = [json.loads(line) for line in archived] + assert {r["frame"] for r in records} == {0, 1} + assert all(r["status"] == "completed" for r in records) + + +def test_invoke_fails_and_keeps_successful_frames_on_vlm_failure(tmp_path: Path) -> None: + """某帧持续失败时节点失败,但成功帧的存档保留供恢复。""" + # 数据:3 帧,vlm 对第 1 帧始终失败。 + frames_manifest, _ = _manifest(tmp_path, texts_per_frame=3) + + def flaky_vlm(request: InvokeRequest): + from wov_sdk.models import InvokeResponse + + image = Path(request.inputs["image_uri"]).name + if image == "frame_0002.png": + return InvokeResponse(status="failed", error="boom") + return InvokeResponse(status="completed", outputs={"text": "ok"}) + + from wov_sdk.models import NodeManifest + + # 注册假 vlm-ocr(使用真实清单文件,变量名与帧清单区分开)。 + vlm_manifest_path = Path(__file__).resolve().parents[3] / "manifests" / "vlm.json" + registry.register(NodeManifest.load(str(vlm_manifest_path)), flaky_vlm) + + # 测试过程 + response = invoke(_request(tmp_path, frames_manifest, pool_max_workers=1)) + + # 验证结果:节点失败,且成功帧已存档。 + assert response.status == "failed" + archived = (tmp_path / "out" / "ocr_partial.jsonl").read_text(encoding="utf-8").splitlines() + assert len(archived) >= 1 + + +def test_invoke_fails_without_manifest(tmp_path: Path) -> None: + """缺少 frames_manifest 时失败。""" + # 数据:空输入。 + request = InvokeRequest( + run_id="r", node_instance_id="n", params={}, inputs={}, output_dir=str(tmp_path) + ) + + # 测试过程 + response = invoke(request) + + # 验证结果 + assert response.status == "failed" + assert "frames_manifest" in (response.error or "") + + +def test_invoke_fails_when_manifest_missing(tmp_path: Path) -> None: + """清单文件不存在时失败。""" + # 数据:不存在的路径。 + # 测试过程 + response = invoke(_request(tmp_path, tmp_path / "nope.json")) + + # 验证结果 + assert response.status == "failed" + assert "not found" in (response.error or "") + + +# --------------------------------------------------------------------------- +# 暂停信号 +# --------------------------------------------------------------------------- + + +def test_invoke_aborts_fast_when_pause_flag_present(tmp_path: Path) -> None: + """暂停信号存在时立即中止,不 OCR 任何帧,返回 failed 且无成功存档。""" + # 数据:帧清单 + run 根目录下的 paused.flag(output_dir 的上两级)。 + manifest_path, _ = _manifest(tmp_path, texts_per_frame=3) + run_root = tmp_path / "runs" / "run-test" + output_dir = run_root / "steps" / "ocr" + output_dir.mkdir(parents=True) + (run_root / "paused.flag").write_text("", encoding="utf-8") + calls: list[int] = [] + _register_fake_vlm(["不应被调用"] * 3, calls=calls) + request = InvokeRequest( + run_id="run-test", node_instance_id="ocr-1", params={}, + inputs={"frames_manifest": str(manifest_path)}, output_dir=str(output_dir), + ) + + # 测试过程 + response = invoke(request) + + # 验证结果:失败、零 OCR 调用、无成功存档。 + assert response.status == "failed" + assert "暂停" in (response.error or "") + assert calls == [] + assert not (output_dir / "ocr_partial.jsonl").exists() + + +def test_pause_requested_exception_is_distinct_type() -> None: + """暂停用独立异常类型表达(调度器据此保持 PAUSED 而不标 FAILED)。""" + # 数据:异常类。 + # 测试过程与验证结果 + assert issubclass(PauseRequested, Exception) + + +# --------------------------------------------------------------------------- +# 真实全量帧数据回归 +# --------------------------------------------------------------------------- + + +@pytest.mark.integration +def test_full_14236_frame_data_reassembles_consistently(tmp_path: Path) -> None: + """真实 14236 帧清单 + 逐帧 OCR 文本:重组装与单线程基线一致。 + + 数据来源:真实任务 run_ac7f480a3ccb(见 docs/testing.md)。 + """ + # 数据:真实清单与真实逐帧文本。 + if not FULL_MANIFEST.is_file() or not FULL_TEXTS.is_file(): + pytest.skip("缺少真实 14236 帧夹具,跳过") + + # 测试过程:用真实数据直接调用汇总逻辑(不调模型)。 + manifest = json.loads(FULL_MANIFEST.read_text(encoding="utf-8")) + texts = json.loads(FULL_TEXTS.read_text(encoding="utf-8")) + if isinstance(texts, dict): + texts = [texts.get(str(i), "") for i in range(len(manifest))] + kept = _merge_kept(manifest, texts) + srt_lines = _assemble_srt(kept, _sampling_interval(manifest, 0.5)) + + # 验证结果:条数稳定、时间轴单调不减、SRT 结构合法。 + assert len(manifest) == 14236 + assert len(kept) > 0 + starts = [k[0] for k in kept] + assert starts == sorted(starts) + assert srt_lines.count("") == len(kept) # 每条一个空行分隔 diff --git a/tests/nodes/test_vad_profiler/__init__.py b/tests/nodes/test_vad_profiler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/nodes/test_vad_profiler/test_profile.py b/tests/nodes/test_vad_profiler/test_profile.py new file mode 100644 index 0000000..559a839 --- /dev/null +++ b/tests/nodes/test_vad_profiler/test_profile.py @@ -0,0 +1,276 @@ +"""nodes/vad_profiler.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`nodes/vad_profiler.py`(每视频自适应 VAD:信号分析 → 参数建议 → +转录质量评分),可独立调用。WAV 用例使用现场合成的**合法 PCM 波形**(真实 +音频格式,非占位字节);评分用例使用真实结构的分段对象。 +""" + +from __future__ import annotations + +import math +import struct +import wave +from pathlib import Path + +from nodes.vad_profiler import ( + HALLUCINATION_TOKENS, + AudioProfile, + _pick_representative_start, + profile_audio, + score_transcript, + suggest_vad_parameters, +) + + +class Segment: + """模拟真实 whisper 分段(仅需 text 字段)。""" + + def __init__(self, text: str) -> None: + self.text = text + + +def _write_wav(path: Path, segments: list[tuple[str, float]], sample_rate: int = 16000) -> None: + """生成合法 WAV:segments 为 [(类型, 秒数)],类型为 'silence' 或 'voice'。 + + 真实 PCM:静音写 0 振幅,语音写 1000Hz 正弦(振幅 3000,超过语音阈值 900)。 + """ + frames = bytearray() + for kind, seconds in segments: + count = int(sample_rate * seconds) + for i in range(count): + value = 0 if kind == "silence" else int(3000 * math.sin(2 * math.pi * 1000 * i / sample_rate)) + frames += struct.pack(" None: + """静音占比由 1s 网格 RMS 统计得出(静音 3s + 语音 1s → 0.75)。""" + # 数据:3 秒静音 + 1 秒语音的合法 WAV。 + audio = tmp_path / "mixed.wav" + _write_wav(audio, [("silence", 3), ("voice", 1)]) + + # 测试过程 + profile = profile_audio(audio) + + # 验证结果:时长、静音占比、语音占比。 + assert profile.duration_seconds == 0.0 or profile.duration_seconds >= 0 # 字段保留 + assert 0.5 <= profile.silence_ratio <= 1.0 + assert profile.voice_ratio <= 0.5 + assert profile.rms_bins + + +def test_profile_audio_all_silence(tmp_path: Path) -> None: + """全静音音频:静音占比为 1,语音占比为 0。""" + # 数据:5 秒静音。 + audio = tmp_path / "silence.wav" + _write_wav(audio, [("silence", 5)]) + + # 测试过程 + profile = profile_audio(audio) + + # 验证结果 + assert profile.silence_ratio == 1.0 + assert profile.voice_ratio == 0.0 + + +def test_profile_audio_all_voice(tmp_path: Path) -> None: + """全语音音频:语音占比为 1。""" + # 数据:4 秒语音。 + audio = tmp_path / "voice.wav" + _write_wav(audio, [("voice", 4)]) + + # 测试过程 + profile = profile_audio(audio) + + # 验证结果 + assert profile.voice_ratio == 1.0 + assert profile.silence_ratio == 0.0 + + +def test_profile_audio_marks_long_silence(tmp_path: Path) -> None: + """连续 ≥5 秒静音被标记为长停顿。""" + # 数据:6 秒静音 + 2 秒语音。 + audio = tmp_path / "long_gap.wav" + _write_wav(audio, [("silence", 6), ("voice", 2)]) + + # 测试过程 + profile = profile_audio(audio) + + # 验证结果 + assert profile.long_silence is True + + +def test_profile_audio_empty_wav(tmp_path: Path) -> None: + """空 WAV(0 帧)返回零值画像,不抛异常。""" + # 数据:0 秒。 + audio = tmp_path / "empty.wav" + _write_wav(audio, []) + + # 测试过程 + profile = profile_audio(audio) + + # 验证结果 + assert profile.rms_bins == [] + assert profile.median_rms == 0.0 + + +# --------------------------------------------------------------------------- +# 参数建议 +# --------------------------------------------------------------------------- + + +def test_suggest_params_for_bgm_heavy() -> None: + """BGM 覆盖广时降低 threshold、减小静音阈值与 padding(增强人声敏感)。""" + # 数据:BGM 覆盖画像。 + profile = AudioProfile(bgm_heavy=True, lowish_ratio=0.7, silence_ratio=0.1) + + # 测试过程 + params = suggest_vad_parameters(profile) + + # 验证结果 + assert params["threshold"] == 0.3 + assert params["min_silence_duration_ms"] == 300 + assert params["speech_pad_ms"] == 0 + + +def test_suggest_params_for_long_silence() -> None: + """长停顿常见时用正常 threshold 并减小 padding(防时间轴漂移)。""" + # 数据:长静音画像。 + profile = AudioProfile(long_silence=True, silence_ratio=0.2) + + # 测试过程 + params = suggest_vad_parameters(profile) + + # 验证结果 + assert params["threshold"] == 0.5 + assert params["speech_pad_ms"] == 200 + + +def test_suggest_params_for_high_silence_ratio() -> None: + """静音占比高时提高 threshold 剔除虚警。""" + # 数据:静音占比 0.5。 + profile = AudioProfile(silence_ratio=0.5) + + # 测试过程 + params = suggest_vad_parameters(profile) + + # 验证结果 + assert params["threshold"] == 0.6 + assert params["min_silence_duration_ms"] == 2000 + + +def test_suggest_params_default_profile() -> None: + """常规画像返回中间档参数。""" + # 数据:无特殊标记的常规画像。 + profile = AudioProfile(silence_ratio=0.1) + + # 测试过程 + params = suggest_vad_parameters(profile) + + # 验证结果 + assert params == { + "threshold": 0.5, + "min_silence_duration_ms": 1000, + "speech_pad_ms": 400, + } + + +# --------------------------------------------------------------------------- +# 转录质量评分 +# --------------------------------------------------------------------------- + + +def test_score_empty_segments_is_zero() -> None: + """无分段时评分为 0。""" + # 数据:空列表。 + # 测试过程与验证结果 + assert score_transcript([]) == 0.0 + + +def test_score_clean_transcript_is_high() -> None: + """正常长度、无碎片无幻觉的转录得分高。""" + # 数据:5 条 15 字左右的中文分段。 + segments = [Segment("这是一句长度适中的正常字幕内容") for _ in range(5)] + + # 测试过程 + score = score_transcript(segments) + + # 验证结果:接近满分。 + assert score > 90.0 + + +def test_score_penalizes_fragments() -> None: + """碎片化(纯语气词)条目被扣分。""" + # 数据:5 条纯假名碎片。 + segments = [Segment("あ") for _ in range(5)] + + # 测试过程 + frag_score = score_transcript(segments) + + # 验证结果:明显低于干净转录。 + assert frag_score < score_transcript([Segment("正常长度的字幕内容")] * 5) + + +def test_score_penalizes_hallucination_tokens() -> None: + """命中寒暄幻觉词的分段被扣分(幻觉越多该参数组合越差)。""" + # 数据:3 条含幻觉词的分段。 + segments = [Segment("ご視聴ありがとうございました") for _ in range(3)] + + # 测试过程 + hall_score = score_transcript(segments) + + # 验证结果:低于同长度无幻觉文本。 + assert hall_score < score_transcript([Segment("ご視聴ありがとうああああ")] * 3) + assert HALLUCINATION_TOKENS # 词表非空 + + +def test_score_penalizes_overlong_segments() -> None: + """平均字长过长(并句)被扣分。""" + # 数据:3 条超长分段。 + long_segments = [Segment("很长" * 30) for _ in range(3)] + normal_segments = [Segment("正常长度字幕") for _ in range(3)] + + # 测试过程与验证结果 + assert score_transcript(long_segments) < score_transcript(normal_segments) + + +def test_score_never_negative() -> None: + """极差转录的得分下限为 0(不出现负数)。""" + # 数据:大量碎片 + 幻觉。 + segments = [Segment("あ") for _ in range(50)] + [Segment("ご視聴ありがとうございました")] * 10 + + # 测试过程与验证结果 + assert score_transcript(segments) == 0.0 + + +# --------------------------------------------------------------------------- +# 代表片段选择 +# --------------------------------------------------------------------------- + + +def test_pick_representative_start_within_bounds() -> None: + """代表片段起点不超过音频长度减去窗口(避免越界)。""" + # 数据:60 秒画像、30 秒窗口。 + profile = AudioProfile(rms_bins=[100.0] * 60) + + # 测试过程 + start = _pick_representative_start(profile, window=30) + + # 验证结果:起点落在 0~30 秒内。 + assert 0 <= start <= 30 + + +def test_pick_representative_start_empty_profile() -> None: + """空画像返回 0(调用方可直接从头开始)。""" + # 数据:空 rms_bins。 + # 测试过程与验证结果 + assert _pick_representative_start(AudioProfile(), window=30) == 0 diff --git a/tests/nodes/test_vlm/__init__.py b/tests/nodes/test_vlm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/nodes/test_vlm/data/ocr_notext.png b/tests/nodes/test_vlm/data/ocr_notext.png new file mode 100644 index 0000000..0086edb Binary files /dev/null and b/tests/nodes/test_vlm/data/ocr_notext.png differ diff --git a/tests/nodes/test_vlm/data/ocr_text.png b/tests/nodes/test_vlm/data/ocr_text.png new file mode 100644 index 0000000..ce2c298 Binary files /dev/null and b/tests/nodes/test_vlm/data/ocr_text.png differ diff --git a/tests/nodes/test_vlm/data/test_real_hav_sub.png b/tests/nodes/test_vlm/data/test_real_hav_sub.png new file mode 100644 index 0000000..a977852 Binary files /dev/null and b/tests/nodes/test_vlm/data/test_real_hav_sub.png differ diff --git a/tests/nodes/test_vlm/test_ocr.py b/tests/nodes/test_vlm/test_ocr.py new file mode 100644 index 0000000..29f421a --- /dev/null +++ b/tests/nodes/test_vlm/test_ocr.py @@ -0,0 +1,410 @@ +"""nodes/vlm.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`nodes/vlm.py`(单帧 OCR:调 Ollama /api/chat 流式识别),可独立 +调用。网络属于允许 mock 的 I/O 边界:单元用例注入假响应对象验证请求体结构、 +流式读取、截断与清洗;集成用例调用真实 Ollama 与真实图片。 +""" + +from __future__ import annotations + +import json +import socket +import urllib.error +import urllib.request +from pathlib import Path + +import pytest + +from nodes.vlm import ( + STOP_SEQUENCE, + _clean_ocr_text, + _consume_stream, + _extract_gettext, + _truncate_at_stop, + invoke, +) +from wov_sdk.models import InvokeRequest + +# 模块专用真实素材:有文字帧 / 无文字帧 / 真实视频字幕截图。 +DATA_DIR = Path(__file__).resolve().parent / "data" +TEXT_IMAGE = DATA_DIR / "ocr_text.png" +NOTEXT_IMAGE = DATA_DIR / "ocr_notext.png" +REAL_SUBTITLE_IMAGE = DATA_DIR / "test_real_hav_sub.png" + +# 集成测试期望识别出的真实字幕文本。 +EXPECTED_REAL_TEXT = "还有没有什么困扰 或者奇怪的地方吗" + + +def _request(tmp_path: Path, image: Path | None, **params) -> InvokeRequest: + """构造真实请求;image 为 None 时表示不传 image_uri。""" + inputs = {} if image is None else {"image_uri": str(image)} + return InvokeRequest( + run_id="run-test", + node_instance_id="vlm-1", + params=params, + inputs=inputs, + output_dir=str(tmp_path / "out"), + ) + + +class _FakeResponse: + """假的流式响应对象:按行返回预置的 Ollama 流式 JSON 块。""" + + def __init__(self, lines: list[bytes]) -> None: + self._lines = list(lines) + + def readline(self) -> bytes: + """返回下一行;耗尽后返回空字节(模拟流结束)。""" + return self._lines.pop(0) if self._lines else b"" + + def __enter__(self): + return self + + def __exit__(self, *exc) -> None: + return None + + +def _stream_lines(*contents: str, done: bool = True) -> list[bytes]: + """把若干内容片段编码成 Ollama 流式行(最后可选追加 done 行)。""" + lines = [ + json.dumps({"message": {"content": c}}).encode("utf-8") for c in contents + ] + if done: + lines.append(json.dumps({"done": True}).encode("utf-8")) + return lines + + +# --------------------------------------------------------------------------- +# 文本清洗与标签提取 +# --------------------------------------------------------------------------- + + +def test_clean_ocr_text_strips_fences_and_blank_lines() -> None: + """清洗去掉 markdown 围栏行与空行,只保留识别文字。""" + # 数据:glm-ocr 典型输出(识别文本后追加大量围栏)。 + raw = "```markdown\n字幕第一行\n\n字幕第二行\n```\n" + + # 测试过程 + cleaned = _clean_ocr_text(raw) + + # 验证结果 + assert cleaned == "字幕第一行\n字幕第二行" + + +def test_clean_ocr_text_keeps_inline_backticks() -> None: + """行内反引号属于正文内容,不能被误删。""" + # 数据:正文含行内反引号。 + raw = "输入 `git status` 查看状态" + + # 测试过程与验证结果 + assert _clean_ocr_text(raw) == "输入 `git status` 查看状态" + + +def test_extract_gettext_takes_first_tag_to_avoid_loops() -> None: + """多个 标签时只取第一个(防重复循环)。""" + # 数据:两个标签,模型循环输出了两次。 + raw = "正确文本正确文本" + + # 测试过程与验证结果 + assert _extract_gettext(raw) == "正确文本" + + +def test_extract_gettext_falls_back_to_raw_without_tags() -> None: + """模型未按格式输出标签时回退原文。""" + # 数据:无标签的纯文本。 + # 测试过程与验证结果 + assert _extract_gettext("没有标签的文本") == "没有标签的文本" + + +def test_truncate_at_stop_cuts_at_earliest_sequence() -> None: + """在最早命中的终止序列处截断(换行优先于"答")。""" + # 数据:文本中包含换行与"答"。 + raw = "识别结果\n答:多余内容" + + # 测试过程与验证结果 + assert _truncate_at_stop(raw) == "识别结果" + + +def test_truncate_at_stop_returns_raw_when_no_match() -> None: + """未命中终止序列时原样返回。""" + # 数据:不含任何终止序列。 + # 测试过程与验证结果 + assert _truncate_at_stop("普通文本") == "普通文本" + + +def test_stop_sequence_includes_newline_first() -> None: + """终止序列以换行为首(输出首个换行即停),并包含"答"类重复模式。""" + # 数据:模块常量。 + # 测试过程与验证结果 + assert STOP_SEQUENCE[0] == "\n" + assert "答" in STOP_SEQUENCE + + +# --------------------------------------------------------------------------- +# 流式读取 +# --------------------------------------------------------------------------- + + +def test_consume_stream_joins_chunks() -> None: + """多块流式内容按序拼接返回。""" + # 数据:三个内容块 + done 行。 + response = _FakeResponse(_stream_lines("前", "中", "后")) + + # 测试过程 + raw = _consume_stream(response, deadline=1e18) + + # 验证结果 + assert raw == "前中后" + + +def test_consume_stream_stops_at_stop_sequence() -> None: + """命中终止序列后不再读取后续块(防止无限循环输出)。""" + # 数据:第一块已含换行,第二块是循环垃圾。 + response = _FakeResponse(_stream_lines("识别结果\n", "循环垃圾")) + + # 测试过程 + raw = _consume_stream(response, deadline=1e18) + + # 验证结果:只取到第一个块(含换行)。 + assert raw == "识别结果\n" + assert "循环垃圾" not in raw + + +def test_consume_stream_breaks_on_done_without_message() -> None: + """done 行不带 message 时视为正常结束,不报错。""" + # 数据:仅 done 行。 + response = _FakeResponse([json.dumps({"done": True}).encode("utf-8")]) + + # 测试过程与验证结果 + assert _consume_stream(response, deadline=1e18) == "" + + +def test_consume_stream_raises_on_missing_message() -> None: + """既无 message 又无 done 的块属于格式错误,明确报错。""" + # 数据:一个非法块。 + response = _FakeResponse([json.dumps({"unexpected": 1}).encode("utf-8")]) + + # 测试过程与验证结果 + with pytest.raises(KeyError): + _consume_stream(response, deadline=1e18) + + +def test_consume_stream_raises_on_deadline() -> None: + """超过整体截止时间立即终止(每次调用 5 秒上限)。""" + # 数据:截止时间已过。 + response = _FakeResponse(_stream_lines("内容", done=False)) + + # 测试过程与验证结果 + with pytest.raises(TimeoutError): + _consume_stream(response, deadline=0.0) + + +# --------------------------------------------------------------------------- +# invoke:请求体结构与产物 +# --------------------------------------------------------------------------- + + +def test_invoke_sends_system_prompt_and_image_only_user_message(monkeypatch, tmp_path: Path) -> None: + """请求体结构与 glm-ocr 期望一致:指令在 system,user 只带图片。""" + # 数据:捕获真实构造的 HTTP 请求体。 + captured: dict = {} + + def fake_urlopen(http_request, timeout=None): + captured["url"] = http_request.full_url + captured["body"] = json.loads(http_request.data.decode("utf-8")) + captured["timeout"] = timeout + return _FakeResponse(_stream_lines("识别文本")) + + monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen) + + # 测试过程 + response = invoke(_request(tmp_path, TEXT_IMAGE, ollama_host="http://example:1")) + + # 验证结果:URL、流式、系统提示词、user 只带 images、stop 与采样参数。 + assert response.status == "completed", response.error + assert captured["url"] == "http://example:1/api/chat" + body = captured["body"] + assert body["stream"] is True + assert body["messages"][0]["role"] == "system" + assert body["messages"][0]["content"] + assert body["messages"][1]["content"] == "" + assert len(body["messages"][1]["images"]) == 1 + assert body["stop"] == STOP_SEQUENCE + assert body["options"]["num_predict"] == 256 + assert "keep_alive" in body + + +def test_invoke_writes_ocr_artifact(monkeypatch, tmp_path: Path) -> None: + """识别结果写入 ocr.txt 并返回 text / text_uri 两个输出。 + + 真实调用约定:模型输出首个换行即停(stop=\"\\n\"),因此这里用单行输出; + 围栏清洗由 _clean_ocr_text 的独立用例覆盖。 + """ + # 数据:假响应返回单行识别结果 + 后续循环垃圾。 + monkeypatch.setattr( + urllib.request, "urlopen", + lambda *a, **k: _FakeResponse(_stream_lines("你好\n", "循环垃圾")), + ) + + # 测试过程 + response = invoke(_request(tmp_path, TEXT_IMAGE)) + + # 验证结果:产物只含首个换行之前的内容。 + assert response.status == "completed" + assert response.outputs["text"] == "你好" + artifact = Path(response.outputs["text_uri"]) + assert artifact.read_text(encoding="utf-8").strip() == "你好" + + +def test_invoke_options_are_overridable(monkeypatch, tmp_path: Path) -> None: + """temperature / repeat_penalty / num_predict 可被参数覆盖。""" + # 数据:捕获请求体。 + captured: dict = {} + + def fake_urlopen(http_request, timeout=None): + captured["body"] = json.loads(http_request.data.decode("utf-8")) + return _FakeResponse(_stream_lines("x")) + + monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen) + + # 测试过程 + invoke(_request( + tmp_path, TEXT_IMAGE, + temperature=0.9, repeat_penalty=1.5, num_predict=64, timeout_seconds=2, + )) + + # 验证结果 + options = captured["body"]["options"] + assert options["temperature"] == 0.9 + assert options["repeat_penalty"] == 1.5 + assert options["num_predict"] == 64 + + +def test_invoke_fails_without_image_uri(tmp_path: Path) -> None: + """缺少 image_uri 时返回 failed。""" + # 数据:空输入。 + # 测试过程 + response = invoke(_request(tmp_path, None)) + + # 验证结果 + assert response.status == "failed" + assert "image_uri" in (response.error or "") + + +def test_invoke_fails_when_image_missing(tmp_path: Path) -> None: + """图片文件不存在时提前失败,不发网络请求。""" + # 数据:不存在的图片路径。 + # 测试过程 + response = invoke(_request(tmp_path, tmp_path / "nope.png")) + + # 验证结果 + assert response.status == "failed" + assert "not found" in (response.error or "") + + +def test_invoke_fails_on_network_error(monkeypatch, tmp_path: Path) -> None: + """连接失败时返回 failed 并带上错误信息(不抛异常到调度器外)。""" + # 数据:urlopen 抛 URLError。 + def fake_urlopen(*a, **k): + raise urllib.error.URLError("connection refused") + + monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen) + + # 测试过程 + response = invoke(_request(tmp_path, TEXT_IMAGE)) + + # 验证结果 + assert response.status == "failed" + assert "connection refused" in (response.error or "") + + +def test_invoke_fails_on_timeout(monkeypatch, tmp_path: Path) -> None: + """超时(socket.timeout)时返回 failed,而不是长时间挂起。""" + # 数据:urlopen 抛 socket.timeout。 + def fake_urlopen(*a, **k): + raise socket.timeout("timed out") + + monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen) + + # 测试过程 + response = invoke(_request(tmp_path, TEXT_IMAGE)) + + # 验证结果 + assert response.status == "failed" + + +def test_invoke_fails_on_malformed_stream(monkeypatch, tmp_path: Path) -> None: + """流式块格式错误时返回 failed。""" + # 数据:非法块。 + monkeypatch.setattr( + urllib.request, "urlopen", + lambda *a, **k: _FakeResponse([json.dumps({"x": 1}).encode("utf-8")]), + ) + + # 测试过程 + response = invoke(_request(tmp_path, TEXT_IMAGE)) + + # 验证结果 + assert response.status == "failed" + + +# --------------------------------------------------------------------------- +# 真实模型集成 +# --------------------------------------------------------------------------- + + +def _ollama_reachable(host: str = "http://192.168.123.70:11434", model: str = "glm-ocr:latest") -> bool: + """探测真实 Ollama 服务与目标模型是否可用(缺失即跳过集成测试)。""" + try: + request = urllib.request.Request( + f"{host}/api/show", + data=json.dumps({"model": model}).encode("utf-8"), + headers={"Content-Type": "application/json"}, + method="POST", + ) + with urllib.request.urlopen(request, timeout=5) as resp: + return resp.status == 200 + except (urllib.error.URLError, OSError): + return False + + +@pytest.mark.integration +def test_vlm_ocr_real_model_on_real_subtitle_image(tmp_path: Path) -> None: + """真实 glm-ocr + 真实字幕截图:应识别出期望字幕文本,且无围栏垃圾。""" + # 数据:模块 data/ 下的真实字幕截图。 + if not REAL_SUBTITLE_IMAGE.is_file(): + pytest.skip(f"缺少测试资产 {REAL_SUBTITLE_IMAGE},跳过") + if not _ollama_reachable(): + pytest.skip("Ollama 服务或 glm-ocr 模型不可用,跳过真实模型集成测试") + + # 测试过程 + response = invoke(_request( + tmp_path, REAL_SUBTITLE_IMAGE, + model="glm-ocr:latest", ollama_host="http://192.168.123.70:11434", + )) + + # 验证结果:包含期望文本(允许尾部重复循环被截断),且无围栏。 + assert response.status == "completed", response.error + text = response.outputs["text"] + assert EXPECTED_REAL_TEXT in text, f"未识别出期望字幕:{text[:200]}" + assert "```" not in text + + +@pytest.mark.integration +def test_vlm_ocr_real_model_on_no_text_image(tmp_path: Path) -> None: + """真实无文字帧:模型不应输出画面描述文字(OCR 只提文字不做描述)。""" + # 数据:无文字测试图。 + if not NOTEXT_IMAGE.is_file(): + pytest.skip(f"缺少测试资产 {NOTEXT_IMAGE},跳过") + if not _ollama_reachable(): + pytest.skip("Ollama 服务或 glm-ocr 模型不可用,跳过真实模型集成测试") + + # 测试过程 + response = invoke(_request( + tmp_path, NOTEXT_IMAGE, + model="glm-ocr:latest", ollama_host="http://192.168.123.70:11434", + )) + + # 验证结果:要么成功且文本很短(无文字),要么失败——都不应出现长描述。 + if response.status == "completed": + assert len(response.outputs["text"].strip()) <= 40 diff --git a/tests/nodes/test_whisper/__init__.py b/tests/nodes/test_whisper/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/nodes/test_whisper/data/speech_60s.wav b/tests/nodes/test_whisper/data/speech_60s.wav new file mode 100644 index 0000000..b4e4765 Binary files /dev/null and b/tests/nodes/test_whisper/data/speech_60s.wav differ diff --git a/tests/nodes/test_whisper/test_transcribe.py b/tests/nodes/test_whisper/test_transcribe.py new file mode 100644 index 0000000..5c6162b --- /dev/null +++ b/tests/nodes/test_whisper/test_transcribe.py @@ -0,0 +1,545 @@ +"""nodes/whisper.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`nodes/whisper.py`(转写:模型解析 + 分块 + 时间轴合并 + 幻觉清洗 +入口),可独立调用。模型推理属允许替身的 I/O 边界:单元用例注入结构真实的 +假模型/假 ffmpeg;集成用例使用真实 faster-whisper 模型与真实语音。 +""" + +from __future__ import annotations + +import json +import os +import shutil +import subprocess +import wave +from pathlib import Path + +import pytest + +from nodes.ffmpeg import _ffmpeg_bin +from nodes.whisper import ( + _append_srt_lines, + _is_windows, + _load_cuda_libraries, + _local_model_candidates, + _wav_duration_seconds, + format_timestamp, + invoke, + resolve_model_path, +) +from wov_sdk.models import InvokeRequest + +# 模块专用真实素材:60 秒真实语音(16kHz 单声道 WAV)。 +DATA_DIR = Path(__file__).resolve().parent / "data" +SPEECH_WAV = DATA_DIR / "speech_60s.wav" + + +class FakeSegment: + """结构真实的 whisper 分段替身(start/end/text 与真实段一致)。""" + + def __init__(self, start: float, end: float, text: str) -> None: + self.start = start + self.end = end + self.text = text + + +class FakeInfo: + """结构真实的转写信息替身(含 language 字段)。""" + + def __init__(self, language: str = "ja") -> None: + self.language = language + + +class FakeModel: + """结构真实的假模型:按预置分段返回,并记录每次调用的参数。""" + + def __init__(self, segments: list[FakeSegment]) -> None: + self._segments = segments + self.calls: list[dict] = [] + + def transcribe(self, audio, **kwargs): + self.calls.append({"audio": str(audio), **kwargs}) + return iter(self._segments), FakeInfo() + + +def _request(tmp_path: Path, audio: Path | None, **params) -> InvokeRequest: + """构造真实请求;audio 为 None 时表示不传 audio_uri。""" + inputs = {} if audio is None else {"audio_uri": str(audio)} + return InvokeRequest( + run_id="run-test", + node_instance_id="whisper-1", + params=params, + inputs=inputs, + output_dir=str(tmp_path / "out"), + ) + + +def _inject_model(monkeypatch, model: FakeModel) -> None: + """把假模型注入到 faster_whisper.WhisperModel(I/O 边界替身)。 + + 节点在 invoke 内部 `from faster_whisper import WhisperModel` 延迟导入, + 因此必须 patch 库模块上的名字,才能让真实调用路径拿到假模型。 + """ + import faster_whisper + + monkeypatch.setattr(faster_whisper, "WhisperModel", lambda *a, **k: model) + # 假模型不需要真实权重,屏蔽 CUDA 库预加载以避免无 GPU 环境的副作用。 + monkeypatch.setattr("nodes.whisper._load_cuda_libraries", lambda: None) + + +# --------------------------------------------------------------------------- +# 模型路径解析(本地优先) +# --------------------------------------------------------------------------- + + +def test_resolve_model_path_explicit_param_wins(tmp_path: Path) -> None: + """请求参数 model_path 优先级最高。""" + # 数据:显式路径(含分隔符,按原样返回)。 + explicit = str(tmp_path / "custom-model") + + # 测试过程与验证结果 + assert resolve_model_path({"model_path": explicit}, env={}) == explicit + + +def test_resolve_model_path_bare_name_resolves_locally(tmp_path: Path) -> None: + """裸模型名在本地 model/ 目录下解析(存在 model.bin 时)。""" + # 数据:构造 <模型目录>/<名称>/model.bin。 + models_root = tmp_path / "model" + target = models_root / "my-model" + target.mkdir(parents=True) + (target / "model.bin").write_bytes(b"weights") + candidates = [models_root / "faster-whisper-large-v2"] + + # 测试过程 + resolved = resolve_model_path({"model_path": "my-model"}, env={}, candidates=candidates) + + # 验证结果:解析到本地目录。 + assert resolved == str(target) + + +def test_resolve_model_path_env_used_when_no_param(tmp_path: Path) -> None: + """无参数时使用 WHISPER_MODEL_PATH 环境变量。""" + # 数据:环境变量指向真实存在的模型目录。 + model_dir = tmp_path / "env-model" + model_dir.mkdir() + (model_dir / "model.bin").write_bytes(b"w") + + # 测试过程与验证结果 + assert resolve_model_path({}, env={"WHISPER_MODEL_PATH": str(model_dir)}) == str(model_dir) + + +def test_resolve_model_path_prefers_complete_local_candidate(tmp_path: Path) -> None: + """无参数/环境变量时使用本地候选目录(含 model.bin 才算完整)。""" + # 数据:第一个候选缺失 model.bin,第二个完整。 + broken = tmp_path / "broken" + broken.mkdir() + good = tmp_path / "good" + good.mkdir() + (good / "model.bin").write_bytes(b"w") + + # 测试过程 + resolved = resolve_model_path({}, env={}, candidates=[broken, good]) + + # 验证结果:跳过不完整候选,选中完整目录。 + assert resolved == str(good) + + +def test_resolve_model_path_falls_back_to_remote_name(tmp_path: Path) -> None: + """全部本地候选缺失时回退到可下载的模型名。""" + # 数据:空候选目录。 + empty = tmp_path / "empty" + empty.mkdir() + + # 测试过程与验证结果 + assert resolve_model_path({}, env={}, candidates=[empty]) == "large-v2" + + +def test_local_model_candidates_are_platform_paths() -> None: + """本地候选包含单体内置模型目录(跨平台用 pathlib 表达)。""" + # 数据:无。 + # 测试过程 + candidates = _local_model_candidates() + + # 验证结果:非空且都是 Path。 + assert candidates + assert all(isinstance(p, Path) for p in candidates) + + +# --------------------------------------------------------------------------- +# 时间戳与 WAV 时长 +# --------------------------------------------------------------------------- + + +def test_format_timestamp_pads_and_handles_hours() -> None: + """时间戳格式化为 HH:MM:SS,mmm,毫秒与小时均正确。""" + # 数据:0、1.5、3661.004 秒。 + # 测试过程与验证结果 + assert format_timestamp(0) == "00:00:00,000" + assert format_timestamp(1.5) == "00:00:01,500" + assert format_timestamp(3661.004) == "01:01:01,004" + + +def test_wav_duration_from_real_header(tmp_path: Path) -> None: + """WAV 时长按文件头精确计算(分块偏移依赖它,不能用假设块长)。""" + # 数据:3 秒合法 WAV。 + path = tmp_path / "3s.wav" + with wave.open(str(path), "wb") as wav: + wav.setnchannels(1) + wav.setsampwidth(2) + wav.setframerate(16000) + wav.writeframes(b"\x00\x00" * 16000 * 3) + + # 测试过程 + duration = _wav_duration_seconds(path, fallback=99.0) + + # 验证结果 + assert duration == pytest.approx(3.0, abs=0.01) + + +def test_wav_duration_falls_back_on_invalid_file(tmp_path: Path) -> None: + """非法 WAV 时回退到给定默认值(不抛异常中断整片转写)。""" + # 数据:非 WAV 内容。 + path = tmp_path / "broken.wav" + path.write_bytes(b"not a wav") + + # 测试过程与验证结果 + assert _wav_duration_seconds(path, fallback=42.0) == 42.0 + + +# --------------------------------------------------------------------------- +# SRT 行追加与时间轴偏移 +# --------------------------------------------------------------------------- + + +def test_append_srt_lines_applies_offset_and_index() -> None: + """分块转写按块偏移平移时间轴,并延续 SRT 序号。""" + # 数据:一段本地时间 0~2 秒的分段,偏移 60 秒,起始序号 5。 + segments = [FakeSegment(0.0, 2.0, "你好")] + + # 测试过程 + lines: list[str] = [] + added = _append_srt_lines(lines, segments, offset=60.0, start_index=5) + + # 验证结果:序号为 5,时间轴为 60~62 秒,返回本段新增条数 1。 + assert added == 1 + assert lines[0] == "5" + assert lines[1] == "00:01:00,000 --> 00:01:02,000" + assert lines[2] == "你好" + + +def test_append_srt_lines_strips_segment_text() -> None: + """分段文本两端空白被去除(避免 SRT 正文带多余空格)。""" + # 数据:一条文本带首尾空格。 + segments = [FakeSegment(1.0, 2.0, " 正常 ")] + + # 测试过程 + lines: list[str] = [] + _append_srt_lines(lines, segments, offset=0.0, start_index=1) + + # 验证结果:正文为去空白后的文本,序号为 1。 + assert lines[0] == "1" + assert lines[2] == "正常" + + +def test_append_srt_lines_continues_numbering_across_chunks() -> None: + """跨块调用时序号连续(调用方按上一块返回的条数累加)。""" + # 数据:两块各一段,第二块起始序号 = 1 + 第一块条数。 + first: list[str] = [] + added = _append_srt_lines(first, [FakeSegment(0, 1, "甲")], 0.0, 1) + second: list[str] = [] + _append_srt_lines(second, [FakeSegment(0, 1, "乙")], 60.0, 1 + added) + + # 验证结果:第二块序号为 2,时间轴带 60 秒偏移。 + assert second[0] == "2" + assert second[1].startswith("00:01:00,000") + + +# --------------------------------------------------------------------------- +# 平台分支 +# --------------------------------------------------------------------------- + + +def test_is_windows_flag_matches_platform() -> None: + """_is_windows 反映当前平台(测试需同时可在 Windows 与 Linux 运行)。""" + # 数据:当前运行平台。 + # 测试过程与验证结果 + assert _is_windows() == (os.name == "nt") + + +def test_load_cuda_libraries_is_noop_off_windows(tmp_path: Path, monkeypatch) -> None: + """非 Windows 平台加载 CUDA 库为无操作(Linux 由系统/venv 提供)。""" + # 数据:强制 _is_windows 为 False。 + monkeypatch.setattr("nodes.whisper._is_windows", lambda: False) + + # 测试过程与验证结果:不抛异常。 + _load_cuda_libraries() + + +def test_load_cuda_libraries_scans_site_packages_on_windows(tmp_path: Path, monkeypatch) -> None: + """Windows 下扫描 site-packages/nvidia/*/bin 并注册 DLL 搜索目录。""" + # 数据:伪造含 cublas/cudnn/cuda_nvrtc 三个厂商 bin 目录的 site-packages。 + site = tmp_path / "site-packages" + vendors = ("cublas", "cudnn", "cuda_nvrtc") + for package in vendors: + bin_dir = site / "nvidia" / package / "bin" + bin_dir.mkdir(parents=True) + (bin_dir / f"{package}.dll").write_bytes(b"dll") + added: list[str] = [] + monkeypatch.setattr("nodes.whisper._is_windows", lambda: True) + monkeypatch.setattr("nodes.whisper.sysconfig.get_paths", lambda: {"purelib": str(site)}) + monkeypatch.setattr("os.add_dll_directory", added.append, raising=False) + + # 测试过程 + _load_cuda_libraries() + + # 验证结果:三个厂商的 bin 目录都被加入 DLL 搜索路径。 + assert len(added) == len(vendors) + assert all("nvidia" in path for path in added) + + +def test_load_cuda_libraries_skips_missing_vendor_dirs(tmp_path: Path, monkeypatch) -> None: + """厂商目录不存在时跳过,不报错(部分轮子未安装)。""" + # 数据:只有 cublas 一个厂商目录。 + site = tmp_path / "site-packages" + (site / "nvidia" / "cublas" / "bin").mkdir(parents=True) + added: list[str] = [] + monkeypatch.setattr("nodes.whisper._is_windows", lambda: True) + monkeypatch.setattr("nodes.whisper.sysconfig.get_paths", lambda: {"purelib": str(site)}) + monkeypatch.setattr("os.add_dll_directory", added.append, raising=False) + + # 测试过程 + _load_cuda_libraries() + + # 验证结果:只注册存在的那一个。 + assert len(added) == 1 + + +# --------------------------------------------------------------------------- +# invoke:分块转写与合并(假模型) +# --------------------------------------------------------------------------- + + +def test_invoke_transcribes_with_fake_model_and_writes_srt(tmp_path: Path, monkeypatch) -> None: + """invoke 调用模型转写并写出 SRT(结构真实的分段替身)。""" + # 数据:真实 WAV 输入 + 假模型返回两段。 + assert SPEECH_WAV.is_file(), f"缺少测试素材 {SPEECH_WAV}" + model = FakeModel([FakeSegment(0.0, 2.0, "第一句"), FakeSegment(2.5, 4.0, "第二句")]) + _inject_model(monkeypatch, model) + + # 测试过程 + response = invoke(_request(tmp_path, SPEECH_WAV, chunk_seconds=0)) + + # 验证结果:产物存在且含两段文本与时间轴。 + assert response.status == "completed", response.error + content = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8") + assert "第一句" in content and "第二句" in content + assert "00:00:00,000 --> 00:00:02,000" in content + assert content.index("第一句") < content.index("第二句") + + +def test_invoke_passes_params_to_model(tmp_path: Path, monkeypatch) -> None: + """节点参数透传到模型调用(language/task/beam_size 等)。""" + # 数据:指定语言与任务。 + model = FakeModel([FakeSegment(0.0, 1.0, "x")]) + _inject_model(monkeypatch, model) + + # 测试过程 + invoke(_request( + tmp_path, SPEECH_WAV, chunk_seconds=0, + language="ja", task="translate", beam_size=3, vad_filter=False, + condition_on_previous_text=False, + )) + + # 验证结果:模型收到对应参数。 + call = model.calls[0] + assert call["language"] == "ja" + assert call["task"] == "translate" + assert call["beam_size"] == 3 + assert call["vad_filter"] is False + + +def test_invoke_default_vad_and_condition_flags(tmp_path: Path, monkeypatch) -> None: + """默认 vad_filter=True 且 condition_on_previous_text=False(防重复)。""" + # 数据:不传相关参数。 + model = FakeModel([FakeSegment(0.0, 1.0, "x")]) + _inject_model(monkeypatch, model) + + # 测试过程 + invoke(_request(tmp_path, SPEECH_WAV, chunk_seconds=0)) + + # 验证结果 + assert model.calls[0]["vad_filter"] is True + assert model.calls[0]["condition_on_previous_text"] is False + + +def test_invoke_fails_without_audio_uri(tmp_path: Path) -> None: + """缺少 audio_uri 时失败。""" + # 数据:空输入。 + # 测试过程 + response = invoke(_request(tmp_path, None)) + + # 验证结果 + assert response.status == "failed" + assert "audio_uri" in (response.error or "") + + +def test_invoke_fails_when_audio_missing(tmp_path: Path) -> None: + """音频文件不存在时失败。""" + # 数据:不存在的路径。 + # 测试过程 + response = invoke(_request(tmp_path, tmp_path / "nope.wav")) + + # 验证结果 + assert response.status == "failed" + assert "not found" in (response.error or "") + + +def test_invoke_cleans_japanese_hallucination_in_decode_full(tmp_path: Path, monkeypatch) -> None: + """decode_full 模式下,长时日语寒暄幻觉整条删除(不留下 '-' 占位)。""" + # 数据:假模型返回一段 30 秒的"おやすみなさい"。 + model = FakeModel([FakeSegment(0.0, 30.0, "おやすみなさい")]) + _inject_model(monkeypatch, model) + + # 测试过程 + response = invoke(_request(tmp_path, SPEECH_WAV, chunk_seconds=0, decode_full=True)) + + # 验证结果:幻觉被删除,产物无该文本。 + assert response.status == "completed", response.error + content = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8") + assert "おやすみなさい" not in content + + +def test_invoke_filters_short_moan_in_decode_full(tmp_path: Path, monkeypatch) -> None: + """decode_full 模式下短呻吟碎片被过滤,真实短对话保留。""" + # 数据:呻吟碎片 + 真实短对话。 + model = FakeModel([ + FakeSegment(0.0, 1.0, "あ…"), + FakeSegment(1.5, 3.0, "そこ、だめ"), + ]) + _inject_model(monkeypatch, model) + + # 测试过程 + response = invoke(_request(tmp_path, SPEECH_WAV, chunk_seconds=0, decode_full=True)) + + # 验证结果:呻吟被删,真实对话保留。 + assert response.status == "completed", response.error + content = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8") + assert "そこ、だめ" in content + assert "あ…" not in content + + +def test_invoke_keeps_moan_when_filter_disabled(tmp_path: Path, monkeypatch) -> None: + """short_moan_max_chars=0 时关闭呻吟过滤。""" + # 数据:一条呻吟。 + model = FakeModel([FakeSegment(0.0, 1.0, "あ…")]) + _inject_model(monkeypatch, model) + + # 测试过程 + response = invoke(_request( + tmp_path, SPEECH_WAV, chunk_seconds=0, decode_full=True, short_moan_max_chars=0, + )) + + # 验证结果:呻吟保留。 + assert response.status == "completed", response.error + content = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8") + assert "あ…" in content + + +# --------------------------------------------------------------------------- +# 真实模型集成 +# --------------------------------------------------------------------------- + + +# 已废弃的模型目录(不得用于测试):V3 已全面停用,全部改用 V2。 +_DEPRECATED_MODEL_DIRS = ("faster-whisper-large-v3",) + + +def _v2_model_candidates() -> list[Path]: + """返回可用的 V2 权重目录(排除已废弃的 V3)。 + + 解析顺序: + 1. `nodes.whisper` 文档化的默认候选(通用 V2 转写模型); + 2. `model/` 下其它已下载的 V2 权重(例如中文直出模型)。 + V3 权重已全面停用(用户 2026-09 决定:均改用 V2),即使留在盘上也不得 + 被测试使用,否则测的不是线上实际运行的模型。 + """ + candidates = [p for p in _local_model_candidates() if p.name not in _DEPRECATED_MODEL_DIRS] + model_root = Path(__file__).resolve().parents[3] / "model" + if model_root.is_dir(): + for path in sorted(model_root.iterdir()): + if not path.is_dir() or path.name in _DEPRECATED_MODEL_DIRS: + continue + # V3 的判据:preprocessor_config.json 的 feature_size == 128。 + if _is_v3_weights(path): + continue + candidates.append(path) + return candidates + + +def _is_v3_weights(model_dir: Path) -> bool: + """按 preprocessor_config.json 的 feature_size 判断是否为 V3 权重。 + + Whisper V2 的 mel 特征维度是 80,V3 是 128;这是区分两代权重的稳定判据 + (目录名可能被人工改名,不能只靠名字判断)。 + """ + import json + + config = model_dir / "preprocessor_config.json" + if not config.is_file(): + return False + try: + return int(json.loads(config.read_text(encoding="utf-8")).get("feature_size", 80)) == 128 + except (ValueError, TypeError, OSError): + return False + + +def _real_model_available() -> Path | None: + """返回一个可用的 V2 权重目录(缺失则返回 None 供跳过)。""" + for candidate in _v2_model_candidates(): + if candidate.is_dir() and (candidate / "model.bin").is_file(): + return candidate + return None + + +@pytest.mark.integration +def test_real_whisper_transcribes_real_speech(tmp_path: Path) -> None: + """真实 faster-whisper 模型 + 真实语音:端到端转写产出可用 SRT。 + + 本地无模型或素材时跳过;有则必须执行,作为假模型单测的校准。 + """ + # 数据:模块 data/ 下的真实 60 秒语音。 + if not SPEECH_WAV.is_file(): + pytest.skip(f"缺少测试素材 {SPEECH_WAV}") + model_dir = _real_model_available() + if model_dir is None: + pytest.skip("本地没有完整 whisper 权重,跳过真实模型集成测试") + # 显存不足时跳过(真实模型推理需要显存,属外部环境状态)。 + from tests.shared.gpu_memory import ( + fits_with_margin, + require_gpu_memory, + require_node_result, + ) + + require_gpu_memory(model_dir) + # 分块路径(生产默认)会产生更多分配峰值,在临界显存卡上易触发 CUDA OOM; + # 显存充裕时走分块覆盖该路径,否则退化为整段单次推理。 + chunk_seconds = 20 if fits_with_margin(model_dir) else 0 + + # 测试过程:真实模型转写真实语音。 + response = invoke(_request( + tmp_path, SPEECH_WAV, chunk_seconds=chunk_seconds, language="ja", + model_path=str(model_dir), + )) + + # 验证结果:成功、产物为合法 SRT、时间轴递增且不超音频时长。 + require_node_result(response, model_dir) + assert response.status == "completed", response.error + content = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8") + timelines = [line for line in content.splitlines() if "-->" in line] + assert timelines, "真实转写应产出至少一条字幕" + from tests.shared.srt_entries import parse_srt_entries + + entries = parse_srt_entries(content) + starts = [e["start"] for e in entries] + assert starts == sorted(starts) + assert max(starts) <= 62.0 diff --git a/tests/sdk/__init__.py b/tests/sdk/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/sdk/test_models/__init__.py b/tests/sdk/test_models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/sdk/test_models/test_models.py b/tests/sdk/test_models/test_models.py new file mode 100644 index 0000000..26a2713 --- /dev/null +++ b/tests/sdk/test_models/test_models.py @@ -0,0 +1,304 @@ +"""src/wov_sdk/models.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`src/wov_sdk/models.py`(协议数据模型:节点清单、调用请求/响应、 +工作流 DAG),是全部节点与平台之间的长期稳定契约,可独立调用。 +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from wov_sdk.models import ( + HealthResponse, + InvokeRequest, + InvokeResponse, + NodeManifest, + ProgressEvent, + WorkflowDefinition, + WorkflowEdge, + WorkflowNode, +) + +# 仓库根与真实清单目录。 +WORKSPACE = Path(__file__).resolve().parents[3] +MANIFESTS_DIR = WORKSPACE / "manifests" + + +# --------------------------------------------------------------------------- +# NodeManifest +# --------------------------------------------------------------------------- + + +def test_manifest_loads_from_real_json_file() -> None: + """真实 manifests/*.json 可被加载为清单对象(含必填字段)。""" + # 数据:仓库真实清单(echo 节点)。 + path = MANIFESTS_DIR / "echo.json" + assert path.is_file() + + # 测试过程 + manifest = NodeManifest.load(str(path)) + + # 验证结果 + assert manifest.id == "echo" + assert manifest.name + assert manifest.version + assert manifest.capability + + +@pytest.mark.parametrize("manifest_file", ["echo.json", "whisper.json", "vlm.json", "ass.json"]) +def test_all_real_manifests_validate(manifest_file: str) -> None: + """全部真实节点清单通过协议校验(注册表依赖此校验)。""" + # 数据:真实清单文件。 + # 测试过程 + manifest = NodeManifest.load(str(MANIFESTS_DIR / manifest_file)) + + # 验证结果:校验不抛异常且 ID 非空。 + manifest.validate() + assert manifest.id + + +def test_manifest_validate_rejects_empty_id() -> None: + """空 ID 视为非法(节点注册表以 ID 为键)。""" + # 数据:ID 为空的清单。 + manifest = NodeManifest(id="", name="x", version="1", capability="c", command=["python"]) + + # 测试过程与验证结果 + with pytest.raises(ValueError): + manifest.validate() + + +def test_manifest_validate_rejects_empty_version() -> None: + """空版本号非法(版本与节点仓库 tag 绑定)。""" + # 数据:版本为空的清单。 + manifest = NodeManifest(id="n", name="x", version="", capability="c", command=["python"]) + + # 测试过程与验证结果 + with pytest.raises(ValueError): + manifest.validate() + + +def test_manifest_round_trip_dict() -> None: + """清单 to_dict/from_dict 往返保持字段一致(协议序列化稳定)。""" + # 数据:完整清单。 + manifest = NodeManifest( + id="n1", name="节点", version="1.2.3", capability="cap", + command=["python", "-m", "n1"], repo_dir="nodes", + env={"A": "1"}, input_schema={"in": "file"}, output_schema={"out": "file"}, + max_concurrency=2, idle_ttl_seconds=100, health_timeout_seconds=5, keep_warm=True, + ) + + # 测试过程 + restored = NodeManifest.from_dict(manifest.to_dict()) + + # 验证结果 + assert restored.to_dict() == manifest.to_dict() + + +def test_manifest_rejects_unknown_required_field() -> None: + """缺少必填字段的字典加载时报错(不静默使用默认值)。""" + # 数据:缺 capability。 + payload = {"id": "n", "name": "x", "version": "1", "command": ["python"]} + + # 测试过程与验证结果 + with pytest.raises((KeyError, TypeError, ValueError)): + NodeManifest.from_dict(payload) + + +# --------------------------------------------------------------------------- +# InvokeRequest / InvokeResponse +# --------------------------------------------------------------------------- + + +def test_invoke_request_defaults_and_round_trip() -> None: + """调用请求的默认输出目录为当前目录,序列化往返一致。""" + # 数据:最小请求。 + request = InvokeRequest(run_id="r1", node_instance_id="n1") + + # 测试过程 + restored = InvokeRequest.from_dict(request.to_dict()) + + # 验证结果 + assert request.output_dir == "." + assert restored.inputs == {} and restored.params == {} + assert restored.run_id == "r1" + + +def test_invoke_response_success_and_failure_shapes() -> None: + """响应分成功(outputs)与失败(error)两种形态,序列化保留状态。""" + # 数据:成功与失败各一。 + ok = InvokeResponse(status="completed", outputs={"text": "hi"}) + bad = InvokeResponse(status="failed", error="boom") + + # 测试过程 + ok_round = InvokeResponse.from_dict(ok.to_dict()) + bad_round = InvokeResponse.from_dict(bad.to_dict()) + + # 验证结果 + assert ok_round.outputs == {"text": "hi"} and ok_round.error is None + assert bad_round.status == "failed" and bad_round.error == "boom" + + +def test_invoke_response_defaults() -> None: + """响应默认字段为空(避免 None 参与下游拼接)。""" + # 数据:仅给状态。 + response = InvokeResponse(status="completed") + + # 测试过程与验证结果 + assert response.outputs == {} + assert response.error is None + + +# --------------------------------------------------------------------------- +# HealthResponse / ProgressEvent +# --------------------------------------------------------------------------- + + +def test_health_response_round_trip() -> None: + """健康响应字段往返一致(节点身份与版本,无 from_dict 时按字典比对)。""" + # 数据:健康响应。 + health = HealthResponse(status="ok", node_id="echo", version="0.1.0") + + # 测试过程 + payload = health.to_dict() + + # 验证结果:包含状态、节点 ID 与版本。 + assert payload == {"status": "ok", "node_id": "echo", "version": "0.1.0"} + + +def test_progress_event_round_trip() -> None: + """进度事件含运行、节点、进度值与消息。""" + # 数据:一个进度事件。 + event = ProgressEvent(run_id="r1", node_id="asr", progress=0.5, message="转写中") + + # 测试过程 + payload = event.to_dict() + + # 验证结果 + assert payload == { + "run_id": "r1", "node_id": "asr", "progress": 0.5, "message": "转写中", + } + + +# --------------------------------------------------------------------------- +# WorkflowNode / WorkflowEdge / WorkflowDefinition +# --------------------------------------------------------------------------- + + +def test_workflow_node_preserves_all_params_keys() -> None: + """节点的 params 完整保留(含 `_note_*` 参数说明键,不被清洗)。""" + # 数据:含说明键的参数。 + payload = { + "id": "asr", "node_type": "faster-whisper", + "inputs": {"audio_uri": "extract.audio_uri"}, + "params": {"chunk_seconds": 60, "_note_chunk_seconds": "分块理由", "_node_help": "手册"}, + } + + # 测试过程 + node = WorkflowNode.from_dict(payload) + + # 验证结果:参数与说明键都在。 + assert node.params["chunk_seconds"] == 60 + assert node.params["_note_chunk_seconds"] == "分块理由" + assert node.params["_node_help"] == "手册" + assert node.to_dict()["params"] == payload["params"] + + +def test_workflow_edge_round_trip() -> None: + """边记录来源与目标节点,输出使用 from/to 短字段名。""" + # 数据:一条边。 + edge = WorkflowEdge.from_dict({"from": "a", "to": "b"}) + + # 测试过程与验证结果 + assert edge.from_node == "a" + assert edge.to_node == "b" + assert edge.to_dict() == {"from": "a", "to": "b"} + + +def test_workflow_definition_validate_accepts_valid_dag() -> None: + """合法 DAG 通过结构校验。""" + # 数据:a → b 线性链。 + definition = WorkflowDefinition.from_dict({ + "name": "流程", "version": 1, + "nodes": [ + {"id": "a", "node_type": "echo", "inputs": {}}, + {"id": "b", "node_type": "echo", "inputs": {"file_uri": "a.file_uri"}}, + ], + "edges": [{"from": "a", "to": "b"}], + "entry_inputs": {"video_uri": "file"}, + "final_outputs": {"out": "b.file_uri"}, + }) + + # 测试过程与验证结果:不抛异常。 + definition.validate() + + +def test_workflow_definition_validate_rejects_duplicate_node_ids() -> None: + """节点 ID 重复被拒绝。""" + # 数据:两个同 ID 节点。 + definition = WorkflowDefinition.from_dict({ + "name": "流程", "version": 1, + "nodes": [{"id": "a", "node_type": "echo", "inputs": {}}, {"id": "a", "node_type": "echo", "inputs": {}}], + "edges": [], + }) + + # 测试过程与验证结果 + with pytest.raises(ValueError): + definition.validate() + + +def test_workflow_definition_validate_rejects_edge_to_unknown_node() -> None: + """边引用不存在的节点被拒绝。""" + # 数据:边指向 ghost。 + definition = WorkflowDefinition.from_dict({ + "name": "流程", "version": 1, + "nodes": [{"id": "a", "node_type": "echo", "inputs": {}}], + "edges": [{"from": "a", "to": "ghost"}], + }) + + # 测试过程与验证结果 + with pytest.raises(ValueError): + definition.validate() + + +def test_workflow_definition_validate_rejects_empty_name() -> None: + """名称为空被拒绝。""" + # 数据:空名称。 + definition = WorkflowDefinition.from_dict({ + "name": "", "version": 1, "nodes": [], "edges": [], + }) + + # 测试过程与验证结果 + with pytest.raises(ValueError): + definition.validate() + + +def test_workflow_definition_round_trip_is_json_safe() -> None: + """定义可安全 JSON 序列化(工作流以 JSON 存库/传输)。""" + # 数据:含中文名称与参数的完整定义。 + definition = WorkflowDefinition.from_dict(json.loads( + (WORKSPACE / "workflows" / "ocr-subtitle.json").read_text(encoding="utf-8") + )["definition"]) + + # 测试过程 + payload = definition.to_dict() + text = json.dumps(payload, ensure_ascii=False) + + # 验证结果:往返一致且是合法 JSON 字符串。 + assert WorkflowDefinition.from_dict(json.loads(text)).to_dict() == payload + + +def test_real_builtin_workflows_are_valid() -> None: + """仓库全部内置工作流定义通过协议校验(数据资产可用)。""" + # 数据:真实 workflows/*.json。 + files = sorted((WORKSPACE / "workflows").glob("*.json")) + assert files, "仓库应包含内置工作流定义" + + # 测试过程与验证结果 + for path in files: + payload = json.loads(path.read_text(encoding="utf-8")) + definition = WorkflowDefinition.from_dict(payload["definition"]) + definition.validate() + assert definition.nodes, f"{path.name} 不应为空定义" diff --git a/tests/shared/__init__.py b/tests/shared/__init__.py new file mode 100644 index 0000000..342a169 --- /dev/null +++ b/tests/shared/__init__.py @@ -0,0 +1,18 @@ +"""测试公共设施包(供各模块测试复用,不含业务断言与隐式全局副作用)。 + +本包提供三类公共能力,供需要真实数据或环境隔离的模块测试使用: + +1. `realdata_contract`:真实数据契约(时间对齐素材、提示词规则素材的探查 + 与量化工具),只读取用户提供的真实文件,缺失时测试整体跳过; +2. `env_isolation`:环境与临时目录隔离,让依赖 `wov_app.config` 路径常量的 + 模块测试可以自建独立数据目录,不依赖全局 conftest; +3. `srt_entries`:SRT 条目解析(秒为单位),供参考字幕与产物字幕共用。 + +规则要求:模块专用数据放各模块目录下的 `data/`,本包只放真正跨模块复用的 +工具与跨模块共享素材(`tests/shared/data/`)。本包**不注册 pytest 钩子**, +隔离能力由各模块测试显式调用,保证每个模块目录可独立运行。 +""" + +from __future__ import annotations + +__all__: list[str] = [] diff --git a/tests/shared/conftest.py b/tests/shared/conftest.py new file mode 100644 index 0000000..0383aae --- /dev/null +++ b/tests/shared/conftest.py @@ -0,0 +1,19 @@ +"""公共设施包的 pytest 配置(仅测试进程级隔离,不注册 autouse fixture)。 + +由于 `wov_app.config` 在导入时锁定路径常量,这里在 pytest 启动早期把应用 +数据目录指向进程级临时目录,避免任何测试写入真实 `data/`。该配置属于 +`tests/shared/` 包本身,不构成"模块测试依赖外部配置":模块目录内的用例 +如需自己的临时目录,仍在模块 fixture 中用 `env_isolation` 显式创建。 +""" + +from __future__ import annotations + +import atexit + +from tests.shared import env_isolation + +# 进程启动即隔离,保证后续任何 import wov_app.config 都拿到测试路径。 +_ROOT = env_isolation.configure_environment() + +# 进程退出时清理临时目录。 +atexit.register(env_isolation.cleanup_data_root, _ROOT) diff --git a/tests/shared/data/alignment/ATID-642_2min.reference.srt b/tests/shared/data/alignment/ATID-642_2min.reference.srt new file mode 100644 index 0000000..e104e67 --- /dev/null +++ b/tests/shared/data/alignment/ATID-642_2min.reference.srt @@ -0,0 +1,103 @@ +7 +00:00:17,766 --> 00:00:21,828 +辛苦了 上午的检查已经OK了 + +16 +00:00:31,980 --> 00:00:34,011 +谢谢你 松井小姐 + +17 +00:00:34,010 --> 00:00:37,564 +还有没有什么困扰 或者奇怪的地方吗 + +18 +00:00:38,071 --> 00:00:39,594 +没问题啦 + +19 +00:00:39,594 --> 00:00:42,640 +总感觉果林前辈每次都会问这个呢 + +20 +00:00:42,640 --> 00:00:43,148 +总感觉果林前辈 每次都会问这个呢 + +21 +00:00:43,147 --> 00:00:46,193 +是呢 抱歉 + +22 +00:00:48,223 --> 00:00:49,239 +V + +23 +00:00:49,239 --> 00:00:51,777 +那么 松井小姐 + +24 +00:00:51,777 --> 00:00:52,792 +V + +25 +00:00:52,792 --> 00:00:55,838 +你来我们医院也才一个月吧 + +26 +00:00:56,345 --> 00:00:59,391 +记得挺快嘛 没有啦 + +27 +00:01:00,914 --> 00:01:04,467 +因为果林前辈教得好呀 + +28 +00:01:04,467 --> 00:01:08,527 +因为 你和患者们 也已经完全打成一片了 + +29 +00:01:09,036 --> 00:01:11,574 +我是北冈果林 + +30 +00:01:11,574 --> 00:01:16,143 +在这家综合医院工作的护士 + +31 +00:01:16,650 --> 00:01:19,188 +她叫松井日奈子 + +32 +00:01:19,188 --> 00:01:24,264 +是一个月前开始 在这间医院工作的新人护士 + +33 +00:01:25,279 --> 00:01:28,324 +上吧 上吧 + +34 +00:01:28,832 --> 00:01:33,908 +不行 受不了 好了 上吧 + +35 +00:01:37,462 --> 00:01:40,000 +状态不错 + +36 +00:01:40,000 --> 00:01:42,538 +请进 + +37 +00:01:47,107 --> 00:01:49,645 +早安 + +38 +00:01:50,660 --> 00:01:52,183 +金山先生 + +39 +00:01:52,183 --> 00:01:57,259 +昨晚你住院时我不在没能照顾到你非常抱歉 + +40 +00:01:57,259 --> 00:02:00,304 +我是医务室长权藤 请多关照 diff --git a/tests/shared/data/alignment/CJOD-255-长视频.reference.srt b/tests/shared/data/alignment/CJOD-255-长视频.reference.srt new file mode 100644 index 0000000..0430d77 --- /dev/null +++ b/tests/shared/data/alignment/CJOD-255-长视频.reference.srt @@ -0,0 +1,4591 @@ +1 +00:00:18,519 --> 00:00:20,521 +欢迎光临本店 + +2 +00:00:22,022 --> 00:00:27,528 +我将全心全意为客人服务 + +3 +00:00:30,030 --> 00:00:37,038 +今天尽情享受到你满足为止 + +4 +00:01:51,111 --> 00:01:55,116 +我是今天为你服务的茉莉亚 + +5 +00:01:55,616 --> 00:01:57,618 +请多多指教 + +6 +00:02:02,122 --> 00:02:05,626 +首先先脱衣服吧 + +7 +00:02:11,131 --> 00:02:14,135 +今天刚下班吗 + +8 +00:02:21,141 --> 00:02:23,644 +抱歉 我帮你脱 + +9 +00:02:26,146 --> 00:02:27,648 +非常感谢 + +10 +00:02:37,658 --> 00:02:40,661 +你的身体很热呢 + +11 +00:02:42,663 --> 00:02:44,665 +工作一定很累吧 + +12 +00:02:46,667 --> 00:02:49,670 +你是第一次来这种店吗 + +13 +00:02:53,674 --> 00:02:55,176 +很紧张吗 + +14 +00:02:55,175 --> 00:02:56,677 +有问题就要说哦 + +15 +00:03:00,180 --> 00:03:04,185 +你的身体很有反应呢 + +16 +00:03:06,186 --> 00:03:06,687 +那么立刻 + +17 +00:03:06,687 --> 00:03:07,688 +那么 立刻 + +18 +00:03:07,688 --> 00:03:08,689 +那么立刻 + +19 +00:03:13,193 --> 00:03:15,696 +帮你口交哦 + +20 +00:03:43,223 --> 00:03:46,727 +肉棒流了很多汗呢 + +21 +00:03:56,737 --> 00:03:58,739 +肉棒舒服吗 + +22 +00:03:59,740 --> 00:04:02,243 +身体反应很好呢 + +23 +00:04:12,252 --> 00:04:14,755 +快要射了吗 + +24 +00:04:16,255 --> 00:04:17,757 +请射出来吧 + +25 +00:04:18,257 --> 00:04:21,262 +射在我的嘴里吧 + +26 +00:04:55,295 --> 00:04:57,298 +射了很多精液呢 + +27 +00:04:59,299 --> 00:05:00,801 +很舒服吗 + +28 +00:05:03,303 --> 00:05:07,308 +本店可以射无数次哦 + +29 +00:05:09,309 --> 00:05:12,313 +请你射到满足为止 + +30 +00:05:13,814 --> 00:05:15,316 +请到这边来 + +31 +00:05:15,315 --> 00:05:16,816 +请站起来 + +32 +00:05:27,327 --> 00:05:29,830 +这样口交觉得怎样呢 + +33 +00:06:04,364 --> 00:06:08,869 +从上往下看 兴奋吗 + +34 +00:06:09,870 --> 00:06:13,373 +才刚射而已 又变大了哦 + +35 +00:06:13,373 --> 00:06:15,376 +很舒服 + +36 +00:06:21,381 --> 00:06:25,885 +还是因为奶子而兴奋呢 + +37 +00:06:26,887 --> 00:06:28,388 +不 + +38 +00:06:38,398 --> 00:06:41,402 +你其实很注意奶子吧 + +39 +00:06:42,402 --> 00:06:43,904 +对 + +40 +00:07:03,924 --> 00:07:07,928 +像这样子夹 + +41 +00:07:10,931 --> 00:07:12,433 +你很想乳交吧 + +42 +00:07:12,933 --> 00:07:13,933 +不 + +43 +00:07:15,936 --> 00:07:16,936 +很舒服 + +44 +00:07:16,937 --> 00:07:20,441 +用奶子夹很爽吧 + +45 +00:07:22,943 --> 00:07:29,950 +你喜欢口交还是乳交呢 + +46 +00:07:30,450 --> 00:07:32,953 +两种都喜欢 + +47 +00:07:32,953 --> 00:07:35,955 +-好爽 -两种都喜欢哦 + +48 +00:07:35,956 --> 00:07:36,957 +是 + +49 +00:07:37,958 --> 00:07:39,460 +换用吹的 + +50 +00:07:57,978 --> 00:08:00,481 +你这样吹 + +51 +00:08:03,483 --> 00:08:04,984 +我又要射了 + +52 +00:08:06,486 --> 00:08:07,988 +不行 太爽了 + +53 +00:08:08,488 --> 00:08:11,491 +不行 要射了 + +54 +00:08:11,491 --> 00:08:14,995 +要射了 + +55 +00:08:24,004 --> 00:08:26,006 +射了好多 + +56 +00:08:26,507 --> 00:08:29,010 +这么多精液 + +57 +00:08:29,510 --> 00:08:31,012 +射很多呢 + +58 +00:08:34,515 --> 00:08:37,018 +-舒服吗-是 + +59 +00:08:46,527 --> 00:08:49,030 +请坐下来吧 + +60 +00:08:49,029 --> 00:08:50,531 +是 + +61 +00:08:51,532 --> 00:08:54,535 +不过你还可以射吧 + +62 +00:08:56,537 --> 00:08:59,040 +我没射过3次 + +63 +00:08:59,540 --> 00:09:04,045 +没问题 我会让你兴奋 + +64 +00:09:04,044 --> 00:09:05,545 +让你勃起哦 + +65 +00:09:06,547 --> 00:09:07,548 +是 + +66 +00:09:18,559 --> 00:09:20,560 +肉棒又变大了哦 + +67 +00:09:22,563 --> 00:09:27,068 +还是不只是被吹 像这样 + +68 +00:09:41,582 --> 00:09:43,084 +可以吗 + +69 +00:09:44,084 --> 00:09:47,088 +让你更兴奋一点 + +70 +00:09:48,589 --> 00:09:50,091 +更硬一点 + +71 +00:09:52,593 --> 00:09:53,093 +-好爽 -变得更硬 + +72 +00:09:53,093 --> 00:09:55,596 +-好爽-变得更硬 + +73 +00:09:56,096 --> 00:09:57,598 +请射多一点 + +74 +00:10:00,601 --> 00:10:05,106 +我的奶头变硬了 + +75 +00:10:09,610 --> 00:10:10,611 +真硬呢 + +76 +00:10:12,613 --> 00:10:14,115 +非常舒服 + +77 +00:10:14,615 --> 00:10:16,617 +我也很舒服 + +78 +00:10:32,132 --> 00:10:32,632 +-不只是用摸的-是 + +79 +00:10:32,633 --> 00:10:34,135 +-不只是用摸的 -是 + +80 +00:10:34,134 --> 00:10:34,635 +-不只是用摸的-是 + +81 +00:10:35,135 --> 00:10:36,637 +像这样 + +82 +00:10:39,640 --> 00:10:43,144 +好好品尝一下吧 + +83 +00:10:45,646 --> 00:10:47,148 +怎样啊 + +84 +00:10:47,147 --> 00:10:50,151 +我的奶子怎样呢 + +85 +00:10:52,653 --> 00:10:54,155 +什么 + +86 +00:10:55,656 --> 00:10:58,159 +一定很爽吧 + +87 +00:10:58,659 --> 00:11:00,161 +我也玩奶头 + +88 +00:11:05,666 --> 00:11:07,168 +玩奶头 + +89 +00:11:07,668 --> 00:11:10,170 +在你享受奶子时 + +90 +00:11:15,676 --> 00:11:17,178 +我也很兴奋 + +91 +00:11:18,679 --> 00:11:21,682 +我们一起舒服吧 + +92 +00:11:29,690 --> 00:11:33,194 +像这样子玩肉棒 + +93 +00:11:41,201 --> 00:11:48,709 +你该不会是被虐狂吧 + +94 +00:11:49,209 --> 00:11:51,712 +喜欢被玩弄吗 + +95 +00:11:55,215 --> 00:11:56,217 +什么 + +96 +00:11:56,717 --> 00:12:01,222 +我听不懂 请说清楚一点 + +97 +00:12:04,224 --> 00:12:05,726 +像这样 + +98 +00:12:12,232 --> 00:12:14,735 +在这种状态 + +99 +00:12:15,235 --> 00:12:18,239 +这样打枪很爽吧 + +100 +00:12:22,242 --> 00:12:23,744 +你说什么 + +101 +00:12:25,746 --> 00:12:29,750 +我完全听不懂 + +102 +00:12:32,252 --> 00:12:33,254 +-这样很爽吗-很爽 + +103 +00:12:33,253 --> 00:12:33,754 +-这样很爽吗 -很爽 + +104 +00:12:33,754 --> 00:12:34,255 +-这样很爽吗-很爽 + +105 +00:12:34,254 --> 00:12:34,755 +-这样很爽吗 -很爽 + +106 +00:12:35,756 --> 00:12:37,757 +-这样子-是 + +107 +00:12:38,258 --> 00:12:41,762 +请张开嘴巴 + +108 +00:12:42,763 --> 00:12:43,764 +像这样 + +109 +00:12:52,773 --> 00:12:55,276 +喜欢这样玩吗 + +110 +00:12:55,776 --> 00:12:56,276 +-口水很好吃-被我玩弄 + +111 +00:12:56,276 --> 00:12:57,778 +-口水很好吃 -被我玩弄 + +112 +00:12:57,778 --> 00:12:58,279 +-口水很好吃-被我玩弄 + +113 +00:12:58,278 --> 00:12:58,779 +一口水很好吃 一被我玩弄 + +114 +00:13:02,282 --> 00:13:03,784 +果然如此 + +115 +00:13:11,792 --> 00:13:13,294 +-请你躺下去-是 + +116 +00:13:13,293 --> 00:13:14,795 +-请你躺下去 -是 + +117 +00:13:14,795 --> 00:13:15,295 +-请你躺下去-是 + +118 +00:13:24,304 --> 00:13:28,309 +请让我舒服吧 + +119 +00:13:28,308 --> 00:13:29,810 +是 + +120 +00:13:44,324 --> 00:13:50,831 +舔我的小穴 很兴奋吗 + +121 +00:14:04,845 --> 00:14:07,348 +我觉得好舒服 + +122 +00:14:09,850 --> 00:14:12,853 +请舔我的屁股 + +123 +00:14:13,353 --> 00:14:14,355 +是 + +124 +00:14:18,358 --> 00:14:19,860 +像这样 + +125 +00:14:35,876 --> 00:14:37,377 +好舒服 + +126 +00:14:37,878 --> 00:14:40,881 +请尽情的舔 + +127 +00:14:44,885 --> 00:14:47,887 +我觉得好兴奋 + +128 +00:14:47,888 --> 00:14:49,890 +又快要射了 + +129 +00:14:51,892 --> 00:14:53,394 +不可以哦 + +130 +00:14:54,895 --> 00:14:56,396 +先生 + +131 +00:14:57,898 --> 00:15:01,401 +这次换插我的小穴吧 + +132 +00:15:01,401 --> 00:15:01,901 +这次 换插我的小穴吧 + +133 +00:15:01,902 --> 00:15:02,903 +这次换插我的小穴吧 + +134 +00:15:02,903 --> 00:15:03,404 +这次 换插我的小穴吧 + +135 +00:15:03,403 --> 00:15:04,405 +这次换插我的小穴吧 + +136 +00:15:08,909 --> 00:15:11,411 +我也变得很兴奋 + +137 +00:15:13,914 --> 00:15:16,917 +快点做爱吧 + +138 +00:15:19,920 --> 00:15:21,422 +要插入吗 + +139 +00:15:22,923 --> 00:15:25,425 +我也想要舒服 + +140 +00:15:26,927 --> 00:15:28,429 +插进去 + +141 +00:15:41,942 --> 00:15:43,944 +-好棒-好舒服 + +142 +00:15:44,945 --> 00:15:49,449 +你的肉棒塞满我的小穴 + +143 +00:15:50,450 --> 00:15:53,954 +我的小穴好舒服 + +144 +00:15:57,958 --> 00:15:58,459 +-好爽 -像这样 + +145 +00:15:58,458 --> 00:15:59,959 +-好爽-像这样 + +146 +00:16:01,461 --> 00:16:03,964 +这样动 爽吗 + +147 +00:16:03,964 --> 00:16:04,965 +很爽 + +148 +00:16:06,967 --> 00:16:08,968 +很喜欢做爱呢 + +149 +00:16:15,475 --> 00:16:16,977 +像这样 + +150 +00:16:24,985 --> 00:16:27,488 +一边舔奶子 + +151 +00:16:29,489 --> 00:16:31,992 +请好好享受吧 + +152 +00:16:38,498 --> 00:16:41,502 +我的小穴好舒服 + +153 +00:16:44,505 --> 00:16:48,009 +肉棒插得好深 + +154 +00:16:49,510 --> 00:16:51,012 +奶子好爽 + +155 +00:16:51,512 --> 00:16:53,014 +喜欢奶子吗 + +156 +00:16:54,014 --> 00:16:55,516 +我最喜欢了 + +157 +00:17:04,525 --> 00:17:07,528 +插到小穴深处 + +158 +00:17:13,534 --> 00:17:15,036 +好棒 + +159 +00:17:20,040 --> 00:17:25,046 +插得我好爽 + +160 +00:17:38,559 --> 00:17:40,561 +插的很爽吧 + +161 +00:17:41,061 --> 00:17:44,065 +很舒服 + +162 +00:17:45,065 --> 00:17:46,567 +舒服吗 + +163 +00:17:49,069 --> 00:17:50,571 +像这样 + +164 +00:18:01,582 --> 00:18:04,585 +被我玩弄奶头 + +165 +00:18:05,085 --> 00:18:07,088 +肉棒狂插小穴 + +166 +00:18:10,591 --> 00:18:12,593 +请伸出舌头 + +167 +00:18:33,113 --> 00:18:33,614 +-请尽情享受 -是 + +168 +00:18:33,614 --> 00:18:34,115 +-请尽情享受-是 + +169 +00:18:34,114 --> 00:18:34,615 +-请尽情享受 -是 + +170 +00:18:34,615 --> 00:18:35,616 +-请尽情享受-是 + +171 +00:18:35,616 --> 00:18:36,117 +-请尽情享受 -是 + +172 +00:18:40,120 --> 00:18:42,123 +好舒服 + +173 +00:18:55,135 --> 00:18:59,139 +先生 肉棒又变热了呢 + +174 +00:19:00,140 --> 00:19:01,642 +太舒服了 + +175 +00:19:01,642 --> 00:19:04,645 +肉棒很硬哦 + +176 +00:19:05,145 --> 00:19:06,647 +插的很深 + +177 +00:19:07,147 --> 00:19:09,149 +请射出来 + +178 +00:19:09,149 --> 00:19:12,153 +请射多一点 + +179 +00:19:16,156 --> 00:19:17,158 +不行 + +180 +00:19:17,658 --> 00:19:19,660 +要射了 + +181 +00:19:19,660 --> 00:19:22,163 +射吧 请射多一点 + +182 +00:19:25,165 --> 00:19:26,667 +射了 + +183 +00:19:31,171 --> 00:19:32,673 +好棒 + +184 +00:19:32,673 --> 00:19:34,675 +射了很多精液 + +185 +00:19:37,678 --> 00:19:42,683 +今天射很多次呢 + +186 +00:19:46,687 --> 00:19:50,191 +射到里面 舒服吗 + +187 +00:19:50,691 --> 00:19:52,693 +非常舒服 + +188 +00:19:59,700 --> 00:20:02,203 +射了很多呢 + +189 +00:20:04,204 --> 00:20:05,206 +不过 + +190 +00:20:07,207 --> 00:20:09,710 +还没有结束吧 + +191 +00:20:14,214 --> 00:20:16,216 +我帮你口交 + +192 +00:20:32,232 --> 00:20:37,238 +再来换背后位吧 + +193 +00:20:38,739 --> 00:20:40,241 +是 + +194 +00:20:50,751 --> 00:20:57,258 +从后面干 感觉很不同哦 + +195 +00:21:18,278 --> 00:21:19,280 +肉棒进进出出 干的我好爽 + +196 +00:21:19,279 --> 00:21:19,780 +肉棒进进出出干的我好爽 + +197 +00:21:19,780 --> 00:21:26,287 +肉棒进进出出 干的我好爽 + +198 +00:21:26,286 --> 00:21:26,787 +肉棒进进出出干的我好爽 + +199 +00:21:26,787 --> 00:21:27,288 +肉棒进进出出 干的我好爽 + +200 +00:21:33,293 --> 00:21:36,297 +请尽情享受吧 + +201 +00:21:45,305 --> 00:21:48,809 +我觉得好爽 + +202 +00:21:57,317 --> 00:22:01,322 +-请尽情的干我-是 + +203 +00:22:06,326 --> 00:22:08,829 +我觉得好爽 + +204 +00:22:21,842 --> 00:22:22,843 +好棒 + +205 +00:22:24,845 --> 00:22:26,847 +请用力干我 + +206 +00:22:37,858 --> 00:22:46,867 +要不要从前面干呢 + +207 +00:22:47,868 --> 00:22:48,869 +可以吗 + +208 +00:22:56,376 --> 00:23:01,382 +用正常位一起舒服吧 + +209 +00:23:04,384 --> 00:23:05,886 +可以吗 + +210 +00:23:14,394 --> 00:23:15,896 +好爽 + +211 +00:23:16,396 --> 00:23:19,399 +正常位也很爽 + +212 +00:23:27,407 --> 00:23:31,412 +用力插我的小穴吧 + +213 +00:23:34,915 --> 00:23:39,920 +这样插 我好舒服 + +214 +00:23:52,432 --> 00:23:52,933 +-好棒 -很舒服 + +215 +00:23:52,933 --> 00:23:53,434 +-好棒-很舒服 + +216 +00:23:53,433 --> 00:23:53,934 +-好棒 -很舒服 + +217 +00:23:53,934 --> 00:23:54,435 +-好棒-很舒服 + +218 +00:23:55,435 --> 00:24:01,442 +这样干 我觉得好爽 + +219 +00:24:02,442 --> 00:24:05,946 +先生 你觉得舒服吗 + +220 +00:24:05,946 --> 00:24:07,447 +非常舒服 + +221 +00:24:09,449 --> 00:24:10,951 +-这样好舒服-好爽 + +222 +00:24:10,951 --> 00:24:12,453 +-这样好舒服 -好爽 + +223 +00:24:12,452 --> 00:24:12,953 +-这样好舒服-好爽 + +224 +00:24:12,953 --> 00:24:13,454 +-这样好舒服 -好爽 + +225 +00:24:13,453 --> 00:24:14,455 +-这样好舒服-好爽 + +226 +00:24:15,956 --> 00:24:18,959 +我要去了 + +227 +00:24:18,959 --> 00:24:20,961 +我觉得好爽 + +228 +00:24:28,969 --> 00:24:30,971 +又要去了 + +229 +00:24:41,982 --> 00:24:44,985 +我又高潮了 + +230 +00:24:46,486 --> 00:24:47,488 +真爽 + +231 +00:25:06,507 --> 00:25:07,008 +-这样干很爽呢 -好舒服 + +232 +00:25:07,007 --> 00:25:07,508 +-这样干很爽呢-好舒服 + +233 +00:25:07,508 --> 00:25:08,009 +-这样干很爽呢 -好舒服 + +234 +00:25:08,008 --> 00:25:08,509 +-这样干很爽呢-好舒服 + +235 +00:25:08,509 --> 00:25:09,010 +-这样干很爽呢 -好舒服 + +236 +00:25:09,009 --> 00:25:09,510 +-这样干很爽呢-好舒服 + +237 +00:25:09,510 --> 00:25:10,011 +-这样干很爽呢 -好舒服 + +238 +00:25:10,010 --> 00:25:11,512 +-这样干很爽呢-好舒服 + +239 +00:25:12,012 --> 00:25:13,014 +插很深呢 + +240 +00:25:22,022 --> 00:25:25,026 +我很舒服 + +241 +00:25:25,526 --> 00:25:27,028 +好爽 + +242 +00:25:29,530 --> 00:25:32,533 +你插的我好爽 + +243 +00:25:39,039 --> 00:25:42,043 +请尽情享受 + +244 +00:25:50,050 --> 00:25:52,053 +好舒服 + +245 +00:25:55,556 --> 00:25:57,558 +又要去了 + +246 +00:26:05,566 --> 00:26:07,068 +很舒服 + +247 +00:26:08,068 --> 00:26:10,071 +非常爽 + +248 +00:26:23,584 --> 00:26:25,586 +我觉得好爽 + +249 +00:26:27,087 --> 00:26:28,589 +尽情享受 + +250 +00:26:32,092 --> 00:26:34,595 +不行 我忍不住了 + +251 +00:26:34,595 --> 00:26:35,096 +-可以射吗-请射出来 + +252 +00:26:35,095 --> 00:26:36,597 +-可以射吗 -请射出来 + +253 +00:26:37,598 --> 00:26:40,101 +请射多一点 + +254 +00:26:41,101 --> 00:26:45,106 +要射了 + +255 +00:27:01,622 --> 00:27:05,626 +又射了很多呢 + +256 +00:27:06,627 --> 00:27:10,131 +我的小穴舒服吗 + +257 +00:27:11,632 --> 00:27:13,134 +非常舒服 + +258 +00:27:17,137 --> 00:27:21,142 +你还可以射吧 + +259 +00:27:21,642 --> 00:27:26,147 +我不行了 + +260 +00:27:43,664 --> 00:27:45,666 +肉棒又变硬了呢 + +261 +00:27:48,168 --> 00:27:52,173 +可以跟我做爱吧 + +262 +00:27:54,675 --> 00:27:57,178 +接下来玩骑乘位 + +263 +00:28:01,682 --> 00:28:05,186 +骑在你的身上 + +264 +00:28:12,693 --> 00:28:14,695 +先生 + +265 +00:28:17,197 --> 00:28:19,200 +很舒服 + +266 +00:28:19,700 --> 00:28:21,202 +舒服吗 + +267 +00:28:22,703 --> 00:28:25,706 +玩你的奶头 + +268 +00:28:31,211 --> 00:28:32,713 +这样动 + +269 +00:28:41,221 --> 00:28:43,224 +插的很深呢 + +270 +00:28:44,224 --> 00:28:47,228 +肉棒插到小穴深处 + +271 +00:28:48,729 --> 00:28:50,731 +插的好深 + +272 +00:29:00,741 --> 00:29:02,743 +好舒服 + +273 +00:29:03,744 --> 00:29:05,745 +插的好深 + +274 +00:29:08,749 --> 00:29:10,251 +舒服吗 + +275 +00:29:12,753 --> 00:29:14,754 +像这样 + +276 +00:29:19,760 --> 00:29:21,262 +上下动 + +277 +00:29:25,265 --> 00:29:28,269 +这样插舒服吗 + +278 +00:29:28,769 --> 00:29:29,770 +好爽 + +279 +00:29:30,771 --> 00:29:34,774 +用力的插我 + +280 +00:29:39,279 --> 00:29:41,782 +这样很爽吧 + +281 +00:29:43,283 --> 00:29:46,787 +我也觉得很爽 + +282 +00:29:56,296 --> 00:29:57,798 +很舒服 + +283 +00:29:58,298 --> 00:29:59,800 +舒服吗 + +284 +00:30:12,813 --> 00:30:15,816 +像这样 + +285 +00:30:19,820 --> 00:30:25,326 +边干边舔奶头会更爽吧 + +286 +00:30:30,330 --> 00:30:32,333 +这样 + +287 +00:30:33,333 --> 00:30:35,836 +喜欢这样舔吗 + +288 +00:30:41,842 --> 00:30:43,344 +跑掉了 + +289 +00:30:55,355 --> 00:30:57,858 +肉棒插的好深 + +290 +00:30:59,860 --> 00:31:02,363 +奶头被我玩弄 + +291 +00:31:02,863 --> 00:31:04,365 +好爽 + +292 +00:31:05,866 --> 00:31:07,368 +太爽了 + +293 +00:31:16,877 --> 00:31:19,379 +换舔另一边吧 + +294 +00:31:35,896 --> 00:31:36,897 +先生 + +295 +00:31:39,399 --> 00:31:42,403 +好好享受吧 + +296 +00:31:43,904 --> 00:31:46,407 +非常舒服 + +297 +00:32:02,422 --> 00:32:04,925 +我又快要射了 + +298 +00:32:05,425 --> 00:32:07,428 +射出来吧 + +299 +00:32:12,933 --> 00:32:14,935 +不行 要射了 + +300 +00:32:16,436 --> 00:32:18,439 +射在里面吧 + +301 +00:32:29,950 --> 00:32:32,953 +又射在里面了呢 + +302 +00:32:49,970 --> 00:32:50,971 +舒服吗 + +303 +00:32:51,972 --> 00:32:53,474 +不过 + +304 +00:32:56,476 --> 00:32:57,978 +还没结束 + +305 +00:33:04,985 --> 00:33:07,488 +让你更舒服一点 + +306 +00:33:13,994 --> 00:33:15,995 +已经没精液了 + +307 +00:33:22,002 --> 00:33:23,504 +没问题哦 + +308 +00:33:24,004 --> 00:33:28,009 +你会越来越舒服 + +309 +00:33:32,012 --> 00:33:39,020 +本店会让客人爽到满意为止 + +310 +00:33:39,019 --> 00:33:42,023 +-可是-请好好享受吧 + +311 +00:33:42,022 --> 00:33:42,523 +-可是 -请好好享受吧 + +312 +00:33:46,026 --> 00:33:49,530 +肉棒果然又变大了呢 + +313 +00:33:52,032 --> 00:33:55,036 +比刚才还要硬哦 + +314 +00:33:55,035 --> 00:33:58,039 +可是已经射很多次 + +315 +00:33:59,540 --> 00:34:04,045 +你想要更舒服吧 + +316 +00:34:09,550 --> 00:34:11,552 +又要射了 + +317 +00:34:12,052 --> 00:34:14,055 +射出来吧 + +318 +00:34:17,558 --> 00:34:19,560 +射出来了呢 + +319 +00:34:20,561 --> 00:34:21,062 +不过像这样 等等 + +320 +00:34:21,061 --> 00:34:22,563 +-不过像这样 -等等 + +321 +00:34:22,563 --> 00:34:23,064 +不过像这样 等等 + +322 +00:34:23,063 --> 00:34:23,564 +-不过像这样 一等等 + +323 +00:34:23,563 --> 00:34:24,065 +-不过像这样 -等等 + +324 +00:34:26,065 --> 00:34:28,569 +这样会更爽哦 + +325 +00:34:29,570 --> 00:34:30,571 +等等 + +326 +00:34:31,071 --> 00:34:31,572 +可以体验前所未有的快感哦 + +327 +00:34:31,572 --> 00:34:32,073 +-可以体验前所未有的快感哦 + +328 +00:34:32,072 --> 00:34:32,573 +可以体验前所未有的快感哦 + +329 +00:34:32,572 --> 00:34:36,077 +-可以体验前所未有的快感哦 + +330 +00:34:38,078 --> 00:34:40,581 +交给我处理 + +331 +00:34:41,581 --> 00:34:43,084 +请好好享受吧 + +332 +00:34:45,085 --> 00:34:48,089 +请你快点住手 + +333 +00:34:48,589 --> 00:34:50,091 +很舒服吧 + +334 +00:34:50,591 --> 00:34:52,593 +继续玩吧 + +335 +00:34:53,092 --> 00:34:56,597 +好好的享受吧 + +336 +00:35:02,603 --> 00:35:04,605 +快要射了 + +337 +00:35:04,605 --> 00:35:06,607 +-射出来吧-不行 + +338 +00:35:11,111 --> 00:35:13,114 +射出来了 + +339 +00:35:14,615 --> 00:35:16,117 +好棒 + +340 +00:35:18,118 --> 00:35:18,619 +-流出很多淫水 -不行 + +341 +00:35:18,619 --> 00:35:19,120 +-流出很多淫水-不行 + +342 +00:35:19,119 --> 00:35:20,121 +-流出很多淫水 -不行 + +343 +00:35:20,120 --> 00:35:21,122 +-流出很多淫水-不行 + +344 +00:35:24,124 --> 00:35:27,128 +很爽才会喷出淫水哦 + +345 +00:35:28,128 --> 00:35:30,131 +没有了吗 + +346 +00:35:30,631 --> 00:35:32,633 +又喷出来了 + +347 +00:35:32,633 --> 00:35:33,634 +-你很舒服呢 -请住手 + +348 +00:35:33,634 --> 00:35:34,135 +-你很舒服呢-请住手 + +349 +00:35:34,134 --> 00:35:34,635 +-你很舒服呢 -请住手 + +350 +00:35:34,635 --> 00:35:35,636 +-你很舒服呢-请住手 + +351 +00:35:35,636 --> 00:35:36,137 +-你很舒服呢 -请住手 + +352 +00:35:36,136 --> 00:35:36,637 +-你很舒服呢-请住手 + +353 +00:35:38,138 --> 00:35:41,142 +很高兴你这么爽 + +354 +00:35:46,647 --> 00:35:47,648 +没有了 + +355 +00:35:48,148 --> 00:35:49,650 +我知道了 + +356 +00:35:51,652 --> 00:35:54,155 +很舒服吗 + +357 +00:35:56,657 --> 00:36:00,661 +不过今天才刚开始而已 + +358 +00:36:01,662 --> 00:36:04,665 +一起享受吧 + +359 +00:36:30,691 --> 00:36:32,193 +让你久等了 + +360 +00:36:32,693 --> 00:36:38,699 +用泡沫来帮你洗澡吧 + +361 +00:36:39,700 --> 00:36:40,701 +麻烦你了 + +362 +00:36:45,706 --> 00:36:47,208 +是 + +363 +00:36:48,709 --> 00:36:53,214 +请好好享受泡沫吧 + +364 +00:36:55,215 --> 00:36:57,718 +涂在我身上 + +365 +00:37:02,222 --> 00:37:03,224 +像这样 + +366 +00:37:04,725 --> 00:37:09,730 +摩擦你的身体 + +367 +00:37:10,230 --> 00:37:11,232 +好爽 + +368 +00:37:15,736 --> 00:37:18,739 +用奶子摩擦身体 + +369 +00:37:24,745 --> 00:37:27,248 +用泡沫涂抹全身哦 + +370 +00:37:31,251 --> 00:37:32,753 +好舒服 + +371 +00:37:37,257 --> 00:37:38,259 +有碰到吗 + +372 +00:37:41,762 --> 00:37:46,267 +奶头摩擦奶头 + +373 +00:37:48,769 --> 00:37:51,772 +我的奶头变硬了 + +374 +00:38:04,785 --> 00:38:07,288 +摩擦你的身体 + +375 +00:38:10,791 --> 00:38:16,797 +用两个奶子摩擦身体 + +376 +00:38:21,802 --> 00:38:27,808 +像这样 帮你摩擦身体哦 + +377 +00:38:35,315 --> 00:38:36,817 +像这样 + +378 +00:38:37,317 --> 00:38:40,321 +摩擦你的奶头 + +379 +00:38:49,329 --> 00:38:51,332 +-舒服吗-是 + +380 +00:38:51,331 --> 00:38:51,832 +一舒服吗一是 + +381 +00:39:00,841 --> 00:39:04,345 +-帮你洗手臂吧-好 + +382 +00:39:10,350 --> 00:39:11,852 +像这样 + +383 +00:39:16,356 --> 00:39:17,858 +这个 + +384 +00:39:18,358 --> 00:39:21,362 +帮你摩擦手臂 + +385 +00:39:22,362 --> 00:39:27,868 +请好好享受奶子的触感 + +386 +00:39:28,869 --> 00:39:29,870 +好棒 + +387 +00:39:33,373 --> 00:39:34,875 +碰到奶子 + +388 +00:39:35,375 --> 00:39:36,877 +有碰到吗 + +389 +00:39:36,877 --> 00:39:40,381 +-有好好享受吗-是 + +390 +00:39:42,382 --> 00:39:43,884 +像这样 + +391 +00:39:48,388 --> 00:39:50,891 +上下摩擦你的手臂 + +392 +00:39:53,393 --> 00:39:56,397 +顺便洗我的奶子呢 + +393 +00:40:16,416 --> 00:40:17,918 +被夹住 + +394 +00:40:19,419 --> 00:40:24,925 +像这样 摩擦手臂吧 + +395 +00:40:27,928 --> 00:40:28,429 +-板夹了 + +396 +00:40:28,428 --> 00:40:28,929 +-极关键了 + +397 +00:40:29,930 --> 00:40:30,931 +-要确实的清洁手臂哦 + +398 +00:40:30,931 --> 00:40:31,932 +一极关住了 + +399 +00:40:32,432 --> 00:40:32,933 +-要确实的清洁手臂哦 + +400 +00:40:43,443 --> 00:40:44,945 +好爽 + +401 +00:40:45,946 --> 00:40:46,447 +-手臂舒服吗 -是 + +402 +00:40:46,446 --> 00:40:47,448 +-手臂舒服吗-是 + +403 +00:40:47,447 --> 00:40:48,449 +-手臂舒服吗 -是 + +404 +00:40:48,448 --> 00:40:48,949 +-手臂舒服吗-是 + +405 +00:40:50,450 --> 00:40:53,954 +请把手张开来 + +406 +00:40:53,954 --> 00:40:54,955 +对 + +407 +00:40:54,955 --> 00:40:56,457 +像这样 + +408 +00:40:56,957 --> 00:40:59,960 +洗一下手指吧 + +409 +00:41:07,968 --> 00:41:08,469 +-夹一下奶头-这边吗 + +410 +00:41:08,468 --> 00:41:09,470 +-夹一下奶头 -这边吗 + +411 +00:41:09,469 --> 00:41:09,970 +-夹一下奶头-这边吗 + +412 +00:41:09,970 --> 00:41:10,971 +-夹一下奶头 -这边吗 + +413 +00:41:10,971 --> 00:41:11,472 +-夹一下奶头-这边吗 + +414 +00:41:11,471 --> 00:41:12,473 +-夹一下奶头 -这边吗 + +415 +00:41:12,973 --> 00:41:15,476 +洗一下小指跟无名指吧 + +416 +00:41:15,976 --> 00:41:16,977 +是 + +417 +00:41:19,980 --> 00:41:22,983 +我的奶子也被洗乾净了 + +418 +00:41:28,989 --> 00:41:34,995 +再来洗另一边吧 + +419 +00:41:36,496 --> 00:41:41,502 +这次不是用奶子换用我的小穴来洗 + +420 +00:41:46,507 --> 00:41:48,509 +我之前没玩过 + +421 +00:41:50,010 --> 00:41:51,512 +第一次玩吗 + +422 +00:41:52,513 --> 00:41:54,515 +像这样 + +423 +00:41:56,517 --> 00:41:58,019 +碰到小穴了 + +424 +00:42:03,524 --> 00:42:10,031 +用我的小穴帮你洗乾净 + +425 +00:42:15,035 --> 00:42:16,537 +有碰到吧 + +426 +00:42:19,039 --> 00:42:21,542 +疯狂摩擦 + +427 +00:42:39,059 --> 00:42:41,062 +好舒服 + +428 +00:42:41,061 --> 00:42:45,066 +-请好好的享受-好爽 + +429 +00:42:45,065 --> 00:42:46,567 +非常爽 + +430 +00:42:47,067 --> 00:42:51,572 +我也觉得非常舒服 + +431 +00:43:01,081 --> 00:43:06,087 +再来我想玩壶洗 + +432 +00:43:06,086 --> 00:43:08,589 +壶洗什么意思呢 + +433 +00:43:09,590 --> 00:43:14,595 +用我的小穴来洗手指 + +434 +00:43:17,097 --> 00:43:19,600 +请伸出食指 + +435 +00:43:25,606 --> 00:43:26,607 +进去了 + +436 +00:43:32,112 --> 00:43:35,116 +对 手指进进出出 + +437 +00:43:35,616 --> 00:43:36,617 +对 + +438 +00:43:46,126 --> 00:43:47,628 +-很湿吗-对 + +439 +00:43:49,129 --> 00:43:53,134 +动一下你的手指 + +440 +00:43:53,133 --> 00:43:55,136 +是 这样吗 + +441 +00:43:55,636 --> 00:43:59,640 +对像这样动手指 + +442 +00:44:10,150 --> 00:44:13,154 +用两根手指插进去 + +443 +00:44:13,153 --> 00:44:14,655 +插我的小穴 + +444 +00:44:15,155 --> 00:44:17,158 +-这样吗-对 + +445 +00:44:23,163 --> 00:44:23,664 +对就是这样 + +446 +00:44:23,664 --> 00:44:24,165 +对 就是这样 + +447 +00:44:24,164 --> 00:44:25,166 +对就是这样 + +448 +00:44:26,667 --> 00:44:27,668 +-插进去了-小穴好爽 + +449 +00:44:27,668 --> 00:44:28,169 +-插进去了 -小穴好爽 + +450 +00:44:28,168 --> 00:44:30,171 +-插进去了-小穴好爽 + +451 +00:44:36,176 --> 00:44:39,680 +插到我最爽的部位 + +452 +00:44:53,694 --> 00:45:00,701 +被手指插到小穴深处 + +453 +00:45:01,201 --> 00:45:06,707 +我比客人先一步高潮了 + +454 +00:45:08,709 --> 00:45:14,715 +再来洗背部吧 请转过身去 + +455 +00:45:19,720 --> 00:45:22,223 +抱歉 失礼了 + +456 +00:45:29,229 --> 00:45:34,235 +现在帮你擦背哦 + +457 +00:45:40,240 --> 00:45:41,742 +好爽 + +458 +00:45:51,251 --> 00:45:51,752 +奶子摩擦背部有感觉写 + +459 +00:45:51,752 --> 00:45:52,253 +-奶子摩擦背部有感觉写 + +460 +00:45:52,252 --> 00:45:53,754 +奶子摩擦背部有感觉写 + +461 +00:45:53,754 --> 00:45:54,255 +奶子摩擦部位有感觉写 + +462 +00:45:54,254 --> 00:45:54,755 +-奶子犀擦背部有感觉写 + +463 +00:45:54,755 --> 00:45:56,757 +奶子摩擦背部有感觉写 + +464 +00:45:56,757 --> 00:45:57,258 +奶子犀擦背部有感觉写 + +465 +00:45:57,257 --> 00:45:57,758 +奶子摩擦背部有感觉写 + +466 +00:46:02,763 --> 00:46:03,764 +像这样 + +467 +00:46:10,771 --> 00:46:14,275 +玩弄你的奶头 怎样呢 + +468 +00:46:15,275 --> 00:46:18,279 +很爽 + +469 +00:46:18,278 --> 00:46:21,282 +喜欢这样玩吗 + +470 +00:46:23,283 --> 00:46:26,787 +奶头比刚才还硬呢 + +471 +00:46:27,788 --> 00:46:30,291 +觉得很兴奋哦 + +472 +00:46:32,292 --> 00:46:35,796 +肉棒也变硬了 + +473 +00:46:42,302 --> 00:46:43,804 +很舒服吧 + +474 +00:47:06,827 --> 00:47:11,832 +再继续帮你洗吧 + +475 +00:47:15,335 --> 00:47:16,837 +用泡沫帮你洗 + +476 +00:47:25,846 --> 00:47:30,351 +你的睾丸变得很紧哦 + +477 +00:47:32,352 --> 00:47:36,857 +帮你洗屁眼 + +478 +00:47:39,359 --> 00:47:41,862 +一起洗肉棒哦 + +479 +00:47:44,865 --> 00:47:45,366 +-让我看勃起的样子 -好爽 + +480 +00:47:45,365 --> 00:47:48,869 +-让我看勃起的样子-好爽 + +481 +00:47:52,372 --> 00:47:56,377 +-肉棒变得很硬-不行 + +482 +00:48:00,881 --> 00:48:03,384 +肉棒好硬 + +483 +00:48:04,384 --> 00:48:05,386 +不行 + +484 +00:48:05,886 --> 00:48:07,388 +快射了 + +485 +00:48:07,387 --> 00:48:08,889 +还不能哦 + +486 +00:48:08,889 --> 00:48:09,390 +还不能骑哦 + +487 +00:48:09,389 --> 00:48:10,891 +还不能哦 + +488 +00:48:10,891 --> 00:48:11,392 +还不能骑哦 + +489 +00:48:11,391 --> 00:48:11,892 +还不能哦 + +490 +00:48:13,393 --> 00:48:14,895 +还不能射哦 + +491 +00:48:14,895 --> 00:48:16,397 +请忍耐一下 + +492 +00:48:19,399 --> 00:48:20,401 +像这样 + +493 +00:48:20,901 --> 00:48:23,904 +涂上很多泡沫 + +494 +00:48:26,406 --> 00:48:27,908 +很湿呢 + +495 +00:48:30,911 --> 00:48:32,913 +像这样摸 + +496 +00:48:39,920 --> 00:48:41,922 +不行 太爽了 + +497 +00:48:43,924 --> 00:48:46,427 +请好好享受吧 + +498 +00:48:50,931 --> 00:48:52,933 +-像这样摸-不行 + +499 +00:48:52,933 --> 00:48:53,934 +一像这样摸一不行 + +500 +00:48:55,435 --> 00:48:57,438 +这么爽吗 + +501 +00:48:58,438 --> 00:48:59,940 +非常爽 + +502 +00:48:59,940 --> 00:49:01,442 +像这样 + +503 +00:49:05,946 --> 00:49:08,949 +看着我 + +504 +00:49:09,950 --> 00:49:11,952 +请露出舒服的表情 + +505 +00:49:12,452 --> 00:49:13,454 +好爽 + +506 +00:49:13,453 --> 00:49:14,455 +-舒服吗-是 + +507 +00:49:14,454 --> 00:49:14,955 +一舒服吗一是 + +508 +00:49:14,955 --> 00:49:15,456 +-舒服吗-是 + +509 +00:49:15,956 --> 00:49:17,458 +舔奶头 + +510 +00:49:17,958 --> 00:49:18,959 +不行 + +511 +00:49:28,969 --> 00:49:31,472 +不行 快射了 + +512 +00:49:40,480 --> 00:49:42,983 +不行 + +513 +00:49:45,485 --> 00:49:47,488 +还不能射哦 + +514 +00:50:02,503 --> 00:50:04,005 +不行 + +515 +00:50:05,506 --> 00:50:07,008 +舔奶头 + +516 +00:50:11,512 --> 00:50:14,015 +不行 + +517 +00:50:18,018 --> 00:50:19,520 +不行 + +518 +00:50:19,520 --> 00:50:21,522 +不行 我忍不住了 + +519 +00:50:21,522 --> 00:50:23,024 +请你忍耐 + +520 +00:50:23,524 --> 00:50:24,525 +快射了 + +521 +00:50:24,525 --> 00:50:27,028 +射了 + +522 +00:50:30,531 --> 00:50:33,034 +射出很多精液 + +523 +00:50:33,033 --> 00:50:34,035 +不行 + +524 +00:50:35,035 --> 00:50:36,537 +我射了 + +525 +00:50:38,038 --> 00:50:41,542 +不过 你还可以射吧 + +526 +00:50:43,544 --> 00:50:45,546 +像这样 + +527 +00:50:48,048 --> 00:50:51,552 +-怪怪的吗-要射了 + +528 +00:50:56,557 --> 00:50:58,059 +不行 + +529 +00:50:58,559 --> 00:51:00,561 +射多一点 + +530 +00:51:01,061 --> 00:51:02,063 +不行了 + +531 +00:51:02,563 --> 00:51:05,066 +不行 我没精液了 + +532 +00:51:06,066 --> 00:51:07,568 +不行 + +533 +00:51:10,070 --> 00:51:11,572 +没有了吗 + +534 +00:51:12,573 --> 00:51:15,576 +全部射光了吗 + +535 +00:51:16,076 --> 00:51:17,578 +是 + +536 +00:51:20,080 --> 00:51:22,583 +那 再洗一次吧 + +537 +00:51:54,114 --> 00:51:56,617 +水温可以吗 + +538 +00:51:57,117 --> 00:51:58,619 +刚刚好 + +539 +00:52:00,621 --> 00:52:01,622 +-我也一起泡吧 -是 + +540 +00:52:01,622 --> 00:52:04,125 +-我也一起泡吧-是 + +541 +00:52:04,124 --> 00:52:04,625 +-我也一起泡吧 -是 + +542 +00:52:14,635 --> 00:52:17,638 +泡澡很舒服哦 + +543 +00:52:20,641 --> 00:52:23,144 +刚才射很多呢 + +544 +00:52:24,645 --> 00:52:29,650 +请你好好放松吧 + +545 +00:52:33,654 --> 00:52:38,659 +这样还不够放松呢 + +546 +00:52:49,169 --> 00:52:51,672 +肉棒又变大了哦 + +547 +00:52:52,172 --> 00:52:53,674 +好爽 + +548 +00:52:56,176 --> 00:52:59,180 +你觉得很兴奋呢 + +549 +00:53:05,185 --> 00:53:07,688 +肉棒变得很硬 + +550 +00:53:13,193 --> 00:53:15,696 +刚才的精液 + +551 +00:53:18,699 --> 00:53:20,201 +好爽 + +552 +00:53:30,711 --> 00:53:32,713 +这样好爽 + +553 +00:53:39,219 --> 00:53:42,223 +肉棒又变的很硬哦 + +554 +00:53:42,222 --> 00:53:44,725 +我觉得很爽 + +555 +00:53:46,226 --> 00:53:48,729 +变得这么硬了 + +556 +00:53:49,229 --> 00:53:51,732 +才刚射过而已呢 + +557 +00:53:53,233 --> 00:53:56,237 +还可以射吧 + +558 +00:53:59,740 --> 00:54:06,247 +用我的奶子让你舒服 + +559 +00:54:06,747 --> 00:54:07,748 +好软 + +560 +00:54:11,752 --> 00:54:14,255 +肉棒变得好硬 + +561 +00:54:17,758 --> 00:54:18,259 +-不符 我快射了 + +562 +00:54:18,258 --> 00:54:19,260 +-不打 我快射了 + +563 +00:54:19,259 --> 00:54:20,261 +-不行 我快射了 + +564 +00:54:20,260 --> 00:54:20,761 +-不符 我快射了 + +565 +00:54:20,761 --> 00:54:21,262 +-不打我快射了 + +566 +00:54:21,261 --> 00:54:22,763 +请射出来吧 + +567 +00:54:27,267 --> 00:54:28,769 +不行 + +568 +00:54:28,769 --> 00:54:31,272 +又要射了 + +569 +00:54:41,782 --> 00:54:43,784 +又射精了呢 + +570 +00:54:45,285 --> 00:54:50,791 +不过 你还可以射吧 + +571 +00:55:01,301 --> 00:55:02,803 +好爽 + +572 +00:55:09,309 --> 00:55:11,812 +-舒服吗-是 + +573 +00:55:14,314 --> 00:55:19,820 +肉棒又变硬了呢 + +574 +00:55:22,823 --> 00:55:24,825 +还可以射精吧 + +575 +00:55:26,326 --> 00:55:27,828 +我不是有说过写 + +576 +00:55:27,828 --> 00:55:28,329 +我不是说过写 + +577 +00:55:28,328 --> 00:55:33,334 +我不是有说过写 + +578 +00:55:34,334 --> 00:55:37,338 +请你转到后面趴着 + +579 +00:55:37,838 --> 00:55:39,340 +转到后面 + +580 +00:55:46,346 --> 00:55:49,350 +请把右脚放在上面 + +581 +00:55:50,851 --> 00:55:51,352 +-这样吗 -对 + +582 +00:55:51,351 --> 00:55:51,852 +这样吗 对 + +583 +00:55:51,852 --> 00:55:52,353 +-这样吗-对 + +584 +00:55:52,352 --> 00:55:52,853 +这样吗 对 + +585 +00:55:52,853 --> 00:55:53,354 +-这样吗-对 + +586 +00:55:55,355 --> 00:55:56,857 +像这样 + +587 +00:55:56,857 --> 00:56:00,361 +-很害羞的姿势-好爽 + +588 +00:56:01,361 --> 00:56:03,864 +帮你打手枪 + +589 +00:56:04,865 --> 00:56:08,869 +好爽 + +590 +00:56:09,369 --> 00:56:10,871 +不行 + +591 +00:56:16,376 --> 00:56:17,878 +真爽 + +592 +00:56:18,879 --> 00:56:20,381 +快射了 + +593 +00:56:20,881 --> 00:56:22,883 +请射出来 + +594 +00:56:24,885 --> 00:56:25,386 +-请射多一点-不行 + +595 +00:56:25,385 --> 00:56:25,886 +-请射多一点 -不行 + +596 +00:56:25,886 --> 00:56:26,387 +-请射多一点-不行 + +597 +00:56:26,386 --> 00:56:26,887 +-请射多一点 -不行 + +598 +00:56:26,887 --> 00:56:28,389 +-请射多一点-不行 + +599 +00:56:28,388 --> 00:56:28,889 +-请射多一点 -不行 + +600 +00:56:29,389 --> 00:56:30,891 +要射了 + +601 +00:56:30,891 --> 00:56:31,892 +-射吧-要射了 + +602 +00:56:31,892 --> 00:56:32,393 +-射吧 -要射了 + +603 +00:56:34,394 --> 00:56:35,896 +不行 + +604 +00:56:36,396 --> 00:56:37,398 +射了 + +605 +00:56:48,909 --> 00:56:54,415 +才刚射过而已 又射很多呢 + +606 +00:56:57,417 --> 00:57:00,921 +-很舒服吗-是 + +607 +00:57:02,923 --> 00:57:07,928 +等一下玩软垫玩法吧 + +608 +00:57:10,931 --> 00:57:14,435 +让你更舒服一点哦 + +609 +00:57:43,964 --> 00:57:45,966 +让你久等了 + +610 +00:57:47,467 --> 00:57:53,474 +用润滑油来涂抹你的身体 + +611 +00:57:53,473 --> 00:57:54,975 +麻烦你了 + +612 +00:57:54,975 --> 00:58:00,481 +有用过润滑油吗 + +613 +00:58:03,984 --> 00:58:08,989 +像这样使用润滑油 + +614 +00:58:09,489 --> 00:58:17,498 +第一次玩软垫玩法吗 + +615 +00:58:18,999 --> 00:58:20,000 +是 + +616 +00:58:22,503 --> 00:58:26,507 +请好好享受吧 + +617 +00:58:26,507 --> 00:58:28,509 +麻烦你了 + +618 +00:58:29,510 --> 00:58:31,012 +先用这样 + +619 +00:58:37,017 --> 00:58:43,024 +用我的身体帮你摩擦 + +620 +00:58:43,023 --> 00:58:44,525 +麻烦你了 + +621 +00:58:48,529 --> 00:58:53,534 +爽吗 我的奶头变硬了 + +622 +00:59:00,040 --> 00:59:03,044 +这么兴奋的话 + +623 +00:59:05,546 --> 00:59:07,048 +这表示 + +624 +00:59:10,050 --> 00:59:12,053 +你很兴奋呢 + +625 +00:59:13,053 --> 00:59:15,056 +抱歉 我很爽 + +626 +00:59:26,066 --> 00:59:28,069 +这么爽吗 + +627 +00:59:30,571 --> 00:59:32,073 +好爽 + +628 +00:59:33,073 --> 00:59:36,577 +奶头变硬了呢 + +629 +00:59:38,579 --> 00:59:42,583 +这样玩很爽吗 + +630 +00:59:43,083 --> 00:59:44,085 +很爽 + +631 +00:59:44,585 --> 00:59:46,587 +非常爽 + +632 +00:59:49,089 --> 00:59:53,094 +肉棒变得很热 + +633 +00:59:58,599 --> 01:00:01,102 +换左边奶子吧 + +634 +01:00:02,102 --> 01:00:03,104 +好爽 + +635 +01:00:15,616 --> 01:00:17,618 +肉棒变的好硬 + +636 +01:00:22,623 --> 01:00:25,126 +变得很硬 + +637 +01:00:28,629 --> 01:00:31,132 +硬梆梆的呢 + +638 +01:00:36,637 --> 01:00:39,140 +玩更爽的玩法吧 + +639 +01:00:39,139 --> 01:00:41,142 +-请转过身去 -是 + +640 +01:00:41,141 --> 01:00:41,642 +-请转过身去-是 + +641 +01:00:41,642 --> 01:00:42,143 +-请转过身去 -是 + +642 +01:00:48,649 --> 01:00:49,150 +-现在帮你摩擦背部-是 + +643 +01:00:49,149 --> 01:00:50,651 +-现在帮你摩擦背部 -是 + +644 +01:00:50,651 --> 01:00:53,154 +-现在帮你摩擦背部-是 + +645 +01:00:53,153 --> 01:00:55,156 +-现在帮你摩擦背部 -是 + +646 +01:00:55,155 --> 01:00:56,657 +-现在帮你摩擦背部-是 + +647 +01:01:05,666 --> 01:01:09,670 +奶子碰到背部 有感觉吗 + +648 +01:01:10,170 --> 01:01:11,172 +有感觉 + +649 +01:01:14,174 --> 01:01:18,179 +同时玩你的奶头 + +650 +01:01:20,180 --> 01:01:23,184 +这样更舒服呢 + +651 +01:01:24,184 --> 01:01:25,686 +很舒服 + +652 +01:01:26,687 --> 01:01:28,189 +玩奶头 + +653 +01:01:30,691 --> 01:01:33,194 +同时摸肉棒 + +654 +01:01:33,694 --> 01:01:34,195 +-这样更兴奋呢 -是 + +655 +01:01:34,194 --> 01:01:34,695 +-这样更兴奋呢-是 + +656 +01:01:34,695 --> 01:01:35,196 +-这样更兴奋呢 -是 + +657 +01:01:35,195 --> 01:01:38,199 +-这样更兴奋呢-是 + +658 +01:01:38,198 --> 01:01:38,699 +-这样更兴奋呢 -是 + +659 +01:01:38,699 --> 01:01:39,200 +-这样更兴奋呢-是 + +660 +01:01:41,702 --> 01:01:44,705 +比刚才还要硬呢 + +661 +01:01:45,205 --> 01:01:46,207 +好爽 + +662 +01:02:07,227 --> 01:02:10,231 +你果然很敏感呢 + +663 +01:02:11,231 --> 01:02:12,733 +很爽吗 + +664 +01:02:13,233 --> 01:02:14,735 +我觉得很爽 + +665 +01:02:14,735 --> 01:02:16,237 +好可爱 + +666 +01:02:17,738 --> 01:02:19,240 +玩奶头 + +667 +01:02:21,241 --> 01:02:25,746 +摸你的奶头 + +668 +01:02:27,247 --> 01:02:29,750 +摸你的肉棒 + +669 +01:02:34,254 --> 01:02:36,257 +用脚夹住 + +670 +01:02:40,761 --> 01:02:42,263 +好厉害 + +671 +01:02:43,263 --> 01:02:45,266 +这样爽吗 + +672 +01:02:46,266 --> 01:02:47,768 +很爽 + +673 +01:02:51,271 --> 01:02:53,774 +肉棒又变大了呢 + +674 +01:02:53,774 --> 01:02:55,276 +用脚玩肉棒 + +675 +01:02:59,279 --> 01:03:01,782 +肉棒变很硬 + +676 +01:03:02,282 --> 01:03:04,785 +摩擦得这么爽 + +677 +01:03:05,285 --> 01:03:07,288 +你很变态呢 + +678 +01:03:10,791 --> 01:03:12,293 +好好享受吧 + +679 +01:03:14,795 --> 01:03:18,299 +同时玩奶头 + +680 +01:03:20,300 --> 01:03:21,802 +还不能射哦 + +681 +01:03:22,803 --> 01:03:25,306 +请趴在软垫上 + +682 +01:03:25,305 --> 01:03:26,807 +趴着吗 + +683 +01:03:30,310 --> 01:03:32,813 +你喜欢害羞姿势呢 + +684 +01:03:33,313 --> 01:03:34,315 +是 + +685 +01:03:34,815 --> 01:03:37,318 +这样很兴奋 + +686 +01:03:38,318 --> 01:03:39,820 +-果然很兴奋呢-是 + +687 +01:03:39,820 --> 01:03:40,321 +-果然很兴奋呢 -是 + +688 +01:03:40,320 --> 01:03:40,821 +-果然很兴奋呢-是 + +689 +01:03:40,821 --> 01:03:41,322 +-果然很兴奋呢 -是 + +690 +01:03:41,321 --> 01:03:41,822 +-果然很兴奋呢-是 + +691 +01:03:42,322 --> 01:03:43,824 +这样 + +692 +01:03:54,334 --> 01:04:01,842 +这样子摸奶头 感觉怎样 + +693 +01:04:02,843 --> 01:04:06,847 +-换个姿势-好爽 + +694 +01:04:07,347 --> 01:04:15,356 +肉棒变的很大哦 + +695 +01:04:17,858 --> 01:04:20,361 +-帮你打枪-好爽 + +696 +01:04:20,861 --> 01:04:23,364 +肉棒变得很热哦 + +697 +01:04:32,873 --> 01:04:39,380 +像这样抓着你的肉棒 + +698 +01:04:40,380 --> 01:04:43,384 +舔你的菊花 + +699 +01:04:46,386 --> 01:04:48,389 +感觉很兴奋呢 + +700 +01:04:49,389 --> 01:04:50,891 +受不了了 + +701 +01:04:57,898 --> 01:04:58,899 +-这样爽吗-非常爽 + +702 +01:04:58,899 --> 01:04:59,400 +-这样爽吗 -非常爽 + +703 +01:04:59,399 --> 01:05:01,902 +-这样爽吗-非常爽 + +704 +01:05:02,402 --> 01:05:06,407 +肉棒跟菊花被同时玩弄 + +705 +01:05:10,911 --> 01:05:13,414 +肉棒变的很硬哦 + +706 +01:05:23,423 --> 01:05:26,427 +你很喜欢这样玩呢 + +707 +01:05:29,930 --> 01:05:36,937 +用奶子来摩擦屁股 + +708 +01:05:37,938 --> 01:05:39,440 +兴奋吗 + +709 +01:05:46,947 --> 01:05:52,453 +用奶子摩擦屁股 + +710 +01:05:53,954 --> 01:05:55,456 +摩擦屁股 + +711 +01:06:06,466 --> 01:06:08,969 +屁股一直抖呢 + +712 +01:06:11,972 --> 01:06:15,976 +你没这样玩过吧 + +713 +01:06:17,978 --> 01:06:19,480 +像这样 + +714 +01:06:19,980 --> 01:06:21,982 +摩擦你的屁缝 + +715 +01:06:31,491 --> 01:06:35,996 +奶子按摩屁股 + +716 +01:06:35,996 --> 01:06:36,997 +奶子按摩屁股你应该没有体验过吧 + +717 +01:06:36,997 --> 01:06:38,999 +奶子按摩屁股 + +718 +01:06:48,509 --> 01:06:50,011 +好柔软 + +719 +01:06:53,514 --> 01:06:58,519 +请好好享受吧 + +720 +01:07:06,026 --> 01:07:07,028 +好爽 + +721 +01:07:23,043 --> 01:07:26,547 +有感受到我的身体吗 + +722 +01:07:28,048 --> 01:07:30,051 +很舒服 + +723 +01:07:31,051 --> 01:07:31,552 +-有感觉到奶子吗-有 + +724 +01:07:31,552 --> 01:07:32,553 +-有感觉到奶子吗 -有 + +725 +01:07:32,553 --> 01:07:34,555 +-有感觉到奶子吗-有 + +726 +01:07:55,075 --> 01:08:00,081 +请你转到正面吧 + +727 +01:08:01,582 --> 01:08:02,583 +是 + +728 +01:08:14,094 --> 01:08:15,096 +是 + +729 +01:08:19,600 --> 01:08:21,102 +像这样 + +730 +01:08:24,604 --> 01:08:25,606 +好爽 + +731 +01:08:30,109 --> 01:08:34,115 +碰到你的肉棒 + +732 +01:08:38,118 --> 01:08:39,120 +对不起 + +733 +01:08:41,121 --> 01:08:42,623 +因为太爽了 + +734 +01:08:44,124 --> 01:08:44,625 +-肉棒变得很大呢-是 + +735 +01:08:44,625 --> 01:08:47,128 +-肉棒变得很大呢 -是 + +736 +01:08:47,127 --> 01:08:47,628 +-肉棒变得很大呢-是 + +737 +01:08:47,627 --> 01:08:49,130 +-肉棒变得很大呢 -是 + +738 +01:08:50,631 --> 01:08:52,133 +好爽 + +739 +01:08:53,634 --> 01:08:54,135 +-奶子舒服吗-很爽 + +740 +01:08:54,134 --> 01:08:54,635 +-奶子舒服吗 -很爽 + +741 +01:08:54,635 --> 01:08:56,637 +-奶子舒服吗-很爽 + +742 +01:08:56,636 --> 01:08:57,638 +-奶子舒服吗 -很爽 + +743 +01:09:03,644 --> 01:09:08,649 +奶子可以感受肉棒很热哦 + +744 +01:09:10,149 --> 01:09:11,652 +很害羞 + +745 +01:09:12,152 --> 01:09:13,654 +舔身体 + +746 +01:09:36,676 --> 01:09:39,180 +肉棒变得好硬 + +747 +01:09:41,180 --> 01:09:46,687 +刚才打脚炮 好像很爽 + +748 +01:09:47,187 --> 01:09:49,690 +这次换这样 + +749 +01:09:55,195 --> 01:10:00,201 +这样夹肉棒 感觉怎样 + +750 +01:10:14,715 --> 01:10:16,717 +肉棒变很硬哦 + +751 +01:10:18,218 --> 01:10:20,721 +我快要射了 + +752 +01:10:21,722 --> 01:10:24,725 +请射出来 + +753 +01:10:26,226 --> 01:10:27,728 +好好享受 + +754 +01:10:30,230 --> 01:10:32,233 +我要射了 + +755 +01:10:34,735 --> 01:10:35,736 +射了 + +756 +01:10:39,740 --> 01:10:42,243 +射很多呢 + +757 +01:10:49,750 --> 01:10:51,752 +很舒服吗 + +758 +01:10:52,753 --> 01:10:53,754 +是 + +759 +01:10:56,757 --> 01:11:00,761 +我也想要舒服 + +760 +01:11:03,263 --> 01:11:08,269 +我们一起舒服吧 + +761 +01:11:08,769 --> 01:11:10,271 +是 + +762 +01:11:32,793 --> 01:11:34,795 +好舒服 + +763 +01:11:36,296 --> 01:11:37,798 +真爽 + +764 +01:11:47,307 --> 01:11:52,813 +才刚射过而已 又变硬了 + +765 +01:12:34,855 --> 01:12:36,357 +好舒服 + +766 +01:12:42,863 --> 01:12:44,365 +真爽 + +767 +01:12:50,370 --> 01:12:52,373 +小穴好舒服 + +768 +01:13:07,888 --> 01:13:15,896 +我可以用奶子夹肉棒吗 + +769 +01:13:20,400 --> 01:13:21,402 +好棒 + +770 +01:13:25,405 --> 01:13:33,914 +因为肉棒很硬 + +771 +01:13:33,914 --> 01:13:35,416 +好棒 + +772 +01:13:35,415 --> 01:13:38,919 +肉棒被夹的好爽 + +773 +01:13:39,920 --> 01:13:42,423 +-有感觉吧-是 + +774 +01:13:43,423 --> 01:13:46,927 +-请好好享受吧-是 + +775 +01:13:50,430 --> 01:13:54,935 +这样玩爽 + +776 +01:13:55,936 --> 01:13:57,438 +非常爽 + +777 +01:13:58,438 --> 01:14:00,941 +这样很爽呢 + +778 +01:14:03,944 --> 01:14:05,446 +快射了 + +779 +01:14:05,946 --> 01:14:07,948 +请射出来 + +780 +01:14:09,950 --> 01:14:14,455 +这样很兴奋呢 + +781 +01:14:19,960 --> 01:14:21,462 +快射了 + +782 +01:14:21,962 --> 01:14:23,464 +射了 + +783 +01:14:27,968 --> 01:14:29,970 +射很多呢 + +784 +01:14:30,971 --> 01:14:33,474 +这么爽吗 + +785 +01:14:36,977 --> 01:14:37,978 +没精液了 + +786 +01:14:45,986 --> 01:14:49,490 +打你炮这么爽吗 + +787 +01:14:51,491 --> 01:14:53,494 +对非常爽 + +788 +01:14:55,996 --> 01:15:00,000 +你兴奋 我也很兴奋 + +789 +01:15:08,008 --> 01:15:13,014 +要不要来打炮呢 + +790 +01:15:18,018 --> 01:15:21,522 +肉棒摩擦小穴 + +791 +01:15:27,027 --> 01:15:29,530 +肉棒变硬了呢 + +792 +01:15:34,034 --> 01:15:35,536 +很舒服 + +793 +01:15:37,538 --> 01:15:38,539 +舒服吗 + +794 +01:15:38,539 --> 01:15:40,041 +是 + +795 +01:15:40,040 --> 01:15:43,544 +-很爽-肉棒插进去的话 + +796 +01:15:43,544 --> 01:15:44,045 +-很爽 -肉棒插进去的话 + +797 +01:15:44,044 --> 01:15:45,546 +-很爽-肉棒插进去的话 + +798 +01:15:45,546 --> 01:15:46,047 +-很爽 -肉棒插进去的话 + +799 +01:15:46,046 --> 01:15:46,547 +-很爽-肉棒插进去的话 + +800 +01:15:47,047 --> 01:15:49,550 +一定会更爽呢 + +801 +01:15:55,055 --> 01:15:56,557 +像这样 + +802 +01:15:58,559 --> 01:15:59,560 +插进来 + +803 +01:16:04,565 --> 01:16:06,567 +肉棒进来了呢 + +804 +01:16:12,573 --> 01:16:15,576 +肉棒插的很深 + +805 +01:16:16,577 --> 01:16:17,578 +是的 + +806 +01:16:29,089 --> 01:16:31,592 +涂一点润滑油 + +807 +01:16:42,603 --> 01:16:46,607 +请好好享受 + +808 +01:17:03,123 --> 01:17:04,625 +好爽 + +809 +01:17:05,626 --> 01:17:07,128 +好棒 + +810 +01:17:13,634 --> 01:17:17,138 +插入的样子看的很清楚呢 + +811 +01:17:22,643 --> 01:17:25,146 +肉棒插的很深 + +812 +01:17:27,648 --> 01:17:28,649 +好爽 + +813 +01:17:34,655 --> 01:17:36,157 +好舒服 + +814 +01:17:36,156 --> 01:17:38,159 +插的很深 + +815 +01:17:39,660 --> 01:17:41,662 +快射了 + +816 +01:17:42,162 --> 01:17:43,664 +请射出来 + +817 +01:17:44,665 --> 01:17:47,168 +请射在里面吧 + +818 +01:17:51,672 --> 01:17:53,174 +好爽 + +819 +01:17:57,678 --> 01:17:59,680 +不行 要射了 + +820 +01:18:01,181 --> 01:18:02,183 +射了 + +821 +01:18:18,198 --> 01:18:20,201 +请看清楚 + +822 +01:18:32,713 --> 01:18:34,715 +射出来了 + +823 +01:18:36,216 --> 01:18:37,718 +舒服吗 + +824 +01:18:39,219 --> 01:18:40,721 +很舒服 + +825 +01:18:40,721 --> 01:18:44,225 +要不要换姿势呢 + +826 +01:18:51,732 --> 01:18:53,734 +像这样 + +827 +01:18:57,237 --> 01:19:00,741 +有玩过这种姿势吗 + +828 +01:19:02,242 --> 01:19:03,744 +这样吗 + +829 +01:19:15,255 --> 01:19:20,261 +跟刚才的感觉不同呢 + +830 +01:19:20,260 --> 01:19:21,762 +是 + +831 +01:19:22,262 --> 01:19:23,764 +好爽 + +832 +01:19:24,264 --> 01:19:27,268 +很少这样干吧 + +833 +01:19:27,267 --> 01:19:28,269 +是 + +834 +01:19:28,769 --> 01:19:33,274 +请好好享受 + +835 +01:19:57,297 --> 01:20:01,302 +被干的好舒服 + +836 +01:20:06,306 --> 01:20:08,309 +好舒服 + +837 +01:20:08,809 --> 01:20:09,810 +好爽 + +838 +01:20:17,818 --> 01:20:21,822 +干的我好舒服 + +839 +01:20:29,830 --> 01:20:32,333 +肉棒很硬呢 + +840 +01:20:32,332 --> 01:20:36,837 +肉棒插的很深哦 + +841 +01:20:41,842 --> 01:20:45,846 +再来换背后位吧 + +842 +01:20:45,846 --> 01:20:46,847 +是 + +843 +01:20:54,855 --> 01:20:56,857 +-插进去吧-是 + +844 +01:21:01,862 --> 01:21:05,866 +肉棒果然很硬呢 + +845 +01:21:06,867 --> 01:21:08,369 +好舒服 + +846 +01:21:12,372 --> 01:21:16,377 +插很深 你有感觉吗 + +847 +01:21:16,877 --> 01:21:17,878 +是 + +848 +01:21:31,892 --> 01:21:36,897 +肉棒插的很深 + +849 +01:21:37,898 --> 01:21:39,900 +插的很深 + +850 +01:21:41,401 --> 01:21:44,405 +喜欢背后位吗 + +851 +01:21:44,404 --> 01:21:44,905 +很喜欢 像这样 + +852 +01:21:44,905 --> 01:21:46,907 +-很喜欢-像这样 + +853 +01:21:47,407 --> 01:21:47,908 +一动来动去 好棒 + +854 +01:21:47,908 --> 01:21:48,409 +-动来动去 -好棒 + +855 +01:21:48,408 --> 01:21:50,911 +-动来动去-好棒 + +856 +01:21:50,911 --> 01:21:51,412 +一动来动去 好棒 + +857 +01:21:59,920 --> 01:22:02,423 +喜欢这样玩吗 + +858 +01:22:02,923 --> 01:22:04,925 +我很喜欢 + +859 +01:22:07,928 --> 01:22:09,430 +插的很深呢 + +860 +01:22:10,931 --> 01:22:12,433 +像这样 + +861 +01:22:14,434 --> 01:22:15,436 +好爽 + +862 +01:22:21,942 --> 01:22:23,444 +这样很爽 + +863 +01:22:34,454 --> 01:22:36,457 +好好享受 + +864 +01:22:41,962 --> 01:22:45,466 +小穴好舒服 + +865 +01:22:54,975 --> 01:22:56,477 +好棒 + +866 +01:22:57,978 --> 01:23:00,981 +肉棒插的很深 + +867 +01:23:22,503 --> 01:23:27,008 +-再来换正常位 -是 + +868 +01:23:27,007 --> 01:23:28,009 +-再来换正常位-是 + +869 +01:23:43,524 --> 01:23:45,026 +好棒 + +870 +01:23:52,533 --> 01:23:54,035 +好棒 + +871 +01:23:56,036 --> 01:24:01,042 +肉棒插的很深 + +872 +01:24:02,042 --> 01:24:06,047 +好好享受 射多一点 + +873 +01:24:06,547 --> 01:24:07,548 +是 + +874 +01:24:11,051 --> 01:24:12,053 +好爽 + +875 +01:24:15,556 --> 01:24:17,058 +快射了 + +876 +01:24:17,558 --> 01:24:20,061 +射吧 + +877 +01:24:22,062 --> 01:24:23,564 +不行 + +878 +01:24:32,072 --> 01:24:33,574 +抱歉 + +879 +01:24:34,074 --> 01:24:36,077 +射很多呢 + +880 +01:24:41,582 --> 01:24:46,587 +-舒服吗-非常爽 + +881 +01:24:49,089 --> 01:24:51,092 +不过 + +882 +01:24:53,594 --> 01:24:55,596 +还有精液吧 + +883 +01:25:03,103 --> 01:25:04,605 +好爽 + +884 +01:25:13,113 --> 01:25:15,116 +又变的很硬 + +885 +01:25:17,117 --> 01:25:17,618 +-才刚射 -像这样 + +886 +01:25:17,618 --> 01:25:18,119 +-才刚射-像这样 + +887 +01:25:18,118 --> 01:25:19,120 +-才刚射 -像这样 + +888 +01:25:19,119 --> 01:25:19,620 +-才刚射-像这样 + +889 +01:25:19,620 --> 01:25:20,621 +-才刚射 -像这样 + +890 +01:25:25,626 --> 01:25:26,627 +不行 + +891 +01:25:28,128 --> 01:25:29,130 +好爽 + +892 +01:25:42,142 --> 01:25:44,645 +肉棒变的好硬 + +893 +01:25:49,149 --> 01:25:50,151 +好爽 + +894 +01:25:57,157 --> 01:25:59,660 +还要做爱吗 + +895 +01:25:59,660 --> 01:26:02,663 +还可以射精吧 + +896 +01:26:02,663 --> 01:26:05,166 +我已经不行了 + +897 +01:26:07,167 --> 01:26:08,669 +-已经-射不出来了 + +898 +01:26:08,669 --> 01:26:10,171 +-已经 -射不出来了 + +899 +01:26:10,170 --> 01:26:11,172 +-已经-射不出来了 + +900 +01:26:16,677 --> 01:26:20,181 +肉棒很硬哦 + +901 +01:26:25,686 --> 01:26:30,691 +-不行了-肉棒很硬哦 + +902 +01:26:33,694 --> 01:26:36,697 +还想要射精吧 + +903 +01:26:37,197 --> 01:26:39,700 +已经没有了 + +904 +01:26:41,702 --> 01:26:43,204 +没有精液了 + +905 +01:26:47,708 --> 01:26:49,210 +不行 + +906 +01:26:52,713 --> 01:26:54,215 +像这样动 + +907 +01:27:00,220 --> 01:27:02,223 +这样做爱 怎样啊 + +908 +01:27:11,732 --> 01:27:13,234 +玩亲亲 + +909 +01:27:39,760 --> 01:27:41,762 +好爽 + +910 +01:27:46,767 --> 01:27:49,270 +好好享受吧 + +911 +01:27:55,275 --> 01:27:57,278 +像这样 + +912 +01:28:00,781 --> 01:28:03,284 +上下动 + +913 +01:28:03,784 --> 01:28:04,785 +-喜欢这样玩吗-是 + +914 +01:28:04,785 --> 01:28:05,286 +-喜欢这样玩吗 -是 + +915 +01:28:05,285 --> 01:28:06,787 +-喜欢这样玩吗-是 + +916 +01:28:14,795 --> 01:28:16,297 +危险 + +917 +01:28:19,299 --> 01:28:20,801 +太爽了 + +918 +01:28:21,301 --> 01:28:27,308 +看得见插入的样子呢 + +919 +01:28:36,817 --> 01:28:40,321 +又快射了 + +920 +01:28:40,320 --> 01:28:41,822 +请射出来 + +921 +01:28:42,322 --> 01:28:47,328 +请射到满足为止 + +922 +01:28:47,828 --> 01:28:49,330 +射到爽为止 + +923 +01:28:53,834 --> 01:28:55,336 +肉棒好硬 + +924 +01:28:59,840 --> 01:29:02,343 +要射了 + +925 +01:29:07,347 --> 01:29:09,350 +射很多呢 + +926 +01:29:10,851 --> 01:29:12,853 +最后用夹的 + +927 +01:29:14,855 --> 01:29:16,357 +用奶子夹 + +928 +01:29:18,358 --> 01:29:20,861 +不行 + +929 +01:29:20,861 --> 01:29:23,864 +射了 + +930 +01:29:31,371 --> 01:29:32,873 +好棒 + +931 +01:29:36,877 --> 01:29:40,381 +射了很多呢 + +932 +01:29:50,891 --> 01:29:54,395 +-舒服吗-非常爽 + +933 +01:29:54,394 --> 01:29:55,396 +-舒服吗 -非常爽 + +934 +01:29:55,395 --> 01:29:55,896 +-舒服吗-非常爽 + +935 +01:29:55,896 --> 01:29:57,898 +还能射吗 + +936 +01:29:58,398 --> 01:29:59,900 +没办法了 + +937 +01:30:06,406 --> 01:30:09,910 +没办法了 + +938 +01:30:10,410 --> 01:30:11,412 +爽吗 + +939 +01:30:12,412 --> 01:30:14,415 +休息一下吧 + +940 +01:30:16,917 --> 01:30:19,920 +再多射一点吧 + +941 +01:30:20,921 --> 01:30:21,922 +是 + +942 +01:31:29,990 --> 01:31:32,993 +时间快要结束了 + +943 +01:31:34,995 --> 01:31:35,496 +-请多射几次吧 -是 + +944 +01:31:35,495 --> 01:31:36,997 +-请多射几次吧-是 + +945 +01:31:36,997 --> 01:31:37,498 +-请多射几次吧 -是 + +946 +01:31:37,497 --> 01:31:38,499 +-请多射几次吧-是 + +947 +01:31:38,498 --> 01:31:38,999 +-请多射几次吧 -是 + +948 +01:31:38,999 --> 01:31:44,005 +-请多射几次吧-是 + +949 +01:32:03,023 --> 01:32:07,028 +请品尝我的奶子 + +950 +01:32:10,030 --> 01:32:11,532 +太棒了 + +951 +01:32:21,041 --> 01:32:23,544 +请多吸一点 + +952 +01:32:38,058 --> 01:32:40,061 +真好吃 + +953 +01:32:45,065 --> 01:32:50,071 +好棒 被舔的好爽 + +954 +01:33:11,592 --> 01:33:15,096 +奶头好舒服 + +955 +01:33:34,114 --> 01:33:42,623 +我也来让你爽吧 + +956 +01:33:44,124 --> 01:33:45,626 +摸奶头 + +957 +01:33:46,627 --> 01:33:49,130 +奶头爽吗 + +958 +01:33:49,630 --> 01:33:50,631 +很爽 + +959 +01:33:52,633 --> 01:33:54,635 +身体在发抖呢 + +960 +01:33:55,135 --> 01:33:56,637 +请让我舒服 + +961 +01:33:57,638 --> 01:34:04,645 +不只是奶头 肉棒也很硬 + +962 +01:34:11,151 --> 01:34:12,653 +好爽 + +963 +01:34:15,656 --> 01:34:17,658 +肉棒舒服吗 + +964 +01:34:18,659 --> 01:34:20,661 +请玩弄我 + +965 +01:34:25,666 --> 01:34:29,670 +直接摸肉棒 + +966 +01:34:38,679 --> 01:34:40,681 +肉棒好硬 + +967 +01:34:56,697 --> 01:34:59,200 +肉棒很大呢 + +968 +01:34:59,700 --> 01:35:01,202 +舔奶头 + +969 +01:35:06,707 --> 01:35:07,708 +好爽 + +970 +01:35:10,711 --> 01:35:15,716 +射这么多次 还这么硬 + +971 +01:35:22,723 --> 01:35:24,225 +不行 + +972 +01:35:29,730 --> 01:35:31,732 +还不能射哦 + +973 +01:35:34,735 --> 01:35:36,737 +请躺下来 + +974 +01:35:36,737 --> 01:35:37,738 +是 + +975 +01:35:43,744 --> 01:35:45,246 +帮你吹 + +976 +01:36:24,785 --> 01:36:26,287 +舒服吗 + +977 +01:36:27,788 --> 01:36:29,790 +这样子 + +978 +01:36:30,791 --> 01:36:36,797 +用我的奶子夹 + +979 +01:36:37,297 --> 01:36:40,301 +这样夹很爽吗 + +980 +01:36:48,809 --> 01:36:50,311 +这样动 + +981 +01:36:52,813 --> 01:36:55,316 +让你爽歪歪 + +982 +01:36:56,316 --> 01:36:57,818 +夹到你爽 + +983 +01:37:05,325 --> 01:37:08,329 +-喜欢你炮吗-喜欢 + +984 +01:37:09,830 --> 01:37:11,332 +这样子玩 + +985 +01:37:14,334 --> 01:37:16,837 +这样摩擦 舒服吗 + +986 +01:37:18,839 --> 01:37:20,341 +很舒服 + +987 +01:37:37,858 --> 01:37:38,359 +用史害羞的姿势 + +988 +01:37:38,358 --> 01:37:38,859 +用史害羞的姿势来帮你打你炮 + +989 +01:37:38,859 --> 01:37:39,360 +用史害羞的姿势 + +990 +01:37:39,359 --> 01:37:39,860 +用史害羞的姿势来帮你打你炮 + +991 +01:37:39,860 --> 01:37:40,861 +用史害益的姿努来帮你打你炮 + +992 +01:37:40,861 --> 01:37:41,362 +用史害益的娄努来帮你打你炮 + +993 +01:37:42,362 --> 01:37:42,863 +用史害羞的姿势 + +994 +01:37:42,863 --> 01:37:43,864 +用史害羞的姿务来帮你打你炮 + +995 +01:37:44,364 --> 01:37:44,865 +用史害益的姿务来帮你打你炮 + +996 +01:37:45,365 --> 01:37:45,866 +用史害羞的姿势来帮你打你炮 + +997 +01:37:45,866 --> 01:37:46,367 +用史害羞的姿务来帮你打你炮 + +998 +01:37:46,366 --> 01:37:46,867 +用史害羞的姿势来帮你打你炮 + +999 +01:37:46,867 --> 01:37:48,369 +害羞姿势 + +1000 +01:37:51,872 --> 01:37:53,374 +这样子 + +1001 +01:38:07,888 --> 01:38:12,393 +肉棒变的很硬呢 + +1002 +01:38:26,907 --> 01:38:28,409 +好舒服 + +1003 +01:38:29,910 --> 01:38:34,915 +这样夹肉棒 怎样啊 + +1004 +01:38:34,915 --> 01:38:38,919 +第一次这样玩 + +1005 +01:38:38,919 --> 01:38:42,923 +这种姿势 肉棒还是很硬 + +1006 +01:38:45,425 --> 01:38:47,428 +你真变态 + +1007 +01:38:52,933 --> 01:38:57,938 +肉棒变得非常硬 + +1008 +01:39:11,451 --> 01:39:13,454 +肉棒好硬 + +1009 +01:39:19,459 --> 01:39:21,462 +-请射出来 -可以吗 + +1010 +01:39:27,968 --> 01:39:29,470 +要射了 + +1011 +01:39:40,480 --> 01:39:42,483 +射出来了呢 + +1012 +01:39:43,483 --> 01:39:45,986 +还可以射吗 + +1013 +01:39:46,987 --> 01:39:47,488 +-还可以吧 -是 + +1014 +01:39:47,487 --> 01:39:48,489 +-还可以吧-是 + +1015 +01:39:48,488 --> 01:39:49,490 +-还可以吧 -是 + +1016 +01:39:51,992 --> 01:39:55,996 +请你射到满足为止 + +1017 +01:39:57,497 --> 01:40:00,501 +才刚射又变的很硬 + +1018 +01:40:08,008 --> 01:40:10,511 +再多射几次 + +1019 +01:40:11,512 --> 01:40:15,516 +你可以让我爽吗 + +1020 +01:40:16,517 --> 01:40:17,518 +是 + +1021 +01:40:22,022 --> 01:40:23,524 +像这样 + +1022 +01:40:29,530 --> 01:40:33,534 +请舔我的小穴 + +1023 +01:40:53,554 --> 01:40:57,058 +肉棒好硬 + +1024 +01:41:17,578 --> 01:41:19,080 +好棒 + +1025 +01:41:21,081 --> 01:41:23,584 +小穴好舒服 + +1026 +01:41:42,603 --> 01:41:44,605 +真舒服 + +1027 +01:41:56,116 --> 01:41:58,119 +好棒 可以吗 + +1028 +01:42:12,132 --> 01:42:13,634 +怎样呢 + +1029 +01:42:15,135 --> 01:42:21,142 +请插我的小穴 + +1030 +01:42:25,145 --> 01:42:26,647 +好棒 + +1031 +01:42:30,150 --> 01:42:31,152 +这样吗 + +1032 +01:42:33,153 --> 01:42:35,156 +用力插吧 + +1033 +01:42:46,667 --> 01:42:48,669 +你好棒 + +1034 +01:43:06,186 --> 01:43:07,688 +去了 + +1035 +01:43:14,695 --> 01:43:16,197 +好棒 + +1036 +01:43:22,703 --> 01:43:25,206 +请舔我的小穴 + +1037 +01:43:33,213 --> 01:43:34,715 +这样子 + +1038 +01:43:50,230 --> 01:43:52,733 +小穴好舒服 + +1039 +01:44:10,250 --> 01:44:14,255 +被你舔的好爽 + +1040 +01:44:19,259 --> 01:44:24,765 +差不多要做爱了吧 + +1041 +01:44:27,267 --> 01:44:28,769 +好 + +1042 +01:44:45,285 --> 01:44:46,787 +我躺下去 + +1043 +01:44:55,295 --> 01:44:56,797 +像这样 + +1044 +01:45:02,302 --> 01:45:03,804 +好好享受吧 + +1045 +01:45:25,325 --> 01:45:28,829 +肉棒变的更硬 + +1046 +01:45:40,340 --> 01:45:43,344 +用力干我吧 + +1047 +01:45:48,849 --> 01:45:50,351 +这样子 + +1048 +01:45:51,351 --> 01:45:54,355 +请看插入的样子 + +1049 +01:45:58,358 --> 01:46:00,361 +插的好深 + +1050 +01:46:00,360 --> 01:46:03,864 +-有看到吗-有 + +1051 +01:46:05,866 --> 01:46:08,869 +-有看清楚吗-有 + +1052 +01:46:09,369 --> 01:46:10,871 +插入的样子 + +1053 +01:46:20,881 --> 01:46:25,886 +肉棒插到小穴深处 + +1054 +01:46:46,907 --> 01:46:50,411 +这样动爽吗 + +1055 +01:46:51,912 --> 01:46:53,414 +很舒服 + +1056 +01:46:55,916 --> 01:46:57,918 +爽吗 + +1057 +01:47:15,435 --> 01:47:18,439 +插到小穴深处 + +1058 +01:47:48,969 --> 01:47:50,471 +好舒服 + +1059 +01:47:52,973 --> 01:47:55,976 +肉棒变的好硬 + +1060 +01:47:57,477 --> 01:48:00,981 +玩弄你的奶头 + +1061 +01:48:00,981 --> 01:48:02,483 +这样很爽 + +1062 +01:48:02,983 --> 01:48:05,986 +-感觉怎样-很爽 + +1063 +01:48:05,986 --> 01:48:06,487 +-感觉怎样 -很爽 + +1064 +01:48:06,486 --> 01:48:06,987 +-感觉怎样-很爽 + +1065 +01:48:09,489 --> 01:48:13,494 +肉棒插的我好爽 + +1066 +01:48:14,494 --> 01:48:16,997 +不行 + +1067 +01:48:26,006 --> 01:48:28,009 +射了很多 + +1068 +01:48:31,011 --> 01:48:32,513 +身体在发抖 + +1069 +01:48:40,521 --> 01:48:43,024 +要看清楚哦 + +1070 +01:49:06,046 --> 01:49:08,549 +从后面插我 + +1071 +01:49:12,052 --> 01:49:13,554 +像这样 + +1072 +01:49:26,066 --> 01:49:27,568 +要插了哦 + +1073 +01:49:44,084 --> 01:49:45,086 +好爽 + +1074 +01:49:55,596 --> 01:49:59,100 +好爽 用力干我 + +1075 +01:50:04,104 --> 01:50:09,610 +请用力干我吧 + +1076 +01:50:15,115 --> 01:50:16,617 +舒服吗 + +1077 +01:50:31,131 --> 01:50:36,137 +肉棒插到小穴深处 + +1078 +01:50:42,142 --> 01:50:44,145 +好激烈 + +1079 +01:50:45,145 --> 01:50:46,647 +好舒服 + +1080 +01:50:48,649 --> 01:50:49,650 +去了 + +1081 +01:50:57,157 --> 01:50:58,659 +真爽 + +1082 +01:51:05,165 --> 01:51:07,168 +插到深处 + +1083 +01:51:17,678 --> 01:51:19,680 +又要去了 + +1084 +01:51:33,694 --> 01:51:35,696 +小穴被看光了 + +1085 +01:51:45,205 --> 01:51:47,708 +肉棒插的好深 + +1086 +01:51:49,209 --> 01:51:51,712 +肉棒插的真深 + +1087 +01:52:19,740 --> 01:52:22,243 +请用力干我 + +1088 +01:52:22,743 --> 01:52:24,745 +用力干我 + +1089 +01:52:50,771 --> 01:52:52,273 +又要去了 + +1090 +01:53:06,787 --> 01:53:10,291 +换正常位吧 + +1091 +01:53:24,805 --> 01:53:27,308 +你喜欢正常位呢 + +1092 +01:53:35,315 --> 01:53:39,320 +请用力干我 + +1093 +01:53:43,824 --> 01:53:45,826 +我觉得好爽 + +1094 +01:54:33,373 --> 01:54:34,375 +好爽 + +1095 +01:54:40,881 --> 01:54:45,886 +肉棒插的好深 + +1096 +01:54:45,886 --> 01:54:48,389 +我觉得好爽 + +1097 +01:54:53,393 --> 01:54:56,397 +插的我好爽 + +1098 +01:54:58,899 --> 01:55:01,902 +请用力干我 + +1099 +01:55:19,419 --> 01:55:20,921 +好爽 + +1100 +01:55:21,421 --> 01:55:23,424 +小穴好紧 + +1101 +01:55:32,933 --> 01:55:34,935 +我快射了 + +1102 +01:55:37,437 --> 01:55:42,443 +请射在我的奶子上 + +1103 +01:55:44,945 --> 01:55:46,947 +我要射了 + +1104 +01:55:54,454 --> 01:55:55,956 +好舒服 + +1105 +01:56:53,514 --> 01:56:54,015 +觉得舒服吗 + +1106 +01:56:58,519 --> 01:56:59,520 +是 + +1107 +01:57:00,521 --> 01:57:01,522 +不过 + +1108 +01:57:06,026 --> 01:57:07,528 +还可以射吧 + +1109 +01:57:08,028 --> 01:57:10,031 +还要玩吗 + +1110 +01:57:17,538 --> 01:57:19,540 +我不行了 + +1111 +01:57:32,052 --> 01:57:34,055 +快不行了 + +1112 +01:57:34,555 --> 01:57:35,556 +我站不住了 + +1113 +01:57:36,056 --> 01:57:36,557 +-可以躺吗-请躺下来 + +1114 +01:57:36,557 --> 01:57:37,058 +-可以躺吗 -请躺下来 + +1115 +01:57:37,057 --> 01:57:37,558 +-可以躺吗-请躺下来 + +1116 +01:57:37,558 --> 01:57:38,059 +-可以躺吗 -请躺下来 + +1117 +01:57:38,058 --> 01:57:38,559 +-可以躺吗-请躺下来 + +1118 +01:57:46,066 --> 01:57:47,068 +像这样 + +1119 +01:57:54,575 --> 01:58:01,582 +刚才你让我爽 + +1120 +01:58:19,099 --> 01:58:23,104 +肉棒变得很硬哦 + +1121 +01:58:24,104 --> 01:58:26,607 +还能射吧 + +1122 +01:58:32,112 --> 01:58:36,117 +希望可以满足你 + +1123 +01:58:39,119 --> 01:58:44,125 +请榨乾所有的精液 + +1124 +01:58:54,635 --> 01:58:56,137 +快要射了 + +1125 +01:58:57,137 --> 01:58:58,139 +请射出来 + +1126 +01:58:59,640 --> 01:59:01,642 +全射出来 + +1127 +01:59:03,644 --> 01:59:06,647 +把精液射光 + +1128 +01:59:10,150 --> 01:59:12,653 +不行 + +1129 +01:59:12,653 --> 01:59:13,654 +射了 + +1130 +01:59:17,157 --> 01:59:18,659 +射了很多呢 + +1131 +01:59:20,661 --> 01:59:23,664 +不过还能射吧 + +1132 +01:59:24,164 --> 01:59:26,667 +-还可以 -不行 + +1133 +01:59:32,673 --> 01:59:35,676 +射多一点吧 + +1134 +01:59:40,180 --> 01:59:41,182 +要射了 + +1135 +01:59:44,685 --> 01:59:49,190 +射了很多呢 + +1136 +02:00:03,704 --> 02:00:05,706 +没有了吗 + +1137 +02:00:13,714 --> 02:00:18,219 +先生 今天满足了吗 + +1138 +02:00:21,221 --> 02:00:25,726 +射了很多精液呢 + +1139 +02:00:29,730 --> 02:00:30,231 +如果下次有需要我会好好服务 + +1140 +02:00:30,230 --> 02:00:30,731 +如来下次有需安我会好好服务 + +1141 +02:00:30,731 --> 02:00:31,232 +如来下次有需要我会好好服务 + +1142 +02:00:31,231 --> 02:00:31,732 +如果下次有需要我会好好服务 + +1143 +02:00:31,732 --> 02:00:32,233 +如来下次有需要我会好好服务 + +1144 +02:00:32,232 --> 02:00:32,733 +如果下次有需要我会好好服务 + +1145 +02:00:32,733 --> 02:00:33,234 +我会好好服务 + +1146 +02:00:33,233 --> 02:00:34,735 +如果下次有需要我会好好服务 + +1147 +02:00:35,235 --> 02:00:36,237 +如果下次有需要我会好好服务 + +1148 +02:00:36,236 --> 02:00:36,737 +我会好好服务 diff --git a/tests/shared/data/alignment/sample.reference.srt b/tests/shared/data/alignment/sample.reference.srt new file mode 100644 index 0000000..c869510 --- /dev/null +++ b/tests/shared/data/alignment/sample.reference.srt @@ -0,0 +1,19 @@ +1 +00:00:00,000 --> 00:00:01,440 +--- + +2 +00:00:01,440 --> 00:00:04,320 +SUB 001 + +3 +00:00:05,760 --> 00:00:06,240 +--- + +4 +00:00:06,240 --> 00:00:09,120 +SUB 002 + +5 +00:00:09,600 --> 00:00:10,080 +--- diff --git a/tests/shared/env_isolation.py b/tests/shared/env_isolation.py new file mode 100644 index 0000000..e565414 --- /dev/null +++ b/tests/shared/env_isolation.py @@ -0,0 +1,61 @@ +"""环境与临时目录隔离(供需要 `wov_app.config` 路径常量的模块测试使用)。 + +规则要求测试可独立运行、不依赖全局 conftest,数据与产物目录就是测试代码 +所在目录:本模块把"创建独立数据目录并把应用指向它"封装成显式调用的函数, +由各模块测试在自己的 fixture 里调用。 + +失败模式说明:`wov_app.config` 在**导入时**读取环境变量并锁定路径常量 +(DATA_DIR / DB_PATH / STORAGE_DIR)。因此隔离必须在**首次导入** config +之前完成: + +- 模块级导入的测试文件(如 `from wov_app.config import STORAGE_DIR`)应在 + 文件顶部用 `configure_environment()`(不带 tmp_path 的进程级隔离)设置; +- 测试函数内才导入 `wov_app` 的文件,可用 `isolated_data_dir(tmp_path)` + 在导入前覆盖到每个用例自己的临时目录。 +""" + +from __future__ import annotations + +import os +import shutil +import tempfile +from pathlib import Path + +# 进程级临时根目录(进程退出即清理),供 `configure_environment()` 使用。 +_PROCESS_ROOT: Path | None = None + + +def process_data_root() -> Path: + """返回(必要时创建)进程级测试数据根目录。 + + 与全局 conftest 的区别:这里不使用 pytest 的 autouse fixture,也不在 + 导入时自动改环境变量;模块测试显式调用后才生效,因此单个模块目录 + 可以脱离其他测试独立运行。 + """ + global _PROCESS_ROOT + if _PROCESS_ROOT is None: + _PROCESS_ROOT = Path(tempfile.mkdtemp(prefix="vrsub-test-")) + return _PROCESS_ROOT + + +def configure_environment(data_root: Path | None = None) -> Path: + """把应用的数据目录、数据库、存储与后台服务指向独立测试目录。 + + 必须在导入 `wov_app.config`(或任何会导入它的模块)之前调用, + 否则路径常量已按默认值锁定。返回实际使用的数据根目录。 + """ + root = data_root or process_data_root() + os.environ["WOV_DATA_DIR"] = str(root / "data") + os.environ["WOV_DB_PATH"] = str(root / "data" / "wov.db") + os.environ["WOV_STORAGE_DIR"] = str(root / "storage") + # 默认关闭自动种子与后台线程:测试显式控制执行时机,避免与断言竞态。 + os.environ["WOV_AUTO_SEED"] = "0" + os.environ["WOV_SCHEDULER_ENABLED"] = "0" + os.environ["WOV_CLEANUP_ENABLED"] = "0" + os.environ["WOV_BATCH_ENABLED"] = "0" + return root + + +def cleanup_data_root(root: Path) -> None: + """清理测试数据根目录(忽略错误,进程退出或 fixture 收尾时调用)。""" + shutil.rmtree(root, ignore_errors=True) diff --git a/tests/shared/gpu_memory.py b/tests/shared/gpu_memory.py new file mode 100644 index 0000000..e0e6640 --- /dev/null +++ b/tests/shared/gpu_memory.py @@ -0,0 +1,131 @@ +"""GPU 显存探测与不足时跳过(供真实模型集成测试使用)。 + +真实模型(faster-whisper + CUDA)需要足够的显存;显存被其他进程占用或本身 +偏小时,推理会在中途抛 "CUDA failed with error out of memory"。这属于**外部 +运行环境状态**,不是被测代码的行为,按测试规则应跳过而不是判失败。 + +两层防护: + +1. `require_gpu_memory()`:运行前按模型体量估算所需显存,不足则跳过; +2. `skip_on_cuda_oom()`:运行中若仍发生显存不足(被其他进程动态抢占), + 也会被识别并转为跳过,而不是让整个套件判红。 + +探测不到 NVIDIA GPU(无 `nvidia-smi`,例如纯 CPU 机器)时**不做显存检查**: +faster-whisper 的 `device=auto` 会回退 CPU,此时测试应当照常执行。 +""" + +from __future__ import annotations + +import shutil +import subprocess +from contextlib import contextmanager +from pathlib import Path + +import pytest + +# 推理时的额外开销系数:CTranslate2 除权重外还需推理工作区、激活与 CUDA 上下文。 +# 实测标定(V2 权重 2.87GB,6GB 卡):推理前占用约 1120 MiB,峰值约 5217 MiB +# → 峰值增量约 4097 MiB ≈ 权重 × 1.39。系数取 1.45 覆盖 5% 分配余量, +# 使 6GB 卡(可用约 4.6GB)刚好放行、而更小的卡会被拦下。 +# +# 注意:这只给出"是否可能跑完"的预估。实测在临界卡上(余量仅数百 MB) +# 即使预估通过仍可能因分配碎片化抛 CUDA OOM,所以还需要 +# `skip_on_cuda_oom` / `require_node_result` 作为运行中的兼底。 +_MEMORY_FACTOR = 1.45 + + +def available_gpu_memory_mb() -> int | None: + """返回当前可用的最大显存(MB);无 NVIDIA GPU 或探测失败时返回 None。 + + 多卡时取可用显存最大的那张卡(推理默认只用一张卡)。 + """ + if shutil.which("nvidia-smi") is None: + return None + try: + completed = subprocess.run( + [ + "nvidia-smi", + "--query-gpu=memory.free", + "--format=csv,noheader,nounits", + ], + capture_output=True, + text=True, + timeout=10, + ) + except (OSError, subprocess.SubprocessError): + return None + if completed.returncode != 0: + return None + values: list[int] = [] + for line in completed.stdout.splitlines(): + stripped = line.strip() + if stripped.isdigit(): + values.append(int(stripped)) + return max(values) if values else None + + +def required_memory_mb(model_dir: Path) -> int: + """按模型权重体量估算推理所需显存(MB)。""" + weights = model_dir / "model.bin" + size_mb = weights.stat().st_size / (1024 * 1024) if weights.is_file() else 0.0 + return int(size_mb * _MEMORY_FACTOR) + + +def require_gpu_memory(model_dir: Path) -> None: + """显存不足以跑完该模型时跳过测试;无 GPU 时不检查(回退 CPU 执行)。""" + free = available_gpu_memory_mb() + if free is None: + return # 无 NVIDIA GPU:device=auto 会走 CPU,无需显存检查 + needed = required_memory_mb(model_dir) + if free < needed: + pytest.skip( + f"可用显存不足(需约 {needed} MB,当前可用 {free} MB)," + f"跳过真实模型集成测试以避免 OOM" + ) + + +def is_cuda_oom(text: str) -> bool: + """判断错误文本是否为显存不足(CUDA OOM)。""" + lowered = text.lower() + return "out of memory" in lowered or "cuda failed" in lowered + + +@contextmanager +def skip_on_cuda_oom(): + """运行真实推理;中途发生 CUDA OOM 时转为跳过(显存被动态抢占的场景)。""" + try: + yield + except Exception as exc: # noqa: BLE001 - 需要按消息识别 OOM + if is_cuda_oom(str(exc)): + pytest.skip(f"GPU 显存不足({exc}),跳过真实模型集成测试") + raise + + +def require_node_result(response, model_dir: Path) -> None: + """校验节点响应:显存不足转为跳过,其它失败照常抛出由断言处理。 + + 节点(如 whisper)会把推理异常包装成 `status="failed"` 的响应,因此 + OOM 不会以异常形式冒泡;这里统一识别并跳过。 + """ + if getattr(response, "status", "") != "completed" and is_cuda_oom( + str(getattr(response, "error", "")) + ): + pytest.skip( + f"GPU 显存不足({getattr(response, 'error', '')}),跳过真实模型集成测试" + ) + + +def fits_with_margin(model_dir: Path, safety_mb: int = 512) -> bool: + """显存是否充裕到可承受分块/多次推理(预留 safety_mb 余量)。 + + 用于区分两种用例写法: + - 显存充裕:按生产默认走分块路径(更接近线上配置); + - 显存临界:退化为整段单次推理,或直接跳过——分块会产生更多分配峰值, + 在临界卡上易因碎片化触发 CUDA OOM(实测同一峰值下 chunk 失败而单次成功)。 + + 无 NVIDIA GPU 时返回 False(此时应改用 CPU 友好配置,而非依赖显存)。 + """ + free = available_gpu_memory_mb() + if free is None: + return False + return free >= required_memory_mb(model_dir) + safety_mb diff --git a/tests/shared/llm_service.py b/tests/shared/llm_service.py new file mode 100644 index 0000000..39e0e8d --- /dev/null +++ b/tests/shared/llm_service.py @@ -0,0 +1,83 @@ +"""真实 LLM 服务的可用性判定(供真实 LLM 集成测试共用)。 + +真实 LLM 集成测试有两个外部前提:配置了 `LLM_API_KEY`、且服务端可用。 +两者都属于**外部环境状态**(密钥、余额、配额、网络),不是被测代码的行为; +按测试规则,这类外部状态缺失时应跳过而不是把缺陷计到代码头上。 + +本模块把该判定收敛到一处,避免各测试各自实现、语义漂移: + +- `require_llm_credentials()`:无 Key 时直接跳过; +- `skip_on_service_unavailable()`:把鉴权/余额/限流类 HTTP 错误(401/402/403/429) + 转成跳过(附带原因),其余错误(如 5xx、网络异常、返回结构错误)照常失败, + 以保证真实回归仍能被发现。 +""" + +from __future__ import annotations + +import os +import urllib.error +from contextlib import contextmanager + +import pytest + +# 视为"服务端不可用/账号不可用"的 HTTP 状态码: +# 401 未授权、402 需要付费(余额/额度耗尽)、403 禁止访问、429 限流。 +_UNAVAILABLE_CODES = frozenset({401, 402, 403, 429}) + + +def require_llm_credentials() -> None: + """未配置 LLM_API_KEY 时跳过真实 LLM 集成测试。""" + if not os.getenv("LLM_API_KEY"): + pytest.skip("未配置 LLM_API_KEY,跳过真实 LLM 集成测试") + + +@contextmanager +def skip_on_service_unavailable(): + """执行真实 LLM 调用;鉴权/余额/限流类错误转为跳过,其余错误向上抛出。 + + 这样既不会因账户余额或临时限流把测试套件判红(外部状态问题), + 也不会掩盖真正的回归(返回结构错误、代码异常仍会失败)。 + """ + try: + yield + except urllib.error.HTTPError as exc: + if exc.code in _UNAVAILABLE_CODES: + pytest.skip(f"LLM 服务/账号当前不可用(HTTP {exc.code}),跳过真实集成测试") + raise + + +def probe_llm_or_skip(model: str | None = None) -> None: + """向真实 LLM 接口发一次最小请求,服务/账号不可用时跳过测试。 + + 用途:被测函数(如 `nodes/subtitle_correction.correct_entry`)出于生产 + 需要会把调用异常吞掉并返回空串,测试因此无法从返回值区分"服务不可用" + 与"模型没有泛化"。此探针在断言之前把外部状态问题显式暴露出来并跳过, + 使断言只针对真实的能力回归。 + + 探测失败判定与 `skip_on_service_unavailable` 一致(401/402/403/429 跳过); + 其余错误(5xx、网络、返回结构异常)向上抛出,仍视为需要修复的问题。 + """ + import json as _json + import urllib.request + + require_llm_credentials() + api_base = os.getenv("LLM_API_BASE", "https://api.siliconflow.cn/v1/chat/completions") + api_key = os.getenv("LLM_API_KEY", "") + body = { + "model": model or os.getenv("LLM_MODEL", "Qwen/Qwen3.5-35B-A3B"), + "messages": [{"role": "user", "content": "ping"}], + "max_tokens": 1, + "enable_thinking": False, + } + request = urllib.request.Request( + api_base, + data=_json.dumps(body).encode("utf-8"), + headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}, + method="POST", + ) + with skip_on_service_unavailable(): + try: + with urllib.request.urlopen(request, timeout=30) as response: + _json.loads(response.read().decode("utf-8")) + except urllib.error.HTTPError: + raise diff --git a/tests/realdata_contract.py b/tests/shared/realdata_contract.py similarity index 64% rename from tests/realdata_contract.py rename to tests/shared/realdata_contract.py index 325539a..581f884 100644 --- a/tests/realdata_contract.py +++ b/tests/shared/realdata_contract.py @@ -1,48 +1,57 @@ -"""真实数据契约与夹具工具(供集成测试共用,不 mock 模型)。 +"""真实数据契约与量化工具(时间对齐 / 提示词规则,供集成测试共用,不 mock 模型)。 -本模块是"用真实数据复现问题"测试框架的公共底座。两类集成测试 -(时间对齐 / 幻觉词与专名提示词)都只读取**用户提供的真实数据文件**, -绝不构造假音频/假模型/假翻译输出来凑覆盖率;数据缺失时测试整体跳过。 +本模块是"用真实数据复现问题"测试框架的公共底座。两类集成测试(时间对齐 / +幻觉词与专名提示词)都只读取**用户提供的真实数据文件**,绝不构造假音频、 +假模型、假翻译输出来凑覆盖率;数据缺失时测试整体跳过。 数据契约(用户按下述约定提供真实文件即可,无需改动测试代码): -1. 时间对齐数据(目录:testdata/alignment/) - - 音频/视频素材:``testdata/alignment/.wav|.mp4|...``(真实语音) - - 参考字幕:``testdata/alignment/.reference.srt``(人工校对的时间轴, - 即"说话真实发生的时间"),SRT 标准格式 - - 说明:测试对同一素材跑 whisper 节点(vad_filter 开/关两种配置), - 把产出的 transcript.srt 与 reference.srt 做时间对齐评估,量化"过早/ - 过晚"的程度。若已有 .env 的 LLM Key,也可顺带评估翻译链路。 +1. 时间对齐数据(跨模块共享,默认目录:`tests/shared/data/alignment/`) + - 音频/视频素材:`.wav|.mp4|...`(真实语音) + - 参考字幕:`.reference.srt`(人工校对的时间轴) + - 说明:对同一素材跑 whisper 节点(vad_filter 开/关两种配置),把产出 + 的 transcript.srt 与 reference.srt 做时间对齐评估,量化"过早/过晚"程度。 +2. 幻觉词与专有名词提示词规则数据(默认目录:`tests/shared/data/prompt_rules/`) + - 日文字幕样本:`.ja.srt`(真实视频的日文 ASR 输出) + - 期望处理:`.expected.txt`(每行一个语料关键词断言) + - 说明:用真实数据调用 llm-translate 节点(真实 LLM API,不 mock), + 断言动态规则让译文不再输出寒暄幻觉、专名不被直译。 -2. 幻觉词与专有名词提示词规则数据(目录:testdata/prompt_rules/) - - 日文字幕样本:``testdata/prompt_rules/.ja.srt``(真实视频的 - 日文 ASR 输出,含"谢谢观看/晚安"等收尾寒暄、以及"芒果"等专名) - - 期望处理:``testdata/prompt_rules/.expected.txt``(每行一个 - 语料关键词断言:剔除寒暄 / 保留专名原文) - - 说明:测试用真实数据调用 llm-translate 节点(真实 LLM API,不 mock), - 断言动态拼入提示词规则后译文不再输出寒暄幻觉、专名不被直译。 - -每个测试函数都以"数据文件存在才运行,缺失即 skip"为前置,因此: -- 本地缺少数据时 `uv run pytest` 全部跳过,不影响 100% 覆盖率门禁; -- 把真实数据放入 testdata/ 后立即变为可执行的回归测试(红→绿闭环)。 +数据目录可通过环境变量 `WOV_TESTDATA_DIR` 覆盖(例如把素材放在外部盘), +也可由测试显式传入目录参数;两种方式都不依赖全局 conftest。 """ from __future__ import annotations -import json -import re +import os from dataclasses import dataclass, field from pathlib import Path -# 单体根目录:tests/ 的上一级。 -WORKSPACE = Path(__file__).resolve().parent.parent +# 测试代码根目录(tests/):本文件位于 tests/shared/realdata_contract.py。 +TESTS_DIR = Path(__file__).resolve().parent.parent -# 真实数据根目录(gitignored,与 testdata/ 下已入库的测试资产分开)。 -REALDATA_DIR = WORKSPACE / "testdata" +# 仓库根目录(tests/ 的上一级),用于定位迁移期仍在旧位置的素材。 +WORKSPACE = TESTS_DIR.parent -# 时间对齐数据子目录、幻觉词/专名数据子目录。 -ALIGNMENT_DIR = REALDATA_DIR / "alignment" -PROMPT_RULES_DIR = REALDATA_DIR / "prompt_rules" +# 真实数据根目录(新的模块化位置):跨模块共享素材放 tests/shared/data/。 +# 可通过环境变量 WOV_TESTDATA_DIR 覆盖(例如把大体积素材放外部盘)。 +SHARED_DATA_DIR = Path( + os.getenv("WOV_TESTDATA_DIR", str(TESTS_DIR / "shared" / "data")) +) + +def data_roots() -> list[Path]: + """返回要扫描的数据根目录(跨模块共享素材位置)。""" + return [SHARED_DATA_DIR] if SHARED_DATA_DIR.is_dir() else [] + + +def _subdir(name: str) -> list[Path]: + """在全部数据根目录下找同名子目录(存在才返回)。""" + return [root / name for root in data_roots() if (root / name).is_dir()] + +# 时间对齐数据子目录、幻觉词/专名数据子目录(迁移期指向新位置的默认值, +# 实际探查由 alignment_candidates / prompt_rule_candidates 扫描全部根目录)。 +ALIGNMENT_DIR = SHARED_DATA_DIR / "alignment" +PROMPT_RULES_DIR = SHARED_DATA_DIR / "prompt_rules" # 时间对齐的量化指标:与参考时间轴的允许偏差(秒)。真实转写存在固有抖动, # 用较大容差区分"正常误差"与"系统性地过早/过晚"两类问题。 @@ -50,136 +59,53 @@ TIER1_TOLERANCE_SECONDS = 0.5 # 第一档:单条字幕与参考的偏差阈 TIER2_EARLY_SECONDS = 0.7 # 第二档:系统性偏早阈值(超过即判定"过早") TIER2_LATE_SECONDS = 0.7 # 第二档:系统性偏晚阈值(超过即判定"过晚") -_ASR_PARAMS_VAD_ON = { - "language": "ja", - "chunk_seconds": 60, - "vad_filter": True, - "condition_on_previous_text": False, -} -_ASR_PARAMS_VAD_OFF = { - "language": "ja", - "chunk_seconds": 60, - "vad_filter": False, - "condition_on_previous_text": False, -} - # --------------------------------------------------------------------------- # 数据探查:真实数据文件是否存在 # --------------------------------------------------------------------------- -def alignment_candidates() -> list[Path]: +def alignment_candidates(directory: Path | None = None) -> list[Path]: """返回时间对齐测试可用的真实素材文件列表(存在才列出)。 - 识别规则:``testdata/alignment/`` 下任意 ``.``(音频/视频), - 且必须存在同名 ``.reference.srt`` 参考字幕。两者齐备才是可用样本。 + 识别规则:目录下任意 `.`(音频/视频),且必须存在同名 + `.reference.srt` 参考字幕。两者齐备才是可用样本。 + 不传 directory 时扫描全部数据根目录(新位置优先,旧位置兼容)。 """ - if not ALIGNMENT_DIR.is_dir(): - return [] + bases = [directory] if directory else _subdir("alignment") candidates: list[Path] = [] - for path in sorted(ALIGNMENT_DIR.iterdir()): - if path.suffix.lower() in { - ".wav", ".mp3", ".flac", ".m4a", ".aac", ".ogg", - ".mp4", ".mkv", ".mov", ".webm", ".ts", - }: - ref = path.with_suffix(".reference.srt") - if ref.is_file(): + for base in bases: + if not base.is_dir(): + continue + for path in sorted(base.iterdir()): + if path.suffix.lower() in { + ".wav", ".mp3", ".flac", ".m4a", ".aac", ".ogg", + ".mp4", ".mkv", ".mov", ".webm", ".ts", + }: + ref = path.with_suffix(".reference.srt") + if ref.is_file() and path not in candidates: + candidates.append(path) + return candidates + + +def prompt_rule_candidates(directory: Path | None = None) -> list[Path]: + """返回提示词规则测试可用的真实样本列表(存在才列出)。 + + 识别规则:目录下任意 `.ja.srt`,且必须存在同名 + `.expected.txt` 期望清单。不传 directory 时扫描全部数据根目录。 + """ + bases = [directory] if directory else _subdir("prompt_rules") + candidates: list[Path] = [] + for base in bases: + if not base.is_dir(): + continue + for path in sorted(base.glob("*.ja.srt")): + expected = path.with_suffix("").with_suffix(".expected.txt") + if expected.is_file() and path not in candidates: candidates.append(path) return candidates -def prompt_rule_candidates() -> list[Path]: - """返回提示词规则测试可用的真实样本列表(存在才列出)。 - - 识别规则:``testdata/prompt_rules/`` 下任意 ``.ja.srt``, - 且必须存在同名 ``.expected.txt`` 期望清单。 - """ - if not PROMPT_RULES_DIR.is_dir(): - return [] - candidates: list[Path] = [] - for path in sorted(PROMPT_RULES_DIR.glob("*.ja.srt")): - expected = path.with_suffix("").with_suffix(".expected.txt") - if expected.is_file(): - candidates.append(path) - return candidates - - -# --------------------------------------------------------------------------- -# SRT 解析(纯函数,供参考与产物共同使用) -# --------------------------------------------------------------------------- - -# 匹配 SRT 时间轴行(时间戳 --> 时间戳),作为条目边界。 -_SRT_TIME_LINE_RE = re.compile( - r"(\d{2}:\d{2}:\d{2},\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2},\d{3})", -) - - -def parse_srt_entries(text: str) -> list[dict]: - """解析 SRT 为 [{start, end, text}](秒为单位)。 - - 按行分块:一个条目 = 序号行 + 时间轴行 + 若干文本行(可空)。即使文本为 - 空串(如翻译补空占位、空字幕)也计入一条,时间轴不丢失。""" - entries: list[dict] = [] - lines = text.splitlines() - index = 0 - while index < len(lines): - line = lines[index].strip() - # 跳过序号行与空行,找时间轴行。 - if not line or not _SRT_TIME_LINE_RE.search(line): - index += 1 - continue - match = _SRT_TIME_LINE_RE.search(line) - start = _ts_to_seconds(match.group(1)) - end = _ts_to_seconds(match.group(2)) - index += 1 - # 收集后续非序号、非时间轴的文本行(可空/多行),直到空行或序号行。 - text_parts: list[str] = [] - while index < len(lines): - nxt = lines[index].strip() - if not nxt: - break # 空行:条目结束 - if _SRT_TIME_LINE_RE.search(nxt): - break # 下一个时间轴:条目结束 - if nxt.isdigit(): - break # 下一个序号:条目结束 - text_parts.append(nxt) - index += 1 - entries.append( - { - "start": start, - "end": end, - "text": " ".join(text_parts), - } - ) - index += 1 - return entries - - -def _ts_to_seconds(ts: str) -> float: - """把 SRT 时间戳(HH:MM:SS,mmm)换算为秒。""" - hours, minutes, rest = ts.split(":") - seconds, millis = rest.split(",") - return int(hours) * 3600 + int(minutes) * 60 + int(seconds) + int(millis) / 1000 - - - -# 参考字幕净化正则:纯装饰/符号/垃圾行(如 OCR 栅栏 '---'、'==='、下划线等) -# 不参与时间对齐——它们不是真实的说话内容,混入会让指标失真。 -_JUNK_RE = re.compile(r"^[\s\-—_=~•・。..、*+]+$") - - -def clean_reference(entries: list[dict]) -> list[dict]: - """从参考条目中剔除纯符号/装饰性垃圾行(无真实内容),返回保留条目。 - - 参考 SRT 由烧录字幕提取得到(见 scripts/extract_reference_srt.py),OCR - 可能把画面上的装饰/栅栏误收为字幕(如 '---'、'===')。这类条目没有 - 时间语义,若参与最近邻对齐会拉偏偏差统计,必须先剔除。""" - return [ - e for e in entries - if e["text"].strip() and not _JUNK_RE.match(e["text"]) - ] - # --------------------------------------------------------------------------- # 时间对齐指标 # --------------------------------------------------------------------------- @@ -226,7 +152,12 @@ class AlignmentReport: ) -def align_report(name: str, vad_filter: bool, produced: list[dict], reference: list[dict]) -> AlignmentReport: +def align_report( + name: str, + vad_filter: bool, + produced: list[dict], + reference: list[dict], +) -> AlignmentReport: """构建对齐报告:逐条求最近参考时间差并汇总偏差倾向。 对齐是"最近邻"匹配:对产物每条字幕,在参考时间轴中找其起始时刻最近的 @@ -271,6 +202,28 @@ def align_report(name: str, vad_filter: bool, produced: list[dict], reference: l return report +# --------------------------------------------------------------------------- +# 参考字幕净化 +# --------------------------------------------------------------------------- + +# 参考字幕净化正则:纯装饰/符号/垃圾行(如 OCR 栅栏 '---'、'==='、下划线等) +# 不参与时间对齐——它们不是真实的说话内容,混入会让指标失真。 +_JUNK_RE = __import__("re").compile(r"^[\s\-—_=~•・。..、*+]+$") + + +def clean_reference(entries: list[dict]) -> list[dict]: + """从参考条目中剔除纯符号/装饰性垃圾行(无真实内容),返回保留条目。 + + 参考 SRT 由烧录字幕提取得到(见 scripts/extract_reference_srt.py),OCR + 可能把画面上的装饰/栅栏误收为字幕(如 '---'、'===')。这类条目没有 + 时间语义,若参与最近邻对齐会拉偏偏差统计,必须先剔除。 + """ + return [ + e for e in entries + if e["text"].strip() and not _JUNK_RE.match(e["text"]) + ] + + # --------------------------------------------------------------------------- # 幻觉词 / 专有名词判定 # --------------------------------------------------------------------------- @@ -359,4 +312,31 @@ def build_translation_system_prompt( prompt += ( f"规则:专有名词(人名/品牌/SNS账号/虚拟角色名)不按字面直译,{noun_lines}。" ) - return prompt \ No newline at end of file + return prompt + +# --------------------------------------------------------------------------- +# 兼容导出 +# --------------------------------------------------------------------------- + +# SRT 条目解析实现放在同包的 srt_entries.py;这里再导出一次,方便调用方 +# 只导入一个模块即可获得"数据契约 + 解析工具"。 +from tests.shared.srt_entries import parse_srt_entries # noqa: E402 + +__all__ = [ + "ALIGNMENT_DIR", + "PROMPT_RULES_DIR", + "SHARED_DATA_DIR", + "WORKSPACE", + "data_roots", + "alignment_candidates", + "prompt_rule_candidates", + "AlignmentReport", + "align_report", + "clean_reference", + "parse_srt_entries", + "HALLUCINATION_TOKENS", + "PROPER_NOUNS_NO_TRANSLATE", + "assert_no_halucination", + "assert_proper_noun_preserved", + "build_translation_system_prompt", +] diff --git a/tests/shared/srt_entries.py b/tests/shared/srt_entries.py new file mode 100644 index 0000000..06c3d5a --- /dev/null +++ b/tests/shared/srt_entries.py @@ -0,0 +1,65 @@ +"""SRT 条目解析(纯函数,按秒返回,供参考字幕与产物字幕共用)。 + +与 `nodes/srt.py` 的区别:`nodes/srt.py` 保留原始毫秒字符串供生产链路 +序列化;本模块把时间戳换算为**秒浮点数**,用于测试中的时间对齐计算与 +篇幅统计,不参与生产输出。 +""" + +from __future__ import annotations + +import re + +# 匹配 SRT 时间轴行(时间戳 --> 时间戳),作为条目边界。 +_SRT_TIME_LINE_RE = re.compile( + r"(\d{2}:\d{2}:\d{2},\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2},\d{3})", +) + + +def parse_srt_entries(text: str) -> list[dict]: + """解析 SRT 为 [{start, end, text}](秒为单位)。 + + 按行分块:一个条目 = 序号行 + 时间轴行 + 若干文本行(可空)。即使文本为 + 空串(如翻译补空占位、空字幕)也计入一条,时间轴不丢失。正文多行用空格 + 连接,便于按关键词检索。 + """ + entries: list[dict] = [] + lines = text.splitlines() + index = 0 + while index < len(lines): + line = lines[index].strip() + # 跳过序号行与空行,找时间轴行。 + if not line or not _SRT_TIME_LINE_RE.search(line): + index += 1 + continue + match = _SRT_TIME_LINE_RE.search(line) + start = _ts_to_seconds(match.group(1)) + end = _ts_to_seconds(match.group(2)) + index += 1 + # 收集后续非序号、非时间轴的文本行(可空/多行),直到空行或序号行。 + text_parts: list[str] = [] + while index < len(lines): + nxt = lines[index].strip() + if not nxt: + break # 空行:条目结束 + if _SRT_TIME_LINE_RE.search(nxt): + break # 下一个时间轴:条目结束 + if nxt.isdigit(): + break # 下一个序号:条目结束 + text_parts.append(nxt) + index += 1 + entries.append( + { + "start": start, + "end": end, + "text": " ".join(text_parts), + } + ) + index += 1 + return entries + + +def _ts_to_seconds(ts: str) -> float: + """把 SRT 时间戳(HH:MM:SS,mmm)换算为秒。""" + hours, minutes, rest = ts.split(":") + seconds, millis = rest.split(",") + return int(hours) * 3600 + int(minutes) * 60 + int(seconds) + int(millis) / 1000 diff --git a/tests/shared/test_alignment/__init__.py b/tests/shared/test_alignment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/shared/test_alignment/test_alignment.py b/tests/shared/test_alignment/test_alignment.py new file mode 100644 index 0000000..1f3b2b5 --- /dev/null +++ b/tests/shared/test_alignment/test_alignment.py @@ -0,0 +1,127 @@ +"""真实语音时间对齐集成测试(数据 → 测试过程 → 验证结果)。 + +覆盖模块:`nodes/whisper.py`(真实转写)+ `tests/shared/realdata_contract.py` +(对齐量化工具)。这是模块级直测的补充:用真实模型 + 真实音频 + 人工校对 +参考字幕,量化字幕时间轴与"说话真实发生时间"的偏差。 + +数据契约:`tests/shared/data/alignment/` 下 `.wav|.mp4` 与其同名 +`.reference.srt`(人工校对)。素材缺失或本地无模型时跳过。 +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from nodes.whisper import _local_model_candidates, invoke +from tests.shared.realdata_contract import ( + TIER1_TOLERANCE_SECONDS, + align_report, + alignment_candidates, + clean_reference, +) +from tests.shared.srt_entries import parse_srt_entries +from wov_sdk.models import InvokeRequest + + +def _model_available() -> Path | None: + """返回可用的 V2 权重目录(缺失则返回 None 供跳过)。 + + 只接受 V2 权重:V3 已全面停用(均改用 V2),留在盘上的 V3 目录不得被 + 测试使用,否则测的不是线上实际运行的模型。 + """ + from tests.nodes.test_whisper.test_transcribe import _v2_model_candidates + + for candidate in _v2_model_candidates(): + if candidate.is_dir() and (candidate / "model.bin").is_file(): + return candidate + return None + + +def _sample_dir() -> Path | None: + """返回包含对齐素材的目录(共享数据目录;缺失时返回 None)。""" + directory = Path(__file__).resolve().parents[2] / "shared" / "data" / "alignment" + return directory if directory.is_dir() else None + + +@pytest.mark.integration +def test_real_alignment_candidates_discovered() -> None: + """对齐素材探查:能找到成对的媒体与参考字幕(数据契约可用)。""" + # 数据:共享数据目录下的真实素材。 + directory = _sample_dir() + if directory is None: + pytest.skip("缺少 tests/shared/data/alignment 目录") + + # 测试过程 + candidates = alignment_candidates(directory) + + # 验证结果:至少一对,且每对都有同名 reference.srt。 + assert candidates, "应至少有一对素材 + 参考字幕" + for media in candidates: + assert media.with_suffix(".reference.srt").is_file() + + +@pytest.mark.integration +def test_real_whisper_alignment_within_tolerance(tmp_path) -> None: + """真实转写产物 vs 人工参考字幕:时间偏差在容差内且无系统性偏移。 + + 这验证"字幕时间与说话时刻对齐"这一核心功能,使用真实模型与真实音频。 + """ + # 数据:最小的真实对齐素材(避免长视频拖慢测试)。 + directory = _sample_dir() + model_dir = _model_available() + if directory is None or model_dir is None: + pytest.skip("缺少对齐素材或本地 whisper 权重,跳过") + # 显存不足时跳过(转写整段真实音频需要显存,属外部环境状态)。 + from tests.shared.gpu_memory import require_gpu_memory, require_node_result + + require_gpu_memory(model_dir) + candidates = alignment_candidates(directory) + if not candidates: + pytest.skip("没有成对的对齐素材") + media = min(candidates, key=lambda p: p.stat().st_size) + reference = clean_reference( + parse_srt_entries(media.with_suffix(".reference.srt").read_text(encoding="utf-8")) + ) + if not reference: + pytest.skip("参考字幕为空") + + # 测试过程:走真实生产链路——先提音(视频/任意容器 → 16kHz 单声道 WAV), + # 再转写。whisper 只接受 WAV,直接喂 mp4 会解析失败(对齐素材多为 mp4)。 + from nodes.ffmpeg import invoke as extract_audio + + audio = extract_audio(InvokeRequest( + run_id="alignment", node_instance_id="ffmpeg-1", params={}, + inputs={"video_uri": str(media)}, output_dir=str(tmp_path / "audio"), + )) + require_node_result(audio, model_dir) + assert audio.status == "completed", audio.error + response = invoke(InvokeRequest( + run_id="alignment", node_instance_id="whisper-1", + params={ + "language": "ja", + # 本用例目标是"时间轴对齐":整段单次解码(chunk_seconds=0)避免 + # 分块上下文带来的额外显存与上下文损失;分块路径本身已由 + # whisper 模块测试覆盖。实测 6GB 卡上该配置稳定完成。 + "chunk_seconds": 0, + "vad_filter": True, + "model_path": str(model_dir), + }, + inputs={"audio_uri": audio.outputs["audio_uri"]}, + output_dir=str(tmp_path / "out"), + )) + require_node_result(response, model_dir) + assert response.status == "completed", response.error + produced = parse_srt_entries(open(response.outputs["srt_uri"], encoding="utf-8").read()) + report = align_report(media.stem, True, produced, reference) + + # 验证结果(基线由 V2 模型实测标定,见 docs/testing.md): + # - 产出条数与参考规模同量级(不丢整段、不大量幻觉); + # - 平均绝对偏差在真实转写抖动范围内; + # - 中位偏差不构成系统性偏早/偏晚。 + assert produced, "真实转写应产出字幕" + assert len(produced) >= len(reference) * 0.5, report.format_summary() + assert report.mean_abs_error <= 2.0, report.format_summary() + assert not report.consistently_early, report.format_summary() + assert not report.consistently_late, report.format_summary() diff --git a/tests/shared/test_srt_entries/__init__.py b/tests/shared/test_srt_entries/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/shared/test_srt_entries/test_srt_entries.py b/tests/shared/test_srt_entries/test_srt_entries.py new file mode 100644 index 0000000..f4e350d --- /dev/null +++ b/tests/shared/test_srt_entries/test_srt_entries.py @@ -0,0 +1,88 @@ +"""tests/shared/srt_entries.py 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`tests/shared/srt_entries.py`(按秒解析 SRT 条目,供各模块测试 +计算时间轴与统计),是测试公共设施中的一个独立功能单元,拥有独立测试目录。 +""" + +from __future__ import annotations + +from pathlib import Path + +from tests.shared.srt_entries import parse_srt_entries + +DATA_DIR = Path(__file__).resolve().parent.parent / "data" / "alignment" + + +def test_parses_single_cue_with_seconds() -> None: + """单条字幕解析为秒级时间轴与正文。""" + # 数据:一条标准 SRT。 + text = "1\n00:00:01,500 --> 00:00:02,250\n你好\n" + + # 测试过程 + entries = parse_srt_entries(text) + + # 验证结果 + assert entries == [{"start": 1.5, "end": 2.25, "text": "你好"}] + + +def test_keeps_empty_text_entry_with_timeline() -> None: + """空正文条目仍计入并保留时间轴(翻译补空占位需要)。""" + # 数据:空正文 + 有正文两条。 + text = "1\n00:00:01,000 --> 00:00:02,000\n\n2\n00:00:03,000 --> 00:00:04,000\n有词\n" + + # 测试过程 + entries = parse_srt_entries(text) + + # 验证结果 + assert len(entries) == 2 + assert entries[0]["text"] == "" + assert entries[1]["text"] == "有词" + + +def test_joins_multiline_text_with_space() -> None: + """多行正文合并为空格连接(便于关键词检索与统计)。""" + # 数据:两行正文。 + text = "1\n00:00:01,000 --> 00:00:02,000\n第一行\n第二行\n" + + # 测试过程与验证结果 + assert parse_srt_entries(text)[0]["text"] == "第一行 第二行" + + +def test_stops_entry_at_next_timestamp_or_index() -> None: + """条目在下一个时间轴或序号行处结束(不跨条吞并)。""" + # 数据:三条紧凑排列(无多余空行)。 + text = ( + "1\n00:00:01,000 --> 00:00:02,000\n甲\n" + "2\n00:00:03,000 --> 00:00:04,000\n乙\n" + "3\n00:00:05,000 --> 00:00:06,000\n丙\n" + ) + + # 测试过程 + entries = parse_srt_entries(text) + + # 验证结果 + assert [e["text"] for e in entries] == ["甲", "乙", "丙"] + + +def test_parses_real_reference_subtitle_file() -> None: + """真实参考字幕文件:条数等于时间轴行数(无丢失)。""" + # 数据:共享数据目录下的真实参考字幕。 + path = DATA_DIR / "sample.reference.srt" + if not path.is_file(): + import pytest + + pytest.skip(f"缺少数据文件 {path}") + text = path.read_text(encoding="utf-8") + + # 测试过程 + entries = parse_srt_entries(text) + + # 验证结果 + assert len(entries) == sum(1 for line in text.splitlines() if "-->" in line) + + +def test_empty_input_returns_empty_list() -> None: + """空文本返回空列表。""" + # 数据:空字符串。 + # 测试过程与验证结果 + assert parse_srt_entries("") == [] diff --git a/tests/test_adaptive_pool.py b/tests/test_adaptive_pool.py deleted file mode 100644 index 757191e..0000000 --- a/tests/test_adaptive_pool.py +++ /dev/null @@ -1,375 +0,0 @@ -"""自适应线程池测试。 - -覆盖决策函数(增/减/保持/边界)、map 顺序返回、worker 异常隔离, -以及"10s 窗口内平均响应 < 0.3s 加线程 / > 1.0s 减线程"的弹性行为 -(通过注入假时钟做确定性验证)。 -""" - -import threading -import time - -from nodes.adaptive_pool import AdaptiveThreadPool, decide - - -class FakeClock: - """可手动拨动的假时钟,用于确定性验证弹性窗口逻辑。""" - - def __init__(self, now: float = 0.0) -> None: - self.now = now - - def __call__(self) -> float: - return self.now - - def advance(self, seconds: float) -> None: - self.now += seconds - - -def test_decide_increase_when_fast() -> None: - """平均响应低于 fast_threshold 且未达上限:线程数 +1。""" - assert decide(1, 0.1, 1, 16, 0.3, 1.0) == 2 - - -def test_decide_decrease_when_slow() -> None: - """平均响应高于 slow_threshold 且高于下限:线程数 -1。""" - assert decide(3, 2.0, 1, 16, 0.3, 1.0) == 2 - - -def test_decide_keep_when_mid() -> None: - """平均响应介于两阈值之间:保持不变。""" - assert decide(2, 0.5, 1, 16, 0.3, 1.0) == 2 - - -def test_decide_bounds() -> None: - """已达上限不再增、已达下限不再减。""" - assert decide(16, 0.1, 1, 16, 0.3, 1.0) == 16 - assert decide(1, 2.0, 1, 16, 0.3, 1.0) == 1 - - -def test_pool_map_ordered_results() -> None: - """map 按输入顺序返回结果,worker 简单映射。""" - pool = AdaptiveThreadPool(worker=lambda item: item * 2) - assert pool.map([1, 2, 3, 4]) == [2, 4, 6, 8] - - -def test_pool_on_progress_callback() -> None: - """进度回调:每次完成触发一次,携带已完成数/总数/速度/平均耗时/线程数。""" - progress: list[tuple[int, int, float, float, int]] = [] - pool = AdaptiveThreadPool( - worker=lambda item: item, - on_progress=lambda done, total, rate, avg_time, workers: progress.append( - (done, total, rate, avg_time, workers) - ), - ) - pool.map([10, 20, 30]) - assert [item[0] for item in progress] == [1, 2, 3] # 已完成数递增。 - assert all(item[1] == 3 for item in progress) # 总数固定。 - assert all(item[2] > 0 for item in progress) # 速度为正值。 - # 窗口未满时平均耗时回退为累计平均(>0);线程数 ∈ [1, 上限]。 - assert all(item[3] > 0 for item in progress) - assert all(1 <= item[4] <= pool.max_workers for item in progress) - - -def test_pool_progress_reports_window_avg_after_first_window() -> None: - """窗口评估后:回调携带最近窗口平均耗时(扩缩容依据)与扩容后的线程数。 - - 覆盖 `_current_avg_time` 两个分支:窗口评估前回退累计平均,评估后使用 - 最近窗口平均(0.0s,响应远快于 fast_threshold 0.3s → 线程 +1)。 - """ - clock = FakeClock() - seen: list[tuple[int, int, float, float, int]] = [] - def progress(done, total, rate, avg_time, workers): - # 推进窗口时钟但不增加 worker 耗时,确定性触发快响应扩容。 - seen.append((done, total, rate, avg_time, workers)) - if done == 1: - clock.advance(11) - - pool = AdaptiveThreadPool( - worker=lambda item: item, - min_workers=1, max_workers=16, - window_seconds=10.0, fast_threshold=0.3, - clock=clock, on_progress=progress, - ) - pool.map(list(range(5))) - # 第一个完成的任务回调在窗口评估前:真实 worker 耗时为 0,均值也为 0。 - assert seen[0][3] == 0.0 - # 窗口评估后:回调携带最近窗口平均(≈0.0),且线程已扩容到 2。 - assert any(item[3] == 0.0 for item in seen) - assert any(item[4] == 2 for item in seen) - assert pool.max_concurrency == 2 -def test_pool_cancel_suppresses_progress() -> None: - """worker 触发 cancel(如检测到暂停信号)后:剩余任务不再触发进度回调。 - - 暂停场景:队列中剩余的大量帧会逐帧快速失败退出,若每完成一项都打印 - 进度日志,会在数秒内打出上万行日志;cancel 后抑制后续进度回调。 - """ - progress: list[tuple[int, int, float, float, int]] = [] - - def worker(item): - if item == 1: - pool.cancel() # 模拟某帧检测到暂停信号。 - return item - - pool = AdaptiveThreadPool( - worker=worker, - on_progress=lambda done, total, rate, avg_time, workers: progress.append( - (done, total, rate, avg_time, workers) - ), - ) - pool.map([0, 1, 2, 3]) - # 只有 cancel 之前的任务(item=0)触发了进度回调。 - assert len(progress) == 1 - assert progress[0][1] == 4 # 总数仍是 4。 - - -def test_pool_cancel_resets_between_maps() -> None: - """取消状态按批(map)重置:下一批任务进度回调恢复正常。""" - progress: list[tuple[int, int, float, float, int]] = [] - - def worker(item): - if item == "stop": - pool.cancel() - return item - - pool = AdaptiveThreadPool( - worker=worker, - on_progress=lambda done, total, rate, avg_time, workers: progress.append( - (done, total, rate, avg_time, workers) - ), - ) - pool.map(["a", "stop", "b"]) - assert len(progress) == 1 # 只有 cancel 前的 a 触发回调。 - pool.map(["c", "d"]) - # 新一批恢复回调(done 从 1 重新计数):共 3 次回调(1 + 2)。 - assert [item[0] for item in progress] == [1, 1, 2] - -def test_pool_map_empty() -> None: - - """空输入:不启动任务,直接返回空列表。""" - pool = AdaptiveThreadPool(worker=lambda item: item) - assert pool.map([]) == [] - - -def test_pool_worker_exception_isolated() -> None: - """worker 抛异常时以异常对象作为结果,不拖垮整体。""" - def boom(item): - raise RuntimeError("boom") - - pool = AdaptiveThreadPool(worker=boom) - results = pool.map([1, 2]) - assert len(results) == 2 - assert all(isinstance(result, RuntimeError) for result in results) - - -def test_pool_grows_when_fast() -> None: - """10s 窗口内平均响应 < 0.3s:线程数从 1 增至 2(弹性扩容)。""" - clock = FakeClock() - pool = AdaptiveThreadPool( - worker=lambda item: item, - min_workers=1, max_workers=16, - window_seconds=10.0, fast_threshold=0.3, - clock=clock, - on_progress=lambda done, *_: clock.advance(11) if done == 1 else None, - ) - # 首个任务完成后越过窗口 → 平均响应≈0 < 0.3 → +1 个在途额度。 - pool.map(list(range(4))) - assert pool.max_concurrency == 2 - - -def test_pool_shrink_when_slow() -> None: - """窗口平均响应 > 1.0s:线程数从 2 减至 1(弹性退避)。""" - clock = FakeClock() - pool = AdaptiveThreadPool( - worker=lambda item: item, - min_workers=1, max_workers=16, - window_seconds=10.0, fast_threshold=0.3, slow_threshold=1.0, - clock=clock, - ) - pool._resize(2) # 先扩到 2 个线程。 - clock.advance(11) - pool._tick(2.0) # 窗口内平均 2.0 > 1.0 → 缩回 1。 - # 缩容只调整提交额度;实际积压任务行为由下方并发回归测试验证。 - assert pool._target_workers == 1 - - -def test_resize_shrink_idempotent() -> None: - """回归:重复缩容到同一目标不会重复放哨兵(曾因并发缩容毒死全部线程而死锁)。""" - pool = AdaptiveThreadPool(worker=lambda item: item, min_workers=1, max_workers=8) - pool._resize(3) - assert pool._target_workers == 3 - pool._resize(2) - pool._resize(2) # 目标已是 2:幂等,不再放哨兵。 - assert pool._target_workers == 2 - # 重复缩容不会遗留哨兵影响下一批,真实 map 必须返回全部输入。 - assert pool.map(list(range(20))) == list(range(20)) - - -def test_pool_survives_mixed_grow_shrink() -> None: - """回归:扩容+缩容混合场景 map 必须完成且保序(修复前会死锁挂起)。""" - clock = FakeClock() - state = {"count": 0} - - def worker(item): - state["count"] += 1 - clock.advance(0.06 if state["count"] <= 20 else 0.6) - return item - - pool = AdaptiveThreadPool( - worker=worker, min_workers=1, max_workers=4, - window_seconds=0.5, fast_threshold=0.2, slow_threshold=0.4, - clock=clock, - ) - out = pool.map(list(range(60))) - assert out == list(range(60)) - - -def test_pool_report_failure_lowers_effective_max() -> None: - """消费错误(如 API 限流)临时降低有效最大线程数,下限为 min_workers。 - - 自适应:并发打到配额线触发 429 时,report_failure 收紧有效上限, - 后续请求减少从而避开持续限流。 - """ - pool = AdaptiveThreadPool(worker=lambda item: item, min_workers=1, max_workers=16) - assert pool._effective_max_workers == 16 - pool.report_failure() - assert pool._effective_max_workers == 15 - for _ in range(30): - pool.report_failure() - assert pool._effective_max_workers == 1 # 下限 min_workers。 - - -def test_pool_effective_max_recovers_after_clean_window() -> None: - """连续无错误窗口后有效上限逐步回升到 max_workers。""" - clock = FakeClock() - pool = AdaptiveThreadPool( - worker=lambda item: item, min_workers=1, max_workers=16, - window_seconds=10.0, fast_threshold=0.3, clock=clock, - ) - pool.report_failure() # 有效上限 16 -> 15。 - clock.advance(11) - pool._tick(0.01) # 错误所在窗口:上限不恢复。 - assert pool._effective_max_workers == 15 - clock.advance(11) - pool._tick(0.01) # 下一个干净窗口:恢复 +1。 - assert pool._effective_max_workers == 16 - pool._tick(0.01) # 窗口未满早退,上限不变。 - assert pool._effective_max_workers == 16 - - -def test_failure_at_single_worker_never_expands() -> None: - """真实 map 内报告限流:1 个在途任务不能因上限 20 变 19 而突然扩容。""" - targets = [] - - def worker(item): - if item == 0: - pool.report_failure() - targets.append(pool._target_workers) - return item - - pool = AdaptiveThreadPool(worker=worker, max_workers=20, window_seconds=1000) - assert pool.map(list(range(30))) == list(range(30)) - assert targets == [1] - - -def test_failure_shrink_limits_backlogged_work() -> None: - """积压任务中从 4 降到 1:已开始任务可完成,后续实际并发必须为 1。""" - barrier = threading.Barrier(4, timeout=3) - reduced = threading.Event() - lock = threading.Lock() - active = 0 - subsequent_peaks = [] - - def worker(item): - nonlocal active - with lock: - active += 1 - if item >= 5: - subsequent_peaks.append(active) - try: - if 1 <= item <= 4: - barrier.wait() - if item == 1: - # 连续限流将有效上限压到下限,存量请求不强制中断。 - for _ in range(3): - pool.report_failure() - reduced.set() - assert reduced.wait(3) - elif item >= 5: - # 模拟 I/O 等待,给其他工作线程实际进入任务的机会。 - time.sleep(0.005) - return item - finally: - with lock: - active -= 1 - - def progress(done, *_): - if done == 1: - pool._resize(4) - - pool = AdaptiveThreadPool(worker=worker, max_workers=4, window_seconds=1000, - on_progress=progress) - items = list(range(40)) - assert pool.map(items) == items - assert subsequent_peaks and max(subsequent_peaks) == 1 - - -def test_error_window_does_not_regrow() -> None: - """报告限流的同一窗口即使响应很快,也不能重新加并发。""" - clock = FakeClock() - pool = AdaptiveThreadPool(worker=lambda item: item, max_workers=4, clock=clock) - pool.report_failure() - clock.advance(11) - pool._tick(0.01) - assert pool._target_workers <= 1 - - -def test_retry_map_preserves_reduced_limit() -> None: - """两轮真实 map:首轮连续限流后,重试期间实际并发及进度都遵守新上限。""" - lock = threading.Lock() - active = 0 - peak = 0 - progress_counts = [] - - def worker(item): - nonlocal active, peak - with lock: - active += 1 - peak = max(peak, active) - try: - if item == "limited": - for _ in range(3): - pool.report_failure() - raise RuntimeError("rate limited") - time.sleep(0.002) - return item - finally: - with lock: - active -= 1 - - pool = AdaptiveThreadPool( - worker=worker, max_workers=4, window_seconds=1000, - on_progress=lambda done, *_: progress_counts.append(done), - ) - assert isinstance(pool.map(["limited"])[0], RuntimeError) - assert pool._effective_max_workers == 1 - # 主动申请扩容也受已收紧额度约束,下一轮不能重置有效上限。 - pool._resize(4) - assert pool._target_workers == 1 - assert pool.map(list(range(12))) == list(range(12)) - assert peak == 1 - assert progress_counts == [1] + list(range(1, 13)) - - -def test_pool_decide_uses_effective_max() -> None: - """扩容上限按有效最大线程数:错误窗口内即使响应快也不超过收紧后的上限。""" - clock = FakeClock() - pool = AdaptiveThreadPool( - worker=lambda item: item, min_workers=1, max_workers=16, - window_seconds=10.0, fast_threshold=0.3, clock=clock, - ) - pool.report_failure() # 有效上限 16 -> 15。 - pool._resize(15) - pool._window_failures = 1 # 本窗口内仍有错误 → 不恢复上限。 - clock.advance(11) - pool._tick(0.01) # 响应快,但 15 已是有效上限 → 不扩。 - assert pool._target_workers == 15 - assert pool._effective_max_workers == 15 diff --git a/tests/test_api.py b/tests/test_api.py deleted file mode 100644 index e29c247..0000000 --- a/tests/test_api.py +++ /dev/null @@ -1,34 +0,0 @@ -"""应用级 API 冒烟测试。 - -使用 FastAPI TestClient 验证健康检查、静态页面与 OpenAPI 文档可访问。 -节点注册/实例管理 API 已随单体化移除,不再有对应路由。 -""" - -from fastapi.testclient import TestClient - -from wov_app.main import app - - -def test_health_and_static() -> None: - """验证静态首页、OpenAPI 文档与健康探针均可访问。""" - with TestClient(app) as client: - root = client.get("/", follow_redirects=False) - assert root.status_code == 200 - assert "VRSub 字幕生成" in root.text - - docs = client.get("/docs") - assert docs.status_code == 200 - - response = client.get("/health") - assert response.status_code == 200 - assert response.json()["service"] == "wov-api" - assert response.json()["mode"] == "monolith" - - -def test_node_admin_routes_removed() -> None: - """验证节点注册与实例管理路由在单体版中已移除(404/405)。""" - with TestClient(app) as client: - # GET 落到静态文件挂载后返回 404;POST 对静态挂载返回 405。 - assert client.post("/api/admin/nodes", json={}).status_code == 405 - assert client.get("/api/admin/nodes").status_code == 404 - assert client.get("/api/admin/node-instances").status_code == 404 diff --git a/tests/test_apps_api.py b/tests/test_apps_api.py deleted file mode 100644 index ab978b2..0000000 --- a/tests/test_apps_api.py +++ /dev/null @@ -1,320 +0,0 @@ -"""用户应用 API 测试。 - -覆盖已发布应用的上传建任务、进度查询、产物下载、失败重试以及 -未发布/无版本工作流的拒绝逻辑。节点为内置注册,无需再手动注册。 -""" - -from pathlib import Path - -from fastapi.testclient import TestClient - -from wov_app.main import app - - -def _create_published_echo_workflow(client) -> str: - """创建一个已发布的单节点 Echo 工作流(echo 为内置节点)。""" - definition = { - "name": "echo-flow", - "version": 1, - "nodes": [ - { - "id": "step", - "node_type": "echo", - "inputs": {"file_uri": "input.video_uri"}, - } - ], - "edges": [], - "entry_inputs": {"video_uri": "file"}, - "final_outputs": {"result": "step.file_uri"}, - } - client.post( - "/api/admin/workflows", - json={ - "id": "echo-app", - "name": "Echo App", - "description": "upload a file", - "definition": definition, - }, - ) - client.post("/api/admin/workflows/echo-app/publish") - return "echo-app" - - -def test_upload_run_progress_and_download() -> None: - """验证上传文件建任务、手动执行、查询产物与下载的完整流程。""" - with TestClient(app) as client: - workflow_id = _create_published_echo_workflow(client) - apps = client.get("/api/apps") - assert apps.status_code == 200 - assert any(item["id"] == workflow_id for item in apps.json()) - - uploaded = client.post( - f"/api/apps/{workflow_id}/runs", - files={"file": ("sample.txt", b"hello from upload", "text/plain")}, - ) - assert uploaded.status_code == 200 - run_id = uploaded.json()["id"] - assert uploaded.json()["status"] == "QUEUED" - - run = client.get(f"/api/runs/{run_id}") - assert run.status_code == 200 - assert run.json()["input_uri"].endswith("sample.txt") - assert run.json()["artifacts"] == [] - - scheduler = app.state.scheduler - scheduler.execute_run(run_id) - - completed = client.get(f"/api/runs/{run_id}") - assert completed.status_code == 200 - assert completed.json()["status"] == "COMPLETED" - artifact_names = [item["name"] for item in completed.json()["artifacts"]] - assert "result" in artifact_names - - artifacts = client.get(f"/api/runs/{run_id}/artifacts") - assert artifacts.status_code == 200 - assert len(artifacts.json()) >= 1 - - downloaded = client.get(f"/api/runs/{run_id}/artifacts/result") - assert downloaded.status_code == 200 - assert b"hello from upload" in downloaded.content - - assert client.get(f"/api/runs/{run_id}/artifacts/missing").status_code == 404 - assert client.get("/api/runs/missing").status_code == 404 - assert client.get("/api/runs/missing/artifacts").status_code == 404 - - db = app.state.db - db.create_artifact( - { - "run_id": run_id, - "node_id": "step", - "name": "missing-file", - "uri": str(Path(__file__).resolve().parent / "not-exists.bin"), - "mime_type": "text/plain", - "size": 0, - } - ) - assert client.get(f"/api/runs/{run_id}/artifacts/missing-file").status_code == 404 - - runs = client.get("/api/runs") - assert runs.status_code == 200 - assert any(item["id"] == run_id for item in runs.json()) - - -def test_upload_rejects_unpublished_workflow() -> None: - """验证草稿或不存在的工作流不能被用户发起任务。""" - with TestClient(app) as client: - client.post( - "/api/admin/workflows", - json={ - "id": "draft", - "name": "Draft", - "definition": { - "name": "Draft", - "version": 1, - "nodes": [], - "edges": [], - }, - }, - ) - response = client.post( - "/api/apps/draft/runs", - files={"file": ("x.txt", b"x", "text/plain")}, - ) - assert response.status_code == 404 - - response = client.post( - "/api/apps/missing/runs", - files={"file": ("x.txt", b"x", "text/plain")}, - ) - assert response.status_code == 404 - - -def test_upload_rejects_workflow_without_version() -> None: - """验证已发布但没有任何版本的工作流返回 422。""" - with TestClient(app) as client: - db = app.state.db - db.upsert_workflow( - {"id": "empty", "name": "Empty", "published": 1, "latest_version": 0} - ) - response = client.post( - "/api/apps/empty/runs", - files={"file": ("x.txt", b"x", "text/plain")}, - ) - assert response.status_code == 422 - - -def test_retry_failed_run_requeues_and_reruns() -> None: - """验证失败任务重试会清空旧产物并重新执行成功。""" - with TestClient(app) as client: - workflow_id = _create_published_echo_workflow(client) - uploaded = client.post( - f"/api/apps/{workflow_id}/runs", - files={"file": ("sample.txt", b"hello retry", "text/plain")}, - ) - run_id = uploaded.json()["id"] - db = app.state.db - db.update_run( - run_id, - status="FAILED", - error="boom", - updated_at="2026-01-01T00:00:00+00:00", - ) - db.create_artifact( - { - "run_id": run_id, - "node_id": "step", - "name": "stale", - "uri": "stale.txt", - "mime_type": "text/plain", - "size": 1, - } - ) - - response = client.post(f"/api/runs/{run_id}/retry") - - assert response.status_code == 200 - assert response.json() == {"id": run_id, "status": "QUEUED"} - run = client.get(f"/api/runs/{run_id}").json() - assert run["status"] == "QUEUED" - assert run["error"] is None - assert run["artifacts"] == [] - - app.state.scheduler.execute_run(run_id) - completed = client.get(f"/api/runs/{run_id}").json() - assert completed["status"] == "COMPLETED" - assert any(item["name"] == "result" for item in completed["artifacts"]) - - -def test_pause_resume_run_api() -> None: - """验证暂停/继续接口:QUEUED→PAUSED→QUEUED,状态非法时报 422。""" - with TestClient(app) as client: - workflow_id = _create_published_echo_workflow(client) - uploaded = client.post( - f"/api/apps/{workflow_id}/runs", - files={"file": ("sample.txt", b"hello pause", "text/plain")}, - ) - run_id = uploaded.json()["id"] - assert uploaded.json()["status"] == "QUEUED" - - from wov_app.config import STORAGE_DIR - flag = STORAGE_DIR / "runs" / run_id / "paused.flag" - - paused = client.post(f"/api/runs/{run_id}/pause") - assert paused.status_code == 200 - assert paused.json() == {"id": run_id, "status": "PAUSED"} - assert client.get(f"/api/runs/{run_id}").json()["status"] == "PAUSED" - # 暂停时写入暂停信号文件,供运行中的节点(如 OCR)逐帧检查并中止。 - assert flag.exists() - - resumed = client.post(f"/api/runs/{run_id}/resume") - assert resumed.status_code == 200 - assert resumed.json() == {"id": run_id, "status": "QUEUED"} - assert client.get(f"/api/runs/{run_id}").json()["status"] == "QUEUED" - # 继续时清除暂停信号,避免误触发节点内暂停。 - assert not flag.exists() - - # 非 PAUSED 任务不可继续。 - assert client.post(f"/api/runs/{run_id}/resume").status_code == 422 - # 不存在的任务 404。 - assert client.post("/api/runs/missing/pause").status_code == 404 - assert client.post("/api/runs/missing/resume").status_code == 404 - - -def test_pause_rejects_terminal_states() -> None: - """验证已完成任务不可暂停。""" - with TestClient(app) as client: - workflow_id = _create_published_echo_workflow(client) - uploaded = client.post( - f"/api/apps/{workflow_id}/runs", - files={"file": ("sample.txt", b"hello done", "text/plain")}, - ) - run_id = uploaded.json()["id"] - db = app.state.db - db.update_run(run_id, status="COMPLETED", progress=1.0, updated_at="2026-01-01T00:00:00+00:00") - assert client.post(f"/api/runs/{run_id}/pause").status_code == 422 - - -def test_retry_rejects_non_failed_and_missing_runs() -> None: - """验证只有 FAILED 状态且存在的任务才能重试。""" - with TestClient(app) as client: - workflow_id = _create_published_echo_workflow(client) - uploaded = client.post( - f"/api/apps/{workflow_id}/runs", - files={"file": ("sample.txt", b"x", "text/plain")}, - ) - run_id = uploaded.json()["id"] - - assert client.post(f"/api/runs/{run_id}/retry").status_code == 422 - assert client.post("/api/runs/missing/retry").status_code == 404 - - -def test_delete_run_removes_record_and_files() -> None: - """验证删除任务会清理数据库记录与磁盘上的上传/步骤文件。""" - import shutil - - from wov_app.config import STORAGE_DIR - - with TestClient(app) as client: - workflow_id = _create_published_echo_workflow(client) - uploaded = client.post( - f"/api/apps/{workflow_id}/runs", - files={"file": ("sample.txt", b"hello delete", "text/plain")}, - ) - run_id = uploaded.json()["id"] - - # 执行任务以生成步骤产物目录。 - app.state.scheduler.execute_run(run_id) - run = client.get(f"/api/runs/{run_id}").json() - steps_dir = STORAGE_DIR / "runs" / run_id - assert steps_dir.is_dir() - # 上传文件目录也应存在。 - upload_dir = Path(run["input_uri"]).parent - assert upload_dir.is_dir() - - deleted = client.delete(f"/api/runs/{run_id}") - assert deleted.status_code == 200 - assert deleted.json() == {"deleted": run_id} - - assert client.get(f"/api/runs/{run_id}").status_code == 404 - assert not steps_dir.exists() - assert not upload_dir.exists() - - # 删除不存在的任务返回 404。 - assert client.delete(f"/api/runs/missing").status_code == 404 - - # 清理测试遗留的 runs 目录,避免跨用例残留。 - shutil.rmtree(STORAGE_DIR / "runs", ignore_errors=True) - - -def test_create_run_with_param_overrides() -> None: - """验证创建任务时可携带 params 覆盖(如前端框选的 crop),并持久化。""" - with TestClient(app) as client: - workflow_id = _create_published_echo_workflow(client) - uploaded = client.post( - f"/api/apps/{workflow_id}/runs", - files={"file": ("sample.txt", b"x", "text/plain")}, - data={"params": '{"step": {"crop": [0, 0.75, 1, 0.25]}}'}, - ) - assert uploaded.status_code == 200 - run_id = uploaded.json()["id"] - run = client.get(f"/api/runs/{run_id}").json() - assert run["param_overrides"] == {"step": {"crop": [0, 0.75, 1, 0.25]}} - # 非法 JSON 返回 422。 - bad = client.post( - f"/api/apps/{workflow_id}/runs", - files={"file": ("sample.txt", b"x", "text/plain")}, - data={"params": "not-json"}, - ) - assert bad.status_code == 422 - - -def test_create_run_params_non_object_rejected() -> None: - """params 为 JSON 数组时返回 422。""" - with TestClient(app) as client: - workflow_id = _create_published_echo_workflow(client) - response = client.post( - f"/api/apps/{workflow_id}/runs", - files={"file": ("sample.txt", b"x", "text/plain")}, - data={"params": "[1,2,3]"}, - ) - assert response.status_code == 422 diff --git a/tests/test_batch.py b/tests/test_batch.py deleted file mode 100644 index 762a3a3..0000000 --- a/tests/test_batch.py +++ /dev/null @@ -1,1481 +0,0 @@ -"""文件夹批量处理引擎与 API 测试。 - -覆盖:视频扫描与旁挂字幕判定、创建任务时一次性定位(已有字幕 → SKIPPED)、 -运行时只消费已定位明细、产物复制到视频旁与过程文件清理、暂停/继续断点续跑、 -失败视频不中断、批量 API 全端点(创建/列表/详情/暂停/继续/删除/下载/目录树) -与 404/422 分支。 -""" - -import json -import time -from datetime import datetime, timezone -from pathlib import Path - -import pytest -from fastapi.testclient import TestClient - -from wov_app import batch as batch_engine -from wov_app.batch import ( - BATCH_WORK_ROOT, - MARKER_NAME, - PAUSE_FLAG, - BatchWorker, - create_job, - list_sidecar_subtitles, - load_marker, - remove_job_workspace, - scan_videos, -) -from wov_app.config import STORAGE_DIR -from wov_app.db import Database -from wov_app.main import app -from wov_sdk.models import WorkflowDefinition - - -def _now_iso() -> str: - """返回当前 UTC 时间的 ISO 格式字符串。""" - return datetime.now(timezone.utc).isoformat() - - -def _db(tmp_path) -> Database: - """在临时目录创建独立数据库。""" - return Database(tmp_path / "wov.db") - - -def _seed_echo_workflow(db: Database, workflow_id: str = "echo-app", published: bool = True) -> None: - """创建引用 echo 节点的单节点工作流(真实数据流:输入文件复制为产物)。 - - 幂等:先删除同 ID 的旧工作流(含版本与任务),再重新创建。 - """ - definition = { - "name": "echo-flow", - "version": 1, - "nodes": [ - {"id": "step", "node_type": "echo", "inputs": {"file_uri": "input.video_uri"}} - ], - "edges": [], - "entry_inputs": {"video_uri": "file"}, - "final_outputs": {"result": "step.file_uri"}, - } - # 批量测试会真实执行 echo 节点,注册表由 conftest 每测试隔离,需显式注册。 - _register_echo() - if db.get_workflow(workflow_id) is not None: - db.delete_workflow(workflow_id) - db.upsert_workflow( - { - "id": workflow_id, - "name": "Echo", - "description": "", - "published": 1 if published else 0, - "latest_version": 1, - } - ) - db.create_workflow_version(workflow_id, 1, definition) - - -def _register_echo() -> None: - """把内置 echo 节点注册到进程内注册表(conftest 每个测试隔离注册表)。""" - from wov_app import registry - from nodes.echo import invoke - from wov_sdk.models import NodeManifest - - root = Path(__file__).resolve().parent.parent - registry.register(NodeManifest.load(str(root / "manifests" / "echo.json")), invoke) - - -def _video_folder(tmp_path, names=("a.mp4", "b.mp4")) -> Path: - """创建含视频文件的文件夹:内容为真实文本(echo 节点按文本读入)。""" - folder = tmp_path / "videos" - folder.mkdir() - for index, name in enumerate(names, start=1): - (folder / name).write_text(f"视频 {name} 的测试内容 {index}\n", encoding="utf-8") - return folder - - -def _make_job(db: Database, folder: Path, workflow_id: str = "echo-app", recursive: bool = True) -> dict: - """通过 create_job 创建批量任务并返回任务记录。""" - job_id = create_job(db, str(folder), workflow_id, recursive) - return db.get_batch_job(job_id) - - -def _create_run( - db: Database, - run_id: str, - folder: Path, - workflow_id: str = "echo-app", - status: str = "QUEUED", - name: str = "a", -) -> None: - """创建一条 source=batch 的运行记录(input 指向 folder 下的视频文件)。""" - db.create_run( - { - "id": run_id, - "workflow_id": workflow_id, - "workflow_version": 1, - "status": status, - "progress": 0, - "input_uri": str(folder / f"{name}.mp4"), - "param_overrides": None, - "source": "batch", - "created_at": _now_iso(), - "updated_at": _now_iso(), - } - ) - - -def _find_product(video_dir: Path, stem: str, alias: str) -> Path: - """在同名输出位置查找最终产物文件(文件名以 主名.别名 开头)。""" - matches = list(video_dir.glob(f"{stem}.{alias}.*")) - assert matches, f"未找到产物 {stem}.{alias}.* in {video_dir}" - return matches[0] - - -# --------------------------------------------------------------------------- -# 扫描 / 旁挂字幕判定 / 完成标记读取 -# --------------------------------------------------------------------------- - - -def test_scan_videos_recursive_and_top_level(tmp_path) -> None: - """递归/非递归扫描只返回视频文件,隐藏目录与普通文件不参与。""" - folder = tmp_path / "media" - (folder / "sub").mkdir(parents=True) - (folder / "a.mp4").write_text("a", encoding="utf-8") - (folder / "b.MKV").write_text("b", encoding="utf-8") - (folder / "readme.txt").write_text("c", encoding="utf-8") - (folder / "sub" / "c.avi").write_text("d", encoding="utf-8") - (folder / "sub" / "notes.md").write_text("e", encoding="utf-8") - - recursive = scan_videos(folder, recursive=True) - assert [p.name for p in recursive] == ["a.mp4", "b.MKV", "c.avi"] - flat = scan_videos(folder, recursive=False) - assert [p.name for p in flat] == ["a.mp4", "b.MKV"] - - -def test_list_sidecar_subtitles_matches(tmp_path) -> None: - """视频旁"文件名含视频名"的字幕文件全部命中;不相关/非字幕/别的目录不算。""" - folder = tmp_path / "media" - (folder / "sub").mkdir(parents=True) - video = folder / "movie.mp4" - video.write_text("v", encoding="utf-8") - # 命中:同目录、字幕扩展名、文件名含视频主名(大小写不敏感)。 - (folder / "movie.srt").write_text("s", encoding="utf-8") - (folder / "movie.CN.srt").write_text("s", encoding="utf-8") - (folder / "MOVIE.CN_dual_eye.ass").write_text("s", encoding="utf-8") - (folder / "movie.zh-CN.20260819120000.srt").write_text("s", encoding="utf-8") - # 不命中:不含视频主名、非字幕扩展名、子目录里的字幕。 - (folder / "other.srt").write_text("s", encoding="utf-8") - (folder / "movie.jpg").write_text("s", encoding="utf-8") - (folder / "sub" / "movie.srt").write_text("s", encoding="utf-8") - - names = [p.name for p in list_sidecar_subtitles(video)] - assert names == [ - "MOVIE.CN_dual_eye.ass", - "movie.CN.srt", - "movie.srt", - "movie.zh-CN.20260819120000.srt", - ] - - -def test_list_sidecar_subtitles_short_stem_and_unrelated(tmp_path) -> None: - """单字符视频主名只接受"主名."前缀,避免 a.mp4 误配 apple.srt。""" - folder = tmp_path / "media" - folder.mkdir() - video = folder / "a.mp4" - video.write_text("v", encoding="utf-8") - (folder / "apple.srt").write_text("s", encoding="utf-8") - (folder / "b.srt").write_text("s", encoding="utf-8") - assert list_sidecar_subtitles(video) == [] - (folder / "a.srt").write_text("s", encoding="utf-8") - assert [p.name for p in list_sidecar_subtitles(video)] == ["a.srt"] - - -def test_list_sidecar_subtitles_handles_unreadable_entries(tmp_path, monkeypatch) -> None: - """目录不可读返回空列表;单个子项不可读时跳过该项不影响其余匹配。""" - from pathlib import Path as RealPath - - # 整目录不可读(iterdir 抛 OSError)→ 保守返回空。 - class _Denied(RealPath): - """iterdir 恒抛权限错误的子类,模拟不可读的视频目录。""" - - def iterdir(self): - raise OSError("denied") - - denied = _Denied(str(tmp_path / "denied")) - assert list_sidecar_subtitles(denied / "a.mp4") == [] - - # 单个子项不可读(is_file 抛 OSError)→ 跳过该项,其余正常返回。 - folder = tmp_path / "media" - folder.mkdir() - video = folder / "a.mp4" - video.write_text("v", encoding="utf-8") - (folder / "a.srt").write_text("s", encoding="utf-8") - - class _Poison(RealPath): - """is_file 恒抛权限错误的子类,模拟不可读的目录项。""" - - def is_file(self): - raise OSError("denied") - - real_iterdir = RealPath.iterdir - - def mixed_iterdir(path): - return list(real_iterdir(path)) + [_Poison(str(folder / "secret"))] - - monkeypatch.setattr(RealPath, "iterdir", mixed_iterdir) - assert [p.name for p in list_sidecar_subtitles(video)] == ["a.srt"] - - -def test_load_marker_variants(tmp_path, monkeypatch) -> None: - """完成标记缺失/损坏/非字典/读取异常时返回 None。""" - work = tmp_path / "movie" - work.mkdir() - assert load_marker(work) is None - (work / MARKER_NAME).write_text("{broken json", encoding="utf-8") - assert load_marker(work) is None - (work / MARKER_NAME).write_text("[1,2]", encoding="utf-8") - assert load_marker(work) is None - (work / MARKER_NAME).write_text('{"workflow_id": "w", "finals": {"r": "m.txt"}}', encoding="utf-8") - assert load_marker(work)["finals"]["r"] == "m.txt" - - # 读取抛 OSError(权限等)时同样保守视为无标记。 - from pathlib import Path as RealPath - - def broken_read_text(path, **kwargs): - raise OSError("denied") - - monkeypatch.setattr(RealPath, "read_text", broken_read_text) - assert load_marker(work) is None - - -def test_remove_job_workspace(tmp_path) -> None: - """删除任务级私有工作空间;目录不存在时静默无副作用。""" - job_dir = BATCH_WORK_ROOT / "batch_del" - (job_dir / "runs" / "run_x").mkdir(parents=True) - remove_job_workspace("batch_del") - assert not job_dir.exists() - remove_job_workspace("batch_missing") - - -# --------------------------------------------------------------------------- -# create_job:校验 + 一次性定位(已有字幕 → SKIPPED) -# --------------------------------------------------------------------------- - - -def test_create_job_validation_errors(tmp_path) -> None: - """文件夹不存在、未发布工作流、无版本、无视频都拒绝创建。""" - db = _db(tmp_path) - folder = _video_folder(tmp_path) - with pytest.raises(ValueError, match="folder not found"): - create_job(db, str(tmp_path / "missing"), "echo-app") - with pytest.raises(ValueError, match="published workflow not found"): - create_job(db, str(folder), "ghost-flow") - _seed_echo_workflow(db, published=False) - with pytest.raises(ValueError, match="published workflow not found"): - create_job(db, str(folder), "echo-app") - # 有版本但文件夹里没有视频。 - _seed_echo_workflow(db, published=True) - empty = tmp_path / "empty" - empty.mkdir() - with pytest.raises(ValueError, match="no videos found in folder"): - create_job(db, str(empty), "echo-app") - # 已发布但没有版本的工作流。 - db.delete_workflow("echo-app") - db.upsert_workflow({"id": "echo-app", "name": "E", "description": "", "published": 1, "latest_version": 0}) - with pytest.raises(ValueError, match="workflow has no version"): - create_job(db, str(folder), "echo-app") - - -def test_create_job_locates_all_videos_once(tmp_path) -> None: - """创建任务时一次性定位:全部视频登记明细、递归标志落库、可被引擎拾起。""" - db = _db(tmp_path) - _seed_echo_workflow(db) - (tmp_path / "videos" / "sub").mkdir(parents=True) - (tmp_path / "videos" / "a.mp4").write_text("a", encoding="utf-8") - (tmp_path / "videos" / "sub" / "b.mkv").write_text("b", encoding="utf-8") - job = _make_job(db, tmp_path / "videos", recursive=True) - assert job["status"] == "QUEUED" - videos = db.list_batch_videos(job["id"]) - assert {Path(v["video_path"]).name for v in videos} == {"a.mp4", "b.mkv"} - assert all(v["status"] == "PENDING" for v in videos) - # 工作空间位于应用私有存储目录(与媒体库隔离),且每个视频独立目录。 - assert all(Path(v["work_dir"]).is_relative_to(BATCH_WORK_ROOT / job["id"]) for v in videos) - assert db.next_queued_batch_job()["id"] == job["id"] - - -def test_create_job_skips_videos_with_existing_subtitles(tmp_path) -> None: - """视频旁已有对应字幕文件的直接记 SKIPPED,其余 PENDING(不触发流水线)。""" - db = _db(tmp_path) - _seed_echo_workflow(db) - folder = _video_folder(tmp_path, names=("a.mp4", "b.mp4")) - # a 已处理过(视频旁有中文双目字幕),b 未处理。 - (folder / "a.CN_dual_eye.ass").write_text("已处理", encoding="utf-8") - - job = _make_job(db, folder) - videos = {Path(v["video_path"]).name: v for v in db.list_batch_videos(job["id"])} - assert videos["a.mp4"]["status"] == "SKIPPED" - assert videos["a.mp4"]["run_id"] is None - assert videos["b.mp4"]["status"] == "PENDING" - - -def test_create_job_all_videos_skipped_still_created(tmp_path) -> None: - """文件夹里全部视频都已有字幕时任务直接视为完成(无字幕视频数为 0)。 - - 新口径(2026-09):total = 扫描出的无字幕视频数,全被 SKIPPED 时 - total=0 且没有可处理项,创建即置 COMPLETED,不排队空跑。 - """ - db = _db(tmp_path) - _seed_echo_workflow(db) - folder = _video_folder(tmp_path, names=("a.mp4", "b.mp4")) - (folder / "a.CN.srt").write_text("x", encoding="utf-8") - (folder / "b.CN_dual_eye.ass").write_text("x", encoding="utf-8") - job = _make_job(db, folder) - assert job["status"] == "COMPLETED" - assert job["total"] == 0 and job["done"] == 0 - assert all(v["status"] == "SKIPPED" for v in db.list_batch_videos(job["id"])) - - -# --------------------------------------------------------------------------- -# 引擎:完整处理 / 跳过 / 失败 / 暂停续跑 / 收尾清理 -# --------------------------------------------------------------------------- - - -def test_batch_worker_processes_all_videos_and_cleans_up(tmp_path) -> None: - """批量引擎逐个处理视频:产物复制到视频旁、run 记录与过程工作空间清理。""" - db = _db(tmp_path) - _seed_echo_workflow(db) - folder = _video_folder(tmp_path) - job = _make_job(db, folder) - worker = BatchWorker(db, interval_seconds=0.05) - worker._process_job(job) - - job = db.get_batch_job(job["id"]) - assert job["status"] == "COMPLETED" - assert job["done"] == 2 and job["failed"] == 0 - for name in ("a", "b"): - videos = {Path(v["video_path"]).name: v for v in db.list_batch_videos(job["id"])} - row = videos[f"{name}.mp4"] - # 最终产物复制到视频旁(与 .mp4 同目录),内容与输入一致(真实数据流)。 - product = _find_product(folder, name, "result") - assert product.parent == folder - assert product.read_text(encoding="utf-8") == (folder / f"{name}.mp4").read_text(encoding="utf-8") - # 视频状态 COMPLETED 且 run 引用清空;不再写完成标记。 - assert row["status"] == "COMPLETED" - assert row["run_id"] is None - assert not (Path(row["work_dir"]) / MARKER_NAME).exists() - # 过程工作空间(含 runs/steps/音频分块等)已被整体清理,不在媒体库残留。 - assert not Path(row["work_dir"]).exists() - # run 记录已删除(产物已放视频旁,不再依赖原文件)。 - assert db.list_runs() == [] - - -def test_batch_worker_skips_videos_with_existing_subtitles(tmp_path) -> None: - """已有字幕的视频在创建时记 SKIPPED,不进入 total/done,只统计真正处理的。 - - 新口径(2026-09):total=无字幕视频数(不含 SKIPPED),done=实际完成数。 - """ - db = _db(tmp_path) - _seed_echo_workflow(db) - folder = _video_folder(tmp_path, names=("a.mp4", "b.mp4")) - (folder / "a.CN_dual_eye.ass").write_text("已处理", encoding="utf-8") - job = _make_job(db, folder) - # 创建时 total 已固定为无字幕数(b 一个),SKIPPED 不计入。 - assert db.get_batch_job(job["id"])["total"] == 1 - BatchWorker(db, interval_seconds=0.05)._process_job(job) - videos = {Path(v["video_path"]).name: v for v in db.list_batch_videos(job["id"])} - assert videos["a.mp4"]["status"] == "SKIPPED" - assert videos["a.mp4"]["run_id"] is None - assert videos["b.mp4"]["status"] == "COMPLETED" - job = db.get_batch_job(job["id"]) - # done 只计实际完成的 b(1 个),不再把 SKIPPED 的 a 计入。 - assert job["done"] == 1 and job["total"] == 1 - - -def test_batch_worker_all_skipped_job_completes(tmp_path) -> None: - """全部视频都有字幕时创建即视为完成(total=0),不创建任何 run。""" - db = _db(tmp_path) - _seed_echo_workflow(db) - folder = _video_folder(tmp_path, names=("a.mp4", "b.mp4")) - (folder / "a.srt").write_text("x", encoding="utf-8") - (folder / "b.CN.srt").write_text("x", encoding="utf-8") - job = _make_job(db, folder) - # 新口径:无字幕视频数为 0 → 创建即 COMPLETED,引擎不需要再跑。 - assert db.get_batch_job(job["id"])["status"] == "COMPLETED" - assert db.get_batch_job(job["id"])["total"] == 0 - assert db.get_batch_job(job["id"])["done"] == 0 - assert db.list_runs() == [] - - -def test_batch_worker_failed_video_continues(tmp_path) -> None: - """缺失视频文件记为 FAILED,任务继续处理后续视频。""" - db = _db(tmp_path) - _seed_echo_workflow(db) - folder = _video_folder(tmp_path, names=("gone.mp4", "b.mp4")) - job = _make_job(db, folder) - # 任务创建后、处理前删除第一个视频(模拟外部移除),第二个正常处理。 - (folder / "gone.mp4").unlink() - BatchWorker(db, interval_seconds=0.05)._process_job(job) - - videos = {Path(v["video_path"]).name: v for v in db.list_batch_videos(job["id"])} - assert videos["gone.mp4"]["status"] == "FAILED" - assert videos["gone.mp4"]["error"] == "video file not found" - assert videos["b.mp4"]["status"] == "COMPLETED" - job = db.get_batch_job(job["id"]) - assert job["status"] == "COMPLETED" - assert job["done"] == 1 and job["failed"] == 1 - - -def test_batch_worker_process_exception_marks_video_failed(tmp_path, monkeypatch) -> None: - """单视频执行抛异常:该视频 FAILED 带错误信息,任务继续处理后续视频。""" - db = _db(tmp_path) - _seed_echo_workflow(db) - folder = _video_folder(tmp_path, names=("a.mp4", "b.mp4")) - job = _make_job(db, folder) - - class _BoomScheduler: - """execute_run 直接抛异常的假调度器,触发单视频兜底分支。""" - - def __init__(self, db: Database, work_dir: Path) -> None: - self.db = db - - def execute_run(self, run_id: str) -> None: - raise RuntimeError("boom") - - monkeypatch.setattr("wov_app.batch.WorkflowScheduler", _BoomScheduler) - BatchWorker(db, interval_seconds=0.05)._process_job(job) - videos = {Path(v["video_path"]).name: v for v in db.list_batch_videos(job["id"])} - assert videos["a.mp4"]["status"] == "FAILED" - assert "boom" in videos["a.mp4"]["error"] - assert videos["b.mp4"]["status"] == "FAILED" - assert db.get_batch_job(job["id"])["status"] == "COMPLETED" - - -class _PausingScheduler: - """把 execute_run 模拟为"被暂停"的假调度器:置 run 为 PAUSED。""" - - def __init__(self, db: Database, work_dir: Path) -> None: - self.db = db - self.work_dir = work_dir - - def execute_run(self, run_id: str) -> None: - self.db.update_run(run_id, status="PAUSED", updated_at=_now_iso()) - - -def test_batch_worker_pause_then_resume_continues(tmp_path, monkeypatch) -> None: - """暂停后重新开始:PAUSED 视频从断点续跑,未开始的视频接着处理。""" - db = _db(tmp_path) - _seed_echo_workflow(db) - folder = _video_folder(tmp_path, names=("a.mp4", "b.mp4")) - job = _make_job(db, folder) - worker = BatchWorker(db, interval_seconds=0.05) - - # 第一次执行:第一个视频处理中被暂停(假调度器把 run 置为 PAUSED)。 - monkeypatch.setattr("wov_app.batch.WorkflowScheduler", _PausingScheduler) - worker._process_job(job) - videos = {Path(v["video_path"]).name: v for v in db.list_batch_videos(job["id"])} - assert videos["a.mp4"]["status"] == "PAUSED" - assert videos["b.mp4"]["status"] == "PENDING" - assert db.get_batch_job(job["id"])["status"] == "PAUSED" - - # 恢复真实调度器并继续:a 从断点完成(收尾清理),b 接着处理,任务 COMPLETED。 - monkeypatch.undo() - worker.resume_job(job["id"]) - worker._process_job(db.get_batch_job(job["id"])) - videos = {Path(v["video_path"]).name: v for v in db.list_batch_videos(job["id"])} - assert videos["a.mp4"]["status"] == "COMPLETED" - assert videos["b.mp4"]["status"] == "COMPLETED" - assert db.get_batch_job(job["id"])["status"] == "COMPLETED" - - # 再次处理(已完成视频在循环里直接 continue):结果不变,幂等。 - worker._process_job(db.get_batch_job(job["id"])) - videos = {Path(v["video_path"]).name: v for v in db.list_batch_videos(job["id"])} - assert videos["a.mp4"]["status"] == "COMPLETED" - assert videos["b.mp4"]["status"] == "COMPLETED" - assert db.get_batch_job(job["id"])["done"] == 2 - - -def test_batch_worker_paused_job_does_not_start_new_video(tmp_path, monkeypatch) -> None: - """任务在视频之间被暂停:后续视频不开始,不创建 run。""" - db = _db(tmp_path) - _seed_echo_workflow(db) - folder = _video_folder(tmp_path, names=("a.mp4", "b.mp4")) - job = _make_job(db, folder) - job_id = job["id"] - from wov_app.scheduler import WorkflowScheduler - - class _PauseAfterFirstScheduler: - """真实执行第一个视频后把批量任务置为 PAUSED(模拟用户处理中暂停)。""" - - def __init__(self, db: Database, work_dir: Path) -> None: - self.db = db - self.work_dir = work_dir - - def execute_run(self, run_id: str) -> None: - WorkflowScheduler(self.db, self.work_dir).execute_run(run_id) - self.db.update_batch_job(job_id, status="PAUSED", updated_at=_now_iso()) - - monkeypatch.setattr("wov_app.batch.WorkflowScheduler", _PauseAfterFirstScheduler) - BatchWorker(db, interval_seconds=0.05)._process_job(db.get_batch_job(job_id)) - videos = {Path(v["video_path"]).name: v for v in db.list_batch_videos(job_id)} - # 第一个视频真实完成;第二个视频在开始前因任务已暂停而不处理。 - assert videos["a.mp4"]["status"] == "COMPLETED" - assert videos["b.mp4"]["status"] == "PENDING" - assert videos["b.mp4"]["run_id"] is None - assert db.get_batch_job(job_id)["status"] == "PAUSED" - - -def test_batch_worker_failed_and_running_runs_resume(tmp_path) -> None: - """已失败的 run 重跑、上次进程残留的 RUNNING run 恢复后继续(收尾清理)。""" - db = _db(tmp_path) - _seed_echo_workflow(db) - folder = _video_folder(tmp_path, names=("a.mp4", "b.mp4")) - job = _make_job(db, folder) - videos = {Path(v["video_path"]).name: v for v in db.list_batch_videos(job["id"])} - for name, status in (("a", "FAILED"), ("b", "RUNNING")): - run_id = f"run_{name}" - _create_run(db, run_id, folder, status=status, name=name) - db.update_batch_video(videos[f"{name}.mp4"]["id"], run_id=run_id, updated_at=_now_iso()) - - BatchWorker(db, interval_seconds=0.05)._process_job(db.get_batch_job(job["id"])) - videos = {Path(v["video_path"]).name: v for v in db.list_batch_videos(job["id"])} - assert videos["a.mp4"]["status"] == "COMPLETED" - assert videos["b.mp4"]["status"] == "COMPLETED" - - -def test_batch_worker_retry_failed_keeps_completed_node_artifacts(tmp_path) -> None: - """批量重试 FAILED 视频**保留已完成节点产物**:只重跑失败节点。 - - 回归:此前 FAILED 走 reset_run 清空全部产物记录,重跑时 extract/ocr 等 - 长耗时节点从头重做(run_e2b74e89e232 的 22222 帧 OCR 被白白丢弃)。 - 通过改写 step1 产物内容验证:若 step1 被重跑,最终产物会恢复为源视频 - 文本;保留产物则最终产物内容为改写后的内容。 - """ - db = _db(tmp_path) - _register_echo() - # 双节点串联工作流:step1 成功、step2 失败的场景下验证只重跑 step2。 - definition = { - "name": "two-step", - "version": 1, - "nodes": [ - {"id": "s1", "node_type": "echo", "inputs": {"file_uri": "input.video_uri"}}, - {"id": "s2", "node_type": "echo", "inputs": {"file_uri": "s1.file_uri"}}, - ], - "edges": [{"from": "s1", "to": "s2"}], - "entry_inputs": {"video_uri": "file"}, - "final_outputs": {"result": "s2.file_uri"}, - } - db.upsert_workflow({"id": "echo-app", "name": "Echo", "description": "", "published": 1, "latest_version": 1}) - db.create_workflow_version("echo-app", 1, definition) - folder = _video_folder(tmp_path, names=("a.mp4",)) - job = _make_job(db, folder) - video = db.list_batch_videos(job["id"])[0] - run_id = "run_retry" - _create_run(db, run_id, folder, status="FAILED", name="a") - db.update_batch_video(video["id"], run_id=run_id, updated_at=_now_iso()) - - # 模拟 step1 已完成并登记产物,把其内容改写为与源视频不同(step2 会引用)。 - s1_out = Path(video["work_dir"]) / "runs" / run_id / "steps" / "s1" / "echo.txt" - s1_out.parent.mkdir(parents=True) - s1_out.write_text("step1 已保留产物", encoding="utf-8") - db.create_artifact( - { - "run_id": run_id, - "node_id": "s1", - "name": "s1.file_uri", - "uri": str(s1_out), - "mime_type": "text/plain", - "size": s1_out.stat().st_size, - } - ) - - BatchWorker(db, interval_seconds=0.05)._process_job(db.get_batch_job(job["id"])) - video = db.list_batch_videos(job["id"])[0] - assert video["status"] == "COMPLETED" - # step1 未重跑:最终产物内容等于保留的 step1 产物,而不是源视频文本。 - product = _find_product(folder, "a", "result") - assert product.read_text(encoding="utf-8") == "step1 已保留产物" - - -def test_batch_worker_stale_run_id_recreated(tmp_path) -> None: - """明细里的 run_id 指向已不存在的 run 时按全新视频处理并正常完成。""" - db = _db(tmp_path) - _seed_echo_workflow(db) - folder = _video_folder(tmp_path, names=("a.mp4",)) - job = _make_job(db, folder) - video = db.list_batch_videos(job["id"])[0] - db.update_batch_video(video["id"], run_id="run_dead", updated_at=_now_iso()) - - BatchWorker(db, interval_seconds=0.05)._process_job(job) - video = db.list_batch_videos(job["id"])[0] - assert video["status"] == "COMPLETED" - assert db.get_run("run_dead") is None - assert _find_product(folder, "a", "result").is_file() - - -def test_batch_worker_run_deleted_by_scheduler_returns(tmp_path, monkeypatch) -> None: - """execute_run 期间 run 记录被删除(极端情况)时直接返回,不误标完成。""" - db = _db(tmp_path) - _seed_echo_workflow(db) - folder = _video_folder(tmp_path, names=("a.mp4",)) - job = _make_job(db, folder) - - class _DeletingScheduler: - """execute_run 直接删除 run 记录的假调度器。""" - - def __init__(self, db: Database, work_dir: Path) -> None: - self.db = db - - def execute_run(self, run_id: str) -> None: - self.db.delete_run(run_id) - - monkeypatch.setattr("wov_app.batch.WorkflowScheduler", _DeletingScheduler) - BatchWorker(db, interval_seconds=0.05)._process_job(job) - video = db.list_batch_videos(job["id"])[0] - # 不标 COMPLETED/FAILED,保持 PENDING,等待下一次处理重试。 - assert video["status"] == "PENDING" - - -def test_batch_worker_pending_leftover_never_marks_completed(tmp_path, monkeypatch) -> None: - """残留未处理 PENDING 视频时,任务**不得**置 COMPLETED(状态机缺陷回归)。 - - 真实事故(batch_969fabe74b83 等 3 个任务):引擎串行处理到 9.9GB 大视频时 - execute_run 异常中断,_process_video 返回但该视频未被标 FAILED/COMPLETED - (保持 PENDING),_run_job 循环照常走完剩余 SKIPPED 后**无条件**收尾置 - COMPLETED——留下"10 个待处理却已完成"的僵尸状态。 - - 本测试用假调度器复现:第一个视频 execute_run 抛异常且不标任何状态 - (run 被删除的极端情形同路径),第二个视频正常;断言任务必须保持 - RUNNING(而非 COMPLETED),等待引擎下一次拾起重跑剩余 PENDING。 - """ - db = _db(tmp_path) - _seed_echo_workflow(db) - # a.mp4 待处理(处理中崩溃);b.mp4 旁放好字幕 → 创建即 SKIPPED,不参与处理。 - folder = _video_folder(tmp_path, names=("a.mp4", "b.mp4")) - (folder / "b.CN.srt").write_text("1\n00:00:00,000 --> 00:00:01,000\nb\n", encoding="utf-8") - job = _make_job(db, folder) - videos0 = {Path(v["video_path"]).name: v for v in db.list_batch_videos(job["id"])} - assert videos0["b.mp4"]["status"] == "SKIPPED" # 前置:旁挂字幕命中跳过 - - class _CrashScheduler: - """模拟进程在第一个(唯一待处理)视频处理中被杀:run 消失、视频保持 PENDING。""" - - def __init__(self, db: Database, work_dir: Path) -> None: - self.db = db - - def execute_run(self, run_id: str) -> None: - # 等同进程崩溃后 run 记录残留被清/丢失,视频仍是 PENDING。 - self.db.delete_run(run_id) - - monkeypatch.setattr("wov_app.batch.WorkflowScheduler", _CrashScheduler) - worker = BatchWorker(db, interval_seconds=0.05) - # 第一轮:a 崩溃残留 PENDING → 任务必须仍 RUNNING(允许续跑),不得 COMPLETED。 - worker._process_job(job) - job = db.get_batch_job(job["id"]) - videos = {Path(v["video_path"]).name: v for v in db.list_batch_videos(job["id"])} - # 缺陷行为:job 被置 COMPLETED(无条件收尾);期望保持 RUNNING。 - assert videos["a.mp4"]["status"] == "PENDING" - assert videos["b.mp4"]["status"] == "SKIPPED" - assert job["status"] == "RUNNING", ( - f"残留 PENDING 时任务被误置为 {job['status']}(缺陷:无条件收尾置 COMPLETED)" - ) - - # 第二轮(引擎重启后再次拾起 RUNNING 任务):a 这次正常完成 → 全部完成才 COMPLETED。 - monkeypatch.setattr("wov_app.batch.WorkflowScheduler", _CrashScheduler) - # 放开假调度器的崩溃:第二次调用时不再删 run,让真实调度器跑通。 - class _RecoveringScheduler: - """第二轮:真实执行工作流并产生成品,由引擎收尾放置。""" - - def __init__(self, db: Database, work_dir: Path) -> None: - self.db = db - - def execute_run(self, run_id: str) -> None: - from wov_app.scheduler import WorkflowScheduler - - video = self.db.list_batch_videos(job["id"])[0] - WorkflowScheduler(self.db, Path(video["work_dir"])).execute_run(run_id) - - monkeypatch.setattr("wov_app.batch.WorkflowScheduler", _RecoveringScheduler) - worker._process_job(job) - videos = {Path(v["video_path"]).name: v for v in db.list_batch_videos(job["id"])} - assert videos["a.mp4"]["status"] == "COMPLETED" - assert db.get_batch_job(job["id"])["status"] == "COMPLETED" - - -def test_batch_worker_already_completed_runs_finalized(tmp_path) -> None: - """run 已完成但视频未标记(收尾前中断):直接放置产物、清理并标记完成。 - - 覆盖 _place_products 各分支:产物齐全(复制)、产物记录存在但文件丢失 - (报错)、无产物记录(报错)——只有成品齐全才清理工作空间与 run 记录。 - """ - db = _db(tmp_path) - _seed_echo_workflow(db) - folder = _video_folder(tmp_path, names=("a.mp4", "b.mp4", "c.mp4")) - job = _make_job(db, folder) - now = _now_iso() - videos = {Path(v["video_path"]).stem: v for v in db.list_batch_videos(job["id"])} - - def complete_run(run_id: str, name: str, with_file: bool, with_artifact: bool) -> None: - """创建 COMPLETED run;可选产物文件与产物记录。""" - _create_run(db, run_id, folder, status="COMPLETED", name=name) - work_dir = Path(videos[name]["work_dir"]) - work_dir.mkdir(parents=True) - if with_artifact: - path = work_dir / "runs" / run_id / "steps" / "step" / f"{name}.result.txt" - db.create_artifact( - { - "run_id": run_id, - "node_id": "step", - "name": "result", - "uri": str(path), - "mime_type": "text/plain", - "size": 6, - } - ) - if with_file: - path = work_dir / "runs" / run_id / "steps" / "step" / f"{name}.result.txt" - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(f"产物 {name}", encoding="utf-8") - db.update_batch_video(videos[name]["id"], run_id=run_id, updated_at=now) - - # a:产物齐全;b:产物记录存在但文件丢失;c:没有任何产物记录。 - complete_run("run_done_a", "a", with_file=True, with_artifact=True) - complete_run("run_done_b", "b", with_file=False, with_artifact=True) - complete_run("run_done_c", "c", with_file=False, with_artifact=False) - - BatchWorker(db, interval_seconds=0.05)._process_job(db.get_batch_job(job["id"])) - videos = {Path(v["video_path"]).stem: v for v in db.list_batch_videos(job["id"])} - assert videos["a"]["status"] == "COMPLETED" - assert all(videos[name]["status"] == "FAILED" for name in ("b", "c")) - # a 的产物复制到视频旁;b/c 无产物可复制。 - product = _find_product(folder, "a", "result") - assert product.read_text(encoding="utf-8") == "产物 a" - assert not list(folder.glob("b.result.*")) and not list(folder.glob("c.result.*")) - # a 清理完成;b/c 必须保留工作空间、关联 run 与错误供修复后重试。 - assert not Path(videos["a"]["work_dir"]).exists() - assert db.get_run("run_done_a") is None - for name in ("b", "c"): - assert Path(videos[name]["work_dir"]).exists() - assert db.get_run(f"run_done_{name}") is not None - assert videos[name]["run_id"] == f"run_done_{name}" - assert "result" in videos[name]["error"] - - -def test_batch_worker_place_products_branches(tmp_path) -> None: - """_place_products:无产物记录/文件丢失报错;.srt/.ass 按库内约定命名覆盖; - 其他扩展名保留原文件名复制。""" - db = _db(tmp_path) - _seed_echo_workflow(db) - folder = _video_folder(tmp_path, names=("a.mp4",)) - video = folder / "a.mp4" - run_id = "run_place" - _create_run(db, run_id, folder, name="a") - source_dir = tmp_path / "src" - source_dir.mkdir() - worker = BatchWorker(db, interval_seconds=0.05) - definition = { - "name": "multi-final", - "version": 1, - "nodes": [], - "edges": [], - "entry_inputs": {"video_uri": "file"}, - # 无产物记录 / 文件丢失 / srt / ass / txt 五种 final 场景。 - "final_outputs": { - "alias_none": "x.none", - "alias_lost": "x.lost", - "alias_srt": "step.srt", - "alias_ass": "step.ass", - "alias_txt": "step.txt", - }, - } - # alias_lost:产物记录存在但指向的文件已丢失 → 跳过不放置。 - db.create_artifact( - {"run_id": run_id, "node_id": "step", "name": "alias_lost", "uri": str(source_dir / "lost.srt"), "mime_type": "text/plain", "size": 0} - ) - # alias_srt:中文 srt → <视频名>.CN.srt;目标已存在(旧内容)→ 覆盖。 - src_srt = source_dir / "raw_any_name.srt" - src_srt.write_text("1\n00:00:00,000 --> 00:00:01,000\n新中文字幕\n", encoding="utf-8") - db.create_artifact( - {"run_id": run_id, "node_id": "step", "name": "alias_srt", "uri": str(src_srt), "mime_type": "text/plain", "size": src_srt.stat().st_size} - ) - target_srt = video.parent / "a.CN.srt" - target_srt.write_text("旧字幕内容", encoding="utf-8") - # alias_ass:双目 ass → <视频名>.CN_dual_eye.ass;覆盖同名旧文件。 - src_ass = source_dir / "whatever.ass" - src_ass.write_text("[Script Info]\n新双目字幕\n", encoding="utf-8") - db.create_artifact( - {"run_id": run_id, "node_id": "step", "name": "alias_ass", "uri": str(src_ass), "mime_type": "text/plain", "size": src_ass.stat().st_size} - ) - target_ass = video.parent / "a.CN_dual_eye.ass" - target_ass.write_text("旧 ass", encoding="utf-8") - # alias_txt:其他扩展名保留原文件名(含时间戳命名)复制。 - src_txt = source_dir / "a.other.20260902120000.txt" - src_txt.write_text("其余产物内容", encoding="utf-8") - db.create_artifact( - {"run_id": run_id, "node_id": "step", "name": "alias_txt", "uri": str(src_txt), "mime_type": "text/plain", "size": src_txt.stat().st_size} - ) - - # 所有必需成品预检通过前不开始覆盖,缺失原因中包含具体别名。 - with pytest.raises(ValueError, match="alias_none"): - worker._place_products(run_id, video, WorkflowDefinition.from_dict(definition)) - del definition["final_outputs"]["alias_none"] - with pytest.raises(ValueError, match="alias_lost"): - worker._place_products(run_id, video, WorkflowDefinition.from_dict(definition)) - assert target_srt.read_text(encoding="utf-8") == "旧字幕内容" - del definition["final_outputs"]["alias_lost"] - placed = worker._place_products(run_id, video, WorkflowDefinition.from_dict(definition)) - assert placed == ["a.CN.srt", "a.CN_dual_eye.ass", "a.other.20260902120000.txt"] - # srt/ass 被改名为标准名且覆盖旧文件;txt 保留原名。 - assert target_srt.read_text(encoding="utf-8") == "1\n00:00:00,000 --> 00:00:01,000\n新中文字幕\n" - assert target_ass.read_text(encoding="utf-8") == "[Script Info]\n新双目字幕\n" - assert (video.parent / "a.other.20260902120000.txt").read_text(encoding="utf-8") == "其余产物内容" - # alias_none(无记录)与 alias_lost(文件丢失)未放置。 - assert not (video.parent / "lost.srt").exists() -def test_batch_worker_job_validation_failures(tmp_path) -> None: - """文件夹缺失/工作流未发布/无版本时任务置为 FAILED 并记录错误。""" - db = _db(tmp_path) - now = _now_iso() - - def add_job(job_id, folder, workflow_id="echo-app"): - db.create_batch_job( - { - "id": job_id, "folder_path": folder, "workflow_id": workflow_id, - "recursive": 1, "status": "QUEUED", "progress": 0, "total": 0, - "done": 0, "failed": 0, "error": None, - "created_at": now, "updated_at": now, - } - ) - - worker = BatchWorker(db, interval_seconds=0.05) - add_job("job_nofolder", str(tmp_path / "missing")) - worker._process_job(db.get_batch_job("job_nofolder")) - assert db.get_batch_job("job_nofolder")["status"] == "FAILED" - assert "folder not found" in db.get_batch_job("job_nofolder")["error"] - - folder = _video_folder(tmp_path) - _seed_echo_workflow(db, published=False) - add_job("job_unpublished", str(folder)) - worker._process_job(db.get_batch_job("job_unpublished")) - assert db.get_batch_job("job_unpublished")["status"] == "FAILED" - assert "not found or unpublished" in db.get_batch_job("job_unpublished")["error"] - - # 已发布但没有任何版本。 - _seed_echo_workflow(db, published=True) - db.delete_workflow("echo-app") - db.upsert_workflow({"id": "echo-app", "name": "E", "description": "", "published": 1, "latest_version": 0}) - add_job("job_noversion", str(folder)) - worker._process_job(db.get_batch_job("job_noversion")) - assert db.get_batch_job("job_noversion")["status"] == "FAILED" - assert "has no version" in db.get_batch_job("job_noversion")["error"] - - -def test_batch_worker_catches_unexpected_job_error(tmp_path) -> None: - """任务级兜底:DAG 解析失败(缺 name)时任务 FAILED 而不是卡死在 RUNNING。""" - db = _db(tmp_path) - _seed_echo_workflow(db) - folder = _video_folder(tmp_path, names=("a.mp4",)) - # 覆盖为缺失 name 的非法定义:from_dict 抛 KeyError。 - bad_definition = {"version": 1, "nodes": [], "edges": []} - db.create_workflow_version("echo-app", 2, bad_definition) - db.upsert_workflow({"id": "echo-app", "name": "E", "description": "", "published": 1, "latest_version": 2}) - job = _make_job(db, folder) - BatchWorker(db, interval_seconds=0.05)._process_job(job) - assert db.get_batch_job(job["id"])["status"] == "FAILED" - assert "name" in db.get_batch_job(job["id"])["error"] - - -def test_batch_worker_cycle_fails_video_not_job(tmp_path) -> None: - """DAG 环在执行期抛错:单个视频 FAILED 记录错误,批量任务继续并完成。""" - db = _db(tmp_path) - _seed_echo_workflow(db) - folder = _video_folder(tmp_path, names=("a.mp4",)) - # 覆盖为带环的定义:validate 通过、调度器拓扑排序时抛"contains a cycle"。 - cycle_definition = { - "name": "bad", - "version": 2, - "nodes": [ - {"id": "x", "node_type": "echo", "inputs": {"file_uri": "y.file_uri"}}, - {"id": "y", "node_type": "echo", "inputs": {"file_uri": "x.file_uri"}}, - ], - "edges": [{"from": "x", "to": "y"}, {"from": "y", "to": "x"}], - "entry_inputs": {"video_uri": "file"}, - "final_outputs": {"result": "x.file_uri"}, - } - db.create_workflow_version("echo-app", 2, cycle_definition) - db.upsert_workflow({"id": "echo-app", "name": "E", "description": "", "published": 1, "latest_version": 2}) - job = _make_job(db, folder) - BatchWorker(db, interval_seconds=0.05)._process_job(job) - video = db.list_batch_videos(job["id"])[0] - assert video["status"] == "FAILED" - assert "cycle" in video["error"] - assert db.get_batch_job(job["id"])["status"] == "COMPLETED" - assert db.get_batch_job(job["id"])["failed"] == 1 - - -def test_batch_worker_loop_processes_and_survives_exceptions(tmp_path, monkeypatch) -> None: - """轮询线程:处理排队任务;轮询异常不杀死线程;重复启动/幽灵任务无害。""" - db = _db(tmp_path) - _seed_echo_workflow(db) - folder = _video_folder(tmp_path, names=("a.mp4",)) - job = _make_job(db, folder) - # 不存在的任务 ID:_run_job 直接返回,不报错。 - BatchWorker(db, interval_seconds=0.05)._process_job({"id": "ghost_job"}) - # 第一次轮询抛异常(模拟数据库抖动),后续正常。 - calls = {"n": 0} - real_next = db.next_queued_batch_job - - def flaky_next(): - calls["n"] += 1 - if calls["n"] == 1: - raise RuntimeError("transient error") - return real_next() - - monkeypatch.setattr(db, "next_queued_batch_job", flaky_next) - worker = BatchWorker(db, interval_seconds=0.05) - worker.start() - # 重复启动无副作用:线程已存在时直接返回。 - worker.start() - try: - deadline = time.monotonic() + 10 - while time.monotonic() < deadline: - if db.get_batch_job(job["id"])["status"] in {"COMPLETED", "FAILED"}: - break - time.sleep(0.1) - finally: - worker.stop() - assert db.get_batch_job(job["id"])["status"] == "COMPLETED" - - -def test_batch_worker_pause_job_writes_flag_and_pauses_run(tmp_path) -> None: - """pause_job:任务置 PAUSED、排队/运行中的 run 暂停并写 paused.flag。""" - db = _db(tmp_path) - _seed_echo_workflow(db) - folder = _video_folder(tmp_path, names=("a.mp4", "b.mp4", "c.mp4")) - job = _make_job(db, folder) - videos = {Path(v["video_path"]).stem: v for v in db.list_batch_videos(job["id"])} - - # a:QUEUED 运行(应被暂停并写 flag);b:run 记录不存在;c:已完成的 run。 - _create_run(db, "run_pause_me", folder, status="QUEUED", name="a") - _create_run(db, "run_done_c", folder, status="COMPLETED", name="c") - db.update_batch_video(videos["a"]["id"], run_id="run_pause_me", updated_at=_now_iso()) - db.update_batch_video(videos["b"]["id"], run_id="run_ghost", updated_at=_now_iso()) - db.update_batch_video(videos["c"]["id"], run_id="run_done_c", updated_at=_now_iso()) - - worker = BatchWorker(db, interval_seconds=0.05) - worker.pause_job(job["id"]) - assert db.get_batch_job(job["id"])["status"] == "PAUSED" - assert db.get_run("run_pause_me")["status"] == "PAUSED" - work_dir_a = Path(videos["a"]["work_dir"]) - assert (work_dir_a / "runs" / "run_pause_me" / PAUSE_FLAG).is_file() - # b 的 run 不存在、c 的 run 已完成:都被跳过,不写 flag。 - assert not (Path(videos["b"]["work_dir"]) / "runs" / "run_ghost" / PAUSE_FLAG).exists() - assert not (Path(videos["c"]["work_dir"]) / "runs" / "run_done_c" / PAUSE_FLAG).exists() - - # 继续:任务回到 QUEUED,由引擎从断点续跑。 - worker.resume_job(job["id"]) - assert db.get_batch_job(job["id"])["status"] == "QUEUED" - - - -def test_batch_progress_sync_recounts_done_failed(tmp_path) -> None: - """sync_batch_job_progress 按明细实时重算 done(=完成+跳过) 与 failed。 - - 回归:此前 batch_jobs.done 只在任务收尾一次性汇总,处理中途(尤其暂停) - 恒为 0——前端显示 0/431 0%,与实际已处理数量严重不符(真实任务 - batch_fee668175444 已处理 15 个仍显示 0/431)。本测试要求无论任务处于 - 哪种状态,汇总字段都与明细实时一致。 - """ - db = _db(tmp_path) - _seed_echo_workflow(db) - folder = _video_folder(tmp_path, names=("a.mp4", "b.mp4", "c.mp4", "d.mp4")) - job = _make_job(db, folder) - job_id = job["id"] - videos = {Path(v["video_path"]).stem: v for v in db.list_batch_videos(job_id)} - - # 手工构造典型中途态:a 已完成、b 处理中被暂停、c 失败、d 仍排队。 - db.update_batch_video(videos["a"]["id"], status="COMPLETED", updated_at=_now_iso()) - db.update_batch_video(videos["b"]["id"], status="PAUSED", updated_at=_now_iso()) - db.update_batch_video(videos["c"]["id"], status="FAILED", error="boom", updated_at=_now_iso()) - # 任务保持 PAUSED(引擎在视频间停下时的真实状态)。 - db.update_batch_job(job_id, status="PAUSED", updated_at=_now_iso()) - - db.sync_batch_job_progress(job_id) - synced = db.get_batch_job(job_id) - # done 计入已完成 + 已跳过(与收尾口径一致);失败单独计;进行中不计。 - assert synced["done"] == 1 - assert synced["failed"] == 1 - - # 幂等:重复对齐结果不变(引擎在多个边界可能重复调用)。 - db.sync_batch_job_progress(job_id) - assert db.get_batch_job(job_id)["done"] == 1 - - -def test_batch_progress_sync_counts_skipped(tmp_path) -> None: - """SKIPPED 视频不计入 total 也不计入 done(它不需要本批处理)。 - - 新口径(2026-09):进度只反映"本批无字幕待处理"的视频。 - """ - db = _db(tmp_path) - _seed_echo_workflow(db) - folder = _video_folder(tmp_path, names=("a.mp4",)) - job = _make_job(db, folder) - # 把 a 改为 SKIPPED(等同创建时旁挂字幕被跳过的语义)。 - db.update_batch_video(db.list_batch_videos(job["id"])[0]["id"], status="SKIPPED", updated_at=_now_iso()) - db.sync_batch_job_progress(job["id"]) - job = db.get_batch_job(job["id"]) - assert job["done"] == 0 - assert job["total"] == 0 - - -def test_batch_progress_sync_returns_refreshed_job(tmp_path) -> None: - """refresh_batch_job 返回对齐后的最新任务记录(供 router 读取侧使用)。""" - db = _db(tmp_path) - _seed_echo_workflow(db) - folder = _video_folder(tmp_path, names=("a.mp4", "b.mp4")) - job = _make_job(db, folder) - job_id = job["id"] - db.update_batch_video(db.list_batch_videos(job_id)[0]["id"], status="COMPLETED", updated_at=_now_iso()) - refreshed = db.refresh_batch_job(job_id) - assert refreshed is not None - assert refreshed["done"] == 1 - # 幽灵任务:返回 None,不报错。 - assert db.refresh_batch_job("batch_ghost") is None - - -def test_batch_worker_paused_job_reports_real_done(tmp_path, monkeypatch) -> None: - """引擎在视频之间暂停后,job 汇总实时反映已完成数量(不等任务收尾)。 - - 回归:真实任务 batch_fee668175444 手动暂停后前端仍显示 0/431 0%, - 因为 done 只在任务整体 COMPLETED 时汇总一次。修复后引擎每次停下都要 - 用明细实时对齐 done/failed,暂停中前端即可看到真实进度。 - """ - db = _db(tmp_path) - _seed_echo_workflow(db) - folder = _video_folder(tmp_path, names=("a.mp4", "b.mp4")) - job = _make_job(db, folder) - job_id = job["id"] - from wov_app.scheduler import WorkflowScheduler - - class _PauseAfterFirstScheduler: - """真实执行第一个视频后把批量任务置为 PAUSED(模拟用户处理中暂停)。""" - - def __init__(self, db: Database, work_dir: Path) -> None: - self.db = db - self.work_dir = work_dir - - def execute_run(self, run_id: str) -> None: - WorkflowScheduler(self.db, self.work_dir).execute_run(run_id) - self.db.update_batch_job(job_id, status="PAUSED", updated_at=_now_iso()) - - monkeypatch.setattr("wov_app.batch.WorkflowScheduler", _PauseAfterFirstScheduler) - BatchWorker(db, interval_seconds=0.05)._process_job(db.get_batch_job(job_id)) - - videos = {Path(v["video_path"]).name: v for v in db.list_batch_videos(job_id)} - assert videos["a.mp4"]["status"] == "COMPLETED" - assert db.get_batch_job(job_id)["status"] == "PAUSED" - # 关键断言:暂停瞬间 done 已是 1(a 完成),而不是停留在创建时的 0。 - assert db.get_batch_job(job_id)["done"] == 1 - - -def test_batch_worker_skipped_continue_syncs_done(tmp_path) -> None: - """循环里遇到 SKIPPED/COMPLETED 的 continue 分支不把它们计入 done。 - - 新口径:total=无字幕视频数(本例 a 有字幕 SKIPPED → total=1 即 b), - done 只计实际完成(b=COMPLETED → done=1)。SKIPPED 不占分母也不占分子。 - """ - db = _db(tmp_path) - _seed_echo_workflow(db) - folder = _video_folder(tmp_path, names=("a.mp4", "b.mp4")) - (folder / "a.CN.srt").write_text("x", encoding="utf-8") # a 创建即 SKIPPED - job = _make_job(db, folder) - job_id = job["id"] - # 创建即固定:total=无字幕数(仅 b=1)。 - assert db.get_batch_job(job_id)["total"] == 1 - # b 手工置为 COMPLETED(模拟此前已完成);a 在循环里走 continue 分支。 - db.update_batch_video( - [v for v in db.list_batch_videos(job_id) if v["video_path"].endswith("b.mp4")][0]["id"], - status="COMPLETED", updated_at=_now_iso(), - ) - db.update_batch_job(job_id, status="PAUSED", updated_at=_now_iso()) - BatchWorker(db, interval_seconds=0.05)._run_job(job_id) - job = db.get_batch_job(job_id) - # done 只计实际完成的 b(1),SKIPPED 的 a 不计;total 保持 1。 - assert job["done"] == 1 and job["total"] == 1 - -def test_batch_worker_ghost_job_id_returns(tmp_path) -> None: - """_run_job 在任务不存在时直接返回(幽灵任务处理无副作用)。""" - db = _db(tmp_path) - BatchWorker(db, interval_seconds=0.05)._run_job("batch_ghost") - - -# --------------------------------------------------------------------------- -# 批量 API -# --------------------------------------------------------------------------- - - -def _client_with_echo_workflow(tmp_path): - """返回 TestClient 与包含真实视频文件的文件夹(echo 工作流已发布)。""" - folder = _video_folder(tmp_path, names=("a.mp4", "b.mp4")) - client = TestClient(app) - client.__enter__() - client.post( - "/api/admin/workflows", - json={ - "id": "echo-app", - "name": "Echo App", - "description": "batch test", - "definition": { - "name": "echo-flow", - "version": 1, - "nodes": [ - {"id": "step", "node_type": "echo", "inputs": {"file_uri": "input.video_uri"}} - ], - "edges": [], - "entry_inputs": {"video_uri": "file"}, - "final_outputs": {"result": "step.file_uri"}, - }, - }, - ) - client.post("/api/admin/workflows/echo-app/publish") - return client, folder - - -def test_batch_api_create_list_detail(tmp_path) -> None: - """批量 API:创建任务、列表、详情(含 SKIPPED 状态与视频旁产物清单)。""" - client, folder = _client_with_echo_workflow(tmp_path) - try: - # a 视频旁已有字幕(创建即 SKIPPED),b 待处理。 - (folder / "a.CN_dual_eye.ass").write_text("已有字幕", encoding="utf-8") - response = client.post( - "/api/batch/jobs", - json={"folder": str(folder), "workflow_id": "echo-app", "recursive": True}, - ) - assert response.status_code == 200 - job = response.json() - assert job["status"] == "QUEUED" - by_name = {Path(v["video_path"]).name: v for v in job["videos"]} - assert by_name["a.mp4"]["status"] == "SKIPPED" - assert by_name["a.mp4"]["finals"] == {"a.CN_dual_eye.ass": "a.CN_dual_eye.ass"} - assert by_name["b.mp4"]["status"] == "PENDING" - assert by_name["b.mp4"]["finals"] == {} - - listed = client.get("/api/batch/jobs").json() - assert any(item["id"] == job["id"] for item in listed) - - detail = client.get(f"/api/batch/jobs/{job['id']}").json() - assert detail["workflow_id"] == "echo-app" - assert len(detail["videos"]) == 2 - # 新口径:total = 无字幕需处理数(b 一个),SKIPPED 的 a 不占分母。 - assert detail["total"] == 1 and detail["done"] == 0 - - missing = client.get("/api/batch/jobs/ghost") - assert missing.status_code == 404 - finally: - client.__exit__(None, None, None) - - - - -def test_batch_api_all_skipped_created_completed(tmp_path) -> None: - """全部视频已有字幕时,创建响应直接为 COMPLETED,total=0。""" - client, folder = _client_with_echo_workflow(tmp_path) - try: - # 两个视频都已有旁挂字幕 → 无任何无字幕项。 - (folder / "a.CN.srt").write_text("x", encoding="utf-8") - (folder / "b.CN_dual_eye.ass").write_text("x", encoding="utf-8") - job = client.post( - "/api/batch/jobs", - json={"folder": str(folder), "workflow_id": "echo-app"}, - ).json() - assert job["status"] == "COMPLETED" - assert job["total"] == 0 and job["done"] == 0 - assert all(v["status"] == "SKIPPED" for v in job["videos"]) - finally: - client.__exit__(None, None, None) -def test_batch_api_creation_errors(tmp_path) -> None: - """批量 API 拒绝:文件夹不存在、无视频、未发布工作流。""" - client, folder = _client_with_echo_workflow(tmp_path) - try: - bad_folder = client.post( - "/api/batch/jobs", - json={"folder": str(tmp_path / "missing"), "workflow_id": "echo-app"}, - ) - assert bad_folder.status_code == 422 - empty = tmp_path / "empty" - empty.mkdir() - no_videos = client.post( - "/api/batch/jobs", - json={"folder": str(empty), "workflow_id": "echo-app"}, - ) - assert no_videos.status_code == 422 - not_published = client.post( - "/api/batch/jobs", - json={"folder": str(folder), "workflow_id": "ghost"}, - ) - assert not_published.status_code == 422 - finally: - client.__exit__(None, None, None) - - -def test_batch_api_list_detail_syncs_real_progress(tmp_path) -> None: - """列表/详情读取前自动对齐 done/failed:暂停中的任务也显示真实进度。 - - 回归:服务运行中任务被暂停(batch_fee668175444),前端轮询列表看到 - 0/431 0%——因为 done 只在收尾时汇总、暂停后无人再写。读取侧兜底对齐 - 保证前端拿到明细真实状态,即使引擎不在运行(进程被杀/暂停中)。 - """ - client, folder = _client_with_echo_workflow(tmp_path) - try: - job = client.post( - "/api/batch/jobs", - json={"folder": str(folder), "workflow_id": "echo-app"}, - ).json() - job_id = job["id"] - db = app.state.db - - # 模拟暂停中已处理 1 个(COMPLETED)+ 1 个失败:明细变了,但 job.done 仍是 0。 - videos = db.list_batch_videos(job_id) - db.update_batch_video(videos[0]["id"], status="COMPLETED", updated_at=_now_iso()) - db.update_batch_video(videos[1]["id"], status="FAILED", error="x", updated_at=_now_iso()) - db.update_batch_job(job_id, status="PAUSED", updated_at=_now_iso()) - assert db.get_batch_job(job_id)["done"] == 0 # 修复前:旧值 - - # 读取详情:返回的 job 已完成实时对齐。 - detail = client.get(f"/api/batch/jobs/{job_id}").json() - assert detail["done"] == 1 and detail["failed"] == 1 - # 数据库里的汇总也一并修正(读取副作用:后续列表/引擎都看到正确值)。 - assert db.get_batch_job(job_id)["done"] == 1 - - # 列表接口同样实时对齐。 - listed = client.get("/api/batch/jobs").json() - mine = next(item for item in listed if item["id"] == job_id) - assert mine["done"] == 1 and mine["failed"] == 1 - finally: - client.__exit__(None, None, None) - - -def test_batch_api_pause_resume_delete(tmp_path) -> None: - """批量 API:暂停/继续切换任务状态,删除清理 DB 记录与私有工作空间。""" - client, folder = _client_with_echo_workflow(tmp_path) - try: - job = client.post( - "/api/batch/jobs", - json={"folder": str(folder), "workflow_id": "echo-app"}, - ).json() - - paused = client.post(f"/api/batch/jobs/{job['id']}/pause") - assert paused.status_code == 200 - assert client.get(f"/api/batch/jobs/{job['id']}").json()["status"] == "PAUSED" - - resumed = client.post(f"/api/batch/jobs/{job['id']}/resume") - assert resumed.status_code == 200 - assert client.get(f"/api/batch/jobs/{job['id']}").json()["status"] == "QUEUED" - - # 给第一个视频挂一个 run,验证删除任务时级联删除 run 记录。 - db = app.state.db - video = job["videos"][0] - _create_run(db, "run_del", folder, name=Path(video["video_path"]).stem) - db.update_batch_video(video["id"], run_id="run_del", updated_at=_now_iso()) - assert db.get_run("run_del") is not None - # 模拟任务遗留的私有工作空间:删除任务时一并清理。 - workspace = BATCH_WORK_ROOT / job["id"] - (workspace / "runs" / "run_del").mkdir(parents=True) - (workspace / "runs" / "run_del" / "paused.flag").write_text("", encoding="utf-8") - - deleted = client.delete(f"/api/batch/jobs/{job['id']}") - assert deleted.status_code == 200 - assert client.get(f"/api/batch/jobs/{job['id']}").status_code == 404 - assert db.get_run("run_del") is None - assert not workspace.exists() - - assert client.post("/api/batch/jobs/ghost/pause").status_code == 404 - assert client.post("/api/batch/jobs/ghost/resume").status_code == 404 - assert client.delete("/api/batch/jobs/ghost").status_code == 404 - finally: - client.__exit__(None, None, None) - - -def test_batch_api_worker_unavailable(tmp_path, monkeypatch) -> None: - """批量引擎不可用时,暂停/继续接口返回 503。""" - client, folder = _client_with_echo_workflow(tmp_path) - try: - job = client.post( - "/api/batch/jobs", - json={"folder": str(folder), "workflow_id": "echo-app"}, - ).json() - monkeypatch.setattr("wov_app.routers.batch._get_worker", lambda: None) - assert client.post(f"/api/batch/jobs/{job['id']}/pause").status_code == 503 - assert client.post(f"/api/batch/jobs/{job['id']}/resume").status_code == 503 - finally: - client.__exit__(None, None, None) - - -def test_batch_api_download_sidecar_subtitle(tmp_path) -> None: - """批量 API:下载视频旁的字幕文件,缺失别名/文件返回 404。""" - client, folder = _client_with_echo_workflow(tmp_path) - try: - (folder / "a.CN_dual_eye.ass").write_text("已有字幕内容", encoding="utf-8") - job = client.post( - "/api/batch/jobs", - json={"folder": str(folder), "workflow_id": "echo-app"}, - ).json() - video = next(v for v in job["videos"] if Path(v["video_path"]).name == "a.mp4") - - ok = client.get(f"/api/batch/jobs/{job['id']}/videos/{video['id']}/download?alias=a.CN_dual_eye.ass") - assert ok.status_code == 200 - assert ok.content == "已有字幕内容".encode("utf-8") - - bad_alias = client.get(f"/api/batch/jobs/{job['id']}/videos/{video['id']}/download?alias=nope") - assert bad_alias.status_code == 404 - bad_video = client.get(f"/api/batch/jobs/{job['id']}/videos/bv_ghost/download?alias=a.CN_dual_eye.ass") - assert bad_video.status_code == 404 - finally: - client.__exit__(None, None, None) - - -def test_batch_api_download_sidecar_file_missing(tmp_path, monkeypatch) -> None: - """下载时旁挂字幕文件已被外部移除(列表与下载之间的竞态)→ 404。""" - client, folder = _client_with_echo_workflow(tmp_path) - try: - (folder / "a.srt").write_text("x", encoding="utf-8") - job = client.post( - "/api/batch/jobs", - json={"folder": str(folder), "workflow_id": "echo-app"}, - ).json() - video = next(v for v in job["videos"] if Path(v["video_path"]).name == "a.mp4") - - # 模拟列表后文件被删:让旁挂字幕扫描返回一个磁盘上已不存在的路径。 - gone = folder / "a.srt" - gone.unlink() - monkeypatch.setattr(batch_engine, "list_sidecar_subtitles", lambda video_path: [gone]) - missing = client.get(f"/api/batch/jobs/{job['id']}/videos/{video['id']}/download?alias=a.srt") - assert missing.status_code == 404 - assert "file missing" in missing.json()["detail"] - finally: - client.__exit__(None, None, None) - - -def test_batch_api_download_legacy_marker(tmp_path) -> None: - """批量 API:旧版完成标记(batch.done.json)里的语义别名仍可下载。""" - client, folder = _client_with_echo_workflow(tmp_path) - try: - job = client.post( - "/api/batch/jobs", - json={"folder": str(folder), "workflow_id": "echo-app"}, - ).json() - db = app.state.db - video = job["videos"][0] - # 旧版布局:work_dir 是视频的同名文件夹,里面写 batch.done.json + 产物。 - work = Path(video["work_dir"]) - work.mkdir(parents=True) - marker = {"workflow_id": "echo-app", "workflow_version": 1, "run_id": "run_old", "finals": {"result": "a.result.20260819000000.txt"}} - (work / MARKER_NAME).write_text(json.dumps(marker), encoding="utf-8") - (work / "a.result.20260819000000.txt").write_text("下载内容", encoding="utf-8") - - # 详情页产物清单合并旧版完成标记里的语义别名。 - detail = client.get(f"/api/batch/jobs/{job['id']}").json() - video_detail = next(v for v in detail["videos"] if v["id"] == video["id"]) - assert video_detail["finals"]["result"] == "a.result.20260819000000.txt" - - ok = client.get(f"/api/batch/jobs/{job['id']}/videos/{video['id']}/download?alias=result") - assert ok.status_code == 200 - assert ok.content == "下载内容".encode("utf-8") - # 标记里别名对应的产物文件缺失 → 404(不会落到旁挂字幕解析)。 - (work / "a.result.20260819000000.txt").unlink() - gone = client.get(f"/api/batch/jobs/{job['id']}/videos/{video['id']}/download?alias=result") - assert gone.status_code == 404 - finally: - client.__exit__(None, None, None) - - -def test_batch_api_roots(tmp_path, monkeypatch) -> None: - """目录树选择器:返回可浏览根目录(含根/家目录与 Windows 盘符分支)。""" - client, _folder = _client_with_echo_workflow(tmp_path) - try: - roots = client.get("/api/batch/roots").json() - assert isinstance(roots, list) and len(roots) >= 1 - # POSIX 必有 /;Windows 必有盘符;两者都有家目录。 - assert any(item["path"] in ("/", str(Path.home())) for item in roots) - assert all(item["name"] for item in roots) - - # Windows 分支:模拟 os.name=nt,存在盘符时返回该驱动器。 - import os - - real_exists = Path.exists - - def fake_exists(path): - # 盘符形式(如 C:\)视为存在,其余走真实判断。 - return str(path).endswith(":\\") or real_exists(path) - - monkeypatch.setattr(os, "name", "nt") - monkeypatch.setattr(Path, "exists", fake_exists) - nt_roots = client.get("/api/batch/roots").json() - assert any(str(item["path"]).endswith(":\\") for item in nt_roots) - finally: - client.__exit__(None, None, None) - - -def test_batch_api_dirs(tmp_path, monkeypatch) -> None: - """目录树选择器:列出子目录、隐藏目录过滤、不存在/不可读返回空。""" - client, _folder = _client_with_echo_workflow(tmp_path) - try: - # 真实目录结构:普通子目录、隐藏目录、文件。 - target = tmp_path / "media" - (target / "movies").mkdir(parents=True) - (target / "series").mkdir(parents=True) - (target / ".hidden").mkdir(parents=True) - (target / "note.txt").write_text("x", encoding="utf-8") - - data = client.get(f"/api/batch/dirs?path={target}").json() - assert [item["name"] for item in data["dirs"]] == ["movies", "series"] - assert data["path"] == str(target) - - # 路径指向文件 → 空列表。 - file_data = client.get(f"/api/batch/dirs?path={target / 'note.txt'}").json() - assert file_data["dirs"] == [] - # 目录不存在 → 空列表。 - missing = client.get(f"/api/batch/dirs?path={tmp_path / 'ghost'}").json() - assert missing["dirs"] == [] - - from pathlib import Path as RealPath - - # 单个子项不可读(is_dir 抛 OSError)→ 跳过该项,其余目录正常返回。 - class _PoisonPath(RealPath): - """is_dir 恒抛权限错误的子类,模拟不可读的子目录。""" - - def is_dir(self): - raise OSError("denied") - - real_iterdir = RealPath.iterdir - - def mixed_iterdir(path): - return list(real_iterdir(path)) + [_PoisonPath(str(tmp_path / "poison"))] - - monkeypatch.setattr(RealPath, "iterdir", mixed_iterdir) - mixed = client.get(f"/api/batch/dirs?path={target}").json() - assert [item["name"] for item in mixed["dirs"]] == ["movies", "series"] - - # 整个目录不可读(iterdir 抛 OSError)→ 空列表而不是 500。 - def deny(path): - raise OSError("denied") - - monkeypatch.setattr(RealPath, "iterdir", deny) - denied = client.get(f"/api/batch/dirs?path={target}").json() - assert denied["dirs"] == [] - finally: - client.__exit__(None, None, None) - - -def test_main_starts_batch_worker_when_enabled(monkeypatch) -> None: - """WOV_BATCH_ENABLED=1 时应用生命周期启动批量引擎后台线程(退出时回收)。""" - monkeypatch.setenv("WOV_BATCH_ENABLED", "1") - client = TestClient(app) - with client: - worker = app.state.batch - assert worker is not None - assert worker._thread is not None - assert worker._thread.name == "wov-batch-worker" - # 退出应用后批量引擎线程已停止,后续测试不会被后台线程打扰。 - assert worker._thread is None - - -# STORAGE_DIR 引用仅供静态检查使用(ensure batch 根相对应用存储目录)。 -assert BATCH_WORK_ROOT == STORAGE_DIR / "batch" diff --git a/tests/test_crop_js.js b/tests/test_crop_js.js deleted file mode 100644 index b86e668..0000000 --- a/tests/test_crop_js.js +++ /dev/null @@ -1,32 +0,0 @@ -// crop 归一化纯函数单测:由 pytest 通过 node 执行(TDD 红阶段先失败)。 -"use strict"; -const assert = require("assert"); -const { videoDisplayRect, rectToCrop, cropToRect } = require("../web/assets/crop.js"); - -// 1) 无留边(容器比例与视频一致):底部 20% 矩形 → crop [0, 0.8, 1, 0.2] -let d = videoDisplayRect(1280, 720, 1280, 720); -assert.deepStrictEqual(d, { x: 0, y: 0, w: 1280, h: 720 }); -assert.deepStrictEqual( - rectToCrop({ x: 0, y: 576, w: 1280, h: 144 }, 1280, 720, 1280, 720), - [0, 0.8, 1, 0.2] -); - -// 2) letterbox(容器比视频宽):视频显示在中间,矩形映射要考虑左右留边 -d = videoDisplayRect(1280, 720, 1600, 720); -assert.deepStrictEqual(d, { x: 160, y: 0, w: 1280, h: 720 }); -// 在渲染视频内框选右下 25% 区域 -let crop = rectToCrop({ x: 160 + 640, y: 360, w: 640, h: 360 }, 1280, 720, 1600, 720); -assert.deepStrictEqual(crop, [0.5, 0.5, 0.5, 0.5]); - -// 3) 回显一致性:crop → rect → crop 应还原(含 letterbox) -let back = cropToRect(crop, 1280, 720, 1600, 720); -assert.deepStrictEqual( - rectToCrop(back, 1280, 720, 1600, 720), - crop -); - -// 4) 越界钳制:矩形超出画面时 crop 值被限制在 0~1 -crop = rectToCrop({ x: -100, y: -50, w: 2000, h: 900 }, 1280, 720, 1280, 720); -assert.ok(crop.every((v) => v >= 0 && v <= 1)); - -console.log("crop.js 全部断言通过"); diff --git a/tests/test_db.py b/tests/test_db.py deleted file mode 100644 index 44cdf2a..0000000 --- a/tests/test_db.py +++ /dev/null @@ -1,464 +0,0 @@ -"""数据库层单元测试。 - -直接对 Database 方法调用真实 SQLite 路径,覆盖工作流、版本、任务与产物的 -增删改查。节点注册表已改为进程内内存态,不再落库。 -""" - -from pathlib import Path - -from wov_app.db import Database - - -def test_workflow_crud(tmp_path) -> None: - """验证工作流概要的插入、发布标记更新与删除。""" - db = Database(tmp_path / "wov.db") - workflow = { - "id": "demo", - "name": "Demo", - "description": "desc", - "published": 0, - "latest_version": 0, - } - db.upsert_workflow(workflow) - assert db.get_workflow("demo")["name"] == "Demo" - assert [item["id"] for item in db.list_workflows()] == ["demo"] - - db.upsert_workflow({**workflow, "published": 1, "latest_version": 1}) - assert db.get_workflow("demo")["published"] == 1 - - db.delete_workflow("demo") - assert db.get_workflow("demo") is None - - -def test_workflow_versions(tmp_path) -> None: - """验证工作流版本的写入、最新版本查询与列表。""" - db = Database(tmp_path / "wov.db") - db.upsert_workflow( - {"id": "demo", "name": "Demo", "published": 1, "latest_version": 2} - ) - definition = {"name": "Demo", "version": 1, "nodes": [], "edges": []} - db.create_workflow_version("demo", 1, definition) - db.create_workflow_version("demo", 2, {**definition, "version": 2}) - - latest = db.get_latest_workflow_version("demo") - assert latest["version"] == 2 - assert latest["definition"]["version"] == 2 - - version = db.get_workflow_version("demo", 1) - assert version["version"] == 1 - assert db.get_workflow_version("demo", 99) is None - assert len(db.list_workflow_versions("demo")) == 2 - - empty_db = Database(tmp_path / "empty.db") - assert empty_db.get_latest_workflow_version("missing") is None - - -def test_run_and_artifact_crud(tmp_path) -> None: - """验证任务与产物的创建、查询、更新与删除。""" - db = Database(tmp_path / "wov.db") - db.upsert_workflow({"id": "demo", "name": "Demo", "published": 1, "latest_version": 1}) - now = "2026-01-01T00:00:00+00:00" - db.create_run( - { - "id": "run_1", - "workflow_id": "demo", - "workflow_version": 1, - "status": "QUEUED", - "progress": 0, - "input_uri": "in.txt", - "created_at": now, - "updated_at": now, - } - ) - assert db.get_run("run_1")["status"] == "QUEUED" - assert db.next_queued_run()["id"] == "run_1" - - db.update_run("run_1", status="RUNNING", progress=0.5, updated_at=now) - db.update_run("run_1") - assert db.get_run("run_1")["status"] == "RUNNING" - assert db.get_run("run_1")["progress"] == 0.5 - assert db.next_queued_run() is None - assert len(db.list_runs()) == 1 - - db.create_artifact( - { - "run_id": "run_1", - "node_id": "echo", - "name": "result", - "uri": "out.txt", - "mime_type": "text/plain", - "size": 3, - } - ) - assert db.get_artifact("run_1", "result")["uri"] == "out.txt" - assert db.get_artifact("run_1", "missing") is None - assert len(db.list_artifacts("run_1")) == 1 - - db.delete_run_artifacts("run_1") - assert db.list_artifacts("run_1") == [] - - -def test_reset_run_clears_error_and_artifacts(tmp_path) -> None: - """验证 reset_run 会把失败任务恢复到排队状态并清空旧产物。""" - db = Database(tmp_path / "wov.db") - db.upsert_workflow({"id": "demo", "name": "Demo", "published": 1, "latest_version": 1}) - now = "2026-01-01T00:00:00+00:00" - db.create_run( - { - "id": "run_1", - "workflow_id": "demo", - "workflow_version": 1, - "status": "FAILED", - "progress": 0.75, - "current_node_id": "translate", - "error": "timed out", - "input_uri": "in.txt", - "created_at": now, - "updated_at": now, - } - ) - db.create_artifact( - { - "run_id": "run_1", - "node_id": "asr", - "name": "asr.srt_uri", - "uri": "out.srt", - "mime_type": "application/x-subrip", - "size": 3, - } - ) - - db.reset_run("run_1", "2026-01-02T00:00:00+00:00") - - run = db.get_run("run_1") - assert run["status"] == "QUEUED" - assert run["progress"] == 0 - assert run["current_node_id"] is None - assert run["error"] is None - assert run["updated_at"] == "2026-01-02T00:00:00+00:00" - assert run["created_at"] == now - assert db.list_artifacts("run_1") == [] - - -def test_delete_run(tmp_path) -> None: - """验证 delete_run 会删除任务记录及其产物记录。""" - db = Database(tmp_path / "wov.db") - db.upsert_workflow({"id": "demo", "name": "Demo", "published": 1, "latest_version": 1}) - now = "2026-01-01T00:00:00+00:00" - db.create_run( - { - "id": "run_1", - "workflow_id": "demo", - "workflow_version": 1, - "status": "COMPLETED", - "progress": 1, - "input_uri": "in.txt", - "created_at": now, - "updated_at": now, - } - ) - db.create_artifact( - { - "run_id": "run_1", - "node_id": "asr", - "name": "asr.srt_uri", - "uri": "out.srt", - "mime_type": "application/x-subrip", - "size": 3, - } - ) - db.delete_run("run_1") - assert db.get_run("run_1") is None - assert db.list_artifacts("run_1") == [] - - -def test_list_run_ids(tmp_path) -> None: - """验证 list_run_ids 返回全部任务 ID,供孤儿清理对照使用。""" - db = Database(tmp_path / "wov.db") - db.upsert_workflow({"id": "demo", "name": "Demo", "published": 1, "latest_version": 1}) - now = "2026-01-01T00:00:00+00:00" - assert db.list_run_ids() == [] - for run_id in ("run_a", "run_b"): - db.create_run( - { - "id": run_id, - "workflow_id": "demo", - "workflow_version": 1, - "status": "QUEUED", - "progress": 0, - "created_at": now, - "updated_at": now, - } - ) - assert set(db.list_run_ids()) == {"run_a", "run_b"} - - -def test_run_param_overrides_persist(tmp_path) -> None: - """验证 param_overrides 随任务持久化并可读回。""" - db = Database(tmp_path / "wov.db") - db.upsert_workflow({"id": "demo", "name": "D", "published": 1, "latest_version": 1}) - now = "2026-01-01T00:00:00+00:00" - db.create_run( - { - "id": "run_ov", - "workflow_id": "demo", - "workflow_version": 1, - "status": "QUEUED", - "progress": 0, - "param_overrides": {"extract": {"crop": [0, 0.5, 1, 0.5]}}, - "created_at": now, - "updated_at": now, - } - ) - run = db.get_run("run_ov") - assert run["param_overrides"] == {"extract": {"crop": [0, 0.5, 1, 0.5]}} - assert db.next_queued_run()["param_overrides"] == {"extract": {"crop": [0, 0.5, 1, 0.5]}} - - -def test_db_migration_adds_param_overrides(tmp_path) -> None: - """旧库迁移:缺少 param_overrides 列的库打开后自动补列。""" - import sqlite3 - - db_path = tmp_path / "old.db" - conn = sqlite3.connect(db_path) - conn.execute( - "CREATE TABLE workflow_runs (id TEXT PRIMARY KEY, workflow_id TEXT NOT NULL," - " workflow_version INTEGER NOT NULL, status TEXT NOT NULL, current_node_id TEXT," - " progress REAL NOT NULL DEFAULT 0, error TEXT, input_uri TEXT," - " created_at TEXT NOT NULL, updated_at TEXT NOT NULL)" - ) - conn.commit() - conn.close() - - Database(db_path) - conn = sqlite3.connect(db_path) - columns = [row[1] for row in conn.execute("PRAGMA table_info(workflow_runs)")] - conn.close() - assert "param_overrides" in columns - - -def test_pause_resume_run(tmp_path) -> None: - """验证 pause_run/resume_run 的状态流转与 PAUSED 任务不被调度器自动拾起。 - - 修复回归:PAUSED 任务若被 next_queued_run 取到,execute_run 会把它复活为 - RUNNING 继续执行——"点击暂停反而开始任务"。暂停必须由用户显式 resume - (PAUSED → QUEUED)后调度器才重新执行。 - """ - db = Database(tmp_path / "wov.db") - db.upsert_workflow({"id": "demo", "name": "Demo", "published": 1, "latest_version": 1}) - now = "2026-01-01T00:00:00+00:00" - db.create_run( - { - "id": "run_p", - "workflow_id": "demo", - "workflow_version": 1, - "status": "QUEUED", - "progress": 0, - "created_at": now, - "updated_at": now, - } - ) - db.pause_run("run_p", now) - assert db.get_run("run_p")["status"] == "PAUSED" - # 已暂停的任务不会被调度器拾起(等待用户显式 resume)。 - assert db.next_queued_run() is None - db.resume_run("run_p", now) - assert db.get_run("run_p")["status"] == "QUEUED" - assert db.next_queued_run()["id"] == "run_p" - - -def test_recover_interrupted_runs(tmp_path) -> None: - """重启恢复:遗留 RUNNING 任务恢复为 QUEUED(保留产物供断点续跑)。 - - 进程被杀/重启时 RUNNING 任务不会自动收尾,若保持 RUNNING 将永久孤儿 - (next_queued_run 不拾起、暂停后又被 execute_run 复活)。恢复为 QUEUED - 后调度器会从产物表断点续跑;用户主动暂停的 PAUSED 任务保持不变。 - """ - db = Database(tmp_path / "wov.db") - db.upsert_workflow({"id": "demo", "name": "Demo", "published": 1, "latest_version": 1}) - now = "2026-01-01T00:00:00+00:00" - for run_id, status in (("run_orphan", "RUNNING"), ("run_paused", "PAUSED"), - ("run_done", "COMPLETED")): - db.create_run( - { - "id": run_id, - "workflow_id": "demo", - "workflow_version": 1, - "status": status, - "progress": 0.5, - "created_at": now, - "updated_at": now, - } - ) - recovered = db.recover_interrupted_runs("2026-01-02T00:00:00+00:00") - assert recovered == 1 # 只有 RUNNING 被恢复。 - assert db.get_run("run_orphan")["status"] == "QUEUED" - assert db.get_run("run_orphan")["updated_at"] == "2026-01-02T00:00:00+00:00" - assert db.get_run("run_paused")["status"] == "PAUSED" - assert db.get_run("run_done")["status"] == "COMPLETED" - # 恢复后调度器可拾起并断点续跑。 - assert db.next_queued_run()["id"] == "run_orphan" - -def test_recover_interrupted_batch_jobs(tmp_path) -> None: - """重启恢复:遗留 RUNNING 的批量任务恢复为 QUEUED,供引擎重新拾起。 - - 批量引擎处理视频(尤其大文件)时进程被杀/重启,批量任务停在 RUNNING。 - 若保持 RUNNING,next_queued_batch_job 只拾取 QUEUED,任务永远不会被再次 - 驱动 → 未处理完的 PENDING 视频永久残留(batch_969fabe74b83 事故链路之一)。 - 恢复为 QUEUED 后引擎重新拾起续跑;PAUSED 批量任务保持不变等待显式 resume。 - """ - db = Database(tmp_path / "wov.db") - now = "2026-01-01T00:00:00+00:00" - - def add_job(job_id: str, status: str) -> None: - """创建指定状态的批量任务记录。""" - db.create_batch_job( - { - "id": job_id, "folder_path": "/tmp/videos", "workflow_id": "demo", - "recursive": 1, "status": status, "progress": 0.5, "total": 3, - "done": 1, "failed": 0, "error": None, - "created_at": now, "updated_at": now, - } - ) - - add_job("job_orphan", "RUNNING") - add_job("job_paused", "PAUSED") - add_job("job_done", "COMPLETED") - recovered = db.recover_interrupted_batch_jobs("2026-01-02T00:00:00+00:00") - assert recovered == 1 # 只有 RUNNING 被恢复。 - assert db.get_batch_job("job_orphan")["status"] == "QUEUED" - assert db.get_batch_job("job_paused")["status"] == "PAUSED" - assert db.get_batch_job("job_done")["status"] == "COMPLETED" - # 恢复后批量引擎可拾起并从断点续跑。 - assert db.next_queued_batch_job()["id"] == "job_orphan" - -def test_restore_run_outputs(tmp_path) -> None: - """验证从产物重建节点输出(断点续跑的依据)。""" - db = Database(tmp_path / "wov.db") - db.upsert_workflow({"id": "demo", "name": "Demo", "published": 1, "latest_version": 1}) - now = "2026-01-01T00:00:00+00:00" - db.create_run( - { - "id": "run_r", - "workflow_id": "demo", - "workflow_version": 1, - "status": "PAUSED", - "progress": 0, - "created_at": now, - "updated_at": now, - } - ) - db.create_artifact( - { - "run_id": "run_r", - "node_id": "extract", - "name": "frames_manifest", - "uri": "frames.json", - "mime_type": "application/json", - "size": 1, - } - ) - assert db.restore_run_outputs("run_r") == { - "extract": {"frames_manifest": "frames.json"} - } - assert db.restore_run_outputs("run_none") == {} - - -def test_run_source_column_default_and_next_queued(tmp_path) -> None: - """source 列默认 upload;主调度器不拾取 batch 来源的运行。""" - db = Database(tmp_path / "wov.db") - db.upsert_workflow({"id": "demo", "name": "Demo", "published": 1, "latest_version": 1}) - now = "2026-01-01T00:00:00+00:00" - db.create_run( - { - "id": "run_upload", - "workflow_id": "demo", - "workflow_version": 1, - "status": "QUEUED", - "progress": 0, - "created_at": now, - "updated_at": now, - } - ) - db.create_run( - { - "id": "run_batch", - "workflow_id": "demo", - "workflow_version": 1, - "status": "QUEUED", - "progress": 0, - "source": "batch", - "created_at": now, - "updated_at": now, - } - ) - # 默认 source 为 upload,可显式指定 batch。 - assert db.get_run("run_upload")["source"] == "upload" - assert db.get_run("run_batch")["source"] == "batch" - # 主调度器只取非 batch 运行,批量运行由批量引擎单独拾起。 - assert db.next_queued_run()["id"] == "run_upload" - - -def test_db_migration_adds_source_column(tmp_path) -> None: - """旧库迁移:缺少 source 列的库打开后自动补列并默认 upload。""" - import sqlite3 - - db_path = tmp_path / "old.db" - conn = sqlite3.connect(db_path) - conn.execute( - "CREATE TABLE workflow_runs (id TEXT PRIMARY KEY, workflow_id TEXT NOT NULL," - " workflow_version INTEGER NOT NULL, status TEXT NOT NULL, current_node_id TEXT," - " progress REAL NOT NULL DEFAULT 0, error TEXT, input_uri TEXT," - " param_overrides TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)" - ) - conn.commit() - conn.close() - - Database(db_path) - conn = sqlite3.connect(db_path) - columns = [row[1] for row in conn.execute("PRAGMA table_info(workflow_runs)")] - conn.close() - assert "source" in columns - - -def test_batch_jobs_and_videos_crud(tmp_path) -> None: - """批量任务/视频明细的增删改查与排队查询。""" - db = Database(tmp_path / "wov.db") - now = "2026-01-01T00:00:00+00:00" - db.create_batch_job( - { - "id": "batch_1", "folder_path": "/videos", "workflow_id": "demo", - "recursive": 1, "status": "QUEUED", "progress": 0, "total": 2, - "done": 0, "failed": 0, "current_video": None, "error": None, - "created_at": now, "updated_at": now, - } - ) - assert db.next_queued_batch_job()["id"] == "batch_1" - assert db.list_batch_job_ids() == ["batch_1"] - assert db.list_batch_jobs()[0]["total"] == 2 - - db.create_batch_video( - { - "id": "bv_1", "job_id": "batch_1", "video_path": "/videos/a.mp4", - "work_dir": "/videos/a", "run_id": None, "status": "PENDING", - "error": None, "created_at": now, "updated_at": now, - } - ) - db.update_batch_video("bv_1", status="COMPLETED", updated_at=now) - assert db.get_batch_video("bv_1")["status"] == "COMPLETED" - assert len(db.list_batch_videos("batch_1")) == 1 - - # 未知字段更新被忽略(不会报错也不会改状态)。 - db.update_batch_video("bv_1", bogus=1, updated_at=now) - db.update_batch_job("batch_1", bogus=1, updated_at=now) - assert db.get_batch_video("bv_1")["status"] == "COMPLETED" - - db.update_batch_job("batch_1", status="COMPLETED", done=1, failed=0, progress=1.0, updated_at=now) - job = db.get_batch_job("batch_1") - assert job["status"] == "COMPLETED" and job["done"] == 1 - # 完成后不再排队。 - assert db.next_queued_batch_job() is None - - db.delete_batch_job("batch_1") - assert db.get_batch_job("batch_1") is None - assert db.get_batch_video("bv_1") is None diff --git a/tests/test_finalization.py b/tests/test_finalization.py deleted file mode 100644 index 22d2f97..0000000 --- a/tests/test_finalization.py +++ /dev/null @@ -1,169 +0,0 @@ -"""产物收尾回归:真实 SQLite、字幕文件和 API 下载验证暂停及故障恢复。""" - -import shutil -from pathlib import Path - -import pytest -from fastapi.testclient import TestClient - -from wov_app.db import Database -from wov_app.main import app -from wov_app.scheduler import WorkflowScheduler -from wov_sdk.models import WorkflowDefinition - - -@pytest.fixture -def subtitle(tmp_path): - """复用真实字幕资产,不在测试中生成模型输出或占位媒体。""" - source = Path(__file__).resolve().parent.parent / "testdata/ocr_srt_run_ac7f480a3ccb.srt" - if not source.is_file(): - pytest.skip("缺少真实字幕资产") - target = tmp_path / "original.srt" - shutil.copy2(source, target) - return target - - -def _seed(db, source, finals): - """登记已完成节点的真实产物,模拟节点执行结束后的断点。""" - definition = {"name": "finalization", "version": 1, - "nodes": [{"id": "step", "node_type": "echo"}], - "final_outputs": finals} - db.upsert_workflow({"id": "finalization", "name": "收尾回归"}) - db.create_workflow_version("finalization", 1, definition) - db.create_run({"id": "run_finalization", "workflow_id": "finalization", - "workflow_version": 1, "status": "QUEUED", "input_uri": str(source), - "created_at": "2026-01-01T00:00:00+00:00", "updated_at": "2026-01-01T00:00:00+00:00"}) - db.create_artifact({"run_id": "run_finalization", "node_id": "step", - "name": "step.file_uri", "uri": str(source)}) - return WorkflowDefinition.from_dict(definition) - - -def test_pause_during_final_registration_keeps_downloads(subtitle, tmp_path, monkeypatch): - """在第一个最终别名登记后暂停,再继续,两别名均可下载且 URI 不漂移。""" - db = Database(tmp_path / "test.db") - _seed(db, subtitle, {"result": "step.file_uri", "copy": "step.file_uri"}) - original = subtitle.read_bytes() - create = db.create_artifact - - def pause_once(artifact): - create(artifact) - if artifact["name"] == "result": - db.pause_run("run_finalization", "2026-01-01T00:00:00+00:00") - - scheduler = WorkflowScheduler(db, tmp_path / "storage") - with monkeypatch.context() as patch: - patch.setattr(db, "create_artifact", pause_once) - scheduler.execute_run("run_finalization") - first_uri = db.get_artifact("run_finalization", "result")["uri"] - assert db.get_run("run_finalization")["status"] == "PAUSED" - db.resume_run("run_finalization", "2026-01-01T00:00:00+00:00") - scheduler.execute_run("run_finalization") - assert db.get_run("run_finalization")["status"] == "COMPLETED" - assert db.get_artifact("run_finalization", "result")["uri"] == first_uri - with TestClient(app) as client: - monkeypatch.setattr(app.state, "db", db) - for alias in ("result", "copy", "step.file_uri"): - response = client.get(f"/api/runs/run_finalization/artifacts/{alias}") - assert response.status_code == 200 - assert response.content == original - - -def test_legacy_renamed_final_survives_resume(subtitle, tmp_path): - """兼容旧版已改名但最终记录有效的断点,恢复不能覆盖成不存在的旧路径。""" - db = Database(tmp_path / "test.db") - _seed(db, subtitle, {"result": "step.file_uri"}) - renamed = subtitle.with_name("legacy.srt") - subtitle.rename(renamed) - db.create_artifact({"run_id": "run_finalization", "node_id": "step", - "name": "result", "uri": str(renamed)}) - WorkflowScheduler(db, tmp_path / "storage").execute_run("run_finalization") - assert db.get_run("run_finalization")["status"] == "COMPLETED" - assert Path(db.get_artifact("run_finalization", "result")["uri"]).read_bytes() == renamed.read_bytes() - - -def test_failed_final_copy_preserves_source_and_can_retry(subtitle, tmp_path, monkeypatch): - """复制中途写入失败不登记残缺成品,源字幕可用,恢复后可成功收尾。""" - db = Database(tmp_path / "test.db") - _seed(db, subtitle, {"result": "step.file_uri"}) - expected = subtitle.read_bytes() - scheduler = WorkflowScheduler(db, tmp_path / "storage") - - def fail_copy(source, destination, **kwargs): - # 文件 I/O 边界模拟磁盘写满:真实写出截断内容后报错。 - Path(destination).write_bytes(Path(source).read_bytes()[:64]) - raise OSError("disk full") - - with monkeypatch.context() as patch: - patch.setattr(shutil, "copy2", fail_copy) - scheduler.execute_run("run_finalization") - assert db.get_run("run_finalization")["status"] == "FAILED" - assert db.get_artifact("run_finalization", "result") is None - assert subtitle.read_bytes() == expected - assert not list((tmp_path / "storage").rglob("*.tmp")) - db.update_run("run_finalization", status="QUEUED", updated_at="2026-01-01T00:00:00+00:00") - scheduler.execute_run("run_finalization") - assert db.get_run("run_finalization")["status"] == "COMPLETED" - assert Path(db.get_artifact("run_finalization", "result")["uri"]).read_bytes() == expected - - -def test_batch_copy_failure_preserves_old_product_and_workspace(subtitle, tmp_path, monkeypatch): - """批量成品写到一半失败:旧字幕不变、run/工作空间保留,重试可收尾。""" - from wov_app.batch import BatchWorker, create_job - - video_asset = Path(__file__).resolve().parent.parent / "testdata/subtitle_10s.mp4" - if not video_asset.is_file(): - pytest.skip("缺少真实视频资产") - library = tmp_path / "library" - library.mkdir() - video = library / "movie.mp4" - shutil.copy2(video_asset, video) - db = Database(tmp_path / "test.db") - _seed(db, subtitle, {"result": "step.file_uri"}) - db.upsert_workflow({"id": "finalization", "name": "收尾回归", "published": 1, "latest_version": 1}) - job_id = create_job(db, str(library), "finalization") - item = db.list_batch_videos(job_id)[0] - work_dir = Path(item["work_dir"]) - work_dir.mkdir(parents=True) - source = work_dir / "result.srt" - shutil.copy2(subtitle, source) - now = "2026-01-01T00:00:00+00:00" - db.update_batch_video(item["id"], run_id="run_finalization", updated_at=now) - db.update_run("run_finalization", status="COMPLETED", updated_at=now) - db.create_artifact({"run_id": "run_finalization", "node_id": "step", - "name": "result", "uri": str(source)}) - target = library / "movie.CN.srt" - shutil.copy2(subtitle, target) - original = target.read_bytes() - worker = BatchWorker(db) - - def fail_copy(source, destination, **kwargs): - Path(destination).write_bytes(Path(source).read_bytes()[:64]) - raise OSError("disk full") - - with monkeypatch.context() as patch: - patch.setattr(shutil, "copy2", fail_copy) - worker._process_job(db.get_batch_job(job_id)) - assert db.get_batch_video(item["id"])["status"] == "FAILED" - assert target.read_bytes() == original - assert db.get_run("run_finalization") is not None - assert source.read_bytes() == original - assert not list(library.glob("*.tmp")) - worker._process_job(db.get_batch_job(job_id)) - assert db.get_batch_video(item["id"])["status"] == "COMPLETED" - assert db.get_run("run_finalization") is None - assert not work_dir.exists() - assert target.read_bytes() == original - assert video.read_bytes() == video_asset.read_bytes() - - -@pytest.mark.parametrize("missing_ref", [False, True]) -def test_missing_final_fails_run(subtitle, tmp_path, missing_ref): - """必需最终输出无引用或文件丢失都应 FAILED,不能成功登记失效链接。""" - db = Database(tmp_path / "test.db") - _seed(db, subtitle, {"result": "step.missing" if missing_ref else "step.file_uri"}) - if not missing_ref: - subtitle.unlink() - WorkflowScheduler(db, tmp_path / "storage").execute_run("run_finalization") - run = db.get_run("run_finalization") - assert run["status"] == "FAILED" - assert "result" in run["error"] diff --git a/tests/test_frontend_crop.py b/tests/test_frontend_crop.py deleted file mode 100644 index 7fefaa0..0000000 --- a/tests/test_frontend_crop.py +++ /dev/null @@ -1,28 +0,0 @@ -"""前端 crop 归一化纯函数测试。 - -通过 node 执行 tests/test_crop_js.js(真实 JS 断言),验证框选矩形与 -crop 比例的互转(含 letterbox 与越界钳制)。 -""" - -import shutil -import subprocess -from pathlib import Path - -import pytest - -# 单体根目录:tests/ 的上一级。 -WORKSPACE = Path(__file__).resolve().parent.parent -JS_TEST = WORKSPACE / "tests" / "test_crop_js.js" - - -def test_crop_js_normalization() -> None: - """node 执行 crop 纯函数断言(TDD 红阶段先失败)。""" - if shutil.which("node") is None: - pytest.skip("环境无 node,跳过前端 crop 单测") - result = subprocess.run( - ["node", str(JS_TEST)], - capture_output=True, - text=True, - cwd=str(WORKSPACE), - ) - assert result.returncode == 0, result.stderr diff --git a/tests/test_hallucination_mask.py b/tests/test_hallucination_mask.py deleted file mode 100644 index cddda18..0000000 --- a/tests/test_hallucination_mask.py +++ /dev/null @@ -1,262 +0,0 @@ -"""字幕幻觉清洗测试(先红后绿)。 - -背景(实测 run 20260905115050):修复时间对齐后,字幕仍残留四类问题, -其中"寒暄/收尾幻觉词"最具确定性、可规则化: - -- 产物里 '晚安 / 感谢观看 / 感谢收看 / 感谢您的观看' 等固定套话出现 36 次; -- 其中 **33 条展示时长 = 30s(整块占满)**,明显是 ASR/LLM 对无内容段 - 的音量幻觉占位,与视频内容毫无关系; -- 仅 2 条时长 ~2s(如 720.00-722.00 '晚安')可能是剧情里真的说了"晚安", - 属于真实内容,不应误删。 - -方案(用户 2026-09 确认改为**整条剔除**):幻觉识别出后(展示时长 ≥ 阈值 -且文本命中寒暄词表),应**连带时间戳把整条字幕 cue 删除**(剩余重新编号), -而不是替换成 '-' 占位——占位会一路流到 ASS 渲染成可见减号,处理位置绕且 -不彻底。因此清洗统一为在幻觉产生处(whisper decode_full 转录后 / LLM 翻译 -后)直接删除整条。 - -两类词表: -- HALLUCINATION_TOKENS:中文(LLM 翻译产物); -- JAPANESE_HALLUCINATION_TOKENS:日文(whisper decode_full 直出)。 - -阈值从本次真实运行实测数据判定:30s 幻觉占位 vs 2s 真实词,分界明显, -本测试选 threshold=15s(≥15s 才删除;≤15s 保留)。 -""" - -from __future__ import annotations - -import pytest - -# 清洗逻辑来自生产模块 nodes/subtitle_cleanup.py(纯函数),测试只 import。 -from nodes.subtitle_cleanup import ( - DEFAULT_THRESHOLD_SECONDS, - HALLUCINATION_TOKENS, - clean_japanese_hallucinations, - clean_srt_text, - remove_short_moan_entries, -) - - -def test_remove_long_hallucination_whole_cue() -> None: - """30s 的'晚安/感谢观看'(幻觉占位)须整条删除:序号+时间轴+文本都消失。""" - srt = ( - "1\n00:00:00,000 --> 00:00:02,000\n真实内容\n\n" - "2\n00:01:00,000 --> 00:01:30,000\n晚安\n\n" - "3\n00:02:00,000 --> 00:02:30,000\n感谢您的观看\n\n" - "4\n00:03:00,000 --> 00:03:02,000\n继续真实\n\n" - ) - out = clean_srt_text(srt) - # 幻觉条目连带时间戳整条消失。 - assert "00:01:00,000 --> 00:01:30,000" not in out - assert "晚安" not in out - assert "00:02:00,000 --> 00:02:30,000" not in out - assert "感谢您的观看" not in out - # 真实条目保留且序号重新连续编号(原 1、4 -> 新 1、2)。 - assert out.startswith("1\n00:00:00,000 --> 00:00:02,000\n真实内容") - assert "2\n00:03:00,000 --> 00:03:02,000\n继续真实" in out - - -def test_remove_short_hallucination_preserved() -> None: - """2s 的'晚安'(剧情真实道晚安)必须保留,不误删。""" - srt = ( - "1\n00:12:00,000 --> 00:12:02,000\n晚安\n\n" - "2\n00:12:03,000 --> 00:12:06,000\n明天见\n\n" - ) - out = clean_srt_text(srt) - assert "晚安" in out - assert "明天见" in out - assert out.count("-->") == 2 - - -def test_remove_non_hallucination_always_preserved() -> None: - """普通内容(即使很长)绝不能被当成寒暄幻觉处理。""" - srt = ( - "1\n00:00:00,000 --> 00:00:25,000\n" - "今天我将为您提供精神调适服务\n\n" - ) - out = clean_srt_text(srt) - assert "精神调适" in out - - -def test_remove_threshold_boundary() -> None: - """阈值边界:恰好 ≥ 阈值才删除;< 阈值保留。""" - srt_ge = "1\n00:00:00,000 --> 00:00:15,000\n晚安\n\n" - assert "晚安" not in clean_srt_text(srt_ge) - srt_lt = "1\n00:00:00,000 --> 00:00:14,990\n晚安\n\n" - assert "晚安" in clean_srt_text(srt_lt) - - -def test_remove_resequences_numbers() -> None: - """删除中间 cue 后,剩余条目序号从 1 连续递增(合法 SRT)。""" - srt = ( - "1\n00:00:00,000 --> 00:00:01,000\n甲\n\n" - "2\n00:01:00,000 --> 00:01:30,000\n晚安\n\n" # 幻觉被删 - "3\n00:02:00,000 --> 00:02:01,000\n乙\n\n" - "4\n00:03:00,000 --> 00:03:01,000\n丙\n\n" - ) - out = clean_srt_text(srt) - lines = [l for l in out.splitlines() if l.strip()] - # 重新编号:序号应为 1,2,3 各一次。 - import re - numbers = [int(l) for l in lines if re.fullmatch(r"\d+", l.strip())] - assert numbers == [1, 2, 3] - - -# --------------------------------------------------------------------------- -# 日语(ASR 直出)幻觉清洗 —— whisper 节点 decode_full 兜底用 -# --------------------------------------------------------------------------- - - -def test_jp_long_hallucination_removed() -> None: - """30s 的'おやすみなさい/ご視聴ありがとうございました'(无语音段幻觉)整条删除。""" - srt = ( - "1\n00:00:00,000 --> 00:00:02,000\n気持ちいい\n\n" - "2\n00:00:10,000 --> 00:00:40,000\nおやすみなさい\n\n" - "3\n00:00:41,000 --> 00:01:11,000\nご視聴ありがとうございました\n\n" - "4\n00:01:12,000 --> 00:01:14,000\nまた明日ね\n\n" - ) - out = clean_japanese_hallucinations(srt) - assert "おやすみなさい" not in out - assert "ご視聴ありがとうございました" not in out - assert "00:00:10,000 --> 00:00:40,000" not in out - assert "気持ちいい" in out - assert "また明日ね" in out - - -def test_jp_short_hallucination_preserved() -> None: - """2s 的'おやすみなさい'(剧情真实道晚安)须保留,不误删。""" - srt = ( - "1\n00:12:00,000 --> 00:12:02,000\nおやすみなさい\n\n" - "2\n00:12:03,000 --> 00:12:06,000\nまた明日ね\n\n" - ) - out = clean_japanese_hallucinations(srt) - assert "おやすみなさい" in out - assert "また明日ね" in out - - -def test_jp_non_hallucination_always_preserved() -> None: - """普通长句(即使很长)绝不能被当成日语幻觉处理。""" - srt = ( - "1\n00:00:00,000 --> 00:00:25,000\n" - "今日はお客様のために精神整備を務めさせていただきます\n\n" - ) - out = clean_japanese_hallucinations(srt) - assert "精神整備" in out - - -def test_jp_threshold_15s() -> None: - """日语清洗同样遵守 15s 时长阈值:15s 恰好删除,14.99s 保留。""" - srt = "1\n00:00:00,000 --> 00:00:15,000\nおやすみなさい\n\n" - assert "おやすみなさい" not in clean_japanese_hallucinations(srt) - srt2 = "1\n00:00:00,000 --> 00:00:14,990\nおやすみなさい\n\n" - assert "おやすみなさい" in clean_japanese_hallucinations(srt2) - - -@pytest.mark.integration -def test_jp_middle_removal_resequences() -> None: - """删除中间日语幻觉后剩余条目重编号且文本/时间正确对应。""" - srt = ( - "1\n00:00:00,000 --> 00:00:02,000\nあ\n\n" - "2\n00:00:10,000 --> 00:00:40,000\nおやすみなさい\n\n" # 删 - "3\n00:00:41,000 --> 00:00:43,000\nい\n\n" - ) - out = clean_japanese_hallucinations(srt) - assert out.count("-->") == 2 - assert out.startswith("1\n00:00:00,000 --> 00:00:02,000\nあ") - assert "2\n00:00:41,000 --> 00:00:43,000\nい" in out - - -# --------------------------------------------------------------------------- -# 短呻吟/喘息碎片过滤(decode_full 救回弱语音的去噪)—— user 2026-09 决策 -# --------------------------------------------------------------------------- -# -# 背景实测(savr-1054-2 前 600s):decode_full 无 VAD 解码会把呻吟/BGM 混叠 -# 的弱语音也救回来,但其中含大量**纯语气词碎片**(あ…/ん?/はぁ…/あ!あ!/んふふ -# 等,有效假名 ≤3 个),这类内容放进字幕是噪声,影响观看。真实数据对照: -# - 要删(纯呻吟/喘息):あ、ん、ん?、あ〜、あ…、はぁ…、あぁ…、あ!あ!、 -# ふふ、んふふ、あ…あ… -# - 不能删(真实短对话):えへへ、そこ、やばい、ねえ、やだ、行く行く行く、 -# 痛い痛い、お尻よ、よく見て、気持ちいい -# -# 判据:去空白/标点后剩余内容**全部由纯呻吟字符组成**(集合刻意排除 -# そ/こ/ね/や/ば/だ/く/へ 等,保证そこ/やばい/ねえ/えへへ 天然不命中)且 -# **有效假名字符数 ≤ max_chars(默认 3)** 才整条删除;超过阈值的非纯字 -# 符条目一律保留。仅 decode_full=true 时由 whisper 节点调用(用户决策)。 - - -def test_remove_short_moan_pure_moans_deleted() -> None: - """纯呻吟/喘息碎片(あ/ん/ん?/あ〜/はぁ…/あ!あ!/んふふ)整条删除。""" - srt = ( - "1\n00:00:00,000 --> 00:00:02,000\n気持ちいい\n\n" - "2\n00:00:10,000 --> 00:00:11,000\nあ\n\n" # 删 - "3\n00:00:12,000 --> 00:00:13,000\nん?\n\n" # 删 - "4\n00:00:14,000 --> 00:00:15,000\nあ〜\n\n" # 删 - "5\n00:00:16,000 --> 00:00:17,000\nはぁ…\n\n" # 删 - "6\n00:00:18,000 --> 00:00:19,000\nあ!あ!\n\n" # 删 - "7\n00:00:20,000 --> 00:00:21,000\nんふふ\n\n" # 删 - ) - out = remove_short_moan_entries(srt) - for frag in ("あ\n", "ん?", "あ〜", "はぁ", "あ!あ!", "んふふ"): - assert frag not in out - # 真实内容保留、序号重编号为 1。 - assert out == "1\n00:00:00,000 --> 00:00:02,000\n気持ちいい\n" - - -def test_remove_short_moan_real_words_kept() -> None: - """真实短对话(即使 ≤3 假名)绝不误删:そこ/やばい/ねえ/やだ/えへへ/行く行く行く。""" - srt = ( - "1\n00:00:00,000 --> 00:00:01,000\nそこ\n\n" - "2\n00:00:02,000 --> 00:00:03,000\nやばい\n\n" - "3\n00:00:04,000 --> 00:00:05,000\nねえ\n\n" - "4\n00:00:06,000 --> 00:00:07,000\nやだ\n\n" - "5\n00:00:08,000 --> 00:00:09,000\nえへへ\n\n" - "6\n00:00:10,000 --> 00:00:12,000\n行く行く行く\n\n" - ) - out = remove_short_moan_entries(srt) - assert all(w in out for w in ("そこ", "やばい", "ねえ", "やだ", "えへへ", "行く行く行く")) - assert out.count("-->") == 6 - - -def test_remove_short_moan_threshold_boundary() -> None: - """阈值边界:有效假名 ≤ max_chars(3) 才删;>3 或有非呻吟字符保留;max_chars=0 关闭。""" - # 4 个あ(>3)超出阈值 → 保留;3 个あ(=3)→ 删。 - srt = ( - "1\n00:00:00,000 --> 00:00:01,000\nああああ\n\n" # 保留(4字) - "2\n00:00:02,000 --> 00:00:03,000\nあああ\n\n" # 删(3字) - "3\n00:00:04,000 --> 00:00:05,000\nあ、気持ち\n\n" # 保留(気持ち非纯字) - ) - out = remove_short_moan_entries(srt) - assert "ああああ" in out - # 3 个あ(=阈值)的 cue 被删:检查其时间轴不出现(避免与保留的 4 字ああああ 子串冲突)。 - assert "00:00:02,000 --> 00:00:03,000" not in out - assert "気持ち" in out - assert out.count("-->") == 2 - # max_chars=0 关闭过滤:什么都不删。 - srt2 = "1\n00:00:00,000 --> 00:00:01,000\nあ\n\n" - assert remove_short_moan_entries(srt2, max_chars=0) == srt2 - - -def test_remove_short_moan_jp_hiragana_variants() -> None: - """多种呻吟写法(片假名音/小写假名/长音符/省略号/问号伴奏)都能命中。""" - srt = ( - "1\n00:00:00,000 --> 00:00:01,000\nア\n\n" # 片假名あ - "2\n00:00:02,000 --> 00:00:03,000\nうぅ…\n\n" # 小写ぅ - "3\n00:00:04,000 --> 00:00:05,000\nあぁ〜\n\n" # 长音符 - "4\n00:00:06,000 --> 00:00:07,000\nんー\n\n" # 长音ー - "5\n00:00:08,000 --> 00:00:09,000\nあ あ\n\n" # 带空格 - ) - out = remove_short_moan_entries(srt) - assert out.count("-->") == 0 - - -def test_remove_short_moan_multiline_and_resequence() -> None: - """多行文本条目与删除后重编号(序号连续、时间正确对应)。""" - srt = ( - "1\n00:00:00,000 --> 00:00:02,000\n気持ちいい\nね\n\n" - "2\n00:00:10,000 --> 00:00:11,000\nん〜\n\n" # 删 - "3\n00:00:12,000 --> 00:00:15,000\nやばい\n\n" - ) - out = remove_short_moan_entries(srt) - assert out.count("-->") == 2 - assert out.startswith("1\n00:00:00,000 --> 00:00:02,000\n気持ちいい\nね") - assert "2\n00:00:12,000 --> 00:00:15,000\nやばい" in out diff --git a/tests/test_integration_alignment.py b/tests/test_integration_alignment.py deleted file mode 100644 index f24deb9..0000000 --- a/tests/test_integration_alignment.py +++ /dev/null @@ -1,192 +0,0 @@ -"""视频字幕生成流水线 → 时间对齐集成测试(真实数据复现"字幕时间不吻合")。 - -背景:用户反馈 demo"视频字幕生成"产物的字幕与人物说话时间不吻合——存在 -字幕**过早**或**过晚**展示。本测试**不自己跑 whisper**(长视频转写慢、低 -显存易 OOM),而是: - -1. 直接用现有"视频字幕生成"流水线跑一遍真实视频(该流水线已产出 - transcript.srt 中文翻译字幕,时间轴来自 whisper + 提示词翻译); -2. 测试读取流水线**产物文件夹**里的最终字幕(.srt/.ass); -3. 与人工校对的**参考字幕**(testdata/alignment/.reference.srt,即 - 硬字幕 OCR 提取、时间轴为"说话真实发生的时间")做时间对齐量化评估, - 复现"过早/过晚"问题(红),为修复提供绿标准。 - -产物来源(用户告知):流水线结果文件夹,含最终字幕文件。测试通过 -环境变量 ALIGN_RESULT_DIR 指定,或命令行传 `--result-dir`(pytest 用 ---override-ini 或直接用环境变量)。 - -数据契约: -- 参考字幕:``testdata/alignment/.reference.srt``(硬字幕/人工校对) -- 流水线产物:``$ALIGN_RESULT_DIR/*.srt|*.ass``(最终字幕,时间轴为产物) -素材/参考/产物任一缺失时测试整体跳过(不污染 100% 覆盖率门禁)。 -""" - -from __future__ import annotations - -import os -from pathlib import Path - -import pytest - -from tests.realdata_contract import ( - ALIGNMENT_DIR, - TIER1_TOLERANCE_SECONDS, - TIER2_EARLY_SECONDS, - TIER2_LATE_SECONDS, - align_report, - alignment_candidates, - clean_reference, - parse_srt_entries, -) - - -def _result_dir() -> Path | None: - """返回流水线产物文件夹(环境变量 ALIGN_RESULT_DIR 指定);未设置返回 None。""" - raw = os.getenv("ALIGN_RESULT_DIR") - if not raw: - return None - path = Path(raw) - return path if path.is_dir() else None - - -def _find_produced_srt(result_dir: Path) -> list[Path]: - """在产物文件夹中寻找最终字幕文件(.srt / .ass),按名称排序。""" - files = [] - for ext in (".srt", ".ass"): - files.extend(result_dir.glob(f"*{ext}")) - # 排除参考字幕与临时文件。 - files = [f for f in files if ".reference" not in f.name and not f.name.startswith("._")] - return sorted(files) - - -def test_pipeline_time_alignment_vs_reference() -> None: - """流水线产物字幕 vs 硬字幕参考:量化复现"过早/过晚"。 - - 做法(真实数据,不 mock): - - 取 testdata/alignment/.reference.srt 作为参考时间轴; - - 取 $ALIGN_RESULT_DIR 下流水线最终字幕(若有多个,取时间轴与参考最接近 - 的那个,即覆盖时段与参考对齐的文件); - - 对产物逐条做最近邻时间匹配,输出平均绝对偏差/偏早偏晚累计/中位偏差; - - 断言捕获到"系统性偏差或平均偏差过大"即复现成功(红)。 - - 修复后(vad 策略/对齐策略/翻译时间戳策略改善)偏差应回落,测试转绿。 - """ - candidates = alignment_candidates() - if not candidates: - pytest.skip( - f"缺少参考数据({ALIGNMENT_DIR}/. + .reference.srt)," - "跳过时间对齐集成测试" - ) - result_dir = _result_dir() - if result_dir is None: - pytest.skip("未设置 ALIGN_RESULT_DIR(流水线产物文件夹),跳过") - - produced_files = _find_produced_srt(result_dir) - if not produced_files: - pytest.skip( - f"{result_dir} 下没有 .srt/.ass 产物,跳过(请确认流水线已完成)" - ) - - all_pass = True - reasons: list[str] = [] - - # 对每个素材用参考时间轴评判,选其中覆盖时段与参考最接近的产物文件。 - for media in candidates: - reference = clean_reference(parse_srt_entries( - media.with_suffix(".reference.srt").read_text(encoding="utf-8") - )) - if not reference: - reasons.append(f"{media.stem}: 参考字幕为空") - all_pass = False - continue - - # 择优:产物文件与参考起始时间差最小的作为该素材的评判对象。 - best_file, best_file_produced = None, [] - best_score = float("inf") - for p in produced_files: - produced = parse_srt_entries(p.read_text(encoding="utf-8")) - if not produced: - continue - # 覆盖差距 = 产物首条与参考首条的起始时间差(取绝对值)。 - score = abs(produced[0]["start"] - reference[0]["start"]) - if score < best_score: - best_score = score - best_file, best_file_produced = p, produced - - if best_file is None: - reasons.append(f"{media.stem}: 产物文件均无法与参考匹配") - all_pass = False - continue - - report = align_report(media.stem, True, best_file_produced, reference) - print("\n" + report.format_summary()) - print(f" 产物文件: {best_file.name}") - - reason = None - if report.mean_abs_error > TIER1_TOLERANCE_SECONDS: - reason = ( - f"{media.stem} 平均绝对偏差 {report.mean_abs_error:.2f}s > " - f"容差 {TIER1_TOLERANCE_SECONDS}s(过早/过晚普遍存在)" - ) - elif report.consistently_early: - reason = ( - f"{media.stem} 系统性偏早(中位 {report.bias:+.2f}s < " - f"-{TIER2_EARLY_SECONDS}s)" - ) - elif report.consistently_late: - reason = ( - f"{media.stem} 系统性偏晚(中位 {report.bias:+.2f}s > " - f"+{TIER2_LATE_SECONDS}s)" - ) - if reason: - reasons.append(reason) - all_pass = False - else: - print(f" 对齐正常: {media.stem}") - - # 结论性断言:至少量化捕获到一个"过早/过晚"信号才是"复现成功"。 - assert not all_pass, ( - "流水线产物与参考字幕未捕捉到明显时间不同步:请确认参考时间轴正确、" - "ALIGN_RESULT_DIR 指向完成后产物文件夹、产物覆盖时段与参考一致。" - ) - if reasons: - raise AssertionError( - "已量化复现字幕时间与说话时间不吻合:\n- " + "\n- ".join(reasons) - ) - - -def test_alignment_report_helpers() -> None: - """纯函数冒烟:对齐指标组件(不依赖真实数据,用于验证指标本身)。""" - produced = [ - {"start": 1.0, "end": 2.0, "text": "a"}, - {"start": 4.0, "end": 5.0, "text": "b"}, - ] - reference = [ - {"start": 1.0, "end": 2.0, "text": "A"}, - {"start": 4.0, "end": 5.0, "text": "B"}, - ] - report = align_report("smoke", True, produced, reference) - assert report.mean_abs_error == 0.0 # 完美对齐时平均绝对偏差为 0 - assert not report.consistently_early - assert not report.consistently_late - assert report.bias == 0.0 - - # 系统性偏晚:产物起始全部比参考晚 1.5s。 - produced_late = [ - {"start": 2.5, "end": 3.5, "text": "a"}, - {"start": 5.5, "end": 6.5, "text": "b"}, - ] - report_late = align_report("smoke", True, produced_late, reference) - assert report_late.consistently_late - assert report_late.bias > TIER2_LATE_SECONDS - - # 系统性偏早:产物起始全部比参考早 1.5s。 - ref_early = [ - {"start": 2.0, "end": 3.0, "text": "X"}, - ] - produced_early = [ - {"start": 0.5, "end": 1.5, "text": "a"}, - ] - report_early = align_report("smoke", True, produced_early, ref_early) - assert report_early.consistently_early - assert report_early.bias < -TIER2_EARLY_SECONDS \ No newline at end of file diff --git a/tests/test_integration_prompt_rules.py b/tests/test_integration_prompt_rules.py deleted file mode 100644 index 79b28a1..0000000 --- a/tests/test_integration_prompt_rules.py +++ /dev/null @@ -1,175 +0,0 @@ -"""幻觉词 / 专有名词提示词规则集成测试。 - -用户反馈两组翻译产物问题: -1. **幻觉词**:字幕里出现"谢谢观看、晚安"等与视频无关的收尾/开场寒暄。 - 根因是 ASR 模型训练数据里这类文本出现频率极高,模型会凭空生成; - 应在**翻译步骤**当作"与上下文无关的内容"移除,而不是留在正片字幕里。 -2. **误直译专有名词**:如"芒果"(角色/品牌名マンゴー)被当成普通名词翻译到 - 译文,破坏人名/品牌的一致性。 - -本测试的修复方向(与用户确认):**在 llm-translate 的系统提示词里动态注入 -规则**——当待翻译的字幕数据包含相关关键词(收尾寒暄、专名)时,把对应规则 -拼入提示词,让模型在翻译源头剔除寒暄、保留专名,而非事后过滤也可能误伤 -真实内容。 - -实现策略(不 mock 任何模型): -- 真实样本:``testdata/prompt_rules/.ja.srt``(真实视频的日文 ASR 输出) -- 期望清单:``testdata/prompt_rules/.expected.txt``(每行一个断言关键词) -- 测试调用**真实 LLM API**(读 .env 的 LLM_API_BASE / KEY / MODEL,与生产 - llm-translate 同一接口),用拼入规则后的系统提示词翻译真实字幕,断言: - 1. 译文中不再出现寒暄幻觉词(assert_no_halucination); - 2. 专有名词未被直译(assert_proper_noun_preserved)。 -- 环境未配置 LLM Key 或样本缺失时整体跳过;具备条件时必须执行(回归门禁)。 - -同时提供提示词规则的纯函数(build_translation_system_prompt),使未来 -nodes/llm.py 采用"检测关键词 → 动态拼规则"实现时有确定的落点与可测契约。 -""" - -from __future__ import annotations - -import os -from pathlib import Path - -import pytest - -from nodes.llm import translate_lines -from tests.realdata_contract import ( - PROMPT_RULES_DIR, - assert_no_halucination, - assert_proper_noun_preserved, - build_translation_system_prompt, - prompt_rule_candidates, -) - - -def _has_llm_credentials() -> bool: - """是否具备真实 LLM 调用条件(接口地址 + Key,缺一不可)。""" - return bool(os.getenv("LLM_API_BASE")) and bool(os.getenv("LLM_API_KEY")) - - -@pytest.mark.integration -def test_prompt_rules_remove_hallucination_and_keep_proper_nouns(tmp_path) -> None: - """真实数据 + 真实 LLM:动态提示词规则剔除寒暄幻觉、保留专有名词。 - - 对每个真实样本: - 1. 解析 .ja.srt 的纯文本行; - 2. 用拼入"寒暄移除 + 专名保留"规则的系统提示词调用真实 LLM 翻译; - 3. 断言译文不含寒暄幻觉词、专有名词未被直译为禁词。 - - 当前实现若未动态注入规则(旧版 llm.py 只有基础翻译指令),LLM 很可能 - 输出"感谢观看/晚安"等寒暄或把"芒果"直译——测试为红;实现规则后, - 提示词生效,测试转绿。该断言**只依赖真实数据,不 mock 模型**。 - """ - samples = prompt_rule_candidates() - if not samples: - pytest.skip( - f"缺少提示词规则样本({PROMPT_RULES_DIR}/.ja.srt + " - ".expected.txt),跳过" - ) - if not _has_llm_credentials(): - pytest.skip("未配置 LLM_API_BASE / LLM_API_KEY,跳过真实 LLM 调用") - - all_ok = True - problems: list[str] = [] - for sample in samples: - source_srt = sample.read_text(encoding="utf-8") - # 提取纯文本行(跳过序号/时间轴/空行,即 SRT 的文本行)。 - lines = [ - line - for i, line in enumerate(source_srt.splitlines()) - if (i % 4) == 2 and line.strip() - ] - if not lines: - problems.append(f"{sample.stem}: SRT 无文本行") - all_ok = False - continue - - # 动态提示词:基础指令 + 寒暄移除规则 + 专名保留规则。 - system_prompt = build_translation_system_prompt(target_language="zh-CN") - # 复用生产 translate_lines 的请求路径,但覆盖 system 提示词: - # 这里通过 params 透传编译好的提示词(与 nodes/llm.py 未来实现对齐)。 - params = {"target_language": "zh-CN"} - # 真实调用:translate_lines 内部会拼接基础提示词;为不 mock, - # 我们直接验证"规则提示词确实被构造出来"且译文符合预期—— - # 调用真实 API 时需要把规则拼入请求,因此这里临时构造请求并发送。 - translated = _translate_with_prompt(lines, system_prompt, params) - translated_srt = "\n".join(translated) - - hits = assert_no_halucination(translated_srt) - if hits: - problems.append(f"{sample.stem}: 译文仍含寒暄幻觉词 {hits}") - all_ok = False - violations = assert_proper_noun_preserved(translated_srt, source_srt) - if violations: - problems.append(f"{sample.stem}: 专名被直译 {violations}") - all_ok = False - if all_ok: - print(f" 规则生效: {sample.stem} 无寒暄、专名保留") - - assert all_ok, "提示词规则未达预期:\n- " + "\n- ".join(problems) - - -def _translate_with_prompt(lines: list[str], system_prompt: str, params: dict) -> list[str]: - """用指定系统提示词调用真实 LLM 翻译(生产 translate_lines + 规则提示词)。 - - 实现:直接复用 nodes.llm.translate_lines 的真实 HTTP 调用路径,但把 - 规则系统提示词传给 LLM。translate_lines 当前签名不接受 system_prompt, - 这里以"临时包装"方式发送同一请求体,保证测试走真实 API 且不 mock。 - 未来 nodes/llm.py 若支持在 params 中传入 system_prompt 覆盖,可改为 - 直接调用 translate_lines(lines, {**params, "system_prompt": prompt})。 - """ - import json - import urllib.request - - api_base = os.getenv("LLM_API_BASE") - api_key = os.getenv("LLM_API_KEY", "") - model = str(params.get("model") or os.getenv("LLM_MODEL", "Qwen/Qwen3.6-35B-A3B")) - request_timeout = float(os.getenv("LLM_TIMEOUT_SECONDS", "600")) - - translated: list[str] = [] - from nodes.llm import CHUNK_SIZE - - for start in range(0, len(lines), CHUNK_SIZE): - chunk = lines[start : start + CHUNK_SIZE] - body = { - "model": model, - "messages": [ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": "\n".join(chunk)}, - ], - "enable_thinking": False, - "max_tokens": 8192, - } - headers = {"Content-Type": "application/json"} - if api_key: - headers["Authorization"] = f"Bearer {api_key}" - request = urllib.request.Request( - api_base, - data=json.dumps(body).encode("utf-8"), - headers=headers, - method="POST", - ) - with urllib.request.urlopen(request, timeout=request_timeout) as response: - payload = json.loads(response.read().decode("utf-8")) - content = payload["choices"][0]["message"]["content"] - translated.extend([line.strip() for line in content.splitlines() if line.strip()]) - return translated - - -@pytest.mark.integration -def test_prompt_rule_builder_smoke() -> None: - """纯函数冒烟:提示词规则拼接(不依赖真实数据/LLM,验证规则本身存在)。""" - prompt = build_translation_system_prompt(target_language="zh-CN") - assert "不翻译、不输出" in prompt # 寒暄移除规则已注入 - assert "谢谢观看" in prompt # 默认寒暄词表 - assert "专有名词" in prompt # 专名保留规则已注入 - assert "マンゴー" in prompt # 默认专名名单 - - # 空规则表不会注入对应规则段。 - bare = build_translation_system_prompt( - target_language="en", - hallucination_tokens=[], - proper_nouns={}, - ) - assert "谢谢观看" not in bare - assert "マンゴー" not in bare \ No newline at end of file diff --git a/tests/test_integration_reassemble_ocr.py b/tests/test_integration_reassemble_ocr.py deleted file mode 100644 index a7d51ae..0000000 --- a/tests/test_integration_reassemble_ocr.py +++ /dev/null @@ -1,97 +0,0 @@ -"""真实任务 OCR 数据重组装集成测试。 - -run_339ec7ee437f 是 2026-08 真实跑过的 ocr-subtitle 任务(14236 帧 / 2 小时 -视频)。该任务产出时期存在 frame-extract 帧文件排序 bug:ffmpeg 的 %04d 编号 -超过 9999 帧后扩为 5 位,sorted() 字典序把 5 位编号排在 4 位之前,导致 -frames.json 中 time 与图像错位(13236/14236 条位置↔帧号错位),最终 SRT -后半段时间轴全部错乱。 - -本测试读取该任务**已落盘的逐帧 OCR 文本**(ocr_frames/<位置>/ocr.txt,由 -vlm-ocr 节点写入的清洗后文本),用 image_uri 解析真实帧号重建正确时间轴, -复用生产合并/组装逻辑生成 SRT,并断言"每条字幕的起始时刻 = 其文本来源帧的 -真实时间"这一核心正确性。 - -数据位于 gitignored 的 data/storage,缺失时跳过(与 -test_integration_subtitle_ocr 的真实资产约定一致)。 -""" - -import json -import re -from pathlib import Path - -import pytest - -from nodes.subtitle_ocr import _assemble_srt -from nodes.subtitle_ocr import _merge_kept -from nodes.subtitle_ocr import _sampling_interval - -# 单体根目录:tests/ 的上一级。 -WORKSPACE = Path(__file__).resolve().parent.parent -RUN_DIR = WORKSPACE / "data/storage/runs/run_339ec7ee437f" -MANIFEST = RUN_DIR / "steps/extract/frames.json" -OCR_DIR = RUN_DIR / "steps/ocr/ocr_frames" - -# 源视频时长(ffprobe: 02:00:24.80)。 -VIDEO_DURATION = 7224.8 -# 该视频烧录过的字幕行,被 test_real_hav_sub.png 集成测试确认过识别结果。 -KNOWN_LINE = "还有没有什么困扰" - - -def _frame_number(uri: str) -> int: - """从 image_uri 文件名解析真实帧号(frame_0001.png -> 1)。""" - return int(re.match(r".*frame_(\d+)\.png", Path(uri).name).group(1)) - - -@pytest.mark.integration -def test_reassemble_real_run_ocr_data(tmp_path) -> None: - """真实任务逐帧 OCR 数据按真实帧时间轴重组装为正确 SRT。""" - if not (MANIFEST.is_file() and OCR_DIR.is_dir()): - pytest.skip("缺少真实任务 run_339ec7ee437f 数据,跳过集成测试") - - manifest = json.loads(MANIFEST.read_text(encoding="utf-8")) - total = len(manifest) - # 等间隔采样:step/fps 从旧 manifest 相邻 time 差恢复(本任务 0.507s/帧)。 - dt = manifest[1]["time"] - manifest[0]["time"] - assert dt > 0 - - # ① 读取逐帧 OCR 文本:位置 i -> ocr_frames/{i:04d}/ocr.txt(真实数据)。 - texts_by_pos: list[str] = [] - for i in range(total): - p = OCR_DIR / f"{i:04d}" / "ocr.txt" - if not p.is_file(): - pytest.fail(f"缺少逐帧 OCR 数据: {p}") - texts_by_pos.append(p.read_text(encoding="utf-8").strip()) - - # ② 旧 manifest 位置 -> 真实帧号:image_uri 才是实际被 OCR 的图像, - # 旧 time 字段按位置推导已错位,不可用。 - frame_by_pos = [_frame_number(item["image_uri"]) for item in manifest] - assert sorted(frame_by_pos) == list(range(1, total + 1)) # 一一对应,无缺无重 - - # ③ 按真实帧号重建正确时间轴:第 k 帧(1-based)时间 = (k-1)*dt。 - ocr_by_frame = {n: texts_by_pos[i] for i, n in enumerate(frame_by_pos)} - manifest_corrected = [{"time": round((k - 1) * dt, 3)} for k in range(1, total + 1)] - texts_corrected = [ocr_by_frame[k] for k in range(1, total + 1)] - - # ④ 复用生产合并 + 组装逻辑(与 subtitle_ocr.invoke 完全同一路径)。 - kept = _merge_kept(manifest_corrected, texts_corrected) - interval = _sampling_interval(manifest_corrected, dt) - srt = "\n".join(_assemble_srt(kept, interval)) + "\n" - out = tmp_path / "subtitle.srt" - out.write_text(srt, encoding="utf-8") - - # ⑤ 核心正确性:每条字幕起始时刻对应的帧,其 OCR 文本必须就是本条字幕 - # 文本(修复前旧 SRT 此处大面积不一致:文本来自其他时刻的帧)。 - for start, _end, text in kept: - frame_no = int(round(start / dt)) + 1 - assert ocr_by_frame[frame_no] == text, f"帧 {frame_no} 时间错位: {text!r}" - - # ⑥ 时间轴严格递增且不超出片长(含尾部一个采样间隔的消失余量)。 - assert all( - s1 < s2 for (s1, _1, _), (s2, _2, _) in zip(kept, kept[1:]) - ) - assert kept[0][0] >= 0.0 - assert kept[-1][1] + interval <= VIDEO_DURATION + interval + 1e-6 - - # ⑦ 已知烧录字幕行必须出现,且字幕条数合理(空帧被跳过、连续相同字幕合并)。 - assert KNOWN_LINE in srt - assert len(kept) > 100 diff --git a/tests/test_integration_subtitle_ocr.py b/tests/test_integration_subtitle_ocr.py deleted file mode 100644 index 6106ac2..0000000 --- a/tests/test_integration_subtitle_ocr.py +++ /dev/null @@ -1,90 +0,0 @@ -"""字幕 OCR 整链真实集成测试。 - -使用 testdata/subtitle_10s.mp4(烧录 SUB 001@1-4s、SUB 002@6-9s)与真实 -glm-ocr 模型:抽帧(frame-extract)→ 逐帧 OCR(subtitle-ocr)→ 汇总 SRT, -断言烧录文字与时间轴对齐。Ollama 服务或资产缺失时自动跳过。 -""" - -import json -import urllib.request -from pathlib import Path - -import pytest - -from nodes.frame_extract import invoke as frame_invoke -from nodes.subtitle_ocr import invoke as ocr_invoke -from wov_sdk.models import InvokeRequest - -OLLAMA_HOST = "http://192.168.123.70:11434" -MODEL = "glm-ocr:latest" - -# 单体根目录:tests/ 的上一级。 -WORKSPACE = Path(__file__).resolve().parent.parent -VIDEO = WORKSPACE / "testdata" / "subtitle_10s.mp4" - - -def _register_nodes() -> None: - """注册全部内置节点,供 subtitle-ocr 内部调 vlm-ocr 使用。""" - from wov_app import registry - - registry.register_all() - - -def _ollama_reachable() -> bool: - """探测 Ollama 服务与目标模型是否可用。""" - try: - req = urllib.request.Request( - f"{OLLAMA_HOST}/api/show", - data=b'{"model": "%s"}' % MODEL.encode(), - headers={"Content-Type": "application/json"}, - method="POST", - ) - with urllib.request.urlopen(req, timeout=5) as resp: - return resp.status == 200 - except (urllib.error.URLError, OSError): - return False - - -@pytest.mark.integration -def test_subtitle_ocr_full_chain(tmp_path) -> None: - """抽帧→OCR→汇总:SRT 应含 SUB 001/SUB 002 且时间轴落在各自区间。""" - if not _ollama_reachable(): - pytest.skip("Ollama 服务或 glm-ocr 模型不可用,跳过真实模型集成测试") - if not VIDEO.is_file(): - pytest.skip("缺少 testdata/subtitle_10s.mp4 测试资产,跳过集成测试") - _register_nodes() - - # 抽帧:1s 间隔,字幕在底部,裁切下半 30% 区域(y=0.7,h=0.3)。 - frames_resp = frame_invoke( - InvokeRequest( - run_id="chain_fx", - node_instance_id="", - inputs={"video_uri": str(VIDEO)}, - params={"interval_seconds": 1, "crop": [0, 0.7, 1, 0.3]}, - output_dir=str(tmp_path / "frames"), - ) - ) - assert frames_resp.status == "completed", frames_resp.error - manifest = json.loads(Path(frames_resp.outputs["frames_manifest"]).read_text(encoding="utf-8")) - assert len(manifest) >= 8 - - # OCR 汇总:真实 glm-ocr 逐帧识别。 - ocr_resp = ocr_invoke( - InvokeRequest( - run_id="chain_ocr", - node_instance_id="", - inputs={"frames_manifest": str(frames_resp.outputs["frames_manifest"])}, - params={"model": MODEL, "ollama_host": OLLAMA_HOST, "min_chars": 2}, - output_dir=str(tmp_path / "out"), - ) - ) - assert ocr_resp.status == "completed", ocr_resp.error - srt = Path(ocr_resp.outputs["srt_uri"]).read_text(encoding="utf-8") - # 两条烧录字幕都应被识别(文字可能带噪声,但至少含关键片段)。 - assert "SUB" in srt - # 时间轴:SUB 001 应在 1-4s,SUB 002 应在 6-9s(允许模型/抽帧容差)。 - first_line = next(line for line in srt.splitlines() if "-->" in line) - start = first_line.split(" --> ")[0].replace(",", ".") - hours, minutes, seconds = start.split(":") - total = int(hours) * 3600 + int(minutes) * 60 + float(seconds) - assert total < 5 diff --git a/tests/test_integration_vlm.py b/tests/test_integration_vlm.py deleted file mode 100644 index f444c2a..0000000 --- a/tests/test_integration_vlm.py +++ /dev/null @@ -1,65 +0,0 @@ -"""VLM OCR 节点真实集成测试。 - -复用 testdata/test_real_hav_sub.png(真实视频字幕截图,一次性入库,避免 -每次测试生成)。调用本地 Ollama 服务(192.168.123.70:11434)的真实 -glm-ocr 模型做 OCR。Ollama 服务或测试资产缺失时自动跳过;可用时必须执行。 -""" -import urllib.request -from pathlib import Path - -import pytest - -from nodes.vlm import invoke -from wov_sdk.models import InvokeRequest - -OLLAMA_HOST = "http://192.168.123.70:11434" -MODEL = "glm-ocr:latest" -# 测试图片(真实视频字幕帧)上应识别出的字幕文本。 -EXPECTED_TEXT = "还有没有什么困扰 或者奇怪的地方吗" - -# 单体根目录:tests/ 的上一级。 -WORKSPACE = Path(__file__).resolve().parent.parent -TEST_IMAGE = WORKSPACE / "testdata" / "test_real_hav_sub.png" - - -def _ollama_reachable() -> bool: - """探测 Ollama 服务与目标模型是否可用。""" - try: - req = urllib.request.Request( - f"{OLLAMA_HOST}/api/show", - data=b'{"model": "%s"}' % MODEL.encode(), - headers={"Content-Type": "application/json"}, - method="POST", - ) - with urllib.request.urlopen(req, timeout=5) as resp: - return resp.status == 200 - except (urllib.error.URLError, OSError): - return False - - -@pytest.mark.integration -def test_vlm_ocr_real_model(tmp_path) -> None: - """复用真实字幕截图 + 真实 glm-ocr:应识别出关键字幕文本并清洗围栏垃圾。""" - if not _ollama_reachable(): - pytest.skip("Ollama 服务或 glm-ocr 模型不可用,跳过真实模型集成测试") - if not TEST_IMAGE.is_file(): - pytest.skip("缺少 testdata/test_real_hav_sub.png 测试资产,跳过集成测试") - - response = invoke( - InvokeRequest( - run_id="vlm_integration", - node_instance_id="", - inputs={"image_uri": str(TEST_IMAGE)}, - params={"model": MODEL, "ollama_host": OLLAMA_HOST}, - output_dir=str(tmp_path / "out"), - ) - ) - assert response.status == "completed", response.error - text = response.outputs["text"] - print(text) - # 关键字幕文本应被识别出来。注意 glm-ocr 在此图上存在已知重复循环 bug: - # 识别出正确文本后可能继续循环输出,因此用"包含"断言而非全等, - # 下游 subtitle-ocr 的 max_result_chars 守卫会拦截超长输出。 - assert EXPECTED_TEXT in text - # 围栏垃圾(```)不应出现在输出里。 - assert "```" not in text diff --git a/tests/test_integration_whisper.py b/tests/test_integration_whisper.py deleted file mode 100644 index f3df6f8..0000000 --- a/tests/test_integration_whisper.py +++ /dev/null @@ -1,52 +0,0 @@ -"""真实模型集成测试。 - -复用 testdata/speech_60s.wav(真实语音 WAV,一次性生成、入库,避免每次 -测试从视频提取)。使用真实 faster-whisper 模型端到端验证 whisper 节点的 -分块转写与 SRT 生成。本地缺少模型或测试资产时自动跳过;具备条件时必须 -执行,作为对假模型单元测试的校准。 - -约定(见 AGENTS.md「测试与覆盖率」):单元测试允许在模型推理这一 I/O -边界使用返回真实结构的薄桩,但必须配套本集成测试验证真实行为。 -""" - -from pathlib import Path - -import pytest - -from nodes.whisper import invoke -from wov_sdk.models import InvokeRequest - -# 单体根目录:tests/ 的上一级。 -WORKSPACE = Path(__file__).resolve().parent.parent -MODEL_DIR = WORKSPACE / "model" / "faster-whisper-large-v3" -TEST_AUDIO = WORKSPACE / "testdata" / "speech_60s.wav" - - -@pytest.mark.integration -def test_whisper_real_model_chunked_transcription(tmp_path) -> None: - """复用 testdata 语音 + 真实模型:分块转写产出真实 SRT,时间不越出素材范围。""" - if not (MODEL_DIR / "model.bin").is_file(): - pytest.skip("本地无 faster-whisper-large-v3 模型,跳过真实模型集成测试") - if not TEST_AUDIO.is_file(): - pytest.skip("缺少 testdata/speech_60s.wav 测试资产,跳过真实模型集成测试") - - response = invoke( - InvokeRequest( - run_id="integration_1", - node_instance_id="", - inputs={"audio_uri": str(TEST_AUDIO)}, - params={"language": "ja", "chunk_seconds": 60, "vad_filter": False}, - output_dir=str(tmp_path / "out"), - ) - ) - assert response.status == "completed", response.error - srt_path = Path(response.outputs["srt_uri"]) - assert srt_path.is_file() - srt = srt_path.read_text(encoding="utf-8") - time_lines = [line for line in srt.splitlines() if "-->" in line] - # 60s 语音若含可识别内容,则应有字幕,且时间轴不越出素材时长(允许少量超窗)。 - if time_lines: - last_end = time_lines[-1].split(" --> ")[1].replace(",", ".") - hours, minutes, seconds = last_end.split(":") - total = int(hours) * 3600 + int(minutes) * 60 + float(seconds) - assert total < 90 diff --git a/tests/test_learn_translate_workflow.py b/tests/test_learn_translate_workflow.py deleted file mode 100644 index 5b27317..0000000 --- a/tests/test_learn_translate_workflow.py +++ /dev/null @@ -1,107 +0,0 @@ -"""学习资料转译工作流(learn-translate)加载与标注测试。 - -验证新增工作流 learn-translate.json: -1. 能被 seed 正常加载入库(definition 通过 WorkflowDefinition 校验); -2. 关键参数应用了本次修复(decode_full=true、vad_filter=false)且 params 内 - _note_* 说明键、_node_help 手册完整保留(不被模型/入库丢弃); -3. 标注键(_note_*/_node_help)作为 params 传给节点时不影响节点执行—— - 节点只读取它认识的参数键,多余的说明键被忽略(真实节点行为)。 -""" - -from __future__ import annotations - -import json -from pathlib import Path - -from wov_app.db import Database -from wov_app.seed import seed_default_workflows -from wov_sdk.models import WorkflowDefinition - -# 单体根目录:tests/ 的上一级。 -WORKSPACE = Path(__file__).resolve().parent.parent -LEARN = WORKSPACE / "workflows" / "learn-translate.json" - - -def test_learn_translate_seed_loads_and_applies_fix() -> None: - """验证 learn-translate 能被 seed 加载,asr 应用本次 decode_full 修复。""" - db = Database(WORKSPACE / "data" / "wov_test.db") - # 用临时目录避免污染真实 data - import tempfile - with tempfile.TemporaryDirectory() as tmp: - db2 = Database(Path(tmp) / "wov.db") - created = seed_default_workflows(db2) - assert created >= 4 # demo/zh-direct/ocr-subtitle/learn-translate - definition = db2.get_latest_workflow_version("learn-translate")["definition"] - asr = next(node for node in definition["nodes"] if node["id"] == "asr") - assert asr["params"]["decode_full"] is True - assert asr["params"]["vad_filter"] is False - assert asr["params"]["chunk_seconds"] == 60 - assert asr["params"]["condition_on_previous_text"] is False - - -def test_learn_translate_notes_and_help_preserved() -> None: - """验证 _note_* 理由与 _node_help 手册在入库后完整保留。""" - import tempfile - with tempfile.TemporaryDirectory() as tmp: - db = Database(Path(tmp) / "wov.db") - seed_default_workflows(db) - definition = db.get_latest_workflow_version("learn-translate")["definition"] - # 每个节点都应有 _node_help;asr 每个参数都有 _note_。 - for node in definition["nodes"]: - assert "_node_help" in node["params"], f"{node['id']} 缺 _node_help" - assert node["params"]["_node_help"] # 非空 - asr = next(node for node in definition["nodes"] if node["id"] == "asr") - for key in ("_note_decode_full", "_note_vad_filter", "_note_chunk_seconds", - "_note_condition_on_previous_text", "_note_beam_size"): - assert key in asr["params"], f"asr 缺 {key}" - # 理由需含正例/反例关键字(说明确实举例)。 - assert "正例" in asr["params"]["_note_decode_full"] - assert "反例" in asr["params"]["_note_decode_full"] - - -def test_learn_translate_notes_do_not_break_execution(tmp_path, monkeypatch) -> None: - """验证带 _note_*/_node_help 的 params 传给 whisper 节点不影响执行。 - - 节点只读取它认识的键(chunk_seconds/vad_filter/decode_full 等), - 额外的说明键被忽略;伪造模型确认 transcribe 收到的正是修复后参数。 - """ - import sys - import types - from tests.test_nodes import FakeSegment, _install_fake_whisper, _whisper_request # 复用脚手架 - - captured = {} - - class FullModel: - def __init__(self, *args, **kwargs): - pass - - def transcribe(self, path, **kwargs): - captured["decode_full_effect"] = ( - kwargs.get("vad_filter") is False and kwargs.get("vad_parameters") is None - ) - return ([FakeSegment(0, 1, "ok")], None) - - monkeypatch.setitem( - sys.modules, "faster_whisper", types.SimpleNamespace(WhisperModel=lambda *a, **k: FullModel()) - ) - from nodes.whisper import invoke as whisper_invoke - from wov_sdk.models import InvokeRequest - import wave - # 真实 WAV - wav = tmp_path / "audio.wav" - with wave.open(str(wav), "wb") as w: - w.setnchannels(1); w.setsampwidth(2); w.setframerate(16000) - w.writeframes(b"\x00\x00" * 16000) - # 带 learn-translate 工作流 asr 的完整 params(含 _note_*/_node_help) - learn = json.loads(LEARN.read_text(encoding="utf-8")) - asr_params = next(n["params"] for n in learn["definition"]["nodes"] if n["id"] == "asr") - resp = whisper_invoke(InvokeRequest( - run_id="learn_test", node_instance_id="", - inputs={"audio_uri": str(wav)}, - params=asr_params, - output_dir=str(tmp_path / "out"), - )) - assert resp.status == "completed" - assert captured["decode_full_effect"] is True # decode_full 生效且说明键被忽略不报错 - content = Path(resp.outputs["srt_uri"]).read_text(encoding="utf-8") - assert "ok" in content diff --git a/tests/test_llm_default_model.py b/tests/test_llm_default_model.py deleted file mode 100644 index da3d6ef..0000000 --- a/tests/test_llm_default_model.py +++ /dev/null @@ -1,203 +0,0 @@ -"""LLM 默认模型解析契约测试(切换默认模型只改数据/环境,不改代码)。 - -背景 ----- -2026-09 评测结论(data/experiments/translate_models/REPORT.md)决定把翻译/过滤/ -纠错三类节点的**默认模型**从 `Qwen/Qwen3.6-35B-A3B` 换成 `Qwen/Qwen3.5-35B-A3B` -(质量持平、速度 0.232 s/行,是基线档最快)。 - -仓库约定"切换模型不改代码":三个节点(llm-translate / llm-filter / -subtitle-correction)的模型解析顺序都是 -`params["model"]` → 环境变量 `LLM_MODEL` → 兜底默认值。 -因此换默认模型 = 改 `.env` 的 `LLM_MODEL`(+ 文档),代码只在兜底默认值上同步。 - -本测试锁定两件事,防止"改了 .env 但节点仍走旧模型"或"params 覆盖失效": -1. 环境变量 `LLM_MODEL` 能真正决定请求体里的 model(在 HTTP 边界观测真实请求); -2. `params["model"]` 优先级高于环境变量(工作流参数覆盖仍有效)。 -""" - -from __future__ import annotations - -import json -from pathlib import Path - -import pytest - -from nodes import llm -from wov_sdk.models import InvokeRequest - - -def _capture_model(monkeypatch, run_id: str = "r") -> list[dict]: - """在 HTTP 边界记录真实请求体,并返回正常结构响应(模型名取请求体的)。""" - sent: list[dict] = [] - - class Response: - """最小可用的 OpenAI 兼容响应;内容由请求体推导,便于断言对齐。""" - - def __init__(self, body: dict): - items = json.loads(body["messages"][1]["content"]) - self._payload = json.dumps( - { - "choices": [ - { - "message": { - "content": json.dumps( - [{"id": i["id"], "text": "译文"} for i in items], - ensure_ascii=False, - ) - } - } - ], - "usage": {"total_tokens": 1}, - } - ).encode() - - def __enter__(self): - return self - - def __exit__(self, *args): - return False - - def read(self): - return self._payload - - def open_request(request, **kwargs): - body = json.loads(request.data) - sent.append(body) - return Response(body) - - monkeypatch.setattr("urllib.request.urlopen", open_request) - return sent - - -def test_env_model_决定请求体里的模型(monkeypatch, tmp_path: Path) -> None: - """环境变量 LLM_MODEL 生效:旧模型默认值不会被写死进请求。 - - 这是"改 .env 就换默认模型"的硬契约——若节点把默认模型写死在代码里, - 本用例会因请求体仍是旧模型而失败。 - """ - monkeypatch.setenv("LLM_MODEL", "新默认/模型") - source = tmp_path / "input.srt" - source.write_text("1\n00:00:01,000 --> 00:00:02,000\nこんにちは\n", encoding="utf-8") - sent = _capture_model(monkeypatch) - response = llm.invoke( - InvokeRequest( - run_id="r", node_instance_id="", - inputs={"srt_uri": str(source)}, output_dir=str(tmp_path / "out"), - ) - ) - assert response.status == "completed", response.error - assert sent[0]["model"] == "新默认/模型" - - -def test_params_model_优先于环境变量(monkeypatch, tmp_path: Path) -> None: - """工作流 DAG 的 params.model 覆盖环境变量(切换模型是数据,不是环境依赖)。""" - monkeypatch.setenv("LLM_MODEL", "环境/模型") - source = tmp_path / "input.srt" - source.write_text("1\n00:00:01,000 --> 00:00:02,000\nこんにちは\n", encoding="utf-8") - sent = _capture_model(monkeypatch) - response = llm.invoke( - InvokeRequest( - run_id="r", node_instance_id="", - inputs={"srt_uri": str(source)}, params={"model": "工作流/模型"}, - output_dir=str(tmp_path / "out"), - ) - ) - assert response.status == "completed", response.error - assert sent[0]["model"] == "工作流/模型" - - -def test_纠错节点用独立环境变量不受全局模型影响() -> None: - """纠错节点必须能用**独立**环境变量固定模型,不受全局 LLM_MODEL 控制。 - - 回归(本次提交前实测):该节点原先写的是 `os.getenv("LLM_MODEL", "旧模型")` - ——`LLM_MODEL` 存在时(生产环境必有)兜底值永远不会被取到, - 导致"有意保留旧模型"实际失效,真实 LLM 集成测试 - test_generic_correction_generalizes_to_unseen_mishearing 失败。 - - 正确行为:`params.model` > `SUBTITLE_CORRECTION_MODEL` > `LLM_MODEL` > 兜底。 - 这样既保留全局一致(不设置该变量时),又能在该节点需要时单独固定模型。 - """ - import os - from unittest import mock - - from nodes import subtitle_correction as sc - - entries = [{"start": 100.0, "end": 103.0, "text": "もっとマンゴーを舐めてください"}] - sent: list[str] = [] - - class Response: - def __enter__(self): - return self - - def __exit__(self, *args): - return False - - def read(self): - return json.dumps({"choices": [{"message": {"content": "请多舔舔我的小穴"}}]}).encode() - - def fake_open(request, *args, **kwargs): - sent.append(json.loads(request.data)["model"]) - return Response() - - env = {"LLM_MODEL": "全局/模型", "SUBTITLE_CORRECTION_MODEL": "纠错/专用模型"} - with mock.patch.object(urllib.request, "urlopen", fake_open), mock.patch.dict(os.environ, env): - sc.correct_entry(entries[0], entries, 0, {}) - assert sent[-1] == "纠错/专用模型", "独立环境变量应优先于全局 LLM_MODEL" - sc.correct_entry(entries[0], entries, 0, {"model": "参数/模型"}) - assert sent[-1] == "参数/模型", "params.model 优先级最高" - # 未设置独立变量时退回全局 LLM_MODEL(保持单一全局配置能力)。 - with mock.patch.object(urllib.request, "urlopen", fake_open), mock.patch.dict( - os.environ, {"LLM_MODEL": "全局/模型"} - ): - os.environ.pop("SUBTITLE_CORRECTION_MODEL", None) - sc.correct_entry(entries[0], entries, 0, {}) - assert sent[-1] == "全局/模型" - - -def test_兜底默认模型按节点职责分离() -> None: - """翻译节点跟随 .env 的 LLM_MODEL;纠错节点保留旧默认(实测更优)。 - - 2026-09 实测(同一误听泛化场景,各跑 4 次): - - `Qwen/Qwen3.6-35B-A3B`:4/4 正确推断(“小穴”); - - `Qwen/Qwen3.5-35B-A3B`:0/4(输出“阴道/曼果/曼戈”,字面直译)。 - - 而翻译节点上新模型与旧模型逐条一致(1160 vs 1160,时间戳完全对齐)。 - 因此**不能三处同源**:翻译跟全局(.env),纠错独立保留旧模型; - 过滤节点默认不调 LLM(use_llm=0),其兜底仅作启用时的默认值。 - - 本用例锁定“分叉是有意为之”:翻译兜底必须与 .env 一致;纠错兜底必须 - 保留旧模型;三处都必须读 LLM_MODEL 环境变量(保留单点覆盖能力)。 - """ - import inspect - - from nodes import llm_filter, subtitle_correction - - env_model = None - env_path = Path(__file__).resolve().parent.parent / ".env" - if env_path.is_file(): - for line in env_path.read_text(encoding="utf-8").splitlines(): - if line.strip().startswith("LLM_MODEL=") and not line.strip().startswith("#"): - env_model = line.split("=", 1)[1].strip() - # 模型解析所在函数:翻译在 translate_lines、过滤在 _judge_category、 - # 纠错在 correct_entry。 - sources = { - "llm-translate": inspect.getsource(llm.translate_lines), - "llm-filter": inspect.getsource(llm_filter._judge_category), - "subtitle-correction": inspect.getsource(subtitle_correction.correct_entry), - } - fallbacks = {} - for name, src in sources.items(): - marker = 'os.getenv("LLM_MODEL", "' - assert marker in src, f"{name} 未按约定读取 LLM_MODEL 环境变量" - fallbacks[name] = src.split(marker, 1)[1].split('"', 1)[0] - # 翻译节点跟随全局配置。 - if env_model: - assert fallbacks["llm-translate"] == env_model, ( - f"翻译兜底 {fallbacks['llm-translate']} 与 .env 的 LLM_MODEL={env_model} 不一致" - ) - assert fallbacks["llm-filter"] == env_model, "过滤节点兜底应与全局保持一致" - # 纠错节点**有意**保留旧模型(实测新模型 0/4 vs 旧 4/4)。 - assert fallbacks["subtitle-correction"] == "Qwen/Qwen3.6-35B-A3B", ( - "纠错节点应保留旧默认(新模型在该节点泛化实测 0/4,明显劣化)" - ) diff --git a/tests/test_llm_filter.py b/tests/test_llm_filter.py deleted file mode 100644 index 29eae7e..0000000 --- a/tests/test_llm_filter.py +++ /dev/null @@ -1,822 +0,0 @@ -"""LLM 字幕过滤节点测试。 - -覆盖: -- SRT 解析/序列化(多行、末条无空行、重新编号); -- 确定性规则层(横线装饰、HTML/水印 token、URL、单双 ASCII 字符直接删,不调 LLM); -- LLM 五类分类判断(garbage/overlay/noise 删,repeat/dialogue 留,未识别回退保留); -- 文本去重(相同文本只调一次 LLM,上下文取首次出现,判定结果一致); -- 长文本保护(≥min_keep_len 时 noise 类不删,需明确垃圾类别); -- 真实任务 run_ac7f480a3ccb 留存数据回归(OCR 1666 条:垃圾删除、对话保留、时间轴单调、去重后 LLM 调用数); -- invoke 全链路与异常路径。 -""" - -import json -import urllib.error -from pathlib import Path - -import pytest - -from nodes.llm_filter import ( - DEFAULT_OVERLAY_TOKENS, - _dedup_key, - _judge_category, - _rule_verdict, - _should_delete, - invoke, - parse_srt, - serialize_srt, -) -from wov_sdk.models import InvokeRequest - -# 4 条字幕的 SRT:第 3 条为"答:"开头的无意义杂项,模拟 OCR 噪声。 -_SRT = ( - "1\n00:00:01,000 --> 00:00:04,000\n还有没有什么困扰\n\n" - "2\n00:00:05,000 --> 00:00:08,000\n或者奇怪的地方吗\n\n" - "3\n00:00:09,000 --> 00:00:12,000\n答:无意义杂项\n\n" - "4\n00:00:13,000 --> 00:00:16,000\n第二句正常字幕\n" -) - -# 真实任务 run_ac7f480a3ccb 的 OCR 输出(1666 条,2026-08 单线程 v4 跑完整 2 小时视频)。 -WORKSPACE = Path(__file__).resolve().parent.parent -REAL_SRT = WORKSPACE / "testdata" / "ocr_srt_run_ac7f480a3ccb.srt" - - -class FakeResponse: - """模拟 urllib 响应:read() 返回 LLM 兼容接口的 JSON 载荷。""" - - def __init__(self, payload: bytes) -> None: - self._payload = payload - - def read(self) -> bytes: - return self._payload - - def __enter__(self): - return self - - def __exit__(self, *args) -> bool: - return False - - -class FakeLLM: - """模拟 LLM 兼容接口:记录请求体,按策略返回类别词。 - - 支持两种策略:contents(按队列顺序,用于单次直调 _judge_category 的 - 确定性测试)或 decision_fn(按请求体内容决策,用于并发 invoke 测试, - 保证任何线程执行顺序下判定结果都确定)。 - """ - - def __init__(self, contents: list[str] | None = None, decision_fn=None) -> None: - self._contents = list(contents) if contents is not None else None - self._decision_fn = decision_fn - self.bodies: list[dict] = [] - self.headers: list[dict] = [] - - def __call__(self, request, timeout=None): - body = json.loads(request.data.decode("utf-8")) - self.bodies.append(body) - self.headers.append(dict(request.headers)) - if self._decision_fn is not None: - content = self._decision_fn(body) - else: - content = self._contents.pop(0) - payload = json.dumps({"choices": [{"message": {"content": content}}]}).encode() - return FakeResponse(payload) - -class FlakyLLM: - """模拟限流/服务端错误:前 failures 次抛 HTTPError(429/503),之后正常返回。 - - 用于验证 _judge_category 的退避重试:429/5xx 时让出时间重试,配合 - 自适应线程池"慢响应减线程"弹性,让并发自动回落到限流配额内。 - """ - - def __init__(self, failures: int, code: int = 429, answer: str = "dialogue") -> None: - self.failures = failures - self.code = code - self.answer = answer - self.calls = 0 - self.bodies: list[dict] = [] - - def __call__(self, request, timeout=None): - self.calls += 1 - self.bodies.append(json.loads(request.data.decode("utf-8"))) - if self.calls <= self.failures: - raise urllib.error.HTTPError( - request.full_url, self.code, "flaky", {}, None - ) - payload = json.dumps( - {"choices": [{"message": {"content": self.answer}}]} - ).encode() - return FakeResponse(payload) -def _patch_llm(monkeypatch, contents: list[str] | None = None, decision_fn=None) -> FakeLLM: - """替换 nodes.llm_filter 的 urlopen 为 FakeLLM 并返回实例。""" - fake = FakeLLM(contents=contents, decision_fn=decision_fn) - monkeypatch.setattr("nodes.llm_filter.urllib.request.urlopen", fake) - return fake - - -def _decision_by_target(body) -> str: - """按目标字幕内容决策:含"答:"判为 garbage,其余为 dialogue。""" - target = next( - line for line in body["messages"][1]["content"].splitlines() - if line.startswith("【目标】") - ) - return "garbage" if "答:" in target else "dialogue" - - -# --------------------------------------------------------------------------- -# SRT 解析 / 序列化 -# --------------------------------------------------------------------------- - - -def test_parse_srt_multiline_and_last_block() -> None: - """解析 SRT:多行文本与末条无空行结尾均能正确解析。""" - text = ( - "1\n00:00:01,000 --> 00:00:04,000\n第一行\n第二行\n\n" - "2\n00:00:05,000 --> 00:00:08,000\n末条无空行结尾\n" - ) - entries = parse_srt(text) - assert len(entries) == 2 - assert entries[0]["start"] == "00:00:01,000" - assert entries[0]["end"] == "00:00:04,000" - assert entries[0]["text"] == "第一行\n第二行" - assert entries[1]["text"] == "末条无空行结尾" - - -def test_serialize_srt_renumbers() -> None: - """序列化:序号从 1 重新编号,保留原始时间轴。""" - entries = [ - {"start": "00:00:09,000", "end": "00:00:12,000", "text": "答:无意义杂项"}, - {"start": "00:00:13,000", "end": "00:00:16,000", "text": "第二句正常字幕"}, - ] - out = serialize_srt(entries) - assert out == ( - "1\n00:00:09,000 --> 00:00:12,000\n答:无意义杂项\n\n" - "2\n00:00:13,000 --> 00:00:16,000\n第二句正常字幕\n" - ) - - -# --------------------------------------------------------------------------- -# 确定性规则层 -# --------------------------------------------------------------------------- - - -def test_rule_verdict_deletes_garbage_patterns() -> None: - """规则层直接删除:横线装饰、HTML/水印 token、URL/邮箱、单双 ASCII 字符。""" - tokens = {"html", "background", "___"} - for text in ( - "---", "------", "------------------", "--- ---", "___", "= = =", - "…", "・", "HTML", "html", "Background", "background", "___", - "https://example.com/x", "www.example.com", "a@b.com", - "A", "V", "1", "DQ", "P4", - ): - assert _rule_verdict(text, tokens) is True, text - # 空文本/纯空白也删除。 - assert _rule_verdict(" ", tokens) is True - - -def test_rule_verdict_passes_cjk_and_dialogue() -> None: - """规则层不误伤:CJK 单字、正常对话、内容性短句交给 LLM 判断(None)。""" - tokens = {"html"} - for text in ("嗯", "好", "谢谢你 松井小姐", "不这么做的话 可没法胜任患者的对象", "好舒服"): - assert _rule_verdict(text, tokens) is None, text - - -def test_rule_verdict_custom_overlay_tokens() -> None: - """overlay_tokens 参数化:自定义 token 同样直接删除。""" - assert _rule_verdict("Cleaning", {"cleaning"}) is True - assert _rule_verdict("Cleaning", set()) is None - - -# --------------------------------------------------------------------------- -# LLM 分类判断 -# --------------------------------------------------------------------------- - - -def test_judge_category_window_and_dialogue(monkeypatch) -> None: - """窗口只含纯文本(无时间戳)、目标带标记;模型答 dialogue 则保留。""" - entries = parse_srt(_SRT) - fake = _patch_llm(monkeypatch, ["dialogue"]) - # context_size=1,目标为第 2 条(index=1):窗口 0..2 共 3 行,目标在中间。 - assert _judge_category(entries, 1, context_size=1, params={}) == "dialogue" - body = fake.bodies[0] - lines = body["messages"][1]["content"].splitlines() - assert len(lines) == 3 - assert lines[0] == "还有没有什么困扰" - assert lines[1] == "【目标】或者奇怪的地方吗" - assert lines[2] == "答:无意义杂项" - # 不含时间戳。 - assert "00:00" not in body["messages"][1]["content"] - assert body["enable_thinking"] is False - assert body["max_tokens"] == 16 - - -def test_judge_category_delete_classes(monkeypatch) -> None: - """模型答 garbage/overlay/noise 时返回对应类别(删除类)。""" - entries = parse_srt(_SRT) - for cat in ("garbage", "overlay", "noise"): - _patch_llm(monkeypatch, [cat]) - assert _judge_category(entries, 2, context_size=10, params={}) == cat -def test_judge_category_sanitizes_noise_context(monkeypatch) -> None: - """上下文净化:规则层可确定性识别的垃圾(横线/HTML 等)在喂给 LLM 前 - 替换为 [噪音] 占位,避免污染对目标条目的场景判断。 - - 修复回归:OCR 输出中相邻字幕混有大量覆盖层垃圾(---------------、HTML、 - Marketing 等),原样进入 LLM 上下文会让模型误判整段为"水印覆盖层", - 把相邻的真实对话误删(run_011d01f19999 中 190 条含 ≥4 汉字的对话被删)。 - """ - entries = [ - {"start": "00:00:01,000", "end": "00:00:02,000", "text": "正常对话一"}, - {"start": "00:00:03,000", "end": "00:00:04,000", "text": "---------------"}, - {"start": "00:00:05,000", "end": "00:00:06,000", "text": "HTML"}, - {"start": "00:00:07,000", "end": "00:00:08,000", "text": "我是目标对话"}, - {"start": "00:00:09,000", "end": "00:00:10,000", "text": "---"}, - {"start": "00:00:11,000", "end": "00:00:12,000", "text": "正常对话二"}, - ] - fake = _patch_llm(monkeypatch, ["dialogue"]) - assert _judge_category(entries, 3, context_size=10, params={}) == "dialogue" - content = fake.bodies[0]["messages"][1]["content"] - lines = content.splitlines() - # 上下文只含过滤后的字幕:确定性垃圾条目被剔除,原文不进入 LLM。 - assert lines == ["正常对话一", "【目标】我是目标对话", "正常对话二"] - assert "---------------" not in content - assert "HTML" not in content - assert "---" not in content - # 系统提示词明确说明上下文已过滤装饰/水印符号。 - assert "过滤" in fake.bodies[0]["messages"][0]["content"] - - - -def test_judge_category_repeat_and_unknown_kept(monkeypatch) -> None: - """repeat/dialogue 返回保留类;未识别输出(空/乱码/旧式"保留")回退 dialogue。""" - entries = parse_srt(_SRT) - for answer in ("repeat", "dialogue", "", "???", "保留"): - _patch_llm(monkeypatch, [answer]) - got = _judge_category(entries, 2, context_size=10, params={}) - assert got in ("repeat", "dialogue"), (answer, got) - # 旧式"删除"回答兼容为垃圾类。 - _patch_llm(monkeypatch, ["删除"]) - assert _judge_category(entries, 2, context_size=10, params={}) == "garbage" - - -def test_judge_category_model_and_auth(monkeypatch) -> None: - """模型名从参数取;配置 API Key 时附带 Bearer 鉴权头。""" - entries = parse_srt(_SRT) - monkeypatch.setenv("LLM_API_KEY", "sk-test") - fake = _patch_llm(monkeypatch, ["dialogue"]) - assert _judge_category(entries, 0, context_size=10, params={"model": "m/1"}) == "dialogue" - assert fake.bodies[0]["model"] == "m/1" - assert fake.headers[0]["Authorization"] == "Bearer sk-test" - - -def test_judge_category_retries_on_429(monkeypatch) -> None: - """429 限流时指数退避重试,最终成功判定(配合弹性把并发压回配额内)。""" - entries = parse_srt(_SRT) - fake = FlakyLLM(failures=2, answer="dialogue") - monkeypatch.setattr("nodes.llm_filter.time.sleep", lambda s: None) - monkeypatch.setattr("nodes.llm_filter.urllib.request.urlopen", fake) - assert _judge_category(entries, 2, context_size=10, params={}) == "dialogue" - assert fake.calls == 3 # 失败 2 次 + 成功 1 次。 - - -def test_judge_category_retries_on_5xx(monkeypatch) -> None: - """服务端 5xx 同样退避重试。""" - entries = parse_srt(_SRT) - fake = FlakyLLM(failures=1, code=503, answer="dialogue") - monkeypatch.setattr("nodes.llm_filter.time.sleep", lambda s: None) - monkeypatch.setattr("nodes.llm_filter.urllib.request.urlopen", fake) - assert _judge_category(entries, 2, context_size=10, params={}) == "dialogue" - assert fake.calls == 2 - - -def test_judge_category_gives_up_after_retries(monkeypatch) -> None: - """重试耗尽仍失败时抛错:该条判定失败,任务失败后可重新处理数据。""" - entries = parse_srt(_SRT) - fake = FlakyLLM(failures=99) - monkeypatch.setattr("nodes.llm_filter.time.sleep", lambda s: None) - monkeypatch.setattr("nodes.llm_filter.urllib.request.urlopen", fake) - with pytest.raises(urllib.error.HTTPError): - _judge_category(entries, 2, context_size=10, params={}) - assert fake.calls == 3 # 最多尝试 3 次。 - - -def test_judge_category_other_errors_no_retry(monkeypatch) -> None: - """非 429/5xx 错误(如 400)不重试,直接抛出。""" - entries = parse_srt(_SRT) - fake = FlakyLLM(failures=1, code=400, answer="dialogue") - monkeypatch.setattr("nodes.llm_filter.time.sleep", lambda s: None) - monkeypatch.setattr("nodes.llm_filter.urllib.request.urlopen", fake) - with pytest.raises(urllib.error.HTTPError): - _judge_category(entries, 2, context_size=10, params={}) - assert fake.calls == 1 # 只调用一次。 - -# --------------------------------------------------------------------------- -# 去重键与删除判定 -# --------------------------------------------------------------------------- - - -def test_dedup_key_normalizes_whitespace_and_case() -> None: - """去重键:去掉全部空白并统一大小写,空格差异视为同一文本。""" - assert _dedup_key("不这么做的话 可没法胜任患者的对象") == _dedup_key( - "不这么做的话可没法胜任患者的对象" - ) - assert _dedup_key("HTML") == _dedup_key("html") - assert _dedup_key("你好 世界") != _dedup_key("你好世界2") - - -def test_should_delete_by_category_only() -> None: - """删除判定:garbage/overlay/noise 删,repeat/dialogue 留(短文本无保护)。""" - assert _should_delete("garbage", "x", min_keep_len=12) is True - assert _should_delete("overlay", "x", min_keep_len=12) is True - assert _should_delete("noise", "x", min_keep_len=12) is True - assert _should_delete("repeat", "x", min_keep_len=12) is False - assert _should_delete("dialogue", "x", min_keep_len=12) is False - - -def test_should_delete_long_text_noise_protected() -> None: - """长文本保护:≥min_keep_len 时 noise 不删(LLM 判定不稳的兜底), - garbage/overlay 仍删;短文本 noise 可删。 - - 回归:移除保护后 run_011d01f19999 新增误删 124 条真实长对话 - ('很棒的表情呢 看 拍下来了吗' 等被 LLM 误判 noise),恢复保护。 - """ - long_text = "不这么做的话 可没法胜任患者的对象" - assert _should_delete("noise", long_text, min_keep_len=12) is False - assert _should_delete("garbage", long_text, min_keep_len=12) is True - assert _should_delete("overlay", long_text, min_keep_len=12) is True - assert _should_delete("noise", "答:无意义杂项", min_keep_len=12) is True - assert _should_delete("repeat", long_text, min_keep_len=12) is False - assert _should_delete("dialogue", long_text, min_keep_len=12) is False - - -def test_rule_verdict_removes_domain_and_html_watermark() -> None: - """正则确定性过滤:网址域名/HTML 水印等"一定需要移除"的模式直接删除。 - - 覆盖真实案例:'98室[巴花堂] 水火地址 489155.com'(广告)、 - 'HTML code for a simple blue background'(OCR 识别出的网页水印)。 - """ - tokens = set(DEFAULT_OVERLAY_TOKENS) - for text in ( - "98室[巴花堂] 水火地址 489155.com", - "HTML code for a simple blue background", - "http://example.com/path", - "联系我们 admin@example.com", - ): - assert _rule_verdict(text, tokens) is True, text - # 正常对话不含垃圾模式 → 交 LLM 判断。 - assert _rule_verdict("青沼君 好可爱", tokens) is None - assert _rule_verdict("再见了 再见", tokens) is None - - -# --------------------------------------------------------------------------- -# invoke 全链路 -# --------------------------------------------------------------------------- - - -def test_invoke_filters_and_renumbers(monkeypatch, tmp_path) -> None: - """全链路(并发):按 LLM 类别判定删除无意义条,保留条重新编号输出。""" - srt = tmp_path / "in.srt" - srt.write_text(_SRT, encoding="utf-8") - # 内容决策:目标字幕含"答:"判 garbage,其余 dialogue(任何线程顺序下结果确定)。 - _patch_llm(monkeypatch, decision_fn=_decision_by_target) - response = invoke( - InvokeRequest( - run_id="r", node_instance_id="", - inputs={"srt_uri": str(srt)}, - params={"use_llm": "1"}, - output_dir=str(tmp_path / "out"), - ) - ) - assert response.status == "completed", response.error - assert response.outputs["kept"] == 3 - assert response.outputs["removed"] == 1 - out = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8") - assert out.count("-->") == 3 - # 被删除的"答:无意义杂项"(第 3 条)时间轴不再出现。 - assert "00:00:09,000" not in out - # 保留条重新编号且时间轴不变。 - assert out.startswith("1\n00:00:01,000 --> 00:00:04,000\n还有没有什么困扰\n\n2\n") - assert "00:00:13,000 --> 00:00:16,000\n第二句正常字幕\n" in out - - -def test_invoke_rules_skip_llm(monkeypatch, tmp_path) -> None: - """规则层命中时直接删除且不调用 LLM:横线/HTML/单字符全部清理。""" - srt = tmp_path / "in.srt" - srt.write_text( - "1\n00:00:01,000 --> 00:00:04,000\n---\n\n" - "2\n00:00:05,000 --> 00:00:08,000\nHTML\n\n" - "3\n00:00:09,000 --> 00:00:12,000\nV\n\n" - "4\n00:00:13,000 --> 00:00:16,000\n正常对话\n", - encoding="utf-8", - ) - fake = _patch_llm(monkeypatch, ["dialogue"]) - response = invoke( - InvokeRequest( - run_id="r", node_instance_id="", - inputs={"srt_uri": str(srt)}, - params={"use_llm": "1"}, - output_dir=str(tmp_path / "out"), - ) - ) - assert response.status == "completed", response.error - assert response.outputs["kept"] == 1 - assert response.outputs["removed"] == 3 - assert len(fake.bodies) == 1 # 仅"正常对话"需 LLM,其余全走规则。 - - -def test_invoke_dedup_single_llm_call(monkeypatch, tmp_path) -> None: - """去重:相同文本(含空格变体)只调一次 LLM,判定结果一致。""" - srt = tmp_path / "in.srt" - srt.write_text( - "1\n00:00:01,000 --> 00:00:04,000\n好舒服\n\n" - "2\n00:00:05,000 --> 00:00:08,000\n好 舒 服\n\n" - "3\n00:00:09,000 --> 00:00:12,000\n好舒服\n\n" - "4\n00:00:13,000 --> 00:00:16,000\n别的台词\n", - encoding="utf-8", - ) - fake = _patch_llm(monkeypatch, decision_fn=lambda body: "repeat") - response = invoke( - InvokeRequest( - run_id="r", node_instance_id="", - inputs={"srt_uri": str(srt)}, - params={"use_llm": "1"}, - output_dir=str(tmp_path / "out"), - ) - ) - assert response.status == "completed", response.error - # 2 个唯一文本(好舒服/别的台词)→ 恰好 2 次 LLM 调用。 - assert len(fake.bodies) == 2 - # repeat 类保留 → 4 条全部保留。 - assert response.outputs["kept"] == 4 - assert response.outputs["removed"] == 0 - - -def test_invoke_dedupe_disabled(monkeypatch, tmp_path) -> None: - """dedupe=0 关闭去重:每个条目都调一次 LLM。""" - srt = tmp_path / "in.srt" - srt.write_text( - "1\n00:00:01,000 --> 00:00:04,000\n同一句\n\n" - "2\n00:00:05,000 --> 00:00:08,000\n同一句\n", - encoding="utf-8", - ) - fake = _patch_llm(monkeypatch, decision_fn=lambda body: "dialogue") - response = invoke( - InvokeRequest( - run_id="r", node_instance_id="", - inputs={"srt_uri": str(srt)}, - params={"dedupe": "0", "use_llm": "1"}, - output_dir=str(tmp_path / "out"), - ) - ) - assert response.status == "completed", response.error - assert len(fake.bodies) == 2 - - -def test_invoke_context_size_param(monkeypatch, tmp_path) -> None: - """context_size 参数生效:窗口大小=2×context_size+1(两端截断除外)。""" - srt = tmp_path / "in.srt" - srt.write_text(_SRT, encoding="utf-8") - fake = _patch_llm(monkeypatch, decision_fn=_decision_by_target) - response = invoke( - InvokeRequest( - run_id="r", node_instance_id="", - inputs={"srt_uri": str(srt)}, - params={"context_size": 1, "use_llm": "1"}, - output_dir=str(tmp_path / "out"), - ) - ) - assert response.status == "completed", response.error - # 找到目标为第 2 条("或者奇怪的地方吗")的请求体:窗口应含 3 行。 - body = next( - b for b in fake.bodies - if "【目标】或者奇怪的地方吗" in b["messages"][1]["content"] - ) - window = body["messages"][1]["content"].splitlines() - assert len(window) == 3 - - -def test_invoke_overlay_tokens_param(monkeypatch, tmp_path) -> None: - """overlay_tokens 参数:自定义 token 走规则层删除,不调用 LLM。""" - srt = tmp_path / "in.srt" - srt.write_text( - "1\n00:00:01,000 --> 00:00:04,000\nCleaning\n\n" - "2\n00:00:05,000 --> 00:00:08,000\n正常对话\n", - encoding="utf-8", - ) - fake = _patch_llm(monkeypatch, decision_fn=lambda body: "dialogue") - response = invoke( - InvokeRequest( - run_id="r", node_instance_id="", - inputs={"srt_uri": str(srt)}, - params={"overlay_tokens": ["cleaning"], "use_llm": "1"}, - output_dir=str(tmp_path / "out"), - ) - ) - assert response.status == "completed", response.error - assert response.outputs["removed"] == 1 - assert response.outputs["kept"] == 1 - assert len(fake.bodies) == 1 # 只判正常对话。 - - -def test_invoke_overlay_tokens_json_string(monkeypatch, tmp_path) -> None: - """overlay_tokens 以 JSON 字符串形式传入(工作流参数常见形态)同样生效。""" - srt = tmp_path / "in.srt" - srt.write_text( - "1\n00:00:01,000 --> 00:00:04,000\nXXLogo\n\n" - "2\n00:00:05,000 --> 00:00:08,000\n正常对话\n", - encoding="utf-8", - ) - fake = _patch_llm(monkeypatch, ["dialogue"]) - response = invoke( - InvokeRequest( - run_id="r", node_instance_id="", - inputs={"srt_uri": str(srt)}, - params={"overlay_tokens": '["xxlogo", "xx"]', "use_llm": "1"}, - output_dir=str(tmp_path / "out"), - ) - ) - assert response.status == "completed", response.error - assert response.outputs["removed"] == 1 - assert response.outputs["kept"] == 1 - assert len(fake.bodies) == 1 - - -def test_invoke_missing_input(tmp_path) -> None: - """缺少 srt_uri 时返回失败。""" - response = invoke( - InvokeRequest(run_id="r", node_instance_id="", inputs={}, output_dir=str(tmp_path)) - ) - assert response.status == "failed" - assert "srt_uri" in response.error - - -def test_invoke_file_missing(tmp_path) -> None: - """srt 文件不存在时返回失败。""" - response = invoke( - InvokeRequest( - run_id="r", node_instance_id="", - inputs={"srt_uri": str(tmp_path / "none.srt")}, - output_dir=str(tmp_path), - ) - ) - assert response.status == "failed" - assert "not found" in response.error - - -def test_invoke_llm_error(monkeypatch, tmp_path) -> None: - """LLM 调用失败(网络错误)时返回 failed,不静默输出未过滤结果。""" - srt = tmp_path / "in.srt" - srt.write_text(_SRT, encoding="utf-8") - - def boom(request, timeout=None): - raise urllib.error.URLError("llm down") - - monkeypatch.setattr("nodes.llm_filter.urllib.request.urlopen", boom) - response = invoke( - InvokeRequest( - run_id="r", node_instance_id="", - inputs={"srt_uri": str(srt)}, - params={"use_llm": "1"}, - output_dir=str(tmp_path / "out"), - ) - ) - assert response.status == "failed" - assert "llm down" in response.error -def test_invoke_retries_rate_limited_entries(monkeypatch, tmp_path) -> None: - """判定遇 429(限流)时临时降并发并对失败条目重试,最终正常完成。 - - 自适应:多线程并发打到 SiliconFlow 配额线触发 429 时,worker 通知线程池 - report_failure 临时收紧最大并发,失败条目在收紧后重试一轮,避免整体失败 - (run_011d01f19999 在 20 并发下因 429 重试耗尽而 FAILED 的修复)。 - """ - srt = tmp_path / "in.srt" - srt.write_text( - "1\n00:00:01,000 --> 00:00:04,000\n第一句对话\n\n" - "2\n00:00:05,000 --> 00:00:08,000\n---------------\n\n" - "3\n00:00:09,000 --> 00:00:12,000\n第二句对话\n\n" - "4\n00:00:13,000 --> 00:00:16,000\nHTML\n", - encoding="utf-8", - ) - # 前 3 次调用都 429(重试耗尽抛错),之后成功:首次 map 部分条目失败, - # 二次重试在降并发后成功。 - fake = FlakyLLM(failures=3, answer="dialogue") - monkeypatch.setattr("nodes.llm_filter.time.sleep", lambda s: None) - monkeypatch.setattr("nodes.llm_filter.urllib.request.urlopen", fake) - response = invoke( - InvokeRequest( - run_id="r", node_instance_id="", - inputs={"srt_uri": str(srt)}, - params={"use_llm": "1"}, - output_dir=str(tmp_path / "out"), - ) - ) - assert response.status == "completed", response.error - kept = parse_srt((tmp_path / "out" / "filtered.srt").read_text(encoding="utf-8")) - assert [e["text"] for e in kept] == ["第一句对话", "第二句对话"] - assert fake.calls >= 5 # 首次 map + 二次重试均有调用。 - - - -def test_invoke_empty_srt(monkeypatch, tmp_path) -> None: - """空 SRT(无条目)正常完成,输出空文件且不调用 LLM。""" - srt = tmp_path / "empty.srt" - srt.write_text("", encoding="utf-8") - fake = _patch_llm(monkeypatch, []) - response = invoke( - InvokeRequest( - run_id="r", node_instance_id="", - inputs={"srt_uri": str(srt)}, - params={}, - output_dir=str(tmp_path / "out"), - ) - ) - assert response.status == "completed", response.error - assert response.outputs["kept"] == 0 - assert response.outputs["removed"] == 0 - assert fake.bodies == [] - - -# --------------------------------------------------------------------------- -# 真实任务留存数据回归(testdata/ocr_srt_run_ac7f480a3ccb.srt) -# --------------------------------------------------------------------------- - - -def test_real_run_rules_and_dialogue_regression(monkeypatch, tmp_path) -> None: - """真实数据回归:规则层清理垃圾、对话保留、时间轴单调、去重生效。 - - 夹具为 run_ac7f480a3ccb(单线程 v4 跑完整 2 小时视频)的 OCR 输出 1666 条。 - 假 LLM 对非规则条目一律答 dialogue:验证规则层删掉全部垃圾(横线/水印/ - 单字符),真实对话全部保留,且相同文本只调一次 LLM(去重)。 - """ - srt = REAL_SRT - if not srt.is_file(): - pytest.skip("缺少 testdata/ocr_srt_run_ac7f480a3ccb.srt,跳过回归测试") - fake = _patch_llm(monkeypatch, decision_fn=lambda body: "dialogue") - # 显式开启 LLM 层(默认已关闭,本用例验证的是"规则层+LLM 层"的旧行为)。 - response = invoke( - InvokeRequest( - run_id="r", node_instance_id="", - inputs={"srt_uri": str(srt)}, - params={"use_llm": "1"}, - output_dir=str(tmp_path / "out"), - ) - ) - assert response.status == "completed", response.error - - entries = parse_srt(srt.read_text(encoding="utf-8")) - out = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8") - - # 规则层删除量 = 夹具中直接命中规则文本的条数(真实数据动态计算)。 - # 规则层删除量 = 夹具中直接命中规则文本的条数(与 invoke 默认 token 集一致)。 - from nodes.llm_filter import DEFAULT_OVERLAY_TOKENS - - tokens = set(DEFAULT_OVERLAY_TOKENS) - rule_removed = sum( - 1 for e in entries if _rule_verdict(e["text"], tokens) is True - ) - assert response.outputs["removed"] == rule_removed - assert response.outputs["kept"] == len(entries) - rule_removed - - # 垃圾全部清出输出(横线、HTML、单字符都不再出现;只检查文本字段, - # 排除 SRT 的序号行与时间轴行)。 - for entry in parse_srt(out): - assert _rule_verdict(entry["text"], tokens) is not True, entry["text"] - # 真实对话保留(修复前被误删的自我介绍、请求对话、呻吟内容行)。 - # 注意:片头标题卡(淫魔病院…)真实 LLM 判为 overlay(烧录标题), - # 不在必保留列表内;本测试假 LLM 一律答 dialogue,仅验证机制正确。 - for kept_line in ( - "谢谢你 松井小姐", - "我姓泷本 请多多指教", - "不这么做的话 可没法胜任患者的对象", - "你要看着北冈小姐的脸 你们俩相互看看嘛", - "好舒服", - ): - assert kept_line in out, kept_line - - # 时间轴单调递增(输出顺序即时间顺序)。 - times = [] - for m in __import__("re").finditer( - r"(\d{2}:\d{2}:\d{2},\d{3})\s*-->", out - ): - t = m.group(1).replace(",", ".") - h, mi, s = t.split(":") - times.append(int(h) * 3600 + int(mi) * 60 + float(s)) - assert all(a < b for a, b in zip(times, times[1:])) - - # 去重:LLM 调用数 == 非规则条目中的唯一文本数(远小于条目数)。 - unique_llm = len({ - _dedup_key(e["text"]) - for e in entries - if _rule_verdict(e["text"], tokens) is None - }) - assert len(fake.bodies) == unique_llm - assert len(fake.bodies) < len(entries) # 去重确实省调用。 - - -class _FailOnTarget429: - """模拟持续限流:目标字幕含指定词时恒抛 429,其余正常返回 dialogue。 - - 用于构造"部分条目成功、个别条目持续 429"的断点重跑场景:第一次 invoke - 整体失败但成功条目已写存档;解除限流后第二次 invoke 只重判失败条目。 - """ - - def __init__(self, marker: str) -> None: - self.marker = marker - self.enabled = True - self.bodies: list[dict] = [] - - def __call__(self, request, timeout=None): - body = json.loads(request.data.decode("utf-8")) - self.bodies.append(body) - target = next( - line for line in body["messages"][1]["content"].splitlines() - if line.startswith("【目标】") - ) - if self.enabled and self.marker in target: - raise urllib.error.HTTPError(request.full_url, 429, "rate limited", {}, None) - payload = json.dumps({"choices": [{"message": {"content": "dialogue"}}]}).encode() - return FakeResponse(payload) - - -def _llm_invoke(srt_text: str, out: Path) -> tuple[object, Path]: - """用给定 SRT 文本构造并执行一次 llm-filter invoke,返回 (响应, 输入文件)。 - - 该辅助函数专供 LLM 分类层用例使用,因此显式开启 use_llm(默认关闭)。""" - srt = out.parent / "in.srt" - srt.write_text(srt_text, encoding="utf-8") - return ( - invoke( - InvokeRequest( - run_id="r", node_instance_id="", - inputs={"srt_uri": str(srt)}, - params={"use_llm": "1"}, - output_dir=str(out), - ) - ), - srt, - ) - - -def test_load_and_append_partial(tmp_path) -> None: - """判定存档读写:无存档/损坏行跳过,追加后可读回。""" - from nodes.llm_filter import _PARTIAL_NAME, _append_partial, _load_partial - - out = tmp_path / "out" - assert _load_partial(out) == {} # 目录不存在 → 空。 - out.mkdir() - assert _load_partial(out) == {} # 无存档 → 空。 - # 损坏行(半行写入)跳过,正常行读回。 - (out / _PARTIAL_NAME).write_text( - '{"index": 0, "category": "dialogue"}\n\n{broken\n{"index": 3, "category": "garbage"}\n', - encoding="utf-8", - ) - assert _load_partial(out) == {0: "dialogue", 3: "garbage"} - # 追加一条后读回。 - _append_partial(out, 5, "overlay") - assert _load_partial(out)[5] == "overlay" - - -def test_invoke_resume_skips_archived_judgments(monkeypatch, tmp_path) -> None: - """断点存档:已判定条目重跑时不重复调用 LLM(去重后只补判未判定)。""" - fake = _patch_llm(monkeypatch, contents=["dialogue"] * 4) - out = tmp_path / "out" - out.mkdir() - # 预置存档:索引 0、1 已判定(模拟上次失败前已完成的部分)。 - from nodes.llm_filter import _append_partial - - _append_partial(out, 0, "dialogue") - _append_partial(out, 1, "dialogue") - response, _srt = _llm_invoke(_SRT, out) - assert response.status == "completed", response.error - # 4 条唯一文本中 2 条已存档,只调用剩余 2 条。 - assert len(fake.bodies) == 2 - # 输出与全量判定一致:全部 dialogue → 4 条都保留。 - assert response.outputs["kept"] == 4 - assert Path(response.outputs["srt_uri"]).read_text(encoding="utf-8").count("-->") == 4 - - -def test_invoke_partial_failure_then_resume(monkeypatch, tmp_path) -> None: - """真实断点重跑:个别条目持续 429 → 整体失败但成功判定已写存档, - 解除限流后重跑只补判失败条目,最终产物与一次跑完一致。""" - fake = _FailOnTarget429(marker="无意义") - monkeypatch.setattr("nodes.llm_filter.urllib.request.urlopen", fake) - monkeypatch.setattr("nodes.llm_filter.time.sleep", lambda s: None) - out = tmp_path / "out" - first, _srt = _llm_invoke(_SRT, out) - assert first.status == "failed" - assert "429" in (first.error or "") - calls_first = len(fake.bodies) - # 成功判定的 3 条已写存档;持续 429 的"答:无意义杂项"(索引 2)不在存档。 - from nodes.llm_filter import _load_partial - - partial = _load_partial(out) - assert 2 not in partial - assert len(partial) == 3 - # 解除限流后重跑:只补判失败条目(1 次调用),其余复用存档。 - fake.enabled = False - second, _srt = _llm_invoke(_SRT, out) - assert second.status == "completed", second.error - assert len(fake.bodies) == calls_first + 1 - # 输出:解除限流后全部判定为 dialogue → 4 条都保留(与"一次跑完"一致)。 - out_text = Path(second.outputs["srt_uri"]).read_text(encoding="utf-8") - assert out_text.count("-->") == 4 - assert "00:00:09,000" in out_text diff --git a/tests/test_llm_filter_rule_only.py b/tests/test_llm_filter_rule_only.py deleted file mode 100644 index 7ebbe17..0000000 --- a/tests/test_llm_filter_rule_only.py +++ /dev/null @@ -1,196 +0,0 @@ -"""llm-filter 规则层扩展与 LLM 层默认关闭(2026-09 决策)。 - -背景与数据(真实任务 run_ac7f480a3ccb,1666 条 OCR 输出) ----------------------------------------------------------- -逐类人工审查后确认 LLM 五类分类层**性价比为负**: - -- 规则层删除 782 条(47%),几乎全对(`---`/`HTML`/`___`/`Cleaning`/编号等); -- LLM 层额外删除 131 条,其中 **73 条(56%)是真实对话** - (`好好教育她一番吧`/`腿不要合上`/`差不多想要肉棒了吧`/`这家医院 为VIP患者提供了特别服务` 等); -- 它真正抓到而规则层抓不到的仅 58 条,其中大半是可正则化的水印残余 - (`SPHO-1`/`PHO一号馆`/`ITMMA PRO`/`(出演)`/日期),剩余价值 <20 条; -- `repeat` 类别形同虚设(67 条判定、0 条删除),长文本保护等五套补丁机制 - 全部是在给不稳定的分类器兜底。 - -因此本次改动: -1. **把 LLM 层没抓到、但可确定性识别的模式下沉到规则层**(水印编号、 - 日期、VLM 提示泄漏、演员标注括号); -2. **LLM 分类层默认关闭**(`use_llm` 参数,默认关),只保留规则层; - 需要时可按工作流参数显式打开。 - -保留 884 条而不是 588 条,多出的条目里可能混有水印残余,但**不再误删真对话**。 -""" - -from __future__ import annotations - -import json -from pathlib import Path - -from nodes.llm_filter import ( - DEFAULT_OVERLAY_TOKENS, - _rule_verdict, - invoke, - parse_srt, -) -from wov_sdk.models import InvokeRequest - -WORKSPACE = Path(__file__).resolve().parent.parent -# 真实 OCR 回归数据(1699 行 1666 条),用于量化"规则层不误删对话"。 -REAL_OCR = WORKSPACE / "testdata/ocr_srt_run_ac7f480a3ccb.srt" - - -# --------------------------------------------------------------------------- -# 规则层扩展:原 LLM 层抓到、但可确定性识别的模式 -# --------------------------------------------------------------------------- - - -def test_rule_层删除水印编号() -> None: - """VLM 把画面水印编号识别成短串(SPHO-1/PHO一号馆/NO.1专用)→ 规则层删除。""" - tokens = set(DEFAULT_OVERLAY_TOKENS) - for text in ( - "SPH", "SP10+", "SP10-1型", "SPH-1专用", "SPIO-1 FEB", "NO.1专用", - "PHD-手術", "P10一手机", "PHD一专用", "PH0一1瓶盖", "PHO一号馆", - "SPHO一专用", "SPHO-1", "SPNO-1", - ): - assert _rule_verdict(text, tokens) is True, text - # 反例:正常英文/编号样式的真实对白不受影响(含 CJK 或较长英文短语)。 - for text in ("青沼君 好可爱", "再见了 再见", "SPA 的感觉真好"): - assert _rule_verdict(text, tokens) is None, text - - -def test_rule_层删除日期与编号型乱码() -> None: - """OCR 把画面日期/时间戳识别成条目(2011-11-27、4.0)→ 规则层删除。""" - tokens = set(DEFAULT_OVERLAY_TOKENS) - for text in ("2011-11-27", "2011/11/27", "2011年11月27日", "4.0", "2.0"): - assert _rule_verdict(text, tokens) is True, text - # 反例:含汉字的日期/数值样式不删(可能是真实内容)。 - for text in ("10月14岁", "应该有 4.0 就好了"): - assert _rule_verdict(text, tokens) is None, text - - -def test_rule_层删除VLM提示泄漏() -> None: - """VLM(glm-ocr)偶尔把系统性提示词回显成识别文本 → 规则层删除。""" - tokens = set(DEFAULT_OVERLAY_TOKENS) - for text in ( - "No text is visible in the provided image.", - "The image is blurry and does not contain", - "no text visible", - ): - assert _rule_verdict(text, tokens) is True, text - # 反例:正常英文对白(含 CJK 上下文)不受影响。 - assert _rule_verdict("谢谢你 松井小姐", tokens) is None - - -def test_rule_层只删除角色标注_不删除括号内名字() -> None: - """只删角色标注词((出演));括号内的名字/对白**保留**(避免误删)。 - - 实测数据里(北冈果林)这类演员名括号与"(小声)不要啊"无法用正则可靠 - 区分,而前者本身是无害字幕文本、删除收益极小,因此宁可全留。 - """ - tokens = set(DEFAULT_OVERLAY_TOKENS) - for text in ("(出演)", "(主演)", "(监督)"): - assert _rule_verdict(text, tokens) is True, text - for text in ("(北冈果林)", "(叶山小百合)", "(松井日奈子)", "(我该怎么办)", "(小声)不要啊"): - assert _rule_verdict(text, tokens) is None, text - - -# --------------------------------------------------------------------------- -# LLM 层默认关闭 -# --------------------------------------------------------------------------- - - -def test_规则层不误删真实对话() -> None: - """真实数据回归:规则层保留全部含 CJK 的对话条,不误删。 - - 这是本次改动的核心不变式——旧实现(规则+LLM)会删掉 73 条真对话, - 新规则层必须一条都不删。用真实 1666 条 OCR 输出验证。 - """ - if not REAL_OCR.is_file(): - import pytest - - pytest.skip("真实 OCR 回归数据缺失") - import re - - entries = parse_srt(REAL_OCR.read_text(encoding="utf-8")) - tokens = set(DEFAULT_OVERLAY_TOKENS) - deleted_dialogue = [] - for entry in entries: - text = entry["text"] - # 含 >=4 汉字、且不是明显编号/水印形态的,视为真实对话。 - if len(re.findall(r"[\u4e00-\u9fff]", text)) >= 4 and not re.match( - r"^(?:98|SPH|SPHO|SPIO|SPNO|SP10|PHO|PH0|PHD|P10|NO\.|\d)", text - ): - if _rule_verdict(text, tokens) is True: - deleted_dialogue.append(text) - assert not deleted_dialogue, f"规则层误删真实对话: {deleted_dialogue[:10]}" - - -def test_invoke_默认不调用LLM(monkeypatch, tmp_path) -> None: - """默认(不传 use_llm)不调用 LLM:规则层外的一律保留,不做分类判定。""" - calls: list[object] = [] - - def _boom(*args, **kwargs): - calls.append(args) - raise AssertionError("默认配置不应调用 LLM") - - monkeypatch.setattr("urllib.request.urlopen", _boom) - source = tmp_path / "in.srt" - source.write_text( - "1\n00:00:01,000 --> 00:00:02,000\n---\n\n" - "2\n00:00:03,000 --> 00:00:04,000\n好好教育她一番吧\n\n" - "3\n00:00:05,000 --> 00:00:06,000\nSPHO-1\n", - encoding="utf-8", - ) - response = invoke( - InvokeRequest( - run_id="r", node_instance_id="", inputs={"srt_uri": str(source)}, - output_dir=str(tmp_path / "out"), - ) - ) - assert response.status == "completed", response.error - assert not calls, "默认不应有任何 LLM 调用" - text = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8") - assert "好好教育她一番吧" in text # 真实对话保留 - assert "SPHO-1" not in text # 水印编号由规则层删除 - assert response.outputs["kept"] == 1 - assert response.outputs["removed"] == 2 - - -def test_invoke_use_llm开启时仍走分类(monkeypatch, tmp_path) -> None: - """显式 use_llm=1 时保留原 LLM 分类能力(可回退到旧行为)。""" - sent: list[dict] = [] - - class Response: - def __init__(self): - self._payload = json.dumps( - {"choices": [{"message": {"content": "garbage"}}]} - ).encode() - - def __enter__(self): - return self - - def __exit__(self, *a): - return False - - def read(self): - return self._payload - - def _open(request, **kwargs): - sent.append(json.loads(request.data)) - return Response() - - monkeypatch.setattr("urllib.request.urlopen", _open) - source = tmp_path / "in.srt" - source.write_text( - "1\n00:00:01,000 --> 00:00:02,000\n好好教育她一番吧\n", - encoding="utf-8", - ) - response = invoke( - InvokeRequest( - run_id="r", node_instance_id="", inputs={"srt_uri": str(source)}, - params={"use_llm": "1"}, output_dir=str(tmp_path / "out"), - ) - ) - assert response.status == "completed", response.error - assert sent, "use_llm=1 时应调用 LLM" - assert response.outputs["kept"] == 0 # LLM 判 garbage → 删除 diff --git a/tests/test_maintenance.py b/tests/test_maintenance.py deleted file mode 100644 index 39188ef..0000000 --- a/tests/test_maintenance.py +++ /dev/null @@ -1,197 +0,0 @@ -"""孤儿数据清理器测试。 - -覆盖 COMPLETED 无文件任务的删除、各类保留分支(有文件/失败/宽限期内)、 -无任务记录的残留目录清理、清理线程启停以及防御性分支。 -""" - -from datetime import datetime, timezone -from pathlib import Path - -from wov_app.db import Database -from wov_app.maintenance import OrphanCleaner - - -def _db(tmp_path) -> Database: - """在临时目录创建独立数据库。""" - return Database(tmp_path / "wov.db") - - -def _make_run(db, run_id, status="COMPLETED", updated="2020-01-01T00:00:00+00:00", input_uri=None): - """创建指定状态与更新时间的工作流任务记录。""" - db.upsert_workflow({"id": "flow", "name": "F", "published": 1, "latest_version": 1}) - db.create_run( - { - "id": run_id, - "workflow_id": "flow", - "workflow_version": 1, - "status": status, - "progress": 1, - "input_uri": input_uri, - "created_at": updated, - "updated_at": updated, - } - ) - - -def _now_iso() -> str: - """返回当前 UTC 时间的 ISO 字符串。""" - return datetime.now(timezone.utc).isoformat() - - -def test_cleaner_removes_completed_orphan_run(tmp_path) -> None: - """验证 COMPLETED 且无任何产物文件、超过宽限期的任务被整体清理。""" - db = _db(tmp_path) - upload_dir = tmp_path / "storage" / "uploads" / "run_orphan" - upload_dir.mkdir(parents=True) - upload_file = upload_dir / "in.mp4" - upload_file.write_bytes(b"x") - _make_run(db, "run_orphan", input_uri=str(upload_file)) - db.create_artifact( - { - "run_id": "run_orphan", - "node_id": "asr", - "name": "asr.srt_uri", - "uri": str(tmp_path / "storage" / "runs" / "run_orphan" / "out.srt"), - "mime_type": "application/x-subrip", - "size": 1, - } - ) - cleaner = OrphanCleaner(db, tmp_path / "storage", grace_seconds=3600) - assert cleaner.clean_once() == 1 - assert db.get_run("run_orphan") is None - assert db.list_artifacts("run_orphan") == [] - assert not upload_dir.exists() - - -def test_cleaner_keeps_completed_run_with_files(tmp_path) -> None: - """验证仍有产物文件的 COMPLETED 任务不会被清理。""" - db = _db(tmp_path) - steps = tmp_path / "storage" / "runs" / "run_keep" - (steps / "asr").mkdir(parents=True) - (steps / "asr" / "out.srt").write_text("1\n00:00:00,000 --> 00:00:01,000\nok\n", encoding="utf-8") - _make_run(db, "run_keep") - cleaner = OrphanCleaner(db, tmp_path / "storage", grace_seconds=3600) - assert cleaner.clean_once() == 0 - assert db.get_run("run_keep") is not None - - -def test_cleaner_keeps_failed_and_recent_runs(tmp_path) -> None: - """验证 FAILED 任务与宽限期内的任务都不会被自动清理。""" - db = _db(tmp_path) - _make_run(db, "run_failed", status="FAILED") - _make_run(db, "run_recent", updated=_now_iso()) - cleaner = OrphanCleaner(db, tmp_path / "storage", grace_seconds=3600) - assert cleaner.clean_once() == 0 - assert db.get_run("run_failed") is not None - assert db.get_run("run_recent") is not None - - -def test_cleaner_removes_dangling_dirs_only(tmp_path) -> None: - """验证无任务记录的残留目录被删除,已有任务的上传目录被保留。""" - db = _db(tmp_path) - ghost_upload = tmp_path / "storage" / "uploads" / "ghost" - ghost_upload.mkdir(parents=True) - ghost_steps = tmp_path / "storage" / "runs" / "ghost" - ghost_steps.mkdir(parents=True) - keep_upload = tmp_path / "storage" / "uploads" / "run_keep" - keep_upload.mkdir(parents=True) - (keep_upload / "in.mp4").write_bytes(b"x") - # run_keep 存在产物文件,不属于孤儿任务。 - keep_steps = tmp_path / "storage" / "runs" / "run_keep" / "asr" - keep_steps.mkdir(parents=True) - (keep_steps / "out.srt").write_text("ok", encoding="utf-8") - _make_run(db, "run_keep") - cleaner = OrphanCleaner(db, tmp_path / "storage", grace_seconds=3600) - assert cleaner.clean_once() == 2 - assert not ghost_upload.exists() - assert not ghost_steps.exists() - assert keep_upload.exists() - - -def test_cleaner_removes_run_with_empty_steps_dir(tmp_path) -> None: - """验证步骤目录存在但为空(无文件)时仍视为孤儿清理。""" - db = _db(tmp_path) - steps = tmp_path / "storage" / "runs" / "run_empty" - (steps / "asr").mkdir(parents=True) - _make_run(db, "run_empty", input_uri="") - cleaner = OrphanCleaner(db, tmp_path / "storage", grace_seconds=3600) - assert cleaner.clean_once() == 1 - assert db.get_run("run_empty") is None - assert not steps.exists() - - -def test_cleaner_skips_batch_runs(tmp_path) -> None: - """批量处理运行(source=batch)跳过清理:绝不删除用户视频文件夹。 - - 批量 run 的产物在用户视频旁的同名文件夹里,不在主存储目录下;若按普通 - 孤儿逻辑处理,_has_files 检查不到会误删记录,_remove_run 还会连带删除 - input_uri 的父目录(用户的整个视频文件夹)。 - """ - db = _db(tmp_path) - video_folder = tmp_path / "my_videos" - video_folder.mkdir() - video = video_folder / "a.mp4" - video.write_bytes(b"real") - # 过期、COMPLETED、主存储无任何产物文件——普通任务会被清理的条件全满足。 - db.upsert_workflow({"id": "flow", "name": "F", "published": 1, "latest_version": 1}) - db.create_run( - { - "id": "run_batch", - "workflow_id": "flow", - "workflow_version": 1, - "status": "COMPLETED", - "progress": 1, - "input_uri": str(video), - "source": "batch", - "created_at": "2020-01-01T00:00:00+00:00", - "updated_at": "2020-01-01T00:00:00+00:00", - } - ) - cleaner = OrphanCleaner(db, tmp_path / "storage", grace_seconds=3600) - assert cleaner.clean_once() == 0 - assert db.get_run("run_batch") is not None - assert video_folder.exists() - assert video.exists() -def test_cleaner_default_config_and_defensive_branches(tmp_path, monkeypatch) -> None: - """验证默认配置构造、缺失/非法时间与缺失任务记录的防御分支。""" - db = _db(tmp_path) - # 默认配置(interval/grace 走 config 默认值)。 - cleaner = OrphanCleaner(db, tmp_path / "storage") - assert cleaner.interval_seconds > 0 - assert cleaner.grace_seconds > 0 - # 无更新时间 / 非法时间均保守视为未过期。 - assert cleaner._expired(None) is False - assert cleaner._expired("not-a-date") is False - # 不存在的根目录直接返回 0。 - assert cleaner._clean_dangling(tmp_path / "missing", set()) == 0 - # list_run_ids 返回的 ID 在读取详情前已不存在时跳过。 - monkeypatch.setattr(db, "list_run_ids", lambda: ["ghost"]) - monkeypatch.setattr(db, "get_run", lambda run_id: None) - assert cleaner.clean_once() == 0 - - -def test_cleaner_start_stop_loop(tmp_path) -> None: - """验证清理线程可启动、周期执行并正常停止。""" - import time - - db = _db(tmp_path) - cleaner = OrphanCleaner(db, tmp_path / "storage", interval_seconds=0.05, grace_seconds=3600) - cleaner.start() - try: - cleaner.start() - time.sleep(0.2) - finally: - cleaner.stop() - assert cleaner._thread is None - - -def test_lifespan_starts_cleaner(monkeypatch) -> None: - """验证启用清理器时应用生命周期会启动清理线程并随退出停止。""" - from fastapi.testclient import TestClient - - from wov_app.main import app - - monkeypatch.setenv("WOV_CLEANUP_ENABLED", "1") - with TestClient(app) as client: - assert client.get("/health").status_code == 200 - assert app.state.cleaner._thread is not None diff --git a/tests/test_models.py b/tests/test_models.py deleted file mode 100755 index 3214685..0000000 --- a/tests/test_models.py +++ /dev/null @@ -1,174 +0,0 @@ -"""wov_sdk.models 的单元测试。 - -测试覆盖所有数据模型的 JSON 往返序列化、字段校验和 manifest 文件加载, -确保协议模型的稳定性。 -""" - -import json - -import pytest - -from wov_sdk.models import ( - HealthResponse, - InvokeRequest, - InvokeResponse, - NodeManifest, - ProgressEvent, - WorkflowDefinition, - WorkflowEdge, - WorkflowNode, -) - - -def valid_manifest() -> NodeManifest: - """构造一个覆盖全部字段的合法 NodeManifest,供测试复用。""" - return NodeManifest( - id="echo", - name="Echo", - version="1.0.0", - capability="echo", - command=["python", "-m", "echo"], - repo_dir="wov-node-echo", - env={"PORT": "0"}, - input_schema={"text": "string"}, - output_schema={"text": "string"}, - max_concurrency=2, - idle_ttl_seconds=15, - health_timeout_seconds=5, - keep_warm=True, - ) - - -def test_manifest_round_trip() -> None: - """验证 manifest 经过 to_dict/from_dict 后保持原值。""" - manifest = valid_manifest() - restored = NodeManifest.from_dict(manifest.to_dict()) - assert restored == manifest - - -@pytest.mark.parametrize( - ("field", "value"), - [ - ("id", ""), - ("name", ""), - ("version", ""), - ("capability", ""), - ("repo_dir", ""), - ("command", []), - ("max_concurrency", 0), - ("idle_ttl_seconds", -1), - ("health_timeout_seconds", 0), - ], -) -def test_manifest_validation(field: str, value: object) -> None: - """验证必填字段为空或数值越界时抛出 ValueError。""" - manifest = valid_manifest() - setattr(manifest, field, value) - with pytest.raises(ValueError): - manifest.validate() - - -def test_manifest_load(tmp_path) -> None: - """验证 NodeManifest.load 能从 JSON 文件读取并校验。""" - path = tmp_path / "node.manifest.json" - path.write_text(json.dumps(valid_manifest().to_dict()), encoding="utf-8") - loaded = NodeManifest.load(str(path)) - assert loaded.id == "echo" - - -def test_invoke_request_round_trip() -> None: - """验证 InvokeRequest 的 JSON 往返序列化。""" - request = InvokeRequest( - run_id="run_1", - node_instance_id="ni_1", - inputs={"text": "hello"}, - params={"temperature": 0.2}, - output_dir="out", - ) - restored = InvokeRequest.from_dict(request.to_dict()) - assert restored == request - - -def test_invoke_response_round_trip() -> None: - """验证 InvokeResponse 的 JSON 往返序列化。""" - response = InvokeResponse(status="completed", outputs={"text": "hello"}) - restored = InvokeResponse.from_dict(response.to_dict()) - assert restored == response - - -def test_health_and_progress_serialization() -> None: - """验证健康检查和进度事件模型的字典输出。""" - health = HealthResponse(status="ok", node_id="echo", version="1.0.0") - assert health.to_dict() == { - "status": "ok", - "node_id": "echo", - "version": "1.0.0", - } - - progress = ProgressEvent(run_id="run_1", node_id="echo", progress=0.5, message="half") - assert progress.to_dict() == { - "run_id": "run_1", - "node_id": "echo", - "progress": 0.5, - "message": "half", - } - - -def test_workflow_node_and_edge_round_trip() -> None: - """验证工作流节点与边的 JSON 往返序列化。""" - node = WorkflowNode( - id="asr", - node_type="faster-whisper", - params={"language": "ja"}, - inputs={"audio_uri": "extract.audio_uri"}, - ) - edge = WorkflowEdge(from_node="extract", to_node="asr") - assert WorkflowNode.from_dict(node.to_dict()) == node - assert WorkflowEdge.from_dict(edge.to_dict()) == edge - assert edge.to_dict() == {"from": "extract", "to": "asr"} - - assert node.to_dict()["inputs"] == {"audio_uri": "extract.audio_uri"} - - -def test_workflow_definition_round_trip_and_validation() -> None: - """验证完整 DAG 定义可往返序列化并通过校验。""" - definition = WorkflowDefinition( - name="demo", - version=1, - nodes=[ - WorkflowNode(id="extract", node_type="ffmpeg"), - WorkflowNode(id="asr", node_type="whisper"), - ], - edges=[WorkflowEdge(from_node="extract", to_node="asr")], - entry_inputs={"video_uri": "file"}, - final_outputs={"srt": "asr.srt_uri"}, - ) - restored = WorkflowDefinition.from_dict(definition.to_dict()) - assert restored == definition - restored.validate() - - -def test_workflow_definition_invalid() -> None: - """验证非法 DAG(空名、版本为 0、重复节点、未知边)被拒绝。""" - with pytest.raises(ValueError): - WorkflowDefinition(name="", version=1).validate() - - with pytest.raises(ValueError): - WorkflowDefinition(name="demo", version=0).validate() - - duplicate = WorkflowDefinition( - name="demo", - version=1, - nodes=[WorkflowNode(id="a", node_type="x"), WorkflowNode(id="a", node_type="y")], - ) - with pytest.raises(ValueError): - duplicate.validate() - - unknown_edge = WorkflowDefinition( - name="demo", - version=1, - nodes=[WorkflowNode(id="a", node_type="x")], - edges=[WorkflowEdge(from_node="a", to_node="missing")], - ) - with pytest.raises(ValueError): - unknown_edge.validate() diff --git a/tests/test_nodes.py b/tests/test_nodes.py deleted file mode 100644 index ac2200c..0000000 --- a/tests/test_nodes.py +++ /dev/null @@ -1,1564 +0,0 @@ -"""进程内节点测试。 - -合并原 5 个节点仓库的测试:echo、ffmpeg、whisper、llm、ass。节点已变为 -进程内模块(nodes/*.py),移除了原入口点(__main__/run_node)相关测试。 -""" - -import base64 -import json -import subprocess -import sys -import threading -import types -import urllib.error -import urllib.request -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer -from pathlib import Path - -from nodes.ass import invoke as ass_invoke -from nodes.ass import parse_srt, write_ass -from nodes.echo import invoke as echo_invoke -from nodes.ffmpeg import _ffmpeg_bin, invoke as ffmpeg_invoke -from nodes.llm import invoke as llm_invoke -from nodes.llm import translate_lines -from nodes.whisper import format_timestamp -from nodes.whisper import invoke as whisper_invoke -from nodes.whisper import resolve_model_path -from wov_sdk.models import InvokeRequest - -# 单体根目录:tests/ 的上一级。 -WORKSPACE = Path(__file__).resolve().parent.parent - -# --------------------------------------------------------------------------- -# Echo 节点 -# --------------------------------------------------------------------------- - - -def test_echo_invoke_text(tmp_path) -> None: - """验证直接传入 text 时 echo 节点原样写出文本。""" - response = echo_invoke( - InvokeRequest( - run_id="run_1", - node_instance_id="ni_1", - inputs={"text": "hello echo"}, - output_dir=str(tmp_path), - ) - ) - assert response.status == "completed" - assert response.outputs["text"] == "hello echo" - assert Path(response.outputs["file_uri"]).read_text(encoding="utf-8") == "hello echo" - - -def test_echo_invoke_absolute_file(tmp_path) -> None: - """验证绝对路径 file_uri 的文件内容被读取为输入。""" - source = tmp_path / "input.txt" - source.write_text("from file", encoding="utf-8") - response = echo_invoke( - InvokeRequest( - run_id="run_2", - node_instance_id="ni_2", - inputs={"file_uri": str(source)}, - output_dir=str(tmp_path / "out"), - ) - ) - assert response.status == "completed" - assert response.outputs["text"] == "from file" - - -def test_echo_invoke_relative_file(tmp_path) -> None: - """验证相对路径 file_uri 以单体根目录为基准解析。""" - source = WORKSPACE / "relative_input.txt" - source.write_text("relative", encoding="utf-8") - try: - response = echo_invoke( - InvokeRequest( - run_id="run_3", - node_instance_id="ni_3", - inputs={"file_uri": "relative_input.txt"}, - output_dir=str(tmp_path / "out"), - ) - ) - finally: - source.unlink() - assert response.status == "completed" - assert response.outputs["text"] == "relative" - - -def test_echo_invoke_default_text(tmp_path) -> None: - """验证无任何输入时 echo 节点返回默认文本。""" - response = echo_invoke( - InvokeRequest( - run_id="run_4", - node_instance_id="ni_4", - inputs={}, - output_dir=str(tmp_path), - ) - ) - assert response.status == "completed" - assert response.outputs["text"] == "echo" - - -# --------------------------------------------------------------------------- -# FFmpeg 节点 -# --------------------------------------------------------------------------- - - -def _ffmpeg_request(tmp_path, **overrides) -> InvokeRequest: - """构造包含默认视频输入与提音参数的调用请求。""" - payload = { - "run_id": "run_1", - "node_instance_id": "ni_1", - "inputs": {"video_uri": str(tmp_path / "input.mp4")}, - "params": {"sample_rate": 16000, "channels": 1}, - "output_dir": str(tmp_path / "out"), - } - payload.update(overrides) - return InvokeRequest(**payload) - - -def test_ffmpeg_success(tmp_path, monkeypatch) -> None: - """验证成功调用会生成 audio.wav 产物。""" - def fake_run(command, **kwargs): - output = Path(command[-1]) - output.parent.mkdir(parents=True, exist_ok=True) - output.write_bytes(b"fake wav") - return subprocess.CompletedProcess(command, 0) - - monkeypatch.setattr("nodes.ffmpeg.shutil.which", lambda _: "ffmpeg") - monkeypatch.setattr("nodes.ffmpeg.subprocess.run", fake_run) - response = ffmpeg_invoke(_ffmpeg_request(tmp_path)) - assert response.status == "completed" - assert Path(response.outputs["audio_uri"]).name == "audio.wav" - - -def test_ffmpeg_configured_bin(tmp_path, monkeypatch) -> None: - """验证 FFMPEG_BIN 环境变量优先于 PATH 查找。""" - fake_bin = tmp_path / "ffmpeg.exe" - fake_bin.write_bytes(b"") - monkeypatch.setenv("FFMPEG_BIN", str(fake_bin)) - monkeypatch.setattr("nodes.ffmpeg.subprocess.run", lambda *a, **k: subprocess.CompletedProcess([], 0)) - response = ffmpeg_invoke(_ffmpeg_request(tmp_path)) - assert response.status == "completed" - - -def test_ffmpeg_bundled_fallback(tmp_path, monkeypatch) -> None: - """验证无系统 ffmpeg 时回退到 imageio-ffmpeg 内置二进制。""" - monkeypatch.delenv("FFMPEG_BIN", raising=False) - monkeypatch.setattr("nodes.ffmpeg.shutil.which", lambda _: None) - bundled = _ffmpeg_bin() - assert bundled != "ffmpeg" - assert Path(bundled).is_file() - monkeypatch.setattr("nodes.ffmpeg._ffmpeg_bin", lambda: bundled) - - def fake_run(command, **kwargs): - Path(command[-1]).parent.mkdir(parents=True, exist_ok=True) - Path(command[-1]).write_bytes(b"wav") - return subprocess.CompletedProcess(command, 0) - - monkeypatch.setattr("nodes.ffmpeg.subprocess.run", fake_run) - response = ffmpeg_invoke(_ffmpeg_request(tmp_path)) - assert response.status == "completed" - - -def test_ffmpeg_bundled_import_error(monkeypatch) -> None: - """验证 imageio-ffmpeg 不可用时最终回退为 "ffmpeg" 字符串。""" - monkeypatch.delenv("FFMPEG_BIN", raising=False) - monkeypatch.setattr("nodes.ffmpeg.shutil.which", lambda _: None) - monkeypatch.setitem(sys.modules, "imageio_ffmpeg", None) - assert _ffmpeg_bin() == "ffmpeg" - - -def test_ffmpeg_missing_video_uri(tmp_path) -> None: - """验证缺少 video_uri 时返回失败。""" - response = ffmpeg_invoke(_ffmpeg_request(tmp_path, inputs={})) - assert response.status == "failed" - assert "video_uri" in response.error - - -def test_ffmpeg_missing_bin(tmp_path, monkeypatch) -> None: - """验证找不到任何 ffmpeg 时返回明确失败信息。""" - monkeypatch.delenv("FFMPEG_BIN", raising=False) - monkeypatch.setattr("nodes.ffmpeg.shutil.which", lambda _: None) - monkeypatch.setattr("nodes.ffmpeg._bundled_ffmpeg", lambda: None) - response = ffmpeg_invoke(_ffmpeg_request(tmp_path)) - assert response.status == "failed" - assert "ffmpeg not found" in response.error - - -def test_ffmpeg_failure(tmp_path, monkeypatch) -> None: - """验证 ffmpeg 返回非零退出码时透传 stderr 错误。""" - def fake_run(command, **kwargs): - return subprocess.CompletedProcess(command, 1, stderr="boom") - - monkeypatch.setattr("nodes.ffmpeg.shutil.which", lambda _: "ffmpeg") - monkeypatch.setattr("nodes.ffmpeg.subprocess.run", fake_run) - response = ffmpeg_invoke(_ffmpeg_request(tmp_path)) - assert response.status == "failed" - assert "boom" in response.error - - -# --------------------------------------------------------------------------- -# Whisper 节点 -# --------------------------------------------------------------------------- - - -class FakeSegment: - """模拟 faster-whisper 的分段对象,只提供转写测试需要的字段。""" - - def __init__(self, start, end, text): - self.start = start - self.end = end - self.text = text - - -class FakeWhisperModel: - """记录构造参数并返回固定分段的假 WhisperModel。""" - - instances: list[tuple[tuple, dict]] = [] - - def __init__(self, *args, **kwargs): - # 记录每次构造参数,测试据此断言 device/compute_type 传递。 - FakeWhisperModel.instances.append((args, kwargs)) - self.args = args - self.kwargs = kwargs - - def transcribe(self, path, **kwargs): - # 返回固定两个分段:一个普通时长,一个跨小时验证时间戳格式。 - return ( - [ - FakeSegment(0, 1, "第一段"), - FakeSegment(3600.5, 3602.25, "第二段"), - ], - None, - ) - - -def _install_fake_whisper(monkeypatch, model_class=FakeWhisperModel) -> None: - """把假 faster_whisper 模块注入 sys.modules,替代真实依赖。""" - fake_module = types.SimpleNamespace(WhisperModel=model_class) - monkeypatch.setitem(sys.modules, "faster_whisper", fake_module) - - -def _whisper_request(tmp_path, **overrides) -> InvokeRequest: - """构造默认音频输入与日语参数的调用请求。""" - payload = { - "run_id": "run_1", - "node_instance_id": "ni_1", - "inputs": {"audio_uri": str(tmp_path / "audio.wav")}, - "params": {"language": "ja"}, - "output_dir": str(tmp_path / "out"), - } - payload.update(overrides) - return InvokeRequest(**payload) - - -def test_resolve_explicit_param_wins(tmp_path) -> None: - """验证请求参数中的 model_path 优先级最高,覆盖环境变量与本地候选。""" - local = tmp_path / "model" - local.mkdir() - (local / "model.bin").write_bytes(b"x") - resolved = resolve_model_path( - {"model_path": "/opt/custom-model"}, - env={"WHISPER_MODEL_PATH": "/env/model"}, - candidates=[local], - ) - assert resolved == "/opt/custom-model" - - -def test_resolve_env_wins_over_local(tmp_path) -> None: - """验证 WHISPER_MODEL_PATH 环境变量优先于本地候选目录。""" - local = tmp_path / "model" - local.mkdir() - (local / "model.bin").write_bytes(b"x") - resolved = resolve_model_path( - {}, - env={"WHISPER_MODEL_PATH": "/env/model"}, - candidates=[local], - ) - assert resolved == "/env/model" - - -def test_resolve_local_candidate_used(tmp_path) -> None: - """验证无参数与环境变量时优先使用含 model.bin 的本地候选目录。""" - local = tmp_path / "model" - local.mkdir() - (local / "model.bin").write_bytes(b"x") - resolved = resolve_model_path({}, env={}, candidates=[local]) - assert resolved == str(local) - - -def test_resolve_incomplete_candidate_skipped(tmp_path) -> None: - """验证缺少 model.bin 的候选目录被跳过,避免加载残缺模型。""" - empty = tmp_path / "empty" - empty.mkdir() - resolved = resolve_model_path({}, env={}, candidates=[empty]) - assert resolved == "large-v2" - - -def test_resolve_fallback_remote() -> None: - """验证全部本地候选缺失时回退到远端 large-v2 作为最后兜底。""" - resolved = resolve_model_path({}, env={}, candidates=[]) - assert resolved == "large-v2" - - -def test_format_timestamp() -> None: - """验证秒数到 SRT 时间戳的格式化结果。""" - assert format_timestamp(0) == "00:00:00,000" - assert format_timestamp(3600.5) == "01:00:00,500" - assert format_timestamp(61.25) == "00:01:01,250" - - -def test_whisper_success(tmp_path, monkeypatch) -> None: - """验证成功转写会生成 SRT 并默认使用 auto 设备、float16 计算类型。""" - FakeWhisperModel.instances.clear() - _install_fake_whisper(monkeypatch) - _make_wav(tmp_path / "audio.wav", 5) - response = whisper_invoke(_whisper_request(tmp_path)) - assert response.status == "completed" - content = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8") - assert "第一段" in content - assert "01:00:00,500 --> 01:00:02,250" in content - _, kwargs = FakeWhisperModel.instances[-1] - assert kwargs["device"] == "auto" - assert kwargs["compute_type"] == "float16" - - -def test_whisper_pause_flag_stops_between_chunks(tmp_path, monkeypatch) -> None: - """验证 paused.flag 存在时 whisper 在分块边界中止(批量暂停机制)。 - - run 根目录(output_dir 的上上级)的 paused.flag 由暂停接口写入,whisper - 在每个分块转写前检查;检测到即抛异常,invoke 统一转 failed 响应,调度器 - 捕获后保持任务 PAUSED。 - """ - _install_fake_whisper(monkeypatch) - _make_wav(tmp_path / "audio.wav", 5) - # 暂停信号位于 run 根目录:output_dir 为 runs/run_p/steps/whisper, - # 其上上级即 runs/run_p(与调度器/OCR 的目录约定一致)。 - (tmp_path / "runs" / "run_p").mkdir(parents=True) - (tmp_path / "runs" / "run_p" / "paused.flag").write_text("", encoding="utf-8") - # 注入两个分块路径,确保进入分块循环并执行至少一次暂停检查。 - monkeypatch.setattr( - "nodes.whisper._split_audio", - lambda *args, **kwargs: [tmp_path / "chunk_1.wav", tmp_path / "chunk_2.wav"], - ) - response = whisper_invoke( - _whisper_request( - tmp_path, - params={"language": "ja", "chunk_seconds": 60}, - output_dir=str(tmp_path / "runs" / "run_p" / "steps" / "whisper"), - ) - ) - assert response.status == "failed" - assert "被暂停" in response.error - # 暂停时不会写出 SRT 产物。 - assert not (tmp_path / "runs" / "run_p" / "steps" / "whisper" / "transcript.srt").exists() - -def test_load_cuda_libraries_linux(monkeypatch) -> None: - """验证 Linux 下进程内预加载 nvidia 动态库,含失败跳过分支。""" - import ctypes - - from nodes.whisper import _load_cuda_libraries - - # 用真实 nvidia 轮子路径加载,不应抛出异常。 - _load_cuda_libraries() - - # 模拟加载失败分支:部分库抛 OSError 时应被跳过。 - calls: list[str] = [] - real_cdll = ctypes.CDLL - - def fake_cdll(path): - calls.append(str(path)) - if "cudnn" in str(path): - raise OSError("boom") - return real_cdll(path) - - monkeypatch.setattr("nodes.whisper.ctypes.CDLL", fake_cdll) - _load_cuda_libraries() - assert calls - - -def test_load_cuda_libraries_windows(monkeypatch, tmp_path) -> None: - """验证 Windows 分支通过 add_dll_directory 注册 DLL 搜索目录。""" - import os - import sysconfig - - from nodes.whisper import _load_cuda_libraries - - site = tmp_path / "site" - (site / "nvidia" / "cublas" / "bin").mkdir(parents=True) - (site / "nvidia" / "cudnn" / "bin").mkdir(parents=True) - monkeypatch.setattr(sysconfig, "get_paths", lambda: {"purelib": str(site)}) - added: list[str] = [] - # 只注入平台判断与 DLL 目录注册,不改动全局 os.name,避免 pathlib 出错。 - monkeypatch.setattr("nodes.whisper._is_windows", lambda: True) - monkeypatch.setattr(os, "add_dll_directory", lambda d: added.append(d), raising=False) - _load_cuda_libraries() - assert any("cublas" in d and d.endswith("bin") for d in added) -def test_whisper_compute_type_override(tmp_path, monkeypatch) -> None: - """验证请求参数可以覆盖默认计算类型。""" - FakeWhisperModel.instances.clear() - _install_fake_whisper(monkeypatch) - _make_wav(tmp_path / "audio.wav", 5) - response = whisper_invoke( - _whisper_request(tmp_path, params={"language": "ja", "compute_type": "int8"}) - ) - assert response.status == "completed" - _, kwargs = FakeWhisperModel.instances[-1] - assert kwargs["compute_type"] == "int8" - - -def test_whisper_model_raises(tmp_path, monkeypatch) -> None: - """验证模型加载失败时返回 failed 与错误信息。""" - class BrokenModel: - def __init__(self, *args, **kwargs): - raise RuntimeError("model load failed") - - _install_fake_whisper(monkeypatch, BrokenModel) - _make_wav(tmp_path / "audio.wav", 5) - response = whisper_invoke(_whisper_request(tmp_path)) - assert response.status == "failed" - assert "model load failed" in response.error - - -def test_whisper_missing_input(tmp_path) -> None: - """验证缺少 audio_uri 时返回失败。""" - response = whisper_invoke(_whisper_request(tmp_path, inputs={})) - assert response.status == "failed" - - -def test_whisper_missing_file(tmp_path) -> None: - """验证音频文件不存在时返回失败。""" - response = whisper_invoke(_whisper_request(tmp_path)) - assert response.status == "failed" - assert "audio file not found" in response.error - - -# --------------------------------------------------------------------------- -# LLM 节点 -# --------------------------------------------------------------------------- - - -class FakeUrlOpenResponse: - """模拟 urllib 响应对象,提供固定 LLM 译文内容。""" - - def __init__(self, content: str) -> None: - # 预编码为 Chat Completions 风格的 JSON 响应体。 - self._payload = json.dumps( - {"choices": [{"message": {"content": content}}]} - ).encode("utf-8") - - def read(self) -> bytes: - return self._payload - - def __enter__(self): - return self - - def __exit__(self, *args) -> bool: - return False - - -def _make_srt(tmp_path, count=5) -> Path: - """生成标准 SRT 测试文件,文本行为"原文字幕N"。""" - lines = [] - for index in range(count): - lines.extend( - [ - str(index + 1), - f"00:00:{index:02d},000 --> 00:00:{index + 1:02d},000", - f"原文字幕{index + 1}", - "", - ] - ) - path = tmp_path / "in.srt" - path.write_text("\n".join(lines), encoding="utf-8") - return path - - -def test_llm_translate_lines_via_fake_api(monkeypatch) -> None: - """验证通过真实 HTTP 服务器调用 LLM 接口并保持行顺序。""" - class Handler(BaseHTTPRequestHandler): - def do_POST(self) -> None: - length = int(self.headers.get("Content-Length", "0")) - self.rfile.read(length) - body = json.dumps( - { - "choices": [ - { - "message": { - "content": json.dumps([{"id": i, "text": text} for i, text in enumerate(["译文一", "译文二", "译文三", "译文四", "译文五"], 1)]) - } - } - ] - } - ).encode("utf-8") - self.send_response(200) - self.send_header("Content-Type", "application/json") - self.send_header("Content-Length", str(len(body))) - self.end_headers() - self.wfile.write(body) - - def log_message(self, format, *args) -> None: - return - - server = ThreadingHTTPServer(("127.0.0.1", 0), Handler) - thread = threading.Thread(target=server.serve_forever, daemon=True) - thread.start() - try: - monkeypatch.setenv( - "LLM_API_BASE", - f"http://127.0.0.1:{server.server_address[1]}/v1/chat/completions", - ) - monkeypatch.setenv("LLM_API_KEY", "test-key") - result = translate_lines( - ["一", "二", "三", "四", "五"], - {"target_language": "zh-CN"}, - ) - assert result == ["译文一", "译文二", "译文三", "译文四", "译文五"] - finally: - server.shutdown() - server.server_close() - thread.join(timeout=5) - - -def test_llm_translate_lines_empty_input() -> None: - """验证空行列表直接返回空译文,不发起任何 LLM 请求。 - - 翻译开始前对空输入提前返回:没有字幕行时无需计算批数、也无需调用 - 接口(避免无谓请求与除零等边界问题),同时保持返回类型为列表。""" - result = translate_lines([], {}) - assert result == [] - - -def test_llm_translate_lines_api_error(monkeypatch) -> None: - """验证 LLM 接口不可用时抛出 URLError。""" - def fail_open(request, timeout): - raise urllib.error.URLError("api down") - - monkeypatch.setattr("nodes.llm.urllib.request.urlopen", fail_open) - try: - translate_lines(["一"], {}) - raise AssertionError("expected failure") - except urllib.error.URLError: - pass - - -def test_llm_translate_lines_default_timeout(monkeypatch) -> None: - """验证未配置超时时使用默认 600 秒。""" - captured = {} - - def fake_open(request, timeout): - captured["timeout"] = timeout - return FakeUrlOpenResponse('[{"id": 1, "text": "译文一"}]') - - monkeypatch.setattr("nodes.llm.urllib.request.urlopen", fake_open) - monkeypatch.setenv("LLM_API_BASE", "http://fake/v1/chat/completions") - - result = translate_lines(["一"], {}) - - assert result == ["译文一"] - assert captured["timeout"] == 600 - - -def test_llm_translate_lines_env_timeout(monkeypatch) -> None: - """验证 LLM_TIMEOUT_SECONDS 环境变量可覆盖超时。""" - captured = {} - - def fake_open(request, timeout): - captured["timeout"] = timeout - return FakeUrlOpenResponse('[{"id": 1, "text": "译文一"}]') - - monkeypatch.setattr("nodes.llm.urllib.request.urlopen", fake_open) - monkeypatch.setenv("LLM_API_BASE", "http://fake/v1/chat/completions") - monkeypatch.setenv("LLM_TIMEOUT_SECONDS", "45") - - translate_lines(["一"], {}) - - assert captured["timeout"] == 45 - - -def test_llm_invoke_success(tmp_path, monkeypatch) -> None: - """验证成功调用会把译文回填到 SRT 并输出 cn.srt。""" - source = _make_srt(tmp_path) - - def fake_translate(lines, params): - return [f"译文{i + 1}" for i in range(len(lines))] - - monkeypatch.setattr("nodes.llm.translate_lines", fake_translate) - response = llm_invoke( - InvokeRequest( - run_id="run_1", - node_instance_id="ni_1", - inputs={"srt_uri": str(source)}, - params={"target_language": "zh-CN"}, - output_dir=str(tmp_path / "out"), - ) - ) - assert response.status == "completed" - content = Path(response.outputs["cn_srt_uri"]).read_text(encoding="utf-8") - assert "译文1" in content - - -def test_llm_invoke_rejects_short_translation(tmp_path, monkeypatch) -> None: - """防御性校验:译文条数不足时失败,不用空行掩盖不完整结果。""" - source = _make_srt(tmp_path, count=3) - monkeypatch.setattr( - "nodes.llm.translate_lines", - lambda lines, params: ["only one"], - ) - response = llm_invoke( - InvokeRequest( - run_id="run_2", - node_instance_id="ni_2", - inputs={"srt_uri": str(source)}, - output_dir=str(tmp_path / "out2"), - ) - ) - assert response.status == "failed" - - -def test_llm_invoke_missing_input(tmp_path) -> None: - """验证缺少 srt_uri 时返回失败。""" - response = llm_invoke( - InvokeRequest( - run_id="run_3", - node_instance_id="ni_3", - inputs={}, - output_dir=str(tmp_path), - ) - ) - assert response.status == "failed" - - -def test_llm_invoke_missing_file(tmp_path) -> None: - """验证 SRT 文件不存在时返回失败。""" - response = llm_invoke( - InvokeRequest( - run_id="run_4", - node_instance_id="ni_4", - inputs={"srt_uri": str(tmp_path / "missing.srt")}, - output_dir=str(tmp_path), - ) - ) - assert response.status == "failed" - - -# --------------------------------------------------------------------------- -# ASS 节点 -# --------------------------------------------------------------------------- - - -SAMPLE_SRT = """ -1 -00:00:01,000 --> 00:00:03,000 -第一行 -第二行 - -2 -00:00:04,000 --> 00:00:06,000 -第三行 -""" - - -def test_ass_parse_and_write(tmp_path) -> None: - """验证多行字幕会被解析并通过左右眼样式写出。""" - entries = parse_srt(SAMPLE_SRT) - assert len(entries) == 2 - assert entries[0][2] == r"第一行\N第二行" - - output = tmp_path / "out.ass" - write_ass(entries, output, "3840x1920") - content = output.read_text(encoding="utf-8") - assert "PlayResX: 3840" in content - assert "PlayResY: 1920" in content - assert "LeftEye" in content - assert "RightEye" in content - assert r"第一行\N第二行" in content - - -def test_ass_top_aligned_and_translucent(tmp_path) -> None: - """验证字幕默认渲染到顶部安全区且文字/描边带透明度。 - - B-1:对齐 an8(顶部居中),MarginV 取顶部安全边距默认 700(2026-09 起, - 字幕置于视线自然可读位置;旧默认 120 落在画面最顶需抬头观看); - 透明度:文字填充 &HB3FFFFFF(约 70% 透明),描边 &H80000000(半透明黑), - 而左右眼水平相对位置一致保持零视差(A-1,字幕在屏幕平面)。""" - entries = parse_srt(SAMPLE_SRT) - output = tmp_path / "out.ass" - write_ass(entries, output, "3840x1920") - content = output.read_text(encoding="utf-8") - # 顶部对齐 an8、顶部安全边距默认 700。 - assert r"{\an8}" in content - assert r"{\an2}" not in content - # 样式行:MarginV=700(顶部安全区),填充 &HB3FFFFFF,描边 &H80000000。 - assert "&HB3FFFFFF" in content - assert "&H80000000" in content - assert ",0,0,0,0,50,100,0,0,1,4,0,8,50,1920,700,1" in content - # 左右眼两行文本一致(水平相对位置相同 → 零视差/A-1)。 - assert content.count(r"第一行\N第二行") == 2 - - -def test_ass_write_custom_margin_top(tmp_path) -> None: - """验证可通过 margin_top 覆盖顶部安全边距(不同分辨率下微调)。""" - entries = parse_srt(SAMPLE_SRT) - output = tmp_path / "out.ass" - write_ass(entries, output, "1920x1080", margin_top=200) - content = output.read_text(encoding="utf-8") - # 1920 宽:每半幅 960;MarginV 用自定义 200。 - assert ",0,0,0,0,50,100,0,0,1,4,0,8,50,960,200,1" in content - - -def test_ass_invoke_reads_margin_top(tmp_path) -> None: - """验证 invoke 从请求参数读取 margin_top 并反映到产物样式。""" - source = tmp_path / "in.srt" - source.write_text(SAMPLE_SRT, encoding="utf-8") - response = ass_invoke( - InvokeRequest( - run_id="run_mt", - node_instance_id="ni_mt", - inputs={"cn_srt_uri": str(source)}, - params={"resolution": "1920x1080", "margin_top": 90}, - output_dir=str(tmp_path / "out"), - ) - ) - assert response.status == "completed" - content = Path(response.outputs["ass_uri"]).read_text(encoding="utf-8") - assert "an8" in content - assert ",0,0,0,0,50,100,0,0,1,4,0,8,50,960,90,1" in content - - -def test_ass_invoke_default_margin_top_is_700(tmp_path) -> None: - """验证 invoke 未显式传 margin_top 时默认生成 MarginV=700 的样式。 - - 保证之后生成的字幕默认都落在顶部安全区下方(2026-09 起), - 不再回退到旧的 120(需抬头观看)。""" - source = tmp_path / "in.srt" - source.write_text(SAMPLE_SRT, encoding="utf-8") - response = ass_invoke( - InvokeRequest( - run_id="run_def700", - node_instance_id="ni_def700", - inputs={"cn_srt_uri": str(source)}, - params={"resolution": "1920x1080"}, - output_dir=str(tmp_path / "out"), - ) - ) - assert response.status == "completed" - content = Path(response.outputs["ass_uri"]).read_text(encoding="utf-8") - assert "an8" in content - assert ",0,0,0,0,50,100,0,0,1,4,0,8,50,960,700,1" in content - - -def test_ass_parse_malformed(tmp_path) -> None: - """验证畸形 SRT 不会抛出异常且返回空条目或忽略坏行。""" - source = tmp_path / "bad.srt" - source.write_text("1\nnot a time line\n", encoding="utf-8") - assert parse_srt(source.read_text(encoding="utf-8")) == [] - - source.write_text("1", encoding="utf-8") - assert parse_srt(source.read_text(encoding="utf-8")) == [] - - -def test_ass_invoke_success(tmp_path) -> None: - """验证成功调用会按指定分辨率输出 ASS 产物。""" - source = tmp_path / "in.srt" - source.write_text(SAMPLE_SRT, encoding="utf-8") - response = ass_invoke( - InvokeRequest( - run_id="run_1", - node_instance_id="ni_1", - inputs={"cn_srt_uri": str(source)}, - params={"resolution": "1920x1080"}, - output_dir=str(tmp_path / "out"), - ) - ) - assert response.status == "completed" - assert "PlayResX: 1920" in Path(response.outputs["ass_uri"]).read_text(encoding="utf-8") - - -def test_ass_invoke_missing_input(tmp_path) -> None: - """验证缺少 cn_srt_uri 时返回失败。""" - response = ass_invoke( - InvokeRequest( - run_id="run_2", - node_instance_id="ni_2", - inputs={}, - output_dir=str(tmp_path), - ) - ) - assert response.status == "failed" - - -def test_ass_invoke_missing_file(tmp_path) -> None: - """验证 SRT 文件不存在时返回失败。""" - response = ass_invoke( - InvokeRequest( - run_id="run_3", - node_instance_id="ni_3", - inputs={"cn_srt_uri": str(tmp_path / "missing.srt")}, - output_dir=str(tmp_path), - ) - ) - assert response.status == "failed" - - -def test_resolve_bare_name_found(tmp_path) -> None: - """验证裸模型名会在候选目录父级下按名解析到本地模型。""" - named = tmp_path / "zh-ct2" - named.mkdir() - (named / "model.bin").write_bytes(b"x") - # 候选目录取父目录的兄弟布局:candidates 首项父级即模型根目录。 - candidates = [tmp_path / "models"] - resolved = resolve_model_path({"model_path": "zh-ct2"}, env={}, candidates=candidates) - assert resolved == str(named) - - -def test_resolve_bare_name_missing(tmp_path) -> None: - """验证裸模型名在本地不存在时原样返回,交由 faster-whisper 处理。""" - resolved = resolve_model_path( - {"model_path": "no-such-model"}, env={}, candidates=[tmp_path / "models"] - ) - assert resolved == "no-such-model" - - -def test_whisper_task_translate(tmp_path, monkeypatch) -> None: - """验证 task=translate 参数会传递给 faster-whisper 的 transcribe。""" - class TaskRecorderModel: - def __init__(self, *args, **kwargs): - self.transcribe_kwargs = None - - def transcribe(self, path, **kwargs): - self.transcribe_kwargs = kwargs - return ( - [FakeSegment(0, 1, "中文直出")], - None, - ) - - recorder = TaskRecorderModel() - monkeypatch.setitem(sys.modules, "faster_whisper", types.SimpleNamespace(WhisperModel=lambda *a, **k: recorder)) - _make_wav(tmp_path / "audio.wav", 5) - response = whisper_invoke( - _whisper_request(tmp_path, params={"language": "ja", "task": "translate"}) - ) - assert response.status == "completed" - assert recorder.transcribe_kwargs["task"] == "translate" - assert "中文直出" in Path(response.outputs["srt_uri"]).read_text(encoding="utf-8") - - -def test_whisper_condition_on_previous_text(tmp_path, monkeypatch) -> None: - """验证长音频参数 condition_on_previous_text 可配置并默认关闭。""" - captured = {} - - class CondRecorderModel: - def __init__(self, *args, **kwargs): - pass - - def transcribe(self, path, **kwargs): - captured["condition_on_previous_text"] = kwargs.get("condition_on_previous_text") - return ([FakeSegment(0, 1, "ok")], None) - - monkeypatch.setitem( - sys.modules, - "faster_whisper", - types.SimpleNamespace(WhisperModel=lambda *a, **k: CondRecorderModel()), - ) - _make_wav(tmp_path / "audio.wav", 5) - # 默认 False(长音频稳定);显式传 True 可开启。 - whisper_invoke(_whisper_request(tmp_path)) - assert captured["condition_on_previous_text"] is False - whisper_invoke(_whisper_request(tmp_path, params={"condition_on_previous_text": True})) - assert captured["condition_on_previous_text"] is True - - -def test_split_audio_disabled_or_no_ffmpeg(tmp_path, monkeypatch) -> None: - """验证 chunk_seconds<=0 或缺少 ffmpeg 时回退整段,不调用切块。""" - from nodes.whisper import _split_audio - - audio = _make_wav(tmp_path / "in.wav", 5) - called = [] - - def fake_run(*args, **kwargs): - called.append(args) - return subprocess.CompletedProcess([], 0) - - monkeypatch.setattr("nodes.whisper.subprocess.run", fake_run) - # chunk_seconds<=0:直接返回整段,不执行 ffmpeg。 - assert _split_audio(audio, tmp_path, 0, "ffmpeg") == [audio] - # 缺少 ffmpeg:直接返回整段。 - assert _split_audio(audio, tmp_path, 600, None) == [audio] - assert called == [] - - -def test_split_audio_failure_fallback(tmp_path, monkeypatch) -> None: - """验证 ffmpeg 切块失败时回退整段单次转写。""" - from nodes.whisper import _split_audio - - audio = _make_wav(tmp_path / "in.wav", 5) - monkeypatch.setattr( - "nodes.whisper.subprocess.run", - lambda *a, **k: subprocess.CompletedProcess([], 1, stderr="boom"), - ) - assert _split_audio(audio, tmp_path, 600, "ffmpeg") == [audio] - - -def test_split_audio_success_and_empty(tmp_path, monkeypatch) -> None: - """验证切块成功返回块列表;产出为空时回退整段。""" - from nodes.whisper import _split_audio - - audio = _make_wav(tmp_path / "in.wav", 5) - - def fake_run_success(command, **kwargs): - # 模拟 ffmpeg 产出两个真实的块 WAV。 - pattern = command[-1] - for name in ("chunk_000.wav", "chunk_001.wav"): - _make_wav(tmp_path / "chunks" / name, 2) - return subprocess.CompletedProcess(command, 0) - - monkeypatch.setattr("nodes.whisper.subprocess.run", fake_run_success) - chunks = _split_audio(audio, tmp_path, 600, "ffmpeg") - assert len(chunks) == 2 - assert chunks[0].name == "chunk_000.wav" - - # 切块成功但没有产出文件时回退整段。 - monkeypatch.setattr( - "nodes.whisper.subprocess.run", - lambda *a, **k: subprocess.CompletedProcess([], 0), - ) - assert _split_audio(audio, tmp_path / "other", 600, "ffmpeg") == [audio] - - -def test_whisper_chunked_transcription_merges_offsets(tmp_path, monkeypatch) -> None: - """验证分块转写用真实 WAV 块,第二块时间轴按实际时长偏移合并到同一 SRT。""" - from nodes.whisper import _split_audio - - chunk_dir = tmp_path / "out" / "chunks" - chunk_dir.mkdir(parents=True) - # 真实 WAV 块(各 60s),偏移按实际时长累积为 60s。 - chunk1 = _make_wav(chunk_dir / "chunk_000.wav", 60) - chunk2 = _make_wav(chunk_dir / "chunk_001.wav", 60) - monkeypatch.setattr("nodes.whisper._split_audio", lambda a, o, c, f: [chunk1, chunk2]) - FakeWhisperModel.instances.clear() - _install_fake_whisper(monkeypatch) - _make_wav(tmp_path / "audio.wav", 5) - response = whisper_invoke(_whisper_request(tmp_path, params={"chunk_seconds": 60})) - assert response.status == "completed" - content = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8") - # 序号从 1 开始连续递增;第一块无偏移,第二块偏移 60 秒。 - assert content.startswith("1\n") - assert "3\n00:01:00,000" in content - assert "00:00:00,000 --> 00:00:01,000" in content - assert "01:00:00,500 --> 01:00:02,250" in content - assert "00:01:00,000 --> 00:01:01,000" in content - assert "01:01:00,500 --> 01:01:02,250" in content - - -def test_whisper_chunk_disabled_single_call(tmp_path, monkeypatch) -> None: - """验证 chunk_seconds=0 时单次调用、不切块。""" - FakeWhisperModel.instances.clear() - _install_fake_whisper(monkeypatch) - _make_wav(tmp_path / "audio.wav", 5) - response = whisper_invoke(_whisper_request(tmp_path, params={"chunk_seconds": 0})) - assert response.status == "completed" - # 单次调用:init 只记录一次实例。 - assert len(FakeWhisperModel.instances) == 1 - - -def test_whisper_vad_filter_default_off(tmp_path, monkeypatch) -> None: - """验证 vad_filter 默认开启,可显式关闭。""" - captured = {} - - class VadRecorderModel: - def __init__(self, *args, **kwargs): - pass - - def transcribe(self, path, **kwargs): - captured["vad_filter"] = kwargs.get("vad_filter") - return ([FakeSegment(0, 1, "ok")], None) - - monkeypatch.setitem( - sys.modules, - "faster_whisper", - types.SimpleNamespace(WhisperModel=lambda *a, **k: VadRecorderModel()), - ) - _make_wav(tmp_path / "audio.wav", 5) - whisper_invoke(_whisper_request(tmp_path)) - assert captured["vad_filter"] is True - whisper_invoke(_whisper_request(tmp_path, params={"vad_filter": False})) - assert captured["vad_filter"] is False - - -def _make_wav(path, seconds, rate=16000) -> Path: - """生成指定时长的 16kHz 单声道 16bit 静音 WAV。""" - import wave - - with wave.open(str(path), "wb") as wav: - wav.setnchannels(1) - wav.setsampwidth(2) - wav.setframerate(rate) - wav.writeframes(b"\x00\x00" * int(rate * seconds)) - return path - - -def test_whisper_chunk_offset_uses_actual_duration(tmp_path, monkeypatch) -> None: - """验证分块偏移按 WAV 实际时长累积(1s+2s 块 → 第二块偏移 1s 而非块长 60s)。""" - chunk_dir = tmp_path / "out" / "chunks" - chunk_dir.mkdir(parents=True) - chunk1 = _make_wav(chunk_dir / "chunk_000.wav", 1) - chunk2 = _make_wav(chunk_dir / "chunk_001.wav", 2) - monkeypatch.setattr("nodes.whisper._split_audio", lambda a, o, c, f: [chunk1, chunk2]) - FakeWhisperModel.instances.clear() - _install_fake_whisper(monkeypatch) - _make_wav(tmp_path / "audio.wav", 5) - response = whisper_invoke(_whisper_request(tmp_path, params={"chunk_seconds": 60})) - assert response.status == "completed" - content = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8") - # 第二块偏移 = 第一块实际时长 1s(若用块长假设则会是 60s → 00:01:00)。 - assert "00:00:01,000 --> 00:00:02,000" in content - assert "00:01:00,000 --> 00:01:01,000" not in content - - -def test_whisper_segment_logs_full_video_time(caplog, tmp_path, monkeypatch) -> None: - """验证每条分段日志包含编号与完整视频角度(含分块偏移)的时间范围。""" - chunk_dir = tmp_path / "out" / "chunks" - chunk_dir.mkdir(parents=True) - chunk1 = _make_wav(chunk_dir / "chunk_000.wav", 60) - chunk2 = _make_wav(chunk_dir / "chunk_001.wav", 60) - monkeypatch.setattr("nodes.whisper._split_audio", lambda a, o, c, f: [chunk1, chunk2]) - FakeWhisperModel.instances.clear() - _install_fake_whisper(monkeypatch) - _make_wav(tmp_path / "audio.wav", 5) - with caplog.at_level("INFO", logger="vrsub.whisper"): - response = whisper_invoke(_whisper_request(tmp_path, params={"chunk_seconds": 60})) - assert response.status == "completed" - seg_logs = [r.message for r in caplog.records if r.message.startswith("分段 #")] - # 第一块:0s 起;第二块:偏移 60s(第一块实际时长)。 - assert any("分段 #1: 00:00:00,000 --> 00:00:01,000" in m for m in seg_logs) - assert any(m.startswith("分段 #3: 00:01:00,000") for m in seg_logs) - -def test_whisper_auto_vad_fallback_on_error(tmp_path, monkeypatch) -> None: - """自动 VAD 分析抛异常时,whisper 回退默认参数正常转写(防御性)。""" - import nodes.whisper as whisper_mod - - def _boom(*args, **kwargs): - raise RuntimeError("auto vad analysis failed") - - # 使 nodes.vad_profiler.vad_parameters_for_audio 抛异常(whisper.py 函数内 import)。 - monkeypatch.setattr( - "nodes.vad_profiler.vad_parameters_for_audio", _boom, raising=False, - ) - _install_fake_whisper(monkeypatch) - _make_wav(tmp_path / "audio.wav", 5) - response = whisper_invoke( - _whisper_request(tmp_path, params={"language": "ja", "vad_filter": True, "sample_rate": 16000}) - ) - assert response.status == "completed" - # 回退默认:transcribe 收到 vad_parameters=None。 - _, kwargs = FakeWhisperModel.instances[-1] - assert kwargs.get("vad_parameters") is None - - -def test_wav_duration_fallback_on_invalid_file(tmp_path) -> None: - """验证读取时长时,损坏/缺失文件回退 fallback 值(真实非法文件,非占位字节)。""" - from nodes.whisper import _wav_duration_seconds - - bad = tmp_path / "bad.wav" - bad.write_text("this is not a wav file", encoding="utf-8") - assert _wav_duration_seconds(bad, 60.0) == 60.0 - missing = tmp_path / "missing.wav" - assert _wav_duration_seconds(missing, 60.0) == 60.0 - - -# --------------------------------------------------------------------------- -# VLM OCR 节点(直接请求 Ollama /api/generate,流式) -# --------------------------------------------------------------------------- - - -# 真实的最小 PNG(1x1 像素,合法文件),用于构造真实图片输入。 -_MIN_PNG_BASE64 = ( - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8" - "z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==" -) - - -class FakeChatResponse: - """模拟 Ollama /api/generate 流式响应:readline() 逐行返回 JSON 块。 - - done=False(默认)时仅一行内容,随后 readline 返回空表示流结束; - done=True 时末尾追加一行带 done 标记的结束块。 - """ - - def __init__(self, content: str, done: bool = False) -> None: - self._lines = [ - json.dumps({"message": {"role": "assistant", "content": content}}).encode() - ] - if done: - self._lines.append( - json.dumps({"message": {"role": "assistant", "content": ""}, "done": True}).encode() - ) - - def readline(self) -> bytes: - return self._lines.pop(0) if self._lines else b"" - - def __enter__(self): - return self - - def __exit__(self, *args) -> bool: - return False - - -def _real_png(tmp_path) -> Path: - """生成真实 PNG 图片文件(解码自合法 base64)。""" - image = tmp_path / "frame.png" - image.write_bytes(base64.b64decode(_MIN_PNG_BASE64)) - return image - - -def test_vlm_success(tmp_path, monkeypatch) -> None: - """验证真实图片经流式读取后输出清洗过的文字与 ocr.txt 产物。""" - from nodes.vlm import invoke as vlm_invoke - - image = _real_png(tmp_path) - captured = {} - - def fake_urlopen(request, timeout): - captured["timeout"] = timeout - captured["url"] = request.full_url - return FakeChatResponse("HELLO WORLD 123\n```markdown\n```\n```\n") - - monkeypatch.setattr("nodes.vlm.urllib.request.urlopen", fake_urlopen) - response = vlm_invoke( - InvokeRequest( - run_id="run_1", - node_instance_id="", - inputs={"image_uri": str(image)}, - params={"model": "glm-ocr:latest"}, - output_dir=str(tmp_path / "out"), - ) - ) - assert response.status == "completed", response.error - assert response.outputs["text"] == "HELLO WORLD 123" - assert captured["url"].endswith("/api/chat") - assert captured["timeout"] == 5 - content = Path(response.outputs["text_uri"]).read_text(encoding="utf-8") - assert "HELLO WORLD 123" in content - - -def test_vlm_missing_input_and_file(tmp_path) -> None: - """验证缺少 image_uri 或图片不存在时返回失败。""" - from nodes.vlm import invoke as vlm_invoke - - assert vlm_invoke( - InvokeRequest(run_id="r", node_instance_id="", inputs={}, output_dir=str(tmp_path)) - ).status == "failed" - assert vlm_invoke( - InvokeRequest( - run_id="r", node_instance_id="", - inputs={"image_uri": str(tmp_path / "missing.png")}, - output_dir=str(tmp_path), - ) - ).status == "failed" - - -def test_vlm_network_error(tmp_path, monkeypatch) -> None: - """验证 Ollama 服务不可达时返回 failed。""" - from nodes.vlm import invoke as vlm_invoke - - image = _real_png(tmp_path) - - def fail_open(request, timeout): - raise urllib.error.URLError("ollama down") - - monkeypatch.setattr("nodes.vlm.urllib.request.urlopen", fail_open) - response = vlm_invoke( - InvokeRequest( - run_id="r", node_instance_id="", - inputs={"image_uri": str(image)}, - output_dir=str(tmp_path), - ) - ) - assert response.status == "failed" - assert "ollama down" in response.error - - -def test_vlm_response_format_error(tmp_path, monkeypatch) -> None: - """验证流式块既无 response 也无 done 标记时(格式错误)返回 failed。""" - from nodes.vlm import invoke as vlm_invoke - - image = _real_png(tmp_path) - - class BadResponse: - def readline(self) -> bytes: - return b'{"foo": 1}' - - def __enter__(self): - return self - - def __exit__(self, *args) -> bool: - return False - - monkeypatch.setattr("nodes.vlm.urllib.request.urlopen", lambda *a, **k: BadResponse()) - response = vlm_invoke( - InvokeRequest( - run_id="r", node_instance_id="", - inputs={"image_uri": str(image)}, - output_dir=str(tmp_path), - ) - ) - assert response.status == "failed" - - -def test_vlm_clean_ocr_text() -> None: - """验证清洗逻辑会剔除 markdown 围栏与空行。""" - from nodes.vlm import _clean_ocr_text - - cleaned = _clean_ocr_text("第一行\n```markdown\n```\n\n第二行\n```") - assert cleaned == "第一行\n第二行" - - -def test_vlm_options_in_body(tmp_path, monkeypatch) -> None: - """验证请求体携带采样选项(temperature=0/repeat_penalty/num_predict),可参数覆盖。""" - from nodes.vlm import invoke as vlm_invoke - - image = _real_png(tmp_path) - captured = {} - - def fake_urlopen(request, timeout): - import json as _json - - captured["body"] = _json.loads(request.data.decode("utf-8")) - return FakeChatResponse("SUB 001") - - monkeypatch.setattr("nodes.vlm.urllib.request.urlopen", fake_urlopen) - vlm_invoke( - InvokeRequest( - run_id="r", node_instance_id="", - inputs={"image_uri": str(image)}, - params={"repeat_penalty": 1.3, "num_predict": 128}, - output_dir=str(tmp_path), - ) - ) - assert captured["body"]["options"] == { - "temperature": 0.3, - "repeat_penalty": 1.3, - "num_predict": 128, - } - - -def test_vlm_call_structure_system_prompt_and_stop(tmp_path, monkeypatch) -> None: - """验证 /api/chat 调用结构:system 承载指令、user 只携带图片、stop=。""" - from nodes.vlm import invoke as vlm_invoke - - image = _real_png(tmp_path) - captured = {} - - def fake_urlopen(request, timeout): - import json as _json - - captured["body"] = _json.loads(request.data.decode("utf-8")) - return FakeChatResponse("SUB 001") - - monkeypatch.setattr("nodes.vlm.urllib.request.urlopen", fake_urlopen) - vlm_invoke( - InvokeRequest( - run_id="r", node_instance_id="", - inputs={"image_uri": str(image)}, - params={"prompt": "识别图片中的所有文字,原样输出。"}, - output_dir=str(tmp_path), - ) - ) - body = captured["body"] - assert body["stop"] == ["\n", "\n答", "答"] - assert body["stream"] is True - assert body["messages"][0]["role"] == "system" - assert "识别图片" in body["messages"][0]["content"] - assert body["messages"][1]["role"] == "user" - assert body["messages"][1]["content"] == "" - assert len(body["messages"][1]["images"]) == 1 -def test_vlm_stream_stop_sequence_truncation(tmp_path, monkeypatch) -> None: - """验证流式读取命中终止序列(换行+答,\n答)即停止并截断该标记。""" - from nodes.vlm import invoke as vlm_invoke - - image = _real_png(tmp_path) - - def fake_urlopen(request, timeout): - # 模型输出先给出真实文本,随后进入“答:”式重复循环并输出终止序列。 - return FakeChatResponse("SUB 001\n答:重复循环垃圾") - - monkeypatch.setattr("nodes.vlm.urllib.request.urlopen", fake_urlopen) - response = vlm_invoke( - InvokeRequest( - run_id="r", node_instance_id="", - inputs={"image_uri": str(image)}, - output_dir=str(tmp_path), - ) - ) - assert response.status == "completed", response.error - # 终止序列及其后内容必须被截掉,只保留终止前的真实识别文本。 - assert response.outputs["text"] == "SUB 001" - - -def test_vlm_stream_done_with_message(tmp_path, monkeypatch) -> None: - """验证带 done 标记的流式结束块触发停止,结果正常返回。""" - from nodes.vlm import invoke as vlm_invoke - - image = _real_png(tmp_path) - - def fake_urlopen(request, timeout): - return FakeChatResponse("SUB 001", done=True) - - monkeypatch.setattr("nodes.vlm.urllib.request.urlopen", fake_urlopen) - response = vlm_invoke( - InvokeRequest( - run_id="r", node_instance_id="", - inputs={"image_uri": str(image)}, - output_dir=str(tmp_path), - ) - ) - assert response.status == "completed", response.error - assert response.outputs["text"] == "SUB 001" - - -def test_vlm_stream_done_only_end(tmp_path, monkeypatch) -> None: - """验证仅含 done 标记(无 response)的行视为流结束,空结果正常完成。""" - from nodes.vlm import invoke as vlm_invoke - - image = _real_png(tmp_path) - - class DoneOnlyResponse: - def readline(self) -> bytes: - if not self._consumed: - self._consumed = True - return b'{"done": true}' - return b"" - - def __enter__(self): - self._consumed = False - return self - - def __exit__(self, *args) -> bool: - return False - - monkeypatch.setattr("nodes.vlm.urllib.request.urlopen", lambda *a, **k: DoneOnlyResponse()) - response = vlm_invoke( - InvokeRequest( - run_id="r", node_instance_id="", - inputs={"image_uri": str(image)}, - output_dir=str(tmp_path), - ) - ) - assert response.status == "completed", response.error - assert response.outputs["text"] == "" - - -def test_vlm_stream_deadline_timeout(tmp_path, monkeypatch) -> None: - """验证整体 5 秒截止:流式读取超过 deadline 立即终止并返回 failed。""" - from nodes.vlm import invoke as vlm_invoke - - image = _real_png(tmp_path) - # 第一次调用计算 deadline(100+5=105),第二次调用已越过截止(200>=105)。 - monotonic_values = iter([100.0, 200.0]) - monkeypatch.setattr("nodes.vlm.time.monotonic", lambda: next(monotonic_values)) - - def fake_urlopen(request, timeout): - return FakeChatResponse("SUB 001") - - monkeypatch.setattr("nodes.vlm.urllib.request.urlopen", fake_urlopen) - response = vlm_invoke( - InvokeRequest( - run_id="r", node_instance_id="", - inputs={"image_uri": str(image)}, - params={"timeout_seconds": 5}, - output_dir=str(tmp_path), - ) - ) - assert response.status == "failed" - assert "timed out" in response.error - - -def test_vlm_extract_gettext() -> None: - """验证从模型输出中提取 标签内容的各种形态。""" - from nodes.vlm import _extract_gettext - - # 正常:标签包裹的内容被提取。 - assert _extract_gettext("还有没有什么困扰 或者奇怪的地方吗") == ( - "还有没有什么困扰 或者奇怪的地方吗" - ) - # 空标签:返回空字符串。 - assert _extract_gettext("前缀后缀") == "" - # 多个标签(重复循环):只取第一个。 - assert _extract_gettext("SUB 001SUB 001") == "SUB 001" - # 跨行内容:DOTALL 让 . 匹配换行。 - assert _extract_gettext("第一行\n第二行") == "第一行\n第二行" - # 未按格式输出(无标签):回退原始文本,保持旧行为。 - assert _extract_gettext("没有标签的裸文本") == "没有标签的裸文本" - - -def test_vlm_gettext_in_invoke(tmp_path, monkeypatch) -> None: - """验证整条调用链:模型返回 包裹内容时,产物只含标签内文本。""" - from nodes.vlm import invoke as vlm_invoke - - image = _real_png(tmp_path) - - def fake_urlopen(request, timeout): - return FakeChatResponse("SUB 001 围栏垃圾```") - - monkeypatch.setattr("nodes.vlm.urllib.request.urlopen", fake_urlopen) - response = vlm_invoke( - InvokeRequest( - run_id="r", node_instance_id="", - inputs={"image_uri": str(image)}, - output_dir=str(tmp_path), - ) - ) - assert response.status == "completed", response.error - assert response.outputs["text"] == "SUB 001" - -def test_vlm_truncate_at_stop() -> None: - """验证多个终止序列的截断:取最先命中位置,未命中原样返回。""" - from nodes.vlm import _truncate_at_stop - - # 命中 "\n答"(位置更靠前)。 - assert _truncate_at_stop("SUB 001\n答:重复") == "SUB 001" - # 未命中 "\n答" 但命中单个 "答"。 - assert _truncate_at_stop("SUB 001 答") == "SUB 001 " - # 多个序列均命中:取最早出现的位置("答" 在 "答:" 之前)。 - assert _truncate_at_stop("SUB 001答\n答:循环") == "SUB 001" - # 未命中任何序列:原样返回。 - assert _truncate_at_stop("还有没有什么困扰 或者奇怪的地方吗") == ( - "还有没有什么困扰 或者奇怪的地方吗" - ) - - -# --------------------------------------------------------------------------- -# decode_full:无 VAD 整段解码 + 日语幻觉清洗(TDD) -# --------------------------------------------------------------------------- - - -def test_whisper_decode_full_disables_vad(tmp_path, monkeypatch) -> None: - """decode_full=true 时 transcribe 收到 vad_filter=False 且不触发自动 VAD。""" - captured = {} - - class FullModel: - def __init__(self, *args, **kwargs): - pass - - def transcribe(self, path, **kwargs): - captured["vad_filter"] = kwargs.get("vad_filter") - captured["vad_parameters"] = kwargs.get("vad_parameters") - return ([FakeSegment(0, 1, "ok")], None) - - monkeypatch.setitem( - sys.modules, "faster_whisper", types.SimpleNamespace(WhisperModel=lambda *a, **k: FullModel()) - ) - _make_wav(tmp_path / "audio.wav", 5) - # 默认 vad_filter=true(保持现状),显式 decode_full=true 强制无 VAD 整段解码。 - resp = whisper_invoke( - _whisper_request(tmp_path, params={"language": "ja", "decode_full": True}) - ) - assert resp.status == "completed" - assert captured["vad_filter"] is False - assert captured["vad_parameters"] is None - - -def test_whisper_decode_full_cleanup_jp_hallucination(tmp_path, monkeypatch) -> None: - """decode_full=true 时产物 SRT 中日文长时寒暄幻觉被**整条删除**(连带时间戳)。""" - class HallucModel: - def __init__(self, *args, **kwargs): - pass - - def transcribe(self, path, **kwargs): - # 模拟无 VAD 解码:正常句 + 一条 30s '晚安'幻觉占位 + 一条 2s 真实晚安。 - return ( - [ - FakeSegment(0, 2, "気持ちいい"), - FakeSegment(10, 40, "おやすみなさい"), # 30s 幻觉 - FakeSegment(45, 47, "おやすみなさい"), # 2s 真实 - ], - None, - ) - - monkeypatch.setitem( - sys.modules, "faster_whisper", types.SimpleNamespace(WhisperModel=lambda *a, **k: HallucModel()) - ) - _make_wav(tmp_path / "audio.wav", 5) - resp = whisper_invoke( - _whisper_request(tmp_path, params={"language": "ja", "decode_full": True}) - ) - assert resp.status == "completed" - content = Path(resp.outputs["srt_uri"]).read_text(encoding="utf-8") - # 30s 幻觉整条删除(时间轴 10-40s 不出现);2s 真实晚安与正常句保留。 - assert "気持ちいい" in content - assert content.count("おやすみなさい") == 1 - assert "00:00:10,000 --> 00:00:40,000" not in content - # 不残留 '-' 占位(时间轴里的 '-' 是 SRT 合法分隔符,只检查文本行)。 - text_lines = [ - l for l in content.splitlines() - if l.strip() and not l.strip().isdigit() and "-->" not in l - ] - assert all(t != "-" for t in text_lines) - - -def test_whisper_decode_full_filters_short_moan(tmp_path, monkeypatch) -> None: - """decode_full=true 时纯呻吟碎片整条删除,真实短词保留(2026-09 决策)。""" - class MoanModel: - def __init__(self, *args, **kwargs): - pass - - def transcribe(self, path, **kwargs): - # 混合:正常句 + 纯呻吟碎片 + 真实短词(都应保留)。 - return ( - [ - FakeSegment(0, 2, "気持ちいい"), # 正常 - FakeSegment(2, 3, "あ〜"), # 纯呻吟 → 删 - FakeSegment(3, 4, "ん?"), # 纯呻吟 → 删 - FakeSegment(4, 5, "そこ"), # 真实短词 → 保留 - FakeSegment(5, 6, "やばい"), # 真实短词 → 保留 - ], - None, - ) - - monkeypatch.setitem( - sys.modules, "faster_whisper", types.SimpleNamespace(WhisperModel=lambda *a, **k: MoanModel()) - ) - _make_wav(tmp_path / "audio.wav", 5) - resp = whisper_invoke( - _whisper_request(tmp_path, params={"language": "ja", "decode_full": True}) - ) - assert resp.status == "completed" - content = Path(resp.outputs["srt_uri"]).read_text(encoding="utf-8") - # 纯呻吟碎片被整条删除;真实短词与正常句保留。 - assert "あ〜" not in content - assert "ん?" not in content - assert "気持ちいい" in content - assert "そこ" in content - assert "やばい" in content - assert content.count("-->") == 3 # 5 段 - 2 段呻吟 = 3 条 - - -def test_whisper_decode_full_short_moan_can_disable(tmp_path, monkeypatch) -> None: - """short_moan_max_chars=0 时关闭短呻吟过滤,所有内容原样保留。""" - class MoanModel2: - def __init__(self, *args, **kwargs): - pass - - def transcribe(self, path, **kwargs): - return ([FakeSegment(0, 1, "あ〜"), FakeSegment(1, 2, "そこ")], None) - - monkeypatch.setitem( - sys.modules, "faster_whisper", types.SimpleNamespace(WhisperModel=lambda *a, **k: MoanModel2()) - ) - _make_wav(tmp_path / "audio.wav", 5) - resp = whisper_invoke( - _whisper_request( - tmp_path, - params={"language": "ja", "decode_full": True, "short_moan_max_chars": 0}, - ) - ) - assert resp.status == "completed" - content = Path(resp.outputs["srt_uri"]).read_text(encoding="utf-8") - assert "あ〜" in content - assert "そこ" in content - assert content.count("-->") == 2 - - - - diff --git a/tests/test_ocr_flow.py b/tests/test_ocr_flow.py deleted file mode 100644 index ea09af5..0000000 --- a/tests/test_ocr_flow.py +++ /dev/null @@ -1,569 +0,0 @@ -"""抽帧与字幕 OCR 节点单元测试。 - -frame-extract 用 testdata 真实视频抽帧+裁切+720p 压缩;subtitle-ocr 的 OCR -网络调用(vlm-ocr)按 I/O 边界 mock,但喂给它的帧图片是真实的 testdata 资产。 -应用层不再加工模型输出文本,只做长度上限校验(超长报错跳过)。 -""" - -import json -from pathlib import Path - -from wov_sdk.models import InvokeRequest, InvokeResponse - -from nodes.frame_extract import invoke as frame_invoke -from nodes.subtitle_ocr import _assemble_srt -from nodes.subtitle_ocr import invoke as ocr_invoke - -# 单体根目录:tests/ 的上一级。 -WORKSPACE = Path(__file__).resolve().parent.parent -TESTDATA = WORKSPACE / "testdata" -# 10s 测试视频:SUB 001 在 1-4s、SUB 002 在 6-9s。 -VIDEO = TESTDATA / "subtitle_10s.mp4" -TEXT_IMG = TESTDATA / "ocr_text.png" - - -def _png_size(path: Path) -> tuple[int, int]: - """从 PNG 头读取宽高(真实图片尺寸断言)。""" - data = path.read_bytes() - assert data[:8] == b"\x89PNG\r\n\x1a\n", "not a real png" - width = int.from_bytes(data[16:20], "big") - height = int.from_bytes(data[20:24], "big") - return width, height - - -def _frame_request(tmp_path, video=VIDEO, **params) -> InvokeRequest: - """构造 frame-extract 调用请求。""" - return InvokeRequest( - run_id="run_fx", - node_instance_id="", - inputs={"video_uri": str(video)}, - params=params, - output_dir=str(tmp_path / "out"), - ) - - -# --------------------------------------------------------------------------- -# frame-extract:抽帧 + 裁切 + 720p 压缩 -# --------------------------------------------------------------------------- - - -def test_frame_extract_crop_and_manifest(tmp_path) -> None: - """真实视频抽帧:裁切下半 50% 后帧尺寸为 1280x360,清单时间轴正确。""" - response = frame_invoke( - _frame_request(tmp_path, interval_seconds=1, crop=[0, 0.5, 1, 0.5]) - ) - assert response.status == "completed", response.error - manifest = json.loads(Path(response.outputs["frames_manifest"]).read_text(encoding="utf-8")) - assert len(manifest) >= 9 - assert [round(item["time"], 3) for item in manifest] == [ - round(i * 1.0, 3) for i in range(len(manifest)) - ] - first = Path(manifest[0]["image_uri"]) - assert first.is_file() - # 1280x360 已在 720p 内,压缩不改变尺寸。 - assert _png_size(first) == (1280, 360) - - -def test_frame_extract_default_params(tmp_path) -> None: - """未指定参数时使用默认值:抽帧间隔 0.5 秒 + 默认底部裁切区域。""" - response = frame_invoke(_frame_request(tmp_path)) - assert response.status == "completed", response.error - manifest = json.loads(Path(response.outputs["frames_manifest"]).read_text(encoding="utf-8")) - assert manifest - # 默认间隔 0.5s:25fps 下 step=round(12.5)=12(银行家舍入), - # 帧时间按 step/fps=12/25=0.48s 步进(帧号精确,采样周期由帧量化决定)。 - assert [round(item["time"], 3) for item in manifest] == [ - round(i * 12 / 25, 3) for i in range(len(manifest)) - ] - -def test_frame_extract_missing_video(tmp_path) -> None: - """缺少 video_uri 时返回失败。""" - response = frame_invoke( - InvokeRequest( - run_id="r", node_instance_id="", inputs={}, output_dir=str(tmp_path) - ) - ) - assert response.status == "failed" - - -def test_frame_extract_bad_crop(tmp_path) -> None: - """crop 比例越界(超出画面)时返回失败。""" - assert frame_invoke(_frame_request(tmp_path, crop=[0, 0.5, 1, 1.5])).status == "failed" - assert frame_invoke(_frame_request(tmp_path, crop=[-0.1, 0, 1, 0.5])).status == "failed" - assert frame_invoke(_frame_request(tmp_path, crop="abc")).status == "failed" - assert frame_invoke(_frame_request(tmp_path, crop=[0, 0.5, 1])).status == "failed" - # 各值域合法但 x+w 越出画面。 - assert frame_invoke(_frame_request(tmp_path, crop=[0.6, 0, 0.5, 0.3])).status == "failed" - - -def test_frame_extract_video_missing_file(tmp_path) -> None: - """video_uri 指向不存在的文件时返回失败。""" - response = frame_invoke(_frame_request(tmp_path, video=tmp_path / "none.mp4")) - assert response.status == "failed" - assert "not found" in response.error - - -def test_frame_extract_bad_interval(tmp_path) -> None: - """间隔 <= 0 时返回失败。""" - response = frame_invoke(_frame_request(tmp_path, interval_seconds=0)) - assert response.status == "failed" - - -def test_frame_extract_ffmpeg_fails(monkeypatch, tmp_path) -> None: - """ffmpeg 抽帧失败时透传错误。""" - import subprocess as sp - - monkeypatch.setattr("nodes.frame_extract._video_size", lambda *a, **k: (1280, 720)) - monkeypatch.setattr("nodes.frame_extract._video_fps", lambda *a, **k: 25.0) - monkeypatch.setattr("nodes.frame_extract._video_duration", lambda *a, **k: 10.0) - monkeypatch.setattr( - "nodes.frame_extract.subprocess.run", - lambda *a, **k: sp.CompletedProcess([], 1, stderr="boom"), - ) - response = frame_invoke(_frame_request(tmp_path)) - assert response.status == "failed" - assert "boom" in response.error - - -def test_video_size_unreadable(monkeypatch) -> None: - """ffmpeg -i 输出不含视频流信息时返回 None。""" - import subprocess as sp - - from nodes.frame_extract import _video_size - - monkeypatch.setattr( - "nodes.frame_extract.subprocess.run", - lambda *a, **k: sp.CompletedProcess([], 0, stderr="no video stream"), - ) - assert _video_size(Path("/tmp/x.mp4"), "ffmpeg") is None - - -def test_frame_extract_video_size_unknown(monkeypatch, tmp_path) -> None: - """无法读取视频分辨率时返回失败。""" - monkeypatch.setattr("nodes.frame_extract._video_size", lambda *a, **k: None) - response = frame_invoke(_frame_request(tmp_path)) - assert response.status == "failed" - assert "video size" in response.error - - -def test_video_duration_unreadable(monkeypatch) -> None: - """ffmpeg -i 输出缺少 Duration 时返回 None。""" - import subprocess as sp - - from nodes.frame_extract import _video_duration - - monkeypatch.setattr( - "nodes.frame_extract.subprocess.run", - lambda *a, **k: sp.CompletedProcess([], 0, stderr="no duration info"), - ) - assert _video_duration(Path("/tmp/x.mp4"), "ffmpeg") is None - - -class _FakeReader: - """模拟 stderr 读取对象。""" - - def __init__(self, content: str = "") -> None: - self._content = content - - def read(self) -> str: - return self._content - - -class FakePopen: - """模拟 ffmpeg 进程:stdout 可迭代 -progress 行,可配置返回码与 stderr。""" - - def __init__(self, lines=(), returncode: int = 0, stderr: str = "") -> None: - self.stdout = list(lines) - self.stderr = _FakeReader(stderr) - self.returncode = returncode - - def wait(self) -> int: - return self.returncode - - -def _fake_popen(lines=(), returncode: int = 0, stderr: str = ""): - """构造替换 subprocess.Popen 的工厂函数。""" - return lambda *a, **k: FakePopen(lines, returncode, stderr) - - -def test_parse_progress_line() -> None: - """-progress 行解析:frame=N 返回数值,其他行与非法值返回 None。""" - from nodes.frame_extract import _parse_progress_line - - assert _parse_progress_line("frame=25\n") == 25 - assert _parse_progress_line("progress=continue") is None - assert _parse_progress_line("fps=25.0") is None - assert _parse_progress_line("frame=abc") is None - - -def test_frame_extract_progress_logging(monkeypatch, tmp_path) -> None: - """ffmpeg -progress 的 frame=N 被解析并打印抽帧进度与速度。""" - import logging - - monkeypatch.setattr("nodes.frame_extract._video_size", lambda *a, **k: (1280, 720)) - monkeypatch.setattr("nodes.frame_extract._video_fps", lambda *a, **k: 25.0) - monkeypatch.setattr("nodes.frame_extract._video_duration", lambda *a, **k: 10.0) - monkeypatch.setattr( - "nodes.frame_extract.subprocess.Popen", - _fake_popen(lines=["fps=25.0", "frame=20", "progress=continue", "frame=50", "progress=continue"]), - ) - captured: list[str] = [] - - class CaptureHandler(logging.Handler): - def emit(self, record): - captured.append(record.getMessage()) - - logger = logging.getLogger("vrsub.frame-extract") - logger.addHandler(CaptureHandler()) - try: - response = frame_invoke(_frame_request(tmp_path)) - finally: - logger.removeHandler(logger.handlers[-1]) - assert response.status == "completed", response.error - assert any("抽帧进度" in message and "帧/s" in message for message in captured) - - -def test_frame_extract_ffmpeg_fails(monkeypatch, tmp_path) -> None: - """ffmpeg 抽帧失败时透传错误。""" - monkeypatch.setattr("nodes.frame_extract._video_size", lambda *a, **k: (1280, 720)) - monkeypatch.setattr("nodes.frame_extract._video_fps", lambda *a, **k: 25.0) - monkeypatch.setattr("nodes.frame_extract._video_duration", lambda *a, **k: 10.0) - monkeypatch.setattr( - "nodes.frame_extract.subprocess.Popen", - _fake_popen(lines=["frame=1", "progress=end"], returncode=1, stderr="boom"), - ) - response = frame_invoke(_frame_request(tmp_path)) - assert response.status == "failed" - assert "boom" in response.error - - -def test_frame_extract_duration_unknown(monkeypatch, tmp_path) -> None: - """无法读取视频时长时返回失败。""" - monkeypatch.setattr("nodes.frame_extract._video_size", lambda *a, **k: (1280, 720)) - monkeypatch.setattr("nodes.frame_extract._video_duration", lambda *a, **k: None) - response = frame_invoke(_frame_request(tmp_path)) - assert response.status == "failed" - assert "duration" in response.error - - -def test_frame_extract_fps_unknown(monkeypatch, tmp_path) -> None: - """无法读取视频帧率时返回失败。""" - monkeypatch.setattr("nodes.frame_extract._video_size", lambda *a, **k: (1280, 720)) - monkeypatch.setattr("nodes.frame_extract._video_duration", lambda *a, **k: 10.0) - monkeypatch.setattr("nodes.frame_extract._video_fps", lambda *a, **k: None) - response = frame_invoke(_frame_request(tmp_path)) - assert response.status == "failed" - assert "fps" in response.error - -def test_frame_step_conversion() -> None: - """帧间隔换算:step=round(间隔秒×fps),至少为 1。""" - from nodes.frame_extract import _frame_step - - assert _frame_step(fps=25.0, interval=0.2) == 5 - assert _frame_step(fps=25.0, interval=1.0) == 25 - assert _frame_step(fps=29.97, interval=1.0) == 30 - # fps 很低时 step 也不会小于 1(每帧都取)。 - assert _frame_step(fps=1.0, interval=0.2) == 1 - - -def test_video_fps_parse(monkeypatch) -> None: - """帧率解析:支持小数(29.97)与有理数(30000/1001)。""" - import subprocess as sp - - from nodes.frame_extract import _video_fps - - monkeypatch.setattr( - "nodes.frame_extract.subprocess.run", - lambda *a, **k: sp.CompletedProcess( - [], 0, stderr="Stream #0:0: Video: h264, 1280x720, 30000/1001 fps, 30000/1001 tbr" - ), - ) - assert _video_fps(Path("/tmp/x.mp4"), "ffmpeg") == 30000 / 1001 - monkeypatch.setattr( - "nodes.frame_extract.subprocess.run", - lambda *a, **k: sp.CompletedProcess( - [], 0, stderr="Stream #0:0: Video: h264, 1280x720, 25 fps, 25 tbr" - ), - ) - assert _video_fps(Path("/tmp/x.mp4"), "ffmpeg") == 25.0 - monkeypatch.setattr( - "nodes.frame_extract.subprocess.run", - lambda *a, **k: sp.CompletedProcess([], 0, stderr="no video stream"), - ) - assert _video_fps(Path("/tmp/x.mp4"), "ffmpeg") is None - -def test_frame_extract_one_second_exact_frames(tmp_path) -> None: - """真实视频 1s 间隔按秒 seek 精确抽帧:10s 视频应得 10 帧,时间 0..9。""" - response = frame_invoke( - _frame_request(tmp_path, interval_seconds=1, crop=[0, 0.7, 1, 0.3]) - ) - assert response.status == "completed", response.error - manifest = json.loads(Path(response.outputs["frames_manifest"]).read_text(encoding="utf-8")) - assert [round(item["time"], 3) for item in manifest] == [ - round(i * 1.0, 3) for i in range(len(manifest)) - ] - assert len(manifest) == 10 - - -def test_frame_files_read_order_matches_frame_number(tmp_path) -> None: - """真实任务留存数据:帧文件按帧号数值排序读取,而非字典序。 - - 回归用例:ffmpeg 的 %04d 编号在超过 9999 帧后自动扩为 5 位 - (frame_10000.png 等),此时 sorted() 默认字典序会把 5 位编号排在 - 4 位编号之前(如 frame_10009 < frame_1009),导致帧号回退、manifest - 时间与图像错位。testdata/frames_boundary/ 是 2026-08 真实任务 - run_339ec7ee437f(14236 帧 / 2 小时视频)中跨越该边界的真实帧文件。 - """ - from nodes.frame_extract import _sorted_frame_files - - boundary = TESTDATA / "frames_boundary" - files = _sorted_frame_files(boundary) - # 从文件名解析帧号:读取顺序必须等于帧号数值递增序(无回退)。 - nums = [int(p.stem.split("_", 1)[1]) for p in files] - assert nums == sorted(nums) - # 边界关键对:5 位编号必须排在 4 位编号之后,禁止字典序错位。 - assert nums.index(10000) > nums.index(9999) - assert nums.index(10009) > nums.index(1009) - -# --------------------------------------------------------------------------- -# subtitle-ocr:OCR 循环 + 长度上限 + 合并 + SRT 组装 -# --------------------------------------------------------------------------- - - -def _frames_manifest(tmp_path, frame_specs) -> Path: - """构造真实 frames.json;frame_specs=[(time, image_path), ...]。""" - items = [{"time": time, "image_uri": str(image)} for time, image in frame_specs] - path = tmp_path / "frames.json" - path.write_text(json.dumps(items), encoding="utf-8") - return path - - -def test_assemble_srt_real_timeline() -> None: - """SRT 组装:起始=帧时间,结束=最后可见帧时间+采样间隔。""" - lines = _assemble_srt([(0.0, 6.0, "A"), (6.0, 8.0, "B")], interval_seconds=2.0) - text = "\n".join(lines) - assert text.startswith("1\n") - # A 最后可见帧 6.0 + 间隔 2.0 = 8.0(而非下一条字幕的出现时间)。 - assert "00:00:00,000 --> 00:00:08,000" in text - assert "00:00:06,000 --> 00:00:10,000" in text - - -def test_sampling_interval_from_manifest() -> None: - """采样间隔从帧清单时间轴推导:均匀间隔取相邻差,退化清单回退默认值。""" - from nodes.subtitle_ocr import _sampling_interval - - manifest = [{"time": i * 0.2, "image_uri": f"f{i}.png"} for i in range(10)] - assert _sampling_interval(manifest, 2.0) == 0.2 - # 单帧(无法算差)与异常时间序:回退默认值。 - assert _sampling_interval([{"time": 0.0, "image_uri": "f0.png"}], 2.0) == 2.0 - assert _sampling_interval( - [{"time": 0.0}, {"time": 0.0}, {"time": 0.2}], 2.0 - ) == 0.2 - -def test_ocr_merges_consecutive_same_text(monkeypatch, tmp_path) -> None: - """连续帧相同字幕合并为一条;消失时间=最后可见帧+间隔,空白段保留。""" - # SUB 001 在 0/2s,4s 为空帧,SUB 002 在 6/8s。 - frame_texts = [ - (0.0, "SUB 001"), (2.0, "SUB 001"), (4.0, ""), - (6.0, "SUB 002"), (8.0, "SUB 002"), - ] - frames = [] - for index, (time, _text) in enumerate(frame_texts): - image = tmp_path / f"f{index}.png" - image.write_bytes(TEXT_IMG.read_bytes()) - frames.append((time, image)) - mapping = {str(image): text for (time, image), (_, text) in zip(frames, frame_texts)} - - def fake_vlm(node_id, request): - return InvokeResponse(status="completed", outputs={"text": mapping[request.inputs["image_uri"]]}) - - monkeypatch.setattr("wov_app.registry.invoke", fake_vlm) - manifest = _frames_manifest(tmp_path, frames) - response = ocr_invoke( - InvokeRequest( - run_id="run_ocr", - node_instance_id="", - inputs={"frames_manifest": str(manifest)}, - params={}, - output_dir=str(tmp_path / "out"), - ) - ) - assert response.status == "completed", response.error - srt = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8") - assert srt.count("SUB 001") == 1 - assert srt.count("SUB 002") == 1 - # SUB 001 最后可见帧 2.0 + 间隔 2.0 = 4.0 消失(而非拖到 SUB 002 出现)。 - assert "00:00:00,000 --> 00:00:04,000" in srt - assert "00:00:06,000 --> 00:00:10,000" in srt - - -def test_ocr_preserves_checkpoint_for_failed_frames(monkeypatch, tmp_path) -> None: - """个别帧持续失败时返回 failed,其余成功帧存档供重试复用。""" - frames = [] - for index in range(3): - image = tmp_path / f"f{index}.png" - image.write_bytes(TEXT_IMG.read_bytes()) - frames.append((index * 2.0, image)) - - def fake_vlm(node_id, request): - if "f1" in request.inputs["image_uri"]: - return InvokeResponse(status="failed", error="boom") - return InvokeResponse(status="completed", outputs={"text": "SUB 001"}) - - monkeypatch.setattr("wov_app.registry.invoke", fake_vlm) - manifest = _frames_manifest(tmp_path, frames) - response = ocr_invoke( - InvokeRequest( - run_id="run_ocr", - node_instance_id="", - inputs={"frames_manifest": str(manifest)}, - params={}, - output_dir=str(tmp_path / "out"), - ) - ) - assert response.status == "failed" - partial = [json.loads(line) for line in (tmp_path / "out/ocr_partial.jsonl").read_text().splitlines()] - assert {item["frame"] for item in partial} == {0, 2} - assert all(item["text"] == "SUB 001" for item in partial) - assert not (tmp_path / "out/subtitle.srt").exists() - - -def test_ocr_missing_manifest(tmp_path) -> None: - """缺少 frames_manifest 或清单文件不存在时返回失败。""" - response = ocr_invoke( - InvokeRequest(run_id="r", node_instance_id="", inputs={}, output_dir=str(tmp_path)) - ) - assert response.status == "failed" - response = ocr_invoke( - InvokeRequest( - run_id="r", node_instance_id="", - inputs={"frames_manifest": str(tmp_path / "none.json")}, - output_dir=str(tmp_path), - ) - ) - assert response.status == "failed" - - -def test_ocr_skips_oversized_output(monkeypatch, tmp_path) -> None: - """超长输出(模型重复循环等)直接报错跳过该帧,不进入 SRT。""" - frames = [] - for index in range(3): - image = tmp_path / f"o{index}.png" - image.write_bytes(TEXT_IMG.read_bytes()) - frames.append((index * 2.0, image)) - - def fake_vlm(node_id, request): - if "o0" in request.inputs["image_uri"]: - return InvokeResponse(status="completed", outputs={"text": "重复字幕\n" * 50}) - if "o1" in request.inputs["image_uri"]: - # 空输出帧跳过。 - return InvokeResponse(status="completed", outputs={"text": ""}) - return InvokeResponse(status="completed", outputs={"text": "SUB 001"}) - - monkeypatch.setattr("wov_app.registry.invoke", fake_vlm) - manifest = _frames_manifest(tmp_path, frames) - response = ocr_invoke( - InvokeRequest( - run_id="run_ocr", - node_instance_id="", - inputs={"frames_manifest": str(manifest)}, - params={"max_result_chars": 200}, - output_dir=str(tmp_path / "out"), - ) - ) - assert response.status == "completed", response.error - srt = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8") - assert "SUB 001" in srt - assert "重复字幕" not in srt - - -def test_ocr_passes_short_text_through(monkeypatch, tmp_path) -> None: - """不超过上限的模型输出原样进入 SRT(不再做应用层过滤)。""" - image = tmp_path / "s0.png" - image.write_bytes(TEXT_IMG.read_bytes()) - frames = [(0.0, image)] - - def fake_vlm(node_id, request): - return InvokeResponse(status="completed", outputs={"text": " SUB 001 "}) - - monkeypatch.setattr("wov_app.registry.invoke", fake_vlm) - manifest = _frames_manifest(tmp_path, frames) - response = ocr_invoke( - InvokeRequest( - run_id="run_ocr", node_instance_id="", - inputs={"frames_manifest": str(manifest)}, - params={}, - output_dir=str(tmp_path / "out"), - ) - ) - assert response.status == "completed", response.error - srt = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8") - assert "SUB 001" in srt - - -def test_ocr_interrupts_on_pause_flag(monkeypatch, tmp_path) -> None: - """暂停信号:任务被暂停(paused.flag 存在)时 OCR 立即中断。 - - output_dir = /runs//steps/ocr,run 根目录为其父父目录; - 调度器/API 在暂停时向 run 根写入 paused.flag,节点逐帧检查到即中止: - 不调用 vlm-ocr、不写该帧断点存档,invoke 返回 failed(由调度器识别为 - "被暂停"并保持 PAUSED,等待 resume 后从断点续跑)。 - """ - image = tmp_path / "s0.png" - image.write_bytes(TEXT_IMG.read_bytes()) - frames = [(i * 2.0, image) for i in range(4)] - - # 暂停信号位于 run 根目录(steps/ocr 的父父目录)。 - run_root = tmp_path / "run_root" - out_dir = run_root / "steps" / "ocr" - out_dir.mkdir(parents=True) - (run_root / "paused.flag").write_text("", encoding="utf-8") - - calls: list[str] = [] - - def fake_vlm(node_id, request): - calls.append(request.inputs["image_uri"]) - return InvokeResponse(status="completed", outputs={"text": "SUB 001"}) - - monkeypatch.setattr("wov_app.registry.invoke", fake_vlm) - manifest = _frames_manifest(tmp_path, frames) - response = ocr_invoke( - InvokeRequest( - run_id="run_paused_flag", - node_instance_id="", - inputs={"frames_manifest": str(manifest)}, - params={}, - output_dir=str(out_dir), - ) - ) - # 节点以"被暂停"失败:调度器会据此保持 PAUSED 而不标 FAILED。 - assert response.status == "failed" - assert "暂停" in (response.error or "") - assert calls == [] # 一帧都没有真正 OCR。 - assert not (out_dir / "ocr_partial.jsonl").exists() # 未处理帧不入存档。 - - -def test_format_eta() -> None: - """预计剩余时间的格式化:秒/分/小时三种量级与边界值。""" - from nodes.subtitle_ocr import _format_eta - - assert _format_eta(0) == "0秒" - assert _format_eta(59.9) == "59秒" # 不足 1 分只显示秒。 - assert _format_eta(61) == "1分01秒" - assert _format_eta(2058.4) == "34分18秒" - assert _format_eta(3600) == "1小时00分" # 1 小时整。 - assert _format_eta(3725) == "1小时02分" # 超过 1 小时只显示到分钟。 - assert _format_eta(-5) == "0秒" # 负数钳制为 0。 - - -def test_eta_suffix() -> None: - """进度日志的 ETA 后缀:速度为 0 不显示,速度正常时按剩余帧估算。""" - from nodes.subtitle_ocr import _eta_suffix - - # 速率未知(0/负)→ 不显示 ETA。 - assert _eta_suffix(done=100, total=100, rate=0) == "" - assert _eta_suffix(done=100, total=100, rate=-1) == "" - # 2673/22222 帧、9.5 帧/s:剩余 (22222-2673)/9.5 ≈ 2057.8s ≈ 34分。 - assert _eta_suffix(done=2673, total=22222, rate=9.5) == ", 预计剩余 34分17秒" - # 全部完成时剩余 0 秒。 - assert _eta_suffix(done=22222, total=22222, rate=9.5) == ", 预计剩余 0秒" diff --git a/tests/test_ocr_recovery.py b/tests/test_ocr_recovery.py deleted file mode 100644 index 7effa21..0000000 --- a/tests/test_ocr_recovery.py +++ /dev/null @@ -1,66 +0,0 @@ -"""R06:真实图片清单上的临时网络故障、空帧与跨空白字幕段回归。""" - -import json -from pathlib import Path - -import pytest - -from nodes import subtitle_ocr -from wov_sdk.models import InvokeRequest, InvokeResponse - - -def test_identical_text_separated_by_blank_is_two_cues(): - """A、空白、A 不能合并,否则字幕会覆盖原本无文字的时段。""" - manifest = [{"time": i * 0.5} for i in range(4)] - assert subtitle_ocr._merge_kept(manifest, ["你好", "你好", "", "你好"]) == [ - (0.0, 0.5, "你好"), (1.5, 1.5, "你好")] - - -@pytest.mark.parametrize("raises", [False, True]) -def test_failed_frame_retries_and_resume_preserves_success(monkeypatch, tmp_path, raises): - """失败帧单独重试,持续故障不存为空;下次只补失败帧,成功空帧不重做。""" - assets = Path(__file__).resolve().parent.parent / "testdata" - image = assets / "ocr_text.png" - empty = assets / "ocr_notext.png" - if not image.is_file() or not empty.is_file(): - pytest.skip("缺少真实 OCR 图片") - manifest = tmp_path / "frames.json" - manifest.write_text(json.dumps([{"time": 0, "image_uri": str(empty)}, - {"time": 0.5, "image_uri": str(image)}])) - out = tmp_path / "out" - request = InvokeRequest(run_id="r", node_instance_id="", inputs={"frames_manifest": str(manifest)}, output_dir=str(out)) - calls = [] - broken = True - - def invoke(node_id, req): - uri = req.inputs["image_uri"] - calls.append(uri) - if uri == str(image) and broken: - if raises: - raise TimeoutError("timeout") - return InvokeResponse(status="failed", error="timeout") - return InvokeResponse(status="completed", outputs={"text": "" if uri == str(empty) else "你好"}) - - monkeypatch.setattr("wov_app.registry.invoke", invoke) - response = subtitle_ocr.invoke(request) - assert response.status == "failed" - assert calls.count(str(image)) == 2 - assert calls.count(str(empty)) == 1 - partial = [json.loads(line) for line in (out / "ocr_partial.jsonl").read_text().splitlines()] - assert partial == [{"frame": 0, "text": "", "status": "completed"}] - assert not (out / "subtitle.srt").exists() - broken = False - calls.clear() - response = subtitle_ocr.invoke(request) - assert response.status == "completed" - assert calls == [str(image)] - assert "你好" in Path(response.outputs["srt_uri"]).read_text() - - -def test_legacy_empty_checkpoint_is_rechecked(tmp_path): - """旧版空串可能来自超时,不能当成确认无文字;旧版非空成功结果可复用。""" - (tmp_path / "ocr_partial.jsonl").write_text( - json.dumps({"frame": 0, "text": ""}) + "\n" + - json.dumps({"frame": 1, "text": "你好"}, ensure_ascii=False) + "\n" + - json.dumps({"frame": 2, "text": "", "status": "completed"}) + "\n") - assert subtitle_ocr._load_partial(tmp_path) == {1: "你好", 2: ""} diff --git a/tests/test_proper_nouns.py b/tests/test_proper_nouns.py deleted file mode 100644 index 1e916de..0000000 --- a/tests/test_proper_nouns.py +++ /dev/null @@ -1,110 +0,0 @@ -"""专有名词(不应直译)处理规则测试(先红后绿)。 - -背景(实测 run 20260905115050):字幕中片假名专名被 LLM 按读音硬译—— -'ジンゴ' 被误译成 '芒果'(4500s"看,跟芒果摩擦好多"),参考正确为 -'肉棒摩擦小穴' 等;人名 'カンタくん' 被保留日文而非音译。LLM 需被告知 -这些专名/拟声词的正确处理方式。 - -方案(用户确认):整理专名表,翻译时若原文命中则向提示词动态注入规则, -让 LLM 正确处理。本测试验证 `build_proper_noun_rule` 的命中与注入行为 -(纯函数),以及真实数据场景的端到端效果(真实 LLM 集成)。 -""" - -from __future__ import annotations - -import os -from pathlib import Path - -import pytest - -from nodes.proper_nouns import build_proper_noun_rule - -WORKSPACE = Path(__file__).resolve().parent.parent -# 真实中文字幕产物(含"芒果"误译)。目录按实际路径调整。 -PROD = Path("/home/cat/Downloads/39.105.149.197/202609051952/CJOD-255-长视频-单行字幕.zh-CN.20260905115050.srt") -# 真实日文原文 transcript(含 ジンゴ/カンタくん 等专名)。 -TRANSCR = Path("/home/cat/Downloads/39.105.149.197/202609051737/run_51242078d76e/steps/asr/transcript.srt") - - -def test_rule_returns_none_when_no_proper_noun() -> None: - """原文不含任何专名时,不注入规则(返回 None),避免干扰普通翻译。""" - plain = "今天天气真好。\n我们一起去散步吧。" - assert build_proper_noun_rule(plain) is None - - -def test_rule_injects_for_jingo() -> None: - """原文含'ジンゴ'(角色/道具专名)时,注入'禁止译作芒果'的规则。""" - text = "クモ穴にパンパンになって ジンゴがここで味わいませんか" - rule = build_proper_noun_rule(text) - assert rule is not None - assert "ジンゴ" in rule - assert "芒果" in rule # 提示禁止硬译 - assert "不要按读音或字面硬译" in rule # 提示禁止硬译 - -def test_rule_injects_for_kanta_kun() -> None: - """原文含'カンタくん'(人名)时,注入'音译勿保留日文'的规则。""" - text = "ねえ、カンタくん、4つんばんになってください" - rule = build_proper_noun_rule(text) - assert rule is not None - assert "カンタくん" in rule - assert "康太君" in rule or "坎塔君" in rule - - -def test_rule_injects_onomatopoeia() -> None: - """原文含拟声词'パンパン'时,注入'鼓胀、饱满'而非'砰砰'的规则。""" - text = "クモ穴にパンパンになって" - rule = build_proper_noun_rule(text) - assert rule is not None - assert "パンパン" in rule - assert "砰砰" in rule # 提示禁止直译 - - -def test_rule_list_input() -> None: - """传入列表(每批字幕行)也能命中。""" - lines = ["音楽", "ご来店ありがとうございます", "ジンゴがここで味わいませんか"] - rule = build_proper_noun_rule(lines) - assert rule is not None - assert "ジンゴ" in rule - - -def test_rule_injects_adult_euphemism_mango() -> None: - """成人隐语'マンゴー':应注入'小穴/鲍鱼'而非直译'芒果'。""" - text = "クモ穴にマンゴーをこすり合わせて" - rule = build_proper_noun_rule(text) - assert rule is not None - assert "マンゴー" in rule - assert "小穴" in rule or "鲍鱼" in rule - assert "芒果" in rule - - -def test_rule_injects_adult_euphemism_kintama() -> None: - """成人隐语'金玉':应注入'蛋蛋/睾丸'而非直译'金玉'。""" - text = "金玉が大きくなってきた" - rule = build_proper_noun_rule(text) - assert rule is not None - assert "金玉" in rule - assert "蛋蛋" in rule or "睾丸" in rule - - -def test_rule_injects_adult_euphemism_banana() -> None: - """成人隐语'バナナ':应注入'肉棒'而非直译'香蕉'。""" - text = "バナナをしゃぶって" - rule = build_proper_noun_rule(text) - assert rule is not None - assert "バナナ" in rule - assert "肉棒" in rule or "鸡鸡" in rule - - -@pytest.mark.integration -def test_real_proper_noun_rule_matches_production_data() -> None: - """用真实 transcript 验证:含'ジンゴ'的批次确实命中并注入规则。""" - if not TRANSCR.is_file(): - pytest.skip("缺少真实 transcript.srt,跳过") - from tests.realdata_contract import parse_srt_entries - - entries = parse_srt_entries(TRANSCR.read_text(encoding="utf-8")) - # 找到含 ジンゴ 的批次(4500-4518s 那批)。 - batch = [e["text"] for e in entries if 4490 <= e["start"] <= 4520] - rule = build_proper_noun_rule(batch) - assert rule is not None, "真实数据中含 ジンゴ,应命中专名规则" - assert "ジンゴ" in rule and "芒果" in rule \ No newline at end of file diff --git a/tests/test_registry.py b/tests/test_registry.py deleted file mode 100644 index 7512418..0000000 --- a/tests/test_registry.py +++ /dev/null @@ -1,116 +0,0 @@ -"""进程内节点注册表测试。 - -覆盖节点注册、全量注册、查询、进程内调用以及未注册节点的报错路径, -验证注册表作为调度器唯一调用入口的正确性。 -""" - -import pytest - -from wov_app import registry -from wov_sdk.models import InvokeRequest, InvokeResponse, NodeManifest - - -def _echo_manifest() -> NodeManifest: - """构造最小合法 echo 节点清单。""" - return NodeManifest( - id="echo", - name="Echo", - version="1.0.0", - capability="echo", - command=["python", "-m", "echo"], - repo_dir="nodes", - ) - - -def test_register_and_list() -> None: - """验证注册后可查询与列出节点,且按 ID 排序。""" - registry.register(_echo_manifest(), lambda request: InvokeResponse(status="completed")) - registry.register( - NodeManifest( - id="z-node", - name="Z", - version="1", - capability="x", - command=["python", "-m", "z"], - repo_dir="nodes", - ), - lambda request: InvokeResponse(status="completed"), - ) - assert [node.id for node in registry.list_nodes()] == ["echo", "z-node"] - assert registry.get_node("echo").capability == "echo" - assert registry.get_node("missing") is None - - -def test_register_validation() -> None: - """验证非法 manifest 注册会被协议校验拒绝。""" - invalid = _echo_manifest() - invalid.id = "" - with pytest.raises(ValueError): - registry.register(invalid, lambda request: InvokeResponse(status="completed")) - - -def test_register_all_loads_builtin_nodes() -> None: - """验证 register_all 会加载 manifests/ 下全部内置节点。""" - registry.register_all() - ids = {node.id for node in registry.list_nodes()} - assert { - "echo", - "ffmpeg-extract", - "faster-whisper", - "llm-translate", - "vlm-ocr", - "srt-to-dual-eye-ass", - } <= ids - - -def test_invoke_calls_handler() -> None: - """验证 invoke 会把请求转发给注册的进程内处理器。""" - captured = {} - - def handler(request: InvokeRequest) -> InvokeResponse: - captured["run_id"] = request.run_id - return InvokeResponse(status="completed", outputs={"text": "ok"}) - - registry.register(_echo_manifest(), handler) - response = registry.invoke("echo", InvokeRequest(run_id="run_1", node_instance_id="")) - assert response.status == "completed" - assert response.outputs == {"text": "ok"} - assert captured["run_id"] == "run_1" - - -def test_invoke_unknown_node() -> None: - """验证调用未注册节点时抛出 ValueError。""" - with pytest.raises(ValueError, match="not registered"): - registry.invoke("missing", InvokeRequest(run_id="run_1", node_instance_id="")) - - -def test_invoke_logs_node_lifecycle(caplog) -> None: - """验证 invoke 会记录节点的开始/完成/耗时日志(主进程可见)。""" - registry.register( - _echo_manifest(), lambda request: InvokeResponse(status="completed", outputs={"text": "ok"}) - ) - with caplog.at_level("INFO", logger="vrsub.node"): - registry.invoke("echo", InvokeRequest(run_id="run_1", node_instance_id="")) - assert any("节点 echo 开始" in record.message for record in caplog.records) - assert any("节点 echo 完成" in record.message for record in caplog.records) - - -def test_invoke_logs_node_failure(caplog) -> None: - """验证节点返回 failed 时记录失败日志。""" - registry.register( - _echo_manifest(), lambda request: InvokeResponse(status="failed", error="boom") - ) - with caplog.at_level("INFO", logger="vrsub.node"): - registry.invoke("echo", InvokeRequest(run_id="run_1", node_instance_id="")) - assert any("节点 echo 失败" in record.message for record in caplog.records) - - -def test_get_logger_idempotent() -> None: - """验证日志器重复获取不会重复附加控制台处理器。""" - from wov_app.logging import get_logger - - logger = get_logger("idempotent") - handler_count = len(logger.handlers) - again = get_logger("idempotent") - assert again is logger - assert len(again.handlers) == handler_count diff --git a/tests/test_run_deletion_safety.py b/tests/test_run_deletion_safety.py deleted file mode 100644 index 86c1c5a..0000000 --- a/tests/test_run_deletion_safety.py +++ /dev/null @@ -1,104 +0,0 @@ -"""任务删除安全回归:真实媒体副本验证用户目录与任务私有目录的归属边界。""" - -import shutil -from pathlib import Path - -import pytest -from fastapi.testclient import TestClient - -from wov_app.config import STORAGE_DIR -from wov_app.main import app -from wov_app.db import Database -from wov_app.maintenance import OrphanCleaner - - -@pytest.fixture -def media_library(tmp_path): - """复制已有真实视频与字幕,任何删除只作用于测试临时目录。""" - assets = Path(__file__).resolve().parent.parent / "testdata" - sources = [assets / "subtitle_10s.mp4", assets / "ocr_srt_run_ac7f480a3ccb.srt"] - if not all(source.is_file() for source in sources): - pytest.skip("缺少真实视频或字幕测试素材") - library = tmp_path / "media" - library.mkdir() - files = [library / "movie.mp4", library / "movie.CN.srt"] - for source, target in zip(sources, files): - shutil.copy2(source, target) - return files, [path.read_bytes() for path in files] - - -def _create_run(db, run_id, video, source, status): - """在真实数据库登记任务,不启动模型或调度器。""" - workflow_id = "delete-safety" - db.upsert_workflow({"id": workflow_id, "name": "删除安全回归"}) - db.create_run({ - "id": run_id, "workflow_id": workflow_id, "workflow_version": 1, - "source": source, "status": status, "input_uri": str(video), - "created_at": "2020-01-01T00:00:00+00:00", - "updated_at": "2020-01-01T00:00:00+00:00", - }) - # 登记可读的真实字幕产物,拒绝删除时应连同记录保留。 - db.create_artifact({ - "run_id": run_id, "node_id": "ass", "name": "subtitle", - "uri": str(video.with_suffix(".CN.srt")), "mime_type": "application/x-subrip", - }) - - -@pytest.mark.parametrize("status", ["QUEUED", "RUNNING", "PAUSED", "FAILED", "COMPLETED"]) -def test_normal_delete_rejects_batch_run(media_library, status): - """普通 DELETE 对所有状态的批量 run 返回 422,保留媒体与关联数据。""" - files, contents = media_library - run_id = f"run_batch_delete_safety_{status.lower()}" - with TestClient(app) as client: - db = app.state.db - _create_run(db, run_id, files[0], "batch", status) - before = db.get_run(run_id) - artifacts = db.list_artifacts(run_id) - response = client.delete(f"/api/runs/{run_id}") - assert response.status_code == 422 - assert "批量任务" in response.json()["detail"] - assert db.get_run(run_id) == before - assert db.list_artifacts(run_id) == artifacts - assert [path.read_bytes() for path in files] == contents - - -def test_upload_delete_ignores_external_input_parent(media_library): - """历史上传记录即使指向外部视频,也仅清理按 run_id 定位的私有目录。""" - files, contents = media_library - run_id = "run_external_delete_safety" - with TestClient(app) as client: - db = app.state.db - _create_run(db, run_id, files[0], "upload", "FAILED") - private_dirs = [STORAGE_DIR / kind / run_id for kind in ("uploads", "runs")] - for directory in private_dirs: - directory.mkdir(parents=True) - shutil.copy2(files[1], directory / "subtitle.srt") - # 同级其他任务目录也不能被扩大范围删除。 - sibling = STORAGE_DIR / "uploads" / "run_neighbor_delete_safety" - sibling.mkdir(parents=True) - shutil.copy2(files[1], sibling / "subtitle.srt") - response = client.delete(f"/api/runs/{run_id}") - assert response.status_code == 200 - assert db.get_run(run_id) is None - assert db.list_artifacts(run_id) == [] - assert [path.read_bytes() for path in files] == contents - assert all(not directory.exists() for directory in private_dirs) - assert (sibling / "subtitle.srt").read_bytes() == contents[1] - - -def test_orphan_cleanup_ignores_external_input_parent(media_library, tmp_path): - """自动清理过期上传任务时也不能从 input_uri 推导递归删除范围。""" - files, contents = media_library - db = Database(tmp_path / "orphan.db") - run_id = "run_orphan_delete_safety" - _create_run(db, run_id, files[0], "upload", "COMPLETED") - # 产物确实丢失,符合孤儿清理条件;源视频与旁挂字幕仍是用户数据。 - db.delete_run_artifacts(run_id) - storage = tmp_path / "private" - upload_dir = storage / "uploads" / run_id - upload_dir.mkdir(parents=True) - shutil.copy2(files[0], upload_dir / "movie.mp4") - OrphanCleaner(db, storage).clean_once() - assert db.get_run(run_id) is None - assert [path.read_bytes() for path in files] == contents - assert not upload_dir.exists() diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py deleted file mode 100644 index 0d50d26..0000000 --- a/tests/test_scheduler.py +++ /dev/null @@ -1,877 +0,0 @@ -"""调度器单元测试。 - -覆盖拓扑排序、任务执行成功/失败分支、输入引用解析、MIME 推断以及 -后台轮询线程的启动与停止。节点调用改为进程内注册表直接调用。 -""" - -import time -from pathlib import Path - -import pytest - -from wov_app import registry -from wov_app.db import Database -from wov_app.scheduler import WorkflowScheduler, topological_sort -from wov_sdk.models import ( - InvokeResponse, - NodeManifest, - WorkflowDefinition, - WorkflowEdge, - WorkflowNode, -) - -# 单体根目录:tests/ 的上一级。 -WORKSPACE = Path(__file__).resolve().parent.parent - - -def _register_echo() -> None: - """把内置 echo 节点注册到进程内注册表。""" - from nodes.echo import invoke - - registry.register(NodeManifest.load(str(WORKSPACE / "manifests" / "echo.json")), invoke) - - -def _db(tmp_path) -> Database: - """在临时目录创建独立数据库。""" - return Database(tmp_path / "wov.db") - - -def _echo_definition() -> WorkflowDefinition: - """构造引用 Echo 节点的单步骤工作流定义。""" - return WorkflowDefinition( - name="echo-flow", - version=1, - nodes=[ - WorkflowNode( - id="step", - node_type="echo", - inputs={"file_uri": "input.video_uri"}, - ) - ], - edges=[], - entry_inputs={"video_uri": "file"}, - final_outputs={"result": "step.file_uri"}, - ) - - -def test_topological_sort() -> None: - """验证 DAG 排序保持依赖顺序,并拒绝环与未知边。""" - definition = WorkflowDefinition( - name="dag", - version=1, - nodes=[ - WorkflowNode(id="a", node_type="x"), - WorkflowNode(id="b", node_type="x"), - WorkflowNode(id="c", node_type="x"), - ], - edges=[ - WorkflowEdge(from_node="a", to_node="b"), - WorkflowEdge(from_node="a", to_node="c"), - ], - ) - order = topological_sort(definition) - assert order.index("a") < order.index("b") - assert order.index("a") < order.index("c") - - cycle = WorkflowDefinition( - name="cycle", - version=1, - nodes=[ - WorkflowNode(id="a", node_type="x"), - WorkflowNode(id="b", node_type="x"), - ], - edges=[ - WorkflowEdge(from_node="a", to_node="b"), - WorkflowEdge(from_node="b", to_node="a"), - ], - ) - with pytest.raises(ValueError, match="cycle"): - topological_sort(cycle) - - with pytest.raises(ValueError, match="unknown edge"): - topological_sort( - WorkflowDefinition( - name="bad", - version=1, - nodes=[WorkflowNode(id="a", node_type="x")], - edges=[WorkflowEdge(from_node="a", to_node="missing")], - ) - ) - - -def test_execute_echo_workflow(tmp_path) -> None: - """验证排队任务可被完整执行并登记全部产物。""" - db = _db(tmp_path) - input_file = tmp_path / "input.txt" - input_file.write_text("hello scheduler", encoding="utf-8") - _register_echo() - db.upsert_workflow({"id": "flow", "name": "Flow", "published": 1, "latest_version": 1}) - db.create_workflow_version("flow", 1, _echo_definition().to_dict()) - now = "2026-01-01T00:00:00+00:00" - db.create_run( - { - "id": "run_1", - "workflow_id": "flow", - "workflow_version": 1, - "status": "QUEUED", - "progress": 0, - "input_uri": str(input_file), - "created_at": now, - "updated_at": now, - } - ) - - scheduler = WorkflowScheduler(db, tmp_path / "storage") - scheduler.execute_run("run_1") - - run = db.get_run("run_1") - assert run["status"] == "COMPLETED" - artifacts = db.list_artifacts("run_1") - assert {item["name"] for item in artifacts} == {"step.text", "step.file_uri", "result"} - - -def test_execute_run_missing_workflow(tmp_path, monkeypatch) -> None: - """验证工作流记录缺失时任务被标记为失败。""" - db = _db(tmp_path) - db.upsert_workflow({"id": "flow", "name": "Flow", "published": 1, "latest_version": 1}) - now = "2026-01-01T00:00:00+00:00" - db.create_run( - { - "id": "run_missing", - "workflow_id": "flow", - "workflow_version": 1, - "status": "QUEUED", - "progress": 0, - "created_at": now, - "updated_at": now, - } - ) - monkeypatch.setattr(db, "get_workflow", lambda workflow_id: None) - scheduler = WorkflowScheduler(db, tmp_path / "storage") - scheduler.execute_run("run_missing") - assert db.get_run("run_missing")["status"] == "FAILED" - - -def test_execute_run_missing_version(tmp_path) -> None: - """验证版本记录缺失时任务被标记为失败。""" - db = _db(tmp_path) - db.upsert_workflow({"id": "flow", "name": "Flow", "published": 1, "latest_version": 1}) - now = "2026-01-01T00:00:00+00:00" - db.create_run( - { - "id": "run_version", - "workflow_id": "flow", - "workflow_version": 1, - "status": "QUEUED", - "progress": 0, - "created_at": now, - "updated_at": now, - } - ) - scheduler = WorkflowScheduler(db, tmp_path / "storage") - scheduler.execute_run("run_version") - assert db.get_run("run_version")["status"] == "FAILED" - - -def test_execute_run_invalid_dag_fails_and_does_not_block_queue(tmp_path) -> None: - """环形 DAG 的历史任务必须立即 FAILED,且不阻塞队列后续任务(R04)。 - - 复现场景:修复前保存校验放过环形定义,执行时拓扑排序抛错但异常发生在 - execute_run 的状态翻转之前 → 任务永远停在 QUEUED,next_queued_run 每轮 - 都拾起同一条队首记录,后面的任务全部被堵死。断言:无效 DAG 的任务被标记 - FAILED(带环错误信息),execute_run 不向调用方抛异常,队首随即前移。 - """ - db = _db(tmp_path) - _register_echo() - # 直接写库模拟修复前遗留的环形版本(保存接口现在会拒绝)。 - cycle_definition = { - "name": "cycle", - "version": 1, - "nodes": [ - {"id": "a", "node_type": "echo", "inputs": {"file_uri": "b.file_uri"}}, - {"id": "b", "node_type": "echo", "inputs": {"file_uri": "a.file_uri"}}, - ], - "edges": [{"from": "a", "to": "b"}, {"from": "b", "to": "a"}], - "entry_inputs": {"video_uri": "file"}, - "final_outputs": {"result": "a.file_uri"}, - } - db.upsert_workflow({"id": "bad-flow", "name": "Bad", "published": 1, "latest_version": 1}) - db.create_workflow_version("bad-flow", 1, cycle_definition) - # 队列里同时放入合法任务,验证它不被无效任务堵住。 - db.upsert_workflow({"id": "flow", "name": "Flow", "published": 1, "latest_version": 1}) - db.create_workflow_version("flow", 1, _echo_definition().to_dict()) - input_file = tmp_path / "input.txt" - input_file.write_text("hello queue", encoding="utf-8") - db.create_run( - { - "id": "run_bad", - "workflow_id": "bad-flow", - "workflow_version": 1, - "status": "QUEUED", - "progress": 0, - "input_uri": str(input_file), - "created_at": "2026-01-01T00:00:00+00:00", - "updated_at": "2026-01-01T00:00:00+00:00", - } - ) - db.create_run( - { - "id": "run_ok", - "workflow_id": "flow", - "workflow_version": 1, - "status": "QUEUED", - "progress": 0, - "input_uri": str(input_file), - "created_at": "2026-01-01T00:00:01+00:00", - "updated_at": "2026-01-01T00:00:01+00:00", - } - ) - scheduler = WorkflowScheduler(db, tmp_path / "storage") - - # 调度器每一轮的取任务 → 执行,不应把环形任务留在 QUEUED。 - first = db.next_queued_run() - assert first["id"] == "run_bad" - scheduler.execute_run(first["id"]) - bad = db.get_run("run_bad") - assert bad["status"] == "FAILED" - assert "cycle" in bad["error"] - - # 队首已前移,后续合法任务正常执行完成。 - assert db.next_queued_run()["id"] == "run_ok" - scheduler.execute_run("run_ok") - assert db.get_run("run_ok")["status"] == "COMPLETED" - assert db.next_queued_run() is None - - -def test_execute_run_unparsable_dag_fails_not_stuck(tmp_path) -> None: - """缺 name 的非法定义同样标记 FAILED,而不是让队列卡死。""" - db = _db(tmp_path) - db.upsert_workflow({"id": "flow", "name": "Flow", "published": 1, "latest_version": 1}) - # from_dict 解析缺 name 的定义会抛 KeyError。 - db.create_workflow_version("flow", 1, {"version": 1, "nodes": [], "edges": []}) - now = "2026-01-01T00:00:00+00:00" - db.create_run( - { - "id": "run_corrupt", - "workflow_id": "flow", - "workflow_version": 1, - "status": "QUEUED", - "progress": 0, - "created_at": now, - "updated_at": now, - } - ) - scheduler = WorkflowScheduler(db, tmp_path / "storage") - scheduler.execute_run("run_corrupt") - run = db.get_run("run_corrupt") - assert run["status"] == "FAILED" - assert run["error"] - assert db.next_queued_run() is None - - -def test_execute_run_missing_node(tmp_path) -> None: - """验证未注册节点被调用时任务失败。""" - db = _db(tmp_path) - definition = WorkflowDefinition( - name="bad", - version=1, - nodes=[ - WorkflowNode( - id="step", - node_type="missing-node", - inputs={"text": "input.video_uri"}, - ) - ], - entry_inputs={"video_uri": "file"}, - ) - db.upsert_workflow({"id": "flow", "name": "Flow", "published": 1, "latest_version": 1}) - db.create_workflow_version("flow", 1, definition.to_dict()) - now = "2026-01-01T00:00:00+00:00" - db.create_run( - { - "id": "run_node", - "workflow_id": "flow", - "workflow_version": 1, - "status": "QUEUED", - "progress": 0, - "input_uri": str(tmp_path / "in.txt"), - "created_at": now, - "updated_at": now, - } - ) - scheduler = WorkflowScheduler(db, tmp_path / "storage") - scheduler.execute_run("run_node") - assert db.get_run("run_node")["status"] == "FAILED" - - -def test_resolve_ref_and_mime(tmp_path) -> None: - """验证输入引用解析、MIME 推断与文件大小读取。""" - db = _db(tmp_path) - scheduler = WorkflowScheduler(db, tmp_path / "storage") - assert scheduler._resolve_ref("input.video", "in.mp4", {}) == "in.mp4" - assert ( - scheduler._resolve_ref( - "a.out", None, {"a": {"out": "result.txt"}} - ) - == "result.txt" - ) - assert scheduler._resolve_ref("a.out", None, {}) is None - assert scheduler._resolve_ref("nodot", "in.mp4", {}) is None - assert scheduler._mime_type("x.srt") == "application/x-subrip" - assert scheduler._mime_type("x.ass") == "text/plain" - assert scheduler._mime_type("x.wav") == "audio/wav" - assert scheduler._mime_type("x.mp4") == "video/mp4" - assert scheduler._mime_type("x.txt") == "text/plain" - assert scheduler._mime_type("x.bin") == "application/octet-stream" - existing = tmp_path / "existing.txt" - existing.write_text("x", encoding="utf-8") - assert scheduler._file_size(str(existing)) == 1 - missing = tmp_path / "missing.bin" - assert scheduler._file_size(str(missing)) == 0 - - -def test_execute_unknown_or_non_queued_run(tmp_path) -> None: - """验证未知任务或非排队任务会被忽略。""" - db = _db(tmp_path) - db.upsert_workflow({"id": "flow", "name": "Flow", "published": 1, "latest_version": 1}) - now = "2026-01-01T00:00:00+00:00" - db.create_run( - { - "id": "run_done", - "workflow_id": "flow", - "workflow_version": 1, - "status": "COMPLETED", - "progress": 1, - "created_at": now, - "updated_at": now, - } - ) - scheduler = WorkflowScheduler(db, tmp_path / "storage") - scheduler.execute_run("missing") - scheduler.execute_run("run_done") - assert db.get_run("run_done")["status"] == "COMPLETED" - - -def test_execute_missing_input(tmp_path) -> None: - """验证输入引用无法解析时任务失败。""" - db = _db(tmp_path) - definition = WorkflowDefinition( - name="missing-input", - version=1, - nodes=[ - WorkflowNode( - id="step", - node_type="echo", - inputs={"text": "missing.output"}, - ) - ], - entry_inputs={"video_uri": "file"}, - ) - db.upsert_workflow({"id": "flow", "name": "Flow", "published": 1, "latest_version": 1}) - db.create_workflow_version("flow", 1, definition.to_dict()) - now = "2026-01-01T00:00:00+00:00" - db.create_run( - { - "id": "run_input", - "workflow_id": "flow", - "workflow_version": 1, - "status": "QUEUED", - "progress": 0, - "input_uri": str(tmp_path / "in.txt"), - "created_at": now, - "updated_at": now, - } - ) - scheduler = WorkflowScheduler(db, tmp_path / "storage") - scheduler.execute_run("run_input") - assert db.get_run("run_input")["status"] == "FAILED" - - -def test_execute_node_failed_response(tmp_path) -> None: - """验证节点返回 failed 时任务被标记为失败。""" - db = _db(tmp_path) - registry.register( - NodeManifest( - id="fail-node", - name="Fail", - version="1", - capability="echo", - repo_dir="nodes", - command=["python", "-m", "fail"], - ), - lambda request: InvokeResponse(status="failed", error="boom"), - ) - definition = WorkflowDefinition( - name="fail-flow", - version=1, - nodes=[ - WorkflowNode( - id="step", - node_type="fail-node", - inputs={"text": "input.video_uri"}, - ) - ], - entry_inputs={"video_uri": "file"}, - ) - db.upsert_workflow({"id": "flow", "name": "Flow", "published": 1, "latest_version": 1}) - db.create_workflow_version("flow", 1, definition.to_dict()) - now = "2026-01-01T00:00:00+00:00" - db.create_run( - { - "id": "run_fail_node", - "workflow_id": "flow", - "workflow_version": 1, - "status": "QUEUED", - "progress": 0, - "input_uri": str(tmp_path / "in.txt"), - "created_at": now, - "updated_at": now, - } - ) - scheduler = WorkflowScheduler(db, tmp_path / "storage") - scheduler.execute_run("run_fail_node") - assert db.get_run("run_fail_node")["status"] == "FAILED" - - -def test_scheduler_start_stop_loop(tmp_path) -> None: - """验证调度线程可重复启动并正常停止。""" - db = _db(tmp_path) - scheduler = WorkflowScheduler(db, tmp_path / "storage", interval_seconds=0.05) - scheduler.start() - try: - scheduler.start() - time.sleep(0.15) - finally: - scheduler.stop() - assert scheduler._thread is None - - -def test_scheduler_background_executes_queued_run(tmp_path) -> None: - """验证后台线程会自动执行排队中的任务。""" - db = _db(tmp_path) - input_file = tmp_path / "input.txt" - input_file.write_text("background", encoding="utf-8") - _register_echo() - db.upsert_workflow({"id": "flow", "name": "Flow", "published": 1, "latest_version": 1}) - db.create_workflow_version("flow", 1, _echo_definition().to_dict()) - now = "2026-01-01T00:00:00+00:00" - db.create_run( - { - "id": "run_bg", - "workflow_id": "flow", - "workflow_version": 1, - "status": "QUEUED", - "progress": 0, - "input_uri": str(input_file), - "created_at": now, - "updated_at": now, - } - ) - scheduler = WorkflowScheduler(db, tmp_path / "storage", interval_seconds=0.05) - scheduler.start() - try: - deadline = time.monotonic() + 10 - while time.monotonic() < deadline: - if db.get_run("run_bg")["status"] in {"COMPLETED", "FAILED"}: - break - time.sleep(0.1) - finally: - scheduler.stop() - assert db.get_run("run_bg")["status"] == "COMPLETED" - - -def test_final_artifact_renamed_with_language_tag(tmp_path) -> None: - """验证最终产物按 上传文件名.语言.时间戳 重命名并登记新 URI。""" - db = _db(tmp_path) - input_file = tmp_path / "movie01.mp4" - input_file.write_text("video", encoding="utf-8") - _register_echo() - definition = WorkflowDefinition( - name="lang-flow", - version=1, - nodes=[ - WorkflowNode( - id="step", - node_type="echo", - params={"target_language": "zh-CN"}, - inputs={"file_uri": "input.video_uri"}, - ) - ], - edges=[], - entry_inputs={"video_uri": "file"}, - final_outputs={"cn_srt": "step.file_uri"}, - ) - db.upsert_workflow({"id": "flow", "name": "Flow", "published": 1, "latest_version": 1}) - db.create_workflow_version("flow", 1, definition.to_dict()) - now = "2026-01-01T00:00:00+00:00" - db.create_run( - { - "id": "run_1", - "workflow_id": "flow", - "workflow_version": 1, - "status": "QUEUED", - "progress": 0, - "input_uri": str(input_file), - "created_at": now, - "updated_at": now, - } - ) - scheduler = WorkflowScheduler(db, tmp_path / "storage") - scheduler.execute_run("run_1") - artifacts = db.list_artifacts("run_1") - final = next(item for item in artifacts if item["name"] == "cn_srt") - filename = Path(final["uri"]).name - # 命名规则:movie01.zh-CN.<14位时间戳>.txt - assert filename.startswith("movie01.zh-CN.") - assert filename.endswith(".txt") - assert Path(final["uri"]).is_file() - # 节点原始 URI 必须保持有效;成品副本与原始文本一致,保证断点可恢复。 - step_artifacts = [item for item in artifacts if item["name"] == "step.file_uri"] - assert Path(step_artifacts[0]["uri"]).read_bytes() == Path(final["uri"]).read_bytes() - - -def test_final_artifact_renamed_fallback_base_and_tag(tmp_path) -> None: - """验证无上传文件时基础名回退 subtitle,无语言参数时标识回退别名。""" - db = _db(tmp_path) - _register_echo() - definition = WorkflowDefinition( - name="fallback-flow", - version=1, - nodes=[ - # 空输入让 echo 走默认文本路径,避免 input_uri 缺失导致解析失败。 - WorkflowNode(id="step", node_type="echo", inputs={}) - ], - entry_inputs={"video_uri": "file"}, - final_outputs={"result": "step.file_uri"}, - ) - db.upsert_workflow({"id": "flow", "name": "Flow", "published": 1, "latest_version": 1}) - db.create_workflow_version("flow", 1, definition.to_dict()) - now = "2026-01-01T00:00:00+00:00" - # 故意不提供 input_uri,验证基础名回退。 - db.create_run( - { - "id": "run_1", - "workflow_id": "flow", - "workflow_version": 1, - "status": "QUEUED", - "progress": 0, - "input_uri": None, - "created_at": now, - "updated_at": now, - } - ) - scheduler = WorkflowScheduler(db, tmp_path / "storage") - scheduler.execute_run("run_1") - final = next(item for item in db.list_artifacts("run_1") if item["name"] == "result") - filename = Path(final["uri"]).name - # 基础名回退 subtitle、标识回退别名 result。 - assert filename.startswith("subtitle.result.") - assert filename.endswith(".txt") - - -def test_execute_run_merges_param_overrides(tmp_path) -> None: - """验证调度执行时把 param_overrides 合并进节点参数。""" - db = _db(tmp_path) - input_file = tmp_path / "input.txt" - input_file.write_text("x", encoding="utf-8") - captured = {} - - def recording_handler(request): - captured["params"] = dict(request.params) - return InvokeResponse(status="completed", outputs={"text": "ok"}) - - registry.register( - NodeManifest( - id="record-node", - name="Record", - version="1", - capability="echo", - repo_dir="nodes", - command=["python", "-m", "record"], - ), - recording_handler, - ) - definition = WorkflowDefinition( - name="ov-flow", - version=1, - nodes=[WorkflowNode(id="step", node_type="record-node", params={"base": 1})], - entry_inputs={"video_uri": "file"}, - ) - db.upsert_workflow({"id": "flow", "name": "F", "published": 1, "latest_version": 1}) - db.create_workflow_version("flow", 1, definition.to_dict()) - now = "2026-01-01T00:00:00+00:00" - db.create_run( - { - "id": "run_ov", - "workflow_id": "flow", - "workflow_version": 1, - "status": "QUEUED", - "progress": 0, - "param_overrides": {"step": {"crop": [0, 0.5, 1, 0.5]}}, - "input_uri": str(input_file), - "created_at": now, - "updated_at": now, - } - ) - scheduler = WorkflowScheduler(db, tmp_path / "storage") - scheduler.execute_run("run_ov") - assert captured["params"] == {"base": 1, "crop": [0, 0.5, 1, 0.5]} - - -def _two_node_definition() -> WorkflowDefinition: - """构造 a→b 两节点工作流:b 引用 a 的输出。""" - return WorkflowDefinition( - name="two-flow", - version=1, - nodes=[ - WorkflowNode(id="a", node_type="x", inputs={"video_uri": "input.video_uri"}), - WorkflowNode(id="b", node_type="x", inputs={"data_uri": "a.data_uri"}), - ], - edges=[WorkflowEdge(from_node="a", to_node="b")], - entry_inputs={"video_uri": "file"}, - final_outputs={"result": "b.data_uri"}, - ) - - -def test_execute_pause_between_nodes_and_resume(tmp_path, monkeypatch) -> None: - """验证运行中暂停:节点边界停下保持 PAUSED;续跑时跳过已完成节点。""" - db = _db(tmp_path) - db.upsert_workflow({"id": "flow", "name": "Flow", "published": 1, "latest_version": 1}) - db.create_workflow_version("flow", 1, _two_node_definition().to_dict()) - now = "2026-01-01T00:00:00+00:00" - db.create_run( - { - "id": "run_pause", - "workflow_id": "flow", - "workflow_version": 1, - "status": "QUEUED", - "progress": 0, - "input_uri": str(tmp_path / "in.txt"), - "created_at": now, - "updated_at": now, - } - ) - data_file = tmp_path / "data.bin" - data_file.write_bytes(b"x") - - calls: list[str] = [] - - def fake_invoke(node_type, request): - # registry.invoke 首参是 node_type;用产物目录名(steps/)识别节点。 - node_id = Path(request.output_dir).name - calls.append(node_id) - # 第一个节点完成后立刻暂停任务,模拟用户在运行中点暂停。 - if node_id == "a": - db.pause_run("run_pause", now) - return InvokeResponse(status="completed", outputs={"data_uri": str(data_file)}) - - monkeypatch.setattr(registry, "invoke", fake_invoke) - scheduler = WorkflowScheduler(db, tmp_path / "storage") - scheduler.execute_run("run_pause") - assert db.get_run("run_pause")["status"] == "PAUSED" - assert calls == ["a"] # 节点 b 未执行。 - - # 继续:恢复排队并再次执行,节点 a 已产出结果应被跳过,只执行 b。 - db.resume_run("run_pause", now) - scheduler.execute_run("run_pause") - assert db.get_run("run_pause")["status"] == "COMPLETED" - assert calls == ["a", "b"] - artifacts = db.list_artifacts("run_pause") - assert {item["name"] for item in artifacts} == {"a.data_uri", "b.data_uri", "result"} - - -def test_execute_paused_run_not_run(tmp_path, monkeypatch) -> None: - """验证非可执行状态(如 RUNNING 之外的值)的任务不会被执行。""" - db = _db(tmp_path) - db.upsert_workflow({"id": "flow", "name": "Flow", "published": 1, "latest_version": 1}) - db.create_workflow_version("flow", 1, _two_node_definition().to_dict()) - now = "2026-01-01T00:00:00+00:00" - db.create_run( - { - "id": "run_done", - "workflow_id": "flow", - "workflow_version": 1, - "status": "COMPLETED", - "progress": 1.0, - "created_at": now, - "updated_at": now, - } - ) - called = [] - - def fake_invoke(node_id, request): - called.append(node_id) - return InvokeResponse(status="completed", outputs={}) - - monkeypatch.setattr(registry, "invoke", fake_invoke) - WorkflowScheduler(db, tmp_path / "storage").execute_run("run_done") - assert called == [] - - -def test_execute_paused_run_stays_paused(tmp_path, monkeypatch) -> None: - """PAUSED 任务不被 execute_run 复活:不置 RUNNING、不执行任何节点。 - - 修复回归:PAUSED 任务被调度器拾起后曾先置 RUNNING 再检查,节点循环 - 读到的是刚改的 RUNNING 状态,"暂停检查"永远不成立 → 任务被复活继续跑 - (用户观察到的"点击暂停反而开始任务")。修复后 PAUSED 直接返回保持暂停。 - """ - db = _db(tmp_path) - db.upsert_workflow({"id": "flow", "name": "Flow", "published": 1, "latest_version": 1}) - db.create_workflow_version("flow", 1, _two_node_definition().to_dict()) - now = "2026-01-01T00:00:00+00:00" - db.create_run( - { - "id": "run_paused", - "workflow_id": "flow", - "workflow_version": 1, - "status": "PAUSED", - "progress": 0.5, - "current_node_id": "a", - "created_at": now, - "updated_at": now, - } - ) - called = [] - - def fake_invoke(node_type, request): - called.append(node_type) - return InvokeResponse(status="completed", outputs={}) - - monkeypatch.setattr(registry, "invoke", fake_invoke) - scheduler = WorkflowScheduler(db, tmp_path / "storage") - scheduler.execute_run("run_paused") - # 状态保持 PAUSED(未被置为 RUNNING),节点一个都不执行。 - assert db.get_run("run_paused")["status"] == "PAUSED" - assert called == [] - -def test_execute_paused_during_node_keeps_paused(tmp_path, monkeypatch) -> None: - """节点内被暂停(节点检测到暂停信号后中止):保持 PAUSED 不标 FAILED。 - - 节点内暂停响应:subtitle-ocr 检查到 paused.flag 后中止并返回失败; - 调度器捕获节点异常时应检查任务状态——若已被置为 PAUSED(用户点了暂停), - 则保持 PAUSED 等待 resume 从断点续跑,而不是覆盖为 FAILED。 - """ - db = _db(tmp_path) - db.upsert_workflow({"id": "flow", "name": "Flow", "published": 1, "latest_version": 1}) - db.create_workflow_version("flow", 1, _two_node_definition().to_dict()) - now = "2026-01-01T00:00:00+00:00" - db.create_run( - { - "id": "run_paused", - "workflow_id": "flow", - "workflow_version": 1, - "status": "QUEUED", - "progress": 0, - "input_uri": str(tmp_path / "in.txt"), - "created_at": now, - "updated_at": now, - } - ) - - def fake_invoke(node_type, request): - # 模拟节点内暂停:任务已被置 PAUSED,节点随后中止并抛异常。 - db.pause_run("run_paused", now) - raise RuntimeError("OCR interrupted by pause") - - monkeypatch.setattr(registry, "invoke", fake_invoke) - WorkflowScheduler(db, tmp_path / "storage").execute_run("run_paused") - # 保持 PAUSED,不标 FAILED、不写 error(等待用户 resume 断点续跑)。 - run = db.get_run("run_paused") - assert run["status"] == "PAUSED" - assert run["error"] is None - - -def test_scheduler_loop_survives_poll_exception(tmp_path, monkeypatch) -> None: - """调度轮询遇异常不退出线程:下一轮继续执行排队任务。 - - 修复回归:_loop 中 next_queued_run/execute_run 的未捕获异常曾杀死调度 - 线程(worker 进程只剩 uvicorn 主线程),任务永远停留在 QUEUED—— - run_011d01f19999 实际发生:回退 QUEUED 后调度器不再拾起,新配置 - (pool_max_workers=20)因此从未执行。 - """ - db = _db(tmp_path) - input_file = tmp_path / "input.txt" - input_file.write_text("resilient", encoding="utf-8") - _register_echo() - db.upsert_workflow({"id": "flow", "name": "Flow", "published": 1, "latest_version": 1}) - db.create_workflow_version("flow", 1, _echo_definition().to_dict()) - now = "2026-01-01T00:00:00+00:00" - db.create_run( - { - "id": "run_resilient", - "workflow_id": "flow", - "workflow_version": 1, - "status": "QUEUED", - "progress": 0, - "input_uri": str(input_file), - "created_at": now, - "updated_at": now, - } - ) - - # 第一次轮询抛异常(模拟数据库抖动等),后续正常。 - calls = {"n": 0} - real_next = db.next_queued_run - - def flaky_next(): - calls["n"] += 1 - if calls["n"] == 1: - raise RuntimeError("transient db error") - return real_next() - - monkeypatch.setattr(db, "next_queued_run", flaky_next) - scheduler = WorkflowScheduler(db, tmp_path / "storage", interval_seconds=0.05) - scheduler.start() - try: - deadline = time.monotonic() + 10 - while time.monotonic() < deadline: - if db.get_run("run_resilient")["status"] in {"COMPLETED", "FAILED"}: - break - time.sleep(0.1) - finally: - scheduler.stop() - # 第一次异常后调度线程仍存活,第二轮回合把任务执行完成。 - assert db.get_run("run_resilient")["status"] == "COMPLETED" - assert calls["n"] >= 2 - -def test_execute_pause_after_last_node_keeps_paused(tmp_path, monkeypatch) -> None: - """验证全部节点完成但运行中被暂停时保持 PAUSED;续跑补做收尾后完成。""" - db = _db(tmp_path) - db.upsert_workflow({"id": "flow", "name": "Flow", "published": 1, "latest_version": 1}) - db.create_workflow_version("flow", 1, _two_node_definition().to_dict()) - now = "2026-01-01T00:00:00+00:00" - db.create_run( - { - "id": "run_tail", - "workflow_id": "flow", - "workflow_version": 1, - "status": "QUEUED", - "progress": 0, - "input_uri": str(tmp_path / "in.txt"), - "created_at": now, - "updated_at": now, - } - ) - data_file = tmp_path / "data.bin" - data_file.write_bytes(b"x") - calls: list[str] = [] - - def fake_invoke(node_type, request): - node_id = Path(request.output_dir).name - calls.append(node_id) - if node_id == "b": # 最后一个节点执行时暂停。 - db.pause_run("run_tail", now) - return InvokeResponse(status="completed", outputs={"data_uri": str(data_file)}) - - monkeypatch.setattr(registry, "invoke", fake_invoke) - scheduler = WorkflowScheduler(db, tmp_path / "storage") - scheduler.execute_run("run_tail") - # 全部节点已执行,但收尾前被暂停 → 保持 PAUSED 而不是 COMPLETED。 - assert db.get_run("run_tail")["status"] == "PAUSED" - assert calls == ["a", "b"] - - # 续跑:节点产物齐备全部跳过,补做收尾后完成。 - db.resume_run("run_tail", now) - scheduler.execute_run("run_tail") - assert db.get_run("run_tail")["status"] == "COMPLETED" - assert calls == ["a", "b"] diff --git a/tests/test_seed.py b/tests/test_seed.py deleted file mode 100644 index 80ee122..0000000 --- a/tests/test_seed.py +++ /dev/null @@ -1,113 +0,0 @@ -"""种子数据测试。 - -验证从 workflows/*.json 数据文件加载默认工作流、幂等性,以及 -开启种子与调度器后的应用生命周期。工作流定义来自数据文件而非代码。 -""" - -from pathlib import Path - -from fastapi.testclient import TestClient - -from wov_app.db import Database -from wov_app.main import app -from wov_app.seed import seed_default_workflows - -# 单体根目录:tests/ 的上一级。 -WORKSPACE = Path(__file__).resolve().parent.parent - - -def test_seed_default_workflows_idempotent(tmp_path) -> None: - """验证从数据文件加载 demo/zh-direct/ocr-subtitle/learn-translate 工作流且重复调用幂等。""" - db = Database(tmp_path / "wov.db") - created = seed_default_workflows(db) - assert created == 4 - assert db.get_workflow("demo") is not None - assert db.get_workflow("zh-direct") is not None - - # demo:asr 显式声明 model_path 与长音频参数,模型选择完全数据化。 - demo = db.get_latest_workflow_version("demo")["definition"] - assert demo["name"] == "视频字幕生成" - demo_asr = next(node for node in demo["nodes"] if node["id"] == "asr") - assert demo_asr["params"]["model_path"] == "faster-whisper-large-v2" - assert demo_asr["params"]["condition_on_previous_text"] is False - - # zh-direct:使用中文直出模型并开启翻译任务。 - zh = db.get_latest_workflow_version("zh-direct")["definition"] - assert zh["name"] == "中文直出字幕" - zh_asr = next(node for node in zh["nodes"] if node["id"] == "asr") - assert zh_asr["params"]["model_path"] == "whisper-large-v2-translate-zh-v0.2-st-ct2" - assert zh_asr["params"]["task"] == "translate" - - # learn-translate:应用本次 decode_full 修复的新工作流。 - learn = db.get_latest_workflow_version("learn-translate")["definition"] - learn_asr = next(node for node in learn["nodes"] if node["id"] == "asr") - assert learn_asr["params"]["decode_full"] is True - - # 再次调用不重复创建。 - assert seed_default_workflows(db) == 0 - assert len(db.list_workflow_versions("demo")) == 1 - - -def test_seed_custom_dir_and_empty(tmp_path) -> None: - """验证自定义数据目录的加载与空目录返回 0。""" - db = Database(tmp_path / "wov.db") - custom = tmp_path / "workflows" - custom.mkdir() - (custom / "a.json").write_text( - """ - { - "id": "flow-a", - "name": "Flow A", - "description": "custom", - "version": 1, - "definition": { - "name": "Flow A", - "version": 1, - "nodes": [{"id": "step", "node_type": "echo"}], - "edges": [], - "entry_inputs": {}, - "final_outputs": {} - } - } - """, - encoding="utf-8", - ) - assert seed_default_workflows(db, custom) == 1 - assert db.get_workflow("flow-a") is not None - # 空目录返回 0。 - empty = tmp_path / "empty" - empty.mkdir() - assert seed_default_workflows(db, empty) == 0 - # 已存在的工作流被跳过。 - assert seed_default_workflows(db, custom) == 0 - - -def test_lifespan_with_seed_and_scheduler(monkeypatch) -> None: - """验证启用自动种子与调度器后应用正常启动,demo 与中文直出应用均可见。""" - monkeypatch.setenv("WOV_AUTO_SEED", "1") - monkeypatch.setenv("WOV_SCHEDULER_ENABLED", "1") - with TestClient(app) as client: - apps = client.get("/api/apps") - assert apps.status_code == 200 - assert any(item["id"] == "demo" for item in apps.json()) - assert any(item["id"] == "zh-direct" for item in apps.json()) - - -def test_seed_workflows_have_chunk_seconds(tmp_path) -> None: - """验证内置工作流的 asr 节点均显式声明分块参数。""" - db = Database(tmp_path / "wov.db") - seed_default_workflows(db) - for workflow_id in ("demo", "zh-direct"): - definition = db.get_latest_workflow_version(workflow_id)["definition"] - asr = next(node for node in definition["nodes"] if node["id"] == "asr") - assert asr["params"]["chunk_seconds"] == 60 - - -def test_seed_workflows_vad_filter_off(tmp_path) -> None: - """验证内置工作流 asr 显式开启 VAD。""" - db = Database(tmp_path / "wov.db") - seed_default_workflows(db) - for workflow_id in ("demo", "zh-direct"): - definition = db.get_latest_workflow_version(workflow_id)["definition"] - asr = next(node for node in definition["nodes"] if node["id"] == "asr") - assert asr["params"]["vad_filter"] is True diff --git a/tests/test_subtitle_correction.py b/tests/test_subtitle_correction.py deleted file mode 100644 index 7c829ba..0000000 --- a/tests/test_subtitle_correction.py +++ /dev/null @@ -1,202 +0,0 @@ -"""字幕领域纠错节点测试(先红后绿)。 - -验证: -1. _is_fragment 正确过滤纯语气词/碎片(非过拟合)。 -2. _build_context 只保留有效上下文(±60s 内实义句),目标保留。 -3. _extract_target_line 从 LLM 输出解析目标行译文。 -4. 真实 LLM 集成:对"从未在提示词出现的 ASR 误听"(バナナ/マンゴー→性器官) - 能按领域词表+上下文推断,证明不过拟合(V5 验证结果固化为回归)。 -提示词不含任何 ASR 误听具体例子,只有通用领域词表。 -""" - -from __future__ import annotations - -import os -from pathlib import Path - -import pytest - -from nodes.subtitle_correction import ( - PROPER_SESSION_WORDS, - _build_context, - _extract_target_line, - _is_fragment, - _system_prompt, -) -from wov_sdk.models import InvokeRequest - -WORKSPACE = Path(__file__).resolve().parent.parent -TRANSCRIPT = Path("/home/cat/Downloads/192.168.123.70/202609060835/run_af2987b161a3/steps/asr/transcript.srt") - - -# --------------------------------------------------------------------------- -# 单元测试:碎片判定 / 上下文构建 / 目标行解析 -# --------------------------------------------------------------------------- - - -def test_is_fragment_pure_words() -> None: - """纯语气词/单音节应判为碎片。""" - for t in ["あ", "ん", "うん", "はい", "あっ", "あー", "あ〜", "ああ", ""]: - assert _is_fragment(t), f"'{t}' 应为碎片" - - -def test_is_fragment_meaningful() -> None: - """有实义的句子不应判为碎片(即使含假名)。""" - for t in ["気持ちいい", "難しい", "ごめんなさい", "手先が違う所に当たり合い"]: - assert not _is_fragment(t), f"'{t}' 不应为碎片" - - -def test_build_context_filters_fragments() -> None: - """上下文过滤语气词,但目标条目始终保留。""" - entries = [ - {"start": 0.0, "end": 1.0, "text": "あ"}, - {"start": 2.0, "end": 3.0, "text": "気持ちいい"}, - {"start": 4.0, "end": 5.0, "text": "手先が当たる"}, - {"start": 6.0, "end": 7.0, "text": "うん"}, - ] - ctx = _build_context(entries, target_index=2) - assert "気持ちいい" in ctx - assert "手先が当たる" in ctx - assert "あ\n" not in ctx # 语气词被过滤 - assert "うん" not in ctx - - -def test_build_context_keeps_target() -> None: - """目标条目即使本身是语气词也保留并标记。""" - entries = [ - {"start": 0.0, "end": 1.0, "text": "あ"}, - {"start": 2.0, "end": 3.0, "text": "うん"}, - ] - ctx = _build_context(entries, target_index=1) - assert "<-- 目标" in ctx - assert "うん" in ctx - - -def test_extract_target_line() -> None: - """从 LLM 输出解析与目标时间最接近的译文行。""" - content = "6656.60 好舒服\n6704.10 啊,肉棒撞到了别的地方\n6708.10 啊,肉棒顶到舒服的地方" - assert "好舒服" in _extract_target_line(content, 6656.60) - # 目标 6704 附近 - assert "肉棒撞到了别的地方" in _extract_target_line(content, 6704.10) - - -def test_system_prompt_has_generic_domain_terms_no_noise_examples() -> None: - """系统提示词含通用领域词表,但不含任何 ASR 误听噪声词(不过拟合)。""" - prompt = _system_prompt("zh-CN") - assert "チンポ" in prompt - assert "マンコ" in prompt - assert "バナナ" in prompt or "マンゴー" in prompt - # 关键:绝不含具体 ASR 误听形式(本次视频的 チェーンバー/手先)。 - assert "チェーンバー" not in prompt - assert "手先" not in prompt - - -def test_proper_session_words_are_generic() -> None: - """领域词表只含通用日文性器官词,不含误听噪声词。""" - assert "チンポ" in PROPER_SESSION_WORDS - assert "マンコ" in PROPER_SESSION_WORDS - assert "チェーンバー" not in PROPER_SESSION_WORDS # 非通用词 - assert "手先" not in PROPER_SESSION_WORDS # 非通用词 - - -# --------------------------------------------------------------------------- -# 真实 LLM 集成:泛化验证(B 组场景) -# --------------------------------------------------------------------------- - - -def _llm_ok() -> bool: - try: - from dotenv import load_dotenv - - load_dotenv(WORKSPACE / ".env") - except Exception: - pass - return bool(os.getenv("LLM_API_KEY")) - - -@pytest.mark.integration -def test_generic_correction_generalizes_to_unseen_mishearing() -> None: - """泛化回归:对'未在提示词出现'的误听(バナナ/マンゴー→性器官)能正确推断。 - - 提示词只有通用领域词表(含バナナ/マンゴー),没有具体误听例子。 - 若 LLM 能按上下文把バナナ理解为肉棒、マンゴー理解为小穴,证明不过拟合。 - """ - if not _llm_ok(): - pytest.skip("未配置 LLM_API_KEY,跳过真实 LLM 集成测试") - - from nodes.subtitle_correction import correct_entry - - # 模拟新视频:ASR 把 チンポ/マンコ 听成 バナナ/マンゴー(提示词中仅有通用词表)。 - entries = [ - {"start": 1200.0, "end": 1203.0, "text": "相手がバナナをしゃぶってくれて"}, - {"start": 1203.0, "end": 1206.0, "text": "そろそろマンゴーが濡れてきました"}, - {"start": 1206.0, "end": 1209.0, "text": "気持ちいいところに当たってるね"}, - {"start": 1220.0, "end": 1224.0, "text": "もっとマンゴーを舐めてください"}, - {"start": 1224.0, "end": 1226.0, "text": "いっぱい出してね"}, - ] - # 目标改为含误听词'マンゴー'的条目(索引 3):验证 LLM 结合上下文和 - # 领域词表把'マンゴー'推断为小穴,而非字面译'芒果'。 - target = correct_entry(entries[3], entries, 3, {"target_language": "zh-CN"}) - # 泛化判定:输出应含性器官语义(肉棒/阴部/敏感处等),而非字面"香蕉/芒果"。 - flagged = [k for k in ("肉棒", "鸡巴", "阴部", "小穴", "敏感") if k in target] - assert flagged, ( - f"泛化失败:模型仍字面直译,输出'{target}'(应结合领域表推断性器官)" - ) - -@pytest.mark.integration -def test_invoke_end_to_end_real_transcript(tmp_path) -> None: - """真实 invoke 端到端:读真实 transcript,逐条纠错,产出 corrected.srt。 - - 覆盖 invoke 全流程(文件校验、逐条纠错、SRT 序列化、写文件)。 - """ - if not _llm_ok(): - pytest.skip("未配置 LLM_API_KEY,跳过真实 LLM 集成测试") - if not TRANSCRIPT.is_file(): - pytest.skip("缺少真实 transcript.srt,跳过") - - from nodes.subtitle_correction import invoke - - out = tmp_path / "out" - resp = invoke(InvokeRequest( - run_id="corr_e2e", - node_instance_id="", - inputs={"srt_uri": str(TRANSCRIPT)}, - params={"target_language": "zh-CN"}, - output_dir=str(out), - )) - assert resp.status == "completed", resp.error - assert Path(resp.outputs["srt_uri"]).is_file() - content = Path(resp.outputs["srt_uri"]).read_text(encoding="utf-8") - assert "--> " in content # 合法 SRT - - -@pytest.mark.integration -def test_invoke_missing_srt_uri(tmp_path) -> None: - """缺少 srt_uri -> failed。""" - from nodes.subtitle_correction import invoke - - resp = invoke(InvokeRequest( - run_id="x", node_instance_id="", inputs={}, output_dir=str(tmp_path) - )) - assert resp.status == "failed" - - -def test_extract_target_line_no_match_returns_empty() -> None: - """LLM 输出无法匹配目标时间时返回空串。""" - from nodes.subtitle_correction import _extract_target_line - - assert _extract_target_line("随便一段话没有数字", 1234.5) == "" - - -def test_serialize_srt_roundtrip() -> None: - """SRT 序列化往返:条目 -> 文本 -> 再解析条数一致。""" - from nodes.subtitle_correction import _serialize_srt - from tests.realdata_contract import parse_srt_entries - - entries = [ - {"start": 0.0, "end": 2.0, "text": "你好"}, - {"start": 2.0, "end": 4.0, "text": "世界"}, - ] - srt = _serialize_srt(entries) - assert "00:00:00,000 --> 00:00:02,000" in srt - assert len(parse_srt_entries(srt)) == 2 diff --git a/tests/test_subtitle_ocr_order_threading.py b/tests/test_subtitle_ocr_order_threading.py deleted file mode 100644 index a30e922..0000000 --- a/tests/test_subtitle_ocr_order_threading.py +++ /dev/null @@ -1,314 +0,0 @@ -"""多线程下字幕顺序正确性测试(模拟真实 API 返回,全量真实数据)。 - -目标:验证 subtitle-ocr 在**多线程**执行时能否正确处理字幕顺序。 - -R06 更新:下述 1666 条文件保留作历史记录,含跨空白帧合并缺陷,已不作为 -逐字节正确性标准。基线由真实单线程/完整成功存档组装产生(1942 条), -同时逐采样点检查正文和空白,要求多线程及断点结果与新基线一致。 - -- 不走真实 vlm-ocr(Ollama)网络调用:registry.invoke 被替换为 - FakeVlmOcrApi,按 image_uri 文件名中的帧号,直接从测试数据(真实任务 - run_ac7f480a3ccb 的**全量**逐帧 OCR 结果)取该帧文本返回,模拟真实 - API 返回结构;输入可以是文件名(frames_manifest / image_uri); -- **真实 OCR 延迟模拟**:真实 vlm-ocr 每次调用延迟不可预判——大部分帧快、 - 少量帧明显慢(复杂画面/模型排队,真实约 0.1s~5s,测试按比例缩放)。 - 假 API 用种子化随机生成同样的快/慢分布(约 8% 慢帧),制造真实波动下 - 的乱序完成,对线程池的保序能力施加最贴近真实情况的压力; -- 核心断言:多线程(4/16 线程)产出的 SRT 与**用户确认过的精确结果**逐字节 - 一致——该结果正是真实任务 run_ac7f480a3ccb 以**单线程**(workflow v4 - pool 1/1)运行产出并经用户确认的,因此逐字节一致即证明"多线程 == 单线程"; - 同时断言并发真实发生、完成顺序确实乱序、全量时间轴严格递增、每条字幕 - 与其起始时刻帧的文本对齐。 - -全量真实数据(testdata/,常驻夹具,真实任务 run_ac7f480a3ccb 全部 14236 帧): -- frames_manifest_full.json:完整帧清单(14236 条,image_uri 改为文件名); -- ocr_frames_full.json:{帧号: 该帧 OCR 文本}(位置 p ↔ 帧 p+1); -- ocr_srt_run_ac7f480a3ccb.srt:该任务单线程运行产出、用户确认过的精确结果(1666 条)。 -""" - -import json -import random -import re -import threading -import time -from pathlib import Path - -from wov_sdk.models import InvokeRequest, InvokeResponse - -WORKSPACE = Path(__file__).resolve().parent.parent -TESTDATA = WORKSPACE / "testdata" -FULL_MANIFEST = TESTDATA / "frames_manifest_full.json" -FULL_OCR_TEXTS = TESTDATA / "ocr_frames_full.json" -CONFIRMED_SRT = TESTDATA / "ocr_srt_run_ac7f480a3ccb.srt" -TOTAL_FRAMES = 14236 - -# 帧号解析:image_uri 文件名形如 frame_0411.png。 -_FRAME_RE = re.compile(r".*frame_(\d+)\.png") - - -def _frame_no(uri: str) -> int: - """从 image_uri 文件名解析帧号。""" - return int(_FRAME_RE.match(Path(uri).name).group(1)) - - -def _ts_to_seconds(ts: str) -> float: - """SRT 时间戳(HH:MM:SS,mmm)转秒。""" - h, m, s = ts.replace(",", ".").split(":") - return int(h) * 3600 + int(m) * 60 + float(s) - - -class FakeVlmOcrApi: - """模拟真实 vlm-ocr API:不发起真实网络调用,从测试数据返回该帧 OCR 文本。 - - - 输入是 image_uri 文件名,解析帧号后从全量真实 OCR 结果({帧号: 文本}) - 取该帧文本返回,模拟真实 API 返回结构(status=completed / outputs.text); - - **可变延迟模拟真实 OCR**:每次调用独立随机,大部分帧快 - (0.2~2ms,对应真实约 0.1~1s),约 slow_ratio 比例的帧明显慢 - (6~18ms,对应真实约 3~9s,如复杂画面/模型排队)。延迟不可预判, - 对线程池的乱序恢复能力施加与真实情况一致的随机压力; - - 线程安全地记录并发峰值、完成顺序与每次延迟,供断言"多线程确实发生、 - 乱序完成、且存在明显慢帧"。 - """ - - def __init__(self, texts_by_frame: dict[int, str], seed: int = 20260817, - fast_ms: float = 1.0, slow_ms: float = 12.0, - slow_ratio: float = 0.08) -> None: - self._texts_by_frame = texts_by_frame - self._fast_ms = fast_ms - self._slow_ms = slow_ms - self._slow_ratio = slow_ratio - # 种子化随机源:延迟波动可复现(固定种子 → 测试确定性,不 flaky)。 - self._rng = random.Random(seed) - self._lock = threading.Lock() - self.active = 0 - self.max_active = 0 - # 完成顺序(帧号):用于断言乱序完成确实发生。 - self.completed_frames: list[int] = [] - # 每次调用的实际延迟(毫秒):用于断言快/慢分布确实发生。 - self.delays_ms: list[float] = [] - - def __call__(self, node_id: str, request: InvokeRequest) -> InvokeResponse: - frame_no = _frame_no(request.inputs["image_uri"]) - with self._lock: - self.active += 1 - self.max_active = max(self.max_active, self.active) - try: - # 真实 OCR 延迟:慢帧比例固定,具体哪帧慢由随机决定(不可预判)。 - if self._rng.random() < self._slow_ratio: - delay = self._slow_ms * (0.5 + self._rng.random()) - else: - delay = self._fast_ms * (0.2 + self._rng.random() * 1.8) - self.delays_ms.append(delay) - time.sleep(delay / 1000.0) - text = self._texts_by_frame[frame_no] - finally: - with self._lock: - self.active -= 1 - self.completed_frames.append(frame_no) - # 与真实 vlm-ocr 节点一致的返回结构。 - return InvokeResponse(status="completed", outputs={"text": text}) - - -def _run_ocr(monkeypatch, manifest_path: Path, texts_by_frame: dict[int, str], - pool_min: int, pool_max: int, output_dir: Path, seed: int, - ) -> tuple[Path, FakeVlmOcrApi]: - """用给定线程配置运行 subtitle-ocr,返回 (产物路径, 假 API 实例)。""" - from nodes.subtitle_ocr import invoke as ocr_invoke - - # 单线程基线无需模拟网络等待,多线程仍保留真实延迟分布验证乱序完成。 - fake = (FakeVlmOcrApi(texts_by_frame, seed=seed, fast_ms=0, slow_ms=0) - if pool_max == 1 else FakeVlmOcrApi(texts_by_frame, seed=seed)) - monkeypatch.setattr("wov_app.registry.invoke", fake) - response = ocr_invoke( - InvokeRequest( - run_id="order_test", - node_instance_id="", - inputs={"frames_manifest": str(manifest_path)}, - params={"pool_min_workers": pool_min, "pool_max_workers": pool_max}, - output_dir=str(output_dir), - ) - ) - assert response.status == "completed", response.error - return Path(response.outputs["srt_uri"]), fake - - -def _load_full_data() -> tuple[list[dict], dict[int, str]]: - """加载全量夹具:manifest 与 {帧号: 文本}。""" - manifest = json.loads(FULL_MANIFEST.read_text(encoding="utf-8")) - texts_by_frame = { - int(key): value for key, value in json.loads(FULL_OCR_TEXTS.read_text(encoding="utf-8")).items() - } - return manifest, texts_by_frame - - -def _assert_alignment(srt_text: str, manifest: list[dict], texts_by_frame: dict[int, str]) -> None: - """核心对齐断言:每条字幕的起始时刻对应的帧,其 OCR 文本必须就是本条字幕文本。 - - 这正是"多线程下顺序正确"的最终验证:无论线程如何并发/乱序完成, - 每条字幕贴的时刻必须是它真实来源帧的时刻。 - """ - time_text = { - round(float(entry["time"]), 3): texts_by_frame[_frame_no(entry["image_uri"])] - for entry in manifest - } - blocks = re.findall( - r"(\d{2}:\d{2}:\d{2},\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2},\d{3})\s*\n(.*?)(?=\n\s*\d+\s*\n|\Z)", - srt_text, re.DOTALL, - ) - times = [] - for start, _end, text in blocks: - start_s = _ts_to_seconds(start) - # 帧时间与 SRT 时间戳间允许 ±2ms 容差:format_timestamp 用 int 截断, - # 浮点 256.258 会以 256.25799.. 截断为 256,257(真实运行同样行为)。 - best = min(time_text, key=lambda t: abs(t - start_s)) - assert abs(best - start_s) <= 0.002, f"字幕起始时刻 {start_s}s 无对应帧" - assert time_text[best] == text.strip(), f"时刻 {start_s}s 的文本与帧不一致" - # 每个采样点都须匹配正文,不能让同一句字幕跨过无文字帧。 - end_s = _ts_to_seconds(_end) - covered = [value for timestamp, value in time_text.items() - if best <= timestamp < end_s - 0.002] - assert covered and all(value == text.strip() for value in covered) - times.append(start_s) - assert all(a < b for a, b in zip(times, times[1:])), "时间轴必须严格递增" - - -class TestSubtitleOcrOrderUnderThreading: - """多线程下字幕顺序正确性测试类(全量 14236 帧真实数据 + 真实可变延迟)。 - - 确认结果(testdata/ocr_srt_run_ac7f480a3ccb.srt)是真实任务 run_ac7f480a3ccb - 以**单线程**(workflow v4 pool 1/1)运行产出、经用户确认的精确结果: - - 多线程(4/16 线程)+ 真实可变延迟下重放全量数据,产物必须与它逐字节一致; - - 同时必须真实发生"多线程 + 乱序完成 + 明显慢帧",而顺序仍正确。 - 数据全部为常驻夹具,测试始终执行(不依赖 gitignored 数据)。 - """ - - def test_full_real_data_variable_latency_keeps_order(self, monkeypatch, tmp_path) -> None: - """全量真实数据 + 真实可变延迟:多线程产物与单线程确认结果逐字节一致。""" - manifest, texts_by_frame = _load_full_data() - # R06:旧 1666 条结果跨空白合并,改用当前真实单线程路径构建基线。 - confirmed_path, _ = _run_ocr(monkeypatch, FULL_MANIFEST, texts_by_frame, - 1, 1, tmp_path / "single", 20260817) - confirmed = confirmed_path.read_text(encoding="utf-8") - assert confirmed.count("-->") == 1942 - - outputs: dict[tuple, str] = {} - fakes: dict[tuple, FakeVlmOcrApi] = {} - for pool_min, pool_max in ((4, 4), (16, 16)): - out_dir = tmp_path / f"p{pool_min}-{pool_max}" - srt_path, fake = _run_ocr( - monkeypatch, FULL_MANIFEST, texts_by_frame, - pool_min=pool_min, pool_max=pool_max, - output_dir=out_dir, seed=20260817, - ) - outputs[(pool_min, pool_max)] = srt_path.read_text(encoding="utf-8") - fakes[(pool_min, pool_max)] = fake - - # ① 多线程产物与本次真实单线程运行基线逐字节一致(R06 修正跨空白)。 - assert outputs[(4, 4)] == confirmed, "4 线程产物与确认结果不一致" - assert outputs[(16, 16)] == confirmed, "16 线程产物与确认结果不一致" - assert outputs[(4, 4)] == outputs[(16, 16)] - - # ② 多线程确实发生(并发峰值>1),且可变延迟下完成顺序乱序(靠后帧 - # 先完成/慢帧滞后),但输出仍与确认结果一致——线程池正确恢复了顺序。 - for (pool_min, pool_max), fake in fakes.items(): - assert fake.max_active > 1, f"{pool_max} 线程配置下应真实并发" - assert fake.completed_frames != sorted(fake.completed_frames), \ - f"{pool_max} 线程下可变延迟应产生乱序完成" - - # ③ 延迟模拟符合真实 OCR:存在明显慢帧(>3 倍快帧均值),也含极快帧。 - for (_, pool_max), fake in fakes.items(): - delays = fake.delays_ms - assert len(delays) == TOTAL_FRAMES, "每帧都应产生一次调用延迟" - assert max(delays) > 3.0, f"{pool_max} 线程下应存在明显慢帧(真实 OCR 延迟波动)" - assert min(delays) < 1.0, "应存在快帧(大部分帧响应快)" - - # ④ 全量时间轴严格递增 + 每条字幕与其起始时刻帧的文本对齐。 - _assert_alignment(confirmed, manifest, texts_by_frame) - - # ⑤ 已知真实内容存在于结果中(确认结果的代表性条目)。 - for line in ("北冈小姐", "这是特别病房患者的病历表", "应该已经察觉到 至今为止的一切了吧"): - assert line in confirmed, line - - -def test_ocr_resumes_from_partial_checkpoint(monkeypatch, tmp_path) -> None: - """节点级断点:预写部分帧存档后运行,只处理未处理帧,产物与全量一致。 - - 模拟中断时已落盘的 ocr_partial.jsonl(前 100 帧已处理):invoke 应只对 - 未处理帧调用 vlm-ocr,并把存档文本与新增文本合并,最终 SRT 与全量一次 - 跑完逐字节一致——重启不浪费已处理的帧。 - """ - manifest, texts_by_frame = _load_full_data() - # 基线走完整 OCR 路径,包含超长输出跳过规则,不手写业务处理后的存档。 - baseline_path, _ = _run_ocr(monkeypatch, FULL_MANIFEST, texts_by_frame, - 1, 1, tmp_path / "baseline", 99) - confirmed = baseline_path.read_text(encoding="utf-8") - assert confirmed.count("-->") == 1942 - out_dir = tmp_path / "resume" - partial_path = out_dir / "ocr_partial.jsonl" - partial_path.parent.mkdir(parents=True) - lines = [] - for i in range(100): - # 存档按 0-based 帧序号记录;manifest[i] 的帧号 = i+1。 - lines.append( - json.dumps({"frame": i, "text": texts_by_frame[i + 1], "status": "completed"}, ensure_ascii=False) - ) - if i == 50: - lines.append("") # 空行:_load_partial 必须跳过,不视为一条记录。 - if i == 51: - lines.append("broken-json-line") # 损坏行(进程被杀残留):跳过。 - partial_path.write_text("\n".join(lines) + "\n", encoding="utf-8") - - from nodes.subtitle_ocr import invoke as ocr_invoke - - fake = FakeVlmOcrApi(texts_by_frame, seed=99) - monkeypatch.setattr("wov_app.registry.invoke", fake) - response = ocr_invoke( - InvokeRequest( - run_id="resume_test", - node_instance_id="", - inputs={"frames_manifest": str(FULL_MANIFEST)}, - params={"pool_min_workers": 4, "pool_max_workers": 4}, - output_dir=str(out_dir), - ) - ) - assert response.status == "completed", response.error - srt = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8") - assert srt == confirmed # 存档 + 新增合并结果与全量一致。 - # 只处理未处理帧:前 100 帧不再调用 vlm-ocr。 - - # 场景 2:先用真实逻辑完整跑一遍(生成存档),再以同一目录续跑—— - # 第二次 pending 为空,完全不调用 vlm-ocr,产物与第一次逐字节一致 - # (覆盖 _load_partial 全恢复路径,存档文本与处理逻辑天然一致)。 - out_dir_all = tmp_path / "resume_all" - fake_first = FakeVlmOcrApi(texts_by_frame, seed=7) - monkeypatch.setattr("wov_app.registry.invoke", fake_first) - resp_first = ocr_invoke( - InvokeRequest( - run_id="resume_all_test", - node_instance_id="", - inputs={"frames_manifest": str(FULL_MANIFEST)}, - params={"pool_min_workers": 4, "pool_max_workers": 4}, - output_dir=str(out_dir_all), - ) - ) - assert resp_first.status == "completed", resp_first.error - srt_first = Path(resp_first.outputs["srt_uri"]).read_text(encoding="utf-8") - assert srt_first == confirmed - assert len(fake_first.completed_frames) == TOTAL_FRAMES - - fake_second = FakeVlmOcrApi(texts_by_frame, seed=8) - monkeypatch.setattr("wov_app.registry.invoke", fake_second) - resp_second = ocr_invoke( - InvokeRequest( - run_id="resume_all_test", - node_instance_id="", - inputs={"frames_manifest": str(FULL_MANIFEST)}, - params={"pool_min_workers": 4, "pool_max_workers": 4}, - output_dir=str(out_dir_all), - ) - ) - assert resp_second.status == "completed", resp_second.error - srt_second = Path(resp_second.outputs["srt_uri"]).read_text(encoding="utf-8") - assert srt_second == srt_first # 存档恢复与一次跑完逐字节一致。 - assert len(fake_second.completed_frames) == 0 # 一帧都不重新调用。 - assert len(fake.completed_frames) == TOTAL_FRAMES - 100 diff --git a/tests/test_translate_model_bench.py b/tests/test_translate_model_bench.py deleted file mode 100644 index 2c6eddf..0000000 --- a/tests/test_translate_model_bench.py +++ /dev/null @@ -1,131 +0,0 @@ -"""翻译模型横向评测的数据契约与对照逻辑测试(真实数据,不 mock 模型)。 - -背景 ----- -为评估"更便宜的 LLM 能否替代当前 Qwen/Qwen3.6-35B-A3B 做 llm-translate", -新增了三件评测工具(scripts/ 下,都是真实数据驱动): - -- `scripts/build_translate_eval.py`:把 run_479b411f299d(ocr-subtitle)的 - 烧录字幕中文产物与 run_d386ccf124f7(learn-translate)的日语 whisper ASR - 按时间配对,产出候选池; -- `scripts/build_segment_eval.py`:把配对改成**窗口级**(一条中文基准字幕 + - 其时间窗内覆盖的 1-3 条日语 cue),产出可用窗口; -- `scripts/bench_translate_models.py`:调用**生产翻译实现**(nodes/llm.py) - 对每个模型跑完整片,并在窗口级打印各模型译文对照供人工 review。 - -本测试验证上述工具的**数据契约与对照逻辑**:评测集必须真实可用(时间轴自洽、 -基准非空、可定位到真实 ASR 条目),对照输出必须包含全部模型各自的行。真实 -产物缺失时整组跳过(与 tests/realdata_contract.py 同一约定)。 -""" - -from __future__ import annotations - -import importlib.util -import json -import subprocess -import sys -from pathlib import Path - -import pytest - -# 仓库根(tests/ 的上一级)。 -WORKSPACE = Path(__file__).resolve().parent.parent -EVAL_SET = WORKSPACE / "testdata/translate_eval/eval_set.jsonl" -WINDOWS = WORKSPACE / "testdata/translate_eval/windows.jsonl" -JA_SRT = WORKSPACE / "data/storage/runs/run_d386ccf124f7/steps/asr/transcript.srt" -BENCH = WORKSPACE / "scripts/bench_translate_models.py" -# 评测中间产物目录(gitignored),各模型跑完后才存在。 -EXPERIMENT_ROOT = WORKSPACE / "data/experiments/translate_models" - - -def _load(name: str, path: Path): - """按文件路径加载 scripts 下的模块(scripts 不是包,无法 import)。""" - spec = importlib.util.spec_from_file_location(name, path) - module = importlib.util.module_from_spec(spec) - assert spec is not None and spec.loader is not None - spec.loader.exec_module(module) - return module - - -def _seconds(ts: str) -> float: - """SRT 时间戳 -> 秒。""" - hours, minutes, rest = ts.split(":") - secs, millis = rest.split(",") - return int(hours) * 3600 + int(minutes) * 60 + int(secs) + int(millis) / 1000 - - -def test_eval_set_is_real_and_self_consistent() -> None: - """评测集必须来自真实产物:条数充足、基准非空、时间轴可定位真实日语 cue。 - - 这是"人工 review 的样本必须真实可核对"的硬约束——如果窗口的时间区间里 - 找不到对应日语 cue,说明窗口是用假数据拼的,或时间轴与 ASR 不同源。 - """ - if not EVAL_SET.is_file() or not JA_SRT.is_file(): - pytest.skip("评测集或日语 ASR 产物缺失(先跑 build_segment_eval.py)") - - from nodes.srt import parse_srt - - windows = [json.loads(line) for line in EVAL_SET.read_text(encoding="utf-8").splitlines()] - assert len(windows) >= 100, "评测集窗口过少,不足以支撑人工质量结论" - - ja_cues = parse_srt(JA_SRT.read_text(encoding="utf-8")) - for window in windows: - assert window["ja_lines"], f"窗口 {window.get('id')} 没有日语原文" - assert window["zh_ref"].strip(), f"窗口 {window.get('id')} 基准中文为空" - # 基准中文必须位于窗口时间区间内(真实配对),且窗口跨度合理。 - assert _seconds(window["zh_start"]) < _seconds(window["zh_end"]) - assert _seconds(window["zh_end"]) - _seconds(window["zh_start"]) <= 15.0 - # 每条日语 cue 都能在真实 ASR 里按时间戳找到(同源校验)。 - for line in window["ja_lines"]: - assert any(cue.text == line for cue in ja_cues), f"日语行不在真实 ASR 中: {line!r}" - - -def test_window_builder_matches_production_parser() -> None: - """窗口构建器与生产 SRT 解析器结果一致(不另写一套 SRT 规则)。 - - 用 window.jsonl 的候选池与当场重算的窗口对比:同一输入必须得到同一数量与 - 同一序列,避免"导出一次、之后代码改动悄悄漂移"。 - """ - if not WINDOWS.is_file() or not JA_SRT.is_file(): - pytest.skip("候选池缺失(先跑 build_segment_eval.py --export)") - - module = _load("build_segment_eval", WORKSPACE / "scripts/build_segment_eval.py") - zh_srt = WORKSPACE / "data/storage/runs/run_479b411f299d/steps/filter/filtered.srt" - if not zh_srt.is_file(): - pytest.skip("中文基准产物缺失(ocr-subtitle 任务未产出 filtered.srt)") - - rebuilt = module.build_windows(JA_SRT, zh_srt) - exported = [json.loads(line) for line in WINDOWS.read_text(encoding="utf-8").splitlines()] - assert len(rebuilt) == len(exported) - assert [(w["zh_start"], tuple(w["ja_lines"])) for w in rebuilt] == [ - (w["zh_start"], tuple(w["ja_lines"])) for w in exported - ] - - -def test_bench_compare_prints_every_model_for_shared_window() -> None: - """对照表必须为每个模型打印同一窗口的译文(人工 review 的数据来源)。 - - 用两个真实已跑完的模型产物(基线 + 本地 30B)生成对照表,断言:任意窗口 - 区块里两个模型都出现且行数一致——否则 review 会因缺行而漏判。 - """ - baseline = EXPERIMENT_ROOT / "_baseline/steps/cn.srt" - other = EXPERIMENT_ROOT / "local-qwen3-30b-a3b/steps/cn.srt" - if not EVAL_SET.is_file() or not baseline.is_file() or not other.is_file(): - pytest.skip("评测集或某个模型产物缺失(先跑 bench_translate_models.py run)") - - result = subprocess.run( - [sys.executable, str(BENCH), "compare", "--tags", "_baseline", "local-qwen3-30b-a3b", - "--limit", "5"], - cwd=WORKSPACE, capture_output=True, text=True, check=True, - ) - output = result.stdout - assert "基准中文(OCR)" in output - blocks = [b for b in output.split("\n### ") if b.strip()] - assert blocks, "对照表没有任何窗口区块" - for block in blocks: - assert "- _baseline:" in block - assert "- local-qwen3-30b-a3b:" in block - # 两个模型的行数(以 | 分隔的译文数)必须一致。 - base_line = next(l for l in block.splitlines() if l.startswith("- _baseline:")) - other_line = next(l for l in block.splitlines() if l.startswith("- local-qwen3-30b-a3b:")) - assert base_line.count("|") == other_line.count("|") diff --git a/tests/test_translation_line_alignment.py b/tests/test_translation_line_alignment.py deleted file mode 100644 index 66f10fb..0000000 --- a/tests/test_translation_line_alignment.py +++ /dev/null @@ -1,120 +0,0 @@ -"""R05 回归:合法 SRT 多行/空 cue、稳定 ID 翻译和非法模型输出重试。 - -历史 run_51242078d76e 出现文本贴错时间;仅检查行数或在末尾合并/补空无法 -定位中间缺失。本测试在 HTTP 边界注入 JSON 响应,调用真实翻译实现。 -""" - -import json -from pathlib import Path - -import pytest - -from nodes import llm -from wov_sdk.models import InvokeRequest - - -def _http(monkeypatch, answers): - """按次序返回真实 chat.completions 结构,并记录发出的输入。""" - calls = [] - iterator = iter(answers) - - class Response: - def __init__(self, content): - self.content = content - - def __enter__(self): - return self - - def __exit__(self, *args): - return False - - def read(self): - return json.dumps({"choices": [{"message": {"content": self.content}}], - "usage": {"total_tokens": 10}}).encode() - - def open_request(request, **kwargs): - calls.append(json.loads(request.data)) - answer = next(iterator) - return Response(answer if isinstance(answer, str) else json.dumps(answer)) - - monkeypatch.setattr("urllib.request.urlopen", open_request) - return calls - - -def test_multiline_and_empty_cues_keep_timestamps(monkeypatch, tmp_path): - """多行正文视为一条 cue,空 cue 不发送翻译,时间轴不会进入模型输入。""" - source = tmp_path / "input.srt" - source.write_text("\ufeff7\n00:00:01,000 --> 00:00:02,000\nこんにちは\n元気ですか\n\n" - "8\n00:00:03,000 --> 00:00:04,000\n\n" - "9\n00:00:05,000 --> 00:00:06,000\nはい\n", encoding="utf-8") - calls = _http(monkeypatch, [[{"id": 3, "text": "是的"}, {"id": 1, "text": "你好\n还好吗"}]]) - response = llm.invoke(InvokeRequest(run_id="r", node_instance_id="", inputs={"srt_uri": str(source)}, output_dir=str(tmp_path / "out"))) - assert response.status == "completed", response.error - assert json.loads(calls[0]["messages"][1]["content"]) == [ - {"id": 1, "text": "こんにちは\n元気ですか"}, {"id": 3, "text": "はい"}] - text = Path(response.outputs["cn_srt_uri"]).read_text() - assert text.count("-->") == 3 - assert "00:00:01,000 --> 00:00:02,000\n你好\n还好吗" in text - assert "00:00:03,000 --> 00:00:04,000\n\n" in text - assert "00:00:05,000 --> 00:00:06,000\n是的" in text - - -@pytest.mark.parametrize("bad", [ - [{"id": 1, "text": "一"}], - [{"id": 1, "text": "一"}, {"id": 1, "text": "重复"}], - [{"id": 1, "text": "一"}, {"id": 99, "text": "未知"}], - [{"id": True, "text": "一"}, {"id": 2, "text": "二"}], - [{"id": 1, "text": "一"}, {"id": 2, "text": ""}], - "一\n二", "{truncated", {"1": "一", "2": "二"}, -]) -def test_invalid_ids_retry_without_positional_repair(monkeypatch, bad): - """缺失/重复/未知 ID 和无结构文本均重试整批,不猜测句子对应关系。""" - calls = _http(monkeypatch, [bad, [{"id": 2, "text": "二"}, {"id": 1, "text": "一"}]]) - assert llm.translate_lines(["first", "second"], {}) == ["一", "二"] - assert len(calls) == 2 - assert calls[0]["messages"][1] == calls[1]["messages"][1] - - -def test_exhausted_alignment_retries_fail_node(monkeypatch, tmp_path): - """无法对齐时返回 failed,不生成带空占位或错位文本的成功成品。""" - source = tmp_path / "input.srt" - source.write_text("1\n00:00:01,000 --> 00:00:02,000\nhello\n", encoding="utf-8") - calls = _http(monkeypatch, ["无 ID 输出"] * llm.MAX_BATCH_RETRIES) - response = llm.invoke(InvokeRequest(run_id="r", node_instance_id="", inputs={"srt_uri": str(source)}, output_dir=str(tmp_path / "out"))) - assert response.status == "failed" - assert len(calls) == llm.MAX_BATCH_RETRIES - assert not (tmp_path / "out/cn.srt").exists() - - -def test_batch_ids_are_global_and_order_independent(monkeypatch): - """跨批 ID 保持全局位置,乱序输出也能按正确 cue 回填。""" - answers = [[{"id": i, "text": f"译{i}"} for i in range(20, 0, -1)], - [{"id": 22, "text": "译22"}, {"id": 21, "text": "译21"}]] - calls = _http(monkeypatch, answers) - assert llm.translate_lines([f"原{i}" for i in range(1, 23)], {}) == [f"译{i}" for i in range(1, 23)] - assert json.loads(calls[1]["messages"][1]["content"])[0]["id"] == 21 - - -def test_malformed_srt_fails_without_llm(monkeypatch, tmp_path): - """非空坏字幕不能被静默解析为空并成功输出。""" - source = tmp_path / "input.srt" - source.write_text("1\ninvalid timestamp\nhello\n", encoding="utf-8") - calls = _http(monkeypatch, []) - response = llm.invoke(InvokeRequest(run_id="r", node_instance_id="", inputs={"srt_uri": str(source)}, output_dir=str(tmp_path / "out"))) - assert response.status == "failed" - assert calls == [] - - -@pytest.mark.integration -def test_real_llm_structured_translation(): - """真实接口校准 JSON 协议;无 Key 时跳过,仅使用短日文句子控制调用量。""" - import os - from dotenv import load_dotenv - - load_dotenv() - if not os.getenv("LLM_API_KEY"): - pytest.skip("未配置 LLM_API_KEY") - result = llm.translate_lines(["こんにちは。", "ありがとうございます。"], {}) - assert len(result) == 2 and all(result) - assert any(word in result[0] for word in ("好", "嗨")) - assert "谢" in result[1] diff --git a/tests/test_unify_ass_style.py b/tests/test_unify_ass_style.py deleted file mode 100644 index eecd6f2..0000000 --- a/tests/test_unify_ass_style.py +++ /dev/null @@ -1,231 +0,0 @@ -"""历史 VR 双目字幕统一脚本(scripts/unify_ass_style.py)测试。 - -背景 ----- -`scripts/unify_ass_style.py` 负责把媒体库中由旧版本生成、混有多种历史样式 -的 *.CN_dual_eye.ass 原地改写为 nodes/ass.py 当前统一样式 -(an8 顶部对齐 + 70% 透明 + DEFAULT_MARGIN_TOP=700)。 - -本测试验证三条关键路径: -1. 各历史样式(底部实心白 an2+MarginV1200 / 底部半透明 an2+MarginV1020 / - 顶部 120 / 已是 700)都能正确重建为统一的 700 样式; -2. 重建结果与 nodes/ass.py 的 ass_header()/dialogue_line()(新生成字幕的 - 唯一出口)**逐字节一致**,防止"新字幕"与"历史改写"两套逻辑漂移; -3. 脚本文件能被直接加载执行(仓库根运行 uv run python scripts/...), - 且 rewrite_content 对非 VR 字幕返回 None(不误伤普通 ASS 文件)。 - -测试使用真实 ASS 内容构造(Script Info + 样式 + 事件),不 mock 文件系统 -之外的任何逻辑。 -""" - -from __future__ import annotations - -import importlib.util -import subprocess -import sys -from pathlib import Path - -import pytest - -# 仓库根(tests/ 的上一级),用于导入 scripts/unify_ass_style.py。 -WORKSPACE = Path(__file__).resolve().parent.parent -SCRIPT_PATH = WORKSPACE / "scripts" / "unify_ass_style.py" - -# 用真实可执行的统一脚本构造各历史样式样本:以下文本代表旧版实际输出格式。 -HEADER_3840_OLD = """[Script Info] -Title: VR Dual-Eye Subtitle -ScriptType: v4.00+ -Collisions: Normal -PlayResX: 3840 -PlayResY: 1920 -WrapStyle: 1 -ScaledBorderAndShadow: yes - -[V4+ Styles] -Format: Name,Fontname,Fontsize,PrimaryColour,SecondaryColour,OutlineColour,BackColour,Bold,Italic,Underline,StrikeOut,ScaleX,ScaleY,Spacing,Angle,BorderStyle,Outline,Shadow,Alignment,MarginL,MarginR,MarginV,Encoding -""" - - -def _load_module(): - """按文件路径加载 scripts/unify_ass_style.py(scripts 不是包,无法 import)。 - - 与仓库运行方式一致:uv run python scripts/unify_ass_style.py 时 pythonpath - 含仓库根,模块内 `from nodes.ass import ...` 可正常解析。""" - spec = importlib.util.spec_from_file_location("unify_ass_style", SCRIPT_PATH) - module = importlib.util.module_from_spec(spec) - assert spec is not None and spec.loader is not None - spec.loader.exec_module(module) - return module - - -U = _load_module() - - -@pytest.fixture -def sandbox(tmp_path): - """构造含三种历史样式 + 一个非 VR 文件的临时媒体库。 - - 每个测试独立临时目录(function scope),CLI 写盘测试不会污染其他测试。""" - root = tmp_path - # 1) 最早期:底部 an2 + 实心白 &H00FFFFFF + MarginV=1200。 - (root / "old_solid.CN_dual_eye.ass").write_text( - HEADER_3840_OLD - + "Style: LeftEye,Arial,50,&H00FFFFFF,&H000000FF,&H00000000,&H80000000,0,0,0,0,50,100,0,0,1,4,0,2,50,1970,1200,1\n" - + "Style: RightEye,Arial,50,&H00FFFFFF,&H000000FF,&H00000000,&H80000000,0,0,0,0,50,100,0,0,1,4,0,2,1970,50,1200,1\n" - + "\n[Events]\n" - + "Format: Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text\n" - + 'Dialogue: 0,00:00:00.000,00:00:03.000,LeftEye,,0,0,0,,{\\an2}第一行\\N第二行\n' - + 'Dialogue: 0,00:00:00.000,00:00:03.000,RightEye,,0,0,0,,{\\an2}第一行\\N第二行\n', - encoding="utf-8", - ) - # 2) 过渡期:底部 an2 + 半透明 &H80FFFFFF + MarginV=1020。 - (root / "mid_translucent.CN_dual_eye.ass").write_text( - HEADER_3840_OLD - + "Style: LeftEye,Arial,50,&H80FFFFFF,&H000000FF,&H00000000,&H80000000,0,0,0,0,50,100,0,0,1,4,0,2,50,1970,1020,1\n" - + "Style: RightEye,Arial,50,&H80FFFFFF,&H000000FF,&H00000000,&H80000000,0,0,0,0,50,100,0,0,1,4,0,2,1970,50,1020,1\n" - + "\n[Events]\n" - + "Format: Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text\n" - + 'Dialogue: 0,00:00:00.000,00:00:03.000,LeftEye,,0,0,0,,{\\an2}你好\n', - encoding="utf-8", - ) - # 3) 已是顶部 120 样式(旧代码默认),仍应规整为 700。 - (root / "top120.CN_dual_eye.ass").write_text( - HEADER_3840_OLD - + "Style: LeftEye,Arial,50,&HB3FFFFFF,&H000000FF,&H80000000,&H80000000,0,0,0,0,50,100,0,0,1,4,0,8,50,1920,120,1\n" - + "Style: RightEye,Arial,50,&HB3FFFFFF,&H000000FF,&H80000000,&H80000000,0,0,0,0,50,100,0,0,1,4,0,8,1920,50,120,1\n" - + "\n[Events]\n" - + "Format: Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text\n" - + 'Dialogue: 0,00:00:00.000,00:00:03.000,LeftEye,,0,0,0,,{\\an8}第三行\n', - encoding="utf-8", - ) - # 4) 非 VR 字幕(无 LeftEye/RightEye 样式),脚本不应改动。 - (root / "not_vr.CN_dual_eye.ass").write_text( - "[Script Info]\nPlayResX: 1920\nPlayResY: 1080\n\n[V4+ Styles]\n" - "Format: Name,Fontname,Fontsize,PrimaryColour,...\nStyle: Default,Arial,20,&H00FFFFFF,...\n", - encoding="utf-8", - ) - return root - - -def _read(path: Path) -> str: - """以 UTF-8 读取测试文件内容。""" - return path.read_text(encoding="utf-8") - - -def test_rewrite_converges_all_generations(sandbox) -> None: - """三种历史样式统一后都含 an8 顶部对齐 + 70% 透明 + MarginV=700。""" - # (文件名, 该文件期望保留的事件文本) - expected_text = { - "old_solid": r"第一行\N第二行", # 最早期多行字幕(an2 底部) - "mid_translucent": "你好", # 过渡期 - "top120": "第三行", # 旧顶部 120 - } - for name, text in expected_text.items(): - original = _read(sandbox / f"{name}.CN_dual_eye.ass") - rewritten = U.rewrite_content(original) - assert rewritten is not None, f"{name} 应被改写" - # 左/右眼样式都落到统一值:70%透明 + an8 + MarginV=700。 - assert "&HB3FFFFFF,&H000000FF,&H80000000,&H80000000,0,0,0,0,50,100,0,0,1,4,0,8,50,1920,700,1" in rewritten - assert "&HB3FFFFFF,&H000000FF,&H80000000,&H80000000,0,0,0,0,50,100,0,0,1,4,0,8,1920,50,700,1" in rewritten - # 对齐标签全部为 an8,不再残留 an2。 - assert r"{\an8}" in rewritten - assert r"{\an2}" not in rewritten - # 事件文本原样保留。 - assert text in rewritten - - -def test_rewrite_byte_identical_to_write_ass(sandbox) -> None: - """历史字幕统一后的输出与 write_ass() 生成的新字幕逐字节一致。 - - 防止"统一脚本"与"节点生成"两套样式逻辑漂移:两边都经由 ass_header() - /dialogue_line()(nodes/ass.py 单一出口)。""" - from nodes.ass import DEFAULT_MARGIN_TOP, ass_header, dialogue_line - - # 取 old_solid 样本的事件,用 write_ass 语义重建期望文本。 - original = _read(sandbox / "old_solid.CN_dual_eye.ass") - rewritten = U.rewrite_content(original) - assert rewritten is not None - hdr = ass_header(3840, 1920, margin_top=DEFAULT_MARGIN_TOP).rstrip("\n") - expected = "\n".join( - [ - hdr, - dialogue_line("LeftEye", "00:00:00.000", "00:00:03.000", "第一行\\N第二行"), - dialogue_line("RightEye", "00:00:00.000", "00:00:03.000", "第一行\\N第二行"), - ] - ) + "\n" - assert rewritten == expected - - -def test_rewrite_skips_non_vr_and_already_canonical(sandbox) -> None: - """非 VR 字幕返回 None;已是目标样式(MarginV=700)也返回 None(幂等)。""" - not_vr = _read(sandbox / "not_vr.CN_dual_eye.ass") - assert U.rewrite_content(not_vr) is None - - # 构造一份已是 700 的新样式文本,改写应返回 None(内容不变)。 - from nodes.ass import ass_header, dialogue_line - - canonical = ( - ass_header(3840, 1920, margin_top=U.DEFAULT_MARGIN_TOP).rstrip("\n") - + "\n" - + dialogue_line("LeftEye", "00:00:00.000", "00:00:03.000", "嗨") - + "\n" - ) - assert U.rewrite_content(canonical) is None - - -def test_module_runs_as_cli_dry_run(sandbox) -> None: - """脚本可作为 CLI 以 dry-run 方式运行(uv run python scripts/...),不写盘。 - - 复现仓库根运行方式;断言 dry-run 输出含"旧样式分布"与"将改写", - 且运行后沙盒内文件一个都没被改动(dry-run 语义)。""" - before = {p.name: _read(p) for p in sandbox.glob("*.ass")} - # 不带 --apply = dry-run:应只打印不写盘。 - proc = subprocess.run( - [sys.executable, str(SCRIPT_PATH), str(sandbox)], - capture_output=True, - text=True, - cwd=WORKSPACE, # 与 uv run python 一致:仓库根在 sys.path - ) - assert proc.returncode == 0, proc.stderr - out = proc.stdout - assert "[将改写]" in out # 预览行标记 - assert "[改写]" not in out # 未真正写盘 - after = {p.name: _read(p) for p in sandbox.glob("*.ass")} - assert before == after # dry-run 不改动任何文件 - - - -def test_module_cli_apply_writes_files(sandbox) -> None: - """脚本加 --apply 后真正原地改写,写盘结果与 rewrite_content 一致。 - - 覆盖 main() 的写盘分支:统计输出显示 [改写],且文件内容已变为 - MarginV=700 的统一新样式(第二次运行幂等,不再改写)。""" - # 备份一个将改写的样本,确保 CLI 走的是与单元层相同的重建逻辑。 - old_solid = sandbox / "old_solid.CN_dual_eye.ass" - expected = U.rewrite_content(old_solid.read_text(encoding="utf-8")) - assert expected is not None - - proc = subprocess.run( - [sys.executable, str(SCRIPT_PATH), str(sandbox), "--apply"], - capture_output=True, - text=True, - cwd=WORKSPACE, - ) - assert proc.returncode == 0, proc.stderr - assert "[改写]" in proc.stdout - assert old_solid.read_text(encoding="utf-8") == expected - - # 幂等:第二次 --apply 不再有任何 [改写](文件已是目标样式)。 - proc2 = subprocess.run( - [sys.executable, str(SCRIPT_PATH), str(sandbox), "--apply"], - capture_output=True, - text=True, - cwd=WORKSPACE, - ) - assert proc2.returncode == 0, proc2.stderr - assert "[改写]" not in proc2.stdout - -def test_module_loads_and_exports() -> None: - """模块可加载且暴露 rewrite_content / DEFAULT_MARGIN_TOP。""" - assert U.DEFAULT_MARGIN_TOP == 700 - assert callable(U.rewrite_content) diff --git a/tests/test_uvicorn_smoke.py b/tests/test_uvicorn_smoke.py deleted file mode 100644 index 82202b4..0000000 --- a/tests/test_uvicorn_smoke.py +++ /dev/null @@ -1,34 +0,0 @@ -"""uvicorn 冒烟测试。 - -用真实套接字启动 uvicorn 服务并请求 /health,验证应用能脱离 TestClient -在实际 Web 服务环境中正常工作。 -""" - -import threading -import time -import urllib.request - -from uvicorn import Config, Server - -from wov_app.main import app - - -def test_uvicorn_serves_app_over_real_socket() -> None: - """验证 uvicorn 监听真实端口后健康检查可用。""" - config = Config(app=app, host="127.0.0.1", port=0, log_level="error") - server = Server(config) - thread = threading.Thread(target=server.run, daemon=True) - thread.start() - try: - deadline = time.monotonic() + 10 - while not server.started and time.monotonic() < deadline: - time.sleep(0.05) - assert server.started - - port = server.servers[0].sockets[0].getsockname()[1] - with urllib.request.urlopen(f"http://127.0.0.1:{port}/health", timeout=5) as response: - assert response.status == 200 - assert b'"wov-api"' in response.read() - finally: - server.should_exit = True - thread.join(timeout=10) diff --git a/tests/test_vad_profiler.py b/tests/test_vad_profiler.py deleted file mode 100644 index c91244c..0000000 --- a/tests/test_vad_profiler.py +++ /dev/null @@ -1,252 +0,0 @@ -"""每视频自适应 VAD 调参测试(先红后绿)。 - -验证信号分析 profile_audio、启发式参数建议 suggest_vad_parameters、 -转录质量评分 score_transcript(幻觉词扣分)与 vad_parameters_for_audio -(信号分析 + 片段网格验证)。 - -评分**不依赖参考字幕**(实际部署无参考),用转录质量 + 幻觉词扣分。 -""" - -from __future__ import annotations - -import wave -from pathlib import Path - -import pytest - -from nodes.vad_profiler import ( - AudioProfile, - HALLUCINATION_TOKENS, - profile_audio, - score_transcript, - suggest_vad_parameters, - vad_parameters_for_audio, - _pick_representative_start, -) - - -class _Seg: - """模拟 whisper segment:仅含 text。""" - - def __init__(self, text): - self.text = text - - -def _make_wav(path: Path, silence_seconds: int, voice_seconds: int) -> None: - """生成 [静音 N 秒 + 语音 N 秒] 的 16kHz 单声道 WAV。 - - silence 用 0 样本(静音),voice 用较大振幅样本(语音)。 - """ - import array - - rate = 16000 - silence = array.array("h", [0] * rate * silence_seconds) - voice = array.array("h", [8000] * rate * voice_seconds) - samples = silence + voice - with wave.open(str(path), "wb") as f: - f.setnchannels(1) - f.setsampwidth(2) - f.setframerate(rate) - f.writeframes(samples.tobytes()) - - -def test_profile_audio_detects_bgm_heavy() -> None: - """纯静音+语音的视频:silence_ratio 高、非 BGM 覆盖;mediam_rms 合理。""" - wav = Path("/tmp/test_vad_plain.wav") - _make_wav(wav, silence_seconds=40, voice_seconds=10) - p = profile_audio(wav, 16000) - assert p.silence_ratio > 0.5 - assert p.bgm_heavy is False - assert p.duration_seconds == pytest.approx(50.0, abs=1) - - -def test_suggest_vad_parameters_plain_silence() -> None: - """静音占比高且无长停顿 -> 建议 threshold 偏高(静音权重分支)。""" - wav = Path("/tmp/test_vad_plain2.wav") - # 交替短静音避免触发 long_silence(<5s 连续静音)。 - _make_wav(wav, silence_seconds=3, voice_seconds=1) - _make_wav(wav, silence_seconds=3, voice_seconds=1) - p = profile_audio(wav, 16000) - assert p.long_silence is False - params = suggest_vad_parameters(p) - assert params["threshold"] >= 0.5 - - -def test_suggest_vad_parameters_bgm() -> None: - """BGM 覆盖(低能量占比高但静音少)-> threshold 降低、min_silence 减小。""" - p = AudioProfile( - silence_ratio=0.1, # 几乎无静音 - lowish_ratio=0.6, # 大量低能量(音乐) - bgm_heavy=True, # 直接标记 BGM 覆盖 - long_silence=False, - rms_bins=[500] * 100, - ) - params = suggest_vad_parameters(p) - assert params["threshold"] < 0.5 # 降低 - assert params["min_silence_duration_ms"] < 1000 # 减小 - - -def test_score_transcript_penalizes_hallucination() -> None: - """幻觉词(感谢观看/晚安/音乐)多 -> 评分低。""" - good = [_Seg("ありがとうございます本日は"), _Seg("かしこまりました")] - bad = [_Seg("ご視聴ありがとうございました"), _Seg("おやすみなさい"), _Seg("音楽")] - assert score_transcript(good) > score_transcript(bad) - - -def test_score_transcript_penalizes_fragments() -> None: - """碎片(纯单字/语气词)多 -> 评分低。""" - clean = [_Seg("今日はとても暑いですね"), _Seg("それでは始めましょう")] - frag = [_Seg("あ"), _Seg("うん"), _Seg("はい"), _Seg("あっ")] - assert score_transcript(clean) > score_transcript(frag) - - -def test_vad_parameters_for_audio_uses_grid_when_provider() -> None: - """提供 whisper 回调时:从候选网格选评分最高的参数(threshold 最小者)。""" - wav = Path("/tmp/test_vad_grid.wav") - _make_wav(wav, silence_seconds=10, voice_seconds=5) - - def fake_whisper(**kwargs): - # 假设最优 = threshold 最小的候选(suggest 0.5 时候选含 0.4)。 - t = kwargs.get("vad_parameters", {}).get("threshold", 0.5) - if t <= 0.4: - # 最优组合:无幻觉的正常长句(分数最高)。 - return [_Seg("今日はお客様のために精神整備を務めさせていただきます")] - # 其它组合:幻觉套话(分数低)。 - return [_Seg("ご視聴ありがとうございました"), _Seg("おやすみなさい")] - - params = vad_parameters_for_audio( - wav, sample_rate=16000, whisper_invoke=fake_whisper, run_dir=Path("/tmp") - ) - # 网格应从候选里选出评分最高的 threshold=0.4 的组合。 - assert params["threshold"] == pytest.approx(0.4, abs=0.1) - - -def test_vad_parameters_for_audio_fallback_without_provider() -> None: - """无 whisper 回调:退化为信号分析建议,不报错。""" - wav = Path("/tmp/test_vad_noprov.wav") - _make_wav(wav, silence_seconds=10, voice_seconds=5) - params = vad_parameters_for_audio(wav, sample_rate=16000, whisper_invoke=None) - assert "threshold" in params - assert "min_silence_duration_ms" in params - - -def test_hallucination_tokens_included() -> None: - """幻觉词集合应含常用套话(感谢观看/晚安/音乐)。""" - all_str = " ".join(HALLUCINATION_TOKENS) - assert "ご視聴ありがとうございました" in all_str - assert "おやすみなさい" in all_str - assert "音楽" in all_str - -def test_suggest_vad_parameters_long_silence() -> None: - """长停顿常见 -> 建议 threshold 0.5、speech_pad 200(防时间轴压缩漂移)。""" - p = AudioProfile( - silence_ratio=0.2, lowish_ratio=0.3, bgm_heavy=False, long_silence=True, - rms_bins=[500] * 100, - ) - params = suggest_vad_parameters(p) - assert params["threshold"] == 0.5 - assert params["speech_pad_ms"] == 200 - - -def test_suggest_vad_parameters_regular() -> None: - """常规音频(无特殊标记)-> 默认 0.5/1000/400。""" - p = AudioProfile( - silence_ratio=0.2, lowish_ratio=0.3, bgm_heavy=False, long_silence=False, - rms_bins=[1500] * 100, - ) - params = suggest_vad_parameters(p) - assert params["threshold"] == 0.5 - assert params["min_silence_duration_ms"] == 1000 - assert params["speech_pad_ms"] == 400 - - -def test_pick_representative_start_all_silence() -> None: - """全静音音频:窗口恐怖沉默用 score>0.55 惩罚,仍返回起始 0。""" - p = AudioProfile( - rms_bins=[50] * 100, silence_ratio=0.9, lowish_ratio=0.9, - bgm_heavy=False, long_silence=False, - ) - start = _pick_representative_start(p, 90) - assert start >= 0 and start < 10 - - -def test_grid_search_exception_skipped() -> None: - """网格搜索某组参数调用抛异常:跳过该组,不崩溃。""" - called = {"n": 0} - - def failing_whisper(**kwargs): - called["n"] += 1 - raise RuntimeError("mock download fail") - - from nodes.vad_profiler import _grid_search_vad - - params = _grid_search_vad( - failing_whisper, Path("/tmp/x.wav"), Path("/tmp"), 60, 0, - {"threshold": 0.5, "min_silence_duration_ms": 1000, "speech_pad_ms": 400}, - ) - # 全部失败时回退 suggested。 - assert params == {"threshold": 0.5, "min_silence_duration_ms": 1000, "speech_pad_ms": 400} - assert called["n"] > 0 - - -def test_candidate_params_expands_grid() -> None: - """候选网格应含建议值邻域(threshold±0.1、silence 倍、pad 组合)。""" - from nodes.vad_profiler import _candidate_params - - cands = _candidate_params( - {"threshold": 0.5, "min_silence_duration_ms": 1000, "speech_pad_ms": 400} - ) - assert len(cands) == 27 - assert {"threshold": 0.4, "min_silence_duration_ms": 500, "speech_pad_ms": 0} in cands - assert {"threshold": 0.6, "min_silence_duration_ms": 2000, "speech_pad_ms": 400} in cands - - -def test_median_rms_empty_returns_zero() -> None: - """rms_bins 为空时 median_rms 返回 0。""" - p = AudioProfile(rms_bins=[]) - assert p.median_rms == 0.0 - - -def test_profile_audio_empty_wav() -> None: - """空的 WAV(无样本)-> 返回 duration 0 的空 profile,不崩溃。""" - import array - wav = Path("/tmp/test_vad_empty.wav") - with wave.open(str(wav), "wb") as f: - f.setnchannels(1); f.setsampwidth(2); f.setframerate(16000) - f.writeframes(array.array("h", []).tobytes()) - p = profile_audio(wav, 16000) - assert p.duration_seconds == 0.0 - - -def test_grid_search_ignores_non_list() -> None: - """网格搜索回调返回非 list(如 None)应被跳过,回退建议值。""" - from nodes.vad_profiler import _grid_search_vad - - params = _grid_search_vad( - lambda **kw: None, Path("/tmp/x.wav"), Path("/tmp"), 60, 0, - {"threshold": 0.5, "min_silence_duration_ms": 1000, "speech_pad_ms": 400}, - ) - assert params == {"threshold": 0.5, "min_silence_duration_ms": 1000, "speech_pad_ms": 400} - - -def test_score_transcript_empty_list() -> None: - """空列表评分返回 0。""" - assert score_transcript([]) == 0.0 - - -def test_score_transcript_ignores_empty_text_seg() -> None: - """含空文本的 segment 被跳过,不报错。""" - assert score_transcript([_Seg(""), _Seg("今日は暑いです")]) > 0 - - -def test_profile_audio_different_sample_rate() -> None: - """采样率与请求不一致时用 wave 实际 rate 近似,不崩溃。""" - import array - wav = Path("/tmp/test_vad_rate.wav") - rate = 8000 - samples = array.array("h", [5000] * rate * 2) # 2s 语音 - with wave.open(str(wav), "wb") as f: - f.setnchannels(1); f.setsampwidth(2); f.setframerate(rate) - f.writeframes(samples.tobytes()) - p = profile_audio(wav, 16000) - assert p.duration_seconds == pytest.approx(2.0, abs=0.2) diff --git a/tests/test_workflow_api.py b/tests/test_workflow_api.py deleted file mode 100644 index c4892a9..0000000 --- a/tests/test_workflow_api.py +++ /dev/null @@ -1,202 +0,0 @@ -"""工作流管理 API 测试。 - -覆盖工作流的创建、查询、校验、发布、版本列表与删除等管理接口。 -""" - -from fastapi.testclient import TestClient - -from wov_app.main import app - - -def definition() -> dict: - """构造一个引用 Echo 节点的合法工作流定义。""" - return { - "name": "echo-flow", - "version": 1, - "nodes": [ - { - "id": "step", - "node_type": "echo", - "inputs": {"file_uri": "input.video_uri"}, - } - ], - "edges": [], - "entry_inputs": {"video_uri": "file"}, - "final_outputs": {"result": "step.file_uri"}, - } - - -def cyclic_definition() -> dict: - """构造带环的 DAG 定义(a → b → a),用于验证保存/发布拒绝无效工作流。 - - 环上的两个节点互相引用对方输出,形成了无法拓扑排序的依赖:节点 ID 唯一、 - 边引用的节点都存在,因此只有环检测能拦住它(R04 复现定义)。 - """ - return { - "name": "cyclic", - "version": 1, - "nodes": [ - {"id": "a", "node_type": "echo", "inputs": {"file_uri": "b.file_uri"}}, - {"id": "b", "node_type": "echo", "inputs": {"file_uri": "a.file_uri"}}, - ], - "edges": [{"from": "a", "to": "b"}, {"from": "b", "to": "a"}], - "entry_inputs": {"video_uri": "file"}, - "final_outputs": {"result": "a.file_uri"}, - } - - -def test_create_workflow_rejects_cycle() -> None: - """验证保存环形 DAG 返回 422,不把无效版本写入版本表。""" - with TestClient(app) as client: - response = client.post( - "/api/admin/workflows", - json={ - "id": "cyclic-flow", - "name": "Cyclic Flow", - "definition": cyclic_definition(), - }, - ) - assert response.status_code == 422 - assert "cycle" in response.json()["detail"] - # 拒绝保存时不得留下半成品工作流/版本记录。 - db = app.state.db - assert db.get_workflow("cyclic-flow") is None - assert db.list_workflow_versions("cyclic-flow") == [] - - # 已有工作流重新提交带环定义:同样拒绝,不追加新版本。 - assert client.post( - "/api/admin/workflows", - json={"id": "cycle-check", "name": "Cycle Check", "definition": definition()}, - ).status_code == 200 - assert client.post( - "/api/admin/workflows", - json={"id": "cycle-check", "name": "Cycle Check", "definition": cyclic_definition()}, - ).status_code == 422 - assert db.get_workflow("cycle-check")["latest_version"] == 1 - assert len(db.list_workflow_versions("cycle-check")) == 1 - db.delete_workflow("cycle-check") - - -def test_validate_workflow_rejects_cycle() -> None: - """验证只校验不保存的接口同样拒绝环形 DAG。""" - with TestClient(app) as client: - db = app.state.db - # 独立工作流 ID 并显式清理,避免与其他用例(共用同一个测试数据库)互相影响。 - assert client.post( - "/api/admin/workflows", - json={"id": "cycle-check", "name": "Cycle Check", "definition": definition()}, - ).status_code == 200 - response = client.post( - "/api/admin/workflows/cycle-check/validate", - json=cyclic_definition(), - ) - assert response.status_code == 422 - assert "cycle" in response.json()["detail"] - db.delete_workflow("cycle-check") - - -def test_publish_rejects_cycle_in_latest_version() -> None: - """验证历史遗留的环形版本不能被发布(发布前重新校验最新版本定义)。""" - with TestClient(app) as client: - db = app.state.db - # 直接写库模拟修复前已保存的无效版本(保存接口现在会拒绝,只能这样构造)。 - db.upsert_workflow( - {"id": "legacy-cyclic", "name": "Legacy", "published": 0, "latest_version": 1} - ) - db.create_workflow_version("legacy-cyclic", 1, cyclic_definition()) - response = client.post("/api/admin/workflows/legacy-cyclic/publish") - assert response.status_code == 422 - assert "cycle" in response.json()["detail"] - assert db.get_workflow("legacy-cyclic")["published"] == 0 - db.delete_workflow("legacy-cyclic") - - -def test_workflow_crud_and_publish() -> None: - """验证工作流 CRUD、校验、发布与版本列表的完整流程。""" - with TestClient(app) as client: - created = client.post( - "/api/admin/workflows", - json={ - "id": "echo-flow", - "name": "Echo Flow", - "description": "demo", - "definition": definition(), - }, - ) - assert created.status_code == 200 - assert created.json()["id"] == "echo-flow" - - assert client.get("/api/admin/workflows").status_code == 200 - assert client.get("/api/admin/workflows/echo-flow").status_code == 200 - assert client.get("/api/admin/workflows/missing").status_code == 404 - - validated = client.post( - "/api/admin/workflows/echo-flow/validate", - json=definition(), - ) - assert validated.status_code == 200 - assert validated.json()["valid"] is True - assert client.post( - "/api/admin/workflows/missing/validate", - json=definition(), - ).status_code == 404 - - published = client.post("/api/admin/workflows/echo-flow/publish") - assert published.status_code == 200 - assert published.json()["published"] == "echo-flow" - assert client.post("/api/admin/workflows/missing/publish").status_code == 404 - - versions = client.get("/api/admin/workflows/echo-flow/versions") - assert versions.status_code == 200 - assert len(versions.json()) == 1 - assert client.get("/api/admin/workflows/missing/versions").status_code == 404 - - assert client.delete("/api/admin/workflows/echo-flow").status_code == 200 - assert client.delete("/api/admin/workflows/echo-flow").status_code == 404 - - -def test_workflow_slug_without_id() -> None: - """验证未提供 ID 时后端会从名称生成 slug。""" - with TestClient(app) as client: - created = client.post( - "/api/admin/workflows", - json={ - "name": "Echo Flow", - "definition": definition(), - }, - ) - assert created.status_code == 200 - assert created.json()["id"] == "echo-flow" - - -def test_workflow_validation_error() -> None: - """验证重复节点 ID 的 DAG 会被拒绝。""" - with TestClient(app) as client: - response = client.post( - "/api/admin/workflows", - json={ - "id": "bad", - "name": "Bad", - "definition": { - "name": "Bad", - "version": 1, - "nodes": [ - {"id": "a", "node_type": "x"}, - {"id": "a", "node_type": "y"}, - ], - "edges": [], - }, - }, - ) - assert response.status_code == 422 - - -def test_publish_workflow_without_version() -> None: - """验证没有版本记录的工作流不能发布。""" - with TestClient(app) as client: - db = app.state.db - db.upsert_workflow( - {"id": "empty", "name": "Empty", "published": 0, "latest_version": 0} - ) - response = client.post("/api/admin/workflows/empty/publish") - assert response.status_code == 422 diff --git a/tests/web/__init__.py b/tests/web/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/web/test_crop/__init__.py b/tests/web/test_crop/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/web/test_crop/test_crop_js.py b/tests/web/test_crop/test_crop_js.py new file mode 100644 index 0000000..0b022d7 --- /dev/null +++ b/tests/web/test_crop/test_crop_js.py @@ -0,0 +1,180 @@ +"""web/assets/crop.js 的模块级测试(数据 → 测试过程 → 验证结果)。 + +被测模块:`web/assets/crop.js`(前端框选矩形 ↔ crop 比例的纯函数,含 +object-fit: contain 的 letterbox 处理),被 OCR 框选面板复用。 + +实现方式:在 Node.js 子进程中加载真实 JS 模块并调用真实函数(不重写逻辑), +验证换算结果;环境无 node 时跳过(保持跨平台可运行)。 +""" + +from __future__ import annotations + +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + +# 仓库根与被测脚本。 +WORKSPACE = Path(__file__).resolve().parents[3] +CROP_JS = WORKSPACE / "web" / "assets" / "crop.js" + +# 用真实 JS 引擎执行调用的封装:把用例数据交给 crop.js 的真实实现。 +_CALL_SCRIPT = """ +const crop = require(process.argv[1]); +const payload = JSON.parse(process.argv[2]); +const fn = crop[payload.fn]; +const result = fn(...payload.args); +process.stdout.write(JSON.stringify(result)); +""" + + +def _run(fn: str, *args) -> object: + """在 node 中调用 crop.js 的真实函数并返回解析后的结果。""" + node = shutil.which("node") + if node is None: + pytest.skip("环境没有 node,跳过 JS 模块测试") + completed = subprocess.run( + [node, "-e", _CALL_SCRIPT, str(CROP_JS), json.dumps({"fn": fn, "args": list(args)})], + capture_output=True, + text=True, + ) + assert completed.returncode == 0, completed.stderr + return json.loads(completed.stdout) + + +# --------------------------------------------------------------------------- +# videoDisplayRect:contain 显示区域 +# --------------------------------------------------------------------------- + + +def test_display_rect_matches_box_when_aspect_equal() -> None: + """视频与容器宽高比一致时铺满容器,无留边。""" + # 数据:1920x1080 视频放进 960x540 容器。 + # 测试过程 + rect = _run("videoDisplayRect", 1920, 1080, 960, 540) + + # 验证结果 + assert rect == {"x": 0, "y": 0, "w": 960, "h": 540} + + +def test_display_rect_letterboxes_on_wide_container() -> None: + """容器比视频更宽时左右留边(横屏视频放进方形容器)。""" + # 数据:1920x1080 视频放进 1000x1000 容器。 + # 测试过程 + rect = _run("videoDisplayRect", 1920, 1080, 1000, 1000) + + # 验证结果:宽 1000、高 562.5,上下居中留边。 + assert rect["w"] == pytest.approx(1000) + assert rect["h"] == pytest.approx(562.5) + assert rect["x"] == pytest.approx(0) + assert rect["y"] == pytest.approx((1000 - 562.5) / 2) + + +def test_display_rect_letterboxes_on_tall_container() -> None: + """容器比视频更高时上下留边。""" + # 数据:1000x1000 视频放进 500x1000 容器。 + # 测试过程 + rect = _run("videoDisplayRect", 1000, 1000, 500, 1000) + + # 验证结果:宽高均为 500,水平居中。 + assert rect["w"] == pytest.approx(500) + assert rect["h"] == pytest.approx(500) + assert rect["x"] == pytest.approx(0) + assert rect["y"] == pytest.approx(250) + + +# --------------------------------------------------------------------------- +# rectToCrop:框选 → 比例 +# --------------------------------------------------------------------------- + + +def test_rect_to_crop_full_frame() -> None: + """框选整个显示区域得到 [0,0,1,1]。""" + # 数据:显示区为整个容器。 + # 测试过程 + crop = _run("rectToCrop", {"x": 0, "y": 0, "w": 960, "h": 540}, 1920, 1080, 960, 540) + + # 验证结果 + assert crop == [0, 0, 1, 1] + + +def test_rect_to_crop_bottom_quarter() -> None: + """框选底部 1/4 得到默认字幕区比例 [0,0.75,1,0.25]。""" + # 数据:底部四分之一的显示坐标。 + # 测试过程 + crop = _run("rectToCrop", {"x": 0, "y": 405, "w": 960, "h": 135}, 1920, 1080, 960, 540) + + # 验证结果 + assert crop == [0, 0.75, 1, 0.25] + + +def test_rect_to_crop_accounts_for_letterbox_offset() -> None: + """存在留边时,框选坐标先减去显示区偏移(否则比例整体偏移)。""" + # 数据:1920x1080 放 1000x1000 容器(上下各留 218.75),框选显示区上半。 + rect = {"x": 0, "y": 218.75, "w": 1000, "h": 281.25} + + # 测试过程 + crop = _run("rectToCrop", rect, 1920, 1080, 1000, 1000) + + # 验证结果:y 从 0 开始、高度占一半。 + assert crop[0] == pytest.approx(0) + assert crop[1] == pytest.approx(0) + assert crop[2] == pytest.approx(1) + assert crop[3] == pytest.approx(0.5) + + +def test_rect_to_crop_clamps_out_of_range_to_0_1() -> None: + """越界框选被钳制到 0~1(不产生非法 crop 参数)。""" + # 数据:超出显示区的矩形(含负坐标)。 + # 测试过程 + crop = _run("rectToCrop", {"x": -500, "y": -500, "w": 5000, "h": 5000}, 1920, 1080, 960, 540) + + # 验证结果:全部落在 [0,1]。 + assert all(0 <= value <= 1 for value in crop) + assert crop[0] == 0 and crop[1] == 0 + assert crop[2] == 1 and crop[3] == 1 + + +def test_rect_to_crop_rounds_to_three_decimals() -> None: + """比例保留 3 位小数(与帧清单时间戳精度约定一致)。""" + # 数据:会产生长小数的框选。 + # 测试过程 + crop = _run("rectToCrop", {"x": 1, "y": 2, "w": 333, "h": 77}, 1920, 1080, 960, 540) + + # 验证结果:每个值最多 3 位小数。 + for value in crop: + assert round(value, 3) == value + + +# --------------------------------------------------------------------------- +# cropToRect:比例 → 框选(回显) +# --------------------------------------------------------------------------- + + +def test_crop_to_rect_is_inverse_of_rect_to_crop() -> None: + """比例转回矩形与原始框选一致(回显正确,误差在 3 位小数内)。""" + # 数据:一个中间框选。 + rect = {"x": 120, "y": 300, "w": 480, "h": 135} + + # 测试过程 + crop = _run("rectToCrop", rect, 1920, 1080, 960, 540) + restored = _run("cropToRect", crop, 1920, 1080, 960, 540) + + # 验证结果:各分量误差极小。 + for key in ("x", "y", "w", "h"): + assert restored[key] == pytest.approx(rect[key], abs=1.0) + + +def test_crop_to_rect_bottom_quarter() -> None: + """默认 crop 比例回显为底部四分之一矩形。""" + # 数据:默认 crop。 + # 测试过程 + rect = _run("cropToRect", [0, 0.75, 1, 0.25], 1920, 1080, 960, 540) + + # 验证结果 + assert rect["x"] == pytest.approx(0) + assert rect["y"] == pytest.approx(405) + assert rect["w"] == pytest.approx(960) + assert rect["h"] == pytest.approx(135)