yonge 1 неделя назад
Родитель
Сommit
9fe033b0f2

+ 1 - 1
dist/assets/index-BWpLu7Ty.js → dist/assets/index-0KJWMmVV.js

@@ -1 +1 @@
-import{_ as t}from"./404-R0y5dkvZ.js";import{E as c}from"./index-D8MJNqy7.js";import{a5 as n,ae as i,af as r,ak as o,aq as _,am as d,P as l,aS as p,bM as m,bN as f,bH as u}from"./index-Doi9nW2k.js";const s=a=>(m("data-v-340bbdaa"),a=a(),f(),a),h={class:"login"},b={class:"error"},g=s(()=>o("img",{src:t,class:"img",alt:""},null,-1)),k=s(()=>o("div",{class:"tit"},"登录已过期或服务器错误!",-1)),v=n({__name:"login",setup(a){function e(){window.close()}return(x,B)=>(i(),r("div",h,[o("div",b,[g,k,_(l(c),{class:"backBtn",type:"primary",plain:"",onClick:e},{default:d(()=>[p("关闭页面")]),_:1})])]))}}),S=u(v,[["__scopeId","data-v-340bbdaa"]]);export{S as default};
+import{_ as t}from"./404-R0y5dkvZ.js";import{E as c}from"./index-Buzi_PL7.js";import{a5 as n,ae as i,af as r,ak as o,aq as _,am as d,P as l,aS as p,bM as m,bN as f,bH as u}from"./index-vIcuuR4g.js";const s=a=>(m("data-v-340bbdaa"),a=a(),f(),a),h={class:"login"},b={class:"error"},g=s(()=>o("img",{src:t,class:"img",alt:""},null,-1)),k=s(()=>o("div",{class:"tit"},"登录已过期或服务器错误!",-1)),v=n({__name:"login",setup(a){function e(){window.close()}return(x,B)=>(i(),r("div",h,[o("div",b,[g,k,_(l(c),{class:"backBtn",type:"primary",plain:"",onClick:e},{default:d(()=>[p("关闭页面")]),_:1})])]))}}),S=u(v,[["__scopeId","data-v-340bbdaa"]]);export{S as default};

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
dist/assets/index-Bp4m2kxY.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
dist/assets/index-Buzi_PL7.js


+ 1 - 1
dist/assets/index-CEqi0-5P.js → dist/assets/index-CVwIohN9.js

@@ -1 +1 @@
-import{_ as a}from"./404-R0y5dkvZ.js";import{a5 as o,en as t,ae as r,af as c,bM as _,bN as d,ak as s,bH as p}from"./index-Doi9nW2k.js";const i=e=>(_("data-v-dda25179"),e=e(),d(),e),n={class:"errorPage"},m=i(()=>s("div",{class:"error"},[s("img",{src:a,class:"img",alt:""}),s("div",{class:"tit"},"页面找不到了~")],-1)),l=[m],f=o({__name:"errorPage",setup(e){return t(),(u,h)=>(r(),c("div",n,l))}}),I=p(f,[["__scopeId","data-v-dda25179"]]);export{I as default};
+import{_ as a}from"./404-R0y5dkvZ.js";import{a5 as o,en as t,ae as r,af as c,bM as _,bN as d,ak as s,bH as p}from"./index-vIcuuR4g.js";const i=e=>(_("data-v-dda25179"),e=e(),d(),e),n={class:"errorPage"},m=i(()=>s("div",{class:"error"},[s("img",{src:a,class:"img",alt:""}),s("div",{class:"tit"},"页面找不到了~")],-1)),l=[m],f=o({__name:"errorPage",setup(e){return t(),(u,h)=>(r(),c("div",n,l))}}),I=p(f,[["__scopeId","data-v-dda25179"]]);export{I as default};

+ 1 - 1
dist/assets/index-Dv5BViNo.js → dist/assets/index-Cwlda7Q4.js

@@ -1 +1 @@
-import{u as c,af as l}from"./index-CPy5wlue.js";import{w as p,bA as d,a5 as g,cB as m,R as t,ae as r,af as u,al as f,ag as w,bH as k}from"./index-Doi9nW2k.js";import"./shapes-DZJHFobw.js";const S=()=>{function o(a){if(window.parent===window)return;const{api:e,playState:n}=a.data||{};e==="headerTogge"?window.parent.postMessage({api:"headerTogge",playState:n},"*"):e==="changeTogge"?window.parent.postMessage({api:"changeTogge"},"*"):["clickTempo","clickViewFigner"].includes(e)&&window.parent.postMessage({api:e},"*")}p(()=>{window.addEventListener("message",o)}),d(()=>{window.removeEventListener("message",o)})},_=g({__name:"mobileScreen",setup(o){S();const a=m(),e=t(!0),n=t(!1),i=c();return a.setScreenMode("mobileScreen"),i.initPPTData().then(()=>{e.value=!1}),(v,s)=>(r(),u("div",{class:"mobileScreen",onClick:s[0]||(s[0]=b=>n.value=!1)},[e.value?w("",!0):(r(),f(l,{key:0}))]))}}),B=k(_,[["__scopeId","data-v-90b2692a"]]);export{B as default};
+import{u as c,af as l}from"./index-DMJHYSHL.js";import{w as p,bA as d,a5 as g,cB as m,R as t,ae as r,af as u,al as f,ag as w,bH as k}from"./index-vIcuuR4g.js";import"./shapes-Cp7YSuKf.js";const S=()=>{function o(a){if(window.parent===window)return;const{api:e,playState:n}=a.data||{};e==="headerTogge"?window.parent.postMessage({api:"headerTogge",playState:n},"*"):e==="changeTogge"?window.parent.postMessage({api:"changeTogge"},"*"):["clickTempo","clickViewFigner"].includes(e)&&window.parent.postMessage({api:e},"*")}p(()=>{window.addEventListener("message",o)}),d(()=>{window.removeEventListener("message",o)})},_=g({__name:"mobileScreen",setup(o){S();const a=m(),e=t(!0),n=t(!1),i=c();return a.setScreenMode("mobileScreen"),i.initPPTData().then(()=>{e.value=!1}),(v,s)=>(r(),u("div",{class:"mobileScreen",onClick:s[0]||(s[0]=b=>n.value=!1)},[e.value?w("",!0):(r(),f(l,{key:0}))]))}}),B=k(_,[["__scopeId","data-v-90b2692a"]]);export{B as default};

+ 1 - 1
dist/assets/index-CPy5wlue.js → dist/assets/index-DMJHYSHL.js

