aiPptResources.ts 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
  1. import type { AiCoursewareOutline } from "@/api/aiCourseware"
  2. import type { Slide } from "@/types/slides"
  3. import type { AiPptQualityIssue } from "@/utils/aiPptQuality"
  4. // 仅在浏览器检查实际可加载性,不以 URL 格式代替媒体可用性。
  5. export async function inspectAiPptResources(outline: AiCoursewareOutline, slides: Slide[]): Promise<AiPptQualityIssue[]> {
  6. const pending = new Map<string, Promise<{ width: number; height: number }>>()
  7. const issues: AiPptQualityIssue[] = []
  8. const probe = (url: string, kind: "image" | "audio" | "video") => {
  9. const key = `${kind}:${url}`
  10. if (!pending.has(key)) pending.set(key, new Promise((resolve, reject) => {
  11. const media = kind === "image" ? new Image() : document.createElement(kind)
  12. const finish = (error?: string) => {
  13. clearTimeout(timer)
  14. media.onload = media.onerror = null
  15. if (media instanceof HTMLMediaElement) {
  16. media.onloadedmetadata = null
  17. media.removeAttribute("src")
  18. media.load()
  19. }
  20. if (error) reject(new Error(error))
  21. else resolve(media instanceof HTMLImageElement ? { width: media.naturalWidth, height: media.naturalHeight } : { width: 0, height: 0 })
  22. }
  23. const timer = window.setTimeout(() => finish("加载超时"), 15000)
  24. media.onerror = () => finish("无法加载或格式不受支持")
  25. if (media instanceof HTMLMediaElement) {
  26. media.preload = "metadata"
  27. media.onloadedmetadata = () => finish()
  28. } else media.onload = () => finish()
  29. media.src = url
  30. }))
  31. return pending.get(key)!
  32. }
  33. await Promise.all(slides.map(async (slide, index) => {
  34. const scoreUrls = new Set((outline.pages[index]?.resourceBindings || []).filter(item => item.resourceType === "MUSIC").map(item => item.scoreImageUrl))
  35. await Promise.all(slide.elements.map(async element => {
  36. const kind = element.type === "image" ? "image" : element.type === "elf" && element.subtype === "elf-audio" ? "audio" : element.type === "elf" && element.subtype === "elf-video" ? "video" : undefined
  37. if (!kind || !("src" in element)) return
  38. try {
  39. const size = await probe(element.src, kind)
  40. // 谱图完整等比放进原槽位,禁止拉伸音符或裁掉小节。
  41. if (element.type === "image" && scoreUrls.has(element.src) && size.width && size.height) {
  42. const scale = Math.min(element.width / size.width, element.height / size.height)
  43. const width = size.width * scale
  44. const height = size.height * scale
  45. element.left += (element.width - width) / 2
  46. element.top += (element.height - height) / 2
  47. element.width = width
  48. element.height = height
  49. }
  50. } catch (error) {
  51. issues.push({ code: "resource_unavailable", severity: "error", pageNo: index + 1,
  52. message: `${kind === "image" ? "图片" : kind === "audio" ? "音频" : "视频"}${error instanceof Error ? error.message : "不可用"},请更换资源后重试` })
  53. }
  54. }))
  55. }))
  56. return issues
  57. }