diff --git a/README.md b/README.md index 52b5f9c..bfb577b 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ # OneCue · 文本到短视频工作流 -OneCue 是一套面向 Apple Silicon Mac 的本地短视频 MVP:Codex 负责理解内容、文案与分镜,Image 2 生成逐镜图片,包 B 生成逐镜人声,包 A/FFmpeg 校验 Schema v1 Job Bundle 并合成竖屏视频。当前已能从一段自然语言需求推进到候选成片,并保留校验、缓存、执行报告和镜头级返修能力。 +OneCue 是一套面向 Apple Silicon Mac 的本地短视频 MVP:Codex 负责理解内容、文案与分镜,Image 2 生成逐镜静态图片,包 B 生成逐镜人声,包 A/FFmpeg 校验 Schema v1 Job Bundle 并合成竖屏视频。当前已能从一段自然语言需求推进到候选成片,并保留校验、缓存、执行报告和镜头级返修能力。 -当前不使用 BGM;音频只来自包 B 的解说或角色对话。基础动态仍是 FFmpeg 虚拟摄影机推拉、平移与轻漂移,并非物体级自然动画;雨落、流水、人物动作等尚未实现。这是当前最明显的质量边界,不在此处夸大。 +当前第一版采用“多张静态分镜图随叙事硬切”的路线:不使用 BGM,不对图片做推拉、平移、漂移、拆层微动态或 I2V;音频只来自包 B 的解说或角色对话。每张图片仍会由包 A 编码为与该镜头人声等长的视频片段,再与字幕和其他镜头合成为成片。动态研究与兼容代码暂留,但不进入默认工作流。 ## 系统构成 -- `【包A】视频引擎包`:Job Bundle 契约、TTS 编排、ASS 字幕、基础运镜、转场、缓存、报告与最终合成。 +- `【包A】视频引擎包`:Job Bundle 契约、TTS 编排、静态镜头编码、ASS 字幕、硬切、缓存、报告与最终合成。 - `【包B】语音引擎包`:基于 dots.tts 的本地 TTS 服务,默认监听 `127.0.0.1:7860`。 - `skills/short-video-director`:可选但强烈推荐的 Codex 薄导演层,识别策划、新建、续接、检查、渲染和返修模式;它不实现另一套视频能力。 -- `【包A】视频引擎包/docs/short_video_v2`:V2 的权威工作流、Schema 说明、图片约定、核心管线、动态边界与验收文档。 +- `【包A】视频引擎包/docs/short_video_v2`:V2 的权威工作流、Schema 说明、图片约定、核心管线与验收文档;动态文档仅作历史研究记录。 ## 仓库不包含的内容 @@ -41,7 +41,7 @@ python3.12 scripts/setup_macos_source.py --model mf 该脚本会在包 B 内创建 `runtime/python`,安装锁定依赖,以 editable 方式安装包 B,下载 `rednote-hilab/dots.tts-mf`,并按仓库中的 manifest 校验大小与 SHA-256。校验不通过时不应启动服务。 -可选质量版模型速度更慢、占用更大: +可选质量版模型速度更慢、占用更大,当前第一版并不需要;仅在另行比较音质时下载: ```bash python3.12 scripts/download_macos_models.py --model soar @@ -118,11 +118,15 @@ Job Bundle 必须是自包含的 Schema v1 目录。请先阅读 `【包A】视 ```bash "【包B】语音引擎包/runtime/python/bin/python3.12" -m unittest \ - "【包A】视频引擎包/tests/test_director_workflow_docs.py" -v -"【包B】语音引擎包/runtime/python/bin/python3.12" -m unittest discover \ - -s "【包A】视频引擎包/tests" -p 'test_v2_*.py' -v + "【包A】视频引擎包/tests/test_director_workflow_docs.py" \ + "【包A】视频引擎包/tests/test_v2_job_bundle_contract.py" \ + "【包A】视频引擎包/tests/test_v2_core_runtime.py" \ + "【包A】视频引擎包/tests/test_v2_core_pipeline.py" \ + "【包A】视频引擎包/tests/test_v2_mvp_acceptance.py" -v ``` +计划 01、04、07 等旧阶段实验测试仍保留在 `【包A】视频引擎包/tests`,但不属于第一版日常门。需要审计历史兼容性时,才使用 `test_v2_*.py` 全量发现。 + 包 B 的运行策略与 API 契约测试: ```bash diff --git a/skills/short-video-director/SKILL.md b/skills/short-video-director/SKILL.md index c8720e2..4f3684d 100644 --- a/skills/short-video-director/SKILL.md +++ b/skills/short-video-director/SKILL.md @@ -1,45 +1,54 @@ --- name: short-video-director -description: Direct OneCue's short-video V2 workflow from natural-language content or an existing Schema v1 Job Bundle. Use when Codex is working in a OneCue checkout and needs to plan, create, resume, inspect, render, or revise a short video, including Brief and storyboard design, Image 2 keyframe planning, Job Bundle validation, package A/B rendering, evidence review, user review, and shot-scoped repair. +description: Direct OneCue short-video work from natural-language content, an existing Schema v1 Job Bundle, or user-supplied footage. Use when Codex needs to plan, create, resume, inspect, render, or revise either the established static-storyboard route or a Codex-owned footage edit built with general-video, media-use, and HyperFrames. --- # Short Video Director -Act only as a thin director and navigation layer over the repository's established workflow. The core Codex → Image 2 → package B → package A/FFmpeg chain does not depend on this skill. Do not implement image, TTS, video, FFmpeg, cache, schema, or state-machine capabilities here. +Act only as a thin director and navigation layer. Route established static-storyboard work to the repository workflow, and route selected user-supplied footage to Codex's media workflow. Do not implement image, TTS, video, FFmpeg, cache, schema, or state-machine capabilities here. ## Route the request Choose exactly one primary mode, then record it: -- **Plan**: Produce the Brief, script, and shot draft. Do not generate images, synthesize speech, or render. -- **Create**: Advance from content to a candidate video through the existing project workflow. Stop for user review after the candidate is ready. -- **Resume**: Read the task record and existing artifacts first. Continue from the first unmet hard gate. -- **Inspect**: Perform read-only checks of a Job Bundle, report, cache, final, services, or current state. -- **Render**: Validate an existing Job Bundle before calling the existing `video_v2 render` CLI. -- **Revise**: Identify the shot and affected layer. Make the smallest change and scope rebuilding with `--shot` when applicable. +- **Plan**: Produce the brief, script, source decisions, and shot draft. Do not generate media, synthesize speech, edit, or render. +- **Create**: Advance from content and selected sources to a candidate through the chosen material route. Stop for user review after the candidate is ready. +- **Resume**: Read the chosen route's task record or composition and existing artifacts first. Continue from the first unmet hard gate. +- **Inspect**: Perform read-only checks of supplied media or the chosen route's bundle, composition, reports, cache, final, services, and current state. +- **Render**: In Static V1, validate the Job Bundle before `video_v2 render`; in Codex footage, check the HyperFrames composition before its established render loop. +- **Revise**: Identify the timestamp, source or shot, and affected layer. Make the smallest change; use `--shot` only in Static V1 when applicable. Infer safe defaults for low-impact omissions. Ask only when missing information changes facts, brand/content direction, external cost or authorization, or the candidate result materially. +## Choose the material route + +After choosing the primary mode, choose one orthogonal material route: + +- **Static V1 route**: Use when the result is built from independent Image 2 stills. Keep the established Codex → Image 2 → package B → package A/FFmpeg contract unchanged. +- **Codex footage route**: Use when the user supplies video for assessment or possible use. Mixed video-and-image work also uses this route. Codex owns the footage analysis, edit plan, media operations, composition, audio treatment, rendering, and review through general-video, media-use, and HyperFrames; package A and package B do not process this route. +- If supplied footage is irrelevant, redundant, unreadable, or weaker than another truthful visual, record why it was omitted. Supplying footage is permission to assess it, not an obligation to force it into the edit. After an `omit` decision, switch to the Static V1 route only if the remaining requested result actually fits that contract. + ## Read only what the mode needs -- Read [references/project-map.md](references/project-map.md) to locate the checkout, authoritative documents, CLI, services, records, and protected baselines. -- Read [references/workflow-and-gates.md](references/workflow-and-gates.md) for Plan, Create, Resume, Render, mode transitions, automatic continuation, and pause conditions. -- Read [references/quality-and-revision.md](references/quality-and-revision.md) for Inspect, review, failure diagnosis, cache interpretation, media evidence, and minimal revision. -- Read the repository's `director_workflow_v1.md` before executing any plan, create, render, or revision path. Follow its linked contracts instead of copying them into this skill. +- For the Static V1 route, read [references/project-map.md](references/project-map.md), [references/workflow-and-gates.md](references/workflow-and-gates.md), and [references/quality-and-revision.md](references/quality-and-revision.md). Read the repository's `director_workflow_v1.md` before executing any plan, create, render, or revision path. +- For the Codex footage route, read [references/video-material-workflow.md](references/video-material-workflow.md). Then load the installed hyperframes, general-video, and media-use skills as that reference directs. Do not load the static Job Bundle contracts unless the request also contains a separate Static V1 task. ## Preserve the project contract +- The following Schema, CLI, cache, and shot rules apply only to the Static V1 route. - Locate the repository from the current workspace or the path supplied by the user. Do not assume one person's absolute checkout path. - Treat natural language as an upstream Codex input. Feed package A only a valid, self-contained Schema v1 Job Bundle. +- For every newly created V1 shot, set `motion.preset` to `static`, `motion.strength` to `low`, and `transition_out` to `cut` with `duration_sec: 0`. Keep `visual.focus` for crop placement. Do not choose motion or crossfade presets merely because Schema v1 still accepts them. - Use the existing `python3 -m video_v2 validate|render` CLI. Do not import the pipeline to create another orchestrator. - Preserve the user's dirty worktree, existing Job Bundles, reports, cache, and final files. Never reset, checkout, clean, or overwrite unrelated work. - Use explicit argv, `shell=False`, and finite timeouts for external commands. Never turn user text into shell syntax. - Do not add a wrapper script by default. Consider one only if two independent real cases prove the existing CLI cannot express the same deterministic step, with tests and a minimal design first. Never add a second Schema, cache, timeline, caption system, media checker, Web UI, or provider routing layer. +- The Codex footage route is deliberately outside Schema v1. Never place supplied video in a V1 Job Bundle, claim package A or package B supports it, or create a parallel OneCue schema or runner. Keep originals intact and store only task-local plans, derivatives, compositions, renders, and review evidence. ## Continue and stop safely Continue through authorized, local, reversible work: read-only checks, task-local records, content design, validation, targeted tests, report reading, and bounded contract repair. -Pause for material ambiguity; new third-party/paid/cloud APIs, credentials, downloads, payment, or publishing; destructive deletion or cache cleanup; a need to change Schema/core pipeline/tool scope; and final user review of a candidate video. When the user has explicitly requested creation, the current session's built-in Image 2 capability is the established keyframe route, not a new external API expansion. +Pause for material ambiguity; new third-party/paid/cloud APIs, credentials, downloads, payment, or publishing; destructive deletion or cache cleanup; a need to change Schema/core pipeline/tool scope; and final user review of a candidate video. In the Static V1 route, also pause for any request to expand the active route into image animation. When the user has explicitly requested creation, the current session's built-in Image 2 capability is the established keyframe route, not a new external API expansion. -Never describe FFmpeg push, pull, pan, tilt, or drift as natural semantic motion. Rain, water flow, vehicle travel, breathing, body motion, and similar object-level movement remain unimplemented; the formal advanced-provider count is zero. +The active V1 result is a sequence of distinct static storyboard images, not one image for the whole narration and not animated stills. In that static route, do not invoke FFmpeg push/pull/pan/tilt/drift, crossfades, HyperFrames, DepthFlow, I2V, BGM, environmental audio, or SFX. The Codex footage route is the separate scope in which HyperFrames may be used, under its own review gates. diff --git a/skills/short-video-director/VERSION b/skills/short-video-director/VERSION new file mode 100644 index 0000000..9084fa2 --- /dev/null +++ b/skills/short-video-director/VERSION @@ -0,0 +1 @@ +1.1.0 diff --git a/skills/short-video-director/agents/openai.yaml b/skills/short-video-director/agents/openai.yaml index fb0523f..4b56fa1 100644 --- a/skills/short-video-director/agents/openai.yaml +++ b/skills/short-video-director/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "短视频导演" - short_description: "以自然语言策划、制作、续接、检查、渲染与返修 OneCue 短视频" - default_prompt: "Use $short-video-director to plan or continue a short-video V2 task from natural-language input." + short_description: "以自然语言统筹静态分镜或用户视频素材的短视频制作" + default_prompt: "Use $short-video-director to plan, create, inspect, or revise a OneCue short video from static storyboard assets or user-supplied footage." diff --git a/skills/short-video-director/references/project-map.md b/skills/short-video-director/references/project-map.md index 3ee29e6..02c02a6 100644 --- a/skills/short-video-director/references/project-map.md +++ b/skills/short-video-director/references/project-map.md @@ -17,7 +17,6 @@ Read these from `/docs/short_video_v2/` as needed: - `job_bundle_v1.md`: machine input contract - `image_workflow_v1.md`: Image 2 keyframe method - `core_pipeline_v1.md`: rendering, cache, failure, and CLI behavior -- `motion_feasibility_v1.md`: honest motion boundary - `mvp_acceptance_v1.md`: accepted MVP baseline - `workflow_acceptance_v1.md`: accepted director workflow and skill boundary - `templates/brief_v1.md`: optional content-design aid @@ -25,6 +24,8 @@ Read these from `/docs/short_video_v2/` as needed: Do not substitute older root-level vision documents for these frozen v1 facts. +`motion_feasibility_v1.md`, `motion_experiment_playbook_v1.md`, plans 04/07, and their experiments are historical motion research. They are not active-route documents and should be opened only when the user explicitly asks to revisit image animation. + ## Evidence and records Task-local Job Bundles, reports, cache, final videos, and acceptance evidence normally live under ignored `成片/` directories. Treat any existing formal acceptance bundle and final as protected. Validate it read-only and use an isolated copy for failure injection or revision rehearsal. diff --git a/skills/short-video-director/references/quality-and-revision.md b/skills/short-video-director/references/quality-and-revision.md index 0ff8f85..531ad05 100644 --- a/skills/short-video-director/references/quality-and-revision.md +++ b/skills/short-video-director/references/quality-and-revision.md @@ -1,5 +1,7 @@ # Quality and revision +This reference governs only the **Static V1 route** and its Job Bundle artifacts. Review or revise a Codex-owned footage edit with [video-material-workflow.md](video-material-workflow.md) instead. + ## Inspect without mutation For an existing Job Bundle, read the task record if it already exists and run `validate --json`. Inspect the render report, cache manifest, final hash, ffprobe facts, and complete decode as requested. Do not create or update a task record, render, modify the formal bundle, or regenerate media merely to answer whether it can continue; report facts in the reply. @@ -16,7 +18,7 @@ Use the repository review template to separate: - contract and service facts; - codec, size, frame rate, pixel format, audio, duration, and complete decode; -- per-shot content, image, voice, caption, and basic-motion observations; +- per-shot content, static image, voice, caption, and hard-cut observations; - cache hits/rebuilds and protected hashes; - user feedback and the first unmet gate. @@ -28,14 +30,14 @@ Map the request to a `shot_id`, time point, and affected layer: - text or caption: change only the necessary shot and adjacent continuity; - voice: rebuild only affected audio/shot dependencies; -- focus or motion: change only legal fields for that shot; +- focus or crop: change only `visual.focus` for that shot and keep motion `static/low`; - keyframe composition: edit or regenerate only that keyframe, then update its SHA-256; -- transition or cross-shot pacing: allow final recomposition while reusing valid audio/shot caches. +- hard-cut or cross-shot pacing: keep `cut/0`; allow final recomposition while reusing valid audio/shot caches. Snapshot the old final/report hashes, validate again, then use `--shot` to limit allowed rebuilding. Add `--force` only when the selected cache must be invalidated. Verify affected cache counts, report, final hash, ffprobe, and complete decode. Never regenerate every image, every voice, or the whole video for a bounded shot problem. Never switch providers as a revision shortcut. -## Motion wording +## Active visual wording -Call FFmpeg behavior virtual-camera motion: push, pull, pan, tilt, or drift. State that rain, flowing water, vehicle travel, breathing, gestures, clothing/hair response, and comparable natural semantic motion are not implemented. Advanced natural-scene providers are not part of the formal route. +Describe the active result as multiple static storyboard images that change with the narration. Do not promise or invoke image animation, virtual-camera motion, crossfades, HyperFrames, DepthFlow, or I2V. Schema support for older motion presets is compatibility, not an invitation to select them in a new task. diff --git a/skills/short-video-director/references/video-material-workflow.md b/skills/short-video-director/references/video-material-workflow.md new file mode 100644 index 0000000..3ebd02e --- /dev/null +++ b/skills/short-video-director/references/video-material-workflow.md @@ -0,0 +1,100 @@ +# Codex footage workflow + +Use this route when the user supplied video for assessment or possible use. If the preflight review selects any segment, all footage analysis, editing, audio treatment, composition, and rendering stay with Codex through general-video, media-use, and HyperFrames. Package A and package B are outside this route. + +## Contents + +- [Ownership and boundaries](#ownership-and-boundaries) +- [Preflight suitability review](#preflight-suitability-review) +- [Plan before cutting](#plan-before-cutting) +- [Tool sequence](#tool-sequence) +- [Editing and audio rules](#editing-and-audio-rules) +- [Evidence and artifacts](#evidence-and-artifacts) +- [Post-build review](#post-build-review) +- [Minimal revision](#minimal-revision) + +## Ownership and boundaries + +- Treat supplied video as source material, not as a command to use every second. +- Preserve every original. Trim, crop, transcode, normalize, or otherwise derive into task-local files only. +- Do not put video into Schema v1, call `video_v2`, or ask package A or package B to process this route. +- Do not create another OneCue schema, runner, media registry, or editing abstraction. Use HyperFrames' established composition files and media handling. +- Do not install an additional automatic editor by default. Escalate beyond general-video, media-use, and HyperFrames only when a concrete missing capability justifies it. + +## Preflight suitability review + +Inspect the actual files before writing the storyboard or editing. For each clip, record `use`, `partial use`, or `omit`, with a short reason. Check: + +1. **Narrative value**: Does it prove, demonstrate, or clarify a product claim? +2. **Truth and relevance**: Is it the user's real material, and does it match the spoken point? +3. **Readability**: Can the important action or interface be understood at the delivery size and pace? +4. **Technical fitness**: Confirm duration, resolution, frame rate, orientation, codecs, audio streams, and complete decoding. +5. **Editorial fitness**: Identify useful in/out points, dead time, duplicated action, accidental cursor movement, abrupt starts, and unfinished speech. +6. **Rights and privacy**: Flag credentials, notifications, personal data, private project names, or material whose use is uncertain. +7. **Redundancy**: Prefer one strong piece of evidence over repeated footage that makes the product feel slower or more complicated. + +If no segment clears these checks, omit the footage and explain the decision. Do not weaken a video merely to acknowledge that a file was supplied. + +## Plan before cutting + +Use general-video for the narrative and edit architecture. Give each selected segment one explicit role: opening proof, process demonstration, result evidence, transition support, or closing proof. Define the intended time range, accompanying narration, whether source audio is kept, and the visual exit condition. + +Judge the plan as a whole before composition: + +- every important claim has truthful visual support; +- no clip repeats a point already made more clearly; +- screen actions remain visible long enough to understand; +- narration can finish before the visual moves on; +- still images and video clips form one coherent rhythm rather than two disconnected montages. + +## Tool sequence + +1. **HyperFrames entry**: Load the hyperframes skill first because it owns the composition contract and required development loop. +2. **general-video**: Establish format, duration, visual system, scene order, and the role of each supplied clip. Keep the plan source-driven. +3. **media-use**: Probe source files and create only the evidence needed to decide: metadata, representative frames or contact sheets, and transcription when speech matters. Use media-use for task-local derivatives only when the composition cannot express the operation safely. +4. **HyperFrames composition**: Register selected media, place it on the timeline, add captions and overlays, mix audio, preview representative frames, lint/check, and render. +5. **Review loop**: Inspect the rendered candidate as a viewer, not merely as valid code. Repair only material defects, then rerun the smallest relevant checks. + +For a three-to-five-second screen recording or other short evidence clip, direct visual inspection is usually better than automated scene detection. Use heavier analysis only when clip length or ambiguity warrants it. + +## Editing and audio rules + +- Prefer non-destructive timeline trims. Create a derived file only for operations such as permanent crop/reframe, speed change, stabilization, audio replacement, or compatibility transcoding. +- Cut on completed ideas and visible actions. Never leave a spoken sentence unfinished merely to meet a planned timestamp. +- Prefer clean cuts. Use a dissolve, wipe, or other transition only when it clarifies a change in time, place, or mode; decorative transitions should not call more attention to themselves than the product. +- For screen recordings, remove waiting and setup time but preserve a short reaction hold after the key action. Zoom or crop only when it materially improves legibility. +- Use speed changes sparingly and within a natural-looking range. Do not accelerate cursor work or speech until it becomes hard to follow. +- Reorder only when chronology is not itself evidence. Preserve causal order for demonstrations. +- Choose one source-audio strategy per segment: keep, mute, duck under narration, replace, or use source audio alone. Record the choice in the storyboard. +- Keep dialogue and narration perceptually even across cuts. Avoid per-sentence fade-outs, sudden tail attenuation, clipping, pumping, or a new loudness level at each sentence. +- Leave intentional pauses between complete thoughts. Let the visual hold when the listener needs a moment; do not fill every gap with a cut. +- Background music and effects are optional. They must not mask speech or make a restrained product introduction feel theatrical. + +## Evidence and artifacts + +Use the HyperFrames project as the working record. Keep the brief/storyboard, source paths, selected time ranges, audio decisions, derived-file provenance, and render checks together in the task directory. Record hashes for irreplaceable or externally supplied source files when practical. + +Do not duplicate this information into a new OneCue Job Bundle. The purpose of the record is reproducibility and bounded revision, not a second product contract. + +## Post-build review + +Before presenting the candidate, review the complete render and representative frames. Confirm: + +- narration, captions, and visuals make the same claim at the same moment; +- no page or title disappears before its sentence completes; +- interface text and focal actions are readable at delivery size; +- no accidental black frame, stale freeze, duplicate clip, awkward crop, or irrelevant source appears; +- cuts feel intentional, with enough breathing room between sentences; +- voice loudness remains stable, especially at sentence endings and joins; +- source audio, narration, and optional music do not compete; +- captions remain within safe areas and match the audible wording; +- duration and pacing serve the message rather than an arbitrary target; +- the file passes metadata checks and complete decode. + +If a problem is visible but low-impact, weigh it against the cost and regression risk of repair. Fix defects that harm comprehension, credibility, continuity, audio comfort, or delivery validity; do not churn the whole composition for cosmetic trivia. + +## Minimal revision + +Map feedback to a timestamp, source segment, and layer: source trim, timeline timing, visual framing, narration, caption, or mix. Preserve the previous candidate, change the smallest responsible layer, and render only what the HyperFrames workflow requires. + +After revision, recheck the edited boundary plus the complete output for new timing conflicts. A local fix is complete only when it no longer causes the next caption, sentence, cut, or audio transition to arrive too early or too late. diff --git a/skills/short-video-director/references/workflow-and-gates.md b/skills/short-video-director/references/workflow-and-gates.md index cde8441..ef76f9f 100644 --- a/skills/short-video-director/references/workflow-and-gates.md +++ b/skills/short-video-director/references/workflow-and-gates.md @@ -1,14 +1,16 @@ # Workflow and gates +This reference governs only the **Static V1 route**. If supplied video is selected for the result, stop here and use [video-material-workflow.md](video-material-workflow.md). Do not send that footage through package A or package B. + ## Plan -Apply safe defaults when omitted: zh-CN vertical video, 30–45 seconds, 6–10 shots, voice only, independent Image 2 keyframes, and basic FFmpeg camera motion. Record assumptions and stop before media generation. +Apply safe defaults when omitted: zh-CN vertical video, 30–45 seconds, 6–10 shots, voice only, independent Image 2 stills, static shots, and hard cuts. Record assumptions and stop before media generation. ## Create 1. Convert the request into a concise Brief, fact boundary, script, and shot plan. 2. Plan or generate independent keyframes through Image 2; preserve references, prompts, ledger entries, and hashes. -3. Assemble only Schema v1 `project.json`, `storyboard.json`, and self-contained assets. +3. Assemble only Schema v1 `project.json`, `storyboard.json`, and self-contained assets. Set every new shot to `motion=static/low` and `transition_out=cut/0` while retaining `visual.focus` for crop placement. 4. Run `validate --json`; repair contract input before any TTS or FFmpeg work. 5. Check package A/B readiness and run the existing renderer. 6. Read the report, cache manifest, media evidence, and review template. @@ -38,7 +40,8 @@ Pause for: - new third-party/paid/cloud APIs, credentials, downloads, publishing, or new authorization; the established built-in Image 2 route is allowed when creation was explicitly requested; - destructive deletion, cache cleanup, or edits to unrelated/old formal artifacts; - a request that requires Schema, core pipeline, or provider-scope changes; +- any request to add image animation, virtual-camera motion, crossfades, HyperFrames, DepthFlow, or I2V to the active V1 route; - the candidate-video user review gate; -- user rejection of the basic motion boundary. +- user rejection of the static-storyboard product direction. Record the blocking gate and leave evidence intact. diff --git "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/core_pipeline_v1.md" "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/core_pipeline_v1.md" index d5d4650..46d7c89 100644 --- "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/core_pipeline_v1.md" +++ "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/core_pipeline_v1.md" @@ -1,6 +1,8 @@ # 短视频 V2 核心管线 v1 -本文描述包 A 的本地渲染核心。它只读取已通过 Schema v1 验证的 Job Bundle,逐镜生成人声、字幕与基础运镜,再以 FFmpeg 合成 `output/final.mp4`。本版不提供 Web API,不接入 BGM、环境音、SFX 或高级图片动态化。 +本文描述包 A 的本地渲染核心。它只读取已通过 Schema v1 验证的 Job Bundle,逐镜生成人声、字幕与静态画面片段,再以 FFmpeg 硬切合成 `output/final.mp4`。本版不提供 Web API,不接入 BGM、环境音、SFX 或图片动态化。 + +当前产品配置要求所有新任务使用 `motion.preset=static`、`motion.strength=low` 与 `transition_out=cut/0`。核心仍解析旧运动预设和短叠化,以保证既有 Job Bundle 可重渲;这些兼容分支不属于第一版默认工作流,也不应由导演 Skill 主动选择。 ## Python 入口 @@ -73,4 +75,3 @@ cd "【包A】视频引擎包/程序文件/引擎" ``` render 退出码:成功 `0`;Job Bundle 契约错误 `2`;可预期运行失败 `3`;取消 `130`;未预期内部错误 `1`。`--json` 模式的 stdout 只有最终一个 JSON envelope,进度写 stderr。可预期错误使用稳定代码,详细证据可查 `output/render_report.json`。 - diff --git "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/director_workflow_v1.md" "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/director_workflow_v1.md" index dd34f88..9957837 100644 --- "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/director_workflow_v1.md" +++ "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/director_workflow_v1.md" @@ -1,14 +1,24 @@ # 短视频 V2 导演工作流 v1 -本文件是本项目从自然语言到候选成片、审查与局部返修的权威操作入口。机器输入合同仍以 [Job Bundle Schema v1](job_bundle_v1.md) 和 `video_v2` 内两份 Schema 为准;图片、渲染、动态边界与 MVP 事实分别见 [Image 2 工作流](image_workflow_v1.md)、[核心管线](core_pipeline_v1.md)、[动态可行性](motion_feasibility_v1.md) 与 [MVP 验收](mvp_acceptance_v1.md)。本文件不复制其实现,也不建立第二套 Schema、runner 或状态机。 +本文件是本项目从自然语言到候选成片、审查与局部返修的权威操作入口。机器输入合同仍以 [Job Bundle Schema v1](job_bundle_v1.md) 和 `video_v2` 内两份 Schema 为准;图片、渲染与 MVP 事实分别见 [Image 2 工作流](image_workflow_v1.md)、[核心管线](core_pipeline_v1.md) 与 [MVP 验收](mvp_acceptance_v1.md)。动态可行性及实验手册是历史研究证据,不是第一版执行入口。本文件不复制实现,也不建立第二套 Schema、runner 或状态机。 `short-video-director` 是强烈推荐的轻量导演/导航层,而非主链的技术依赖。即使 Skill 暂时未安装或未被某次会话发现,Codex 内容与分镜、Image 2 图片、包 B 逐镜人声、包 A/FFmpeg 校验与合成的既有主链仍成立;此时可直接按本文件和模板执行,并分别报告核心工作流与 Skill 的状态。 +## 0. 第一版活跃范围 + +| 状态 | 内容 | 执行规则 | +| --- | --- | --- | +| 第一版启用 | 多张静态分镜图、逐镜人声、ASS 字幕、硬切、校验、缓存、报告、镜头级返修 | 每个新镜头固定写 `motion.preset: static`、`motion.strength: low`、`transition_out.type: cut`、`transition_out.duration_sec: 0` | +| 暂不启用 | FFmpeg 推拉/平移/漂移、短叠化、SOAR 质量模型、HyperFrames、DepthFlow、本地 I2V、BGM/SFX、Web UI | 不进入新任务、不主动安装或调用;只有用户另行开启专项评估时才讨论 | +| 历史归档 | 计划 04/07、动态可行性文档、实验代码与样片证据 | 保留事实与复盘价值,但不得据此改变第一版默认路线 | + +Schema v1 与包 A 继续兼容既有运动预设和 `crossfade`,此乃兼容边界,并非产品默认。第一版不要求一张图承载整段视频,而是让各镜头分别使用一张静态图,并按文案推进自然切换。 + ## 1. 最低输入与默认值 用户只需提供以下任一种:故事/主题/观点/资料、一段待改编文本、图片与表达目标,或已有 Job Bundle 加上“检查、续接、渲染、返修”的意图。Brief 模板是 Codex 的可裁剪辅助,不要求用户逐项填写。 -未指定且不会改变事实或方向时,采用并记录默认值:中文竖屏、30–45 秒、6–10 镜、仅人声、Image 2 独立关键帧、包 A 的基础 FFmpeg 虚拟摄影机运动。受众、时长、风格或音色仅在缺失会显著改变内容、成本、授权或成片方向时询问。 +未指定且不会改变事实或方向时,采用并记录默认值:中文竖屏、30–45 秒、6–10 镜、仅人声、Image 2 独立静态关键帧、包 A 静态镜头、镜间硬切。受众、时长、风格或音色仅在缺失会显著改变内容、成本、授权或成片方向时询问。 可选信息包括受众、发布语境、必须保留/禁止内容、事实来源、角色或品牌参考、已验证包 B 音色、逐字台词、自定义字幕、时长/镜数/节奏,以及希望停在策划、任务包、候选成片或具体返修层。 @@ -27,12 +37,12 @@ ## 3. 职责边界 -- 用户:给内容目标与关键选择,审看候选成片,指出镜头/时间点问题,决定是否接受当前动态边界。 +- 用户:给内容目标与关键选择,审看候选成片,指出镜头/时间点问题,决定是否接受当前静态叙事效果。 - Codex / `short-video-director`:理解自然语言,形成 Brief、文案、分镜和图片任务;组装/校验 Job Bundle;调用现有 CLI;读取报告;保护工作树和既有产物。 - `short-video-director` 不实现图片、TTS、视频、FFmpeg 或缓存能力,只识别六种模式并导航本项目已有合同、命令、报告、执行记录、暂停点、用户门与最小返修路径。 - Image 2:生成角色/风格参考、独立关键帧与有限小步编辑;不做字幕、时间线或最终视频,也不嵌入包 A。 - 包 B:按镜头输出人声 WAV;不负责内容理解、图片、字幕或视频合成。 -- 包 A / FFmpeg:只消费合法且自包含的 Schema v1 Job Bundle,负责 TTS 编排、基础运镜、ASS 字幕、转场、缓存、报告和 final。 +- 包 A / FFmpeg:只消费合法且自包含的 Schema v1 Job Bundle,负责 TTS 编排、静态镜头编码、ASS 字幕、硬切、缓存、报告和 final。 自然语言只存在于 Codex 上游。包 A 的正式输入始终是 Schema v1 Job Bundle;不得向 JSON 添加命令、URL、绝对资源路径、Provider 私参、BGM、SFX 或任意新字段。 @@ -41,10 +51,10 @@ 1. 理解:记录目标、受众、事实/授权边界、默认值和真正缺口。 2. 内容:按 [Brief 模板](templates/brief_v1.md) 形成成片文案和镜头草案;策划模式于此停止。 3. 图片:按现有 Image 2 工作流建立参考、逐镜提示、独立关键帧、生成账和 SHA-256;图片不烧录字幕/水印。 -4. Job Bundle:仅按 Schema v1 写 `project.json`、`storyboard.json` 与自包含资产;机器合同以 `job_bundle_v1.md` 为唯一解释入口。 +4. Job Bundle:仅按 Schema v1 写 `project.json`、`storyboard.json` 与自包含资产;第一版逐镜固定为 `static + low + cut(0)`;机器合同以 `job_bundle_v1.md` 为唯一解释入口。 5. validate:任何渲染之前必须通过只读校验;契约错误先最小修正输入,禁止绕过。 6. render:先核包 A/B 状态,再调用现有 `video_v2 render`;失败先读 `output/render_report.json`、`cache/manifest.json` 与事件。 -7. 审查:用 [Review 模板](templates/review_v1.md) 记录技术规格、逐镜问题、动态边界与缓存事实。 +7. 审查:用 [Review 模板](templates/review_v1.md) 记录技术规格、逐镜静态画面/切换问题与缓存事实,并确认没有非预期推拉、平移、漂移或叠化。 8. 用户终审:候选成片完成后暂停。自动评分、ffprobe、完整解码或 Codex 质检均不能替代用户观看与接受。 9. 返修:只改被证据定位的影响层;重新 validate,再做镜头级重渲或必要的全片合成,不无界重做图片/语音/终版。 @@ -88,7 +98,7 @@ env "PYTHONPATH=$PWD/程序文件/引擎" \ - 需要新增第三方/付费/云 API、凭据、下载、外部发布或新的授权;用户已明确要求制作时,使用当前会话内置 Image 2 能力不属于新增 API 扩张; - 需要破坏性删除、清缓存或改动非本任务旧产物; - 候选成片等待用户终审; -- 用户认为基础动态整体不可接受; +- 用户要求把当前第一版扩张为图片动态化或接入新的动态工具; - 继续需要修改 Schema、核心 pipeline 或扩大正式工具路线。 失败后先保存现场,再从报告、缓存和事件判定影响层。同一失败不得通过换目录或反复长跑掩盖。恢复时从首个未满足硬门续接;已成功的图片、WAV、镜头、final 与测试证据不无故重做。 @@ -99,9 +109,9 @@ env "PYTHONPATH=$PWD/程序文件/引擎" \ - 字幕断行/字数:优先调整该镜头 speech/custom caption;会改变人声时只重建对应 audio/shot。 - 文案事实/语气:只改涉及镜头及相邻衔接,重新计算相关声明哈希并 validate。 -- 焦点/基础运镜:只改该镜头合法 `focus`/motion 字段,使用 `--shot` 限定范围;输入变更会自然使该镜缓存失效。 +- 画面裁切/主体焦点:只改该镜头合法 `visual.focus`,保持 `motion.preset=static`,使用 `--shot` 限定范围;输入变更会自然使该镜缓存失效。 - 画面主体/构图:优先一次小步编辑或重生单镜关键帧,登记新图与 SHA-256,再用 `--shot` 限定范围。 -- 跨镜节奏/转场:影响 final 时间线时允许重新合成 final,但复用未失效的 audio/shot 缓存。 +- 跨镜节奏/切换:第一版保持硬切;影响 final 时间线时允许重新合成 final,但复用未失效的 audio/shot 缓存。 执行前快照旧 final/report 哈希;执行后核缓存统计、受影响镜头、final/report、ffprobe 与完整解码。不得因一镜问题重做整套图片、全部 TTS 或更换 Provider。 @@ -111,5 +121,5 @@ env "PYTHONPATH=$PWD/程序文件/引擎" \ - 只对隔离副本运行破坏性故障注入、真实演练或返修验证;正式包默认只读检查与 validate。 - 不自动下载、删除、发布、注入凭据、调用付费服务或停止包 B。 - 不新增工作流包装脚本,除非至少两个独立真实案例证明现有 CLI 无法表达同一机械步骤,且先有测试和最小设计。 -- 当前基础动态只是 FFmpeg 虚拟摄影机推拉、平移与轻漂移;自然语义动态尚未实现。雨落、流水、车辆行驶、人物呼吸/摆动等不能描述成已具备能力。正式高级 Provider 数量仍为 0。 -- V2 Web UI、高级自然语义动态、认证、发布与多人权限均属后续独立决策;本工作流不顺手实施。 +- 第一版不调用 FFmpeg 推拉/平移/漂移、HyperFrames、DepthFlow 或 I2V,也不以伪动态冒充主体运动;逐镜保持静态,仅通过多镜头构图与硬切推进叙事。 +- Schema 与核心管线中的旧运动预设仅保留兼容性。V2 Web UI、图片动态化、认证、发布与多人权限均属后续独立决策;本工作流不顺手实施。 diff --git "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/image_workflow_v1.md" "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/image_workflow_v1.md" index b4fe580..40be0b6 100644 --- "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/image_workflow_v1.md" +++ "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/image_workflow_v1.md" @@ -1,12 +1,12 @@ # Image 2 关键帧工作流 v1 -本文固化计划 04 的“雨夜信使入城”实测方法。ImageGen 是 Codex 制作阶段的外部图片能力,不嵌入包 A,不读取或保存 API 密钥;包 A 仍只消费已经落盘并登记哈希的图片。 +本文固化计划 04 的“雨夜信使入城”实测方法。ImageGen 是 Codex 制作阶段的外部图片能力,不嵌入包 A,不读取或保存 API 密钥;包 A 仍只消费已经落盘并登记哈希的图片。当前第一版把每张入选图直接作为一个静态镜头,不再为图片动态化额外拆层或预留运动素材。 ## 固定控制面 - 角色:24–28 岁东亚男性山地信使,瘦而结实;黑发高束、少量雨湿碎发;靛青窄袖衣、炭灰内层、黑色护腕、深褐腰带与单只深褐斜挎信袋;无甲、无帽、无武器。 - 风格:半写实中国叙事插画;古代山地边城、雨夜、冷靛/石板灰/炭黑,灯火只用克制琥珀色;湿石、湿木、湿布与轻微笔触。 -- 构图:9:16;下方约 18% 保持安静、偏暗,四周留运镜余量。 +- 构图:9:16;下方约 18% 保持安静、偏暗,以容纳字幕;主体焦点须适合包 A 的竖屏规格化裁切。 - 禁止:画面文字、字母、标志、水印、现代物件;不得用锐化、投影、重绘或滤镜掩盖原始缺陷。 ## 两阶段方法 @@ -27,7 +27,7 @@ | 005 | 山脊警灯接力 | 城墙沿山脊递进的纯环境远景 | 1 | 可入 Bundle;`landscape` 基准 | | 006 | 号角与全城响应 | 城垛侧面人物吹号、全城远景 | 2 | 可入 Bundle | -最终六镜尺寸均为 941×1672,比例接近 9:16;不以图片后处理伪装为原生 1080×1920。进入动态实验时,由包 A 已验证的 FFmpeg 规格化为 1080×1920、30 FPS、H.264/yuv420p。 +最终六镜尺寸均为 941×1672,比例接近 9:16;不以图片后处理伪装为原生 1080×1920。进入正式静态工作流时,由包 A 已验证的 FFmpeg 规格化为 1080×1920、30 FPS、H.264/yuv420p,并按该镜头人声时长保持画面静止。 ## 验收与复用 diff --git "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/job_bundle_v1.md" "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/job_bundle_v1.md" index 4482c03..8e7324e 100644 --- "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/job_bundle_v1.md" +++ "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/job_bundle_v1.md" @@ -51,6 +51,8 @@ job-/ 基础运动预设固定为:`static`、`slow_push_in`、`slow_pull_out`、`pan_left`、`pan_right`、`tilt_up`、`tilt_down`、`gentle_drift`。它们是高级工具缺失时仍可确定执行的语义,不代表绑定某个 Provider。 +当前第一版的产品默认比 Schema 能力更窄:所有新建镜头统一写 `motion.preset: "static"`、`motion.strength: "low"`,所有镜间切换统一写 `transition_out.type: "cut"`、`transition_out.duration_sec: 0`。其余运动预设与 `crossfade` 仅为旧任务兼容能力,不由当前工作流主动生成。`visual.focus` 仍须保留,它用于竖屏规格化裁切,并不意味着运镜。 + ## 路径与完整性 JSON 中的文件路径统一使用 Bundle 内 POSIX 相对路径。允许中文和空格;拒绝空值、首尾空白、控制字符、反斜杠、URI、`~`、`.`/`..` 段、POSIX/Windows/UNC 绝对路径、解析后越界,以及 Bundle 根、祖先或目标符号链接。 diff --git "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/motion_experiment_playbook_v1.md" "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/motion_experiment_playbook_v1.md" new file mode 100644 index 0000000..08ac03e --- /dev/null +++ "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/motion_experiment_playbook_v1.md" @@ -0,0 +1,101 @@ +# 短视频 V2 主体微动态实验与配方账(v1) + +> 状态:历史实验手册,非第一版活跃工作流。自 2026-08-13 起,第一版不做图片动态化;新建、续接、渲染与返修任务不得按本手册启动 HyperFrames、DepthFlow、I2V 或其他动态实验。仅当用户另行开启动态专项时,才可把它作为过往方法与失败证据查阅。 + +本文件回答两件事:成功方法如何跨会话复用;何时应停止逐动作试错,给出“多数镜头可用”或“不宜继续”的有限结论。它只约束实验与人工作者流程,不修改 Schema v1、Job Bundle 或包 A/B 正式管线。 + +## 1. 配方状态 + +每条动作配方只能依次进入以下状态,不得跳级: + +```text +draft +→ technical_candidate +→ replay_verified +→ user_accepted +→ production_recipe + ↘ rejected +``` + +- `technical_candidate`:资产、时间线、媒体和自动门成立。 +- `replay_verified`:冻结工程在新鲜会话中至少冷重放两次,同一 seek 时点像素哈希一致。 +- `user_accepted`:用户已看连续成片,并确认自然度与瑕疵可接受。 +- `production_recipe`:还须通过本文件第 4 节的跨视频广度门;单一成功镜头不能直接升级。 +- `rejected`:出现硬缺陷,或越过时间盒仍未达到门槛;须记录失败边界,不反复换参数续命。 + +计划 07 最终两条配方已分流:“小鹿眨眼+单耳加权形变”为 `user_accepted / manual_only`;“植物语义连通拆层+多叶局部网格错相摆动”的技术状态保留 `replay_verified_technical_candidate`,但用户状态为 `user_rejected_due_to_ghosting / rejected`。二者都不是 `production_recipe`。 + +## 2. 每条配方必须冻结什么 + +配方 manifest 至少记录: + +1. 输入与输出:源图、clean plate、透明层、证明帧、成片的路径、尺寸、alpha 语义与 SHA-256。 +2. 绑定:父子层级、遮挡顺序、唯一像素归属、枢轴、活动 ROI、刚性区与柔性权重区。 +3. 时间线:唯一 paused 主时间线、绝对关键时点、持续时间、缓动、幅度、回稳与有限状态数。 +4. 运行时:HyperFrames、Node、GSAP、Chrome、FFmpeg、Python/Pillow、Swift/macOS 版本;命令以 argv 数组记录。 +5. 验证:静态重组、峰值 ROI、乱序 seek、strict check、媒体规格、全解码、逐帧瑕疵门与用户结论。 +6. 成本:作者用时、返修次数、缓存和输出体积。 +7. 边界:哪些素材适用,哪些动作会导致木偶、重影、接缝、身份漂移或语义不成立;判废的尝试也须保留。 + +冻结包必须自包含,不得只指向某次会话的缓存文件。新鲜会话应能仅凭 manifest、脚本、资产和命令重建或重放。 + +## 3. 已验证的窄配方 + +### 3.1 动物:眨眼与单耳微摆 + +- clean background、身体、完整头部、闭眼状态、颈部遮挡与前景各自独立。 +- 眨眼是父级头部上的局部状态,不让眼睑另起时钟。 +- 单耳采用同一纹理、同一拓扑网格;耳根固定,位移沿耳轴渐增,脸、头骨、颈和另一耳保持冻结。 +- SpriteKit 离线烘焙有限状态;HyperFrames/GSAP 以显式状态层在单一时间线上切换。相同 `t` 的乱序 seek 必须得到相同 PNG 哈希。 +- 不再使用整头 Z 轴侧倾冒充低头;不使用独立生成姿态交叉淡化或光流融合。 + +### 3.2 植物:唯一像素归属与错相叶动 + +- 每个可见主体像素只能归一个活动层;先按画面前后顺序反向分配前景叶、左叶、顶叶、右叶,茎作为剩余所有者,静态 seam cover 归零。 +- 多阈值 alpha 的两两重叠、缺口与额外像素必须均为 0;零姿态重组须与 canonical subject 等价。 +- 顶叶、左叶、前景叶共用单一时间线,但相位和回稳不同;露珠等附件必须成为对应叶层的子级。 +- 批次 8 的 6–14px 只是技术基线,用户已判定幅度偏小,不能再写作“已通过的可感知门”。批次 9 改用顶/左/右下前景株约 21.9/16.0/16.0px 的错峰峰值,并要求至少两组主叶同时达约 10px 可见位移持续不少于15帧。这仍须以用户连续成片观感为准,不以数字或缩小联系表代替。 +- 当前只验证了三个语义连通局部叶层的轻风错峰动作;整株风场耦合与其他叶片响应的语义自然度尚未验证。 +- 若大幅刚性旋转会拉开语义不连通残片,必须改为连通资产与根部锁定局部网格;不得以加大整层角度继续“调参续命”。 +- 批次 9 用户终审确认“动作明显”且“像一阵风”,但同时确认“明显重影”。因而可感知性和风感门通过,瑕疵门失败;该配方最终 `rejected`。 +- 自动 high 逐帧门曾报 PASS,却漏掉了用户在连续观看中明确感知的重影,属于自动视觉门假阴性。配方状态必须同时保留“技术可重放”与“用户视觉拒绝”,且后者在产品决策中优先。 +- 事后解码复核显示,重影并非 crossfade 或纯编码拖尾:每层始终只有一个可见 state,但活动层仍是包含多片叶和枝杈纹理的粗语义簇,独立形变后会彼此穿插,并与静态主茎或右叶形成结构性错叠。今后“语义连通”门不得只检查 8 连通与 alpha 互斥,还须检查每层的形变语义纯度、扫掠区内动态—动态及动态—静态结构关系,以及原尺寸连续播放下的双轮廓。 +- 离散 pose ladder 还须报告全片状态保持率;本例 149 对相邻帧中有 77 对保持同一组合 state。它不等于重影根因,却会增强阶梯与视觉滞留,故不能再只以相邻叶尖位移和乱序 seek 哈希作为时间域质量证明。 + +## 4. “多数镜头可用”的有限广度门 + +不得围绕一个耳朵或一片叶子无限深挖。真正的产品判断采用预注册的 `3 个真实视频 × 每个 4 类镜头 = 12 镜`: + +1. 先做适配性筛选:至少 9/12 为 2D eligible,且每个视频至少 3/4 eligible;否则路线只可为 `manual_only`。 +2. 六镜小试:至少 5/6 通过才继续;4/6 为 `manual_only`;不超过 3/6 则停止。 +3. 十二镜总门:至少 9/12 通过、2D eligible 镜头通过率至少 80%、每个视频至少 3/4、每种已声明拓扑至少 2/3,且用户不报告木偶、重影、断缝、闪烁或明显语义错误。 +4. 通过后才可标为 `majority_shot_eligible`;其余分别归入 `manual_only`、`rejected` 或 `semantic_motion_required`。 + +每镜时间盒为:首次作者工作不超过 30 分钟,唯一一次有界返修不超过 15 分钟;45 分钟即停止,绝对上限 60 分钟。若十二镜中位作者时长超过 30 分钟或 P90 超过 45 分钟,即使像素可用,也不属于多数镜头方案。 + +计划 07 终审中,案例 B 用户视觉门失败,故本广度门保持 `not_run`,最终处置为 `cancelled_due_to_failed_prerequisite`,失败前置为 `case_b_user_visual_gate`。这是终止条件,不自动启动批次 10。 + +## 5. 当前 2D 路线的停止边界 + +以下动作不得在当前拆层路线里逐个硬做:走路、转身、大幅肢体运动、手与物体交互、口型同步、复杂表情、长发和衣物大幅运动、水/烟/火、完整风场耦合、显著新视角或新表面。它们应标为 `semantic_motion_required`,另立角色动画或 I2V 计划。 + +### 5.1 用户授权最终因果例外的终止规则 + +- 路线已拒绝且后续批次已关闭时,只有用户明确授权的一次性因果排除才可登记为 `user_authorized_final_causal_exception`;它不是下一批次、Round 3、新候选或生产重开。 +- 例外须预先冻结单一表示与唯一有效首跑。本例仅允许 single-owner `36x48` unified mesh:G4 动作门 `PASS`,但 G5 `visible-strain` `HARD FAIL`(`sigma_min=0.435627`、`sigma_max=1.622578`、`cond=2.435135`、`area=0.462118–1.552697`、`p99=1.324495`),并记录 `zero-warp drift`;无效的 `G6 proxy` 不作裁决,也不能翻转硬门。 +- 例外一旦命中硬失败,立即停止,不建 HF composition/draft、不产出审片候选、不作第二次调参。批次 9 多层结构互穿只保留为重要贡献机制,不冒称唯一或完整根因;single-owner 统一网格因可见纹理应变不可用,记为 `single_owner_unified_mesh_hard_fail`。 +- 本植物路线据此永久终止为 `plant_route_permanently_stopped`。计划仍为 `complete / CONDITIONAL / manual_only`,小鹿仍是唯一 `user_accepted` 窄配方;普通自然主体生产、12 镜广度门与批次 10 继续关闭。 + +## 6. 成熟工具的渐进接入 + +- HyperFrames/GSAP 继续负责唯一主时间与最终 MP4;成熟 rig 只负责“某一绝对时间的形变姿态”。 +- 若批准购买,优先有界评估 Spine Professional:weighted mesh 与 `Animation.apply(..., time)` 最接近当前 seek 合同;首轮禁用 physics。 +- Rive 的 raster mesh 与 Web runtime 技术可行,但 Editor 为 online-first;严格无云边界下不用于敏感生产资产。 +- Live2D 更适合正面人物,但无已证明的无状态 seek 契约,且通用视频生成器可能落入 Expandable Application 特别许可;未书面确认前不接入。 +- After Effects Puppet Pin 适合已有授权时烘焙少量高价值镜头;本轮不新增约 8GB 安装与订阅。 + +无论采用何种作者工具,首个实验只允许一个 512–768px 裁切 ROI、3–5 层、约 3 根骨骼、1 个 weighted mesh、4 秒/30fps;须断网可运行、十个以上时点乱序 seek 同哈希、三次冷启动一致、静态重组等价、黑白洋红底无毛边,方可从“烘焙 atlas”升级到 runtime 直连。 + +## 7. 当前结论 + +小鹿证明了一条用户接受的窄动物配方,只可 `manual_only`;植物则证明层所有权、语义连通性、根部约束与数字位移门全部成立时,仍可在连续观看中留下人眼可见的重影。因此计划 07 最终为 `CONDITIONAL / manual_only`;植物 `user_rejected_due_to_ghosting / rejected`,普通自然主体生产路由关闭,广度门取消,不自动进入批次 10。 diff --git "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/motion_feasibility_v1.md" "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/motion_feasibility_v1.md" index 3d05ff2..ef72300 100644 --- "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/motion_feasibility_v1.md" +++ "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/motion_feasibility_v1.md" @@ -1,11 +1,13 @@ # 短视频 V2 图片动态化可行性与计划 05 交接(v1) +> 状态:历史研究记录,非第一版活跃工作流。自 2026-08-13 起,第一版统一采用多张静态分镜图、逐镜人声、字幕与硬切;新任务不读取本文件作默认路由,也不调用其中的动态工具。只有用户明确重开图片动态化专项时,才以本文件作为既往证据复核,且不得据此绕过新的授权、质量与工程评估。 + ## 1. 当前决定 - 默认路线始终是包 A 既有 FFmpeg 预设;它负责低复杂度动态、统一标准化与最终合成。 -- 用户匿名审片已完成。四组虽都相对选择了高级版本,但用户明确认为自然场景候选仍只是微放大、平移或晃动,缺少真正的场景内/主体内运动;因此正式高级候选为零。 -- HyperFrames 只保留信息设计型人工选项;DepthFlow 与 HyperFrames 自然场景构图不进入计划 05 正式路线。 -- MFLUX 因冷模型下载耗时失控归为 `research_only`;Draw Things / 本地 I2V 因未获授权归为 `research_only`。 +- 用户匿名审片已完成。四组虽都相对选择了高级版本,但用户明确认为自然场景候选仍只是微放大、平移或晃动,缺少真正的场景内/主体内运动;因此计划 05 当时的正式高级候选为零。 +- HyperFrames 信息设计型人工选项继续保留为 `manual_only`。计划 07 Round 1 匿名审片的 `FAIL / rejected` 仍是冻结历史;Round 2 最终为 `CONDITIONAL / manual_only`:仅小鹿眨眼+单耳局部网格窄配方获用户接受。植物动作虽明显且像一阵风,但用户确认明显重影,故为 `user_rejected_due_to_ghosting / rejected`。普通人物/动物/植物生产路由关闭,12 镜广度门不启动。 +- 本地静态图后备路线已退役;Image 2 仍是唯一经实际成片验证的图片生成主路。Draw Things / 本地 I2V 因未获授权归为 `research_only`。 - 高级工具不得写 Schema v1、不得进入 Job Bundle 公共输入、不得接管时间线、字幕、TTS、缓存或 final。 ## 2. 镜头路由 @@ -15,8 +17,8 @@ | 普通人物、建筑、风景图 | FFmpeg `slow_push_in` / `gentle_drift` | 无需高级工具 | 正式默认;优先微放大,避免无叙事依据的左右晃动 | | 有明显前中后景、轮廓清楚的环境 hero 镜头 | FFmpeg | DepthFlow 外部 2.5D | `rejected`;不能产生雨落、旗动、人物动作等语义运动 | | 路线、警灯、档案状态、信息图或可寻址 DOM 动画 | FFmpeg 静态推拉兜底 | HyperFrames 设计型单镜头 | `manual_only`;用户认为动态更明显,可用于少量特殊镜头 | -| 普通人物氛围层或环境分层 | FFmpeg | HyperFrames 手工构图 | `rejected`;当前雨层/摆动没有形成足够可感知的真实动态 | -| 本地静态生成后备 | Image 2 仍是主路径 | MFLUX | `research_only` | +| 普通人物/动物/植物拆层微动作 | 明确要求静态时用静帧;否则仅用 FFmpeg 克制推近 | 仅保留小鹿眨眼+单耳窄配方作人工参考 | 普通自然主体生产路由关闭;植物 `user_rejected_due_to_ghosting / rejected`;不启动广度门 | +| 静态图生成 | Image 2 | 用户提供关键帧 | 无另一本地生成后备 | | 本地 I2V | 不进入 MVP | Draw Things / SkyReels | `research_only` | ## 3. 计划 05 最小接口 @@ -53,15 +55,14 @@ result: - 所有命令必须由 `CommandRunner` 以 `list[str]`、`shell=False`、有限超时和统一取消执行。 - 外部工具先写隔离 raw;包 A 再标准化到无声 H.264/yuv420p。DepthFlow 不进入计划 05 正式适配,因此其 raw 原子暂存缺口不扩展为正式工程工作。 -- 未来缓存键最少包含:输入图 SHA-256、route/version、时长/尺寸/FPS、运动参数;HyperFrames 另含 composition 源 SHA-256,DepthFlow 另含模型 revision。 +- 未来缓存键最少包含:输入图 SHA-256、route/version、时长/尺寸/FPS、运动参数;HyperFrames 另含 composition 源 SHA-256。 - FFmpeg 对照不得删除;高级路线失败、超时、取消或媒体校验失败时,应回落默认路线并保护旧产物。 -- 中央实验根为 `/Users/yuh/Library/Caches/text-video-plan04-feasibility/`,其 `mflux/`、`depthflow/`、`hyperframes/` 可逐路线单独移除;不得笼统清理其他用户缓存。 +- 中央实验根为 `/Users/yuh/Library/Caches/text-video-plan04-feasibility/`;退役的本地静态图路线已清理,余下路线仍必须按精确子目录单独处理,不得笼统清理其他用户缓存。 ## 5. 许可与分发 - HyperFrames:Apache-2.0;当前仅中央实验项目与浏览器运行时,不打入包 A 发布物。 - DepthFlow:AGPL-3.0;仅作为独立外部人工工具继续评估,不复制或链接其代码进包 A。 -- MFLUX:代码 MIT,所选模型 Apache-2.0;当前模型未完整取得。 - Draw Things:社区 CLI GPL-3.0;SkyReels 为 Skywork Community License;未下载,生产前仍需许可复核。 ## 6. 用户审片与最终判定 @@ -78,9 +79,59 @@ result: | 镜头微放大、推拉、平移 | 是 | FFmpeg 已足够;属于虚拟摄影机运动,不是画面对象自身运动 | | 深度视差、轻微换视点 | 是 | DepthFlow 可做,但本质仍是估深后的摄影机运动,不能让雨、人物、旗帜真正运动 | | 程序化雨线、雾、灯光闪烁、信息图动画 | 有条件支持 | HyperFrames 可生成真实随时间变化的 2D 叠层;当前样片雨层过弱,用户未感知到有效增益。加强后仍是合成层,不是原图中物体的生成式运动 | -| 人物整体轻摆 | 有条件支持 | 先把人物与背景、躯干/头部等拆成透明层,再以枢轴做 2D puppet;人工准备较重,容易出现纸片感,只适合少量风格化镜头 | +| 人物/动物 2D 拆层枢轴动作 | 窄配方成立,广度未证明 | 小鹿以 clean background、身体、头部、眼睑、遮挡补片、单耳同拓扑局部网格与单一可 seek 时间线实现真局部动作,批次 8 已获用户接受;但仅此一例,不外推为普通镜头能力 | +| 植物叶片 2D 拆层微动 | 用户门失败 / `rejected` | 批次 9 的根部锁定网格峰值约 16–22px,用户确认动作明显、多叶像同一阵风,但同时报告明显重影。自动高质视觉门的 PASS 是假阴性,用户视觉门优先 | | 衣发、表情、肢体、雨水与环境自然联动 | 当前未支持 | 需要 I2V/视频扩散或专门的角色动画模型。FFmpeg、DepthFlow、当前 HyperFrames 构图都不能自动完成 | | 本地 I2V | 技术上可能,尚未验证 | M1 Pro 32GB 理论可试小型量化模型,但预计约 9.344 GiB 模型、较长推理、身份漂移与许可风险;用户当前未授权,计划 04 未下载 | | 云端 I2V | 未评估/未接入 | 当前项目没有经授权、经验证的视频生成 Provider;若未来评估,仍须保持包 A 时间线、字幕、缓存与最终 FFmpeg 合成职责 | -计划 05 应先采用“FFmpeg 微放大默认 + HyperFrames 信息设计人工可选”的保守交接。若产品必须达到雨落、人物呼吸/摆动、旗帜与烟雾等自然运动,应另立有界 I2V 可行性批次;在新的下载/API/费用授权前,不以二维晃动冒充真正动态。 +计划 05 已采用“FFmpeg 微放大默认 + HyperFrames 信息设计人工可选”的保守交接。计划 07 完成透明层、枢轴、遮罩与局部状态的正确复测后,仍未过用户视觉门,故不建立主体微动态正式或人工生产路由。若未来产品仍需走路、转身、表情、衣发或环境语义联动,必须另立有界 I2V/角色动画可行性计划,且需新的下载/API/费用授权。本轮不以整图晃动冒充主体动态。 + +## 8. 计划 07 纠偏状态 + +- 计划 04 的 HyperFrames CLI、Chrome、确定性渲染、信息设计和弱环境叠层证据继续有效; +- “自然人物已拒绝”的旧结论只适用于当时的整图推近、雨层和光罩方案,不适用于从未实施的头部、眼睑、身体、衣物或植物分层动作; +- 计划 07 已完成同源 S/W/L、受控动物和真实项目植物迁移;静态分层、枢轴、遮挡、单一可 seek 时间线、focused keyframes 与媒体硬门均成立; +- 匿名揭盲:案例 01 候选 01=L、02=W、03=S;案例 02 候选 01=W、02=S、03=L。 +- 用户认为案例 01 L 最像会动,但动作生硬且木偶感明显;案例 02 L 仅被感知为画面变大,局部叶片动作不可见。用户未见明显接缝、闪烁或晕动,但认为所有候选均不适合多数普通镜头,且当前效果差到无必要指定减弱/加强/删除某一动作。 +- 因用户否定整体基本边界且未给出有价值的有界返修目标,批次 6 不臆造参数调整、不重渲。计划 07 最终为 `FAIL`,HyperFrames 自然主体拆层微动态分类为 `rejected`。 +- 该拒绝不否定 HyperFrames `manual_only` 的信息设计用途,也不改变 Image 2 + 包 B + 包 A/FFmpeg 主链已通过的事实;不新建主体微动态 Provider,不修改 Schema v1、包 B 或包 A 正式管线。 + +## 9. Round 2 有界重开最终状态 + +- Round 1 的 `FAIL / rejected` 是已归档的用户审片结论,旧媒体、匿名审片、映射、评分与 `final-manifest.json` 均保持原状,不能被第二轮覆盖或改写。 +- 用户后续反馈已给出可执行的姿态表示与局部动作感知诊断,故仅撤销“没有有价值的有界返修目标”这一停止依据;它不推翻 Round 1 的视觉失败事实,也不表示主体微动态已获生产准入。 +- Round 2 已在批次 9 用户终审后收口为 `complete / CONDITIONAL / manual_only`。独立证据树保留全部历史门状态;普通人物、动物、植物的正式生产路由关闭,信息设计用途仍为 `manual_only`。 +- Round 2 不得修改 Schema v1、Job Bundle 公共输入、包 A/B 正式管线或计划 04 冻结工程;不得新增模型、Provider、云 API、费用或音频。 +- 第二轮已依成熟 2D 绑定原则(同纹理/同拓扑、刚性与权重形变分离)作最小本地验证。独立姿态生成、交叉淡化、双向光流与整头网格压缩皆因身份漂移、重影/晕边或头骨变扁而判废。 +- Round 2 小鹿最终为单耳 2.7° 加权网格微摆+两次眨眼,脸和颈部像素不变,已获用户接受。植物批次 9 的约 21.9/16.0/16.0px 错峰局部网格位移保留 `replay_verified_technical_candidate`,但用户因明显重影将其拒绝。工程门通过不能替代用户视觉门;正式生产路由不变。 + +### 9.1 用户复核后的证据更新 + +- 用户现已确认 Round 2 两案都读作“画面中的事物在动”,鹿的眨眼/耳部与植物叶片均可见;鹿总体自然,植物的主要失败是旧位同源叶片仍在,形成重影。用户主观上认为若修复,此路线可考虑多数普通镜头。 +- 此处记录的是批次 8 前的阶段性诊断:当时已确认植物层所有权重复是旧位双像的一项直接机制,而非时间线残帧;尚不能据此把它外推为整条植物路线的唯一根因。受影响修订只做两件事:鹿耳峰值由 2.4° 微增至 2.7°;植物改为每个主体像素唯一归一层,删除静态 seam cover,动作时序不变。 +- 植物新资产在 alpha 0–255 全阈值均为 0 holes / 0 extras / 0 pairwise overlap,层零姿态与 canonical subject RGBA 误差为 0;逐帧视频门未见旧位叶片、断根、洞、黑边或色边。鹿逐帧门未见跳变、双头、耳根裂缝或脸颈牵连,乱序 seek 同时点哈希一致。 +- 两案已冻结为 `replay_verified_awaiting_revision_review`,而非 `production_recipe`;用户返修审片前,现行 `rejected` 正式路由不变。 + +### 9.2 何时才能判断“视频多数镜头可动态化” + +- 两个正例只能证明窄能力,不能证明多数镜头。已建立 `motion_experiment_playbook_v1.md`,后续用一次性 `3 个真实视频 × 每个 4 类镜头 = 12 镜` 广度门收束,不再围绕单一耳朵或叶片无限调参。 +- 通过线为:至少 9/12、每视频至少 3/4、2D eligible 镜头通过率至少 80%、每种拓扑至少 2/3,且用户无木偶、重影、断缝、闪烁或明显语义错误。每镜最多 30 分钟首次作者工作 + 15 分钟唯一返修;45 分钟即止。 +- 走路、转身、手物交互、口型、复杂表情、长发衣物大幅运动、水烟火、完整风场联动、新视角/新表面不再逐项塞入当前 2D 路线,而归入 `semantic_motion_required`。 +- 若未来批准新作者工具与费用,优先有界评估 Spine weighted mesh 作为上游形变作者工具,HyperFrames/GSAP 仍掌唯一主时间与最终 MP4;Rive、Live2D 与 After Effects 分别受云、seek/许可或安装/订阅边界限制,本轮未接入。 + +### 9.3 批次 9 植物最终有界返修 + +- 批次 8 用户复核已将小鹿标为 `user_accepted`,对植物则给出“幅度太小、应让不止一片叶子同时摇动”的明确修订目标。这是对植物的最后一次自动有界调整,`automatic_batch10_forbidden=true`。 +- 直接增大批次 8 刚性整层角度的诊断稿露出严重斜缝和西瓜纵切缝,已判废且未对用户展示。根因是这些层含语义不连通残片;因此正式返修将顶叶、左叶、右下前景株重建为三个语义连通层,西瓜与其他残余回归静态所有者。 +- 每个活动层使用根部锁定的局部 SpriteKit 网格烘焙 31 个状态,由 HyperFrames/GSAP 单一 paused 时间线以绝对时间错峰切换,无交叉淡化、整图摇动、滤镜或粒子代替。顶/左/右下前景株峰值约 21.9/16.0/16.0px,两组主叶同时可见窗口为16帧。 +- 256 个 alpha 阈值的 holes/extras/pairwise-overlap 均为 0,零姿态与根部锁定 RGBA 误差均为 0,乱序 seek 同时点像素哈希一致。高质成片 150/150 帧视觉门未见重影、断根、运动开缝、闪烁、方块或色边,西瓜与相机保持静止。 +- 植物的技术状态保留为 `replay_verified technical candidate`,用户状态则是 `user_rejected_due_to_ghosting / rejected`。自动高质视觉门未发现用户看到的重影,故此次为自动视觉门假阴性,用户门优先。12 镜广度门保持 `not_run / cancelled_due_to_failed_prerequisite`,失败前置为 `case_b_user_visual_gate`;不启动批次 10,不重开正式集成。 +- 用户回复后的解码帧复核在约 1.27–3.03s 复现结构性双轮廓,1.53–2.00s 最明显。交叉淡化与纯编码拖尾已被排除;粗语义簇独立形变造成的动态—动态及动态—静态结构错叠,被确认为重要贡献机制,但最终因果例外已表明它不是已证明的唯一或完整根因。31 态硬切带来的状态持帧会加重滞留感,但只列为次因。 + +### 9.4 用户授权的最终因果例外与路线终止 + +- 一次性 single-owner `36x48` unified mesh 排除试验登记为 `user_authorized_final_causal_exception`,不属于批次 10、Round 3 或新候选;既有 `complete / CONDITIONAL / manual_only`、小鹿 `user_accepted`、普通自然主体生产路由关闭及广度门取消均不变。 +- 唯一有效首跑的 G4 动作门 `PASS`;G5 `visible-strain` 为 `HARD FAIL`,指标为 `sigma_min=0.435627`、`sigma_max=1.622578`、`cond=2.435135`、`area=0.462118–1.552697`、`p99=1.324495`,另记录 `zero-warp drift`。`G6 proxy` 无效,不作裁决。 +- 本例未创建 HF composition/draft、未形成用户审片媒体,也未作第二次调参。批次 9 多层结构互穿只可表述为重要贡献机制,不能再写成唯一或完整根因;single-owner 统一网格又因可见纹理应变而不可用。 +- 最终技术标记为 `single_owner_unified_mesh_hard_fail`,路线标记为 `plant_route_permanently_stopped`;不启动批次 10,不恢复 12 镜广度门或正式 Provider 集成。 diff --git "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/templates/brief_v1.md" "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/templates/brief_v1.md" index bc1053f..2caf98e 100644 --- "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/templates/brief_v1.md" +++ "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/templates/brief_v1.md" @@ -25,7 +25,7 @@ - 目标时长/镜数:默认 30–45 秒、6–10 镜 - 声音:默认仅包 B 人声;音色使用已验证选择 - 图片:默认 Image 2 独立关键帧,画面无字幕/水印 -- 动态:默认基础 FFmpeg 虚拟摄影机运动;不承诺自然语义动态 +- 画面:每镜一张静态图;`motion=static/low`,镜间 `cut/0`,不调用图片动态化 - 本次采用默认值及理由: - 尚须用户选择的关键问题: @@ -46,13 +46,13 @@ - Image 2 的职责: - 包 B 人声职责: - 包 A/FFmpeg 的职责: -- 已知动态边界如何向用户说明: +- 静态叙事、镜头切换与字幕约定: ## 镜头草案 -| shot_id | 叙事目的 | speech 类型/文本摘要 | 关键画面与焦点 | 基础运镜意图 | 字幕 | 转场 | 风险/证据 | +| shot_id | 叙事目的 | speech 类型/文本摘要 | 静态关键画面与裁切焦点 | 画面合同 | 字幕 | 切换 | 风险/证据 | | --- | --- | --- | --- | --- | --- | --- | --- | -| shot-001 | | | | | | | | +| shot-001 | | | | static/low | | cut/0 | | ## 制作与停止条件 @@ -62,4 +62,3 @@ - 候选成片用户终审问题: - 失败时首查报告/缓存/事件: - 返修时允许的最小影响层: - diff --git "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/templates/review_v1.md" "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/templates/review_v1.md" index 898f153..9a1a660 100644 --- "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/templates/review_v1.md" +++ "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/templates/review_v1.md" @@ -32,11 +32,11 @@ ## 逐镜审查 -| shot_id/时间点 | 内容/事实 | 画面/角色 | 人声 | 字幕 | 基础运动 | 问题级别 | 证据 | +| shot_id/时间点 | 内容/事实 | 静态画面/角色 | 人声 | 字幕 | 画面保持/硬切 | 问题级别 | 证据 | | --- | --- | --- | --- | --- | --- | --- | --- | | shot-001 | | | | | | | | -基础运动只按虚拟摄影机推拉、平移、轻漂移评估;雨落、流水、车辆、人物动作等自然语义动态未实现,不得以二维运镜冒充。 +第一版应逐镜保持静态,并在镜间硬切。审查时确认没有非预期推拉、平移、漂移、拆层动作或叠化;不以任何二维晃动冒充自然动态。 ## 用户反馈 @@ -46,13 +46,13 @@ - 暂停点是否合适: - 镜头返修方式是否清楚: - 是否仍需大量底层操作: -- 是否接受当前动态边界与 UI 延期: +- 是否接受当前静态叙事方向与 UI 延期: - 判定:accepted / revision_requested / rejected / awaiting ## 最小返修 - 用户指出的镜头/时间点: -- 根因与影响层:文本 / 字幕 / 音频 / 关键帧 / focus-motion / final 时间线 +- 根因与影响层:文本 / 字幕 / 音频 / 关键帧 / focus-crop / final 时间线 - 最小改动: - 需重建:audio / shot / final - 不应重建: diff --git "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/workflow_acceptance_v1.md" "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/workflow_acceptance_v1.md" index c1a92ae..4a29cd5 100644 --- "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/workflow_acceptance_v1.md" +++ "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/docs/short_video_v2/workflow_acceptance_v1.md" @@ -3,6 +3,8 @@ > 状态:`complete` > 日期:2026-08-12 > 当前总判定:`PASS`;首轮六份计划全部完成。 +> +> 2026-08-13 产品配置更新:本记录继续证明 Schema v1、包 A/B 主链和导演 Skill 可用,但新任务的视觉默认已收束为多张静态分镜图与硬切。记录中的基础运镜/动态边界措辞只反映当时验收现场,不再指导第一版执行。 ## 1. 分拆判定 @@ -15,7 +17,7 @@ | 用户工作流可用性 | PASS | 用户已通过实际操作生成一条符合要求的视频,并明确判定“工作流通过” | | 计划 06 总判定 | PASS | 文档、Skill、行为、演练、保护检查与用户真实使用门全部通过;首轮六份计划完成 | -Skill 不是视频主链的技术依赖。即使它暂时未被某个会话发现,Codex 内容与分镜 → Image 2 关键帧 → 包 B 逐镜人声 → 包 A/FFmpeg 校验与合成的主链仍可直接按本文件、`director_workflow_v1.md` 和模板执行;不得把 Skill 问题反写成计划 05 主链失败。 +Skill 不是视频主链的技术依赖。即使它暂时未被某个会话发现,Codex 内容与静态分镜 → Image 2 静态关键帧 → 包 B 逐镜人声 → 包 A/FFmpeg 校验、静态镜头编码与硬切合成的主链仍可直接按 `director_workflow_v1.md` 和模板执行;不得把 Skill 问题反写成计划 05 主链失败。 ## 2. 最短使用方式 diff --git "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase4/README.md" "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase4/README.md" index ad68e3b..e99c070 100644 --- "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase4/README.md" +++ "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase4/README.md" @@ -1,5 +1,7 @@ # 短视频 V2 计划 04 隔离实验 +> 状态(2026-08-13):历史实验目录,第一版不调用。图片主路仍为 Image 2;动态适配器与基准测试只保留既往可行性证据,不进入正式工作流。 + 本目录只承载图片与单镜头动态化的可行性验证,不属于包 A 正式管线。实验输入、原始输出、标准化输出、日志与 manifest 都写入被 `**/成片/` 忽略的工作区;不得写入正式 Job Bundle 的 `cache/manifest.json`。 ## 固定边界 @@ -10,7 +12,7 @@ - 所有外部命令都经 `video_v2.runtime.CommandRunner` 以 `list[str]`、`shell=False` 执行。 - 每个 run 使用独立 `run_id`,分开保存 `raw/`、`normalized/` 与 `manifest.json`;已存在 run 拒绝覆盖。 - 不读取或写入 OpenAI API 密钥,不生成 BGM/SFX/TTS,不修改 Schema、正式缓存、时间线、字幕或 final。 -- MFLUX、DepthFlow、HyperFrames、Draw Things/I2V 只有在获得下载/安装授权并实际进入实验时才新增 adapter;不要预留空壳模块。 +- 已退役的本地静态图后备不再携带 adapter、环境或模型缓存。DepthFlow/HyperFrames 适配器仅作完成过的计划 04 实验记录,不是正式 Provider。 ## FFmpeg 基线 diff --git "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase4/providers/mflux_adapter.py" "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase4/providers/mflux_adapter.py" deleted file mode 100644 index e9f7008..0000000 --- "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase4/providers/mflux_adapter.py" +++ /dev/null @@ -1,122 +0,0 @@ -"""计划 04 的 MFLUX 静态图片隔离适配器。""" - -from __future__ import annotations - -import math -import os -from pathlib import Path -from typing import Mapping - -from video_v2.runtime import CommandResult, CommandRunner - - -MFLUX_ROOT = Path("/Users/yuh/Library/Caches/text-video-plan04-feasibility/mflux") -MFLUX_MODEL = "mlx-community/flux2-klein-4b-4bit" -MFLUX_BASE_MODEL = "flux2-klein-4b" - - -def _under(root: Path, candidate: Path) -> Path: - resolved_root = root.resolve(strict=False) - resolved = candidate.resolve(strict=False) - try: - resolved.relative_to(resolved_root) - except ValueError as exc: - raise ValueError(f"路径必须位于隔离根目录:{resolved_root}") from exc - return resolved - - -def build_mflux_env(root: Path = MFLUX_ROOT) -> dict[str, str]: - isolated = root.resolve(strict=False) - env = dict(os.environ) - env.update({ - "HF_HOME": str(isolated / "hf"), - "HUGGINGFACE_HUB_CACHE": str(isolated / "hf" / "hub"), - "HF_HUB_DISABLE_XET": "1", - "XDG_CACHE_HOME": str(isolated / "cache"), - "TOKENIZERS_PARALLELISM": "false", - }) - return env - - -def build_mflux_generate_argv( - prompt: str, - output: Path, - *, - executable: Path | None = None, - root: Path = MFLUX_ROOT, - seed: int, - width: int, - height: int, - steps: int = 4, - cache_limit_gb: int = 8, -) -> list[str]: - isolated = root.resolve(strict=False) - command = _under( - isolated, - executable or isolated / "venv" / "bin" / "mflux-generate-flux2", - ) - target = _under(isolated / "output", output) - if target.suffix.lower() != ".png": - raise ValueError("MFLUX 输出必须是 PNG") - if target.exists(): - raise FileExistsError(target) - clean_prompt = str(prompt).strip() - if not clean_prompt or len(clean_prompt) > 10_000: - raise ValueError("prompt 必须是 1–10000 字符的非空文本") - if not isinstance(seed, int) or seed < 0: - raise ValueError("seed 必须是非负整数") - if any(not isinstance(value, int) or value < 256 or value > 2048 or value % 16 for value in (width, height)): - raise ValueError("width/height 必须是 256–2048 范围内的 16 倍数") - if not isinstance(steps, int) or steps < 1 or steps > 50: - raise ValueError("steps 必须在 1–50") - if not isinstance(cache_limit_gb, int) or cache_limit_gb < 1 or cache_limit_gb > 8: - raise ValueError("MLX cache 上限必须在 1–8 GiB") - target.parent.mkdir(parents=True, exist_ok=True) - return [ - str(command), - "--model", MFLUX_MODEL, - "--base-model", MFLUX_BASE_MODEL, - "--prompt", clean_prompt, - "--seed", str(seed), - "--height", str(height), - "--width", str(width), - "--steps", str(steps), - "--guidance", "1.0", - "--low-ram", - "--mlx-cache-limit-gb", str(cache_limit_gb), - "--metadata", - "--output", str(target), - ] - - -def run_mflux_generate( - prompt: str, - output: Path, - *, - runner: CommandRunner, - root: Path = MFLUX_ROOT, - executable: Path | None = None, - seed: int, - width: int, - height: int, - steps: int = 4, - timeout_sec: float = 900.0, -) -> CommandResult: - timeout = float(timeout_sec) - if not math.isfinite(timeout) or timeout <= 0 or timeout > 1800: - raise ValueError("timeout_sec 必须是 (0, 1800] 的有限值") - return runner.run( - build_mflux_generate_argv( - prompt, - output, - executable=executable, - root=root, - seed=seed, - width=width, - height=height, - steps=steps, - ), - env=build_mflux_env(root), - check=True, - timeout=timeout, - ) diff --git "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/README.md" "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/README.md" new file mode 100644 index 0000000..205a920 --- /dev/null +++ "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/README.md" @@ -0,0 +1,77 @@ +# 短视频 V2 Phase 7:HyperFrames 拆层微动态实验合同 + +> 状态(2026-08-13):历史实验目录,第一版不调用。保留合同、测试与证据以便复盘;不得由新建、续接、渲染或返修任务自动运行。 + +本目录只保存计划 07 的轻量、可追溯实验合同。HyperFrames 工程、npm 缓存、Chrome、可再生 draft 和中间帧位于 `/Users/yuh/Library/Caches/text-video-plan07-hyperframes/`;最终审片证据位于本项目 `成片/短视频V2样片/phase7-hyperframes-micro-motion/`。 + +## 最终状态 + +- 计划等级:`CONDITIONAL`;最终分类:`manual_only`。Round 1 的 `FAIL / rejected` 作为冻结历史保留,不被 Round 2 覆盖。 +- 只保留一条已获用户接受的窄配方:小鹿两次眨眼 + 单耳加权网格微摆。它不外推为其他动物、人物或多数普通镜头的能力。 +- 植物批次 9 的动作被用户确认为“明显”且“像一阵风”,但同时存在“明显重影”,故用户门最终为 `user_rejected_due_to_ghosting / rejected`。 +- 工程、冷重放、seek 和自动 high 逐帧门的 PASS 仅保留为 `replay_verified_technical_candidate`证据。自动门未发现用户看到的重影,属假阴性;用户视觉门优先。 +- 普通自然主体生产路由关闭;12 镜广度门 `not_run / cancelled_due_to_failed_prerequisite`,失败前置为 `case_b_user_visual_gate`;不启动批次 10。HyperFrames 信息设计仍保持 `manual_only`,Image 2 + 包 B + 包 A/FFmpeg 主链不受影响。 + +## 实验问题 + +不使用生成式 I2V 时,一张静态图经真实拆层后,HyperFrames 能否以父子层、正确枢轴、遮罩和单一 seek-safe 时间线实现可感知局部动作,且优于静态与旧式整图运动? + +## 两个案例与公平对照 + +- 案例 A:原创、轮廓清楚、头颈/眼睛可拆的受控动物或角色。 +- 案例 B:当前项目真实素材,优先叶片轮廓清楚的西瓜幼苗/植物镜头,其次为头颈与背景较清楚的人物近景。 +- S:同源静态主图,无动作。 +- W:同源主图,仅整图推近/平移与简单叠层。 +- L:同源主图,含镜头层、主体局部动作、局部状态与环境/前景层。 + +对照必须同尺寸、同时长、同 FPS、同编码目标、无音轨,画面不暴露工具或“新版更好”暗示。 + +## 四层动作合同 + +1. 镜头层:camera rig 的克制推近,不与主体 scale 争用同一元素。 +2. 场景层:前中后景、草叶、光粒或局部光线,固定种子且有限循环。 +3. 主体层:body/subject 低幅呼吸,head 围绕颈部枢轴点动,或 leaf 围绕叶柄不同相位摆动。 +4. 局部状态层:眼睑/闭眼状态、露珠/局部高光等,必须与其父层同步。 + +案例 A 必须覆盖镜头层、主体层、局部状态层,并有一种环境/前景动作。不得以整图晃动、滤镜、粒子或光效冒充主体动态。 + +## 资产与静态姿态门 + +- 动物/人物:`master`、`background-clean`、`subject-cutout`、`body`、`head`、`eyes-closed|eyelid`、`foreground`。 +- 植物:`master`、`background-clean`、`stem|base`、`leaf-01..N`、可选 `fruit`、`foreground`、`light|particle`。 +- 所有资产须有来源、提示词或操作、尺寸、alpha、SHA-256、用途和版本。 +- 先把各层静态叠回 master,再审查动作峰值、闭眼/局部状态、浅/深底 alpha 边缘;静态不成立禁止进入动画。 + +## HyperFrames 实现合同 + +- 每个 composition 只有一个同步创建的 `gsap.timeline({ paused: true })`,以根 `data-composition-id` 为 `window.__timelines` 键。 +- camera/subject/head|leaf/eyelid|state/foreground/particle 使用稳定选择器与父子层;同一元素的同一 transform 属性不并发竞争。 +- 无 `Date.now`、`performance.now`、未种子 `Math.random`、运行时 fetch、计时器、未注册 rAF 或 `repeat:-1`。 +- 关键姿态:第一帧、动作前稳定、主动作峰值、局部状态峰值、回稳、final-minus-hold、最终帧。 +- 证据链:`lint`、`check --strict --snapshots`、真实主体 focused `keyframes --shot`、定点 `snapshot`、draft 自审、high render、ffprobe、完整解码、孤儿进程检查。 + +## 安全、资源与用户门 + +- 实验命令仅用 list argv、`shell=false`、有限 timeout,且输出路径必须在计划 07 根内。 +- 计划 07 新增下载/缓存上限 `1 GiB`;新模型、第三方 Provider、云/API/费用、破坏性操作须暂停。 +- 批次 5 冻结匿名审片材料后曾合法停于 `awaiting_user_review / CONDITIONAL`;映射与 Codex 评分均在用户观看前封存。 +- 用户已答复五问,批次 6 只做揭盲、失败分类、文档与回归;未改动作或重渲。最终不宣称 PASS 或正式候选。 + +## Round 2 重开状态(已收口) + +- Round 1 的 `FAIL / rejected`、原始媒体、匿名映射与 `final-manifest.json` 均为冻结历史,不得覆盖、改写或以 Round 2 产物替代。 +- 用户后续提出了可验证的姿态表示与局部动作感知问题;这只撤销当时“没有可执行的有界返修目标”这一停止依据,不撤销 Round 1 的用户视觉失败证据,也不恢复生产路由。 +- Round 2 已依用户终审收口为 `complete / CONDITIONAL / manual_only`。新媒体、历史审片集、私有映射和 `round2-manifest.json` 仍位于独立 `round2-reopen/` 证据树,冻结批次文件保留当时的 waiting 状态,最终结论只写在聚合 manifest 与现行文档。 +- 本轮仍不得修改 Schema v1、Job Bundle 公共契约、包 A/B 正式管线、计划 04 工程或已验收成片;不新增 Provider、模型、云 API、费用或音频。 + +Round 2 首次复核后,用户已确认两案均为真实对象运动:鹿总体自然,植物则因静态层保留同源叶片而出现重影。批次 8 以唯一像素所有权消除重影后,用户已明确接受小鹿;植物则因幅度偏小、同时动叶不足而要求最后一次有界返修。 + +批次 9 未继续放大整个不连通叶层;该路径已由斜缝压力试验判废。冻结候选将顶叶、左叶、右下前景株重建为三个语义连通层,每层以根部锁定的 31 状态局部网格在单一 paused 时间线上错峰摆动。三组峰值位移约 21.9/16.0/16.0px,有16帧至少两组主叶同时可见。自动 high 逐帧门当时报 PASS,但用户终审报告“明显重影”,故自动门为假阴性。最终状态为 `case_a=user_accepted / case_b=user_rejected_due_to_ghosting`;技术候选证据保留,但不获生产准入。 + +终审后的解码帧复核在约 1.27–3.03s 复现结构性双轮廓。它不是 crossfade、背景旧主体残留或单纯编码拖尾;现有门只证明每层唯一可见、alpha 互斥、8 连通与零姿态等价,却未证明活动层内的多叶/枝杈纹理具有同一形变语义,也未检查独立形变后的动态—动态及动态—静态结构错叠。31 态硬切及较高持帧率会加重滞留感,但列为次因。此问题仅作失败取证,不据此生成批次 10。 + +跨会话配方、失败边界与原预注册的 `3 视频 × 4 镜头` 有限广度门见 `docs/short_video_v2/motion_experiment_playbook_v1.md`。该门因案例 B 用户视觉前置失败而取消;不宣称多数普通镜头可自动动态化。走路、转身、手物交互、口型、复杂衣发、水烟火、完整风场耦合与新视角归为 `semantic_motion_required`。 + +## 用户授权的最终因果例外(永久收口) + +`user_authorized_final_causal_exception` 不是批次 10、Round 3 或新候选。唯一有效首跑以 single-owner `36x48` unified mesh 获 G4 动作门 `PASS`,但 G5 `visible-strain` `HARD FAIL`(`sigma_min=0.435627`、`sigma_max=1.622578`、`cond=2.435135`、`area=0.462118–1.552697`、`p99=1.324495`),并记录 `zero-warp drift`;`G6 proxy` 无效,不作裁决。试验未建 HF composition/draft、未二次调参。故批次 9 多层结构互穿只认定为重要贡献机制,而非唯一或完整根因;统一网格补救亦因可见纹理应变不可用,冻结为 `single_owner_unified_mesh_hard_fail / plant_route_permanently_stopped`。既有 `complete / CONDITIONAL / manual_only`、小鹿 `user_accepted`、普通自然主体生产路由关闭、广度门取消与不启动批次 10 均不变。 diff --git "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/build_case_a_flow_poses.py" "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/build_case_a_flow_poses.py" new file mode 100644 index 0000000..368745c --- /dev/null +++ "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/build_case_a_flow_poses.py" @@ -0,0 +1,112 @@ +"""Build deterministic in-between head poses from Apple Vision optical flow. + +The flow is computed outside this script by the local macOS Vision framework. +No model or package is downloaded. This script only remaps the two frozen RGBA +pose layers and blends their warped pixels at fixed fractions. +""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +import numpy as np +from PIL import Image + + +def read_flow(path: Path) -> np.ndarray: + with path.open("rb") as handle: + width, height, row_bytes = map(int, handle.readline().split()) + payload = handle.read() + rows = np.frombuffer(payload, dtype=np.uint8).reshape(height, row_bytes) + return rows[:, : width * 8].copy().view(np.float32).reshape(height, width, 2) + + +def resize_flow(flow: np.ndarray, width: int, height: int) -> np.ndarray: + """Resize a coarse flow field and preserve its displacement in full-res pixels.""" + + source_height, source_width = flow.shape[:2] + if (source_width, source_height) == (width, height): + return flow + x = np.asarray( + Image.fromarray(flow[..., 0], mode="F").resize((width, height), Image.Resampling.BILINEAR), + dtype=np.float32, + ) * (width / source_width) + y = np.asarray( + Image.fromarray(flow[..., 1], mode="F").resize((width, height), Image.Resampling.BILINEAR), + dtype=np.float32, + ) * (height / source_height) + return np.stack((x, y), axis=2) + + +def bilinear(image: np.ndarray, x: np.ndarray, y: np.ndarray) -> np.ndarray: + height, width, _ = image.shape + x = np.clip(x, 0, width - 1) + y = np.clip(y, 0, height - 1) + x0 = np.floor(x).astype(np.int32) + y0 = np.floor(y).astype(np.int32) + x1 = np.minimum(x0 + 1, width - 1) + y1 = np.minimum(y0 + 1, height - 1) + wx = (x - x0)[..., None] + wy = (y - y0)[..., None] + top = image[y0, x0] * (1 - wx) + image[y0, x1] * wx + bottom = image[y1, x0] * (1 - wx) + image[y1, x1] * wx + return top * (1 - wy) + bottom * wy + + +def warp(image: np.ndarray, flow: np.ndarray, fraction: float) -> np.ndarray: + height, width, _ = image.shape + yy, xx = np.mgrid[:height, :width].astype(np.float32) + return bilinear(image, xx - flow[..., 0] * fraction, yy - flow[..., 1] * fraction) + + +def premultiply(rgba: np.ndarray) -> np.ndarray: + alpha = rgba[..., 3:4] / 255.0 + return np.concatenate((rgba[..., :3] * alpha, alpha * 255.0), axis=2) + + +def unpremultiply(rgba: np.ndarray) -> np.ndarray: + alpha = rgba[..., 3:4] / 255.0 + rgb = np.divide(rgba[..., :3], alpha, out=np.zeros_like(rgba[..., :3]), where=alpha > 1e-5) + return np.concatenate((rgb, rgba[..., 3:4]), axis=2) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--neutral", type=Path, required=True) + parser.add_argument("--nod", type=Path, required=True) + parser.add_argument("--forward-flow", type=Path, required=True) + parser.add_argument("--backward-flow", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--fractions", default="0.2,0.4,0.6,0.8") + parser.add_argument( + "--flow-scale", + type=float, + default=1.0, + help="Diagnostic gain applied to Vision's pixel displacement vectors.", + ) + args = parser.parse_args() + + neutral = np.asarray(Image.open(args.neutral).convert("RGBA"), dtype=np.float32) + nod = np.asarray(Image.open(args.nod).convert("RGBA"), dtype=np.float32) + if neutral.shape != nod.shape: + raise SystemExit(f"pose dimensions differ: {neutral.shape} != {nod.shape}") + height, width = neutral.shape[:2] + forward = resize_flow(read_flow(args.forward_flow), width, height) * args.flow_scale + backward = resize_flow(read_flow(args.backward_flow), width, height) * args.flow_scale + + neutral_p = premultiply(neutral) + nod_p = premultiply(nod) + args.output.mkdir(parents=True, exist_ok=True) + for fraction in [float(value) for value in args.fractions.split(",")]: + if not 0 < fraction < 1: + raise SystemExit(f"invalid fraction: {fraction}") + start = warp(neutral_p, forward, fraction) + end = warp(nod_p, backward, 1.0 - fraction) + mixed = unpremultiply(start * (1.0 - fraction) + end * fraction) + name = f"head-flow-{round(fraction * 100):02d}.png" + Image.fromarray(np.clip(mixed, 0, 255).astype(np.uint8), "RGBA").save(args.output / name) + + +if __name__ == "__main__": + main() diff --git "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/prepare_case_a_assets.py" "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/prepare_case_a_assets.py" new file mode 100644 index 0000000..2f32ac5 --- /dev/null +++ "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/prepare_case_a_assets.py" @@ -0,0 +1,240 @@ +"""计划 07 案例 A 的确定性资产切分与静态姿态证明。 + +本脚本只处理已冻结的本地 PNG,不下载模型、不连网、不渲染视频, +也不实现新的动画引擎。输出用于 HyperFrames 父子层和批次 2 静态硬门。 +""" + +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageChops, ImageDraw, ImageFilter + + +CANVAS = (1080, 1920) +SOURCE_SIZE = (941, 1672) + + +def _scaled_point(x: float, y: float) -> tuple[int, int]: + return round(x * CANVAS[0] / SOURCE_SIZE[0]), round(y * CANVAS[1] / SOURCE_SIZE[1]) + + +def _scaled_box(box: tuple[float, float, float, float]) -> tuple[int, int, int, int]: + x0, y0 = _scaled_point(box[0], box[1]) + x1, y1 = _scaled_point(box[2], box[3]) + return x0, y0, x1, y1 + + +def _resize_rgba(path: Path) -> Image.Image: + with Image.open(path) as image: + return image.convert("RGBA").resize(CANVAS, Image.Resampling.LANCZOS) + + +def _replace_alpha(image: Image.Image, alpha: Image.Image) -> Image.Image: + output = image.copy() + output.putalpha(alpha) + return output + + +def _vertical_mask(*, start: int, end: int, invert: bool = False) -> Image.Image: + mask = Image.new("L", CANVAS, 0) + pixels = mask.load() + for y in range(CANVAS[1]): + if y <= start: + value = 0 + elif y >= end: + value = 255 + else: + value = round(255 * (y - start) / (end - start)) + if invert: + value = 255 - value + for x in range(CANVAS[0]): + pixels[x, y] = value + return mask + + +def _polygon_layer(image: Image.Image, polygons: list[list[tuple[int, int]]]) -> Image.Image: + shape = Image.new("L", CANVAS, 0) + draw = ImageDraw.Draw(shape) + for polygon in polygons: + draw.polygon(polygon, fill=255) + shape = shape.filter(ImageFilter.GaussianBlur(1.2)) + return _replace_alpha(image, shape) + + +def _closed_eye_layer(closed: Image.Image) -> Image.Image: + alpha = Image.new("L", CANVAS, 0) + draw = ImageDraw.Draw(alpha) + draw.ellipse(_scaled_box((348, 584, 449, 682)), fill=255) + draw.ellipse(_scaled_box((493, 584, 606, 682)), fill=255) + alpha = alpha.filter(ImageFilter.GaussianBlur(5.0)) + return _replace_alpha(closed, alpha) + + +def _pose_head_layer(pose_subject: Image.Image) -> Image.Image: + """Keep only the alternate head and upper neck for the Round 2 pose state. + + The source is a locally chroma-keyed full-body edit. The fade is intentionally + narrower than the Round 1 head/body overlap so the stable body remains the + identity anchor while the actual head geometry can change. + """ + + pose_alpha = pose_subject.getchannel("A") + head_mask = _vertical_mask(start=940, end=1050, invert=True) + return _replace_alpha(pose_subject, ImageChops.multiply(pose_alpha, head_mask)) + + +def _checkerboard(size: tuple[int, int], cell: int = 48) -> Image.Image: + image = Image.new("RGBA", size, (236, 236, 231, 255)) + draw = ImageDraw.Draw(image) + for y in range(0, size[1], cell): + for x in range(0, size[0], cell): + if (x // cell + y // cell) % 2: + draw.rectangle((x, y, x + cell - 1, y + cell - 1), fill=(55, 60, 64, 255)) + return image + + +def _labeled_panel(image: Image.Image, label: str) -> Image.Image: + panel = image.resize((270, 480), Image.Resampling.LANCZOS) + canvas = Image.new("RGBA", (270, 530), (24, 27, 29, 255)) + canvas.alpha_composite(panel, (0, 50)) + ImageDraw.Draw(canvas).text((14, 14), label, fill=(242, 238, 224, 255)) + return canvas + + +def prepare( + source_root: Path, + layer_root: Path, + proof_root: Path, + pose_source: Path | None = None, + pose_layer_output: Path | None = None, + pose_proof_output: Path | None = None, +) -> None: + source_root = source_root.resolve(strict=True) + layer_root.mkdir(parents=True, exist_ok=True) + proof_root.mkdir(parents=True, exist_ok=True) + + master = _resize_rgba(source_root / "master.png") + background = _resize_rgba(source_root / "background-clean.png") + closed = _resize_rgba(source_root / "eyes-closed-source.png") + subject = _resize_rgba(layer_root / "subject-cutout.png") + + subject_alpha = subject.getchannel("A") + head_mask = _vertical_mask(start=1010, end=1110, invert=True) + body_mask = _vertical_mask(start=960, end=1045) + head = _replace_alpha(subject, ImageChops.multiply(subject_alpha, head_mask)) + body = _replace_alpha(subject, ImageChops.multiply(subject_alpha, body_mask)) + + neck_mask = Image.new("L", CANVAS, 0) + neck_draw = ImageDraw.Draw(neck_mask) + neck_draw.ellipse(_scaled_box((372, 842, 576, 978)), fill=255) + neck_mask = neck_mask.filter(ImageFilter.GaussianBlur(7.0)) + neck_cover = _replace_alpha(subject, ImageChops.multiply(subject_alpha, neck_mask)) + eyes_closed = _closed_eye_layer(closed) + pose_head = None + if pose_source is not None: + pose_head = _pose_head_layer(_resize_rgba(pose_source)) + + # 前景只从同源 master 的已有草叶取样;多边形贴合可识别的叶片, + # 不使用方形贴片,避免位移时暴露背景块。 + left_polygons = [ + [_scaled_point(0, 1672), _scaled_point(0, 1260), _scaled_point(28, 1360), _scaled_point(58, 1672)], + [_scaled_point(38, 1672), _scaled_point(91, 1402), _scaled_point(106, 1416), _scaled_point(82, 1672)], + [_scaled_point(70, 1672), _scaled_point(155, 1434), _scaled_point(168, 1452), _scaled_point(118, 1672)], + ] + right_polygons = [ + [_scaled_point(941, 1672), _scaled_point(941, 1218), _scaled_point(904, 1338), _scaled_point(879, 1672)], + [_scaled_point(908, 1672), _scaled_point(846, 1376), _scaled_point(830, 1390), _scaled_point(858, 1672)], + [_scaled_point(862, 1672), _scaled_point(786, 1468), _scaled_point(774, 1482), _scaled_point(813, 1672)], + ] + foreground_left = _polygon_layer(master, left_polygons) + foreground_right = _polygon_layer(master, right_polygons) + + master.save(layer_root / "master.png") + background.save(layer_root / "background-clean.png") + subject.save(layer_root / "subject-cutout.png") + body.save(layer_root / "body.png") + head.save(layer_root / "head.png") + neck_cover.save(layer_root / "neck-cover.png") + eyes_closed.save(layer_root / "eyes-closed.png") + if pose_head is not None: + pose_layer_output = pose_layer_output or layer_root / "head-nod.png" + pose_layer_output.parent.mkdir(parents=True, exist_ok=True) + pose_head.save(pose_layer_output) + foreground_left.save(layer_root / "foreground-left.png") + foreground_right.save(layer_root / "foreground-right.png") + + hero = background.copy() + hero.alpha_composite(body) + hero.alpha_composite(head) + hero.alpha_composite(neck_cover) + hero.alpha_composite(foreground_left) + hero.alpha_composite(foreground_right) + hero.save(proof_root / "hero-recomposition.png") + + pivot = _scaled_point(482, 938) + rotated_head = head.rotate( + -2.6, + resample=Image.Resampling.BICUBIC, + center=pivot, + translate=(0, 6), + ) + subject_peak = background.copy() + subject_peak.alpha_composite(body) + subject_peak.alpha_composite(rotated_head) + subject_peak.alpha_composite(neck_cover) + subject_peak.alpha_composite(foreground_left) + subject_peak.alpha_composite(foreground_right) + subject_peak.save(proof_root / "subject-peak.png") + + closed_proof = hero.copy() + closed_proof.alpha_composite(eyes_closed) + closed_proof.save(proof_root / "eyes-closed-proof.png") + + if pose_head is not None: + nod_proof = background.copy() + nod_proof.alpha_composite(body) + nod_proof.alpha_composite(pose_head) + nod_proof.alpha_composite(neck_cover) + nod_proof.alpha_composite(foreground_left) + nod_proof.alpha_composite(foreground_right) + pose_proof_output = pose_proof_output or proof_root / "head-nod-proof.png" + pose_proof_output.parent.mkdir(parents=True, exist_ok=True) + nod_proof.save(pose_proof_output) + + for name, color in (("alpha-light", (241, 238, 224, 255)), ("alpha-dark", (18, 23, 28, 255))): + base = Image.new("RGBA", CANVAS, color) + base.alpha_composite(subject) + base.save(proof_root / f"{name}.png") + + checker = _checkerboard(CANVAS) + checker.alpha_composite(subject) + panels = [ + _labeled_panel(master, "MASTER"), + _labeled_panel(background, "CLEAN BACKGROUND"), + _labeled_panel(hero, "STATIC RECOMPOSITION"), + _labeled_panel(subject_peak, "HEAD PEAK"), + _labeled_panel(closed_proof, "EYES CLOSED"), + _labeled_panel(checker, "ALPHA CHECK"), + ] + sheet = Image.new("RGBA", (810, 1060), (12, 15, 17, 255)) + for index, panel in enumerate(panels): + sheet.alpha_composite(panel, ((index % 3) * 270, (index // 3) * 530)) + sheet.save(proof_root / "contact-sheet.png") + + +if __name__ == "__main__": + case_root = Path( + "/Users/yuh/Desktop/项目/文本视音屏生成器/" + "【包A】视频引擎包/成片/短视频V2样片/phase7-hyperframes-micro-motion/" + "case-a-controlled" + ) + round2_root = case_root.parent / "round2-reopen" / "case-a" + prepare( + case_root / "assets" / "source", + case_root / "assets" / "layers", + case_root / "proofs", + round2_root / "assets" / "source" / "head-nod-subject.png", + round2_root / "assets" / "layers" / "head-nod.png", + round2_root / "proofs" / "head-nod-proof.png", + ) diff --git "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/prepare_case_a_round2_ear_layers.py" "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/prepare_case_a_round2_ear_layers.py" new file mode 100644 index 0000000..95bf4f6 --- /dev/null +++ "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/prepare_case_a_round2_ear_layers.py" @@ -0,0 +1,67 @@ +"""Prepare bounded Round 2 ear layers for Plan 07 case A. + +The input is the already frozen full-canvas RGBA head layer. No generated +pixels, model, or dependency are introduced: masks divide the two ears from +the stable face/head base, and soft overlap around each ear root keeps the +rotation seam inside existing fur texture. +""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +from PIL import Image, ImageChops, ImageDraw, ImageFilter + + +CANVAS = (1080, 1920) + + +def soft_polygon(points: list[tuple[int, int]], blur: float = 4.0) -> Image.Image: + mask = Image.new("L", CANVAS, 0) + ImageDraw.Draw(mask).polygon(points, fill=255) + return mask.filter(ImageFilter.GaussianBlur(blur)) + + +def with_alpha(image: Image.Image, alpha: Image.Image) -> Image.Image: + output = image.copy() + output.putalpha(alpha) + return output + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--head", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + + head = Image.open(args.head).convert("RGBA") + if head.size != CANVAS: + raise SystemExit(f"expected {CANVAS}, got {head.size}") + alpha = head.getchannel("A") + + # The diagonals cross inside the fur-covered ear roots. A feathered, + # intentionally overlapping root gives each child layer a few pixels of + # safe rotation room while the base retains the face and neck identity. + left_mask = soft_polygon([(248, 425), (466, 442), (480, 636), (395, 686), (295, 624)]) + right_mask = soft_polygon([(832, 425), (614, 442), (600, 636), (685, 686), (785, 624)]) + left_alpha = ImageChops.multiply(alpha, left_mask) + right_alpha = ImageChops.multiply(alpha, right_mask) + union = ImageChops.lighter(left_mask, right_mask) + base_alpha = ImageChops.multiply(alpha, ImageChops.invert(union)) + + args.output.mkdir(parents=True, exist_ok=True) + with_alpha(head, base_alpha).save(args.output / "head-base.png") + with_alpha(head, left_alpha).save(args.output / "ear-left.png") + with_alpha(head, right_alpha).save(args.output / "ear-right.png") + + # Static reconstruction proof must match the input before motion starts. + proof = Image.new("RGBA", CANVAS, (0, 0, 0, 0)) + proof.alpha_composite(with_alpha(head, base_alpha)) + proof.alpha_composite(with_alpha(head, left_alpha)) + proof.alpha_composite(with_alpha(head, right_alpha)) + proof.save(args.output / "head-reconstruction.png") + + +if __name__ == "__main__": + main() diff --git "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/prepare_case_b_assets.py" "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/prepare_case_b_assets.py" new file mode 100644 index 0000000..d299f2e --- /dev/null +++ "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/prepare_case_b_assets.py" @@ -0,0 +1,152 @@ +from __future__ import annotations + +import hashlib +from pathlib import Path + +from PIL import Image, ImageChops, ImageDraw, ImageFilter + + +ROOT = Path(__file__).resolve().parents[2] +CASE = ( + ROOT + / "成片" + / "短视频V2样片" + / "phase7-hyperframes-micro-motion" + / "case-b-project-migration" +) +SOURCE = CASE / "assets" / "source" +LAYERS = CASE / "assets" / "layers" +PROOFS = CASE / "proofs" +SIZE = (1080, 1920) + + +def fit(path: Path, mode: str = "RGBA") -> Image.Image: + return Image.open(path).convert(mode).resize(SIZE, Image.Resampling.LANCZOS) + + +def polygon_mask(points: list[tuple[int, int]], blur: float = 2.0) -> Image.Image: + mask = Image.new("L", SIZE, 0) + ImageDraw.Draw(mask).polygon(points, fill=255) + return mask.filter(ImageFilter.GaussianBlur(blur)) + + +def masked_subject(subject: Image.Image, mask: Image.Image) -> Image.Image: + result = subject.copy() + result.putalpha(ImageChops.multiply(subject.getchannel("A"), mask)) + return result + + +def composite(background: Image.Image, layers: list[Image.Image]) -> Image.Image: + frame = background.convert("RGBA") + for layer in layers: + frame.alpha_composite(layer) + return frame + + +def sha(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def main() -> None: + LAYERS.mkdir(parents=True, exist_ok=True) + PROOFS.mkdir(parents=True, exist_ok=True) + + master = fit(SOURCE / "master.png") + background = fit(SOURCE / "background-clean.png") + subject = fit(SOURCE / "subject-cutout.png") + + leaf_top_mask = polygon_mask( + [(210, 190), (540, 190), (575, 420), (520, 575), (390, 700), (280, 575), (205, 400)], + 0.0, + ) + leaf_left_mask = polygon_mask( + [(0, 405), (295, 405), (380, 505), (345, 645), (175, 690), (0, 620)], + 0.0, + ) + leaf_right_mask = polygon_mask( + [(445, 390), (735, 390), (920, 545), (890, 730), (650, 785), (475, 655)], + 0.0, + ) + foreground_mask = polygon_mask( + [(775, 710), (1080, 655), (1080, 1120), (875, 1160), (740, 965)], + 0.0, + ) + seam_cover_mask = polygon_mask( + [(345, 570), (455, 555), (490, 735), (350, 760)], 3.0 + ) + seam_cover_mask = ImageChops.lighter( + seam_cover_mask, + polygon_mask([(235, 545), (420, 555), (455, 705), (245, 725)], 3.0), + ) + seam_cover_mask = ImageChops.lighter( + seam_cover_mask, + polygon_mask([(750, 900), (930, 880), (990, 1170), (770, 1180)], 3.0), + ) + + union = ImageChops.lighter(leaf_top_mask, leaf_left_mask) + union = ImageChops.lighter(union, leaf_right_mask) + union = ImageChops.lighter(union, foreground_mask) + base_mask = ImageChops.invert(union.point(lambda value: 255 if value > 8 else 0)) + + layers = { + "master": master, + "background-clean": background, + "subject-cutout": subject, + "stem-base": masked_subject(subject, base_mask), + "leaf-top": masked_subject(subject, leaf_top_mask), + "leaf-left": masked_subject(subject, leaf_left_mask), + "leaf-right": masked_subject(subject, leaf_right_mask), + "foreground": masked_subject(subject, foreground_mask), + "seam-covers": masked_subject(subject, seam_cover_mask), + } + for name, image in layers.items(): + image.save(LAYERS / f"{name}.png", optimize=True) + + hero = composite( + background, + [ + layers["stem-base"], + layers["leaf-top"], + layers["leaf-left"], + layers["leaf-right"], + layers["foreground"], + layers["seam-covers"], + ], + ) + hero.save(PROOFS / "hero-recomposition.png", optimize=True) + + peak_layers = [layers["stem-base"]] + transforms = [ + (layers["leaf-top"], (390, 680), 0.7), + (layers["leaf-left"], (330, 625), -0.5), + (layers["leaf-right"], (520, 730), 0.0), + (layers["foreground"], (820, 1080), -0.45), + ] + for layer, pivot, angle in transforms: + rotated = layer.rotate(angle, resample=Image.Resampling.BICUBIC, center=pivot) + peak_layers.append(rotated) + peak_layers.append(layers["seam-covers"]) + peak = composite(background, peak_layers) + peak.save(PROOFS / "leaf-peak.png", optimize=True) + + alpha = subject.getchannel("A") + for name, color in (("alpha-light", (242, 242, 242, 255)), ("alpha-dark", (22, 24, 28, 255))): + plate = Image.new("RGBA", SIZE, color) + plate.alpha_composite(subject) + plate.save(PROOFS / f"{name}.png", optimize=True) + + thumbs = [master, background, subject, hero, peak] + sheet = Image.new("RGB", (270 * len(thumbs), 480), "#171717") + for index, image in enumerate(thumbs): + sheet.paste(image.convert("RGB").resize((270, 480), Image.Resampling.LANCZOS), (index * 270, 0)) + sheet.save(PROOFS / "contact-sheet.jpg", quality=92) + + diff = ImageChops.difference(hero.convert("RGB"), peak.convert("RGB")) + print("peak_diff_bbox", diff.getbbox()) + print("alpha_corners", [alpha.getpixel(point) for point in ((0, 0), (1079, 0), (0, 1919), (1079, 1919))]) + for path in sorted((*LAYERS.glob("*.png"), *PROOFS.glob("*"))): + print(path.relative_to(CASE), sha(path)) + + +if __name__ == "__main__": + main() diff --git "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/prepare_round2_case_b_exclusive_assets.py" "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/prepare_round2_case_b_exclusive_assets.py" new file mode 100644 index 0000000..9fa7eeb --- /dev/null +++ "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/prepare_round2_case_b_exclusive_assets.py" @@ -0,0 +1,264 @@ +from __future__ import annotations + +import argparse +import hashlib +import json +from itertools import combinations +from pathlib import Path + +from PIL import Image, ImageChops + + +# Front-most independently moving artwork owns shared source pixels first. +# The static stem is the residual owner so every visible subject pixel belongs +# to exactly one layer and no old-position duplicate can remain behind a tween. +OWNERSHIP_PRIORITY = ( + "foreground", + "leaf-left", + "leaf-top", + "leaf-right", +) +OUTPUT_LAYERS = ( + "stem-base", + "leaf-right", + "leaf-top", + "leaf-left", + "foreground", +) +VALIDATION_THRESHOLDS = tuple(range(256)) +EXPECTED_INPUT_SHA256 = { + "background-clean": "0b39c25141235462b85723ede5a0be1aac0f59e8e8d89adc6253d22d72327dc7", + "subject-cutout": "65fdab0cd3987e5058448202e235b7ece1f707aeaf38ff0723d49709b0d19225", + "foreground": "a9b4df4d9a37503a15b4b2924ce6bdd806e4313f8f389be637c50053bdd88654", + "leaf-left": "00d90a04c867a430e975048efa4a79c3712f9b48f0bd80b05997d1f2fc63f585", + "leaf-top": "3d92c748dcd93d2a0d6249346563d37914f90b0a6b951ec4da28bd5e4c4618ce", + "leaf-right": "da2a722b721e08b227dc6059cbe9eda2d5bfa26e6b9c33896dce79edba83aef0", +} + + +def load_rgba(root: Path, name: str) -> Image.Image: + return Image.open(root / f"{name}.png").convert("RGBA") + + +def binary_alpha(image: Image.Image, threshold: int) -> Image.Image: + return image.getchannel("A").point( + lambda value: 255 if value > threshold else 0 + ) + + +def nonzero_count(image: Image.Image) -> int: + return sum(image.histogram()[1:]) + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def make_owned_layer( + subject: Image.Image, + assignment: Image.Image, +) -> Image.Image: + transparent = Image.new("RGBA", subject.size, (0, 0, 0, 0)) + result = Image.composite(subject, transparent, assignment) + result.putalpha(ImageChops.multiply(subject.getchannel("A"), assignment)) + return result + + +def union_masks(masks: list[Image.Image], size: tuple[int, int]) -> Image.Image: + result = Image.new("L", size, 0) + for mask in masks: + result = ImageChops.lighter(result, mask) + return result + + +def main() -> None: + parser = argparse.ArgumentParser( + description=( + "Create Round 2 case-B layers with one-and-only-one alpha owner " + "for every visible subject pixel." + ) + ) + parser.add_argument("--input-root", required=True, type=Path) + parser.add_argument("--output-root", required=True, type=Path) + parser.add_argument("--report", required=True, type=Path) + parser.add_argument("--proof", type=Path) + args = parser.parse_args() + + input_paths = { + name: args.input_root / f"{name}.png" + for name in EXPECTED_INPUT_SHA256 + } + input_report = { + name: { + "path": str(path), + "sha256": sha256(path), + "expected_sha256": EXPECTED_INPUT_SHA256[name], + "match": sha256(path) == EXPECTED_INPUT_SHA256[name], + } + for name, path in input_paths.items() + } + frozen_inputs_match = all( + item["match"] for item in input_report.values() + ) + if not frozen_inputs_match: + raise SystemExit("case-B frozen input hash gate failed") + + subject = load_rgba(args.input_root, "subject-cutout") + background = load_rgba(args.input_root, "background-clean") + candidates = { + name: load_rgba(args.input_root, name) + for name in OWNERSHIP_PRIORITY + } + if any(image.size != subject.size for image in candidates.values()): + raise SystemExit("case-B layer dimensions do not match subject-cutout") + if background.size != subject.size: + raise SystemExit("case-B background dimensions do not match subject-cutout") + + subject_support = binary_alpha(subject, 0) + owned = Image.new("L", subject.size, 0) + assignments: dict[str, Image.Image] = {} + for name in OWNERSHIP_PRIORITY: + candidate_support = binary_alpha(candidates[name], 0) + available = ImageChops.subtract(subject_support, owned) + assignment = ImageChops.multiply(candidate_support, available) + assignments[name] = assignment + owned = ImageChops.lighter(owned, assignment) + + assignments["stem-base"] = ImageChops.subtract(subject_support, owned) + layers = { + name: make_owned_layer(subject, assignments[name]) + for name in OUTPUT_LAYERS + } + + args.output_root.mkdir(parents=True, exist_ok=True) + output_paths: dict[str, Path] = {} + for name, image in layers.items(): + output_path = args.output_root / f"{name}.png" + image.save(output_path, optimize=True) + output_paths[name] = output_path + + # Keep a zero-byte-ownership compatibility asset for diagnostics only. + seam_path = args.output_root / "seam-covers.png" + Image.new("RGBA", subject.size, (0, 0, 0, 0)).save( + seam_path, optimize=True + ) + output_paths["seam-covers"] = seam_path + + # All hard gates operate on the files reloaded from disk, not on the + # pre-save Pillow objects, so the report proves the actual frozen outputs. + layers = { + name: Image.open(output_paths[name]).convert("RGBA") + for name in OUTPUT_LAYERS + } + + reconstructed = Image.new("RGBA", subject.size, (0, 0, 0, 0)) + for name in OUTPUT_LAYERS: + reconstructed.paste(layers[name], (0, 0), assignments[name]) + normalized_subject = subject.copy() + normalized_subject.paste( + (0, 0, 0, 0), + mask=ImageChops.invert(subject_support), + ) + identity_difference = ImageChops.difference( + reconstructed, normalized_subject + ) + identity_max_error = max( + maximum for _, maximum in identity_difference.getextrema() + ) + + expected_zero_pose = Image.alpha_composite(background, normalized_subject) + actual_zero_pose = Image.alpha_composite(background, reconstructed) + zero_pose_difference = ImageChops.difference( + actual_zero_pose, expected_zero_pose + ) + zero_pose_max_error = max( + maximum for _, maximum in zero_pose_difference.getextrema() + ) + if args.proof: + args.proof.parent.mkdir(parents=True, exist_ok=True) + actual_zero_pose.convert("RGB").save(args.proof, optimize=True) + + threshold_reports: dict[str, object] = {} + threshold_gate_passed = True + for threshold in VALIDATION_THRESHOLDS: + subject_mask = binary_alpha(subject, threshold) + layer_masks = { + name: binary_alpha(layers[name], threshold) + for name in OUTPUT_LAYERS + } + output_union = union_masks(list(layer_masks.values()), subject.size) + holes = ImageChops.subtract(subject_mask, output_union) + extras = ImageChops.subtract(output_union, subject_mask) + overlaps: dict[str, int] = {} + for left_name, right_name in combinations(OUTPUT_LAYERS, 2): + overlap = ImageChops.multiply( + layer_masks[left_name], layer_masks[right_name] + ) + overlaps[f"{left_name}:{right_name}"] = nonzero_count(overlap) + threshold_passed = ( + nonzero_count(holes) == 0 + and nonzero_count(extras) == 0 + and all(count == 0 for count in overlaps.values()) + ) + threshold_gate_passed = threshold_gate_passed and threshold_passed + threshold_reports[str(threshold)] = { + "holes": nonzero_count(holes), + "extras": nonzero_count(extras), + "pairwise_overlap": overlaps, + "passed": threshold_passed, + } + + report: dict[str, object] = { + "schema": "short-video-v2.phase7.case-b-exclusive-alpha-report.v2", + "ownership_threshold": 0, + "ownership_priority": list(OWNERSHIP_PRIORITY), + "residual_owner": "stem-base", + "runtime_layers": list(OUTPUT_LAYERS), + "removed_runtime_layer": "seam-covers", + "inputs": input_report, + "validation_threshold_count": len(VALIDATION_THRESHOLDS), + "thresholds": threshold_reports, + "identity": { + "normalized_rgba_max_error": identity_max_error, + "zero_pose_background_composite_max_error": zero_pose_max_error, + }, + "outputs": { + name: { + "path": str(path), + "sha256": sha256(path), + "size": path.stat().st_size, + "alpha_bbox": Image.open(path).convert("RGBA").getchannel("A").getbbox(), + "alpha_nonzero_pixels": nonzero_count( + Image.open(path).convert("RGBA").getchannel("A") + ), + } + for name, path in output_paths.items() + }, + "hard_gate": { + "frozen_inputs_match": frozen_inputs_match, + "multi_threshold_unique_ownership": threshold_gate_passed, + "normalized_rgba_identity": identity_max_error == 0, + "zero_pose_identity": zero_pose_max_error == 0, + "passed": ( + frozen_inputs_match + and threshold_gate_passed + and identity_max_error == 0 + and zero_pose_max_error == 0 + ), + }, + } + args.report.parent.mkdir(parents=True, exist_ok=True) + args.report.write_text( + json.dumps(report, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + if not report["hard_gate"]["passed"]: + raise SystemExit("exclusive alpha ownership gate failed") + + +if __name__ == "__main__": + main() diff --git "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/prepare_round2_case_b_semantic_motion_assets.py" "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/prepare_round2_case_b_semantic_motion_assets.py" new file mode 100644 index 0000000..9ae3291 --- /dev/null +++ "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/prepare_round2_case_b_semantic_motion_assets.py" @@ -0,0 +1,564 @@ +from __future__ import annotations + +import argparse +import hashlib +import json +import math +from itertools import combinations +from pathlib import Path + +from PIL import Image, ImageChops + + +FROZEN_INPUT_SHA256 = { + "background-clean": ( + "0b39c25141235462b85723ede5a0be1aac0f59e8e8d89adc6253d22d72327dc7" + ), + "subject-cutout": ( + "65fdab0cd3987e5058448202e235b7ece1f707aeaf38ff0723d49709b0d19225" + ), +} +EXCLUSIVE_INPUT_SHA256 = { + "stem-base": ( + "647c73665da1627321f703cf2be88f977328144e8909020315bbc6105ad637d7" + ), + "leaf-right": ( + "a1909ea4b7b1dc4bdb0ced9397383266c17f0fd8529c69efd5c72d74d05c8942" + ), + "leaf-top": ( + "b8be4165f23c7505c1ef1e8f17b423b87c0167cf4b5b9c80cdd156bf9636b99a" + ), + "leaf-left": ( + "776373648871e7424363482acf612724c7ef4898389c873eb8af77a90dbd81fe" + ), + "foreground": ( + "a79026e169483b3578be85a8a86d22ed1d0e492bee01fc79b91ed25735340eaa" + ), +} +SOURCE_LAYERS = tuple(EXCLUSIVE_INPUT_SHA256) +OUTPUT_LAYERS = ( + "stem-base", + "static-residual", + "leaf-right", + "leaf-top", + "leaf-left", + "foreground-plant", +) +DYNAMIC_LAYERS = ( + "leaf-top", + "leaf-left", + "foreground-plant", +) +VALIDATION_THRESHOLDS = tuple(range(256)) +CONNECTIVITY = 8 +ROOT_BAND_RATIO = 0.12 +PIVOT_HINTS = { + "leaf-top": { + "anchor_px": [390, 680], + "semantics": "narrow lower stem attachment", + }, + "leaf-left": { + "anchor_px": [330, 626], + "semantics": ( + "right-side stem attachment; do not infer the pivot from the " + "lower edge of the left blade" + ), + }, + "foreground-plant": { + "anchor_px": [916, 1148], + "semantics": "lower stem attachment of the right-side plant", + }, +} + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def load_rgba(path: Path) -> Image.Image: + return Image.open(path).convert("RGBA") + + +def binary_alpha(image: Image.Image, threshold: int) -> Image.Image: + return image.getchannel("A").point( + lambda value: 255 if value > threshold else 0 + ) + + +def nonzero_count(image: Image.Image) -> int: + return sum(image.histogram()[1:]) + + +def maximum_channel_error(left: Image.Image, right: Image.Image) -> int: + difference = ImageChops.difference(left, right) + return max(maximum for _, maximum in difference.getextrema()) + + +def connected_components( + alpha: Image.Image, + *, + retain_pixels: bool = False, +) -> list[dict[str, object]]: + width, height = alpha.size + values = alpha.tobytes() + seen = bytearray(width * height) + components: list[dict[str, object]] = [] + + for seed, seed_alpha in enumerate(values): + if seed_alpha == 0 or seen[seed]: + continue + seen[seed] = 1 + stack = [seed] + pixels: list[int] | None = [] if retain_pixels else None + count = 0 + alpha_sum = 0 + alpha_max = 0 + min_x = width + min_y = height + max_x = -1 + max_y = -1 + + while stack: + index = stack.pop() + y, x = divmod(index, width) + value = values[index] + count += 1 + alpha_sum += value + alpha_max = max(alpha_max, value) + min_x = min(min_x, x) + min_y = min(min_y, y) + max_x = max(max_x, x) + max_y = max(max_y, y) + if pixels is not None: + pixels.append(index) + + for neighbor_y in range(max(0, y - 1), min(height, y + 2)): + row_start = neighbor_y * width + for neighbor_x in range(max(0, x - 1), min(width, x + 2)): + neighbor = row_start + neighbor_x + if neighbor == index: + continue + if values[neighbor] and not seen[neighbor]: + seen[neighbor] = 1 + stack.append(neighbor) + + component: dict[str, object] = { + "alpha_nonzero_pixels": count, + "alpha_sum": alpha_sum, + "alpha_max": alpha_max, + "alpha_bbox": [min_x, min_y, max_x + 1, max_y + 1], + "touches_canvas_edge": ( + min_x == 0 + or min_y == 0 + or max_x == width - 1 + or max_y == height - 1 + ), + } + if pixels is not None: + component["pixels"] = pixels + components.append(component) + + components.sort( + key=lambda component: ( + int(component["alpha_nonzero_pixels"]), + int(component["alpha_sum"]), + ), + reverse=True, + ) + return components + + +def public_component_stats( + components: list[dict[str, object]], +) -> list[dict[str, object]]: + return [ + { + key: value + for key, value in component.items() + if key != "pixels" + } + for component in components + ] + + +def mask_from_pixels( + size: tuple[int, int], pixels: list[int] +) -> Image.Image: + mask = bytearray(size[0] * size[1]) + for index in pixels: + mask[index] = 255 + return Image.frombytes("L", size, bytes(mask)) + + +def split_largest_component( + image: Image.Image, +) -> tuple[Image.Image, Image.Image, list[dict[str, object]]]: + components = connected_components( + image.getchannel("A"), retain_pixels=True + ) + if not components: + raise SystemExit("semantic source layer has no visible alpha component") + selected_pixels = components[0]["pixels"] + if not isinstance(selected_pixels, list): + raise AssertionError("component pixel retention failed") + selected_mask = mask_from_pixels(image.size, selected_pixels) + transparent = Image.new("RGBA", image.size, (0, 0, 0, 0)) + selected = Image.composite(image, transparent, selected_mask) + residual = Image.composite(transparent, image, selected_mask) + return selected, residual, components + + +def merge_disjoint(images: list[Image.Image]) -> Image.Image: + if not images: + raise ValueError("at least one image is required") + result = Image.new("RGBA", images[0].size, (0, 0, 0, 0)) + for image in images: + support = binary_alpha(image, 0) + result = Image.composite(image, result, support) + return result + + +def root_band_report(image: Image.Image) -> dict[str, object]: + alpha = image.getchannel("A") + bbox = alpha.getbbox() + if bbox is None: + raise ValueError("dynamic layer has no alpha bbox") + x0, y0, x1, y1 = bbox + band_height = max(1, math.ceil((y1 - y0) * ROOT_BAND_RATIO)) + band_y0 = y1 - band_height + band = alpha.crop((x0, band_y0, x1, y1)) + band_bbox = band.getbbox() + absolute_band_bbox = None + weighted_x = 0 + weighted_y = 0 + alpha_sum = 0 + if band_bbox is not None: + bx0, by0, bx1, by1 = band_bbox + absolute_band_bbox = [ + x0 + bx0, + band_y0 + by0, + x0 + bx1, + band_y0 + by1, + ] + band_values = band.load() + for local_y in range(band.height): + for local_x in range(band.width): + value = band_values[local_x, local_y] + if value: + weighted_x += (x0 + local_x) * value + weighted_y += (band_y0 + local_y) * value + alpha_sum += value + centroid = ( + [round(weighted_x / alpha_sum, 3), round(weighted_y / alpha_sum, 3)] + if alpha_sum + else None + ) + return { + "definition": "bottom 12% of the selected alpha bbox; morphology hint only", + "ratio": ROOT_BAND_RATIO, + "y_range": [band_y0, y1], + "alpha_bbox": absolute_band_bbox, + "alpha_nonzero_pixels": nonzero_count(band), + "alpha_weighted_centroid_px": centroid, + } + + +def input_record(path: Path, expected_sha256: str) -> dict[str, object]: + actual_sha256 = sha256(path) + return { + "path": str(path), + "sha256": actual_sha256, + "expected_sha256": expected_sha256, + "match": actual_sha256 == expected_sha256, + "size": path.stat().st_size, + } + + +def main() -> None: + parser = argparse.ArgumentParser( + description=( + "Split Round 2 case-B exclusive assets into three semantically " + "connected motion layers plus static residual ownership." + ) + ) + parser.add_argument("--frozen-root", required=True, type=Path) + parser.add_argument("--exclusive-root", required=True, type=Path) + parser.add_argument("--output-root", required=True, type=Path) + parser.add_argument("--report", required=True, type=Path) + parser.add_argument("--proof", type=Path) + args = parser.parse_args() + + frozen_paths = { + name: args.frozen_root / f"{name}.png" + for name in FROZEN_INPUT_SHA256 + } + exclusive_paths = { + name: args.exclusive_root / f"{name}.png" + for name in EXCLUSIVE_INPUT_SHA256 + } + input_report = { + "frozen": { + name: input_record(path, FROZEN_INPUT_SHA256[name]) + for name, path in frozen_paths.items() + }, + "exclusive_v2": { + name: input_record(path, EXCLUSIVE_INPUT_SHA256[name]) + for name, path in exclusive_paths.items() + }, + } + frozen_inputs_match = all( + record["match"] + for group in input_report.values() + for record in group.values() + ) + if not frozen_inputs_match: + raise SystemExit("case-B frozen/exclusive input hash gate failed") + + background = load_rgba(frozen_paths["background-clean"]) + subject = load_rgba(frozen_paths["subject-cutout"]) + sources = { + name: load_rgba(path) for name, path in exclusive_paths.items() + } + if background.size != subject.size or any( + image.size != subject.size for image in sources.values() + ): + raise SystemExit("case-B semantic input dimensions do not match") + + leaf_top, top_residual, top_components = split_largest_component( + sources["leaf-top"] + ) + leaf_left, left_residual, left_components = split_largest_component( + sources["leaf-left"] + ) + foreground_plant, foreground_residual, foreground_components = ( + split_largest_component(sources["foreground"]) + ) + static_residual = merge_disjoint( + [top_residual, left_residual, foreground_residual] + ) + outputs = { + "stem-base": sources["stem-base"], + "static-residual": static_residual, + "leaf-right": sources["leaf-right"], + "leaf-top": leaf_top, + "leaf-left": leaf_left, + "foreground-plant": foreground_plant, + } + + args.output_root.mkdir(parents=True, exist_ok=True) + output_paths: dict[str, Path] = {} + for name in OUTPUT_LAYERS: + output_path = args.output_root / f"{name}.png" + outputs[name].save(output_path, optimize=True) + output_paths[name] = output_path + + # All gates below prove the serialized PNGs, not pre-save Pillow objects. + outputs = { + name: load_rgba(output_paths[name]) for name in OUTPUT_LAYERS + } + source_reconstruction = merge_disjoint( + [sources[name] for name in SOURCE_LAYERS] + ) + output_reconstruction = merge_disjoint( + [outputs[name] for name in OUTPUT_LAYERS] + ) + subject_support = binary_alpha(subject, 0) + normalized_subject = Image.new("RGBA", subject.size, (0, 0, 0, 0)) + normalized_subject = Image.composite( + subject, normalized_subject, subject_support + ) + source_to_subject_error = maximum_channel_error( + source_reconstruction, normalized_subject + ) + output_to_source_error = maximum_channel_error( + output_reconstruction, source_reconstruction + ) + output_to_subject_error = maximum_channel_error( + output_reconstruction, normalized_subject + ) + expected_zero_pose = Image.alpha_composite( + background, normalized_subject + ) + actual_zero_pose = Image.alpha_composite( + background, output_reconstruction + ) + zero_pose_error = maximum_channel_error( + actual_zero_pose, expected_zero_pose + ) + if args.proof: + args.proof.parent.mkdir(parents=True, exist_ok=True) + actual_zero_pose.convert("RGB").save(args.proof, optimize=True) + + threshold_reports: dict[str, object] = {} + threshold_gate_passed = True + for threshold in VALIDATION_THRESHOLDS: + subject_mask = binary_alpha(subject, threshold) + output_masks = { + name: binary_alpha(outputs[name], threshold) + for name in OUTPUT_LAYERS + } + output_union = Image.new("L", subject.size, 0) + for mask in output_masks.values(): + output_union = ImageChops.lighter(output_union, mask) + holes = nonzero_count( + ImageChops.subtract(subject_mask, output_union) + ) + extras = nonzero_count( + ImageChops.subtract(output_union, subject_mask) + ) + overlaps = { + f"{left}:{right}": nonzero_count( + ImageChops.multiply( + output_masks[left], output_masks[right] + ) + ) + for left, right in combinations(OUTPUT_LAYERS, 2) + } + passed = ( + holes == 0 + and extras == 0 + and all(count == 0 for count in overlaps.values()) + ) + threshold_gate_passed = threshold_gate_passed and passed + threshold_reports[str(threshold)] = { + "holes": holes, + "extras": extras, + "pairwise_overlap": overlaps, + "passed": passed, + } + + output_components = { + name: connected_components(outputs[name].getchannel("A")) + for name in OUTPUT_LAYERS + } + dynamic_semantic_connected = all( + len(output_components[name]) == 1 for name in DYNAMIC_LAYERS + ) + foreground_bbox = outputs["foreground-plant"].getchannel("A").getbbox() + foreground_is_right_plant = ( + foreground_bbox is not None + and foreground_bbox[2] == subject.width + and foreground_bbox[0] > subject.width // 2 + ) + + source_split_report = { + "leaf-top": { + "selection": "largest 8-connected alpha>0 component", + "components": public_component_stats(top_components), + "selected_component_index": 0, + "residual_component_count": max(0, len(top_components) - 1), + }, + "leaf-left": { + "selection": "largest 8-connected alpha>0 component", + "components": public_component_stats(left_components), + "selected_component_index": 0, + "residual_component_count": max(0, len(left_components) - 1), + }, + "foreground": { + "selection": ( + "largest 8-connected alpha>0 component; verified as the " + "right-side plant touching the right canvas edge" + ), + "components": public_component_stats(foreground_components), + "selected_component_index": 0, + "residual_component_count": max( + 0, len(foreground_components) - 1 + ), + }, + } + dynamic_geometry = { + name: { + "alpha_bbox": list( + outputs[name].getchannel("A").getbbox() or () + ), + "root_band_hint": root_band_report(outputs[name]), + "authored_pivot_hint": PIVOT_HINTS[name], + } + for name in DYNAMIC_LAYERS + } + report: dict[str, object] = { + "schema": "short-video-v2.phase7.case-b-semantic-motion-assets.v3", + "connectivity": CONNECTIVITY, + "split_threshold": "alpha > 0", + "inputs": input_report, + "source_layers": list(SOURCE_LAYERS), + "output_layers": list(OUTPUT_LAYERS), + "dynamic_layers": list(DYNAMIC_LAYERS), + "static_layers": [ + name for name in OUTPUT_LAYERS if name not in DYNAMIC_LAYERS + ], + "source_splits": source_split_report, + "outputs": { + name: { + "path": str(output_paths[name]), + "sha256": sha256(output_paths[name]), + "size": output_paths[name].stat().st_size, + "alpha_bbox": list( + outputs[name].getchannel("A").getbbox() or () + ), + "alpha_nonzero_pixels": nonzero_count( + outputs[name].getchannel("A") + ), + "connected_component_count": len(output_components[name]), + "components": public_component_stats( + output_components[name] + ), + } + for name in OUTPUT_LAYERS + }, + "dynamic_geometry": dynamic_geometry, + "validation_threshold_count": len(VALIDATION_THRESHOLDS), + "thresholds": threshold_reports, + "identity": { + "source_layers_to_normalized_subject_rgba_max_error": ( + source_to_subject_error + ), + "outputs_to_source_layers_rgba_max_error": output_to_source_error, + "outputs_to_normalized_subject_rgba_max_error": ( + output_to_subject_error + ), + "zero_pose_background_composite_rgba_max_error": zero_pose_error, + }, + "hard_gate": { + "frozen_inputs_match": frozen_inputs_match, + "foreground_largest_component_is_right_plant": ( + foreground_is_right_plant + ), + "all_dynamic_layers_semantically_connected": ( + dynamic_semantic_connected + ), + "multi_threshold_unique_ownership": threshold_gate_passed, + "source_layers_match_normalized_subject": ( + source_to_subject_error == 0 + ), + "output_rgba_identity": ( + output_to_source_error == 0 and output_to_subject_error == 0 + ), + "zero_pose_identity": zero_pose_error == 0, + "passed": ( + frozen_inputs_match + and foreground_is_right_plant + and dynamic_semantic_connected + and threshold_gate_passed + and source_to_subject_error == 0 + and output_to_source_error == 0 + and output_to_subject_error == 0 + and zero_pose_error == 0 + ), + }, + } + args.report.parent.mkdir(parents=True, exist_ok=True) + args.report.write_text( + json.dumps(report, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + if not report["hard_gate"]["passed"]: + raise SystemExit("case-B semantic motion asset gate failed") + + +if __name__ == "__main__": + main() diff --git "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/templates/asset_manifest_v1.json" "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/templates/asset_manifest_v1.json" new file mode 100644 index 0000000..73fcf6e --- /dev/null +++ "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/templates/asset_manifest_v1.json" @@ -0,0 +1,147 @@ +{ + "schema": "short-video-v2.phase7.asset-manifest.v1", + "status": "template", + "experiment": { + "case_id": "", + "source_kind": "", + "master_source": "", + "master_sha256": "<64-lowercase-hex>", + "motion_brief": "", + "revision_count": 0, + "revision_limit": 1 + }, + "isolation": { + "cache_root": "/Users/yuh/Library/Caches/text-video-plan07-hyperframes", + "evidence_root": "", + "forbid_symlink_escape": true, + "protected_roots": [ + "/Users/yuh/Library/Caches/text-video-plan04-feasibility/hyperframes" + ] + }, + "resource_policy": { + "new_download_cache_limit_bytes": 1073741824, + "requires_pause_above_limit": true, + "new_model_requires_pause": true, + "third_party_provider_requires_pause": true, + "cloud_api_or_fee_requires_pause": true + }, + "command_policy": { + "argv_type": "list[str]", + "shell": false, + "finite_timeout_required": true, + "runtime_network_forbidden": true + }, + "media_spec": { + "width": 1080, + "height": 1920, + "fps": 30, + "duration_sec_min": 4, + "duration_sec_max": 6, + "codec": "h264", + "pixel_format": "yuv420p", + "audio_streams": 0 + }, + "variants": [ + {"id": "S", "meaning": "same-source static"}, + {"id": "W", "meaning": "same-source whole-image motion"}, + {"id": "L", "meaning": "same-source layered micro-motion"} + ], + "motion_layers": [ + "camera", + "scene", + "subject", + "local_state" + ], + "required_asset_roles": { + "character_or_animal": [ + "master", + "background-clean", + "subject-cutout", + "body", + "head", + "eyelid-or-eyes-closed", + "foreground" + ], + "plant": [ + "master", + "background-clean", + "stem-or-base", + "leaf-01", + "leaf-02", + "foreground", + "light-or-particle" + ] + }, + "assets": [ + { + "role": "", + "path": "", + "source": "", + "prompt_or_operation": "", + "width": 1080, + "height": 1920, + "has_alpha": false, + "sha256": "<64-lowercase-hex>", + "purpose": "", + "version": 1 + } + ], + "static_proofs": { + "hero_recomposition": "", + "subject_peak": "", + "local_state_peak": "", + "alpha_on_light": "", + "alpha_on_dark": "", + "contact_sheet": "", + "passed": false + }, + "timeline_contract": { + "single_paused_timeline": true, + "parent_child_layers": true, + "stable_selectors": [ + "camera-rig", + "subject-root", + "head-or-leaf-pivot", + "eyelid-or-local-state", + "foreground", + "particle" + ], + "finite_repeats": true, + "seeded_or_explicit_particles": true, + "forbidden_runtime_sources": [ + "Date.now", + "performance.now", + "unseeded Math.random", + "runtime fetch", + "infinite repeat", + "timer", + "unregistered requestAnimationFrame" + ] + }, + "proof_poses": [ + "first_frame", + "pre_action_hold", + "subject_peak", + "local_state_peak", + "return_to_rest", + "final_minus_hold", + "final_frame" + ], + "visual_failure_checks": [ + "broken-neck-or-joint", + "floating-subject-part", + "original-part-ghost", + "double-eyes-or-face", + "background-hole-or-repeat", + "eyelid-or-state-drift", + "alpha-fringe-or-hard-edge", + "mechanical-loop-or-motion-sickness", + "subject-motion-not-perceptible", + "effect-used-to-hide-defect" + ], + "review_gate": { + "batch5_status": "awaiting_user_review", + "maximum_verdict_before_user_reply": "CONDITIONAL", + "batch6_forbidden_before_user_reply": true + } +} diff --git "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/templates/layered_motion_brief_v1.md" "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/templates/layered_motion_brief_v1.md" new file mode 100644 index 0000000..40a86e3 --- /dev/null +++ "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/templates/layered_motion_brief_v1.md" @@ -0,0 +1,72 @@ +# Layered Motion Brief v1 + +> 本模板是计划 07 实验制作说明,不是 Job Bundle Schema,不得复制到正式公共契约。 + +## 项目与目标 + +- case_id:`` +- source_image:`` +- 目标:把同一张主图制作为 4–6 秒、9:16、30 FPS、无声的 S/W/L 受控对照。 +- 成片感受:`<安静|温暖|克制|生动|...>` +- 身份锚点:`<脸/眼睛/毛发/服装/叶缘/果实等不得改变的内容>` +- 主图焦点:`` + +## 四层动作 + +- 镜头层:`` +- 场景层:`<前中后景/草叶/光粒/局部光线及相位>` +- 主体层:`` +- 局部状态层:`` + +## 层级、枢轴与遮挡 + +- 层级:`background-clean / camera-rig / subject-root / body|base / head-pivot|leaf-pivot / eyelid|state / foreground / particle` +- 枢轴:`<颈部、叶柄或身体重心的像素/百分比坐标>` +- 遮挡顺序:`<谁在谁之前>` +- 重叠余量:`<颈部/叶柄/边缘的像素量>` +- 空洞/残影控制:`` + +## 单一 seek-safe 时间线 + +- composition_id:`` +- timeline key:`window.__timelines[""]` +- 时间线:`0.0s 建立;... 主动作;... 状态变化;... 回稳;末尾 hold` +- 确定性:固定种子或显式常量表,有限 repeat,无运行时网络、时钟或计时器。 + +## 姿态与证明帧 + +1. `first_frame` +2. `pre_action_hold` +3. `subject_peak` +4. `local_state_peak` +5. `return_to_rest` +6. `final_minus_hold` +7. `final_frame` + +每一项填写秒数/帧号、真实动画选择器、期望姿态和 snapshot/focused-shot 路径。 + +## 视觉失败项 + +- 断颈、漂浮、关节错位、叶柄断裂。 +- 原头残影、双眼/双脸、背景空洞或重复纹理。 +- 眼睑/局部状态漂移、颜色不匹配、闪烁。 +- alpha 白边、毛发/叶缘硬切、前景遮挡眼睛或主体锚点。 +- 整图晃动、木偶循环、晕动,或动作小到 30 FPS/H.264 成片不可感知。 +- 以滤镜、锐化、强光、粒子、字幕或缩小画面掩盖问题。 + +## 验证链 + +- 静态 hero 叠合、峰值姿态、局部状态、浅/深底 alpha 边缘先通过。 +- `hyperframes lint` +- `hyperframes check --strict --snapshots` +- `hyperframes keyframes --selector --shot ` +- `hyperframes snapshot --at ` +- draft 视觉自审,最多一次有界动作参数修订。 +- high render,再做 ffprobe、完整解码、SHA-256、资源/磁盘/孤儿进程检查。 + +## 禁止 + +- 整图左右晃动、滤镜或粒子冒充主体动作。 +- 同一元素的同一 transform 属性被并发 tween 重复写入。 +- `repeat:-1`、`Date.now`、`performance.now`、未种子 `Math.random`、运行时 fetch、未注册 rAF、计时器。 +- 身份变化、遮罩白边、断裂、重影、末帧黑屏或复位。 diff --git "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/templates/scorecard_v1.json" "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/templates/scorecard_v1.json" new file mode 100644 index 0000000..0de2c4d --- /dev/null +++ "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/experiments/short_video_v2_phase7/templates/scorecard_v1.json" @@ -0,0 +1,67 @@ +{ + "schema": "short-video-v2.phase7.scorecard.v1", + "status": "template", + "scale": { + "min": 0, + "max": 3, + "meaning": { + "0": "hard failure or absent", + "1": "weak or distracting", + "2": "usable with bounded concerns", + "3": "clear, natural, and fit for the case" + } + }, + "metrics": [ + "subject_motion_perceptibility", + "motion_naturalness", + "identity_stability", + "seam_and_occlusion_quality", + "scene_depth_and_environment", + "camera_comfort", + "engineering_complexity", + "overall_usability" + ], + "hard_failures": [ + "only_whole_image_motion", + "broken_neck_or_joint", + "double_face_or_ghost", + "background_hole", + "eyelid_or_state_drift", + "alpha_fringe_or_hard_cut", + "motion_sickness", + "subject_motion_not_perceptible", + "identity_change", + "audio_or_text_bias_in_review" + ], + "cases": [ + { + "case_id": "case-a", + "required_variants": ["S", "W", "L"], + "scores": {}, + "hard_failures_observed": [], + "notes": "" + }, + { + "case_id": "case-b", + "required_variants": ["W", "L"], + "preferred_variants": ["S", "W", "L"], + "scores": {}, + "hard_failures_observed": [], + "notes": "" + } + ], + "blind_review_questions": [ + "Which version most looks like the picture itself is moving?", + "Are the local subject or leaf motions natural and clearly visible?", + "Is there unacceptable puppet feel, seam, flicker, or motion sickness?", + "Is this worth using for a small number of shots or most ordinary shots?", + "Which motion should be reduced, strengthened, or removed?" + ], + "review_policy": { + "private_mapping_required": true, + "tool_identity_hidden": true, + "codex_scores_sealed_before_user_review": true, + "user_reply_required_for_batch6": true, + "maximum_verdict_before_user_reply": "CONDITIONAL" + } +} diff --git "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/tests/README.md" "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/tests/README.md" index e68cf7d..9aac9af 100644 --- "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/tests/README.md" +++ "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/tests/README.md" @@ -61,9 +61,9 @@ cd 程序文件/引擎 核心当前只支持对一个 Job Bundle 单写;不要并发渲染同一任务目录。失败或取消应查阅 `output/render_report.json`,并保留旧的有效 WAV、镜头和 final。 -## 短视频 V2 计划 04 可行性实验 +## 短视频 V2 计划 04 可行性实验(历史、非第一版日常门) -隔离实验协议、评分模板与 FFmpeg 基线测试: +以下仅用于复核旧隔离实验协议、评分模板与 FFmpeg 基线,不由当前静态工作流自动运行: ```text 程序文件/runtime/bin/python3 -B -m unittest discover -s tests -p 'test_v2_phase4_feasibility.py' -v diff --git "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/tests/test_director_workflow_docs.py" "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/tests/test_director_workflow_docs.py" index 61ba2ba..715f10f 100644 --- "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/tests/test_director_workflow_docs.py" +++ "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/tests/test_director_workflow_docs.py" @@ -3,8 +3,10 @@ PACKAGE_ROOT = Path(__file__).resolve().parents[1] +REPOSITORY_ROOT = PACKAGE_ROOT.parent DOC_ROOT = PACKAGE_ROOT / "docs" / "short_video_v2" ACCEPTANCE_PATH = DOC_ROOT / "workflow_acceptance_v1.md" +SKILL_ROOT = REPOSITORY_ROOT / "skills" / "short-video-director" class DirectorWorkflowDocumentationTests(unittest.TestCase): @@ -34,7 +36,8 @@ def test_workflow_routes_to_existing_contract_and_cli(self): "python3 -m video_v2 render", "--shot", "用户终审", - "自然语义动态尚未实现", + "motion.preset: static", + "transition_out.type: cut", "轻量导演/导航层", "而非主链的技术依赖", ): @@ -49,6 +52,55 @@ def test_templates_are_human_aids_not_machine_contracts(self): self.assertIn("用户反馈", review) self.assertIn("最小返修", review) + def test_static_storyboard_is_the_active_product_profile(self): + director = (DOC_ROOT / "director_workflow_v1.md").read_text(encoding="utf-8") + job_bundle = (DOC_ROOT / "job_bundle_v1.md").read_text(encoding="utf-8") + brief = (DOC_ROOT / "templates" / "brief_v1.md").read_text(encoding="utf-8") + review = (DOC_ROOT / "templates" / "review_v1.md").read_text(encoding="utf-8") + skill = (SKILL_ROOT / "SKILL.md").read_text(encoding="utf-8") + gates = (SKILL_ROOT / "references" / "workflow-and-gates.md").read_text( + encoding="utf-8" + ) + + self.assertIn("第一版活跃范围", director) + self.assertIn("static + low + cut(0)", director) + self.assertIn('motion.preset: "static"', job_bundle) + self.assertIn('transition_out.type: "cut"', job_bundle) + self.assertIn("每镜一张静态图", brief) + self.assertIn("没有非预期推拉", review) + self.assertIn("sequence of distinct static storyboard images", skill) + self.assertIn("motion=static/low", gates) + self.assertNotIn("默认基础 FFmpeg 虚拟摄影机运动", director) + self.assertNotIn("basic FFmpeg camera motion", gates) + + def test_supplied_footage_uses_a_codex_owned_route(self): + skill = (SKILL_ROOT / "SKILL.md").read_text(encoding="utf-8") + workflow_path = SKILL_ROOT / "references" / "video-material-workflow.md" + self.assertEqual((SKILL_ROOT / "VERSION").read_text(encoding="utf-8").strip(), "1.1.0") + self.assertTrue(workflow_path.is_file()) + workflow = workflow_path.read_text(encoding="utf-8") + + for required in ( + "Codex footage route", + "general-video", + "media-use", + "HyperFrames", + "package A and package B do not process this route", + ): + with self.subTest(required=required): + self.assertIn(required, skill) + + for required in ( + "Preflight suitability review", + "Post-build review", + "use`, `partial use`, or `omit", + "Package A and package B are outside this route", + "voice loudness remains stable", + ): + with self.subTest(required=required): + self.assertIn(required, workflow) + self.assertNotRegex(workflow, r"/Users/[^/]+/") + def test_open_source_review_borrows_principles_without_runtime(self): evidence = ACCEPTANCE_PATH.read_text(encoding="utf-8") for source in ("OpenMontage", "PixVerse Skills", "ffmpeg-ai"): diff --git "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/tests/test_v2_phase4_feasibility.py" "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/tests/test_v2_phase4_feasibility.py" index f1f6847..61da26a 100644 --- "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/tests/test_v2_phase4_feasibility.py" +++ "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/tests/test_v2_phase4_feasibility.py" @@ -41,7 +41,6 @@ def test_required_protocol_files_exist(self): "normalize_clip.py", "scorecard.py", "providers/ffmpeg_baseline.py", - "providers/mflux_adapter.py", "providers/depthflow_adapter.py", "providers/hyperframes_adapter.py", "templates/image_prompt_template.md", @@ -180,55 +179,6 @@ def run(self, argv, **kwargs): self.assertIs(runner.kwargs["check"], True) self.assertEqual(runner.kwargs["timeout"], 9.0) - def test_mflux_adapter_is_fixed_to_one_model_and_isolated_root(self): - adapter = load_module( - "phase4_mflux_adapter", - EXPERIMENT_DIR / "providers" / "mflux_adapter.py", - ) - - class CapturingRunner: - def run(self, argv, **kwargs): - self.argv = argv - self.kwargs = kwargs - return object() - - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) / "mflux" - executable = root / "venv" / "bin" / "mflux-generate-flux2" - executable.parent.mkdir(parents=True) - executable.write_bytes(b"fixture") - runner = CapturingRunner() - adapter.run_mflux_generate( - "rainy ancient gate without text", - root / "output" / "architecture.png", - runner=runner, - root=root, - executable=executable, - seed=404001, - width=448, - height=768, - timeout_sec=30.0, - ) - self.assertIsInstance(runner.argv, list) - self.assertEqual( - runner.argv[runner.argv.index("--model") + 1], - "mlx-community/flux2-klein-4b-4bit", - ) - self.assertEqual(runner.argv[runner.argv.index("--guidance") + 1], "1.0") - self.assertEqual(runner.kwargs["timeout"], 30.0) - self.assertEqual(runner.kwargs["env"]["HF_HOME"], str(root.resolve() / "hf")) - self.assertEqual(runner.kwargs["env"]["HF_HUB_DISABLE_XET"], "1") - with self.assertRaises(ValueError): - adapter.build_mflux_generate_argv( - "escape", - Path(temporary) / "outside.png", - executable=executable, - root=root, - seed=1, - width=448, - height=768, - ) - def test_depthflow_adapter_uses_isolated_cache_and_fixed_visual_spec(self): adapter = load_module( "phase4_depthflow_adapter", diff --git "a/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/tests/test_v2_phase7_hyperframes_micro_motion.py" "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/tests/test_v2_phase7_hyperframes_micro_motion.py" new file mode 100644 index 0000000..6d6b924 --- /dev/null +++ "b/\343\200\220\345\214\205A\343\200\221\350\247\206\351\242\221\345\274\225\346\223\216\345\214\205/tests/test_v2_phase7_hyperframes_micro_motion.py" @@ -0,0 +1,1041 @@ +from __future__ import annotations + +import json +import hashlib +import subprocess +import unittest +from pathlib import Path + +from PIL import Image + + +PACKAGE_ROOT = Path(__file__).resolve().parents[1] +PHASE7_ROOT = PACKAGE_ROOT / "experiments" / "short_video_v2_phase7" +TEMPLATES = PHASE7_ROOT / "templates" +README = PHASE7_ROOT / "README.md" +BRIEF = TEMPLATES / "layered_motion_brief_v1.md" +ASSET_MANIFEST = TEMPLATES / "asset_manifest_v1.json" +SCORECARD = TEMPLATES / "scorecard_v1.json" +CASE_A_ROOT = ( + PACKAGE_ROOT + / "成片" + / "短视频V2样片" + / "phase7-hyperframes-micro-motion" + / "case-a-controlled" +) +CASE_A_MANIFEST = CASE_A_ROOT / "asset-manifest.json" +CASE_B_ROOT = ( + PACKAGE_ROOT + / "成片" + / "短视频V2样片" + / "phase7-hyperframes-micro-motion" + / "case-b-project-migration" +) +CASE_B_MANIFEST = CASE_B_ROOT / "asset-manifest.json" +REVIEW_ROOT = CASE_A_ROOT.parent / "review-batch5" +REVIEW_MANIFEST = REVIEW_ROOT / "review-manifest.json" +FINAL_MANIFEST = CASE_A_ROOT.parent / "final-manifest.json" +ROUND2_MANIFEST = CASE_A_ROOT.parent / "round2-reopen" / "round2-manifest.json" +ROUND2_ROOT = ROUND2_MANIFEST.parent +ROUND2_REVIEW_MANIFEST = ROUND2_ROOT / "review-batch7" / "review-manifest.json" +ROUND2_REVISION_ROOT = ROUND2_ROOT / "revision-batch8" +ROUND2_RECIPE_MANIFEST = ROUND2_REVISION_ROOT / "recipe-manifest.json" +ROUND2_REVISION_REVIEW_MANIFEST = ( + ROUND2_ROOT / "review-batch8" / "review-manifest.json" +) +ROUND2_BATCH9_REVISION_ROOT = ROUND2_ROOT / "revision-batch9" +ROUND2_BATCH9_RECIPE_MANIFEST = ROUND2_BATCH9_REVISION_ROOT / "recipe-manifest.json" +ROUND2_BATCH9_HASH_LEDGER = ROUND2_BATCH9_REVISION_ROOT / "artifact-sha256.txt" +ROUND2_BATCH9_REVIEW_MANIFEST = ROUND2_ROOT / "review-batch9" / "review-manifest.json" +ROUND2_BATCH9_COLD_REPORT = ROUND2_ROOT / "diagnostics" / "batch9-cold-replay-report.json" +ROUND2_FINAL_CAUSAL_ROOT = ROUND2_ROOT / "final-causal-exception" +ROUND2_FINAL_CAUSAL_REPORT = ROUND2_FINAL_CAUSAL_ROOT / "final-causal-report.json" +ROUND2_FINAL_CAUSAL_LEDGER = ROUND2_FINAL_CAUSAL_ROOT / "artifact-sha256.txt" +MOTION_FEASIBILITY = PACKAGE_ROOT / "docs" / "short_video_v2" / "motion_feasibility_v1.md" +MOTION_PLAYBOOK = PACKAGE_ROOT / "docs" / "short_video_v2" / "motion_experiment_playbook_v1.md" +OVERALL_PLAN = PACKAGE_ROOT.parent / "短视频V2规划文档" / "短视频V2总体目标与阶段规划.md" +EXECUTION_RECORD = ( + PACKAGE_ROOT.parent + / "短视频V2规划文档" + / "执行记录" + / "07-HyperFrames拆层微动态专项验证执行记录.md" +) +PLAN07_CACHE_ROOT = Path("/Users/yuh/Library/Caches/text-video-plan07-hyperframes") +PLAN04_FROZEN_ROOT = Path( + "/Users/yuh/Library/Caches/text-video-plan04-feasibility/hyperframes" +) + + +class Phase7HyperFramesMicroMotionContractTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.readme = README.read_text(encoding="utf-8") + cls.brief = BRIEF.read_text(encoding="utf-8") + cls.asset_manifest = json.loads(ASSET_MANIFEST.read_text(encoding="utf-8")) + cls.scorecard = json.loads(SCORECARD.read_text(encoding="utf-8")) + + def test_required_contract_files_exist(self) -> None: + for path in (README, BRIEF, ASSET_MANIFEST, SCORECARD): + self.assertTrue(path.is_file(), path) + self.assertFalse(path.is_symlink(), path) + + def test_cases_variants_layers_and_static_gate_are_explicit(self) -> None: + for token in ( + "案例 A", + "案例 B", + "S:", + "W:", + "L:", + "镜头层", + "场景层", + "主体层", + "局部状态层", + "静态不成立禁止进入动画", + ): + self.assertIn(token, self.readme) + self.assertEqual( + [item["id"] for item in self.asset_manifest["variants"]], + ["S", "W", "L"], + ) + self.assertEqual( + self.asset_manifest["motion_layers"], + ["camera", "scene", "subject", "local_state"], + ) + + def test_asset_roles_and_traceability_are_generic_and_complete(self) -> None: + roles = self.asset_manifest["required_asset_roles"] + self.assertEqual( + roles["character_or_animal"], + [ + "master", + "background-clean", + "subject-cutout", + "body", + "head", + "eyelid-or-eyes-closed", + "foreground", + ], + ) + self.assertIn("leaf-01", roles["plant"]) + asset = self.asset_manifest["assets"][0] + for field in ( + "role", + "path", + "source", + "prompt_or_operation", + "width", + "height", + "has_alpha", + "sha256", + "purpose", + "version", + ): + self.assertIn(field, asset) + for forbidden_fixture in ("小羊", "sheep", "once-only-sha"): + self.assertNotIn( + forbidden_fixture, + json.dumps(self.asset_manifest, ensure_ascii=False).lower(), + ) + + def test_resource_path_and_command_policy_are_bounded(self) -> None: + isolation = self.asset_manifest["isolation"] + self.assertEqual(Path(isolation["cache_root"]), PLAN07_CACHE_ROOT) + self.assertNotEqual(PLAN07_CACHE_ROOT, PLAN04_FROZEN_ROOT) + self.assertIn(str(PLAN04_FROZEN_ROOT), isolation["protected_roots"]) + self.assertTrue(isolation["forbid_symlink_escape"]) + resource = self.asset_manifest["resource_policy"] + self.assertEqual(resource["new_download_cache_limit_bytes"], 1024**3) + self.assertTrue(resource["requires_pause_above_limit"]) + self.assertTrue(resource["new_model_requires_pause"]) + self.assertTrue(resource["third_party_provider_requires_pause"]) + self.assertTrue(resource["cloud_api_or_fee_requires_pause"]) + command = self.asset_manifest["command_policy"] + self.assertEqual(command["argv_type"], "list[str]") + self.assertIs(command["shell"], False) + self.assertTrue(command["finite_timeout_required"]) + self.assertTrue(command["runtime_network_forbidden"]) + + def test_media_timeline_and_proof_pose_contract(self) -> None: + media = self.asset_manifest["media_spec"] + self.assertEqual( + (media["width"], media["height"], media["fps"]), + (1080, 1920, 30), + ) + self.assertEqual((media["duration_sec_min"], media["duration_sec_max"]), (4, 6)) + self.assertEqual((media["codec"], media["pixel_format"]), ("h264", "yuv420p")) + self.assertEqual(media["audio_streams"], 0) + timeline = self.asset_manifest["timeline_contract"] + self.assertTrue(timeline["single_paused_timeline"]) + self.assertTrue(timeline["parent_child_layers"]) + self.assertTrue(timeline["finite_repeats"]) + self.assertTrue(timeline["seeded_or_explicit_particles"]) + self.assertEqual( + self.asset_manifest["proof_poses"], + [ + "first_frame", + "pre_action_hold", + "subject_peak", + "local_state_peak", + "return_to_rest", + "final_minus_hold", + "final_frame", + ], + ) + + def test_brief_forbids_fake_subject_motion_and_nondeterminism(self) -> None: + for token in ( + "整图左右晃动", + "滤镜或粒子冒充主体动作", + "repeat:-1", + "Date.now", + "performance.now", + "Math.random", + "运行时 fetch", + "末帧黑屏或复位", + ): + self.assertIn(token, self.brief) + + def test_scorecard_and_user_gate_cannot_be_auto_passed(self) -> None: + self.assertEqual( + set(self.scorecard["metrics"]), + { + "subject_motion_perceptibility", + "motion_naturalness", + "identity_stability", + "seam_and_occlusion_quality", + "scene_depth_and_environment", + "camera_comfort", + "engineering_complexity", + "overall_usability", + }, + ) + self.assertEqual(len(self.scorecard["blind_review_questions"]), 5) + policy = self.scorecard["review_policy"] + self.assertTrue(policy["private_mapping_required"]) + self.assertTrue(policy["tool_identity_hidden"]) + self.assertTrue(policy["codex_scores_sealed_before_user_review"]) + self.assertTrue(policy["user_reply_required_for_batch6"]) + self.assertEqual(policy["maximum_verdict_before_user_reply"], "CONDITIONAL") + gate = self.asset_manifest["review_gate"] + self.assertEqual(gate["batch5_status"], "awaiting_user_review") + self.assertTrue(gate["batch6_forbidden_before_user_reply"]) + + def test_case_a_frozen_layers_are_traceable_and_pixel_valid(self) -> None: + manifest = json.loads(CASE_A_MANIFEST.read_text(encoding="utf-8")) + self.assertIn(manifest["status"], {"frozen_batch_2", "frozen_batch_3"}) + self.assertEqual(manifest["case_id"], "case-a") + self.assertEqual(manifest["canvas"], {"width": 1080, "height": 1920}) + self.assertEqual(manifest["generation"]["bounded_asset_revisions"], 1) + + roles = {asset["role"] for asset in manifest["assets"]} + self.assertTrue( + { + "master", + "background-clean", + "subject-cutout", + "body", + "head", + "neck-cover", + "eyelid-or-eyes-closed", + "foreground-left", + "foreground-right", + }.issubset(roles) + ) + + for asset in (*manifest["source_assets"], *manifest["assets"]): + path = CASE_A_ROOT / asset["path"] + self.assertTrue(path.is_file(), path) + self.assertFalse(path.is_symlink(), path) + digest = hashlib.sha256(path.read_bytes()).hexdigest() + self.assertEqual(digest, asset["sha256"], path) + self.assertRegex(asset["sha256"], r"^[0-9a-f]{64}$") + with Image.open(path) as image: + self.assertEqual(image.size, (asset["width"], asset["height"])) + alpha_range = ( + image.getchannel("A").getextrema() + if "A" in image.getbands() + else (255, 255) + ) + self.assertEqual(alpha_range[0] < 255, asset["has_alpha"]) + + for proof in manifest["static_proofs"].values(): + if not isinstance(proof, dict) or "path" not in proof: + continue + path = CASE_A_ROOT / proof["path"] + self.assertTrue(path.is_file(), path) + self.assertEqual(hashlib.sha256(path.read_bytes()).hexdigest(), proof["sha256"]) + self.assertTrue(manifest["static_proofs"]["passed"]) + self.assertFalse(manifest["visual_review"]["hard_alpha_fringe_after_revision"]) + + if manifest["status"] == "frozen_batch_3": + self.assertEqual({item["variant"] for item in manifest["videos"]}, {"S", "W", "L"}) + for video in manifest["videos"]: + path = CASE_A_ROOT / video["path"] + self.assertTrue(path.is_file(), path) + self.assertEqual(hashlib.sha256(path.read_bytes()).hexdigest(), video["sha256"]) + self.assertEqual((video["width"], video["height"], video["fps"]), (1080, 1920, 30)) + self.assertEqual((video["codec"], video["pixel_format"]), ("h264", "yuv420p")) + self.assertEqual(video["audio_streams"], 0) + self.assertTrue(video["full_decode_ok"]) + + def test_case_b_frozen_migration_is_traceable_and_pixel_valid(self) -> None: + manifest = json.loads(CASE_B_MANIFEST.read_text(encoding="utf-8")) + self.assertEqual(manifest["status"], "frozen_batch_4") + self.assertEqual(manifest["case_id"], "case-b") + self.assertEqual(manifest["source_kind"], "current-project-migration") + self.assertEqual(manifest["canvas"], {"width": 1080, "height": 1920}) + self.assertEqual(manifest["generation"]["bounded_asset_revisions"], 1) + self.assertEqual(manifest["hyperframes"]["action_parameter_revisions"], 0) + + roles = {asset["role"] for asset in manifest["assets"]} + self.assertTrue( + { + "master", + "background-clean", + "subject-cutout", + "stem-base", + "leaf-top", + "leaf-left", + "leaf-right", + "foreground", + "seam-covers", + }.issubset(roles) + ) + + for asset in (*manifest["source_assets"], *manifest["assets"]): + path = CASE_B_ROOT / asset["path"] + self.assertTrue(path.is_file(), path) + self.assertFalse(path.is_symlink(), path) + digest = hashlib.sha256(path.read_bytes()).hexdigest() + self.assertEqual(digest, asset["sha256"], path) + self.assertRegex(asset["sha256"], r"^[0-9a-f]{64}$") + with Image.open(path) as image: + self.assertEqual(image.size, (asset["width"], asset["height"])) + alpha_range = ( + image.getchannel("A").getextrema() + if "A" in image.getbands() + else (255, 255) + ) + self.assertEqual(alpha_range[0] < 255, asset["has_alpha"]) + + for proof in manifest["static_proofs"].values(): + if not isinstance(proof, dict) or "path" not in proof: + continue + path = CASE_B_ROOT / proof["path"] + self.assertTrue(path.is_file(), path) + self.assertEqual(hashlib.sha256(path.read_bytes()).hexdigest(), proof["sha256"]) + + self.assertTrue(manifest["static_proofs"]["passed"]) + self.assertFalse(manifest["visual_review"]["hard_partition_seam_after_revision"]) + self.assertEqual({item["variant"] for item in manifest["videos"]}, {"S", "W", "L"}) + for video in manifest["videos"]: + path = CASE_B_ROOT / video["path"] + self.assertTrue(path.is_file(), path) + self.assertEqual(hashlib.sha256(path.read_bytes()).hexdigest(), video["sha256"]) + self.assertEqual((video["width"], video["height"], video["fps"]), (1080, 1920, 30)) + self.assertEqual(video["frames"], 150) + self.assertEqual((video["codec"], video["pixel_format"]), ("h264", "yuv420p")) + self.assertEqual(video["audio_streams"], 0) + self.assertTrue(video["full_decode_ok"]) + + def test_batch5_anonymous_review_set_is_frozen_and_sealed(self) -> None: + manifest = json.loads(REVIEW_MANIFEST.read_text(encoding="utf-8")) + self.assertEqual(manifest["status"], "awaiting_user_review") + self.assertEqual(manifest["mapping"], "private/mapping.json") + self.assertEqual(manifest["codex_scores"], "private/codex-scores.json") + self.assertTrue(manifest["review_policy"]["tool_identity_hidden"]) + self.assertTrue(manifest["review_policy"]["quality_hint_hidden"]) + self.assertTrue(manifest["review_policy"]["codex_scores_sealed_before_user_review"]) + self.assertTrue(manifest["review_policy"]["user_reply_required_for_batch6"]) + self.assertEqual( + manifest["review_policy"]["maximum_verdict_before_user_reply"], + "CONDITIONAL", + ) + + public_videos = [] + for item in manifest["public_files"]: + path = REVIEW_ROOT / item["path"] + self.assertTrue(path.is_file(), path) + self.assertFalse(path.is_symlink(), path) + self.assertEqual(hashlib.sha256(path.read_bytes()).hexdigest(), item["sha256"]) + self.assertNotRegex(path.name.lower(), r"(^|[-_])(s|w|l)([-_.]|$)") + if path.suffix == ".mp4": + public_videos.append(path) + + self.assertEqual(len(public_videos), 6) + self.assertEqual( + {path.parent.name for path in public_videos}, + {"case-01", "case-02"}, + ) + self.assertTrue((REVIEW_ROOT / "public/review-guide.md").is_file()) + self.assertTrue((REVIEW_ROOT / "public/technical-ledger.md").is_file()) + + for item in manifest["private_artifacts"]: + path = REVIEW_ROOT / item["path"] + self.assertTrue(path.is_file(), path) + self.assertEqual(hashlib.sha256(path.read_bytes()).hexdigest(), item["sha256"]) + + mapping = json.loads((REVIEW_ROOT / manifest["mapping"]).read_text(encoding="utf-8")) + scores = json.loads((REVIEW_ROOT / manifest["codex_scores"]).read_text(encoding="utf-8")) + self.assertTrue(mapping["do_not_disclose_before_batch6"]) + self.assertEqual(mapping["status"], "sealed_before_user_review") + self.assertEqual(scores["status"], "sealed_before_user_review") + self.assertTrue(scores["policy"]["does_not_replace_user_judgment"]) + + def test_batch6_final_rejection_is_traceable_without_media_rewrite(self) -> None: + manifest = json.loads(FINAL_MANIFEST.read_text(encoding="utf-8")) + self.assertEqual(manifest["status"], "complete") + self.assertEqual(manifest["director_mode"], "Resume") + self.assertEqual(manifest["plan_grade"], "FAIL") + self.assertEqual(manifest["classification"], "rejected") + self.assertFalse(manifest["revision"]["performed"]) + self.assertEqual(manifest["revision"]["bounded_revision_budget_used"], 0) + self.assertFalse(manifest["revision"]["media_changed_after_user_review"]) + self.assertFalse(manifest["revision"]["rerendered_after_user_review"]) + self.assertTrue( + manifest["codex_score_reconciliation"]["user_judgment_overrides_automatic_score"] + ) + self.assertEqual( + manifest["preserved_capabilities"]["hyperframes_information_design"], + "manual_only", + ) + self.assertFalse( + manifest["preserved_capabilities"]["formal_micro_motion_provider_created"] + ) + self.assertEqual( + manifest["failure_classification"]["visual_quality"], + "failed_user_gate", + ) + + evidence_root = FINAL_MANIFEST.parent + for case in manifest["final_media"].values(): + if not isinstance(case, dict) or not {"S", "W", "L"}.issubset(case): + continue + for variant in ("S", "W", "L"): + item = case[variant] + path = evidence_root / item["path"] + self.assertTrue(path.is_file(), path) + self.assertEqual(hashlib.sha256(path.read_bytes()).hexdigest(), item["sha256"]) + + for path in (MOTION_FEASIBILITY, OVERALL_PLAN, README): + text = path.read_text(encoding="utf-8") + self.assertIn("rejected", text) + self.assertIn("FAIL", MOTION_FEASIBILITY.read_text(encoding="utf-8")) + self.assertIn("FAIL / rejected", OVERALL_PLAN.read_text(encoding="utf-8")) + self.assertIn("FAIL", README.read_text(encoding="utf-8")) + + def test_round2_reopen_manifest_is_additive_and_records_final_user_gate(self) -> None: + self.assertEqual( + hashlib.sha256(FINAL_MANIFEST.read_bytes()).hexdigest(), + "d63a67e9ab0d945f46935de83d6103d52f2735fd63384f7f8931081443cd342b", + ) + self.assertTrue(ROUND2_MANIFEST.is_file(), ROUND2_MANIFEST) + self.assertNotEqual(ROUND2_MANIFEST, FINAL_MANIFEST) + + manifest = json.loads(ROUND2_MANIFEST.read_text(encoding="utf-8")) + self.assertEqual( + manifest["schema"], "short-video-v2.phase7.round2-reopen-manifest.v1" + ) + self.assertEqual(manifest["round"], 2) + self.assertEqual(manifest["status"], "complete") + self.assertEqual(manifest["plan_grade"], "CONDITIONAL") + self.assertEqual(manifest["classification"], "manual_only") + self.assertFalse(manifest["supersedes_round1"]) + self.assertEqual(manifest["parent_round1_manifest"]["path"], "../final-manifest.json") + self.assertEqual( + manifest["parent_round1_manifest"]["sha256"], + "d63a67e9ab0d945f46935de83d6103d52f2735fd63384f7f8931081443cd342b", + ) + self.assertEqual(manifest["round1_verdict"]["plan_grade"], "FAIL") + self.assertEqual(manifest["round1_verdict"]["classification"], "rejected") + self.assertEqual( + manifest["reopen_trigger"]["invalidated_stop_reason"], + "no_actionable_bounded_revision_target", + ) + self.assertTrue(manifest["reopen_trigger"]["does_not_retract_round1_evidence"]) + self.assertEqual( + manifest["review"]["maximum_verdict_before_user_reply"], "CONDITIONAL" + ) + self.assertEqual(manifest["integration_decision"], "not_reopened_for_production") + final_decision = manifest["final_decision"] + self.assertEqual( + final_decision["accepted_narrow_recipe"], + "controlled_animal_blink_and_single_ear_v1", + ) + self.assertEqual( + final_decision["rejected_recipe"], + "plant_unique_ownership_multi_leaf_sway_v2", + ) + self.assertEqual( + final_decision["rejected_reason"], + "user_rejected_due_to_ghosting", + ) + self.assertEqual( + final_decision["ordinary_natural_subject_production_route"], + "closed", + ) + self.assertTrue( + final_decision["user_visual_gate_overrides_automatic_visual_gate"] + ) + self.assertEqual( + manifest["user_review_batch8"]["case-a"]["outcome"], + "user_accepted", + ) + self.assertEqual( + manifest["user_review_batch8"]["case-b"]["outcome"], + "revision_requested", + ) + self.assertEqual( + set(manifest["user_review_batch8"]["case-b"]["reasons"]), + { + "motion_amplitude_too_small_to_confirm", + "at_least_two_leaves_should_move_concurrently", + }, + ) + review_state = manifest["current_review"] + self.assertEqual(review_state["batch"], "review-batch9") + self.assertEqual(review_state["status"], "closed") + self.assertTrue(review_state["no_longer_waiting"]) + self.assertFalse( + review_state["user_reply_required_before_case_b_acceptance"] + ) + self.assertTrue( + review_state["production_or_majority_shot_claim_forbidden"] + ) + batch9_review = manifest["user_review_batch9"] + self.assertTrue(batch9_review["received"]) + self.assertEqual( + batch9_review["feedback"], + { + "motion_visibility": "动作明显。", + "wind_coherence": "像一阵风。", + "visual_defect": "明显重影。", + }, + ) + self.assertEqual(batch9_review["motion_visibility"], "PASS") + self.assertEqual(batch9_review["wind_coherence"], "PASS") + self.assertEqual(batch9_review["artifact_gate"], "FAIL") + self.assertEqual( + batch9_review["outcome"], "user_rejected_due_to_ghosting" + ) + self.assertEqual( + batch9_review["automatic_visual_gate_assessment"], "false_negative" + ) + self.assertTrue(batch9_review["user_visual_gate_has_priority"]) + forensics = manifest["post_review_forensics"] + self.assertEqual(forensics["status"], "structural_ghosting_reproduced") + self.assertEqual( + forensics["visible_frame_window_zero_based"], + {"broad": [38, 90], "strongest": [50, 66]}, + ) + self.assertIn("crossfade_between_states", forensics["excluded_primary_causes"]) + self.assertIn( + "semantic_deformation_purity_inside_each_connected_dynamic_layer", + forensics["automatic_gate_gaps"], + ) + self.assertIn("coarse semantic clusters", forensics["important_contributing_mechanism"]) + self.assertEqual( + forensics["causal_boundary_after_final_exception"], + "important contributing mechanism, not established as the unique or complete root cause", + ) + self.assertNotIn("primary_cause", forensics) + self.assertIn("77 of 149", forensics["secondary_cause"]) + breadth = manifest["feasibility_breadth_gate"] + self.assertEqual(breadth["current_status"], "not_run") + self.assertEqual( + breadth["final_disposition"], + "cancelled_due_to_failed_prerequisite", + ) + self.assertEqual( + breadth["failed_prerequisite"], "case_b_user_visual_gate" + ) + + for candidate in manifest["candidates"].values(): + path = ROUND2_ROOT / candidate["path"] + self.assertTrue(path.is_file(), path) + self.assertFalse(path.is_symlink(), path) + self.assertEqual(hashlib.sha256(path.read_bytes()).hexdigest(), candidate["sha256"]) + media = candidate["media"] + self.assertEqual((media["width"], media["height"], media["fps"]), (1080, 1920, 30)) + self.assertEqual(media["frames"], 150) + self.assertEqual((media["codec"], media["pixel_format"]), ("h264", "yuv420p")) + self.assertEqual(media["audio_streams"], 0) + self.assertTrue(media["full_decode_ok"]) + + review = json.loads(ROUND2_REVIEW_MANIFEST.read_text(encoding="utf-8")) + self.assertEqual(review["status"], "awaiting_user_review") + self.assertTrue(review["review_policy"]["only_affected_round2_candidates_are_shown"]) + self.assertTrue(review["review_policy"]["local_slow_views_are_companions_not_candidates"]) + self.assertTrue(review["review_policy"]["user_reply_required_before_final_round2_verdict"]) + self.assertEqual( + review["review_policy"]["maximum_verdict_before_user_reply"], + "CONDITIONAL", + ) + review_root = ROUND2_REVIEW_MANIFEST.parent + for item in (*review["public_files"], *review["private_artifacts"]): + path = review_root / item["path"] + self.assertTrue(path.is_file(), path) + self.assertFalse(path.is_symlink(), path) + self.assertEqual(hashlib.sha256(path.read_bytes()).hexdigest(), item["sha256"]) + + def test_round2_revision_is_replay_verified_but_not_promoted(self) -> None: + self.assertTrue(MOTION_PLAYBOOK.is_file(), MOTION_PLAYBOOK) + playbook = MOTION_PLAYBOOK.read_text(encoding="utf-8") + for token in ( + "production_recipe", + "3 个真实视频 × 每个 4 类镜头 = 12 镜", + "至少 9/12", + "45 分钟即停止", + "semantic_motion_required", + ): + self.assertIn(token, playbook) + + recipe = json.loads(ROUND2_RECIPE_MANIFEST.read_text(encoding="utf-8")) + self.assertEqual( + recipe["status"], "replay_verified_awaiting_revision_review" + ) + self.assertFalse(recipe["supersedes_round1"]) + self.assertTrue(recipe["project"]["self_contained"]) + self.assertTrue(recipe["project"]["strict_check"]["ok"]) + self.assertTrue( + recipe["recipes"]["controlled_animal_blink_and_single_ear_v1"] + ["seek_repeat"]["same_time_png_sha256_match"] + ) + plant = recipe["recipes"]["plant_unique_ownership_leaf_sway_v1"] + self.assertEqual(plant["hard_gate"]["alpha_thresholds_checked"], 256) + self.assertEqual(plant["hard_gate"]["holes"], 0) + self.assertEqual(plant["hard_gate"]["extras"], 0) + self.assertEqual(plant["hard_gate"]["pairwise_overlap"], 0) + self.assertTrue(recipe["promotion_gate"]["user_revision_review_required"]) + self.assertTrue( + recipe["promotion_gate"]["breadth_gate_required_for_production_recipe"] + ) + + for item in recipe["recipes"].values(): + path = ROUND2_REVISION_ROOT / item["final_media"] + self.assertTrue(path.is_file(), path) + self.assertFalse(path.is_symlink(), path) + + review = json.loads( + ROUND2_REVISION_REVIEW_MANIFEST.read_text(encoding="utf-8") + ) + self.assertEqual(review["status"], "awaiting_revision_review") + self.assertTrue( + review["review_policy"]["user_reply_required_before_recipe_promotion"] + ) + self.assertEqual( + review["review_policy"]["maximum_claim_before_user_reply"], + "replay_verified_technical_candidate", + ) + review_root = ROUND2_REVISION_REVIEW_MANIFEST.parent + for item in (*review["public_files"], *review["private_artifacts"]): + path = review_root / item["path"] + self.assertTrue(path.is_file(), path) + self.assertFalse(path.is_symlink(), path) + self.assertEqual( + hashlib.sha256(path.read_bytes()).hexdigest(), item["sha256"] + ) + + def test_batch9_is_case_b_only_replay_verified_and_not_promoted(self) -> None: + frozen_hashes = { + FINAL_MANIFEST: "d63a67e9ab0d945f46935de83d6103d52f2735fd63384f7f8931081443cd342b", + CASE_A_MANIFEST: "665ba91ba51f916495522619fad20fbd91471f14206a721a83b49068f0ed2883", + ROUND2_RECIPE_MANIFEST: "4174b4eb1e4a419690a8f1dc16d7651c46041cc4f6cce087eb63c7330e018000", + ROUND2_REVISION_ROOT / "artifact-sha256.txt": "c5891d556e1f1c3ab4f57c4aeae5ecdc0b5c244e94aff45d301060ad8d1df37d", + ROUND2_REVISION_REVIEW_MANIFEST: "6752d439ce272bb82ff98160fa74f2b149ebf6fce74d0e0262024af1123763ff", + ROUND2_REVISION_ROOT / "videos/case-a-revision-high.mp4": "d301e9818d62ffc18b95c4b8ecb1e3ec789a7f394ac87b3671e0955804cea6ba", + ROUND2_REVISION_ROOT / "videos/case-b-revision-high.mp4": "2958770ddfd5ad9940903f07bd33ce1ed47da4b77c9b20cd938f4ee7de02ed02", + ROUND2_BATCH9_RECIPE_MANIFEST: "7a45795d1d43739166d45012489a89a06404260f41e4719ba3017c1862dc1904", + ROUND2_BATCH9_HASH_LEDGER: "1791fdd6fc5d14b76e47d73194dad707d4350dc8990902d5e94c47919214131f", + ROUND2_BATCH9_REVIEW_MANIFEST: "e65a6be5055b30f8986c3dad72d22402dd96344e8a98abbc64582dfdef75564e", + ROUND2_BATCH9_COLD_REPORT: "c0d654e8a966bc8b69aaa11cab812d8ebb78f82b3a1b2e691bfc4469a1ed14b2", + ROUND2_BATCH9_REVISION_ROOT / "videos/case-b-plant-multi-leaf-high.mp4": "94a59bd1740b8444ec4d823ffaadf6825dfe9f0ed6b2abb3aa25b518da254822", + } + for path, expected in frozen_hashes.items(): + self.assertTrue(path.is_file(), path) + self.assertEqual(hashlib.sha256(path.read_bytes()).hexdigest(), expected, path) + + total = json.loads(ROUND2_MANIFEST.read_text(encoding="utf-8")) + batch9 = total["revision_batch9"] + self.assertEqual(batch9["scope"], "case-b-only") + self.assertEqual(batch9["status"], "user_review_received") + self.assertEqual(batch9["case-a"]["status"], "user_accepted_preserved") + self.assertFalse(batch9["case-a"]["modified"]) + self.assertFalse(batch9["case-a"]["shown_again"]) + self.assertEqual( + batch9["case-a"]["sha256"], + "d301e9818d62ffc18b95c4b8ecb1e3ec789a7f394ac87b3671e0955804cea6ba", + ) + self.assertTrue(batch9["revision_budget"]["automatic_batch10_forbidden"]) + self.assertEqual( + batch9["case-b"]["technical_status"], + "replay_verified_technical_candidate", + ) + self.assertEqual( + batch9["case-b"]["status"], "user_rejected_due_to_ghosting" + ) + self.assertEqual(batch9["case-b"]["user_visual_gate"], "FAIL") + cold = batch9["cold_replay"] + self.assertEqual(cold["path"], "diagnostics/batch9-cold-replay-report.json") + self.assertEqual(cold["status"], "PASS") + self.assertTrue(cold["frozen_media_bit_identical"]) + self.assertEqual( + cold["validated_recipe_sha256"], + "7a45795d1d43739166d45012489a89a06404260f41e4719ba3017c1862dc1904", + ) + self.assertTrue(ROUND2_BATCH9_COLD_REPORT.is_file()) + self.assertEqual( + hashlib.sha256(ROUND2_BATCH9_COLD_REPORT.read_bytes()).hexdigest(), + cold["sha256"], + ) + cold_report = json.loads(ROUND2_BATCH9_COLD_REPORT.read_text(encoding="utf-8")) + self.assertEqual(cold_report["verdict"]["cold_replay"], "PASS") + self.assertTrue(cold_report["cold_high_render"]["mp4_bytes_identical"]) + self.assertEqual( + cold_report["frozen_hashes"]["recipe_manifest_sha256"], + cold["validated_recipe_sha256"], + ) + self.assertEqual( + cold_report["frozen_hashes"]["artifact_ledger_sha256"], + cold["validated_artifact_ledger_sha256"], + ) + + recipe = json.loads(ROUND2_BATCH9_RECIPE_MANIFEST.read_text(encoding="utf-8")) + self.assertEqual(recipe["scope"], "case-b-only") + self.assertEqual( + recipe["parent_batch8_recipe"]["sha256"], + "4174b4eb1e4a419690a8f1dc16d7651c46041cc4f6cce087eb63c7330e018000", + ) + self.assertTrue(recipe["parent_batch8_recipe"]["preserved_unchanged"]) + self.assertTrue(recipe["project"]["self_contained_for_check_and_render"]) + self.assertTrue(recipe["project"]["fresh_frozen_check"]["ok"]) + provenance = recipe["project"]["diagnostic_provenance"] + self.assertTrue(provenance["historical_cache_reports_retained_verbatim"]) + self.assertFalse(provenance["runtime_dependency_on_historical_absolute_paths"]) + self.assertEqual(set(recipe["recipes"]), {"plant_unique_ownership_multi_leaf_sway_v2"}) + plant = recipe["recipes"]["plant_unique_ownership_multi_leaf_sway_v2"] + self.assertFalse(plant["camera_motion"]) + self.assertFalse(plant["runtime_seam_cover"]) + self.assertFalse(plant["crossfade"]) + self.assertTrue(plant["single_paused_gsap_timeline"]) + self.assertGreater(plant["motion"]["leaf_top_peak_tip_displacement_px"], 13.45) + self.assertGreater(plant["motion"]["leaf_left_peak_tip_displacement_px"], 6.96) + self.assertGreaterEqual( + plant["motion"]["concurrent_two_main_leaves"]["frames"], 15 + ) + self.assertTrue(plant["motion"]["concurrent_two_main_leaves"]["passed"]) + self.assertEqual(plant["high_visual_gate"]["status"], "PASS") + self.assertTrue( + recipe["promotion_gate"]["production_or_majority_shot_claim_forbidden"] + ) + self.assertEqual(recipe["promotion_gate"]["breadth_gate_status"], "not_run") + self.assertTrue(recipe["revision_budget"]["automatic_batch10_forbidden"]) + commands = recipe["commands"] + self.assertEqual(commands["working_directory"], "project") + self.assertEqual(commands["fresh_check_argv"][0:3], ["npx", "--yes", "hyperframes@0.7.107"]) + self.assertIn("--strict", commands["fresh_check_argv"]) + self.assertIn("--strict-all", commands["high_render_argv"]) + self.assertEqual(commands["seek_repeat_argv"][0], "node") + self.assertEqual(commands["frame_sweep_argv"][0], "node") + self.assertEqual(commands["policy"]["argv_type"], "list[str]") + self.assertFalse(commands["policy"]["shell"]) + self.assertTrue(commands["policy"]["runtime_network_forbidden"]) + + semantic_report = json.loads( + ( + ROUND2_BATCH9_REVISION_ROOT + / "project/diagnostics/case-b-semantic-v3/ownership-report.json" + ).read_text(encoding="utf-8") + ) + self.assertEqual(semantic_report["validation_threshold_count"], 256) + self.assertTrue(semantic_report["hard_gate"]["passed"]) + self.assertEqual(semantic_report["identity"]["outputs_to_normalized_subject_rgba_max_error"], 0) + self.assertEqual(semantic_report["identity"]["zero_pose_background_composite_rgba_max_error"], 0) + for threshold in semantic_report["thresholds"].values(): + self.assertEqual(threshold["holes"], 0) + self.assertEqual(threshold["extras"], 0) + self.assertTrue(threshold["passed"]) + self.assertTrue(all(value == 0 for value in threshold["pairwise_overlap"].values())) + + mesh_report = json.loads( + ( + ROUND2_BATCH9_REVISION_ROOT + / "project/diagnostics/case-b-l4/mesh-assets-report.json" + ).read_text(encoding="utf-8") + ) + self.assertTrue(mesh_report["hard_gate"]["passed"]) + for layer in mesh_report["layers"].values(): + self.assertEqual(layer["rest_crop_max_rgba_error"], 0) + self.assertEqual(layer["pin"]["max_rgba_error_all_states"], 0) + self.assertLessEqual(layer["theoretical_adjacent_tip_step_px"], 1.5) + self.assertEqual(layer["unique_state_hashes"], 31) + + review = json.loads(ROUND2_BATCH9_REVIEW_MANIFEST.read_text(encoding="utf-8")) + # review-batch9 is frozen pre-review evidence; the aggregate manifest, + # rather than this historical artifact, records the user's final answer. + self.assertEqual(review["status"], "awaiting_plant_revision_review") + self.assertEqual(review["scope"], "case-02-only") + self.assertTrue(review["review_policy"]["accepted_case_a_is_not_reshown_or_modified"]) + self.assertTrue(review["review_policy"]["automatic_batch10_forbidden"]) + review_root = ROUND2_BATCH9_REVIEW_MANIFEST.parent + public_videos = [] + for item in (*review["public_files"], *review["private_artifacts"]): + path = review_root / item["path"] + self.assertTrue(path.is_file(), path) + self.assertFalse(path.is_symlink(), path) + self.assertEqual(hashlib.sha256(path.read_bytes()).hexdigest(), item["sha256"]) + if path.suffix == ".mp4": + public_videos.append(path) + self.assertEqual(len(public_videos), 1) + self.assertEqual(public_videos[0].parent.name, "case-02") + + media = public_videos[0] + self.assertEqual( + hashlib.sha256(media.read_bytes()).hexdigest(), + "94a59bd1740b8444ec4d823ffaadf6825dfe9f0ed6b2abb3aa25b518da254822", + ) + probe = subprocess.run( + [ + "ffprobe", "-v", "error", "-show_entries", + "format=duration:stream=codec_name,codec_type,width,height,pix_fmt,r_frame_rate,nb_frames", + "-of", "json", str(media), + ], + check=True, + capture_output=True, + text=True, + ) + probe_data = json.loads(probe.stdout) + video_streams = [stream for stream in probe_data["streams"] if stream["codec_type"] == "video"] + audio_streams = [stream for stream in probe_data["streams"] if stream["codec_type"] == "audio"] + self.assertEqual(len(video_streams), 1) + self.assertEqual(len(audio_streams), 0) + stream = video_streams[0] + self.assertEqual((stream["width"], stream["height"]), (1080, 1920)) + self.assertEqual(stream["r_frame_rate"], "30/1") + self.assertEqual(stream["nb_frames"], "150") + self.assertEqual((stream["codec_name"], stream["pix_fmt"]), ("h264", "yuv420p")) + self.assertEqual(float(probe_data["format"]["duration"]), 5.0) + decoded = subprocess.run( + ["ffmpeg", "-v", "error", "-i", str(media), "-f", "null", "-"], + capture_output=True, + text=True, + ) + self.assertEqual(decoded.returncode, 0, decoded.stderr) + + self.assertTrue(ROUND2_BATCH9_HASH_LEDGER.is_file()) + ledger_entries = ROUND2_BATCH9_HASH_LEDGER.read_text(encoding="utf-8").splitlines() + self.assertGreaterEqual(len(ledger_entries), 130) + for line in ledger_entries: + expected, relative = line.split(" ", 1) + path = PACKAGE_ROOT.parent / relative + self.assertTrue(path.is_file(), path) + self.assertEqual(hashlib.sha256(path.read_bytes()).hexdigest(), expected, path) + + def test_final_causal_exception_is_bounded_hashed_and_permanently_stopped(self) -> None: + expected_hashes = { + "artifact-sha256.txt": "e05bce3885643202ae992eb86eb8257976f7a4b5aa382915224976ac7718c7bb", + "config.json": "5bb0142ac73564c852f798b6027f1670dc7ebd95f0c85c7d7e7470b7d6a01b75", + "final-causal-report.json": "27d4b76de4fc63c6a52eba3bf986a64091e27d054894cea965d149f88afe2182", + "generate_unified_mesh.swift": "474fda5e6031d5bf24858dda84477a620f705ed8c1110bd704ebf572f254887e", + "hard-gate-report.json": "4cf1305b1131a11bb7eea45d4034fd5bd15506fc270a7ac23167cf2d6e54c990", + "project/assets/mesh-geometry.json": "2ce99e1364ea1b7f6234723221fba8042168ad6096a72d8d841d878386ab585f", + "project/assets/subject-states/rest-warp.png": "50d1e4dff914187f1227e6d9ff95600ac5ee13d98197b1bddd60e99fe848e8ed", + "proofs/frame-46.png": "4a96f7bd657219857dea8803b42b84ed59fce44c226f76d407525b1a0e1fdbc8", + "proofs/frame-53.png": "e15374397d1ddabc1619d4b44c8829fea84920e6d67a4150954da29e6d3f2b8c", + "proofs/frame-60.png": "6d6b9a486cd618d94e1ab440eb992b028bc60db4879ebbd637422d27583715b0", + "proofs/pressure-46-53-60.png": "96633a6891e671840b6b6911297fc6649f327d6d82fad90dff3bd6ab5edfce11", + "strain-ghost-report.json": "1d3d70d6750c82f0f8bd2aee94aa2df5caa8756b6634e8706263aa7c4ac9af87", + "verify_hard_gates.py": "f8325a428c5577ae198681cd2596920e5fbc6f287c25bd6202b6099f143cec90", + "verify_strain_ghost.py": "6eb801835d20a8a9a77fc7d471d877708f2b9da3c0e25d356d57ec698ca73d6a", + } + self.assertTrue(ROUND2_FINAL_CAUSAL_ROOT.is_dir()) + self.assertFalse(ROUND2_FINAL_CAUSAL_ROOT.is_symlink()) + for path in ROUND2_FINAL_CAUSAL_ROOT.rglob("*"): + self.assertFalse(path.is_symlink(), path) + + actual_files = { + path.relative_to(ROUND2_FINAL_CAUSAL_ROOT).as_posix() + for path in ROUND2_FINAL_CAUSAL_ROOT.rglob("*") + if path.is_file() + } + self.assertEqual(actual_files, set(expected_hashes)) + for relative, expected in expected_hashes.items(): + path = ROUND2_FINAL_CAUSAL_ROOT / relative + self.assertEqual(hashlib.sha256(path.read_bytes()).hexdigest(), expected, path) + + ledger_entries = {} + for line in ROUND2_FINAL_CAUSAL_LEDGER.read_text(encoding="utf-8").splitlines(): + expected, relative = line.split(" ", 1) + ledger_entries[relative] = expected + self.assertEqual( + ledger_entries, + { + relative: expected + for relative, expected in expected_hashes.items() + if relative != "artifact-sha256.txt" + }, + ) + + manifest = json.loads(ROUND2_MANIFEST.read_text(encoding="utf-8")) + self.assertEqual(manifest["status"], "complete") + self.assertEqual(manifest["plan_grade"], "CONDITIONAL") + self.assertEqual(manifest["classification"], "manual_only") + self.assertEqual(manifest["integration_decision"], "not_reopened_for_production") + exception = manifest["final_causal_exception"] + self.assertEqual( + exception, + { + "evidence_root": "final-causal-exception", + "report": { + "path": "final-causal-exception/final-causal-report.json", + "sha256": expected_hashes["final-causal-report.json"], + }, + "artifact_ledger": { + "path": "final-causal-exception/artifact-sha256.txt", + "sha256": expected_hashes["artifact-sha256.txt"], + }, + "frozen_inputs": { + "subject": { + "path": "revision-batch9/project/assets/case-b/subject-cutout.png", + "sha256": "65fdab0cd3987e5058448202e235b7ece1f707aeaf38ff0723d49709b0d19225", + }, + "background": { + "path": "revision-batch9/project/assets/case-b/background-clean.png", + "sha256": "0b39c25141235462b85723ede5a0be1aac0f59e8e8d89adc6253d22d72327dc7", + }, + }, + }, + ) + for frozen in exception["frozen_inputs"].values(): + path = ROUND2_ROOT / frozen["path"] + self.assertTrue(path.is_file(), path) + self.assertFalse(path.is_symlink(), path) + self.assertEqual(hashlib.sha256(path.read_bytes()).hexdigest(), frozen["sha256"]) + + report = json.loads(ROUND2_FINAL_CAUSAL_REPORT.read_text(encoding="utf-8")) + self.assertEqual( + report["schema"], + "short-video-v2.phase7.final-causal-exception-report.v1", + ) + self.assertEqual( + report["authorization"]["basis"], + "user_authorized_final_causal_exception", + ) + self.assertEqual( + report["authorization"]["round_classification"], + "not_batch10/not_round3", + ) + self.assertTrue(report["authorization"]["preserves_round2_status_and_classification"]) + self.assertTrue(report["authorization"]["preserves_frozen_batch8_and_batch9"]) + self.assertEqual(report["scope"]["result_run"], "only_valid_run") + self.assertEqual(report["scope"]["valid_run_count"], 1) + self.assertFalse(report["scope"]["uncalibrated_early_outputs_are_results"]) + self.assertFalse(report["effective_run"]["parameter_revision_after_result"]) + + g4 = report["gates"]["g4"] + self.assertEqual(g4["status"], "PASS") + self.assertEqual(g4["simultaneous_10_to_18px_source_frames"], [57, 58, 59, 60]) + self.assertEqual(g4["top_max_adjacent_step_px"], 1.1052748609490886) + self.assertEqual(g4["left_max_adjacent_step_px"], 0.7056290390179525) + g5 = report["gates"]["g5"] + self.assertEqual(g5["status"], "FAIL") + self.assertTrue(g5["used_for_stop"]) + self.assertEqual( + { + key: g5[key] + for key in ( + "sigma_min", + "sigma_max", + "condition_max", + "area_min", + "area_max", + "area_p01", + "area_p99", + "edge_stretch_p05", + "edge_stretch_p95", + ) + }, + { + "sigma_min": 0.4356269491426868, + "sigma_max": 1.6225781559159698, + "condition_max": 2.435135276382128, + "area_min": 0.46211766403766275, + "area_max": 1.5526969937060449, + "area_p01": 0.7859413722531491, + "area_p99": 1.3244949151589778, + "edge_stretch_p05": 0.9893953951197354, + "edge_stretch_p95": 1.0270136679765456, + }, + ) + zero_warp = report["gates"]["zero_warp"] + self.assertEqual(zero_warp["status"], "zero_warp_drift_disclosed") + self.assertTrue(zero_warp["direct_rest_rgba_exact"]) + self.assertFalse(zero_warp["same_renderer_zero_warp_rgba_exact"]) + self.assertEqual(zero_warp["zero_warp_max_rgba_error"], 255) + self.assertEqual(zero_warp["alpha_changed_pixels"], 122011) + self.assertEqual(zero_warp["alpha_max_difference"], 86) + self.assertEqual(zero_warp["alpha_difference_gt_8_pixels"], 39766) + g6 = report["gates"]["g6"] + self.assertEqual(g6["raw_proxy_status"], "FAIL") + self.assertEqual(g6["adjudication"], "invalid_proxy_not_used_for_stop") + self.assertFalse(g6["used_for_stop"]) + + self.assertEqual( + report["causal_conclusion"]["prior_multilayer_interpenetration"], + "important_contributing_factor_not_unique_root_cause", + ) + self.assertEqual( + report["final_disposition"]["status"], + "plant_route_permanently_stopped", + ) + self.assertFalse(report["final_disposition"]["further_parameter_tuning_allowed"]) + for field in ( + "hyperframes_project_created", + "hyperframes_checks_run", + "draft_video_created", + "candidate_video_created", + "second_parameter_revision", + "compiled_generator_copied", + "runtime_dependency_on_cache", + ): + self.assertFalse(report["artifact_boundary"][field], field) + self.assertEqual(report["artifact_boundary"]["copied_action_state_png_count"], 0) + + self.assertFalse((ROUND2_ROOT / "revision-batch10").exists()) + self.assertFalse((ROUND2_FINAL_CAUSAL_ROOT / "generate_unified_mesh").exists()) + self.assertEqual(list(ROUND2_FINAL_CAUSAL_ROOT.rglob("state-*.png")), []) + for relative in actual_files: + self.assertNotIn(Path(relative).suffix.lower(), {".mp4", ".mov", ".webm"}) + self.assertFalse( + any(token in relative.lower() for token in ("hyperframes", "draft", "candidate")), + relative, + ) + + def test_plan07_final_user_gate_is_synchronized_across_current_docs(self) -> None: + current_docs = ( + EXECUTION_RECORD, + MOTION_FEASIBILITY, + MOTION_PLAYBOOK, + README, + OVERALL_PLAN, + ) + for path in current_docs: + text = path.read_text(encoding="utf-8") + self.assertIn("CONDITIONAL", text, path) + self.assertIn("manual_only", text, path) + self.assertIn("user_rejected_due_to_ghosting", text, path) + + for path in (EXECUTION_RECORD, MOTION_FEASIBILITY, MOTION_PLAYBOOK, README): + text = path.read_text(encoding="utf-8") + self.assertIn("假阴性", text, path) + self.assertIn("用户视觉门", text, path) + self.assertIn("cancelled_due_to_failed_prerequisite", text, path) + + record = EXECUTION_RECORD.read_text(encoding="utf-8") + for feedback in ("动作明显", "像一阵风", "明显重影"): + self.assertIn(feedback, record) + self.assertIn("automatic_batch10_forbidden=true", record) + + +if __name__ == "__main__": + unittest.main() diff --git "a/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/04-\345\233\276\347\211\207\347\224\237\346\210\220\344\270\216\351\253\230\347\272\247\345\212\250\346\200\201\345\214\226\345\217\257\350\241\214\346\200\247\350\256\241\345\210\222.md" "b/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/04-\345\233\276\347\211\207\347\224\237\346\210\220\344\270\216\351\253\230\347\272\247\345\212\250\346\200\201\345\214\226\345\217\257\350\241\214\346\200\247\350\256\241\345\210\222.md" index 8679151..ac32dc7 100644 --- "a/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/04-\345\233\276\347\211\207\347\224\237\346\210\220\344\270\216\351\253\230\347\272\247\345\212\250\346\200\201\345\214\226\345\217\257\350\241\214\346\200\247\350\256\241\345\210\222.md" +++ "b/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/04-\345\233\276\347\211\207\347\224\237\346\210\220\344\270\216\351\253\230\347\272\247\345\212\250\346\200\201\345\214\226\345\217\257\350\241\214\346\200\247\350\256\241\345\210\222.md" @@ -1,5 +1,7 @@ # 短视频 V2 实施计划 04:图片生成与高级动态化可行性 +> 历史状态:本计划已于 2026-08-12 执行完成。其中 MFLUX 安装、下载、续传、适配与后备推荐条款已被后续决策废止,不得再作为执行指令。原文仅为保留当时评估过程;当前路线以 `docs/short_video_v2/motion_feasibility_v1.md` 为准。 + > 文档性质:可直接执行的阶段计划。 > > 上位依据:`短视频V2规划文档/短视频V2总体目标与阶段规划.md` 第 8、9、13 节。 diff --git "a/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/05-\347\253\257\345\210\260\347\253\257\350\264\250\351\207\217\351\252\214\350\257\201\344\270\216MVP\351\252\214\346\224\266\350\256\241\345\210\222.md" "b/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/05-\347\253\257\345\210\260\347\253\257\350\264\250\351\207\217\351\252\214\350\257\201\344\270\216MVP\351\252\214\346\224\266\350\256\241\345\210\222.md" index a0a0a3b..11cd730 100644 --- "a/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/05-\347\253\257\345\210\260\347\253\257\350\264\250\351\207\217\351\252\214\350\257\201\344\270\216MVP\351\252\214\346\224\266\350\256\241\345\210\222.md" +++ "b/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/05-\347\253\257\345\210\260\347\253\257\350\264\250\351\207\217\351\252\214\350\257\201\344\270\216MVP\351\252\214\346\224\266\350\256\241\345\210\222.md" @@ -16,7 +16,7 @@ - FFmpeg 是普通人物、建筑和风景镜头的正式默认,优先 `slow_push_in`,谨慎使用 `gentle_drift`,避免无叙事依据的左右晃动; - HyperFrames 仅保留信息设计型镜头的 `manual_only` 结论,不进入普通自然场景主线; - DepthFlow 对正式与人工自然场景路线均已拒绝; -- MFLUX 因冷模型下载耗时失控归为 `research_only`; +- MFLUX 在计划 04 当时因冷模型下载耗时失控归为 `research_only`;后续已决定退役并清理,本条仅保留历史承接,不构成未来推荐; - Draw Things/本地 I2V 因未获授权归为 `research_only`; - 正式高级动态 `formal_candidate` 为零,这符合计划 04 原定“可为零”的决策门; - 用户明确指出微放大、平移和晃动不等于真正动态,风景雨落、人物轻摆等仍是当前能力缺口。 diff --git "a/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/07-HyperFrames\346\213\206\345\261\202\345\276\256\345\212\250\346\200\201\344\270\223\351\241\271\351\252\214\350\257\201\350\256\241\345\210\222.md" "b/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/07-HyperFrames\346\213\206\345\261\202\345\276\256\345\212\250\346\200\201\344\270\223\351\241\271\351\252\214\350\257\201\350\256\241\345\210\222.md" new file mode 100644 index 0000000..c407656 --- /dev/null +++ "b/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/07-HyperFrames\346\213\206\345\261\202\345\276\256\345\212\250\346\200\201\344\270\223\351\241\271\351\252\214\350\257\201\350\256\241\345\210\222.md" @@ -0,0 +1,793 @@ +# 短视频 V2 计划 07:HyperFrames 拆层微动态专项验证 + +> 后续状态(2026-08-13):`complete / historical / inactive_for_v1`。本计划的验证与用户审片结论均保留,但第一版已决定不做图片动态化;不得从本文自动启动实验或把 HyperFrames 接回默认工作流。未来若重开动态专项,应另立当期计划并重新确认范围。 + +> 文档定位:本计划是首轮六阶段完成后的专项纠偏与能力验证,不推翻计划 01–06 已经通过的短视频主链。它只回答一个尚未被正确测试的问题:在不使用生成式 I2V 的前提下,Codex、Image 2 与 HyperFrames 能否把一张静态人物、动物或植物图片做成具有可感知局部动作的短镜头,而不是只让整张图片推拉、平移或晃动。 +> +> 新旧结论关系:计划 04 对 HyperFrames 的 CLI、Chrome、确定性渲染、信息设计和弱环境叠层已有有效证据;但其人物实验主动禁止分割、独立身体运动、眨眼、呼吸和衣袂形变,不能代表拆层微动态能力。本计划是对这一缺口的专项补测。若本文与计划 04 中“自然人物 HyperFrames 已拒绝”的表述冲突,以本文对“拆层微动态”这一更窄范围的后续决定为准;计划 04 的原始过程仍保留为历史证据。 + +--- + +## 1. 前置事实与问题复盘 + +### 1.1 已经确定的主链事实 + +- 计划 06 已证明 Codex 可从自然语言连续完成内容、Image 2 关键帧、Job Bundle、包 B 人声和包 A/FFmpeg 成片; +- 用户已认可现有成片的内容和声音,当前主要视觉问题是图片动态效果不足; +- 包 A/FFmpeg 的基础运镜和最终合成仍是可靠底座,本计划不替换它; +- MFLUX 已退役并清理,DepthFlow 已证明只能做视差,二者均不进入本计划; +- 本计划不使用 BGM、环境音或 SFX,不修改包 B; +- 本计划不接云端或本地 I2V,不以视频扩散模型掩盖 HyperFrames 本身的能力边界。 + +### 1.2 前次 HyperFrames 测试为何不能作为能力上限 + +前次人物实验的本地方案文件明确包含以下限制: + +- no_segmentation=true; +- no_independent_body_motion=true; +- no_mesh_warp=true; +- “不得增加口型、眨眼、呼吸或衣袂形变”; +- 唯一主导动效是整张图片从 1.000 缓慢放大到 1.024; +- 其余只有雨线整体位移和灯光遮罩移动。 + +实际 HTML 也只有完整人物图片、雨层和光罩,并没有身体、头部、眼睑、衣物或前后景的独立层。因此,那次实验有效证明的是: + +1. HyperFrames 能在本机稳定渲染; +2. 整图推近与弱叠层不足以满足用户; +3. 信息设计镜头较适合 HyperFrames。 + +它没有证明: + +1. 头部以颈部为枢轴轻微点头是否可行; +2. 闭眼贴片或眼睑遮罩能否形成自然眨眼; +3. 身体分组能否形成轻微呼吸; +4. 前中后景、主体和粒子是否能形成可感知层次; +5. Image 2 能否为 HyperFrames 准备对齐的动态资产; +6. 这套方法能否迁移到当前短视频工作流的真实镜头。 + +### 1.3 修正后的判断 + +HyperFrames 不是语义 I2V 模型,它不会自动理解图片并凭空生成新动作;但它可以把已经准备好的透明层、遮罩、局部状态、DOM、Canvas 或 WebGL 元素按确定性时间线运动。因此当前应把能力拆为四类: + +| 能力 | 当前结论 | +| --- | --- | +| 整图推拉、平移、轻漂移 | 已验证,单独使用效果不足 | +| 光粒、雨线、雾、光罩、前后景视差 | 技术可行,前次强度和设计不足,需重新编排 | +| 头部点动、眨眼、呼吸、耳朵/衣摆/叶片轻摆等拆层微动作 | 前次没有真正测试,本计划的核心对象 | +| 走路、转身、复杂表情、连续口型、衣发与环境自然联动 | 非本计划目标,通常仍需更复杂角色动画或 I2V | + +口型、眨眼、呼吸和衣物变化不再被一概禁止。是否使用某项动作,应由镜头语义、素材条件和质量决定,而不是为了“保护原图”提前封死。精确语音口型同步需要音素或 viseme 时间线,不作为本计划 PASS 的必需条件;若受控样片顺利,可追加一次两状态嘴部或耳朵动作作为非阻塞扩展。 + +--- + +## 2. 官方能力、本机现状与可复用条件 + +### 2.1 官方能力依据 + +截至规划日,HyperFrames 官方材料表明: + +- HTML 是视频源,框架逐帧 seek 并交给 FFmpeg 编码; +- 支持 GSAP、CSS、WAAPI、Anime.js、Lottie、Three.js 与自定义 Frame Adapter; +- 支持透明图片/视频、分层 DOM、遮罩、clip-path、粒子、Canvas/WebGL; +- 官方 remove-background 可把 JPG/PNG 输出为透明 PNG,也可处理视频透明层; +- keyframes、check、snapshot 和 focused shot 可证明实际动画目标、姿态、遮罩与最终像素; +- Registry 可复用推近、粒子、漂移、视差等现成组件; +- 所有渲染关键动画必须可 seek、有限循环、无运行时随机和网络依赖。 + +主要官方入口: + +- https://github.com/heygen-com/hyperframes +- https://github.com/heygen-com/hyperframes/blob/main/skills/hyperframes-keyframes/SKILL.md +- https://github.com/heygen-com/hyperframes/blob/main/skills/media-use/audio/references/remove-background.md +- https://github.com/heygen-com/hyperframes/blob/main/skills/hyperframes-creative/references/motion-principles.md + +### 2.2 本机事实 + +- 计划 04 隔离 HyperFrames 根当前约 750 MB; +- 旧工程固定 HyperFrames 0.7.106; +- 规划日 npm latest 为 0.7.107;执行时必须重新只读核对,不把该版本永久写死; +- Node 24.17.0、包 A FFmpeg 与隔离 Chrome 已有运行证据; +- Registry 词面检索可找到 particle burst、push-in、camera punch-in、drift-hold、parallax 等组件; +- Registry 对 puppet、eyelid 没有直接命中,说明头部和眼睑动作不应伪装成现成一键组件,而应使用项目自有分层资产与 GSAP 关键姿态; +- 不启用约 33 MB 的 on-device 语义目录索引也不阻塞本计划;词面目录足以找到已知组件,缺失动作可按官方 keyframes 合同手工实现。 + +### 2.3 版本策略 + +新会话必须先运行项目 pin 的只读升级检查,再决定是否从 0.7.106 小步升级到当时最新稳定版: + +~~~bash +npx hyperframes@latest upgrade --project . --check --json +~~~ + +若升级,只允许修改新建的计划 07 实验工程,升级后必须运行 check;不得修改计划 04 冻结工程来制造版本混合证据。若升级检查或 check 失败,继续使用原 pin 并记录原因。 + +--- + +## 3. 计划目标、成功定义与非目标 + +### 3.1 核心目标 + +1. 建立一套“静态图 → 动作设计 → 分层资产 → HyperFrames 关键姿态 → 可验证镜头”的正确方法; +2. 制作一个受控动物/角色样片,至少含镜头推进、主体局部动作、眨眼/状态变化和环境微动; +3. 将通过的最小方法迁移到一个当前项目相关的人物或植物镜头; +4. 用同源图片制作静态、旧式整图运动和新式拆层微动态对照; +5. 让用户判断新路线是否达到“画面会动、自然、有趣、基本可用”的目标; +6. 形成后续可复用的动作 Brief、素材清单、提示词方法、验证命令和失败边界; +7. 仅在证据通过后,决定是否另立正式集成计划。 + +### 3.2 本阶段“通过”的含义 + +PASS 不要求大片级写实动作,也不要求自动适配所有图片。PASS 至少要求: + +- 同一张主图中,主体有两个可独立识别的局部动作,而不是只有整图变换; +- 画面另有一种环境或前景运动,并保留克制镜头运动; +- 关键动作在 30 FPS 成片中肉眼可见,又不显得机械或夸张; +- 不出现明显断颈、重影、空洞、遮罩白边、眼睑漂移、背景穿帮或身份变化; +- 动作可被 HyperFrames keyframes 和 proof snapshots 复核; +- 至少一个本项目相关镜头能复用方法,而非只在专门为测试制作的小羊图上成立; +- 用户在 A/B 审片中明确认为新版本优于静态或旧式晃动,并达到基本可用; +- 新路线的素材准备与编排复杂度仍适合少量重点镜头,不需要每镜人工精修数小时; +- 失败时仍可回退静态或 FFmpeg,不影响既有短视频主链。 + +### 3.3 明确不做 + +- 不修改 Schema v1、Job Bundle 公共字段、包 A 正式 Motion Router 或包 B; +- 不把 HyperFrames 变成新的整片时间线、字幕、TTS 或最终合成内核; +- 不接入 MFLUX、DepthFlow、Draw Things、I2V 或新的云视频 Provider; +- 不做完整人物走路、转身、复杂手势、自然说话口型或大幅肢体运动; +- 不为测试创建第二套状态机、工作流引擎、Web UI 或缓存系统; +- 不把滤镜、锐化、光晕或镜头晃动当作主体动作证据; +- 不以“技术上发生了像素变化”替代用户对自然度和可感知性的判断; +- 不清理计划 04、计划 05 或用户新生成视频的旧证据; +- 不处理对动态效果影响很低的问题。 + +--- + +## 4. 正确的图片动态化方法 + +### 4.1 总流程 + +~~~text +镜头语义与用户期望 + ↓ +动作 Brief:谁动、为何动、何时动、动到什么程度 + ↓ +资产方案:主图、干净背景、主体、身体、头部、眼睑/状态、前景、粒子 + ↓ +遮挡与枢轴设计:颈部、眼睛、身体重心、前后景顺序 + ↓ +静态 hero frame 与关键姿态证明 + ↓ +HyperFrames 单一 seek-safe 时间线 + ↓ +lint/check/keyframes/snapshot + ↓ +同源 A/B 视频与用户审片 +~~~ + +不能颠倒为“先随便拿一张图写 HTML,发现不动后再加晃动”。动作需求必须先于资产准备;素材若不支持目标动作,应换素材方案,而不是用整图抖动补救。 + +### 4.2 四层动作设计 + +每个受控样片都应从下列四层中有意选择,不要求每项全上: + +1. 镜头层:推近、轻微焦点偏移或短暂稳态; +2. 场景层:前中后景不同速度、云/草/光罩/露珠/雨线; +3. 主体层:身体呼吸、头部点动、耳朵/衣摆/叶片轻摆; +4. 局部状态层:眨眼、闭眼贴片、嘴部两状态、局部高光。 + +PASS 样片必须至少覆盖镜头层、主体层和局部状态层;环境层作为第四类补充。只有镜头层与场景叠层,不算完成本计划目标。 + +### 4.3 分层 DOM 建议 + +~~~text +scene +├── background-clean +├── camera-rig +│ ├── midground +│ └── subject-root +│ ├── body-layer +│ ├── head-pivot +│ │ ├── head-layer +│ │ └── eyelid-or-closed-eye-layer +│ └── optional-secondary-layer +├── foreground-layer +├── light-layer +└── particle-layer +~~~ + +- 镜头运动写在 camera-rig; +- 身体呼吸写在 subject-root 或 body-layer; +- 头部动作写在 head-pivot,transform origin 固定在颈部附近; +- 眼睑状态写在头部内部,随头部共同运动; +- 粒子和前景独立,不与主体抢同一 transform; +- 同一元素的同一 transform 属性不得由并发 tween 重复写入; +- 大镜头运动与局部遮罩变化应分父子层,避免冲突。 + +### 4.4 动作强度只是起点,不是硬编码答案 + +可用下列范围作为第一版试探,但必须依据实际画面几何和审片调整: + +| 动作 | 推荐初始范围 | +| --- | --- | +| 镜头推近 | 4–6 秒内约 2%–5%,围绕主体焦点 | +| 头部点动 | 约 1°–4°,含轻微 y 位移与回稳 | +| 眨眼 | 每次约 3–6 帧完成闭合—短停—张开,4–6 秒内 1–2 次且不等间隔 | +| 呼吸 | 约 0.2%–0.8% 的 scale/y 变化,周期约 2–3 秒 | +| 耳朵/衣摆/叶片 | 小角度、不同相位,不与头部完全同步 | +| 前中后景 | 位移幅度与速度随深度递增,避免所有层同向同速 | +| 粒子 | 固定种子、数量克制、不同生命周期但有限重复 | + +若动作太小导致 30 FPS 与 H.264 下不可感知,应适度提高;若出现木偶感、晕动或遮挡穿帮,应降低幅度或减少动作种类。测试目标是可感知且自然,不是数值越大越好。 + +--- + +## 5. 素材准备方法 + +### 5.1 推荐资产清单 + +受控角色样片至少准备: + +1. master.png:最终构图与身份基准; +2. background-clean.png:移除主体并合理补全后的背景; +3. subject-cutout.png:透明主体总层; +4. body.png:不含需独立运动头部的身体层,颈部保留遮挡余量; +5. head.png:透明头部层,边缘和颈部有重叠; +6. eyes-closed.png 或 eyelid.png:与头部严格对齐的闭眼局部状态; +7. foreground.png:可选花草、叶片或遮挡前景; +8. asset-manifest.json:来源、生成/编辑提示词、尺寸、透明通道、SHA-256、用途和版本。 + +迁移到植物镜头时,可替换为 background-clean、stem/base、leaf-01…N、fruit、foreground、light/particle 等层。 + +### 5.2 资产获得顺序 + +1. 先确定 master 与动作需求; +2. 优先用 Image 2 做“只改变目标区域”的小步编辑,生成干净背景和闭眼状态; +3. 对适合的 JPG/PNG,可试 HyperFrames 官方 remove-background 输出透明 PNG; +4. 官方去背对动物、植物或复杂边缘不应盲信,必须检查毛发、叶缘、半透明和空洞; +5. 头部/身体可由同一透明主体通过 SVG mask、clip-path 或预先裁切得到,但必须处理颈部重叠和原头部残影; +6. 若自动去背或局部状态明显失败,最多做一次针对性 Image 2 修订或改用更适合分层的主图,不连续下载其他分割模型; +7. 所有层先在静态 hero frame 叠回 master;若静态叠合都不一致,禁止进入动画阶段。 + +### 5.3 Image 2 资产提示词原则 + +提示词必须描述“保持什么不变、只改什么、输出层的用途和对齐要求”,例如: + +~~~text +以输入图为唯一构图、身份、光线和色彩基准;除指定区域外不得改变任何像素语义。输出同尺寸 9:16 图片。只把小羊的双眼自然闭合,保持头部姿态、毛发轮廓、耳朵、鼻口、背景、景深和光照完全一致;闭眼状态将作为 3–6 帧眨眼贴片使用,因此眼睑位置、大小和透视必须与原图精确对齐,不增加文字、水印或新物体。 +~~~ + +干净背景编辑应明确移除主体后补全原本被遮挡的草地、天空或环境,不得留下轮廓洞、重复肢体或模糊色块。头部/身体分层不要求 Image 2 每次直接输出完美透明 PNG;可由同一主图和透明主体切分,减少生成差异。 + +--- + +## 6. 动作 Brief 与 HyperFrames 提示词 + +### 6.1 一句自然语言只是意图种子 + +“把静态小羊做成会点头、眨眼和有光粒的竖屏视频”足以表达用户目标,但不能直接充当生产规格。Codex 必须先把它扩展为动作 Brief,再写 HTML。提示词质量不取决于字数,而取决于是否回答: + +- 主体是谁,哪一部分必须保持身份稳定; +- 哪些层需要独立控制; +- 每个动作由谁执行、围绕哪个枢轴; +- 动作在何时发生、持续多久、如何回稳; +- 哪些层彼此遮挡; +- 镜头、主体和环境的动作是否冲突; +- 哪些关键姿态能够证明动作真的发生; +- 什么瑕疵会直接判失败。 + +### 6.2 动作 Brief 模板 + +~~~text +项目:<名称> +目标:把 <来源图片> 做成 <4–6 秒、9:16、30 FPS> 的微动态镜头 +成片感受:<安静、温暖、克制、生动等> +身份锚点:<脸、眼睛、毛发、服装、果实、叶片等不得改变的内容> +主图焦点:<归一化坐标或像素位置> +层级:背景 / 身体 / 头部 / 眼睑或状态 / 前景 / 光线 / 粒子 +镜头动作:<起点、终点、持续时间、焦点> +主体动作:<动作、枢轴、关键姿态、回稳> +局部状态:<眨眼或嘴部状态发生时间和帧数> +环境动作:<粒子、草叶、光罩、云等> +遮挡规则:<谁在谁前面,移动后如何避免空洞与重影> +时间线:0.0s 建立;…;末尾保留稳定帧 +禁止:整图左右晃动冒充主体运动;无限循环;运行时随机;身份变化;遮罩白边;断裂;重影 +验证:lint、check、keyframes 目标选择器、proof shot、指定时间 snapshot、ffprobe、完整解码、A/B +~~~ + +### 6.3 受控小羊样片的示例生产提示 + +~~~text +使用 HyperFrames 把同一张 9:16 小羊主图制作成 5 秒无声微动态镜头。先不要写 HTML,先根据图片建立动作 Brief 与资产清单,并确认静态叠合能够还原主图。画面至少拆为干净背景、身体、以颈部为枢轴的头部、闭眼局部状态、前景草叶和固定种子的光粒层。镜头在 5 秒内围绕小羊脸部克制推近;小羊在中段完成一次轻微点头并自然回稳,4–6 秒内出现一至两次不等间隔眨眼,身体有低幅呼吸;前景草叶与光粒缓慢运动但不得遮住眼睛。镜头运动、头部、呼吸和眼睑必须位于不同嵌套层,不能让同一元素承担冲突 transform。所有动画建立在单一 paused GSAP timeline,有限重复、无 Math.random、无网络依赖。输出同图静态版、旧式整图推近版和拆层微动态版;运行 lint、check、keyframes focused shot、关键时间 snapshot、ffprobe 和完整解码,并明确检查断颈、重影、眼睑漂移、背景空洞、毛发白边与木偶感。 +~~~ + +这段提示只作为动作规格示例,不是硬编码样片名称或唯一美术风格。执行会话仍须依据实际图片重新写动作 Brief。 + +--- + +## 7. 统一测试案例与公平对照 + +### 7.1 案例 A:受控角色/动物微动态 + +默认采用轮廓清楚、头颈关系明确、画面遮挡较少的 9:16 小羊或等价动物主图。若没有可用素材,由 Image 2 生成一张适合拆层的原创主图。 + +必须测试: + +- 克制镜头推近; +- 一次头部点动或等价主体动作; +- 一至两次眨眼或等价局部状态; +- 低幅呼吸; +- 前景或粒子运动; +- 不少于一个稳定 hold。 + +案例 A 的作用是证明能力上限与正确方法,不代表生产迁移已经完成。 + +### 7.2 案例 B:当前项目迁移镜头 + +从当前已有短视频中选择一个最适合迁移的镜头,优先顺序: + +1. 西瓜幼苗或叶片轮廓清楚的植物镜头; +2. 计划 05 人物近景中头颈与背景较清晰的镜头; +3. 其他用户已生成、构图适合分层的重点镜头。 + +执行者必须先解释为何该镜头适合,再选择动作。植物默认测试叶片不同相位轻摆、主体呼吸式生长感、露珠/光粒与克制推近;人物默认复用点头/眨眼/呼吸中的一至两项。不得为了通过而只选信息图。 + +### 7.3 同源对照组 + +每个案例至少输出: + +| 版本 | 内容 | +| --- | --- | +| S:静态 | 同一主图,无动作 | +| W:旧式 | 同一主图,仅整图推近/平移与简单叠层 | +| L:拆层 | 同一主图,镜头 + 主体局部动作 + 局部状态 + 环境层 | + +三版必须同尺寸、同时长、同 FPS、同编码目标,不加工具名称或引导文字。A/B 用户审片前保留匿名映射;技术报告可另行列真实版本。 + +--- + +## 8. HyperFrames 实现合同 + +### 8.1 组合结构 + +- 新建独立计划 07 实验工程,不在计划 04 rainy-messenger-motion 上直接改写; +- 主组合或每个案例子组合只能注册一个 paused GSAP timeline; +- 所有本地图片、脚本与 Registry 组件冻结到工程内,不在渲染时联网; +- 先静态完成 hero frame,再编排动作; +- camera、subject、head、eyelid、foreground、particle 使用明确的稳定选择器; +- 眼睑是头部的子层,头部动作时眼睑必须跟随; +- 呼吸与镜头缩放分处内外层,避免同一 scale 冲突; +- 任意粒子使用固定表或有种子的 PRNG,循环次数从镜头时长计算; +- 不使用 repeat:-1、Date.now、performance.now、未注册 requestAnimationFrame 或运行时 fetch; +- 末帧不得意外黑屏或复位到第一帧。 + +### 8.2 目录优先复用 + +编写动作前,先用 catalog 查询效果语义: + +- 镜头推近可评估 push-in 或 camera punch-in; +- 粒子可评估 particle burst、particle image reveal 或自有轻量粒子表; +- 环境持有可评估 drift-hold; +- 视差可参考 parallax 或 depth-stack 类组件。 + +只有组件与镜头语义、透明层结构和确定性规则相符时才安装。头部点动和眼睑没有现成匹配时,允许按 hyperframes-keyframes 合同写最小 GSAP 关键姿态;不为“避免写十几行动画”引入整套第三方角色框架。 + +### 8.3 关键姿态证明 + +每个 L 版本至少指定: + +- 第一帧; +- 动作前稳定帧; +- 点头或叶片摆动的峰值帧; +- 眨眼闭合帧; +- 回稳帧; +- final-minus-hold; +- 最终帧。 + +必须对真实动画目标运行 focused keyframes shot,不得只对 camera-rig 或辅助框做证明。若 CLI 报 flat,且动作本应有中间姿态,应增加可见关键姿态而不是忽略报告。 + +--- + +## 9. 验证与质量门 + +### 9.1 技术门 + +- HyperFrames pin、Node、Chrome、FFmpeg 与操作系统信息有记录; +- lint 通过; +- check 通过,严格模式下无未解释持久 warning; +- keyframes 能定位 camera、subject、head/leaf、eyelid 和 particle 中实际存在的目标; +- 指定 snapshot 可看到闭眼、动作峰值和回稳; +- 输出为 1080×1920、30 FPS、H.264/yuv420p、4–6 秒、无声视频; +- ffprobe 正常,完整解码退出码 0; +- 同输入重渲在固定版本与环境下可复现;若无法字节级一致,记录实际差异; +- 无孤儿 Chrome/Node/FFmpeg 进程和临时半成品覆盖。 + +### 9.2 资产门 + +- 所有层同尺寸或有明确坐标变换; +- alpha 边缘在浅色和深色底上都检查; +- 静态叠合能近似还原 master; +- background-clean 没有主体轮廓洞; +- 头颈和叶柄有遮挡余量; +- 闭眼状态与原眼位置严格对齐; +- asset manifest 保存提示词、来源、尺寸、哈希和用途; +- 旧素材、旧图和旧成片不被覆盖。 + +### 9.3 动作门 + +- 运动不是只有整图 transform; +- 主体至少有两个独立可感知动作; +- 镜头动作不掩盖主体动作; +- 环境动作不遮挡主体关键部位; +- 眨眼不是缓慢淡出眼睛,也不是整脸闪烁; +- 点头有枢轴、峰值和回稳,不是全身上下抖动; +- 呼吸低幅、连续,不像缩放脉冲; +- 不同叶片/草叶不同相位,不整片同频摆动; +- 粒子不使用运行时随机,数量与亮度不喧宾夺主; +- 动作与画面情绪一致,静止区仍有呼吸空间。 + +### 9.4 视觉失败项 + +任一明显出现即不可判 PASS: + +- 断颈、头部漂浮、关节错位; +- 原头残影或双眼/双脸; +- 背景空洞、重复纹理或突兀补图; +- 眼睑位置漂移、颜色不匹配或闪烁; +- 透明边缘白边、毛发/叶片被硬切; +- 镜头晃动、机械循环或晕动; +- 粒子遮脸、穿过眼睛或形成噪点; +- 动作幅度小到用户无法感知; +- 为掩盖问题添加过强滤镜、锐化、光效或字幕。 + +### 9.5 用户质量门 + +最终必须向用户展示匿名 S/W/L 视频与少量关键帧联系表,只询问: + +1. 哪一版最像“画面本身会动”; +2. 点头/眨眼/呼吸或叶片运动是否自然且看得见; +3. 是否存在不可接受的木偶感、接缝、闪烁或晕动; +4. 这种效果是否值得用于少量或多数普通镜头; +5. 哪一处动作最需要减弱、加强或删除。 + +用户反馈前最多判 CONDITIONAL,不得由自动评分替代。 + +--- + +## 10. 资源、隔离与安全边界 + +### 10.1 实验目录 + +建议使用: + +~~~text +/Users/yuh/Library/Caches/text-video-plan07-hyperframes/ +├── npm-cache/ +├── chrome/ +├── models/ +├── project/ +│ └── short-video-v2-micro-motion/ +└── output/ +~~~ + +项目内证据根: + +~~~text +【包A】视频引擎包/成片/短视频V2样片/phase7-hyperframes-micro-motion/ +├── case-a-controlled/ +├── case-b-project-transfer/ +├── anonymous-review/ +└── final-review/ +~~~ + +实验环境、Chrome、npm cache 和可再生 draft 不进入 Git;计划、模板、轻量测试、执行记录、manifest 与最终选择证据可按现有忽略规则处理。 + +### 10.2 下载边界 + +- 允许在计划 07 专用隔离根内使用 HyperFrames 当时最新稳定 patch 与其官方必要浏览器/去背模型; +- 本计划新增下载与缓存默认上限为 1 GiB,执行前后记录 du; +- 若已有计划 04 Chrome 可安全复用且不破坏版本隔离,可只读引用;若复制或链接会造成旧证据混淆,则使用新根; +- 超过 1 GiB、引入新的分割模型、第三方角色框架、云服务、付费 API 或 I2V 前必须暂停请求用户决定; +- 不重新启用 MFLUX,不下载 DepthFlow 或 Draw Things。 + +### 10.3 工作树保护 + +- 开始前记录 git status、当前 HEAD 与用户已有修改; +- 不 reset、clean、checkout、stash 或覆盖 config.ini 等用户修改; +- 计划 04 冻结工程与旧输出只读; +- 所有删除只能针对本计划新建且经核对的临时目录; +- 不自动提交或推送 GitHub,除非用户另行明确要求。 + +--- + +## 11. 计划内文件 + +### 11.1 预计新增 + +1. 短视频V2规划文档/执行记录/07-HyperFrames拆层微动态专项验证执行记录.md +2. 【包A】视频引擎包/experiments/short_video_v2_phase7/README.md +3. 【包A】视频引擎包/experiments/short_video_v2_phase7/templates/layered_motion_brief_v1.md +4. 【包A】视频引擎包/experiments/short_video_v2_phase7/templates/asset_manifest_v1.json +5. 【包A】视频引擎包/experiments/short_video_v2_phase7/templates/scorecard_v1.json +6. 【包A】视频引擎包/tests/test_v2_phase7_hyperframes_micro_motion.py +7. 计划 07 专用隔离 HyperFrames 工程、资产、输出和机器可读证据 + +模板与测试只约束实验目录、素材角色、命令安全、证据字段与媒体规格,不实现新的视频引擎。 + +### 11.2 允许最小修改 + +- 【包A】视频引擎包/docs/short_video_v2/motion_feasibility_v1.md +- 短视频V2规划文档/短视频V2总体目标与阶段规划.md +- 必要时为 short-video-director 的项目导航文档补充计划 07 状态,但不改 Skill 的视频能力边界 + +只在最终结论有证据后更新正式分类;执行中先标 re-evaluation_pending。 + +### 11.3 禁止修改 + +- Schema v1 与 Job Bundle 契约; +- 包 A video_v2 正式 pipeline、motion、timeline、captions、tts、cache; +- 包 B; +- 计划 05/06 已验收成片和 manifest; +- 旧计划 04 实验 HTML、视频、匿名映射和用户反馈; +- 用户未授权的仓库设置、发布与远端内容。 + +--- + +## 12. 分批实施 + +### 批次 1:现状复核、错误测试审计与实验合同 + +目标:冻结真实基线,避免再次用错误问题测试工具。 + +操作: + +1. 阅读计划 07、总体规划、计划 04 最终记录、计划 06 最终记录、motion_feasibility_v1 和旧 HyperFrames 工程; +2. 核对旧方案的限制与实际 HTML,明确哪些证据有效、哪些结论不适用于拆层微动态; +3. 调用 HyperFrames 主技能及 core、animation、keyframes、cli、creative、media-use; +4. 只读核对 npm latest、旧 pin、doctor、磁盘和进程; +5. 新建计划 07 执行记录、实验 README、动作 Brief/asset manifest/scorecard 模板和测试骨架; +6. 写出案例 A/B、S/W/L 对照、动作层、证明帧和失败项; +7. 运行模板/安全/路径定向测试。 + +退出条件: + +- 不再存在“先禁止主体动作,再判断主体动作能力”的逻辑; +- 测试合同能区分镜头运动、环境运动和主体局部动作; +- 新实验根、旧证据保护和资源上限明确; +- 不生成图片、不渲染视频也可完成本批。 + +### 批次 2:受控样片资产与静态姿态证明 + +目标:先证明素材能支持动作,再开始动画。 + +操作: + +1. 选择或用 Image 2 生成一个原创、轮廓清楚的小羊/动物 master; +2. 写动作 Brief 与 Image 2 局部编辑提示词; +3. 准备 background-clean、subject、body、head、eyes-closed/eyelid、foreground; +4. 可用官方 remove-background 做一次候选透明主体,但必须进行边缘审查; +5. 生成静态叠合图、头部峰值姿态图、闭眼姿态图和联系表; +6. 检查颈部、眼睛、毛发、空洞、对齐和 alpha; +7. 最多做一次针对性资产修订;仍失败则换用更适合分层的原创主图并记录原因; +8. 冻结资产哈希和 manifest。 + +退出条件: + +- 静态叠合近似 master; +- 点头峰值与闭眼状态在静态证明中无明显穿帮; +- 身份和构图稳定; +- 所有层与提示词可追溯; +- 未通过不得进入批次 3。 + +### 批次 3:受控样片 S/W/L 实现与技术验证 + +目标:真正测试拆层主体动作。 + +操作: + +1. 先 catalog 检索并选择可复用的镜头/粒子组件; +2. 建立新 HyperFrames 工程,按新版本策略固定 pin; +3. 实现 S 静态、W 整图旧式、L 拆层三版; +4. L 至少实现镜头推近、点头、眨眼、呼吸和环境/粒子; +5. 严格使用父子层和单一 paused timeline; +6. 运行 lint、check、keyframes、focused shots 与 proof snapshots; +7. 以 draft 迭代,技术与视觉门通过后再高质量渲染; +8. ffprobe、完整解码、哈希、耗时、峰值资源和磁盘有记录; +9. Codex 做一次有界自审和最多一次动作参数修订,不无限润色。 + +退出条件: + +- S/W/L 媒体规格一致; +- L 的主体动作可独立证明; +- 无明显失败项; +- 若技术通过但自然度尚待用户判断,进入批次 4,不提前 PASS。 + +### 批次 4:当前项目镜头迁移 + +目标:证明方法不只适配为测试而生的小羊图。 + +操作: + +1. 从西瓜植物、人像或其他当前项目素材中选择一镜并记录理由; +2. 根据实际构图重新写动作 Brief,不复制小羊参数; +3. 植物优先拆叶片/藤蔓/果实/前景,人物优先选择一至两项局部动作; +4. 只复用批次 3 已证明有效的组件和关键姿态方法; +5. 输出 S/W/L 或至少 W/L 公平对照; +6. 运行同等技术、姿态、媒体和视觉门; +7. 最多一次资产或动作修订;若素材先天不适合,应明确是素材失败还是方法失败。 + +退出条件: + +- 至少一个项目相关镜头完成新式微动态; +- 结果不存在为了通过而退回信息图或只有粒子的情况; +- 工程复杂度、资产循环和渲染耗时已记录。 + +### 批次 5:匿名审片材料与用户门 + +目标:让用户以最终像素判断路线价值。 + +操作: + +1. 统一 S/W/L 的分辨率、时长、编码和命名; +2. 生成匿名映射、联系表、动作峰值图和简短技术账; +3. Codex 先按固定评分表独立评分,但不向用户暗示答案; +4. 向用户展示最小审片集,询问本计划 9.5 的五个问题; +5. 在用户反馈前更新执行记录并合法暂停; +6. 不以“动作数量多”替代用户偏好,不宣称正式 Provider 通过。 + +退出条件: + +- 用户反馈前:计划最多 CONDITIONAL; +- 用户回复后进入批次 6。 + +### 批次 6:最小返修、最终分类与交接 + +目标:依据用户意见做一次最小修订并形成可信结论。 + +操作: + +1. 读取用户对具体动作和时间点的意见; +2. 每个案例最多做一次动作参数或单一资产局部修订,不重做全部主图; +3. 重跑受影响的 check、keyframes、snapshot、render、probe 和解码; +4. 重新展示仅受影响的最小对照;若用户无需再次查看,记录其明确意见; +5. 给 HyperFrames 拆层微动态分类:formal_candidate、manual_only、research_only 或 rejected; +6. 更新 motion_feasibility_v1、总体规划、实验 README 和最终 manifest; +7. 运行 phase7 定向测试与 V2 核心回归; +8. 明确后续若接入主链,需另立“实验 Provider/资产协议集成计划”,本计划不直接修改正式 pipeline。 + +退出条件: + +- 用户意见、修改、最终视频和哈希可追溯; +- 分类与证据一致; +- 旧计划结论的有效范围已正确标注; +- 所有测试与工作树保护完成。 + +--- + +## 13. 推荐验证命令原则 + +实际参数以执行时 HyperFrames 版本帮助和技能合同为准,不盲复制过时命令。至少覆盖: + +~~~bash +npx hyperframes@latest upgrade --project . --check --json +npx hyperframes doctor --json +npx hyperframes catalog --query "camera push zoom" --json +npx hyperframes catalog --query "particle burst" --json +npx hyperframes lint +npx hyperframes check --strict --snapshots +npx hyperframes keyframes . --json +npx hyperframes keyframes . --selector "" --shot "" --samples +npx hyperframes snapshot . --at +npx hyperframes render --quality draft --output +npx hyperframes render --quality high --output +ffprobe -v error -show_streams -show_format +ffmpeg -v error -i -f null - +~~~ + +执行前必须读取当时对应 hyperframes-cli 与 hyperframes-keyframes 指令;若命令已变更,以当前官方帮助为准并更新记录。 + +--- + +## 14. 测试矩阵 + +| 层级 | 必测内容 | +| --- | --- | +| 文档/模板 | Motion Brief、asset manifest、scorecard 字段完整 | +| 路径安全 | 新实验根封闭、拒绝符号链接逃逸、旧证据只读 | +| 命令安全 | list argv、有限 timeout、取消、无 shell 字符串拼接 | +| 组合结构 | root ID、尺寸、时长、clip、timeline key 一致 | +| 确定性 | 无运行时随机/网络/无限循环,固定 seed 与 finite repeats | +| 分层 | camera/subject/head/eyelid/foreground 分离,无 transform 冲突 | +| 姿态 | 点头峰值、闭眼、回稳、末帧均有像素证明 | +| 媒体 | 1080×1920、30 FPS、H.264/yuv420p、无声、完整解码 | +| 对照 | S/W/L 同源、同时长、同规格、匿名映射 | +| 回归 | phase7 定向测试 + 既有 V2 核心测试 | +| 用户 | 最终 A/B 偏好、不可接受瑕疵、适用镜头范围 | + +--- + +## 15. 最终结论等级 + +### PASS + +- 受控角色样片与项目迁移镜头均通过技术门; +- 至少一个案例的点头/眨眼/呼吸或叶片动作自然、可感知; +- 用户认为 L 明显优于 S/W,达到基本可用; +- 无明显接缝、空洞、重影和身份问题; +- 工程复杂度适合重点镜头; +- 可进入后续正式集成规划。 + +### CONDITIONAL + +- 受控样片技术成立,但项目迁移尚未通过; +- 或用户审片/一次最小返修尚未完成; +- 或效果只适合特定风格、动物/植物或少量重点镜头; +- 主链不受影响,继续维持实验/人工路线。 + +### FAIL + +- 拆层后仍只有整图运动; +- 主体动作不可感知或木偶感明显; +- 资产准备长期不稳定,普通镜头难以复用; +- 接缝、重影、背景空洞或身份问题无法在一次有界修订内改善; +- 用户认为不如静态图; +- 则 HyperFrames 自然主体微动态在当前项目条件下拒绝,保留信息设计用途。 + +无论 PASS、CONDITIONAL 或 FAIL,都不改变 Image 2 + 包 B + 包 A/FFmpeg 主链已经可用的事实。 + +--- + +## 16. 风险与控制 + +| 风险 | 控制 | +| --- | --- | +| 小羊图为测试专门设计,无法迁移 | 必须执行案例 B | +| Image 2 局部编辑造成整图变化 | 只取局部状态层,做像素/联系表审查 | +| 自动去背不适合毛发/植物 | 只做一个候选;失败转 Image 2 或 SVG mask | +| 头部旋转露出原头/空洞 | 使用 clean background、body/head 分层和颈部重叠 | +| 眨眼像闪烁 | 用对齐眼睑/闭眼状态,按帧设计闭合—短停—张开 | +| 呼吸像缩放脉冲 | 低幅、长周期、独立于 camera | +| 所有层同频产生机械感 | 不同相位、不同幅度,保留稳定 hold | +| 效果太弱看不见 | 关键姿态证明与 30 FPS 成片双重检查 | +| 为追求复杂度无限加动作 | 每镜一个主动作、若干支持动作;一次有界修订 | +| CLI 升级导致旧项目不兼容 | 新工程试升级,失败回原 pin,旧工程不动 | +| 测试通过后直接污染正式主链 | 只给分类和交接,另立集成计划 | + +--- + +## 17. 执行记录要求 + +执行记录固定为: + +/Users/yuh/Desktop/项目/文本视音屏生成器/短视频V2规划文档/执行记录/07-HyperFrames拆层微动态专项验证执行记录.md + +每批至少记录: + +- 执行目标; +- 读取的权威资料与版本; +- 实际完成; +- 资产/提示词/动作 Brief; +- 改动文件; +- 命令和退出码; +- 关键姿态与媒体证据; +- 图片/视频哈希; +- 耗时、内存、磁盘; +- 问题与修订; +- 剩余风险; +- 是否允许进入下一批。 + +最终记录必须包含: + +- 用户原始反馈; +- 旧实验误区与修正; +- S/W/L 匿名映射; +- 用户选择与不可接受瑕疵; +- 工具最终分类; +- 是否建议另立正式集成计划; +- 未触碰 Schema、包 B、正式 pipeline 和旧证据的保护说明。 + +--- + +## 18. 最短执行说明 + +> 不再把完整静态图左右晃动当作 HyperFrames 动态能力。先由 Codex 为镜头写清“谁动、如何动、何时动”,再由 Image 2 与可控遮罩准备背景、身体、头部、眼睑和前景层,HyperFrames 以单一可 seek 时间线完成点头、眨眼、呼吸、叶片或粒子等微动作;最后用同源静态、旧式运镜和拆层微动态作匿名对照,并以用户审片决定是否值得接入。 diff --git "a/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/\346\211\247\350\241\214\346\217\220\347\244\272\350\257\215/04-\345\233\276\347\211\207\347\224\237\346\210\220\344\270\216\351\253\230\347\272\247\345\212\250\346\200\201\345\214\226\345\217\257\350\241\214\346\200\247\346\211\247\350\241\214\346\217\220\347\244\272\350\257\215.md" "b/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/\346\211\247\350\241\214\346\217\220\347\244\272\350\257\215/04-\345\233\276\347\211\207\347\224\237\346\210\220\344\270\216\351\253\230\347\272\247\345\212\250\346\200\201\345\214\226\345\217\257\350\241\214\346\200\247\346\211\247\350\241\214\346\217\220\347\244\272\350\257\215.md" index 0876a5c..fce3c74 100644 --- "a/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/\346\211\247\350\241\214\346\217\220\347\244\272\350\257\215/04-\345\233\276\347\211\207\347\224\237\346\210\220\344\270\216\351\253\230\347\272\247\345\212\250\346\200\201\345\214\226\345\217\257\350\241\214\346\200\247\346\211\247\350\241\214\346\217\220\347\244\272\350\257\215.md" +++ "b/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/\346\211\247\350\241\214\346\217\220\347\244\272\350\257\215/04-\345\233\276\347\211\207\347\224\237\346\210\220\344\270\216\351\253\230\347\272\247\345\212\250\346\200\201\345\214\226\345\217\257\350\241\214\346\200\247\346\211\247\350\241\214\346\217\220\347\244\272\350\257\215.md" @@ -1,5 +1,7 @@ # 短视频 V2 计划 04:分批执行提示词 +> 历史状态:计划 04 已完成。本文中所有 MFLUX 安装、下载、续传、适配或恢复提示均已失效,不得再复制执行;仅保留为当时任务过程记录。 + 本文档服务于《04-图片生成与高级动态化可行性计划》。通常不需要逐段复制:用户先在新会话粘贴由规划会话单独提供的“总执行提示词”,新会话便应读取本文并按批次推进。背景提示词和七个批次提示词主要用于只读预热、局部重跑、下载授权后的续接、失败恢复或用户主动要求分批控制。 分工保持不变: diff --git "a/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/\346\211\247\350\241\214\346\217\220\347\244\272\350\257\215/07-HyperFrames\346\213\206\345\261\202\345\276\256\345\212\250\346\200\201\344\270\223\351\241\271\351\252\214\350\257\201\346\211\247\350\241\214\346\217\220\347\244\272\350\257\215.md" "b/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/\346\211\247\350\241\214\346\217\220\347\244\272\350\257\215/07-HyperFrames\346\213\206\345\261\202\345\276\256\345\212\250\346\200\201\344\270\223\351\241\271\351\252\214\350\257\201\346\211\247\350\241\214\346\217\220\347\244\272\350\257\215.md" new file mode 100644 index 0000000..6068557 --- /dev/null +++ "b/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/\346\211\247\350\241\214\346\217\220\347\244\272\350\257\215/07-HyperFrames\346\213\206\345\261\202\345\276\256\345\212\250\346\200\201\344\270\223\351\241\271\351\252\214\350\257\201\346\211\247\350\241\214\346\217\220\347\244\272\350\257\215.md" @@ -0,0 +1,131 @@ +# 短视频 V2 计划 07:分批执行提示词 + +> 状态(2026-08-13):历史执行提示词,计划已完成,第一版动态路线已关闭。新会话不得复制本文继续运行;仅供复盘当时的实验步骤与证据。 + +本文档服务于《07-HyperFrames拆层微动态专项验证计划》。通常无需逐段复制:用户在新会话粘贴由规划会话单独提供的“总执行提示词”后,执行会话应完整阅读本文,从第一个未完成批次连续推进。背景提示词和六个批次提示词用于只读预热、局部重跑、断点恢复、用户审片后的续接,或用户主动要求分批控制。 + +分工如下: + +- 总提示词负责完整阅读、调用正确的 HyperFrames 技能、连续推进、工作树保护、执行记录、用户审片门和最终完成条件; +- 实施计划负责旧测试纠偏、图片分层、动作 Brief、案例 A/B、S/W/L 对照、资源门、技术门和最终分类; +- 背景提示词只做只读熟悉,不实施、不创建执行记录; +- 分批提示词是可独立复制的局部执行合同,每段重申本批不可缺少的边界; +- 执行记录是断点续接和最终验收的事实来源; +- 总提示词不写入本文,只在规划会话最终答复中单独展示。 + +主要依据: + +- /Users/yuh/Desktop/项目/文本视音屏生成器/短视频V2规划文档/07-HyperFrames拆层微动态专项验证计划.md +- /Users/yuh/Desktop/项目/文本视音屏生成器/短视频V2规划文档/短视频V2总体目标与阶段规划.md +- /Users/yuh/Desktop/项目/文本视音屏生成器/短视频V2规划文档/执行记录/04-图片生成与高级动态化可行性执行记录.md +- /Users/yuh/Desktop/项目/文本视音屏生成器/短视频V2规划文档/执行记录/06-半自动工作流固化与后续自动化执行记录.md +- /Users/yuh/Desktop/项目/文本视音屏生成器/【包A】视频引擎包/docs/short_video_v2/motion_feasibility_v1.md +- /Users/yuh/Desktop/项目/文本视音屏生成器/【包A】视频引擎包/docs/short_video_v2/director_workflow_v1.md +- /Users/yuh/Desktop/项目/文本视音屏生成器/短视频V2核心需求与完整方案.md +- /Users/yuh/Desktop/项目/文本视音屏生成器/项目交接文档.md + +旧实验重点核对: + +- /Users/yuh/Library/Caches/text-video-plan04-feasibility/hyperframes/project/rainy-messenger-motion/shot-plans/03-narrative-character.json +- /Users/yuh/Library/Caches/text-video-plan04-feasibility/hyperframes/project/rainy-messenger-motion/compositions/narrative-character.html +- /Users/yuh/Desktop/项目/文本视音屏生成器/【包A】视频引擎包/experiments/short_video_v2_phase4/providers/hyperframes_adapter.py + +本计划执行记录: + +- /Users/yuh/Desktop/项目/文本视音屏生成器/短视频V2规划文档/执行记录/07-HyperFrames拆层微动态专项验证执行记录.md + +计划 07 隔离根: + +- /Users/yuh/Library/Caches/text-video-plan07-hyperframes/ + +计划 07 项目证据根: + +- /Users/yuh/Desktop/项目/文本视音屏生成器/【包A】视频引擎包/成片/短视频V2样片/phase7-hyperframes-micro-motion/ + +明确排除:修改 Schema v1、重写包 A/B 正式管线、把 HyperFrames 变成整片时间线或 TTS/字幕内核、重开 MFLUX/DepthFlow/Draw Things/I2V、下载超过计划上限的新模型、加入 BGM/SFX/环境音、自动提交或推送 GitHub、清理旧计划证据,以及处理对动态效果影响很低的问题。 + +--- + +## 建议执行模型(非复制区) + +以下只作为新会话的模型与推理强度建议,不要复制到具体提示词正文中。由于前次测试曾因范围理解错误产生错误结论,本计划各批宁可使用较强模型减少返工。 + +| 提示词/批次 | 建议模型/推理强度 | 原因 | +| --- | --- | --- | +| 背景交代 | frontier/high,例如当前可用 GPT-5.4 high | 历史结论、现行边界和新纠偏范围容易混淆 | +| 批次 1 | frontier/high | 需要审计旧实验、官方技能、版本和测试合同 | +| 批次 2 | frontier/high + 原生图像理解/编辑能力 | 分层资产、眼睑对齐、颈部遮挡决定后续上限 | +| 批次 3 | frontier/high | 涉及 GSAP 层级、确定性、关键姿态与视觉迭代 | +| 批次 4 | frontier/high | 需要判断方法能否迁移到真实项目素材 | +| 批次 5 | frontier/high | 匿名材料、公平对照和质量解释不能误导用户 | +| 批次 6 | frontier/high | 需依据用户反馈最小返修并形成最终架构结论 | + +--- + +## 推荐批次划分 + +| 批次 | 是否与相邻批次合并 | 原因 | +| --- | --- | --- | +| 1. 审计与合同 | 不合并 | 先修正测试问题,避免后续继续验证错误目标 | +| 2. 受控资产 | 不合并 | 静态分层若不成立,动画只会放大瑕疵 | +| 3. 受控实现 | 不合并 | 用统一素材证明 HyperFrames 局部动作上限 | +| 4. 项目迁移 | 不合并 | 独立检验可迁移性,不能被小羊样片替代 | +| 5. 用户门 | 不合并 | 必须冻结匿名候选后等待用户判断 | +| 6. 最终收口 | 不合并 | 依赖用户意见,只做一次有界返修和分类 | + +总提示词模式下,执行会话仍应自动从批次 1 连续推进至批次 5;只有计划写明的用户审片、资源越界、外部服务、破坏性操作或真实阻塞才暂停。 + +--- + +## 新会话背景交代提示词 + +本轮只做计划07的背景熟悉和执行前核对,不实施、不修改代码或文档、不创建执行记录、不生成或编辑图片、不安装或升级工具、不下载模型、不写HyperFrames组合、不渲染视频;请完整阅读 /Users/yuh/Desktop/项目/文本视音屏生成器/短视频V2规划文档/07-HyperFrames拆层微动态专项验证计划.md、本文提示词文档、总体规划、计划04与计划06最终执行记录、motion_feasibility_v1.md、director_workflow_v1.md、核心需求与项目交接文档,再只读检查当前git工作树、旧HyperFrames人物方案JSON与HTML、计划04适配器、现有隔离环境和样片证据;必须调用并完整遵循当前HyperFrames入口技能,按需阅读hyperframes-core、hyperframes-animation、hyperframes-keyframes、hyperframes-cli、hyperframes-creative和media-use的本地说明,同时用当前官方文档或Context7核对发生变化的版本/命令,不凭旧记忆;重点确认前次人物实验主动设置no_segmentation、no_independent_body_motion并禁止眨眼/呼吸等动作,故旧结果只能否定整图推近与弱叠层,不能否定拆层微动态;准确复述计划07的两案例、S/W/L同源对照、动作Brief、素材层、关键姿态、资源上限、用户门、禁止修改范围和PASS/CONDITIONAL/FAIL;若文档与当前代码或官方技能冲突,记录差异但本轮不得修复;子代理仅可用于边界明确的只读代码映射或官方资料核对,主执行者必须复核并结束;完成后只报告目标理解、旧测试误区、当前HyperFrames/Node/Chrome/FFmpeg事实、关键文件、预计风险、工作树保护、是否具备进入批次1的条件,不开始任何执行。 + +--- + +## 批次 1 提示词:现状复核、错误测试审计与实验合同 + +把本段视为可独立执行的计划07批次1合同;先完整阅读 /Users/yuh/Desktop/项目/文本视音屏生成器/短视频V2规划文档/07-HyperFrames拆层微动态专项验证计划.md、本文提示词、总体规划、计划04/06最终记录、motion_feasibility_v1.md、旧人物shot plan与HTML、当前HyperFrames相关技能和最新官方命令,再读取 /Users/yuh/Desktop/项目/文本视音屏生成器/短视频V2规划文档/执行记录/07-HyperFrames拆层微动态专项验证执行记录.md,若不存在则先创建简洁标题、背景与批次1章节;本批节奏固定为只读核对现状→列目标/文件/测试/风险/最小改动→建立实验合同与轻量测试→自审→定向验证→更新记录,不生成/编辑图片、不渲染视频、不修改正式pipeline;必须记录git HEAD/status并保护用户已有修改,不reset/clean/checkout/stash,不碰计划04冻结工程;调用HyperFrames入口技能以及core/animation/keyframes/cli/creative/media-use,按技能要求核对旧pin、npm latest、doctor、Node、Chrome、FFmpeg、磁盘和进程,只对新计划07工程运行只读upgrade check,不在旧工程升级;明确写出旧测试哪些证据仍有效、哪些结论不适用于拆层微动态,禁止再次用“保护原图”排除主体动作;在 【包A】视频引擎包/experiments/short_video_v2_phase7/ 下建立README、layered_motion_brief_v1.md、asset_manifest_v1.json、scorecard_v1.json,并建立只约束路径/命令/模板/媒体/动作证明字段的 test_v2_phase7_hyperframes_micro_motion.py,不实现第二套引擎、不新增依赖、不硬编码小羊名称、一次性哈希或评分答案;合同必须区分镜头层、场景层、主体层和局部状态层,定义案例A受控动物、案例B项目迁移、S/W/L同源对照、关键姿态、视觉失败项、1GiB新增上限和用户审片门;如需创建计划07隔离根,只创建空目录与轻量元数据,不下载模型或浏览器;运行phase7定向测试及必要的现有phase4合同测试,失败先修合同而非放宽门槛;子代理只可做旧代码只读审计、官方资料核对或测试审查,结束前必须收束;完成后更新执行记录,写明实际文件、版本、命令、测试、差异、风险与是否允许进入批次2,并在最终报告列改动、验证和保护情况后自动进入批次2。 + +--- + +## 批次 2 提示词:受控样片资产与静态姿态证明 + +把本段视为可独立执行的计划07批次2合同;开始前完整阅读计划07第4–7、9–12节、当前HyperFrames与ImageGen/Image2技能、批次1执行记录和新模板,确认批次1测试通过,再按只读盘点→写动作Brief和资产策略→生成/编辑→静态叠合与关键姿态审查→一次有界修订→冻结manifest→测试→记录的节奏执行;本批只为案例A准备一个原创、轮廓清楚、头颈与眼睛可分层的9:16小羊或等价动物主图,不写HyperFrames动画、不渲染S/W/L视频;必须先明确身份锚点、主图焦点、镜头/主体/局部状态/环境动作、层级、枢轴、遮挡、证明帧与失败项,再使用当前Codex ImageGen/Image2能力生成或选择master,保存完整提示词和生成账;准备background-clean、subject-cutout、body、head、eyes-closed或eyelid、foreground及asset manifest,优先小步Image2编辑和同图切分,可对适合输入试一次HyperFrames官方remove-background候选,但需先记录是否触发官方模型下载并把全部新增限制在 /Users/yuh/Library/Caches/text-video-plan07-hyperframes/ 与1GiB内,自动去背对动物边缘失败时不得连下其他模型;所有层先叠回master,另制作头部动作峰值、闭眼状态、浅/深底alpha边缘和联系表,逐项检查颈部余量、原头残影、双眼/双脸、背景空洞、毛发硬边、闭眼透视和构图安全区;最多允许一次针对性资产修订,若主图先天不适合可换一张更适合分层的原创图,但必须记录原因和旧图,不能以滤镜、强光或缩小画面掩盖缺陷;不把生成式修改结果伪称为HyperFrames动作,不新增云API/密钥,不修改Schema、包A/B、旧样片或正式路线;子代理仅可做独立图像审查、alpha/哈希/manifest核验,不能并行生成另一套不一致主角,主执行者须统一选择并结束;冻结全部资产尺寸、alpha、SHA-256、来源和用途后运行phase7定向测试,更新执行记录的提示词、循环、资产、静态证明、问题、磁盘、测试与批次结论,静态层未通过不得进入批次3,通过后自动进入批次3。 + +--- + +## 批次 3 提示词:受控样片 S/W/L 实现与技术验证 + +把本段视为可独立执行的计划07批次3合同;开始前读取计划07第4、6–9、12–15节、HyperFrames入口/core/animation/keyframes/cli/creative/media-use技能、批次1–2执行记录和冻结asset manifest,确认静态叠合/点头峰值/闭眼姿态通过;本批节奏为核对输入和当前pin→catalog先行→建立新实验工程与hero frame→S/W/L实现→lint/check/keyframes/snapshot→draft视觉审查→最多一次动作参数修订→high render/probe/decode→记录,不修改正式pipeline;先用词面catalog查询camera push zoom、particle burst、drift/parallax等语义并只安装确实适配的组件,记录tier和结果,puppet/eyelid没有合适组件时按hyperframes-keyframes写最小GSAP关键姿态,不启用未授权的on-device目录索引;新工程不得改写计划04 rainy-messenger-motion,先只读upgrade check,若把0.7.106升到执行时最新稳定patch,只修改新工程package pin且upgrade后check必须通过,否则回原pin并记录;S为同图静态,W为同图整图推近/简单叠层,L必须在不同嵌套层实现克制推近、一次头部点动与回稳、一至两次非等间隔眨眼、低幅呼吸和一种前景/粒子运动,眼睑跟随head,camera/subject/head/eyelid/particle不得竞争同一transform;所有渲染关键动画同步建立在一个paused timeline,有限重复、固定种子、无Date.now/performance.now/未种子Math.random/网络fetch/无限循环,末帧有稳定hold且不黑屏/复位;对真实动画目标分别运行lint、strict check、keyframes JSON、focused shot与第一帧/动作前/峰值/闭眼/回稳/final-minus-hold/最终帧snapshot,不能只证明camera-rig,若动作太弱不可感知须调高而非用晃动代替,若木偶感或接缝明显须降低幅度或修层;只以draft迭代,技术和视觉门通过后再high render,S/W/L同为4–6秒、1080×1920、30FPS、H.264/yuv420p、video-only,执行ffprobe和完整解码,记录命令、退出码、哈希、耗时、RSS、磁盘与孤儿进程检查;最多一次有界参数修订,不无限润色,不加BGM/TTS/字幕/强滤镜;子代理只可做独立动画图谱审查、抽帧/媒体核验或测试复核,不能替代主执行者整合;更新执行记录与manifest后运行phase7和受影响phase4测试,最终报告列S/W/L、动作证明、问题、修订、测试、风险和是否允许进入批次4,通过后自动进入批次4。 + +--- + +## 批次 4 提示词:当前项目镜头迁移 + +把本段视为可独立执行的计划07批次4合同;先读取计划07第7.2、7.3、8–15节、批次1–3执行记录、案例A资产/组合/动作证明与当前项目最近一次成功短视频素材,先只读比较候选镜头的主体轮廓、头颈/叶柄、遮挡、背景可补全性和动作语义,再写选择理由与新的motion brief;本批目标是把批次3已经证明的最小方法迁移到一个真实项目相关镜头,优先西瓜幼苗/叶片清楚的植物镜头,其次人物近景或用户近期其他合适素材,不得用信息图、纯粒子或专门新造的第二张测试图替代迁移;植物路线应拆分background-clean、stem/base、若干关键叶片/藤蔓/果实、foreground与光粒,测试不同相位叶片轻摆、克制推近和一种局部光/露珠变化;人物路线只选一至两项点头/眨眼/呼吸并沿用颈部/眼睑/背景遮挡合同,不机械复制小羊的角度和时间;准备资产时保留原图、提示词、编辑循环、哈希与静态叠合证明,最多一次资产修订,自动去背失败不得下载新的大模型;至少输出W与L,资源允许时补S,所有版本同源、同时长、同规格,L必须含真实主体或植物层运动而非只有camera/particles;严格复用单一paused timeline、父子层、固定种子、finite repeat、离线资产和HyperFrames检查链,运行focused keyframes到真实head/leaf/eyelid目标、proof snapshots、draft/high render、ffprobe、完整解码和孤儿进程检查;明确区分“该素材先天不适合”“资产准备方法失败”“HyperFrames编排失败”和“视觉质量不足”,不可用换术语掩盖;不修改Schema、包A/B正式pipeline、旧成片或short-video-director能力声明,不加入音频;子代理仅可做候选素材只读评估、独立视觉审查、媒体/测试核验,主执行者整合并结束;更新执行记录的选择理由、动作Brief、资产、实际复杂度、循环、动作证明、媒体、性能、问题、测试和批次结论,形成可比较候选后自动进入批次5。 + +--- + +## 批次 5 提示词:匿名审片材料与用户门 + +把本段视为可独立执行的计划07批次5合同;先读取计划07第7.3、9、12、14–17节、全部案例A/B的master、manifest、S/W/L视频、proof snapshots、媒体探测和执行记录,确认没有缺失版本或未解释硬失败;本批节奏为冻结候选哈希→统一媒体规格→制作匿名映射/联系表/动作峰值证明→Codex独立评分→技术复杂度简表→展示用户→记录并暂停,不再改变动作、不重渲、不提前下结论;案例A必须提供同源S/W/L,案例B至少W/L,统一为1080×1920、30FPS、同时长、同编码目标,匿名文件名和画面不得暴露工具/版本/“新版更好”等暗示,private mapping单独保存;联系表只覆盖第一帧、点头/叶片峰值、闭眼、回稳、最终帧,另提供简短技术账说明层数、生成/编辑循环、作者时间、渲染时间和新增磁盘,不以复杂度多证明质量好;Codex按主体动作可感知性、自然度、身份稳定、接缝/遮挡、场景层次、镜头舒适度、工程复杂度和整体可用性评分并封存原始结果,不向用户展示可引导的工具映射或结论;随后必须把最小审片集交给用户,只问:哪版最像画面本身会动、局部动作是否自然且看得见、是否有木偶/接缝/闪烁/晕动、值得用于少量还是多数镜头、哪处应减弱/加强/删除;在用户答复前,把状态写为awaiting_user_review,计划最多CONDITIONAL,更新执行记录并暂停,不能继续批次6或宣称HyperFrames拆层微动态通过;不借用户门处理无关代码、Git提交或新工具;子代理只可做匿名性和证据独立核验,主执行者必须收束;最终报告只列审片入口、候选规格/哈希、盲化保护、已知风险、执行记录状态和等待用户回答的五项问题。 + +--- + +## 批次 6 提示词:最小返修、最终分类与交接 + +把本段视为可独立执行的计划07批次6合同;只有用户已完成批次5审片后才能开始,先读取用户逐项反馈、private mapping、封存Codex评分、全部执行记录和计划07最终等级,准确区分相对偏好与不可接受瑕疵;本批节奏为揭盲→定位用户指出的具体动作/时间点→每案例最多一次最小资产或参数返修→只重跑受影响检查和视频→必要时展示最小复核→冻结final manifest→更新现行文档→回归→最终结论;不得因用户偏好某版就忽略其对断颈、眼睑漂移、木偶感、晕动或动作不可感知的明确意见,不得用自动评分覆盖用户判断;返修优先调整动作幅度、相位、时间、枢轴、遮挡或单一局部状态,不能重做整套主图、换工具、加I2V/滤镜/音频或修改正式pipeline;重跑受影响的lint、strict check、focused keyframes、proof snapshots、high render、ffprobe与完整解码,并更新哈希、耗时、问题前后对照;依据两案例和用户意见给HyperFrames拆层微动态formal_candidate/manual_only/research_only/rejected之一,formal_candidate至少要求受控与项目迁移均通过且用户明确认为L优于S/W、基本可用,只有受控通过或适用面很窄应为manual_only/conditional,等待证据不得写PASS;更新 /Users/yuh/Desktop/项目/文本视音屏生成器/【包A】视频引擎包/docs/short_video_v2/motion_feasibility_v1.md、总体规划、phase7 README与final manifest,清楚标注计划04旧结论只覆盖整图/弱叠层,若建议正式接入则另立集成计划而非本批修改Schema或包A Motion Router;运行phase7定向测试和V2核心矩阵,检查旧样片、服务、工作树、磁盘、进程和隔离根,不自动删除缓存、不提交/推送;子代理只可做最终证据复核、回归失败分类或文档一致性审查,主执行者整合并结束;最后把用户反馈、返修、最终视频/哈希、动作证明、性能、资源、许可、测试、保护情况、最终等级、适用镜头和后续是否另立集成计划完整写入执行记录,并报告改动、验证、剩余风险和计划07是否完成。 + +--- + +## 每批进入下一批的硬性条件 + +1. 批次 1:旧测试误区、案例、模板、资源门和测试合同完整,定向测试通过; +2. 批次 2:静态叠合、头部峰值和闭眼姿态无明显穿帮,资产可追溯; +3. 批次 3:受控 L 版主体动作可独立证明,S/W/L 同规格且媒体通过; +4. 批次 4:至少一个本项目镜头含真实局部层运动,复杂度和失败类型有记录; +5. 批次 5:匿名候选冻结并等待用户,用户答复前不得进入批次 6; +6. 批次 6:用户意见已落实、一次最小返修完成、文档与回归通过、最终分类有证据。 + +若某批硬门失败,应在该批范围内做最小修复;若失败来自素材先天不适合,可以按计划允许的唯一次数更换或修订素材,但不得通过扩张工具、降低门槛或虚构动作证明继续。 diff --git "a/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/\346\211\247\350\241\214\350\256\260\345\275\225/04-\345\233\276\347\211\207\347\224\237\346\210\220\344\270\216\351\253\230\347\272\247\345\212\250\346\200\201\345\214\226\345\217\257\350\241\214\346\200\247\346\211\247\350\241\214\350\256\260\345\275\225.md" "b/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/\346\211\247\350\241\214\350\256\260\345\275\225/04-\345\233\276\347\211\207\347\224\237\346\210\220\344\270\216\351\253\230\347\272\247\345\212\250\346\200\201\345\214\226\345\217\257\350\241\214\346\200\247\346\211\247\350\241\214\350\256\260\345\275\225.md" index 74ae142..995efa1 100644 --- "a/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/\346\211\247\350\241\214\350\256\260\345\275\225/04-\345\233\276\347\211\207\347\224\237\346\210\220\344\270\216\351\253\230\347\272\247\345\212\250\346\200\201\345\214\226\345\217\257\350\241\214\346\200\247\346\211\247\350\241\214\350\256\260\345\275\225.md" +++ "b/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/\346\211\247\350\241\214\350\256\260\345\275\225/04-\345\233\276\347\211\207\347\224\237\346\210\220\344\270\216\351\253\230\347\272\247\345\212\250\346\200\201\345\214\226\345\217\257\350\241\214\346\200\247\346\211\247\350\241\214\350\256\260\345\275\225.md" @@ -406,3 +406,24 @@ - 若产品必须实现雨落、人物呼吸/摆动、旗帜/烟雾等自然运动,另立 I2V 有界验证,并重新取得下载/API/费用授权。 - 详细能力矩阵、内部 request/result、缓存、许可与回落边界已更新到 `docs/short_video_v2/motion_feasibility_v1.md`。 - **计划 04 最终判定:PASS。**允许进入计划 05;正式高级动态候选为零,人工可选仅保留 HyperFrames 信息设计镜头。 + +## 8. 后续决策:MFLUX 路线退役与本地清理(2026-08-12) + +### 8.1 决策 + +- MFLUX 只是本地静态图片后备实验,不具备图片动态化能力;其模型下载未完成,也没有实际成图证据证明优于已通过成片验证的 Image 2。 +- 后续不再把 MFLUX 列为正式、人工或研究推荐路线,不续传模型、不恢复适配器、不重新安装。计划 04 前文保留当时评估过程与 `research_only` 分类,只作历史审计,不再代表现行架构。 +- 当前静态图主路径仍为 Image 2;若 Image 2 暂不可用,由用户提供关键帧或暂停图片阶段。自然主体与场景运动另行讨论真正的 I2V 路线,不能由 MFLUX、DepthFlow 或基础运镜代替。 + +### 8.2 清理范围与证据 + +- 清理前唯一专用根为 `/Users/yuh/Library/Caches/text-video-plan04-feasibility/mflux/`,实测占用 3,893,624 KiB(3,987,070,976 B,约 3.71 GiB),包含独立 venv、uv cache、Hugging Face 未完成模型缓存及空输出目录。 +- 已先确认该根是用户所属的真实目录、不是符号链接,且未发现运行中的 MFLUX Provider;随后只对这一精确根执行递归永久删除,没有触及同级 `depthflow/`、`hyperframes/` 或任何全局缓存。 +- 删除后 `test ! -e` 通过;`mflux-generate`、`mflux-generate-flux1`、`mflux-save` 均不存在;在 `~/Library/Caches`、`~/.cache` 与 `~/.local` 的限定深度复查未发现 MFLUX 遗留。 +- APFS 可用空间由 303,771,596 KiB 增至 306,617,252 KiB,观察到约 2.71 GiB 可用空间回升;该值可能受 APFS 快照、压缩与可回收块影响,故以删除前精确目录占用与删除后路径不存在作为主要清理证据。 + +### 8.3 项目收束 + +- 删除实验适配器 `experiments/short_video_v2_phase4/providers/mflux_adapter.py` 及其本地 Python 字节码,并移除 phase4 清单与测试中的 MFLUX 执行入口;只保留 `evidence/mflux-research-only.json`、计划正文和本记录中的历史事实。 +- 总体规划、现行动态路线文档、计划 04 顶部状态与执行提示词均已明确:MFLUX 条款失效,不得复制执行;计划 05 的相关承接也改为“历史分类、后续退役”。 +- fresh phase4 测试为 14/14 PASS。MFLUX 清理不改变 Schema v1、Image 2、包 B 人声、包 A/FFmpeg 正式主链,也不改变计划 04 原有 PASS 结论。 diff --git "a/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/\346\211\247\350\241\214\350\256\260\345\275\225/07-HyperFrames\346\213\206\345\261\202\345\276\256\345\212\250\346\200\201\344\270\223\351\241\271\351\252\214\350\257\201\346\211\247\350\241\214\350\256\260\345\275\225.md" "b/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/\346\211\247\350\241\214\350\256\260\345\275\225/07-HyperFrames\346\213\206\345\261\202\345\276\256\345\212\250\346\200\201\344\270\223\351\241\271\351\252\214\350\257\201\346\211\247\350\241\214\350\256\260\345\275\225.md" new file mode 100644 index 0000000..419e690 --- /dev/null +++ "b/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/\346\211\247\350\241\214\350\256\260\345\275\225/07-HyperFrames\346\213\206\345\261\202\345\276\256\345\212\250\346\200\201\344\270\223\351\241\271\351\252\214\350\257\201\346\211\247\350\241\214\350\256\260\345\275\225.md" @@ -0,0 +1,311 @@ +# 短视频 V2 计划 07:HyperFrames 拆层微动态专项验证执行记录 + +> 后续产品决定(2026-08-13):本记录转为历史证据,第一版不做图片动态化。原有 `complete / CONDITIONAL / manual_only` 是该实验自身判定,不代表 HyperFrames 仍在活跃工作流中。 + +> 工作区:`/Users/yuh/Desktop/项目/文本视音屏生成器` +> 开始日期:2026-08-12(Asia/Shanghai) +> 当前状态:`complete / CONDITIONAL / manual_only` +> 当前结论:Round 1 的 `FAIL / rejected` 仍为冻结历史;Round 2 仅小鹿“两次眨眼 + 单耳微摆”窄配方获用户接受。植物虽动作明显且像一阵风,但用户确认存在明显重影,最终为 `user_rejected_due_to_ghosting`。普通自然主体生产路由关闭,12 镜广度门不启动 + +## 0. 任务边界与工作树保护 + +- 本轮只验证“静态主图 → 干净背景/身体/头部/眼睑或闭眼状态/前景等独立资产 → 父子层+正确枢轴+遮罩+单一可 seek 时间线 → 无声单镜头”。 +- 旧实验 `no_segmentation=true`、`no_independent_body_motion=true`,并明确禁止眨眼、呼吸与衣袂形变;因此只能否定整图推近与弱叠层,不能否定本轮拆层微动态。 +- 禁止修改 Schema v1、Job Bundle 公共契约、包 A/B 正式管线、旧计划 04 工程、已验收成片与旧审片证据。 +- 不启用 MFLUX、DepthFlow、Draw Things 或 I2V;不加 BGM、SFX、环境音、TTS 或字幕。 +- 不执行 `reset/clean/checkout/stash`,不自动 stage、commit 或 push;所有新产物只落在计划 07 实验根与证据根。 +- 资源门:计划 07 新增下载/缓存默认不超过 `1 GiB`;新模型、第三方 Provider、云/API/费用、破坏性操作须暂停。 + +### 0.1 起始 Git 现场 + +- 分支:`main` +- HEAD:`99e3c14835f769a9aa26bfaccf8e8140204b9a96` +- 工作树开始时已脏;已记录并保护 10 项 tracked 变更与两份未跟踪的计划 07 规划/提示词文档。 +- 其中包含现行 `motion_feasibility_v1.md`、计划 04 退役 MFLUX 相关修改、`config.ini` 及总体规划等用户已有内容;本轮不覆盖、不回退。 + +## 1. 批次 1:现状复核、错误测试审计与实验合同 + +### 1.1 执行目标 + +1. 冻结旧实验真实范围,不再用“先禁止主体动作”的实验判断主体动作能力。 +2. 冻结案例 A/B、S/W/L 公平对照、四层动作、关键姿态、资产/媒体/资源/用户门。 +3. 仅新建轻量合同、模板与定向测试;不生成图片、不写动画、不渲染视频。 + +### 1.2 权威资料与当前事实 + +- 已完整阅读计划 07 正文与执行提示词,并按序核对总体规划、计划 04/06 最终记录、`motion_feasibility_v1.md`、旧 shot plan/HTML、计划 04 适配器、导演工作流、核心需求与交接文档。 +- 已完整读取 HyperFrames 入口、core、animation、keyframes、CLI、creative 与 media-use Skill;并读取本任务所需的 determinism、GSAP、motion principles、init/check/render/doctor/upgrade 及 remove-background 参考。 +- Context7 解析 `/heygen-com/hyperframes`,确认项目 pin 只读检查命令为 `npx hyperframes@latest upgrade --project . --check --json`;最终门为 `check`、真实目标 focused keyframes/snapshot 与渲染媒体校验。 +- npm 当前 latest:`hyperframes 0.7.107`,`dist.unpackedSize=25,686,311 B`,Apache-2.0。 +- 旧项目 pin:`0.7.106`;Node `24.17.0`,npm/npx `11.13.0`,Chrome Headless Shell `152.0.7928.2`,包 A FFmpeg/ffprobe `8.1.2`。 +- 旧隔离根约 `1,142,540 KiB`;数据卷可用约 `291.9 GiB`。计划 07 根已只创建空目录,未下载模型、浏览器或 CLI。 +- 旧 pin 的 `doctor --json` 必要项(Node/CPU/内存/磁盘/FFmpeg/ffprobe/Chrome)通过;总 `ok=false` 仅因本轮不需要的 Whisper/TTS/BGM/Docker 可选项缺失。离线 doctor 把 0.7.106 视为 latest,版本结论以联网 `npm view` 的 0.7.107 为准。 +- 当前无计划 07 HyperFrames/Chrome/FFmpeg 孤儿进程;工作区存在 Codex/CodeGraph/Context7 自身 Node 进程,不属于本计划,不处理。 + +### 1.3 旧测试证据分类 + +**仍然有效** + +- HyperFrames 0.7.106 + Chrome + FFmpeg 能在本机确定性检查、高质量渲染、取消并输出 1080×1920/30 FPS/H.264/yuv420p 无声单镜头。 +- 设计信息镜头可用;整图推近、弱雨层和光罩对自然人物的观感增益不足。 +- 旧适配器的 list argv、`shell=False`、有限 timeout、隔离输出与取消收束证据仍可作工程参考。 + +**不适用于本轮上限判断** + +- 旧人物只有一张完整 ``,没有 background-clean/body/head/eyelid/foreground 独立层。 +- 旧 HTML 只对整图 `scale 1.000→1.024`、雨层整体位移与光罩位移/透明度变化;无头部枢轴、呼吸、眼睑、衣物或前后景独立动作。 +- 因旧 brief 主动禁止拆层与主体动作,“自然人物 HyperFrames 已拒绝”不可外推为拆层微动态失败。 + +### 1.4 本批计划内改动 + +- 新建本执行记录。 +- 新建 `experiments/short_video_v2_phase7/README.md`。 +- 新建 `templates/layered_motion_brief_v1.md`、`asset_manifest_v1.json`、`scorecard_v1.json`。 +- 新建 `tests/test_v2_phase7_hyperframes_micro_motion.py`,仅约束路径、命令、模板、媒体、动作证明与用户门,不实现第二套引擎。 +- 新建空的计划 07 隔离根与证据根;本批不产生图片或视频。 + +### 1.5 验证、问题与批次结论 + +- phase7 定向测试首轮 6 PASS / 1 FAIL;失败仅因测试期待英文 `runtime fetch`,模板实际写为等价且更准确的中文 `运行时 fetch`。仅修正该词面断言后 fresh 7/7 PASS。 +- phase4 合同回归 fresh 14/14 PASS;未修改 phase4 源码、计划 04 冻结工程或旧产物。 +- `git diff --check` 对本批新文件无报错。 +- 已只读筛选案例 B 真实素材:首选 `user-creations/watermelon-growth-20260812/assets/keyframes/shot-003.png`(941×1672),因为主藤、多片掌状叶与叶柄枢轴清楚,适合不同相位叶片轻摆;叶缘茸毛与交叠遮挡为主要风险。 +- 计划 07 新工程的 pin 只读 upgrade check 将在批次 3 scaffold 创建后立即执行;本批未创建 HyperFrames 项目,不在旧工程上执行升级。 +- **批次 1:PASS。** 错误测试边界、两案例、S/W/L、四层动作、资产/姿态/媒体/资源/用户门与定向测试均已冻结,允许进入批次 2。 + +## 2. 批次 2:受控样片资产与静态姿态证明 + +> 状态:`PASS` + +- 输入与边界已核对:本批只为案例 A 准备原创 9:16 受控动物主图、干净背景、透明主体、身体、头部、眼睑/闭眼状态与前景;不写 HyperFrames 动画,不渲染 S/W/L。 +- 已依 ImageGen Skill 生成 1 张原创小鹿主图,并以同一构图为源制作 clean background、闭眼状态与色键主体源;全部提示词已写入 `case-a-controlled/prompts.md`。 +- 确定性资产准备产出 1080×1920 的 `background-clean/body/head/neck-cover/eyes-closed/foreground-left/foreground-right`等独立层;头部以颈根为枢轴,头/身保留重叠区并以 `neck-cover` 处理遮挡缝。 +- 首轮浅/深底 alpha 审查发现细窄洋红边;仅执行计划允许的一次有界修订(`edge-contract 1` + `edge-feather 0.25`),复审无硬洋红边。高反差底上仍有纤细暖色自然毛边,列为渲染像素复查风险。 +- 静态证明已冻结:hero 重组、主体峰值、闭眼峰值、浅/深底 alpha 与接触表;局部差分框分别为 `[259,446,850,1121]` 与 `[392,662,703,794]`,证明变化发生于主体局部而非整帧。 +- 清单校验捕获色键源实际为 941×1671(非预期 941×1672);已如实修正 manifest,且下游所有层均已确定性归一到 1080×1920。 +- phase7 定向测试 fresh 8/8 PASS,包含所有源/分层/证明的实际尺寸、透明性语义与 SHA-256 反查;phase4 回归 fresh 14/14 PASS。`git diff --check` 只报告任务开始前已存在的 `config.ini` 空行,本批新文件无新的空白错误。 +- 计划 07 缓存根仍为 0 KiB;未新增模型、第三方 Provider、云/API 或费用。 +- **批次 2:PASS。** 静态不成立禁止动画的门已通过,允许进入批次 3。 + +## 3. 批次 3:受控样片 S/W/L HyperFrames 公平对照 + +> 状态:`PASS` + +- 输入与边界已核对:只使用批次 2 冻结的小鹿同源资产;于计划 07 独立缓存根创建新工程,不修改旧计划 04 工程。 +- 以当前实际包 `hyperframes 0.7.107` 核对 `init/catalog/check/keyframes/snapshot/render` 帮助,新工程 pin 为 0.7.107;`upgrade --project . --check --json` 返回 `changed=false`。 +- `npx @latest` 首次隔离下载卡在无输出状态,在计划 07 缓存占用约 347 MiB 时终止本轮新建的三个悬挂 npm 进程,保留缓存避免重复下载;随后直接使用已落地的 0.7.107 包入口,未越过 1 GiB 门。 +- catalog 词面查询 `camera push zoom` / `particle burst` / `foreground drift parallax` 均为 `tier=words`;结果含 `push-in`、确定性粒子与 parallax 参照,但无匹配动物颈根枢轴+眼睑状态的组件,故未安装 registry 项,依 keyframes 合同手写最小 GSAP。未启用 on-device 索引。 +- 工程完全位于 `/Users/yuh/Library/Caches/text-video-plan07-hyperframes/project/short-video-v2-micro-motion`;GSAP 3.14.2 冻结为本地离线资产(SHA-256 `c174bfce…d8280`),运行时无 CDN/fetch。只读复用旧隔离根的 Chrome Headless Shell 152.0.7928.2,旧工程未改动。 +- S 为同图静态;W 为同图 1.000→1.035 整图推近+显式光粒;L 以独立 `camera/scene/body/head-rig/eyes-closed/foreground/motes` 完成同幅推近、2.6° 一次点头及回稳、1.30s/3.62s 两次非等间隔眨眼、两段低幅呼吸及前景/显式光粒。所有动作在每组合单一 paused timeline 内,有限 repeat,无随机、时钟或网络依赖。 +- strict check 在 69 个时间/转场样本上 0 error / 0 warning / 0 issue;focused keyframes 分别证明 `#head-rig`、`#eyes-closed`、`#body`,且 head 报告明确为 `0°→2.6°→0°`。 +- 首轮可寻址抽样在组合边界 5.000s 触发 end-exclusive 黑帧;这不是视频末帧故障。终帧证明改取 30 FPS 实际最后一帧 4.966s/帧 149,S/W/L 均有图且未复位。为消除 CLI 对回稳起点的模糊静态推断,把等值 `to` 重写为显式 `fromTo`;数值未变,不计入动作参数修订。 +- draft W/L 经完整解码与接触表自审:L 主体动作可感知,未见断颈、双脸、眼睑漂移、背景空洞或硬毛边;未使用计划允许的一次参数修订。 +- high S/W/L 均为 5.000s、1080×1920、30 FPS、H.264/yuv420p、video-only,`ffprobe` 通过且 `ffmpeg -f null -` 完整解码通过。SHA-256:S `4b8b4870…30e98e6d`,W `f9d8399d…6322d2`,L `4284f7ea…b66ec8`。 +- high 渲染节点耗时/RSS:S 6.06s / 722,141,184 B,W 9.86s / 737,492,992 B;L 产物于 17:58:48 正常落盘,当次串行命令输出在采集中被截止,故不伪造 L 的独立 time/RSS,以媒体完整性为成功证据。 +- 计划 07 隔离根当前 458,944 KiB,低于 1 GiB;无新模型/第三方 Provider/云/API/费用。本计划所属渲染/Chrome 孤儿进程检查为空。 +- phase7 定向测试 fresh 8/8 PASS,phase4 回归 fresh 14/14 PASS。 +- **批次 3:PASS。** 受控 L 版已对真实主体目标独立证明,S/W/L 同源、同时长、同规格且媒体通过,允许进入批次 4。 + +## 4. 批次 4:当前项目真实素材迁移 + +> 状态:`PASS` + +- 输入与边界已核对:首选真实项目素材 `watermelon-growth-20260812/assets/keyframes/shot-003.png`(941×1672),主藤、多片掌状叶与叶柄枢轴清楚,适合不同相位轻摆;叶缘茸毛与交叠遮挡为主要风险。 +- 该素材来自当前项目近期技术成功的用户创作,任务记录仍待用户验收;本记录不把它误称为“已验收成片”。 +- 输入原图 SHA-256 为 `b9b489b0…b8360ec`;仅对这一张原图做 clean plate 与紫红色键主体源各1次编辑,完整提示词与操作账写入 `case-b-project-migration/prompts.md`;未生成第二张替代镜头。 +- 静态分层冻结为 `background-clean/subject-cutout/stem-base/leaf-top/leaf-left/leaf-right/foreground/seam-covers`,均已归一到1080×1920;顶叶、左叶与前景各有叶柄或根部枢轴,右叶静态保护西瓜遮挡。 +- 首轮宽掩码在峰值姿态露出横切暗缝;依计划仅使用1次有界资产修订,收窄叶簇、改为内部硬分区并加局部 `seam-covers`。hero 重组、叶片峰值、浅/深底 alpha 与接触表复审未见硬缝、背景空洞、双主体或西瓜漂移。此后未再修改资产。 +- S 为同图静态;W 为同图旧式整图推近;L 为同组 `camera > scene > background/stem-base/leaf-rigs/seam-covers/dew/motes`:镜头 1.000→1.028,顶叶 `0°→0.7°→-0.25°→0°`,左叶 `0°→-0.28°→0.16°→0°`,前景低幅轻摆,两枚显式露珠只做1次透明度脉冲,3枚光粒为有限显式位移。叶片主体动作不由整图、滤镜或粒子代替。 +- 每个组合仅一条 paused GSAP timeline,无 `repeat:-1`、随机、时钟或运行时网络。focused keyframes 分别锁定 `#leaf-top` 与 `#leaf-left`,数值、相位与回稳皆与 brief 一致;未使用动作参数修订配额。 +- HyperFrames 0.7.107 strict check:lint 0 error/0 warning,runtime 0 issue,layout 0 issue,53个时间样本,transition sample 丢失0。可寻址 snapshot 覆盖 0/0.7/1.55/2.55/3.45/4.2/4.55/4.966s;末帧取实际帧149,三版均无黑屏或复位。 +- draft L 耗时/RSS 为 28.21s / 272,908,288 B;high S/W/L 分别为 6.50s / 728,137,728 B、9.98s / 732,889,088 B、30.12s / 732,119,040 B。 +- high S/W/L 均为 5.000s、1080×1920、30 FPS、150帧、H.264/yuv420p、video-only,`ffprobe` 通过且 `ffmpeg -f null -` 全片解码通过。SHA-256:S `4f94b2ea…09c5501`,W `60600972…774d25`,L `470ab757…3594de`。 +- 资产清单已冻结为 `frozen_batch_4`,所有源、分层、证明与视频均由测试反查 SHA-256、尺寸与透明性语义。phase7 fresh 9/9 PASS,phase4 回归 fresh 14/14 PASS。 +- 计划 07 隔离根当前 514,636 KiB,低于1 GiB;未新增模型、第三方 Provider、云/API或费用,本计划渲染与 Chrome 孤儿进程检查为空。 +- 自审结论:真实素材已生成可比的 S/W/L 候选,局部动作方法技术成立;残余风险是亚度叶片转动可能观感过弱,以及稠密遮挡区可能仅在连续播放中显出细线。二者均留待批次 5 匿名审片,不于此先行裁决。 +- **批次 4:PASS。** 真实项目迁移、拆层微动、同源 S/W/L 与媒体硬门均已完成,允许进入批次 5。 + +## 5. 批次 5:匿名对照冻结与用户审片门 + +> 状态:`awaiting_user_review` + +- 输入与边界已核对:只使用批次 3/4 已冻结的两组 S/W/L;不再调整资产、动作或渲染参数,不向用户暴露 S/W/L 映射。 +- 已于 `review-batch5/public/case-01` 与 `case-02` 各冻结 3 份同源候选;文件名仅为 `candidate-01/02/03`,画面、文件名和公开指引均不包含工具、S/W/L、新旧或质量暗示。 +- 为避免审片哈希与源 manifest 直接对照揭盲,6 份公开 MP4 统一做无转码、无定向元数据的中性重封装;画面码流与帧数不变,未重渲、未调参。 +- 候选均为 5.000s、1080×1920、30 FPS、150帧、H.264/yuv420p、video-only;6份审片副本均再次完整解码通过。 +- 案例 01 同时点接触表覆盖首帧/眼睑状态/头部峰值/回稳/最后帧;案例 02 覆盖首帧/顶叶峰值/露珠与前景状态/回稳/最后帧。六份接触表已独立查看,无文字或标签泄露映射;其局限已写明:须以连续播放判断微动与暂态接缝。 +- 私有映射位于 `review-batch5/private/mapping.json`,Codex 按8项0–3固定量表的原始分数位于 `private/codex-scores.json`;两者均在展示前封存,不以自动分数替代用户判断。 +- 简短技术账已按案例记录独立层数、生成/编辑循环、作者落盘时间窗、渲染耗时与磁盘,并明示“复杂度不证明质量”。 +- 公开候选 SHA-256:案例 01 依次为 `88a17f87…24b92b83`、`06ddd711…8b9accc5`、`cdfe66d6…69a0bea4`;案例 02 依次为 `37e9b09a…86c7cb30`、`310d4c61…6b87eef5`、`73ce492d…aec8160`。公开视频/接触表/指引/技术账及私有映射/评分的完整哈希均冻结于 `review-manifest.json`,逐项 `shasum -c` 全部通过。 +- 首轮冻结校验的 `jq` 流表达式因运算优先级未展开为校验行,命令当即失败且未改文件;改为先显式合并 public/private 数组后重跑,全部哈希通过。 +- phase7 合同新增审片冻结、匿名命名、映射/评分封存与实体哈希反查,fresh 10/10 PASS;phase4 回归 fresh 14/14 PASS。 +- 最终磁盘:计划 07 隔离缓存 514,636 KiB,实验证据根 147,784 KiB(其中审片副本 29,788 KiB),合计约 646.9 MiB,低于1 GiB。HyperFrames render/Chrome 孤儿进程检查为空。 +- `git diff --check` 仍只报告任务开始前已存在的 `【包A】视频引擎包/程序文件/config.ini:7` 文件尾空行;开始时的 tracked 修改状态仍在,未执行 reset/clean/checkout/stash/stage/commit/push。 +- **批次 5 当前门状态:`awaiting_user_review`。** 最小审片集已冻结,当前结论严格保持 `CONDITIONAL`;用户答复下列5问前,不进入批次 6: + 1. 哪版最像画面本身会动? + 2. 局部主体或叶片动作是否自然且看得见? + 3. 是否存在不可接受的木偶感、接缝、闪烁或晕动? + 4. 这种方法值得用于少量镜头,还是多数普通镜头? + 5. 哪一处动作应减弱、加强或删除? + +## 6. 批次 6:用户反馈、揭盲、最终分类与交接 + +> 状态:`PASS`(本批的“PASS”仅表示反馈已准确落实、分类与回归已完成;实验路线总结论为 `FAIL / rejected`) + +### 6.1 输入与模式 + +- 输入为用户对批次 5 五问的完整答复;匿名媒体、私有映射与 Codex 评分仍是用户观看时的冻结版,未预先泄露。 +- 已重新读取当前 HyperFrames 入口 Skill,并依 `short-video-director` 选择 `Resume` 模式:从第一个未满足硬门(用户意见落实)续接,不重做批次 1–5。 +- 导演工作流把“用户认为基础动态整体不可接受”列为强制停止信号。用户认为当前效果不佳,并明确说明无必要给出某一动作应减弱/加强/删除的返修意见。因此本批不臆造参数、不消耗“每案一次”的返修配额、不重渲,保留原媒体作为失败证据。 + +### 6.2 用户反馈原意归类 + +1. “最像画面本身会动”的是案例 01 候选 01,但动起来生硬。案例 01 候选 02 只有轻微逐步放大,候选 03 看不出变化。案例 02 候选 01 有放大,候选 02 没有变化,候选 03 也只感觉画面变大。 +2. “局部主体或叶片动作看不到”。 +3. 仅案例 01 候选 01 有明显动作,但木偶感较明显;未明显感受到接缝、闪烁或晕动。 +4. 无论木偶感、放大或不变的版本,均不应用于多数普通镜头;只有当产品明确是“静态图片视频”时,不变的静图才可作为多数镜头。 +5. 因当前动作视频整体效果不佳,用户认为没有必要指定哪一处动作应减弱、加强或删除。 + +### 6.3 揭盲与 Codex 评分校正 + +- 案例 01:候选 01=`L`,候选 02=`W`,候选 03=`S`。用户正确区分了真拆层局部动作、整图推近和静止;但 L 的木偶感构成计划 07 视觉失败项。 +- 案例 02:候选 01=`W`,候选 02=`S`,候选 03=`L`。L 确有 `#leaf-top` / `#leaf-left` focused keyframes,但用户只感知到画面变大,说明技术姿态证明不能代替最终像素的动作可感知性。 +- 封存 Codex 评分对案例 A L 的自然度/木偶感与案例 B L 的主体动作可感知性判断过于乐观。用户视觉判断优先,已在 final manifest 中明示覆盖自动分数。 + +### 6.4 最终失败分类 + +- **素材先天不适合:否。** 受控动物是为分层选的原创素材,植物是当前项目真实迁移素材;两者静态姿态门均通过。 +- **资产准备失败:否。** clean background、透明主体、头部/身体/眼睑或叶片/基座/前景/遮挡补片均可追溯,浅/深底 alpha 与静态重组通过。 +- **HyperFrames 编排失败:否(技术层)。** 单一 paused timeline、父子层、枢轴、遮挡、有限动作、strict check、focused keyframes、snapshot、high render、ffprobe 和完整解码均成立。 +- **视觉质量与产品价值失败:是。** 受控 L 动作可感知但生硬/木偶;项目 L 局部动作不可感知;用户否定用于多数普通镜头的价值。 +- 依计划 07 第 15 节 FAIL 条件(“主体动作不可感知或木偶感明显”),总结论为 `FAIL`;主体拆层微动态分类为 `rejected`,不建立正式 Provider,不另立集成计划。 +- HyperFrames 路线/数字/档案状态等信息设计用途仍是 `manual_only`;Image 2 + 包 B + 包 A/FFmpeg 已验收主链不受影响。 + +### 6.5 产物、性能、资源与验证 + +- 未返修/未重渲,故最终 S/W/L 就是批次 3/4 冻结媒体,完整哈希见 `phase7-hyperframes-micro-motion/final-manifest.json`(SHA-256 `d63a67e9…43cd342b`)。案例 A/B 六份原媒体哈希逐项复核通过,且六份再次 `ffmpeg -f null -` 全片解码通过。 +- 性能不变:案例 A high S/W 为 6.06s/722,141,184 B 与 9.86s/737,492,992 B,L 当次独立 time/RSS 未捕获;案例 B high S/W/L 为 6.50s/728,137,728 B、9.98s/732,889,088 B、30.12s/732,119,040 B。 +- 最终资源:计划 07 缓存 513,092 KiB,证据根 146,368 KiB,审片副本 29,788 KiB;缓存+证据约 644.0 MiB,低于1 GiB。未新增模型、第三方 Provider、云/API或费用。 +- V2 完整矩阵:`python3 -m unittest discover -s tests -p 'test_v2_*.py' -v` fresh 运行 116 项,114 PASS,2 项仅限计划 05 隔离副本的长故障注入按合同 SKIP,0 FAIL。其中 phase7 11/11 PASS。 +- HyperFrames render/Chrome 孤儿进程检查为空。`git diff --check` 只报告任务开始前已存的 `config.ini:7` 文件尾空行。 +- 未修改 Schema v1、Job Bundle 公共契约、包 A/B 正式管线、计划 04 冻结工程或已验收成片;未执行 reset/clean/checkout/stash/stage/commit/push。 + +### 6.6 最终文档交接 + +- `docs/short_video_v2/motion_feasibility_v1.md`:将主体拆层微动态从 `re-evaluation_pending` 改为 `rejected`,保留信息设计 `manual_only`,写明技术成立与视觉失败并不矛盾。 +- `短视频V2总体目标与阶段规划.md`:更新动效路由、工具表、阶段 6 与计划 07 状态,明确不进入集成。 +- `experiments/short_video_v2_phase7/README.md`:写入最终 `FAIL / rejected`、用户反馈、不返修理由与安全边界。 +- `final-manifest.json`:冻结揭盲、用户意见、自动评分校正、最终媒体/哈希、失败分类、资源和回归证据。 +- **批次 6:PASS;计划 07:完成,总结论 `FAIL / rejected`。** + +## 7. 用户复核后重开:第二轮有界诊断与返修 + +> 状态:`reopened_after_user_methodology_feedback` + +### 7.1 重开依据 + +- 用户在首轮结论后继续逐项复核候选与联系表,将最终像素准确归为三类:有动作但木偶、只有整图放大、基本无变化;并进一步提出应检查姿态数量与姿态本身,而不能只看最终视频文件存在与否。 +- 这份补充意见构成批次 6 所缺少的明确返修目标,故推翻 6.1 中“不消耗返修配额、不重渲”的当时依据。首轮 `FAIL / rejected` 仍作为 Round 1 历史事实冻结,不再作为尚未测试改良姿态表示与感知门槛后的最终能力上限。 +- 本轮仅使用计划 07 既有的“每案一次”有界返修:保留旧 S/W/L、旧匿名映射、旧 manifest 与哈希;新增 Round 2 组合、媒体、诊断和 manifest,不覆盖前证。 + +### 7.2 首轮方法审计结论 + +- 批次 5 的五格不是五张生成式关键帧,而是从每条 5 秒、30 FPS、150 帧 MP4 中抽出的五个时点。准确链路是“少量分层 PNG + 单一 paused GSAP 时间线逐帧求值 → 150 帧 MP4 → 五时点抽帧”。 +- 用户对证据失效的判断成立:全画面五联帧每格仅 216×384,案例 B 顶叶峰值缩小后约 1.1px,左叶约 0.28px;这类联系表无法证明局部微动,且与 L 同幅的 2.8% 全局推近会把叶片信号完全压住。 +- 案例 A 的木偶感并非输出帧率不足。原 L 已有 150 个连续输出帧,但“点头”实为同一张头颈平面作 2.6° Z 轴侧倾;头、身体、静止颈部补片互为平级并在颈根大幅重叠,身体又整卡纵向缩放。增加更多时间采样不能补出真实俯仰、颈部弯曲与耳部跟随。 +- 原 strict check 的通过只证明 lint/runtime/layout 等合同成立,不能证明动作在最终像素中可感知或自然;原静态 peak 亦不得替代真实运行时峰值。 + +### 7.3 Round 2 修订合同 + +- 案例 A:取消整图推近、粒子证明与整身拉伸;修正父子层、颈根枢轴与非对称动作节奏。优先验证新增的真实低头姿态状态;若其身份、配准或局部编辑边界不合格,则不得强用,并明确记录资产门失败。 +- 案例 B:镜头固定,叶片动作以最终交付尺寸的叶尖位移为门槛;顶叶、左叶与前景错相且非对称回稳,露珠须跟随叶片父层,粒子不得作为主体动作证据。 +- 两案均须先产出 camera-frozen 的局部动作版本;技术检查之后,以 1×连续播放、局部 ROI 峰值条、实际运行时峰值、首帧差分/运动能量和 seek 重复性共同验证。全画幅五格只作构图辅助,不再承担局部动作证明。 +- Round 2 仍不得修改 Schema v1、Job Bundle 公共契约、包 A/B 正式管线、计划 04 工程或已验收成片;不得重启禁用 Provider,不加入音频,不提交或推送。 + +### 7.4 外部成熟方法输入与本地边界 + +- 依用户建议,补查 Live2D 父子 deformer、Spine mesh/weights、Rive bones/mesh 与 Adobe Puppet/Mesh Warp 的官方方法。共同原则为:使用同一纹理和同一拓扑,头骨等刚性区与颈部/耳尖等柔性过渡分开,以权重或网格变形,而非多张独立人像直接淡化。 +- 本机无 Rive、Spine、Live2D、After Effects 或 Blender 可用作者工具/运行时;本轮不新增下载、付费软件或 Provider。最小本地类比实现选择 macOS SpriteKit `SKWarpGeometryGrid` 离线烘焙同拓扑状态,仍由 HyperFrames/GSAP 单时间线编排。 +- 经有界验证后判废的路线:独立生成中间低头姿态(身份/背景漂移)、姿态交叉淡化(双耳/双脸重影)、全分辨率及 1/8 粗尺度双向光流(耳缘/额顶晕边)、九态整头网格压缩(用户指出头骨变扁,且 10Hz 阶梯)。 + +### 7.5 受控动物最小返修 + +- 依用户逐帧意见,低头动作整体判废,不以增加关键姿态数量美化错误形变;保留用户认可的两次眨眼。 +- 新主体动作为画面左侧耳尖一次低幅摆动:24×40 SpriteKit 网格,耳根为锚点,峰值 2.4°,仅耳轴权重区变形;另一只耳、脸、头骨和颈部的输出像素不变。 +- 第一版多 `` 同时 opacity set 产生 0.4s 缺头,判废;第二版逐帧换 `src` 只有约 15 FPS 有效更新,判废。最终改为预解码状态图集 + 单一 canvas 同步绘制;耳部 ROI 在 28 个相邻输出间隔中 26 个有动作,脸/颈 ROI 为 0 个。 +- seek 重复验证:`2.2→1.6→2.2→1.6s` 的两组同时点 PNG 分别字节级同哈希;`check --strict --at-transitions` 为 0 errors / 0 warnings。独立视觉复核认为无明显阶梯、闪断、脸颈牵连或眨眼残影,可进入用户复核,但非生产 PASS。 +- high 媒体:5.000s、1080×1920、30 FPS、150 帧、H.264/yuv420p、无音轨,整片解码通过;SHA-256 `2a1448e0…a96910ba7`。 + +### 7.6 真实项目植物返修 + +- 删除原 L 的 2.8% 全局推近、粒子与整体干扰;顶叶、左叶和前景叶分别使用错相多段姿态,露珠移入顶叶父层。 +- 最终实测局部特征位移约为:顶叶 14px、左叶 6–7px、前景 6–8px;背景天空差异 0.435,远低于 Round 1 整体运镜的 5.859,证明没有以推近冒充叶片动作。 +- 独立视觉审查未见明显暗缝、重影、错层或全局闪烁,但小尺寸五格仍会将位移压缩到约 1px;故 Round 2 审片同时附原速全画面和同源局部慢放。 +- high 媒体:5.000s、1080×1920、30 FPS、150 帧、H.264/yuv420p、无音轨,整片解码通过;SHA-256 `ded3f096…b4047c`。 + +### 7.7 Round 2 审片门 + +- 冻结新证据树 `phase7-hyperframes-micro-motion/round2-reopen/`;`round2-manifest.json` 状态为 `awaiting_user_review`,且显式引用未变的 Round 1 manifest SHA-256 `d63a67e9…342b`。 +- Round 1 case-A 曾被后续系统 Python 3.9/Pillow 11.3 误重跑覆盖 11 张 PNG;已用原 Python 3.12/Pillow 12.2/12.3 产物精确恢复。恢复后 4 source + 9 layers + 6 proofs + 3 videos = 22/22 哈希匹配,Round 1 asset manifest、final manifest 和六条 S/W/L 媒体均未改。 +- Round 1 已经完成同源 S/W/L 公平对照;Round 2 只复核受影响的两条 L2,不重复展示已判为放大/静态的 S/W。局部慢放是同一 high 媒体的裁切时序证明,不计为新候选。 +- 用户回复前,最高等级仍为 `CONDITIONAL`,不得进入 Round 2 最终分类或声称主体拆层微动态通过。 + +### 7.8 用户复核、问题根因与有界返修 + +- 用户确认两案均比 Round 1 更像画面中的事物在动;小鹿眨眼与耳部动作可见且总体自然,建议耳部略强;植物动作可见,但移动前、移动中和移动后的同源叶片同时存在,形成重影。用户认为若瑕疵解决,此路线可考虑多数普通镜头。 +- 植物故障并非 GSAP 残帧或编码拖影,而是资产所有权错误:动态叶片与静态 `leaf-right` / `seam-covers` 共享大量同源 RGB,`leaf-top` 与 `leaf-left` 也有异相重复。旧位层不动,故叶片一移便显出过去姿态。 +- 修订严格限于两项:小鹿单耳同拓扑形变峰值由 2.4° 增至 2.7°,不改眨眼、脸颈或节奏;植物动作参数不变,只按 `foreground > leaf-left > leaf-top > leaf-right > stem residual` 重建唯一像素归属,并删除运行时静态 seam cover。 +- 植物生成器现将 6 份冻结输入 expected/actual SHA-256 纳入 hard gate,输出保存后重新载入再验;alpha 0–255 全部 256 个阈值均为 0 holes、0 extras、0 pairwise overlap。五层零姿态重组与 canonical `subject-cutout` 的 RGBA 最大误差为 0。此结论只证明层资产恒等,不冒称完整浏览器首帧等于 `master.png`。 +- 独立逐帧视觉门:小鹿 high 150/150 帧 PASS,耳尖约抬 5–6px,目标耳峰值 ROI 差 7.457/255,另一耳、脸和颈仅编码噪声;两次眨眼完整,无空头、双头、跳帧或耳根裂缝。植物 150/150 帧 PASS,顶/左/前景叶峰值位移约 13.45/6.96/7.99px,无旧位叶片、洞、断根、黑边、色边或一帧闪缝。 +- 小鹿显式 DOM 状态层在乱序序列 `0→1.70→2.20→1.70→0→2.20→1.70s` 下,相同时间的 PNG SHA-256 完全一致;植物以唯一 paused GSAP 时间线求值。HyperFrames 0.7.107 strict check 均为 0 error / 0 warning / 0 layout issue。 +- 两条 high 媒体均为 5.000s、1080×1920、30 FPS、150帧、H.264/yuv420p、无音轨并全解码;SHA-256 分别为小鹿 `d301e981…4cea6ba`、植物 `2958770d…02ed02`。 + +### 7.9 配方冻结、成熟路线与有限可行性门 + +- 已建立 `docs/short_video_v2/motion_experiment_playbook_v1.md`,配方状态固定为 `draft → technical_candidate → replay_verified → user_accepted → production_recipe/rejected`。每条必须冻结输入/输出哈希、层级/枢轴/所有权、时间线、运行时/argv、验证、成本与失败边界。 +- 新增自包含证据包 `round2-reopen/revision-batch8/`:包含两份 composition、本地 GSAP、全部运行资产、SpriteKit 源、唯一所有权生成器、诊断、配方 manifest、high 成片与完整哈希账。离开原缓存后 cold strict check 仍为 0/0,并可独立渲染两条 150 帧 draft;故当前状态为 `replay_verified_awaiting_revision_review`,尚非生产配方。 +- 依当前官方资料比较成熟工具:若未来批准新作者工具与费用,Spine Professional 的 weighted mesh 与绝对时间 `Animation.apply(..., time)` 最适合先作有界验证;Rive 技术上顺,但 Editor 为 online-first,与严格无云边界不合;Live2D 的随机 seek 与通用视频生成器许可均需先确认;After Effects Puppet Pin 只适合已有订阅时烘焙少量重点镜头。本轮未下载、安装或付费。 +- “多数镜头可用”不由两例推断。后续须一次性执行 `3 个真实视频 × 每个 4 类镜头 = 12 镜` 广度门:至少 9/12、每视频至少 3/4、2D eligible 通过率至少 80%、每种拓扑至少 2/3,且用户无硬瑕疵;每镜时间盒 30 分钟+一次 15 分钟返修,45 分钟即停。 +- 走路、转身、手物交互、口型、复杂表情、长发衣物大幅运动、水烟火、完整风场耦合、新视角/新表面列为 `semantic_motion_required`,不得继续在当前 2D 路线逐动作硬做。 + +### 7.10 Round 2 返修审片门 + +- `review-batch8` 只含用户点名的两条受影响 high 成片及定位联系表,不重复 Round 1 S/W/L,也不把联系表当作连续动作证明。 +- 审片前最高只可称 `narrow replay-verified technical candidate`;现行正式路由与 Round 1 final manifest 均未改。 +- 用户只需复核:小鹿耳部略增后是否仍自然;植物旧位重影是否消失,以及是否新增洞、断根、接缝、闪烁或色边。回复前不升级为 `user_accepted`,亦不进入 12 镜广度门。 + +### 7.11 批次 8 用户复核与植物最终修订边界 + +- 用户明确回复:小鹿“通过,正常”,故案例 A 升为 `user_accepted`,接受媒体 SHA-256 为 `d301e981…4cea6ba`;批次 9 不改它、不重渲、不重复展示。 +- 植物未被否定,而是“幅度偏小,尚无法确认”;用户要求增大摇动并让不止一片叶子同时动。该回复构成明确的有界例外返修,但 `automatic_batch10_forbidden=true`;若此次仍不接受,将停止继续调这一株植物。 +- 先作一次大幅刚性整层旋转诊断;虽然 HyperFrames lint/strict 无通用工程错误,可视峰值出现严重斜缝和西瓜纵切缝,故该 L3 当场判废,未渲成 high,也不对用户展示。此事证明:自动 strict 不能代替视觉缝隙门。 +- 根因不是关键帧数量,而是批次 8 的叶层虽像素唯一,却含多个语义不连通的残片;整层旋转会将残片一并拉开。故批次 9 必须转为“语义连通独立层 + 根部锁定局部网格”,不再用整层加大角度。 + +### 7.12 批次 9:植物语义拆层、局部网格与最小审片集 + +- 新资产将主体分为静态主茎、西瓜/其他残余、右叶,以及语义连通的顶叶、左叶、右下前景株三个活动层。alpha 0–255 共 256 阈值的 holes/extras/pairwise-overlap 均为 0,动层皆语义连通,零姿态 RGBA 误差为 0。 +- 三个动层均以本地 SpriteKit 网格烘焙 31 个同拓扑状态,根部锁定误差为 0,相邻叶尖理论步进不超过 1.5px。HyperFrames 仅用一条 paused GSAP 时间线以显式状态切换推进,每层每时刻只有一张可见,无交叉淡化、计时器、随机、网络或运行时补缝。 +- 顶叶、左叶、右下前景株峰值位移分别约 21.9/16.0/16.0px;帧 50–65 共16帧有两组主叶同时达约 10px 可见位移,峰值帧 53/59/73 错开,西瓜与相机静止。 +- 乱序 seek 同时点截图哈希一致;150帧状态扫描中每层唯一状态、首末回稳。high 成片 150/150 帧视觉门 PASS,未见旧位重影、断根、运动开缝、闪烁、方块或色边;媒体为 5.000s、1080×1920、30fps、150帧、H.264/yuv420p、无音轨,完整解码通过,SHA-256 `94a59bd1…a254822`。 +- 已冻结 `revision-batch9/` 自包含工程、配方 manifest、high 成片与完整哈希账;`review-batch9/` 只公开案例 02 这一条成片及定位联系表。小鹿保持 `user_accepted`,植物当前仅为 `replay_verified technical candidate`。 +- 独立将冻结工程复制到全新临时根后,cold strict check、双序 seek、150帧状态扫描与 high 重渲均 PASS;冷重渲 MP4 与冻结审片成片逐字节同 SHA-256。五份历史诊断 JSON 仅保留原 cache 绝对路径作 provenance,它们不是 composition、check、seek 或 render 的运行依赖,已列为低级非运行时提示。 +- **审片前门状态(历史):`awaiting_plant_revision_review`。** 当时不将植物升为 `user_accepted`,不进入 12 镜广度门,不修改正式集成结论。 + +### 7.13 批次 9 用户终审与 Round 2 收口 + +- 用户对最终植物候选的三点回复为:“动作明显”、“像一阵风”、“明显重影”。因此动作可感知性与多叶风感同步门均记为 PASS,但瑕疵门为 FAIL,案例 B 最终状态是 `user_rejected_due_to_ghosting / rejected`。 +- 批次 9 冻结工程仍保留 `replay_verified_technical_candidate`:它证明自包含冷重放、乱序 seek、单时间线、媒体规格与多叶位移均成立;但技术可复现不等于视觉可接受,该状态不得升格为 `user_accepted`。 +- Codex 的 high 逐帧视觉门曾记为 PASS,而用户在连续成片中明确看见重影;故该自动视觉门是假阴性。此处以用户视觉门为最终优先级,不以 alpha 所有权、根部锁定、像素哈希或自动帧扫描覆盖人眼审片结论。 +- 用户回复后对冻结 high 成片作了解码帧复核,已在 0-based 帧 38–90(约 1.27–3.03s)复现结构性双轮廓,帧 50–66(约 1.67–2.20s)最明显。它不是单纯的 H.264 拖尾:解码单帧本身已有旧/新轮廓,且 HTML 每层每时刻只有一个 state、没有交叉淡化。 +- 批次 9 终审后的阶段性取证以中高置信度确认了一项重要贡献机制:`leaf-top`、`leaf-left`、`foreground-plant` 虽各自满足唯一像素所有权、8 连通与零姿态重组,却仍是包含多片叶和枝杈纹理的粗语义簇。它们被独立形变后,会彼此穿插,并与静态主茎或右叶形成结构性错叠,视觉上读作旧/新双轮廓。既有自动门把“连通、互斥”误当成“同一形变语义”,也没有检查活动扫掠区内的动态—动态、动态—静态结构关系;7.14 的最终因果例外进一步限定此机制并非已证明的唯一或完整根因。 +- 31 个预烘焙状态的硬切换是次因:全片 149 对相邻帧中有 77 对保持同一组合 state,会加强阶梯与滞留感,但并非双像主因。此取证只说明工程机制;因用户未提供具体注视区域或截图,不断言用户注意的恰是哪一片叶。 +- 计划 07 最终等级为 `CONDITIONAL`,分类为 `manual_only`:只保留已被用户接受的小鹿眨眼+单耳窄配方,且不外推至其他动物、人物、植物或多数普通镜头。 +- 12 镜广度门保持 `not_run`,最终处置为 `cancelled_due_to_failed_prerequisite`,失败前置是 `case_b_user_visual_gate`。`automatic_batch10_forbidden=true`,不再对同一株植物自动调参;正式集成仍为 `not_reopened_for_production`。 + +### 7.14 用户授权的最终因果例外与永久终止 + +- 本次一次性排除试验登记为 `user_authorized_final_causal_exception`;它不是批次 10、Round 3 或新候选,不改变既有 `complete / CONDITIONAL / manual_only`,也不重开普通自然主体生产路由或 12 镜广度门。 +- 唯一有效首跑采用 single-owner `36x48` unified mesh:G4 动作门 `PASS`,但 G5 `visible-strain` 为 `HARD FAIL`;量化值为 `sigma_min=0.435627`、`sigma_max=1.622578`、`cond=2.435135`、`area=0.462118–1.552697`、`p99=1.324495`,并记录 `zero-warp drift`。 +- `G6 proxy` 无效,不作裁决,亦不得覆盖 G5 硬失败。本例未创建 HF composition/draft,未渲染新审片候选,且未作第二次调参。 +- 因果表述据此收窄:批次 9 的多层结构互穿是重影的重要贡献机制,但不是已证明的唯一或完整根因;single-owner 统一网格虽排除了多层互穿,却因可见纹理应变同样不可用。 +- 植物单所有者统一网格结论冻结为 `single_owner_unified_mesh_hard_fail`,植物路线最终标记为 `plant_route_permanently_stopped`。小鹿继续保持 `user_accepted / manual_only`;普通人物、动物、植物生产路由、广度门与批次 10 均保持关闭。 diff --git "a/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/\347\237\255\350\247\206\351\242\221V2\346\200\273\344\275\223\347\233\256\346\240\207\344\270\216\351\230\266\346\256\265\350\247\204\345\210\222.md" "b/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/\347\237\255\350\247\206\351\242\221V2\346\200\273\344\275\223\347\233\256\346\240\207\344\270\216\351\230\266\346\256\265\350\247\204\345\210\222.md" index 6340c5f..565d378 100644 --- "a/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/\347\237\255\350\247\206\351\242\221V2\346\200\273\344\275\223\347\233\256\346\240\207\344\270\216\351\230\266\346\256\265\350\247\204\345\210\222.md" +++ "b/\347\237\255\350\247\206\351\242\221V2\350\247\204\345\210\222\346\226\207\346\241\243/\347\237\255\350\247\206\351\242\221V2\346\200\273\344\275\223\347\233\256\346\240\207\344\270\216\351\230\266\346\256\265\350\247\204\345\210\222.md" @@ -4,6 +4,8 @@ > > 本文件不是直接实施清单,不规定某个类、函数或接口的最终写法。后续每一阶段应另建可执行计划,并在不偏离本总纲的前提下细化文件、任务、测试和验收步骤。 +> **当前第一版执行基线(2026-08-13)**:经实际成片与动态专项验证,第一版现已收束为“多张静态分镜图 + 包 B 逐镜人声 + ASS 字幕 + 硬切 + 包 A/FFmpeg 合成”。所有新镜头统一使用 `motion=static/low` 与 `transition=cut/0`;不使用 FFmpeg 推拉/平移/漂移、DepthFlow、HyperFrames、本地 I2V、BGM、环境音或 SFX。本文后文关于动态提供器、运动预设和动态阶段的内容保留为历史架构构思与已完成研究记录,不覆盖此执行基线。若未来重开动态能力,应另立计划,不得悄然回到默认工作流。 + --- ## 1. 文档作用与依据 @@ -37,7 +39,7 @@ - 全部开发、模型运行和媒体处理均在当前 Mac 上完成,不再采用 Windows 存储、SSH 连接 Mac 执行的方式。 - 第一版采用“两阶段半自动”模式:Codex 先完成内容与素材准备,包 A+B 再本地执行;以后再逐步提高自动化程度。 - 第一版音频只使用包 B 根据文本生成的解说或角色对白,不加入 BGM、环境音或 SFX。 -- 第一版不追求大片级复杂动效,先保证完整流程跑通、成片基本可用。 +- 第一版不做图片动态化,以多张高质量静态分镜图随叙事切换,先保证完整流程稳定、成片基本可用。 - 第一版不要求角色口型与语音精确同步。 - 本地生成式 I2V 不是 MVP 必需项;若后续实验,单个 3–4 秒镜头以约 20–30 分钟为初始容忍上限,一条视频最多使用 1–2 个。 - 首个视觉样片优先采用中文叙事插画或半写实插画风格,暂不以高难度写真人物连续性作为第一道门槛。 @@ -52,7 +54,7 @@ 将当前项目发展为一套由 Codex 伴随策划、Mac 本地确定性执行的短视频生产工作流: -> 用户提供主题、故事、事件、原始文案或参考材料;Codex 完成内容理解、文案改写、叙事结构、分镜、视觉规划和关键帧准备;包 B 按镜头生成解说或角色对白;包 A 按结构化任务将图片动态化,完成镜头组织、字幕、转场、音画同步、编码、检查与输出,最终得到一条可以直接观看和继续返修的竖屏短视频。 +> 用户提供主题、故事、事件、原始文案或参考材料;Codex 完成内容理解、文案改写、叙事结构、分镜、视觉规划和静态关键帧准备;包 B 按镜头生成解说或角色对白;包 A 按结构化任务将每张图片编码为与人声等长的静态镜头,完成字幕、硬切、音画同步、检查与输出,最终得到一条可以直接观看和继续返修的竖屏短视频。 ### 2.2 目标成片 @@ -63,7 +65,7 @@ - 单条视频使用统一的主视觉风格; - 以第三人称解说为主,也允许少量角色对白; - 画面按叙事推进而切换,不是一张图承载整段音频; -- 每个镜头至少有一种与构图相符的可感知运动; +- 每个镜头使用一张与叙事相符的静态图,镜头间随内容推进切换; - 有清晰字幕,字幕不长期遮挡人物面部和核心内容; - 画面、字幕和语音在语义与节奏上基本一致; - 某个镜头失败或效果不佳时可以单独重做; @@ -75,7 +77,7 @@ 1. 从原始内容到 `final.mp4` 的链路可重复完成; 2. 成片不是黑底字幕,也不是单张图片从头放到尾; -3. 即使高级动效工具尚未接入,只用高质量关键帧、合理分镜、FFmpeg 运镜、转场、字幕和包 B 人声,也能形成一条基本成立的叙事短视频; +3. 只用高质量静态关键帧、合理分镜、硬切、字幕和包 B 人声,也能形成一条基本成立的叙事短视频; 4. 同一输入与相同素材可以稳定重渲,不因临时工具失效而整片失败; 5. 用户或 Codex 能根据报告定位问题镜头,并只返修该镜头。 @@ -123,7 +125,7 @@ flowchart LR J["自包含 Job Bundle"] A["阶段二:包 A V2 本地执行面"] B["包 B:逐镜头解说/对白 WAV"] - M["动效提供器:FFmpeg 为默认,其余按条件增强"] + S["静态镜头编码:FFmpeg"] O["final.mp4 + 镜头 + 字幕 + 报告"] Q["Codex/用户质检与镜头返修"] @@ -134,8 +136,8 @@ flowchart LR J --> A A --> B B --> A - A --> M - M --> A + A --> S + S --> A A --> O O --> Q Q -.只返修问题镜头.-> C @@ -162,10 +164,10 @@ Codex 是导演、编剧和制作控制面,负责: - 设计叙事节奏与镜头; - 建立风格约束和必要的角色约束; - 编写图片提示词并生成或组织关键帧; -- 为镜头选择语义化运镜预设和转场意图; +- 为每镜指定静态关键帧、裁切焦点与硬切; - 生成结构化 Job Bundle; - 读取抽帧、日志与渲染报告; -- 判断问题属于文案、图片、语音、动态、字幕还是合成,并发起镜头级返修。 +- 判断问题属于文案、图片、语音、字幕、裁切还是合成,并发起镜头级返修。 Codex 不作为包 A 内部免费常驻 API,也不在每次任务中直接生成任意 shell、FFmpeg 或 GSAP 代码交给运行时执行。 @@ -187,20 +189,19 @@ Codex 不作为包 A 内部免费常驻 API,也不在每次任务中直接生 - 校验 Job Bundle、Schema、素材路径和输出目录; - 调用包 B 并以真实语音时长修正镜头时间; -- 根据镜头语义选择动效提供器; -- 将图片或高级工具结果渲染为标准化单镜头; -- 组织镜头、硬切或短叠化、ASS 字幕和人声音轨; +- 将每张图片按真实人声时长编码为标准化静态单镜头; +- 组织静态镜头、硬切、ASS 字幕和人声音轨; - 完成最终编码、技术检查、缓存、回退、取消和报告; - 支持预览、最终渲染和镜头级重渲。 包 A V2 不负责自行理解任意自然语言,也不负责替代 Codex 进行开放式创意决策。 -### 4.6 图片与动效提供器职责 +### 4.6 图片与历史动态研究边界 - 图片提供器只负责生成或提供关键帧,不承担整片时间线。 -- 动效提供器只负责一个镜头或一个视觉层的动态化,不决定故事结构。 -- 所有提供器通过稳定文件或版本化接口与包 A 协作。 -- 所有高级提供器都必须允许失败、超时和回退。 +- 第一版不设动态提供器路由;FFmpeg 只负责把静态图规格化、编码和硬切合成。 +- Schema 中的运动预设及既有实验适配器只为旧任务兼容与历史证据保留。 +- 若未来重开动态能力,应另立专项计划;不得直接借旧实验改变第一版主链。 --- @@ -216,7 +217,7 @@ Codex 不作为包 A 内部免费常驻 API,也不在每次任务中直接生 6. 为每个镜头准备关键帧提示词、参考图和构图安全区; 7. 通过 GPT Image 2、用户素材或本地图片工具获得关键帧; 8. 先检查图片的角色、服装、场景、时代、文字和明显畸形; -9. 为每个镜头指定旁白/对白、字幕、运镜意图和基础转场; +9. 为每个镜头指定旁白/对白、字幕、`static/low` 与 `cut/0`; 10. 输出可由包 A 独立读取的 Job Bundle。 ### 5.2 阶段二:包 A+B 本地执行 @@ -224,37 +225,24 @@ Codex 不作为包 A 内部免费常驻 API,也不在每次任务中直接生 1. 包 A 校验任务包和所有本地素材; 2. 包 A 按镜头将解说或对白交给包 B; 3. 包 B 返回 WAV,包 A 读取实际时长; -4. 包 A 依据实际语音时长确定镜头长度、头尾留白与转场重叠; -5. Motion Router 为每个镜头选择可用提供器; -6. 默认用 FFmpeg 固定预设完成推、拉、平移或轻漂移; -7. 若后续高级提供器已经通过实测,可按镜头条件升级; -8. 所有单镜头统一规格后再进入整片合成; -9. 包 A 添加 ASS 字幕、镜头切换和包 B 人声; -10. 输出预览、最终视频、单镜头、字幕和渲染报告; -11. 用户或 Codex 审看后,只返修不合格镜头。 +4. 包 A 依据实际语音时长确定静态镜头长度与头尾留白; +5. 包 A 用 FFmpeg 将每张关键帧规格化并编码为等长静态镜头; +6. 所有单镜头统一规格后再进入整片合成; +7. 包 A 添加 ASS 字幕、硬切和包 B 人声; +8. 输出预览、最终视频、单镜头、字幕和渲染报告; +9. 用户或 Codex 审看后,只返修不合格镜头。 -### 5.3 动效路由总原则 +### 5.3 第一版画面合同 ```text -分层设计型镜头 ──> HyperFrames(通过实测后) -适合景深视差 ───> DepthFlow(通过实测后) -少量真实动作镜头 -> 本地 I2V(通过实测且预算允许时) -其余所有镜头 ───> FFmpeg 固定运镜 -任何高级路径失败 -> FFmpeg 固定运镜 +每个 shot ─> 一张独立静态关键帧 +motion ────> static / low +transition ─> cut / 0 +声音 ──────> 包 B 逐镜人声 +合成 ──────> 包 A / FFmpeg + ASS 字幕 ``` -第一版只需要冻结少量稳定预设,例如: - -- `static`; -- `slow_push_in`; -- `slow_pull_out`; -- `pan_left`; -- `pan_right`; -- `tilt_up`; -- `tilt_down`; -- `gentle_drift`。 - -预设可带主体焦点和低、中、高三个强度等级,但不允许任务包直接携带任意 FFmpeg 命令。 +第一版只主动生成 `static` 预设与 `low` 强度。主体焦点仍用于竖屏裁切;其余运动预设与短叠化只作兼容,不进入新任务。任务包不得携带任意 FFmpeg 命令。 --- @@ -327,8 +315,8 @@ job-/ | --- | --- | --- | --- | | 内容与分镜 | Codex | 正式控制面 | 已确认 | | 主图片生成 | [当前 Codex ImageGen / GPT Image 2](https://developers.openai.com/api/docs/models/gpt-image-2) | 第一选择 | 生图能力确认;三镜头人物一致性尚须样片验证 | -| 本地图片后备 | [MFLUX + FLUX.2 Klein 4B](https://github.com/filipstrand/mflux) | 无 Codex 生图时的优先候选 | 适配 Apple Silicon,尚未在本项目安装与实测 | -| 本地图片/I2V 后备 | [Draw Things CLI/gRPC](https://github.com/drawthingsai/draw-things-community) | 第二图片后备及 I2V 实验入口 | 尚未实测自动化、模型速度和稳定性 | +| 本地静态图后备 | 无正式工具 | Image 2 不可用时由用户提供图片或暂停生图 | 不再维护额外本地生图环境 | +| 真正 I2V 后续研究 | 工具/Provider 待单独评估 | 少量需要主体或场景内运动的重点镜头 | 未实测;需另立质量、成本、授权与工程门 | | 解说/对白 | 包 B dots.tts | 正式语音引擎 | 历史 M1 Pro 能力已有证据;仍须在当前工作树做新鲜端到端验证 | | 基础动态 | [FFmpeg](https://ffmpeg.org/ffmpeg-filters.html) 固定预设 | 必须、默认、最终兜底 | 当前 Mac FFmpeg 所需能力已确认 | | 字幕 | ASS + libass | 正式方案 | 当前 FFmpeg 支持已确认 | @@ -340,8 +328,8 @@ job-/ | 工具 | 适用范围 | 是否阻塞 MVP | 接入前必须证明 | | --- | --- | --- | --- | -| [DepthFlow](https://github.com/BrokenSource/DepthFlow) + [Depth Anything V2 Small](https://github.com/DepthAnything/Depth-Anything-V2) | 风景、建筑、清晰前中后景的 2.5D 视差 | 否 | M1 Pro 速度、边缘质量、ModernGL/无头运行、取消与许可证边界 | -| [HyperFrames](https://github.com/heygen-com/hyperframes) | 标题、地图、数字、分层图片、遮罩、粒子和设计型镜头 | 否 | CLI/Chrome 本机运行、单镜头输出稳定性、启动成本、取消和版本固定 | +| [DepthFlow](https://github.com/BrokenSource/DepthFlow) + [Depth Anything V2 Small](https://github.com/DepthAnything/Depth-Anything-V2) | 风景、建筑的 2.5D 视差 | 否 | 已实测拒绝:只能产生摄影机视差,收益不足以承担工程与许可成本 | +| [HyperFrames](https://github.com/heygen-com/hyperframes) | 信息设计、分层图片、遮罩、粒子 | 否 | CLI 与信息设计保持 `manual_only`;计划 07 Round 2 最终 `CONDITIONAL / manual_only`,仅保留已获用户接受的小鹿眨眼+单耳窄配方。植物因用户终审明显重影而 `rejected`,普通自然主体生产路由关闭 | | [Draw Things](https://github.com/drawthingsai/draw-things-community) + Wan/SkyReels 等 | 少量真正需要语义动作的重点镜头 | 否 | 3–4 秒耗时、内存、磁盘、失败率、脸/手/背景稳定性及自动化接口 | 这些工具只有通过技术样片的质量与工程门槛后,才能成为正式 Provider;未通过时保留为研究记录,不进入生产依赖。 @@ -461,6 +449,26 @@ job-/ 退出条件:每项增强都能独立关闭或回退,不降低基础 FFmpeg 链路的稳定性,也不破坏既有 Job Bundle。 +### 阶段 6:HyperFrames 拆层微动态专项纠偏 + +目标:纠正计划 04 以整图推近和弱叠层代替主体动作的测试误区,单独验证“镜头推进 + 点头/眨眼/呼吸或叶片动作 + 环境粒子”的可行性。 + +> 最终状态:`CONDITIONAL / manual_only`。Round 1 的 `FAIL / rejected` 仍为冻结历史;Round 2 仅小鹿眨眼+单耳窄配方获用户接受,植物因明显重影未过用户视觉门。 + +主要结果: + +- 建立动作 Brief、透明分层资产、枢轴与遮挡方法; +- 用同源静态、旧式整图运动和拆层微动态作公平对照; +- 先用受控动物/角色样片证明方法,再迁移到一个当前项目镜头; +- 用 HyperFrames keyframes、snapshot、媒体检查和用户审片共同验收; +- 只给出实验分类与后续集成建议,不在本阶段修改 Schema 或正式管线。 +- Round 1 用户认为受控动物拆层版生硬且木偶,项目植物拆层版的叶片局部动作不可见;该轮 `FAIL / rejected` 结论冻结保留。 +- Round 2 依成熟 2D 绑定原则完成同拓扑网格、唯一像素所有权、根部锁定与单一 seek 时间线返修。用户接受小鹿两次眨眼+单耳微摆;该结论只限一条窄配方。 +- 植物批次 9 的动作被用户确认为明显且像一阵风,但同时存在明显重影,最终 `user_rejected_due_to_ghosting / rejected`。自动 high 视觉门的 PASS 为假阴性,用户视觉门优先。 +- 用户随后只授权一次 `user_authorized_final_causal_exception`,它不是批次 10、Round 3 或新候选。唯一有效首跑的 single-owner `36x48` unified mesh 虽使 G4 动作门 `PASS`,G5 `visible-strain` 却为 `HARD FAIL`(`sigma_min=0.435627`、`sigma_max=1.622578`、`cond=2.435135`、`area=0.462118–1.552697`、`p99=1.324495`),并记录 `zero-warp drift`;`G6 proxy` 无效,不作裁决。该例外未建 HF composition/draft、未二次调参,故批次 9 多层结构互穿只保留为重要贡献机制而非唯一或完整根因;统一网格补救亦不可用,最终标记为 `single_owner_unified_mesh_hard_fail / plant_route_permanently_stopped`。计划仍为 `complete / CONDITIONAL / manual_only`,小鹿仍为 `user_accepted`,普通自然主体生产、广度门与批次 10 均保持关闭。 + +退出条件:已满足。计划等级为 `CONDITIONAL`、分类为 `manual_only`;普通自然主体生产路由关闭。12 镜广度门保持 `not_run / cancelled_due_to_failed_prerequisite`,不启动批次 10,不进入正式 Provider 集成。HyperFrames 信息设计用途仍为 `manual_only`,首轮主链已通过的事实不受影响。 + --- ## 9. 验收与质量门 @@ -529,7 +537,7 @@ job-/ ### 10.3 顶层契约稳定,底层工具可替换 -任务包表达语义,包 A 把语义映射为工具参数。更换 MFLUX、Draw Things、DepthFlow 或 HyperFrames 时,不应迫使 Codex 重写整个分镜格式。 +任务包表达语义,包 A 把语义映射为工具参数。未来更换 I2V Provider 或 HyperFrames 设计型组合时,不应迫使 Codex 重写整个分镜格式。 ### 10.4 镜头级隔离 @@ -602,7 +610,7 @@ DepthFlow、HyperFrames、Draw Things 和 I2V 均保持外部进程、CLI、服 ## 13. 后续具体计划文档体系 -本总纲之后,建议按执行边界另建以下六份计划文档: +本总纲先按执行边界建立六份首轮计划;首轮主链完成后,依据真实使用反馈追加专项计划 07: 六份计划与本总纲统一放在 `短视频V2规划文档/`;与每份计划对应的执行提示词放在 `短视频V2规划文档/执行提示词/`,实际执行证据与阶段记录放在 `短视频V2规划文档/执行记录/`。计划文件以两位数字编号,使阅读顺序稳定,也避免后续文档散落在项目根目录。 @@ -616,7 +624,7 @@ DepthFlow、HyperFrames、Draw Things 和 I2V 均保持外部进程、CLI、服 覆盖独立入口、镜头级 TTS、时间线、FFmpeg Provider、标准化、字幕、转场、最终合成、缓存、取消、报告和重渲。 4. **图片生成与高级动态化可行性计划** - 覆盖 GPT Image 2 一致性、MFLUX/Draw Things 后备,以及 DepthFlow、HyperFrames、本地 I2V 的安装、质量、性能、许可证和接入决策。 + 已完成 GPT Image 2 一致性、DepthFlow、HyperFrames 与本地 I2V 类别的安装、质量、性能、许可证和接入取舍;后续结论以计划 04 执行记录与 `motion_feasibility_v1.md` 为准。 5. **端到端质量验证与 MVP 验收计划** 覆盖测试矩阵、故障注入、音画同步、技术规格、联系表/抽帧审查、两条不同内容样片和最终验收报告。 @@ -624,7 +632,10 @@ DepthFlow、HyperFrames、Draw Things 和 I2V 均保持外部进程、CLI、服 6. **半自动工作流固化与后续自动化计划** 覆盖 Codex 操作模板、任务包生成规范、镜头返修流程、可选 UI、专用 Skill,以及未来自然语言入口的演进边界。 -这些计划按阶段创建,不需要现在一次写完。原则上,当前阶段只创建即将执行的一份计划;前一阶段的实测结论应成为后一份计划的输入。 +7. **HyperFrames 拆层微动态专项验证计划** + 已完成两轮纠偏实测。Round 1 `FAIL / rejected` 冻结保留;Round 2 仅小鹿眨眼+单耳窄配方获用户接受,植物虽动作明显且像风吹,但因明显重影被用户拒绝。最终 `CONDITIONAL / manual_only`,普通自然主体生产路由关闭,不进入正式 Provider 集成。 + +这些计划按阶段创建;前一阶段的实测结论应成为后一份计划的输入。计划 07 是用户审看真实成片后追加的专项纠偏,不追溯否定计划 01–06 已经完成的主链与工作流验收。 ---