refactor(*): 稳定期治理 - 收敛核心架构、错误语义并修复跨项目数据竞态
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
// AI 编辑命令契约(ADR-0007 / Runbook M6.1):宿主校验 + LLM 提示词约束的双契约。
|
||||
// 本文件不复制 handler 约束——命令合法性硬守门在 M2.1 命令注册表(fail-closed),
|
||||
// 这里只做:(1) LLM 输出 JSON 的结构解析与批级预检(dry-run 走同一注册表,零重复),
|
||||
// (2) 生成喂给 LLM 的 op 契约(op 集合与注册表黄金同步,见测试)。
|
||||
// AI 编辑命令的边界层:同一份注册表同时约束模型输出和宿主执行,避免提示词与真实能力列表分叉。
|
||||
// 本文件只负责 JSON 结构解析、批量预检和提示词契约生成;命令的参数合法性仍由编辑命令注册表统一判定。
|
||||
// 预检使用与正式执行相同的纯函数注册表,保证“模型看见的能力”和“宿主实际允许的能力”一致。
|
||||
|
||||
import { getEditorCommandRegistry, type EditCommand } from "@/lib/timeline/editor-commands";
|
||||
import type { TimelineProject } from "@/types/timeline";
|
||||
@@ -79,8 +78,8 @@ function assertPlanShape(raw: unknown): asserts raw is AiCommandPlan {
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 LLM 文本输出为 AiCommandPlan;结构非法时抛 AiCommandPlanError(供 UI 展示并回填模型)。
|
||||
* commands 为空数组表示「只读问答 / 无法用现有命令完成」,面板直接展示 reasoning 作为回答。
|
||||
* 解析模型文本为 AiCommandPlan;结构非法时抛 AiCommandPlanError,调用方可将明确原因展示给用户或回传模型修正。
|
||||
* commands 为空数组表示只读问答或当前能力无法完成,调用方不应把它当成“执行成功的空修改”。
|
||||
*/
|
||||
export function parseAiCommandPlan(text: string): AiCommandPlan {
|
||||
let raw: unknown;
|
||||
@@ -96,8 +95,8 @@ export function parseAiCommandPlan(text: string): AiCommandPlan {
|
||||
|
||||
/**
|
||||
* 批级预检:把整批命令按序 dry-run 到当前时间线(immutable 纯函数,无副作用)。
|
||||
* 任何一条触发注册表 fail-closed(未知 op / 非法 payload / 引用不存在)即整批拒绝,
|
||||
* 返回首个出错命令的下标与消息——ADR-0007「任一非法→整批拒绝,错误回填 LLM 修正」。
|
||||
* 任何一条触发注册表的拒绝规则(未知 op、非法 payload 或引用不存在)即整批拒绝,
|
||||
* 返回首个失败命令的位置和原因;正式执行不得绕过这一步或只提交部分命令。
|
||||
*/
|
||||
export function validateAiCommandBatch(project: TimelineProject, plan: AiCommandPlan): AiCommandBatchVerdict {
|
||||
const registry = getEditorCommandRegistry();
|
||||
@@ -114,13 +113,13 @@ export function validateAiCommandBatch(project: TimelineProject, plan: AiCommand
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LLM 提示词契约(ADR-0007:schema 同时用于宿主校验与模型输出约束)
|
||||
// 模型提示词契约:schema 同时用于宿主校验与模型输出约束,具体交互决策见 ADR-0007。
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** AI 命令 schema 版本(Runbook M6.1):提示词契约/解析规则变更时递增,供 golden 与缓存失效对齐。 */
|
||||
/** AI 命令 schema 版本;提示词契约或解析规则变化时递增,用于 golden 测试和缓存失效对齐。 */
|
||||
export const AI_COMMAND_SCHEMA_VERSION = 1;
|
||||
|
||||
/** 单次 AI 编辑最多允许的命令条数(ADR-0007:防止模型一次输出失控编辑)。 */
|
||||
/** 单次 AI 编辑最多允许的命令条数,防止一次模型输出造成不可控的大范围变更。 */
|
||||
export const AI_EDITING_MAX_COMMANDS = 8;
|
||||
|
||||
/** 12 个黄金 op 的 LLM 可读 payload 契约;op 集合与注册表黄金同步(见测试)。 */
|
||||
@@ -203,7 +202,7 @@ export const AI_EDITING_OUTPUT_FORMAT = `输出要求:
|
||||
const AI_EDITING_OUTPUT_EXAMPLE = `示例:
|
||||
{ "reasoning": "把第一个视频片段移到第 5 秒", "commands": [ { "op": "moveClip", "payload": { "id": "clip-a", "startMs": 5000 } } ] }`;
|
||||
|
||||
/** 组装 AI 编辑系统提示词:时间线摘要 + op 契约 + 输出约束(M6.3 面板喂给模型)。 */
|
||||
/** 组装 AI 编辑系统提示词:时间线摘要、可用 op 契约和输出约束必须来自同一份注册表。 */
|
||||
export function buildAiEditingSystemPrompt(timelineSummary: string): string {
|
||||
const catalogLines = AI_EDITING_OP_CATALOG.map(
|
||||
(entry) => `- ${entry.op}:${entry.desc}。payload:${entry.payload}`,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// 资产 → 时间线片段 的纯转换(M3.4 asset-ingest)。
|
||||
// 资产到时间线片段的纯转换:只做类型映射和结构组装,不读取网络或修改编辑器状态。
|
||||
// 资产是"仅时间线作用域"的直连媒体:nodeId 用 `asset:<id>` 前缀(不指向画布节点),
|
||||
// 预览/导出从 directMedia 解析媒体,不回画布查节点。
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export type EditorCommandRegistry = {
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 黄金命令集合 payload 类型(首版一期能力,对应 Runbook M2.1)
|
||||
// 内置命令的 payload 类型;注册表、AI 命令契约和黄金测试必须共享同一组 op。
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type AddClipPayload = { clip: TimelineClip };
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// Mock 转写(M3.6):M4 后端转写任务未就绪前,用确定性伪转写模拟 ASR 结果。
|
||||
// 同一 title + durationMs 永远生成相同的 SrtEntry[],便于测试与回归;
|
||||
// M4 接真实任务客户端后本模块移除,UI 契约(SrtEntry[] 落字幕轨道)不变。
|
||||
|
||||
import type { SrtEntry } from "@/types/timeline";
|
||||
|
||||
const MOCK_CHUNK_MS = 1800;
|
||||
const MOCK_GAP_MS = 200;
|
||||
|
||||
/** 从标题确定性生成一组模拟字幕条目(基于字符串 hash 稳定切分)。 */
|
||||
export function mockTranscriptionEntries(title: string, durationMs: number): SrtEntry[] {
|
||||
const text = title.trim() || "未命名素材";
|
||||
const chunks = splitStable(text, 6);
|
||||
const usable = Math.max(1, Math.floor(durationMs / (MOCK_CHUNK_MS + MOCK_GAP_MS)));
|
||||
const count = Math.min(chunks.length, usable);
|
||||
const step = durationMs / count;
|
||||
|
||||
const entries: SrtEntry[] = [];
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
const startMs = Math.round(i * step);
|
||||
const endMs = Math.min(durationMs, Math.round(startMs + MOCK_CHUNK_MS));
|
||||
if (endMs <= startMs) break;
|
||||
entries.push({ index: i, startMs, endMs, text: chunks[i % chunks.length] });
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
/** 稳定切分:按可打印字符逐段取满 maxLen,确定性无随机。 */
|
||||
function splitStable(text: string, maxLen: number): string[] {
|
||||
const chars = Array.from(text);
|
||||
const chunks: string[] = [];
|
||||
for (let i = 0; i < chars.length; i += maxLen) {
|
||||
chunks.push(chars.slice(i, i + maxLen).join(""));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import type { SrtEntry } from "@/types/timeline";
|
||||
|
||||
export interface SubtitleHighlightLLMResult {
|
||||
entryIndex: number;
|
||||
shouldHighlight: boolean;
|
||||
highlightText: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export function buildSubtitleHighlightSystemPrompt(): string {
|
||||
return `你是一个视频字幕关键词高亮助手。请输出严格 JSON。
|
||||
|
||||
任务目标:
|
||||
- 为每条字幕判断是否需要高亮
|
||||
- 每条字幕最多高亮 1 段
|
||||
- 只返回最值得高亮的一个关键词或短语
|
||||
- start 和 end 使用 JavaScript 字符串下标
|
||||
- end 为 exclusive
|
||||
- 没有明确重点时返回 shouldHighlight=false
|
||||
|
||||
高亮标准:
|
||||
- 优先高亮结论词、数字、身份词、首次/突破词、强反差词
|
||||
- 不要高亮虚词、口头语、整句长短语
|
||||
|
||||
边界约束:
|
||||
- 不负责颜色、字号、位置、动效
|
||||
- 只返回 highlights 数组
|
||||
- highlightText 必须严格等于原字幕文本切片结果`;
|
||||
}
|
||||
|
||||
export function buildSubtitleHighlightUserMessage(entries: SrtEntry[]): string {
|
||||
const lines = entries.map((entry) => {
|
||||
return `entryIndex=${entry.index} | startMs=${entry.startMs} | endMs=${entry.endMs} | text=${entry.text}`;
|
||||
});
|
||||
|
||||
return `请根据以下字幕和上下文,为每条字幕判断是否需要高亮,并返回 JSON:
|
||||
|
||||
{
|
||||
"highlights": [
|
||||
{
|
||||
"entryIndex": 1,
|
||||
"shouldHighlight": true,
|
||||
"highlightText": "示例",
|
||||
"start": 0,
|
||||
"end": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
字幕列表:
|
||||
${lines.join("\n")}`;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { runBackendCanvasGenerationTask } from "@/lib/canvas/canvas-project-generation";
|
||||
import type { AiConfig } from "@/stores/use-config-store";
|
||||
import type { SrtEntry, SubtitleHighlight } from "@/types/timeline";
|
||||
import { buildSubtitleHighlightSystemPrompt, buildSubtitleHighlightUserMessage } from "./subtitle-highlight-ai";
|
||||
import { buildSubtitleHighlightSystemPrompt, buildSubtitleHighlightUserMessage } from "@/lib/prompts";
|
||||
import { parseSubtitleHighlightResponse } from "./subtitle-highlight-service";
|
||||
|
||||
export interface SubtitleHighlightProgress {
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import type { SrtEntry, SubtitleHighlight } from "@/types/timeline";
|
||||
import { filterValidSubtitleHighlights } from "./subtitle-highlights";
|
||||
import type { SubtitleHighlightLLMResult } from "./subtitle-highlight-ai";
|
||||
interface SubtitleHighlightLLMResult {
|
||||
entryIndex: number;
|
||||
shouldHighlight: boolean;
|
||||
highlightText: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
function isSubtitleHighlightLLMResult(value: unknown): value is SubtitleHighlightLLMResult {
|
||||
if (!value || typeof value !== "object") {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// 时间线确定性摘要(Runbook M6.2):把 TimelineProject v2 压缩成喂给 LLM 的中文上下文。
|
||||
// 时间线确定性摘要:把 TimelineProject 压缩成稳定、可控长度的中文上下文,供 AI 编辑和诊断使用。
|
||||
// 同一输入永远产出同一输出(无时间戳/无随机),供命令引用 id 与 AI 面板上下文注入。
|
||||
|
||||
import type { TimelineClip, TimelineProject, TimelineTrack } from "@/types/timeline";
|
||||
|
||||
Reference in New Issue
Block a user