diff --git a/.github/scripts/check-dependency-tests.py b/.github/scripts/check-dependency-tests.py new file mode 100644 index 000000000..f3d101586 --- /dev/null +++ b/.github/scripts/check-dependency-tests.py @@ -0,0 +1,34 @@ +"""Require named dependency-backed Go tests to run and pass (not merely avoid SKIP).""" + +import json +import sys + +REQUIRED = { + ("database", "TestPostgresAssetIDMigration"), + ("canvas", "TestRepairAssetBytesPostgres"), + ("repository", "TestCanvasRevision/postgres"), + ("platform", "TestRuntimeCoordinatorRedisLeasePreservesLongestTTL"), + ("app", "TestClearAdminRuntimeCacheUsesRedisWhitelist"), +} + + +def missing_tests(events): + ran, passed, failed = set(), set(), set() + for event in events: + key = (event.get("Package", "").rsplit("/", 1)[-1], event.get("Test")) + action = event.get("Action") + if action == "run": + ran.add(key) + elif action == "pass": + passed.add(key) + elif action in {"skip", "fail"}: + failed.add(key) + return REQUIRED - ((ran & passed) - failed) + + +if __name__ == "__main__": + with open(sys.argv[1], encoding="utf-8") as stream: + missing = missing_tests(json.loads(line) for line in stream if line.strip()) + if missing: + sys.exit("Dependency tests missing, skipped or failed: " + repr(sorted(missing))) + print("All five dependency-backed tests ran and passed.") diff --git a/.github/scripts/test_check_dependency_tests.py b/.github/scripts/test_check_dependency_tests.py new file mode 100644 index 000000000..75ad7a738 --- /dev/null +++ b/.github/scripts/test_check_dependency_tests.py @@ -0,0 +1,29 @@ +import importlib.util +from pathlib import Path +import unittest + +spec = importlib.util.spec_from_file_location( + "check", Path(__file__).with_name("check-dependency-tests.py") +) +check = importlib.util.module_from_spec(spec) +spec.loader.exec_module(check) + + +class DependencyChecks(unittest.TestCase): + def test_requires_run_and_pass_for_every_exact_name(self): + events = [ + {"Package": "example/internal/" + package, "Test": name, "Action": action} + for package, name in check.REQUIRED + for action in ("run", "pass") + ] + self.assertFalse(check.missing_tests(events)) + self.assertTrue(check.missing_tests([])) + self.assertTrue(check.missing_tests(events[1:])) + self.assertTrue(check.missing_tests(events[:-1])) + for action in ("skip", "fail"): + self.assertTrue(check.missing_tests(events + [dict(events[-1], Action=action)])) + self.assertTrue(check.missing_tests([dict(e, Test=e["Test"] + "Renamed") for e in events])) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index eac53dbfc..e0188375e 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -55,14 +55,8 @@ jobs: - name: Ensure dependency-backed tests were not skipped shell: bash run: | - set -o pipefail - go test ./internal/database/ ./internal/canvas/ ./internal/repository/ ./internal/platform/ ./internal/app/ \ - -run 'TestPostgresAssetIDMigration|TestRepairAssetBytesPostgres|TestCanvasRevision|TestRuntimeCoordinatorRedisLeasePreservesLongestTTL|TestClearAdminRuntimeCacheUsesRedisWhitelist' \ - -count=1 -v | tee "$RUNNER_TEMP/dependency-tests.log" - if grep -q -- "--- SKIP" "$RUNNER_TEMP/dependency-tests.log"; then - echo "::error::依赖外部服务的用例被跳过:请检查 CANVAS_TEST_POSTGRES_DSN 与 redis-server" - exit 1 - fi + python3 ../.github/scripts/test_check_dependency_tests.py + python3 ../.github/scripts/check-dependency-tests.py "$RUNNER_TEMP/go-tests.jsonl" - name: Upload Go test timings if: always() uses: actions/upload-artifact@v4 diff --git a/backend/internal/app/admin_storage_delete.go b/backend/internal/app/admin_storage_delete.go index 39ae100b4..f2943574a 100644 --- a/backend/internal/app/admin_storage_delete.go +++ b/backend/internal/app/admin_storage_delete.go @@ -165,7 +165,7 @@ func (s *Service) DeleteAdminResources(actor *model.User, req AdminResourceDelet return nil, err } if len(deletionJobs) > 0 { - go s.drainResourceDeletionJobs(len(deletionJobs)) + s.runWorkerTask(func() { s.drainResourceDeletionJobs(len(deletionJobs)) }) } result := &AdminResourceDeleteResult{Deleted: []string{}, Blocked: []AdminResourceDeleteBlocked{}} diff --git a/backend/internal/app/admin_storage_delete_test.go b/backend/internal/app/admin_storage_delete_test.go index d829d8cc5..e4ad0d10e 100644 --- a/backend/internal/app/admin_storage_delete_test.go +++ b/backend/internal/app/admin_storage_delete_test.go @@ -170,7 +170,9 @@ func newAdminStorageDeleteTestService(t *testing.T) (*Service, *gorm.DB, string, t.Fatal(err) } dataDir := t.TempDir() - return New(repository.New(db), dataDir), db, dataDir, admin + svc := New(repository.New(db), dataDir) + startDeletionTestWorkers(t, svc) + return svc, db, dataDir, admin } func assertModelCount(t *testing.T, db *gorm.DB, value any, query string, expected int64, args ...any) { diff --git a/backend/internal/app/cloud_agent_tool_schema_budget_test.go b/backend/internal/app/cloud_agent_tool_schema_budget_test.go index b47d32f55..a0ba20e67 100644 --- a/backend/internal/app/cloud_agent_tool_schema_budget_test.go +++ b/backend/internal/app/cloud_agent_tool_schema_budget_test.go @@ -2,7 +2,7 @@ package app import ( "encoding/json" - "strings" + "reflect" "testing" ) @@ -24,31 +24,37 @@ func TestCloudAgentToolSchemaStaysCompact(t *testing.T) { tools, raw := platformToolSchema(t) t.Logf("platform tool schema: %d tools, %d bytes", len(tools), len(raw)) - // 顶层 type 枚举 + 服务端按 action 强校验已经表达了"每种操作要哪些字段", - // 再挂一份 oneOf 只是重复:同一个约束会被下发两遍。 + // 条件必填不是 type 枚举能表达的约束,压缩描述不能删掉协议语义。 + found := false for _, tool := range tools { function, _ := tool["function"].(map[string]any) if function["name"] != "canvas_apply_ops" { continue } + found = true parameters, _ := function["parameters"].(map[string]any) ops, _ := parameters["properties"].(map[string]any) items, _ := ops["ops"].(map[string]any) opItem, _ := items["items"].(map[string]any) - if _, exists := opItem["oneOf"]; exists { - t.Fatal("canvas_apply_ops 的 ops.items 不该再带重复的 oneOf") + want := []map[string]any{ + {"properties": map[string]any{"type": map[string]any{"const": "add_node"}}, "required": []string{"nodeType"}}, + {"properties": map[string]any{"type": map[string]any{"const": "update_node"}}, "required": []string{"patch"}}, + {"properties": map[string]any{"type": map[string]any{"const": "connect_nodes"}}, "required": []string{"fromNodeId", "toNodeId"}}, + } + if !reflect.DeepEqual(opItem["oneOf"], want) { + t.Fatalf("操作类型条件必填约束丢失: %#v", opItem["oneOf"]) } if props, ok := opItem["properties"].(map[string]any); !ok || len(props) == 0 { t.Fatal("canvas_apply_ops 的 ops.items 必须保留 properties") } } + if !found { + t.Fatal("canvas_apply_ops 未暴露") + } // 体积预算:当前 20 个工具约 22.4 KB,这里留 ~7% 余量;新增工具或字段时请重新测量并 // 有意识地调整这个数字,而不是让 schema 悄悄膨胀(它每一步都要发、还在前缀最前面)。 if len(raw) > 24000 { t.Fatalf("平台工具 schema 体积 %d 字节超出预算 24000:请压缩描述或显式调整预算", len(raw)) } - if strings.Count(string(raw), "oneOf") > 2 { - t.Fatalf("工具 schema 里出现了过多 oneOf:%s", string(raw[:200])) - } } diff --git a/backend/internal/app/cloud_agent_tools.go b/backend/internal/app/cloud_agent_tools.go index ea30dceab..742038745 100644 --- a/backend/internal/app/cloud_agent_tools.go +++ b/backend/internal/app/cloud_agent_tools.go @@ -251,6 +251,11 @@ func compileCloudAgentTools(req CloudAgentRequest, includeProfileTool bool) []ma "properties": opProperties, "required": []string{"type", "id"}, "additionalProperties": false, + "oneOf": []map[string]any{ + {"properties": map[string]any{"type": map[string]any{"const": "add_node"}}, "required": []string{"nodeType"}}, + {"properties": map[string]any{"type": map[string]any{"const": "update_node"}}, "required": []string{"patch"}}, + {"properties": map[string]any{"type": map[string]any{"const": "connect_nodes"}}, "required": []string{"fromNodeId", "toNodeId"}}, + }, } add("canvas_apply_ops", "创建空白节点、修改提示词或建立引用连线,不提交生成任务、不产生生成费用;先读取画布并传 snapshotHash。提交媒体生成使用 generate_media。每次最多20项,禁止删除、任意 metadata 和媒体 URL。每项都需要 type 和 id:add_node 还需要 nodeType,update_node 还需要按节点能力清单填写 patch,connect_nodes 还需要 fromNodeId 与 toNodeId。连线是生成输入关系,不会改变已提交任务的输入;来源须 canSource,目标须 canTarget 且接受来源 inputKind,能力以注册表为准。", map[string]any{"snapshotHash": str("canvas_get_state返回的snapshotHash"), "ops": map[string]any{"type": "array", "maxItems": 20, "items": opItem}}, "snapshotHash", "ops") } diff --git a/backend/internal/app/resource_delete.go b/backend/internal/app/resource_delete.go index bac55654c..55d4d3498 100644 --- a/backend/internal/app/resource_delete.go +++ b/backend/internal/app/resource_delete.go @@ -146,7 +146,8 @@ func (s *Service) deleteUserAssetWithResources(userID string, assetID string) er return fmt.Errorf("素材记录删除失败,请重试:%w", err) } if len(deletionJobs) > 0 { - go s.drainResourceDeletionJobs(len(deletionJobs)) + // 纳入停机等待;排空期间拒绝新 worker 时,已提交的 outbox 留待下次启动处理。 + s.runWorkerTask(func() { s.drainResourceDeletionJobs(len(deletionJobs)) }) } return nil } diff --git a/backend/internal/app/resource_delete_test.go b/backend/internal/app/resource_delete_test.go index b0a640477..f2ab072f9 100644 --- a/backend/internal/app/resource_delete_test.go +++ b/backend/internal/app/resource_delete_test.go @@ -513,5 +513,7 @@ func newResourceDeletionTestService(t *testing.T) (*Service, *gorm.DB, string) { t.Fatal(err) } dataDir := t.TempDir() - return New(repository.New(db), dataDir), db, dataDir + svc := New(repository.New(db), dataDir) + startDeletionTestWorkers(t, svc) + return svc, db, dataDir } diff --git a/backend/internal/app/resource_deletion_worker.go b/backend/internal/app/resource_deletion_worker.go index 2a3fb1369..b52cb04de 100644 --- a/backend/internal/app/resource_deletion_worker.go +++ b/backend/internal/app/resource_deletion_worker.go @@ -126,7 +126,7 @@ func (s *Service) cleanupDetachedUserResources(userID string, candidates []model } log.Printf("detached resource cleanup: removed %d resource rows for user %s", len(detached), userID) if len(deletionJobs) > 0 { - go s.drainResourceDeletionJobs(len(deletionJobs)) + s.runWorkerTask(func() { s.drainResourceDeletionJobs(len(deletionJobs)) }) } return nil } diff --git a/backend/internal/app/task_worker.go b/backend/internal/app/task_worker.go index ea51d3fae..528c03dc1 100644 --- a/backend/internal/app/task_worker.go +++ b/backend/internal/app/task_worker.go @@ -227,16 +227,11 @@ func (w *taskWorkerCoordinator) processClaimedTask(task *model.Task, globalSlot if code, _ := ChannelSlotFailureDetails(err); code != "" { channelSlotFailedBeforeRequest = true } - // 我方执行时限到点不是"租约丢失":续租请求可能正好被同一个时限打断, - // 此时必须继续走失败收尾,否则任务停在 running、租约过期后被反复重跑。 - deadlineExpired := errors.Is(ctx.Err(), context.DeadlineExceeded) + // 续租使用独立 context;即使执行同时超时,真实租约失效仍必须阻止写入。 select { case leaseErr := <-leaseLost: - if !deadlineExpired { - _ = s.log(task.UserID, task.ID, "warn", "任务租约失效,等待其他 worker 恢复", leaseErr.Error()) - return leaseErr - } - _ = s.log(task.UserID, task.ID, "warn", "任务执行时限到点,按失败收尾(不视为租约失效)", leaseErr.Error()) + _ = s.log(task.UserID, task.ID, "warn", "任务租约失效,等待其他 worker 恢复", leaseErr.Error()) + return leaseErr default: } decryptedInput, decryptErr := s.decryptTaskInputJSON(task.InputJSON) diff --git a/backend/internal/app/task_worker_test.go b/backend/internal/app/task_worker_test.go index e7832f8dc..f4f340b75 100644 --- a/backend/internal/app/task_worker_test.go +++ b/backend/internal/app/task_worker_test.go @@ -2,8 +2,12 @@ package app import ( "context" + "errors" "testing" "time" + + "infinite-canvas/backend/internal/model" + "infinite-canvas/backend/internal/repository" ) // 续租必须比"这条任务的执行时限"活得更久:否则父 context 到点时,续租请求会被自己的 @@ -39,3 +43,66 @@ func TestTaskLeaseRenewContextSurvivesExecutionDeadline(t *testing.T) { t.Fatalf("父 context 取消不该连带取消续租 context:%v", err) } } + +func TestTaskDeadlineRenewalAndTerminalFencing(t *testing.T) { + for _, lost := range []bool{false, true} { + name := "deadline_with_valid_lease" + if lost { + name = "deadline_with_reclaimed_lease" + } + t.Run(name, func(t *testing.T) { + db := newSQLiteTestDB(t) + if err := db.AutoMigrate(&model.Task{}); err != nil { + t.Fatal(err) + } + repo := repository.New(db) + if err := db.Create(&model.Task{ID: "deadline-task", UserID: "user", Type: "canvas_text", Status: model.TaskStatusQueued}).Error; err != nil { + t.Fatal(err) + } + task, err := repo.ClaimNextTask("worker-a", time.Minute) + if err != nil || task == nil { + t.Fatalf("claim: %v %v", task, err) + } + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(-time.Second)) + defer cancel() + renew, cancelRenew := taskLeaseRenewContext(ctx) + defer cancelRenew() + if lost { + if err := db.Model(&model.Task{}).Where("id = ?", task.ID).Update("lease_expires_at", time.Now().Add(-time.Second)).Error; err != nil { + t.Fatal(err) + } + reclaimed, err := repo.ClaimNextTask("worker-b", time.Minute) + if err != nil || reclaimed == nil { + t.Fatalf("reclaim: %v %v", reclaimed, err) + } + } + err = repo.WithContext(renew).RenewTaskLease(task.ID, task.LeaseOwner, time.Minute) + if (err != nil) != lost { + t.Fatalf("renew lost=%v: %v", lost, err) + } + billing, replay := &taskTerminalBillingStub{}, &taskTerminalReplayStub{} + terminal := newTaskTerminalCoordinatorForTest(repo, billing, replay, &taskTerminalLoggerStub{}, &taskTerminalOutputStub{}) + err = terminal.handleExecutionFailure(task, ctx.Err(), false, false) + stored, readErr := repo.Task(task.ID) + if readErr != nil { + t.Fatal(readErr) + } + if lost { + if !errors.Is(err, repository.ErrTaskStateConflict) || stored.Status != model.TaskStatusRunning || stored.LeaseOwner != "worker-b" { + t.Fatalf("stale terminal write: %v %#v", err, stored) + } + if len(billing.refund)+len(billing.uncertain)+len(replay.statuses) != 0 { + t.Fatal("stale worker emitted terminal side effects") + } + } else { + if !errors.Is(err, context.DeadlineExceeded) || stored.Status != model.TaskStatusFailed { + t.Fatalf("deadline not finalized: %v %#v", err, stored) + } + next, err := repo.ClaimNextTask("worker-b", time.Minute) + if err != nil || next != nil { + t.Fatalf("terminal task reclaimed: %v %v", next, err) + } + } + }) + } +} diff --git a/backend/internal/app/test_database_test.go b/backend/internal/app/test_database_test.go index 55f71bc37..690801cd3 100644 --- a/backend/internal/app/test_database_test.go +++ b/backend/internal/app/test_database_test.go @@ -1,8 +1,8 @@ package app import ( + "context" "fmt" - "os" "path/filepath" "strings" "sync" @@ -21,27 +21,43 @@ import ( // 而当前驱动没有启用。实测并发读写 400 次能撞到 70+ 次,CI 上因此偶发失败 // (TestDeleteGeneratedAssetTaskReferences/cancelled_output 就是这样挂的)。 // -// 因此凡是会触发后台删除 outbox(`go s.drainResourceDeletionJobs`)的用例,统一用 +// 因此凡是会触发后台删除 outbox 的用例,统一用 // 文件库 + WAL + busy_timeout:WAL 允许"一写多读"并发,写冲突退化成 SQLITE_BUSY 并由 // busy_timeout 重试。 // -// 库文件刻意放在 `os.MkdirTemp` 而不是 `t.TempDir()` 下:outbox goroutine 可能在用例返回后 -// 仍在写库,`t.TempDir()` 的清理遇到这种写入会以 "directory not empty" 直接判失败, -// 而它本身与用例断言无关。 +// 清理顺序为等待 service worker、关闭 SQL 连接、删除临时目录,不忽略清理错误。 func newSQLiteTestDB(t *testing.T) *gorm.DB { t.Helper() - directory, err := os.MkdirTemp("", "canvas-test-db-") - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { _ = os.RemoveAll(directory) }) + directory := t.TempDir() db, err := gorm.Open(sqlite.Open(filepath.Join(directory, "test.db")+"?_journal_mode=WAL&_busy_timeout=5000"), &gorm.Config{}) if err != nil { t.Fatal(err) } + sqlDB, err := db.DB() + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := sqlDB.Close(); err != nil { + t.Errorf("close test database: %v", err) + } + }) return db } +func startDeletionTestWorkers(t *testing.T, svc *Service) { + t.Helper() + // 只启用生命周期跟踪,不启动生成、支付等无关的周期 worker。 + svc.backgroundWorkers().Start() + t.Cleanup(func() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := svc.StopWorker(ctx); err != nil { + t.Errorf("stop deletion workers: %v", err) + } + }) +} + // TestSQLiteTestDBAllowsConcurrentReadWrite 守的是 newSQLiteTestDB 的选型:并发读写不得报锁。 // 换回 `mode=memory&cache=shared` 会立刻变红(见上面的机制说明)。 func TestSQLiteTestDBAllowsConcurrentReadWrite(t *testing.T) { diff --git a/backend/internal/skills/seed/skills.json b/backend/internal/skills/seed/skills.json index dd013f833..e7536e458 100644 --- a/backend/internal/skills/seed/skills.json +++ b/backend/internal/skills/seed/skills.json @@ -1106,18 +1106,18 @@ "instruction": "# 模板陷阱清单\n\n## R — 原文要点 (Reading)\n\n来源方法(MIT,蒸馏自 freestylefly style-library 各模板 Pitfalls 节选):**通用陷阱**——①**长段正文塞进画面**:图里禁长段落,文字要短标签级;②**模块数超限**:先限制模块数量再补视觉细节(信息图 3-5 模块、科学尺度图 6-8 框);③**文字不可读**:约束文字可读性与平台/模板特征;④**布局通用化**:避免「通用放大镜式布局」「泛平台 App mockup」这类无特征构图;⑤**尺度框同质**:每个尺度框视觉要有区分,禁全部一样。**对应约束写法**:文字块写精确内容+短标签;布局块写模块数上限+层级;负向块写「禁长段文字/禁通用布局/禁同质重复元素」。\n\n## I — 方法论骨架 (Interpretation):\n\n1. **信息密度有上限**:图的承载力远低于文档——长段必崩。\n2. **先减后加**:模块数定顶再补细节;边加边看必拥挤。\n3. **特征是记忆点**:通用化布局=没有品牌=看完就忘。\n4. **同质即失败**:重复元素无区分=信息图变壁纸。\n5. **陷阱可前置**:所有陷阱都能写进负向约束块预防(配 freestyle-prompt-blocks)。\n\n## A1 — 应用案例 (Past Application)\n\n- 来源实践:信息图模板约束「3-5 模块+短标签+色块箭头控制复杂度」→一次通过;去掉模块数上限的版本拥挤到读不动。\n- 反例:「画一个科技感信息图」——无模块数无文字约束,出来一张漂亮但读不了的壁纸。\n\n## A2 — 触发场景 (Future Trigger)\n\n- 信息图/图表/UI/科普图生成。\n- 模板产出「漂亮但读不了」的修复。\n- 影策 Agent 生图任务负向约束的预置清单。\n\n## E — 可执行步骤 (Execution)\n\n1. **定模块数**:按模板类型设上限(信息图 3-5/尺度图 6-8)。\n *完成标准*:模块数上限写入提示词。\n2. **文字短签化**:画面文字全部短标签级。\n *完成标准*:无长段文字。\n3. **布局特征化**:指定具体布局结构,禁通用式。\n *完成标准*:布局描述具体可辨。\n4. **元素区分**:重复类元素(尺度框/卡片)逐一定差异。\n *完成标准*:无同质元素。\n5. **负向预置**:陷阱清单写进约束块。\n *完成标准*:负向块覆盖五类陷阱。\n\n---\n\n许可与署名:本技能方法论蒸馏自开源项目 https://github.com/freestylefly/awesome-gpt-image-2(MIT),遵循其署名要求;由剧典技能库结构化整理为技能格式。\n", "status": 1, "markdown_url": "", - "create_time": 1789934570, - "update_time": 1789934570, + "create_time": 1789934570000, + "update_time": 1789934570000, "source": 3, "tag": "creative", "sort_weight": 101, "is_private": false, "like_count": 0, - "owner_uid": "", + "owner_uid": "community-itswyatt-k", "effective_user": { "name": "剧典技能库(itsWyatt-K)", "avatar_url": "", - "uid": "" + "uid": "community-itswyatt-k" }, "showcase_media": [], "added_count": 0, @@ -1130,18 +1130,18 @@ "instruction": "# 风格库六块组装\n\n## R — 原文要点 (Reading)\n\n来源方法(MIT,蒸馏自 freestylefly style-library SKILL.md):最终提示词按六块组装——①**主体与任务**(subject and task);②**构图与布局**(composition and layout);③**视觉风格与材质**(visual style and materials);④**文字与标签要求**(text and label requirements——精确文字内容、可读性、层级);⑤**画幅与输出格式**(aspect ratio and output format);⑥**约束与负向细节**(constraints and negative details——要避免的工件)。**约束要具体**:精确文字、画幅、可读标签、布局层级、避免的工件——不写约束等于让模型猜。**多概念复用**:用户要多个概念时,复用同一模板、只变主体/构图/色板/场景——保系列一致性。**语言跟随用户**:中文请求最终提示词用中文(除非用户要英文)——gpt-image 对中文请求出中文提示词表现好。\n\n## I — 方法论骨架 (Interpretation):\n\n1. **六块=六个施工面**:缺块即缺指令,模型必自由发挥。\n2. **文字块是 GPT-Image 强项**:画面文字要精确到内容+可读性+层级,别只说「带标题」。\n3. **约束块防工件**:把常见工件(乱码/多余元素/水印感)写进负向。\n4. **同模板变主体=系列感**:系列图锁模板变变量,不锁模板必散。\n5. **语言策略**:中文请求默认中文提示词——别无脑翻译成英文。\n\n## A1 — 应用案例 (Past Application)\n\n- 来源实践:UI 截图系统模板六块化——平台锁定+比例+层级+可见文字+UI chrome(状态栏/Tab/操作行/评论层)+约束(避平台描述过泛/文字可读)。\n- 反例:只写「做个科技感海报」——六块缺五块,模型交啥全靠运气。\n\n## A2 — 触发场景 (Future Trigger)\n\n- 风格库模板落成提示词的组装步。\n- 系列图/多概念批量出图。\n- 画面文字类需求(海报/UI/信息图)。\n\n## E — 可执行步骤 (Execution)\n\n1. **六块铺满**:主体/构图/风格材质/文字/画幅/约束逐块写实。\n *完成标准*:六块无空块。\n2. **文字精确化**:画面文字内容+可读性+层级三要素。\n *完成标准*:文字块含三要素。\n3. **负向列工件**:常见工件写进约束块。\n *完成标准*:负向清单非空。\n4. **系列锁模板**:多概念同模板变变量。\n *完成标准*:系列模板一致。\n5. **语言核对**:提示词语言与请求语言策略一致。\n *完成标准*:语言策略已声明。\n\n---\n\n许可与署名:本技能方法论蒸馏自开源项目 https://github.com/freestylefly/awesome-gpt-image-2(MIT),遵循其署名要求;由剧典技能库结构化整理为技能格式。\n", "status": 1, "markdown_url": "", - "create_time": 1789934570, - "update_time": 1789934570, + "create_time": 1789934570000, + "update_time": 1789934570000, "source": 3, "tag": "creative", "sort_weight": 102, "is_private": false, "like_count": 0, - "owner_uid": "", + "owner_uid": "community-itswyatt-k", "effective_user": { "name": "剧典技能库(itsWyatt-K)", "avatar_url": "", - "uid": "" + "uid": "community-itswyatt-k" }, "showcase_media": [], "added_count": 0, @@ -1154,18 +1154,18 @@ "instruction": "# 风格库四级匹配序\n\n## R — 原文要点 (Reading)\n\n来源方法(MIT,蒸馏自 freestylefly gpt-image-2-style-library):**四级匹配序**——①显式产品类型对**模板类目**(product/poster/UI/infographic/brand/photography/character/document);②视觉词对**风格标签**(realistic/3D/illustration/classical/brand/poster/UI);③语境词对**场景标签**(commerce/education/social/food/travel/story/history/tech/creative);④仍不定则对**最近示例案例**。**请求模糊时**:呈现 2-3 个强模板方向+短理由让用户选,不独自决定。**输出三件套**:选中的模板名、可直贴的 GPT-Image2 提示词、文字/画幅/布局/负向细节的简明约束。**模板实例(风格库节选)**:UI 截图系统(锁平台/比例/层级/画面文字,避平台描述过泛)、信息图引擎(3-5 模块/信息流/层级/短标签,避长段塞图)、科学尺度缩放图(6-8 尺度框/单位/倍率)、海报排版系统等。**维护纪律**:源库变动时重新生成参考文件,以参考文件为准而非记忆。\n\n## I — 方法论骨架 (Interpretation):\n\n1. **漏斗不跳级**:类目没定不要先看风格——行当错则全错。\n2. **模糊即呈现**:拿不准就给 2-3 个方向+理由(配 options-presentation)。\n3. **模板名是溯源键**:输出带模板名,用户与后续迭代都能对上账。\n4. **约束是交付一部分**:文字/画幅/布局/负向细节不写=模板白选。\n5. **参考文件优先于记忆**:模板名/类目/封面以生成参考为准。\n\n## A1 — 应用案例 (Past Application)\n\n- 来源实践:用户「生成城市生命系统图谱」→类目 infographic→风格 infographic/charts→场景 tech→输出带模板名+可贴提示词+约束。\n- 反例:凭记忆写模板名——模板早已更新,名不对版。\n\n## A2 — 触发场景 (Future Trigger)\n\n- 用 GPT-Image2 风格库做任何图。\n- 用户说「找个合适的模板」。\n- 影策 Agent 生图前的模板决策。\n\n## E — 可执行步骤 (Execution)\n\n1. **判类目**:按产品类型定模板类目。\n *完成标准*:类目唯一确定。\n2. **对风格**:视觉词对风格标签。\n *完成标准*:风格标签命中。\n3. **对场景**:语境词对场景标签。\n *完成标准*:场景标签命中。\n4. **兜底案例**:仍不定则对最近案例/呈现 2-3 方向。\n *完成标准*:候选收敛或已呈现选项。\n5. **输出三件套**:模板名+可贴提示词+约束清单。\n *完成标准*:三件齐全。\n\n---\n\n许可与署名:本技能方法论蒸馏自开源项目 https://github.com/freestylefly/awesome-gpt-image-2(MIT),遵循其署名要求;由剧典技能库结构化整理为技能格式。\n", "status": 1, "markdown_url": "", - "create_time": 1789934570, - "update_time": 1789934570, + "create_time": 1789934570000, + "update_time": 1789934570000, "source": 3, "tag": "creative", "sort_weight": 103, "is_private": false, "like_count": 0, - "owner_uid": "", + "owner_uid": "community-itswyatt-k", "effective_user": { "name": "剧典技能库(itsWyatt-K)", "avatar_url": "", - "uid": "" + "uid": "community-itswyatt-k" }, "showcase_media": [], "added_count": 0, @@ -1178,18 +1178,18 @@ "instruction": "# 批量变体:单变量与钩 CTA 配对\n\n## R — 原文要点 (Reading)\n\n来源方法(MIT,蒸馏自 iart ad-creative-video):**两条让变体值得跑的规则**:①**单变量隔离**——测试只有在一个东西变化时才教得会东西;布局/运镜/颜色/时长全锁定,只换被测字段(钩子/优惠/CTA);混改两个变量,胜者无法解读。②**钩子-CTA 配对(message-match)**——前三秒的承诺必须由按钮兑现:「每天浪费 2 小时」的钩子结尾是「Save 2 hours — try free」而不是通用「Shop now」;钩子与 CTA 在数据表每一行里是配对关系。**广告解剖(15-30s)**:Hook 0-3s(停滑,抛问题或模式打断)→Context 3-8s(放大痛点)→Payoff 8-18s(产品即解,一个清晰收益)→Proof 18-25s(一个具体数字/演示/结果)→CTA 末 3s(单一行动,与钩子配对,定格 ≥2s)。钩子情绪触发要在 **2 秒标记前**落地(判断约 1.7 秒形成且划速递增)。**钩子先测**:钩子对 CPA 的影响大于其他一切元素。**数据驱动模板**:一切营销者可能测的都不硬编码——构图读单一 variant 对象(hook/benefit/proof/cta/bg/accent),渲染器永不改组件;每个动画值是帧的纯函数(禁 CSS transition/库计时器,会失步)。\n\n## I — 方法论骨架 (Interpretation):\n\n1. **变量隔离是科学底线**:A/B 的价值在归因,混改=放弃归因。\n2. **承诺要闭环**:钩 CTA 不配对=观众被承诺吸引、被陌生 CTA 劝退。\n3. **2 秒死线**:情绪触发晚于 2 秒=观众已划走。\n4. **数据驱动=工业化**:变体从表里长出来,不从设计稿里长出来。\n5. **钩子是第一测试对象**:预算有限先测钩子。\n\n## A1 — 应用案例 (Past Application)\n\n- 来源实践:5-10 条钩子串×同一正片=一版一条,最干净的钩子测试;变体表每行 hook/CTA 成对。\n- 反例:变体同时换了钩子和背景色——点击高 20% 但无法归因给谁,测试白跑。\n\n## A2 — 触发场景 (Future Trigger)\n\n- 投放素材批量生产与测试设计。\n- 「测了但不知道为啥赢」的复盘(先查变量隔离)。\n- 影策 Agent 批量生成素材任务的结构设计。\n\n## E — 可执行步骤 (Execution)\n\n1. **定测试变量**:本轮只测一个字段(钩子/优惠/CTA)。\n *完成标准*:变量唯一。\n2. **锁其余**:布局/运镜/颜色/时长全锁定。\n *完成标准*:非变量项零变化。\n3. **钩 CTA 配对**:数据表逐行检查承诺-兑现对应。\n *完成标准*:配对率 100%。\n4. **2 秒核**:钩子情绪触发 <2s 标记。\n *完成标准*:触发点位达标。\n5. **表驱生产**:变体从 variant 表渲染,帧函数确定。\n *完成标准*:变体逐行可追溯。\n\n---\n\n许可与署名:本技能方法论蒸馏自开源项目 https://github.com/iart-ai/ad-video-skills(MIT),遵循其署名要求;由剧典技能库结构化整理为技能格式。\n", "status": 1, "markdown_url": "", - "create_time": 1789934570, - "update_time": 1789934570, + "create_time": 1789934570000, + "update_time": 1789934570000, "source": 3, "tag": "ecommerce", "sort_weight": 104, "is_private": false, "like_count": 0, - "owner_uid": "", + "owner_uid": "community-itswyatt-k", "effective_user": { "name": "剧典技能库(itsWyatt-K)", "avatar_url": "", - "uid": "" + "uid": "community-itswyatt-k" }, "showcase_media": [], "added_count": 0, @@ -1202,18 +1202,18 @@ "instruction": "# 卡点发布:瞬态切点与砸点\n\n## R — 原文要点 (Reading)\n\n来源方法(MIT,蒸馏自 iart launch-video 卡点章节):**测 drop 精确时间码**——在音频里找到 drop 的确切位置,让品牌标在那个帧上 snap 满(配微过冲:scale 1.18→1.0 用 0.18s power3.out 回弹+白色闪光 0.25s)。**切在瞬态上,不切在固定网格**——最重的切点落在 kick/snare 敲击上;进 drop 前用 time-remap ramp 速度,出 drop 硬切。**蒙太奇节拍驱动**:一拍一卖点,每拍 0.6-1.0s,硬切在拍上;beats 数组(13.0/13.8/14.6/15.4/16.2…)逐拍调度镜头。**运动语言统一**:整个蒙太奇同一进场曲线同一退场——速度读出来是自信不是混乱。参考实现(Remotion 思路):所有动画值是 `useCurrentFrame()` 的纯函数,禁 CSS transition 与库计时器(会失步);variant 对象驱动一切可变项。\n\n## I — 方法论骨架 (Interpretation):\n\n1. **时间码先于剪辑**:drop 与瞬态是测量出来的,不是听出来的。\n2. **重切配重拍**:kick/snare 位放大切点力度,轻拍位放过渡。\n3. **ramp-then-hardcut**:进 drop 加速、出 drop 硬切——张弛结构。\n4. **一拍一信息**:0.6-1.0s 一拍,每拍只讲一个卖点(关键词+单一支撑画面)。\n5. **确定性渲染**:帧函数式动画=逐帧确定,禁时间器类失步源。\n\n## A1 — 应用案例 (Past Application)\n\n- 来源实践:beats 数组逐拍调度 feature 卡(pop 0.35s cubic-bezier(.22,1,.36,1))——速度感来自统一曲线而非快切本身。\n- 反例:按 1s 整数网格切——画面节奏与音乐长期微漂移,观感「两张皮」。\n\n## A2 — 触发场景 (Future Trigger)\n\n- 任何要卡点的广告/短片/MV。\n- 「画面音乐不合拍」的修复(先查瞬态切点)。\n- 影策 Agent 生成卡点结构的分镜参数。\n\n## E — 可执行步骤 (Execution)\n\n1. **测时间码**:记录 drop 与主要瞬态位置。\n *完成标准*:时间码表成文。\n2. **排砸点**:品牌标 snap 于 drop 帧+微过冲+白闪。\n *完成标准*:砸点=drop 帧。\n3. **配瞬态切**:重切落 kick/snare,过渡落轻拍。\n *完成标准*:切点-拍点对应表成立。\n4. **ramp 进出**:进 drop ramp,出 drop 硬切。\n *完成标准*:ramp 与硬切在位。\n5. **蒙太奇拍驱**:0.6-1.0s 一拍一卖点+统一运动曲线。\n *完成标准*:拍-卖点对应且曲线统一。\n\n---\n\n许可与署名:本技能方法论蒸馏自开源项目 https://github.com/iart-ai/ad-video-skills(MIT),遵循其署名要求;由剧典技能库结构化整理为技能格式。\n", "status": 1, "markdown_url": "", - "create_time": 1789934570, - "update_time": 1789934570, + "create_time": 1789934570000, + "update_time": 1789934570000, "source": 3, "tag": "ecommerce", "sort_weight": 105, "is_private": false, "like_count": 0, - "owner_uid": "", + "owner_uid": "community-itswyatt-k", "effective_user": { "name": "剧典技能库(itsWyatt-K)", "avatar_url": "", - "uid": "" + "uid": "community-itswyatt-k" }, "showcase_media": [], "added_count": 0, @@ -1226,18 +1226,18 @@ "instruction": "# 发布片五拍弧线\n\n## R — 原文要点 (Reading)\n\n来源方法(MIT,蒸馏自 iart ad-video-skills launch-video):15-60s 高级感发布片弧线——①**Hook(0-3s)**:一个停滑的帧或运动;②**Tease(3-9s)**:暗示产品,建立好奇,卡点建立;③**Reveal(9-13s)**:产品/logo 砸在音乐 drop 上;④**Feature montage(13-25s)**:快节奏运动感,一镜一卖点;⑤**End card(25-30s)**:logo+slogan+CTA 干净定格。**30s 比例分配表**即上表;15s 收紧、60s 加长蒙太奇(**绝不加长 hook**)。两条铁律:①**声音设计引领画面**——先锁音轨、标出 beats 与 drop,再让画面切在标记上;永远不要给剪好的片子配乐(reveal 要落在 drop 上不是附近)。②**质量胜过数量**——几个完美镜头胜一堆平庸镜头,不高级的镜头全砍。\n\n## I — 方法论骨架 (Interpretation):\n\n1. **比例即结构**:五拍占比是骨架——15s/60s 只缩放中段,hook 恒定 3s。\n2. **先锁轨后剪画面**:声音是时间轴的主人,画面是客人。\n3. **Reveal 是唯一高潮**:全片为品牌砸点服务,蒙太奇不许抢戏。\n4. **End card 要静**:定格 ≥2 秒,让品牌落地——CTA 区禁忙动。\n5. **高级感=减法**:砍镜头是发布片的主要创作手段。\n\n## A1 — 应用案例 (Past Application)\n\n- 来源实践:新品发布片先锁 30s 音轨标 drop@9.0s→reveal 精确落点+微过冲(scale 1.18→1.0,0.18s 回弹)→品牌记忆点成立。\n- 反例:先剪画面后配乐——reveal 落在 drop 附近而非点上,冲击力减半。\n\n## A2 — 触发场景 (Future Trigger)\n\n- 新品上市/功能发布/sizzle 片。\n- 「品牌时刻没砸中」的重剪(先查锁轨顺序)。\n- 影策 Agent 生成发布片结构的默认骨架。\n\n## E — 可执行步骤 (Execution)\n\n1. **定总长**:15/30/60s,按比例表铺五拍。\n *完成标准*:五拍占比合规(hook 恒 3s)。\n2. **锁音轨**:先选轨标 beats/drop。\n *完成标准*:drop 时间码已记录。\n3. **排 reveal**:品牌砸点对 drop+微过冲。\n *完成标准*:砸点=drop 帧。\n4. **铺蒙太奇**:一镜一卖点,运动语言统一。\n *完成标准*:卖点-镜头一一对应。\n5. **静尾板**:end card 定格 ≥2s 无竞争动效。\n *完成标准*:尾板静置达标。\n\n---\n\n许可与署名:本技能方法论蒸馏自开源项目 https://github.com/iart-ai/ad-video-skills(MIT),遵循其署名要求;由剧典技能库结构化整理为技能格式。\n", "status": 1, "markdown_url": "", - "create_time": 1789934570, - "update_time": 1789934570, + "create_time": 1789934570000, + "update_time": 1789934570000, "source": 3, "tag": "ecommerce", "sort_weight": 106, "is_private": false, "like_count": 0, - "owner_uid": "", + "owner_uid": "community-itswyatt-k", "effective_user": { "name": "剧典技能库(itsWyatt-K)", "avatar_url": "", - "uid": "" + "uid": "community-itswyatt-k" }, "showcase_media": [], "added_count": 0, @@ -1250,18 +1250,18 @@ "instruction": "# 多画幅安全区法则\n\n## R — 原文要点 (Reading)\n\n来源方法(MIT,蒸馏自 iart launch-video 多画幅章节):**中心安全区构图**——一条母版干净裁到所有画幅。三画幅参数:①**16:9**(YouTube/落地 hero/X)1920×1080,关键内容在中心 90%;②**9:16**(Reels/TikTok/Shorts/Stories)1080×1920,文字在中心 80% 高内,**避开顶部 12% 与底部 18%(平台 UI 区)**;③**1:1**(Feed)1080×1080,即 16:9 帧的中心方区。**核心法则:hero/logo/CTA 全部设计在 1:1 中心方区内**——这样才活得过每次裁切。**母版渲染后重构,不是加黑边**:渲 16:9 母版,再从同一项目重构 9:16 与 1:1(reframe 而非 letterbox)。\n\n## I — 方法论骨架 (Interpretation):\n\n1. **为交集设计**:母版构图按三画幅交集(1:1 中心方区)布关键元素。\n2. **UI 区是禁区**:竖屏的顶 12% 底 18% 永远不放关键信息——平台会挡。\n3. **重构≠信箱**:多画幅是重新构图不是加黑边——黑边版是懒惰税。\n4. **一源多版**:同一项目出多画幅,保风格一致(配 iart-ab-batch-variants 的模板数据驱动)。\n5. **文字最脆弱**:字幕/CTA 是裁切第一受害者,优先护文字。\n\n## A1 — 应用案例 (Past Application)\n\n- 来源实践:hero logo+CTA 锁在 1:1 中心方区→16:9/9:16/1:1 三版裁切后品牌元素全存活。\n- 反例:CTA 放 16:9 右下角→9:16 裁切后 CTA 整个消失。\n\n## A2 — 触发场景 (Future Trigger)\n\n- 一素材多平台投放(千川/Reels/TikTok/YouTube)。\n- 竖屏版主体被裁的修复。\n- 影策 Agent 生成多规格素材时的构图约束。\n\n## E — 可执行步骤 (Execution)\n\n1. **锁交集**:关键元素(hero/logo/CTA/字幕)布进 1:1 中心方区。\n *完成标准*:关键元素全在交集内。\n2. **标禁区**:竖屏顶 12% 底 18% 无关键信息。\n *完成标准*:禁区零占用。\n3. **渲母版**:16:9 母版先出。\n *完成标准*:母版达标。\n4. **重构多版**:9:16/1:1 重新构图(非信箱)。\n *完成标准*:三版无黑边且关键元素存活。\n5. **跨版验收**:逐版对照关键元素清单。\n *完成标准*:三版全存活。\n\n---\n\n许可与署名:本技能方法论蒸馏自开源项目 https://github.com/iart-ai/ad-video-skills(MIT),遵循其署名要求;由剧典技能库结构化整理为技能格式。\n", "status": 1, "markdown_url": "", - "create_time": 1789934570, - "update_time": 1789934570, + "create_time": 1789934570000, + "update_time": 1789934570000, "source": 3, "tag": "ecommerce", "sort_weight": 107, "is_private": false, "like_count": 0, - "owner_uid": "", + "owner_uid": "community-itswyatt-k", "effective_user": { "name": "剧典技能库(itsWyatt-K)", "avatar_url": "", - "uid": "" + "uid": "community-itswyatt-k" }, "showcase_media": [], "added_count": 0, @@ -1274,18 +1274,18 @@ "instruction": "## R — 原文 (Reading)\n\nH3 的 reference-to-video 支持 `reference_image`/`reference_video`/`reference_audio` 三类参考素材;提示词内用 ``、`` 等别名引用。全能参考控制允许同时给多份参考图(角色/场景/道具),模型在生成时保留参考图的身份特征。注意:参考素材与首尾帧(first_frame/last_frame)分属两类输入,不得混在同一请求。角色资产卡的视觉锚点(脸型/发型/服装/配色)由 ip-character-bible 定义,画风规范字段由 ip-style-decoupling 解耦——换画风不重写人物。\n\n## I — 方法论骨架 (Interpretation)\n\n资产绑定三步:\n\n1. **资产卡 → 参考图**:assets.json 里每张角色卡/场景卡/道具卡对应一张参考图(角色定妆照/场景概念图/道具特写)。参考图质量决定一致性上限:角色参考图必须正脸、光线均匀、无遮挡。\n2. **参考图 → 素材别名**:在提示词素材定义段声明——` = 角色A定妆照(reference_image)`、` = 客厅场景图(reference_image)`。一镜内别名唯一。\n3. **别名 → 保留关系**:在保留关系段写\"保留 的脸型/发型/服装\"——文字侧保险,与参考图双保险。漫剧/真人风格差异只写在画风规范字段(如\"2D 国漫平涂风格\"),不改人物描述。\n\n**一致性失效排查序**:先查参考图质量(是否正脸/清晰)→ 再查保留关系段是否抄全视觉锚点 → 最后查是否违规混用首尾帧。\n\n## A1 — 书中的应用 (Past Application)\n\n- **双角色同框**:``=角色A、``=角色B,保留关系段分别声明两人锚点;生成时两人身份不互串。\n- **漫剧改真人**:同一份角色卡,画风规范字段从\"2D 国漫平涂\"改为\"真人实拍质感\",人物描述不动——ip-style-decoupling 的画风解耦在 H3 侧的落地。\n\n## A2 — 触发场景 (Future Trigger)\n\n- 阶 ⑧→⑨ 交接:资产卡确认后,逐张绑定为素材别名,产出绑定清单(assets.json 增补 alias 字段)。\n- 阶 ⑩ 衔接轴:检查角色资产跨镜一致(别名是否全程指向同一参考图)。\n- 语言信号:\"换镜头变脸 / 角色不一致 / 画风漂移\"。\n\n## E — 可执行步骤 (Execution)\n\n1. **核对参考图**:每个角色/场景/道具是否有合格参考图?角色图必须正脸清晰;缺图→先补图再绑定。\n *完成标准*:assets.json 每项含 reference_image 路径。\n2. **分配别名**:按出场顺序给资产分配 ``/``,写入 assets.json 的 alias 字段;全片别名映射唯一且不变。\n *完成标准*:别名映射表生成。\n3. **写进提示词**:每镜素材定义段声明本镜用到的别名;保留关系段抄资产卡视觉锚点。\n *完成标准*:抽任一镜,别名与 assets.json 一致。\n4. **画风检查**:漫剧/真人风格只出现在画风规范字段,人物描述无风格词污染。\n *完成标准*:换画风无需改人物段。\n\n---\n\n许可与署名:本技能方法论蒸馏自开源项目 MiniMax H3 官方技能文档(即梦 Dreamina 官方 skills 语料,raw-materials/MiniMax-H3-skills)(MiniMax 官方文档(署名引用)),遵循其署名要求;由剧典技能库结构化整理为技能格式。\n", "status": 1, "markdown_url": "", - "create_time": 1789934570, - "update_time": 1789934570, + "create_time": 1789934570000, + "update_time": 1789934570000, "source": 3, "tag": "creative", "sort_weight": 108, "is_private": false, "like_count": 0, - "owner_uid": "", + "owner_uid": "community-itswyatt-k", "effective_user": { "name": "剧典技能库(itsWyatt-K)", "avatar_url": "", - "uid": "" + "uid": "community-itswyatt-k" }, "showcase_media": [], "added_count": 0, @@ -1298,18 +1298,18 @@ "instruction": "## R — 原文 (Reading)\n\nH3 的输入分两类且**不得混在同一请求**:`first_frame`/`last_frame` 属图生视频;`reference_image`/`reference_video`/`reference_audio` 属 reference-to-video。连接首尾画面时,首尾输入之间有明确\"变形桥梁\"时衔接效果最好,桥梁可以是颜色、光线、建筑或主体姿势。跨视频衔接公式:`[视频1] + [转场] + 连接到[视频2] + [转场逻辑]`,参考输入分别给视频 1 与视频 2,提示词形如 \"Connect Video 1 to Video 2 in 8 seconds…\"。\n\n## I — 方法论骨架 (Interpretation)\n\n每个镜头在 shot-prompts.json 里必须带一个 `衔接策略` 字段,二选一:\n\n1. **首尾帧衔接**(强约束,用于同场景连续动作):\n - 上一镜的尾帧 = 下一镜的首帧(同一张图,物理保证连续)。\n - 适用:动作接力(起身→走位)、同机位推拉、对白正反打的轴线保持。\n - 注意:用了 first_frame/last_frame 就不能再带 reference_image,角色一致性只能靠提示词文字描述兜底——所以首尾帧方案要求资产卡的视觉锚点抄进保留关系段。\n2. **变形桥衔接**(弱约束,用于跨场景/跨时空):\n - 选一个跨镜头保持的锚:颜色(红伞→红门)、光线(同一束逆光)、建筑(同一面墙)、姿势(同一背影)。\n - 提示词写明桥梁:\"保留上一镜的红色元素作为转场桥梁,连接到…\"。\n - 适用:转场、回忆切入、漫剧分格过渡。\n\n**选哪个**:同场景连续动作→首尾帧;跨场景→变形桥;拿不准→变形桥(不锁死画面,容错高)。\n\n## A1 — 书中的应用 (Past Application)\n\n- **同场景接力**:镜头 3 角色摔门,镜头 4 门外反应——镜头 3 尾帧作镜头 4 首帧,门的位置物理连续,无跳轴感。\n- **跨时空变形桥**:现实办公室冷蓝光 → 回忆老家暖黄光,桥梁选\"主体姿势\"(同一个托腮动作),提示词写\"保留托腮姿势,光线由冷蓝渐变为暖黄,连接到回忆场景\"。\n\n## A2 — 触发场景 (Future Trigger)\n\n- 阶 ⑨:每生成一镜,自动为它与上一镜之间填衔接策略字段。\n- 阶 ⑩ 衔接轴会审:逐对相邻镜头检查——策略是否写明、桥梁锚是否真实存在于两镜、首尾帧方案是否违规混用参考素材。\n- 语言信号:\"接不上 / 跳了 / 转场生硬\"。\n\n## E — 可执行步骤 (Execution)\n\n1. **判类型**:下一镜与上一镜是否同场景连续动作?是→首尾帧;否→变形桥。\n *完成标准*:策略二选一并记录理由。\n2. **首尾帧分支**:导出上一镜尾帧 → 设为下一镜 first_frame;检查下一镜请求未携带 reference_*;把资产卡视觉锚点抄入保留关系段。\n *完成标准*:无输入类型混用。\n3. **变形桥分支**:从颜色/光线/建筑/姿势里选一个在两镜中都存在的锚 → 写进提示词(\"保留 X 作为桥梁,连接到…\")。\n *完成标准*:桥梁锚在两镜画面里都可被指出。\n4. **登记**:把策略写进 shot-prompts.json 该镜的 `衔接策略` 字段。\n *完成标准*:字段非空,会审衔接轴可读。\n\n---\n\n许可与署名:本技能方法论蒸馏自开源项目 MiniMax H3 官方技能文档(即梦 Dreamina 官方 skills 语料,raw-materials/MiniMax-H3-skills)(MiniMax 官方文档(署名引用)),遵循其署名要求;由剧典技能库结构化整理为技能格式。\n", "status": 1, "markdown_url": "", - "create_time": 1789934570, - "update_time": 1789934570, + "create_time": 1789934570000, + "update_time": 1789934570000, "source": 3, "tag": "creative", "sort_weight": 109, "is_private": false, "like_count": 0, - "owner_uid": "", + "owner_uid": "community-itswyatt-k", "effective_user": { "name": "剧典技能库(itsWyatt-K)", "avatar_url": "", - "uid": "" + "uid": "community-itswyatt-k" }, "showcase_media": [], "added_count": 0, @@ -1322,18 +1322,18 @@ "instruction": "## R — 原文 (Reading)\n\nMiniMax H3 接收文字、图片、视频、音频四类素材,四种输入模式:\n- **T2VA**(文生视频)/ **I2VA**(图生视频,含首尾帧 first_frame/last_frame)/ **FL2VA**(首尾帧+参考)/ **L2VA**(参考图/视频/音频 reference_image/reference_video/reference_audio)。\n- 首尾帧与参考素材**两类输入不得混在同一请求**(选其一)。\n- 提示词内 ``/``/`