当前位置: 首页 > news >正文

AI-Course-Presenter

功能特点

一个基于 AI 的课程内容转换工具,可以将教学讲义转换为 类PPT 格式的演示文稿。

  • 支持将 Markdown 格式的教学讲义转换为包含讲解稿的 PPT 文档
  • 自动生成每个章节的幻灯片内容和讲解稿
  • 保持原有的代码高亮、表格、列表等 Markdown 格式特性
  • 提供在线预览功能
  • 支持文件拖拽上传
  • 支持配置 API Key

快速开始

环境要求

  • Java 8+
  • Maven 3.6+
  • Spring Boot 2.3.4.RELEASE

安装步骤

  1. 克隆项目
git clone https://gitee.com/anxwefndu/ai-course-presenter.git
  1. 进入项目目录
cd ai-course-presenter/code
  1. 编译运行
mvn spring-boot:run
  1. 访问应用
  • 打开浏览器访问: http://localhost:10010
  • 转换工具页面: http://localhost:10010/transfer.html
  • 预览页面: http://localhost:10010/preview.html

使用说明

  1. 准备讲义文件

    • 使用 Markdown 格式编写教学讲义
    • 建议参考项目中的示例模板
  2. 转换讲义

    • 访问转换工具页面
    • 上传或拖拽 Markdown 文件
    • 等待转换完成
  3. 查看结果

    • 预览生成的 PPT 内容
    • 下载转换后的文件

项目结构

ai-course-presenter/
├── src/
│   └── main/
│       ├── java/
│       │   └── com/boot/
│       │       ├── controller/    # 控制器
│       │       └── Application.java
│       └── resources/
│           ├── static/           # 前端资源
│           └── application.properties
└── pom.xml

技术栈

  • 后端:Spring Boot
  • 前端:HTML5 + TailwindCSS
  • AI:OpenAI API
  • 工具库:
    • marked.js (Markdown 解析)
    • highlight.js (代码高亮)
    • reveal.js (PPT 展示)

接入通义TTS

采用通义的TTS接口来进行讲解稿阅读

核心代码

code/src/main/java/com/boot/util/VoiceUtil.java

package com.boot.util;import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesisParam;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesizer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;import java.nio.ByteBuffer;@Service
@Slf4j
public class VoiceUtil {@Value("${sk}")private String sk;private static final String model = "cosyvoice-v1";private static final String voice = "longxiaochun";public byte[] synthesizeAudio(String text) {SpeechSynthesisParam param = SpeechSynthesisParam.builder().apiKey(sk).model(model).voice(voice).build();SpeechSynthesizer synthesizer = new SpeechSynthesizer(param, null);ByteBuffer audio = synthesizer.call(text);log.info("语音合成请求ID: {}", synthesizer.getLastRequestId());return audio.array();}public byte[] textToSpeech(String text) {try {return synthesizeAudio(text);} catch (Exception e) {log.error("语音合成失败", e);throw new RuntimeException("语音合成失败: " + e.getMessage());}}
}

code/src/main/java/com/boot/controller/TTSController.java

package com.boot.controller;import com.boot.util.VoiceUtil;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;@RestController
@RequestMapping("/tts")
public class TTSController {@Resourceprivate VoiceUtil voiceUtil;@GetMapping("/synthesize")public ResponseEntity<byte[]> synthesizeAudio(@RequestParam String text) {byte[] audioData = voiceUtil.textToSpeech(text);return ResponseEntity.ok().contentType(MediaType.parseMediaType("audio/mpeg")).body(audioData);}
}

code/src/main/resources/static/preview.html

