上传文件至 /
This commit is contained in:
parent
bca678cdc7
commit
239f923074
89
README.md
Normal file
89
README.md
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
# AI 写作助手(AI Writing Assistant)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.1 团队成员与贡献
|
||||||
|
|
||||||
|
| 姓名 | 学号 | 主要贡献(具体分工) |
|
||||||
|
|----|----|----------------|
|
||||||
|
| 索梦露 | 2411020218 | (组长)项目整体设计、后端核心逻辑开发、编写 |
|
||||||
|
| 李秀芬 | 2411020130 | Web 前端界面设计、页面样式美化、 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.2 项目简介 & 运行指南
|
||||||
|
|
||||||
|
### 简介
|
||||||
|
|
||||||
|
本项目是一个基于 Python 和大语言模型 API 的 **AI 写作助手系统**,
|
||||||
|
旨在解决学生和内容创作者在日常写作中 **表达不够通顺、反复修改效率低** 的问题。
|
||||||
|
用户只需输入原始文本,即可通过 AI 自动生成更加流畅、自然的改写结果,从而提升写作效率和质量。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 如何运行
|
||||||
|
```bash
|
||||||
|
# 1️⃣ 安装依赖
|
||||||
|
uv sync # 安装依赖
|
||||||
|
# 2️⃣ 配置 API Key
|
||||||
|
#在 .env 中填写你的 API Key,例如:
|
||||||
|
DASHSCOPE_API_KEY=sk-xxxxxxxxxxxxxxxx
|
||||||
|
#3️⃣ 启动项目
|
||||||
|
uv run app.py
|
||||||
|
#启动成功后,在浏览器中访问
|
||||||
|
http://127.0.0.1:5000
|
||||||
|
#即可打开 AI 写作助手网页界面并进行演示
|
||||||
|
```
|
||||||
|
---
|
||||||
|
## 2.3 开发心得
|
||||||
|
### 选题思考:为什么做这个?解决了谁的痛苦?
|
||||||
|
#### 在日常学习和课程作业中,我们经常需要撰写实验报告、课程设计说明或总结性文字。
|
||||||
|
很多时候,并不是没有想法,而是**不知道如何把想法组织成通顺、专业的文字**,
|
||||||
|
往往需要反复修改,耗费大量时间。
|
||||||
|
|
||||||
|
因此,我们选择了“AI 写作助手”作为课程设计题目,希望借助当前大语言模型在自然语言处理方面的能力,
|
||||||
|
为学生和内容创作者提供一个**低门槛、易使用、效果直观**的写作辅助工具,
|
||||||
|
帮助用户提升写作效率,减少无意义的重复修改。
|
||||||
|
|
||||||
|
### AI 协作体验
|
||||||
|
这是我们第一次在完整项目中深度使用 AI 来协助编程和功能设计。
|
||||||
|
|
||||||
|
在开发过程中,AI 在以下方面给予了非常大的帮助:
|
||||||
|
|
||||||
|
1.后端接口逻辑的设计思路
|
||||||
|
|
||||||
|
2.Prompt 的不断优化与改进
|
||||||
|
|
||||||
|
3.前后端交互流程的梳理
|
||||||
|
|
||||||
|
4.常见错误的快速定位与修复
|
||||||
|
|
||||||
|
其中,让人直呼“牛逼”的 Prompt 是:
|
||||||
|
|
||||||
|
“请帮我润色以下文本,使其更加通顺自然,语气正式但不过于生硬。”
|
||||||
|
|
||||||
|
这一 Prompt 能够在多种输入情况下稳定输出高质量文本,极大提升了系统实用性。
|
||||||
|
|
||||||
|
但也并非所有时候都一帆风顺。有时由于 Prompt 描述不够明确,
|
||||||
|
AI 会输出偏离预期的内容,或者在代码细节上出现不符合实际环境的问题,
|
||||||
|
这时就需要人工不断尝试、调整和验证,甚至“推翻重来”,这一过程也让我们更加理解了
|
||||||
|
**“如何正确地向 AI 提问”本身就是一项重要能力。**
|
||||||
|
|
||||||
|
### 自我反思:AI 时代,程序员的核心竞争力是什么?
|
||||||
|
通过本次课程设计,我们逐渐意识到:
|
||||||
|
|
||||||
|
在 AI 时代,程序员的核心竞争力并不是死记硬背语法,而是:
|
||||||
|
|
||||||
|
问题拆解能力 —— 能否把一个模糊需求拆解成清晰的模块
|
||||||
|
|
||||||
|
工程思维 —— 是否具备代码结构、项目规范和安全意识
|
||||||
|
|
||||||
|
Prompt 设计能力 —— 是否能高效地与 AI 协作
|
||||||
|
|
||||||
|
判断与验证能力 —— 是否能判断 AI 给出的结果是否合理、可用
|
||||||
|
|
||||||
|
AI 并不会取代程序员,但会放大程序员之间的差距。
|
||||||
|
只有真正理解需求、善于思考并具备持续学习能力的人,
|
||||||
|
才能在 AI 时代持续保持竞争力。
|
||||||
|
|
||||||
|
本次项目不仅锻炼了我们的 Python 编程能力,也让我们对 AI 技术的实际应用有了更加清晰和理性的认识。
|
||||||
196
app.py
Normal file
196
app.py
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
import os
|
||||||
|
from flask import Flask, render_template, request, jsonify
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import dashscope
|
||||||
|
from dashscope import Generation
|
||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
dashscope.api_key = os.getenv("DASHSCOPE_API_KEY")
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# 配置
|
||||||
|
# --------------------------
|
||||||
|
SCENE_PROMPTS = {
|
||||||
|
"general": "这是通用写作场景,语言自然清晰。",
|
||||||
|
"media": "这是自媒体写作,语言有吸引力,适合传播。",
|
||||||
|
"script": "这是剧本写作,语言有画面感和情绪张力。",
|
||||||
|
"essay": "这是学生作文写作,结构清晰、表达规范。",
|
||||||
|
"academic_scene": "这是学术写作,语言严谨、逻辑清楚。"
|
||||||
|
}
|
||||||
|
|
||||||
|
POLISH_STYLES = {
|
||||||
|
"formal": "正式、书面",
|
||||||
|
"literary": "文学性强、有文采",
|
||||||
|
"spoken": "口语化、自然",
|
||||||
|
"business": "商务、专业",
|
||||||
|
"internet": "网络风格、轻松"
|
||||||
|
}
|
||||||
|
|
||||||
|
history_records = []
|
||||||
|
MAX_HISTORY = 10
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# AI 调用
|
||||||
|
# --------------------------
|
||||||
|
def call_ai(prompt):
|
||||||
|
resp = Generation.call(model="qwen-turbo", prompt=prompt)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
return resp.output.text
|
||||||
|
return "❌ AI 调用失败"
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# 多版本润色
|
||||||
|
# --------------------------
|
||||||
|
def multi_version_rewrite(text, style):
|
||||||
|
prompt = f"""
|
||||||
|
请对以下文本进行润色,生成 3 个不同版本:
|
||||||
|
要求:
|
||||||
|
1. 保持原意
|
||||||
|
2. 风格:{style}
|
||||||
|
3. 输出 3 个版本,分别标记 版本1/版本2/版本3
|
||||||
|
原文:
|
||||||
|
{text}
|
||||||
|
"""
|
||||||
|
return call_ai(prompt)
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# 扩写
|
||||||
|
# --------------------------
|
||||||
|
def expand_text(text, target_length):
|
||||||
|
"""
|
||||||
|
扩写文本到指定长度(字数/字符)
|
||||||
|
"""
|
||||||
|
prompt = f"""
|
||||||
|
请将以下文本扩写,保持原意,但丰富细节和表达,使字数达到 {target_length} 字左右。
|
||||||
|
可以生成 3 个不同版本,分别标记 版本1/版本2/版本3。
|
||||||
|
原文:
|
||||||
|
{text}
|
||||||
|
"""
|
||||||
|
return call_ai(prompt)
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# 修改原因生成
|
||||||
|
# --------------------------
|
||||||
|
def generate_revision_reason(original, revised):
|
||||||
|
prompt = f"""
|
||||||
|
请对下面两段文本进行对比,说明修改的原因。
|
||||||
|
仅输出文字说明,不要高亮或格式化。
|
||||||
|
|
||||||
|
原文:
|
||||||
|
{original}
|
||||||
|
|
||||||
|
改写后:
|
||||||
|
{revised}
|
||||||
|
|
||||||
|
请详细说明改动原因。
|
||||||
|
"""
|
||||||
|
return call_ai(prompt)
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# 文学参考
|
||||||
|
# --------------------------
|
||||||
|
def generate_literary_reference(text):
|
||||||
|
prompt = f"""
|
||||||
|
请理解下面这句话的含义,然后给出 3 条
|
||||||
|
“文学作品风格中的相似表达示例”。
|
||||||
|
|
||||||
|
要求:
|
||||||
|
1. 不是原文引用
|
||||||
|
2. 给出【风格参考作者】和【作品名】
|
||||||
|
3. 仅作为文学风格参考
|
||||||
|
|
||||||
|
输出格式:
|
||||||
|
1. 句子内容
|
||||||
|
—— 风格参考:作者《作品名》
|
||||||
|
|
||||||
|
原句:
|
||||||
|
{text}
|
||||||
|
"""
|
||||||
|
return call_ai(prompt)
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# 历史记录
|
||||||
|
# --------------------------
|
||||||
|
def save_history(data, result):
|
||||||
|
history_records.insert(0, {
|
||||||
|
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"mode": data.get("mode"),
|
||||||
|
"scene": data.get("scene"),
|
||||||
|
"input": data.get("text", "")[:100],
|
||||||
|
"output": result[:200]
|
||||||
|
})
|
||||||
|
if len(history_records) > MAX_HISTORY:
|
||||||
|
history_records.pop()
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# 情感分析
|
||||||
|
# --------------------------
|
||||||
|
def sentiment_analysis(text):
|
||||||
|
prompt = f"""
|
||||||
|
请分析下面文本的情感倾向,仅返回严格 JSON 格式:
|
||||||
|
{{"positive": %, "neutral": %, "negative": %}}
|
||||||
|
文本:
|
||||||
|
{text}
|
||||||
|
"""
|
||||||
|
result = call_ai(prompt)
|
||||||
|
try:
|
||||||
|
parsed = json.loads(result.replace(":", ":").replace("%",""))
|
||||||
|
return parsed
|
||||||
|
except:
|
||||||
|
return {"positive":0, "neutral":0, "negative":0}
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# 路由
|
||||||
|
# --------------------------
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
return render_template("index.html")
|
||||||
|
|
||||||
|
@app.route("/rewrite", methods=["POST"])
|
||||||
|
def rewrite():
|
||||||
|
data = request.json
|
||||||
|
text = data.get("text","")
|
||||||
|
mode = data.get("mode")
|
||||||
|
scene = data.get("scene","general")
|
||||||
|
|
||||||
|
# 根据模式生成文本
|
||||||
|
if mode=="polish":
|
||||||
|
style = data.get("style","formal")
|
||||||
|
result = multi_version_rewrite(text, style)
|
||||||
|
elif mode=="expand":
|
||||||
|
target_length = data.get("target_length", 300)
|
||||||
|
result = expand_text(text, target_length)
|
||||||
|
else:
|
||||||
|
result = f"普通生成模式:{text}" # 保留原功能
|
||||||
|
|
||||||
|
save_history(data, result)
|
||||||
|
return jsonify({"result": result})
|
||||||
|
|
||||||
|
@app.route("/literary", methods=["POST"])
|
||||||
|
def literary():
|
||||||
|
text = request.json.get("text","")
|
||||||
|
return jsonify({"reference": generate_literary_reference(text)})
|
||||||
|
|
||||||
|
@app.route("/history")
|
||||||
|
def history():
|
||||||
|
return jsonify(history_records)
|
||||||
|
|
||||||
|
@app.route("/revision_reason", methods=["POST"])
|
||||||
|
def revision_reason():
|
||||||
|
data = request.json
|
||||||
|
original = data.get("original","")
|
||||||
|
revised = data.get("revised","")
|
||||||
|
reason = generate_revision_reason(original, revised)
|
||||||
|
return jsonify({"reason": reason})
|
||||||
|
|
||||||
|
@app.route("/sentiment", methods=["POST"])
|
||||||
|
def sentiment():
|
||||||
|
text = request.json.get("text","")
|
||||||
|
return jsonify({"result": sentiment_analysis(text)})
|
||||||
|
|
||||||
|
if __name__=="__main__":
|
||||||
|
app.run(debug=True)
|
||||||
174
index.html
Normal file
174
index.html
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>AI 写作助手 Pro</title>
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app">
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<h1>🌙 AI 写作助手 Pro</h1>
|
||||||
|
<div class="time" id="time">当前时间:<span id="currentTime"></span></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<textarea id="inputText" placeholder="请输入文本..."></textarea>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<select id="scene">
|
||||||
|
<option value="general">通用写作</option>
|
||||||
|
<option value="media">自媒体</option>
|
||||||
|
<option value="script">剧本</option>
|
||||||
|
<option value="essay">作文</option>
|
||||||
|
<option value="academic_scene">学术</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="mode" onchange="updateOptions()">
|
||||||
|
<option value="polish">润色</option>
|
||||||
|
<option value="expand">扩写</option>
|
||||||
|
<option value="summary">总结</option>
|
||||||
|
<option value="simple">简化</option>
|
||||||
|
<option value="academic">学术改写</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="extraOptions" class="extra-options"></div>
|
||||||
|
<button onclick="rewriteText()">🚀 生成</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>✍️ AI 输出(点击选择版本)</h2>
|
||||||
|
<div id="versions"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>🔍 修改原因</h2>
|
||||||
|
<div id="revisionReason"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>📚 文学参考</h2>
|
||||||
|
<div id="literaryBox" class="literary-box"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>😊 情感分析</h2>
|
||||||
|
<button onclick="analyzeSentiment()">分析情感</button>
|
||||||
|
<pre id="sentimentBox"></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function updateOptions(){
|
||||||
|
const mode=document.getElementById("mode").value;
|
||||||
|
const box=document.getElementById("extraOptions");
|
||||||
|
box.innerHTML="";
|
||||||
|
if(mode==="polish"){
|
||||||
|
box.innerHTML=`<label>风格:</label>
|
||||||
|
<select id="style">
|
||||||
|
<option value="formal">正式</option>
|
||||||
|
<option value="literary">文学</option>
|
||||||
|
<option value="spoken">口语</option>
|
||||||
|
<option value="business">商务</option>
|
||||||
|
<option value="internet">网络</option>
|
||||||
|
</select>`;
|
||||||
|
}
|
||||||
|
if(mode==="expand"){
|
||||||
|
box.innerHTML=`<label>目标字数:</label><input type="number" id="target_length" value="300">`;
|
||||||
|
}
|
||||||
|
if(mode==="summary" || mode==="simple"){
|
||||||
|
box.innerHTML=`<label>不超过字数:</label><input type="number" id="max_length" value="100">`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateOptions();
|
||||||
|
|
||||||
|
// -------------------
|
||||||
|
// 时间显示
|
||||||
|
// -------------------
|
||||||
|
function updateTime(){
|
||||||
|
const now=new Date();
|
||||||
|
document.getElementById("currentTime").innerText=now.toLocaleTimeString();
|
||||||
|
}
|
||||||
|
setInterval(updateTime,1000);
|
||||||
|
|
||||||
|
// -------------------
|
||||||
|
// 生成文本 + 多版本
|
||||||
|
// -------------------
|
||||||
|
let lastInput="";
|
||||||
|
|
||||||
|
function rewriteText(){
|
||||||
|
lastInput=document.getElementById("inputText").value;
|
||||||
|
const data={
|
||||||
|
text:lastInput,
|
||||||
|
mode:document.getElementById("mode").value,
|
||||||
|
scene:document.getElementById("scene").value
|
||||||
|
};
|
||||||
|
if(data.mode==="polish") data.style=document.getElementById("style").value;
|
||||||
|
if(data.mode==="expand") data.target_length=document.getElementById("target_length").value;
|
||||||
|
|
||||||
|
fetch("/rewrite",{
|
||||||
|
method:"POST",
|
||||||
|
headers:{"Content-Type":"application/json"},
|
||||||
|
body:JSON.stringify(data)
|
||||||
|
}).then(r=>r.json()).then(d=>{
|
||||||
|
const vbox=document.getElementById("versions");
|
||||||
|
vbox.innerHTML="";
|
||||||
|
const lines=d.result.split("\n");
|
||||||
|
lines.forEach((line)=>{
|
||||||
|
if(line.trim().length>0){
|
||||||
|
const div=document.createElement("div");
|
||||||
|
div.className="version";
|
||||||
|
div.innerText=line;
|
||||||
|
div.onclick=()=>{selectVersion(line)};
|
||||||
|
vbox.appendChild(div);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 文学参考
|
||||||
|
fetch("/literary",{
|
||||||
|
method:"POST",
|
||||||
|
headers:{"Content-Type":"application/json"},
|
||||||
|
body:JSON.stringify({text:lastInput})
|
||||||
|
}).then(r=>r.json()).then(d=>{
|
||||||
|
document.getElementById("literaryBox").innerText=d.reference;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------
|
||||||
|
// 选择版本继续优化 + 显示修改原因
|
||||||
|
// -------------------
|
||||||
|
function selectVersion(text){
|
||||||
|
fetch("/revision_reason",{
|
||||||
|
method:"POST",
|
||||||
|
headers:{"Content-Type":"application/json"},
|
||||||
|
body:JSON.stringify({original:lastInput,revised:text})
|
||||||
|
}).then(r=>r.json()).then(d=>{
|
||||||
|
document.getElementById("revisionReason").innerText=d.reason;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("inputText").value=text;
|
||||||
|
lastInput=text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------
|
||||||
|
// 情感分析
|
||||||
|
// -------------------
|
||||||
|
function analyzeSentiment(){
|
||||||
|
const text=document.getElementById("inputText").value;
|
||||||
|
fetch("/sentiment",{
|
||||||
|
method:"POST",
|
||||||
|
headers:{"Content-Type":"application/json"},
|
||||||
|
body:JSON.stringify({text:text})
|
||||||
|
}).then(r=>r.json()).then(d=>{
|
||||||
|
const s=d.result;
|
||||||
|
document.getElementById("sentimentBox").innerText=
|
||||||
|
`积极: ${s.positive}%\n中立: ${s.neutral}%\n消极: ${s.negative}%`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
requirements.txt
Normal file
BIN
requirements.txt
Normal file
Binary file not shown.
22
style.css
Normal file
22
style.css
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
body {
|
||||||
|
margin:0;
|
||||||
|
padding:0;
|
||||||
|
font-family:"Segoe UI",Tahoma,Geneva,Verdana,sans-serif;
|
||||||
|
background-color:#1e1e2f;
|
||||||
|
color:#e0e0e0;
|
||||||
|
}
|
||||||
|
.app{max-width:900px;margin:20px auto;padding:20px;}
|
||||||
|
header{text-align:center;margin-bottom:20px;}
|
||||||
|
header h1{font-size:2em;color:#fff;margin-bottom:5px;}
|
||||||
|
header .time{font-size:0.9em;color:#a0a0a0;}
|
||||||
|
.card{background-color:#2a2a40;padding:20px;border-radius:12px;margin-bottom:20px;box-shadow:0 5px 15px rgba(0,0,0,0.4);}
|
||||||
|
textarea{width:100%;min-height:120px;border-radius:8px;border:1px solid #444;background-color:#1f1f30;color:#e0e0e0;padding:10px;font-size:1em;resize:vertical;}
|
||||||
|
textarea:focus{outline:none;border-color:#5a5aff;}
|
||||||
|
select{border-radius:8px;border:1px solid #555;background-color:#1f1f30;color:#e0e0e0;padding:6px 10px;font-size:1em;margin-right:10px;}
|
||||||
|
select:focus{outline:none;border-color:#5a5aff;}
|
||||||
|
input[type="number"]{border-radius:8px;border:1px solid #555;background-color:#1f1f30;color:#e0e0e0;padding:5px 8px;font-size:1em;width:80px;}
|
||||||
|
button{border:none;border-radius:8px;padding:8px 16px;font-size:1em;cursor:pointer;background:linear-gradient(90deg,#5a5aff,#00d4ff);color:#fff;transition:all 0.3s ease;margin-top:10px;}
|
||||||
|
button:hover{transform:translateY(-2px);box-shadow:0 5px 10px rgba(0,0,0,0.4);}
|
||||||
|
.version{padding:8px;border-radius:6px;background-color:#3a3a55;margin-bottom:5px;cursor:pointer;transition:all 0.2s ease;}
|
||||||
|
.version:hover{background-color:#5a5aff;color:#fff;}
|
||||||
|
#revisionReason,#literaryBox,#sentimentBox{background-color:#1f1f30;padding:10px;border-radius:8px;font-size:0.95em;line-height:1.5em;}
|
||||||
Loading…
Reference in New Issue
Block a user