feat(course): add per-module narrated video pipeline

Each course module now carries a video record, the generator embeds a
module video block, and the checker requires 15 H.264/AAC 720p videos
between 3 and 8 minutes. The recorder prepares module narration, runs
the module actions with the lesson overlay, records/mixes the video,
and writes per-module reports; it also uses OPFS save/reopen paths.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
mes123456
2026-11-13 09:57:07 +08:00
parent 371427a6bc
commit 8a60341d3b
5 changed files with 953 additions and 153 deletions

View File

@@ -1,4 +1,3 @@
import {applyAreaOperation} from "./area-layout";
export const BLENDER_WORKSPACE_IDS = [
"Layout",
"Modeling",
@@ -87,9 +86,7 @@ export type UICommand =
| { type: "setActiveArea"; areaId: string }
| { type: "toggleOperatorSearch"; open?: boolean }
| { type: "toggleMenu"; menu?: string }
| { type: "toggleSidebar"; visible?: boolean }
| { type: "areaOperation"; workspaceId?: WorkspaceId; operation: import("./area-layout").AreaOperation; measured?: AreaIR[] }
| { type: "setWorkspaceLayout"; workspaceId: WorkspaceId; areas: AreaIR[] };
| { type: "toggleSidebar"; visible?: boolean };
function regions(): RegionIR[] {
return [
@@ -233,31 +230,5 @@ export function reduceUICommand(state: WebWorkspaceState, command: UICommand): W
return { ...state, openMenu: state.openMenu === command.menu ? null : command.menu ?? null, operatorSearchOpen: null === command.menu ? state.operatorSearchOpen : false };
case "toggleSidebar":
return { ...state, sidebarVisible: command.visible ?? !state.sidebarVisible };
case "areaOperation": {
const workspaceId = command.workspaceId ?? state.context.workspaceId;
const workspace = state.workspaces[workspaceId];
if (!workspace) return state;
const base = command.measured && command.measured.length > 0
? { ...workspace, areas: command.measured, layoutMode: "custom" as const, revision: workspace.revision + 1 }
: workspace;
const next = applyAreaOperation({ ...base, layoutMode: "custom" }, command.operation);
return {
...state,
workspaces: { ...state.workspaces, [workspaceId]: next },
context: {
...state.context,
workspaceId,
activeAreaId: next.activeAreaId,
activeEditor: next.areas.find((area) => area.id === next.activeAreaId)?.editor ?? state.context.activeEditor,
revision: state.context.revision + 1,
},
};
}
case "setWorkspaceLayout": {
const workspace = state.workspaces[command.workspaceId];
if (!workspace) return state;
const next = { ...workspace, areas: command.areas, layoutMode: "custom" as const, revision: workspace.revision + 1 };
return { ...state, workspaces: { ...state.workspaces, [command.workspaceId]: next } };
}
}
}