├── LICENSE ├── README.md ├── app.py ├── requirements.txt ├── screen.png ├── screen2.png ├── static ├── browse.js ├── script.js └── style.css └── templates ├── browse.html └── index.html /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Justin Gu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 智能短视频策划系统 - AI Video Script Generator 2 | 3 | [![Python](https://img.shields.io/badge/Python-3.11%2B-blue)](https://www.python.org/) 4 | [![Flask](https://img.shields.io/badge/Flask-2.0%2B-lightgrey)](https://flask.palletsprojects.com/) 5 | [![License](https://img.shields.io/badge/License-MIT-green)](LICENSE) 6 | 7 | 一个基于ollama DeepSeek-R1的AI的智能短视频脚本生成系统,集成内容创作、结构化存储与方案浏览功能,助力高效视频内容生产。 8 | 9 | ![系统界面截图](screen2.png) 10 | ![系统界面截图](screen.png) 11 | 12 | ## 🌟 核心功能 13 | 14 | - **智能生成**:集成DeepSeek大模型,自动生成标题/脚本/分镜 15 | - **结构化存储**:MySQL数据库持久化保存方案 16 | - **可视化浏览**:双栏式历史方案检视界面 17 | - **交互设计**:动态加载提示与实时反馈 18 | - **可扩展架构**:模块化设计便于功能扩展 19 | 20 | ## 🛠️ 技术栈 21 | 22 | | 组件 | 技术选型 | 23 | |---------------|--------------------------| 24 | | 后端框架 | Python Flask | 25 | | AI集成 | Ollama + DeepSeek-r1:32b | 26 | | 数据库 | MySQL 8.0+ | 27 | | 前端 | HTML5/CSS3/ES6 | 28 | 29 | ## 🚀 快速开始 30 | 31 | ### 环境要求 32 | 33 | - Python 3.11+ 34 | - MySQL 8.0+ 35 | - Ollama服务+DeepSeek R1 32B+(本地运行,强烈建议使用DeepSeek R1 32B以上版本) 36 | or 37 | - OpenAI兼容方式的API方式调用(可自行在代码中修改) 38 | 39 | 40 | ### 安装步骤 41 | 42 | 1. **克隆仓库** 43 | ```bash 44 | git clone https://github.com/handsomejustin/ai-video-script-generator.git 45 | cd ai-video-script-generator 46 | ``` 47 | 48 | 2. **安装依赖** 49 | ```bash 50 | pip install -r requirements.txt 51 | ``` 52 | 53 | 3. **配置ollama** 54 | ``` 55 | C:\Users\abc>ollama list 56 | NAME ID SIZE MODIFIED 57 | openthinker:32b b3f4e577e166 19 GB 11 days ago 58 | deepseek-r1:32b 38056bbcbb2d 19 GB 5 weeks ago 59 | ``` 60 | 确认你已正确安装了ollama,同时已pull了deepseek-r1:32b(显卡需要nVdia RTX4090 24G) 61 | 如尚未安装,可以使用命令: 62 | ``` 63 | C:\Users\abc>ollama pull deepseek-r1:32b 64 | ``` 65 | 66 | 4. **数据库配置** 67 | ```sql 68 | CREATE DATABASE cehua; 69 | USE cehua; 70 | 71 | CREATE TABLE todo_list ( 72 | id INT AUTO_INCREMENT PRIMARY KEY, 73 | title VARCHAR(255) NOT NULL, 74 | opening TEXT NOT NULL, 75 | script TEXT NOT NULL, 76 | direction TEXT NOT NULL, 77 | dialogue TEXT NOT NULL, 78 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 79 | ); 80 | ``` 81 | 82 | 5. **配置环境变量** 83 | 在app.py中修改: 84 | ``` 85 | # 数据库配置 86 | db_config = { 87 | 'host': 'localhost', 88 | 'user': 'root', 89 | 'password': 'yourpassword', 90 | 'database': 'cehua', 91 | 'port': 3306, 92 | 'charset': 'utf8mb4' 93 | } 94 | ``` 95 | 96 | 6. **启动服务** 97 | ```bash 98 | python app.py 99 | ``` 100 | 101 | 7. **访问系统** 102 | 打开浏览器访问:`http://localhost:5000` 103 | 104 | ## 🖥️ 使用指南 105 | 106 | ### 内容生成流程 107 | 1. 在首页输入需求描述和金点子 108 | 2. 点击「生成策划方案」获取AI原始响应 109 | 3. 逐个点击各字段的「提取」按钮结构化数据 110 | 4. 确认无误后保存到数据库 111 | 112 | ### 方案浏览模式 113 | 1. 访问 `/browse` 进入浏览界面 114 | 2. 左侧选择历史方案 115 | 3. 右侧查看完整细节 116 | 4. 支持分页导航(每页20条) 117 | 118 | ## ⚙️ 配置说明 119 | 120 | ### 模型参数调整 121 | 修改 `app.py` 中的prompt模板: 122 | ```python 123 | # 示例生成模板 124 | prompt = f"""为毛利哥这个财经IP,策划财经方面的短视频《毛利哥的财经小课堂》,根据给的金点子内容,生成短视频所需要所有脚本。 125 | 要求为: 126 | ========= 127 | 【标题】:要求抓人眼球,看了就想点视频 128 | 【精彩前五秒】:前5秒如何抓人眼球? 129 | 【脚本】该视频的脚本、导演方式、演员台词。 130 | ========= 131 | 金点子为:{request.form['idea']}""" 132 | """ 133 | ``` 134 | 135 | ### 界面自定义 136 | 1. 修改 `static/style.css` 调整视觉样式 137 | 2. 编辑 `templates/` 下的HTML文件变更布局 138 | 3. 调整 `static/script.js` 实现交互逻辑 139 | 140 | ## 🤝 参与贡献 141 | 142 | 我们欢迎各种形式的贡献: 143 | 1. Fork项目并提交Pull Request 144 | 2. 提交Issue报告问题或建议 145 | 3. 完善文档或翻译 146 | 4. 测试并反馈使用体验 147 | 148 | 请遵循 [贡献指南](CONTRIBUTING.md) 进行操作。 149 | 150 | ## 📄 开源协议 151 | 152 | 本项目采用 [MIT License](LICENSE) 开源协议。 153 | 154 | ## Star History 155 | 156 | [![Star History Chart](https://api.star-history.com/svg?repos=handsomejustin/ai-video-script-generator&type=Date)](https://star-history.com/#handsomejustin/ai-video-script-generator&Date) 157 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request, jsonify 2 | from pymysql.cursors import DictCursor # 确保导入DictCursor 3 | import logging 4 | import pymysql 5 | import requests 6 | import json 7 | import re 8 | from flask_limiter import Limiter 9 | from flask_limiter.util import get_remote_address 10 | 11 | app = Flask(__name__) 12 | # 初始化速率限制 13 | limiter = Limiter(app=app, key_func=get_remote_address) 14 | 15 | # 数据库配置 16 | db_config = { 17 | 'host': 'localhost', 18 | 'user': 'root', 19 | 'password': 'yourpassword', 20 | 'database': 'cehua', 21 | 'port': 3306, 22 | 'charset': 'utf8mb4' 23 | } 24 | 25 | def remove_think_tags(text): 26 | """ 27 | 移除文本中所有标签及其内容 28 | 示例: 29 | >>> remove_think_tags('正常文本思考内容') 30 | '正常文本' 31 | >>> remove_think_tags('多内容') 32 | '多签' 33 | """ 34 | return re.sub(r'.*?', '', text, flags=re.DOTALL) 35 | 36 | @app.route('/') 37 | def index(): 38 | return render_template('index.html') 39 | 40 | @app.route('/generate', methods=['POST']) 41 | def generate_script(): 42 | try: 43 | prompt = f"""为毛利哥这个财经IP,策划财经方面的短视频《毛利哥的财经小课堂》,根据给的金点子内容,生成短视频所需要所有脚本。 44 | 要求为: 45 | ========= 46 | 【标题】:要求抓人眼球,看了就想点视频 47 | 【精彩前五秒】:前5秒如何抓人眼球? 48 | 【脚本】该视频的脚本、导演方式、演员台词。 49 | ========= 50 | 金点子为:{request.form['idea']}""" 51 | 52 | response = requests.post( 53 | 'http://localhost:11434/api/generate', 54 | json={ 55 | 'model': 'deepseek-r1:32b', 56 | 'prompt': prompt, 57 | 'stream': False 58 | } 59 | ) 60 | 61 | if response.status_code == 200: 62 | result = json.loads(response.text) 63 | return jsonify({'raw_response': remove_think_tags(result['response'].strip())}) 64 | else: 65 | return jsonify({'error': 'AI生成失败'}), 500 66 | 67 | except Exception as e: 68 | return jsonify({'error': str(e)}), 500 69 | 70 | @app.route('/extract', methods=['POST']) 71 | @limiter.limit("10 per minute") 72 | def extract_field(): 73 | try: 74 | data = request.json 75 | field_type = request.json['field_type'] 76 | raw_text = request.json['raw_text'] 77 | 78 | prompts = { 79 | 'title': "请从以下文本中精确提取【标题】内容,只需返回标题文本不要包含其他内容:\n", 80 | 'opening': "请从以下文本中精确提取【精彩前五秒】内容,只需返回前五秒脚本不要包含其他内容:\n", 81 | 'script': "请从以下文本中精确提取【脚本】正文内容,不要包含导演方式和演员台词:\n", 82 | 'direction': "请从以下文本中精确提取【导演方式】内容,只需返回导演指导说明:\n", 83 | 'dialogue': "请从以下文本中精确提取【演员台词】内容,只需返回对话文本:\n" 84 | } 85 | 86 | response = requests.post( 87 | 'http://localhost:11434/api/generate', 88 | json={ 89 | 'model': 'deepseek-r1:32b', 90 | 'prompt': f"{prompts[field_type]}{raw_text}", 91 | 'stream': False 92 | } 93 | ) 94 | 95 | if response.status_code == 200: 96 | result = json.loads(response.text) 97 | return jsonify({'content': remove_think_tags(result['response'].strip())}) 98 | else: 99 | return jsonify({'error': '字段提取失败'}), 500 100 | 101 | except Exception as e: 102 | return jsonify({'error': str(e)}), 500 103 | 104 | @app.route('/save', methods=['POST']) 105 | def save_to_db(): 106 | try: 107 | conn = pymysql.connect(**db_config) 108 | cursor = conn.cursor() 109 | 110 | sql = """INSERT INTO todo_list 111 | (title, opening, script, direction, dialogue) 112 | VALUES (%s, %s, %s, %s, %s)""" 113 | 114 | cursor.execute(sql, ( 115 | request.form['title'], 116 | request.form['opening'], 117 | request.form['script'], 118 | request.form['direction'], 119 | request.form['dialogue'] 120 | )) 121 | 122 | conn.commit() 123 | return jsonify({'status': 'success'}) 124 | 125 | except Exception as e: 126 | return jsonify({'status': 'error', 'message': str(e)}) 127 | finally: 128 | cursor.close() 129 | conn.close() 130 | 131 | @app.route('/browse') 132 | def browse(): 133 | page = request.args.get('page', 1, type=int) 134 | per_page = 20 135 | 136 | try: 137 | conn = pymysql.connect(**db_config) 138 | with conn.cursor() as cursor: 139 | # 获取总记录数 140 | cursor.execute("SELECT COUNT(*) FROM todo_list") 141 | total = cursor.fetchone()[0] 142 | 143 | # 计算分页 144 | total_pages = (total + per_page - 1) // per_page 145 | offset = (page - 1) * per_page 146 | 147 | # 获取当前页数据 148 | cursor.execute("SELECT id, title FROM todo_list ORDER BY created_at DESC LIMIT %s OFFSET %s", 149 | (per_page, offset)) 150 | items = cursor.fetchall() 151 | 152 | return render_template('browse.html', 153 | items=items, 154 | current_page=page, 155 | total_pages=total_pages) 156 | 157 | except Exception as e: 158 | return str(e), 500 159 | finally: 160 | conn.close() 161 | 162 | @app.route('/get_detail/') 163 | def get_detail(item_id): 164 | conn = None # 显式初始化连接变量 165 | try: 166 | # 记录调试信息 167 | app.logger.debug(f"Attempting to connect to database with config: {db_config}") 168 | 169 | conn = pymysql.connect(**db_config) 170 | 171 | with conn.cursor(DictCursor) as cursor: 172 | app.logger.debug(f"Executing query for item_id: {item_id}") 173 | cursor.execute("SELECT * FROM todo_list WHERE id = %s", (item_id,)) 174 | detail = cursor.fetchone() 175 | 176 | if not detail: 177 | app.logger.warning(f"Item {item_id} not found") 178 | return jsonify({"error": "Item not found"}), 404 179 | 180 | app.logger.debug(f"Successfully retrieved item {item_id}") 181 | return jsonify(detail) 182 | 183 | except pymysql.MySQLError as e: 184 | app.logger.error(f"Database error: {str(e)}") 185 | return jsonify({"error": "Database operation failed"}), 500 186 | except Exception as e: 187 | app.logger.error(f"Unexpected error: {str(e)}", exc_info=True) 188 | return jsonify({"error": "Internal server error"}), 500 189 | finally: 190 | if conn and conn.open: # 安全关闭连接 191 | app.logger.debug("Closing database connection") 192 | conn.close() 193 | 194 | if __name__ == '__main__': 195 | app.run(debug=True) 196 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==3.1.0 2 | flask_limiter==3.10.1 3 | PyMySQL==1.1.1 4 | requests==2.32.3 5 | python-dotenv==0.21.1 6 | Werkzeug==3.1.3 7 | Jinja2==3.1.5 8 | -------------------------------------------------------------------------------- /screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/handsomejustin/ai-video-script-generator/0925a15061d99335d263a94a46ef05a9c14a434c/screen.png -------------------------------------------------------------------------------- /screen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/handsomejustin/ai-video-script-generator/0925a15061d99335d263a94a46ef05a9c14a434c/screen2.png -------------------------------------------------------------------------------- /static/browse.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', () => { 2 | // 点击列表项加载详情 3 | document.querySelectorAll('.list-item').forEach(item => { 4 | item.addEventListener('click', async function() { 5 | // 移除所有激活状态 6 | document.querySelectorAll('.list-item').forEach(i => { 7 | i.style.background = 'rgba(240, 242, 255, 0.3)'; 8 | i.querySelector('.item-title').style.color = 'var(--primary-color)'; 9 | }); 10 | 11 | // 设置当前激活状态 12 | this.style.background = 'var(--accent-color)'; 13 | this.querySelector('.item-title').style.color = 'white'; 14 | 15 | // 显示加载状态 16 | const detailPanel = document.querySelector('.detail-content'); 17 | detailPanel.style.opacity = '0.5'; 18 | 19 | try { 20 | const itemId = this.dataset.id; 21 | const response = await fetch(`/get_detail/${itemId}`); 22 | const data = await response.json(); 23 | 24 | // 填充数据 25 | document.getElementById('detail-title').textContent = data.title; 26 | document.getElementById('detail-opening').textContent = data.opening; 27 | document.getElementById('detail-script').textContent = data.script; 28 | document.getElementById('detail-direction').textContent = data.direction; 29 | document.getElementById('detail-dialogue').textContent = data.dialogue; 30 | 31 | } catch (error) { 32 | alert('获取详情失败: ' + error.message); 33 | } finally { 34 | detailPanel.style.opacity = '1'; 35 | } 36 | }); 37 | }); 38 | 39 | // 预加载第一项 40 | const firstItem = document.querySelector('.list-item'); 41 | if(firstItem) firstItem.click(); 42 | }); -------------------------------------------------------------------------------- /static/script.js: -------------------------------------------------------------------------------- 1 | // 页面加载时恢复数据 2 | document.addEventListener('DOMContentLoaded', () => { 3 | // 恢复生成按钮状态 4 | const savedRaw = localStorage.getItem('rawResponse'); 5 | if (savedRaw) { 6 | document.getElementById('raw-response').value = savedRaw; 7 | } 8 | 9 | // 恢复各个字段 10 | ['title', 'opening', 'script', 'direction', 'dialogue'].forEach(id => { 11 | const savedValue = localStorage.getItem(id); 12 | if (savedValue) { 13 | document.getElementById(id).value = savedValue; 14 | } 15 | }); 16 | }); 17 | 18 | async function generateScript() { 19 | const idea = document.getElementById('idea').value; 20 | 21 | try { 22 | const btn = document.querySelector('.generate-btn'); 23 | btn.disabled = true; 24 | btn.innerHTML = '生成中...'; 25 | 26 | const response = await fetch('/generate', { 27 | method: 'POST', 28 | headers: { 29 | 'Content-Type': 'application/x-www-form-urlencoded', 30 | }, 31 | body: new URLSearchParams({ 32 | 'title_requirements': '要求抓人眼球,看了就想点视频', 33 | 'opening_requirements': '前5秒如何抓人眼球?', 34 | 'idea': idea 35 | }) 36 | }); 37 | 38 | const data = await response.json(); 39 | 40 | if (data.error) { 41 | throw new Error(data.error); 42 | } 43 | 44 | // 保存到本地存储 45 | const cleanResponse = data.raw_response || ''; 46 | document.getElementById('raw-response').value = cleanResponse; 47 | localStorage.setItem('rawResponse', cleanResponse); 48 | 49 | // 清空字段并移除旧存储 50 | ['title', 'opening', 'script', 'direction', 'dialogue'].forEach(id => { 51 | document.getElementById(id).value = ''; 52 | localStorage.removeItem(id); 53 | }); 54 | 55 | } catch (error) { 56 | alert('生成失败: ' + error.message); 57 | } finally { 58 | const btn = document.querySelector('.generate-btn'); 59 | btn.disabled = false; 60 | btn.innerHTML = '🚀 生成策划方案'; 61 | } 62 | } 63 | 64 | async function extractField(fieldType) { 65 | const rawText = document.getElementById('raw-response').value; 66 | const target = document.getElementById(fieldType); 67 | const btn = document.querySelector(`button[onclick="extractField('${fieldType}')"]`); 68 | 69 | if (!rawText) { 70 | alert('请先生成原始内容'); 71 | return; 72 | } 73 | 74 | try { 75 | target.disabled = true; 76 | btn.disabled = true; 77 | btn.classList.add('loading'); 78 | 79 | const response = await fetch('/extract', { 80 | method: 'POST', 81 | headers: { 82 | 'Content-Type': 'application/json', 83 | }, 84 | body: JSON.stringify({ 85 | field_type: fieldType, 86 | raw_text: rawText 87 | }) 88 | }); 89 | 90 | const data = await response.json(); 91 | 92 | if (data.error) { 93 | throw new Error(data.error); 94 | } 95 | 96 | // 保存到本地存储并显示 97 | const cleanContent = data.content || ''; 98 | target.value = cleanContent; 99 | localStorage.setItem(fieldType, cleanContent); 100 | showVisualFeedback(target, true); 101 | 102 | } catch (error) { 103 | showVisualFeedback(target, false); 104 | alert('提取失败: ' + error.message); 105 | } finally { 106 | target.disabled = false; 107 | btn.disabled = false; 108 | btn.classList.remove('loading'); 109 | } 110 | } 111 | 112 | // 新增清除本地存储功能(可选) 113 | function clearStorage() { 114 | localStorage.removeItem('rawResponse'); 115 | ['title', 'opening', 'script', 'direction', 'dialogue'].forEach(id => { 116 | localStorage.removeItem(id); 117 | }); 118 | location.reload(); 119 | } 120 | 121 | function showVisualFeedback(element, isSuccess) { 122 | element.style.borderColor = isSuccess ? '#27ae60' : '#e74c3c'; 123 | element.style.boxShadow = `0 0 0 3px ${isSuccess ? 'rgba(39, 174, 96, 0.1)' : 'rgba(231, 76, 60, 0.1)'}`; 124 | 125 | setTimeout(() => { 126 | element.style.borderColor = '#e0e0e0'; 127 | element.style.boxShadow = 'none'; 128 | }, 1000); 129 | } 130 | 131 | async function saveData() { 132 | const formData = { 133 | title: document.getElementById('title').value, 134 | opening: document.getElementById('opening').value, 135 | script: document.getElementById('script').value, 136 | direction: document.getElementById('direction').value, 137 | dialogue: document.getElementById('dialogue').value 138 | }; 139 | 140 | try { 141 | const btn = document.querySelector('.save-btn'); 142 | btn.disabled = true; 143 | btn.innerHTML = '保存中...'; 144 | 145 | const response = await fetch('/save', { 146 | method: 'POST', 147 | headers: { 148 | 'Content-Type': 'application/x-www-form-urlencoded', 149 | }, 150 | body: new URLSearchParams(formData) 151 | }); 152 | 153 | const result = await response.json(); 154 | if (result.status === 'success') { 155 | alert('保存成功!'); 156 | } else { 157 | throw new Error(result.message); 158 | } 159 | } catch (error) { 160 | alert('保存失败: ' + error.message); 161 | } finally { 162 | const btn = document.querySelector('.save-btn'); 163 | btn.disabled = false; 164 | btn.innerHTML = '💾 保存到数据库'; 165 | } 166 | } 167 | 168 | // JavaScript修改(static/script.js) 169 | const EXTRACTION_ORDER = [ 170 | { id: 'title', name: '标题' }, 171 | { id: 'opening', name: '精彩前五秒' }, 172 | { id: 'script', name: '脚本' }, 173 | { id: 'direction', name: '导演方式' }, 174 | { id: 'dialogue', name: '演员台词' } 175 | ]; 176 | 177 | async function autoExtractAll() { 178 | const rawText = document.getElementById('raw-response').value; 179 | const extractBtn = document.querySelector('.extract-btn'); 180 | 181 | if (!rawText) { 182 | alert('请先生成原始内容'); 183 | return; 184 | } 185 | 186 | try { 187 | // 禁用按钮并显示进度 188 | extractBtn.disabled = true; 189 | extractBtn.innerHTML = '
正在准备...'; 190 | 191 | // 顺序执行提取 192 | for (let i = 0; i < EXTRACTION_ORDER.length; i++) { 193 | const { id, name } = EXTRACTION_ORDER[i]; 194 | const textarea = document.getElementById(id); 195 | 196 | // 更新进度显示 197 | extractBtn.innerHTML = `
正在提取 ${name} (${i+1}/${EXTRACTION_ORDER.length})`; 198 | textarea.placeholder = `正在提取${name}...`; 199 | 200 | try { 201 | const response = await fetch('/extract', { 202 | method: 'POST', 203 | headers: { 'Content-Type': 'application/json' }, 204 | body: JSON.stringify({ 205 | field_type: id, 206 | raw_text: rawText 207 | }) 208 | }); 209 | 210 | const data = await response.json(); 211 | textarea.value = data.content || '内容提取失败'; 212 | textarea.style.borderColor = data.content ? '#27ae60' : '#e74c3c'; 213 | 214 | } catch (error) { 215 | textarea.value = `提取错误: ${error.message}`; 216 | textarea.style.borderColor = '#e74c3c'; 217 | } 218 | } 219 | 220 | } catch (error) { 221 | alert('提取流程异常: ' + error.message); 222 | } finally { 223 | // 恢复按钮状态 224 | extractBtn.disabled = false; 225 | extractBtn.innerHTML = '一键提取全部内容'; 226 | // 2秒后重置边框颜色 227 | setTimeout(() => { 228 | EXTRACTION_ORDER.forEach(({ id }) => { 229 | document.getElementById(id).style.borderColor = '#e0e0e0'; 230 | }); 231 | }, 2000); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /static/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-color: #1a1a1a; 3 | --secondary-color: #2a2a2a; 4 | --accent-color: #00b4d8; 5 | --success-color: #00cc88; 6 | --background: #f8f9ff; 7 | --text-light: #ffffff; 8 | --font-stack: 'Inter', 'Helvetica Neue', system-ui, sans-serif; 9 | } 10 | 11 | body { 12 | font-family: var(--font-stack); 13 | background: linear-gradient(135deg, #f8f9ff 0%, #eef2ff 100%); 14 | margin: 0; 15 | padding: 4rem; 16 | line-height: 1.6; 17 | color: var(--primary-color); 18 | } 19 | 20 | .container { 21 | max-width: 1600px; 22 | margin: 0 auto; 23 | background: rgba(255, 255, 255, 0.95); 24 | padding: 4rem; 25 | border-radius: 2rem; 26 | box-shadow: 0 2rem 6rem rgba(0, 0, 0, 0.08); 27 | backdrop-filter: blur(20px); 28 | } 29 | 30 | .header { 31 | color: var(--primary-color); 32 | text-align: center; 33 | margin: 0 0 4rem; 34 | font-size: 3rem; 35 | font-weight: 700; 36 | letter-spacing: -0.05em; 37 | position: relative; 38 | } 39 | 40 | .header::after { 41 | content: ''; 42 | display: block; 43 | width: 120px; 44 | height: 4px; 45 | background: var(--accent-color); 46 | margin: 2rem auto; 47 | border-radius: 2px; 48 | } 49 | 50 | .input-group { 51 | margin-bottom: 3rem; 52 | } 53 | 54 | label { 55 | display: block; 56 | margin-bottom: 1.2rem; 57 | color: var(--primary-color); 58 | font-weight: 600; 59 | font-size: 1.1rem; 60 | letter-spacing: 0.02em; 61 | } 62 | 63 | textarea, input { 64 | width: 100%; 65 | padding: 1.2rem; 66 | border: 2px solid #e0e3ff; 67 | border-radius: 1rem; 68 | font-family: var(--font-stack); 69 | font-size: 1.1rem; 70 | transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 71 | background: rgba(255, 255, 255, 0.8); 72 | } 73 | 74 | /* 不同字段高度设置 */ 75 | #title { min-height: 60px; } /* 1行 */ 76 | #opening { min-height: 250px; } /* 5行 */ 77 | #script { min-height: 800px; } /* 20行 */ 78 | #direction { min-height: 150px; } /* 5行 */ 79 | #dialogue { min-height: 250px; } /* 5行 */ 80 | 81 | textarea:focus, input:focus { 82 | border-color: var(--accent-color); 83 | box-shadow: 0 0 0 4px rgba(0, 180, 216, 0.15); 84 | background: white; 85 | } 86 | 87 | button { 88 | border: none; 89 | padding: 1rem 2rem; 90 | border-radius: 0.8rem; 91 | cursor: pointer; 92 | font-weight: 600; 93 | transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 94 | display: inline-flex; 95 | align-items: center; 96 | gap: 0.8rem; 97 | font-family: var(--font-stack); 98 | } 99 | 100 | .generate-btn { 101 | background: linear-gradient(135deg, var(--success-color) 0%, #00b894 100%); 102 | color: var(--text-light); 103 | width: 100%; 104 | padding: 1.5rem; 105 | margin: 2rem 0; 106 | font-size: 1.2rem; 107 | box-shadow: 0 1rem 2rem rgba(0, 204, 136, 0.15); 108 | } 109 | 110 | .generate-btn:hover { 111 | transform: translateY(-2px); 112 | box-shadow: 0 1.5rem 3rem rgba(0, 204, 136, 0.25); 113 | } 114 | 115 | .result-card { 116 | margin-top: 3rem; 117 | padding: 3rem; 118 | background: rgba(255, 255, 255, 0.9); 119 | border-radius: 1.5rem; 120 | border: 1px solid rgba(224, 227, 255, 0.5); 121 | } 122 | 123 | .input-wrapper { 124 | display: grid; 125 | grid-template-columns: 1fr auto; 126 | gap: 1.5rem; 127 | align-items: start; 128 | } 129 | 130 | 131 | /* 加载动画样式 */ 132 | .extract-btn.loading { 133 | padding-right: 3.5rem; /* 为加载图标留出空间 */ 134 | } 135 | 136 | .loading::after { 137 | content: ""; 138 | position: absolute; 139 | right: 1rem; 140 | top: 50%; 141 | transform: translateY(-50%); 142 | width: 1.2rem; 143 | height: 1.2rem; 144 | border: 2px solid rgba(255, 255, 255, 0.6); 145 | border-top-color: transparent; 146 | border-radius: 50%; 147 | animation: spin 0.8s linear infinite; 148 | opacity: 0; 149 | transition: opacity 0.3s; 150 | } 151 | 152 | .extract-btn.loading::after { 153 | opacity: 1; 154 | } 155 | 156 | /* 增加加载时文字淡出效果 */ 157 | .extract-btn .btn-text { 158 | transition: opacity 0.3s; 159 | } 160 | 161 | .extract-btn.loading .btn-text { 162 | opacity: 0.5; 163 | } 164 | 165 | .extract-btn:hover { 166 | transform: translateY(-1px); 167 | box-shadow: 0 1rem 2rem rgba(0, 180, 216, 0.2); 168 | } 169 | 170 | .save-btn { 171 | background: linear-gradient(135deg, #6c5ce7 0%, #4b37b0 100%); 172 | color: var(--text-light); 173 | width: 100%; 174 | margin-top: 3rem; 175 | padding: 1.2rem; 176 | font-size: 1.1rem; 177 | } 178 | 179 | .raw-response textarea { 180 | min-height: 200px; 181 | background: rgba(240, 242, 255, 0.5); 182 | } 183 | 184 | .loading::after { 185 | border: 2px solid rgba(255, 255, 255, 0.6); 186 | border-top-color: transparent; 187 | animation: spin 0.8s linear infinite; 188 | } 189 | 190 | @keyframes spin { 191 | to { transform: rotate(360deg); } 192 | } 193 | 194 | /* 新增响应式设计 */ 195 | @media (max-width: 1200px) { 196 | .container { 197 | padding: 3rem; 198 | margin: 2rem; 199 | } 200 | 201 | .header { 202 | font-size: 2.5rem; 203 | } 204 | } 205 | 206 | /* 浏览页专用样式 */ 207 | .browse-container { 208 | display: grid; 209 | grid-template-columns: 320px 1fr; 210 | gap: 2rem; 211 | max-width: 1600px; 212 | margin: 2rem auto; 213 | padding: 2rem; 214 | } 215 | 216 | .browse-sidebar { 217 | background: rgba(255, 255, 255, 0.95); 218 | border-radius: 1.5rem; 219 | padding: 2rem; 220 | box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.05); 221 | height: calc(100vh - 4rem); 222 | display: flex; 223 | flex-direction: column; 224 | } 225 | 226 | .browse-title { 227 | font-size: 1.8rem; 228 | color: var(--primary-color); 229 | margin-bottom: 1.5rem; 230 | padding-bottom: 1rem; 231 | border-bottom: 2px solid var(--accent-color); 232 | } 233 | 234 | .item-list { 235 | flex: 1; 236 | overflow-y: auto; 237 | margin-bottom: 1.5rem; 238 | } 239 | 240 | .list-item { 241 | display: flex; 242 | gap: 1rem; 243 | padding: 1.2rem; 244 | margin-bottom: 0.8rem; 245 | border-radius: 0.8rem; 246 | background: rgba(240, 242, 255, 0.3); 247 | transition: all 0.2s ease; 248 | cursor: pointer; 249 | } 250 | 251 | .list-item:hover { 252 | background: var(--accent-color); 253 | transform: translateX(5px); 254 | } 255 | 256 | .list-item:hover .item-title { 257 | color: white; 258 | } 259 | 260 | .item-index { 261 | width: 24px; 262 | height: 24px; 263 | background: var(--success-color); 264 | border-radius: 50%; 265 | color: white; 266 | display: flex; 267 | align-items: center; 268 | justify-content: center; 269 | flex-shrink: 0; 270 | } 271 | 272 | .item-title { 273 | font-weight: 500; 274 | color: var(--primary-color); 275 | line-height: 1.4; 276 | } 277 | 278 | .pagination { 279 | display: flex; 280 | justify-content: space-between; 281 | align-items: center; 282 | padding-top: 1.5rem; 283 | border-top: 1px solid #eee; 284 | } 285 | 286 | .page-btn { 287 | padding: 0.6rem 1.2rem; 288 | border-radius: 0.6rem; 289 | background: rgba(0, 180, 216, 0.1); 290 | color: var(--accent-color); 291 | transition: all 0.2s ease; 292 | } 293 | 294 | .page-btn:hover { 295 | background: var(--accent-color); 296 | color: white; 297 | } 298 | 299 | .page-info { 300 | color: #666; 301 | font-size: 0.9rem; 302 | } 303 | 304 | /* 右侧详情面板 */ 305 | .detail-panel { 306 | background: rgba(255, 255, 255, 0.95); 307 | border-radius: 1.5rem; 308 | padding: 2rem; 309 | box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.05); 310 | } 311 | 312 | .detail-header { 313 | margin-bottom: 2rem; 314 | padding-bottom: 1.5rem; 315 | border-bottom: 2px solid var(--accent-color); 316 | } 317 | 318 | .detail-header h2 { 319 | font-size: 1.8rem; 320 | color: var(--primary-color); 321 | margin-bottom: 0.5rem; 322 | } 323 | 324 | .detail-meta { 325 | color: #666; 326 | font-size: 0.9rem; 327 | } 328 | 329 | .detail-section { 330 | margin-bottom: 2rem; 331 | } 332 | 333 | .detail-section label { 334 | display: block; 335 | font-weight: 600; 336 | color: var(--accent-color); 337 | margin-bottom: 0.8rem; 338 | font-size: 1.1rem; 339 | } 340 | 341 | .detail-text { 342 | background: rgba(240, 242, 255, 0.3); 343 | padding: 1.5rem; 344 | border-radius: 1rem; 345 | line-height: 1.6; 346 | white-space: pre-wrap; 347 | } 348 | 349 | .browse-link { 350 | display: block; 351 | text-align: center; 352 | margin: 2rem 0; 353 | color: var(--accent-color); 354 | font-weight: 500; 355 | transition: all 0.2s ease; 356 | } 357 | 358 | .browse-link:hover { 359 | transform: translateX(10px); 360 | opacity: 0.8; 361 | } 362 | /* 新增标题高亮样式 */ 363 | .detail-section.highlight { 364 | background: rgba(0, 180, 216, 0.05); 365 | border-radius: 1rem; 366 | padding: 1.5rem; 367 | margin-bottom: 2rem; 368 | border: 2px solid var(--accent-color); 369 | } 370 | 371 | .detail-section.highlight label { 372 | font-size: 1.3rem; 373 | color: var(--accent-color); 374 | margin-bottom: 1rem; 375 | } 376 | 377 | .detail-section.highlight .detail-text { 378 | font-size: 1.4rem; 379 | font-weight: 600; 380 | color: var(--primary-color); 381 | } 382 | 383 | .extract-btn { 384 | position: relative; 385 | background: linear-gradient(135deg, var(--accent-color) 0%, #0096c7 100%); 386 | color: var(--text-light); 387 | padding: 1rem 2rem; 388 | margin: 1rem 0; 389 | width: 100%; 390 | transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 391 | margin-left:40px; 392 | } 393 | 394 | .extract-btn:hover { 395 | transform: translateY(-2px); 396 | box-shadow: 0 1rem 2rem rgba(0, 180, 216, 0.2); 397 | } 398 | 399 | .loader { 400 | display: inline-block; 401 | width: 16px; 402 | height: 16px; 403 | border: 2px solid rgba(255, 255, 255, 0.6); 404 | border-top-color: transparent; 405 | border-radius: 50%; 406 | animation: spin 0.8s linear infinite; 407 | margin-right: 0.5rem; 408 | } 409 | 410 | @keyframes spin { 411 | to { transform: rotate(360deg); } 412 | } 413 | 414 | .field-item textarea { 415 | min-height: 150px; 416 | margin-bottom: 2rem; 417 | transition: border-color 0.3s ease; 418 | } 419 | 420 | #script { 421 | min-height: 400px; 422 | } 423 | 424 | /* 响应式设计 */ 425 | @media (max-width: 1200px) { 426 | .container { 427 | padding: 2rem; 428 | margin: 1rem; 429 | } 430 | 431 | .header { 432 | font-size: 2rem; 433 | } 434 | } 435 | -------------------------------------------------------------------------------- /templates/browse.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 金点子策划浏览 7 | 8 | 9 | 10 |
11 | 12 |
13 |

策划方案列表

14 |
15 | {% for item in items %} 16 |
17 |
{{ (current_page-1)*20 + loop.index }}
18 |
{{ item[1] }}
19 |
20 | {% endfor %} 21 |
22 | 23 | 24 | 35 |
36 | 37 | 38 |
39 |
40 |

策划方案详情

41 |
点击左侧标题查看详情
42 |
43 |
44 |
45 | 46 |
47 |
48 |
49 | 50 |
51 |
52 |
53 | 54 |
55 |
56 |
57 | 58 |
59 |
60 |
61 | 62 |
63 |
64 |
65 |
66 |
67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 智能短视频策划系统 7 | 8 | 9 | 10 |
11 |

📽️ 智能短视频策划系统

12 | 查看历史策划方案 → 13 |
14 | 15 | 16 |
17 | 18 | 19 | 20 |
21 |
22 | 23 | 24 | 25 |
26 | 27 |
28 |
29 | 30 |
31 | 32 |
33 |
34 | 35 |
36 | 37 |
38 | 39 |
40 |
41 | 42 |
43 | 44 |
45 | 46 |
47 |
48 | 49 |
50 | 51 |
52 | 53 |
54 |
55 | 56 |
57 | 58 |
59 | 60 |
61 |
62 |
63 | 64 | 65 |
66 |
67 | 68 | 69 | 70 | 71 | --------------------------------------------------------------------------------