<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>MD - PPT 预览页面</title><link rel="stylesheet" href="lib/reveal.css"><link rel="stylesheet" href="lib/white.css"><link rel="stylesheet" href="css/custom.css"><link rel="stylesheet" href="css/base.css"><link rel="stylesheet" href="css/notification/notification.css"><style>/* 加载提示样式 */.loading-overlay {position: fixed;top: 0;left: 0;right: 0;bottom: 0;background: rgba(255, 255, 255, 0.9);display: flex;justify-content: center;align-items: center;z-index: 1000;font-size: 1.5rem;color: #3498db;}.loading-message {text-align: center;padding: 20px;border-radius: 8px;box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);}</style></head><body><!-- 加载提示 --><div class="loading-overlay" id="loadingOverlay"><div class="loading-message">正在加载内容,请稍候...<div class="spinner"></div></div></div><div class="reveal" id="contentContainer"><div class="slides"><!-- Markdown内容将被动态加载到这里 --></div></div><div class="ui-notification-container ui-notification-container--top-right"></div><script src="lib/reveal.js"></script><script src="lib/marked.min.js"></script><script src="lib/highlight.min.js"></script><link href="lib/github.min.css" rel="stylesheet"><script>// 显示加载提示function showLoading() {document.getElementById('loadingOverlay').style.display = 'flex';document.getElementById('contentContainer').style.display = 'none';}// 隐藏加载提示并显示内容function hideLoading() {document.getElementById('loadingOverlay').style.display = 'none';document.getElementById('contentContainer').style.display = 'block';}function viewPpt(markdown) {// 按幻灯片分割内容const slides = markdown.split('==== slide ====');const slidesContainer = document.querySelector('.slides');slidesContainer.innerHTML = "";slides.forEach(slide => {const section = document.createElement('section');section.setAttribute('data-markdown', '');// 处理讲解稿const parts = slide.split('#### 讲解稿:');let content = parts[0];const notes = parts[1];// 移除"内容:"标记content = content.replace('#### 内容:', '');// 移除幻灯片标题标记// 正则表达式const regex = /### \*\*幻灯片\s+(\d+)[^:]*:(.+?)\*\*/g;content = content.replace(regex, '### $1. $2');// 使用marked将Markdown转换为HTMLcontent = marked.parse(content);// 创建内容容器const contentDiv = document.createElement('div');contentDiv.innerHTML = content;section.appendChild(contentDiv);// 高亮代码块section.querySelectorAll('pre code').forEach((block) => {hljs.highlightBlock(block);});// 如果有讲解稿,添加到aside元素中if (notes) {const aside = document.createElement('aside');aside.className = 'notes';aside.innerHTML = marked.parse(notes);section.appendChild(aside);}slidesContainer.appendChild(section);});// 初始化reveal.js,添加更多配置Reveal.initialize({hash: true,// 显示演讲者注记showNotes: true,// 控制按钮controls: true,// 进度条progress: true,// 页码slideNumber: true,// 键盘快捷键keyboard: true,// 概览模式overview: true,// 中心缩放center: true,// 转场动画transition: 'slide',// 背景切换动画backgroundTransition: 'fade',// 内容缩放以适应屏幕width: '100%',height: '100%',margin: 0.1,minScale: 0.2,maxScale: 1.5});// 添加音频播放器管理let currentAudio = null;let currentIndex = 0;// 停止当前正在播放的音频function stopCurrentAudio() {if (currentAudio) {currentAudio.pause();currentAudio.currentTime = 0;currentAudio = null;}}// 播放新的音频async function playSlideAudio(slideIndex) {stopCurrentAudio();let audioUrl = audioCache.get(slideIndex);if (!audioUrl) {stopCurrentAudio();showInfo("音频数据请求中");audioCache.set(slideIndex, "音频数据请求中");await preloadAudios(slideIndex);audioUrl = audioCache.get(slideIndex);} else if (audioUrl === "音频数据请求中") {return;}if (currentIndex !== slideIndex) {return;}currentAudio = new Audio(audioUrl);currentAudio.play().catch(error => {console.error('音频播放失败:', error);});}// 修改事件监听,使用缓存的音频Reveal.addEventListener('slidechanged', function () {const slideIndex = Reveal.getIndices().h;currentIndex = slideIndex;playSlideAudio(slideIndex);});// 创建音频缓存对象const audioCache = new Map();// 预加载所有音频async function preloadAudios(slideIndex) {for (let i = 0; i < slides.length; i++) {if (i === slideIndex) {const parts = slides[i].split('#### 讲解稿:');const notes = parts[1];if (notes) {const text = notes.trim();if (text) {try {const response = await fetch(`/tts/synthesize?text=${encodeURIComponent(text)}`);const blob = await response.blob();const audioUrl = URL.createObjectURL(blob);audioCache.set(i, audioUrl);} catch (error) {console.error(`预加载音频失败 (幻灯片 ${i + 1}):`, error);}}}}}}// 播放第一个幻灯片的音频playSlideAudio(0);}function loadContent() {showLoading(); // 显示加载提示const params = new URLSearchParams(window.location.search);const id = params.get('id');// 使用 Fetch API 获取文件fetch(`/file/getFile?id=${id}`).then(response => response.json()).then(data => {if (data.status === "success") {const file = data.file;hideLoading();viewPpt(file.content);} else {alert("无法获取文件:" + data.message);}}).catch(error => {console.error('Error:', error);alert("获取文件列表时出错!");hideLoading(); // 隐藏加载提示});}// 页面加载时自动调用window.onload = () => {loadContent();};// 信息提示function showInfo(text) {createNotification({type: 'info',title: '信息提示',message: text,icon: 'ℹ️'});}// 关闭通知function closeNotification(notification) {notification.classList.add('is-closing');setTimeout(() => {notification.remove();}, 300);}// 创建通知function createNotification(options) {const container = document.querySelector('.ui-notification-container');const notification = document.createElement('div');notification.className = `ui-notification ui-notification--${options.type || 'info'}`;// 通知内容notification.innerHTML = `<div class="ui-notification__icon">${options.icon || 'ℹ️'}</div><div class="ui-notification__content"><h4 class="ui-notification__title">${options.title}</h4><p class="ui-notification__message">${options.message}</p></div><span class="ui-notification__close">×</span>${options.progress ? '<div class="ui-notification__progress"></div>' : ''}`;container.appendChild(notification);// 关闭按钮事件const closeBtn = notification.querySelector('.ui-notification__close');closeBtn.addEventListener('click', () => closeNotification(notification));// 自动关闭if (options.duration !== 0) {setTimeout(() => closeNotification(notification), options.duration || 4500);}// 进度条if (options.progress) {const progressBar = notification.querySelector('.ui-notification__progress');let width = 100;const interval = setInterval(() => {width--;progressBar.style.width = width + '%';if (width <= 0) {clearInterval(interval);closeNotification(notification);}}, (options.duration || 4500) / 100);}return notification;}</script></body></html>

源码下载

AI-Course-Presenter

http://www.xdnf.cn/news/486577.html

相关文章:

  • Houdini POP入门学习01
  • 后端框架(1):Mybatis
  • “分布形态“
  • 浅谈“量子计算应用:从基础原理到行业破局”
  • ohttps开启群晖ssl证书自动更新
  • lesson03-简单回归案例(理论+代码)
  • 文章记单词 | 第87篇(六级)
  • FC7300 ADC MCAL配置引导
  • sqli-labs靶场23-28a关(过滤)
  • conda init执行了还是不好用
  • 2025年长三角+山东省赛+ 认证杯二阶段论文发布!
  • python是如何调用前后双下划线的函数的
  • mysql集群
  • [前端] wang 富文本 vue3
  • 【GaussDB迁移攻略】DRS支持CDC,解决大规模数据迁移挑战
  • 芯谷产业园:双流元宇宙开放数字贸易新坐标
  • C++:字符串操作函数
  • 刷leetcodehot100返航版--双指针5/16
  • 虚拟来电 4.3.0 |集虚拟来电与短信于一体,解锁VIP优雅脱身
  • 腾讯云代码助手CodeBuddy使用体验
  • 7.1Java多线程安全和同步
  • vue 指令
  • python版本管理工具-pyenv轻松切换多个Python版本
  • DATE_FORMAT可以接收date类型,也可以接收String类型!
  • this.$set的用法-响应式数据更新
  • oracle主备切换参考
  • 初学者如何用 Python 写第一个爬虫?
  • 【LLM】大模型落地应用的技术 ——— 推理训练 MOE,AI搜索 RAG,AI Agent MCP
  • ​小店推客系统开发SEO全攻略:从技术架构到流量裂变,打造私域增长引擎
  • Android framework 中间件开发(二)