templates.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import { copyFile, writeFile, mkdir } from 'node:fs/promises'
  2. import { resolve, join } from 'node:path'
  3. import { randomUUID } from 'node:crypto'
  4. import type { CourseTemplate } from '../src/types.js'
  5. export const COURSE_TEMPLATES: CourseTemplate[] = [
  6. {
  7. id: 'party-building-standard',
  8. name: '《坚持人民至上根本立场》党课视频',
  9. category: '党政专题',
  10. tags: ['精品党课', 'AI数字人', '1080P高清', 'PPT同步论证'],
  11. badge: '官方推荐示范',
  12. summary: '严格依据党课制作任务书规范,包含审定讲稿、全屏课件切换逻辑、数字人讲师近景与全景运镜、精准语速停顿控制。',
  13. description: `【制作目标】PPT配合讲稿,完成一支可用于网络党课学习的横版课程视频。
  14. 【形式结构】(课程包装片头—片名)—讲师讲述—PPT论证—讲师回场—章节转场—继续讲述—总结—片尾。
  15. 【运镜与节奏】全片约 30% 为讲师中近景/全景,约 70% 为全屏课件;单页 PPT 驻留 10—45 秒;讲师近景口型眼神对齐,现代演播室虚化背景。
  16. 【语速与语气】语速 210—225 汉字/分钟;句间停顿 0.3—0.5 秒,段落转折 0.6—0.9 秒,章节切换 1.2—1.8 秒;语气正式、平稳、有教学交流感。
  17. 【交付规格】1920×1080、25fps、H.264/AAC、48kHz 立体声(1080p 高清)。`,
  18. keySpecs: {
  19. resolution: '1080P (1920×1080) 25fps',
  20. aspectRatio: '16:9 横版',
  21. speechSpeed: '210—225 汉字/分钟',
  22. presenterRatio: '30% 讲师出镜 / 70% 全屏课件',
  23. voiceTone: '正式、清晰、平稳教学感'
  24. },
  25. taskBrief: {
  26. focalPoints: [
  27. '视频质量 1080p 高清,少 AI 味,人物形象清晰,手要露出来,表情自然',
  28. 'AI 生成音频要跟原稿完全一致,严禁读错字、漏字或擅自改写'
  29. ],
  30. goals: [
  31. '口播稿:《坚持人民至上根本立场 课程讲稿》,字幕原则上与审定讲稿完全一致',
  32. '课件:坚持人民至上根本立场.pptx,配合讲稿完成逻辑严密的同步展示',
  33. '镜头逻辑:结构上“讲师讲述—PPT论证—讲师回场—章节转场”保持庄重感',
  34. '技术参数:1920×1080、25fps、H.264/AAC、48kHz 立体声'
  35. ],
  36. cameraAndPacing: [
  37. '讲师近景与中景切换通常约 20—90 秒,避免静态数字人长时间无动作',
  38. '单页 PPT 始终全屏展示(无需画中画),常驻通常约 10—45 秒',
  39. '画面切换应落在完整句、自然停顿或主题转折处,严禁半句话中间切镜头',
  40. '句间停顿 0.3—0.5 秒,段落转折 0.6—0.9 秒,章节切换 1.2—1.8 秒'
  41. ],
  42. acceptanceCriteria: [
  43. '讲稿无漏句、错句;全部数字、年份、人名、地名读音准确无误',
  44. 'PPT 使用顺序、讲述内容和动画出现点严格一致,每页均可追溯',
  45. '章节层级清楚,各一级部分与小节均有明确进入与收束'
  46. ]
  47. },
  48. instructors: [
  49. {
  50. name: '特邀党建专家 张教授',
  51. organization: '中共党史与理论研究中心',
  52. introduction: '资深党建研究专家、特聘理论导师,长年从事党史党建理论研究与党课教学,语调稳健、讲授权威亲和。',
  53. sampleImageFile: '人物_站.jpg'
  54. }
  55. ],
  56. episodes: [
  57. {
  58. episodeNumber: 1,
  59. title: '第一讲:江山就是人民,人民就是江山',
  60. lectureNotes: '同志们好!今天我们开始第一讲的专题学习。坚持人民至上,是我们党的根本立场,也是马克思主义唯物史观的集中体现……'
  61. },
  62. {
  63. episodeNumber: 2,
  64. title: '第二讲:把人民对美好生活的向往作为奋斗目标',
  65. lectureNotes: '在第二讲中,我们将从实践维度来深入领会。治国有常,利民为本。如何把发展成果转化为人民群众实实在在的幸福感……'
  66. },
  67. {
  68. episodeNumber: 3,
  69. title: '第三讲:紧紧依靠人民创造历史伟业',
  70. lectureNotes: '第三讲,我们聚焦新时代青年理论骨干的使命担当。历史是人民书写的,一切成就归功于人民……'
  71. }
  72. ],
  73. assets: [
  74. {
  75. originalName: '坚持人民至上根本立场 课程讲稿(审定版).docx',
  76. mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  77. sizeText: '156 KB',
  78. description: '第一讲口播讲稿,字词及发音注音已审定',
  79. episodeNumber: 1
  80. },
  81. {
  82. originalName: '第一讲_坚持人民至上根本立场.pptx',
  83. mimeType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
  84. sizeText: '40.5 MB',
  85. description: '第一讲16:9 高清论证课件,对应核心要点',
  86. episodeNumber: 1
  87. },
  88. {
  89. originalName: '第二讲_美好生活奋斗目标讲义.docx',
  90. mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  91. sizeText: '142 KB',
  92. description: '第二讲讲稿与案例参考',
  93. episodeNumber: 2
  94. },
  95. {
  96. originalName: 'teacher_voice_reference_replace.wav',
  97. mimeType: 'audio/wav',
  98. sizeText: '8.2 MB',
  99. description: '讲师原声录音切片,全套课程声纹克隆通用参考'
  100. }
  101. ]
  102. },
  103. {
  104. id: 'higher-edu-calculus',
  105. name: '《深入浅出微积分:极限与导数之美》公开课',
  106. category: '高校公开课',
  107. tags: ['高校公开课', '数理基础', '公式板书', '生动启发'],
  108. badge: '高校示范',
  109. summary: '高校精品开放课程标准,聚焦概念启发与几何直观推导,支持讲师半身出镜与动态板书公式画中画同步。',
  110. description: `【制作目标】打造通俗易懂、几何直观性强的大学数学精品微课,适合线上慕课与翻转课堂。
  111. 【形式结构】知识点引言—几何直观演示—严密数学推导—典型例题精讲—思考总结。
  112. 【运镜与节奏】讲师半身讲述约 40%,公式板书与动态动画演示约 60%;支持黑板/手写板与讲师同框画中画。
  113. 【语速与语气】语速 190—210 汉字/分钟,关键公式推导处适当放慢并留白停顿;语气启发温和、循序渐进。
  114. 【交付规格】1080P 60fps 高帧率(保证公式动画平滑),立体声 48kHz。`,
  115. keySpecs: {
  116. resolution: '1080P (1920×1080) 60fps',
  117. aspectRatio: '16:9 横版',
  118. speechSpeed: '190—210 汉字/分钟',
  119. presenterRatio: '40% 讲师半身 / 60% 动效课件板书',
  120. voiceTone: '启发亲切、循序渐进'
  121. },
  122. taskBrief: {
  123. focalPoints: [
  124. '公式与图形动画平滑无卡顿,60fps 高帧率输出',
  125. '讲解与手写板书笔迹同步出现,重点定理高亮标红'
  126. ],
  127. goals: [
  128. '讲稿:《微积分第一讲:极限的直观与定义》讲义',
  129. '课件:微积分动效演示.pptx(含动画与函数图像)',
  130. '音画同步:讲师语调配合板书推进节奏'
  131. ],
  132. cameraAndPacing: [
  133. '引言部分讲师中景亲切互动,概念阐述时平滑切至动态图像',
  134. '推导定理时采用双画面布局(左侧公式、右侧讲师视线交互)'
  135. ],
  136. acceptanceCriteria: [
  137. '数学符号、希腊字母及公式读法严谨规范',
  138. '函数曲线动画与讲述点完全对齐'
  139. ]
  140. },
  141. instructors: [
  142. {
  143. name: '林副教授',
  144. organization: '数学与应用数学学院',
  145. introduction: '国家级精品课程主讲教师,擅长以直观几何图像剖析抽象数学概念,教学风格深入浅出。',
  146. sampleImageFile: '人物_坐.jpg'
  147. }
  148. ],
  149. episodes: [
  150. {
  151. episodeNumber: 1,
  152. title: '第 1 集:极限的直观与严密定义 (ε-δ 语言)',
  153. lectureNotes: '同学们好!欢迎来到《深入浅出微积分》第1集。今天我们要探索微积分大厦的基石——极限。首先我们来看一个割线逼近切线的动态图像……'
  154. },
  155. {
  156. episodeNumber: 2,
  157. title: '第 2 集:导数的几何意义与瞬时变化率',
  158. lectureNotes: '在第2集中,我们将从瞬时速度的物理问题出发,正式引出导数定义。请大家看屏幕左侧的割线斜率变化……'
  159. },
  160. {
  161. episodeNumber: 3,
  162. title: '第 3 集:微分中值定理与函数单调性',
  163. lectureNotes: '第3集我们将迎来微分学的核心支柱——拉格朗日中值定理。它是联系局部导数与整体增量的桥梁……'
  164. }
  165. ],
  166. assets: [
  167. {
  168. originalName: '第1集_极限直观与定义讲稿.docx',
  169. mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  170. sizeText: '98 KB',
  171. description: '第1集课堂讲稿与板书推导脚本',
  172. episodeNumber: 1
  173. },
  174. {
  175. originalName: '第1集_极限动效演示课件.pptx',
  176. mimeType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
  177. sizeText: '18.6 MB',
  178. description: '第1集16:9 动态数学图像课件',
  179. episodeNumber: 1
  180. },
  181. {
  182. originalName: '第2集_导数几何意义讲稿.docx',
  183. mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  184. sizeText: '112 KB',
  185. description: '第2集导数与切线推导手稿',
  186. episodeNumber: 2
  187. },
  188. {
  189. originalName: '微积分全课程教学大纲与符号规范.pdf',
  190. mimeType: 'application/pdf',
  191. sizeText: '1.2 MB',
  192. description: '全课程通用大纲与板书排版技术标准'
  193. }
  194. ]
  195. }
  196. ]
  197. export async function populateCourseFromTemplate(
  198. pool: any,
  199. courseId: string,
  200. templateId: string,
  201. uploadDir: string,
  202. sampleDir: string
  203. ) {
  204. const template = COURSE_TEMPLATES.find((t) => t.id === templateId) || COURSE_TEMPLATES[0]
  205. // 1. 填充讲师
  206. for (const inst of template.instructors) {
  207. let storedImageName: string | null = null
  208. let origImageName: string | null = null
  209. let mimeType: string | null = null
  210. if (inst.sampleImageFile) {
  211. try {
  212. const sourcePath = resolve(sampleDir, inst.sampleImageFile)
  213. storedImageName = `instructor-${randomUUID()}.jpg`
  214. const destPath = resolve(uploadDir, storedImageName)
  215. await copyFile(sourcePath, destPath)
  216. origImageName = inst.sampleImageFile
  217. mimeType = 'image/jpeg'
  218. } catch (err) {
  219. console.error('Failed to copy sample instructor image:', err)
  220. }
  221. }
  222. await pool.query(
  223. `INSERT INTO instructors(id, course_id, name, organization, introduction, image_original_name, image_stored_name, image_mime_type)
  224. VALUES($1, $2, $3, $4, $5, $6, $7, $8)`,
  225. [
  226. randomUUID(),
  227. courseId,
  228. inst.name,
  229. inst.organization,
  230. inst.introduction,
  231. origImageName,
  232. storedImageName,
  233. mimeType
  234. ]
  235. )
  236. }
  237. // 2. 填充预设分集 (Episodes)
  238. const episodeMap = new Map<number, string>() // episodeNumber -> episodeId
  239. if (template.episodes && template.episodes.length > 0) {
  240. for (const ep of template.episodes) {
  241. const epId = randomUUID()
  242. episodeMap.set(ep.episodeNumber, epId)
  243. await pool.query(
  244. `INSERT INTO episodes(id, course_id, episode_number, title, summary, lecture_notes)
  245. VALUES($1, $2, $3, $4, $5, $6)`,
  246. [epId, courseId, ep.episodeNumber, ep.title, '', ep.lectureNotes || '']
  247. )
  248. }
  249. }
  250. // 3. 填充示例课件资产(支持分配到指定分集或作为通用资产)
  251. for (const asset of template.assets) {
  252. const ext = asset.originalName.split('.').pop() || 'dat'
  253. const storedName = `${courseId}-${randomUUID()}.${ext}`
  254. const destPath = resolve(uploadDir, storedName)
  255. const targetEpId = asset.episodeNumber ? episodeMap.get(asset.episodeNumber) || null : null
  256. // 创建示例占位内容文件
  257. const sampleContent = `=== 星痕智能课程工坊 示范资产 ===\n课程名称:${template.name}\n资产名称:${asset.originalName}\n所属分集:${asset.episodeNumber ? `第 ${asset.episodeNumber} 集` : '课程通用素材'}\n类型描述:${asset.description || '示例教学资料'}\n\n该文件为系统预设课程示例,用户可作为制作任务书与课件标准进行参考。`
  258. await writeFile(destPath, Buffer.from(sampleContent, 'utf8'))
  259. // 虚拟大小 (参考展示)
  260. let approxSize = 1024 * 256
  261. if (asset.sizeText.includes('MB')) {
  262. approxSize = Math.round(parseFloat(asset.sizeText) * 1024 * 1024)
  263. } else if (asset.sizeText.includes('KB')) {
  264. approxSize = Math.round(parseFloat(asset.sizeText) * 1024)
  265. }
  266. await pool.query(
  267. `INSERT INTO course_assets(id, course_id, episode_id, original_name, stored_name, size, mime_type)
  268. VALUES($1, $2, $3, $4, $5, $6, $7)`,
  269. [randomUUID(), courseId, targetEpId, asset.originalName, storedName, approxSize, asset.mimeType]
  270. )
  271. }
  272. }