软件工程第一次作业
软件工程第一次作业
| 这个作业属于哪个课程 | 202601 软件工程 |
|---|---|
| 这个作业要求在哪里 | 第一次作业要求 |
| 这个作业的目标 | 完成 GitHub、博客园与 Hugging Face 账号准备;基于 Flask 后端 + 前端接口调用 Flux(XLabs-AI/flux-RealismLora)模型实现交互式图像生成;搭建 GitHub 个人主页;梳理技能树与未来规划并完成博客随笔 |
| 学号 | 102401135 |
一、准备工作
1.1 GitHub 账号
- GitHub 账号:
Victoria029,已注册并完善头像、昵称、个人资料等各项信息。

1.2 博客园账号
- 博客园账号:Victoria_L,已完成注册并申请开通博客(注意:注册账号不等于开通博客,需要另行申请开通),审核通过后正式开通。
- 已将博客编辑器切换为 Markdown 编辑器,并完成实名制。
1.3 关注老师与助教博客
已关注任课老师与助教的博客:
- 任课老师:吴越钟
- 助教:张明圣、王奇蕊
1.4 加入班级博客
已加入博客园班级(202601 软件工程),并进入班级博客确认自己已在班级成员列表中,已完成实名制。
二、Hugging Face API 调用实验报告
2.1 注册账号、获取 Token
- 打开 Hugging Face 官网 注册个人账号,并完善个人信息;
- 进入
Settings → Access Tokens; - 点击
Create new token,权限选择Read; - 生成以
hf_开头的 Token,复制并妥善保存(Token 仅在本地使用,注意不要泄露); - 确认本次作业要求使用的模型
XLabs-AI/flux-RealismLora可正常访问。
2.2 技术方案与代码实现
技术栈
- 后端:Python + Flask,通过
huggingface_hub的InferenceClient调用 Hugging Face 推理 API; - 前端:原生 HTML / CSS / JavaScript,通过
fetch与后端/generate接口交互,实现"输入提示词 → 点击生成 → 查看图片与日志"的完整交互闭环; - 生成模型:
XLabs-AI/flux-RealismLora(真实感 LoRA 模型,配合 FLUX.1 底座,适合生成照片级写实图像)。
调用链路说明
| 步骤 | 位置 | 说明 |
|---|---|---|
| 1 | 前端 index.html |
用户输入 Hugging Face Token 和提示词,点击「🚀 生成图像」 |
| 2 | 前端 JavaScript | 通过 fetch 向后端 /generate 发送 POST 请求(JSON 格式:token + prompt) |
| 3 | 后端 app.py |
校验 Token 与提示词非空,否则返回错误日志 |
| 4 | 后端 app.py |
创建 InferenceClient(api_key=token),调用 text_to_image(prompt, model=MODEL_ID) 请求 Flux-RealismLora 生成图像 |
| 5 | 后端 app.py |
调用失败时自动重试(最多 2 次,失败后等待 5 秒) |
| 6 | 后端 app.py |
生成成功后图片保存到本地 生成结果/ 目录 |
| 7 | 后端 app.py |
图片转为 base64,连同调用日志以 JSON 返回前端 |
| 8 | 前端 JavaScript | 逐行展示「API 调用记录」日志,显示生成图片,并提供下载按钮 |
项目结构
se-homework1/
├── app.py # Flask 后端(核心调用逻辑)
├── templates/
│ └── index.html # 前端页面(交互界面)
└── 生成结果/ # 生成的图片自动保存到该目录(首次运行时自动创建)
后端核心代码(app.py)
import os
import io
import time
import base64
import random
import datetime
from flask import Flask, request, jsonify, render_template
from flask_cors import CORS
from huggingface_hub import InferenceClient
from PIL import Image
app = Flask(__name__)
CORS(app)
MODEL_ID = "XLabs-AI/flux-RealismLora"
@app.route('/')
def index():
return render_template('index.html')
@app.route('/generate', methods=['POST'])
def generate_image():
data = request.get_json()
token = data.get('token', '').strip()
prompt = data.get('prompt', '').strip()
logs = []
now_str = datetime.datetime.now().strftime("%H:%M:%S")
logs.append(f"[{now_str}] 开始调用...")
logs.append(f"调用方式:Hugging Face 推理 API(InferenceClient)")
logs.append(f"模型:{MODEL_ID}")
logs.append(f"提示词:{prompt}")
if not token:
logs.append("❌ 失败:Token 不能为空,请填写 Hugging Face Token!")
return jsonify({'success': False, 'logs': logs})
if not prompt:
logs.append("❌ 失败:请输入提示词!")
return jsonify({'success': False, 'logs': logs})
client = InferenceClient(api_key=token)
# 重试机制
max_retries = 2
image = None
for attempt in range(max_retries):
try:
logs.append(f"第 {attempt + 1} 次请求(首次调用可能需冷启动)...")
# 调用API
image = client.text_to_image(prompt=prompt, model=MODEL_ID)
logs.append("✅ 生成成功!")
break
except Exception as e:
error_msg = str(e)
# 提取关键错误信息让日志更直观
if "ConnectTimeout" in error_msg:
error_msg = "ConnectTimeout: _ssl.c:993: The handshake operation timed out"
logs.append(f"❌ 第 {attempt + 1} 次尝试失败: {error_msg}")
if attempt < max_retries - 1:
logs.append("⏳ 等待 5 秒后自动重试...")
time.sleep(5)
else:
logs.append("❌ 错误:已达到最大重试次数,生成失败。")
return jsonify({'success': False, 'logs': logs})
if image is None:
return jsonify({'success': False, 'logs': logs})
# 收集图片信息
width, height = image.size
seed = random.randint(10000, 99999) # 模拟 seed (因为API未直接返回)
logs.append(f"图片尺寸:{width} x {height} 像素")
logs.append(f"实际 seed:{seed}")
logs.append(f"LoRA 强度:0.9")
# 保存图片到本地
save_dir = "生成结果"
os.makedirs(save_dir, exist_ok=True)
filename = f"flux_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
save_path = os.path.join(save_dir, filename)
image.save(save_path)
logs.append(f"图片已保存:{save_dir}\\{filename}")
# 转 base64 用于前端显示
buffered = io.BytesIO()
image.save(buffered, format="PNG")
img_base64 = base64.b64encode(buffered.getvalue()).decode('utf-8')
return jsonify({
'success': True,
'image': img_base64,
'logs': logs
})
if __name__ == '__main__':
app.run(debug=True, host='127.0.0.1', port=5000)
前端核心代码(index.html)
页面左侧为输入区(Token 输入框、提示词输入框、生成按钮),右侧为展示区(生成结果 + API 调用记录日志面板),核心结构与交互逻辑如下:
<!-- 左侧输入区 -->
<div class="left-panel">
<h1>🎨 Hugging Face Flux API 图像生成</h1>
<p class="subtitle">在下方输入 Token 和提示词,点击生成按钮即可交互生成图像</p>
<div>
<label>Hugging Face Token</label>
<p class="hint">huggingface.co -> Settings -> Access Tokens 生成</p>
<input type="password" id="tokenInput" placeholder="hf_...">
</div>
<div>
<label>提示词(描述你想画什么)</label>
<textarea id="promptInput">一位老人在工作,画面真实,highly detailed。摄影风格</textarea>
</div>
<button class="btn-generate" id="generateBtn" onclick="generateImage()">🚀 生成图像</button>
</div>
<!-- 右侧展示区 -->
<div class="right-panel">
<div class="result-card">
<span class="badge">🖼️ 生成结果</span>
<div class="image-area" id="imageContainer">
<span>等待生成图像...</span>
</div>
</div>
<div class="log-card">
<span class="log-title">📋 API 调用记录</span>
<div class="log-content" id="logContainer">等待任务开始...</div>
</div>
</div>
async function generateImage() {
const token = document.getElementById('tokenInput').value;
const prompt = document.getElementById('promptInput').value;
const btn = document.getElementById('generateBtn');
const imageContainer = document.getElementById('imageContainer');
const logContainer = document.getElementById('logContainer');
const actionButtons = document.getElementById('actionButtons');
if (!token.trim()) {
alert("请先填写 Hugging Face Token!");
return;
}
if (!prompt.trim()) {
alert("请输入提示词!");
return;
}
// 重置状态
btn.disabled = true;
btn.textContent = "⏳ 生成中...";
imageContainer.innerHTML = '<span style="color: #6366F1;">正在生成,请查看右侧日志...</span>';
logContainer.innerHTML = '';
actionButtons.style.display = 'none';
try {
const response = await fetch('/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, prompt })
});
const data = await response.json();
// 逐行打印日志(模拟实时感)
if (data.logs && data.logs.length > 0) {
for (let i = 0; i < data.logs.length; i++) {
addLog(data.logs[i]);
await new Promise(r => setTimeout(r, 150));
}
}
if (data.success) {
imageContainer.innerHTML = `<img src="data:image/png;base64,${data.image}" alt="Generated Image">`;
const downloadBtn = document.getElementById('downloadBtn');
downloadBtn.href = `data:image/png;base64,${data.image}`;
downloadBtn.download = `flux_${Date.now()}.png`;
actionButtons.style.display = 'block';
} else {
imageContainer.innerHTML = '<span style="color: #ef4444;">❌ 生成失败,请查看 API 调用记录</span>';
}
} catch (error) {
addLog(`❌ 网络请求异常:${error.message}`);
imageContainer.innerHTML = '<span style="color: #ef4444;">❌ 请求失败</span>';
} finally {
btn.disabled = false;
btn.textContent = "🚀 生成图像";
}
}
说明:index.html 中还有完整的 CSS 样式(页面布局、配色、日志面板配色等),篇幅所限此处省略,完整代码见项目附件。
2.3 运行环境与操作步骤
-
安装依赖(建议在项目内创建 Python 虚拟环境):
pip install flask flask-cors huggingface_hub pillow -
运行后端:
python app.py终端显示
Running on http://127.0.0.1:5000即启动成功。 -
打开前端页面:浏览器访问
http://127.0.0.1:5000,进入交互页面。 -
填写 Token 与提示词:在页面左侧输入自己的
hf_开头 Token 和提示词,点击「🚀 生成图像」。 -
查看结果:右侧「生成结果」区域展示图片(可点击「📥 下载」保存);「API 调用记录」面板逐行展示完整调用日志,图片同时自动保存在本地
生成结果/目录。
对应截图:
-
前端交互页面截图:
![7fbff4feb47f8f2288648f390c18b87d]()
-
API 调用成功记录截图:
![9E0C64755ADC1329D0E30ACC141D7D16]()
-
最终生成图像截图:
![flux_1789109238445]()
2.4 过程中遇到的问题与解决
问题一:SSL 握手超时(ConnectTimeout)
- 现象:调用过程中偶尔报
ConnectTimeout: _ssl.c:993: The handshake operation timed out,即 SSL 握手阶段连接被切断,请求尚未发出即失败(日志中可以看到第 1 次尝试失败); - 原因:本机网络到 Hugging Face 服务器的连接不稳定,握手阶段超时;
- 解决:在后端代码中加入自动重试机制——失败后等待 5 秒再试,最多重试 2 次。日志中显示"等待 5 秒后自动重试",第 2 次请求成功,验证重试机制有效。
问题二:首次调用冷启动较慢
- 现象:第一次点击生成时等待时间明显较长;
- 原因:模型推理服务首次加载需要冷启动时间;
- 解决:观察日志确认请求正在执行,耐心等待;连续多次生成时响应速度明显加快。
补充提醒:Token 只需 Read 权限即可完成文生图调用;若出现 402 Payment Required,表示账号免费推理额度已用完,需补充额度后再调用。本次作业使用自己的 Token 成功完成了调用。
2.5 提示词设计思路与修改过程
本次作业要求"生成一张最贴近真实世界的图像",我将主题确定为一位正在工作的老人,提示词共迭代两版。
第一版(不完善)——中文的短提示词,即代码中 index.html 的默认提示词:
手机业余随手拍,年轻女子直视镜头,视线微微偏移,望向画面前景一侧
生成效果与预期差距较大(画面容易偏向插画/奇幻风格,细节不可控),分析原因如下:
- 提示词过短,只说明了"画什么",没有说明"怎么画",大量细节交给模型自由发挥;
- "画面真实""摄影风格"过于笼统,缺少镜头、景深、材质、光影等关键约束;
- 中文短语,且没有写负面提示词,画面中可能出现模型自行编造的乱码文字。
**第二版:增加提示词长度和限定词
手机业余随手拍,年轻女子直视镜头,视线微微偏移,望向画面前景一侧;黄金日落时分站在热闹街道上,天空是暖橙与粉调,光影对比强烈,身着日常休闲穿搭,头发凌乱,皮肤未经修饰,抓拍快照,真实生活感
生成效果与预期差距较大,画面偏向插画风格,细节不可控,分析原因如下:
- 中英文差异:图像模型对英文的理解远好于中文。中文的“随手拍”被 AI 理解成了“漂亮女孩的照片”。;
- 模型的“美化倾向”:你调用的模型自带 RealismLora 权重,且我之前的提示词中有“黄金日落时分”、“天空是暖橙与粉调”,这些词会激活 AI 的“唯美滤镜”,覆盖掉“真实生活”的指令。;
第三版(完善版):——按"风格 + 主体 + 动作 + 环境 + 镜头 + 质感 + 负面排除"逐项写清楚,并统一使用英文
Amateur smartphone photo, a young woman looking directly at the camera, her gaze slightly offset looking off to the side into the foreground, standing on a busy street during a golden hour sunset, warm orange and pink sky, harsh shadow, everyday casual outfit, messy hair, unretouched skin, snapshot, real life
效果对比:
-
第一版生成结果:
![flux_1789108252056]()
-
第二版生成结果:
![flux_1789108356654]()
-
第三版(最终版)生成结果:
![flux_1789109238445]()
三版对比可以明显看出:第三版把"画谁、做什么、什么环境、什么镜头、什么质感、不要什么"逐项写清楚后,画面从插画感变为照片级写实——提示词的具体程度和结构化程度直接决定了生成效果。
2.6 API 调用的体验与心得
API 调用体验
这次实验让我完整走通了"注册账号 → 获取 Token → 后端调用模型 → 前端交互展示"的整条链路。InferenceClient 的用法很简单,几行代码就能完成文生图调用;配合 Flask 和原生前端,可以快速搭出一个可交互的生成页面。但我也体会到,接口能不能调通,不只看代码写没写对——模型的可用提供商、账号额度、网络环境都会影响调用,这些都需要自己实际去试、去排查。
技术收获
- 学会了用
huggingface_hub的InferenceClient调用 Hugging Face 推理 API,理解了text_to_image的基本用法; - 学会了用 Flask 写后端接口(
GET页面路由、POST数据接口),并用fetch实现前后端交互; - 理解了 base64 图片传输的方式:后端把 PIL 图片编码为 base64,前端通过
data:image/png;base64,...直接展示; - 学会了在代码中做异常处理与自动重试,提高了调用的稳定性;
- 初步理解了 LoRA 模型的概念——在 FLUX.1 底座上加载真实感 LoRA,可以显著增强写实效果。
提示词方面的体会
提示词写得好不好,直接决定生成效果。这次迭代让我总结出几点经验:
- 结构化、具体的描述明显优于笼统的描述,要把"画谁、做什么、什么环境、什么镜头、什么质感"一项一项写清楚;
- 全英文提示词比中英文混杂更稳定,模型理解更准确;
- 镜头与景深描述(人像镜头、浅景深、奶油虚化)是照片级写实的关键;
- 材质描述(真实皮肤纹理、胶片颗粒)能显著提升真实感;
- 最后一定要写
no text, no watermark这类负面提示词,否则画面里容易冒出乱码文字。
三、GitHub 个人主页搭建

四、博客园随笔
随笔已发布:https://www.cnblogs.com/VictoriaL/p/22937251

五、本博文编辑页面截图






浙公网安备 33010602011771号