@@ -1,4 +1,4 @@
-import{cF as dw,cG as hw,aG as vw,cH as pw,cI as gw,cJ as mw,cK as yw,ah as Yt,cL as Sw,cM as bw,cN as xw,cO as Cw,aM as ww,aF as Tw,cP as Iw,aB as c1,bg as _w,cQ as Aw,cR as Ew,cS as Pw,cT as Mw,cU as Rw,cV as Dw,cW as Lw,aE as kw,cX as Ow,s as Ae,bl as Bw,al as je,ag as ze,af as ve,ak as Z,cY as Nw,cZ as Fw,c_ as $w,c$ as zw,bk as Uw,cw as ul,aS as Cv,aq as Ye,d0 as Hw,d1 as Vw,a5 as $e,d2 as Gw,d3 as Ww,d4 as jw,d5 as Kw,d6 as Xw,d7 as Yw,d8 as Zw,d9 as Qw,da as Jw,db as qw,dc as eT,v as tT,dd as rT,de as nT,df as iT,aW as aT,dg as oT,dh as sT,di as lT,dj as uT,dk as cT,ay as sr,dl as fT,dm as dT,dn as hT,dp as vT,A as pT,dq as gT,dr as mT,bq as yT,ds as ST,dt as bT,du as xT,ap as CT,ac as f1,ai as dt,dv as wT,au as Ce,dw as TT,X as d1,y as IT,dx as _T,aP as AT,dy as ET,w as _r,dz as PT,dA as MT,H as RT,dB as DT,bA as hn,aD as LT,ae as ie,bN as ea,$ as wc,dC as kT,bM as ta,dD as OT,aC as BT,aO as NT,R as fe,dE as FT,br as rh,b3 as Lr,aj as Hs,b1 as lt,b2 as Ua,an as wv,dF as $T,dG as zT,dH as UT,dI as HT,dJ as VT,dK as GT,dL as WT,Q as jT,dM as KT,dN as XT,dO as YT,at as Mt,dP as ZT,aR as QT,aY as JT,ad as cl,aZ as qT,dQ as eI,dR as tI,dS as rI,P as le,a6 as nI,dT as iI,dU as aI,dV as oI,dW as sI,a7 as lI,dX as uI,dY as cI,dZ as fI,d_ as dI,d$ as hI,b4 as vI,aA as Tv,e0 as pI,e1 as gI,E as gr,x as mI,e2 as yI,e3 as SI,e4 as bI,am as _n,e5 as xI,az as hr,aU as nh,e6 as CI,as as jr,e7 as wI,e8 as Iv,cE as mr,by as ih,ct as ah,bI as TI,bH as Ve,bu as oh,bv as _v,bx as Wt,bB as Tc,bL as di,e9 as wu,bE as hi,ea as II,bJ as ra,bC as _I,bw as Bt,eb as AI,ec as ui,cu as EI,ed as PI,ee as MI,ef as RI,eg as DI,bK as Zp,eh as LI,bF as Qp,cB as Ic,cA as kI,ei as Hu,ej as Il}from"./index-Doi9nW2k.js";/**
+import{cF as dw,cG as hw,aG as vw,cH as pw,cI as gw,cJ as mw,cK as yw,ah as Yt,cL as Sw,cM as bw,cN as xw,cO as Cw,aM as ww,aF as Tw,cP as Iw,aB as c1,bg as _w,cQ as Aw,cR as Ew,cS as Pw,cT as Mw,cU as Rw,cV as Dw,cW as Lw,aE as kw,cX as Ow,s as Ae,bl as Bw,al as je,ag as ze,af as ve,ak as Z,cY as Nw,cZ as Fw,c_ as $w,c$ as zw,bk as Uw,cw as ul,aS as Cv,aq as Ye,d0 as Hw,d1 as Vw,a5 as $e,d2 as Gw,d3 as Ww,d4 as jw,d5 as Kw,d6 as Xw,d7 as Yw,d8 as Zw,d9 as Qw,da as Jw,db as qw,dc as eT,v as tT,dd as rT,de as nT,df as iT,aW as aT,dg as oT,dh as sT,di as lT,dj as uT,dk as cT,ay as sr,dl as fT,dm as dT,dn as hT,dp as vT,A as pT,dq as gT,dr as mT,bq as yT,ds as ST,dt as bT,du as xT,ap as CT,ac as f1,ai as dt,dv as wT,au as Ce,dw as TT,X as d1,y as IT,dx as _T,aP as AT,dy as ET,w as _r,dz as PT,dA as MT,H as RT,dB as DT,bA as hn,aD as LT,ae as ie,bN as ea,$ as wc,dC as kT,bM as ta,dD as OT,aC as BT,aO as NT,R as fe,dE as FT,br as rh,b3 as Lr,aj as Hs,b1 as lt,b2 as Ua,an as wv,dF as $T,dG as zT,dH as UT,dI as HT,dJ as VT,dK as GT,dL as WT,Q as jT,dM as KT,dN as XT,dO as YT,at as Mt,dP as ZT,aR as QT,aY as JT,ad as cl,aZ as qT,dQ as eI,dR as tI,dS as rI,P as le,a6 as nI,dT as iI,dU as aI,dV as oI,dW as sI,a7 as lI,dX as uI,dY as cI,dZ as fI,d_ as dI,d$ as hI,b4 as vI,aA as Tv,e0 as pI,e1 as gI,E as gr,x as mI,e2 as yI,e3 as SI,e4 as bI,am as _n,e5 as xI,az as hr,aU as nh,e6 as CI,as as jr,e7 as wI,e8 as Iv,cE as mr,by as ih,ct as ah,bI as TI,bH as Ve,bu as oh,bv as _v,bx as Wt,bB as Tc,bL as di,e9 as wu,bE as hi,ea as II,bJ as ra,bC as _I,bw as Bt,eb as AI,ec as ui,cu as EI,ed as PI,ee as MI,ef as RI,eg as DI,bK as Zp,eh as LI,bF as Qp,cB as Ic,cA as kI,ei as Hu,ej as Il}from"./index-vIcuuR4g.js";/**
 * vue v3.4.34
 * vue v3.4.34
 * (c) 2018-present Yuxi (Evan) You and Vue contributors
 * (c) 2018-present Yuxi (Evan) You and Vue contributors
 * @license MIT
 * @license MIT

+ 1 - 1
dist/assets/index-EUiDQWTW.js → dist/assets/index-DzVY2zkY.js

@@ -1 +1 @@
-import{u as s,af as r}from"./index-CPy5wlue.js";import{a5 as c,cB as n,R as p,ae as t,af as _,al as u,ag as i,bH as f}from"./index-Doi9nW2k.js";const l={class:"pptScreen"},m=c({__name:"pptScreen",setup(d){const a=n(),e=p(!0),o=s();return a.setScreenMode("pptScreen"),o.initPPTData().then(()=>{e.value=!1}),(S,k)=>(t(),_("div",l,[e.value?i("",!0):(t(),u(r,{key:0}))]))}}),P=f(m,[["__scopeId","data-v-678905e9"]]);export{P as default};
+import{u as s,af as r}from"./index-DMJHYSHL.js";import{a5 as c,cB as n,R as p,ae as t,af as _,al as u,ag as i,bH as f}from"./index-vIcuuR4g.js";const l={class:"pptScreen"},m=c({__name:"pptScreen",setup(d){const a=n(),e=p(!0),o=s();return a.setScreenMode("pptScreen"),o.initPPTData().then(()=>{e.value=!1}),(S,k)=>(t(),_("div",l,[e.value?i("",!0):(t(),u(r,{key:0}))]))}}),P=f(m,[["__scopeId","data-v-678905e9"]]);export{P as default};

Разница между файлами не показана из-за своего большого размера
+ 1 - 1
dist/assets/index-vIcuuR4g.js


+ 1 - 1
dist/assets/shapes-DZJHFobw.js → dist/assets/shapes-Cp7YSuKf.js

@@ -1,4 +1,4 @@
-import{cE as k,bB as $1}from"./index-Doi9nW2k.js";import{ag as Z,ah as H1,ai as U,aj as n0,ak as c0,al as i0,am as j1,an as K1,ao as Y1,ap as R}from"./index-CPy5wlue.js";var z1={exports:{}};/*!
+import{cE as k,bB as $1}from"./index-vIcuuR4g.js";import{ag as Z,ah as H1,ai as U,aj as n0,ak as c0,al as i0,am as j1,an as K1,ao as Y1,ap as R}from"./index-DMJHYSHL.js";var z1={exports:{}};/*!
  * clipboard.js v2.0.11
  * clipboard.js v2.0.11
  * https://clipboardjs.com/
  * https://clipboardjs.com/
  *
  *

+ 1 - 1
dist/index.html

@@ -10,7 +10,7 @@
     <meta name="description" content="ppt编辑器" />
     <meta name="description" content="ppt编辑器" />
     <meta name="keywords" content="ppt,powerpoint,office powerpoint,在线ppt,幻灯片,演示文稿,ppt在线制作,Vue3,TypeScript" />
     <meta name="keywords" content="ppt,powerpoint,office powerpoint,在线ppt,幻灯片,演示文稿,ppt在线制作,Vue3,TypeScript" />
     <title>PPT</title>
     <title>PPT</title>
-    <script type="module" crossorigin src="./assets/index-Doi9nW2k.js"></script>
+    <script type="module" crossorigin src="./assets/index-vIcuuR4g.js"></script>
     <link rel="stylesheet" crossorigin href="./assets/index-AavUEaYy.css">
     <link rel="stylesheet" crossorigin href="./assets/index-AavUEaYy.css">
   </head>
   </head>
   <body>
   <body>

+ 3 - 0
src/api/aiCourseware.ts

@@ -69,6 +69,9 @@ export interface AiCoursewarePage {
     url: string
     url: string
     keyword: string
     keyword: string
     provider: string
     provider: string
+    width?: number
+    height?: number
+    score?: number
   }
   }
 }
 }
 
 

+ 361 - 338
src/utils/aiPptTemplateEngine.ts

@@ -1,495 +1,511 @@
 import { nanoid } from "nanoid"
 import { nanoid } from "nanoid"
-import type { Slide, PPTElement, SlideTheme, PPTTextElement, PPTImageElement, PPTShapeElement, SlideBackground, PPTElementShadow } from "@/types/slides"
+import type {
+  Gradient,
+  PPTElement,
+  PPTElementShadow,
+  PPTImageElement,
+  PPTLineElement,
+  PPTShapeElement,
+  PPTTextElement,
+  Slide,
+  SlideBackground,
+  SlideTheme,
+} from "@/types/slides"
 import type { AiCoursewareOutline, AiCoursewarePage } from "@/api/aiCourseware"
 import type { AiCoursewareOutline, AiCoursewarePage } from "@/api/aiCourseware"
 
 
 interface AiPptOptions {
 interface AiPptOptions {
   backgroundImage?: string
   backgroundImage?: string
 }
 }
 
 
+type PageRole = "cover" | "intro" | "goal" | "story" | "listen" | "lyric" | "knowledge" | "rhythm" | "interaction" | "activity" | "summary" | "homework" | "content"
+
 interface Palette {
 interface Palette {
   primary: string
   primary: string
   secondary: string
   secondary: string
   accent: string
   accent: string
   warm: string
   warm: string
   background: string
   background: string
-  panel: string
-  panelAlt: string
+  surface: string
+  soft: string
   text: string
   text: string
   muted: string
   muted: string
+  dark: string
 }
 }
 
 
-interface VisualRect {
+interface ImageRect {
   left: number
   left: number
   top: number
   top: number
   width: number
   width: number
   height: number
   height: number
+  radius?: number
 }
 }
 
 
 const SLIDE_WIDTH = 1600
 const SLIDE_WIDTH = 1600
 const SLIDE_HEIGHT = 900
 const SLIDE_HEIGHT = 900
 const FONT_NAME = "Microsoft Yahei"
 const FONT_NAME = "Microsoft Yahei"
+const GENERIC_PAGE_TITLES = new Set(["封面", "课程导入", "故事导入", "导入", "首页", "cover"])
 
 
 export function buildAiPptSlides(outline: AiCoursewareOutline, options: AiPptOptions = {}): { title: string; theme: Partial<SlideTheme>; slides: Slide[] } {
 export function buildAiPptSlides(outline: AiCoursewareOutline, options: AiPptOptions = {}): { title: string; theme: Partial<SlideTheme>; slides: Slide[] } {
-  const palette = buildPalette(outline)
+  const normalized = normalizeOutline(outline)
+  const palette = buildPalette(normalized)
+  const slides = normalized.pages.map((page, index) => buildSlide(page, normalized, palette, options, index))
+  runPreflight(slides)
   return {
   return {
-    title: outline.title || outline.chapterName || "音乐课件",
+    title: normalized.title || normalized.chapterName || "音乐课件",
     theme: {
     theme: {
       themeColor: palette.primary,
       themeColor: palette.primary,
       backgroundColor: palette.background,
       backgroundColor: palette.background,
       fontColor: palette.text,
       fontColor: palette.text,
       fontName: FONT_NAME,
       fontName: FONT_NAME,
-      fontSize: "36px"
+      fontSize: "36px",
     },
     },
-    slides: outline.pages.map(page => buildSlide(page, outline, palette, options))
+    slides,
   }
   }
 }
 }
 
 
-function buildSlide(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, options: AiPptOptions): Slide {
-  const elements = baseElements(palette, options)
-  const kind = pageKind(page)
-
-  if (page.pageNo === 1 || kind.includes("cover")) {
-    elements.push(...coverTemplate(page, outline, palette))
-  } else if (kind.includes("goal")) {
-    elements.push(...cardsTemplate(page, outline, palette, "学习目标"))
-  } else if (kind.includes("listen")) {
-    elements.push(...listenTemplate(page, outline, palette))
-  } else if (kind.includes("lyric") || kind.includes("content")) {
-    elements.push(...contentTemplate(page, outline, palette))
-  } else if (kind.includes("rhythm") || kind.includes("skill")) {
-    elements.push(...rhythmTemplate(page, outline, palette))
-  } else if (kind.includes("knowledge") || kind.includes("extension")) {
-    elements.push(...cardsTemplate(page, outline, palette, "音乐知识"))
-  } else if (kind.includes("interaction") || kind.includes("activity")) {
-    elements.push(...activityTemplate(page, outline, palette))
-  } else if (kind.includes("summary")) {
-    elements.push(...summaryTemplate(page, outline, palette))
-  } else if (kind.includes("homework")) {
-    elements.push(...homeworkTemplate(page, outline, palette))
-  } else {
-    elements.push(...splitTemplate(page, outline, palette))
-  }
-
+function buildSlide(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, options: AiPptOptions, index: number): Slide {
+  const role = pageRole(page, index)
+  const elements = templateFor(role)(page, outline, palette, index)
   return {
   return {
     id: nanoid(10),
     id: nanoid(10),
     elements: keepInCanvas(elements),
     elements: keepInCanvas(elements),
-    background: backgroundConfig(palette, options),
-    remark: slideRemark(page)
+    background: backgroundConfig(role, palette, options),
+    remark: slideRemark(page),
+  }
+}
+
+function templateFor(role: PageRole) {
+  switch (role) {
+    case "cover": return coverTemplate
+    case "goal": return goalTemplate
+    case "listen": return listenTemplate
+    case "lyric": return lyricTemplate
+    case "rhythm": return rhythmTemplate
+    case "knowledge": return knowledgeTemplate
+    case "interaction": return interactionTemplate
+    case "activity": return activityTemplate
+    case "summary": return summaryTemplate
+    case "homework": return homeworkTemplate
+    case "intro":
+    case "story": return storyTemplate
+    default: return contentTemplate
   }
   }
 }
 }
 
 
 function coverTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
 function coverTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
+  const title = chapterTitle(outline, page)
+  const subtitle = [outline.gradeName, conciseTextbook(outline.textbookName), outline.unitName].filter(Boolean).join(" · ")
+  const points = studentPoints(page, 2)
+  if (hasImage(page)) {
+    const elements: PPTElement[] = [imageElement(page, { left: 0, top: 0, width: SLIDE_WIDTH, height: SLIDE_HEIGHT })]
+    elements.push(shapeElement({ left: 0, top: 0, width: 910, height: 900, fill: palette.dark, opacity: 0.78, radius: 0 }))
+    elements.push(textElement({ left: 100, top: 150, width: 700, height: 150, content: title, fontSize: fitFont(title, 700, 150, 66, 48), color: "#FFFFFF", bold: true }))
+    if (subtitle) elements.push(textElement({ left: 104, top: 330, width: 680, height: 48, content: subtitle, fontSize: 24, color: "#EAF2F7" }))
+    elements.push(shapeElement({ left: 104, top: 430, width: 84, height: 10, fill: palette.accent, radius: 4 }))
+    points.forEach((point, index) => elements.push(textElement({ left: 104, top: 500 + index * 64, width: 680, height: 44, content: point, fontSize: fitFont(point, 680, 44, 28, 22), color: "#FFFFFF", bold: index === 0 })))
+    return elements
+  }
+
   const elements: PPTElement[] = []
   const elements: PPTElement[] = []
-  const subtitle = [outline.gradeName, outline.textbookName, outline.unitName].filter(Boolean).join(" · ")
-  elements.push(titleMark(92, 92, palette.primary))
-  elements.push(textElement({ left: 92, top: 138, width: 780, height: 100, content: pageTitle(page, outline), fontSize: 54, color: palette.primary, bold: true }))
-  if (subtitle) elements.push(textElement({ left: 96, top: 266, width: 780, height: 34, content: subtitle, fontSize: 22, color: palette.text }))
-  elements.push(...heroVisual(page, palette, { left: 940, top: 122, width: 470, height: 420 }))
-  elements.push(...compactBullets(studentPoints(page, 2), palette, 96, 390, 660, "♪"))
-  addTeacherAction(elements, page, palette, 96, 715, 900)
+  elements.push(shapeElement({ left: 0, top: 0, width: 560, height: 900, fill: palette.primary, radius: 0 }))
+  elements.push(shapeElement({ left: 560, top: 0, width: 1040, height: 900, fill: palette.background, radius: 0 }))
+  elements.push(textElement({ left: 92, top: 112, width: 380, height: 70, content: "音乐课堂", fontSize: 30, color: "#FFFFFF", bold: true }))
+  elements.push(textElement({ left: 650, top: 170, width: 780, height: 170, content: title, fontSize: fitFont(title, 780, 170, 70, 50), color: palette.primary, bold: true }))
+  if (subtitle) elements.push(textElement({ left: 656, top: 370, width: 740, height: 44, content: subtitle, fontSize: 24, color: palette.muted }))
+  elements.push(...melodyStaff(120, 350, 360, palette))
+  elements.push(shapeElement({ left: 654, top: 486, width: 120, height: 10, fill: palette.secondary, radius: 4 }))
+  points.forEach((point, index) => elements.push(textElement({ left: 656, top: 548 + index * 66, width: 720, height: 46, content: point, fontSize: fitFont(point, 720, 46, 30, 23), color: palette.text, bold: index === 0 })))
   return elements
   return elements
 }
 }
 
 
-function splitTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
-  const elements: PPTElement[] = []
-  addSlideTitle(elements, page, outline, palette)
-  elements.push(...visualBlock(page, palette, { left: 925, top: 178, width: 500, height: 430 }))
-  elements.push(...largeStudentPanel(page, palette, 96, 188, 700, 440))
-  addTeacherAction(elements, page, palette, 96, 692, 1190)
+function storyTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, index: number): PPTElement[] {
+  const elements = slideHeader(page, outline, palette, "进入情境")
+  const imageLeft = index % 2 === 0
+  if (hasImage(page)) {
+    const imageRect = imageLeft ? { left: 86, top: 190, width: 860, height: 560, radius: 8 } : { left: 654, top: 190, width: 860, height: 560, radius: 8 }
+    elements.push(imageElement(page, imageRect))
+    const panelLeft = imageLeft ? 1010 : 86
+    elements.push(shapeElement({ left: panelLeft, top: 230, width: 500, height: 470, fill: palette.surface, radius: 8, shadow: softShadow() }))
+    elements.push(...studentPanel(page, palette, panelLeft + 42, 278, 416))
+  } else {
+    elements.push(shapeElement({ left: 86, top: 210, width: 1428, height: 480, fill: palette.surface, radius: 8, shadow: softShadow() }))
+    elements.push(textElement({ left: 156, top: 270, width: 1280, height: 90, content: studentHeadline(page) || page.title, fontSize: fitFont(studentHeadline(page) || page.title, 1280, 90, 42, 30), color: palette.primary, bold: true }))
+    elements.push(...storySequence(studentPoints(page, 4), palette, 156, 420, 1280))
+  }
+  addClassroomCue(elements, page, palette)
   return elements
   return elements
 }
 }
 
 
-function contentTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
-  const elements: PPTElement[] = []
-  addSlideTitle(elements, page, outline, palette)
-  elements.push(shapeElement({ left: 96, top: 184, width: 720, height: 460, fill: palette.panel, radius: 24, shadow: softShadow() }))
-  elements.push(textElement({ left: 136, top: 226, width: 640, height: 54, content: studentHeadline(page) || page.title, fontSize: 34, color: palette.primary, bold: true }))
-  studentPoints(page, 4).forEach((point, index) => {
-    const top = 318 + index * 72
-    elements.push(shapeElement({ left: 138, top, width: 24, height: 24, fill: index % 2 ? palette.secondary : palette.primary, radius: 6 }))
-    elements.push(textElement({ left: 186, top: top - 4, width: 560, height: 42, content: point, fontSize: 28, color: palette.text }))
+function goalTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
+  const elements = slideHeader(page, outline, palette, "本课目标")
+  const points = studentPoints(page, 3)
+  points.forEach((point, index) => {
+    const left = 92 + index * 500
+    const colors = [palette.primary, palette.secondary, palette.accent]
+    elements.push(shapeElement({ left, top: 240, width: 430, height: 380, fill: palette.surface, radius: 8, shadow: softShadow() }))
+    elements.push(shapeElement({ left, top: 240, width: 430, height: 18, fill: colors[index], radius: 0 }))
+    elements.push(textElement({ left: left + 34, top: 300, width: 96, height: 74, content: `0${index + 1}`, fontSize: 50, color: colors[index], bold: true }))
+    elements.push(textElement({ left: left + 34, top: 410, width: 350, height: 120, content: point, fontSize: fitFont(point, 350, 120, 32, 25), color: palette.text, bold: true, lineHeight: 1.35 }))
   })
   })
-  elements.push(...visualBlock(page, palette, { left: 900, top: 204, width: 510, height: 410 }))
-  addTeacherAction(elements, page, palette, 110, 700, 1180)
+  addClassroomCue(elements, page, palette)
   return elements
   return elements
 }
 }
 
 
 function listenTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
 function listenTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
-  const elements: PPTElement[] = []
-  addSlideTitle(elements, page, outline, palette)
-  const task = activityTask(page) || studentHeadline(page) || "听一听:音乐中有哪些声音?"
-  elements.push(shapeElement({ left: 96, top: 184, width: 760, height: 150, fill: palette.primary, radius: 26, shadow: softShadow() }))
-  elements.push(textElement({ left: 142, top: 226, width: 660, height: 60, content: task, fontSize: 34, color: "#FFFFFF", bold: true, lineHeight: 1.25 }))
-  elements.push(...soundWave(136, 390, palette, 610, 115))
-  elements.push(...compactBullets(studentPoints(page, 3), palette, 104, 552, 690, "听"))
-  elements.push(...visualBlock(page, palette, { left: 925, top: 190, width: 490, height: 430 }))
-  addTeacherAction(elements, page, palette, 110, 724, 1180)
+  const elements = slideHeader(page, outline, palette, "听赏任务")
+  const task = activityTask(page) || studentHeadline(page) || "安静聆听,找一找音乐中的声音"
+  if (hasImage(page)) {
+    elements.push(imageElement(page, { left: 84, top: 185, width: 1432, height: 520, radius: 8 }))
+    elements.push(shapeElement({ left: 84, top: 510, width: 1432, height: 195, fill: palette.dark, opacity: 0.8, radius: 0 }))
+    elements.push(textElement({ left: 130, top: 550, width: 1340, height: 84, content: task, fontSize: fitFont(task, 1340, 84, 42, 30), color: "#FFFFFF", bold: true }))
+  } else {
+    elements.push(shapeElement({ left: 92, top: 210, width: 1416, height: 220, fill: palette.dark, radius: 8 }))
+    elements.push(textElement({ left: 150, top: 270, width: 1300, height: 92, content: task, fontSize: fitFont(task, 1300, 92, 44, 30), color: "#FFFFFF", bold: true }))
+    elements.push(...soundWave(250, 520, palette, 1080, 150))
+  }
+  addClassroomCue(elements, page, palette)
+  return elements
+}
+
+function lyricTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
+  const elements = slideHeader(page, outline, palette, "学唱与歌词")
+  const points = studentPoints(page, 4)
+  elements.push(shapeElement({ left: 90, top: 205, width: 1420, height: 470, fill: palette.surface, radius: 8, shadow: softShadow() }))
+  elements.push(shapeElement({ left: 90, top: 205, width: 26, height: 470, fill: palette.secondary, radius: 0 }))
+  points.forEach((point, index) => {
+    const top = 265 + index * 92
+    elements.push(textElement({ left: 166, top, width: 1250, height: 58, content: point, fontSize: fitFont(point, 1250, 58, 36, 27), color: index % 2 === 0 ? palette.text : palette.primary, bold: index % 2 === 1 }))
+  })
+  addClassroomCue(elements, page, palette)
   return elements
   return elements
 }
 }
 
 
 function rhythmTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
 function rhythmTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
-  const elements: PPTElement[] = []
-  addSlideTitle(elements, page, outline, palette)
+  const elements = slideHeader(page, outline, palette, "节奏练习")
   const task = activityTask(page) || studentHeadline(page) || "拍一拍,感受节奏"
   const task = activityTask(page) || studentHeadline(page) || "拍一拍,感受节奏"
-  elements.push(shapeElement({ left: 105, top: 185, width: 570, height: 96, fill: palette.primary, radius: 22, shadow: softShadow() }))
-  elements.push(textElement({ left: 144, top: 216, width: 500, height: 36, content: task, fontSize: 30, color: "#FFFFFF", bold: true }))
+  elements.push(textElement({ left: 92, top: 190, width: 1410, height: 72, content: task, fontSize: fitFont(task, 1410, 72, 40, 30), color: palette.text, bold: true }))
   const beats = activitySteps(page, 4)
   const beats = activitySteps(page, 4)
   beats.forEach((beat, index) => {
   beats.forEach((beat, index) => {
-    const left = 108 + index * 205
-    elements.push(shapeElement({ left, top: 365, width: 160, height: 160, fill: index % 2 ? palette.panelAlt : palette.panel, radius: 24, shadow: softShadow() }))
-    elements.push(textElement({ left: left + 44, top: 400, width: 74, height: 48, content: `${index + 1}`, fontSize: 38, color: palette.primary, bold: true }))
-    elements.push(textElement({ left: left + 22, top: 468, width: 116, height: 40, content: beat, fontSize: 20, color: palette.text, lineHeight: 1.25 }))
+    const left = 92 + index * 370
+    elements.push(shapeElement({ left, top: 330, width: 320, height: 260, fill: index % 2 === 0 ? palette.soft : palette.surface, radius: 8, shadow: lightShadow() }))
+    elements.push(textElement({ left: left + 28, top: 370, width: 70, height: 56, content: `${index + 1}`, fontSize: 42, color: index % 2 === 0 ? palette.primary : palette.secondary, bold: true }))
+    elements.push(textElement({ left: left + 28, top: 460, width: 260, height: 90, content: beat, fontSize: fitFont(beat, 260, 90, 29, 23), color: palette.text, bold: true, lineHeight: 1.3 }))
   })
   })
-  elements.push(...visualBlock(page, palette, { left: 980, top: 230, width: 360, height: 360 }))
-  addTeacherAction(elements, page, palette, 108, 704, 1190)
+  addClassroomCue(elements, page, palette)
   return elements
   return elements
 }
 }
 
 
-function cardsTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, fallbackHeadline: string): PPTElement[] {
-  const elements: PPTElement[] = []
-  addSlideTitle(elements, page, outline, palette)
+function knowledgeTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, index: number): PPTElement[] {
+  const elements = slideHeader(page, outline, palette, "音乐知识")
   const points = studentPoints(page, 4)
   const points = studentPoints(page, 4)
-  points.forEach((point, index) => {
-    const left = 108 + (index % 2) * 520
-    const top = 210 + Math.floor(index / 2) * 188
-    elements.push(shapeElement({ left, top, width: 460, height: 136, fill: index % 2 ? palette.panelAlt : palette.panel, radius: 22, shadow: softShadow() }))
-    elements.push(shapeElement({ left: left + 28, top: top + 34, width: 42, height: 42, fill: index % 2 ? palette.secondary : palette.primary, radius: 12 }))
-    elements.push(textElement({ left: left + 42, top: top + 42, width: 18, height: 22, content: `${index + 1}`, fontSize: 20, color: "#FFFFFF", bold: true }))
-    elements.push(textElement({ left: left + 92, top: top + 34, width: 320, height: 58, content: point, fontSize: 25, color: palette.text, bold: true, lineHeight: 1.3 }))
+  if (hasImage(page)) {
+    const imageRight = index % 2 === 0
+    elements.push(imageElement(page, imageRight ? { left: 900, top: 190, width: 610, height: 500, radius: 8 } : { left: 90, top: 190, width: 610, height: 500, radius: 8 }))
+    elements.push(...knowledgeList(points, palette, imageRight ? 90 : 770, 220, 720))
+  } else {
+    elements.push(...knowledgeList(points, palette, 90, 220, 1420))
+  }
+  addClassroomCue(elements, page, palette)
+  return elements
+}
+
+function interactionTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
+  const elements = slideHeader(page, outline, palette, "互动挑战")
+  const question = page.interaction?.question || activityTask(page) || studentHeadline(page) || "你发现了什么?"
+  elements.push(shapeElement({ left: 90, top: 200, width: 1420, height: 190, fill: palette.primary, radius: 8 }))
+  elements.push(textElement({ left: 150, top: 245, width: 1300, height: 100, content: question, fontSize: fitFont(question, 1300, 100, 44, 30), color: "#FFFFFF", bold: true }))
+  const steps = activitySteps(page, 3)
+  steps.forEach((step, index) => {
+    const left = 90 + index * 490
+    elements.push(textElement({ left, top: 468, width: 88, height: 54, content: `0${index + 1}`, fontSize: 40, color: [palette.primary, palette.secondary, palette.accent][index], bold: true }))
+    elements.push(textElement({ left: left + 96, top: 466, width: 340, height: 100, content: step, fontSize: fitFont(step, 340, 100, 29, 23), color: palette.text, bold: true, lineHeight: 1.3 }))
+    if (index < steps.length - 1) elements.push(lineElement(left + 452, 492, left + 478, 492, palette.muted))
   })
   })
-  elements.push(...visualBlock(page, palette, { left: 1145, top: 248, width: 265, height: 265 }))
-  addTeacherAction(elements, page, palette, 108, 706, 1180, activityTask(page) || fallbackHeadline)
+  addClassroomCue(elements, page, palette)
   return elements
   return elements
 }
 }
 
 
 function activityTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
 function activityTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
-  const elements: PPTElement[] = []
-  addSlideTitle(elements, page, outline, palette)
-  const task = activityTask(page) || page.interaction?.question || studentHeadline(page) || "课堂互动"
-  elements.push(shapeElement({ left: 108, top: 185, width: 900, height: 165, fill: palette.primary, radius: 28, shadow: softShadow() }))
-  elements.push(textElement({ left: 154, top: 235, width: 800, height: 58, content: task, fontSize: 36, color: "#FFFFFF", bold: true, lineHeight: 1.25 }))
-  activitySteps(page, 3).forEach((step, index) => {
-    const left = 118 + index * 392
-    elements.push(shapeElement({ left, top: 455, width: 335, height: 142, fill: palette.panel, radius: 22, shadow: softShadow() }))
-    elements.push(textElement({ left: left + 26, top: 484, width: 88, height: 26, content: `步骤${index + 1}`, fontSize: 21, color: palette.primary, bold: true }))
-    elements.push(textElement({ left: left + 26, top: 532, width: 280, height: 42, content: step, fontSize: 24, color: palette.text, lineHeight: 1.28 }))
-  })
-  elements.push(...visualBlock(page, palette, { left: 1095, top: 195, width: 300, height: 300 }))
-  addTeacherAction(elements, page, palette, 116, 710, 1170, page.interaction?.answer || "")
+  const elements = slideHeader(page, outline, palette, "课堂活动")
+  const task = activityTask(page) || studentHeadline(page) || "一起完成课堂挑战"
+  elements.push(textElement({ left: 92, top: 190, width: 1400, height: 90, content: task, fontSize: fitFont(task, 1400, 90, 42, 30), color: palette.primary, bold: true }))
+  elements.push(...storySequence(activitySteps(page, 4), palette, 92, 360, 1416))
+  addClassroomCue(elements, page, palette)
   return elements
   return elements
 }
 }
 
 
 function summaryTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
 function summaryTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
-  const elements: PPTElement[] = []
-  addSlideTitle(elements, page, outline, palette)
-  elements.push(shapeElement({ left: 112, top: 188, width: 900, height: 470, fill: palette.panel, radius: 26, shadow: softShadow() }))
+  const elements = slideHeader(page, outline, palette, "课堂小结")
   studentPoints(page, 4).forEach((point, index) => {
   studentPoints(page, 4).forEach((point, index) => {
-    const top = 240 + index * 90
-    elements.push(shapeElement({ left: 160, top, width: 44, height: 44, fill: palette.secondary, radius: 14 }))
-    elements.push(textElement({ left: 172, top: top + 8, width: 20, height: 20, content: "✓", fontSize: 23, color: "#FFFFFF", bold: true }))
-    elements.push(textElement({ left: 236, top: top + 4, width: 690, height: 38, content: point, fontSize: 28, color: palette.text, bold: true }))
+    const top = 220 + index * 112
+    const color = index % 2 === 0 ? palette.secondary : palette.primary
+    elements.push(shapeElement({ left: 100, top, width: 62, height: 62, fill: color, radius: 8 }))
+    elements.push(textElement({ left: 118, top: top + 12, width: 30, height: 30, content: "✓", fontSize: 28, color: "#FFFFFF", bold: true }))
+    elements.push(textElement({ left: 210, top: top + 4, width: 1240, height: 64, content: point, fontSize: fitFont(point, 1240, 64, 34, 26), color: palette.text, bold: true }))
   })
   })
-  elements.push(...visualBlock(page, palette, { left: 1120, top: 250, width: 280, height: 280 }))
   return elements
   return elements
 }
 }
 
 
 function homeworkTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
 function homeworkTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
-  const elements: PPTElement[] = []
-  addSlideTitle(elements, page, outline, palette)
-  elements.push(shapeElement({ left: 112, top: 196, width: 610, height: 460, fill: palette.panel, radius: 28, shadow: softShadow() }))
-  elements.push(textElement({ left: 154, top: 244, width: 460, height: 44, content: studentHeadline(page) || "课后练习", fontSize: 34, color: palette.primary, bold: true }))
-  elements.push(...compactBullets(activitySteps(page, 4), palette, 154, 340, 480, "做"))
-  elements.push(...visualBlock(page, palette, { left: 850, top: 200, width: 500, height: 420 }))
+  const elements = slideHeader(page, outline, palette, "课后延伸")
+  elements.push(shapeElement({ left: 92, top: 210, width: 1416, height: 440, fill: palette.surface, radius: 8, shadow: softShadow() }))
+  activitySteps(page, 4).forEach((step, index) => {
+    const top = 270 + index * 86
+    elements.push(textElement({ left: 150, top, width: 70, height: 48, content: `${index + 1}.`, fontSize: 34, color: palette.accent, bold: true }))
+    elements.push(textElement({ left: 230, top, width: 1180, height: 58, content: step, fontSize: fitFont(step, 1180, 58, 31, 24), color: palette.text, bold: true }))
+  })
   return elements
   return elements
 }
 }
 
 
-function largeStudentPanel(page: AiCoursewarePage, palette: Palette, left: number, top: number, width: number, height: number): PPTElement[] {
+function contentTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, index: number): PPTElement[] {
+  return knowledgeTemplate(page, outline, palette, index)
+}
+
+function slideHeader(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, eyebrow: string): PPTElement[] {
+  const title = displayPageTitle(page, outline)
+  return [
+    textElement({ left: 92, top: 64, width: 300, height: 34, content: eyebrow, fontSize: 20, color: palette.secondary, bold: true }),
+    textElement({ left: 92, top: 102, width: 1320, height: 66, content: title, fontSize: fitFont(title, 1320, 66, 42, 32), color: palette.text, bold: true }),
+    shapeElement({ left: 1450, top: 72, width: 58, height: 8, fill: palette.accent, radius: 4 }),
+  ]
+}
+
+function studentPanel(page: AiCoursewarePage, palette: Palette, left: number, top: number, width: number): PPTElement[] {
   const elements: PPTElement[] = []
   const elements: PPTElement[] = []
   const headline = studentHeadline(page)
   const headline = studentHeadline(page)
-  elements.push(shapeElement({ left, top, width, height, fill: palette.panel, radius: 26, shadow: softShadow() }))
-  if (headline) {
-    elements.push(textElement({ left: left + 42, top: top + 40, width: width - 84, height: 44, content: headline, fontSize: 34, color: palette.primary, bold: true }))
-  }
-  studentPoints(page, 4).forEach((point, index) => {
-    const itemTop = top + (headline ? 130 : 68) + index * 70
-    elements.push(shapeElement({ left: left + 42, top: itemTop + 8, width: 26, height: 26, fill: index % 2 ? palette.secondary : palette.primary, radius: 7 }))
-    elements.push(textElement({ left: left + 92, top: itemTop, width: width - 140, height: 38, content: point, fontSize: 27, color: palette.text, lineHeight: 1.25 }))
+  if (headline) elements.push(textElement({ left, top, width, height: 90, content: headline, fontSize: fitFont(headline, width, 90, 36, 27), color: palette.primary, bold: true }))
+  studentPoints(page, 3).forEach((point, index) => {
+    const itemTop = top + (headline ? 120 : 0) + index * 74
+    elements.push(shapeElement({ left, top: itemTop + 8, width: 18, height: 18, fill: index % 2 ? palette.secondary : palette.accent, radius: 4 }))
+    elements.push(textElement({ left: left + 38, top: itemTop, width: width - 38, height: 54, content: point, fontSize: fitFont(point, width - 38, 54, 27, 21), color: palette.text, lineHeight: 1.3 }))
   })
   })
   return elements
   return elements
 }
 }
 
 
-function compactBullets(items: string[], palette: Palette, left: number, top: number, width: number, mark: string): PPTElement[] {
+function storySequence(items: string[], palette: Palette, left: number, top: number, width: number): PPTElement[] {
   const elements: PPTElement[] = []
   const elements: PPTElement[] = []
+  const count = Math.max(1, Math.min(items.length, 4))
+  const gap = 28
+  const cardWidth = (width - gap * (count - 1)) / count
   items.slice(0, 4).forEach((item, index) => {
   items.slice(0, 4).forEach((item, index) => {
-    const itemTop = top + index * 64
-    elements.push(shapeElement({ left, top: itemTop, width, height: 48, fill: "#FFFFFF", opacity: 0.94, radius: 14, shadow: lightShadow() }))
-    elements.push(shapeElement({ left: left + 20, top: itemTop + 13, width: 22, height: 22, fill: index % 2 ? palette.secondary : palette.primary, radius: 5 }))
-    elements.push(textElement({ left: left + 62, top: itemTop + 8, width: width - 88, height: 30, content: item, fontSize: 24, color: palette.text }))
+    const cardLeft = left + index * (cardWidth + gap)
+    elements.push(shapeElement({ left: cardLeft, top, width: cardWidth, height: 190, fill: index % 2 === 0 ? palette.soft : palette.surface, radius: 8, shadow: lightShadow() }))
+    elements.push(textElement({ left: cardLeft + 24, top: top + 24, width: 64, height: 38, content: `0${index + 1}`, fontSize: 26, color: index % 2 === 0 ? palette.primary : palette.secondary, bold: true }))
+    elements.push(textElement({ left: cardLeft + 24, top: top + 82, width: cardWidth - 48, height: 78, content: item, fontSize: fitFont(item, cardWidth - 48, 78, 27, 21), color: palette.text, bold: true, lineHeight: 1.3 }))
   })
   })
   return elements
   return elements
 }
 }
 
 
-function visualBlock(page: AiCoursewarePage, palette: Palette, rect: VisualRect): PPTElement[] {
-  if (page.image?.url) {
-    return [
-      shapeElement({ left: rect.left - 14, top: rect.top + 16, width: rect.width, height: rect.height, fill: palette.warm, opacity: 0.45, radius: 26 }),
-      imageElement({ left: rect.left, top: rect.top, width: rect.width, height: rect.height, src: page.image.url, radius: 28 })
-    ]
-  }
-  return fallbackVisual(page, palette, rect)
-}
-
-function heroVisual(page: AiCoursewarePage, palette: Palette, rect: VisualRect): PPTElement[] {
-  return visualBlock(page, palette, rect)
+function knowledgeList(items: string[], palette: Palette, left: number, top: number, width: number): PPTElement[] {
+  const elements: PPTElement[] = []
+  items.forEach((item, index) => {
+    const itemTop = top + index * 108
+    elements.push(shapeElement({ left, top: itemTop, width, height: 82, fill: index % 2 === 0 ? palette.surface : palette.soft, radius: 8, shadow: lightShadow() }))
+    elements.push(shapeElement({ left, top: itemTop, width: 14, height: 82, fill: index % 2 === 0 ? palette.primary : palette.secondary, radius: 0 }))
+    elements.push(textElement({ left: left + 42, top: itemTop + 18, width: width - 74, height: 48, content: item, fontSize: fitFont(item, width - 74, 48, 29, 22), color: palette.text, bold: true }))
+  })
+  return elements
 }
 }
 
 
-function fallbackVisual(page: AiCoursewarePage, palette: Palette, rect: VisualRect): PPTElement[] {
+function melodyStaff(left: number, top: number, width: number, palette: Palette): PPTElement[] {
   const elements: PPTElement[] = []
   const elements: PPTElement[] = []
-  const visual = visualConfig(page, palette)
-  const label = visualMain(page) || visual.label
-  const tags = visualElements(page).slice(0, 3)
-  elements.push(shapeElement({ left: rect.left, top: rect.top, width: rect.width, height: rect.height, fill: visual.light, radius: 28, shadow: softShadow() }))
-  elements.push(shapeElement({ left: rect.left + rect.width * 0.18, top: rect.top + rect.height * 0.13, width: rect.width * 0.64, height: rect.height * 0.36, fill: "#FFFFFF", opacity: 0.92, radius: 30 }))
-  elements.push(textElement({ left: rect.left + rect.width * 0.42, top: rect.top + rect.height * 0.2, width: rect.width * 0.18, height: 58, content: visual.icon, fontSize: 54, color: visual.color, bold: true }))
-  elements.push(...soundWave(rect.left + rect.width * 0.24, rect.top + rect.height * 0.58, palette, rect.width * 0.5, 70))
-  elements.push(textElement({ left: rect.left + rect.width * 0.14, top: rect.top + rect.height * 0.43, width: rect.width * 0.72, height: 34, content: trimText(label, 14), fontSize: 21, color: visual.color, bold: true }))
-  tags.forEach((tag, index) => {
-    const tagTop = rect.top + rect.height * 0.76 + index * 34
-    elements.push(shapeElement({ left: rect.left + rect.width * 0.22, top: tagTop, width: rect.width * 0.56, height: 26, fill: "#FFFFFF", opacity: 0.86, radius: 13 }))
-    elements.push(textElement({ left: rect.left + rect.width * 0.26, top: tagTop + 5, width: rect.width * 0.48, height: 16, content: trimText(tag, 12), fontSize: 14, color: palette.muted }))
+  for (let i = 0; i < 5; i++) elements.push(lineElement(left, top + i * 34, left + width, top + i * 34, "rgba(255,255,255,0.55)"))
+  ;[0.12, 0.36, 0.62, 0.84].forEach((position, index) => {
+    elements.push(shapeElement({ left: left + width * position, top: top + 48 + (index % 2) * 30, width: 34, height: 28, fill: index % 2 ? palette.accent : palette.warm, radius: 8 }))
+    elements.push(lineElement(left + width * position + 30, top + 10 + (index % 2) * 30, left + width * position + 30, top + 60 + (index % 2) * 30, index % 2 ? palette.accent : palette.warm, 5))
   })
   })
   return elements
   return elements
 }
 }
 
 
-function addSlideTitle(elements: PPTElement[], page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette) {
-  elements.push(titleMark(92, 74, palette.primary))
-  elements.push(textElement({ left: 92, top: 102, width: 1120, height: 58, content: page.title || outline.title || outline.chapterName, fontSize: 40, color: palette.primary, bold: true }))
+function soundWave(left: number, top: number, palette: Palette, width: number, maxHeight: number): PPTElement[] {
+  const heights = [0.28, 0.55, 0.82, 0.45, 1, 0.7, 0.38, 0.64, 0.32]
+  return heights.map((scale, index) => shapeElement({
+    left: left + index * (width / heights.length), top: top + (maxHeight - maxHeight * scale) / 2,
+    width: Math.max(18, width / 24), height: maxHeight * scale,
+    fill: index % 3 === 0 ? palette.accent : index % 2 === 0 ? palette.secondary : palette.primary, radius: 8,
+  }))
 }
 }
 
 
-function addTeacherAction(elements: PPTElement[], page: AiCoursewarePage, palette: Palette, left: number, top: number, width: number, fallback = "") {
-  const text = fallback || page.interaction?.question || page.classroomAction?.studentTask || activityTask(page)
-  if (!text) return
-  elements.push(shapeElement({ left, top, width, height: 54, fill: palette.panelAlt, opacity: 0.92, radius: 14 }))
-  elements.push(textElement({ left: left + 24, top: top + 15, width: width - 48, height: 24, content: trimText(text, 48), fontSize: 20, color: palette.text }))
+function addClassroomCue(elements: PPTElement[], page: AiCoursewarePage, palette: Palette) {
+  const cue = page.interaction?.question || activityTask(page)
+  if (!cue) return
+  elements.push(shapeElement({ left: 92, top: 754, width: 1416, height: 62, fill: palette.soft, radius: 8 }))
+  elements.push(textElement({ left: 120, top: 772, width: 1360, height: 30, content: cue, fontSize: fitFont(cue, 1360, 30, 22, 18), color: palette.text, bold: true }))
 }
 }
 
 
-function titleMark(left: number, top: number, color: string): PPTShapeElement {
-  return shapeElement({ left, top, width: 78, height: 8, fill: color, radius: 5 })
+function normalizeOutline(outline: AiCoursewareOutline): AiCoursewareOutline {
+  const chapter = cleanText(outline.chapterName || outline.title || "音乐课件")
+  return {
+    ...outline,
+    title: GENERIC_PAGE_TITLES.has(cleanText(outline.title).toLowerCase()) ? chapter : cleanText(outline.title || chapter),
+    pages: (outline.pages || []).map((page, index) => ({
+      ...page,
+      pageNo: index + 1,
+      title: index === 0 && GENERIC_PAGE_TITLES.has(cleanText(page.title).toLowerCase()) ? chapter : cleanText(page.title || chapter),
+      bullets: (page.bullets || []).map(cleanText).filter(Boolean),
+      imageKeywords: (page.imageKeywords || []).map(cleanText).filter(Boolean),
+    })),
+  }
 }
 }
 
 
-function soundWave(left: number, top: number, palette: Palette, width = 330, maxHeight = 120): PPTElement[] {
-  const elements: PPTElement[] = []
-  const heights = [42, 76, 108, 70, 120, 88, 52].map(height => (height / 120) * maxHeight)
-  heights.forEach((height, index) => {
-    const barWidth = Math.max(14, width / 22)
-    const gap = Math.max(18, width / 9)
-    elements.push(shapeElement({ left: left + index * gap, top: top + (maxHeight - height) / 2, width: barWidth, height, fill: index % 2 === 0 ? palette.primary : palette.accent, radius: 8 }))
-  })
-  return elements
+function pageRole(page: AiCoursewarePage, index: number): PageRole {
+  if (index === 0) return "cover"
+  const kind = `${page.type || ""} ${page.visualPlan?.layout || ""} ${page.visual?.layout || ""}`.toLowerCase()
+  if (kind.includes("goal")) return "goal"
+  if (kind.includes("listen")) return "listen"
+  if (kind.includes("lyric") || kind.includes("sing")) return "lyric"
+  if (kind.includes("rhythm") || kind.includes("skill")) return "rhythm"
+  if (kind.includes("knowledge") || kind.includes("extension")) return "knowledge"
+  if (kind.includes("interaction")) return "interaction"
+  if (kind.includes("activity")) return "activity"
+  if (kind.includes("summary")) return "summary"
+  if (kind.includes("homework")) return "homework"
+  if (kind.includes("intro")) return "intro"
+  if (kind.includes("background") || kind.includes("story")) return "story"
+  return "content"
 }
 }
 
 
-function baseElements(palette: Palette, options: AiPptOptions): PPTElement[] {
-  const elements: PPTElement[] = []
-  if (options.backgroundImage) {
-    elements.push(shapeElement({ left: 0, top: 0, width: SLIDE_WIDTH, height: SLIDE_HEIGHT, fill: "rgba(255,255,255,0.82)", opacity: 0.96 }))
-  }
-  elements.push(shapeElement({ left: 0, top: 0, width: 10, height: SLIDE_HEIGHT, fill: palette.primary, radius: 0 }))
-  elements.push(shapeElement({ left: 1260, top: 70, width: 210, height: 210, fill: palette.panelAlt, opacity: 0.28, radius: 100 }))
-  elements.push(shapeElement({ left: 1380, top: 690, width: 88, height: 88, fill: palette.warm, opacity: 0.3, radius: 42 }))
-  return elements
+function displayPageTitle(page: AiCoursewarePage, outline: AiCoursewareOutline) {
+  const title = cleanText(page.title)
+  return !title || GENERIC_PAGE_TITLES.has(title.toLowerCase()) ? chapterTitle(outline, page) : title
+}
+
+function chapterTitle(outline: AiCoursewareOutline, page: AiCoursewarePage) {
+  return cleanText(outline.chapterName || outline.title || page.title || "音乐课件")
+}
+
+function conciseTextbook(value?: string) {
+  return cleanText(value).replace(/^.*?·/, "")
+}
+
+function hasImage(page: AiCoursewarePage) {
+  return Boolean(page.image?.url)
 }
 }
 
 
 function studentHeadline(page: AiCoursewarePage) {
 function studentHeadline(page: AiCoursewarePage) {
   return cleanText(page.studentContent?.headline || page.subtitle || "")
   return cleanText(page.studentContent?.headline || page.subtitle || "")
 }
 }
 
 
-function studentPoints(page: AiCoursewarePage, limit: number): string[] {
+function studentPoints(page: AiCoursewarePage, limit: number) {
   const structured = (page.studentContent?.points || []).map(cleanText).filter(Boolean)
   const structured = (page.studentContent?.points || []).map(cleanText).filter(Boolean)
   const legacy = (page.bullets || []).map(cleanText).filter(Boolean)
   const legacy = (page.bullets || []).map(cleanText).filter(Boolean)
-  const source = structured.length ? structured : legacy
-  return (source.length ? source : [studentHeadline(page) || page.title || "课堂内容"]).slice(0, limit).map(item => trimText(item, 34))
+  const points = structured.length ? structured : legacy
+  return (points.length ? points : [studentHeadline(page) || cleanText(page.title) || "课堂内容"]).slice(0, limit)
 }
 }
 
 
 function activityTask(page: AiCoursewarePage) {
 function activityTask(page: AiCoursewarePage) {
   return cleanText(page.classroomAction?.studentTask || page.activity?.task || "")
   return cleanText(page.classroomAction?.studentTask || page.activity?.task || "")
 }
 }
 
 
-function activitySteps(page: AiCoursewarePage, limit: number): string[] {
-  const steps = [page.classroomAction?.studentTask, ...(page.activity?.steps || [])].map(cleanText).filter(Boolean)
-  return (steps.length ? steps : studentPoints(page, limit)).slice(0, limit).map(item => trimText(item, 32))
-}
-
-function pageTitle(page: AiCoursewarePage, outline: AiCoursewareOutline) {
-  return cleanText(page.title || outline.title || outline.chapterName || "音乐课件")
-}
-
-function pageKind(page: AiCoursewarePage) {
-  return `${page.type || ""} ${page.visualPlan?.layout || ""} ${page.visual?.layout || ""}`.toLowerCase()
+function activitySteps(page: AiCoursewarePage, limit: number) {
+  const steps = (page.activity?.steps || []).map(cleanText).filter(Boolean)
+  const task = activityTask(page)
+  const values = steps.length ? steps : task ? [task, ...studentPoints(page, limit)] : studentPoints(page, limit)
+  return values.filter((value, index, all) => all.indexOf(value) === index).slice(0, limit)
 }
 }
 
 
 function slideRemark(page: AiCoursewarePage) {
 function slideRemark(page: AiCoursewarePage) {
   return [page.speakerNotes, page.teacherTips, page.classroomAction?.teacherInstruction, page.teachingPurpose]
   return [page.speakerNotes, page.teacherTips, page.classroomAction?.teacherInstruction, page.teachingPurpose]
-    .map(cleanText)
-    .filter(Boolean)
-    .filter((item, index, array) => array.indexOf(item) === index)
-    .join("\n")
-}
-
-function visualMain(page: AiCoursewarePage) {
-  return cleanText(page.visualPlan?.mainVisual || page.visual?.mainVisual || "")
-}
-
-function visualElements(page: AiCoursewarePage) {
-  const elements = (page.visualPlan?.elements || []).map(cleanText).filter(Boolean)
-  const keywords = (page.visualPlan?.imageKeywords || page.visual?.imageKeywords || page.imageKeywords || []).map(cleanText).filter(Boolean)
-  return elements.length ? elements : keywords
+    .map(cleanText).filter(Boolean).filter((item, index, all) => all.indexOf(item) === index).join("\n")
 }
 }
 
 
-function visualConfig(page: AiCoursewarePage, palette: Palette) {
-  const kind = pageKind(page)
-  const label = trimText(visualMain(page) || page.visual?.style || "", 12)
-  if (kind.includes("interaction") || kind.includes("activity")) return { icon: "?", label: label || "课堂互动", color: palette.accent, light: "#EEF2FF" }
-  if (kind.includes("summary")) return { icon: "✓", label: label || "课堂小结", color: "#2E7D32", light: "#EAF7EC" }
-  if (kind.includes("goal")) return { icon: "◎", label: label || "学习目标", color: "#7B1FA2", light: "#F3E8FF" }
-  if (kind.includes("listen")) return { icon: "♪", label: label || "听赏任务", color: palette.secondary, light: "#E5F6F3" }
-  if (kind.includes("rhythm") || kind.includes("skill")) return { icon: "♩", label: label || "节奏练习", color: palette.primary, light: "#FFF1E7" }
-  if (kind.includes("homework")) return { icon: "✎", label: label || "课后练习", color: "#6D4C41", light: "#F3ECE8" }
-  return { icon: "♫", label: label || "音乐课堂", color: palette.primary, light: palette.warm }
+function buildPalette(outline: AiCoursewareOutline): Palette {
+  const clean = outline.style === "clean" || /[初高]中|七年级|八年级|九年级/.test(outline.gradeName || "")
+  if (clean) return {
+    primary: "#2563EB", secondary: "#0F766E", accent: "#E9553D", warm: "#F5D06F",
+    background: "#F4F7FB", surface: "#FFFFFF", soft: "#EAF1F8", text: "#17222E", muted: "#64748B", dark: "#132A3A",
+  }
+  return {
+    primary: "#E64A78", secondary: "#168F86", accent: "#F28C28", warm: "#FFD66B",
+    background: "#FFF9F5", surface: "#FFFFFF", soft: "#EAF7F4", text: "#263238", muted: "#6B7C8F", dark: "#283B4A",
+  }
 }
 }
 
 
-function backgroundConfig(palette: Palette, options: AiPptOptions): SlideBackground {
+function backgroundConfig(_role: PageRole, palette: Palette, options: AiPptOptions): SlideBackground {
   if (options.backgroundImage) return { type: "image", image: options.backgroundImage, imageSize: "cover" }
   if (options.backgroundImage) return { type: "image", image: options.backgroundImage, imageSize: "cover" }
   return { type: "solid", color: palette.background }
   return { type: "solid", color: palette.background }
 }
 }
 
 
-function buildPalette(outline: AiCoursewareOutline): Palette {
-  if (outline.style === "clean") {
-    return {
-      primary: "#2563EB",
-      secondary: "#0F766E",
-      accent: "#7C3AED",
-      warm: "#FEF3C7",
-      background: "#F7FAFC",
-      panel: "#FFFFFF",
-      panelAlt: "#EAF2FF",
-      text: "#1F2933",
-      muted: "#64748B"
-    }
+function fitFont(content: string, width: number, height: number, preferred: number, minimum: number) {
+  const text = cleanText(content)
+  if (!text) return preferred
+  let size = preferred
+  while (size > minimum) {
+    const charsPerLine = Math.max(1, Math.floor(width / size))
+    const lines = Math.ceil(text.length / charsPerLine)
+    if (lines * size * 1.35 <= height) break
+    size -= 2
   }
   }
-  const variants: Palette[] = [
-    {
-      primary: "#FF7A3D",
-      secondary: "#2BAE9F",
-      accent: "#5B8DEF",
-      warm: "#FFE8B8",
-      background: "#FFF7EC",
-      panel: "#FFFFFF",
-      panelAlt: "#EAF6F3",
-      text: "#263238",
-      muted: "#6B7C8F"
-    },
-    {
-      primary: "#EF5DA8",
-      secondary: "#36B37E",
-      accent: "#4C6FFF",
-      warm: "#FFF0C7",
-      background: "#FFF8F8",
-      panel: "#FFFFFF",
-      panelAlt: "#ECFDF5",
-      text: "#263238",
-      muted: "#6B7C8F"
-    }
-  ]
-  return variants[Math.abs((outline.chapterName || outline.title || "").length) % variants.length]
+  return size
 }
 }
 
 
-function textElement(params: {
-  left: number
-  top: number
-  width: number
-  height: number
-  content: string
-  fontSize: number
-  color: string
-  bold?: boolean
-  lineHeight?: number
-}): PPTTextElement {
+function textElement(params: { left: number; top: number; width: number; height: number; content: string; fontSize: number; color: string; bold?: boolean; lineHeight?: number }): PPTTextElement {
   return {
   return {
-    id: nanoid(10),
-    type: "text",
-    left: params.left,
-    top: params.top,
-    width: params.width,
-    height: params.height,
-    rotate: 0,
+    id: nanoid(10), type: "text", left: params.left, top: params.top, width: params.width, height: params.height, rotate: 0,
     content: `<div style="font-family:${FONT_NAME};font-size:${params.fontSize}px;${params.bold ? "font-weight:700;" : ""}">${escapeHtml(params.content)}</div>`,
     content: `<div style="font-family:${FONT_NAME};font-size:${params.fontSize}px;${params.bold ? "font-weight:700;" : ""}">${escapeHtml(params.content)}</div>`,
-    defaultFontName: FONT_NAME,
-    defaultColor: params.color,
-    lineHeight: params.lineHeight || 1.25,
-    wordSpace: 0,
-    opacity: 1
+    defaultFontName: FONT_NAME, defaultColor: params.color, lineHeight: params.lineHeight || 1.25, wordSpace: 0, opacity: 1,
   }
   }
 }
 }
 
 
-function imageElement(params: { left: number; top: number; width: number; height: number; src: string; radius?: number }): PPTImageElement {
+function imageElement(page: AiCoursewarePage, rect: ImageRect): PPTImageElement {
+  const width = page.image?.width || 0
+  const height = page.image?.height || 0
   return {
   return {
-    id: nanoid(10),
-    type: "image",
-    left: params.left,
-    top: params.top,
-    width: params.width,
-    height: params.height,
-    rotate: 0,
-    src: params.src,
-    fixedRatio: false,
-    radius: params.radius || 18,
-    shadow: softShadow()
+    id: nanoid(10), type: "image", left: rect.left, top: rect.top, width: rect.width, height: rect.height, rotate: 0,
+    src: page.image?.url || "", fixedRatio: false, radius: rect.radius || 0, shadow: rect.radius ? softShadow() : undefined,
+    clip: width > 0 && height > 0 ? { shape: "rect", range: coverRange(width, height, rect.width, rect.height) } : undefined,
   }
   }
 }
 }
 
 
-function shapeElement(params: {
-  left: number
-  top: number
-  width: number
-  height: number
-  fill: string
-  opacity?: number
-  radius?: number
-  shadow?: PPTElementShadow
-}): PPTShapeElement {
-  return {
-    id: nanoid(10),
-    type: "shape",
-    left: params.left,
-    top: params.top,
-    width: params.width,
-    height: params.height,
-    rotate: 0,
-    viewBox: [200, 200],
-    path: roundedRectPath(params.radius ?? 18),
-    fixedRatio: false,
-    fill: params.fill,
-    opacity: params.opacity ?? 1,
-    shadow: params.shadow
+function coverRange(sourceWidth: number, sourceHeight: number, targetWidth: number, targetHeight: number): [[number, number], [number, number]] {
+  const sourceRatio = sourceWidth / sourceHeight
+  const targetRatio = targetWidth / targetHeight
+  if (sourceRatio > targetRatio) {
+    const visible = (targetRatio / sourceRatio) * 100
+    const start = (100 - visible) / 2
+    return [[start, 0], [100 - start, 100]]
   }
   }
+  const visible = (sourceRatio / targetRatio) * 100
+  const start = (100 - visible) / 2
+  return [[0, start], [100, 100 - start]]
 }
 }
 
 
-function keepInCanvas(elements: PPTElement[]) {
-  return elements.map(element => ({
-    ...element,
-    left: Math.max(0, Math.min(element.left, SLIDE_WIDTH - 1)),
-    top: Math.max(0, Math.min(element.top, SLIDE_HEIGHT - 1)),
-    width: Math.max(1, Math.min(element.width, SLIDE_WIDTH - element.left)),
-    height: Math.max(1, Math.min(element.height, SLIDE_HEIGHT - element.top))
-  })) as PPTElement[]
+function shapeElement(params: { left: number; top: number; width: number; height: number; fill: string; opacity?: number; radius?: number; shadow?: PPTElementShadow; gradient?: Gradient }): PPTShapeElement {
+  return {
+    id: nanoid(10), type: "shape", left: params.left, top: params.top, width: params.width, height: params.height, rotate: 0,
+    viewBox: [200, 200], path: roundedRectPath(params.radius || 0), fixedRatio: false, fill: params.fill,
+    opacity: params.opacity ?? 1, shadow: params.shadow, gradient: params.gradient,
+  }
 }
 }
 
 
-function softShadow(): PPTElementShadow {
-  return { h: 0, v: 8, blur: 22, color: "rgba(38, 50, 56, 0.14)" }
+function lineElement(x1: number, y1: number, x2: number, y2: number, color: string, width = 2): PPTLineElement {
+  const left = Math.min(x1, x2)
+  const top = Math.min(y1, y2)
+  const lineWidth = Math.abs(x2 - x1)
+  const lineHeight = Math.abs(y2 - y1)
+  return {
+    id: nanoid(10), type: "line", left, top, width,
+    start: [x1 === left ? 0 : lineWidth, y1 === top ? 0 : lineHeight],
+    end: [x2 === left ? 0 : lineWidth, y2 === top ? 0 : lineHeight],
+    style: "solid", color, points: ["", ""],
+  }
 }
 }
 
 
-function lightShadow(): PPTElementShadow {
-  return { h: 0, v: 6, blur: 16, color: "rgba(38, 50, 56, 0.1)" }
+function keepInCanvas(elements: PPTElement[]) {
+  return elements.map(element => {
+    const left = Math.max(0, Math.min(element.left, SLIDE_WIDTH - 1))
+    const top = Math.max(0, Math.min(element.top, SLIDE_HEIGHT - 1))
+    if (element.type === "line") return { ...element, left, top, width: Math.max(1, Math.min(element.width, 20)) }
+    return { ...element, left, top, width: Math.max(1, Math.min(element.width, SLIDE_WIDTH - left)), height: Math.max(1, Math.min(element.height, SLIDE_HEIGHT - top)) }
+  }) as PPTElement[]
+}
+
+function runPreflight(slides: Slide[]) {
+  slides.forEach((slide, index) => {
+    const invalid = slide.elements.filter(element => element.left < 0 || element.top < 0 || element.left + element.width > SLIDE_WIDTH + 1 || (element.type !== "line" && element.top + element.height > SLIDE_HEIGHT + 1))
+    const emptyText = slide.elements.filter(element => element.type === "text" && !stripHtml(element.content))
+    if (invalid.length || emptyText.length) console.warn("AI PPT preflight issue", { pageNo: index + 1, invalid: invalid.length, emptyText: emptyText.length })
+  })
 }
 }
 
 
 function roundedRectPath(radius: number) {
 function roundedRectPath(radius: number) {
@@ -497,15 +513,22 @@ function roundedRectPath(radius: number) {
   return `M ${r} 0 L ${200 - r} 0 Q 200 0 200 ${r} L 200 ${200 - r} Q 200 200 ${200 - r} 200 L ${r} 200 Q 0 200 0 ${200 - r} L 0 ${r} Q 0 0 ${r} 0 Z`
   return `M ${r} 0 L ${200 - r} 0 Q 200 0 200 ${r} L 200 ${200 - r} Q 200 200 ${200 - r} 200 L ${r} 200 Q 0 200 0 ${200 - r} L 0 ${r} Q 0 0 ${r} 0 Z`
 }
 }
 
 
-function cleanText(value?: string) {
-  return String(value || "").replace(/\s+/g, " ").trim()
+function softShadow(): PPTElementShadow {
+  return { h: 0, v: 8, blur: 22, color: "rgba(23, 34, 46, 0.14)" }
 }
 }
 
 
-function trimText(value: string, maxLength: number) {
-  const text = cleanText(value)
-  return text.length > maxLength ? `${text.slice(0, maxLength - 1)}...` : text
+function lightShadow(): PPTElementShadow {
+  return { h: 0, v: 4, blur: 14, color: "rgba(23, 34, 46, 0.1)" }
+}
+
+function cleanText(value?: string) {
+  return String(value || "").replace(/\s+/g, " ").trim()
 }
 }
 
 
 function escapeHtml(value: string) {
 function escapeHtml(value: string) {
   return cleanText(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
   return cleanText(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
 }
 }
+
+function stripHtml(value: string) {
+  return value.replace(/<[^>]+>/g, "").trim()
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов