diff --git a/README_WEB.md b/README_WEB.md new file mode 100644 index 0000000..bd81bc0 --- /dev/null +++ b/README_WEB.md @@ -0,0 +1,203 @@ +# 网页版使用说明 + +## 功能介绍 + +这是一个基于Flask的网页应用,提供了**背诵排序**功能,可以帮助你: + +1. **识别知识点**:从输入的文本中自动识别出要背诵的知识点 +2. **随机排序**:对识别出的知识点进行随机排序 +3. **转盘抽背**:通过转盘功能随机选择背诵内容 + +## 快速开始 + +### 1. 安装依赖 + +```bash +pip install Flask>=3.0.0 +``` + +或者安装所有依赖: + +```bash +pip install -r requirements.txt +``` + +### 2. 启动应用 + +运行启动脚本: + +```bash +python start_web.py +``` + +或者直接运行: + +```bash +python web_app.py +``` + +### 3. 访问应用 + +在浏览器中打开: + +- 首页:http://localhost:5000 +- 背诵排序:http://localhost:5000/recitation + +## 使用步骤 + +### 第一步:输入背诵内容 + +在文本框中粘贴包含知识点列表的文本,支持以下格式: + +- 列表格式(数字开头) +- 表格格式(从表格中复制) +- 普通文本(每行一个知识点) + +示例: +``` +第一章 西周 +夏商学校名称 +西周学在官府 +国学乡学 +六艺 +私学兴起的原因与意义 +稷下学宫 +``` + +### 第二步:识别知识点 + +点击"识别知识点"按钮,系统会自动: +- 过滤无关内容(表头、页码等) +- 提取有效的知识点 +- 显示识别结果 + +### 第三步:随机排序 + +点击"开始随机排序"按钮,系统会: +- 对知识点进行随机打乱 +- 生成随机排序列表 +- 创建转盘界面 + +### 第四步:转盘抽背 + +点击"转动转盘"按钮: +- 转盘会旋转3圈后停下 +- 随机选中一个知识点 +- 显示选中的内容 + +同时,页面下方会显示完整的随机排序结果列表。 + +## 技术说明 + +### 后端技术 +- **Flask**:轻量级Web框架 +- **Python正则表达式**:文本解析和知识点提取 + +### 前端技术 +- **HTML5 + CSS3**:响应式页面设计 +- **JavaScript (原生)**:交互逻辑 +- **SVG**:转盘可视化 + +### 知识点识别规则 + +系统会智能识别以下内容: +1. 以数字或章节号开头的行(如"第一章"、"1. 知识点") +2. 以列表符号开头的行(如"- 知识点"、"? 知识点") +3. 包含中文且非空的行 + +系统会自动过滤: +- 表头行(包含"章节"、"知识点"等关键词) +- 页码行(如"第1页") +- 说明文字 +- 空行 + +## API接口 + +### 提取知识点 + +**POST** `/api/extract` + +请求体: +```json +{ + "text": "输入文本内容" +} +``` + +响应: +```json +{ + "success": true, + "items": ["知识点1", "知识点2", ...], + "count": 2 +} +``` + +### 随机排序 + +**POST** `/api/sort` + +请求体: +```json +{ + "items": ["知识点1", "知识点2", ...] +} +``` + +响应: +```json +{ + "success": true, + "items": ["知识点2", "知识点1", ...], + "count": 2 +} +``` + +## 目录结构 + +``` +diet_recommendation_app/ +├── web_app.py # Flask应用主文件 +├── start_web.py # 启动脚本 +├── templates/ # HTML模板 +│ ├── index.html # 首页 +│ └── recitation.html # 背诵排序页面 +├── static/ # 静态资源 +│ ├── css/ +│ │ ├── style.css # 通用样式 +│ │ └── recitation.css # 背诵排序页面样式 +│ └── js/ +│ └── recitation.js # 前端交互逻辑 +└── logs/ # 日志文件 + └── web_app.log +``` + +## 注意事项 + +1. 首次运行会自动创建必要的目录(templates、static、logs) +2. 建议在本地环境中使用,如需公网访问请配置防火墙和反向代理 +3. 日志文件保存在 `logs/web_app.log` + +## 故障排除 + +### 问题:无法启动应用 + +**解决方案**: +- 检查Flask是否已安装:`pip list | grep Flask` +- 检查端口5000是否被占用 +- 查看日志文件 `logs/web_app.log` + +### 问题:无法识别知识点 + +**解决方案**: +- 确保输入文本格式正确 +- 尝试手动整理文本,每行一个知识点 +- 检查是否包含特殊字符 + +### 问题:转盘不显示或旋转异常 + +**解决方案**: +- 检查浏览器是否支持SVG +- 清除浏览器缓存 +- 使用现代浏览器(Chrome、Firefox、Edge等) + diff --git a/requirements.txt b/requirements.txt index 9e99d50..40f83da 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,4 +27,6 @@ easyocr>=1.7.0 # 绉诲姩绔敮鎸 (鍙) kivy>=2.1.0 -kivymd>=1.1.1 \ No newline at end of file +kivymd>=1.1.1 +# 缂冩垿銆夌粩顖涙暜閹镐梗nFlask>=3.0.0 +Werkzeug>=3.0.0 diff --git a/start_web.py b/start_web.py new file mode 100644 index 0000000..55ecf98 --- /dev/null +++ b/start_web.py @@ -0,0 +1,57 @@ +锘# -*- coding: utf-8 -*- +""" +鍚姩缃戦〉搴旂敤鐨勮剼鏈 +""" + +import sys +from pathlib import Path + +# 娣诲姞椤圭洰鏍圭洰褰曞埌Python璺緞 +project_root = Path(__file__).parent +sys.path.insert(0, str(project_root)) + +def check_flask(): + """妫鏌lask鏄惁宸插畨瑁""" + try: + import flask + print(f"鉁 Flask宸插畨瑁 (鐗堟湰: {flask.__version__})") + return True + except ImportError: + print("鉁 Flask鏈畨瑁咃紝璇疯繍琛: pip install Flask") + return False + +def main(): + """鍚姩缃戦〉搴旂敤""" + print("馃寪 鍚姩缃戦〉搴旂敤...") + print("=" * 50) + + if not check_flask(): + return False + + # 鍒涘缓蹇呰鐨勭洰褰 + Path('templates').mkdir(exist_ok=True) + Path('static/css').mkdir(parents=True, exist_ok=True) + Path('static/js').mkdir(parents=True, exist_ok=True) + Path('logs').mkdir(exist_ok=True) + + print("\n馃殌 姝e湪鍚姩缃戦〉鏈嶅姟鍣...") + print("=" * 50) + print("馃摫 璁块棶鍦板潃: http://localhost:5000") + print("馃摑 鑳岃鎺掑簭: http://localhost:5000/recitation") + print("\n鎸 Ctrl+C 鍋滄鏈嶅姟鍣╘n") + + try: + from web_app import app + app.run(debug=True, host='0.0.0.0', port=5000) + except KeyboardInterrupt: + print("\n馃憢 鏈嶅姟鍣ㄥ凡鍋滄") + except Exception as e: + print(f"\n鉂 鍚姩澶辫触: {e}") + return False + + return True + +if __name__ == "__main__": + success = main() + if not success: + sys.exit(1) diff --git a/static/css/recitation.css b/static/css/recitation.css new file mode 100644 index 0000000..a67aca2 --- /dev/null +++ b/static/css/recitation.css @@ -0,0 +1,212 @@ +.recitation-container { + max-width: 900px; + margin: 0 auto; +} + +.input-section, +.extracted-section, +.wheel-section, +.result-section { + margin-bottom: 40px; + padding: 30px; + background: #f8f9fa; + border-radius: 15px; +} + +.input-section h2, +.extracted-section h2, +.wheel-section h2, +.result-section h2 { + font-size: 1.8em; + margin-bottom: 15px; + color: #333; +} + +.hint, +.info { + color: #666; + margin-bottom: 20px; + line-height: 1.6; +} + +.info span { + color: #667eea; + font-weight: bold; +} + +.text-input { + width: 100%; + padding: 15px; + border: 2px solid #e9ecef; + border-radius: 10px; + font-size: 1em; + font-family: inherit; + resize: vertical; + margin-bottom: 20px; + transition: border-color 0.3s; +} + +.text-input:focus { + outline: none; + border-color: #667eea; +} + +.items-list { + max-height: 400px; + overflow-y: auto; + padding: 15px; + background: white; + border-radius: 10px; + margin-bottom: 20px; +} + +.item-tag { + display: inline-block; + padding: 8px 15px; + margin: 5px; + background: #667eea; + color: white; + border-radius: 20px; + font-size: 0.9em; +} + +/* 转盘样式 */ +.wheel-container { + position: relative; + width: 400px; + height: 400px; + margin: 40px auto; +} + +.wheel { + width: 100%; + height: 100%; + border-radius: 50%; + position: relative; + border: 8px solid #667eea; + background: white; + overflow: hidden; + transition: transform 3s cubic-bezier(0.17, 0.67, 0.12, 0.99); + box-shadow: 0 0 20px rgba(0, 0, 0, 0.2); +} + +.wheel svg { + transition: transform 3s cubic-bezier(0.17, 0.67, 0.12, 0.99); +} + +.wheel-pointer { + position: absolute; + top: -15px; + left: 50%; + transform: translateX(-50%); + width: 0; + height: 0; + border-left: 15px solid transparent; + border-right: 15px solid transparent; + border-top: 30px solid #667eea; + z-index: 10; + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2)); +} + +.btn-spin { + display: block; + margin: 30px auto; + padding: 15px 50px; + font-size: 1.2em; + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + color: white; + border: none; + border-radius: 30px; + cursor: pointer; + transition: all 0.3s; +} + +.btn-spin:hover { + transform: scale(1.05); + box-shadow: 0 5px 20px rgba(245, 87, 108, 0.4); +} + +.btn-spin:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +.current-item { + text-align: center; + padding: 20px; + background: white; + border-radius: 10px; + margin-top: 20px; + font-size: 1.3em; + font-weight: bold; + color: #667eea; + min-height: 60px; + display: flex; + align-items: center; + justify-content: center; +} + +/* 排序结果列表 */ +.sorted-list { + background: white; + border-radius: 10px; + padding: 20px; + max-height: 500px; + overflow-y: auto; +} + +.sorted-item { + padding: 15px; + margin: 10px 0; + background: #f8f9fa; + border-radius: 8px; + border-left: 4px solid #667eea; + display: flex; + align-items: center; + transition: all 0.3s; +} + +.sorted-item:hover { + background: #e9ecef; + transform: translateX(5px); +} + +.sorted-item-number { + display: inline-flex; + align-items: center; + justify-content: center; + width: 35px; + height: 35px; + background: #667eea; + color: white; + border-radius: 50%; + font-weight: bold; + margin-right: 15px; + flex-shrink: 0; +} + +.sorted-item-text { + flex: 1; + color: #333; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .wheel-container { + width: 300px; + height: 300px; + } + + .wheel-item { + font-size: 0.7em; + } + + .input-section, + .extracted-section, + .wheel-section, + .result-section { + padding: 20px; + } +} + diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..de45c28 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,179 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + padding: 20px; + color: #333; +} + +.container { + max-width: 1200px; + margin: 0 auto; + background: white; + border-radius: 20px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1); + overflow: hidden; +} + +.header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 40px 30px; + text-align: center; +} + +.header h1 { + font-size: 2.5em; + margin-bottom: 10px; +} + +.subtitle { + font-size: 1.1em; + opacity: 0.9; +} + +.nav { + display: flex; + background: #f8f9fa; + padding: 0; + border-bottom: 2px solid #e9ecef; +} + +.nav-item { + flex: 1; + padding: 15px 20px; + text-align: center; + text-decoration: none; + color: #666; + transition: all 0.3s; + border-bottom: 3px solid transparent; +} + +.nav-item:hover { + background: #e9ecef; + color: #667eea; +} + +.nav-item.active { + color: #667eea; + border-bottom-color: #667eea; + font-weight: bold; +} + +.main { + padding: 40px 30px; + min-height: 500px; +} + +.welcome-section { + text-align: center; +} + +.welcome-section h2 { + font-size: 2em; + margin-bottom: 20px; + color: #333; +} + +.welcome-section p { + font-size: 1.1em; + color: #666; + margin-bottom: 40px; +} + +.feature-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 30px; + margin-top: 40px; +} + +.card { + background: #f8f9fa; + border-radius: 15px; + padding: 30px; + text-align: center; + transition: transform 0.3s, box-shadow 0.3s; +} + +.card:hover { + transform: translateY(-5px); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); +} + +.card-icon { + font-size: 3em; + margin-bottom: 20px; +} + +.card h3 { + font-size: 1.5em; + margin-bottom: 15px; + color: #333; +} + +.card p { + color: #666; + margin-bottom: 20px; + line-height: 1.6; +} + +.btn { + padding: 12px 30px; + border: none; + border-radius: 25px; + font-size: 1em; + cursor: pointer; + transition: all 0.3s; + text-decoration: none; + display: inline-block; +} + +.btn-primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); +} + +.btn-secondary { + background: #6c757d; + color: white; +} + +.btn-secondary:hover { + background: #5a6268; +} + +.footer { + background: #f8f9fa; + padding: 20px; + text-align: center; + color: #666; + border-top: 1px solid #e9ecef; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .header h1 { + font-size: 2em; + } + + .main { + padding: 20px 15px; + } + + .feature-cards { + grid-template-columns: 1fr; + } +} + diff --git a/static/js/recitation.js b/static/js/recitation.js new file mode 100644 index 0000000..bbccc6f --- /dev/null +++ b/static/js/recitation.js @@ -0,0 +1,285 @@ +// 背诵排序功能脚本 + +let extractedItems = []; +let sortedItems = []; +let currentSpinIndex = 0; +let isSpinning = false; + +// 颜色配置 - 转盘使用不同颜色 +const colors = [ + '#667eea', '#764ba2', '#f093fb', '#f5576c', + '#4facfe', '#00f2fe', '#43e97b', '#38f9d7', + '#fa709a', '#fee140', '#30cfd0', '#330867' +]; + +// DOM元素 +const textInput = document.getElementById('textInput'); +const extractBtn = document.getElementById('extractBtn'); +const extractedSection = document.getElementById('extractedSection'); +const itemsList = document.getElementById('itemsList'); +const itemCount = document.getElementById('itemCount'); +const sortBtn = document.getElementById('sortBtn'); +const wheelSection = document.getElementById('wheelSection'); +const wheel = document.getElementById('wheel'); +const spinBtn = document.getElementById('spinBtn'); +const currentItem = document.getElementById('currentItem'); +const resultSection = document.getElementById('resultSection'); +const sortedList = document.getElementById('sortedList'); +const resetBtn = document.getElementById('resetBtn'); + +// 提取知识点 +extractBtn.addEventListener('click', async () => { + const text = textInput.value.trim(); + + if (!text) { + alert('请输入要处理的文本'); + return; + } + + extractBtn.disabled = true; + extractBtn.textContent = '识别中...'; + + try { + const response = await fetch('/api/extract', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ text }) + }); + + const data = await response.json(); + + if (data.success) { + extractedItems = data.items; + displayExtractedItems(extractedItems); + extractedSection.style.display = 'block'; + textInput.disabled = true; + } else { + alert(data.message || '提取失败'); + } + } catch (error) { + console.error('提取失败:', error); + alert('提取失败,请检查网络连接'); + } finally { + extractBtn.disabled = false; + extractBtn.textContent = '识别知识点'; + } +}); + +// 显示提取的项目 +function displayExtractedItems(items) { + itemCount.textContent = items.length; + itemsList.innerHTML = ''; + + items.forEach((item, index) => { + const tag = document.createElement('span'); + tag.className = 'item-tag'; + tag.textContent = `${index + 1}. ${item}`; + itemsList.appendChild(tag); + }); +} + +// 随机排序 +sortBtn.addEventListener('click', async () => { + if (extractedItems.length === 0) { + alert('请先提取知识点'); + return; + } + + sortBtn.disabled = true; + sortBtn.textContent = '排序中...'; + + try { + const response = await fetch('/api/sort', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ items: extractedItems }) + }); + + const data = await response.json(); + + if (data.success) { + sortedItems = data.items; + displaySortedItems(sortedItems); + createWheel(sortedItems); + wheelSection.style.display = 'block'; + resultSection.style.display = 'block'; + currentSpinIndex = 0; + } else { + alert(data.message || '排序失败'); + } + } catch (error) { + console.error('排序失败:', error); + alert('排序失败,请检查网络连接'); + } finally { + sortBtn.disabled = false; + sortBtn.textContent = '开始随机排序'; + } +}); + +// 创建转盘 - 使用SVG实现更真实的转盘效果 +function createWheel(items) { + wheel.innerHTML = ''; + + if (items.length === 0) return; + + const anglePerItem = 360 / items.length; + const radius = 190; // 转盘半径(考虑边框) + const centerX = 200; + const centerY = 200; + + // 创建SVG转盘 + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('width', '400'); + svg.setAttribute('height', '400'); + svg.setAttribute('viewBox', '0 0 400 400'); + svg.style.position = 'absolute'; + svg.style.top = '0'; + svg.style.left = '0'; + svg.style.width = '100%'; + svg.style.height = '100%'; + + items.forEach((item, index) => { + const startAngle = (index * anglePerItem - 90) * Math.PI / 180; + const endAngle = ((index + 1) * anglePerItem - 90) * Math.PI / 180; + + const x1 = centerX + radius * Math.cos(startAngle); + const y1 = centerY + radius * Math.sin(startAngle); + const x2 = centerX + radius * Math.cos(endAngle); + const y2 = centerY + radius * Math.sin(endAngle); + + const largeArcFlag = anglePerItem > 180 ? 1 : 0; + + const pathData = [ + `M ${centerX} ${centerY}`, + `L ${x1} ${y1}`, + `A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2}`, + 'Z' + ].join(' '); + + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + path.setAttribute('d', pathData); + const colorIndex = index % colors.length; + path.setAttribute('fill', colors[colorIndex]); + path.setAttribute('stroke', '#fff'); + path.setAttribute('stroke-width', '2'); + + svg.appendChild(path); + + // 添加文本 + const midAngle = (startAngle + endAngle) / 2; + const textRadius = radius * 0.7; + const textX = centerX + textRadius * Math.cos(midAngle); + const textY = centerY + textRadius * Math.sin(midAngle); + + const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + text.setAttribute('x', textX); + text.setAttribute('y', textY); + text.setAttribute('text-anchor', 'middle'); + text.setAttribute('fill', 'white'); + text.setAttribute('font-size', items.length > 8 ? '12' : '14'); + text.setAttribute('font-weight', 'bold'); + text.setAttribute('transform', `rotate(${(midAngle * 180 / Math.PI + 90)}, ${textX}, ${textY})`); + + const displayText = item.length > 12 ? item.substring(0, 12) + '...' : item; + text.textContent = displayText; + + svg.appendChild(text); + }); + + wheel.appendChild(svg); +} + +// 转动转盘 +spinBtn.addEventListener('click', () => { + if (isSpinning || sortedItems.length === 0) return; + + isSpinning = true; + spinBtn.disabled = true; + currentItem.textContent = '转盘中...'; + + // 随机选择一个索引(添加多圈旋转效果) + const randomIndex = Math.floor(Math.random() * sortedItems.length); + const spins = 3; // 转3圈 + const anglePerItem = 360 / sortedItems.length; + // 计算目标角度:多转几圈 + 指向选中项 + const targetAngle = spins * 360 + (360 - (randomIndex * anglePerItem) - anglePerItem / 2); + + // 获取当前角度 + const svg = wheel.querySelector('svg'); + const currentAngle = getCurrentRotation(svg); + + // 计算总旋转角度(考虑当前角度) + const totalRotation = currentAngle + targetAngle; + + svg.style.transform = `rotate(${totalRotation}deg)`; + + // 转盘停止后显示结果 + setTimeout(() => { + currentItem.textContent = `${randomIndex + 1}. ${sortedItems[randomIndex]}`; + currentSpinIndex = randomIndex; + isSpinning = false; + spinBtn.disabled = false; + }, 3000); +}); + +// 获取当前旋转角度 +function getCurrentRotation(element) { + const style = window.getComputedStyle(element); + const transform = style.transform; + if (transform === 'none') return 0; + + const matrix = new DOMMatrix(transform); + const angle = Math.atan2(matrix.b, matrix.a) * (180 / Math.PI); + return angle; +} + +// 显示排序结果 +function displaySortedItems(items) { + sortedList.innerHTML = ''; + + items.forEach((item, index) => { + const itemDiv = document.createElement('div'); + itemDiv.className = 'sorted-item'; + + const numberSpan = document.createElement('span'); + numberSpan.className = 'sorted-item-number'; + numberSpan.textContent = index + 1; + + const textSpan = document.createElement('span'); + textSpan.className = 'sorted-item-text'; + textSpan.textContent = item; + + itemDiv.appendChild(numberSpan); + itemDiv.appendChild(textSpan); + sortedList.appendChild(itemDiv); + }); +} + +// 重置 +resetBtn.addEventListener('click', () => { + extractedItems = []; + sortedItems = []; + currentSpinIndex = 0; + + textInput.value = ''; + textInput.disabled = false; + + extractedSection.style.display = 'none'; + wheelSection.style.display = 'none'; + resultSection.style.display = 'none'; + + wheel.innerHTML = ''; + const svg = wheel.querySelector('svg'); + if (svg) { + svg.style.transform = 'rotate(0deg)'; + } + currentItem.textContent = ''; + + isSpinning = false; + spinBtn.disabled = false; +}); + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..a36e378 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,42 @@ +锘 + + + + + 涓у寲楗鎺ㄨ崘鍔╂墜 + + + +
+
+

涓у寲楗鎺ㄨ崘鍔╂墜

+

鏅鸿兘楗鎺ㄨ崘 + 鑳岃鎺掑簭宸ュ叿

+
+ + + +
+
+

娆㈣繋浣跨敤

+

杩欐槸涓涓泦鎴愪簡鏅鸿兘楗鎺ㄨ崘鍜岃儗璇垫帓搴忓姛鑳界殑缃戦〉搴旂敤銆

+ +
+
+
馃幆
+

鑳岃鎺掑簭

+

杈撳叆浣犵殑鑳岃鍐呭锛岀郴缁熶細鑷姩璇嗗埆鐭ヨ瘑鐐瑰苟闅忔満鎺掑簭锛屽府鍔╀綘鏇村ソ鍦板涔犮

+ 寮濮嬩娇鐢 +
+
+
+
+ + +
+ + diff --git a/templates/recitation.html b/templates/recitation.html new file mode 100644 index 0000000..b79729e --- /dev/null +++ b/templates/recitation.html @@ -0,0 +1,72 @@ +锘 + + + + + 鑳岃鎺掑簭 - 杞洏鎶借儗 + + + + +
+
+

馃幆 鑳岃鎺掑簭宸ュ叿

+

闅忔満鎶借儗锛岄珮鏁堝涔

+
+ + + +
+
+ +
+

绗竴姝ワ細杈撳叆鑳岃鍐呭

+

璇风矘璐村寘鍚煡璇嗙偣鍒楄〃鐨勬枃鏈紙鏀寔浠庤〃鏍笺佸垪琛ㄧ瓑褰㈠紡涓嚜鍔ㄨ瘑鍒級

+ + +
+ + + + + + + + + +
+
+ + +
+ + + + diff --git a/web_app.py b/web_app.py new file mode 100644 index 0000000..4e2ea0b --- /dev/null +++ b/web_app.py @@ -0,0 +1,205 @@ +锘# -*- coding: utf-8 -*- +""" +缃戦〉绔簲鐢 - 涓у寲楗鎺ㄨ崘鍔╂墜 + 鑳岃鎺掑簭鍔熻兘 +""" + +from flask import Flask, render_template, request, jsonify +import re +import random +import logging +from pathlib import Path + +# 閰嶇疆鏃ュ織 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('logs/web_app.log', encoding='utf-8'), + logging.StreamHandler() + ] +) + +logger = logging.getLogger(__name__) + +app = Flask(__name__) +app.config['SECRET_KEY'] = 'your-secret-key-here' +# 纭繚妯℃澘鏂囦欢浣跨敤UTF-8缂栫爜璇诲彇 +app.jinja_env.auto_reload = True +app.config['TEMPLATES_AUTO_RELOAD'] = True + + +class RecitationSorter: + """鑳岃鎺掑簭鍣""" + + def __init__(self): + self.items = [] + + def extract_items(self, text): + """浠庢枃鏈腑鎻愬彇鑳岃椤圭洰""" + items = [] + + # 鏂规硶1: 鎸夎鍒嗗壊锛岃繃婊ょ┖琛屽拰鏃犲叧琛 + lines = text.strip().split('\n') + for line in lines: + line = line.strip() + # 璺宠繃绌鸿 + if not line: + continue + + # 璺宠繃鏄庢樉鐨勮〃澶磋锛堝寘鍚"绔犺妭"銆"鐭ヨ瘑鐐"绛夛級 + if any(keyword in line for keyword in ['绔犺妭', '鐭ヨ瘑鐐', '閫夋嫨棰', '涓昏棰', '瀹屾垚', '鍒']): + continue + + # 璺宠繃椤电爜琛 + if re.match(r'^绗琝d+椤', line) or re.match(r'^鍏盶d+椤', line): + continue + + # 璺宠繃璇存槑鏂囧瓧 + if any(keyword in line for keyword in ['浣跨敤璇存槑', '绁:', '鍑▼', '妗嗘灦', '鐞嗚В', '鑳岃']): + continue + + # 鎻愬彇鐭ヨ瘑鐐圭殑鍑犵妯″紡 + # 妯″紡1: 浠ユ暟瀛楁垨瀛楁瘝寮澶达紙濡"1. 鐭ヨ瘑鐐"鎴"绗竴绔 鍐呭"锛 + match = re.match(r'^[绗琞?[涓浜屼笁鍥涗簲鍏竷鍏節鍗乗d]+[绔犺妭]?\s*[锛:銆乚?\s*(.+)', line) + if match: + item = match.group(1).strip() + if item and len(item) > 1: # 鑷冲皯2涓瓧绗︽墠璁や负鏄湁鏁堢煡璇嗙偣 + items.append(item) + continue + + # 妯″紡2: 浠"-"鎴"鈥"寮澶寸殑鍒楄〃椤 + match = re.match(r'^[-鈥\s*(.+)', line) + if match: + item = match.group(1).strip() + if item and len(item) > 1: + items.append(item) + continue + + # 妯″紡3: 琛ㄦ牸涓殑鐭ヨ瘑鐐癸紙閫氬父涓嶅寘鍚壒娈婃爣璁扮锛 + # 濡傛灉琛屼腑鍖呭惈甯歌鐨勪腑鏂囨爣鐐癸紝浣嗕笉鍖呭惈琛ㄦ牸鏍囪绗︼紝鍙兘鏄煡璇嗙偣 + if len(line) > 2 and not re.match(r'^[鉁撁椻垰鉁梊s]+$', line): + # 妫鏌ユ槸鍚﹀寘鍚父瑙佺殑涓枃鍐呭 + if re.search(r'[\u4e00-\u9fff]', line): # 鍖呭惈涓枃 + # 鎺掗櫎鏄庢樉鐨勮〃鏍煎垎闅旂 + if not re.match(r'^[|+\-\s]+$', line): + items.append(line) + + # 鍘婚噸 + unique_items = [] + seen = set() + for item in items: + # 鏍囧噯鍖栵細鍘婚櫎棣栧熬绌烘牸锛岀粺涓鏍囩偣 + normalized = item.strip() + if normalized and normalized not in seen: + seen.add(normalized) + unique_items.append(normalized) + + return unique_items + + def random_sort(self, items): + """闅忔満鎺掑簭椤圭洰""" + shuffled = items.copy() + random.shuffle(shuffled) + return shuffled + + +# 鍒涘缓鍏ㄥ眬鎺掑簭鍣ㄥ疄渚 +sorter = RecitationSorter() + + +@app.route('/') +def index(): + """棣栭〉""" + return render_template('index.html') + + +@app.route('/recitation') +def recitation(): + """鑳岃鎺掑簭椤甸潰""" + return render_template('recitation.html') + + +@app.route('/api/extract', methods=['POST']) +def extract_items(): + """鎻愬彇鑳岃椤圭洰API""" + try: + data = request.get_json() + text = data.get('text', '') + + if not text: + return jsonify({ + 'success': False, + 'message': '璇疯緭鍏ヨ澶勭悊鐨勬枃鏈' + }), 400 + + # 鎻愬彇椤圭洰 + items = sorter.extract_items(text) + + if not items: + return jsonify({ + 'success': False, + 'message': '鏈兘璇嗗埆鍒拌儗璇靛唴瀹癸紝璇锋鏌ユ枃鏈牸寮' + }), 400 + + logger.info(f"鎻愬彇鍒 {len(items)} 涓儗璇甸」鐩") + + return jsonify({ + 'success': True, + 'items': items, + 'count': len(items) + }) + + except Exception as e: + logger.error(f"鎻愬彇椤圭洰澶辫触: {e}") + return jsonify({ + 'success': False, + 'message': f'澶勭悊澶辫触: {str(e)}' + }), 500 + + +@app.route('/api/sort', methods=['POST']) +def sort_items(): + """闅忔満鎺掑簭API""" + try: + data = request.get_json() + items = data.get('items', []) + + if not items: + return jsonify({ + 'success': False, + 'message': '璇峰厛鎻愬彇鑳岃椤圭洰' + }), 400 + + # 闅忔満鎺掑簭 + sorted_items = sorter.random_sort(items) + + logger.info(f"瀵 {len(sorted_items)} 涓」鐩繘琛岄殢鏈烘帓搴") + + return jsonify({ + 'success': True, + 'items': sorted_items, + 'count': len(sorted_items) + }) + + except Exception as e: + logger.error(f"鎺掑簭澶辫触: {e}") + return jsonify({ + 'success': False, + 'message': f'鎺掑簭澶辫触: {str(e)}' + }), 500 + + +@app.route('/health') +def health(): + """鍋ュ悍妫鏌""" + return jsonify({'status': 'ok'}) + + +if __name__ == '__main__': + # 鍒涘缓蹇呰鐨勭洰褰 + Path('templates').mkdir(exist_ok=True) + Path('static').mkdir(exist_ok=True) + Path('logs').mkdir(exist_ok=True) + + # 鍚姩搴旂敤 + app.run(debug=True, host='0.0.0.0', port=5000)